fix(dev-infra): spawned child processes messing with tty output (#41948)

Currently we have a common utility method for running commands
in a child process. This method pipes all stdout and stderr, but sets
the `stdin` to `inherited`. This seemed to work as expected in terms of
allowing interactive commands being executed, but it messes with the
TTY in Windows (and potentially other platforms) so that colors and
prompts no longer work properly. See attached screenshot.

We fix this by not inheriting the stdin by default; but exposing
a dedicated method for interactive commands. This results in more
readable and obvious code too, so it's worth making this change
regardless of the TTY issues.

PR Close #41948
This commit is contained in:
Paul Gschwendtner 2021-05-04 17:22:11 +02:00 committed by Misko Hevery
parent fb38175a40
commit 21c2a06811
4 changed files with 43 additions and 9 deletions

View File

@ -5212,6 +5212,21 @@ const ReleaseBuildCommandModule = {
* Use of this source code is governed by an MIT-style license that can be
* found in the LICENSE file at https://angular.io/license
*/
/**
* Spawns a given command with the specified arguments inside an interactive shell. All process
* stdin, stdout and stderr output is printed to the current console.
*
* @returns a Promise resolving on success, and rejecting on command failure with the status code.
*/
function spawnInteractiveCommand(command, args, options) {
if (options === void 0) { options = {}; }
return new Promise(function (resolve, reject) {
var commandText = command + " " + args.join(' ');
debug("Executing command: " + commandText);
var childProcess = child_process.spawn(command, args, tslib.__assign(tslib.__assign({}, options), { shell: true, stdio: 'inherit' }));
childProcess.on('exit', function (status) { return status === 0 ? resolve() : reject(status); });
});
}
/**
* Spawns a given command with the specified arguments inside a shell. All process stdout
* output is captured and returned as resolution on completion. Depending on the chosen
@ -5226,7 +5241,7 @@ function spawnWithDebugOutput(command, args, options) {
var commandText = command + " " + args.join(' ');
var outputMode = options.mode;
debug("Executing command: " + commandText);
var childProcess = child_process.spawn(command, args, tslib.__assign(tslib.__assign({}, options), { shell: true, stdio: ['inherit', 'pipe', 'pipe'] }));
var childProcess = child_process.spawn(command, args, tslib.__assign(tslib.__assign({}, options), { shell: true, stdio: 'pipe' }));
var logOutput = '';
var stdout = '';
var stderr = '';
@ -5324,7 +5339,7 @@ function npmIsLoggedIn(registryUrl) {
}
/**
* Log into NPM at a provided registry.
* @throws With the process log output if the login fails.
* @throws With the `npm login` status code if the login failed.
*/
function npmLogin(registryUrl) {
return tslib.__awaiter(this, void 0, void 0, function* () {
@ -5335,7 +5350,9 @@ function npmLogin(registryUrl) {
if (registryUrl !== undefined) {
args.splice(1, 0, '--registry', registryUrl);
}
yield spawnWithDebugOutput('npm', args);
// The login command prompts for username, password and other profile information. Hence
// the process needs to be interactive (i.e. respecting current TTYs stdin).
yield spawnInteractiveCommand('npm', args);
});
}
/**

View File

@ -12,7 +12,6 @@ import {spawnWithDebugOutput} from '../../utils/child-process';
import {GithubConfig} from '../../utils/config';
import {debug, error, info, log, promptConfirm, red, yellow} from '../../utils/console';
import {GitClient} from '../../utils/git/index';
import {exec} from '../../utils/shelljs';
import {ReleaseConfig} from '../config/index';
import {ActiveReleaseTrains, fetchActiveReleaseTrains, nextBranchName} from '../versioning/active-release-trains';
import {npmIsLoggedIn, npmLogin, npmLogout} from '../versioning/npm-publish';

View File

@ -7,7 +7,7 @@
*/
import * as semver from 'semver';
import {spawnWithDebugOutput} from '../../utils/child-process';
import {spawnInteractiveCommand, spawnWithDebugOutput} from '../../utils/child-process';
/**
* Runs NPM publish within a specified package directory.
@ -57,7 +57,7 @@ export async function npmIsLoggedIn(registryUrl: string|undefined): Promise<bool
/**
* Log into NPM at a provided registry.
* @throws With the process log output if the login fails.
* @throws With the `npm login` status code if the login failed.
*/
export async function npmLogin(registryUrl: string|undefined) {
const args = ['login', '--no-browser'];
@ -67,7 +67,9 @@ export async function npmLogin(registryUrl: string|undefined) {
if (registryUrl !== undefined) {
args.splice(1, 0, '--registry', registryUrl);
}
await spawnWithDebugOutput('npm', args);
// The login command prompts for username, password and other profile information. Hence
// the process needs to be interactive (i.e. respecting current TTYs stdin).
await spawnInteractiveCommand('npm', args);
}
/**

View File

@ -23,6 +23,22 @@ export interface SpawnedProcessResult {
stderr: string;
}
/**
* Spawns a given command with the specified arguments inside an interactive shell. All process
* stdin, stdout and stderr output is printed to the current console.
*
* @returns a Promise resolving on success, and rejecting on command failure with the status code.
*/
export function spawnInteractiveCommand(
command: string, args: string[], options: Omit<SpawnOptions, 'stdio'> = {}) {
return new Promise<void>((resolve, reject) => {
const commandText = `${command} ${args.join(' ')}`;
debug(`Executing command: ${commandText}`);
const childProcess = spawn(command, args, {...options, shell: true, stdio: 'inherit'});
childProcess.on('exit', status => status === 0 ? resolve() : reject(status));
});
}
/**
* Spawns a given command with the specified arguments inside a shell. All process stdout
* output is captured and returned as resolution on completion. Depending on the chosen
@ -40,8 +56,7 @@ export function spawnWithDebugOutput(
debug(`Executing command: ${commandText}`);
const childProcess =
spawn(command, args, {...options, shell: true, stdio: ['inherit', 'pipe', 'pipe']});
const childProcess = spawn(command, args, {...options, shell: true, stdio: 'pipe'});
let logOutput = '';
let stdout = '';
let stderr = '';
@ -57,6 +72,7 @@ export function spawnWithDebugOutput(
process.stderr.write(message);
}
});
childProcess.stdout.on('data', message => {
stdout += message;
logOutput += message;