130 lines
6.2 KiB
Markdown
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).
|