Files
api/app/core/utils/misc/get-node-service.ts
Alexis Tyler 4e1b0bd72c chore: lint
2021-01-28 15:45:14 +10:30

160 lines
4.1 KiB
TypeScript

import fs from 'fs';
import path from 'path';
import execa from 'execa';
import { JSONSchemaForNPMPackageJsonFiles as PackageJson } from '@schemastore/package';
import { coreLogger } from '../../log';
import { User } from '../../types';
import { CacheManager } from '../../cache-manager';
import { cleanStdout, ensurePermission } from '../../utils';
export interface NodeService {
online?: boolean;
uptime?: number;
version?: string;
}
export const getNodeService = async (user: User, namespace: string): Promise<NodeService> => {
// Check permissions
ensurePermission(user, {
resource: `service/${namespace}`,
action: 'read',
possession: 'any'
});
const cache = new CacheManager(`unraid:services/${namespace}`, Infinity);
const getCachedValue = (name: string) => {
return cache.get<string>(name);
};
const setCachedValue = (name: string, value: string) => {
cache.set(name, value);
return value;
};
const getUptime = async (pid: string | number) => {
const uptime = await execa('ps', ['-p', String(pid), '-o', 'etimes', '--no-headers'])
.then(cleanStdout)
.catch(async error => {
// No clue why this failed
if (!error.stderr.includes('keyword not found')) {
return '';
}
// Retry with macos way
// Borrowed from https://stackoverflow.com/a/28856613
const command = `ps -p ${pid} -oetime= | tr '-' ':' | awk -F: '{ total=0; m=1; } { for (i=0; i < NF; i++) {total += $(NF-i)*m; m *= i >= 2 ? 24 : 60 }} {print total}'`;
return execa.command(command, { shell: true }).then(cleanStdout).catch(() => '');
});
const parsedUptime = Number.parseInt(uptime, 10);
return parsedUptime >= 0 ? parsedUptime : -1;
};
const getPid = async (): Promise<string | void> => {
let pid = getCachedValue('pid');
// Return cached pid
if (pid) {
return pid;
}
coreLogger.debug('No PID found in cache for %s', namespace);
pid = await execa.command(`pidof ${namespace}`)
.then(output => {
const pids = cleanStdout(output).split('\n');
return pids[0];
})
.catch(async error => {
// `pidof` is missing
if (error.code === 'ENOENT') {
// Fall back to using ps
// ps axc returns a line like the following
// 31424 s005 S+ 0:01.66 node
// We match the last column and return the first
return execa.command('ps axc').then(({ stdout }) => {
const foundProcess = stdout.split(/\n/).find(line => {
const field = line.trim().split(/\s+/)[4];
return field === namespace;
})?.trim();
return foundProcess ? foundProcess.split(/\s/)[0] : '';
});
}
return '';
});
// Process is offline
if (!pid) {
return;
}
coreLogger.debug('Setting pid for %s to %s', namespace, pid);
// Update cache
setCachedValue('pid', pid);
// Return new pid
return pid;
};
const getPkgFilePath = async (pid: number | string) => {
coreLogger.debug('Getting package.json path for %s', namespace);
return execa.command(`pwdx ${pid}`).then(({ stdout }) => {
return path.resolve(stdout.split(/\n/)[0].split(':')[1].trim());
}).catch(async error => {
if (error.code === 'ENOENT') {
return execa.command(`lsof -a -d cwd -p ${pid} -n -Fn`).then(({ stdout }) => stdout.split(/\n/)[2].substr(1));
}
return '';
});
};
const getVersion = async (pid: number | string) => {
let cachedVersion = getCachedValue('version');
// Return cached version
if (cachedVersion) {
return cachedVersion;
}
// Update local vars
const pkgFilePath = await getPkgFilePath(pid);
const version = await fs.promises.readFile(`${pkgFilePath}/package.json`)
.then(buffer => buffer.toString())
.then(JSON.parse)
.then((pkg: PackageJson) => pkg.version);
if (!version) {
return;
}
// Update cache
setCachedValue('version', version);
return version;
};
const pid = await getPid();
// If the pid doesn't exist it's offline
if (!pid) {
return {
online: false
};
}
const version = await getVersion(pid);
const uptime = await getUptime(pid);
const online = uptime >= 1;
coreLogger.silly('%s version=%s uptime=%s online=%s', namespace, version, uptime, online);
return {
online,
uptime,
version
};
};