From be63a59e9c6648c01a38c733502b50610addcd33 Mon Sep 17 00:00:00 2001 From: Kacper Date: Fri, 16 Jan 2026 20:27:53 +0100 Subject: [PATCH 1/3] fix: improve process termination handling for Windows Updated the process termination logic in ClaudeUsageService to handle Windows environments correctly. The code now checks the operating system and calls the appropriate kill method, ensuring consistent behavior across platforms. --- apps/server/src/services/claude-usage-service.ts | 7 ++++++- 1 file changed, 6 insertions(+), 1 deletion(-) diff --git a/apps/server/src/services/claude-usage-service.ts b/apps/server/src/services/claude-usage-service.ts index 64dceb6a..59f22f20 100644 --- a/apps/server/src/services/claude-usage-service.ts +++ b/apps/server/src/services/claude-usage-service.ts @@ -277,9 +277,14 @@ export class ClaudeUsageService { ptyProcess.write('\x1b'); // Send escape key // Fallback: if ESC doesn't exit (Linux), use SIGTERM after 2s + // Windows doesn't support signals, so just call kill() without args setTimeout(() => { if (!settled && ptyProcess && !ptyProcess.killed) { - ptyProcess.kill('SIGTERM'); + if (this.isWindows) { + ptyProcess.kill(); + } else { + ptyProcess.kill('SIGTERM'); + } } }, 2000); } From 0e9369816fd5f62eb8efc2a91ca32430091b4545 Mon Sep 17 00:00:00 2001 From: Kacper Date: Fri, 16 Jan 2026 20:34:12 +0100 Subject: [PATCH 2/3] fix: unify PTY process termination handling across platforms Refactored the process termination logic in both ClaudeUsageService and TerminalService to use a centralized method for killing PTY processes. This ensures consistent handling of process termination across Windows and Unix-like systems, improving reliability and maintainability of the code. --- .../src/services/claude-usage-service.ts | 28 +++++++++++++------ apps/server/src/services/terminal-service.ts | 25 +++++++++++++++-- 2 files changed, 42 insertions(+), 11 deletions(-) diff --git a/apps/server/src/services/claude-usage-service.ts b/apps/server/src/services/claude-usage-service.ts index 59f22f20..35c00a20 100644 --- a/apps/server/src/services/claude-usage-service.ts +++ b/apps/server/src/services/claude-usage-service.ts @@ -23,6 +23,22 @@ export class ClaudeUsageService { private isWindows = os.platform() === 'win32'; private isLinux = os.platform() === 'linux'; + /** + * Kill a PTY process with platform-specific handling. + * Windows doesn't support Unix signals like SIGTERM, so we call kill() without arguments. + * On Unix-like systems (macOS, Linux), we can specify the signal. + * + * @param ptyProcess - The PTY process to kill + * @param signal - The signal to send on Unix-like systems (default: 'SIGTERM') + */ + private killPtyProcess(ptyProcess: pty.IPty, signal: string = 'SIGTERM'): void { + if (this.isWindows) { + ptyProcess.kill(); + } else { + ptyProcess.kill(signal); + } + } + /** * Check if Claude CLI is available on the system */ @@ -211,7 +227,7 @@ export class ClaudeUsageService { if (!settled) { settled = true; if (ptyProcess && !ptyProcess.killed) { - ptyProcess.kill(); + this.killPtyProcess(ptyProcess); } // Don't fail if we have data - return it instead if (output.includes('Current session')) { @@ -253,7 +269,7 @@ export class ClaudeUsageService { if (!settled) { settled = true; if (ptyProcess && !ptyProcess.killed) { - ptyProcess.kill(); + this.killPtyProcess(ptyProcess); } reject( new Error( @@ -277,14 +293,10 @@ export class ClaudeUsageService { ptyProcess.write('\x1b'); // Send escape key // Fallback: if ESC doesn't exit (Linux), use SIGTERM after 2s - // Windows doesn't support signals, so just call kill() without args + // Windows doesn't support signals, so killPtyProcess handles platform differences setTimeout(() => { if (!settled && ptyProcess && !ptyProcess.killed) { - if (this.isWindows) { - ptyProcess.kill(); - } else { - ptyProcess.kill('SIGTERM'); - } + this.killPtyProcess(ptyProcess); } }, 2000); } diff --git a/apps/server/src/services/terminal-service.ts b/apps/server/src/services/terminal-service.ts index c309975c..bd4481a8 100644 --- a/apps/server/src/services/terminal-service.ts +++ b/apps/server/src/services/terminal-service.ts @@ -70,6 +70,23 @@ export class TerminalService extends EventEmitter { private sessions: Map = new Map(); private dataCallbacks: Set = new Set(); private exitCallbacks: Set = new Set(); + private isWindows = os.platform() === 'win32'; + + /** + * Kill a PTY process with platform-specific handling. + * Windows doesn't support Unix signals like SIGTERM/SIGKILL, so we call kill() without arguments. + * On Unix-like systems (macOS, Linux), we can specify the signal. + * + * @param ptyProcess - The PTY process to kill + * @param signal - The signal to send on Unix-like systems (default: 'SIGTERM') + */ + private killPtyProcess(ptyProcess: pty.IPty, signal: string = 'SIGTERM'): void { + if (this.isWindows) { + ptyProcess.kill(); + } else { + ptyProcess.kill(signal); + } + } /** * Detect the best shell for the current platform @@ -477,8 +494,9 @@ export class TerminalService extends EventEmitter { } // First try graceful SIGTERM to allow process cleanup + // On Windows, killPtyProcess calls kill() without signal since Windows doesn't support Unix signals logger.info(`Session ${sessionId} sending SIGTERM`); - session.pty.kill('SIGTERM'); + this.killPtyProcess(session.pty, 'SIGTERM'); // Schedule SIGKILL fallback if process doesn't exit gracefully // The onExit handler will remove session from map when it actually exits @@ -486,7 +504,7 @@ export class TerminalService extends EventEmitter { if (this.sessions.has(sessionId)) { logger.info(`Session ${sessionId} still alive after SIGTERM, sending SIGKILL`); try { - session.pty.kill('SIGKILL'); + this.killPtyProcess(session.pty, 'SIGKILL'); } catch { // Process may have already exited } @@ -588,7 +606,8 @@ export class TerminalService extends EventEmitter { if (session.flushTimeout) { clearTimeout(session.flushTimeout); } - session.pty.kill(); + // Use platform-specific kill to ensure proper termination on Windows + this.killPtyProcess(session.pty); } catch { // Ignore errors during cleanup } From 98c50d44a42f7c38af0b33dbd6b6e7b45e25dbb2 Mon Sep 17 00:00:00 2001 From: Kacper Date: Fri, 16 Jan 2026 20:38:29 +0100 Subject: [PATCH 3/3] test: mock Unix platform for SIGTERM behavior in ClaudeUsageService tests Added a mock for the Unix platform in the SIGTERM test case to ensure proper behavior during testing on non-Windows systems. This change enhances the reliability of the tests by simulating the expected environment for process termination. --- apps/server/tests/unit/services/claude-usage-service.test.ts | 2 ++ 1 file changed, 2 insertions(+) diff --git a/apps/server/tests/unit/services/claude-usage-service.test.ts b/apps/server/tests/unit/services/claude-usage-service.test.ts index af2d10c8..024c4e3a 100644 --- a/apps/server/tests/unit/services/claude-usage-service.test.ts +++ b/apps/server/tests/unit/services/claude-usage-service.test.ts @@ -586,6 +586,8 @@ Resets in 2h it('should send SIGTERM after ESC if process does not exit', async () => { vi.useFakeTimers(); + // Mock Unix platform to test SIGTERM behavior (Windows calls kill() without signal) + vi.mocked(os.platform).mockReturnValue('darwin'); const ptyService = new ClaudeUsageService(); let dataCallback: Function | undefined;