Skip to content

Commit d3f9bdc

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

File tree

4 files changed

+262
-4
lines changed

4 files changed

+262
-4
lines changed

build/gulpfile.server.js

Lines changed: 2 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -102,7 +102,8 @@ function defineTasks(options) {
102102
buildfile.entrypoint(`vs/${qualifier}/node/server`),
103103
buildfile.entrypoint('vs/workbench/services/extensions/node/extensionHostProcess'),
104104
buildfile.entrypoint('vs/platform/files/node/watcher/unix/watcherApp'),
105-
buildfile.entrypoint('vs/platform/files/node/watcher/nsfw/watcherApp')
105+
buildfile.entrypoint('vs/platform/files/node/watcher/nsfw/watcherApp'),
106+
buildfile.entrypoint('vs/platform/terminal/node/ptyHostMain')
106107
]);
107108

108109
const outWeb = `out-${qualifier}-web`;

src/vs/server/browser/workbench/workbench.ts

Lines changed: 8 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -1,6 +1,5 @@
11
/*---------------------------------------------------------------------------------------------
2-
* Copyright (c) Microsoft Corporation. All rights reserved.
3-
* Licensed under the MIT License. See License.txt in the project root for license information.
2+
* Copyright (c) Gitpod. All rights reserved.
43
*--------------------------------------------------------------------------------------------*/
54

65
import { isStandalone } from 'vs/base/browser/browser';
@@ -374,8 +373,15 @@ class WindowIndicator implements IWindowIndicator {
374373
const webviewEndpoint = new URL(window.location.href);
375374
webviewEndpoint.pathname = '/out/vs/workbench/contrib/webview/browser/pre/';
376375
webviewEndpoint.search = '';
376+
377+
// TODO(ak) secure by using external endpoint
378+
const webWorkerExtensionEndpoint = new URL(window.location.href);
379+
webWorkerExtensionEndpoint.pathname = `/out/vs/workbench/services/extensions/worker/${window.location.protocol === 'https:' ? 'https' : 'http'}WebWorkerExtensionHostIframe.html`;
380+
webWorkerExtensionEndpoint.search = '';
381+
377382
create(document.body, {
378383
webviewEndpoint: webviewEndpoint.href,
384+
webWorkerExtensionHostIframeSrc: webWorkerExtensionEndpoint.href,
379385
remoteAuthority,
380386
webSocketFactory: {
381387
create: url => {

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

Lines changed: 246 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,246 @@
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 { 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+
if (event === '$onExecuteCommand') {
64+
return Event.None;
65+
}
66+
const serviceRecord = ptyHostService as unknown as Record<string, Event<any>>;
67+
const result = serviceRecord[event.substring(1, event.endsWith('Event') ? event.length - 'Event'.length : undefined)];
68+
if (!result) {
69+
logService.error('Unknown event: ' + event);
70+
return Event.None;
71+
}
72+
return result;
73+
}
74+
75+
});
76+
}
77+
78+
interface Services {
79+
ptyHostService: PtyHostService, logService: ILogService
80+
}
81+
82+
async function createProcess(args: ICreateTerminalProcessArguments, services: Services): Promise<ICreateTerminalProcessResult> {
83+
const shellLaunchConfigDto = args.shellLaunchConfig;
84+
// See $spawnExtHostProcess in src/vs/workbench/api/node/extHostTerminalService.ts for a reference implementation
85+
const shellLaunchConfig: IShellLaunchConfig = {
86+
name: shellLaunchConfigDto.name,
87+
executable: shellLaunchConfigDto.executable,
88+
args: shellLaunchConfigDto.args,
89+
cwd: typeof shellLaunchConfigDto.cwd === 'string' ? shellLaunchConfigDto.cwd : URI.revive(shellLaunchConfigDto.cwd),
90+
env: shellLaunchConfigDto.env
91+
};
92+
93+
let lastActiveWorkspace: IWorkspaceFolder | undefined;
94+
if (args.activeWorkspaceFolder) {
95+
lastActiveWorkspace = toWorkspaceFolder(args.activeWorkspaceFolder);
96+
}
97+
98+
const processEnv = { ...process.env, ...args.resolverEnv } as platform.IProcessEnvironment;
99+
const configurationResolverService = new RemoteTerminalVariableResolverService(
100+
args.workspaceFolders.map(toWorkspaceFolder),
101+
args.resolvedVariables,
102+
args.activeFileResource ? URI.revive(args.activeFileResource) : undefined,
103+
processEnv
104+
);
105+
const variableResolver = createVariableResolver(lastActiveWorkspace, processEnv, configurationResolverService);
106+
107+
// Merge in shell and args from settings
108+
if (!shellLaunchConfig.executable) {
109+
shellLaunchConfig.executable = getDefaultShell(
110+
key => args.configuration[key],
111+
getSystemShellSync(platform.OS, process.env as platform.IProcessEnvironment),
112+
process.env.hasOwnProperty('PROCESSOR_ARCHITEW6432'),
113+
process.env.windir,
114+
variableResolver,
115+
services.logService,
116+
false
117+
);
118+
shellLaunchConfig.args = getDefaultShellArgs(
119+
key => args.configuration[key],
120+
false,
121+
variableResolver,
122+
services.logService
123+
);
124+
} else if (variableResolver) {
125+
shellLaunchConfig.executable = variableResolver(shellLaunchConfig.executable);
126+
if (shellLaunchConfig.args) {
127+
if (Array.isArray(shellLaunchConfig.args)) {
128+
const resolvedArgs: string[] = [];
129+
for (const arg of shellLaunchConfig.args) {
130+
resolvedArgs.push(variableResolver(arg));
131+
}
132+
shellLaunchConfig.args = resolvedArgs;
133+
} else {
134+
shellLaunchConfig.args = variableResolver(shellLaunchConfig.args);
135+
}
136+
}
137+
}
138+
139+
// Get the initial cwd
140+
const initialCwd = getCwd(
141+
shellLaunchConfig,
142+
os.homedir(),
143+
variableResolver,
144+
lastActiveWorkspace?.uri,
145+
args.configuration['terminal.integrated.cwd'], services.logService
146+
);
147+
shellLaunchConfig.cwd = initialCwd;
148+
149+
const env = createTerminalEnvironment(
150+
shellLaunchConfig,
151+
args.configuration['terminal.integrated.env.linux'],
152+
variableResolver,
153+
product.version,
154+
args.configuration['terminal.integrated.detectLocale'] || 'auto',
155+
processEnv
156+
);
157+
158+
// Apply extension environment variable collections to the environment
159+
if (!shellLaunchConfig.strictEnv) {
160+
const collection = new Map<string, IEnvironmentVariableCollection>();
161+
for (const [name, serialized] of args.envVariableCollections) {
162+
collection.set(name, {
163+
map: deserializeEnvironmentVariableCollection(serialized)
164+
});
165+
}
166+
const mergedCollection = new MergedEnvironmentVariableCollection(collection);
167+
mergedCollection.applyToProcessEnvironment(env, variableResolver);
168+
}
169+
170+
const persistentTerminalId = await services.ptyHostService.createProcess(shellLaunchConfig, initialCwd, args.cols, args.rows,
171+
env, processEnv, false, args.shouldPersistTerminal, args.workspaceId, args.workspaceName);
172+
return {
173+
persistentTerminalId,
174+
resolvedShellLaunchConfig: shellLaunchConfig
175+
};
176+
}
177+
178+
function toWorkspaceFolder(data: IWorkspaceFolderData): IWorkspaceFolder {
179+
return {
180+
uri: URI.revive(data.uri),
181+
name: data.name,
182+
index: data.index,
183+
toResource: () => {
184+
throw new Error('Not implemented');
185+
}
186+
};
187+
}
188+
189+
/**
190+
* See ExtHostVariableResolverService in src/vs/workbench/api/common/extHostDebugService.ts for a reference implementation.
191+
*/
192+
class RemoteTerminalVariableResolverService extends AbstractVariableResolverService {
193+
194+
private readonly structure = TernarySearchTree.forUris<IWorkspaceFolder>(() => false);
195+
196+
constructor(folders: IWorkspaceFolder[], resolvedVariables: { [name: string]: string }, activeFileResource: URI | undefined, env: platform.IProcessEnvironment) {
197+
super({
198+
getFolderUri: (folderName: string): URI | undefined => {
199+
const found = folders.filter(f => f.name === folderName);
200+
if (found && found.length > 0) {
201+
return found[0].uri;
202+
}
203+
return undefined;
204+
},
205+
getWorkspaceFolderCount: (): number => {
206+
return folders.length;
207+
},
208+
getConfigurationValue: (folderUri: URI | undefined, section: string): string | undefined => {
209+
return resolvedVariables['config:' + section];
210+
},
211+
getAppRoot: (): string | undefined => {
212+
return env['VSCODE_CWD'] || process.cwd();
213+
},
214+
getExecPath: (): string | undefined => {
215+
return env['VSCODE_EXEC_PATH'];
216+
},
217+
getFilePath: (): string | undefined => {
218+
if (activeFileResource) {
219+
return path.normalize(activeFileResource.fsPath);
220+
}
221+
return undefined;
222+
},
223+
getWorkspaceFolderPathForFile: (): string | undefined => {
224+
if (activeFileResource) {
225+
const ws = this.structure.findSubstr(activeFileResource);
226+
if (ws) {
227+
return path.normalize(ws.uri.fsPath);
228+
}
229+
}
230+
return undefined;
231+
},
232+
getSelectedText: (): string | undefined => {
233+
return resolvedVariables.selectedText;
234+
},
235+
getLineNumber: (): string | undefined => {
236+
return resolvedVariables.lineNumber;
237+
}
238+
}, undefined, Promise.resolve(env));
239+
240+
// Set up the workspace folder data structure
241+
folders.forEach(folder => {
242+
this.structure.set(folder.uri, folder);
243+
});
244+
}
245+
246+
}

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)