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
import com.jetbrains.toolbox.api.remoteDev.states.RemoteEnvironmentState
2525
import com.jetbrains.toolbox.api.ui.actions.ActionDescription
2626
import com.squareup.moshi.Moshi
27+
import kotlinx.coroutines.CoroutineName
2728
import kotlinx.coroutines.Job
2829
import kotlinx.coroutines.delay
2930
import kotlinx.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-
private fun pollNetworkMetrics(): Job = context.cs.launch {
173+
private fun pollNetworkMetrics(): 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
fun startSshConnection(): 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
override val 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
import com.jetbrains.toolbox.api.ui.actions.ActionDelimiter
2727
import com.jetbrains.toolbox.api.ui.actions.ActionDescription
2828
import com.jetbrains.toolbox.api.ui.components.UiPage
29+
import kotlinx.coroutines.CoroutineName
2930
import kotlinx.coroutines.ExperimentalCoroutinesApi
3031
import kotlinx.coroutines.Job
3132
import kotlinx.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-
private fun poll(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+
private fun poll(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 is APIResponseException && 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 is APIResponseException && 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
override val 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)