Skip to content
Draft
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
37 changes: 37 additions & 0 deletions packages/insomnia-smoke-test/tests/smoke/stdout-epipe.test.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1,37 @@
import { expect } from '@playwright/test';

import { test } from '../../playwright/test';

// Regression tests for https://github.com/Kong/insomnia/issues/9951
// On Linux, launching from a .desktop entry (or any context without a writable stdout)
// could crash the main process via an async EPIPE error event on process.stdout.
test.describe('stdout EPIPE resilience', () => {
test('EPIPE error handler is registered on process.stdout at startup', async ({ app }) => {
const listenerCount = await app.evaluate(() => process.stdout.listenerCount('error'));
expect.soft(listenerCount).toBeGreaterThan(0);
});

test('app survives an async EPIPE error on stdout', async ({ app }) => {
// Schedule the emit via nextTick so it fires outside evaluate()'s synchronous
// context — mirroring the real scenario where EPIPE arrives as an async I/O
// event from the OS. Without the fix, the throw escapes into the event loop
// as an uncaught exception and crashes the main process.
await app.evaluate(() => {
process.nextTick(() => {
const err: NodeJS.ErrnoException = new Error('write EPIPE');
err.code = 'EPIPE';
process.stdout.emit('error', err);
});
});

// Flush the main-process event loop before asserting — setImmediate fires
// after all I/O events in the current iteration, confirming the error event
// was fully handled without triggering an uncaught exception.
await app.evaluate(() => new Promise<void>(resolve => setImmediate(resolve)));

// The app must still be alive. If the main process crashed from an uncaught
// EPIPE exception, evaluate() would throw with a "Target closed" error.
const alive = await app.evaluate(() => true);
expect.soft(alive).toBe(true);
});
});
2 changes: 1 addition & 1 deletion packages/insomnia/src/entry.main.ts
Original file line number Diff line number Diff line change
Expand Up @@ -50,9 +50,9 @@ const dataPath =
path.join(app.getPath('userData'), '../', isDevelopment() ? 'insomnia-app' : userDataFolder);

app.setPath('userData', dataPath);
initElectronStorage(dataPath);

initializeLogging();
initElectronStorage(dataPath);

initializeSentry();

Expand Down
11 changes: 11 additions & 0 deletions packages/insomnia/src/main/log.ts
Original file line number Diff line number Diff line change
Expand Up @@ -8,6 +8,17 @@ import { isDevelopment } from '../common/constants';
log.initialize();

export const initializeLogging = () => {
// EPIPE is emitted asynchronously on process.stdout when the read end of the
// pipe is closed (e.g. launched from a desktop entry, or `app | head -0`).
// It surfaces as an 'error' event after the write is dispatched, so it
// escapes any try/catch around the console.log call itself. Without a
// handler it becomes an uncaught exception that crashes the main process.
process.stdout.on('error', (err: NodeJS.ErrnoException) => {
if (err.code !== 'EPIPE') {
throw err;
}
});

if (isDevelopment()) {
// Disable file logging during development
log.transports.file.level = false;
Expand Down
Loading