Closes the remaining gap after the previous commit's start-time guard:
restart's non-systemd branch used to be an unconditional
"cmd_stop; cmd_start -b" - if nothing was running locally, cmd_stop was
a silent no-op and cmd_start went ahead and started a brand-new local
instance regardless of whether the real one was already up elsewhere
(exactly how the cons-03/04/05 crosstalk happened - cmd_start's own new
guard already covers a plain `start`, but `restart` reached the same
code with different framing and needed its own check before getting
there, not just relying on the guard to reject it after the fact).
New cmd_restart(): local pid found -> stop+start in place (unchanged
behavior for the normal case); none found but $URL is healthy
elsewhere -> respawn that one instead (self-re-exec over HTTP, not a
new local process); nothing running anywhere -> start fresh here, same
as before. managed_by_systemd path unchanged.
Verified by stubbing server_pid/remote_duplicate_running/cmd_stop/
cmd_start/cmd_respawn and confirming cmd_restart dispatches to exactly
the right one for each of the three cases - not against any real host.
Co-Authored-By: Claude Sonnet 5 <noreply@anthropic.com>
Direct fix for a real incident (2026-09-16): three independent
eco.status_server instances ended up running simultaneously across
saresb-cons-03/04/05, each holding its own silently-drifting in-memory
namespace, discovered only by manually sweeping every console host.
cmd_start now checks remote_duplicate_running() before starting: if
$URL (almost always the site default from the env file, e.g.
saresb-cons-04) resolves to a *different* host than the one `start` is
running on, and that host's /health reports ready, refuse to start a
second one here. Deliberately does NOT block starting elsewhere when
$URL's host is actually down/unreachable - a genuine "the real one is
gone, stand up a replacement" case is not the mistake this guards
against. No shared lock file: the config directory isn't group-writable
across every account that might run this (same class of problem
eco_cnf_bernina/memory/ once hit), and $URL/health is already the right
source of truth, so there is no extra state to keep in sync or clean up
after a crash. -f/--force skips the check for the rare legitimate case.
Verified against local fake HTTP servers (different-host+healthy ->
refused; same-host -> allowed without even checking; different-host+
unreachable -> allowed) - never against any of the three real hosts.
Co-Authored-By: Claude Sonnet 5 <noreply@anthropic.com>
restart/stop/start only ever manage a process on the local host
(pgrep/systemctl are inherently local) -- pointing $URL at a server
running elsewhere, as a different account, does nothing useful there:
it just finds no local process and starts a brand-new one right here.
respawn instead POSTs /admin/restart to $URL over HTTP, same as the
GUI's "Restart process" button (client.reinit(mode="restart") ->
client.restart()) -- the server re-execs *itself* (os.execv, same
PID/user/host it already runs as), so this correctly reaches the real
process regardless of where the CLI is invoked from. -w polls /health
afterward until it reports ready with a new instance_id, mirroring
StatusServerClient.restart(wait=True)'s own timing.
Verified against a local fake HTTP server (no-wait, --wait, and the
connection-refused path) -- not against the real one, deliberately.
Co-Authored-By: Claude Sonnet 5 <noreply@anthropic.com>
- Remove det_jf: now that det_diff was fixed to JF01T03V01 (previous
commit), det_jf was a second device pointing at the same physical
Jungfrau, otherwise unreferenced anywhere.
- Add bernina.dummy (DummyMexicanHat): x, y (DummyAdjustable) + imex
(DetectorGet reading a live 2D Mexican-hat signal of their current
values, (1-r^2)*exp(-r^2/2), r=sqrt(x^2+y^2) -- stays within [-1, 1]
for any x, y, small Gaussian noise by default). No real hardware;
for testing grid-scan / live 2D counter-grid machinery.
- Fix Scans.meshscan(..., repetitions=N>1): grid_specs["index_plan"] is
built once, for a single pass over the grid, before StepScan.__init__
repeats `values`/`pulses_per_step` by `repetitions` -- so
do_next_step()'s gridspecs["index_plan"][self.next_step] indexed past
the end (IndexError) the moment the scan entered its second
repetition. Now repeats index_plan the same way.
- Add scripts/eco-escape-test: like eco-dev, but also prepends a sibling
escape-fel checkout to PYTHONPATH (assumed to live next to this eco
checkout), for testing local checkouts of both together instead of
whatever's installed.
Co-Authored-By: Claude Sonnet 5 <noreply@anthropic.com>