@theia/task 1.53.0-next.4 → 1.53.0-next.55

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
Files changed (60) hide show
  1. package/README.md +193 -193
  2. package/lib/browser/task-schema-updater.js +1 -1
  3. package/package.json +13 -13
  4. package/src/browser/index.ts +22 -22
  5. package/src/browser/process/process-task-contribution.ts +31 -31
  6. package/src/browser/process/process-task-frontend-module.ts +27 -27
  7. package/src/browser/process/process-task-resolver.ts +89 -89
  8. package/src/browser/provided-task-configurations.spec.ts +46 -46
  9. package/src/browser/provided-task-configurations.ts +213 -213
  10. package/src/browser/quick-open-task.ts +831 -831
  11. package/src/browser/style/index.css +19 -19
  12. package/src/browser/task-configuration-manager.ts +256 -256
  13. package/src/browser/task-configuration-model.ts +101 -101
  14. package/src/browser/task-configurations.ts +508 -508
  15. package/src/browser/task-contribution.ts +266 -266
  16. package/src/browser/task-definition-registry.spec.ts +203 -203
  17. package/src/browser/task-definition-registry.ts +131 -131
  18. package/src/browser/task-frontend-contribution.ts +402 -402
  19. package/src/browser/task-frontend-module.ts +86 -86
  20. package/src/browser/task-name-resolver.ts +55 -55
  21. package/src/browser/task-node.ts +37 -37
  22. package/src/browser/task-preferences.ts +40 -40
  23. package/src/browser/task-problem-matcher-registry.ts +308 -308
  24. package/src/browser/task-problem-pattern-registry.ts +196 -196
  25. package/src/browser/task-schema-updater.ts +701 -701
  26. package/src/browser/task-service.ts +1164 -1164
  27. package/src/browser/task-source-resolver.ts +36 -36
  28. package/src/browser/task-templates.ts +168 -168
  29. package/src/browser/task-terminal-widget-manager.ts +224 -224
  30. package/src/browser/tasks-monaco-contribution.ts +27 -27
  31. package/src/common/index.ts +20 -20
  32. package/src/common/problem-matcher-protocol.ts +234 -234
  33. package/src/common/process/task-protocol.ts +97 -97
  34. package/src/common/task-common-module.ts +34 -34
  35. package/src/common/task-protocol.ts +317 -317
  36. package/src/common/task-util.ts +43 -43
  37. package/src/common/task-watcher.ts +78 -78
  38. package/src/node/custom/custom-task-runner-backend-module.ts +37 -37
  39. package/src/node/custom/custom-task-runner-contribution.ts +30 -30
  40. package/src/node/custom/custom-task-runner.ts +60 -60
  41. package/src/node/custom/custom-task.ts +73 -73
  42. package/src/node/index.ts +19 -19
  43. package/src/node/process/process-task-runner-backend-module.ts +37 -37
  44. package/src/node/process/process-task-runner-contribution.ts +31 -31
  45. package/src/node/process/process-task-runner.ts +371 -371
  46. package/src/node/process/process-task.spec.ts +30 -30
  47. package/src/node/process/process-task.ts +144 -144
  48. package/src/node/task-abstract-line-matcher.ts +312 -312
  49. package/src/node/task-backend-application-contribution.ts +36 -36
  50. package/src/node/task-backend-module.ts +57 -57
  51. package/src/node/task-line-matchers.ts +127 -127
  52. package/src/node/task-manager.ts +129 -129
  53. package/src/node/task-problem-collector.spec.ts +338 -338
  54. package/src/node/task-problem-collector.ts +62 -62
  55. package/src/node/task-runner-protocol.ts +33 -33
  56. package/src/node/task-runner.ts +96 -96
  57. package/src/node/task-server.slow-spec.ts +444 -444
  58. package/src/node/task-server.ts +263 -263
  59. package/src/node/task.ts +103 -103
  60. package/src/node/test/task-test-container.ts +63 -63
@@ -1,444 +1,444 @@
1
- // *****************************************************************************
2
- // Copyright (C) 2017-2019 Ericsson and others.
3
- //
4
- // This program and the accompanying materials are made available under the
5
- // terms of the Eclipse Public License v. 2.0 which is available at
6
- // http://www.eclipse.org/legal/epl-2.0.
7
- //
8
- // This Source Code may also be made available under the following Secondary
9
- // Licenses when the conditions for such availability set forth in the Eclipse
10
- // Public License v. 2.0 are satisfied: GNU General Public License, version 2
11
- // with the GNU Classpath Exception which is available at
12
- // https://www.gnu.org/software/classpath/license.html.
13
- //
14
- // SPDX-License-Identifier: EPL-2.0 OR GPL-2.0-only WITH Classpath-exception-2.0
15
- // *****************************************************************************
16
-
17
- // tslint:disable-next-line:no-implicit-dependencies
18
- import 'reflect-metadata';
19
- import { createTaskTestContainer } from './test/task-test-container';
20
- import { BackendApplication } from '@theia/core/lib/node/backend-application';
21
- import { TaskExitedEvent, TaskInfo, TaskServer, TaskWatcher, TaskConfiguration } from '../common';
22
- import { ProcessType, ProcessTaskConfiguration } from '../common/process/task-protocol';
23
- import * as http from 'http';
24
- import * as https from 'https';
25
- import { isWindows, isOSX } from '@theia/core/lib/common/os';
26
- import { FileUri } from '@theia/core/lib/node';
27
- import { terminalsPath } from '@theia/terminal/lib/common/terminal-protocol';
28
- import { expectThrowsAsync } from '@theia/core/lib/common/test/expect';
29
- import { TestWebSocketChannelSetup } from '@theia/core/lib/node/messaging/test/test-web-socket-channel';
30
- import { expect } from 'chai';
31
- import URI from '@theia/core/lib/common/uri';
32
- import { StringBufferingStream } from '@theia/terminal/lib/node/buffering-stream';
33
-
34
- // test scripts that we bundle with tasks
35
- const commandShortRunning = './task';
36
- const commandShortRunningOsx = './task-osx';
37
- const commandShortRunningWindows = '.\\task.bat';
38
-
39
- const commandLongRunning = './task-long-running';
40
- const commandLongRunningOsx = './task-long-running-osx';
41
- const commandLongRunningWindows = '.\\task-long-running.bat';
42
-
43
- const bogusCommand = 'thisisnotavalidcommand';
44
-
45
- const commandUnixNoop = 'true';
46
- const commandWindowsNoop = 'rundll32.exe';
47
-
48
- /** Expects argv to be ['a', 'b', 'c'] */
49
- const script0 = './test-arguments-0.js';
50
- /** Expects argv to be ['a', 'b', ' c'] */
51
- const script1 = './test-arguments-1.js';
52
- /** Expects argv to be ['a', 'b', 'c"'] */
53
- const script2 = './test-arguments-2.js';
54
-
55
- // we use test-resources subfolder ('<theia>/packages/task/test-resources/'),
56
- // as workspace root, for these tests
57
- const wsRootUri: URI = FileUri.create(__dirname).resolve('../../test-resources');
58
- const wsRoot: string = FileUri.fsPath(wsRootUri);
59
-
60
- describe('Task server / back-end', function (): void {
61
- this.timeout(10000);
62
-
63
- let backend: BackendApplication;
64
- let server: http.Server | https.Server;
65
- let taskServer: TaskServer;
66
- let taskWatcher: TaskWatcher;
67
-
68
- beforeEach(async () => {
69
- delete process.env['THEIA_TASK_TEST_DEBUG'];
70
- const testContainer = createTaskTestContainer();
71
- taskWatcher = testContainer.get(TaskWatcher);
72
- taskServer = testContainer.get(TaskServer);
73
- taskServer.setClient(taskWatcher.getTaskClient());
74
- backend = testContainer.get(BackendApplication);
75
- server = await backend.start(3000, 'localhost');
76
- });
77
-
78
- afterEach(async () => {
79
- const _backend = backend;
80
- const _server = server;
81
- backend = undefined!;
82
- taskServer = undefined!;
83
- taskWatcher = undefined!;
84
- server = undefined!;
85
- _backend['onStop']();
86
- _server.close();
87
- });
88
-
89
- it('task running in terminal - expected data is received from the terminal ws server', async function (): Promise<void> {
90
- const someString = 'someSingleWordString';
91
-
92
- // create task using terminal process
93
- const command = isWindows ? commandShortRunningWindows : (isOSX ? commandShortRunningOsx : commandShortRunning);
94
- const taskInfo: TaskInfo = await taskServer.run(createProcessTaskConfig('shell', `${command} ${someString}`), wsRoot);
95
- const terminalId = taskInfo.terminalId;
96
-
97
- const messagesToWaitFor = 10;
98
- const messages: string[] = [];
99
-
100
- // check output of task on terminal is what we expect
101
- const expected = `${isOSX ? 'tasking osx' : 'tasking'}... ${someString}`;
102
-
103
- // hook-up to terminal's ws and confirm that it outputs expected tasks' output
104
- await new Promise<void>((resolve, reject) => {
105
- const setup = new TestWebSocketChannelSetup({ server, path: `${terminalsPath}/${terminalId}` });
106
- const stringBuffer = new StringBufferingStream();
107
- setup.connectionProvider.listen(`${terminalsPath}/${terminalId}`, (path, channel) => {
108
- channel.onMessage(e => stringBuffer.push(e().readString()));
109
- channel.onError(reject);
110
- channel.onClose(() => reject(new Error('Channel has been closed')));
111
- }, false);
112
- stringBuffer.onData(currentMessage => {
113
- // Instead of waiting for one message from the terminal, we wait for several ones as the very first message can be something unexpected.
114
- // For instance: `nvm is not compatible with the \"PREFIX\" environment variable: currently set to \"/usr/local\"\r\n`
115
- messages.unshift(currentMessage);
116
- if (currentMessage.includes(expected)) {
117
- resolve();
118
- } else if (messages.length >= messagesToWaitFor) {
119
- reject(new Error(`expected sub-string not found in terminal output. Expected: "${expected}" vs Actual messages: ${JSON.stringify(messages)}`));
120
- }
121
- });
122
- });
123
- });
124
-
125
- it('task using raw process - task server success response shall not contain a terminal id', async function (): Promise<void> {
126
- const someString = 'someSingleWordString';
127
- const command = isWindows ? commandShortRunningWindows : (isOSX ? commandShortRunningOsx : commandShortRunning);
128
- const executable = FileUri.fsPath(wsRootUri.resolve(command));
129
-
130
- // create task using raw process
131
- const taskInfo: TaskInfo = await taskServer.run(createProcessTaskConfig('process', executable, [someString]), wsRoot);
132
-
133
- await new Promise<void>((resolve, reject) => {
134
- const toDispose = taskWatcher.onTaskExit((event: TaskExitedEvent) => {
135
- if (event.taskId === taskInfo.taskId && event.code === 0) {
136
- if (typeof taskInfo.terminalId === 'number') {
137
- resolve();
138
- } else {
139
- reject(new Error(`terminal id was expected to be a number, got: ${typeof taskInfo.terminalId}`));
140
- }
141
- toDispose.dispose();
142
- }
143
- });
144
- });
145
- });
146
-
147
- it('task is executed successfully with cwd as a file URI', async function (): Promise<void> {
148
- const command = isWindows ? commandShortRunningWindows : (isOSX ? commandShortRunningOsx : commandShortRunning);
149
- const config = createProcessTaskConfig('shell', command, undefined, FileUri.create(wsRoot).toString());
150
- const taskInfo: TaskInfo = await taskServer.run(config, wsRoot);
151
- await checkSuccessfulProcessExit(taskInfo, taskWatcher);
152
- });
153
-
154
- it('task is executed successfully using terminal process', async function (): Promise<void> {
155
- const command = isWindows ? commandShortRunningWindows : (isOSX ? commandShortRunningOsx : commandShortRunning);
156
- const taskInfo: TaskInfo = await taskServer.run(createProcessTaskConfig('shell', command, undefined), wsRoot);
157
- await checkSuccessfulProcessExit(taskInfo, taskWatcher);
158
- });
159
-
160
- it('task is executed successfully using raw process', async function (): Promise<void> {
161
- const command = isWindows ? commandShortRunningWindows : (isOSX ? commandShortRunningOsx : commandShortRunning);
162
- const executable = FileUri.fsPath(wsRootUri.resolve(command));
163
- const taskInfo: TaskInfo = await taskServer.run(createProcessTaskConfig('process', executable, []));
164
- await checkSuccessfulProcessExit(taskInfo, taskWatcher);
165
- });
166
-
167
- it('task without a specific runner is executed successfully using as a process', async function (): Promise<void> {
168
- const command = isWindows ? commandWindowsNoop : commandUnixNoop;
169
-
170
- // there's no runner registered for the 'npm' task type
171
- const taskConfig: TaskConfiguration = createTaskConfig('npm', command, []);
172
- const taskInfo: TaskInfo = await taskServer.run(taskConfig, wsRoot);
173
- await checkSuccessfulProcessExit(taskInfo, taskWatcher);
174
- });
175
-
176
- it('task can successfully execute command found in system path using a terminal process', async function (): Promise<void> {
177
- const command = isWindows ? commandWindowsNoop : commandUnixNoop;
178
- const opts: TaskConfiguration = createProcessTaskConfig('shell', command, []);
179
- const taskInfo: TaskInfo = await taskServer.run(opts, wsRoot);
180
- await checkSuccessfulProcessExit(taskInfo, taskWatcher);
181
- });
182
-
183
- it('task can successfully execute command found in system path using a raw process', async function (): Promise<void> {
184
- const command = isWindows ? commandWindowsNoop : commandUnixNoop;
185
- const taskInfo: TaskInfo = await taskServer.run(createProcessTaskConfig('process', command, []), wsRoot);
186
- await checkSuccessfulProcessExit(taskInfo, taskWatcher);
187
- });
188
-
189
- it('task using type "shell" can be killed', async function (): Promise<void> {
190
- const taskInfo: TaskInfo = await taskServer.run(createTaskConfigTaskLongRunning('shell'), wsRoot);
191
-
192
- const exitStatusPromise = getExitStatus(taskInfo, taskWatcher);
193
- taskServer.kill(taskInfo.taskId);
194
- const exitStatus = await exitStatusPromise;
195
-
196
- // node-pty reports different things on Linux/macOS vs Windows when
197
- // killing a process. This is not ideal, but that's how things are
198
- // currently. Ideally, its behavior should be aligned as much as
199
- // possible on what node's child_process module does.
200
- if (isWindows) {
201
- // On Windows, node-pty just reports an exit code of 0.
202
- expect(exitStatus).equals(0);
203
- } else {
204
- // On Linux/macOS, node-pty sends SIGHUP by default, for some reason.
205
- expect(exitStatus).equals('SIGHUP');
206
- }
207
- });
208
-
209
- it('task using type "process" can be killed', async function (): Promise<void> {
210
- const taskInfo: TaskInfo = await taskServer.run(createTaskConfigTaskLongRunning('process'), wsRoot);
211
-
212
- const exitStatusPromise = getExitStatus(taskInfo, taskWatcher);
213
- taskServer.kill(taskInfo.taskId);
214
- const exitStatus = await exitStatusPromise;
215
-
216
- // node-pty reports different things on Linux/macOS vs Windows when
217
- // killing a process. This is not ideal, but that's how things are
218
- // currently. Ideally, its behavior should be aligned as much as
219
- // possible on what node's child_process module does.
220
- if (isWindows) {
221
- // On Windows, node-pty just reports an exit code of 0.
222
- expect(exitStatus).equals(0);
223
- } else {
224
- // On Linux/macOS, node-pty sends SIGHUP by default, for some reason.
225
- expect(exitStatus).equals('SIGHUP');
226
- }
227
- });
228
-
229
- /**
230
- * TODO: Figure out how to debug a process that correctly starts but exits with a return code > 0
231
- */
232
- it('task using terminal process can handle command that does not exist', async function (): Promise<void> {
233
- const taskInfo: TaskInfo = await taskServer.run(createProcessTaskConfig2('shell', bogusCommand, []), wsRoot);
234
- const code = await new Promise<number>((resolve, reject) => {
235
- taskWatcher.onTaskExit((event: TaskExitedEvent) => {
236
- if (event.taskId !== taskInfo.taskId || event.code === undefined) {
237
- reject(new Error(JSON.stringify(event)));
238
- } else {
239
- resolve(event.code);
240
- }
241
- });
242
- });
243
- // node-pty reports different things on Linux/macOS vs Windows when
244
- // killing a process. This is not ideal, but that's how things are
245
- // currently. Ideally, its behavior should be aligned as much as
246
- // possible on what node's child_process module does.
247
- if (isWindows) {
248
- expect(code).equals(1);
249
- } else {
250
- expect(code).equals(127);
251
- }
252
- });
253
-
254
- it('task using raw process can handle command that does not exist', async function (): Promise<void> {
255
- const p = taskServer.run(createProcessTaskConfig2('process', bogusCommand, []), wsRoot);
256
- await expectThrowsAsync(p, 'ENOENT');
257
- });
258
-
259
- it('getTasks(ctx) returns tasks according to created context', async function (): Promise<void> {
260
- const context1 = 'aContext';
261
- const context2 = 'anotherContext';
262
-
263
- // create some tasks: 4 for context1, 2 for context2
264
- const task1 = await taskServer.run(createTaskConfigTaskLongRunning('shell'), context1);
265
- const task2 = await taskServer.run(createTaskConfigTaskLongRunning('process'), context2);
266
- const task3 = await taskServer.run(createTaskConfigTaskLongRunning('shell'), context1);
267
- const task4 = await taskServer.run(createTaskConfigTaskLongRunning('process'), context2);
268
- const task5 = await taskServer.run(createTaskConfigTaskLongRunning('shell'), context1);
269
- const task6 = await taskServer.run(createTaskConfigTaskLongRunning('process'), context1);
270
-
271
- const runningTasksCtx1 = await taskServer.getTasks(context1); // should return 4 tasks
272
- const runningTasksCtx2 = await taskServer.getTasks(context2); // should return 2 tasks
273
- const runningTasksAll = await taskServer.getTasks(); // should return 6 tasks
274
-
275
- if (runningTasksCtx1.length !== 4) {
276
- throw new Error(`Error: unexpected number of running tasks for context 1: expected: 4, actual: ${runningTasksCtx1.length}`);
277
- } if (runningTasksCtx2.length !== 2) {
278
- throw new Error(`Error: unexpected number of running tasks for context 2: expected: 2, actual: ${runningTasksCtx1.length}`);
279
- } if (runningTasksAll.length !== 6) {
280
- throw new Error(`Error: unexpected total number of running tasks for all contexts: expected: 6, actual: ${runningTasksCtx1.length}`);
281
- }
282
-
283
- // cleanup
284
- await taskServer.kill(task1.taskId);
285
- await taskServer.kill(task2.taskId);
286
- await taskServer.kill(task3.taskId);
287
- await taskServer.kill(task4.taskId);
288
- await taskServer.kill(task5.taskId);
289
- await taskServer.kill(task6.taskId);
290
- });
291
-
292
- it('creating and killing a bunch of tasks works as expected', async function (): Promise<void> {
293
- // const command = isWindows ? command_absolute_path_long_running_windows : command_absolute_path_long_running;
294
- const numTasks = 20;
295
- const taskInfo: TaskInfo[] = [];
296
-
297
- // create a mix of terminal and raw processes
298
- for (let i = 0; i < numTasks; i++) {
299
- if (i % 2 === 0) {
300
- taskInfo.push(await taskServer.run(createTaskConfigTaskLongRunning('shell')));
301
- } else {
302
- taskInfo.push(await taskServer.run(createTaskConfigTaskLongRunning('process')));
303
- }
304
- }
305
-
306
- const numRunningTasksAfterCreated = await taskServer.getTasks();
307
-
308
- for (let i = 0; i < taskInfo.length; i++) {
309
- await taskServer.kill(taskInfo[i].taskId);
310
- }
311
- const numRunningTasksAfterKilled = await taskServer.getTasks();
312
-
313
- if (numRunningTasksAfterCreated.length !== numTasks) {
314
- throw new Error(`Error: unexpected number of running tasks: expected: ${numTasks}, actual: ${numRunningTasksAfterCreated.length}`);
315
- } if (numRunningTasksAfterKilled.length !== 0) {
316
- throw new Error(`Error: remaining running tasks, after all killed: expected: 0, actual: ${numRunningTasksAfterKilled.length}`);
317
- }
318
-
319
- });
320
-
321
- it('shell task should execute the command as a whole if not arguments are specified', async function (): Promise<void> {
322
- const taskInfo = await taskServer.run(createProcessTaskConfig2('shell', `node ${script0} debug-hint:0a a b c`));
323
- const exitStatus = await getExitStatus(taskInfo, taskWatcher);
324
- expect(exitStatus).eq(0);
325
- });
326
-
327
- it('shell task should fail if user defines a full command line and arguments', async function (): Promise<void> {
328
- const taskInfo = await taskServer.run(createProcessTaskConfig2('shell', `node ${script0} debug-hint:0b a b c`, []));
329
- const exitStatus = await getExitStatus(taskInfo, taskWatcher);
330
- expect(exitStatus).not.eq(0);
331
- });
332
-
333
- it('shell task should be able to exec using simple arguments', async function (): Promise<void> {
334
- const taskInfo = await taskServer.run(createProcessTaskConfig2('shell', 'node', [script0, 'debug-hint:0c', 'a', 'b', 'c']));
335
- const exitStatus = await getExitStatus(taskInfo, taskWatcher);
336
- expect(exitStatus).eq(0);
337
- });
338
-
339
- it('shell task should be able to run using arguments containing whitespace', async function (): Promise<void> {
340
- const taskInfo = await taskServer.run(createProcessTaskConfig2('shell', 'node', [script1, 'debug-hint:1', 'a', 'b', ' c']));
341
- const exitStatus = await getExitStatus(taskInfo, taskWatcher);
342
- expect(exitStatus).eq(0);
343
- });
344
-
345
- it('shell task will fail if user specify problematic arguments', async function (): Promise<void> {
346
- const taskInfo = await taskServer.run(createProcessTaskConfig2('shell', 'node', [script2, 'debug-hint:2a', 'a', 'b', 'c"']));
347
- const exitStatus = await getExitStatus(taskInfo, taskWatcher);
348
- expect(exitStatus).not.eq(0);
349
- });
350
-
351
- it('shell task should be able to run using arguments specifying which quoting method to use', async function (): Promise<void> {
352
- const taskInfo = await taskServer.run(createProcessTaskConfig2('shell', 'node', [script2, 'debug-hint:2b', 'a', 'b', { value: 'c"', quoting: 'escape' }]));
353
- const exitStatus = await getExitStatus(taskInfo, taskWatcher);
354
- expect(exitStatus).eq(0);
355
- });
356
-
357
- it('shell task should be able to run using arguments with forbidden characters but no whitespace', async function (): Promise<void> {
358
- const taskInfo = await taskServer.run(createProcessTaskConfig2('shell', 'node', ['-e', 'setTimeout(console.log,1000,1+2)']));
359
- const exitStatus = await getExitStatus(taskInfo, taskWatcher);
360
- expect(exitStatus).eq(0);
361
- });
362
-
363
- });
364
-
365
- function createTaskConfig(taskType: string, command: string, args: string[]): TaskConfiguration {
366
- const options: TaskConfiguration = {
367
- label: 'test task',
368
- type: taskType,
369
- _source: '/source/folder',
370
- _scope: '/source/folder',
371
- command,
372
- args,
373
- options: { cwd: wsRoot }
374
- };
375
- return options;
376
- }
377
-
378
- function createProcessTaskConfig(processType: ProcessType, command: string, args?: string[], cwd: string = wsRoot): TaskConfiguration {
379
- return <ProcessTaskConfiguration>{
380
- label: 'test task',
381
- type: processType,
382
- _source: '/source/folder',
383
- _scope: '/source/folder',
384
- command,
385
- args,
386
- options: { cwd },
387
- };
388
- }
389
-
390
- // eslint-disable-next-line @typescript-eslint/no-explicit-any
391
- function createProcessTaskConfig2(processType: ProcessType, command: string, args?: any[]): TaskConfiguration {
392
- return <ProcessTaskConfiguration>{
393
- label: 'test task',
394
- type: processType,
395
- command,
396
- args,
397
- options: { cwd: wsRoot },
398
- };
399
- }
400
-
401
- function createTaskConfigTaskLongRunning(processType: ProcessType): TaskConfiguration {
402
- return <ProcessTaskConfiguration>{
403
- label: '[Task] long running test task (~300s)',
404
- type: processType,
405
- _source: '/source/folder',
406
- _scope: '/source/folder',
407
- options: { cwd: wsRoot },
408
- command: commandLongRunning,
409
- windows: {
410
- command: FileUri.fsPath(wsRootUri.resolve(commandLongRunningWindows)),
411
- options: { cwd: wsRoot }
412
- },
413
- osx: {
414
- command: FileUri.fsPath(wsRootUri.resolve(commandLongRunningOsx))
415
- }
416
- };
417
- }
418
-
419
- function checkSuccessfulProcessExit(taskInfo: TaskInfo, taskWatcher: TaskWatcher): Promise<void> {
420
- return new Promise<void>((resolve, reject) => {
421
- const toDispose = taskWatcher.onTaskExit((event: TaskExitedEvent) => {
422
- if (event.taskId === taskInfo.taskId && event.code === 0) {
423
- toDispose.dispose();
424
- resolve();
425
- }
426
- });
427
- });
428
- }
429
-
430
- function getExitStatus(taskInfo: TaskInfo, taskWatcher: TaskWatcher): Promise<string | number> {
431
- return new Promise<string | number>((resolve, reject) => {
432
- taskWatcher.onTaskExit((event: TaskExitedEvent) => {
433
- if (event.taskId === taskInfo.taskId) {
434
- if (typeof event.signal === 'string') {
435
- resolve(event.signal);
436
- } else if (typeof event.code === 'number') {
437
- resolve(event.code);
438
- } else {
439
- reject(new Error('no code nor signal'));
440
- }
441
- }
442
- });
443
- });
444
- }
1
+ // *****************************************************************************
2
+ // Copyright (C) 2017-2019 Ericsson and others.
3
+ //
4
+ // This program and the accompanying materials are made available under the
5
+ // terms of the Eclipse Public License v. 2.0 which is available at
6
+ // http://www.eclipse.org/legal/epl-2.0.
7
+ //
8
+ // This Source Code may also be made available under the following Secondary
9
+ // Licenses when the conditions for such availability set forth in the Eclipse
10
+ // Public License v. 2.0 are satisfied: GNU General Public License, version 2
11
+ // with the GNU Classpath Exception which is available at
12
+ // https://www.gnu.org/software/classpath/license.html.
13
+ //
14
+ // SPDX-License-Identifier: EPL-2.0 OR GPL-2.0-only WITH Classpath-exception-2.0
15
+ // *****************************************************************************
16
+
17
+ // tslint:disable-next-line:no-implicit-dependencies
18
+ import 'reflect-metadata';
19
+ import { createTaskTestContainer } from './test/task-test-container';
20
+ import { BackendApplication } from '@theia/core/lib/node/backend-application';
21
+ import { TaskExitedEvent, TaskInfo, TaskServer, TaskWatcher, TaskConfiguration } from '../common';
22
+ import { ProcessType, ProcessTaskConfiguration } from '../common/process/task-protocol';
23
+ import * as http from 'http';
24
+ import * as https from 'https';
25
+ import { isWindows, isOSX } from '@theia/core/lib/common/os';
26
+ import { FileUri } from '@theia/core/lib/node';
27
+ import { terminalsPath } from '@theia/terminal/lib/common/terminal-protocol';
28
+ import { expectThrowsAsync } from '@theia/core/lib/common/test/expect';
29
+ import { TestWebSocketChannelSetup } from '@theia/core/lib/node/messaging/test/test-web-socket-channel';
30
+ import { expect } from 'chai';
31
+ import URI from '@theia/core/lib/common/uri';
32
+ import { StringBufferingStream } from '@theia/terminal/lib/node/buffering-stream';
33
+
34
+ // test scripts that we bundle with tasks
35
+ const commandShortRunning = './task';
36
+ const commandShortRunningOsx = './task-osx';
37
+ const commandShortRunningWindows = '.\\task.bat';
38
+
39
+ const commandLongRunning = './task-long-running';
40
+ const commandLongRunningOsx = './task-long-running-osx';
41
+ const commandLongRunningWindows = '.\\task-long-running.bat';
42
+
43
+ const bogusCommand = 'thisisnotavalidcommand';
44
+
45
+ const commandUnixNoop = 'true';
46
+ const commandWindowsNoop = 'rundll32.exe';
47
+
48
+ /** Expects argv to be ['a', 'b', 'c'] */
49
+ const script0 = './test-arguments-0.js';
50
+ /** Expects argv to be ['a', 'b', ' c'] */
51
+ const script1 = './test-arguments-1.js';
52
+ /** Expects argv to be ['a', 'b', 'c"'] */
53
+ const script2 = './test-arguments-2.js';
54
+
55
+ // we use test-resources subfolder ('<theia>/packages/task/test-resources/'),
56
+ // as workspace root, for these tests
57
+ const wsRootUri: URI = FileUri.create(__dirname).resolve('../../test-resources');
58
+ const wsRoot: string = FileUri.fsPath(wsRootUri);
59
+
60
+ describe('Task server / back-end', function (): void {
61
+ this.timeout(10000);
62
+
63
+ let backend: BackendApplication;
64
+ let server: http.Server | https.Server;
65
+ let taskServer: TaskServer;
66
+ let taskWatcher: TaskWatcher;
67
+
68
+ beforeEach(async () => {
69
+ delete process.env['THEIA_TASK_TEST_DEBUG'];
70
+ const testContainer = createTaskTestContainer();
71
+ taskWatcher = testContainer.get(TaskWatcher);
72
+ taskServer = testContainer.get(TaskServer);
73
+ taskServer.setClient(taskWatcher.getTaskClient());
74
+ backend = testContainer.get(BackendApplication);
75
+ server = await backend.start(3000, 'localhost');
76
+ });
77
+
78
+ afterEach(async () => {
79
+ const _backend = backend;
80
+ const _server = server;
81
+ backend = undefined!;
82
+ taskServer = undefined!;
83
+ taskWatcher = undefined!;
84
+ server = undefined!;
85
+ _backend['onStop']();
86
+ _server.close();
87
+ });
88
+
89
+ it('task running in terminal - expected data is received from the terminal ws server', async function (): Promise<void> {
90
+ const someString = 'someSingleWordString';
91
+
92
+ // create task using terminal process
93
+ const command = isWindows ? commandShortRunningWindows : (isOSX ? commandShortRunningOsx : commandShortRunning);
94
+ const taskInfo: TaskInfo = await taskServer.run(createProcessTaskConfig('shell', `${command} ${someString}`), wsRoot);
95
+ const terminalId = taskInfo.terminalId;
96
+
97
+ const messagesToWaitFor = 10;
98
+ const messages: string[] = [];
99
+
100
+ // check output of task on terminal is what we expect
101
+ const expected = `${isOSX ? 'tasking osx' : 'tasking'}... ${someString}`;
102
+
103
+ // hook-up to terminal's ws and confirm that it outputs expected tasks' output
104
+ await new Promise<void>((resolve, reject) => {
105
+ const setup = new TestWebSocketChannelSetup({ server, path: `${terminalsPath}/${terminalId}` });
106
+ const stringBuffer = new StringBufferingStream();
107
+ setup.connectionProvider.listen(`${terminalsPath}/${terminalId}`, (path, channel) => {
108
+ channel.onMessage(e => stringBuffer.push(e().readString()));
109
+ channel.onError(reject);
110
+ channel.onClose(() => reject(new Error('Channel has been closed')));
111
+ }, false);
112
+ stringBuffer.onData(currentMessage => {
113
+ // Instead of waiting for one message from the terminal, we wait for several ones as the very first message can be something unexpected.
114
+ // For instance: `nvm is not compatible with the \"PREFIX\" environment variable: currently set to \"/usr/local\"\r\n`
115
+ messages.unshift(currentMessage);
116
+ if (currentMessage.includes(expected)) {
117
+ resolve();
118
+ } else if (messages.length >= messagesToWaitFor) {
119
+ reject(new Error(`expected sub-string not found in terminal output. Expected: "${expected}" vs Actual messages: ${JSON.stringify(messages)}`));
120
+ }
121
+ });
122
+ });
123
+ });
124
+
125
+ it('task using raw process - task server success response shall not contain a terminal id', async function (): Promise<void> {
126
+ const someString = 'someSingleWordString';
127
+ const command = isWindows ? commandShortRunningWindows : (isOSX ? commandShortRunningOsx : commandShortRunning);
128
+ const executable = FileUri.fsPath(wsRootUri.resolve(command));
129
+
130
+ // create task using raw process
131
+ const taskInfo: TaskInfo = await taskServer.run(createProcessTaskConfig('process', executable, [someString]), wsRoot);
132
+
133
+ await new Promise<void>((resolve, reject) => {
134
+ const toDispose = taskWatcher.onTaskExit((event: TaskExitedEvent) => {
135
+ if (event.taskId === taskInfo.taskId && event.code === 0) {
136
+ if (typeof taskInfo.terminalId === 'number') {
137
+ resolve();
138
+ } else {
139
+ reject(new Error(`terminal id was expected to be a number, got: ${typeof taskInfo.terminalId}`));
140
+ }
141
+ toDispose.dispose();
142
+ }
143
+ });
144
+ });
145
+ });
146
+
147
+ it('task is executed successfully with cwd as a file URI', async function (): Promise<void> {
148
+ const command = isWindows ? commandShortRunningWindows : (isOSX ? commandShortRunningOsx : commandShortRunning);
149
+ const config = createProcessTaskConfig('shell', command, undefined, FileUri.create(wsRoot).toString());
150
+ const taskInfo: TaskInfo = await taskServer.run(config, wsRoot);
151
+ await checkSuccessfulProcessExit(taskInfo, taskWatcher);
152
+ });
153
+
154
+ it('task is executed successfully using terminal process', async function (): Promise<void> {
155
+ const command = isWindows ? commandShortRunningWindows : (isOSX ? commandShortRunningOsx : commandShortRunning);
156
+ const taskInfo: TaskInfo = await taskServer.run(createProcessTaskConfig('shell', command, undefined), wsRoot);
157
+ await checkSuccessfulProcessExit(taskInfo, taskWatcher);
158
+ });
159
+
160
+ it('task is executed successfully using raw process', async function (): Promise<void> {
161
+ const command = isWindows ? commandShortRunningWindows : (isOSX ? commandShortRunningOsx : commandShortRunning);
162
+ const executable = FileUri.fsPath(wsRootUri.resolve(command));
163
+ const taskInfo: TaskInfo = await taskServer.run(createProcessTaskConfig('process', executable, []));
164
+ await checkSuccessfulProcessExit(taskInfo, taskWatcher);
165
+ });
166
+
167
+ it('task without a specific runner is executed successfully using as a process', async function (): Promise<void> {
168
+ const command = isWindows ? commandWindowsNoop : commandUnixNoop;
169
+
170
+ // there's no runner registered for the 'npm' task type
171
+ const taskConfig: TaskConfiguration = createTaskConfig('npm', command, []);
172
+ const taskInfo: TaskInfo = await taskServer.run(taskConfig, wsRoot);
173
+ await checkSuccessfulProcessExit(taskInfo, taskWatcher);
174
+ });
175
+
176
+ it('task can successfully execute command found in system path using a terminal process', async function (): Promise<void> {
177
+ const command = isWindows ? commandWindowsNoop : commandUnixNoop;
178
+ const opts: TaskConfiguration = createProcessTaskConfig('shell', command, []);
179
+ const taskInfo: TaskInfo = await taskServer.run(opts, wsRoot);
180
+ await checkSuccessfulProcessExit(taskInfo, taskWatcher);
181
+ });
182
+
183
+ it('task can successfully execute command found in system path using a raw process', async function (): Promise<void> {
184
+ const command = isWindows ? commandWindowsNoop : commandUnixNoop;
185
+ const taskInfo: TaskInfo = await taskServer.run(createProcessTaskConfig('process', command, []), wsRoot);
186
+ await checkSuccessfulProcessExit(taskInfo, taskWatcher);
187
+ });
188
+
189
+ it('task using type "shell" can be killed', async function (): Promise<void> {
190
+ const taskInfo: TaskInfo = await taskServer.run(createTaskConfigTaskLongRunning('shell'), wsRoot);
191
+
192
+ const exitStatusPromise = getExitStatus(taskInfo, taskWatcher);
193
+ taskServer.kill(taskInfo.taskId);
194
+ const exitStatus = await exitStatusPromise;
195
+
196
+ // node-pty reports different things on Linux/macOS vs Windows when
197
+ // killing a process. This is not ideal, but that's how things are
198
+ // currently. Ideally, its behavior should be aligned as much as
199
+ // possible on what node's child_process module does.
200
+ if (isWindows) {
201
+ // On Windows, node-pty just reports an exit code of 0.
202
+ expect(exitStatus).equals(0);
203
+ } else {
204
+ // On Linux/macOS, node-pty sends SIGHUP by default, for some reason.
205
+ expect(exitStatus).equals('SIGHUP');
206
+ }
207
+ });
208
+
209
+ it('task using type "process" can be killed', async function (): Promise<void> {
210
+ const taskInfo: TaskInfo = await taskServer.run(createTaskConfigTaskLongRunning('process'), wsRoot);
211
+
212
+ const exitStatusPromise = getExitStatus(taskInfo, taskWatcher);
213
+ taskServer.kill(taskInfo.taskId);
214
+ const exitStatus = await exitStatusPromise;
215
+
216
+ // node-pty reports different things on Linux/macOS vs Windows when
217
+ // killing a process. This is not ideal, but that's how things are
218
+ // currently. Ideally, its behavior should be aligned as much as
219
+ // possible on what node's child_process module does.
220
+ if (isWindows) {
221
+ // On Windows, node-pty just reports an exit code of 0.
222
+ expect(exitStatus).equals(0);
223
+ } else {
224
+ // On Linux/macOS, node-pty sends SIGHUP by default, for some reason.
225
+ expect(exitStatus).equals('SIGHUP');
226
+ }
227
+ });
228
+
229
+ /**
230
+ * TODO: Figure out how to debug a process that correctly starts but exits with a return code > 0
231
+ */
232
+ it('task using terminal process can handle command that does not exist', async function (): Promise<void> {
233
+ const taskInfo: TaskInfo = await taskServer.run(createProcessTaskConfig2('shell', bogusCommand, []), wsRoot);
234
+ const code = await new Promise<number>((resolve, reject) => {
235
+ taskWatcher.onTaskExit((event: TaskExitedEvent) => {
236
+ if (event.taskId !== taskInfo.taskId || event.code === undefined) {
237
+ reject(new Error(JSON.stringify(event)));
238
+ } else {
239
+ resolve(event.code);
240
+ }
241
+ });
242
+ });
243
+ // node-pty reports different things on Linux/macOS vs Windows when
244
+ // killing a process. This is not ideal, but that's how things are
245
+ // currently. Ideally, its behavior should be aligned as much as
246
+ // possible on what node's child_process module does.
247
+ if (isWindows) {
248
+ expect(code).equals(1);
249
+ } else {
250
+ expect(code).equals(127);
251
+ }
252
+ });
253
+
254
+ it('task using raw process can handle command that does not exist', async function (): Promise<void> {
255
+ const p = taskServer.run(createProcessTaskConfig2('process', bogusCommand, []), wsRoot);
256
+ await expectThrowsAsync(p, 'ENOENT');
257
+ });
258
+
259
+ it('getTasks(ctx) returns tasks according to created context', async function (): Promise<void> {
260
+ const context1 = 'aContext';
261
+ const context2 = 'anotherContext';
262
+
263
+ // create some tasks: 4 for context1, 2 for context2
264
+ const task1 = await taskServer.run(createTaskConfigTaskLongRunning('shell'), context1);
265
+ const task2 = await taskServer.run(createTaskConfigTaskLongRunning('process'), context2);
266
+ const task3 = await taskServer.run(createTaskConfigTaskLongRunning('shell'), context1);
267
+ const task4 = await taskServer.run(createTaskConfigTaskLongRunning('process'), context2);
268
+ const task5 = await taskServer.run(createTaskConfigTaskLongRunning('shell'), context1);
269
+ const task6 = await taskServer.run(createTaskConfigTaskLongRunning('process'), context1);
270
+
271
+ const runningTasksCtx1 = await taskServer.getTasks(context1); // should return 4 tasks
272
+ const runningTasksCtx2 = await taskServer.getTasks(context2); // should return 2 tasks
273
+ const runningTasksAll = await taskServer.getTasks(); // should return 6 tasks
274
+
275
+ if (runningTasksCtx1.length !== 4) {
276
+ throw new Error(`Error: unexpected number of running tasks for context 1: expected: 4, actual: ${runningTasksCtx1.length}`);
277
+ } if (runningTasksCtx2.length !== 2) {
278
+ throw new Error(`Error: unexpected number of running tasks for context 2: expected: 2, actual: ${runningTasksCtx1.length}`);
279
+ } if (runningTasksAll.length !== 6) {
280
+ throw new Error(`Error: unexpected total number of running tasks for all contexts: expected: 6, actual: ${runningTasksCtx1.length}`);
281
+ }
282
+
283
+ // cleanup
284
+ await taskServer.kill(task1.taskId);
285
+ await taskServer.kill(task2.taskId);
286
+ await taskServer.kill(task3.taskId);
287
+ await taskServer.kill(task4.taskId);
288
+ await taskServer.kill(task5.taskId);
289
+ await taskServer.kill(task6.taskId);
290
+ });
291
+
292
+ it('creating and killing a bunch of tasks works as expected', async function (): Promise<void> {
293
+ // const command = isWindows ? command_absolute_path_long_running_windows : command_absolute_path_long_running;
294
+ const numTasks = 20;
295
+ const taskInfo: TaskInfo[] = [];
296
+
297
+ // create a mix of terminal and raw processes
298
+ for (let i = 0; i < numTasks; i++) {
299
+ if (i % 2 === 0) {
300
+ taskInfo.push(await taskServer.run(createTaskConfigTaskLongRunning('shell')));
301
+ } else {
302
+ taskInfo.push(await taskServer.run(createTaskConfigTaskLongRunning('process')));
303
+ }
304
+ }
305
+
306
+ const numRunningTasksAfterCreated = await taskServer.getTasks();
307
+
308
+ for (let i = 0; i < taskInfo.length; i++) {
309
+ await taskServer.kill(taskInfo[i].taskId);
310
+ }
311
+ const numRunningTasksAfterKilled = await taskServer.getTasks();
312
+
313
+ if (numRunningTasksAfterCreated.length !== numTasks) {
314
+ throw new Error(`Error: unexpected number of running tasks: expected: ${numTasks}, actual: ${numRunningTasksAfterCreated.length}`);
315
+ } if (numRunningTasksAfterKilled.length !== 0) {
316
+ throw new Error(`Error: remaining running tasks, after all killed: expected: 0, actual: ${numRunningTasksAfterKilled.length}`);
317
+ }
318
+
319
+ });
320
+
321
+ it('shell task should execute the command as a whole if not arguments are specified', async function (): Promise<void> {
322
+ const taskInfo = await taskServer.run(createProcessTaskConfig2('shell', `node ${script0} debug-hint:0a a b c`));
323
+ const exitStatus = await getExitStatus(taskInfo, taskWatcher);
324
+ expect(exitStatus).eq(0);
325
+ });
326
+
327
+ it('shell task should fail if user defines a full command line and arguments', async function (): Promise<void> {
328
+ const taskInfo = await taskServer.run(createProcessTaskConfig2('shell', `node ${script0} debug-hint:0b a b c`, []));
329
+ const exitStatus = await getExitStatus(taskInfo, taskWatcher);
330
+ expect(exitStatus).not.eq(0);
331
+ });
332
+
333
+ it('shell task should be able to exec using simple arguments', async function (): Promise<void> {
334
+ const taskInfo = await taskServer.run(createProcessTaskConfig2('shell', 'node', [script0, 'debug-hint:0c', 'a', 'b', 'c']));
335
+ const exitStatus = await getExitStatus(taskInfo, taskWatcher);
336
+ expect(exitStatus).eq(0);
337
+ });
338
+
339
+ it('shell task should be able to run using arguments containing whitespace', async function (): Promise<void> {
340
+ const taskInfo = await taskServer.run(createProcessTaskConfig2('shell', 'node', [script1, 'debug-hint:1', 'a', 'b', ' c']));
341
+ const exitStatus = await getExitStatus(taskInfo, taskWatcher);
342
+ expect(exitStatus).eq(0);
343
+ });
344
+
345
+ it('shell task will fail if user specify problematic arguments', async function (): Promise<void> {
346
+ const taskInfo = await taskServer.run(createProcessTaskConfig2('shell', 'node', [script2, 'debug-hint:2a', 'a', 'b', 'c"']));
347
+ const exitStatus = await getExitStatus(taskInfo, taskWatcher);
348
+ expect(exitStatus).not.eq(0);
349
+ });
350
+
351
+ it('shell task should be able to run using arguments specifying which quoting method to use', async function (): Promise<void> {
352
+ const taskInfo = await taskServer.run(createProcessTaskConfig2('shell', 'node', [script2, 'debug-hint:2b', 'a', 'b', { value: 'c"', quoting: 'escape' }]));
353
+ const exitStatus = await getExitStatus(taskInfo, taskWatcher);
354
+ expect(exitStatus).eq(0);
355
+ });
356
+
357
+ it('shell task should be able to run using arguments with forbidden characters but no whitespace', async function (): Promise<void> {
358
+ const taskInfo = await taskServer.run(createProcessTaskConfig2('shell', 'node', ['-e', 'setTimeout(console.log,1000,1+2)']));
359
+ const exitStatus = await getExitStatus(taskInfo, taskWatcher);
360
+ expect(exitStatus).eq(0);
361
+ });
362
+
363
+ });
364
+
365
+ function createTaskConfig(taskType: string, command: string, args: string[]): TaskConfiguration {
366
+ const options: TaskConfiguration = {
367
+ label: 'test task',
368
+ type: taskType,
369
+ _source: '/source/folder',
370
+ _scope: '/source/folder',
371
+ command,
372
+ args,
373
+ options: { cwd: wsRoot }
374
+ };
375
+ return options;
376
+ }
377
+
378
+ function createProcessTaskConfig(processType: ProcessType, command: string, args?: string[], cwd: string = wsRoot): TaskConfiguration {
379
+ return <ProcessTaskConfiguration>{
380
+ label: 'test task',
381
+ type: processType,
382
+ _source: '/source/folder',
383
+ _scope: '/source/folder',
384
+ command,
385
+ args,
386
+ options: { cwd },
387
+ };
388
+ }
389
+
390
+ // eslint-disable-next-line @typescript-eslint/no-explicit-any
391
+ function createProcessTaskConfig2(processType: ProcessType, command: string, args?: any[]): TaskConfiguration {
392
+ return <ProcessTaskConfiguration>{
393
+ label: 'test task',
394
+ type: processType,
395
+ command,
396
+ args,
397
+ options: { cwd: wsRoot },
398
+ };
399
+ }
400
+
401
+ function createTaskConfigTaskLongRunning(processType: ProcessType): TaskConfiguration {
402
+ return <ProcessTaskConfiguration>{
403
+ label: '[Task] long running test task (~300s)',
404
+ type: processType,
405
+ _source: '/source/folder',
406
+ _scope: '/source/folder',
407
+ options: { cwd: wsRoot },
408
+ command: commandLongRunning,
409
+ windows: {
410
+ command: FileUri.fsPath(wsRootUri.resolve(commandLongRunningWindows)),
411
+ options: { cwd: wsRoot }
412
+ },
413
+ osx: {
414
+ command: FileUri.fsPath(wsRootUri.resolve(commandLongRunningOsx))
415
+ }
416
+ };
417
+ }
418
+
419
+ function checkSuccessfulProcessExit(taskInfo: TaskInfo, taskWatcher: TaskWatcher): Promise<void> {
420
+ return new Promise<void>((resolve, reject) => {
421
+ const toDispose = taskWatcher.onTaskExit((event: TaskExitedEvent) => {
422
+ if (event.taskId === taskInfo.taskId && event.code === 0) {
423
+ toDispose.dispose();
424
+ resolve();
425
+ }
426
+ });
427
+ });
428
+ }
429
+
430
+ function getExitStatus(taskInfo: TaskInfo, taskWatcher: TaskWatcher): Promise<string | number> {
431
+ return new Promise<string | number>((resolve, reject) => {
432
+ taskWatcher.onTaskExit((event: TaskExitedEvent) => {
433
+ if (event.taskId === taskInfo.taskId) {
434
+ if (typeof event.signal === 'string') {
435
+ resolve(event.signal);
436
+ } else if (typeof event.code === 'number') {
437
+ resolve(event.code);
438
+ } else {
439
+ reject(new Error('no code nor signal'));
440
+ }
441
+ }
442
+ });
443
+ });
444
+ }