Skip to content

Commit 2954e4e

Browse files
committed
fix(desktop): make dev server start idempotent and agent-safe
dev.sh start no longer stops a healthy server, uses start_new_session for macOS detach, flock to prevent parallel start races, and adds status. main.py exits 0 when ABI_DESKTOP_PORT is set and our app is already healthy.
1 parent 1d86e70 commit 2954e4e

6 files changed

Lines changed: 289 additions & 47 deletions

File tree

libs/naas-abi/naas_abi/apps/desktop/AGENTS.md

Lines changed: 9 additions & 4 deletions
Original file line numberDiff line numberDiff line change
@@ -353,10 +353,13 @@ Conventions and seams:
353353
Run only one dev instance at a time (the embedded Oxigraph graph uses on-disk RocksDB).
354354

355355
```bash
356-
# from repo root — detached, survives agent shells; opens nothing automatically
356+
# from repo root — idempotent; safe for parallel agents; never kills a healthy server
357357
make dev-desktop
358358

359-
# stop supervisor + server + clear stale locks
359+
# check health + PIDs without restarting
360+
libs/naas-abi/naas_abi/apps/desktop/scripts/dev.sh status
361+
362+
# stop supervisor + server (only when you intend to shut down)
360363
libs/naas-abi/naas_abi/apps/desktop/scripts/dev.sh stop
361364
```
362365

@@ -365,8 +368,10 @@ libs/naas-abi/naas_abi/apps/desktop/scripts/dev.sh stop
365368
- **Meta:** `~/.abi-desktop/server.json` (URL, port, worker PID)
366369
- **Hot reload:** Python under `desktop/` restarts on save. Frontend is vanilla JS/CSS under
367370
`gui/web/` — refresh the browser after edits; no bundler.
368-
- **Instance lock:** skipped when `--reload` is active; `dev.sh` kills stale listeners and
369-
clears `instance.lock` before start. Do not run two dev servers against the same data dir.
371+
- **Instance lock:** skipped when `--reload` is active. `dev.sh start` is idempotent: if
372+
`/api/health` returns 200, it prints the URL and exits 0 without stopping anything.
373+
Only `dev.sh stop` tears down the supervisor. Do not run two dev servers against
374+
different data dirs on the same port.
370375

371376
Foreground browser dev (no supervisor):
372377

libs/naas-abi/naas_abi/apps/desktop/README.md

Lines changed: 29 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -20,18 +20,46 @@ uv sync --all-extras
2020

2121
Recommended daily workflow: run from source in your browser. No PyInstaller rebuild required.
2222

23+
**Canonical command** (idempotent: safe to run repeatedly; never kills a healthy server):
24+
2325
```bash
2426
# from repo root — detached supervisor, Python hot reload, crash restart
2527
make dev-desktop
28+
# same as:
29+
libs/naas-abi/naas_abi/apps/desktop/scripts/dev.sh start
30+
# or:
31+
libs/naas-abi/naas_abi/apps/desktop/scripts/ensure-dev.sh
32+
```
33+
34+
Check status without restarting:
35+
36+
```bash
37+
libs/naas-abi/naas_abi/apps/desktop/scripts/dev.sh status
38+
```
2639

27-
# stop the dev server
40+
Stop only when you explicitly want to shut down:
41+
42+
```bash
2843
libs/naas-abi/naas_abi/apps/desktop/scripts/dev.sh stop
2944
```
3045

3146
Open [http://127.0.0.1:54242](http://127.0.0.1:54242). Override the port with `ABI_DESKTOP_PORT` if needed.
3247

3348
Logs: `~/.abi-desktop/server.log`. Server metadata (URL, PID): `~/.abi-desktop/server.json`.
3449

50+
### Troubleshooting
51+
52+
| Symptom | Cause | Fix |
53+
|---|---|---|
54+
| `already running at http://127.0.0.1:54242` | Healthy server on port | No action needed; open the URL |
55+
| `port 54242 in use by a non-ABI process` | Another app holds the port | Quit that app, or `dev.sh stop`, or set `ABI_DESKTOP_PORT` |
56+
| `supervisor running but server unhealthy` | Stuck/crashed worker | `dev.sh stop` then `dev.sh start` |
57+
| Server dies when a Cursor agent exits | Old supervisor not detached | Update to latest `dev.sh` (uses `start_new_session`) |
58+
| `instance.lock` / graph LOCK errors | Two dev servers on same data dir | Run only one `dev.sh start`; use `dev.sh stop` to reset |
59+
| Exit code 143 / SIGTERM in `server.log` | Agent killed the process group | Use `make dev-desktop` (detached supervisor), not foreground `uv run` in agent shells |
60+
61+
**Do not** call `dev.sh stop` unless you intend to shut down. Parallel `dev.sh start` calls are safe: they detect health and exit 0 without restarting.
62+
3563
Use the **workspace switcher** (icon rail top: logo only, Nexus-style) to open or switch IDE workspaces. The **status bar** (left) shows the current workspace basename and git branch as read-only context. Each workspace is a folder on disk (e.g. `~/abi`). In browser dev, **Open Folder…** accepts a typed path; in the pywebview app, **Browse…** opens the native folder picker.
3664

3765
**Reload behavior:**

libs/naas-abi/naas_abi/apps/desktop/main.py

Lines changed: 6 additions & 5 deletions
Original file line numberDiff line numberDiff line change
@@ -155,6 +155,10 @@ def resolve_server_port(*, allow_fallback: bool = True) -> int:
155155
)
156156
raise SystemExit(1)
157157
if not _port_available(port):
158+
if _is_abi_desktop_on_port(port):
159+
url = _read_server_url() or f"http://127.0.0.1:{port}"
160+
print(f"{APP_NAME} is already running at {url}")
161+
raise SystemExit(0)
158162
print(_port_in_use_message(port), file=sys.stderr)
159163
raise SystemExit(1)
160164
return port
@@ -165,11 +169,8 @@ def resolve_server_port(*, allow_fallback: bool = True) -> int:
165169

166170
if _is_abi_desktop_on_port(port):
167171
url = _read_server_url() or f"http://127.0.0.1:{port}"
168-
print(
169-
f"{APP_NAME} is already serving {url} (see ~/.abi-desktop/server.json).",
170-
file=sys.stderr,
171-
)
172-
raise SystemExit(1)
172+
print(f"{APP_NAME} is already running at {url}")
173+
raise SystemExit(0)
173174

174175
if allow_fallback and _port_available(port + 1):
175176
print(

libs/naas-abi/naas_abi/apps/desktop/main_test.py

Lines changed: 21 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -125,6 +125,27 @@ def test_resolve_server_port_in_use_exits(
125125
assert "ABI_DESKTOP_PORT" in err
126126

127127

128+
def test_resolve_server_port_env_already_running_abi(
129+
monkeypatch: pytest.MonkeyPatch,
130+
capsys: pytest.CaptureFixture[str],
131+
ephemeral_default_port: int,
132+
) -> None:
133+
"""When ABI_DESKTOP_PORT is set and our app is healthy, exit 0 (idempotent)."""
134+
from unittest.mock import patch
135+
136+
port = ephemeral_default_port
137+
monkeypatch.setenv("ABI_DESKTOP_PORT", str(port))
138+
with socket.socket(socket.AF_INET, socket.SOCK_STREAM) as sock:
139+
sock.bind(("127.0.0.1", port))
140+
with patch("desktop.main._is_abi_desktop_on_port", return_value=True):
141+
with patch("desktop.main._read_server_url", return_value=f"http://127.0.0.1:{port}"):
142+
with pytest.raises(SystemExit) as exc:
143+
resolve_server_port(allow_fallback=False)
144+
assert exc.value.code == 0
145+
out = capsys.readouterr().out
146+
assert "already running" in out
147+
148+
128149
@pytest.fixture
129150
def bound_port() -> Iterator[int]:
130151
with socket.socket(socket.AF_INET, socket.SOCK_STREAM) as sock:

0 commit comments

Comments
 (0)