Skip to content

Commit b131c1c

Browse files
committed
Merge branch 'main' into fix-misleading-msg-during-manual-auth
2 parents 2c7824b + 9fdd99b commit b131c1c

File tree

4 files changed

+46
-13
lines changed

4 files changed

+46
-13
lines changed

CHANGELOG.md

+2-1
Original file line numberDiff line numberDiff line change
@@ -8,7 +8,8 @@
88

99
### Fixed
1010

11-
- misleading message saying that there are no workspaces rendered during manual authentication
11+
- rendering glitches when a Workspace is stopped while SSH connection is alive
12+
- misleading message saying that there are no workspaces rendered during manual authentication
1213

1314
## 0.2.0 - 2025-04-24
1415

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

+22-6
Original file line numberDiff line numberDiff line change
@@ -7,6 +7,7 @@ import com.coder.toolbox.sdk.CoderRestClient
77
import com.coder.toolbox.sdk.ex.APIResponseException
88
import com.coder.toolbox.sdk.v2.models.Workspace
99
import com.coder.toolbox.sdk.v2.models.WorkspaceAgent
10+
import com.coder.toolbox.util.waitForFalseWithTimeout
1011
import com.coder.toolbox.util.withPath
1112
import com.coder.toolbox.views.Action
1213
import com.coder.toolbox.views.EnvironmentView
@@ -43,6 +44,10 @@ class CoderRemoteEnvironment(
4344
private var wsRawStatus = WorkspaceAndAgentStatus.from(workspace, agent)
4445

4546
override var name: String = "${workspace.name}.${agent.name}"
47+
48+
private var isConnected: MutableStateFlow<Boolean> = MutableStateFlow(false)
49+
override val connectionRequest: MutableStateFlow<Boolean> = MutableStateFlow(false)
50+
4651
override val state: MutableStateFlow<RemoteEnvironmentState> =
4752
MutableStateFlow(wsRawStatus.toRemoteEnvironmentState(context))
4853
override val description: MutableStateFlow<EnvironmentDescription> =
@@ -106,6 +111,8 @@ class CoderRemoteEnvironment(
106111
} else {
107112
actions.add(Action(context.i18n.ptrl("Stop")) {
108113
context.cs.launch {
114+
tryStopSshConnection()
115+
109116
val build = client.stopWorkspace(workspace)
110117
update(workspace.copy(latestBuild = build), agent)
111118
}
@@ -115,18 +122,30 @@ class CoderRemoteEnvironment(
115122
return actions
116123
}
117124

125+
private suspend fun tryStopSshConnection() {
126+
if (isConnected.value) {
127+
connectionRequest.update {
128+
false
129+
}
130+
131+
if (isConnected.waitForFalseWithTimeout(10.seconds) == null) {
132+
context.logger.warn("The SSH connection to workspace $name could not be dropped in time, going to stop the workspace while the SSH connection is live")
133+
}
134+
}
135+
}
136+
118137
override fun getBeforeConnectionHooks(): List<BeforeConnectionHook> = listOf(this)
119138

120139
override fun getAfterDisconnectHooks(): List<AfterDisconnectHook> = listOf(this)
121140

122141
override fun beforeConnection() {
123142
context.logger.info("Connecting to $id...")
124-
this.isConnected = true
143+
isConnected.update { true }
125144
}
126145

127146
override fun afterDisconnect() {
128147
this.connectionRequest.update { false }
129-
this.isConnected = false
148+
isConnected.update { false }
130149
context.logger.info("Disconnected from $id")
131150
}
132151

@@ -161,17 +180,14 @@ class CoderRemoteEnvironment(
161180
agent
162181
)
163182

164-
private var isConnected = false
165-
override val connectionRequest: MutableStateFlow<Boolean> = MutableStateFlow(false)
166-
167183
/**
168184
* Does nothing. In theory, we could do something like start the workspace
169185
* when you click into the workspace, but you would still need to press
170186
* "connect" anyway before the content is populated so there does not seem
171187
* to be much value.
172188
*/
173189
override fun setVisible(visibilityState: EnvironmentVisibilityState) {
174-
if (wsRawStatus.ready() && visibilityState.contentsVisible == true && isConnected == false) {
190+
if (wsRawStatus.ready() && visibilityState.contentsVisible == true && isConnected.value == false) {
175191
context.cs.launch {
176192
connectionRequest.update {
177193
true

src/main/kotlin/com/coder/toolbox/util/CoderProtocolHandler.kt

-6
Original file line numberDiff line numberDiff line change
@@ -13,7 +13,6 @@ import com.jetbrains.toolbox.api.localization.LocalizableString
1313
import kotlinx.coroutines.TimeoutCancellationException
1414
import kotlinx.coroutines.delay
1515
import kotlinx.coroutines.flow.StateFlow
16-
import kotlinx.coroutines.flow.first
1716
import kotlinx.coroutines.launch
1817
import kotlinx.coroutines.time.withTimeout
1918
import java.net.HttpURLConnection
@@ -331,9 +330,4 @@ private fun CoderToolboxContext.popupPluginMainPage() {
331330
this.envPageManager.showPluginEnvironmentsPage(true)
332331
}
333332

334-
/**
335-
* Suspends the coroutine until first true value is received.
336-
*/
337-
suspend fun StateFlow<Boolean>.waitForTrue() = this.first { it }
338-
339333
class MissingArgumentException(message: String, ex: Throwable? = null) : IllegalArgumentException(message, ex)
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,22 @@
1+
package com.coder.toolbox.util
2+
3+
import kotlinx.coroutines.flow.StateFlow
4+
import kotlinx.coroutines.flow.first
5+
import kotlinx.coroutines.withTimeoutOrNull
6+
import kotlin.time.Duration
7+
8+
/**
9+
* Suspends the coroutine until first true value is received.
10+
*/
11+
suspend fun StateFlow<Boolean>.waitForTrue() = this.first { it }
12+
13+
/**
14+
* Suspends the coroutine until first false value is received.
15+
*/
16+
suspend fun StateFlow<Boolean>.waitForFalseWithTimeout(duration: Duration): Boolean? {
17+
if (!this.value) return false
18+
19+
return withTimeoutOrNull(duration) {
20+
this@waitForFalseWithTimeout.first { !it }
21+
}
22+
}

0 commit comments

Comments
 (0)