feat: platform-aware install-service — launchd on macOS, systemd on Linux
- Add _install_launchd() that writes ~/Library/LaunchAgents/com.muxplex.plist - Refactor existing systemd code into _install_systemd() - install_service() now detects sys.platform == 'darwin' and delegates accordingly - Update install-service help text to mention both platforms - Tests: launchd plist on macOS, systemd on Linux, no cross-contamination
This commit is contained in:
+51
-4
@@ -56,10 +56,44 @@ def serve(
|
||||
uvicorn.run(app, host=host, port=port, log_level="warning")
|
||||
|
||||
|
||||
def install_service(*, system: bool = False) -> None:
|
||||
"""Install muxplex as a systemd service."""
|
||||
executable = sys.executable
|
||||
def _install_launchd(executable: str) -> None:
|
||||
"""Install a macOS launchd agent plist to ~/Library/LaunchAgents/."""
|
||||
label = "com.muxplex"
|
||||
plist = f"""<?xml version="1.0" encoding="UTF-8"?>
|
||||
<!DOCTYPE plist PUBLIC "-//Apple//DTD PLIST 1.0//EN" "http://www.apple.com/DTDs/PropertyList-1.0.dtd">
|
||||
<plist version="1.0">
|
||||
<dict>
|
||||
<key>Label</key>
|
||||
<string>{label}</string>
|
||||
<key>ProgramArguments</key>
|
||||
<array>
|
||||
<string>{executable}</string>
|
||||
<string>-m</string>
|
||||
<string>muxplex</string>
|
||||
</array>
|
||||
<key>RunAtLoad</key>
|
||||
<true/>
|
||||
<key>KeepAlive</key>
|
||||
<true/>
|
||||
<key>StandardOutPath</key>
|
||||
<string>/tmp/muxplex.log</string>
|
||||
<key>StandardErrorPath</key>
|
||||
<string>/tmp/muxplex.err</string>
|
||||
</dict>
|
||||
</plist>
|
||||
"""
|
||||
path = Path.home() / "Library" / "LaunchAgents" / f"{label}.plist"
|
||||
path.parent.mkdir(parents=True, exist_ok=True)
|
||||
path.write_text(plist)
|
||||
print(f"Launch agent written to {path}")
|
||||
print("Enable with:")
|
||||
print(f" launchctl load {path}")
|
||||
print("Disable with:")
|
||||
print(f" launchctl unload {path}")
|
||||
|
||||
|
||||
def _install_systemd(executable: str, *, system: bool = False) -> None:
|
||||
"""Install a Linux systemd service unit file."""
|
||||
_raw_path = os.environ.get("PATH", "/usr/local/bin:/usr/bin:/bin")
|
||||
_safe_path = ":".join(p for p in _raw_path.split(":") if not p.startswith("/mnt/"))
|
||||
_safe_path = _safe_path or "/usr/local/bin:/usr/bin:/bin"
|
||||
@@ -97,6 +131,16 @@ WantedBy={"multi-user.target" if system else "default.target"}
|
||||
print(f"Enable with:\n {reload_cmd}")
|
||||
|
||||
|
||||
def install_service(*, system: bool = False) -> None:
|
||||
"""Install muxplex as a background service (launchd on macOS, systemd on Linux)."""
|
||||
executable = sys.executable
|
||||
|
||||
if sys.platform == "darwin":
|
||||
_install_launchd(executable)
|
||||
else:
|
||||
_install_systemd(executable, system=system)
|
||||
|
||||
|
||||
def main() -> None:
|
||||
"""CLI entry point."""
|
||||
parser = argparse.ArgumentParser(
|
||||
@@ -124,7 +168,10 @@ def main() -> None:
|
||||
sub = parser.add_subparsers(dest="command")
|
||||
sub.add_parser("serve", help="Start the server (default)")
|
||||
|
||||
svc = sub.add_parser("install-service", help="Install systemd service unit")
|
||||
svc = sub.add_parser(
|
||||
"install-service",
|
||||
help="Install as a background service (systemd on Linux, launchd on macOS)",
|
||||
)
|
||||
svc.add_argument(
|
||||
"--system", action="store_true", help="System-wide (requires sudo)"
|
||||
)
|
||||
|
||||
@@ -260,6 +260,76 @@ def test_reset_secret_prints_warning(tmp_path, monkeypatch, capsys):
|
||||
)
|
||||
|
||||
|
||||
def test_install_service_writes_launchd_plist_on_macos(tmp_path, monkeypatch):
|
||||
"""install_service() on macOS writes a launchd plist to ~/Library/LaunchAgents/."""
|
||||
from muxplex.cli import install_service
|
||||
|
||||
fake_home = tmp_path / "home"
|
||||
fake_home.mkdir()
|
||||
monkeypatch.setattr(Path, "home", staticmethod(lambda: fake_home))
|
||||
monkeypatch.setattr("sys.platform", "darwin")
|
||||
|
||||
install_service(system=False)
|
||||
|
||||
plist_path = fake_home / "Library" / "LaunchAgents" / "com.muxplex.plist"
|
||||
assert plist_path.exists(), "Plist file must be created on macOS"
|
||||
content = plist_path.read_text()
|
||||
assert "com.muxplex" in content
|
||||
assert "RunAtLoad" in content
|
||||
assert "ProgramArguments" in content
|
||||
assert "LaunchAgents" in str(plist_path)
|
||||
|
||||
|
||||
def test_install_service_does_not_write_systemd_on_macos(tmp_path, monkeypatch):
|
||||
"""install_service() on macOS must NOT write a systemd unit file."""
|
||||
from muxplex.cli import install_service
|
||||
|
||||
fake_home = tmp_path / "home"
|
||||
fake_home.mkdir()
|
||||
monkeypatch.setattr(Path, "home", staticmethod(lambda: fake_home))
|
||||
monkeypatch.setattr("sys.platform", "darwin")
|
||||
|
||||
install_service(system=False)
|
||||
|
||||
systemd_path = fake_home / ".config" / "systemd" / "user" / "muxplex.service"
|
||||
assert not systemd_path.exists(), "No systemd unit file should be written on macOS"
|
||||
|
||||
|
||||
def test_install_service_writes_systemd_on_linux(tmp_path, monkeypatch):
|
||||
"""install_service() on Linux writes a systemd unit to ~/.config/systemd/user/."""
|
||||
from muxplex.cli import install_service
|
||||
|
||||
fake_home = tmp_path / "home"
|
||||
fake_home.mkdir()
|
||||
monkeypatch.setattr(Path, "home", staticmethod(lambda: fake_home))
|
||||
monkeypatch.setattr("sys.platform", "linux")
|
||||
|
||||
install_service(system=False)
|
||||
|
||||
unit_path = fake_home / ".config" / "systemd" / "user" / "muxplex.service"
|
||||
assert unit_path.exists(), "Systemd unit file must be created on Linux"
|
||||
content = unit_path.read_text()
|
||||
assert "[Unit]" in content
|
||||
assert "[Service]" in content
|
||||
|
||||
|
||||
def test_install_service_help_text_mentions_background_service():
|
||||
"""install-service help must mention 'service', not just 'systemd'."""
|
||||
import io
|
||||
from muxplex.cli import main
|
||||
|
||||
buf = io.StringIO()
|
||||
with patch("sys.argv", ["muxplex", "install-service", "--help"]):
|
||||
try:
|
||||
with patch("sys.stdout", buf):
|
||||
main()
|
||||
except SystemExit:
|
||||
pass
|
||||
|
||||
help_text = buf.getvalue().lower()
|
||||
assert "service" in help_text
|
||||
|
||||
|
||||
def test_dunder_main_calls_main():
|
||||
"""python -m muxplex must call cli.main()."""
|
||||
import importlib.util
|
||||
|
||||
Reference in New Issue
Block a user