Skip to content

Commit 142c803

Browse files
committed
Add configurable autostart behavior for SSH connections
1 parent 6d57292 commit 142c803

File tree

8 files changed

+198
-112
lines changed

8 files changed

+198
-112
lines changed

package.json

Lines changed: 15 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -120,6 +120,21 @@
120120
"type": "boolean",
121121
"default": false
122122
},
123+
"coder.disableAutostart": {
124+
"markdownDescription": "Disable starting the workspace automatically when connecting via SSH.",
125+
"type": "string",
126+
"enum": [
127+
"auto",
128+
"always",
129+
"never"
130+
],
131+
"markdownEnumDescriptions": [
132+
"Disables autostart on macOS only (recommended to avoid sleep/wake issues)",
133+
"Disables on all platforms",
134+
"Keeps autostart enabled on all platforms"
135+
],
136+
"default": "auto"
137+
},
123138
"coder.globalFlags": {
124139
"markdownDescription": "Global flags to pass to every Coder CLI invocation. Enter each flag as a separate array item; values are passed verbatim and in order. Do **not** include the `coder` command itself. See the [CLI reference](https://coder.com/docs/reference/cli) for available global flags.\n\nNote that for `--header-command`, precedence is: `#coder.headerCommand#` setting, then `CODER_HEADER_COMMAND` environment variable, then the value specified here. The `--global-config` flag is explicitly ignored.",
125140
"type": "array",

src/api/workspace.ts

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -8,8 +8,8 @@ import {
88
import { spawn } from "node:child_process";
99
import * as vscode from "vscode";
1010

11+
import { getGlobalFlags } from "../cliConfig";
1112
import { type FeatureSet } from "../featureSet";
12-
import { getGlobalFlags } from "../globalFlags";
1313
import { escapeCommandArg } from "../util";
1414
import { type UnidirectionalStream } from "../websocket/eventStreamConnection";
1515

src/cliConfig.ts

Lines changed: 40 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,40 @@
1+
import { type WorkspaceConfiguration } from "vscode";
2+
3+
import { getHeaderArgs } from "./headers";
4+
import { escapeCommandArg } from "./util";
5+
6+
/**
7+
* Returns global configuration flags for Coder CLI commands.
8+
* Always includes the `--global-config` argument with the specified config directory.
9+
*/
10+
export function getGlobalFlags(
11+
configs: WorkspaceConfiguration,
12+
configDir: string,
13+
): string[] {
14+
// Last takes precedence/overrides previous ones
15+
return [
16+
...(configs.get<string[]>("coder.globalFlags") || []),
17+
"--global-config",
18+
escapeCommandArg(configDir),
19+
...getHeaderArgs(configs),
20+
];
21+
}
22+
23+
type DisableAutostartSetting = "auto" | "always" | "never";
24+
25+
/**
26+
* Determines whether autostart should be disabled based on the setting and platform.
27+
* - "always": disable on all platforms
28+
* - "never": never disable
29+
* - "auto": disable only on macOS (due to sleep/wake issues)
30+
*/
31+
export function shouldDisableAutostart(
32+
configs: WorkspaceConfiguration,
33+
platform: NodeJS.Platform,
34+
): boolean {
35+
const setting = configs.get<DisableAutostartSetting>(
36+
"coder.disableAutostart",
37+
"auto",
38+
);
39+
return setting === "always" || (setting === "auto" && platform === "darwin");
40+
}

src/commands.ts

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -10,14 +10,14 @@ import * as vscode from "vscode";
1010
import { createWorkspaceIdentifier, extractAgents } from "./api/api-helper";
1111
import { CoderApi } from "./api/coderApi";
1212
import { needToken } from "./api/utils";
13+
import { getGlobalFlags } from "./cliConfig";
1314
import { type CliManager } from "./core/cliManager";
1415
import { type ServiceContainer } from "./core/container";
1516
import { type ContextManager } from "./core/contextManager";
1617
import { type MementoManager } from "./core/mementoManager";
1718
import { type PathResolver } from "./core/pathResolver";
1819
import { type SecretsManager } from "./core/secretsManager";
1920
import { CertificateError } from "./error";
20-
import { getGlobalFlags } from "./globalFlags";
2121
import { type Logger } from "./logging/logger";
2222
import { maybeAskAgent, maybeAskUrl } from "./promptUtils";
2323
import { escapeCommandArg, toRemoteAuthority, toSafeHost } from "./util";

src/globalFlags.ts

Lines changed: 0 additions & 20 deletions
This file was deleted.

src/remote/remote.ts

Lines changed: 9 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -22,14 +22,14 @@ import {
2222
import { extractAgents } from "../api/api-helper";
2323
import { CoderApi } from "../api/coderApi";
2424
import { needToken } from "../api/utils";
25+
import { getGlobalFlags, shouldDisableAutostart } from "../cliConfig";
2526
import { type Commands } from "../commands";
2627
import { type CliManager } from "../core/cliManager";
2728
import * as cliUtils from "../core/cliUtils";
2829
import { type ServiceContainer } from "../core/container";
2930
import { type ContextManager } from "../core/contextManager";
3031
import { type PathResolver } from "../core/pathResolver";
3132
import { featureSetForVersion, type FeatureSet } from "../featureSet";
32-
import { getGlobalFlags } from "../globalFlags";
3333
import { Inbox } from "../inbox";
3434
import { type Logger } from "../logging/logger";
3535
import {
@@ -674,7 +674,7 @@ export class Remote {
674674
const globalConfigs = this.globalConfigs(label);
675675

676676
const proxyCommand = featureSet.wildcardSSH
677-
? `${escapeCommandArg(binaryPath)}${globalConfigs} ssh --stdio --usage-app=vscode --disable-autostart --network-info-dir ${escapeCommandArg(this.pathResolver.getNetworkInfoPath())}${await this.formatLogArg(logDir)} --ssh-host-prefix ${hostPrefix} %h`
677+
? `${escapeCommandArg(binaryPath)}${globalConfigs} ssh --stdio --usage-app=vscode${this.disableAutostartConfig()} --network-info-dir ${escapeCommandArg(this.pathResolver.getNetworkInfoPath())}${await this.formatLogArg(logDir)} --ssh-host-prefix ${hostPrefix} %h`
678678
: `${escapeCommandArg(binaryPath)}${globalConfigs} vscodessh --network-info-dir ${escapeCommandArg(
679679
this.pathResolver.getNetworkInfoPath(),
680680
)}${await this.formatLogArg(logDir)} --session-token-file ${escapeCommandArg(this.pathResolver.getSessionTokenPath(label))} --url-file ${escapeCommandArg(
@@ -741,6 +741,13 @@ export class Remote {
741741
return ` ${args.join(" ")}`;
742742
}
743743

744+
private disableAutostartConfig(): string {
745+
const configs = vscode.workspace.getConfiguration();
746+
return shouldDisableAutostart(configs, process.platform)
747+
? " --disable-autostart"
748+
: "";
749+
}
750+
744751
// showNetworkUpdates finds the SSH process ID that is being used by this
745752
// workspace and reads the file being created by the Coder CLI.
746753
private showNetworkUpdates(sshPid: number): vscode.Disposable {

test/unit/cliConfig.test.ts

Lines changed: 132 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,132 @@
1+
import { it, expect, describe } from "vitest";
2+
import { type WorkspaceConfiguration } from "vscode";
3+
4+
import { getGlobalFlags, shouldDisableAutostart } from "@/cliConfig";
5+
6+
import { isWindows } from "../utils/platform";
7+
8+
describe("cliConfig", () => {
9+
describe("getGlobalFlags", () => {
10+
it("should return global-config and header args when no global flags configured", () => {
11+
const config = {
12+
get: () => undefined,
13+
} as unknown as WorkspaceConfiguration;
14+
15+
expect(getGlobalFlags(config, "/config/dir")).toStrictEqual([
16+
"--global-config",
17+
'"/config/dir"',
18+
]);
19+
});
20+
21+
it("should return global flags from config with global-config appended", () => {
22+
const config = {
23+
get: (key: string) =>
24+
key === "coder.globalFlags"
25+
? ["--verbose", "--disable-direct-connections"]
26+
: undefined,
27+
} as unknown as WorkspaceConfiguration;
28+
29+
expect(getGlobalFlags(config, "/config/dir")).toStrictEqual([
30+
"--verbose",
31+
"--disable-direct-connections",
32+
"--global-config",
33+
'"/config/dir"',
34+
]);
35+
});
36+
37+
it("should not filter duplicate global-config flags, last takes precedence", () => {
38+
const config = {
39+
get: (key: string) =>
40+
key === "coder.globalFlags"
41+
? [
42+
"-v",
43+
"--global-config /path/to/ignored",
44+
"--disable-direct-connections",
45+
]
46+
: undefined,
47+
} as unknown as WorkspaceConfiguration;
48+
49+
expect(getGlobalFlags(config, "/config/dir")).toStrictEqual([
50+
"-v",
51+
"--global-config /path/to/ignored",
52+
"--disable-direct-connections",
53+
"--global-config",
54+
'"/config/dir"',
55+
]);
56+
});
57+
58+
it("should not filter header-command flags, header args appended at end", () => {
59+
const headerCommand = "echo test";
60+
const config = {
61+
get: (key: string) => {
62+
if (key === "coder.headerCommand") {
63+
return headerCommand;
64+
}
65+
if (key === "coder.globalFlags") {
66+
return ["-v", "--header-command custom", "--no-feature-warning"];
67+
}
68+
return undefined;
69+
},
70+
} as unknown as WorkspaceConfiguration;
71+
72+
const result = getGlobalFlags(config, "/config/dir");
73+
expect(result).toStrictEqual([
74+
"-v",
75+
"--header-command custom", // ignored by CLI
76+
"--no-feature-warning",
77+
"--global-config",
78+
'"/config/dir"',
79+
"--header-command",
80+
quoteCommand(headerCommand),
81+
]);
82+
});
83+
});
84+
85+
describe("shouldDisableAutostart", () => {
86+
const mockConfig = (setting: string) =>
87+
({
88+
get: (key: string) =>
89+
key === "coder.disableAutostart" ? setting : undefined,
90+
}) as unknown as WorkspaceConfiguration;
91+
92+
it("returns true when setting is 'always' regardless of platform", () => {
93+
const config = mockConfig("always");
94+
expect(shouldDisableAutostart(config, "darwin")).toBe(true);
95+
expect(shouldDisableAutostart(config, "linux")).toBe(true);
96+
expect(shouldDisableAutostart(config, "win32")).toBe(true);
97+
});
98+
99+
it("returns false when setting is 'never' regardless of platform", () => {
100+
const config = mockConfig("never");
101+
expect(shouldDisableAutostart(config, "darwin")).toBe(false);
102+
expect(shouldDisableAutostart(config, "linux")).toBe(false);
103+
expect(shouldDisableAutostart(config, "win32")).toBe(false);
104+
});
105+
106+
it("returns true when setting is 'auto' and platform is darwin", () => {
107+
const config = mockConfig("auto");
108+
expect(shouldDisableAutostart(config, "darwin")).toBe(true);
109+
});
110+
111+
it("returns false when setting is 'auto' and platform is not darwin", () => {
112+
const config = mockConfig("auto");
113+
expect(shouldDisableAutostart(config, "linux")).toBe(false);
114+
expect(shouldDisableAutostart(config, "win32")).toBe(false);
115+
expect(shouldDisableAutostart(config, "freebsd")).toBe(false);
116+
});
117+
118+
it("defaults to 'auto' when setting is not configured", () => {
119+
const config = {
120+
get: (_key: string, defaultValue: unknown) => defaultValue,
121+
} as unknown as WorkspaceConfiguration;
122+
expect(shouldDisableAutostart(config, "darwin")).toBe(true);
123+
expect(shouldDisableAutostart(config, "linux")).toBe(false);
124+
});
125+
});
126+
});
127+
128+
function quoteCommand(value: string): string {
129+
// Used to escape environment variables in commands. See `getHeaderArgs` in src/headers.ts
130+
const quote = isWindows() ? '"' : "'";
131+
return `${quote}${value}${quote}`;
132+
}

test/unit/globalFlags.test.ts

Lines changed: 0 additions & 88 deletions
This file was deleted.

0 commit comments

Comments
 (0)