Skip to content
Closed
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension


Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
14 changes: 14 additions & 0 deletions CHANGELOG.md
Original file line number Diff line number Diff line change
Expand Up @@ -13,6 +13,20 @@ hashes are given where a change is easiest to understand by reading the commit.

## October 2026

### Monitor and CI

- **`machine-start` is idempotent and acknowledges the transition, and
`machine-status` reports guest state and monitor readiness.** The monitor
command now prints `idle -> starting -> running` (or `running (no change)`),
`machine-stop` acknowledges symmetrically, and `machine-status [json]` gives
a caller one place to ask whether the guest is `idle`, `starting`, `running`
or `halted` and whether the monitor has bound its port. The CI socket gains
the same `status` command, `iris-ci status [--wait-ready SECS]` polls it
instead of sleeping, and `--ci` creates the control socket only after
`Machine::start` has bound the monitor, so a successful connection is
itself a readiness signal. Tests cover a command sent before the monitor is
ready, duplicate starts, start after halt/reset, and invalid transitions.

### NAT gateway

- **Gateway replies no longer wait for a poll tick.** A reply to a guest
Expand Down
6 changes: 6 additions & 0 deletions Cargo.toml
Original file line number Diff line number Diff line change
Expand Up @@ -343,6 +343,12 @@ windows-sys = { version = "0.61", features = ["Win32_Foundation", "Win32_System_
[target.'cfg(not(windows))'.dependencies]
libc = "0.2"

[dev-dependencies]
# Integration tests (`tests/`) are separate crates and do not inherit the
# package's normal dependencies; serde_json is needed to parse the monitor's
# and CI socket's JSON replies.
serde_json = "1.0"

[profile.release]
lto = "fat"
codegen-units = 1
Expand Down
19 changes: 19 additions & 0 deletions STORAGE.md
Original file line number Diff line number Diff line change
Expand Up @@ -116,6 +116,7 @@ debugging the wrapper itself.
./target/release/iris --ci

# In another terminal: drive it
./target/release/iris-ci status --wait-ready 60 # guest state + monitor readiness
./target/release/iris-ci boot # PROM menu → IRIS console login (one cmd)
./target/release/iris-ci login # send root + dismiss vt100 prompt + wait #
./target/release/iris-ci run 'ls /' # send shell command, get stdout + exit code
Expand All @@ -134,6 +135,24 @@ Run `iris-ci --help` for the full list, or `iris-ci <subcmd> --help` for any
subcommand. Every operation has a typed clap arg — no JSON quoting, no
hand-managed timeouts.

### Waiting for a fresh machine

The control socket is created only after `Machine::start` has bound the monitor,
so a successful connection (or `iris-ci ping`) also means the monitor is
listening. A harness that launches iris should poll, not sleep:
`iris-ci status --wait-ready 60` retries until `monitor_ready` is true and then
prints the guest state (`idle`, `starting`, `running`, `halted`) and the
monitor address. `--json` emits the same fields as one object. A `--ci` machine
sits in `starting` with the CPU parked until `iris-ci start` (or the monitor's
`machine-start`), whose reply acknowledges the transition; repeating it is a
no-op. See [debug.md](debug.md#machine) for `machine-status` on the monitor
side.

`script FILE` (also spelled `batch FILE`) runs one command per line through a
single control-socket connection, so a multi-step scenario costs one connect
rather than one per line. It prints per-step status and stops at the first
failure; `--keep-going` runs the remaining steps and still exits non-zero.

For automation that doesn't want to depend on `iris-ci`, the underlying socket
protocol is newline-delimited JSON; `cmd` and `args` per request, `{ok, data,
error}` per response. See `src/ci.rs` for the dispatch table.
Expand Down
29 changes: 28 additions & 1 deletion debug.md
Original file line number Diff line number Diff line change
Expand Up @@ -81,11 +81,38 @@ accepted in every build but do nothing without the feature.

| Command | Description |
|---|---|
| `machine-start` / `machine-stop` | Start / stop the CPU and all peripherals |
| `machine-start` | Start the CPU and all peripherals. Idempotent: prints the transition it performed, e.g. `machine-start: halted -> starting -> running`, or `machine-start: running (no change)` |
| `machine-stop` | Stop the CPU and all peripherals. Idempotent: prints `machine-stop: running -> halted` |
| `machine-status [json]` | Guest state (`idle`, `starting`, `running`, `halted`) and whether the monitor is accepting commands; `json` emits one object |
| `reset` | Reset all hardware to power-on state |
| `save <name>` / `load <name>` | Save / load a snapshot under `saves/<name>/` (see [STORAGE.md](STORAGE.md#snapshots-and-rollback)) |
| `testdev` | Bare-metal test device status (`cpu-tests/`, `bench/`) |

The machine lifecycle is one value, and every surface reports it the same way:
`idle` (not started; the monitor is not listening), `starting` (the monitor
accepts commands but the CPU is parked), `running` (the CPU is executing), and
`halted` (stopped by `machine-stop`, `reset` or a guest power-off). `running` is
read from the CPU's own running flag, so the `start`/`stop` execution commands
and breakpoints are reflected without a separate bookkeeping step.

A fresh `--ci` machine is **ready for commands while still `starting`**: the
monitor is up and the peripherals are running, but the CPU stays parked until
`machine-start` (or `iris-ci start`). Do not sleep before sending it — poll
instead. Either retry the monitor TCP connect until it answers, or use the CI
socket, which iris creates only after the monitor has bound:

```sh
iris-ci --socket /tmp/iris.sock status --wait-ready 60
tools/mon.py "machine-start"
```

`machine-status` is available as soon as a monitor connection is accepted; the
`json` form is one object with `state`, `cpu_running`, `monitor_ready` and
`monitor_addr`. `iris-ci status` prints the same fields from `Machine::status`.
If the monitor could not bind its port (another iris is using it), the CI
socket still answers but reports `monitor_ready` false, so a harness can tell a
bind failure from a guest that never started.

### Execution control

| Command | Description |
Expand Down
19 changes: 17 additions & 2 deletions src/ci.rs
Original file line number Diff line number Diff line change
Expand Up @@ -257,6 +257,7 @@ fn dispatch(server: &CiServer, req: &Request) -> Response {
"ping" => Response::ok(),
"quit" => cmd_quit(server, &req.args),
"start" => cmd_start(server),
"status" => cmd_status(server),
"save" => cmd_save(server, &req.args),
"restore" => cmd_restore(server, &req.args),
"rollback" => cmd_rollback(server),
Expand Down Expand Up @@ -396,8 +397,22 @@ fn cmd_quit(server: &CiServer, args: &Value) -> Response {
}

fn cmd_start(server: &CiServer) -> Response {
match server.with_machine(|m| m.cpu_start()) {
Ok(()) => Response::ok(),
// Same path as the monitor's `machine-start`: idempotent, and it brings
// peripherals back before releasing the CPU after a stop. A machine that
// has not finished `Machine::start` reports `idle` and the harness should
// poll `status` first; the CI socket is created after that startup step,
// so a connected client normally cannot see `idle`.
match server.with_machine_result(|m| m.start_guest()) {
Ok(_) => Response::ok(),
Err(e) => Response::err(e),
}
}

/// Guest state and monitor readiness, from the same `Machine::status` the
/// monitor's `machine-status` prints.
fn cmd_status(server: &CiServer) -> Response {
match server.with_machine(|m| m.status()) {
Ok(status) => Response::data(status.to_json()),
Err(e) => Response::err(e),
}
}
Expand Down
71 changes: 70 additions & 1 deletion src/iris_ci_main.rs
Original file line number Diff line number Diff line change
Expand Up @@ -63,7 +63,15 @@ struct Cli {
enum Cmd {
/// Liveness check — returns "ok" if the socket is reachable.
Ping,
/// Start the CPU thread (no-op if already running).
/// Show the guest lifecycle state and whether the monitor is accepting
/// commands. `--wait-ready SECS` polls until it is.
Status {
/// Poll until the monitor has bound its port, up to SECS seconds
/// (0 = one query). Exits non-zero on timeout.
#[arg(long, default_value_t = 0, value_name = "SECS")]
wait_ready: u64,
},
/// Start the CPU and peripherals (no-op if already running).
Start,
/// Cleanly shut down iris. `--sync-chd [ID|all]` folds pending CHD diffs
/// back into their bases first (default: all disks).
Expand Down Expand Up @@ -268,6 +276,7 @@ fn main() {
fn dispatch(opts: &Opts, cmd: Cmd) -> Result<()> {
match cmd {
Cmd::Ping => simple(opts, "ping", json!({}), "ok"),
Cmd::Status { wait_ready } => cmd_status(opts, wait_ready),
Cmd::Start => simple(opts, "start", json!({}), "started"),
Cmd::Quit { sync_chd } => match sync_chd {
// A fold recompresses the whole base, so it can far outrun the 300s default.
Expand Down Expand Up @@ -475,6 +484,66 @@ fn print_response(opts: &Opts, ok_msg: &str, data: &Value) {
}
}

/// `iris-ci status`: guest lifecycle state and monitor readiness from the same
/// `Machine::status` the monitor's `machine-status` prints. With
/// `--wait-ready`, keep polling (including through "socket not up yet") until
/// the monitor has bound its port, then stop — a readiness poll a harness can
/// use instead of a fixed sleep.
fn cmd_status(opts: &Opts, wait_ready: u64) -> Result<()> {
let deadline = Instant::now() + Duration::from_secs(wait_ready);
loop {
match send(opts, "status", json!({})) {
Ok(data) => {
let ready = data.get("monitor_ready").and_then(Value::as_bool).unwrap_or(false);
if ready {
print_status(opts, &data);
return Ok(());
}
if wait_ready == 0 {
// A one-shot query reports whatever the machine says; the
// caller decides whether "not ready yet" matters.
print_status(opts, &data);
return Ok(());
}
if Instant::now() >= deadline {
print_status(opts, &data);
let state = data.get("state").and_then(Value::as_str).unwrap_or("unknown");
return Err(Error::Local(format!(
"monitor not ready after {wait_ready}s (state {state}); see iris stderr for a bind failure"
)));
}
}
Err(Error::Connection(_)) if wait_ready > 0 && Instant::now() < deadline => {
// The CI socket itself may not exist yet: iris creates it only
// after the monitor has bound.
}
Err(e) => return Err(e),
}
std::thread::sleep(Duration::from_millis(50));
}
}

fn print_status(opts: &Opts, data: &Value) {
if opts.json {
println!("{}", serde_json::to_string_pretty(data).unwrap_or_else(|_| data.to_string()));
return;
}
if opts.quiet { return; }
let state = data.get("state").and_then(Value::as_str).unwrap_or("unknown");
let cpu = if data.get("cpu_running").and_then(Value::as_bool).unwrap_or(false) {
"running"
} else {
"stopped"
};
let monitor = match data.get("monitor_addr").and_then(Value::as_str) {
Some(addr) => format!("ready ({addr})"),
None => "not ready".to_string(),
};
println!("state {state}");
println!("cpu {cpu}");
println!("monitor {monitor}");
}

fn cmd_list(opts: &Opts) -> Result<()> {
let data = send(opts, "list", json!({}))?;
if opts.json { println!("{}", serde_json::to_string_pretty(&data).unwrap_or_default()); return Ok(()); }
Expand Down
1 change: 1 addition & 0 deletions src/lib.rs
Original file line number Diff line number Diff line change
Expand Up @@ -170,6 +170,7 @@ pub mod prombini2;
pub mod prombinip28;
pub mod ppmem;
pub mod machine;
pub mod machine_state;
pub mod platform;
pub mod physical;
pub mod monitor;
Expand Down
Loading
Loading