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