@theia/process 1.48.0 → 1.48.1

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 (69) hide show
  1. package/README.md +30 -30
  2. package/lib/common/process-common-module.d.ts +3 -3
  3. package/lib/common/process-common-module.js +22 -22
  4. package/lib/common/process-manager-types.d.ts +35 -35
  5. package/lib/common/process-manager-types.js +23 -23
  6. package/lib/common/shell-command-builder.d.ts +49 -49
  7. package/lib/common/shell-command-builder.js +169 -169
  8. package/lib/common/shell-command-builder.slow-spec.d.ts +9 -9
  9. package/lib/common/shell-command-builder.slow-spec.js +404 -404
  10. package/lib/common/shell-quoting.d.ts +91 -91
  11. package/lib/common/shell-quoting.js +145 -145
  12. package/lib/common/shell-quoting.spec.d.ts +1 -1
  13. package/lib/common/shell-quoting.spec.js +170 -170
  14. package/lib/node/dev-null-stream.d.ts +17 -17
  15. package/lib/node/dev-null-stream.js +41 -41
  16. package/lib/node/index.d.ts +6 -6
  17. package/lib/node/index.js +24 -24
  18. package/lib/node/multi-ring-buffer.d.ts +68 -68
  19. package/lib/node/multi-ring-buffer.js +299 -299
  20. package/lib/node/multi-ring-buffer.spec.d.ts +1 -1
  21. package/lib/node/multi-ring-buffer.spec.js +422 -422
  22. package/lib/node/process-backend-module.d.ts +3 -3
  23. package/lib/node/process-backend-module.js +56 -56
  24. package/lib/node/process-manager.d.ts +33 -33
  25. package/lib/node/process-manager.js +102 -102
  26. package/lib/node/process.d.ts +95 -95
  27. package/lib/node/process.js +142 -142
  28. package/lib/node/pseudo-pty.d.ts +22 -22
  29. package/lib/node/pseudo-pty.js +38 -38
  30. package/lib/node/raw-process.d.ts +45 -45
  31. package/lib/node/raw-process.js +104 -104
  32. package/lib/node/raw-process.spec.d.ts +1 -1
  33. package/lib/node/raw-process.spec.js +164 -164
  34. package/lib/node/task-terminal-process.d.ts +10 -10
  35. package/lib/node/task-terminal-process.js +42 -42
  36. package/lib/node/terminal-process.d.ts +60 -60
  37. package/lib/node/terminal-process.js +248 -248
  38. package/lib/node/terminal-process.spec.d.ts +1 -1
  39. package/lib/node/terminal-process.spec.js +103 -103
  40. package/lib/node/test/process-test-container.d.ts +2 -2
  41. package/lib/node/test/process-test-container.js +28 -28
  42. package/lib/node/utils.d.ts +16 -16
  43. package/lib/node/utils.js +77 -77
  44. package/package.json +4 -4
  45. package/src/common/process-common-module.ts +22 -22
  46. package/src/common/process-manager-types.ts +58 -58
  47. package/src/common/shell-command-builder.slow-spec.ts +486 -486
  48. package/src/common/shell-command-builder.ts +187 -187
  49. package/src/common/shell-quoting.spec.ts +176 -176
  50. package/src/common/shell-quoting.ts +236 -236
  51. package/src/common/tests/$weird(),file=name.js +1 -1
  52. package/src/common/tests/white space.js +1 -1
  53. package/src/node/dev-null-stream.ts +47 -47
  54. package/src/node/index.ts +22 -22
  55. package/src/node/multi-ring-buffer.spec.ts +486 -486
  56. package/src/node/multi-ring-buffer.ts +348 -348
  57. package/src/node/process-backend-module.ts +67 -67
  58. package/src/node/process-manager.ts +107 -107
  59. package/src/node/process.ts +207 -207
  60. package/src/node/pseudo-pty.ts +54 -54
  61. package/src/node/raw-process.spec.ts +199 -199
  62. package/src/node/raw-process.ts +156 -156
  63. package/src/node/string-argv.d.ts +21 -21
  64. package/src/node/task-terminal-process.ts +41 -41
  65. package/src/node/terminal-process.spec.ts +121 -121
  66. package/src/node/terminal-process.ts +290 -290
  67. package/src/node/test/process-fork-test.js +22 -22
  68. package/src/node/test/process-test-container.ts +27 -27
  69. package/src/node/utils.ts +79 -79
@@ -1,405 +1,405 @@
1
- "use strict";
2
- // *****************************************************************************
3
- // Copyright (C) 2020 Ericsson and others.
4
- //
5
- // This program and the accompanying materials are made available under the
6
- // terms of the Eclipse Public License v. 2.0 which is available at
7
- // http://www.eclipse.org/legal/epl-2.0.
8
- //
9
- // This Source Code may also be made available under the following Secondary
10
- // Licenses when the conditions for such availability set forth in the Eclipse
11
- // Public License v. 2.0 are satisfied: GNU General Public License, version 2
12
- // with the GNU Classpath Exception which is available at
13
- // https://www.gnu.org/software/classpath/license.html.
14
- //
15
- // SPDX-License-Identifier: EPL-2.0 OR GPL-2.0-only WITH Classpath-exception-2.0
16
- // *****************************************************************************
17
- Object.defineProperty(exports, "__esModule", { value: true });
18
- /**
19
- * This test suite assumes that we run in a NodeJS environment!
20
- */
21
- const child_process_1 = require("child_process");
22
- const path_1 = require("path");
23
- const shell_command_builder_1 = require("./shell-command-builder");
24
- const chalk = require("chalk"); // tslint:disable-line:no-implicit-dependencies
25
- const isWindows = process.platform === 'win32';
26
- /**
27
- * Extra debugging info (very verbose).
28
- */
29
- const _debug = Boolean(process.env['THEIA_PROCESS_TEST_DEBUG']);
30
- /**
31
- * On Windows, some shells simply mess up the terminal's output.
32
- * Enable if you still want to test those.
33
- */
34
- const _runWeirdShell = Boolean(process.env['THEIA_PROCESS_TEST_WEIRD_SHELL']) || undefined;
35
- /**
36
- * You might only have issues with a specific shell (`cmd.exe` I am looking at you).
37
- */
38
- const _onlyTestShell = process.env['THEIA_PROCESS_TEST_ONLY'] || undefined;
39
- /**
40
- * Only log if environment variable is set.
41
- */
42
- // eslint-disable-next-line @typescript-eslint/no-explicit-any
43
- function debug(...parts) {
44
- if (_debug) {
45
- console.debug(...parts);
46
- }
47
- }
48
- const testResources = (0, path_1.join)(__dirname, '../../src/common/tests');
49
- const spawnOptions = {
50
- // We do our own quoting, don't rely on the one done by NodeJS:
51
- windowsVerbatimArguments: true,
52
- stdio: ['pipe', 'pipe', 'pipe'],
53
- };
54
- // Formatting options, used with `scanLines` for debugging.
55
- const stdoutFormat = (prefix) => (data) => `${chalk.bold(chalk.yellow(`${prefix} STDOUT:`))} ${chalk.bgYellow(chalk.black(data))}`;
56
- const stderrFormat = (prefix) => (data) => `${chalk.bold(chalk.red(`${prefix} STDERR:`))} ${chalk.bgRed(chalk.white(data))}`;
57
- // Default error scanner
58
- const errorScanner = (handle) => {
59
- if (/^\s*\w+Error:/.test(handle.line) ||
60
- /^\s*Cannot find /.test(handle.line)) {
61
- throw new Error(handle.text);
62
- }
63
- };
64
- // Yarn mangles the PATH and creates some proxy script around node(.exe),
65
- // which messes up our environment, failing the tests.
66
- const hostNodePath = process.env['npm_node_execpath'] ||
67
- process.env['NODE'];
68
- if (!hostNodePath) {
69
- throw new Error('Could not determine the real node path.');
70
- }
71
- const shellCommandBuilder = new shell_command_builder_1.ShellCommandBuilder();
72
- const shellConfigs = [{
73
- name: 'bash',
74
- path: isWindows
75
- ? _runWeirdShell && execShellCommand('where bash.exe')
76
- : execShellCommand('command -v bash'),
77
- nodePath: isWindows && 'node' // Good enough
78
- }, {
79
- name: 'wsl',
80
- path: isWindows
81
- ? _runWeirdShell && execShellCommand('where wsl.exe')
82
- : undefined,
83
- nodePath: isWindows && 'node' // Good enough
84
- }, {
85
- name: 'cmd',
86
- path: isWindows
87
- ? execShellCommand('where cmd.exe')
88
- : undefined,
89
- }, {
90
- name: 'powershell',
91
- path: execShellCommand(isWindows
92
- ? 'where powershell'
93
- : 'command -v pwsh'),
94
- }];
95
- /* eslint-disable max-len */
96
- // 18d/12m/19y - Ubuntu 16.04:
97
- // Powershell sometimes fails when running as part of an npm lifecycle script.
98
- // See following error:
99
- //
100
- //
101
- // FailFast:
102
- // The type initializer for 'Microsoft.PowerShell.ApplicationInsightsTelemetry' threw an exception.
103
- //
104
- // at System.Environment.FailFast(System.String, System.Exception)
105
- // at System.Environment.FailFast(System.String, System.Exception)
106
- // at Microsoft.PowerShell.UnmanagedPSEntry.Start(System.String, System.String[], Int32)
107
- // at Microsoft.PowerShell.ManagedPSEntry.Main(System.String[])
108
- //
109
- // Exception details:
110
- // System.TypeInitializationException: The type initializer for 'Microsoft.PowerShell.ApplicationInsightsTelemetry' threw an exception. ---> System.ArgumentException: Item has already been added. Key in dictionary: 'SPAWN_WRAP_SHIM_ROOT' Key being added: 'SPAWN_WRAP_SHIM_ROOT'
111
- // at System.Collections.Hashtable.Insert(Object key, Object nvalue, Boolean add)
112
- // at System.Environment.ToHashtable(IEnumerable`1 pairs)
113
- // at System.Environment.GetEnvironmentVariables()
114
- // at Microsoft.ApplicationInsights.Extensibility.Implementation.Platform.PlatformImplementation..ctor()
115
- // at Microsoft.ApplicationInsights.Extensibility.Implementation.Platform.PlatformSingleton.get_Current()
116
- // at Microsoft.ApplicationInsights.Extensibility.Implementation.TelemetryConfigurationFactory.Initialize(TelemetryConfiguration configuration, TelemetryModules modules)
117
- // at Microsoft.ApplicationInsights.Extensibility.TelemetryConfiguration.get_Active()
118
- // at Microsoft.PowerShell.ApplicationInsightsTelemetry..cctor()
119
- // --- End of inner exception stack trace ---
120
- // at Microsoft.PowerShell.ApplicationInsightsTelemetry.SendPSCoreStartupTelemetry()
121
- // at Microsoft.PowerShell.ConsoleHost.Start(String bannerText, String helpText, String[] args)
122
- // at Microsoft.PowerShell.ConsoleShell.Start(String bannerText, String helpText, String[] args)
123
- // at Microsoft.PowerShell.UnmanagedPSEntry.Start(String consoleFilePath, String[] args, Int32 argc)
124
- /* eslint-enable max-len */
125
- let id = 0;
126
- for (const shellConfig of shellConfigs) {
127
- let skipMessage;
128
- if (typeof _onlyTestShell === 'string' && shellConfig.name !== _onlyTestShell) {
129
- skipMessage = `only testing ${_onlyTestShell}`;
130
- }
131
- else if (!shellConfig.path) {
132
- // For each shell, skip if we could not find the executable path.
133
- skipMessage = 'cannot find shell';
134
- }
135
- else {
136
- // Run a test in the shell to catch runtime issues.
137
- // CI seems to have issues with some shells depending on the environment...
138
- try {
139
- const debugName = `${shellConfig.name}/test`;
140
- const shellTest = (0, child_process_1.spawnSync)(shellConfig.path, {
141
- input: 'echo abcdefghijkl\n\n',
142
- timeout: 5000,
143
- });
144
- debug(stdoutFormat(debugName)(shellTest.stdout.toString()));
145
- debug(stderrFormat(debugName)(shellTest.stderr.toString()));
146
- if (!/abcdefghijkl/m.test(shellTest.output.toString())) {
147
- skipMessage = 'wrong test output';
148
- }
149
- }
150
- catch (error) {
151
- console.error(error);
152
- skipMessage = 'error occurred';
153
- }
154
- }
155
- /**
156
- * If skipMessage is set, we should skip the test and explain why.
157
- */
158
- const describeOrSkip = (callback) => {
159
- const describeMessage = `test ${shellConfig.name} commands`;
160
- if (typeof skipMessage === 'undefined') {
161
- describe(describeMessage, callback);
162
- }
163
- else {
164
- describe.skip(`${describeMessage} - skip: ${skipMessage}`, callback);
165
- }
166
- };
167
- describeOrSkip(function () {
168
- this.timeout(10000);
169
- let nodePath;
170
- let cwd;
171
- let submit;
172
- let processInfo;
173
- let context;
174
- beforeEach(() => {
175
- // In WSL, the node path is different than the host one (Windows vs Linux).
176
- nodePath = shellConfig.nodePath || hostNodePath;
177
- // On windows, when running bash we need to convert paths from Windows
178
- // to their mounting point, assuming bash is running within WSL.
179
- if (isWindows && /bash|wsl/.test(shellConfig.name)) {
180
- cwd = convertWindowsPath(testResources);
181
- }
182
- else {
183
- cwd = testResources;
184
- }
185
- // When running powershell, it seems like good measure to send `\n` twice...
186
- if (shellConfig.name === 'powershell') {
187
- submit = '\n\n';
188
- }
189
- // TestContext holds all state for a given test.
190
- const testContextName = `${shellConfig.name}/${++id}`;
191
- context = new TestCaseContext(testContextName, submit);
192
- processInfo = createShell(context, shellConfig.path);
193
- });
194
- afterEach(() => {
195
- processInfo.shell.kill();
196
- context.finalize();
197
- });
198
- it('use simple environment variables', async () => {
199
- const envName = 'SIMPLE_NAME';
200
- const envValue = 'SIMPLE_VALUE';
201
- await testCommandLine(context, processInfo, {
202
- cwd, args: [nodePath, '-p', `\`[\${process.env['${envName}']}]\``],
203
- env: {
204
- [envName]: envValue,
205
- }
206
- }, [
207
- // stderr
208
- scanLines(context, processInfo.shell.stderr, errorScanner, stderrFormat(context.name)),
209
- // stdout
210
- scanLines(context, processInfo.shell.stdout, handle => {
211
- errorScanner(handle);
212
- if (handle.line.includes(`[${envValue}]`)) {
213
- handle.resolve();
214
- }
215
- }, stdoutFormat(context.name)),
216
- ]);
217
- });
218
- it('use problematic environment variables', async () => {
219
- const envName = 'A?B_C | D $PATH';
220
- const envValue = 'SUCCESS';
221
- await testCommandLine(context, processInfo, {
222
- cwd, args: [nodePath, '-p', `\`[\${process.env['${envName}']}]\``],
223
- env: {
224
- [envName]: envValue,
225
- }
226
- }, [
227
- // stderr
228
- scanLines(context, processInfo.shell.stderr, errorScanner, stderrFormat(context.name)),
229
- // stdout
230
- scanLines(context, processInfo.shell.stdout, handle => {
231
- errorScanner(handle);
232
- if (handle.line.includes(`[${envValue}]`)) {
233
- handle.resolve();
234
- }
235
- if (handle.line.includes('[undefined]')) {
236
- handle.reject(new Error(handle.text));
237
- }
238
- }, stdoutFormat(context.name)),
239
- ]);
240
- });
241
- it('command with complex arguments', async () => {
242
- const left = 'ABC';
243
- const right = 'DEF';
244
- await testCommandLine(context, processInfo, {
245
- cwd, args: [nodePath, '-e', `{
246
- const left = '${left}';
247
- const right = '${right}';
248
- console.log(\`[\${left}|\${right}]\`);
249
- }`],
250
- }, [
251
- // stderr
252
- scanLines(context, processInfo.shell.stderr, errorScanner, stderrFormat(context.name)),
253
- // stdout
254
- scanLines(context, processInfo.shell.stdout, handle => {
255
- errorScanner(handle);
256
- if (handle.line.includes(`[${left}|${right}]`)) {
257
- handle.resolve();
258
- }
259
- }, stdoutFormat(context.name)),
260
- ]);
261
- });
262
- });
263
- }
264
- /**
265
- * Allow `command` to fail and return undefined instead.
266
- */
267
- function execShellCommand(command) {
268
- try {
269
- // If trimmed output is an empty string, return `undefined` instead:
270
- return (0, child_process_1.execSync)(command).toString().trim() || undefined;
271
- }
272
- catch (error) {
273
- console.error(command, error);
274
- return undefined;
275
- }
276
- }
277
- /**
278
- * When executing `bash.exe` on Windows, the `C:`, `D:`, etc drives are mounted under `/mnt/<drive>/...`
279
- */
280
- function convertWindowsPath(windowsPath) {
281
- return windowsPath
282
- // Convert back-slashes to forward-slashes
283
- .replace(/\\/g, '/')
284
- // Convert drive-letter to usual mounting point in WSL
285
- .replace(/^[A-Za-z]:\//, s => `/mnt/${s[0].toLowerCase()}/`);
286
- }
287
- /**
288
- * Display trailing whitespace in a string, such as \r and \n.
289
- */
290
- function displayWhitespaces(line) {
291
- return line
292
- .replace(/\r?\n/, s => s.length === 2 ? '<\\r\\n>\r\n' : '<\\n>\n');
293
- }
294
- /**
295
- * Actually run `prepareCommandLine`.
296
- */
297
- async function testCommandLine(context, processInfo, options,
298
- // eslint-disable-next-line @typescript-eslint/no-explicit-any
299
- firstOf) {
300
- const commandLine = shellCommandBuilder.buildCommand(processInfo, options);
301
- debug(`${chalk.bold(chalk.white(`${context.name} STDIN:`))} ${chalk.bgWhite(chalk.black(displayWhitespaces(commandLine)))}`);
302
- processInfo.shell.stdin.write(commandLine + context.submit);
303
- return Promise.race(firstOf);
304
- }
305
- /**
306
- * Creates a `(Test)ProcessInfo` object by spawning the specified shell.
307
- */
308
- function createShell(context, shellExecutable, shellArguments = []) {
309
- const shell = (0, child_process_1.spawn)(shellExecutable, shellArguments, spawnOptions);
310
- debug(chalk.magenta(`${chalk.bold(`${context.name} SPAWN:`)} ${shellExecutable} ${shellArguments.join(' ')}`));
311
- shell.on('close', (code, signal) => debug(chalk.magenta(`${chalk.bold(`${context.name} CLOSE:`)} ${shellExecutable} code(${code}) signal(${signal})`)));
312
- return {
313
- executable: shellExecutable,
314
- arguments: [],
315
- shell,
316
- };
317
- }
318
- /**
319
- * Fire `callback` once per new detected line.
320
- */
321
- async function scanLines(context, stream, callback, debugFormat = (s) => s) {
322
- return new Promise((resolve, reject) => {
323
- let line = '';
324
- let text = '';
325
- stream.on('close', () => {
326
- debug(debugFormat('<CLOSED>'));
327
- });
328
- // The `data` listener will be collected on 'close', which will happen
329
- // once we kill the process.
330
- stream.on('data', data => {
331
- if (context.resolved) {
332
- return;
333
- }
334
- const split = data.toString().split('\n');
335
- while (!context.resolved && split.length > 1) {
336
- line += split.shift() + '\n';
337
- text += line;
338
- debug(debugFormat(displayWhitespaces(line)));
339
- try {
340
- callback({
341
- resolve: (value) => {
342
- if (!context.resolved) {
343
- context.resolve();
344
- resolve(value);
345
- debug(chalk.bold(chalk.green(`${context.name} SCANLINES RESOLVED`)));
346
- }
347
- },
348
- reject: (reason) => {
349
- if (!context.resolved) {
350
- context.resolve();
351
- reject(reason);
352
- debug(chalk.bold(chalk.red(`${context.name} SCANLINES REJECTED`)));
353
- }
354
- },
355
- line,
356
- text,
357
- });
358
- }
359
- catch (error) {
360
- debug(chalk.bold(chalk.red(`${context.name} SCANLINES THROWED`)));
361
- context.resolve();
362
- reject(error);
363
- break;
364
- }
365
- line = '';
366
- }
367
- line += split[0];
368
- });
369
- });
370
- }
371
- /**
372
- * We need a test case context to help with catching listeners that timed-out,
373
- * and synchronize multiple listeners so that when one resolves the test case,
374
- * the others can be put in "sleep mode" until destruction.
375
- */
376
- class TestCaseContext {
377
- constructor(
378
- /**
379
- * A name associated with this context, to help with debugging.
380
- */
381
- name,
382
- /**
383
- * The characters to send in order to submit a command (mostly
384
- * powershell is causing issues).
385
- */
386
- submit = '\n',
387
- /**
388
- * @internal Current state of the test case, if it is finished or not.
389
- */
390
- resolved = false) {
391
- this.name = name;
392
- this.submit = submit;
393
- this.resolved = resolved;
394
- }
395
- resolve() {
396
- this.resolved = true;
397
- }
398
- finalize() {
399
- if (!this.resolved) {
400
- this.resolve();
401
- debug(chalk.red(`${chalk.bold(`${this.name} CONTEXT:`)} context wasn't resolved when finalizing, resolving!`));
402
- }
403
- }
404
- }
1
+ "use strict";
2
+ // *****************************************************************************
3
+ // Copyright (C) 2020 Ericsson and others.
4
+ //
5
+ // This program and the accompanying materials are made available under the
6
+ // terms of the Eclipse Public License v. 2.0 which is available at
7
+ // http://www.eclipse.org/legal/epl-2.0.
8
+ //
9
+ // This Source Code may also be made available under the following Secondary
10
+ // Licenses when the conditions for such availability set forth in the Eclipse
11
+ // Public License v. 2.0 are satisfied: GNU General Public License, version 2
12
+ // with the GNU Classpath Exception which is available at
13
+ // https://www.gnu.org/software/classpath/license.html.
14
+ //
15
+ // SPDX-License-Identifier: EPL-2.0 OR GPL-2.0-only WITH Classpath-exception-2.0
16
+ // *****************************************************************************
17
+ Object.defineProperty(exports, "__esModule", { value: true });
18
+ /**
19
+ * This test suite assumes that we run in a NodeJS environment!
20
+ */
21
+ const child_process_1 = require("child_process");
22
+ const path_1 = require("path");
23
+ const shell_command_builder_1 = require("./shell-command-builder");
24
+ const chalk = require("chalk"); // tslint:disable-line:no-implicit-dependencies
25
+ const isWindows = process.platform === 'win32';
26
+ /**
27
+ * Extra debugging info (very verbose).
28
+ */
29
+ const _debug = Boolean(process.env['THEIA_PROCESS_TEST_DEBUG']);
30
+ /**
31
+ * On Windows, some shells simply mess up the terminal's output.
32
+ * Enable if you still want to test those.
33
+ */
34
+ const _runWeirdShell = Boolean(process.env['THEIA_PROCESS_TEST_WEIRD_SHELL']) || undefined;
35
+ /**
36
+ * You might only have issues with a specific shell (`cmd.exe` I am looking at you).
37
+ */
38
+ const _onlyTestShell = process.env['THEIA_PROCESS_TEST_ONLY'] || undefined;
39
+ /**
40
+ * Only log if environment variable is set.
41
+ */
42
+ // eslint-disable-next-line @typescript-eslint/no-explicit-any
43
+ function debug(...parts) {
44
+ if (_debug) {
45
+ console.debug(...parts);
46
+ }
47
+ }
48
+ const testResources = (0, path_1.join)(__dirname, '../../src/common/tests');
49
+ const spawnOptions = {
50
+ // We do our own quoting, don't rely on the one done by NodeJS:
51
+ windowsVerbatimArguments: true,
52
+ stdio: ['pipe', 'pipe', 'pipe'],
53
+ };
54
+ // Formatting options, used with `scanLines` for debugging.
55
+ const stdoutFormat = (prefix) => (data) => `${chalk.bold(chalk.yellow(`${prefix} STDOUT:`))} ${chalk.bgYellow(chalk.black(data))}`;
56
+ const stderrFormat = (prefix) => (data) => `${chalk.bold(chalk.red(`${prefix} STDERR:`))} ${chalk.bgRed(chalk.white(data))}`;
57
+ // Default error scanner
58
+ const errorScanner = (handle) => {
59
+ if (/^\s*\w+Error:/.test(handle.line) ||
60
+ /^\s*Cannot find /.test(handle.line)) {
61
+ throw new Error(handle.text);
62
+ }
63
+ };
64
+ // Yarn mangles the PATH and creates some proxy script around node(.exe),
65
+ // which messes up our environment, failing the tests.
66
+ const hostNodePath = process.env['npm_node_execpath'] ||
67
+ process.env['NODE'];
68
+ if (!hostNodePath) {
69
+ throw new Error('Could not determine the real node path.');
70
+ }
71
+ const shellCommandBuilder = new shell_command_builder_1.ShellCommandBuilder();
72
+ const shellConfigs = [{
73
+ name: 'bash',
74
+ path: isWindows
75
+ ? _runWeirdShell && execShellCommand('where bash.exe')
76
+ : execShellCommand('command -v bash'),
77
+ nodePath: isWindows && 'node' // Good enough
78
+ }, {
79
+ name: 'wsl',
80
+ path: isWindows
81
+ ? _runWeirdShell && execShellCommand('where wsl.exe')
82
+ : undefined,
83
+ nodePath: isWindows && 'node' // Good enough
84
+ }, {
85
+ name: 'cmd',
86
+ path: isWindows
87
+ ? execShellCommand('where cmd.exe')
88
+ : undefined,
89
+ }, {
90
+ name: 'powershell',
91
+ path: execShellCommand(isWindows
92
+ ? 'where powershell'
93
+ : 'command -v pwsh'),
94
+ }];
95
+ /* eslint-disable max-len */
96
+ // 18d/12m/19y - Ubuntu 16.04:
97
+ // Powershell sometimes fails when running as part of an npm lifecycle script.
98
+ // See following error:
99
+ //
100
+ //
101
+ // FailFast:
102
+ // The type initializer for 'Microsoft.PowerShell.ApplicationInsightsTelemetry' threw an exception.
103
+ //
104
+ // at System.Environment.FailFast(System.String, System.Exception)
105
+ // at System.Environment.FailFast(System.String, System.Exception)
106
+ // at Microsoft.PowerShell.UnmanagedPSEntry.Start(System.String, System.String[], Int32)
107
+ // at Microsoft.PowerShell.ManagedPSEntry.Main(System.String[])
108
+ //
109
+ // Exception details:
110
+ // System.TypeInitializationException: The type initializer for 'Microsoft.PowerShell.ApplicationInsightsTelemetry' threw an exception. ---> System.ArgumentException: Item has already been added. Key in dictionary: 'SPAWN_WRAP_SHIM_ROOT' Key being added: 'SPAWN_WRAP_SHIM_ROOT'
111
+ // at System.Collections.Hashtable.Insert(Object key, Object nvalue, Boolean add)
112
+ // at System.Environment.ToHashtable(IEnumerable`1 pairs)
113
+ // at System.Environment.GetEnvironmentVariables()
114
+ // at Microsoft.ApplicationInsights.Extensibility.Implementation.Platform.PlatformImplementation..ctor()
115
+ // at Microsoft.ApplicationInsights.Extensibility.Implementation.Platform.PlatformSingleton.get_Current()
116
+ // at Microsoft.ApplicationInsights.Extensibility.Implementation.TelemetryConfigurationFactory.Initialize(TelemetryConfiguration configuration, TelemetryModules modules)
117
+ // at Microsoft.ApplicationInsights.Extensibility.TelemetryConfiguration.get_Active()
118
+ // at Microsoft.PowerShell.ApplicationInsightsTelemetry..cctor()
119
+ // --- End of inner exception stack trace ---
120
+ // at Microsoft.PowerShell.ApplicationInsightsTelemetry.SendPSCoreStartupTelemetry()
121
+ // at Microsoft.PowerShell.ConsoleHost.Start(String bannerText, String helpText, String[] args)
122
+ // at Microsoft.PowerShell.ConsoleShell.Start(String bannerText, String helpText, String[] args)
123
+ // at Microsoft.PowerShell.UnmanagedPSEntry.Start(String consoleFilePath, String[] args, Int32 argc)
124
+ /* eslint-enable max-len */
125
+ let id = 0;
126
+ for (const shellConfig of shellConfigs) {
127
+ let skipMessage;
128
+ if (typeof _onlyTestShell === 'string' && shellConfig.name !== _onlyTestShell) {
129
+ skipMessage = `only testing ${_onlyTestShell}`;
130
+ }
131
+ else if (!shellConfig.path) {
132
+ // For each shell, skip if we could not find the executable path.
133
+ skipMessage = 'cannot find shell';
134
+ }
135
+ else {
136
+ // Run a test in the shell to catch runtime issues.
137
+ // CI seems to have issues with some shells depending on the environment...
138
+ try {
139
+ const debugName = `${shellConfig.name}/test`;
140
+ const shellTest = (0, child_process_1.spawnSync)(shellConfig.path, {
141
+ input: 'echo abcdefghijkl\n\n',
142
+ timeout: 5000,
143
+ });
144
+ debug(stdoutFormat(debugName)(shellTest.stdout.toString()));
145
+ debug(stderrFormat(debugName)(shellTest.stderr.toString()));
146
+ if (!/abcdefghijkl/m.test(shellTest.output.toString())) {
147
+ skipMessage = 'wrong test output';
148
+ }
149
+ }
150
+ catch (error) {
151
+ console.error(error);
152
+ skipMessage = 'error occurred';
153
+ }
154
+ }
155
+ /**
156
+ * If skipMessage is set, we should skip the test and explain why.
157
+ */
158
+ const describeOrSkip = (callback) => {
159
+ const describeMessage = `test ${shellConfig.name} commands`;
160
+ if (typeof skipMessage === 'undefined') {
161
+ describe(describeMessage, callback);
162
+ }
163
+ else {
164
+ describe.skip(`${describeMessage} - skip: ${skipMessage}`, callback);
165
+ }
166
+ };
167
+ describeOrSkip(function () {
168
+ this.timeout(10000);
169
+ let nodePath;
170
+ let cwd;
171
+ let submit;
172
+ let processInfo;
173
+ let context;
174
+ beforeEach(() => {
175
+ // In WSL, the node path is different than the host one (Windows vs Linux).
176
+ nodePath = shellConfig.nodePath || hostNodePath;
177
+ // On windows, when running bash we need to convert paths from Windows
178
+ // to their mounting point, assuming bash is running within WSL.
179
+ if (isWindows && /bash|wsl/.test(shellConfig.name)) {
180
+ cwd = convertWindowsPath(testResources);
181
+ }
182
+ else {
183
+ cwd = testResources;
184
+ }
185
+ // When running powershell, it seems like good measure to send `\n` twice...
186
+ if (shellConfig.name === 'powershell') {
187
+ submit = '\n\n';
188
+ }
189
+ // TestContext holds all state for a given test.
190
+ const testContextName = `${shellConfig.name}/${++id}`;
191
+ context = new TestCaseContext(testContextName, submit);
192
+ processInfo = createShell(context, shellConfig.path);
193
+ });
194
+ afterEach(() => {
195
+ processInfo.shell.kill();
196
+ context.finalize();
197
+ });
198
+ it('use simple environment variables', async () => {
199
+ const envName = 'SIMPLE_NAME';
200
+ const envValue = 'SIMPLE_VALUE';
201
+ await testCommandLine(context, processInfo, {
202
+ cwd, args: [nodePath, '-p', `\`[\${process.env['${envName}']}]\``],
203
+ env: {
204
+ [envName]: envValue,
205
+ }
206
+ }, [
207
+ // stderr
208
+ scanLines(context, processInfo.shell.stderr, errorScanner, stderrFormat(context.name)),
209
+ // stdout
210
+ scanLines(context, processInfo.shell.stdout, handle => {
211
+ errorScanner(handle);
212
+ if (handle.line.includes(`[${envValue}]`)) {
213
+ handle.resolve();
214
+ }
215
+ }, stdoutFormat(context.name)),
216
+ ]);
217
+ });
218
+ it('use problematic environment variables', async () => {
219
+ const envName = 'A?B_C | D $PATH';
220
+ const envValue = 'SUCCESS';
221
+ await testCommandLine(context, processInfo, {
222
+ cwd, args: [nodePath, '-p', `\`[\${process.env['${envName}']}]\``],
223
+ env: {
224
+ [envName]: envValue,
225
+ }
226
+ }, [
227
+ // stderr
228
+ scanLines(context, processInfo.shell.stderr, errorScanner, stderrFormat(context.name)),
229
+ // stdout
230
+ scanLines(context, processInfo.shell.stdout, handle => {
231
+ errorScanner(handle);
232
+ if (handle.line.includes(`[${envValue}]`)) {
233
+ handle.resolve();
234
+ }
235
+ if (handle.line.includes('[undefined]')) {
236
+ handle.reject(new Error(handle.text));
237
+ }
238
+ }, stdoutFormat(context.name)),
239
+ ]);
240
+ });
241
+ it('command with complex arguments', async () => {
242
+ const left = 'ABC';
243
+ const right = 'DEF';
244
+ await testCommandLine(context, processInfo, {
245
+ cwd, args: [nodePath, '-e', `{
246
+ const left = '${left}';
247
+ const right = '${right}';
248
+ console.log(\`[\${left}|\${right}]\`);
249
+ }`],
250
+ }, [
251
+ // stderr
252
+ scanLines(context, processInfo.shell.stderr, errorScanner, stderrFormat(context.name)),
253
+ // stdout
254
+ scanLines(context, processInfo.shell.stdout, handle => {
255
+ errorScanner(handle);
256
+ if (handle.line.includes(`[${left}|${right}]`)) {
257
+ handle.resolve();
258
+ }
259
+ }, stdoutFormat(context.name)),
260
+ ]);
261
+ });
262
+ });
263
+ }
264
+ /**
265
+ * Allow `command` to fail and return undefined instead.
266
+ */
267
+ function execShellCommand(command) {
268
+ try {
269
+ // If trimmed output is an empty string, return `undefined` instead:
270
+ return (0, child_process_1.execSync)(command).toString().trim() || undefined;
271
+ }
272
+ catch (error) {
273
+ console.error(command, error);
274
+ return undefined;
275
+ }
276
+ }
277
+ /**
278
+ * When executing `bash.exe` on Windows, the `C:`, `D:`, etc drives are mounted under `/mnt/<drive>/...`
279
+ */
280
+ function convertWindowsPath(windowsPath) {
281
+ return windowsPath
282
+ // Convert back-slashes to forward-slashes
283
+ .replace(/\\/g, '/')
284
+ // Convert drive-letter to usual mounting point in WSL
285
+ .replace(/^[A-Za-z]:\//, s => `/mnt/${s[0].toLowerCase()}/`);
286
+ }
287
+ /**
288
+ * Display trailing whitespace in a string, such as \r and \n.
289
+ */
290
+ function displayWhitespaces(line) {
291
+ return line
292
+ .replace(/\r?\n/, s => s.length === 2 ? '<\\r\\n>\r\n' : '<\\n>\n');
293
+ }
294
+ /**
295
+ * Actually run `prepareCommandLine`.
296
+ */
297
+ async function testCommandLine(context, processInfo, options,
298
+ // eslint-disable-next-line @typescript-eslint/no-explicit-any
299
+ firstOf) {
300
+ const commandLine = shellCommandBuilder.buildCommand(processInfo, options);
301
+ debug(`${chalk.bold(chalk.white(`${context.name} STDIN:`))} ${chalk.bgWhite(chalk.black(displayWhitespaces(commandLine)))}`);
302
+ processInfo.shell.stdin.write(commandLine + context.submit);
303
+ return Promise.race(firstOf);
304
+ }
305
+ /**
306
+ * Creates a `(Test)ProcessInfo` object by spawning the specified shell.
307
+ */
308
+ function createShell(context, shellExecutable, shellArguments = []) {
309
+ const shell = (0, child_process_1.spawn)(shellExecutable, shellArguments, spawnOptions);
310
+ debug(chalk.magenta(`${chalk.bold(`${context.name} SPAWN:`)} ${shellExecutable} ${shellArguments.join(' ')}`));
311
+ shell.on('close', (code, signal) => debug(chalk.magenta(`${chalk.bold(`${context.name} CLOSE:`)} ${shellExecutable} code(${code}) signal(${signal})`)));
312
+ return {
313
+ executable: shellExecutable,
314
+ arguments: [],
315
+ shell,
316
+ };
317
+ }
318
+ /**
319
+ * Fire `callback` once per new detected line.
320
+ */
321
+ async function scanLines(context, stream, callback, debugFormat = (s) => s) {
322
+ return new Promise((resolve, reject) => {
323
+ let line = '';
324
+ let text = '';
325
+ stream.on('close', () => {
326
+ debug(debugFormat('<CLOSED>'));
327
+ });
328
+ // The `data` listener will be collected on 'close', which will happen
329
+ // once we kill the process.
330
+ stream.on('data', data => {
331
+ if (context.resolved) {
332
+ return;
333
+ }
334
+ const split = data.toString().split('\n');
335
+ while (!context.resolved && split.length > 1) {
336
+ line += split.shift() + '\n';
337
+ text += line;
338
+ debug(debugFormat(displayWhitespaces(line)));
339
+ try {
340
+ callback({
341
+ resolve: (value) => {
342
+ if (!context.resolved) {
343
+ context.resolve();
344
+ resolve(value);
345
+ debug(chalk.bold(chalk.green(`${context.name} SCANLINES RESOLVED`)));
346
+ }
347
+ },
348
+ reject: (reason) => {
349
+ if (!context.resolved) {
350
+ context.resolve();
351
+ reject(reason);
352
+ debug(chalk.bold(chalk.red(`${context.name} SCANLINES REJECTED`)));
353
+ }
354
+ },
355
+ line,
356
+ text,
357
+ });
358
+ }
359
+ catch (error) {
360
+ debug(chalk.bold(chalk.red(`${context.name} SCANLINES THROWED`)));
361
+ context.resolve();
362
+ reject(error);
363
+ break;
364
+ }
365
+ line = '';
366
+ }
367
+ line += split[0];
368
+ });
369
+ });
370
+ }
371
+ /**
372
+ * We need a test case context to help with catching listeners that timed-out,
373
+ * and synchronize multiple listeners so that when one resolves the test case,
374
+ * the others can be put in "sleep mode" until destruction.
375
+ */
376
+ class TestCaseContext {
377
+ constructor(
378
+ /**
379
+ * A name associated with this context, to help with debugging.
380
+ */
381
+ name,
382
+ /**
383
+ * The characters to send in order to submit a command (mostly
384
+ * powershell is causing issues).
385
+ */
386
+ submit = '\n',
387
+ /**
388
+ * @internal Current state of the test case, if it is finished or not.
389
+ */
390
+ resolved = false) {
391
+ this.name = name;
392
+ this.submit = submit;
393
+ this.resolved = resolved;
394
+ }
395
+ resolve() {
396
+ this.resolved = true;
397
+ }
398
+ finalize() {
399
+ if (!this.resolved) {
400
+ this.resolve();
401
+ debug(chalk.red(`${chalk.bold(`${this.name} CONTEXT:`)} context wasn't resolved when finalizing, resolving!`));
402
+ }
403
+ }
404
+ }
405
405
  //# sourceMappingURL=shell-command-builder.slow-spec.js.map