fix: make a native install take effect on systemd and stay out of Docker's way

From review of #2800:

- systemd `enable --now` starts nothing when the unit is already active, so a
  second `up --native` kept the old ExecStart and left the API on its previous
  port. start enables and then restarts, as the launchd path already did by
  booting the job out first.
- An explicit `home` now wins over XDG_CONFIG_HOME, which is what callers pass
  it for.
- The ExecStart program must be a real executable: when `docsgpt` is not on
  PATH, sys.argv[0] is accepted only if it can be run, and otherwise the
  failure is raised before any unit is written.
- `up --native` over a directory holding a Docker install now refuses and says
  how to proceed, instead of starting native services beside containers that
  down, status and uninstall would no longer see.
- Docs: without a terminal only --postgres-uri is required, and the Windows
  fallback names `docsgpt beat`, which the worker cannot embed there.

SystemdServices was the least covered part of the module and cannot be run on
this machine, so it now has tests for install, start, stop, remove, is_running
and a failing systemctl.
This commit is contained in:
Alex committed 2026-09-16 22:35:14 +01:00
1 parent e2c40f7a78
commit f9f52e99f0
4 files changed
+130 -6

No files matched your search

+10 -3
View File
@@ -152,8 +152,12 @@ class SystemdServices:
def __init__(self, runner=subprocess.run, home: Optional[Path] = None) -> None:
self._run = runner
base = os.environ.get("XDG_CONFIG_HOME")
root = Path(base) if base else (home or Path.home()) / ".config"
# An explicit home wins: it is what a caller passes to redirect the unit directory.
if home is not None:
root = home / ".config"
else:
base = os.environ.get("XDG_CONFIG_HOME")
root = Path(base) if base else Path.home() / ".config"
self.directory = root / "systemd" / "user"
def _unit_file(self, name: str) -> Path:
@@ -171,7 +175,10 @@ class SystemdServices:
self._systemctl("daemon-reload")
def start(self, name: str) -> None:
self._systemctl("enable", "--now", f"{name}.service", check=True)
# restart, not `enable --now`: --now starts nothing when the unit is already active, so a
# reinstalled unit would keep running with the ExecStart and environment it started with.
self._systemctl("enable", f"{name}.service", check=True)
self._systemctl("restart", f"{name}.service", check=True)
def stop(self, name: str) -> None:
self._systemctl("stop", f"{name}.service")