Skip to content

Commit 71fdf50

Browse files
spoenemannakosyakov
authored andcommitted
Added remote terminal channel handler
1 parent 9001270 commit 71fdf50

File tree

2 files changed

+249
-1
lines changed

2 files changed

+249
-1
lines changed

src/vs/server/node/remote-terminal.ts

Lines changed: 243 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,243 @@
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+
}

src/vs/server/node/server.ts

Lines changed: 6 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -2,6 +2,11 @@
22
* Copyright (c) Gitpod. All rights reserved.
33
*--------------------------------------------------------------------------------------------*/
44

5+
import { registerRemoteTerminal } from 'vs/server/node/remote-terminal';
56
import { main } from 'vs/server/node/server.main';
67

7-
main({});
8+
main({
9+
start: (services, channelServer) => {
10+
registerRemoteTerminal(services, channelServer);
11+
}
12+
});

0 commit comments

Comments
 (0)