fix: five from the full review

- doctor printed OPENAI_BASE_URL raw, the third place a credential-bearing URL
  reached the terminal; it goes through _endpoint like the others.
- dev.run left its output readers unjoined, so a child's last lines could be
  lost on exit. The threads are kept and joined during teardown.
- logs read each file and then reopened it to follow, so anything written in
  between appeared in neither. One handle now serves both.
- dev took --port straight from argparse: 0 would have served on an ephemeral
  port while printing 0, and oversized values reach socket.bind. It goes
  through _port_number first.
- doctor --redis-url overrode only the broker, so a stale result backend or
  cache was still pinged and the flag looked broken. All three endpoints now
  come from the URL given.
This commit is contained in:
Alex committed 2026-09-17 13:15:56 +01:00
1 parent d0a0b352e0
commit da58c072a0
4 files changed
+131 -18

No files matched your search

+41 -17
View File
@@ -615,24 +615,35 @@ def logs(args, context: Optional[Context] = None) -> int:
if _mode(directory) == "native":
logs_dir = directory / "logs"
wanted = args.services or ["api", "worker"]
# Opened before the first read and kept: a line written between printing what is there and
# starting to follow would otherwise appear in neither.
handles = {}
for service in wanted:
path = logs_dir / f"{service}.log"
print(f"=== {path}")
if path.is_file():
lines = path.read_text(encoding="utf-8", errors="replace").splitlines()
print("\n".join(lines[-args.tail:] if args.tail else lines))
else:
if not path.is_file():
print("(nothing logged yet)")
continue
handle = path.open("r", encoding="utf-8", errors="replace")
lines = handle.read().splitlines()
print("\n".join(lines[-args.tail:] if args.tail else lines))
handles[service] = handle
if args.follow:
return _follow(logs_dir, wanted)
return _follow(logs_dir, wanted, handles)
for handle in handles.values():
handle.close()
return 0
options = (["--follow"] if args.follow else []) + (["--tail", str(args.tail)] if args.tail else [])
return context.docker.compose(directory, "logs", *options, *args.services, check=False).returncode
def _follow(logs_dir: Path, services: list) -> int:
"""Print new lines from each service's log until the terminal interrupts, prefixed by service."""
handles: dict = {}
def _follow(logs_dir: Path, services: list, handles: Optional[dict] = None) -> int:
"""Print new lines from each service's log until the terminal interrupts, prefixed by service.
``handles`` are the files already read, positioned where that read left off, so nothing written
in between is skipped. Files that did not exist yet are opened as they appear.
"""
handles = dict(handles or {})
try:
while True:
for service in services:
@@ -734,7 +745,8 @@ def dev(args, context: Optional[Context] = None) -> int:
"`docsgpt dev` runs the code in a source checkout, and this is an installed package. "
"Clone the repository and run it from there, or use `docsgpt up --native` to run this copy."
)
if not _port_is_free(args.port):
port = _port_number(args.port, "--port")
if not _port_is_free(port):
raise DeployError(
f"port {args.port} is already in use, so the API cannot bind it. Stop what is on it "
f"(a previous `docsgpt dev`, or `docsgpt down` for an install), or pass --port."
@@ -879,7 +891,25 @@ def _check_provider(env: Mapping[str, str]) -> Check:
return Check("provider", "ok", "the DocsGPT public API (no key needed)")
if not (env.get("API_KEY") or env.get("OPENAI_API_KEY")):
return Check("provider", "fail", f"{provider} is configured but no API_KEY is set")
return Check("provider", "ok", f"{provider}{' at ' + env['OPENAI_BASE_URL'] if env.get('OPENAI_BASE_URL') else ''}")
endpoint = _endpoint(env["OPENAI_BASE_URL"]) if env.get("OPENAI_BASE_URL") else ""
return Check("provider", "ok", f"{provider}{' at ' + endpoint if endpoint else ''}")
def _redis_to_check(args, env: Mapping[str, str]) -> dict:
"""The Redis endpoints to ping: all three from --redis-url when given, else what .env holds.
Overriding only the broker would still ping a stale result backend or cache, and the check
fails on the first endpoint that does not answer -- so --redis-url would appear not to work.
"""
if args.redis_url:
names = {"CELERY_BROKER_URL": "broker", "CELERY_RESULT_BACKEND": "results", "CACHE_REDIS_URL": "cache"}
return {names[key]: value for key, value in _redis_urls(args.redis_url).items()}
pairs = (
("broker", env.get("CELERY_BROKER_URL")),
("results", env.get("CELERY_RESULT_BACKEND")),
("cache", env.get("CACHE_REDIS_URL")),
)
return {key: value for key, value in pairs if value}
def doctor(args, context: Optional[Context] = None) -> int:
@@ -898,13 +928,7 @@ def doctor(args, context: Optional[Context] = None) -> int:
Check("settings", "ok" if env_path.is_file() else "warn",
f"{env_path}" if env_path.is_file() else f"{env_path} does not exist yet; defaults are in use"),
_check_postgres(args.postgres_uri or env.get("POSTGRES_URI")),
_check_redis({
key: value for key, value in (
("broker", args.redis_url or env.get("CELERY_BROKER_URL")),
("results", env.get("CELERY_RESULT_BACKEND")),
("cache", env.get("CACHE_REDIS_URL")),
) if value
}),
_check_redis(_redis_to_check(args, env)),
_check_provider(env),
]
+8 -1
View File
@@ -181,6 +181,7 @@ def run(
colour = out.isatty() if colour is None else colour
lock = threading.Lock()
running: list[tuple[Child, object]] = []
pumps: list[threading.Thread] = []
try:
for child in children:
process = spawn(
@@ -195,7 +196,9 @@ def run(
start_new_session=os.name != "nt",
)
running.append((child, process))
threading.Thread(target=_pump, args=(child, process, out, lock, colour), daemon=True).start()
pump = threading.Thread(target=_pump, args=(child, process, out, lock, colour), daemon=True)
pump.start()
pumps.append(pump)
while True:
for child, process in running:
@@ -214,3 +217,7 @@ def run(
return 0
finally:
_stop(running, grace, sleep)
# Join the readers: a child's last lines are still in flight when it exits, and dropping
# them loses exactly the output that says why it stopped.
for pump in pumps:
pump.join(timeout=grace)