Skip to main content
Glama
pidFileManager.crash.test.ts9.04 kB
import { EventEmitter } from 'events'; import fs from 'fs'; import path from 'path'; import { registerPidFileCleanup, registerPidFileSignalHandlers, ServerPidInfo, writePidFile, } from '@src/core/server/pidFileManager.js'; import { afterEach, beforeEach, describe, expect, it, vi } from 'vitest'; describe('PID File Manager - Crash Scenario Tests', () => { const testConfigDir = path.join(process.cwd(), '.tmp-test-pid-crash'); const testPidFilePath = path.join(testConfigDir, 'server.pid'); beforeEach(() => { // Create test directory if (!fs.existsSync(testConfigDir)) { fs.mkdirSync(testConfigDir, { recursive: true }); } }); afterEach(() => { // Clean up test directory if (fs.existsSync(testPidFilePath)) { fs.unlinkSync(testPidFilePath); } if (fs.existsSync(testConfigDir)) { fs.rmdirSync(testConfigDir); } vi.restoreAllMocks(); }); describe('Signal Handler Conflict Simulation', () => { let mockEventEmitter: EventEmitter; let originalProcessOn: typeof process.on; let originalProcessExit: typeof process.exit; let processExitCalls: any[] = []; let cleanupCalls: any[] = []; beforeEach(() => { processExitCalls = []; cleanupCalls = []; mockEventEmitter = new EventEmitter(); // Store original methods originalProcessOn = process.on; originalProcessExit = process.exit; // Mock process.on to capture listeners vi.spyOn(process, 'on').mockImplementation((event: string | symbol, listener: any) => { mockEventEmitter.on(event, listener); return process; }); // Mock process.exit to track calls without actually exiting vi.spyOn(process, 'exit').mockImplementation((code?: string | number | null | undefined) => { processExitCalls.push(code); throw new Error(`process.exit(${code}) called - would have crashed`); }); }); afterEach(() => { // Restore original methods process.on = originalProcessOn; process.exit = originalProcessExit; mockEventEmitter.removeAllListeners(); }); it('should demonstrate crash scenario with conflicting signal handlers', () => { const serverInfo: ServerPidInfo = { pid: process.pid, url: 'http://localhost:3050/mcp', port: 3050, host: 'localhost', transport: 'http', startedAt: new Date().toISOString(), configDir: testConfigDir, }; // Write PID file writePidFile(testConfigDir, serverInfo); expect(fs.existsSync(testPidFilePath)).toBe(true); // Simulate the OLD behavior that causes crashes: // 1. PID file manager registers signal handlers that call process.exit immediately registerPidFileSignalHandlers(testConfigDir); // 2. Application also registers its own signal handlers (for graceful shutdown) const gracefulShutdown = vi.fn(); process.on('SIGINT', gracefulShutdown); process.on('SIGTERM', gracefulShutdown); process.on('SIGHUP', gracefulShutdown); // Simulate a signal (this would happen during connection errors) expect(() => { mockEventEmitter.emit('SIGINT'); }).toThrow('process.exit(0) called - would have crashed'); // Verify the crash scenario: expect(processExitCalls).toEqual([0]); expect(gracefulShutdown).not.toHaveBeenCalled(); // PID file is cleaned up by the immediate handler expect(fs.existsSync(testPidFilePath)).toBe(false); console.log('✅ Demonstrated crash scenario: Signal handler conflict causes immediate exit'); }); it('should demonstrate fixed behavior with proper signal handling', () => { const serverInfo: ServerPidInfo = { pid: process.pid, url: 'http://localhost:3050/mcp', port: 3050, host: 'localhost', transport: 'http', startedAt: new Date().toISOString(), configDir: testConfigDir, }; // Write PID file writePidFile(testConfigDir, serverInfo); expect(fs.existsSync(testPidFilePath)).toBe(true); // Simulate the NEW behavior that prevents crashes: // 1. PID file manager registers only for 'exit' event, not signals registerPidFileCleanup(testConfigDir); // 2. Application registers its own signal handlers for graceful shutdown const gracefulShutdown = vi.fn(() => { // Simulate graceful shutdown cleanup cleanupCalls.push('graceful-shutdown'); }); process.on('SIGINT', gracefulShutdown); process.on('SIGTERM', gracefulShutdown); process.on('SIGHUP', gracefulShutdown); // Simulate a signal (this would happen during connection errors) expect(() => { mockEventEmitter.emit('SIGINT'); }).not.toThrow(); // Verify the fixed behavior: expect(processExitCalls).toEqual([]); // No immediate exit expect(gracefulShutdown).toHaveBeenCalled(); // Graceful shutdown runs expect(cleanupCalls).toContain('graceful-shutdown'); // PID file is NOT cleaned up by signal handler (will be cleaned by graceful shutdown) expect(fs.existsSync(testPidFilePath)).toBe(true); console.log('✅ Demonstrated fixed behavior: No crash, graceful shutdown runs'); }); it('should simulate ECONNRESET error scenario', async () => { const serverInfo: ServerPidInfo = { pid: process.pid, url: 'http://localhost:3050/mcp', port: 3050, host: 'localhost', transport: 'http', startedAt: new Date().toISOString(), configDir: testConfigDir, }; writePidFile(testConfigDir, serverInfo); // Create a mock client that triggers an ECONNRESET error const mockClient = { onerror: null as ((error: Error) => void) | null, }; // Simulate the client error handler (from ClientManager) const clientErrorHandler = (error: Error) => { console.log(`Client error: ${error.message}`); // In the original code, this error might have triggered signals // Now it just logs the error }; mockClient.onerror = clientErrorHandler; // Simulate ECONNRESET error const econnresetError = new Error('The socket connection was closed unexpectedly'); (econnresetError as any).code = 'ECONNRESET'; // This should NOT crash the process expect(() => { if (mockClient.onerror) { mockClient.onerror(econnresetError); } }).not.toThrow(); // Verify process is still alive and PID file exists expect(fs.existsSync(testPidFilePath)).toBe(true); console.log('✅ ECONNRESET error handled gracefully without crashing'); }); it('should show that graceful shutdown properly cleans up PID file', async () => { const serverInfo: ServerPidInfo = { pid: process.pid, url: 'http://localhost:3050/mcp', port: 3050, host: 'localhost', transport: 'http', startedAt: new Date().toISOString(), configDir: testConfigDir, }; writePidFile(testConfigDir, serverInfo); expect(fs.existsSync(testPidFilePath)).toBe(true); // Register for exit only registerPidFileCleanup(testConfigDir); // Simulate graceful shutdown with PID cleanup const { cleanupPidFileOnExit } = await import('@src/core/server/pidFileManager.js'); // This would be called by the graceful shutdown handler in serve.ts cleanupPidFileOnExit(testConfigDir); // Verify PID file is cleaned up expect(fs.existsSync(testPidFilePath)).toBe(false); console.log('✅ PID file properly cleaned up during graceful shutdown'); }); }); describe('Connection Error Resilience', () => { it('should handle multiple rapid connection errors without crashing', () => { const serverInfo: ServerPidInfo = { pid: process.pid, url: 'http://localhost:3050/mcp', port: 3050, host: 'localhost', transport: 'http', startedAt: new Date().toISOString(), configDir: testConfigDir, }; writePidFile(testConfigDir, serverInfo); // Register only exit handler (new behavior) registerPidFileCleanup(testConfigDir); // Simulate multiple connection errors const errors = [ new Error('ECONNRESET: Connection reset by peer'), new Error('ETIMEDOUT: Connection timed out'), new Error('ENOTFOUND: DNS lookup failed'), new Error('ECONNREFUSED: Connection refused'), ]; errors.forEach((error, index) => { (error as any).code = error.message.split(':')[0]; // Simulate client error handling console.log(`Connection error ${index + 1}: ${error.message}`); // Process should still be alive expect(fs.existsSync(testPidFilePath)).toBe(true); }); console.log('✅ Multiple connection errors handled without crashing'); }); }); });

Latest Blog Posts

MCP directory API

We provide all the information about MCP servers via our MCP API.

curl -X GET 'https://glama.ai/api/mcp/v1/servers/1mcp-app/agent'

If you have feedback or need assistance with the MCP directory API, please join our Discord server