Skip to main content
Glama
pid.test.ts14.3 kB
// SPDX-FileCopyrightText: Copyright Orangebot, Inc. and Medplum contributors // SPDX-License-Identifier: Apache-2.0 import { sleep } from '@medplum/core'; import type { PathOrFileDescriptor } from 'node:fs'; import fs from 'node:fs'; import os from 'node:os'; import { dirname, join } from 'node:path'; import { createPidFile, deregisterAgentCleanup, ensureDirectoryExists, forceKillApp, getAppPid, getPidFilePath, pidLogger, registerAgentCleanup, removePidFile, waitForPidFile, } from './pid'; jest.mock('node:fs'); jest.mock('node:os', () => ({ ...jest.requireActual('node:os'), platform: jest.fn(() => 'darwin'), tmpdir: jest.fn(() => '/tmp'), })); const mockedFs = jest.mocked(fs); const mockedOs = jest.mocked(os); const APP_NAME = 'test-pid-app'; const PID_DIR = dirname(getPidFilePath(APP_NAME)); const TEST_PID_PATH = join('/tmp', 'medplum-agent', 'test-pid-app.pid'); const createdPidFiles = new Set<string>(); describe('PID File Manager', () => { beforeEach(() => { jest.resetAllMocks(); mockedFs.existsSync.mockImplementation((filePath: PathOrFileDescriptor) => { // This is for the `ensureDirectoryExists` check if (filePath.toString() === PID_DIR) { return true; } if (createdPidFiles.has(filePath.toString())) { return true; } return false; }); mockedFs.mkdirSync.mockImplementation(() => undefined); mockedFs.readFileSync.mockImplementation((filePath: PathOrFileDescriptor) => { if (createdPidFiles.has(filePath.toString())) { return process.pid.toString(); } const err = new Error('ENOENT'); (err as Error & { code: string }).code = 'ENOENT'; throw err; }); mockedFs.writeFileSync.mockImplementation((filePath: PathOrFileDescriptor) => { if (createdPidFiles.has(filePath.toString())) { throw new Error('File already exists'); } createdPidFiles.add(filePath.toString()); return undefined; }); mockedFs.unlinkSync.mockImplementation((filePath: PathOrFileDescriptor) => { createdPidFiles.delete(filePath.toString()); return undefined; }); mockedOs.platform.mockImplementation(() => 'darwin'); mockedOs.tmpdir.mockImplementation(() => '/tmp'); }); afterEach(() => { // Clean up any listeners we may have added process.removeAllListeners('SIGTERM'); process.removeAllListeners('SIGINT'); process.removeAllListeners('uncaughtException'); createdPidFiles.clear(); }); test('creates and removes PID file on normal process lifecycle', () => { // Create PID file const pidFilePath = createPidFile(APP_NAME); expect(pidFilePath).toBe(TEST_PID_PATH); // Verify write was called with correct arguments expect(mockedFs.writeFileSync).toHaveBeenCalledWith( expect.stringContaining('.pid'), process.pid.toString(), expect.objectContaining({ flag: 'wx', }) ); // Remove PID file removePidFile(APP_NAME); // Verify unlink was called expect(mockedFs.unlinkSync).toHaveBeenCalledWith(TEST_PID_PATH); }); test('prevents running multiple instances of the same app', () => { createPidFile(APP_NAME); // Mock process.kill to simulate existing process const originalKill = process.kill; process.kill = jest.fn(); try { // Attempt to create PID file should throw expect(() => createPidFile(APP_NAME)).toThrow('test-pid-app already running'); expect(process.kill).toHaveBeenCalledWith(process.pid, 0); } finally { process.kill = originalKill; } }); test('handles stale PID files correctly', () => { createPidFile(APP_NAME); // Mock process.kill to simulate non-existent process const originalKill = process.kill; process.kill = jest.fn().mockImplementation(() => { // This is the Error thrown from Node when a process does not exist const esrchError = new Error(); (esrchError as Error & { code: string }).code = 'ESRCH'; throw esrchError; }); try { // Should succeed and overwrite stale PID file const pidFilePath = createPidFile(APP_NAME); expect(pidFilePath).toBe(TEST_PID_PATH); // Make sure stale PID file was deleted expect(mockedFs.unlinkSync).toHaveBeenCalledWith(TEST_PID_PATH); // Verify write operations expect(mockedFs.writeFileSync).toHaveBeenCalledWith( expect.stringContaining('.pid'), process.pid.toString(), expect.objectContaining({ flag: 'wx', }) ); } finally { process.kill = originalKill; } }); test('handles EPERM errors correctly', () => { createPidFile(APP_NAME); mockedFs.writeFileSync.mockClear(); // Mock process.kill to simulate non-existent process const originalKill = process.kill; process.kill = jest.fn().mockImplementation(() => { const epermError = new Error(); (epermError as Error & { code: string }).code = 'EPERM'; throw epermError; }); try { // Should fail expect(() => createPidFile(APP_NAME)).toThrow(`${APP_NAME} already running`); // Verify write operations didn't occur expect(mockedFs.writeFileSync).not.toHaveBeenCalled(); } finally { process.kill = originalKill; } }); test('handles other errors', () => { createPidFile(APP_NAME); mockedFs.writeFileSync.mockClear(); // Mock process.kill to simulate non-existent process const originalKill = process.kill; process.kill = jest.fn().mockImplementation(() => { throw new Error('Unknown error'); }); try { // Should fail expect(() => createPidFile(APP_NAME)).toThrow(new Error('Unknown error')); // Verify write operations didn't occur expect(mockedFs.writeFileSync).not.toHaveBeenCalled(); } finally { process.kill = originalKill; } }); test.each(['darwin', 'linux'] as const)('returns appropriate file path for the current OS -- %s', (os) => { mockedOs.platform.mockImplementationOnce(() => os); const pidFilePath = getPidFilePath(APP_NAME); expect(pidFilePath).toEqual(TEST_PID_PATH); }); test('returns appropriate file path for the current OS -- win32', () => { mockedOs.platform.mockImplementationOnce(() => 'win32'); const pidFilePath = getPidFilePath(APP_NAME); expect(pidFilePath).toEqual(join('C:', 'ProgramData', 'MedplumAgent', 'pids', `${APP_NAME}.pid`)); }); test('throws on unsupported or invalid OS', () => { mockedOs.platform.mockImplementationOnce(() => 'freebsd'); expect(() => getPidFilePath(APP_NAME)).toThrow(new Error('Invalid OS')); }); test('safely handles non-existent PID file during removal', () => { // Should not throw removePidFile(APP_NAME); // Verify unlink was not called expect(mockedFs.unlinkSync).not.toHaveBeenCalled(); }); test('handles file system errors during PID file creation', () => { // Mock write failure mockedFs.writeFileSync.mockImplementation(() => { throw new Error('Permission denied'); }); // Should throw expect(() => createPidFile(APP_NAME)).toThrow('Permission denied'); }); test('handles file system errors during PID file removal', () => { // Mock file existing but unlink failing mockedFs.existsSync.mockReturnValue(true); mockedFs.unlinkSync.mockImplementation(() => { throw new Error('Permission denied'); }); // Should not throw expect(() => removePidFile(APP_NAME)).not.toThrow('Permission denied'); // Verify unlink was attempted expect(mockedFs.unlinkSync).toHaveBeenCalledWith(TEST_PID_PATH); }); test('waitForPidFile waits until PID file available', async () => { mockedFs.existsSync.mockReturnValue(false); const waitForPromise = waitForPidFile(APP_NAME); let resolved = false; waitForPromise .then(() => { resolved = true; }) .catch(console.error); // Wait and check twice that the promise has not resolved await sleep(100); expect(resolved).toStrictEqual(false); await sleep(100); expect(resolved).toStrictEqual(false); mockedFs.existsSync.mockReturnValue(true); // Await next tick so that promise can resolve await sleep(0); expect(resolved).toStrictEqual(true); }); test('waitForPidFile times out if PID file does not exist before `timeoutMs` milliseconds', async () => { mockedFs.existsSync.mockReturnValue(false); const waitForPromise = waitForPidFile(APP_NAME, 200); let resolved = false; let err: Error | undefined = undefined; waitForPromise .then(() => { resolved = true; }) .catch((_err) => { err = _err; }); // Wait and check twice that the promise has not resolved await sleep(100); expect(resolved).toStrictEqual(false); expect(err).toBeUndefined(); await sleep(200); expect(resolved).toStrictEqual(false); expect(err).toStrictEqual(new Error('Timeout while waiting for PID file')); }); test('getAppPid -- non-numeric PID in PID file is ignored', () => { mockedFs.existsSync.mockReturnValue(true); mockedFs.readFileSync.mockReturnValue('abc'); expect(getAppPid('test-app')).toBeUndefined(); }); test('forceKillApp -- kills running process', () => { const processKillSpy = jest.spyOn(process, 'kill').mockImplementation(); createPidFile('test-app'); forceKillApp('test-app'); removePidFile('test-app'); expect(processKillSpy).toHaveBeenCalledWith(process.pid, 'SIGTERM'); processKillSpy.mockRestore(); }); describe('registerAgentCleanup', () => { let originalExit: typeof process.exit; let processOnMock: jest.SpyInstance; let processEvents: Record<string, (err?: Error) => void> = {}; beforeEach(() => { originalExit = process.exit; process.exit = jest.fn() as unknown as typeof process.exit; processOnMock = jest.spyOn(process, 'on').mockImplementation((event, cb) => { processEvents[event.toString()] = cb; return process; // For chaining }); }); afterEach(() => { process.exit = originalExit; processOnMock.mockClear(); processEvents = {}; deregisterAgentCleanup(); }); test('registers handlers for SIGTERM, SIGINT, SIGHUP, and uncaughtException', () => { const addListenerSpy = jest.spyOn(process, 'on'); registerAgentCleanup(); expect(addListenerSpy).toHaveBeenCalledWith('SIGTERM', expect.any(Function)); expect(addListenerSpy).toHaveBeenCalledWith('SIGINT', expect.any(Function)); expect(addListenerSpy).toHaveBeenCalledWith('SIGHUP', expect.any(Function)); expect(addListenerSpy).toHaveBeenCalledWith('uncaughtException', expect.any(Function)); addListenerSpy.mockClear(); }); test('removes PID file and exits on process.exit', async () => { registerAgentCleanup(); createPidFile(APP_NAME); processEvents.exit?.(); expect(mockedFs.unlinkSync).toHaveBeenCalledWith(TEST_PID_PATH); expect(mockedFs.existsSync).toHaveBeenCalledWith(TEST_PID_PATH); }); test('removes PID file and exits on SIGTERM', async () => { registerAgentCleanup(); createPidFile(APP_NAME); processEvents.SIGTERM?.(); expect(mockedFs.unlinkSync).toHaveBeenCalledWith(TEST_PID_PATH); expect(mockedFs.existsSync).toHaveBeenCalledWith(TEST_PID_PATH); expect(process.exit).toHaveBeenCalledWith(0); }); test('removes PID file and exits on SIGINT', () => { registerAgentCleanup(); createPidFile(APP_NAME); processEvents.SIGINT?.(); expect(mockedFs.unlinkSync).toHaveBeenCalledWith(TEST_PID_PATH); expect(mockedFs.existsSync).toHaveBeenCalledWith(TEST_PID_PATH); expect(process.exit).toHaveBeenCalledWith(0); }); test('removes PID file and exits on SIGHUP', () => { registerAgentCleanup(); createPidFile(APP_NAME); processEvents.SIGHUP?.(); expect(mockedFs.unlinkSync).toHaveBeenCalledWith(TEST_PID_PATH); expect(mockedFs.existsSync).toHaveBeenCalledWith(TEST_PID_PATH); expect(process.exit).toHaveBeenCalledWith(0); }); test('removes PID file and exits on uncaughtException', () => { registerAgentCleanup(); createPidFile(APP_NAME); processEvents.uncaughtException?.(new Error('Test error')); expect(mockedFs.unlinkSync).toHaveBeenCalledWith(TEST_PID_PATH); expect(mockedFs.existsSync).toHaveBeenCalledWith(TEST_PID_PATH); expect(process.exit).toHaveBeenCalledWith(1); }); test('handles non-existent PID file during cleanup and still exits', () => { registerAgentCleanup(); const pidFilePath = createPidFile(APP_NAME); mockedFs.unlinkSync(pidFilePath); mockedFs.unlinkSync.mockReset(); processEvents.SIGTERM?.(); expect(mockedFs.existsSync).toHaveBeenCalledWith(TEST_PID_PATH); expect(mockedFs.unlinkSync).not.toHaveBeenCalled(); expect(process.exit).toHaveBeenCalledWith(0); }); test('handles file system errors during cleanup and still exits', () => { const pidLoggerErrorSpy = jest.spyOn(pidLogger, 'error').mockImplementation(); mockedFs.unlinkSync.mockImplementation(() => { throw new Error('Permission denied'); }); registerAgentCleanup(); createPidFile(APP_NAME); processEvents.SIGTERM?.(); expect(pidLoggerErrorSpy).toHaveBeenCalledWith( expect.stringContaining(`Error removing PID file: ${TEST_PID_PATH}`), new Error('Permission denied') ); expect(process.exit).toHaveBeenCalledWith(0); pidLoggerErrorSpy.mockRestore(); }); }); describe('ensureDirectoryExists', () => { test('Path already exists', () => { mockedFs.existsSync.mockImplementation(() => true); ensureDirectoryExists('test/path/to/file'); expect(mockedFs.mkdirSync).not.toHaveBeenCalled(); }); test('Path does NOT exist already', () => { mockedFs.existsSync.mockImplementation(() => false); ensureDirectoryExists('test/path/to/file'); expect(mockedFs.mkdirSync).toHaveBeenCalledWith('test/path/to/file', { recursive: true }); }); }); });

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/medplum/medplum'

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