diff --git a/extensions/open-remote-ssh/package.json b/extensions/open-remote-ssh/package.json index 315c432b121..63942c6cd33 100644 --- a/extensions/open-remote-ssh/package.json +++ b/extensions/open-remote-ssh/package.json @@ -103,6 +103,15 @@ "description": "**Experimental:** The name of the server binary, use this **only if** you are using a client without a corresponding server release", "scope": "application", "default": "" + }, + "remoteSSH.serverInstallPath": { + "type": "object", + "markdownDescription": "A map of remote hostname to the remote directory where the Positron server data will be installed.\n\nFor the keys, you may use the `*` character to match 0 or more characters in the hostname or host's alias. The first matching host will be used.\n\nFor the values, you may include tildes or environment variables of the form `$ENV_VAR`, which will be resolved on the remote host upon connecting. Relative paths will be parsed relative to `$HOME`.\n\nFor any host not matched here, the server is installed in `$HOME/.positron-server`.", + "scope": "application", + "additionalProperties": { + "type": "string" + }, + "default": {} } } }, diff --git a/extensions/open-remote-ssh/src/authResolver.ts b/extensions/open-remote-ssh/src/authResolver.ts index 511a01963eb..a2ae66be943 100644 --- a/extensions/open-remote-ssh/src/authResolver.ts +++ b/extensions/open-remote-ssh/src/authResolver.ts @@ -199,7 +199,7 @@ export class RemoteSSHResolver implements vscode.RemoteAuthorityResolver, vscode envVariables['SSH_AUTH_SOCK'] = null; } - const installResult = await installCodeServer(this.sshConnection, serverDownloadUrlTemplate, defaultExtensions, Object.keys(envVariables), remotePlatformMap[sshDest.hostname], remoteServerListenOnSocket, this.logger); + const installResult = await installCodeServer(this.sshConnection, serverDownloadUrlTemplate, defaultExtensions, Object.keys(envVariables), remotePlatformMap[sshDest.hostname], remoteServerListenOnSocket, this.logger, sshHostName, sshDest.hostname); for (const key of Object.keys(envVariables)) { if (installResult[key] !== undefined) { diff --git a/extensions/open-remote-ssh/src/serverSetup.ts b/extensions/open-remote-ssh/src/serverSetup.ts index 6a3a9596e4c..c7949e49404 100644 --- a/extensions/open-remote-ssh/src/serverSetup.ts +++ b/extensions/open-remote-ssh/src/serverSetup.ts @@ -6,6 +6,7 @@ // The code in extensions/open-remote-ssh has been adapted from https://github.com/jeanp413/open-remote-ssh, // which is licensed under the MIT license. +import * as vscode from 'vscode'; import * as crypto from 'crypto'; import Log from './common/logger'; import { getVSCodeServerConfig } from './serverConfig'; @@ -45,7 +46,48 @@ export class ServerInstallError extends Error { const DEFAULT_DOWNLOAD_URL_TEMPLATE = 'https://cdn.posit.co/positron/dailies/reh/${arch-long}/positron-reh-${os}-${arch}-${version}.tar.gz'; -export async function installCodeServer(conn: SSHConnection, serverDownloadUrlTemplate: string | undefined, extensionIds: string[], envVariables: string[], platform: string | undefined, useSocketPath: boolean, logger: Log): Promise { +/** + * Converts a wildcard pattern to a regular expression. + * Supports patterns like: + * - "*" matches everything + * - "posit.*" matches strings starting with "posit." + * - "posit.co" matches exactly "posit.co" + */ +function wildcardToRegex(pattern: string): RegExp { + if (pattern === '*') { + return /.*/; + } + + // Escape special regex characters except * + const escaped = pattern.replace(/[.+?^${}()|[\]\\]/g, '\\$&'); + + // Replace * with .* + const regexPattern = '^' + escaped.replace(/\*/g, '.*') + '$'; + + return new RegExp(regexPattern); +} + +/** + * Finds the first matching path from the serverInstallPath setting. + * Iterates through the setting keys in order and returns the path for the first pattern + * that matches either the hostname or hostAlias. + * + * @param hostname - The hostname to match + * @param hostAlias - The host alias to match + * @returns The matching path or undefined if no match is found + */ +function findFirstMatchingPath(hostname: string, hostAlias: string): string | undefined { + const settings = vscode.workspace.getConfiguration('remoteSSH').get<{ [key: string]: string }>('serverInstallPath', {}); + for (const [pattern, path] of Object.entries(settings)) { + const regex = wildcardToRegex(pattern); + if (regex.test(hostname) || regex.test(hostAlias)) { + return path; + } + } + return undefined; +} + +export async function installCodeServer(conn: SSHConnection, serverDownloadUrlTemplate: string | undefined, extensionIds: string[], envVariables: string[], platform: string | undefined, useSocketPath: boolean, logger: Log, hostname: string, hostAlias: string): Promise { let shell = 'powershell'; // detect platform and shell for windows @@ -78,6 +120,14 @@ export async function installCodeServer(conn: SSHConnection, serverDownloadUrlTe const scriptId = crypto.randomBytes(12).toString('hex'); const vscodeServerConfig = await getVSCodeServerConfig(); + + // Check the remoteSSH.serverInstallPath setting + let serverDataFolderName = vscodeServerConfig.serverDataFolderName; + const matchedPath = findFirstMatchingPath(hostname, hostAlias); + if (matchedPath) { + serverDataFolderName = matchedPath; + } + const installOptions: ServerInstallOptions = { id: scriptId, version: vscodeServerConfig.version, @@ -88,7 +138,7 @@ export async function installCodeServer(conn: SSHConnection, serverDownloadUrlTe envVariables, useSocketPath, serverApplicationName: vscodeServerConfig.serverApplicationName, - serverDataFolderName: vscodeServerConfig.serverDataFolderName, + serverDataFolderName, serverDownloadUrlTemplate: serverDownloadUrlTemplate || vscodeServerConfig.serverDownloadUrlTemplate || DEFAULT_DOWNLOAD_URL_TEMPLATE, }; @@ -98,6 +148,7 @@ export async function installCodeServer(conn: SSHConnection, serverDownloadUrlTe logger.trace('Server install command:', installServerScript); + // TODO upon supporting windows hosts: respect the remoteSSH.serverInstallPath setting here const installDir = `$HOME\\${vscodeServerConfig.serverDataFolderName}\\install`; const installScript = `${installDir}\\${vscodeServerConfig.commit}.ps1`; const endRegex = new RegExp(`${scriptId}: end`); @@ -226,7 +277,14 @@ DISTRO_VSCODIUM_RELEASE="${release ?? ''}" SERVER_APP_NAME="${serverApplicationName}" SERVER_INITIAL_EXTENSIONS="${extensions}" SERVER_LISTEN_FLAG="${useSocketPath ? `--socket-path="$TMP_DIR/vscode-server-sock-${crypto.randomUUID()}"` : '--port=0'}" -SERVER_DATA_DIR="$HOME/${serverDataFolderName}" +SERVER_DATA_DIR="${serverDataFolderName}" + +# If SERVER_DATA_DIR is relative, make it relative to $HOME +if [[ "$SERVER_DATA_DIR" != /* ]] && [[ "$SERVER_DATA_DIR" != ~* ]]; then + SERVER_DATA_DIR="$HOME/$SERVER_DATA_DIR" +fi +echo "Using server data dir: $SERVER_DATA_DIR" + SERVER_DIR="$SERVER_DATA_DIR/bin/$DISTRO_COMMIT" SERVER_SCRIPT="$SERVER_DIR/bin/$SERVER_APP_NAME" SERVER_LOGFILE="$SERVER_DATA_DIR/.$DISTRO_COMMIT.log" @@ -467,6 +525,7 @@ $DISTRO_VSCODIUM_RELEASE="${release ?? ''}" $SERVER_APP_NAME="${serverApplicationName}" $SERVER_INITIAL_EXTENSIONS="${extensions}" $SERVER_LISTEN_FLAG="${useSocketPath ? `--socket-path="$TMP_DIR/vscode-server-sock-${crypto.randomUUID()}"` : '--port=0'}" +# TODO upon supporting windows hosts: respect the remoteSSH.serverInstallPath setting here $SERVER_DATA_DIR="$(Resolve-Path ~)\\${serverDataFolderName}" $SERVER_DIR="$SERVER_DATA_DIR\\bin\\$DISTRO_COMMIT" $SERVER_SCRIPT="$SERVER_DIR\\bin\\$SERVER_APP_NAME.cmd"