mirror of
https://github.com/unraid/api.git
synced 2026-01-07 09:10:05 -06:00
feat: Add process termination and management in NodemonService
- Implemented findDirectMainPids and terminatePids methods to identify and terminate existing unraid-api processes before starting nodemon. - Enhanced the start method to include checks for running processes, ensuring proper cleanup and logging. - Updated unit tests to validate the new process management functionality, improving overall robustness.
This commit is contained in:
@@ -48,6 +48,14 @@ describe('NodemonService', () => {
|
||||
NodemonService.prototype as unknown as { findMatchingNodemonPids: () => Promise<number[]> },
|
||||
'findMatchingNodemonPids'
|
||||
);
|
||||
const findDirectMainSpy = vi.spyOn(
|
||||
NodemonService.prototype as unknown as { findDirectMainPids: () => Promise<number[]> },
|
||||
'findDirectMainPids'
|
||||
);
|
||||
const terminateSpy = vi.spyOn(
|
||||
NodemonService.prototype as unknown as { terminatePids: (pids: number[]) => Promise<void> },
|
||||
'terminatePids'
|
||||
);
|
||||
|
||||
beforeEach(() => {
|
||||
vi.clearAllMocks();
|
||||
@@ -57,6 +65,8 @@ describe('NodemonService', () => {
|
||||
vi.mocked(fileExists).mockResolvedValue(false);
|
||||
killSpy.mockReturnValue(true);
|
||||
findMatchingSpy.mockResolvedValue([]);
|
||||
findDirectMainSpy.mockResolvedValue([]);
|
||||
terminateSpy.mockResolvedValue();
|
||||
stopPm2Spy.mockResolvedValue();
|
||||
});
|
||||
|
||||
@@ -272,6 +282,35 @@ describe('NodemonService', () => {
|
||||
expect(execa).not.toHaveBeenCalled();
|
||||
});
|
||||
|
||||
it('terminates direct main.js processes before starting nodemon', async () => {
|
||||
const service = new NodemonService(logger);
|
||||
findMatchingSpy.mockResolvedValue([]);
|
||||
findDirectMainSpy.mockResolvedValue([321, 654]);
|
||||
|
||||
const logStream = { pipe: vi.fn(), close: vi.fn() };
|
||||
vi.mocked(createWriteStream).mockReturnValue(
|
||||
logStream as unknown as ReturnType<typeof createWriteStream>
|
||||
);
|
||||
const stdout = { pipe: vi.fn() };
|
||||
const stderr = { pipe: vi.fn() };
|
||||
const unref = vi.fn();
|
||||
vi.mocked(execa).mockReturnValue({
|
||||
pid: 777,
|
||||
stdout,
|
||||
stderr,
|
||||
unref,
|
||||
} as unknown as ReturnType<typeof execa>);
|
||||
|
||||
await service.start();
|
||||
|
||||
expect(terminateSpy).toHaveBeenCalledWith([321, 654]);
|
||||
expect(execa).toHaveBeenCalledWith(
|
||||
'/usr/bin/nodemon',
|
||||
['--config', '/etc/unraid-api/nodemon.json', '--quiet'],
|
||||
expect.objectContaining({ cwd: '/usr/local/unraid-api' })
|
||||
);
|
||||
});
|
||||
|
||||
it('returns not running when pid file is missing', async () => {
|
||||
const service = new NodemonService(logger);
|
||||
vi.mocked(fileExists).mockResolvedValue(false);
|
||||
|
||||
@@ -1,7 +1,6 @@
|
||||
import { Injectable } from '@nestjs/common';
|
||||
import { createWriteStream } from 'node:fs';
|
||||
import { mkdir, readFile, rm, writeFile } from 'node:fs/promises';
|
||||
import { homedir } from 'node:os';
|
||||
import { dirname, join } from 'node:path';
|
||||
|
||||
import { execa } from 'execa';
|
||||
@@ -51,20 +50,34 @@ export class NodemonService {
|
||||
)
|
||||
).find(Boolean) ?? null;
|
||||
|
||||
if (!pm2Path) return;
|
||||
|
||||
try {
|
||||
const { stdout } = await execa(pm2Path, ['jlist']);
|
||||
const processes = JSON.parse(stdout);
|
||||
const hasUnraid =
|
||||
Array.isArray(processes) && processes.some((proc) => proc?.name === 'unraid-api');
|
||||
if (!hasUnraid) return;
|
||||
await execa(pm2Path, ['delete', 'unraid-api']);
|
||||
this.logger.info('Stopped pm2-managed unraid-api before starting nodemon.');
|
||||
} catch (error) {
|
||||
// PM2 may not be installed or responding; keep this quiet to avoid noisy startup.
|
||||
this.logger.debug?.('Skipping pm2 cleanup (not installed or not running).');
|
||||
if (pm2Path) {
|
||||
try {
|
||||
const { stdout } = await execa(pm2Path, ['jlist']);
|
||||
const processes = JSON.parse(stdout);
|
||||
const hasUnraid =
|
||||
Array.isArray(processes) && processes.some((proc) => proc?.name === 'unraid-api');
|
||||
if (hasUnraid) {
|
||||
await execa(pm2Path, ['delete', 'unraid-api']);
|
||||
this.logger.info('Stopped pm2-managed unraid-api before starting nodemon.');
|
||||
}
|
||||
} catch (error) {
|
||||
// PM2 may not be installed or responding; keep this quiet to avoid noisy startup.
|
||||
this.logger.debug?.('Skipping pm2 cleanup (not installed or not running).');
|
||||
}
|
||||
}
|
||||
|
||||
// Fallback: directly kill the pm2 daemon and remove its state, even if pm2 binary is missing.
|
||||
try {
|
||||
const pidText = (await readFile(pm2PidPath, 'utf-8')).trim();
|
||||
const pid = Number.parseInt(pidText, 10);
|
||||
if (!Number.isNaN(pid)) {
|
||||
process.kill(pid, 'SIGTERM');
|
||||
this.logger.debug?.(`Sent SIGTERM to pm2 daemon (pid ${pid}).`);
|
||||
}
|
||||
} catch {
|
||||
// ignore
|
||||
}
|
||||
await rm('/var/log/.pm2', { recursive: true, force: true });
|
||||
}
|
||||
|
||||
private async getStoredPid(): Promise<number | null> {
|
||||
@@ -100,6 +113,37 @@ export class NodemonService {
|
||||
}
|
||||
}
|
||||
|
||||
private async findDirectMainPids(): Promise<number[]> {
|
||||
try {
|
||||
const mainPath = join(UNRAID_API_CWD, 'dist', 'main.js');
|
||||
const { stdout } = await execa('ps', ['-eo', 'pid,args']);
|
||||
return stdout
|
||||
.split('\n')
|
||||
.map((line) => line.trim())
|
||||
.map((line) => line.match(/^(\d+)\s+(.*)$/))
|
||||
.filter((match): match is RegExpMatchArray => Boolean(match))
|
||||
.map(([, pid, cmd]) => ({ pid: Number.parseInt(pid, 10), cmd }))
|
||||
.filter(({ cmd }) => cmd.includes(mainPath))
|
||||
.map(({ pid }) => pid)
|
||||
.filter((pid) => Number.isInteger(pid));
|
||||
} catch {
|
||||
return [];
|
||||
}
|
||||
}
|
||||
|
||||
private async terminatePids(pids: number[]) {
|
||||
for (const pid of pids) {
|
||||
try {
|
||||
process.kill(pid, 'SIGTERM');
|
||||
this.logger.debug?.(`Sent SIGTERM to existing unraid-api process (pid ${pid}).`);
|
||||
} catch (error) {
|
||||
this.logger.debug?.(
|
||||
`Failed to send SIGTERM to pid ${pid}: ${error instanceof Error ? error.message : error}`
|
||||
);
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
async start(options: StartOptions = {}) {
|
||||
try {
|
||||
await this.ensureNodemonDependencies();
|
||||
@@ -137,6 +181,14 @@ export class NodemonService {
|
||||
return;
|
||||
}
|
||||
|
||||
const directMainPids = await this.findDirectMainPids();
|
||||
if (directMainPids.length > 0) {
|
||||
this.logger.warn(
|
||||
`Found existing unraid-api process(es) running directly: ${directMainPids.join(', ')}. Stopping them before starting nodemon.`
|
||||
);
|
||||
await this.terminatePids(directMainPids);
|
||||
}
|
||||
|
||||
const overrides = Object.fromEntries(
|
||||
Object.entries(options.env ?? {}).filter(([, value]) => value !== undefined)
|
||||
);
|
||||
|
||||
Reference in New Issue
Block a user