diff --git a/examples/greet-component/Cargo.lock b/examples/greet-component/Cargo.lock new file mode 100644 index 00000000..20925d93 --- /dev/null +++ b/examples/greet-component/Cargo.lock @@ -0,0 +1,442 @@ +# This file is automatically @generated by Cargo. +# It is not intended for manual editing. +version = 4 + +[[package]] +name = "anyhow" +version = "1.0.102" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "7f202df86484c868dbad7eaa557ef785d5c66295e41b460ef922eca0723b842c" + +[[package]] +name = "bitflags" +version = "2.11.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "843867be96c8daad0d758b57df9392b6d8d271134fce549de6ce169ff98a92af" + +[[package]] +name = "equivalent" +version = "1.0.2" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "877a4ace8713b0bcf2a4e7eec82529c029f1d0619886d18145fea96c3ffe5c0f" + +[[package]] +name = "foldhash" +version = "0.1.5" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "d9c4f5dac5e15c24eb999c26181a6ca40b39fe946cbe4c263c7209467bc83af2" + +[[package]] +name = "futures" +version = "0.3.32" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "8b147ee9d1f6d097cef9ce628cd2ee62288d963e16fb287bd9286455b241382d" +dependencies = [ + "futures-channel", + "futures-core", + "futures-executor", + "futures-io", + "futures-sink", + "futures-task", + "futures-util", +] + +[[package]] +name = "futures-channel" +version = "0.3.32" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "07bbe89c50d7a535e539b8c17bc0b49bdb77747034daa8087407d655f3f7cc1d" +dependencies = [ + "futures-core", + "futures-sink", +] + +[[package]] +name = "futures-core" +version = "0.3.32" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "7e3450815272ef58cec6d564423f6e755e25379b217b0bc688e295ba24df6b1d" + +[[package]] +name = "futures-executor" +version = "0.3.32" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "baf29c38818342a3b26b5b923639e7b1f4a61fc5e76102d4b1981c6dc7a7579d" +dependencies = [ + "futures-core", + "futures-task", + "futures-util", +] + +[[package]] +name = "futures-io" +version = "0.3.32" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "cecba35d7ad927e23624b22ad55235f2239cfa44fd10428eecbeba6d6a717718" + +[[package]] +name = "futures-macro" +version = "0.3.32" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "e835b70203e41293343137df5c0664546da5745f82ec9b84d40be8336958447b" +dependencies = [ + "proc-macro2", + "quote", + "syn", +] + +[[package]] +name = "futures-sink" +version = "0.3.32" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "c39754e157331b013978ec91992bde1ac089843443c49cbc7f46150b0fad0893" + +[[package]] +name = "futures-task" +version = "0.3.32" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "037711b3d59c33004d3856fbdc83b99d4ff37a24768fa1be9ce3538a1cde4393" + +[[package]] +name = "futures-util" +version = "0.3.32" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "389ca41296e6190b48053de0321d02a77f32f8a5d2461dd38762c0593805c6d6" +dependencies = [ + "futures-channel", + "futures-core", + "futures-io", + "futures-macro", + "futures-sink", + "futures-task", + "memchr", + "pin-project-lite", + "slab", +] + +[[package]] +name = "greet-component" +version = "0.1.0" +dependencies = [ + "wit-bindgen", +] + +[[package]] +name = "hashbrown" +version = "0.15.5" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "9229cfe53dfd69f0609a49f65461bd93001ea1ef889cd5529dd176593f5338a1" +dependencies = [ + "foldhash", +] + +[[package]] +name = "hashbrown" +version = "0.16.1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "841d1cc9bed7f9236f321df977030373f4a4163ae1a7dbfe1a51a2c1a51d9100" + +[[package]] +name = "heck" +version = "0.5.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "2304e00983f87ffb38b55b444b5e3b60a884b5d30c0fca7d82fe33449bbe55ea" + +[[package]] +name = "id-arena" +version = "2.3.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "3d3067d79b975e8844ca9eb072e16b31c3c1c36928edf9c6789548c524d0d954" + +[[package]] +name = "indexmap" +version = "2.13.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "7714e70437a7dc3ac8eb7e6f8df75fd8eb422675fc7678aff7364301092b1017" +dependencies = [ + "equivalent", + "hashbrown 0.16.1", + "serde", + "serde_core", +] + +[[package]] +name = "itoa" +version = "1.0.18" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "8f42a60cbdf9a97f5d2305f08a87dc4e09308d1276d28c869c684d7777685682" + +[[package]] +name = "leb128fmt" +version = "0.1.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "09edd9e8b54e49e587e4f6295a7d29c3ea94d469cb40ab8ca70b288248a81db2" + +[[package]] +name = "log" +version = "0.4.29" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "5e5032e24019045c762d3c0f28f5b6b8bbf38563a65908389bf7978758920897" + +[[package]] +name = "memchr" +version = "2.8.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "f8ca58f447f06ed17d5fc4043ce1b10dd205e060fb3ce5b979b8ed8e59ff3f79" + +[[package]] +name = "once_cell" +version = "1.21.4" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "9f7c3e4beb33f85d45ae3e3a1792185706c8e16d043238c593331cc7cd313b50" + +[[package]] +name = "pin-project-lite" +version = "0.2.17" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "a89322df9ebe1c1578d689c92318e070967d1042b512afbe49518723f4e6d5cd" + +[[package]] +name = "prettyplease" +version = "0.2.37" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "479ca8adacdd7ce8f1fb39ce9ecccbfe93a3f1344b3d0d97f20bc0196208f62b" +dependencies = [ + "proc-macro2", + "syn", +] + +[[package]] +name = "proc-macro2" +version = "1.0.106" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "8fd00f0bb2e90d81d1044c2b32617f68fcb9fa3bb7640c23e9c748e53fb30934" +dependencies = [ + "unicode-ident", +] + +[[package]] +name = "quote" +version = "1.0.45" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "41f2619966050689382d2b44f664f4bc593e129785a36d6ee376ddf37259b924" +dependencies = [ + "proc-macro2", +] + +[[package]] +name = "semver" +version = "1.0.27" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "d767eb0aabc880b29956c35734170f26ed551a859dbd361d140cdbeca61ab1e2" + +[[package]] +name = "serde" +version = "1.0.228" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "9a8e94ea7f378bd32cbbd37198a4a91436180c5bb472411e48b5ec2e2124ae9e" +dependencies = [ + "serde_core", +] + +[[package]] +name = "serde_core" +version = "1.0.228" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "41d385c7d4ca58e59fc732af25c3983b67ac852c1a25000afe1175de458b67ad" +dependencies = [ + "serde_derive", +] + +[[package]] +name = "serde_derive" +version = "1.0.228" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "d540f220d3187173da220f885ab66608367b6574e925011a9353e4badda91d79" +dependencies = [ + "proc-macro2", + "quote", + "syn", +] + +[[package]] +name = "serde_json" +version = "1.0.149" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "83fc039473c5595ace860d8c4fafa220ff474b3fc6bfdb4293327f1a37e94d86" +dependencies = [ + "itoa", + "memchr", + "serde", + "serde_core", + "zmij", +] + +[[package]] +name = "slab" +version = "0.4.12" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "0c790de23124f9ab44544d7ac05d60440adc586479ce501c1d6d7da3cd8c9cf5" + +[[package]] +name = "syn" +version = "2.0.117" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "e665b8803e7b1d2a727f4023456bbbbe74da67099c585258af0ad9c5013b9b99" +dependencies = [ + "proc-macro2", + "quote", + "unicode-ident", +] + +[[package]] +name = "unicode-ident" +version = "1.0.24" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "e6e4313cd5fcd3dad5cafa179702e2b244f760991f45397d14d4ebf38247da75" + +[[package]] +name = "unicode-xid" +version = "0.2.6" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "ebc1c04c71510c7f702b52b7c350734c9ff1295c464a03335b00bb84fc54f853" + +[[package]] +name = "wasm-encoder" +version = "0.235.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "b3bc393c395cb621367ff02d854179882b9a351b4e0c93d1397e6090b53a5c2a" +dependencies = [ + "leb128fmt", + "wasmparser", +] + +[[package]] +name = "wasm-metadata" +version = "0.235.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "b055604ba04189d54b8c0ab2c2fc98848f208e103882d5c0b984f045d5ea4d20" +dependencies = [ + "anyhow", + "indexmap", + "wasm-encoder", + "wasmparser", +] + +[[package]] +name = "wasmparser" +version = "0.235.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "161296c618fa2d63f6ed5fffd1112937e803cb9ec71b32b01a76321555660917" +dependencies = [ + "bitflags", + "hashbrown 0.15.5", + "indexmap", + "semver", +] + +[[package]] +name = "wit-bindgen" +version = "0.43.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "9a18712ff1ec5bd09da500fe1e91dec11256b310da0ff33f8b4ec92b927cf0c6" +dependencies = [ + "wit-bindgen-rt", + "wit-bindgen-rust-macro", +] + +[[package]] +name = "wit-bindgen-core" +version = "0.43.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "2c53468e077362201de11999c85c07c36e12048a990a3e0d69da2bd61da355d0" +dependencies = [ + "anyhow", + "heck", + "wit-parser", +] + +[[package]] +name = "wit-bindgen-rt" +version = "0.43.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "9fd734226eac1fd7c450956964e3a9094c9cee65e9dafdf126feef8c0096db65" +dependencies = [ + "bitflags", + "futures", + "once_cell", +] + +[[package]] +name = "wit-bindgen-rust" +version = "0.43.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "531ebfcec48e56473805285febdb450e270fa75b2dacb92816861d0473b4c15f" +dependencies = [ + "anyhow", + "heck", + "indexmap", + "prettyplease", + "syn", + "wasm-metadata", + "wit-bindgen-core", + "wit-component", +] + +[[package]] +name = "wit-bindgen-rust-macro" +version = "0.43.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "7852bf8a9d1ea80884d26b864ddebd7b0c7636697c6ca10f4c6c93945e023966" +dependencies = [ + "anyhow", + "prettyplease", + "proc-macro2", + "quote", + "syn", + "wit-bindgen-core", + "wit-bindgen-rust", +] + +[[package]] +name = "wit-component" +version = "0.235.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "64a57a11109cc553396f89f3a38a158a97d0b1adaec113bd73e0f64d30fb601f" +dependencies = [ + "anyhow", + "bitflags", + "indexmap", + "log", + "serde", + "serde_derive", + "serde_json", + "wasm-encoder", + "wasm-metadata", + "wasmparser", + "wit-parser", +] + +[[package]] +name = "wit-parser" +version = "0.235.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "0a1f95a87d03a33e259af286b857a95911eb46236a0f726cbaec1227b3dfc67a" +dependencies = [ + "anyhow", + "id-arena", + "indexmap", + "log", + "semver", + "serde", + "serde_derive", + "serde_json", + "unicode-xid", + "wasmparser", +] + +[[package]] +name = "zmij" +version = "1.0.21" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "b8848ee67ecc8aedbaf3e4122217aff892639231befc6a1b58d29fff4c2cabaa" diff --git a/examples/greet-component/Cargo.toml b/examples/greet-component/Cargo.toml new file mode 100644 index 00000000..4656cb58 --- /dev/null +++ b/examples/greet-component/Cargo.toml @@ -0,0 +1,10 @@ +[package] +name = "greet-component" +version = "0.1.0" +edition = "2021" + +[lib] +crate-type = ["cdylib"] + +[dependencies] +wit-bindgen = "0.43.0" diff --git a/examples/greet-component/src/lib.rs b/examples/greet-component/src/lib.rs new file mode 100644 index 00000000..65a3b020 --- /dev/null +++ b/examples/greet-component/src/lib.rs @@ -0,0 +1,11 @@ +wit_bindgen::generate!({ world: "greet-world" }); + +struct Component; + +impl Guest for Component { + fn greet(name: String) -> String { + format!("Hello, {}!", name) + } +} + +export!(Component); diff --git a/examples/greet-component/wit/world.wit b/examples/greet-component/wit/world.wit new file mode 100644 index 00000000..8a40f184 --- /dev/null +++ b/examples/greet-component/wit/world.wit @@ -0,0 +1,5 @@ +package propeller:greet-component; + +world greet-world { + export greet: func(name: string) -> string; +} diff --git a/pkg/task/task.go b/pkg/task/task.go index 4d8bdafe..32961ce0 100644 --- a/pkg/task/task.go +++ b/pkg/task/task.go @@ -1,11 +1,38 @@ package task import ( + "encoding/json" + "fmt" "time" "github.com/absmach/propeller/pkg/proplet" ) +type FlexStrings []string + +func (f *FlexStrings) UnmarshalJSON(data []byte) error { + var raw []json.RawMessage + if err := json.Unmarshal(data, &raw); err != nil { + return err + } + *f = make(FlexStrings, len(raw)) + for i, r := range raw { + var s string + if err := json.Unmarshal(r, &s); err == nil { + (*f)[i] = s + + continue + } + var n json.Number + if err := json.Unmarshal(r, &n); err != nil { + return fmt.Errorf("inputs[%d]: expected string or number", i) + } + (*f)[i] = n.String() + } + + return nil +} + type State uint8 const ( @@ -70,7 +97,7 @@ type Task struct { ImageURL string `json:"image_url,omitempty"` File []byte `json:"file,omitempty"` CLIArgs []string `json:"cli_args"` - Inputs []uint64 `json:"inputs,omitempty"` + Inputs FlexStrings `json:"inputs,omitempty"` Env map[string]string `json:"env,omitempty"` Daemon bool `json:"daemon"` Encrypted bool `json:"encrypted"` diff --git a/proplet/Cargo.lock b/proplet/Cargo.lock index 9e6cfb06..eb647498 100644 --- a/proplet/Cargo.lock +++ b/proplet/Cargo.lock @@ -425,6 +425,12 @@ version = "1.8.3" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "2af50177e190e07a26ab74f8b1efbfe2ef87da2116221318cb1c2e82baf7de06" +[[package]] +name = "beef" +version = "0.5.2" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "3a8241f3ebb85c056b509d4327ad0358fbbba6ffb340bf388f26350aeda225b1" + [[package]] name = "bincode" version = "1.3.3" @@ -2996,6 +3002,39 @@ version = "0.4.29" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "5e5032e24019045c762d3c0f28f5b6b8bbf38563a65908389bf7978758920897" +[[package]] +name = "logos" +version = "0.14.4" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "7251356ef8cb7aec833ddf598c6cb24d17b689d20b993f9d11a3d764e34e6458" +dependencies = [ + "logos-derive", +] + +[[package]] +name = "logos-codegen" +version = "0.14.4" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "59f80069600c0d66734f5ff52cc42f2dabd6b29d205f333d61fd7832e9e9963f" +dependencies = [ + "beef", + "fnv", + "lazy_static", + "proc-macro2", + "quote", + "regex-syntax", + "syn 2.0.117", +] + +[[package]] +name = "logos-derive" +version = "0.14.4" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "24fb722b06a9dc12adb0963ed585f19fc61dc5413e6a9be9422ef92c091e731d" +dependencies = [ + "logos-codegen", +] + [[package]] name = "loopdev" version = "0.5.0" @@ -3927,6 +3966,7 @@ dependencies = [ "tracing-subscriber", "url", "uuid", + "wasm-wave", "wasmtime", "wasmtime-wasi", "wasmtime-wasi-http", @@ -5989,6 +6029,17 @@ dependencies = [ "web-sys", ] +[[package]] +name = "wasm-wave" +version = "0.244.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "d1c34a4c10a1b9260f8131929d680e36edf00836bb8e76524d3004522bd6f287" +dependencies = [ + "logos", + "thiserror 2.0.18", + "wit-parser 0.244.0", +] + [[package]] name = "wasmparser" version = "0.215.0" @@ -6073,6 +6124,7 @@ dependencies = [ "tempfile", "wasm-compose", "wasm-encoder 0.244.0", + "wasm-wave", "wasmparser 0.244.0", "wasmtime-environ", "wasmtime-internal-cache", diff --git a/proplet/Cargo.toml b/proplet/Cargo.toml index 7c1b37a3..8af09636 100644 --- a/proplet/Cargo.toml +++ b/proplet/Cargo.toml @@ -9,7 +9,7 @@ rumqttc = "0.25.1" serde = { version = "1.0", features = ["derive"] } serde_json = "1.0" toml = "0.9.8" -wasmtime = { version = "42.0.1", features = ["component-model", "async"] } +wasmtime = { version = "42.0.1", features = ["component-model", "async", "wave"] } wasmtime-wasi = "42.0.1" wasmtime-wasi-http = "42.0.1" hyper = { version = "1.7", features = ["full"] } @@ -41,6 +41,7 @@ futures-util = { version = "0.3" } # ELASTIC TEE HAL — hardware abstraction layer for TEE workloads elastic-tee-hal = { git = "https://github.com/elasticproject-eu/wasmhal", default-features = false, features = ["amd-sev"] } +wasm-wave = "0.244.0" [features] default = [] diff --git a/proplet/src/config.rs b/proplet/src/config.rs index bdc7c7fe..223b13d0 100644 --- a/proplet/src/config.rs +++ b/proplet/src/config.rs @@ -382,6 +382,13 @@ impl PropletConfig { mod tests { use super::*; use std::env; + use std::sync::{Mutex, OnceLock}; + + static ENV_MUTEX: OnceLock> = OnceLock::new(); + + fn env_lock() -> std::sync::MutexGuard<'static, ()> { + ENV_MUTEX.get_or_init(|| Mutex::new(())).lock().unwrap() + } #[test] fn test_proplet_config_default() { @@ -469,6 +476,7 @@ mod tests { #[test] fn test_proplet_config_from_env_log_level() { + let _lock = env_lock(); env::set_var("PROPLET_LOG_LEVEL", "debug"); let config = PropletConfig::from_env(); env::remove_var("PROPLET_LOG_LEVEL"); @@ -478,6 +486,7 @@ mod tests { #[test] fn test_proplet_config_from_env_mqtt_address() { + let _lock = env_lock(); env::set_var("PROPLET_MQTT_ADDRESS", "tcp://mqtt.example.com:1883"); let config = PropletConfig::from_env(); env::remove_var("PROPLET_MQTT_ADDRESS"); @@ -487,6 +496,7 @@ mod tests { #[test] fn test_proplet_config_from_env_mqtt_timeout() { + let _lock = env_lock(); env::set_var("PROPLET_MQTT_TIMEOUT", "120"); let config = PropletConfig::from_env(); env::remove_var("PROPLET_MQTT_TIMEOUT"); @@ -496,6 +506,7 @@ mod tests { #[test] fn test_proplet_config_from_env_mqtt_qos() { + let _lock = env_lock(); env::set_var("PROPLET_MQTT_QOS", "1"); let config = PropletConfig::from_env(); env::remove_var("PROPLET_MQTT_QOS"); @@ -505,6 +516,7 @@ mod tests { #[test] fn test_proplet_config_from_env_liveliness_interval() { + let _lock = env_lock(); env::set_var("PROPLET_LIVELINESS_INTERVAL", "20"); let config = PropletConfig::from_env(); env::remove_var("PROPLET_LIVELINESS_INTERVAL"); @@ -514,6 +526,7 @@ mod tests { #[test] fn test_proplet_config_from_env_domain_id() { + let _lock = env_lock(); env::set_var("PROPLET_DOMAIN_ID", "domain-123"); let config = PropletConfig::from_env(); env::remove_var("PROPLET_DOMAIN_ID"); @@ -523,6 +536,7 @@ mod tests { #[test] fn test_proplet_config_from_env_channel_id() { + let _lock = env_lock(); env::set_var("PROPLET_CHANNEL_ID", "channel-456"); let config = PropletConfig::from_env(); env::remove_var("PROPLET_CHANNEL_ID"); @@ -532,6 +546,7 @@ mod tests { #[test] fn test_proplet_config_from_env_client_id() { + let _lock = env_lock(); env::set_var("PROPLET_CLIENT_ID", "client-789"); let config = PropletConfig::from_env(); env::remove_var("PROPLET_CLIENT_ID"); @@ -541,6 +556,7 @@ mod tests { #[test] fn test_proplet_config_from_env_client_key() { + let _lock = env_lock(); env::set_var("PROPLET_CLIENT_KEY", "secret-key"); let config = PropletConfig::from_env(); env::remove_var("PROPLET_CLIENT_KEY"); @@ -550,6 +566,7 @@ mod tests { #[test] fn test_proplet_config_from_env_k8s_namespace() { + let _lock = env_lock(); env::set_var("PROPLET_MANAGER_K8S_NAMESPACE", "production"); let config = PropletConfig::from_env(); env::remove_var("PROPLET_MANAGER_K8S_NAMESPACE"); @@ -559,6 +576,7 @@ mod tests { #[test] fn test_proplet_config_from_env_external_runtime() { + let _lock = env_lock(); env::set_var("PROPLET_EXTERNAL_WASM_RUNTIME", "/usr/local/bin/wasmtime"); let config = PropletConfig::from_env(); env::remove_var("PROPLET_EXTERNAL_WASM_RUNTIME"); @@ -571,6 +589,7 @@ mod tests { #[test] fn test_proplet_config_from_env_mqtt_timeout_invalid() { + let _lock = env_lock(); env::set_var("PROPLET_MQTT_TIMEOUT", "not-a-number"); let config = PropletConfig::from_env(); env::remove_var("PROPLET_MQTT_TIMEOUT"); @@ -580,6 +599,7 @@ mod tests { #[test] fn test_proplet_config_from_env_mqtt_qos_invalid() { + let _lock = env_lock(); env::set_var("PROPLET_MQTT_QOS", "invalid"); let config = PropletConfig::from_env(); env::remove_var("PROPLET_MQTT_QOS"); @@ -589,6 +609,7 @@ mod tests { #[test] fn test_proplet_config_from_env_no_env_vars() { + let _lock = env_lock(); let vars_to_clear = vec![ "PROPLET_LOG_LEVEL", "PROPLET_MQTT_ADDRESS", diff --git a/proplet/src/runtime/host.rs b/proplet/src/runtime/host.rs index 463bf2e7..8d050ed1 100644 --- a/proplet/src/runtime/host.rs +++ b/proplet/src/runtime/host.rs @@ -52,6 +52,13 @@ impl HostRuntime { } Ok(()) } + + fn is_wasm_component(binary: &[u8]) -> bool { + // Component model binary layer marker: 0x0a (older wasm-tools) or 0x0d (wasm-tools >= 0.200) + binary.len() >= 8 + && (binary[4..8] == [0x0a, 0x00, 0x01, 0x00] + || binary[4..8] == [0x0d, 0x00, 0x01, 0x00]) + } } #[async_trait] @@ -73,13 +80,20 @@ impl Runtime for HostRuntime { cmd.arg("run"); + let is_component = Self::is_wasm_component(&config.wasm_binary); let cli_args_has_invoke = config.cli_args.iter().any(|a| a == "--invoke"); - if !config.function_name.is_empty() + let has_custom_export = !config.function_name.is_empty() && config.function_name != "_start" && !config.function_name.starts_with("fl-round-") - && !cli_args_has_invoke - { - cmd.arg("--invoke").arg(&config.function_name); + && !cli_args_has_invoke; + + if has_custom_export { + if is_component { + let wave_call = format!("{}({})", config.function_name, config.args.join(", ")); + cmd.arg("--invoke").arg(&wave_call); + } else { + cmd.arg("--invoke").arg(&config.function_name); + } } for arg in &config.cli_args { @@ -103,8 +117,10 @@ impl Runtime for HostRuntime { cmd.arg(&temp_file); - for arg in &config.args { - cmd.arg(arg.to_string()); + if !is_component || !has_custom_export { + for arg in &config.args { + cmd.arg(arg); + } } cmd.envs(&config.env); @@ -391,4 +407,28 @@ mod tests { runtime.cleanup_temp_file(file_path).await.unwrap(); } + + #[test] + fn test_is_wasm_component_old_format() { + let binary = [0x00, 0x61, 0x73, 0x6d, 0x0a, 0x00, 0x01, 0x00, 0x00]; + assert!(HostRuntime::is_wasm_component(&binary)); + } + + #[test] + fn test_is_wasm_component_new_format() { + let binary = [0x00, 0x61, 0x73, 0x6d, 0x0d, 0x00, 0x01, 0x00, 0x00]; + assert!(HostRuntime::is_wasm_component(&binary)); + } + + #[test] + fn test_is_wasm_component_rejects_core_module() { + let binary = [0x00, 0x61, 0x73, 0x6d, 0x01, 0x00, 0x00, 0x00, 0x00]; + assert!(!HostRuntime::is_wasm_component(&binary)); + } + + #[test] + fn test_is_wasm_component_rejects_too_short() { + let binary = [0x00, 0x61, 0x73, 0x6d]; + assert!(!HostRuntime::is_wasm_component(&binary)); + } } diff --git a/proplet/src/runtime/mod.rs b/proplet/src/runtime/mod.rs index b14523ca..cf4da0aa 100644 --- a/proplet/src/runtime/mod.rs +++ b/proplet/src/runtime/mod.rs @@ -14,7 +14,7 @@ pub struct StartConfig { pub wasm_binary: Vec, pub cli_args: Vec, pub env: HashMap, - pub args: Vec, + pub args: Vec, #[allow(dead_code)] pub mode: Option, } diff --git a/proplet/src/runtime/tee_runtime.rs b/proplet/src/runtime/tee_runtime.rs index 83282e7e..d26d14aa 100644 --- a/proplet/src/runtime/tee_runtime.rs +++ b/proplet/src/runtime/tee_runtime.rs @@ -242,7 +242,7 @@ impl TeeWasmRuntime { cmd.arg(wasm_path); for arg in &config.args { - cmd.arg(arg.to_string()); + cmd.arg(arg); } cmd.stdout(std::process::Stdio::piped()); diff --git a/proplet/src/runtime/wasmtime_runtime.rs b/proplet/src/runtime/wasmtime_runtime.rs index 9bd1940b..55639757 100644 --- a/proplet/src/runtime/wasmtime_runtime.rs +++ b/proplet/src/runtime/wasmtime_runtime.rs @@ -14,6 +14,7 @@ use tokio::sync::watch; use tokio::sync::Mutex; use tokio::task::JoinHandle; use tracing::{error, info, warn}; +use wasm_wave; use wasmtime::component::ResourceTable; use wasmtime::*; use wasmtime_wasi::p2::bindings::sync::Command; @@ -118,8 +119,14 @@ impl Runtime for WasmtimeRuntime { is_proxy, ); + let has_custom_export = !config.function_name.is_empty() + && config.function_name != "_start" + && !config.function_name.starts_with("fl-round-"); + if is_proxy { self.start_app_proxy(config).await + } else if is_component && has_custom_export { + self.start_app_component_export(config).await } else if is_component { self.start_app_component(config).await } else { @@ -317,6 +324,144 @@ impl WasmtimeRuntime { } } + async fn start_app_component_export(&self, config: StartConfig) -> Result> { + info!( + "Compiling WASM component for custom export '{}' for task: {}", + config.function_name, config.id + ); + + let component = match component::Component::from_binary(&self.engine, &config.wasm_binary) { + Ok(c) => c, + Err(e) => { + return Err(anyhow::anyhow!( + "Failed to compile WASM component from binary: {e}" + )) + } + }; + + let mut wasi_builder = WasiCtxBuilder::new(); + wasi_builder.inherit_stdio(); + for (key, value) in &config.env { + wasi_builder.env(key, value); + } + for dir in &self.preopened_dirs { + let _ = wasi_builder + .preopened_dir(dir, dir, DirPerms::all(), FilePerms::all()) + .map_err(|e| format!("Failed to preopen directory '{dir}': {e}")); + } + let wasi = wasi_builder.build(); + + let store_data = StoreData { + wasi, + http: WasiHttpCtx::new(), + table: ResourceTable::new(), + }; + + let mut store = Store::new(&self.engine, store_data); + + let mut linker: component::Linker = component::Linker::new(&self.engine); + let _ = wasmtime_wasi::p2::add_to_linker_sync(&mut linker) + .map_err(|e| format!("Failed to add WASI P2 to component linker: {e}")); + + let task_id = config.id.clone(); + let task_id_for_cleanup = task_id.clone(); + let function_name = config.function_name.clone(); + let args = config.args.clone(); + let tasks = self.tasks.clone(); + + let (result_tx, result_rx) = oneshot::channel(); + + let handle = tokio::task::spawn(async move { + let result = tokio::task::spawn_blocking(move || { + let instance = match linker.instantiate(&mut store, &component) { + Ok(i) => i, + Err(e) => { + return Err(anyhow::anyhow!("Failed to instantiate WASM component: {e}")) + } + }; + + let func = instance + .get_func(&mut store, &function_name) + .ok_or_else(|| { + anyhow::anyhow!( + "Export '{}' not found in component for task {}", + function_name, + task_id + ) + })?; + + let func_ty = func.ty(&store); + let param_types: Vec<_> = func_ty.params().collect(); + let result_count = func_ty.results().count(); + + if args.len() != param_types.len() { + return Err(anyhow::anyhow!( + "Argument count mismatch for '{}': expected {} but got {}", + function_name, + param_types.len(), + args.len() + )); + } + + let wasm_args: Vec = args + .iter() + .zip(param_types.iter()) + .map(|(wave_str, (_, ty))| { + wasm_wave::from_str::(ty, wave_str).map_err(|e| { + anyhow::anyhow!( + "Failed to parse WAVE argument '{}' for export '{}': {e}", + wave_str, + function_name + ) + }) + }) + .collect::>>()?; + + let mut results: Vec = (0..result_count) + .map(|_| component::Val::Bool(false)) + .collect(); + + func.call(&mut store, &wasm_args, &mut results) + .map_err(|e| { + anyhow::anyhow!("Failed to call export '{}': {e}", function_name) + })?; + + let result_string = results + .first() + .and_then(|v| wasm_wave::to_string(v).ok()) + .unwrap_or_default(); + + info!( + "Export '{}' for task {} completed, result: {}", + function_name, task_id, result_string + ); + + Ok(result_string.into_bytes()) + }) + .await; + + tasks.lock().await.remove(&task_id_for_cleanup); + + let final_result = match result { + Ok(Ok(data)) => Ok(data), + Ok(Err(e)) => Err(e), + Err(e) => Err(anyhow::anyhow!("Task join error: {e}")), + }; + + let _ = result_tx.send(final_result); + }); + + { + let mut tasks_map = self.tasks.lock().await; + tasks_map.insert(config.id.clone(), handle); + } + + match result_rx.await { + Ok(result) => result, + Err(_) => Err(anyhow::anyhow!("Task was cancelled or panicked")), + } + } + async fn start_app_proxy(&self, config: StartConfig) -> Result> { if !self.http_enabled { return Err(anyhow::anyhow!( @@ -593,13 +738,25 @@ impl WasmtimeRuntime { .iter() .zip(param_types.iter()) .map(|(arg, param_type)| match param_type { - ValType::I32 => Val::I32(*arg as i32), - ValType::I64 => Val::I64(*arg as i64), - ValType::F32 => Val::F32((*arg as f32).to_bits()), - ValType::F64 => Val::F64((*arg as f64).to_bits()), - _ => Val::I32(*arg as i32), + ValType::I32 => arg + .parse::() + .map(Val::I32) + .map_err(|e| anyhow::anyhow!("Failed to parse '{}' as i32: {e}", arg)), + ValType::I64 => arg + .parse::() + .map(Val::I64) + .map_err(|e| anyhow::anyhow!("Failed to parse '{}' as i64: {e}", arg)), + ValType::F32 => arg + .parse::() + .map(|f| Val::F32(f.to_bits())) + .map_err(|e| anyhow::anyhow!("Failed to parse '{}' as f32: {e}", arg)), + ValType::F64 => arg + .parse::() + .map(|f| Val::F64(f.to_bits())) + .map_err(|e| anyhow::anyhow!("Failed to parse '{}' as f64: {e}", arg)), + _ => Err(anyhow::anyhow!("Unsupported Wasm value type for arg '{}'", arg)), }) - .collect(); + .collect::>>()?; info!( "Calling function '{}' with {} params, expects {} results", diff --git a/proplet/src/types.rs b/proplet/src/types.rs index 80cc1270..f97d7fef 100644 --- a/proplet/src/types.rs +++ b/proplet/src/types.rs @@ -51,7 +51,7 @@ pub struct StartRequest { #[serde(default, deserialize_with = "deserialize_null_default")] pub image_url: String, #[serde(default, deserialize_with = "deserialize_null_default")] - pub inputs: Vec, + pub inputs: Vec, #[serde(default)] pub daemon: bool, #[serde(default)] @@ -83,7 +83,7 @@ impl StartRequest { return Err(anyhow::anyhow!("id is required")); } if self.name.is_empty() { - return Err(anyhow::anyhow!("function name is required")); + return Err(anyhow::anyhow!("name is required")); } if self.encrypted { @@ -304,7 +304,7 @@ mod tests { state: 0, file: "base64encodeddata".to_string(), image_url: String::new(), - inputs: vec![1, 2, 3], + inputs: vec!["1".to_string(), "2".to_string(), "3".to_string()], daemon: false, env: Some(HashMap::new()), monitoring_profile: None, @@ -384,7 +384,7 @@ mod tests { let result = req.validate(); assert!(result.is_err()); - assert_eq!(result.unwrap_err().to_string(), "function name is required"); + assert_eq!(result.unwrap_err().to_string(), "name is required"); } #[test] @@ -525,7 +525,7 @@ mod tests { "cli_args": ["--arg1", "value1"], "file": "ZGF0YQ==", "image_url": "", - "inputs": [10, 20, 30], + "inputs": ["10", "20", "30"], "daemon": true, "env": { "KEY1": "value1", @@ -538,7 +538,7 @@ mod tests { assert_eq!(req.id, "task-complete"); assert_eq!(req.cli_args.len(), 2); - assert_eq!(req.inputs, vec![10, 20, 30]); + assert_eq!(req.inputs, vec!["10", "20", "30"]); assert!(req.daemon); assert_eq!(req.env.as_ref().unwrap().len(), 2); }