Escape paths and profile names when generating launchd, systemd, and Windows service files

This commit is contained in:
Justin Oro
2026-08-24 10:45:23 -07:00
parent 3007c1caa1
commit ed49dbf429
+32 -11
View File
@@ -2,6 +2,7 @@ import os
import subprocess import subprocess
import sys import sys
from pathlib import Path from pathlib import Path
from xml.sax.saxutils import escape as xml_escape
from . import paths from . import paths
@@ -73,13 +74,20 @@ def environment():
def launchd_plist(profile_name): def launchd_plist(profile_name):
label = service_label(profile_name) label = xml_escape(service_label(profile_name))
python = xml_escape(preferred_python())
script = script_path() script = script_path()
script_escaped = xml_escape(script)
working_dir = xml_escape(str(Path(script).parent))
profile_arg = xml_escape(profile_name)
log_dir = paths.LOG_DIR log_dir = paths.LOG_DIR
env_entries = "" env_entries = ""
for key, value in environment().items(): for key, value in environment().items():
env_entries += f" <key>{key}</key>\n <string>{value}</string>\n" env_entries += (
f" <key>{xml_escape(key)}</key>\n"
f" <string>{xml_escape(value)}</string>\n"
)
env_block = "" env_block = ""
if env_entries: if env_entries:
@@ -90,6 +98,9 @@ def launchd_plist(profile_name):
" </dict>\n" " </dict>\n"
) )
out_path = xml_escape(str(log_dir / f"{profile_name}.out.log"))
err_path = xml_escape(str(log_dir / f"{profile_name}.err.log"))
return f"""<?xml version="1.0" encoding="UTF-8"?> return f"""<?xml version="1.0" encoding="UTF-8"?>
<!DOCTYPE plist PUBLIC "-//Apple//DTD PLIST 1.0//EN" <!DOCTYPE plist PUBLIC "-//Apple//DTD PLIST 1.0//EN"
"http://www.apple.com/DTDs/PropertyList-1.0.dtd"> "http://www.apple.com/DTDs/PropertyList-1.0.dtd">
@@ -100,15 +111,15 @@ def launchd_plist(profile_name):
<key>ProgramArguments</key> <key>ProgramArguments</key>
<array> <array>
<string>{preferred_python()}</string> <string>{python}</string>
<string>{script}</string> <string>{script_escaped}</string>
<string>run</string> <string>run</string>
<string>{profile_name}</string> <string>{profile_arg}</string>
<string>--quiet</string> <string>--quiet</string>
</array> </array>
<key>WorkingDirectory</key> <key>WorkingDirectory</key>
<string>{Path(script).parent}</string> <string>{working_dir}</string>
{env_block} <key>RunAtLoad</key> {env_block} <key>RunAtLoad</key>
<true/> <true/>
@@ -120,19 +131,29 @@ def launchd_plist(profile_name):
<integer>60</integer> <integer>60</integer>
<key>StandardOutPath</key> <key>StandardOutPath</key>
<string>{log_dir / f"{profile_name}.out.log"}</string> <string>{out_path}</string>
<key>StandardErrorPath</key> <key>StandardErrorPath</key>
<string>{log_dir / f"{profile_name}.err.log"}</string> <string>{err_path}</string>
</dict> </dict>
</plist> </plist>
""" """
def _systemd_quote(value):
escaped = str(value).replace("\\", "\\\\").replace('"', '\\"')
return f'"{escaped}"'
def systemd_unit(profile_name): def systemd_unit(profile_name):
script = script_path() script = script_path()
exec_start = " ".join(
_systemd_quote(token)
for token in (preferred_python(), script, "run", profile_name, "--quiet")
)
env_lines = "".join( env_lines = "".join(
f"Environment={key}={value}\n" for key, value in environment().items() f"Environment={_systemd_quote(f'{key}={value}')}\n"
for key, value in environment().items()
) )
return f"""[Unit] return f"""[Unit]
@@ -141,7 +162,7 @@ After=network-online.target
[Service] [Service]
Type=simple Type=simple
ExecStart={preferred_python()} {script} run {profile_name} --quiet ExecStart={exec_start}
WorkingDirectory={Path(script).parent} WorkingDirectory={Path(script).parent}
{env_lines}Restart=always {env_lines}Restart=always
RestartSec=60 RestartSec=60
@@ -158,7 +179,7 @@ def windows_instructions(profile_name):
others. Use Task Scheduler: others. Use Task Scheduler:
schtasks /create /tn "{label}" /sc onlogon /rl highest ^ schtasks /create /tn "{label}" /sc onlogon /rl highest ^
/tr "\\"{preferred_python()}\\" \\"{script}\\" run {profile_name} --quiet" /tr "\\"{preferred_python()}\\" \\"{script}\\" run \\"{profile_name}\\" --quiet"
To remove it: To remove it: