mirror of
https://github.com/samsonjs/vibetunnel.git
synced 2026-03-25 09:25:50 +00:00
fix: handle tmux session detachment gracefully instead of kill errors
- Add detection for tmux attachment sessions (commands containing 'tmux attach' or names starting with 'tmux:') - Implement smart detachment using Ctrl-B,d sequence instead of SIGTERM - Add fallback to :detach-client command if initial detach fails - Update API responses to distinguish between 'killed' and 'detached' sessions - Prevents 500 errors when trying to kill the last tmux session This allows users to cleanly exit VibeTunnel tmux attachments without destroying the underlying tmux session, which can then be reattached later.
This commit is contained in:
parent
12ef75386c
commit
61e487fff3
3 changed files with 84 additions and 2 deletions
|
|
@ -517,6 +517,14 @@ export class PtyManager extends EventEmitter {
|
|||
}
|
||||
}
|
||||
|
||||
// Detect if this is a tmux attachment session
|
||||
const isTmuxAttachment =
|
||||
(resolvedCommand.includes('tmux') &&
|
||||
(resolvedCommand.includes('attach-session') ||
|
||||
resolvedCommand.includes('attach') ||
|
||||
resolvedCommand.includes('a'))) ||
|
||||
sessionName.startsWith('tmux:');
|
||||
|
||||
const session: PtySession = {
|
||||
id: sessionId,
|
||||
sessionInfo,
|
||||
|
|
@ -531,6 +539,7 @@ export class PtyManager extends EventEmitter {
|
|||
isExternalTerminal: !!options.forwardToStdout,
|
||||
currentWorkingDir: workingDir,
|
||||
titleFilter: new TitleSequenceFilter(),
|
||||
isTmuxAttachment,
|
||||
};
|
||||
|
||||
this.sessions.set(sessionId, session);
|
||||
|
|
@ -1509,6 +1518,54 @@ export class PtyManager extends EventEmitter {
|
|||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* Detach from a tmux session gracefully
|
||||
* @param sessionId The session ID of the tmux attachment
|
||||
* @returns Promise that resolves when detached
|
||||
*/
|
||||
private async detachFromTmux(sessionId: string): Promise<boolean> {
|
||||
const session = this.sessions.get(sessionId);
|
||||
if (!session || !session.isTmuxAttachment || !session.ptyProcess) {
|
||||
return false;
|
||||
}
|
||||
|
||||
try {
|
||||
logger.log(chalk.cyan(`Detaching from tmux session (${sessionId})`));
|
||||
|
||||
// Try the standard detach sequence first (Ctrl-B, d)
|
||||
await this.sendInput(sessionId, { text: '\x02d' }); // \x02 is Ctrl-B
|
||||
|
||||
// Wait for detachment
|
||||
await new Promise((resolve) => setTimeout(resolve, 300));
|
||||
|
||||
// Check if the process is still running
|
||||
if (!ProcessUtils.isProcessRunning(session.ptyProcess.pid)) {
|
||||
logger.log(chalk.green(`Successfully detached from tmux (${sessionId})`));
|
||||
return true;
|
||||
}
|
||||
|
||||
// If still running, try sending the detach-client command
|
||||
logger.debug('First detach attempt failed, trying detach-client command');
|
||||
await this.sendInput(sessionId, { text: ':detach-client\n' });
|
||||
|
||||
// Wait a bit longer
|
||||
await new Promise((resolve) => setTimeout(resolve, 500));
|
||||
|
||||
// Final check
|
||||
if (!ProcessUtils.isProcessRunning(session.ptyProcess.pid)) {
|
||||
logger.log(
|
||||
chalk.green(`Successfully detached from tmux using detach-client (${sessionId})`)
|
||||
);
|
||||
return true;
|
||||
}
|
||||
|
||||
return false;
|
||||
} catch (error) {
|
||||
logger.error(`Error detaching from tmux: ${error}`);
|
||||
return false;
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* Kill a session with proper SIGTERM -> SIGKILL escalation
|
||||
* Returns a promise that resolves when the process is actually terminated
|
||||
|
|
@ -1517,6 +1574,19 @@ export class PtyManager extends EventEmitter {
|
|||
const memorySession = this.sessions.get(sessionId);
|
||||
|
||||
try {
|
||||
// Special handling for tmux attachment sessions
|
||||
if (memorySession?.isTmuxAttachment) {
|
||||
const detached = await this.detachFromTmux(sessionId);
|
||||
if (detached) {
|
||||
// The PTY process should exit cleanly after detaching
|
||||
// Let the normal exit handler clean up the session
|
||||
return;
|
||||
}
|
||||
|
||||
logger.warn(`Failed to detach from tmux, falling back to normal kill`);
|
||||
// Fall through to normal kill logic
|
||||
}
|
||||
|
||||
// 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
|
||||
|
|
|
|||
|
|
@ -116,6 +116,8 @@ export interface PtySession {
|
|||
currentCommand?: string; // Command line of current foreground process
|
||||
commandStartTime?: number; // When current command started (timestamp)
|
||||
processPollingInterval?: NodeJS.Timeout; // Interval for checking process state
|
||||
// Tmux attachment tracking
|
||||
isTmuxAttachment?: boolean; // True if this session is attached to tmux
|
||||
}
|
||||
|
||||
export class PtyError extends Error {
|
||||
|
|
|
|||
|
|
@ -611,9 +611,19 @@ export function createSessionRoutes(config: SessionRoutesConfig): Router {
|
|||
logger.log(chalk.yellow(`local session ${sessionId} cleaned up`));
|
||||
res.json({ success: true, message: 'Session cleaned up' });
|
||||
} else {
|
||||
// Check if this is a tmux attachment before killing
|
||||
const isTmuxAttachment =
|
||||
session.name?.startsWith('tmux:') || session.command?.includes('tmux attach');
|
||||
|
||||
await ptyManager.killSession(sessionId, 'SIGTERM');
|
||||
logger.log(chalk.yellow(`local session ${sessionId} killed`));
|
||||
res.json({ success: true, message: 'Session killed' });
|
||||
|
||||
if (isTmuxAttachment) {
|
||||
logger.log(chalk.yellow(`local session ${sessionId} detached from tmux`));
|
||||
res.json({ success: true, message: 'Detached from tmux session' });
|
||||
} else {
|
||||
logger.log(chalk.yellow(`local session ${sessionId} killed`));
|
||||
res.json({ success: true, message: 'Session killed' });
|
||||
}
|
||||
}
|
||||
} catch (error) {
|
||||
logger.error('error killing session:', error);
|
||||
|
|
|
|||
Loading…
Reference in a new issue