refactor(src): split oversized modules
This commit is contained in:
126
src/daemon/systemd-unit.ts
Normal file
126
src/daemon/systemd-unit.ts
Normal file
@@ -0,0 +1,126 @@
|
||||
function systemdEscapeArg(value: string): string {
|
||||
if (!/[\\s"\\\\]/.test(value)) return value;
|
||||
return `"${value.replace(/\\\\/g, "\\\\\\\\").replace(/"/g, '\\\\"')}"`;
|
||||
}
|
||||
|
||||
function renderEnvLines(
|
||||
env: Record<string, string | undefined> | undefined,
|
||||
): string[] {
|
||||
if (!env) return [];
|
||||
const entries = Object.entries(env).filter(
|
||||
([, value]) => typeof value === "string" && value.trim(),
|
||||
);
|
||||
if (entries.length === 0) return [];
|
||||
return entries.map(
|
||||
([key, value]) =>
|
||||
`Environment=${systemdEscapeArg(`${key}=${value?.trim() ?? ""}`)}`,
|
||||
);
|
||||
}
|
||||
|
||||
export function buildSystemdUnit({
|
||||
description,
|
||||
programArguments,
|
||||
workingDirectory,
|
||||
environment,
|
||||
}: {
|
||||
description?: string;
|
||||
programArguments: string[];
|
||||
workingDirectory?: string;
|
||||
environment?: Record<string, string | undefined>;
|
||||
}): string {
|
||||
const execStart = programArguments.map(systemdEscapeArg).join(" ");
|
||||
const descriptionLine = `Description=${description?.trim() || "Clawdbot Gateway"}`;
|
||||
const workingDirLine = workingDirectory
|
||||
? `WorkingDirectory=${systemdEscapeArg(workingDirectory)}`
|
||||
: null;
|
||||
const envLines = renderEnvLines(environment);
|
||||
return [
|
||||
"[Unit]",
|
||||
descriptionLine,
|
||||
"After=network-online.target",
|
||||
"Wants=network-online.target",
|
||||
"",
|
||||
"[Service]",
|
||||
`ExecStart=${execStart}`,
|
||||
"Restart=always",
|
||||
"RestartSec=5",
|
||||
// KillMode=process ensures systemd only waits for the main process to exit.
|
||||
// Without this, podman's conmon (container monitor) processes block shutdown
|
||||
// since they run as children of the gateway and stay in the same cgroup.
|
||||
"KillMode=process",
|
||||
workingDirLine,
|
||||
...envLines,
|
||||
"",
|
||||
"[Install]",
|
||||
"WantedBy=default.target",
|
||||
"",
|
||||
]
|
||||
.filter((line) => line !== null)
|
||||
.join("\n");
|
||||
}
|
||||
|
||||
export function parseSystemdExecStart(value: string): string[] {
|
||||
const args: string[] = [];
|
||||
let current = "";
|
||||
let inQuotes = false;
|
||||
let escapeNext = false;
|
||||
|
||||
for (const char of value) {
|
||||
if (escapeNext) {
|
||||
current += char;
|
||||
escapeNext = false;
|
||||
continue;
|
||||
}
|
||||
if (char === "\\\\") {
|
||||
escapeNext = true;
|
||||
continue;
|
||||
}
|
||||
if (char === '"') {
|
||||
inQuotes = !inQuotes;
|
||||
continue;
|
||||
}
|
||||
if (!inQuotes && /\\s/.test(char)) {
|
||||
if (current) {
|
||||
args.push(current);
|
||||
current = "";
|
||||
}
|
||||
continue;
|
||||
}
|
||||
current += char;
|
||||
}
|
||||
if (current) args.push(current);
|
||||
return args;
|
||||
}
|
||||
|
||||
export function parseSystemdEnvAssignment(
|
||||
raw: string,
|
||||
): { key: string; value: string } | null {
|
||||
const trimmed = raw.trim();
|
||||
if (!trimmed) return null;
|
||||
|
||||
const unquoted = (() => {
|
||||
if (!(trimmed.startsWith('"') && trimmed.endsWith('"'))) return trimmed;
|
||||
let out = "";
|
||||
let escapeNext = false;
|
||||
for (const ch of trimmed.slice(1, -1)) {
|
||||
if (escapeNext) {
|
||||
out += ch;
|
||||
escapeNext = false;
|
||||
continue;
|
||||
}
|
||||
if (ch === "\\\\") {
|
||||
escapeNext = true;
|
||||
continue;
|
||||
}
|
||||
out += ch;
|
||||
}
|
||||
return out;
|
||||
})();
|
||||
|
||||
const eq = unquoted.indexOf("=");
|
||||
if (eq <= 0) return null;
|
||||
const key = unquoted.slice(0, eq).trim();
|
||||
if (!key) return null;
|
||||
const value = unquoted.slice(eq + 1);
|
||||
return { key, value };
|
||||
}
|
||||
Reference in New Issue
Block a user