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
9 changes: 9 additions & 0 deletions src/handlers/terminal-handlers.ts
Original file line number Diff line number Diff line change
@@ -1,6 +1,7 @@
import {
startProcess,
readProcessOutput,
readProcessOutput2,
interactWithProcess,
forceTerminate,
listSessions
Expand Down Expand Up @@ -32,6 +33,14 @@ export async function handleReadProcessOutput(args: unknown): Promise<ServerResu
return readProcessOutput(parsed);
}

/**
* Handle read_process_output2 command - alternative behavior for running processes
*/
export async function handleReadProcessOutput2(args: unknown): Promise<ServerResult> {
const parsed = ReadProcessOutputArgsSchema.parse(args);
return readProcessOutput2(parsed);
}

/**
* Handle interact_with_process command (improved send_input)
*/
Expand Down
34 changes: 34 additions & 0 deletions src/server.ts
Original file line number Diff line number Diff line change
Expand Up @@ -582,6 +582,36 @@ server.setRequestHandler(ListToolsRequestSchema, async () => {
${CMD_PREFIX_DESCRIPTION}`,
inputSchema: zodToJsonSchema(ReadProcessOutputArgsSchema),
},
{
name: "read_process_output2",
description: `
Read output from a running process - Version 2 with different timeout behavior.

DIFFERENT BEHAVIOR FROM read_process_output:
- Only exits early for processes waiting for input or finished processes
- For running processes, waits until timeout to collect complete output
- Useful when you want to collect all output from long-running operations

SMART FEATURES:
- Immediate exit when process is waiting for input (>>>, >, etc.)
- Immediate exit when process is finished
- For running processes: collects output until timeout (no early exit)
- Clear status messages about collection strategy

USE CASES:
- Long compilation or build processes
- Data processing operations that output progress
- When you want complete output rather than early response
- Operations where you prefer timeout-based completion

DETECTION STATES:
Process waiting for input (immediate return)
Process finished execution (immediate return)
Timeout reached with complete output collection

${CMD_PREFIX_DESCRIPTION}`,
inputSchema: zodToJsonSchema(ReadProcessOutputArgsSchema),
},
{
name: "interact_with_process",
description: `
Expand Down Expand Up @@ -941,6 +971,10 @@ server.setRequestHandler(CallToolRequestSchema, async (request: CallToolRequest)
case "read_process_output":
result = await handlers.handleReadProcessOutput(args);
break;

case "read_process_output2":
result = await handlers.handleReadProcessOutput2(args);
break;

case "interact_with_process":
result = await handlers.handleInteractWithProcess(args);
Expand Down
143 changes: 143 additions & 0 deletions src/tools/improved-process-tools.ts
Original file line number Diff line number Diff line change
Expand Up @@ -277,6 +277,149 @@ export async function readProcessOutput(args: unknown): Promise<ServerResult> {
};
}

/**
* Read output from a running process - Version 2
* Different behavior: Only exits early for waiting input or finished processes.
* For running processes, waits until timeout to collect complete output.
*/
export async function readProcessOutput2(args: unknown): Promise<ServerResult> {
const parsed = ReadProcessOutputArgsSchema.safeParse(args);
if (!parsed.success) {
return {
content: [{ type: "text", text: `Error: Invalid arguments for read_process_output2: ${parsed.error}` }],
isError: true,
};
}

const { pid, timeout_ms = 5000 } = parsed.data;

const session = terminalManager.getSession(pid);
if (!session) {
// Check if this is a completed session
const completedOutput = terminalManager.getNewOutput(pid);
if (completedOutput) {
return {
content: [{
type: "text",
text: completedOutput
}],
};
}

// Neither active nor completed session found
return {
content: [{ type: "text", text: `No session found for PID ${pid}` }],
isError: true,
};
}

let output = "";
let timeoutReached = false;
let earlyExit = false;
let processState: ProcessState | undefined;

try {
const outputPromise: Promise<string> = new Promise<string>((resolve) => {
const initialOutput = terminalManager.getNewOutput(pid);
if (initialOutput && initialOutput.length > 0) {
// Immediate check on existing output - only exit early for finished/waiting states
const state = analyzeProcessState(initialOutput, pid);
if (state.isWaitingForInput || state.isFinished) {
earlyExit = true;
processState = state;
resolve(initialOutput);
return;
}
// If not waiting/finished, fall through to periodic collection
}

let resolved = false;
let interval: NodeJS.Timeout | null = null;
let timeout: NodeJS.Timeout | null = null;
let collectedOutput = initialOutput || "";

const cleanup = () => {
if (interval) clearInterval(interval);
if (timeout) clearTimeout(timeout);
};

let resolveOnce = (value: string, isTimeout = false) => {
if (resolved) return;
resolved = true;
cleanup();
timeoutReached = isTimeout;
resolve(value);
};

// Periodic collection without early exit for running processes
interval = setInterval(() => {
const newOutput = terminalManager.getNewOutput(pid);
if (newOutput && newOutput.length > 0) {
collectedOutput += newOutput;

// Analyze current state
const state = analyzeProcessState(collectedOutput, pid);

// Only exit early for waiting input or finished states
if (state.isWaitingForInput || state.isFinished) {
earlyExit = true;
processState = state;
resolveOnce(collectedOutput);
return;
}

// For running processes, continue collecting until timeout
// Don't update output here, just continue collecting
}
}, 200); // Check every 200ms

// Timeout handler - return whatever we've collected
timeout = setTimeout(() => {
const finalOutput = terminalManager.getNewOutput(pid);
if (finalOutput) {
collectedOutput += finalOutput;
}
resolveOnce(collectedOutput, true);
}, timeout_ms);
});

const newOutput = await outputPromise;
output += newOutput;

// Analyze final state if not already done
if (!processState) {
processState = analyzeProcessState(output, pid);
}

} catch (error) {
return {
content: [{ type: "text", text: `Error reading output: ${error}` }],
isError: true,
};
}

// Format response based on what we detected
let statusMessage = '';
if (earlyExit && processState?.isWaitingForInput) {
statusMessage = `\n🔄 ${formatProcessStateMessage(processState, pid)}`;
} else if (earlyExit && processState?.isFinished) {
statusMessage = `\n✅ ${formatProcessStateMessage(processState, pid)}`;
} else if (timeoutReached) {
statusMessage = '\n⏱️ Timeout reached - collected all available output';
} else {
statusMessage = '\n📊 Output collection completed';
}

const responseText = output || 'No new output available';

return {
content: [{
type: "text",
text: `${responseText}${statusMessage}`
}],
};
}

/**
* Interact with a running process (renamed from send_input)
* Automatically detects when process is ready and returns output
Expand Down
128 changes: 128 additions & 0 deletions test/test-read-process-output2.js
Original file line number Diff line number Diff line change
@@ -0,0 +1,128 @@
import assert from 'assert';
import { startProcess, readProcessOutput, readProcessOutput2 } from '../dist/tools/improved-process-tools.js';

/**
* Test readProcessOutput2 behavior compared to readProcessOutput
*/
async function testReadProcessOutput2Behavior() {
console.log('🧪 Testing readProcessOutput2 behavior...');

try {
// Test 1: Quick command - both should behave similarly
console.log('\n📝 Test 1: Quick command (echo)');
const echoResult = await startProcess({
command: 'echo "Hello World"',
timeout_ms: 2000
});

if (echoResult.isError) {
throw new Error('Failed to start echo process');
}

const echoPid = extractPid(echoResult.content[0].text);
console.log(`Started echo process with PID: ${echoPid}`);

// Wait a bit then try both functions
await new Promise(resolve => setTimeout(resolve, 1000));

const output1 = await readProcessOutput({ pid: echoPid, timeout_ms: 2000 });
const output2 = await readProcessOutput2({ pid: echoPid, timeout_ms: 2000 });

console.log('📊 readProcessOutput:', output1.content[0].text.substring(0, 100));
console.log('📊 readProcessOutput2:', output2.content[0].text.substring(0, 100));

// Test 2: Sleep command - should show different behavior
console.log('\n📝 Test 2: Sleep command (longer running)');
const sleepResult = await startProcess({
command: 'sleep 3 && echo "Sleep finished"',
timeout_ms: 1000
});

if (sleepResult.isError) {
throw new Error('Failed to start sleep process');
}

const sleepPid = extractPid(sleepResult.content[0].text);
console.log(`Started sleep process with PID: ${sleepPid}`);

// Test readProcessOutput with short timeout (should timeout early)
console.log('Testing readProcessOutput with 2s timeout...');
const startTime1 = Date.now();
const sleepOutput1 = await readProcessOutput({ pid: sleepPid, timeout_ms: 2000 });
const elapsed1 = Date.now() - startTime1;
console.log(`📊 readProcessOutput took ${elapsed1}ms`);
console.log('Result:', sleepOutput1.content[0].text.substring(0, 100));

// Test readProcessOutput2 with same timeout (should also timeout but collect more)
console.log('Testing readProcessOutput2 with 2s timeout...');
const startTime2 = Date.now();
const sleepOutput2 = await readProcessOutput2({ pid: sleepPid, timeout_ms: 2000 });
const elapsed2 = Date.now() - startTime2;
console.log(`📊 readProcessOutput2 took ${elapsed2}ms`);
console.log('Result:', sleepOutput2.content[0].text.substring(0, 100));

// Test 3: Python REPL - should both detect waiting for input immediately
console.log('\n📝 Test 3: Python REPL (should detect waiting for input)');
const pythonResult = await startProcess({
command: 'python3 -i',
timeout_ms: 3000
});

if (pythonResult.isError) {
console.log('Python not available, skipping REPL test');
} else {
const pythonPid = extractPid(pythonResult.content[0].text);
console.log(`Started Python REPL with PID: ${pythonPid}`);

Comment on lines +64 to +76
Copy link
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

⚠️ Potential issue

Terminate the Python REPL to avoid leaking processes.

Add cleanup via forceTerminate when REPL is started.

Apply:

-import { startProcess, readProcessOutput, readProcessOutput2 } from '../dist/tools/improved-process-tools.js';
+import { startProcess, readProcessOutput, readProcessOutput2, forceTerminate } from '../dist/tools/improved-process-tools.js';
@@
-    } else {
+    } else {
       const pythonPid = extractPid(pythonResult.content[0].text);
       console.log(`Started Python REPL with PID: ${pythonPid}`);
@@
-      // Check if either one detected the prompt correctly
+      // Check if either one detected the prompt correctly
       if (elapsed4 < 2000) {
         console.log('✅ readProcessOutput2 detected REPL prompt correctly');
       } else if (elapsed3 < 2000) {
         console.log('✅ readProcessOutput detected REPL prompt correctly');
       } else {
         console.log('❌ Neither function detected REPL prompt correctly');
         throw new Error('Neither function detected REPL prompt correctly');
       }
+      // Cleanup REPL
+      try {
+        await forceTerminate({ pid: pythonPid });
+      } catch (_) {
+        // best-effort
+      }
     }

Also applies to: 80-104

// Wait a moment for Python to fully start
await new Promise(resolve => setTimeout(resolve, 1000));

// Test readProcessOutput2 first this time
const startTime4 = Date.now();
const pythonOutput2 = await readProcessOutput2({ pid: pythonPid, timeout_ms: 3000 });
const elapsed4 = Date.now() - startTime4;
console.log(`📊 readProcessOutput2 took ${elapsed4}ms for REPL`);
console.log('Result:', pythonOutput2.content[0].text.substring(0, 200));
console.log('Full result length:', pythonOutput2.content[0].text.length);

const startTime3 = Date.now();
const pythonOutput1 = await readProcessOutput({ pid: pythonPid, timeout_ms: 3000 });
const elapsed3 = Date.now() - startTime3;
console.log(`📊 readProcessOutput took ${elapsed3}ms for REPL`);
console.log('Result:', pythonOutput1.content[0].text.substring(0, 200));
console.log('Full result length:', pythonOutput1.content[0].text.length);

// Check if either one detected the prompt correctly
if (elapsed4 < 2000) {
console.log('✅ readProcessOutput2 detected REPL prompt correctly');
} else if (elapsed3 < 2000) {
console.log('✅ readProcessOutput detected REPL prompt correctly');
} else {
console.log('❌ Neither function detected REPL prompt correctly');
throw new Error('Neither function detected REPL prompt correctly');
}
}

console.log('\n✅ All tests passed! readProcessOutput2 is working correctly.');
return true;

} catch (error) {
console.error('❌ Test failed:', error);
return false;
}
}

function extractPid(text) {
const match = text.match(/PID (\d+)/);
return match ? parseInt(match[1]) : null;
}

// Run the test
testReadProcessOutput2Behavior()
.then(success => {
process.exit(success ? 0 : 1);
})
.catch(error => {
console.error('Test error:', error);
process.exit(1);
});