@simon-says-build/harbormaster 0.10.0 → 0.11.1
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- package/README.md +13 -0
- package/harbormaster +56 -5
- package/package.json +1 -1
package/README.md
CHANGED
|
@@ -124,3 +124,16 @@ while a container holds it. Simon Says' local Supabase stacks are containers
|
|
|
124
124
|
on harbormaster blocks. On simon-linux-1 (2026-10-09), four jobs were handed a
|
|
125
125
|
block a stack still held. No docker means the empty set, never a failure.
|
|
126
126
|
`python3 test/docker_ports.py`.
|
|
127
|
+
|
|
128
|
+
## A managed broker (0.11.0)
|
|
129
|
+
|
|
130
|
+
`harbormaster serve --managed` is for a broker that a service manager
|
|
131
|
+
(systemd, launchd) runs and restarts. It keeps a marker in the state dir
|
|
132
|
+
fresh, touched every minute. While the marker is fresh, clients never replace
|
|
133
|
+
the broker, even an older one, and never spawn their own. If it is briefly
|
|
134
|
+
down, they wait up to 2 minutes for the service manager to bring it back. Upgrades
|
|
135
|
+
are the service manager's job: restart it after installing a new version. A
|
|
136
|
+
stale marker means the service is gone, and clients spawn as before. Without
|
|
137
|
+
this, the broker belonged to whichever client happened to start it: on
|
|
138
|
+
simon-linux-1 that was a CI job, whose orphan cleanup killed the broker and
|
|
139
|
+
every emulator it ran for other jobs mid-suite. `python3 test/managed.py`.
|
package/harbormaster
CHANGED
|
@@ -59,7 +59,9 @@ independent suites, so they never collide on ports or data.
|
|
|
59
59
|
JVMs / functions workers re-parented to pid 1), stray run-*/alloc-* sims.
|
|
60
60
|
--fix evicts them; exits 1 when findings remain unfixed. The daemon runs
|
|
61
61
|
the same sweep itself at startup and every FBPORTS_SWEEP_INTERVAL (300s).
|
|
62
|
-
serve
|
|
62
|
+
serve [--managed] Run the daemon (auto-starts on first command). --managed: a service
|
|
63
|
+
manager (systemd/launchd) runs and restarts it; clients then never
|
|
64
|
+
replace or spawn a broker while its marker is fresh.
|
|
63
65
|
|
|
64
66
|
NOTE: firebase.json ports are optional — the daemon injects (and overrides) a
|
|
65
67
|
per-lease pool block, so concurrent suites never collide. Ledger:
|
|
@@ -82,7 +84,7 @@ import uuid
|
|
|
82
84
|
from datetime import datetime, timezone
|
|
83
85
|
from pathlib import Path
|
|
84
86
|
|
|
85
|
-
VERSION = "0.
|
|
87
|
+
VERSION = "0.11.1"
|
|
86
88
|
|
|
87
89
|
# Overridable for tests / parallel installs; default is the shared user daemon.
|
|
88
90
|
# The dir keeps the old "firebase-ports" name: pre-rename binaries still installed
|
|
@@ -1246,8 +1248,43 @@ class _Handler(http.server.BaseHTTPRequestHandler):
|
|
|
1246
1248
|
self._send(500, {"error": str(e)})
|
|
1247
1249
|
|
|
1248
1250
|
|
|
1249
|
-
|
|
1251
|
+
# A MANAGED broker (0.11.0): one a service manager runs and keeps alive —
|
|
1252
|
+
# `harbormaster serve --managed` under systemd or launchd. Its marker in the
|
|
1253
|
+
# state dir is touched every minute; while it is fresh, clients never replace
|
|
1254
|
+
# the broker and never spawn their own: the service manager owns its lifecycle
|
|
1255
|
+
# and its upgrades. Without this, the broker belonged to whichever client
|
|
1256
|
+
# happened to start it. On simon-linux-1 (2026-10-09) that was an SSB job's own
|
|
1257
|
+
# checkout inside the runner's systemd unit, and before it a CI job, whose
|
|
1258
|
+
# orphan cleanup killed the broker — and every emulator it ran for OTHER jobs —
|
|
1259
|
+
# mid-suite. A stale marker means the service is gone: clients spawn as before.
|
|
1260
|
+
MANAGED_MARK = STATE_DIR / "managed"
|
|
1261
|
+
MANAGED_FRESH_S = 300
|
|
1262
|
+
|
|
1263
|
+
|
|
1264
|
+
def _managed_recent():
|
|
1265
|
+
try:
|
|
1266
|
+
return time.time() - MANAGED_MARK.stat().st_mtime < MANAGED_FRESH_S
|
|
1267
|
+
except OSError:
|
|
1268
|
+
return False
|
|
1269
|
+
|
|
1270
|
+
|
|
1271
|
+
def _touch_managed():
|
|
1272
|
+
try:
|
|
1273
|
+
MANAGED_MARK.write_text(json.dumps({"pid": os.getpid(), "version": VERSION, "exe": str(Path(__file__).resolve())}))
|
|
1274
|
+
except OSError:
|
|
1275
|
+
pass
|
|
1276
|
+
|
|
1277
|
+
|
|
1278
|
+
def cmd_serve(managed=False):
|
|
1250
1279
|
STATE_DIR.mkdir(parents=True, exist_ok=True)
|
|
1280
|
+
if managed:
|
|
1281
|
+
_touch_managed()
|
|
1282
|
+
|
|
1283
|
+
def keep_marked():
|
|
1284
|
+
while True:
|
|
1285
|
+
time.sleep(60)
|
|
1286
|
+
_touch_managed()
|
|
1287
|
+
threading.Thread(target=keep_marked, daemon=True).start()
|
|
1251
1288
|
dropped = _scrub_environ()
|
|
1252
1289
|
if dropped:
|
|
1253
1290
|
print(f"harbormaster: scrubbed inherited env before serving: {', '.join(dropped)}", flush=True)
|
|
@@ -1362,8 +1399,11 @@ def _spawn_daemon():
|
|
|
1362
1399
|
|
|
1363
1400
|
def _ensure():
|
|
1364
1401
|
v = _daemon_version()
|
|
1402
|
+
managed = _managed_recent()
|
|
1365
1403
|
if v is not None:
|
|
1366
|
-
|
|
1404
|
+
# A managed broker is never replaced by a client: its service manager
|
|
1405
|
+
# owns upgrades (restart it to pick up a new install).
|
|
1406
|
+
if managed or _ver_tuple(v) >= _ver_tuple(VERSION):
|
|
1367
1407
|
return
|
|
1368
1408
|
# A newer client found an older daemon (e.g. a stale node_modules copy
|
|
1369
1409
|
# started it). Replace it: leases persist in the ledger and every leased
|
|
@@ -1381,6 +1421,17 @@ def _ensure():
|
|
|
1381
1421
|
raise RuntimeError(
|
|
1382
1422
|
f"daemon {v} is older than this client ({VERSION}) and did not "
|
|
1383
1423
|
f"shut down — kill the 'harbormaster serve' process and retry")
|
|
1424
|
+
if managed:
|
|
1425
|
+
# Down, but a service runs it: it is restarting. Wait for it rather than
|
|
1426
|
+
# start one of our own that would die with this client. Two minutes: a
|
|
1427
|
+
# busy box's startup sweep is not quick — the studio's first managed
|
|
1428
|
+
# start reaped 75 leftovers and served after ~40 s (2026-10-09), and a
|
|
1429
|
+
# client that gives up early starts a broker that fights the service.
|
|
1430
|
+
for _ in range(1200):
|
|
1431
|
+
if _up():
|
|
1432
|
+
return
|
|
1433
|
+
time.sleep(0.1)
|
|
1434
|
+
print("harbormaster: the managed broker did not come back in 120s — starting one here", file=sys.stderr)
|
|
1384
1435
|
_spawn_daemon()
|
|
1385
1436
|
for _ in range(50):
|
|
1386
1437
|
if _up():
|
|
@@ -1494,7 +1545,7 @@ def main():
|
|
|
1494
1545
|
if cmd == "version":
|
|
1495
1546
|
print(VERSION)
|
|
1496
1547
|
elif cmd == "serve":
|
|
1497
|
-
cmd_serve()
|
|
1548
|
+
cmd_serve(managed="managed" in flags)
|
|
1498
1549
|
elif cmd in ("acquire", "up"):
|
|
1499
1550
|
_ensure()
|
|
1500
1551
|
pid, start = _owner(opts)
|
package/package.json
CHANGED
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
{
|
|
2
2
|
"name": "@simon-says-build/harbormaster",
|
|
3
|
-
"version": "0.
|
|
3
|
+
"version": "0.11.1",
|
|
4
4
|
"description": "A localhost daemon that leases each caller its own port block — isolated Firebase emulator sandboxes, Metro/Expo ports, private iOS simulators — reaped when the caller exits. No port collisions, no shared instances, no pkill.",
|
|
5
5
|
"bin": {
|
|
6
6
|
"harbormaster": "harbormaster",
|