Skip to content
Merged
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
6 changes: 6 additions & 0 deletions AGENTS.md
Original file line number Diff line number Diff line change
Expand Up @@ -246,6 +246,12 @@ Dependencies flow **upward** — a crate may only depend on crates above it.

## Landmines & gotchas

- **`board add` accepts probe-rs `list` output directly.** `paavo-cli board add
--probe` takes a probe-rs selector token (`1fc9:0143-0:SERIAL`, where `-0` is
the USB interface) or a full pasted `probe-rs list` line. VID/PID are
validated as hex at registration (CLI **and** `POST /boards`), not deferred to
`probe_attach`. The canonical parser is `ProbeSelector::parse` in
`paavo-proto`.
- **Workspace-excluded crates won't build with the host toolchain** (with one
exception, noted below). These are intentionally outside `[workspace] members`
(see `Cargo.toml` `exclude`): `tests/fixtures/smoke-crate`, `soak-tests/`,
Expand Down
1 change: 1 addition & 0 deletions Cargo.lock

Some generated files are not rendered by default. Learn more about how customized files appear on GitHub.

13 changes: 10 additions & 3 deletions contrib/99-probes.rules
Original file line number Diff line number Diff line change
Expand Up @@ -2,10 +2,17 @@
# Source: https://probe.rs/docs/getting-started/probe-setup/

# Daplink (e.g. mcxa eval boards)
SUBSYSTEM=="usb", ATTRS{idVendor}=="0d28", ATTRS{idProduct}=="0204", MODE="0660", GROUP="plugdev"
SUBSYSTEM=="usb", ATTRS{idVendor}=="0d28", ATTRS{idProduct}=="0204", MODE="0660", GROUP="paavo"

# NXP MCU-Link / on-board CMSIS-DAP (e.g. MCX-A266 EVK)
SUBSYSTEM=="usb", ATTRS{idVendor}=="1fc9", MODE="0660", GROUP="paavo"

# Segger J-Link
SUBSYSTEM=="usb", ATTRS{idVendor}=="1366", MODE="0660", GROUP="plugdev"
SUBSYSTEM=="usb", ATTRS{idVendor}=="1366", MODE="0660", GROUP="paavo"

# ST-Link (in case you mix)
SUBSYSTEM=="usb", ATTRS{idVendor}=="0483", ATTRS{idProduct}=="3748", MODE="0660", GROUP="plugdev"
SUBSYSTEM=="usb", ATTRS{idVendor}=="0483", ATTRS{idProduct}=="3748", MODE="0660", GROUP="paavo"

# Single-purpose HIL box? Instead of maintaining a per-VID allowlist above,
# grant the paavo service every USB device with one rule:
# SUBSYSTEM=="usb", ENV{DEVTYPE}=="usb_device", MODE="0660", GROUP="paavo"
8 changes: 8 additions & 0 deletions contrib/README.md
Original file line number Diff line number Diff line change
Expand Up @@ -11,6 +11,14 @@ sudo install -m 0644 paavo.toml.example /etc/paavo/paavo.toml # then edit
# still lives in the state_dir (/var/lib/paavo, owned via StateDirectory=).
sudo useradd --system --create-home --home-dir /home/paavo --shell /bin/bash paavo
sudo chown paavo:paavo /var/lib/paavo # StateDirectory= also enforces this on start
# paavod builds submitted test crates with cargo; ProtectHome=true in the unit
# hides /home, so install the toolchain UNDER the state dir (matches the
# CARGO_HOME/RUSTUP_HOME in paavod.service). Needs crates.io egress.
sudo -u paavo env CARGO_HOME=/var/lib/paavo/.cargo RUSTUP_HOME=/var/lib/paavo/.rustup \
sh -c 'curl --proto "=https" --tlsv1.2 -sSf https://sh.rustup.rs | sh -s -- -y --no-modify-path'
# add the embedded target(s) your boards build for (MCX-A266 shown):
sudo -u paavo env CARGO_HOME=/var/lib/paavo/.cargo RUSTUP_HOME=/var/lib/paavo/.rustup \
/var/lib/paavo/.cargo/bin/rustup target add thumbv8m.main-none-eabihf
sudo install -m 0755 ../target/release/paavod /usr/local/bin/
sudo install -m 0755 ../target/release/paavo-web /usr/local/bin/
sudo install -m 0644 paavod.service /etc/systemd/system/
Expand Down
6 changes: 6 additions & 0 deletions contrib/paavod.service
Original file line number Diff line number Diff line change
Expand Up @@ -12,6 +12,12 @@ Restart=on-failure
RestartSec=5
StateDirectory=paavo
ReadWritePaths=/var/lib/paavo
# paavod builds submitted test crates with cargo. ProtectHome=true hides /home,
# so the toolchain must live under the (writable) state dir — see contrib/README.md
# for the one-time install. These must match RUSTUP_HOME/CARGO_HOME there.
Environment=CARGO_HOME=/var/lib/paavo/.cargo
Environment=RUSTUP_HOME=/var/lib/paavo/.rustup
Environment=PATH=/var/lib/paavo/.cargo/bin:/usr/local/sbin:/usr/local/bin:/usr/sbin:/usr/bin:/sbin:/bin
ProtectSystem=strict
ProtectHome=true
NoNewPrivileges=true
Expand Down
4 changes: 3 additions & 1 deletion crates/paavo-cli/src/cli.rs
Original file line number Diff line number Diff line change
Expand Up @@ -142,7 +142,9 @@ pub enum BoardOp {
/// Instance id (e.g. mcxa266-02).
#[arg(long)]
instance: String,
/// VID:PID:serial.
/// Probe selector. Accepts a probe-rs selector token
/// (`1fc9:0143:SERIAL`, optional `-IFACE`) or a full `probe-rs list`
/// line pasted verbatim.
#[arg(long)]
probe: String,
/// probe-rs chip name.
Expand Down
22 changes: 8 additions & 14 deletions crates/paavo-cli/src/cmd_boards.rs
Original file line number Diff line number Diff line change
Expand Up @@ -32,23 +32,17 @@ pub async fn op(client: &Client, op: BoardOp) -> Result<()> {
target,
wiring_profile,
} => {
let mut parts = probe.split(':');
let vid = parts
.next()
.ok_or_else(|| anyhow!("probe missing VID"))?
.to_string();
let pid = parts
.next()
.ok_or_else(|| anyhow!("probe missing PID"))?
.to_string();
let serial = parts
.next()
.ok_or_else(|| anyhow!("probe missing serial"))?
.to_string();
let probe_selector = ProbeSelector::parse(&probe).map_err(|e| {
anyhow!(
"invalid --probe {probe:?}: {e}\n\n\
Paste a probe-rs selector (`1fc9:0143:SERIAL`) or a full \
`probe-rs list` line."
)
})?;
let spec = BoardSpec {
id: instance,
kind,
probe_selector: ProbeSelector { vid, pid, serial },
probe_selector,
chip_name: chip,
target_name: target,
wiring_profile: Some(wiring_profile),
Expand Down
1 change: 1 addition & 0 deletions crates/paavo-cli/src/cmd_run.rs
Original file line number Diff line number Diff line change
Expand Up @@ -387,6 +387,7 @@ mod tests {
vid: "1366".into(),
pid: "1015".into(),
serial: format!("S-{id}"),
interface: None,
},
chip_name: "MCXA266".into(),
target_name: format!("target-{kind}"),
Expand Down
121 changes: 121 additions & 0 deletions crates/paavo-cli/tests/board_add_accepts_list_line.rs
Original file line number Diff line number Diff line change
@@ -0,0 +1,121 @@
//! End-to-end proof that `paavo-cli board add --probe "<full probe-rs list
//! line>"` threads the *parsed* selector — including the `-IFACE` interface
//! suffix — all the way through `POST /boards` and into the DB.
//!
//! Harness mirrors `cli_run_board_resolution.rs`: build a `Config`, open a
//! `Db` in a tempdir, wrap it in the same `Arc<Mutex<Db>>` the `AppState`
//! uses, build the router via `paavod::app::build_router`, bind a real
//! `TcpListener`, and `tokio::spawn` `axum::serve` so the daemon is reachable
//! over TCP (the `board add` command performs a real `POST /boards`).
//!
//! Because the served daemon writes through the *same* `Arc<Mutex<Db>>`
//! handle we hold here, we read the stored row straight back with
//! `BoardRow::find` (no extra dependency, no second sqlite connection) and
//! assert the daemon actually persisted the normalized selector.

use assert_cmd::Command as AssertCommand;
use paavo_db::{BoardRow, Db};
use paavod::app::build_router;
use paavod::app_state::{AppState, DrainState};
use paavod::cancellation::CancellationRegistry;
use paavod::config::{
BuildCacheConfig, Config, QuarantineConfig, RetentionConfig, SchedulerConfig, ServerConfig,
TimeoutsConfig, WebConfig,
};
use paavod::job_logs::JobLogsBroker;
use parking_lot::Mutex;
use std::sync::Arc;
use tempfile::tempdir;

#[tokio::test(flavor = "multi_thread", worker_threads = 2)]
async fn board_add_accepts_full_probe_rs_list_line() {
let tmp = tempdir().unwrap();
let sd = paavod::state_dir::StateDir::from_root(tmp.path());
sd.ensure_dirs().unwrap();

// Keep our own clone of the shared DB handle so we can read the row back
// after the CLI's POST commits. The served daemon mutates *this* handle.
let db = Arc::new(Mutex::new(Db::open(&sd.sqlite_path).unwrap()));

let cfg = Arc::new(Config {
server: ServerConfig {
bind: "127.0.0.1:0".into(),
state_dir: tmp.path().to_path_buf(),
max_upload_bytes: 256 * 1024 * 1024,
},
web: WebConfig {
bind: "127.0.0.1:0".into(),
},
timeouts: TimeoutsConfig::default(),
scheduler: SchedulerConfig {
nightly_cron: "0 0 19 * * *".into(),
starvation_threshold_s: 21_600,
max_concurrent_builds: 5,
},
build_cache: BuildCacheConfig::default(),
retention: RetentionConfig::default(),
quarantine: QuarantineConfig::default(),
corpus: vec![],
});
let state = AppState {
db: db.clone(),
config: cfg,
inventory: Arc::new(Mutex::new(vec![])),
drain: DrainState::default(),
cancellation: CancellationRegistry::default(),
build_cancel: paavod::cancellation::BuildCancelRegistry::default(),
job_logs: JobLogsBroker::new(),
};

let listener = tokio::net::TcpListener::bind("127.0.0.1:0").await.unwrap();
let addr = listener.local_addr().unwrap();
let app = build_router(state);
let server = tokio::spawn(async move {
axum::serve(listener, app).await.unwrap();
});

// Paste a full `probe-rs list` line verbatim. The `-0` after the PID is
// the USB interface index; the CLI must parse it client-side and ship the
// structured selector to the daemon.
AssertCommand::cargo_bin("paavo-cli")
.unwrap()
.env("PAAVO_HOST", format!("http://{addr}"))
.args([
"board",
"add",
"--kind",
"mcxa266",
"--instance",
"mcxa266-77",
"--probe",
"[0]: MCU-LINK on-board (r2E4) CMSIS-DAP V3.172 \
-- 1fc9:0143-0:EDFHUAFM4J5ZJ (CMSIS-DAP)",
"--chip",
"MCXA266VFL",
"--target",
"frdm-mcx-a266",
])
.assert()
.success()
.stdout(predicates::str::contains("added: mcxa266-77"));

// Read the row the daemon just persisted, through the same shared handle.
let stored = {
let guard = db.lock();
BoardRow::find(guard.raw_conn(), "mcxa266-77")
.unwrap()
.expect("board mcxa266-77 should have been inserted by POST /boards")
};

let sel = &stored.spec.probe_selector;
assert_eq!(sel.vid, "1fc9");
assert_eq!(sel.pid, "0143");
assert_eq!(sel.serial, "EDFHUAFM4J5ZJ");
assert_eq!(
sel.interface,
Some(0),
"the `-0` interface suffix must survive the CLI -> POST /boards -> DB round-trip"
);

server.abort();
}
28 changes: 28 additions & 0 deletions crates/paavo-cli/tests/board_add_selector.rs
Original file line number Diff line number Diff line change
@@ -0,0 +1,28 @@
//! `board add` must reject a malformed --probe locally, before any network
//! call. We point PAAVO_HOST at an unreachable address; if the command fails
//! with the parse message (not a connection error) we know it never POSTed.

use assert_cmd::Command as AssertCommand;
use predicates::prelude::PredicateBooleanExt;

#[test]
fn board_add_rejects_invalid_probe_before_network() {
let mut cmd = AssertCommand::cargo_bin("paavo-cli").unwrap();
cmd.env("PAAVO_HOST", "http://127.0.0.1:1").args([
"board",
"add",
"--kind",
"mcxa266",
"--instance",
"mcxa266-99",
"--probe",
"zz:gg:NOSER",
"--chip",
"MCXA266VFL",
"--target",
"frdm-mcx-a266",
]);
cmd.assert().failure().stderr(
predicates::str::contains("invalid --probe").and(predicates::str::contains("bad VID")),
);
}
1 change: 1 addition & 0 deletions crates/paavo-cli/tests/cli_run_board_resolution.rs
Original file line number Diff line number Diff line change
Expand Up @@ -31,6 +31,7 @@ fn spec(id: &str, kind: &str) -> BoardSpec {
vid: "1366".into(),
pid: "1015".into(),
serial: format!("S-{id}"),
interface: None,
},
chip_name: "MCXA266".into(),
target_name: format!("target-{kind}"),
Expand Down
1 change: 1 addition & 0 deletions crates/paavo-core/tests/common/mod.rs
Original file line number Diff line number Diff line change
Expand Up @@ -45,6 +45,7 @@ pub fn insert_board(db: &Db, id: &str, kind: &str, health: BoardHealth) -> Board
vid: "1366".into(),
pid: "1015".into(),
serial: id.into(),
interface: None,
},
chip_name: "X".into(),
target_name: format!("target-{kind}"),
Expand Down
1 change: 1 addition & 0 deletions crates/paavo-db/src/job.rs
Original file line number Diff line number Diff line change
Expand Up @@ -928,6 +928,7 @@ mod tests {
vid: "1366".into(),
pid: "1015".into(),
serial: id.into(),
interface: None,
},
chip_name: "MCXA266VFL".into(),
target_name: "frdm-mcx-a266".into(),
Expand Down
1 change: 1 addition & 0 deletions crates/paavo-db/tests/board_ops.rs
Original file line number Diff line number Diff line change
Expand Up @@ -19,6 +19,7 @@ fn sample_board() -> BoardSpec {
vid: "1366".into(),
pid: "1015".into(),
serial: "ABC".into(),
interface: None,
},
chip_name: "MCXA266VFL".into(),
target_name: "frdm-mcx-a266".into(),
Expand Down
1 change: 1 addition & 0 deletions crates/paavo-db/tests/job_ops.rs
Original file line number Diff line number Diff line change
Expand Up @@ -25,6 +25,7 @@ fn insert_default_board(db: &Db) {
vid: "1366".into(),
pid: "1015".into(),
serial: "ABC".into(),
interface: None,
},
chip_name: "MCXA266VFL".into(),
target_name: "frdm-mcx-a266".into(),
Expand Down
2 changes: 1 addition & 1 deletion crates/paavo-probe/src/session.rs
Original file line number Diff line number Diff line change
Expand Up @@ -161,7 +161,7 @@ impl RealSession {
let selector = DebugProbeSelector {
vendor_id: vid,
product_id: pid,
interface: None,
interface: opts.probe_selector.interface,
serial_number: serial_filter.clone(),
};
let probe = lister.open(selector).map_err(|e| {
Expand Down
1 change: 1 addition & 0 deletions crates/paavo-probe/tests/real_session_connect.rs
Original file line number Diff line number Diff line change
Expand Up @@ -72,6 +72,7 @@ fn connect_flashes_and_returns_live_session() {
vid: "1fc9".into(), // NXP
pid: "0143".into(), // MCU-Link CMSIS-DAP
serial: "EDFHUAFM4J5ZJ".into(), // Felipe's specific EVK
interface: None,
},
chip_name: "MCXA276".into(), // NOT MCXA266; spike finding
elf_path: elf_fixture(),
Expand Down
1 change: 1 addition & 0 deletions crates/paavo-probe/tests/real_session_drive.rs
Original file line number Diff line number Diff line change
Expand Up @@ -66,6 +66,7 @@ fn next_event_streams_decoded_test_ok_then_bkpt() {
vid: "1fc9".into(), // NXP
pid: "0143".into(), // MCU-Link CMSIS-DAP
serial: "EDFHUAFM4J5ZJ".into(), // Felipe's specific EVK
interface: None,
},
chip_name: "MCXA276".into(), // NOT MCXA266; spike finding
elf_path: elf_fixture(),
Expand Down
1 change: 1 addition & 0 deletions crates/paavo-probe/tests/real_session_ram_boot.rs
Original file line number Diff line number Diff line change
Expand Up @@ -187,6 +187,7 @@ fn connect_takes_ram_boot_path_with_vector_table_at_ram_origin() {
vid: "1fc9".into(), // NXP
pid: "0143".into(), // MCU-Link CMSIS-DAP
serial: "EDFHUAFM4J5ZJ".into(), // Felipe's specific EVK
interface: None,
},
chip_name: "MCXA276".into(), // NOT MCXA266; spike finding
elf_path: elf_fixture(),
Expand Down
1 change: 1 addition & 0 deletions crates/paavo-proto/Cargo.toml
Original file line number Diff line number Diff line change
Expand Up @@ -9,6 +9,7 @@ description = "Wire types and protocol definitions shared across the paavo works

[dependencies]
serde = { workspace = true }
thiserror = { workspace = true }
ulid = { workspace = true }

[dev-dependencies]
Expand Down
Loading
Loading