191 lines
7.4 KiB
Markdown
191 lines
7.4 KiB
Markdown
# Plan: fix interactive `cloud login` retry killing the shell
|
|
|
|
## Context
|
|
|
|
Interactive `cloud login` now verifies a candidate password by asking the cloud daemon to reconnect
|
|
with that candidate, then waiting for either `trapos_cloud_connected` or
|
|
`trapos_cloud_unauthorized`.
|
|
|
|
Observed bug:
|
|
|
|
- Run `cloud login` with no password argument.
|
|
- Enter a wrong password.
|
|
- Instead of printing `invalid password, try again` and prompting again, the command exits back to
|
|
the CraftOS shell.
|
|
- The shell's in-memory history is gone, so up-arrow no longer recalls the previous `cloud login`
|
|
command.
|
|
|
|
The interactive branch in `packages/trapos-cloud/programs/cloud.lua` already looks logically
|
|
correct: on an `unauthorized` verdict it calls `restore()`, prints the retry message, and continues
|
|
the loop. Existing unit coverage also models wrong-then-right retry successfully. That points to the
|
|
live boot/runtime topology rather than the branch itself.
|
|
|
|
## Root Cause Hypothesis
|
|
|
|
The short-lived verifier event loop inside `cloud login` can accidentally stop the boot event loop.
|
|
|
|
Relevant flow:
|
|
|
|
- `packages/trapos-cloud/programs/cloud.lua` creates a local event loop inside `verifyPassword()`.
|
|
- When the daemon emits `trapos_cloud_unauthorized`, the verifier calls `el.stopLoop()`.
|
|
- `packages/trapos-core/apis/eventloop.lua` implements `stopLoop()` by globally queueing a synthetic
|
|
event named `@libeventloop/END_OF_LOOP/<id>`.
|
|
- Event loop IDs currently come from a module-local counter.
|
|
- If `/apis/eventloop.lua` is loaded as independent module instances, both the boot event loop and
|
|
the local verifier loop can use ID `1`.
|
|
- The verifier's synthetic stop event can therefore match the boot loop's stop event name.
|
|
- `packages/trapos-boot/startup/boot.lua` runs the shell and boot event loop with
|
|
`parallel.waitForAny(shellFn, eventLoopFn)`.
|
|
- If the boot event loop exits, `parallel.waitForAny` returns and the shell coroutine is killed.
|
|
- Killing/restarting the shell explains losing shell history.
|
|
|
|
The stale hello `messageId` issue found during exploration is real hardening work, but it does not
|
|
directly explain shell history loss. It can cause wrong verdict attribution, not shell teardown.
|
|
|
|
## Goals
|
|
|
|
- Wrong passwords in interactive `cloud login` re-prompt without leaving the command.
|
|
- Stopping a short-lived local event loop must not stop `_G.bootEventLoop`.
|
|
- Preserve CraftOS shell history after wrong-password retry.
|
|
- Keep the fix minimal and local to the event loop semantics where possible.
|
|
- Add regression tests that reproduce the event-loop collision class.
|
|
|
|
## Non-Goals
|
|
|
|
- Do not replace `cloud login` verification with a program-owned websocket.
|
|
- Do not add a standalone Lua test harness.
|
|
- Do not redesign `startup/boot.lua` or shell lifecycle unless the event-loop fix proves insufficient.
|
|
- Do not add persisted shell history as a workaround for shell teardown.
|
|
|
|
## Implementation
|
|
|
|
### 1. Make event loop IDs process-global
|
|
|
|
File: `packages/trapos-core/apis/eventloop.lua`
|
|
|
|
Replace the module-local `next_eventloop_id` allocation with a `_G`-backed counter so independently
|
|
loaded copies of the module cannot reuse the same synthetic end event name.
|
|
|
|
Sketch:
|
|
|
|
```lua
|
|
local COUNTER_KEY = '__trapos_next_eventloop_id';
|
|
|
|
local function nextEventLoopId()
|
|
local id = tonumber(_G[COUNTER_KEY]) or 1;
|
|
_G[COUNTER_KEY] = id + 1;
|
|
return id;
|
|
end
|
|
```
|
|
|
|
Then use:
|
|
|
|
```lua
|
|
local eventloop_id = nextEventLoopId();
|
|
```
|
|
|
|
This keeps the existing `@libeventloop/END_OF_LOOP/<id>` design but removes collisions across module
|
|
instances.
|
|
|
|
### 2. Avoid queued stop events when stopping from inside the same loop
|
|
|
|
File: `packages/trapos-core/apis/eventloop.lua`
|
|
|
|
Add a local `stopRequested = false` flag.
|
|
|
|
When `stopLoop()` is called while the loop is actively dispatching its own handler or timeout, set
|
|
`stopRequested = true` instead of queueing a global event. After dispatching the current event, break
|
|
the loop if `stopRequested` is set.
|
|
|
|
Keep queueing the synthetic end event when `stopLoop()` is called from another coroutine, because the
|
|
loop may be blocked inside `os.pullEventRaw()` and needs to be woken.
|
|
|
|
This is a second layer of protection: the verifier's `finish()` calls `stopLoop()` from inside its own
|
|
event handler, so no global end event should be needed for that path at all.
|
|
|
|
### 3. Add an event-loop collision regression test
|
|
|
|
File: `packages/trapos-core/tests/eventloop.lua`
|
|
|
|
Add a test that simulates independent module loads:
|
|
|
|
- Load `/apis/eventloop.lua` twice via `loadfile()` or an equivalent isolated environment.
|
|
- Create one boot-like loop from the first module and one verifier-like loop from the second module.
|
|
- Run both under `parallel`.
|
|
- Stop the verifier loop from one of its handlers.
|
|
- Assert the boot-like loop still handles a later probe event.
|
|
- Stop the boot-like loop explicitly at the end so the test cannot hang.
|
|
|
|
The regression should fail against the current module-local ID allocation and pass after the fix.
|
|
|
|
### 4. Add or extend cloud program coverage
|
|
|
|
File: `packages/trapos-cloud/tests/cloud-program.lua`
|
|
|
|
Keep the existing wrong-then-right interactive test, because it verifies the program branch behavior.
|
|
|
|
Add a small test for prompt-time terminate handling if practical:
|
|
|
|
- Fake `read('*')` raises `Terminated`.
|
|
- Assert `cloud login` queues `login-restore` when needed and re-raises `Terminated`.
|
|
|
|
This is not the primary wrong-password bug, but it closes an adjacent interactive cancellation gap.
|
|
|
|
### 5. Optional cloud hardening: match hello responses by `messageId`
|
|
|
|
File: `packages/trapos-cloud/apis/libcloud.lua`
|
|
|
|
Store the active hello `messageId`, pass `frame.messageId` through `api.onMessage()` to `onHello`,
|
|
and ignore hello responses that do not match the currently active hello attempt.
|
|
|
|
This prevents stale hello responses from earlier reconnects being attributed to the current
|
|
`cloud login` token. It should be treated as a separate hardening change if the event-loop fix is
|
|
intended to stay small.
|
|
|
|
## Package Versions
|
|
|
|
If only `eventloop.lua` changes:
|
|
|
|
- Bump `trapos-core` in `packages/trapos-core/ccpm.json`.
|
|
- Mirror the same version in `packages/index.json`.
|
|
|
|
If cloud program or cloud daemon behavior also changes:
|
|
|
|
- Bump `trapos-cloud` in `packages/trapos-cloud/ccpm.json`.
|
|
- Mirror the same version in `packages/index.json`.
|
|
|
|
Only bump the full `trapos` meta-package if the repository's release convention for this change
|
|
requires it.
|
|
|
|
## Verification
|
|
|
|
Targeted iteration:
|
|
|
|
```sh
|
|
just trapos-exec 'shell.run("/programs/runtest.lua", "--pretty", "/tests/eventloop.lua", "/tests/cloud-program.lua", "/tests/cloud.lua")'
|
|
```
|
|
|
|
Required after Lua/package edits:
|
|
|
|
```sh
|
|
just check
|
|
just test --pretty
|
|
```
|
|
|
|
Manual validation on a running TrapOS computer:
|
|
|
|
- Run `cloud login --force`.
|
|
- Enter a known wrong password.
|
|
- Confirm it prints `invalid password, try again (ctrl+t to cancel)` and prompts again.
|
|
- Press up-arrow after eventually cancelling or completing, and confirm shell history still contains
|
|
`cloud login --force`.
|
|
|
|
## Risks
|
|
|
|
- Event loop stop semantics are shared by servers and programs, so regression tests should cover both
|
|
same-loop stop and cross-coroutine stop.
|
|
- If CraftOS-PC loads `require('/apis/eventloop')` as a singleton in more cases than expected, the ID
|
|
collision may be intermittent; the `_G` counter still makes the behavior deterministic and safer.
|
|
- The optional hello `messageId` hardening will require updating existing cloud tests that currently
|
|
send hello responses without message IDs.
|