Skip to content

Commit fd1c517

Browse files
authored
refactor(api): pm2 usage in cli (#1104)
* invoke pm2 via PM2Service * fix `unraid-api logs` command * default to LOG_LEVEL=debug in non-production envs * rm pm2 dump file after `pm2 update` * add PM2_HOME to `@app/environment`
1 parent 353e012 commit fd1c517

File tree

9 files changed

+138
-59
lines changed

9 files changed

+138
-59
lines changed

api/src/environment.ts

Lines changed: 6 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -1,3 +1,6 @@
1+
import { homedir } from 'node:os';
2+
import { join } from 'node:path';
3+
14
import { version } from 'package.json';
25

36
export const API_VERSION =
@@ -26,9 +29,11 @@ export const LOG_LEVEL = process.env.LOG_LEVEL
2629
? (process.env.LOG_LEVEL.toUpperCase() as 'TRACE' | 'DEBUG' | 'INFO' | 'WARN' | 'ERROR' | 'FATAL')
2730
: process.env.ENVIRONMENT === 'production'
2831
? 'INFO'
29-
: 'TRACE';
32+
: 'DEBUG';
3033
export const MOTHERSHIP_GRAPHQL_LINK = process.env.MOTHERSHIP_GRAPHQL_LINK
3134
? process.env.MOTHERSHIP_GRAPHQL_LINK
3235
: ENVIRONMENT === 'staging'
3336
? 'https://staging.mothership.unraid.net/ws'
3437
: 'https://mothership.unraid.net/ws';
38+
39+
export const PM2_HOME = process.env.PM2_HOME ?? join(homedir(), '.pm2');

api/src/unraid-api/cli/cli.module.ts

Lines changed: 2 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -8,6 +8,7 @@ import { DeveloperCommand } from '@app/unraid-api/cli/developer/developer.comman
88
import { DeveloperQuestions } from '@app/unraid-api/cli/developer/developer.questions';
99
import { LogService } from '@app/unraid-api/cli/log.service';
1010
import { LogsCommand } from '@app/unraid-api/cli/logs.command';
11+
import { PM2Service } from '@app/unraid-api/cli/pm2.service';
1112
import { ReportCommand } from '@app/unraid-api/cli/report.command';
1213
import { RestartCommand } from '@app/unraid-api/cli/restart.command';
1314
import { AddSSOUserCommand } from '@app/unraid-api/cli/sso/add-sso-user.command';
@@ -31,6 +32,7 @@ import { VersionCommand } from '@app/unraid-api/cli/version.command';
3132
RemoveSSOUserQuestionSet,
3233
ListSSOUserCommand,
3334
LogService,
35+
PM2Service,
3436
StartCommand,
3537
StopCommand,
3638
RestartCommand,
Lines changed: 9 additions & 14 deletions
Original file line numberDiff line numberDiff line change
@@ -1,16 +1,14 @@
1-
import { execa } from 'execa';
21
import { Command, CommandRunner, Option } from 'nest-commander';
32

4-
import { ECOSYSTEM_PATH, PM2_PATH } from '@app/consts';
5-
import { LogService } from '@app/unraid-api/cli/log.service';
3+
import { PM2Service } from '@app/unraid-api/cli/pm2.service';
64

75
interface LogsOptions {
86
lines: number;
97
}
108

119
@Command({ name: 'logs' })
1210
export class LogsCommand extends CommandRunner {
13-
constructor(private readonly logger: LogService) {
11+
constructor(private readonly pm2: PM2Service) {
1412
super();
1513
}
1614

@@ -22,15 +20,12 @@ export class LogsCommand extends CommandRunner {
2220

2321
async run(_: string[], options?: LogsOptions): Promise<void> {
2422
const lines = options?.lines ?? 100;
25-
const subprocess = execa(PM2_PATH, ['logs', ECOSYSTEM_PATH, '--lines', lines.toString()]);
26-
subprocess.stdout?.on('data', (data) => {
27-
this.logger.log(data.toString());
28-
});
29-
30-
subprocess.stderr?.on('data', (data) => {
31-
this.logger.error(data.toString());
32-
});
33-
34-
await subprocess;
23+
await this.pm2.run(
24+
{ tag: 'PM2 Logs', stdio: 'inherit' },
25+
'logs',
26+
'unraid-api',
27+
'--lines',
28+
lines.toString()
29+
);
3530
}
3631
}
Lines changed: 71 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,71 @@
1+
import { Injectable } from '@nestjs/common';
2+
import { rm } from 'node:fs/promises';
3+
import { join } from 'node:path';
4+
5+
import type { Options, Result, ResultPromise } from 'execa';
6+
import { execa } from 'execa';
7+
8+
import { PM2_PATH } from '@app/consts';
9+
import { PM2_HOME } from '@app/environment';
10+
import { LogService } from '@app/unraid-api/cli/log.service';
11+
12+
type CmdContext = Options & {
13+
/** A tag for logging & debugging purposes. Should represent the operation being performed. */
14+
tag: string;
15+
/** Default: false.
16+
*
17+
* When true, results will not be automatically handled and logged.
18+
* The caller must handle desired effects, such as logging, error handling, etc.
19+
*/
20+
raw?: boolean;
21+
};
22+
23+
@Injectable()
24+
export class PM2Service {
25+
constructor(private readonly logger: LogService) {}
26+
27+
// Type Overload: if raw is true, return an execa ResultPromise (which is a Promise with extra properties)
28+
run<T extends CmdContext>(context: T & { raw: true }, ...args: string[]): ResultPromise<T>;
29+
30+
// Type Overload: if raw is false, return a plain Promise<Result>
31+
run(context: CmdContext & { raw?: false }, ...args: string[]): Promise<Result>;
32+
33+
/**
34+
* Executes a PM2 command with the provided arguments and environment variables.
35+
*
36+
* @param context - An object containing a tag for logging purposes and optional environment variables (merging with current env).
37+
* @param args - Arguments to pass to the PM2 command. Each arguement is escaped.
38+
* @returns A promise that resolves to a Result object containing the command's output.
39+
* Logs debug information on success and error details on failure.
40+
*/
41+
async run(context: CmdContext, ...args: string[]) {
42+
const { tag, raw, ...execOptions } = context;
43+
const runCommand = () => execa(PM2_PATH, [...args], execOptions satisfies Options);
44+
if (raw) {
45+
return runCommand();
46+
}
47+
return runCommand()
48+
.then((result) => {
49+
this.logger.debug(result.stdout);
50+
this.logger.log(`Operation "${tag}" completed.`);
51+
return result;
52+
})
53+
.catch((result: Result) => {
54+
this.logger.error(`PM2 error occurred from tag "${tag}": ${result.stdout}\n`);
55+
return result;
56+
});
57+
}
58+
59+
/**
60+
* Deletes the PM2 dump file.
61+
*
62+
* This method removes the PM2 dump file located at `~/.pm2/dump.pm2`.
63+
* It logs a message indicating that the PM2 dump has been cleared.
64+
*
65+
* @returns A promise that resolves once the dump file is removed.
66+
*/
67+
async deleteDump(dumpFile = join(PM2_HOME, 'dump.pm2')) {
68+
await rm(dumpFile, { force: true });
69+
this.logger.log('PM2 dump cleared.');
70+
}
71+
}

api/src/unraid-api/cli/restart.command.ts

Lines changed: 13 additions & 8 deletions
Original file line numberDiff line numberDiff line change
@@ -1,28 +1,33 @@
1-
import { execa } from 'execa';
21
import { Command, CommandRunner } from 'nest-commander';
32

4-
import { ECOSYSTEM_PATH, PM2_PATH } from '@app/consts';
3+
import { ECOSYSTEM_PATH } from '@app/consts';
54
import { LogService } from '@app/unraid-api/cli/log.service';
5+
import { PM2Service } from '@app/unraid-api/cli/pm2.service';
66

77
@Command({ name: 'restart', description: 'Restart / Start the Unraid API' })
88
export class RestartCommand extends CommandRunner {
9-
constructor(private readonly logger: LogService) {
9+
constructor(
10+
private readonly logger: LogService,
11+
private readonly pm2: PM2Service
12+
) {
1013
super();
1114
}
1215

1316
async run(_): Promise<void> {
1417
try {
1518
this.logger.info('Restarting the Unraid API...');
16-
const { stderr, stdout } = await execa(PM2_PATH, [
19+
const { stderr, stdout } = await this.pm2.run(
20+
{ tag: 'PM2 Restart', raw: true },
1721
'restart',
1822
ECOSYSTEM_PATH,
19-
'--update-env',
20-
]);
23+
'--update-env'
24+
);
25+
2126
if (stderr) {
22-
this.logger.error(stderr);
27+
this.logger.error(stderr.toString());
2328
process.exit(1);
2429
} else if (stdout) {
25-
this.logger.info(stdout);
30+
this.logger.info(stdout.toString());
2631
} else {
2732
this.logger.info('Unraid API restarted');
2833
}

api/src/unraid-api/cli/start.command.ts

Lines changed: 22 additions & 21 deletions
Original file line numberDiff line numberDiff line change
@@ -1,52 +1,53 @@
1-
import { execa } from 'execa';
21
import { Command, CommandRunner, Option } from 'nest-commander';
32

43
import type { LogLevel } from '@app/core/log';
5-
import { ECOSYSTEM_PATH, PM2_PATH } from '@app/consts';
4+
import { ECOSYSTEM_PATH } from '@app/consts';
65
import { levels } from '@app/core/log';
76
import { LogService } from '@app/unraid-api/cli/log.service';
7+
import { PM2Service } from '@app/unraid-api/cli/pm2.service';
88

99
interface StartCommandOptions {
1010
'log-level'?: string;
1111
}
1212

1313
@Command({ name: 'start' })
1414
export class StartCommand extends CommandRunner {
15-
constructor(private readonly logger: LogService) {
15+
constructor(
16+
private readonly logger: LogService,
17+
private readonly pm2: PM2Service
18+
) {
1619
super();
1720
}
1821

1922
async cleanupPM2State() {
20-
await execa(PM2_PATH, ['stop', ECOSYSTEM_PATH])
21-
.then(({ stdout }) => this.logger.debug(stdout))
22-
.catch(({ stderr }) => this.logger.error('PM2 Stop Error: ' + stderr));
23-
await execa(PM2_PATH, ['delete', ECOSYSTEM_PATH])
24-
.then(({ stdout }) => this.logger.debug(stdout))
25-
.catch(({ stderr }) => this.logger.error('PM2 Delete API Error: ' + stderr));
26-
27-
// Update PM2
28-
await execa(PM2_PATH, ['update'])
29-
.then(({ stdout }) => this.logger.debug(stdout))
30-
.catch(({ stderr }) => this.logger.error('PM2 Update Error: ' + stderr));
23+
await this.pm2.run({ tag: 'PM2 Stop' }, 'stop', ECOSYSTEM_PATH);
24+
await this.pm2.run({ tag: 'PM2 Update' }, 'update');
25+
await this.pm2.deleteDump();
26+
await this.pm2.run({ tag: 'PM2 Delete' }, 'delete', ECOSYSTEM_PATH);
3127
}
3228

3329
async run(_: string[], options: StartCommandOptions): Promise<void> {
3430
this.logger.info('Starting the Unraid API');
3531
await this.cleanupPM2State();
36-
const envLog = options['log-level'] ? `LOG_LEVEL=${options['log-level']}` : '';
37-
const { stderr, stdout } = await execa(`${envLog} ${PM2_PATH}`.trim(), [
32+
33+
const env: Record<string, string> = {};
34+
if (options['log-level']) {
35+
env.LOG_LEVEL = options['log-level'];
36+
}
37+
38+
const { stderr, stdout } = await this.pm2.run(
39+
{ tag: 'PM2 Start', env, raw: true },
3840
'start',
3941
ECOSYSTEM_PATH,
40-
'--update-env',
41-
]);
42+
'--update-env'
43+
);
4244
if (stdout) {
43-
this.logger.log(stdout);
45+
this.logger.log(stdout.toString());
4446
}
4547
if (stderr) {
46-
this.logger.error(stderr);
48+
this.logger.error(stderr.toString());
4749
process.exit(1);
4850
}
49-
process.exit(0);
5051
}
5152

5253
@Option({
Lines changed: 9 additions & 5 deletions
Original file line numberDiff line numberDiff line change
@@ -1,13 +1,17 @@
1-
import { execSync } from 'child_process';
2-
31
import { Command, CommandRunner } from 'nest-commander';
42

5-
import { PM2_PATH } from '@app/consts';
3+
import { PM2Service } from '@app/unraid-api/cli/pm2.service';
64

75
@Command({ name: 'status', description: 'Check status of unraid-api service' })
86
export class StatusCommand extends CommandRunner {
7+
constructor(private readonly pm2: PM2Service) {
8+
super();
9+
}
910
async run(): Promise<void> {
10-
execSync(`${PM2_PATH} status unraid-api`, { stdio: 'inherit' });
11-
process.exit(0);
11+
await this.pm2.run(
12+
{ tag: 'PM2 Status', stdio: 'inherit', shell: 'bash', raw: true },
13+
'status',
14+
'unraid-api'
15+
);
1216
}
1317
}

api/src/unraid-api/cli/stop.command.ts

Lines changed: 5 additions & 9 deletions
Original file line numberDiff line numberDiff line change
@@ -1,22 +1,18 @@
1-
import { execa } from 'execa';
21
import { Command, CommandRunner } from 'nest-commander';
32

4-
import { ECOSYSTEM_PATH, PM2_PATH } from '@app/consts';
5-
import { LogService } from '@app/unraid-api/cli/log.service';
3+
import { ECOSYSTEM_PATH } from '@app/consts';
4+
import { PM2Service } from '@app/unraid-api/cli/pm2.service';
65

76
@Command({
87
name: 'stop',
98
})
109
export class StopCommand extends CommandRunner {
11-
constructor(private readonly logger: LogService) {
10+
constructor(private readonly pm2: PM2Service) {
1211
super();
1312
}
1413
async run() {
15-
const { stderr, stdout } = await execa(PM2_PATH, ['stop', ECOSYSTEM_PATH]);
16-
if (stdout) {
17-
this.logger.info(stdout);
18-
} else if (stderr) {
19-
this.logger.warn(stderr);
14+
const { stderr } = await this.pm2.run({ tag: 'PM2 Stop' }, 'stop', ECOSYSTEM_PATH);
15+
if (stderr) {
2016
process.exit(1);
2117
}
2218
process.exit(0);

api/src/unraid-api/cli/switch-env.command.ts

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -84,6 +84,6 @@ export class SwitchEnvCommand extends CommandRunner {
8484
await copyFile(source, destination);
8585

8686
cliLogger.info('Now using %s', newEnv);
87-
await this.startCommand.run(null, {});
87+
await this.startCommand.run([], {});
8888
}
8989
}

0 commit comments

Comments
 (0)