mirror of
https://github.com/samsonjs/vibetunnel.git
synced 2026-04-18 13:25:52 +00:00
Implement cross-platform ProcessUtils for reliable process checking
- Add ProcessUtils class with Windows/Unix process detection methods - Replace raw process.kill(pid, 0) calls with ProcessUtils.isProcessRunning() - Support Windows tasklist and Unix kill signal 0 approaches - Add process killing and waiting utilities for better process management - Update SessionManager, PtyManager, and PtyService to use ProcessUtils - Improves reliability of cmd.exe session detection on Windows 🤖 Generated with [Claude Code](https://claude.ai/code) Co-Authored-By: Claude <noreply@anthropic.com>
This commit is contained in:
parent
cbc1a4e49d
commit
62f0579d07
5 changed files with 163 additions and 20 deletions
139
web/src/pty/ProcessUtils.ts
Normal file
139
web/src/pty/ProcessUtils.ts
Normal file
|
|
@ -0,0 +1,139 @@
|
|||
/**
|
||||
* ProcessUtils - Cross-platform process management utilities
|
||||
*
|
||||
* Provides reliable process existence checking across Windows, macOS, and Linux.
|
||||
*/
|
||||
|
||||
import { spawnSync } from 'child_process';
|
||||
|
||||
export class ProcessUtils {
|
||||
/**
|
||||
* Check if a process is currently running by PID
|
||||
* Uses platform-appropriate methods for reliable detection
|
||||
*/
|
||||
static isProcessRunning(pid: number): boolean {
|
||||
if (!pid || pid <= 0) {
|
||||
return false;
|
||||
}
|
||||
|
||||
try {
|
||||
if (process.platform === 'win32') {
|
||||
// Windows: Use tasklist command
|
||||
return ProcessUtils.isProcessRunningWindows(pid);
|
||||
} else {
|
||||
// Unix/Linux/macOS: Use kill with signal 0
|
||||
return ProcessUtils.isProcessRunningUnix(pid);
|
||||
}
|
||||
} catch (error) {
|
||||
console.warn(`Error checking if process ${pid} is running:`, error);
|
||||
return false;
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* Windows-specific process check using tasklist
|
||||
*/
|
||||
private static isProcessRunningWindows(pid: number): boolean {
|
||||
try {
|
||||
const result = spawnSync('tasklist', ['/FI', `PID eq ${pid}`, '/NH', '/FO', 'CSV'], {
|
||||
encoding: 'utf8',
|
||||
windowsHide: true,
|
||||
timeout: 5000, // 5 second timeout
|
||||
});
|
||||
|
||||
// Check if the command succeeded and PID appears in output
|
||||
if (result.status === 0 && result.stdout) {
|
||||
// tasklist outputs CSV format with PID in quotes
|
||||
return result.stdout.includes(`"${pid}"`);
|
||||
}
|
||||
|
||||
return false;
|
||||
} catch (error) {
|
||||
console.warn(`Windows process check failed for PID ${pid}:`, error);
|
||||
return false;
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* Unix-like systems process check using kill signal 0
|
||||
*/
|
||||
private static isProcessRunningUnix(pid: number): boolean {
|
||||
try {
|
||||
// Send signal 0 to check if process exists
|
||||
// This doesn't actually kill the process, just checks existence
|
||||
process.kill(pid, 0);
|
||||
return true;
|
||||
} catch (error) {
|
||||
// If we get ESRCH, the process doesn't exist
|
||||
// If we get EPERM, the process exists but we don't have permission
|
||||
const err = error as NodeJS.ErrnoException;
|
||||
if (err.code === 'EPERM') {
|
||||
// Process exists but we don't have permission to signal it
|
||||
return true;
|
||||
}
|
||||
// ESRCH or other errors mean process doesn't exist
|
||||
return false;
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* Get basic process information if available
|
||||
* Returns null if process is not running or info cannot be retrieved
|
||||
*/
|
||||
static getProcessInfo(pid: number): { pid: number; exists: boolean } | null {
|
||||
if (!ProcessUtils.isProcessRunning(pid)) {
|
||||
return null;
|
||||
}
|
||||
|
||||
return {
|
||||
pid,
|
||||
exists: true,
|
||||
};
|
||||
}
|
||||
|
||||
/**
|
||||
* Kill a process with platform-appropriate method
|
||||
* Returns true if the kill signal was sent successfully
|
||||
*/
|
||||
static killProcess(pid: number, signal: NodeJS.Signals | number = 'SIGTERM'): boolean {
|
||||
if (!pid || pid <= 0) {
|
||||
return false;
|
||||
}
|
||||
|
||||
try {
|
||||
if (process.platform === 'win32') {
|
||||
// Windows: Use taskkill command for more reliable termination
|
||||
const result = spawnSync('taskkill', ['/PID', pid.toString(), '/F'], {
|
||||
windowsHide: true,
|
||||
timeout: 5000,
|
||||
});
|
||||
return result.status === 0;
|
||||
} else {
|
||||
// Unix-like: Use built-in process.kill
|
||||
process.kill(pid, signal);
|
||||
return true;
|
||||
}
|
||||
} catch (error) {
|
||||
console.warn(`Error killing process ${pid}:`, error);
|
||||
return false;
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* Wait for a process to exit with timeout
|
||||
* Returns true if process exited within timeout, false otherwise
|
||||
*/
|
||||
static async waitForProcessExit(pid: number, timeoutMs: number = 5000): Promise<boolean> {
|
||||
const startTime = Date.now();
|
||||
const checkInterval = 100; // Check every 100ms
|
||||
|
||||
while (Date.now() - startTime < timeoutMs) {
|
||||
if (!ProcessUtils.isProcessRunning(pid)) {
|
||||
return true;
|
||||
}
|
||||
await new Promise((resolve) => setTimeout(resolve, checkInterval));
|
||||
}
|
||||
|
||||
return false;
|
||||
}
|
||||
}
|
||||
|
|
@ -21,6 +21,7 @@ import {
|
|||
} from './types.js';
|
||||
import { AsciinemaWriter } from './AsciinemaWriter.js';
|
||||
import { SessionManager } from './SessionManager.js';
|
||||
import { ProcessUtils } from './ProcessUtils.js';
|
||||
|
||||
export class PtyManager {
|
||||
private sessions = new Map<string, PtySession>();
|
||||
|
|
@ -408,11 +409,7 @@ export class PtyManager {
|
|||
await new Promise((resolve) => setTimeout(resolve, checkInterval));
|
||||
|
||||
// Check if process is still alive
|
||||
try {
|
||||
process.kill(pid, 0); // Signal 0 just checks if process exists
|
||||
// Process still exists, continue waiting
|
||||
console.log(`Session ${sessionId} still alive after ${(i + 1) * checkInterval}ms...`);
|
||||
} catch (_error) {
|
||||
if (!ProcessUtils.isProcessRunning(pid)) {
|
||||
// Process no longer exists - it terminated gracefully
|
||||
console.log(
|
||||
`Session ${sessionId} terminated gracefully after ${(i + 1) * checkInterval}ms`
|
||||
|
|
@ -420,6 +417,9 @@ export class PtyManager {
|
|||
this.sessions.delete(sessionId);
|
||||
return;
|
||||
}
|
||||
|
||||
// Process still exists, continue waiting
|
||||
console.log(`Session ${sessionId} still alive after ${(i + 1) * checkInterval}ms...`);
|
||||
}
|
||||
|
||||
// Process didn't terminate gracefully within 3 seconds, force kill
|
||||
|
|
|
|||
|
|
@ -18,6 +18,7 @@ import {
|
|||
PtyError,
|
||||
} from './types.js';
|
||||
import { PtyManager } from './PtyManager.js';
|
||||
import { ProcessUtils } from './ProcessUtils.js';
|
||||
|
||||
export class PtyService {
|
||||
private config: PtyConfig;
|
||||
|
|
@ -301,17 +302,16 @@ export class PtyService {
|
|||
await new Promise((resolve) => setTimeout(resolve, checkInterval));
|
||||
|
||||
// Check if process is still alive
|
||||
try {
|
||||
process.kill(pid, 0); // Signal 0 just checks if process exists
|
||||
// Process still exists, continue waiting
|
||||
console.log(`Session ${sessionId} still alive after ${(i + 1) * checkInterval}ms...`);
|
||||
} catch (_error) {
|
||||
if (!ProcessUtils.isProcessRunning(pid)) {
|
||||
// Process no longer exists - it terminated gracefully
|
||||
console.log(
|
||||
`Session ${sessionId} terminated gracefully after ${(i + 1) * checkInterval}ms`
|
||||
);
|
||||
return;
|
||||
}
|
||||
|
||||
// Process still exists, continue waiting
|
||||
console.log(`Session ${sessionId} still alive after ${(i + 1) * checkInterval}ms...`);
|
||||
}
|
||||
|
||||
// Process didn't terminate gracefully within 3 seconds, force kill
|
||||
|
|
|
|||
|
|
@ -9,6 +9,7 @@ import * as fs from 'fs';
|
|||
import * as path from 'path';
|
||||
import * as os from 'os';
|
||||
import { SessionInfo, SessionEntryWithId, PtyError } from './types.js';
|
||||
import { ProcessUtils } from './ProcessUtils.js';
|
||||
|
||||
export class SessionManager {
|
||||
private controlPath: string;
|
||||
|
|
@ -325,15 +326,10 @@ export class SessionManager {
|
|||
|
||||
/**
|
||||
* Check if a process is still running
|
||||
* Uses cross-platform process detection for reliability
|
||||
*/
|
||||
isProcessRunning(pid: number): boolean {
|
||||
try {
|
||||
// Send signal 0 to check if process exists
|
||||
process.kill(pid, 0);
|
||||
return true;
|
||||
} catch (_error) {
|
||||
return false;
|
||||
}
|
||||
return ProcessUtils.isProcessRunning(pid);
|
||||
}
|
||||
|
||||
/**
|
||||
|
|
@ -341,10 +337,17 @@ export class SessionManager {
|
|||
*/
|
||||
getProcessStatus(pid: number): { isAlive: boolean; isWaiting: boolean } {
|
||||
try {
|
||||
// First check if process exists
|
||||
process.kill(pid, 0);
|
||||
// First check if process exists using cross-platform method
|
||||
if (!ProcessUtils.isProcessRunning(pid)) {
|
||||
return { isAlive: false, isWaiting: false };
|
||||
}
|
||||
|
||||
// Use ps command to get process state like tty-fwd does (Unix only)
|
||||
if (process.platform === 'win32') {
|
||||
// On Windows, we can't easily get process state, so assume running
|
||||
return { isAlive: true, isWaiting: false };
|
||||
}
|
||||
|
||||
// Use ps command to get process state like tty-fwd does
|
||||
const { spawnSync } = require('child_process');
|
||||
const result = spawnSync('ps', ['-p', pid.toString(), '-o', 'stat='], {
|
||||
encoding: 'utf8',
|
||||
|
|
|
|||
|
|
@ -15,6 +15,7 @@ export { PtyService } from './PtyService.js';
|
|||
export { PtyManager } from './PtyManager.js';
|
||||
export { AsciinemaWriter } from './AsciinemaWriter.js';
|
||||
export { SessionManager } from './SessionManager.js';
|
||||
export { ProcessUtils } from './ProcessUtils.js';
|
||||
|
||||
// Re-export for convenience
|
||||
export { PtyError } from './types.js';
|
||||
|
|
|
|||
Loading…
Reference in a new issue