From dac9a1cc1939a8144470d9f2a9733664e67400f9 Mon Sep 17 00:00:00 2001 From: showiix <2138757206@qq.com> Date: Wed, 29 Apr 2026 09:39:13 +0800 Subject: [PATCH 1/3] fix: wait for stdio close before completing processes --- src/terminal-manager.ts | 7 ++-- test/test-process-output-close.js | 68 +++++++++++++++++++++++++++++++ 2 files changed, 72 insertions(+), 3 deletions(-) create mode 100644 test/test-process-output-close.js diff --git a/src/terminal-manager.ts b/src/terminal-manager.ts index ee991709..d374f3b8 100644 --- a/src/terminal-manager.ts +++ b/src/terminal-manager.ts @@ -336,9 +336,10 @@ export class TerminalManager { }); }, timeoutMs); - childProcess.on('exit', (code: any) => { + childProcess.on('close', (code: any) => { if (childProcess.pid) { - // Store completed session before removing active session + // Store completed session only after stdio closes, so fast-exiting + // processes do not lose stdout/stderr that arrives after exit. this.completedSessions.set(childProcess.pid, { pid: childProcess.pid, outputLines: [...session.outputLines], // Copy line buffer @@ -626,4 +627,4 @@ export class TerminalManager { } } -export const terminalManager = new TerminalManager(); \ No newline at end of file +export const terminalManager = new TerminalManager(); diff --git a/test/test-process-output-close.js b/test/test-process-output-close.js new file mode 100644 index 00000000..27c6c3e4 --- /dev/null +++ b/test/test-process-output-close.js @@ -0,0 +1,68 @@ +import assert from 'assert'; +import fs from 'fs/promises'; +import os from 'os'; +import path from 'path'; + +import { terminalManager } from '../dist/terminal-manager.js'; + +const TEST_DIR = path.join(os.tmpdir(), 'desktop-commander-process-output-close'); +const SCRIPT_PATH = path.join(TEST_DIR, 'fast-stderr.js'); + +function quoteForShell(value) { + return `"${value.replace(/"/g, '\\"')}"`; +} + +async function setup() { + await fs.mkdir(TEST_DIR, { recursive: true }); + await fs.writeFile( + SCRIPT_PATH, + [ + "import fs from 'fs';", + "fs.writeSync(2, 'FAST_STDERR_START\\n');", + "fs.writeSync(2, 'x'.repeat(256 * 1024));", + "fs.writeSync(2, '\\nFAST_STDERR_END\\n');", + 'process.exit(1);', + ].join('\n'), + ); +} + +async function teardown() { + await fs.rm(TEST_DIR, { recursive: true, force: true }); +} + +async function testFastExitStderrIsFlushed() { + const command = `${quoteForShell(process.execPath)} ${quoteForShell(SCRIPT_PATH)}`; + const result = await terminalManager.executeCommand(command, 2000); + + assert.strictEqual(result.isBlocked, false, 'Fast-failing process should be marked complete'); + + const output = terminalManager.readOutputPaginated(result.pid, 0, 100); + assert(output, 'Completed process output should remain readable'); + + const text = output.lines.join('\n'); + assert.strictEqual(output.exitCode, 1, 'Process exit code should be preserved'); + assert(text.includes('FAST_STDERR_START'), 'stderr start marker should be captured'); + assert(text.includes('FAST_STDERR_END'), 'stderr end marker should be captured after process exit'); + + console.log('✓ fast-exiting process stderr is flushed before completion'); +} + +export default async function runTests() { + try { + await setup(); + await testFastExitStderrIsFlushed(); + console.log('\n✅ process output close tests passed!'); + return true; + } catch (error) { + console.error('❌ process output close test failed:', error instanceof Error ? error.message : String(error)); + return false; + } finally { + await teardown(); + } +} + +if (import.meta.url === `file://${process.argv[1]}`) { + runTests().then((ok) => { + process.exit(ok ? 0 : 1); + }); +} From c4c8550f80bf45e69f0b6be30c8870081123237d Mon Sep 17 00:00:00 2001 From: showiix <2138757206@qq.com> Date: Wed, 29 Apr 2026 09:45:51 +0800 Subject: [PATCH 2/3] test: use module script fixture for process output --- test/test-process-output-close.js | 27 ++++++++++++++++----------- 1 file changed, 16 insertions(+), 11 deletions(-) diff --git a/test/test-process-output-close.js b/test/test-process-output-close.js index 27c6c3e4..7e11b4f3 100644 --- a/test/test-process-output-close.js +++ b/test/test-process-output-close.js @@ -5,17 +5,17 @@ import path from 'path'; import { terminalManager } from '../dist/terminal-manager.js'; -const TEST_DIR = path.join(os.tmpdir(), 'desktop-commander-process-output-close'); -const SCRIPT_PATH = path.join(TEST_DIR, 'fast-stderr.js'); +const TEST_DIR_PREFIX = path.join(os.tmpdir(), 'desktop-commander-process-output-close-'); function quoteForShell(value) { return `"${value.replace(/"/g, '\\"')}"`; } async function setup() { - await fs.mkdir(TEST_DIR, { recursive: true }); + const testDir = await fs.mkdtemp(TEST_DIR_PREFIX); + const scriptPath = path.join(testDir, 'fast-stderr.mjs'); await fs.writeFile( - SCRIPT_PATH, + scriptPath, [ "import fs from 'fs';", "fs.writeSync(2, 'FAST_STDERR_START\\n');", @@ -24,14 +24,15 @@ async function setup() { 'process.exit(1);', ].join('\n'), ); + return { testDir, scriptPath }; } -async function teardown() { - await fs.rm(TEST_DIR, { recursive: true, force: true }); +async function teardown(testDir) { + await fs.rm(testDir, { recursive: true, force: true }); } -async function testFastExitStderrIsFlushed() { - const command = `${quoteForShell(process.execPath)} ${quoteForShell(SCRIPT_PATH)}`; +async function testFastExitStderrIsFlushed(scriptPath) { + const command = `${quoteForShell(process.execPath)} ${quoteForShell(scriptPath)}`; const result = await terminalManager.executeCommand(command, 2000); assert.strictEqual(result.isBlocked, false, 'Fast-failing process should be marked complete'); @@ -48,16 +49,20 @@ async function testFastExitStderrIsFlushed() { } export default async function runTests() { + let testDir; try { - await setup(); - await testFastExitStderrIsFlushed(); + const fixture = await setup(); + testDir = fixture.testDir; + await testFastExitStderrIsFlushed(fixture.scriptPath); console.log('\n✅ process output close tests passed!'); return true; } catch (error) { console.error('❌ process output close test failed:', error instanceof Error ? error.message : String(error)); return false; } finally { - await teardown(); + if (testDir) { + await teardown(testDir); + } } } From e07dbfe5d31afee1b8b190ad0e10b99d87f56daa Mon Sep 17 00:00:00 2001 From: showiix <2138757206@qq.com> Date: Wed, 29 Apr 2026 09:52:58 +0800 Subject: [PATCH 3/3] test: harden process output fixture cleanup --- test/test-process-output-close.js | 30 ++++++++++++++++++------------ 1 file changed, 18 insertions(+), 12 deletions(-) diff --git a/test/test-process-output-close.js b/test/test-process-output-close.js index 7e11b4f3..06a209bf 100644 --- a/test/test-process-output-close.js +++ b/test/test-process-output-close.js @@ -2,6 +2,7 @@ import assert from 'assert'; import fs from 'fs/promises'; import os from 'os'; import path from 'path'; +import { pathToFileURL } from 'url'; import { terminalManager } from '../dist/terminal-manager.js'; @@ -14,17 +15,22 @@ function quoteForShell(value) { async function setup() { const testDir = await fs.mkdtemp(TEST_DIR_PREFIX); const scriptPath = path.join(testDir, 'fast-stderr.mjs'); - await fs.writeFile( - scriptPath, - [ - "import fs from 'fs';", - "fs.writeSync(2, 'FAST_STDERR_START\\n');", - "fs.writeSync(2, 'x'.repeat(256 * 1024));", - "fs.writeSync(2, '\\nFAST_STDERR_END\\n');", - 'process.exit(1);', - ].join('\n'), - ); - return { testDir, scriptPath }; + try { + await fs.writeFile( + scriptPath, + [ + "import fs from 'fs';", + "fs.writeSync(2, 'FAST_STDERR_START\\n');", + "fs.writeSync(2, 'x'.repeat(256 * 1024));", + "fs.writeSync(2, '\\nFAST_STDERR_END\\n');", + 'process.exit(1);', + ].join('\n'), + ); + return { testDir, scriptPath }; + } catch (error) { + await fs.rm(testDir, { recursive: true, force: true }); + throw error; + } } async function teardown(testDir) { @@ -66,7 +72,7 @@ export default async function runTests() { } } -if (import.meta.url === `file://${process.argv[1]}`) { +if (process.argv[1] && import.meta.url === pathToFileURL(process.argv[1]).href) { runTests().then((ok) => { process.exit(ok ? 0 : 1); });