@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.
Files changed (3) hide show
  1. package/README.md +13 -0
  2. package/harbormaster +56 -5
  3. 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 Run the daemon (auto-starts on first command).
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.10.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
- def cmd_serve():
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
- if _ver_tuple(v) >= _ver_tuple(VERSION):
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.10.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",