7.6 KiB
Merged Plan: fix interactive cloud login session teardown
Goal
Fix the user-visible bug where interactive cloud login with a wrong password prints the retry
message, then drops back to a fresh CraftOS shell with lost in-memory command history instead of
prompting again.
This should be handled as one end-to-end fix plan, split into small independently testable changes.
Current Assessment
The interactive cloud login branch itself is already covered and appears logically correct:
- wrong password returns an unauthorized verdict;
- the program queues
login-restore; - it prints
invalid password, try again (ctrl+t to cancel); - it loops back to
readPassword().
The observed shell/history loss points to the surrounding runtime being torn down, not to the login loop simply returning.
There are two plausible and concrete runtime failure mechanisms in the current code. Either one can explain the same user-visible symptom, and both are worth fixing because both affect boot-session stability.
Bugs / Fix Areas
1. Event-loop stop event collision
packages/trapos-core/apis/eventloop.lua allocates loop IDs from a module-local counter and stops a
running loop by queueing a synthetic global event named @libeventloop/END_OF_LOOP/<id>.
If /apis/eventloop.lua is loaded as independent module instances, two loops can both use ID 1.
The short-lived verifier loop created by cloud login can then queue an end event that also matches
the boot event loop. Since startup/boot.lua runs shell + boot loop under parallel.waitForAny, a
boot-loop exit kills the shell coroutine and loses shell history.
Fix:
- Allocate event loop IDs from a
_G-backed process-global counter. - Avoid queueing a global synthetic stop event when
stopLoop()is called from inside the same event loop's handler or timeout. Use an internalstopRequestedflag and break after the current dispatch. - Keep queued synthetic stop events for external stop calls, where the loop may need to wake from
os.pullEventRaw().
2. Boot event-loop handler errors can tear down the session
eventloop.lua currently dispatches handlers directly. A thrown handler escapes runLoop(). The
boot event loop is run directly inside parallel.waitForAny(shellFn, eventLoopFn), so an unhandled
server/daemon error can terminate the TrapOS startup session and drop the user into a fresh shell.
Fix:
- Add protected handler dispatch as an explicit event-loop option, for example
createEventLoop({ onError = function(eventName, err) ... end }). - Use protected dispatch for
_G.bootEventLoopinpackages/trapos-boot/startup/boot.lua. - Keep the default unprotected for short-lived program-local loops so development/test failures still fail loudly unless explicitly isolated.
3. Cloud daemon websocket open can throw during reconnect
packages/trapos-cloud/apis/libcloud.lua calls httpLike.websocketAsync(url) without pcall.
During wrong-password verification the daemon rapidly reconnects with the candidate secret, receives
an unauthorized response, restores the persisted secret, and reconnects again. If websocketAsync
throws during that churn, the error can currently escape through the boot event loop.
Fix:
- Forward-declare
scheduleReconnectsoconnect()can call it. - Wrap
httpLike.websocketAsync(url)inpcall. - On failure, log a warning and schedule a normal reconnect instead of throwing.
Out Of Scope For This Fix
Stale hello messageId matching
Matching hello responses by the active hello messageId is useful cloud-login hardening. It can
prevent stale hello responses from an older reconnect from being attributed to the current candidate.
However, this does not directly explain shell/session teardown. Treat it as a follow-up unless the main fixes still leave wrong verdict attribution in manual or probe testing.
Persisted shell history
Do not add persisted shell history as a workaround. The correct fix is to stop tearing down the TrapOS session.
Implementation Steps
Step 1: Add regression coverage
packages/trapos-core/tests/eventloop.lua:
- Add a test that simulates independent module loads of
/apis/eventloop.lua. - Create one boot-like loop and one verifier-like loop from separate module instances.
- Stop the verifier-like loop from inside its handler.
- Assert the boot-like loop still handles a later probe event.
- Explicitly stop the boot-like loop at the end so the test cannot hang.
packages/trapos-core/tests/eventloop.lua:
- Add a protected-dispatch test where one handler throws but a sibling or later handler/event still runs.
- Assert the configured error sink receives the error.
packages/trapos-cloud/tests/cloud.lua:
- Add a daemon test where
http.websocketAsyncthrows duringstartSession()or a reconnect. - Assert the error does not propagate.
- Assert a reconnect timer is scheduled.
Keep existing packages/trapos-cloud/tests/cloud-program.lua retry tests. They already prove the
program-level wrong-then-right interactive branch.
Step 2: Fix eventloop.lua
Change packages/trapos-core/apis/eventloop.lua:
- Replace module-local event-loop ID allocation with a
_Gcounter. - Add same-loop
stopRequestedhandling. - Add optional protected dispatch support for handlers and timeout callbacks if practical. At minimum, protect regular event handlers used by boot servers.
- Preserve existing
api.STOPunregister behavior on successful handler return. - Preserve existing error contracts for duplicate registration and stopping an already stopped loop.
Step 3: Use protected boot event loop
Change packages/trapos-boot/startup/boot.lua:
- Construct
_G.bootEventLoopwith an error handler. - Error handler should report the failure using
printErrorif available, otherwiseprint. - The boot loop should continue after a server handler error.
Step 4: Harden cloud reconnect
Change packages/trapos-cloud/apis/libcloud.lua:
- Forward-declare
scheduleReconnectbeforeconnect(). - Wrap
httpLike.websocketAsync(url)withpcall. - On failure, log
websocket open failedand callscheduleReconnect(). - Avoid duplicate noisy logs while already in reconnect-loop mode.
Step 5: Package versions
Bump owning package versions and mirror them in packages/index.json:
trapos-coreforapis/eventloop.lua.trapos-bootforstartup/boot.lua.trapos-cloudforapis/libcloud.lua.
Do not bump the full trapos meta-package unless repository release convention requires it for this
change set.
Verification
Targeted iteration:
just trapos-exec 'shell.run("/programs/runtest.lua", "--pretty", "/tests/eventloop.lua", "/tests/cloud.lua", "/tests/cloud-program.lua")'
Required after Lua/package edits:
just check
just test --pretty
Manual validation on a disposable TrapOS computer:
- Deploy updated
trapos-core,trapos-boot, andtrapos-cloud. - Reboot so the cloud daemon and boot loop use the updated code.
- Run
cloud login --force. - Enter a known wrong password.
- Confirm the retry message is printed and the password prompt returns.
- Cancel or complete the login, then use up-arrow to confirm shell history still contains the command.
- Enter the correct password and confirm
logged in. - Run
cloud statusto confirm the daemon recovered normally.
Recommended Split
Keep this as one merged fix plan, but implement it in three reviewable chunks:
- Event-loop stop collision and same-loop stop semantics.
- Boot-loop protected dispatch.
- Cloud reconnect crash-safety.
This split makes each failure mode testable while still fixing the single user-facing bug end to end.