@simon-says-build/harbormaster 0.8.0 → 0.8.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 (2) hide show
  1. package/harbormaster +42 -6
  2. package/package.json +1 -1
package/harbormaster CHANGED
@@ -80,7 +80,7 @@ import uuid
80
80
  from datetime import datetime, timezone
81
81
  from pathlib import Path
82
82
 
83
- VERSION = "0.8.0"
83
+ VERSION = "0.8.1"
84
84
 
85
85
  # Overridable for tests / parallel installs; default is the shared user daemon.
86
86
  # The dir keeps the old "firebase-ports" name: pre-rename binaries still installed
@@ -127,6 +127,22 @@ SWEEP_INTERVAL = int(os.environ.get("FBPORTS_SWEEP_INTERVAL", "300"))
127
127
  SWEEP_SIGNATURES = ("functionsEmulatorRuntime", "cloud-firestore-emulator",
128
128
  "cloud-storage-rules-runtime", "pubsub-emulator",
129
129
  "cloud-datastore-emulator", "firebase-database-emulator")
130
+ # What the listener sweep may KILL. Everything a lease has ever put on a pool
131
+ # port — firebase hubs and their JVMs, Metro/vite/dev servers, test drivers —
132
+ # runs under node, java, or python. A listener that is none of those is a
133
+ # bystander that happened to bind in-range (ollama's default 11434 sits inside
134
+ # the default pool, and the sweep killed it on 2026-08-11), and killing
135
+ # strangers is the pkill blast radius this tool exists to end. Bystanders are
136
+ # still reported — a squatted block can't be leased, so doctor must show them —
137
+ # but they are never signalled. The trade: an escapee from a LOST lease whose
138
+ # --spawn was some other binary now outlives the sweep; report-only is the
139
+ # right side of that trade.
140
+ LEASE_SHAPES = ("node", "java", "python")
141
+
142
+
143
+ def _lease_shaped(cmdline):
144
+ exe = os.path.basename((cmdline.split() or [""])[0]).lower()
145
+ return exe.startswith(LEASE_SHAPES)
130
146
 
131
147
 
132
148
  def _now():
@@ -918,8 +934,9 @@ class Broker:
918
934
  if POOL_BASE <= port < pool_end and port not in leased_ports:
919
935
  for pid in pids:
920
936
  if pid not in lease_pids:
937
+ cmd = _proc_field(pid, "command=")[:200]
921
938
  findings.append({"kind": "listener", "pid": pid, "port": port,
922
- "cmd": _proc_field(pid, "command=")[:200]})
939
+ "cmd": cmd, "evict": _lease_shaped(cmd)})
923
940
  try:
924
941
  r = subprocess.run(["ps", "-xo", "pid=,ppid=,command="],
925
942
  capture_output=True, text=True, timeout=10)
@@ -944,7 +961,9 @@ class Broker:
944
961
  and udid not in leased_udids):
945
962
  findings.append({"kind": "sim", "udid": udid, "name": name})
946
963
  if apply and findings:
947
- pids = {f["pid"] for f in findings if f["kind"] in ("listener", "orphan")}
964
+ pids = {f["pid"] for f in findings
965
+ if f["kind"] == "orphan"
966
+ or (f["kind"] == "listener" and f.get("evict"))}
948
967
  if pids:
949
968
  killed = _kill_wave(pids)
950
969
  print(f"world sweep killed {killed}", flush=True)
@@ -953,6 +972,14 @@ class Broker:
953
972
  gone = _sim_teardown(f["udid"])
954
973
  print(f"world sweep sim {f['name']} ({f['udid'][:8]}): "
955
974
  + ("deleted" if gone else "DELETE FAILED"), flush=True)
975
+ elif f["kind"] == "listener" and not f.get("evict"):
976
+ # Logged once per pid — a bystander survives every sweep by
977
+ # design, and 288 identical lines a day is how logs go unread.
978
+ seen = self.__dict__.setdefault("_bystanders_logged", set())
979
+ if f["pid"] not in seen:
980
+ seen.add(f["pid"])
981
+ print(f"world sweep bystander on port {f['port']} left alone "
982
+ f"(not lease-shaped): pid {f['pid']} — {f['cmd']}", flush=True)
956
983
  else:
957
984
  print(f"world sweep {f['kind']}: pid {f['pid']}"
958
985
  + (f" port {f['port']}" if f.get("port") else "")
@@ -1339,14 +1366,23 @@ def main():
1339
1366
  if not findings:
1340
1367
  print(f"clean — {out.get('leases', 0)} lease(s), no strays.")
1341
1368
  return
1369
+ spared = [f for f in findings if f["kind"] == "listener" and not f.get("evict", True)]
1342
1370
  for f in findings:
1343
1371
  if f["kind"] == "sim":
1344
1372
  print(f" stray sim {f['name']} {f['udid']}")
1345
1373
  else:
1346
1374
  where = f" port {f['port']}" if f.get("port") else ""
1347
- print(f" {f['kind']:<12} pid {f['pid']}{where} {f['cmd']}")
1348
- print(f"{len(findings)} finding(s) " +
1349
- ("evicted." if out.get("fixed") else "— run `harbormaster doctor --fix` to evict."))
1375
+ tag = " (bystander — never evicted)" if f in spared else ""
1376
+ print(f" {f['kind']:<12} pid {f['pid']}{where} {f['cmd']}{tag}")
1377
+ evictable = len(findings) - len(spared)
1378
+ if out.get("fixed"):
1379
+ print(f"{evictable} finding(s) evicted."
1380
+ + (f" {len(spared)} bystander(s) left alone — they squat pool "
1381
+ f"ports harbormaster can't lease; move them or the pool."
1382
+ if spared else ""))
1383
+ else:
1384
+ print(f"{len(findings)} finding(s) — run `harbormaster doctor --fix` to evict"
1385
+ + (f" ({len(spared)} bystander(s) will be left alone)." if spared else "."))
1350
1386
  if not out.get("fixed"):
1351
1387
  sys.exit(1)
1352
1388
  elif cmd in ("status", "leases"):
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@simon-says-build/harbormaster",
3
- "version": "0.8.0",
3
+ "version": "0.8.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",