mirror of
https://github.com/tiennm99/DocsGPT.git
synced 2026-10-14 18:18:08 +00:00
fix: make a native install recoverable, honest and safe to quote
From the outside-diff findings on #2800: - install.json is written before the services are installed and started. A service that fails to start used to leave units behind in a directory that status, down and uninstall no longer recognised as a native install, so nothing could clean them up. - systemd stop and removal propagate failures: `down` reporting success while the unit still runs, or `uninstall` dropping the unit file and the record while systemd still runs the service, is worse than an error. Removing a unit that is already gone stays harmless. - WorkingDirectory and each Environment value are quoted and escaped for systemd. `--dir` takes a free-form path, and one with a space in it is not hypothetical: this checkout lives in one. - Native status checks and prints http://localhost:<port>, which is what the units bind. With a LAN DOCSGPT_BIND it used to poll an address nothing listened on and call a healthy install dead.
This commit is contained in:
1 parent
dbbed28f88
commit
4d9f1d47a9
3 files changed
+100
-13
No files matched your search
@@ -292,17 +292,19 @@ def _native_up(args, context: Context, directory: Path) -> int:
|
||||
if context.run([*launcher, "migrate"], stack_env) != 0:
|
||||
raise DeployError("`docsgpt migrate` failed; check the database URL and that the server is reachable.")
|
||||
|
||||
# The record goes in before the services: if one fails to start, this is still a native install
|
||||
# that status, down and uninstall can see and clean up, rather than orphaned units.
|
||||
now = datetime.now(timezone.utc).isoformat(timespec="seconds")
|
||||
record = record or {"installed_at": now}
|
||||
record.update(version=context.version, mode="native", updated_at=now)
|
||||
(directory / stack.RECORD_FILE).write_text(json.dumps(record, indent=2) + "\n", encoding="utf-8")
|
||||
|
||||
for unit in native.units_for(directory, launcher, port, directory):
|
||||
services.install(unit)
|
||||
for name in native.SERVICES:
|
||||
print(f"Starting {name} ...")
|
||||
services.start(name)
|
||||
|
||||
now = datetime.now(timezone.utc).isoformat(timespec="seconds")
|
||||
record = record or {"installed_at": now}
|
||||
record.update(version=context.version, mode="native", updated_at=now)
|
||||
(directory / stack.RECORD_FILE).write_text(json.dumps(record, indent=2) + "\n", encoding="utf-8")
|
||||
|
||||
health = f"http://127.0.0.1:{port}/api/health"
|
||||
print("Waiting for DocsGPT to answer ...")
|
||||
if not context.wait(health, args.timeout):
|
||||
@@ -443,11 +445,15 @@ def status(args, context: Optional[Context] = None) -> int:
|
||||
return 1
|
||||
if _mode(directory) == "native":
|
||||
services = context.service_manager()
|
||||
# The units bind 127.0.0.1 whatever DOCSGPT_BIND says, so that is what gets printed and checked:
|
||||
# against a LAN bind, stack.url and stack.health_url would advertise and poll an address
|
||||
# nothing listens on, and status would call a healthy install dead.
|
||||
port = env.get("DOCSGPT_PORT") or stack.DEFAULT_PORT
|
||||
print(f"DocsGPT {_record(directory).get('version', 'unknown')} in {directory} (native, {services.name})")
|
||||
print(f"Address: {stack.url(env, context.lan_ip())}")
|
||||
print(f"Address: http://localhost:{port}")
|
||||
for name in native.SERVICES:
|
||||
print(f" {name}: {'running' if services.is_running(name) else 'stopped'}")
|
||||
healthy = context.wait(stack.health_url(env), 0)
|
||||
healthy = context.wait(f"http://127.0.0.1:{port}/api/health", 0)
|
||||
print("API: answering" if healthy else "API: not answering (see `docsgpt logs`)")
|
||||
return 0 if healthy else 1
|
||||
|
||||
|
||||
@@ -61,9 +61,17 @@ def launchd_plist(unit: Unit, label: Optional[str] = None) -> str:
|
||||
return plistlib.dumps(body).decode("utf-8")
|
||||
|
||||
|
||||
def _systemd_quote(value: str) -> str:
|
||||
"""A unit-file value, double-quoted with backslashes and quotes escaped, as systemd reads them."""
|
||||
escaped = value.replace("\\", "\\\\").replace('"', '\\"')
|
||||
return f'"{escaped}"'
|
||||
|
||||
|
||||
def systemd_unit(unit: Unit) -> str:
|
||||
"""The systemd user unit for ``unit``; arguments are quoted, so a path with spaces survives."""
|
||||
environment = "\n".join(f'Environment="{key}={value}"' for key, value in sorted(unit.environment.items()))
|
||||
"""The systemd user unit for ``unit``; values are quoted, so a path with spaces survives."""
|
||||
environment = "\n".join(
|
||||
f"Environment={_systemd_quote(f'{key}={value}')}" for key, value in sorted(unit.environment.items())
|
||||
)
|
||||
command = " ".join(shlex.quote(argument) for argument in unit.arguments)
|
||||
return f"""[Unit]
|
||||
Description=DocsGPT ({unit.name})
|
||||
@@ -72,7 +80,7 @@ After=network-online.target
|
||||
[Service]
|
||||
Type=simple
|
||||
ExecStart={command}
|
||||
WorkingDirectory={unit.working_directory}
|
||||
WorkingDirectory={_systemd_quote(unit.working_directory)}
|
||||
{environment}
|
||||
Restart=always
|
||||
RestartSec=5
|
||||
@@ -181,12 +189,15 @@ class SystemdServices:
|
||||
self._systemctl("restart", f"{name}.service", check=True)
|
||||
|
||||
def stop(self, name: str) -> None:
|
||||
self._systemctl("stop", f"{name}.service")
|
||||
self._systemctl("stop", f"{name}.service", check=True)
|
||||
|
||||
def remove(self, name: str) -> None:
|
||||
self._systemctl("disable", "--now", f"{name}.service")
|
||||
# Only disable a unit systemd still knows about, so removing twice stays harmless; a real
|
||||
# failure has to surface before the unit file and the install record are thrown away.
|
||||
if self._unit_file(name).is_file():
|
||||
self._systemctl("disable", "--now", f"{name}.service", check=True)
|
||||
self._unit_file(name).unlink(missing_ok=True)
|
||||
self._systemctl("daemon-reload")
|
||||
self._systemctl("daemon-reload", check=True)
|
||||
|
||||
def is_running(self, name: str) -> bool:
|
||||
return self._systemctl("is-active", "--quiet", f"{name}.service").returncode == 0
|
||||
|
||||
Reference in new issue
Block a user