Skip to content
Open
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
7 changes: 4 additions & 3 deletions src/terminal-manager.ts
Original file line number Diff line number Diff line change
Expand Up @@ -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
Expand Down Expand Up @@ -626,4 +627,4 @@ export class TerminalManager {
}
}

export const terminalManager = new TerminalManager();
export const terminalManager = new TerminalManager();
79 changes: 79 additions & 0 deletions test/test-process-output-close.js
Original file line number Diff line number Diff line change
@@ -0,0 +1,79 @@
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';

const TEST_DIR_PREFIX = path.join(os.tmpdir(), 'desktop-commander-process-output-close-');

function quoteForShell(value) {
return `"${value.replace(/"/g, '\\"')}"`;
}

async function setup() {
const testDir = await fs.mkdtemp(TEST_DIR_PREFIX);
const scriptPath = path.join(testDir, 'fast-stderr.mjs');
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) {
await fs.rm(testDir, { recursive: true, force: true });
Comment thread
coderabbitai[bot] marked this conversation as resolved.
}

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');

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() {
let testDir;
try {
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 {
if (testDir) {
await teardown(testDir);
}
}
}

if (process.argv[1] && import.meta.url === pathToFileURL(process.argv[1]).href) {
runTests().then((ok) => {
process.exit(ok ? 0 : 1);
});
}