@sussudio/platform
Version:
Internal APIs for VS Code's service injection the base services.
286 lines (285 loc) • 11.4 kB
JavaScript
/*---------------------------------------------------------------------------------------------
* Copyright (c) Microsoft Corporation. All rights reserved.
* Licensed under the MIT License. See License.txt in the project root for license information.
*--------------------------------------------------------------------------------------------*/
import * as os from 'os';
import { FileAccess } from '@sussudio/base/common/network.mjs';
import { getCaseInsensitive } from '@sussudio/base/common/objects.mjs';
import * as path from '@sussudio/base/common/path.mjs';
import { isWindows } from '@sussudio/base/common/platform.mjs';
import * as process from '@sussudio/base/common/process.mjs';
import { format } from '@sussudio/base/common/strings.mjs';
import { isString } from '@sussudio/base/common/types.mjs';
import * as pfs from '@sussudio/base/node/pfs.mjs';
export function getWindowsBuildNumber() {
const osVersion = /(\d+)\.(\d+)\.(\d+)/g.exec(os.release());
let buildNumber = 0;
if (osVersion && osVersion.length === 4) {
buildNumber = parseInt(osVersion[3]);
}
return buildNumber;
}
export async function findExecutable(command, cwd, paths, env = process.env, exists = pfs.Promises.exists) {
// If we have an absolute path then we take it.
if (path.isAbsolute(command)) {
return (await exists(command)) ? command : undefined;
}
if (cwd === undefined) {
cwd = process.cwd();
}
const dir = path.dirname(command);
if (dir !== '.') {
// We have a directory and the directory is relative (see above). Make the path absolute
// to the current working directory.
const fullPath = path.join(cwd, command);
return (await exists(fullPath)) ? fullPath : undefined;
}
const envPath = getCaseInsensitive(env, 'PATH');
if (paths === undefined && isString(envPath)) {
paths = envPath.split(path.delimiter);
}
// No PATH environment. Make path absolute to the cwd.
if (paths === undefined || paths.length === 0) {
const fullPath = path.join(cwd, command);
return (await exists(fullPath)) ? fullPath : undefined;
}
// We have a simple file name. We get the path variable from the env
// and try to find the executable on the path.
for (const pathEntry of paths) {
// The path entry is absolute.
let fullPath;
if (path.isAbsolute(pathEntry)) {
fullPath = path.join(pathEntry, command);
} else {
fullPath = path.join(cwd, pathEntry, command);
}
if (await exists(fullPath)) {
return fullPath;
}
if (isWindows) {
let withExtension = fullPath + '.com';
if (await exists(withExtension)) {
return withExtension;
}
withExtension = fullPath + '.exe';
if (await exists(withExtension)) {
return withExtension;
}
}
}
const fullPath = path.join(cwd, command);
return (await exists(fullPath)) ? fullPath : undefined;
}
/**
* For a given shell launch config, returns arguments to replace and an optional environment to
* mixin to the SLC's environment to enable shell integration. This must be run within the context
* that creates the process to ensure accuracy. Returns undefined if shell integration cannot be
* enabled.
*/
export function getShellIntegrationInjection(shellLaunchConfig, options, env, logService, productService) {
// Shell integration arg injection is disabled when:
// - The global setting is disabled
// - There is no executable (not sure what script to run)
// - The terminal is used by a feature like tasks or debugging
if (
!options.shellIntegration.enabled ||
!shellLaunchConfig.executable ||
shellLaunchConfig.isFeatureTerminal ||
shellLaunchConfig.hideFromUser ||
shellLaunchConfig.ignoreShellIntegration ||
(isWindows && !options.windowsEnableConpty)
) {
return undefined;
}
const originalArgs = shellLaunchConfig.args;
const shell =
process.platform === 'win32'
? path.basename(shellLaunchConfig.executable).toLowerCase()
: path.basename(shellLaunchConfig.executable);
const appRoot = path.dirname(FileAccess.asFileUri('').fsPath);
let newArgs;
const envMixin = {
VSCODE_INJECTION: '1',
};
// Windows
if (isWindows) {
if (shell === 'pwsh.exe' || shell === 'powershell.exe') {
if (!originalArgs || arePwshImpliedArgs(originalArgs)) {
newArgs = shellIntegrationArgs.get(ShellIntegrationExecutable.WindowsPwsh);
} else if (arePwshLoginArgs(originalArgs)) {
newArgs = shellIntegrationArgs.get(ShellIntegrationExecutable.WindowsPwshLogin);
}
if (!newArgs) {
return undefined;
}
newArgs = [...newArgs]; // Shallow clone the array to avoid setting the default array
newArgs[newArgs.length - 1] = format(newArgs[newArgs.length - 1], appRoot, '');
return { newArgs, envMixin };
}
logService.warn(
`Shell integration cannot be enabled for executable "${shellLaunchConfig.executable}" and args`,
shellLaunchConfig.args,
);
return undefined;
}
// Linux & macOS
switch (shell) {
case 'bash': {
if (!originalArgs || originalArgs.length === 0) {
newArgs = shellIntegrationArgs.get(ShellIntegrationExecutable.Bash);
} else if (areZshBashLoginArgs(originalArgs)) {
envMixin['VSCODE_SHELL_LOGIN'] = '1';
newArgs = shellIntegrationArgs.get(ShellIntegrationExecutable.Bash);
}
if (!newArgs) {
return undefined;
}
newArgs = [...newArgs]; // Shallow clone the array to avoid setting the default array
newArgs[newArgs.length - 1] = format(newArgs[newArgs.length - 1], appRoot);
return { newArgs, envMixin };
}
case 'fish': {
// The injection mechanism used for fish is to add a custom dir to $XDG_DATA_DIRS which
// is similar to $ZDOTDIR in zsh but contains a list of directories to run from.
const oldDataDirs = env?.XDG_DATA_DIRS ?? '/usr/local/share:/usr/share';
const newDataDir = path.join(appRoot, 'out/vs/workbench/contrib/xdg_data');
envMixin['XDG_DATA_DIRS'] = `${oldDataDirs}:${newDataDir}`;
return { newArgs: undefined, envMixin };
}
case 'pwsh': {
if (!originalArgs || arePwshImpliedArgs(originalArgs)) {
newArgs = shellIntegrationArgs.get(ShellIntegrationExecutable.Pwsh);
} else if (arePwshLoginArgs(originalArgs)) {
newArgs = shellIntegrationArgs.get(ShellIntegrationExecutable.PwshLogin);
}
if (!newArgs) {
return undefined;
}
newArgs = [...newArgs]; // Shallow clone the array to avoid setting the default array
newArgs[newArgs.length - 1] = format(newArgs[newArgs.length - 1], appRoot, '');
return { newArgs, envMixin };
}
case 'zsh': {
if (!originalArgs || originalArgs.length === 0) {
newArgs = shellIntegrationArgs.get(ShellIntegrationExecutable.Zsh);
} else if (areZshBashLoginArgs(originalArgs)) {
newArgs = shellIntegrationArgs.get(ShellIntegrationExecutable.ZshLogin);
} else if (
originalArgs === shellIntegrationArgs.get(ShellIntegrationExecutable.Zsh) ||
originalArgs === shellIntegrationArgs.get(ShellIntegrationExecutable.ZshLogin)
) {
newArgs = originalArgs;
}
if (!newArgs) {
return undefined;
}
newArgs = [...newArgs]; // Shallow clone the array to avoid setting the default array
newArgs[newArgs.length - 1] = format(newArgs[newArgs.length - 1], appRoot);
// Move .zshrc into $ZDOTDIR as the way to activate the script
let username;
try {
username = os.userInfo().username;
} catch {
username = 'unknown';
}
const zdotdir = path.join(os.tmpdir(), `${username}-${productService.applicationName}-zsh`);
envMixin['ZDOTDIR'] = zdotdir;
const userZdotdir = env?.ZDOTDIR ?? os.homedir() ?? `~`;
envMixin['USER_ZDOTDIR'] = userZdotdir;
const filesToCopy = [];
filesToCopy.push({
source: path.join(appRoot, 'out/vs/workbench/contrib/terminal/browser/media/shellIntegration-rc.zsh'),
dest: path.join(zdotdir, '.zshrc'),
});
filesToCopy.push({
source: path.join(appRoot, 'out/vs/workbench/contrib/terminal/browser/media/shellIntegration-profile.zsh'),
dest: path.join(zdotdir, '.zprofile'),
});
filesToCopy.push({
source: path.join(appRoot, 'out/vs/workbench/contrib/terminal/browser/media/shellIntegration-env.zsh'),
dest: path.join(zdotdir, '.zshenv'),
});
filesToCopy.push({
source: path.join(appRoot, 'out/vs/workbench/contrib/terminal/browser/media/shellIntegration-login.zsh'),
dest: path.join(zdotdir, '.zlogin'),
});
return { newArgs, envMixin, filesToCopy };
}
}
logService.warn(
`Shell integration cannot be enabled for executable "${shellLaunchConfig.executable}" and args`,
shellLaunchConfig.args,
);
return undefined;
}
var ShellIntegrationExecutable;
(function (ShellIntegrationExecutable) {
ShellIntegrationExecutable['WindowsPwsh'] = 'windows-pwsh';
ShellIntegrationExecutable['WindowsPwshLogin'] = 'windows-pwsh-login';
ShellIntegrationExecutable['Pwsh'] = 'pwsh';
ShellIntegrationExecutable['PwshLogin'] = 'pwsh-login';
ShellIntegrationExecutable['Zsh'] = 'zsh';
ShellIntegrationExecutable['ZshLogin'] = 'zsh-login';
ShellIntegrationExecutable['Bash'] = 'bash';
})(ShellIntegrationExecutable || (ShellIntegrationExecutable = {}));
const shellIntegrationArgs = new Map();
// The try catch swallows execution policy errors in the case of the archive distributable
shellIntegrationArgs.set(ShellIntegrationExecutable.WindowsPwsh, [
'-noexit',
'-command',
'try { . "{0}\\out\\vs\\workbench\\contrib\\terminal\\browser\\media\\shellIntegration.ps1" } catch {}{1}',
]);
shellIntegrationArgs.set(ShellIntegrationExecutable.WindowsPwshLogin, [
'-l',
'-noexit',
'-command',
'try { . "{0}\\out\\vs\\workbench\\contrib\\terminal\\browser\\media\\shellIntegration.ps1" } catch {}{1}',
]);
shellIntegrationArgs.set(ShellIntegrationExecutable.Pwsh, [
'-noexit',
'-command',
'. "{0}/out/vs/workbench/contrib/terminal/browser/media/shellIntegration.ps1"{1}',
]);
shellIntegrationArgs.set(ShellIntegrationExecutable.PwshLogin, [
'-l',
'-noexit',
'-command',
'. "{0}/out/vs/workbench/contrib/terminal/browser/media/shellIntegration.ps1"',
]);
shellIntegrationArgs.set(ShellIntegrationExecutable.Zsh, ['-i']);
shellIntegrationArgs.set(ShellIntegrationExecutable.ZshLogin, ['-il']);
shellIntegrationArgs.set(ShellIntegrationExecutable.Bash, [
'--init-file',
'{0}/out/vs/workbench/contrib/terminal/browser/media/shellIntegration-bash.sh',
]);
const loginArgs = ['-login', '-l'];
const pwshImpliedArgs = ['-nol', '-nologo'];
function arePwshLoginArgs(originalArgs) {
if (typeof originalArgs === 'string') {
return loginArgs.includes(originalArgs.toLowerCase());
} else {
return (
(originalArgs.length === 1 && loginArgs.includes(originalArgs[0].toLowerCase())) ||
(originalArgs.length === 2 &&
(loginArgs.includes(originalArgs[0].toLowerCase()) || loginArgs.includes(originalArgs[1].toLowerCase())) &&
(pwshImpliedArgs.includes(originalArgs[0].toLowerCase()) ||
pwshImpliedArgs.includes(originalArgs[1].toLowerCase())))
);
}
}
function arePwshImpliedArgs(originalArgs) {
if (typeof originalArgs === 'string') {
return pwshImpliedArgs.includes(originalArgs.toLowerCase());
} else {
return (
originalArgs.length === 0 ||
(originalArgs?.length === 1 && pwshImpliedArgs.includes(originalArgs[0].toLowerCase()))
);
}
}
function areZshBashLoginArgs(originalArgs) {
return (
(originalArgs === 'string' && loginArgs.includes(originalArgs.toLowerCase())) ||
(typeof originalArgs !== 'string' && originalArgs.length === 1 && loginArgs.includes(originalArgs[0].toLowerCase()))
);
}