src/vs/platform/terminal/node/terminalProfiles.ts

512 LOC · 197 covered · 315 uncovered · 33 ranges · 3 concepts · 2 introducers · 2 tests

File neighbourhood

The centred file is linked to every concept that introduces one of its ranges, every test that runs code from the file, and the gray connector concepts standing between those tests and the file's own introducer concepts. Undirected links join concepts to every file where they introduce source and concepts to the tests they introduce; arrows show specialization between the displayed concepts and bridge only concepts omitted from this view. Concept colors match the source ranges below; connector concepts have no source color and are shown in gray.

Focused file, its introducer and connector concepts, their introduced files, and tests that run code from the file

In the embedded map, ordinary wheel input scrolls the page; use the visible controls to zoom and drag to pan. Open the full-screen map for canvas navigation: wheel pans, Ctrl/Command plus wheel zooms, and arrow keys pan when this region is focused. On touch screens, open the full-screen map to pan or pinch. If JavaScript or WebGL is unavailable, use the related-file, concept, and source links on this page.

Graph controls are ready.

Interactive rendering requires JavaScript and WebGL. Use the related-file, concept, and source links on this page while the interactive map is unavailable.

1 > /*--------------------------------------------------------------------------------------------- terminalProfiles.ts ×14
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 fs from 'fs';
7 > import * as cp from 'child_process';
8 > import { Codicon } from '../../../base/common/codicons.js';
9 > import { basename, delimiter, normalize, dirname, resolve } from '../../../base/common/path.js';
10 > import { isLinux, isWindows } from '../../../base/common/platform.js';
11 > import { findExecutable } from '../../../base/node/processes.js';
12 > import { hasKey, isObject, isString } from '../../../base/common/types.js';
13 > import { URI } from '../../../base/common/uri.js';
14 > import * as pfs from '../../../base/node/pfs.js';
15 > import { enumeratePowerShellInstallations } from '../../../base/node/powershell.js';
16 > import { IConfigurationService } from '../../configuration/common/configuration.js';
17 > import { ILogService } from '../../log/common/log.js';
18 > import { ITerminalEnvironment, ITerminalExecutable, ITerminalProfile, ITerminalProfileSource, ITerminalUnsafePath, ProfileSource, TerminalIcon, TerminalSettingId } from '../common/terminal.js';
19 > import { ThemeIcon } from '../../../base/common/themables.js';
20 > import { getWindowsBuildNumberAsync } from '../../../base/node/windowsVersion.js';
21 >
22 > const enum Constants {
23 > UnixShellsPath = '/etc/shells'
24 > }
25 >
26 > let profileSources: Map<string, IPotentialTerminalProfile> | undefined;
27 > let logIfWslNotInstalled: boolean = true;
28 >
29 > export function detectAvailableProfiles(
30 > profiles: unknown, terminalProfiles.ts ×19
31 > defaultProfile: unknown,
32 > includeDetectedProfiles: boolean,
33 > configurationService: IConfigurationService,
34 > shellEnv: typeof process.env = process.env,
35 > fsProvider?: IFsProvider,
36 > logService?: ILogService,
37 > variableResolver?: (text: string[]) => Promise<string[]>,
38 > testPwshSourcePaths?: string[]
39 > ): Promise<ITerminalProfile[]> {
40 > fsProvider = fsProvider || {
41 existsFile: pfs.SymlinkSupport.existsFile,
42 readFile: fs.promises.readFile
43 };
44 > if (isWindows) { terminalProfiles.ts ×19
45 return detectAvailableWindowsProfiles(
46 includeDetectedProfiles,
47 fsProvider,
48 shellEnv,
49 logService,
50 configurationService.getValue(TerminalSettingId.UseWslProfiles) !== false,
51 profiles && isObject(profiles) ? { ...profiles } : configurationService.getValue<{ [key: string]: IUnresolvedTerminalProfile }>(TerminalSettingId.ProfilesWindows),
52 isString(defaultProfile) ? defaultProfile : configurationService.getValue<string>(TerminalSettingId.DefaultProfileWindows),
53 testPwshSourcePaths,
54 variableResolver
55 );
56 }
57 > return detectAvailableUnixProfiles( terminalProfiles.ts ×19
58 > fsProvider,
59 > logService,
60 > includeDetectedProfiles,
61 > profiles && isObject(profiles) ? { ...profiles } : configurationService.getValue<{ [key: string]: IUnresolvedTerminalProfile }>(isLinux ? TerminalSettingId.ProfilesLinux : TerminalSettingId.ProfilesMacOs),
62 > isString(defaultProfile) ? defaultProfile : configurationService.getValue<string>(isLinux ? TerminalSettingId.DefaultProfileLinux : TerminalSettingId.DefaultProfileMacOs),
63 > testPwshSourcePaths,
64 > variableResolver,
65 > shellEnv
66 > );
67 > }
69 async function detectAvailableWindowsProfiles(
70 includeDetectedProfiles: boolean,
71 fsProvider: IFsProvider,
72 shellEnv: typeof process.env,
73 logService?: ILogService,
74 useWslProfiles?: boolean,
75 configProfiles?: { [key: string]: IUnresolvedTerminalProfile },
76 defaultProfileName?: string,
77 testPwshSourcePaths?: string[],
78 variableResolver?: (text: string[]) => Promise<string[]>
79 ): Promise<ITerminalProfile[]> {
80 // Determine the correct System32 path. We want to point to Sysnative
81 // when the 32-bit version of VS Code is running on a 64-bit machine.
82 // The reason for this is because PowerShell's important PSReadline
83 // module doesn't work if this is not the case. See #27915.
84 const is32ProcessOn64Windows = process.env.hasOwnProperty('PROCESSOR_ARCHITEW6432');
85 const system32Path = `${process.env['windir']}\\${is32ProcessOn64Windows ? 'Sysnative' : 'System32'}`;
86
87 // WSL 2 released in the May 2020 Update, this is where the `-d` flag was added that we depend
88 // upon
89 const allowWslDiscovery = await getWindowsBuildNumberAsync() >= 19041;
90
91 await initializeWindowsProfiles(testPwshSourcePaths);
92
93 const detectedProfiles: Map<string, IUnresolvedTerminalProfile> = new Map();
94
95 // Add auto detected profiles
96 if (includeDetectedProfiles) {
97 detectedProfiles.set('PowerShell', {
98 source: ProfileSource.Pwsh,
99 icon: Codicon.terminalPowershell,
100 isAutoDetected: true
101 });
102 detectedProfiles.set('Windows PowerShell', {
103 path: `${system32Path}\\WindowsPowerShell\\v1.0\\powershell.exe`,
104 icon: Codicon.terminalPowershell,
105 isAutoDetected: true
106 });
107 detectedProfiles.set('Git Bash', {
108 source: ProfileSource.GitBash,
109 icon: Codicon.terminalGitBash,
110 isAutoDetected: true
111 });
112 detectedProfiles.set('Command Prompt', {
113 path: `${system32Path}\\cmd.exe`,
114 icon: Codicon.terminalCmd,
115 isAutoDetected: true
116 });
117 detectedProfiles.set('Cygwin', {
118 path: [
119 { path: `${process.env['HOMEDRIVE']}\\cygwin64\\bin\\bash.exe`, isUnsafe: true },
120 { path: `${process.env['HOMEDRIVE']}\\cygwin\\bin\\bash.exe`, isUnsafe: true }
121 ],
122 args: ['--login'],
123 isAutoDetected: true
124 });
125 detectedProfiles.set('bash (MSYS2)', {
126 path: [
127 { path: `${process.env['HOMEDRIVE']}\\msys64\\usr\\bin\\bash.exe`, isUnsafe: true },
128 ],
129 args: ['--login', '-i'],
130 // CHERE_INVOKING retains current working directory
131 env: { CHERE_INVOKING: '1' },
132 icon: Codicon.terminalBash,
133 isAutoDetected: true
134 });
135 const cmderPath = `${process.env['CMDER_ROOT'] || `${process.env['HOMEDRIVE']}\\cmder`}\\vendor\\bin\\vscode_init.cmd`;
136 detectedProfiles.set('Cmder', {
137 path: `${system32Path}\\cmd.exe`,
138 args: ['/K', cmderPath],
139 // The path is safe if it was derived from CMDER_ROOT
140 requiresPath: process.env['CMDER_ROOT'] ? cmderPath : { path: cmderPath, isUnsafe: true },
141 isAutoDetected: true
142 });
143 }
144
145 applyConfigProfilesToMap(configProfiles, detectedProfiles);
146
147 const resultProfiles: ITerminalProfile[] = await transformToTerminalProfiles(detectedProfiles.entries(), defaultProfileName, fsProvider, shellEnv, logService, variableResolver);
148
149 if (includeDetectedProfiles && useWslProfiles && allowWslDiscovery) {
150 try {
151 const result = await getWslProfiles(`${system32Path}\\wsl.exe`, defaultProfileName);
152 for (const wslProfile of result) {
153 if (!configProfiles || !Object.prototype.hasOwnProperty.call(configProfiles, wslProfile.profileName)) {
154 resultProfiles.push(wslProfile);
155 }
156 }
157 } catch (e) {
158 if (logIfWslNotInstalled) {
159 logService?.trace('WSL is not installed, so could not detect WSL profiles');
160 logIfWslNotInstalled = false;
161 }
162 }
163 }
164
165 return resultProfiles;
166 }
168 > async function transformToTerminalProfiles( terminalProfiles.ts ×19
169 > entries: IterableIterator<[string, IUnresolvedTerminalProfile]>,
170 > defaultProfileName: string | undefined,
171 > fsProvider: IFsProvider,
172 > shellEnv: typeof process.env = process.env,
173 > logService?: ILogService,
174 > variableResolver?: (text: string[]) => Promise<string[]>,
175 > ): Promise<ITerminalProfile[]> {
176 > const promises: Promise<ITerminalProfile | undefined>[] = [];
177 > for (const [profileName, profile] of entries) {
178 > promises.push(getValidatedProfile(profileName, profile, defaultProfileName, fsProvider, shellEnv, logService, variableResolver));
179 > }
180 > return (await Promise.all(promises)).filter(e => !!e);
181 > }
183 > async function getValidatedProfile( terminalProfiles.ts ×19
184 > profileName: string,
185 > profile: IUnresolvedTerminalProfile,
186 > defaultProfileName: string | undefined,
187 > fsProvider: IFsProvider,
188 > shellEnv: typeof process.env = process.env,
189 > logService?: ILogService,
190 > variableResolver?: (text: string[]) => Promise<string[]>
191 > ): Promise<ITerminalProfile | undefined> {
192 > if (profile === null) {
193 return undefined;
194 }
195 > let originalPaths: (string | ITerminalUnsafePath)[]; terminalProfiles.ts ×19
196 > let args: string[] | string | undefined;
197 > let icon: ThemeIcon | URI | { light: URI; dark: URI } | undefined = undefined;
198 > // use calculated values if path is not specified
199 > if (hasKey(profile, { source: true })) {
200 const source = profileSources?.get(profile.source);
201 if (!source) {
202 return undefined;
203 }
204 originalPaths = source.paths;
205
206 // if there are configured args, override the default ones
207 args = profile.args || source.args;
208 if (profile.icon) {
209 icon = validateIcon(profile.icon);
210 } else if (source.icon) {
211 icon = source.icon;
212 }
213 > } else { terminalProfiles.ts ×19
214 > originalPaths = Array.isArray(profile.path) ? profile.path : [profile.path];
215 > args = isWindows ? profile.args : Array.isArray(profile.args) ? profile.args : undefined;
216 > icon = validateIcon(profile.icon);
217 > }
218 >
219 > let paths: (string | ITerminalUnsafePath)[];
220 > if (variableResolver) {
221 // Convert to string[] for resolve
222 const mapped = originalPaths.map(e => isString(e) ? e : e.path);
223
224 const resolved = await variableResolver(mapped);
225 // Convert resolved back to (T | string)[]
226 paths = new Array(originalPaths.length);
227 for (let i = 0; i < originalPaths.length; i++) {
228 if (isString(originalPaths[i])) {
229 paths[i] = resolved[i];
230 } else {
231 paths[i] = {
232 path: resolved[i],
233 isUnsafe: true
234 };
235 }
236 }
237 > } else { terminalProfiles.ts ×19
238 > paths = originalPaths.slice();
239 > }
240 >
241 > let requiresUnsafePath: string | undefined;
242 > if (profile.requiresPath) {
243 // Validate requiresPath exists
244 let actualRequiredPath: string;
245 if (isString(profile.requiresPath)) {
246 actualRequiredPath = profile.requiresPath;
247 } else {
248 actualRequiredPath = profile.requiresPath.path;
249 if (profile.requiresPath.isUnsafe) {
250 requiresUnsafePath = actualRequiredPath;
251 }
252 }
253 const result = await fsProvider.existsFile(actualRequiredPath);
254 if (!result) {
255 return;
256 }
257 }
259 > const validatedProfile = await validateProfilePaths(profileName, defaultProfileName, paths, fsProvider, shellEnv, args, profile.env, profile.overrideName, profile.isAutoDetected, requiresUnsafePath);
260 > if (!validatedProfile) {
261 > logService?.debug('Terminal profile not validated', profileName, originalPaths);
262 > return undefined;
263 > }
264 >
265 > validatedProfile.isAutoDetected = profile.isAutoDetected;
266 > validatedProfile.icon = icon;
267 > validatedProfile.color = profile.color;
268 > return validatedProfile;
269 > }
271 > function validateIcon(icon: string | TerminalIcon | undefined): TerminalIcon | undefined { terminalProfiles.ts ×19
272 > if (isString(icon)) {
273 return { id: icon };
274 }
275 > return icon; terminalProfiles.ts ×19
276 > }
278 async function initializeWindowsProfiles(testPwshSourcePaths?: string[]): Promise<void> {
279 if (profileSources && !testPwshSourcePaths) {
280 return;
281 }
282
283 const [gitBashPaths, pwshPaths] = await Promise.all([getGitBashPaths(), testPwshSourcePaths || getPowershellPaths()]);
284
285 profileSources = new Map();
286 profileSources.set(
287 ProfileSource.GitBash, {
288 profileName: 'Git Bash',
289 paths: gitBashPaths,
290 args: ['--login', '-i']
291 });
292 profileSources.set(ProfileSource.Pwsh, {
293 profileName: 'PowerShell',
294 paths: pwshPaths,
295 icon: Codicon.terminalPowershell
296 });
297 }
299 async function getGitBashPaths(): Promise<string[]> {
300 const gitDirs: Set<string> = new Set();
301
302 // Look for git.exe on the PATH and use that if found. git.exe is located at
303 // `<installdir>/cmd/git.exe`. This is not an unsafe location because the git executable is
304 // located on the PATH which is only controlled by the user/admin.
305 const gitExePath = await findExecutable('git.exe');
306 if (gitExePath) {
307 const gitExeDir = dirname(gitExePath);
308 gitDirs.add(resolve(gitExeDir, '../..'));
309 }
310 function addTruthy<T>(set: Set<T>, value: T | undefined): void {
311 if (value) {
312 set.add(value);
313 }
314 }
315
316 // Add common git install locations
317 addTruthy(gitDirs, process.env['ProgramW6432']);
318 addTruthy(gitDirs, process.env['ProgramFiles']);
319 addTruthy(gitDirs, process.env['ProgramFiles(X86)']);
320 addTruthy(gitDirs, `${process.env['LocalAppData']}\\Program`);
321
322 const gitBashPaths: string[] = [];
323 for (const gitDir of gitDirs) {
324 gitBashPaths.push(
325 `${gitDir}\\Git\\bin\\bash.exe`,
326 `${gitDir}\\Git\\usr\\bin\\bash.exe`,
327 `${gitDir}\\usr\\bin\\bash.exe` // using Git for Windows SDK
328 );
329 }
330
331 // Add special installs that don't follow the standard directory structure
332 gitBashPaths.push(`${process.env['UserProfile']}\\scoop\\apps\\git\\current\\bin\\bash.exe`);
333 gitBashPaths.push(`${process.env['UserProfile']}\\scoop\\apps\\git-with-openssh\\current\\bin\\bash.exe`);
334
335 return gitBashPaths;
336 }
338 async function getPowershellPaths(): Promise<string[]> {
339 const paths: string[] = [];
340 // Add all of the different kinds of PowerShells
341 for await (const pwshExe of enumeratePowerShellInstallations()) {
342 paths.push(pwshExe.exePath);
343 }
344 return paths;
345 }
347 async function getWslProfiles(wslPath: string, defaultProfileName: string | undefined): Promise<ITerminalProfile[]> {
348 const profiles: ITerminalProfile[] = [];
349 const distroOutput = await new Promise<string>((resolve, reject) => {
350 // wsl.exe output is encoded in utf16le (ie. A -> 0x4100) by default, force it in case the
351 // user changed https://github.com/microsoft/vscode/issues/276253
352 cp.exec('wsl.exe -l -q', { encoding: 'utf16le', env: { ...process.env, WSL_UTF8: '0' }, timeout: 1000 }, (err, stdout) => {
353 if (err) {
354 return reject('Problem occurred when getting wsl distros');
355 }
356 resolve(stdout);
357 });
358 });
359 if (!distroOutput) {
360 return [];
361 }
362 const distroNames = distroOutput.split(/\r?\n/).filter(t => t.trim().length > 0);
363 for (const distroName of distroNames) {
364 // Skip empty lines
365 if (distroName === '') {
366 continue;
367 }
368
369 // docker-desktop and docker-desktop-data are treated as implementation details of
370 // Docker Desktop for Windows and therefore not exposed
371 if (distroName.startsWith('docker-desktop')) {
372 continue;
373 }
374
375 // Create the profile, adding the icon depending on the distro
376 const profileName = `${distroName} (WSL)`;
377 const profile: ITerminalProfile = {
378 profileName,
379 path: wslPath,
380 args: [`-d`, `${distroName}`],
381 isDefault: profileName === defaultProfileName,
382 icon: getWslIcon(distroName),
383 isAutoDetected: false
384 };
385 // Add the profile
386 profiles.push(profile);
387 }
388 return profiles;
389 }
391 function getWslIcon(distroName: string): ThemeIcon {
392 if (distroName.includes('Ubuntu')) {
393 return Codicon.terminalUbuntu;
394 } else if (distroName.includes('Debian')) {
395 return Codicon.terminalDebian;
396 } else {
397 return Codicon.terminalLinux;
398 }
399 }
401 > async function detectAvailableUnixProfiles( terminalProfiles.ts ×19
402 > fsProvider: IFsProvider,
403 > logService?: ILogService,
404 > includeDetectedProfiles?: boolean,
405 > configProfiles?: { [key: string]: IUnresolvedTerminalProfile },
406 > defaultProfileName?: string,
407 > testPaths?: string[],
408 > variableResolver?: (text: string[]) => Promise<string[]>,
409 > shellEnv?: typeof process.env
410 > ): Promise<ITerminalProfile[]> {
411 > const detectedProfiles: Map<string, IUnresolvedTerminalProfile> = new Map();
412 >
413 > // Add non-quick launch profiles
414 > if (includeDetectedProfiles && await fsProvider.existsFile(Constants.UnixShellsPath)) {
415 const contents = (await fsProvider.readFile(Constants.UnixShellsPath)).toString();
416 const profiles = (
417 (testPaths || contents.split('\n'))
418 .map(e => {
419 const index = e.indexOf('#');
420 return index === -1 ? e : e.substring(0, index);
421 })
422 .filter(e => e.trim().length > 0)
423 );
424 const counts: Map<string, number> = new Map();
425 for (const profile of profiles) {
426 let profileName = basename(profile);
427 let count = counts.get(profileName) || 0;
428 count++;
429 if (count > 1) {
430 profileName = `${profileName} (${count})`;
431 }
432 counts.set(profileName, count);
433 detectedProfiles.set(profileName, { path: profile, isAutoDetected: true });
434 }
435 }
437 > applyConfigProfilesToMap(configProfiles, detectedProfiles);
438 >
439 > return await transformToTerminalProfiles(detectedProfiles.entries(), defaultProfileName, fsProvider, shellEnv, logService, variableResolver);
440 > }
442 > function applyConfigProfilesToMap(configProfiles: { [key: string]: IUnresolvedTerminalProfile } | undefined, profilesMap: Map<string, IUnresolvedTerminalProfile>) { terminalProfiles.ts ×19
443 > if (!configProfiles) {
444 return;
445 }
446 > for (const [profileName, value] of Object.entries(configProfiles)) { terminalProfiles.ts ×19
447 > if (value === null || !isObject(value) || (!hasKey(value, { path: true }) && !hasKey(value, { source: true }))) {
448 profilesMap.delete(profileName);
449 > } else { terminalProfiles.ts ×19
450 > value.icon = value.icon || profilesMap.get(profileName)?.icon;
451 > profilesMap.set(profileName, value);
452 > }
453 > }
454 > }
456 > async function validateProfilePaths(profileName: string, defaultProfileName: string | undefined, potentialPaths: (string | ITerminalUnsafePath)[], fsProvider: IFsProvider, shellEnv: typeof process.env, args?: string[] | string, env?: ITerminalEnvironment, overrideName?: boolean, isAutoDetected?: boolean, requiresUnsafePath?: string): Promise<ITerminalProfile | undefined> { terminalProfiles.ts ×19
457 > if (potentialPaths.length === 0) {
458 > return Promise.resolve(undefined);
459 > }
460 > const path = potentialPaths.shift()!;
461 > if (path === '') {
462 return validateProfilePaths(profileName, defaultProfileName, potentialPaths, fsProvider, shellEnv, args, env, overrideName, isAutoDetected);
463 }
464 > const isUnsafePath = !isString(path) && path.isUnsafe; terminalProfiles.ts ×19
465 > const actualPath = isString(path) ? path : path.path;
466 >
467 > const profile: ITerminalProfile = {
468 > profileName,
469 > path: actualPath,
470 > args,
471 > env,
472 > overrideName,
473 > isAutoDetected,
474 > isDefault: profileName === defaultProfileName,
475 > isUnsafePath,
476 > requiresUnsafePath
477 > };
478 >
479 > // For non-absolute paths, check if it's available on $PATH
480 > if (basename(actualPath) === actualPath) {
481 // The executable isn't an absolute path, try find it on the PATH
482 const envPaths: string[] | undefined = shellEnv.PATH ? shellEnv.PATH.split(delimiter) : undefined;
483 const executable = await findExecutable(actualPath, undefined, envPaths, undefined, fsProvider.existsFile);
484 if (!executable) {
485 return validateProfilePaths(profileName, defaultProfileName, potentialPaths, fsProvider, shellEnv, args);
486 }
487 profile.path = executable;
488 profile.isFromPath = true;
489 return profile;
490 }
492 > const result = await fsProvider.existsFile(normalize(actualPath));
493 > if (result) {
494 > return profile;
495 > }
496 >
497 > return validateProfilePaths(profileName, defaultProfileName, potentialPaths, fsProvider, shellEnv, args, env, overrideName, isAutoDetected);
498 > }
500 > export interface IFsProvider {
501 > existsFile(path: string): Promise<boolean>;
502 > readFile(path: string): Promise<Buffer>;
503 > }
504 >
505 > interface IPotentialTerminalProfile {
506 > profileName: string;
507 > paths: string[];
508 > args?: string[];
509 > icon?: ThemeIcon | URI | { light: URI; dark: URI };
510 > }
511 >
512 > export type IUnresolvedTerminalProfile = ITerminalExecutable | ITerminalProfileSource | null;