122 lines
3.1 KiB
TypeScript
122 lines
3.1 KiB
TypeScript
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 };
|
|
}
|