diff --git a/files/launchagent.py b/files/launchagent.py new file mode 100644 index 0000000..706b3ea --- /dev/null +++ b/files/launchagent.py @@ -0,0 +1,39 @@ +#!/usr/bin/env python3 +"""Пишет описание службы launchd для сервера панели: запуск при входе, перезапуск при падении. + + launchagent.py write +""" +import os +import plistlib +import sys + +LABEL = "com.andrey.claude-agents" # то же, что SERVICE_LABEL в claude_agents.py + + +def plist(python, script, log): + return { + "Label": LABEL, + "ProgramArguments": [python, script, "--headless"], + "RunAtLoad": True, + "KeepAlive": True, + "StandardOutPath": log, + "StandardErrorPath": log, + } + + +def main(): + if len(sys.argv) != 6 or sys.argv[1] != "write": + print(__doc__.strip(), file=sys.stderr) + sys.exit(2) + path, python, script, log = (os.path.expanduser(a) for a in sys.argv[2:]) + os.makedirs(os.path.dirname(path), exist_ok=True) + os.makedirs(os.path.dirname(log), exist_ok=True) + tmp = path + ".tmp" + with open(tmp, "wb") as f: + plistlib.dump(plist(python, script, log), f, fmt=plistlib.FMT_XML) + os.replace(tmp, path) + print(path) + + +if __name__ == "__main__": + main() diff --git a/tests/test_launchagent.py b/tests/test_launchagent.py new file mode 100644 index 0000000..5f1a144 --- /dev/null +++ b/tests/test_launchagent.py @@ -0,0 +1,49 @@ +import os +import plistlib +import subprocess +import sys +import tempfile +import unittest + +ROOT = os.path.join(os.path.dirname(os.path.abspath(__file__)), "..") +sys.path.insert(0, os.path.join(ROOT, "files")) +import claude_agents # noqa: E402 +import launchagent # noqa: E402 + +SCRIPT = os.path.join(ROOT, "files", "launchagent.py") + + +class Plist(unittest.TestCase): + def test_label_matches_server(self): + self.assertEqual(launchagent.LABEL, "com.andrey.claude-agents") + self.assertEqual(launchagent.LABEL, claude_agents.SERVICE_LABEL) + + def test_plist_shape(self): + self.assertEqual( + launchagent.plist("/opt/homebrew/bin/python3", "/x/claude_agents.py", "/l/panel.log"), + {"Label": "com.andrey.claude-agents", + "ProgramArguments": ["/opt/homebrew/bin/python3", "/x/claude_agents.py", "--headless"], + "RunAtLoad": True, "KeepAlive": True, + "StandardOutPath": "/l/panel.log", "StandardErrorPath": "/l/panel.log"}) + + +class Cli(unittest.TestCase): + def test_cli_writes_plist_with_spaces(self): + with tempfile.TemporaryDirectory() as tmp: + target = f"{tmp}/LaunchAgents/a.plist" + args = ("/usr/bin/python3", f"{tmp}/Application Support/claude_agents.py", + f"{tmp}/Logs/claude-agents/panel.log") + r = subprocess.run([sys.executable, SCRIPT, "write", target, *args], + capture_output=True, text=True) + self.assertEqual(r.returncode, 0, r.stderr) + with open(target, "rb") as f: + self.assertEqual(plistlib.load(f), launchagent.plist(*args)) + self.assertTrue(os.path.isdir(f"{tmp}/Logs/claude-agents")) + + def test_cli_bad_args(self): + r = subprocess.run([sys.executable, SCRIPT, "write"], capture_output=True, text=True) + self.assertEqual(r.returncode, 2) + + +if __name__ == "__main__": + unittest.main()