@theia/task
Version:
Theia - Task extension. This extension adds support for executing raw or terminal processes in the backend.
391 lines • 20.7 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 });
// tslint:disable-next-line:no-implicit-dependencies
require("reflect-metadata");
const task_test_container_1 = require("./test/task-test-container");
const backend_application_1 = require("@theia/core/lib/node/backend-application");
const common_1 = require("../common");
const os_1 = require("@theia/core/lib/common/os");
const node_1 = require("@theia/core/lib/node");
const terminal_protocol_1 = require("@theia/terminal/lib/common/terminal-protocol");
const test_web_socket_channel_1 = require("@theia/core/lib/node/messaging/test/test-web-socket-channel");
const chai_1 = require("chai");
const buffering_stream_1 = require("@theia/terminal/lib/node/buffering-stream");
// test scripts that we bundle with tasks
const commandShortRunning = './task';
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';
const commandUnixNoop = 'true';
const commandWindowsNoop = 'rundll32.exe';
/** Expects argv to be ['a', 'b', 'c'] */
const script0 = './test-arguments-0.js';
/** Expects argv to be ['a', 'b', ' c'] */
const script1 = './test-arguments-1.js';
/** Expects argv to be ['a', 'b', 'c"'] */
const script2 = './test-arguments-2.js';
// we use test-resources subfolder ('<theia>/packages/task/test-resources/'),
// as workspace root, for these tests
const wsRootUri = node_1.FileUri.create(__dirname).resolve('../../test-resources');
const wsRoot = node_1.FileUri.fsPath(wsRootUri);
describe('Task server / back-end', function () {
this.timeout(10000);
let backend;
let server;
let taskServer;
let taskWatcher;
beforeEach(async () => {
delete process.env['THEIA_TASK_TEST_DEBUG'];
const testContainer = (0, task_test_container_1.createTaskTestContainer)();
taskWatcher = testContainer.get(common_1.TaskWatcher);
taskServer = testContainer.get(common_1.TaskServer);
taskServer.setClient(taskWatcher.getTaskClient());
backend = testContainer.get(backend_application_1.BackendApplication);
server = await backend.start(3000, 'localhost');
});
afterEach(async () => {
const _backend = backend;
const _server = server;
backend = undefined;
taskServer = undefined;
taskWatcher = undefined;
server = undefined;
_backend['onStop']();
_server.close();
});
it('task running in terminal - expected data is received from the terminal ws server', async function () {
const someString = 'someSingleWordString';
// create task using terminal process
const command = os_1.isWindows ? commandShortRunningWindows : (os_1.isOSX ? commandShortRunningOsx : commandShortRunning);
const taskInfo = await taskServer.run(createProcessTaskConfig('shell', `${command} ${someString}`), wsRoot);
const terminalId = taskInfo.terminalId;
const messagesToWaitFor = 10;
const messages = [];
// check output of task on terminal is what we expect
const expected = `${os_1.isOSX ? 'tasking osx' : 'tasking'}... ${someString}`;
// hook-up to terminal's ws and confirm that it outputs expected tasks' output
await new Promise((resolve, reject) => {
const setup = new test_web_socket_channel_1.TestWebSocketChannelSetup({ server, path: `${terminal_protocol_1.terminalsPath}/${terminalId}` });
const stringBuffer = new buffering_stream_1.StringBufferingStream();
setup.connectionProvider.listen(`${terminal_protocol_1.terminalsPath}/${terminalId}`, (path, channel) => {
channel.onMessage(e => stringBuffer.push(e().readString()));
channel.onError(reject);
channel.onClose(() => reject(new Error('Channel has been closed')));
}, false);
stringBuffer.onData(currentMessage => {
// Instead of waiting for one message from the terminal, we wait for several ones as the very first message can be something unexpected.
// For instance: `nvm is not compatible with the \"PREFIX\" environment variable: currently set to \"/usr/local\"\r\n`
messages.unshift(currentMessage);
if (currentMessage.includes(expected)) {
resolve();
}
else if (messages.length >= messagesToWaitFor) {
reject(new Error(`expected sub-string not found in terminal output. Expected: "${expected}" vs Actual messages: ${JSON.stringify(messages)}`));
}
});
});
});
it('task using raw process - task server success response shall not contain a terminal id', async function () {
const someString = 'someSingleWordString';
const command = os_1.isWindows ? commandShortRunningWindows : (os_1.isOSX ? commandShortRunningOsx : commandShortRunning);
const executable = node_1.FileUri.fsPath(wsRootUri.resolve(command));
// create task using raw process
const taskInfo = await taskServer.run(createProcessTaskConfig('process', executable, [someString]), wsRoot);
await new Promise((resolve, reject) => {
const toDispose = taskWatcher.onTaskExit((event) => {
if (event.taskId === taskInfo.taskId && event.code === 0) {
if (typeof taskInfo.terminalId === 'number') {
resolve();
}
else {
reject(new Error(`terminal id was expected to be a number, got: ${typeof taskInfo.terminalId}`));
}
toDispose.dispose();
}
});
});
});
it('task is executed successfully with cwd as a file URI', async function () {
const command = os_1.isWindows ? commandShortRunningWindows : (os_1.isOSX ? commandShortRunningOsx : commandShortRunning);
const config = createProcessTaskConfig('shell', command, undefined, node_1.FileUri.create(wsRoot).toString());
const taskInfo = await taskServer.run(config, wsRoot);
await checkSuccessfulProcessExit(taskInfo, taskWatcher);
});
it('task is executed successfully using terminal process', async function () {
const command = os_1.isWindows ? commandShortRunningWindows : (os_1.isOSX ? commandShortRunningOsx : commandShortRunning);
const taskInfo = await taskServer.run(createProcessTaskConfig('shell', command, undefined), wsRoot);
await checkSuccessfulProcessExit(taskInfo, taskWatcher);
});
it('task is executed successfully using raw process', async function () {
const command = os_1.isWindows ? commandShortRunningWindows : (os_1.isOSX ? commandShortRunningOsx : commandShortRunning);
const executable = node_1.FileUri.fsPath(wsRootUri.resolve(command));
const taskInfo = await taskServer.run(createProcessTaskConfig('process', executable, []));
await checkSuccessfulProcessExit(taskInfo, taskWatcher);
});
it('task without a specific runner is executed successfully using as a process', async function () {
const command = os_1.isWindows ? commandWindowsNoop : commandUnixNoop;
// there's no runner registered for the 'npm' task type
const taskConfig = createTaskConfig('npm', command, []);
const taskInfo = await taskServer.run(taskConfig, wsRoot);
await checkSuccessfulProcessExit(taskInfo, taskWatcher);
});
it('task can successfully execute command found in system path using a terminal process', async function () {
const command = os_1.isWindows ? commandWindowsNoop : commandUnixNoop;
const opts = createProcessTaskConfig('shell', command, []);
const taskInfo = await taskServer.run(opts, wsRoot);
await checkSuccessfulProcessExit(taskInfo, taskWatcher);
});
it('task can successfully execute command found in system path using a raw process', async function () {
const command = os_1.isWindows ? commandWindowsNoop : commandUnixNoop;
const taskInfo = await taskServer.run(createProcessTaskConfig('process', command, []), wsRoot);
await checkSuccessfulProcessExit(taskInfo, taskWatcher);
});
it('task using type "shell" can be killed', async function () {
const taskInfo = await taskServer.run(createTaskConfigTaskLongRunning('shell'), wsRoot);
const exitStatusPromise = getExitStatus(taskInfo, taskWatcher);
taskServer.kill(taskInfo.taskId);
const exitStatus = await exitStatusPromise;
// node-pty reports different things on Linux/macOS vs Windows when
// killing a process. This is not ideal, but that's how things are
// currently. Ideally, its behavior should be aligned as much as
// possible on what node's child_process module does.
if (os_1.isWindows) {
// On Windows, node-pty just reports an exit code of 0.
(0, chai_1.expect)(exitStatus).equals(1);
}
else {
// On Linux/macOS, node-pty sends SIGHUP by default, for some reason.
(0, chai_1.expect)(exitStatus).equals('SIGHUP');
}
});
it('task using type "process" can be killed', async function () {
const taskInfo = await taskServer.run(createTaskConfigTaskLongRunning('process'), wsRoot);
const exitStatusPromise = getExitStatus(taskInfo, taskWatcher);
taskServer.kill(taskInfo.taskId);
const exitStatus = await exitStatusPromise;
// node-pty reports different things on Linux/macOS vs Windows when
// killing a process. This is not ideal, but that's how things are
// currently. Ideally, its behavior should be aligned as much as
// possible on what node's child_process module does.
if (os_1.isWindows) {
// On Windows, node-pty just reports an exit code of 1.
(0, chai_1.expect)(exitStatus).equals(1);
}
else {
// On Linux/macOS, node-pty sends SIGHUP by default, for some reason.
(0, chai_1.expect)(exitStatus).equals('SIGHUP');
}
});
/**
* TODO: Figure out how to debug a process that correctly starts but exits with a return code > 0
*/
it('task using terminal process can handle command that does not exist', async function () {
const taskInfo = await taskServer.run(createProcessTaskConfig2('shell', bogusCommand, []), wsRoot);
const code = await new Promise((resolve, reject) => {
taskWatcher.onTaskExit((event) => {
if (event.taskId !== taskInfo.taskId || event.code === undefined) {
reject(new Error(JSON.stringify(event)));
}
else {
resolve(event.code);
}
});
});
// node-pty reports different things on Linux/macOS vs Windows when
// killing a process. This is not ideal, but that's how things are
// currently. Ideally, its behavior should be aligned as much as
// possible on what node's child_process module does.
if (os_1.isWindows) {
(0, chai_1.expect)(code).equals(1);
}
else {
(0, chai_1.expect)(code).equals(127);
}
});
it('getTasks(ctx) returns tasks according to created context', async function () {
const context1 = 'aContext';
const context2 = 'anotherContext';
// create some tasks: 4 for context1, 2 for context2
const task1 = await taskServer.run(createTaskConfigTaskLongRunning('shell'), context1);
const task2 = await taskServer.run(createTaskConfigTaskLongRunning('process'), context2);
const task3 = await taskServer.run(createTaskConfigTaskLongRunning('shell'), context1);
const task4 = await taskServer.run(createTaskConfigTaskLongRunning('process'), context2);
const task5 = await taskServer.run(createTaskConfigTaskLongRunning('shell'), context1);
const task6 = await taskServer.run(createTaskConfigTaskLongRunning('process'), context1);
const runningTasksCtx1 = await taskServer.getTasks(context1); // should return 4 tasks
const runningTasksCtx2 = await taskServer.getTasks(context2); // should return 2 tasks
const runningTasksAll = await taskServer.getTasks(); // should return 6 tasks
if (runningTasksCtx1.length !== 4) {
throw new Error(`Error: unexpected number of running tasks for context 1: expected: 4, actual: ${runningTasksCtx1.length}`);
}
if (runningTasksCtx2.length !== 2) {
throw new Error(`Error: unexpected number of running tasks for context 2: expected: 2, actual: ${runningTasksCtx1.length}`);
}
if (runningTasksAll.length !== 6) {
throw new Error(`Error: unexpected total number of running tasks for all contexts: expected: 6, actual: ${runningTasksCtx1.length}`);
}
// cleanup
await taskServer.kill(task1.taskId);
await taskServer.kill(task2.taskId);
await taskServer.kill(task3.taskId);
await taskServer.kill(task4.taskId);
await taskServer.kill(task5.taskId);
await taskServer.kill(task6.taskId);
});
it('creating and killing a bunch of tasks works as expected', async function () {
// const command = isWindows ? command_absolute_path_long_running_windows : command_absolute_path_long_running;
const numTasks = 20;
const taskInfo = [];
// create a mix of terminal and raw processes
for (let i = 0; i < numTasks; i++) {
if (i % 2 === 0) {
taskInfo.push(await taskServer.run(createTaskConfigTaskLongRunning('shell')));
}
else {
taskInfo.push(await taskServer.run(createTaskConfigTaskLongRunning('process')));
}
}
const numRunningTasksAfterCreated = await taskServer.getTasks();
for (let i = 0; i < taskInfo.length; i++) {
await taskServer.kill(taskInfo[i].taskId);
}
const numRunningTasksAfterKilled = await taskServer.getTasks();
if (numRunningTasksAfterCreated.length !== numTasks) {
throw new Error(`Error: unexpected number of running tasks: expected: ${numTasks}, actual: ${numRunningTasksAfterCreated.length}`);
}
if (numRunningTasksAfterKilled.length !== 0) {
throw new Error(`Error: remaining running tasks, after all killed: expected: 0, actual: ${numRunningTasksAfterKilled.length}`);
}
});
it('shell task should execute the command as a whole if not arguments are specified', async function () {
const taskInfo = await taskServer.run(createProcessTaskConfig2('shell', `node ${script0} debug-hint:0a a b c`));
const exitStatus = await getExitStatus(taskInfo, taskWatcher);
(0, chai_1.expect)(exitStatus).eq(0);
});
it('shell task should fail if user defines a full command line and arguments', async function () {
const taskInfo = await taskServer.run(createProcessTaskConfig2('shell', `node ${script0} debug-hint:0b a b c`, []));
const exitStatus = await getExitStatus(taskInfo, taskWatcher);
(0, chai_1.expect)(exitStatus).not.eq(0);
});
it('shell task should be able to exec using simple arguments', async function () {
const taskInfo = await taskServer.run(createProcessTaskConfig2('shell', 'node', [script0, 'debug-hint:0c', 'a', 'b', 'c']));
const exitStatus = await getExitStatus(taskInfo, taskWatcher);
(0, chai_1.expect)(exitStatus).eq(0);
});
it('shell task should be able to run using arguments containing whitespace', async function () {
const taskInfo = await taskServer.run(createProcessTaskConfig2('shell', 'node', [script1, 'debug-hint:1', 'a', 'b', ' c']));
const exitStatus = await getExitStatus(taskInfo, taskWatcher);
(0, chai_1.expect)(exitStatus).eq(0);
});
it('shell task will fail if user specify problematic arguments', async function () {
const taskInfo = await taskServer.run(createProcessTaskConfig2('shell', 'node', [script2, 'debug-hint:2a', 'a', 'b', 'c"']));
const exitStatus = await getExitStatus(taskInfo, taskWatcher);
(0, chai_1.expect)(exitStatus).not.eq(0);
});
it('shell task should be able to run using arguments specifying which quoting method to use', async function () {
const taskInfo = await taskServer.run(createProcessTaskConfig2('shell', 'node', [script2, 'debug-hint:2b', 'a', 'b', { value: 'c"', quoting: 'escape' }]));
const exitStatus = await getExitStatus(taskInfo, taskWatcher);
(0, chai_1.expect)(exitStatus).eq(0);
});
it('shell task should be able to run using arguments with forbidden characters but no whitespace', async function () {
const taskInfo = await taskServer.run(createProcessTaskConfig2('shell', 'node', ['-e', 'setTimeout(console.log,1000,1+2)']));
const exitStatus = await getExitStatus(taskInfo, taskWatcher);
(0, chai_1.expect)(exitStatus).eq(0);
});
});
function createTaskConfig(taskType, command, args) {
const options = {
label: 'test task',
type: taskType,
_source: '/source/folder',
_scope: '/source/folder',
command,
args,
options: { cwd: wsRoot }
};
return options;
}
function createProcessTaskConfig(processType, command, args, cwd = wsRoot) {
return {
label: 'test task',
type: processType,
_source: '/source/folder',
_scope: '/source/folder',
command,
args,
options: { cwd },
};
}
// eslint-disable-next-line @typescript-eslint/no-explicit-any
function createProcessTaskConfig2(processType, command, args) {
return {
label: 'test task',
type: processType,
command,
args,
options: { cwd: wsRoot },
};
}
function createTaskConfigTaskLongRunning(processType) {
return {
label: '[Task] long running test task (~300s)',
type: processType,
_source: '/source/folder',
_scope: '/source/folder',
options: { cwd: wsRoot },
command: commandLongRunning,
windows: {
command: node_1.FileUri.fsPath(wsRootUri.resolve(commandLongRunningWindows)),
options: { cwd: wsRoot }
},
osx: {
command: node_1.FileUri.fsPath(wsRootUri.resolve(commandLongRunningOsx))
}
};
}
function checkSuccessfulProcessExit(taskInfo, taskWatcher) {
return new Promise((resolve, reject) => {
const toDispose = taskWatcher.onTaskExit((event) => {
if (event.taskId === taskInfo.taskId && event.code === 0) {
toDispose.dispose();
resolve();
}
});
});
}
function getExitStatus(taskInfo, taskWatcher) {
return new Promise((resolve, reject) => {
taskWatcher.onTaskExit((event) => {
if (event.taskId === taskInfo.taskId) {
if (typeof event.signal === 'string') {
resolve(event.signal);
}
else if (typeof event.code === 'number') {
resolve(event.code);
}
else {
reject(new Error('no code nor signal'));
}
}
});
});
}
//# sourceMappingURL=task-server.slow-spec.js.map
;