diff --git a/.changeset/systemd-unit-unquoted-paths.md b/.changeset/systemd-unit-unquoted-paths.md new file mode 100644 index 0000000..0351494 --- /dev/null +++ b/.changeset/systemd-unit-unquoted-paths.md @@ -0,0 +1,5 @@ +--- +"@smsunarto/bb-plugin-agent-proxy": patch +--- + +Emit `WorkingDirectory=`, `StandardOutput=`, and `StandardError=` unquoted in the generated systemd user unit. systemd rejects a quoted `WorkingDirectory=` as a fatal unit error, so the core service never loaded on Linux, and quoted output directives silently sent core logs to the journal instead of `core.log`. diff --git a/plugins/agent-proxy/lib/core-process.ts b/plugins/agent-proxy/lib/core-process.ts index 69b368f..b6b501c 100644 --- a/plugins/agent-proxy/lib/core-process.ts +++ b/plugins/agent-proxy/lib/core-process.ts @@ -500,10 +500,16 @@ interface SystemdJobInfo { exitCode: number | null; } -function systemdQuote(value: string): string { +function systemdValue(value: string): string { if (/\r|\n|\0/.test(value)) throw new Error("systemd service values cannot contain control characters"); - return `"${value.replaceAll("\\", "\\\\").replaceAll('"', '\\"').replaceAll("%", "%%")}"`; + return value.replaceAll("%", "%%"); +} + +// Quoting is only defined for command-line directives such as ExecStart=; +// plain assignments like WorkingDirectory= take their value literally. +function systemdQuote(value: string): string { + return `"${systemdValue(value).replaceAll("\\", "\\\\").replaceAll('"', '\\"')}"`; } export function renderSystemdUserUnit(options: { @@ -513,7 +519,7 @@ export function renderSystemdUserUnit(options: { logPath: string; }): string { const command = [options.binPath, "--config", options.configPath].map(systemdQuote).join(" "); - const logTarget = systemdQuote(`append:${options.logPath}`); + const logTarget = systemdValue(`append:${options.logPath}`); return `[Unit] Description=Agent Proxy (${options.label}) After=network-online.target @@ -522,7 +528,7 @@ Wants=network-online.target [Service] Type=simple ExecStart=${command} -WorkingDirectory=${systemdQuote(dirname(options.configPath))} +WorkingDirectory=${systemdValue(dirname(options.configPath))} UMask=0077 Restart=always RestartSec=2 diff --git a/plugins/agent-proxy/test/core-process.test.ts b/plugins/agent-proxy/test/core-process.test.ts index 84dd95b..2501a63 100644 --- a/plugins/agent-proxy/test/core-process.test.ts +++ b/plugins/agent-proxy/test/core-process.test.ts @@ -360,6 +360,17 @@ test("renders and parses a persistent user systemd service", () => { assert.match(unit, /Restart=always/); assert.match(unit, /UMask=0077/); assert.match(unit, /ExecStart="\/home\/test\/Agent Proxy\/proxy" "--config"/); + assert.match(unit, /^WorkingDirectory=\/home\/test\/Agent Proxy$/m); + assert.match(unit, /^StandardOutput=append:\/home\/test\/Agent Proxy\/core\.log$/m); + assert.match(unit, /^StandardError=append:\/home\/test\/Agent Proxy\/core\.log$/m); + const percentUnit = renderSystemdUserUnit({ + label: "com.example.proxy", + binPath: "/home/test/100% sure/proxy", + configPath: "/home/test/100% sure/config.yaml", + logPath: "/home/test/100% sure/core.log", + }); + assert.match(percentUnit, /^WorkingDirectory=\/home\/test\/100%% sure$/m); + assert.match(percentUnit, /^StandardOutput=append:\/home\/test\/100%% sure\/core\.log$/m); assert.deepEqual( parseSystemctlShow( systemctlOutput({