From 6a0f40ccdfda4116a0d4d49d23e174f2deaf865a Mon Sep 17 00:00:00 2001 From: Dhruvi Sompura Date: Thu, 19 Dec 2024 14:08:17 -0800 Subject: [PATCH] Settings: Improve titles (#5791) ### Description Addressess #1487 This is a first pass at improving the setting titles displayed in the settings table of contents for built in positron extensions. Any titles prefixed with "Positron" has been removed and ambiguity in extension titles have been addressed. The setting title in the TOC and category title for each setting item has been unified to match where possible. configuration keys have been changed to fix formatting of setting categories where possible. Extension configuration keys that are prefixed with `positron` have been left as is for now (excluding theInterpreter settings) since the prefix does not effect the settings UI for now. We may want to address improving the key names for folks who interact with settings via the JSON file. It may be worth tackling after settling on a standard for configuration name formatting for bundled extensions. The "Positron > Interpreters" settings now live under "Interpreters"/ The following extensions were modified and have the following setting titles in the TOC: - open-remote-ssh: Remote SSH - positron-notebook-controllers: Notebook Controllers - positron-r: R - positron-reticulate: Reticulate - positron-run-app: App Launcher - positron-supervisor: Kernel Supervisor - positron-viewer: URL Viewer ### QA Notes We'll want to verify there aren't any regressions with the settings for the extensions listed above, excluding any experimental feature settings. For the interpreter settings we should verify that changing the automatic startup behavior, restart on crash behavior, and viewing sessions for interpreters are respected as usual. Some of this behavior should be captured by our e2e tests which will be run against this change before merging. Note that as part of this change, the qa-example-content repo did need a change due to a setting key being renamed: https://github.com/posit-dev/qa-example-content/pull/41 ### Screenshots https://github.com/user-attachments/assets/fc66391a-5e0e-49a9-b717-a0d5401e2d1e --- extensions/open-remote-ssh/package.json | 20 +- .../open-remote-ssh/src/authResolver.ts | 846 +++++++++--------- .../open-remote-ssh/src/hostTreeView.ts | 152 ++-- .../open-remote-ssh/src/serverConfig.ts | 2 +- .../open-remote-ssh/src/ssh/sshConfig.ts | 172 ++-- .../package.json | 2 +- .../src/client/positron/session.ts | 2 +- .../src/test/positron/initialize.ts | 2 +- extensions/positron-r/package.nls.json | 4 +- extensions/positron-r/src/session.ts | 2 +- .../positron-reticulate/package.nls.json | 2 +- extensions/positron-run-app/package.json | 9 +- extensions/positron-run-app/package.nls.json | 4 +- extensions/positron-run-app/src/api.ts | 4 +- extensions/positron-run-app/src/extension.ts | 4 +- .../positron-run-app/src/test/api.test.ts | 4 +- extensions/positron-supervisor/package.json | 14 +- .../src/KallichoreAdapterApi.ts | 4 +- .../src/KallichoreSession.ts | 4 +- extensions/positron-viewer/package.json | 2 +- .../positronRuntimeSessions.contribution.ts | 6 +- .../preferences/browser/settingsLayout.ts | 13 +- .../languageRuntime/common/languageRuntime.ts | 4 +- .../browser/positronConsoleService.ts | 2 +- .../runtimeSession/common/runtimeSession.ts | 2 +- .../test/common/runtimeSession.test.ts | 4 +- .../runtimeStartup/common/runtimeStartup.ts | 6 +- test/e2e/areas/reticulate/reticulate.test.ts | 2 +- 28 files changed, 644 insertions(+), 650 deletions(-) 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'] ]);