Fix PTY session operations to support external sessions

- 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 <noreply@anthropic.com>
This commit is contained in:
Mario Zechner 2025-06-19 05:51:00 +02:00
parent a229a7f360
commit 0fcddd2194

View file

@ -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<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);
}
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(