@theia/task
Version:
Theia - Task extension. This extension adds support for executing raw or terminal processes in the backend.
348 lines • 15.9 kB
JavaScript
// *****************************************************************************
// Copyright (C) 2017-2019 Ericsson and others.
//
// This program and the accompanying materials are made available under the
// terms of the Eclipse Public License v. 2.0 which is available at
// http://www.eclipse.org/legal/epl-2.0.
//
// This Source Code may also be made available under the following Secondary
// Licenses when the conditions for such availability set forth in the Eclipse
// Public License v. 2.0 are satisfied: GNU General Public License, version 2
// with the GNU Classpath Exception which is available at
// https://www.gnu.org/software/classpath/license.html.
//
// SPDX-License-Identifier: EPL-2.0 OR GPL-2.0-only WITH Classpath-exception-2.0
// *****************************************************************************
Object.defineProperty(exports, "__esModule", { value: true });
exports.ProcessTaskRunner = void 0;
const tslib_1 = require("tslib");
/*---------------------------------------------------------------------------------------------
* Copyright (c) Microsoft Corporation. All rights reserved.
* Licensed under the MIT License. See License.txt in the project root for license information.
*--------------------------------------------------------------------------------------------*/
const inversify_1 = require("@theia/core/shared/inversify");
const core_1 = require("@theia/core");
const node_1 = require("@theia/core/lib/node");
const node_2 = require("@theia/process/lib/node");
const shell_quoting_1 = require("@theia/process/lib/common/shell-quoting");
const process_task_1 = require("./process-task");
const task_protocol_1 = require("../../common/process/task-protocol");
const fs = require("fs");
const shell_process_1 = require("@theia/terminal/lib/node/shell-process");
/**
* Task runner that runs a task as a process or a command inside a shell.
*/
let ProcessTaskRunner = class ProcessTaskRunner {
/**
* Runs a task from the given task configuration.
* @param taskConfig task configuration to run a task from. The provided task configuration must have a shape of `CommandProperties`.
*/
async run(taskConfig, ctx) {
if (!taskConfig.command) {
throw new Error("Process task config must have 'command' property specified");
}
try {
// Always spawn a task in a pty, the only difference between shell/process tasks is the
// way the command is passed:
// - process: directly look for an executable and pass a specific set of arguments/options.
// - shell: defer the spawning to a shell that will evaluate a command line with our executable.
const terminalProcessOptions = this.getResolvedCommand(taskConfig);
const terminal = this.taskTerminalProcessFactory(terminalProcessOptions);
// Wait for the confirmation that the process is successfully started, or has failed to start.
await new Promise((resolve, reject) => {
terminal.onStart(resolve);
terminal.onError((error) => {
reject(task_protocol_1.ProcessTaskError.CouldNotRun(error.code));
});
});
const processType = (taskConfig.executionType || taskConfig.type);
return this.taskFactory({
label: taskConfig.label,
process: terminal,
processType,
context: ctx,
config: taskConfig,
command: this.getCommand(processType, terminalProcessOptions)
});
}
catch (error) {
this.logger.error(`Error occurred while creating task: ${error}`);
throw error;
}
}
getResolvedCommand(taskConfig) {
const osSpecificCommand = this.getOsSpecificCommand(taskConfig);
const options = osSpecificCommand.options;
// Use task's cwd with spawned process and pass node env object to
// new process, so e.g. we can re-use the system path
if (options) {
options.env = {
...process.env,
...(options.env || {})
};
}
/** Executable to actually spawn. */
let command;
/** List of arguments passed to `command`. */
let args;
/**
* Only useful on Windows, has to do with how node-pty handles complex commands.
* This string should not include the executable, only what comes after it (arguments).
*/
let commandLine;
if ((taskConfig.executionType || taskConfig.type) === 'shell') {
// When running a shell task, we have to spawn a shell process somehow,
// and tell it to run the command the user wants to run inside of it.
//
// E.g:
// - Spawning a process:
// spawn(process_exe, [...args])
// - Spawning a shell and run a command:
// spawn(shell_exe, [shell_exec_cmd_flag, command])
//
// The fun part is, the `command` to pass as an argument usually has to be
// what you would type verbatim inside the shell, so escaping rules apply.
//
// What's even more funny is that on Windows, node-pty uses a special
// mechanism to pass complex escaped arguments, via a string.
//
// We need to accommodate most shells, so we need to get specific.
const { shell } = osSpecificCommand.options;
command = (shell === null || shell === void 0 ? void 0 : shell.executable) || shell_process_1.ShellProcess.getShellExecutablePath();
const { execArgs, quotingFunctions } = this.getShellSpecificOptions(command);
// Allow overriding shell options from task configuration.
args = (shell === null || shell === void 0 ? void 0 : shell.args) ? [...shell.args] : [...execArgs];
// Check if an argument list is defined or not. Empty is ok.
/** Shell command to run: */
const shellCommand = this.buildShellCommand(osSpecificCommand, quotingFunctions);
if (core_1.isWindows && /cmd(.exe)?$/.test(command)) {
// Let's take the following command, including an argument containing whitespace:
// cmd> node -p process.argv 1 2 " 3"
//
// We would expect the following output:
// json> [ '...\\node.exe', '1', '2', ' 3' ]
//
// Let's run this command through `cmd.exe` using `child_process`:
// js> void childprocess.spawn('cmd.exe', ['/s', '/c', 'node -p process.argv 1 2 " 3"']).stderr.on('data', console.log)
//
// We get the correct output, but when using node-pty:
// js> void nodepty.spawn('cmd.exe', ['/s', '/c', 'node -p process.argv 1 2 " 3"']).on('data', console.log)
//
// Then the output looks like:
// json> [ '...\\node.exe', '1', '2', '"', '3"' ]
//
// To fix that, we need to use a special node-pty feature and pass arguments as one string:
// js> nodepty.spawn('cmd.exe', '/s /c "node -p process.argv 1 2 " 3""')
//
// Note the extra quotes that need to be added around the whole command.
commandLine = [...args, `"${shellCommand}"`].join(' ');
}
args.push(shellCommand);
}
else {
// When running process tasks, `command` is the executable to run,
// and `args` are the arguments we want to pass to it.
command = osSpecificCommand.command;
if (Array.isArray(osSpecificCommand.args)) {
// Process task doesn't handle quotation: Normalize arguments from `ShellQuotedString` to raw `string`.
args = osSpecificCommand.args.map(arg => typeof arg === 'string' ? arg : arg.value);
}
else {
args = [];
}
}
return { command, args, commandLine, options };
}
buildShellCommand(systemSpecificCommand, quotingFunctions) {
var _a;
if (Array.isArray(systemSpecificCommand.args)) {
const commandLineElements = [systemSpecificCommand.command, ...systemSpecificCommand.args].map(arg => {
// We want to quote arguments only if needed.
if (quotingFunctions && typeof arg === 'string' && this.argumentNeedsQuotes(arg, quotingFunctions)) {
return {
quoting: "strong" /* ShellQuoting.Strong */,
value: arg,
};
}
else {
return arg;
}
});
return (0, shell_quoting_1.createShellCommandLine)(commandLineElements, quotingFunctions);
}
else {
// No arguments are provided, so `command` is actually the full command line to execute.
return (_a = systemSpecificCommand.command) !== null && _a !== void 0 ? _a : '';
}
}
getShellSpecificOptions(command) {
if (/bash(.exe)?$/.test(command)) {
return {
quotingFunctions: shell_quoting_1.BashQuotingFunctions,
execArgs: ['-c']
};
}
else if (/wsl(.exe)?$/.test(command)) {
return {
quotingFunctions: shell_quoting_1.BashQuotingFunctions,
execArgs: ['-e']
};
}
else if (/cmd(.exe)?$/.test(command)) {
return {
quotingFunctions: shell_quoting_1.CmdQuotingFunctions,
execArgs: ['/S', '/C']
};
}
else if (/(ps|pwsh|powershell)(.exe)?/.test(command)) {
return {
quotingFunctions: shell_quoting_1.PowershellQuotingFunctions,
execArgs: ['-c']
};
}
else {
return {
quotingFunctions: shell_quoting_1.BashQuotingFunctions,
execArgs: ['-l', '-c']
};
}
}
getOsSpecificCommand(taskConfig) {
// on windows, windows-specific options, if available, take precedence
if (core_1.isWindows && taskConfig.windows !== undefined) {
return this.getSystemSpecificCommand(taskConfig, 'windows');
}
else if (core_1.isOSX && taskConfig.osx !== undefined) { // on macOS, mac-specific options, if available, take precedence
return this.getSystemSpecificCommand(taskConfig, 'osx');
}
else if (!core_1.isWindows && !core_1.isOSX && taskConfig.linux !== undefined) { // on linux, linux-specific options, if available, take precedence
return this.getSystemSpecificCommand(taskConfig, 'linux');
}
else { // system-specific options are unavailable, use the default
return this.getSystemSpecificCommand(taskConfig, undefined);
}
}
getCommand(processType, terminalProcessOptions) {
if (terminalProcessOptions.args) {
if (processType === 'shell') {
return terminalProcessOptions.args[terminalProcessOptions.args.length - 1];
}
else if (processType === 'process') {
return `${terminalProcessOptions.command} ${terminalProcessOptions.args.join(' ')}`;
}
}
}
/**
* This is task specific, to align with VS Code's behavior.
*
* When parsing arguments, VS Code will try to detect if the user already
* tried to quote things.
*
* See: https://github.com/microsoft/vscode/blob/d363b988e1e58cf49963841c498681cdc6cb55a3/src/vs/workbench/contrib/tasks/browser/terminalTaskSystem.ts#L1101-L1127
*
* @param value
* @param shellQuotingOptions
*/
argumentNeedsQuotes(value, shellQuotingOptions) {
const { characters } = shellQuotingOptions;
const needQuotes = new Set([' ', ...characters.needQuotes || []]);
if (!characters) {
return false;
}
if (value.length >= 2) {
const first = value[0] === characters.strong ? characters.strong : value[0] === characters.weak ? characters.weak : undefined;
if (first === value[value.length - 1]) {
return false;
}
}
let quote;
for (let i = 0; i < value.length; i++) {
// We found the end quote.
const ch = value[i];
if (ch === quote) {
quote = undefined;
}
else if (quote !== undefined) {
// skip the character. We are quoted.
continue;
}
else if (ch === characters.escape) {
// Skip the next character
i++;
}
else if (ch === characters.strong || ch === characters.weak) {
quote = ch;
}
else if (needQuotes.has(ch)) {
return true;
}
}
return false;
}
getSystemSpecificCommand(taskConfig, system) {
// initialize with default values from the `taskConfig`
let command = taskConfig.command;
let args = taskConfig.args;
let options = (0, core_1.deepClone)(taskConfig.options) || {};
if (system) {
if (taskConfig[system].command) {
command = taskConfig[system].command;
}
if (taskConfig[system].args) {
args = taskConfig[system].args;
}
if (taskConfig[system].options) {
options = taskConfig[system].options;
}
}
if (options.cwd) {
options.cwd = this.asFsPath(options.cwd);
}
if (command === undefined) {
throw new Error('The `command` field of a task cannot be undefined.');
}
return { command, args, options };
}
asFsPath(uriOrPath) {
return (uriOrPath.startsWith('file:'))
? node_1.FileUri.fsPath(uriOrPath)
: uriOrPath;
}
/**
* @deprecated
*
* Remove ProcessTaskRunner.findCommand, introduce process "started" event
* Checks for the existence of a file, at the provided path, and make sure that
* it's readable and executable.
*/
async executableFileExists(filePath) {
return new Promise(async (resolve, reject) => {
fs.access(filePath, fs.constants.F_OK | fs.constants.X_OK, err => {
resolve(err ? false : true);
});
});
}
};
exports.ProcessTaskRunner = ProcessTaskRunner;
tslib_1.__decorate([
(0, inversify_1.inject)(core_1.ILogger),
(0, inversify_1.named)('task'),
tslib_1.__metadata("design:type", Object)
], ProcessTaskRunner.prototype, "logger", void 0);
tslib_1.__decorate([
(0, inversify_1.inject)(node_2.RawProcessFactory),
tslib_1.__metadata("design:type", Function)
], ProcessTaskRunner.prototype, "rawProcessFactory", void 0);
tslib_1.__decorate([
(0, inversify_1.inject)(node_2.TaskTerminalProcessFactory),
tslib_1.__metadata("design:type", Function)
], ProcessTaskRunner.prototype, "taskTerminalProcessFactory", void 0);
tslib_1.__decorate([
(0, inversify_1.inject)(process_task_1.TaskFactory),
tslib_1.__metadata("design:type", Function)
], ProcessTaskRunner.prototype, "taskFactory", void 0);
exports.ProcessTaskRunner = ProcessTaskRunner = tslib_1.__decorate([
(0, inversify_1.injectable)()
], ProcessTaskRunner);
//# sourceMappingURL=process-task-runner.js.map
;