mirror of
https://github.com/openclaw/openclaw.git
synced 2026-05-30 13:21:08 +00:00
* refactor: share talk event metric extraction * refactor: reuse shared coercion helpers * refactor: reuse shared primitive guards * refactor: reuse shared record guard * refactor: reuse shared primitive helpers * refactor: reuse shared string guards * refactor: reuse shared non-empty string guard * refactor: share plugin primitive coercion helpers * refactor: reuse plugin coercion helpers * refactor: reuse plugin coercion helpers in more plugins * refactor: reuse channel coercion helpers * refactor: reuse monitor coercion helpers * refactor: reuse provider coercion helpers * refactor: reuse core coercion helpers * refactor: reuse runtime coercion helpers * refactor: reuse helper coercion in codex paths * refactor: reuse helper coercion in runtime paths * refactor: reuse codex app-server coercion helpers * refactor: reuse codex record helpers * refactor: reuse migration and qa record helpers * refactor: reuse feishu and core helper guards * refactor: reuse browser and policy coercion helpers * refactor: reuse memory wiki record helper * refactor: share boolean coercion helpers * refactor: reuse finite number coercion * refactor: reuse trimmed string list helpers * refactor: reuse string list normalization * refactor: reuse remaining string list helpers * refactor: reuse string entry normalizer * refactor: share sorted string helpers * refactor: share string list normalization * test: preserve command registry browser imports * refactor: reuse trimmed list helpers * refactor: reuse string dedupe helpers * refactor: reuse local dedupe helpers * refactor: reuse more string dedupe helpers * refactor: reuse command string dedupe helpers * refactor: dedupe memory path lists with helper * refactor: expose string dedupe helpers to plugins * refactor: reuse core string dedupe helpers * refactor: reuse shared unique value helpers * refactor: reuse unique helpers in agent utilities * refactor: reuse unique helpers in config plumbing * refactor: reuse unique helpers in extensions * refactor: reuse unique helpers in core utilities * refactor: reuse unique helpers in qa plugins * refactor: reuse unique helpers in memory plugins * refactor: reuse unique helpers in channel plugins * refactor: reuse unique helpers in core tails * refactor: reuse unique helper in comfy workflow * refactor: reuse unique helpers in test utilities * refactor: expose unique value helper to plugins * refactor: reuse unique helpers for numeric lists * refactor: replace index dedupe filters * refactor: reuse string entry normalization * refactor: reuse string normalization in plugin helpers * refactor: reuse string normalization in extension helpers * refactor: reuse string normalization in channel parsers * refactor: reuse string normalization in memory search * refactor: reuse string normalization in provider parsers * refactor: reuse string normalization in qa helpers * refactor: reuse string normalization in infra parsers * refactor: reuse string normalization in messaging parsers * refactor: reuse string normalization in core parsers * refactor: reuse string normalization in extension parsers * refactor: reuse string normalization in remaining parsers * refactor: reuse string normalization in final parser spots * refactor: reuse string normalization in qa media helpers * refactor: reuse normalization in provider and media lists * refactor: reuse normalization for remaining set filters * refactor: reuse normalization in policy allowlists * refactor: reuse normalization in session and owner lists * refactor: centralize primitive string lists * refactor: reuse lowercase entry helpers * refactor: reuse sorted string helpers * refactor: reuse unique trimmed helpers * refactor: reuse string normalization helpers * refactor: reuse catalog string helpers * refactor: reuse remaining string helpers * refactor: simplify remaining list normalization * refactor: reuse codex auth order normalization * chore: refresh plugin sdk api baseline * fix: make shared string sorting deterministic * chore: refresh plugin sdk api baseline * fix: align host env security ordering
154 lines
4.4 KiB
TypeScript
154 lines
4.4 KiB
TypeScript
import { normalizeStringEntries } from "../shared/string-normalization.js";
|
|
import { splitArgsPreservingQuotes } from "./arg-split.js";
|
|
import type { GatewayServiceRenderArgs } from "./service-types.js";
|
|
|
|
const SYSTEMD_LINE_BREAKS = /[\r\n]/;
|
|
|
|
function assertNoSystemdLineBreaks(value: string, label: string): void {
|
|
if (SYSTEMD_LINE_BREAKS.test(value)) {
|
|
throw new Error(`${label} cannot contain CR or LF characters.`);
|
|
}
|
|
}
|
|
|
|
function systemdEscapeArg(value: string): string {
|
|
assertNoSystemdLineBreaks(value, "Systemd unit values");
|
|
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]) => {
|
|
const rawValue = value ?? "";
|
|
assertNoSystemdLineBreaks(key, "Systemd environment variable names");
|
|
assertNoSystemdLineBreaks(rawValue, "Systemd environment variable values");
|
|
return `Environment=${systemdEscapeArg(`${key}=${rawValue.trim()}`)}`;
|
|
});
|
|
}
|
|
|
|
function renderEnvironmentFileLines(environmentFiles: string[] | undefined): string[] {
|
|
if (!environmentFiles) {
|
|
return [];
|
|
}
|
|
return normalizeStringEntries(environmentFiles).map((entry) => {
|
|
assertNoSystemdLineBreaks(entry, "Systemd EnvironmentFile values");
|
|
return `EnvironmentFile=-${systemdEscapeArg(entry)}`;
|
|
});
|
|
}
|
|
|
|
export function buildSystemdUnit({
|
|
description,
|
|
programArguments,
|
|
workingDirectory,
|
|
environment,
|
|
environmentFiles,
|
|
}: GatewayServiceRenderArgs): string {
|
|
const execStart = programArguments.map(systemdEscapeArg).join(" ");
|
|
const descriptionValue = description?.trim() || "OpenClaw Gateway";
|
|
assertNoSystemdLineBreaks(descriptionValue, "Systemd Description");
|
|
const descriptionLine = `Description=${descriptionValue}`;
|
|
const workingDirLine = workingDirectory
|
|
? `WorkingDirectory=${systemdEscapeArg(workingDirectory)}`
|
|
: null;
|
|
const envLines = renderEnvLines(environment);
|
|
const environmentFileLines = renderEnvironmentFileLines(environmentFiles);
|
|
return [
|
|
"[Unit]",
|
|
descriptionLine,
|
|
"After=network-online.target",
|
|
"Wants=network-online.target",
|
|
"StartLimitBurst=5",
|
|
"StartLimitIntervalSec=60",
|
|
"",
|
|
"[Service]",
|
|
`ExecStart=${execStart}`,
|
|
"Restart=always",
|
|
"RestartSec=5",
|
|
"RestartPreventExitStatus=78",
|
|
"TimeoutStopSec=30",
|
|
"TimeoutStartSec=30",
|
|
"SuccessExitStatus=0 143",
|
|
// Keep service children in the same lifecycle so restarts do not leave
|
|
// orphan ACP/runtime workers behind.
|
|
"KillMode=control-group",
|
|
workingDirLine,
|
|
...environmentFileLines,
|
|
...envLines,
|
|
"",
|
|
"[Install]",
|
|
"WantedBy=default.target",
|
|
"",
|
|
]
|
|
.filter((line) => line !== null)
|
|
.join("\n");
|
|
}
|
|
|
|
export function parseSystemdExecStart(value: string): string[] {
|
|
return splitArgsPreservingQuotes(value, { escapeMode: "backslash" });
|
|
}
|
|
|
|
export function parseSystemdEnvAssignment(raw: string): { key: string; value: string } | null {
|
|
const trimmed = raw.trim();
|
|
if (!trimmed) {
|
|
return null;
|
|
}
|
|
|
|
const unquoted = (() => {
|
|
const quote = trimmed[0];
|
|
if (!((quote === '"' || quote === "'") && trimmed.endsWith(quote))) {
|
|
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 };
|
|
}
|
|
|
|
export function parseSystemdEnvAssignments(raw: string): Array<{ key: string; value: string }> {
|
|
return splitArgsPreservingQuotes(raw, {
|
|
escapeMode: "backslash",
|
|
quoteChars: ['"', "'"],
|
|
quoteStart: "item-start",
|
|
}).flatMap((entry) => {
|
|
const parsed = parseSystemdEnvAssignment(entry);
|
|
return parsed ? [parsed] : [];
|
|
});
|
|
}
|
|
|
|
export function renderSystemdEnvAssignment(key: string, value: string): string {
|
|
return systemdEscapeArg(`${key}=${value}`);
|
|
}
|