From 65ffea53952a98b72ecfacce6daba239ea7e1eeb Mon Sep 17 00:00:00 2001 From: elaihau Date: Tue, 25 Jun 2019 13:19:39 -0400 Subject: [PATCH] support linux and osx specific command properties - as per https://github.com/microsoft/vscode/blob/1.35.1/src/vs/workbench/contrib/tasks/common/taskConfiguration.ts#L255, vsCode supports having separated command properties for Windows, OSX, and Linux. This change adds the same support to Theia. - changed the `command` property in CommandProperties interface from mandatory to optional. - part of #5516 Signed-off-by: elaihau --- packages/task/README.md | 32 ++++- .../browser/process/process-task-resolver.ts | 10 ++ .../task/src/browser/task-schema-updater.ts | 121 ++++++++++-------- .../task/src/common/process/task-protocol.ts | 14 +- .../src/node/process/process-task-runner.ts | 101 ++++++++++----- .../task/src/node/task-server.slow-spec.ts | 21 +-- .../task/test-resources/task-long-running-osx | 7 + packages/task/test-resources/task-osx | 7 + 8 files changed, 210 insertions(+), 103 deletions(-) create mode 100755 packages/task/test-resources/task-long-running-osx create mode 100755 packages/task/test-resources/task-osx diff --git a/packages/task/README.md b/packages/task/README.md index 5e8c08ddbb312..d89174b75ebb6 100644 --- a/packages/task/README.md +++ b/packages/task/README.md @@ -14,8 +14,7 @@ Each task configuration looks like this: "-alR" ], "options": { - "cwd": "${workspaceFolder}", - + "cwd": "${workspaceFolder}" }, "windows": { "command": "cmd.exe", @@ -41,8 +40,13 @@ Each task configuration looks like this: - *env*: the environment of the executed program or shell. If omitted the parent process' environment is used. - *shell*: configuration of the shell when task type is `shell`, where users can specify the shell to use with *shell*, and the arguments to be passed to the shell executable to run in command mode with *args*. +By default, *command* and *args* above are used on all platforms. However it's not always possible to express a task in the same way, both on Unix and Windows. The command and/or arguments may be different, for example. If a task needs to work on Linux, MacOS, and Windows, it is better to have separated command, command arguments, and options. + +*windows*: if *windows* is defined, its command, command arguments, and options (i.e., *windows.command*, *windows.args*, and *windows.options*) will take precedence over the *command*, *args*, and *options*, when the task is executed on a Windows backend. + +*osx*: if *osx* is defined, its command, command arguments, and options (i.e., *osx.command*, *osx.args*, and *osx.options*) will take precedence over the *command*, *args*, and *options*, when the task is executed on a MacOS backend. -*windows*: by default, *command* and *args* above are used on all platforms. However it's not always possible to express a task in the same way, both on Unix and Windows. The command and/or arguments may be different, for example. If a task needs to work on both Linux/MacOS and Windows, it can be better to have two separate process options. If *windows* is defined, it will be used instead of *command* and *args*, when a task is executed on a Windows backend. +*linux*: if *linux* is defined, its command, command arguments, and options (i.e., *linux.command*, *linux.args*, and *linux.options*) will take precedence over the *command*, *args*, and *options*, when the task is executed on a Linux backend. Here is a sample tasks.json that can be used to test tasks. Just add this content under the theia source directory, in directory `.theia`: ``` json @@ -54,9 +58,9 @@ Here is a sample tasks.json that can be used to test tasks. Just add this conten "type": "shell", "command": "./task", "args": [ - "1", - "2", - "3" + "default 1", + "default 2", + "default 3" ], "options": { "cwd": "${workspaceFolder}/packages/task/src/node/test-resources/" @@ -66,7 +70,14 @@ Here is a sample tasks.json that can be used to test tasks. Just add this conten "args": [ "/c", "task.bat", - "abc" + "windows abc" + ] + }, + "linux": { + "args": [ + "linux 1", + "linux 2", + "linux 3" ] } }, @@ -128,6 +139,13 @@ The variables are supported in the following properties, using `${variableName}` - `options.cwd` - `windows.command` - `windows.args` +- `windows.options.cwd` +- `osx.command` +- `osx.args` +- `osx.options.cwd` +- `linux.command` +- `linux.args` +- `linux.options.cwd` See [here](https://www.theia-ide.org/doc/index.html) for a detailed documentation. diff --git a/packages/task/src/browser/process/process-task-resolver.ts b/packages/task/src/browser/process/process-task-resolver.ts index 6cf1cc53a6bc3..511406951c3c9 100644 --- a/packages/task/src/browser/process/process-task-resolver.ts +++ b/packages/task/src/browser/process/process-task-resolver.ts @@ -49,6 +49,16 @@ export class ProcessTaskResolver implements TaskResolver { args: processTaskConfig.windows.args ? await this.variableResolverService.resolveArray(processTaskConfig.windows.args, variableResolverOptions) : undefined, options: processTaskConfig.windows.options } : undefined, + osx: processTaskConfig.osx ? { + command: await this.variableResolverService.resolve(processTaskConfig.osx.command, variableResolverOptions), + args: processTaskConfig.osx.args ? await this.variableResolverService.resolveArray(processTaskConfig.osx.args, variableResolverOptions) : undefined, + options: processTaskConfig.osx.options + } : undefined, + linux: processTaskConfig.linux ? { + command: await this.variableResolverService.resolve(processTaskConfig.linux.command, variableResolverOptions), + args: processTaskConfig.linux.args ? await this.variableResolverService.resolveArray(processTaskConfig.linux.args, variableResolverOptions) : undefined, + options: processTaskConfig.linux.options + } : undefined, options: { cwd: await this.variableResolverService.resolve(processTaskConfig.options && processTaskConfig.options.cwd || '${workspaceFolder}', variableResolverOptions), env: processTaskConfig.options && processTaskConfig.options.env, diff --git a/packages/task/src/browser/task-schema-updater.ts b/packages/task/src/browser/task-schema-updater.ts index 4678c6f6fa0a0..5dfaff4e1a164 100644 --- a/packages/task/src/browser/task-schema-updater.ts +++ b/packages/task/src/browser/task-schema-updater.ts @@ -59,10 +59,57 @@ export class TaskSchemaUpdater { } } +const commandSchema: IJSONSchema = { + type: 'string', + description: 'The actual command or script to execute' +}; + +const commandArgSchema: IJSONSchema = { + type: 'array', + description: 'A list of strings, each one being one argument to pass to the command', + items: { + type: 'string' + } +}; + +const commandOptionsSchema: IJSONSchema = { + type: 'object', + description: 'The command options used when the command is executed', + properties: { + cwd: { + type: 'string', + description: 'The directory in which the command will be executed', + default: '${workspaceFolder}' + }, + env: { + type: 'object', + description: 'The environment of the executed program or shell. If omitted the parent process\' environment is used' + }, + shell: { + type: 'object', + description: 'Configuration of the shell when task type is `shell`', + properties: { + executable: { + type: 'string', + description: 'The shell to use' + }, + args: { + type: 'array', + description: `The arguments to be passed to the shell executable to run in command mode + (e.g ['-c'] for bash or ['/S', '/C'] for cmd.exe)`, + items: { + type: 'string' + } + } + } + } + } +}; + const taskConfigurationSchema: IJSONSchema = { oneOf: [ { - 'allOf': [ + allOf: [ { type: 'object', required: ['type', 'label'], @@ -77,68 +124,36 @@ const taskConfigurationSchema: IJSONSchema = { default: 'shell', description: 'Determines what type of process will be used to execute the task. Only shell types will have output shown on the user interface' }, - command: { - type: 'string', - description: 'The actual command or script to execute' - }, - args: { - type: 'array', - description: 'A list of strings, each one being one argument to pass to the command', - items: { - type: 'string' + command: commandSchema, + args: commandArgSchema, + options: commandOptionsSchema, + windows: { + type: 'object', + description: 'Windows specific command configuration that overrides the command, args, and options', + properties: { + command: commandSchema, + args: commandArgSchema, + options: commandOptionsSchema } }, - options: { + osx: { type: 'object', - description: 'The command options used when the command is executed', + description: 'MacOS specific command configuration that overrides the command, args, and options', properties: { - cwd: { - type: 'string', - description: 'The directory in which the command will be executed', - default: '${workspaceFolder}' - }, - env: { - type: 'object', - description: 'The environment of the executed program or shell. If omitted the parent process\' environment is used' - }, - shell: { - type: 'object', - description: 'Configuration of the shell when task type is `shell`', - properties: { - executable: { - type: 'string', - description: 'The shell to use' - }, - args: { - type: 'array', - description: `The arguments to be passed to the shell executable to run in command mode - (e.g ['-c'] for bash or ['/S', '/C'] for cmd.exe)`, - items: { - type: 'string' - } - } - } - } + command: commandSchema, + args: commandArgSchema, + options: commandOptionsSchema } }, - windows: { + linux: { type: 'object', - description: 'Windows specific command configuration overrides command and args', + description: 'Linux specific command configuration that overrides the default command, args, and options', properties: { - command: { - type: 'string', - description: 'The actual command or script to execute' - }, - args: { - type: 'array', - description: 'A list of strings, each one being one argument to pass to the command', - items: { - type: 'string' - } - }, + command: commandSchema, + args: commandArgSchema, + options: commandOptionsSchema } } - } } ] diff --git a/packages/task/src/common/process/task-protocol.ts b/packages/task/src/common/process/task-protocol.ts index 23110af34dff7..7e02962c0d5e6 100644 --- a/packages/task/src/common/process/task-protocol.ts +++ b/packages/task/src/common/process/task-protocol.ts @@ -52,7 +52,7 @@ export interface CommandOptions { } export interface CommandProperties { - readonly command: string; + readonly command?: string; readonly args?: T[]; readonly options?: CommandOptions; } @@ -62,9 +62,19 @@ export interface ProcessTaskConfiguration extends TaskConfiguration, readonly type: ProcessType; /** - * Windows version of CommandProperties. Used in preference on Windows, if defined. + * Windows specific task configuration */ readonly windows?: CommandProperties; + + /** + * macOS specific task configuration + */ + readonly osx?: CommandProperties; + + /** + * Linux specific task configuration + */ + readonly linux?: CommandProperties; } export interface ProcessTaskInfo extends TaskInfo { diff --git a/packages/task/src/node/process/process-task-runner.ts b/packages/task/src/node/process/process-task-runner.ts index 47e4f4ce6b672..72f8e1ee7a138 100644 --- a/packages/task/src/node/process/process-task-runner.ts +++ b/packages/task/src/node/process/process-task-runner.ts @@ -15,7 +15,7 @@ ********************************************************************************/ import { injectable, inject, named } from 'inversify'; -import { isWindows, ILogger } from '@theia/core'; +import { isWindows, isOSX, ILogger } from '@theia/core'; import { FileUri } from '@theia/core/lib/node'; import { TerminalProcessOptions, @@ -59,39 +59,9 @@ export class ProcessTaskRunner implements TaskRunner { throw new Error("Process task config must have 'command' property specified"); } - let command: string | undefined; - let args: Array | undefined; - let options: CommandOptions = {}; - - // on windows, windows-specific options, if available, takes precedence - if (isWindows && taskConfig.windows !== undefined) { - if (taskConfig.windows.command) { - command = taskConfig.windows.command; - args = taskConfig.windows.args; - options = taskConfig.windows.options; - } - } else { - command = taskConfig.command; - args = taskConfig.args; - options = taskConfig.options; - } - - // sanity checks: - // - we expect the cwd to be set by the client. - if (!options || !options.cwd) { - throw new Error("Can't run a task when 'cwd' is not provided by the client"); - } - - // 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 || {}) - }; - } - try { + const { command, args, options } = this.getResolvedCommand(taskConfig); + const processType = taskConfig.type === 'process' ? 'process' : 'shell'; let proc: Process; @@ -139,6 +109,71 @@ export class ProcessTaskRunner implements TaskRunner { } } + private getResolvedCommand(taskConfig: TaskConfiguration): { + command: string | undefined, + args: Array | undefined, + options: CommandOptions + } { + let systemSpecificCommand: { + command: string | undefined, + args: Array | 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); + } + + const options = systemSpecificCommand.options; + // sanity checks: + // - we expect the cwd to be set by the client. + if (!options || !options.cwd) { + throw new Error("Can't run a task when 'cwd' is not provided by the client"); + } + + // 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 || {}) + }; + } + + return systemSpecificCommand; + } + + private getSystemSpecificCommand(taskConfig: TaskConfiguration, system: 'windows' | 'linux' | 'osx' | undefined): { + command: string | undefined, + args: Array | undefined, + options: CommandOptions + } { + // initialise with default values from the `taskConfig` + let command: string | undefined = taskConfig.command; + let args: Array | undefined = taskConfig.args; + let options: CommandOptions = 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; + } + } + + return { command, args, options }; + } + protected asFsPath(uriOrPath: string) { return (uriOrPath.startsWith('file:/')) ? FileUri.fsPath(uriOrPath) diff --git a/packages/task/src/node/task-server.slow-spec.ts b/packages/task/src/node/task-server.slow-spec.ts index d7102038489e2..8722b81ddaff4 100644 --- a/packages/task/src/node/task-server.slow-spec.ts +++ b/packages/task/src/node/task-server.slow-spec.ts @@ -20,7 +20,7 @@ import { TaskExitedEvent, TaskInfo, TaskServer, TaskWatcher, TaskConfiguration } import { ProcessType, ProcessTaskConfiguration } from '../common/process/task-protocol'; import * as http from 'http'; import * as https from 'https'; -import { isWindows } from '@theia/core/lib/common/os'; +import { isWindows, isOSX } from '@theia/core/lib/common/os'; import { FileUri } from '@theia/core/lib/node'; import { terminalsPath } from '@theia/terminal/lib/common/terminal-protocol'; import { expectThrowsAsync } from '@theia/core/lib/common/test/expect'; @@ -34,9 +34,11 @@ import URI from '@theia/core/lib/common/uri'; // test scripts that we bundle with tasks const commandShortRunning = './task'; -const commandShortrunningindows = '.\\task.bat'; +const commandShortRunningOsx = './task-osx'; +const commandShortRunningWindows = '.\\task.bat'; const commandLongRunning = './task-long-running'; +const commandLongRunningOsx = './task-long-running-osx'; const commandLongRunningWindows = '.\\task-long-running.bat'; const bogusCommand = 'thisisnotavalidcommand'; @@ -82,7 +84,7 @@ describe('Task server / back-end', function () { } // create task using terminal process - const command = isWindows ? commandShortrunningindows : commandShortRunning; + const command = isWindows ? commandShortRunningWindows : (isOSX ? commandShortRunningOsx : commandShortRunning); const taskInfo: TaskInfo = await taskServer.run(createProcessTaskConfig('shell', `${command} ${someString}`), wsRoot); const terminalId = taskInfo.terminalId; @@ -93,7 +95,7 @@ describe('Task server / back-end', function () { channel.onClose((code, reason) => reject(new Error(`channel is closed with '${code}' code and '${reason}' reason`))); channel.onMessage(msg => { // check output of task on terminal is what we expect - const expected = `tasking... ${someString}`; + const expected = `${isOSX ? 'tasking osx' : 'tasking'}... ${someString}`; if (msg.toString().indexOf(expected) !== -1) { resolve(); } else { @@ -106,7 +108,7 @@ describe('Task server / back-end', function () { it('task using raw process - task server success response shall not contain a terminal id', async function () { const someString = 'someSingleWordString'; - const command = isWindows ? commandShortrunningindows : commandShortRunning; + const command = isWindows ? commandShortRunningWindows : (isOSX ? commandShortRunningOsx : commandShortRunning); const executable = FileUri.fsPath(wsRootUri.resolve(command)); // create task using raw process @@ -129,7 +131,7 @@ describe('Task server / back-end', function () { }); it('task is executed successfully with cwd as a file URI', async function () { - const command = isWindows ? commandShortrunningindows : commandShortRunning; + const command = isWindows ? commandShortRunningWindows : (isOSX ? commandShortRunningOsx : commandShortRunning); const config = createProcessTaskConfig('shell', command, [], FileUri.create(wsRoot).toString()); const taskInfo: TaskInfo = await taskServer.run(config, wsRoot); @@ -139,7 +141,7 @@ describe('Task server / back-end', function () { }); it('task is executed successfully using terminal process', async function () { - const command = isWindows ? commandShortrunningindows : commandShortRunning; + const command = isWindows ? commandShortRunningWindows : (isOSX ? commandShortRunningOsx : commandShortRunning); const taskInfo: TaskInfo = await taskServer.run(createProcessTaskConfig('shell', command, []), wsRoot); const p = checkSuccessfullProcessExit(taskInfo, taskWatcher); @@ -148,7 +150,7 @@ describe('Task server / back-end', function () { }); it('task is executed successfully using raw process', async function () { - const command = isWindows ? commandShortrunningindows : commandShortRunning; + const command = isWindows ? commandShortRunningWindows : (isOSX ? commandShortRunningOsx : commandShortRunning); const executable = FileUri.fsPath(wsRootUri.resolve(command)); const taskInfo: TaskInfo = await taskServer.run(createProcessTaskConfig('process', executable, [])); @@ -403,6 +405,9 @@ function createTaskConfigTaskLongRunning(processType: ProcessType): TaskConfigur command: FileUri.fsPath(wsRootUri.resolve(commandLongRunningWindows)), args: [], options: { cwd: wsRoot } + }, + osx: { + command: FileUri.fsPath(wsRootUri.resolve(commandLongRunningOsx)) } }; } diff --git a/packages/task/test-resources/task-long-running-osx b/packages/task/test-resources/task-long-running-osx new file mode 100755 index 0000000000000..844a777754288 --- /dev/null +++ b/packages/task/test-resources/task-long-running-osx @@ -0,0 +1,7 @@ +#!/bin/bash + +for i in {1..300} +do + sleep 1 + echo "tasking osx... $i" +done diff --git a/packages/task/test-resources/task-osx b/packages/task/test-resources/task-osx new file mode 100755 index 0000000000000..b20b71b367de8 --- /dev/null +++ b/packages/task/test-resources/task-osx @@ -0,0 +1,7 @@ +#!/bin/bash + +for i in $@ +do + sleep 1 + echo "tasking osx... $i" +done