Skip to content

Commit b1451cb

Browse files
committed
fix(@angular/cli): improve resilience of logging during process exit
In certain situations the existing console logger created via `@angular-devkit/core` `createConsoleLogger` could try to write to a closed stdout pipe stream. This would result in an error during execution. For cases such as the completion script command, this would also prevent the command from functioning. To mitigate these cases, `createConsoleLogger` is no longer used and instead a logger instance is directly created within the CLI that uses `Console.log` and `Console.error` to write output. Exiting the CLI also now waits for messages to be logged before proceeding with the exit. (cherry picked from commit f6f3782)
1 parent f001987 commit b1451cb

File tree

2 files changed

+52
-14
lines changed

2 files changed

+52
-14
lines changed

packages/angular/cli/lib/cli/index.ts

Lines changed: 40 additions & 9 deletions
Original file line numberDiff line numberDiff line change
@@ -6,7 +6,7 @@
66
* found in the LICENSE file at https://angular.io/license
77
*/
88

9-
import { createConsoleLogger } from '@angular-devkit/core/node';
9+
import { logging } from '@angular-devkit/core';
1010
import { format } from 'util';
1111
import { CommandModuleError } from '../../src/command-builder/command-module';
1212
import { runCommand } from '../../src/command-builder/command-runner';
@@ -16,26 +16,54 @@ import { writeErrorToLogFile } from '../../src/utilities/log-file';
1616

1717
export { VERSION } from '../../src/utilities/version';
1818

19+
const MIN_NODEJS_VERISON = [14, 15] as const;
20+
1921
/* eslint-disable no-console */
2022
export default async function (options: { cliArgs: string[] }) {
2123
// This node version check ensures that the requirements of the project instance of the CLI are met
2224
const [major, minor] = process.versions.node.split('.').map((part) => Number(part));
23-
if (major < 14 || (major === 14 && minor < 15)) {
25+
if (
26+
major < MIN_NODEJS_VERISON[0] ||
27+
(major === MIN_NODEJS_VERISON[0] && minor < MIN_NODEJS_VERISON[1])
28+
) {
2429
process.stderr.write(
2530
`Node.js version ${process.version} detected.\n` +
26-
'The Angular CLI requires a minimum v14.15.\n\n' +
31+
`The Angular CLI requires a minimum of v${MIN_NODEJS_VERISON[0]}.${MIN_NODEJS_VERISON[1]}.\n\n` +
2732
'Please update your Node.js version or visit https://nodejs.org/ for additional instructions.\n',
2833
);
2934

3035
return 3;
3136
}
3237

33-
const logger = createConsoleLogger(ngDebug, process.stdout, process.stderr, {
34-
info: (s) => (colors.enabled ? s : removeColor(s)),
35-
debug: (s) => (colors.enabled ? s : removeColor(s)),
36-
warn: (s) => (colors.enabled ? colors.bold.yellow(s) : removeColor(s)),
37-
error: (s) => (colors.enabled ? colors.bold.red(s) : removeColor(s)),
38-
fatal: (s) => (colors.enabled ? colors.bold.red(s) : removeColor(s)),
38+
const colorLevels: Record<string, (message: string) => string> = {
39+
info: (s) => s,
40+
debug: (s) => s,
41+
warn: (s) => colors.bold.yellow(s),
42+
error: (s) => colors.bold.red(s),
43+
fatal: (s) => colors.bold.red(s),
44+
};
45+
const logger = new logging.IndentLogger('cli-main-logger');
46+
const logInfo = console.log;
47+
const logError = console.error;
48+
49+
const loggerFinished = logger.forEach((entry) => {
50+
if (!ngDebug && entry.level === 'debug') {
51+
return;
52+
}
53+
54+
const color = colors.enabled ? colorLevels[entry.level] : removeColor;
55+
const message = color(entry.message);
56+
57+
switch (entry.level) {
58+
case 'warn':
59+
case 'fatal':
60+
case 'error':
61+
logError(message);
62+
break;
63+
default:
64+
logInfo(message);
65+
break;
66+
}
3967
});
4068

4169
// Redirect console to logger
@@ -83,5 +111,8 @@ export default async function (options: { cliArgs: string[] }) {
83111
}
84112

85113
return 1;
114+
} finally {
115+
logger.complete();
116+
await loggerFinished;
86117
}
87118
}

tests/legacy-cli/e2e/tests/misc/completion-script.ts

Lines changed: 12 additions & 5 deletions
Original file line numberDiff line numberDiff line change
@@ -1,4 +1,4 @@
1-
import { execAndWaitForOutputToMatch } from '../../utils/process';
1+
import { exec, execAndWaitForOutputToMatch } from '../../utils/process';
22

33
export default async function () {
44
// ng build
@@ -54,10 +54,17 @@ export default async function () {
5454
['--get-yargs-completions', 'ng', 'run', 'test-project:'],
5555
/test-project\\:test/,
5656
);
57-
await execAndWaitForOutputToMatch(
57+
58+
const { stdout: noServeStdout } = await exec(
5859
'ng',
59-
['--get-yargs-completions', 'ng', 'run', 'test-project:build'],
60-
// does not include 'test-project:serve'
61-
/^((?!:serve).)*$/,
60+
'--get-yargs-completions',
61+
'ng',
62+
'run',
63+
'test-project:build',
6264
);
65+
if (noServeStdout.includes(':serve')) {
66+
throw new Error(
67+
`':serve' should not have been listed as a completion option.\nSTDOUT:\n${noServeStdout}`,
68+
);
69+
}
6370
}

0 commit comments

Comments
 (0)