import { lstat, readdir, readFile } from "node:fs/promises"; import process from "node:process"; import { setTimeout as sleep } from "node:timers/promises"; import { output, spawn } from "./command.ts"; import type { CommandChild, CommandOptions, CommandOutput } from "./command.ts"; import { isNotFound } from "./errors.ts"; import type { RuntimeProfile } from "./runtime.ts"; export type AgentCommandOptions = Omit; export interface AgentCommand { output(): Promise; spawn(): CommandChild; } export function agentCommand( profile: RuntimeProfile, command: string, args: string[], options: AgentCommandOptions = {}, ): AgentCommand { const full: CommandOptions = { ...options, args, uid: profile.uid, gid: profile.gid }; return { output: () => output(command, full), spawn: () => spawn(command, full), }; } export function isRunnableProfileProcess(status: string, uid: number): boolean { const owner = /^Uid:\s+(\d+)/mu.exec(status)?.[1]; const state = /^State:\s+(\w)/mu.exec(status)?.[1]; return owner !== undefined && Number(owner) === uid && state !== "Z"; } async function profileProcessIds(uid: number): Promise { const entries = await readdir("/proc", { withFileTypes: true }); const pids = entries .filter((entry) => entry.isDirectory() && /^\d+$/u.test(entry.name)) .map((entry) => entry.name); const statuses = await Promise.all( pids.map((pid) => readFile(`/proc/${pid}/status`, "utf8").catch(() => null)), ); return pids .filter((_pid, index) => { const status = statuses[index]; return status !== null && status !== undefined && isRunnableProfileProcess(status, uid); }) .map(Number); } // Kills the UID's processes every 10 ms until none is left, for up to 100 rounds. export async function killProfileProcesses(uid: number, attempt = 0): Promise { if (attempt === 100) { throw new Error(`profile UID ${uid} still owns processes after cleanup`); } const ids = await profileProcessIds(uid); if (ids.length === 0) { return; } for (const pid of ids) { try { process.kill(pid, "SIGKILL"); } catch (error) { if (!isNotFound(error)) throw error; } } await sleep(10); await killProfileProcesses(uid, attempt + 1); } export async function readProfileFile(profile: RuntimeProfile, path: string): Promise { const stat = await lstat(path); if ( !stat.isFile() || stat.isSymbolicLink() || stat.uid !== profile.uid || (stat.mode & 0o077) !== 0 ) { throw new Error(`${path}: expected a profile-owned regular private file`); } return readFile(path, "utf8"); }