Book a Demo!
CoCalc Logo Icon
StoreFeaturesDocsShareSupportNewsAboutPoliciesSign UpSign In
microsoft
GitHub Repository: microsoft/vscode
Path: blob/main/src/vs/platform/externalTerminal/node/externalTerminalService.ts
3296 views
1
/*---------------------------------------------------------------------------------------------
2
* Copyright (c) Microsoft Corporation. All rights reserved.
3
* Licensed under the MIT License. See License.txt in the project root for license information.
4
*--------------------------------------------------------------------------------------------*/
5
6
import * as cp from 'child_process';
7
import { memoize } from '../../../base/common/decorators.js';
8
import { FileAccess } from '../../../base/common/network.js';
9
import * as path from '../../../base/common/path.js';
10
import * as env from '../../../base/common/platform.js';
11
import { sanitizeProcessEnvironment } from '../../../base/common/processes.js';
12
import * as pfs from '../../../base/node/pfs.js';
13
import * as processes from '../../../base/node/processes.js';
14
import * as nls from '../../../nls.js';
15
import { DEFAULT_TERMINAL_OSX, IExternalTerminalService, IExternalTerminalSettings, ITerminalForPlatform } from '../common/externalTerminal.js';
16
import { ITerminalEnvironment } from '../../terminal/common/terminal.js';
17
18
const TERMINAL_TITLE = nls.localize('console.title', "VS Code Console");
19
20
abstract class ExternalTerminalService {
21
public _serviceBrand: undefined;
22
23
async getDefaultTerminalForPlatforms(): Promise<ITerminalForPlatform> {
24
return {
25
windows: WindowsExternalTerminalService.getDefaultTerminalWindows(),
26
linux: await LinuxExternalTerminalService.getDefaultTerminalLinuxReady(),
27
osx: 'xterm'
28
};
29
}
30
}
31
32
export class WindowsExternalTerminalService extends ExternalTerminalService implements IExternalTerminalService {
33
private static readonly CMD = 'cmd.exe';
34
private static _DEFAULT_TERMINAL_WINDOWS: string;
35
36
public openTerminal(configuration: IExternalTerminalSettings, cwd?: string): Promise<void> {
37
return this.spawnTerminal(cp, configuration, processes.getWindowsShell(), cwd);
38
}
39
40
public spawnTerminal(spawner: typeof cp, configuration: IExternalTerminalSettings, command: string, cwd?: string): Promise<void> {
41
const exec = configuration.windowsExec || WindowsExternalTerminalService.getDefaultTerminalWindows();
42
43
// Make the drive letter uppercase on Windows (see #9448)
44
if (cwd && cwd[1] === ':') {
45
cwd = cwd[0].toUpperCase() + cwd.substr(1);
46
}
47
48
// cmder ignores the environment cwd and instead opts to always open in %USERPROFILE%
49
// unless otherwise specified
50
const basename = path.basename(exec, '.exe').toLowerCase();
51
if (basename === 'cmder') {
52
spawner.spawn(exec, cwd ? [cwd] : undefined);
53
return Promise.resolve(undefined);
54
}
55
56
const cmdArgs = ['/c', 'start', '/wait'];
57
if (exec.indexOf(' ') >= 0) {
58
// The "" argument is the window title. Without this, exec doesn't work when the path
59
// contains spaces. #6590
60
// Title is Execution Path. #220129
61
cmdArgs.push(exec);
62
}
63
cmdArgs.push(exec);
64
// Add starting directory parameter for Windows Terminal (see #90734)
65
if (basename === 'wt') {
66
cmdArgs.push('-d .');
67
}
68
69
return new Promise<void>((c, e) => {
70
const env = getSanitizedEnvironment(process);
71
const child = spawner.spawn(command, cmdArgs, { cwd, env, detached: true });
72
child.on('error', e);
73
child.on('exit', () => c());
74
});
75
}
76
77
public async runInTerminal(title: string, dir: string, args: string[], envVars: ITerminalEnvironment, settings: IExternalTerminalSettings): Promise<number | undefined> {
78
const exec = 'windowsExec' in settings && settings.windowsExec ? settings.windowsExec : WindowsExternalTerminalService.getDefaultTerminalWindows();
79
const wt = await WindowsExternalTerminalService.getWtExePath();
80
81
return new Promise<number | undefined>((resolve, reject) => {
82
83
const title = `"${dir} - ${TERMINAL_TITLE}"`;
84
const command = `"${args.join('" "')}" & pause`; // use '|' to only pause on non-zero exit code
85
86
// merge environment variables into a copy of the process.env
87
const env = Object.assign({}, getSanitizedEnvironment(process), envVars);
88
89
// delete environment variables that have a null value
90
Object.keys(env).filter(v => env[v] === null).forEach(key => delete env[key]);
91
92
const options: any = {
93
cwd: dir,
94
env: env,
95
windowsVerbatimArguments: true
96
};
97
98
let spawnExec: string;
99
let cmdArgs: string[];
100
101
if (path.basename(exec, '.exe') === 'wt') {
102
// Handle Windows Terminal specially; -d to set the cwd and run a cmd.exe instance
103
// inside it
104
spawnExec = exec;
105
cmdArgs = ['-d', '.', WindowsExternalTerminalService.CMD, '/c', command];
106
} else if (wt) {
107
// prefer to use the window terminal to spawn if it's available instead
108
// of start, since that allows ctrl+c handling (#81322)
109
spawnExec = wt;
110
cmdArgs = ['-d', '.', exec, '/c', command];
111
} else {
112
spawnExec = WindowsExternalTerminalService.CMD;
113
cmdArgs = ['/c', 'start', title, '/wait', exec, '/c', `"${command}"`];
114
}
115
116
const cmd = cp.spawn(spawnExec, cmdArgs, options);
117
118
cmd.on('error', err => {
119
reject(improveError(err));
120
});
121
122
resolve(undefined);
123
});
124
}
125
126
public static getDefaultTerminalWindows(): string {
127
if (!WindowsExternalTerminalService._DEFAULT_TERMINAL_WINDOWS) {
128
const isWoW64 = !!process.env.hasOwnProperty('PROCESSOR_ARCHITEW6432');
129
WindowsExternalTerminalService._DEFAULT_TERMINAL_WINDOWS = `${process.env.windir ? process.env.windir : 'C:\\Windows'}\\${isWoW64 ? 'Sysnative' : 'System32'}\\cmd.exe`;
130
}
131
return WindowsExternalTerminalService._DEFAULT_TERMINAL_WINDOWS;
132
}
133
134
@memoize
135
private static async getWtExePath() {
136
try {
137
return await processes.findExecutable('wt');
138
} catch {
139
return undefined;
140
}
141
}
142
}
143
144
export class MacExternalTerminalService extends ExternalTerminalService implements IExternalTerminalService {
145
private static readonly OSASCRIPT = '/usr/bin/osascript'; // osascript is the AppleScript interpreter on OS X
146
147
public openTerminal(configuration: IExternalTerminalSettings, cwd?: string): Promise<void> {
148
return this.spawnTerminal(cp, configuration, cwd);
149
}
150
151
public runInTerminal(title: string, dir: string, args: string[], envVars: ITerminalEnvironment, settings: IExternalTerminalSettings): Promise<number | undefined> {
152
153
const terminalApp = settings.osxExec || DEFAULT_TERMINAL_OSX;
154
155
return new Promise<number | undefined>((resolve, reject) => {
156
157
if (terminalApp === DEFAULT_TERMINAL_OSX || terminalApp === 'iTerm.app') {
158
159
// On OS X we launch an AppleScript that creates (or reuses) a Terminal window
160
// and then launches the program inside that window.
161
162
const script = terminalApp === DEFAULT_TERMINAL_OSX ? 'TerminalHelper' : 'iTermHelper';
163
const scriptpath = FileAccess.asFileUri(`vs/workbench/contrib/externalTerminal/node/${script}.scpt`).fsPath;
164
165
const osaArgs = [
166
scriptpath,
167
'-t', title || TERMINAL_TITLE,
168
'-w', dir,
169
];
170
171
for (const a of args) {
172
osaArgs.push('-a');
173
osaArgs.push(a);
174
}
175
176
if (envVars) {
177
// merge environment variables into a copy of the process.env
178
const env = Object.assign({}, getSanitizedEnvironment(process), envVars);
179
180
for (const key in env) {
181
const value = env[key];
182
if (value === null) {
183
osaArgs.push('-u');
184
osaArgs.push(key);
185
} else {
186
osaArgs.push('-e');
187
osaArgs.push(`${key}=${value}`);
188
}
189
}
190
}
191
192
let stderr = '';
193
const osa = cp.spawn(MacExternalTerminalService.OSASCRIPT, osaArgs);
194
osa.on('error', err => {
195
reject(improveError(err));
196
});
197
osa.stderr.on('data', (data) => {
198
stderr += data.toString();
199
});
200
osa.on('exit', (code: number) => {
201
if (code === 0) { // OK
202
resolve(undefined);
203
} else {
204
if (stderr) {
205
const lines = stderr.split('\n', 1);
206
reject(new Error(lines[0]));
207
} else {
208
reject(new Error(nls.localize('mac.terminal.script.failed', "Script '{0}' failed with exit code {1}", script, code)));
209
}
210
}
211
});
212
} else {
213
reject(new Error(nls.localize('mac.terminal.type.not.supported', "'{0}' not supported", terminalApp)));
214
}
215
});
216
}
217
218
spawnTerminal(spawner: typeof cp, configuration: IExternalTerminalSettings, cwd?: string): Promise<void> {
219
const terminalApp = configuration.osxExec || DEFAULT_TERMINAL_OSX;
220
221
return new Promise<void>((c, e) => {
222
const args = ['-a', terminalApp];
223
if (cwd) {
224
args.push(cwd);
225
}
226
const env = getSanitizedEnvironment(process);
227
const child = spawner.spawn('/usr/bin/open', args, { cwd, env });
228
child.on('error', e);
229
child.on('exit', () => c());
230
});
231
}
232
}
233
234
export class LinuxExternalTerminalService extends ExternalTerminalService implements IExternalTerminalService {
235
236
private static readonly WAIT_MESSAGE = nls.localize('press.any.key', "Press any key to continue...");
237
238
public openTerminal(configuration: IExternalTerminalSettings, cwd?: string): Promise<void> {
239
return this.spawnTerminal(cp, configuration, cwd);
240
}
241
242
public runInTerminal(title: string, dir: string, args: string[], envVars: ITerminalEnvironment, settings: IExternalTerminalSettings): Promise<number | undefined> {
243
244
const execPromise = settings.linuxExec ? Promise.resolve(settings.linuxExec) : LinuxExternalTerminalService.getDefaultTerminalLinuxReady();
245
246
return new Promise<number | undefined>((resolve, reject) => {
247
248
const termArgs: string[] = [];
249
//termArgs.push('--title');
250
//termArgs.push(`"${TERMINAL_TITLE}"`);
251
execPromise.then(exec => {
252
if (exec.indexOf('gnome-terminal') >= 0) {
253
termArgs.push('-x');
254
} else {
255
termArgs.push('-e');
256
}
257
termArgs.push('bash');
258
termArgs.push('-c');
259
260
const bashCommand = `${quote(args)}; echo; read -p "${LinuxExternalTerminalService.WAIT_MESSAGE}" -n1;`;
261
termArgs.push(`''${bashCommand}''`); // wrapping argument in two sets of ' because node is so "friendly" that it removes one set...
262
263
264
// merge environment variables into a copy of the process.env
265
const env = Object.assign({}, getSanitizedEnvironment(process), envVars);
266
267
// delete environment variables that have a null value
268
Object.keys(env).filter(v => env[v] === null).forEach(key => delete env[key]);
269
270
const options: any = {
271
cwd: dir,
272
env: env
273
};
274
275
let stderr = '';
276
const cmd = cp.spawn(exec, termArgs, options);
277
cmd.on('error', err => {
278
reject(improveError(err));
279
});
280
cmd.stderr.on('data', (data) => {
281
stderr += data.toString();
282
});
283
cmd.on('exit', (code: number) => {
284
if (code === 0) { // OK
285
resolve(undefined);
286
} else {
287
if (stderr) {
288
const lines = stderr.split('\n', 1);
289
reject(new Error(lines[0]));
290
} else {
291
reject(new Error(nls.localize('linux.term.failed', "'{0}' failed with exit code {1}", exec, code)));
292
}
293
}
294
});
295
});
296
});
297
}
298
299
private static _DEFAULT_TERMINAL_LINUX_READY: Promise<string>;
300
301
public static async getDefaultTerminalLinuxReady(): Promise<string> {
302
if (!LinuxExternalTerminalService._DEFAULT_TERMINAL_LINUX_READY) {
303
if (!env.isLinux) {
304
LinuxExternalTerminalService._DEFAULT_TERMINAL_LINUX_READY = Promise.resolve('xterm');
305
} else {
306
const isDebian = await pfs.Promises.exists('/etc/debian_version');
307
LinuxExternalTerminalService._DEFAULT_TERMINAL_LINUX_READY = new Promise<string>(r => {
308
if (isDebian) {
309
r('x-terminal-emulator');
310
} else if (process.env.DESKTOP_SESSION === 'gnome' || process.env.DESKTOP_SESSION === 'gnome-classic') {
311
r('gnome-terminal');
312
} else if (process.env.DESKTOP_SESSION === 'kde-plasma') {
313
r('konsole');
314
} else if (process.env.COLORTERM) {
315
r(process.env.COLORTERM);
316
} else if (process.env.TERM) {
317
r(process.env.TERM);
318
} else {
319
r('xterm');
320
}
321
});
322
}
323
}
324
return LinuxExternalTerminalService._DEFAULT_TERMINAL_LINUX_READY;
325
}
326
327
spawnTerminal(spawner: typeof cp, configuration: IExternalTerminalSettings, cwd?: string): Promise<void> {
328
const execPromise = configuration.linuxExec ? Promise.resolve(configuration.linuxExec) : LinuxExternalTerminalService.getDefaultTerminalLinuxReady();
329
330
return new Promise<void>((c, e) => {
331
execPromise.then(exec => {
332
const env = getSanitizedEnvironment(process);
333
const child = spawner.spawn(exec, [], { cwd, env });
334
child.on('error', e);
335
child.on('exit', () => c());
336
});
337
});
338
}
339
}
340
341
function getSanitizedEnvironment(process: NodeJS.Process) {
342
const env = { ...process.env };
343
sanitizeProcessEnvironment(env);
344
return env;
345
}
346
347
/**
348
* tries to turn OS errors into more meaningful error messages
349
*/
350
function improveError(err: Error & { errno?: string; path?: string }): Error {
351
if ('errno' in err && err['errno'] === 'ENOENT' && 'path' in err && typeof err['path'] === 'string') {
352
return new Error(nls.localize('ext.term.app.not.found', "can't find terminal application '{0}'", err['path']));
353
}
354
return err;
355
}
356
357
/**
358
* Quote args if necessary and combine into a space separated string.
359
*/
360
function quote(args: string[]): string {
361
let r = '';
362
for (const a of args) {
363
if (a.indexOf(' ') >= 0) {
364
r += '"' + a + '"';
365
} else {
366
r += a;
367
}
368
r += ' ';
369
}
370
return r;
371
}
372
373