163 lines
5 KiB
TypeScript
163 lines
5 KiB
TypeScript
/**
|
|
* setup/hostd.ts — Install and start the privileged host daemon (hostd).
|
|
*
|
|
* This is a root-only step. It installs an rc.d service that runs:
|
|
* node dist/hostd/index.js
|
|
*
|
|
* The daemon socket path is derived from AGENT_NAME in src/hostd/types.ts.
|
|
* This setup step exports AGENT_NAME explicitly for the rc.d service.
|
|
*/
|
|
import { execFileSync, execSync } from 'child_process';
|
|
import fs from 'fs';
|
|
import os from 'os';
|
|
import path from 'path';
|
|
|
|
import { AGENT_NAME } from '../src/config.js';
|
|
import { logger } from '../src/logger.js';
|
|
import { commandExists, getNodePath, getPlatform, isRoot } from './platform.js';
|
|
import { emitStatus } from './status.js';
|
|
|
|
const LOG = 'logs/setup.log';
|
|
|
|
function getSudoContext(): { user: string | null; uid: number | null; gid: number | null } {
|
|
const user = (process.env.SUDO_USER || '').trim() || null;
|
|
const uidRaw = (process.env.SUDO_UID || '').trim();
|
|
const gidRaw = (process.env.SUDO_GID || '').trim();
|
|
const uid = uidRaw ? Number(uidRaw) : null;
|
|
const gid = gidRaw ? Number(gidRaw) : null;
|
|
return {
|
|
user,
|
|
uid: Number.isFinite(uid) ? uid : null,
|
|
gid: Number.isFinite(gid) ? gid : null,
|
|
};
|
|
}
|
|
|
|
function buildProject(projectRoot: string): void {
|
|
const sudo = getSudoContext();
|
|
if (isRoot() && sudo.user) {
|
|
execFileSync(
|
|
'su',
|
|
['-m', sudo.user, '-c', `cd ${JSON.stringify(projectRoot)} && npm run build`],
|
|
{ cwd: projectRoot, stdio: 'ignore' },
|
|
);
|
|
return;
|
|
}
|
|
execSync('npm run build', { cwd: projectRoot, stdio: 'ignore' });
|
|
}
|
|
|
|
function chownIfSudoContext(filePath: string): void {
|
|
const sudo = getSudoContext();
|
|
if (!isRoot() || sudo.uid === null || sudo.gid === null) return;
|
|
try {
|
|
fs.chownSync(filePath, sudo.uid, sudo.gid);
|
|
} catch {
|
|
// best-effort only
|
|
}
|
|
}
|
|
|
|
function writeFile(filePath: string, contents: string, mode = 0o755): void {
|
|
fs.writeFileSync(filePath, contents, { mode });
|
|
}
|
|
|
|
function generateRunScript(agentName: string, nodePath: string, projectRoot: string): string {
|
|
return [
|
|
'#!/bin/sh',
|
|
'set -eu',
|
|
`export AGENT_NAME=${JSON.stringify(agentName)}`,
|
|
`export HOME=${JSON.stringify(os.homedir())}`,
|
|
'export PATH="/usr/local/bin:/usr/bin:/bin"',
|
|
`cd ${JSON.stringify(projectRoot)}`,
|
|
`exec ${JSON.stringify(nodePath)} ${JSON.stringify(path.join(projectRoot, 'dist/hostd/index.js'))}`,
|
|
'',
|
|
].join('\n');
|
|
}
|
|
|
|
function generateRcdService(agentName: string, projectRoot: string, runScriptPath: string): string {
|
|
const name = `${agentName}_hostd`;
|
|
const logPath = path.join(projectRoot, 'logs', `${agentName}-hostd.log`);
|
|
return [
|
|
'#!/bin/sh',
|
|
'#',
|
|
`# PROVIDE: ${name}`,
|
|
'# REQUIRE: NETWORKING LOGIN',
|
|
'# KEYWORD: shutdown',
|
|
'',
|
|
'. /etc/rc.subr',
|
|
'',
|
|
`name="${name}"`,
|
|
`rcvar="${name}_enable"`,
|
|
'command="/usr/sbin/daemon"',
|
|
`pidfile="/var/run/${agentName}-hostd-supervisor.pid"`,
|
|
'command_args="-P ${pidfile} -r -o ' +
|
|
JSON.stringify(logPath) +
|
|
' ' +
|
|
JSON.stringify(runScriptPath) +
|
|
'"',
|
|
'',
|
|
'load_rc_config $name',
|
|
': ${' + name + '_enable:="NO"}',
|
|
'',
|
|
'run_rc_command "$1"',
|
|
'',
|
|
].join('\n');
|
|
}
|
|
|
|
export async function run(_args: string[]): Promise<void> {
|
|
const projectRoot = process.cwd();
|
|
const platform = getPlatform();
|
|
const nodePath = getNodePath();
|
|
const agentName = AGENT_NAME;
|
|
|
|
if (platform !== 'freebsd') {
|
|
emitStatus('SETUP_HOSTD', { STATUS: 'failed', ERROR: 'unsupported_platform', LOG });
|
|
process.exit(1);
|
|
}
|
|
if (!isRoot()) {
|
|
emitStatus('SETUP_HOSTD', { STATUS: 'failed', ERROR: 'requires_root', LOG });
|
|
throw new Error('setup_hostd_requires_root');
|
|
}
|
|
if (!commandExists('daemon')) {
|
|
emitStatus('SETUP_HOSTD', { STATUS: 'failed', ERROR: 'missing_daemon_bin', LOG });
|
|
throw new Error('missing_daemon_bin');
|
|
}
|
|
|
|
logger.info({ projectRoot, agentName }, 'Setting up hostd service');
|
|
|
|
// Ensure dist exists
|
|
try {
|
|
buildProject(projectRoot);
|
|
} catch {
|
|
emitStatus('SETUP_HOSTD', { STATUS: 'failed', ERROR: 'build_failed', LOG });
|
|
process.exit(1);
|
|
}
|
|
|
|
fs.mkdirSync(path.join(projectRoot, 'logs'), { recursive: true });
|
|
chownIfSudoContext(path.join(projectRoot, 'logs'));
|
|
|
|
const runScriptPath = path.join(projectRoot, `run-${agentName}-hostd.sh`);
|
|
const rcdPath = `/usr/local/etc/rc.d/${agentName}_hostd`;
|
|
|
|
writeFile(runScriptPath, generateRunScript(agentName, nodePath, projectRoot));
|
|
chownIfSudoContext(runScriptPath);
|
|
writeFile(rcdPath, generateRcdService(agentName, projectRoot, runScriptPath));
|
|
|
|
// Enable + start
|
|
try {
|
|
execSync(`sysrc ${agentName}_hostd_enable=YES`, { stdio: 'ignore' });
|
|
} catch (err) {
|
|
logger.warn({ err }, 'sysrc hostd enable failed');
|
|
}
|
|
try {
|
|
execSync(`service ${agentName}_hostd restart`, { stdio: 'ignore' });
|
|
} catch (err) {
|
|
logger.warn({ err }, 'service hostd restart failed');
|
|
}
|
|
|
|
emitStatus('SETUP_HOSTD', {
|
|
STATUS: 'success',
|
|
SERVICE_NAME: `${agentName}_hostd`,
|
|
RCD_PATH: rcdPath,
|
|
RUN_SCRIPT: runScriptPath,
|
|
LOG,
|
|
});
|
|
}
|