@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.
- package/harbormaster +42 -6
- 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.
|
|
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":
|
|
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
|
|
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
|
-
|
|
1348
|
-
|
|
1349
|
-
|
|
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.
|
|
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",
|