Close the nine open SDK tasks: one engine per directory, a tabbed dashboard, re-pairing, run recovery
Docs / docs (push) Successful in 27s
Playwright Tests / test-playwright (1, 2) (push) Failing after 17s
Playwright Tests / test-playwright (2, 2) (push) Failing after 12s
pre-commit / pre-commit (push) Failing after 1m59s
Test Backend / test-backend (push) Failing after 2m30s
Compose Smoke Test / test-compose (push) Failing after 13s
Playwright Tests / merge-reports (push) Failing after 2m19s
Docs / docs (push) Successful in 27s
Playwright Tests / test-playwright (1, 2) (push) Failing after 17s
Playwright Tests / test-playwright (2, 2) (push) Failing after 12s
pre-commit / pre-commit (push) Failing after 1m59s
Test Backend / test-backend (push) Failing after 2m30s
Compose Smoke Test / test-compose (push) Failing after 13s
Playwright Tests / merge-reports (push) Failing after 2m19s
serve: refuse a second engine for one data directory whatever port it was asked for, using the pidfile and a token this directory signed. The check runs before the database is touched and before the credential is written, which is what left every later CLI call pointing at a dead port. The terminal dashboard is three tabs (Overview, Runs, Logs) with the toolbar following the focused pane, the engine's output goes to serve.log rather than down a pipe, and closing the screen stops both reader threads so the prompt comes back. It adopts a running engine on every start, so stop/start and restart work on one it did not start, and a stop waits for the process to be gone before the next start. Enrolment reports itself in the modal. enroll: a new claim code replaces the pairing instead of being refused. The code is redeemed before anything is written, mappings to a portal being left are cleared, and a running engine redials when the stored enrolment changes. runs: an engine re-queues the runs left `queued` by the one before it, and `fluksio retry <id>` / `retry --group <sweep>` submits an interrupted run again with the same inputs and group, recorded through Run.parent_id. Co-Authored-By: Claude Opus 5 (1M context) <noreply@anthropic.com> Claude-Session: https://claude.ai/code/session_01U9BoNGq6V9MdRWAte7JBuC
This commit is contained in:
+32
-8
@@ -186,9 +186,9 @@ def _prepare(data_dir: Path) -> None:
|
||||
def _enroll(portal: str, code: str, as_email: str | None) -> int:
|
||||
from sqlmodel import Session
|
||||
|
||||
from fluksio.cloud import config as cloud_config
|
||||
from fluksio.cloud import enroll as enroll_mod
|
||||
from fluksio.core.bootstrap import ensure_superuser, pick_superuser
|
||||
from fluksio.core.config import settings
|
||||
from fluksio.core.db import engine
|
||||
|
||||
with Session(engine) as session:
|
||||
@@ -203,19 +203,15 @@ def _enroll(portal: str, code: str, as_email: str | None) -> int:
|
||||
# Read before the session closes: the instance is detached after it,
|
||||
# and touching an attribute then goes back to a database that is gone.
|
||||
email = user.email
|
||||
previous = cloud_config.load()
|
||||
try:
|
||||
config = enroll_mod.enroll(session, user, portal, code)
|
||||
except enroll_mod.AlreadyEnrolled as exc:
|
||||
print(
|
||||
f"error: {exc.detail} (see {settings.CLOUD_CONFIG_FILE}).",
|
||||
file=sys.stderr,
|
||||
flush=True,
|
||||
)
|
||||
return 1
|
||||
except enroll_mod.EnrollError as exc:
|
||||
print(f"error: {exc.detail}", file=sys.stderr, flush=True)
|
||||
return 1
|
||||
|
||||
if previous is not None and previous.instance_id != config.instance_id:
|
||||
_say(f"Replaced the connection to {previous.portal_url}.")
|
||||
_say(
|
||||
f"Connected to {config.portal_url} as {email} (instance {config.instance_id})."
|
||||
)
|
||||
@@ -387,6 +383,26 @@ def _free_port(host: str, start: int) -> int:
|
||||
return start
|
||||
|
||||
|
||||
def already_serving(data_dir: Path, host: str) -> str:
|
||||
"""Where this directory's engine is answering, if one is.
|
||||
|
||||
Two engines on one SQLite file is not a supported shape, and the second
|
||||
one does more than fail: it repoints `client.json` at itself, so every
|
||||
later CLI call goes to a port that dies with it. The pidfile says where to
|
||||
look and the token says whether what answers there is ours.
|
||||
"""
|
||||
running = read_pidfile(data_dir)
|
||||
if running is None:
|
||||
return ""
|
||||
reachable = "127.0.0.1" if host in ("0.0.0.0", "::", "") else host
|
||||
url = f"http://{reachable}:{running['port']}"
|
||||
# A pid can be reused, so the file alone is not proof. What answers has to
|
||||
# accept this directory's token as well.
|
||||
if probe_engine(url, _token_for(data_dir)) != "ours":
|
||||
return ""
|
||||
return f"{url} (pid {running['pid']})"
|
||||
|
||||
|
||||
def cmd_serve(args: argparse.Namespace) -> int:
|
||||
# At a terminal this is a dashboard with the engine as a child of it. The
|
||||
# import is here rather than at the top because it is only ever needed on
|
||||
@@ -397,6 +413,14 @@ def cmd_serve(args: argparse.Namespace) -> int:
|
||||
return run_tui(args)
|
||||
|
||||
data_dir = _data_dir(args.data_dir, args.shared)
|
||||
# Before `_prepare`, so a refusal never migrates the database another
|
||||
# engine is serving out of, and before `_sign_in`, so the credential keeps
|
||||
# pointing at the engine that is actually up.
|
||||
where = already_serving(data_dir, args.host)
|
||||
if where:
|
||||
_say(f"An engine for {data_dir} is already serving at {where}.")
|
||||
_say(" fluksio status talks to it; stop it to start another.")
|
||||
return 0
|
||||
for flag, name in CONCURRENCY_FLAGS.items():
|
||||
value = getattr(args, flag, None)
|
||||
if value is not None:
|
||||
|
||||
Reference in New Issue
Block a user