mirror of
https://github.com/microsoft/vscode.git
synced 2026-05-22 16:19:52 +01:00
27da25e70f
Part of #143965
319 lines
15 KiB
TypeScript
319 lines
15 KiB
TypeScript
/*---------------------------------------------------------------------------------------------
|
|
* Copyright (c) Microsoft Corporation. All rights reserved.
|
|
* Licensed under the MIT License. See License.txt in the project root for license information.
|
|
*--------------------------------------------------------------------------------------------*/
|
|
|
|
import * as os from 'os';
|
|
import { Emitter, Event } from 'vs/base/common/event';
|
|
import { cloneAndChange } from 'vs/base/common/objects';
|
|
import { Disposable } from 'vs/base/common/lifecycle';
|
|
import * as path from 'vs/base/common/path';
|
|
import * as platform from 'vs/base/common/platform';
|
|
import { URI } from 'vs/base/common/uri';
|
|
import { IURITransformer } from 'vs/base/common/uriIpc';
|
|
import { IServerChannel } from 'vs/base/parts/ipc/common/ipc';
|
|
import { createRandomIPCHandle } from 'vs/base/parts/ipc/node/ipc.net';
|
|
import { ILogService } from 'vs/platform/log/common/log';
|
|
import { RemoteAgentConnectionContext } from 'vs/platform/remote/common/remoteAgentEnvironment';
|
|
import { IPtyService, IShellLaunchConfig, ITerminalProfile } from 'vs/platform/terminal/common/terminal';
|
|
import { IGetTerminalLayoutInfoArgs, ISetTerminalLayoutInfoArgs } from 'vs/platform/terminal/common/terminalProcess';
|
|
import { IWorkspaceFolder } from 'vs/platform/workspace/common/workspace';
|
|
import { createURITransformer } from 'vs/workbench/api/node/uriTransformer';
|
|
import { CLIServerBase, ICommandsExecuter } from 'vs/workbench/api/node/extHostCLIServer';
|
|
import { IEnvironmentVariableCollection } from 'vs/workbench/contrib/terminal/common/environmentVariable';
|
|
import { MergedEnvironmentVariableCollection } from 'vs/workbench/contrib/terminal/common/environmentVariableCollection';
|
|
import { deserializeEnvironmentVariableCollection } from 'vs/workbench/contrib/terminal/common/environmentVariableShared';
|
|
import { ICreateTerminalProcessArguments, ICreateTerminalProcessResult, IWorkspaceFolderData } from 'vs/workbench/contrib/terminal/common/remoteTerminalChannel';
|
|
import * as terminalEnvironment from 'vs/workbench/contrib/terminal/common/terminalEnvironment';
|
|
import { AbstractVariableResolverService } from 'vs/workbench/services/configurationResolver/common/variableResolver';
|
|
import { buildUserEnvironment } from 'vs/server/node/extensionHostConnection';
|
|
import { IServerEnvironmentService } from 'vs/server/node/serverEnvironmentService';
|
|
import { IProductService } from 'vs/platform/product/common/productService';
|
|
|
|
class CustomVariableResolver extends AbstractVariableResolverService {
|
|
constructor(
|
|
env: platform.IProcessEnvironment,
|
|
workspaceFolders: IWorkspaceFolder[],
|
|
activeFileResource: URI | undefined,
|
|
resolvedVariables: { [name: string]: string }
|
|
) {
|
|
super({
|
|
getFolderUri: (folderName: string): URI | undefined => {
|
|
const found = workspaceFolders.filter(f => f.name === folderName);
|
|
if (found && found.length > 0) {
|
|
return found[0].uri;
|
|
}
|
|
return undefined;
|
|
},
|
|
getWorkspaceFolderCount: (): number => {
|
|
return workspaceFolders.length;
|
|
},
|
|
getConfigurationValue: (folderUri: URI, section: string): string | undefined => {
|
|
return resolvedVariables[`config:${section}`];
|
|
},
|
|
getExecPath: (): string | undefined => {
|
|
return env['VSCODE_EXEC_PATH'];
|
|
},
|
|
getAppRoot: (): string | undefined => {
|
|
return env['VSCODE_CWD'];
|
|
},
|
|
getFilePath: (): string | undefined => {
|
|
if (activeFileResource) {
|
|
return path.normalize(activeFileResource.fsPath);
|
|
}
|
|
return undefined;
|
|
},
|
|
getSelectedText: (): string | undefined => {
|
|
return resolvedVariables['selectedText'];
|
|
},
|
|
getLineNumber: (): string | undefined => {
|
|
return resolvedVariables['lineNumber'];
|
|
}
|
|
}, undefined, Promise.resolve(os.homedir()), Promise.resolve(env));
|
|
}
|
|
}
|
|
|
|
export class RemoteTerminalChannel extends Disposable implements IServerChannel<RemoteAgentConnectionContext> {
|
|
|
|
private _lastReqId = 0;
|
|
private readonly _pendingCommands = new Map<number, {
|
|
resolve: (data: any) => void;
|
|
reject: (err: any) => void;
|
|
uriTransformer: IURITransformer;
|
|
}>();
|
|
|
|
private readonly _onExecuteCommand = this._register(new Emitter<{ reqId: number; commandId: string; commandArgs: any[] }>());
|
|
readonly onExecuteCommand = this._onExecuteCommand.event;
|
|
|
|
constructor(
|
|
private readonly _environmentService: IServerEnvironmentService,
|
|
private readonly _logService: ILogService,
|
|
private readonly _ptyService: IPtyService,
|
|
private readonly _productService: IProductService
|
|
) {
|
|
super();
|
|
}
|
|
|
|
async call(ctx: RemoteAgentConnectionContext, command: string, args?: any): Promise<any> {
|
|
switch (command) {
|
|
case '$restartPtyHost': return this._ptyService.restartPtyHost?.apply(this._ptyService, args);
|
|
|
|
case '$createProcess': {
|
|
const uriTransformer = createURITransformer(ctx.remoteAuthority);
|
|
return this._createProcess(uriTransformer, <ICreateTerminalProcessArguments>args);
|
|
}
|
|
case '$attachToProcess': return this._ptyService.attachToProcess.apply(this._ptyService, args);
|
|
case '$detachFromProcess': return this._ptyService.detachFromProcess.apply(this._ptyService, args);
|
|
|
|
case '$listProcesses': return this._ptyService.listProcesses.apply(this._ptyService, args);
|
|
case '$orphanQuestionReply': return this._ptyService.orphanQuestionReply.apply(this._ptyService, args);
|
|
case '$acceptPtyHostResolvedVariables': return this._ptyService.acceptPtyHostResolvedVariables?.apply(this._ptyService, args);
|
|
|
|
case '$start': return this._ptyService.start.apply(this._ptyService, args);
|
|
case '$input': return this._ptyService.input.apply(this._ptyService, args);
|
|
case '$acknowledgeDataEvent': return this._ptyService.acknowledgeDataEvent.apply(this._ptyService, args);
|
|
case '$shutdown': return this._ptyService.shutdown.apply(this._ptyService, args);
|
|
case '$resize': return this._ptyService.resize.apply(this._ptyService, args);
|
|
case '$getInitialCwd': return this._ptyService.getInitialCwd.apply(this._ptyService, args);
|
|
case '$getCwd': return this._ptyService.getCwd.apply(this._ptyService, args);
|
|
|
|
case '$processBinary': return this._ptyService.processBinary.apply(this._ptyService, args);
|
|
|
|
case '$sendCommandResult': return this._sendCommandResult(args[0], args[1], args[2]);
|
|
case '$installAutoReply': return this._ptyService.installAutoReply.apply(this._ptyService, args);
|
|
case '$uninstallAllAutoReplies': return this._ptyService.uninstallAllAutoReplies.apply(this._ptyService, args);
|
|
case '$getDefaultSystemShell': return this._getDefaultSystemShell.apply(this, args);
|
|
case '$getProfiles': return this._getProfiles.apply(this, args);
|
|
case '$getEnvironment': return this._getEnvironment();
|
|
case '$getWslPath': return this._getWslPath(args[0]);
|
|
case '$getTerminalLayoutInfo': return this._ptyService.getTerminalLayoutInfo(<IGetTerminalLayoutInfoArgs>args);
|
|
case '$setTerminalLayoutInfo': return this._ptyService.setTerminalLayoutInfo(<ISetTerminalLayoutInfoArgs>args);
|
|
case '$serializeTerminalState': return this._ptyService.serializeTerminalState.apply(this._ptyService, args);
|
|
case '$reviveTerminalProcesses': return this._ptyService.reviveTerminalProcesses.apply(this._ptyService, args);
|
|
case '$setUnicodeVersion': return this._ptyService.setUnicodeVersion.apply(this._ptyService, args);
|
|
case '$reduceConnectionGraceTime': return this._reduceConnectionGraceTime();
|
|
case '$updateIcon': return this._ptyService.updateIcon.apply(this._ptyService, args);
|
|
case '$updateTitle': return this._ptyService.updateTitle.apply(this._ptyService, args);
|
|
case '$updateProperty': return this._ptyService.updateProperty.apply(this._ptyService, args);
|
|
case '$refreshProperty': return this._ptyService.refreshProperty.apply(this._ptyService, args);
|
|
case '$requestDetachInstance': return this._ptyService.requestDetachInstance(args[0], args[1]);
|
|
case '$acceptDetachedInstance': return this._ptyService.acceptDetachInstanceReply(args[0], args[1]);
|
|
}
|
|
|
|
throw new Error(`IPC Command ${command} not found`);
|
|
}
|
|
|
|
listen(_: any, event: string, arg: any): Event<any> {
|
|
switch (event) {
|
|
case '$onPtyHostExitEvent': return this._ptyService.onPtyHostExit || Event.None;
|
|
case '$onPtyHostStartEvent': return this._ptyService.onPtyHostStart || Event.None;
|
|
case '$onPtyHostUnresponsiveEvent': return this._ptyService.onPtyHostUnresponsive || Event.None;
|
|
case '$onPtyHostResponsiveEvent': return this._ptyService.onPtyHostResponsive || Event.None;
|
|
case '$onPtyHostRequestResolveVariablesEvent': return this._ptyService.onPtyHostRequestResolveVariables || Event.None;
|
|
case '$onProcessDataEvent': return this._ptyService.onProcessData;
|
|
case '$onProcessReadyEvent': return this._ptyService.onProcessReady;
|
|
case '$onProcessExitEvent': return this._ptyService.onProcessExit;
|
|
case '$onProcessReplayEvent': return this._ptyService.onProcessReplay;
|
|
case '$onProcessOrphanQuestion': return this._ptyService.onProcessOrphanQuestion;
|
|
case '$onExecuteCommand': return this.onExecuteCommand;
|
|
case '$onDidRequestDetach': return this._ptyService.onDidRequestDetach || Event.None;
|
|
case '$onDidChangeProperty': return this._ptyService.onDidChangeProperty;
|
|
default:
|
|
break;
|
|
}
|
|
|
|
throw new Error('Not supported');
|
|
}
|
|
|
|
private async _createProcess(uriTransformer: IURITransformer, args: ICreateTerminalProcessArguments): Promise<ICreateTerminalProcessResult> {
|
|
const shellLaunchConfig: IShellLaunchConfig = {
|
|
name: args.shellLaunchConfig.name,
|
|
executable: args.shellLaunchConfig.executable,
|
|
args: args.shellLaunchConfig.args,
|
|
cwd: (
|
|
typeof args.shellLaunchConfig.cwd === 'string' || typeof args.shellLaunchConfig.cwd === 'undefined'
|
|
? args.shellLaunchConfig.cwd
|
|
: URI.revive(uriTransformer.transformIncoming(args.shellLaunchConfig.cwd))
|
|
),
|
|
env: args.shellLaunchConfig.env,
|
|
useShellEnvironment: args.shellLaunchConfig.useShellEnvironment
|
|
};
|
|
|
|
|
|
const baseEnv = await buildUserEnvironment(args.resolverEnv, !!args.shellLaunchConfig.useShellEnvironment, platform.language, false, this._environmentService, this._logService);
|
|
this._logService.trace('baseEnv', baseEnv);
|
|
|
|
const reviveWorkspaceFolder = (workspaceData: IWorkspaceFolderData): IWorkspaceFolder => {
|
|
return {
|
|
uri: URI.revive(uriTransformer.transformIncoming(workspaceData.uri)),
|
|
name: workspaceData.name,
|
|
index: workspaceData.index,
|
|
toResource: () => {
|
|
throw new Error('Not implemented');
|
|
}
|
|
};
|
|
};
|
|
const workspaceFolders = args.workspaceFolders.map(reviveWorkspaceFolder);
|
|
const activeWorkspaceFolder = args.activeWorkspaceFolder ? reviveWorkspaceFolder(args.activeWorkspaceFolder) : undefined;
|
|
const activeFileResource = args.activeFileResource ? URI.revive(uriTransformer.transformIncoming(args.activeFileResource)) : undefined;
|
|
const customVariableResolver = new CustomVariableResolver(baseEnv, workspaceFolders, activeFileResource, args.resolvedVariables);
|
|
const variableResolver = terminalEnvironment.createVariableResolver(activeWorkspaceFolder, process.env, customVariableResolver);
|
|
|
|
// Get the initial cwd
|
|
const initialCwd = terminalEnvironment.getCwd(shellLaunchConfig, os.homedir(), variableResolver, activeWorkspaceFolder?.uri, args.configuration['terminal.integrated.cwd'], this._logService);
|
|
shellLaunchConfig.cwd = initialCwd;
|
|
|
|
const envPlatformKey = platform.isWindows ? 'terminal.integrated.env.windows' : (platform.isMacintosh ? 'terminal.integrated.env.osx' : 'terminal.integrated.env.linux');
|
|
const envFromConfig = args.configuration[envPlatformKey];
|
|
const env = terminalEnvironment.createTerminalEnvironment(
|
|
shellLaunchConfig,
|
|
envFromConfig,
|
|
variableResolver,
|
|
this._productService.version,
|
|
args.configuration['terminal.integrated.detectLocale'],
|
|
baseEnv
|
|
);
|
|
|
|
// Apply extension environment variable collections to the environment
|
|
if (!shellLaunchConfig.strictEnv) {
|
|
const entries: [string, IEnvironmentVariableCollection][] = [];
|
|
for (const [k, v] of args.envVariableCollections) {
|
|
entries.push([k, { map: deserializeEnvironmentVariableCollection(v) }]);
|
|
}
|
|
const envVariableCollections = new Map<string, IEnvironmentVariableCollection>(entries);
|
|
const mergedCollection = new MergedEnvironmentVariableCollection(envVariableCollections);
|
|
mergedCollection.applyToProcessEnvironment(env);
|
|
}
|
|
|
|
// Fork the process and listen for messages
|
|
this._logService.debug(`Terminal process launching on remote agent`, { shellLaunchConfig, initialCwd, cols: args.cols, rows: args.rows, env });
|
|
|
|
// Setup the CLI server to support forwarding commands run from the CLI
|
|
const ipcHandlePath = createRandomIPCHandle();
|
|
env.VSCODE_IPC_HOOK_CLI = ipcHandlePath;
|
|
const commandsExecuter: ICommandsExecuter = {
|
|
executeCommand: <T>(id: string, ...args: any[]): Promise<T> => this._executeCommand(id, args, uriTransformer)
|
|
};
|
|
const cliServer = new CLIServerBase(commandsExecuter, this._logService, ipcHandlePath);
|
|
|
|
const id = await this._ptyService.createProcess(shellLaunchConfig, initialCwd, args.cols, args.rows, args.unicodeVersion, env, baseEnv, args.options, args.shouldPersistTerminal, args.workspaceId, args.workspaceName);
|
|
this._ptyService.onProcessExit(e => e.id === id && cliServer.dispose());
|
|
|
|
return {
|
|
persistentTerminalId: id,
|
|
resolvedShellLaunchConfig: shellLaunchConfig
|
|
};
|
|
}
|
|
|
|
private _executeCommand<T>(commandId: string, commandArgs: any[], uriTransformer: IURITransformer): Promise<T> {
|
|
let resolve!: (data: any) => void;
|
|
let reject!: (err: any) => void;
|
|
const result = new Promise<T>((_resolve, _reject) => {
|
|
resolve = _resolve;
|
|
reject = _reject;
|
|
});
|
|
|
|
const reqId = ++this._lastReqId;
|
|
this._pendingCommands.set(reqId, { resolve, reject, uriTransformer });
|
|
|
|
const serializedCommandArgs = cloneAndChange(commandArgs, (obj) => {
|
|
if (obj && obj.$mid === 1) {
|
|
// this is UriComponents
|
|
return uriTransformer.transformOutgoing(obj);
|
|
}
|
|
if (obj && obj instanceof URI) {
|
|
return uriTransformer.transformOutgoingURI(obj);
|
|
}
|
|
return undefined;
|
|
});
|
|
this._onExecuteCommand.fire({
|
|
reqId,
|
|
commandId,
|
|
commandArgs: serializedCommandArgs
|
|
});
|
|
|
|
return result;
|
|
}
|
|
|
|
private _sendCommandResult(reqId: number, isError: boolean, serializedPayload: any): void {
|
|
const data = this._pendingCommands.get(reqId);
|
|
if (!data) {
|
|
return;
|
|
}
|
|
this._pendingCommands.delete(reqId);
|
|
const payload = cloneAndChange(serializedPayload, (obj) => {
|
|
if (obj && obj.$mid === 1) {
|
|
// this is UriComponents
|
|
return data.uriTransformer.transformIncoming(obj);
|
|
}
|
|
return undefined;
|
|
});
|
|
if (isError) {
|
|
data.reject(payload);
|
|
} else {
|
|
data.resolve(payload);
|
|
}
|
|
}
|
|
|
|
private _getDefaultSystemShell(osOverride?: platform.OperatingSystem): Promise<string> {
|
|
return this._ptyService.getDefaultSystemShell(osOverride);
|
|
}
|
|
|
|
private async _getProfiles(workspaceId: string, profiles: unknown, defaultProfile: unknown, includeDetectedProfiles?: boolean): Promise<ITerminalProfile[]> {
|
|
return this._ptyService.getProfiles?.(workspaceId, profiles, defaultProfile, includeDetectedProfiles) || [];
|
|
}
|
|
|
|
private _getEnvironment(): platform.IProcessEnvironment {
|
|
return { ...process.env };
|
|
}
|
|
|
|
private _getWslPath(original: string): Promise<string> {
|
|
return this._ptyService.getWslPath(original);
|
|
}
|
|
|
|
|
|
private _reduceConnectionGraceTime(): Promise<void> {
|
|
return this._ptyService.reduceConnectionGraceTime();
|
|
}
|
|
}
|