Skip to content

Commit

Permalink
Handle EPIPE on stdout gracefully
Browse files Browse the repository at this point in the history
  • Loading branch information
arbscht committed Apr 12, 2019
1 parent 8a671f1 commit 0e67b6c
Show file tree
Hide file tree
Showing 2 changed files with 104 additions and 0 deletions.
96 changes: 96 additions & 0 deletions __tests__/pipe.js
@@ -0,0 +1,96 @@
/* @flow */
/* eslint max-len: 0 */

import execa from 'execa';
import {sh} from 'puka';
import makeTemp from './_temp.js';
import * as fs from '../src/util/fs.js';

const path = require('path');

function runYarnStreaming(args: Array<string> = [], options: Object = {}): execa.ExecaChildPromise {
if (!options['env']) {
options['env'] = {...process.env};
options['extendEnv'] = false;
}
options['env']['FORCE_COLOR'] = 0;

return execa.shell(sh`${path.resolve(__dirname, '../bin/yarn')} ${args}`, options);
}

test('terminate console stream quietly on EPIPE', async () => {
const cwd = await makeTemp();
const packageJsonPath = path.join(cwd, 'package.json');
const initialManifestFile = JSON.stringify({name: 'test', license: 'ISC', version: '1.0.0'});

await fs.writeFile(packageJsonPath, initialManifestFile);

const {stdout, stderr} = runYarnStreaming(['versions'], {cwd});

stdout.destroy();

await new Promise((resolve, reject) => {
let stderrOutput = '';
stderr.on('readable', () => {
const chunk = stderr.read();
if (chunk) {
stderrOutput += chunk;
} else {
resolve(stderrOutput);
}
});
stderr.on('error', err => {
reject(err);
});
})
.then(stderrOutput => {
expect(stderrOutput).not.toMatch(/EPIPE/);
})
.catch(err => {
expect(err).toBeFalsy();
});
});

test('terminate console stream preserving zero exit code on EPIPE', async () => {
const cwd = await makeTemp();
const packageJsonPath = path.join(cwd, 'package.json');
const initialManifestFile = JSON.stringify({name: 'test', license: 'ISC', version: '1.0.0'});

await fs.writeFile(packageJsonPath, initialManifestFile);

const proc = runYarnStreaming(['versions'], {cwd});

const {stdout} = proc;

stdout.destroy();

await new Promise(resolve => {
proc.on('exit', function(code, signal) {
resolve(code);
});
}).then(exitCode => {
expect(exitCode).toEqual(0);
});
});

test('terminate console stream preserving non-zero exit code on EPIPE', async () => {
const cwd = await makeTemp();
const packageJsonPath = path.join(cwd, 'package.json');
const initialManifestFile = JSON.stringify({name: 'test', license: 'ISC', version: '1.0.0'});

await fs.writeFile(packageJsonPath, initialManifestFile);

const proc = runYarnStreaming(['add'], {cwd});

const {stdout} = proc;

stdout.destroy();

await new Promise(resolve => {
proc.on('exit', function(code, signal) {
resolve(code);
});
}).then(exitCode => {
expect(exitCode).toEqual(1);
});
});
8 changes: 8 additions & 0 deletions src/cli/index.js
Expand Up @@ -26,6 +26,14 @@ import handleSignals from '../util/signal-handler.js';
import {boolify, boolifyWithDefault} from '../util/conversion.js';
import {ProcessTermError} from '../errors';

process.stdout.prependListener('error', err => {
// swallow err only if downstream consumer process closed pipe early
if (err.code === 'EPIPE' || err.code === 'ERR_STREAM_DESTROYED') {
return;
}
throw err;
});

function findProjectRoot(base: string): string {
let prev = null;
let dir = base;
Expand Down

0 comments on commit 0e67b6c

Please sign in to comment.