From a45be57beba36f4612d96a185e30717c8895dbb3 Mon Sep 17 00:00:00 2001 From: Ahmed Tadde Date: Tue, 4 Feb 2025 13:10:05 -0500 Subject: [PATCH] refactor: some progress... still unable to run tests at this point Signed-off-by: Ahmed Tadde --- crates/wash/src/bin/wash.rs | 40 +- crates/wash/src/cli/app/mod.rs | 20 +- crates/wash/src/cli/appearance/spinner.rs | 2 +- crates/wash/src/cli/cmd/config/delete.rs | 6 +- crates/wash/src/cli/cmd/config/get.rs | 4 +- crates/wash/src/cli/cmd/config/mod.rs | 2 +- crates/wash/src/cli/cmd/config/put.rs | 4 +- crates/wash/src/cli/cmd/dev/deps.rs | 16 +- crates/wash/src/cli/cmd/dev/devloop.rs | 20 +- crates/wash/src/cli/cmd/dev/manifest.rs | 10 +- crates/wash/src/cli/cmd/dev/mod.rs | 25 +- crates/wash/src/cli/cmd/dev/session.rs | 10 +- crates/wash/src/cli/cmd/dev/wit.rs | 2 +- crates/wash/src/cli/cmd/link/del.rs | 15 +- crates/wash/src/cli/cmd/link/mod.rs | 4 +- crates/wash/src/cli/cmd/link/put.rs | 4 +- crates/wash/src/cli/cmd/link/query.rs | 4 +- crates/wash/src/cli/cmd/up.rs | 32 +- crates/wash/src/cli/cmd/wit/build.rs | 8 +- crates/wash/src/cli/cmd/wit/deps.rs | 8 +- crates/wash/src/cli/cmd/wit/mod.rs | 2 +- crates/wash/src/cli/cmd/wit/publish.rs | 4 +- crates/wash/src/cli/common/get_cmd.rs | 12 +- crates/wash/src/cli/common/label_cmd.rs | 2 +- crates/wash/src/cli/common/registry_cmd.rs | 10 +- crates/wash/src/cli/common/scale_cmd.rs | 2 +- crates/wash/src/cli/common/start_cmd.rs | 6 +- crates/wash/src/cli/common/stop_cmd.rs | 6 +- crates/wash/src/cli/common/update_cmd.rs | 2 +- crates/wash/src/cli/ctx/mod.rs | 12 +- crates/wash/src/cli/drain.rs | 6 +- crates/wash/src/cli/generate.rs | 8 +- crates/wash/src/cli/keys.rs | 6 +- crates/wash/src/cli/mod.rs | 32 +- crates/wash/src/cli/par.rs | 8 +- crates/wash/src/cli/plugin.rs | 2 +- crates/wash/src/lib.rs | 13 +- crates/wash/src/lib/cli/inspect.rs | 10 +- crates/wash/src/lib/cli/link.rs | 12 +- crates/wash/src/lib/cli/start.rs | 34 +- crates/wash/src/lib/parser/mod.rs | 45 +- crates/wash/src/lib/start/mod.rs | 4 +- crates/wash/src/lib/start/nats.rs | 8 +- crates/wash/src/lib/start/wadm.rs | 6 +- crates/wash/src/lib/start/wasmcloud.rs | 8 +- crates/wash/src/wit/deps.lock | 31 + crates/wash/src/wit/deps.toml | 2 + crates/wash/src/wit/deps/cli/command.wit | 7 + crates/wash/src/wit/deps/cli/environment.wit | 18 + crates/wash/src/wit/deps/cli/exit.wit | 4 + crates/wash/src/wit/deps/cli/imports.wit | 20 + crates/wash/src/wit/deps/cli/run.wit | 4 + crates/wash/src/wit/deps/cli/stdio.wit | 17 + crates/wash/src/wit/deps/cli/terminal.wit | 49 ++ .../src/wit/deps/clocks/monotonic-clock.wit | 45 ++ .../wash/src/wit/deps/clocks/wall-clock.wit | 42 ++ crates/wash/src/wit/deps/clocks/world.wit | 6 + .../wash/src/wit/deps/filesystem/preopens.wit | 8 + crates/wash/src/wit/deps/filesystem/types.wit | 634 ++++++++++++++++++ crates/wash/src/wit/deps/filesystem/world.wit | 6 + crates/wash/src/wit/deps/http/handler.wit | 43 ++ crates/wash/src/wit/deps/http/proxy.wit | 32 + crates/wash/src/wit/deps/http/types.wit | 570 ++++++++++++++++ crates/wash/src/wit/deps/io/error.wit | 34 + crates/wash/src/wit/deps/io/poll.wit | 41 ++ crates/wash/src/wit/deps/io/streams.wit | 262 ++++++++ crates/wash/src/wit/deps/io/world.wit | 6 + .../src/wit/deps/random/insecure-seed.wit | 25 + crates/wash/src/wit/deps/random/insecure.wit | 22 + crates/wash/src/wit/deps/random/random.wit | 26 + crates/wash/src/wit/deps/random/world.wit | 7 + .../src/wit/deps/sockets/instance-network.wit | 9 + .../src/wit/deps/sockets/ip-name-lookup.wit | 51 ++ crates/wash/src/wit/deps/sockets/network.wit | 145 ++++ .../wit/deps/sockets/tcp-create-socket.wit | 27 + crates/wash/src/wit/deps/sockets/tcp.wit | 353 ++++++++++ .../wit/deps/sockets/udp-create-socket.wit | 27 + crates/wash/src/wit/deps/sockets/udp.wit | 266 ++++++++ crates/wash/src/wit/deps/sockets/world.wit | 11 + crates/wash/src/wit/subcommand.wit | 39 ++ crates/wash/src/wit/world.wit | 17 + crates/wash/tests/app.rs | 56 ++ crates/wash/tests/common.rs | 16 + crates/wash/tests/common/mod.rs | 10 +- .../wash/tests/fixtures/hello_plugin_s.wasm | Bin 0 -> 660580 bytes crates/wash/tests/github_fetch_test.rs | 250 +++++++ .../tests/parser/files/folder/wasmcloud.toml | 17 + .../parser/files/minimal_rust_component.toml | 7 + .../minimal_rust_component_core_module.toml | 8 + .../files/minimal_rust_component_wasip1.toml | 8 + .../files/minimal_rust_component_wasip2.toml | 9 + .../wash/tests/parser/files/no_component.toml | 12 + .../wash/tests/parser/files/no_interface.toml | 12 + .../wash/tests/parser/files/no_provider.toml | 8 + .../wash/tests/parser/files/noconfig/.gitkeep | 0 crates/wash/tests/parser/files/random.txt | 1 + .../parser/files/rust_actor_custom_build.toml | 9 + .../tests/parser/files/rust_component.toml | 17 + .../files/rust_component_claims_metadata.toml | 22 + .../files/rust_component_with_revision.toml | 18 + .../files/rust_provider_claims_metadata.toml | 13 + .../parser/files/separate_project_paths.toml | 20 + crates/wash/tests/parser/files/tags.toml | 14 + .../tests/parser/files/tinygo_component.toml | 16 + .../parser/files/tinygo_component_module.toml | 17 + .../files/tinygo_component_scheduler_gc.toml | 17 + .../parser/files/withcargotoml/Cargo.toml | 11 + .../minimal_rust_component_with_cargo.toml | 5 + .../parser/files/withcargotoml/src/main.rs | 3 + crates/wash/tests/parser/main.rs | 563 ++++++++++++++++ crates/wash/tests/plugins.rs | 68 ++ .../tests/plugins/hello_plugin/.gitignore | 6 + .../tests/plugins/hello_plugin/Cargo.toml | 15 + .../tests/plugins/hello_plugin/src/lib.rs | 282 ++++++++ .../tests/plugins/hello_plugin/wasmcloud.lock | 30 + .../tests/plugins/hello_plugin/wasmcloud.toml | 7 + .../wit/deps/wasi-cli-0.2.0/package.wit | 159 +++++ .../wit/deps/wasi-clocks-0.2.0/package.wit | 29 + .../deps/wasi-filesystem-0.2.0/package.wit | 158 +++++ .../wit/deps/wasi-http-0.2.0/package.wit | 571 ++++++++++++++++ .../wit/deps/wasi-io-0.2.0/package.wit | 48 ++ .../wit/deps/wasi-random-0.2.0/package.wit | 18 + .../wit/deps/wasi-sockets-0.2.0/package.wit | 179 +++++ .../wit/deps/wasmcloud-wash-0.1.0/package.wit | 78 +++ .../tests/plugins/hello_plugin/wit/world.wit | 9 + crates/wash/tests/start_nats.rs | 104 +++ crates/wash/tests/start_wadm.rs | 71 ++ crates/wash/tests/start_wasmcloud.rs | 167 +++++ crates/wash/tests/wash_app.rs | 4 +- crates/wash/tests/wash_build.rs | 2 +- crates/wash/tests/wash_call.rs | 2 +- crates/wash/tests/wash_config.rs | 2 +- crates/wash/tests/wash_get.rs | 2 +- crates/wash/tests/wash_label.rs | 2 +- crates/wash/tests/wash_link.rs | 2 +- crates/wash/tests/wash_scale.rs | 2 +- crates/wash/tests/wash_secrets.rs | 2 +- crates/wash/tests/wash_stop.rs | 2 +- crates/wash/tests/wash_up.rs | 4 +- crates/wash/tests/wash_update.rs | 2 +- 140 files changed, 6306 insertions(+), 282 deletions(-) create mode 100644 crates/wash/src/wit/deps.lock create mode 100644 crates/wash/src/wit/deps.toml create mode 100644 crates/wash/src/wit/deps/cli/command.wit create mode 100644 crates/wash/src/wit/deps/cli/environment.wit create mode 100644 crates/wash/src/wit/deps/cli/exit.wit create mode 100644 crates/wash/src/wit/deps/cli/imports.wit create mode 100644 crates/wash/src/wit/deps/cli/run.wit create mode 100644 crates/wash/src/wit/deps/cli/stdio.wit create mode 100644 crates/wash/src/wit/deps/cli/terminal.wit create mode 100644 crates/wash/src/wit/deps/clocks/monotonic-clock.wit create mode 100644 crates/wash/src/wit/deps/clocks/wall-clock.wit create mode 100644 crates/wash/src/wit/deps/clocks/world.wit create mode 100644 crates/wash/src/wit/deps/filesystem/preopens.wit create mode 100644 crates/wash/src/wit/deps/filesystem/types.wit create mode 100644 crates/wash/src/wit/deps/filesystem/world.wit create mode 100644 crates/wash/src/wit/deps/http/handler.wit create mode 100644 crates/wash/src/wit/deps/http/proxy.wit create mode 100644 crates/wash/src/wit/deps/http/types.wit create mode 100644 crates/wash/src/wit/deps/io/error.wit create mode 100644 crates/wash/src/wit/deps/io/poll.wit create mode 100644 crates/wash/src/wit/deps/io/streams.wit create mode 100644 crates/wash/src/wit/deps/io/world.wit create mode 100644 crates/wash/src/wit/deps/random/insecure-seed.wit create mode 100644 crates/wash/src/wit/deps/random/insecure.wit create mode 100644 crates/wash/src/wit/deps/random/random.wit create mode 100644 crates/wash/src/wit/deps/random/world.wit create mode 100644 crates/wash/src/wit/deps/sockets/instance-network.wit create mode 100644 crates/wash/src/wit/deps/sockets/ip-name-lookup.wit create mode 100644 crates/wash/src/wit/deps/sockets/network.wit create mode 100644 crates/wash/src/wit/deps/sockets/tcp-create-socket.wit create mode 100644 crates/wash/src/wit/deps/sockets/tcp.wit create mode 100644 crates/wash/src/wit/deps/sockets/udp-create-socket.wit create mode 100644 crates/wash/src/wit/deps/sockets/udp.wit create mode 100644 crates/wash/src/wit/deps/sockets/world.wit create mode 100644 crates/wash/src/wit/subcommand.wit create mode 100644 crates/wash/src/wit/world.wit create mode 100644 crates/wash/tests/app.rs create mode 100644 crates/wash/tests/common.rs create mode 100644 crates/wash/tests/fixtures/hello_plugin_s.wasm create mode 100644 crates/wash/tests/github_fetch_test.rs create mode 100644 crates/wash/tests/parser/files/folder/wasmcloud.toml create mode 100644 crates/wash/tests/parser/files/minimal_rust_component.toml create mode 100644 crates/wash/tests/parser/files/minimal_rust_component_core_module.toml create mode 100644 crates/wash/tests/parser/files/minimal_rust_component_wasip1.toml create mode 100644 crates/wash/tests/parser/files/minimal_rust_component_wasip2.toml create mode 100644 crates/wash/tests/parser/files/no_component.toml create mode 100644 crates/wash/tests/parser/files/no_interface.toml create mode 100644 crates/wash/tests/parser/files/no_provider.toml create mode 100644 crates/wash/tests/parser/files/noconfig/.gitkeep create mode 100644 crates/wash/tests/parser/files/random.txt create mode 100644 crates/wash/tests/parser/files/rust_actor_custom_build.toml create mode 100644 crates/wash/tests/parser/files/rust_component.toml create mode 100644 crates/wash/tests/parser/files/rust_component_claims_metadata.toml create mode 100644 crates/wash/tests/parser/files/rust_component_with_revision.toml create mode 100644 crates/wash/tests/parser/files/rust_provider_claims_metadata.toml create mode 100644 crates/wash/tests/parser/files/separate_project_paths.toml create mode 100644 crates/wash/tests/parser/files/tags.toml create mode 100644 crates/wash/tests/parser/files/tinygo_component.toml create mode 100644 crates/wash/tests/parser/files/tinygo_component_module.toml create mode 100644 crates/wash/tests/parser/files/tinygo_component_scheduler_gc.toml create mode 100644 crates/wash/tests/parser/files/withcargotoml/Cargo.toml create mode 100644 crates/wash/tests/parser/files/withcargotoml/minimal_rust_component_with_cargo.toml create mode 100644 crates/wash/tests/parser/files/withcargotoml/src/main.rs create mode 100644 crates/wash/tests/parser/main.rs create mode 100644 crates/wash/tests/plugins.rs create mode 100644 crates/wash/tests/plugins/hello_plugin/.gitignore create mode 100644 crates/wash/tests/plugins/hello_plugin/Cargo.toml create mode 100644 crates/wash/tests/plugins/hello_plugin/src/lib.rs create mode 100644 crates/wash/tests/plugins/hello_plugin/wasmcloud.lock create mode 100644 crates/wash/tests/plugins/hello_plugin/wasmcloud.toml create mode 100644 crates/wash/tests/plugins/hello_plugin/wit/deps/wasi-cli-0.2.0/package.wit create mode 100644 crates/wash/tests/plugins/hello_plugin/wit/deps/wasi-clocks-0.2.0/package.wit create mode 100644 crates/wash/tests/plugins/hello_plugin/wit/deps/wasi-filesystem-0.2.0/package.wit create mode 100644 crates/wash/tests/plugins/hello_plugin/wit/deps/wasi-http-0.2.0/package.wit create mode 100644 crates/wash/tests/plugins/hello_plugin/wit/deps/wasi-io-0.2.0/package.wit create mode 100644 crates/wash/tests/plugins/hello_plugin/wit/deps/wasi-random-0.2.0/package.wit create mode 100644 crates/wash/tests/plugins/hello_plugin/wit/deps/wasi-sockets-0.2.0/package.wit create mode 100644 crates/wash/tests/plugins/hello_plugin/wit/deps/wasmcloud-wash-0.1.0/package.wit create mode 100644 crates/wash/tests/plugins/hello_plugin/wit/world.wit create mode 100644 crates/wash/tests/start_nats.rs create mode 100644 crates/wash/tests/start_wadm.rs create mode 100644 crates/wash/tests/start_wasmcloud.rs diff --git a/crates/wash/src/bin/wash.rs b/crates/wash/src/bin/wash.rs index fb8e145296..f4c0764e54 100644 --- a/crates/wash/src/bin/wash.rs +++ b/crates/wash/src/bin/wash.rs @@ -3,27 +3,27 @@ use std::fmt::{Display, Formatter}; use std::io::{stdout, BufWriter, Write}; use std::path::{Path, PathBuf}; +use crate::lib::cli::capture::{CaptureCommand, CaptureSubcommand}; +use crate::lib::cli::claims::ClaimsCliCommand; +use crate::lib::cli::get::GetCommand; +use crate::lib::cli::inspect::InspectCliCommand; +use crate::lib::cli::label::LabelHostCommand; +use crate::lib::cli::link::LinkCommand; +use crate::lib::cli::registry::{RegistryPullCommand, RegistryPushCommand}; +use crate::lib::cli::scale::ScaleCommand; +use crate::lib::cli::spy::SpyCommand; +use crate::lib::cli::start::StartCommand; +use crate::lib::cli::stop::StopCommand; +use crate::lib::cli::update::UpdateCommand; +use crate::lib::cli::{CommandOutput, OutputKind}; +use crate::lib::drain::Drain as DrainSelection; +use crate::lib::plugin::subcommand::{DirMapping, SubcommandRunner}; use anyhow::bail; use clap::{self, Arg, Command, FromArgMatches, Parser, Subcommand}; use console::style; use crossterm::style::Stylize; use serde_json::json; use tracing_subscriber::EnvFilter; -use wash_lib::cli::capture::{CaptureCommand, CaptureSubcommand}; -use wash_lib::cli::claims::ClaimsCliCommand; -use wash_lib::cli::get::GetCommand; -use wash_lib::cli::inspect::InspectCliCommand; -use wash_lib::cli::label::LabelHostCommand; -use wash_lib::cli::link::LinkCommand; -use wash_lib::cli::registry::{RegistryPullCommand, RegistryPushCommand}; -use wash_lib::cli::scale::ScaleCommand; -use wash_lib::cli::spy::SpyCommand; -use wash_lib::cli::start::StartCommand; -use wash_lib::cli::stop::StopCommand; -use wash_lib::cli::update::UpdateCommand; -use wash_lib::cli::{CommandOutput, OutputKind}; -use wash_lib::drain::Drain as DrainSelection; -use wash_lib::plugin::subcommand::{DirMapping, SubcommandRunner}; use wash_cli::app::{self, AppCliCommand}; use wash_cli::build::{self, BuildCommand}; @@ -548,13 +548,13 @@ async fn main() { if !cli.experimental { experimental_error_message("capture") } else if let Some(CaptureSubcommand::Replay(cmd)) = capture_cli.replay { - wash_lib::cli::capture::handle_replay_command(cmd).await + crate::lib::cli::capture::handle_replay_command(cmd).await } else { - wash_lib::cli::capture::handle_command(capture_cli).await + crate::lib::cli::capture::handle_command(capture_cli).await } } CliCommand::Claims(claims_cli) => { - wash_lib::cli::claims::handle_command(claims_cli, output_kind).await + crate::lib::cli::claims::handle_command(claims_cli, output_kind).await } CliCommand::Completions(completions_cli) => { completions::handle_command(completions_cli, Cli::command()) @@ -566,7 +566,7 @@ async fn main() { CliCommand::Drain(drain_cli) => drain::handle_command(drain_cli), CliCommand::Get(get_cli) => common::get_cmd::handle_command(get_cli, output_kind).await, CliCommand::Inspect(inspect_cli) => { - wash_lib::cli::inspect::handle_command(inspect_cli, output_kind).await + crate::lib::cli::inspect::handle_command(inspect_cli, output_kind).await } CliCommand::Keys(keys_cli) => keys::handle_command(keys_cli), CliCommand::Link(link_cli) => link::invoke(link_cli, output_kind).await, @@ -583,7 +583,7 @@ async fn main() { if !cli.experimental { experimental_error_message("spy") } else { - wash_lib::cli::spy::handle_command(spy_cli).await + crate::lib::cli::spy::handle_command(spy_cli).await } } CliCommand::Scale(scale_cli) => { diff --git a/crates/wash/src/cli/app/mod.rs b/crates/wash/src/cli/app/mod.rs index a785e29f40..2b7af1fa23 100644 --- a/crates/wash/src/cli/app/mod.rs +++ b/crates/wash/src/cli/app/mod.rs @@ -280,7 +280,7 @@ async fn undeploy_model(cmd: UndeployCommand) -> Result { vec![model_name] } // If no model name was specified, use command-specified filters to determine which models to act on - None if cmd.all => wash_lib::app::get_models(&client, lattice.clone()) + None if cmd.all => crate::lib::app::get_models(&client, lattice.clone()) .await? .into_iter() .map(|m| m.name) @@ -293,7 +293,7 @@ async fn undeploy_model(cmd: UndeployCommand) -> Result { // Undeploy models for model_name in models.iter() { - match wash_lib::app::undeploy_model(&client, lattice.clone(), model_name).await { + match crate::lib::app::undeploy_model(&client, lattice.clone(), model_name).await { Ok(_) => undeployed.push(model_name), Err(e) => eprintln!("failed to undeploy model [{model_name}]: {e}"), } @@ -338,7 +338,7 @@ async fn deploy_model(cmd: DeployCommand) -> Result { app_manifest.version().map(ToString::to_string), ) { if let Err(e) = - wash_lib::app::delete_model_version(&client, lattice.clone(), name, version).await + crate::lib::app::delete_model_version(&client, lattice.clone(), name, version).await { eprintln!("🟨 Failed to delete model during replace operation: {e}"); } @@ -355,7 +355,7 @@ pub(crate) async fn deploy_model_from_manifest( version: Option, ) -> Result { let (name, version) = match manifest { - AppManifest::SerializedModel(manifest) => wash_lib::app::put_and_deploy_model( + AppManifest::SerializedModel(manifest) => crate::lib::app::put_and_deploy_model( client, lattice, serde_yaml::to_string(&manifest) @@ -365,7 +365,7 @@ pub(crate) async fn deploy_model_from_manifest( .await .map(|(name, version)| (name, Some(version))), AppManifest::ModelName(model_name) => { - wash_lib::app::deploy_model(client, lattice, &model_name, version.clone()).await + crate::lib::app::deploy_model(client, lattice, &model_name, version.clone()).await } }?; @@ -393,7 +393,7 @@ async fn put_model(cmd: PutCommand) -> Result { }; let (name, version) = match app_manifest { - AppManifest::SerializedModel(manifest) => wash_lib::app::put_model( + AppManifest::SerializedModel(manifest) => crate::lib::app::put_model( &client, lattice, serde_yaml::to_string(&manifest) @@ -424,7 +424,7 @@ async fn get_application_versions(cmd: HistoryCommand) -> Result let client = connection_opts.into_nats_client().await?; - let versions = wash_lib::app::get_model_history(&client, lattice, &cmd.app_name).await?; + let versions = crate::lib::app::get_model_history(&client, lattice, &cmd.app_name).await?; let mut map = HashMap::new(); map.insert("revisions".to_string(), json!(versions)); Ok(CommandOutput::new( @@ -440,7 +440,7 @@ async fn get_model_status(cmd: StatusCommand) -> Result { let client = connection_opts.into_nats_client().await?; - let status = wash_lib::app::get_model_status(&client, lattice, &cmd.app_name).await?; + let status = crate::lib::app::get_model_status(&client, lattice, &cmd.app_name).await?; let mut map = HashMap::new(); map.insert("status".to_string(), json!(status)); @@ -502,7 +502,7 @@ async fn delete_application_version(cmd: DeleteCommand) -> Result vec![(model_name, version)] } // If no model name was specified, use command-specified filters to determine which models to act on - None if cmd.all_undeployed => wash_lib::app::get_models(&client, lattice.clone()) + None if cmd.all_undeployed => crate::lib::app::get_models(&client, lattice.clone()) .await? .into_iter() .filter_map(|m| match m.detailed_status.info.status_type { @@ -578,7 +578,7 @@ async fn get_application_list(cmd: ListCommand, sp: &Spinner) -> Result blobstore-fs - (WIT_NS_WASI, Some(WIT_PKG_BLOBSTORE), Some(interface)) - | (WIT_NS_WRPC, Some(WIT_PKG_BLOBSTORE), Some(interface)) + (WIT_NS_WASI | WIT_NS_WRPC, Some(WIT_PKG_BLOBSTORE), Some(interface)) if matches!(interface, WIT_IFACE_BLOBSTORE) => { Some(Self::Exports(DependencySpecInner { @@ -1025,8 +1024,7 @@ impl ProjectDeps { interfaces.as_ref(), version, ) { - (WIT_NS_WASI, "blobstore", interfaces, _) - | (WIT_NS_WRPC, "blobstore", interfaces, _) + (WIT_NS_WASI | WIT_NS_WRPC, "blobstore", interfaces, _) if interfaces.is_some_and(|interfaces| { interfaces.iter().any(|i| i == "blobstore") }) => @@ -1210,7 +1208,7 @@ impl ProjectDeps { .context("failed to generate manifests")? .into_iter() { - wash_lib::app::delete_model_version( + crate::lib::app::delete_model_version( client, Some(lattice.into()), &manifest.metadata.name, diff --git a/crates/wash/src/cli/cmd/dev/devloop.rs b/crates/wash/src/cli/cmd/dev/devloop.rs index 3480156989..12bbf46dcf 100644 --- a/crates/wash/src/cli/cmd/dev/devloop.rs +++ b/crates/wash/src/cli/cmd/dev/devloop.rs @@ -2,20 +2,20 @@ use std::collections::{BTreeSet, HashMap}; use std::path::PathBuf; use crate::lib::app::AppManifest; +use crate::lib::cli::stop::stop_provider; +use crate::lib::component::{scale_component, ScaleComponentArgs}; use anyhow::{bail, ensure, Context as _, Result}; use console::style; use tracing::{debug, warn}; -use wash_lib::cli::stop::stop_provider; -use wash_lib::component::{scale_component, ScaleComponentArgs}; use wasmcloud_control_interface::Client as CtlClient; -use wadm_types::{ConfigProperty, Manifest, Properties, SecretProperty, SecretSourceProperty}; -use wash_lib::build::{build_project, SignConfig}; -use wash_lib::cli::{CommonPackageArgs, OutputKind}; -use wash_lib::generate::emoji; -use wash_lib::parser::{ +use crate::lib::build::{build_project, SignConfig}; +use crate::lib::cli::{CommonPackageArgs, OutputKind}; +use crate::lib::generate::emoji; +use crate::lib::parser::{ DevConfigSpec, DevManifestComponentTarget, DevSecretSpec, ProjectConfig, TypeConfig, }; +use wadm_types::{ConfigProperty, Manifest, Properties, SecretProperty, SecretSourceProperty}; use crate::app::deploy_model_from_manifest; use crate::appearance::spinner::Spinner; @@ -452,7 +452,7 @@ pub(crate) async fn run(state: &mut RunLoopState<'_>) -> Result<()> { serde_json::to_string(&manifest).context("failed to convert manifest to JSON")?; // Put the manifest - match wash_lib::app::put_model( + match crate::lib::app::put_model( state.nats_client, Some(state.lattice.to_string()), &model_json, @@ -522,7 +522,7 @@ async fn scale_down_component( // Scale the WADM component (which can be either a component or provider) down, // expecting that WADM should restore it (and trigger a reload) match project_cfg.project_type { - wash_lib::parser::TypeConfig::Component(_) => { + crate::lib::parser::TypeConfig::Component(_) => { scale_component(ScaleComponentArgs { client, host_id, @@ -539,7 +539,7 @@ async fn scale_down_component( format!("failed to scale down component [{component_id}] for reload") })?; } - wash_lib::parser::TypeConfig::Provider(_) => { + crate::lib::parser::TypeConfig::Provider(_) => { if let Err(e) = stop_provider( client, Some(host_id), diff --git a/crates/wash/src/cli/cmd/dev/manifest.rs b/crates/wash/src/cli/cmd/dev/manifest.rs index d9cad9fcba..d211a34907 100644 --- a/crates/wash/src/cli/cmd/dev/manifest.rs +++ b/crates/wash/src/cli/cmd/dev/manifest.rs @@ -5,7 +5,7 @@ use wadm_types::{ SpreadScalerProperty, TraitProperty, }; -use wash_lib::{generate::emoji, parser::ProjectConfig}; +use crate::lib::{generate::emoji, parser::ProjectConfig}; /// Generate the a configuration name for a dependency, given it's namespace and package pub(crate) fn config_name(ns: &str, pkg: &str) -> String { @@ -117,7 +117,7 @@ pub(crate) fn generate_component_from_project_cfg( Ok(Component { name: component_id.into(), properties: match &cfg.project_type { - wash_lib::parser::TypeConfig::Component(_c) => Properties::Component { + crate::lib::parser::TypeConfig::Component(_c) => Properties::Component { properties: ComponentProperties { image: Some(image_ref.into()), application: None, @@ -126,7 +126,7 @@ pub(crate) fn generate_component_from_project_cfg( secrets: Vec::with_capacity(0), }, }, - wash_lib::parser::TypeConfig::Provider(_p) => Properties::Capability { + crate::lib::parser::TypeConfig::Provider(_p) => Properties::Capability { properties: CapabilityProperties { image: Some(image_ref.into()), application: None, @@ -137,14 +137,14 @@ pub(crate) fn generate_component_from_project_cfg( }, }, traits: match &cfg.project_type { - wash_lib::parser::TypeConfig::Component(_c) => Some(vec![wadm_types::Trait { + crate::lib::parser::TypeConfig::Component(_c) => Some(vec![wadm_types::Trait { trait_type: "spreadscaler".into(), properties: TraitProperty::SpreadScaler(SpreadScalerProperty { instances: 100, spread: Vec::new(), }), }]), - wash_lib::parser::TypeConfig::Provider(_p) => Some(vec![wadm_types::Trait { + crate::lib::parser::TypeConfig::Provider(_p) => Some(vec![wadm_types::Trait { trait_type: "spreadscaler".into(), properties: TraitProperty::SpreadScaler(SpreadScalerProperty { instances: 1, diff --git a/crates/wash/src/cli/cmd/dev/mod.rs b/crates/wash/src/cli/cmd/dev/mod.rs index b61ab5fd49..2ba1568806 100644 --- a/crates/wash/src/cli/cmd/dev/mod.rs +++ b/crates/wash/src/cli/cmd/dev/mod.rs @@ -11,11 +11,11 @@ use semver::Version; use session::{SessionMetadata, WashDevSession}; use tokio::{select, sync::mpsc}; +use crate::lib::cli::{CommandOutput, CommonPackageArgs}; +use crate::lib::generate::emoji; +use crate::lib::id::ServerId; +use crate::lib::parser::load_config; use tracing::trace; -use wash_lib::cli::{CommandOutput, CommonPackageArgs}; -use wash_lib::generate::emoji; -use wash_lib::id::ServerId; -use wash_lib::parser::load_config; use crate::cmd::up::{ nats_client_from_wasmcloud_opts, remove_wadm_pidfile, NatsOpts, WadmOpts, WasmcloudOpts, @@ -47,7 +47,7 @@ const DEFAULT_PROVIDER_STOP_TIMEOUT_MS: u64 = 3000; /// The path to the dev directory for wash async fn dev_dir() -> Result { - let dir = wash_lib::config::dev_dir().context("failed to resolve config dir")?; + let dir = crate::lib::config::dev_dir().context("failed to resolve config dir")?; if !tokio::fs::try_exists(&dir) .await .context("failed to check if dev dir exists")? @@ -124,7 +124,7 @@ pub struct DevCommand { /// Handle `wash dev` pub async fn handle_command( cmd: DevCommand, - output_kind: wash_lib::cli::OutputKind, + output_kind: crate::lib::cli::OutputKind, ) -> Result { let current_dir = std::env::current_dir().context("failed to get current directory for wash dev")?; @@ -293,17 +293,8 @@ pub async fn handle_command( let mut watcher = notify::recommended_watcher(move |res: _| match res { Ok(event) => match event { NotifyEvent { - kind: EventKind::Create(_), - paths, - .. - } - | NotifyEvent { - kind: EventKind::Modify(ModifyKind::Data(_)), - paths, - .. - } - | NotifyEvent { - kind: EventKind::Remove(_), + kind: + EventKind::Create(_) | EventKind::Modify(ModifyKind::Data(_)) | EventKind::Remove(_), paths, .. } => { diff --git a/crates/wash/src/cli/cmd/dev/session.rs b/crates/wash/src/cli/cmd/dev/session.rs index a4bf2dd2f3..fbe9bfce4e 100644 --- a/crates/wash/src/cli/cmd/dev/session.rs +++ b/crates/wash/src/cli/cmd/dev/session.rs @@ -11,12 +11,12 @@ use semver::Version; use serde::{Deserialize, Serialize}; use tokio::io::AsyncBufReadExt as _; use tokio::process::Child; -use wash_lib::common::CommandGroupUsage; +use crate::lib::common::CommandGroupUsage; -use wash_lib::config::downloads_dir; -use wash_lib::generate::emoji; -use wash_lib::id::ServerId; -use wash_lib::start::{ +use crate::lib::config::downloads_dir; +use crate::lib::generate::emoji; +use crate::lib::id::ServerId; +use crate::lib::start::{ ensure_nats_server, ensure_wadm, ensure_wasmcloud, start_wadm, start_wasmcloud_host, NatsConfig, WadmConfig, NATS_SERVER_BINARY, }; diff --git a/crates/wash/src/cli/cmd/dev/wit.rs b/crates/wash/src/cli/cmd/dev/wit.rs index 5e8d650097..41e873b0ae 100644 --- a/crates/wash/src/cli/cmd/dev/wit.rs +++ b/crates/wash/src/cli/cmd/dev/wit.rs @@ -3,7 +3,7 @@ use std::collections::HashSet; use anyhow::{bail, Context as _, Result}; use wit_parser::{Resolve, WorldId}; -use wash_lib::parser::ProjectConfig; +use crate::lib::parser::ProjectConfig; use super::deps::DependencySpec; diff --git a/crates/wash/src/cli/cmd/link/del.rs b/crates/wash/src/cli/cmd/link/del.rs index 972e589c30..9a069776ac 100644 --- a/crates/wash/src/cli/cmd/link/del.rs +++ b/crates/wash/src/cli/cmd/link/del.rs @@ -2,13 +2,13 @@ use std::collections::HashMap; +use crate::lib::cli::link::{delete_link, get_links, LinkDelCommand}; +use crate::lib::cli::{CommandOutput, OutputKind}; +use crate::lib::config::WashConnectionOptions; +use crate::lib::generate::interactive::prompt_for_choice; +use crate::lib::generate::project_variables::StringEntry; use anyhow::{anyhow, bail, ensure, Context as _, Result}; use serde_json::json; -use wash_lib::cli::link::{delete_link, get_links, LinkDelCommand}; -use wash_lib::cli::{CommandOutput, OutputKind}; -use wash_lib::config::WashConnectionOptions; -use wash_lib::generate::interactive::prompt_for_choice; -use wash_lib::generate::project_variables::StringEntry; use crate::appearance::spinner::Spinner; @@ -80,8 +80,8 @@ pub async fn invoke( return delete_links(wco.clone()) .await .context("failed to delete all links"); - } else { - match prompt_for_choice( + } + match prompt_for_choice( &StringEntry { default: Some("cancel".to_string()), choices: Some(vec!["Delete all links".to_string(), "Cancel".to_string()]), @@ -93,7 +93,6 @@ pub async fn invoke( Ok(1) => bail!("Link deletion cancelled"), _ => unreachable!("unexpected choice received"), } - } } let sp: Spinner = Spinner::new(&output_kind)?; diff --git a/crates/wash/src/cli/cmd/link/mod.rs b/crates/wash/src/cli/cmd/link/mod.rs index 98cca22364..427a8441b4 100644 --- a/crates/wash/src/cli/cmd/link/mod.rs +++ b/crates/wash/src/cli/cmd/link/mod.rs @@ -1,8 +1,8 @@ //! Functionality enabling the `wash link` group of subcommands +use crate::lib::cli::link::LinkCommand; +use crate::lib::cli::{CommandOutput, OutputKind}; use anyhow::Result; -use wash_lib::cli::link::LinkCommand; -use wash_lib::cli::{CommandOutput, OutputKind}; mod del; mod put; diff --git a/crates/wash/src/cli/cmd/link/put.rs b/crates/wash/src/cli/cmd/link/put.rs index 0c3e8cc619..2430bb3a2e 100644 --- a/crates/wash/src/cli/cmd/link/put.rs +++ b/crates/wash/src/cli/cmd/link/put.rs @@ -2,10 +2,10 @@ use std::collections::HashMap; +use crate::lib::cli::link::{put_link, LinkPutCommand}; +use crate::lib::cli::{CommandOutput, OutputKind}; use anyhow::{anyhow, bail, Result}; use serde_json::json; -use wash_lib::cli::link::{put_link, LinkPutCommand}; -use wash_lib::cli::{CommandOutput, OutputKind}; use wasmcloud_control_interface::Link; use crate::appearance::spinner::Spinner; diff --git a/crates/wash/src/cli/cmd/link/query.rs b/crates/wash/src/cli/cmd/link/query.rs index 96f4f5af41..499ca419d8 100644 --- a/crates/wash/src/cli/cmd/link/query.rs +++ b/crates/wash/src/cli/cmd/link/query.rs @@ -2,10 +2,10 @@ use std::collections::HashMap; +use crate::lib::cli::link::{get_links, LinkQueryCommand}; +use crate::lib::cli::{CommandOutput, OutputKind}; use anyhow::Result; use serde_json::json; -use wash_lib::cli::link::{get_links, LinkQueryCommand}; -use wash_lib::cli::{CommandOutput, OutputKind}; use wasmcloud_control_interface::Link; use crate::appearance::spinner::Spinner; diff --git a/crates/wash/src/cli/cmd/up.rs b/crates/wash/src/cli/cmd/up.rs index 33acdc72d7..265f03b5c3 100644 --- a/crates/wash/src/cli/cmd/up.rs +++ b/crates/wash/src/cli/cmd/up.rs @@ -1,4 +1,18 @@ use crate::lib::app::{load_app_manifest, AppManifest, AppManifestSource}; +use crate::lib::cli::{CommandOutput, OutputKind}; +use crate::lib::common::CommandGroupUsage; +use crate::lib::config::{ + create_nats_client_from_opts, downloads_dir, host_pid_file, DEFAULT_NATS_TIMEOUT_MS, +}; +use crate::lib::context::fs::ContextDir; +use crate::lib::context::ContextManager; +use crate::lib::generate::emoji; +use crate::lib::start::{ + ensure_nats_server, ensure_wadm, ensure_wasmcloud, find_wasmcloud_binary, nats_pid_path, + new_patch_version_of_after_string, start_nats_server, start_wadm, start_wasmcloud_host, + NatsConfig, WadmConfig, GITHUB_WASMCLOUD_ORG, GITHUB_WASMCLOUD_WADM_REPO, + GITHUB_WASMCLOUD_WASMCLOUD_REPO, NATS_SERVER_BINARY, NATS_SERVER_CONF, WADM_PID, +}; use anyhow::{anyhow, bail, Context, Result}; use async_nats::Client; use clap::Parser; @@ -19,20 +33,6 @@ use tokio::{ process::Child, }; use tracing::{debug, warn}; -use wash_lib::cli::{CommandOutput, OutputKind}; -use wash_lib::common::CommandGroupUsage; -use wash_lib::config::{ - create_nats_client_from_opts, downloads_dir, host_pid_file, DEFAULT_NATS_TIMEOUT_MS, -}; -use wash_lib::context::fs::ContextDir; -use wash_lib::context::ContextManager; -use wash_lib::generate::emoji; -use wash_lib::start::{ - ensure_nats_server, ensure_wadm, ensure_wasmcloud, find_wasmcloud_binary, nats_pid_path, - new_patch_version_of_after_string, start_nats_server, start_wadm, start_wasmcloud_host, - NatsConfig, WadmConfig, GITHUB_WASMCLOUD_ORG, GITHUB_WASMCLOUD_WADM_REPO, - GITHUB_WASMCLOUD_WASMCLOUD_REPO, NATS_SERVER_BINARY, NATS_SERVER_CONF, WADM_PID, -}; use wasmcloud_control_interface::{Client as CtlClient, ClientBuilder as CtlClientBuilder}; use crate::app::deploy_model_from_manifest; @@ -878,7 +878,7 @@ async fn deploy_wadm_application( lattice: &str, ) -> Result<()> { let model_name = manifest.name().context("failed to find model name")?; - let _ = wash_lib::app::undeploy_model(client, Some(lattice.into()), model_name).await; + let _ = crate::lib::app::undeploy_model(client, Some(lattice.into()), model_name).await; match deploy_model_from_manifest(client, Some(lattice.into()), manifest, None).await { // Ignore if the model is already deployed Err(e) if e.to_string().contains("already exists") => {} @@ -1096,7 +1096,7 @@ async fn is_wadm_running( .await?; Ok( - wash_lib::app::get_models(&client, Some(lattice.to_string())) + crate::lib::app::get_models(&client, Some(lattice.to_string())) .await .is_ok(), ) diff --git a/crates/wash/src/cli/cmd/wit/build.rs b/crates/wash/src/cli/cmd/wit/build.rs index 93a8576f4a..f9c47a6449 100644 --- a/crates/wash/src/cli/cmd/wit/build.rs +++ b/crates/wash/src/cli/cmd/wit/build.rs @@ -1,10 +1,10 @@ use std::path::PathBuf; +use crate::lib::build::load_lock_file; +use crate::lib::cli::{CommandOutput, CommonPackageArgs}; +use crate::lib::deps::WkgFetcher; +use crate::lib::parser::load_config; use clap::Args; -use wash_lib::build::load_lock_file; -use wash_lib::cli::{CommandOutput, CommonPackageArgs}; -use wash_lib::deps::WkgFetcher; -use wash_lib::parser::load_config; use wasm_pkg_core::wit::{self}; diff --git a/crates/wash/src/cli/cmd/wit/deps.rs b/crates/wash/src/cli/cmd/wit/deps.rs index 1d0d08aa8d..c26b99386e 100644 --- a/crates/wash/src/cli/cmd/wit/deps.rs +++ b/crates/wash/src/cli/cmd/wit/deps.rs @@ -1,10 +1,10 @@ use std::path::PathBuf; +use crate::lib::build::load_lock_file; +use crate::lib::cli::{CommandOutput, CommonPackageArgs}; use clap::Args; -use wash_lib::build::load_lock_file; -use wash_lib::cli::{CommandOutput, CommonPackageArgs}; -use wash_lib::parser::{load_config, CommonConfig, ProjectConfig, RegistryConfig}; +use crate::lib::parser::{load_config, CommonConfig, ProjectConfig, RegistryConfig}; use wasm_pkg_core::wit::OutputType; /// Arguments to `wash wit deps` @@ -57,7 +57,7 @@ pub async fn invoke( let mut lock_file = load_lock_file(&project_cfg.wasmcloud_toml_dir).await?; // Start building the wkg client config - let mut wkg = wash_lib::deps::WkgFetcher::from_common(&common, wkg_config).await?; + let mut wkg = crate::lib::deps::WkgFetcher::from_common(&common, wkg_config).await?; // If a project configuration was provided, apply any pull-related overrides // in the new "extended" configuration format if let Some(ProjectConfig { diff --git a/crates/wash/src/cli/cmd/wit/mod.rs b/crates/wash/src/cli/cmd/wit/mod.rs index eeb1707aa5..0dd73fdfe9 100644 --- a/crates/wash/src/cli/cmd/wit/mod.rs +++ b/crates/wash/src/cli/cmd/wit/mod.rs @@ -1,5 +1,5 @@ +use crate::lib::cli::CommandOutput; use clap::Subcommand; -use wash_lib::cli::CommandOutput; mod build; mod deps; diff --git a/crates/wash/src/cli/cmd/wit/publish.rs b/crates/wash/src/cli/cmd/wit/publish.rs index d60e2b4799..80ec668099 100644 --- a/crates/wash/src/cli/cmd/wit/publish.rs +++ b/crates/wash/src/cli/cmd/wit/publish.rs @@ -1,10 +1,10 @@ use std::path::PathBuf; -use clap::Args; -use wash_lib::{ +use crate::lib::{ cli::{CommandOutput, CommonPackageArgs}, deps::WkgFetcher, }; +use clap::Args; use wasm_pkg_client::{PublishOpts, Registry}; /// Arguments for invoking `wash wit publish` diff --git a/crates/wash/src/cli/common/get_cmd.rs b/crates/wash/src/cli/common/get_cmd.rs index 8dfef68670..d16e3aecfc 100644 --- a/crates/wash/src/cli/common/get_cmd.rs +++ b/crates/wash/src/cli/common/get_cmd.rs @@ -1,3 +1,9 @@ +use crate::lib::cli::claims::get_claims; +use crate::lib::cli::get::{ + get_host_inventories, get_hosts, GetCommand, GetHostInventoriesCommand, GetLinksCommand, +}; +use crate::lib::cli::link::{LinkCommand, LinkQueryCommand}; +use crate::lib::cli::{CommandOutput, OutputKind}; use anyhow::Result; use crossterm::{ cursor, execute, @@ -5,12 +11,6 @@ use crossterm::{ }; use std::{collections::HashMap, io::Write, time::Duration}; use tokio::time::sleep; -use wash_lib::cli::claims::get_claims; -use wash_lib::cli::get::{ - get_host_inventories, get_hosts, GetCommand, GetHostInventoriesCommand, GetLinksCommand, -}; -use wash_lib::cli::link::{LinkCommand, LinkQueryCommand}; -use wash_lib::cli::{CommandOutput, OutputKind}; use crate::appearance::spinner::Spinner; use crate::cmd::link::invoke as invoke_link_cmd; diff --git a/crates/wash/src/cli/common/label_cmd.rs b/crates/wash/src/cli/common/label_cmd.rs index 3872e1b16a..9808ac92aa 100644 --- a/crates/wash/src/cli/common/label_cmd.rs +++ b/crates/wash/src/cli/common/label_cmd.rs @@ -1,6 +1,6 @@ use anyhow::Result; -use wash_lib::cli::{ +use crate::lib::cli::{ label::{handle_label_host, LabelHostCommand}, CommandOutput, OutputKind, }; diff --git a/crates/wash/src/cli/common/registry_cmd.rs b/crates/wash/src/cli/common/registry_cmd.rs index 6c6e5b1e55..a72466d27f 100644 --- a/crates/wash/src/cli/common/registry_cmd.rs +++ b/crates/wash/src/cli/common/registry_cmd.rs @@ -8,10 +8,10 @@ use tokio::fs::File; use tokio::io::AsyncWriteExt; use tracing::warn; -use wash_lib::cli::registry::{RegistryPullCommand, RegistryPushCommand}; -use wash_lib::cli::{input_vec_to_hashmap, CommandOutput, OutputKind}; -use wash_lib::parser::{load_config, ProjectConfig}; -use wash_lib::registry::{ +use crate::lib::cli::registry::{RegistryPullCommand, RegistryPushCommand}; +use crate::lib::cli::{input_vec_to_hashmap, CommandOutput, OutputKind}; +use crate::lib::parser::{load_config, ProjectConfig}; +use crate::lib::registry::{ identify_artifact, pull_oci_artifact, push_oci_artifact, ArtifactType, OciPullOptions, OciPushOptions, }; @@ -219,9 +219,9 @@ async fn resolve_registry_credentials(registry: &str) -> Result Result { if matches!(cmd, Drain::All | Drain::Downloads) { diff --git a/crates/wash/src/cli/generate.rs b/crates/wash/src/cli/generate.rs index a621cc45e4..4d1f78dd6d 100644 --- a/crates/wash/src/cli/generate.rs +++ b/crates/wash/src/cli/generate.rs @@ -1,12 +1,12 @@ use std::{collections::HashMap, path::PathBuf}; -use anyhow::{Context, Result}; -use clap::{Args, Subcommand}; -use serde_json::json; -use wash_lib::{ +use crate::lib::{ cli::CommandOutput, generate::{generate_project, Project, ProjectKind}, }; +use anyhow::{Context, Result}; +use clap::{Args, Subcommand}; +use serde_json::json; /// Create a new project from template #[derive(Debug, Clone, Subcommand)] diff --git a/crates/wash/src/cli/keys.rs b/crates/wash/src/cli/keys.rs index 68080a532e..f820ef982c 100644 --- a/crates/wash/src/cli/keys.rs +++ b/crates/wash/src/cli/keys.rs @@ -1,12 +1,12 @@ use std::{collections::HashMap, path::PathBuf}; +use crate::lib::cli::CommandOutput; +use crate::lib::config::cfg_dir; +use crate::lib::keys::{fs::KeyDir, KeyManager}; use anyhow::Result; use clap::Subcommand; use nkeys::{KeyPair, KeyPairType}; use serde_json::json; -use wash_lib::cli::CommandOutput; -use wash_lib::config::cfg_dir; -use wash_lib::keys::{fs::KeyDir, KeyManager}; const NKEYS_EXTENSION: &str = ".nk"; diff --git a/crates/wash/src/cli/mod.rs b/crates/wash/src/cli/mod.rs index 40a3e7788f..4ed68f4888 100644 --- a/crates/wash/src/cli/mod.rs +++ b/crates/wash/src/cli/mod.rs @@ -8,8 +8,10 @@ pub mod completions; pub mod config; pub mod creds; pub mod ctl; +pub mod ctx; pub mod down; pub mod drain; +pub mod errors; pub mod generate; pub mod keys; pub mod par; @@ -19,19 +21,17 @@ pub mod style; pub mod ui; pub mod util; -// Re-export commonly used types and functions -pub use self::app::*; -pub use self::build::*; -pub use self::call::*; -pub use self::common::*; -pub use self::completions::*; -pub use self::config::*; -pub use self::creds::*; -pub use self::ctl::*; -pub use self::down::*; -pub use self::drain::*; -pub use self::generate::*; -pub use self::keys::*; -pub use self::par::*; -pub use self::plugin::*; -pub use self::secrets::*; +// // Re-export commonly used types and functions +// pub use self::build::*; +// pub use self::call::*; +// pub use self::common::*; +// pub use self::completions::*; +// pub use self::config::*; +// pub use self::creds::*; +// pub use self::ctl::*; +// pub use self::down::*; +// pub use self::generate::*; +// pub use self::keys::*; +// pub use self::par::*; +// pub use self::plugin::*; +// pub use self::secrets::*; diff --git a/crates/wash/src/cli/par.rs b/crates/wash/src/cli/par.rs index ec0a5928c0..e37ed2c93d 100644 --- a/crates/wash/src/cli/par.rs +++ b/crates/wash/src/cli/par.rs @@ -2,16 +2,16 @@ use std::fs::File; use std::io::Read; use std::{collections::HashMap, path::PathBuf}; +use crate::lib::cli::par::{ + convert_error, create_provider_archive, detect_arch, insert_provider_binary, +}; +use crate::lib::cli::{extract_keypair, inspect, par, CommandOutput, OutputKind}; use anyhow::{anyhow, bail, Context, Result}; use clap::{Parser, Subcommand}; use nkeys::KeyPairType; use provider_archive::ProviderArchive; use serde_json::json; use tracing::warn; -use wash_lib::cli::par::{ - convert_error, create_provider_archive, detect_arch, insert_provider_binary, -}; -use wash_lib::cli::{extract_keypair, inspect, par, CommandOutput, OutputKind}; const GZIP_MAGIC: [u8; 2] = [0x1f, 0x8b]; diff --git a/crates/wash/src/cli/plugin.rs b/crates/wash/src/cli/plugin.rs index 4f1265d7ed..76d55d7a23 100644 --- a/crates/wash/src/cli/plugin.rs +++ b/crates/wash/src/cli/plugin.rs @@ -11,7 +11,7 @@ use oci_client::Reference; use sha2::{Digest, Sha256}; use tokio::io::AsyncWriteExt; -use crate::lib::{ +use super::{ appearance::spinner::Spinner, ctl::plugins_table, util::{ensure_plugin_dir, load_plugins}, diff --git a/crates/wash/src/lib.rs b/crates/wash/src/lib.rs index 71026bb9d4..5f3b98c58d 100644 --- a/crates/wash/src/lib.rs +++ b/crates/wash/src/lib.rs @@ -7,9 +7,9 @@ )] #![allow(clippy::module_name_repetitions)] -// Re-export commonly used types -pub use errors::Error; -pub type Result = std::result::Result; +// // Re-export commonly used types +// pub use errors::Error; +// pub type Result = std::result::Result; // Library modules (from wash-lib) pub mod lib { @@ -32,18 +32,13 @@ pub mod lib { pub mod spier; pub mod start; pub mod wait; - - // Re-exports of commonly used types/functions from the library - pub use self::cli::CommandOutput; - pub use self::config::WashConnectionOptions; - pub use self::id::ServerId; } // CLI modules pub mod cli; // Root level modules -pub mod errors; +// pub mod errors; // Re-exports for backward compatibility pub use cli::*; diff --git a/crates/wash/src/lib/cli/inspect.rs b/crates/wash/src/lib/cli/inspect.rs index 28fdbf0c49..931b0bb24e 100644 --- a/crates/wash/src/lib/cli/inspect.rs +++ b/crates/wash/src/lib/cli/inspect.rs @@ -115,12 +115,10 @@ pub async fn handle_command( let resolve = witty.resolve(); let main = witty.package(); let mut printer = wit_component::WitPrinter::default(); - CommandOutput::from_key_and_text( - "wit", - printer - .print(resolve, main, &[]) - .context("should be able to print WIT world from a component")?, - ) + printer + .print(resolve, main, &[]) + .context("should be able to print WIT world from a component")?; + CommandOutput::from_key_and_text("wit", printer.output) } // Catch trying to inspect a WIT from a WASI Preview 1 module Some(Ok(wasmparser::Payload::Version { diff --git a/crates/wash/src/lib/cli/link.rs b/crates/wash/src/lib/cli/link.rs index 734617e8f0..2cb786a42a 100644 --- a/crates/wash/src/lib/cli/link.rs +++ b/crates/wash/src/lib/cli/link.rs @@ -106,8 +106,8 @@ pub enum LinkCommand { /// # Examples /// /// ```no_run -/// # use wash_lib::cli::link::get_links; -/// use wash_lib::config::WashConnectionOptions; +/// # use crate::lib::cli::link::get_links; +/// use crate::lib::config::WashConnectionOptions; /// # async fn doc() -> anyhow::Result<()> { /// let links = get_links(WashConnectionOptions::default()).await?; /// println!("{links:?}"); @@ -136,8 +136,8 @@ pub async fn get_links(wco: WashConnectionOptions) -> Result> { /// # Examples /// /// ```no_run -/// # use wash_lib::cli::link::delete_link; -/// use wash_lib::config::WashConnectionOptions; +/// # use crate::lib::cli::link::delete_link; +/// use crate::lib::config::WashConnectionOptions; /// # async fn doc() -> anyhow::Result<()> { /// let ack = delete_link( /// WashConnectionOptions::default(), @@ -179,8 +179,8 @@ pub async fn delete_link( /// # Examples /// /// ```no_run -/// # use wash_lib::cli::link::put_link; -/// use wash_lib::config::WashConnectionOptions; +/// # use crate::lib::cli::link::put_link; +/// use crate::lib::config::WashConnectionOptions; /// use wasmcloud_control_interface::Link; /// # async fn doc() -> anyhow::Result<()> { /// let ack = put_link( diff --git a/crates/wash/src/lib/cli/start.rs b/crates/wash/src/lib/cli/start.rs index 99700ff88b..cc0ac7c9ef 100644 --- a/crates/wash/src/lib/cli/start.rs +++ b/crates/wash/src/lib/cli/start.rs @@ -143,16 +143,15 @@ pub async fn handle_start_component(cmd: StartComponentCommand) -> Result>(); - let ack = acks.first().context("No suitable hosts found")?; - ack.host_id() - .parse() - .with_context(|| format!("Failed to parse host id: {}", ack.host_id()))? } + let acks = suitable_hosts + .into_iter() + .filter_map(|h| h.into_data()) + .collect::>(); + let ack = acks.first().context("No suitable hosts found")?; + ack.host_id() + .parse() + .with_context(|| format!("Failed to parse host id: {}", ack.host_id()))? } }; @@ -267,16 +266,15 @@ pub async fn handle_start_provider(cmd: StartProviderCommand) -> Result>(); - let ack = acks.first().context("No suitable hosts found")?; - ack.host_id() - .parse() - .with_context(|| format!("Failed to parse host id: {}", ack.host_id()))? } + let acks = suitable_hosts + .into_iter() + .filter_map(|h| h.into_data()) + .collect::>(); + let ack = acks.first().context("No suitable hosts found")?; + ack.host_id() + .parse() + .with_context(|| format!("Failed to parse host id: {}", ack.host_id()))? } }; diff --git a/crates/wash/src/lib/parser/mod.rs b/crates/wash/src/lib/parser/mod.rs index 26ec90df30..de516cbea4 100644 --- a/crates/wash/src/lib/parser/mod.rs +++ b/crates/wash/src/lib/parser/mod.rs @@ -646,7 +646,7 @@ impl WitInterfaceSpec { /// /// ``` /// use std::str::FromStr; - /// use wash_lib::parser::WitInterfaceSpec; + /// use crate::lib::parser::WitInterfaceSpec; /// assert!(WitInterfaceSpec::from_str("wasi:http").unwrap().includes(WitInterfaceSpec::from_str("wasi:http/incoming-handler").as_ref().unwrap())); /// assert!(WitInterfaceSpec::from_str("wasi:http/incoming-handler").unwrap().includes(WitInterfaceSpec::from_str("wasi:http/incoming-handler.handle").as_ref().unwrap())); /// ``` @@ -664,11 +664,7 @@ impl WitInterfaceSpec { // If interfaces don't match, this interface can't contain the other one match (self.interfaces.as_ref(), other.interfaces.as_ref()) { // If they both have no interface specified, then we do overlap - (None, None) | - // If the other has no interface, but this one does, this *does* overlap - (Some(_), None) | - // If this spec has no interface, but the other does, then we do overlap - (None, Some(_)) => { + (None | Some(_), None) | (None, Some(_)) => { return false; } // If both specify different interfaces, we don't overlap @@ -682,13 +678,7 @@ impl WitInterfaceSpec { // At this point, we know that the interfaces must match match (self.function.as_ref(), other.function.as_ref()) { // If neither have functions, they cannot be disjoint - (None, None) | - // If only self has a function, then they are not disjoint - // (other contains self) - (Some(_), None) | - // If only the other has a function, then they are not disjoint - // (self contains other) - (None, Some(_)) => { + (None | Some(_), None) | (None, Some(_)) => { return false; } // If the functions differ, these are disjoint @@ -700,16 +690,10 @@ impl WitInterfaceSpec { } // Compare the versions - match (self.version.as_ref(), other.version.as_ref()) { - // If the neither have versions, they cannot be disjoint - (None, None) | - // If only self has a version, they cannot be disjoint - // (self contains other) - (Some(_), None) | - // If only the other has a version, they cannot be disjoint - // (other contains self) - (None, Some(_)) => { - false + match (self.version.as_ref(), other.version.as_ref()) { + // If neither have versions or only one has a version, they cannot be disjoint + (None | Some(_), None) | (None, Some(_)) => { + return false; } // If the *either* version matches the other in semantic version terms, they cannot be disjoint // @@ -717,12 +701,18 @@ impl WitInterfaceSpec { // we assume that semantic versioning semantics should ensure that 0.2.0 and 0.2.1 are backwards compatible // (though for <1.x versions, there is no such "real" guarantee) // - (Some(v), Some(other_v)) if VersionReq::parse(&format!("^{v}")).is_ok_and(|req| req.matches(other_v)) => { false } - (Some(v), Some(other_v)) if VersionReq::parse(&format!("^{other_v}")).is_ok_and(|req| req.matches(v)) => { + (Some(v), Some(other_v)) + if VersionReq::parse(&format!("^{v}")).is_ok_and(|req| req.matches(other_v)) => + { + false + } + (Some(v), Some(other_v)) + if VersionReq::parse(&format!("^{other_v}")).is_ok_and(|req| req.matches(v)) => + { false } // The only option left is that the versions are the same and their versions are incompatible/different - _ => true + _ => true, } } } @@ -1395,9 +1385,10 @@ fn canonicalize_or_create(path: PathBuf) -> Result { // TODO(joonas): Remove these once doctests are run as part of CI. #[cfg(test)] mod tests { - use crate::parser::WitInterfaceSpec; use std::str::FromStr; + use crate::lib::parser::WitInterfaceSpec; + #[test] fn test_includes() { let wasi_http = WitInterfaceSpec::from_str("wasi:http") diff --git a/crates/wash/src/lib/start/mod.rs b/crates/wash/src/lib/start/mod.rs index 875e0e350f..f3f9983541 100644 --- a/crates/wash/src/lib/start/mod.rs +++ b/crates/wash/src/lib/start/mod.rs @@ -4,8 +4,8 @@ //! # Downloading and Starting NATS and wasmCloud //! ```no_run //! use anyhow::{anyhow, Result}; -//! use wash_lib::common::CommandGroupUsage; -//! use wash_lib::start::{ +//! use crate::lib::common::CommandGroupUsage; +//! use crate::lib::start::{ //! start_wasmcloud_host, //! start_nats_server, //! ensure_nats_server, diff --git a/crates/wash/src/lib/start/nats.rs b/crates/wash/src/lib/start/nats.rs index 7c87bd3c80..30a18903fe 100644 --- a/crates/wash/src/lib/start/nats.rs +++ b/crates/wash/src/lib/start/nats.rs @@ -30,7 +30,7 @@ pub const NATS_SERVER_BINARY: &str = "nats-server.exe"; /// ```no_run /// # #[tokio::main] /// # async fn main() { -/// use wash_lib::start::ensure_nats_server; +/// use crate::lib::start::ensure_nats_server; /// let res = ensure_nats_server("v2.10.7", "/tmp/").await; /// assert!(res.is_ok()); /// assert!(res.unwrap().to_string_lossy() == "/tmp/nats-server"); @@ -58,7 +58,7 @@ where /// ```no_run /// # #[tokio::main] /// # async fn main() { -/// use wash_lib::start::ensure_nats_server_for_os_arch_pair; +/// use crate::lib::start::ensure_nats_server_for_os_arch_pair; /// let os = std::env::consts::OS; /// let arch = std::env::consts::ARCH; /// let res = ensure_nats_server_for_os_arch_pair(os, arch, "v2.10.7", "/tmp/").await; @@ -119,7 +119,7 @@ where /// ```no_run /// # #[tokio::main] /// # async fn main() { -/// use wash_lib::start::download_nats_server; +/// use crate::lib::start::download_nats_server; /// let res = download_nats_server("v2.10.7", "/tmp/").await; /// assert!(res.is_ok()); /// assert!(res.unwrap().to_string_lossy() == "/tmp/nats-server"); @@ -385,7 +385,7 @@ mod test { use anyhow::Result; use tokio::io::AsyncReadExt; - use crate::start::NatsConfig; + use crate::lib::start::NatsConfig; #[tokio::test] async fn can_write_properly_formed_credsfile() -> Result<()> { diff --git a/crates/wash/src/lib/start/wadm.rs b/crates/wash/src/lib/start/wadm.rs index 9ff38bcf76..1895799222 100644 --- a/crates/wash/src/lib/start/wadm.rs +++ b/crates/wash/src/lib/start/wadm.rs @@ -27,7 +27,7 @@ pub const WADM_BINARY: &str = "wadm.exe"; /// ```no_run /// # #[tokio::main] /// # async fn main() { -/// use wash_lib::start::ensure_wadm; +/// use crate::lib::start::ensure_wadm; /// let res = ensure_wadm("v0.4.0-alpha.1", "/tmp/").await; /// assert!(res.is_ok()); /// assert!(res.unwrap().to_string_lossy() == "/tmp/wadm"); @@ -54,7 +54,7 @@ where /// ```no_run /// # #[tokio::main] /// # async fn main() { -/// use wash_lib::start::ensure_wadm_for_os_arch_pair; +/// use crate::lib::start::ensure_wadm_for_os_arch_pair; /// let os = std::env::consts::OS; /// let arch = std::env::consts::ARCH; /// let res = ensure_wadm_for_os_arch_pair(os, arch, "v0.4.0-alpha.1", "/tmp/").await; @@ -109,7 +109,7 @@ where /// ```no_run /// # #[tokio::main] /// # async fn main() { -/// use wash_lib::start::download_wadm; +/// use crate::lib::start::download_wadm; /// let res = download_wadm("v0.4.0-alpha.1", "/tmp/").await; /// assert!(res.is_ok()); /// assert!(res.unwrap().to_string_lossy() == "/tmp/wadm"); diff --git a/crates/wash/src/lib/start/wasmcloud.rs b/crates/wash/src/lib/start/wasmcloud.rs index f28b90f8c4..6e3ee61fc4 100644 --- a/crates/wash/src/lib/start/wasmcloud.rs +++ b/crates/wash/src/lib/start/wasmcloud.rs @@ -40,7 +40,7 @@ const MINIMUM_WASMCLOUD_VERSION: &str = "0.81.0"; /// ```no_run /// # #[tokio::main] /// # async fn main() { -/// use wash_lib::start::ensure_wasmcloud; +/// use crate::lib::start::ensure_wasmcloud; /// let res = ensure_wasmcloud("v0.63.0", "/tmp/wasmcloud/").await; /// assert!(res.is_ok()); /// assert!(res.unwrap().to_string_lossy() == "/tmp/wasmcloud/v0.63.0/wasmcloud_host".to_string()); @@ -72,7 +72,7 @@ where /// ```no_run /// # #[tokio::main] /// # async fn main() { -/// use wash_lib::start::ensure_wasmcloud_for_os_arch_pair; +/// use crate::lib::start::ensure_wasmcloud_for_os_arch_pair; /// let os = std::env::consts::OS; /// let arch = std::env::consts::ARCH; /// let res = ensure_wasmcloud_for_os_arch_pair("v0.63.0", "/tmp/wasmcloud/").await; @@ -106,7 +106,7 @@ where /// ```no_run /// # #[tokio::main] /// # async fn main() { -/// use wash_lib::start::download_wasmcloud; +/// use crate::lib::start::download_wasmcloud; /// let res = download_wasmcloud("v0.57.1", "/tmp/wasmcloud/").await; /// assert!(res.is_ok()); /// assert!(res.unwrap().to_string_lossy() == "/tmp/wasmcloud/v0.63.0/wasmcloud_host".to_string()); @@ -135,7 +135,7 @@ where /// ```rust,ignore /// # #[tokio::main] /// # async fn main() { -/// use wash_lib::start::download_wasmcloud_for_os_arch_pair; +/// use crate::lib::start::download_wasmcloud_for_os_arch_pair; /// let os = std::env::consts::OS; /// let arch = std::env::consts::ARCH; /// let res = download_wasmcloud_for_os_arch_pair("v0.63.0", "/tmp/wasmcloud/").await; diff --git a/crates/wash/src/wit/deps.lock b/crates/wash/src/wit/deps.lock new file mode 100644 index 0000000000..834ddd54f1 --- /dev/null +++ b/crates/wash/src/wit/deps.lock @@ -0,0 +1,31 @@ +[cli] +url = "https://github.com/WebAssembly/wasi-cli/archive/v0.2.0.tar.gz" +sha256 = "285865a31d777181b075f39e92bcfe59c89cd6bacce660be1b9a627646956258" +sha512 = "da2622210a9e3eea82b99f1a5b8a44ce5443d009cb943f7bca0bf9cf4360829b289913d7ee727c011f0f72994ea7dc8e661ebcc0a6b34b587297d80cd9b3f7e8" +deps = ["clocks", "filesystem", "io", "random", "sockets"] + +[clocks] +sha256 = "468b4d12892fe926b8eb5d398dbf579d566c93231fa44f415440572c695b7613" +sha512 = "e6b53a07221f1413953c9797c68f08b815fdaebf66419bbc1ea3e8b7dece73731062693634731f311a03957b268cf9cc509c518bd15e513c318aa04a8459b93a" + +[filesystem] +sha256 = "498c465cfd04587db40f970fff2185daa597d074c20b68a8bcbae558f261499b" +sha512 = "ead452f9b7bfb88593a502ec00d76d4228003d51c40fd0408aebc32d35c94673551b00230d730873361567cc209ec218c41fb4e95bad194268592c49e7964347" + +[http] +url = "https://github.com/WebAssembly/wasi-http/archive/v0.2.0.tar.gz" +sha256 = "8f44402bde16c48e28c47dc53eab0b26af5b3b3482a1852cf77673e0880ba1c1" +sha512 = "760695f9a25c25bf75a25b731cb21c3bda9e288e450edda823324ecbc73d5d798bbb5de2edad999566980836f037463ee9e57d61789d04b3f3e381475b1a9a0f" +deps = ["clocks", "filesystem", "io", "random", "sockets"] + +[io] +sha256 = "7210e5653539a15478f894d4da24cc69d61924cbcba21d2804d69314a88e5a4c" +sha512 = "49184a1b0945a889abd52d25271172ed3dc2db6968fcdddb1bab7ee0081f4a3eeee0977ad2291126a37631c0d86eeea75d822fa8af224c422134500bf9f0f2bb" + +[random] +sha256 = "7371d03c037d924caba2587fb2e7c5773a0d3c5fcecbf7971e0e0ba57973c53d" +sha512 = "964c4e8925a53078e4d94ba907b54f89a0b7e154f46823a505391471466c17f53c8692682e5c85771712acd88b348686173fc07c53a3cfe3d301b8cd8ddd0de4" + +[sockets] +sha256 = "622bd28bbeb43736375dc02bd003fd3a016ff8ee91e14bab488325c6b38bf966" +sha512 = "5a63c1f36de0c4548e1d2297bdbededb28721cbad94ef7825c469eae29d7451c97e00b4c1d6730ee1ec0c4a5aac922961a2795762d4a0c3bb54e30a391a84bae" diff --git a/crates/wash/src/wit/deps.toml b/crates/wash/src/wit/deps.toml new file mode 100644 index 0000000000..426d2da02d --- /dev/null +++ b/crates/wash/src/wit/deps.toml @@ -0,0 +1,2 @@ +cli = "https://github.com/WebAssembly/wasi-cli/archive/v0.2.0.tar.gz" +http = "https://github.com/WebAssembly/wasi-http/archive/v0.2.0.tar.gz" diff --git a/crates/wash/src/wit/deps/cli/command.wit b/crates/wash/src/wit/deps/cli/command.wit new file mode 100644 index 0000000000..d8005bd388 --- /dev/null +++ b/crates/wash/src/wit/deps/cli/command.wit @@ -0,0 +1,7 @@ +package wasi:cli@0.2.0; + +world command { + include imports; + + export run; +} diff --git a/crates/wash/src/wit/deps/cli/environment.wit b/crates/wash/src/wit/deps/cli/environment.wit new file mode 100644 index 0000000000..70065233e8 --- /dev/null +++ b/crates/wash/src/wit/deps/cli/environment.wit @@ -0,0 +1,18 @@ +interface environment { + /// Get the POSIX-style environment variables. + /// + /// Each environment variable is provided as a pair of string variable names + /// and string value. + /// + /// Morally, these are a value import, but until value imports are available + /// in the component model, this import function should return the same + /// values each time it is called. + get-environment: func() -> list>; + + /// Get the POSIX-style arguments to the program. + get-arguments: func() -> list; + + /// Return a path that programs should use as their initial current working + /// directory, interpreting `.` as shorthand for this. + initial-cwd: func() -> option; +} diff --git a/crates/wash/src/wit/deps/cli/exit.wit b/crates/wash/src/wit/deps/cli/exit.wit new file mode 100644 index 0000000000..d0c2b82ae2 --- /dev/null +++ b/crates/wash/src/wit/deps/cli/exit.wit @@ -0,0 +1,4 @@ +interface exit { + /// Exit the current instance and any linked instances. + exit: func(status: result); +} diff --git a/crates/wash/src/wit/deps/cli/imports.wit b/crates/wash/src/wit/deps/cli/imports.wit new file mode 100644 index 0000000000..083b84a036 --- /dev/null +++ b/crates/wash/src/wit/deps/cli/imports.wit @@ -0,0 +1,20 @@ +package wasi:cli@0.2.0; + +world imports { + include wasi:clocks/imports@0.2.0; + include wasi:filesystem/imports@0.2.0; + include wasi:sockets/imports@0.2.0; + include wasi:random/imports@0.2.0; + include wasi:io/imports@0.2.0; + + import environment; + import exit; + import stdin; + import stdout; + import stderr; + import terminal-input; + import terminal-output; + import terminal-stdin; + import terminal-stdout; + import terminal-stderr; +} diff --git a/crates/wash/src/wit/deps/cli/run.wit b/crates/wash/src/wit/deps/cli/run.wit new file mode 100644 index 0000000000..a70ee8c038 --- /dev/null +++ b/crates/wash/src/wit/deps/cli/run.wit @@ -0,0 +1,4 @@ +interface run { + /// Run the program. + run: func() -> result; +} diff --git a/crates/wash/src/wit/deps/cli/stdio.wit b/crates/wash/src/wit/deps/cli/stdio.wit new file mode 100644 index 0000000000..31ef35b5a7 --- /dev/null +++ b/crates/wash/src/wit/deps/cli/stdio.wit @@ -0,0 +1,17 @@ +interface stdin { + use wasi:io/streams@0.2.0.{input-stream}; + + get-stdin: func() -> input-stream; +} + +interface stdout { + use wasi:io/streams@0.2.0.{output-stream}; + + get-stdout: func() -> output-stream; +} + +interface stderr { + use wasi:io/streams@0.2.0.{output-stream}; + + get-stderr: func() -> output-stream; +} diff --git a/crates/wash/src/wit/deps/cli/terminal.wit b/crates/wash/src/wit/deps/cli/terminal.wit new file mode 100644 index 0000000000..38c724efc8 --- /dev/null +++ b/crates/wash/src/wit/deps/cli/terminal.wit @@ -0,0 +1,49 @@ +/// Terminal input. +/// +/// In the future, this may include functions for disabling echoing, +/// disabling input buffering so that keyboard events are sent through +/// immediately, querying supported features, and so on. +interface terminal-input { + /// The input side of a terminal. + resource terminal-input; +} + +/// Terminal output. +/// +/// In the future, this may include functions for querying the terminal +/// size, being notified of terminal size changes, querying supported +/// features, and so on. +interface terminal-output { + /// The output side of a terminal. + resource terminal-output; +} + +/// An interface providing an optional `terminal-input` for stdin as a +/// link-time authority. +interface terminal-stdin { + use terminal-input.{terminal-input}; + + /// If stdin is connected to a terminal, return a `terminal-input` handle + /// allowing further interaction with it. + get-terminal-stdin: func() -> option; +} + +/// An interface providing an optional `terminal-output` for stdout as a +/// link-time authority. +interface terminal-stdout { + use terminal-output.{terminal-output}; + + /// If stdout is connected to a terminal, return a `terminal-output` handle + /// allowing further interaction with it. + get-terminal-stdout: func() -> option; +} + +/// An interface providing an optional `terminal-output` for stderr as a +/// link-time authority. +interface terminal-stderr { + use terminal-output.{terminal-output}; + + /// If stderr is connected to a terminal, return a `terminal-output` handle + /// allowing further interaction with it. + get-terminal-stderr: func() -> option; +} diff --git a/crates/wash/src/wit/deps/clocks/monotonic-clock.wit b/crates/wash/src/wit/deps/clocks/monotonic-clock.wit new file mode 100644 index 0000000000..4e4dc3a199 --- /dev/null +++ b/crates/wash/src/wit/deps/clocks/monotonic-clock.wit @@ -0,0 +1,45 @@ +package wasi:clocks@0.2.0; +/// WASI Monotonic Clock is a clock API intended to let users measure elapsed +/// time. +/// +/// It is intended to be portable at least between Unix-family platforms and +/// Windows. +/// +/// A monotonic clock is a clock which has an unspecified initial value, and +/// successive reads of the clock will produce non-decreasing values. +/// +/// It is intended for measuring elapsed time. +interface monotonic-clock { + use wasi:io/poll@0.2.0.{pollable}; + + /// An instant in time, in nanoseconds. An instant is relative to an + /// unspecified initial value, and can only be compared to instances from + /// the same monotonic-clock. + type instant = u64; + + /// A duration of time, in nanoseconds. + type duration = u64; + + /// Read the current value of the clock. + /// + /// The clock is monotonic, therefore calling this function repeatedly will + /// produce a sequence of non-decreasing values. + now: func() -> instant; + + /// Query the resolution of the clock. Returns the duration of time + /// corresponding to a clock tick. + resolution: func() -> duration; + + /// Create a `pollable` which will resolve once the specified instant + /// occured. + subscribe-instant: func( + when: instant, + ) -> pollable; + + /// Create a `pollable` which will resolve once the given duration has + /// elapsed, starting at the time at which this function was called. + /// occured. + subscribe-duration: func( + when: duration, + ) -> pollable; +} diff --git a/crates/wash/src/wit/deps/clocks/wall-clock.wit b/crates/wash/src/wit/deps/clocks/wall-clock.wit new file mode 100644 index 0000000000..440ca0f336 --- /dev/null +++ b/crates/wash/src/wit/deps/clocks/wall-clock.wit @@ -0,0 +1,42 @@ +package wasi:clocks@0.2.0; +/// WASI Wall Clock is a clock API intended to let users query the current +/// time. The name "wall" makes an analogy to a "clock on the wall", which +/// is not necessarily monotonic as it may be reset. +/// +/// It is intended to be portable at least between Unix-family platforms and +/// Windows. +/// +/// A wall clock is a clock which measures the date and time according to +/// some external reference. +/// +/// External references may be reset, so this clock is not necessarily +/// monotonic, making it unsuitable for measuring elapsed time. +/// +/// It is intended for reporting the current date and time for humans. +interface wall-clock { + /// A time and date in seconds plus nanoseconds. + record datetime { + seconds: u64, + nanoseconds: u32, + } + + /// Read the current value of the clock. + /// + /// This clock is not monotonic, therefore calling this function repeatedly + /// will not necessarily produce a sequence of non-decreasing values. + /// + /// The returned timestamps represent the number of seconds since + /// 1970-01-01T00:00:00Z, also known as [POSIX's Seconds Since the Epoch], + /// also known as [Unix Time]. + /// + /// The nanoseconds field of the output is always less than 1000000000. + /// + /// [POSIX's Seconds Since the Epoch]: https://pubs.opengroup.org/onlinepubs/9699919799/xrat/V4_xbd_chap04.html#tag_21_04_16 + /// [Unix Time]: https://en.wikipedia.org/wiki/Unix_time + now: func() -> datetime; + + /// Query the resolution of the clock. + /// + /// The nanoseconds field of the output is always less than 1000000000. + resolution: func() -> datetime; +} diff --git a/crates/wash/src/wit/deps/clocks/world.wit b/crates/wash/src/wit/deps/clocks/world.wit new file mode 100644 index 0000000000..c0224572a5 --- /dev/null +++ b/crates/wash/src/wit/deps/clocks/world.wit @@ -0,0 +1,6 @@ +package wasi:clocks@0.2.0; + +world imports { + import monotonic-clock; + import wall-clock; +} diff --git a/crates/wash/src/wit/deps/filesystem/preopens.wit b/crates/wash/src/wit/deps/filesystem/preopens.wit new file mode 100644 index 0000000000..da801f6d60 --- /dev/null +++ b/crates/wash/src/wit/deps/filesystem/preopens.wit @@ -0,0 +1,8 @@ +package wasi:filesystem@0.2.0; + +interface preopens { + use types.{descriptor}; + + /// Return the set of preopened directories, and their path. + get-directories: func() -> list>; +} diff --git a/crates/wash/src/wit/deps/filesystem/types.wit b/crates/wash/src/wit/deps/filesystem/types.wit new file mode 100644 index 0000000000..11108fcda2 --- /dev/null +++ b/crates/wash/src/wit/deps/filesystem/types.wit @@ -0,0 +1,634 @@ +package wasi:filesystem@0.2.0; +/// WASI filesystem is a filesystem API primarily intended to let users run WASI +/// programs that access their files on their existing filesystems, without +/// significant overhead. +/// +/// It is intended to be roughly portable between Unix-family platforms and +/// Windows, though it does not hide many of the major differences. +/// +/// Paths are passed as interface-type `string`s, meaning they must consist of +/// a sequence of Unicode Scalar Values (USVs). Some filesystems may contain +/// paths which are not accessible by this API. +/// +/// The directory separator in WASI is always the forward-slash (`/`). +/// +/// All paths in WASI are relative paths, and are interpreted relative to a +/// `descriptor` referring to a base directory. If a `path` argument to any WASI +/// function starts with `/`, or if any step of resolving a `path`, including +/// `..` and symbolic link steps, reaches a directory outside of the base +/// directory, or reaches a symlink to an absolute or rooted path in the +/// underlying filesystem, the function fails with `error-code::not-permitted`. +/// +/// For more information about WASI path resolution and sandboxing, see +/// [WASI filesystem path resolution]. +/// +/// [WASI filesystem path resolution]: https://github.com/WebAssembly/wasi-filesystem/blob/main/path-resolution.md +interface types { + use wasi:io/streams@0.2.0.{input-stream, output-stream, error}; + use wasi:clocks/wall-clock@0.2.0.{datetime}; + + /// File size or length of a region within a file. + type filesize = u64; + + /// The type of a filesystem object referenced by a descriptor. + /// + /// Note: This was called `filetype` in earlier versions of WASI. + enum descriptor-type { + /// The type of the descriptor or file is unknown or is different from + /// any of the other types specified. + unknown, + /// The descriptor refers to a block device inode. + block-device, + /// The descriptor refers to a character device inode. + character-device, + /// The descriptor refers to a directory inode. + directory, + /// The descriptor refers to a named pipe. + fifo, + /// The file refers to a symbolic link inode. + symbolic-link, + /// The descriptor refers to a regular file inode. + regular-file, + /// The descriptor refers to a socket. + socket, + } + + /// Descriptor flags. + /// + /// Note: This was called `fdflags` in earlier versions of WASI. + flags descriptor-flags { + /// Read mode: Data can be read. + read, + /// Write mode: Data can be written to. + write, + /// Request that writes be performed according to synchronized I/O file + /// integrity completion. The data stored in the file and the file's + /// metadata are synchronized. This is similar to `O_SYNC` in POSIX. + /// + /// The precise semantics of this operation have not yet been defined for + /// WASI. At this time, it should be interpreted as a request, and not a + /// requirement. + file-integrity-sync, + /// Request that writes be performed according to synchronized I/O data + /// integrity completion. Only the data stored in the file is + /// synchronized. This is similar to `O_DSYNC` in POSIX. + /// + /// The precise semantics of this operation have not yet been defined for + /// WASI. At this time, it should be interpreted as a request, and not a + /// requirement. + data-integrity-sync, + /// Requests that reads be performed at the same level of integrety + /// requested for writes. This is similar to `O_RSYNC` in POSIX. + /// + /// The precise semantics of this operation have not yet been defined for + /// WASI. At this time, it should be interpreted as a request, and not a + /// requirement. + requested-write-sync, + /// Mutating directories mode: Directory contents may be mutated. + /// + /// When this flag is unset on a descriptor, operations using the + /// descriptor which would create, rename, delete, modify the data or + /// metadata of filesystem objects, or obtain another handle which + /// would permit any of those, shall fail with `error-code::read-only` if + /// they would otherwise succeed. + /// + /// This may only be set on directories. + mutate-directory, + } + + /// File attributes. + /// + /// Note: This was called `filestat` in earlier versions of WASI. + record descriptor-stat { + /// File type. + %type: descriptor-type, + /// Number of hard links to the file. + link-count: link-count, + /// For regular files, the file size in bytes. For symbolic links, the + /// length in bytes of the pathname contained in the symbolic link. + size: filesize, + /// Last data access timestamp. + /// + /// If the `option` is none, the platform doesn't maintain an access + /// timestamp for this file. + data-access-timestamp: option, + /// Last data modification timestamp. + /// + /// If the `option` is none, the platform doesn't maintain a + /// modification timestamp for this file. + data-modification-timestamp: option, + /// Last file status-change timestamp. + /// + /// If the `option` is none, the platform doesn't maintain a + /// status-change timestamp for this file. + status-change-timestamp: option, + } + + /// Flags determining the method of how paths are resolved. + flags path-flags { + /// As long as the resolved path corresponds to a symbolic link, it is + /// expanded. + symlink-follow, + } + + /// Open flags used by `open-at`. + flags open-flags { + /// Create file if it does not exist, similar to `O_CREAT` in POSIX. + create, + /// Fail if not a directory, similar to `O_DIRECTORY` in POSIX. + directory, + /// Fail if file already exists, similar to `O_EXCL` in POSIX. + exclusive, + /// Truncate file to size 0, similar to `O_TRUNC` in POSIX. + truncate, + } + + /// Number of hard links to an inode. + type link-count = u64; + + /// When setting a timestamp, this gives the value to set it to. + variant new-timestamp { + /// Leave the timestamp set to its previous value. + no-change, + /// Set the timestamp to the current time of the system clock associated + /// with the filesystem. + now, + /// Set the timestamp to the given value. + timestamp(datetime), + } + + /// A directory entry. + record directory-entry { + /// The type of the file referred to by this directory entry. + %type: descriptor-type, + + /// The name of the object. + name: string, + } + + /// Error codes returned by functions, similar to `errno` in POSIX. + /// Not all of these error codes are returned by the functions provided by this + /// API; some are used in higher-level library layers, and others are provided + /// merely for alignment with POSIX. + enum error-code { + /// Permission denied, similar to `EACCES` in POSIX. + access, + /// Resource unavailable, or operation would block, similar to `EAGAIN` and `EWOULDBLOCK` in POSIX. + would-block, + /// Connection already in progress, similar to `EALREADY` in POSIX. + already, + /// Bad descriptor, similar to `EBADF` in POSIX. + bad-descriptor, + /// Device or resource busy, similar to `EBUSY` in POSIX. + busy, + /// Resource deadlock would occur, similar to `EDEADLK` in POSIX. + deadlock, + /// Storage quota exceeded, similar to `EDQUOT` in POSIX. + quota, + /// File exists, similar to `EEXIST` in POSIX. + exist, + /// File too large, similar to `EFBIG` in POSIX. + file-too-large, + /// Illegal byte sequence, similar to `EILSEQ` in POSIX. + illegal-byte-sequence, + /// Operation in progress, similar to `EINPROGRESS` in POSIX. + in-progress, + /// Interrupted function, similar to `EINTR` in POSIX. + interrupted, + /// Invalid argument, similar to `EINVAL` in POSIX. + invalid, + /// I/O error, similar to `EIO` in POSIX. + io, + /// Is a directory, similar to `EISDIR` in POSIX. + is-directory, + /// Too many levels of symbolic links, similar to `ELOOP` in POSIX. + loop, + /// Too many links, similar to `EMLINK` in POSIX. + too-many-links, + /// Message too large, similar to `EMSGSIZE` in POSIX. + message-size, + /// Filename too long, similar to `ENAMETOOLONG` in POSIX. + name-too-long, + /// No such device, similar to `ENODEV` in POSIX. + no-device, + /// No such file or directory, similar to `ENOENT` in POSIX. + no-entry, + /// No locks available, similar to `ENOLCK` in POSIX. + no-lock, + /// Not enough space, similar to `ENOMEM` in POSIX. + insufficient-memory, + /// No space left on device, similar to `ENOSPC` in POSIX. + insufficient-space, + /// Not a directory or a symbolic link to a directory, similar to `ENOTDIR` in POSIX. + not-directory, + /// Directory not empty, similar to `ENOTEMPTY` in POSIX. + not-empty, + /// State not recoverable, similar to `ENOTRECOVERABLE` in POSIX. + not-recoverable, + /// Not supported, similar to `ENOTSUP` and `ENOSYS` in POSIX. + unsupported, + /// Inappropriate I/O control operation, similar to `ENOTTY` in POSIX. + no-tty, + /// No such device or address, similar to `ENXIO` in POSIX. + no-such-device, + /// Value too large to be stored in data type, similar to `EOVERFLOW` in POSIX. + overflow, + /// Operation not permitted, similar to `EPERM` in POSIX. + not-permitted, + /// Broken pipe, similar to `EPIPE` in POSIX. + pipe, + /// Read-only file system, similar to `EROFS` in POSIX. + read-only, + /// Invalid seek, similar to `ESPIPE` in POSIX. + invalid-seek, + /// Text file busy, similar to `ETXTBSY` in POSIX. + text-file-busy, + /// Cross-device link, similar to `EXDEV` in POSIX. + cross-device, + } + + /// File or memory access pattern advisory information. + enum advice { + /// The application has no advice to give on its behavior with respect + /// to the specified data. + normal, + /// The application expects to access the specified data sequentially + /// from lower offsets to higher offsets. + sequential, + /// The application expects to access the specified data in a random + /// order. + random, + /// The application expects to access the specified data in the near + /// future. + will-need, + /// The application expects that it will not access the specified data + /// in the near future. + dont-need, + /// The application expects to access the specified data once and then + /// not reuse it thereafter. + no-reuse, + } + + /// A 128-bit hash value, split into parts because wasm doesn't have a + /// 128-bit integer type. + record metadata-hash-value { + /// 64 bits of a 128-bit hash value. + lower: u64, + /// Another 64 bits of a 128-bit hash value. + upper: u64, + } + + /// A descriptor is a reference to a filesystem object, which may be a file, + /// directory, named pipe, special file, or other object on which filesystem + /// calls may be made. + resource descriptor { + /// Return a stream for reading from a file, if available. + /// + /// May fail with an error-code describing why the file cannot be read. + /// + /// Multiple read, write, and append streams may be active on the same open + /// file and they do not interfere with each other. + /// + /// Note: This allows using `read-stream`, which is similar to `read` in POSIX. + read-via-stream: func( + /// The offset within the file at which to start reading. + offset: filesize, + ) -> result; + + /// Return a stream for writing to a file, if available. + /// + /// May fail with an error-code describing why the file cannot be written. + /// + /// Note: This allows using `write-stream`, which is similar to `write` in + /// POSIX. + write-via-stream: func( + /// The offset within the file at which to start writing. + offset: filesize, + ) -> result; + + /// Return a stream for appending to a file, if available. + /// + /// May fail with an error-code describing why the file cannot be appended. + /// + /// Note: This allows using `write-stream`, which is similar to `write` with + /// `O_APPEND` in in POSIX. + append-via-stream: func() -> result; + + /// Provide file advisory information on a descriptor. + /// + /// This is similar to `posix_fadvise` in POSIX. + advise: func( + /// The offset within the file to which the advisory applies. + offset: filesize, + /// The length of the region to which the advisory applies. + length: filesize, + /// The advice. + advice: advice + ) -> result<_, error-code>; + + /// Synchronize the data of a file to disk. + /// + /// This function succeeds with no effect if the file descriptor is not + /// opened for writing. + /// + /// Note: This is similar to `fdatasync` in POSIX. + sync-data: func() -> result<_, error-code>; + + /// Get flags associated with a descriptor. + /// + /// Note: This returns similar flags to `fcntl(fd, F_GETFL)` in POSIX. + /// + /// Note: This returns the value that was the `fs_flags` value returned + /// from `fdstat_get` in earlier versions of WASI. + get-flags: func() -> result; + + /// Get the dynamic type of a descriptor. + /// + /// Note: This returns the same value as the `type` field of the `fd-stat` + /// returned by `stat`, `stat-at` and similar. + /// + /// Note: This returns similar flags to the `st_mode & S_IFMT` value provided + /// by `fstat` in POSIX. + /// + /// Note: This returns the value that was the `fs_filetype` value returned + /// from `fdstat_get` in earlier versions of WASI. + get-type: func() -> result; + + /// Adjust the size of an open file. If this increases the file's size, the + /// extra bytes are filled with zeros. + /// + /// Note: This was called `fd_filestat_set_size` in earlier versions of WASI. + set-size: func(size: filesize) -> result<_, error-code>; + + /// Adjust the timestamps of an open file or directory. + /// + /// Note: This is similar to `futimens` in POSIX. + /// + /// Note: This was called `fd_filestat_set_times` in earlier versions of WASI. + set-times: func( + /// The desired values of the data access timestamp. + data-access-timestamp: new-timestamp, + /// The desired values of the data modification timestamp. + data-modification-timestamp: new-timestamp, + ) -> result<_, error-code>; + + /// Read from a descriptor, without using and updating the descriptor's offset. + /// + /// This function returns a list of bytes containing the data that was + /// read, along with a bool which, when true, indicates that the end of the + /// file was reached. The returned list will contain up to `length` bytes; it + /// may return fewer than requested, if the end of the file is reached or + /// if the I/O operation is interrupted. + /// + /// In the future, this may change to return a `stream`. + /// + /// Note: This is similar to `pread` in POSIX. + read: func( + /// The maximum number of bytes to read. + length: filesize, + /// The offset within the file at which to read. + offset: filesize, + ) -> result, bool>, error-code>; + + /// Write to a descriptor, without using and updating the descriptor's offset. + /// + /// It is valid to write past the end of a file; the file is extended to the + /// extent of the write, with bytes between the previous end and the start of + /// the write set to zero. + /// + /// In the future, this may change to take a `stream`. + /// + /// Note: This is similar to `pwrite` in POSIX. + write: func( + /// Data to write + buffer: list, + /// The offset within the file at which to write. + offset: filesize, + ) -> result; + + /// Read directory entries from a directory. + /// + /// On filesystems where directories contain entries referring to themselves + /// and their parents, often named `.` and `..` respectively, these entries + /// are omitted. + /// + /// This always returns a new stream which starts at the beginning of the + /// directory. Multiple streams may be active on the same directory, and they + /// do not interfere with each other. + read-directory: func() -> result; + + /// Synchronize the data and metadata of a file to disk. + /// + /// This function succeeds with no effect if the file descriptor is not + /// opened for writing. + /// + /// Note: This is similar to `fsync` in POSIX. + sync: func() -> result<_, error-code>; + + /// Create a directory. + /// + /// Note: This is similar to `mkdirat` in POSIX. + create-directory-at: func( + /// The relative path at which to create the directory. + path: string, + ) -> result<_, error-code>; + + /// Return the attributes of an open file or directory. + /// + /// Note: This is similar to `fstat` in POSIX, except that it does not return + /// device and inode information. For testing whether two descriptors refer to + /// the same underlying filesystem object, use `is-same-object`. To obtain + /// additional data that can be used do determine whether a file has been + /// modified, use `metadata-hash`. + /// + /// Note: This was called `fd_filestat_get` in earlier versions of WASI. + stat: func() -> result; + + /// Return the attributes of a file or directory. + /// + /// Note: This is similar to `fstatat` in POSIX, except that it does not + /// return device and inode information. See the `stat` description for a + /// discussion of alternatives. + /// + /// Note: This was called `path_filestat_get` in earlier versions of WASI. + stat-at: func( + /// Flags determining the method of how the path is resolved. + path-flags: path-flags, + /// The relative path of the file or directory to inspect. + path: string, + ) -> result; + + /// Adjust the timestamps of a file or directory. + /// + /// Note: This is similar to `utimensat` in POSIX. + /// + /// Note: This was called `path_filestat_set_times` in earlier versions of + /// WASI. + set-times-at: func( + /// Flags determining the method of how the path is resolved. + path-flags: path-flags, + /// The relative path of the file or directory to operate on. + path: string, + /// The desired values of the data access timestamp. + data-access-timestamp: new-timestamp, + /// The desired values of the data modification timestamp. + data-modification-timestamp: new-timestamp, + ) -> result<_, error-code>; + + /// Create a hard link. + /// + /// Note: This is similar to `linkat` in POSIX. + link-at: func( + /// Flags determining the method of how the path is resolved. + old-path-flags: path-flags, + /// The relative source path from which to link. + old-path: string, + /// The base directory for `new-path`. + new-descriptor: borrow, + /// The relative destination path at which to create the hard link. + new-path: string, + ) -> result<_, error-code>; + + /// Open a file or directory. + /// + /// The returned descriptor is not guaranteed to be the lowest-numbered + /// descriptor not currently open/ it is randomized to prevent applications + /// from depending on making assumptions about indexes, since this is + /// error-prone in multi-threaded contexts. The returned descriptor is + /// guaranteed to be less than 2**31. + /// + /// If `flags` contains `descriptor-flags::mutate-directory`, and the base + /// descriptor doesn't have `descriptor-flags::mutate-directory` set, + /// `open-at` fails with `error-code::read-only`. + /// + /// If `flags` contains `write` or `mutate-directory`, or `open-flags` + /// contains `truncate` or `create`, and the base descriptor doesn't have + /// `descriptor-flags::mutate-directory` set, `open-at` fails with + /// `error-code::read-only`. + /// + /// Note: This is similar to `openat` in POSIX. + open-at: func( + /// Flags determining the method of how the path is resolved. + path-flags: path-flags, + /// The relative path of the object to open. + path: string, + /// The method by which to open the file. + open-flags: open-flags, + /// Flags to use for the resulting descriptor. + %flags: descriptor-flags, + ) -> result; + + /// Read the contents of a symbolic link. + /// + /// If the contents contain an absolute or rooted path in the underlying + /// filesystem, this function fails with `error-code::not-permitted`. + /// + /// Note: This is similar to `readlinkat` in POSIX. + readlink-at: func( + /// The relative path of the symbolic link from which to read. + path: string, + ) -> result; + + /// Remove a directory. + /// + /// Return `error-code::not-empty` if the directory is not empty. + /// + /// Note: This is similar to `unlinkat(fd, path, AT_REMOVEDIR)` in POSIX. + remove-directory-at: func( + /// The relative path to a directory to remove. + path: string, + ) -> result<_, error-code>; + + /// Rename a filesystem object. + /// + /// Note: This is similar to `renameat` in POSIX. + rename-at: func( + /// The relative source path of the file or directory to rename. + old-path: string, + /// The base directory for `new-path`. + new-descriptor: borrow, + /// The relative destination path to which to rename the file or directory. + new-path: string, + ) -> result<_, error-code>; + + /// Create a symbolic link (also known as a "symlink"). + /// + /// If `old-path` starts with `/`, the function fails with + /// `error-code::not-permitted`. + /// + /// Note: This is similar to `symlinkat` in POSIX. + symlink-at: func( + /// The contents of the symbolic link. + old-path: string, + /// The relative destination path at which to create the symbolic link. + new-path: string, + ) -> result<_, error-code>; + + /// Unlink a filesystem object that is not a directory. + /// + /// Return `error-code::is-directory` if the path refers to a directory. + /// Note: This is similar to `unlinkat(fd, path, 0)` in POSIX. + unlink-file-at: func( + /// The relative path to a file to unlink. + path: string, + ) -> result<_, error-code>; + + /// Test whether two descriptors refer to the same filesystem object. + /// + /// In POSIX, this corresponds to testing whether the two descriptors have the + /// same device (`st_dev`) and inode (`st_ino` or `d_ino`) numbers. + /// wasi-filesystem does not expose device and inode numbers, so this function + /// may be used instead. + is-same-object: func(other: borrow) -> bool; + + /// Return a hash of the metadata associated with a filesystem object referred + /// to by a descriptor. + /// + /// This returns a hash of the last-modification timestamp and file size, and + /// may also include the inode number, device number, birth timestamp, and + /// other metadata fields that may change when the file is modified or + /// replaced. It may also include a secret value chosen by the + /// implementation and not otherwise exposed. + /// + /// Implementations are encourated to provide the following properties: + /// + /// - If the file is not modified or replaced, the computed hash value should + /// usually not change. + /// - If the object is modified or replaced, the computed hash value should + /// usually change. + /// - The inputs to the hash should not be easily computable from the + /// computed hash. + /// + /// However, none of these is required. + metadata-hash: func() -> result; + + /// Return a hash of the metadata associated with a filesystem object referred + /// to by a directory descriptor and a relative path. + /// + /// This performs the same hash computation as `metadata-hash`. + metadata-hash-at: func( + /// Flags determining the method of how the path is resolved. + path-flags: path-flags, + /// The relative path of the file or directory to inspect. + path: string, + ) -> result; + } + + /// A stream of directory entries. + resource directory-entry-stream { + /// Read a single directory entry from a `directory-entry-stream`. + read-directory-entry: func() -> result, error-code>; + } + + /// Attempts to extract a filesystem-related `error-code` from the stream + /// `error` provided. + /// + /// Stream operations which return `stream-error::last-operation-failed` + /// have a payload with more information about the operation that failed. + /// This payload can be passed through to this function to see if there's + /// filesystem-related information about the error to return. + /// + /// Note that this function is fallible because not all stream-related + /// errors are filesystem-related errors. + filesystem-error-code: func(err: borrow) -> option; +} diff --git a/crates/wash/src/wit/deps/filesystem/world.wit b/crates/wash/src/wit/deps/filesystem/world.wit new file mode 100644 index 0000000000..663f57920d --- /dev/null +++ b/crates/wash/src/wit/deps/filesystem/world.wit @@ -0,0 +1,6 @@ +package wasi:filesystem@0.2.0; + +world imports { + import types; + import preopens; +} diff --git a/crates/wash/src/wit/deps/http/handler.wit b/crates/wash/src/wit/deps/http/handler.wit new file mode 100644 index 0000000000..a34a0649d5 --- /dev/null +++ b/crates/wash/src/wit/deps/http/handler.wit @@ -0,0 +1,43 @@ +/// This interface defines a handler of incoming HTTP Requests. It should +/// be exported by components which can respond to HTTP Requests. +interface incoming-handler { + use types.{incoming-request, response-outparam}; + + /// This function is invoked with an incoming HTTP Request, and a resource + /// `response-outparam` which provides the capability to reply with an HTTP + /// Response. The response is sent by calling the `response-outparam.set` + /// method, which allows execution to continue after the response has been + /// sent. This enables both streaming to the response body, and performing other + /// work. + /// + /// The implementor of this function must write a response to the + /// `response-outparam` before returning, or else the caller will respond + /// with an error on its behalf. + handle: func( + request: incoming-request, + response-out: response-outparam + ); +} + +/// This interface defines a handler of outgoing HTTP Requests. It should be +/// imported by components which wish to make HTTP Requests. +interface outgoing-handler { + use types.{ + outgoing-request, request-options, future-incoming-response, error-code + }; + + /// This function is invoked with an outgoing HTTP Request, and it returns + /// a resource `future-incoming-response` which represents an HTTP Response + /// which may arrive in the future. + /// + /// The `options` argument accepts optional parameters for the HTTP + /// protocol's transport layer. + /// + /// This function may return an error if the `outgoing-request` is invalid + /// or not allowed to be made. Otherwise, protocol errors are reported + /// through the `future-incoming-response`. + handle: func( + request: outgoing-request, + options: option + ) -> result; +} diff --git a/crates/wash/src/wit/deps/http/proxy.wit b/crates/wash/src/wit/deps/http/proxy.wit new file mode 100644 index 0000000000..687c24d233 --- /dev/null +++ b/crates/wash/src/wit/deps/http/proxy.wit @@ -0,0 +1,32 @@ +package wasi:http@0.2.0; + +/// The `wasi:http/proxy` world captures a widely-implementable intersection of +/// hosts that includes HTTP forward and reverse proxies. Components targeting +/// this world may concurrently stream in and out any number of incoming and +/// outgoing HTTP requests. +world proxy { + /// HTTP proxies have access to time and randomness. + include wasi:clocks/imports@0.2.0; + import wasi:random/random@0.2.0; + + /// Proxies have standard output and error streams which are expected to + /// terminate in a developer-facing console provided by the host. + import wasi:cli/stdout@0.2.0; + import wasi:cli/stderr@0.2.0; + + /// TODO: this is a temporary workaround until component tooling is able to + /// gracefully handle the absence of stdin. Hosts must return an eof stream + /// for this import, which is what wasi-libc + tooling will do automatically + /// when this import is properly removed. + import wasi:cli/stdin@0.2.0; + + /// This is the default handler to use when user code simply wants to make an + /// HTTP request (e.g., via `fetch()`). + import outgoing-handler; + + /// The host delivers incoming HTTP requests to a component by calling the + /// `handle` function of this exported interface. A host may arbitrarily reuse + /// or not reuse component instance when delivering incoming HTTP requests and + /// thus a component must be able to handle 0..N calls to `handle`. + export incoming-handler; +} diff --git a/crates/wash/src/wit/deps/http/types.wit b/crates/wash/src/wit/deps/http/types.wit new file mode 100644 index 0000000000..755ac6a6bc --- /dev/null +++ b/crates/wash/src/wit/deps/http/types.wit @@ -0,0 +1,570 @@ +/// This interface defines all of the types and methods for implementing +/// HTTP Requests and Responses, both incoming and outgoing, as well as +/// their headers, trailers, and bodies. +interface types { + use wasi:clocks/monotonic-clock@0.2.0.{duration}; + use wasi:io/streams@0.2.0.{input-stream, output-stream}; + use wasi:io/error@0.2.0.{error as io-error}; + use wasi:io/poll@0.2.0.{pollable}; + + /// This type corresponds to HTTP standard Methods. + variant method { + get, + head, + post, + put, + delete, + connect, + options, + trace, + patch, + other(string) + } + + /// This type corresponds to HTTP standard Related Schemes. + variant scheme { + HTTP, + HTTPS, + other(string) + } + + /// These cases are inspired by the IANA HTTP Proxy Error Types: + /// https://www.iana.org/assignments/http-proxy-status/http-proxy-status.xhtml#table-http-proxy-error-types + variant error-code { + DNS-timeout, + DNS-error(DNS-error-payload), + destination-not-found, + destination-unavailable, + destination-IP-prohibited, + destination-IP-unroutable, + connection-refused, + connection-terminated, + connection-timeout, + connection-read-timeout, + connection-write-timeout, + connection-limit-reached, + TLS-protocol-error, + TLS-certificate-error, + TLS-alert-received(TLS-alert-received-payload), + HTTP-request-denied, + HTTP-request-length-required, + HTTP-request-body-size(option), + HTTP-request-method-invalid, + HTTP-request-URI-invalid, + HTTP-request-URI-too-long, + HTTP-request-header-section-size(option), + HTTP-request-header-size(option), + HTTP-request-trailer-section-size(option), + HTTP-request-trailer-size(field-size-payload), + HTTP-response-incomplete, + HTTP-response-header-section-size(option), + HTTP-response-header-size(field-size-payload), + HTTP-response-body-size(option), + HTTP-response-trailer-section-size(option), + HTTP-response-trailer-size(field-size-payload), + HTTP-response-transfer-coding(option), + HTTP-response-content-coding(option), + HTTP-response-timeout, + HTTP-upgrade-failed, + HTTP-protocol-error, + loop-detected, + configuration-error, + /// This is a catch-all error for anything that doesn't fit cleanly into a + /// more specific case. It also includes an optional string for an + /// unstructured description of the error. Users should not depend on the + /// string for diagnosing errors, as it's not required to be consistent + /// between implementations. + internal-error(option) + } + + /// Defines the case payload type for `DNS-error` above: + record DNS-error-payload { + rcode: option, + info-code: option + } + + /// Defines the case payload type for `TLS-alert-received` above: + record TLS-alert-received-payload { + alert-id: option, + alert-message: option + } + + /// Defines the case payload type for `HTTP-response-{header,trailer}-size` above: + record field-size-payload { + field-name: option, + field-size: option + } + + /// Attempts to extract a http-related `error` from the wasi:io `error` + /// provided. + /// + /// Stream operations which return + /// `wasi:io/stream/stream-error::last-operation-failed` have a payload of + /// type `wasi:io/error/error` with more information about the operation + /// that failed. This payload can be passed through to this function to see + /// if there's http-related information about the error to return. + /// + /// Note that this function is fallible because not all io-errors are + /// http-related errors. + http-error-code: func(err: borrow) -> option; + + /// This type enumerates the different kinds of errors that may occur when + /// setting or appending to a `fields` resource. + variant header-error { + /// This error indicates that a `field-key` or `field-value` was + /// syntactically invalid when used with an operation that sets headers in a + /// `fields`. + invalid-syntax, + + /// This error indicates that a forbidden `field-key` was used when trying + /// to set a header in a `fields`. + forbidden, + + /// This error indicates that the operation on the `fields` was not + /// permitted because the fields are immutable. + immutable, + } + + /// Field keys are always strings. + type field-key = string; + + /// Field values should always be ASCII strings. However, in + /// reality, HTTP implementations often have to interpret malformed values, + /// so they are provided as a list of bytes. + type field-value = list; + + /// This following block defines the `fields` resource which corresponds to + /// HTTP standard Fields. Fields are a common representation used for both + /// Headers and Trailers. + /// + /// A `fields` may be mutable or immutable. A `fields` created using the + /// constructor, `from-list`, or `clone` will be mutable, but a `fields` + /// resource given by other means (including, but not limited to, + /// `incoming-request.headers`, `outgoing-request.headers`) might be be + /// immutable. In an immutable fields, the `set`, `append`, and `delete` + /// operations will fail with `header-error.immutable`. + resource fields { + + /// Construct an empty HTTP Fields. + /// + /// The resulting `fields` is mutable. + constructor(); + + /// Construct an HTTP Fields. + /// + /// The resulting `fields` is mutable. + /// + /// The list represents each key-value pair in the Fields. Keys + /// which have multiple values are represented by multiple entries in this + /// list with the same key. + /// + /// The tuple is a pair of the field key, represented as a string, and + /// Value, represented as a list of bytes. In a valid Fields, all keys + /// and values are valid UTF-8 strings. However, values are not always + /// well-formed, so they are represented as a raw list of bytes. + /// + /// An error result will be returned if any header or value was + /// syntactically invalid, or if a header was forbidden. + from-list: static func( + entries: list> + ) -> result; + + /// Get all of the values corresponding to a key. If the key is not present + /// in this `fields`, an empty list is returned. However, if the key is + /// present but empty, this is represented by a list with one or more + /// empty field-values present. + get: func(name: field-key) -> list; + + /// Returns `true` when the key is present in this `fields`. If the key is + /// syntactically invalid, `false` is returned. + has: func(name: field-key) -> bool; + + /// Set all of the values for a key. Clears any existing values for that + /// key, if they have been set. + /// + /// Fails with `header-error.immutable` if the `fields` are immutable. + set: func(name: field-key, value: list) -> result<_, header-error>; + + /// Delete all values for a key. Does nothing if no values for the key + /// exist. + /// + /// Fails with `header-error.immutable` if the `fields` are immutable. + delete: func(name: field-key) -> result<_, header-error>; + + /// Append a value for a key. Does not change or delete any existing + /// values for that key. + /// + /// Fails with `header-error.immutable` if the `fields` are immutable. + append: func(name: field-key, value: field-value) -> result<_, header-error>; + + /// Retrieve the full set of keys and values in the Fields. Like the + /// constructor, the list represents each key-value pair. + /// + /// The outer list represents each key-value pair in the Fields. Keys + /// which have multiple values are represented by multiple entries in this + /// list with the same key. + entries: func() -> list>; + + /// Make a deep copy of the Fields. Equivelant in behavior to calling the + /// `fields` constructor on the return value of `entries`. The resulting + /// `fields` is mutable. + clone: func() -> fields; + } + + /// Headers is an alias for Fields. + type headers = fields; + + /// Trailers is an alias for Fields. + type trailers = fields; + + /// Represents an incoming HTTP Request. + resource incoming-request { + + /// Returns the method of the incoming request. + method: func() -> method; + + /// Returns the path with query parameters from the request, as a string. + path-with-query: func() -> option; + + /// Returns the protocol scheme from the request. + scheme: func() -> option; + + /// Returns the authority from the request, if it was present. + authority: func() -> option; + + /// Get the `headers` associated with the request. + /// + /// The returned `headers` resource is immutable: `set`, `append`, and + /// `delete` operations will fail with `header-error.immutable`. + /// + /// The `headers` returned are a child resource: it must be dropped before + /// the parent `incoming-request` is dropped. Dropping this + /// `incoming-request` before all children are dropped will trap. + headers: func() -> headers; + + /// Gives the `incoming-body` associated with this request. Will only + /// return success at most once, and subsequent calls will return error. + consume: func() -> result; + } + + /// Represents an outgoing HTTP Request. + resource outgoing-request { + + /// Construct a new `outgoing-request` with a default `method` of `GET`, and + /// `none` values for `path-with-query`, `scheme`, and `authority`. + /// + /// * `headers` is the HTTP Headers for the Request. + /// + /// It is possible to construct, or manipulate with the accessor functions + /// below, an `outgoing-request` with an invalid combination of `scheme` + /// and `authority`, or `headers` which are not permitted to be sent. + /// It is the obligation of the `outgoing-handler.handle` implementation + /// to reject invalid constructions of `outgoing-request`. + constructor( + headers: headers + ); + + /// Returns the resource corresponding to the outgoing Body for this + /// Request. + /// + /// Returns success on the first call: the `outgoing-body` resource for + /// this `outgoing-request` can be retrieved at most once. Subsequent + /// calls will return error. + body: func() -> result; + + /// Get the Method for the Request. + method: func() -> method; + /// Set the Method for the Request. Fails if the string present in a + /// `method.other` argument is not a syntactically valid method. + set-method: func(method: method) -> result; + + /// Get the combination of the HTTP Path and Query for the Request. + /// When `none`, this represents an empty Path and empty Query. + path-with-query: func() -> option; + /// Set the combination of the HTTP Path and Query for the Request. + /// When `none`, this represents an empty Path and empty Query. Fails is the + /// string given is not a syntactically valid path and query uri component. + set-path-with-query: func(path-with-query: option) -> result; + + /// Get the HTTP Related Scheme for the Request. When `none`, the + /// implementation may choose an appropriate default scheme. + scheme: func() -> option; + /// Set the HTTP Related Scheme for the Request. When `none`, the + /// implementation may choose an appropriate default scheme. Fails if the + /// string given is not a syntactically valid uri scheme. + set-scheme: func(scheme: option) -> result; + + /// Get the HTTP Authority for the Request. A value of `none` may be used + /// with Related Schemes which do not require an Authority. The HTTP and + /// HTTPS schemes always require an authority. + authority: func() -> option; + /// Set the HTTP Authority for the Request. A value of `none` may be used + /// with Related Schemes which do not require an Authority. The HTTP and + /// HTTPS schemes always require an authority. Fails if the string given is + /// not a syntactically valid uri authority. + set-authority: func(authority: option) -> result; + + /// Get the headers associated with the Request. + /// + /// The returned `headers` resource is immutable: `set`, `append`, and + /// `delete` operations will fail with `header-error.immutable`. + /// + /// This headers resource is a child: it must be dropped before the parent + /// `outgoing-request` is dropped, or its ownership is transfered to + /// another component by e.g. `outgoing-handler.handle`. + headers: func() -> headers; + } + + /// Parameters for making an HTTP Request. Each of these parameters is + /// currently an optional timeout applicable to the transport layer of the + /// HTTP protocol. + /// + /// These timeouts are separate from any the user may use to bound a + /// blocking call to `wasi:io/poll.poll`. + resource request-options { + /// Construct a default `request-options` value. + constructor(); + + /// The timeout for the initial connect to the HTTP Server. + connect-timeout: func() -> option; + + /// Set the timeout for the initial connect to the HTTP Server. An error + /// return value indicates that this timeout is not supported. + set-connect-timeout: func(duration: option) -> result; + + /// The timeout for receiving the first byte of the Response body. + first-byte-timeout: func() -> option; + + /// Set the timeout for receiving the first byte of the Response body. An + /// error return value indicates that this timeout is not supported. + set-first-byte-timeout: func(duration: option) -> result; + + /// The timeout for receiving subsequent chunks of bytes in the Response + /// body stream. + between-bytes-timeout: func() -> option; + + /// Set the timeout for receiving subsequent chunks of bytes in the Response + /// body stream. An error return value indicates that this timeout is not + /// supported. + set-between-bytes-timeout: func(duration: option) -> result; + } + + /// Represents the ability to send an HTTP Response. + /// + /// This resource is used by the `wasi:http/incoming-handler` interface to + /// allow a Response to be sent corresponding to the Request provided as the + /// other argument to `incoming-handler.handle`. + resource response-outparam { + + /// Set the value of the `response-outparam` to either send a response, + /// or indicate an error. + /// + /// This method consumes the `response-outparam` to ensure that it is + /// called at most once. If it is never called, the implementation + /// will respond with an error. + /// + /// The user may provide an `error` to `response` to allow the + /// implementation determine how to respond with an HTTP error response. + set: static func( + param: response-outparam, + response: result, + ); + } + + /// This type corresponds to the HTTP standard Status Code. + type status-code = u16; + + /// Represents an incoming HTTP Response. + resource incoming-response { + + /// Returns the status code from the incoming response. + status: func() -> status-code; + + /// Returns the headers from the incoming response. + /// + /// The returned `headers` resource is immutable: `set`, `append`, and + /// `delete` operations will fail with `header-error.immutable`. + /// + /// This headers resource is a child: it must be dropped before the parent + /// `incoming-response` is dropped. + headers: func() -> headers; + + /// Returns the incoming body. May be called at most once. Returns error + /// if called additional times. + consume: func() -> result; + } + + /// Represents an incoming HTTP Request or Response's Body. + /// + /// A body has both its contents - a stream of bytes - and a (possibly + /// empty) set of trailers, indicating that the full contents of the + /// body have been received. This resource represents the contents as + /// an `input-stream` and the delivery of trailers as a `future-trailers`, + /// and ensures that the user of this interface may only be consuming either + /// the body contents or waiting on trailers at any given time. + resource incoming-body { + + /// Returns the contents of the body, as a stream of bytes. + /// + /// Returns success on first call: the stream representing the contents + /// can be retrieved at most once. Subsequent calls will return error. + /// + /// The returned `input-stream` resource is a child: it must be dropped + /// before the parent `incoming-body` is dropped, or consumed by + /// `incoming-body.finish`. + /// + /// This invariant ensures that the implementation can determine whether + /// the user is consuming the contents of the body, waiting on the + /// `future-trailers` to be ready, or neither. This allows for network + /// backpressure is to be applied when the user is consuming the body, + /// and for that backpressure to not inhibit delivery of the trailers if + /// the user does not read the entire body. + %stream: func() -> result; + + /// Takes ownership of `incoming-body`, and returns a `future-trailers`. + /// This function will trap if the `input-stream` child is still alive. + finish: static func(this: incoming-body) -> future-trailers; + } + + /// Represents a future which may eventaully return trailers, or an error. + /// + /// In the case that the incoming HTTP Request or Response did not have any + /// trailers, this future will resolve to the empty set of trailers once the + /// complete Request or Response body has been received. + resource future-trailers { + + /// Returns a pollable which becomes ready when either the trailers have + /// been received, or an error has occured. When this pollable is ready, + /// the `get` method will return `some`. + subscribe: func() -> pollable; + + /// Returns the contents of the trailers, or an error which occured, + /// once the future is ready. + /// + /// The outer `option` represents future readiness. Users can wait on this + /// `option` to become `some` using the `subscribe` method. + /// + /// The outer `result` is used to retrieve the trailers or error at most + /// once. It will be success on the first call in which the outer option + /// is `some`, and error on subsequent calls. + /// + /// The inner `result` represents that either the HTTP Request or Response + /// body, as well as any trailers, were received successfully, or that an + /// error occured receiving them. The optional `trailers` indicates whether + /// or not trailers were present in the body. + /// + /// When some `trailers` are returned by this method, the `trailers` + /// resource is immutable, and a child. Use of the `set`, `append`, or + /// `delete` methods will return an error, and the resource must be + /// dropped before the parent `future-trailers` is dropped. + get: func() -> option, error-code>>>; + } + + /// Represents an outgoing HTTP Response. + resource outgoing-response { + + /// Construct an `outgoing-response`, with a default `status-code` of `200`. + /// If a different `status-code` is needed, it must be set via the + /// `set-status-code` method. + /// + /// * `headers` is the HTTP Headers for the Response. + constructor(headers: headers); + + /// Get the HTTP Status Code for the Response. + status-code: func() -> status-code; + + /// Set the HTTP Status Code for the Response. Fails if the status-code + /// given is not a valid http status code. + set-status-code: func(status-code: status-code) -> result; + + /// Get the headers associated with the Request. + /// + /// The returned `headers` resource is immutable: `set`, `append`, and + /// `delete` operations will fail with `header-error.immutable`. + /// + /// This headers resource is a child: it must be dropped before the parent + /// `outgoing-request` is dropped, or its ownership is transfered to + /// another component by e.g. `outgoing-handler.handle`. + headers: func() -> headers; + + /// Returns the resource corresponding to the outgoing Body for this Response. + /// + /// Returns success on the first call: the `outgoing-body` resource for + /// this `outgoing-response` can be retrieved at most once. Subsequent + /// calls will return error. + body: func() -> result; + } + + /// Represents an outgoing HTTP Request or Response's Body. + /// + /// A body has both its contents - a stream of bytes - and a (possibly + /// empty) set of trailers, inducating the full contents of the body + /// have been sent. This resource represents the contents as an + /// `output-stream` child resource, and the completion of the body (with + /// optional trailers) with a static function that consumes the + /// `outgoing-body` resource, and ensures that the user of this interface + /// may not write to the body contents after the body has been finished. + /// + /// If the user code drops this resource, as opposed to calling the static + /// method `finish`, the implementation should treat the body as incomplete, + /// and that an error has occured. The implementation should propogate this + /// error to the HTTP protocol by whatever means it has available, + /// including: corrupting the body on the wire, aborting the associated + /// Request, or sending a late status code for the Response. + resource outgoing-body { + + /// Returns a stream for writing the body contents. + /// + /// The returned `output-stream` is a child resource: it must be dropped + /// before the parent `outgoing-body` resource is dropped (or finished), + /// otherwise the `outgoing-body` drop or `finish` will trap. + /// + /// Returns success on the first call: the `output-stream` resource for + /// this `outgoing-body` may be retrieved at most once. Subsequent calls + /// will return error. + write: func() -> result; + + /// Finalize an outgoing body, optionally providing trailers. This must be + /// called to signal that the response is complete. If the `outgoing-body` + /// is dropped without calling `outgoing-body.finalize`, the implementation + /// should treat the body as corrupted. + /// + /// Fails if the body's `outgoing-request` or `outgoing-response` was + /// constructed with a Content-Length header, and the contents written + /// to the body (via `write`) does not match the value given in the + /// Content-Length. + finish: static func( + this: outgoing-body, + trailers: option + ) -> result<_, error-code>; + } + + /// Represents a future which may eventaully return an incoming HTTP + /// Response, or an error. + /// + /// This resource is returned by the `wasi:http/outgoing-handler` interface to + /// provide the HTTP Response corresponding to the sent Request. + resource future-incoming-response { + /// Returns a pollable which becomes ready when either the Response has + /// been received, or an error has occured. When this pollable is ready, + /// the `get` method will return `some`. + subscribe: func() -> pollable; + + /// Returns the incoming HTTP Response, or an error, once one is ready. + /// + /// The outer `option` represents future readiness. Users can wait on this + /// `option` to become `some` using the `subscribe` method. + /// + /// The outer `result` is used to retrieve the response or error at most + /// once. It will be success on the first call in which the outer option + /// is `some`, and error on subsequent calls. + /// + /// The inner `result` represents that either the incoming HTTP Response + /// status and headers have recieved successfully, or that an error + /// occured. Errors may also occur while consuming the response body, + /// but those will be reported by the `incoming-body` and its + /// `output-stream` child. + get: func() -> option>>; + + } +} diff --git a/crates/wash/src/wit/deps/io/error.wit b/crates/wash/src/wit/deps/io/error.wit new file mode 100644 index 0000000000..22e5b64894 --- /dev/null +++ b/crates/wash/src/wit/deps/io/error.wit @@ -0,0 +1,34 @@ +package wasi:io@0.2.0; + + +interface error { + /// A resource which represents some error information. + /// + /// The only method provided by this resource is `to-debug-string`, + /// which provides some human-readable information about the error. + /// + /// In the `wasi:io` package, this resource is returned through the + /// `wasi:io/streams/stream-error` type. + /// + /// To provide more specific error information, other interfaces may + /// provide functions to further "downcast" this error into more specific + /// error information. For example, `error`s returned in streams derived + /// from filesystem types to be described using the filesystem's own + /// error-code type, using the function + /// `wasi:filesystem/types/filesystem-error-code`, which takes a parameter + /// `borrow` and returns + /// `option`. + /// + /// The set of functions which can "downcast" an `error` into a more + /// concrete type is open. + resource error { + /// Returns a string that is suitable to assist humans in debugging + /// this error. + /// + /// WARNING: The returned string should not be consumed mechanically! + /// It may change across platforms, hosts, or other implementation + /// details. Parsing this string is a major platform-compatibility + /// hazard. + to-debug-string: func() -> string; + } +} diff --git a/crates/wash/src/wit/deps/io/poll.wit b/crates/wash/src/wit/deps/io/poll.wit new file mode 100644 index 0000000000..ddc67f8b7a --- /dev/null +++ b/crates/wash/src/wit/deps/io/poll.wit @@ -0,0 +1,41 @@ +package wasi:io@0.2.0; + +/// A poll API intended to let users wait for I/O events on multiple handles +/// at once. +interface poll { + /// `pollable` represents a single I/O event which may be ready, or not. + resource pollable { + + /// Return the readiness of a pollable. This function never blocks. + /// + /// Returns `true` when the pollable is ready, and `false` otherwise. + ready: func() -> bool; + + /// `block` returns immediately if the pollable is ready, and otherwise + /// blocks until ready. + /// + /// This function is equivalent to calling `poll.poll` on a list + /// containing only this pollable. + block: func(); + } + + /// Poll for completion on a set of pollables. + /// + /// This function takes a list of pollables, which identify I/O sources of + /// interest, and waits until one or more of the events is ready for I/O. + /// + /// The result `list` contains one or more indices of handles in the + /// argument list that is ready for I/O. + /// + /// If the list contains more elements than can be indexed with a `u32` + /// value, this function traps. + /// + /// A timeout can be implemented by adding a pollable from the + /// wasi-clocks API to the list. + /// + /// This function does not return a `result`; polling in itself does not + /// do any I/O so it doesn't fail. If any of the I/O sources identified by + /// the pollables has an error, it is indicated by marking the source as + /// being reaedy for I/O. + poll: func(in: list>) -> list; +} diff --git a/crates/wash/src/wit/deps/io/streams.wit b/crates/wash/src/wit/deps/io/streams.wit new file mode 100644 index 0000000000..6d2f871e3b --- /dev/null +++ b/crates/wash/src/wit/deps/io/streams.wit @@ -0,0 +1,262 @@ +package wasi:io@0.2.0; + +/// WASI I/O is an I/O abstraction API which is currently focused on providing +/// stream types. +/// +/// In the future, the component model is expected to add built-in stream types; +/// when it does, they are expected to subsume this API. +interface streams { + use error.{error}; + use poll.{pollable}; + + /// An error for input-stream and output-stream operations. + variant stream-error { + /// The last operation (a write or flush) failed before completion. + /// + /// More information is available in the `error` payload. + last-operation-failed(error), + /// The stream is closed: no more input will be accepted by the + /// stream. A closed output-stream will return this error on all + /// future operations. + closed + } + + /// An input bytestream. + /// + /// `input-stream`s are *non-blocking* to the extent practical on underlying + /// platforms. I/O operations always return promptly; if fewer bytes are + /// promptly available than requested, they return the number of bytes promptly + /// available, which could even be zero. To wait for data to be available, + /// use the `subscribe` function to obtain a `pollable` which can be polled + /// for using `wasi:io/poll`. + resource input-stream { + /// Perform a non-blocking read from the stream. + /// + /// When the source of a `read` is binary data, the bytes from the source + /// are returned verbatim. When the source of a `read` is known to the + /// implementation to be text, bytes containing the UTF-8 encoding of the + /// text are returned. + /// + /// This function returns a list of bytes containing the read data, + /// when successful. The returned list will contain up to `len` bytes; + /// it may return fewer than requested, but not more. The list is + /// empty when no bytes are available for reading at this time. The + /// pollable given by `subscribe` will be ready when more bytes are + /// available. + /// + /// This function fails with a `stream-error` when the operation + /// encounters an error, giving `last-operation-failed`, or when the + /// stream is closed, giving `closed`. + /// + /// When the caller gives a `len` of 0, it represents a request to + /// read 0 bytes. If the stream is still open, this call should + /// succeed and return an empty list, or otherwise fail with `closed`. + /// + /// The `len` parameter is a `u64`, which could represent a list of u8 which + /// is not possible to allocate in wasm32, or not desirable to allocate as + /// as a return value by the callee. The callee may return a list of bytes + /// less than `len` in size while more bytes are available for reading. + read: func( + /// The maximum number of bytes to read + len: u64 + ) -> result, stream-error>; + + /// Read bytes from a stream, after blocking until at least one byte can + /// be read. Except for blocking, behavior is identical to `read`. + blocking-read: func( + /// The maximum number of bytes to read + len: u64 + ) -> result, stream-error>; + + /// Skip bytes from a stream. Returns number of bytes skipped. + /// + /// Behaves identical to `read`, except instead of returning a list + /// of bytes, returns the number of bytes consumed from the stream. + skip: func( + /// The maximum number of bytes to skip. + len: u64, + ) -> result; + + /// Skip bytes from a stream, after blocking until at least one byte + /// can be skipped. Except for blocking behavior, identical to `skip`. + blocking-skip: func( + /// The maximum number of bytes to skip. + len: u64, + ) -> result; + + /// Create a `pollable` which will resolve once either the specified stream + /// has bytes available to read or the other end of the stream has been + /// closed. + /// The created `pollable` is a child resource of the `input-stream`. + /// Implementations may trap if the `input-stream` is dropped before + /// all derived `pollable`s created with this function are dropped. + subscribe: func() -> pollable; + } + + + /// An output bytestream. + /// + /// `output-stream`s are *non-blocking* to the extent practical on + /// underlying platforms. Except where specified otherwise, I/O operations also + /// always return promptly, after the number of bytes that can be written + /// promptly, which could even be zero. To wait for the stream to be ready to + /// accept data, the `subscribe` function to obtain a `pollable` which can be + /// polled for using `wasi:io/poll`. + resource output-stream { + /// Check readiness for writing. This function never blocks. + /// + /// Returns the number of bytes permitted for the next call to `write`, + /// or an error. Calling `write` with more bytes than this function has + /// permitted will trap. + /// + /// When this function returns 0 bytes, the `subscribe` pollable will + /// become ready when this function will report at least 1 byte, or an + /// error. + check-write: func() -> result; + + /// Perform a write. This function never blocks. + /// + /// When the destination of a `write` is binary data, the bytes from + /// `contents` are written verbatim. When the destination of a `write` is + /// known to the implementation to be text, the bytes of `contents` are + /// transcoded from UTF-8 into the encoding of the destination and then + /// written. + /// + /// Precondition: check-write gave permit of Ok(n) and contents has a + /// length of less than or equal to n. Otherwise, this function will trap. + /// + /// returns Err(closed) without writing if the stream has closed since + /// the last call to check-write provided a permit. + write: func( + contents: list + ) -> result<_, stream-error>; + + /// Perform a write of up to 4096 bytes, and then flush the stream. Block + /// until all of these operations are complete, or an error occurs. + /// + /// This is a convenience wrapper around the use of `check-write`, + /// `subscribe`, `write`, and `flush`, and is implemented with the + /// following pseudo-code: + /// + /// ```text + /// let pollable = this.subscribe(); + /// while !contents.is_empty() { + /// // Wait for the stream to become writable + /// pollable.block(); + /// let Ok(n) = this.check-write(); // eliding error handling + /// let len = min(n, contents.len()); + /// let (chunk, rest) = contents.split_at(len); + /// this.write(chunk ); // eliding error handling + /// contents = rest; + /// } + /// this.flush(); + /// // Wait for completion of `flush` + /// pollable.block(); + /// // Check for any errors that arose during `flush` + /// let _ = this.check-write(); // eliding error handling + /// ``` + blocking-write-and-flush: func( + contents: list + ) -> result<_, stream-error>; + + /// Request to flush buffered output. This function never blocks. + /// + /// This tells the output-stream that the caller intends any buffered + /// output to be flushed. the output which is expected to be flushed + /// is all that has been passed to `write` prior to this call. + /// + /// Upon calling this function, the `output-stream` will not accept any + /// writes (`check-write` will return `ok(0)`) until the flush has + /// completed. The `subscribe` pollable will become ready when the + /// flush has completed and the stream can accept more writes. + flush: func() -> result<_, stream-error>; + + /// Request to flush buffered output, and block until flush completes + /// and stream is ready for writing again. + blocking-flush: func() -> result<_, stream-error>; + + /// Create a `pollable` which will resolve once the output-stream + /// is ready for more writing, or an error has occured. When this + /// pollable is ready, `check-write` will return `ok(n)` with n>0, or an + /// error. + /// + /// If the stream is closed, this pollable is always ready immediately. + /// + /// The created `pollable` is a child resource of the `output-stream`. + /// Implementations may trap if the `output-stream` is dropped before + /// all derived `pollable`s created with this function are dropped. + subscribe: func() -> pollable; + + /// Write zeroes to a stream. + /// + /// This should be used precisely like `write` with the exact same + /// preconditions (must use check-write first), but instead of + /// passing a list of bytes, you simply pass the number of zero-bytes + /// that should be written. + write-zeroes: func( + /// The number of zero-bytes to write + len: u64 + ) -> result<_, stream-error>; + + /// Perform a write of up to 4096 zeroes, and then flush the stream. + /// Block until all of these operations are complete, or an error + /// occurs. + /// + /// This is a convenience wrapper around the use of `check-write`, + /// `subscribe`, `write-zeroes`, and `flush`, and is implemented with + /// the following pseudo-code: + /// + /// ```text + /// let pollable = this.subscribe(); + /// while num_zeroes != 0 { + /// // Wait for the stream to become writable + /// pollable.block(); + /// let Ok(n) = this.check-write(); // eliding error handling + /// let len = min(n, num_zeroes); + /// this.write-zeroes(len); // eliding error handling + /// num_zeroes -= len; + /// } + /// this.flush(); + /// // Wait for completion of `flush` + /// pollable.block(); + /// // Check for any errors that arose during `flush` + /// let _ = this.check-write(); // eliding error handling + /// ``` + blocking-write-zeroes-and-flush: func( + /// The number of zero-bytes to write + len: u64 + ) -> result<_, stream-error>; + + /// Read from one stream and write to another. + /// + /// The behavior of splice is equivelant to: + /// 1. calling `check-write` on the `output-stream` + /// 2. calling `read` on the `input-stream` with the smaller of the + /// `check-write` permitted length and the `len` provided to `splice` + /// 3. calling `write` on the `output-stream` with that read data. + /// + /// Any error reported by the call to `check-write`, `read`, or + /// `write` ends the splice and reports that error. + /// + /// This function returns the number of bytes transferred; it may be less + /// than `len`. + splice: func( + /// The stream to read from + src: borrow, + /// The number of bytes to splice + len: u64, + ) -> result; + + /// Read from one stream and write to another, with blocking. + /// + /// This is similar to `splice`, except that it blocks until the + /// `output-stream` is ready for writing, and the `input-stream` + /// is ready for reading, before performing the `splice`. + blocking-splice: func( + /// The stream to read from + src: borrow, + /// The number of bytes to splice + len: u64, + ) -> result; + } +} diff --git a/crates/wash/src/wit/deps/io/world.wit b/crates/wash/src/wit/deps/io/world.wit new file mode 100644 index 0000000000..5f0b43fe50 --- /dev/null +++ b/crates/wash/src/wit/deps/io/world.wit @@ -0,0 +1,6 @@ +package wasi:io@0.2.0; + +world imports { + import streams; + import poll; +} diff --git a/crates/wash/src/wit/deps/random/insecure-seed.wit b/crates/wash/src/wit/deps/random/insecure-seed.wit new file mode 100644 index 0000000000..47210ac6bd --- /dev/null +++ b/crates/wash/src/wit/deps/random/insecure-seed.wit @@ -0,0 +1,25 @@ +package wasi:random@0.2.0; +/// The insecure-seed interface for seeding hash-map DoS resistance. +/// +/// It is intended to be portable at least between Unix-family platforms and +/// Windows. +interface insecure-seed { + /// Return a 128-bit value that may contain a pseudo-random value. + /// + /// The returned value is not required to be computed from a CSPRNG, and may + /// even be entirely deterministic. Host implementations are encouraged to + /// provide pseudo-random values to any program exposed to + /// attacker-controlled content, to enable DoS protection built into many + /// languages' hash-map implementations. + /// + /// This function is intended to only be called once, by a source language + /// to initialize Denial Of Service (DoS) protection in its hash-map + /// implementation. + /// + /// # Expected future evolution + /// + /// This will likely be changed to a value import, to prevent it from being + /// called multiple times and potentially used for purposes other than DoS + /// protection. + insecure-seed: func() -> tuple; +} diff --git a/crates/wash/src/wit/deps/random/insecure.wit b/crates/wash/src/wit/deps/random/insecure.wit new file mode 100644 index 0000000000..c58f4ee852 --- /dev/null +++ b/crates/wash/src/wit/deps/random/insecure.wit @@ -0,0 +1,22 @@ +package wasi:random@0.2.0; +/// The insecure interface for insecure pseudo-random numbers. +/// +/// It is intended to be portable at least between Unix-family platforms and +/// Windows. +interface insecure { + /// Return `len` insecure pseudo-random bytes. + /// + /// This function is not cryptographically secure. Do not use it for + /// anything related to security. + /// + /// There are no requirements on the values of the returned bytes, however + /// implementations are encouraged to return evenly distributed values with + /// a long period. + get-insecure-random-bytes: func(len: u64) -> list; + + /// Return an insecure pseudo-random `u64` value. + /// + /// This function returns the same type of pseudo-random data as + /// `get-insecure-random-bytes`, represented as a `u64`. + get-insecure-random-u64: func() -> u64; +} diff --git a/crates/wash/src/wit/deps/random/random.wit b/crates/wash/src/wit/deps/random/random.wit new file mode 100644 index 0000000000..0c017f0934 --- /dev/null +++ b/crates/wash/src/wit/deps/random/random.wit @@ -0,0 +1,26 @@ +package wasi:random@0.2.0; +/// WASI Random is a random data API. +/// +/// It is intended to be portable at least between Unix-family platforms and +/// Windows. +interface random { + /// Return `len` cryptographically-secure random or pseudo-random bytes. + /// + /// This function must produce data at least as cryptographically secure and + /// fast as an adequately seeded cryptographically-secure pseudo-random + /// number generator (CSPRNG). It must not block, from the perspective of + /// the calling program, under any circumstances, including on the first + /// request and on requests for numbers of bytes. The returned data must + /// always be unpredictable. + /// + /// This function must always return fresh data. Deterministic environments + /// must omit this function, rather than implementing it with deterministic + /// data. + get-random-bytes: func(len: u64) -> list; + + /// Return a cryptographically-secure random or pseudo-random `u64` value. + /// + /// This function returns the same type of data as `get-random-bytes`, + /// represented as a `u64`. + get-random-u64: func() -> u64; +} diff --git a/crates/wash/src/wit/deps/random/world.wit b/crates/wash/src/wit/deps/random/world.wit new file mode 100644 index 0000000000..3da34914a4 --- /dev/null +++ b/crates/wash/src/wit/deps/random/world.wit @@ -0,0 +1,7 @@ +package wasi:random@0.2.0; + +world imports { + import random; + import insecure; + import insecure-seed; +} diff --git a/crates/wash/src/wit/deps/sockets/instance-network.wit b/crates/wash/src/wit/deps/sockets/instance-network.wit new file mode 100644 index 0000000000..e455d0ff7b --- /dev/null +++ b/crates/wash/src/wit/deps/sockets/instance-network.wit @@ -0,0 +1,9 @@ + +/// This interface provides a value-export of the default network handle.. +interface instance-network { + use network.{network}; + + /// Get a handle to the default network. + instance-network: func() -> network; + +} diff --git a/crates/wash/src/wit/deps/sockets/ip-name-lookup.wit b/crates/wash/src/wit/deps/sockets/ip-name-lookup.wit new file mode 100644 index 0000000000..8e639ec596 --- /dev/null +++ b/crates/wash/src/wit/deps/sockets/ip-name-lookup.wit @@ -0,0 +1,51 @@ + +interface ip-name-lookup { + use wasi:io/poll@0.2.0.{pollable}; + use network.{network, error-code, ip-address}; + + + /// Resolve an internet host name to a list of IP addresses. + /// + /// Unicode domain names are automatically converted to ASCII using IDNA encoding. + /// If the input is an IP address string, the address is parsed and returned + /// as-is without making any external requests. + /// + /// See the wasi-socket proposal README.md for a comparison with getaddrinfo. + /// + /// This function never blocks. It either immediately fails or immediately + /// returns successfully with a `resolve-address-stream` that can be used + /// to (asynchronously) fetch the results. + /// + /// # Typical errors + /// - `invalid-argument`: `name` is a syntactically invalid domain name or IP address. + /// + /// # References: + /// - + /// - + /// - + /// - + resolve-addresses: func(network: borrow, name: string) -> result; + + resource resolve-address-stream { + /// Returns the next address from the resolver. + /// + /// This function should be called multiple times. On each call, it will + /// return the next address in connection order preference. If all + /// addresses have been exhausted, this function returns `none`. + /// + /// This function never returns IPv4-mapped IPv6 addresses. + /// + /// # Typical errors + /// - `name-unresolvable`: Name does not exist or has no suitable associated IP addresses. (EAI_NONAME, EAI_NODATA, EAI_ADDRFAMILY) + /// - `temporary-resolver-failure`: A temporary failure in name resolution occurred. (EAI_AGAIN) + /// - `permanent-resolver-failure`: A permanent failure in name resolution occurred. (EAI_FAIL) + /// - `would-block`: A result is not available yet. (EWOULDBLOCK, EAGAIN) + resolve-next-address: func() -> result, error-code>; + + /// Create a `pollable` which will resolve once the stream is ready for I/O. + /// + /// Note: this function is here for WASI Preview2 only. + /// It's planned to be removed when `future` is natively supported in Preview3. + subscribe: func() -> pollable; + } +} diff --git a/crates/wash/src/wit/deps/sockets/network.wit b/crates/wash/src/wit/deps/sockets/network.wit new file mode 100644 index 0000000000..9cadf0650a --- /dev/null +++ b/crates/wash/src/wit/deps/sockets/network.wit @@ -0,0 +1,145 @@ + +interface network { + /// An opaque resource that represents access to (a subset of) the network. + /// This enables context-based security for networking. + /// There is no need for this to map 1:1 to a physical network interface. + resource network; + + /// Error codes. + /// + /// In theory, every API can return any error code. + /// In practice, API's typically only return the errors documented per API + /// combined with a couple of errors that are always possible: + /// - `unknown` + /// - `access-denied` + /// - `not-supported` + /// - `out-of-memory` + /// - `concurrency-conflict` + /// + /// See each individual API for what the POSIX equivalents are. They sometimes differ per API. + enum error-code { + /// Unknown error + unknown, + + /// Access denied. + /// + /// POSIX equivalent: EACCES, EPERM + access-denied, + + /// The operation is not supported. + /// + /// POSIX equivalent: EOPNOTSUPP + not-supported, + + /// One of the arguments is invalid. + /// + /// POSIX equivalent: EINVAL + invalid-argument, + + /// Not enough memory to complete the operation. + /// + /// POSIX equivalent: ENOMEM, ENOBUFS, EAI_MEMORY + out-of-memory, + + /// The operation timed out before it could finish completely. + timeout, + + /// This operation is incompatible with another asynchronous operation that is already in progress. + /// + /// POSIX equivalent: EALREADY + concurrency-conflict, + + /// Trying to finish an asynchronous operation that: + /// - has not been started yet, or: + /// - was already finished by a previous `finish-*` call. + /// + /// Note: this is scheduled to be removed when `future`s are natively supported. + not-in-progress, + + /// The operation has been aborted because it could not be completed immediately. + /// + /// Note: this is scheduled to be removed when `future`s are natively supported. + would-block, + + + /// The operation is not valid in the socket's current state. + invalid-state, + + /// A new socket resource could not be created because of a system limit. + new-socket-limit, + + /// A bind operation failed because the provided address is not an address that the `network` can bind to. + address-not-bindable, + + /// A bind operation failed because the provided address is already in use or because there are no ephemeral ports available. + address-in-use, + + /// The remote address is not reachable + remote-unreachable, + + + /// The TCP connection was forcefully rejected + connection-refused, + + /// The TCP connection was reset. + connection-reset, + + /// A TCP connection was aborted. + connection-aborted, + + + /// The size of a datagram sent to a UDP socket exceeded the maximum + /// supported size. + datagram-too-large, + + + /// Name does not exist or has no suitable associated IP addresses. + name-unresolvable, + + /// A temporary failure in name resolution occurred. + temporary-resolver-failure, + + /// A permanent failure in name resolution occurred. + permanent-resolver-failure, + } + + enum ip-address-family { + /// Similar to `AF_INET` in POSIX. + ipv4, + + /// Similar to `AF_INET6` in POSIX. + ipv6, + } + + type ipv4-address = tuple; + type ipv6-address = tuple; + + variant ip-address { + ipv4(ipv4-address), + ipv6(ipv6-address), + } + + record ipv4-socket-address { + /// sin_port + port: u16, + /// sin_addr + address: ipv4-address, + } + + record ipv6-socket-address { + /// sin6_port + port: u16, + /// sin6_flowinfo + flow-info: u32, + /// sin6_addr + address: ipv6-address, + /// sin6_scope_id + scope-id: u32, + } + + variant ip-socket-address { + ipv4(ipv4-socket-address), + ipv6(ipv6-socket-address), + } + +} diff --git a/crates/wash/src/wit/deps/sockets/tcp-create-socket.wit b/crates/wash/src/wit/deps/sockets/tcp-create-socket.wit new file mode 100644 index 0000000000..c7ddf1f228 --- /dev/null +++ b/crates/wash/src/wit/deps/sockets/tcp-create-socket.wit @@ -0,0 +1,27 @@ + +interface tcp-create-socket { + use network.{network, error-code, ip-address-family}; + use tcp.{tcp-socket}; + + /// Create a new TCP socket. + /// + /// Similar to `socket(AF_INET or AF_INET6, SOCK_STREAM, IPPROTO_TCP)` in POSIX. + /// On IPv6 sockets, IPV6_V6ONLY is enabled by default and can't be configured otherwise. + /// + /// This function does not require a network capability handle. This is considered to be safe because + /// at time of creation, the socket is not bound to any `network` yet. Up to the moment `bind`/`connect` + /// is called, the socket is effectively an in-memory configuration object, unable to communicate with the outside world. + /// + /// All sockets are non-blocking. Use the wasi-poll interface to block on asynchronous operations. + /// + /// # Typical errors + /// - `not-supported`: The specified `address-family` is not supported. (EAFNOSUPPORT) + /// - `new-socket-limit`: The new socket resource could not be created because of a system limit. (EMFILE, ENFILE) + /// + /// # References + /// - + /// - + /// - + /// - + create-tcp-socket: func(address-family: ip-address-family) -> result; +} diff --git a/crates/wash/src/wit/deps/sockets/tcp.wit b/crates/wash/src/wit/deps/sockets/tcp.wit new file mode 100644 index 0000000000..5902b9ee05 --- /dev/null +++ b/crates/wash/src/wit/deps/sockets/tcp.wit @@ -0,0 +1,353 @@ + +interface tcp { + use wasi:io/streams@0.2.0.{input-stream, output-stream}; + use wasi:io/poll@0.2.0.{pollable}; + use wasi:clocks/monotonic-clock@0.2.0.{duration}; + use network.{network, error-code, ip-socket-address, ip-address-family}; + + enum shutdown-type { + /// Similar to `SHUT_RD` in POSIX. + receive, + + /// Similar to `SHUT_WR` in POSIX. + send, + + /// Similar to `SHUT_RDWR` in POSIX. + both, + } + + /// A TCP socket resource. + /// + /// The socket can be in one of the following states: + /// - `unbound` + /// - `bind-in-progress` + /// - `bound` (See note below) + /// - `listen-in-progress` + /// - `listening` + /// - `connect-in-progress` + /// - `connected` + /// - `closed` + /// See + /// for a more information. + /// + /// Note: Except where explicitly mentioned, whenever this documentation uses + /// the term "bound" without backticks it actually means: in the `bound` state *or higher*. + /// (i.e. `bound`, `listen-in-progress`, `listening`, `connect-in-progress` or `connected`) + /// + /// In addition to the general error codes documented on the + /// `network::error-code` type, TCP socket methods may always return + /// `error(invalid-state)` when in the `closed` state. + resource tcp-socket { + /// Bind the socket to a specific network on the provided IP address and port. + /// + /// If the IP address is zero (`0.0.0.0` in IPv4, `::` in IPv6), it is left to the implementation to decide which + /// network interface(s) to bind to. + /// If the TCP/UDP port is zero, the socket will be bound to a random free port. + /// + /// Bind can be attempted multiple times on the same socket, even with + /// different arguments on each iteration. But never concurrently and + /// only as long as the previous bind failed. Once a bind succeeds, the + /// binding can't be changed anymore. + /// + /// # Typical errors + /// - `invalid-argument`: The `local-address` has the wrong address family. (EAFNOSUPPORT, EFAULT on Windows) + /// - `invalid-argument`: `local-address` is not a unicast address. (EINVAL) + /// - `invalid-argument`: `local-address` is an IPv4-mapped IPv6 address. (EINVAL) + /// - `invalid-state`: The socket is already bound. (EINVAL) + /// - `address-in-use`: No ephemeral ports available. (EADDRINUSE, ENOBUFS on Windows) + /// - `address-in-use`: Address is already in use. (EADDRINUSE) + /// - `address-not-bindable`: `local-address` is not an address that the `network` can bind to. (EADDRNOTAVAIL) + /// - `not-in-progress`: A `bind` operation is not in progress. + /// - `would-block`: Can't finish the operation, it is still in progress. (EWOULDBLOCK, EAGAIN) + /// + /// # Implementors note + /// When binding to a non-zero port, this bind operation shouldn't be affected by the TIME_WAIT + /// state of a recently closed socket on the same local address. In practice this means that the SO_REUSEADDR + /// socket option should be set implicitly on all platforms, except on Windows where this is the default behavior + /// and SO_REUSEADDR performs something different entirely. + /// + /// Unlike in POSIX, in WASI the bind operation is async. This enables + /// interactive WASI hosts to inject permission prompts. Runtimes that + /// don't want to make use of this ability can simply call the native + /// `bind` as part of either `start-bind` or `finish-bind`. + /// + /// # References + /// - + /// - + /// - + /// - + start-bind: func(network: borrow, local-address: ip-socket-address) -> result<_, error-code>; + finish-bind: func() -> result<_, error-code>; + + /// Connect to a remote endpoint. + /// + /// On success: + /// - the socket is transitioned into the `connection` state. + /// - a pair of streams is returned that can be used to read & write to the connection + /// + /// After a failed connection attempt, the socket will be in the `closed` + /// state and the only valid action left is to `drop` the socket. A single + /// socket can not be used to connect more than once. + /// + /// # Typical errors + /// - `invalid-argument`: The `remote-address` has the wrong address family. (EAFNOSUPPORT) + /// - `invalid-argument`: `remote-address` is not a unicast address. (EINVAL, ENETUNREACH on Linux, EAFNOSUPPORT on MacOS) + /// - `invalid-argument`: `remote-address` is an IPv4-mapped IPv6 address. (EINVAL, EADDRNOTAVAIL on Illumos) + /// - `invalid-argument`: The IP address in `remote-address` is set to INADDR_ANY (`0.0.0.0` / `::`). (EADDRNOTAVAIL on Windows) + /// - `invalid-argument`: The port in `remote-address` is set to 0. (EADDRNOTAVAIL on Windows) + /// - `invalid-argument`: The socket is already attached to a different network. The `network` passed to `connect` must be identical to the one passed to `bind`. + /// - `invalid-state`: The socket is already in the `connected` state. (EISCONN) + /// - `invalid-state`: The socket is already in the `listening` state. (EOPNOTSUPP, EINVAL on Windows) + /// - `timeout`: Connection timed out. (ETIMEDOUT) + /// - `connection-refused`: The connection was forcefully rejected. (ECONNREFUSED) + /// - `connection-reset`: The connection was reset. (ECONNRESET) + /// - `connection-aborted`: The connection was aborted. (ECONNABORTED) + /// - `remote-unreachable`: The remote address is not reachable. (EHOSTUNREACH, EHOSTDOWN, ENETUNREACH, ENETDOWN, ENONET) + /// - `address-in-use`: Tried to perform an implicit bind, but there were no ephemeral ports available. (EADDRINUSE, EADDRNOTAVAIL on Linux, EAGAIN on BSD) + /// - `not-in-progress`: A connect operation is not in progress. + /// - `would-block`: Can't finish the operation, it is still in progress. (EWOULDBLOCK, EAGAIN) + /// + /// # Implementors note + /// The POSIX equivalent of `start-connect` is the regular `connect` syscall. + /// Because all WASI sockets are non-blocking this is expected to return + /// EINPROGRESS, which should be translated to `ok()` in WASI. + /// + /// The POSIX equivalent of `finish-connect` is a `poll` for event `POLLOUT` + /// with a timeout of 0 on the socket descriptor. Followed by a check for + /// the `SO_ERROR` socket option, in case the poll signaled readiness. + /// + /// # References + /// - + /// - + /// - + /// - + start-connect: func(network: borrow, remote-address: ip-socket-address) -> result<_, error-code>; + finish-connect: func() -> result, error-code>; + + /// Start listening for new connections. + /// + /// Transitions the socket into the `listening` state. + /// + /// Unlike POSIX, the socket must already be explicitly bound. + /// + /// # Typical errors + /// - `invalid-state`: The socket is not bound to any local address. (EDESTADDRREQ) + /// - `invalid-state`: The socket is already in the `connected` state. (EISCONN, EINVAL on BSD) + /// - `invalid-state`: The socket is already in the `listening` state. + /// - `address-in-use`: Tried to perform an implicit bind, but there were no ephemeral ports available. (EADDRINUSE) + /// - `not-in-progress`: A listen operation is not in progress. + /// - `would-block`: Can't finish the operation, it is still in progress. (EWOULDBLOCK, EAGAIN) + /// + /// # Implementors note + /// Unlike in POSIX, in WASI the listen operation is async. This enables + /// interactive WASI hosts to inject permission prompts. Runtimes that + /// don't want to make use of this ability can simply call the native + /// `listen` as part of either `start-listen` or `finish-listen`. + /// + /// # References + /// - + /// - + /// - + /// - + start-listen: func() -> result<_, error-code>; + finish-listen: func() -> result<_, error-code>; + + /// Accept a new client socket. + /// + /// The returned socket is bound and in the `connected` state. The following properties are inherited from the listener socket: + /// - `address-family` + /// - `keep-alive-enabled` + /// - `keep-alive-idle-time` + /// - `keep-alive-interval` + /// - `keep-alive-count` + /// - `hop-limit` + /// - `receive-buffer-size` + /// - `send-buffer-size` + /// + /// On success, this function returns the newly accepted client socket along with + /// a pair of streams that can be used to read & write to the connection. + /// + /// # Typical errors + /// - `invalid-state`: Socket is not in the `listening` state. (EINVAL) + /// - `would-block`: No pending connections at the moment. (EWOULDBLOCK, EAGAIN) + /// - `connection-aborted`: An incoming connection was pending, but was terminated by the client before this listener could accept it. (ECONNABORTED) + /// - `new-socket-limit`: The new socket resource could not be created because of a system limit. (EMFILE, ENFILE) + /// + /// # References + /// - + /// - + /// - + /// - + accept: func() -> result, error-code>; + + /// Get the bound local address. + /// + /// POSIX mentions: + /// > If the socket has not been bound to a local name, the value + /// > stored in the object pointed to by `address` is unspecified. + /// + /// WASI is stricter and requires `local-address` to return `invalid-state` when the socket hasn't been bound yet. + /// + /// # Typical errors + /// - `invalid-state`: The socket is not bound to any local address. + /// + /// # References + /// - + /// - + /// - + /// - + local-address: func() -> result; + + /// Get the remote address. + /// + /// # Typical errors + /// - `invalid-state`: The socket is not connected to a remote address. (ENOTCONN) + /// + /// # References + /// - + /// - + /// - + /// - + remote-address: func() -> result; + + /// Whether the socket is in the `listening` state. + /// + /// Equivalent to the SO_ACCEPTCONN socket option. + is-listening: func() -> bool; + + /// Whether this is a IPv4 or IPv6 socket. + /// + /// Equivalent to the SO_DOMAIN socket option. + address-family: func() -> ip-address-family; + + /// Hints the desired listen queue size. Implementations are free to ignore this. + /// + /// If the provided value is 0, an `invalid-argument` error is returned. + /// Any other value will never cause an error, but it might be silently clamped and/or rounded. + /// + /// # Typical errors + /// - `not-supported`: (set) The platform does not support changing the backlog size after the initial listen. + /// - `invalid-argument`: (set) The provided value was 0. + /// - `invalid-state`: (set) The socket is in the `connect-in-progress` or `connected` state. + set-listen-backlog-size: func(value: u64) -> result<_, error-code>; + + /// Enables or disables keepalive. + /// + /// The keepalive behavior can be adjusted using: + /// - `keep-alive-idle-time` + /// - `keep-alive-interval` + /// - `keep-alive-count` + /// These properties can be configured while `keep-alive-enabled` is false, but only come into effect when `keep-alive-enabled` is true. + /// + /// Equivalent to the SO_KEEPALIVE socket option. + keep-alive-enabled: func() -> result; + set-keep-alive-enabled: func(value: bool) -> result<_, error-code>; + + /// Amount of time the connection has to be idle before TCP starts sending keepalive packets. + /// + /// If the provided value is 0, an `invalid-argument` error is returned. + /// Any other value will never cause an error, but it might be silently clamped and/or rounded. + /// I.e. after setting a value, reading the same setting back may return a different value. + /// + /// Equivalent to the TCP_KEEPIDLE socket option. (TCP_KEEPALIVE on MacOS) + /// + /// # Typical errors + /// - `invalid-argument`: (set) The provided value was 0. + keep-alive-idle-time: func() -> result; + set-keep-alive-idle-time: func(value: duration) -> result<_, error-code>; + + /// The time between keepalive packets. + /// + /// If the provided value is 0, an `invalid-argument` error is returned. + /// Any other value will never cause an error, but it might be silently clamped and/or rounded. + /// I.e. after setting a value, reading the same setting back may return a different value. + /// + /// Equivalent to the TCP_KEEPINTVL socket option. + /// + /// # Typical errors + /// - `invalid-argument`: (set) The provided value was 0. + keep-alive-interval: func() -> result; + set-keep-alive-interval: func(value: duration) -> result<_, error-code>; + + /// The maximum amount of keepalive packets TCP should send before aborting the connection. + /// + /// If the provided value is 0, an `invalid-argument` error is returned. + /// Any other value will never cause an error, but it might be silently clamped and/or rounded. + /// I.e. after setting a value, reading the same setting back may return a different value. + /// + /// Equivalent to the TCP_KEEPCNT socket option. + /// + /// # Typical errors + /// - `invalid-argument`: (set) The provided value was 0. + keep-alive-count: func() -> result; + set-keep-alive-count: func(value: u32) -> result<_, error-code>; + + /// Equivalent to the IP_TTL & IPV6_UNICAST_HOPS socket options. + /// + /// If the provided value is 0, an `invalid-argument` error is returned. + /// + /// # Typical errors + /// - `invalid-argument`: (set) The TTL value must be 1 or higher. + hop-limit: func() -> result; + set-hop-limit: func(value: u8) -> result<_, error-code>; + + /// The kernel buffer space reserved for sends/receives on this socket. + /// + /// If the provided value is 0, an `invalid-argument` error is returned. + /// Any other value will never cause an error, but it might be silently clamped and/or rounded. + /// I.e. after setting a value, reading the same setting back may return a different value. + /// + /// Equivalent to the SO_RCVBUF and SO_SNDBUF socket options. + /// + /// # Typical errors + /// - `invalid-argument`: (set) The provided value was 0. + receive-buffer-size: func() -> result; + set-receive-buffer-size: func(value: u64) -> result<_, error-code>; + send-buffer-size: func() -> result; + set-send-buffer-size: func(value: u64) -> result<_, error-code>; + + /// Create a `pollable` which can be used to poll for, or block on, + /// completion of any of the asynchronous operations of this socket. + /// + /// When `finish-bind`, `finish-listen`, `finish-connect` or `accept` + /// return `error(would-block)`, this pollable can be used to wait for + /// their success or failure, after which the method can be retried. + /// + /// The pollable is not limited to the async operation that happens to be + /// in progress at the time of calling `subscribe` (if any). Theoretically, + /// `subscribe` only has to be called once per socket and can then be + /// (re)used for the remainder of the socket's lifetime. + /// + /// See + /// for a more information. + /// + /// Note: this function is here for WASI Preview2 only. + /// It's planned to be removed when `future` is natively supported in Preview3. + subscribe: func() -> pollable; + + /// Initiate a graceful shutdown. + /// + /// - `receive`: The socket is not expecting to receive any data from + /// the peer. The `input-stream` associated with this socket will be + /// closed. Any data still in the receive queue at time of calling + /// this method will be discarded. + /// - `send`: The socket has no more data to send to the peer. The `output-stream` + /// associated with this socket will be closed and a FIN packet will be sent. + /// - `both`: Same effect as `receive` & `send` combined. + /// + /// This function is idempotent. Shutting a down a direction more than once + /// has no effect and returns `ok`. + /// + /// The shutdown function does not close (drop) the socket. + /// + /// # Typical errors + /// - `invalid-state`: The socket is not in the `connected` state. (ENOTCONN) + /// + /// # References + /// - + /// - + /// - + /// - + shutdown: func(shutdown-type: shutdown-type) -> result<_, error-code>; + } +} diff --git a/crates/wash/src/wit/deps/sockets/udp-create-socket.wit b/crates/wash/src/wit/deps/sockets/udp-create-socket.wit new file mode 100644 index 0000000000..0482d1fe73 --- /dev/null +++ b/crates/wash/src/wit/deps/sockets/udp-create-socket.wit @@ -0,0 +1,27 @@ + +interface udp-create-socket { + use network.{network, error-code, ip-address-family}; + use udp.{udp-socket}; + + /// Create a new UDP socket. + /// + /// Similar to `socket(AF_INET or AF_INET6, SOCK_DGRAM, IPPROTO_UDP)` in POSIX. + /// On IPv6 sockets, IPV6_V6ONLY is enabled by default and can't be configured otherwise. + /// + /// This function does not require a network capability handle. This is considered to be safe because + /// at time of creation, the socket is not bound to any `network` yet. Up to the moment `bind` is called, + /// the socket is effectively an in-memory configuration object, unable to communicate with the outside world. + /// + /// All sockets are non-blocking. Use the wasi-poll interface to block on asynchronous operations. + /// + /// # Typical errors + /// - `not-supported`: The specified `address-family` is not supported. (EAFNOSUPPORT) + /// - `new-socket-limit`: The new socket resource could not be created because of a system limit. (EMFILE, ENFILE) + /// + /// # References: + /// - + /// - + /// - + /// - + create-udp-socket: func(address-family: ip-address-family) -> result; +} diff --git a/crates/wash/src/wit/deps/sockets/udp.wit b/crates/wash/src/wit/deps/sockets/udp.wit new file mode 100644 index 0000000000..d987a0a908 --- /dev/null +++ b/crates/wash/src/wit/deps/sockets/udp.wit @@ -0,0 +1,266 @@ + +interface udp { + use wasi:io/poll@0.2.0.{pollable}; + use network.{network, error-code, ip-socket-address, ip-address-family}; + + /// A received datagram. + record incoming-datagram { + /// The payload. + /// + /// Theoretical max size: ~64 KiB. In practice, typically less than 1500 bytes. + data: list, + + /// The source address. + /// + /// This field is guaranteed to match the remote address the stream was initialized with, if any. + /// + /// Equivalent to the `src_addr` out parameter of `recvfrom`. + remote-address: ip-socket-address, + } + + /// A datagram to be sent out. + record outgoing-datagram { + /// The payload. + data: list, + + /// The destination address. + /// + /// The requirements on this field depend on how the stream was initialized: + /// - with a remote address: this field must be None or match the stream's remote address exactly. + /// - without a remote address: this field is required. + /// + /// If this value is None, the send operation is equivalent to `send` in POSIX. Otherwise it is equivalent to `sendto`. + remote-address: option, + } + + + + /// A UDP socket handle. + resource udp-socket { + /// Bind the socket to a specific network on the provided IP address and port. + /// + /// If the IP address is zero (`0.0.0.0` in IPv4, `::` in IPv6), it is left to the implementation to decide which + /// network interface(s) to bind to. + /// If the port is zero, the socket will be bound to a random free port. + /// + /// # Typical errors + /// - `invalid-argument`: The `local-address` has the wrong address family. (EAFNOSUPPORT, EFAULT on Windows) + /// - `invalid-state`: The socket is already bound. (EINVAL) + /// - `address-in-use`: No ephemeral ports available. (EADDRINUSE, ENOBUFS on Windows) + /// - `address-in-use`: Address is already in use. (EADDRINUSE) + /// - `address-not-bindable`: `local-address` is not an address that the `network` can bind to. (EADDRNOTAVAIL) + /// - `not-in-progress`: A `bind` operation is not in progress. + /// - `would-block`: Can't finish the operation, it is still in progress. (EWOULDBLOCK, EAGAIN) + /// + /// # Implementors note + /// Unlike in POSIX, in WASI the bind operation is async. This enables + /// interactive WASI hosts to inject permission prompts. Runtimes that + /// don't want to make use of this ability can simply call the native + /// `bind` as part of either `start-bind` or `finish-bind`. + /// + /// # References + /// - + /// - + /// - + /// - + start-bind: func(network: borrow, local-address: ip-socket-address) -> result<_, error-code>; + finish-bind: func() -> result<_, error-code>; + + /// Set up inbound & outbound communication channels, optionally to a specific peer. + /// + /// This function only changes the local socket configuration and does not generate any network traffic. + /// On success, the `remote-address` of the socket is updated. The `local-address` may be updated as well, + /// based on the best network path to `remote-address`. + /// + /// When a `remote-address` is provided, the returned streams are limited to communicating with that specific peer: + /// - `send` can only be used to send to this destination. + /// - `receive` will only return datagrams sent from the provided `remote-address`. + /// + /// This method may be called multiple times on the same socket to change its association, but + /// only the most recently returned pair of streams will be operational. Implementations may trap if + /// the streams returned by a previous invocation haven't been dropped yet before calling `stream` again. + /// + /// The POSIX equivalent in pseudo-code is: + /// ```text + /// if (was previously connected) { + /// connect(s, AF_UNSPEC) + /// } + /// if (remote_address is Some) { + /// connect(s, remote_address) + /// } + /// ``` + /// + /// Unlike in POSIX, the socket must already be explicitly bound. + /// + /// # Typical errors + /// - `invalid-argument`: The `remote-address` has the wrong address family. (EAFNOSUPPORT) + /// - `invalid-argument`: The IP address in `remote-address` is set to INADDR_ANY (`0.0.0.0` / `::`). (EDESTADDRREQ, EADDRNOTAVAIL) + /// - `invalid-argument`: The port in `remote-address` is set to 0. (EDESTADDRREQ, EADDRNOTAVAIL) + /// - `invalid-state`: The socket is not bound. + /// - `address-in-use`: Tried to perform an implicit bind, but there were no ephemeral ports available. (EADDRINUSE, EADDRNOTAVAIL on Linux, EAGAIN on BSD) + /// - `remote-unreachable`: The remote address is not reachable. (ECONNRESET, ENETRESET, EHOSTUNREACH, EHOSTDOWN, ENETUNREACH, ENETDOWN, ENONET) + /// - `connection-refused`: The connection was refused. (ECONNREFUSED) + /// + /// # References + /// - + /// - + /// - + /// - + %stream: func(remote-address: option) -> result, error-code>; + + /// Get the current bound address. + /// + /// POSIX mentions: + /// > If the socket has not been bound to a local name, the value + /// > stored in the object pointed to by `address` is unspecified. + /// + /// WASI is stricter and requires `local-address` to return `invalid-state` when the socket hasn't been bound yet. + /// + /// # Typical errors + /// - `invalid-state`: The socket is not bound to any local address. + /// + /// # References + /// - + /// - + /// - + /// - + local-address: func() -> result; + + /// Get the address the socket is currently streaming to. + /// + /// # Typical errors + /// - `invalid-state`: The socket is not streaming to a specific remote address. (ENOTCONN) + /// + /// # References + /// - + /// - + /// - + /// - + remote-address: func() -> result; + + /// Whether this is a IPv4 or IPv6 socket. + /// + /// Equivalent to the SO_DOMAIN socket option. + address-family: func() -> ip-address-family; + + /// Equivalent to the IP_TTL & IPV6_UNICAST_HOPS socket options. + /// + /// If the provided value is 0, an `invalid-argument` error is returned. + /// + /// # Typical errors + /// - `invalid-argument`: (set) The TTL value must be 1 or higher. + unicast-hop-limit: func() -> result; + set-unicast-hop-limit: func(value: u8) -> result<_, error-code>; + + /// The kernel buffer space reserved for sends/receives on this socket. + /// + /// If the provided value is 0, an `invalid-argument` error is returned. + /// Any other value will never cause an error, but it might be silently clamped and/or rounded. + /// I.e. after setting a value, reading the same setting back may return a different value. + /// + /// Equivalent to the SO_RCVBUF and SO_SNDBUF socket options. + /// + /// # Typical errors + /// - `invalid-argument`: (set) The provided value was 0. + receive-buffer-size: func() -> result; + set-receive-buffer-size: func(value: u64) -> result<_, error-code>; + send-buffer-size: func() -> result; + set-send-buffer-size: func(value: u64) -> result<_, error-code>; + + /// Create a `pollable` which will resolve once the socket is ready for I/O. + /// + /// Note: this function is here for WASI Preview2 only. + /// It's planned to be removed when `future` is natively supported in Preview3. + subscribe: func() -> pollable; + } + + resource incoming-datagram-stream { + /// Receive messages on the socket. + /// + /// This function attempts to receive up to `max-results` datagrams on the socket without blocking. + /// The returned list may contain fewer elements than requested, but never more. + /// + /// This function returns successfully with an empty list when either: + /// - `max-results` is 0, or: + /// - `max-results` is greater than 0, but no results are immediately available. + /// This function never returns `error(would-block)`. + /// + /// # Typical errors + /// - `remote-unreachable`: The remote address is not reachable. (ECONNRESET, ENETRESET on Windows, EHOSTUNREACH, EHOSTDOWN, ENETUNREACH, ENETDOWN, ENONET) + /// - `connection-refused`: The connection was refused. (ECONNREFUSED) + /// + /// # References + /// - + /// - + /// - + /// - + /// - + /// - + /// - + /// - + receive: func(max-results: u64) -> result, error-code>; + + /// Create a `pollable` which will resolve once the stream is ready to receive again. + /// + /// Note: this function is here for WASI Preview2 only. + /// It's planned to be removed when `future` is natively supported in Preview3. + subscribe: func() -> pollable; + } + + resource outgoing-datagram-stream { + /// Check readiness for sending. This function never blocks. + /// + /// Returns the number of datagrams permitted for the next call to `send`, + /// or an error. Calling `send` with more datagrams than this function has + /// permitted will trap. + /// + /// When this function returns ok(0), the `subscribe` pollable will + /// become ready when this function will report at least ok(1), or an + /// error. + /// + /// Never returns `would-block`. + check-send: func() -> result; + + /// Send messages on the socket. + /// + /// This function attempts to send all provided `datagrams` on the socket without blocking and + /// returns how many messages were actually sent (or queued for sending). This function never + /// returns `error(would-block)`. If none of the datagrams were able to be sent, `ok(0)` is returned. + /// + /// This function semantically behaves the same as iterating the `datagrams` list and sequentially + /// sending each individual datagram until either the end of the list has been reached or the first error occurred. + /// If at least one datagram has been sent successfully, this function never returns an error. + /// + /// If the input list is empty, the function returns `ok(0)`. + /// + /// Each call to `send` must be permitted by a preceding `check-send`. Implementations must trap if + /// either `check-send` was not called or `datagrams` contains more items than `check-send` permitted. + /// + /// # Typical errors + /// - `invalid-argument`: The `remote-address` has the wrong address family. (EAFNOSUPPORT) + /// - `invalid-argument`: The IP address in `remote-address` is set to INADDR_ANY (`0.0.0.0` / `::`). (EDESTADDRREQ, EADDRNOTAVAIL) + /// - `invalid-argument`: The port in `remote-address` is set to 0. (EDESTADDRREQ, EADDRNOTAVAIL) + /// - `invalid-argument`: The socket is in "connected" mode and `remote-address` is `some` value that does not match the address passed to `stream`. (EISCONN) + /// - `invalid-argument`: The socket is not "connected" and no value for `remote-address` was provided. (EDESTADDRREQ) + /// - `remote-unreachable`: The remote address is not reachable. (ECONNRESET, ENETRESET on Windows, EHOSTUNREACH, EHOSTDOWN, ENETUNREACH, ENETDOWN, ENONET) + /// - `connection-refused`: The connection was refused. (ECONNREFUSED) + /// - `datagram-too-large`: The datagram is too large. (EMSGSIZE) + /// + /// # References + /// - + /// - + /// - + /// - + /// - + /// - + /// - + /// - + send: func(datagrams: list) -> result; + + /// Create a `pollable` which will resolve once the stream is ready to send again. + /// + /// Note: this function is here for WASI Preview2 only. + /// It's planned to be removed when `future` is natively supported in Preview3. + subscribe: func() -> pollable; + } +} diff --git a/crates/wash/src/wit/deps/sockets/world.wit b/crates/wash/src/wit/deps/sockets/world.wit new file mode 100644 index 0000000000..f8bb92ae04 --- /dev/null +++ b/crates/wash/src/wit/deps/sockets/world.wit @@ -0,0 +1,11 @@ +package wasi:sockets@0.2.0; + +world imports { + import instance-network; + import network; + import udp; + import udp-create-socket; + import tcp; + import tcp-create-socket; + import ip-name-lookup; +} diff --git a/crates/wash/src/wit/subcommand.wit b/crates/wash/src/wit/subcommand.wit new file mode 100644 index 0000000000..dd8ec28018 --- /dev/null +++ b/crates/wash/src/wit/subcommand.wit @@ -0,0 +1,39 @@ +/// The interface for a subcommand plugin. This is used to register plugins and to provide the host +/// with an interface it can use to interact with the plugin. +interface subcommand { + /// Information about an argument + record argument { + /// The description of the argument. Used for documentation in the CLI + description: string, + /// Whether or not the argument is a path. If the argument is a path, wash will load this + // path (with access to only the file if it is a file path and access to the directory if + /// it is a directory path) and pass it as a preopened dir at the exact same path + is-path: bool, + /// Whether or not the argument is required. + required: bool, + } + + /// The metadata for a plugin used for registration and setup + record metadata { + /// The friendly name of the plugin + name: string, + /// The ID of the plugin. This must be unique across all plugins and is used as the name of + /// the subcommand added to wash. This ID should contain no whitespace + id: string, + /// The version of the plugin + version: string, + /// The author of the plugin + author: string, + /// The description of the plugin. This will be used as the top level help text for the plugin + description: string, + /// The list of flags and their documentation that can be used with this plugin. The key is + /// the name of the flag. + %flags: list>, + /// The list of positional arguments that can be used with this plugin. The key is the name + /// of the argument. + arguments: list>, + } + + /// The function to register a plugin. This is called by the host to register the plugin. + register: func() -> metadata; +} \ No newline at end of file diff --git a/crates/wash/src/wit/world.wit b/crates/wash/src/wit/world.wit new file mode 100644 index 0000000000..44850fd848 --- /dev/null +++ b/crates/wash/src/wit/world.wit @@ -0,0 +1,17 @@ +package wasmcloud:wash@0.1.0; + +/// The world that subcommand plugins can consume and provide. Any plugin is invoked using the +/// `wasi:cli/run` function and is passed all relevant flags, arguments, and environment variables. +world subcommands { + include wasi:cli/imports@0.2.0; + import wasi:http/outgoing-handler@0.2.0; + // TODO: Once we get the ctl interface updated, we should include that here as well + + export subcommand; + export wasi:cli/run@0.2.0; +} + +// TODO: Other types of plugins we'll want to support: +// - Auth providers +// - Registry providers +// - Resource providers (for things like custom DBs or other things a platform may provide) diff --git a/crates/wash/tests/app.rs b/crates/wash/tests/app.rs new file mode 100644 index 0000000000..f5d6acc6c0 --- /dev/null +++ b/crates/wash/tests/app.rs @@ -0,0 +1,56 @@ +use std::str::FromStr; + +use anyhow::Result; +use tempfile::tempdir; +use wash_lib::app::{load_app_manifest, AppManifest, AppManifestSource}; + +#[tokio::test] +#[cfg_attr( + not(can_reach_raw_githubusercontent_com), + ignore = "raw.githubusercontent.com is not reachable" +)] +async fn test_load_app_manifest() -> Result<()> { + // test stdin + let stdin = AppManifestSource::AsyncReadSource(Box::new(std::io::Cursor::new( + "iam batman!".as_bytes(), + ))); + + let manifest = load_app_manifest(stdin).await?; + assert!( + matches!(manifest, AppManifest::SerializedModel(manifest) if manifest == "iam batman!"), + "expected AppManifest::SerializedModel('iam batman!')" + ); + + // create temporary file for this test + let tmp_dir = tempdir()?; + tokio::fs::write(tmp_dir.path().join("foo.yaml"), "foo").await?; + + // test file + let file = AppManifestSource::from_str(tmp_dir.path().join("foo.yaml").to_str().unwrap())?; + let manifest = load_app_manifest(file).await?; + assert!( + matches!(manifest, AppManifest::SerializedModel(manifest) if manifest == "foo"), + "expected AppManifest::SerializedModel('foo')" + ); + + // test url + let url = AppManifestSource::from_str( + "https://raw.githubusercontent.com/wasmCloud/wasmCloud/main/examples/rust/components/http-hello-world/wadm.yaml", + )?; + + let manifest = load_app_manifest(url).await?; + assert!( + matches!(manifest, AppManifest::SerializedModel(_)), + "expected AppManifest::SerializedModel(_)" + ); + + // test model + let model = AppManifestSource::from_str("foo")?; + let manifest = load_app_manifest(model).await?; + assert!( + matches!(manifest, AppManifest::ModelName(name) if name == "foo"), + "expected AppManifest::ModelName('foo')" + ); + + Ok(()) +} diff --git a/crates/wash/tests/common.rs b/crates/wash/tests/common.rs new file mode 100644 index 0000000000..5d356cfb5f --- /dev/null +++ b/crates/wash/tests/common.rs @@ -0,0 +1,16 @@ +use std::net::{Ipv4Addr, SocketAddrV4}; + +use anyhow::{Context as _, Result}; +use tokio::net::TcpListener; + +pub const NATS_SERVER_VERSION: &str = "v2.10.20"; + +/// Returns an open port on the interface, searching within the range endpoints, inclusive +pub async fn find_open_port() -> Result { + TcpListener::bind(SocketAddrV4::new(Ipv4Addr::LOCALHOST, 0)) + .await + .context("failed to bind random port")? + .local_addr() + .map(|addr| addr.port()) + .context("failed to get local address from opened TCP socket") +} diff --git a/crates/wash/tests/common/mod.rs b/crates/wash/tests/common/mod.rs index dd5e2ba8ac..452d5e77c6 100644 --- a/crates/wash/tests/common/mod.rs +++ b/crates/wash/tests/common/mod.rs @@ -19,17 +19,17 @@ use tokio::{ time::Duration, }; -use wash_cli::config::{WADM_VERSION, WASMCLOUD_HOST_VERSION}; -use wash_lib::cli::output::{ +use crate::lib::cli::output::{ AppDeleteCommandOutput, AppDeployCommandOutput, AppGetCommandOutput, AppListCommandOutput, AppUndeployCommandOutput, CallCommandOutput, GetHostsCommandOutput, PullCommandOutput, StartCommandOutput, StopCommandOutput, UpCommandOutput, }; -use wash_lib::common::CommandGroupUsage; -use wash_lib::config::{host_pid_file, wadm_pid_file}; -use wash_lib::start::{ +use crate::lib::common::CommandGroupUsage; +use crate::lib::config::{host_pid_file, wadm_pid_file}; +use crate::lib::start::{ ensure_nats_server, start_nats_server, NatsConfig, WADM_BINARY, WASMCLOUD_HOST_BIN, }; +use wash_cli::config::{WADM_VERSION, WASMCLOUD_HOST_VERSION}; use wasmcloud_control_interface::Host; #[allow(unused)] diff --git a/crates/wash/tests/fixtures/hello_plugin_s.wasm b/crates/wash/tests/fixtures/hello_plugin_s.wasm new file mode 100644 index 0000000000000000000000000000000000000000..7ff2f153d7fe8f38aabd526390a7dd138c323707 GIT binary patch literal 660580 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 q7YYc5~s%!kAGZ=o+cqYXxa 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

HjJH2>4aRm<*tIB~o6irc(YSdyX;skR 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 @)O+r$`oH!(*3&8!Dm41%{gdMgK85HGMb z9RwuUni96AgZ>Rgi?-~uK|3naaBPLE872XEQ0i%`L%rOcA#sV$ZXO;k@XIvO&Ck)@ z%?Xm4=jQBboTNe>h2QS%g)p!(aGP(J*c;xxvd%a^z(CZ{?!(~cl34b zYip4>6oMZK<&fy+8Q|jMryOjR=<4R|?Bb~$Y@O)q=i@fg#n08p3k0u1#UuiMk~&BK z=q&<%{>)H}PUsT0HFlPEs^zJ8%Y9i2w{`8Ybc{5RT3PozR!@vtEQ zx`#nMA8e*Xfu7SIz917FpKL_>fTDoQF-RF@YFb=;V()$r6syIjXXDu`)@_E>(Zb#m zz>GoZ8tiVt`aq~e6ZR~y7R_I5(vY@Cm}B?S^qmAss*_VE^tS5t30yxR)Z(VY#1;n~ z9TgrGg;e~a^$_e1krbLycdG^^14YyKh}7f>Fb75obA`zlY8@8tW`G)YLKmPwxJp9G zeQ{_#YR+Lr9Nt59YZ1l;f!B4YDzPeFq#!mrEgR}E5iJkWU~&kTTma=96`zsV%MB?Y zMjl|v2wR3&?Wj;^ns&5dtf3$+I}xg30;~&?Q*_`a4eWD~nL?p}V)>+~1laq>y)}da z7eJ6cKG4Go)ka#Qsz$qGprtJ%onQsD=7YUGGF_PMklJ8avO^W1Us(M*ilbL*u^MJ5 zFG8CePC)na6zU>5BQ+9nSXy>;7HELOnpJT@U^`Nmb8lV5NE;l)nF?HjbjNmYk|68u zEgBRLa_wGkAvJ^0lwe&9tEHZLL(6QTvgcl^lzVOA2=&yV@PunyQeQLLWWx%hiTye} z;DPpBXr{5rsd$4GTY04+-G69(FNCX)gr6>GbaaB|3i@@?Pl0wa?0?~3P-ERH$>C>w zGBhG#qf9~gdb})57T7{PL_vz+KYZ8g8{)XLZ!^w}gR6eRh8&Jo@vs9)1O;09V1EGV z6#X-e^mr>6tvX?cH#!xmjfRUY_^=mv8?Q-I!a>X71mG9Y*JsNj69kGW(dbgsi?Vv3dZ!lp~IKmZg6EMasnz`)Fu$Gpl%`^I!}}b#L?Yb*Pj0F0(5H< z+7d(;(W5;bnmVW;L`ssO4}(t=psmB&J(?z)lIMc+B&?jMxLyQTH~?zRLUcsJ4q#us$Ia2W78F}n2p zLV9j}(Fh@o8o9XQ;X$~E>4ruJ;csUsVx(d>$}1xsE1X8Vn7+g)H4GRUBJ>pw0zEN| z0!^dgF2dd)S^I&$WwaFu7mje>58{ZjUAnt|*5SLQz034||eO%^7H)(LV{Dq2c*}e|NifNsfV>c*S_Yc_ox# zlDDuw_d@3{{=Iv0vVEcMDJ49?2+p$NQ%sEv4e>9dWTJaY#Y`0nir%3ofW17}$u_{; zNHAYQ{A8jnCqny4B95qF{($;o=y7^=jlavvshOxaVD!R$EZY7@gEBM)7KIJ-StQ62 z2^&PekwJnD5iJEq8VnNDNbU=w-^j>-L2k$|+84A2g9I!hp@zsWoIF#|r6Uv`vO{jj zKME5GW)uU11Vkb`nNAw%{BljUu9LPWNhti7TMh2x3(PVtc9eso1McrUrt9yhN~mc(doz1Ve05mC}25IM@LapRGO`$qp1TlRRZ2R93_g5 zyox&2h~k?OiF!DS8|bk97yU10u#Q-+$5a6(H7K(G*E zo`f1H9-`1x5O3k*FH^!{z6LWqbbbhFm;{?F6lg|hgd&D5l#QNQ9i>+*gqbOx+@j@3 zk1uo-05jdxc$gGqBz4cj;X`7AiYAs6+z%KK(cuXoeBXQ8EDV~`-TU>-uPHs9F|;!1 zy`c)AoB;UJQ*_-k-CsT9RR{(3_UK%q*UE!}y6&EZ1cm6X@j`*(X5=NTPqGSx$%aZ0 zt~k1@Kv;f6{r1$5+b?SdiXUvf^vsDVsJ9eO;e-*9MMchbDP(U5Mb&HCOYzN4j7WxU zF=7gh)&~@_|0L=hdQA%Sp2AZ{I4L65Bxp`W)E!EB4C0q4Q27jqKIr`+t>gek849b? zKF;WiNYMEmh33nc-;gJ=ATaNxlgCv(apTE6`V@6|y<_NRE^=ugTzVx(g8FC*jAby! zqlSQR4luLA&Br?x*fcpKO#lNcoUBK|VzXD>!`w778ZO>bhQaI|OSC}Wch88y0g};u z)ieg>93wT*p%%prDGW!qyU}t4!Cs;$Xmo-#RvMg{LHVUcLn^=)2AA|?*jo|S7%Br{ zm=HW_-D#+2Pp3G;EHgfd7$}5=1R)dP5Q+z|99k0g6j*n#nJ`;}^CBdJ0}T$s(7K&b zmU^n`7whhk5htXFB3V2p98U0w5v$t~Hz8q;y4%99aR_G=sNtZcG=-#lx1!%`nMuTC zs&_*auH;cY5E~5ujzGOoPlkMP;i0%9$)!F;XophOW7*T>$(1~?c8YM|@5L0oZSe$& zU~6LOOu>_y?lmpN1Hnn+U{T(4uIUt>5*`7{(9`3i(QZQ;?4ZO)#sjaw_(D7xG*3zc z^)cfBLy=mhcL|o zZQuwU+%RF}g{1?isz+9UX^Kw8O`TXx0U#?XIsrzx!S({&DU4wRdjMGU9R1ct4nsk~HFq%;#*K)ppdPavIrkB+ZICOdKgvI3@V=+B_Lqx7-Le<@a z9Z{J6(iAE+I%eDYTOA3Q9{B#`gT>z-e|*=Gj_Lh*U0XL-@7crZoPz0jBc>!ULnCGl z=$wveU*qHEVO@vohjh-zwEw_^3+K=9TsEe238s%s)k)FJ{N9}2xeC+8`=9M!d!%_| zac3o_m*l3;z7o3S+m_BMOkXM6T)a5N`M{~pT1-#i$<%#o6Pw%Ic^K1sO-!Qw2h?2p z-gyesRnu~=o_RcEg=u=3##;PV*19SwD=X8np@qv?qOQ`Jf-l%o>?7{ zT~9Inn$KAjCHCy_)UG#}zBt=ES^VPR`763UVOl?0>G5cVeb;t(wPPC9ImmQhb-4>6 zFNlY|;pU4MU1;@zo`|Z`ud}M@Le&J*#g}}`vKf-EMj=G`@8*ft=Ugs5W=+w=zEj4& z%DHShwb+-UkLkF)m)k0>uir|fm}A;SW2M4CEw8n+DR!9F`g!W6$-2n5>nP5c-dufd zzIF72nj;it|hv}876Ni3R4LS9JG8WULTa-8I$9pW2qDEo*)q)g$ zR4cVfi<*Gx5t;8+u2`SAZYY%)$!1K*tQ|ddosQ`fr^|vD z%?zHoh&mtB_94s8o%XdjS4CZd>D}d}KgQPTE zQ_6l`rMD#jzGIAg|33>8CNz<9yiiCZysF}(+2me=nhZ& z)oq~bW4gG@d7ROz*rH=}b4)w#pTBCgg4xX`x*evsC_kxPbXRH3XSy?{?_@R~OsZS< zT880{=|3MQVpHSOGX89s&=U}SEDW#OCIUMjBl|0j+VW!(F}8Pbdmsv{eJ>>`&;7s z5dIL&>_SsP#$8q@I=*)L&8857(U?>=olLM!Ggps zO551P)Xdz%(h5#YBO*ae*wAE{WoCM05a360Dh^wvU>hA}E9l0F1d~UXghaQ#@`(gt z=m=FPpNN>>2`dGp^&yFHIDKd)2(vy|JP^n^!Xwz&+xN2<-rVV7k1)KsAxMpnjZ23; zdf3a0j)EI0$ktZCBJ2UY{EIts5c&Wo^yB~%P>{pYXXOfVwYA|L(#^UtiRnf#w}f_P zob`W3z9eMiUwM8h9P$VYBzGYYXtx`I=z(3P$T)oY0w(bWuy!M^6oBY0XwdO#uqzcq zF65(#KgpDET#lan!%=WXk4bOiX4?kQ=lnHsW4T=)Tx@( z@r((y9rT^F!?3WwM!P9_OT2;BNNb`#WIm#`FkaAF1#OIPv>%M0R7tHtgFU^9*Q{Ba zGjH*-b-NDE+$ka|ZfQR_;N#V63^_$htAN01Tet5xV)2kWYfjM`hLkjit7Bqr>oUx3 zgr`?j^xXLiiuWErdFsrChOvA1DXWNzu_WXaENyHzZ@GO(+^S^ZW>MCl!7=g0i#W-l zb*=BlM0{%R^7UI;W@x0P?Z3LbVtwVN&D94FpAwOfPBk*jD^R7I$fz(h#nqU4OlO9)Zr)ZAO@<~zTg+U-lWs1VZzV3z5>p)H zVndG<6E~J;_MHu6t|??GWntmNl|Y%D>ErGQA077$UwTP z7%lIr51VeKFQzNcAE+XyAnplKIZG*tvP4|Pw8S$ch7Q&h8N_6Xj1r+T*>t8vkQhxh zLd{i-mA7%6hKmGCL`v3Hge4-UqR&vsJ75>(E9olE8s?(xD&{NYCd$g&t0B!28OBnj z4|lVoOGB(SqO5#NCDB22Re!3CsZ?QEOol|>sk!cvQqzsu@~q-AX2I~42W)4aDXE7xhB6+v;n1lOK zQw->Q1})z~onyr;v%7uX2|E!g!=I^aPRp0pXGBQ` zu=2KBt4irJ#6@Y+B6%wdZ!cz^cl3l`F@hBbQaTER7FHw zH1EfB5ek*gWQvH;L`B3z#W^e$2_;Fs6kA$ShQX$Dxw7J*xdS%Bl<0g>WvU8IU0y)f zqZ>#VQjHlVG*jv(#unOE#x}7Zw4cllS|`0rygDm;-hy?;furZmFHyNKEi=;nXS<=% zkg*}5kEbsvTDWM-&Vz?epQ%6p;OVn23IjI)E89W#ZX-gb7lC8#!9!=xU%vA68Kt{< z3_|T=TvYV*g=MSjFJF=3=-3W&aSIF{J1#UTdcndi@X6`==BLlvq&P0H?VC4!*Wsge zw;J2t7tWYhx$$V->9bdw?z%2Na_P+FD{h`%fkER!=M)w1+`F&t_?fef9C?M{v0uJ+ zcI72be9$bdo|LSj8k#$G`wovohvgO2)Q1iC^g@kf>a;zlZ{E7s_WpBfT5)>DvVn$1 zn|AE0JA0+Ec_qc6bh&Y{`sFLvZ(i>5^coW^Dkj6$GHQL7lx$@`WT;EYLf_bobM;rR z-M-V(*+mhAY7{gx3Y^8186uqgD(Sp!Om(q*6}plbm0`p%XNc0Nq9URkmbVO7)L)d& zP+^JF#pt4R8uZSR3?^MdgeuKrdWtHG28z-|6ePVFPILq4(KsS9lC})h0il9K#`pnw z=a>aGbiPPI2R%qsUR+TevE6uRB7Bh`Q9b4`mOcZRgKi?B&)|zl(DSOm$;c${lb9V{ zhCY~SEvCmT=;A1f8F38g{bc&d=u3oh4!|HcU)XBwa#{K86t_o>!=-!jczvXXMQl*;Xm3z%Z#` zl2Q)i&vH8zaR>HpVKCMtoFzPlr$A#kb~brh8*Dk&C0rHM<3y3v#&JSx>nnhG&8C8(O>)EEX;1QJD4 zqA}=fh7|l1p~@g}+6)!CDou^%K%(~Ti#kVKe9I6=wNXp)dGBdQr>pGIRs zzIAD0)K*k%R9INC*=&&hO?^yV%B0XCmke##tJnZ>I5H_TFUBwl6GjfzQbq@I%A%V< zxS~{hx+aqMKq0RN$gs1&Gwb0(9n0J#;Bp-9r{6zE7O)FF&fz)g^MLz)7G zNf!|lqlv09me46y3{x?xG*zBSm4T2sI8klpdYyQbdekYhearzzK|AZC7iS)foMsX}@Izf?G;*Q`QN#MZ% zc5*b~odPrhYRrT;O4%%UYxJVe329L(8*l#jf*bPB0UCWzen;kN9M~hk32Bc&9SGdP z0E2)XWH{mQTMZ2Ppzjf|gmfPVzmYph2f=ToxwIX^9{_JMd;~aA1cTy0KoZiwKm11N zBxye(9YOmw-C&LLjp9e36Vm@|_}x3anL_#ze2>yW#{Uy&Tpo1PVj&E83wpVuaM45h zHv@MBz#(bZK6D7s2q+T+Zxlc2FAHcCPZ7Kk$cMD2FbLWV-ss>f0N(2G&VaWZyyw6h zrK_CDri8+KB~%2eUy?2X8Ziv{p!A~e$Oq9Vy8J055O7?l4cpeMk)3IZ^JH%*L9$v|#E zBlk?85kJj@H;QAHkX{CF1RSb>H{uP_9r;1eVWIzi@LLiLN;k?ED!abv)&#pQ@ZUS# zi1+@H0WUaQ6gFA5WSPO9DNZl)BPzkhWhn!1l)iry?nH4mB@;{mm^old!IXhn4`wr% zYA}buTm^F%%yTer!Mp?W6HFHvDHiYxn1Nu7z*vD93}zIVNH8g2vcb#-vlz?@Fq^>a z0CO13F);ODZh?6MMjO&imZJ{*?oI0gjqJj95_PQ)zYV|`f-wSv9#lri7=!8kEvj=< zFsM#Yxl}?Kjfb}z7}U9u^a%Kk>XM`pPol8?QCX7iX5g3XKg{7bddM_c5M(!v!ks9| zrli7qBN!$aG938X?avAk@Q~qH6J$4y!a)z|ZbOjWv~3@H5YQ+dJ1}Hf+xPN=+V@}~ zjea9u8X}}gzHwNcD2QiCcNw4&&mL1}Q^MiB9==Co z5=ox}8jTqweF@Oyvn05qy+(tR^5Ajoz?)}tS|`-2Gp zbK0FKm&@>mpea}S(6@m`VM|Jckq+K9!0##^l*i=9K2cJ zt_E)uhI$`b2WV6eq`M8!Xly5GGF~#=5yJHJO)qKRH@&34YzP_;m2k<2t zdq`RX_*oC=?cg7x!1E0V=ks(XJ; zx@4Zf#s;3>GnfSvht2`~43a2;Kti&13C5U9$K64bn4sMi7)wGT=O}_eB1(vya}ZhN zoTJD&=kWiYs_r{|(}cS-JOB6Qv$cJ@@9nDU>I$dM`JLa9_j2#DdmJ~vVm7CZA|_<_v-q|Z{*(llj`<3JY(9SKCh2wab4QIKEIT5oBg4_ z{%n3X(fHkStNbpij`jJ))%o+P+(kz(!cp{j%SnV_B@Br=aTMEl6&Ouo@bbO z_`V$F+JADC{Oj)*tLqP}j`j84V|{;peTm;of9bvcpemmSSI6@>${e)7jkn-fpRaJf zr%{g|^E-LJb$<2l&Z_RK%l#?8llNQyuD;$|9vB9_?$-G{vgiDMoHy(9XU=a!zP})U znQx^pTaELR_$>2I{rz3}eB;J>%4f;1{(gba&HOIpT;`?v`)BZ3EWc_>j~Gcm^JT($I*ZF@l#wExuib-4(HOx>hm9PE}u8L|EK&; z+Q0X{`u_U*FRJ|N^Ivi<{qBz(rQY@R`goPOV|9DpoTK!ETXGb+<^qn=mY0&Gztrb1 z?>T=P=hFUv!N>O60d}Vdt8#oRsx4!@5e1F>k_t)S5 zmG7lJ#}u zvi{wp_+IAg`n*0~z;$WYVc)-u@BfkSKg?0))B3*pSl|C;zMtXySCK-JTCCcW&+=FQ zzHaZ{{#AcpAM4-M=k@Ut@|F3fzW++j<-7X*wH|n^o`34!*Tvg}r8Q;q| zvN?)8TYq04>-yBcyFI_V8o#>}M`_p7tMmGI^?miRzWzjhTaUXZ@%hQ5gjD~@@1H_? z(Q@~CDc{TZlk`+M;jcb_nmIk@MjWM2J)Lv`N#@RHlAc9+Hc95z`cKMv4(Yk1i%1uf zo=18<=>??vPk#48(u+thCS5|3Ip!s#%SiQ~{O+Zsmyu-7c?Ic}B>A26iTY1|_iEB> zNUtTmj`Vtx{O%1T`Ck6+y=vY^v%O^AYM$ScbGi1O%a1+gjU3-ZlC?xWznPSghqTdt zj)=SZPufSWy@m8vlFWy1C(ZI3S$Ce!QToXZJBT({JnR@@`~4Il%P;92ZFe>Fb=& zaoj_a`R$d5y5-0#xK{V$5ApfKq=Z!e$uoR}bn4;meq84(?|zz8pG%)k^G|0bX1ebnby@>%LR`Cq0U^>vI(T2A1rPQIU! z6aMOQKFjA@a-Wp*CDKhu@>h=$DZBpuGvs#*&d-0yv04YBct6)_oZqA8{NH=d|C4ih zj{5!soJXAhi2S5JKdX+v;@IN+57qf!s`JVJHovii+q`FYj(BRI-)Keal4747$W(pyOH zB)yk(1?gj?Pm{ht`YP#4()UO|CjEl+ThgCMWAc2YYmkm5U5|7_(m#^+kZwV`Ey*X{ zo^%}P&ZJXFdr9(Fzt3;zv$gYYa{d+SDSf%NdC5=e-%H<-zxw=JeC9Rlzq_Ap##w#* zHdpI$^&LKcm-IbS^ZNHW{{iWTJ=cH4`Hx9I>AC(>&VNSwIq4UqI{#nt`B$W0lXB`I ztPDl6^(1#WCbIAvfJ?8OTm$~9)bmTo8-^%$tIo8*ss~FwTjm|fH{*&BzW4^D? zf6w`?8|Q!I{AP{wt5V;8Zk+#Z&-vw?-?s7nTYAo4$+^sRx260WaXgiy)Ki{Wo@<$N z`TT|I+RHeXx$B>3wA*m}5a+k7zT=>u`=Tq3J?4H7Gw*tt+3eZb2-mBN*g?pqpVwXUHRB4zeT=3 zd8BF6cp_9ylAJ8(_fMD&$=_)g~3`*M``<}e6~^lj2lNWUXp^`XpFq=7iu6^|uSjF`3GuJN%tomAe~7%m-Ix^3rMdZ zy_xiW(kDq@CVhh>B)~U+49`qk*K?KFWUo%T2I*^0GUN4U9Hrl1pCoI`6e)SMKt@QV^IlnGPpL2)fORMvzR{31UQSSW`#~W}wnIz+&K9{~O z=TCUbv3h);$!DoYeSWa#{7}#NgL=*{=e(Jpj2r25b$&7yq;J;eXYsjN-r=5eX{+Y< z(gw}*YxSHjaW3;po&Q13@6|XzkMm~vCFjlhiEGh6HGVJaL$kbVSLbhhCbnUICo-F? zHTC(We3swW=P&6wzpUr{O$+`5Yx_&?9{H~4j4_D`ho@MYaZ>j26pTD)|T=rbjp7rsbH;&S7_q*6U@4dKL?rWakym5Zlmg(2^_lJ7U4_D_8B0rgj9?4PGy2o;q@-O5l zZS@?E*Wh?5M|tj#ag;Uma~#Q{?{{C}y41J6|CgLgJBr+QYf|fZMrR{!CgZq1zdD~q zzWDHq&3l(VD9=-$--pkV?~nLijxV8dQontdnEd~N%E1m<3Se~ASC!^vY*|X|cdy-{+=)SoP)9>`XD5oY3*>mT8d%S#UVNw5Q zhX=C2@$=LP?I2A2GIZ70eWfZ+GapatU|VZ-@O2ezYpn_)go5+!WW~0Kw$R4K2&O4= zcZ(zqVk;@^B8jc2P~-nOICp1eAvA1y%eJj4xAtmoJ=;x%{{*L&He_RYZ}zdBI!>55 zo|oi7oT!O=4$a=z+*H0Mcd-4H1t9I|>V9$TBlF&Tt*neP+k)}5j8flKlMf=_C>r{D z;3%Bd`A<*l0*>U3gt?dHS&&;+jJrr!sHu|&Ka_W1Wo6O4ir~bVVm4oPp&spdlq~aO zKe8;l2%N-q1JxSRM2*~R|J2o${gC4CKONo`=vW5iYx7ziXkfFx8-QxPcAJ zt!=BTB_rhTO8&_zysc1`9quV2@Ut`t^0crZNVbAfU48!uMZ{8skW98E&Pkj1wbWZs z;^dz12SHiVUS652Yh)W1YD#WA+fdYm{}y=!u0fM{4XwJN75fW8qv-o$FbtHNG(fU>pmMkqaE}LT;s8d(` z_=Ws1m1b_7>&Z8d9Xrm0tn@tJ@faY-K2#Q&I9T_ERCDn3bYl)alGhSMR#sSai8A#{ zyRg)C9;Ry%53NP_JfZvGJ{VeO;Zr`6ig`eXbkkPZg6yR%`MKXA4$(jc#lqM zTTIEeTOt-+zu0VBG-W#5*DY}7Od?WJ{|w_4PUv`k6hwjN+HRt5@CY-chR^w39k@rx zIFcv!lh}8h#PYl}w8AV`H#}kR6C-DOvgqjldb*HjWv-n^UhL+v9|xBDpIe%%Dp|?U zG#t@fWygi(=UJNO%nMoRsDC(5Pnw&Ys_KbB)`lZ#)Y!88!p-u;b6wxD67|3CJ-AWb zY<33Lp*6x}+b7T#1E#~E+}qOhoWQk;h<8OMrHT1Y-(LkFwoZZ)Y^A_L|v#SKP&XdZ7EJ-Yd;Kq=*Iho)o3Ec*VVS#WoCYO=ruwHL zTdzjG!-PI*^q%aU+z#C`vivOZX_i#oM0P1=r|1$6qFqO9$Q?(vkL~68WD#;YWL4p! zWSX;-^Q2+;=LU-vIcTDhsa`JYQLuVuOEX1Ch- zCdf6iHW6M|{`op>smEMf=vdmBuzucLb{v7P0Xq*w56hccPTMv?Jfn*gWMdDo&jqbk8S~ zxbs8aTpD`tAcgF8@;FG6Oj+%+xvS)3 z2K%0qPusKSbW)>6>kMo+WA|u#dFWY;2{%>lh0^$*+~O{-EZL4D{TyZ~G=s3ZN)wvw zAXtW2Ri09gqv-}u*i*eaWH|Qmye)mKUk_~-hAcaQYiFSw#ZhJ{Pd{s2Jz=71GkTV(DK>;r)dOh3s{4`5ccv@G6L0^X5k9wyCRASZE&*bQ{&GyjM zB}7FE!Z=dVzjfsp_T|`&#yGH&aU8^@la*;&goRIFu~-&@PIqC7?dvVG$zr<-LpVL% z`GDpIYEwqa3Fbrfj;zq~q9}6R(6McgzJ0r-D}T2y|L#VR<#>9!OO)42&lBMPsfsAl z(2nvv2#Ul*Ovu#j=bA-?{V%S&RjQzVFte_poMRzC;%j$K;PyVnd{Oth!VM$O_rfCe zQy1~!UqmwV9lbPnjrw{l2tyUmKp+!o9SO_2BFi@KG(a`XGA@%9iqpu-lYlHeE3no7 z`FC;+x)!sZK{aGTFd;)E_iB2{%OphK?%H+|lnE97*9Vhz?CJ`S*c5$b4_&2gpCMKF zSyZN3V5h#i!=q$EA}HGcU5Fmtr)i5tbH3~D5(J7t;m>;#5s7kF>2!QNFg?8%PJ4R# z&Z0|}R*05hu!0DGiB=ZXP#Uq9bVEXl#g@9Gbd_+Bno3utS&8nJVvU$NQ^XR}+fPhH zN;@p&p?cFJf#MQ3OT47?T*tM7!uHg0x0OflvUkMVm?0Z=sb6g8L7MWG(mZn`OC2wB zQ1=ZU%b4)Bw=*5ksJ(1rQek4ys#2!iN|;?HLn5dVg{#QPGqljssS|GAX|>x)quq9` z&F)Bq!)fa#b#vXxVI`Sq6YhRR3!}QFH@HXr9eK{&jx8EN-R8;AkqlSoPjvJvUz;$Q z7}3B8*&(FKGa``|p2IAW`F0#Kw`H!nv+gHVH{EmkiF*iAq~FI{E@nl=FvQCsTscfF zvTU6$Z=w0-^bG@k%g>lN*6tR~!)hj#$s8-lf;35Nc8+dQs=H{j2par)#^(VXdtk}7 zt0xrRM>a#aXL&Rf7P%9+WrBvpiyU>*DgDhcEH|$VJR|>T0!^|j;}{EC9H4(LJljsy zUAK~*sho_Vd6Z0KhfOZ?&}d+?u_H0A-0dEluASp_J!`w>FDKgs-9OZmuEWxYra4z9 zZ@mdM$)_H;mLn}!fcLxT1V3M`gbd}{MfgbqMcJaO5G$ypBFKIp8<4X&%Z*JGWe73)_WF z_em95xF`a!&S0}H$^>nP#`_RP8#Qlk2bzKy)$u}F7$8u1z%HExuz_VqR)WPZQ<=L_ zHs~j8hh24mG{8!W-A?5vag*oijgLsQUAx8R@ax;d*`db}^b0>O*-e*)DyAhXxu^No zQIM}L%bJW{luZOi)~rO~?-#1H2K_#iCA>W&6L(aiK?wskX=zH&x7B{1J30%Yult1U z>cEZ2QN&=<^L?B+C0fxiq*hKEmFj>D!ocXj)}*r@-9uyhsDD$b#&ox%#|368D!mh2eZRV#n{nEg&W9u(xTt^RMqxq*}pd!ruBu!Hf*wC46M zHVdM+-N_|oQLI%*Qo#BP#{=J|H6C5te$>W#8t8e2z;;AEG(9aUZkd5)n&|Fa9gQ1m zs_ALeQ}S~4i>_bzDC!dO0WLJJH71^H%w_PQjzW0%ncAZ?Mq9G9L_0-BFWp^;Z2JcS zNAxk!S2S>C;3(nnx%fBMd}L%&A(HzS@0bSShSxGBElov0P$8T~oa{Hg-SJ zYO&rJWfi8sbcT13MV@OVz$-#KX2TP!Gw(lA0Z!X!a+V5!cJrNLv^4T8;O2`@>u737B1p|DfhF?ULrwL~2f zS-{tGh$pe8?clhjZj)uKRlo$7KFIwJi@jJ3J$WIZBC82M^dbN~E>eaSqpXO$C_a27 z)kKlbf)^qsr|2Z}M4c7uW^E3&T4ZqrdP!}EPo*YTVxoPbzltt zwf^t1B_>6gCkU}onR$+S$Y{o?I4L3zC?|pkmJBCV|9xlb=^1tRx-KLoI7ex5zQS@9kI>$b{VLL zj+ztTvO-J%ffZ$81RngnQF8*7m3j`U85IBWckUDAa^1x5lQr^`ixr#FPALWIsWBz;<0L+6sEGqkyx9Ra`YyUa}1S$8XM2(zm7 zt>8n&C%K~Rf6mT@us2?BJhde_s!z(Ksj6Ml29Vk83}Lb@QZN?-B-m04(cohTOshX73Ai$@I3YOv8~ziMaaYYT}ydvE-Epvnvyb* zeVOOufrBp24>EOuo-Kkd#g_@nAcoO{(glx~d-ZhNHA^zD1GVat!!7puCB9pU&HN=R z)-$%d1fRLF3>>U+31$f=SI?|Ee&29!BE;8$ZK62|qR2-xgmE3soLi`8jooGNklz5S z$?TEXVG$rYr&;W#QLdh?CnGbPg;gwI+cp?X5iCVyF5M){)6#RWy~LnbY8{CkTfXmt zDZ(cMV@2r3&V^$FRqn|yW+e2&2qVe6UG-Z(#9UO^IS7%GWmr9Dlw6zw)u11GURGi@ zVLU!}lw3gFvqcFVzeLsOWP!S9q+D{8NrJa>N;Yojx-K3i7Yt|sPcqRBGwHE-K5vv< z3K!uz$O@kYI7Hp0ok!iq7zi4D>v|ty3 zDFSCB^txH0UOZ|}cv!`G#y+q}oS1b>T{1Sy1BaXR7}`+L(6a?9LmMv~#5!;Qk)5XM z(lOB?cEnf=y}eRwb6V0cmtHi~o#vNyHBL(o&n_5zqRa_N;s}~vJYmJ_PrYR9K(i#k zAiX6S@pWNQ>;==)#=m0LD@ zKUTMdO?%1~@}*>KD`o7a%Nfwt87}nHtuR`_@|udwPl+ z`2nudE_ebT)myAyzTG86Q5*o5V-8HQB?82J#depFVEWHJz^Gw~36FK@mD^nc-mFfF z_zEnUCqw6kgd+H!QqwZrEI@Nn){FyFgy7)OInSzJ6?8N*(Sz*VWD&PW64VtZQW% zS{&OdSWnqysW%MU2{xQ!?19cUK*+E`W3T~EgEpRBzIx-B#?o}z6!f;Tk=eQ)HUL<# zWueytF$;zT_bc_Lv1_*5OP~tddd`3I*tM%u)Gwf}v$u3E0DzYZZyo!a<_&ELl~&|$ z8@plG)j~8ZwO`WP$L>a}cS{s~0&T=2I$nT#cfw-nRRS&W77L}A6}i}9;{a2(^NvxZ z9Jcyt0(HC;YjhF&t20=RWYkEDilQ#v8Dx`j*T@79KGSugHp{D6qWujfnxgzs+rH zE;*xU>t0Qh#~>mwQ5Sh0ryl!O_5Lw&syV_6+fHq`u0VDG=rGQBjum3oN6=Cq7!yC+ z(CFa@H2Ym~OSlJ}s`54&GD-Hm-ZZzsOJ#Uz;N2S8iTdE^S{w@!5DN|ijE7+s#Gblh z^t{-3mJZ?_@0`E_J2Abn|;6kXZjcDNTLQ-$4b!3x&^=$+X4BMsE_KkYP*U8p)4@YMuA1>I2le- z6c=ash^dc_2`Y5E3XBZ^*WjumoVab2FBp|=AIquwc;Dovz3=S`TMVo{(Ku6nA1CbG z0fT{iWB2rlu`{-@lD1WWa4Yy%ZsWJe0))S53gSb3Qp;uA)g&M!iL_1+J5U^IcOEgt z8xt)XJ~YId2+t~UIvaA*hm<0uC6bY+GYPs-+0E_+fm{Puq{r=Ug# zBh2$?*CA`tj&4B9Q1yJdxv^>FeJ_Z9?qpmUPJ^Y%xL|BXm+N~8qM8efESlUFRYD=hS4Z83}ct8N%5;%nC zIO4?r^|2TI|D>Kh*j<}-CB`7`Sc-zvM`5eD}SmSID7a`;3j>c-;TY!RNoxKG+E2hpk$+Rbv_eSm9Ga{G!PJ{(X{ms!BowkO70z%rS5w* zW=i^F?O>!)ud(*Muc{N^o$D15sA?cdC8(AwO5@?;Wa3jPQw*(Z8Q(dmUJ+=d|yYJ`$b|kR4p6<{G<^Jvt+k(HRS0c07 z1X;5L%u3#V2XiMA5Tgs-wf2RkWpR)9T%QSxWzqMOP<_8S8 ztA3=nYn4{TRGC(up02jN*aZ=@>rHQeO}c;E`5ljGTY87A?XDCI5ud1D*@@;k`0hyk zxH7sm5nRHGvIvsgMX9vdUMX0Tky)bSR#z78!j%eH<%s6pA4{tZ;(~p z)1@HkMy{7-IZ8daf(i^3>Zf`v^_Y?e*3NdMYWu3XI+Zg-U3cs$rUcwWEgy3`ZW)*i zQ4#!XOuMgj!S)7My1Rs`5!m%Im$13SfRp0IrG7r9{mr(VpKY5Sl%zZ!O#!r|0} zW=V0iOVuwbe8t8u%Jz*lqP;B0$TJFU)<+YDM+Wo(xbz?js9%n0cg3yOvufE2j)$u* zfK$MD5M!WrNYt# z6l_g*n-@7iD|{c=HREMvA5|T6vS`y#g?jmK`{_`Pi=dQI+=}!ry#WpW*_Vk=Z=e_ z*}N$PV*KX%?8{xeAzCA(GG6(eJhmRkW zM?C^((5{W|FptauB|l=kp(*K^f2!GMWU>8`0AK3SdIITL?*`@Q z*!rqoIJ@)AZO?;a2)sWq0lWg$W5xx5xrsc5$5V4?z@5R`Xo&?KOC4&8$Bv8XZxdPA zzNr?{pYb>0Av1_{fmy*RhT6|gbM?5*=LQ^UVfM_T4ew%ZRvL>{5#mPhl5hsWbD|!< z`F9FyGty{9zJi2DywY zCotma=?X%85)fjo4{0vw&D%Dh1*kL@pqM=z2r%K|!YzP#=yZek3er`ca-0F5@Rbwz27VjQF_l*1S7Kp`=lSy1AnCE~q{yNY_o4iv%`B@hx$oPC(SF{VFr2MUQX zc}wIC_HtlDA)a{F_)gG1f`Eos6cm{nqHf)@O)I~c|3|2g#6}X*c zx#JQeMP0lj#mF|xjJXO~62pX=Et+0z*af~FRF5RVy;PHn~j062H z1~t*4zht*6k~swHC zU8;x&H;xMt3k=|a16W?Ye3vQ$Nr6!M&>AT&R4#b^U8)EIYakSzcq_%sY=wH|E>$G- zFao0Cbf8r$faIxH?NUWx&17E%ED(iB%rI83-mQv&4NVb@G**%jJL)yNR1uJ{c%h;9 zfzR7PPFJtpt%?wvLw2Mt<{%HJ2lcvLsz?T9ML?hef`#C6m>25xyHydk8Zi0gxrK{! zMAU>g>{3Mv=xYHgz-JypP=XiV8+WZDXzn~f=Qc)ajKJzmyHyd4@4z@?cF)oj1MCUa=5l*Y(W+#=fpv7sol={2j%<6AGqnF9KR|GylYz`GcPxr1it0~ z3wi{b82*BJO)@*+t$tu!*s?bFcySvid~HLM$I75#hDj-r#?E~57`^~l(=I&aCK&h2xa@1l0~s3OI!(uwMdvh zay0MiW8;DX-dv^ZcKv5CkboYtv z$Q1hPz`_lKXuO4&`RbEf>NN6F+Qf*%mMas>oV+0_iJm;g^9tu^AH&Za>OM@2#la&5uhsCzfW)Jog4g_fVsrqP-;j=q^8jC;0oaLM`Xk^zewo1dFQ4}spg!+No&(o4QSQT_f-&z4{|f5NUu1%|biSy)o(mzl0?%MQg0SKHiUmUOzh<61AB*npyBHL=++hDL5M# z1ig|N-t7Hshd8_6*m^Z}w!)+?QDB6(HYN%I8-2jC>YL*xu4}W;ZBWg)a?b$eaZsuW zyhXP_5rlYVVn>J~INp49p8;1CzDH0{^{sVQsKJ@Rg=@U2 z+XrWI4fTp`==CB3lVw7;Z3pVxdRFURpeyukw1R8TK~{D>Ui$Nj95kH7ZH-(Q8xxEs z-&tq18&W~L>OV4~csG9T(|ID8*05djj?{O@&ylqxHta{uH`FyN3>ut$D@h*>X6^7N zpdM1qF%6q1L77}Jlu;C@oQ+|7^^LmRd%tme?= z{!n9ed%B^)2d?oNYDXJpVL`ZAr|1ZH`Ei1F6QXB9m@T)3b8@ASusl%Tf>z3KYjf#L zS*CuZ_n;#dVI_+K)ssTVo1nrBa>o-zA#vRk_2Y39Xt~)r`e+%4Ro|VNFTrlD#XTsn z&w2!Ia#+|UOp*G@fPqzIkEoMCpUp(cW_1640A@HK5*a9;$=in)R@-w$BCQenSpn9H zK;@nYyqWsx5oZZyt_$n|@)z*v$ouMNN1P=gt7C#yg(XxWcyEX5=L1%d^&dQJKDcxT z0Yl=ED$ZGMf@znCSg!iT_%24>v2ZAGLEHvHy$P|R`la4SkHB9IO&^{?jyS2KF7{mY zt8V!Qw0f_6qr;8j`9^5M)A;}R)v|)mm7uP4=C<8M++`KpzPls9k}>_l7F{@Dzo2@Z<<;jbh8)2)=A)E+ZQ}@))rxuKxC&6 z;^m|MG%l>l>sKTanH8+5$QCRGVZn5nS~jp3g6aNwfaufvBzB#H1~D(MmV_`OFZ4|z zu;?@)W*tH6OZ68$@G2&b<|@z-Q&_f4aE!srbN?#oK>&mgPFf=Ova!w7U&n>}vip(y zmKo7-nx$sBi~ZNGqZ%0s)fAVe(Dz~U1{jIosycf@^Q#zdsqRM@&cZw7QF^EYajFX( z3(s;$OJvbh=S)1ps0a-6!X`^Qsf)M9XvVW{=VSB;<~o$Y)6@G1CB#jYeCKGdWZc={ z^2KuF*cowPEp_gMrfeCs?;QHvgKDB>tlk}H0WQ=gJ`-kVJZ~Xbe(1zzlp|felvI;qtA?Sp%cIEM^A{-S?@B} zeY7~8$x%0Jjt!iH=ZdBM@`O2EC}(abQoRR)AXJZ;5KpTO^K)$zRZY8cL;FGG$XPbZ zD+k&{EtcH{dE=A=!#ENtX6F!=XZ6_a%R6L6%5Buec%c&zBUX>wzP#C+!*d2!4ftMC zKm`5x?aLcZ2l^`}C_Gi*D0bBoCXR+y?THh+vI~FGgxE}LW!#Z(hVLc*+XJk)eY@}{ zPxK2~m1f3YT~78F{ypUXiY$Q$Kx`Y)n?T1W&Z6om6NZhmxwa0to%s_DG%@9LmZtl! z%&h3C!%aLkj48Cu?4u#yR!^Piw`UH@v$9W{ED_`C42c8_4jab1JcNJ%aC$Y{L4PPg zcwpHl<_*L=IQl(pVt@;xVW((5J&Sm=`i-)H@w!N7kbr=4hAmz_eL`#jJ;GCej<#RX z10Y7}2VfQma(0fy8E~QYCB|u?F4%12innED%F3%It|FNMyMiO6aM7IMuFfv-858{m z#?1uMSc?|nU@QWkL9_*wd?IW-^N6zq;{pLk0<90h*1&(LXHE38E^MZjc7QTbG$?Q+ z9Vig$MI=W-gT)Y226410!dK7U&fKZ4FFrYh{KDi4f5wH|nY%CKoQx01|3cnis-81y zW1EN^vXH?}iSr*wxFC)2#8uB7(9OCVo0mL~TrVPGC$dTm2VW-y3Tku2U9Y4mP}dTt z=%R_;v-vxv&F`hpU?)I zzZZ5q_OBim1lZAY-qrIre=m6a2tg8_H)4CiW29cNg?qCsgoiN1<=Df|k>CZJzc;6@ z;wEARnCl>zR4>~6y#-9ag;i!O&jn7IfqL=g?*;P!%#Ni5>H+9WBX!AypgfumoxmhC zIhBM!pG6$Q-im{(4&W^iiki5}!n}u0nQ+k9Wpmu(E}hVUZp{vJK%iN@XCuIitd5N^ zuy}?*W{bxgJ1HE+Z1oadqY?CF7);X~2Te>fUIDL`x=gPLBZa?(wi_N8- zOGnQO-j=cu@_xuRfzsnzFnV4wa|khOkoht;SgEC6KCzKqnAOK43{81MIJOLzMx~GtM{Wdbqr(@6Zhe9dzuu!W&cZa5JD($@scO_B!)(G*_Qjt386jcRP#s?kFPo^a_ph%*l8~yDK zx|4P{K`*_$gXJSHiCN9m3#@CsKL5s19vX^zqOXXikJT?H(!iTW$;F0|3~v&&udy3M zAOqh#N-mCggksr-&jXJ;D^zb8DHrB(g5$w*fjd;}M(VAjWg#~xuBQg0h6 z7uJ9bLRm0!cXx(#Hy>p~o1WpO2 zl9dhN79rqWqvVq3gvKRo7y*%xC;%^AK1wb{&O0K4B+kSo@sK_BZmr4)487_$6Y0E1 z=iR+YtQe|29dAPGc~f=mM9(>AbEn@m+P!n@13|4bKz$4jRtbnO8n>^ks=2y;f{6U`9_q4bA(Dvk;0`UV@PUMb&U>c>bK0MJ+ z!8f?&*}PXl)TQl9H0#nK0C_;RLf{r8EL1Lu`pAH->yQlNHOPB2OYBql*X3yRq0uL_ zpZchl^*7VUVw5(3eGT1UK?WEaAwhz=6^n`b*hD`be@NB9Q2@^X_rU+Fq8NsERszYO zxW!Rb5H%kkpi~-?SyX4X_PpfCD0D5+s1tUO@gA#BOb8*{W**aVK{vy%zkgorkv-H_ z39PIzZfIK4f~WrEL_gE%;EFo_95nV&NUf=v>Q_#E(Fw+$MVKhi2B?Kf^{FkiK%H?{ z3jjwc1XcsS)^`G6HJ=`!RvTQ^wb&OD!h+gKm{;8Y1#jRuDFfy+T2$IhYuf(G!UAT_ zIxwhMh*>BSmpB|Tn?-^#fc~B6XZIRXHBYiDi-V@`NN&Dw$&*6A2epS#ntyJh->qP9 zZbH&Y6oK}<4*AhuDA#f&E|?1u5wY$f@bPfme_q=@HucEC>iik=#I_ex^y-`e31ZOp zNI+-ykn_O32!gP581IZ#VR6foWifl;@mHetg9*mNqb2gy7bgVLI;fiA zDU!o}qL^s^1URp_A17!#<(;x!gCq^_i7#m_diVX6<+>TKmYpV>L9;D{8XH!tS?!cD zyO3g=(c@a)4WS1LL^lLlo24!7nB@j+5x+cO4&JC>+tVt_Hh~YAF=u4TBz`hp4R9y} zM1_Kr$N>}mVUIV=H!#4~E}}LU&1B#b>M@Q8aG9s-s{=%k4RZ#jAduNAXCkGp>NOk7 zCrWjA;>j9)rTUuQ;coFYRCzCC_$4h7ZKIGLE$d+ALj@}z0Cm<9p2I~*P+uP~QZ{@7 z%{nLG*F)`<1zH^21B{`57=u0uEKrHx7|;bPwyfzHDeU4005OAdAKl@kwQD0H^($o;(-t8NvDj>7+c)h%}Y#dpy7OQH$=q{?!D0Z)=|+>d|O+yE04$lnmTQx=a~e2p@Ry^ zLnTOg`hh(CwU+ke8ZXm@1kx@eBu6JFY*M1WqxAwi#54b0Z7DqJJoDe%9Rua}cgsNe zgNY{@5YHgUP}S?rqA)#u`b>7<#A<=)im) zixOy9OegAxLj}*k+*&i1o7Ds_gJqfE3G)Mx7gJXzOBP6vAL(t#KoIo0Bf1X})q_yt zAq)_yAQma%k#LoR^OK!}`td|RpQ#Zr>YDeWdcA~@XfP*WW#HL~75tNl+Yec$t1Kb` zfmJgk6Ke<*1C6f`VxS58>4b#-4J)^;E>m~U>r^@b)-W%ez6frZPcf6hAevK5?5FBy z6a8eQU5^SuXyZKt_0Tc~?}<3O1)-@i0|?)qm8hRj><)csPE z2WB!BjH+iVi3m!EPAmN2T(WeAP|)thWJIVL0&n|M^_DTZdKYGd`=HMo^0PLa4uB{M6_k$g^wFRRP4@5YCRI6`2L+yXBQ%xr(y;D*vi$5)oPTKZ7P=Dzyln$4#e+fjeo@D^9Q^OU%#6*4EAo zwfWD=)H#zo5N_$*$(^CnK#s7uO2ZuFQqis$!5cCSjt9^ z1+90M7CFIZ2qy`0Q$2F>iCR+aipbM}`ZF`fJyE#K($FKh$yAS2qNSd0v9;lj$C5EK z50rFz`Xo8X9B;%uH-$w4%MPxwDYGaf`;+~6uewgQqoq?rn6YHrdb6lI)KUW&0r3yG zXMqj};6&0$J$h2hLZR{8JFQDT>k{WW6>b)TbHe=LMoCilDopB9p+kBi9^;1#oE_Ia zfzBJtGFB^>y(`LTCRGa#fG~Q@U*snRUb6};*ZZU%TYeOQ z{^!k{dLJfCh}n%%N?gvEuM73qoozg@nHYJA5}wmQ>{f<1-s5(*A|POJF9lWvRU&qP zb>8CRceo;=ohC|aj%L6C=0Cheo-p}5Gkyo{G>7!%;};fYoA19VsBLB}Y}kR|-U1Hh zf>$L5BcK%Z#GP%fED?hZc6anZmcYe_j=m>NUTm6cU9aix(N{SGZg-2Buo)~hSRgQw zf=Q2o{#t3Ko;;}?VYYFuvMr4^!VprnDUl%GE?bbmVk6^%(H!1hPuc0J5Ry=WC!oe; zH-u?xcvYUd(^UabMVt}f8^p5*NRL1#npT5&oDPovx z*+jz2WaIC;&I`I=QWFwvW4G-L5jO>7?cy5)1vQ4Je0Y02W9KU2W8h?Ij4_%M505Bv zLtn}>cd!zLFP*}PAboP!{130hvv#f$;Korim6=4nAx_ph^X;>Dt`b<`a_F}a9zbwd zVGRr$AaY$ed9FDt>C6~8c%B;MGN2aE*~wZIoEJs<=d9=NWHpFo6JWJus&Inq*XPosQlB^| z2`K>3fRwn-TZ9ql5D4d;{V_CqcD5oRarXcQfZxOu zlbGPcTjZrXTanlWPhkmjjR#oGUZ)~2+u4d_J{nSwK+U`!7p}G8E%Nf6t%w*@e2kjV zy&((0V>@&(zhY7X#YThFVS3YwzOd428t~Jy(b+bY7FzjhNGAFWtONvsE($dY040K3 z5&&bepPILV;24X7?KBGlS|&tqbbDK3N1gw-HqFf&7_UNb2UsK)rzM-|~O&YZabO~hQ0&m{9MssKXKA3#rvlE%kx z9rI?60@}PL8|UVTNT4_d;#LU!CbGiUZasHD$E1WH6htLtmPy?k2j%{{t>+H&z6Agn z5*VBd2yDGh-Cw`;+=c4dPU*r)#Bvm`m-`zgguH;aB20H2d(1mw$-RK0D|quP}- zrl#F|z=E(=3wy;(2IOi{)-LhagXocMD;#)fn5egJ>b5*{iED(45$tA)(}sG-rf!4G z1H%$d?7oAEImpyIwNWoHIFgPD9s~P9LBAaoqxkWCO8v(4)?*Yaa9n^a4J;hYJus}R zcWJX^V3ffle<*Rp3?w-T7le;XH z#O>hSxP%v0@0k>mr-1;_7#VdY7bJn>?uQx!8Zk4Ew+#Hn4vt&OWGwF^mho}gSg4V(%d-my=_qk%6;N-&p3j^DghE|0`2lT} z@GH(QJ>dKOhnu9VP4Q>2%?9GjVnxr{VsKqqIK6W&H5+1gTq80T;7xSXHfonVR3Fmw!v@1B#+YITR8$Qu0j}W3M$}WtyKN$qTDXvWSR*$! z$_s$4cHdbhg1Z@<0E(+C;PoAX(jcDWDi7ylfJe`y5G!eZN5M zlDdF5%NQ3G_3>S*2+`>yAX^fdM@ZUB^@&}o2!W%@l-OEu>;`u31?rP#DLQJeMx#xu z*Mqy1SA-IjmbKA>gY{FpSQ&}WC!2yWBKoi!6zbEvSQ+6Mqsw>&@|stO8`LgV#*cj5 zvH{j1!a$ResL$?VWuT<+Z5yR3R3_k3E%mwGtV|hE33NgwE>Q(O(VySN%H$wdY@&_e zL|+scSjI2xVr2>fYoja>3=q*KA?W|&j!1qe?MZVn&fztLq(pQR3;zi4 zE^xu5v8TSi)eItx`Zx?^Xp~X86F6dv86XWYANx?^03s>jBmd?WGYH_9AVw1erBF;j z0jsXuVg^K>0Sb^JVrg!$Kz(bA8N>`4P{I z`bA6OF;SbcGvqC(~vAzrMZ`q`*C#RLI}GjzQY zS_RK;_4844O3|1KV?NGX>~S-^eO9X6G3MczduyFKgn9624;X7)@L-;`l)# z$u5Q7m#JS)O3=>0?1`6G@KS+de0q9e*~A(;b$KswhI;qGv&A9o1&44l(ch~>E3$Kq z*b!?UIb{T+0didv3RJ$FDwE~LFfGI z$#lC4^Oy0#77r&p`ZI<z2ZfR()asNe&1dYWne0yfw7#RG{ypw1nQ4N zidZa)Gx)t5#yA@UxB<8X1jncQPm|7|DiK~uWB?gKI4QtW58pOM5TMieN~%8(DP-9c z;t?z}ASMwL4`eCgv!k`xo#%)(j$Q)C#52on{2`J>(aXq)^oYsLBE`Ig8sx8fp&W3V z9dX}$ff|M&ofXLg8DJ5Gv7m@z{Nb5qLUs03*Tb!q8=_y5n0(yi#YC^nnMxoJa^S>J z=M24xLaUo%KSnfSi2Jw;q4rScPPxryTq`M}BppzYqL6|lUU7j*fqf<&`k_PbqNw^V zxKrpxzLiCpZNayt&Kr7{iNT|pSFo7lLS%z3A2*gF?2PmgA1JAi6Fq!NrsK}bYu+kvub5k0(MEn$ z5^&}wXe2Cx!^i3oQ{sa=@V+6h9o}&kPkJW90H$G2J#s1-mPM?GP8F0!7*9|YVbj7n z2683!s3~#Y9b7hSQw%!Ph^w$K$k20g;&S9fFhIHli{`0EPl<)OZ#rCSvLl-g9fD@y zzmItYvi%4b-p5RhY$745trg=70iQbIlTZymc1m>1p@c(38|yI-$4y&-I91oJ5)=L9yHfeYhEOlTRH@}4?sPFRHrLkze!N!exLsrs~0b3*eCd8!4}4S!Mk zrh59QISFI|i#L=|1REYKAJNlf8Z3)M45&nt*3=HmThG=bzEgi+GxK+*2||xB z&88tQA%F(S2a@iGU9VN>|M!0@tnMnoza|ZJHP5s|5uY4rfZqW zb=*)0O+&(57PhONvzdCK7%ADXL6(MT7iHvgr?!u1m8fob|BJmC=s6t1>Y`25C=-`z zwlrXL0DU4qUA#d{LA}4lmcnoV6%KV`v<9#-spoB?USRj=RfrT0VF!reqMkpsOt-C| zSl#btdfIURYYF~16O-3iI1$^Mue2fP_r6p_c3x_3G4*EXicwa=^p&X>Om$qkWw4me zR`mn39|(N-wjnb~5UH7Tef7c(Zz04D$R&&Sg&2d3re3t+EezirT^4>wVw6R8eDQ|2 zz!u0$1$~v}tl5RHF4^#wlJ|tDPu%JNnJ5m_rCMrqY3|1E7-6(bR%ch*poV07;4tGs zGT;GPAdD{j_b<_M!RBqw$k8EcE}9XxJHocB%chQmAR5pTGdg`Oh0~EqS-o^hs42~i z0WCIyKri)^?oXZlKf%9>{lbVY3W4^4WImsj4d{uQ+Jucg8=TXU!?O#Cj!{JmfFxtC z0XR*g!3hNsxbCc-5^N-K*G=s# zwZrSTIyqQ*fS0iid3egSnW;DE)x*p#;Zg$kBcd0agYz@X2ij2R35rFObf{0T96A7jiy?4~@EyU& zhzXeu62va*EmOkzBKJBo3jy+R{c&Qv9wHwPT9`G?}%1zUdPoXNEtB?odC4~F3y`8z~UXKqDjff&y zfd2xfnozxcN;KEH)MEZj+cuPiD}rR>=P>iyAqYYjM>Y#UrFzHIKR13JvcX<5+iN#~ z@sCYjSo&}f;=S_DDIt38dYo)wZVnm^&qNW+L}AN>?s!Y9^Cpf`#+A0715p)ebq{(5 zF$d*7IxWJ+!>c{@1OY|qd{O)cM(-Q@ZJEM*#AwvJhFPd0$k4MqvlUG|P-`snY*PW* zKvo%G22>t23QBx7Wm)d{U__X{)a6rJdutS{I&)s&no2r7yi#zgY5#`EME&z4F@jyj z65+?eDY5BR@1D}`iq(_qQcP6Z6Bgzc@xqv2S?vU4EO&J=4wq82hDD{Qj6euv_kQEEMhZ)0F z@0}7@uDnH0p2D`)o+4zu($`R70gt3zNPB4WVgLI^P86sq?(7a!fWvODQo{fFoydUVJU z{8Ok6LGnZ376!KZz|@1+YHU0SqOB;wdn>b4EG|I@z9?Z{d5NcUDi2w$sz6Zeh@pt% zXAZ%cr9L=y+uq_0_mqc1R_1H4JQ3Df9I08|EwI8@Xr)@$xHZ#|gBr~-Z`BN3?oK=w z*Z|6VqGSRH4%CO5vwd&fjTkkgA^Q|N0FErEM2WQm;2x{2`mo-L>pmlJ%6LH-yJp!t z+Tp2u)O#adx~4y|EmozK8SsQ>$+QN9*1QBY8*Wt3}+vk64vYbUD=LV zq@+z)JRrDTnK{F;PEO7`ycdu%l*2M61~pF^$YfR)0aoG=dSvzSDM2(=Fj2fTrtbLtohHVCv`8kd~AOUd_VkbvL=2(5^XzYU#3X)htyrPJT5WCc8k4mWp zA_M^iAP<(!w5L9IG)he);*2pS9bTTq-cX;{mH_E?)kY?kaDcKFVS;3^YdtY<()I$p zj~oz*3^m@5`ofg3ZR@F_cR8{B*LNWI#5yKdjhw?l9j11b^4TE*u2wN-NcW~u#fvJrNXk6rOk$;7KO!Er|f3q z=qgd;`ccF<0=)^k1<%98g18ri>T7ytsgUP-=96|sXsD*?%}(M(4T6n2M!P{sU3Q-8 z>r=O=DkI#4LZ;5Nj8rR?V2epNK)izf40VwDMrZhoL`?ATsvme}(mxQ#8N*+g070kX z0IR;KcR2DIyKk`Fm(5ANq57&@Z*9SrU@umP%eRX)4Z)ZEIW` z7qlJm=Lj@}8WKYRQ9&~G?WucL?cCVesIRAF=nD*v61gkZLU4$R?3Eu}^GO7g2zR4!k_Hk@GsBGf?Ssqao*gD1&njhz3z zscV_DywEp)zdmQTHOp)M!PIpdziyvZEG-oI54GAtUUTEw7(ZGu)b6M|kW4Jx3<+yO ztqEGBex%hFJ@<9#)HZAkqQrSD22e=C)Cq^G`f;Z*8g;Ra{wiBoonIj?M#jTvo=*@^ zxWy4C0QC!zR&4c?sq5&MYTgSzrHIQvozm`4HRik;d=)@u3ktHnA*w4iJ>Ap|i4+s9 zBxlIXc!+nMGf(EI6WJu%sS5!9XH(~C9~8^TtUF$r%EiKW>1v3wa26Bb2`m54wHV#K z`Pqgh8KRs@@V&&U2o@8stkjCtFQzn4VBFZ^>DCm?(vVlOSH!Ojon@`C9OZq0mlEs( zY8}L&fuM; zr_(=lLD&>i<6%;NzXWM2q0|2A-GI~YMKr3N0Ba)zi$kZRTqO)FLyYBb3enY~{ zRQIzDSzZ=t3j{!bNr}w?c%&S6jX!l-udfy+7eV0=w?%IX65{a)qeK)JUIfF0`t#H+ ztCh_xKapW&)KytO_P6=^uX-twC6t{JYBc?#&Tfew!jm1Y z?at1m#xa=i-Z|>^w%*_I1gy~(0E{Ta{(~VsQRlQ;bGkv#ZCy2=H6I_^x)uPxMOmNq zSLd}R%cY_?ztzfT<-@~TEw*K>X%BB*rFtEYXk9Bupb`)8>;S=P9@)A!Co_kjZzs>0 z{qv7%U57WKwSApyk8Vx1)uEY{$Fy$HZcD|fc7|9W3pa-|X9%V9*w+7Qe7Ck0uD^d= zi)B^rtuOH0E01qozvrR=@RsGPC$x^Oz7nC0SSB+oPi#$T!R<+{7C*=i=1*=-E~iUp zJVl>VkEgb#^3}Py!%u5nt=*o@kl^Q+pH90KbI`es%WQE#Bbr%3`*7_`+68E-xN_j{cCZE#>$wgl?eb61Zk0mF3;n+&=Ro(J5QdP!^hO!mMO zPzHd@!Y)p*>Qr6U5-Xaik@??_s;AJSw(~4LFO?88AsXNaJ|c{qt%r?sE5K&0gcc=> zz5~JF|0){&J^@9KtLbT@wnI|vWy205_R++sz}*3BC_BxUwf-#9$^UyTF+?sPb|0Ek zHn*TdQ6(qp<*h%O1~71!|985AX@DMS151xvM)*>MUd*~%s8_TWdHoend2KyubgODu zXu!TU|4dJxuvZHk)6=M8T&flu*%ks+d9%ZhRUuOn$Ru44U@um3*AaWjH*|)MB!<&5jPIx`oPo- z7$;_*wnX6TNHh{j1(1h}eG~9(>Z(__1j1U^Vs51qe+I47Tp}hr=tii%?+~DyviXOH z5}>!&v?MlZbtC0EJ-q5%l7BL(?-GULQ(-!Oqw%`zqTbd%7Ha^swkpbzN7RK zHBZQ_N}W*=u&sGrOUs^H3Jj|$zj$xL{3TWM^#yW=IUW~7(Wym1pCOKWeQRg!*>7m= zv|ay=ErIM-Z%2&X!95KqV;>+9&EUX`5!q#c-wOC|BE%;!v9NW3w(d>3qiU>$uW6W| zO(1rg6X=slCe@ePqOcSP7#cMrL~@z}DUXvJwJVU^Z8u=)R*mJA5w>IS%1W=5S%Ysb^ zEWj*@ddJ$siK|Ii&aR-_$q2?rw3lN4;ULEZvM?XdtZSzcjq(vi6g`9z8|1?1j7!P1` z*$(%jumg8bTy@m7C9|lyTz9t4y~FCyd}QVntpqG|EG`$nb71uZR?-8|Qtxh^SU354 zZLt;{>`iPHkvPct-4kn5(d! zU<(bwXsP$EW!LDv9d#LSc86YsVn--+$R?jTlbAeO?MHSnbSA=_lmbDQ01oPX8#Gx} zyJ|LO+rm>;Rr4kEB5nvm;ze-UT)n>~IF;eeKQu3|;;cN}CWxf5c4Ll;0apNngX1+R z)dyPQsL+?U9h#mbMlm{{?0KYPAe>9|APkk_ikqnqw#4IY^ynW$gcr8IAUsQ-6&K(b1`HI>5M(EIM2zRPeb`zTFrV7glm@`}kqHYIX0b=2 zI94C-9#sThkrkQNYA+=@yJgd3v<(3uH10Pp5#GeYhfmQ*T7u0mO;7eR8eC+F^&HU} zCJu(0*1W8CA+MRMP}&D9=GYIc7y=l*{CD0jvDgMwkS7zuEWnm|E|kp7Tk2!2BSVCD z*9!^?P$S~Clw%115&rSk&6*GI*ZK^guOZ}za5C^>#A@}4RxVx42p`5^v9Jhcg+2TJ z0+fLqM3_A(KO}4ZX0=gZ1sMq?LQ=qcDby!h5)s(1;^k?t4!A?7rzLd6Nt@Mi+3VA4t&n-NC(0F(p?7R#XVh0gdSoqZE z^jp&<5a%~p3g?QI0}J`G(~D!_JITimjE^95TDrPl0^iE4mbz8}w*X=h^~Ei<Al(TOrbJ_nvy7ajGLwU&`C4svQ{7SD@i+t%n=DzKWZ_~lHY;ZYJv z4-}IJdtijhp9ssXWUbzfNwjCPGQiR)o4)DYh_GEC z!*tx^Aqs`rg$R_E=%}+?eanbMW{Q<9_!c197?0sjg|QR*SP(Mm+glnCgD2Lm0YT^= zgx-YB3`PZUjQGx)V6-L-fFx>?0CYCbPJlZK3?mr2fp8%ge77~CXPpa9%XV?jBNn=k z1g^f<5^K3%xyI{-NiP=oJnO*m1E7`q`+kEy-q1*?RwUsJ5k@Ta{nmc6?h1ORZ>@)* z7-aL>b*7;!X$+=bz!~G7se!4FwcWqr ze%KPgO*NAZn@|D+4X3Udj27Y&E!-;j-+1arYcIZgLbCLPv~P70x7K<-DjdKsIint> ze2}}P`th*ota0~R;i#tYZO59+T$7>zS3l7@^Zv?5W-=^Re9$^eV!DDj32*ZdI-a;F z{=eqFH9C&tI*T&dL&DpY4oEWw{BJ4y7$(-w?N8`*9zjd3+`@_^>!SMT&NfBNr>+N z+Yx{cr`w$Ix={Am^%f*&4TC>7F6BCGJ%OPGMJKeujTG^^;eZ~;X_{C-{*RA*TkR5( zQ0-?{5Urp7m{PbR6bEu<-qrLn209 z6~T;2o6bV04%kD$dKt&A;YimksPfyAOn~8iD+tS0uIU;MQ_Pn zN1Ec)@Io0br)_LAU>OFx3kO;FQ^k$HtBh()4X|3k2-b0GK14`Z?Ze?6*%R=>{QFAH z2T^ro9hXxMf5@i;1;wyogMiEsTwtgKf!6r-dJ2ja3L?u}3Y$ruVHi0$epc+a*h8>@ z1KMSv#;gzxAP{i+UlarY=4l9wiUX|w!958IIb%sj&XT4es1xj%=-|#t#Q(wa@sEzN z~-*QufCai*dWYh5?>N=Ik;bboi z@+6dI5Iv0h{9<}aHa?4U`6?7xy8I+yVE~a12S~#tD+L?6`~8AqNEI)}N2QXe!44rn zLs;S*lxr}wzyN@V@|%9zu#??I>yN3n6k9|VZw2bC!8#e%bV*n`Anwoue#Zh`rScN3 z0U2B*aJCn0kh*a|=9KYazffqI0xAg_<(JB%MeT^99kB46fbalQQ8+mMmJ_u^Eh-gM z0#h78N;(M#(urss4)^HKgz-_oO?hN}2?`rVcHR(gaGW=y!D-s^ zMVK~ZE?}FU?75&w!Qm629vJvI+cjr=%x_Cwt}=iK_)!SQ94gcjK(U5^dmg_uvp5H9 zCUFEV)I_AY0LWE9ca zDU44#J1GWGFPT=9;o#iPG4w>Syb%`Jyrb!0#ri?46r84>oWd?2bgXpvsee7r_T_~| zr^+Fd#d%CqWO9bxQ@8~hkN9mhI3Y#rmLg4I!i@`s7u0KrIFUhY4UpCDT-^8_|9hhF zN+lfB0QyMSz~h9uIQ+u9U>^LaUzp9%0#+(%C$0U57$Q$5Ctz5fgr+r_OBo+u7t+cL z6i%e!I1vOUn@B?M1$R1(#ZO3rBNkR@`ighcW@M|hgj+v{i2~;Z1mM9wCY>9RfFsK% z{lvD*QH_#PW#cr6k!D55>Vp7}0W0W9W2XP7dT&>5|6-ha3ny)ow)I-_hfLm<4}OEV*4SBKUkqNQ&Yxc?@cAH z5g5l|BWMd4_zZ%y#3qd2^}kEasjGOzN`qkx!j>Ue&lJ`$<8i-Brzjcibt;WPo*Z)s zevAnI2mLI?;X%M{;|V`)U&?+5`#_p$Ga`RE#a(#{;?g=`R|NVLgdBmjOc%nA7@uBO z4dthss|{%c0fIjQqT^%{l@E4spIKK8<=fz@fr10VaSb?1L9++{)F=J4zwg2l>FVk9tDF^C}w;|BO(#vuJtWVGLNGBj1SD@A+= z+zYVv4+}`B!J{}n+xWMBn6+U$aAhX`IX@Z5El)4u-v9iL1FVq@-r#3|ZAxMN$9En( zd8U(eqBts@R>sU+Co*H_86H_#EUHSZTmoSixv(g$5wSHlGd~U$=qmQrh+$BtDbLjE z!i1wxo2y+W7uom7<-@b19CQK*0ljyjEF`A39pND1O`2a`Tv(e!W(nVxgpY0)HfZA+ zY=y&1+wfg0Ti-w3SyTR%vH+9}@^#XiRTniV(Ln}g%hqbHY@)-9EsxjC8<+TM*8B!l zRy1UBzCyInotO#9Qzys7KKvvCRO2*ZI`DY{E*m+c_EK43OHm{ho#iN|B^z1;)DsfsmWM#P~D)3>7z!UK63jR ztoN`;PRCPF;XwBRL+A;k?)~Zg8!XE8OJxyi#Zj#=$$GIXv-UxNLte4S(eW1D6BKZ? z;d0qJ&7JJRNU$gN3C!{lINFhpd$QtwV95;6f=(j}Z#oGx60ivwqfsy4i;mHkpq;gp zG2E-4GSEvfYSrLK2)D}|cGhB3M(BK5*c0!COy2bVjDS_wN?O9RH@`o%nk+8qu>>BA z__Pfd%i_^Ii%N`v&dwb6n8#*TrjJfekoxizEqiMpcJLNN92wYbBY-A6(TtX=UH9%} z{?_u-$fis02cx*Le4X!W5XDW8@v>8>TWVm4^>I=}m^#ZHMKhKoTog$NAGXg5D$&-M zE`xBLl)i(+DPuoY_z^m&Ck!34*hb4n5xSMUqK&qHRTi$IgL_P9(xeAR5UF&p{2Ld9 zns$?khZIz`lUicz zc>ft=_>|sjj1TCiFk@gdnZ+rkh=Uu0rW-M{wp=MYW74M8gzH!*(?VOSoMEXgV)L$f z<{lJV@5s=QF-XfQBEUiz zgiRSdv$~<{H@0soEd}UVSlrQ5`T-(Enc`zJxmf>E6pnZZFe;qHUKbQ^aQZTK_Ejvy z4jlkR90fD%>K`0FqYw7ds)Eo2S#bW{*t|kC|4hyZe_Twf60ZB7)8n|kyG+}pud&1N z!3xGkCVs1Pv_%D*jgGTrvP#rCwsURsKH943I(GZX9{u+5Q~F?k?|ARY5z(6n1BMM# z*oYx=5Mnwcj9njCL&=HbCr{o+Q)n^`EKen|tDA{sU@^CQtZZ&cr6FgR*jAdxeX(A> z_qcI%+-6G=!Ii8arefHd*J3eaPkB;olYb$^S7u4ei`X}?J=$AUJd)eQ@1-%)zC0nX zWQe{mo8$Y%04k{~@$U8fHBAchcP=fG2*SNW5@CPOYoHg;txmx5MG4HosBINNQ54S* zoC2BJt7)vxj7cz=i)62Oy(B?TY}Y!w;+@g(eoSWAW~O5CFocyvMAIU%)B!J+!G*iW zwXROBGZrD2y2x^FHoSj&W#z*1;X{XDDLlQJ?Zi5BXm0M3yhKn-9xQGc<5h!Z0g<&3 zmlB2_*rC9EG}o2FG=Snh2Hh!+#GE)#e|cslf-+?A3174`7VXqR^fC(@7wFd`1S-8x zbVfyD>-_{?YU$1>8*PNwBgrXKa3p}M;Ij8%FuOW88Xo$?V2Fr?8a>*gqx}wPwRfkh0i8ls9}yeZjF->3wR}#QM=hUcclkV7c)ya%d}L~F zb$Oa`D7xs_g7JAwN`yQacljza|Bm9G#IF1z9Bj?55nV4&TwPq6nOwHeMdL)S#S*u* zG(_YJ7AOyI=MCT$jg5elG$B{34YFNUVTI8p!{kOT&19@@Qd&ggOju_YB&anes51)| zq-Fy2n!7Q0!2)t5iFpB!T$#W_&bieGrrkRZ0I_8V6H6zAyjmOWLE$KZV5hmps^;8O z20PG5t}w3d*&Sg18pc^|E%$Qn5q3H)&v>?x)&}X?%nsE4P3P`b&aWv)LWeq7JqsP| zV4-L*%z_hmyNl`3*$%#&g}EL+%npTl&l}(Svj@KY^rxTuDND0`c>dhC|NMbZJp7km zdmYa^@%+o{k3aO-Z@>8LPw>16&o967{V)IiBY*Wrui?27#Wd#i_R!`q3w1IUe9EIJ z*Z94mw5-Upn<+<1|A$&Sy!#cVzxiK3|B!O5o`v!Bf8SQEXCC$*P(1Cvuvhn_6i?Wr zhw`{@$ss%jcwIiMgw@cdc2?&N;seqL=;G-Eq=4ptQkPcRkgzSU zqdGI8$Y?)%D5C(Ps6Q)0Kbt^&r_tX#p z^o{H67G^@GpiewKul(S%U;ffppLn)5&GzH*<)8iOCqMF&7oPhi`Yw#;U;gO+k3Ij} zFFyBUJe&F2D4$+?*m~xn&FEJFv;`PFpdW(NKlu(XLB-PO7ccb_b(YyrUg{_6Ec3fZ zkLEG*Snx@et27kDvmL56Lc)AT=QI`=Fx}xnN8*Ncc=P(TypG{Idok?A$Q`KXKnkBl z3gu+IqCZP5S7pmp+^Bh-TCP$HcH+;=Kl$vJeJQpztoHo;mHR*cx4-zzqpzZ|8}R(v zOZPwVyWjuOv*9$FqV~M}!ZW}AZ=wc{6OufT{7t7KK{na0QoiKTj zNWgc0>w)il^@*>eJ-hJu=FdL$g$JK{{2O@OipT%;!lxei!NWg#mIl=Z`ux(LefhCh zAO9l>E7ya0eGgz`p$Mo4jfh}I))<=YPym4v3Z|4g%pMP>0?kGM71~AfiADz36`F?} zKRgHUd`&!uPSK@Op_lm)Addm{G@K z9x%G&Xg>;N$k0xt=vuzRQ_q5g80v1FH5QRa{aYJ>9YxM4z$T`XLde>NhtIw8sW13b zpbZMviDy3fKfdz#vww$f4&nLL7yk4!k3IZX&;2!?19<-V55M*6-+$z@k5ac&IW(C> zIrJbg{sWNUQj9A{^ydEuWa_VbLR(ny z9QIfiM3w9%BI@ks+e4A7M$^!ll&F4kYHo1vT0E(Q5QsV&y0zIx0c%Gf5> zAGBOH_EDO(7&wn?q#E^4h(FILvgXiiJJ)^fiq(zp{x3TIZGN#~58KTZ)#LT~12wh5 zy84F3P*d}Ume!4%+BR?5x^4T8o#FP5T_Ct15x4LL{Ar*u&kyki!0N{@fL|?sA^bMr z*Mi?h{5Ipa1;4HMZNqOnemn6CvnhnmAmgf-B#rJKgXjPAfV z9-|p;Qnc@InfY}lsiv7bkPmI3Ords8h^N~CEviIarc=sUx;voWl~zy5cc)TFB;FlQ zt6lP4S4!D0-$m1ESl)%xYDnI-rPToLIKhI95q1`UJ)KsM%cmpqDJq}#%cnL>+A!-; zWGc!^1n6}}r1?Q}+(>4Cut3w4159vtf?g6sMqC6v6CPUV+2S%35K5ZM7@Tr0tD#!d zt(vJB#xAmePa#SO4ATf^xW!AmNE9lP2PmbO;et4#rvx!H*$Wg};4QY{!6t@k5iwMY zh#@!t7co=?F%&!s6)K70uxl_7VFI2Dz1n1VX9ul)qC@MjPzcOYH`HO6h}r5GBq!}M z!HJuN3G(I1hw4Ouo(RKw0E}R5BGQUp0)v%Q>UzQthVe156*wpMPb{PT))W^a1oZ_* zD`*Kifn&thGktrzLZ#rTF|-4t9AB7Rp&3?aWT0{|^fQ2rzM)41+b8b>Sn}wdV+|K9 zI9`R!Pc5$HokiFI$q*n(TZdg04+ej!T8j%!5nB>*t}4JijRyeD0W>wk`5%K!=?6R< zH|k1WzbD`BrS+J&5FgeK8bIyz5o@ZV^V_hjhP|SsumVv8jisUN6MtE|ABz_Cro~8b z`s@&T6yO-_G7oLn11J^pMqNAJ5sTwJ#P7p=8o)y21vaB|`3?(jrf|fNHM9>n^c>PqNA(-i#)BXaOb;Ml8U1 zDv_u}b!LU4I(fgJ`ZLT9fQ;~`KLlz*gF^s96EhSh+2L%DavEGcphhwz0mn=%6r(Cd za!@mjC1eEGB*qKzt5l32An1p;-&QXM-{LBiboUAwS8|fFGYQWifmYAb&}_FL#C?md z3aLX6+`^SQA&cQ{nEL&I+J;5HFcXQLAVL(Xmt<7|Uc?JAtV12*t$~pt$YM=s2bD-G zs4SZ%mzb#HLr~{v9xVgZ(;#>RCCRTKKy@gPm`E|5!Tw>I6Eei(EgXf2Sp@R*B&P-V zx~f30a4%MUwA(8g*D$B~0=84O8{^hX;u{LS4_$#v|JDwl;HpUy@9@hZ;%`tC6)>sb zB++yp_Ce&LagENPicunw95frbJfYp9gJ}9t3NBONk01fmRbg+)XCYqx4SOSu0W##r zFh}=N!$5MpCdt9)z}AjH4}aiPJ@q&h0)C6|66SyzDMNl)h#U=g4#;O-fD*{_(6A2l zgg^Qv0<#D_Pf%8g?+;RDk6k5!L*-BbM7@+F6;dKiA*JXVvobWFy4LC}F#s$;ioTHq zE$Hf}Imn@N z{Iy{zAqCI3v5--NzWX-GB>J9Eatoe>>L*165D#B{38U*Raf^(v(+vjqUkG*0?|2}U zVQ9ZD8CqbSCLxkN91S&))TAqinMSV;vv63UWs}4gA%lT_K8+sk;U5O~5^5Bw|2u=$ zMlO-pS)P*9eEKefrov83Z%}GRmeUmqik$axU|;40;jwbQRgzP^g?}_EOZYgKMNH$5 zkP`m@AAn;a7y?lW2_sKZ;_K0Tm=Zr3&4=0*3_Ilr4gUWa?eNk>#_jcJJ0*CW=OOlq z47C1uFtZgZPb?m|V8VbpQZ2;Ns9&QBMS{>yB0h{n&j{n~eyD#$<7wG38$;FnhiFQ9 zl_0tgO!UtPM5OwkqLls0*YNQL`Uv!1r+5Ct?4@W})&GI@f$mvwL53h0@!pdc$RHUO z(EA|n`S-UO-_AA7Bhk`9hL6d;@kudrWF~tCI%-mZ0`y7(=xS z9SUg_kos4AE{tA|E;$8!i}whM@f-I7mcj$zObGpY=p+^0uF@)m zF&$>Jx_Yq#lC&3}Nj#^IVczaTC!qDfE)5SKnrF2VZ3GK}783RK%!1B-=wb)D&E%QF z;7e;zCKVOX`SE%l#^XwdPj9=3)xU;P*OKTC<{z|}-g?5d=weZ&mk@r5Ru0zT3wu_U zsN;#(Z3mSkmKS0yY6k@ZC8W|>xTc*0Rwxk6VN#kJY5jnRY7Y~CLmV8~8KT09WPs3O zv!z(2s4mf;c1v0C-*c$<)`LQUJ~1iXMVUzR5E@W939X832iAOkQNRB;KOC5AS44}5 ze|5GR?eK_plx+a@le;b!tVMUy_z5KnxAEW}j18Ujd5-9OH*WCXq?B;5Z?B zzY*u9(-tXNHd&jCPK%ry7YN55n8RUHb;#VS6pl!=Uzwl_U`8A_xP;-j1}zGA1q6Cg zo)$32r5n_ZtB^3u<4(k8XJ_Zh)fMa_4vW3d_ZnU}K;uY{^u!d7uERlc8ADyF(p_B^ zZe>{BXaTiJfms|HZX??aHZP)wL@{V6P9S!5fFqn;h6qoY6vBMa!IBBX^OV35g?L9K zoAuyGY{pt~Fddd`H!kDGT{#u+MkF)zWLC?-F|gb4enoa=YTaO(`_^z^qy6Cbrza6^ z0=}>?SVOE_4bCQpj}CaOH1XU$2}`N838NNs4mbE_Qp7ql+_JkHvlJ^LiIZC=aNI`{ zp)~x{@4iAa5R$v5iPU6_LgRKLMh!fsG$XL~L0Y+K32V=LTjN)l%%mN(B`Hxh^g( z$2BpI9dn9^q+?$l#mABfOrc4{@rI@C6oTyq$vvu4@$PPqgYKHPaaxp$mRs?~n25vC z7$F}JtuO`bM~%7AGB^bm?g6C1eolC6EeJ0_-3BI5OUGPZ9LLSTHq9D!iwrHg=;+3+d<}(vZI5|#p#PmTS2vdMTgV1US zaG2_X`R>MYMj4yPv%FHt-94-G7jz1!pA>egC>78kS_LqPGpupEQ`~4fQKbM2v@Ut% z7d+!IJ>ece!NR~7BA_RN`{B$6I0PeXjIo&_1XlvaEXzJwD2>=@I0H}wV!#TPGq$V= zNp27(aH<}>q;haxL#)17m$B7p+p?|Hv}HSmcdZ1X>_ivor)I_xG-E1?K$D1D)Qy0N zJN_H-AC(G$D`eG^HpvNkJ?vG1blPmQfmnA-!tn?$0fG~?ys0JE>N=%CaAgaNfuK3s*VC%u^1wc?51!b zm2ef)*$k7#0LMnleTuG#OplXO zjQ!OJkQVDPO*02zqH4zpjb)RG6i$fm&ZR&d5Dxf2!AS0=+p56n9z#W|JerSqEI7o6 zf|TOGDY(@fT*K-k1Arj3Ogu3KbAN<^%i@GDwoG~I4p|leEtra6`tr&4cMk?G}LF%FSix4&!#Hl@q?vLq>^K9Xf3nUqd8)QRO7}<#&A{R#ye-{D1a9C2> zNRk;hzNN&;FBh()8%%iUILFMytiMYwb$D;K{M-vIR6@n=U<2jM`MEUbuX9@R^MWOR zlq~t-X`Da+{bX8N@<-E=n0A1FeWPv3ey;v5zpz=itk=ny-C@@*4ccL?Zke%5qjs`> zH%;EH57MZ;@ImcpXB_)Os^al_6-Dv+{bY$z8}wt(2>JkC^}U>=@iUY=q;o@RdD#R9yW)hJq&)hh5{2r60(%1SK`9krHV4T_dzjf$3H zAw^5GCPnLF&5G8|HYl+uYf)kvYgJ+~wo!@2*(N2HU~Nh)$u=vo6x*W2V2h^2y4W@) z*3Gsn@hIDJ~4iN~O7rQ^1tYgvZ9?sIB;tEu>dij#I&eL8D`fkKnxtcRx6g?m-WFOn{760- zk~-QA&0sYOJ}HMOz6AeOV>`Q*G^0y!ZxP-whYK8@(-9+5#tNKF`y-hZ{=s)zfLH>R zzIU1(+HfXV+JbXfoVY;EaOUk*1k{r@Kt~&2wyhCEJUZEUn-~;MLs)75lE( zNOn4ibXp3j2*Kt%TvCM4c1m9Yy8x*HU0ICeD6R=L4!UoDk2^29JeBQZ=nU?LOZVNV z@!~2@2k69d5Lt%KZ+bhx zZqDphx^0NNQwT2-d3qe}jF>4=6b7w0Q;yy?;($OWo-1d9ubGUT8%n#gIOu`fohL$d zS^5dfUNd<2%9qrlP6pK&03fF;P)??QlA4JMPA1AyL^0TM%AK9G5otztfURJdCy! z&9k)wMI(jUy!8X`6c}Qhw-wc2k$hs%6&&oGE_2(9x@3(dh;?d5(KKIUUYmN0?S!2J zZUn5i5CKWSuPyve=PLh0^`C%uR{jg`V8js%mY5Mw%)k`ml3*$WTZ~JJ9@EUjz!`fP zSYt0E-dMp^6IaSa?6D8m1oPvX6xWov!s;Gxx>yaa-K>@omrPeM*0{zf4H#vV08UxO zH7V|?Qeu|r+9j^ttd0@Cj5lDI>7McySZ3nyYxZ&Mga=6;sBVzGekN_3o6Dh`+kJj6(j}CC zqU6lq4aUC*yT7syE!xFuj=L-@P%Y8DGYkA;LoV|oyuVpzuf|}EZ@dz z%6l8v`r0^W>V`YS*-J}^B)zzFrw9nW9Pr``W4;bbu-%owUXe}aP9){b7sT*VeP(t* z28j@!oETJaWWlvdT)R=+O%|{f`5DA%A-zp@Qk0FMyNAFCTlq>EAfoQ85@;1bt5g?J z0r5uzT#aU36uT2DkYz$$yKg&z+*Sz;O4H%nNNI2=ZxQE5A9wj$>0_t_Blx!4*FxXm zM=v-04aHgA)qTEimF?zJQo~ko}dn zJe)U|>q8c--f^t1wP+T|&SUkqLvwM<9E?0`*lYDi*;F>G&Khs7ny`o&XQ6eB57uJVdFZGZ7fQ==0M8QbQo$2evI5-w;DjfU_(dtkngA&Ue%rhxG(fwZ zAPu`}_?AnF0VaRj1y!OoUsTvF{$SOorCy;Jeu%{i9pw-(*}k%T$$sB%!Iu#8B|1h953pb2DJB1; zm2XnlR$)E%SBS}Rz$S4H)C$+BlB1$vTyWX&*H9F&t(P{C5 z6k#NV(aBkjBcg~{fFWaqP?+O2OZys-(q9PZAreC9Kmok)mUW2|16EcbNhE}v;Vcoa zS+}nSCHh2s50M6v%EO>eTt@sL$2E$Y(;Tce@$0IRqx$mEQJNddlB34r@GZ@u8r|C_lFYY{O0h+T;%W;7 zO4Lh>u*k6%AtOLTssT9gBn5BJt4j_7szwv4{m81?bQ7IZQO8S&4hcjbq&#b66h;3u ziEJ8WTNm3XGB=CN)M={SX*TC$v0#?YRv>Q!A+Mb>ROJ9gT4LyZ2Pmyg;;QjS0(Y$@ z?g;Hnusos%Hp(7o!CP-JuOsqpGV_6CF|V8V5#QU)?^HrvG4mr*Y&KH_k-}qgKjOm{ z^TWIk6mnyM)4_v_*THicZU=7*{7xC@smyYgU83r(RP|}*1L0=D@+-5z^ftmY4Y!~@ z+#Hjjc$pSiNR$(u6SOqK7I}71o-kEouV{`umd;Tm-APHeQPN7QxkwhKWHkN? zste4M!s6~+M@0rWmPim38w7U-(O8kRgObwlQ|uPHle$+}k!Kg>iBq0+F1I50ZpvMG zgzb^Nsr);x@Q#9}i*!l1K$G+VsOvrmZQXA`S$8XRbw{A8)1eILfhwREdb*<|pc|y6 z>!U5~W2B=yPI5p$Y3NSS7WQqlg?&4*>I0;m8zkl2Nz%=oBGudwF^9vXm^(vyxe?mJ z9wn{Z7%An>(iZkPQpuesjoclikoy8%kNHckvsyv~~+;EplsLDA{c>3&r_+RUm?2P}YkGwSWkf?4PXN-)T@3I?!bkb^}=0SfV)-{YQq|264tt42zA-; zfdQ%a_5=N@g+n*|d#%^Be^Ef~&92vWC5M80INN!HZuizRe?ZAI2Fz)oN;GJMY$hTm zF0Goe<#ypN;KSE(7W3R{4+$$Lt8L+|(d>5#^XdW=xz>01XJM&B8BpFKFxy7@Q<8jN zunjW`@XBb7cHQ1a4F2_68hpfCZ?}9re|cr{%8BUYS$*c@%!!M+`01J3drwSfk4?~X z-}$5c+Ta-@*}o9&M7oQ)W3%*rCeyc=%#LZ=$nnw3!?DGC#*FTvv18LCW22*~{+Y`& znX$fTe}3_v!K(w&!Tf|an4i^9?uCi?0OjvFJv(^rO!WN8vANvvurbtodUX2eaCE4D zC^~rjv^G67dUWW_=-g~{@XR@FL>uZE&7VIvoF6=X=IZF!ur_#hXyim@bY$_$Sa1Jw z|H7blZl*ugKdTu-bE79mjt=zlhGtjJ59Eh3L;0cOqgr3z@JRB^@a*Z>86$Us+K`L&T|F1;yWF3@knW#PPG`qP z2=>MEWA`j)uV|OE$43`3V+q37;<>Yfi)io6;L#Jkr>{=L2aW#3V1Bgk)m_2dy zq-f*N^m)SF+_HcxJ2tqSjgQXWK0kOVJKQ}tdAw(CVt#Jr{8%!2ZtU{R+2PAG6Z4}n zz;qe##m=AY2kiZqZ_gX61ERd1E7|zDnUnM9r?bZg=cbU}YVXjqh%f{S7>Yg(fBbO!@&i3Ax>zn9~=y(4=FO8Ri literal 0 HcmV?d00001 diff --git a/crates/wash/tests/github_fetch_test.rs b/crates/wash/tests/github_fetch_test.rs new file mode 100644 index 0000000000..99825930be --- /dev/null +++ b/crates/wash/tests/github_fetch_test.rs @@ -0,0 +1,250 @@ +#![cfg(target_os = "linux")] +// NOTE: These are only run on linux for CI purposes, because they rely on the docker client being +// available, and for various reasons this has proven to be problematic on both the Windows and +// MacOS runners we use. + +use std::ffi::{OsStr, OsString}; + +use tempfile::tempdir; +use tokio::io::AsyncBufReadExt; +use wasmcloud_test_util::testcontainers::{AsyncRunner as _, ImageExt, Mount, SquidProxy}; + +use wash_lib::start::{get_download_client, new_patch_releases_after, DOWNLOAD_CLIENT_USER_AGENT}; + +// For squid config reference, see: https://www.squid-cache.org/Doc/config/ +// Sets up a squid-proxy listening on port 3128 that requires basic auth +const SQUID_CONFIG_WITH_BASIC_AUTH: &str = r#" +# Listen on port 3128 for traffic, allows proxy to run as http endpoint, +# while still serving responses for both HTTP_PROXY and HTTPS_PROXY. +http_port 3128 +# log to stdout to make the logs accessible +logfile_rotate 0 +# This format translates to: |||| +logformat wasmcloud %rm|%ru|%>Hs|%{User-Agent}>h|%[un +cache_log stdio:/dev/stdout +access_log stdio:/dev/stderr wasmcloud +cache_store_log stdio:/dev/stdout +# This set of directives tells squid to require basic auth, +# but the passed in credentials can be whatever to make testing easier. +auth_param basic program /usr/libexec/basic_fake_auth +acl authenticated proxy_auth REQUIRED +http_access allow authenticated +http_access deny all +shutdown_lifetime 1 seconds +"#; + +// Sets up a squid-proxy listening on port 3128 that does not require any auth +const SQUID_CONFIG_WITHOUT_AUTH: &str = r#" +http_port 3128 +# log to stdout to make the logs accessible +logfile_rotate 0 +logformat wasmcloud %rm|%ru|%>Hs|%{User-Agent}>h|%[un +cache_log stdio:/dev/stdout +access_log stdio:/dev/stderr wasmcloud +cache_store_log stdio:/dev/stdout +# Log query params +strip_query_terms off +# allow unauthenticated http(s) access +http_access allow all +shutdown_lifetime 1 seconds +"#; + +struct EnvVarGuard { + var_name: OsString, + var_value: Option, +} + +impl Drop for EnvVarGuard { + fn drop(&mut self) { + if let Some(val) = self.var_value.take() { + std::env::set_var(&self.var_name, val); + } else { + std::env::remove_var(&self.var_name); + } + } +} + +impl EnvVarGuard { + /// Sets the environment variable `key` to `val` and returns a guard that will reset the + /// environment variable to its original value when dropped. + pub fn set(key: impl AsRef, val: impl AsRef) -> Self { + let var_name = OsString::from(key.as_ref()); + let var_value = std::env::var_os(&var_name); + std::env::set_var(&var_name, val); + Self { + var_name, + var_value, + } + } +} + +#[tokio::test] +#[cfg_attr(not(docker_available), ignore = "docker isn't available")] +async fn test_download_client_with_proxy_settings() { + // NOTE: This is intentional to avoid the two tests running in parallel + // and contaminating each other's environment variables for configuring + // the http client based on the environment. + test_http_proxy_without_auth().await; + test_http_proxy_with_basic_auth().await; +} + +async fn test_http_proxy_without_auth() { + let dir_path = tempdir().expect("Couldn't create tempdir"); + + let squid_config_path = dir_path.path().join("squid.conf"); + tokio::fs::write(squid_config_path.clone(), SQUID_CONFIG_WITHOUT_AUTH) + .await + .unwrap(); + + let container = SquidProxy::default() + .with_mount(Mount::bind_mount( + squid_config_path.to_string_lossy().to_string(), + "/etc/squid.conf", + )) + .start() + .await + .expect("failed to start squid-proxy container"); + + let proxy_val = format!( + "http://localhost:{}", + container + .get_host_port_ipv4(3128) + .await + .expect("failed to get squid-proxy host port") + ); + let _http_proxy_var = EnvVarGuard::set("HTTP_PROXY", &proxy_val); + let _https_proxy_var = EnvVarGuard::set("HTTPS_PROXY", &proxy_val); + + let client = get_download_client().unwrap(); + let http_endpoint = "http://httpbin.org/get"; + let https_endpoint = "https://httpbin.org/get"; + let http = client.get(http_endpoint).send().await.unwrap(); + let https = client.get(https_endpoint).send().await.unwrap(); + + let _ = container.stop().await; + + assert_eq!(http.status(), reqwest::StatusCode::OK); + assert_eq!(https.status(), reqwest::StatusCode::OK); + + let mut stderr = vec![]; + let mut lines = container.stderr(false).lines(); + while let Some(line) = lines.next_line().await.unwrap() { + stderr.push(line); + } + + // GET|http://httpbin.org/get|200|wash-lib/0.21.1|- + let http_log_entry = format!("GET|{http_endpoint}|200|{}|-", DOWNLOAD_CLIENT_USER_AGENT); + assert!( + stderr.contains(&http_log_entry), + "Didn't find connection log entry, logs:\n {}", + stderr.join("\n") + ); + + // CONNECT|httpbin.org:443|200|wash-lib/0.21.1|- + let https_url = url::Url::parse(https_endpoint).unwrap(); + let https_log_entry = format!( + "CONNECT|{}:{}|200|{}|-", + https_url.host_str().unwrap(), + https_url.port_or_known_default().unwrap(), + DOWNLOAD_CLIENT_USER_AGENT + ); + assert!(stderr.contains(&https_log_entry)); +} + +async fn test_http_proxy_with_basic_auth() { + let dir_path = tempdir().expect("Couldn't create tempdir"); + + let squid_config_path = dir_path.path().join("squid.conf"); + tokio::fs::write(squid_config_path.clone(), SQUID_CONFIG_WITH_BASIC_AUTH) + .await + .unwrap(); + + let container = SquidProxy::default() + .with_mount(Mount::bind_mount( + squid_config_path.to_string_lossy().to_string(), + "/etc/squid.conf", + )) + .start() + .await + .expect("failed to start squid-proxy container"); + + let proxy_val = format!( + "http://localhost:{}", + container + .get_host_port_ipv4(3128) + .await + .expect("failed to get squid-proxy host port") + ); + let _http_proxy_var = EnvVarGuard::set("HTTP_PROXY", &proxy_val); + let _https_proxy_var = EnvVarGuard::set("HTTPS_PROXY", &proxy_val); + + let proxy_username = "wasmcloud"; + let _proxy_username = EnvVarGuard::set("WASH_PROXY_USERNAME", proxy_username); + let _proxy_password = EnvVarGuard::set("WASH_PROXY_PASSWORD", "this-can-be-whatever"); + + let client = get_download_client().unwrap(); + let http_endpoint = "http://httpbin.org/get"; + let https_endpoint = "https://httpbin.org/get"; + let http = client.get(http_endpoint).send().await.unwrap(); + let https = client.get(https_endpoint).send().await.unwrap(); + + let _ = container.stop().await; + + assert_eq!(http.status(), reqwest::StatusCode::OK); + assert_eq!(https.status(), reqwest::StatusCode::OK); + + let mut stderr = vec![]; + let mut lines = container.stderr(false).lines(); + while let Some(line) = lines.next_line().await.unwrap() { + stderr.push(line); + } + + // GET|http://httpbin.org/get|200|wash-lib/0.21.1|wasmcloud + let http_log_entry = format!( + "GET|{http_endpoint}|200|{}|{proxy_username}", + DOWNLOAD_CLIENT_USER_AGENT + ); + assert!( + stderr.contains(&http_log_entry), + "Didn't find connection log entry, logs:\n {}", + stderr.join("\n") + ); + + // CONNECT|httpbin.org:443|200|wash-lib/0.21.1|wasmcloud + let https_url = url::Url::parse(https_endpoint).unwrap(); + let https_log_entry = format!( + "CONNECT|{}:{}|200|{}|{proxy_username}", + https_url.host_str().unwrap(), + https_url.port_or_known_default().unwrap(), + DOWNLOAD_CLIENT_USER_AGENT + ); + assert!(stderr.contains(&https_log_entry)); +} + +/// Test if the GitHubRelease struct is parsed correctly from the raw string. +/// Using an already "outdated" patch version to test if the sorting works correctly and comparable to the current version. +#[tokio::test] +#[cfg_attr(not(can_reach_github_com), ignore = "github.com is not reachable")] +async fn test_fetching_wasm_cloud_patch_versions_after_v_1_0_3() { + let owner = &"wasmCloud"; + let repo = &"wasmCloud"; + let latest_version = semver::Version::new(1, 0, 3); + // Use 1.0.3 as the latest version, since there is a newer version + let patch_releases = new_patch_releases_after(owner, repo, &latest_version) + .await + .expect("Should have been able to fetch releases"); + for new_path_release in patch_releases { + let semver::Version { + major, + minor, + patch, + .. + } = new_path_release + .get_main_artifact_release() + .expect("new patch version is semver conventional versions"); + + assert_eq!(latest_version.major, major, "major version is not changed"); + assert_eq!(latest_version.minor, minor, "minor version is not changed"); + assert!(latest_version.patch < patch, "patch version is bigger"); + } +} diff --git a/crates/wash/tests/parser/files/folder/wasmcloud.toml b/crates/wash/tests/parser/files/folder/wasmcloud.toml new file mode 100644 index 0000000000..01a525738d --- /dev/null +++ b/crates/wash/tests/parser/files/folder/wasmcloud.toml @@ -0,0 +1,17 @@ +language = "rust" +type = "component" +name = "testcomponent" +version = "0.1.0" + +[component] +claims = ["wasmcloud:httpserver"] +registry = "localhost:8080" +push_insecure = false +key_directory = "./keys" +filename = "testcomponent.wasm" +wasm_target = "wasm32-unknown-unknown" +call_alias = "testcomponent" + +[rust] +cargo_path = "./cargo" +target_path = "./target" \ No newline at end of file diff --git a/crates/wash/tests/parser/files/minimal_rust_component.toml b/crates/wash/tests/parser/files/minimal_rust_component.toml new file mode 100644 index 0000000000..3f5022bd2c --- /dev/null +++ b/crates/wash/tests/parser/files/minimal_rust_component.toml @@ -0,0 +1,7 @@ +language = "rust" +type = "component" +name = "testcomponent" +version = "0.1.0" + +[component] +claims = ["wasmcloud:httpserver"] diff --git a/crates/wash/tests/parser/files/minimal_rust_component_core_module.toml b/crates/wash/tests/parser/files/minimal_rust_component_core_module.toml new file mode 100644 index 0000000000..bc2fd8e541 --- /dev/null +++ b/crates/wash/tests/parser/files/minimal_rust_component_core_module.toml @@ -0,0 +1,8 @@ +language = "rust" +type = "component" +name = "testcomponent" +version = "0.1.0" + +[component] +claims = ["wasmcloud:httpserver"] +wasm_target = "wasm32-unknown-unknown" \ No newline at end of file diff --git a/crates/wash/tests/parser/files/minimal_rust_component_wasip1.toml b/crates/wash/tests/parser/files/minimal_rust_component_wasip1.toml new file mode 100644 index 0000000000..d2dfd566b2 --- /dev/null +++ b/crates/wash/tests/parser/files/minimal_rust_component_wasip1.toml @@ -0,0 +1,8 @@ +language = "rust" +type = "component" +name = "testcomponent" +version = "0.1.0" + +[component] +claims = ["wasmcloud:httpserver"] +wasm_target = "wasm32-wasi-preview1" \ No newline at end of file diff --git a/crates/wash/tests/parser/files/minimal_rust_component_wasip2.toml b/crates/wash/tests/parser/files/minimal_rust_component_wasip2.toml new file mode 100644 index 0000000000..595700595b --- /dev/null +++ b/crates/wash/tests/parser/files/minimal_rust_component_wasip2.toml @@ -0,0 +1,9 @@ +language = "rust" +type = "component" +name = "testcomponent" +version = "0.1.0" + +[component] +claims = ["wasmcloud:httpserver"] +wasm_target = "wasm32-wasip2" +wit_world = "test-world" \ No newline at end of file diff --git a/crates/wash/tests/parser/files/no_component.toml b/crates/wash/tests/parser/files/no_component.toml new file mode 100644 index 0000000000..2e0aef0a6e --- /dev/null +++ b/crates/wash/tests/parser/files/no_component.toml @@ -0,0 +1,12 @@ +language = "rust" +type = "component" +name = "testcomponent" +version = "0.1.0" + +[provider] +capability_id = "wasmcloud:httpserver" +vendor = "NoVendor" + +[rust] +cargo_path = "./cargo" +target_path = "./target" \ No newline at end of file diff --git a/crates/wash/tests/parser/files/no_interface.toml b/crates/wash/tests/parser/files/no_interface.toml new file mode 100644 index 0000000000..213df2d518 --- /dev/null +++ b/crates/wash/tests/parser/files/no_interface.toml @@ -0,0 +1,12 @@ +language = "rust" +type = "interface" +name = "testcomponent" +version = "0.1.0" + +[provider] +capability_id = "wasmcloud:httpserver" +vendor = "NoVendor" + +[rust] +cargo_path = "./cargo" +target_path = "./target" \ No newline at end of file diff --git a/crates/wash/tests/parser/files/no_provider.toml b/crates/wash/tests/parser/files/no_provider.toml new file mode 100644 index 0000000000..1f542b9cee --- /dev/null +++ b/crates/wash/tests/parser/files/no_provider.toml @@ -0,0 +1,8 @@ +language = "rust" +type = "provider" +name = "testcomponent" +version = "0.1.0" + +[rust] +cargo_path = "./cargo" +target_path = "./target" \ No newline at end of file diff --git a/crates/wash/tests/parser/files/noconfig/.gitkeep b/crates/wash/tests/parser/files/noconfig/.gitkeep new file mode 100644 index 0000000000..e69de29bb2 diff --git a/crates/wash/tests/parser/files/random.txt b/crates/wash/tests/parser/files/random.txt new file mode 100644 index 0000000000..dd3229baf6 --- /dev/null +++ b/crates/wash/tests/parser/files/random.txt @@ -0,0 +1 @@ +this is not a config file \ No newline at end of file diff --git a/crates/wash/tests/parser/files/rust_actor_custom_build.toml b/crates/wash/tests/parser/files/rust_actor_custom_build.toml new file mode 100644 index 0000000000..6f6783d4db --- /dev/null +++ b/crates/wash/tests/parser/files/rust_actor_custom_build.toml @@ -0,0 +1,9 @@ +language = "rust" +type = "component" +name = "custombuildcomponent" + +[component] +claims = ["wasmcloud:httpserver"] +build_artifact = "target/wasm32-wasip1/release/custombuildcomponent.wasm" +build_command = "cargo component build --release --target wasm32-wasip1" +destination = "./build/custombuildcomponent_s.wasm" diff --git a/crates/wash/tests/parser/files/rust_component.toml b/crates/wash/tests/parser/files/rust_component.toml new file mode 100644 index 0000000000..3f27584d44 --- /dev/null +++ b/crates/wash/tests/parser/files/rust_component.toml @@ -0,0 +1,17 @@ +language = "rust" +type = "component" +name = "testcomponent" +version = "0.1.0" + +[component] +claims = ["wasmcloud:httpserver"] +registry = "localhost:8080" +push_insecure = false +key_directory = "./keys" +destination = "./build/testcomponent.wasm" +wasm_target = "wasm32-unknown-unknown" +call_alias = "testcomponent" + +[rust] +cargo_path = "./cargo" +target_path = "./target" diff --git a/crates/wash/tests/parser/files/rust_component_claims_metadata.toml b/crates/wash/tests/parser/files/rust_component_claims_metadata.toml new file mode 100644 index 0000000000..2ef71bbeaa --- /dev/null +++ b/crates/wash/tests/parser/files/rust_component_claims_metadata.toml @@ -0,0 +1,22 @@ +language = "rust" +type = "component" +name = "testcomponent" +version = "0.1.0" +revision = 666 + +[component] +claims = ["wasmcloud:httpserver", "wasmcloud:httpclient", "lexcorp:quantum-simulator"] +registry = "localhost:8080" +push_insecure = false +key_directory = "./keys" +destination = "./build/testcomponent.wasm" +call_alias = "test-component" +tags = [ + "test", + "test", + "wasmcloud.com/experimental", +] + +[rust] +cargo_path = "./cargo" +target_path = "./target" \ No newline at end of file diff --git a/crates/wash/tests/parser/files/rust_component_with_revision.toml b/crates/wash/tests/parser/files/rust_component_with_revision.toml new file mode 100644 index 0000000000..24ce17ef56 --- /dev/null +++ b/crates/wash/tests/parser/files/rust_component_with_revision.toml @@ -0,0 +1,18 @@ +language = "rust" +type = "component" +name = "testcomponent" +version = "0.1.0" +revision = 666 + +[component] +claims = ["wasmcloud:httpserver"] +registry = "localhost:8080" +push_insecure = false +key_directory = "./keys" +destination = "./build/testcomponent.wasm" +wasm_target = "wasm32-unknown-unknown" +call_alias = "testcomponent" + +[rust] +cargo_path = "./cargo" +target_path = "./target" diff --git a/crates/wash/tests/parser/files/rust_provider_claims_metadata.toml b/crates/wash/tests/parser/files/rust_provider_claims_metadata.toml new file mode 100644 index 0000000000..bdffa67203 --- /dev/null +++ b/crates/wash/tests/parser/files/rust_provider_claims_metadata.toml @@ -0,0 +1,13 @@ +language = "rust" +type = "provider" +name = "testprovider" +version = "0.1.0" +revision = 666 + +[provider] +wit_world = "wasmcloud:httpserver" +vendor = "wayne-industries" + +[rust] +cargo_path = "./cargo" +target_path = "./target" \ No newline at end of file diff --git a/crates/wash/tests/parser/files/separate_project_paths.toml b/crates/wash/tests/parser/files/separate_project_paths.toml new file mode 100644 index 0000000000..39b5cc4fa8 --- /dev/null +++ b/crates/wash/tests/parser/files/separate_project_paths.toml @@ -0,0 +1,20 @@ +# This TOML file validates that our parser can handle paths relative to the manifest, +# and that the project, WIT, build, etc directories can be in different locations. +language = "rust" +type = "component" +name = "testcomponent" +version = "0.1.0" +path = "../" +# Absolute paths to other directories +build = "/tmp/some/other/build" +wit = "/tmp/nested/wit" + +[component] +# Relative paths (to the project path) +build_artifact = "build/testcomponent_raw.wasm" +destination = "./build/testcomponent.wasm" + +[rust] +# Relative paths (to the project path) +cargo_path = "../cargo" +target_path = "./target" diff --git a/crates/wash/tests/parser/files/tags.toml b/crates/wash/tests/parser/files/tags.toml new file mode 100644 index 0000000000..75f3aef2f3 --- /dev/null +++ b/crates/wash/tests/parser/files/tags.toml @@ -0,0 +1,14 @@ +language = "rust" +type = "component" +name = "testcomponent" +version = "0.1.0" + +[component] +claims = ["wasmcloud:httpserver"] +wasm_target = "wasm32-wasip2" +wit_world = "test-world" +tags = [ + "test", + "test", + "wasmcloud.com/experimental", +] \ No newline at end of file diff --git a/crates/wash/tests/parser/files/tinygo_component.toml b/crates/wash/tests/parser/files/tinygo_component.toml new file mode 100644 index 0000000000..14eb5568bc --- /dev/null +++ b/crates/wash/tests/parser/files/tinygo_component.toml @@ -0,0 +1,16 @@ +language = "tinygo" +type = "component" +name = "testcomponent" +version = "0.1.0" + +[component] +claims = ["wasmcloud:httpserver"] +registry = "localhost:8080" +push_insecure = false +key_directory = "./keys" +destination = "./build/testcomponent.wasm" +wasm_target = "wasm32-wasip2" +call_alias = "testcomponent" + +[tinygo] +tinygo_path = "path/to/tinygo" diff --git a/crates/wash/tests/parser/files/tinygo_component_module.toml b/crates/wash/tests/parser/files/tinygo_component_module.toml new file mode 100644 index 0000000000..0199aeeff1 --- /dev/null +++ b/crates/wash/tests/parser/files/tinygo_component_module.toml @@ -0,0 +1,17 @@ +language = "tinygo" +type = "component" +name = "testcomponent" +version = "0.1.0" + +[component] +claims = ["wasmcloud:httpserver"] +registry = "localhost:8080" +push_insecure = false +key_directory = "./keys" +destination = "./build/testcomponent.wasm" +wasm_target = "wasm" +call_alias = "testcomponent" + +[tinygo] +tinygo_path = "path/to/tinygo" +go_path = "path/to/go" diff --git a/crates/wash/tests/parser/files/tinygo_component_scheduler_gc.toml b/crates/wash/tests/parser/files/tinygo_component_scheduler_gc.toml new file mode 100644 index 0000000000..05c7f7670e --- /dev/null +++ b/crates/wash/tests/parser/files/tinygo_component_scheduler_gc.toml @@ -0,0 +1,17 @@ +language = "tinygo" +type = "component" +name = "testcomponent" +version = "0.1.0" + +[component] +claims = ["wasmcloud:httpserver"] +registry = "localhost:8080" +push_insecure = false +key_directory = "./keys" +destination = "./build/testcomponent.wasm" +wasm_target = "wasm32-wasip2" +call_alias = "testcomponent" + +[tinygo] +scheduler = "none" +garbage_collector = "leaking" diff --git a/crates/wash/tests/parser/files/withcargotoml/Cargo.toml b/crates/wash/tests/parser/files/withcargotoml/Cargo.toml new file mode 100644 index 0000000000..6125f865c2 --- /dev/null +++ b/crates/wash/tests/parser/files/withcargotoml/Cargo.toml @@ -0,0 +1,11 @@ +[package] +name = "withcargotoml" +version = "0.200.0" +edition = "2021" + +# See more keys and their definitions at https://doc.rust-lang.org/cargo/reference/manifest.html + +[lib] +crate-type = ["cdylib", "rlib"] + +[dependencies] diff --git a/crates/wash/tests/parser/files/withcargotoml/minimal_rust_component_with_cargo.toml b/crates/wash/tests/parser/files/withcargotoml/minimal_rust_component_with_cargo.toml new file mode 100644 index 0000000000..4cec22ed5b --- /dev/null +++ b/crates/wash/tests/parser/files/withcargotoml/minimal_rust_component_with_cargo.toml @@ -0,0 +1,5 @@ +language = "rust" +type = "component" + +[component] +claims = ["wasmcloud:httpserver"] \ No newline at end of file diff --git a/crates/wash/tests/parser/files/withcargotoml/src/main.rs b/crates/wash/tests/parser/files/withcargotoml/src/main.rs new file mode 100644 index 0000000000..e7a11a969c --- /dev/null +++ b/crates/wash/tests/parser/files/withcargotoml/src/main.rs @@ -0,0 +1,3 @@ +fn main() { + println!("Hello, world!"); +} diff --git a/crates/wash/tests/parser/main.rs b/crates/wash/tests/parser/main.rs new file mode 100644 index 0000000000..21c5a08467 --- /dev/null +++ b/crates/wash/tests/parser/main.rs @@ -0,0 +1,563 @@ +use std::{collections::HashSet, fs, path::PathBuf}; + +use claims::{assert_err, assert_ok}; +use semver::Version; +use wash_lib::parser::{ + load_config, CommonConfig, ComponentConfig, LanguageConfig, RegistryConfig, RustConfig, + TinyGoConfig, TinyGoGarbageCollector, TinyGoScheduler, TypeConfig, WasmTarget, +}; + +#[tokio::test] +async fn rust_component() { + let result = load_config( + Some(PathBuf::from("./tests/parser/files/rust_component.toml")), + None, + ) + .await; + + let config = assert_ok!(result); + + assert_eq!( + config.language, + LanguageConfig::Rust(RustConfig { + cargo_path: Some("./cargo".into()), + target_path: Some("./target".into()), + debug: false, + }) + ); + + assert_eq!( + config.project_type, + TypeConfig::Component(ComponentConfig { + key_directory: PathBuf::from("./keys"), + destination: Some(PathBuf::from("./build/testcomponent.wasm".to_string())), + wasip1_adapter_path: None, + wasm_target: WasmTarget::CoreModule, + ..ComponentConfig::default() + }) + ); + + assert_eq!( + config.common, + CommonConfig { + name: "testcomponent".to_string(), + version: Version::parse("0.1.0").unwrap(), + revision: 0, + project_dir: PathBuf::from("./tests/parser/files/") + .canonicalize() + .unwrap(), + build_dir: PathBuf::from("./tests/parser/files/") + .canonicalize() + .unwrap() + .join("build"), + wit_dir: PathBuf::from("./tests/parser/files/") + .canonicalize() + .unwrap() + .join("wit"), + wasm_bin_name: None, + registry: RegistryConfig::default(), + } + ); +} + +#[tokio::test] +/// When given a specific toml file's path, it should parse the file and return a `ProjectConfig`. +async fn rust_component_with_revision() { + let result = load_config( + Some(PathBuf::from( + "./tests/parser/files/rust_component_with_revision.toml", + )), + None, + ) + .await; + + let config = assert_ok!(result); + + assert_eq!( + config.language, + LanguageConfig::Rust(RustConfig { + cargo_path: Some("./cargo".into()), + target_path: Some("./target".into()), + debug: false, + }) + ); + + assert_eq!( + config.project_type, + TypeConfig::Component(ComponentConfig { + key_directory: PathBuf::from("./keys"), + destination: Some(PathBuf::from("./build/testcomponent.wasm".to_string())), + wasip1_adapter_path: None, + wasm_target: WasmTarget::CoreModule, + wit_world: None, + ..ComponentConfig::default() + }) + ); + + assert_eq!( + config.common, + CommonConfig { + name: "testcomponent".to_string(), + version: Version::parse("0.1.0").unwrap(), + revision: 666, + project_dir: PathBuf::from("./tests/parser/files/") + .canonicalize() + .unwrap(), + build_dir: PathBuf::from("./tests/parser/files/") + .canonicalize() + .unwrap() + .join("build"), + wit_dir: PathBuf::from("./tests/parser/files/") + .canonicalize() + .unwrap() + .join("wit"), + wasm_bin_name: None, + registry: RegistryConfig::default(), + } + ); +} + +#[tokio::test] +async fn tinygo_component_module_scheduler_gc() { + let result = load_config( + Some(PathBuf::from( + "./tests/parser/files/tinygo_component_scheduler_gc.toml", + )), + None, + ) + .await; + + let config = assert_ok!(result); + + assert_eq!( + config.language, + LanguageConfig::TinyGo(TinyGoConfig { + tinygo_path: None, + disable_go_generate: false, + scheduler: Some(TinyGoScheduler::None), + garbage_collector: Some(TinyGoGarbageCollector::Leaking), + }) + ); +} + +#[tokio::test] +async fn tinygo_component_module() { + let result = load_config( + Some(PathBuf::from( + "./tests/parser/files/tinygo_component_module.toml", + )), + None, + ) + .await; + + let config = assert_ok!(result); + + assert_eq!( + config.language, + LanguageConfig::TinyGo(TinyGoConfig { + tinygo_path: Some("path/to/tinygo".into()), + disable_go_generate: false, + scheduler: None, + garbage_collector: None, + }) + ); + + assert_eq!( + config.project_type, + TypeConfig::Component(ComponentConfig { + key_directory: PathBuf::from("./keys"), + destination: Some(PathBuf::from("./build/testcomponent.wasm".to_string())), + wasip1_adapter_path: None, + wasm_target: WasmTarget::CoreModule, + ..ComponentConfig::default() + }) + ); + + assert_eq!( + config.common, + CommonConfig { + name: "testcomponent".to_string(), + version: Version::parse("0.1.0").unwrap(), + revision: 0, + project_dir: PathBuf::from("./tests/parser/files/") + .canonicalize() + .unwrap(), + build_dir: PathBuf::from("./tests/parser/files/") + .canonicalize() + .unwrap() + .join("build"), + wit_dir: PathBuf::from("./tests/parser/files/") + .canonicalize() + .unwrap() + .join("wit"), + wasm_bin_name: None, + registry: RegistryConfig::default(), + } + ); +} + +#[tokio::test] +async fn tinygo_component() { + let result = load_config( + Some(PathBuf::from("./tests/parser/files/tinygo_component.toml")), + None, + ) + .await; + + let config = assert_ok!(result); + + assert_eq!( + config.project_type, + TypeConfig::Component(ComponentConfig { + key_directory: PathBuf::from("./keys"), + destination: Some(PathBuf::from("./build/testcomponent.wasm".to_string())), + wasip1_adapter_path: None, + wasm_target: WasmTarget::WasiP2, + ..ComponentConfig::default() + }) + ); +} + +#[tokio::test] +/// When given a folder, should automatically grab a wasmcloud.toml file inside it and parse it. +async fn folder_path() { + let result = load_config(Some(PathBuf::from("./tests/parser/files/folder")), None).await; + + let config = assert_ok!(result); + + assert_eq!( + config.language, + LanguageConfig::Rust(RustConfig { + cargo_path: Some("./cargo".into()), + target_path: Some("./target".into()), + debug: false, + }) + ); +} + +/// Gets the full path of a local path. Test helper. +fn get_full_path(path: &str) -> String { + match fs::canonicalize(path) { + Ok(path) => path.to_str().unwrap().to_string(), + Err(_) => panic!("get_full_path helper error. Could not find path: {path}"), + } +} + +#[tokio::test] +/// When given a folder with no wasmcloud.toml file, should return an error. +async fn folder_path_with_no_config() { + let result = load_config(Some(PathBuf::from("./tests/parser/files/noconfig")), None).await; + + let err = assert_err!(result); + assert_eq!( + format!( + "failed to find wasmcloud.toml in [{}]", + get_full_path("./tests/parser/files/noconfig") + ), + err.to_string().as_str() + ); +} + +#[tokio::test] +/// When given a random file, should return an error. +async fn random_file() { + let result = load_config(Some(PathBuf::from("./tests/parser/files/random.txt")), None).await; + + let err = assert_err!(result); + assert_eq!( + format!( + "invalid config file: {}", + get_full_path("./tests/parser/files/random.txt") + ), + err.to_string().as_str() + ); +} + +#[tokio::test] +/// When given a nonexistent file or path, should return an error. +async fn nonexistent_file() { + let result = load_config( + Some(PathBuf::from("./tests/parser/files/nonexistent.toml")), + None, + ) + .await; + + let err = assert_err!(result); + assert_eq!( + "path ./tests/parser/files/nonexistent.toml does not exist", + err.to_string().as_str() + ); +} + +#[tokio::test] +async fn nonexistent_folder() { + let result = load_config( + Some(PathBuf::from("./tests/parser/files/nonexistent/")), + None, + ) + .await; + + let err = assert_err!(result); + assert_eq!( + "path ./tests/parser/files/nonexistent/ does not exist", + err.to_string().as_str() + ); +} + +#[tokio::test] +async fn minimal_rust_component() { + let result = load_config( + Some(PathBuf::from( + "./tests/parser/files/minimal_rust_component.toml", + )), + None, + ) + .await; + + let config = assert_ok!(result); + + let mut expected_key_dir = + etcetera::home_dir().expect("Unable to determine the user's home directory"); + expected_key_dir.push(".wash/keys"); + + assert_eq!( + config.language, + LanguageConfig::Rust(RustConfig { + cargo_path: None, + target_path: None, + debug: false, + }) + ); + + assert_eq!( + config.project_type, + TypeConfig::Component(ComponentConfig { + key_directory: expected_key_dir, + destination: None, + wasip1_adapter_path: None, + wasm_target: WasmTarget::CoreModule, + ..ComponentConfig::default() + }) + ); + + assert_eq!( + config.common, + CommonConfig { + name: "testcomponent".to_string(), + version: Version::parse("0.1.0").unwrap(), + project_dir: PathBuf::from("./tests/parser/files/") + .canonicalize() + .unwrap(), + build_dir: PathBuf::from("./tests/parser/files/") + .canonicalize() + .unwrap() + .join("build"), + wit_dir: PathBuf::from("./tests/parser/files/") + .canonicalize() + .unwrap() + .join("wit"), + revision: 0, + wasm_bin_name: None, + registry: RegistryConfig::default(), + } + ); +} + +#[tokio::test] +async fn cargo_toml_component() { + let result = load_config( + Some(PathBuf::from( + "./tests/parser/files/withcargotoml/minimal_rust_component_with_cargo.toml", + )), + None, + ) + .await; + + let config = assert_ok!(result); + + let mut expected_key_dir = + etcetera::home_dir().expect("Unable to determine the user's home directory"); + expected_key_dir.push(".wash/keys"); + + assert_eq!( + config.language, + LanguageConfig::Rust(RustConfig { + cargo_path: None, + target_path: None, + debug: false, + }) + ); + + assert_eq!( + config.project_type, + TypeConfig::Component(ComponentConfig { + key_directory: expected_key_dir, + destination: None, + wasip1_adapter_path: None, + wasm_target: WasmTarget::CoreModule, + ..ComponentConfig::default() + }) + ); + + assert_eq!( + config.common, + CommonConfig { + name: "withcargotoml".to_string(), + version: Version::parse("0.200.0").unwrap(), + project_dir: PathBuf::from("./tests/parser/files/withcargotoml") + .canonicalize() + .unwrap(), + build_dir: PathBuf::from("./tests/parser/files/withcargotoml") + .canonicalize() + .unwrap() + .join("build"), + wit_dir: PathBuf::from("./tests/parser/files/withcargotoml") + .canonicalize() + .unwrap() + .join("wit"), + revision: 0, + wasm_bin_name: None, + registry: RegistryConfig::default(), + } + ); +} + +/// wasm_target=wasm32-wasip2 is properly parsed +/// see: https://github.com/wasmCloud/wash/issues/640 +#[tokio::test] +async fn minimal_rust_component_p2() { + let result = load_config( + Some(PathBuf::from( + "./tests/parser/files/minimal_rust_component_wasip2.toml", + )), + None, + ) + .await; + + let config = assert_ok!(result); + + let mut expected_default_key_dir = + etcetera::home_dir().expect("Unable to determine the user's home directory"); + expected_default_key_dir.push(".wash/keys"); + + assert_eq!( + config.project_type, + TypeConfig::Component(ComponentConfig { + key_directory: expected_default_key_dir, + wasm_target: WasmTarget::WasiP2, + wit_world: Some("test-world".to_string()), + ..Default::default() + }) + ); +} + +/// wasm_target=wasm32-wasip1 is properly parsed +/// see: https://github.com/wasmCloud/wash/issues/640 +#[tokio::test] +async fn minimal_rust_component_wasip1() { + let result = load_config( + Some(PathBuf::from( + "./tests/parser/files/minimal_rust_component_wasip1.toml", + )), + None, + ) + .await; + + let config = assert_ok!(result); + assert!(matches!( + config.project_type, + TypeConfig::Component(ComponentConfig { + wasm_target: WasmTarget::WasiP1, + .. + }) + )); +} + +/// wasm_target=wasm32-unknown-unknown is properly parsed +/// see: https://github.com/wasmCloud/wash/issues/640 +#[tokio::test] +async fn minimal_rust_component_core_module() { + let result = load_config( + Some(PathBuf::from( + "./tests/parser/files/minimal_rust_component_core_module.toml", + )), + None, + ) + .await; + + let config = assert_ok!(result); + assert!(matches!( + config.project_type, + TypeConfig::Component(ComponentConfig { + wasm_target: WasmTarget::CoreModule, + .. + }) + )); +} + +/// Tags are properly handled (duplicates, pre-existing experimental tag) +/// see: https://github.com/wasmCloud/wash/pull/951 +#[tokio::test] +async fn tags() { + let result = load_config(Some(PathBuf::from("./tests/parser/files/tags.toml")), None).await; + + let config = assert_ok!(result); + assert!(matches!( + config.project_type, + TypeConfig::Component(ComponentConfig { + tags, + .. + }) if tags == Some(HashSet::from(["test".into(), "wasmcloud.com/experimental".into()])), + )); +} + +/// Projects with overridden paths should be properly handled +/// +/// NOTE: this test uses hard-coded paths in config that include '/tmp' +/// and as such, doesn't work on windows. +#[tokio::test] +#[cfg(not(target_os = "windows"))] +async fn separate_project_paths() { + let result = load_config( + Some(PathBuf::from( + "./tests/parser/files/separate_project_paths.toml", + )), + None, + ) + .await; + let config = assert_ok!(result); + // Different project path handled + assert_eq!( + config.common.project_dir, + PathBuf::from("./tests/parser") + .canonicalize() + .expect("failed to canonicalize test path") + ); + // Absolute paths properly handled + assert_eq!( + config.common.build_dir, + PathBuf::from("/tmp/some/other/build") + ); + assert_eq!(config.common.wit_dir, PathBuf::from("/tmp/nested/wit")); + + // Relative paths properly handled + assert!(matches!( + config.project_type, + TypeConfig::Component(ComponentConfig { + build_artifact: Some(build_artifact), + destination: Some(destination), + .. + }) if build_artifact == PathBuf::from("build/testcomponent_raw.wasm") + && destination == PathBuf::from("./build/testcomponent.wasm") + )); + + assert!(matches!( + config.language, + LanguageConfig::Rust(RustConfig { + cargo_path: Some(cargo_path), + target_path: Some(target_path), + debug: false, + }) if cargo_path == PathBuf::from("../cargo") + && target_path == PathBuf::from("./target") + )); +} diff --git a/crates/wash/tests/plugins.rs b/crates/wash/tests/plugins.rs new file mode 100644 index 0000000000..fb4738baea --- /dev/null +++ b/crates/wash/tests/plugins.rs @@ -0,0 +1,68 @@ +use std::path::PathBuf; + +use wash_lib::plugin::subcommand::DirMapping; + +#[tokio::test] +async fn test_subcommand() { + let mut subcommand = wash_lib::plugin::subcommand::SubcommandRunner::new().unwrap(); + // NOTE: All the joins are to avoid any problems with cross-platform paths + let plugin_path = + // This is pre-compiled to save on test time. To rebuild this plugin when changes are needed + // (assuming relative paths from this file) run: + // `pushd plugins/hello_plugin && wash build && cp build/hello_plugin_s.wasm ../../fixtures/hello_plugin_s.wasm && popd` + PathBuf::from(std::env::var("CARGO_MANIFEST_DIR").expect("Unable to find manifest dir")) + .join("tests") + .join("fixtures") + .join("hello_plugin_s.wasm"); + let metadata = subcommand + .add_plugin(plugin_path) + .await + .expect("Should be able to add plugin"); + assert_eq!(metadata.name, "Hello Plugin"); + assert_eq!(metadata.version, "0.1.0"); + assert_eq!(metadata.id, "hello"); + + let temp = tempfile::tempdir().unwrap(); + let extra_dir = tempfile::tempdir().unwrap(); + tokio::fs::write(extra_dir.path().join("hello.txt"), "hello") + .await + .unwrap(); + tokio::fs::write(extra_dir.path().join("world.txt"), "world") + .await + .unwrap(); + + let file_dir = tempfile::tempdir().unwrap(); + let file = file_dir.path().join("hello.txt"); + tokio::fs::write(&file, "Hello from a file").await.unwrap(); + + // TODO: allow configuration of stdout/stderr so we can check for output + subcommand + .run( + "hello", + temp.path().to_path_buf(), + vec![ + DirMapping { + host_path: extra_dir.path().to_path_buf(), + component_path: None, + }, + DirMapping { + host_path: file.clone(), + component_path: None, + }, + ], + vec![ + "hello".to_string(), + "--foo".to_string(), + extra_dir.path().to_str().unwrap().to_string(), + file.to_str().unwrap().to_string(), + ], + ) + .await + .expect("Should be able to run plugin"); + + // Check that the file was written + let file = tokio::fs::read_to_string(temp.path().join("hello.txt")) + .await + .unwrap(); + assert_eq!(file, "Hello from the plugin"); +} diff --git a/crates/wash/tests/plugins/hello_plugin/.gitignore b/crates/wash/tests/plugins/hello_plugin/.gitignore new file mode 100644 index 0000000000..e259685ee3 --- /dev/null +++ b/crates/wash/tests/plugins/hello_plugin/.gitignore @@ -0,0 +1,6 @@ +# Rust build artifacts +Cargo.lock + +# Wash build artifacts +build/ +keys/ diff --git a/crates/wash/tests/plugins/hello_plugin/Cargo.toml b/crates/wash/tests/plugins/hello_plugin/Cargo.toml new file mode 100644 index 0000000000..834131bbc2 --- /dev/null +++ b/crates/wash/tests/plugins/hello_plugin/Cargo.toml @@ -0,0 +1,15 @@ +[package] +name = "hello-plugin" +edition = "2021" +version = "0.1.0" + +[workspace] + +[lib] +crate-type = ["cdylib"] + +[dependencies] +clap = { version = "4", features = ["derive"] } +serde = { version = "1", features = ["derive"] } +serde_json = "1" +wit-bindgen = { version = "0.24", features = ["default"] } diff --git a/crates/wash/tests/plugins/hello_plugin/src/lib.rs b/crates/wash/tests/plugins/hello_plugin/src/lib.rs new file mode 100644 index 0000000000..4e94870a49 --- /dev/null +++ b/crates/wash/tests/plugins/hello_plugin/src/lib.rs @@ -0,0 +1,282 @@ +#![allow(clippy::missing_safety_doc)] +wit_bindgen::generate!(); + +use std::io::Read; +use std::path::{Path, PathBuf}; + +use clap::builder::ValueParser; +use clap::{Arg, CommandFactory, FromArgMatches}; +use exports::wasi::cli::run::Guest as RunGuest; +use exports::wasmcloud::wash::subcommand::{Argument, Guest as SubcommandGuest, Metadata}; +use wasi::cli::environment; +use wasi::filesystem::preopens::get_directories; +use wasi::filesystem::types::{Descriptor, DescriptorFlags, OpenFlags, PathFlags}; +use wasi::http::types::*; + +impl From<&Arg> for Argument { + fn from(arg: &Arg) -> Self { + Self { + description: arg.get_help().map(ToString::to_string).unwrap_or_default(), + is_path: arg.get_value_parser().type_id() == ValueParser::path_buf().type_id(), + required: arg.is_required_set(), + } + } +} + +#[derive(clap::Parser)] +#[clap(name = "hello")] +struct Hello { + /// A random string + #[clap(long = "bar")] + bar: Option, + + /// A directory to read + #[clap(long = "foo")] + foo: PathBuf, + + /// A file to read + #[clap(id = "path")] + path: PathBuf, +} + +#[derive(serde::Deserialize)] +struct DogResponse { + message: String, + status: String, +} + +struct HelloPlugin; + +impl RunGuest for HelloPlugin { + fn run() -> Result<(), ()> { + let args = environment::get_arguments(); + println!("I got some arguments: {:?}", args); + println!( + "I got some environment variables: {:?}", + environment::get_environment() + ); + + let cmd = Hello::command(); + let matches = match cmd.try_get_matches_from(args) { + Ok(matches) => matches, + Err(err) => { + eprintln!("Error parsing arguments: {}", err); + return Err(()); + } + }; + let args = match Hello::from_arg_matches(&matches) { + Ok(args) => args, + Err(err) => { + eprintln!("Error parsing arguments: {}", err); + return Err(()); + } + }; + + let req = wasi::http::outgoing_handler::OutgoingRequest::new(Fields::new()); + req.set_scheme(Some(&Scheme::Https))?; + req.set_authority(Some("dog.ceo"))?; + req.set_path_with_query(Some("/api/breeds/image/random"))?; + match wasi::http::outgoing_handler::handle(req, None) { + Ok(resp) if wasi::io::poll::poll(&[&resp.subscribe()]) == [0] => { + let response = resp + .get() + .expect("HTTP request response missing") + .expect("HTTP request response requested more than once") + .expect("HTTP request failed"); + if response.status() == 200 { + let response_body = response + .consume() + .expect("failed to get incoming request body"); + let body = { + let mut buf = vec![]; + let mut stream = response_body + .stream() + .expect("failed to get HTTP request response stream"); + InputStreamReader::from(&mut stream) + .read_to_end(&mut buf) + .expect("failed to read value from HTTP request response stream"); + buf + }; + let _trailers = wasi::http::types::IncomingBody::finish(response_body); + let dog_response: DogResponse = match serde_json::from_slice(&body) { + Ok(d) => d, + Err(e) => { + println!("Failed to deserialize dog response: {}", e); + DogResponse { + message: "Failed to deserialize dog response".to_string(), + status: "failure".to_string(), + } + } + }; + println!( + "{}! Here have a dog picture: {}", + dog_response.status, dog_response.message + ); + } else { + eprintln!("HTTP request failed with status code {}", response.status()); + } + } + Ok(_) => { + eprintln!("Got response, but it wasn't ready"); + } + Err(e) => { + eprintln!("Got error when trying to fetch dog: {}", e); + } + } + if let Ok(dir) = get_dir("/") { + let file = dir + .open_at( + PathFlags::empty(), + "hello.txt", + OpenFlags::CREATE, + DescriptorFlags::READ | DescriptorFlags::WRITE, + ) + .expect("Should be able to access file"); + file.write(b"Hello from the plugin", 0) + .expect("Should be able to write to file"); + } + + if let Ok(dir) = get_dir(&args.foo) { + let entries = dir.read_directory().map_err(|e| { + eprintln!("Failed to read directory: {}", e); + })?; + println!("Directory entries for {}:", args.foo.display()); + while let Some(res) = entries.read_directory_entry().transpose() { + let entry = res.map_err(|e| { + eprintln!("Failed to read directory entry: {}", e); + })?; + println!("{}", entry.name); + } + } + + let file = + open_file(&args.path, OpenFlags::empty(), DescriptorFlags::READ).map_err(|e| { + eprintln!("Failed to open file: {}", e); + })?; + + let mut body = file.read_via_stream(0).map_err(|e| { + eprintln!("Failed to read file: {}", e); + })?; + let mut buf = vec![]; + InputStreamReader::from(&mut body) + .read_to_end(&mut buf) + .map_err(|e| { + eprintln!("Failed to read file: {}", e); + })?; + println!( + "The file {} has the contents {}", + args.path.display(), + String::from_utf8_lossy(&buf) + ); + + println!("Hello from the plugin"); + Ok(()) + } +} + +impl SubcommandGuest for HelloPlugin { + fn register() -> Metadata { + let cmd = Hello::command(); + let (arguments, flags): (Vec<_>, Vec<_>) = + cmd.get_arguments().partition(|arg| arg.is_positional()); + // There isn't a partition_map function without importing another crate + let arguments = arguments + .into_iter() + .map(|arg| (arg.get_id().to_string(), Argument::from(arg))) + .collect(); + let flags = flags + .into_iter() + .map(|arg| (arg.get_id().to_string(), Argument::from(arg))) + .collect(); + Metadata { + name: "Hello Plugin".to_string(), + id: "hello".to_string(), + description: "A simple plugin that says hello and logs a bunch of things".to_string(), + author: "WasmCloud".to_string(), + version: env!("CARGO_PKG_VERSION").to_string(), + flags, + arguments, + } + } +} + +fn get_dir(path: impl AsRef) -> Result { + get_directories() + .into_iter() + .find_map(|(dir, dir_path)| { + (>::as_ref(&dir_path) == path.as_ref()) + .then_some(dir) + }) + .ok_or_else(|| format!("Could not find directory {}", path.as_ref().display())) +} + +/// Opens the given file. This should be the canonicalized path to the file. +fn open_file( + path: impl AsRef, + open_flags: OpenFlags, + descriptor_flags: DescriptorFlags, +) -> Result { + let dir = path + .as_ref() + .parent() + // I mean, if someone passed a path that is at the root, that probably wasn't a good idea + .ok_or_else(|| { + format!( + "Could not find parent directory of {}", + path.as_ref().display() + ) + })?; + let dir = get_dir(dir)?; + dir.open_at( + PathFlags::empty(), + path.as_ref() + .file_name() + .ok_or_else(|| format!("Path did not have a file name: {}", path.as_ref().display()))? + .to_str() + .ok_or_else(|| "Path is not a valid string".to_string())?, + open_flags, + descriptor_flags, + ) + .map_err(|e| format!("Failed to open file {}: {}", path.as_ref().display(), e)) +} + +pub struct InputStreamReader<'a> { + stream: &'a mut crate::wasi::io::streams::InputStream, +} + +impl<'a> From<&'a mut crate::wasi::io::streams::InputStream> for InputStreamReader<'a> { + fn from(stream: &'a mut crate::wasi::io::streams::InputStream) -> Self { + Self { stream } + } +} + +impl std::io::Read for InputStreamReader<'_> { + fn read(&mut self, buf: &mut [u8]) -> std::io::Result { + use crate::wasi::io::streams::StreamError; + use std::io; + + let n = buf + .len() + .try_into() + .map_err(|e| io::Error::new(io::ErrorKind::Other, e))?; + match self.stream.blocking_read(n) { + Ok(chunk) => { + let n = chunk.len(); + if n > buf.len() { + return Err(io::Error::new( + io::ErrorKind::Other, + "more bytes read than requested", + )); + } + buf[..n].copy_from_slice(&chunk); + Ok(n) + } + Err(StreamError::Closed) => Ok(0), + Err(StreamError::LastOperationFailed(e)) => { + Err(io::Error::new(io::ErrorKind::Other, e.to_debug_string())) + } + } + } +} + +export!(HelloPlugin); diff --git a/crates/wash/tests/plugins/hello_plugin/wasmcloud.lock b/crates/wash/tests/plugins/hello_plugin/wasmcloud.lock new file mode 100644 index 0000000000..c60b7ac881 --- /dev/null +++ b/crates/wash/tests/plugins/hello_plugin/wasmcloud.lock @@ -0,0 +1,30 @@ +# This file is automatically generated. +# It is not intended for manual editing. +version = 1 + +[[packages]] +name = "wasi:cli" +registry = "wasi.dev" + +[[packages.versions]] +requirement = "=0.2.0" +version = "0.2.0" +digest = "sha256:e7e85458e11caf76554b724ebf4f113259decf0f3b1ee2e2930de096f72114a7" + +[[packages]] +name = "wasi:http" +registry = "wasi.dev" + +[[packages.versions]] +requirement = "=0.2.0" +version = "0.2.0" +digest = "sha256:5a568e6e2d60c1ce51220e1833cdd5b88db9f615720edc762a9b4a6f36b383bd" + +[[packages]] +name = "wasmcloud:wash" +registry = "wasmcloud.com" + +[[packages.versions]] +requirement = "=0.1.0" +version = "0.1.0" +digest = "sha256:1a7b5ad447b08ba8e744d359993f38f0b0888479a16e2b109de7951b35e81a5c" diff --git a/crates/wash/tests/plugins/hello_plugin/wasmcloud.toml b/crates/wash/tests/plugins/hello_plugin/wasmcloud.toml new file mode 100644 index 0000000000..dcf12db8f4 --- /dev/null +++ b/crates/wash/tests/plugins/hello_plugin/wasmcloud.toml @@ -0,0 +1,7 @@ +name = "hello-plugin" +language = "rust" +type = "component" + +[component] +wit_world = "plugin" +wasm_target = "wasm32-wasip2" diff --git a/crates/wash/tests/plugins/hello_plugin/wit/deps/wasi-cli-0.2.0/package.wit b/crates/wash/tests/plugins/hello_plugin/wit/deps/wasi-cli-0.2.0/package.wit new file mode 100644 index 0000000000..0a2737b7c1 --- /dev/null +++ b/crates/wash/tests/plugins/hello_plugin/wit/deps/wasi-cli-0.2.0/package.wit @@ -0,0 +1,159 @@ +package wasi:cli@0.2.0; + +interface environment { + /// Get the POSIX-style environment variables. + /// + /// Each environment variable is provided as a pair of string variable names + /// and string value. + /// + /// Morally, these are a value import, but until value imports are available + /// in the component model, this import function should return the same + /// values each time it is called. + get-environment: func() -> list>; + + /// Get the POSIX-style arguments to the program. + get-arguments: func() -> list; + + /// Return a path that programs should use as their initial current working + /// directory, interpreting `.` as shorthand for this. + initial-cwd: func() -> option; +} + +interface exit { + /// Exit the current instance and any linked instances. + exit: func(status: result); +} + +interface run { + /// Run the program. + run: func() -> result; +} + +interface stdin { + use wasi:io/streams@0.2.0.{input-stream}; + + get-stdin: func() -> input-stream; +} + +interface stdout { + use wasi:io/streams@0.2.0.{output-stream}; + + get-stdout: func() -> output-stream; +} + +interface stderr { + use wasi:io/streams@0.2.0.{output-stream}; + + get-stderr: func() -> output-stream; +} + +/// Terminal input. +/// +/// In the future, this may include functions for disabling echoing, +/// disabling input buffering so that keyboard events are sent through +/// immediately, querying supported features, and so on. +interface terminal-input { + /// The input side of a terminal. + resource terminal-input; +} + +/// Terminal output. +/// +/// In the future, this may include functions for querying the terminal +/// size, being notified of terminal size changes, querying supported +/// features, and so on. +interface terminal-output { + /// The output side of a terminal. + resource terminal-output; +} + +/// An interface providing an optional `terminal-input` for stdin as a +/// link-time authority. +interface terminal-stdin { + use terminal-input.{terminal-input}; + + /// If stdin is connected to a terminal, return a `terminal-input` handle + /// allowing further interaction with it. + get-terminal-stdin: func() -> option; +} + +/// An interface providing an optional `terminal-output` for stdout as a +/// link-time authority. +interface terminal-stdout { + use terminal-output.{terminal-output}; + + /// If stdout is connected to a terminal, return a `terminal-output` handle + /// allowing further interaction with it. + get-terminal-stdout: func() -> option; +} + +/// An interface providing an optional `terminal-output` for stderr as a +/// link-time authority. +interface terminal-stderr { + use terminal-output.{terminal-output}; + + /// If stderr is connected to a terminal, return a `terminal-output` handle + /// allowing further interaction with it. + get-terminal-stderr: func() -> option; +} + +world imports { + import environment; + import exit; + import wasi:io/error@0.2.0; + import wasi:io/poll@0.2.0; + import wasi:io/streams@0.2.0; + import stdin; + import stdout; + import stderr; + import terminal-input; + import terminal-output; + import terminal-stdin; + import terminal-stdout; + import terminal-stderr; + import wasi:clocks/monotonic-clock@0.2.0; + import wasi:clocks/wall-clock@0.2.0; + import wasi:filesystem/types@0.2.0; + import wasi:filesystem/preopens@0.2.0; + import wasi:sockets/network@0.2.0; + import wasi:sockets/instance-network@0.2.0; + import wasi:sockets/udp@0.2.0; + import wasi:sockets/udp-create-socket@0.2.0; + import wasi:sockets/tcp@0.2.0; + import wasi:sockets/tcp-create-socket@0.2.0; + import wasi:sockets/ip-name-lookup@0.2.0; + import wasi:random/random@0.2.0; + import wasi:random/insecure@0.2.0; + import wasi:random/insecure-seed@0.2.0; +} +world command { + import environment; + import exit; + import wasi:io/error@0.2.0; + import wasi:io/poll@0.2.0; + import wasi:io/streams@0.2.0; + import stdin; + import stdout; + import stderr; + import terminal-input; + import terminal-output; + import terminal-stdin; + import terminal-stdout; + import terminal-stderr; + import wasi:clocks/monotonic-clock@0.2.0; + import wasi:clocks/wall-clock@0.2.0; + import wasi:filesystem/types@0.2.0; + import wasi:filesystem/preopens@0.2.0; + import wasi:sockets/network@0.2.0; + import wasi:sockets/instance-network@0.2.0; + import wasi:sockets/udp@0.2.0; + import wasi:sockets/udp-create-socket@0.2.0; + import wasi:sockets/tcp@0.2.0; + import wasi:sockets/tcp-create-socket@0.2.0; + import wasi:sockets/ip-name-lookup@0.2.0; + import wasi:random/random@0.2.0; + import wasi:random/insecure@0.2.0; + import wasi:random/insecure-seed@0.2.0; + + export run; +} diff --git a/crates/wash/tests/plugins/hello_plugin/wit/deps/wasi-clocks-0.2.0/package.wit b/crates/wash/tests/plugins/hello_plugin/wit/deps/wasi-clocks-0.2.0/package.wit new file mode 100644 index 0000000000..9e0ba3dca2 --- /dev/null +++ b/crates/wash/tests/plugins/hello_plugin/wit/deps/wasi-clocks-0.2.0/package.wit @@ -0,0 +1,29 @@ +package wasi:clocks@0.2.0; + +interface monotonic-clock { + use wasi:io/poll@0.2.0.{pollable}; + + type instant = u64; + + type duration = u64; + + now: func() -> instant; + + resolution: func() -> duration; + + subscribe-instant: func(when: instant) -> pollable; + + subscribe-duration: func(when: duration) -> pollable; +} + +interface wall-clock { + record datetime { + seconds: u64, + nanoseconds: u32, + } + + now: func() -> datetime; + + resolution: func() -> datetime; +} + diff --git a/crates/wash/tests/plugins/hello_plugin/wit/deps/wasi-filesystem-0.2.0/package.wit b/crates/wash/tests/plugins/hello_plugin/wit/deps/wasi-filesystem-0.2.0/package.wit new file mode 100644 index 0000000000..cb6a2beb83 --- /dev/null +++ b/crates/wash/tests/plugins/hello_plugin/wit/deps/wasi-filesystem-0.2.0/package.wit @@ -0,0 +1,158 @@ +package wasi:filesystem@0.2.0; + +interface types { + use wasi:io/streams@0.2.0.{input-stream, output-stream, error}; + use wasi:clocks/wall-clock@0.2.0.{datetime}; + + type filesize = u64; + + enum descriptor-type { + unknown, + block-device, + character-device, + directory, + fifo, + symbolic-link, + regular-file, + socket, + } + + flags descriptor-flags { + read, + write, + file-integrity-sync, + data-integrity-sync, + requested-write-sync, + mutate-directory, + } + + flags path-flags { + symlink-follow, + } + + flags open-flags { + create, + directory, + exclusive, + truncate, + } + + type link-count = u64; + + record descriptor-stat { + %type: descriptor-type, + link-count: link-count, + size: filesize, + data-access-timestamp: option, + data-modification-timestamp: option, + status-change-timestamp: option, + } + + variant new-timestamp { + no-change, + now, + timestamp(datetime), + } + + record directory-entry { + %type: descriptor-type, + name: string, + } + + enum error-code { + access, + would-block, + already, + bad-descriptor, + busy, + deadlock, + quota, + exist, + file-too-large, + illegal-byte-sequence, + in-progress, + interrupted, + invalid, + io, + is-directory, + loop, + too-many-links, + message-size, + name-too-long, + no-device, + no-entry, + no-lock, + insufficient-memory, + insufficient-space, + not-directory, + not-empty, + not-recoverable, + unsupported, + no-tty, + no-such-device, + overflow, + not-permitted, + pipe, + read-only, + invalid-seek, + text-file-busy, + cross-device, + } + + enum advice { + normal, + sequential, + random, + will-need, + dont-need, + no-reuse, + } + + record metadata-hash-value { + lower: u64, + upper: u64, + } + + resource descriptor { + read-via-stream: func(offset: filesize) -> result; + write-via-stream: func(offset: filesize) -> result; + append-via-stream: func() -> result; + advise: func(offset: filesize, length: filesize, advice: advice) -> result<_, error-code>; + sync-data: func() -> result<_, error-code>; + get-flags: func() -> result; + get-type: func() -> result; + set-size: func(size: filesize) -> result<_, error-code>; + set-times: func(data-access-timestamp: new-timestamp, data-modification-timestamp: new-timestamp) -> result<_, error-code>; + read: func(length: filesize, offset: filesize) -> result, bool>, error-code>; + write: func(buffer: list, offset: filesize) -> result; + read-directory: func() -> result; + sync: func() -> result<_, error-code>; + create-directory-at: func(path: string) -> result<_, error-code>; + stat: func() -> result; + stat-at: func(path-flags: path-flags, path: string) -> result; + set-times-at: func(path-flags: path-flags, path: string, data-access-timestamp: new-timestamp, data-modification-timestamp: new-timestamp) -> result<_, error-code>; + link-at: func(old-path-flags: path-flags, old-path: string, new-descriptor: borrow, new-path: string) -> result<_, error-code>; + open-at: func(path-flags: path-flags, path: string, open-flags: open-flags, %flags: descriptor-flags) -> result; + readlink-at: func(path: string) -> result; + remove-directory-at: func(path: string) -> result<_, error-code>; + rename-at: func(old-path: string, new-descriptor: borrow, new-path: string) -> result<_, error-code>; + symlink-at: func(old-path: string, new-path: string) -> result<_, error-code>; + unlink-file-at: func(path: string) -> result<_, error-code>; + is-same-object: func(other: borrow) -> bool; + metadata-hash: func() -> result; + metadata-hash-at: func(path-flags: path-flags, path: string) -> result; + } + + resource directory-entry-stream { + read-directory-entry: func() -> result, error-code>; + } + + filesystem-error-code: func(err: borrow) -> option; +} + +interface preopens { + use types.{descriptor}; + + get-directories: func() -> list>; +} + diff --git a/crates/wash/tests/plugins/hello_plugin/wit/deps/wasi-http-0.2.0/package.wit b/crates/wash/tests/plugins/hello_plugin/wit/deps/wasi-http-0.2.0/package.wit new file mode 100644 index 0000000000..11f7ff44d6 --- /dev/null +++ b/crates/wash/tests/plugins/hello_plugin/wit/deps/wasi-http-0.2.0/package.wit @@ -0,0 +1,571 @@ +package wasi:http@0.2.0; + +/// This interface defines all of the types and methods for implementing +/// HTTP Requests and Responses, both incoming and outgoing, as well as +/// their headers, trailers, and bodies. +interface types { + use wasi:clocks/monotonic-clock@0.2.0.{duration}; + use wasi:io/streams@0.2.0.{input-stream, output-stream}; + use wasi:io/error@0.2.0.{error as io-error}; + use wasi:io/poll@0.2.0.{pollable}; + + /// This type corresponds to HTTP standard Methods. + variant method { + get, + head, + post, + put, + delete, + connect, + options, + trace, + patch, + other(string), + } + + /// This type corresponds to HTTP standard Related Schemes. + variant scheme { + HTTP, + HTTPS, + other(string), + } + + /// Defines the case payload type for `DNS-error` above: + record DNS-error-payload { + rcode: option, + info-code: option, + } + + /// Defines the case payload type for `TLS-alert-received` above: + record TLS-alert-received-payload { + alert-id: option, + alert-message: option, + } + + /// Defines the case payload type for `HTTP-response-{header,trailer}-size` above: + record field-size-payload { + field-name: option, + field-size: option, + } + + /// These cases are inspired by the IANA HTTP Proxy Error Types: + /// https://www.iana.org/assignments/http-proxy-status/http-proxy-status.xhtml#table-http-proxy-error-types + variant error-code { + DNS-timeout, + DNS-error(DNS-error-payload), + destination-not-found, + destination-unavailable, + destination-IP-prohibited, + destination-IP-unroutable, + connection-refused, + connection-terminated, + connection-timeout, + connection-read-timeout, + connection-write-timeout, + connection-limit-reached, + TLS-protocol-error, + TLS-certificate-error, + TLS-alert-received(TLS-alert-received-payload), + HTTP-request-denied, + HTTP-request-length-required, + HTTP-request-body-size(option), + HTTP-request-method-invalid, + HTTP-request-URI-invalid, + HTTP-request-URI-too-long, + HTTP-request-header-section-size(option), + HTTP-request-header-size(option), + HTTP-request-trailer-section-size(option), + HTTP-request-trailer-size(field-size-payload), + HTTP-response-incomplete, + HTTP-response-header-section-size(option), + HTTP-response-header-size(field-size-payload), + HTTP-response-body-size(option), + HTTP-response-trailer-section-size(option), + HTTP-response-trailer-size(field-size-payload), + HTTP-response-transfer-coding(option), + HTTP-response-content-coding(option), + HTTP-response-timeout, + HTTP-upgrade-failed, + HTTP-protocol-error, + loop-detected, + configuration-error, + /// This is a catch-all error for anything that doesn't fit cleanly into a + /// more specific case. It also includes an optional string for an + /// unstructured description of the error. Users should not depend on the + /// string for diagnosing errors, as it's not required to be consistent + /// between implementations. + internal-error(option), + } + + /// This type enumerates the different kinds of errors that may occur when + /// setting or appending to a `fields` resource. + variant header-error { + /// This error indicates that a `field-key` or `field-value` was + /// syntactically invalid when used with an operation that sets headers in a + /// `fields`. + invalid-syntax, + /// This error indicates that a forbidden `field-key` was used when trying + /// to set a header in a `fields`. + forbidden, + /// This error indicates that the operation on the `fields` was not + /// permitted because the fields are immutable. + immutable, + } + + /// Field keys are always strings. + type field-key = string; + + /// Field values should always be ASCII strings. However, in + /// reality, HTTP implementations often have to interpret malformed values, + /// so they are provided as a list of bytes. + type field-value = list; + + /// This following block defines the `fields` resource which corresponds to + /// HTTP standard Fields. Fields are a common representation used for both + /// Headers and Trailers. + /// + /// A `fields` may be mutable or immutable. A `fields` created using the + /// constructor, `from-list`, or `clone` will be mutable, but a `fields` + /// resource given by other means (including, but not limited to, + /// `incoming-request.headers`, `outgoing-request.headers`) might be be + /// immutable. In an immutable fields, the `set`, `append`, and `delete` + /// operations will fail with `header-error.immutable`. + resource fields { + /// Construct an empty HTTP Fields. + /// + /// The resulting `fields` is mutable. + constructor(); + /// Construct an HTTP Fields. + /// + /// The resulting `fields` is mutable. + /// + /// The list represents each key-value pair in the Fields. Keys + /// which have multiple values are represented by multiple entries in this + /// list with the same key. + /// + /// The tuple is a pair of the field key, represented as a string, and + /// Value, represented as a list of bytes. In a valid Fields, all keys + /// and values are valid UTF-8 strings. However, values are not always + /// well-formed, so they are represented as a raw list of bytes. + /// + /// An error result will be returned if any header or value was + /// syntactically invalid, or if a header was forbidden. + from-list: static func(entries: list>) -> result; + /// Get all of the values corresponding to a key. If the key is not present + /// in this `fields`, an empty list is returned. However, if the key is + /// present but empty, this is represented by a list with one or more + /// empty field-values present. + get: func(name: field-key) -> list; + /// Returns `true` when the key is present in this `fields`. If the key is + /// syntactically invalid, `false` is returned. + has: func(name: field-key) -> bool; + /// Set all of the values for a key. Clears any existing values for that + /// key, if they have been set. + /// + /// Fails with `header-error.immutable` if the `fields` are immutable. + set: func(name: field-key, value: list) -> result<_, header-error>; + /// Delete all values for a key. Does nothing if no values for the key + /// exist. + /// + /// Fails with `header-error.immutable` if the `fields` are immutable. + delete: func(name: field-key) -> result<_, header-error>; + /// Append a value for a key. Does not change or delete any existing + /// values for that key. + /// + /// Fails with `header-error.immutable` if the `fields` are immutable. + append: func(name: field-key, value: field-value) -> result<_, header-error>; + /// Retrieve the full set of keys and values in the Fields. Like the + /// constructor, the list represents each key-value pair. + /// + /// The outer list represents each key-value pair in the Fields. Keys + /// which have multiple values are represented by multiple entries in this + /// list with the same key. + entries: func() -> list>; + /// Make a deep copy of the Fields. Equivelant in behavior to calling the + /// `fields` constructor on the return value of `entries`. The resulting + /// `fields` is mutable. + clone: func() -> fields; + } + + /// Headers is an alias for Fields. + type headers = fields; + + /// Trailers is an alias for Fields. + type trailers = fields; + + /// Represents an incoming HTTP Request. + resource incoming-request { + /// Returns the method of the incoming request. + method: func() -> method; + /// Returns the path with query parameters from the request, as a string. + path-with-query: func() -> option; + /// Returns the protocol scheme from the request. + scheme: func() -> option; + /// Returns the authority from the request, if it was present. + authority: func() -> option; + /// Get the `headers` associated with the request. + /// + /// The returned `headers` resource is immutable: `set`, `append`, and + /// `delete` operations will fail with `header-error.immutable`. + /// + /// The `headers` returned are a child resource: it must be dropped before + /// the parent `incoming-request` is dropped. Dropping this + /// `incoming-request` before all children are dropped will trap. + headers: func() -> headers; + /// Gives the `incoming-body` associated with this request. Will only + /// return success at most once, and subsequent calls will return error. + consume: func() -> result; + } + + /// Represents an outgoing HTTP Request. + resource outgoing-request { + /// Construct a new `outgoing-request` with a default `method` of `GET`, and + /// `none` values for `path-with-query`, `scheme`, and `authority`. + /// + /// * `headers` is the HTTP Headers for the Request. + /// + /// It is possible to construct, or manipulate with the accessor functions + /// below, an `outgoing-request` with an invalid combination of `scheme` + /// and `authority`, or `headers` which are not permitted to be sent. + /// It is the obligation of the `outgoing-handler.handle` implementation + /// to reject invalid constructions of `outgoing-request`. + constructor(headers: headers); + /// Returns the resource corresponding to the outgoing Body for this + /// Request. + /// + /// Returns success on the first call: the `outgoing-body` resource for + /// this `outgoing-request` can be retrieved at most once. Subsequent + /// calls will return error. + body: func() -> result; + /// Get the Method for the Request. + method: func() -> method; + /// Set the Method for the Request. Fails if the string present in a + /// `method.other` argument is not a syntactically valid method. + set-method: func(method: method) -> result; + /// Get the combination of the HTTP Path and Query for the Request. + /// When `none`, this represents an empty Path and empty Query. + path-with-query: func() -> option; + /// Set the combination of the HTTP Path and Query for the Request. + /// When `none`, this represents an empty Path and empty Query. Fails is the + /// string given is not a syntactically valid path and query uri component. + set-path-with-query: func(path-with-query: option) -> result; + /// Get the HTTP Related Scheme for the Request. When `none`, the + /// implementation may choose an appropriate default scheme. + scheme: func() -> option; + /// Set the HTTP Related Scheme for the Request. When `none`, the + /// implementation may choose an appropriate default scheme. Fails if the + /// string given is not a syntactically valid uri scheme. + set-scheme: func(scheme: option) -> result; + /// Get the HTTP Authority for the Request. A value of `none` may be used + /// with Related Schemes which do not require an Authority. The HTTP and + /// HTTPS schemes always require an authority. + authority: func() -> option; + /// Set the HTTP Authority for the Request. A value of `none` may be used + /// with Related Schemes which do not require an Authority. The HTTP and + /// HTTPS schemes always require an authority. Fails if the string given is + /// not a syntactically valid uri authority. + set-authority: func(authority: option) -> result; + /// Get the headers associated with the Request. + /// + /// The returned `headers` resource is immutable: `set`, `append`, and + /// `delete` operations will fail with `header-error.immutable`. + /// + /// This headers resource is a child: it must be dropped before the parent + /// `outgoing-request` is dropped, or its ownership is transfered to + /// another component by e.g. `outgoing-handler.handle`. + headers: func() -> headers; + } + + /// Parameters for making an HTTP Request. Each of these parameters is + /// currently an optional timeout applicable to the transport layer of the + /// HTTP protocol. + /// + /// These timeouts are separate from any the user may use to bound a + /// blocking call to `wasi:io/poll.poll`. + resource request-options { + /// Construct a default `request-options` value. + constructor(); + /// The timeout for the initial connect to the HTTP Server. + connect-timeout: func() -> option; + /// Set the timeout for the initial connect to the HTTP Server. An error + /// return value indicates that this timeout is not supported. + set-connect-timeout: func(duration: option) -> result; + /// The timeout for receiving the first byte of the Response body. + first-byte-timeout: func() -> option; + /// Set the timeout for receiving the first byte of the Response body. An + /// error return value indicates that this timeout is not supported. + set-first-byte-timeout: func(duration: option) -> result; + /// The timeout for receiving subsequent chunks of bytes in the Response + /// body stream. + between-bytes-timeout: func() -> option; + /// Set the timeout for receiving subsequent chunks of bytes in the Response + /// body stream. An error return value indicates that this timeout is not + /// supported. + set-between-bytes-timeout: func(duration: option) -> result; + } + + /// Represents the ability to send an HTTP Response. + /// + /// This resource is used by the `wasi:http/incoming-handler` interface to + /// allow a Response to be sent corresponding to the Request provided as the + /// other argument to `incoming-handler.handle`. + resource response-outparam { + /// Set the value of the `response-outparam` to either send a response, + /// or indicate an error. + /// + /// This method consumes the `response-outparam` to ensure that it is + /// called at most once. If it is never called, the implementation + /// will respond with an error. + /// + /// The user may provide an `error` to `response` to allow the + /// implementation determine how to respond with an HTTP error response. + set: static func(param: response-outparam, response: result); + } + + /// This type corresponds to the HTTP standard Status Code. + type status-code = u16; + + /// Represents an incoming HTTP Response. + resource incoming-response { + /// Returns the status code from the incoming response. + status: func() -> status-code; + /// Returns the headers from the incoming response. + /// + /// The returned `headers` resource is immutable: `set`, `append`, and + /// `delete` operations will fail with `header-error.immutable`. + /// + /// This headers resource is a child: it must be dropped before the parent + /// `incoming-response` is dropped. + headers: func() -> headers; + /// Returns the incoming body. May be called at most once. Returns error + /// if called additional times. + consume: func() -> result; + } + + /// Represents an incoming HTTP Request or Response's Body. + /// + /// A body has both its contents - a stream of bytes - and a (possibly + /// empty) set of trailers, indicating that the full contents of the + /// body have been received. This resource represents the contents as + /// an `input-stream` and the delivery of trailers as a `future-trailers`, + /// and ensures that the user of this interface may only be consuming either + /// the body contents or waiting on trailers at any given time. + resource incoming-body { + /// Returns the contents of the body, as a stream of bytes. + /// + /// Returns success on first call: the stream representing the contents + /// can be retrieved at most once. Subsequent calls will return error. + /// + /// The returned `input-stream` resource is a child: it must be dropped + /// before the parent `incoming-body` is dropped, or consumed by + /// `incoming-body.finish`. + /// + /// This invariant ensures that the implementation can determine whether + /// the user is consuming the contents of the body, waiting on the + /// `future-trailers` to be ready, or neither. This allows for network + /// backpressure is to be applied when the user is consuming the body, + /// and for that backpressure to not inhibit delivery of the trailers if + /// the user does not read the entire body. + %stream: func() -> result; + /// Takes ownership of `incoming-body`, and returns a `future-trailers`. + /// This function will trap if the `input-stream` child is still alive. + finish: static func(this: incoming-body) -> future-trailers; + } + + /// Represents a future which may eventaully return trailers, or an error. + /// + /// In the case that the incoming HTTP Request or Response did not have any + /// trailers, this future will resolve to the empty set of trailers once the + /// complete Request or Response body has been received. + resource future-trailers { + /// Returns a pollable which becomes ready when either the trailers have + /// been received, or an error has occured. When this pollable is ready, + /// the `get` method will return `some`. + subscribe: func() -> pollable; + /// Returns the contents of the trailers, or an error which occured, + /// once the future is ready. + /// + /// The outer `option` represents future readiness. Users can wait on this + /// `option` to become `some` using the `subscribe` method. + /// + /// The outer `result` is used to retrieve the trailers or error at most + /// once. It will be success on the first call in which the outer option + /// is `some`, and error on subsequent calls. + /// + /// The inner `result` represents that either the HTTP Request or Response + /// body, as well as any trailers, were received successfully, or that an + /// error occured receiving them. The optional `trailers` indicates whether + /// or not trailers were present in the body. + /// + /// When some `trailers` are returned by this method, the `trailers` + /// resource is immutable, and a child. Use of the `set`, `append`, or + /// `delete` methods will return an error, and the resource must be + /// dropped before the parent `future-trailers` is dropped. + get: func() -> option, error-code>>>; + } + + /// Represents an outgoing HTTP Response. + resource outgoing-response { + /// Construct an `outgoing-response`, with a default `status-code` of `200`. + /// If a different `status-code` is needed, it must be set via the + /// `set-status-code` method. + /// + /// * `headers` is the HTTP Headers for the Response. + constructor(headers: headers); + /// Get the HTTP Status Code for the Response. + status-code: func() -> status-code; + /// Set the HTTP Status Code for the Response. Fails if the status-code + /// given is not a valid http status code. + set-status-code: func(status-code: status-code) -> result; + /// Get the headers associated with the Request. + /// + /// The returned `headers` resource is immutable: `set`, `append`, and + /// `delete` operations will fail with `header-error.immutable`. + /// + /// This headers resource is a child: it must be dropped before the parent + /// `outgoing-request` is dropped, or its ownership is transfered to + /// another component by e.g. `outgoing-handler.handle`. + headers: func() -> headers; + /// Returns the resource corresponding to the outgoing Body for this Response. + /// + /// Returns success on the first call: the `outgoing-body` resource for + /// this `outgoing-response` can be retrieved at most once. Subsequent + /// calls will return error. + body: func() -> result; + } + + /// Represents an outgoing HTTP Request or Response's Body. + /// + /// A body has both its contents - a stream of bytes - and a (possibly + /// empty) set of trailers, inducating the full contents of the body + /// have been sent. This resource represents the contents as an + /// `output-stream` child resource, and the completion of the body (with + /// optional trailers) with a static function that consumes the + /// `outgoing-body` resource, and ensures that the user of this interface + /// may not write to the body contents after the body has been finished. + /// + /// If the user code drops this resource, as opposed to calling the static + /// method `finish`, the implementation should treat the body as incomplete, + /// and that an error has occured. The implementation should propogate this + /// error to the HTTP protocol by whatever means it has available, + /// including: corrupting the body on the wire, aborting the associated + /// Request, or sending a late status code for the Response. + resource outgoing-body { + /// Returns a stream for writing the body contents. + /// + /// The returned `output-stream` is a child resource: it must be dropped + /// before the parent `outgoing-body` resource is dropped (or finished), + /// otherwise the `outgoing-body` drop or `finish` will trap. + /// + /// Returns success on the first call: the `output-stream` resource for + /// this `outgoing-body` may be retrieved at most once. Subsequent calls + /// will return error. + write: func() -> result; + /// Finalize an outgoing body, optionally providing trailers. This must be + /// called to signal that the response is complete. If the `outgoing-body` + /// is dropped without calling `outgoing-body.finalize`, the implementation + /// should treat the body as corrupted. + /// + /// Fails if the body's `outgoing-request` or `outgoing-response` was + /// constructed with a Content-Length header, and the contents written + /// to the body (via `write`) does not match the value given in the + /// Content-Length. + finish: static func(this: outgoing-body, trailers: option) -> result<_, error-code>; + } + + /// Represents a future which may eventaully return an incoming HTTP + /// Response, or an error. + /// + /// This resource is returned by the `wasi:http/outgoing-handler` interface to + /// provide the HTTP Response corresponding to the sent Request. + resource future-incoming-response { + /// Returns a pollable which becomes ready when either the Response has + /// been received, or an error has occured. When this pollable is ready, + /// the `get` method will return `some`. + subscribe: func() -> pollable; + /// Returns the incoming HTTP Response, or an error, once one is ready. + /// + /// The outer `option` represents future readiness. Users can wait on this + /// `option` to become `some` using the `subscribe` method. + /// + /// The outer `result` is used to retrieve the response or error at most + /// once. It will be success on the first call in which the outer option + /// is `some`, and error on subsequent calls. + /// + /// The inner `result` represents that either the incoming HTTP Response + /// status and headers have recieved successfully, or that an error + /// occured. Errors may also occur while consuming the response body, + /// but those will be reported by the `incoming-body` and its + /// `output-stream` child. + get: func() -> option>>; + } + + /// Attempts to extract a http-related `error` from the wasi:io `error` + /// provided. + /// + /// Stream operations which return + /// `wasi:io/stream/stream-error::last-operation-failed` have a payload of + /// type `wasi:io/error/error` with more information about the operation + /// that failed. This payload can be passed through to this function to see + /// if there's http-related information about the error to return. + /// + /// Note that this function is fallible because not all io-errors are + /// http-related errors. + http-error-code: func(err: borrow) -> option; +} + +/// This interface defines a handler of incoming HTTP Requests. It should +/// be exported by components which can respond to HTTP Requests. +interface incoming-handler { + use types.{incoming-request, response-outparam}; + + /// This function is invoked with an incoming HTTP Request, and a resource + /// `response-outparam` which provides the capability to reply with an HTTP + /// Response. The response is sent by calling the `response-outparam.set` + /// method, which allows execution to continue after the response has been + /// sent. This enables both streaming to the response body, and performing other + /// work. + /// + /// The implementor of this function must write a response to the + /// `response-outparam` before returning, or else the caller will respond + /// with an error on its behalf. + handle: func(request: incoming-request, response-out: response-outparam); +} + +/// This interface defines a handler of outgoing HTTP Requests. It should be +/// imported by components which wish to make HTTP Requests. +interface outgoing-handler { + use types.{outgoing-request, request-options, future-incoming-response, error-code}; + + /// This function is invoked with an outgoing HTTP Request, and it returns + /// a resource `future-incoming-response` which represents an HTTP Response + /// which may arrive in the future. + /// + /// The `options` argument accepts optional parameters for the HTTP + /// protocol's transport layer. + /// + /// This function may return an error if the `outgoing-request` is invalid + /// or not allowed to be made. Otherwise, protocol errors are reported + /// through the `future-incoming-response`. + handle: func(request: outgoing-request, options: option) -> result; +} + +/// The `wasi:http/proxy` world captures a widely-implementable intersection of +/// hosts that includes HTTP forward and reverse proxies. Components targeting +/// this world may concurrently stream in and out any number of incoming and +/// outgoing HTTP requests. +world proxy { + import wasi:random/random@0.2.0; + import wasi:io/error@0.2.0; + import wasi:io/poll@0.2.0; + import wasi:io/streams@0.2.0; + import wasi:cli/stdout@0.2.0; + import wasi:cli/stderr@0.2.0; + import wasi:cli/stdin@0.2.0; + import wasi:clocks/monotonic-clock@0.2.0; + import types; + import outgoing-handler; + import wasi:clocks/wall-clock@0.2.0; + + export incoming-handler; +} diff --git a/crates/wash/tests/plugins/hello_plugin/wit/deps/wasi-io-0.2.0/package.wit b/crates/wash/tests/plugins/hello_plugin/wit/deps/wasi-io-0.2.0/package.wit new file mode 100644 index 0000000000..184002998e --- /dev/null +++ b/crates/wash/tests/plugins/hello_plugin/wit/deps/wasi-io-0.2.0/package.wit @@ -0,0 +1,48 @@ +package wasi:io@0.2.0; + +interface error { + resource error { + to-debug-string: func() -> string; + } +} + +interface poll { + resource pollable { + ready: func() -> bool; + block: func(); + } + + poll: func(in: list>) -> list; +} + +interface streams { + use error.{error}; + use poll.{pollable}; + + variant stream-error { + last-operation-failed(error), + closed, + } + + resource input-stream { + read: func(len: u64) -> result, stream-error>; + blocking-read: func(len: u64) -> result, stream-error>; + skip: func(len: u64) -> result; + blocking-skip: func(len: u64) -> result; + subscribe: func() -> pollable; + } + + resource output-stream { + check-write: func() -> result; + write: func(contents: list) -> result<_, stream-error>; + blocking-write-and-flush: func(contents: list) -> result<_, stream-error>; + flush: func() -> result<_, stream-error>; + blocking-flush: func() -> result<_, stream-error>; + subscribe: func() -> pollable; + write-zeroes: func(len: u64) -> result<_, stream-error>; + blocking-write-zeroes-and-flush: func(len: u64) -> result<_, stream-error>; + splice: func(src: borrow, len: u64) -> result; + blocking-splice: func(src: borrow, len: u64) -> result; + } +} + diff --git a/crates/wash/tests/plugins/hello_plugin/wit/deps/wasi-random-0.2.0/package.wit b/crates/wash/tests/plugins/hello_plugin/wit/deps/wasi-random-0.2.0/package.wit new file mode 100644 index 0000000000..58c179e1b3 --- /dev/null +++ b/crates/wash/tests/plugins/hello_plugin/wit/deps/wasi-random-0.2.0/package.wit @@ -0,0 +1,18 @@ +package wasi:random@0.2.0; + +interface random { + get-random-bytes: func(len: u64) -> list; + + get-random-u64: func() -> u64; +} + +interface insecure { + get-insecure-random-bytes: func(len: u64) -> list; + + get-insecure-random-u64: func() -> u64; +} + +interface insecure-seed { + insecure-seed: func() -> tuple; +} + diff --git a/crates/wash/tests/plugins/hello_plugin/wit/deps/wasi-sockets-0.2.0/package.wit b/crates/wash/tests/plugins/hello_plugin/wit/deps/wasi-sockets-0.2.0/package.wit new file mode 100644 index 0000000000..0602b855d8 --- /dev/null +++ b/crates/wash/tests/plugins/hello_plugin/wit/deps/wasi-sockets-0.2.0/package.wit @@ -0,0 +1,179 @@ +package wasi:sockets@0.2.0; + +interface network { + resource network; + + enum error-code { + unknown, + access-denied, + not-supported, + invalid-argument, + out-of-memory, + timeout, + concurrency-conflict, + not-in-progress, + would-block, + invalid-state, + new-socket-limit, + address-not-bindable, + address-in-use, + remote-unreachable, + connection-refused, + connection-reset, + connection-aborted, + datagram-too-large, + name-unresolvable, + temporary-resolver-failure, + permanent-resolver-failure, + } + + enum ip-address-family { + ipv4, + ipv6, + } + + type ipv4-address = tuple; + + type ipv6-address = tuple; + + variant ip-address { + ipv4(ipv4-address), + ipv6(ipv6-address), + } + + record ipv4-socket-address { + port: u16, + address: ipv4-address, + } + + record ipv6-socket-address { + port: u16, + flow-info: u32, + address: ipv6-address, + scope-id: u32, + } + + variant ip-socket-address { + ipv4(ipv4-socket-address), + ipv6(ipv6-socket-address), + } +} + +interface instance-network { + use network.{network}; + + instance-network: func() -> network; +} + +interface udp { + use wasi:io/poll@0.2.0.{pollable}; + use network.{network, error-code, ip-socket-address, ip-address-family}; + + record incoming-datagram { + data: list, + remote-address: ip-socket-address, + } + + record outgoing-datagram { + data: list, + remote-address: option, + } + + resource udp-socket { + start-bind: func(network: borrow, local-address: ip-socket-address) -> result<_, error-code>; + finish-bind: func() -> result<_, error-code>; + %stream: func(remote-address: option) -> result, error-code>; + local-address: func() -> result; + remote-address: func() -> result; + address-family: func() -> ip-address-family; + unicast-hop-limit: func() -> result; + set-unicast-hop-limit: func(value: u8) -> result<_, error-code>; + receive-buffer-size: func() -> result; + set-receive-buffer-size: func(value: u64) -> result<_, error-code>; + send-buffer-size: func() -> result; + set-send-buffer-size: func(value: u64) -> result<_, error-code>; + subscribe: func() -> pollable; + } + + resource incoming-datagram-stream { + receive: func(max-results: u64) -> result, error-code>; + subscribe: func() -> pollable; + } + + resource outgoing-datagram-stream { + check-send: func() -> result; + send: func(datagrams: list) -> result; + subscribe: func() -> pollable; + } +} + +interface udp-create-socket { + use network.{network, error-code, ip-address-family}; + use udp.{udp-socket}; + + create-udp-socket: func(address-family: ip-address-family) -> result; +} + +interface tcp { + use wasi:io/streams@0.2.0.{input-stream, output-stream}; + use wasi:io/poll@0.2.0.{pollable}; + use wasi:clocks/monotonic-clock@0.2.0.{duration}; + use network.{network, error-code, ip-socket-address, ip-address-family}; + + enum shutdown-type { + receive, + send, + both, + } + + resource tcp-socket { + start-bind: func(network: borrow, local-address: ip-socket-address) -> result<_, error-code>; + finish-bind: func() -> result<_, error-code>; + start-connect: func(network: borrow, remote-address: ip-socket-address) -> result<_, error-code>; + finish-connect: func() -> result, error-code>; + start-listen: func() -> result<_, error-code>; + finish-listen: func() -> result<_, error-code>; + accept: func() -> result, error-code>; + local-address: func() -> result; + remote-address: func() -> result; + is-listening: func() -> bool; + address-family: func() -> ip-address-family; + set-listen-backlog-size: func(value: u64) -> result<_, error-code>; + keep-alive-enabled: func() -> result; + set-keep-alive-enabled: func(value: bool) -> result<_, error-code>; + keep-alive-idle-time: func() -> result; + set-keep-alive-idle-time: func(value: duration) -> result<_, error-code>; + keep-alive-interval: func() -> result; + set-keep-alive-interval: func(value: duration) -> result<_, error-code>; + keep-alive-count: func() -> result; + set-keep-alive-count: func(value: u32) -> result<_, error-code>; + hop-limit: func() -> result; + set-hop-limit: func(value: u8) -> result<_, error-code>; + receive-buffer-size: func() -> result; + set-receive-buffer-size: func(value: u64) -> result<_, error-code>; + send-buffer-size: func() -> result; + set-send-buffer-size: func(value: u64) -> result<_, error-code>; + subscribe: func() -> pollable; + shutdown: func(shutdown-type: shutdown-type) -> result<_, error-code>; + } +} + +interface tcp-create-socket { + use network.{network, error-code, ip-address-family}; + use tcp.{tcp-socket}; + + create-tcp-socket: func(address-family: ip-address-family) -> result; +} + +interface ip-name-lookup { + use wasi:io/poll@0.2.0.{pollable}; + use network.{network, error-code, ip-address}; + + resource resolve-address-stream { + resolve-next-address: func() -> result, error-code>; + subscribe: func() -> pollable; + } + + resolve-addresses: func(network: borrow, name: string) -> result; +} + diff --git a/crates/wash/tests/plugins/hello_plugin/wit/deps/wasmcloud-wash-0.1.0/package.wit b/crates/wash/tests/plugins/hello_plugin/wit/deps/wasmcloud-wash-0.1.0/package.wit new file mode 100644 index 0000000000..1a12d8b515 --- /dev/null +++ b/crates/wash/tests/plugins/hello_plugin/wit/deps/wasmcloud-wash-0.1.0/package.wit @@ -0,0 +1,78 @@ +package wasmcloud:wash@0.1.0; + +/// The interface for a subcommand plugin. This is used to register plugins and to provide the host +/// with an interface it can use to interact with the plugin. +interface subcommand { + /// Information about an argument + record argument { + /// The description of the argument. Used for documentation in the CLI + description: string, + /// Whether or not the argument is a path. If the argument is a path, wash will load this + /// path (with access to only the file if it is a file path and access to the directory if + /// it is a directory path) and pass it as a preopened dir at the exact same path + is-path: bool, + /// Whether or not the argument is required. + required: bool, + } + + /// The metadata for a plugin used for registration and setup + record metadata { + /// The friendly name of the plugin + name: string, + /// The ID of the plugin. This must be unique across all plugins and is used as the name of + /// the subcommand added to wash. This ID should contain no whitespace + id: string, + /// The version of the plugin + version: string, + /// The author of the plugin + author: string, + /// The description of the plugin. This will be used as the top level help text for the plugin + description: string, + /// The list of flags and their documentation that can be used with this plugin. The key is + /// the name of the flag. + %flags: list>, + /// The list of positional arguments that can be used with this plugin. The key is the name + /// of the argument. + arguments: list>, + } + + /// The function to register a plugin. This is called by the host to register the plugin. + register: func() -> metadata; +} + +/// The world that subcommand plugins can consume and provide. Any plugin is invoked using the +/// `wasi:cli/run` function and is passed all relevant flags, arguments, and environment variables. +world subcommands { + import wasi:io/poll@0.2.0; + import wasi:clocks/monotonic-clock@0.2.0; + import wasi:io/error@0.2.0; + import wasi:io/streams@0.2.0; + import wasi:http/types@0.2.0; + import wasi:http/outgoing-handler@0.2.0; + import wasi:cli/environment@0.2.0; + import wasi:cli/exit@0.2.0; + import wasi:cli/stdin@0.2.0; + import wasi:cli/stdout@0.2.0; + import wasi:cli/stderr@0.2.0; + import wasi:cli/terminal-input@0.2.0; + import wasi:cli/terminal-output@0.2.0; + import wasi:cli/terminal-stdin@0.2.0; + import wasi:cli/terminal-stdout@0.2.0; + import wasi:cli/terminal-stderr@0.2.0; + import wasi:clocks/wall-clock@0.2.0; + import wasi:filesystem/types@0.2.0; + import wasi:filesystem/preopens@0.2.0; + import wasi:sockets/network@0.2.0; + import wasi:sockets/instance-network@0.2.0; + import wasi:sockets/udp@0.2.0; + import wasi:sockets/udp-create-socket@0.2.0; + import wasi:sockets/tcp@0.2.0; + import wasi:sockets/tcp-create-socket@0.2.0; + import wasi:sockets/ip-name-lookup@0.2.0; + import wasi:random/random@0.2.0; + import wasi:random/insecure@0.2.0; + import wasi:random/insecure-seed@0.2.0; + + export subcommand; + export wasi:cli/run@0.2.0; +} diff --git a/crates/wash/tests/plugins/hello_plugin/wit/world.wit b/crates/wash/tests/plugins/hello_plugin/wit/world.wit new file mode 100644 index 0000000000..64c2bb5b4d --- /dev/null +++ b/crates/wash/tests/plugins/hello_plugin/wit/world.wit @@ -0,0 +1,9 @@ +package wasmcloud:test; + +world plugin { + include wasi:cli/imports@0.2.0; + import wasi:http/outgoing-handler@0.2.0; + + export wasmcloud:wash/subcommand@0.1.0; + export wasi:cli/run@0.2.0; +} diff --git a/crates/wash/tests/start_nats.rs b/crates/wash/tests/start_nats.rs new file mode 100644 index 0000000000..c47763985a --- /dev/null +++ b/crates/wash/tests/start_nats.rs @@ -0,0 +1,104 @@ +use anyhow::Result; +use tempfile::tempdir; + +use wash_lib::common::CommandGroupUsage; +use wash_lib::start::{ensure_nats_server, start_nats_server, NatsConfig, NATS_SERVER_BINARY}; + +mod common; +use common::{find_open_port, NATS_SERVER_VERSION}; + +#[tokio::test] +#[cfg_attr(not(can_reach_github_com), ignore = "github.com is not reachable")] +async fn can_handle_missing_nats_version() -> Result<()> { + let install_dir = tempdir().expect("Couldn't create tempdir"); + + let res = ensure_nats_server("v300.22.1111223", &install_dir).await; + assert!(res.is_err()); + + Ok(()) +} + +#[tokio::test] +#[cfg_attr(not(can_reach_github_com), ignore = "github.com is not reachable")] +async fn can_download_and_start_nats() -> Result<()> { + let install_dir = tempdir().expect("Couldn't create tempdir"); + + let res = ensure_nats_server(NATS_SERVER_VERSION, &install_dir).await; + assert!(res.is_ok()); + + let log_path = install_dir.path().join("nats.log"); + let log_file = tokio::fs::File::create(&log_path).await?.into_std().await; + + let nats_port = find_open_port().await?; + let nats_ws_port = find_open_port().await?; + let mut config = NatsConfig::new_standalone("127.0.0.1", nats_port, None); + config.websocket_port = nats_ws_port; + let child_res = start_nats_server( + &install_dir.path().join(NATS_SERVER_BINARY), + log_file, + config, + CommandGroupUsage::UseParent, + ) + .await; + assert!(child_res.is_ok()); + + // Give NATS max 5 seconds to start up + for _ in 0..4 { + let log_contents = tokio::fs::read_to_string(&log_path).await?; + if log_contents.is_empty() { + println!("NATS server hasn't started up yet, waiting 1 second"); + tokio::time::sleep(std::time::Duration::from_millis(1000)).await; + } else { + // Give just a little bit of time for the startup logs to flow in + tokio::time::sleep(std::time::Duration::from_millis(1000)).await; + + assert!(log_contents.contains("Starting nats-server")); + assert!(log_contents.contains("Starting JetStream")); + assert!(log_contents.contains("Server is ready")); + break; + } + } + + child_res.unwrap().kill().await?; + Ok(()) +} + +#[tokio::test] +#[cfg_attr(not(can_reach_github_com), ignore = "github.com is not reachable")] +async fn can_gracefully_fail_running_nats() -> Result<()> { + let install_dir = tempdir().expect("Couldn't create tempdir"); + + let res = ensure_nats_server(NATS_SERVER_VERSION, &install_dir).await; + assert!(res.is_ok()); + + let nats_port = find_open_port().await?; + let nats_ws_port = find_open_port().await?; + let mut config = + NatsConfig::new_standalone("127.0.0.1", nats_port, Some("extender".to_string())); + config.websocket_port = nats_ws_port; + let nats_one = start_nats_server( + &install_dir.path().join(NATS_SERVER_BINARY), + std::process::Stdio::null(), + config.clone(), + CommandGroupUsage::UseParent, + ) + .await; + assert!(nats_one.is_ok()); + + // Give NATS a few seconds to start up and listen + tokio::time::sleep(std::time::Duration::from_millis(5000)).await; + let log_path = install_dir.path().join("nats.log"); + let log = std::fs::File::create(&log_path)?; + let nats_two = start_nats_server( + &install_dir.path().join(NATS_SERVER_BINARY), + log, + config, + CommandGroupUsage::UseParent, + ) + .await; + assert!(nats_two.is_err()); + + nats_one.unwrap().kill().await?; + + Ok(()) +} diff --git a/crates/wash/tests/start_wadm.rs b/crates/wash/tests/start_wadm.rs new file mode 100644 index 0000000000..5eccd5f0bc --- /dev/null +++ b/crates/wash/tests/start_wadm.rs @@ -0,0 +1,71 @@ +use wash_lib::common::CommandGroupUsage; +use wash_lib::start::{ensure_wadm, start_wadm, WadmConfig, WADM_BINARY, WADM_PID}; + +use anyhow::Result; +use tempfile::tempdir; + +const WADM_VERSION: &str = "v0.18.0"; + +#[tokio::test] +#[cfg_attr(not(can_reach_github_com), ignore = "github.com is not reachable")] +async fn can_handle_missing_wadm_version() -> Result<()> { + let install_dir = tempdir().expect("Unable to create tempdir"); + + let major: u8 = 123; + let minor: u8 = 52; + let patch: u8 = 222; + + let res = ensure_wadm(&format!("v{major}.{minor}.{patch}"), &install_dir).await; + assert!(res.is_err()); + + Ok(()) +} + +#[tokio::test] +#[cfg_attr(not(can_reach_github_com), ignore = "github.com is not reachable")] +async fn can_download_and_start_wadm() -> Result<()> { + let install_dir = tempdir().expect("Unable to create tempdir"); + + let res = ensure_wadm(WADM_VERSION, &install_dir).await; + assert!(res.is_ok()); + + let log_path = install_dir.path().join("wadm.log"); + let log_file = tokio::fs::File::create(&log_path).await?.into_std().await; + + let config = WadmConfig { + structured_logging: false, + js_domain: None, + nats_server_url: "nats://127.0.0.1:54321".to_string(), + nats_credsfile: None, + }; + + let child_res = start_wadm( + &install_dir, + &install_dir.path().join(WADM_BINARY), + log_file, + Some(config), + CommandGroupUsage::UseParent, + ) + .await; + assert!(child_res.is_ok()); + + // Wait for process to exit since NATS couldn't connect + assert!(child_res.unwrap().wait().await.is_ok()); + let log_contents = tokio::fs::read_to_string(&log_path).await?; + // wadm couldn't connect to NATS but that's okay + + // Assert that the pid file get created in the expected state_dir, + // which in this case is set to install_dir. + let pid_path = install_dir.path().join(WADM_PID); + assert!(tokio::fs::try_exists(pid_path).await?); + + // Different OS-es have different error codes, but all I care about is that wadm executed at all + #[cfg(target_os = "macos")] + assert!(log_contents.contains("Connection refused (os error 61)")); + #[cfg(target_os = "linux")] + assert!(log_contents.contains("Connection refused (os error 111)")); + #[cfg(target_os = "windows")] + assert!(log_contents.contains("No connection could be made because the target machine actively refused it. (os error 10061)")); + + Ok(()) +} diff --git a/crates/wash/tests/start_wasmcloud.rs b/crates/wash/tests/start_wasmcloud.rs new file mode 100644 index 0000000000..dd60a4f6c9 --- /dev/null +++ b/crates/wash/tests/start_wasmcloud.rs @@ -0,0 +1,167 @@ +use std::collections::HashMap; + +use anyhow::Context; +use tempfile::tempdir; +use tokio::time::Duration; + +use wash_lib::common::CommandGroupUsage; +use wash_lib::start::{ + ensure_nats_server, ensure_wasmcloud, ensure_wasmcloud_for_os_arch_pair, find_wasmcloud_binary, + start_nats_server, start_wasmcloud_host, NatsConfig, NATS_SERVER_BINARY, +}; + +mod common; +use common::{find_open_port, NATS_SERVER_VERSION}; + +const WASMCLOUD_VERSION: &str = "v1.4.2"; + +#[tokio::test] +#[cfg_attr(not(can_reach_github_com), ignore = "github.com is not reachable")] +async fn can_download_wasmcloud_host() { + let download_dir = tempdir().expect("Unable to create tempdir"); + let res = ensure_wasmcloud_for_os_arch_pair(WASMCLOUD_VERSION, &download_dir) + .await + .expect("Should be able to download tarball"); + + // Make sure we can find the binary and that it matches the path we got back from ensure + assert_eq!( + find_wasmcloud_binary(&download_dir, WASMCLOUD_VERSION) + .await + .expect("Should have found installed wasmcloud"), + res + ); + + // Just to triple check, make sure the paths actually exist + assert!( + download_dir.path().join(WASMCLOUD_VERSION).exists(), + "Directory should exist" + ); +} + +#[tokio::test] +#[cfg_attr(not(can_reach_github_com), ignore = "github.com is not reachable")] +async fn can_handle_missing_wasmcloud_version() { + let download_dir = tempdir().expect("Unable to create tempdir"); + let res = ensure_wasmcloud("v10233.123.3.4", &download_dir).await; + + assert!(res.is_err()); +} + +#[tokio::test] +#[cfg_attr(not(can_reach_github_com), ignore = "github.com is not reachable")] +async fn can_download_and_start_wasmcloud() -> anyhow::Result<()> { + let install_dir = tempdir().expect("Unable to create tempdir"); + + // Install and start NATS server for this test + let nats_port = find_open_port().await?; + let nats_ws_port = find_open_port().await?; + ensure_nats_server(NATS_SERVER_VERSION, &install_dir) + .await + .expect("Should be able to install NATS server"); + + let mut config = NatsConfig::new_standalone("127.0.0.1", nats_port, None); + config.websocket_port = nats_ws_port; + let mut nats_child = start_nats_server( + install_dir.path().join(NATS_SERVER_BINARY), + std::process::Stdio::null(), + config, + CommandGroupUsage::UseParent, + ) + .await + .expect("Unable to start nats process"); + + let wasmcloud_binary = ensure_wasmcloud(WASMCLOUD_VERSION, &install_dir) + .await + .expect("Unable to ensure wasmcloud"); + + let stderr_log_path = wasmcloud_binary + .parent() + .unwrap() + .parent() + .unwrap() + .join("wasmcloud_stderr.log"); + let stderr_log_file = tokio::fs::File::create(&stderr_log_path) + .await? + .into_std() + .await; + let stdout_log_path = wasmcloud_binary + .parent() + .unwrap() + .parent() + .unwrap() + .join("wasmcloud_stdout.log"); + let stdout_log_file = tokio::fs::File::create(&stdout_log_path) + .await? + .into_std() + .await; + + let mut host_env = HashMap::new(); + host_env.insert("WASMCLOUD_RPC_PORT".to_string(), nats_port.to_string()); + host_env.insert("WASMCLOUD_CTL_PORT".to_string(), nats_port.to_string()); + let mut host_child = start_wasmcloud_host( + &wasmcloud_binary, + stdout_log_file, + stderr_log_file, + host_env, + ) + .await + .expect("Unable to start wasmcloud host"); + + // Wait at most 10 seconds for wasmcloud to start + println!("waiting for wasmcloud to start.."); + let startup_log_path = stderr_log_path.clone(); + tokio::time::timeout(Duration::from_secs(10), async move { + loop { + match tokio::fs::read_to_string(&startup_log_path).await { + Ok(file_contents) if !file_contents.is_empty() => break, + _ => { + println!("wasmCloud hasn't started up yet, waiting 1 second"); + tokio::time::sleep(Duration::from_secs(1)).await; + } + } + } + }) + .await + .context("failed to start wasmcloud (log path is missing)")?; + + // Wait for up to 15 seconds for the logs to contain expected lines + println!("wasmCloud has started, waiting for expected startup logs..."); + let startup_log_path = stderr_log_path.clone(); + tokio::time::timeout(Duration::from_secs(15), async move { + loop { + match tokio::fs::read_to_string(&startup_log_path).await { + Ok(file_contents) => { + if file_contents.contains("wasmCloud host started") { + // After wasmcloud says it's ready, it still requires some seconds to start up. + tokio::time::sleep(Duration::from_secs(3)).await; + break; + } + } + _ => { + println!("no host startup logs in output yet, waiting 1 second"); + tokio::time::sleep(Duration::from_secs(1)).await; + } + } + } + }) + .await + .context("failed to start wasmcloud (logs did not contain expected content)")?; + + // We support multiple hosts, so this should work fine + let mut host_env = HashMap::new(); + host_env.insert("WASMCLOUD_RPC_PORT".to_string(), nats_port.to_string()); + host_env.insert("WASMCLOUD_CTL_PORT".to_string(), nats_port.to_string()); + let mut child_res = start_wasmcloud_host( + &wasmcloud_binary, + std::process::Stdio::null(), + std::process::Stdio::null(), + host_env, + ) + .await + .expect("Unable to start host"); + child_res.kill().await?; + + host_child.kill().await?; + nats_child.kill().await?; + Ok(()) +} diff --git a/crates/wash/tests/wash_app.rs b/crates/wash/tests/wash_app.rs index 68dd1fb97e..4d9f742d31 100644 --- a/crates/wash/tests/wash_app.rs +++ b/crates/wash/tests/wash_app.rs @@ -5,8 +5,8 @@ use anyhow::{Context, Result}; use serial_test::serial; use tokio::process::Command; use wadm_types::api::StatusType; -use wash_lib::cli::get::parse_watch_interval; -use wash_lib::cli::output::{AppDeployCommandOutput, AppValidateOutput}; +use crate::lib::cli::get::parse_watch_interval; +use crate::lib::cli::output::{AppDeployCommandOutput, AppValidateOutput}; mod common; use common::TestWashInstance; diff --git a/crates/wash/tests/wash_build.rs b/crates/wash/tests/wash_build.rs index 20ed571273..3bbf79fae7 100644 --- a/crates/wash/tests/wash_build.rs +++ b/crates/wash/tests/wash_build.rs @@ -4,7 +4,7 @@ use common::{init, init_path, init_provider, init_workspace, load_fixture}; use anyhow::{Context, Result}; use tokio::{fs::File, process::Command}; -use wash_lib::build::PACKAGE_LOCK_FILE_NAME; +use crate::lib::build::PACKAGE_LOCK_FILE_NAME; #[tokio::test] #[cfg_attr(not(can_reach_ghcr_io), ignore = "ghcr.io is not reachable")] diff --git a/crates/wash/tests/wash_call.rs b/crates/wash/tests/wash_call.rs index ebe79d3164..dc6954f753 100644 --- a/crates/wash/tests/wash_call.rs +++ b/crates/wash/tests/wash_call.rs @@ -4,7 +4,7 @@ use anyhow::{Context, Result}; use serial_test::serial; use tokio::task::JoinSet; -use wash_lib::cli::output::{CallCommandOutput, StartCommandOutput}; +use crate::lib::cli::output::{CallCommandOutput, StartCommandOutput}; mod common; use common::{TestWashInstance, FERRIS_SAYS_OCI_REF, HTTP_JSONIFY_OCI_REF}; diff --git a/crates/wash/tests/wash_config.rs b/crates/wash/tests/wash_config.rs index 994e2d529f..30d2b18663 100644 --- a/crates/wash/tests/wash_config.rs +++ b/crates/wash/tests/wash_config.rs @@ -5,7 +5,7 @@ use common::TestWashInstance; use std::collections::HashMap; use wash_cli::cmd::config::ConfigCliCommand; -use wash_lib::cli::{CliConnectionOpts, OutputKind}; +use crate::lib::cli::{CliConnectionOpts, OutputKind}; #[tokio::test] async fn test_config_put_and_get() -> anyhow::Result<()> { diff --git a/crates/wash/tests/wash_get.rs b/crates/wash/tests/wash_get.rs index 780179d37c..a30d05180c 100644 --- a/crates/wash/tests/wash_get.rs +++ b/crates/wash/tests/wash_get.rs @@ -5,7 +5,7 @@ use common::TestWashInstance; use anyhow::{bail, Context, Result}; use serial_test::serial; use tokio::process::Command; -use wash_lib::cli::output::{ +use crate::lib::cli::output::{ GetClaimsCommandOutput, GetHostInventoriesCommandOutput, GetHostsCommandOutput, LinkQueryCommandOutput, }; diff --git a/crates/wash/tests/wash_label.rs b/crates/wash/tests/wash_label.rs index 4053893128..611de48916 100644 --- a/crates/wash/tests/wash_label.rs +++ b/crates/wash/tests/wash_label.rs @@ -5,7 +5,7 @@ use common::TestWashInstance; use anyhow::{Context, Result}; use serial_test::serial; use tokio::process::Command; -use wash_lib::cli::output::LabelHostCommandOutput; +use crate::lib::cli::output::LabelHostCommandOutput; #[tokio::test] #[serial] diff --git a/crates/wash/tests/wash_link.rs b/crates/wash/tests/wash_link.rs index 1921259135..656cb6c206 100644 --- a/crates/wash/tests/wash_link.rs +++ b/crates/wash/tests/wash_link.rs @@ -5,7 +5,7 @@ use common::TestWashInstance; use anyhow::{Context, Result}; use serial_test::serial; use tokio::process::Command; -use wash_lib::cli::output::LinkQueryCommandOutput; +use crate::lib::cli::output::LinkQueryCommandOutput; #[tokio::test] #[serial] diff --git a/crates/wash/tests/wash_scale.rs b/crates/wash/tests/wash_scale.rs index 5d961ce15f..2f926e903b 100644 --- a/crates/wash/tests/wash_scale.rs +++ b/crates/wash/tests/wash_scale.rs @@ -5,7 +5,7 @@ use common::{TestWashInstance, HELLO_OCI_REF}; use anyhow::{Context, Result}; use serial_test::serial; use tokio::process::Command; -use wash_lib::cli::output::{GetHostInventoriesCommandOutput, ScaleCommandOutput}; +use crate::lib::cli::output::{GetHostInventoriesCommandOutput, ScaleCommandOutput}; #[tokio::test] #[serial] diff --git a/crates/wash/tests/wash_secrets.rs b/crates/wash/tests/wash_secrets.rs index afacae8ec8..f82512de8b 100644 --- a/crates/wash/tests/wash_secrets.rs +++ b/crates/wash/tests/wash_secrets.rs @@ -5,7 +5,7 @@ use std::collections::HashMap; use anyhow::{bail, Context as _}; use common::TestWashInstance; use wash_cli::secrets::SecretsCliCommand; -use wash_lib::cli::{CliConnectionOpts, OutputKind}; +use crate::lib::cli::{CliConnectionOpts, OutputKind}; use wasmcloud_secrets_types::{SECRET_POLICY_PROPERTIES_TYPE, SECRET_TYPE}; #[tokio::test] diff --git a/crates/wash/tests/wash_stop.rs b/crates/wash/tests/wash_stop.rs index c2697db831..147f796b88 100644 --- a/crates/wash/tests/wash_stop.rs +++ b/crates/wash/tests/wash_stop.rs @@ -4,7 +4,7 @@ use common::{wait_for_no_hosts, TestWashInstance, HELLO_OCI_REF, PROVIDER_HTTPSE use anyhow::{Context, Result}; use serial_test::serial; -use wash_lib::cli::output::StartCommandOutput; +use crate::lib::cli::output::StartCommandOutput; #[tokio::test] #[serial] diff --git a/crates/wash/tests/wash_up.rs b/crates/wash/tests/wash_up.rs index e6d9f7dd29..82953a24d7 100644 --- a/crates/wash/tests/wash_up.rs +++ b/crates/wash/tests/wash_up.rs @@ -339,8 +339,8 @@ async fn integration_up_works_with_specific_wasmcloud_host_version() -> Result<( #[tokio::test] #[serial] async fn integration_up_works_with_specified_wadm_version() -> Result<()> { - use wash_lib::config::{DOWNLOADS_DIR, WASH_DIR}; - use wash_lib::start::WADM_BINARY; + use crate::lib::config::{DOWNLOADS_DIR, WASH_DIR}; + use crate::lib::start::WADM_BINARY; // 0.12.0 is a sufficient version to test the latest is 0.12.2 let previous_wadm_version = "v0.12.0"; let home = etcetera::home_dir().context("no home directory found. Please set $HOME")?; diff --git a/crates/wash/tests/wash_update.rs b/crates/wash/tests/wash_update.rs index 316b5ba9d0..fb22bd8191 100644 --- a/crates/wash/tests/wash_update.rs +++ b/crates/wash/tests/wash_update.rs @@ -2,10 +2,10 @@ mod common; use common::{TestWashInstance, HELLO_OCI_REF}; +use crate::lib::cli::output::{GetHostInventoriesCommandOutput, StartCommandOutput}; use anyhow::{Context, Result}; use serial_test::serial; use tokio::process::Command; -use wash_lib::cli::output::{GetHostInventoriesCommandOutput, StartCommandOutput}; const OLD_HELLO_OCI_REF: &str = "ghcr.io/brooksmtownsend/http-hello-world-rust:0.1.0";