Skip to content

Commit 23ebb16

Browse files
authored
Add the ability to disable autostart (#366)
On macOS this is checked by default for 2.5.0 and above.
1 parent 0a0fb52 commit 23ebb16

File tree

10 files changed

+146
-33
lines changed

10 files changed

+146
-33
lines changed

CHANGELOG.md

Lines changed: 6 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -4,6 +4,12 @@
44

55
## Unreleased
66

7+
### Changed
8+
9+
- Disable autostarting workspaces by default on macOS to prevent an issue where
10+
it wakes periodically and keeps the workspace on. This can be toggled via the
11+
"Disable autostart" setting.
12+
713
## 2.9.3 - 2024-02-10
814

915
### Fixed

src/main/kotlin/com/coder/gateway/CoderSettingsConfigurable.kt

Lines changed: 8 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -1,8 +1,8 @@
11
package com.coder.gateway
22

33
import com.coder.gateway.services.CoderSettingsService
4-
import com.coder.gateway.util.canCreateDirectory
54
import com.coder.gateway.services.CoderSettingsState
5+
import com.coder.gateway.util.canCreateDirectory
66
import com.intellij.openapi.components.service
77
import com.intellij.openapi.options.BoundConfigurable
88
import com.intellij.openapi.ui.DialogPanel
@@ -102,6 +102,13 @@ class CoderSettingsConfigurable : BoundConfigurable("Coder") {
102102
CoderGatewayBundle.message("gateway.connector.settings.tls-alt-name.comment")
103103
)
104104
}.layout(RowLayout.PARENT_GRID)
105+
row(CoderGatewayBundle.message("gateway.connector.settings.disable-autostart.heading")) {
106+
checkBox(CoderGatewayBundle.message("gateway.connector.settings.disable-autostart.title"))
107+
.bindSelected(state::disableAutostart)
108+
.comment(
109+
CoderGatewayBundle.message("gateway.connector.settings.disable-autostart.comment")
110+
)
111+
}.layout(RowLayout.PARENT_GRID)
105112
}
106113
}
107114

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

Lines changed: 43 additions & 13 deletions
Original file line numberDiff line numberDiff line change
@@ -96,6 +96,13 @@ fun ensureCLI(
9696
return if (cliMatches == null && dataCLIMatches != null) dataCLI else cli
9797
}
9898

99+
/**
100+
* The supported features of the CLI.
101+
*/
102+
data class Features (
103+
val disableAutostart: Boolean = false,
104+
)
105+
99106
/**
100107
* Manage the CLI for a single deployment.
101108
*/
@@ -199,9 +206,11 @@ class CoderCLIManager(
199206

200207
/**
201208
* Configure SSH to use this binary.
209+
*
210+
* This can take supported features for testing purposes only.
202211
*/
203-
fun configSsh(workspaceNames: List<String>) {
204-
writeSSHConfig(modifySSHConfig(readSSHConfig(), workspaceNames))
212+
fun configSsh(workspaceNames: List<String>, feats: Features = features) {
213+
writeSSHConfig(modifySSHConfig(readSSHConfig(), workspaceNames, feats))
205214
}
206215

207216
/**
@@ -219,8 +228,11 @@ class CoderCLIManager(
219228
* Given an existing SSH config modify it to add or remove the config for
220229
* this deployment and return the modified config or null if it does not
221230
* need to be modified.
231+
*
232+
* If features are not provided, calculate them based on the binary
233+
* version.
222234
*/
223-
private fun modifySSHConfig(contents: String?, workspaceNames: List<String>): String? {
235+
private fun modifySSHConfig(contents: String?, workspaceNames: List<String>, feats: Features): String? {
224236
val host = deploymentURL.safeHost()
225237
val startBlock = "# --- START CODER JETBRAINS $host"
226238
val endBlock = "# --- END CODER JETBRAINS $host"
@@ -230,15 +242,16 @@ class CoderCLIManager(
230242
"--global-config", escape(coderConfigPath.toString()),
231243
if (settings.headerCommand.isNotBlank()) "--header-command" else null,
232244
if (settings.headerCommand.isNotBlank()) escapeSubcommand(settings.headerCommand) else null,
233-
"ssh", "--stdio")
245+
"ssh", "--stdio",
246+
if (settings.disableAutostart && feats.disableAutostart) "--disable-autostart" else null)
234247
val blockContent = workspaceNames.joinToString(
235248
System.lineSeparator(),
236249
startBlock + System.lineSeparator(),
237250
System.lineSeparator() + endBlock,
238251
transform = {
239252
"""
240253
Host ${getHostName(deploymentURL, it)}
241-
ProxyCommand ${proxyArgs.joinToString(" ")} ${it}
254+
ProxyCommand ${proxyArgs.joinToString(" ")} $it
242255
ConnectTimeout 0
243256
StrictHostKeyChecking no
244257
UserKnownHostsFile /dev/null
@@ -333,31 +346,36 @@ class CoderCLIManager(
333346
}
334347

335348
/**
336-
* Returns true if the CLI has the same major/minor/patch version as the
337-
* provided version, false if it does not match, or null if the CLI version
338-
* could not be determined because the binary could not be executed or the
339-
* version could not be parsed.
349+
* Like version(), but logs errors instead of throwing them.
340350
*/
341-
fun matchesVersion(rawBuildVersion: String): Boolean? {
342-
val cliVersion = try {
351+
private fun tryVersion(): SemVer? {
352+
return try {
343353
version()
344354
} catch (e: Exception) {
345355
when (e) {
346356
is JsonSyntaxException,
347357
is InvalidVersionException -> {
348358
logger.info("Got invalid version from $localBinaryPath: ${e.message}")
349-
return null
350359
}
351360
else -> {
352361
// An error here most likely means the CLI does not exist or
353362
// it executed successfully but output no version which
354363
// suggests it is not the right binary.
355364
logger.info("Unable to determine $localBinaryPath version: ${e.message}")
356-
return null
357365
}
358366
}
367+
null
359368
}
369+
}
360370

371+
/**
372+
* Returns true if the CLI has the same major/minor/patch version as the
373+
* provided version, false if it does not match, or null if the CLI version
374+
* could not be determined because the binary could not be executed or the
375+
* version could not be parsed.
376+
*/
377+
fun matchesVersion(rawBuildVersion: String): Boolean? {
378+
val cliVersion = tryVersion() ?: return null
361379
val buildVersion = try {
362380
SemVer.parse(rawBuildVersion)
363381
} catch (e: InvalidVersionException) {
@@ -383,6 +401,18 @@ class CoderCLIManager(
383401
return stdout
384402
}
385403

404+
val features: Features
405+
get() {
406+
val version = tryVersion()
407+
return if (version == null) {
408+
Features()
409+
} else {
410+
Features(
411+
// Autostart with SSH was added in 2.5.0.
412+
disableAutostart = version >= SemVer(2, 5, 0))
413+
}
414+
}
415+
386416
companion object {
387417
val logger = Logger.getInstance(CoderCLIManager::class.java.simpleName)
388418

src/main/kotlin/com/coder/gateway/services/CoderSettingsState.kt

Lines changed: 6 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -1,5 +1,7 @@
11
package com.coder.gateway.services
22

3+
import com.coder.gateway.util.OS
4+
import com.coder.gateway.util.getOS
35
import com.intellij.openapi.components.PersistentStateComponent
46
import com.intellij.openapi.components.RoamingType
57
import com.intellij.openapi.components.Service
@@ -56,6 +58,10 @@ class CoderSettingsState(
5658
// connections. This is useful when the hostname used to connect to the
5759
// Coder service does not match the hostname in the TLS certificate.
5860
var tlsAlternateHostname: String = "",
61+
// Whether to add --disable-autostart to the proxy command. This works
62+
// around issues on macOS where it periodically wakes and Gateway
63+
// reconnects, keeping the workspace constantly up.
64+
var disableAutostart: Boolean = getOS() == OS.MAC,
5965
) : PersistentStateComponent<CoderSettingsState> {
6066
override fun getState(): CoderSettingsState {
6167
return this

src/main/kotlin/com/coder/gateway/settings/CoderSettings.kt

Lines changed: 3 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -35,6 +35,9 @@ open class CoderSettings(
3535
val headerCommand: String
3636
get() = state.headerCommand
3737

38+
val disableAutostart: Boolean
39+
get() = state.disableAutostart
40+
3841
/**
3942
* Where the specified deployment should put its data.
4043
*/

src/main/resources/messages/CoderGatewayBundle.properties

Lines changed: 6 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -111,3 +111,9 @@ gateway.connector.settings.tls-alt-name.comment=Optionally set this to \
111111
an alternate hostname used for verifying TLS connections. This is useful \
112112
when the hostname used to connect to the Coder service does not match the \
113113
hostname in the TLS certificate.
114+
gateway.connector.settings.disable-autostart.heading=Autostart:
115+
gateway.connector.settings.disable-autostart.title=Disable autostart
116+
gateway.connector.settings.disable-autostart.comment=Checking this box will \
117+
cause the plugin to configure the CLI with --disable-autostart. You must go \
118+
through the IDE selection again for the plugin to reconfigure the CLI with \
119+
this setting.
Lines changed: 9 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,9 @@
1+
# --- START CODER JETBRAINS test.coder.invalid
2+
Host coder-jetbrains--foo--test.coder.invalid
3+
ProxyCommand /tmp/coder-gateway/test.coder.invalid/coder-linux-amd64 --global-config /tmp/coder-gateway/test.coder.invalid/config ssh --stdio --disable-autostart foo
4+
ConnectTimeout 0
5+
StrictHostKeyChecking no
6+
UserKnownHostsFile /dev/null
7+
LogLevel ERROR
8+
SetEnv CODER_SSH_SESSION_TYPE=JetBrains
9+
# --- END CODER JETBRAINS test.coder.invalid
Lines changed: 9 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,9 @@
1+
# --- START CODER JETBRAINS test.coder.invalid
2+
Host coder-jetbrains--foo--test.coder.invalid
3+
ProxyCommand /tmp/coder-gateway/test.coder.invalid/coder-linux-amd64 --global-config /tmp/coder-gateway/test.coder.invalid/config ssh --stdio foo
4+
ConnectTimeout 0
5+
StrictHostKeyChecking no
6+
UserKnownHostsFile /dev/null
7+
LogLevel ERROR
8+
SetEnv CODER_SSH_SESSION_TYPE=JetBrains
9+
# --- END CODER JETBRAINS test.coder.invalid

src/test/kotlin/com/coder/gateway/cli/CoderCLIManagerTest.kt

Lines changed: 43 additions & 7 deletions
Original file line numberDiff line numberDiff line change
@@ -33,18 +33,18 @@ import kotlin.test.assertTrue
3333

3434
internal class CoderCLIManagerTest {
3535
private fun mkbin(version: String): String {
36-
return listOf("#!/bin/sh", """echo '{"version": "${version}"}'""")
36+
return listOf("#!/bin/sh", """echo '{"version": "$version"}'""")
3737
.joinToString("\n")
3838
}
3939

40-
private fun mockServer(errorCode: Int = 0): Pair<HttpServer, URL> {
40+
private fun mockServer(errorCode: Int = 0, version: String? = null): Pair<HttpServer, URL> {
4141
val srv = HttpServer.create(InetSocketAddress(0), 0)
4242
srv.createContext("/") {exchange ->
4343
var code = HttpURLConnection.HTTP_OK
4444
// TODO: Is there some simple way to create an executable file on
4545
// Windows without having to execute something to generate said
4646
// executable or having to commit one to the repo?
47-
var response = mkbin("${srv.address.port}.0.0")
47+
var response = mkbin(version ?: "${srv.address.port}.0.0")
4848
val eTags = exchange.requestHeaders["If-None-Match"]
4949
if (exchange.requestURI.path == "/bin/override") {
5050
code = HttpURLConnection.HTTP_OK
@@ -234,7 +234,15 @@ internal class CoderCLIManagerTest {
234234
srv2.stop(0)
235235
}
236236

237-
data class SSHTest(val workspaces: List<String>, val input: String?, val output: String, val remove: String, val headerCommand: String?)
237+
data class SSHTest(
238+
val workspaces: List<String>,
239+
val input: String?,
240+
val output: String,
241+
val remove: String,
242+
val headerCommand: String?,
243+
val disableAutostart: Boolean = false,
244+
val features: Features? = null,
245+
)
238246

239247
@Test
240248
fun testConfigureSSH() {
@@ -256,13 +264,16 @@ internal class CoderCLIManagerTest {
256264
SSHTest(listOf("header"), null, "header-command-windows", "blank", """"C:\Program Files\My Header Command\HeaderCommand.exe" --url="%CODER_URL%" --test="foo bar"""")
257265
} else {
258266
SSHTest(listOf("header"), null, "header-command", "blank", "my-header-command --url=\"\$CODER_URL\" --test=\"foo bar\" --literal='\$CODER_URL'")
259-
}
267+
},
268+
SSHTest(listOf("foo"), null, "disable-autostart", "blank", null, true, Features(true)),
269+
SSHTest(listOf("foo"), null, "no-disable-autostart", "blank", null, true, Features(false)),
260270
)
261271

262272
val newlineRe = "\r?\n".toRegex()
263273

264274
tests.forEach {
265275
val settings = CoderSettings(CoderSettingsState(
276+
disableAutostart = it.disableAutostart,
266277
dataDirectory = tmpdir.resolve("configure-ssh").toString(),
267278
headerCommand = it.headerCommand ?: ""),
268279
sshConfigPath = tmpdir.resolve(it.input + "_to_" + it.output + ".conf"))
@@ -285,12 +296,12 @@ internal class CoderCLIManagerTest {
285296
.replace("/tmp/coder-gateway/test.coder.invalid/coder-linux-amd64", escape(ccm.localBinaryPath.toString()))
286297

287298
// Add workspaces.
288-
ccm.configSsh(it.workspaces)
299+
ccm.configSsh(it.workspaces, it.features ?: Features())
289300

290301
assertEquals(expectedConf, settings.sshConfigPath.toFile().readText())
291302

292303
// Remove configuration.
293-
ccm.configSsh(emptyList())
304+
ccm.configSsh(emptyList(), it.features ?: Features())
294305

295306
// Remove is the configuration we expect after removing.
296307
assertEquals(
@@ -540,6 +551,31 @@ internal class CoderCLIManagerTest {
540551
srv.stop(0)
541552
}
542553

554+
@Test
555+
fun testFeatures() {
556+
if (getOS() == OS.WINDOWS) {
557+
return // Cannot execute mock binaries on Windows.
558+
}
559+
560+
val tests = listOf(
561+
Pair("2.5.0", Features(true)),
562+
Pair("4.9.0", Features(true)),
563+
Pair("2.4.9", Features(false)),
564+
Pair("1.0.1", Features(false)),
565+
)
566+
567+
tests.forEach {
568+
val (srv, url) = mockServer(version = it.first)
569+
val ccm = CoderCLIManager(url, CoderSettings(CoderSettingsState(
570+
dataDirectory = tmpdir.resolve("features").toString()))
571+
)
572+
assertEquals(true, ccm.download())
573+
assertEquals(it.second, ccm.features, "version: ${it.first}")
574+
575+
srv.stop(0)
576+
}
577+
}
578+
543579
companion object {
544580
private val tmpdir: Path = Path.of(System.getProperty("java.io.tmpdir")).resolve("coder-gateway-test/cli-manager")
545581

src/test/kotlin/com/coder/gateway/settings/CoderSettingsTest.kt

Lines changed: 13 additions & 12 deletions
Original file line numberDiff line numberDiff line change
@@ -1,15 +1,14 @@
11
package com.coder.gateway.settings
22

33
import com.coder.gateway.services.CoderSettingsState
4-
import kotlin.test.Test
5-
import kotlin.test.assertContains
6-
import kotlin.test.assertEquals
7-
84
import com.coder.gateway.util.OS
95
import com.coder.gateway.util.getOS
106
import com.coder.gateway.util.withPath
117
import java.net.URL
128
import java.nio.file.Path
9+
import kotlin.test.Test
10+
import kotlin.test.assertContains
11+
import kotlin.test.assertEquals
1312

1413
internal class CoderSettingsTest {
1514
@Test
@@ -179,14 +178,15 @@ internal class CoderSettingsTest {
179178
// Make sure the remaining settings are being conveyed.
180179
val settings = CoderSettings(
181180
CoderSettingsState(
182-
enableDownloads = false,
183-
enableBinaryDirectoryFallback = true,
184-
headerCommand = "test header",
185-
tlsCertPath = "tls cert path",
186-
tlsKeyPath = "tls key path",
187-
tlsCAPath = "tls ca path",
188-
tlsAlternateHostname = "tls alt hostname",
189-
)
181+
enableDownloads = false,
182+
enableBinaryDirectoryFallback = true,
183+
headerCommand = "test header",
184+
tlsCertPath = "tls cert path",
185+
tlsKeyPath = "tls key path",
186+
tlsCAPath = "tls ca path",
187+
tlsAlternateHostname = "tls alt hostname",
188+
disableAutostart = true,
189+
)
190190
)
191191

192192
assertEquals(false, settings.enableDownloads)
@@ -196,5 +196,6 @@ internal class CoderSettingsTest {
196196
assertEquals("tls key path", settings.tls.keyPath)
197197
assertEquals("tls ca path", settings.tls.caPath)
198198
assertEquals("tls alt hostname", settings.tls.altHostname)
199+
assertEquals(true, settings.disableAutostart)
199200
}
200201
}

0 commit comments

Comments
 (0)
pFad - Phonifier reborn

Pfad - The Proxy pFad of © 2024 Garber Painting. All rights reserved.

Note: This service is not intended for secure transactions such as banking, social media, email, or purchasing. Use at your own risk. We assume no liability whatsoever for broken pages.


Alternative Proxies:

Alternative Proxy

pFad Proxy

pFad v3 Proxy

pFad v4 Proxy