diff --git a/package-lock.json b/package-lock.json index d32963a73..30a051b54 100644 --- a/package-lock.json +++ b/package-lock.json @@ -1319,7 +1319,6 @@ "integrity": "sha512-PC0PDZfJg8sP7cmKe6L3QIL8GZwU5aRvUFedqSIpw3B+QjRSUZeeITC2M5XKeMXEzL6wccN196iy3JLwKNvDVA==", "dev": true, "license": "MIT", - "peer": true, "dependencies": { "@typescript-eslint/scope-manager": "8.48.1", "@typescript-eslint/types": "8.48.1", @@ -1751,7 +1750,6 @@ "integrity": "sha512-NZyJarBfL7nWwIq+FDL6Zp/yHEhePMNnnJ0y3qfieCrmNvYct8uvtiV41UvlSe6apAfk0fY1FbWx+NwfmpvtTg==", "dev": true, "license": "MIT", - "peer": true, "bin": { "acorn": "bin/acorn" }, @@ -2294,7 +2292,6 @@ "integrity": "sha512-BhHmn2yNOFA9H9JmmIVKJmd288g9hrVRDkdoIgRCRuSySRUHH7r/DI6aAXW9T1WwUuY3DFgrcaqB+deURBLR5g==", "dev": true, "license": "MIT", - "peer": true, "dependencies": { "@eslint-community/eslint-utils": "^4.8.0", "@eslint-community/regexpp": "^4.12.1", @@ -2613,7 +2610,6 @@ "resolved": "https://registry.npmjs.org/express/-/express-5.2.1.tgz", "integrity": "sha512-hIS4idWWai69NezIdRt2xFVofaF4j+6INOpJlVOLDO8zXGpUVEVzIYk12UUi2JzjEzWL3IOAxcTubgz9Po0yXw==", "license": "MIT", - "peer": true, "dependencies": { "accepts": "^2.0.0", "body-parser": "^2.2.1", @@ -4071,7 +4067,6 @@ "integrity": "sha512-5gTmgEY/sqK6gFXLIsQNH19lWb4ebPDLA4SdLP7dsWkIXHWlG66oPuVvXSGFPppYZz8ZDZq0dYYrbHfBCVUb1Q==", "dev": true, "license": "MIT", - "peer": true, "engines": { "node": ">=12" }, @@ -4152,7 +4147,6 @@ "integrity": "sha512-4H8vUNGNjQ4V2EOoGw005+c+dGuPSnhpPBPHBtsZdGZBk/iJb4kguGlPWaZTZ3q5nMtFOEsY0nRDlh9PJyd6SQ==", "dev": true, "license": "MIT", - "peer": true, "dependencies": { "esbuild": "~0.25.0", "get-tsconfig": "^4.7.5" @@ -4198,7 +4192,6 @@ "resolved": "https://registry.npmjs.org/typescript/-/typescript-5.6.3.tgz", "integrity": "sha512-hjcS1mhfuyi4WW8IWtjP7brDrG2cuDZukyrYrSauoXGNgx0S7zceP07adYkJycEr56BOUTNPzbInooiN3fn1qw==", "dev": true, - "peer": true, "bin": { "tsc": "bin/tsc", "tsserver": "bin/tsserver" @@ -4393,7 +4386,6 @@ "integrity": "sha512-5gTmgEY/sqK6gFXLIsQNH19lWb4ebPDLA4SdLP7dsWkIXHWlG66oPuVvXSGFPppYZz8ZDZq0dYYrbHfBCVUb1Q==", "dev": true, "license": "MIT", - "peer": true, "engines": { "node": ">=12" }, @@ -4407,7 +4399,6 @@ "integrity": "sha512-BxAKBWmIbrDgrokdGZH1IgkIk/5mMHDreLDmCJ0qpyJaAteP8NvMhkwr/ZCQNqNH97bw/dANTE9PDzqwJghfMQ==", "dev": true, "license": "MIT", - "peer": true, "dependencies": { "esbuild": "^0.25.0", "fdir": "^6.5.0", @@ -4560,7 +4551,6 @@ "resolved": "https://registry.npmjs.org/zod/-/zod-3.25.76.tgz", "integrity": "sha512-gzUt/qt81nXsFGKIFcC3YnfEAx5NkunCfnDlvuBSSFS02bcXu4Lmea0AFIUwbLWxWPx3d9p8S5QoaujKcNQxcQ==", "license": "MIT", - "peer": true, "funding": { "url": "https://github.com/sponsors/colinhacks" } diff --git a/src/client/stdio.ts b/src/client/stdio.ts index e488dcd24..f41c84ef7 100644 --- a/src/client/stdio.ts +++ b/src/client/stdio.ts @@ -94,6 +94,8 @@ export class StdioClientTransport implements Transport { private _readBuffer: ReadBuffer = new ReadBuffer(); private _serverParams: StdioServerParameters; private _stderrStream: PassThrough | null = null; + private _onServerDataHandler?: (chunk: Buffer) => void; + private _onServerErrorHandler?: (error: Error) => void; onclose?: () => void; onerror?: (error: Error) => void; @@ -129,33 +131,31 @@ export class StdioClientTransport implements Transport { cwd: this._serverParams.cwd }); + this._onServerDataHandler = (chunk: Buffer) => { + this._readBuffer.append(chunk); + this.processReadBuffer(); + }; + this._onServerErrorHandler = (error: Error) => { + this.onerror?.(error); + }; + + this._process.stdout?.on('data', this._onServerDataHandler); + this._process.stdout?.on('error', this._onServerErrorHandler); + this._process.stdin?.on('error', this._onServerErrorHandler); + this._process.on('error', error => { reject(error); this.onerror?.(error); }); - - this._process.on('spawn', () => { - resolve(); - }); - - this._process.on('close', _code => { + this._process.once('spawn', () => resolve()); + this._process.once('close', _code => { + if (this._process) { + this.cleanupListeners(this._process); + } this._process = undefined; this.onclose?.(); }); - this._process.stdin?.on('error', error => { - this.onerror?.(error); - }); - - this._process.stdout?.on('data', chunk => { - this._readBuffer.append(chunk); - this.processReadBuffer(); - }); - - this._process.stdout?.on('error', error => { - this.onerror?.(error); - }); - if (this._stderrStream && this._process.stderr) { this._process.stderr.pipe(this._stderrStream); } @@ -201,8 +201,19 @@ export class StdioClientTransport implements Transport { } } + private cleanupListeners(process: ChildProcess) { + if (this._onServerDataHandler) { + process.stdout?.off('data', this._onServerDataHandler); + } + if (this._onServerErrorHandler) { + process.stdout?.off('error', this._onServerErrorHandler); + process.stdin?.off('error', this._onServerErrorHandler); + } + } + async close(): Promise { if (this._process) { + this.cleanupListeners(this._process); const processToClose = this._process; this._process = undefined; diff --git a/test/client/cross-spawn.test.ts b/test/client/cross-spawn.test.ts index 26ae682fe..419b7d3ce 100644 --- a/test/client/cross-spawn.test.ts +++ b/test/client/cross-spawn.test.ts @@ -2,7 +2,7 @@ import { StdioClientTransport, getDefaultEnvironment } from '../../src/client/st import spawn from 'cross-spawn'; import { JSONRPCMessage } from '../../src/types.js'; import { ChildProcess } from 'node:child_process'; -import { Mock, MockedFunction } from 'vitest'; +import { Mock, MockedFunction, vi } from 'vitest'; // mock cross-spawn vi.mock('cross-spawn'); @@ -14,8 +14,9 @@ describe('StdioClientTransport using cross-spawn', () => { mockSpawn.mockImplementation(() => { const mockProcess: { on: Mock; - stdin?: { on: Mock; write: Mock }; - stdout?: { on: Mock }; + once: Mock; + stdin?: { on: Mock; write: Mock; off: Mock }; + stdout?: { on: Mock; off: Mock }; stderr?: null; } = { on: vi.fn((event: string, callback: () => void) => { @@ -24,12 +25,20 @@ describe('StdioClientTransport using cross-spawn', () => { } return mockProcess; }), + once: vi.fn((event: string, callback: () => void) => { + if (event === 'spawn') { + callback(); + } + return mockProcess; + }), stdin: { on: vi.fn(), - write: vi.fn().mockReturnValue(true) + write: vi.fn().mockReturnValue(true), + off: vi.fn() }, stdout: { - on: vi.fn() + on: vi.fn(), + off: vi.fn() }, stderr: null }; @@ -107,13 +116,16 @@ describe('StdioClientTransport using cross-spawn', () => { // get the mock process object const mockProcess: { on: Mock; + once: Mock; stdin: { on: Mock; write: Mock; once: Mock; + off: Mock; }; stdout: { on: Mock; + off: Mock; }; stderr: null; } = { @@ -123,13 +135,21 @@ describe('StdioClientTransport using cross-spawn', () => { } return mockProcess; }), + once: vi.fn((event: string, callback: () => void) => { + if (event === 'spawn') { + callback(); + } + return mockProcess; + }), stdin: { on: vi.fn(), write: vi.fn().mockReturnValue(true), - once: vi.fn() + once: vi.fn(), + off: vi.fn() }, stdout: { - on: vi.fn() + on: vi.fn(), + off: vi.fn() }, stderr: null };