Skip to content

Commit 3867a15

Browse files
authored
fix: report errors while running actions (#193)
JetBrains team reported in the past a couple of errors in the log, one of them being `A workspace build is already active`. The issue can be reproduced if the user hits the `Stop` action for example quite quick. It takes maybe one or two seconds to make rest api request, then for the backend to enqueue the build and change the workspace action. If we hit the action buttons really fast then this error could be reproduced. One approach I tried was to disable the action buttons in the context menu for the duration the request is executed. But for some reason the "enabled" property is not working in context menu, only when the actions are rendered on a UI "page". Instead, I decided to refactor the existing code and (also) visually report the errors in the UI screen to make the user aware in some cases that a job is already running on the backend. Another error reported by JetBrains is a `RejectedExecutionException` in the rest api client, and from the stack trace it seems the thread pool in the rest client was at some point shutdown. I think it is some sort of race condition, some thread calling shutting down the rest api client while the UI thread still executes polling and user's action. I tried to reproduce the issue with no success, and so I'm improving the logging around plugin de-initialization in the hope that next time the sequence of events is more helpful.
1 parent 7a49167 commit 3867a15

File tree

6 files changed

+80
-66
lines changed

6 files changed

+80
-66
lines changed

CHANGELOG.md

Lines changed: 1 addition & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -5,6 +5,7 @@
55
### Fixed
66

77
- token is no longer required when authentication is done via certificates
8+
- errors while running actions are now reported
89

910
## 0.6.4 - 2025-09-03
1011

src/main/kotlin/com/coder/toolbox/CoderRemoteEnvironment.kt

Lines changed: 38 additions & 45 deletions
Original file line numberDiff line numberDiff line change
@@ -81,68 +81,61 @@ class CoderRemoteEnvironment(
8181
private fun getAvailableActions(): List<ActionDescription> {
8282
val actions = mutableListOf<Action>()
8383
if (wsRawStatus.canStop()) {
84-
actions.add(Action(context.i18n.ptrl("Open web terminal")) {
85-
context.cs.launch(CoroutineName("Open Web Terminal Action")) {
86-
context.desktop.browse(client.url.withPath("/${workspace.ownerName}/$name/terminal").toString()) {
87-
context.ui.showErrorInfoPopup(it)
88-
}
84+
actions.add(Action(context, "Open web terminal") {
85+
context.desktop.browse(client.url.withPath("/${workspace.ownerName}/$name/terminal").toString()) {
86+
context.ui.showErrorInfoPopup(it)
8987
}
90-
})
88+
}
89+
)
9190
}
9291
actions.add(
93-
Action(context.i18n.ptrl("Open in dashboard")) {
94-
context.cs.launch(CoroutineName("Open in Dashboard Action")) {
95-
context.desktop.browse(
96-
client.url.withPath("/@${workspace.ownerName}/${workspace.name}").toString()
97-
) {
98-
context.ui.showErrorInfoPopup(it)
99-
}
100-
}
101-
})
102-
103-
actions.add(Action(context.i18n.ptrl("View template")) {
104-
context.cs.launch(CoroutineName("View Template Action")) {
105-
context.desktop.browse(client.url.withPath("/templates/${workspace.templateName}").toString()) {
92+
Action(context, "Open in dashboard") {
93+
context.desktop.browse(
94+
client.url.withPath("/@${workspace.ownerName}/${workspace.name}").toString()
95+
) {
10696
context.ui.showErrorInfoPopup(it)
10797
}
10898
}
109-
})
99+
)
100+
101+
actions.add(Action(context, "View template") {
102+
context.desktop.browse(client.url.withPath("/templates/${workspace.templateName}").toString()) {
103+
context.ui.showErrorInfoPopup(it)
104+
}
105+
}
106+
)
110107

111108
if (wsRawStatus.canStart()) {
112109
if (workspace.outdated) {
113-
actions.add(Action(context.i18n.ptrl("Update and start")) {
114-
context.cs.launch(CoroutineName("Update and Start Action")) {
115-
val build = client.updateWorkspace(workspace)
116-
update(workspace.copy(latestBuild = build), agent)
117-
}
118-
})
110+
actions.add(Action(context, "Update and start") {
111+
val build = client.updateWorkspace(workspace)
112+
update(workspace.copy(latestBuild = build), agent)
113+
}
114+
)
119115
} else {
120-
actions.add(Action(context.i18n.ptrl("Start")) {
121-
context.cs.launch(CoroutineName("Start Action")) {
122-
val build = client.startWorkspace(workspace)
123-
update(workspace.copy(latestBuild = build), agent)
116+
actions.add(Action(context, "Start") {
117+
val build = client.startWorkspace(workspace)
118+
update(workspace.copy(latestBuild = build), agent)
124119

125-
}
126-
})
120+
}
121+
)
127122
}
128123
}
129124
if (wsRawStatus.canStop()) {
130125
if (workspace.outdated) {
131-
actions.add(Action(context.i18n.ptrl("Update and restart")) {
132-
context.cs.launch(CoroutineName("Update and Restart Action")) {
133-
val build = client.updateWorkspace(workspace)
134-
update(workspace.copy(latestBuild = build), agent)
135-
}
136-
})
137-
}
138-
actions.add(Action(context.i18n.ptrl("Stop")) {
139-
context.cs.launch(CoroutineName("Stop Action")) {
140-
tryStopSshConnection()
141-
142-
val build = client.stopWorkspace(workspace)
126+
actions.add(Action(context, "Update and restart") {
127+
val build = client.updateWorkspace(workspace)
143128
update(workspace.copy(latestBuild = build), agent)
144129
}
145-
})
130+
)
131+
}
132+
actions.add(Action(context, "Stop") {
133+
tryStopSshConnection()
134+
135+
val build = client.stopWorkspace(workspace)
136+
update(workspace.copy(latestBuild = build), agent)
137+
}
138+
)
146139
}
147140
return actions
148141
}

src/main/kotlin/com/coder/toolbox/CoderRemoteProvider.kt

Lines changed: 14 additions & 10 deletions
Original file line numberDiff line numberDiff line change
@@ -202,8 +202,10 @@ class CoderRemoteProvider(
202202
* first page.
203203
*/
204204
private fun logout() {
205+
context.logger.info("Logging out ${client?.me?.username}...")
205206
WorkspaceConnectionManager.reset()
206207
close()
208+
context.logger.info("User ${client?.me?.username} logged out successfully")
207209
}
208210

209211
/**
@@ -222,15 +224,13 @@ class CoderRemoteProvider(
222224

223225
override val additionalPluginActions: StateFlow<List<ActionDescription>> = MutableStateFlow(
224226
listOf(
225-
Action(context.i18n.ptrl("Create workspace")) {
226-
context.cs.launch(CoroutineName("Create Workspace Action")) {
227-
context.desktop.browse(client?.url?.withPath("/templates").toString()) {
228-
context.ui.showErrorInfoPopup(it)
229-
}
227+
Action(context, "Create workspace") {
228+
context.desktop.browse(client?.url?.withPath("/templates").toString()) {
229+
context.ui.showErrorInfoPopup(it)
230230
}
231231
},
232232
CoderDelimiter(context.i18n.pnotr("")),
233-
Action(context.i18n.ptrl("Settings")) {
233+
Action(context, "Settings") {
234234
context.ui.showUiPage(settingsPage)
235235
},
236236
)
@@ -246,12 +246,16 @@ class CoderRemoteProvider(
246246
it.cancel()
247247
context.logger.info("Cancelled workspace poll job ${pollJob.toString()}")
248248
}
249-
client?.close()
249+
client?.let {
250+
it.close()
251+
context.logger.info("REST API client closed and resources released")
252+
}
253+
client = null
250254
lastEnvironments.clear()
251255
environments.value = LoadableState.Value(emptyList())
252256
isInitialized.update { false }
253-
client = null
254257
CoderCliSetupWizardState.goToFirstStep()
258+
context.logger.info("Coder plugin is now closed")
255259
}
256260

257261
override val svgIcon: SvgIcon =
@@ -319,12 +323,12 @@ class CoderRemoteProvider(
319323
uri,
320324
shouldDoAutoSetup()
321325
) { restClient, cli ->
322-
// stop polling and de-initialize resources
326+
context.logger.info("Stopping workspace polling and de-initializing resources")
323327
close()
324328
isInitialized.update {
325329
false
326330
}
327-
// start initialization with the new settings
331+
context.logger.info("Starting initialization with the new settings")
328332
this@CoderRemoteProvider.client = restClient
329333
coderHeaderPage.setTitle(context.i18n.pnotr(restClient.url.toString()))
330334

src/main/kotlin/com/coder/toolbox/views/CoderCliSetupWizardPage.kt

Lines changed: 6 additions & 6 deletions
Original file line numberDiff line numberDiff line change
@@ -24,9 +24,9 @@ class CoderCliSetupWizardPage(
2424
) -> Unit,
2525
) : CoderPage(MutableStateFlow(context.i18n.ptrl("Setting up Coder")), false) {
2626
private val shouldAutoSetup = MutableStateFlow(initialAutoSetup)
27-
private val settingsAction = Action(context.i18n.ptrl("Settings"), actionBlock = {
27+
private val settingsAction = Action(context, "Settings") {
2828
context.ui.showUiPage(settingsPage)
29-
})
29+
}
3030

3131
private val deploymentUrlStep = DeploymentUrlStep(context, visibilityState)
3232
private val tokenStep = TokenStep(context)
@@ -60,7 +60,7 @@ class CoderCliSetupWizardPage(
6060
}
6161
actionButtons.update {
6262
listOf(
63-
Action(context.i18n.ptrl("Next"), closesPage = false, actionBlock = {
63+
Action(context, "Next", closesPage = false, actionBlock = {
6464
if (deploymentUrlStep.onNext()) {
6565
displaySteps()
6666
}
@@ -77,13 +77,13 @@ class CoderCliSetupWizardPage(
7777
}
7878
actionButtons.update {
7979
listOf(
80-
Action(context.i18n.ptrl("Connect"), closesPage = false, actionBlock = {
80+
Action(context, "Connect", closesPage = false, actionBlock = {
8181
if (tokenStep.onNext()) {
8282
displaySteps()
8383
}
8484
}),
8585
settingsAction,
86-
Action(context.i18n.ptrl("Back"), closesPage = false, actionBlock = {
86+
Action(context, "Back", closesPage = false, actionBlock = {
8787
tokenStep.onBack()
8888
displaySteps()
8989
})
@@ -99,7 +99,7 @@ class CoderCliSetupWizardPage(
9999
actionButtons.update {
100100
listOf(
101101
settingsAction,
102-
Action(context.i18n.ptrl("Back"), closesPage = false, actionBlock = {
102+
Action(context, "Back", closesPage = false, actionBlock = {
103103
connectStep.onBack()
104104
shouldAutoSetup.update {
105105
false

src/main/kotlin/com/coder/toolbox/views/CoderPage.kt

Lines changed: 20 additions & 4 deletions
Original file line numberDiff line numberDiff line change
@@ -1,12 +1,16 @@
11
package com.coder.toolbox.views
22

3+
import com.coder.toolbox.CoderToolboxContext
4+
import com.coder.toolbox.sdk.ex.APIResponseException
35
import com.jetbrains.toolbox.api.core.ui.icons.SvgIcon
46
import com.jetbrains.toolbox.api.core.ui.icons.SvgIcon.IconType
57
import com.jetbrains.toolbox.api.localization.LocalizableString
68
import com.jetbrains.toolbox.api.ui.actions.RunnableActionDescription
79
import com.jetbrains.toolbox.api.ui.components.UiPage
10+
import kotlinx.coroutines.CoroutineName
811
import kotlinx.coroutines.flow.MutableStateFlow
912
import kotlinx.coroutines.flow.update
13+
import kotlinx.coroutines.launch
1014

1115
/**
1216
* Base page that handles the icon, displaying error notifications, and
@@ -48,15 +52,27 @@ abstract class CoderPage(
4852
* An action that simply runs the provided callback.
4953
*/
5054
class Action(
51-
description: LocalizableString,
55+
private val context: CoderToolboxContext,
56+
private val description: String,
5257
closesPage: Boolean = false,
5358
enabled: () -> Boolean = { true },
54-
private val actionBlock: () -> Unit,
59+
private val actionBlock: suspend () -> Unit,
5560
) : RunnableActionDescription {
56-
override val label: LocalizableString = description
61+
override val label: LocalizableString = context.i18n.ptrl(description)
5762
override val shouldClosePage: Boolean = closesPage
5863
override val isEnabled: Boolean = enabled()
5964
override fun run() {
60-
actionBlock()
65+
context.cs.launch(CoroutineName("$description Action")) {
66+
try {
67+
actionBlock()
68+
} catch (ex: Exception) {
69+
val textError = if (ex is APIResponseException) {
70+
if (!ex.reason.isNullOrBlank()) {
71+
ex.reason
72+
} else ex.message
73+
} else ex.message
74+
context.logAndShowError("Error while running `$description`", textError ?: "", ex)
75+
}
76+
}
6177
}
6278
}

src/main/kotlin/com/coder/toolbox/views/CoderSettingsPage.kt

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -116,7 +116,7 @@ class CoderSettingsPage(private val context: CoderToolboxContext, triggerSshConf
116116

117117
override val actionButtons: StateFlow<List<RunnableActionDescription>> = MutableStateFlow(
118118
listOf(
119-
Action(context.i18n.ptrl("Save"), closesPage = true) {
119+
Action(context, "Save", closesPage = true) {
120120
context.settingsStore.updateBinarySource(binarySourceField.contentState.value)
121121
context.settingsStore.updateBinaryDirectory(binaryDirectoryField.contentState.value)
122122
context.settingsStore.updateDataDirectory(dataDirectoryField.contentState.value)

0 commit comments

Comments
 (0)