mirror of
https://github.com/ckaczor/azuredatastudio.git
synced 2026-02-02 17:23:40 -05:00
441 lines
13 KiB
TypeScript
441 lines
13 KiB
TypeScript
/*---------------------------------------------------------------------------------------------
|
|
* Copyright (c) Microsoft Corporation. All rights reserved.
|
|
* Licensed under the Source EULA. See License.txt in the project root for license information.
|
|
*--------------------------------------------------------------------------------------------*/
|
|
|
|
import * as cp from 'child_process';
|
|
import * as nls from 'vs/nls';
|
|
import * as env from 'vs/base/common/platform';
|
|
import * as pfs from 'vs/base/node/pfs';
|
|
import { assign } from 'vs/base/common/objects';
|
|
import { ITerminalLauncher, ITerminalSettings } from 'vs/workbench/contrib/debug/common/debug';
|
|
import { getPathFromAmdModule } from 'vs/base/common/amd';
|
|
import { getDefaultShell } from 'vs/workbench/contrib/terminal/node/terminal';
|
|
|
|
const TERMINAL_TITLE = nls.localize('console.title', "VS Code Console");
|
|
|
|
let terminalLauncher: ITerminalLauncher | undefined = undefined;
|
|
|
|
export function getTerminalLauncher() {
|
|
if (!terminalLauncher) {
|
|
if (env.isWindows) {
|
|
terminalLauncher = new WinTerminalService();
|
|
} else if (env.isMacintosh) {
|
|
terminalLauncher = new MacTerminalService();
|
|
} else if (env.isLinux) {
|
|
terminalLauncher = new LinuxTerminalService();
|
|
}
|
|
}
|
|
return terminalLauncher;
|
|
}
|
|
|
|
let _DEFAULT_TERMINAL_LINUX_READY: Promise<string> | null = null;
|
|
|
|
export function getDefaultTerminalLinuxReady(): Promise<string> {
|
|
if (!_DEFAULT_TERMINAL_LINUX_READY) {
|
|
_DEFAULT_TERMINAL_LINUX_READY = new Promise<string>(resolve => {
|
|
if (env.isLinux) {
|
|
Promise.all<any>([pfs.exists('/etc/debian_version'), process.lazyEnv]).then(([isDebian]) => {
|
|
if (isDebian) {
|
|
resolve('x-terminal-emulator');
|
|
} else if (process.env.DESKTOP_SESSION === 'gnome' || process.env.DESKTOP_SESSION === 'gnome-classic') {
|
|
resolve('gnome-terminal');
|
|
} else if (process.env.DESKTOP_SESSION === 'kde-plasma') {
|
|
resolve('konsole');
|
|
} else if (process.env.COLORTERM) {
|
|
resolve(process.env.COLORTERM);
|
|
} else if (process.env.TERM) {
|
|
resolve(process.env.TERM);
|
|
} else {
|
|
resolve('xterm');
|
|
}
|
|
});
|
|
return;
|
|
}
|
|
|
|
resolve('xterm');
|
|
});
|
|
}
|
|
return _DEFAULT_TERMINAL_LINUX_READY;
|
|
}
|
|
|
|
let _DEFAULT_TERMINAL_WINDOWS: string | null = null;
|
|
|
|
export function getDefaultTerminalWindows(): string {
|
|
if (!_DEFAULT_TERMINAL_WINDOWS) {
|
|
const isWoW64 = !!process.env.hasOwnProperty('PROCESSOR_ARCHITEW6432');
|
|
_DEFAULT_TERMINAL_WINDOWS = `${process.env.windir ? process.env.windir : 'C:\\Windows'}\\${isWoW64 ? 'Sysnative' : 'System32'}\\cmd.exe`;
|
|
}
|
|
return _DEFAULT_TERMINAL_WINDOWS;
|
|
}
|
|
|
|
abstract class TerminalLauncher implements ITerminalLauncher {
|
|
runInTerminal(args: DebugProtocol.RunInTerminalRequestArguments, config: ITerminalSettings): Promise<number | undefined> {
|
|
return this.runInTerminal0(args.title!, args.cwd, args.args, args.env || {}, config);
|
|
}
|
|
|
|
abstract runInTerminal0(title: string, dir: string, args: string[], envVars: env.IProcessEnvironment | {}, config: ITerminalSettings): Promise<number | undefined>;
|
|
}
|
|
|
|
class WinTerminalService extends TerminalLauncher {
|
|
|
|
private static readonly CMD = 'cmd.exe';
|
|
|
|
runInTerminal0(title: string, dir: string, args: string[], envVars: env.IProcessEnvironment, configuration: ITerminalSettings): Promise<number | undefined> {
|
|
|
|
const exec = configuration.external.windowsExec || getDefaultTerminalWindows();
|
|
|
|
return new Promise<number | undefined>((resolve, reject) => {
|
|
|
|
const title = `"${dir} - ${TERMINAL_TITLE}"`;
|
|
const command = `""${args.join('" "')}" & pause"`; // use '|' to only pause on non-zero exit code
|
|
|
|
const cmdArgs = [
|
|
'/c', 'start', title, '/wait', exec, '/c', command
|
|
];
|
|
|
|
// merge environment variables into a copy of the process.env
|
|
const env = assign({}, process.env, envVars);
|
|
|
|
// delete environment variables that have a null value
|
|
Object.keys(env).filter(v => env[v] === null).forEach(key => delete env[key]);
|
|
|
|
const options: any = {
|
|
cwd: dir,
|
|
env: env,
|
|
windowsVerbatimArguments: true
|
|
};
|
|
|
|
const cmd = cp.spawn(WinTerminalService.CMD, cmdArgs, options);
|
|
cmd.on('error', err => {
|
|
reject(improveError(err));
|
|
});
|
|
|
|
resolve(undefined);
|
|
});
|
|
}
|
|
}
|
|
|
|
class MacTerminalService extends TerminalLauncher {
|
|
|
|
private static readonly DEFAULT_TERMINAL_OSX = 'Terminal.app';
|
|
private static readonly OSASCRIPT = '/usr/bin/osascript'; // osascript is the AppleScript interpreter on OS X
|
|
|
|
runInTerminal0(title: string, dir: string, args: string[], envVars: env.IProcessEnvironment, configuration: ITerminalSettings): Promise<number | undefined> {
|
|
|
|
const terminalApp = configuration.external.osxExec || MacTerminalService.DEFAULT_TERMINAL_OSX;
|
|
|
|
return new Promise<number | undefined>((resolve, reject) => {
|
|
|
|
if (terminalApp === MacTerminalService.DEFAULT_TERMINAL_OSX || terminalApp === 'iTerm.app') {
|
|
|
|
// On OS X we launch an AppleScript that creates (or reuses) a Terminal window
|
|
// and then launches the program inside that window.
|
|
|
|
const script = terminalApp === MacTerminalService.DEFAULT_TERMINAL_OSX ? 'TerminalHelper' : 'iTermHelper';
|
|
const scriptpath = getPathFromAmdModule(require, `vs/workbench/contrib/externalTerminal/electron-browser/${script}.scpt`);
|
|
|
|
const osaArgs = [
|
|
scriptpath,
|
|
'-t', title || TERMINAL_TITLE,
|
|
'-w', dir,
|
|
];
|
|
|
|
for (let a of args) {
|
|
osaArgs.push('-a');
|
|
osaArgs.push(a);
|
|
}
|
|
|
|
if (envVars) {
|
|
for (let key in envVars) {
|
|
const value = envVars[key];
|
|
if (value === null) {
|
|
osaArgs.push('-u');
|
|
osaArgs.push(key);
|
|
} else {
|
|
osaArgs.push('-e');
|
|
osaArgs.push(`${key}=${value}`);
|
|
}
|
|
}
|
|
}
|
|
|
|
let stderr = '';
|
|
const osa = cp.spawn(MacTerminalService.OSASCRIPT, osaArgs);
|
|
osa.on('error', err => {
|
|
reject(improveError(err));
|
|
});
|
|
osa.stderr.on('data', (data) => {
|
|
stderr += data.toString();
|
|
});
|
|
osa.on('exit', (code: number) => {
|
|
if (code === 0) { // OK
|
|
resolve(undefined);
|
|
} else {
|
|
if (stderr) {
|
|
const lines = stderr.split('\n', 1);
|
|
reject(new Error(lines[0]));
|
|
} else {
|
|
reject(new Error(nls.localize('mac.terminal.script.failed', "Script '{0}' failed with exit code {1}", script, code)));
|
|
}
|
|
}
|
|
});
|
|
} else {
|
|
reject(new Error(nls.localize('mac.terminal.type.not.supported', "'{0}' not supported", terminalApp)));
|
|
}
|
|
});
|
|
}
|
|
}
|
|
|
|
class LinuxTerminalService extends TerminalLauncher {
|
|
|
|
private static readonly WAIT_MESSAGE = nls.localize('press.any.key', "Press any key to continue...");
|
|
|
|
runInTerminal0(title: string, dir: string, args: string[], envVars: env.IProcessEnvironment, configuration: ITerminalSettings): Promise<number | undefined> {
|
|
|
|
const terminalConfig = configuration.external;
|
|
const execThenable: Promise<string> = terminalConfig.linuxExec ? Promise.resolve(terminalConfig.linuxExec) : getDefaultTerminalLinuxReady();
|
|
|
|
return new Promise<number | undefined>((resolve, reject) => {
|
|
|
|
let termArgs: string[] = [];
|
|
//termArgs.push('--title');
|
|
//termArgs.push(`"${TERMINAL_TITLE}"`);
|
|
execThenable.then(exec => {
|
|
if (exec.indexOf('gnome-terminal') >= 0) {
|
|
termArgs.push('-x');
|
|
} else {
|
|
termArgs.push('-e');
|
|
}
|
|
termArgs.push('bash');
|
|
termArgs.push('-c');
|
|
|
|
const bashCommand = `${quote(args)}; echo; read -p "${LinuxTerminalService.WAIT_MESSAGE}" -n1;`;
|
|
termArgs.push(`''${bashCommand}''`); // wrapping argument in two sets of ' because node is so "friendly" that it removes one set...
|
|
|
|
// merge environment variables into a copy of the process.env
|
|
const env = assign({}, process.env, envVars);
|
|
|
|
// delete environment variables that have a null value
|
|
Object.keys(env).filter(v => env[v] === null).forEach(key => delete env[key]);
|
|
|
|
const options: any = {
|
|
cwd: dir,
|
|
env: env
|
|
};
|
|
|
|
let stderr = '';
|
|
const cmd = cp.spawn(exec, termArgs, options);
|
|
cmd.on('error', err => {
|
|
reject(improveError(err));
|
|
});
|
|
cmd.stderr.on('data', (data) => {
|
|
stderr += data.toString();
|
|
});
|
|
cmd.on('exit', (code: number) => {
|
|
if (code === 0) { // OK
|
|
resolve(undefined);
|
|
} else {
|
|
if (stderr) {
|
|
const lines = stderr.split('\n', 1);
|
|
reject(new Error(lines[0]));
|
|
} else {
|
|
reject(new Error(nls.localize('linux.term.failed', "'{0}' failed with exit code {1}", exec, code)));
|
|
}
|
|
}
|
|
});
|
|
});
|
|
});
|
|
}
|
|
}
|
|
|
|
/**
|
|
* tries to turn OS errors into more meaningful error messages
|
|
*/
|
|
function improveError(err: Error): Error {
|
|
if (err['errno'] === 'ENOENT' && err['path']) {
|
|
return new Error(nls.localize('ext.term.app.not.found', "can't find terminal application '{0}'", err['path']));
|
|
}
|
|
return err;
|
|
}
|
|
|
|
/**
|
|
* 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;
|
|
}
|
|
|
|
|
|
export function hasChildProcesses(processId: number): boolean {
|
|
if (processId) {
|
|
try {
|
|
// if shell has at least one child process, assume that shell is busy
|
|
if (env.isWindows) {
|
|
const result = cp.spawnSync('wmic', ['process', 'get', 'ParentProcessId']);
|
|
if (result.stdout) {
|
|
const pids = result.stdout.toString().split('\r\n');
|
|
if (!pids.some(p => parseInt(p) === processId)) {
|
|
return false;
|
|
}
|
|
}
|
|
} else {
|
|
const result = cp.spawnSync('/usr/bin/pgrep', ['-lP', String(processId)]);
|
|
if (result.stdout) {
|
|
const r = result.stdout.toString().trim();
|
|
if (r.length === 0 || r.indexOf(' tmux') >= 0) { // ignore 'tmux'; see #43683
|
|
return false;
|
|
}
|
|
}
|
|
}
|
|
}
|
|
catch (e) {
|
|
// silently ignore
|
|
}
|
|
}
|
|
// fall back to safe side
|
|
return true;
|
|
}
|
|
|
|
const enum ShellType { cmd, powershell, bash }
|
|
|
|
export function prepareCommand(args: DebugProtocol.RunInTerminalRequestArguments, config: ITerminalSettings): string {
|
|
|
|
let shellType: ShellType;
|
|
|
|
// get the shell configuration for the current platform
|
|
let shell: string;
|
|
const shell_config = config.integrated.shell;
|
|
if (env.isWindows) {
|
|
shell = shell_config.windows || getDefaultShell(env.Platform.Windows);
|
|
shellType = ShellType.cmd;
|
|
} else if (env.isLinux) {
|
|
shell = shell_config.linux || getDefaultShell(env.Platform.Linux);
|
|
shellType = ShellType.bash;
|
|
} else if (env.isMacintosh) {
|
|
shell = shell_config.osx || getDefaultShell(env.Platform.Mac);
|
|
shellType = ShellType.bash;
|
|
} else {
|
|
throw new Error('Unknown platform');
|
|
}
|
|
|
|
// try to determine the shell type
|
|
shell = shell.trim().toLowerCase();
|
|
if (shell.indexOf('powershell') >= 0 || shell.indexOf('pwsh') >= 0) {
|
|
shellType = ShellType.powershell;
|
|
} else if (shell.indexOf('cmd.exe') >= 0) {
|
|
shellType = ShellType.cmd;
|
|
} else if (shell.indexOf('bash') >= 0) {
|
|
shellType = ShellType.bash;
|
|
} else if (shell.indexOf('git\\bin\\bash.exe') >= 0) {
|
|
shellType = ShellType.bash;
|
|
}
|
|
|
|
let quote: (s: string) => string;
|
|
let command = '';
|
|
|
|
switch (shellType) {
|
|
|
|
case ShellType.powershell:
|
|
|
|
quote = (s: string) => {
|
|
s = s.replace(/\'/g, '\'\'');
|
|
return `'${s}'`;
|
|
//return s.indexOf(' ') >= 0 || s.indexOf('\'') >= 0 || s.indexOf('"') >= 0 ? `'${s}'` : s;
|
|
};
|
|
|
|
if (args.cwd) {
|
|
command += `cd '${args.cwd}'; `;
|
|
}
|
|
if (args.env) {
|
|
for (let key in args.env) {
|
|
const value = args.env[key];
|
|
if (value === null) {
|
|
command += `Remove-Item env:${key}; `;
|
|
} else {
|
|
command += `\${env:${key}}='${value}'; `;
|
|
}
|
|
}
|
|
}
|
|
if (args.args && args.args.length > 0) {
|
|
const cmd = quote(args.args.shift()!);
|
|
command += (cmd[0] === '\'') ? `& ${cmd} ` : `${cmd} `;
|
|
for (let a of args.args) {
|
|
command += `${quote(a)} `;
|
|
}
|
|
}
|
|
break;
|
|
|
|
case ShellType.cmd:
|
|
|
|
quote = (s: string) => {
|
|
s = s.replace(/\"/g, '""');
|
|
return (s.indexOf(' ') >= 0 || s.indexOf('"') >= 0) ? `"${s}"` : s;
|
|
};
|
|
|
|
if (args.cwd) {
|
|
command += `cd ${quote(args.cwd)} && `;
|
|
}
|
|
if (args.env) {
|
|
command += 'cmd /C "';
|
|
for (let key in args.env) {
|
|
let value = args.env[key];
|
|
if (value === null) {
|
|
command += `set "${key}=" && `;
|
|
} else {
|
|
value = value.replace(/[\^\&]/g, s => `^${s}`);
|
|
command += `set "${key}=${value}" && `;
|
|
}
|
|
}
|
|
}
|
|
for (let a of args.args) {
|
|
command += `${quote(a)} `;
|
|
}
|
|
if (args.env) {
|
|
command += '"';
|
|
}
|
|
break;
|
|
|
|
case ShellType.bash:
|
|
|
|
quote = (s: string) => {
|
|
s = s.replace(/\"/g, '\\"');
|
|
return (s.indexOf(' ') >= 0 || s.indexOf('\\') >= 0) ? `"${s}"` : s;
|
|
};
|
|
|
|
const hardQuote = (s: string) => {
|
|
return /[^\w@%\/+=,.:^-]/.test(s) ? `'${s.replace(/'/g, '\'\\\'\'')}'` : s;
|
|
};
|
|
|
|
if (args.cwd) {
|
|
command += `cd ${quote(args.cwd)} ; `;
|
|
}
|
|
if (args.env) {
|
|
command += 'env';
|
|
for (let key in args.env) {
|
|
const value = args.env[key];
|
|
if (value === null) {
|
|
command += ` -u ${hardQuote(key)}`;
|
|
} else {
|
|
command += ` ${hardQuote(`${key}=${value}`)}`;
|
|
}
|
|
}
|
|
command += ' ';
|
|
}
|
|
for (let a of args.args) {
|
|
command += `${quote(a)} `;
|
|
}
|
|
break;
|
|
}
|
|
|
|
return command;
|
|
}
|