diff --git a/.cargo/config.toml b/.cargo/config.toml index d5589141..523b384f 100644 --- a/.cargo/config.toml +++ b/.cargo/config.toml @@ -1,6 +1,6 @@ [alias] update-api = "run --manifest-path ./crates/api-desc/crates/update/Cargo.toml" -wasefire = "run --manifest-path ./crates/cli/Cargo.toml --" +wasefire = "run --manifest-path ./crates/cli/Cargo.toml --features=_dev --" xtask = "run --manifest-path ./crates/xtask/Cargo.toml --" [build] diff --git a/.github/CODEOWNERS b/.github/CODEOWNERS index 511120a6..6f0fb9a9 100644 --- a/.github/CODEOWNERS +++ b/.github/CODEOWNERS @@ -1,2 +1,3 @@ * @ia0 -/crates/runner-host/crates/web-client/ @chris-dietz @ia0 +/crates/runner-host/crates/web-client/ @chris-dietz @ia0 @ia0-review +/.github/CODEOWNERS @ia0 diff --git a/crates/board/CHANGELOG.md b/crates/board/CHANGELOG.md index 713360b5..4705e11d 100644 --- a/crates/board/CHANGELOG.md +++ b/crates/board/CHANGELOG.md @@ -161,4 +161,4 @@ ## 0.1.0 - + diff --git a/crates/board/Cargo.lock b/crates/board/Cargo.lock index fc0ec255..ba82ed36 100644 --- a/crates/board/Cargo.lock +++ b/crates/board/Cargo.lock @@ -754,6 +754,7 @@ dependencies = [ "wasefire-applet-api", "wasefire-error", "wasefire-logger", + "wasefire-protocol", "wasefire-store", ] @@ -777,6 +778,14 @@ dependencies = [ name = "wasefire-one-of" version = "0.1.0-git" +[[package]] +name = "wasefire-protocol" +version = "0.2.0-git" +dependencies = [ + "wasefire-error", + "wasefire-wire", +] + [[package]] name = "wasefire-store" version = "0.3.0-git" @@ -784,6 +793,23 @@ dependencies = [ "wasefire-error", ] +[[package]] +name = "wasefire-wire" +version = "0.1.1-git" +dependencies = [ + "wasefire-error", + "wasefire-wire-derive", +] + +[[package]] +name = "wasefire-wire-derive" +version = "0.1.1-git" +dependencies = [ + "proc-macro2", + "quote", + "syn 2.0.66", +] + [[package]] name = "zeroize" version = "1.7.0" diff --git a/crates/board/Cargo.toml b/crates/board/Cargo.toml index ee104808..ed779bd6 100644 --- a/crates/board/Cargo.toml +++ b/crates/board/Cargo.toml @@ -36,6 +36,7 @@ usb-device = { version = "0.3.2", default-features = false, optional = true } usbd-serial = { version = "0.2.2", default-features = false, optional = true } wasefire-error = { version = "0.1.2-git", path = "../error" } wasefire-logger = { version = "0.1.6-git", path = "../logger" } +wasefire-protocol = { version = "0.2.0-git", path = "../protocol" } wasefire-store = { version = "0.3.0-git", path = "../store", optional = true } [dependencies.wasefire-applet-api] diff --git a/crates/board/src/applet.rs b/crates/board/src/applet.rs index 3015e264..e14d0667 100644 --- a/crates/board/src/applet.rs +++ b/crates/board/src/applet.rs @@ -14,6 +14,8 @@ //! Applet interface. +use wasefire_protocol::applet::ExitStatus; + use crate::Error; /// Applet interface. @@ -45,4 +47,12 @@ pub trait Api: Send { /// Implementations should make sure that if the platform reboots before this call, the /// persisted applet is empty. fn finish() -> Result<(), Error>; + + /// Notifies an applet start. + fn notify_start() {} + + /// Notifies an applet exit. + fn notify_exit(status: ExitStatus) { + let _ = status; + } } diff --git a/crates/cli-tools/CHANGELOG.md b/crates/cli-tools/CHANGELOG.md index 90592e34..94ac1661 100644 --- a/crates/cli-tools/CHANGELOG.md +++ b/crates/cli-tools/CHANGELOG.md @@ -10,6 +10,10 @@ ### Minor +- Add `action::PlatformInfo` to print platform serial and version +- Add `cmd::spawn()` for more control on command execution +- Add `fs::remove_dir_all()` to remove a directory recursively +- Add `fs::rename()` to rename a file - Add `cargo` and `changelog` modules and features - Handle more errors during platform discovery and `action::PlatformReboot` - Extend `fs::write()` first parameter to set the `OpenOptions` too @@ -35,4 +39,4 @@ ## 0.1.0 - + diff --git a/crates/cli-tools/Cargo.lock b/crates/cli-tools/Cargo.lock index 42c5af53..f9201025 100644 --- a/crates/cli-tools/Cargo.lock +++ b/crates/cli-tools/Cargo.lock @@ -570,6 +570,7 @@ dependencies = [ "derive-where", "wasefire-error", "wasefire-logger", + "wasefire-protocol", "wasefire-store", ] diff --git a/crates/cli-tools/src/action.rs b/crates/cli-tools/src/action.rs index 32a3600a..c3c67c52 100644 --- a/crates/cli-tools/src/action.rs +++ b/crates/cli-tools/src/action.rs @@ -112,10 +112,7 @@ pub struct AppletExitStatus { impl AppletExitStatus { fn print(status: Option) { match status { - Some(applet::ExitStatus::Exit) => println!("The applet exited."), - Some(applet::ExitStatus::Abort) => println!("The applet aborted."), - Some(applet::ExitStatus::Trap) => println!("The applet trapped."), - Some(applet::ExitStatus::Kill) => println!("The applet was killed."), + Some(status) => println!("{status}."), None => println!("The applet is still running."), } } @@ -286,6 +283,22 @@ impl PlatformList { } } +/// Prints the informations of a platform. +#[derive(clap::Args)] +pub struct PlatformInfo {} + +impl PlatformInfo { + pub async fn run(self, connection: &mut dyn Connection) -> Result<()> { + let PlatformInfo {} = self; + let info = connection.call::(()).await?; + let serial = protocol::Hex(info.get().serial.to_vec()); + let version = protocol::Hex(info.get().version.to_vec()); + println!(" serial: {serial}"); + println!("version: {version}"); + Ok(()) + } +} + /// Updates a platform. #[derive(clap::Args)] pub struct PlatformUpdate { diff --git a/crates/cli-tools/src/cmd.rs b/crates/cli-tools/src/cmd.rs index 864eda1d..b0c07b0a 100644 --- a/crates/cli-tools/src/cmd.rs +++ b/crates/cli-tools/src/cmd.rs @@ -18,12 +18,17 @@ use std::os::unix::process::CommandExt; use std::process::Output; use anyhow::{ensure, Context, Result}; -use tokio::process::Command; +use tokio::process::{Child, Command}; + +/// Spawns a command. +pub fn spawn(command: &mut Command) -> Result { + debug!("{:?}", command.as_std()); + Ok(command.spawn()?) +} /// Executes a command making sure it's successful. pub async fn execute(command: &mut Command) -> Result<()> { - debug!("{:?}", command.as_std()); - let code = command.spawn()?.wait().await?.code().context("no error code")?; + let code = spawn(command)?.wait().await?.code().context("no error code")?; ensure!(code == 0, "failed with code {code}"); Ok(()) } diff --git a/crates/cli-tools/src/fs.rs b/crates/cli-tools/src/fs.rs index 932d5c1e..38092c53 100644 --- a/crates/cli-tools/src/fs.rs +++ b/crates/cli-tools/src/fs.rs @@ -139,12 +139,28 @@ pub async fn read_stdin() -> Result> { Ok(data) } +pub async fn remove_dir_all(path: impl AsRef) -> Result<()> { + let name = path.as_ref().display(); + debug!("rm -r {name:?}"); + tokio::fs::remove_dir_all(path.as_ref()).await.with_context(|| format!("removing {name}")) +} + pub async fn remove_file(path: impl AsRef) -> Result<()> { let name = path.as_ref().display(); debug!("rm {name:?}"); tokio::fs::remove_file(path.as_ref()).await.with_context(|| format!("removing {name}")) } +pub async fn rename(from: impl AsRef, to: impl AsRef) -> Result<()> { + let src = from.as_ref().display(); + let dst = to.as_ref().display(); + create_parent(to.as_ref()).await?; + debug!("mv {src:?} {dst:?}"); + tokio::fs::rename(from.as_ref(), to.as_ref()) + .await + .with_context(|| format!("renaming {src} to {dst}")) +} + pub async fn touch(path: impl AsRef) -> Result<()> { if exists(path.as_ref()).await { return Ok(()); diff --git a/crates/cli/CHANGELOG.md b/crates/cli/CHANGELOG.md index 7318de10..5cd8dec6 100644 --- a/crates/cli/CHANGELOG.md +++ b/crates/cli/CHANGELOG.md @@ -9,6 +9,8 @@ ### Minor +- Add `platform-info` to print platform serial and version +- Add `host` to start a host platform - Support `RUST_LOG` to control logging - Add `platform-lock` to lock a platform protocol - Add `applet-exit-status` to get an applet exit status @@ -45,4 +47,4 @@ ## 0.1.0 - + diff --git a/crates/cli/Cargo.lock b/crates/cli/Cargo.lock index 14431d6a..cd0abd03 100644 --- a/crates/cli/Cargo.lock +++ b/crates/cli/Cargo.lock @@ -777,6 +777,7 @@ dependencies = [ "derive-where", "wasefire-error", "wasefire-logger", + "wasefire-protocol", "wasefire-store", ] diff --git a/crates/cli/Cargo.toml b/crates/cli/Cargo.toml index b7518146..edaf1c2c 100644 --- a/crates/cli/Cargo.toml +++ b/crates/cli/Cargo.toml @@ -11,6 +11,9 @@ include = ["/LICENSE", "/src/"] keywords = ["cli", "embedded", "framework", "wasm"] categories = ["command-line-utilities", "embedded", "wasm"] +[package.metadata.docs.rs] +features = ["_dev"] + [[bin]] name = "wasefire" path = "src/main.rs" @@ -27,6 +30,9 @@ version = "1.40.0" default-features = false features = ["macros", "parking_lot", "rt", "rt-multi-thread"] +[features] +_dev = [] + [lints] clippy.unit-arg = "allow" rust.unreachable-pub = "warn" diff --git a/crates/cli/src/main.rs b/crates/cli/src/main.rs index a4d44d0b..1da6aac9 100644 --- a/crates/cli/src/main.rs +++ b/crates/cli/src/main.rs @@ -12,14 +12,17 @@ // See the License for the specific language governing permissions and // limitations under the License. +#![feature(never_type)] + use std::fs::File; use std::io::Write; use std::path::{Path, PathBuf}; -use anyhow::{bail, Result}; +use anyhow::{bail, Context, Result}; use clap::{CommandFactory, Parser, ValueHint}; use clap_complete::Shell; -use wasefire_cli_tools::{action, fs}; +use tokio::process::Command; +use wasefire_cli_tools::{action, cmd, fs}; #[derive(Parser)] #[command(name = "wasefire", version, about)] @@ -83,6 +86,17 @@ enum Action { action: action::AppletRpc, }, + /// Starts a host platform. + Host(Host), + + #[group(id = "Action::PlatformInfo")] + PlatformInfo { + #[command(flatten)] + options: action::ConnectionOptions, + #[command(flatten)] + action: action::PlatformInfo, + }, + PlatformList(action::PlatformList), /// Prints the platform update metadata (possibly binary output). @@ -140,6 +154,27 @@ enum AppletInstallCommand { }, } +#[derive(clap::Args)] +struct Host { + /// Path of the directory containing the host platform files. + /// + /// Such a directory may contain: + /// - `applet.bin` the persistent applet + /// - `platform.bin` the platform code + /// - `storage.bin` the persistent storage + /// - `uart0` the UNIX socket for the UART + /// - `web` the web interface assets + /// + /// If the platform code is missing (including if the directory does not exist), a default + /// platform code is created and started. + #[arg(long, default_value = "wasefire/host", value_hint = ValueHint::DirPath)] + dir: PathBuf, + + /// Arguments to forward to the runner. + #[arg(trailing_var_arg = true, allow_hyphen_values = true)] + args: Vec, +} + #[derive(clap::Args)] struct Completion { /// Generates a completion file for this shell (tries to guess by default). @@ -150,6 +185,38 @@ struct Completion { output: PathBuf, } +impl Host { + async fn run(&self) -> Result { + let bin = self.dir.join("platform.bin"); + #[cfg(feature = "_dev")] + if !fs::exists(&bin).await { + let bundle = "target/wasefire/platform.bin"; + anyhow::ensure!( + fs::exists(&bundle).await, + "Run `cargo xtask runner host bundle` first" + ); + fs::copy(bundle, &bin).await?; + } + #[cfg(not(feature = "_dev"))] + if !fs::exists(&bin).await { + fs::create_dir_all(&self.dir).await?; + static HOST_PLATFORM: &[u8] = include_bytes!(env!("WASEFIRE_HOST_PLATFORM")); + let mut params = fs::WriteParams::new(&bin); + params.options().write(true).create_new(true).mode(0o777); + fs::write(params, HOST_PLATFORM).await?; + } + loop { + let mut host = Command::new(&bin); + host.arg(&self.dir); + host.args(&self.args); + let code = cmd::spawn(&mut host)?.wait().await?.code().context("no error code")?; + if code != 0 { + std::process::exit(code); + } + } + } +} + impl Completion { async fn run(&self) -> Result<()> { let shell = match self.shell.or_else(Shell::from_env) { @@ -195,6 +262,8 @@ async fn main() -> Result<()> { action.run(&mut options.connect().await?).await } Action::AppletRpc { options, action } => action.run(&mut options.connect().await?).await, + Action::Host(x) => x.run().await?, + Action::PlatformInfo { options, action } => action.run(&mut options.connect().await?).await, Action::PlatformList(x) => x.run().await, Action::PlatformUpdateMetadata { options } => { let metadata = action::PlatformUpdate::metadata(&mut options.connect().await?).await?; diff --git a/crates/cli/test.sh b/crates/cli/test.sh index cee477e7..2fba149b 100755 --- a/crates/cli/test.sh +++ b/crates/cli/test.sh @@ -19,4 +19,4 @@ set -e test_helper -cargo test --bin=wasefire +cargo test --bin=wasefire --features=_dev diff --git a/crates/interpreter/tests/spec.rs b/crates/interpreter/tests/spec.rs index aa5667dc..d63c544f 100644 --- a/crates/interpreter/tests/spec.rs +++ b/crates/interpreter/tests/spec.rs @@ -24,7 +24,7 @@ use wast::lexer::Lexer; use wast::token::Id; use wast::{parser, QuoteWat, Wast, WastArg, WastDirective, WastExecute, WastInvoke, WastRet, Wat}; -fn test(repo: &str, name: &str) { +fn test(repo: &str, name: &str, skip: usize) { let path = format!("../../third_party/WebAssembly/{repo}/test/core/{name}.wast"); let content = std::fs::read_to_string(path).unwrap(); let mut lexer = Lexer::new(&content); @@ -45,8 +45,8 @@ fn test(repo: &str, name: &str) { env.register_id(m.id, env.inst); } WastDirective::Wat(mut wat) => env.instantiate(name, &wat.encode().unwrap()), - WastDirective::AssertMalformed { module, .. } => assert_malformed(module), - WastDirective::AssertInvalid { module, .. } => assert_invalid(module), + WastDirective::AssertMalformed { module, .. } => assert_malformed(&mut env, module), + WastDirective::AssertInvalid { module, .. } => assert_invalid(&mut env, module), WastDirective::AssertReturn { exec, results, .. } => { assert_return(&mut env, exec, results) } @@ -58,6 +58,7 @@ fn test(repo: &str, name: &str) { _ => unimplemented!("{:?}", directive), } } + assert_eq!(env.skip, skip); } fn pool_size(name: &str) -> usize { @@ -125,11 +126,12 @@ impl Sup { } macro_rules! only_sup { - ($x:expr) => { + ($e:expr, $x:expr) => { match $x { Ok(x) => Ok(x), Err(Error::Unsupported(x)) => { eprintln!("skip unsupported {x:?}"); + $e.skip += 1; return; } Err(x) => Err(x), @@ -142,11 +144,12 @@ struct Env<'m> { store: Store<'m>, inst: Sup, map: HashMap, Sup>, + skip: usize, } impl<'m> Env<'m> { fn new(pool: &'m mut [u8]) -> Self { - Env { pool, store: Store::default(), inst: Sup::Uninit, map: HashMap::new() } + Env { pool, store: Store::default(), inst: Sup::Uninit, map: HashMap::new(), skip: 0 } } fn alloc(&mut self, size: usize) -> &'m mut [u8] { @@ -290,7 +293,7 @@ fn spectest() -> Vec { } fn assert_return(env: &mut Env, exec: WastExecute, expected: Vec) { - let actual = only_sup!(wast_execute(env, exec)).unwrap(); + let actual = only_sup!(env, wast_execute(env, exec)).unwrap(); assert_eq!(actual.len(), expected.len()); for (actual, expected) in actual.into_iter().zip(expected.into_iter()) { use wast::core::HeapType; @@ -328,30 +331,30 @@ fn assert_return(env: &mut Env, exec: WastExecute, expected: Vec) { } fn assert_trap(env: &mut Env, exec: WastExecute) { - assert_eq!(only_sup!(wast_execute(env, exec)), Err(Error::Trap)); + assert_eq!(only_sup!(env, wast_execute(env, exec)), Err(Error::Trap)); } fn assert_invoke(env: &mut Env, invoke: WastInvoke) { - assert_eq!(only_sup!(wast_invoke(env, invoke)), Ok(Vec::new())); + assert_eq!(only_sup!(env, wast_invoke(env, invoke)), Ok(Vec::new())); } -fn assert_malformed(mut wat: QuoteWat) { +fn assert_malformed(env: &mut Env, mut wat: QuoteWat) { if let Ok(wasm) = wat.encode() { - assert_eq!(only_sup!(Module::new(&wasm)).err(), Some(Error::Invalid)); + assert_eq!(only_sup!(env, Module::new(&wasm)).err(), Some(Error::Invalid)); } } -fn assert_invalid(mut wat: QuoteWat) { +fn assert_invalid(env: &mut Env, mut wat: QuoteWat) { let wasm = wat.encode().unwrap(); - assert_eq!(only_sup!(Module::new(&wasm)).err(), Some(Error::Invalid)); + assert_eq!(only_sup!(env, Module::new(&wasm)).err(), Some(Error::Invalid)); } fn assert_exhaustion(env: &mut Env, call: WastInvoke) { - assert_eq!(only_sup!(wast_invoke(env, call)), Err(Error::Trap)); + assert_eq!(only_sup!(env, wast_invoke(env, call)), Err(Error::Trap)); } fn assert_unlinkable(env: &mut Env, mut wat: Wat) { - let inst = only_sup!(env.maybe_instantiate("", &wat.encode().unwrap())); + let inst = only_sup!(env, env.maybe_instantiate("", &wat.encode().unwrap())); assert_eq!(inst.err(), Some(Error::NotFound)); } @@ -406,20 +409,32 @@ fn wast_arg_core(core: WastArgCore) -> Val { } macro_rules! test { - ($(#[$m:meta])* $name: ident$(, $file: literal)?) => { - test!($(#[$m])* "spec", $name$(, $file)?); + ($(#[$m:meta])* $($repo:literal,)? $name:ident$(, $file:literal)?$(; $skip:literal)?) => { + test!(=1 {$(#[$m])*} [$($repo)?] $name [$($file)?] [$($skip)?]); }; - ($(#[$m:meta])* $repo: literal, $name: ident) => { - test!([1] $(#[$m])* $name [$repo $name]); + (=1 $meta:tt [] $name:ident $file:tt $skip:tt) => { + test!(=2 $meta "spec" $name $file $skip); }; - ($(#[$m:meta])* $repo: literal, $name: ident, $file: literal) => { - test!([1] $(#[$m])* $name [$repo $file]); + (=1 $meta:tt [$repo:literal] $name:ident $file:tt $skip:tt) => { + test!(=2 $meta $repo $name $file $skip); }; - ([1] $(#[$m:meta])* $name: ident [$repo: literal $($file: tt)*]) => { - #[test] $(#[$m])* fn $name() { test($repo, test!([2] $($file)*)); } + (=2 $meta:tt $repo:literal $name:ident [] $skip:tt) => { + test!(=3 $meta $repo $name $name $skip); }; - ([2] $file: ident) => { stringify!($file) }; - ([2] $file: literal) => { $file }; + (=2 $meta:tt $repo:literal $name:ident [$file:literal] $skip:tt) => { + test!(=3 $meta $repo $name $file $skip); + }; + (=3 $meta:tt $repo:literal $name:ident $file:tt []) => { + test!(=4 $meta $repo $name $file 0); + }; + (=3 $meta:tt $repo:literal $name:ident $file:tt [$skip:literal]) => { + test!(=4 $meta $repo $name $file $skip); + }; + (=4 {$(#[$m:meta])*} $repo:literal $name:ident $file:tt $skip:literal) => { + #[test] $(#[$m])* fn $name() { test($repo, test!(=5 $file), $skip); } + }; + (=5 $name:ident) => { stringify!($name) }; + (=5 $file:literal) => { $file }; } test!(address); @@ -487,7 +502,7 @@ test!(ref_is_null); test!(ref_null); test!(return_, "return"); test!(select); -test!(skip_stack_guard_page, "skip-stack-guard-page"); +test!(skip_stack_guard_page, "skip-stack-guard-page"; 10); test!(stack); test!(start); test!(store); diff --git a/crates/protocol-tokio/Cargo.lock b/crates/protocol-tokio/Cargo.lock index 9921aff5..0436d296 100644 --- a/crates/protocol-tokio/Cargo.lock +++ b/crates/protocol-tokio/Cargo.lock @@ -247,6 +247,7 @@ dependencies = [ "derive-where", "wasefire-error", "wasefire-logger", + "wasefire-protocol", "wasefire-store", ] diff --git a/crates/protocol-usb/Cargo.lock b/crates/protocol-usb/Cargo.lock index 5ef2cafd..5deabc8f 100644 --- a/crates/protocol-usb/Cargo.lock +++ b/crates/protocol-usb/Cargo.lock @@ -279,6 +279,7 @@ dependencies = [ "derive-where", "wasefire-error", "wasefire-logger", + "wasefire-protocol", "wasefire-store", ] diff --git a/crates/protocol/CHANGELOG.md b/crates/protocol/CHANGELOG.md index 33230580..9d68ef64 100644 --- a/crates/protocol/CHANGELOG.md +++ b/crates/protocol/CHANGELOG.md @@ -8,6 +8,7 @@ ### Minor +- Add `serde` feature - Add `PlatformLock` to lock a platform protocol - Add `AppletExitStatus` and `applet::ExitStatus` to get an applet exit status - Add `Applet{Install,Uninstall}` for applet management diff --git a/crates/protocol/Cargo.lock b/crates/protocol/Cargo.lock index 13b50f5b..b4f13651 100644 --- a/crates/protocol/Cargo.lock +++ b/crates/protocol/Cargo.lock @@ -46,11 +46,31 @@ dependencies = [ "proc-macro2", ] +[[package]] +name = "serde" +version = "1.0.213" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "3ea7893ff5e2466df8d720bb615088341b295f849602c6956047f8f80f0e9bc1" +dependencies = [ + "serde_derive", +] + +[[package]] +name = "serde_derive" +version = "1.0.213" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "7e85ad2009c50b58e87caa8cd6dac16bdf511bbfb7af6c33df902396aa480fa5" +dependencies = [ + "proc-macro2", + "quote", + "syn", +] + [[package]] name = "syn" -version = "2.0.66" +version = "2.0.85" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "c42f3f41a2de00b01c0aaad383c5a45241efc8b2d1eda5661812fda5f3cdcff5" +checksum = "5023162dfcd14ef8f32034d8bcd4cc5ddc61ef7a247c024a33e24e1f24d21b56" dependencies = [ "proc-macro2", "quote", @@ -75,6 +95,7 @@ name = "wasefire-protocol" version = "0.2.0-git" dependencies = [ "anyhow", + "serde", "wasefire-error", "wasefire-wire", ] diff --git a/crates/protocol/Cargo.toml b/crates/protocol/Cargo.toml index 6fbc6baf..f097b4fe 100644 --- a/crates/protocol/Cargo.toml +++ b/crates/protocol/Cargo.toml @@ -16,6 +16,7 @@ features = ["device", "host"] [dependencies] anyhow = { version = "1.0.86", default-features = false, features = ["std"], optional = true } +serde = { version = "1.0.202", default-features = false, features = ["derive"], optional = true } wasefire-error = { version = "0.1.2-git", path = "../error" } wasefire-wire = { version = "0.1.1-git", path = "../wire" } @@ -24,6 +25,7 @@ _descriptor = [] _exhaustive = [] device = [] host = ["dep:anyhow", "wasefire-error/std"] +serde = ["dep:serde"] [lints] clippy.unit-arg = "allow" diff --git a/crates/protocol/src/applet.rs b/crates/protocol/src/applet.rs index c47f2003..87c880a1 100644 --- a/crates/protocol/src/applet.rs +++ b/crates/protocol/src/applet.rs @@ -12,6 +12,8 @@ // See the License for the specific language governing permissions and // limitations under the License. +use core::fmt::Display; + use wasefire_wire::Wire; #[derive(Debug, Wire)] @@ -29,7 +31,8 @@ pub struct Tunnel<'a> { pub delimiter: &'a [u8], } -#[derive(Debug, Copy, Clone, Wire)] +#[derive(Debug, Copy, Clone, PartialEq, Eq, PartialOrd, Ord, Wire)] +#[cfg_attr(feature = "serde", derive(serde::Serialize, serde::Deserialize))] pub enum ExitStatus { /// The applet exited successfully. Exit, @@ -43,3 +46,14 @@ pub enum ExitStatus { /// The applet was killed (e.g. it was uninstalled). Kill, } + +impl Display for ExitStatus { + fn fmt(&self, f: &mut core::fmt::Formatter<'_>) -> core::fmt::Result { + match self { + ExitStatus::Exit => write!(f, "The applet exited"), + ExitStatus::Abort => write!(f, "The applet aborted"), + ExitStatus::Trap => write!(f, "The applet trapped"), + ExitStatus::Kill => write!(f, "The applet was killed"), + } + } +} diff --git a/crates/protocol/src/lib.rs b/crates/protocol/src/lib.rs index 13938702..8eaf09b5 100644 --- a/crates/protocol/src/lib.rs +++ b/crates/protocol/src/lib.rs @@ -33,6 +33,7 @@ extern crate alloc; +#[cfg(any(feature = "device", feature = "host"))] use alloc::boxed::Box; #[cfg(feature = "host")] diff --git a/crates/protocol/test.sh b/crates/protocol/test.sh index c2cc6b92..df50aa83 100755 --- a/crates/protocol/test.sh +++ b/crates/protocol/test.sh @@ -23,3 +23,4 @@ cargo test --lib --features=_descriptor,host cargo test --lib --features=_descriptor,device cargo check --lib --features=host cargo check --lib --target=thumbv7em-none-eabi --features=device +cargo check --lib --features=serde diff --git a/crates/runner-host/Cargo.lock b/crates/runner-host/Cargo.lock index bb472092..657da41a 100644 --- a/crates/runner-host/Cargo.lock +++ b/crates/runner-host/Cargo.lock @@ -1248,6 +1248,7 @@ dependencies = [ "wasefire-interpreter", "wasefire-logger", "wasefire-one-of", + "wasefire-protocol", "wasefire-protocol-tokio", "wasefire-protocol-usb", "wasefire-scheduler", @@ -1849,6 +1850,7 @@ dependencies = [ "usbd-serial", "wasefire-error", "wasefire-logger", + "wasefire-protocol", "wasefire-store", ] @@ -1895,6 +1897,7 @@ version = "0.1.0-git" name = "wasefire-protocol" version = "0.2.0-git" dependencies = [ + "serde", "wasefire-error", "wasefire-wire", ] @@ -1985,6 +1988,7 @@ name = "web-common" version = "0.1.0" dependencies = [ "serde", + "wasefire-protocol", ] [[package]] @@ -1998,6 +2002,7 @@ dependencies = [ "tokio", "warp", "wasefire-logger", + "wasefire-protocol", "web-common", ] diff --git a/crates/runner-host/Cargo.toml b/crates/runner-host/Cargo.toml index 7da53997..d12b73f8 100644 --- a/crates/runner-host/Cargo.toml +++ b/crates/runner-host/Cargo.toml @@ -22,6 +22,7 @@ wasefire-error = { path = "../error" } wasefire-interpreter = { path = "../interpreter", optional = true } wasefire-logger = { path = "../logger" } wasefire-one-of = { path = "../one-of" } +wasefire-protocol = { path = "../protocol" } wasefire-protocol-tokio = { path = "../protocol-tokio", features = ["device"] } wasefire-protocol-usb = { path = "../protocol-usb", features = ["device", "std"] } wasefire-store = { path = "../store", features = ["std"] } diff --git a/crates/runner-host/crates/web-client/.gitignore b/crates/runner-host/crates/web-client/.gitignore index 178135c2..a6af0b26 100644 --- a/crates/runner-host/crates/web-client/.gitignore +++ b/crates/runner-host/crates/web-client/.gitignore @@ -1 +1,2 @@ -/dist/ +/web.tar.gz +/web/ diff --git a/crates/runner-host/crates/web-client/Cargo.lock b/crates/runner-host/crates/web-client/Cargo.lock index 3634c18c..cbb704d2 100644 --- a/crates/runner-host/crates/web-client/Cargo.lock +++ b/crates/runner-host/crates/web-client/Cargo.lock @@ -721,6 +721,26 @@ dependencies = [ "libc", ] +[[package]] +name = "num_enum" +version = "0.7.3" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "4e613fc340b2220f734a8595782c551f1250e969d87d3be1ae0579e8d4065179" +dependencies = [ + "num_enum_derive", +] + +[[package]] +name = "num_enum_derive" +version = "0.7.3" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "af1844ef2428cc3e1cb900be36181049ef3d3193c63e43026cfe202983b27a56" +dependencies = [ + "proc-macro2", + "quote", + "syn 2.0.66", +] + [[package]] name = "object" version = "0.36.0" @@ -1078,6 +1098,39 @@ version = "0.9.4" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "49874b5167b65d7193b8aba1567f5c7d93d001cafc34600cee003eda787e483f" +[[package]] +name = "wasefire-error" +version = "0.1.2-git" +dependencies = [ + "num_enum", +] + +[[package]] +name = "wasefire-protocol" +version = "0.2.0-git" +dependencies = [ + "serde", + "wasefire-error", + "wasefire-wire", +] + +[[package]] +name = "wasefire-wire" +version = "0.1.1-git" +dependencies = [ + "wasefire-error", + "wasefire-wire-derive", +] + +[[package]] +name = "wasefire-wire-derive" +version = "0.1.1-git" +dependencies = [ + "proc-macro2", + "quote", + "syn 2.0.66", +] + [[package]] name = "wasi" version = "0.11.0+wasi-snapshot-preview1" @@ -1181,6 +1234,7 @@ name = "web-common" version = "0.1.0" dependencies = [ "serde", + "wasefire-protocol", ] [[package]] diff --git a/crates/runner-host/crates/web-client/Makefile b/crates/runner-host/crates/web-client/Makefile new file mode 100644 index 00000000..67f3c0c4 --- /dev/null +++ b/crates/runner-host/crates/web-client/Makefile @@ -0,0 +1,18 @@ +# Copyright 2024 Google LLC +# +# Licensed under the Apache License, Version 2.0 (the "License"); +# you may not use this file except in compliance with the License. +# You may obtain a copy of the License at +# +# http://www.apache.org/licenses/LICENSE-2.0 +# +# Unless required by applicable law or agreed to in writing, software +# distributed under the License is distributed on an "AS IS" BASIS, +# WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. +# See the License for the specific language governing permissions and +# limitations under the License. + +WRAPPER=../../../../scripts/wrapper.sh +web.tar.gz: $(WRAPPER) Cargo.* index.html $(shell find src -type f) + $(WRAPPER) trunk build --release --dist=web + tar czf $@ web diff --git a/crates/runner-host/crates/web-client/src/console.rs b/crates/runner-host/crates/web-client/src/console.rs index bc9b0bc3..ea552f2a 100644 --- a/crates/runner-host/crates/web-client/src/console.rs +++ b/crates/runner-host/crates/web-client/src/console.rs @@ -58,6 +58,12 @@ pub fn console(Props { id, command_state, on_new_console_msg }: &Props) -> Html Command::Log { message } => { history.push(format!("[recv]: {message}")); } + Command::Start => { + history.push("Applet running".to_string()); + } + Command::Exit { status } => { + history.push(format!("{status}")); + } Command::Disconnected => { history.push("Disconnected from runner".to_string()); button_enabled.set(false); diff --git a/crates/runner-host/crates/web-common/Cargo.lock b/crates/runner-host/crates/web-common/Cargo.lock index 99b2dbe6..cdf5578c 100644 --- a/crates/runner-host/crates/web-common/Cargo.lock +++ b/crates/runner-host/crates/web-common/Cargo.lock @@ -8,6 +8,26 @@ version = "1.0.11" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "49f1f14873335454500d59611f1cf4a4b0f786f9ac11f4312a78e4cf2566695b" +[[package]] +name = "num_enum" +version = "0.7.3" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "4e613fc340b2220f734a8595782c551f1250e969d87d3be1ae0579e8d4065179" +dependencies = [ + "num_enum_derive", +] + +[[package]] +name = "num_enum_derive" +version = "0.7.3" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "af1844ef2428cc3e1cb900be36181049ef3d3193c63e43026cfe202983b27a56" +dependencies = [ + "proc-macro2", + "quote", + "syn", +] + [[package]] name = "proc-macro2" version = "1.0.83" @@ -80,10 +100,44 @@ version = "1.0.12" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "3354b9ac3fae1ff6755cb6db53683adb661634f67557942dea4facebec0fee4b" +[[package]] +name = "wasefire-error" +version = "0.1.2-git" +dependencies = [ + "num_enum", +] + +[[package]] +name = "wasefire-protocol" +version = "0.2.0-git" +dependencies = [ + "serde", + "wasefire-error", + "wasefire-wire", +] + +[[package]] +name = "wasefire-wire" +version = "0.1.1-git" +dependencies = [ + "wasefire-error", + "wasefire-wire-derive", +] + +[[package]] +name = "wasefire-wire-derive" +version = "0.1.1-git" +dependencies = [ + "proc-macro2", + "quote", + "syn", +] + [[package]] name = "web-common" version = "0.1.0" dependencies = [ "serde", "serde_json", + "wasefire-protocol", ] diff --git a/crates/runner-host/crates/web-common/Cargo.toml b/crates/runner-host/crates/web-common/Cargo.toml index 397a678b..3b37027e 100644 --- a/crates/runner-host/crates/web-common/Cargo.toml +++ b/crates/runner-host/crates/web-common/Cargo.toml @@ -8,6 +8,7 @@ edition = "2021" [dependencies] serde = { version = "1.0.202", features = ["derive"] } +wasefire-protocol = { path = "../../../protocol", features = ["serde"] } [dev-dependencies] serde_json = "1.0.117" diff --git a/crates/runner-host/crates/web-common/src/lib.rs b/crates/runner-host/crates/web-common/src/lib.rs index 858fdd05..914e05ec 100644 --- a/crates/runner-host/crates/web-common/src/lib.rs +++ b/crates/runner-host/crates/web-common/src/lib.rs @@ -17,6 +17,7 @@ //! The web client plays the role of the hardware. The web server is the host runner. use serde::{Deserialize, Serialize}; +use wasefire_protocol::applet::ExitStatus; /// Events from the hardware. #[derive(Debug, Clone, PartialEq, Eq, PartialOrd, Ord, Serialize, Deserialize)] @@ -51,6 +52,12 @@ pub enum Command { /// Prints a debug message. Log { message: String }, + + /// Indicates that the applet started. + Start, + + /// Indicates that the applet exited. + Exit { status: ExitStatus }, } #[derive(Debug, Clone, PartialEq, Eq, PartialOrd, Ord, Serialize, Deserialize)] diff --git a/crates/runner-host/crates/web-server/Cargo.lock b/crates/runner-host/crates/web-server/Cargo.lock index ff30f0e3..2c1b77b9 100644 --- a/crates/runner-host/crates/web-server/Cargo.lock +++ b/crates/runner-host/crates/web-server/Cargo.lock @@ -509,6 +509,26 @@ dependencies = [ "windows-sys", ] +[[package]] +name = "num_enum" +version = "0.7.3" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "4e613fc340b2220f734a8595782c551f1250e969d87d3be1ae0579e8d4065179" +dependencies = [ + "num_enum_derive", +] + +[[package]] +name = "num_enum_derive" +version = "0.7.3" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "af1844ef2428cc3e1cb900be36181049ef3d3193c63e43026cfe202983b27a56" +dependencies = [ + "proc-macro2", + "quote", + "syn", +] + [[package]] name = "object" version = "0.32.2" @@ -1038,10 +1058,43 @@ dependencies = [ "tracing", ] +[[package]] +name = "wasefire-error" +version = "0.1.2-git" +dependencies = [ + "num_enum", +] + [[package]] name = "wasefire-logger" version = "0.1.6-git" +[[package]] +name = "wasefire-protocol" +version = "0.2.0-git" +dependencies = [ + "serde", + "wasefire-error", + "wasefire-wire", +] + +[[package]] +name = "wasefire-wire" +version = "0.1.1-git" +dependencies = [ + "wasefire-error", + "wasefire-wire-derive", +] + +[[package]] +name = "wasefire-wire-derive" +version = "0.1.1-git" +dependencies = [ + "proc-macro2", + "quote", + "syn", +] + [[package]] name = "wasi" version = "0.11.0+wasi-snapshot-preview1" @@ -1053,6 +1106,7 @@ name = "web-common" version = "0.1.0" dependencies = [ "serde", + "wasefire-protocol", ] [[package]] @@ -1066,6 +1120,7 @@ dependencies = [ "tokio", "warp", "wasefire-logger", + "wasefire-protocol", "web-common", ] diff --git a/crates/runner-host/crates/web-server/Cargo.toml b/crates/runner-host/crates/web-server/Cargo.toml index 00b617b8..8eb166e1 100644 --- a/crates/runner-host/crates/web-server/Cargo.toml +++ b/crates/runner-host/crates/web-server/Cargo.toml @@ -14,6 +14,7 @@ serde_json = "1.0.117" tokio = { version = "1.40.0", features = ["full", "rt-multi-thread", "sync"] } warp = "0.3.7" wasefire-logger = { path = "../../../logger" } +wasefire-protocol = { path = "../../../protocol" } web-common = { path = "../web-common" } [lints] diff --git a/crates/runner-host/crates/web-server/src/lib.rs b/crates/runner-host/crates/web-server/src/lib.rs index 9341961a..2a5acbb5 100644 --- a/crates/runner-host/crates/web-server/src/lib.rs +++ b/crates/runner-host/crates/web-server/src/lib.rs @@ -13,6 +13,7 @@ // limitations under the License. use std::net::SocketAddr; +use std::path::PathBuf; use std::sync::{Arc, Mutex}; use std::time::Duration; @@ -22,6 +23,7 @@ use tokio::sync::{mpsc, oneshot}; use warp::ws::{Message, WebSocket}; use warp::Filter; use wasefire_logger as log; +use wasefire_protocol::applet::ExitStatus; use web_common::{ButtonState, Command, Component}; #[derive(Debug, Copy, Clone)] @@ -35,11 +37,11 @@ pub struct Client { } impl Client { - pub async fn new(addr: SocketAddr, events: mpsc::Sender) -> Result { + pub async fn new(dir: PathBuf, addr: SocketAddr, events: mpsc::Sender) -> Result { let (sender, mut receiver) = oneshot::channel(); let client = Arc::new(Mutex::new(Some((sender, events)))); - let static_files = warp::fs::dir("crates/web-client/dist"); + let static_files = warp::fs::dir(dir); let ws = warp::path("board") .and(warp::ws()) .and(warp::any().map(move || client.clone())) @@ -73,6 +75,14 @@ impl Client { self.send(Command::Set { component_id: LED_ID, state }); } + pub fn start(&self) { + self.send(Command::Start); + } + + pub fn exit(&self, status: ExitStatus) { + self.send(Command::Exit { status }); + } + fn send(&self, command: Command) { if let Err(e) = self.sender.try_send(command) { log::warn!("Failed to send a command to the client: {:?}", e); diff --git a/crates/runner-host/src/board/applet.rs b/crates/runner-host/src/board/applet.rs index a4472372..1136c0a3 100644 --- a/crates/runner-host/src/board/applet.rs +++ b/crates/runner-host/src/board/applet.rs @@ -20,6 +20,7 @@ use wasefire_board_api::applet::Api; use wasefire_board_api::Error; use wasefire_cli_tools::fs; use wasefire_error::Code; +use wasefire_protocol::applet::ExitStatus; pub enum Impl {} @@ -39,9 +40,26 @@ impl Api for Impl { fn finish() -> Result<(), Error> { with_state(|x| Handle::current().block_on(x.finish())) } + + fn notify_start() { + crate::with_state(|state| { + if let Some(web) = &mut state.web { + web.start(); + } + }) + } + + fn notify_exit(status: ExitStatus) { + crate::with_state(|state| { + if let Some(web) = &mut state.web { + web.exit(status); + } + }) + } } -pub async fn init(path: PathBuf) { +pub async fn init() { + let path = crate::FLAGS.dir.join("applet.bin"); let applet = read(&path).await; *STATE.lock().unwrap() = Some(State { path, applet, update: None }); } diff --git a/crates/runner-host/src/board/platform.rs b/crates/runner-host/src/board/platform.rs index 0faa18a8..67378411 100644 --- a/crates/runner-host/src/board/platform.rs +++ b/crates/runner-host/src/board/platform.rs @@ -17,46 +17,29 @@ use std::borrow::Cow; use data_encoding::HEXLOWER_PERMISSIVE; use wasefire_board_api::platform::Api; use wasefire_board_api::Error; -use wasefire_error::Code; + +use crate::FLAGS; pub mod protocol; +mod update; pub enum Impl {} impl Api for Impl { type Protocol = protocol::Impl; - type Update = UpdateImpl; + type Update = update::Impl; fn serial() -> Cow<'static, [u8]> { - from_hex(option_env!("WASEFIRE_HOST_SERIAL")) + from_hex(FLAGS.serial.as_deref()) } fn version() -> Cow<'static, [u8]> { - from_hex(option_env!("WASEFIRE_HOST_VERSION")) + from_hex(FLAGS.version.as_deref()) } fn reboot() -> Result { - Err(Error::world(Code::NotImplemented)) - } -} - -pub enum UpdateImpl {} -impl wasefire_board_api::Support for UpdateImpl { - const SUPPORT: bool = false; -} -impl wasefire_board_api::platform::update::Api for UpdateImpl { - fn metadata() -> Result, Error> { - Err(Error::world(Code::NotImplemented)) - } - fn initialize(_dry_run: bool) -> Result<(), Error> { - Err(Error::world(Code::NotImplemented)) - } - fn process(_chunk: &[u8]) -> Result<(), Error> { - Err(Error::world(Code::NotImplemented)) - } - fn finalize() -> Result<(), Error> { - Err(Error::world(Code::NotImplemented)) + crate::cleanup::shutdown(0) } } diff --git a/crates/runner-host/src/board/platform/update.rs b/crates/runner-host/src/board/platform/update.rs new file mode 100644 index 00000000..b8cebf91 --- /dev/null +++ b/crates/runner-host/src/board/platform/update.rs @@ -0,0 +1,74 @@ +// Copyright 2024 Google LLC +// +// Licensed under the Apache License, Version 2.0 (the "License"); +// you may not use this file except in compliance with the License. +// You may obtain a copy of the License at +// +// http://www.apache.org/licenses/LICENSE-2.0 +// +// Unless required by applicable law or agreed to in writing, software +// distributed under the License is distributed on an "AS IS" BASIS, +// WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. +// See the License for the specific language governing permissions and +// limitations under the License. + +use std::sync::Mutex; + +use anyhow::Result; +use tokio::runtime::Handle; +use wasefire_board_api::platform::update::Api; +use wasefire_board_api::Supported; +use wasefire_cli_tools::fs; +use wasefire_error::{Code, Error}; + +pub enum Impl {} + +impl Supported for Impl {} + +impl Api for Impl { + fn metadata() -> Result, Error> { + Ok(Box::new([])) + } + + fn initialize(dry_run: bool) -> Result<(), Error> { + Ok(*STATE.lock().unwrap() = Some(State { dry_run, buffer: Vec::new() })) + } + + fn process(chunk: &[u8]) -> Result<(), Error> { + match &mut *STATE.lock().unwrap() { + None => Err(Error::user(Code::InvalidState)), + Some(state) => Ok(state.buffer.extend_from_slice(chunk)), + } + } + + fn finalize() -> Result<(), Error> { + match STATE.lock().unwrap().take() { + None => Err(Error::user(Code::InvalidState)), + Some(State { dry_run: true, .. }) => Ok(()), + Some(State { dry_run: false, buffer }) => { + Handle::current().block_on(write(&buffer)).map_err(|_| Error::world(0))?; + crate::cleanup::shutdown(0) + } + } + } +} + +static STATE: Mutex> = Mutex::new(None); + +struct State { + dry_run: bool, + buffer: Vec, +} + +async fn write(contents: &[u8]) -> Result<()> { + let tmp = crate::FLAGS.dir.join("platform.tmp"); + let bin = crate::FLAGS.dir.join("platform.bin"); + let mut params = fs::WriteParams::new(&tmp); + params.options().write(true).create_new(true).mode(0o777); + fs::write(params, contents).await?; + let web_dir = crate::FLAGS.dir.join("web"); + if fs::exists(&web_dir).await { + fs::remove_dir_all(&web_dir).await?; + } + fs::rename(tmp, bin).await +} diff --git a/crates/runner-host/src/board/uart.rs b/crates/runner-host/src/board/uart.rs index 159915b3..2b62c29b 100644 --- a/crates/runner-host/src/board/uart.rs +++ b/crates/runner-host/src/board/uart.rs @@ -35,9 +35,9 @@ impl Uarts { } pub fn init() { - const UART: &str = "../../target/wasefire/uart0"; - let _ = std::fs::remove_file(UART); - let listener = UnixListener::bind(UART).unwrap(); + let uart = crate::FLAGS.dir.join("uart0"); + let _ = std::fs::remove_file(&uart); + let listener = UnixListener::bind(&uart).unwrap(); tokio::spawn(async move { loop { match listener.accept().await { diff --git a/crates/runner-host/src/cleanup.rs b/crates/runner-host/src/cleanup.rs index e6e7f5f5..870eec43 100644 --- a/crates/runner-host/src/cleanup.rs +++ b/crates/runner-host/src/cleanup.rs @@ -22,6 +22,7 @@ pub fn push(cleanup: Cleanup) { } pub fn shutdown(status: i32) -> ! { + wasefire_logger::info!("Shutting down."); let cleanups = std::mem::take(&mut *CLEANUP.lock().unwrap()); for cleanup in cleanups { cleanup(); diff --git a/crates/runner-host/src/main.rs b/crates/runner-host/src/main.rs index 357d936e..327fcb3d 100644 --- a/crates/runner-host/src/main.rs +++ b/crates/runner-host/src/main.rs @@ -17,7 +17,7 @@ use std::net::SocketAddr; use std::path::PathBuf; -use std::sync::Mutex; +use std::sync::{LazyLock, Mutex}; use anyhow::Result; use clap::Parser; @@ -36,12 +36,14 @@ use crate::board::Board; mod board; mod cleanup; +mod web; exactly_one_of!["debug", "release"]; exactly_one_of!["native", "wasm"]; static STATE: Mutex> = Mutex::new(None); static RECEIVER: Mutex>>> = Mutex::new(None); +static FLAGS: LazyLock = LazyLock::new(Flags::parse); fn with_state(f: impl FnOnce(&mut board::State) -> R) -> R { f(STATE.lock().unwrap().as_mut().unwrap()) @@ -49,9 +51,8 @@ fn with_state(f: impl FnOnce(&mut board::State) -> R) -> R { #[derive(Parser)] struct Flags { - /// Directory containing files representing the flash. - #[arg(long, default_value = "../../target/wasefire")] - flash_dir: PathBuf, + /// Path of the directory containing the platform files. + dir: PathBuf, /// Transport to listen to for the platform protocol. #[arg(long, default_value = "usb")] @@ -83,6 +84,14 @@ struct Flags { /// Socket address to bind to when --interface=web (ignored otherwise). #[arg(long, default_value = "127.0.0.1:5000")] web_addr: SocketAddr, + + /// Platform version (in hexadecimal). + #[arg(long, default_value = option_env!("WASEFIRE_HOST_VERSION").unwrap_or_default())] + version: Option, + + /// Platform serial (in hexadecimal). + #[arg(long, default_value = option_env!("WASEFIRE_HOST_SERIAL").unwrap_or_default())] + serial: Option, } #[test] @@ -106,7 +115,7 @@ enum Interface { #[tokio::main] async fn main() -> Result<()> { env_logger::init(); - let flags = Flags::parse(); + LazyLock::force(&FLAGS); std::panic::set_hook(Box::new(|info| { eprintln!("{info}"); cleanup::shutdown(1) @@ -121,52 +130,34 @@ async fn main() -> Result<()> { }; cleanup::shutdown(128 + signal.as_raw_value()); }); - wasefire_cli_tools::fs::create_dir_all(&flags.flash_dir).await?; - let storage = flags.flash_dir.join("storage.bin"); + wasefire_cli_tools::fs::create_dir_all(&FLAGS.dir).await?; let options = FileOptions { word_size: 4, page_size: 4096, num_pages: 16 }; - let storage = Some(FileStorage::new(&storage, options)?); - board::applet::init(flags.flash_dir.join("applet.bin")).await; + let storage = Some(FileStorage::new(&FLAGS.dir.join("storage.bin"), options)?); + board::applet::init().await; let (sender, receiver) = channel(10); *RECEIVER.lock().unwrap() = Some(receiver); - let web = match flags.interface { + let web = match FLAGS.interface { Interface::Stdio => None, - Interface::Web => { - let (sender, mut receiver) = channel(10); - tokio::spawn(async move { - while let Some(event) = receiver.recv().await { - match event { - web_server::Event::Exit => cleanup::shutdown(0), - web_server::Event::Button { pressed } => { - with_state(|state| board::button::event(state, Some(pressed))); - } - } - } - }); - let mut trunk = tokio::process::Command::new("../../scripts/wrapper.sh"); - trunk.args(["trunk", "build", "--release", "crates/web-client/index.html"]); - wasefire_cli_tools::cmd::execute(&mut trunk).await?; - Some(web_server::Client::new(flags.web_addr, sender).await?) - } + Interface::Web => Some(web::init().await?), }; let push = { use wasefire_board_api::platform::protocol::Event; let sender = sender.clone(); move |event: Event| drop(sender.try_send(event.into())) }; - let protocol = match flags.protocol { - Protocol::Tcp => ProtocolState::Pipe(Pipe::new_tcp(flags.tcp_addr, push).await.unwrap()), + let protocol = match FLAGS.protocol { + Protocol::Tcp => ProtocolState::Pipe(Pipe::new_tcp(FLAGS.tcp_addr, push).await.unwrap()), Protocol::Unix => { - let pipe = Pipe::new_unix(&flags.unix_path, push).await.unwrap(); - let unix_path = flags.unix_path.clone(); - cleanup::push(Box::new(move || drop(std::fs::remove_file(unix_path)))); + let pipe = Pipe::new_unix(&FLAGS.unix_path, push).await.unwrap(); + cleanup::push(Box::new(move || drop(std::fs::remove_file(&FLAGS.unix_path)))); ProtocolState::Pipe(pipe) } Protocol::Usb => ProtocolState::Usb, }; let usb = board::usb::State::new( - &flags.usb_vid_pid, + &FLAGS.usb_vid_pid, matches!(protocol, ProtocolState::Usb), - flags.usb_serial, + FLAGS.usb_serial, ); *STATE.lock().unwrap() = Some(board::State { sender, @@ -181,7 +172,7 @@ async fn main() -> Result<()> { }); board::uart::Uarts::init(); board::usb::init()?; - if matches!(flags.interface, Interface::Stdio) { + if matches!(FLAGS.interface, Interface::Stdio) { tokio::task::spawn_blocking(|| { use std::io::BufRead; // The tokio::io::Stdin documentation recommends to use blocking IO in a dedicated @@ -201,7 +192,7 @@ async fn main() -> Result<()> { } }); } - println!("Board initialized. Starting scheduler."); + println!("Host platform running."); // Not sure why Rust doesn't figure out this can't return (maybe async). let _: ! = tokio::task::spawn_blocking(|| Scheduler::::run()).await?; } diff --git a/crates/runner-host/src/web.rs b/crates/runner-host/src/web.rs new file mode 100644 index 00000000..edc81f3c --- /dev/null +++ b/crates/runner-host/src/web.rs @@ -0,0 +1,52 @@ +// Copyright 2024 Google LLC +// +// Licensed under the Apache License, Version 2.0 (the "License"); +// you may not use this file except in compliance with the License. +// You may obtain a copy of the License at +// +// http://www.apache.org/licenses/LICENSE-2.0 +// +// Unless required by applicable law or agreed to in writing, software +// distributed under the License is distributed on an "AS IS" BASIS, +// WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. +// See the License for the specific language governing permissions and +// limitations under the License. + +use std::process::Stdio; + +use anyhow::{ensure, Result}; +use tokio::io::AsyncWriteExt as _; +use tokio::process::Command; +use tokio::sync::mpsc::channel; +use wasefire_cli_tools::{cmd, fs}; + +use crate::{board, cleanup, with_state, FLAGS}; + +pub async fn init() -> Result { + let (sender, mut receiver) = channel(10); + tokio::spawn(async move { + while let Some(event) = receiver.recv().await { + match event { + web_server::Event::Exit => cleanup::shutdown(130), // like SIGINT + web_server::Event::Button { pressed } => { + with_state(|state| board::button::event(state, Some(pressed))); + } + } + } + }); + let web_dir = FLAGS.dir.join("web"); + if !fs::exists(&web_dir).await { + let mut tar = Command::new("tar"); + tar.current_dir(&FLAGS.dir); + tar.arg("xz"); + tar.stdin(Stdio::piped()); + let mut tar = cmd::spawn(&mut tar)?; + let mut stdin = tar.stdin.take().unwrap(); + stdin.write_all(TARBALL).await?; + drop(stdin); + ensure!(tar.wait().await?.success(), "Extracting web-client tarball failed."); + } + web_server::Client::new(web_dir, FLAGS.web_addr, sender).await +} + +const TARBALL: &[u8] = include_bytes!("../crates/web-client/web.tar.gz"); diff --git a/crates/runner-host/test.sh b/crates/runner-host/test.sh index f7624706..3a3ddf9e 100755 --- a/crates/runner-host/test.sh +++ b/crates/runner-host/test.sh @@ -18,6 +18,7 @@ set -e . "$(git rev-parse --show-toplevel)"/scripts/test-helper.sh ensure_applet +( cd crates/web-client && make ) test_helper diff --git a/crates/runner-nordic/Cargo.lock b/crates/runner-nordic/Cargo.lock index fa0d208e..4595f8d7 100644 --- a/crates/runner-nordic/Cargo.lock +++ b/crates/runner-nordic/Cargo.lock @@ -1138,6 +1138,7 @@ dependencies = [ "wasefire-applet-api", "wasefire-error", "wasefire-logger", + "wasefire-protocol", "wasefire-store", ] diff --git a/crates/scheduler/CHANGELOG.md b/crates/scheduler/CHANGELOG.md index f9c034ee..c47f0521 100644 --- a/crates/scheduler/CHANGELOG.md +++ b/crates/scheduler/CHANGELOG.md @@ -8,6 +8,8 @@ ### Minor +- Exit applets when main exits with no registered callbacks +- Call the `applet::notify_{start,exit}()` hooks - Trap applets calling into host during init (except for debug printing) - Support `PlatformLock` protocol call - Support `AppletExitStatus` protocol call (the platform keeps running when the applet exits) diff --git a/crates/scheduler/Cargo.lock b/crates/scheduler/Cargo.lock index 99c4fc34..c7460dc2 100644 --- a/crates/scheduler/Cargo.lock +++ b/crates/scheduler/Cargo.lock @@ -769,6 +769,7 @@ dependencies = [ "wasefire-applet-api", "wasefire-error", "wasefire-logger", + "wasefire-protocol", "wasefire-store", ] diff --git a/crates/scheduler/src/applet.rs b/crates/scheduler/src/applet.rs index d5cd5134..15ccc015 100644 --- a/crates/scheduler/src/applet.rs +++ b/crates/scheduler/src/applet.rs @@ -208,6 +208,10 @@ impl Applet { self.handlers.get(&key) } + pub fn has_handlers(&self) -> bool { + !self.handlers.is_empty() + } + pub fn len(&self) -> usize { self.events.len() } diff --git a/crates/scheduler/src/lib.rs b/crates/scheduler/src/lib.rs index 5d25cd3b..ac7d0b34 100644 --- a/crates/scheduler/src/lib.rs +++ b/crates/scheduler/src/lib.rs @@ -313,6 +313,7 @@ impl Scheduler { None => return, }; log::info!("Stopping applet."); + as board::applet::Api>::notify_exit(status); applet.free(); self.applet = applet::Slot::Exited(status); #[cfg(feature = "native")] @@ -341,6 +342,7 @@ impl Scheduler { return Ok(()); } log::info!("Starting applet."); + as board::applet::Api>::notify_start(); self.applet = applet::Slot::Running(Applet::default()); #[cfg(not(feature = "unsafe-skip-validation"))] let module = Module::new(wasm)?; @@ -434,8 +436,13 @@ impl Scheduler { let call = match applet.store_mut().last_call() { Some(x) => x, None => { - // When main exits, we continue processing events. - let _ = self.process_event(); + if applet.has_handlers() { + // Continue processing events when main exits and at least one callback is + // registered. + let _ = self.process_event(); + } else { + self.stop_applet(ExitStatus::Exit); + } return; } }; diff --git a/crates/xtask/Cargo.lock b/crates/xtask/Cargo.lock index a65834ec..76d50f3f 100644 --- a/crates/xtask/Cargo.lock +++ b/crates/xtask/Cargo.lock @@ -1838,6 +1838,7 @@ dependencies = [ "derive-where", "wasefire-error", "wasefire-logger", + "wasefire-protocol", "wasefire-store", ] diff --git a/crates/xtask/src/main.rs b/crates/xtask/src/main.rs index 5f4d00f9..fe66b25f 100644 --- a/crates/xtask/src/main.rs +++ b/crates/xtask/src/main.rs @@ -190,6 +190,12 @@ struct RunnerOptions { #[clap(long)] version: Option, + /// Host platform serial. + /// + /// This is only used for the host runner. It must be an hexadecimal byte sequence. + #[clap(long)] + serial: Option, + /// Cargo no-default-features. #[clap(long)] no_default_features: bool, @@ -465,7 +471,12 @@ impl RunnerOptions { if self.name == "host" { if let Some(version) = &self.version { cargo.env("WASEFIRE_HOST_VERSION", version); - }; + } + if let Some(serial) = &self.serial { + cargo.env("WASEFIRE_HOST_SERIAL", serial); + } + cmd::execute(Command::new("make").current_dir("crates/runner-host/crates/web-client")) + .await?; } if self.name == "nordic" { rustflags.push(format!("-C link-arg=--defsym=RUNNER_SIDE={step}")); @@ -533,7 +544,7 @@ impl RunnerOptions { } } } - cargo.arg("--"); + cargo.args(["--", "../../target/wasefire"]); if std::env::var_os("CODESPACES").is_some() { log::warn!("Assuming runner --arg=--protocol=unix when running in a codespace."); cargo.arg("--protocol=unix"); @@ -600,10 +611,14 @@ impl RunnerOptions { } } if matches!(cmd, Some(RunnerCommand::Bundle)) { - let mut objcopy = wrap_command().await?; - objcopy.args(["rust-objcopy", "-O", "binary", &elf]); - objcopy.arg(format!("target/wasefire/platform{side}.bin")); - cmd::execute(&mut objcopy).await?; + let bundle = format!("target/wasefire/platform{side}.bin"); + if self.name == "host" { + fs::copy(elf, bundle).await?; + } else { + let mut objcopy = wrap_command().await?; + objcopy.args(["rust-objcopy", "-O", "binary", &elf, &bundle]); + cmd::execute(&mut objcopy).await?; + } if step < max_step { return Box::pin(self.execute(main, step + 1, cmd)).await; } diff --git a/examples/rust/protocol/host/Cargo.lock b/examples/rust/protocol/host/Cargo.lock index e25798fb..8e0f3a6c 100644 --- a/examples/rust/protocol/host/Cargo.lock +++ b/examples/rust/protocol/host/Cargo.lock @@ -549,6 +549,7 @@ dependencies = [ "derive-where", "wasefire-error", "wasefire-logger", + "wasefire-protocol", "wasefire-store", ] diff --git a/scripts/artifacts.sh b/scripts/artifacts.sh index a3c9d672..b73b2bb7 100755 --- a/scripts/artifacts.sh +++ b/scripts/artifacts.sh @@ -34,17 +34,23 @@ You may also download the provenance attestation and use the \`--bundle\` flag: [changelog]: https://github.com/google/wasefire/blob/main/docs/releases/$DATE.md EOF -i "Generate artifacts and artifacts.txt" -mkdir artifacts +x mkdir artifacts -i "Build the CLI for supported targets" +i "Build web-client once for all supported targets" +( cd crates/runner-host/crates/web-client && make ) + +i "Build the CLI for each supported target" TARGETS=' x86_64-unknown-linux-gnu ' -( cd crates/cli - for target in $TARGETS; do - x cargo build --release --target=$target - cp ../../target/$target/release/wasefire ../../artifacts/wasefire-$target - echo "artifacts/wasefire-$target#Wasefire CLI ($target)" >> ../../artifacts.txt - done -) +for target in $TARGETS; do + ( set -x + cargo build --manifest-path=crates/runner-host/Cargo.toml --release --target=$target \ + --features=debug,wasm + export WASEFIRE_HOST_PLATFORM=$PWD/target/$target/release/runner-host + cargo build --manifest-path=crates/cli/Cargo.toml --release --target=$target + ) + artifact=artifacts/wasefire-$target + cp target/$target/release/wasefire $artifact + echo "$artifact#Wasefire CLI ($target)" >> artifacts.txt +done diff --git a/scripts/wrapper.sh b/scripts/wrapper.sh index 7448b233..2f8f056c 100755 --- a/scripts/wrapper.sh +++ b/scripts/wrapper.sh @@ -35,11 +35,9 @@ run() { ensure_cargo() { local flags="$1@$2" - local locked=--locked { cargo install --list --root="$CARGO_ROOT" | grep -q "^$1 v$2:\$"; } && return - [ "$1" = trunk ] && locked= shift 2 - x cargo install $locked --root="$CARGO_ROOT" "$flags" "$@" + x cargo install --locked --root="$CARGO_ROOT" "$flags" "$@" } IS_CARGO=y @@ -55,7 +53,7 @@ case "$1" in probe-rs) ensure_cargo probe-rs-tools 0.24.0 ;; rust-objcopy|rust-size) ensure_cargo cargo-binutils 0.3.6 ;; taplo) ensure_cargo taplo-cli 0.9.3 ;; - trunk) ensure_cargo trunk 0.19.3 ;; + trunk) ensure_cargo trunk 0.21.1 ;; twiggy) ensure_cargo twiggy 0.7.0 ;; *) IS_CARGO=n ;; esac