mirror of
https://github.com/unraid/api.git
synced 2026-01-01 14:10:10 -06:00
feat: pm2 fully working
This commit is contained in:
@@ -5,7 +5,13 @@
|
||||
"script": "npm",
|
||||
"args": "run boot:unraid",
|
||||
"log": "/var/log/unraid-api/unraid-api.log",
|
||||
"exec_mode": "fork"
|
||||
"exec_mode": "fork",
|
||||
"ignore_watch": [
|
||||
"node_modules",
|
||||
"src",
|
||||
".env.*",
|
||||
"myservers.cfg"
|
||||
]
|
||||
}
|
||||
]
|
||||
}
|
||||
|
||||
@@ -1,31 +0,0 @@
|
||||
import { beforeEach, expect, test, vi } from 'vitest';
|
||||
|
||||
// Preloading imports for faster tests
|
||||
import '@app/cli/commands/restart';
|
||||
import '@app/cli/commands/start';
|
||||
import '@app/cli/commands/stop';
|
||||
|
||||
beforeEach(() => {
|
||||
vi.resetAllMocks();
|
||||
});
|
||||
|
||||
test('calls stop and then start', async () => {
|
||||
vi.mock('@app/cli/commands/start');
|
||||
vi.mock('@app/cli/commands/stop');
|
||||
// Call restart
|
||||
const { restart } = await import('@app/cli/commands/restart');
|
||||
const { start } = await import('@app/cli/commands/start');
|
||||
const { stop } = await import('@app/cli/commands/stop');
|
||||
await restart();
|
||||
|
||||
// Check stop was called
|
||||
expect(vi.mocked(stop).mock.calls.length).toBe(1);
|
||||
|
||||
// Check start was called
|
||||
expect(vi.mocked(start).mock.calls.length).toBe(1);
|
||||
|
||||
// Check stop was called first
|
||||
expect(vi.mocked(stop).mock.invocationCallOrder[0]).toBeLessThan(
|
||||
vi.mocked(start).mock.invocationCallOrder[0]
|
||||
);
|
||||
});
|
||||
@@ -1,78 +0,0 @@
|
||||
import { expect, test, vi } from 'vitest';
|
||||
import { stop } from '@app/cli/commands/stop';
|
||||
import { spawn } from 'node:child_process';
|
||||
import { cliLogger } from '@app/core/log';
|
||||
import { sleep } from '@app/core/utils/misc/sleep';
|
||||
|
||||
import { getAllUnraidApiPids } from '@app/cli/get-unraid-api-pid';
|
||||
import path from 'node:path';
|
||||
|
||||
const spawnUnraidApiTestDaemon = (difficulty: 'easy' | 'hard') => {
|
||||
const pathToJsFile = difficulty === 'easy' ? '../../setup/child-process-easy-to-kill.js' : '../../setup/child-process-hard-to-kill.js';
|
||||
// Spawn child
|
||||
console.log('Spawning child process at', path.join(import.meta.dirname, pathToJsFile));
|
||||
const child = spawn('node', [path.join(import.meta.dirname, pathToJsFile)], {
|
||||
// In the parent set the tracking environment variable
|
||||
env: Object.assign(process.env, { _DAEMONIZE_PROCESS: '1' }),
|
||||
|
||||
stdio: 'ignore',
|
||||
detached: true,
|
||||
});
|
||||
|
||||
// Convert process into daemon
|
||||
child.unref();
|
||||
|
||||
cliLogger.debug('Daemonized successfully!');
|
||||
};
|
||||
|
||||
test('It stops successfully (easy)', async () => {
|
||||
spawnUnraidApiTestDaemon('easy');
|
||||
spawnUnraidApiTestDaemon('easy');
|
||||
|
||||
await sleep(100);
|
||||
|
||||
const { cliLogger } = await import('@app/core/log');
|
||||
const loggerSpy = vi.spyOn(cliLogger, 'info');
|
||||
|
||||
const pids = await getAllUnraidApiPids();
|
||||
expect(pids.length).toBe(2);
|
||||
await stop();
|
||||
const pids2 = await getAllUnraidApiPids();
|
||||
expect(pids2.length).toBe(0);
|
||||
expect(loggerSpy).toHaveBeenNthCalledWith(1,
|
||||
'Stopping %s unraid-api process(es)...',
|
||||
2,
|
||||
);
|
||||
});
|
||||
|
||||
test('It stops successfully (easy and hard)', async () => {
|
||||
spawnUnraidApiTestDaemon('easy');
|
||||
spawnUnraidApiTestDaemon('easy');
|
||||
spawnUnraidApiTestDaemon('hard');
|
||||
spawnUnraidApiTestDaemon('hard');
|
||||
|
||||
await sleep(100);
|
||||
|
||||
const { cliLogger } = await import('@app/core/log');
|
||||
const loggerSpy = vi.spyOn(cliLogger, 'info');
|
||||
|
||||
const pids = await getAllUnraidApiPids();
|
||||
expect(pids.length).toBe(4);
|
||||
await stop();
|
||||
const pids2 = await getAllUnraidApiPids();
|
||||
expect(pids2.length).toBe(0);
|
||||
expect(loggerSpy).toHaveBeenNthCalledWith(1,
|
||||
'Stopping %s unraid-api process(es)...',
|
||||
4,
|
||||
);
|
||||
expect(loggerSpy).toHaveBeenNthCalledWith(2,
|
||||
'Stopping %s unraid-api process(es)...',
|
||||
2,
|
||||
);
|
||||
expect(loggerSpy).toHaveBeenNthCalledWith(3,
|
||||
'Stopping %s unraid-api process(es)...',
|
||||
2,
|
||||
);
|
||||
expect(loggerSpy).toHaveBeenNthCalledWith(4, 'Process did not exit cleanly, forcing shutdown', expect.any(Error));
|
||||
}, 15_000);
|
||||
|
||||
@@ -1,7 +1,7 @@
|
||||
import ipRegex from 'ip-regex';
|
||||
import readLine from 'readline';
|
||||
import { setEnv } from '@app/cli/set-env';
|
||||
import { getUnraidApiPid } from '@app/cli/get-unraid-api-pid';
|
||||
import { isUnraidApiRunning } from '@app/core/utils/pm2/unraid-api-running';
|
||||
import { cliLogger } from '@app/core/log';
|
||||
import { getters, store } from '@app/store';
|
||||
import { stdout } from 'process';
|
||||
@@ -18,12 +18,8 @@ import { API_VERSION } from '@app/environment';
|
||||
import { loadStateFiles } from '@app/store/modules/emhttp';
|
||||
import { ApolloClient, ApolloQueryResult, NormalizedCacheObject } from '@apollo/client/core';
|
||||
|
||||
type CloudQueryResult = NonNullable<
|
||||
ApolloQueryResult<getCloudQuery>['data']['cloud']
|
||||
>;
|
||||
type ServersQueryResultServer = NonNullable<
|
||||
ApolloQueryResult<getServersQuery>['data']['servers']
|
||||
>[0];
|
||||
type CloudQueryResult = NonNullable<ApolloQueryResult<getCloudQuery>['data']['cloud']>;
|
||||
type ServersQueryResultServer = NonNullable<ApolloQueryResult<getServersQuery>['data']['servers']>[0];
|
||||
|
||||
type Verbosity = '' | '-v' | '-vv';
|
||||
|
||||
@@ -128,8 +124,7 @@ export const getServersData = async ({
|
||||
const hashUrlRegex = () => /(.*)([a-z0-9]{40})(.*)/g;
|
||||
|
||||
export const anonymiseOrigins = (origins?: string[]): string[] => {
|
||||
const originsWithoutSocks =
|
||||
origins?.filter((url) => !url.endsWith('.sock')) ?? [];
|
||||
const originsWithoutSocks = origins?.filter((url) => !url.endsWith('.sock')) ?? [];
|
||||
return originsWithoutSocks
|
||||
.map((origin) =>
|
||||
origin
|
||||
@@ -138,29 +133,17 @@ export const anonymiseOrigins = (origins?: string[]): string[] => {
|
||||
// Replace ipv4 address using . separator with "IPV4ADDRESS"
|
||||
.replace(ipRegex(), 'IPV4ADDRESS')
|
||||
// Replace ipv4 address using - separator with "IPV4ADDRESS"
|
||||
.replace(
|
||||
new RegExp(ipRegex().toString().replace('\\.', '-')),
|
||||
'/IPV4ADDRESS'
|
||||
)
|
||||
.replace(new RegExp(ipRegex().toString().replace('\\.', '-')), '/IPV4ADDRESS')
|
||||
// Report WAN port
|
||||
.replace(
|
||||
`:${getters.config().remote.wanport || 443}`,
|
||||
':WANPORT'
|
||||
)
|
||||
.replace(`:${getters.config().remote.wanport || 443}`, ':WANPORT')
|
||||
)
|
||||
.filter(Boolean);
|
||||
};
|
||||
|
||||
const getAllowedOrigins = (
|
||||
cloud: CloudQueryResult | null,
|
||||
v: Verbosity
|
||||
): string[] | null => {
|
||||
const getAllowedOrigins = (cloud: CloudQueryResult | null, v: Verbosity): string[] | null => {
|
||||
switch (v) {
|
||||
case '-vv':
|
||||
return (
|
||||
cloud?.allowedOrigins.filter((url) => !url.endsWith('.sock')) ??
|
||||
[]
|
||||
);
|
||||
return cloud?.allowedOrigins.filter((url) => !url.endsWith('.sock')) ?? [];
|
||||
case '-v':
|
||||
return anonymiseOrigins(cloud?.allowedOrigins ?? []);
|
||||
default:
|
||||
@@ -168,37 +151,23 @@ const getAllowedOrigins = (
|
||||
}
|
||||
};
|
||||
|
||||
const getReadableCloudDetails = (
|
||||
reportObject: ReportObject,
|
||||
v: Verbosity
|
||||
): string => {
|
||||
const error = reportObject.cloud.error
|
||||
? `\n ERROR [${reportObject.cloud.error}]`
|
||||
: '';
|
||||
const status = reportObject.cloud.status
|
||||
? reportObject.cloud.status
|
||||
: 'disconnected';
|
||||
const ip =
|
||||
reportObject.cloud.ip && v !== ''
|
||||
? `\n IP: [${reportObject.cloud.ip}]`
|
||||
: '';
|
||||
const getReadableCloudDetails = (reportObject: ReportObject, v: Verbosity): string => {
|
||||
const error = reportObject.cloud.error ? `\n ERROR [${reportObject.cloud.error}]` : '';
|
||||
const status = reportObject.cloud.status ? reportObject.cloud.status : 'disconnected';
|
||||
const ip = reportObject.cloud.ip && v !== '' ? `\n IP: [${reportObject.cloud.ip}]` : '';
|
||||
return `
|
||||
STATUS: [${status}] ${ip} ${error}`;
|
||||
};
|
||||
|
||||
const getReadableMinigraphDetails = (reportObject: ReportObject): string => {
|
||||
const statusLine = `STATUS: [${reportObject.minigraph.status}]`;
|
||||
const errorLine = reportObject.minigraph.error
|
||||
? ` ERROR: [${reportObject.minigraph.error}]`
|
||||
: null;
|
||||
const errorLine = reportObject.minigraph.error ? ` ERROR: [${reportObject.minigraph.error}]` : null;
|
||||
const timeoutLine = reportObject.minigraph.timeout
|
||||
? ` TIMEOUT: [${(reportObject.minigraph.timeout || 1) / 1_000}s]`
|
||||
: null; // 1 in case of divide by zero
|
||||
|
||||
return `
|
||||
${statusLine}${errorLine ? `\n${errorLine}` : ''}${
|
||||
timeoutLine ? `\n${timeoutLine}` : ''
|
||||
}`;
|
||||
${statusLine}${errorLine ? `\n${errorLine}` : ''}${timeoutLine ? `\n${timeoutLine}` : ''}`;
|
||||
};
|
||||
|
||||
// Convert server to string output
|
||||
@@ -211,10 +180,7 @@ const serverToString = (v: Verbosity) => (server: ServersQueryResultServer) =>
|
||||
: ''
|
||||
}`;
|
||||
|
||||
const getReadableServerDetails = (
|
||||
reportObject: ReportObject,
|
||||
v: Verbosity
|
||||
): string => {
|
||||
const getReadableServerDetails = (reportObject: ReportObject, v: Verbosity): string => {
|
||||
if (!reportObject.servers) {
|
||||
return '';
|
||||
}
|
||||
@@ -232,9 +198,7 @@ const getReadableServerDetails = (
|
||||
return `
|
||||
SERVERS:
|
||||
ONLINE: ${reportObject.servers.online.map(serverToString(v)).join(',')}
|
||||
OFFLINE: ${reportObject.servers.offline
|
||||
.map(serverToString(v))
|
||||
.join(',')}${invalid}`;
|
||||
OFFLINE: ${reportObject.servers.offline.map(serverToString(v)).join(',')}${invalid}`;
|
||||
};
|
||||
|
||||
const getReadableAllowedOrigins = (reportObject: ReportObject): string => {
|
||||
@@ -258,7 +222,7 @@ const getVerbosity = (argv: string[]): Verbosity => {
|
||||
|
||||
return '';
|
||||
};
|
||||
|
||||
|
||||
export const report = async (...argv: string[]) => {
|
||||
// Check if the user has raw output enabled
|
||||
const rawOutput = argv.includes('--raw');
|
||||
@@ -290,7 +254,7 @@ export const report = async (...argv: string[]) => {
|
||||
const v = getVerbosity(argv);
|
||||
|
||||
// Find all processes called "unraid-api" which aren't this process
|
||||
const unraidApiPid = await getUnraidApiPid();
|
||||
const unraidApiRunning = await isUnraidApiRunning();
|
||||
|
||||
// Load my servers config file into store
|
||||
await store.dispatch(loadConfigFile());
|
||||
@@ -316,43 +280,37 @@ export const report = async (...argv: string[]) => {
|
||||
const reportObject: ReportObject = {
|
||||
os: {
|
||||
serverName: emhttp.var.name,
|
||||
version: emhttp.var.version
|
||||
version: emhttp.var.version,
|
||||
},
|
||||
api: {
|
||||
version: API_VERSION,
|
||||
status: unraidApiPid ? 'running' : 'stopped',
|
||||
environment:
|
||||
process.env.ENVIRONMENT ??
|
||||
'THIS_WILL_BE_REPLACED_WHEN_BUILT',
|
||||
status: unraidApiRunning ? 'running' : 'stopped',
|
||||
environment: process.env.ENVIRONMENT ?? 'THIS_WILL_BE_REPLACED_WHEN_BUILT',
|
||||
nodeVersion: process.version,
|
||||
},
|
||||
apiKey: isApiKeyValid ? 'valid' : cloud?.apiKey.error ?? 'invalid',
|
||||
...(servers ? { servers } : {}),
|
||||
myServers: {
|
||||
status: config?.remote?.username
|
||||
? 'authenticated'
|
||||
: 'signed out',
|
||||
status: config?.remote?.username ? 'authenticated' : 'signed out',
|
||||
...(config?.remote?.username
|
||||
? { myServersUsername: config?.remote?.username?.includes('@') ? 'REDACTED' : config?.remote.username }
|
||||
? {
|
||||
myServersUsername: config?.remote?.username?.includes('@')
|
||||
? 'REDACTED'
|
||||
: config?.remote.username,
|
||||
}
|
||||
: {}),
|
||||
},
|
||||
minigraph: {
|
||||
status: cloud?.minigraphql.status ?? MinigraphStatus.PRE_INIT,
|
||||
timeout: cloud?.minigraphql.timeout ?? null,
|
||||
error:
|
||||
cloud?.minigraphql.error ?? !cloud?.minigraphql.status
|
||||
? 'API Disconnected'
|
||||
: null,
|
||||
cloud?.minigraphql.error ?? !cloud?.minigraphql.status ? 'API Disconnected' : null,
|
||||
},
|
||||
cloud: {
|
||||
status: cloud?.cloud.status ?? 'error',
|
||||
...(cloud?.cloud.error ? { error: cloud.cloud.error } : {}),
|
||||
...(cloud?.cloud.status === 'ok'
|
||||
? { ip: cloud.cloud.ip ?? 'NO_IP' }
|
||||
: {}),
|
||||
...(getAllowedOrigins(cloud, v)
|
||||
? { allowedOrigins: getAllowedOrigins(cloud, v) }
|
||||
: {}),
|
||||
...(cloud?.cloud.status === 'ok' ? { ip: cloud.cloud.ip ?? 'NO_IP' } : {}),
|
||||
...(getAllowedOrigins(cloud, v) ? { allowedOrigins: getAllowedOrigins(cloud, v) } : {}),
|
||||
},
|
||||
};
|
||||
|
||||
@@ -365,8 +323,8 @@ export const report = async (...argv: string[]) => {
|
||||
|
||||
if (jsonReport) {
|
||||
stdout.write(JSON.stringify(reportObject) + '\n');
|
||||
stdoutLogger.close();
|
||||
return reportObject;
|
||||
stdoutLogger.close();
|
||||
return reportObject;
|
||||
} else {
|
||||
// Generate the actual report
|
||||
const report = `
|
||||
@@ -383,9 +341,7 @@ MY_SERVERS: ${reportObject.myServers.status}${
|
||||
: ''
|
||||
}
|
||||
CLOUD: ${getReadableCloudDetails(reportObject, v)}
|
||||
MINI-GRAPH: ${getReadableMinigraphDetails(
|
||||
reportObject
|
||||
)}${getReadableServerDetails(
|
||||
MINI-GRAPH: ${getReadableMinigraphDetails(reportObject)}${getReadableServerDetails(
|
||||
reportObject,
|
||||
v
|
||||
)}${getReadableAllowedOrigins(reportObject)}
|
||||
@@ -400,9 +356,7 @@ MINI-GRAPH: ${getReadableMinigraphDetails(
|
||||
console.log({ error });
|
||||
if (error instanceof Error) {
|
||||
cliLogger.trace(error);
|
||||
stdoutLogger.write(
|
||||
`\nFailed generating report with "${error.message}"\n`
|
||||
);
|
||||
stdoutLogger.write(`\nFailed generating report with "${error.message}"\n`);
|
||||
return;
|
||||
}
|
||||
|
||||
|
||||
@@ -7,6 +7,6 @@ import { execSync } from 'child_process';
|
||||
export const start = async () => {
|
||||
cliLogger.info('Starting unraid-api@v%s', API_VERSION);
|
||||
|
||||
execSync('pm2 start ecosystem.config.json --update-env', { env: process.env, stdio: 'inherit' });
|
||||
execSync(`pm2 start ecosystem.config.json --update-env`, { env: process.env, stdio: 'inherit' });
|
||||
// Start API
|
||||
};
|
||||
|
||||
@@ -1,19 +1,5 @@
|
||||
import prettyMs from 'pretty-ms';
|
||||
import pidUsage from 'pidusage';
|
||||
import { cliLogger } from '@app/core/log';
|
||||
import { getUnraidApiPid } from '@app/cli/get-unraid-api-pid';
|
||||
import { setEnv } from '@app/cli/set-env';
|
||||
import { execSync } from 'child_process';
|
||||
|
||||
export const status = async () => {
|
||||
setEnv('LOG_TYPE', 'raw');
|
||||
|
||||
// Find all processes called "unraid-api" which aren't this process
|
||||
const unraidApiPid = await getUnraidApiPid();
|
||||
if (!unraidApiPid) {
|
||||
cliLogger.info('Found no running processes.');
|
||||
return;
|
||||
}
|
||||
|
||||
const stats = await pidUsage(unraidApiPid);
|
||||
cliLogger.info(`API has been running for ${prettyMs(stats.elapsed)} and is in "${process.env.ENVIRONMENT ?? 'ERR: Unknown Environment'}" mode!`);
|
||||
execSync('pm2 status unraid-api', { stdio: 'inherit' });
|
||||
};
|
||||
|
||||
@@ -1,27 +1,18 @@
|
||||
import { copyFile, readFile, writeFile } from 'fs/promises';
|
||||
import { join } from 'path';
|
||||
import { cliLogger } from '@app/core/log';
|
||||
import { getUnraidApiPid } from '@app/cli/get-unraid-api-pid';
|
||||
import { setEnv } from '@app/cli/set-env';
|
||||
import { getters } from '@app/store';
|
||||
import { start } from '@app/cli/commands/start';
|
||||
import { stop } from '@app/cli/commands/stop';
|
||||
|
||||
export const switchEnv = async () => {
|
||||
setEnv('LOG_TYPE', 'raw');
|
||||
|
||||
const paths = getters.paths();
|
||||
const basePath = paths['unraid-api-base'];
|
||||
const envFlashFilePath = paths['myservers-env'];
|
||||
const envFile = await readFile(envFlashFilePath, 'utf-8').catch(() => '');
|
||||
|
||||
let shouldStartAfterRunning = false;
|
||||
if (await getUnraidApiPid()) {
|
||||
cliLogger.info('unraid-api is running, stopping...');
|
||||
// Stop Running Process
|
||||
await stop();
|
||||
shouldStartAfterRunning = true;
|
||||
}
|
||||
await stop();
|
||||
|
||||
cliLogger.debug(
|
||||
'Checking %s for current ENV, found %s',
|
||||
@@ -70,11 +61,5 @@ export const switchEnv = async () => {
|
||||
await copyFile(source, destination);
|
||||
|
||||
cliLogger.info('Now using %s', newEnv);
|
||||
if (shouldStartAfterRunning) {
|
||||
cliLogger.debug('Restarting unraid-api');
|
||||
// Start Process
|
||||
await start();
|
||||
} else {
|
||||
cliLogger.info('Run "unraid-api start" to start the API.');
|
||||
}
|
||||
await start();
|
||||
};
|
||||
|
||||
@@ -4,6 +4,7 @@ import { type Flags, mainOptions, options, args } from '@app/cli/options';
|
||||
import { setEnv } from '@app/cli/set-env';
|
||||
import { env } from '@app/dotenv';
|
||||
import { getters } from '@app/store';
|
||||
import { execSync } from 'child_process';
|
||||
|
||||
const command = mainOptions.command as unknown as string;
|
||||
|
||||
@@ -46,11 +47,13 @@ export const main = async (...argv: string[]) => {
|
||||
start: import('@app/cli/commands/start').then((pkg) => pkg.start),
|
||||
stop: import('@app/cli/commands/stop').then((pkg) => pkg.stop),
|
||||
restart: import('@app/cli/commands/restart').then((pkg) => pkg.restart),
|
||||
logs: async () => execSync('pm2 logs unraid-api --lines 200', { stdio: 'inherit' }),
|
||||
'switch-env': import('@app/cli/commands/switch-env').then((pkg) => pkg.switchEnv),
|
||||
version: import('@app/cli/commands/version').then((pkg) => pkg.version),
|
||||
status: import('@app/cli/commands/status').then((pkg) => pkg.status),
|
||||
report: import('@app/cli/commands/report').then((pkg) => pkg.report),
|
||||
'validate-token': import('@app/cli/commands/validate-token').then((pkg) => pkg.validateToken),
|
||||
|
||||
};
|
||||
|
||||
// Unknown command
|
||||
|
||||
29
api/src/core/utils/pm2/unraid-api-running.ts
Normal file
29
api/src/core/utils/pm2/unraid-api-running.ts
Normal file
@@ -0,0 +1,29 @@
|
||||
import pm2 from 'pm2';
|
||||
|
||||
export const isUnraidApiRunning = async (): Promise<boolean | undefined> => {
|
||||
return new Promise((resolve, reject) => {
|
||||
pm2.connect(function (err) {
|
||||
if (err) {
|
||||
console.error(err);
|
||||
reject('Could not connect to pm2');
|
||||
}
|
||||
|
||||
pm2.describe('unraid-api', function (err, processDescription) {
|
||||
console.log(err)
|
||||
if (err || processDescription.length === 0) {
|
||||
console.log(false); // Service not found or error occurred
|
||||
resolve(false);
|
||||
} else {
|
||||
const isOnline = processDescription?.[0]?.pm2_env?.status === 'online';
|
||||
console.log(isOnline); // Output true if online, false otherwise
|
||||
resolve(isOnline);
|
||||
}
|
||||
|
||||
pm2.disconnect();
|
||||
|
||||
|
||||
});
|
||||
|
||||
});
|
||||
});
|
||||
};
|
||||
Reference in New Issue
Block a user