@simon-says-build/harbormaster 0.11.0 → 0.12.0
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 +18 -1
- package/harbormaster +52 -6
- package/package.json +1 -1
package/README.md
CHANGED
|
@@ -131,9 +131,26 @@ block a stack still held. No docker means the empty set, never a failure.
|
|
|
131
131
|
(systemd, launchd) runs and restarts. It keeps a marker in the state dir
|
|
132
132
|
fresh, touched every minute. While the marker is fresh, clients never replace
|
|
133
133
|
the broker, even an older one, and never spawn their own. If it is briefly
|
|
134
|
-
down, they wait up to
|
|
134
|
+
down, they wait up to 2 minutes for the service manager to bring it back. Upgrades
|
|
135
135
|
are the service manager's job: restart it after installing a new version. A
|
|
136
136
|
stale marker means the service is gone, and clients spawn as before. Without
|
|
137
137
|
this, the broker belonged to whichever client happened to start it: on
|
|
138
138
|
simon-linux-1 that was a CI job, whose orphan cleanup killed the broker and
|
|
139
139
|
every emulator it ran for other jobs mid-suite. `python3 test/managed.py`.
|
|
140
|
+
|
|
141
|
+
## A project's own suite env (0.12.0)
|
|
142
|
+
|
|
143
|
+
`--fn-env` is per caller, so a setting every suite of a project needs had to
|
|
144
|
+
be repeated by every caller (session tests, e2e, agent runs), and the one that
|
|
145
|
+
forgot ran against production. A project now names it once, in
|
|
146
|
+
`harbormaster.env` next to its `firebase.json`:
|
|
147
|
+
|
|
148
|
+
# every emulator suite of this project runs under the TEST service account
|
|
149
|
+
GOOGLE_APPLICATION_CREDENTIALS=./impulse-test-bq.json
|
|
150
|
+
|
|
151
|
+
`KEY=VALUE` lines, `#` comments. A value starting with `./` or `../` is made
|
|
152
|
+
absolute against the project, since the emulator runs functions (and
|
|
153
|
+
extensions) in other directories. `acquire`: on top of the daemon's scrubbed
|
|
154
|
+
env, under the caller's explicit `--fn-env`. `exec`: on top of the caller's
|
|
155
|
+
ambient env, so a key exported in some shell can't leak into the suite.
|
|
156
|
+
`python3 test/project_env.py`.
|
package/harbormaster
CHANGED
|
@@ -84,7 +84,7 @@ import uuid
|
|
|
84
84
|
from datetime import datetime, timezone
|
|
85
85
|
from pathlib import Path
|
|
86
86
|
|
|
87
|
-
VERSION = "0.
|
|
87
|
+
VERSION = "0.12.0"
|
|
88
88
|
|
|
89
89
|
# Overridable for tests / parallel installs; default is the shared user daemon.
|
|
90
90
|
# The dir keeps the old "firebase-ports" name: pre-rename binaries still installed
|
|
@@ -699,7 +699,7 @@ class Broker:
|
|
|
699
699
|
# daemon's env — the functions emulator runtime inherits it, which is
|
|
700
700
|
# the only way a CALLER can set per-suite env (e.g. LLM_CACHE_DIR)
|
|
701
701
|
# when the daemon, not the caller, spawns the emulator.
|
|
702
|
-
spawn_env = {**os.environ, "TMPDIR": str(suite_tmp)}
|
|
702
|
+
spawn_env = {**os.environ, **project_env(cwd), "TMPDIR": str(suite_tmp)}
|
|
703
703
|
if extra_env:
|
|
704
704
|
spawn_env.update({str(k): str(v) for k, v in dict(extra_env).items()})
|
|
705
705
|
with open(log, "w") as lf:
|
|
@@ -1333,6 +1333,46 @@ def _daemon_version():
|
|
|
1333
1333
|
return None
|
|
1334
1334
|
|
|
1335
1335
|
|
|
1336
|
+
# A PROJECT'S OWN SUITE ENV: <cwd>/harbormaster.env.
|
|
1337
|
+
#
|
|
1338
|
+
# --fn-env is per CALLER, so a setting every suite of a project needs (the
|
|
1339
|
+
# credentials its emulator must run under, say) had to be repeated by every
|
|
1340
|
+
# caller — session tests, e2e, agent runs — and the one that forgot ran
|
|
1341
|
+
# against production. impulse-functions (2026-10-16): its BigQuery export
|
|
1342
|
+
# extensions run under the Extensions emulator and stream to whatever project
|
|
1343
|
+
# the ambient credentials can reach; only a test service account keeps that
|
|
1344
|
+
# test-only. So the project names it once, in the repo:
|
|
1345
|
+
#
|
|
1346
|
+
# # harbormaster.env, next to firebase.json
|
|
1347
|
+
# GOOGLE_APPLICATION_CREDENTIALS=./impulse-test-bq.json
|
|
1348
|
+
#
|
|
1349
|
+
# KEY=VALUE lines; blank lines and `#` comments skipped; a value starting
|
|
1350
|
+
# with ./ or ../ is a path relative to the project, made absolute (the
|
|
1351
|
+
# emulator's functions run in other directories). acquire: on top of the
|
|
1352
|
+
# daemon's (scrubbed) env, UNDER the caller's explicit --fn-env. exec: on top
|
|
1353
|
+
# of the caller's ambient env, which is not a choice. Pure; missing file = {}.
|
|
1354
|
+
PROJECT_ENV_FILE = "harbormaster.env"
|
|
1355
|
+
|
|
1356
|
+
|
|
1357
|
+
def project_env(cwd):
|
|
1358
|
+
f = Path(cwd) / PROJECT_ENV_FILE
|
|
1359
|
+
if not f.is_file():
|
|
1360
|
+
return {}
|
|
1361
|
+
out = {}
|
|
1362
|
+
for raw in f.read_text().splitlines():
|
|
1363
|
+
line = raw.strip()
|
|
1364
|
+
if not line or line.startswith("#") or "=" not in line:
|
|
1365
|
+
continue
|
|
1366
|
+
k, v = line.split("=", 1)
|
|
1367
|
+
k, v = k.strip(), v.strip()
|
|
1368
|
+
if len(v) >= 2 and v[0] == v[-1] and v[0] in "'\"":
|
|
1369
|
+
v = v[1:-1]
|
|
1370
|
+
if v.startswith("./") or v.startswith("../"):
|
|
1371
|
+
v = str((Path(cwd) / v).resolve())
|
|
1372
|
+
out[k] = v
|
|
1373
|
+
return out
|
|
1374
|
+
|
|
1375
|
+
|
|
1336
1376
|
def _ver_tuple(v):
|
|
1337
1377
|
try:
|
|
1338
1378
|
return tuple(int(x) for x in str(v).split("."))
|
|
@@ -1423,12 +1463,15 @@ def _ensure():
|
|
|
1423
1463
|
f"shut down — kill the 'harbormaster serve' process and retry")
|
|
1424
1464
|
if managed:
|
|
1425
1465
|
# 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.
|
|
1427
|
-
|
|
1466
|
+
# start one of our own that would die with this client. Two minutes: a
|
|
1467
|
+
# busy box's startup sweep is not quick — the studio's first managed
|
|
1468
|
+
# start reaped 75 leftovers and served after ~40 s (2026-10-09), and a
|
|
1469
|
+
# client that gives up early starts a broker that fights the service.
|
|
1470
|
+
for _ in range(1200):
|
|
1428
1471
|
if _up():
|
|
1429
1472
|
return
|
|
1430
1473
|
time.sleep(0.1)
|
|
1431
|
-
print("harbormaster: the managed broker did not come back in
|
|
1474
|
+
print("harbormaster: the managed broker did not come back in 120s — starting one here", file=sys.stderr)
|
|
1432
1475
|
_spawn_daemon()
|
|
1433
1476
|
for _ in range(50):
|
|
1434
1477
|
if _up():
|
|
@@ -1623,7 +1666,10 @@ def main():
|
|
|
1623
1666
|
if only:
|
|
1624
1667
|
run += ["--only", ",".join(only)]
|
|
1625
1668
|
run.append(script)
|
|
1626
|
-
|
|
1669
|
+
# The project's env beats the caller's AMBIENT env here: a key
|
|
1670
|
+
# exported in some shell for a one-off script is exactly what
|
|
1671
|
+
# harbormaster.env exists to keep out of the suite (project_env).
|
|
1672
|
+
rc = subprocess.call(run, cwd=cwd, env={**os.environ, **project_env(cwd)})
|
|
1627
1673
|
finally:
|
|
1628
1674
|
try:
|
|
1629
1675
|
_call("POST", "/release", {"leaseId": lid})
|
package/package.json
CHANGED
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
{
|
|
2
2
|
"name": "@simon-says-build/harbormaster",
|
|
3
|
-
"version": "0.
|
|
3
|
+
"version": "0.12.0",
|
|
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",
|