Skip to content
Open
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
90 changes: 11 additions & 79 deletions src/remote/remote.ts
Original file line number Diff line number Diff line change
Expand Up @@ -4,7 +4,6 @@ import {
type Workspace,
type WorkspaceAgent,
} from "coder/site/src/api/typesGenerated";
import * as jsonc from "jsonc-parser";
import * as fs from "node:fs/promises";
import * as os from "node:os";
import * as path from "node:path";
Expand Down Expand Up @@ -60,6 +59,7 @@ import {
} from "./sshConfig";
import { SshProcessMonitor } from "./sshProcess";
import { computeSSHProperties, sshSupportsSetEnv } from "./sshSupport";
import { applySettingOverrides, buildSshOverrides } from "./userSettings";
import { WorkspaceStateMachine } from "./workspaceStateMachine";

export interface RemoteDetails extends vscode.Disposable {
Expand Down Expand Up @@ -459,85 +459,17 @@ export class Remote {
const inbox = await Inbox.create(workspace, workspaceClient, this.logger);
disposables.push(inbox);

// Do some janky setting manipulation.
this.logger.info("Modifying settings...");
const remotePlatforms = vscodeProposed.workspace
.getConfiguration()
.get<Record<string, string>>("remote.SSH.remotePlatform", {});
const connTimeout = vscodeProposed.workspace
.getConfiguration()
.get<number | undefined>("remote.SSH.connectTimeout");

// We have to directly munge the settings file with jsonc because trying to
// update properly through the extension API hangs indefinitely. Possibly
// VS Code is trying to update configuration on the remote, which cannot
// connect until we finish here leading to a deadlock. We need to update it
// locally, anyway, and it does not seem possible to force that via API.
let settingsContent = "{}";
try {
settingsContent = await fs.readFile(
this.pathResolver.getUserSettingsPath(),
"utf8",
);
} catch {
// Ignore! It's probably because the file doesn't exist.
}

// Add the remote platform for this host to bypass a step where VS Code asks
// the user for the platform.
let mungedPlatforms = false;
if (
!remotePlatforms[parts.sshHost] ||
remotePlatforms[parts.sshHost] !== agent.operating_system
) {
remotePlatforms[parts.sshHost] = agent.operating_system;
settingsContent = jsonc.applyEdits(
settingsContent,
jsonc.modify(
settingsContent,
["remote.SSH.remotePlatform"],
remotePlatforms,
{},
),
);
mungedPlatforms = true;
}

// VS Code ignores the connect timeout in the SSH config and uses a default
// of 15 seconds, which can be too short in the case where we wait for
// startup scripts. For now we hardcode a longer value. Because this is
// potentially overwriting user configuration, it feels a bit sketchy. If
// microsoft/vscode-remote-release#8519 is resolved we can remove this.
const minConnTimeout = 1800;
let mungedConnTimeout = false;
if (!connTimeout || connTimeout < minConnTimeout) {
settingsContent = jsonc.applyEdits(
settingsContent,
jsonc.modify(
settingsContent,
["remote.SSH.connectTimeout"],
minConnTimeout,
{},
),
);
mungedConnTimeout = true;
}

if (mungedPlatforms || mungedConnTimeout) {
try {
await fs.writeFile(
this.pathResolver.getUserSettingsPath(),
settingsContent,
);
} catch (ex) {
// This could be because the user's settings.json is read-only. This is
// the case when using home-manager on NixOS, for example. Failure to
// write here is not necessarily catastrophic since the user will be
// asked for the platform and the default timeout might be sufficient.
mungedPlatforms = mungedConnTimeout = false;
this.logger.warn("Failed to configure settings", ex);
}
}
const overrides = buildSshOverrides(
vscodeProposed.workspace.getConfiguration(),
parts.sshHost,
agent.operating_system,
);
await applySettingOverrides(
this.pathResolver.getUserSettingsPath(),
overrides,
this.logger,
);

const logDir = this.getLogDir(featureSet);

Expand Down
98 changes: 98 additions & 0 deletions src/remote/userSettings.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1,98 @@
import * as jsonc from "jsonc-parser";
import * as fs from "node:fs/promises";

import type { WorkspaceConfiguration } from "vscode";

import type { Logger } from "../logging/logger";

export interface SettingOverride {
key: string;
value: unknown;
}

/**
* Build the list of VS Code setting overrides needed for a remote SSH
* connection to a Coder workspace.
*/
export function buildSshOverrides(
config: Pick<WorkspaceConfiguration, "get">,
sshHost: string,
agentOS: string,
): SettingOverride[] {
const overrides: SettingOverride[] = [];

// Bypass the platform prompt by setting the remote platform for this host.
const remotePlatforms = config.get<Record<string, string>>(
"remote.SSH.remotePlatform",
{},
);
if (remotePlatforms[sshHost] !== agentOS) {
overrides.push({
key: "remote.SSH.remotePlatform",
value: { ...remotePlatforms, [sshHost]: agentOS },
});
}

// VS Code's default connect timeout of 15s is too short when waiting for
// startup scripts. Enforce a minimum.
const minConnTimeout = 1800;
const connTimeout = config.get<number>("remote.SSH.connectTimeout");
if (!connTimeout || connTimeout < minConnTimeout) {
overrides.push({
key: "remote.SSH.connectTimeout",
value: minConnTimeout,
});
}

// VS Code's default reconnection grace time (ProtocolConstants.ReconnectionGraceTime)
// is 3 hours (10800s). Coder workspaces commonly go offline overnight, so we
// bump to 8 hours. See https://github.com/microsoft/vscode/blob/main/src/vs/base/parts/ipc/common/ipc.net.ts
if (config.get<number>("remote.SSH.reconnectionGraceTime") === undefined) {
overrides.push({
key: "remote.SSH.reconnectionGraceTime",
value: 28800, // 8 hours in seconds
});
}

return overrides;
}

/**
* Apply setting overrides to the user's settings.json file.
*
* We munge the file directly with jsonc instead of using the VS Code API
* because the API hangs indefinitely during remote connection setup (likely
* a deadlock from trying to update config on the not-yet-connected remote).
*/
export async function applySettingOverrides(
settingsFilePath: string,
overrides: SettingOverride[],
logger: Logger,
): Promise<boolean> {
if (overrides.length === 0) {
return false;
}

let settingsContent = "{}";
try {
settingsContent = await fs.readFile(settingsFilePath, "utf8");
} catch {
// File probably doesn't exist yet.
}

for (const { key, value } of overrides) {
settingsContent = jsonc.applyEdits(
settingsContent,
jsonc.modify(settingsContent, [key], value, {}),
);
}

try {
await fs.writeFile(settingsFilePath, settingsContent);
return true;
} catch (ex) {
// Could be read-only (e.g. home-manager on NixOS). Not catastrophic.
logger.warn("Failed to configure settings", ex);
return false;
}
}
Loading