7.4 KiB
Plan: fix interactive cloud login retry killing the shell
Context
Interactive cloud login now verifies a candidate password by asking the cloud daemon to reconnect
with that candidate, then waiting for either trapos_cloud_connected or
trapos_cloud_unauthorized.
Observed bug:
- Run
cloud loginwith no password argument. - Enter a wrong password.
- Instead of printing
invalid password, try againand prompting again, the command exits back to the CraftOS shell. - The shell's in-memory history is gone, so up-arrow no longer recalls the previous
cloud logincommand.
The interactive branch in packages/trapos-cloud/programs/cloud.lua already looks logically
correct: on an unauthorized verdict it calls restore(), prints the retry message, and continues
the loop. Existing unit coverage also models wrong-then-right retry successfully. That points to the
live boot/runtime topology rather than the branch itself.
Root Cause Hypothesis
The short-lived verifier event loop inside cloud login can accidentally stop the boot event loop.
Relevant flow:
packages/trapos-cloud/programs/cloud.luacreates a local event loop insideverifyPassword().- When the daemon emits
trapos_cloud_unauthorized, the verifier callsel.stopLoop(). packages/trapos-core/apis/eventloop.luaimplementsstopLoop()by globally queueing a synthetic event named@libeventloop/END_OF_LOOP/<id>.- Event loop IDs currently come from a module-local counter.
- If
/apis/eventloop.luais loaded as independent module instances, both the boot event loop and the local verifier loop can use ID1. - The verifier's synthetic stop event can therefore match the boot loop's stop event name.
packages/trapos-boot/startup/boot.luaruns the shell and boot event loop withparallel.waitForAny(shellFn, eventLoopFn).- If the boot event loop exits,
parallel.waitForAnyreturns and the shell coroutine is killed. - Killing/restarting the shell explains losing shell history.
The stale hello messageId issue found during exploration is real hardening work, but it does not
directly explain shell history loss. It can cause wrong verdict attribution, not shell teardown.
Goals
- Wrong passwords in interactive
cloud loginre-prompt without leaving the command. - Stopping a short-lived local event loop must not stop
_G.bootEventLoop. - Preserve CraftOS shell history after wrong-password retry.
- Keep the fix minimal and local to the event loop semantics where possible.
- Add regression tests that reproduce the event-loop collision class.
Non-Goals
- Do not replace
cloud loginverification with a program-owned websocket. - Do not add a standalone Lua test harness.
- Do not redesign
startup/boot.luaor shell lifecycle unless the event-loop fix proves insufficient. - Do not add persisted shell history as a workaround for shell teardown.
Implementation
1. Make event loop IDs process-global
File: packages/trapos-core/apis/eventloop.lua
Replace the module-local next_eventloop_id allocation with a _G-backed counter so independently
loaded copies of the module cannot reuse the same synthetic end event name.
Sketch:
local COUNTER_KEY = '__trapos_next_eventloop_id';
local function nextEventLoopId()
local id = tonumber(_G[COUNTER_KEY]) or 1;
_G[COUNTER_KEY] = id + 1;
return id;
end
Then use:
local eventloop_id = nextEventLoopId();
This keeps the existing @libeventloop/END_OF_LOOP/<id> design but removes collisions across module
instances.
2. Avoid queued stop events when stopping from inside the same loop
File: packages/trapos-core/apis/eventloop.lua
Add a local stopRequested = false flag.
When stopLoop() is called while the loop is actively dispatching its own handler or timeout, set
stopRequested = true instead of queueing a global event. After dispatching the current event, break
the loop if stopRequested is set.
Keep queueing the synthetic end event when stopLoop() is called from another coroutine, because the
loop may be blocked inside os.pullEventRaw() and needs to be woken.
This is a second layer of protection: the verifier's finish() calls stopLoop() from inside its own
event handler, so no global end event should be needed for that path at all.
3. Add an event-loop collision regression test
File: packages/trapos-core/tests/eventloop.lua
Add a test that simulates independent module loads:
- Load
/apis/eventloop.luatwice vialoadfile()or an equivalent isolated environment. - Create one boot-like loop from the first module and one verifier-like loop from the second module.
- Run both under
parallel. - Stop the verifier loop from one of its handlers.
- Assert the boot-like loop still handles a later probe event.
- Stop the boot-like loop explicitly at the end so the test cannot hang.
The regression should fail against the current module-local ID allocation and pass after the fix.
4. Add or extend cloud program coverage
File: packages/trapos-cloud/tests/cloud-program.lua
Keep the existing wrong-then-right interactive test, because it verifies the program branch behavior.
Add a small test for prompt-time terminate handling if practical:
- Fake
read('*')raisesTerminated. - Assert
cloud loginqueueslogin-restorewhen needed and re-raisesTerminated.
This is not the primary wrong-password bug, but it closes an adjacent interactive cancellation gap.
5. Optional cloud hardening: match hello responses by messageId
File: packages/trapos-cloud/apis/libcloud.lua
Store the active hello messageId, pass frame.messageId through api.onMessage() to onHello,
and ignore hello responses that do not match the currently active hello attempt.
This prevents stale hello responses from earlier reconnects being attributed to the current
cloud login token. It should be treated as a separate hardening change if the event-loop fix is
intended to stay small.
Package Versions
If only eventloop.lua changes:
- Bump
trapos-coreinpackages/trapos-core/ccpm.json. - Mirror the same version in
packages/index.json.
If cloud program or cloud daemon behavior also changes:
- Bump
trapos-cloudinpackages/trapos-cloud/ccpm.json. - Mirror the same version in
packages/index.json.
Only bump the full trapos meta-package if the repository's release convention for this change
requires it.
Verification
Targeted iteration:
just trapos-exec 'shell.run("/programs/runtest.lua", "--pretty", "/tests/eventloop.lua", "/tests/cloud-program.lua", "/tests/cloud.lua")'
Required after Lua/package edits:
just check
just test --pretty
Manual validation on a running TrapOS computer:
- Run
cloud login --force. - Enter a known wrong password.
- Confirm it prints
invalid password, try again (ctrl+t to cancel)and prompts again. - Press up-arrow after eventually cancelling or completing, and confirm shell history still contains
cloud login --force.
Risks
- Event loop stop semantics are shared by servers and programs, so regression tests should cover both same-loop stop and cross-coroutine stop.
- If CraftOS-PC loads
require('/apis/eventloop')as a singleton in more cases than expected, the ID collision may be intermittent; the_Gcounter still makes the behavior deterministic and safer. - The optional hello
messageIdhardening will require updating existing cloud tests that currently send hello responses without message IDs.