84 lines
2.6 KiB
TypeScript
84 lines
2.6 KiB
TypeScript
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<CommandOptions, "args" | "uid" | "gid">;
|
|
|
|
export interface AgentCommand {
|
|
output(): Promise<CommandOutput>;
|
|
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<number[]> {
|
|
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<void> {
|
|
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<string> {
|
|
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");
|
|
}
|