From 82c354d70ec0ac9af59f0ff1c90dcbbd0912c8b5 Mon Sep 17 00:00:00 2001 From: ScottW514 Date: Wed, 23 Sep 2026 16:29:55 -0400 Subject: [PATCH] setup suite: the port wait, and the first run's accounts on a failure Two harness faults found on the bench reference in the campaign on image 20260923084705. setup.gate-blocks-controllers probed the Grbl port once, at the moment GET /mode first read the controller running. The supervisor reports running from the fork and the controller binds TCP 23 a few tens of milliseconds later, so the probe failed whenever the 0.25 s poll landed inside that window ("the Grbl port is closed with the controller running"). Timed on the board with the controller restarted through forgectrl: /mode read running at 0.066 s, the port accepted at 0.087 s. The test now waits up to 10 s for the port (50 ms polls) and records how long it took (port_open_after_s). PASS on the bench (0.001 s). first_run() replayed the system accounts (forgefirm-users reload) and removed the temporary home only after its block succeeded. When the block failed, the records came back but /etc/passwd kept the temporary account in place of the operator's, and image.health failed after it (the record's account missing from /etc/passwd). The replay and the home removal now run in a finally. Proof on the bench: the failing case run on purpose (setup.first-run-flow against the image's own forgectrl, which refuses a home by hand at the cloud step) leaves the operator's account the one account from uid 1000 up and no temporary home; the same failure before the fix left the temporary account in /etc/passwd and its home in /data/forgefirm/home. forgetest's unit tests: 452 OK. setup.first-run-flow and setup.gate-blocks-controllers PASS on the bench with this file mounted. --- forgetest/forgetest/suite/setup.py | 51 ++++++++++++++++++------------ 1 file changed, 30 insertions(+), 21 deletions(-) diff --git a/forgetest/forgetest/suite/setup.py b/forgetest/forgetest/suite/setup.py index 62fbe11..64ffed9 100644 --- a/forgetest/forgetest/suite/setup.py +++ b/forgetest/forgetest/suite/setup.py @@ -427,8 +427,12 @@ def gate_blocks_controllers(ctx): ev["override_mode"] = m ctx.check(took is not None and isinstance(m, dict) and m.get("controller") == "running", "the controller did not come back under the override: %s", m) - ctx.check(hw.grbl_port_open(timeout=5), "the Grbl port is closed with the controller running") - ctx.log("override: the controller is back (%s)", m) + # /mode reads running from the fork; the controller binds its port + # a few tens of milliseconds later + opened = ctx.wait_for(lambda: hw.grbl_port_open(timeout=1), 10, poll=0.05) + ev["port_open_after_s"] = opened + ctx.check(opened is not None, "the Grbl port is closed with the controller running") + ctx.log("override: the controller is back (%s), the Grbl port accepts after %.3f s", m, opened) # the real record is back under a restart; the override reads as found after = wiz(fc) @@ -1419,25 +1423,30 @@ def first_run(ctx): except OSError: homes_before = set() state = {"temp_account": None} - with Restore(ctx, FIRST_RUN_SETTINGS): - with installed(ctx, {record_path(): seed, users_path(): None, override_path(): b""}): - w = wiz(fc) - ev["fresh"] = wiz_summary(w) - ctx.check(w.get("first_run") is True and not (w.get("users") or {}).get("exists") - and w.get("advisories_complete") is False and w.get("acceptance_done") is False, - "the machine does not read as a first run: %s", wiz_summary(w)) - m = mode(fc) - ctx.check(m.get("controller") == "gated", "the controller is %s on a first run", m.get("controller")) - yield state - # the real records are back; the system accounts follow the record - rc, out = hw.initd("forgefirm-users", "reload") - ev["users_reload_rc"] = rc - ctx.log("forgefirm-users reload -> rc %s %s", rc, out.strip()[:200]) - temp = state.get("temp_account") - if temp and temp not in homes_before and temp != ev["account_before"]: - import shutil - shutil.rmtree(os.path.join(homes, temp), ignore_errors=True) - ctx.log("removed the temporary home directory of %r", temp) + try: + with Restore(ctx, FIRST_RUN_SETTINGS): + with installed(ctx, {record_path(): seed, users_path(): None, override_path(): b""}): + w = wiz(fc) + ev["fresh"] = wiz_summary(w) + ctx.check(w.get("first_run") is True and not (w.get("users") or {}).get("exists") + and w.get("advisories_complete") is False and w.get("acceptance_done") is False, + "the machine does not read as a first run: %s", wiz_summary(w)) + m = mode(fc) + ctx.check(m.get("controller") == "gated", "the controller is %s on a first run", + m.get("controller")) + yield state + finally: + # the real records are back, whether or not the block passed; the + # system accounts follow the record, and a temporary account the + # block created goes with its home + rc, out = hw.initd("forgefirm-users", "reload") + ev["users_reload_rc"] = rc + ctx.log("forgefirm-users reload -> rc %s %s", rc, out.strip()[:200]) + temp = state.get("temp_account") + if temp and temp not in homes_before and temp != ev["account_before"]: + import shutil + shutil.rmtree(os.path.join(homes, temp), ignore_errors=True) + ctx.log("removed the temporary home directory of %r", temp) after = wiz(fc) ev["after"] = wiz_summary(after) ctx.check((after.get("users") or {}).get("name") == ev["account_before"],