From cccd360d1506cf261864c2b559df381165cfa14d Mon Sep 17 00:00:00 2001 From: "Rowan (OpenClaw)" Date: Fri, 20 Feb 2026 02:26:34 +0100 Subject: [PATCH 01/10] Add theseus-qemu Rust runner (profiles + optional serial/QMP) --- Cargo.toml | 4 +- tools/theseus-qemu/Cargo.toml | 10 + tools/theseus-qemu/src/main.rs | 331 +++++++++++++++++++++++++++++++++ 3 files changed, 343 insertions(+), 2 deletions(-) create mode 100644 tools/theseus-qemu/Cargo.toml create mode 100644 tools/theseus-qemu/src/main.rs diff --git a/Cargo.toml b/Cargo.toml index ccd5a56..8c66b26 100644 --- a/Cargo.toml +++ b/Cargo.toml @@ -1,5 +1,5 @@ [workspace] -members = ["bootloader", "kernel", "shared"] +members = ["bootloader", "kernel", "shared", "tools/theseus-qemu"] resolver = "2" [workspace.dependencies] @@ -20,4 +20,4 @@ panic = "abort" opt-level = "z" lto = true codegen-units = 1 -panic = "abort" \ No newline at end of file +panic = "abort" diff --git a/tools/theseus-qemu/Cargo.toml b/tools/theseus-qemu/Cargo.toml new file mode 100644 index 0000000..a847f61 --- /dev/null +++ b/tools/theseus-qemu/Cargo.toml @@ -0,0 +1,10 @@ +[package] +name = "theseus-qemu" +version = "0.1.0" +edition = "2024" + +[dependencies] +anyhow = "1.0.101" +clap = { version = "4", features = ["derive"] } +serde = { version = "1", features = ["derive"] } +serde_json = "1" diff --git a/tools/theseus-qemu/src/main.rs b/tools/theseus-qemu/src/main.rs new file mode 100644 index 0000000..ca69412 --- /dev/null +++ b/tools/theseus-qemu/src/main.rs @@ -0,0 +1,331 @@ +use std::{ + path::{Path, PathBuf}, + process::{Command, ExitStatus}, +}; + +use anyhow::{Context, Result, anyhow, bail}; +use clap::{Parser, ValueEnum}; +use serde::{Deserialize, Serialize}; + +#[derive(Parser, Debug)] +#[command( + name = "theseus-qemu", + version, + about = "Build and run TheseusOS under QEMU with profiles" +)] +pub struct Cli { + #[command(subcommand)] + pub cmd: Cmd, +} + +#[derive(clap::Subcommand, Debug)] +pub enum Cmd { + /// Print the resolved QEMU argv (no execution) + Print(Args), + /// Run QEMU + Run(Args), + /// Emit a JSON artifact containing the resolved configuration + argv + Artifact(ArtifactArgs), +} + +#[derive(Parser, Debug, Clone)] +pub struct ArtifactArgs { + #[command(flatten)] + pub args: Args, + + /// Where to write the JSON artifact + #[arg(long, default_value = "build/qemu-argv.json")] + pub out: PathBuf, +} + +#[derive(Parser, Debug, Clone)] +pub struct Args { + /// Profile (selects device set and defaults) + #[arg(long, default_value = "default")] + pub profile: Profile, + + /// Headless mode (no display) + #[arg(long)] + pub headless: bool, + + /// Add a timeout wrapper (seconds). Only applies to `run`. + #[arg(long, default_value_t = 0)] + pub timeout_secs: u64, + + /// QEMU accelerator (e.g. kvm:tcg, tcg) + #[arg(long, default_value = "kvm:tcg")] + pub accel: String, + + /// QEMU irqchip mode (off|on|split) + #[arg(long, default_value = "split")] + pub irqchip: String, + + /// Enable QMP socket. If omitted, disabled. + #[arg(long)] + pub qmp: Option, + + /// Enable HMP monitor socket. If omitted, disabled. + #[arg(long)] + pub hmp: Option, + + /// Serial mode. + #[arg(long, value_enum, default_value_t = SerialMode::Off)] + pub serial: SerialMode, + + /// Serial endpoint for unix mode (e.g. /tmp/theseus-serial.sock) + #[arg(long, default_value = "/tmp/theseus-serial.sock")] + pub serial_path: String, + + /// Extra raw QEMU args appended verbatim + #[arg(long)] + pub extra: Vec, +} + +#[derive(ValueEnum, Debug, Clone, Copy, PartialEq, Eq)] +pub enum SerialMode { + /// Do not expose a serial device (use debugcon instead) + Off, + /// Use stdio for serial + Stdio, + /// Expose a unix socket at --serial-path + Unix, +} + +#[derive(ValueEnum, Debug, Clone, Copy, PartialEq, Eq, Serialize, Deserialize)] +pub enum Profile { + /// Mirrors the current startQemu.sh defaults (q35 + nvme + xhci + virtio-gpu + virtio-net) + Default, + /// Minimal headless, no USB/GPU/NIC. Useful for bring-up. + Min, + /// USB keyboard profile (xhci + usb-kbd) + UsbKbd, +} + +#[derive(Debug, Serialize)] +pub struct Artifact { + pub profile: Profile, + pub headless: bool, + pub argv: Vec, + pub cwd: PathBuf, +} + +fn main() -> Result<()> { + let cli = Cli::parse(); + + match cli.cmd { + Cmd::Print(args) => { + let argv = build_qemu_argv(&args)?; + println!("{}", shell_join(&argv)); + } + Cmd::Run(args) => { + let argv = build_qemu_argv(&args)?; + let status = run_qemu(&args, &argv)?; + // forward exit code + std::process::exit(status.code().unwrap_or(1)); + } + Cmd::Artifact(artifact) => { + let argv = build_qemu_argv(&artifact.args)?; + let cwd = std::env::current_dir()?; + let doc = Artifact { + profile: artifact.args.profile, + headless: artifact.args.headless, + argv: argv.clone(), + cwd, + }; + if let Some(parent) = artifact.out.parent() { + std::fs::create_dir_all(parent).ok(); + } + std::fs::write(&artifact.out, serde_json::to_string_pretty(&doc)?) + .with_context(|| format!("write artifact {}", artifact.out.display()))?; + println!("Wrote {}", artifact.out.display()); + } + } + + Ok(()) +} + +fn repo_root() -> Result { + // We live in tools/theseus-qemu; repo root is two parents up. + let exe_dir = std::env::current_dir()?; + // We prefer locating by walking upward until we find Cargo.toml with a workspace. + for ancestor in exe_dir.ancestors() { + let p = ancestor.join("Cargo.toml"); + if p.is_file() { + let txt = std::fs::read_to_string(&p).unwrap_or_default(); + if txt.contains("[workspace]") { + return Ok(ancestor.to_path_buf()); + } + } + } + Err(anyhow!( + "could not locate repo root (workspace Cargo.toml not found)" + )) +} + +fn build_qemu_argv(args: &Args) -> Result> { + let root = repo_root()?; + + // Paths matching startQemu.sh + let ovmf_code = root.join("OVMF/OVMF_CODE.fd"); + let ovmf_vars = root.join("build/OVMF_VARS.fd"); + let disk_img = root.join("build/disk.img"); + + // We intentionally do NOT auto-build here (runner should be usable even when build is blocked). + ensure_exists(&ovmf_code, "OVMF code")?; + ensure_exists(&ovmf_vars, "OVMF vars")?; + ensure_exists(&disk_img, "disk image")?; + + let mut argv: Vec = vec!["qemu-system-x86_64".into()]; + argv.extend([ + "-machine".into(), + format!("q35,accel={},kernel-irqchip={}", args.accel, args.irqchip), + "-cpu".into(), + "max".into(), + "-smp".into(), + "4".into(), + "-m".into(), + "2G".into(), + ]); + + // Firmware + argv.extend([ + "-drive".into(), + format!( + "if=pflash,format=raw,readonly=on,file={}", + ovmf_code.display() + ), + "-drive".into(), + format!("if=pflash,format=raw,file={}", ovmf_vars.display()), + ]); + + // Deterministic exit and debugcon + argv.extend([ + "-device".into(), + "isa-debug-exit,iobase=0xf4,iosize=0x04".into(), + "-device".into(), + "isa-debugcon,chardev=debugcon".into(), + ]); + + if args.headless { + argv.extend(["-display".into(), "none".into()]); + argv.extend(["-chardev".into(), "stdio,id=debugcon".into()]); + argv.extend(["-d".into(), "int,guest_errors,cpu_reset".into()]); + } else { + argv.extend(["-chardev".into(), "file,id=debugcon,path=debug.log".into()]); + // default: provide a monitor on stdio if headed. + argv.extend(["-monitor".into(), "stdio".into()]); + } + + // Optional serial + match args.serial { + SerialMode::Off => {} + SerialMode::Stdio => { + argv.extend(["-serial".into(), "stdio".into()]); + } + SerialMode::Unix => { + argv.extend([ + "-serial".into(), + format!("unix:{},server,nowait", args.serial_path), + ]); + } + } + + // Optional QMP/HMP + if let Some(qmp) = &args.qmp { + argv.extend(["-qmp".into(), format!("unix:{},server,nowait", qmp)]); + } + if let Some(hmp) = &args.hmp { + argv.extend(["-monitor".into(), format!("unix:{},server,nowait", hmp)]); + } + + // Storage: match existing NVMe setup + argv.extend([ + "-drive".into(), + format!("if=none,id=nvme0,file={},format=raw", disk_img.display()), + "-device".into(), + "nvme,drive=nvme0,serial=deadbeef".into(), + ]); + + // PCIe root ports + argv.extend([ + "-device".into(), + "pcie-root-port,id=rp0,slot=0,chassis=1".into(), + "-device".into(), + "pcie-root-port,id=rp1,slot=1,chassis=2".into(), + "-device".into(), + "pcie-root-port,id=rp2,slot=2,chassis=3".into(), + ]); + + // Apply profile-specific devices + match args.profile { + Profile::Default => { + argv.extend(["-device".into(), "virtio-gpu-pci,bus=rp0".into()]); + argv.extend(["-device".into(), "qemu-xhci,id=xhci0".into()]); + argv.extend(["-device".into(), "usb-kbd,bus=xhci0.0".into()]); + argv.extend(["-device".into(), "usb-mouse,bus=xhci0.0".into()]); + argv.extend([ + "-device".into(), + "virtio-net-pci,id=nic0,bus=rp2".into(), + "-nic".into(), + "none".into(), + ]); + } + Profile::Min => { + // no gpu/usb/nic + argv.extend(["-nic".into(), "none".into()]); + } + Profile::UsbKbd => { + argv.extend(["-device".into(), "qemu-xhci,id=xhci0".into()]); + argv.extend(["-device".into(), "usb-kbd,bus=xhci0.0".into()]); + argv.extend(["-nic".into(), "none".into()]); + } + } + + argv.push("-no-reboot".into()); + + // Extra args + argv.extend(args.extra.iter().cloned()); + + Ok(argv) +} + +fn run_qemu(args: &Args, argv: &[String]) -> Result { + let mut cmd = Command::new(&argv[0]); + cmd.args(&argv[1..]); + + if args.timeout_secs > 0 { + // crude timeout wrapper (Linux). If you want portable, we can add a Rust timer. + let mut t = Command::new("timeout"); + t.arg("--foreground") + .arg(format!("{}s", args.timeout_secs)) + .arg(&argv[0]) + .args(&argv[1..]); + return t.status().context("run qemu (timeout)"); + } + + cmd.status().context("run qemu") +} + +fn ensure_exists(path: &Path, what: &str) -> Result<()> { + if !path.exists() { + bail!( + "Missing {} at {}. Build first (e.g. `make all`).", + what, + path.display() + ); + } + Ok(()) +} + +fn shell_join(argv: &[String]) -> String { + argv.iter().map(shell_escape).collect::>().join(" ") +} + +fn shell_escape(s: &String) -> String { + if s.chars() + .all(|c| c.is_ascii_alphanumeric() || "-._/:,=+".contains(c)) + { + return s.clone(); + } + format!("'{}'", s.replace('\\', "\\\\").replace('\'', "'\\''")) +} From 89c7107e97f20ff8ab07b9fc44df394b926e202f Mon Sep 17 00:00:00 2001 From: "Rowan (OpenClaw)" Date: Fri, 20 Feb 2026 02:37:36 +0100 Subject: [PATCH 02/10] theseus-qemu: default run mode + --dry to print without artifacts --- tools/theseus-qemu/src/main.rs | 54 +++++++++++++++++++++------------- 1 file changed, 33 insertions(+), 21 deletions(-) diff --git a/tools/theseus-qemu/src/main.rs b/tools/theseus-qemu/src/main.rs index ca69412..31269e5 100644 --- a/tools/theseus-qemu/src/main.rs +++ b/tools/theseus-qemu/src/main.rs @@ -14,17 +14,19 @@ use serde::{Deserialize, Serialize}; about = "Build and run TheseusOS under QEMU with profiles" )] pub struct Cli { + /// Default action: run QEMU with the provided args. + #[command(flatten)] + pub run: Args, + #[command(subcommand)] - pub cmd: Cmd, + pub cmd: Option, } #[derive(clap::Subcommand, Debug)] pub enum Cmd { - /// Print the resolved QEMU argv (no execution) + /// Print the resolved QEMU argv (no execution). Implies --dry. Print(Args), - /// Run QEMU - Run(Args), - /// Emit a JSON artifact containing the resolved configuration + argv + /// Emit a JSON artifact containing the resolved configuration + argv (implies --dry). Artifact(ArtifactArgs), } @@ -40,6 +42,11 @@ pub struct ArtifactArgs { #[derive(Parser, Debug, Clone)] pub struct Args { + /// Do not require build artifacts to exist (OVMF vars, disk image, etc.). + /// Useful for printing commands in locked-down environments. + #[arg(long)] + pub dry: bool, + /// Profile (selects device set and defaults) #[arg(long, default_value = "default")] pub profile: Profile, @@ -48,7 +55,7 @@ pub struct Args { #[arg(long)] pub headless: bool, - /// Add a timeout wrapper (seconds). Only applies to `run`. + /// Add a timeout wrapper (seconds). Only applies to run mode. #[arg(long, default_value_t = 0)] pub timeout_secs: u64, @@ -113,22 +120,20 @@ fn main() -> Result<()> { let cli = Cli::parse(); match cli.cmd { - Cmd::Print(args) => { + Some(Cmd::Print(mut args)) => { + args.dry = true; let argv = build_qemu_argv(&args)?; println!("{}", shell_join(&argv)); + Ok(()) } - Cmd::Run(args) => { + Some(Cmd::Artifact(artifact)) => { + let mut args = artifact.args; + args.dry = true; let argv = build_qemu_argv(&args)?; - let status = run_qemu(&args, &argv)?; - // forward exit code - std::process::exit(status.code().unwrap_or(1)); - } - Cmd::Artifact(artifact) => { - let argv = build_qemu_argv(&artifact.args)?; let cwd = std::env::current_dir()?; let doc = Artifact { - profile: artifact.args.profile, - headless: artifact.args.headless, + profile: args.profile, + headless: args.headless, argv: argv.clone(), cwd, }; @@ -138,10 +143,15 @@ fn main() -> Result<()> { std::fs::write(&artifact.out, serde_json::to_string_pretty(&doc)?) .with_context(|| format!("write artifact {}", artifact.out.display()))?; println!("Wrote {}", artifact.out.display()); + Ok(()) + } + None => { + // Default behavior: run. + let argv = build_qemu_argv(&cli.run)?; + let status = run_qemu(&cli.run, &argv)?; + std::process::exit(status.code().unwrap_or(1)); } } - - Ok(()) } fn repo_root() -> Result { @@ -171,9 +181,11 @@ fn build_qemu_argv(args: &Args) -> Result> { let disk_img = root.join("build/disk.img"); // We intentionally do NOT auto-build here (runner should be usable even when build is blocked). - ensure_exists(&ovmf_code, "OVMF code")?; - ensure_exists(&ovmf_vars, "OVMF vars")?; - ensure_exists(&disk_img, "disk image")?; + if !args.dry { + ensure_exists(&ovmf_code, "OVMF code")?; + ensure_exists(&ovmf_vars, "OVMF vars")?; + ensure_exists(&disk_img, "disk image")?; + } let mut argv: Vec = vec!["qemu-system-x86_64".into()]; argv.extend([ From 193b9de97c19720b610cd470cdbbec39de2a2d4a Mon Sep 17 00:00:00 2001 From: "Rowan (OpenClaw)" Date: Fri, 20 Feb 2026 02:59:21 +0100 Subject: [PATCH 03/10] docs: add QEMU runner (theseus-qemu) --- docs/index.md | 1 + docs/qemu-runner.md | 111 ++++++++++++++++++++++++++++++++++++++++++++ 2 files changed, 112 insertions(+) create mode 100644 docs/qemu-runner.md diff --git a/docs/index.md b/docs/index.md index ad00062..b613feb 100644 --- a/docs/index.md +++ b/docs/index.md @@ -9,5 +9,6 @@ Welcome! This living wiki covers how the unified TheseusOS bootloader and kernel - [Hardware & Drivers](hardware-and-drivers.md) — device inventory, driver manager, serial stack, and interrupts. - [Driver Systems Deep Dive](driver-systems-deep-dive.md) — comprehensive technical guide to DMA, PCI discovery, and driver framework. - [Development & Debugging](development-and-debugging.md) — building, running under QEMU, logging, and the serial monitor. +- [QEMU Runner](qemu-runner.md) — the `theseus-qemu` Rust runner (profiles, opt-in sockets, reproducible argv). Looking for historical notes? Everything that pre-dates this rewrite now lives in [docs/archive](archive/). diff --git a/docs/qemu-runner.md b/docs/qemu-runner.md new file mode 100644 index 0000000..dc3637f --- /dev/null +++ b/docs/qemu-runner.md @@ -0,0 +1,111 @@ +# QEMU Runner (`theseus-qemu`) + +TheseusOS includes a small Rust CLI tool to **generate** and **run** the QEMU command-line in a reproducible way. + +It is intended to replace (or eventually supersede) `startQemu.sh` once we’ve reached feature parity and added profiles. + +## Why a Rust runner? + +- **Profiles**: named, composable configurations (minimal, usb-kbd, net, storage, etc.). +- **Opt-in sockets**: when working in restricted environments (e.g. editor sandboxes), you may not be allowed to bind `/tmp` sockets. The runner makes serial/QMP/HMP endpoints explicit flags. +- **Reproducibility**: `print` and `artifact` make it easy to copy/paste or store the exact argv. + +## Location + +- Source: `tools/theseus-qemu/` +- Run via Cargo workspace: + +```bash +cargo run -p theseus-qemu -- --help +``` + +## Basic usage + +### Default behavior: run + +Running **without** a subcommand executes QEMU: + +```bash +# headed by default (QEMU window) +cargo run -p theseus-qemu -- + +# headless +cargo run -p theseus-qemu -- --headless +``` + +### Print the command instead of running + +```bash +cargo run -p theseus-qemu -- print --headless +``` + +### Dry mode (print without requiring build artifacts) + +`--dry` disables the runner’s preflight checks for required files (OVMF vars, disk image, etc.). +This is useful when you want to **see** the command, even if you can’t build/run right now. + +```bash +cargo run -p theseus-qemu -- print --dry --profile min --headless +``` + +Notes: +- `print` and `artifact` imply `--dry` automatically. +- `--dry` does **not** make QEMU succeed if the files truly don’t exist; it only skips the runner checks. + +## Profiles + +Current profiles (early days): +- `default`: mirrors `startQemu.sh` device defaults (q35, nvme, root ports, xhci, virtio-gpu, virtio-net) +- `min`: minimal bring-up (no GPU/USB/NIC) +- `usb-kbd`: xHCI + usb keyboard + +Example: + +```bash +cargo run -p theseus-qemu -- --profile usb-kbd --headless +``` + +## Opt-in automation endpoints (serial/QMP/HMP) + +These flags are intentionally explicit so humans can run without sockets by default, while automation can enable them. + +### Serial + +```bash +# serial over stdio +cargo run -p theseus-qemu -- --serial stdio + +# serial over unix socket (for automation) +cargo run -p theseus-qemu -- --serial unix --serial-path /tmp/theseus-serial.sock +``` + +### QMP (recommended for automation) + +```bash +cargo run -p theseus-qemu -- --qmp /tmp/theseus-qmp.sock +``` + +### HMP monitor socket (optional) + +```bash +cargo run -p theseus-qemu -- --hmp /tmp/theseus-hmp.sock +``` + +## Artifacts + +You can emit a JSON file with the resolved argv: + +```bash +cargo run -p theseus-qemu -- artifact --out build/qemu-argv.json +``` + +## Relationship with `startQemu.sh` + +Right now `startQemu.sh` remains the reference implementation and includes build + timeout + success-marker parsing. + +The Rust runner currently focuses on: +- stable argv generation +- profile selection +- explicit socket toggles + +We’ll incrementally migrate the remaining features (build orchestration, success-marker exit normalization, richer device profiles) into the Rust runner. From 7dc4b7fc6c8b84e4f02f15bd8391b496bcd6b7df Mon Sep 17 00:00:00 2001 From: "Rowan (OpenClaw)" Date: Fri, 20 Feb 2026 11:01:38 +0100 Subject: [PATCH 04/10] theseus-qemu: build-before-run, timeout, success-marker parity with startQemu.sh --- docs/qemu-runner.md | 14 +++- tools/theseus-qemu/src/main.rs | 120 +++++++++++++++++++++++++++++++-- 2 files changed, 124 insertions(+), 10 deletions(-) diff --git a/docs/qemu-runner.md b/docs/qemu-runner.md index dc3637f..3efaa28 100644 --- a/docs/qemu-runner.md +++ b/docs/qemu-runner.md @@ -101,11 +101,19 @@ cargo run -p theseus-qemu -- artifact --out build/qemu-argv.json ## Relationship with `startQemu.sh` -Right now `startQemu.sh` remains the reference implementation and includes build + timeout + success-marker parsing. +Right now `startQemu.sh` remains the historical reference implementation. -The Rust runner currently focuses on: +As of the `feat/theseus-qemu-parity` work, the Rust runner supports several of the practical conveniences from `startQemu.sh`: +- **Build-before-run** (default): runs `make all` before launching QEMU. Disable with `--no-build`. +- **Timeout**: `--timeout-secs N` runs QEMU under `timeout --foreground`. +- **Success marker**: if QEMU output contains the marker string (default: `Kernel environment test completed successfully`), the runner forces exit code 0. Override via `--success-marker`. + +The runner still focuses on: - stable argv generation - profile selection - explicit socket toggles -We’ll incrementally migrate the remaining features (build orchestration, success-marker exit normalization, richer device profiles) into the Rust runner. +Next steps for parity: +- richer profiles (net/storage variants) +- optional log cleanup and artifact capture +- first-class QMP/HMP helpers (later skills will consume these) diff --git a/tools/theseus-qemu/src/main.rs b/tools/theseus-qemu/src/main.rs index 31269e5..cd74247 100644 --- a/tools/theseus-qemu/src/main.rs +++ b/tools/theseus-qemu/src/main.rs @@ -1,6 +1,9 @@ use std::{ + io::{Read, Write}, + os::unix::process::ExitStatusExt, path::{Path, PathBuf}, - process::{Command, ExitStatus}, + process::{Command, ExitStatus, Stdio}, + thread, }; use anyhow::{Context, Result, anyhow, bail}; @@ -47,6 +50,15 @@ pub struct Args { #[arg(long)] pub dry: bool, + /// Build the project before running QEMU (equivalent to `make all`). + /// Enabled by default for human convenience. + #[arg(long, default_value_t = true)] + pub build: bool, + + /// Skip building before running QEMU. + #[arg(long)] + pub no_build: bool, + /// Profile (selects device set and defaults) #[arg(long, default_value = "default")] pub profile: Profile, @@ -59,6 +71,10 @@ pub struct Args { #[arg(long, default_value_t = 0)] pub timeout_secs: u64, + /// Kernel success marker string. If present in QEMU output, exit code is forced to 0. + #[arg(long, default_value = "Kernel environment test completed successfully")] + pub success_marker: String, + /// QEMU accelerator (e.g. kvm:tcg, tcg) #[arg(long, default_value = "kvm:tcg")] pub accel: String, @@ -302,20 +318,48 @@ fn build_qemu_argv(args: &Args) -> Result> { } fn run_qemu(args: &Args, argv: &[String]) -> Result { - let mut cmd = Command::new(&argv[0]); - cmd.args(&argv[1..]); + // Normalize build flags: --no-build wins. + let build = if args.no_build { false } else { args.build }; + + if build { + run_make_all()?; + } + + // We run QEMU and capture its combined stdout/stderr to a temp file, while also printing. + // This enables success-marker detection similar to startQemu.sh. + let out_path = + std::env::temp_dir().join(format!("theseus-qemu-output-{}.log", std::process::id())); - if args.timeout_secs > 0 { - // crude timeout wrapper (Linux). If you want portable, we can add a Rust timer. + let status = if args.timeout_secs > 0 { + // Use `timeout` for simplicity (Linux). Keep output merged. let mut t = Command::new("timeout"); t.arg("--foreground") .arg(format!("{}s", args.timeout_secs)) .arg(&argv[0]) .args(&argv[1..]); - return t.status().context("run qemu (timeout)"); + run_with_tee(t, &out_path).context("run qemu (timeout)")? + } else { + let mut cmd = Command::new(&argv[0]); + cmd.args(&argv[1..]); + run_with_tee(cmd, &out_path).context("run qemu")? + }; + + // Success marker override: if kernel prints it, force exit code 0. + if output_contains(&out_path, &args.success_marker)? { + eprintln!("✓ Detected success marker from kernel"); + return Ok(ExitStatus::from_raw(0)); + } + + // Match startQemu.sh messaging a bit. + if let Some(code) = status.code() { + if code == 1 { + eprintln!("✓ QEMU exited gracefully from guest"); + } else if code == 124 { + eprintln!("⚠ QEMU timed out after {}s", args.timeout_secs); + } } - cmd.status().context("run qemu") + Ok(status) } fn ensure_exists(path: &Path, what: &str) -> Result<()> { @@ -329,6 +373,68 @@ fn ensure_exists(path: &Path, what: &str) -> Result<()> { Ok(()) } +fn run_make_all() -> Result<()> { + let root = repo_root()?; + eprintln!("Building project (make all)..."); + let status = Command::new("make") + .arg("all") + .current_dir(root) + .status() + .context("run make all")?; + if !status.success() { + bail!("make all failed with status {status}"); + } + Ok(()) +} + +fn run_with_tee(mut cmd: Command, out_path: &Path) -> Result { + let mut child = cmd + .stdout(Stdio::piped()) + .stderr(Stdio::piped()) + .spawn() + .context("spawn")?; + + let mut out_file = std::fs::File::create(out_path) + .with_context(|| format!("create {}", out_path.display()))?; + + let mut stdout = child.stdout.take().context("take stdout")?; + let mut stderr = child.stderr.take().context("take stderr")?; + + let t1 = thread::spawn(move || { + let mut b = Vec::new(); + let _ = stdout.read_to_end(&mut b); + b + }); + + let t2 = thread::spawn(move || { + let mut b = Vec::new(); + let _ = stderr.read_to_end(&mut b); + b + }); + + let status = child.wait().context("wait")?; + + let stdout_buf = t1.join().unwrap_or_default(); + let stderr_buf = t2.join().unwrap_or_default(); + + // Write combined output to file and to our stdout/stderr. + out_file.write_all(&stdout_buf).ok(); + out_file.write_all(&stderr_buf).ok(); + std::io::stdout().write_all(&stdout_buf).ok(); + std::io::stderr().write_all(&stderr_buf).ok(); + + Ok(status) +} + +fn output_contains(path: &Path, needle: &str) -> Result { + let mut s = String::new(); + std::fs::File::open(path) + .with_context(|| format!("open {}", path.display()))? + .read_to_string(&mut s) + .ok(); + Ok(s.contains(needle)) +} + fn shell_join(argv: &[String]) -> String { argv.iter().map(shell_escape).collect::>().join(" ") } From 6e25441d56acfc7b4125dba25ba8908f5bb6ceb5 Mon Sep 17 00:00:00 2001 From: "Rowan (OpenClaw)" Date: Fri, 20 Feb 2026 11:16:55 +0100 Subject: [PATCH 05/10] theseus-qemu: default /tmp socket paths for --qmp/--hmp --- docs/qemu-runner.md | 8 ++++++++ tools/theseus-qemu/src/main.rs | 8 ++++---- 2 files changed, 12 insertions(+), 4 deletions(-) diff --git a/docs/qemu-runner.md b/docs/qemu-runner.md index 3efaa28..859db06 100644 --- a/docs/qemu-runner.md +++ b/docs/qemu-runner.md @@ -82,12 +82,20 @@ cargo run -p theseus-qemu -- --serial unix --serial-path /tmp/theseus-serial.soc ### QMP (recommended for automation) ```bash +# default path +cargo run -p theseus-qemu -- --qmp + +# custom path cargo run -p theseus-qemu -- --qmp /tmp/theseus-qmp.sock ``` ### HMP monitor socket (optional) ```bash +# default path +cargo run -p theseus-qemu -- --hmp + +# custom path cargo run -p theseus-qemu -- --hmp /tmp/theseus-hmp.sock ``` diff --git a/tools/theseus-qemu/src/main.rs b/tools/theseus-qemu/src/main.rs index cd74247..cc09282 100644 --- a/tools/theseus-qemu/src/main.rs +++ b/tools/theseus-qemu/src/main.rs @@ -83,12 +83,12 @@ pub struct Args { #[arg(long, default_value = "split")] pub irqchip: String, - /// Enable QMP socket. If omitted, disabled. - #[arg(long)] + /// Enable QMP socket. If provided without a value, defaults to `/tmp/theseus-qmp.sock`. + #[arg(long, num_args = 0..=1, default_missing_value = "/tmp/theseus-qmp.sock")] pub qmp: Option, - /// Enable HMP monitor socket. If omitted, disabled. - #[arg(long)] + /// Enable HMP monitor socket. If provided without a value, defaults to `/tmp/theseus-hmp.sock`. + #[arg(long, num_args = 0..=1, default_missing_value = "/tmp/theseus-hmp.sock")] pub hmp: Option, /// Serial mode. From 98411e5968dc5e0338c9d286b831c6d4268a10ea Mon Sep 17 00:00:00 2001 From: "Rowan (OpenClaw)" Date: Fri, 20 Feb 2026 11:34:21 +0100 Subject: [PATCH 06/10] Add systemd user relay units for /tmp QEMU PTYs + default qmp path --- docs/qemu-runner.md | 31 ++++++++++++++++++-- scripts/install-qemu-relays.sh | 44 +++++++++++++++++++++++++++++ scripts/qemu_debugcon_relay.service | 9 +++--- scripts/qemu_monitor_relay.service | 9 +++--- scripts/qemu_qmp_relay.service | 15 ++++++++++ scripts/qemu_serial_relay.service | 9 +++--- tools/theseus-qemu/src/main.rs | 34 +++++++++++++++++----- 7 files changed, 126 insertions(+), 25 deletions(-) create mode 100755 scripts/install-qemu-relays.sh create mode 100644 scripts/qemu_qmp_relay.service diff --git a/docs/qemu-runner.md b/docs/qemu-runner.md index 859db06..ee143ef 100644 --- a/docs/qemu-runner.md +++ b/docs/qemu-runner.md @@ -79,17 +79,42 @@ cargo run -p theseus-qemu -- --serial stdio cargo run -p theseus-qemu -- --serial unix --serial-path /tmp/theseus-serial.sock ``` -### QMP (recommended for automation) +### Monitor + serial relays (recommended for interactive debugging) + +TheseusOS ships systemd user units (see `scripts/`) that create stable PTY endpoints under `/tmp`: + +- monitor: `/tmp/qemu-monitor` (QEMU side) and `/tmp/qemu-monitor-host` (your terminal/minicom) +- serial: `/tmp/qemu-serial` (QEMU side) and `/tmp/qemu-serial-host` (your terminal/minicom) +- debugcon: `/tmp/qemu-debugcon` (QEMU side) and `/tmp/qemu-debugcon-host` (tail/follow output) + +Enable them: + +```bash +./scripts/install-qemu-relays.sh +``` + +Use them with the runner: + +```bash +cargo run -p theseus-qemu -- --serial unix --serial-path /tmp/qemu-serial +cargo run -p theseus-qemu -- --monitor-pty +cargo run -p theseus-qemu -- --debugcon-pty +``` + +### QMP (machine control) ```bash # default path cargo run -p theseus-qemu -- --qmp # custom path -cargo run -p theseus-qemu -- --qmp /tmp/theseus-qmp.sock +cargo run -p theseus-qemu -- --qmp /tmp/qemu-qmp.sock ``` -### HMP monitor socket (optional) +If you also enable the QMP relay unit, it exposes a stable host socket: +- `/tmp/qemu-qmp-host.sock` → forwards to `/tmp/qemu-qmp.sock` + +### HMP unix socket (optional) ```bash # default path diff --git a/scripts/install-qemu-relays.sh b/scripts/install-qemu-relays.sh new file mode 100755 index 0000000..2b228bf --- /dev/null +++ b/scripts/install-qemu-relays.sh @@ -0,0 +1,44 @@ +#!/usr/bin/env bash +set -euo pipefail + +# Install TheseusOS QEMU relay units (user systemd) +# +# Creates persistent /tmp endpoints used for interactive debugging: +# - /tmp/qemu-serial-host (PTY) -> QEMU uses /tmp/qemu-serial +# - /tmp/qemu-monitor-host (PTY) -> QEMU uses /tmp/qemu-monitor +# - /tmp/qemu-debugcon-host (PTY) -> QEMU uses /tmp/qemu-debugcon +# - /tmp/qemu-qmp-host.sock (unix) -> QEMU uses /tmp/qemu-qmp.sock +# +# Requirements: systemd user session + socat. + +SCRIPT_DIR=$(cd -- "$(dirname -- "${BASH_SOURCE[0]}")" &> /dev/null && pwd) + +UNITS=( + qemu_serial_relay.service + qemu_monitor_relay.service + qemu_debugcon_relay.service + qemu_qmp_relay.service +) + +mkdir -p "${HOME}/.config/systemd/user" + +for u in "${UNITS[@]}"; do + src="${SCRIPT_DIR}/${u}" + dst="${HOME}/.config/systemd/user/${u}" + if [[ ! -f "$src" ]]; then + echo "Missing unit file: $src" >&2 + exit 1 + fi + cp -f "$src" "$dst" + echo "Installed $u" +done + +systemctl --user daemon-reload + +for u in "${UNITS[@]}"; do + systemctl --user enable --now "$u" + systemctl --user status "$u" --no-pager --full | sed -n '1,8p' + echo "---" +done + +echo "✓ QEMU relay units installed and started." diff --git a/scripts/qemu_debugcon_relay.service b/scripts/qemu_debugcon_relay.service index b9b8a04..4f11d52 100644 --- a/scripts/qemu_debugcon_relay.service +++ b/scripts/qemu_debugcon_relay.service @@ -1,12 +1,11 @@ [Unit] -Description=Persistent QEMU Debugcon Relay (named pipe pair) +Description=Persistent QEMU debugcon relay (PTY pair) After=default.target [Service] -Type=simple -ExecStartPre=/usr/bin/rm -f /tmp/qemu-debugcon.in /tmp/qemu-debugcon.out -ExecStart=/usr/bin/mkfifo /tmp/qemu-debugcon.in /tmp/qemu-debugcon.out; \ - exec /usr/bin/socat -u PIPE:/tmp/qemu-debugcon.in PIPE:/tmp/qemu-debugcon.out +ExecStart=/usr/bin/socat \ + PTY,link=/tmp/qemu-debugcon,raw,echo=0,mode=666 \ + PTY,link=/tmp/qemu-debugcon-host,raw,echo=0,mode=666 Restart=always RestartSec=1 diff --git a/scripts/qemu_monitor_relay.service b/scripts/qemu_monitor_relay.service index f12cb1e..c585cf5 100644 --- a/scripts/qemu_monitor_relay.service +++ b/scripts/qemu_monitor_relay.service @@ -1,12 +1,11 @@ [Unit] -Description=Persistent QEMU Monitor Relay (named pipe pair) +Description=Persistent QEMU monitor relay (PTY pair) After=default.target [Service] -Type=simple -ExecStartPre=/usr/bin/rm -f /tmp/qemu-monitor.in /tmp/qemu-monitor.out -ExecStart=/usr/bin/mkfifo /tmp/qemu-monitor.in /tmp/qemu-monitor.out; \ - exec /usr/bin/socat -u PIPE:/tmp/qemu-monitor.in PIPE:/tmp/qemu-monitor.out +ExecStart=/usr/bin/socat \ + PTY,link=/tmp/qemu-monitor,raw,echo=0,mode=666 \ + PTY,link=/tmp/qemu-monitor-host,raw,echo=0,mode=666 Restart=always RestartSec=1 diff --git a/scripts/qemu_qmp_relay.service b/scripts/qemu_qmp_relay.service new file mode 100644 index 0000000..4444b65 --- /dev/null +++ b/scripts/qemu_qmp_relay.service @@ -0,0 +1,15 @@ +[Unit] +Description=Persistent QEMU QMP relay (unix socket host → qemu socket) +After=default.target + +[Service] +# Listens on /tmp/qemu-qmp-host.sock and forwards each connection to /tmp/qemu-qmp.sock. +# This lets tooling connect to a stable path even if QEMU restarts. +ExecStart=/usr/bin/socat \ + UNIX-LISTEN:/tmp/qemu-qmp-host.sock,fork,reuseaddr,mode=666 \ + UNIX-CONNECT:/tmp/qemu-qmp.sock +Restart=always +RestartSec=1 + +[Install] +WantedBy=default.target diff --git a/scripts/qemu_serial_relay.service b/scripts/qemu_serial_relay.service index e0ab980..09d8ea9 100644 --- a/scripts/qemu_serial_relay.service +++ b/scripts/qemu_serial_relay.service @@ -1,12 +1,11 @@ [Unit] -Description=Persistent QEMU Serial Relay (named pipe pair) +Description=Persistent QEMU serial relay (PTY pair) After=default.target [Service] -Type=simple -ExecStartPre=/usr/bin/rm -f /tmp/qemu-serial.in /tmp/qemu-serial.out -ExecStart=/usr/bin/mkfifo /tmp/qemu-serial.in /tmp/qemu-serial.out; \ - exec /usr/bin/socat -u PIPE:/tmp/qemu-serial.in PIPE:/tmp/qemu-serial.out +ExecStart=/usr/bin/socat \ + PTY,link=/tmp/qemu-serial,raw,echo=0,mode=666 \ + PTY,link=/tmp/qemu-serial-host,raw,echo=0,mode=666 Restart=always RestartSec=1 diff --git a/tools/theseus-qemu/src/main.rs b/tools/theseus-qemu/src/main.rs index cc09282..c5c273b 100644 --- a/tools/theseus-qemu/src/main.rs +++ b/tools/theseus-qemu/src/main.rs @@ -83,20 +83,28 @@ pub struct Args { #[arg(long, default_value = "split")] pub irqchip: String, - /// Enable QMP socket. If provided without a value, defaults to `/tmp/theseus-qmp.sock`. - #[arg(long, num_args = 0..=1, default_missing_value = "/tmp/theseus-qmp.sock")] + /// Enable QMP socket. If provided without a value, defaults to `/tmp/qemu-qmp.sock`. + #[arg(long, num_args = 0..=1, default_missing_value = "/tmp/qemu-qmp.sock")] pub qmp: Option, /// Enable HMP monitor socket. If provided without a value, defaults to `/tmp/theseus-hmp.sock`. #[arg(long, num_args = 0..=1, default_missing_value = "/tmp/theseus-hmp.sock")] pub hmp: Option, + /// Enable a PTY-backed QEMU monitor endpoint. If provided without a value, defaults to `/tmp/qemu-monitor`. + #[arg(long, num_args = 0..=1, default_missing_value = "/tmp/qemu-monitor")] + pub monitor_pty: Option, + + /// Send isa-debugcon output to a PTY path. If provided without a value, defaults to `/tmp/qemu-debugcon`. + #[arg(long, num_args = 0..=1, default_missing_value = "/tmp/qemu-debugcon")] + pub debugcon_pty: Option, + /// Serial mode. #[arg(long, value_enum, default_value_t = SerialMode::Off)] pub serial: SerialMode, /// Serial endpoint for unix mode (e.g. /tmp/theseus-serial.sock) - #[arg(long, default_value = "/tmp/theseus-serial.sock")] + #[arg(long, default_value = "/tmp/qemu-serial")] pub serial_path: String, /// Extra raw QEMU args appended verbatim @@ -236,10 +244,18 @@ fn build_qemu_argv(args: &Args) -> Result> { if args.headless { argv.extend(["-display".into(), "none".into()]); - argv.extend(["-chardev".into(), "stdio,id=debugcon".into()]); + if let Some(path) = &args.debugcon_pty { + argv.extend(["-chardev".into(), format!("file,id=debugcon,path={}", path)]); + } else { + argv.extend(["-chardev".into(), "stdio,id=debugcon".into()]); + } argv.extend(["-d".into(), "int,guest_errors,cpu_reset".into()]); } else { - argv.extend(["-chardev".into(), "file,id=debugcon,path=debug.log".into()]); + if let Some(path) = &args.debugcon_pty { + argv.extend(["-chardev".into(), format!("file,id=debugcon,path={}", path)]); + } else { + argv.extend(["-chardev".into(), "file,id=debugcon,path=debug.log".into()]); + } // default: provide a monitor on stdio if headed. argv.extend(["-monitor".into(), "stdio".into()]); } @@ -258,11 +274,15 @@ fn build_qemu_argv(args: &Args) -> Result> { } } - // Optional QMP/HMP + // Optional QMP/HMP / monitor PTY if let Some(qmp) = &args.qmp { argv.extend(["-qmp".into(), format!("unix:{},server,nowait", qmp)]); } - if let Some(hmp) = &args.hmp { + + // If monitor_pty is set, use it. Otherwise fall back to HMP unix socket if configured. + if let Some(mon) = &args.monitor_pty { + argv.extend(["-monitor".into(), mon.clone()]); + } else if let Some(hmp) = &args.hmp { argv.extend(["-monitor".into(), format!("unix:{},server,nowait", hmp)]); } From e61883f1feeedc590d9725fcf9ff4fa141cd54e9 Mon Sep 17 00:00:00 2001 From: "Rowan (OpenClaw)" Date: Fri, 20 Feb 2026 11:40:17 +0100 Subject: [PATCH 07/10] theseus-qemu: add --relays convenience flag --- docs/qemu-runner.md | 4 ++++ tools/theseus-qemu/src/main.rs | 38 ++++++++++++++++++++++++++++++++++ 2 files changed, 42 insertions(+) diff --git a/docs/qemu-runner.md b/docs/qemu-runner.md index ee143ef..13682db 100644 --- a/docs/qemu-runner.md +++ b/docs/qemu-runner.md @@ -96,6 +96,10 @@ Enable them: Use them with the runner: ```bash +# single convenience flag +cargo run -p theseus-qemu -- --relays + +# or explicit flags cargo run -p theseus-qemu -- --serial unix --serial-path /tmp/qemu-serial cargo run -p theseus-qemu -- --monitor-pty cargo run -p theseus-qemu -- --debugcon-pty diff --git a/tools/theseus-qemu/src/main.rs b/tools/theseus-qemu/src/main.rs index c5c273b..3d9fead 100644 --- a/tools/theseus-qemu/src/main.rs +++ b/tools/theseus-qemu/src/main.rs @@ -107,6 +107,18 @@ pub struct Args { #[arg(long, default_value = "/tmp/qemu-serial")] pub serial_path: String, + /// Enable the standard /tmp relays used for interactive debugging. + /// + /// This is a convenience flag equivalent to enabling: + /// - --serial unix --serial-path /tmp/qemu-serial + /// - --monitor-pty /tmp/qemu-monitor + /// - --debugcon-pty /tmp/qemu-debugcon + /// - --qmp /tmp/qemu-qmp.sock + /// + /// Explicit flags always win (e.g., passing your own --serial/stdio overrides this). + #[arg(long)] + pub relays: bool, + /// Extra raw QEMU args appended verbatim #[arg(long)] pub extra: Vec, @@ -196,7 +208,33 @@ fn repo_root() -> Result { )) } +fn apply_relay_defaults(args: &mut Args) { + if !args.relays { + return; + } + + // Enable defaults only when the user hasn't explicitly set alternatives. + if matches!(args.serial, SerialMode::Off) { + args.serial = SerialMode::Unix; + } + + if args.monitor_pty.is_none() { + args.monitor_pty = Some("/tmp/qemu-monitor".to_string()); + } + + if args.debugcon_pty.is_none() { + args.debugcon_pty = Some("/tmp/qemu-debugcon".to_string()); + } + + if args.qmp.is_none() { + args.qmp = Some("/tmp/qemu-qmp.sock".to_string()); + } +} + fn build_qemu_argv(args: &Args) -> Result> { + let mut args = args.clone(); + apply_relay_defaults(&mut args); + let root = repo_root()?; // Paths matching startQemu.sh From f20ae50e2664a56be20117e98fd77af102807e2d Mon Sep 17 00:00:00 2001 From: "Rowan (OpenClaw)" Date: Fri, 20 Feb 2026 12:03:05 +0100 Subject: [PATCH 08/10] theseus-qemu: make --relays use PTY relays via file chardevs --- docs/qemu-runner.md | 2 ++ tools/theseus-qemu/src/main.rs | 26 +++++++++++++++++++++++--- 2 files changed, 25 insertions(+), 3 deletions(-) diff --git a/docs/qemu-runner.md b/docs/qemu-runner.md index 13682db..8dd68f0 100644 --- a/docs/qemu-runner.md +++ b/docs/qemu-runner.md @@ -99,6 +99,8 @@ Use them with the runner: # single convenience flag cargo run -p theseus-qemu -- --relays +Note: `--relays` assumes you have the relay PTYs running (see `./scripts/install-qemu-relays.sh`). It routes QEMU monitor/serial/debugcon into the QEMU-side PTYs under `/tmp/qemu-*`. + # or explicit flags cargo run -p theseus-qemu -- --serial unix --serial-path /tmp/qemu-serial cargo run -p theseus-qemu -- --monitor-pty diff --git a/tools/theseus-qemu/src/main.rs b/tools/theseus-qemu/src/main.rs index 3d9fead..7a3399c 100644 --- a/tools/theseus-qemu/src/main.rs +++ b/tools/theseus-qemu/src/main.rs @@ -132,6 +132,9 @@ pub enum SerialMode { Stdio, /// Expose a unix socket at --serial-path Unix, + /// Use an existing PTY/tty at --serial-path (pairs well with socat PTY relays) + /// Use an existing PTY at --serial-path by opening it as a file chardev. + PseudoTtyFile, } #[derive(ValueEnum, Debug, Clone, Copy, PartialEq, Eq, Serialize, Deserialize)] @@ -215,7 +218,8 @@ fn apply_relay_defaults(args: &mut Args) { // Enable defaults only when the user hasn't explicitly set alternatives. if matches!(args.serial, SerialMode::Off) { - args.serial = SerialMode::Unix; + // For the standard relay setup, QEMU should use the QEMU-side PTY. + args.serial = SerialMode::PseudoTtyFile; } if args.monitor_pty.is_none() { @@ -310,6 +314,16 @@ fn build_qemu_argv(args: &Args) -> Result> { format!("unix:{},server,nowait", args.serial_path), ]); } + SerialMode::PseudoTtyFile => { + // QEMU does not always include a `tty` chardev backend, but the `file` backend + // can open an existing PTY device node just fine. + argv.extend([ + "-chardev".into(), + format!("file,id=serial0,path={}", args.serial_path), + "-serial".into(), + "chardev:serial0".into(), + ]); + } } // Optional QMP/HMP / monitor PTY @@ -317,9 +331,15 @@ fn build_qemu_argv(args: &Args) -> Result> { argv.extend(["-qmp".into(), format!("unix:{},server,nowait", qmp)]); } - // If monitor_pty is set, use it. Otherwise fall back to HMP unix socket if configured. + // If monitor_pty is set, use it via a tty chardev. Otherwise fall back to HMP unix socket if configured. if let Some(mon) = &args.monitor_pty { - argv.extend(["-monitor".into(), mon.clone()]); + // Use file backend so it can open an existing PTY device node. + argv.extend([ + "-chardev".into(), + format!("file,id=mon0,path={}", mon), + "-monitor".into(), + "chardev:mon0".into(), + ]); } else if let Some(hmp) = &args.hmp { argv.extend(["-monitor".into(), format!("unix:{},server,nowait", hmp)]); } From 7ac8116368d06175b319bbe3e176d2bbbc003f60 Mon Sep 17 00:00:00 2001 From: "Rowan (OpenClaw)" Date: Fri, 20 Feb 2026 12:10:01 +0100 Subject: [PATCH 09/10] scripts/docs: add debugcon logger unit; document using cat vs tail for PTY --- docs/qemu-runner.md | 3 ++- scripts/install-qemu-relays.sh | 1 + scripts/qemu_debugcon_logger.service | 13 +++++++++++++ 3 files changed, 16 insertions(+), 1 deletion(-) create mode 100644 scripts/qemu_debugcon_logger.service diff --git a/docs/qemu-runner.md b/docs/qemu-runner.md index 8dd68f0..5f7e142 100644 --- a/docs/qemu-runner.md +++ b/docs/qemu-runner.md @@ -85,7 +85,8 @@ TheseusOS ships systemd user units (see `scripts/`) that create stable PTY endpo - monitor: `/tmp/qemu-monitor` (QEMU side) and `/tmp/qemu-monitor-host` (your terminal/minicom) - serial: `/tmp/qemu-serial` (QEMU side) and `/tmp/qemu-serial-host` (your terminal/minicom) -- debugcon: `/tmp/qemu-debugcon` (QEMU side) and `/tmp/qemu-debugcon-host` (tail/follow output) +- debugcon: `/tmp/qemu-debugcon` (QEMU side) and `/tmp/qemu-debugcon-host` (PTY stream; use `cat`, not `tail -f`) + - optional log file (enable `qemu_debugcon_logger.service`): `/tmp/qemu-debugcon.log` (then you can `tail -f`) Enable them: diff --git a/scripts/install-qemu-relays.sh b/scripts/install-qemu-relays.sh index 2b228bf..ed1044e 100755 --- a/scripts/install-qemu-relays.sh +++ b/scripts/install-qemu-relays.sh @@ -18,6 +18,7 @@ UNITS=( qemu_monitor_relay.service qemu_debugcon_relay.service qemu_qmp_relay.service + qemu_debugcon_logger.service ) mkdir -p "${HOME}/.config/systemd/user" diff --git a/scripts/qemu_debugcon_logger.service b/scripts/qemu_debugcon_logger.service new file mode 100644 index 0000000..e86af71 --- /dev/null +++ b/scripts/qemu_debugcon_logger.service @@ -0,0 +1,13 @@ +[Unit] +Description=QEMU debugcon logger (reads PTY host end → log file) +After=default.target + +[Service] +# Requires qemu_debugcon_relay.service to be running (creates /tmp/qemu-debugcon-host) +# Writes an append-only log you can `tail -f`. +ExecStart=/usr/bin/bash -lc 'set -euo pipefail; mkdir -p /tmp; : > /tmp/qemu-debugcon.log; exec stdbuf -o0 cat /tmp/qemu-debugcon-host >> /tmp/qemu-debugcon.log' +Restart=always +RestartSec=1 + +[Install] +WantedBy=default.target From e6659b09186e1e6f5d6a228e39681855925b4704 Mon Sep 17 00:00:00 2001 From: "Rowan (OpenClaw)" Date: Sun, 22 Feb 2026 02:36:33 +0100 Subject: [PATCH 10/10] theseus-qemu: make QEMU -d debug flags configurable; default to guest_errors --- docs/qemu-runner.md | 1 + tools/theseus-qemu/src/main.rs | 29 ++++++++++++++++++++++++++++- 2 files changed, 29 insertions(+), 1 deletion(-) diff --git a/docs/qemu-runner.md b/docs/qemu-runner.md index 5f7e142..b6dbf02 100644 --- a/docs/qemu-runner.md +++ b/docs/qemu-runner.md @@ -147,6 +147,7 @@ As of the `feat/theseus-qemu-parity` work, the Rust runner supports several of t - **Build-before-run** (default): runs `make all` before launching QEMU. Disable with `--no-build`. - **Timeout**: `--timeout-secs N` runs QEMU under `timeout --foreground`. - **Success marker**: if QEMU output contains the marker string (default: `Kernel environment test completed successfully`), the runner forces exit code 0. Override via `--success-marker`. +- **High-signal QEMU debug output by default** (headless): uses `-d guest_errors` unless you opt into noisier flags. The runner still focuses on: - stable argv generation diff --git a/tools/theseus-qemu/src/main.rs b/tools/theseus-qemu/src/main.rs index 7a3399c..fac6a4a 100644 --- a/tools/theseus-qemu/src/main.rs +++ b/tools/theseus-qemu/src/main.rs @@ -119,6 +119,20 @@ pub struct Args { #[arg(long)] pub relays: bool, + /// QEMU debug flags passed via `-d ` (comma-separated). + /// + /// In headless mode we default to a minimal, high-signal set (`guest_errors`) to + /// avoid massive log output (e.g. from `cpu_reset`). + /// + /// Pass `--qemu-debug-flags int,guest_errors,cpu_reset` when you explicitly + /// want the noisy bring-up output. + #[arg(long, num_args = 0..=1, default_missing_value = "guest_errors")] + pub qemu_debug_flags: Option, + + /// Disable QEMU debug output entirely (omit `-d`). + #[arg(long)] + pub no_qemu_debug: bool, + /// Extra raw QEMU args appended verbatim #[arg(long)] pub extra: Vec, @@ -291,7 +305,20 @@ fn build_qemu_argv(args: &Args) -> Result> { } else { argv.extend(["-chardev".into(), "stdio,id=debugcon".into()]); } - argv.extend(["-d".into(), "int,guest_errors,cpu_reset".into()]); + + // Keep headless output high-signal by default. + // Users can opt into noisy bring-up flags explicitly. + let debug_flags = if args.no_qemu_debug { + None + } else { + args.qemu_debug_flags + .clone() + .or(Some("guest_errors".to_string())) + }; + + if let Some(flags) = debug_flags { + argv.extend(["-d".into(), flags]); + } } else { if let Some(path) = &args.debugcon_pty { argv.extend(["-chardev".into(), format!("file,id=debugcon,path={}", path)]);