| @@ -340,11 +340,24 @@ export interface SystemdUnitOptions { · 339 unmodified lines |
| 340 | 340 | readonly stderrPath: string; |
| 341 | 341 | } |
| 342 | 342 | |
| 343 | +const SYSTEMD_BARE_VALUE = /^[A-Za-z0-9_@%+=:,./-]+$/; |
| 344 | + |
| 345 | +const systemdQuote = (value: string): string => { |
| 346 | + if (SYSTEMD_BARE_VALUE.test(value)) return value; |
| 347 | + const escaped = value |
| 348 | + .replaceAll("\\", "\\\\") |
| 349 | + .replaceAll('"', '\\"') |
| 350 | + .replaceAll("\n", "\\n") |
| 351 | + .replaceAll("\r", "\\r") |
| 352 | + .replaceAll("\t", "\\t"); |
| 353 | + return `"${escaped}"`; |
| 354 | +}; |
| 355 | + |
| 343 | 356 | /** Render a systemd --user unit. Pure (snapshot-tested). */ |
| 344 | 357 | export const generateSystemdUnit = (options: SystemdUnitOptions): string => { |
| 345 | | − const execStart = options.execStart.map((arg) => (/\s/.test(arg) ? `"${arg}"` : arg)).join(" "); |
| 358 | + const execStart = options.execStart.map(systemdQuote).join(" "); |
| 346 | 359 | const env = Object.entries(options.environment) |
| 347 | | − .map(([key, value]) => `Environment=${key}=${value}`) |
| 360 | + .map(([key, value]) => `Environment=${systemdQuote(`${key}=${value}`)}`) |
| 348 | 361 | .join("\n"); |
| 349 | 362 | return `[Unit] |
| 350 | 363 | Description=Executor supervised daemon |
| @@ -354,9 +367,9 @@ After=default.target · 3 unmodified lines |
| 354 | 367 | Type=simple |
| 355 | 368 | ExecStart=${execStart} |
| 356 | 369 | ${env} |
| 357 | | −WorkingDirectory=${options.workingDirectory} |
| 358 | | −StandardOutput=append:${options.stdoutPath} |
| 359 | | −StandardError=append:${options.stderrPath} |
| 370 | +WorkingDirectory=${systemdQuote(options.workingDirectory)} |
| 371 | +StandardOutput=${systemdQuote(`append:${options.stdoutPath}`)} |
| 372 | +StandardError=${systemdQuote(`append:${options.stderrPath}`)} |
| 360 | 373 | Restart=on-failure |
| 361 | 374 | RestartSec=5s |
| 362 | 375 | |