From 5cb23af3b93305a00c3023bf3395b656229b6fcd Mon Sep 17 00:00:00 2001 From: Austin Dickey Date: Fri, 17 Oct 2025 13:09:33 -0500 Subject: [PATCH 1/3] remote ssh: add custom serverInstallPath setting --- extensions/open-remote-ssh/package.json | 6 ++++++ extensions/open-remote-ssh/src/serverConfig.ts | 3 ++- extensions/open-remote-ssh/src/serverSetup.ts | 10 +++++++--- product.json | 2 +- 4 files changed, 16 insertions(+), 5 deletions(-) diff --git a/extensions/open-remote-ssh/package.json b/extensions/open-remote-ssh/package.json index 315c432b121..1c3007c1957 100644 --- a/extensions/open-remote-ssh/package.json +++ b/extensions/open-remote-ssh/package.json @@ -103,6 +103,12 @@ "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": "string", + "markdownDescription": "A custom directory to install the Positron server data on the remote machine. By default, the server data is installed in `~/.positron-server`.", + "scope": "application", + "default": "" } } }, diff --git a/extensions/open-remote-ssh/src/serverConfig.ts b/extensions/open-remote-ssh/src/serverConfig.ts index f4b81450e47..acc8e094f25 100644 --- a/extensions/open-remote-ssh/src/serverConfig.ts +++ b/extensions/open-remote-ssh/src/serverConfig.ts @@ -35,6 +35,7 @@ export async function getVSCodeServerConfig(): Promise { const productJson = await getVSCodeProductJson(); const customServerBinaryName = vscode.workspace.getConfiguration('remoteSSH.experimental').get('serverBinaryName', ''); + const customDataFolderName = vscode.workspace.getConfiguration('remoteSSH').get('serverInstallPath', ''); const version = `${positron.version}-${positron.buildNumber}`; @@ -44,7 +45,7 @@ export async function getVSCodeServerConfig(): Promise { quality: productJson.quality, release: productJson.release, serverApplicationName: customServerBinaryName || productJson.serverApplicationName, - serverDataFolderName: productJson.serverDataFolderName, + serverDataFolderName: customDataFolderName || productJson.serverDataFolderName, serverDownloadUrlTemplate: productJson.serverDownloadUrlTemplate }; } diff --git a/extensions/open-remote-ssh/src/serverSetup.ts b/extensions/open-remote-ssh/src/serverSetup.ts index 6a3a9596e4c..e78bd31c5b5 100644 --- a/extensions/open-remote-ssh/src/serverSetup.ts +++ b/extensions/open-remote-ssh/src/serverSetup.ts @@ -94,11 +94,15 @@ export async function installCodeServer(conn: SSHConnection, serverDownloadUrlTe let commandOutput: { stdout: string; stderr: string }; if (platform === 'windows') { + // If the default was not changed, adjust the path for PowerShell on Windows + if (installOptions.serverDataFolderName === '$HOME/.positron-server') { + installOptions.serverDataFolderName = '$HOME\\.positron-server'; + } const installServerScript = generatePowerShellInstallScript(installOptions); logger.trace('Server install command:', installServerScript); - const installDir = `$HOME\\${vscodeServerConfig.serverDataFolderName}\\install`; + const installDir = `${vscodeServerConfig.serverDataFolderName}\\install`; const installScript = `${installDir}\\${vscodeServerConfig.commit}.ps1`; const endRegex = new RegExp(`${scriptId}: end`); // investigate if it's possible to use `-EncodedCommand` flag @@ -226,7 +230,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'}" -SERVER_DATA_DIR="$HOME/${serverDataFolderName}" +SERVER_DATA_DIR="${serverDataFolderName}" 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,7 +471,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'}" -$SERVER_DATA_DIR="$(Resolve-Path ~)\\${serverDataFolderName}" +$SERVER_DATA_DIR="${serverDataFolderName}" $SERVER_DIR="$SERVER_DATA_DIR\\bin\\$DISTRO_COMMIT" $SERVER_SCRIPT="$SERVER_DIR\\bin\\$SERVER_APP_NAME.cmd" $SERVER_LOGFILE="$SERVER_DATA_DIR\\.$DISTRO_COMMIT.log" diff --git a/product.json b/product.json index 5073425c8de..17ab99582a6 100644 --- a/product.json +++ b/product.json @@ -14,7 +14,7 @@ "serverLicense": [], "serverLicensePrompt": "", "serverApplicationName": "positron-server", - "serverDataFolderName": ".positron-server", + "serverDataFolderName": "$HOME/.positron-server", "serverDownloadUrlTemplate": "https://cdn.posit.co/positron/dailies/reh/${arch-long}/positron-reh-${os}-${arch}-${version}.tar.gz", "tunnelApplicationName": "positron-tunnel", "win32DirName": "Positron", From 30ab6b00f0e46e155a7ee0835c92c9fcc0392d66 Mon Sep 17 00:00:00 2001 From: Austin Dickey Date: Tue, 21 Oct 2025 14:08:12 -0500 Subject: [PATCH 2/3] overhaul! --- extensions/open-remote-ssh/package.json | 9 ++++-- .../open-remote-ssh/src/authResolver.ts | 2 +- .../open-remote-ssh/src/serverConfig.ts | 3 +- extensions/open-remote-ssh/src/serverSetup.ts | 31 ++++++++++++++----- product.json | 2 +- 5 files changed, 32 insertions(+), 15 deletions(-) diff --git a/extensions/open-remote-ssh/package.json b/extensions/open-remote-ssh/package.json index 1c3007c1957..84bbe44e605 100644 --- a/extensions/open-remote-ssh/package.json +++ b/extensions/open-remote-ssh/package.json @@ -105,10 +105,13 @@ "default": "" }, "remoteSSH.serverInstallPath": { - "type": "string", - "markdownDescription": "A custom directory to install the Positron server data on the remote machine. By default, the server data is installed in `~/.positron-server`.", + "type": "object", + "markdownDescription": "A map of remote host to the remote directory where the Positron server data will be installed. 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`. For any host not included here, the server is installed in `$HOME/.positron-server`.", "scope": "application", - "default": "" + "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/serverConfig.ts b/extensions/open-remote-ssh/src/serverConfig.ts index acc8e094f25..f4b81450e47 100644 --- a/extensions/open-remote-ssh/src/serverConfig.ts +++ b/extensions/open-remote-ssh/src/serverConfig.ts @@ -35,7 +35,6 @@ export async function getVSCodeServerConfig(): Promise { const productJson = await getVSCodeProductJson(); const customServerBinaryName = vscode.workspace.getConfiguration('remoteSSH.experimental').get('serverBinaryName', ''); - const customDataFolderName = vscode.workspace.getConfiguration('remoteSSH').get('serverInstallPath', ''); const version = `${positron.version}-${positron.buildNumber}`; @@ -45,7 +44,7 @@ export async function getVSCodeServerConfig(): Promise { quality: productJson.quality, release: productJson.release, serverApplicationName: customServerBinaryName || productJson.serverApplicationName, - serverDataFolderName: customDataFolderName || productJson.serverDataFolderName, + serverDataFolderName: productJson.serverDataFolderName, serverDownloadUrlTemplate: productJson.serverDownloadUrlTemplate }; } diff --git a/extensions/open-remote-ssh/src/serverSetup.ts b/extensions/open-remote-ssh/src/serverSetup.ts index e78bd31c5b5..e070a7c80db 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,7 @@ 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 { +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 +79,15 @@ export async function installCodeServer(conn: SSHConnection, serverDownloadUrlTe const scriptId = crypto.randomBytes(12).toString('hex'); const vscodeServerConfig = await getVSCodeServerConfig(); + + let serverDataFolderName = vscodeServerConfig.serverDataFolderName; + const dataFolderSetting = vscode.workspace.getConfiguration('remoteSSH').get<{ [key: string]: string }>('serverInstallPath', {}); + if (dataFolderSetting.hasOwnProperty(hostname)) { + serverDataFolderName = dataFolderSetting[hostname]; + } else if (dataFolderSetting.hasOwnProperty(hostAlias)) { + serverDataFolderName = dataFolderSetting[hostAlias]; + } + const installOptions: ServerInstallOptions = { id: scriptId, version: vscodeServerConfig.version, @@ -88,21 +98,18 @@ export async function installCodeServer(conn: SSHConnection, serverDownloadUrlTe envVariables, useSocketPath, serverApplicationName: vscodeServerConfig.serverApplicationName, - serverDataFolderName: vscodeServerConfig.serverDataFolderName, + serverDataFolderName, serverDownloadUrlTemplate: serverDownloadUrlTemplate || vscodeServerConfig.serverDownloadUrlTemplate || DEFAULT_DOWNLOAD_URL_TEMPLATE, }; let commandOutput: { stdout: string; stderr: string }; if (platform === 'windows') { - // If the default was not changed, adjust the path for PowerShell on Windows - if (installOptions.serverDataFolderName === '$HOME/.positron-server') { - installOptions.serverDataFolderName = '$HOME\\.positron-server'; - } const installServerScript = generatePowerShellInstallScript(installOptions); logger.trace('Server install command:', installServerScript); - const installDir = `${vscodeServerConfig.serverDataFolderName}\\install`; + // 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`); // investigate if it's possible to use `-EncodedCommand` flag @@ -231,6 +238,13 @@ 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="${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" @@ -471,7 +485,8 @@ $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="${serverDataFolderName}" +# 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" $SERVER_LOGFILE="$SERVER_DATA_DIR\\.$DISTRO_COMMIT.log" diff --git a/product.json b/product.json index 17ab99582a6..5073425c8de 100644 --- a/product.json +++ b/product.json @@ -14,7 +14,7 @@ "serverLicense": [], "serverLicensePrompt": "", "serverApplicationName": "positron-server", - "serverDataFolderName": "$HOME/.positron-server", + "serverDataFolderName": ".positron-server", "serverDownloadUrlTemplate": "https://cdn.posit.co/positron/dailies/reh/${arch-long}/positron-reh-${os}-${arch}-${version}.tar.gz", "tunnelApplicationName": "positron-tunnel", "win32DirName": "Positron", From cf5b6929a375d78bfcc58063fe03f4b7d4126e1f Mon Sep 17 00:00:00 2001 From: Austin Dickey Date: Wed, 22 Oct 2025 10:54:56 -0500 Subject: [PATCH 3/3] add wildcard support --- extensions/open-remote-ssh/package.json | 2 +- extensions/open-remote-ssh/src/serverSetup.ts | 50 +++++++++++++++++-- 2 files changed, 46 insertions(+), 6 deletions(-) diff --git a/extensions/open-remote-ssh/package.json b/extensions/open-remote-ssh/package.json index 84bbe44e605..63942c6cd33 100644 --- a/extensions/open-remote-ssh/package.json +++ b/extensions/open-remote-ssh/package.json @@ -106,7 +106,7 @@ }, "remoteSSH.serverInstallPath": { "type": "object", - "markdownDescription": "A map of remote host to the remote directory where the Positron server data will be installed. 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`. For any host not included here, the server is installed in `$HOME/.positron-server`.", + "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" diff --git a/extensions/open-remote-ssh/src/serverSetup.ts b/extensions/open-remote-ssh/src/serverSetup.ts index e070a7c80db..c7949e49404 100644 --- a/extensions/open-remote-ssh/src/serverSetup.ts +++ b/extensions/open-remote-ssh/src/serverSetup.ts @@ -46,6 +46,47 @@ 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'; +/** + * 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'; @@ -80,12 +121,11 @@ export async function installCodeServer(conn: SSHConnection, serverDownloadUrlTe const vscodeServerConfig = await getVSCodeServerConfig(); + // Check the remoteSSH.serverInstallPath setting let serverDataFolderName = vscodeServerConfig.serverDataFolderName; - const dataFolderSetting = vscode.workspace.getConfiguration('remoteSSH').get<{ [key: string]: string }>('serverInstallPath', {}); - if (dataFolderSetting.hasOwnProperty(hostname)) { - serverDataFolderName = dataFolderSetting[hostname]; - } else if (dataFolderSetting.hasOwnProperty(hostAlias)) { - serverDataFolderName = dataFolderSetting[hostAlias]; + const matchedPath = findFirstMatchingPath(hostname, hostAlias); + if (matchedPath) { + serverDataFolderName = matchedPath; } const installOptions: ServerInstallOptions = {