externalTerminalService.ts 14.1 KB
Newer Older
E
Erich Gamma 已提交
1 2 3 4
/*---------------------------------------------------------------------------------------------
 *  Copyright (c) Microsoft Corporation. All rights reserved.
 *  Licensed under the MIT License. See License.txt in the project root for license information.
 *--------------------------------------------------------------------------------------------*/
5

6
import * as cp from 'child_process';
7
import * as path from 'vs/base/common/path';
8 9
import * as processes from 'vs/base/node/processes';
import * as nls from 'vs/nls';
10 11
import * as pfs from 'vs/base/node/pfs';
import * as env from 'vs/base/common/platform';
Y
Yuki Ueda 已提交
12
import { assign } from 'vs/base/common/objects';
A
Andre Weinand 已提交
13
import { IExternalTerminalService, IExternalTerminalConfiguration, IExternalTerminalSettings } from 'vs/workbench/contrib/externalTerminal/common/externalTerminal';
J
Johannes Rieken 已提交
14
import { IConfigurationService } from 'vs/platform/configuration/common/configuration';
15
import { getPathFromAmdModule } from 'vs/base/common/amd';
16 17 18
import { IConfigurationRegistry, Extensions, ConfigurationScope } from 'vs/platform/configuration/common/configurationRegistry';
import { registerSingleton } from 'vs/platform/instantiation/common/extensions';
import { Registry } from 'vs/platform/registry/common/platform';
19
import { optional } from 'vs/platform/instantiation/common/instantiation';
E
Erich Gamma 已提交
20

21

22
const TERMINAL_TITLE = nls.localize('console.title', "VS Code Console");
23
export const DEFAULT_TERMINAL_OSX = 'Terminal.app';
24

25
export class WindowsExternalTerminalService implements IExternalTerminalService {
26
	public _serviceBrand: undefined;
E
Erich Gamma 已提交
27

28
	private static readonly CMD = 'cmd.exe';
29

E
Erich Gamma 已提交
30
	constructor(
31
		@optional(IConfigurationService) private readonly _configurationService: IConfigurationService
E
Erich Gamma 已提交
32 33 34
	) {
	}

D
Daniel Imms 已提交
35
	public openTerminal(cwd?: string): void {
36 37 38 39
		if (this._configurationService) {
			const configuration = this._configurationService.getValue<IExternalTerminalConfiguration>();
			this.spawnTerminal(cp, configuration, processes.getWindowsShell(), cwd);
		}
40 41
	}

A
Andre Weinand 已提交
42
	public runInTerminal(title: string, dir: string, args: string[], envVars: env.IProcessEnvironment, settings: IExternalTerminalSettings): Promise<number | undefined> {
43

A
Andre Weinand 已提交
44
		const exec = settings.windowsExec || WindowsExternalTerminalService.getDefaultTerminalWindows();
45 46

		return new Promise<number | undefined>((resolve, reject) => {
47 48 49 50 51

			const title = `"${dir} - ${TERMINAL_TITLE}"`;
			const command = `""${args.join('" "')}" & pause"`; // use '|' to only pause on non-zero exit code

			const cmdArgs = [
52
				'/c', 'start', title, '/wait', exec, '/c', command
53 54 55
			];

			// merge environment variables into a copy of the process.env
Y
Yuki Ueda 已提交
56
			const env = assign({}, process.env, envVars);
57

58
			// delete environment variables that have a null value
59
			Object.keys(env).filter(v => env[v] === null).forEach(key => delete env[key]);
60

61 62 63 64 65 66
			const options: any = {
				cwd: dir,
				env: env,
				windowsVerbatimArguments: true
			};

67
			const cmd = cp.spawn(WindowsExternalTerminalService.CMD, cmdArgs, options);
68 69 70
			cmd.on('error', err => {
				reject(improveError(err));
			});
71

72
			resolve(undefined);
73 74 75
		});
	}

76
	private spawnTerminal(spawner: typeof cp, configuration: IExternalTerminalConfiguration, command: string, cwd?: string): Promise<void> {
D
Daniel Imms 已提交
77
		const terminalConfig = configuration.terminal.external;
78
		const exec = terminalConfig.windowsExec || WindowsExternalTerminalService.getDefaultTerminalWindows();
79

80
		// Make the drive letter uppercase on Windows (see #9448)
D
Daniel Imms 已提交
81 82
		if (cwd && cwd[1] === ':') {
			cwd = cwd[0].toUpperCase() + cwd.substr(1);
83 84
		}

85 86
		// cmder ignores the environment cwd and instead opts to always open in %USERPROFILE%
		// unless otherwise specified
87 88
		const basename = path.basename(exec).toLowerCase();
		if (basename === 'cmder' || basename === 'cmder.exe') {
89
			spawner.spawn(exec, cwd ? [cwd] : undefined);
R
Rob Lourens 已提交
90
			return Promise.resolve(undefined);
91 92
		}

D
Daniel Imms 已提交
93 94 95 96 97 98 99
		const cmdArgs = ['/c', 'start', '/wait'];
		if (exec.indexOf(' ') >= 0) {
			// The "" argument is the window title. Without this, exec doesn't work when the path
			// contains spaces
			cmdArgs.push('""');
		}
		cmdArgs.push(exec);
100

101
		return new Promise<void>((c, e) => {
R
Rob Lourens 已提交
102
			const env = cwd ? { cwd: cwd } : undefined;
D
Daniel Imms 已提交
103
			const child = spawner.spawn(command, cmdArgs, env);
104
			child.on('error', e);
M
Matt Bierner 已提交
105
			child.on('exit', () => c());
106
		});
E
Erich Gamma 已提交
107
	}
108

109 110 111 112 113 114
	private static _DEFAULT_TERMINAL_WINDOWS: string;

	public static getDefaultTerminalWindows(): string {
		if (!WindowsExternalTerminalService._DEFAULT_TERMINAL_WINDOWS) {
			const isWoW64 = !!process.env.hasOwnProperty('PROCESSOR_ARCHITEW6432');
			WindowsExternalTerminalService._DEFAULT_TERMINAL_WINDOWS = `${process.env.windir ? process.env.windir : 'C:\\Windows'}\\${isWoW64 ? 'Sysnative' : 'System32'}\\cmd.exe`;
115
		}
116
		return WindowsExternalTerminalService._DEFAULT_TERMINAL_WINDOWS;
117
	}
E
Erich Gamma 已提交
118 119
}

120
export class MacExternalTerminalService implements IExternalTerminalService {
121
	public _serviceBrand: undefined;
122

123
	private static readonly OSASCRIPT = '/usr/bin/osascript';	// osascript is the AppleScript interpreter on OS X
124

125
	constructor(
126
		@optional(IConfigurationService) private readonly _configurationService: IConfigurationService
127
	) { }
E
Erich Gamma 已提交
128

D
Daniel Imms 已提交
129
	public openTerminal(cwd?: string): void {
130 131 132 133
		if (this._configurationService) {
			const configuration = this._configurationService.getValue<IExternalTerminalConfiguration>();
			this.spawnTerminal(cp, configuration, cwd);
		}
134 135
	}

A
Andre Weinand 已提交
136
	public runInTerminal(title: string, dir: string, args: string[], envVars: env.IProcessEnvironment, settings: IExternalTerminalSettings): Promise<number | undefined> {
137

A
Andre Weinand 已提交
138
		const terminalApp = settings.osxExec || DEFAULT_TERMINAL_OSX;
139 140

		return new Promise<number | undefined>((resolve, reject) => {
141

142
			if (terminalApp === DEFAULT_TERMINAL_OSX || terminalApp === 'iTerm.app') {
143

144 145
				// On OS X we launch an AppleScript that creates (or reuses) a Terminal window
				// and then launches the program inside that window.
146

147
				const script = terminalApp === DEFAULT_TERMINAL_OSX ? 'TerminalHelper' : 'iTermHelper';
148
				const scriptpath = getPathFromAmdModule(require, `vs/workbench/contrib/externalTerminal/node/${script}.scpt`);
149

150
				const osaArgs = [
151
					scriptpath,
152 153 154
					'-t', title || TERMINAL_TITLE,
					'-w', dir,
				];
155

156
				for (let a of args) {
A
Andre Weinand 已提交
157
					osaArgs.push('-a');
158
					osaArgs.push(a);
159 160
				}

161 162
				if (envVars) {
					for (let key in envVars) {
163 164 165 166 167 168 169 170
						const value = envVars[key];
						if (value === null) {
							osaArgs.push('-u');
							osaArgs.push(key);
						} else {
							osaArgs.push('-e');
							osaArgs.push(`${key}=${value}`);
						}
171 172
					}
				}
173 174

				let stderr = '';
175
				const osa = cp.spawn(MacExternalTerminalService.OSASCRIPT, osaArgs);
176 177 178
				osa.on('error', err => {
					reject(improveError(err));
				});
179 180 181 182 183
				osa.stderr.on('data', (data) => {
					stderr += data.toString();
				});
				osa.on('exit', (code: number) => {
					if (code === 0) {	// OK
184
						resolve(undefined);
185 186 187
					} else {
						if (stderr) {
							const lines = stderr.split('\n', 1);
188
							reject(new Error(lines[0]));
189
						} else {
190
							reject(new Error(nls.localize('mac.terminal.script.failed', "Script '{0}' failed with exit code {1}", script, code)));
191 192 193
						}
					}
				});
194
			} else {
195
				reject(new Error(nls.localize('mac.terminal.type.not.supported', "'{0}' not supported", terminalApp)));
196
			}
197 198 199
		});
	}

200
	private spawnTerminal(spawner: typeof cp, configuration: IExternalTerminalConfiguration, cwd?: string): Promise<void> {
D
Daniel Imms 已提交
201 202
		const terminalConfig = configuration.terminal.external;
		const terminalApp = terminalConfig.osxExec || DEFAULT_TERMINAL_OSX;
E
Erich Gamma 已提交
203

204
		return new Promise<void>((c, e) => {
205 206 207 208 209
			const args = ['-a', terminalApp];
			if (cwd) {
				args.push(cwd);
			}
			const child = spawner.spawn('/usr/bin/open', args);
E
Erich Gamma 已提交
210
			child.on('error', e);
M
Matt Bierner 已提交
211
			child.on('exit', () => c());
212
		});
E
Erich Gamma 已提交
213 214 215
	}
}

216
export class LinuxExternalTerminalService implements IExternalTerminalService {
217
	public _serviceBrand: undefined;
E
Erich Gamma 已提交
218

219
	private static readonly WAIT_MESSAGE = nls.localize('press.any.key', "Press any key to continue...");
220

221
	constructor(
222
		@optional(IConfigurationService) private readonly _configurationService: IConfigurationService
223 224
	) { }

D
Daniel Imms 已提交
225
	public openTerminal(cwd?: string): void {
226 227 228 229
		if (this._configurationService) {
			const configuration = this._configurationService.getValue<IExternalTerminalConfiguration>();
			this.spawnTerminal(cp, configuration, cwd);
		}
230 231
	}

A
Andre Weinand 已提交
232
	public runInTerminal(title: string, dir: string, args: string[], envVars: env.IProcessEnvironment, settings: IExternalTerminalSettings): Promise<number | undefined> {
233

A
Andre Weinand 已提交
234
		const execPromise = settings.linuxExec ? Promise.resolve(settings.linuxExec) : LinuxExternalTerminalService.getDefaultTerminalLinuxReady();
235

236
		return new Promise<number | undefined>((resolve, reject) => {
237

238 239 240
			let termArgs: string[] = [];
			//termArgs.push('--title');
			//termArgs.push(`"${TERMINAL_TITLE}"`);
241 242 243 244 245 246 247 248
			execPromise.then(exec => {
				if (exec.indexOf('gnome-terminal') >= 0) {
					termArgs.push('-x');
				} else {
					termArgs.push('-e');
				}
				termArgs.push('bash');
				termArgs.push('-c');
249

250
				const bashCommand = `${quote(args)}; echo; read -p "${LinuxExternalTerminalService.WAIT_MESSAGE}" -n1;`;
251
				termArgs.push(`''${bashCommand}''`);	// wrapping argument in two sets of ' because node is so "friendly" that it removes one set...
252

253
				// merge environment variables into a copy of the process.env
Y
Yuki Ueda 已提交
254
				const env = assign({}, process.env, envVars);
255

256
				// delete environment variables that have a null value
257
				Object.keys(env).filter(v => env[v] === null).forEach(key => delete env[key]);
258

259 260 261 262
				const options: any = {
					cwd: dir,
					env: env
				};
263

264 265
				let stderr = '';
				const cmd = cp.spawn(exec, termArgs, options);
266 267 268
				cmd.on('error', err => {
					reject(improveError(err));
				});
269 270 271 272 273
				cmd.stderr.on('data', (data) => {
					stderr += data.toString();
				});
				cmd.on('exit', (code: number) => {
					if (code === 0) {	// OK
274
						resolve(undefined);
A
Andre Weinand 已提交
275
					} else {
276 277
						if (stderr) {
							const lines = stderr.split('\n', 1);
278
							reject(new Error(lines[0]));
279
						} else {
280
							reject(new Error(nls.localize('linux.term.failed', "'{0}' failed with exit code {1}", exec, code)));
281
						}
A
Andre Weinand 已提交
282
					}
283
				});
284 285 286 287
			});
		});
	}

288
	private spawnTerminal(spawner: typeof cp, configuration: IExternalTerminalConfiguration, cwd?: string): Promise<void> {
D
Daniel Imms 已提交
289
		const terminalConfig = configuration.terminal.external;
290
		const execPromise = terminalConfig.linuxExec ? Promise.resolve(terminalConfig.linuxExec) : LinuxExternalTerminalService.getDefaultTerminalLinuxReady();
291

292
		return new Promise<void>((c, e) => {
293
			execPromise.then(exec => {
D
Daniel Imms 已提交
294
				const env = cwd ? { cwd } : undefined;
D
Daniel Imms 已提交
295
				const child = spawner.spawn(exec, [], env);
296
				child.on('error', e);
M
Matt Bierner 已提交
297
				child.on('exit', () => c());
298
			});
299 300
		});
	}
301 302 303 304 305 306 307

	private static _DEFAULT_TERMINAL_LINUX_READY: Promise<string>;

	public static getDefaultTerminalLinuxReady(): Promise<string> {
		if (!LinuxExternalTerminalService._DEFAULT_TERMINAL_LINUX_READY) {
			LinuxExternalTerminalService._DEFAULT_TERMINAL_LINUX_READY = new Promise<string>(c => {
				if (env.isLinux) {
M
Matt Bierner 已提交
308
					Promise.all([pfs.exists('/etc/debian_version'), Promise.resolve(process.lazyEnv) || Promise.resolve(undefined)]).then(([isDebian]) => {
309 310 311 312 313 314 315 316 317 318 319 320 321 322 323 324 325 326 327 328 329 330
						if (isDebian) {
							c('x-terminal-emulator');
						} else if (process.env.DESKTOP_SESSION === 'gnome' || process.env.DESKTOP_SESSION === 'gnome-classic') {
							c('gnome-terminal');
						} else if (process.env.DESKTOP_SESSION === 'kde-plasma') {
							c('konsole');
						} else if (process.env.COLORTERM) {
							c(process.env.COLORTERM);
						} else if (process.env.TERM) {
							c(process.env.TERM);
						} else {
							c('xterm');
						}
					});
					return;
				}

				c('xterm');
			});
		}
		return LinuxExternalTerminalService._DEFAULT_TERMINAL_LINUX_READY;
	}
E
Erich Gamma 已提交
331
}
332

333 334 335 336
/**
 * tries to turn OS errors into more meaningful error messages
 */
function improveError(err: Error): Error {
337
	if ('errno' in err && err['errno'] === 'ENOENT' && 'path' in err && typeof err['path'] === 'string') {
338 339 340 341 342
		return new Error(nls.localize('ext.term.app.not.found', "can't find terminal application '{0}'", err['path']));
	}
	return err;
}

343 344 345 346 347 348 349 350 351 352 353 354 355 356
/**
 * Quote args if necessary and combine into a space separated string.
 */
function quote(args: string[]): string {
	let r = '';
	for (let a of args) {
		if (a.indexOf(' ') >= 0) {
			r += '"' + a + '"';
		} else {
			r += a;
		}
		r += ' ';
	}
	return r;
D
Daniel Imms 已提交
357
}
358 359 360 361 362 363 364 365 366

if (env.isWindows) {
	registerSingleton(IExternalTerminalService, WindowsExternalTerminalService, true);
} else if (env.isMacintosh) {
	registerSingleton(IExternalTerminalService, MacExternalTerminalService, true);
} else if (env.isLinux) {
	registerSingleton(IExternalTerminalService, LinuxExternalTerminalService, true);
}

367
LinuxExternalTerminalService.getDefaultTerminalLinuxReady().then(defaultTerminalLinux => {
368 369 370 371 372 373 374 375 376 377 378 379 380 381 382 383 384 385 386 387 388 389 390
	let configurationRegistry = Registry.as<IConfigurationRegistry>(Extensions.Configuration);
	configurationRegistry.registerConfiguration({
		id: 'externalTerminal',
		order: 100,
		title: nls.localize('terminalConfigurationTitle', "External Terminal"),
		type: 'object',
		properties: {
			'terminal.explorerKind': {
				type: 'string',
				enum: [
					'integrated',
					'external'
				],
				enumDescriptions: [
					nls.localize('terminal.explorerKind.integrated', "Use VS Code's integrated terminal."),
					nls.localize('terminal.explorerKind.external', "Use the configured external terminal.")
				],
				description: nls.localize('explorer.openInTerminalKind', "Customizes what kind of terminal to launch."),
				default: 'integrated'
			},
			'terminal.external.windowsExec': {
				type: 'string',
				description: nls.localize('terminal.external.windowsExec', "Customizes which terminal to run on Windows."),
391
				default: WindowsExternalTerminalService.getDefaultTerminalWindows(),
392 393 394 395 396 397 398 399 400 401 402 403 404 405 406 407 408
				scope: ConfigurationScope.APPLICATION
			},
			'terminal.external.osxExec': {
				type: 'string',
				description: nls.localize('terminal.external.osxExec', "Customizes which terminal application to run on macOS."),
				default: DEFAULT_TERMINAL_OSX,
				scope: ConfigurationScope.APPLICATION
			},
			'terminal.external.linuxExec': {
				type: 'string',
				description: nls.localize('terminal.external.linuxExec', "Customizes which terminal to run on Linux."),
				default: defaultTerminalLinux,
				scope: ConfigurationScope.APPLICATION
			}
		}
	});
});