Skip to content

Commit 317e0fa

Browse files
authored
Revert "feat!(api): swap daemonizer to nodemon instead of PM2" (#1836)
Reverts #1798
1 parent 331c913 commit 317e0fa

File tree

61 files changed

+1383
-1800
lines changed

Some content is hidden

Large Commits have some content hidden by default. Use the searchbox below for content that may be hidden.

61 files changed

+1383
-1800
lines changed

api/ecosystem.config.json

Lines changed: 22 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,22 @@
1+
{
2+
"$schema": "https://json.schemastore.org/pm2-ecosystem",
3+
"apps": [
4+
{
5+
"name": "unraid-api",
6+
"script": "./dist/main.js",
7+
"cwd": "/usr/local/unraid-api",
8+
"exec_mode": "fork",
9+
"wait_ready": true,
10+
"listen_timeout": 15000,
11+
"max_restarts": 10,
12+
"min_uptime": 10000,
13+
"watch": false,
14+
"interpreter": "/usr/local/bin/node",
15+
"ignore_watch": ["node_modules", "src", ".env.*", "myservers.cfg"],
16+
"out_file": "/var/log/graphql-api.log",
17+
"error_file": "/var/log/graphql-api.log",
18+
"merge_logs": true,
19+
"kill_timeout": 10000
20+
}
21+
]
22+
}

api/generated-schema.graphql

Lines changed: 2 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -1673,8 +1673,8 @@ type PackageVersions {
16731673
"""npm version"""
16741674
npm: String
16751675

1676-
"""nodemon version"""
1677-
nodemon: String
1676+
"""pm2 version"""
1677+
pm2: String
16781678

16791679
"""Git version"""
16801680
git: String

api/legacy/generated-schema-legacy.graphql

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -1257,7 +1257,7 @@ type Versions {
12571257
openssl: String
12581258
perl: String
12591259
php: String
1260-
nodemon: String
1260+
pm2: String
12611261
postfix: String
12621262
postgresql: String
12631263
python: String

api/nodemon.json

Lines changed: 0 additions & 17 deletions
This file was deleted.

api/package.json

Lines changed: 2 additions & 3 deletions
Original file line numberDiff line numberDiff line change
@@ -129,7 +129,6 @@
129129
"nestjs-pino": "4.4.0",
130130
"node-cache": "5.1.2",
131131
"node-window-polyfill": "1.0.4",
132-
"nodemon": "3.1.10",
133132
"openid-client": "6.6.4",
134133
"p-retry": "7.0.0",
135134
"passport-custom": "1.1.1",
@@ -138,7 +137,7 @@
138137
"pino": "9.9.0",
139138
"pino-http": "10.5.0",
140139
"pino-pretty": "13.1.1",
141-
"proper-lockfile": "^4.1.2",
140+
"pm2": "6.0.8",
142141
"reflect-metadata": "^0.1.14",
143142
"rxjs": "7.8.2",
144143
"semver": "7.7.2",
@@ -189,7 +188,6 @@
189188
"@types/mustache": "4.2.6",
190189
"@types/node": "22.18.0",
191190
"@types/pify": "6.1.0",
192-
"@types/proper-lockfile": "^4.1.4",
193191
"@types/semver": "7.7.0",
194192
"@types/sendmail": "1.4.7",
195193
"@types/stoppable": "1.1.3",
@@ -205,6 +203,7 @@
205203
"eslint-plugin-no-relative-import-paths": "1.6.1",
206204
"eslint-plugin-prettier": "5.5.4",
207205
"jiti": "2.5.1",
206+
"nodemon": "3.1.10",
208207
"prettier": "3.6.2",
209208
"rollup-plugin-node-externals": "8.1.0",
210209
"supertest": "7.1.4",

api/scripts/build.ts

Lines changed: 2 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -7,7 +7,7 @@ import { exit } from 'process';
77
import type { PackageJson } from 'type-fest';
88
import { $, cd } from 'zx';
99

10-
import { getDeploymentVersion } from '@app/../scripts/get-deployment-version.js';
10+
import { getDeploymentVersion } from './get-deployment-version.js';
1111

1212
type ApiPackageJson = PackageJson & {
1313
version: string;
@@ -94,7 +94,7 @@ try {
9494

9595
await writeFile('./deploy/pack/package.json', JSON.stringify(parsedPackageJson, null, 4));
9696
// Copy necessary files to the pack directory
97-
await $`cp -r dist README.md .env.* nodemon.json ./deploy/pack/`;
97+
await $`cp -r dist README.md .env.* ecosystem.config.json ./deploy/pack/`;
9898

9999
// Change to the pack directory and install dependencies
100100
cd('./deploy/pack');
Lines changed: 5 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,5 @@
1+
/* eslint-disable no-undef */
2+
// Dummy process for PM2 testing
3+
setInterval(() => {
4+
// Keep process alive
5+
}, 1000);
Lines changed: 222 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,222 @@
1+
import { existsSync } from 'node:fs';
2+
import { homedir } from 'node:os';
3+
import { join } from 'node:path';
4+
import { fileURLToPath } from 'node:url';
5+
6+
import { execa } from 'execa';
7+
import pm2 from 'pm2';
8+
import { afterAll, afterEach, beforeAll, describe, expect, it } from 'vitest';
9+
10+
import { isUnraidApiRunning } from '@app/core/utils/pm2/unraid-api-running.js';
11+
12+
const __dirname = fileURLToPath(new URL('.', import.meta.url));
13+
const PROJECT_ROOT = join(__dirname, '../../../../..');
14+
const DUMMY_PROCESS_PATH = join(__dirname, 'dummy-process.js');
15+
const CLI_PATH = join(PROJECT_ROOT, 'dist/cli.js');
16+
const TEST_PROCESS_NAME = 'test-unraid-api';
17+
18+
// Shared PM2 connection state
19+
let pm2Connected = false;
20+
21+
// Helper to ensure PM2 connection is established
22+
async function ensurePM2Connection() {
23+
if (pm2Connected) return;
24+
25+
return new Promise<void>((resolve, reject) => {
26+
pm2.connect((err) => {
27+
if (err) {
28+
reject(err);
29+
return;
30+
}
31+
pm2Connected = true;
32+
resolve();
33+
});
34+
});
35+
}
36+
37+
// Helper to delete specific test processes (lightweight, reuses connection)
38+
async function deleteTestProcesses() {
39+
if (!pm2Connected) {
40+
// No connection, nothing to clean up
41+
return;
42+
}
43+
44+
const deletePromise = new Promise<void>((resolve) => {
45+
// Delete specific processes we might have created
46+
const processNames = ['unraid-api', TEST_PROCESS_NAME];
47+
let deletedCount = 0;
48+
49+
const deleteNext = () => {
50+
if (deletedCount >= processNames.length) {
51+
resolve();
52+
return;
53+
}
54+
55+
const processName = processNames[deletedCount];
56+
pm2.delete(processName, () => {
57+
// Ignore errors, process might not exist
58+
deletedCount++;
59+
deleteNext();
60+
});
61+
};
62+
63+
deleteNext();
64+
});
65+
66+
const timeoutPromise = new Promise<void>((resolve) => {
67+
setTimeout(() => resolve(), 3000); // 3 second timeout
68+
});
69+
70+
return Promise.race([deletePromise, timeoutPromise]);
71+
}
72+
73+
// Helper to ensure PM2 is completely clean (heavy cleanup with daemon kill)
74+
async function cleanupAllPM2Processes() {
75+
// First delete test processes if we have a connection
76+
if (pm2Connected) {
77+
await deleteTestProcesses();
78+
}
79+
80+
return new Promise<void>((resolve) => {
81+
// Always connect fresh for daemon kill (in case we weren't connected)
82+
pm2.connect((err) => {
83+
if (err) {
84+
// If we can't connect, assume PM2 is not running
85+
pm2Connected = false;
86+
resolve();
87+
return;
88+
}
89+
90+
// Kill the daemon to ensure fresh state
91+
pm2.killDaemon(() => {
92+
pm2.disconnect();
93+
pm2Connected = false;
94+
// Small delay to let PM2 fully shutdown
95+
setTimeout(resolve, 500);
96+
});
97+
});
98+
});
99+
}
100+
101+
describe.skipIf(!!process.env.CI)('PM2 integration tests', () => {
102+
beforeAll(async () => {
103+
// Set PM2_HOME to use home directory for testing (not /var/log)
104+
process.env.PM2_HOME = join(homedir(), '.pm2');
105+
106+
// Build the CLI if it doesn't exist (only for CLI tests)
107+
if (!existsSync(CLI_PATH)) {
108+
console.log('Building CLI for integration tests...');
109+
try {
110+
await execa('pnpm', ['build'], {
111+
cwd: PROJECT_ROOT,
112+
stdio: 'inherit',
113+
timeout: 120000, // 2 minute timeout for build
114+
});
115+
} catch (error) {
116+
console.error('Failed to build CLI:', error);
117+
throw new Error(
118+
'Cannot run CLI integration tests without built CLI. Run `pnpm build` first.'
119+
);
120+
}
121+
}
122+
123+
// Only do a full cleanup once at the beginning
124+
await cleanupAllPM2Processes();
125+
}, 150000); // 2.5 minute timeout for setup
126+
127+
afterAll(async () => {
128+
// Only do a full cleanup once at the end
129+
await cleanupAllPM2Processes();
130+
});
131+
132+
afterEach(async () => {
133+
// Lightweight cleanup after each test - just delete our test processes
134+
await deleteTestProcesses();
135+
}, 5000); // 5 second timeout for cleanup
136+
137+
describe('isUnraidApiRunning function', () => {
138+
it('should return false when PM2 is not running the unraid-api process', async () => {
139+
const result = await isUnraidApiRunning();
140+
expect(result).toBe(false);
141+
});
142+
143+
it('should return true when PM2 has unraid-api process running', async () => {
144+
// Ensure PM2 connection
145+
await ensurePM2Connection();
146+
147+
// Start a dummy process with the name 'unraid-api'
148+
await new Promise<void>((resolve, reject) => {
149+
pm2.start(
150+
{
151+
script: DUMMY_PROCESS_PATH,
152+
name: 'unraid-api',
153+
},
154+
(startErr) => {
155+
if (startErr) return reject(startErr);
156+
resolve();
157+
}
158+
);
159+
});
160+
161+
// Give PM2 time to start the process
162+
await new Promise((resolve) => setTimeout(resolve, 2000));
163+
164+
const result = await isUnraidApiRunning();
165+
expect(result).toBe(true);
166+
}, 30000);
167+
168+
it('should return false when unraid-api process is stopped', async () => {
169+
// Ensure PM2 connection
170+
await ensurePM2Connection();
171+
172+
// Start and then stop the process
173+
await new Promise<void>((resolve, reject) => {
174+
pm2.start(
175+
{
176+
script: DUMMY_PROCESS_PATH,
177+
name: 'unraid-api',
178+
},
179+
(startErr) => {
180+
if (startErr) return reject(startErr);
181+
182+
// Stop the process after starting
183+
setTimeout(() => {
184+
pm2.stop('unraid-api', (stopErr) => {
185+
if (stopErr) return reject(stopErr);
186+
resolve();
187+
});
188+
}, 1000);
189+
}
190+
);
191+
});
192+
193+
await new Promise((resolve) => setTimeout(resolve, 1000));
194+
195+
const result = await isUnraidApiRunning();
196+
expect(result).toBe(false);
197+
}, 30000);
198+
199+
it('should handle PM2 connection errors gracefully', async () => {
200+
// Disconnect PM2 first to ensure we're testing fresh connection
201+
await new Promise<void>((resolve) => {
202+
pm2.disconnect();
203+
pm2Connected = false;
204+
setTimeout(resolve, 100);
205+
});
206+
207+
// Set an invalid PM2_HOME to force connection failure
208+
const originalPM2Home = process.env.PM2_HOME;
209+
process.env.PM2_HOME = '/invalid/path/that/does/not/exist';
210+
211+
const result = await isUnraidApiRunning();
212+
expect(result).toBe(false);
213+
214+
// Restore original PM2_HOME
215+
if (originalPM2Home) {
216+
process.env.PM2_HOME = originalPM2Home;
217+
} else {
218+
delete process.env.PM2_HOME;
219+
}
220+
}, 15000); // 15 second timeout to allow for the Promise.race timeout
221+
});
222+
});

api/src/__test__/core/utils/process/unraid-api-running.integration.test.ts

Lines changed: 0 additions & 54 deletions
This file was deleted.

0 commit comments

Comments
 (0)