feat: pm2 initial setup

This commit is contained in:
Eli Bosley
2024-10-23 16:24:57 -04:00
parent 92b5f2226e
commit b0efcc0d51
20 changed files with 1719 additions and 464 deletions

View File

@@ -17,3 +17,4 @@ NODE_TLS_REJECT_UNAUTHORIZED=0
BYPASS_PERMISSION_CHECKS=false
BYPASS_CORS_CHECKS=true
CHOKIDAR_USEPOLLING=true
LOG_TRANSPORT=console

View File

@@ -1,7 +1,7 @@
###########################################################
# Development/Build Image
###########################################################
FROM node:22-bookworm-slim As development
FROM node:20-bookworm-slim As development
# Install build tools and dependencies
RUN apt-get update -y && apt-get install -y \
@@ -24,13 +24,10 @@ ENV NODE_ENV=development
ENV PKG_CACHE_PATH /app/.pkg-cache
RUN mkdir -p ${PKG_CACHE_PATH}
COPY tsconfig.json tsup.config.ts .eslintrc.cjs .npmrc .env.production .env.staging ./
COPY tsconfig.json .eslintrc.ts .npmrc .env.production .env.staging ./
COPY package.json package-lock.json ./
# Install pkg
RUN npm i -g pkg zx
# Install deps
RUN npm i

View File

@@ -21,4 +21,4 @@ dynamicRemoteAccessType="DISABLED"
[upc]
apikey="unupc_fab6ff6ffe51040595c6d9ffb63a353ba16cc2ad7d93f813a2e80a5810"
[connectionStatus]
minigraph="ERROR_RETRYING"
minigraph="CONNECTING"

View File

@@ -23,6 +23,8 @@ x-volumes: &volumes
- ./codegen.yml:/app/codegen.yml
- ./fix-array-type.cjs:/app/fix-array-type.cjs
- /var/run/docker.sock:/var/run/docker.sock
- ./unraid-api.js:/app/unraid-api.js
- ./ecosystem.config.json:/app/ecosystem.config.json
services:

10
api/ecosystem.config.json Normal file
View File

@@ -0,0 +1,10 @@
{
"apps": [
{
"name": "unraid-api",
"script": "npm",
"args": "run tsx:unraid -- boot",
"instances": 1
}
]
}

1823
api/package-lock.json generated

File diff suppressed because it is too large Load Diff

View File

@@ -1,33 +1,14 @@
{
"name": "@unraid/api",
"version": "3.11.0",
"main": "dist/index.js",
"bin": "dist/unraid-api.cjs",
"main": "src/cli/index.ts",
"type": "module",
"repository": "git@github.com:unraid/api.git",
"author": "Alexis Tyler <xo@wvvw.me> (https://wvvw.me/)",
"author": "Lime Technology, Inc. <unraid.net>",
"license": "UNLICENSED",
"engines": {
"node": ">=16.5.0"
},
"pkg": {
"assets": [
"dist/index.cjs",
"node_modules/@vmngr/libvirt/build/Release",
"node_modules/ts-invariant/",
"src/**/*.graphql"
],
"targets": [
"node18-linux-x64"
],
"outputPath": "dist"
},
"scripts": {
"compile": "tsup --config ./tsup.config.ts",
"bundle": "pkg . --public",
"build": "npm run compile && npm run bundle",
"build:docker": "./scripts/dc.sh run --rm builder",
"build-pkg": "./scripts/build.mjs",
"build:pack": "./scripts/build.mjs",
"codegen": "MOTHERSHIP_GRAPHQL_LINK='https://staging.mothership.unraid.net/ws' graphql-codegen --config codegen.yml -r dotenv/config './.env.staging'",
"codegen:watch": "DOTENV_CONFIG_PATH='./.env.staging' graphql-codegen --config codegen.yml --watch -r dotenv/config",
"codegen:local": "NODE_TLS_REJECT_UNAUTHORIZED=0 MOTHERSHIP_GRAPHQL_LINK='https://mothership.localhost/ws' graphql-codegen-esm --config codegen.yml --watch",
@@ -41,6 +22,7 @@
"release": "standard-version",
"typesync": "typesync",
"install:unraid": "./scripts/install-in-unraid.sh",
"tsx:unraid": "tsx -r dotenv/config src/cli.ts",
"start:plugin": "INTROSPECTION=true LOG_MOTHERSHIP_MESSAGES=true LOG_TYPE=pretty LOG_LEVEL=trace unraid-api start --debug",
"start:plugin-verbose": "LOG_CONTEXT=true LOG_MOTHERSHIP_MESSAGES=true LOG_TYPE=pretty LOG_LEVEL=trace unraid-api start --debug",
"run:tsx": "LOG_MOTHERSHIP_MESSAGES=true LOG_TYPE=pretty LOG_LEVEL=trace DOTENV_CONFIG_PATH=./.env.development tsx -r dotenv/config src/cli.ts",
@@ -58,9 +40,13 @@
"files": [
".env.staging",
".env.production",
"dist",
"unraid-api"
"README.md",
"src",
"node_modules/"
],
"bin": {
"unraid-api": "unraid-api.js"
},
"dependencies": {
"@apollo/client": "^3.10.4",
"@apollo/server": "^4.10.4",
@@ -132,6 +118,7 @@
"pino": "^9.1.0",
"pino-http": "^9.0.0",
"pino-pretty": "^11.0.0",
"pm2": "^5.4.2",
"reflect-metadata": "^0.1.14",
"request": "^2.88.2",
"semver": "^7.6.2",
@@ -207,11 +194,6 @@
"optionalDependencies": {
"@vmngr/libvirt": "github:unraid/libvirt"
},
"config": {
"commitizen": {
"path": "./node_modules/cz-conventional-changelog"
}
},
"overrides": {
"eslint": {
"jiti": "2"

View File

@@ -2,71 +2,80 @@
import { exit } from 'process';
import { cd, $ } from 'zx';
import getTags from './get-tags.mjs'
import getTags from './get-tags.mjs';
try {
// Enable colours in output
process.env.FORCE_COLOR = '1';
// Enable colours in output
process.env.FORCE_COLOR = '1';
// Ensure we have the correct working directory
process.env.WORKDIR = process.env.WORKDIR ?? process.env.PWD;
cd(process.env.WORKDIR);
// Ensure we have the correct working directory
process.env.WORKDIR = process.env.WORKDIR ?? process.env.PWD;
cd(process.env.WORKDIR);
// Clean up last deploy
await $`rm -rf ./deploy/release`;
await $`rm -rf ./deploy/pre-pack`;
await $`mkdir -p ./deploy/release/`;
await $`mkdir -p ./deploy/pre-pack/`;
// Clean up last deploy
await $`rm -rf ./deploy/release`;
await $`rm -rf ./deploy/pre-pack`;
await $`mkdir -p ./deploy/release/`;
await $`mkdir -p ./deploy/pre-pack/`;
// Ensure all deps are installed
await $`npm i`;
await $`npm install -g npm-pack-all`;
// Build Generated Types
await $`npm run codegen`;
// Build binary
await $`npm run build`;
// Ensure all deps are installed
await $`npm i`;
// Copy binary + extra files to deployment directory
await $`cp ./dist/api ./deploy/pre-pack/unraid-api`;
await $`cp ./.env.production ./deploy/pre-pack/.env.production`;
await $`cp ./.env.staging ./deploy/pre-pack/.env.staging`;
// Build Generated Types
await $`npm run codegen`;
// Get package details
const { name, version } = await import('../package.json', {
assert: { type: 'json' },
}).then(pkg => pkg.default);
// Copy app files to plugin directory
await $`cp -r ./src/ ./deploy/pre-pack/src/`;
const tags = getTags(process.env);
// Decide whether to use full version or just tag
const isTaggedRelease = tags.isTagged;
const gitShaShort = tags.shortSha;
const deploymentVersion = isTaggedRelease ? version : `${version}+${gitShaShort}`;
// Copy environment to deployment directory
await $`cp ./.env.production ./deploy/pre-pack/.env.production`;
await $`cp ./.env.staging ./deploy/pre-pack/.env.staging`;
// Create deployment package.json
await $`echo ${JSON.stringify({ name, version: deploymentVersion })} > ./deploy/pre-pack/package.json`;
// Get package details
const { name, version, ...rest } = await import('../package.json', {
assert: { type: 'json' },
}).then((pkg) => pkg.default);
// # Create final tgz
await $`cp ./README.md ./deploy/pre-pack/`;
cd('./deploy/pre-pack');
await $`npm pack`;
const tags = getTags(process.env);
// Move unraid-api.tgz to release directory
await $`mv unraid-api-${deploymentVersion}.tgz ../release`;
// Decide whether to use full version or just tag
const isTaggedRelease = tags.isTagged;
const gitShaShort = tags.shortSha;
// Set API_VERSION output based on this command
await $`echo "::set-output name=API_VERSION::${deploymentVersion}"`;
const deploymentVersion = isTaggedRelease ? version : `${version}+${gitShaShort}`;
// Create deployment package.json
await $`echo ${JSON.stringify({
name,
version: deploymentVersion,
...rest,
})} > ./deploy/pre-pack/package.json`;
// # Create final tgz
await $`cp ./README.md ./deploy/pre-pack/`;
cd('./deploy/pre-pack');
// Install production dependencies
await $`npm i --omit=dev`;
await $`npm-pack-all`;
// Move unraid-api.tgz to release directory
await $`mv unraid-api-${deploymentVersion}.tgz ../release`;
// Set API_VERSION output based on this command
await $`echo "::set-output name=API_VERSION::${deploymentVersion}"`;
} catch (error) {
// Error with a command
if (Object.keys(error).includes('stderr')) {
console.log(`Failed building package. Exit code: ${error.exitCode}`);
console.log(`Error: ${error.stderr}`);
} else {
// Normal js error
console.log('Failed building package.');
console.log(`Error: ${error.message}`);
}
// Error with a command
if (Object.keys(error).includes('stderr')) {
console.log(`Failed building package. Exit code: ${error.exitCode}`);
console.log(`Error: ${error.stderr}`);
} else {
// Normal js error
console.log('Failed building package.');
console.log(`Error: ${error.message}`);
}
exit(error.exitCode);
exit(error.exitCode);
}

View File

@@ -0,0 +1,4 @@
export const boot = async () => {
console.log('Booting...', process.env);
await import('@app/index.ts');
}

View File

@@ -1,85 +1,12 @@
import { spawn } from 'child_process';
import { addExitCallback } from 'catch-exit';
import { cliLogger } from '@app/core/log';
import { mainOptions } from '@app/cli/options';
import { logToSyslog } from '@app/cli/log-to-syslog';
import { getters } from '@app/store/index';
import { getAllUnraidApiPids } from '@app/cli/get-unraid-api-pid';
import { API_VERSION } from '@app/environment';
import { execSync } from 'child_process';
/**
* Start a new API process.
*/
export const start = async () => {
// Set process title
process.title = 'unraid-api';
const runningProcesses = await getAllUnraidApiPids();
if (runningProcesses.length > 0) {
cliLogger.info('unraid-api is Already Running!');
cliLogger.info('Run "unraid-api restart" to stop all running processes and restart');
process.exit(1);
}
// Start API
cliLogger.info('Starting unraid-api@v%s', API_VERSION);
// in debug but ARE in the child process
if (mainOptions.debug || process.env._DAEMONIZE_PROCESS) {
// Log when the API exits
addExitCallback((signal, exitCode, error) => {
if (exitCode === 0 || exitCode === 130 || signal === 'SIGTERM') {
logToSyslog('👋 Farewell. UNRAID API shutting down!');
return;
}
// Log when the API crashes
if (signal === 'uncaughtException' && error) {
logToSyslog(`⚠️ Caught exception: ${error.message}`);
}
// Log when we crash
if (exitCode) {
logToSyslog(`⚠️ UNRAID API crashed with exit code ${exitCode}`);
return;
}
logToSyslog('🛑 UNRAID API crashed without an exit code?');
});
logToSyslog('✔️ UNRAID API started successfully!');
}
await import ('@app/index.ts');
if (!mainOptions.debug) {
console.log('Daemonizing process. %s %o', process.execPath, process.argv);
if ('_DAEMONIZE_PROCESS' in process.env) {
// In the child, clean up the tracking environment variable
delete process.env._DAEMONIZE_PROCESS;
} else {
cliLogger.debug('Daemonizing process. %s %o', process.execPath, process.argv);
// Spawn child
// First arg is path (inside PKG), second arg is restart, stop, etc, rest is args to main argument
const [path, , ...rest] = process.argv.slice(1);
const replacedCommand = [path, 'start', ...rest];
const child = spawn(process.execPath, replacedCommand, {
// In the parent set the tracking environment variable
env: Object.assign(process.env, { _DAEMONIZE_PROCESS: '1' }),
// The process MUST have it's cwd set to the
// path where it resides within the Nexe VFS
cwd: getters.paths()['unraid-api-base'],
stdio: 'ignore',
detached: true,
});
// Convert process into daemon
child.unref();
cliLogger.debug('Daemonized successfully!');
// Exit cleanly
process.exit(0);
}
}
execSync('pm2 start ecosystem.config.json --update-env', { env: process.env });
// Start API
};

View File

@@ -1,44 +1,5 @@
import { cliLogger } from '@app/core/log';
import { getAllUnraidApiPids } from '@app/cli/get-unraid-api-pid';
import { sleep } from '@app/core/utils/misc/sleep';
import pRetry from 'p-retry';
/**
* Stop a running API process.
*/
import { execSync } from 'child_process';
export const stop = async () => {
try {
await pRetry(async (attempts) => {
const runningApis = await getAllUnraidApiPids();
if (runningApis.length > 0) {
cliLogger.info('Stopping %s unraid-api process(es)...', runningApis.length);
runningApis.forEach(pid => process.kill(pid, 'SIGTERM'));
await sleep(50);
const newPids = await getAllUnraidApiPids();
if (newPids.length > 0) {
throw new Error('Not all processes have exited yet');
}
} else if (attempts < 1) {
cliLogger.info('Found no running processes.');
}
return true;
}, {
retries: 2,
minTimeout: 1_000,
factor: 1,
});
} catch (error: unknown) {
cliLogger.info('Process did not exit cleanly, forcing shutdown', error);
const processes = await getAllUnraidApiPids();
for (const pid of processes) {
process.kill(pid, 'SIGKILL');
await sleep(100);
}
}
await sleep(500);
execSync('pm2 stop unraid-api');
};

14
api/src/cli/index.ts Normal file → Executable file
View File

@@ -17,10 +17,7 @@ export const main = async (...argv: string[]) => {
setEnv('DEBUG', mainOptions.debug ?? false);
setEnv('ENVIRONMENT', process.env.ENVIRONMENT ?? 'production');
setEnv('PORT', process.env.PORT ?? mainOptions.port ?? '9000');
setEnv(
'LOG_LEVEL',
process.env.LOG_LEVEL ?? mainOptions['log-level'] ?? 'INFO'
);
setEnv('LOG_LEVEL', process.env.LOG_LEVEL ?? mainOptions['log-level'] ?? 'INFO');
if (!process.env.LOG_TRANSPORT) {
if (process.env.ENVIRONMENT === 'production' && !mainOptions.debug) {
setEnv('LOG_TRANSPORT', 'file');
@@ -48,16 +45,13 @@ export const main = async (...argv: string[]) => {
const commands = {
start: import('@app/cli/commands/start').then((pkg) => pkg.start),
stop: import('@app/cli/commands/stop').then((pkg) => pkg.stop),
boot: import('@app/cli/commands/boot').then((pkg) => pkg.boot),
restart: import('@app/cli/commands/restart').then((pkg) => pkg.restart),
'switch-env': import('@app/cli/commands/switch-env').then(
(pkg) => pkg.switchEnv
),
'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
),
'validate-token': import('@app/cli/commands/validate-token').then((pkg) => pkg.validateToken),
};
// Unknown command

View File

@@ -26,6 +26,8 @@ const makeLoggingDirectoryIfNotExists = () => {
}
};
console.log(LOG_TRANSPORT);
if (LOG_TRANSPORT === 'file') {
makeLoggingDirectoryIfNotExists();
}

View File

@@ -1,3 +1,4 @@
/* eslint-disable */
import * as types from './graphql.js';
import type { TypedDocumentNode as DocumentNode } from '@graphql-typed-document-node/core';

View File

@@ -1,3 +1,4 @@
/* eslint-disable */
import { z } from 'zod'
import { AccessUrlInput, ArrayCapacityBytesInput, ArrayCapacityInput, ClientType, ConfigErrorState, DashboardAppsInput, DashboardArrayInput, DashboardCaseInput, DashboardConfigInput, DashboardDisplayInput, DashboardInput, DashboardOsInput, DashboardServiceInput, DashboardServiceUptimeInput, DashboardTwoFactorInput, DashboardTwoFactorLocalInput, DashboardTwoFactorRemoteInput, DashboardVarsInput, DashboardVersionsInput, DashboardVmsInput, EventType, Importance, NetworkInput, NotificationInput, NotificationStatus, PingEventSource, RegistrationState, RemoteAccessEventActionType, RemoteAccessInput, RemoteGraphQLClientInput, RemoteGraphQLEventType, RemoteGraphQLServerInput, ServerStatus, URL_TYPE, UpdateType } from '@app/graphql/generated/client/graphql'

View File

@@ -4,7 +4,6 @@ import 'global-agent/bootstrap';
import http from 'http';
import https from 'https';
import CacheableLookup from 'cacheable-lookup';
import exitHook from 'async-exit-hook';
import { store } from '@app/store';
import { loadConfigFile } from '@app/store/modules/config';
import { logger } from '@app/core/log';
@@ -93,8 +92,11 @@ try {
await validateApiKeyIfPresent();
console.log('Started')
// On process exit stop HTTP server - this says it supports async but it doesnt seem to
/*
exitHook(() => {
console.log('exithook');
server?.close?.();
// If port is unix socket, delete socket before exiting
unlinkUnixPort();
@@ -102,8 +104,11 @@ try {
shutdownApiEvent();
process.exit(0);
});
}); */
console.log('Stopped')
} catch (error: unknown) {
console.log('exit');
if (error instanceof Error) {
logger.error('API-ERROR %s %s', error.message, error.stack);
}

View File

@@ -187,6 +187,9 @@ export class GraphQLClient {
MOTHERSHIP_GRAPHQL_LINK.replace('http', 'ws')
);
});
GraphQLClient.client.on('error', (err) => {
console.log('error', err);
})
GraphQLClient.client.on('connected', () => {
store.dispatch(
setGraphqlConnectionStatus({

View File

@@ -37,14 +37,15 @@ export async function bootstrapNestServer(): Promise<NestFastifyApplication> {
app.useGlobalFilters(new GraphQLExceptionsFilter(), new HttpExceptionFilter());
await app.init();
if (Number.isNaN(parseInt(PORT))) {
server.listen({ path: '/var/run/unraid-api.sock' });
} else {
server.listen({ port: parseInt(PORT), host: '0.0.0.0' });
}
if (Number.isNaN(parseInt(PORT))) {
await server.listen({ path: '/var/run/unraid-api.sock' });
} else {
await server.listen({ port: parseInt(PORT), host: '0.0.0.0' });
}
//await app.getHttpAdapter().listen(PORT);
apiLogger.debug('Nest Server is now listening');
app.flushLogs();
return app;
}

View File

@@ -3,7 +3,7 @@
"src/**/*",
".eslintrc.ts",
"vite.config.ts"
],
, "unraid-api-cli.js" ],
"exclude": [
"node_modules",
"vite.config.ts"
@@ -39,6 +39,5 @@
"esModuleInterop": true, /* Enables emit interoperability between CommonJS and ES Modules via creation of namespace objects for all imports. Implies 'allowSyntheticDefaultImports'. */
"forceConsistentCasingInFileNames": true /* Disallow inconsistently-cased references to the same file. */,
"resolveJsonModule": false,
"allowImportingTsExtensions": true
}
}

15
api/unraid-api.js Executable file
View File

@@ -0,0 +1,15 @@
#!/usr/bin/env node
import { execSync } from "child_process";
// Collect all arguments passed to the script
const args = process.argv.slice(2).join(" ");
// Run the TypeScript app using tsx and pass the arguments
try {
execSync(`npm run tsx:unraid -- ${args}`, { stdio: "inherit", cwd: import.meta.dirname });
} catch (error) {
console.error("Failed to run the TypeScript app", error);
process.exit(1);
}