@simon-says-build/harbormaster 0.9.0 → 0.9.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 +24 -3
  2. package/package.json +1 -1
package/harbormaster CHANGED
@@ -72,6 +72,7 @@ import json
72
72
  import os
73
73
  import signal
74
74
  import socket
75
+ import shutil
75
76
  import subprocess
76
77
  import sys
77
78
  import threading
@@ -80,7 +81,7 @@ import uuid
80
81
  from datetime import datetime, timezone
81
82
  from pathlib import Path
82
83
 
83
- VERSION = "0.9.0"
84
+ VERSION = "0.9.1"
84
85
 
85
86
  # Overridable for tests / parallel installs; default is the shared user daemon.
86
87
  # The dir keeps the old "firebase-ports" name: pre-rename binaries still installed
@@ -88,6 +89,9 @@ VERSION = "0.9.0"
88
89
  STATE_DIR = Path(os.environ.get("FBPORTS_STATE_DIR",
89
90
  str(Path.home() / ".local" / "state" / "firebase-ports")))
90
91
  LEDGER = STATE_DIR / "ledger.json"
92
+ # Per-suite TMPDIR for the emulator process (see EmulatorPool._boot).
93
+ def _suite_tmpdir(label):
94
+ return STATE_DIR / "tmp" / str(label)
91
95
  BROKER_HOST, BROKER_PORT = "127.0.0.1", int(os.environ.get("FBPORTS_PORT", "4999"))
92
96
  # Blocks of 20 ports, 11000-14999 by default. Overridable so an isolated test
93
97
  # daemon (FBPORTS_PORT + FBPORTS_STATE_DIR) can also use a DISJOINT pool — its
@@ -639,6 +643,18 @@ class Broker:
639
643
  run_ports = {s: all_ports[s] for s in (only or declared) if s in all_ports}
640
644
  check = {s: p for s, p in run_ports.items() if s not in ("ui", "hub", "logging")}
641
645
  log = STATE_DIR / f"{label}.log"
646
+ # Each suite gets its OWN temp dir. firebase-tools keeps the Storage
647
+ # emulator's blobs at `${os.tmpdir()}/firebase/storage/blobs` — one
648
+ # fixed path for every instance on the box — and `stop()` rm -rf's it.
649
+ # So any suite shutting down wiped the blobs of every other suite still
650
+ # running, and their next upload died with ENOENT inside firebase-tools
651
+ # ("An unexpected error has occurred.", exit 2, all emulators gone).
652
+ # On a box that runs several suites at once that is not a corner case:
653
+ # 2026-08-28, three walk2gether e2e runs in a row on the studio, each
654
+ # killed by a daemon verify drive finishing. os.tmpdir() honours
655
+ # TMPDIR, so a per-suite one makes the path private.
656
+ suite_tmp = _suite_tmpdir(label)
657
+ suite_tmp.mkdir(parents=True, exist_ok=True)
642
658
  cmd = ["firebase", "emulators:start", "--config", tmp]
643
659
  if only:
644
660
  cmd += ["--only", ",".join(only)]
@@ -648,9 +664,9 @@ class Broker:
648
664
  # daemon's env — the functions emulator runtime inherits it, which is
649
665
  # the only way a CALLER can set per-suite env (e.g. LLM_CACHE_DIR)
650
666
  # when the daemon, not the caller, spawns the emulator.
651
- spawn_env = None
667
+ spawn_env = {**os.environ, "TMPDIR": str(suite_tmp)}
652
668
  if extra_env:
653
- spawn_env = {**os.environ, **{str(k): str(v) for k, v in dict(extra_env).items()}}
669
+ spawn_env.update({str(k): str(v) for k, v in dict(extra_env).items()})
654
670
  with open(log, "w") as lf:
655
671
  proc = subprocess.Popen(cmd, cwd=cwd, stdout=lf, stderr=subprocess.STDOUT, start_new_session=True, env=spawn_env)
656
672
  if on_spawn:
@@ -770,6 +786,11 @@ class Broker:
770
786
  tmp = rec.get("tmp_config")
771
787
  if tmp and rec.get("cwd") and (Path(rec["cwd"]) / tmp).exists():
772
788
  (Path(rec["cwd"]) / tmp).unlink()
789
+ # The suite's private TMPDIR (see _boot). Derived from the label the
790
+ # same way _boot derived it, so old ledger records need no new field.
791
+ label = rec.get("label") or (Path(rec["log"]).stem if rec.get("log") else None)
792
+ if label:
793
+ shutil.rmtree(_suite_tmpdir(label), ignore_errors=True)
773
794
  if rec.get("sim_udid"):
774
795
  gone = _sim_teardown(rec["sim_udid"])
775
796
  actions.append(f"sim {rec['sim_udid'][:8]} " +
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@simon-says-build/harbormaster",
3
- "version": "0.9.0",
3
+ "version": "0.9.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",