Skip to content

Commit 7d5655d

Browse files
brettburleyFrozenPandaz
authored andcommitted
fix(core): clean up stale socket files before listening (#34236)
## Current Behavior When running Nx tasks in CI environments (e.g., Buildkite) where the host's /tmp is mounted to containers, intermittent EADDRINUSE errors occur in PseudoIPCServer.init(). This happens because: 1. PseudoIPCServer doesn't clean up its Unix socket file before calling listen() 2. ForkedProcessTaskRunner.createPseudoTerminal() instantiates PseudoTerminal directly instead of using the createPseudoTerminal() helper, bypassing shutdown callback registration When a new container starts with the same PID as a previous run (PID recycling), it generates the same socket path and hits EADDRINUSE because the stale socket file still exists. ## Expected Behavior No EADDRINUSE errors should occur. The PseudoIPCServer should defensively remove any stale socket file before attempting to listen, similar to how the daemon server handles this. ## Related Issue(s) Fixes #34233 (cherry picked from commit f5a7ea1)
1 parent 95fceae commit 7d5655d

3 files changed

Lines changed: 80 additions & 2 deletions

File tree

packages/nx/src/tasks-runner/forked-process-task-runner.ts

Lines changed: 7 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -10,7 +10,11 @@ import { stripIndents } from '../utils/strip-indents';
1010
import { BatchMessageType } from './batch/batch-messages';
1111
import { DefaultTasksRunnerOptions } from './default-tasks-runner';
1212
import { getProcessMetricsService } from './process-metrics-service';
13-
import { PseudoTerminal, PseudoTtyProcess } from './pseudo-terminal';
13+
import {
14+
createPseudoTerminal as createPseudoTerminalWithShutdown,
15+
PseudoTerminal,
16+
PseudoTtyProcess,
17+
} from './pseudo-terminal';
1418
import { BatchProcess } from './running-tasks/batch-process';
1519
import {
1620
NodeChildProcessWithDirectOutput,
@@ -186,7 +190,8 @@ export class ForkedProcessTaskRunner {
186190
}
187191

188192
private async createPseudoTerminal() {
189-
const terminal = new PseudoTerminal(new RustPseudoTerminal());
193+
// Use the helper to ensure shutdown callbacks are registered
194+
const terminal = createPseudoTerminalWithShutdown(true);
190195

191196
await terminal.init();
192197

Lines changed: 56 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,56 @@
1+
import { PseudoIPCServer } from './pseudo-ipc';
2+
import { existsSync, writeFileSync, unlinkSync, mkdirSync } from 'fs';
3+
import { join } from 'path';
4+
import { tmpdir } from 'os';
5+
6+
describe('PseudoIPCServer', () => {
7+
const testSocketDir = join(tmpdir(), 'nx-pseudo-ipc-test');
8+
let socketPath: string;
9+
10+
beforeAll(() => {
11+
try {
12+
mkdirSync(testSocketDir, { recursive: true });
13+
} catch {}
14+
});
15+
16+
beforeEach(() => {
17+
socketPath = join(testSocketDir, `test-${process.pid}-${Date.now()}.sock`);
18+
});
19+
20+
afterEach(() => {
21+
try {
22+
unlinkSync(socketPath);
23+
} catch {}
24+
});
25+
26+
it('should clean up stale socket file before listening', async () => {
27+
// Create a fake stale socket file
28+
writeFileSync(socketPath, '');
29+
expect(existsSync(socketPath)).toBe(true);
30+
31+
const server = new PseudoIPCServer(socketPath);
32+
await server.init();
33+
34+
// Should have successfully listened (stale file was cleaned up)
35+
// The socket file should exist again (created by the server)
36+
expect(existsSync(socketPath)).toBe(true);
37+
38+
server.close();
39+
});
40+
41+
it('should work when no stale socket file exists', async () => {
42+
// Ensure no file exists
43+
try {
44+
unlinkSync(socketPath);
45+
} catch {}
46+
expect(existsSync(socketPath)).toBe(false);
47+
48+
const server = new PseudoIPCServer(socketPath);
49+
await server.init();
50+
51+
// Should have successfully listened
52+
expect(existsSync(socketPath)).toBe(true);
53+
54+
server.close();
55+
});
56+
});

packages/nx/src/tasks-runner/pseudo-ipc.ts

Lines changed: 17 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -18,11 +18,26 @@
1818
*/
1919

2020
import { connect, Server, Socket } from 'net';
21+
import { unlinkSync } from 'fs';
2122
import {
2223
consumeMessagesFromSocket,
2324
MESSAGE_END_SEQ,
2425
} from '../utils/consume-messages-from-socket';
2526
import { Serializable } from 'child_process';
27+
import { isWindows } from '../daemon/socket-utils';
28+
29+
/**
30+
* Remove a stale socket file if it exists.
31+
* This handles cases where a previous process with the same PID
32+
* left behind a socket file (e.g., due to PID recycling in containers).
33+
*/
34+
function cleanupSocketFile(path: string): void {
35+
if (!isWindows) {
36+
try {
37+
unlinkSync(path);
38+
} catch {}
39+
}
40+
}
2641

2742
export interface PseudoIPCMessage {
2843
type: 'TO_CHILDREN_FROM_PARENT' | 'TO_PARENT_FROM_CHILDREN' | 'CHILD_READY';
@@ -44,6 +59,8 @@ export class PseudoIPCServer {
4459

4560
init(): Promise<void> {
4661
return new Promise((res) => {
62+
cleanupSocketFile(this.path);
63+
4764
this.server = new Server((socket) => {
4865
this.sockets.add(socket);
4966
this.registerChildMessages(socket);

0 commit comments

Comments
 (0)