fix hanging stdio servers (#1200)
Co-authored-by: Johnny Santos <johnnyadsantos@gmail.com> Co-authored-by: TomasRup <tomasr@wix.com>
This commit is contained in:
parent
5aaf6ee67e
commit
ace9b62d3c
1 changed files with 220 additions and 47 deletions
113
src/integration-tests/processCleanup.test.ts
Normal file
113
src/integration-tests/processCleanup.test.ts
Normal file
|
|
@ -0,0 +1,113 @@
|
|||
import path from 'node:path';
|
||||
import { Readable, Writable } from 'node:stream';
|
||||
import { Client } from '../client/index.js';
|
||||
import { StdioClientTransport } from '../client/stdio.js';
|
||||
import { Server } from '../server/index.js';
|
||||
import { StdioServerTransport } from '../server/stdio.js';
|
||||
import { LoggingMessageNotificationSchema } from '../types.js';
|
||||
|
||||
const FIXTURES_DIR = path.resolve(__dirname, '../__fixtures__');
|
||||
|
||||
describe('Process cleanup', () => {
|
||||
vi.setConfig({ testTimeout: 5000 }); // 5 second timeout
|
||||
|
||||
it('server should exit cleanly after closing transport', async () => {
|
||||
const server = new Server(
|
||||
{
|
||||
name: 'test-server',
|
||||
version: '1.0.0'
|
||||
},
|
||||
{
|
||||
capabilities: {}
|
||||
}
|
||||
);
|
||||
|
||||
const mockReadable = new Readable({
|
||||
read() {
|
||||
this.push(null); // signal EOF
|
||||
}
|
||||
}),
|
||||
mockWritable = new Writable({
|
||||
write(chunk, encoding, callback) {
|
||||
callback();
|
||||
}
|
||||
});
|
||||
|
||||
// Attach mock streams to process for the server transport
|
||||
const transport = new StdioServerTransport(mockReadable, mockWritable);
|
||||
await server.connect(transport);
|
||||
|
||||
// Close the transport
|
||||
await transport.close();
|
||||
|
||||
// ensure a proper disposal mock streams
|
||||
mockReadable.destroy();
|
||||
mockWritable.destroy();
|
||||
|
||||
// If we reach here without hanging, the test passes
|
||||
// The test runner will fail if the process hangs
|
||||
expect(true).toBe(true);
|
||||
});
|
||||
|
||||
it('onclose should be called exactly once', async () => {
|
||||
const client = new Client({
|
||||
name: 'test-client',
|
||||
version: '1.0.0'
|
||||
});
|
||||
|
||||
const transport = new StdioClientTransport({
|
||||
command: 'node',
|
||||
args: ['--import', 'tsx', 'testServer.ts'],
|
||||
cwd: FIXTURES_DIR
|
||||
});
|
||||
|
||||
await client.connect(transport);
|
||||
|
||||
let onCloseWasCalled = 0;
|
||||
client.onclose = () => {
|
||||
onCloseWasCalled++;
|
||||
};
|
||||
|
||||
await client.close();
|
||||
|
||||
// A short delay to allow the close event to propagate
|
||||
await new Promise(resolve => setTimeout(resolve, 50));
|
||||
|
||||
expect(onCloseWasCalled).toBe(1);
|
||||
});
|
||||
|
||||
it('should exit cleanly for a server that hangs', async () => {
|
||||
const client = new Client({
|
||||
name: 'test-client',
|
||||
version: '1.0.0'
|
||||
});
|
||||
|
||||
const transport = new StdioClientTransport({
|
||||
command: 'node',
|
||||
args: ['--import', 'tsx', 'serverThatHangs.ts'],
|
||||
cwd: FIXTURES_DIR
|
||||
});
|
||||
|
||||
await client.connect(transport);
|
||||
await client.setLoggingLevel('debug');
|
||||
client.setNotificationHandler(LoggingMessageNotificationSchema, notification => {
|
||||
console.debug('server log: ' + notification.params.data);
|
||||
});
|
||||
const serverPid = transport.pid!;
|
||||
|
||||
await client.close();
|
||||
|
||||
// A short delay to allow the close event to propagate
|
||||
await new Promise(resolve => setTimeout(resolve, 50));
|
||||
|
||||
try {
|
||||
process.kill(serverPid, 9);
|
||||
throw new Error('Expected server to be dead but it is alive');
|
||||
} catch (err: unknown) {
|
||||
// 'ESRCH' the process doesn't exist
|
||||
if (err && typeof err === 'object' && 'code' in err && err.code === 'ESRCH') {
|
||||
// success
|
||||
} else throw err;
|
||||
}
|
||||
});
|
||||
});
|
||||
Loading…
Reference in a new issue