From 0fcddd21943da467c19e9fd40a0e482486a549c4 Mon Sep 17 00:00:00 2001 From: Mario Zechner Date: Thu, 19 Jun 2025 05:51:00 +0200 Subject: [PATCH] Fix PTY session operations to support external sessions MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit - Modify sendInput to write to stdin pipe for external sessions - Update resizeSession to handle external sessions gracefully - Fix killSession to work with sessions created by fwd.ts - Check filesystem for session info when not in memory map - Support both in-memory and disk-only session management This fixes frontend operations failing with "Session not found" when interacting with sessions created by external tools. 🤖 Generated with [Claude Code](https://claude.ai/code) Co-Authored-By: Claude --- web/src/pty/PtyManager.ts | 114 +++++++++++++++++++++++++++++--------- 1 file changed, 88 insertions(+), 26 deletions(-) diff --git a/web/src/pty/PtyManager.ts b/web/src/pty/PtyManager.ts index 89bdf8ea..2cb8e7b6 100644 --- a/web/src/pty/PtyManager.ts +++ b/web/src/pty/PtyManager.ts @@ -265,13 +265,13 @@ export class PtyManager { * Send text input to a session */ sendInput(sessionId: string, input: SessionInput): void { - const session = this.sessions.get(sessionId); - if (!session) { - throw new PtyError(`Session ${sessionId} not found`, 'SESSION_NOT_FOUND', sessionId); - } + // First try to get session from memory (for sessions we created) + const memorySession = this.sessions.get(sessionId); - if (!session.ptyProcess) { - throw new PtyError(`Session ${sessionId} has no active PTY`, 'NO_ACTIVE_PTY', sessionId); + // If not in memory, check if session exists on filesystem + const diskSession = this.sessionManager.getSession(sessionId); + if (!diskSession) { + throw new PtyError(`Session ${sessionId} not found`, 'SESSION_NOT_FOUND', sessionId); } try { @@ -285,11 +285,23 @@ export class PtyManager { throw new PtyError('No text or key specified in input', 'INVALID_INPUT'); } - // Send to PTY - session.ptyProcess.write(dataToSend); - - // Record input in asciinema - session.asciinemaWriter?.writeInput(dataToSend); + // If we have an in-memory session with active PTY, use it + if (memorySession?.ptyProcess) { + memorySession.ptyProcess.write(dataToSend); + memorySession.asciinemaWriter?.writeInput(dataToSend); + } else { + // Otherwise, write to the session's stdin pipe + const stdinPath = diskSession.stdin; + if (stdinPath && fs.existsSync(stdinPath)) { + fs.writeFileSync(stdinPath, dataToSend); + } else { + throw new PtyError( + `Session ${sessionId} stdin pipe not found at ${stdinPath}`, + 'STDIN_NOT_FOUND', + sessionId + ); + } + } } catch (error) { throw new PtyError( `Failed to send input to session ${sessionId}: ${error instanceof Error ? error.message : String(error)}`, @@ -326,18 +338,27 @@ export class PtyManager { * Resize a session terminal */ resizeSession(sessionId: string, cols: number, rows: number): void { - const session = this.sessions.get(sessionId); - if (!session) { + // First try to get session from memory (for sessions we created) + const memorySession = this.sessions.get(sessionId); + + // If not in memory, check if session exists on filesystem + const diskSession = this.sessionManager.getSession(sessionId); + if (!diskSession) { throw new PtyError(`Session ${sessionId} not found`, 'SESSION_NOT_FOUND', sessionId); } - if (!session.ptyProcess) { - throw new PtyError(`Session ${sessionId} has no active PTY`, 'NO_ACTIVE_PTY', sessionId); - } - try { - session.ptyProcess.resize(cols, rows); - session.asciinemaWriter?.writeResize(cols, rows); + // If we have an in-memory session with active PTY, resize it + if (memorySession?.ptyProcess) { + memorySession.ptyProcess.resize(cols, rows); + memorySession.asciinemaWriter?.writeResize(cols, rows); + } else { + // For external sessions, we can't directly resize the PTY + // but we don't throw an error - the session should handle SIGWINCH automatically + console.log( + `Cannot resize external session ${sessionId} directly, PTY should handle SIGWINCH automatically` + ); + } } catch (error) { throw new PtyError( `Failed to resize session ${sessionId}: ${error instanceof Error ? error.message : String(error)}`, @@ -352,16 +373,21 @@ export class PtyManager { * Returns a promise that resolves when the process is actually terminated */ async killSession(sessionId: string, signal: string | number = 'SIGTERM'): Promise { - const session = this.sessions.get(sessionId); - if (!session) { + // First try to get session from memory (for sessions we created) + const memorySession = this.sessions.get(sessionId); + + // If not in memory, check if session exists on filesystem + const diskSession = this.sessionManager.getSession(sessionId); + if (!diskSession) { throw new PtyError(`Session ${sessionId} not found`, 'SESSION_NOT_FOUND', sessionId); } try { - if (session.ptyProcess) { + // If we have an in-memory session with active PTY, kill it directly + if (memorySession?.ptyProcess) { // If signal is already SIGKILL, send it immediately and wait briefly if (signal === 'SIGKILL' || signal === 9) { - session.ptyProcess.kill('SIGKILL'); + memorySession.ptyProcess.kill('SIGKILL'); this.sessions.delete(sessionId); // Wait a bit for SIGKILL to take effect await new Promise((resolve) => setTimeout(resolve, 100)); @@ -369,10 +395,46 @@ export class PtyManager { } // Start with SIGTERM and escalate if needed - await this.killSessionWithEscalation(sessionId, session); + await this.killSessionWithEscalation(sessionId, memorySession); } else { - // No PTY process, just remove from sessions - this.sessions.delete(sessionId); + // For external sessions, kill by PID + if (diskSession.pid && ProcessUtils.isProcessRunning(diskSession.pid)) { + console.log( + `Killing external session ${sessionId} (PID: ${diskSession.pid}) with ${signal}...` + ); + + if (signal === 'SIGKILL' || signal === 9) { + process.kill(diskSession.pid, 'SIGKILL'); + await new Promise((resolve) => setTimeout(resolve, 100)); + return; + } + + // Send SIGTERM first + process.kill(diskSession.pid, 'SIGTERM'); + + // Wait up to 3 seconds for graceful termination + const maxWaitTime = 3000; + const checkInterval = 500; + const maxChecks = maxWaitTime / checkInterval; + + for (let i = 0; i < maxChecks; i++) { + await new Promise((resolve) => setTimeout(resolve, checkInterval)); + + if (!ProcessUtils.isProcessRunning(diskSession.pid)) { + console.log( + `External session ${sessionId} terminated gracefully after ${(i + 1) * checkInterval}ms` + ); + return; + } + } + + // Process didn't terminate gracefully, force kill + console.log( + `External session ${sessionId} didn't terminate gracefully, sending SIGKILL...` + ); + process.kill(diskSession.pid, 'SIGKILL'); + await new Promise((resolve) => setTimeout(resolve, 100)); + } } } catch (error) { throw new PtyError(