terminalEnvironment.ts 13.1 KB
Newer Older
1 2 3 4 5
/*---------------------------------------------------------------------------------------------
 *  Copyright (c) Microsoft Corporation. All rights reserved.
 *  Licensed under the MIT License. See License.txt in the project root for license information.
 *--------------------------------------------------------------------------------------------*/

6
import * as path from 'vs/base/common/path';
7
import * as platform from 'vs/base/common/platform';
8
import { URI as Uri } from 'vs/base/common/uri';
9
import { IWorkspaceFolder } from 'vs/platform/workspace/common/workspace';
10
import { IShellLaunchConfig, ITerminalEnvironment } from 'vs/workbench/contrib/terminal/common/terminal';
11
import { IConfigurationResolverService } from 'vs/workbench/services/configurationResolver/common/configurationResolver';
12
import { sanitizeProcessEnvironment } from 'vs/base/common/processes';
D
Daniel Imms 已提交
13
import { ILogService } from 'vs/platform/log/common/log';
14 15 16 17 18

/**
 * This module contains utility functions related to the environment, cwd and paths.
 */

D
Daniel Imms 已提交
19
export function mergeEnvironments(parent: platform.IProcessEnvironment, other: ITerminalEnvironment | undefined): void {
20 21 22 23 24 25 26
	if (!other) {
		return;
	}

	// On Windows apply the new values ignoring case, while still retaining
	// the case of the original key.
	if (platform.isWindows) {
D
Daniel Imms 已提交
27
		for (const configKey in other) {
28
			let actualKey = configKey;
D
Daniel Imms 已提交
29
			for (const envKey in parent) {
30 31 32 33 34 35 36 37 38 39 40 41 42 43 44 45
				if (configKey.toLowerCase() === envKey.toLowerCase()) {
					actualKey = envKey;
					break;
				}
			}
			const value = other[configKey];
			_mergeEnvironmentValue(parent, actualKey, value);
		}
	} else {
		Object.keys(other).forEach((key) => {
			const value = other[key];
			_mergeEnvironmentValue(parent, key, value);
		});
	}
}

46
function _mergeEnvironmentValue(env: ITerminalEnvironment, key: string, value: string | null): void {
47 48 49 50 51 52 53
	if (typeof value === 'string') {
		env[key] = value;
	} else {
		delete env[key];
	}
}

54
export function addTerminalEnvironmentKeys(env: platform.IProcessEnvironment, version: string | undefined, locale: string | undefined, detectLocale: 'auto' | 'off' | 'on'): void {
55
	env['TERM_PROGRAM'] = 'vscode';
D
Daniel Imms 已提交
56 57 58
	if (version) {
		env['TERM_PROGRAM_VERSION'] = version;
	}
59 60
	if (shouldSetLangEnvVariable(env, detectLocale)) {
		env['LANG'] = getLangEnvVariable(locale);
D
Daniel Imms 已提交
61
	}
62
	env['COLORTERM'] = 'truecolor';
63 64
}

B
Benjamin Pasero 已提交
65
function mergeNonNullKeys(env: platform.IProcessEnvironment, other: ITerminalEnvironment | undefined) {
D
Daniel Imms 已提交
66 67 68 69 70 71 72 73 74 75 76
	if (!other) {
		return;
	}
	for (const key of Object.keys(other)) {
		const value = other[key];
		if (value) {
			env[key] = value;
		}
	}
}

77
function resolveConfigurationVariables(configurationResolverService: IConfigurationResolverService, env: ITerminalEnvironment, lastActiveWorkspaceRoot: IWorkspaceFolder | null): ITerminalEnvironment {
78
	Object.keys(env).forEach((key) => {
79 80
		const value = env[key];
		if (typeof value === 'string' && lastActiveWorkspaceRoot !== null) {
81 82 83 84 85
			try {
				env[key] = configurationResolverService.resolve(lastActiveWorkspaceRoot, value);
			} catch (e) {
				env[key] = value;
			}
86 87 88 89 90
		}
	});
	return env;
}

91
export function shouldSetLangEnvVariable(env: platform.IProcessEnvironment, detectLocale: 'auto' | 'off' | 'on'): boolean {
92 93 94 95 96 97 98 99 100
	if (detectLocale === 'on') {
		return true;
	}
	if (detectLocale === 'auto') {
		return !env['LANG'] || env['LANG'].search(/\.UTF\-8$/) === -1;
	}
	return false; // 'off'
}

101
export function getLangEnvVariable(locale?: string): string {
102 103 104
	const parts = locale ? locale.split('-') : [];
	const n = parts.length;
	if (n === 0) {
D
Daniel Imms 已提交
105
		// Fallback to en_US if the locale is unknown
106 107 108
		return 'en_US.UTF-8';
	}
	if (n === 1) {
109 110 111 112 113
		// The local may only contain the language, not the variant, if this is the case guess the
		// variant such that it can be used as a valid $LANG variable. The language variant chosen
		// is the original and/or most prominent with help from
		// https://stackoverflow.com/a/2502675/1156119
		// The list of locales was generated by running `locale -a` on macOS
114
		const languageVariants: { [key: string]: string } = {
115 116 117 118 119
			af: 'ZA',
			am: 'ET',
			be: 'BY',
			bg: 'BG',
			ca: 'ES',
120
			cs: 'CZ',
121 122 123
			da: 'DK',
			// de: 'AT',
			// de: 'CH',
124
			de: 'DE',
125 126 127 128 129 130
			el: 'GR',
			// en: 'AU',
			// en: 'CA',
			// en: 'GB',
			// en: 'IE',
			// en: 'NZ',
131 132
			en: 'US',
			es: 'ES',
133 134
			et: 'EE',
			eu: 'ES',
135
			fi: 'FI',
136 137 138
			// fr: 'BE',
			// fr: 'CA',
			// fr: 'CH',
139
			fr: 'FR',
140 141
			he: 'IL',
			hr: 'HR',
D
Daniel Imms 已提交
142
			hu: 'HU',
143 144 145
			hy: 'AM',
			is: 'IS',
			// it: 'CH',
146 147
			it: 'IT',
			ja: 'JP',
148
			kk: 'KZ',
149
			ko: 'KR',
150 151 152 153
			lt: 'LT',
			// nl: 'BE',
			nl: 'NL',
			no: 'NO',
154
			pl: 'PL',
155 156 157
			pt: 'BR',
			// pt: 'PT',
			ro: 'RO',
158
			ru: 'RU',
T
Tomas Kovar 已提交
159
			sk: 'SK',
160 161 162 163 164 165
			sl: 'SI',
			sr: 'YU',
			sv: 'SE',
			tr: 'TR',
			uk: 'UA',
			zh: 'CN',
166 167 168 169 170
		};
		if (parts[0] in languageVariants) {
			parts.push(languageVariants[parts[0]]);
		}
	} else {
D
Daniel Imms 已提交
171
		// Ensure the variant is uppercase to be a valid $LANG
172 173 174 175 176
		parts[1] = parts[1].toUpperCase();
	}
	return parts.join('_') + '.UTF-8';
}

D
Daniel Imms 已提交
177 178 179 180 181 182 183
export function getCwd(
	shell: IShellLaunchConfig,
	userHome: string,
	lastActiveWorkspace: IWorkspaceFolder | undefined,
	configurationResolverService: IConfigurationResolverService | undefined,
	root: Uri | undefined,
	customCwd: string | undefined,
184
	logService?: ILogService
D
Daniel Imms 已提交
185
): string {
186
	if (shell.cwd) {
187 188 189
		const unresolved = (typeof shell.cwd === 'object') ? shell.cwd.fsPath : shell.cwd;
		const resolved = _resolveCwd(unresolved, lastActiveWorkspace, configurationResolverService);
		return resolved || unresolved;
190 191
	}

192
	let cwd: string | undefined;
193

194
	if (!shell.ignoreConfigurationCwd && customCwd) {
195
		if (configurationResolverService) {
196
			customCwd = _resolveCwd(customCwd, lastActiveWorkspace, configurationResolverService, logService);
197
		}
198 199 200 201 202 203
		if (customCwd) {
			if (path.isAbsolute(customCwd)) {
				cwd = customCwd;
			} else if (root) {
				cwd = path.join(root.fsPath, customCwd);
			}
204 205 206 207 208
		}
	}

	// If there was no custom cwd or it was relative with no workspace
	if (!cwd) {
209
		cwd = root ? root.fsPath : userHome;
210 211 212 213 214
	}

	return _sanitizeCwd(cwd);
}

215 216 217 218 219 220 221 222 223 224 225 226
function _resolveCwd(cwd: string, lastActiveWorkspace: IWorkspaceFolder | undefined, configurationResolverService: IConfigurationResolverService | undefined, logService?: ILogService): string | undefined {
	if (configurationResolverService) {
		try {
			return configurationResolverService.resolve(lastActiveWorkspace, cwd);
		} catch (e) {
			logService?.error('Could not resolve terminal cwd', e);
			return undefined;
		}
	}
	return cwd;
}

227 228 229 230 231 232 233
function _sanitizeCwd(cwd: string): string {
	// Make the drive letter uppercase on Windows (see #9448)
	if (platform.platform === platform.Platform.Windows && cwd && cwd[1] === ':') {
		return cwd[0].toUpperCase() + cwd.substr(1);
	}
	return cwd;
}
234

235 236 237 238
export function escapeNonWindowsPath(path: string): string {
	let newPath = path;
	if (newPath.indexOf('\\') !== 0) {
		newPath = newPath.replace(/\\/g, '\\\\');
239
	}
240 241 242 243
	if (!newPath && (newPath.indexOf('"') !== -1)) {
		newPath = '\'' + newPath + '\'';
	} else if (newPath.indexOf(' ') !== -1) {
		newPath = newPath.replace(/ /g, '\\ ');
244
	}
245
	return newPath;
246
}
D
Daniel Imms 已提交
247

D
Daniel Imms 已提交
248
export function getDefaultShell(
S
rename  
Sandeep Somavarapu 已提交
249
	fetchSetting: (key: string) => { userValue?: string | string[], value?: string | string[], defaultValue?: string | string[] },
D
Daniel Imms 已提交
250
	isWorkspaceShellAllowed: boolean,
251
	defaultShell: string,
252 253
	isWoW64: boolean,
	windir: string | undefined,
254 255
	lastActiveWorkspace: IWorkspaceFolder | undefined,
	configurationResolverService: IConfigurationResolverService | undefined,
256
	logService: ILogService,
257
	useAutomationShell: boolean,
258
	platformOverride: platform.Platform = platform.platform
D
Daniel Imms 已提交
259
): string {
D
Daniel Imms 已提交
260 261 262 263 264 265 266 267
	let maybeExecutable: string | null = null;
	if (useAutomationShell) {
		// If automationShell is specified, this should override the normal setting
		maybeExecutable = getShellSetting(fetchSetting, isWorkspaceShellAllowed, 'automationShell', platformOverride);
	}
	if (!maybeExecutable) {
		maybeExecutable = getShellSetting(fetchSetting, isWorkspaceShellAllowed, 'shell', platformOverride);
	}
D
Daniel Imms 已提交
268
	let executable: string = maybeExecutable || defaultShell;
D
Daniel Imms 已提交
269 270 271 272

	// Change Sysnative to System32 if the OS is Windows but NOT WoW64. It's
	// safe to assume that this was used by accident as Sysnative does not
	// exist and will break the terminal in non-WoW64 environments.
273
	if ((platformOverride === platform.Platform.Windows) && !isWoW64 && windir) {
D
Daniel Imms 已提交
274
		const sysnativePath = path.join(windir, 'Sysnative').replace(/\//g, '\\').toLowerCase();
D
Daniel Imms 已提交
275 276
		if (executable && executable.toLowerCase().indexOf(sysnativePath) === 0) {
			executable = path.join(windir, 'System32', executable.substr(sysnativePath.length + 1));
D
Daniel Imms 已提交
277 278 279 280
		}
	}

	// Convert / to \ on Windows for convenience
D
Daniel Imms 已提交
281 282
	if (executable && platformOverride === platform.Platform.Windows) {
		executable = executable.replace(/\//g, '\\');
D
Daniel Imms 已提交
283
	}
D
Daniel Imms 已提交
284

285
	if (configurationResolverService) {
286
		try {
D
Daniel Imms 已提交
287
			executable = configurationResolverService.resolve(lastActiveWorkspace, executable);
288
		} catch (e) {
289
			logService.error(`Could not resolve shell`, e);
290
		}
291 292
	}

D
Daniel Imms 已提交
293
	return executable;
D
Daniel Imms 已提交
294 295
}

D
Daniel Imms 已提交
296
export function getDefaultShellArgs(
S
rename  
Sandeep Somavarapu 已提交
297
	fetchSetting: (key: string) => { userValue?: string | string[], value?: string | string[], defaultValue?: string | string[] },
D
Daniel Imms 已提交
298
	isWorkspaceShellAllowed: boolean,
299
	useAutomationShell: boolean,
300 301
	lastActiveWorkspace: IWorkspaceFolder | undefined,
	configurationResolverService: IConfigurationResolverService | undefined,
302
	logService: ILogService,
303 304
	platformOverride: platform.Platform = platform.platform,
): string | string[] {
305
	if (useAutomationShell) {
D
Daniel Imms 已提交
306
		if (!!getShellSetting(fetchSetting, isWorkspaceShellAllowed, 'automationShell', platformOverride)) {
307 308 309 310
			return [];
		}
	}

D
Daniel Imms 已提交
311 312
	const platformKey = platformOverride === platform.Platform.Windows ? 'windows' : platformOverride === platform.Platform.Mac ? 'osx' : 'linux';
	const shellArgsConfigValue = fetchSetting(`terminal.integrated.shellArgs.${platformKey}`);
S
rename  
Sandeep Somavarapu 已提交
313
	let args = <string[] | string>((isWorkspaceShellAllowed ? shellArgsConfigValue.value : shellArgsConfigValue.userValue) || shellArgsConfigValue.defaultValue);
314 315 316 317 318 319
	if (typeof args === 'string' && platformOverride === platform.Platform.Windows) {
		return configurationResolverService ? configurationResolverService.resolve(lastActiveWorkspace, args) : args;
	}
	if (configurationResolverService) {
		const resolvedArgs: string[] = [];
		for (const arg of args) {
320 321 322 323 324 325
			try {
				resolvedArgs.push(configurationResolverService.resolve(lastActiveWorkspace, arg));
			} catch (e) {
				logService.error(`Could not resolve terminal.integrated.shellArgs.${platformKey}`, e);
				resolvedArgs.push(arg);
			}
326 327 328
		}
		args = resolvedArgs;
	}
D
Daniel Imms 已提交
329 330 331
	return args;
}

D
Daniel Imms 已提交
332
function getShellSetting(
333
	fetchSetting: (key: string) => { user?: string | string[], value?: string | string[], default?: string | string[] },
334
	isWorkspaceShellAllowed: boolean,
D
Daniel Imms 已提交
335
	type: 'automationShell' | 'shell',
336 337 338
	platformOverride: platform.Platform = platform.platform,
): string | null {
	const platformKey = platformOverride === platform.Platform.Windows ? 'windows' : platformOverride === platform.Platform.Mac ? 'osx' : 'linux';
D
Daniel Imms 已提交
339
	const shellConfigValue = fetchSetting(`terminal.integrated.${type}.${platformKey}`);
340 341 342 343
	const executable = (isWorkspaceShellAllowed ? <string>shellConfigValue.value : <string>shellConfigValue.user) || (<string | null>shellConfigValue.default);
	return executable;
}

344 345 346
export function createTerminalEnvironment(
	shellLaunchConfig: IShellLaunchConfig,
	lastActiveWorkspace: IWorkspaceFolder | null,
S
rename  
Sandeep Somavarapu 已提交
347
	envFromConfig: { userValue?: ITerminalEnvironment, value?: ITerminalEnvironment, defaultValue?: ITerminalEnvironment },
348 349 350
	configurationResolverService: IConfigurationResolverService | undefined,
	isWorkspaceShellAllowed: boolean,
	version: string | undefined,
351
	detectLocale: 'auto' | 'off' | 'on',
D
Daniel Imms 已提交
352
	baseEnv: platform.IProcessEnvironment
353 354 355 356 357 358 359 360
): platform.IProcessEnvironment {
	// Create a terminal environment based on settings, launch config and permissions
	let env: platform.IProcessEnvironment = {};
	if (shellLaunchConfig.strictEnv) {
		// strictEnv is true, only use the requested env (ignoring null entries)
		mergeNonNullKeys(env, shellLaunchConfig.env);
	} else {
		// Merge process env with the env from config and from shellLaunchConfig
D
Daniel Imms 已提交
361
		mergeNonNullKeys(env, baseEnv);
362 363 364

		// const platformKey = platform.isWindows ? 'windows' : (platform.isMacintosh ? 'osx' : 'linux');
		// const envFromConfigValue = this._workspaceConfigurationService.inspect<ITerminalEnvironment | undefined>(`terminal.integrated.env.${platformKey}`);
S
rename  
Sandeep Somavarapu 已提交
365
		const allowedEnvFromConfig = { ...(isWorkspaceShellAllowed ? envFromConfig.value : envFromConfig.userValue) };
366 367 368 369 370 371 372 373 374 375 376 377 378 379 380

		// Resolve env vars from config and shell
		if (configurationResolverService) {
			if (allowedEnvFromConfig) {
				resolveConfigurationVariables(configurationResolverService, allowedEnvFromConfig, lastActiveWorkspace);
			}
			if (shellLaunchConfig.env) {
				resolveConfigurationVariables(configurationResolverService, shellLaunchConfig.env, lastActiveWorkspace);
			}
		}

		// Sanitize the environment, removing any undesirable VS Code and Electron environment
		// variables
		sanitizeProcessEnvironment(env, 'VSCODE_IPC_HOOK_CLI');

381 382 383 384
		// Merge config (settings) and ShellLaunchConfig environments
		mergeEnvironments(env, allowedEnvFromConfig);
		mergeEnvironments(env, shellLaunchConfig.env);

385
		// Adding other env keys necessary to create the process
386
		addTerminalEnvironmentKeys(env, version, platform.locale, detectLocale);
387 388
	}
	return env;
389
}