Skip to content
Merged
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
6 changes: 4 additions & 2 deletions src/lib/remote-xpc/remote-xpc-framed-transport.ts
Original file line number Diff line number Diff line change
Expand Up @@ -135,11 +135,13 @@ export class RemoteXpcFramedTransport extends EventEmitter {
this.handleData(chunk);
});
socket.on('error', (error: Error) => {
if (this.closing) {
const wasConnected = this.connected;
this.connected = false;
if (this.closing || !wasConnected) {
log.debug(`RemoteXPC transport error outside connected phase: ${error.message}`);
return;
}
log.error(`RemoteXPC transport error: ${error.message}`);
this.connected = false;
this.emit('error', error);
});
socket.on('close', () => {
Expand Down
57 changes: 57 additions & 0 deletions test/unit/remote-xpc/remote-xpc-framed-transport.spec.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1,57 @@
import assert from 'node:assert/strict';
import * as net from 'node:net';
import {describe, it} from 'node:test';

import {RemoteXpcFramedTransport} from '../../../src/lib/remote-xpc/remote-xpc-framed-transport.js';

function listenOnLoopback(server: net.Server): Promise<number> {
return new Promise<number>((resolve, reject) => {
server.once('error', reject);
server.listen(0, '::1', (): void => {
resolve((server.address() as net.AddressInfo).port);
});
});
}

function closeServer(server: net.Server): Promise<void> {
return new Promise<void>((resolve) => server.close(() => resolve()));
}

describe('RemoteXpcFramedTransport', function () {
it('rejects connect() on socket failure instead of emitting an unlistened error', async function () {
const probe = net.createServer();
const port = await listenOnLoopback(probe);
await closeServer(probe);

const transport = new RemoteXpcFramedTransport(['::1', port]);
await assert.rejects(transport.connect({timeoutMs: 2000}), /ECONNREFUSED/);
});

it('emits error when the socket fails in the connected phase', async function () {
let onAccepted: (socket: net.Socket) => void = () => undefined;
const acceptedPromise = new Promise<net.Socket>((resolve) => {
onAccepted = resolve;
});
const server = net.createServer((socket): void => {
onAccepted(socket);
});
const port = await listenOnLoopback(server);

let acceptedSocket: net.Socket | undefined;
const transport = new RemoteXpcFramedTransport(['::1', port]);
try {
await transport.connect({timeoutMs: 2000});
acceptedSocket = await acceptedPromise;

const errorPromise = new Promise<Error>((resolve) => transport.once('error', resolve));
acceptedSocket.resetAndDestroy();

const error = await errorPromise;
assert.match(error.message, /ECONNRESET/);
} finally {
await transport.close();
acceptedSocket?.destroy();
await closeServer(server);
}
});
});