Skip to content

Commit

Permalink
Refactor to improve extensibility of ProcessTaskRunner (#10392)
Browse files Browse the repository at this point in the history
Signed-off-by: Colin Grant <colin.grant@ericsson.com>
  • Loading branch information
colin-grant-work authored Nov 18, 2021
1 parent 29ebe4c commit 07aacaa
Showing 1 changed file with 98 additions and 83 deletions.
181 changes: 98 additions & 83 deletions packages/task/src/node/process/process-task-runner.ts
Original file line number Diff line number Diff line change
Expand Up @@ -40,6 +40,19 @@ import { ProcessTaskError, CommandOptions } from '../../common/process/task-prot
import * as fs from 'fs';
import { ShellProcess } from '@theia/terminal/lib/node/shell-process';

interface OsSpecificCommand {
command: string,
args: Array<string | ShellQuotedString> | undefined,
options: CommandOptions
}

interface ShellSpecificOptions {
/** Arguments passed to the shell, aka `command` here. */
execArgs: string[];
/** Pack of functions used to escape the `subCommand` and `subArgs` to run in the shell. */
quotingFunctions?: ShellQuotingFunctions;
}

/**
* Task runner that runs a task as a process or a command inside a shell.
*/
Expand Down Expand Up @@ -97,24 +110,10 @@ export class ProcessTaskRunner implements TaskRunner {
}
}

private getResolvedCommand(taskConfig: TaskConfiguration): TerminalProcessOptions {
let systemSpecificCommand: {
command: string | undefined
args: Array<string | ShellQuotedString> | undefined
options: CommandOptions
};
// on windows, windows-specific options, if available, take precedence
if (isWindows && taskConfig.windows !== undefined) {
systemSpecificCommand = this.getSystemSpecificCommand(taskConfig, 'windows');
} else if (isOSX && taskConfig.osx !== undefined) { // on macOS, mac-specific options, if available, take precedence
systemSpecificCommand = this.getSystemSpecificCommand(taskConfig, 'osx');
} else if (!isWindows && !isOSX && taskConfig.linux !== undefined) { // on linux, linux-specific options, if available, take precedence
systemSpecificCommand = this.getSystemSpecificCommand(taskConfig, 'linux');
} else { // system-specific options are unavailable, use the default
systemSpecificCommand = this.getSystemSpecificCommand(taskConfig, undefined);
}
protected getResolvedCommand(taskConfig: TaskConfiguration): TerminalProcessOptions {
const osSpecificCommand = this.getOsSpecificCommand(taskConfig);

const options = systemSpecificCommand.options;
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
Expand All @@ -125,10 +124,6 @@ export class ProcessTaskRunner implements TaskRunner {
};
}

if (typeof systemSpecificCommand.command === 'undefined') {
throw new Error('The `command` field of a task cannot be undefined.');
}

/** Executable to actually spawn. */
let command: string;
/** List of arguments passed to `command`. */
Expand Down Expand Up @@ -156,63 +151,20 @@ export class ProcessTaskRunner implements TaskRunner {
// 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 for most shells, so we need to get specific.
// We need to accommodate most shells, so we need to get specific.

/** Shell command to run: */
let shellCommand: string;
/** Arguments passed to the shell, aka `command` here. */
let execArgs: string[] = [];
/** Pack of functions used to escape the `subCommand` and `subArgs` to run in the shell. */
let quotingFunctions: ShellQuotingFunctions | undefined;

const { shell } = systemSpecificCommand.options;
command = shell && shell.executable || ShellProcess.getShellExecutablePath();
args = [];

if (/bash(.exe)?$/.test(command)) {
quotingFunctions = BashQuotingFunctions;
execArgs = ['-c'];

} else if (/wsl(.exe)?$/.test(command)) {
quotingFunctions = BashQuotingFunctions;
execArgs = ['-e'];

} else if (/cmd(.exe)?$/.test(command)) {
quotingFunctions = CmdQuotingFunctions;
execArgs = ['/S', '/C'];

} else if (/(ps|pwsh|powershell)(.exe)?/.test(command)) {
quotingFunctions = PowershellQuotingFunctions;
execArgs = ['-c'];
} else {
quotingFunctions = BashQuotingFunctions;
execArgs = ['-l', '-c'];
const { shell } = osSpecificCommand.options;

command = shell?.executable || ShellProcess.getShellExecutablePath();
const { execArgs, quotingFunctions } = this.getShellSpecificOptions(command);

}
// Allow overriding shell options from task configuration.
if (shell && shell.args) {
args = [...shell.args];
} else {
args = [...execArgs];
}
args = shell?.args ? [...shell.args] : [...execArgs];

// Check if an argument list is defined or not. Empty is ok.
if (Array.isArray(systemSpecificCommand.args)) {
const commandLineElements: Array<string | ShellQuotedString> = [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: ShellQuoting.Strong,
value: arg,
};
} else {
return arg;
}
});
shellCommand = createShellCommandLine(commandLineElements, quotingFunctions);
} else {
// No arguments are provided, so `command` is actually the full command line to execute.
shellCommand = systemSpecificCommand.command;
}
/** Shell command to run: */
const shellCommand = this.buildShellCommand(osSpecificCommand, quotingFunctions);

if (isWindows && /cmd(.exe)?$/.test(command)) {
// Let's take the following command, including an argument containing whitespace:
// cmd> node -p process.argv 1 2 " 3"
Expand All @@ -235,22 +187,85 @@ export class ProcessTaskRunner implements TaskRunner {
// 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 = systemSpecificCommand.command;
if (Array.isArray(systemSpecificCommand.args)) {
command = osSpecificCommand.command;
if (Array.isArray(osSpecificCommand.args)) {
// Process task doesn't handle quotation: Normalize arguments from `ShellQuotedString` to raw `string`.
args = systemSpecificCommand.args.map(arg => typeof arg === 'string' ? arg : arg.value);
args = osSpecificCommand.args.map(arg => typeof arg === 'string' ? arg : arg.value);
} else {
args = [];
}
}
return { command, args, commandLine, options };
}

private getCommand(processType: 'process' | 'shell', terminalProcessOptions: TerminalProcessOptions): string | undefined {
protected buildShellCommand(systemSpecificCommand: OsSpecificCommand, quotingFunctions?: ShellQuotingFunctions): string {
if (Array.isArray(systemSpecificCommand.args)) {
const commandLineElements: Array<string | ShellQuotedString> = [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: ShellQuoting.Strong,
value: arg,
};
} else {
return arg;
}
});
return createShellCommandLine(commandLineElements, quotingFunctions);
} else {
// No arguments are provided, so `command` is actually the full command line to execute.
return systemSpecificCommand.command ?? '';
}
}

protected getShellSpecificOptions(command: string): ShellSpecificOptions {
if (/bash(.exe)?$/.test(command)) {
return {
quotingFunctions: BashQuotingFunctions,
execArgs: ['-c']
};
} else if (/wsl(.exe)?$/.test(command)) {
return {
quotingFunctions: BashQuotingFunctions,
execArgs: ['-e']
};
} else if (/cmd(.exe)?$/.test(command)) {
return {
quotingFunctions: CmdQuotingFunctions,
execArgs: ['/S', '/C']
};
} else if (/(ps|pwsh|powershell)(.exe)?/.test(command)) {
return {
quotingFunctions: PowershellQuotingFunctions,
execArgs: ['-c']
};
} else {
return {
quotingFunctions: BashQuotingFunctions,
execArgs: ['-l', '-c']
};
}
}

protected getOsSpecificCommand(taskConfig: TaskConfiguration): OsSpecificCommand {
// on windows, windows-specific options, if available, take precedence
if (isWindows && taskConfig.windows !== undefined) {
return this.getSystemSpecificCommand(taskConfig, 'windows');
} else if (isOSX && taskConfig.osx !== undefined) { // on macOS, mac-specific options, if available, take precedence
return this.getSystemSpecificCommand(taskConfig, 'osx');
} else if (!isWindows && !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);
}
}

protected getCommand(processType: 'process' | 'shell', terminalProcessOptions: TerminalProcessOptions): string | undefined {
if (terminalProcessOptions.args) {
if (processType === 'shell') {
return terminalProcessOptions.args[terminalProcessOptions.args.length - 1];
Expand Down Expand Up @@ -304,11 +319,7 @@ export class ProcessTaskRunner implements TaskRunner {
return false;
}

private getSystemSpecificCommand(taskConfig: TaskConfiguration, system: 'windows' | 'linux' | 'osx' | undefined): {
command: string | undefined,
args: Array<string | ShellQuotedString> | undefined,
options: CommandOptions
} {
protected getSystemSpecificCommand(taskConfig: TaskConfiguration, system: 'windows' | 'linux' | 'osx' | undefined): OsSpecificCommand {
// initialize with default values from the `taskConfig`
let command: string | undefined = taskConfig.command;
let args: Array<string | ShellQuotedString> | undefined = taskConfig.args;
Expand All @@ -330,6 +341,10 @@ export class ProcessTaskRunner implements TaskRunner {
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 };
}

Expand Down

0 comments on commit 07aacaa

Please sign in to comment.