cc-libs/.plans/cloud-login-session-crash-fix-plan.md

130 lines
6.2 KiB
Markdown

# 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).