Skip to content

Commit da7656f

Browse files
authored
Impl: update ssh config when settings change (#51)
- right now the ssh config is triggered only when at the login and only when new workspaces are created. In the last case, the ssh config updates only the new sections. - but the user can go into settings page and enable or disable the wildcard ssh config. With this patch after the user hits Save, the ssh re-configuration is triggered, without the need to restart Toolbox. Also a major fix - read only settings acted as snapshots in the sense that they did not reflect any subsequent update to the underlying store because updates create a new readonly instance - with this patch we simplified the code even more by exposing a readonly interface with all of the implementation in the CoderSettingsStore. PluginSettingsStore and Environments are the only persist-able stores. - because read only instances share the same settings store instances any update on the writable will reflect on the readable instances as well.
1 parent 09e15db commit da7656f

File tree

9 files changed

+387
-314
lines changed

9 files changed

+387
-314
lines changed

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

+4
Original file line numberDiff line numberDiff line change
@@ -204,4 +204,8 @@ class CoderRemoteEnvironment(
204204
* Companion to equals, for sets.
205205
*/
206206
override fun hashCode(): Int = id.hashCode()
207+
208+
override fun toString(): String {
209+
return "CoderRemoteEnvironment(name='$name')"
210+
}
207211
}

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

+29-14
Original file line numberDiff line numberDiff line change
@@ -20,13 +20,16 @@ import com.jetbrains.toolbox.api.remoteDev.RemoteProvider
2020
import com.jetbrains.toolbox.api.remoteDev.RemoteProviderEnvironment
2121
import com.jetbrains.toolbox.api.ui.actions.ActionDescription
2222
import com.jetbrains.toolbox.api.ui.components.UiPage
23+
import kotlinx.coroutines.ExperimentalCoroutinesApi
2324
import kotlinx.coroutines.Job
24-
import kotlinx.coroutines.delay
25+
import kotlinx.coroutines.channels.Channel
2526
import kotlinx.coroutines.flow.MutableStateFlow
2627
import kotlinx.coroutines.flow.StateFlow
2728
import kotlinx.coroutines.flow.update
2829
import kotlinx.coroutines.isActive
2930
import kotlinx.coroutines.launch
31+
import kotlinx.coroutines.selects.onTimeout
32+
import kotlinx.coroutines.selects.select
3033
import okhttp3.OkHttpClient
3134
import java.net.URI
3235
import java.net.URL
@@ -35,18 +38,20 @@ import kotlin.time.Duration.Companion.seconds
3538
import com.jetbrains.toolbox.api.ui.components.AccountDropdownField as DropDownMenu
3639
import com.jetbrains.toolbox.api.ui.components.AccountDropdownField as dropDownFactory
3740

41+
@OptIn(ExperimentalCoroutinesApi::class)
3842
class CoderRemoteProvider(
3943
private val context: CoderToolboxContext,
4044
private val httpClient: OkHttpClient,
4145
) : RemoteProvider("Coder") {
4246
// Current polling job.
4347
private var pollJob: Job? = null
44-
private var lastEnvironments: Set<CoderRemoteEnvironment>? = null
48+
private val lastEnvironments = mutableSetOf<CoderRemoteEnvironment>()
4549

46-
private val cSettings = context.settingsStore.readOnly()
50+
private val settings = context.settingsStore.readOnly()
4751

4852
// Create our services from the Toolbox ones.
49-
private val settingsPage: CoderSettingsPage = CoderSettingsPage(context)
53+
private val triggerSshConfig = Channel<Boolean>(Channel.CONFLATED)
54+
private val settingsPage: CoderSettingsPage = CoderSettingsPage(context, triggerSshConfig)
5055
private val dialogUi = DialogUi(context)
5156

5257
// The REST client, if we are signed in
@@ -92,7 +97,7 @@ class CoderRemoteProvider(
9297
}?.map { agent ->
9398
// If we have an environment already, update that.
9499
val env = CoderRemoteEnvironment(context, client, ws, agent)
95-
lastEnvironments?.firstOrNull { it == env }?.let {
100+
lastEnvironments.firstOrNull { it == env }?.let {
96101
it.update(ws, agent)
97102
it
98103
} ?: env
@@ -107,9 +112,7 @@ class CoderRemoteProvider(
107112

108113
// Reconfigure if a new environment is found.
109114
// TODO@JB: Should we use the add/remove listeners instead?
110-
val newEnvironments = lastEnvironments
111-
?.let { resolvedEnvironments.subtract(it) }
112-
?: resolvedEnvironments
115+
val newEnvironments = resolvedEnvironments.subtract(lastEnvironments)
113116
if (newEnvironments.isNotEmpty()) {
114117
context.logger.info("Found new environment(s), reconfiguring CLI: $newEnvironments")
115118
cli.configSsh(newEnvironments.map { it.name }.toSet())
@@ -124,8 +127,10 @@ class CoderRemoteProvider(
124127
true
125128
}
126129
}
127-
128-
lastEnvironments = resolvedEnvironments
130+
lastEnvironments.apply {
131+
clear()
132+
addAll(resolvedEnvironments)
133+
}
129134
} catch (_: CancellationException) {
130135
context.logger.debug("${client.url} polling loop canceled")
131136
break
@@ -136,7 +141,17 @@ class CoderRemoteProvider(
136141
break
137142
}
138143
// TODO: Listening on a web socket might be better?
139-
delay(5.seconds)
144+
select<Unit> {
145+
onTimeout(5.seconds) {
146+
context.logger.trace("workspace poller waked up by the 5 seconds timeout")
147+
}
148+
triggerSshConfig.onReceive { shouldTrigger ->
149+
if (shouldTrigger) {
150+
context.logger.trace("workspace poller waked up because it should reconfigure the ssh configurations")
151+
cli.configSsh(lastEnvironments.map { it.name }.toSet())
152+
}
153+
}
154+
}
140155
}
141156
}
142157

@@ -178,7 +193,7 @@ class CoderRemoteProvider(
178193
override fun close() {
179194
pollJob?.cancel()
180195
client?.close()
181-
lastEnvironments = null
196+
lastEnvironments.clear()
182197
environments.value = LoadableState.Value(emptyList())
183198
isInitialized.update { false }
184199
}
@@ -270,7 +285,7 @@ class CoderRemoteProvider(
270285
var autologinEx: Exception? = null
271286
context.secrets.lastToken.let { lastToken ->
272287
context.secrets.lastDeploymentURL.let { lastDeploymentURL ->
273-
if (autologin && lastDeploymentURL.isNotBlank() && (lastToken.isNotBlank() || !cSettings.requireTokenAuth)) {
288+
if (autologin && lastDeploymentURL.isNotBlank() && (lastToken.isNotBlank() || !settings.requireTokenAuth)) {
274289
try {
275290
return createConnectPage(URL(lastDeploymentURL), lastToken)
276291
} catch (ex: Exception) {
@@ -342,7 +357,7 @@ class CoderRemoteProvider(
342357
if (it.isNotBlank() && context.secrets.lastDeploymentURL == deploymentURL.toString()) {
343358
it to SettingSource.LAST_USED
344359
} else {
345-
cSettings.token(deploymentURL)
360+
settings.token(deploymentURL)
346361
}
347362
}
348363

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

+5-5
Original file line numberDiff line numberDiff line change
@@ -6,7 +6,7 @@ import com.coder.toolbox.cli.ex.ResponseException
66
import com.coder.toolbox.cli.ex.SSHConfigFormatException
77
import com.coder.toolbox.sdk.v2.models.Workspace
88
import com.coder.toolbox.sdk.v2.models.WorkspaceAgent
9-
import com.coder.toolbox.settings.CoderSettings
9+
import com.coder.toolbox.settings.ReadOnlyCoderSettings
1010
import com.coder.toolbox.util.CoderHostnameVerifier
1111
import com.coder.toolbox.util.InvalidVersionException
1212
import com.coder.toolbox.util.OS
@@ -125,7 +125,7 @@ class CoderCLIManager(
125125
private val deploymentURL: URL,
126126
private val logger: Logger,
127127
// Plugin configuration.
128-
private val settings: CoderSettings,
128+
private val settings: ReadOnlyCoderSettings,
129129
// If the binary directory is not writable, this can be used to force the
130130
// manager to download to the data directory instead.
131131
forceDownloadToData: Boolean = false,
@@ -267,21 +267,21 @@ class CoderCLIManager(
267267
"--url",
268268
escape(deploymentURL.toString()),
269269
if (!settings.headerCommand.isNullOrBlank()) "--header-command" else null,
270-
if (!settings.headerCommand.isNullOrBlank()) escapeSubcommand(settings.headerCommand) else null,
270+
if (!settings.headerCommand.isNullOrBlank()) escapeSubcommand(settings.headerCommand!!) else null,
271271
"ssh",
272272
"--stdio",
273273
if (settings.disableAutostart && feats.disableAutostart) "--disable-autostart" else null,
274274
)
275275
val proxyArgs = baseArgs + listOfNotNull(
276276
if (!settings.sshLogDirectory.isNullOrBlank()) "--log-dir" else null,
277-
if (!settings.sshLogDirectory.isNullOrBlank()) escape(settings.sshLogDirectory) else null,
277+
if (!settings.sshLogDirectory.isNullOrBlank()) escape(settings.sshLogDirectory!!) else null,
278278
if (feats.reportWorkspaceUsage) "--usage-app=jetbrains" else null,
279279
)
280280
val backgroundProxyArgs =
281281
baseArgs + listOfNotNull(if (feats.reportWorkspaceUsage) "--usage-app=disable" else null)
282282
val extraConfig =
283283
if (!settings.sshConfigOptions.isNullOrBlank()) {
284-
"\n" + settings.sshConfigOptions.prependIndent(" ")
284+
"\n" + settings.sshConfigOptions!!.prependIndent(" ")
285285
} else {
286286
""
287287
}

0 commit comments

Comments
 (0)