|
| 1 | +/*--------------------------------------------------------------------------------------------- |
| 2 | + * Copyright (c) Gitpod. All rights reserved. |
| 3 | + *--------------------------------------------------------------------------------------------*/ |
| 4 | + |
| 5 | +import * as os from 'os'; |
| 6 | +import * as path from 'path'; |
| 7 | +import { CancellationToken } from 'vs/base/common/cancellation'; |
| 8 | +import { URI } from 'vs/base/common/uri'; |
| 9 | +import { Emitter, Event } from 'vs/base/common/event'; |
| 10 | +import { IConfigurationService } from 'vs/platform/configuration/common/configuration'; |
| 11 | +import { ILogService } from 'vs/platform/log/common/log'; |
| 12 | +import product from 'vs/platform/product/common/product'; |
| 13 | +import { RemoteAgentConnectionContext } from 'vs/platform/remote/common/remoteAgentEnvironment'; |
| 14 | +import { ITelemetryService } from 'vs/platform/telemetry/common/telemetry'; |
| 15 | +import { IShellLaunchConfig, LocalReconnectConstants } from 'vs/platform/terminal/common/terminal'; |
| 16 | +import { PtyHostService } from 'vs/platform/terminal/node/ptyHostService'; |
| 17 | +import { ICreateTerminalProcessArguments, ICreateTerminalProcessResult, REMOTE_TERMINAL_CHANNEL_NAME } from 'vs/workbench/contrib/terminal/common/remoteTerminalChannel'; |
| 18 | +import * as platform from 'vs/base/common/platform'; |
| 19 | +import { IWorkspaceFolderData } from 'vs/platform/terminal/common/terminalProcess'; |
| 20 | +import { IWorkspaceFolder } from 'vs/platform/workspace/common/workspace'; |
| 21 | +import { createTerminalEnvironment, createVariableResolver, getCwd, getDefaultShell, getDefaultShellArgs } from 'vs/workbench/contrib/terminal/common/terminalEnvironment'; |
| 22 | +import { deserializeEnvironmentVariableCollection } from 'vs/workbench/contrib/terminal/common/environmentVariableShared'; |
| 23 | +import { MergedEnvironmentVariableCollection } from 'vs/workbench/contrib/terminal/common/environmentVariableCollection'; |
| 24 | +import { IEnvironmentVariableCollection } from 'vs/workbench/contrib/terminal/common/environmentVariable'; |
| 25 | +import { getSystemShellSync } from 'vs/base/node/shell'; |
| 26 | +import { ServicesAccessor } from 'vs/platform/instantiation/common/instantiation'; |
| 27 | +import { IPCServer } from 'vs/base/parts/ipc/common/ipc'; |
| 28 | +import { AbstractVariableResolverService } from 'vs/workbench/services/configurationResolver/common/variableResolver'; |
| 29 | +import { TernarySearchTree } from 'vs/base/common/map'; |
| 30 | + |
| 31 | +export function registerRemoteTerminal(services: ServicesAccessor, channelServer: IPCServer<RemoteAgentConnectionContext>) { |
| 32 | + const reconnectConstants = { |
| 33 | + GraceTime: LocalReconnectConstants.GraceTime, |
| 34 | + ShortGraceTime: LocalReconnectConstants.ShortGraceTime |
| 35 | + }; |
| 36 | + const configurationService = services.get(IConfigurationService); |
| 37 | + const logService = services.get(ILogService); |
| 38 | + const telemetryService = services.get(ITelemetryService); |
| 39 | + const ptyHostService = new PtyHostService(reconnectConstants, configurationService, logService, telemetryService); |
| 40 | + const resolvedServices: Services = { logService, ptyHostService }; |
| 41 | + channelServer.registerChannel(REMOTE_TERMINAL_CHANNEL_NAME, { |
| 42 | + |
| 43 | + call: async (ctx: RemoteAgentConnectionContext, command: string, arg?: any, cancellationToken?: CancellationToken) => { |
| 44 | + if (command === '$createProcess') { |
| 45 | + return createProcess(arg, resolvedServices); |
| 46 | + } |
| 47 | + |
| 48 | + // Generic method handling for all other commands |
| 49 | + const serviceRecord = ptyHostService as unknown as Record<string, (arg?: any) => Promise<any>>; |
| 50 | + const serviceFunc = serviceRecord[command.substring(1)]; |
| 51 | + if (!serviceFunc) { |
| 52 | + logService.error('Unknown command: ' + command); |
| 53 | + return undefined; |
| 54 | + } |
| 55 | + if (Array.isArray(arg)) { |
| 56 | + return serviceFunc.call(ptyHostService, ...arg); |
| 57 | + } else { |
| 58 | + return serviceFunc.call(ptyHostService, arg); |
| 59 | + } |
| 60 | + }, |
| 61 | + |
| 62 | + listen: (ctx: RemoteAgentConnectionContext, event: string) => { |
| 63 | + const serviceRecord = ptyHostService as unknown as Record<string, Event<any>>; |
| 64 | + const result = serviceRecord[event.substring(1, event.endsWith('Event') ? event.length - 'Event'.length : undefined)]; |
| 65 | + if (!result) { |
| 66 | + logService.error('Unknown event: ' + event); |
| 67 | + return new Emitter<any>().event; |
| 68 | + } |
| 69 | + return result; |
| 70 | + } |
| 71 | + |
| 72 | + }); |
| 73 | +} |
| 74 | + |
| 75 | +interface Services { |
| 76 | + ptyHostService: PtyHostService, logService: ILogService |
| 77 | +} |
| 78 | + |
| 79 | +async function createProcess(args: ICreateTerminalProcessArguments, services: Services): Promise<ICreateTerminalProcessResult> { |
| 80 | + const shellLaunchConfigDto = args.shellLaunchConfig; |
| 81 | + // See $spawnExtHostProcess in src/vs/workbench/api/node/extHostTerminalService.ts for a reference implementation |
| 82 | + const shellLaunchConfig: IShellLaunchConfig = { |
| 83 | + name: shellLaunchConfigDto.name, |
| 84 | + executable: shellLaunchConfigDto.executable, |
| 85 | + args: shellLaunchConfigDto.args, |
| 86 | + cwd: typeof shellLaunchConfigDto.cwd === 'string' ? shellLaunchConfigDto.cwd : URI.revive(shellLaunchConfigDto.cwd), |
| 87 | + env: shellLaunchConfigDto.env |
| 88 | + }; |
| 89 | + |
| 90 | + let lastActiveWorkspace: IWorkspaceFolder | undefined; |
| 91 | + if (args.activeWorkspaceFolder) { |
| 92 | + lastActiveWorkspace = toWorkspaceFolder(args.activeWorkspaceFolder); |
| 93 | + } |
| 94 | + |
| 95 | + const processEnv = { ...process.env, ...args.resolverEnv } as platform.IProcessEnvironment; |
| 96 | + const configurationResolverService = new RemoteTerminalVariableResolverService( |
| 97 | + args.workspaceFolders.map(toWorkspaceFolder), |
| 98 | + args.resolvedVariables, |
| 99 | + args.activeFileResource ? URI.revive(args.activeFileResource) : undefined, |
| 100 | + processEnv |
| 101 | + ); |
| 102 | + const variableResolver = createVariableResolver(lastActiveWorkspace, processEnv, configurationResolverService); |
| 103 | + |
| 104 | + // Merge in shell and args from settings |
| 105 | + if (!shellLaunchConfig.executable) { |
| 106 | + shellLaunchConfig.executable = getDefaultShell( |
| 107 | + key => args.configuration[key], |
| 108 | + getSystemShellSync(platform.OS, process.env as platform.IProcessEnvironment), |
| 109 | + process.env.hasOwnProperty('PROCESSOR_ARCHITEW6432'), |
| 110 | + process.env.windir, |
| 111 | + variableResolver, |
| 112 | + services.logService, |
| 113 | + false |
| 114 | + ); |
| 115 | + shellLaunchConfig.args = getDefaultShellArgs( |
| 116 | + key => args.configuration[key], |
| 117 | + false, |
| 118 | + variableResolver, |
| 119 | + services.logService |
| 120 | + ); |
| 121 | + } else if (variableResolver) { |
| 122 | + shellLaunchConfig.executable = variableResolver(shellLaunchConfig.executable); |
| 123 | + if (shellLaunchConfig.args) { |
| 124 | + if (Array.isArray(shellLaunchConfig.args)) { |
| 125 | + const resolvedArgs: string[] = []; |
| 126 | + for (const arg of shellLaunchConfig.args) { |
| 127 | + resolvedArgs.push(variableResolver(arg)); |
| 128 | + } |
| 129 | + shellLaunchConfig.args = resolvedArgs; |
| 130 | + } else { |
| 131 | + shellLaunchConfig.args = variableResolver(shellLaunchConfig.args); |
| 132 | + } |
| 133 | + } |
| 134 | + } |
| 135 | + |
| 136 | + // Get the initial cwd |
| 137 | + const initialCwd = getCwd( |
| 138 | + shellLaunchConfig, |
| 139 | + os.homedir(), |
| 140 | + variableResolver, |
| 141 | + lastActiveWorkspace?.uri, |
| 142 | + args.configuration['terminal.integrated.cwd'], services.logService |
| 143 | + ); |
| 144 | + shellLaunchConfig.cwd = initialCwd; |
| 145 | + |
| 146 | + const env = createTerminalEnvironment( |
| 147 | + shellLaunchConfig, |
| 148 | + args.configuration['terminal.integrated.env.linux'], |
| 149 | + variableResolver, |
| 150 | + product.version, |
| 151 | + args.configuration['terminal.integrated.detectLocale'] || 'auto', |
| 152 | + processEnv |
| 153 | + ); |
| 154 | + |
| 155 | + // Apply extension environment variable collections to the environment |
| 156 | + if (!shellLaunchConfig.strictEnv) { |
| 157 | + const collection = new Map<string, IEnvironmentVariableCollection>(); |
| 158 | + for (const [name, serialized] of args.envVariableCollections) { |
| 159 | + collection.set(name, { |
| 160 | + map: deserializeEnvironmentVariableCollection(serialized) |
| 161 | + }); |
| 162 | + } |
| 163 | + const mergedCollection = new MergedEnvironmentVariableCollection(collection); |
| 164 | + mergedCollection.applyToProcessEnvironment(env, variableResolver); |
| 165 | + } |
| 166 | + |
| 167 | + const persistentTerminalId = await services.ptyHostService.createProcess(shellLaunchConfig, initialCwd, args.cols, args.rows, |
| 168 | + env, processEnv, false, args.shouldPersistTerminal, args.workspaceId, args.workspaceName); |
| 169 | + return { |
| 170 | + persistentTerminalId, |
| 171 | + resolvedShellLaunchConfig: shellLaunchConfig |
| 172 | + }; |
| 173 | +} |
| 174 | + |
| 175 | +function toWorkspaceFolder(data: IWorkspaceFolderData): IWorkspaceFolder { |
| 176 | + return { |
| 177 | + uri: URI.revive(data.uri), |
| 178 | + name: data.name, |
| 179 | + index: data.index, |
| 180 | + toResource: () => { |
| 181 | + throw new Error('Not implemented'); |
| 182 | + } |
| 183 | + }; |
| 184 | +} |
| 185 | + |
| 186 | +/** |
| 187 | + * See ExtHostVariableResolverService in src/vs/workbench/api/common/extHostDebugService.ts for a reference implementation. |
| 188 | + */ |
| 189 | +class RemoteTerminalVariableResolverService extends AbstractVariableResolverService { |
| 190 | + |
| 191 | + private readonly structure = TernarySearchTree.forUris<IWorkspaceFolder>(() => false); |
| 192 | + |
| 193 | + constructor(folders: IWorkspaceFolder[], resolvedVariables: { [name: string]: string }, activeFileResource: URI | undefined, env: platform.IProcessEnvironment) { |
| 194 | + super({ |
| 195 | + getFolderUri: (folderName: string): URI | undefined => { |
| 196 | + const found = folders.filter(f => f.name === folderName); |
| 197 | + if (found && found.length > 0) { |
| 198 | + return found[0].uri; |
| 199 | + } |
| 200 | + return undefined; |
| 201 | + }, |
| 202 | + getWorkspaceFolderCount: (): number => { |
| 203 | + return folders.length; |
| 204 | + }, |
| 205 | + getConfigurationValue: (folderUri: URI | undefined, section: string): string | undefined => { |
| 206 | + return resolvedVariables['config:' + section]; |
| 207 | + }, |
| 208 | + getAppRoot: (): string | undefined => { |
| 209 | + return env['VSCODE_CWD'] || process.cwd(); |
| 210 | + }, |
| 211 | + getExecPath: (): string | undefined => { |
| 212 | + return env['VSCODE_EXEC_PATH']; |
| 213 | + }, |
| 214 | + getFilePath: (): string | undefined => { |
| 215 | + if (activeFileResource) { |
| 216 | + return path.normalize(activeFileResource.fsPath); |
| 217 | + } |
| 218 | + return undefined; |
| 219 | + }, |
| 220 | + getWorkspaceFolderPathForFile: (): string | undefined => { |
| 221 | + if (activeFileResource) { |
| 222 | + const ws = this.structure.findSubstr(activeFileResource); |
| 223 | + if (ws) { |
| 224 | + return path.normalize(ws.uri.fsPath); |
| 225 | + } |
| 226 | + } |
| 227 | + return undefined; |
| 228 | + }, |
| 229 | + getSelectedText: (): string | undefined => { |
| 230 | + return resolvedVariables.selectedText; |
| 231 | + }, |
| 232 | + getLineNumber: (): string | undefined => { |
| 233 | + return resolvedVariables.lineNumber; |
| 234 | + } |
| 235 | + }, undefined, Promise.resolve(env)); |
| 236 | + |
| 237 | + // Set up the workspace folder data structure |
| 238 | + folders.forEach(folder => { |
| 239 | + this.structure.set(folder.uri, folder); |
| 240 | + }); |
| 241 | + } |
| 242 | + |
| 243 | +} |
0 commit comments