Skip to content

Commit 22f53f6

Browse files
authored
impl: improved debugging with named coroutines and additional logging (#185)
- this commit add descriptive names to launched coroutines for better stack traces. - extra logging lines during connection setup. Should be helpful in cases where the coroutines fail with exceptions
1 parent 9822876 commit 22f53f6

File tree

9 files changed

+169
-135
lines changed

9 files changed

+169
-135
lines changed

‎CHANGELOG.md‎

Lines changed: 4 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -2,6 +2,10 @@
22

33
## Unreleased
44

5+
### Added
6+
7+
- improved diagnose support
8+
59
## 0.6.3 - 2025-08-25
610

711
### Added

‎gradle.properties‎

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -1,3 +1,3 @@
1-
version=0.6.3
1+
version=0.6.4
22
group=com.coder.toolbox
33
name=coder-toolbox

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

Lines changed: 13 additions & 12 deletions
Original file line numberDiff line numberDiff line change
@@ -24,6 +24,7 @@ import com.jetbrains.toolbox.api.remoteDev.states.EnvironmentDescription
2424
importcom.jetbrains.toolbox.api.remoteDev.states.RemoteEnvironmentState
2525
importcom.jetbrains.toolbox.api.ui.actions.ActionDescription
2626
importcom.squareup.moshi.Moshi
27+
importkotlinx.coroutines.CoroutineName
2728
importkotlinx.coroutines.Job
2829
importkotlinx.coroutines.delay
2930
importkotlinx.coroutines.flow.MutableStateFlow
@@ -81,7 +82,7 @@ class CoderRemoteEnvironment(
8182
val actions = mutableListOf<Action>()
8283
if (wsRawStatus.canStop()){
8384
actions.add(Action(context.i18n.ptrl("Open web terminal")){
84-
context.cs.launch{
85+
context.cs.launch(CoroutineName("Open Web Terminal Action")){
8586
context.desktop.browse(client.url.withPath("/${workspace.ownerName}/$name/terminal").toString()){
8687
context.ui.showErrorInfoPopup(it)
8788
}
@@ -90,7 +91,7 @@ class CoderRemoteEnvironment(
9091
}
9192
actions.add(
9293
Action(context.i18n.ptrl("Open in dashboard")){
93-
context.cs.launch{
94+
context.cs.launch(CoroutineName("Open in Dashboard Action")){
9495
context.desktop.browse(
9596
client.url.withPath("/@${workspace.ownerName}/${workspace.name}").toString()
9697
){
@@ -100,7 +101,7 @@ class CoderRemoteEnvironment(
100101
})
101102

102103
actions.add(Action(context.i18n.ptrl("View template")){
103-
context.cs.launch{
104+
context.cs.launch(CoroutineName("View Template Action")){
104105
context.desktop.browse(client.url.withPath("/templates/${workspace.templateName}").toString()){
105106
context.ui.showErrorInfoPopup(it)
106107
}
@@ -110,14 +111,14 @@ class CoderRemoteEnvironment(
110111
if (wsRawStatus.canStart()){
111112
if (workspace.outdated){
112113
actions.add(Action(context.i18n.ptrl("Update and start")){
113-
context.cs.launch{
114+
context.cs.launch(CoroutineName("Update and Start Action")){
114115
val build = client.updateWorkspace(workspace)
115116
update(workspace.copy(latestBuild = build), agent)
116117
}
117118
})
118119
} else{
119120
actions.add(Action(context.i18n.ptrl("Start")){
120-
context.cs.launch{
121+
context.cs.launch(CoroutineName("Start Action")){
121122
val build = client.startWorkspace(workspace)
122123
update(workspace.copy(latestBuild = build), agent)
123124

@@ -128,14 +129,14 @@ class CoderRemoteEnvironment(
128129
if (wsRawStatus.canStop()){
129130
if (workspace.outdated){
130131
actions.add(Action(context.i18n.ptrl("Update and restart")){
131-
context.cs.launch{
132+
context.cs.launch(CoroutineName("Update and Restart Action")){
132133
val build = client.updateWorkspace(workspace)
133134
update(workspace.copy(latestBuild = build), agent)
134135
}
135136
})
136137
}
137138
actions.add(Action(context.i18n.ptrl("Stop")){
138-
context.cs.launch{
139+
context.cs.launch(CoroutineName("Stop Action")){
139140
tryStopSshConnection()
140141

141142
val build = client.stopWorkspace(workspace)
@@ -169,7 +170,7 @@ class CoderRemoteEnvironment(
169170
pollJob = pollNetworkMetrics()
170171
}
171172

172-
privatefunpollNetworkMetrics(): Job= context.cs.launch{
173+
privatefunpollNetworkMetrics(): Job= context.cs.launch(CoroutineName("Network Metrics Poller")){
173174
context.logger.info("Starting the network metrics poll job for $id")
174175
while (isActive){
175176
context.logger.debug("Searching SSH command's PID for workspace $id...")
@@ -227,7 +228,7 @@ class CoderRemoteEnvironment(
227228
actionsList.update{
228229
getAvailableActions()
229230
}
230-
context.cs.launch{
231+
context.cs.launch(CoroutineName("Workspace Status Updater")){
231232
state.update{
232233
wsRawStatus.toRemoteEnvironmentState(context)
233234
}
@@ -262,7 +263,7 @@ class CoderRemoteEnvironment(
262263
*/
263264
funstartSshConnection(): Boolean{
264265
if (wsRawStatus.ready() &&!isConnected.value){
265-
context.cs.launch{
266+
context.cs.launch(CoroutineName("SSH Connection Trigger")){
266267
connectionRequest.update{
267268
true
268269
}
@@ -284,7 +285,7 @@ class CoderRemoteEnvironment(
284285
}
285286

286287
overrideval deleteActionFlow:StateFlow<(() ->Unit)?>=MutableStateFlow{
287-
context.cs.launch{
288+
context.cs.launch(CoroutineName("Delete Workspace Action")){
288289
try{
289290
client.removeWorkspace(workspace)
290291
// mark the env as deleting otherwise we will have to
@@ -293,7 +294,7 @@ class CoderRemoteEnvironment(
293294
WorkspaceAndAgentStatus.DELETING.toRemoteEnvironmentState(context)
294295
}
295296

296-
context.cs.launch{
297+
context.cs.launch(CoroutineName("Workspace Deletion Poller")){
297298
withTimeout(5.minutes){
298299
var workspaceStillExists =true
299300
while (context.cs.isActive && workspaceStillExists){

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

Lines changed: 101 additions & 93 deletions
Original file line numberDiff line numberDiff line change
@@ -26,6 +26,7 @@ import com.jetbrains.toolbox.api.remoteDev.RemoteProvider
2626
importcom.jetbrains.toolbox.api.ui.actions.ActionDelimiter
2727
importcom.jetbrains.toolbox.api.ui.actions.ActionDescription
2828
importcom.jetbrains.toolbox.api.ui.components.UiPage
29+
importkotlinx.coroutines.CoroutineName
2930
importkotlinx.coroutines.ExperimentalCoroutinesApi
3031
importkotlinx.coroutines.Job
3132
importkotlinx.coroutines.channels.Channel
@@ -87,113 +88,114 @@ class CoderRemoteProvider(
8788
* workspace is added, reconfigure SSH using the provided cli (including the
8889
* first time).
8990
*/
90-
privatefunpoll(client:CoderRestClient, cli:CoderCLIManager): Job= context.cs.launch{
91-
var lastPollTime =TimeSource.Monotonic.markNow()
92-
while (isActive){
93-
try{
94-
context.logger.debug("Fetching workspace agents from ${client.url}")
95-
val resolvedEnvironments = client.workspaces().flatMap{ws ->
96-
// Agents are not included in workspaces that are off
97-
// so fetch them separately.
98-
when (ws.latestBuild.status){
99-
WorkspaceStatus.RUNNING-> ws.latestBuild.resources
100-
else-> emptyList()
101-
}.ifEmpty{
102-
client.resources(ws)
103-
}.flatMap{resource ->
104-
resource.agents?.distinctBy{
105-
// There can be duplicates with coder_agent_instance.
106-
// TODO: Can we just choose one or do they hold
107-
// different information?
108-
it.name
109-
}?.map{agent ->
110-
// If we have an environment already, update that.
111-
val env =CoderRemoteEnvironment(context, client, cli, ws, agent)
112-
lastEnvironments.firstOrNull{it == env }?.let{
113-
it.update(ws, agent)
114-
it
115-
} ?: env
116-
} ?: emptyList()
117-
}
118-
}.toSet()
91+
privatefunpoll(client:CoderRestClient, cli:CoderCLIManager): Job=
92+
context.cs.launch(CoroutineName("Workspace Poller")){
93+
var lastPollTime =TimeSource.Monotonic.markNow()
94+
while (isActive){
95+
try{
96+
context.logger.debug("Fetching workspace agents from ${client.url}")
97+
val resolvedEnvironments = client.workspaces().flatMap{ws ->
98+
// Agents are not included in workspaces that are off
99+
// so fetch them separately.
100+
when (ws.latestBuild.status){
101+
WorkspaceStatus.RUNNING-> ws.latestBuild.resources
102+
else-> emptyList()
103+
}.ifEmpty{
104+
client.resources(ws)
105+
}.flatMap{resource ->
106+
resource.agents?.distinctBy{
107+
// There can be duplicates with coder_agent_instance.
108+
// TODO: Can we just choose one or do they hold
109+
// different information?
110+
it.name
111+
}?.map{agent ->
112+
// If we have an environment already, update that.
113+
val env =CoderRemoteEnvironment(context, client, cli, ws, agent)
114+
lastEnvironments.firstOrNull{it == env }?.let{
115+
it.update(ws, agent)
116+
it
117+
} ?: env
118+
} ?: emptyList()
119+
}
120+
}.toSet()
119121

120-
// In case we logged out while running the query.
121-
if (!isActive){
122-
return@launch
123-
}
122+
// In case we logged out while running the query.
123+
if (!isActive){
124+
return@launch
125+
}
124126

125-
// Reconfigure if environments changed.
126-
if (lastEnvironments.size != resolvedEnvironments.size || lastEnvironments != resolvedEnvironments){
127-
context.logger.info("Workspaces have changed, reconfiguring CLI: $resolvedEnvironments")
128-
cli.configSsh(resolvedEnvironments.map{it.asPairOfWorkspaceAndAgent() }.toSet())
129-
}
127+
// Reconfigure if environments changed.
128+
if (lastEnvironments.size != resolvedEnvironments.size || lastEnvironments != resolvedEnvironments){
129+
context.logger.info("Workspaces have changed, reconfiguring CLI: $resolvedEnvironments")
130+
cli.configSsh(resolvedEnvironments.map{it.asPairOfWorkspaceAndAgent() }.toSet())
131+
}
130132

131-
environments.update{
132-
LoadableState.Value(resolvedEnvironments.toList())
133-
}
134-
if (!isInitialized.value){
135-
context.logger.info("Environments for ${client.url} are now initialized")
136-
isInitialized.update{
137-
true
133+
environments.update{
134+
LoadableState.Value(resolvedEnvironments.toList())
135+
}
136+
if (!isInitialized.value){
137+
context.logger.info("Environments for ${client.url} are now initialized")
138+
isInitialized.update{
139+
true
140+
}
141+
}
142+
lastEnvironments.apply{
143+
clear()
144+
addAll(resolvedEnvironments.sortedBy{it.id })
138145
}
139-
}
140-
lastEnvironments.apply{
141-
clear()
142-
addAll(resolvedEnvironments.sortedBy{it.id })
143-
}
144146

145-
if (WorkspaceConnectionManager.shouldEstablishWorkspaceConnections){
146-
WorkspaceConnectionManager.allConnected().forEach{wsId ->
147-
val env = lastEnvironments.firstOrNull(){it.id == wsId }
148-
if (env !=null&&!env.isConnected()){
149-
context.logger.info("Establishing lost SSH connection for workspace with id $wsId")
150-
if (!env.startSshConnection()){
151-
context.logger.info("Can't establish lost SSH connection for workspace with id $wsId")
147+
if (WorkspaceConnectionManager.shouldEstablishWorkspaceConnections){
148+
WorkspaceConnectionManager.allConnected().forEach{wsId ->
149+
val env = lastEnvironments.firstOrNull(){it.id == wsId }
150+
if (env !=null&&!env.isConnected()){
151+
context.logger.info("Establishing lost SSH connection for workspace with id $wsId")
152+
if (!env.startSshConnection()){
153+
context.logger.info("Can't establish lost SSH connection for workspace with id $wsId")
154+
}
152155
}
153156
}
157+
WorkspaceConnectionManager.reset()
154158
}
155-
WorkspaceConnectionManager.reset()
156-
}
157159

158-
WorkspaceConnectionManager.collectStatuses(lastEnvironments)
159-
} catch (_:CancellationException){
160-
context.logger.debug("${client.url} polling loop canceled")
161-
break
162-
} catch (ex:Exception){
163-
val elapsed = lastPollTime.elapsedNow()
164-
if (elapsed >POLL_INTERVAL*2){
165-
context.logger.info("wake-up from an OS sleep was detected")
166-
} else{
167-
context.logger.error(ex, "workspace polling error encountered")
168-
if (ex isAPIResponseException&& ex.isTokenExpired){
169-
WorkspaceConnectionManager.shouldEstablishWorkspaceConnections =true
170-
close()
171-
context.envPageManager.showPluginEnvironmentsPage()
172-
errorBuffer.add(ex)
173-
break
160+
WorkspaceConnectionManager.collectStatuses(lastEnvironments)
161+
} catch (_:CancellationException){
162+
context.logger.debug("${client.url} polling loop canceled")
163+
break
164+
} catch (ex:Exception){
165+
val elapsed = lastPollTime.elapsedNow()
166+
if (elapsed >POLL_INTERVAL*2){
167+
context.logger.info("wake-up from an OS sleep was detected")
168+
} else{
169+
context.logger.error(ex, "workspace polling error encountered")
170+
if (ex isAPIResponseException&& ex.isTokenExpired){
171+
WorkspaceConnectionManager.shouldEstablishWorkspaceConnections =true
172+
close()
173+
context.envPageManager.showPluginEnvironmentsPage()
174+
errorBuffer.add(ex)
175+
break
176+
}
174177
}
175178
}
176-
}
177179

178-
select{
179-
onTimeout(POLL_INTERVAL){
180-
context.logger.debug("workspace poller waked up by the $POLL_INTERVAL timeout")
181-
}
182-
triggerSshConfig.onReceive{shouldTrigger ->
183-
if (shouldTrigger){
184-
context.logger.debug("workspace poller waked up because it should reconfigure the ssh configurations")
185-
cli.configSsh(lastEnvironments.map{it.asPairOfWorkspaceAndAgent() }.toSet())
180+
select{
181+
onTimeout(POLL_INTERVAL){
182+
context.logger.debug("workspace poller waked up by the $POLL_INTERVAL timeout")
186183
}
187-
}
188-
triggerProviderVisible.onReceive{isCoderProviderVisible ->
189-
if (isCoderProviderVisible){
190-
context.logger.debug("workspace poller waked up by Coder Toolbox which is currently visible, fetching latest workspace statuses")
184+
triggerSshConfig.onReceive{shouldTrigger ->
185+
if (shouldTrigger){
186+
context.logger.debug("workspace poller waked up because it should reconfigure the ssh configurations")
187+
cli.configSsh(lastEnvironments.map{it.asPairOfWorkspaceAndAgent() }.toSet())
188+
}
189+
}
190+
triggerProviderVisible.onReceive{isCoderProviderVisible ->
191+
if (isCoderProviderVisible){
192+
context.logger.debug("workspace poller waked up by Coder Toolbox which is currently visible, fetching latest workspace statuses")
193+
}
191194
}
192195
}
196+
lastPollTime =TimeSource.Monotonic.markNow()
193197
}
194-
lastPollTime =TimeSource.Monotonic.markNow()
195198
}
196-
}
197199

198200
/**
199201
* Stop polling, clear the client and environments, then go back to the
@@ -221,7 +223,7 @@ class CoderRemoteProvider(
221223
overrideval additionalPluginActions:StateFlow<List<ActionDescription>> =MutableStateFlow(
222224
listOf(
223225
Action(context.i18n.ptrl("Create workspace")){
224-
context.cs.launch{
226+
context.cs.launch(CoroutineName("Create Workspace Action")){
225227
context.desktop.browse(client?.url?.withPath("/templates").toString()){
226228
context.ui.showErrorInfoPopup(it)
227229
}
@@ -299,7 +301,7 @@ class CoderRemoteProvider(
299301
visibility
300302
}
301303
if (visibility.providerVisible){
302-
context.cs.launch{
304+
context.cs.launch(CoroutineName("Notify Plugin Visibility")){
303305
triggerProviderVisible.send(true)
304306
}
305307
}
@@ -396,11 +398,17 @@ class CoderRemoteProvider(
396398
context.secrets.lastDeploymentURL = client.url.toString()
397399
context.secrets.lastToken = client.token ?:""
398400
context.secrets.storeTokenFor(client.url, context.secrets.lastToken)
401+
context.logger.info("Deployment URL and token were stored and will be available for automatic connection")
399402
this.client = client
400-
pollJob?.cancel()
403+
pollJob?.let{
404+
it.cancel()
405+
context.logger.info("Workspace poll job with reference ${pollJob} was canceled")
406+
}
401407
environments.showLoadingMessage()
402408
coderHeaderPage.setTitle(context.i18n.pnotr(client.url.toString()))
409+
context.logger.info("Displaying ${client.url} in the UI")
403410
pollJob = poll(client, cli)
411+
context.logger.info("Workspace poll job created with reference $pollJob")
404412
context.envPageManager.showPluginEnvironmentsPage()
405413
}
406414

‎src/main/kotlin/com/coder/toolbox/cli/CoderCLIManager.kt‎

Lines changed: 1 addition & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -315,6 +315,7 @@ class CoderCLIManager(
315315
){
316316
context.logger.info("Configuring SSH config at ${context.settingsStore.sshConfigPath}")
317317
writeSSHConfig(modifySSHConfig(readSSHConfig(), wsWithAgents, feats))
318+
context.logger.info("Finished configuring SSH config")
318319
}
319320

320321
/**

0 commit comments

Comments
(0)