Skip to main content
Glama
pidFileManager.test.ts12.5 kB
import { EventEmitter } from 'events'; import fs from 'fs'; import path from 'path'; import { cleanupPidFile, cleanupPidFileOnExit, getPidFilePath, isProcessAlive, readPidFile, registerPidFileCleanup, registerPidFileSignalHandlers, ServerPidInfo, writePidFile, } from '@src/core/server/pidFileManager.js'; import { afterEach, beforeEach, describe, expect, it, vi } from 'vitest'; describe('pidFileManager', () => { const testConfigDir = path.join(process.cwd(), '.tmp-test-pid'); const testPidFilePath = getPidFilePath(testConfigDir); 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); } }); describe('getPidFilePath', () => { it('should return correct PID file path', () => { const pidPath = getPidFilePath('/test/config'); expect(pidPath).toBe(path.join('/test/config', 'server.pid')); }); }); describe('isProcessAlive', () => { it('should return true for current process', () => { expect(isProcessAlive(process.pid)).toBe(true); }); it('should return false for non-existent process', () => { // Use a very high PID that is unlikely to exist expect(isProcessAlive(99999999)).toBe(false); }); }); describe('writePidFile', () => { it('should write PID file with correct format', () => { 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); const content = fs.readFileSync(testPidFilePath, 'utf-8'); const parsed = JSON.parse(content); expect(parsed.pid).toBe(process.pid); expect(parsed.url).toBe('http://localhost:3050/mcp'); expect(parsed.port).toBe(3050); expect(parsed.host).toBe('localhost'); expect(parsed.transport).toBe('http'); }); it('should create config directory if it does not exist', () => { const newConfigDir = path.join(testConfigDir, 'nested'); const serverInfo: ServerPidInfo = { pid: process.pid, url: 'http://localhost:3050/mcp', port: 3050, host: 'localhost', transport: 'http', startedAt: new Date().toISOString(), configDir: newConfigDir, }; writePidFile(newConfigDir, serverInfo); expect(fs.existsSync(newConfigDir)).toBe(true); expect(fs.existsSync(getPidFilePath(newConfigDir))).toBe(true); // Cleanup fs.unlinkSync(getPidFilePath(newConfigDir)); fs.rmdirSync(newConfigDir); }); }); describe('readPidFile', () => { it('should read valid PID file with alive process', () => { 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); const result = readPidFile(testConfigDir); expect(result).not.toBeNull(); expect(result?.pid).toBe(process.pid); expect(result?.url).toBe('http://localhost:3050/mcp'); }); it('should return null for non-existent PID file', () => { const result = readPidFile(testConfigDir); expect(result).toBeNull(); }); it('should return null for dead process', () => { const serverInfo: ServerPidInfo = { pid: 99999999, // Non-existent process url: 'http://localhost:3050/mcp', port: 3050, host: 'localhost', transport: 'http', startedAt: new Date().toISOString(), configDir: testConfigDir, }; writePidFile(testConfigDir, serverInfo); const result = readPidFile(testConfigDir); expect(result).toBeNull(); }); it('should return null for invalid JSON', () => { fs.writeFileSync(testPidFilePath, 'invalid json', 'utf-8'); const result = readPidFile(testConfigDir); expect(result).toBeNull(); }); it('should return null for missing required fields', () => { fs.writeFileSync(testPidFilePath, JSON.stringify({ pid: process.pid }), 'utf-8'); const result = readPidFile(testConfigDir); expect(result).toBeNull(); }); }); describe('cleanupPidFile', () => { it('should delete PID file if it exists', () => { 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); cleanupPidFile(testConfigDir); expect(fs.existsSync(testPidFilePath)).toBe(false); }); it('should not throw error if PID file does not exist', () => { expect(() => cleanupPidFile(testConfigDir)).not.toThrow(); }); }); describe('cleanupPidFileOnExit', () => { it('should delete PID file when called', () => { 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); cleanupPidFileOnExit(testConfigDir); expect(fs.existsSync(testPidFilePath)).toBe(false); }); }); describe('registerPidFileCleanup', () => { // Store original process methods to restore later let originalProcessOn: typeof process.on; let originalProcessListeners: typeof process.listeners; beforeEach(() => { // Mock process methods originalProcessOn = process.on; originalProcessListeners = process.listeners; vi.spyOn(process, 'on').mockImplementation((event: string | symbol, listener: () => void) => { // For 'exit' event, call the listener immediately to test it if (event === 'exit') { listener(); } return process; }); vi.spyOn(process, 'listeners').mockReturnValue([]); }); afterEach(() => { // Restore original process methods process.on = originalProcessOn; process.listeners = originalProcessListeners; vi.restoreAllMocks(); }); it('should register only for exit event, not signal events', () => { 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); // This should only register for 'exit' event registerPidFileCleanup(testConfigDir); // Verify the PID file is cleaned up (exit handler was called) expect(fs.existsSync(testPidFilePath)).toBe(false); // Verify only 'exit' event was registered expect(process.on).toHaveBeenCalledWith('exit', expect.any(Function)); // Should NOT have registered for signal events expect(process.on).not.toHaveBeenCalledWith('SIGINT', expect.any(Function)); expect(process.on).not.toHaveBeenCalledWith('SIGTERM', expect.any(Function)); expect(process.on).not.toHaveBeenCalledWith('SIGHUP', expect.any(Function)); }); }); describe('registerPidFileSignalHandlers', () => { // Store original process methods to restore later let originalProcessOn: typeof process.on; let originalProcessExit: typeof process.exit; const mockEventEmitter = new EventEmitter(); beforeEach(() => { // Mock process methods originalProcessOn = process.on; originalProcessExit = process.exit; vi.spyOn(process, 'on').mockImplementation((event: string | symbol, listener: () => void) => { // Store listeners for later testing mockEventEmitter.on(event, listener); return process; }); vi.spyOn(process, 'exit').mockImplementation(() => { // Don't actually exit in tests return undefined as never; }); }); afterEach(() => { // Restore original process methods process.on = originalProcessOn; process.exit = originalProcessExit; mockEventEmitter.removeAllListeners(); vi.restoreAllMocks(); }); it('should register for all signal events and call process.exit', () => { 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 signal handlers registerPidFileSignalHandlers(testConfigDir); // Verify all signal events were registered expect(process.on).toHaveBeenCalledWith('SIGINT', expect.any(Function)); expect(process.on).toHaveBeenCalledWith('SIGTERM', expect.any(Function)); expect(process.on).toHaveBeenCalledWith('SIGHUP', expect.any(Function)); // Test SIGINT mockEventEmitter.emit('SIGINT'); expect(fs.existsSync(testPidFilePath)).toBe(false); expect(process.exit).toHaveBeenCalledWith(0); // Recreate file for next test writePidFile(testConfigDir, serverInfo); // Test SIGTERM mockEventEmitter.emit('SIGTERM'); expect(fs.existsSync(testPidFilePath)).toBe(false); expect(process.exit).toHaveBeenCalledWith(0); // Recreate file for next test writePidFile(testConfigDir, serverInfo); // Test SIGHUP mockEventEmitter.emit('SIGHUP'); expect(fs.existsSync(testPidFilePath)).toBe(false); expect(process.exit).toHaveBeenCalledWith(0); }); }); describe('Signal Handler Conflict Prevention', () => { let mockProcessExit: any; beforeEach(() => { // Track whether process.exit has been called mockProcessExit = vi.fn(); // Mock process.exit to prevent actual exit vi.stubGlobal('process', { ...process, exit: mockProcessExit, on: vi.fn(), removeListener: vi.fn(), }); }); afterEach(() => { vi.unstubAllGlobals(); }); it('should NOT call process.exit when registerPidFileCleanup is used', () => { 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 cleanup handler (no signal handlers) registerPidFileCleanup(testConfigDir); // Verify process.exit was NOT called expect(mockProcessExit).not.toHaveBeenCalled(); }); it('should call process.exit when registerPidFileSignalHandlers is used', () => { 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 signal handlers registerPidFileSignalHandlers(testConfigDir); // Get the registered listeners and simulate SIGINT const calls = (process.on as any).mock.calls; const sigintListener = calls.find((call: any) => call[0] === 'SIGINT')?.[1]; if (sigintListener) { sigintListener(); } // Verify process.exit WAS called expect(mockProcessExit).toHaveBeenCalledWith(0); }); }); });

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