diff --git a/extensions/open-remote-ssh/package.json b/extensions/open-remote-ssh/package.json index e13e6c20b74..d35523c45bf 100644 --- a/extensions/open-remote-ssh/package.json +++ b/extensions/open-remote-ssh/package.json @@ -38,22 +38,22 @@ "main": "./out/extension.js", "contributes": { "configuration": { - "title": "Remote - SSH", + "title": "Remote SSH", "properties": { - "remote.SSH.configFile": { + "remoteSSH.configFile": { "type": "string", "description": "The absolute file path to a custom SSH config file.", "default": "", "scope": "application" }, - "remote.SSH.connectTimeout": { + "remoteSSH.connectTimeout": { "type": "number", "description": "Specifies the timeout in seconds used for the SSH command that connects to the remote.", "default": 60, "scope": "application", "minimum": 1 }, - "remote.SSH.defaultExtensions": { + "remoteSSH.defaultExtensions": { "type": "array", "items": { "type": "string" @@ -61,25 +61,25 @@ "description": "List of extensions that should be installed automatically on all SSH hosts.", "scope": "application" }, - "remote.SSH.enableDynamicForwarding": { + "remoteSSH.enableDynamicForwarding": { "type": "boolean", "description": "Whether to use SSH dynamic forwarding to allow setting up new port tunnels over an existing SSH connection.", "scope": "application", "default": true }, - "remote.SSH.enableAgentForwarding": { + "remoteSSH.enableAgentForwarding": { "type": "boolean", "markdownDescription": "Enable fixing the remote environment so that the SSH config option `ForwardAgent` will take effect as expected from VS Code's remote extension host.", "scope": "application", "default": true }, - "remote.SSH.serverDownloadUrlTemplate": { + "remoteSSH.serverDownloadUrlTemplate": { "type": "string", "description": "The URL from where the Positron server will be downloaded. You can use the following variables and they will be replaced dynamically:\n- ${quality}: Positron server quality, e.g. stable or insiders\n- ${version}: Positron server version, e.g. 2024.10.0-123\n- ${commit}: Positron server release commit\n- ${arch}: Positron server arch, e.g. x64, armhf, arm64", "scope": "application", "default": "https://github.com/posit-dev/positron/releases/download/${version}/positron-reh-${os}-${arch}-${version}.tar.gz" }, - "remote.SSH.remotePlatform": { + "remoteSSH.remotePlatform": { "type": "object", "description": "A map of the remote hostname to the platform for that remote. Valid values: linux, macos, windows.", "scope": "application", @@ -93,12 +93,12 @@ ] } }, - "remote.SSH.remoteServerListenOnSocket": { + "remoteSSH.remoteServerListenOnSocket": { "type": "boolean", "description": "When true, the remote vscode server will listen on a socket path instead of opening a port. Only valid for Linux and macOS remotes. Requires `AllowStreamLocalForwarding` to be enabled for the SSH server.", "default": false }, - "remote.SSH.experimental.serverBinaryName": { + "remoteSSH.experimental.serverBinaryName": { "type": "string", "description": "**Experimental:** The name of the server binary, use this **only if** you are using a client without a corresponding server release", "scope": "application", diff --git a/extensions/open-remote-ssh/src/authResolver.ts b/extensions/open-remote-ssh/src/authResolver.ts index bc2a57d6b2f..dbdbaf95c1d 100644 --- a/extensions/open-remote-ssh/src/authResolver.ts +++ b/extensions/open-remote-ssh/src/authResolver.ts @@ -24,437 +24,437 @@ const PASSPHRASE_RETRY_COUNT = 3; export const REMOTE_SSH_AUTHORITY = 'ssh-remote'; export function getRemoteAuthority(host: string) { - return `${REMOTE_SSH_AUTHORITY}+${host}`; + return `${REMOTE_SSH_AUTHORITY}+${host}`; } class TunnelInfo implements vscode.Disposable { - constructor( - readonly localPort: number, - readonly remotePortOrSocketPath: number | string, - private disposables: vscode.Disposable[] - ) { - } - - dispose() { - disposeAll(this.disposables); - } + constructor( + readonly localPort: number, + readonly remotePortOrSocketPath: number | string, + private disposables: vscode.Disposable[] + ) { + } + + dispose() { + disposeAll(this.disposables); + } } interface SSHKey { - filename: string; - parsedKey: ParsedKey; - fingerprint: string; - agentSupport?: boolean; - isPrivate?: boolean; + filename: string; + parsedKey: ParsedKey; + fingerprint: string; + agentSupport?: boolean; + isPrivate?: boolean; } export class RemoteSSHResolver implements vscode.RemoteAuthorityResolver, vscode.Disposable { - private proxyConnections: SSHConnection[] = []; - private sshConnection: SSHConnection | undefined; - private sshAgentSock: string | undefined; - private proxyCommandProcess: cp.ChildProcessWithoutNullStreams | undefined; - - private socksTunnel: SSHTunnelConfig | undefined; - private tunnels: TunnelInfo[] = []; - - private labelFormatterDisposable: vscode.Disposable | undefined; - - constructor( - readonly context: vscode.ExtensionContext, - readonly logger: Log - ) { - } - - resolve(authority: string, context: vscode.RemoteAuthorityResolverContext): Thenable { - const [type, dest] = authority.split('+'); - if (type !== REMOTE_SSH_AUTHORITY) { - throw new Error(`Invalid authority type for SSH resolver: ${type}`); - } - - this.logger.info(`Resolving ssh remote authority '${authority}' (attemp #${context.resolveAttempt})`); - - const sshDest = SSHDestination.parseEncoded(dest); - - // It looks like default values are not loaded yet when resolving a remote, - // so let's hardcode the default values here - const remoteSSHconfig = vscode.workspace.getConfiguration('remote.SSH'); - const enableDynamicForwarding = remoteSSHconfig.get('enableDynamicForwarding', true)!; - const enableAgentForwarding = remoteSSHconfig.get('enableAgentForwarding', true)!; - const serverDownloadUrlTemplate = remoteSSHconfig.get('serverDownloadUrlTemplate'); - const defaultExtensions = remoteSSHconfig.get('defaultExtensions', []); - const remotePlatformMap = remoteSSHconfig.get>('remotePlatform', {}); - const remoteServerListenOnSocket = remoteSSHconfig.get('remoteServerListenOnSocket', false)!; - const connectTimeout = remoteSSHconfig.get('connectTimeout', 60)!; - - return vscode.window.withProgress({ - title: `Setting up SSH Host ${sshDest.hostname}`, - location: vscode.ProgressLocation.Notification, - cancellable: false - }, async () => { - try { - const sshconfig = await SSHConfiguration.loadFromFS(); - const sshHostConfig = sshconfig.getHostConfiguration(sshDest.hostname); - const sshHostName = sshHostConfig['HostName'] ? sshHostConfig['HostName'].replace('%h', sshDest.hostname) : sshDest.hostname; - const sshUser = sshHostConfig['User'] || sshDest.user || os.userInfo().username || ''; // https://github.com/openssh/openssh-portable/blob/5ec5504f1d328d5bfa64280cd617c3efec4f78f3/sshconnect.c#L1561-L1562 - const sshPort = sshHostConfig['Port'] ? parseInt(sshHostConfig['Port'], 10) : (sshDest.port || 22); - - this.sshAgentSock = sshHostConfig['IdentityAgent'] || process.env['SSH_AUTH_SOCK'] || (isWindows ? '\\\\.\\pipe\\openssh-ssh-agent' : undefined); - this.sshAgentSock = this.sshAgentSock ? untildify(this.sshAgentSock) : undefined; - const agentForward = enableAgentForwarding && (sshHostConfig['ForwardAgent'] || 'no').toLowerCase() === 'yes'; - const agent = agentForward && this.sshAgentSock ? new ssh2.OpenSSHAgent(this.sshAgentSock) : undefined; - - const preferredAuthentications = sshHostConfig['PreferredAuthentications'] ? sshHostConfig['PreferredAuthentications'].split(',').map(s => s.trim()) : ['publickey', 'password', 'keyboard-interactive']; - - const identityFiles: string[] = (sshHostConfig['IdentityFile'] as unknown as string[]) || []; - const identitiesOnly = (sshHostConfig['IdentitiesOnly'] || 'no').toLowerCase() === 'yes'; - const identityKeys = await gatherIdentityFiles(identityFiles, this.sshAgentSock, identitiesOnly, this.logger); - - // Create proxy jump connections if any - let proxyStream: ssh2.ClientChannel | stream.Duplex | undefined; - if (sshHostConfig['ProxyJump']) { - const proxyJumps = sshHostConfig['ProxyJump'].split(',').filter(i => !!i.trim()) - .map(i => { - const proxy = SSHDestination.parse(i); - const proxyHostConfig = sshconfig.getHostConfiguration(proxy.hostname); - return [proxy, proxyHostConfig] as [SSHDestination, Record]; - }); - for (let i = 0; i < proxyJumps.length; i++) { - const [proxy, proxyHostConfig] = proxyJumps[i]; - const proxyhHostName = proxyHostConfig['HostName'] || proxy.hostname; - const proxyUser = proxyHostConfig['User'] || proxy.user || sshUser; - const proxyPort = proxyHostConfig['Port'] ? parseInt(proxyHostConfig['Port'], 10) : (proxy.port || sshPort); - - const proxyAgentForward = enableAgentForwarding && (proxyHostConfig['ForwardAgent'] || 'no').toLowerCase() === 'yes'; - const proxyAgent = proxyAgentForward && this.sshAgentSock ? new ssh2.OpenSSHAgent(this.sshAgentSock) : undefined; - - const proxyIdentityFiles: string[] = (proxyHostConfig['IdentityFile'] as unknown as string[]) || []; - const proxyIdentitiesOnly = (proxyHostConfig['IdentitiesOnly'] || 'no').toLowerCase() === 'yes'; - const proxyIdentityKeys = await gatherIdentityFiles(proxyIdentityFiles, this.sshAgentSock, proxyIdentitiesOnly, this.logger); - - const proxyAuthHandler = this.getSSHAuthHandler(proxyUser, proxyhHostName, proxyIdentityKeys, preferredAuthentications); - const proxyConnection = new SSHConnection({ - host: !proxyStream ? proxyhHostName : undefined, - port: !proxyStream ? proxyPort : undefined, - sock: proxyStream, - username: proxyUser, - readyTimeout: connectTimeout * 1000, - strictVendor: false, - agentForward: proxyAgentForward, - agent: proxyAgent, - authHandler: (arg0, arg1, arg2) => (proxyAuthHandler(arg0, arg1, arg2), undefined) - }); - this.proxyConnections.push(proxyConnection); - - const nextProxyJump = i < proxyJumps.length - 1 ? proxyJumps[i + 1] : undefined; - const destIP = nextProxyJump ? (nextProxyJump[1]['HostName'] || nextProxyJump[0].hostname) : sshHostName; - const destPort = nextProxyJump ? ((nextProxyJump[1]['Port'] && parseInt(nextProxyJump[1]['Port'], 10)) || nextProxyJump[0].port || 22) : sshPort; - proxyStream = await proxyConnection.forwardOut('127.0.0.1', 0, destIP, destPort); - } - } else if (sshHostConfig['ProxyCommand']) { - let proxyArgs = (sshHostConfig['ProxyCommand'] as unknown as string[]) - .map((arg) => arg.replace('%h', sshHostName).replace('%p', sshPort.toString()).replace('%r', sshUser)); - let proxyCommand = proxyArgs.shift()!; - - let options = {}; - if (isWindows && /\.(bat|cmd)$/.test(proxyCommand)) { - proxyCommand = `"${proxyCommand}"`; - proxyArgs = proxyArgs.map((arg) => arg.includes(' ') ? `"${arg}"` : arg); - options = { shell: true, windowsHide: true, windowsVerbatimArguments: true }; - } - - this.logger.trace(`Spawning ProxyCommand: ${proxyCommand} ${proxyArgs.join(' ')}`); - - const child = cp.spawn(proxyCommand, proxyArgs, options); - proxyStream = stream.Duplex.from({ readable: child.stdout, writable: child.stdin }); - this.proxyCommandProcess = child; - } - - // Create final shh connection - const sshAuthHandler = this.getSSHAuthHandler(sshUser, sshHostName, identityKeys, preferredAuthentications); - - this.sshConnection = new SSHConnection({ - host: !proxyStream ? sshHostName : undefined, - port: !proxyStream ? sshPort : undefined, - sock: proxyStream, - username: sshUser, - readyTimeout: connectTimeout * 1000, - strictVendor: false, - agentForward, - agent, - authHandler: (arg0, arg1, arg2) => (sshAuthHandler(arg0, arg1, arg2), undefined), - }); - await this.sshConnection.connect(); - - const envVariables: Record = {}; - if (agentForward) { - envVariables['SSH_AUTH_SOCK'] = null; - } - - const installResult = await installCodeServer(this.sshConnection, serverDownloadUrlTemplate, defaultExtensions, Object.keys(envVariables), remotePlatformMap[sshDest.hostname], remoteServerListenOnSocket, this.logger); - - for (const key of Object.keys(envVariables)) { - if (installResult[key] !== undefined) { - envVariables[key] = installResult[key]; - } - } - - // Update terminal env variables - this.context.environmentVariableCollection.persistent = false; - for (const [key, value] of Object.entries(envVariables)) { - if (value) { - this.context.environmentVariableCollection.replace(key, value); - } - } - - if (enableDynamicForwarding) { - const socksPort = await findRandomPort(); - this.socksTunnel = await this.sshConnection!.addTunnel({ - name: `ssh_tunnel_socks_${socksPort}`, - localPort: socksPort, - socks: true - }); - } - - const tunnelConfig = await this.openTunnel(0, installResult.listeningOn); - this.tunnels.push(tunnelConfig); - - // Enable ports view - vscode.commands.executeCommand('setContext', 'forwardedPortsViewEnabled', true); - - this.labelFormatterDisposable?.dispose(); - this.labelFormatterDisposable = vscode.workspace.registerResourceLabelFormatter({ - scheme: 'vscode-remote', - authority: `${REMOTE_SSH_AUTHORITY}+*`, - formatting: { - label: '${path}', - separator: '/', - tildify: true, - workspaceSuffix: `SSH: ${sshDest.hostname}` + (sshDest.port && sshDest.port !== 22 ? `:${sshDest.port}` : '') - } - }); - - const resolvedResult: vscode.ResolverResult = new vscode.ResolvedAuthority('127.0.0.1', tunnelConfig.localPort, installResult.connectionToken); - resolvedResult.extensionHostEnv = envVariables; - return resolvedResult; - } catch (e: unknown) { - this.logger.error(`Error resolving authority`, e); - - // Initial connection - if (context.resolveAttempt === 1) { - this.logger.show(); - - const closeRemote = 'Close Remote'; - const retry = 'Retry'; - const result = await vscode.window.showErrorMessage(`Could not establish connection to "${sshDest.hostname}"`, { modal: true }, closeRemote, retry); - if (result === closeRemote) { - await vscode.commands.executeCommand('workbench.action.remote.close'); - } else if (result === retry) { - await vscode.commands.executeCommand('workbench.action.reloadWindow'); - } - } - - if (e instanceof ServerInstallError || !(e instanceof Error)) { - throw vscode.RemoteAuthorityResolverError.NotAvailable(e instanceof Error ? e.message : String(e)); - } else { - throw vscode.RemoteAuthorityResolverError.TemporarilyNotAvailable(e.message); - } - } - }); - } - - private async openTunnel(localPort: number, remotePortOrSocketPath: number | string) { - localPort = localPort > 0 ? localPort : await findRandomPort(); - - const disposables: vscode.Disposable[] = []; - const remotePort = typeof remotePortOrSocketPath === 'number' ? remotePortOrSocketPath : undefined; - const remoteSocketPath = typeof remotePortOrSocketPath === 'string' ? remotePortOrSocketPath : undefined; - if (this.socksTunnel && remotePort) { - const forwardingServer = await new Promise((resolve, reject) => { - this.logger.trace(`Creating forwarding server ${localPort}(local) => ${this.socksTunnel!.localPort!}(socks) => ${remotePort}(remote)`); - const socksOptions: SocksClientOptions = { - proxy: { - host: '127.0.0.1', - port: this.socksTunnel!.localPort!, - type: 5 - }, - command: 'connect', - destination: { - host: '127.0.0.1', - port: remotePort - } - }; - const server: net.Server = net.createServer() - .on('error', reject) - .on('connection', async (socket: net.Socket) => { - try { - const socksConn = await SocksClient.createConnection(socksOptions); - socket.pipe(socksConn.socket); - socksConn.socket.pipe(socket); - } catch (error) { - this.logger.error(`Error while creating SOCKS connection`, error); - } - }) - .on('listening', () => resolve(server)) - .listen(localPort); - }); - disposables.push({ - dispose: () => forwardingServer.close(() => { - this.logger.trace(`SOCKS forwading server closed`); - }), - }); - } else { - this.logger.trace(`Opening tunnel ${localPort}(local) => ${remotePortOrSocketPath}(remote)`); - const tunnelConfig = await this.sshConnection!.addTunnel({ - name: `ssh_tunnel_${localPort}_${remotePortOrSocketPath}`, - remoteAddr: '127.0.0.1', - remotePort, - remoteSocketPath, - localPort - }); - disposables.push({ - dispose: () => { - this.sshConnection?.closeTunnel(tunnelConfig.name); - this.logger.trace(`Tunnel ${tunnelConfig.name} closed`); - } - }); - } - - return new TunnelInfo(localPort, remotePortOrSocketPath, disposables); - } - - private getSSHAuthHandler(sshUser: string, sshHostName: string, identityKeys: SSHKey[], preferredAuthentications: string[]) { - let passwordRetryCount = PASSWORD_RETRY_COUNT; - let keyboardRetryCount = PASSWORD_RETRY_COUNT; - identityKeys = identityKeys.slice(); - return async (methodsLeft: string[] | null, _partialSuccess: boolean | null, callback: (nextAuth: ssh2.AuthHandlerResult) => void) => { - if (methodsLeft === null) { - this.logger.info(`Trying no-auth authentication`); - - return callback({ - type: 'none', - username: sshUser, - }); - } - if (methodsLeft.includes('publickey') && identityKeys.length && preferredAuthentications.includes('publickey')) { - const identityKey = identityKeys.shift()!; - - this.logger.info(`Trying publickey authentication: ${identityKey.filename} ${identityKey.parsedKey.type} SHA256:${identityKey.fingerprint}`); - - if (identityKey.agentSupport) { - return callback({ - type: 'agent', - username: sshUser, - agent: new class extends ssh2.OpenSSHAgent { - // Only return the current key - override getIdentities(callback: (err: Error | undefined, publicKeys?: ParsedKey[]) => void): void { - callback(undefined, [identityKey.parsedKey]); - } - }(this.sshAgentSock!) - }); - } - if (identityKey.isPrivate) { - return callback({ - type: 'publickey', - username: sshUser, - key: identityKey.parsedKey - }); - } - if (!await fileExists(identityKey.filename)) { - // Try next identity file - return callback(null as any); - } - - const keyBuffer = await fs.promises.readFile(identityKey.filename); - let result = ssh2.utils.parseKey(keyBuffer); // First try without passphrase - if (result instanceof Error && result.message === 'Encrypted private OpenSSH key detected, but no passphrase given') { - let passphraseRetryCount = PASSPHRASE_RETRY_COUNT; - while (result instanceof Error && passphraseRetryCount > 0) { - const passphrase = await vscode.window.showInputBox({ - title: `Enter passphrase for ${identityKey.filename}`, - password: true, - ignoreFocusOut: true - }); - if (!passphrase) { - break; - } - result = ssh2.utils.parseKey(keyBuffer, passphrase); - passphraseRetryCount--; - } - } - if (!result || result instanceof Error) { - // Try next identity file - return callback(null as any); - } - - const key = Array.isArray(result) ? result[0] : result; - return callback({ - type: 'publickey', - username: sshUser, - key - }); - } - if (methodsLeft.includes('password') && passwordRetryCount > 0 && preferredAuthentications.includes('password')) { - if (passwordRetryCount === PASSWORD_RETRY_COUNT) { - this.logger.info(`Trying password authentication`); - } - - const password = await vscode.window.showInputBox({ - title: `Enter password for ${sshUser}@${sshHostName}`, - password: true, - ignoreFocusOut: true - }); - passwordRetryCount--; - - return callback(password - ? { - type: 'password', - username: sshUser, - password - } - : false); - } - if (methodsLeft.includes('keyboard-interactive') && keyboardRetryCount > 0 && preferredAuthentications.includes('keyboard-interactive')) { - if (keyboardRetryCount === PASSWORD_RETRY_COUNT) { - this.logger.info(`Trying keyboard-interactive authentication`); - } - - return callback({ - type: 'keyboard-interactive', - username: sshUser, - prompt: async (_name, _instructions, _instructionsLang, prompts, finish) => { - const responses: string[] = []; - for (const prompt of prompts) { - const response = await vscode.window.showInputBox({ - title: `(${sshUser}@${sshHostName}) ${prompt.prompt}`, - password: !prompt.echo, - ignoreFocusOut: true - }); - if (response === undefined) { - keyboardRetryCount = 0; - break; - } - responses.push(response); - } - keyboardRetryCount--; - finish(responses); - } - }); - } - - callback(false); - }; - } - - dispose() { - disposeAll(this.tunnels); - // If there's proxy connections then just close the parent connection - if (this.proxyConnections.length) { - this.proxyConnections[0].close(); - } else { - this.sshConnection?.close(); - } - this.proxyCommandProcess?.kill(); - this.labelFormatterDisposable?.dispose(); - } + private proxyConnections: SSHConnection[] = []; + private sshConnection: SSHConnection | undefined; + private sshAgentSock: string | undefined; + private proxyCommandProcess: cp.ChildProcessWithoutNullStreams | undefined; + + private socksTunnel: SSHTunnelConfig | undefined; + private tunnels: TunnelInfo[] = []; + + private labelFormatterDisposable: vscode.Disposable | undefined; + + constructor( + readonly context: vscode.ExtensionContext, + readonly logger: Log + ) { + } + + resolve(authority: string, context: vscode.RemoteAuthorityResolverContext): Thenable { + const [type, dest] = authority.split('+'); + if (type !== REMOTE_SSH_AUTHORITY) { + throw new Error(`Invalid authority type for SSH resolver: ${type}`); + } + + this.logger.info(`Resolving ssh remote authority '${authority}' (attemp #${context.resolveAttempt})`); + + const sshDest = SSHDestination.parseEncoded(dest); + + // It looks like default values are not loaded yet when resolving a remote, + // so let's hardcode the default values here + const remoteSSHconfig = vscode.workspace.getConfiguration('remoteSSH'); + const enableDynamicForwarding = remoteSSHconfig.get('enableDynamicForwarding', true)!; + const enableAgentForwarding = remoteSSHconfig.get('enableAgentForwarding', true)!; + const serverDownloadUrlTemplate = remoteSSHconfig.get('serverDownloadUrlTemplate'); + const defaultExtensions = remoteSSHconfig.get('defaultExtensions', []); + const remotePlatformMap = remoteSSHconfig.get>('remotePlatform', {}); + const remoteServerListenOnSocket = remoteSSHconfig.get('remoteServerListenOnSocket', false)!; + const connectTimeout = remoteSSHconfig.get('connectTimeout', 60)!; + + return vscode.window.withProgress({ + title: `Setting up SSH Host ${sshDest.hostname}`, + location: vscode.ProgressLocation.Notification, + cancellable: false + }, async () => { + try { + const sshconfig = await SSHConfiguration.loadFromFS(); + const sshHostConfig = sshconfig.getHostConfiguration(sshDest.hostname); + const sshHostName = sshHostConfig['HostName'] ? sshHostConfig['HostName'].replace('%h', sshDest.hostname) : sshDest.hostname; + const sshUser = sshHostConfig['User'] || sshDest.user || os.userInfo().username || ''; // https://github.com/openssh/openssh-portable/blob/5ec5504f1d328d5bfa64280cd617c3efec4f78f3/sshconnect.c#L1561-L1562 + const sshPort = sshHostConfig['Port'] ? parseInt(sshHostConfig['Port'], 10) : (sshDest.port || 22); + + this.sshAgentSock = sshHostConfig['IdentityAgent'] || process.env['SSH_AUTH_SOCK'] || (isWindows ? '\\\\.\\pipe\\openssh-ssh-agent' : undefined); + this.sshAgentSock = this.sshAgentSock ? untildify(this.sshAgentSock) : undefined; + const agentForward = enableAgentForwarding && (sshHostConfig['ForwardAgent'] || 'no').toLowerCase() === 'yes'; + const agent = agentForward && this.sshAgentSock ? new ssh2.OpenSSHAgent(this.sshAgentSock) : undefined; + + const preferredAuthentications = sshHostConfig['PreferredAuthentications'] ? sshHostConfig['PreferredAuthentications'].split(',').map(s => s.trim()) : ['publickey', 'password', 'keyboard-interactive']; + + const identityFiles: string[] = (sshHostConfig['IdentityFile'] as unknown as string[]) || []; + const identitiesOnly = (sshHostConfig['IdentitiesOnly'] || 'no').toLowerCase() === 'yes'; + const identityKeys = await gatherIdentityFiles(identityFiles, this.sshAgentSock, identitiesOnly, this.logger); + + // Create proxy jump connections if any + let proxyStream: ssh2.ClientChannel | stream.Duplex | undefined; + if (sshHostConfig['ProxyJump']) { + const proxyJumps = sshHostConfig['ProxyJump'].split(',').filter(i => !!i.trim()) + .map(i => { + const proxy = SSHDestination.parse(i); + const proxyHostConfig = sshconfig.getHostConfiguration(proxy.hostname); + return [proxy, proxyHostConfig] as [SSHDestination, Record]; + }); + for (let i = 0; i < proxyJumps.length; i++) { + const [proxy, proxyHostConfig] = proxyJumps[i]; + const proxyhHostName = proxyHostConfig['HostName'] || proxy.hostname; + const proxyUser = proxyHostConfig['User'] || proxy.user || sshUser; + const proxyPort = proxyHostConfig['Port'] ? parseInt(proxyHostConfig['Port'], 10) : (proxy.port || sshPort); + + const proxyAgentForward = enableAgentForwarding && (proxyHostConfig['ForwardAgent'] || 'no').toLowerCase() === 'yes'; + const proxyAgent = proxyAgentForward && this.sshAgentSock ? new ssh2.OpenSSHAgent(this.sshAgentSock) : undefined; + + const proxyIdentityFiles: string[] = (proxyHostConfig['IdentityFile'] as unknown as string[]) || []; + const proxyIdentitiesOnly = (proxyHostConfig['IdentitiesOnly'] || 'no').toLowerCase() === 'yes'; + const proxyIdentityKeys = await gatherIdentityFiles(proxyIdentityFiles, this.sshAgentSock, proxyIdentitiesOnly, this.logger); + + const proxyAuthHandler = this.getSSHAuthHandler(proxyUser, proxyhHostName, proxyIdentityKeys, preferredAuthentications); + const proxyConnection = new SSHConnection({ + host: !proxyStream ? proxyhHostName : undefined, + port: !proxyStream ? proxyPort : undefined, + sock: proxyStream, + username: proxyUser, + readyTimeout: connectTimeout * 1000, + strictVendor: false, + agentForward: proxyAgentForward, + agent: proxyAgent, + authHandler: (arg0, arg1, arg2) => (proxyAuthHandler(arg0, arg1, arg2), undefined) + }); + this.proxyConnections.push(proxyConnection); + + const nextProxyJump = i < proxyJumps.length - 1 ? proxyJumps[i + 1] : undefined; + const destIP = nextProxyJump ? (nextProxyJump[1]['HostName'] || nextProxyJump[0].hostname) : sshHostName; + const destPort = nextProxyJump ? ((nextProxyJump[1]['Port'] && parseInt(nextProxyJump[1]['Port'], 10)) || nextProxyJump[0].port || 22) : sshPort; + proxyStream = await proxyConnection.forwardOut('127.0.0.1', 0, destIP, destPort); + } + } else if (sshHostConfig['ProxyCommand']) { + let proxyArgs = (sshHostConfig['ProxyCommand'] as unknown as string[]) + .map((arg) => arg.replace('%h', sshHostName).replace('%p', sshPort.toString()).replace('%r', sshUser)); + let proxyCommand = proxyArgs.shift()!; + + let options = {}; + if (isWindows && /\.(bat|cmd)$/.test(proxyCommand)) { + proxyCommand = `"${proxyCommand}"`; + proxyArgs = proxyArgs.map((arg) => arg.includes(' ') ? `"${arg}"` : arg); + options = { shell: true, windowsHide: true, windowsVerbatimArguments: true }; + } + + this.logger.trace(`Spawning ProxyCommand: ${proxyCommand} ${proxyArgs.join(' ')}`); + + const child = cp.spawn(proxyCommand, proxyArgs, options); + proxyStream = stream.Duplex.from({ readable: child.stdout, writable: child.stdin }); + this.proxyCommandProcess = child; + } + + // Create final shh connection + const sshAuthHandler = this.getSSHAuthHandler(sshUser, sshHostName, identityKeys, preferredAuthentications); + + this.sshConnection = new SSHConnection({ + host: !proxyStream ? sshHostName : undefined, + port: !proxyStream ? sshPort : undefined, + sock: proxyStream, + username: sshUser, + readyTimeout: connectTimeout * 1000, + strictVendor: false, + agentForward, + agent, + authHandler: (arg0, arg1, arg2) => (sshAuthHandler(arg0, arg1, arg2), undefined), + }); + await this.sshConnection.connect(); + + const envVariables: Record = {}; + if (agentForward) { + envVariables['SSH_AUTH_SOCK'] = null; + } + + const installResult = await installCodeServer(this.sshConnection, serverDownloadUrlTemplate, defaultExtensions, Object.keys(envVariables), remotePlatformMap[sshDest.hostname], remoteServerListenOnSocket, this.logger); + + for (const key of Object.keys(envVariables)) { + if (installResult[key] !== undefined) { + envVariables[key] = installResult[key]; + } + } + + // Update terminal env variables + this.context.environmentVariableCollection.persistent = false; + for (const [key, value] of Object.entries(envVariables)) { + if (value) { + this.context.environmentVariableCollection.replace(key, value); + } + } + + if (enableDynamicForwarding) { + const socksPort = await findRandomPort(); + this.socksTunnel = await this.sshConnection!.addTunnel({ + name: `ssh_tunnel_socks_${socksPort}`, + localPort: socksPort, + socks: true + }); + } + + const tunnelConfig = await this.openTunnel(0, installResult.listeningOn); + this.tunnels.push(tunnelConfig); + + // Enable ports view + vscode.commands.executeCommand('setContext', 'forwardedPortsViewEnabled', true); + + this.labelFormatterDisposable?.dispose(); + this.labelFormatterDisposable = vscode.workspace.registerResourceLabelFormatter({ + scheme: 'vscode-remote', + authority: `${REMOTE_SSH_AUTHORITY}+*`, + formatting: { + label: '${path}', + separator: '/', + tildify: true, + workspaceSuffix: `SSH: ${sshDest.hostname}` + (sshDest.port && sshDest.port !== 22 ? `:${sshDest.port}` : '') + } + }); + + const resolvedResult: vscode.ResolverResult = new vscode.ResolvedAuthority('127.0.0.1', tunnelConfig.localPort, installResult.connectionToken); + resolvedResult.extensionHostEnv = envVariables; + return resolvedResult; + } catch (e: unknown) { + this.logger.error(`Error resolving authority`, e); + + // Initial connection + if (context.resolveAttempt === 1) { + this.logger.show(); + + const closeRemote = 'Close Remote'; + const retry = 'Retry'; + const result = await vscode.window.showErrorMessage(`Could not establish connection to "${sshDest.hostname}"`, { modal: true }, closeRemote, retry); + if (result === closeRemote) { + await vscode.commands.executeCommand('workbench.action.remote.close'); + } else if (result === retry) { + await vscode.commands.executeCommand('workbench.action.reloadWindow'); + } + } + + if (e instanceof ServerInstallError || !(e instanceof Error)) { + throw vscode.RemoteAuthorityResolverError.NotAvailable(e instanceof Error ? e.message : String(e)); + } else { + throw vscode.RemoteAuthorityResolverError.TemporarilyNotAvailable(e.message); + } + } + }); + } + + private async openTunnel(localPort: number, remotePortOrSocketPath: number | string) { + localPort = localPort > 0 ? localPort : await findRandomPort(); + + const disposables: vscode.Disposable[] = []; + const remotePort = typeof remotePortOrSocketPath === 'number' ? remotePortOrSocketPath : undefined; + const remoteSocketPath = typeof remotePortOrSocketPath === 'string' ? remotePortOrSocketPath : undefined; + if (this.socksTunnel && remotePort) { + const forwardingServer = await new Promise((resolve, reject) => { + this.logger.trace(`Creating forwarding server ${localPort}(local) => ${this.socksTunnel!.localPort!}(socks) => ${remotePort}(remote)`); + const socksOptions: SocksClientOptions = { + proxy: { + host: '127.0.0.1', + port: this.socksTunnel!.localPort!, + type: 5 + }, + command: 'connect', + destination: { + host: '127.0.0.1', + port: remotePort + } + }; + const server: net.Server = net.createServer() + .on('error', reject) + .on('connection', async (socket: net.Socket) => { + try { + const socksConn = await SocksClient.createConnection(socksOptions); + socket.pipe(socksConn.socket); + socksConn.socket.pipe(socket); + } catch (error) { + this.logger.error(`Error while creating SOCKS connection`, error); + } + }) + .on('listening', () => resolve(server)) + .listen(localPort); + }); + disposables.push({ + dispose: () => forwardingServer.close(() => { + this.logger.trace(`SOCKS forwading server closed`); + }), + }); + } else { + this.logger.trace(`Opening tunnel ${localPort}(local) => ${remotePortOrSocketPath}(remote)`); + const tunnelConfig = await this.sshConnection!.addTunnel({ + name: `ssh_tunnel_${localPort}_${remotePortOrSocketPath}`, + remoteAddr: '127.0.0.1', + remotePort, + remoteSocketPath, + localPort + }); + disposables.push({ + dispose: () => { + this.sshConnection?.closeTunnel(tunnelConfig.name); + this.logger.trace(`Tunnel ${tunnelConfig.name} closed`); + } + }); + } + + return new TunnelInfo(localPort, remotePortOrSocketPath, disposables); + } + + private getSSHAuthHandler(sshUser: string, sshHostName: string, identityKeys: SSHKey[], preferredAuthentications: string[]) { + let passwordRetryCount = PASSWORD_RETRY_COUNT; + let keyboardRetryCount = PASSWORD_RETRY_COUNT; + identityKeys = identityKeys.slice(); + return async (methodsLeft: string[] | null, _partialSuccess: boolean | null, callback: (nextAuth: ssh2.AuthHandlerResult) => void) => { + if (methodsLeft === null) { + this.logger.info(`Trying no-auth authentication`); + + return callback({ + type: 'none', + username: sshUser, + }); + } + if (methodsLeft.includes('publickey') && identityKeys.length && preferredAuthentications.includes('publickey')) { + const identityKey = identityKeys.shift()!; + + this.logger.info(`Trying publickey authentication: ${identityKey.filename} ${identityKey.parsedKey.type} SHA256:${identityKey.fingerprint}`); + + if (identityKey.agentSupport) { + return callback({ + type: 'agent', + username: sshUser, + agent: new class extends ssh2.OpenSSHAgent { + // Only return the current key + override getIdentities(callback: (err: Error | undefined, publicKeys?: ParsedKey[]) => void): void { + callback(undefined, [identityKey.parsedKey]); + } + }(this.sshAgentSock!) + }); + } + if (identityKey.isPrivate) { + return callback({ + type: 'publickey', + username: sshUser, + key: identityKey.parsedKey + }); + } + if (!await fileExists(identityKey.filename)) { + // Try next identity file + return callback(null as any); + } + + const keyBuffer = await fs.promises.readFile(identityKey.filename); + let result = ssh2.utils.parseKey(keyBuffer); // First try without passphrase + if (result instanceof Error && result.message === 'Encrypted private OpenSSH key detected, but no passphrase given') { + let passphraseRetryCount = PASSPHRASE_RETRY_COUNT; + while (result instanceof Error && passphraseRetryCount > 0) { + const passphrase = await vscode.window.showInputBox({ + title: `Enter passphrase for ${identityKey.filename}`, + password: true, + ignoreFocusOut: true + }); + if (!passphrase) { + break; + } + result = ssh2.utils.parseKey(keyBuffer, passphrase); + passphraseRetryCount--; + } + } + if (!result || result instanceof Error) { + // Try next identity file + return callback(null as any); + } + + const key = Array.isArray(result) ? result[0] : result; + return callback({ + type: 'publickey', + username: sshUser, + key + }); + } + if (methodsLeft.includes('password') && passwordRetryCount > 0 && preferredAuthentications.includes('password')) { + if (passwordRetryCount === PASSWORD_RETRY_COUNT) { + this.logger.info(`Trying password authentication`); + } + + const password = await vscode.window.showInputBox({ + title: `Enter password for ${sshUser}@${sshHostName}`, + password: true, + ignoreFocusOut: true + }); + passwordRetryCount--; + + return callback(password + ? { + type: 'password', + username: sshUser, + password + } + : false); + } + if (methodsLeft.includes('keyboard-interactive') && keyboardRetryCount > 0 && preferredAuthentications.includes('keyboard-interactive')) { + if (keyboardRetryCount === PASSWORD_RETRY_COUNT) { + this.logger.info(`Trying keyboard-interactive authentication`); + } + + return callback({ + type: 'keyboard-interactive', + username: sshUser, + prompt: async (_name, _instructions, _instructionsLang, prompts, finish) => { + const responses: string[] = []; + for (const prompt of prompts) { + const response = await vscode.window.showInputBox({ + title: `(${sshUser}@${sshHostName}) ${prompt.prompt}`, + password: !prompt.echo, + ignoreFocusOut: true + }); + if (response === undefined) { + keyboardRetryCount = 0; + break; + } + responses.push(response); + } + keyboardRetryCount--; + finish(responses); + } + }); + } + + callback(false); + }; + } + + dispose() { + disposeAll(this.tunnels); + // If there's proxy connections then just close the parent connection + if (this.proxyConnections.length) { + this.proxyConnections[0].close(); + } else { + this.sshConnection?.close(); + } + this.proxyCommandProcess?.kill(); + this.labelFormatterDisposable?.dispose(); + } } diff --git a/extensions/open-remote-ssh/src/hostTreeView.ts b/extensions/open-remote-ssh/src/hostTreeView.ts index c32c344d00e..452acf3bcd5 100644 --- a/extensions/open-remote-ssh/src/hostTreeView.ts +++ b/extensions/open-remote-ssh/src/hostTreeView.ts @@ -7,99 +7,99 @@ import { addNewHost, openRemoteSSHLocationWindow, openRemoteSSHWindow, openSSHCo import SSHDestination from './ssh/sshDestination'; class HostItem { - constructor( - public hostname: string, - public locations: string[] - ) { - } + constructor( + public hostname: string, + public locations: string[] + ) { + } } class HostLocationItem { - constructor( - public path: string, - public hostname: string - ) { - } + constructor( + public path: string, + public hostname: string + ) { + } } type DataTreeItem = HostItem | HostLocationItem; export class HostTreeDataProvider extends Disposable implements vscode.TreeDataProvider { - private readonly _onDidChangeTreeData = this._register(new vscode.EventEmitter()); - public readonly onDidChangeTreeData = this._onDidChangeTreeData.event; + private readonly _onDidChangeTreeData = this._register(new vscode.EventEmitter()); + public readonly onDidChangeTreeData = this._onDidChangeTreeData.event; - constructor( - private locationHistory: RemoteLocationHistory - ) { - super(); + constructor( + private locationHistory: RemoteLocationHistory + ) { + super(); - this._register(vscode.commands.registerCommand('openremotessh.explorer.add', () => addNewHost())); - this._register(vscode.commands.registerCommand('openremotessh.explorer.configure', () => openSSHConfigFile())); - this._register(vscode.commands.registerCommand('openremotessh.explorer.refresh', () => this.refresh())); - this._register(vscode.commands.registerCommand('openremotessh.explorer.emptyWindowInNewWindow', e => this.openRemoteSSHWindow(e, false))); - this._register(vscode.commands.registerCommand('openremotessh.explorer.emptyWindowInCurrentWindow', e => this.openRemoteSSHWindow(e, true))); - this._register(vscode.commands.registerCommand('openremotessh.explorer.reopenFolderInNewWindow', e => this.openRemoteSSHLocationWindow(e, false))); - this._register(vscode.commands.registerCommand('openremotessh.explorer.reopenFolderInCurrentWindow', e => this.openRemoteSSHLocationWindow(e, true))); - this._register(vscode.commands.registerCommand('openremotessh.explorer.deleteFolderHistoryItem', e => this.deleteHostLocation(e))); + this._register(vscode.commands.registerCommand('openremotessh.explorer.add', () => addNewHost())); + this._register(vscode.commands.registerCommand('openremotessh.explorer.configure', () => openSSHConfigFile())); + this._register(vscode.commands.registerCommand('openremotessh.explorer.refresh', () => this.refresh())); + this._register(vscode.commands.registerCommand('openremotessh.explorer.emptyWindowInNewWindow', e => this.openRemoteSSHWindow(e, false))); + this._register(vscode.commands.registerCommand('openremotessh.explorer.emptyWindowInCurrentWindow', e => this.openRemoteSSHWindow(e, true))); + this._register(vscode.commands.registerCommand('openremotessh.explorer.reopenFolderInNewWindow', e => this.openRemoteSSHLocationWindow(e, false))); + this._register(vscode.commands.registerCommand('openremotessh.explorer.reopenFolderInCurrentWindow', e => this.openRemoteSSHLocationWindow(e, true))); + this._register(vscode.commands.registerCommand('openremotessh.explorer.deleteFolderHistoryItem', e => this.deleteHostLocation(e))); - this._register(vscode.workspace.onDidChangeConfiguration(e => { - if (e.affectsConfiguration('remote.SSH.configFile')) { - this.refresh(); - } - })); - this._register(vscode.workspace.onDidSaveTextDocument(e => { - if (e.uri.fsPath === getSSHConfigPath()) { - this.refresh(); - } - })); - } + this._register(vscode.workspace.onDidChangeConfiguration(e => { + if (e.affectsConfiguration('remoteSSH.configFile')) { + this.refresh(); + } + })); + this._register(vscode.workspace.onDidSaveTextDocument(e => { + if (e.uri.fsPath === getSSHConfigPath()) { + this.refresh(); + } + })); + } - getTreeItem(element: DataTreeItem): vscode.TreeItem { - if (element instanceof HostLocationItem) { - const label = path.posix.basename(element.path).replace(/\.code-workspace$/, ' (Workspace)'); - const treeItem = new vscode.TreeItem(label); - treeItem.description = path.posix.dirname(element.path); - treeItem.iconPath = new vscode.ThemeIcon('folder'); - treeItem.contextValue = 'openremotessh.explorer.folder'; - return treeItem; - } + getTreeItem(element: DataTreeItem): vscode.TreeItem { + if (element instanceof HostLocationItem) { + const label = path.posix.basename(element.path).replace(/\.code-workspace$/, ' (Workspace)'); + const treeItem = new vscode.TreeItem(label); + treeItem.description = path.posix.dirname(element.path); + treeItem.iconPath = new vscode.ThemeIcon('folder'); + treeItem.contextValue = 'openremotessh.explorer.folder'; + return treeItem; + } - const treeItem = new vscode.TreeItem(element.hostname); - treeItem.collapsibleState = element.locations.length ? vscode.TreeItemCollapsibleState.Collapsed : vscode.TreeItemCollapsibleState.None; - treeItem.iconPath = new vscode.ThemeIcon('vm'); - treeItem.contextValue = 'openremotessh.explorer.host'; - return treeItem; - } + const treeItem = new vscode.TreeItem(element.hostname); + treeItem.collapsibleState = element.locations.length ? vscode.TreeItemCollapsibleState.Collapsed : vscode.TreeItemCollapsibleState.None; + treeItem.iconPath = new vscode.ThemeIcon('vm'); + treeItem.contextValue = 'openremotessh.explorer.host'; + return treeItem; + } - async getChildren(element?: HostItem): Promise { - if (!element) { - const sshConfigFile = await SSHConfiguration.loadFromFS(); - const hosts = sshConfigFile.getAllConfiguredHosts(); - return hosts.map(hostname => new HostItem(hostname, this.locationHistory.getHistory(hostname))); - } - if (element instanceof HostItem) { - return element.locations.map(location => new HostLocationItem(location, element.hostname)); - } - return []; - } + async getChildren(element?: HostItem): Promise { + if (!element) { + const sshConfigFile = await SSHConfiguration.loadFromFS(); + const hosts = sshConfigFile.getAllConfiguredHosts(); + return hosts.map(hostname => new HostItem(hostname, this.locationHistory.getHistory(hostname))); + } + if (element instanceof HostItem) { + return element.locations.map(location => new HostLocationItem(location, element.hostname)); + } + return []; + } - private refresh() { - this._onDidChangeTreeData.fire(); - } + private refresh() { + this._onDidChangeTreeData.fire(); + } - private async deleteHostLocation(element: HostLocationItem) { - await this.locationHistory.removeLocation(element.hostname, element.path); - this.refresh(); - } + private async deleteHostLocation(element: HostLocationItem) { + await this.locationHistory.removeLocation(element.hostname, element.path); + this.refresh(); + } - private async openRemoteSSHWindow(element: HostItem, reuseWindow: boolean) { - const sshDest = new SSHDestination(element.hostname); - openRemoteSSHWindow(sshDest.toEncodedString(), reuseWindow); - } + private async openRemoteSSHWindow(element: HostItem, reuseWindow: boolean) { + const sshDest = new SSHDestination(element.hostname); + openRemoteSSHWindow(sshDest.toEncodedString(), reuseWindow); + } - private async openRemoteSSHLocationWindow(element: HostLocationItem, reuseWindow: boolean) { - const sshDest = new SSHDestination(element.hostname); - openRemoteSSHLocationWindow(sshDest.toEncodedString(), element.path, reuseWindow); - } + private async openRemoteSSHLocationWindow(element: HostLocationItem, reuseWindow: boolean) { + const sshDest = new SSHDestination(element.hostname); + openRemoteSSHLocationWindow(sshDest.toEncodedString(), element.path, reuseWindow); + } } diff --git a/extensions/open-remote-ssh/src/serverConfig.ts b/extensions/open-remote-ssh/src/serverConfig.ts index a878c288ef9..bdbea9c4b91 100644 --- a/extensions/open-remote-ssh/src/serverConfig.ts +++ b/extensions/open-remote-ssh/src/serverConfig.ts @@ -26,7 +26,7 @@ export interface IServerConfig { export async function getVSCodeServerConfig(): Promise { const productJson = await getVSCodeProductJson(); - const customServerBinaryName = vscode.workspace.getConfiguration('remote.SSH.experimental').get('serverBinaryName', ''); + const customServerBinaryName = vscode.workspace.getConfiguration('remoteSSH.experimental').get('serverBinaryName', ''); const version = `${positron.version}-${positron.buildNumber}`; diff --git a/extensions/open-remote-ssh/src/ssh/sshConfig.ts b/extensions/open-remote-ssh/src/ssh/sshConfig.ts index 3a69fa7241a..be536c40e87 100644 --- a/extensions/open-remote-ssh/src/ssh/sshConfig.ts +++ b/extensions/open-remote-ssh/src/ssh/sshConfig.ts @@ -11,115 +11,115 @@ const systemSSHConfig = isWindows ? path.resolve(process.env.ALLUSERSPROFILE || const defaultSSHConfigPath = path.resolve(os.homedir(), '.ssh/config'); export function getSSHConfigPath() { - const sshConfigPath = vscode.workspace.getConfiguration('remote.SSH').get('configFile'); - return sshConfigPath ? untildify(sshConfigPath) : defaultSSHConfigPath; + const sshConfigPath = vscode.workspace.getConfiguration('remoteSSH').get('configFile'); + return sshConfigPath ? untildify(sshConfigPath) : defaultSSHConfigPath; } function isDirective(line: Line): line is Directive { - return line.type === SSHConfig.DIRECTIVE; + return line.type === SSHConfig.DIRECTIVE; } function isHostSection(line: Line): line is Section { - return isDirective(line) && line.param === 'Host' && !!line.value && !!(line as Section).config; + return isDirective(line) && line.param === 'Host' && !!line.value && !!(line as Section).config; } function isIncludeDirective(line: Line): line is Section { - return isDirective(line) && line.param === 'Include' && !!line.value; + return isDirective(line) && line.param === 'Include' && !!line.value; } const SSH_CONFIG_PROPERTIES: Record = { - 'host': 'Host', - 'hostname': 'HostName', - 'user': 'User', - 'port': 'Port', - 'identityagent': 'IdentityAgent', - 'identitiesonly': 'IdentitiesOnly', - 'identityfile': 'IdentityFile', - 'forwardagent': 'ForwardAgent', - 'preferredauthentications': 'PreferredAuthentications', - 'proxyjump': 'ProxyJump', - 'proxycommand': 'ProxyCommand', - 'include': 'Include', + 'host': 'Host', + 'hostname': 'HostName', + 'user': 'User', + 'port': 'Port', + 'identityagent': 'IdentityAgent', + 'identitiesonly': 'IdentitiesOnly', + 'identityfile': 'IdentityFile', + 'forwardagent': 'ForwardAgent', + 'preferredauthentications': 'PreferredAuthentications', + 'proxyjump': 'ProxyJump', + 'proxycommand': 'ProxyCommand', + 'include': 'Include', }; function normalizeProp(prop: Directive) { - prop.param = SSH_CONFIG_PROPERTIES[prop.param.toLowerCase()] || prop.param; + prop.param = SSH_CONFIG_PROPERTIES[prop.param.toLowerCase()] || prop.param; } function normalizeSSHConfig(config: SSHConfig) { - for (const line of config) { - if (isDirective(line)) { - normalizeProp(line); - } - if (isHostSection(line)) { - normalizeSSHConfig(line.config); - } - } - return config; + for (const line of config) { + if (isDirective(line)) { + normalizeProp(line); + } + if (isHostSection(line)) { + normalizeSSHConfig(line.config); + } + } + return config; } async function parseSSHConfigFromFile(filePath: string, userConfig: boolean) { - let content = ''; - if (await fileExists(filePath)) { - content = (await fs.promises.readFile(filePath, 'utf8')).trim(); - } - const config = normalizeSSHConfig(SSHConfig.parse(content)); - - const includedConfigs: [number, SSHConfig[]][] = []; - for (let i = 0; i < config.length; i++) { - const line = config[i]; - if (isIncludeDirective(line)) { - const values = (line.value as string).split(',').map(s => s.trim()); - const configs: SSHConfig[] = []; - for (const value of values) { - const includePaths = await glob(normalizeToSlash(untildify(value)), { - absolute: true, - cwd: normalizeToSlash(path.dirname(userConfig ? defaultSSHConfigPath : systemSSHConfig)) - }); - for (const p of includePaths) { - configs.push(await parseSSHConfigFromFile(p, userConfig)); - } - } - includedConfigs.push([i, configs]); - } - } - for (const [idx, includeConfigs] of includedConfigs.reverse()) { - config.splice(idx, 1, ...includeConfigs.flat()); - } - - return config; + let content = ''; + if (await fileExists(filePath)) { + content = (await fs.promises.readFile(filePath, 'utf8')).trim(); + } + const config = normalizeSSHConfig(SSHConfig.parse(content)); + + const includedConfigs: [number, SSHConfig[]][] = []; + for (let i = 0; i < config.length; i++) { + const line = config[i]; + if (isIncludeDirective(line)) { + const values = (line.value as string).split(',').map(s => s.trim()); + const configs: SSHConfig[] = []; + for (const value of values) { + const includePaths = await glob(normalizeToSlash(untildify(value)), { + absolute: true, + cwd: normalizeToSlash(path.dirname(userConfig ? defaultSSHConfigPath : systemSSHConfig)) + }); + for (const p of includePaths) { + configs.push(await parseSSHConfigFromFile(p, userConfig)); + } + } + includedConfigs.push([i, configs]); + } + } + for (const [idx, includeConfigs] of includedConfigs.reverse()) { + config.splice(idx, 1, ...includeConfigs.flat()); + } + + return config; } export default class SSHConfiguration { - static async loadFromFS(): Promise { - const config = await parseSSHConfigFromFile(getSSHConfigPath(), true); - config.push(...await parseSSHConfigFromFile(systemSSHConfig, false)); - - return new SSHConfiguration(config); - } - - constructor(private sshConfig: SSHConfig) { - } - - getAllConfiguredHosts(): string[] { - const hosts = new Set(); - for (const line of this.sshConfig) { - if (isHostSection(line)) { - const value = Array.isArray(line.value) ? line.value[0] : line.value; - const isPattern = /^!/.test(value) || /[?*]/.test(value); - if (!isPattern) { - hosts.add(value); - } - } - } - - return [...hosts.keys()]; - } - - getHostConfiguration(host: string): Record { - // Only a few directives return an array - // https://github.com/jeanp413/ssh-config/blob/8d187bb8f1d83a51ff2b1d127e6b6269d24092b5/src/ssh-config.ts#L9C1-L9C118 - return this.sshConfig.compute(host) as Record; - } + static async loadFromFS(): Promise { + const config = await parseSSHConfigFromFile(getSSHConfigPath(), true); + config.push(...await parseSSHConfigFromFile(systemSSHConfig, false)); + + return new SSHConfiguration(config); + } + + constructor(private sshConfig: SSHConfig) { + } + + getAllConfiguredHosts(): string[] { + const hosts = new Set(); + for (const line of this.sshConfig) { + if (isHostSection(line)) { + const value = Array.isArray(line.value) ? line.value[0] : line.value; + const isPattern = /^!/.test(value) || /[?*]/.test(value); + if (!isPattern) { + hosts.add(value); + } + } + } + + return [...hosts.keys()]; + } + + getHostConfiguration(host: string): Record { + // Only a few directives return an array + // https://github.com/jeanp413/ssh-config/blob/8d187bb8f1d83a51ff2b1d127e6b6269d24092b5/src/ssh-config.ts#L9C1-L9C118 + return this.sshConfig.compute(host) as Record; + } } diff --git a/extensions/positron-notebook-controllers/package.json b/extensions/positron-notebook-controllers/package.json index f1238039a9b..01bfe9584dc 100644 --- a/extensions/positron-notebook-controllers/package.json +++ b/extensions/positron-notebook-controllers/package.json @@ -26,7 +26,7 @@ ], "configuration": [ { - "title": "Positron Notebook Controllers", + "title": "Notebook Controllers", "properties": { "notebook.experimental.showExecutionInfo": { "type": "boolean", diff --git a/extensions/positron-python/src/client/positron/session.ts b/extensions/positron-python/src/client/positron/session.ts index 048dbe905e2..3970fb22124 100644 --- a/extensions/positron-python/src/client/positron/session.ts +++ b/extensions/positron-python/src/client/positron/session.ts @@ -467,7 +467,7 @@ export class PythonRuntimeSession implements positron.LanguageRuntimeSession, vs } private async createKernel(): Promise { - const config = vscode.workspace.getConfiguration('positronKernelSupervisor'); + const config = vscode.workspace.getConfiguration('kernelSupervisor'); if (config.get('enable', true) && this.runtimeMetadata.runtimeId !== 'reticulate') { // Use the Positron kernel supervisor if enabled const ext = vscode.extensions.getExtension('positron.positron-supervisor'); diff --git a/extensions/positron-python/src/test/positron/initialize.ts b/extensions/positron-python/src/test/positron/initialize.ts index dba08894368..e5cbaaa456a 100644 --- a/extensions/positron-python/src/test/positron/initialize.ts +++ b/extensions/positron-python/src/test/positron/initialize.ts @@ -12,6 +12,6 @@ export function initializePositron(): void { // Don't start Positron interpreters automatically during tests. vscode.workspace - .getConfiguration('positron.interpreters') + .getConfiguration('interpreters') .update('automaticStartup', false, vscode.ConfigurationTarget.Global); } diff --git a/extensions/positron-r/package.nls.json b/extensions/positron-r/package.nls.json index 258e7f4ba15..f07e37fdafa 100644 --- a/extensions/positron-r/package.nls.json +++ b/extensions/positron-r/package.nls.json @@ -30,8 +30,8 @@ "r.menu.packageCheck.title": "Check R Package", "r.menu.packageDocument.title": "Document R Package", "r.menu.rmarkdownRender.title": "Render Document With R Markdown", - "r.configuration.title": "Positron R Language Pack", - "r.configuration.title-dev": "Positron R Language Pack (advanced)", + "r.configuration.title": "R", + "r.configuration.title-dev": "Advanced", "r.configuration.customRootFolders.markdownDescription": "List of additional folders to search for R installations. These folders are searched after and in the same way as the default folder for your operating system (e.g. `C:/Program Files/R` on Windows).", "r.configuration.customBinaries.markdownDescription": "List of additional R binaries. If you want to use an R installation that is not automatically discovered, provide the path to its binary here. For example, on Windows this might look like `C:/some/unusual/location/R-4.4.1/bin/x64/R.exe`.", "r.configuration.kernelPath.description": "Path on disk to the ARK kernel executable; use this to override the default (embedded) kernel. Note that this is not the path to R.", diff --git a/extensions/positron-r/src/session.ts b/extensions/positron-r/src/session.ts index f9f03b6c978..f62b5a231b8 100644 --- a/extensions/positron-r/src/session.ts +++ b/extensions/positron-r/src/session.ts @@ -575,7 +575,7 @@ export class RSession implements positron.LanguageRuntimeSession, vscode.Disposa } private async createKernel(): Promise { - const config = vscode.workspace.getConfiguration('positronKernelSupervisor'); + const config = vscode.workspace.getConfiguration('kernelSupervisor'); if (config.get('enable', true)) { // Use the Positron kernel supervisor if enabled const ext = vscode.extensions.getExtension('positron.positron-supervisor'); diff --git a/extensions/positron-reticulate/package.nls.json b/extensions/positron-reticulate/package.nls.json index 92429c0308e..8d4d750fbe5 100644 --- a/extensions/positron-reticulate/package.nls.json +++ b/extensions/positron-reticulate/package.nls.json @@ -1,6 +1,6 @@ { "displayName": "Positron Reticulate", "description": "Provides reticulate support for positron", - "configurationTitle": "Reticulate settings", + "configurationTitle": "Reticulate", "enabledDescription": "Enables reticulate console sessions" } diff --git a/extensions/positron-run-app/package.json b/extensions/positron-run-app/package.json index 27384b3d10a..3bce7fd577b 100644 --- a/extensions/positron-run-app/package.json +++ b/extensions/positron-run-app/package.json @@ -14,16 +14,17 @@ "main": "./out/extension.js", "contributes": { "configuration": { + "title": "App Launcher", "properties": { - "positron.runApplication.showEnableShellIntegrationMessage": { + "positron.appLauncher.showEnableShellIntegrationMessage": { "type": "boolean", "default": true, - "description": "%configuration.positron.runApplication.showEnableShellIntegrationMessage%" + "description": "%configuration.appLauncher.showEnableShellIntegrationMessage%" }, - "positron.runApplication.showShellIntegrationNotSupportedMessage": { + "positron.appLauncher.showShellIntegrationNotSupportedMessage": { "type": "boolean", "default": true, - "description": "%configuration.positron.runApplication.showShellIntegrationNotSupportedMessage%" + "description": "%configuration.appLauncher.showShellIntegrationNotSupportedMessage%" } } } diff --git a/extensions/positron-run-app/package.nls.json b/extensions/positron-run-app/package.nls.json index 54d3dcacbf8..ec7c5579583 100644 --- a/extensions/positron-run-app/package.nls.json +++ b/extensions/positron-run-app/package.nls.json @@ -1,6 +1,6 @@ { "displayName": "Positron Run App", "description": "Generic 'Run App' Framework for Positron", - "configuration.positron.runApplication.showEnableShellIntegrationMessage": "Show a prompt when shell integration is disabled.", - "configuration.positron.runApplication.showShellIntegrationNotSupportedMessage": "Show a warning when shell integration is not supported by the active terminal." + "configuration.appLauncher.showEnableShellIntegrationMessage": "Show a prompt when shell integration is disabled.", + "configuration.appLauncher.showShellIntegrationNotSupportedMessage": "Show a warning when shell integration is not supported by the active terminal." } diff --git a/extensions/positron-run-app/src/api.ts b/extensions/positron-run-app/src/api.ts index d311be3bf81..7a9334d638b 100644 --- a/extensions/positron-run-app/src/api.ts +++ b/extensions/positron-run-app/src/api.ts @@ -509,7 +509,7 @@ async function showEnableShellIntegrationMessage(rerunApplicationCallback: () => } } else if (selection === dontAskAgain) { // Disable the prompt for future runs. - const runAppConfig = vscode.workspace.getConfiguration('positron.runApplication'); + const runAppConfig = vscode.workspace.getConfiguration('positron.appLauncher'); await runAppConfig.update('showShellIntegrationPrompt', false, vscode.ConfigurationTarget.Global); } } @@ -538,7 +538,7 @@ async function showShellIntegrationNotSupportedMessage(): Promise { await vscode.env.openExternal(vscode.Uri.parse('https://code.visualstudio.com/docs/terminal/shell-integration')); } else if (selection === dontShowAgain) { // Disable the prompt for future runs. - const runAppConfig = vscode.workspace.getConfiguration('positron.runApplication'); + const runAppConfig = vscode.workspace.getConfiguration('positron.appLauncher'); await runAppConfig.update('showShellIntegrationNotSupportedMessage', false, vscode.ConfigurationTarget.Global); } } diff --git a/extensions/positron-run-app/src/extension.ts b/extensions/positron-run-app/src/extension.ts index b422044865d..d5fc433494a 100644 --- a/extensions/positron-run-app/src/extension.ts +++ b/extensions/positron-run-app/src/extension.ts @@ -12,8 +12,8 @@ export const log = vscode.window.createOutputChannel('Positron Run App', { log: export enum Config { ShellIntegrationEnabled = 'terminal.integrated.shellIntegration.enabled', - ShowEnableShellIntegrationMessage = 'positron.runApplication.showEnableShellIntegrationMessage', - ShowShellIntegrationNotSupportedMessage = 'positron.runApplication.showShellIntegrationNotSupportedMessage', + ShowEnableShellIntegrationMessage = 'positron.appLauncher.showEnableShellIntegrationMessage', + ShowShellIntegrationNotSupportedMessage = 'positron.appLauncher.showShellIntegrationNotSupportedMessage', } export async function activate(context: vscode.ExtensionContext): Promise { diff --git a/extensions/positron-run-app/src/test/api.test.ts b/extensions/positron-run-app/src/test/api.test.ts index fa960195e7a..040c643d812 100644 --- a/extensions/positron-run-app/src/test/api.test.ts +++ b/extensions/positron-run-app/src/test/api.test.ts @@ -117,7 +117,7 @@ suite('PositronRunApp', () => { assert(terminal, 'Terminal not found'); } - test('runApplication: shell integration supported', async () => { + test('appLauncher: shell integration supported', async () => { // Run the application. await verifyRunTestApplication(); @@ -129,7 +129,7 @@ suite('PositronRunApp', () => { sinon.assert.calledOnceWithMatch(previewUrlStub, localhostUriMatch); }); - test('runApplication: shell integration disabled, user enables and reruns', async () => { + test('appLauncher: shell integration disabled, user enables and reruns', async () => { // Disable shell integration. await vscode.workspace.getConfiguration('terminal.integrated.shellIntegration').update('enabled', false); diff --git a/extensions/positron-supervisor/package.json b/extensions/positron-supervisor/package.json index 41e3b39fdb8..36a1d3cd2b9 100644 --- a/extensions/positron-supervisor/package.json +++ b/extensions/positron-supervisor/package.json @@ -27,24 +27,24 @@ "contributes": { "configuration": { "type": "object", - "title": "Positron Kernel Supervisor Configuration", + "title": "Kernel Supervisor", "properties": { - "positronKernelSupervisor.enable": { + "kernelSupervisor.enable": { "type": "boolean", "default": true, "description": "%configuration.enable.description%" }, - "positronKernelSupervisor.showTerminal": { + "kernelSupervisor.showTerminal": { "type": "boolean", "default": false, "description": "%configuration.showTerminal.description%" }, - "positronKernelSupervisor.connectionTimeout": { + "kernelSupervisor.connectionTimeout": { "type": "integer", "default": 30, "description": "%configuration.connectionTimeout.description%" }, - "positronKernelSupervisor.logLevel": { + "kernelSupervisor.logLevel": { "scope": "window", "type": "string", "enum": [ @@ -64,13 +64,13 @@ "default": "debug", "description": "%configuration.logLevel.description%" }, - "positronKernelSupervisor.attachOnStartup": { + "kernelSupervisor.attachOnStartup": { "scope": "window", "type": "boolean", "default": false, "description": "%configuration.attachOnStartup.description%" }, - "positronKernelSupervisor.sleepOnStartup": { + "kernelSupervisor.sleepOnStartup": { "scope": "window", "type": "number", "description": "%configuration.sleepOnStartup.description%" diff --git a/extensions/positron-supervisor/src/KallichoreAdapterApi.ts b/extensions/positron-supervisor/src/KallichoreAdapterApi.ts index a9f88c46143..34259ab9c27 100644 --- a/extensions/positron-supervisor/src/KallichoreAdapterApi.ts +++ b/extensions/positron-supervisor/src/KallichoreAdapterApi.ts @@ -88,7 +88,7 @@ export class KCApi implements KallichoreAdapterApi { // If the Kallichore server is enabled in the configuration, start it // eagerly so it's warm when we start trying to create or restore sessions. - if (vscode.workspace.getConfiguration('positronKernelSupervisor').get('enable', true)) { + if (vscode.workspace.getConfiguration('kernelSupervisor').get('enable', true)) { this.ensureStarted().catch((err) => { this._log.appendLine(`Failed to start Kallichore server: ${err}`); }); @@ -169,7 +169,7 @@ export class KCApi implements KallichoreAdapterApi { // Get the log level from the configuration - const config = vscode.workspace.getConfiguration('positronKernelSupervisor'); + const config = vscode.workspace.getConfiguration('kernelSupervisor'); const logLevel = config.get('logLevel') ?? 'warn'; // Export the Positron version as an environment variable diff --git a/extensions/positron-supervisor/src/KallichoreSession.ts b/extensions/positron-supervisor/src/KallichoreSession.ts index f925bfe1f8a..ef43ccab1ae 100644 --- a/extensions/positron-supervisor/src/KallichoreSession.ts +++ b/extensions/positron-supervisor/src/KallichoreSession.ts @@ -251,7 +251,7 @@ export class KallichoreSession implements JupyterLanguageRuntimeSession { // Initialize extra functionality, if any. These settings modify the // argument list `args` in place, so need to happen right before we send // the arg list to the server. - const config = vscode.workspace.getConfiguration('positronKernelSupervisor'); + const config = vscode.workspace.getConfiguration('kernelSupervisor'); const attachOnStartup = config.get('attachOnStartup', false) && this._extra?.attachOnStartup; const sleepOnStartup = config.get('sleepOnStartup', undefined) && this._extra?.sleepOnStartup; const connectionTimeout = config.get('connectionTimeout', 30); @@ -798,7 +798,7 @@ export class KallichoreSession implements JupyterLanguageRuntimeSession { // Before connecting, check if we should attach to the session on // startup - const config = vscode.workspace.getConfiguration('positronKernelSupervisor'); + const config = vscode.workspace.getConfiguration('kernelSupervisor'); const attachOnStartup = config.get('attachOnStartup', false) && this._extra?.attachOnStartup; if (attachOnStartup) { try { diff --git a/extensions/positron-viewer/package.json b/extensions/positron-viewer/package.json index 008f4952427..01967bea827 100644 --- a/extensions/positron-viewer/package.json +++ b/extensions/positron-viewer/package.json @@ -32,7 +32,7 @@ "contributes": { "configuration": [ { - "title": "Positron URL Viewer", + "title": "URL Viewer", "properties": { "positron.viewer.openLocalhostUrls": { "type": "boolean", diff --git a/src/vs/workbench/contrib/positronRuntimeSessions/browser/positronRuntimeSessions.contribution.ts b/src/vs/workbench/contrib/positronRuntimeSessions/browser/positronRuntimeSessions.contribution.ts index e2fe89f308d..3f5a962879a 100644 --- a/src/vs/workbench/contrib/positronRuntimeSessions/browser/positronRuntimeSessions.contribution.ts +++ b/src/vs/workbench/contrib/positronRuntimeSessions/browser/positronRuntimeSessions.contribution.ts @@ -30,18 +30,18 @@ const positronRuntimeSessionsViewIcon = registerIcon( ); // The configuration key for showing the sessions view. -const SHOW_SESSIONS_CONFIG_KEY = 'positron.interpreters.showSessions'; +const SHOW_SESSIONS_CONFIG_KEY = 'interpreters.showSessions'; // Register configuration options for the runtime service const configurationRegistry = Registry.as(ConfigurationExtensions.Configuration); configurationRegistry.registerConfiguration({ ...positronConfigurationNodeBase, properties: { - 'positron.interpreters.showSessions': { + 'interpreters.showSessions': { scope: ConfigurationScope.MACHINE, type: 'boolean', default: false, - description: nls.localize('positron.interpreters.showSessions', "Enable debug Runtimes pane listing active interpreter sessions.") + description: nls.localize('interpreters.showSessions', "Enable debug Runtimes pane listing active interpreter sessions.") }, } }); diff --git a/src/vs/workbench/contrib/preferences/browser/settingsLayout.ts b/src/vs/workbench/contrib/preferences/browser/settingsLayout.ts index ed127dd846a..a70246ada78 100644 --- a/src/vs/workbench/contrib/preferences/browser/settingsLayout.ts +++ b/src/vs/workbench/contrib/preferences/browser/settingsLayout.ts @@ -313,16 +313,9 @@ export const tocData: ITOCEntry = { // --- Start Positron --- , { - id: 'positron', - label: localize('positron', "Positron"), - settings: [], - children: [ - { - id: 'positron/interpreters', - label: localize('interpreters', "Interpreters"), - settings: ['positron.interpreters.*'] - } - ] + id: 'interpreters', + label: localize('interpreters', "Interpreters"), + settings: ['interpreters.*'] } // --- End Positron --- ] diff --git a/src/vs/workbench/services/languageRuntime/common/languageRuntime.ts b/src/vs/workbench/services/languageRuntime/common/languageRuntime.ts index a76223a9641..15e92dc74a9 100644 --- a/src/vs/workbench/services/languageRuntime/common/languageRuntime.ts +++ b/src/vs/workbench/services/languageRuntime/common/languageRuntime.ts @@ -137,13 +137,13 @@ const configurationRegistry = Registry.as(ConfigurationE configurationRegistry.registerConfiguration({ ...positronConfigurationNodeBase, properties: { - 'positron.interpreters.restartOnCrash': { + 'interpreters.restartOnCrash': { scope: ConfigurationScope.MACHINE_OVERRIDABLE, type: 'boolean', default: true, description: nls.localize('positron.runtime.restartOnCrash', "When enabled, interpreters are automatically restarted after a crash.") }, - 'positron.interpreters.automaticStartup': { + 'interpreters.automaticStartup': { scope: ConfigurationScope.MACHINE_OVERRIDABLE, type: 'boolean', default: true, diff --git a/src/vs/workbench/services/positronConsole/browser/positronConsoleService.ts b/src/vs/workbench/services/positronConsole/browser/positronConsoleService.ts index 228634b0e54..00a82c895af 100644 --- a/src/vs/workbench/services/positronConsole/browser/positronConsoleService.ts +++ b/src/vs/workbench/services/positronConsole/browser/positronConsoleService.ts @@ -1725,7 +1725,7 @@ class PositronConsoleInstance extends Disposable implements IPositronConsoleInst // Show restart button if crashed and user has disabled automatic restarts const crashedAndNeedRestartButton = exit.reason === RuntimeExitReason.Error && - !this._configurationService.getValue('positron.interpreters.restartOnCrash'); + !this._configurationService.getValue('interpreters.restartOnCrash'); // In the case of a forced quit, normal shutdown, or unknown shutdown where the exit // code was `0`, we don't attempt to automatically start the runtime again. In this diff --git a/src/vs/workbench/services/runtimeSession/common/runtimeSession.ts b/src/vs/workbench/services/runtimeSession/common/runtimeSession.ts index 259eeea7f24..81cc27fe08f 100644 --- a/src/vs/workbench/services/runtimeSession/common/runtimeSession.ts +++ b/src/vs/workbench/services/runtimeSession/common/runtimeSession.ts @@ -616,7 +616,7 @@ export class RuntimeSessionService extends Disposable implements IRuntimeSession ): Promise { // Check the setting to see if we should be auto-starting. const autoStart = this._configurationService.getValue( - 'positron.interpreters.automaticStartup'); + 'interpreters.automaticStartup'); if (!autoStart) { this._logService.info(`Language runtime ` + `${formatLanguageRuntimeMetadata(metadata)} ` + diff --git a/src/vs/workbench/services/runtimeSession/test/common/runtimeSession.test.ts b/src/vs/workbench/services/runtimeSession/test/common/runtimeSession.test.ts index 05418ba8dab..368736b07c3 100644 --- a/src/vs/workbench/services/runtimeSession/test/common/runtimeSession.test.ts +++ b/src/vs/workbench/services/runtimeSession/test/common/runtimeSession.test.ts @@ -60,7 +60,7 @@ suite('Positron - RuntimeSessionService', () => { unregisteredRuntime = { runtimeId: 'unregistered-runtime-id' } as ILanguageRuntimeMetadata; // Enable automatic startup. - configService.setUserConfiguration('positron.interpreters.automaticStartup', true); + configService.setUserConfiguration('interpreters.automaticStartup', true); // Trust the workspace. workspaceTrustManagementService.setWorkspaceTrust(true); @@ -622,7 +622,7 @@ suite('Positron - RuntimeSessionService', () => { }); test('auto start console does nothing if automatic startup is disabled', async () => { - configService.setUserConfiguration('positron.interpreters.automaticStartup', false); + configService.setUserConfiguration('interpreters.automaticStartup', false); const sessionId = await runtimeSessionService.autoStartRuntime(runtime, startReason); diff --git a/src/vs/workbench/services/runtimeStartup/common/runtimeStartup.ts b/src/vs/workbench/services/runtimeStartup/common/runtimeStartup.ts index b0739366ffe..b0b33c98482 100644 --- a/src/vs/workbench/services/runtimeStartup/common/runtimeStartup.ts +++ b/src/vs/workbench/services/runtimeStartup/common/runtimeStartup.ts @@ -469,7 +469,7 @@ export class RuntimeStartupService extends Disposable implements IRuntimeStartup // Check the setting to see if we should be auto-starting. const autoStart = this._configurationService.getValue( - 'positron.interpreters.automaticStartup'); + 'interpreters.automaticStartup'); if (!autoStart) { this._logService.info(`Language runtime ` + `${formatLanguageRuntimeMetadata(affiliatedRuntimeMetadata)} ` + @@ -677,7 +677,7 @@ export class RuntimeStartupService extends Disposable implements IRuntimeStartup if (affiliatedRuntimeMetadata) { // Check the setting to see if we should be auto-starting. const autoStart = this._configurationService.getValue( - 'positron.interpreters.automaticStartup'); + 'interpreters.automaticStartup'); if (autoStart) { @@ -846,7 +846,7 @@ export class RuntimeStartupService extends Disposable implements IRuntimeStartup } const restartOnCrash = - this._configurationService.getValue('positron.interpreters.restartOnCrash'); + this._configurationService.getValue('interpreters.restartOnCrash'); let action; diff --git a/test/e2e/areas/reticulate/reticulate.test.ts b/test/e2e/areas/reticulate/reticulate.test.ts index e3d4ac186fe..50e4baa8b42 100644 --- a/test/e2e/areas/reticulate/reticulate.test.ts +++ b/test/e2e/areas/reticulate/reticulate.test.ts @@ -22,7 +22,7 @@ test.describe('Reticulate', { // remove this once https://github.com/posit-dev/positron/issues/5226 // is resolved await userSettings.set([ - ['positronKernelSupervisor.enable', 'false'], + ['kernelSupervisor.enable', 'false'], ['positron.reticulate.enabled', 'true'] ]);