6.2 KiB
Fix: interactive cloud login tears down the whole trapos session on a wrong password
Context
cloud login (interactive) was just rewritten to verify the password before saving
(commit 1d4789b). When testing it at the local in-game terminal, typing a wrong
password produces two bugs:
- It prints
invalid password, try again (ctrl+t to cancel)and then quits to the shell instead of re-prompting. - The CraftOS shell command history is gone (up-arrow no longer recalls
cloud login).
Why this happens (root cause — confirmed by code, not guessed)
The interactive loop itself is correct: verified three ways (logic trace, a faithful
real-eventloop+daemon simulation, and a real-CC probe on 8:trap2) that a wrong
password returns 'unauthorized', runs the re-prompt branch (which is why the user sees
the "invalid password" line), and loops back to readPassword(). So the program is not
the thing that exits — the whole trapos session is being torn down underneath it.
The teardown chain, all confirmed in the code:
- A wrong password triggers a rapid reconnect storm on the daemon:
login-verify(close live socket → open with candidate → rejected) immediately followed bylogin-restore(close → open with persisted secret), plus anyreconnectDelayretries. connect()opens the socket withhttpLike.websocketAsync(url)un-pcall'd (packages/trapos-cloud/apis/libcloud.lua:256). On rapid open/close this can throw (CC's per-computer websocket limit / "already open" races).- The shared boot event loop dispatches handlers un-
pcall'd (packages/trapos-core/apis/eventloop.lua:294—handler(table.unpack(packed))). - The boot loop runs under
parallel.waitForAny(shellFn, eventLoopFn)with no isolation (packages/trapos-boot/startup/boot.lua:80). A throw ineventLoopFnpropagates out ofwaitForAny, thestartupscript dies, and CraftOS drops to its default shell with a fresh history — i.e. "quit to shell" + "history lost". It fires right after the "invalid password" line because the throw happens while the daemon processes thelogin-restorereconnect.
The connection bouncing is harmless on a local terminal; the session crash is the bug.
Fix
Two small, complementary changes. The eventloop change is the real fix (it makes the
shared boot loop fault-isolated so no server hiccup can ever kill the session); the
connect() change removes the specific throw and keeps the daemon retrying correctly.
1. (Recommended, systemic) Isolate handler errors in the boot event loop
packages/trapos-core/apis/eventloop.lua, in runLoop (~line 293):
Wrap the handler call so a throwing handler can never escape runLoop (and thus never
crash parallel.waitForAny / the trapos session). Keep the existing api.STOP
unregister behaviour for the success case; on error, report and continue.
-- before:
local result_handler = handler(table.unpack(packed))
if result_handler == api.STOP then
api.unregister(eventName, handler)
end
-- after:
local ok, result_handler = pcall(handler, table.unpack(packed))
if not ok then
-- one bad handler must not tear down the shared boot loop / session
printError('eventloop handler error ('..eventName..'): '..tostring(result_handler))
elseif result_handler == api.STOP then
api.unregister(eventName, handler)
end
(Use whatever error sink fits the file's style — printError if available, else a no-op /
existing logger. The point is: catch, don't propagate.)
2. (Cause-specific) Make the daemon's socket-open crash-safe
packages/trapos-cloud/apis/libcloud.lua, connect() (~line 246-257):
pcall the websocketAsync call and, on failure, fall back to scheduleReconnect() so a
throw becomes a normal retry instead of a fatal error. scheduleReconnect is defined
after connect, so add a forward declaration local scheduleReconnect before connect
and drop the local on its later definition.
local function connect()
if clearReconnectTimeout then clearReconnectTimeout(); clearReconnectTimeout = nil; end
secret = secretOverride or getSecret();
state = 'connecting';
if not reconnecting then log('info', 'connecting', { url = url }); end
local ok = pcall(function() httpLike.websocketAsync(url); end)
if not ok then
log('warn', 'websocket open failed', { url = url });
scheduleReconnect();
end
end
After these, the wrong-password flow is: verify → unauthorized → print "invalid
password" → login-restore (now crash-safe) → readPassword() re-prompts. Both symptoms
are fixed: the session survives and the prompt re-appears.
Note (not the cause, but flagged by the grilling plans)
The interactive terminate branch still does error('Terminated', 0)
(packages/trapos-cloud/programs/cloud.lua:182). With the eventloop hardened this no
longer risks the session, but a clean print('login cancelled'); return; for the Ctrl+T
cancel is a reasonable follow-up (optional, out of scope for the crash fix).
Tests
packages/trapos-core/tests/eventloop.lua— add: a handler that throws does not propagate out ofrunLoop; sibling handlers and subsequent events still fire.packages/trapos-cloud/tests/cloud.lua— add (uses the existingfakes.fakeHttp()/ctx.el.fireharness, ~line 186-205): configurehttp.websocketAsynctoerror(), firetrapos_cloud_reconnect, assert no error propagates and the daemon schedules a retry rather than dying.- Re-run the existing cloud suites (
tests/cloud.lua,tests/cloud-program.lua) — the fake-eventloop program tests are unaffected.
Verification (end to end)
On a disposable/test CC computer (do not use the shared 8/10 machines — driving a
wrong-password verify bounces their live gateway connection):
- Deploy the updated
trapos-core+trapos-cloud, setcloud.url, reboot so the daemon runs. cloud login --force, type a wrong password, Enter.- Expect:
invalid password, try again (ctrl+t to cancel)and the prompt returns — the shell/session stays alive and command history is intact.
- Expect:
- Type the correct password →
logged in. - Sanity:
cloud statusstill works (daemon recovered after the verify bounce).