mirror of
https://github.com/samsonjs/vibetunnel.git
synced 2026-04-27 15:17:38 +00:00
Fix fwd.ts hanging on process exit and remove redundant control pipe creation
- Add proper cleanup of all intervals and FIFO streams on exit - Detect exit events directly from asciinema stream for faster response - Reduce session monitoring polling from 1000ms to 500ms - Remove redundant createControlPipeForExternalSession function - fwd.ts now creates its own control pipe, PtyManager no longer needs to create them This should eliminate the hanging issue when exiting processes wrapped with fwd.ts. 🤖 Generated with [Claude Code](https://claude.ai/code) Co-Authored-By: Claude <noreply@anthropic.com>
This commit is contained in:
parent
69e3a6d47e
commit
dc562f5f9e
2 changed files with 69 additions and 72 deletions
|
|
@ -82,6 +82,10 @@ async function main() {
|
||||||
console.log(`Session created with ID: ${result.sessionId}`);
|
console.log(`Session created with ID: ${result.sessionId}`);
|
||||||
console.log(`Implementation: ${ptyService.getCurrentImplementation()}`);
|
console.log(`Implementation: ${ptyService.getCurrentImplementation()}`);
|
||||||
|
|
||||||
|
// Track all intervals and streams for cleanup
|
||||||
|
const intervals: NodeJS.Timeout[] = [];
|
||||||
|
const streams: any[] = [];
|
||||||
|
|
||||||
// Get session info
|
// Get session info
|
||||||
const session = ptyService.getSession(result.sessionId);
|
const session = ptyService.getSession(result.sessionId);
|
||||||
if (!session) {
|
if (!session) {
|
||||||
|
|
@ -152,6 +156,7 @@ async function main() {
|
||||||
// Unix FIFO approach
|
// Unix FIFO approach
|
||||||
const controlFd = fs.openSync(controlPath, 'r+');
|
const controlFd = fs.openSync(controlPath, 'r+');
|
||||||
const controlStream = fs.createReadStream('', { fd: controlFd, encoding: 'utf8' });
|
const controlStream = fs.createReadStream('', { fd: controlFd, encoding: 'utf8' });
|
||||||
|
streams.push(controlStream);
|
||||||
|
|
||||||
controlStream.on('data', (chunk: string | Buffer) => {
|
controlStream.on('data', (chunk: string | Buffer) => {
|
||||||
const data = chunk.toString('utf8');
|
const data = chunk.toString('utf8');
|
||||||
|
|
@ -224,18 +229,7 @@ async function main() {
|
||||||
|
|
||||||
// Poll every 100ms on Windows
|
// Poll every 100ms on Windows
|
||||||
const controlInterval = setInterval(pollControl, 100);
|
const controlInterval = setInterval(pollControl, 100);
|
||||||
|
intervals.push(controlInterval);
|
||||||
// Clean up control polling on exit
|
|
||||||
process.on('exit', () => {
|
|
||||||
clearInterval(controlInterval);
|
|
||||||
try {
|
|
||||||
if (fs.existsSync(controlPath)) {
|
|
||||||
fs.unlinkSync(controlPath);
|
|
||||||
}
|
|
||||||
} catch (_e) {
|
|
||||||
// Ignore cleanup errors
|
|
||||||
}
|
|
||||||
});
|
|
||||||
}
|
}
|
||||||
|
|
||||||
// Handle control messages
|
// Handle control messages
|
||||||
|
|
@ -307,6 +301,7 @@ async function main() {
|
||||||
// Open FIFO for both read and write (like tty-fwd) to keep it open
|
// Open FIFO for both read and write (like tty-fwd) to keep it open
|
||||||
const stdinFd = fs.openSync(stdinPath, 'r+'); // r+ = read/write
|
const stdinFd = fs.openSync(stdinPath, 'r+'); // r+ = read/write
|
||||||
const stdinStream = fs.createReadStream('', { fd: stdinFd, encoding: 'utf8' });
|
const stdinStream = fs.createReadStream('', { fd: stdinFd, encoding: 'utf8' });
|
||||||
|
streams.push(stdinStream);
|
||||||
|
|
||||||
stdinStream.on('data', (chunk: string | Buffer) => {
|
stdinStream.on('data', (chunk: string | Buffer) => {
|
||||||
const data = chunk.toString('utf8');
|
const data = chunk.toString('utf8');
|
||||||
|
|
@ -382,9 +377,33 @@ async function main() {
|
||||||
if (line.trim()) {
|
if (line.trim()) {
|
||||||
try {
|
try {
|
||||||
const record = JSON.parse(line);
|
const record = JSON.parse(line);
|
||||||
if (Array.isArray(record) && record.length >= 3 && record[1] === 'o') {
|
if (Array.isArray(record) && record.length >= 3) {
|
||||||
// This is an output record: [timestamp, 'o', text]
|
if (record[1] === 'o') {
|
||||||
process.stdout.write(record[2]);
|
// This is an output record: [timestamp, 'o', text]
|
||||||
|
process.stdout.write(record[2]);
|
||||||
|
} else if (record[0] === 'exit') {
|
||||||
|
// This is an exit event: ['exit', exitCode, sessionId]
|
||||||
|
console.log(`\n\nDetected exit event with code: ${record[1]}`);
|
||||||
|
// Clean up all intervals and streams immediately
|
||||||
|
intervals.forEach((interval) => clearInterval(interval));
|
||||||
|
streams.forEach((stream) => {
|
||||||
|
try {
|
||||||
|
stream.destroy?.();
|
||||||
|
} catch (_e) {
|
||||||
|
// Ignore cleanup errors
|
||||||
|
}
|
||||||
|
});
|
||||||
|
|
||||||
|
// Restore terminal settings
|
||||||
|
if (!monitorOnly && process.stdin.isTTY) {
|
||||||
|
process.stdin.setRawMode(false);
|
||||||
|
}
|
||||||
|
if (!monitorOnly) {
|
||||||
|
process.stdin.pause();
|
||||||
|
}
|
||||||
|
|
||||||
|
process.exit(record[1] || 0);
|
||||||
|
}
|
||||||
}
|
}
|
||||||
} catch (_e) {
|
} catch (_e) {
|
||||||
// If JSON parse fails, might be partial line, skip it
|
// If JSON parse fails, might be partial line, skip it
|
||||||
|
|
@ -401,11 +420,7 @@ async function main() {
|
||||||
|
|
||||||
// Start monitoring
|
// Start monitoring
|
||||||
const streamInterval = setInterval(readNewData, 50);
|
const streamInterval = setInterval(readNewData, 50);
|
||||||
|
intervals.push(streamInterval);
|
||||||
// Clean up on exit
|
|
||||||
process.on('exit', () => {
|
|
||||||
clearInterval(streamInterval);
|
|
||||||
});
|
|
||||||
}
|
}
|
||||||
|
|
||||||
// Set up signal handlers for graceful shutdown
|
// Set up signal handlers for graceful shutdown
|
||||||
|
|
@ -444,11 +459,26 @@ async function main() {
|
||||||
process.on('SIGINT', () => shutdown('SIGINT'));
|
process.on('SIGINT', () => shutdown('SIGINT'));
|
||||||
process.on('SIGTERM', () => shutdown('SIGTERM'));
|
process.on('SIGTERM', () => shutdown('SIGTERM'));
|
||||||
|
|
||||||
// Monitor session status
|
// Monitor session status with faster polling and better exit detection
|
||||||
const checkInterval = setInterval(() => {
|
const checkInterval = setInterval(() => {
|
||||||
try {
|
try {
|
||||||
const currentSession = ptyService.getSession(result.sessionId);
|
const currentSession = ptyService.getSession(result.sessionId);
|
||||||
if (!currentSession || currentSession.status === 'exited') {
|
if (!currentSession || currentSession.status === 'exited') {
|
||||||
|
console.log('\n\nSession has exited.');
|
||||||
|
if (currentSession?.exit_code !== undefined) {
|
||||||
|
console.log(`Exit code: ${currentSession.exit_code}`);
|
||||||
|
}
|
||||||
|
|
||||||
|
// Clean up all intervals and streams
|
||||||
|
intervals.forEach((interval) => clearInterval(interval));
|
||||||
|
streams.forEach((stream) => {
|
||||||
|
try {
|
||||||
|
stream.destroy?.();
|
||||||
|
} catch (_e) {
|
||||||
|
// Ignore cleanup errors
|
||||||
|
}
|
||||||
|
});
|
||||||
|
|
||||||
// Restore terminal settings before exit (only if we were in interactive mode)
|
// Restore terminal settings before exit (only if we were in interactive mode)
|
||||||
if (!monitorOnly && process.stdin.isTTY) {
|
if (!monitorOnly && process.stdin.isTTY) {
|
||||||
process.stdin.setRawMode(false);
|
process.stdin.setRawMode(false);
|
||||||
|
|
@ -457,19 +487,24 @@ async function main() {
|
||||||
process.stdin.pause();
|
process.stdin.pause();
|
||||||
}
|
}
|
||||||
|
|
||||||
console.log('\n\nSession has exited.');
|
|
||||||
if (currentSession?.exit_code !== undefined) {
|
|
||||||
console.log(`Exit code: ${currentSession.exit_code}`);
|
|
||||||
}
|
|
||||||
clearInterval(checkInterval);
|
|
||||||
process.exit(currentSession?.exit_code || 0);
|
process.exit(currentSession?.exit_code || 0);
|
||||||
}
|
}
|
||||||
} catch (error) {
|
} catch (error) {
|
||||||
console.error('Error monitoring session:', error);
|
console.error('Error monitoring session:', error);
|
||||||
clearInterval(checkInterval);
|
// Clean up all intervals and streams on error too
|
||||||
|
intervals.forEach((interval) => clearInterval(interval));
|
||||||
|
streams.forEach((stream) => {
|
||||||
|
try {
|
||||||
|
stream.destroy?.();
|
||||||
|
} catch (_e) {
|
||||||
|
// Ignore cleanup errors
|
||||||
|
}
|
||||||
|
});
|
||||||
process.exit(1);
|
process.exit(1);
|
||||||
}
|
}
|
||||||
}, 1000); // Check every second
|
}, 500); // Check every 500ms for faster exit detection
|
||||||
|
|
||||||
|
intervals.push(checkInterval);
|
||||||
|
|
||||||
// Keep the process alive
|
// Keep the process alive
|
||||||
await new Promise<void>((resolve) => {
|
await new Promise<void>((resolve) => {
|
||||||
|
|
|
||||||
|
|
@ -308,43 +308,6 @@ export class PtyManager {
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
/**
|
|
||||||
* Try to create a control pipe for an existing external session
|
|
||||||
*/
|
|
||||||
private createControlPipeForExternalSession(sessionId: string): string | null {
|
|
||||||
const diskSession = this.sessionManager.getSession(sessionId);
|
|
||||||
if (!diskSession || !diskSession.stdin) {
|
|
||||||
return null;
|
|
||||||
}
|
|
||||||
|
|
||||||
try {
|
|
||||||
// Create control pipe in the same directory as stdin
|
|
||||||
const controlPipePath = path.join(path.dirname(diskSession.stdin), 'control');
|
|
||||||
|
|
||||||
// Create the control pipe file if it doesn't exist
|
|
||||||
if (!fs.existsSync(controlPipePath)) {
|
|
||||||
fs.writeFileSync(controlPipePath, '');
|
|
||||||
}
|
|
||||||
|
|
||||||
// Update session.json to include the control pipe
|
|
||||||
const sessionDir = path.dirname(diskSession.stdin);
|
|
||||||
const sessionInfoPath = path.join(sessionDir, 'session.json');
|
|
||||||
|
|
||||||
if (fs.existsSync(sessionInfoPath)) {
|
|
||||||
const sessionInfo = JSON.parse(fs.readFileSync(sessionInfoPath, 'utf8'));
|
|
||||||
sessionInfo.control = controlPipePath;
|
|
||||||
fs.writeFileSync(sessionInfoPath, JSON.stringify(sessionInfo, null, 2));
|
|
||||||
|
|
||||||
console.log(`Created control pipe for external session ${sessionId}: ${controlPipePath}`);
|
|
||||||
return controlPipePath;
|
|
||||||
}
|
|
||||||
} catch (error) {
|
|
||||||
console.warn(`Failed to create control pipe for session ${sessionId}:`, error);
|
|
||||||
}
|
|
||||||
|
|
||||||
return null;
|
|
||||||
}
|
|
||||||
|
|
||||||
/**
|
/**
|
||||||
* Send a control message to an external session
|
* Send a control message to an external session
|
||||||
*/
|
*/
|
||||||
|
|
@ -357,15 +320,14 @@ export class PtyManager {
|
||||||
return false;
|
return false;
|
||||||
}
|
}
|
||||||
|
|
||||||
let controlPipe = diskSession.control;
|
const controlPipe = diskSession.control;
|
||||||
|
|
||||||
// If no control pipe exists, try to create one for external sessions
|
// External sessions should already have control pipe created by fwd.ts
|
||||||
if (!controlPipe) {
|
if (!controlPipe) {
|
||||||
const createdPipe = this.createControlPipeForExternalSession(sessionId);
|
console.warn(
|
||||||
if (!createdPipe) {
|
`No control pipe found for session ${sessionId} - external session should create its own control pipe`
|
||||||
return false;
|
);
|
||||||
}
|
return false;
|
||||||
controlPipe = createdPipe;
|
|
||||||
}
|
}
|
||||||
|
|
||||||
try {
|
try {
|
||||||
|
|
|
||||||
Loading…
Reference in a new issue