# 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: 1. It prints `invalid password, try again (ctrl+t to cancel)` and then **quits to the shell instead of re-prompting**. 2. 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 by `login-restore` (close → open with persisted secret), plus any `reconnectDelay` retries. - `connect()` opens the socket with `httpLike.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 in `eventLoopFn` propagates out of `waitForAny`, the `startup` script 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 the `login-restore` reconnect. 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. ```lua -- 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. ```lua 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 of `runLoop`; sibling handlers and subsequent events still fire. - **`packages/trapos-cloud/tests/cloud.lua`** — add (uses the existing `fakes.fakeHttp()` / `ctx.el.fire` harness, ~line 186-205): configure `http.websocketAsync` to `error()`, fire `trapos_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): 1. Deploy the updated `trapos-core` + `trapos-cloud`, set `cloud.url`, reboot so the daemon runs. 2. `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. 3. Type the correct password → `logged in`. 4. Sanity: `cloud status` still works (daemon recovered after the verify bounce).