commit 767c66fb6ab5bdee5f3d7e1c58f62887bc21d79f Author: Timur Gordon <31495328+timurgordon@users.noreply.github.com> Date: Tue Aug 26 14:49:21 2025 +0200 initial commit diff --git a/.gitignore b/.gitignore new file mode 100644 index 0000000..1de5659 --- /dev/null +++ b/.gitignore @@ -0,0 +1 @@ +target \ No newline at end of file diff --git a/Cargo.lock b/Cargo.lock new file mode 100644 index 0000000..3f70611 --- /dev/null +++ b/Cargo.lock @@ -0,0 +1,2687 @@ +# This file is automatically @generated by Cargo. +# It is not intended for manual editing. +version = 4 + +[[package]] +name = "addr2line" +version = "0.24.2" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "dfbe277e56a376000877090da837660b4427aad530e3028d44e0bffe4f89a1c1" +dependencies = [ + "gimli", +] + +[[package]] +name = "adler2" +version = "2.0.1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "320119579fcad9c21884f5c4861d16174d0e06250625266f50fe6898340abefa" + +[[package]] +name = "aho-corasick" +version = "1.1.3" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "8e60d3430d3a69478ad0993f19238d2df97c507009a52b3c10addcd7f6bcb916" +dependencies = [ + "memchr", +] + +[[package]] +name = "android-tzdata" +version = "0.1.1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "e999941b234f3131b00bc13c22d06e8c5ff726d1b6318ac7eb276997bbb4fef0" + +[[package]] +name = "android_system_properties" +version = "0.1.5" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "819e7219dbd41043ac279b19830f2efc897156490d7fd6ea916720117ee66311" +dependencies = [ + "libc", +] + +[[package]] +name = "anstream" +version = "0.6.20" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "3ae563653d1938f79b1ab1b5e668c87c76a9930414574a6583a7b7e11a8e6192" +dependencies = [ + "anstyle", + "anstyle-parse", + "anstyle-query", + "anstyle-wincon", + "colorchoice", + "is_terminal_polyfill", + "utf8parse", +] + +[[package]] +name = "anstyle" +version = "1.0.11" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "862ed96ca487e809f1c8e5a8447f6ee2cf102f846893800b20cebdf541fc6bbd" + +[[package]] +name = "anstyle-parse" +version = "0.2.7" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "4e7644824f0aa2c7b9384579234ef10eb7efb6a0deb83f9630a49594dd9c15c2" +dependencies = [ + "utf8parse", +] + +[[package]] +name = "anstyle-query" +version = "1.1.4" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "9e231f6134f61b71076a3eab506c379d4f36122f2af15a9ff04415ea4c3339e2" +dependencies = [ + "windows-sys 0.60.2", +] + +[[package]] +name = "anstyle-wincon" +version = "3.0.10" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "3e0633414522a32ffaac8ac6cc8f748e090c5717661fddeea04219e2344f5f2a" +dependencies = [ + "anstyle", + "once_cell_polyfill", + "windows-sys 0.60.2", +] + +[[package]] +name = "anyhow" +version = "1.0.99" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "b0674a1ddeecb70197781e945de4b3b8ffb61fa939a5597bcf48503737663100" + +[[package]] +name = "async-stream" +version = "0.3.6" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "0b5a71a6f37880a80d1d7f19efd781e4b5de42c88f0722cc13bcb6cc2cfe8476" +dependencies = [ + "async-stream-impl", + "futures-core", + "pin-project-lite", +] + +[[package]] +name = "async-stream-impl" +version = "0.3.6" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "c7c24de15d275a1ecfd47a380fb4d5ec9bfe0933f309ed5e705b775596a3574d" +dependencies = [ + "proc-macro2", + "quote", + "syn", +] + +[[package]] +name = "async-trait" +version = "0.1.89" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "9035ad2d096bed7955a320ee7e2230574d28fd3c3a0f186cbea1ff3c7eed5dbb" +dependencies = [ + "proc-macro2", + "quote", + "syn", +] + +[[package]] +name = "atomic-waker" +version = "1.1.2" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "1505bd5d3d116872e7271a6d4e16d81d0c8570876c8de68093a09ac269d8aac0" + +[[package]] +name = "autocfg" +version = "1.5.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "c08606f8c3cbf4ce6ec8e28fb0014a2c086708fe954eaa885384a6165172e7e8" + +[[package]] +name = "backtrace" +version = "0.3.75" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "6806a6321ec58106fea15becdad98371e28d92ccbc7c8f1b3b6dd724fe8f1002" +dependencies = [ + "addr2line", + "cfg-if", + "libc", + "miniz_oxide", + "object", + "rustc-demangle", + "windows-targets 0.52.6", +] + +[[package]] +name = "base64" +version = "0.22.1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "72b3254f16251a8381aa12e40e3c4d2f0199f8c6508fbecb9d91f575e0fbb8c6" + +[[package]] +name = "bitflags" +version = "2.9.3" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "34efbcccd345379ca2868b2b2c9d3782e9cc58ba87bc7d79d5b53d9c9ae6f25d" + +[[package]] +name = "block-buffer" +version = "0.10.4" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "3078c7629b62d3f0439517fa394996acacc5cbc91c5a20d8c658e77abd503a71" +dependencies = [ + "generic-array", +] + +[[package]] +name = "bumpalo" +version = "3.19.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "46c5e41b57b8bba42a04676d81cb89e9ee8e859a1a66f80a5a72e1cb76b34d43" + +[[package]] +name = "bytes" +version = "1.10.1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "d71b6127be86fdcfddb610f7182ac57211d4b18a3e9c82eb2d17662f2227ad6a" + +[[package]] +name = "cc" +version = "1.2.34" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "42bc4aea80032b7bf409b0bc7ccad88853858911b7713a8062fdc0623867bedc" +dependencies = [ + "shlex", +] + +[[package]] +name = "cesu8" +version = "1.1.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "6d43a04d8753f35258c91f8ec639f792891f748a1edbd759cf1dcea3382ad83c" + +[[package]] +name = "cfg-if" +version = "1.0.3" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "2fd1289c04a9ea8cb22300a459a72a385d7c73d3259e2ed7dcb2af674838cfa9" + +[[package]] +name = "chrono" +version = "0.4.41" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "c469d952047f47f91b68d1cba3f10d63c11d73e4636f24f08daf0278abf01c4d" +dependencies = [ + "android-tzdata", + "iana-time-zone", + "js-sys", + "num-traits", + "serde", + "wasm-bindgen", + "windows-link", +] + +[[package]] +name = "clap" +version = "4.5.45" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "1fc0e74a703892159f5ae7d3aac52c8e6c392f5ae5f359c70b5881d60aaac318" +dependencies = [ + "clap_builder", + "clap_derive", +] + +[[package]] +name = "clap_builder" +version = "4.5.44" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "b3e7f4214277f3c7aa526a59dd3fbe306a370daee1f8b7b8c987069cd8e888a8" +dependencies = [ + "anstream", + "anstyle", + "clap_lex", + "strsim", +] + +[[package]] +name = "clap_derive" +version = "4.5.45" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "14cb31bb0a7d536caef2639baa7fad459e15c3144efefa6dbd1c84562c4739f6" +dependencies = [ + "heck", + "proc-macro2", + "quote", + "syn", +] + +[[package]] +name = "clap_lex" +version = "0.7.5" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "b94f61472cee1439c0b966b47e3aca9ae07e45d070759512cd390ea2bebc6675" + +[[package]] +name = "colorchoice" +version = "1.0.4" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "b05b61dc5112cbb17e4b6cd61790d9845d13888356391624cbe7e41efeac1e75" + +[[package]] +name = "combine" +version = "4.6.7" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "ba5a308b75df32fe02788e748662718f03fde005016435c444eea572398219fd" +dependencies = [ + "bytes", + "futures-core", + "memchr", + "pin-project-lite", + "tokio", + "tokio-util", +] + +[[package]] +name = "console_log" +version = "1.0.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "be8aed40e4edbf4d3b4431ab260b63fdc40f5780a4766824329ea0f1eefe3c0f" +dependencies = [ + "log", + "web-sys", +] + +[[package]] +name = "core-foundation" +version = "0.10.1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "b2a6cd9ae233e7f62ba4e9353e81a88df7fc8a5987b8d445b4d90c879bd156f6" +dependencies = [ + "core-foundation-sys", + "libc", +] + +[[package]] +name = "core-foundation-sys" +version = "0.8.7" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "773648b94d0e5d620f64f280777445740e61fe701025087ec8b57f45c791888b" + +[[package]] +name = "cpufeatures" +version = "0.2.17" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "59ed5838eebb26a2bb2e58f6d5b5316989ae9d08bab10e0e6d103e656d1b0280" +dependencies = [ + "libc", +] + +[[package]] +name = "crypto-common" +version = "0.1.6" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "1bfb12502f3fc46cca1bb51ac28df9d618d813cdc3d2f25b9fe775a34af26bb3" +dependencies = [ + "generic-array", + "typenum", +] + +[[package]] +name = "deranged" +version = "0.4.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "9c9e6a11ca8224451684bc0d7d5a7adbf8f2fd6887261a1cfc3c0432f9d4068e" +dependencies = [ + "powerfmt", +] + +[[package]] +name = "digest" +version = "0.10.7" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "9ed9a281f7bc9b7576e61468ba615a66a5c8cfdff42420a70aa82701a3b1e292" +dependencies = [ + "block-buffer", + "crypto-common", +] + +[[package]] +name = "displaydoc" +version = "0.2.5" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "97369cbbc041bc366949bc74d34658d6cda5621039731c6310521892a3a20ae0" +dependencies = [ + "proc-macro2", + "quote", + "syn", +] + +[[package]] +name = "env_filter" +version = "0.1.3" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "186e05a59d4c50738528153b83b0b0194d3a29507dfec16eccd4b342903397d0" +dependencies = [ + "log", + "regex", +] + +[[package]] +name = "env_logger" +version = "0.10.2" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "4cd405aab171cb85d6735e5c8d9db038c17d3ca007a4d2c25f337935c3d90580" +dependencies = [ + "humantime", + "is-terminal", + "log", + "regex", + "termcolor", +] + +[[package]] +name = "env_logger" +version = "0.11.8" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "13c863f0904021b108aa8b2f55046443e6b1ebde8fd4a15c399893aae4fa069f" +dependencies = [ + "anstream", + "anstyle", + "env_filter", + "jiff", + "log", +] + +[[package]] +name = "equivalent" +version = "1.0.2" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "877a4ace8713b0bcf2a4e7eec82529c029f1d0619886d18145fea96c3ffe5c0f" + +[[package]] +name = "escargot" +version = "0.5.15" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "11c3aea32bc97b500c9ca6a72b768a26e558264303d101d3409cf6d57a9ed0cf" +dependencies = [ + "log", + "serde", + "serde_json", +] + +[[package]] +name = "fnv" +version = "1.0.7" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "3f9eec918d3f24069decb9af1554cad7c880e2da24a9afd88aca000531ab82c1" + +[[package]] +name = "form_urlencoded" +version = "1.2.2" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "cb4cb245038516f5f85277875cdaa4f7d2c9a0fa0468de06ed190163b1581fcf" +dependencies = [ + "percent-encoding", +] + +[[package]] +name = "futures" +version = "0.3.31" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "65bc07b1a8bc7c85c5f2e110c476c7389b4554ba72af57d8445ea63a576b0876" +dependencies = [ + "futures-channel", + "futures-core", + "futures-executor", + "futures-io", + "futures-sink", + "futures-task", + "futures-util", +] + +[[package]] +name = "futures-channel" +version = "0.3.31" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "2dff15bf788c671c1934e366d07e30c1814a8ef514e1af724a602e8a2fbe1b10" +dependencies = [ + "futures-core", + "futures-sink", +] + +[[package]] +name = "futures-core" +version = "0.3.31" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "05f29059c0c2090612e8d742178b0580d2dc940c837851ad723096f87af6663e" + +[[package]] +name = "futures-executor" +version = "0.3.31" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "1e28d1d997f585e54aebc3f97d39e72338912123a67330d723fdbb564d646c9f" +dependencies = [ + "futures-core", + "futures-task", + "futures-util", +] + +[[package]] +name = "futures-io" +version = "0.3.31" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "9e5c1b78ca4aae1ac06c48a526a655760685149f0d465d21f37abfe57ce075c6" + +[[package]] +name = "futures-macro" +version = "0.3.31" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "162ee34ebcb7c64a8abebc059ce0fee27c2262618d7b60ed8faf72fef13c3650" +dependencies = [ + "proc-macro2", + "quote", + "syn", +] + +[[package]] +name = "futures-sink" +version = "0.3.31" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "e575fab7d1e0dcb8d0c7bcf9a63ee213816ab51902e6d244a95819acacf1d4f7" + +[[package]] +name = "futures-task" +version = "0.3.31" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "f90f7dce0722e95104fcb095585910c0977252f286e354b5e3bd38902cd99988" + +[[package]] +name = "futures-util" +version = "0.3.31" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "9fa08315bb612088cc391249efdc3bc77536f16c91f6cf495e6fbe85b20a4a81" +dependencies = [ + "futures-channel", + "futures-core", + "futures-io", + "futures-macro", + "futures-sink", + "futures-task", + "memchr", + "pin-project-lite", + "pin-utils", + "slab", +] + +[[package]] +name = "generic-array" +version = "0.14.7" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "85649ca51fd72272d7821adaf274ad91c288277713d9c18820d8499a7ff69e9a" +dependencies = [ + "typenum", + "version_check", +] + +[[package]] +name = "getrandom" +version = "0.2.16" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "335ff9f135e4384c8150d6f27c6daed433577f86b4750418338c01a1a2528592" +dependencies = [ + "cfg-if", + "js-sys", + "libc", + "wasi 0.11.1+wasi-snapshot-preview1", + "wasm-bindgen", +] + +[[package]] +name = "getrandom" +version = "0.3.3" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "26145e563e54f2cadc477553f1ec5ee650b00862f0a58bcd12cbdc5f0ea2d2f4" +dependencies = [ + "cfg-if", + "libc", + "r-efi", + "wasi 0.14.2+wasi-0.2.4", +] + +[[package]] +name = "gimli" +version = "0.31.1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "07e28edb80900c19c28f1072f2e8aeca7fa06b23cd4169cefe1af5aa3260783f" + +[[package]] +name = "h2" +version = "0.4.12" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "f3c0b69cfcb4e1b9f1bf2f53f95f766e4661169728ec61cd3fe5a0166f2d1386" +dependencies = [ + "atomic-waker", + "bytes", + "fnv", + "futures-core", + "futures-sink", + "http", + "indexmap", + "slab", + "tokio", + "tokio-util", + "tracing", +] + +[[package]] +name = "hashbrown" +version = "0.15.5" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "9229cfe53dfd69f0609a49f65461bd93001ea1ef889cd5529dd176593f5338a1" + +[[package]] +name = "heck" +version = "0.5.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "2304e00983f87ffb38b55b444b5e3b60a884b5d30c0fca7d82fe33449bbe55ea" + +[[package]] +name = "hermit-abi" +version = "0.5.2" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "fc0fef456e4baa96da950455cd02c081ca953b141298e41db3fc7e36b1da849c" + +[[package]] +name = "hero-supervisor" +version = "0.1.0" +dependencies = [ + "anyhow", + "async-trait", + "chrono", + "clap", + "env_logger 0.10.2", + "escargot", + "hero-supervisor-openrpc-client", + "jsonrpsee", + "log", + "redis", + "sal-service-manager", + "serde", + "serde_json", + "thiserror", + "tokio", + "tokio-test", + "toml", + "tower", + "tower-http", + "uuid", +] + +[[package]] +name = "hero-supervisor-openrpc-client" +version = "0.1.0" +dependencies = [ + "chrono", + "console_log", + "env_logger 0.11.8", + "getrandom 0.2.16", + "hero-supervisor", + "js-sys", + "jsonrpsee", + "log", + "serde", + "serde_json", + "thiserror", + "tokio", + "uuid", + "wasm-bindgen", + "wasm-bindgen-futures", + "web-sys", +] + +[[package]] +name = "http" +version = "1.3.1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "f4a85d31aea989eead29a3aaf9e1115a180df8282431156e533de47660892565" +dependencies = [ + "bytes", + "fnv", + "itoa", +] + +[[package]] +name = "http-body" +version = "1.0.1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "1efedce1fb8e6913f23e0c92de8e62cd5b772a67e7b3946df930a62566c93184" +dependencies = [ + "bytes", + "http", +] + +[[package]] +name = "http-body-util" +version = "0.1.3" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "b021d93e26becf5dc7e1b75b1bed1fd93124b374ceb73f43d4d4eafec896a64a" +dependencies = [ + "bytes", + "futures-core", + "http", + "http-body", + "pin-project-lite", +] + +[[package]] +name = "httparse" +version = "1.10.1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "6dbf3de79e51f3d586ab4cb9d5c3e2c14aa28ed23d180cf89b4df0454a69cc87" + +[[package]] +name = "httpdate" +version = "1.0.3" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "df3b46402a9d5adb4c86a0cf463f42e19994e3ee891101b1841f30a545cb49a9" + +[[package]] +name = "humantime" +version = "2.2.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "9b112acc8b3adf4b107a8ec20977da0273a8c386765a3ec0229bd500a1443f9f" + +[[package]] +name = "hyper" +version = "1.7.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "eb3aa54a13a0dfe7fbe3a59e0c76093041720fdc77b110cc0fc260fafb4dc51e" +dependencies = [ + "atomic-waker", + "bytes", + "futures-channel", + "futures-core", + "h2", + "http", + "http-body", + "httparse", + "httpdate", + "itoa", + "pin-project-lite", + "pin-utils", + "smallvec", + "tokio", + "want", +] + +[[package]] +name = "hyper-rustls" +version = "0.27.7" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "e3c93eb611681b207e1fe55d5a71ecf91572ec8a6705cdb6857f7d8d5242cf58" +dependencies = [ + "http", + "hyper", + "hyper-util", + "log", + "rustls", + "rustls-pki-types", + "tokio", + "tokio-rustls", + "tower-service", +] + +[[package]] +name = "hyper-util" +version = "0.1.16" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "8d9b05277c7e8da2c93a568989bb6207bef0112e8d17df7a6eda4a3cf143bc5e" +dependencies = [ + "bytes", + "futures-channel", + "futures-core", + "futures-util", + "http", + "http-body", + "hyper", + "libc", + "pin-project-lite", + "socket2 0.6.0", + "tokio", + "tower-service", + "tracing", +] + +[[package]] +name = "iana-time-zone" +version = "0.1.63" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "b0c919e5debc312ad217002b8048a17b7d83f80703865bbfcfebb0458b0b27d8" +dependencies = [ + "android_system_properties", + "core-foundation-sys", + "iana-time-zone-haiku", + "js-sys", + "log", + "wasm-bindgen", + "windows-core", +] + +[[package]] +name = "iana-time-zone-haiku" +version = "0.1.2" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "f31827a206f56af32e590ba56d5d2d085f558508192593743f16b2306495269f" +dependencies = [ + "cc", +] + +[[package]] +name = "icu_collections" +version = "2.0.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "200072f5d0e3614556f94a9930d5dc3e0662a652823904c3a75dc3b0af7fee47" +dependencies = [ + "displaydoc", + "potential_utf", + "yoke", + "zerofrom", + "zerovec", +] + +[[package]] +name = "icu_locale_core" +version = "2.0.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "0cde2700ccaed3872079a65fb1a78f6c0a36c91570f28755dda67bc8f7d9f00a" +dependencies = [ + "displaydoc", + "litemap", + "tinystr", + "writeable", + "zerovec", +] + +[[package]] +name = "icu_normalizer" +version = "2.0.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "436880e8e18df4d7bbc06d58432329d6458cc84531f7ac5f024e93deadb37979" +dependencies = [ + "displaydoc", + "icu_collections", + "icu_normalizer_data", + "icu_properties", + "icu_provider", + "smallvec", + "zerovec", +] + +[[package]] +name = "icu_normalizer_data" +version = "2.0.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "00210d6893afc98edb752b664b8890f0ef174c8adbb8d0be9710fa66fbbf72d3" + +[[package]] +name = "icu_properties" +version = "2.0.1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "016c619c1eeb94efb86809b015c58f479963de65bdb6253345c1a1276f22e32b" +dependencies = [ + "displaydoc", + "icu_collections", + "icu_locale_core", + "icu_properties_data", + "icu_provider", + "potential_utf", + "zerotrie", + "zerovec", +] + +[[package]] +name = "icu_properties_data" +version = "2.0.1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "298459143998310acd25ffe6810ed544932242d3f07083eee1084d83a71bd632" + +[[package]] +name = "icu_provider" +version = "2.0.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "03c80da27b5f4187909049ee2d72f276f0d9f99a42c306bd0131ecfe04d8e5af" +dependencies = [ + "displaydoc", + "icu_locale_core", + "stable_deref_trait", + "tinystr", + "writeable", + "yoke", + "zerofrom", + "zerotrie", + "zerovec", +] + +[[package]] +name = "idna" +version = "1.1.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "3b0875f23caa03898994f6ddc501886a45c7d3d62d04d2d90788d47be1b1e4de" +dependencies = [ + "idna_adapter", + "smallvec", + "utf8_iter", +] + +[[package]] +name = "idna_adapter" +version = "1.2.1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "3acae9609540aa318d1bc588455225fb2085b9ed0c4f6bd0d9d5bcd86f1a0344" +dependencies = [ + "icu_normalizer", + "icu_properties", +] + +[[package]] +name = "indexmap" +version = "2.11.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "f2481980430f9f78649238835720ddccc57e52df14ffce1c6f37391d61b563e9" +dependencies = [ + "equivalent", + "hashbrown", +] + +[[package]] +name = "io-uring" +version = "0.7.10" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "046fa2d4d00aea763528b4950358d0ead425372445dc8ff86312b3c69ff7727b" +dependencies = [ + "bitflags", + "cfg-if", + "libc", +] + +[[package]] +name = "is-terminal" +version = "0.4.16" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "e04d7f318608d35d4b61ddd75cbdaee86b023ebe2bd5a66ee0915f0bf93095a9" +dependencies = [ + "hermit-abi", + "libc", + "windows-sys 0.59.0", +] + +[[package]] +name = "is_terminal_polyfill" +version = "1.70.1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "7943c866cc5cd64cbc25b2e01621d07fa8eb2a1a23160ee81ce38704e97b8ecf" + +[[package]] +name = "itoa" +version = "1.0.15" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "4a5f13b858c8d314ee3e8f639011f7ccefe71f97f96e50151fb991f267928e2c" + +[[package]] +name = "jiff" +version = "0.2.15" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "be1f93b8b1eb69c77f24bbb0afdf66f54b632ee39af40ca21c4365a1d7347e49" +dependencies = [ + "jiff-static", + "log", + "portable-atomic", + "portable-atomic-util", + "serde", +] + +[[package]] +name = "jiff-static" +version = "0.2.15" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "03343451ff899767262ec32146f6d559dd759fdadf42ff0e227c7c48f72594b4" +dependencies = [ + "proc-macro2", + "quote", + "syn", +] + +[[package]] +name = "jni" +version = "0.21.1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "1a87aa2bb7d2af34197c04845522473242e1aa17c12f4935d5856491a7fb8c97" +dependencies = [ + "cesu8", + "cfg-if", + "combine", + "jni-sys", + "log", + "thiserror", + "walkdir", + "windows-sys 0.45.0", +] + +[[package]] +name = "jni-sys" +version = "0.3.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "8eaf4bc02d17cbdd7ff4c7438cafcdf7fb9a4613313ad11b4f8fefe7d3fa0130" + +[[package]] +name = "js-sys" +version = "0.3.77" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "1cfaf33c695fc6e08064efbc1f72ec937429614f25eef83af942d0e227c3a28f" +dependencies = [ + "once_cell", + "wasm-bindgen", +] + +[[package]] +name = "jsonrpsee" +version = "0.24.9" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "37b26c20e2178756451cfeb0661fb74c47dd5988cb7e3939de7e9241fd604d42" +dependencies = [ + "jsonrpsee-core", + "jsonrpsee-http-client", + "jsonrpsee-proc-macros", + "jsonrpsee-server", + "jsonrpsee-types", + "tokio", + "tracing", +] + +[[package]] +name = "jsonrpsee-core" +version = "0.24.9" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "456196007ca3a14db478346f58c7238028d55ee15c1df15115596e411ff27925" +dependencies = [ + "async-trait", + "bytes", + "futures-util", + "http", + "http-body", + "http-body-util", + "jsonrpsee-types", + "parking_lot", + "rand", + "rustc-hash", + "serde", + "serde_json", + "thiserror", + "tokio", + "tracing", +] + +[[package]] +name = "jsonrpsee-http-client" +version = "0.24.9" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "c872b6c9961a4ccc543e321bb5b89f6b2d2c7fe8b61906918273a3333c95400c" +dependencies = [ + "async-trait", + "base64", + "http-body", + "hyper", + "hyper-rustls", + "hyper-util", + "jsonrpsee-core", + "jsonrpsee-types", + "rustls", + "rustls-platform-verifier", + "serde", + "serde_json", + "thiserror", + "tokio", + "tower", + "tracing", + "url", +] + +[[package]] +name = "jsonrpsee-proc-macros" +version = "0.24.9" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "5e65763c942dfc9358146571911b0cd1c361c2d63e2d2305622d40d36376ca80" +dependencies = [ + "heck", + "proc-macro-crate", + "proc-macro2", + "quote", + "syn", +] + +[[package]] +name = "jsonrpsee-server" +version = "0.24.9" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "55e363146da18e50ad2b51a0a7925fc423137a0b1371af8235b1c231a0647328" +dependencies = [ + "futures-util", + "http", + "http-body", + "http-body-util", + "hyper", + "hyper-util", + "jsonrpsee-core", + "jsonrpsee-types", + "pin-project", + "route-recognizer", + "serde", + "serde_json", + "soketto", + "thiserror", + "tokio", + "tokio-stream", + "tokio-util", + "tower", + "tracing", +] + +[[package]] +name = "jsonrpsee-types" +version = "0.24.9" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "08a8e70baf945b6b5752fc8eb38c918a48f1234daf11355e07106d963f860089" +dependencies = [ + "http", + "serde", + "serde_json", + "thiserror", +] + +[[package]] +name = "libc" +version = "0.2.175" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "6a82ae493e598baaea5209805c49bbf2ea7de956d50d7da0da1164f9c6d28543" + +[[package]] +name = "litemap" +version = "0.8.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "241eaef5fd12c88705a01fc1066c48c4b36e0dd4377dcdc7ec3942cea7a69956" + +[[package]] +name = "lock_api" +version = "0.4.13" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "96936507f153605bddfcda068dd804796c84324ed2510809e5b2a624c81da765" +dependencies = [ + "autocfg", + "scopeguard", +] + +[[package]] +name = "log" +version = "0.4.27" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "13dc2df351e3202783a1fe0d44375f7295ffb4049267b0f3018346dc122a1d94" + +[[package]] +name = "memchr" +version = "2.7.5" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "32a282da65faaf38286cf3be983213fcf1d2e2a58700e808f83f4ea9a4804bc0" + +[[package]] +name = "miniz_oxide" +version = "0.8.9" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "1fa76a2c86f704bdb222d66965fb3d63269ce38518b83cb0575fca855ebb6316" +dependencies = [ + "adler2", +] + +[[package]] +name = "mio" +version = "1.0.4" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "78bed444cc8a2160f01cbcf811ef18cac863ad68ae8ca62092e8db51d51c761c" +dependencies = [ + "libc", + "wasi 0.11.1+wasi-snapshot-preview1", + "windows-sys 0.59.0", +] + +[[package]] +name = "num-conv" +version = "0.1.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "51d515d32fb182ee37cda2ccdcb92950d6a3c2893aa280e540671c2cd0f3b1d9" + +[[package]] +name = "num-traits" +version = "0.2.19" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "071dfc062690e90b734c0b2273ce72ad0ffa95f0c74596bc250dcfd960262841" +dependencies = [ + "autocfg", +] + +[[package]] +name = "object" +version = "0.36.7" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "62948e14d923ea95ea2c7c86c71013138b66525b86bdc08d2dcc262bdb497b87" +dependencies = [ + "memchr", +] + +[[package]] +name = "once_cell" +version = "1.21.3" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "42f5e15c9953c5e4ccceeb2e7382a716482c34515315f7b03532b8b4e8393d2d" + +[[package]] +name = "once_cell_polyfill" +version = "1.70.1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "a4895175b425cb1f87721b59f0f286c2092bd4af812243672510e1ac53e2e0ad" + +[[package]] +name = "openssl-probe" +version = "0.1.6" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "d05e27ee213611ffe7d6348b942e8f942b37114c00cc03cec254295a4a17852e" + +[[package]] +name = "parking_lot" +version = "0.12.4" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "70d58bf43669b5795d1576d0641cfb6fbb2057bf629506267a92807158584a13" +dependencies = [ + "lock_api", + "parking_lot_core", +] + +[[package]] +name = "parking_lot_core" +version = "0.9.11" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "bc838d2a56b5b1a6c25f55575dfc605fabb63bb2365f6c2353ef9159aa69e4a5" +dependencies = [ + "cfg-if", + "libc", + "redox_syscall", + "smallvec", + "windows-targets 0.52.6", +] + +[[package]] +name = "percent-encoding" +version = "2.3.2" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "9b4f627cb1b25917193a259e49bdad08f671f8d9708acfd5fe0a8c1455d87220" + +[[package]] +name = "pin-project" +version = "1.1.10" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "677f1add503faace112b9f1373e43e9e054bfdd22ff1a63c1bc485eaec6a6a8a" +dependencies = [ + "pin-project-internal", +] + +[[package]] +name = "pin-project-internal" +version = "1.1.10" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "6e918e4ff8c4549eb882f14b3a4bc8c8bc93de829416eacf579f1207a8fbf861" +dependencies = [ + "proc-macro2", + "quote", + "syn", +] + +[[package]] +name = "pin-project-lite" +version = "0.2.16" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "3b3cff922bd51709b605d9ead9aa71031d81447142d828eb4a6eba76fe619f9b" + +[[package]] +name = "pin-utils" +version = "0.1.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "8b870d8c151b6f2fb93e84a13146138f05d02ed11c7e7c54f8826aaaf7c9f184" + +[[package]] +name = "plist" +version = "1.7.4" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "3af6b589e163c5a788fab00ce0c0366f6efbb9959c2f9874b224936af7fce7e1" +dependencies = [ + "base64", + "indexmap", + "quick-xml", + "serde", + "time", +] + +[[package]] +name = "portable-atomic" +version = "1.11.1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "f84267b20a16ea918e43c6a88433c2d54fa145c92a811b5b047ccbe153674483" + +[[package]] +name = "portable-atomic-util" +version = "0.2.4" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "d8a2f0d8d040d7848a709caf78912debcc3f33ee4b3cac47d73d1e1069e83507" +dependencies = [ + "portable-atomic", +] + +[[package]] +name = "potential_utf" +version = "0.1.2" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "e5a7c30837279ca13e7c867e9e40053bc68740f988cb07f7ca6df43cc734b585" +dependencies = [ + "zerovec", +] + +[[package]] +name = "powerfmt" +version = "0.2.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "439ee305def115ba05938db6eb1644ff94165c5ab5e9420d1c1bcedbba909391" + +[[package]] +name = "ppv-lite86" +version = "0.2.21" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "85eae3c4ed2f50dcfe72643da4befc30deadb458a9b590d720cde2f2b1e97da9" +dependencies = [ + "zerocopy", +] + +[[package]] +name = "proc-macro-crate" +version = "3.3.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "edce586971a4dfaa28950c6f18ed55e0406c1ab88bbce2c6f6293a7aaba73d35" +dependencies = [ + "toml_edit", +] + +[[package]] +name = "proc-macro2" +version = "1.0.101" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "89ae43fd86e4158d6db51ad8e2b80f313af9cc74f5c0e03ccb87de09998732de" +dependencies = [ + "unicode-ident", +] + +[[package]] +name = "quick-xml" +version = "0.38.2" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "d200a41a7797e6461bd04e4e95c3347053a731c32c87f066f2f0dda22dbdbba8" +dependencies = [ + "memchr", +] + +[[package]] +name = "quote" +version = "1.0.40" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "1885c039570dc00dcb4ff087a89e185fd56bae234ddc7f056a945bf36467248d" +dependencies = [ + "proc-macro2", +] + +[[package]] +name = "r-efi" +version = "5.3.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "69cdb34c158ceb288df11e18b4bd39de994f6657d83847bdffdbd7f346754b0f" + +[[package]] +name = "rand" +version = "0.8.5" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "34af8d1a0e25924bc5b7c43c079c942339d8f0a8b57c39049bef581b46327404" +dependencies = [ + "libc", + "rand_chacha", + "rand_core", +] + +[[package]] +name = "rand_chacha" +version = "0.3.1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "e6c10a63a0fa32252be49d21e7709d4d4baf8d231c2dbce1eaa8141b9b127d88" +dependencies = [ + "ppv-lite86", + "rand_core", +] + +[[package]] +name = "rand_core" +version = "0.6.4" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "ec0be4795e2f6a28069bec0b5ff3e2ac9bafc99e6a9a7dc3547996c5c816922c" +dependencies = [ + "getrandom 0.2.16", +] + +[[package]] +name = "redis" +version = "0.25.4" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "e0d7a6955c7511f60f3ba9e86c6d02b3c3f144f8c24b288d1f4e18074ab8bbec" +dependencies = [ + "async-trait", + "bytes", + "combine", + "futures-util", + "itoa", + "percent-encoding", + "pin-project-lite", + "ryu", + "sha1_smol", + "socket2 0.5.10", + "tokio", + "tokio-util", + "url", +] + +[[package]] +name = "redox_syscall" +version = "0.5.17" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "5407465600fb0548f1442edf71dd20683c6ed326200ace4b1ef0763521bb3b77" +dependencies = [ + "bitflags", +] + +[[package]] +name = "regex" +version = "1.11.1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "b544ef1b4eac5dc2db33ea63606ae9ffcfac26c1416a2806ae0bf5f56b201191" +dependencies = [ + "aho-corasick", + "memchr", + "regex-automata", + "regex-syntax", +] + +[[package]] +name = "regex-automata" +version = "0.4.9" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "809e8dc61f6de73b46c85f4c96486310fe304c434cfa43669d7b40f711150908" +dependencies = [ + "aho-corasick", + "memchr", + "regex-syntax", +] + +[[package]] +name = "regex-syntax" +version = "0.8.5" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "2b15c43186be67a4fd63bee50d0303afffcef381492ebe2c5d87f324e1b8815c" + +[[package]] +name = "ring" +version = "0.17.14" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "a4689e6c2294d81e88dc6261c768b63bc4fcdb852be6d1352498b114f61383b7" +dependencies = [ + "cc", + "cfg-if", + "getrandom 0.2.16", + "libc", + "untrusted", + "windows-sys 0.52.0", +] + +[[package]] +name = "route-recognizer" +version = "0.3.1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "afab94fb28594581f62d981211a9a4d53cc8130bbcbbb89a0440d9b8e81a7746" + +[[package]] +name = "rustc-demangle" +version = "0.1.26" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "56f7d92ca342cea22a06f2121d944b4fd82af56988c270852495420f961d4ace" + +[[package]] +name = "rustc-hash" +version = "2.1.1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "357703d41365b4b27c590e3ed91eabb1b663f07c4c084095e60cbed4362dff0d" + +[[package]] +name = "rustls" +version = "0.23.31" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "c0ebcbd2f03de0fc1122ad9bb24b127a5a6cd51d72604a3f3c50ac459762b6cc" +dependencies = [ + "log", + "once_cell", + "ring", + "rustls-pki-types", + "rustls-webpki", + "subtle", + "zeroize", +] + +[[package]] +name = "rustls-native-certs" +version = "0.8.1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "7fcff2dd52b58a8d98a70243663a0d234c4e2b79235637849d15913394a247d3" +dependencies = [ + "openssl-probe", + "rustls-pki-types", + "schannel", + "security-framework", +] + +[[package]] +name = "rustls-pki-types" +version = "1.12.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "229a4a4c221013e7e1f1a043678c5cc39fe5171437c88fb47151a21e6f5b5c79" +dependencies = [ + "zeroize", +] + +[[package]] +name = "rustls-platform-verifier" +version = "0.5.3" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "19787cda76408ec5404443dc8b31795c87cd8fec49762dc75fa727740d34acc1" +dependencies = [ + "core-foundation", + "core-foundation-sys", + "jni", + "log", + "once_cell", + "rustls", + "rustls-native-certs", + "rustls-platform-verifier-android", + "rustls-webpki", + "security-framework", + "security-framework-sys", + "webpki-root-certs 0.26.11", + "windows-sys 0.59.0", +] + +[[package]] +name = "rustls-platform-verifier-android" +version = "0.1.1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "f87165f0995f63a9fbeea62b64d10b4d9d8e78ec6d7d51fb2125fda7bb36788f" + +[[package]] +name = "rustls-webpki" +version = "0.103.4" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "0a17884ae0c1b773f1ccd2bd4a8c72f16da897310a98b0e84bf349ad5ead92fc" +dependencies = [ + "ring", + "rustls-pki-types", + "untrusted", +] + +[[package]] +name = "rustversion" +version = "1.0.22" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "b39cdef0fa800fc44525c84ccb54a029961a8215f9619753635a9c0d2538d46d" + +[[package]] +name = "ryu" +version = "1.0.20" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "28d3b2b1366ec20994f1fd18c3c594f05c5dd4bc44d8bb0c1c632c8d6829481f" + +[[package]] +name = "sal-service-manager" +version = "0.1.0" +dependencies = [ + "async-trait", + "chrono", + "futures", + "log", + "once_cell", + "plist", + "serde", + "serde_json", + "thiserror", + "tokio", + "zinit-client", +] + +[[package]] +name = "same-file" +version = "1.0.6" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "93fc1dc3aaa9bfed95e02e6eadabb4baf7e3078b0bd1b4d7b6b0b68378900502" +dependencies = [ + "winapi-util", +] + +[[package]] +name = "schannel" +version = "0.1.27" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "1f29ebaa345f945cec9fbbc532eb307f0fdad8161f281b6369539c8d84876b3d" +dependencies = [ + "windows-sys 0.59.0", +] + +[[package]] +name = "scopeguard" +version = "1.2.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "94143f37725109f92c262ed2cf5e59bce7498c01bcc1502d7b9afe439a4e9f49" + +[[package]] +name = "security-framework" +version = "3.3.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "80fb1d92c5028aa318b4b8bd7302a5bfcf48be96a37fc6fc790f806b0004ee0c" +dependencies = [ + "bitflags", + "core-foundation", + "core-foundation-sys", + "libc", + "security-framework-sys", +] + +[[package]] +name = "security-framework-sys" +version = "2.14.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "49db231d56a190491cb4aeda9527f1ad45345af50b0851622a7adb8c03b01c32" +dependencies = [ + "core-foundation-sys", + "libc", +] + +[[package]] +name = "serde" +version = "1.0.219" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "5f0e2c6ed6606019b4e29e69dbaba95b11854410e5347d525002456dbbb786b6" +dependencies = [ + "serde_derive", +] + +[[package]] +name = "serde_derive" +version = "1.0.219" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "5b0276cf7f2c73365f7157c8123c21cd9a50fbbd844757af28ca1f5925fc2a00" +dependencies = [ + "proc-macro2", + "quote", + "syn", +] + +[[package]] +name = "serde_json" +version = "1.0.143" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "d401abef1d108fbd9cbaebc3e46611f4b1021f714a0597a71f41ee463f5f4a5a" +dependencies = [ + "itoa", + "memchr", + "ryu", + "serde", +] + +[[package]] +name = "serde_spanned" +version = "0.6.9" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "bf41e0cfaf7226dca15e8197172c295a782857fcb97fad1808a166870dee75a3" +dependencies = [ + "serde", +] + +[[package]] +name = "sha1" +version = "0.10.6" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "e3bf829a2d51ab4a5ddf1352d8470c140cadc8301b2ae1789db023f01cedd6ba" +dependencies = [ + "cfg-if", + "cpufeatures", + "digest", +] + +[[package]] +name = "sha1_smol" +version = "1.0.1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "bbfa15b3dddfee50a0fff136974b3e1bde555604ba463834a7eb7deb6417705d" + +[[package]] +name = "shlex" +version = "1.3.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "0fda2ff0d084019ba4d7c6f371c95d8fd75ce3524c3cb8fb653a3023f6323e64" + +[[package]] +name = "signal-hook-registry" +version = "1.4.6" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "b2a4719bff48cee6b39d12c020eeb490953ad2443b7055bd0b21fca26bd8c28b" +dependencies = [ + "libc", +] + +[[package]] +name = "slab" +version = "0.4.11" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "7a2ae44ef20feb57a68b23d846850f861394c2e02dc425a50098ae8c90267589" + +[[package]] +name = "smallvec" +version = "1.15.1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "67b1b7a3b5fe4f1376887184045fcf45c69e92af734b7aaddc05fb777b6fbd03" + +[[package]] +name = "socket2" +version = "0.5.10" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "e22376abed350d73dd1cd119b57ffccad95b4e585a7cda43e286245ce23c0678" +dependencies = [ + "libc", + "windows-sys 0.52.0", +] + +[[package]] +name = "socket2" +version = "0.6.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "233504af464074f9d066d7b5416c5f9b894a5862a6506e306f7b816cdd6f1807" +dependencies = [ + "libc", + "windows-sys 0.59.0", +] + +[[package]] +name = "soketto" +version = "0.8.1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "2e859df029d160cb88608f5d7df7fb4753fd20fdfb4de5644f3d8b8440841721" +dependencies = [ + "base64", + "bytes", + "futures", + "http", + "httparse", + "log", + "rand", + "sha1", +] + +[[package]] +name = "stable_deref_trait" +version = "1.2.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "a8f112729512f8e442d81f95a8a7ddf2b7c6b8a1a6f509a95864142b30cab2d3" + +[[package]] +name = "strsim" +version = "0.11.1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "7da8b5736845d9f2fcb837ea5d9e2628564b3b043a70948a3f0b778838c5fb4f" + +[[package]] +name = "subtle" +version = "2.6.1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "13c2bddecc57b384dee18652358fb23172facb8a2c51ccc10d74c157bdea3292" + +[[package]] +name = "syn" +version = "2.0.106" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "ede7c438028d4436d71104916910f5bb611972c5cfd7f89b8300a8186e6fada6" +dependencies = [ + "proc-macro2", + "quote", + "unicode-ident", +] + +[[package]] +name = "synstructure" +version = "0.13.2" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "728a70f3dbaf5bab7f0c4b1ac8d7ae5ea60a4b5549c8a5914361c99147a709d2" +dependencies = [ + "proc-macro2", + "quote", + "syn", +] + +[[package]] +name = "termcolor" +version = "1.4.1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "06794f8f6c5c898b3275aebefa6b8a1cb24cd2c6c79397ab15774837a0bc5755" +dependencies = [ + "winapi-util", +] + +[[package]] +name = "thiserror" +version = "1.0.69" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "b6aaf5339b578ea85b50e080feb250a3e8ae8cfcdff9a461c9ec2904bc923f52" +dependencies = [ + "thiserror-impl", +] + +[[package]] +name = "thiserror-impl" +version = "1.0.69" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "4fee6c4efc90059e10f81e6d42c60a18f76588c3d74cb83a0b242a2b6c7504c1" +dependencies = [ + "proc-macro2", + "quote", + "syn", +] + +[[package]] +name = "time" +version = "0.3.41" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "8a7619e19bc266e0f9c5e6686659d394bc57973859340060a69221e57dbc0c40" +dependencies = [ + "deranged", + "itoa", + "num-conv", + "powerfmt", + "serde", + "time-core", + "time-macros", +] + +[[package]] +name = "time-core" +version = "0.1.4" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "c9e9a38711f559d9e3ce1cdb06dd7c5b8ea546bc90052da6d06bb76da74bb07c" + +[[package]] +name = "time-macros" +version = "0.2.22" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "3526739392ec93fd8b359c8e98514cb3e8e021beb4e5f597b00a0221f8ed8a49" +dependencies = [ + "num-conv", + "time-core", +] + +[[package]] +name = "tinystr" +version = "0.8.1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "5d4f6d1145dcb577acf783d4e601bc1d76a13337bb54e6233add580b07344c8b" +dependencies = [ + "displaydoc", + "zerovec", +] + +[[package]] +name = "tokio" +version = "1.47.1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "89e49afdadebb872d3145a5638b59eb0691ea23e46ca484037cfab3b76b95038" +dependencies = [ + "backtrace", + "bytes", + "io-uring", + "libc", + "mio", + "parking_lot", + "pin-project-lite", + "signal-hook-registry", + "slab", + "socket2 0.6.0", + "tokio-macros", + "windows-sys 0.59.0", +] + +[[package]] +name = "tokio-macros" +version = "2.5.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "6e06d43f1345a3bcd39f6a56dbb7dcab2ba47e68e8ac134855e7e2bdbaf8cab8" +dependencies = [ + "proc-macro2", + "quote", + "syn", +] + +[[package]] +name = "tokio-rustls" +version = "0.26.2" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "8e727b36a1a0e8b74c376ac2211e40c2c8af09fb4013c60d910495810f008e9b" +dependencies = [ + "rustls", + "tokio", +] + +[[package]] +name = "tokio-stream" +version = "0.1.17" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "eca58d7bba4a75707817a2c44174253f9236b2d5fbd055602e9d5c07c139a047" +dependencies = [ + "futures-core", + "pin-project-lite", + "tokio", + "tokio-util", +] + +[[package]] +name = "tokio-test" +version = "0.4.4" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "2468baabc3311435b55dd935f702f42cd1b8abb7e754fb7dfb16bd36aa88f9f7" +dependencies = [ + "async-stream", + "bytes", + "futures-core", + "tokio", + "tokio-stream", +] + +[[package]] +name = "tokio-util" +version = "0.7.16" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "14307c986784f72ef81c89db7d9e28d6ac26d16213b109ea501696195e6e3ce5" +dependencies = [ + "bytes", + "futures-core", + "futures-io", + "futures-sink", + "pin-project-lite", + "tokio", +] + +[[package]] +name = "toml" +version = "0.8.23" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "dc1beb996b9d83529a9e75c17a1686767d148d70663143c7854d8b4a09ced362" +dependencies = [ + "serde", + "serde_spanned", + "toml_datetime", + "toml_edit", +] + +[[package]] +name = "toml_datetime" +version = "0.6.11" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "22cddaf88f4fbc13c51aebbf5f8eceb5c7c5a9da2ac40a13519eb5b0a0e8f11c" +dependencies = [ + "serde", +] + +[[package]] +name = "toml_edit" +version = "0.22.27" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "41fe8c660ae4257887cf66394862d21dbca4a6ddd26f04a3560410406a2f819a" +dependencies = [ + "indexmap", + "serde", + "serde_spanned", + "toml_datetime", + "toml_write", + "winnow", +] + +[[package]] +name = "toml_write" +version = "0.1.2" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "5d99f8c9a7727884afe522e9bd5edbfc91a3312b36a77b5fb8926e4c31a41801" + +[[package]] +name = "tower" +version = "0.4.13" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "b8fa9be0de6cf49e536ce1851f987bd21a43b771b09473c3549a6c853db37c1c" +dependencies = [ + "futures-core", + "futures-util", + "pin-project", + "pin-project-lite", + "tower-layer", + "tower-service", + "tracing", +] + +[[package]] +name = "tower-http" +version = "0.5.2" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "1e9cd434a998747dd2c4276bc96ee2e0c7a2eadf3cae88e52be55a05fa9053f5" +dependencies = [ + "bitflags", + "bytes", + "http", + "http-body", + "http-body-util", + "pin-project-lite", + "tower-layer", + "tower-service", +] + +[[package]] +name = "tower-layer" +version = "0.3.3" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "121c2a6cda46980bb0fcd1647ffaf6cd3fc79a013de288782836f6df9c48780e" + +[[package]] +name = "tower-service" +version = "0.3.3" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "8df9b6e13f2d32c91b9bd719c00d1958837bc7dec474d94952798cc8e69eeec3" + +[[package]] +name = "tracing" +version = "0.1.41" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "784e0ac535deb450455cbfa28a6f0df145ea1bb7ae51b821cf5e7927fdcfbdd0" +dependencies = [ + "log", + "pin-project-lite", + "tracing-attributes", + "tracing-core", +] + +[[package]] +name = "tracing-attributes" +version = "0.1.30" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "81383ab64e72a7a8b8e13130c49e3dab29def6d0c7d76a03087b3cf71c5c6903" +dependencies = [ + "proc-macro2", + "quote", + "syn", +] + +[[package]] +name = "tracing-core" +version = "0.1.34" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "b9d12581f227e93f094d3af2ae690a574abb8a2b9b7a96e7cfe9647b2b617678" +dependencies = [ + "once_cell", +] + +[[package]] +name = "try-lock" +version = "0.2.5" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "e421abadd41a4225275504ea4d6566923418b7f05506fbc9c0fe86ba7396114b" + +[[package]] +name = "typenum" +version = "1.18.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "1dccffe3ce07af9386bfd29e80c0ab1a8205a2fc34e4bcd40364df902cfa8f3f" + +[[package]] +name = "unicode-ident" +version = "1.0.18" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "5a5f39404a5da50712a4c1eecf25e90dd62b613502b7e925fd4e4d19b5c96512" + +[[package]] +name = "untrusted" +version = "0.9.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "8ecb6da28b8a351d773b68d5825ac39017e680750f980f3a1a85cd8dd28a47c1" + +[[package]] +name = "url" +version = "2.5.6" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "137a3c834eaf7139b73688502f3f1141a0337c5d8e4d9b536f9b8c796e26a7c4" +dependencies = [ + "form_urlencoded", + "idna", + "percent-encoding", +] + +[[package]] +name = "utf8_iter" +version = "1.0.4" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "b6c140620e7ffbb22c2dee59cafe6084a59b5ffc27a8859a5f0d494b5d52b6be" + +[[package]] +name = "utf8parse" +version = "0.2.2" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "06abde3611657adf66d383f00b093d7faecc7fa57071cce2578660c9f1010821" + +[[package]] +name = "uuid" +version = "1.18.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "f33196643e165781c20a5ead5582283a7dacbb87855d867fbc2df3f81eddc1be" +dependencies = [ + "getrandom 0.3.3", + "js-sys", + "serde", + "wasm-bindgen", +] + +[[package]] +name = "version_check" +version = "0.9.5" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "0b928f33d975fc6ad9f86c8f283853ad26bdd5b10b7f1542aa2fa15e2289105a" + +[[package]] +name = "walkdir" +version = "2.5.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "29790946404f91d9c5d06f9874efddea1dc06c5efe94541a7d6863108e3a5e4b" +dependencies = [ + "same-file", + "winapi-util", +] + +[[package]] +name = "want" +version = "0.3.1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "bfa7760aed19e106de2c7c0b581b509f2f25d3dacaf737cb82ac61bc6d760b0e" +dependencies = [ + "try-lock", +] + +[[package]] +name = "wasi" +version = "0.11.1+wasi-snapshot-preview1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "ccf3ec651a847eb01de73ccad15eb7d99f80485de043efb2f370cd654f4ea44b" + +[[package]] +name = "wasi" +version = "0.14.2+wasi-0.2.4" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "9683f9a5a998d873c0d21fcbe3c083009670149a8fab228644b8bd36b2c48cb3" +dependencies = [ + "wit-bindgen-rt", +] + +[[package]] +name = "wasm-bindgen" +version = "0.2.100" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "1edc8929d7499fc4e8f0be2262a241556cfc54a0bea223790e71446f2aab1ef5" +dependencies = [ + "cfg-if", + "once_cell", + "rustversion", + "wasm-bindgen-macro", +] + +[[package]] +name = "wasm-bindgen-backend" +version = "0.2.100" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "2f0a0651a5c2bc21487bde11ee802ccaf4c51935d0d3d42a6101f98161700bc6" +dependencies = [ + "bumpalo", + "log", + "proc-macro2", + "quote", + "syn", + "wasm-bindgen-shared", +] + +[[package]] +name = "wasm-bindgen-futures" +version = "0.4.50" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "555d470ec0bc3bb57890405e5d4322cc9ea83cebb085523ced7be4144dac1e61" +dependencies = [ + "cfg-if", + "js-sys", + "once_cell", + "wasm-bindgen", + "web-sys", +] + +[[package]] +name = "wasm-bindgen-macro" +version = "0.2.100" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "7fe63fc6d09ed3792bd0897b314f53de8e16568c2b3f7982f468c0bf9bd0b407" +dependencies = [ + "quote", + "wasm-bindgen-macro-support", +] + +[[package]] +name = "wasm-bindgen-macro-support" +version = "0.2.100" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "8ae87ea40c9f689fc23f209965b6fb8a99ad69aeeb0231408be24920604395de" +dependencies = [ + "proc-macro2", + "quote", + "syn", + "wasm-bindgen-backend", + "wasm-bindgen-shared", +] + +[[package]] +name = "wasm-bindgen-shared" +version = "0.2.100" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "1a05d73b933a847d6cccdda8f838a22ff101ad9bf93e33684f39c1f5f0eece3d" +dependencies = [ + "unicode-ident", +] + +[[package]] +name = "web-sys" +version = "0.3.77" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "33b6dd2ef9186f1f2072e409e99cd22a975331a6b3591b12c764e0e55c60d5d2" +dependencies = [ + "js-sys", + "wasm-bindgen", +] + +[[package]] +name = "webpki-root-certs" +version = "0.26.11" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "75c7f0ef91146ebfb530314f5f1d24528d7f0767efbfd31dce919275413e393e" +dependencies = [ + "webpki-root-certs 1.0.2", +] + +[[package]] +name = "webpki-root-certs" +version = "1.0.2" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "4e4ffd8df1c57e87c325000a3d6ef93db75279dc3a231125aac571650f22b12a" +dependencies = [ + "rustls-pki-types", +] + +[[package]] +name = "winapi-util" +version = "0.1.10" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "0978bf7171b3d90bac376700cb56d606feb40f251a475a5d6634613564460b22" +dependencies = [ + "windows-sys 0.60.2", +] + +[[package]] +name = "windows-core" +version = "0.61.2" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "c0fdd3ddb90610c7638aa2b3a3ab2904fb9e5cdbecc643ddb3647212781c4ae3" +dependencies = [ + "windows-implement", + "windows-interface", + "windows-link", + "windows-result", + "windows-strings", +] + +[[package]] +name = "windows-implement" +version = "0.60.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "a47fddd13af08290e67f4acabf4b459f647552718f683a7b415d290ac744a836" +dependencies = [ + "proc-macro2", + "quote", + "syn", +] + +[[package]] +name = "windows-interface" +version = "0.59.1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "bd9211b69f8dcdfa817bfd14bf1c97c9188afa36f4750130fcdf3f400eca9fa8" +dependencies = [ + "proc-macro2", + "quote", + "syn", +] + +[[package]] +name = "windows-link" +version = "0.1.3" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "5e6ad25900d524eaabdbbb96d20b4311e1e7ae1699af4fb28c17ae66c80d798a" + +[[package]] +name = "windows-result" +version = "0.3.4" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "56f42bd332cc6c8eac5af113fc0c1fd6a8fd2aa08a0119358686e5160d0586c6" +dependencies = [ + "windows-link", +] + +[[package]] +name = "windows-strings" +version = "0.4.2" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "56e6c93f3a0c3b36176cb1327a4958a0353d5d166c2a35cb268ace15e91d3b57" +dependencies = [ + "windows-link", +] + +[[package]] +name = "windows-sys" +version = "0.45.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "75283be5efb2831d37ea142365f009c02ec203cd29a3ebecbc093d52315b66d0" +dependencies = [ + "windows-targets 0.42.2", +] + +[[package]] +name = "windows-sys" +version = "0.52.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "282be5f36a8ce781fad8c8ae18fa3f9beff57ec1b52cb3de0789201425d9a33d" +dependencies = [ + "windows-targets 0.52.6", +] + +[[package]] +name = "windows-sys" +version = "0.59.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "1e38bc4d79ed67fd075bcc251a1c39b32a1776bbe92e5bef1f0bf1f8c531853b" +dependencies = [ + "windows-targets 0.52.6", +] + +[[package]] +name = "windows-sys" +version = "0.60.2" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "f2f500e4d28234f72040990ec9d39e3a6b950f9f22d3dba18416c35882612bcb" +dependencies = [ + "windows-targets 0.53.3", +] + +[[package]] +name = "windows-targets" +version = "0.42.2" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "8e5180c00cd44c9b1c88adb3693291f1cd93605ded80c250a75d472756b4d071" +dependencies = [ + "windows_aarch64_gnullvm 0.42.2", + "windows_aarch64_msvc 0.42.2", + "windows_i686_gnu 0.42.2", + "windows_i686_msvc 0.42.2", + "windows_x86_64_gnu 0.42.2", + "windows_x86_64_gnullvm 0.42.2", + "windows_x86_64_msvc 0.42.2", +] + +[[package]] +name = "windows-targets" +version = "0.52.6" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "9b724f72796e036ab90c1021d4780d4d3d648aca59e491e6b98e725b84e99973" +dependencies = [ + "windows_aarch64_gnullvm 0.52.6", + "windows_aarch64_msvc 0.52.6", + "windows_i686_gnu 0.52.6", + "windows_i686_gnullvm 0.52.6", + "windows_i686_msvc 0.52.6", + "windows_x86_64_gnu 0.52.6", + "windows_x86_64_gnullvm 0.52.6", + "windows_x86_64_msvc 0.52.6", +] + +[[package]] +name = "windows-targets" +version = "0.53.3" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "d5fe6031c4041849d7c496a8ded650796e7b6ecc19df1a431c1a363342e5dc91" +dependencies = [ + "windows-link", + "windows_aarch64_gnullvm 0.53.0", + "windows_aarch64_msvc 0.53.0", + "windows_i686_gnu 0.53.0", + "windows_i686_gnullvm 0.53.0", + "windows_i686_msvc 0.53.0", + "windows_x86_64_gnu 0.53.0", + "windows_x86_64_gnullvm 0.53.0", + "windows_x86_64_msvc 0.53.0", +] + +[[package]] +name = "windows_aarch64_gnullvm" +version = "0.42.2" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "597a5118570b68bc08d8d59125332c54f1ba9d9adeedeef5b99b02ba2b0698f8" + +[[package]] +name = "windows_aarch64_gnullvm" +version = "0.52.6" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "32a4622180e7a0ec044bb555404c800bc9fd9ec262ec147edd5989ccd0c02cd3" + +[[package]] +name = "windows_aarch64_gnullvm" +version = "0.53.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "86b8d5f90ddd19cb4a147a5fa63ca848db3df085e25fee3cc10b39b6eebae764" + +[[package]] +name = "windows_aarch64_msvc" +version = "0.42.2" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "e08e8864a60f06ef0d0ff4ba04124db8b0fb3be5776a5cd47641e942e58c4d43" + +[[package]] +name = "windows_aarch64_msvc" +version = "0.52.6" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "09ec2a7bb152e2252b53fa7803150007879548bc709c039df7627cabbd05d469" + +[[package]] +name = "windows_aarch64_msvc" +version = "0.53.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "c7651a1f62a11b8cbd5e0d42526e55f2c99886c77e007179efff86c2b137e66c" + +[[package]] +name = "windows_i686_gnu" +version = "0.42.2" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "c61d927d8da41da96a81f029489353e68739737d3beca43145c8afec9a31a84f" + +[[package]] +name = "windows_i686_gnu" +version = "0.52.6" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "8e9b5ad5ab802e97eb8e295ac6720e509ee4c243f69d781394014ebfe8bbfa0b" + +[[package]] +name = "windows_i686_gnu" +version = "0.53.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "c1dc67659d35f387f5f6c479dc4e28f1d4bb90ddd1a5d3da2e5d97b42d6272c3" + +[[package]] +name = "windows_i686_gnullvm" +version = "0.52.6" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "0eee52d38c090b3caa76c563b86c3a4bd71ef1a819287c19d586d7334ae8ed66" + +[[package]] +name = "windows_i686_gnullvm" +version = "0.53.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "9ce6ccbdedbf6d6354471319e781c0dfef054c81fbc7cf83f338a4296c0cae11" + +[[package]] +name = "windows_i686_msvc" +version = "0.42.2" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "44d840b6ec649f480a41c8d80f9c65108b92d89345dd94027bfe06ac444d1060" + +[[package]] +name = "windows_i686_msvc" +version = "0.52.6" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "240948bc05c5e7c6dabba28bf89d89ffce3e303022809e73deaefe4f6ec56c66" + +[[package]] +name = "windows_i686_msvc" +version = "0.53.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "581fee95406bb13382d2f65cd4a908ca7b1e4c2f1917f143ba16efe98a589b5d" + +[[package]] +name = "windows_x86_64_gnu" +version = "0.42.2" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "8de912b8b8feb55c064867cf047dda097f92d51efad5b491dfb98f6bbb70cb36" + +[[package]] +name = "windows_x86_64_gnu" +version = "0.52.6" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "147a5c80aabfbf0c7d901cb5895d1de30ef2907eb21fbbab29ca94c5b08b1a78" + +[[package]] +name = "windows_x86_64_gnu" +version = "0.53.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "2e55b5ac9ea33f2fc1716d1742db15574fd6fc8dadc51caab1c16a3d3b4190ba" + +[[package]] +name = "windows_x86_64_gnullvm" +version = "0.42.2" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "26d41b46a36d453748aedef1486d5c7a85db22e56aff34643984ea85514e94a3" + +[[package]] +name = "windows_x86_64_gnullvm" +version = "0.52.6" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "24d5b23dc417412679681396f2b49f3de8c1473deb516bd34410872eff51ed0d" + +[[package]] +name = "windows_x86_64_gnullvm" +version = "0.53.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "0a6e035dd0599267ce1ee132e51c27dd29437f63325753051e71dd9e42406c57" + +[[package]] +name = "windows_x86_64_msvc" +version = "0.42.2" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "9aec5da331524158c6d1a4ac0ab1541149c0b9505fde06423b02f5ef0106b9f0" + +[[package]] +name = "windows_x86_64_msvc" +version = "0.52.6" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "589f6da84c646204747d1270a2a5661ea66ed1cced2631d546fdfb155959f9ec" + +[[package]] +name = "windows_x86_64_msvc" +version = "0.53.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "271414315aff87387382ec3d271b52d7ae78726f5d44ac98b4f4030c91880486" + +[[package]] +name = "winnow" +version = "0.7.13" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "21a0236b59786fed61e2a80582dd500fe61f18b5dca67a4a067d0bc9039339cf" +dependencies = [ + "memchr", +] + +[[package]] +name = "wit-bindgen-rt" +version = "0.39.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "6f42320e61fe2cfd34354ecb597f86f413484a798ba44a8ca1165c58d42da6c1" +dependencies = [ + "bitflags", +] + +[[package]] +name = "writeable" +version = "0.6.1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "ea2f10b9bb0928dfb1b42b65e1f9e36f7f54dbdf08457afefb38afcdec4fa2bb" + +[[package]] +name = "yoke" +version = "0.8.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "5f41bb01b8226ef4bfd589436a297c53d118f65921786300e427be8d487695cc" +dependencies = [ + "serde", + "stable_deref_trait", + "yoke-derive", + "zerofrom", +] + +[[package]] +name = "yoke-derive" +version = "0.8.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "38da3c9736e16c5d3c8c597a9aaa5d1fa565d0532ae05e27c24aa62fb32c0ab6" +dependencies = [ + "proc-macro2", + "quote", + "syn", + "synstructure", +] + +[[package]] +name = "zerocopy" +version = "0.8.26" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "1039dd0d3c310cf05de012d8a39ff557cb0d23087fd44cad61df08fc31907a2f" +dependencies = [ + "zerocopy-derive", +] + +[[package]] +name = "zerocopy-derive" +version = "0.8.26" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "9ecf5b4cc5364572d7f4c329661bcc82724222973f2cab6f050a4e5c22f75181" +dependencies = [ + "proc-macro2", + "quote", + "syn", +] + +[[package]] +name = "zerofrom" +version = "0.1.6" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "50cc42e0333e05660c3587f3bf9d0478688e15d870fab3346451ce7f8c9fbea5" +dependencies = [ + "zerofrom-derive", +] + +[[package]] +name = "zerofrom-derive" +version = "0.1.6" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "d71e5d6e06ab090c67b5e44993ec16b72dcbaabc526db883a360057678b48502" +dependencies = [ + "proc-macro2", + "quote", + "syn", + "synstructure", +] + +[[package]] +name = "zeroize" +version = "1.8.1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "ced3678a2879b30306d323f4542626697a464a97c0a07c9aebf7ebca65cd4dde" + +[[package]] +name = "zerotrie" +version = "0.2.2" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "36f0bbd478583f79edad978b407914f61b2972f5af6fa089686016be8f9af595" +dependencies = [ + "displaydoc", + "yoke", + "zerofrom", +] + +[[package]] +name = "zerovec" +version = "0.11.4" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "e7aa2bd55086f1ab526693ecbe444205da57e25f4489879da80635a46d90e73b" +dependencies = [ + "yoke", + "zerofrom", + "zerovec-derive", +] + +[[package]] +name = "zerovec-derive" +version = "0.11.1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "5b96237efa0c878c64bd89c436f661be4e46b2f3eff1ebb976f7ef2321d2f58f" +dependencies = [ + "proc-macro2", + "quote", + "syn", +] + +[[package]] +name = "zinit-client" +version = "0.4.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "4121c3ba22f1b3ccc4546de32072c9530c7e2735b734641ada5280ac422ac9cd" +dependencies = [ + "async-stream", + "async-trait", + "chrono", + "futures", + "rand", + "serde", + "serde_json", + "thiserror", + "tokio", + "tracing", +] diff --git a/Cargo.toml b/Cargo.toml new file mode 100644 index 0000000..d5497ac --- /dev/null +++ b/Cargo.toml @@ -0,0 +1,73 @@ +[package] +name = "hero-supervisor" +version = "0.1.0" +edition = "2021" + +[dependencies] +# Async runtime +tokio = { version = "1.0", features = ["full"] } + +# Async trait support +async-trait = "0.1" + +# Redis client +redis = { version = "0.25", features = ["aio", "tokio-comp"] } + +# Job module dependencies (now integrated) +uuid = { version = "1.0", features = ["v4"] } + +# Logging +log = "0.4" +thiserror = "1.0" +chrono = "0.4" +serde = { version = "1.0", features = ["derive"] } +serde_json = "1.0" +env_logger = "0.10" +sal-service-manager = { path = "../sal/service_manager" } + +# CLI argument parsing +clap = { version = "4.0", features = ["derive"] } +toml = "0.8" + +# OpenRPC dependencies (now always included) +jsonrpsee = { version = "0.24", features = ["server", "macros"] } +anyhow = "1.0" + +# CORS support for OpenRPC server +tower-http = { version = "0.5", features = ["cors"] } +tower = "0.4" + +[dev-dependencies] +tokio-test = "0.4" +hero-supervisor-openrpc-client = { path = "clients/openrpc" } +escargot = "0.5" + +[features] +default = ["cli"] +cli = [] + +[[bin]] +name = "supervisor" +path = "cmd/supervisor.rs" + +# Examples +[[example]] +name = "openrpc_comprehensive" +path = "examples/basic_openrpc_client.rs" + +[[example]] +name = "test_queue_and_wait" +path = "examples/test_queue_and_wait.rs" + +[[example]] +name = "test_openrpc_methods" +path = "examples/test_openrpc_methods.rs" + +[[example]] +name = "mock_runner" +path = "examples/mock_runner.rs" + +[[example]] +name = "supervisor" +path = "examples/supervisor/run_supervisor.rs" + diff --git a/README.md b/README.md new file mode 100644 index 0000000..3f3fd2b --- /dev/null +++ b/README.md @@ -0,0 +1,195 @@ +# Hero Supervisor + +A Rust-based actor management system for the Hero ecosystem that provides unified process management, job queuing, and optional OpenRPC server integration. + +## Architecture Overview + +The Hero Supervisor uses a clean, feature-gated architecture that separates library functionality from CLI/server features to avoid dependency cycles and maintain modularity. + +``` +hero-supervisor/ +├── src/ # Core library (no CLI dependencies) +│ ├── lib.rs # Main library exports and documentation +│ ├── supervisor.rs # Core supervisor logic and actor management +│ ├── runner.rs # Runner implementation for actor process management +│ ├── job.rs # Job data structures, builder pattern, and Redis key management +│ └── openrpc.rs # OpenRPC server (feature-gated) +├── cmd/ # CLI binaries + +## Features + +The crate uses Rust's feature system to provide conditional compilation: + +- **`default`**: Includes all functionality - supervisor, OpenRPC server, and CLI binary +- **`cli`**: Enables the supervisor binary (included in default) + +## Architecture + +The Hero Supervisor uses a clean, simplified architecture with centralized resource management: + +### Core Components + +#### `SupervisorBuilder` → `Supervisor` → `SupervisorApp` +- **`SupervisorBuilder`**: Configures Redis URL, namespace, secrets, runners, and process manager +- **`Supervisor`**: Core engine that owns Redis client and process manager, manages runners centrally +- **`SupervisorApp`**: Main application that wraps supervisor and provides `start()` method for complete lifecycle management + +### Key Design Decisions + +- **Centralized Resources**: Supervisor exclusively owns Redis client and process manager (no per-runner instances) +- **Builder Pattern**: Flexible configuration through `SupervisorBuilder` with method chaining +- **Direct OpenRPC Integration**: RPC trait implemented directly on `Arc>` (no wrapper layers) +- **Simplified App**: `SupervisorApp::start()` handles everything - runners, OpenRPC server, graceful shutdown + +## File Documentation + +### Core Library Files + +#### `src/lib.rs` +Main library entry point that exports `Supervisor`, `SupervisorBuilder`, `SupervisorApp`, and related types. + +#### `src/supervisor.rs` +Core supervisor implementation with builder pattern. Manages runners, owns shared Redis client and process manager. Provides job queuing, runner lifecycle management, and status monitoring. + +#### `src/app.rs` +Main application wrapper that provides `start()` method for complete lifecycle management. Handles OpenRPC server startup, graceful shutdown, and keeps the application running. + +#### `src/runner.rs` +Simplified runner configuration and management. Contains `Runner` struct with configuration data only - no resource ownership. Integrates with supervisor's shared resources. + +#### `src/job.rs` +Job data structures, builder pattern, and Redis key management. Defines `Job` struct with metadata, script content, and status tracking. + +#### `src/openrpc.rs` +OpenRPC server implementation that exposes all supervisor functionality over JSON-RPC. Implements RPC trait directly on the supervisor for clean integration. + +### Binary Files + +#### `cmd/supervisor.rs` +Main supervisor binary that creates a supervisor using the builder pattern and starts the complete application with `app.start()`. The OpenRPC server is always enabled and starts automatically. + +## Usage + +### Running the Supervisor Binary + +```bash +# Run with default (error) logging +cargo run --bin supervisor + +# Run with info logging +RUST_LOG=info cargo run --bin supervisor + +# Run with debug logging +RUST_LOG=debug cargo run --bin supervisor + +# Run with trace logging (very verbose) +RUST_LOG=trace cargo run --bin supervisor + +# Run with specific module logging +RUST_LOG=hero_supervisor=debug cargo run --bin supervisor +``` + +### Library Usage + +```rust +use hero_supervisor::{SupervisorBuilder, SupervisorApp}; + +#[tokio::main] +async fn main() -> Result<(), Box> { + // Build supervisor with configuration + let supervisor = SupervisorBuilder::new() + .redis_url("redis://localhost:6379") + .namespace("hero") + .build() + .await?; + + // Create and start the complete application + let mut app = SupervisorApp::new(supervisor); + app.start().await?; + + Ok(()) +} +``` + +### As a Dependency +```toml +[dependencies] +hero-supervisor = "0.1.0" +``` + +## OpenRPC Server + +The supervisor automatically starts an OpenRPC server on `127.0.0.1:3030` that exposes all supervisor functionality via JSON-RPC. + +### Available Methods + +- `add_runner` - Add a new actor/runner +- `remove_runner` - Remove an actor/runner +- `list_runners` - List all runner IDs +- `start_runner` - Start a specific runner +- `stop_runner` - Stop a specific runner +- `get_runner_status` - Get status of a specific runner +- `get_runner_logs` - Get logs for a specific runner +- `queue_job_to_runner` - Queue a job to a specific runner +- `get_all_runner_status` - Get status of all runners +- `start_all` - Start all runners +- `stop_all` - Stop all runners +- `get_all_status` - Get status summary for all runners + +### Example JSON-RPC Call + +```bash +curl -X POST -H "Content-Type: application/json" \ + -d '{"jsonrpc":"2.0","method":"list_runners","id":1}' \ + http://127.0.0.1:3030 +``` + +## Development + +### Building +```bash +# Library only +cargo build --no-default-features + +# With CLI +cargo build --features cli + +# With OpenRPC server +cargo build --features openrpc +``` + +### Testing +```bash +cargo test --all-features +``` + +### Running +```bash +# Start supervisor with OpenRPC server +RUST_LOG=info cargo run --features openrpc +``` + +## Dependencies + +### Core Dependencies +- `tokio` - Async runtime +- `redis` - Redis client for job queuing +- `serde` - Serialization +- `log` - Logging +- `sal-service-manager` - Process management + +### Feature-Gated Dependencies +- `jsonrpsee` - JSON-RPC server (openrpc feature) +- `anyhow` - Error handling (openrpc feature) + +## Architecture Benefits + +1. **No Cyclic Dependencies**: Library and OpenRPC server are in the same crate, eliminating dependency cycles +2. **Feature-Gated**: CLI and server functionality only compiled when needed +3. **Clean Separation**: Library can be used independently without CLI dependencies +4. **Conditional Compilation**: Rust's feature system ensures minimal dependencies for library users +5. **Single Binary**: One supervisor binary with optional OpenRPC server integration + +## License + +[Add your license information here] diff --git a/admin-ui/Cargo.toml b/admin-ui/Cargo.toml new file mode 100644 index 0000000..e69de29 diff --git a/admin-ui/src/app.rs b/admin-ui/src/app.rs new file mode 100644 index 0000000..e69de29 diff --git a/admin-ui/src/jobs.rs b/admin-ui/src/jobs.rs new file mode 100644 index 0000000..e69de29 diff --git a/admin-ui/src/runners.rs b/admin-ui/src/runners.rs new file mode 100644 index 0000000..e69de29 diff --git a/admin-ui/styles.css b/admin-ui/styles.css new file mode 100644 index 0000000..e69de29 diff --git a/clients/admin-ui/.gitignore b/clients/admin-ui/.gitignore new file mode 100644 index 0000000..6047329 --- /dev/null +++ b/clients/admin-ui/.gitignore @@ -0,0 +1,2 @@ +dist +target \ No newline at end of file diff --git a/clients/admin-ui/Cargo.lock b/clients/admin-ui/Cargo.lock new file mode 100644 index 0000000..24d1114 --- /dev/null +++ b/clients/admin-ui/Cargo.lock @@ -0,0 +1,3347 @@ +# This file is automatically @generated by Cargo. +# It is not intended for manual editing. +version = 4 + +[[package]] +name = "addr2line" +version = "0.24.2" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "dfbe277e56a376000877090da837660b4427aad530e3028d44e0bffe4f89a1c1" +dependencies = [ + "gimli", +] + +[[package]] +name = "adler2" +version = "2.0.1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "320119579fcad9c21884f5c4861d16174d0e06250625266f50fe6898340abefa" + +[[package]] +name = "aho-corasick" +version = "1.1.3" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "8e60d3430d3a69478ad0993f19238d2df97c507009a52b3c10addcd7f6bcb916" +dependencies = [ + "memchr", +] + +[[package]] +name = "android-tzdata" +version = "0.1.1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "e999941b234f3131b00bc13c22d06e8c5ff726d1b6318ac7eb276997bbb4fef0" + +[[package]] +name = "android_system_properties" +version = "0.1.5" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "819e7219dbd41043ac279b19830f2efc897156490d7fd6ea916720117ee66311" +dependencies = [ + "libc", +] + +[[package]] +name = "anstream" +version = "0.6.20" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "3ae563653d1938f79b1ab1b5e668c87c76a9930414574a6583a7b7e11a8e6192" +dependencies = [ + "anstyle", + "anstyle-parse", + "anstyle-query", + "anstyle-wincon", + "colorchoice", + "is_terminal_polyfill", + "utf8parse", +] + +[[package]] +name = "anstyle" +version = "1.0.11" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "862ed96ca487e809f1c8e5a8447f6ee2cf102f846893800b20cebdf541fc6bbd" + +[[package]] +name = "anstyle-parse" +version = "0.2.7" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "4e7644824f0aa2c7b9384579234ef10eb7efb6a0deb83f9630a49594dd9c15c2" +dependencies = [ + "utf8parse", +] + +[[package]] +name = "anstyle-query" +version = "1.1.4" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "9e231f6134f61b71076a3eab506c379d4f36122f2af15a9ff04415ea4c3339e2" +dependencies = [ + "windows-sys 0.60.2", +] + +[[package]] +name = "anstyle-wincon" +version = "3.0.10" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "3e0633414522a32ffaac8ac6cc8f748e090c5717661fddeea04219e2344f5f2a" +dependencies = [ + "anstyle", + "once_cell_polyfill", + "windows-sys 0.60.2", +] + +[[package]] +name = "anyhow" +version = "1.0.99" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "b0674a1ddeecb70197781e945de4b3b8ffb61fa939a5597bcf48503737663100" + +[[package]] +name = "anymap2" +version = "0.13.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "d301b3b94cb4b2f23d7917810addbbaff90738e0ca2be692bd027e70d7e0330c" + +[[package]] +name = "async-stream" +version = "0.3.6" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "0b5a71a6f37880a80d1d7f19efd781e4b5de42c88f0722cc13bcb6cc2cfe8476" +dependencies = [ + "async-stream-impl", + "futures-core", + "pin-project-lite", +] + +[[package]] +name = "async-stream-impl" +version = "0.3.6" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "c7c24de15d275a1ecfd47a380fb4d5ec9bfe0933f309ed5e705b775596a3574d" +dependencies = [ + "proc-macro2", + "quote", + "syn 2.0.106", +] + +[[package]] +name = "async-trait" +version = "0.1.89" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "9035ad2d096bed7955a320ee7e2230574d28fd3c3a0f186cbea1ff3c7eed5dbb" +dependencies = [ + "proc-macro2", + "quote", + "syn 2.0.106", +] + +[[package]] +name = "atomic-waker" +version = "1.1.2" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "1505bd5d3d116872e7271a6d4e16d81d0c8570876c8de68093a09ac269d8aac0" + +[[package]] +name = "autocfg" +version = "1.5.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "c08606f8c3cbf4ce6ec8e28fb0014a2c086708fe954eaa885384a6165172e7e8" + +[[package]] +name = "backtrace" +version = "0.3.75" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "6806a6321ec58106fea15becdad98371e28d92ccbc7c8f1b3b6dd724fe8f1002" +dependencies = [ + "addr2line", + "cfg-if", + "libc", + "miniz_oxide", + "object", + "rustc-demangle", + "windows-targets 0.52.6", +] + +[[package]] +name = "base64" +version = "0.22.1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "72b3254f16251a8381aa12e40e3c4d2f0199f8c6508fbecb9d91f575e0fbb8c6" + +[[package]] +name = "bincode" +version = "1.3.3" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "b1f45e9417d87227c7a56d22e471c6206462cba514c7590c09aff4cf6d1ddcad" +dependencies = [ + "serde", +] + +[[package]] +name = "bitflags" +version = "2.9.3" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "34efbcccd345379ca2868b2b2c9d3782e9cc58ba87bc7d79d5b53d9c9ae6f25d" + +[[package]] +name = "block-buffer" +version = "0.10.4" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "3078c7629b62d3f0439517fa394996acacc5cbc91c5a20d8c658e77abd503a71" +dependencies = [ + "generic-array", +] + +[[package]] +name = "boolinator" +version = "2.4.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "cfa8873f51c92e232f9bac4065cddef41b714152812bfc5f7672ba16d6ef8cd9" + +[[package]] +name = "bumpalo" +version = "3.19.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "46c5e41b57b8bba42a04676d81cb89e9ee8e859a1a66f80a5a72e1cb76b34d43" + +[[package]] +name = "bytes" +version = "1.10.1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "d71b6127be86fdcfddb610f7182ac57211d4b18a3e9c82eb2d17662f2227ad6a" + +[[package]] +name = "cc" +version = "1.2.34" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "42bc4aea80032b7bf409b0bc7ccad88853858911b7713a8062fdc0623867bedc" +dependencies = [ + "shlex", +] + +[[package]] +name = "cesu8" +version = "1.1.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "6d43a04d8753f35258c91f8ec639f792891f748a1edbd759cf1dcea3382ad83c" + +[[package]] +name = "cfg-if" +version = "1.0.3" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "2fd1289c04a9ea8cb22300a459a72a385d7c73d3259e2ed7dcb2af674838cfa9" + +[[package]] +name = "chrono" +version = "0.4.41" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "c469d952047f47f91b68d1cba3f10d63c11d73e4636f24f08daf0278abf01c4d" +dependencies = [ + "android-tzdata", + "iana-time-zone", + "js-sys", + "num-traits", + "serde", + "wasm-bindgen", + "windows-link", +] + +[[package]] +name = "clap" +version = "4.5.45" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "1fc0e74a703892159f5ae7d3aac52c8e6c392f5ae5f359c70b5881d60aaac318" +dependencies = [ + "clap_builder", + "clap_derive", +] + +[[package]] +name = "clap_builder" +version = "4.5.44" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "b3e7f4214277f3c7aa526a59dd3fbe306a370daee1f8b7b8c987069cd8e888a8" +dependencies = [ + "anstream", + "anstyle", + "clap_lex", + "strsim", +] + +[[package]] +name = "clap_derive" +version = "4.5.45" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "14cb31bb0a7d536caef2639baa7fad459e15c3144efefa6dbd1c84562c4739f6" +dependencies = [ + "heck", + "proc-macro2", + "quote", + "syn 2.0.106", +] + +[[package]] +name = "clap_lex" +version = "0.7.5" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "b94f61472cee1439c0b966b47e3aca9ae07e45d070759512cd390ea2bebc6675" + +[[package]] +name = "colorchoice" +version = "1.0.4" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "b05b61dc5112cbb17e4b6cd61790d9845d13888356391624cbe7e41efeac1e75" + +[[package]] +name = "combine" +version = "4.6.7" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "ba5a308b75df32fe02788e748662718f03fde005016435c444eea572398219fd" +dependencies = [ + "bytes", + "futures-core", + "memchr", + "pin-project-lite", + "tokio", + "tokio-util", +] + +[[package]] +name = "console_error_panic_hook" +version = "0.1.7" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "a06aeb73f470f66dcdbf7223caeebb85984942f22f1adb2a088cf9668146bbbc" +dependencies = [ + "cfg-if", + "wasm-bindgen", +] + +[[package]] +name = "console_log" +version = "1.0.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "be8aed40e4edbf4d3b4431ab260b63fdc40f5780a4766824329ea0f1eefe3c0f" +dependencies = [ + "log", + "web-sys", +] + +[[package]] +name = "core-foundation" +version = "0.10.1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "b2a6cd9ae233e7f62ba4e9353e81a88df7fc8a5987b8d445b4d90c879bd156f6" +dependencies = [ + "core-foundation-sys", + "libc", +] + +[[package]] +name = "core-foundation-sys" +version = "0.8.7" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "773648b94d0e5d620f64f280777445740e61fe701025087ec8b57f45c791888b" + +[[package]] +name = "cpufeatures" +version = "0.2.17" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "59ed5838eebb26a2bb2e58f6d5b5316989ae9d08bab10e0e6d103e656d1b0280" +dependencies = [ + "libc", +] + +[[package]] +name = "crypto-common" +version = "0.1.6" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "1bfb12502f3fc46cca1bb51ac28df9d618d813cdc3d2f25b9fe775a34af26bb3" +dependencies = [ + "generic-array", + "typenum", +] + +[[package]] +name = "deranged" +version = "0.4.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "9c9e6a11ca8224451684bc0d7d5a7adbf8f2fd6887261a1cfc3c0432f9d4068e" +dependencies = [ + "powerfmt", +] + +[[package]] +name = "digest" +version = "0.10.7" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "9ed9a281f7bc9b7576e61468ba615a66a5c8cfdff42420a70aa82701a3b1e292" +dependencies = [ + "block-buffer", + "crypto-common", +] + +[[package]] +name = "displaydoc" +version = "0.2.5" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "97369cbbc041bc366949bc74d34658d6cda5621039731c6310521892a3a20ae0" +dependencies = [ + "proc-macro2", + "quote", + "syn 2.0.106", +] + +[[package]] +name = "env_filter" +version = "0.1.3" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "186e05a59d4c50738528153b83b0b0194d3a29507dfec16eccd4b342903397d0" +dependencies = [ + "log", + "regex", +] + +[[package]] +name = "env_logger" +version = "0.10.2" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "4cd405aab171cb85d6735e5c8d9db038c17d3ca007a4d2c25f337935c3d90580" +dependencies = [ + "humantime", + "is-terminal", + "log", + "regex", + "termcolor", +] + +[[package]] +name = "env_logger" +version = "0.11.8" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "13c863f0904021b108aa8b2f55046443e6b1ebde8fd4a15c399893aae4fa069f" +dependencies = [ + "anstream", + "anstyle", + "env_filter", + "jiff", + "log", +] + +[[package]] +name = "equivalent" +version = "1.0.2" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "877a4ace8713b0bcf2a4e7eec82529c029f1d0619886d18145fea96c3ffe5c0f" + +[[package]] +name = "fnv" +version = "1.0.7" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "3f9eec918d3f24069decb9af1554cad7c880e2da24a9afd88aca000531ab82c1" + +[[package]] +name = "form_urlencoded" +version = "1.2.2" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "cb4cb245038516f5f85277875cdaa4f7d2c9a0fa0468de06ed190163b1581fcf" +dependencies = [ + "percent-encoding", +] + +[[package]] +name = "futures" +version = "0.3.31" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "65bc07b1a8bc7c85c5f2e110c476c7389b4554ba72af57d8445ea63a576b0876" +dependencies = [ + "futures-channel", + "futures-core", + "futures-executor", + "futures-io", + "futures-sink", + "futures-task", + "futures-util", +] + +[[package]] +name = "futures-channel" +version = "0.3.31" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "2dff15bf788c671c1934e366d07e30c1814a8ef514e1af724a602e8a2fbe1b10" +dependencies = [ + "futures-core", + "futures-sink", +] + +[[package]] +name = "futures-core" +version = "0.3.31" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "05f29059c0c2090612e8d742178b0580d2dc940c837851ad723096f87af6663e" + +[[package]] +name = "futures-executor" +version = "0.3.31" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "1e28d1d997f585e54aebc3f97d39e72338912123a67330d723fdbb564d646c9f" +dependencies = [ + "futures-core", + "futures-task", + "futures-util", +] + +[[package]] +name = "futures-io" +version = "0.3.31" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "9e5c1b78ca4aae1ac06c48a526a655760685149f0d465d21f37abfe57ce075c6" + +[[package]] +name = "futures-macro" +version = "0.3.31" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "162ee34ebcb7c64a8abebc059ce0fee27c2262618d7b60ed8faf72fef13c3650" +dependencies = [ + "proc-macro2", + "quote", + "syn 2.0.106", +] + +[[package]] +name = "futures-sink" +version = "0.3.31" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "e575fab7d1e0dcb8d0c7bcf9a63ee213816ab51902e6d244a95819acacf1d4f7" + +[[package]] +name = "futures-task" +version = "0.3.31" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "f90f7dce0722e95104fcb095585910c0977252f286e354b5e3bd38902cd99988" + +[[package]] +name = "futures-util" +version = "0.3.31" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "9fa08315bb612088cc391249efdc3bc77536f16c91f6cf495e6fbe85b20a4a81" +dependencies = [ + "futures-channel", + "futures-core", + "futures-io", + "futures-macro", + "futures-sink", + "futures-task", + "memchr", + "pin-project-lite", + "pin-utils", + "slab", +] + +[[package]] +name = "generic-array" +version = "0.14.7" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "85649ca51fd72272d7821adaf274ad91c288277713d9c18820d8499a7ff69e9a" +dependencies = [ + "typenum", + "version_check", +] + +[[package]] +name = "getrandom" +version = "0.2.16" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "335ff9f135e4384c8150d6f27c6daed433577f86b4750418338c01a1a2528592" +dependencies = [ + "cfg-if", + "js-sys", + "libc", + "wasi 0.11.1+wasi-snapshot-preview1", + "wasm-bindgen", +] + +[[package]] +name = "getrandom" +version = "0.3.3" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "26145e563e54f2cadc477553f1ec5ee650b00862f0a58bcd12cbdc5f0ea2d2f4" +dependencies = [ + "cfg-if", + "libc", + "r-efi", + "wasi 0.14.2+wasi-0.2.4", +] + +[[package]] +name = "gimli" +version = "0.31.1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "07e28edb80900c19c28f1072f2e8aeca7fa06b23cd4169cefe1af5aa3260783f" + +[[package]] +name = "gloo" +version = "0.8.1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "28999cda5ef6916ffd33fb4a7b87e1de633c47c0dc6d97905fee1cdaa142b94d" +dependencies = [ + "gloo-console 0.2.3", + "gloo-dialogs 0.1.1", + "gloo-events 0.1.2", + "gloo-file 0.2.3", + "gloo-history 0.1.5", + "gloo-net 0.3.1", + "gloo-render 0.1.1", + "gloo-storage 0.2.2", + "gloo-timers 0.2.6", + "gloo-utils 0.1.7", + "gloo-worker 0.2.1", +] + +[[package]] +name = "gloo" +version = "0.10.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "cd35526c28cc55c1db77aed6296de58677dbab863b118483a27845631d870249" +dependencies = [ + "gloo-console 0.3.0", + "gloo-dialogs 0.2.0", + "gloo-events 0.2.0", + "gloo-file 0.3.0", + "gloo-history 0.2.2", + "gloo-net 0.4.0", + "gloo-render 0.2.0", + "gloo-storage 0.3.0", + "gloo-timers 0.3.0", + "gloo-utils 0.2.0", + "gloo-worker 0.4.0", +] + +[[package]] +name = "gloo" +version = "0.11.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "d15282ece24eaf4bd338d73ef580c6714c8615155c4190c781290ee3fa0fd372" +dependencies = [ + "gloo-console 0.3.0", + "gloo-dialogs 0.2.0", + "gloo-events 0.2.0", + "gloo-file 0.3.0", + "gloo-history 0.2.2", + "gloo-net 0.5.0", + "gloo-render 0.2.0", + "gloo-storage 0.3.0", + "gloo-timers 0.3.0", + "gloo-utils 0.2.0", + "gloo-worker 0.5.0", +] + +[[package]] +name = "gloo-console" +version = "0.2.3" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "82b7ce3c05debe147233596904981848862b068862e9ec3e34be446077190d3f" +dependencies = [ + "gloo-utils 0.1.7", + "js-sys", + "serde", + "wasm-bindgen", + "web-sys", +] + +[[package]] +name = "gloo-console" +version = "0.3.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "2a17868f56b4a24f677b17c8cb69958385102fa879418052d60b50bc1727e261" +dependencies = [ + "gloo-utils 0.2.0", + "js-sys", + "serde", + "wasm-bindgen", + "web-sys", +] + +[[package]] +name = "gloo-dialogs" +version = "0.1.1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "67062364ac72d27f08445a46cab428188e2e224ec9e37efdba48ae8c289002e6" +dependencies = [ + "wasm-bindgen", + "web-sys", +] + +[[package]] +name = "gloo-dialogs" +version = "0.2.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "bf4748e10122b01435750ff530095b1217cf6546173459448b83913ebe7815df" +dependencies = [ + "wasm-bindgen", + "web-sys", +] + +[[package]] +name = "gloo-events" +version = "0.1.2" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "68b107f8abed8105e4182de63845afcc7b69c098b7852a813ea7462a320992fc" +dependencies = [ + "wasm-bindgen", + "web-sys", +] + +[[package]] +name = "gloo-events" +version = "0.2.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "27c26fb45f7c385ba980f5fa87ac677e363949e065a083722697ef1b2cc91e41" +dependencies = [ + "wasm-bindgen", + "web-sys", +] + +[[package]] +name = "gloo-file" +version = "0.2.3" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "a8d5564e570a38b43d78bdc063374a0c3098c4f0d64005b12f9bbe87e869b6d7" +dependencies = [ + "gloo-events 0.1.2", + "js-sys", + "wasm-bindgen", + "web-sys", +] + +[[package]] +name = "gloo-file" +version = "0.3.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "97563d71863fb2824b2e974e754a81d19c4a7ec47b09ced8a0e6656b6d54bd1f" +dependencies = [ + "futures-channel", + "gloo-events 0.2.0", + "js-sys", + "wasm-bindgen", + "web-sys", +] + +[[package]] +name = "gloo-history" +version = "0.1.5" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "85725d90bf0ed47063b3930ef28e863658a7905989e9929a8708aab74a1d5e7f" +dependencies = [ + "gloo-events 0.1.2", + "gloo-utils 0.1.7", + "serde", + "serde-wasm-bindgen 0.5.0", + "serde_urlencoded", + "thiserror", + "wasm-bindgen", + "web-sys", +] + +[[package]] +name = "gloo-history" +version = "0.2.2" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "903f432be5ba34427eac5e16048ef65604a82061fe93789f2212afc73d8617d6" +dependencies = [ + "getrandom 0.2.16", + "gloo-events 0.2.0", + "gloo-utils 0.2.0", + "serde", + "serde-wasm-bindgen 0.6.5", + "serde_urlencoded", + "thiserror", + "wasm-bindgen", + "web-sys", +] + +[[package]] +name = "gloo-net" +version = "0.3.1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "a66b4e3c7d9ed8d315fd6b97c8b1f74a7c6ecbbc2320e65ae7ed38b7068cc620" +dependencies = [ + "futures-channel", + "futures-core", + "futures-sink", + "gloo-utils 0.1.7", + "http 0.2.12", + "js-sys", + "pin-project", + "serde", + "serde_json", + "thiserror", + "wasm-bindgen", + "wasm-bindgen-futures", + "web-sys", +] + +[[package]] +name = "gloo-net" +version = "0.4.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "8ac9e8288ae2c632fa9f8657ac70bfe38a1530f345282d7ba66a1f70b72b7dc4" +dependencies = [ + "futures-channel", + "futures-core", + "futures-sink", + "gloo-utils 0.2.0", + "http 0.2.12", + "js-sys", + "pin-project", + "serde", + "serde_json", + "thiserror", + "wasm-bindgen", + "wasm-bindgen-futures", + "web-sys", +] + +[[package]] +name = "gloo-net" +version = "0.5.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "43aaa242d1239a8822c15c645f02166398da4f8b5c4bae795c1f5b44e9eee173" +dependencies = [ + "futures-channel", + "futures-core", + "futures-sink", + "gloo-utils 0.2.0", + "http 0.2.12", + "js-sys", + "pin-project", + "serde", + "serde_json", + "thiserror", + "wasm-bindgen", + "wasm-bindgen-futures", + "web-sys", +] + +[[package]] +name = "gloo-render" +version = "0.1.1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "2fd9306aef67cfd4449823aadcd14e3958e0800aa2183955a309112a84ec7764" +dependencies = [ + "wasm-bindgen", + "web-sys", +] + +[[package]] +name = "gloo-render" +version = "0.2.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "56008b6744713a8e8d98ac3dcb7d06543d5662358c9c805b4ce2167ad4649833" +dependencies = [ + "wasm-bindgen", + "web-sys", +] + +[[package]] +name = "gloo-storage" +version = "0.2.2" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "5d6ab60bf5dbfd6f0ed1f7843da31b41010515c745735c970e821945ca91e480" +dependencies = [ + "gloo-utils 0.1.7", + "js-sys", + "serde", + "serde_json", + "thiserror", + "wasm-bindgen", + "web-sys", +] + +[[package]] +name = "gloo-storage" +version = "0.3.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "fbc8031e8c92758af912f9bc08fbbadd3c6f3cfcbf6b64cdf3d6a81f0139277a" +dependencies = [ + "gloo-utils 0.2.0", + "js-sys", + "serde", + "serde_json", + "thiserror", + "wasm-bindgen", + "web-sys", +] + +[[package]] +name = "gloo-timers" +version = "0.2.6" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "9b995a66bb87bebce9a0f4a95aed01daca4872c050bfcb21653361c03bc35e5c" +dependencies = [ + "js-sys", + "wasm-bindgen", +] + +[[package]] +name = "gloo-timers" +version = "0.3.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "bbb143cf96099802033e0d4f4963b19fd2e0b728bcf076cd9cf7f6634f092994" +dependencies = [ + "futures-channel", + "futures-core", + "js-sys", + "wasm-bindgen", +] + +[[package]] +name = "gloo-utils" +version = "0.1.7" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "037fcb07216cb3a30f7292bd0176b050b7b9a052ba830ef7d5d65f6dc64ba58e" +dependencies = [ + "js-sys", + "serde", + "serde_json", + "wasm-bindgen", + "web-sys", +] + +[[package]] +name = "gloo-utils" +version = "0.2.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "0b5555354113b18c547c1d3a98fbf7fb32a9ff4f6fa112ce823a21641a0ba3aa" +dependencies = [ + "js-sys", + "serde", + "serde_json", + "wasm-bindgen", + "web-sys", +] + +[[package]] +name = "gloo-worker" +version = "0.2.1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "13471584da78061a28306d1359dd0178d8d6fc1c7c80e5e35d27260346e0516a" +dependencies = [ + "anymap2", + "bincode", + "gloo-console 0.2.3", + "gloo-utils 0.1.7", + "js-sys", + "serde", + "wasm-bindgen", + "wasm-bindgen-futures", + "web-sys", +] + +[[package]] +name = "gloo-worker" +version = "0.4.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "76495d3dd87de51da268fa3a593da118ab43eb7f8809e17eb38d3319b424e400" +dependencies = [ + "bincode", + "futures", + "gloo-utils 0.2.0", + "gloo-worker-macros", + "js-sys", + "pinned", + "serde", + "thiserror", + "wasm-bindgen", + "wasm-bindgen-futures", + "web-sys", +] + +[[package]] +name = "gloo-worker" +version = "0.5.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "085f262d7604911c8150162529cefab3782e91adb20202e8658f7275d2aefe5d" +dependencies = [ + "bincode", + "futures", + "gloo-utils 0.2.0", + "gloo-worker-macros", + "js-sys", + "pinned", + "serde", + "thiserror", + "wasm-bindgen", + "wasm-bindgen-futures", + "web-sys", +] + +[[package]] +name = "gloo-worker-macros" +version = "0.1.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "956caa58d4857bc9941749d55e4bd3000032d8212762586fa5705632967140e7" +dependencies = [ + "proc-macro-crate 1.3.1", + "proc-macro2", + "quote", + "syn 2.0.106", +] + +[[package]] +name = "h2" +version = "0.4.12" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "f3c0b69cfcb4e1b9f1bf2f53f95f766e4661169728ec61cd3fe5a0166f2d1386" +dependencies = [ + "atomic-waker", + "bytes", + "fnv", + "futures-core", + "futures-sink", + "http 1.3.1", + "indexmap", + "slab", + "tokio", + "tokio-util", + "tracing", +] + +[[package]] +name = "hashbrown" +version = "0.15.5" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "9229cfe53dfd69f0609a49f65461bd93001ea1ef889cd5529dd176593f5338a1" + +[[package]] +name = "heck" +version = "0.5.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "2304e00983f87ffb38b55b444b5e3b60a884b5d30c0fca7d82fe33449bbe55ea" + +[[package]] +name = "hermit-abi" +version = "0.5.2" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "fc0fef456e4baa96da950455cd02c081ca953b141298e41db3fc7e36b1da849c" + +[[package]] +name = "hero-supervisor" +version = "0.1.0" +dependencies = [ + "anyhow", + "async-trait", + "chrono", + "clap", + "env_logger 0.10.2", + "jsonrpsee", + "log", + "redis", + "sal-service-manager", + "serde", + "serde_json", + "thiserror", + "tokio", + "toml", + "tower", + "tower-http", + "uuid", +] + +[[package]] +name = "hero-supervisor-openrpc-client" +version = "0.1.0" +dependencies = [ + "chrono", + "console_log", + "env_logger 0.11.8", + "getrandom 0.2.16", + "hero-supervisor", + "js-sys", + "jsonrpsee", + "log", + "serde", + "serde_json", + "thiserror", + "tokio", + "uuid", + "wasm-bindgen", + "wasm-bindgen-futures", + "web-sys", +] + +[[package]] +name = "http" +version = "0.2.12" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "601cbb57e577e2f5ef5be8e7b83f0f63994f25aa94d673e54a92d5c516d101f1" +dependencies = [ + "bytes", + "fnv", + "itoa", +] + +[[package]] +name = "http" +version = "1.3.1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "f4a85d31aea989eead29a3aaf9e1115a180df8282431156e533de47660892565" +dependencies = [ + "bytes", + "fnv", + "itoa", +] + +[[package]] +name = "http-body" +version = "1.0.1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "1efedce1fb8e6913f23e0c92de8e62cd5b772a67e7b3946df930a62566c93184" +dependencies = [ + "bytes", + "http 1.3.1", +] + +[[package]] +name = "http-body-util" +version = "0.1.3" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "b021d93e26becf5dc7e1b75b1bed1fd93124b374ceb73f43d4d4eafec896a64a" +dependencies = [ + "bytes", + "futures-core", + "http 1.3.1", + "http-body", + "pin-project-lite", +] + +[[package]] +name = "httparse" +version = "1.10.1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "6dbf3de79e51f3d586ab4cb9d5c3e2c14aa28ed23d180cf89b4df0454a69cc87" + +[[package]] +name = "httpdate" +version = "1.0.3" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "df3b46402a9d5adb4c86a0cf463f42e19994e3ee891101b1841f30a545cb49a9" + +[[package]] +name = "humantime" +version = "2.2.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "9b112acc8b3adf4b107a8ec20977da0273a8c386765a3ec0229bd500a1443f9f" + +[[package]] +name = "hyper" +version = "1.7.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "eb3aa54a13a0dfe7fbe3a59e0c76093041720fdc77b110cc0fc260fafb4dc51e" +dependencies = [ + "atomic-waker", + "bytes", + "futures-channel", + "futures-core", + "h2", + "http 1.3.1", + "http-body", + "httparse", + "httpdate", + "itoa", + "pin-project-lite", + "pin-utils", + "smallvec", + "tokio", + "want", +] + +[[package]] +name = "hyper-rustls" +version = "0.27.7" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "e3c93eb611681b207e1fe55d5a71ecf91572ec8a6705cdb6857f7d8d5242cf58" +dependencies = [ + "http 1.3.1", + "hyper", + "hyper-util", + "log", + "rustls", + "rustls-pki-types", + "tokio", + "tokio-rustls", + "tower-service", +] + +[[package]] +name = "hyper-util" +version = "0.1.16" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "8d9b05277c7e8da2c93a568989bb6207bef0112e8d17df7a6eda4a3cf143bc5e" +dependencies = [ + "bytes", + "futures-channel", + "futures-core", + "futures-util", + "http 1.3.1", + "http-body", + "hyper", + "libc", + "pin-project-lite", + "socket2 0.6.0", + "tokio", + "tower-service", + "tracing", +] + +[[package]] +name = "iana-time-zone" +version = "0.1.63" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "b0c919e5debc312ad217002b8048a17b7d83f80703865bbfcfebb0458b0b27d8" +dependencies = [ + "android_system_properties", + "core-foundation-sys", + "iana-time-zone-haiku", + "js-sys", + "log", + "wasm-bindgen", + "windows-core", +] + +[[package]] +name = "iana-time-zone-haiku" +version = "0.1.2" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "f31827a206f56af32e590ba56d5d2d085f558508192593743f16b2306495269f" +dependencies = [ + "cc", +] + +[[package]] +name = "icu_collections" +version = "2.0.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "200072f5d0e3614556f94a9930d5dc3e0662a652823904c3a75dc3b0af7fee47" +dependencies = [ + "displaydoc", + "potential_utf", + "yoke", + "zerofrom", + "zerovec", +] + +[[package]] +name = "icu_locale_core" +version = "2.0.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "0cde2700ccaed3872079a65fb1a78f6c0a36c91570f28755dda67bc8f7d9f00a" +dependencies = [ + "displaydoc", + "litemap", + "tinystr", + "writeable", + "zerovec", +] + +[[package]] +name = "icu_normalizer" +version = "2.0.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "436880e8e18df4d7bbc06d58432329d6458cc84531f7ac5f024e93deadb37979" +dependencies = [ + "displaydoc", + "icu_collections", + "icu_normalizer_data", + "icu_properties", + "icu_provider", + "smallvec", + "zerovec", +] + +[[package]] +name = "icu_normalizer_data" +version = "2.0.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "00210d6893afc98edb752b664b8890f0ef174c8adbb8d0be9710fa66fbbf72d3" + +[[package]] +name = "icu_properties" +version = "2.0.1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "016c619c1eeb94efb86809b015c58f479963de65bdb6253345c1a1276f22e32b" +dependencies = [ + "displaydoc", + "icu_collections", + "icu_locale_core", + "icu_properties_data", + "icu_provider", + "potential_utf", + "zerotrie", + "zerovec", +] + +[[package]] +name = "icu_properties_data" +version = "2.0.1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "298459143998310acd25ffe6810ed544932242d3f07083eee1084d83a71bd632" + +[[package]] +name = "icu_provider" +version = "2.0.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "03c80da27b5f4187909049ee2d72f276f0d9f99a42c306bd0131ecfe04d8e5af" +dependencies = [ + "displaydoc", + "icu_locale_core", + "stable_deref_trait", + "tinystr", + "writeable", + "yoke", + "zerofrom", + "zerotrie", + "zerovec", +] + +[[package]] +name = "idna" +version = "1.1.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "3b0875f23caa03898994f6ddc501886a45c7d3d62d04d2d90788d47be1b1e4de" +dependencies = [ + "idna_adapter", + "smallvec", + "utf8_iter", +] + +[[package]] +name = "idna_adapter" +version = "1.2.1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "3acae9609540aa318d1bc588455225fb2085b9ed0c4f6bd0d9d5bcd86f1a0344" +dependencies = [ + "icu_normalizer", + "icu_properties", +] + +[[package]] +name = "implicit-clone" +version = "0.4.9" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "f8a9aa791c7b5a71b636b7a68207fdebf171ddfc593d9c8506ec4cbc527b6a84" +dependencies = [ + "implicit-clone-derive", + "indexmap", +] + +[[package]] +name = "implicit-clone-derive" +version = "0.1.2" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "699c1b6d335e63d0ba5c1e1c7f647371ce989c3bcbe1f7ed2b85fa56e3bd1a21" +dependencies = [ + "quote", + "syn 2.0.106", +] + +[[package]] +name = "indexmap" +version = "2.11.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "f2481980430f9f78649238835720ddccc57e52df14ffce1c6f37391d61b563e9" +dependencies = [ + "equivalent", + "hashbrown", +] + +[[package]] +name = "io-uring" +version = "0.7.10" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "046fa2d4d00aea763528b4950358d0ead425372445dc8ff86312b3c69ff7727b" +dependencies = [ + "bitflags", + "cfg-if", + "libc", +] + +[[package]] +name = "is-terminal" +version = "0.4.16" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "e04d7f318608d35d4b61ddd75cbdaee86b023ebe2bd5a66ee0915f0bf93095a9" +dependencies = [ + "hermit-abi", + "libc", + "windows-sys 0.59.0", +] + +[[package]] +name = "is_terminal_polyfill" +version = "1.70.1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "7943c866cc5cd64cbc25b2e01621d07fa8eb2a1a23160ee81ce38704e97b8ecf" + +[[package]] +name = "itoa" +version = "1.0.15" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "4a5f13b858c8d314ee3e8f639011f7ccefe71f97f96e50151fb991f267928e2c" + +[[package]] +name = "jiff" +version = "0.2.15" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "be1f93b8b1eb69c77f24bbb0afdf66f54b632ee39af40ca21c4365a1d7347e49" +dependencies = [ + "jiff-static", + "log", + "portable-atomic", + "portable-atomic-util", + "serde", +] + +[[package]] +name = "jiff-static" +version = "0.2.15" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "03343451ff899767262ec32146f6d559dd759fdadf42ff0e227c7c48f72594b4" +dependencies = [ + "proc-macro2", + "quote", + "syn 2.0.106", +] + +[[package]] +name = "jni" +version = "0.21.1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "1a87aa2bb7d2af34197c04845522473242e1aa17c12f4935d5856491a7fb8c97" +dependencies = [ + "cesu8", + "cfg-if", + "combine", + "jni-sys", + "log", + "thiserror", + "walkdir", + "windows-sys 0.45.0", +] + +[[package]] +name = "jni-sys" +version = "0.3.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "8eaf4bc02d17cbdd7ff4c7438cafcdf7fb9a4613313ad11b4f8fefe7d3fa0130" + +[[package]] +name = "js-sys" +version = "0.3.77" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "1cfaf33c695fc6e08064efbc1f72ec937429614f25eef83af942d0e227c3a28f" +dependencies = [ + "once_cell", + "wasm-bindgen", +] + +[[package]] +name = "jsonrpsee" +version = "0.24.9" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "37b26c20e2178756451cfeb0661fb74c47dd5988cb7e3939de7e9241fd604d42" +dependencies = [ + "jsonrpsee-core", + "jsonrpsee-http-client", + "jsonrpsee-proc-macros", + "jsonrpsee-server", + "jsonrpsee-types", + "tokio", + "tracing", +] + +[[package]] +name = "jsonrpsee-core" +version = "0.24.9" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "456196007ca3a14db478346f58c7238028d55ee15c1df15115596e411ff27925" +dependencies = [ + "async-trait", + "bytes", + "futures-util", + "http 1.3.1", + "http-body", + "http-body-util", + "jsonrpsee-types", + "parking_lot", + "rand", + "rustc-hash", + "serde", + "serde_json", + "thiserror", + "tokio", + "tracing", +] + +[[package]] +name = "jsonrpsee-http-client" +version = "0.24.9" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "c872b6c9961a4ccc543e321bb5b89f6b2d2c7fe8b61906918273a3333c95400c" +dependencies = [ + "async-trait", + "base64", + "http-body", + "hyper", + "hyper-rustls", + "hyper-util", + "jsonrpsee-core", + "jsonrpsee-types", + "rustls", + "rustls-platform-verifier", + "serde", + "serde_json", + "thiserror", + "tokio", + "tower", + "tracing", + "url", +] + +[[package]] +name = "jsonrpsee-proc-macros" +version = "0.24.9" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "5e65763c942dfc9358146571911b0cd1c361c2d63e2d2305622d40d36376ca80" +dependencies = [ + "heck", + "proc-macro-crate 3.3.0", + "proc-macro2", + "quote", + "syn 2.0.106", +] + +[[package]] +name = "jsonrpsee-server" +version = "0.24.9" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "55e363146da18e50ad2b51a0a7925fc423137a0b1371af8235b1c231a0647328" +dependencies = [ + "futures-util", + "http 1.3.1", + "http-body", + "http-body-util", + "hyper", + "hyper-util", + "jsonrpsee-core", + "jsonrpsee-types", + "pin-project", + "route-recognizer", + "serde", + "serde_json", + "soketto", + "thiserror", + "tokio", + "tokio-stream", + "tokio-util", + "tower", + "tracing", +] + +[[package]] +name = "jsonrpsee-types" +version = "0.24.9" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "08a8e70baf945b6b5752fc8eb38c918a48f1234daf11355e07106d963f860089" +dependencies = [ + "http 1.3.1", + "serde", + "serde_json", + "thiserror", +] + +[[package]] +name = "libc" +version = "0.2.175" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "6a82ae493e598baaea5209805c49bbf2ea7de956d50d7da0da1164f9c6d28543" + +[[package]] +name = "litemap" +version = "0.8.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "241eaef5fd12c88705a01fc1066c48c4b36e0dd4377dcdc7ec3942cea7a69956" + +[[package]] +name = "lock_api" +version = "0.4.13" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "96936507f153605bddfcda068dd804796c84324ed2510809e5b2a624c81da765" +dependencies = [ + "autocfg", + "scopeguard", +] + +[[package]] +name = "log" +version = "0.4.27" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "13dc2df351e3202783a1fe0d44375f7295ffb4049267b0f3018346dc122a1d94" + +[[package]] +name = "memchr" +version = "2.7.5" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "32a282da65faaf38286cf3be983213fcf1d2e2a58700e808f83f4ea9a4804bc0" + +[[package]] +name = "miniz_oxide" +version = "0.8.9" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "1fa76a2c86f704bdb222d66965fb3d63269ce38518b83cb0575fca855ebb6316" +dependencies = [ + "adler2", +] + +[[package]] +name = "mio" +version = "1.0.4" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "78bed444cc8a2160f01cbcf811ef18cac863ad68ae8ca62092e8db51d51c761c" +dependencies = [ + "libc", + "wasi 0.11.1+wasi-snapshot-preview1", + "windows-sys 0.59.0", +] + +[[package]] +name = "num-conv" +version = "0.1.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "51d515d32fb182ee37cda2ccdcb92950d6a3c2893aa280e540671c2cd0f3b1d9" + +[[package]] +name = "num-traits" +version = "0.2.19" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "071dfc062690e90b734c0b2273ce72ad0ffa95f0c74596bc250dcfd960262841" +dependencies = [ + "autocfg", +] + +[[package]] +name = "num_cpus" +version = "1.17.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "91df4bbde75afed763b708b7eee1e8e7651e02d97f6d5dd763e89367e957b23b" +dependencies = [ + "hermit-abi", + "libc", +] + +[[package]] +name = "object" +version = "0.36.7" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "62948e14d923ea95ea2c7c86c71013138b66525b86bdc08d2dcc262bdb497b87" +dependencies = [ + "memchr", +] + +[[package]] +name = "once_cell" +version = "1.21.3" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "42f5e15c9953c5e4ccceeb2e7382a716482c34515315f7b03532b8b4e8393d2d" + +[[package]] +name = "once_cell_polyfill" +version = "1.70.1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "a4895175b425cb1f87721b59f0f286c2092bd4af812243672510e1ac53e2e0ad" + +[[package]] +name = "openssl-probe" +version = "0.1.6" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "d05e27ee213611ffe7d6348b942e8f942b37114c00cc03cec254295a4a17852e" + +[[package]] +name = "parking_lot" +version = "0.12.4" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "70d58bf43669b5795d1576d0641cfb6fbb2057bf629506267a92807158584a13" +dependencies = [ + "lock_api", + "parking_lot_core", +] + +[[package]] +name = "parking_lot_core" +version = "0.9.11" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "bc838d2a56b5b1a6c25f55575dfc605fabb63bb2365f6c2353ef9159aa69e4a5" +dependencies = [ + "cfg-if", + "libc", + "redox_syscall", + "smallvec", + "windows-targets 0.52.6", +] + +[[package]] +name = "percent-encoding" +version = "2.3.2" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "9b4f627cb1b25917193a259e49bdad08f671f8d9708acfd5fe0a8c1455d87220" + +[[package]] +name = "pin-project" +version = "1.1.10" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "677f1add503faace112b9f1373e43e9e054bfdd22ff1a63c1bc485eaec6a6a8a" +dependencies = [ + "pin-project-internal", +] + +[[package]] +name = "pin-project-internal" +version = "1.1.10" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "6e918e4ff8c4549eb882f14b3a4bc8c8bc93de829416eacf579f1207a8fbf861" +dependencies = [ + "proc-macro2", + "quote", + "syn 2.0.106", +] + +[[package]] +name = "pin-project-lite" +version = "0.2.16" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "3b3cff922bd51709b605d9ead9aa71031d81447142d828eb4a6eba76fe619f9b" + +[[package]] +name = "pin-utils" +version = "0.1.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "8b870d8c151b6f2fb93e84a13146138f05d02ed11c7e7c54f8826aaaf7c9f184" + +[[package]] +name = "pinned" +version = "0.1.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "a829027bd95e54cfe13e3e258a1ae7b645960553fb82b75ff852c29688ee595b" +dependencies = [ + "futures", + "rustversion", + "thiserror", +] + +[[package]] +name = "plist" +version = "1.7.4" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "3af6b589e163c5a788fab00ce0c0366f6efbb9959c2f9874b224936af7fce7e1" +dependencies = [ + "base64", + "indexmap", + "quick-xml", + "serde", + "time", +] + +[[package]] +name = "portable-atomic" +version = "1.11.1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "f84267b20a16ea918e43c6a88433c2d54fa145c92a811b5b047ccbe153674483" + +[[package]] +name = "portable-atomic-util" +version = "0.2.4" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "d8a2f0d8d040d7848a709caf78912debcc3f33ee4b3cac47d73d1e1069e83507" +dependencies = [ + "portable-atomic", +] + +[[package]] +name = "potential_utf" +version = "0.1.2" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "e5a7c30837279ca13e7c867e9e40053bc68740f988cb07f7ca6df43cc734b585" +dependencies = [ + "zerovec", +] + +[[package]] +name = "powerfmt" +version = "0.2.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "439ee305def115ba05938db6eb1644ff94165c5ab5e9420d1c1bcedbba909391" + +[[package]] +name = "ppv-lite86" +version = "0.2.21" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "85eae3c4ed2f50dcfe72643da4befc30deadb458a9b590d720cde2f2b1e97da9" +dependencies = [ + "zerocopy", +] + +[[package]] +name = "prettyplease" +version = "0.2.37" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "479ca8adacdd7ce8f1fb39ce9ecccbfe93a3f1344b3d0d97f20bc0196208f62b" +dependencies = [ + "proc-macro2", + "syn 2.0.106", +] + +[[package]] +name = "proc-macro-crate" +version = "1.3.1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "7f4c021e1093a56626774e81216a4ce732a735e5bad4868a03f3ed65ca0c3919" +dependencies = [ + "once_cell", + "toml_edit 0.19.15", +] + +[[package]] +name = "proc-macro-crate" +version = "3.3.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "edce586971a4dfaa28950c6f18ed55e0406c1ab88bbce2c6f6293a7aaba73d35" +dependencies = [ + "toml_edit 0.22.27", +] + +[[package]] +name = "proc-macro-error" +version = "1.0.4" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "da25490ff9892aab3fcf7c36f08cfb902dd3e71ca0f9f9517bea02a73a5ce38c" +dependencies = [ + "proc-macro-error-attr", + "proc-macro2", + "quote", + "syn 1.0.109", + "version_check", +] + +[[package]] +name = "proc-macro-error-attr" +version = "1.0.4" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "a1be40180e52ecc98ad80b184934baf3d0d29f979574e439af5a55274b35f869" +dependencies = [ + "proc-macro2", + "quote", + "version_check", +] + +[[package]] +name = "proc-macro2" +version = "1.0.101" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "89ae43fd86e4158d6db51ad8e2b80f313af9cc74f5c0e03ccb87de09998732de" +dependencies = [ + "unicode-ident", +] + +[[package]] +name = "prokio" +version = "0.1.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "03b55e106e5791fa5a13abd13c85d6127312e8e09098059ca2bc9b03ca4cf488" +dependencies = [ + "futures", + "gloo 0.8.1", + "num_cpus", + "once_cell", + "pin-project", + "pinned", + "tokio", + "tokio-stream", + "wasm-bindgen-futures", +] + +[[package]] +name = "quick-xml" +version = "0.38.3" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "42a232e7487fc2ef313d96dde7948e7a3c05101870d8985e4fd8d26aedd27b89" +dependencies = [ + "memchr", +] + +[[package]] +name = "quote" +version = "1.0.40" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "1885c039570dc00dcb4ff087a89e185fd56bae234ddc7f056a945bf36467248d" +dependencies = [ + "proc-macro2", +] + +[[package]] +name = "r-efi" +version = "5.3.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "69cdb34c158ceb288df11e18b4bd39de994f6657d83847bdffdbd7f346754b0f" + +[[package]] +name = "rand" +version = "0.8.5" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "34af8d1a0e25924bc5b7c43c079c942339d8f0a8b57c39049bef581b46327404" +dependencies = [ + "libc", + "rand_chacha", + "rand_core", +] + +[[package]] +name = "rand_chacha" +version = "0.3.1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "e6c10a63a0fa32252be49d21e7709d4d4baf8d231c2dbce1eaa8141b9b127d88" +dependencies = [ + "ppv-lite86", + "rand_core", +] + +[[package]] +name = "rand_core" +version = "0.6.4" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "ec0be4795e2f6a28069bec0b5ff3e2ac9bafc99e6a9a7dc3547996c5c816922c" +dependencies = [ + "getrandom 0.2.16", +] + +[[package]] +name = "redis" +version = "0.25.4" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "e0d7a6955c7511f60f3ba9e86c6d02b3c3f144f8c24b288d1f4e18074ab8bbec" +dependencies = [ + "async-trait", + "bytes", + "combine", + "futures-util", + "itoa", + "percent-encoding", + "pin-project-lite", + "ryu", + "sha1_smol", + "socket2 0.5.10", + "tokio", + "tokio-util", + "url", +] + +[[package]] +name = "redox_syscall" +version = "0.5.17" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "5407465600fb0548f1442edf71dd20683c6ed326200ace4b1ef0763521bb3b77" +dependencies = [ + "bitflags", +] + +[[package]] +name = "regex" +version = "1.11.2" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "23d7fd106d8c02486a8d64e778353d1cffe08ce79ac2e82f540c86d0facf6912" +dependencies = [ + "aho-corasick", + "memchr", + "regex-automata", + "regex-syntax", +] + +[[package]] +name = "regex-automata" +version = "0.4.10" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "6b9458fa0bfeeac22b5ca447c63aaf45f28439a709ccd244698632f9aa6394d6" +dependencies = [ + "aho-corasick", + "memchr", + "regex-syntax", +] + +[[package]] +name = "regex-syntax" +version = "0.8.6" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "caf4aa5b0f434c91fe5c7f1ecb6a5ece2130b02ad2a590589dda5146df959001" + +[[package]] +name = "ring" +version = "0.17.14" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "a4689e6c2294d81e88dc6261c768b63bc4fcdb852be6d1352498b114f61383b7" +dependencies = [ + "cc", + "cfg-if", + "getrandom 0.2.16", + "libc", + "untrusted", + "windows-sys 0.52.0", +] + +[[package]] +name = "route-recognizer" +version = "0.3.1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "afab94fb28594581f62d981211a9a4d53cc8130bbcbbb89a0440d9b8e81a7746" + +[[package]] +name = "rustc-demangle" +version = "0.1.26" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "56f7d92ca342cea22a06f2121d944b4fd82af56988c270852495420f961d4ace" + +[[package]] +name = "rustc-hash" +version = "2.1.1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "357703d41365b4b27c590e3ed91eabb1b663f07c4c084095e60cbed4362dff0d" + +[[package]] +name = "rustls" +version = "0.23.31" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "c0ebcbd2f03de0fc1122ad9bb24b127a5a6cd51d72604a3f3c50ac459762b6cc" +dependencies = [ + "log", + "once_cell", + "ring", + "rustls-pki-types", + "rustls-webpki", + "subtle", + "zeroize", +] + +[[package]] +name = "rustls-native-certs" +version = "0.8.1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "7fcff2dd52b58a8d98a70243663a0d234c4e2b79235637849d15913394a247d3" +dependencies = [ + "openssl-probe", + "rustls-pki-types", + "schannel", + "security-framework", +] + +[[package]] +name = "rustls-pki-types" +version = "1.12.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "229a4a4c221013e7e1f1a043678c5cc39fe5171437c88fb47151a21e6f5b5c79" +dependencies = [ + "zeroize", +] + +[[package]] +name = "rustls-platform-verifier" +version = "0.5.3" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "19787cda76408ec5404443dc8b31795c87cd8fec49762dc75fa727740d34acc1" +dependencies = [ + "core-foundation", + "core-foundation-sys", + "jni", + "log", + "once_cell", + "rustls", + "rustls-native-certs", + "rustls-platform-verifier-android", + "rustls-webpki", + "security-framework", + "security-framework-sys", + "webpki-root-certs 0.26.11", + "windows-sys 0.59.0", +] + +[[package]] +name = "rustls-platform-verifier-android" +version = "0.1.1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "f87165f0995f63a9fbeea62b64d10b4d9d8e78ec6d7d51fb2125fda7bb36788f" + +[[package]] +name = "rustls-webpki" +version = "0.103.4" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "0a17884ae0c1b773f1ccd2bd4a8c72f16da897310a98b0e84bf349ad5ead92fc" +dependencies = [ + "ring", + "rustls-pki-types", + "untrusted", +] + +[[package]] +name = "rustversion" +version = "1.0.22" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "b39cdef0fa800fc44525c84ccb54a029961a8215f9619753635a9c0d2538d46d" + +[[package]] +name = "ryu" +version = "1.0.20" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "28d3b2b1366ec20994f1fd18c3c594f05c5dd4bc44d8bb0c1c632c8d6829481f" + +[[package]] +name = "sal-service-manager" +version = "0.1.0" +dependencies = [ + "async-trait", + "chrono", + "futures", + "log", + "once_cell", + "plist", + "serde", + "serde_json", + "thiserror", + "tokio", + "zinit-client", +] + +[[package]] +name = "same-file" +version = "1.0.6" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "93fc1dc3aaa9bfed95e02e6eadabb4baf7e3078b0bd1b4d7b6b0b68378900502" +dependencies = [ + "winapi-util", +] + +[[package]] +name = "schannel" +version = "0.1.27" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "1f29ebaa345f945cec9fbbc532eb307f0fdad8161f281b6369539c8d84876b3d" +dependencies = [ + "windows-sys 0.59.0", +] + +[[package]] +name = "scopeguard" +version = "1.2.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "94143f37725109f92c262ed2cf5e59bce7498c01bcc1502d7b9afe439a4e9f49" + +[[package]] +name = "security-framework" +version = "3.3.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "80fb1d92c5028aa318b4b8bd7302a5bfcf48be96a37fc6fc790f806b0004ee0c" +dependencies = [ + "bitflags", + "core-foundation", + "core-foundation-sys", + "libc", + "security-framework-sys", +] + +[[package]] +name = "security-framework-sys" +version = "2.14.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "49db231d56a190491cb4aeda9527f1ad45345af50b0851622a7adb8c03b01c32" +dependencies = [ + "core-foundation-sys", + "libc", +] + +[[package]] +name = "serde" +version = "1.0.219" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "5f0e2c6ed6606019b4e29e69dbaba95b11854410e5347d525002456dbbb786b6" +dependencies = [ + "serde_derive", +] + +[[package]] +name = "serde-wasm-bindgen" +version = "0.5.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "f3b143e2833c57ab9ad3ea280d21fd34e285a42837aeb0ee301f4f41890fa00e" +dependencies = [ + "js-sys", + "serde", + "wasm-bindgen", +] + +[[package]] +name = "serde-wasm-bindgen" +version = "0.6.5" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "8302e169f0eddcc139c70f139d19d6467353af16f9fce27e8c30158036a1e16b" +dependencies = [ + "js-sys", + "serde", + "wasm-bindgen", +] + +[[package]] +name = "serde_derive" +version = "1.0.219" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "5b0276cf7f2c73365f7157c8123c21cd9a50fbbd844757af28ca1f5925fc2a00" +dependencies = [ + "proc-macro2", + "quote", + "syn 2.0.106", +] + +[[package]] +name = "serde_json" +version = "1.0.143" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "d401abef1d108fbd9cbaebc3e46611f4b1021f714a0597a71f41ee463f5f4a5a" +dependencies = [ + "itoa", + "memchr", + "ryu", + "serde", +] + +[[package]] +name = "serde_spanned" +version = "0.6.9" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "bf41e0cfaf7226dca15e8197172c295a782857fcb97fad1808a166870dee75a3" +dependencies = [ + "serde", +] + +[[package]] +name = "serde_urlencoded" +version = "0.7.1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "d3491c14715ca2294c4d6a88f15e84739788c1d030eed8c110436aafdaa2f3fd" +dependencies = [ + "form_urlencoded", + "itoa", + "ryu", + "serde", +] + +[[package]] +name = "sha1" +version = "0.10.6" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "e3bf829a2d51ab4a5ddf1352d8470c140cadc8301b2ae1789db023f01cedd6ba" +dependencies = [ + "cfg-if", + "cpufeatures", + "digest", +] + +[[package]] +name = "sha1_smol" +version = "1.0.1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "bbfa15b3dddfee50a0fff136974b3e1bde555604ba463834a7eb7deb6417705d" + +[[package]] +name = "shlex" +version = "1.3.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "0fda2ff0d084019ba4d7c6f371c95d8fd75ce3524c3cb8fb653a3023f6323e64" + +[[package]] +name = "signal-hook-registry" +version = "1.4.6" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "b2a4719bff48cee6b39d12c020eeb490953ad2443b7055bd0b21fca26bd8c28b" +dependencies = [ + "libc", +] + +[[package]] +name = "slab" +version = "0.4.11" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "7a2ae44ef20feb57a68b23d846850f861394c2e02dc425a50098ae8c90267589" + +[[package]] +name = "smallvec" +version = "1.15.1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "67b1b7a3b5fe4f1376887184045fcf45c69e92af734b7aaddc05fb777b6fbd03" + +[[package]] +name = "socket2" +version = "0.5.10" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "e22376abed350d73dd1cd119b57ffccad95b4e585a7cda43e286245ce23c0678" +dependencies = [ + "libc", + "windows-sys 0.52.0", +] + +[[package]] +name = "socket2" +version = "0.6.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "233504af464074f9d066d7b5416c5f9b894a5862a6506e306f7b816cdd6f1807" +dependencies = [ + "libc", + "windows-sys 0.59.0", +] + +[[package]] +name = "soketto" +version = "0.8.1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "2e859df029d160cb88608f5d7df7fb4753fd20fdfb4de5644f3d8b8440841721" +dependencies = [ + "base64", + "bytes", + "futures", + "http 1.3.1", + "httparse", + "log", + "rand", + "sha1", +] + +[[package]] +name = "stable_deref_trait" +version = "1.2.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "a8f112729512f8e442d81f95a8a7ddf2b7c6b8a1a6f509a95864142b30cab2d3" + +[[package]] +name = "strsim" +version = "0.11.1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "7da8b5736845d9f2fcb837ea5d9e2628564b3b043a70948a3f0b778838c5fb4f" + +[[package]] +name = "subtle" +version = "2.6.1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "13c2bddecc57b384dee18652358fb23172facb8a2c51ccc10d74c157bdea3292" + +[[package]] +name = "supervisor-admin-ui" +version = "0.1.0" +dependencies = [ + "gloo 0.11.0", + "hero-supervisor-openrpc-client", + "js-sys", + "log", + "serde", + "serde_json", + "uuid", + "wasm-bindgen", + "wasm-bindgen-futures", + "wasm-logger", + "web-sys", + "yew", +] + +[[package]] +name = "syn" +version = "1.0.109" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "72b64191b275b66ffe2469e8af2c1cfe3bafa67b529ead792a6d0160888b4237" +dependencies = [ + "proc-macro2", + "unicode-ident", +] + +[[package]] +name = "syn" +version = "2.0.106" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "ede7c438028d4436d71104916910f5bb611972c5cfd7f89b8300a8186e6fada6" +dependencies = [ + "proc-macro2", + "quote", + "unicode-ident", +] + +[[package]] +name = "synstructure" +version = "0.13.2" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "728a70f3dbaf5bab7f0c4b1ac8d7ae5ea60a4b5549c8a5914361c99147a709d2" +dependencies = [ + "proc-macro2", + "quote", + "syn 2.0.106", +] + +[[package]] +name = "termcolor" +version = "1.4.1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "06794f8f6c5c898b3275aebefa6b8a1cb24cd2c6c79397ab15774837a0bc5755" +dependencies = [ + "winapi-util", +] + +[[package]] +name = "thiserror" +version = "1.0.69" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "b6aaf5339b578ea85b50e080feb250a3e8ae8cfcdff9a461c9ec2904bc923f52" +dependencies = [ + "thiserror-impl", +] + +[[package]] +name = "thiserror-impl" +version = "1.0.69" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "4fee6c4efc90059e10f81e6d42c60a18f76588c3d74cb83a0b242a2b6c7504c1" +dependencies = [ + "proc-macro2", + "quote", + "syn 2.0.106", +] + +[[package]] +name = "time" +version = "0.3.41" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "8a7619e19bc266e0f9c5e6686659d394bc57973859340060a69221e57dbc0c40" +dependencies = [ + "deranged", + "itoa", + "num-conv", + "powerfmt", + "serde", + "time-core", + "time-macros", +] + +[[package]] +name = "time-core" +version = "0.1.4" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "c9e9a38711f559d9e3ce1cdb06dd7c5b8ea546bc90052da6d06bb76da74bb07c" + +[[package]] +name = "time-macros" +version = "0.2.22" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "3526739392ec93fd8b359c8e98514cb3e8e021beb4e5f597b00a0221f8ed8a49" +dependencies = [ + "num-conv", + "time-core", +] + +[[package]] +name = "tinystr" +version = "0.8.1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "5d4f6d1145dcb577acf783d4e601bc1d76a13337bb54e6233add580b07344c8b" +dependencies = [ + "displaydoc", + "zerovec", +] + +[[package]] +name = "tokio" +version = "1.47.1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "89e49afdadebb872d3145a5638b59eb0691ea23e46ca484037cfab3b76b95038" +dependencies = [ + "backtrace", + "bytes", + "io-uring", + "libc", + "mio", + "parking_lot", + "pin-project-lite", + "signal-hook-registry", + "slab", + "socket2 0.6.0", + "tokio-macros", + "windows-sys 0.59.0", +] + +[[package]] +name = "tokio-macros" +version = "2.5.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "6e06d43f1345a3bcd39f6a56dbb7dcab2ba47e68e8ac134855e7e2bdbaf8cab8" +dependencies = [ + "proc-macro2", + "quote", + "syn 2.0.106", +] + +[[package]] +name = "tokio-rustls" +version = "0.26.2" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "8e727b36a1a0e8b74c376ac2211e40c2c8af09fb4013c60d910495810f008e9b" +dependencies = [ + "rustls", + "tokio", +] + +[[package]] +name = "tokio-stream" +version = "0.1.17" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "eca58d7bba4a75707817a2c44174253f9236b2d5fbd055602e9d5c07c139a047" +dependencies = [ + "futures-core", + "pin-project-lite", + "tokio", + "tokio-util", +] + +[[package]] +name = "tokio-util" +version = "0.7.16" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "14307c986784f72ef81c89db7d9e28d6ac26d16213b109ea501696195e6e3ce5" +dependencies = [ + "bytes", + "futures-core", + "futures-io", + "futures-sink", + "pin-project-lite", + "tokio", +] + +[[package]] +name = "toml" +version = "0.8.23" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "dc1beb996b9d83529a9e75c17a1686767d148d70663143c7854d8b4a09ced362" +dependencies = [ + "serde", + "serde_spanned", + "toml_datetime", + "toml_edit 0.22.27", +] + +[[package]] +name = "toml_datetime" +version = "0.6.11" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "22cddaf88f4fbc13c51aebbf5f8eceb5c7c5a9da2ac40a13519eb5b0a0e8f11c" +dependencies = [ + "serde", +] + +[[package]] +name = "toml_edit" +version = "0.19.15" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "1b5bb770da30e5cbfde35a2d7b9b8a2c4b8ef89548a7a6aeab5c9a576e3e7421" +dependencies = [ + "indexmap", + "toml_datetime", + "winnow 0.5.40", +] + +[[package]] +name = "toml_edit" +version = "0.22.27" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "41fe8c660ae4257887cf66394862d21dbca4a6ddd26f04a3560410406a2f819a" +dependencies = [ + "indexmap", + "serde", + "serde_spanned", + "toml_datetime", + "toml_write", + "winnow 0.7.13", +] + +[[package]] +name = "toml_write" +version = "0.1.2" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "5d99f8c9a7727884afe522e9bd5edbfc91a3312b36a77b5fb8926e4c31a41801" + +[[package]] +name = "tower" +version = "0.4.13" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "b8fa9be0de6cf49e536ce1851f987bd21a43b771b09473c3549a6c853db37c1c" +dependencies = [ + "futures-core", + "futures-util", + "pin-project", + "pin-project-lite", + "tower-layer", + "tower-service", + "tracing", +] + +[[package]] +name = "tower-http" +version = "0.5.2" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "1e9cd434a998747dd2c4276bc96ee2e0c7a2eadf3cae88e52be55a05fa9053f5" +dependencies = [ + "bitflags", + "bytes", + "http 1.3.1", + "http-body", + "http-body-util", + "pin-project-lite", + "tower-layer", + "tower-service", +] + +[[package]] +name = "tower-layer" +version = "0.3.3" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "121c2a6cda46980bb0fcd1647ffaf6cd3fc79a013de288782836f6df9c48780e" + +[[package]] +name = "tower-service" +version = "0.3.3" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "8df9b6e13f2d32c91b9bd719c00d1958837bc7dec474d94952798cc8e69eeec3" + +[[package]] +name = "tracing" +version = "0.1.41" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "784e0ac535deb450455cbfa28a6f0df145ea1bb7ae51b821cf5e7927fdcfbdd0" +dependencies = [ + "log", + "pin-project-lite", + "tracing-attributes", + "tracing-core", +] + +[[package]] +name = "tracing-attributes" +version = "0.1.30" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "81383ab64e72a7a8b8e13130c49e3dab29def6d0c7d76a03087b3cf71c5c6903" +dependencies = [ + "proc-macro2", + "quote", + "syn 2.0.106", +] + +[[package]] +name = "tracing-core" +version = "0.1.34" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "b9d12581f227e93f094d3af2ae690a574abb8a2b9b7a96e7cfe9647b2b617678" +dependencies = [ + "once_cell", +] + +[[package]] +name = "try-lock" +version = "0.2.5" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "e421abadd41a4225275504ea4d6566923418b7f05506fbc9c0fe86ba7396114b" + +[[package]] +name = "typenum" +version = "1.18.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "1dccffe3ce07af9386bfd29e80c0ab1a8205a2fc34e4bcd40364df902cfa8f3f" + +[[package]] +name = "unicode-ident" +version = "1.0.18" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "5a5f39404a5da50712a4c1eecf25e90dd62b613502b7e925fd4e4d19b5c96512" + +[[package]] +name = "untrusted" +version = "0.9.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "8ecb6da28b8a351d773b68d5825ac39017e680750f980f3a1a85cd8dd28a47c1" + +[[package]] +name = "url" +version = "2.5.7" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "08bc136a29a3d1758e07a9cca267be308aeebf5cfd5a10f3f67ab2097683ef5b" +dependencies = [ + "form_urlencoded", + "idna", + "percent-encoding", + "serde", +] + +[[package]] +name = "utf8_iter" +version = "1.0.4" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "b6c140620e7ffbb22c2dee59cafe6084a59b5ffc27a8859a5f0d494b5d52b6be" + +[[package]] +name = "utf8parse" +version = "0.2.2" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "06abde3611657adf66d383f00b093d7faecc7fa57071cce2578660c9f1010821" + +[[package]] +name = "uuid" +version = "1.18.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "f33196643e165781c20a5ead5582283a7dacbb87855d867fbc2df3f81eddc1be" +dependencies = [ + "getrandom 0.3.3", + "js-sys", + "serde", + "wasm-bindgen", +] + +[[package]] +name = "version_check" +version = "0.9.5" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "0b928f33d975fc6ad9f86c8f283853ad26bdd5b10b7f1542aa2fa15e2289105a" + +[[package]] +name = "walkdir" +version = "2.5.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "29790946404f91d9c5d06f9874efddea1dc06c5efe94541a7d6863108e3a5e4b" +dependencies = [ + "same-file", + "winapi-util", +] + +[[package]] +name = "want" +version = "0.3.1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "bfa7760aed19e106de2c7c0b581b509f2f25d3dacaf737cb82ac61bc6d760b0e" +dependencies = [ + "try-lock", +] + +[[package]] +name = "wasi" +version = "0.11.1+wasi-snapshot-preview1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "ccf3ec651a847eb01de73ccad15eb7d99f80485de043efb2f370cd654f4ea44b" + +[[package]] +name = "wasi" +version = "0.14.2+wasi-0.2.4" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "9683f9a5a998d873c0d21fcbe3c083009670149a8fab228644b8bd36b2c48cb3" +dependencies = [ + "wit-bindgen-rt", +] + +[[package]] +name = "wasm-bindgen" +version = "0.2.100" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "1edc8929d7499fc4e8f0be2262a241556cfc54a0bea223790e71446f2aab1ef5" +dependencies = [ + "cfg-if", + "once_cell", + "rustversion", + "wasm-bindgen-macro", +] + +[[package]] +name = "wasm-bindgen-backend" +version = "0.2.100" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "2f0a0651a5c2bc21487bde11ee802ccaf4c51935d0d3d42a6101f98161700bc6" +dependencies = [ + "bumpalo", + "log", + "proc-macro2", + "quote", + "syn 2.0.106", + "wasm-bindgen-shared", +] + +[[package]] +name = "wasm-bindgen-futures" +version = "0.4.50" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "555d470ec0bc3bb57890405e5d4322cc9ea83cebb085523ced7be4144dac1e61" +dependencies = [ + "cfg-if", + "js-sys", + "once_cell", + "wasm-bindgen", + "web-sys", +] + +[[package]] +name = "wasm-bindgen-macro" +version = "0.2.100" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "7fe63fc6d09ed3792bd0897b314f53de8e16568c2b3f7982f468c0bf9bd0b407" +dependencies = [ + "quote", + "wasm-bindgen-macro-support", +] + +[[package]] +name = "wasm-bindgen-macro-support" +version = "0.2.100" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "8ae87ea40c9f689fc23f209965b6fb8a99ad69aeeb0231408be24920604395de" +dependencies = [ + "proc-macro2", + "quote", + "syn 2.0.106", + "wasm-bindgen-backend", + "wasm-bindgen-shared", +] + +[[package]] +name = "wasm-bindgen-shared" +version = "0.2.100" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "1a05d73b933a847d6cccdda8f838a22ff101ad9bf93e33684f39c1f5f0eece3d" +dependencies = [ + "unicode-ident", +] + +[[package]] +name = "wasm-logger" +version = "0.2.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "074649a66bb306c8f2068c9016395fa65d8e08d2affcbf95acf3c24c3ab19718" +dependencies = [ + "log", + "wasm-bindgen", + "web-sys", +] + +[[package]] +name = "web-sys" +version = "0.3.77" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "33b6dd2ef9186f1f2072e409e99cd22a975331a6b3591b12c764e0e55c60d5d2" +dependencies = [ + "js-sys", + "wasm-bindgen", +] + +[[package]] +name = "webpki-root-certs" +version = "0.26.11" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "75c7f0ef91146ebfb530314f5f1d24528d7f0767efbfd31dce919275413e393e" +dependencies = [ + "webpki-root-certs 1.0.2", +] + +[[package]] +name = "webpki-root-certs" +version = "1.0.2" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "4e4ffd8df1c57e87c325000a3d6ef93db75279dc3a231125aac571650f22b12a" +dependencies = [ + "rustls-pki-types", +] + +[[package]] +name = "winapi-util" +version = "0.1.10" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "0978bf7171b3d90bac376700cb56d606feb40f251a475a5d6634613564460b22" +dependencies = [ + "windows-sys 0.60.2", +] + +[[package]] +name = "windows-core" +version = "0.61.2" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "c0fdd3ddb90610c7638aa2b3a3ab2904fb9e5cdbecc643ddb3647212781c4ae3" +dependencies = [ + "windows-implement", + "windows-interface", + "windows-link", + "windows-result", + "windows-strings", +] + +[[package]] +name = "windows-implement" +version = "0.60.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "a47fddd13af08290e67f4acabf4b459f647552718f683a7b415d290ac744a836" +dependencies = [ + "proc-macro2", + "quote", + "syn 2.0.106", +] + +[[package]] +name = "windows-interface" +version = "0.59.1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "bd9211b69f8dcdfa817bfd14bf1c97c9188afa36f4750130fcdf3f400eca9fa8" +dependencies = [ + "proc-macro2", + "quote", + "syn 2.0.106", +] + +[[package]] +name = "windows-link" +version = "0.1.3" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "5e6ad25900d524eaabdbbb96d20b4311e1e7ae1699af4fb28c17ae66c80d798a" + +[[package]] +name = "windows-result" +version = "0.3.4" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "56f42bd332cc6c8eac5af113fc0c1fd6a8fd2aa08a0119358686e5160d0586c6" +dependencies = [ + "windows-link", +] + +[[package]] +name = "windows-strings" +version = "0.4.2" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "56e6c93f3a0c3b36176cb1327a4958a0353d5d166c2a35cb268ace15e91d3b57" +dependencies = [ + "windows-link", +] + +[[package]] +name = "windows-sys" +version = "0.45.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "75283be5efb2831d37ea142365f009c02ec203cd29a3ebecbc093d52315b66d0" +dependencies = [ + "windows-targets 0.42.2", +] + +[[package]] +name = "windows-sys" +version = "0.52.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "282be5f36a8ce781fad8c8ae18fa3f9beff57ec1b52cb3de0789201425d9a33d" +dependencies = [ + "windows-targets 0.52.6", +] + +[[package]] +name = "windows-sys" +version = "0.59.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "1e38bc4d79ed67fd075bcc251a1c39b32a1776bbe92e5bef1f0bf1f8c531853b" +dependencies = [ + "windows-targets 0.52.6", +] + +[[package]] +name = "windows-sys" +version = "0.60.2" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "f2f500e4d28234f72040990ec9d39e3a6b950f9f22d3dba18416c35882612bcb" +dependencies = [ + "windows-targets 0.53.3", +] + +[[package]] +name = "windows-targets" +version = "0.42.2" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "8e5180c00cd44c9b1c88adb3693291f1cd93605ded80c250a75d472756b4d071" +dependencies = [ + "windows_aarch64_gnullvm 0.42.2", + "windows_aarch64_msvc 0.42.2", + "windows_i686_gnu 0.42.2", + "windows_i686_msvc 0.42.2", + "windows_x86_64_gnu 0.42.2", + "windows_x86_64_gnullvm 0.42.2", + "windows_x86_64_msvc 0.42.2", +] + +[[package]] +name = "windows-targets" +version = "0.52.6" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "9b724f72796e036ab90c1021d4780d4d3d648aca59e491e6b98e725b84e99973" +dependencies = [ + "windows_aarch64_gnullvm 0.52.6", + "windows_aarch64_msvc 0.52.6", + "windows_i686_gnu 0.52.6", + "windows_i686_gnullvm 0.52.6", + "windows_i686_msvc 0.52.6", + "windows_x86_64_gnu 0.52.6", + "windows_x86_64_gnullvm 0.52.6", + "windows_x86_64_msvc 0.52.6", +] + +[[package]] +name = "windows-targets" +version = "0.53.3" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "d5fe6031c4041849d7c496a8ded650796e7b6ecc19df1a431c1a363342e5dc91" +dependencies = [ + "windows-link", + "windows_aarch64_gnullvm 0.53.0", + "windows_aarch64_msvc 0.53.0", + "windows_i686_gnu 0.53.0", + "windows_i686_gnullvm 0.53.0", + "windows_i686_msvc 0.53.0", + "windows_x86_64_gnu 0.53.0", + "windows_x86_64_gnullvm 0.53.0", + "windows_x86_64_msvc 0.53.0", +] + +[[package]] +name = "windows_aarch64_gnullvm" +version = "0.42.2" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "597a5118570b68bc08d8d59125332c54f1ba9d9adeedeef5b99b02ba2b0698f8" + +[[package]] +name = "windows_aarch64_gnullvm" +version = "0.52.6" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "32a4622180e7a0ec044bb555404c800bc9fd9ec262ec147edd5989ccd0c02cd3" + +[[package]] +name = "windows_aarch64_gnullvm" +version = "0.53.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "86b8d5f90ddd19cb4a147a5fa63ca848db3df085e25fee3cc10b39b6eebae764" + +[[package]] +name = "windows_aarch64_msvc" +version = "0.42.2" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "e08e8864a60f06ef0d0ff4ba04124db8b0fb3be5776a5cd47641e942e58c4d43" + +[[package]] +name = "windows_aarch64_msvc" +version = "0.52.6" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "09ec2a7bb152e2252b53fa7803150007879548bc709c039df7627cabbd05d469" + +[[package]] +name = "windows_aarch64_msvc" +version = "0.53.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "c7651a1f62a11b8cbd5e0d42526e55f2c99886c77e007179efff86c2b137e66c" + +[[package]] +name = "windows_i686_gnu" +version = "0.42.2" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "c61d927d8da41da96a81f029489353e68739737d3beca43145c8afec9a31a84f" + +[[package]] +name = "windows_i686_gnu" +version = "0.52.6" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "8e9b5ad5ab802e97eb8e295ac6720e509ee4c243f69d781394014ebfe8bbfa0b" + +[[package]] +name = "windows_i686_gnu" +version = "0.53.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "c1dc67659d35f387f5f6c479dc4e28f1d4bb90ddd1a5d3da2e5d97b42d6272c3" + +[[package]] +name = "windows_i686_gnullvm" +version = "0.52.6" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "0eee52d38c090b3caa76c563b86c3a4bd71ef1a819287c19d586d7334ae8ed66" + +[[package]] +name = "windows_i686_gnullvm" +version = "0.53.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "9ce6ccbdedbf6d6354471319e781c0dfef054c81fbc7cf83f338a4296c0cae11" + +[[package]] +name = "windows_i686_msvc" +version = "0.42.2" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "44d840b6ec649f480a41c8d80f9c65108b92d89345dd94027bfe06ac444d1060" + +[[package]] +name = "windows_i686_msvc" +version = "0.52.6" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "240948bc05c5e7c6dabba28bf89d89ffce3e303022809e73deaefe4f6ec56c66" + +[[package]] +name = "windows_i686_msvc" +version = "0.53.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "581fee95406bb13382d2f65cd4a908ca7b1e4c2f1917f143ba16efe98a589b5d" + +[[package]] +name = "windows_x86_64_gnu" +version = "0.42.2" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "8de912b8b8feb55c064867cf047dda097f92d51efad5b491dfb98f6bbb70cb36" + +[[package]] +name = "windows_x86_64_gnu" +version = "0.52.6" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "147a5c80aabfbf0c7d901cb5895d1de30ef2907eb21fbbab29ca94c5b08b1a78" + +[[package]] +name = "windows_x86_64_gnu" +version = "0.53.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "2e55b5ac9ea33f2fc1716d1742db15574fd6fc8dadc51caab1c16a3d3b4190ba" + +[[package]] +name = "windows_x86_64_gnullvm" +version = "0.42.2" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "26d41b46a36d453748aedef1486d5c7a85db22e56aff34643984ea85514e94a3" + +[[package]] +name = "windows_x86_64_gnullvm" +version = "0.52.6" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "24d5b23dc417412679681396f2b49f3de8c1473deb516bd34410872eff51ed0d" + +[[package]] +name = "windows_x86_64_gnullvm" +version = "0.53.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "0a6e035dd0599267ce1ee132e51c27dd29437f63325753051e71dd9e42406c57" + +[[package]] +name = "windows_x86_64_msvc" +version = "0.42.2" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "9aec5da331524158c6d1a4ac0ab1541149c0b9505fde06423b02f5ef0106b9f0" + +[[package]] +name = "windows_x86_64_msvc" +version = "0.52.6" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "589f6da84c646204747d1270a2a5661ea66ed1cced2631d546fdfb155959f9ec" + +[[package]] +name = "windows_x86_64_msvc" +version = "0.53.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "271414315aff87387382ec3d271b52d7ae78726f5d44ac98b4f4030c91880486" + +[[package]] +name = "winnow" +version = "0.5.40" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "f593a95398737aeed53e489c785df13f3618e41dbcd6718c6addbf1395aa6876" +dependencies = [ + "memchr", +] + +[[package]] +name = "winnow" +version = "0.7.13" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "21a0236b59786fed61e2a80582dd500fe61f18b5dca67a4a067d0bc9039339cf" +dependencies = [ + "memchr", +] + +[[package]] +name = "wit-bindgen-rt" +version = "0.39.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "6f42320e61fe2cfd34354ecb597f86f413484a798ba44a8ca1165c58d42da6c1" +dependencies = [ + "bitflags", +] + +[[package]] +name = "writeable" +version = "0.6.1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "ea2f10b9bb0928dfb1b42b65e1f9e36f7f54dbdf08457afefb38afcdec4fa2bb" + +[[package]] +name = "yew" +version = "0.21.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "5f1a03f255c70c7aa3e9c62e15292f142ede0564123543c1cc0c7a4f31660cac" +dependencies = [ + "console_error_panic_hook", + "futures", + "gloo 0.10.0", + "implicit-clone", + "indexmap", + "js-sys", + "prokio", + "rustversion", + "serde", + "slab", + "thiserror", + "tokio", + "tracing", + "wasm-bindgen", + "wasm-bindgen-futures", + "web-sys", + "yew-macro", +] + +[[package]] +name = "yew-macro" +version = "0.21.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "02fd8ca5166d69e59f796500a2ce432ff751edecbbb308ca59fd3fe4d0343de2" +dependencies = [ + "boolinator", + "once_cell", + "prettyplease", + "proc-macro-error", + "proc-macro2", + "quote", + "syn 2.0.106", +] + +[[package]] +name = "yoke" +version = "0.8.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "5f41bb01b8226ef4bfd589436a297c53d118f65921786300e427be8d487695cc" +dependencies = [ + "serde", + "stable_deref_trait", + "yoke-derive", + "zerofrom", +] + +[[package]] +name = "yoke-derive" +version = "0.8.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "38da3c9736e16c5d3c8c597a9aaa5d1fa565d0532ae05e27c24aa62fb32c0ab6" +dependencies = [ + "proc-macro2", + "quote", + "syn 2.0.106", + "synstructure", +] + +[[package]] +name = "zerocopy" +version = "0.8.26" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "1039dd0d3c310cf05de012d8a39ff557cb0d23087fd44cad61df08fc31907a2f" +dependencies = [ + "zerocopy-derive", +] + +[[package]] +name = "zerocopy-derive" +version = "0.8.26" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "9ecf5b4cc5364572d7f4c329661bcc82724222973f2cab6f050a4e5c22f75181" +dependencies = [ + "proc-macro2", + "quote", + "syn 2.0.106", +] + +[[package]] +name = "zerofrom" +version = "0.1.6" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "50cc42e0333e05660c3587f3bf9d0478688e15d870fab3346451ce7f8c9fbea5" +dependencies = [ + "zerofrom-derive", +] + +[[package]] +name = "zerofrom-derive" +version = "0.1.6" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "d71e5d6e06ab090c67b5e44993ec16b72dcbaabc526db883a360057678b48502" +dependencies = [ + "proc-macro2", + "quote", + "syn 2.0.106", + "synstructure", +] + +[[package]] +name = "zeroize" +version = "1.8.1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "ced3678a2879b30306d323f4542626697a464a97c0a07c9aebf7ebca65cd4dde" + +[[package]] +name = "zerotrie" +version = "0.2.2" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "36f0bbd478583f79edad978b407914f61b2972f5af6fa089686016be8f9af595" +dependencies = [ + "displaydoc", + "yoke", + "zerofrom", +] + +[[package]] +name = "zerovec" +version = "0.11.4" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "e7aa2bd55086f1ab526693ecbe444205da57e25f4489879da80635a46d90e73b" +dependencies = [ + "yoke", + "zerofrom", + "zerovec-derive", +] + +[[package]] +name = "zerovec-derive" +version = "0.11.1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "5b96237efa0c878c64bd89c436f661be4e46b2f3eff1ebb976f7ef2321d2f58f" +dependencies = [ + "proc-macro2", + "quote", + "syn 2.0.106", +] + +[[package]] +name = "zinit-client" +version = "0.4.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "4121c3ba22f1b3ccc4546de32072c9530c7e2735b734641ada5280ac422ac9cd" +dependencies = [ + "async-stream", + "async-trait", + "chrono", + "futures", + "rand", + "serde", + "serde_json", + "thiserror", + "tokio", + "tracing", +] diff --git a/clients/admin-ui/Cargo.toml b/clients/admin-ui/Cargo.toml new file mode 100644 index 0000000..bf115b0 --- /dev/null +++ b/clients/admin-ui/Cargo.toml @@ -0,0 +1,29 @@ +[package] +name = "supervisor-admin-ui" +version = "0.1.0" +edition = "2021" + +[lib] +crate-type = ["cdylib"] + +[dependencies] +yew = { version = "0.21", features = ["csr"] } +wasm-bindgen = "0.2" +wasm-bindgen-futures = "0.4" +web-sys = { version = "0.3", features = [ + "console", + "Document", + "Element", + "HtmlElement", + "Window", +] } +js-sys = "0.3" +serde = { version = "1.0", features = ["derive"] } +serde_json = "1.0" +gloo = { version = "0.11", features = ["console", "timers", "futures"] } +log = "0.4" +wasm-logger = "0.2" +uuid = { version = "1.0", features = ["v4", "js"] } + +# Use our new WASM OpenRPC client +hero-supervisor-openrpc-client = { path = "../clients/openrpc" } diff --git a/clients/admin-ui/Trunk.toml b/clients/admin-ui/Trunk.toml new file mode 100644 index 0000000..ee06fae --- /dev/null +++ b/clients/admin-ui/Trunk.toml @@ -0,0 +1,16 @@ +[build] +target = "index.html" +dist = "dist" + +[watch] +watch = ["src", "index.html", "styles.css"] + +[serve] +address = "127.0.0.1" +port = 8080 +open = false + +[[hooks]] +stage = "pre_build" +command = "echo" +command_arguments = ["Building Supervisor Admin UI..."] diff --git a/clients/admin-ui/index.html b/clients/admin-ui/index.html new file mode 100644 index 0000000..08a1f56 --- /dev/null +++ b/clients/admin-ui/index.html @@ -0,0 +1,13 @@ + + + + + + Hero Supervisor + + + + +
+ + diff --git a/clients/admin-ui/src/app.rs b/clients/admin-ui/src/app.rs new file mode 100644 index 0000000..da4cecb --- /dev/null +++ b/clients/admin-ui/src/app.rs @@ -0,0 +1,630 @@ +use yew::prelude::*; +use wasm_bindgen_futures::spawn_local; +use gloo::console; +use hero_supervisor_openrpc_client::wasm::{WasmSupervisorClient, WasmJob}; +use gloo::timers::callback::Interval; + + +use crate::sidebar::{Sidebar, SupervisorInfo}; +use crate::runners::{Runners, RegisterForm}; +use crate::jobs::Jobs; + +/// Generate a unique job ID client-side using UUID v4 +fn generate_job_id() -> String { + uuid::Uuid::new_v4().to_string() +} + +#[derive(Clone, Default)] +pub struct JobForm { + pub payload: String, + pub runner_name: String, + pub executor: String, + pub secret: String, +} + +#[derive(Clone, Debug, PartialEq)] +pub enum PingState { + Idle, + Waiting, + Success(String), // Result message + Error(String), // Error message +} + +impl Default for PingState { + fn default() -> Self { + PingState::Idle + } +} + +#[derive(Clone)] +pub struct AppState { + pub server_url: String, + pub runners: Vec<(String, String)>, // (name, status) + pub jobs: Vec, + pub ongoing_jobs: Vec, // Job IDs being polled + pub loading: bool, + pub register_form: RegisterForm, + pub job_form: JobForm, + pub supervisor_info: Option, + pub admin_secret: String, + pub ping_states: std::collections::HashMap, // runner_name -> ping_state +} + + + + + +#[function_component(App)] +pub fn app() -> Html { + let state = use_state(|| AppState { + server_url: "http://localhost:3030".to_string(), + runners: vec![], + jobs: vec![], + ongoing_jobs: vec![], + loading: false, + register_form: RegisterForm { + name: String::new(), + secret: String::new(), + }, + job_form: JobForm { + payload: String::new(), + runner_name: String::new(), + executor: String::new(), + secret: String::new(), + }, + supervisor_info: None, + admin_secret: String::new(), + ping_states: std::collections::HashMap::new(), + }); + + // Set up polling for ongoing jobs every 2 seconds + { + let state = state.clone(); + use_effect_with((), move |_| { + let state = state.clone(); + + let poll_jobs = { + let state = state.clone(); + Callback::from(move |_| { + let current_state = (*state).clone(); + if !current_state.ongoing_jobs.is_empty() { + let client = WasmSupervisorClient::new(current_state.server_url.clone()); + let state_clone = state.clone(); + + spawn_local(async move { + console::log!("Polling ongoing jobs:", format!("{:?}", current_state.ongoing_jobs)); + let mut updated_state = (*state_clone).clone(); + let mut jobs_to_remove = Vec::new(); + + // Poll each ongoing job + for job_id in ¤t_state.ongoing_jobs { + match client.get_job(job_id).await { + Ok(updated_job) => { + // Find and update the job in the jobs list + if let Some(job_index) = updated_state.jobs.iter().position(|j| j.id() == *job_id) { + updated_state.jobs[job_index] = updated_job.clone(); + console::log!("Updated job status for:", job_id); + } + } + Err(e) => { + console::error!("Failed to poll job:", job_id, format!("{:?}", e)); + // Remove failed jobs from ongoing list + jobs_to_remove.push(job_id.clone()); + } + } + } + + // Remove completed/failed jobs from ongoing list + for job_id in jobs_to_remove { + updated_state.ongoing_jobs.retain(|id| id != &job_id); + } + + state_clone.set(updated_state); + }); + } + }) + }; + + let interval = Interval::new(2000, move || { + poll_jobs.emit(()); + }); + + move || drop(interval) + }); + } + + // Load initial data when component mounts + let load_initial_data = { + let state = state.clone(); + let client_url = state.server_url.clone(); + Callback::from(move |_: ()| { + let state = state.clone(); + let client = WasmSupervisorClient::new(client_url.clone()); + spawn_local(async move { + console::log!("Loading initial data..."); + let mut current_state = (*state).clone(); + current_state.loading = true; + state.set(current_state.clone()); + + // Load runners and jobs in parallel + let runners_result = client.list_runners().await; + let jobs_result = client.list_jobs().await; + + match (runners_result, jobs_result) { + (Ok(runner_names), Ok(job_ids)) => { + console::log!("Successfully loaded runners:", format!("{:?}", runner_names)); + console::log!("Successfully loaded jobs:", format!("{:?}", job_ids)); + + let runners_with_status: Vec<(String, String)> = runner_names + .into_iter() + .map(|name| (name, "Running".to_string())) + .collect(); + + // Fetch full job details for each job ID and identify unfinished jobs + let mut jobs = Vec::new(); + let mut ongoing_jobs = Vec::new(); + + for job_id in job_ids { + match client.get_job(&job_id).await { + Ok(job) => { + // Check if job is unfinished (you may need to adjust this logic based on your job status field) + // For now, we'll assume all jobs are ongoing until we have proper status checking + ongoing_jobs.push(job_id.clone()); + jobs.push(job); + } + Err(e) => { + console::error!("Failed to fetch job details for:", &job_id, format!("{:?}", e)); + // Create placeholder job if fetch fails + jobs.push(WasmJob::new(job_id.clone(), "Loading...".to_string(), "Unknown".to_string(), "Unknown".to_string())); + } + } + } + + let mut updated_state = (*state).clone(); + updated_state.runners = runners_with_status; + updated_state.jobs = jobs; + updated_state.ongoing_jobs = ongoing_jobs.clone(); + updated_state.loading = false; + console::log!("Added ongoing jobs to polling:", format!("{:?}", ongoing_jobs)); + state.set(updated_state); + } + (Ok(runner_names), Err(jobs_err)) => { + console::log!("Successfully loaded runners:", format!("{:?}", runner_names)); + console::error!("Failed to load jobs:", format!("{:?}", jobs_err)); + + let runners_with_status: Vec<(String, String)> = runner_names + .into_iter() + .map(|name| (name, "Running".to_string())) + .collect(); + + let mut updated_state = (*state).clone(); + updated_state.runners = runners_with_status; + updated_state.loading = false; + state.set(updated_state); + } + (Err(runners_err), Ok(job_ids)) => { + console::error!("Failed to load runners:", format!("{:?}", runners_err)); + console::log!("Successfully loaded jobs:", format!("{:?}", job_ids)); + + // Convert job IDs to WasmJob objects + let jobs: Vec = job_ids + .into_iter() + .map(|id| { + WasmJob::new(id.clone(), "Loading...".to_string(), "Unknown".to_string(), "Unknown".to_string()) + }) + .collect(); + + let mut updated_state = (*state).clone(); + updated_state.jobs = jobs; + updated_state.loading = false; + state.set(updated_state); + } + (Err(runners_err), Err(jobs_err)) => { + console::error!("Failed to load runners:", format!("{:?}", runners_err)); + console::error!("Failed to load jobs:", format!("{:?}", jobs_err)); + let mut updated_state = (*state).clone(); + updated_state.loading = false; + state.set(updated_state); + } + } + }); + }) + }; + + use_effect_with((), move |_| { + load_initial_data.emit(()); + || () + }); + + let on_load_runners = { + let state = state.clone(); + let client_url = state.server_url.clone(); + Callback::from(move |_: ()| { + let state = state.clone(); + let client = WasmSupervisorClient::new(client_url.clone()); + spawn_local(async move { + console::log!("Loading runners..."); + let mut current_state = (*state).clone(); + current_state.loading = true; + state.set(current_state.clone()); + + match client.list_runners().await { + Ok(runner_names) => { + console::log!("Successfully loaded runners:", format!("{:?}", runner_names)); + // For now, assume all runners are "Running" - we'd need a separate status call + let runners_with_status: Vec<(String, String)> = runner_names + .into_iter() + .map(|name| (name, "Running".to_string())) + .collect(); + + let mut updated_state = (*state).clone(); + updated_state.runners = runners_with_status; + updated_state.loading = false; + state.set(updated_state); + } + Err(e) => { + console::error!("Failed to load runners:", format!("{:?}", e)); + let mut updated_state = (*state).clone(); + updated_state.loading = false; + state.set(updated_state); + } + } + }); + }) + }; + + + + + + let on_register_form_change = { + let state = state.clone(); + Callback::from(move |(field, value): (String, String)| { + let mut new_form = state.register_form.clone(); + match field.as_str() { + "name" => new_form.name = value, + "secret" => new_form.secret = value, + _ => {} + } + let new_state = AppState { + server_url: state.server_url.clone(), + runners: state.runners.clone(), + jobs: state.jobs.clone(), + ongoing_jobs: state.ongoing_jobs.clone(), + loading: state.loading, + register_form: new_form, + job_form: state.job_form.clone(), + supervisor_info: state.supervisor_info.clone(), + admin_secret: state.admin_secret.clone(), + ping_states: state.ping_states.clone(), + }; + state.set(new_state); + }) + }; + + let on_register_runner = { + let state = state.clone(); + Callback::from(move |_: ()| { + let current_state = (*state).clone(); + + // Add runner to UI immediately with "Registering" status + let new_runner = ( + current_state.register_form.name.clone(), + "Registering".to_string(), + ); + + let mut updated_runners = current_state.runners.clone(); + updated_runners.push(new_runner); + + let mut temp_state = current_state.clone(); + temp_state.runners = updated_runners; + + // Clear form and update status to "Running" + temp_state.register_form = RegisterForm { + name: String::new(), + secret: String::new(), + }; + + // Update the newly added runner status to "Running" + if let Some(runner) = temp_state.runners.iter_mut() + .find(|(name, _)| name == ¤t_state.register_form.name) { + runner.1 = "Running".to_string(); + } + + state.set(temp_state); + }) + }; + + + + // Admin secret change callback + let on_admin_secret_change = { + let state = state.clone(); + Callback::from(move |admin_secret: String| { + let mut new_state = (*state).clone(); + new_state.admin_secret = admin_secret; + state.set(new_state); + }) + }; + + // Job form change callback + let on_job_form_change = { + let state = state.clone(); + Callback::from(move |(field, value): (String, String)| { + let mut new_form = state.job_form.clone(); + match field.as_str() { + "payload" => new_form.payload = value, + "runner_name" => new_form.runner_name = value, + "executor" => new_form.executor = value, + "secret" => new_form.secret = value, + _ => {} + } + let mut new_state = (*state).clone(); + new_state.job_form = new_form; + state.set(new_state); + }) + }; + + // Run job callback - now uses create_job for immediate display and polling + let on_run_job = { + let state = state.clone(); + Callback::from(move |_| { + let current_state = (*state).clone(); + let client = WasmSupervisorClient::new(current_state.server_url.clone()); + let job_form = current_state.job_form.clone(); + let state_clone = state.clone(); + + spawn_local(async move { + console::log!("Creating job..."); + + // Generate unique job ID client-side + let job_id = generate_job_id(); + + // Create WasmJob from form data with client-generated ID + let job = WasmJob::new( + job_id.clone(), + job_form.payload.clone(), + job_form.executor.clone(), + job_form.runner_name.clone(), + ); + + // Immediately add job to the list with "pending" status + let mut updated_state = (*state_clone).clone(); + updated_state.jobs.push(job.clone()); + updated_state.ongoing_jobs.push(job_id.clone()); + // Clear the job form + updated_state.job_form = JobForm::default(); + state_clone.set(updated_state); + console::log!("Job added to list immediately with ID:", &job_id); + + // Create the job using fire-and-forget create_job method + match client.create_job(job_form.secret.clone(), job).await { + Ok(returned_job_id) => { + console::log!("Job created successfully with ID:", &returned_job_id); + } + Err(e) => { + console::error!("Failed to create job:", format!("{:?}", e)); + // Remove job from ongoing jobs if creation failed + let mut error_state = (*state_clone).clone(); + error_state.ongoing_jobs.retain(|id| id != &job_id); + state_clone.set(error_state); + } + } + }); + }) + }; + + // Supervisor info loaded callback + let on_supervisor_info_loaded = { + let state = state.clone(); + Callback::from(move |supervisor_info: SupervisorInfo| { + let mut new_state = (*state).clone(); + new_state.supervisor_info = Some(supervisor_info); + state.set(new_state); + }) + }; + + // Remove runner callback + let on_remove_runner = { + let state = state.clone(); + Callback::from(move |runner_id: String| { + let current_state = (*state).clone(); + let client = WasmSupervisorClient::new(current_state.server_url.clone()); + let state_clone = state.clone(); + + spawn_local(async move { + console::log!("Removing runner:", &runner_id); + + match client.remove_runner(&runner_id).await { + Ok(_) => { + console::log!("Runner removed successfully"); + // Remove runner from the list + let mut updated_state = (*state_clone).clone(); + updated_state.runners.retain(|(name, _)| name != &runner_id); + state_clone.set(updated_state); + } + Err(e) => { + console::error!("Failed to remove runner:", format!("{:?}", e)); + } + } + }); + }) + }; + + // Stop job callback + let on_stop_job = { + let state = state.clone(); + Callback::from(move |job_id: String| { + let current_state = (*state).clone(); + let client = WasmSupervisorClient::new(current_state.server_url.clone()); + let state_clone = state.clone(); + + spawn_local(async move { + console::log!("Stopping job:", &job_id); + + match client.stop_job(&job_id).await { + Ok(_) => { + console::log!("Job stopped successfully"); + // Remove job from ongoing jobs list + let mut updated_state = (*state_clone).clone(); + updated_state.ongoing_jobs.retain(|id| id != &job_id); + state_clone.set(updated_state); + } + Err(e) => { + console::error!("Failed to stop job:", format!("{:?}", e)); + } + } + }); + }) + }; + + // Delete job callback + let on_delete_job = { + let state = state.clone(); + Callback::from(move |job_id: String| { + let current_state = (*state).clone(); + let client = WasmSupervisorClient::new(current_state.server_url.clone()); + let state_clone = state.clone(); + + spawn_local(async move { + console::log!("Deleting job:", &job_id); + + match client.delete_job(&job_id).await { + Ok(_) => { + console::log!("Job deleted successfully"); + // Remove job from both jobs list and ongoing jobs list + let mut updated_state = (*state_clone).clone(); + updated_state.jobs.retain(|job| job.id() != job_id); + updated_state.ongoing_jobs.retain(|id| id != &job_id); + state_clone.set(updated_state); + } + Err(e) => { + console::error!("Failed to delete job:", format!("{:?}", e)); + } + } + }); + }) + }; + + // Ping runner callback - uses run_job for immediate result with proper state management + let on_ping_runner = { + let state = state.clone(); + Callback::from(move |(runner_id, secret): (String, String)| { + let current_state = (*state).clone(); + let client = WasmSupervisorClient::new(current_state.server_url.clone()); + let state_clone = state.clone(); + + // Set ping state to waiting + { + let mut updated_state = (*state_clone).clone(); + updated_state.ping_states.insert(runner_id.clone(), PingState::Waiting); + state_clone.set(updated_state); + } + + spawn_local(async move { + console::log!("Pinging runner:", &runner_id); + + // Generate unique job ID client-side + let job_id = generate_job_id(); + + // Create ping job with client-generated ID + let ping_job = WasmJob::new( + job_id.clone(), + "ping".to_string(), + "ping".to_string(), + runner_id.clone(), + ); + + // Use run_job for immediate result instead of create_job + match client.run_job(secret, ping_job).await { + Ok(result) => { + console::log!("Ping successful, result:", &result); + // Set ping state to success with result + let mut success_state = (*state_clone).clone(); + success_state.ping_states.insert(runner_id.clone(), PingState::Success(result)); + state_clone.set(success_state); + + // Reset to idle after 3 seconds + let state_reset = state_clone.clone(); + let runner_id_reset = runner_id.clone(); + spawn_local(async move { + gloo::timers::future::TimeoutFuture::new(3000).await; + let mut reset_state = (*state_reset).clone(); + reset_state.ping_states.insert(runner_id_reset, PingState::Idle); + state_reset.set(reset_state); + }); + } + Err(e) => { + console::error!("Failed to ping runner:", format!("{:?}", e)); + // Set ping state to error + let mut error_state = (*state_clone).clone(); + let error_msg = format!("Error: {:?}", e); + error_state.ping_states.insert(runner_id.clone(), PingState::Error(error_msg)); + state_clone.set(error_state); + + // Reset to idle after 3 seconds + let state_reset = state_clone.clone(); + let runner_id_reset = runner_id.clone(); + spawn_local(async move { + gloo::timers::future::TimeoutFuture::new(3000).await; + let mut reset_state = (*state_reset).clone(); + reset_state.ping_states.insert(runner_id_reset, PingState::Idle); + state_reset.set(reset_state); + }); + } + } + }); + }) + }; + + // Load initial data + use_effect_with((), { + let on_load_runners = on_load_runners.clone(); + move |_| { + on_load_runners.emit(()); + || () + } + }); + + html! { +
+ + +
+ + + + + // Floating refresh button + +
+
+ } +} diff --git a/clients/admin-ui/src/app.rs.backup b/clients/admin-ui/src/app.rs.backup new file mode 100644 index 0000000..e2ea211 --- /dev/null +++ b/clients/admin-ui/src/app.rs.backup @@ -0,0 +1,1099 @@ +use yew::prelude::*; +use wasm_bindgen_futures::spawn_local; +use gloo::console; +use hero_supervisor_openrpc_client::wasm::{WasmSupervisorClient, WasmJob}; +use wasm_bindgen::JsCast; + +#[derive(Clone, PartialEq)] +pub struct RunnerStatus { + pub name: String, + pub status: String, +} + +#[derive(Clone, PartialEq)] +pub struct Job { + pub id: String, + pub command: String, + pub type_: String, + pub runner: String, +} + +#[derive(Clone, PartialEq)] +pub struct RegisterForm { + pub name: String, + pub secret: String, + pub queue: String, +} + +#[derive(Clone, PartialEq)] +pub struct SupervisorInfo { + pub server_url: String, + pub admin_secrets_count: usize, + pub user_secrets_count: usize, + pub register_secrets_count: usize, + pub runners_count: usize, +} + +#[derive(Clone, PartialEq)] +pub struct AppState { + pub runners: Vec, + pub jobs: Vec, + pub register_form: RegisterForm, + pub loading: bool, + pub error: Option, + pub supervisor_info: Option, + // Secret management fields + pub add_admin_secret: String, + pub remove_admin_secret: String, + pub add_user_secret: String, + pub remove_user_secret: String, + pub add_register_secret: String, + pub remove_register_secret: String, + pub admin_secret_for_changes: String, +} + +impl Default for AppState { + fn default() -> Self { + Self { + runners: Vec::new(), + jobs: Vec::new(), + register_form: RegisterForm { + name: String::new(), + secret: String::new(), + queue: String::new(), + }, + loading: false, + error: None, + supervisor_info: None, + add_admin_secret: String::new(), + remove_admin_secret: String::new(), + add_user_secret: String::new(), + remove_user_secret: String::new(), + add_register_secret: String::new(), + remove_register_secret: String::new(), + admin_secret_for_changes: String::new(), + } + } +} + +#[function_component(App)] +pub fn app() -> Html { + let state = use_state(AppState::default); + + // Load initial data + let load_initial_data = { + let state = state.clone(); + Callback::from(move |_: ()| { + let state = state.clone(); + let client = WasmSupervisorClient::new("http://localhost:3030".to_string()); + spawn_local(async move { + console::log!("Loading initial data..."); + let mut current_state = (*state).clone(); + current_state.loading = true; + state.set(current_state.clone()); + + // Load runners + match client.list_runners().await { + Ok(runner_names) => { + console::log!("Successfully loaded runners:", format!("{:?}", runner_names)); + let runners_with_status: Vec = runner_names + .into_iter() + .map(|name| RunnerStatus { name, status: "Running".to_string() }) + .collect(); + + let mut updated_state = (*state).clone(); + updated_state.runners = runners_with_status; + updated_state.loading = false; + state.set(updated_state); + } + Err(e) => { + console::error!("Failed to load runners:", format!("{:?}", e)); + let mut updated_state = (*state).clone(); + updated_state.loading = false; + state.set(updated_state); + } + } + + // Load mock jobs for now + let mock_jobs = vec![ + Job { + id: "job-1".to_string(), + command: "echo 'Hello World'".to_string(), + type_: "script".to_string(), + runner: "runner-1".to_string(), + }, + Job { + id: "job-2".to_string(), + command: "ls -la".to_string(), + type_: "command".to_string(), + runner: "runner-2".to_string(), + }, + ]; + + let mut final_state = (*state).clone(); + final_state.jobs = mock_jobs; + state.set(final_state); + }); + }) + }; + + use_effect_with((), move |_| { + load_initial_data.emit(()); + || () + }); + + // Register form change handler + let on_register_form_change = { + let state = state.clone(); + Callback::from(move |(field, value): (String, String)| { + let mut new_state = (*state).clone(); + match field.as_str() { + "name" => new_state.register_form.name = value, + "secret" => new_state.register_form.secret = value, + "queue" => new_state.register_form.queue = value, + _ => {} + } + state.set(new_state); + }) + }; + + // Register runner handler + let on_register_runner = { + let state = state.clone(); + Callback::from(move |_: ()| { + let state = state.clone(); + let client = WasmSupervisorClient::new("http://localhost:3030".to_string()); + spawn_local(async move { + console::log!("Registering runner..."); + let current_state = (*state).clone(); + + // Validate form data + if current_state.register_form.name.is_empty() { + console::error!("Runner name is required"); + return; + } + if current_state.register_form.secret.is_empty() { + console::error!("Secret is required"); + return; + } + if current_state.register_form.queue.is_empty() { + console::error!("Queue name is required"); + return; + } + + // Add runner to UI immediately with "Registering" status + let new_runner = RunnerStatus { + name: current_state.register_form.name.clone(), + status: "Registering".to_string(), + }; + + let mut updated_runners = current_state.runners.clone(); + updated_runners.push(new_runner); + + let mut temp_state = current_state.clone(); + temp_state.runners = updated_runners; + state.set(temp_state); + + // Make actual registration call + match client.register_runner( + ¤t_state.register_form.secret, + ¤t_state.register_form.name, + ¤t_state.register_form.queue, + ).await { + Ok(_) => { + console::log!("Runner registered successfully!"); + + // Update status to "Running" and clear form + let mut final_state = (*state).clone(); + if let Some(runner) = final_state.runners.iter_mut() + .find(|r| r.name == current_state.register_form.name) { + runner.status = "Running".to_string(); + } + final_state.register_form = RegisterForm { + name: String::new(), + secret: String::new(), + queue: String::new(), + }; + state.set(final_state); + } + Err(e) => { + console::error!("Failed to register runner:", format!("{:?}", e)); + + // Remove the failed runner from the list + let mut error_state = (*state).clone(); + error_state.runners.retain(|r| r.name != current_state.register_form.name); + state.set(error_state); + } + } + }); + }) + }; + + html! { +
+ // Persistent Island Column Sidebar + + + // Main Content Area +
+ // Runners Section +
+

{"Runners"}

+
+ // Registration Card (First Card) +
+
+
{"Register New Runner"}
+
+
+
+ +
+
+ +
+
+ +
+ +
+
+ + // Existing Runners + {for state.runners.iter().map(|runner| { + html! { +
+
+
{&runner.name}
+ + {&runner.status} + +
+
+

{"Status: "}{&runner.status}

+
+
+ } + })} +
+
+ + // Jobs Section +
+

{"Jobs"}

+
+ + + + + + + + + + + {for state.jobs.iter().map(|job| { + html! { + + + + + + + } + })} + +
{"ID"}{"Command"}{"Type"}{"Runner"}
{&job.id}{&job.command}{&job.type_}{&job.runner}
+
+
+
+
+ } +} + supervisor_info: None, + add_admin_secret: String::new(), + remove_admin_secret: String::new(), + add_user_secret: String::new(), + remove_user_secret: String::new(), + add_register_secret: String::new(), + remove_register_secret: String::new(), + admin_secret_for_changes: String::new(), + }); + + // Load initial data when component mounts + let load_initial_data = { + let state = state.clone(); + Callback::from(move |_: ()| { + let state = state.clone(); + let client = WasmSupervisorClient::new("http://localhost:3030".to_string()); + spawn_local(async move { + console::log!("Loading initial data..."); + let mut current_state = (*state).clone(); + current_state.loading = true; + state.set(current_state.clone()); + + // Load supervisor info + match client.get_supervisor_info("".to_string()).await { + Ok(info) => { + console::log!("Successfully loaded supervisor info"); + let mut updated_state = (*state).clone(); + updated_state.supervisor_info = Some(SupervisorInfo { + server_url: "http://localhost:3030".to_string(), + admin_secrets_count: info.admin_secrets_count, + user_secrets_count: info.user_secrets_count, + register_secrets_count: info.register_secrets_count, + runners_count: info.runners_count, + }); + updated_state.loading = false; + state.set(updated_state); + } + Err(e) => { + console::error!("Failed to load supervisor info:", format!("{:?}", e)); + let mut updated_state = (*state).clone(); + updated_state.loading = false; + state.set(updated_state); + } + } + + // Load runners + match client.list_runners().await { + Ok(runner_names) => { + console::log!("Successfully loaded runners:", format!("{:?}", runner_names)); + let runners_with_status: Vec = runner_names + .into_iter() + .map(|name| RunnerStatus { name, status: "Running".to_string() }) + .collect(); + + let mut updated_state = (*state).clone(); + updated_state.runners = runners_with_status; + state.set(updated_state); + } + Err(e) => { + console::error!("Failed to load runners:", format!("{:?}", e)); + } + } + }); + }) + }; + + use_effect_with((), move |_| { + load_initial_data.emit(()); + || () + }); + + // Register form change handler + let on_register_form_change = { + let state = state.clone(); + Callback::from(move |(field, value): (String, String)| { + let mut new_state = (*state).clone(); + match field.as_str() { + "name" => new_state.register_form.name = value, + "secret" => new_state.register_form.secret = value, + "queue" => new_state.register_form.queue = value, + _ => {} + } + state.set(new_state); + }) + }; + + + // Register runner handler + let on_register_runner = { + let state = state.clone(); + Callback::from(move |_: ()| { + let state = state.clone(); + let client = WasmSupervisorClient::new("http://localhost:3030".to_string()); + spawn_local(async move { + console::log!("Registering runner..."); + let current_state = (*state).clone(); + + // Validate form data + if current_state.register_form.name.is_empty() { + console::error!("Runner name is required"); + return; + } + if current_state.register_form.secret.is_empty() { + console::error!("Secret is required"); + return; + } + if current_state.register_form.queue.is_empty() { + console::error!("Queue name is required"); + return; + } + + // Add runner to UI immediately with "Registering" status + let new_runner = RunnerStatus { + name: current_state.register_form.name.clone(), + status: "Registering".to_string(), + }; + + let mut updated_runners = current_state.runners.clone(); + updated_runners.push(new_runner); + + let mut temp_state = current_state.clone(); + temp_state.runners = updated_runners; + state.set(temp_state); + + // Make actual registration call + match client.register_runner( + ¤t_state.register_form.secret, + ¤t_state.register_form.name, + ¤t_state.register_form.queue, + ).await { + Ok(_) => { + console::log!("Runner registered successfully!"); + + // Update status to "Running" and clear form + let mut final_state = (*state).clone(); + if let Some(runner) = final_state.runners.iter_mut() + .find(|r| r.name == current_state.register_form.name) { + runner.status = "Running".to_string(); + } + final_state.register_form = RegisterForm { + name: String::new(), + secret: String::new(), + queue: String::new(), + }; + state.set(final_state); + } + Err(e) => { + console::error!("Failed to register runner:", format!("{:?}", e)); + + // Remove the failed runner from the list + let mut error_state = (*state).clone(); + error_state.runners.retain(|r| r.name != current_state.register_form.name); + state.set(error_state); + } + } + }); + }) + }; + } + Err(e) => { + console::error!("Failed to load supervisor info:", format!("{:?}", e)); + } + } + }); + }) + }; + + // Load initial data + use_effect_with((), { + let on_load_runners = on_load_runners.clone(); + move |_| { + on_load_runners.emit(()); + || () + } + }); + + html! { +
+ // Persistent Island Column Sidebar + + + // Main Content Area (no navbar) +
+ + // Runners section +
+ // Registration card (first card) +
+
{"+ Register Runner"}
+
+
+ +
+
+ +
+
+ +
+ +
+
+ + // Existing runner cards + {for state.runners.iter().map(|runner| { + let status_class = match runner.status.as_str() { + "Running" => "status-running", + "Stopped" => "status-stopped", + "Starting" => "status-starting", + "Stopping" => "status-starting", + "Registering" => "status-registering", + _ => "status-stopped", + }; + html! { +
+
{&runner.name}
+ {&runner.status} +
+ } + })} +
+ + // Jobs section +

{"Jobs"}

+
+ + + + + + + + + + + {for state.jobs.iter().map(|job| { + html! { + + + + + + + } + })} + +
{"ID"}{"Type"}{"Runner"}{"Command"}
{&job.id}{&job.type_}{&job.runner}{&job.command}
+
+ + // Floating refresh button + +
+
+ + } +} diff --git a/clients/admin-ui/src/components/add_runner.rs b/clients/admin-ui/src/components/add_runner.rs new file mode 100644 index 0000000..6f3503e --- /dev/null +++ b/clients/admin-ui/src/components/add_runner.rs @@ -0,0 +1,296 @@ +use yew::prelude::*; +use yew_router::prelude::*; +use gloo::console; +use wasm_bindgen_futures::spawn_local; +use web_sys::HtmlInputElement; +use std::collections::HashMap; + +use crate::app::Route; +use crate::types::{AddRunnerForm, RunnerType, ProcessManagerType}; +use crate::services::{SupervisorService, use_supervisor_service}; + +#[function_component(AddRunner)] +pub fn add_runner() -> Html { + let navigator = use_navigator().unwrap(); + let server_url = "http://localhost:8081"; + let (service, service_error) = use_supervisor_service(server_url); + + let form = use_state(|| AddRunnerForm::default()); + let loading = use_state(|| false); + let error = use_state(|| None::); + let success = use_state(|| false); + + let on_actor_id_change = { + let form = form.clone(); + Callback::from(move |e: Event| { + let input: HtmlInputElement = e.target_unchecked_into(); + let mut new_form = (*form).clone(); + new_form.actor_id = input.value(); + form.set(new_form); + }) + }; + + let on_runner_type_change = { + let form = form.clone(); + Callback::from(move |e: Event| { + let select: web_sys::HtmlSelectElement = e.target_unchecked_into(); + let mut new_form = (*form).clone(); + new_form.runner_type = match select.value().as_str() { + "SALRunner" => RunnerType::SALRunner, + "OSISRunner" => RunnerType::OSISRunner, + "VRunner" => RunnerType::VRunner, + _ => RunnerType::SALRunner, + }; + form.set(new_form); + }) + }; + + let on_binary_path_change = { + let form = form.clone(); + Callback::from(move |e: Event| { + let input: HtmlInputElement = e.target_unchecked_into(); + let mut new_form = (*form).clone(); + new_form.binary_path = input.value(); + form.set(new_form); + }) + }; + + let on_script_type_change = { + let form = form.clone(); + Callback::from(move |e: Event| { + let input: HtmlInputElement = e.target_unchecked_into(); + let mut new_form = (*form).clone(); + new_form.script_type = input.value(); + form.set(new_form); + }) + }; + + let on_process_manager_change = { + let form = form.clone(); + Callback::from(move |e: Event| { + let select: web_sys::HtmlSelectElement = e.target_unchecked_into(); + let mut new_form = (*form).clone(); + new_form.process_manager_type = match select.value().as_str() { + "Tmux" => ProcessManagerType::Tmux, + "Simple" => ProcessManagerType::Simple, + _ => ProcessManagerType::Simple, + }; + form.set(new_form); + }) + }; + + let on_submit = { + let form = form.clone(); + let service = service.clone(); + let loading = loading.clone(); + let error = error.clone(); + let success = success.clone(); + let navigator = navigator.clone(); + + Callback::from(move |e: SubmitEvent| { + e.prevent_default(); + + if let Some(service) = &service { + let form = form.clone(); + let service = service.clone(); + let loading = loading.clone(); + let error = error.clone(); + let success = success.clone(); + let navigator = navigator.clone(); + + loading.set(true); + error.set(None); + success.set(false); + + spawn_local(async move { + let config = form.to_runner_config(); + match service.add_runner(config, form.process_manager_type.clone()).await { + Ok(_) => { + console::log!("Runner added successfully"); + success.set(true); + // Navigate back to runners list after a short delay + gloo::timers::callback::Timeout::new(1500, move || { + navigator.push(&Route::Runners); + }).forget(); + } + Err(e) => { + console::error!("Failed to add runner:", e.to_string()); + error.set(Some(e.to_string())); + } + } + loading.set(false); + }); + } + }) + }; + + let on_cancel = { + let navigator = navigator.clone(); + Callback::from(move |_| navigator.push(&Route::Runners)) + }; + + html! { +
+
+
+
+

+ + {"Add New Runner"} +

+ +
+
+
+ + // Error display + if let Some(err) = service_error { +
+
+
+ + {"Service Error: "}{err} +
+
+
+ } + + if let Some(err) = error.as_ref() { +
+
+
+ + {"Error: "}{err} +
+
+
+ } + + if *success { +
+
+
+ + {"Runner added successfully! Redirecting..."} +
+
+
+ } + +
+
+
+
+
{"Runner Configuration"}
+
+
+
+
+
+ + +
{"Unique identifier for this runner"}
+
+
+ + +
+
+ +
+
+ + +
{"Full path to the runner executable"}
+
+
+ + +
{"Type of scripts this runner will execute"}
+
+
+ +
+
+ + +
{"Process management system to use"}
+
+
+ +
+ + +
+
+
+
+
+
+
+ } +} diff --git a/clients/admin-ui/src/components/dashboard.rs b/clients/admin-ui/src/components/dashboard.rs new file mode 100644 index 0000000..7d9dc8d --- /dev/null +++ b/clients/admin-ui/src/components/dashboard.rs @@ -0,0 +1,294 @@ +use yew::prelude::*; +use gloo::console; +use wasm_bindgen_futures::spawn_local; + +use crate::types::{RunnerInfo, ProcessStatus}; +use crate::components::{status_badge::StatusBadge, runner_card::RunnerCard}; +use crate::services::{SupervisorService, use_supervisor_service}; + +#[function_component(Dashboard)] +pub fn dashboard() -> Html { + let server_url = "http://localhost:8081"; // Default supervisor server URL + let (service, service_error) = use_supervisor_service(server_url); + let runners = use_state(|| Vec::::new()); + let loading = use_state(|| false); + let error = use_state(|| None::); + + // Load runners on component mount and when service is available + { + let runners = runners.clone(); + let loading = loading.clone(); + let error = error.clone(); + let service = service.clone(); + + use_effect_with(service.clone(), move |service| { + if let Some(service) = service { + let runners = runners.clone(); + let loading = loading.clone(); + let error = error.clone(); + let service = service.clone(); + + loading.set(true); + spawn_local(async move { + match service.get_all_runners().await { + Ok(runner_list) => { + runners.set(runner_list); + error.set(None); + } + Err(e) => { + console::error!("Failed to load runners:", e.to_string()); + error.set(Some(e.to_string())); + } + } + loading.set(false); + }); + } + }); + } + + let on_refresh = { + let runners = runners.clone(); + let loading = loading.clone(); + let error = error.clone(); + let service = service.clone(); + + Callback::from(move |_: MouseEvent| { + if let Some(service) = &service { + let runners = runners.clone(); + let loading = loading.clone(); + let error = error.clone(); + let service = service.clone(); + + loading.set(true); + spawn_local(async move { + match service.get_all_runners().await { + Ok(runner_list) => { + runners.set(runner_list); + error.set(None); + } + Err(e) => { + console::error!("Failed to refresh runners:", e.to_string()); + error.set(Some(e.to_string())); + } + } + loading.set(false); + }); + } + }) + }; + + let on_start_all = { + let service = service.clone(); + let on_refresh = on_refresh.clone(); + let loading = loading.clone(); + + Callback::from(move |_: MouseEvent| { + if let Some(service) = &service { + let service = service.clone(); + let on_refresh = on_refresh.clone(); + let loading = loading.clone(); + + loading.set(true); + spawn_local(async move { + match service.start_all().await { + Ok(results) => { + console::log!("Start all results:", format!("{:?}", results)); + on_refresh.emit(web_sys::MouseEvent::new("click").unwrap()); + } + Err(e) => { + console::error!("Failed to start all runners:", e.to_string()); + } + } + loading.set(false); + }); + } + }) + }; + + let on_stop_all = { + let service = service.clone(); + let on_refresh = on_refresh.clone(); + let loading = loading.clone(); + + Callback::from(move |_: MouseEvent| { + if let Some(service) = &service { + if gloo::dialogs::confirm("Are you sure you want to stop all runners?") { + let service = service.clone(); + let on_refresh = on_refresh.clone(); + let loading = loading.clone(); + + loading.set(true); + spawn_local(async move { + match service.stop_all(false).await { + Ok(results) => { + console::log!("Stop all results:", format!("{:?}", results)); + on_refresh.emit(web_sys::MouseEvent::new("click").unwrap()); + } + Err(e) => { + console::error!("Failed to stop all runners:", e.to_string()); + } + } + loading.set(false); + }); + } + } + }) + }; + + // Create a proper on_update callback for RunnerCard + let on_runner_update = { + let on_refresh = on_refresh.clone(); + Callback::from(move |_: ()| { + on_refresh.emit(web_sys::MouseEvent::new("click").unwrap()); + }) + }; + + // Calculate statistics + let total_runners = runners.len(); + let running_count = runners.iter().filter(|r| r.status == ProcessStatus::Running).count(); + let stopped_count = runners.iter().filter(|r| r.status == ProcessStatus::Stopped).count(); + let failed_count = runners.iter().filter(|r| r.status == ProcessStatus::Failed).count(); + + html! { +
+
+
+
+

+ + {"Dashboard"} +

+
+ + + +
+
+
+
+ + // Error display + if let Some(err) = service_error { +
+
+
+ + {"Service Error: "}{err} +
+
+
+ } + + if let Some(err) = error.as_ref() { +
+
+
+ + {"Error: "}{err} +
+
+
+ } + + // Statistics cards +
+
+
+
+

{total_runners}

+

{"Total Runners"}

+
+
+
+
+
+
+

{running_count}

+

{"Running"}

+
+
+
+
+
+
+

{stopped_count}

+

{"Stopped"}

+
+
+
+
+
+
+

{failed_count}

+

{"Failed"}

+
+
+
+
+ + // Loading state + if *loading { +
+
+
+ {"Loading..."} +
+

{"Loading runners..."}

+
+
+ } + + // Runners grid + if !*loading && total_runners > 0 { +
+
+

{"Active Runners"}

+
+
+
+ {for runners.iter().map(|runner| { + if let Some(service) = &service { + html! { + + } + } else { + html! {} + } + })} +
+ } + + // Empty state + if !*loading && total_runners == 0 && service.is_some() { +
+
+
+
+ +

{"No Runners Found"}

+

{"Get started by adding your first runner."}

+ + + {"Add Runner"} + +
+
+
+
+ } +
+ } +} diff --git a/clients/admin-ui/src/components/mod.rs b/clients/admin-ui/src/components/mod.rs new file mode 100644 index 0000000..bc6a435 --- /dev/null +++ b/clients/admin-ui/src/components/mod.rs @@ -0,0 +1,7 @@ +pub mod navbar; +pub mod dashboard; +pub mod runners; +pub mod runner_detail; +pub mod add_runner; +pub mod runner_card; +pub mod status_badge; diff --git a/clients/admin-ui/src/components/navbar.rs b/clients/admin-ui/src/components/navbar.rs new file mode 100644 index 0000000..bc5bb41 --- /dev/null +++ b/clients/admin-ui/src/components/navbar.rs @@ -0,0 +1,67 @@ +use yew::prelude::*; +use yew_router::prelude::*; +use crate::app::Route; + +#[function_component(Navbar)] +pub fn navbar() -> Html { + let navigator = use_navigator().unwrap(); + + let on_dashboard_click = { + let navigator = navigator.clone(); + Callback::from(move |_| navigator.push(&Route::Dashboard)) + }; + + let on_runners_click = { + let navigator = navigator.clone(); + Callback::from(move |_| navigator.push(&Route::Runners)) + }; + + let on_add_runner_click = { + let navigator = navigator.clone(); + Callback::from(move |_| navigator.push(&Route::AddRunner)) + }; + + html! { + + } +} diff --git a/clients/admin-ui/src/components/runner_card.rs b/clients/admin-ui/src/components/runner_card.rs new file mode 100644 index 0000000..e287dd2 --- /dev/null +++ b/clients/admin-ui/src/components/runner_card.rs @@ -0,0 +1,191 @@ +use yew::prelude::*; +use yew_router::prelude::*; +use gloo::console; +use wasm_bindgen_futures::spawn_local; + +use crate::app::Route; +use crate::types::{RunnerInfo, ProcessStatus}; +use crate::components::status_badge::StatusBadge; +use crate::services::SupervisorService; + +#[derive(Properties, PartialEq)] +pub struct RunnerCardProps { + pub runner: RunnerInfo, + pub service: SupervisorService, + pub on_update: Callback<()>, +} + +#[function_component(RunnerCard)] +pub fn runner_card(props: &RunnerCardProps) -> Html { + let navigator = use_navigator().unwrap(); + let loading = use_state(|| false); + + let runner_id = props.runner.id.clone(); + let on_view_details = { + let navigator = navigator.clone(); + let runner_id = runner_id.clone(); + Callback::from(move |_| { + navigator.push(&Route::RunnerDetail { id: runner_id.clone() }); + }) + }; + + let on_start = { + let service = props.service.clone(); + let runner_id = runner_id.clone(); + let loading = loading.clone(); + let on_update = props.on_update.clone(); + Callback::from(move |_| { + let service = service.clone(); + let runner_id = runner_id.clone(); + let loading = loading.clone(); + let on_update = on_update.clone(); + + loading.set(true); + spawn_local(async move { + match service.start_runner(&runner_id).await { + Ok(_) => { + console::log!("Runner started successfully"); + on_update.emit(()); + } + Err(e) => { + console::error!("Failed to start runner:", e.to_string()); + } + } + loading.set(false); + }); + }) + }; + + let on_stop = { + let service = props.service.clone(); + let runner_id = runner_id.clone(); + let loading = loading.clone(); + let on_update = props.on_update.clone(); + Callback::from(move |_| { + let service = service.clone(); + let runner_id = runner_id.clone(); + let loading = loading.clone(); + let on_update = on_update.clone(); + + loading.set(true); + spawn_local(async move { + match service.stop_runner(&runner_id, false).await { + Ok(_) => { + console::log!("Runner stopped successfully"); + on_update.emit(()); + } + Err(e) => { + console::error!("Failed to stop runner:", e.to_string()); + } + } + loading.set(false); + }); + }) + }; + + let on_remove = { + let service = props.service.clone(); + let runner_id = runner_id.clone(); + let loading = loading.clone(); + let on_update = props.on_update.clone(); + Callback::from(move |_| { + if gloo::dialogs::confirm("Are you sure you want to remove this runner?") { + let service = service.clone(); + let runner_id = runner_id.clone(); + let loading = loading.clone(); + let on_update = on_update.clone(); + + loading.set(true); + spawn_local(async move { + match service.remove_runner(&runner_id).await { + Ok(_) => { + console::log!("Runner removed successfully"); + on_update.emit(()); + } + Err(e) => { + console::error!("Failed to remove runner:", e.to_string()); + } + } + loading.set(false); + }); + } + }) + }; + + let is_loading = *loading; + let can_start = matches!(props.runner.status, ProcessStatus::Stopped | ProcessStatus::Failed); + let can_stop = matches!(props.runner.status, ProcessStatus::Running); + + html! { +
+
+
+
+ + {&props.runner.id} +
+ +
+
+
+ {"Type: "} + + {format!("{:?}", props.runner.config.runner_type)} + +
+
+ {"Script: "} + {&props.runner.config.script_type} +
+
+ {"Binary: "} + {props.runner.config.binary_path.to_string_lossy()} +
+ + if !props.runner.logs.is_empty() { +
+ {"Recent logs: "} +
+ {for props.runner.logs.iter().take(3).map(|log| html! { +
{&log.message}
+ })} +
+
+ } +
+ +
+
+ } +} diff --git a/clients/admin-ui/src/components/runner_detail.rs b/clients/admin-ui/src/components/runner_detail.rs new file mode 100644 index 0000000..7c92d2e --- /dev/null +++ b/clients/admin-ui/src/components/runner_detail.rs @@ -0,0 +1,437 @@ +use yew::prelude::*; +use yew_router::prelude::*; +use gloo::console; +use wasm_bindgen_futures::spawn_local; +use web_sys::HtmlTextAreaElement; + +use crate::app::Route; +use crate::types::{RunnerInfo, ProcessStatus, JobBuilder, JobType}; +use crate::components::status_badge::StatusBadge; +use crate::services::{SupervisorService, use_supervisor_service}; + +#[derive(Properties, PartialEq)] +pub struct RunnerDetailProps { + pub runner_id: String, +} + +#[function_component(RunnerDetail)] +pub fn runner_detail(props: &RunnerDetailProps) -> Html { + let navigator = use_navigator().unwrap(); + let server_url = "http://localhost:8081"; + let (service, service_error) = use_supervisor_service(server_url); + + let runner = use_state(|| None::); + let loading = use_state(|| false); + let error = use_state(|| None::); + let logs_loading = use_state(|| false); + let job_script = use_state(|| String::new()); + let job_loading = use_state(|| false); + let job_result = use_state(|| None::); + + // Load runner details + { + let runner = runner.clone(); + let loading = loading.clone(); + let error = error.clone(); + let service = service.clone(); + let runner_id = props.runner_id.clone(); + + use_effect_with((service.clone(), runner_id.clone()), move |(service, runner_id)| { + if let Some(service) = service { + let runner = runner.clone(); + let loading = loading.clone(); + let error = error.clone(); + let service = service.clone(); + let runner_id = runner_id.clone(); + + loading.set(true); + spawn_local(async move { + match service.get_all_runners().await { + Ok(runners) => { + if let Some(found_runner) = runners.into_iter().find(|r| r.id == runner_id) { + runner.set(Some(found_runner)); + error.set(None); + } else { + error.set(Some("Runner not found".to_string())); + } + } + Err(e) => { + console::error!("Failed to load runner:", e.to_string()); + error.set(Some(e.to_string())); + } + } + loading.set(false); + }); + } + }); + } + + let on_back = { + let navigator = navigator.clone(); + Callback::from(move |_| navigator.push(&Route::Runners)) + }; + + let on_start = { + let service = service.clone(); + let runner_id = props.runner_id.clone(); + let runner = runner.clone(); + let loading = loading.clone(); + + Callback::from(move |_| { + if let Some(service) = &service { + let service = service.clone(); + let runner_id = runner_id.clone(); + let runner = runner.clone(); + let loading = loading.clone(); + + loading.set(true); + spawn_local(async move { + match service.start_runner(&runner_id).await { + Ok(_) => { + console::log!("Runner started successfully"); + // Refresh runner status + if let Ok(status) = service.get_runner_status(&runner_id).await { + if let Some(mut current_runner) = (*runner).clone() { + current_runner.status = status; + runner.set(Some(current_runner)); + } + } + } + Err(e) => { + console::error!("Failed to start runner:", e.to_string()); + } + } + loading.set(false); + }); + } + }) + }; + + let on_stop = { + let service = service.clone(); + let runner_id = props.runner_id.clone(); + let runner = runner.clone(); + let loading = loading.clone(); + + Callback::from(move |_| { + if let Some(service) = &service { + let service = service.clone(); + let runner_id = runner_id.clone(); + let runner = runner.clone(); + let loading = loading.clone(); + + loading.set(true); + spawn_local(async move { + match service.stop_runner(&runner_id, false).await { + Ok(_) => { + console::log!("Runner stopped successfully"); + // Refresh runner status + if let Ok(status) = service.get_runner_status(&runner_id).await { + if let Some(mut current_runner) = (*runner).clone() { + current_runner.status = status; + runner.set(Some(current_runner)); + } + } + } + Err(e) => { + console::error!("Failed to stop runner:", e.to_string()); + } + } + loading.set(false); + }); + } + }) + }; + + let on_refresh_logs = { + let service = service.clone(); + let runner_id = props.runner_id.clone(); + let runner = runner.clone(); + let logs_loading = logs_loading.clone(); + + Callback::from(move |_| { + if let Some(service) = &service { + let service = service.clone(); + let runner_id = runner_id.clone(); + let runner = runner.clone(); + let logs_loading = logs_loading.clone(); + + logs_loading.set(true); + spawn_local(async move { + match service.get_runner_logs(&runner_id, Some(100), false).await { + Ok(logs) => { + if let Some(mut current_runner) = (*runner).clone() { + current_runner.logs = logs; + runner.set(Some(current_runner)); + } + } + Err(e) => { + console::error!("Failed to refresh logs:", e.to_string()); + } + } + logs_loading.set(false); + }); + } + }) + }; + + let on_script_change = { + let job_script = job_script.clone(); + Callback::from(move |e: Event| { + let textarea: HtmlTextAreaElement = e.target_unchecked_into(); + job_script.set(textarea.value()); + }) + }; + + let on_run_job = { + let service = service.clone(); + let runner_id = props.runner_id.clone(); + let job_script = job_script.clone(); + let job_loading = job_loading.clone(); + let job_result = job_result.clone(); + + Callback::from(move |_| { + if let Some(service) = &service { + let script = (*job_script).clone(); + if !script.trim().is_empty() { + let service = service.clone(); + let runner_id = runner_id.clone(); + let job_loading = job_loading.clone(); + let job_result = job_result.clone(); + + job_loading.set(true); + job_result.set(None); + spawn_local(async move { + let job = JobBuilder::new() + .caller_id("admin-ui") + .context_id("test-job") + .payload(script) + .job_type(JobType::SAL) + .runner_name(&runner_id) + .build(); + + match job { + Ok(job) => { + match service.queue_and_wait(&runner_id, job, 30).await { + Ok(result) => { + job_result.set(result); + } + Err(e) => { + job_result.set(Some(format!("Error: {}", e))); + } + } + } + Err(e) => { + job_result.set(Some(format!("Job creation error: {}", e))); + } + } + job_loading.set(false); + }); + } + } + }) + }; + + html! { +
+
+
+
+

+ + {"Runner Details: "}{&props.runner_id} +

+ +
+
+
+ + // Error display + if let Some(err) = service_error { +
+
+
+ + {"Service Error: "}{err} +
+
+
+ } + + if let Some(err) = error.as_ref() { +
+
+
+ + {"Error: "}{err} +
+
+
+ } + + // Loading state + if *loading { +
+
+
+ {"Loading..."} +
+

{"Loading runner details..."}

+
+
+ } + + // Runner details + if let Some(runner_info) = runner.as_ref() { +
+ // Left column - Runner info and controls +
+
+
+
{"Runner Information"}
+ +
+
+
+
{"ID:"}
+
{&runner_info.id}
+
+
+
{"Type:"}
+
+ + {format!("{:?}", runner_info.config.runner_type)} + +
+
+
+
{"Script Type:"}
+
{&runner_info.config.script_type}
+
+
+
{"Binary Path:"}
+
{runner_info.config.binary_path.to_string_lossy()}
+
+
+
{"Restart Policy:"}
+
{&runner_info.config.restart_policy}
+
+
+ +
+
+ + // Right column - Job execution +
+
+
+
{"Test Job Execution"}
+
+
+
+ + +
+ + + if let Some(result) = job_result.as_ref() { +
+ +
+
{result}
+
+
+ } +
+
+
+
+ + // Logs section +
+
+
+
+
{"Logs"}
+ +
+
+ if runner_info.logs.is_empty() { +
+ {"No logs available"} +
+ } else { +
+ {for runner_info.logs.iter().map(|log| html! { +
+ {&log.timestamp} + {&log.message} +
+ })} +
+ } +
+
+
+
+ } +
+ } +} diff --git a/clients/admin-ui/src/components/runners.rs b/clients/admin-ui/src/components/runners.rs new file mode 100644 index 0000000..a39d33b --- /dev/null +++ b/clients/admin-ui/src/components/runners.rs @@ -0,0 +1,278 @@ +use yew::prelude::*; +use yew_router::prelude::*; +use gloo::console; +use wasm_bindgen_futures::spawn_local; + +use crate::app::Route; +use crate::types::{RunnerInfo, ProcessStatus}; +use crate::components::{status_badge::StatusBadge, runner_card::RunnerCard}; +use crate::services::{SupervisorService, use_supervisor_service}; + +#[function_component(RunnersList)] +pub fn runners_list() -> Html { + let navigator = use_navigator().unwrap(); + let server_url = "http://localhost:8081"; // Default supervisor server URL + let (service, service_error) = use_supervisor_service(server_url); + let runners = use_state(|| Vec::::new()); + let loading = use_state(|| false); + let error = use_state(|| None::); + let view_mode = use_state(|| "grid"); // "grid" or "table" + + // Load runners on component mount and when service is available + { + let runners = runners.clone(); + let loading = loading.clone(); + let error = error.clone(); + let service = service.clone(); + + use_effect_with(service.clone(), move |service| { + if let Some(service) = service { + let runners = runners.clone(); + let loading = loading.clone(); + let error = error.clone(); + let service = service.clone(); + + loading.set(true); + spawn_local(async move { + match service.get_all_runners().await { + Ok(runner_list) => { + runners.set(runner_list); + error.set(None); + } + Err(e) => { + console::error!("Failed to load runners:", e.to_string()); + error.set(Some(e.to_string())); + } + } + loading.set(false); + }); + } + }); + } + + let on_refresh = { + let runners = runners.clone(); + let loading = loading.clone(); + let error = error.clone(); + let service = service.clone(); + + Callback::from(move |_: MouseEvent| { + if let Some(service) = &service { + let runners = runners.clone(); + let loading = loading.clone(); + let error = error.clone(); + let service = service.clone(); + + loading.set(true); + spawn_local(async move { + match service.get_all_runners().await { + Ok(runner_list) => { + runners.set(runner_list); + error.set(None); + } + Err(e) => { + console::error!("Failed to refresh runners:", e.to_string()); + error.set(Some(e.to_string())); + } + } + loading.set(false); + }); + } + }) + }; + + let on_add_runner = { + let navigator = navigator.clone(); + Callback::from(move |_: MouseEvent| navigator.push(&Route::AddRunner)) + }; + + let on_toggle_view = { + let view_mode = view_mode.clone(); + Callback::from(move |_: MouseEvent| { + let current: &str = view_mode.as_ref(); + view_mode.set(if current == "grid" { "table" } else { "grid" }); + }) + }; + + // Create a separate callback for runner updates that matches the expected signature + let on_runner_update = { + let on_refresh = on_refresh.clone(); + Callback::from(move |_: ()| { + on_refresh.emit(web_sys::MouseEvent::new("click").unwrap()); + }) + }; + + html! { +
+
+
+
+

+ + {"Runners"} +

+
+ + + +
+
+
+
+ + // Error display + if let Some(err) = service_error { +
+
+
+ + {"Service Error: "}{err} +
+
+
+ } + + if let Some(err) = error.as_ref() { +
+
+
+ + {"Error: "}{err} +
+
+
+ } + + // Loading state + if *loading { +
+
+
+ {"Loading..."} +
+

{"Loading runners..."}

+
+
+ } + + // Content based on view mode + if !*loading && !runners.is_empty() { + if *view_mode == "grid" { + // Grid view +
+ {for runners.iter().map(|runner| { + if let Some(service) = &service { + html! { + + } + } else { + html! {} + } + })} +
+ } else { + // Table view +
+
+
+
+
+ + + + + + + + + + + + + {for runners.iter().map(|runner| { + let runner_id = runner.id.clone(); + let on_view_details = { + let navigator = navigator.clone(); + let runner_id = runner_id.clone(); + Callback::from(move |_| { + navigator.push(&Route::RunnerDetail { id: runner_id.clone() }); + }) + }; + + html! { + + + + + + + + + } + })} + +
{"ID"}{"Type"}{"Status"}{"Script Type"}{"Binary Path"}{"Actions"}
+ {&runner.id} + + + {format!("{:?}", runner.config.runner_type)} + + + + + {&runner.config.script_type} + + {runner.config.binary_path.to_string_lossy()} + + +
+
+
+
+
+
+ } + } + + // Empty state + if !*loading && runners.is_empty() && service.is_some() { +
+
+
+
+ +

{"No Runners Found"}

+

{"Get started by adding your first runner."}

+ +
+
+
+
+ } +
+ } +} diff --git a/clients/admin-ui/src/components/status_badge.rs b/clients/admin-ui/src/components/status_badge.rs new file mode 100644 index 0000000..895bf73 --- /dev/null +++ b/clients/admin-ui/src/components/status_badge.rs @@ -0,0 +1,30 @@ +use yew::prelude::*; +use crate::types::ProcessStatus; + +#[derive(Properties, PartialEq)] +pub struct StatusBadgeProps { + pub status: ProcessStatus, + #[prop_or_default] + pub size: Option, +} + +#[function_component(StatusBadge)] +pub fn status_badge(props: &StatusBadgeProps) -> Html { + let (badge_class, icon, text) = match props.status { + ProcessStatus::Running => ("badge bg-success", "bi-play-circle-fill", "Running"), + ProcessStatus::Stopped => ("badge bg-danger", "bi-stop-circle-fill", "Stopped"), + ProcessStatus::Starting => ("badge bg-warning", "bi-hourglass-split", "Starting"), + ProcessStatus::Stopping => ("badge bg-warning", "bi-hourglass-split", "Stopping"), + ProcessStatus::Failed => ("badge bg-danger", "bi-exclamation-triangle-fill", "Failed"), + ProcessStatus::Unknown => ("badge bg-secondary", "bi-question-circle-fill", "Unknown"), + }; + + let size_class = props.size.as_deref().unwrap_or(""); + + html! { + + + {text} + + } +} diff --git a/clients/admin-ui/src/jobs.rs b/clients/admin-ui/src/jobs.rs new file mode 100644 index 0000000..357800a --- /dev/null +++ b/clients/admin-ui/src/jobs.rs @@ -0,0 +1,185 @@ +use yew::prelude::*; +use hero_supervisor_openrpc_client::wasm::WasmJob; +use crate::app::JobForm; +use web_sys::{Event, HtmlInputElement, MouseEvent}; + + +#[derive(Properties)] +pub struct JobsProps { + pub jobs: Vec, + pub server_url: String, + pub job_form: JobForm, + pub runners: Vec<(String, String)>, // (name, status) - list of registered runners + pub on_job_form_change: Callback<(String, String)>, + pub on_run_job: Callback<()>, + pub on_stop_job: Callback, + pub on_delete_job: Callback, +} + +impl PartialEq for JobsProps { + fn eq(&self, other: &Self) -> bool { + // Since WasmJob doesn't implement PartialEq, we'll compare by length + // This is a simple comparison that will trigger re-renders when the job list changes + self.jobs.len() == other.jobs.len() && + self.server_url == other.server_url && + self.job_form.payload == other.job_form.payload && + self.job_form.runner_name == other.job_form.runner_name && + self.job_form.executor == other.job_form.executor && + self.job_form.secret == other.job_form.secret && + self.runners.len() == other.runners.len() + // Note: Callbacks don't implement PartialEq, so we skip them + } +} + +#[function_component(Jobs)] +pub fn jobs(props: &JobsProps) -> Html { + let on_payload_change = { + let on_change = props.on_job_form_change.clone(); + Callback::from(move |e: Event| { + let input: web_sys::HtmlInputElement = e.target_unchecked_into(); + on_change.emit(("payload".to_string(), input.value())); + }) + }; + + let on_runner_name_change = { + let on_change = props.on_job_form_change.clone(); + Callback::from(move |e: Event| { + let input: HtmlInputElement = e.target_unchecked_into(); + on_change.emit(("runner_name".to_string(), input.value())); + }) + }; + + let on_executor_change = { + let on_change = props.on_job_form_change.clone(); + Callback::from(move |e: Event| { + let input: web_sys::HtmlInputElement = e.target_unchecked_into(); + on_change.emit(("executor".to_string(), input.value())); + }) + }; + + let on_secret_change = { + let on_change = props.on_job_form_change.clone(); + Callback::from(move |e: Event| { + let input: web_sys::HtmlInputElement = e.target_unchecked_into(); + on_change.emit(("secret".to_string(), input.value())); + }) + }; + + let on_run_click = { + let on_run = props.on_run_job.clone(); + Callback::from(move |_: MouseEvent| { + on_run.emit(()); + }) + }; + + html! { +
+

{"Jobs"}

+
+ + + + + + + + + + + + // Job creation form as first row + + + + + + + + + // Existing jobs + {for props.jobs.iter().map(|job| { + let job_id = job.id(); + let on_stop = props.on_stop_job.clone(); + let on_delete = props.on_delete_job.clone(); + let job_id_stop = job_id.clone(); + let job_id_delete = job_id.clone(); + + html! { + + + + + + + + } + })} + +
{"Job ID"}{"Payload"}{"Runner"}{"Executor"}{"Status"}
+ {"New Job"} + + + + + + + + + +
{job_id}{job.payload()}{job.runner_name()}{job.executor()} + {"Queued"} + + +
+
+
+ } +} diff --git a/clients/admin-ui/src/lib.rs b/clients/admin-ui/src/lib.rs new file mode 100644 index 0000000..a815f95 --- /dev/null +++ b/clients/admin-ui/src/lib.rs @@ -0,0 +1,12 @@ +use wasm_bindgen::prelude::*; + +mod app; +mod sidebar; +mod runners; +mod jobs; + +#[wasm_bindgen(start)] +pub fn main() { + wasm_logger::init(wasm_logger::Config::default()); + yew::Renderer::::new().render(); +} diff --git a/clients/admin-ui/src/runners.rs b/clients/admin-ui/src/runners.rs new file mode 100644 index 0000000..91fa503 --- /dev/null +++ b/clients/admin-ui/src/runners.rs @@ -0,0 +1,219 @@ +use yew::prelude::*; +use wasm_bindgen_futures::spawn_local; +use gloo::console; +use hero_supervisor_openrpc_client::wasm::WasmSupervisorClient; +use wasm_bindgen::JsCast; +use crate::app::PingState; +use std::collections::HashMap; + +#[derive(Clone, PartialEq)] +pub struct RegisterForm { + pub name: String, + pub secret: String, +} + +#[derive(Properties, PartialEq)] +pub struct RunnersProps { + pub server_url: String, + pub runners: Vec<(String, String)>, // (name, status) + pub register_form: RegisterForm, + pub ping_states: HashMap, // runner_name -> ping_state + pub on_register_form_change: Callback<(String, String)>, + pub on_register_runner: Callback<()>, + pub on_load_runners: Callback<()>, + pub on_remove_runner: Callback, + pub on_ping_runner: Callback<(String, String)>, // (runner_name, secret) +} + +#[function_component(Runners)] +pub fn runners(props: &RunnersProps) -> Html { + let on_register_runner = { + let server_url = props.server_url.clone(); + let register_form = props.register_form.clone(); + let on_register_runner = props.on_register_runner.clone(); + Callback::from(move |_: ()| { + let server_url = server_url.clone(); + let register_form = register_form.clone(); + let on_register_runner = on_register_runner.clone(); + let client = WasmSupervisorClient::new(server_url); + spawn_local(async move { + console::log!("Registering runner..."); + + // Validate form data + if register_form.name.is_empty() { + console::error!("Runner name is required"); + return; + } + if register_form.secret.is_empty() { + console::error!("Secret is required"); + return; + } + + // Make actual registration call (use name as queue) + match client.register_runner( + ®ister_form.secret, + ®ister_form.name, + ®ister_form.name, // queue = name + ).await { + Ok(runner_name) => { + console::log!("Runner registered successfully:", runner_name); + on_register_runner.emit(()); + } + Err(e) => { + console::error!("Failed to register runner:", format!("{:?}", e)); + } + } + }); + }) + }; + + html! { +
+ // Registration card (first card) +
+
{"+ Register Runner"}
+
+
+ +
+ +
+ + +
+
+
+ + // Existing runner cards + {for props.runners.iter().map(|(name, status)| { + let status_class = match status.as_str() { + "Running" => "status-running", + "Stopped" => "status-stopped", + "Starting" => "status-starting", + "Stopping" => "status-starting", + "Registering" => "status-registering", + _ => "status-stopped", + }; + + let name_clone = name.clone(); + let name_clone2 = name.clone(); + let on_remove = props.on_remove_runner.clone(); + let on_ping = props.on_ping_runner.clone(); + + html! { +
+
+
+
+ + {"●"} + +
{name}
+
+ + {"redis://localhost:6379/runner:"}{name} + +
+
+ +
+
+
+
+ {"📊 Live job count chart (5s updates)"} +
+
+
+ { + match props.ping_states.get(name).cloned().unwrap_or(PingState::Idle) { + PingState::Idle => html! { +
+ + +
+ }, + PingState::Waiting => html! { +
+ {"⏳"} + {"Waiting for response..."} +
+ }, + PingState::Success(result) => html! { +
+ {"✅"} + {format!("Success: {}", result)} +
+ }, + PingState::Error(error) => html! { +
+ {"❌"} + {error} +
+ }, + } + } +
+
+ } + })} +
+ } +} diff --git a/clients/admin-ui/src/services.rs b/clients/admin-ui/src/services.rs new file mode 100644 index 0000000..83566fc --- /dev/null +++ b/clients/admin-ui/src/services.rs @@ -0,0 +1,145 @@ +use gloo::console; +use std::rc::Rc; +use std::cell::RefCell; +use crate::wasm_client::{WasmSupervisorClient, WasmClientResult as ClientResult, RunnerConfig, ProcessManagerType, ProcessStatus, LogInfo, Job, RunnerType}; +use wasm_bindgen_futures::spawn_local; +use yew::prelude::*; + +use crate::types::{RunnerInfo, AppState}; + +/// Service for managing supervisor client operations +#[derive(Clone)] +pub struct SupervisorService { + client: Rc>, +} + +impl PartialEq for SupervisorService { + fn eq(&self, other: &Self) -> bool { + // Compare by server URL since that's the main identifier + self.client.borrow().server_url() == other.client.borrow().server_url() + } +} + +impl SupervisorService { + pub fn new(server_url: &str) -> ClientResult { + let client = WasmSupervisorClient::new(server_url); + Ok(Self { + client: Rc::new(RefCell::new(client)), + }) + } + + /// Get all runners with their status and basic info + pub async fn get_all_runners(&self) -> ClientResult> { + let runner_ids = self.client.borrow_mut().list_runners().await?; + let mut runners = Vec::new(); + + for id in runner_ids { + let status = self.client.borrow_mut().get_runner_status(&id).await.unwrap_or(ProcessStatus::Unknown); + let logs = self.client.borrow_mut().get_runner_logs(&id, Some(50), false).await.unwrap_or_default(); + + // Create a basic runner config since we don't have a get_runner_config method + let config = RunnerConfig { + actor_id: id.clone(), + runner_type: RunnerType::SALRunner, // Default + binary_path: std::path::PathBuf::from("unknown"), + script_type: "unknown".to_string(), + args: vec![], + env_vars: std::collections::HashMap::new(), + working_dir: None, + restart_policy: "always".to_string(), + health_check_command: None, + dependencies: vec![], + }; + + runners.push(RunnerInfo { + id, + config, + status, + logs, + }); + } + + Ok(runners) + } + + /// Add a new runner + pub async fn add_runner(&self, config: RunnerConfig, process_manager_type: ProcessManagerType) -> ClientResult<()> { + self.client.borrow_mut().add_runner(config, process_manager_type).await + } + + /// Remove a runner + pub async fn remove_runner(&self, actor_id: &str) -> ClientResult<()> { + self.client.borrow_mut().remove_runner(actor_id).await + } + + /// Start a runner + pub async fn start_runner(&self, actor_id: &str) -> ClientResult<()> { + self.client.borrow_mut().start_runner(actor_id).await + } + + /// Stop a runner + pub async fn stop_runner(&self, actor_id: &str, force: bool) -> ClientResult<()> { + self.client.borrow_mut().stop_runner(actor_id, force).await + } + + /// Get runner status + pub async fn get_runner_status(&self, actor_id: &str) -> ClientResult { + self.client.borrow_mut().get_runner_status(actor_id).await + } + + /// Get runner logs + pub async fn get_runner_logs(&self, actor_id: &str, lines: Option, follow: bool) -> ClientResult> { + self.client.borrow_mut().get_runner_logs(actor_id, lines, follow).await + } + + /// Start all runners + pub async fn start_all(&self) -> ClientResult> { + self.client.borrow_mut().start_all().await + } + + /// Stop all runners + pub async fn stop_all(&self, force: bool) -> ClientResult> { + self.client.borrow_mut().stop_all(force).await + } + + /// Queue a job to a runner + pub async fn queue_job(&self, runner_name: &str, job: Job) -> ClientResult<()> { + self.client.borrow_mut().queue_job_to_runner(runner_name, job).await + } + + /// Queue a job and wait for result + pub async fn queue_and_wait(&self, runner_name: &str, job: Job, timeout_secs: u64) -> ClientResult> { + self.client.borrow_mut().queue_and_wait(runner_name, job, timeout_secs).await + } +} + +/// Hook for managing supervisor service state +#[hook] +pub fn use_supervisor_service(server_url: &str) -> (Option, Option) { + let server_url = server_url.to_string(); + let service_state = use_state(|| None); + let error_state = use_state(|| None); + + { + let service_state = service_state.clone(); + let error_state = error_state.clone(); + let server_url = server_url.clone(); + + use_effect_with(server_url.clone(), move |_| { + spawn_local(async move { + match SupervisorService::new(&server_url) { + Ok(service) => { + service_state.set(Some(service)); + error_state.set(None); + } + Err(e) => { + console::error!("Failed to create supervisor service:", e.to_string()); + error_state.set(Some(e.to_string())); + } + } + }); + }); + } + + ((*service_state).clone(), (*error_state).clone()) +} diff --git a/clients/admin-ui/src/sidebar.rs b/clients/admin-ui/src/sidebar.rs new file mode 100644 index 0000000..fe304ae --- /dev/null +++ b/clients/admin-ui/src/sidebar.rs @@ -0,0 +1,292 @@ +use yew::prelude::*; +use wasm_bindgen::JsCast; +use wasm_bindgen_futures::spawn_local; +use gloo::console; +use hero_supervisor_openrpc_client::wasm::WasmSupervisorClient; + + +#[derive(Clone, PartialEq)] +pub struct SupervisorInfo { + pub server_url: String, + pub admin_secrets_count: usize, + pub user_secrets_count: usize, + pub register_secrets_count: usize, + pub runners_count: usize, +} + +#[derive(Properties, PartialEq)] +pub struct SidebarProps { + pub server_url: String, + pub supervisor_info: Option, + pub admin_secret: String, + pub on_admin_secret_change: Callback, + pub on_supervisor_info_loaded: Callback, +} + +#[function_component(Sidebar)] +pub fn sidebar(props: &SidebarProps) -> Html { + let is_unlocked = use_state(|| false); + let unlock_secret = use_state(|| String::new()); + let admin_secrets = use_state(|| Vec::::new()); + let user_secrets = use_state(|| Vec::::new()); + let register_secrets = use_state(|| Vec::::new()); + let is_loading = use_state(|| false); + + + let on_unlock_secret_change = { + let unlock_secret = unlock_secret.clone(); + Callback::from(move |e: web_sys::Event| { + let input: web_sys::HtmlInputElement = e.target().unwrap().dyn_into().unwrap(); + unlock_secret.set(input.value()); + }) + }; + + + + let on_unlock_submit = { + let unlock_secret = unlock_secret.clone(); + let is_unlocked = is_unlocked.clone(); + let is_loading = is_loading.clone(); + let admin_secrets = admin_secrets.clone(); + let user_secrets = user_secrets.clone(); + let register_secrets = register_secrets.clone(); + let server_url = props.server_url.clone(); + + Callback::from(move |_: web_sys::MouseEvent| { + let unlock_secret = unlock_secret.clone(); + let is_unlocked = is_unlocked.clone(); + let is_loading = is_loading.clone(); + let admin_secrets = admin_secrets.clone(); + let user_secrets = user_secrets.clone(); + let register_secrets = register_secrets.clone(); + let server_url = server_url.clone(); + let secret_value = (*unlock_secret).clone(); + + if secret_value.is_empty() { + return; + } + + is_loading.set(true); + + spawn_local(async move { + let client = WasmSupervisorClient::new(server_url); + + // Try to load all secrets + match client.list_admin_secrets(&secret_value).await { + Ok(secrets) => { + admin_secrets.set(secrets); + + // Load user secrets + if let Ok(user_secs) = client.list_user_secrets(&secret_value).await { + user_secrets.set(user_secs); + } + + // Load register secrets + if let Ok(reg_secs) = client.list_register_secrets(&secret_value).await { + register_secrets.set(reg_secs); + } + + is_unlocked.set(true); + unlock_secret.set(String::new()); + console::log!("Secrets unlocked successfully"); + } + Err(e) => { + console::error!("Failed to unlock secrets:", format!("{:?}", e)); + } + } + + is_loading.set(false); + }); + }) + }; + + let on_lock_click = { + let is_unlocked = is_unlocked.clone(); + let admin_secrets = admin_secrets.clone(); + let user_secrets = user_secrets.clone(); + let register_secrets = register_secrets.clone(); + + Callback::from(move |_: web_sys::MouseEvent| { + is_unlocked.set(false); + admin_secrets.set(Vec::new()); + user_secrets.set(Vec::new()); + register_secrets.set(Vec::new()); + console::log!("Secrets locked"); + }) + }; + + html! { + + } +} diff --git a/clients/admin-ui/src/types.rs b/clients/admin-ui/src/types.rs new file mode 100644 index 0000000..7a4ebfe --- /dev/null +++ b/clients/admin-ui/src/types.rs @@ -0,0 +1,61 @@ +use serde::{Deserialize, Serialize}; +use std::collections::HashMap; +use std::path::PathBuf; +use std::time::Duration; + +// Re-export types from the WASM client +pub use crate::wasm_client::{ + WasmClientError as ClientError, WasmClientResult as ClientResult, JobType, ProcessStatus, + RunnerType, RunnerConfig, ProcessManagerType, LogInfo, Job, JobBuilder +}; + +/// UI-specific runner information combining config and status +#[derive(Debug, Clone, PartialEq, Serialize, Deserialize)] +pub struct RunnerInfo { + pub id: String, + pub config: RunnerConfig, + pub status: ProcessStatus, + pub logs: Vec, +} + +/// Form data for adding a new runner +#[derive(Debug, Clone, Default)] +pub struct AddRunnerForm { + pub actor_id: String, + pub runner_type: RunnerType, + pub binary_path: String, + pub script_type: String, + pub args: Vec, + pub env_vars: HashMap, + pub working_dir: Option, + pub restart_policy: String, + pub health_check_command: Option, + pub dependencies: Vec, + pub process_manager_type: ProcessManagerType, +} + +impl AddRunnerForm { + pub fn to_runner_config(&self) -> RunnerConfig { + RunnerConfig { + actor_id: self.actor_id.clone(), + runner_type: self.runner_type.clone(), + binary_path: PathBuf::from(&self.binary_path), + script_type: self.script_type.clone(), + args: self.args.clone(), + env_vars: self.env_vars.clone(), + working_dir: self.working_dir.clone(), + restart_policy: self.restart_policy.clone(), + health_check_command: self.health_check_command.clone(), + dependencies: self.dependencies.clone(), + } + } +} + +/// Application state for managing runners +#[derive(Debug, Clone, Default)] +pub struct AppState { + pub runners: Vec, + pub loading: bool, + pub error: Option, + pub server_url: String, +} diff --git a/clients/admin-ui/src/wasm_client.rs b/clients/admin-ui/src/wasm_client.rs new file mode 100644 index 0000000..ba0ad97 --- /dev/null +++ b/clients/admin-ui/src/wasm_client.rs @@ -0,0 +1,378 @@ +use gloo::net::http::Request; +use serde::{Deserialize, Serialize}; +use serde_json::{json, Value}; +use std::collections::HashMap; +use std::path::PathBuf; +use thiserror::Error; +use uuid::Uuid; + +/// WASM-compatible client for Hero Supervisor OpenRPC server +#[derive(Clone)] +pub struct WasmSupervisorClient { + server_url: String, + request_id: u64, +} + +/// Error types for client operations +#[derive(Error, Debug)] +pub enum WasmClientError { + #[error("HTTP request error: {0}")] + Http(String), + + #[error("JSON serialization error: {0}")] + Serialization(#[from] serde_json::Error), + + #[error("Server error: {message}")] + Server { message: String }, +} + +/// Result type for client operations +pub type WasmClientResult = Result; + +/// Types of runners supported by the supervisor +#[derive(Debug, Clone, PartialEq, Serialize, Deserialize)] +pub enum RunnerType { + SALRunner, + OSISRunner, + VRunner, +} + +impl Default for RunnerType { + fn default() -> Self { + RunnerType::SALRunner + } +} + +/// Process manager types +#[derive(Debug, Clone, PartialEq, Serialize, Deserialize)] +pub enum ProcessManagerType { + Simple, + Tmux, +} + +impl Default for ProcessManagerType { + fn default() -> Self { + ProcessManagerType::Simple + } +} + +/// Configuration for an actor runner +#[derive(Debug, Clone, PartialEq, Serialize, Deserialize)] +pub struct RunnerConfig { + pub actor_id: String, + pub runner_type: RunnerType, + pub binary_path: PathBuf, + pub script_type: String, + pub args: Vec, + pub env_vars: HashMap, + pub working_dir: Option, + pub restart_policy: String, + pub health_check_command: Option, + pub dependencies: Vec, +} + +/// Job type enumeration +#[derive(Debug, Clone, PartialEq, Serialize, Deserialize)] +pub enum JobType { + SAL, + OSIS, + V, +} + +/// Job structure for creating and managing jobs +#[derive(Debug, Clone, PartialEq, Serialize, Deserialize)] +pub struct Job { + pub id: String, + pub caller_id: String, + pub context_id: String, + pub payload: String, + pub job_type: JobType, + pub runner_name: String, + pub timeout: Option, + pub env_vars: HashMap, +} + +/// Process status information +#[derive(Debug, Clone, PartialEq, Serialize, Deserialize)] +pub enum ProcessStatus { + Running, + Stopped, + Starting, + Stopping, + Failed, + Unknown, +} + +/// Log information structure +#[derive(Debug, Clone, PartialEq, Serialize, Deserialize)] +pub struct LogInfo { + pub timestamp: String, + pub level: String, + pub message: String, +} + +impl WasmSupervisorClient { + /// Create a new supervisor client + pub fn new(server_url: impl Into) -> Self { + Self { + server_url: server_url.into(), + request_id: 0, + } + } + + /// Get the server URL + pub fn server_url(&self) -> &str { + &self.server_url + } + + /// Make a JSON-RPC request + async fn make_request(&mut self, method: &str, params: Value) -> WasmClientResult + where + T: for<'de> Deserialize<'de>, + { + self.request_id += 1; + + let request_body = json!({ + "jsonrpc": "2.0", + "method": method, + "params": params, + "id": self.request_id + }); + + let response = Request::post(&self.server_url) + .header("Content-Type", "application/json") + .json(&request_body) + .map_err(|e| WasmClientError::Http(e.to_string()))? + .send() + .await + .map_err(|e| WasmClientError::Http(e.to_string()))?; + + if !response.ok() { + return Err(WasmClientError::Http(format!( + "HTTP error: {} {}", + response.status(), + response.status_text() + ))); + } + + let response_text = response + .text() + .await + .map_err(|e| WasmClientError::Http(e.to_string()))?; + + let response_json: Value = serde_json::from_str(&response_text)?; + + if let Some(error) = response_json.get("error") { + return Err(WasmClientError::Server { + message: error.get("message") + .and_then(|m| m.as_str()) + .unwrap_or("Unknown server error") + .to_string(), + }); + } + + let result = response_json + .get("result") + .ok_or_else(|| WasmClientError::Server { + message: "No result in response".to_string(), + })?; + + serde_json::from_value(result.clone()).map_err(Into::into) + } + + /// Add a new runner to the supervisor + pub async fn add_runner( + &mut self, + config: RunnerConfig, + process_manager_type: ProcessManagerType, + ) -> WasmClientResult<()> { + let params = json!({ + "config": config, + "process_manager_type": process_manager_type + }); + + self.make_request("add_runner", params).await + } + + /// Remove a runner from the supervisor + pub async fn remove_runner(&mut self, actor_id: &str) -> WasmClientResult<()> { + let params = json!({ "actor_id": actor_id }); + self.make_request("remove_runner", params).await + } + + /// List all runner IDs + pub async fn list_runners(&mut self) -> WasmClientResult> { + self.make_request("list_runners", json!({})).await + } + + /// Start a specific runner + pub async fn start_runner(&mut self, actor_id: &str) -> WasmClientResult<()> { + let params = json!({ "actor_id": actor_id }); + self.make_request("start_runner", params).await + } + + /// Stop a specific runner + pub async fn stop_runner(&mut self, actor_id: &str, force: bool) -> WasmClientResult<()> { + let params = json!({ "actor_id": actor_id, "force": force }); + self.make_request("stop_runner", params).await + } + + /// Get status of a specific runner + pub async fn get_runner_status(&mut self, actor_id: &str) -> WasmClientResult { + let params = json!({ "actor_id": actor_id }); + self.make_request("get_runner_status", params).await + } + + /// Get logs for a specific runner + pub async fn get_runner_logs( + &mut self, + actor_id: &str, + lines: Option, + follow: bool, + ) -> WasmClientResult> { + let params = json!({ + "actor_id": actor_id, + "lines": lines, + "follow": follow + }); + self.make_request("get_runner_logs", params).await + } + + /// Queue a job to a specific runner + pub async fn queue_job_to_runner(&mut self, runner_name: &str, job: Job) -> WasmClientResult<()> { + let params = json!({ + "runner_name": runner_name, + "job": job + }); + self.make_request("queue_job_to_runner", params).await + } + + /// Queue a job to a specific runner and wait for the result + pub async fn queue_and_wait( + &mut self, + runner_name: &str, + job: Job, + timeout_secs: u64, + ) -> WasmClientResult> { + let params = json!({ + "runner_name": runner_name, + "job": job, + "timeout_secs": timeout_secs + }); + self.make_request("queue_and_wait", params).await + } + + /// Get job result by job ID + pub async fn get_job_result(&mut self, job_id: &str) -> WasmClientResult> { + let params = json!({ "job_id": job_id }); + self.make_request("get_job_result", params).await + } + + /// Get status of all runners + pub async fn get_all_runner_status(&mut self) -> WasmClientResult> { + self.make_request("get_all_runner_status", json!({})).await + } + + /// Start all runners + pub async fn start_all(&mut self) -> WasmClientResult> { + self.make_request("start_all", json!({})).await + } + + /// Stop all runners + pub async fn stop_all(&mut self, force: bool) -> WasmClientResult> { + let params = json!({ "force": force }); + self.make_request("stop_all", params).await + } +} + +/// Builder for creating jobs with a fluent API +#[derive(Debug, Clone, Default)] +pub struct JobBuilder { + id: Option, + caller_id: Option, + context_id: Option, + payload: Option, + job_type: Option, + runner_name: Option, + timeout: Option, + env_vars: HashMap, +} + +impl JobBuilder { + /// Create a new job builder + pub fn new() -> Self { + Self::default() + } + + /// Set the caller ID for this job + pub fn caller_id(mut self, caller_id: impl Into) -> Self { + self.caller_id = Some(caller_id.into()); + self + } + + /// Set the context ID for this job + pub fn context_id(mut self, context_id: impl Into) -> Self { + self.context_id = Some(context_id.into()); + self + } + + /// Set the payload (script content) for this job + pub fn payload(mut self, payload: impl Into) -> Self { + self.payload = Some(payload.into()); + self + } + + /// Set the job type + pub fn job_type(mut self, job_type: JobType) -> Self { + self.job_type = Some(job_type); + self + } + + /// Set the runner name for this job + pub fn runner_name(mut self, runner_name: impl Into) -> Self { + self.runner_name = Some(runner_name.into()); + self + } + + /// Set the timeout for job execution + pub fn timeout(mut self, timeout_secs: u64) -> Self { + self.timeout = Some(timeout_secs); + self + } + + /// Set a single environment variable + pub fn env_var(mut self, key: impl Into, value: impl Into) -> Self { + self.env_vars.insert(key.into(), value.into()); + self + } + + /// Set multiple environment variables from a HashMap + pub fn env_vars(mut self, env_vars: HashMap) -> Self { + self.env_vars = env_vars; + self + } + + /// Build the job + pub fn build(self) -> WasmClientResult { + Ok(Job { + id: self.id.unwrap_or_else(|| Uuid::new_v4().to_string()), + caller_id: self.caller_id.ok_or_else(|| WasmClientError::Server { + message: "caller_id is required".to_string(), + })?, + context_id: self.context_id.ok_or_else(|| WasmClientError::Server { + message: "context_id is required".to_string(), + })?, + payload: self.payload.ok_or_else(|| WasmClientError::Server { + message: "payload is required".to_string(), + })?, + job_type: self.job_type.ok_or_else(|| WasmClientError::Server { + message: "job_type is required".to_string(), + })?, + runner_name: self.runner_name.ok_or_else(|| WasmClientError::Server { + message: "runner_name is required".to_string(), + })?, + timeout: self.timeout, + env_vars: self.env_vars, + }) + } +} diff --git a/clients/admin-ui/styles.css b/clients/admin-ui/styles.css new file mode 100644 index 0000000..7039aac --- /dev/null +++ b/clients/admin-ui/styles.css @@ -0,0 +1,1129 @@ +/* Hero Supervisor Admin UI - Dark Theme */ + +* { + margin: 0; + padding: 0; + box-sizing: border-box; +} + +body { + background: #0a0a0a; + color: #e8e8e8; + font-family: -apple-system, BlinkMacSystemFont, 'Inter', 'SF Pro Display', system-ui, sans-serif; + font-size: 14px; + line-height: 1.5; + overflow-x: hidden; +} + +.container { + max-width: 1200px; + margin: 0 auto; + padding: 40px 20px; +} + +h1 { + font-size: 24px; + font-weight: 600; + color: #ffffff; + margin-bottom: 24px; + letter-spacing: -0.025em; +} + +h2 { + font-size: 18px; + font-weight: 500; + color: #ffffff; + margin: 48px 0 20px 0; + letter-spacing: -0.025em; +} + +.runners-grid { + display: grid; + grid-template-columns: repeat(auto-fill, minmax(280px, 1fr)); + gap: 16px; + margin-bottom: 48px; +} + +.card { + background: #111111; + border: 1px solid #222222; + border-radius: 8px; + padding: 20px; + transition: all 0.2s ease; +} + +.card:hover { + border-color: #333333; + background: #131313; +} + +.register-card { + border: 1px dashed #333333; + background: #0f0f0f; +} + +.register-card:hover { + border-color: #ff6b5a; + background: #111111; +} + +.card-title { + font-size: 16px; + font-weight: 500; + color: #ffffff; + margin: 0; +} + +.status { + display: inline-block; + padding: 4px 8px; + border-radius: 4px; + font-size: 12px; + font-weight: 500; + text-transform: uppercase; + letter-spacing: 0.5px; +} + +.status-running { background: #1a4d3a; color: #4ade80; } +.status-stopped { background: #4d1a1a; color: #f87171; } +.status-starting { background: #4d3d1a; color: #fbbf24; } +.status-registering { background: #1a3a4d; color: #60a5fa; } + +.form-group { + margin-bottom: 16px; +} + +.form-control { + width: 100%; + padding: 12px; + background: #1a1a1a; + border: 1px solid #333333; + border-radius: 6px; + color: #e8e8e8; + font-size: 14px; + transition: border-color 0.2s ease; +} + +.form-control:focus { + outline: none; + border-color: #ff6b5a; +} + +.form-control::placeholder { + color: #666666; +} + +.btn { + padding: 12px 20px; + border: none; + border-radius: 6px; + font-size: 14px; + font-weight: 500; + cursor: pointer; + transition: all 0.2s ease; +} + +.lock-btn { + font-weight: 500; + cursor: pointer; + transition: all 0.2s ease; +} + +.lock-btn.locked { + background: #f8f9fa; + border: 1px solid #dee2e6; +} + +.lock-btn.locked:hover { + background: #e9ecef; +} + +.lock-btn.unlocked { + background: #d4edda; + border: 1px solid #c3e6cb; + color: #155724; +} + +.lock-btn.unlocked:hover { + background: #c3e6cb; +} + +.btn-primary { + background: #ff6b5a; + color: #000000; +} + +.btn-primary:hover { + background: #ff5a47; + transform: translateY(-1px); +} + +.btn-ghost { + background: transparent; + color: #999999; + border: 1px solid #333333; +} + +.btn-ghost:hover { + color: #e8e8e8; + border-color: #555555; +} + +.table-container { + background: #111111; + border: 1px solid #222222; + border-radius: 8px; + overflow: hidden; +} + +.table { + width: 100%; + border-collapse: collapse; +} + +.table th { + background: #0f0f0f; + padding: 16px; + text-align: left; + font-weight: 500; + color: #ffffff; + border-bottom: 1px solid #222222; + font-size: 13px; + text-transform: uppercase; + letter-spacing: 0.5px; +} + +.table td { + padding: 16px; + border-bottom: 1px solid #1a1a1a; + color: #e8e8e8; +} + +.table tr:hover { + background: #131313; +} + +.code { + background: #0a0a0a; + padding: 8px 12px; + border-radius: 4px; + font-family: 'SF Mono', Monaco, 'Cascadia Code', monospace; + font-size: 12px; + color: #a3a3a3; + max-width: 300px; + overflow: hidden; + text-overflow: ellipsis; + white-space: nowrap; +} + +.refresh-btn { + position: fixed; + bottom: 24px; + right: 24px; + width: 48px; + height: 48px; + border-radius: 50%; + background: #1a1a1a; + border: 1px solid #333333; + color: #999999; + cursor: pointer; + display: flex; + align-items: center; + justify-content: center; + transition: all 0.2s ease; +} + +.refresh-btn:hover { + background: #222222; + color: #e8e8e8; + border-color: #555555; +} + +.status-grid { + display: flex; + flex-direction: column; + gap: 0.75rem; +} + +.status-item { + display: flex; + justify-content: space-between; + align-items: center; + padding: 0.5rem 0; +} + +.status-label { + color: #ccc; + font-size: 0.9rem; + font-weight: 500; +} + +.status-value { + color: #fff; + font-size: 0.8rem; + background: #333; + padding: 0.25rem 0.5rem; + border-radius: 4px; + font-family: monospace; +} + +.status-badge { + background: #4a90e2; + color: #fff; + padding: 0.25rem 0.5rem; + border-radius: 12px; + font-size: 0.8rem; + font-weight: 600; + min-width: 24px; + text-align: center; +} + +.secrets-overview { + display: flex; + flex-direction: column; + gap: 1rem; +} + +.secret-type { + display: flex; + align-items: center; + justify-content: space-between; + padding: 0.75rem; + background: #2a2a2a; + border-radius: 8px; + border: 1px solid #333; +} + +.secret-label { + color: #fff; + font-weight: 600; + font-size: 0.9rem; +} + +.secret-count { + background: #ff6b6b; + color: #fff; + padding: 0.25rem 0.5rem; + border-radius: 12px; + font-size: 0.8rem; + font-weight: 600; + min-width: 24px; + text-align: center; +} + +.secret-actions { + display: flex; + gap: 0.25rem; +} + +.btn-sm { + padding: 0.25rem 0.5rem; + font-size: 0.75rem; + border-radius: 4px; + border: none; + cursor: pointer; + font-weight: 600; + transition: all 0.2s ease; +} + +.btn-success { + background: #28a745; + color: #fff; +} + +.btn-success:hover { + background: #218838; + transform: scale(1.05); +} + +.btn-danger { + background: #dc3545; + color: #fff; +} + +.btn-danger:hover { + background: #c82333; + transform: scale(1.05); +} + +.btn-block { + width: 100%; +} + +.sidebar-footer { + border-top: 1px solid #333; + padding: 1.5rem; + background: #0f0f0f; + margin-top: auto; +} + +.docs-section h5 { + color: #4a90e2; + margin: 0 0 1rem 0; + font-size: 0.9rem; + font-weight: 600; + text-transform: uppercase; + letter-spacing: 0.5px; +} + +.docs-links { + display: flex; + flex-direction: column; + gap: 0.5rem; +} + +.doc-link { + color: #ccc; + text-decoration: none; + font-size: 0.85rem; + padding: 0.5rem 0.75rem; + border-radius: 6px; + background: #2a2a2a; + border: 1px solid #333; + transition: all 0.3s ease; + display: flex; + align-items: center; + gap: 0.5rem; +} + +.doc-link:hover { + background: #333; + color: #4a90e2; + transform: translateX(4px); + border-color: #4a90e2; +} + +/* App container with flexbox layout */ +.app-container { + display: flex; + min-height: 100vh; + background: #0a0a0a; + padding: 1.5rem; + gap: 1.5rem; +} + +/* Sidebar as island card */ +.sidebar { + width: 320px; + min-width: 320px; + background: #1a1a1a; + border: 1px solid #333; + border-radius: 12px; + overflow-y: auto; + box-shadow: 0 8px 32px rgba(0, 0, 0, 0.4); + display: flex; + flex-direction: column; + padding: 2rem 1.5rem; + height: fit-content; + max-height: calc(100vh - 3rem); +} + +/* Server info section styling */ +.server-info { + margin-bottom: 2rem; +} + +.server-header { + margin-bottom: 1rem; +} + +.supervisor-title { + color: #fff; + font-size: 1.2rem; + font-weight: 600; + margin: 0; + letter-spacing: -0.025em; +} + +.server-url { + display: flex; + align-items: center; + gap: 0.5rem; + padding: 0.5rem 0; +} + +.connection-indicator { + width: 8px; + height: 8px; + border-radius: 50%; + background: #4ade80; + box-shadow: 0 0 8px rgba(74, 222, 128, 0.4); +} + +.connection-indicator.connected { + background: #4ade80; + box-shadow: 0 0 8px rgba(74, 222, 128, 0.4); +} + +.connection-indicator.disconnected { + background: #f87171; + box-shadow: 0 0 8px rgba(248, 113, 113, 0.4); +} + +.url-text { + color: #ccc; + font-size: 0.85rem; + font-family: 'SF Mono', Monaco, 'Cascadia Code', monospace; +} + +/* Secrets section styling */ +.secrets-section { + margin: 2rem 0; +} + +.secrets-header { + display: flex; + justify-content: space-between; + align-items: center; + margin-bottom: 0.5rem; +} + +.secrets-title { + color: #fff; + font-size: 1rem; + font-weight: 500; +} + +.lock-btn { + background: none; + border: 1px solid #444; + border-radius: 6px; + padding: 0.25rem 0.5rem; + cursor: pointer; + transition: all 0.2s ease; +} + +.lock-btn.locked { + border-color: #ff6b6b; +} + +.lock-btn.unlocked { + border-color: #4ade80; +} + +.lock-btn:hover { + background: rgba(255, 255, 255, 0.05); +} + +.lock-icon { + font-size: 0.9rem; +} + +.unlock-input-row { + margin-bottom: 1rem; +} + +.unlock-input { + flex: 1; + background: #2a2a2a; + border: 1px solid #444; + color: #fff; + padding: 0.5rem 0.75rem; + border-radius: 4px; + font-size: 0.85rem; + transition: border-color 0.2s ease; +} + +.unlock-input:focus { + outline: none; + border-color: #4a90e2; +} + +.unlock-input::placeholder { + color: #666; +} + +.unlock-btn { + background: #4a90e2; + border: none; + color: #fff; + padding: 0.5rem; + border-radius: 4px; + font-size: 0.85rem; + cursor: pointer; + transition: background-color 0.2s ease; + display: flex; + align-items: center; + justify-content: center; + min-width: 32px; + height: 32px; +} + +.unlock-btn:hover { + background: #357abd; +} + +.lock-btn { + background: #ff6b5a; + border: none; + color: #fff; + padding: 0.5rem; + border-radius: 4px; + font-size: 0.85rem; + cursor: pointer; + transition: background-color 0.2s ease; + display: flex; + align-items: center; + justify-content: center; + min-width: 32px; + height: 32px; +} + +.lock-btn:hover { + background: #e55a4a; +} + +.secrets-content { + margin-top: 1rem; +} + +.secret-group { + margin-bottom: 1.5rem; +} + +.secret-header { + display: flex; + justify-content: space-between; + align-items: center; + margin-bottom: 0.5rem; +} + +.secret-title { + color: #ccc; + font-size: 0.85rem; + font-weight: 500; +} + +.secret-controls { + display: flex; + gap: 0.25rem; +} + +.btn-icon { + background: none; + border: 1px solid #444; + color: #ccc; + width: 24px; + height: 24px; + border-radius: 4px; + display: flex; + align-items: center; + justify-content: center; + font-size: 0.75rem; + cursor: pointer; + transition: all 0.2s ease; +} + +.btn-add:hover { + border-color: #4a90e2; + color: #4a90e2; +} + +.btn-remove:hover { + border-color: #ff6b6b; + color: #ff6b6b; +} + +.secret-list { + display: flex; + flex-direction: column; + gap: 0.5rem; +} + +.secret-item { + display: flex; + align-items: center; + justify-content: space-between; + padding: 0.5rem 0; + border-bottom: 1px solid #333; +} + +.secret-item:last-child { + border-bottom: none; +} + +.secret-item:hover { + background: rgba(255, 255, 255, 0.02); +} + +.secret-value { + color: #fff; + font-size: 0.85rem; + font-family: 'Monaco', 'Menlo', 'Ubuntu Mono', monospace; + flex: 1; + margin-right: 0.5rem; + word-break: break-all; +} + +.secret-add-row { + display: flex; + align-items: center; + gap: 0.5rem; + padding: 0.5rem 0; +} + +.secret-add-input { + flex: 1; + background: #2a2a2a; + border: 1px solid #444; + color: #fff; + padding: 0.5rem 0.75rem; + border-radius: 4px; + font-size: 0.85rem; + transition: border-color 0.2s ease; +} + +.secret-add-input:focus { + outline: none; + border-color: #4a90e2; +} + +.secret-add-input::placeholder { + color: #666; +} + +.secret-inputs { + display: flex; + flex-direction: column; + gap: 0.5rem; +} + +.secret-input-row { + display: flex; + gap: 0.5rem; + align-items: center; +} + +.secret-input { + flex: 1; + background: #2a2a2a; + border: 1px solid #444; + color: #fff; + padding: 0.5rem 0.75rem; + border-radius: 4px; + font-size: 0.85rem; + transition: border-color 0.2s ease; +} + +.secret-input:focus { + outline: none; + border-color: #4a90e2; +} + +.secret-input::placeholder { + color: #666; +} + +.secret-list { + display: flex; + flex-direction: column; + gap: 0.5rem; + margin-top: 0.5rem; +} + +.secret-item { + display: flex; + gap: 0.5rem; + align-items: center; +} + +.secret-value { + flex: 1; + background: #2a2a2a; + border: 1px solid #444; + color: #fff; + padding: 0.5rem 0.75rem; + border-radius: 4px; + font-size: 0.85rem; + font-family: 'SF Mono', Monaco, 'Cascadia Code', monospace; +} + +/* Save section styling */ +.save-section { + margin-top: 2rem; + padding-top: 1.5rem; + border-top: 1px solid #2a2a2a; +} + +.save-changes-btn { + width: 100%; + background: #4a90e2; + border: none; + color: #fff; + padding: 0.75rem 1rem; + border-radius: 6px; + font-size: 0.9rem; + font-weight: 500; + cursor: pointer; + transition: background-color 0.2s ease; +} + +.save-changes-btn:hover { + background: #357abd; +} + +.save-changes-btn:disabled { + background: #333; + color: #666; + cursor: not-allowed; +} + +.main-content { + flex: 1; + padding: 2rem; + overflow-y: auto; + background: #111111; + border: 1px solid #222222; + border-radius: 12px; + box-shadow: 0 4px 16px rgba(0, 0, 0, 0.2); +} + +/* Remove header styles since header is removed */ + +/* Remove sidebar toggle styles since toggle is removed */ + +/* Container styles for new layout */ +.runners-grid, +.jobs-section { + padding: 0; +} + +/* Table input styling for job form row */ +.table-input { + width: 100%; + padding: 8px 12px; + background: #1a1a1a; + border: 1px solid #333; + border-radius: 6px; + color: #e8e8e8; + font-size: 13px; + transition: all 0.2s ease; + margin: 0; +} + +.table-input:focus { + outline: none; + border-color: #007acc; + background: #222; + box-shadow: 0 0 0 2px rgba(0, 122, 204, 0.1); +} + +.table-input::placeholder { + color: #666; + font-size: 12px; +} + +/* Job form row styling */ +.job-form-row { + background: #0d0d0d; + border-bottom: 2px solid #333; +} + +.job-form-row td { + padding: 12px 8px; + vertical-align: middle; +} + +/* Form row for runner registration */ +.form-row { + display: flex; + gap: 8px; + align-items: center; +} + +.form-control-inline { + flex: 1; + margin-right: 8px; +} + +/* Action cell styling for jobs table */ +.action-cell { + display: flex; + gap: 8px; + align-items: center; + min-width: 200px; +} + +.secret-input { + width: 120px; + flex-shrink: 0; +} + +/* Status badge styling */ +.status-badge { + background: #333; + color: #fff; + padding: 4px 8px; + border-radius: 4px; + font-size: 11px; + font-weight: 500; + text-transform: uppercase; +} + +/* Runner card redesign */ +.runner-card { + position: relative; +} + +.card-header { + display: flex; + justify-content: space-between; + align-items: flex-start; + margin-bottom: 8px; +} + +.runner-actions-top { + display: flex; + align-items: center; + gap: 4px; +} + +.status-icon { + font-size: 12px; + margin-right: 4px; +} + +.status-icon.status-running { + color: #4ade80; +} + +.status-icon.status-stopped { + color: #ef4444; +} + +.status-icon.status-starting { + color: #f59e0b; +} + +.status-icon.status-registering { + color: #3b82f6; +} + +/* New UI improvements */ +.runner-title-section { + flex: 1; +} + +.runner-title-with-dot { + display: flex; + align-items: center; + gap: 8px; + margin-bottom: 4px; +} + +.connection-dot { + font-size: 10px; + line-height: 1; + display: inline-block; + width: 10px; + height: 10px; + text-align: center; +} + +.connection-dot.status-running { + color: #22c55e; +} + +.connection-dot.status-stopped { + color: #ef4444; +} + +.connection-dot.status-starting { + color: #f59e0b; +} + +.connection-dot.status-registering { + color: #3b82f6; +} + +.queue-info { + color: #888; + font-size: 11px; + margin-left: 18px; /* Align with title after dot */ + display: block; +} + +.card-header { + display: flex; + justify-content: space-between; + align-items: flex-start; + margin-bottom: 16px; +} + +.ping-section { + margin-top: 16px; + padding-top: 12px; + border-top: 1px solid #222; +} + +.ping-section .input-group { + display: flex; +} + +.ping-section .form-control { + border-top-right-radius: 0; + border-bottom-right-radius: 0; + border-right: none; + font-size: 12px; + padding: 6px 8px; +} + +.ping-section .btn { + border-top-left-radius: 0; + border-bottom-left-radius: 0; + font-size: 12px; + padding: 6px 12px; + white-space: nowrap; +} + +.trash-icon { + width: 14px; + height: 14px; + stroke: currentColor; +} + +.btn-remove { + padding: 4px 8px; + display: flex; + align-items: center; + justify-content: center; +} + +.input-group-sm .form-control { + font-size: 12px; + padding: 6px 8px; +} + +.input-group-sm .btn { + font-size: 12px; + padding: 6px 12px; +} + +.runner-chart { + margin: 12px 0; + padding: 12px; + background: #0a0a0a; + border-radius: 6px; + border: 1px solid #222; +} + +.ping-status { + display: flex; + align-items: center; + gap: 8px; + padding: 8px 12px; + border-radius: 4px; + font-size: 12px; + min-height: 32px; +} + +.ping-waiting { + background: #1a3a4d; + color: #60a5fa; + border: 1px solid #3b82f6; +} + +.ping-success { + background: #1a4d3a; + color: #4ade80; + border: 1px solid #22c55e; +} + +.ping-error { + background: #4d1a1a; + color: #f87171; + border: 1px solid #ef4444; +} + +.ping-spinner { + animation: spin 1s linear infinite; +} + +@keyframes spin { + from { transform: rotate(0deg); } + to { transform: rotate(360deg); } +} + +.ping-icon { + flex-shrink: 0; +} + +.btn-icon { + background: none; + border: none; + font-size: 14px; + cursor: pointer; + padding: 2px 4px; + border-radius: 3px; + transition: background-color 0.2s ease; +} + +.btn-icon:hover { + background-color: rgba(255, 255, 255, 0.1); +} + +.btn-ping:hover { + background-color: rgba(59, 130, 246, 0.2); +} + +.btn-remove:hover { + background-color: rgba(239, 68, 68, 0.2); +} + +.btn-stop:hover { + background-color: rgba(245, 158, 11, 0.2); +} + +.btn-delete:hover { + background-color: rgba(239, 68, 68, 0.2); +} + +/* Runner info styling */ +.runner-info { + margin: 8px 0; + padding: 4px 0; +} + +.queue-info { + font-size: 9px; + color: #666; + word-break: break-all; + font-family: monospace; +} + +/* Runner chart placeholder */ +.runner-chart { + margin-top: 8px; + padding: 8px 0; + border-top: 1px solid #333; +} + +.chart-placeholder { + font-size: 10px; + color: #888; + text-align: center; + padding: 12px 4px; + background: #0a0a0a; + border-radius: 4px; + border: 1px dashed #333; +} + +/* Responsive design for sidebar */ +@media (max-width: 768px) { + .app-container { + flex-direction: column; + padding: 1rem; + gap: 1rem; + } + + .sidebar { + width: 100%; + min-width: auto; + max-height: 50vh; + height: auto; + } + + .main-content { + padding: 1.5rem; + } +} diff --git a/clients/openrpc/.gitignore b/clients/openrpc/.gitignore new file mode 100644 index 0000000..39e6caf --- /dev/null +++ b/clients/openrpc/.gitignore @@ -0,0 +1,2 @@ +pkg +target \ No newline at end of file diff --git a/clients/openrpc/Cargo-wasm.toml b/clients/openrpc/Cargo-wasm.toml new file mode 100644 index 0000000..42a4af9 --- /dev/null +++ b/clients/openrpc/Cargo-wasm.toml @@ -0,0 +1,59 @@ +[package] +name = "hero-supervisor-openrpc-client-wasm" +version = "0.1.0" +edition = "2021" +description = "WASM-compatible OpenRPC client for Hero Supervisor" +license = "MIT OR Apache-2.0" + +[lib] +crate-type = ["cdylib", "rlib"] + +[dependencies] +# WASM bindings +wasm-bindgen = "0.2" +wasm-bindgen-futures = "0.4" +js-sys = "0.3" + +# Web APIs +web-sys = { version = "0.3", features = [ + "console", + "Request", + "RequestInit", + "RequestMode", + "Response", + "Window", + "Headers", + "AbortController", + "AbortSignal", +] } + +# Serialization +serde = { version = "1.0", features = ["derive"] } +serde_json = "1.0" +serde-wasm-bindgen = "0.6" + +# Error handling +thiserror = "1.0" + +# UUID for job IDs +uuid = { version = "1.0", features = ["v4", "serde", "js"] } + +# Time handling +chrono = { version = "0.4", features = ["serde", "wasmbind"] } + +# Collections +indexmap = "2.0" + +# Logging for WASM +log = "0.4" +console_log = "1.0" + +# Async utilities +futures = "0.3" + +[dependencies.getrandom] +version = "0.2" +features = ["js"] + +[dev-dependencies] +wasm-bindgen-test = "0.3" diff --git a/clients/openrpc/Cargo.lock b/clients/openrpc/Cargo.lock new file mode 100644 index 0000000..77da94c --- /dev/null +++ b/clients/openrpc/Cargo.lock @@ -0,0 +1,2710 @@ +# This file is automatically @generated by Cargo. +# It is not intended for manual editing. +version = 4 + +[[package]] +name = "addr2line" +version = "0.24.2" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "dfbe277e56a376000877090da837660b4427aad530e3028d44e0bffe4f89a1c1" +dependencies = [ + "gimli", +] + +[[package]] +name = "adler2" +version = "2.0.1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "320119579fcad9c21884f5c4861d16174d0e06250625266f50fe6898340abefa" + +[[package]] +name = "aho-corasick" +version = "1.1.3" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "8e60d3430d3a69478ad0993f19238d2df97c507009a52b3c10addcd7f6bcb916" +dependencies = [ + "memchr", +] + +[[package]] +name = "android-tzdata" +version = "0.1.1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "e999941b234f3131b00bc13c22d06e8c5ff726d1b6318ac7eb276997bbb4fef0" + +[[package]] +name = "android_system_properties" +version = "0.1.5" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "819e7219dbd41043ac279b19830f2efc897156490d7fd6ea916720117ee66311" +dependencies = [ + "libc", +] + +[[package]] +name = "anstream" +version = "0.6.20" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "3ae563653d1938f79b1ab1b5e668c87c76a9930414574a6583a7b7e11a8e6192" +dependencies = [ + "anstyle", + "anstyle-parse", + "anstyle-query", + "anstyle-wincon", + "colorchoice", + "is_terminal_polyfill", + "utf8parse", +] + +[[package]] +name = "anstyle" +version = "1.0.11" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "862ed96ca487e809f1c8e5a8447f6ee2cf102f846893800b20cebdf541fc6bbd" + +[[package]] +name = "anstyle-parse" +version = "0.2.7" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "4e7644824f0aa2c7b9384579234ef10eb7efb6a0deb83f9630a49594dd9c15c2" +dependencies = [ + "utf8parse", +] + +[[package]] +name = "anstyle-query" +version = "1.1.4" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "9e231f6134f61b71076a3eab506c379d4f36122f2af15a9ff04415ea4c3339e2" +dependencies = [ + "windows-sys 0.60.2", +] + +[[package]] +name = "anstyle-wincon" +version = "3.0.10" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "3e0633414522a32ffaac8ac6cc8f748e090c5717661fddeea04219e2344f5f2a" +dependencies = [ + "anstyle", + "once_cell_polyfill", + "windows-sys 0.60.2", +] + +[[package]] +name = "anyhow" +version = "1.0.99" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "b0674a1ddeecb70197781e945de4b3b8ffb61fa939a5597bcf48503737663100" + +[[package]] +name = "async-stream" +version = "0.3.6" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "0b5a71a6f37880a80d1d7f19efd781e4b5de42c88f0722cc13bcb6cc2cfe8476" +dependencies = [ + "async-stream-impl", + "futures-core", + "pin-project-lite", +] + +[[package]] +name = "async-stream-impl" +version = "0.3.6" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "c7c24de15d275a1ecfd47a380fb4d5ec9bfe0933f309ed5e705b775596a3574d" +dependencies = [ + "proc-macro2", + "quote", + "syn", +] + +[[package]] +name = "async-trait" +version = "0.1.89" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "9035ad2d096bed7955a320ee7e2230574d28fd3c3a0f186cbea1ff3c7eed5dbb" +dependencies = [ + "proc-macro2", + "quote", + "syn", +] + +[[package]] +name = "atomic-waker" +version = "1.1.2" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "1505bd5d3d116872e7271a6d4e16d81d0c8570876c8de68093a09ac269d8aac0" + +[[package]] +name = "autocfg" +version = "1.5.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "c08606f8c3cbf4ce6ec8e28fb0014a2c086708fe954eaa885384a6165172e7e8" + +[[package]] +name = "backtrace" +version = "0.3.75" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "6806a6321ec58106fea15becdad98371e28d92ccbc7c8f1b3b6dd724fe8f1002" +dependencies = [ + "addr2line", + "cfg-if", + "libc", + "miniz_oxide", + "object", + "rustc-demangle", + "windows-targets 0.52.6", +] + +[[package]] +name = "base64" +version = "0.22.1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "72b3254f16251a8381aa12e40e3c4d2f0199f8c6508fbecb9d91f575e0fbb8c6" + +[[package]] +name = "bitflags" +version = "2.9.3" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "34efbcccd345379ca2868b2b2c9d3782e9cc58ba87bc7d79d5b53d9c9ae6f25d" + +[[package]] +name = "block-buffer" +version = "0.10.4" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "3078c7629b62d3f0439517fa394996acacc5cbc91c5a20d8c658e77abd503a71" +dependencies = [ + "generic-array", +] + +[[package]] +name = "bumpalo" +version = "3.19.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "46c5e41b57b8bba42a04676d81cb89e9ee8e859a1a66f80a5a72e1cb76b34d43" + +[[package]] +name = "bytes" +version = "1.10.1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "d71b6127be86fdcfddb610f7182ac57211d4b18a3e9c82eb2d17662f2227ad6a" + +[[package]] +name = "cc" +version = "1.2.34" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "42bc4aea80032b7bf409b0bc7ccad88853858911b7713a8062fdc0623867bedc" +dependencies = [ + "shlex", +] + +[[package]] +name = "cesu8" +version = "1.1.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "6d43a04d8753f35258c91f8ec639f792891f748a1edbd759cf1dcea3382ad83c" + +[[package]] +name = "cfg-if" +version = "1.0.3" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "2fd1289c04a9ea8cb22300a459a72a385d7c73d3259e2ed7dcb2af674838cfa9" + +[[package]] +name = "chrono" +version = "0.4.41" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "c469d952047f47f91b68d1cba3f10d63c11d73e4636f24f08daf0278abf01c4d" +dependencies = [ + "android-tzdata", + "iana-time-zone", + "js-sys", + "num-traits", + "serde", + "wasm-bindgen", + "windows-link", +] + +[[package]] +name = "clap" +version = "4.5.45" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "1fc0e74a703892159f5ae7d3aac52c8e6c392f5ae5f359c70b5881d60aaac318" +dependencies = [ + "clap_builder", + "clap_derive", +] + +[[package]] +name = "clap_builder" +version = "4.5.44" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "b3e7f4214277f3c7aa526a59dd3fbe306a370daee1f8b7b8c987069cd8e888a8" +dependencies = [ + "anstream", + "anstyle", + "clap_lex", + "strsim", +] + +[[package]] +name = "clap_derive" +version = "4.5.45" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "14cb31bb0a7d536caef2639baa7fad459e15c3144efefa6dbd1c84562c4739f6" +dependencies = [ + "heck", + "proc-macro2", + "quote", + "syn", +] + +[[package]] +name = "clap_lex" +version = "0.7.5" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "b94f61472cee1439c0b966b47e3aca9ae07e45d070759512cd390ea2bebc6675" + +[[package]] +name = "colorchoice" +version = "1.0.4" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "b05b61dc5112cbb17e4b6cd61790d9845d13888356391624cbe7e41efeac1e75" + +[[package]] +name = "combine" +version = "4.6.7" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "ba5a308b75df32fe02788e748662718f03fde005016435c444eea572398219fd" +dependencies = [ + "bytes", + "futures-core", + "memchr", + "pin-project-lite", + "tokio", + "tokio-util", +] + +[[package]] +name = "console_log" +version = "1.0.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "be8aed40e4edbf4d3b4431ab260b63fdc40f5780a4766824329ea0f1eefe3c0f" +dependencies = [ + "log", + "web-sys", +] + +[[package]] +name = "core-foundation" +version = "0.10.1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "b2a6cd9ae233e7f62ba4e9353e81a88df7fc8a5987b8d445b4d90c879bd156f6" +dependencies = [ + "core-foundation-sys", + "libc", +] + +[[package]] +name = "core-foundation-sys" +version = "0.8.7" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "773648b94d0e5d620f64f280777445740e61fe701025087ec8b57f45c791888b" + +[[package]] +name = "cpufeatures" +version = "0.2.17" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "59ed5838eebb26a2bb2e58f6d5b5316989ae9d08bab10e0e6d103e656d1b0280" +dependencies = [ + "libc", +] + +[[package]] +name = "crypto-common" +version = "0.1.6" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "1bfb12502f3fc46cca1bb51ac28df9d618d813cdc3d2f25b9fe775a34af26bb3" +dependencies = [ + "generic-array", + "typenum", +] + +[[package]] +name = "deranged" +version = "0.4.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "9c9e6a11ca8224451684bc0d7d5a7adbf8f2fd6887261a1cfc3c0432f9d4068e" +dependencies = [ + "powerfmt", +] + +[[package]] +name = "digest" +version = "0.10.7" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "9ed9a281f7bc9b7576e61468ba615a66a5c8cfdff42420a70aa82701a3b1e292" +dependencies = [ + "block-buffer", + "crypto-common", +] + +[[package]] +name = "displaydoc" +version = "0.2.5" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "97369cbbc041bc366949bc74d34658d6cda5621039731c6310521892a3a20ae0" +dependencies = [ + "proc-macro2", + "quote", + "syn", +] + +[[package]] +name = "env_filter" +version = "0.1.3" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "186e05a59d4c50738528153b83b0b0194d3a29507dfec16eccd4b342903397d0" +dependencies = [ + "log", + "regex", +] + +[[package]] +name = "env_logger" +version = "0.10.2" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "4cd405aab171cb85d6735e5c8d9db038c17d3ca007a4d2c25f337935c3d90580" +dependencies = [ + "humantime", + "is-terminal", + "log", + "regex", + "termcolor", +] + +[[package]] +name = "env_logger" +version = "0.11.8" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "13c863f0904021b108aa8b2f55046443e6b1ebde8fd4a15c399893aae4fa069f" +dependencies = [ + "anstream", + "anstyle", + "env_filter", + "jiff", + "log", +] + +[[package]] +name = "equivalent" +version = "1.0.2" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "877a4ace8713b0bcf2a4e7eec82529c029f1d0619886d18145fea96c3ffe5c0f" + +[[package]] +name = "fnv" +version = "1.0.7" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "3f9eec918d3f24069decb9af1554cad7c880e2da24a9afd88aca000531ab82c1" + +[[package]] +name = "form_urlencoded" +version = "1.2.2" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "cb4cb245038516f5f85277875cdaa4f7d2c9a0fa0468de06ed190163b1581fcf" +dependencies = [ + "percent-encoding", +] + +[[package]] +name = "futures" +version = "0.3.31" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "65bc07b1a8bc7c85c5f2e110c476c7389b4554ba72af57d8445ea63a576b0876" +dependencies = [ + "futures-channel", + "futures-core", + "futures-executor", + "futures-io", + "futures-sink", + "futures-task", + "futures-util", +] + +[[package]] +name = "futures-channel" +version = "0.3.31" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "2dff15bf788c671c1934e366d07e30c1814a8ef514e1af724a602e8a2fbe1b10" +dependencies = [ + "futures-core", + "futures-sink", +] + +[[package]] +name = "futures-core" +version = "0.3.31" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "05f29059c0c2090612e8d742178b0580d2dc940c837851ad723096f87af6663e" + +[[package]] +name = "futures-executor" +version = "0.3.31" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "1e28d1d997f585e54aebc3f97d39e72338912123a67330d723fdbb564d646c9f" +dependencies = [ + "futures-core", + "futures-task", + "futures-util", +] + +[[package]] +name = "futures-io" +version = "0.3.31" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "9e5c1b78ca4aae1ac06c48a526a655760685149f0d465d21f37abfe57ce075c6" + +[[package]] +name = "futures-macro" +version = "0.3.31" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "162ee34ebcb7c64a8abebc059ce0fee27c2262618d7b60ed8faf72fef13c3650" +dependencies = [ + "proc-macro2", + "quote", + "syn", +] + +[[package]] +name = "futures-sink" +version = "0.3.31" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "e575fab7d1e0dcb8d0c7bcf9a63ee213816ab51902e6d244a95819acacf1d4f7" + +[[package]] +name = "futures-task" +version = "0.3.31" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "f90f7dce0722e95104fcb095585910c0977252f286e354b5e3bd38902cd99988" + +[[package]] +name = "futures-util" +version = "0.3.31" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "9fa08315bb612088cc391249efdc3bc77536f16c91f6cf495e6fbe85b20a4a81" +dependencies = [ + "futures-channel", + "futures-core", + "futures-io", + "futures-macro", + "futures-sink", + "futures-task", + "memchr", + "pin-project-lite", + "pin-utils", + "slab", +] + +[[package]] +name = "generic-array" +version = "0.14.7" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "85649ca51fd72272d7821adaf274ad91c288277713d9c18820d8499a7ff69e9a" +dependencies = [ + "typenum", + "version_check", +] + +[[package]] +name = "getrandom" +version = "0.2.16" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "335ff9f135e4384c8150d6f27c6daed433577f86b4750418338c01a1a2528592" +dependencies = [ + "cfg-if", + "js-sys", + "libc", + "wasi 0.11.1+wasi-snapshot-preview1", + "wasm-bindgen", +] + +[[package]] +name = "getrandom" +version = "0.3.3" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "26145e563e54f2cadc477553f1ec5ee650b00862f0a58bcd12cbdc5f0ea2d2f4" +dependencies = [ + "cfg-if", + "libc", + "r-efi", + "wasi 0.14.2+wasi-0.2.4", +] + +[[package]] +name = "gimli" +version = "0.31.1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "07e28edb80900c19c28f1072f2e8aeca7fa06b23cd4169cefe1af5aa3260783f" + +[[package]] +name = "h2" +version = "0.4.12" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "f3c0b69cfcb4e1b9f1bf2f53f95f766e4661169728ec61cd3fe5a0166f2d1386" +dependencies = [ + "atomic-waker", + "bytes", + "fnv", + "futures-core", + "futures-sink", + "http", + "indexmap", + "slab", + "tokio", + "tokio-util", + "tracing", +] + +[[package]] +name = "hashbrown" +version = "0.15.5" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "9229cfe53dfd69f0609a49f65461bd93001ea1ef889cd5529dd176593f5338a1" + +[[package]] +name = "heck" +version = "0.5.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "2304e00983f87ffb38b55b444b5e3b60a884b5d30c0fca7d82fe33449bbe55ea" + +[[package]] +name = "hermit-abi" +version = "0.5.2" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "fc0fef456e4baa96da950455cd02c081ca953b141298e41db3fc7e36b1da849c" + +[[package]] +name = "hero-supervisor" +version = "0.1.0" +dependencies = [ + "anyhow", + "async-trait", + "chrono", + "clap", + "env_logger 0.10.2", + "jsonrpsee", + "log", + "redis", + "sal-service-manager", + "serde", + "serde_json", + "thiserror", + "tokio", + "toml", + "tower", + "tower-http", + "uuid", +] + +[[package]] +name = "hero-supervisor-openrpc-client" +version = "0.1.0" +dependencies = [ + "chrono", + "console_log", + "env_logger 0.11.8", + "getrandom 0.2.16", + "hero-supervisor", + "js-sys", + "jsonrpsee", + "log", + "serde", + "serde_json", + "thiserror", + "tokio", + "tokio-test", + "uuid", + "wasm-bindgen", + "wasm-bindgen-futures", + "wasm-bindgen-test", + "web-sys", +] + +[[package]] +name = "http" +version = "1.3.1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "f4a85d31aea989eead29a3aaf9e1115a180df8282431156e533de47660892565" +dependencies = [ + "bytes", + "fnv", + "itoa", +] + +[[package]] +name = "http-body" +version = "1.0.1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "1efedce1fb8e6913f23e0c92de8e62cd5b772a67e7b3946df930a62566c93184" +dependencies = [ + "bytes", + "http", +] + +[[package]] +name = "http-body-util" +version = "0.1.3" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "b021d93e26becf5dc7e1b75b1bed1fd93124b374ceb73f43d4d4eafec896a64a" +dependencies = [ + "bytes", + "futures-core", + "http", + "http-body", + "pin-project-lite", +] + +[[package]] +name = "httparse" +version = "1.10.1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "6dbf3de79e51f3d586ab4cb9d5c3e2c14aa28ed23d180cf89b4df0454a69cc87" + +[[package]] +name = "httpdate" +version = "1.0.3" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "df3b46402a9d5adb4c86a0cf463f42e19994e3ee891101b1841f30a545cb49a9" + +[[package]] +name = "humantime" +version = "2.2.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "9b112acc8b3adf4b107a8ec20977da0273a8c386765a3ec0229bd500a1443f9f" + +[[package]] +name = "hyper" +version = "1.7.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "eb3aa54a13a0dfe7fbe3a59e0c76093041720fdc77b110cc0fc260fafb4dc51e" +dependencies = [ + "atomic-waker", + "bytes", + "futures-channel", + "futures-core", + "h2", + "http", + "http-body", + "httparse", + "httpdate", + "itoa", + "pin-project-lite", + "pin-utils", + "smallvec", + "tokio", + "want", +] + +[[package]] +name = "hyper-rustls" +version = "0.27.7" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "e3c93eb611681b207e1fe55d5a71ecf91572ec8a6705cdb6857f7d8d5242cf58" +dependencies = [ + "http", + "hyper", + "hyper-util", + "log", + "rustls", + "rustls-pki-types", + "tokio", + "tokio-rustls", + "tower-service", +] + +[[package]] +name = "hyper-util" +version = "0.1.16" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "8d9b05277c7e8da2c93a568989bb6207bef0112e8d17df7a6eda4a3cf143bc5e" +dependencies = [ + "bytes", + "futures-channel", + "futures-core", + "futures-util", + "http", + "http-body", + "hyper", + "libc", + "pin-project-lite", + "socket2 0.6.0", + "tokio", + "tower-service", + "tracing", +] + +[[package]] +name = "iana-time-zone" +version = "0.1.63" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "b0c919e5debc312ad217002b8048a17b7d83f80703865bbfcfebb0458b0b27d8" +dependencies = [ + "android_system_properties", + "core-foundation-sys", + "iana-time-zone-haiku", + "js-sys", + "log", + "wasm-bindgen", + "windows-core", +] + +[[package]] +name = "iana-time-zone-haiku" +version = "0.1.2" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "f31827a206f56af32e590ba56d5d2d085f558508192593743f16b2306495269f" +dependencies = [ + "cc", +] + +[[package]] +name = "icu_collections" +version = "2.0.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "200072f5d0e3614556f94a9930d5dc3e0662a652823904c3a75dc3b0af7fee47" +dependencies = [ + "displaydoc", + "potential_utf", + "yoke", + "zerofrom", + "zerovec", +] + +[[package]] +name = "icu_locale_core" +version = "2.0.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "0cde2700ccaed3872079a65fb1a78f6c0a36c91570f28755dda67bc8f7d9f00a" +dependencies = [ + "displaydoc", + "litemap", + "tinystr", + "writeable", + "zerovec", +] + +[[package]] +name = "icu_normalizer" +version = "2.0.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "436880e8e18df4d7bbc06d58432329d6458cc84531f7ac5f024e93deadb37979" +dependencies = [ + "displaydoc", + "icu_collections", + "icu_normalizer_data", + "icu_properties", + "icu_provider", + "smallvec", + "zerovec", +] + +[[package]] +name = "icu_normalizer_data" +version = "2.0.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "00210d6893afc98edb752b664b8890f0ef174c8adbb8d0be9710fa66fbbf72d3" + +[[package]] +name = "icu_properties" +version = "2.0.1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "016c619c1eeb94efb86809b015c58f479963de65bdb6253345c1a1276f22e32b" +dependencies = [ + "displaydoc", + "icu_collections", + "icu_locale_core", + "icu_properties_data", + "icu_provider", + "potential_utf", + "zerotrie", + "zerovec", +] + +[[package]] +name = "icu_properties_data" +version = "2.0.1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "298459143998310acd25ffe6810ed544932242d3f07083eee1084d83a71bd632" + +[[package]] +name = "icu_provider" +version = "2.0.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "03c80da27b5f4187909049ee2d72f276f0d9f99a42c306bd0131ecfe04d8e5af" +dependencies = [ + "displaydoc", + "icu_locale_core", + "stable_deref_trait", + "tinystr", + "writeable", + "yoke", + "zerofrom", + "zerotrie", + "zerovec", +] + +[[package]] +name = "idna" +version = "1.1.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "3b0875f23caa03898994f6ddc501886a45c7d3d62d04d2d90788d47be1b1e4de" +dependencies = [ + "idna_adapter", + "smallvec", + "utf8_iter", +] + +[[package]] +name = "idna_adapter" +version = "1.2.1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "3acae9609540aa318d1bc588455225fb2085b9ed0c4f6bd0d9d5bcd86f1a0344" +dependencies = [ + "icu_normalizer", + "icu_properties", +] + +[[package]] +name = "indexmap" +version = "2.11.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "f2481980430f9f78649238835720ddccc57e52df14ffce1c6f37391d61b563e9" +dependencies = [ + "equivalent", + "hashbrown", +] + +[[package]] +name = "io-uring" +version = "0.7.10" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "046fa2d4d00aea763528b4950358d0ead425372445dc8ff86312b3c69ff7727b" +dependencies = [ + "bitflags", + "cfg-if", + "libc", +] + +[[package]] +name = "is-terminal" +version = "0.4.16" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "e04d7f318608d35d4b61ddd75cbdaee86b023ebe2bd5a66ee0915f0bf93095a9" +dependencies = [ + "hermit-abi", + "libc", + "windows-sys 0.59.0", +] + +[[package]] +name = "is_terminal_polyfill" +version = "1.70.1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "7943c866cc5cd64cbc25b2e01621d07fa8eb2a1a23160ee81ce38704e97b8ecf" + +[[package]] +name = "itoa" +version = "1.0.15" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "4a5f13b858c8d314ee3e8f639011f7ccefe71f97f96e50151fb991f267928e2c" + +[[package]] +name = "jiff" +version = "0.2.15" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "be1f93b8b1eb69c77f24bbb0afdf66f54b632ee39af40ca21c4365a1d7347e49" +dependencies = [ + "jiff-static", + "log", + "portable-atomic", + "portable-atomic-util", + "serde", +] + +[[package]] +name = "jiff-static" +version = "0.2.15" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "03343451ff899767262ec32146f6d559dd759fdadf42ff0e227c7c48f72594b4" +dependencies = [ + "proc-macro2", + "quote", + "syn", +] + +[[package]] +name = "jni" +version = "0.21.1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "1a87aa2bb7d2af34197c04845522473242e1aa17c12f4935d5856491a7fb8c97" +dependencies = [ + "cesu8", + "cfg-if", + "combine", + "jni-sys", + "log", + "thiserror", + "walkdir", + "windows-sys 0.45.0", +] + +[[package]] +name = "jni-sys" +version = "0.3.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "8eaf4bc02d17cbdd7ff4c7438cafcdf7fb9a4613313ad11b4f8fefe7d3fa0130" + +[[package]] +name = "js-sys" +version = "0.3.77" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "1cfaf33c695fc6e08064efbc1f72ec937429614f25eef83af942d0e227c3a28f" +dependencies = [ + "once_cell", + "wasm-bindgen", +] + +[[package]] +name = "jsonrpsee" +version = "0.24.9" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "37b26c20e2178756451cfeb0661fb74c47dd5988cb7e3939de7e9241fd604d42" +dependencies = [ + "jsonrpsee-core", + "jsonrpsee-http-client", + "jsonrpsee-proc-macros", + "jsonrpsee-server", + "jsonrpsee-types", + "tokio", + "tracing", +] + +[[package]] +name = "jsonrpsee-core" +version = "0.24.9" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "456196007ca3a14db478346f58c7238028d55ee15c1df15115596e411ff27925" +dependencies = [ + "async-trait", + "bytes", + "futures-util", + "http", + "http-body", + "http-body-util", + "jsonrpsee-types", + "parking_lot", + "rand", + "rustc-hash", + "serde", + "serde_json", + "thiserror", + "tokio", + "tracing", +] + +[[package]] +name = "jsonrpsee-http-client" +version = "0.24.9" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "c872b6c9961a4ccc543e321bb5b89f6b2d2c7fe8b61906918273a3333c95400c" +dependencies = [ + "async-trait", + "base64", + "http-body", + "hyper", + "hyper-rustls", + "hyper-util", + "jsonrpsee-core", + "jsonrpsee-types", + "rustls", + "rustls-platform-verifier", + "serde", + "serde_json", + "thiserror", + "tokio", + "tower", + "tracing", + "url", +] + +[[package]] +name = "jsonrpsee-proc-macros" +version = "0.24.9" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "5e65763c942dfc9358146571911b0cd1c361c2d63e2d2305622d40d36376ca80" +dependencies = [ + "heck", + "proc-macro-crate", + "proc-macro2", + "quote", + "syn", +] + +[[package]] +name = "jsonrpsee-server" +version = "0.24.9" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "55e363146da18e50ad2b51a0a7925fc423137a0b1371af8235b1c231a0647328" +dependencies = [ + "futures-util", + "http", + "http-body", + "http-body-util", + "hyper", + "hyper-util", + "jsonrpsee-core", + "jsonrpsee-types", + "pin-project", + "route-recognizer", + "serde", + "serde_json", + "soketto", + "thiserror", + "tokio", + "tokio-stream", + "tokio-util", + "tower", + "tracing", +] + +[[package]] +name = "jsonrpsee-types" +version = "0.24.9" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "08a8e70baf945b6b5752fc8eb38c918a48f1234daf11355e07106d963f860089" +dependencies = [ + "http", + "serde", + "serde_json", + "thiserror", +] + +[[package]] +name = "libc" +version = "0.2.175" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "6a82ae493e598baaea5209805c49bbf2ea7de956d50d7da0da1164f9c6d28543" + +[[package]] +name = "litemap" +version = "0.8.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "241eaef5fd12c88705a01fc1066c48c4b36e0dd4377dcdc7ec3942cea7a69956" + +[[package]] +name = "lock_api" +version = "0.4.13" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "96936507f153605bddfcda068dd804796c84324ed2510809e5b2a624c81da765" +dependencies = [ + "autocfg", + "scopeguard", +] + +[[package]] +name = "log" +version = "0.4.27" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "13dc2df351e3202783a1fe0d44375f7295ffb4049267b0f3018346dc122a1d94" + +[[package]] +name = "memchr" +version = "2.7.5" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "32a282da65faaf38286cf3be983213fcf1d2e2a58700e808f83f4ea9a4804bc0" + +[[package]] +name = "minicov" +version = "0.3.7" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "f27fe9f1cc3c22e1687f9446c2083c4c5fc7f0bcf1c7a86bdbded14985895b4b" +dependencies = [ + "cc", + "walkdir", +] + +[[package]] +name = "miniz_oxide" +version = "0.8.9" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "1fa76a2c86f704bdb222d66965fb3d63269ce38518b83cb0575fca855ebb6316" +dependencies = [ + "adler2", +] + +[[package]] +name = "mio" +version = "1.0.4" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "78bed444cc8a2160f01cbcf811ef18cac863ad68ae8ca62092e8db51d51c761c" +dependencies = [ + "libc", + "wasi 0.11.1+wasi-snapshot-preview1", + "windows-sys 0.59.0", +] + +[[package]] +name = "num-conv" +version = "0.1.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "51d515d32fb182ee37cda2ccdcb92950d6a3c2893aa280e540671c2cd0f3b1d9" + +[[package]] +name = "num-traits" +version = "0.2.19" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "071dfc062690e90b734c0b2273ce72ad0ffa95f0c74596bc250dcfd960262841" +dependencies = [ + "autocfg", +] + +[[package]] +name = "object" +version = "0.36.7" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "62948e14d923ea95ea2c7c86c71013138b66525b86bdc08d2dcc262bdb497b87" +dependencies = [ + "memchr", +] + +[[package]] +name = "once_cell" +version = "1.21.3" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "42f5e15c9953c5e4ccceeb2e7382a716482c34515315f7b03532b8b4e8393d2d" + +[[package]] +name = "once_cell_polyfill" +version = "1.70.1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "a4895175b425cb1f87721b59f0f286c2092bd4af812243672510e1ac53e2e0ad" + +[[package]] +name = "openssl-probe" +version = "0.1.6" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "d05e27ee213611ffe7d6348b942e8f942b37114c00cc03cec254295a4a17852e" + +[[package]] +name = "parking_lot" +version = "0.12.4" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "70d58bf43669b5795d1576d0641cfb6fbb2057bf629506267a92807158584a13" +dependencies = [ + "lock_api", + "parking_lot_core", +] + +[[package]] +name = "parking_lot_core" +version = "0.9.11" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "bc838d2a56b5b1a6c25f55575dfc605fabb63bb2365f6c2353ef9159aa69e4a5" +dependencies = [ + "cfg-if", + "libc", + "redox_syscall", + "smallvec", + "windows-targets 0.52.6", +] + +[[package]] +name = "percent-encoding" +version = "2.3.2" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "9b4f627cb1b25917193a259e49bdad08f671f8d9708acfd5fe0a8c1455d87220" + +[[package]] +name = "pin-project" +version = "1.1.10" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "677f1add503faace112b9f1373e43e9e054bfdd22ff1a63c1bc485eaec6a6a8a" +dependencies = [ + "pin-project-internal", +] + +[[package]] +name = "pin-project-internal" +version = "1.1.10" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "6e918e4ff8c4549eb882f14b3a4bc8c8bc93de829416eacf579f1207a8fbf861" +dependencies = [ + "proc-macro2", + "quote", + "syn", +] + +[[package]] +name = "pin-project-lite" +version = "0.2.16" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "3b3cff922bd51709b605d9ead9aa71031d81447142d828eb4a6eba76fe619f9b" + +[[package]] +name = "pin-utils" +version = "0.1.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "8b870d8c151b6f2fb93e84a13146138f05d02ed11c7e7c54f8826aaaf7c9f184" + +[[package]] +name = "plist" +version = "1.7.4" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "3af6b589e163c5a788fab00ce0c0366f6efbb9959c2f9874b224936af7fce7e1" +dependencies = [ + "base64", + "indexmap", + "quick-xml", + "serde", + "time", +] + +[[package]] +name = "portable-atomic" +version = "1.11.1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "f84267b20a16ea918e43c6a88433c2d54fa145c92a811b5b047ccbe153674483" + +[[package]] +name = "portable-atomic-util" +version = "0.2.4" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "d8a2f0d8d040d7848a709caf78912debcc3f33ee4b3cac47d73d1e1069e83507" +dependencies = [ + "portable-atomic", +] + +[[package]] +name = "potential_utf" +version = "0.1.2" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "e5a7c30837279ca13e7c867e9e40053bc68740f988cb07f7ca6df43cc734b585" +dependencies = [ + "zerovec", +] + +[[package]] +name = "powerfmt" +version = "0.2.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "439ee305def115ba05938db6eb1644ff94165c5ab5e9420d1c1bcedbba909391" + +[[package]] +name = "ppv-lite86" +version = "0.2.21" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "85eae3c4ed2f50dcfe72643da4befc30deadb458a9b590d720cde2f2b1e97da9" +dependencies = [ + "zerocopy", +] + +[[package]] +name = "proc-macro-crate" +version = "3.3.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "edce586971a4dfaa28950c6f18ed55e0406c1ab88bbce2c6f6293a7aaba73d35" +dependencies = [ + "toml_edit", +] + +[[package]] +name = "proc-macro2" +version = "1.0.101" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "89ae43fd86e4158d6db51ad8e2b80f313af9cc74f5c0e03ccb87de09998732de" +dependencies = [ + "unicode-ident", +] + +[[package]] +name = "quick-xml" +version = "0.38.3" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "42a232e7487fc2ef313d96dde7948e7a3c05101870d8985e4fd8d26aedd27b89" +dependencies = [ + "memchr", +] + +[[package]] +name = "quote" +version = "1.0.40" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "1885c039570dc00dcb4ff087a89e185fd56bae234ddc7f056a945bf36467248d" +dependencies = [ + "proc-macro2", +] + +[[package]] +name = "r-efi" +version = "5.3.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "69cdb34c158ceb288df11e18b4bd39de994f6657d83847bdffdbd7f346754b0f" + +[[package]] +name = "rand" +version = "0.8.5" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "34af8d1a0e25924bc5b7c43c079c942339d8f0a8b57c39049bef581b46327404" +dependencies = [ + "libc", + "rand_chacha", + "rand_core", +] + +[[package]] +name = "rand_chacha" +version = "0.3.1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "e6c10a63a0fa32252be49d21e7709d4d4baf8d231c2dbce1eaa8141b9b127d88" +dependencies = [ + "ppv-lite86", + "rand_core", +] + +[[package]] +name = "rand_core" +version = "0.6.4" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "ec0be4795e2f6a28069bec0b5ff3e2ac9bafc99e6a9a7dc3547996c5c816922c" +dependencies = [ + "getrandom 0.2.16", +] + +[[package]] +name = "redis" +version = "0.25.4" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "e0d7a6955c7511f60f3ba9e86c6d02b3c3f144f8c24b288d1f4e18074ab8bbec" +dependencies = [ + "async-trait", + "bytes", + "combine", + "futures-util", + "itoa", + "percent-encoding", + "pin-project-lite", + "ryu", + "sha1_smol", + "socket2 0.5.10", + "tokio", + "tokio-util", + "url", +] + +[[package]] +name = "redox_syscall" +version = "0.5.17" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "5407465600fb0548f1442edf71dd20683c6ed326200ace4b1ef0763521bb3b77" +dependencies = [ + "bitflags", +] + +[[package]] +name = "regex" +version = "1.11.2" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "23d7fd106d8c02486a8d64e778353d1cffe08ce79ac2e82f540c86d0facf6912" +dependencies = [ + "aho-corasick", + "memchr", + "regex-automata", + "regex-syntax", +] + +[[package]] +name = "regex-automata" +version = "0.4.10" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "6b9458fa0bfeeac22b5ca447c63aaf45f28439a709ccd244698632f9aa6394d6" +dependencies = [ + "aho-corasick", + "memchr", + "regex-syntax", +] + +[[package]] +name = "regex-syntax" +version = "0.8.6" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "caf4aa5b0f434c91fe5c7f1ecb6a5ece2130b02ad2a590589dda5146df959001" + +[[package]] +name = "ring" +version = "0.17.14" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "a4689e6c2294d81e88dc6261c768b63bc4fcdb852be6d1352498b114f61383b7" +dependencies = [ + "cc", + "cfg-if", + "getrandom 0.2.16", + "libc", + "untrusted", + "windows-sys 0.52.0", +] + +[[package]] +name = "route-recognizer" +version = "0.3.1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "afab94fb28594581f62d981211a9a4d53cc8130bbcbbb89a0440d9b8e81a7746" + +[[package]] +name = "rustc-demangle" +version = "0.1.26" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "56f7d92ca342cea22a06f2121d944b4fd82af56988c270852495420f961d4ace" + +[[package]] +name = "rustc-hash" +version = "2.1.1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "357703d41365b4b27c590e3ed91eabb1b663f07c4c084095e60cbed4362dff0d" + +[[package]] +name = "rustls" +version = "0.23.31" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "c0ebcbd2f03de0fc1122ad9bb24b127a5a6cd51d72604a3f3c50ac459762b6cc" +dependencies = [ + "log", + "once_cell", + "ring", + "rustls-pki-types", + "rustls-webpki", + "subtle", + "zeroize", +] + +[[package]] +name = "rustls-native-certs" +version = "0.8.1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "7fcff2dd52b58a8d98a70243663a0d234c4e2b79235637849d15913394a247d3" +dependencies = [ + "openssl-probe", + "rustls-pki-types", + "schannel", + "security-framework", +] + +[[package]] +name = "rustls-pki-types" +version = "1.12.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "229a4a4c221013e7e1f1a043678c5cc39fe5171437c88fb47151a21e6f5b5c79" +dependencies = [ + "zeroize", +] + +[[package]] +name = "rustls-platform-verifier" +version = "0.5.3" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "19787cda76408ec5404443dc8b31795c87cd8fec49762dc75fa727740d34acc1" +dependencies = [ + "core-foundation", + "core-foundation-sys", + "jni", + "log", + "once_cell", + "rustls", + "rustls-native-certs", + "rustls-platform-verifier-android", + "rustls-webpki", + "security-framework", + "security-framework-sys", + "webpki-root-certs 0.26.11", + "windows-sys 0.59.0", +] + +[[package]] +name = "rustls-platform-verifier-android" +version = "0.1.1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "f87165f0995f63a9fbeea62b64d10b4d9d8e78ec6d7d51fb2125fda7bb36788f" + +[[package]] +name = "rustls-webpki" +version = "0.103.4" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "0a17884ae0c1b773f1ccd2bd4a8c72f16da897310a98b0e84bf349ad5ead92fc" +dependencies = [ + "ring", + "rustls-pki-types", + "untrusted", +] + +[[package]] +name = "rustversion" +version = "1.0.22" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "b39cdef0fa800fc44525c84ccb54a029961a8215f9619753635a9c0d2538d46d" + +[[package]] +name = "ryu" +version = "1.0.20" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "28d3b2b1366ec20994f1fd18c3c594f05c5dd4bc44d8bb0c1c632c8d6829481f" + +[[package]] +name = "sal-service-manager" +version = "0.1.0" +dependencies = [ + "async-trait", + "chrono", + "futures", + "log", + "once_cell", + "plist", + "serde", + "serde_json", + "thiserror", + "tokio", + "zinit-client", +] + +[[package]] +name = "same-file" +version = "1.0.6" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "93fc1dc3aaa9bfed95e02e6eadabb4baf7e3078b0bd1b4d7b6b0b68378900502" +dependencies = [ + "winapi-util", +] + +[[package]] +name = "schannel" +version = "0.1.27" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "1f29ebaa345f945cec9fbbc532eb307f0fdad8161f281b6369539c8d84876b3d" +dependencies = [ + "windows-sys 0.59.0", +] + +[[package]] +name = "scopeguard" +version = "1.2.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "94143f37725109f92c262ed2cf5e59bce7498c01bcc1502d7b9afe439a4e9f49" + +[[package]] +name = "security-framework" +version = "3.3.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "80fb1d92c5028aa318b4b8bd7302a5bfcf48be96a37fc6fc790f806b0004ee0c" +dependencies = [ + "bitflags", + "core-foundation", + "core-foundation-sys", + "libc", + "security-framework-sys", +] + +[[package]] +name = "security-framework-sys" +version = "2.14.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "49db231d56a190491cb4aeda9527f1ad45345af50b0851622a7adb8c03b01c32" +dependencies = [ + "core-foundation-sys", + "libc", +] + +[[package]] +name = "serde" +version = "1.0.219" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "5f0e2c6ed6606019b4e29e69dbaba95b11854410e5347d525002456dbbb786b6" +dependencies = [ + "serde_derive", +] + +[[package]] +name = "serde_derive" +version = "1.0.219" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "5b0276cf7f2c73365f7157c8123c21cd9a50fbbd844757af28ca1f5925fc2a00" +dependencies = [ + "proc-macro2", + "quote", + "syn", +] + +[[package]] +name = "serde_json" +version = "1.0.143" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "d401abef1d108fbd9cbaebc3e46611f4b1021f714a0597a71f41ee463f5f4a5a" +dependencies = [ + "itoa", + "memchr", + "ryu", + "serde", +] + +[[package]] +name = "serde_spanned" +version = "0.6.9" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "bf41e0cfaf7226dca15e8197172c295a782857fcb97fad1808a166870dee75a3" +dependencies = [ + "serde", +] + +[[package]] +name = "sha1" +version = "0.10.6" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "e3bf829a2d51ab4a5ddf1352d8470c140cadc8301b2ae1789db023f01cedd6ba" +dependencies = [ + "cfg-if", + "cpufeatures", + "digest", +] + +[[package]] +name = "sha1_smol" +version = "1.0.1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "bbfa15b3dddfee50a0fff136974b3e1bde555604ba463834a7eb7deb6417705d" + +[[package]] +name = "shlex" +version = "1.3.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "0fda2ff0d084019ba4d7c6f371c95d8fd75ce3524c3cb8fb653a3023f6323e64" + +[[package]] +name = "signal-hook-registry" +version = "1.4.6" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "b2a4719bff48cee6b39d12c020eeb490953ad2443b7055bd0b21fca26bd8c28b" +dependencies = [ + "libc", +] + +[[package]] +name = "slab" +version = "0.4.11" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "7a2ae44ef20feb57a68b23d846850f861394c2e02dc425a50098ae8c90267589" + +[[package]] +name = "smallvec" +version = "1.15.1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "67b1b7a3b5fe4f1376887184045fcf45c69e92af734b7aaddc05fb777b6fbd03" + +[[package]] +name = "socket2" +version = "0.5.10" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "e22376abed350d73dd1cd119b57ffccad95b4e585a7cda43e286245ce23c0678" +dependencies = [ + "libc", + "windows-sys 0.52.0", +] + +[[package]] +name = "socket2" +version = "0.6.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "233504af464074f9d066d7b5416c5f9b894a5862a6506e306f7b816cdd6f1807" +dependencies = [ + "libc", + "windows-sys 0.59.0", +] + +[[package]] +name = "soketto" +version = "0.8.1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "2e859df029d160cb88608f5d7df7fb4753fd20fdfb4de5644f3d8b8440841721" +dependencies = [ + "base64", + "bytes", + "futures", + "http", + "httparse", + "log", + "rand", + "sha1", +] + +[[package]] +name = "stable_deref_trait" +version = "1.2.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "a8f112729512f8e442d81f95a8a7ddf2b7c6b8a1a6f509a95864142b30cab2d3" + +[[package]] +name = "strsim" +version = "0.11.1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "7da8b5736845d9f2fcb837ea5d9e2628564b3b043a70948a3f0b778838c5fb4f" + +[[package]] +name = "subtle" +version = "2.6.1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "13c2bddecc57b384dee18652358fb23172facb8a2c51ccc10d74c157bdea3292" + +[[package]] +name = "syn" +version = "2.0.106" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "ede7c438028d4436d71104916910f5bb611972c5cfd7f89b8300a8186e6fada6" +dependencies = [ + "proc-macro2", + "quote", + "unicode-ident", +] + +[[package]] +name = "synstructure" +version = "0.13.2" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "728a70f3dbaf5bab7f0c4b1ac8d7ae5ea60a4b5549c8a5914361c99147a709d2" +dependencies = [ + "proc-macro2", + "quote", + "syn", +] + +[[package]] +name = "termcolor" +version = "1.4.1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "06794f8f6c5c898b3275aebefa6b8a1cb24cd2c6c79397ab15774837a0bc5755" +dependencies = [ + "winapi-util", +] + +[[package]] +name = "thiserror" +version = "1.0.69" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "b6aaf5339b578ea85b50e080feb250a3e8ae8cfcdff9a461c9ec2904bc923f52" +dependencies = [ + "thiserror-impl", +] + +[[package]] +name = "thiserror-impl" +version = "1.0.69" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "4fee6c4efc90059e10f81e6d42c60a18f76588c3d74cb83a0b242a2b6c7504c1" +dependencies = [ + "proc-macro2", + "quote", + "syn", +] + +[[package]] +name = "time" +version = "0.3.41" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "8a7619e19bc266e0f9c5e6686659d394bc57973859340060a69221e57dbc0c40" +dependencies = [ + "deranged", + "itoa", + "num-conv", + "powerfmt", + "serde", + "time-core", + "time-macros", +] + +[[package]] +name = "time-core" +version = "0.1.4" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "c9e9a38711f559d9e3ce1cdb06dd7c5b8ea546bc90052da6d06bb76da74bb07c" + +[[package]] +name = "time-macros" +version = "0.2.22" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "3526739392ec93fd8b359c8e98514cb3e8e021beb4e5f597b00a0221f8ed8a49" +dependencies = [ + "num-conv", + "time-core", +] + +[[package]] +name = "tinystr" +version = "0.8.1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "5d4f6d1145dcb577acf783d4e601bc1d76a13337bb54e6233add580b07344c8b" +dependencies = [ + "displaydoc", + "zerovec", +] + +[[package]] +name = "tokio" +version = "1.47.1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "89e49afdadebb872d3145a5638b59eb0691ea23e46ca484037cfab3b76b95038" +dependencies = [ + "backtrace", + "bytes", + "io-uring", + "libc", + "mio", + "parking_lot", + "pin-project-lite", + "signal-hook-registry", + "slab", + "socket2 0.6.0", + "tokio-macros", + "windows-sys 0.59.0", +] + +[[package]] +name = "tokio-macros" +version = "2.5.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "6e06d43f1345a3bcd39f6a56dbb7dcab2ba47e68e8ac134855e7e2bdbaf8cab8" +dependencies = [ + "proc-macro2", + "quote", + "syn", +] + +[[package]] +name = "tokio-rustls" +version = "0.26.2" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "8e727b36a1a0e8b74c376ac2211e40c2c8af09fb4013c60d910495810f008e9b" +dependencies = [ + "rustls", + "tokio", +] + +[[package]] +name = "tokio-stream" +version = "0.1.17" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "eca58d7bba4a75707817a2c44174253f9236b2d5fbd055602e9d5c07c139a047" +dependencies = [ + "futures-core", + "pin-project-lite", + "tokio", + "tokio-util", +] + +[[package]] +name = "tokio-test" +version = "0.4.4" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "2468baabc3311435b55dd935f702f42cd1b8abb7e754fb7dfb16bd36aa88f9f7" +dependencies = [ + "async-stream", + "bytes", + "futures-core", + "tokio", + "tokio-stream", +] + +[[package]] +name = "tokio-util" +version = "0.7.16" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "14307c986784f72ef81c89db7d9e28d6ac26d16213b109ea501696195e6e3ce5" +dependencies = [ + "bytes", + "futures-core", + "futures-io", + "futures-sink", + "pin-project-lite", + "tokio", +] + +[[package]] +name = "toml" +version = "0.8.23" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "dc1beb996b9d83529a9e75c17a1686767d148d70663143c7854d8b4a09ced362" +dependencies = [ + "serde", + "serde_spanned", + "toml_datetime", + "toml_edit", +] + +[[package]] +name = "toml_datetime" +version = "0.6.11" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "22cddaf88f4fbc13c51aebbf5f8eceb5c7c5a9da2ac40a13519eb5b0a0e8f11c" +dependencies = [ + "serde", +] + +[[package]] +name = "toml_edit" +version = "0.22.27" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "41fe8c660ae4257887cf66394862d21dbca4a6ddd26f04a3560410406a2f819a" +dependencies = [ + "indexmap", + "serde", + "serde_spanned", + "toml_datetime", + "toml_write", + "winnow", +] + +[[package]] +name = "toml_write" +version = "0.1.2" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "5d99f8c9a7727884afe522e9bd5edbfc91a3312b36a77b5fb8926e4c31a41801" + +[[package]] +name = "tower" +version = "0.4.13" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "b8fa9be0de6cf49e536ce1851f987bd21a43b771b09473c3549a6c853db37c1c" +dependencies = [ + "futures-core", + "futures-util", + "pin-project", + "pin-project-lite", + "tower-layer", + "tower-service", + "tracing", +] + +[[package]] +name = "tower-http" +version = "0.5.2" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "1e9cd434a998747dd2c4276bc96ee2e0c7a2eadf3cae88e52be55a05fa9053f5" +dependencies = [ + "bitflags", + "bytes", + "http", + "http-body", + "http-body-util", + "pin-project-lite", + "tower-layer", + "tower-service", +] + +[[package]] +name = "tower-layer" +version = "0.3.3" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "121c2a6cda46980bb0fcd1647ffaf6cd3fc79a013de288782836f6df9c48780e" + +[[package]] +name = "tower-service" +version = "0.3.3" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "8df9b6e13f2d32c91b9bd719c00d1958837bc7dec474d94952798cc8e69eeec3" + +[[package]] +name = "tracing" +version = "0.1.41" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "784e0ac535deb450455cbfa28a6f0df145ea1bb7ae51b821cf5e7927fdcfbdd0" +dependencies = [ + "log", + "pin-project-lite", + "tracing-attributes", + "tracing-core", +] + +[[package]] +name = "tracing-attributes" +version = "0.1.30" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "81383ab64e72a7a8b8e13130c49e3dab29def6d0c7d76a03087b3cf71c5c6903" +dependencies = [ + "proc-macro2", + "quote", + "syn", +] + +[[package]] +name = "tracing-core" +version = "0.1.34" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "b9d12581f227e93f094d3af2ae690a574abb8a2b9b7a96e7cfe9647b2b617678" +dependencies = [ + "once_cell", +] + +[[package]] +name = "try-lock" +version = "0.2.5" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "e421abadd41a4225275504ea4d6566923418b7f05506fbc9c0fe86ba7396114b" + +[[package]] +name = "typenum" +version = "1.18.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "1dccffe3ce07af9386bfd29e80c0ab1a8205a2fc34e4bcd40364df902cfa8f3f" + +[[package]] +name = "unicode-ident" +version = "1.0.18" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "5a5f39404a5da50712a4c1eecf25e90dd62b613502b7e925fd4e4d19b5c96512" + +[[package]] +name = "untrusted" +version = "0.9.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "8ecb6da28b8a351d773b68d5825ac39017e680750f980f3a1a85cd8dd28a47c1" + +[[package]] +name = "url" +version = "2.5.7" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "08bc136a29a3d1758e07a9cca267be308aeebf5cfd5a10f3f67ab2097683ef5b" +dependencies = [ + "form_urlencoded", + "idna", + "percent-encoding", + "serde", +] + +[[package]] +name = "utf8_iter" +version = "1.0.4" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "b6c140620e7ffbb22c2dee59cafe6084a59b5ffc27a8859a5f0d494b5d52b6be" + +[[package]] +name = "utf8parse" +version = "0.2.2" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "06abde3611657adf66d383f00b093d7faecc7fa57071cce2578660c9f1010821" + +[[package]] +name = "uuid" +version = "1.18.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "f33196643e165781c20a5ead5582283a7dacbb87855d867fbc2df3f81eddc1be" +dependencies = [ + "getrandom 0.3.3", + "js-sys", + "serde", + "wasm-bindgen", +] + +[[package]] +name = "version_check" +version = "0.9.5" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "0b928f33d975fc6ad9f86c8f283853ad26bdd5b10b7f1542aa2fa15e2289105a" + +[[package]] +name = "walkdir" +version = "2.5.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "29790946404f91d9c5d06f9874efddea1dc06c5efe94541a7d6863108e3a5e4b" +dependencies = [ + "same-file", + "winapi-util", +] + +[[package]] +name = "want" +version = "0.3.1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "bfa7760aed19e106de2c7c0b581b509f2f25d3dacaf737cb82ac61bc6d760b0e" +dependencies = [ + "try-lock", +] + +[[package]] +name = "wasi" +version = "0.11.1+wasi-snapshot-preview1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "ccf3ec651a847eb01de73ccad15eb7d99f80485de043efb2f370cd654f4ea44b" + +[[package]] +name = "wasi" +version = "0.14.2+wasi-0.2.4" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "9683f9a5a998d873c0d21fcbe3c083009670149a8fab228644b8bd36b2c48cb3" +dependencies = [ + "wit-bindgen-rt", +] + +[[package]] +name = "wasm-bindgen" +version = "0.2.100" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "1edc8929d7499fc4e8f0be2262a241556cfc54a0bea223790e71446f2aab1ef5" +dependencies = [ + "cfg-if", + "once_cell", + "rustversion", + "wasm-bindgen-macro", +] + +[[package]] +name = "wasm-bindgen-backend" +version = "0.2.100" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "2f0a0651a5c2bc21487bde11ee802ccaf4c51935d0d3d42a6101f98161700bc6" +dependencies = [ + "bumpalo", + "log", + "proc-macro2", + "quote", + "syn", + "wasm-bindgen-shared", +] + +[[package]] +name = "wasm-bindgen-futures" +version = "0.4.50" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "555d470ec0bc3bb57890405e5d4322cc9ea83cebb085523ced7be4144dac1e61" +dependencies = [ + "cfg-if", + "js-sys", + "once_cell", + "wasm-bindgen", + "web-sys", +] + +[[package]] +name = "wasm-bindgen-macro" +version = "0.2.100" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "7fe63fc6d09ed3792bd0897b314f53de8e16568c2b3f7982f468c0bf9bd0b407" +dependencies = [ + "quote", + "wasm-bindgen-macro-support", +] + +[[package]] +name = "wasm-bindgen-macro-support" +version = "0.2.100" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "8ae87ea40c9f689fc23f209965b6fb8a99ad69aeeb0231408be24920604395de" +dependencies = [ + "proc-macro2", + "quote", + "syn", + "wasm-bindgen-backend", + "wasm-bindgen-shared", +] + +[[package]] +name = "wasm-bindgen-shared" +version = "0.2.100" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "1a05d73b933a847d6cccdda8f838a22ff101ad9bf93e33684f39c1f5f0eece3d" +dependencies = [ + "unicode-ident", +] + +[[package]] +name = "wasm-bindgen-test" +version = "0.3.50" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "66c8d5e33ca3b6d9fa3b4676d774c5778031d27a578c2b007f905acf816152c3" +dependencies = [ + "js-sys", + "minicov", + "wasm-bindgen", + "wasm-bindgen-futures", + "wasm-bindgen-test-macro", +] + +[[package]] +name = "wasm-bindgen-test-macro" +version = "0.3.50" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "17d5042cc5fa009658f9a7333ef24291b1291a25b6382dd68862a7f3b969f69b" +dependencies = [ + "proc-macro2", + "quote", + "syn", +] + +[[package]] +name = "web-sys" +version = "0.3.77" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "33b6dd2ef9186f1f2072e409e99cd22a975331a6b3591b12c764e0e55c60d5d2" +dependencies = [ + "js-sys", + "wasm-bindgen", +] + +[[package]] +name = "webpki-root-certs" +version = "0.26.11" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "75c7f0ef91146ebfb530314f5f1d24528d7f0767efbfd31dce919275413e393e" +dependencies = [ + "webpki-root-certs 1.0.2", +] + +[[package]] +name = "webpki-root-certs" +version = "1.0.2" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "4e4ffd8df1c57e87c325000a3d6ef93db75279dc3a231125aac571650f22b12a" +dependencies = [ + "rustls-pki-types", +] + +[[package]] +name = "winapi-util" +version = "0.1.10" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "0978bf7171b3d90bac376700cb56d606feb40f251a475a5d6634613564460b22" +dependencies = [ + "windows-sys 0.60.2", +] + +[[package]] +name = "windows-core" +version = "0.61.2" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "c0fdd3ddb90610c7638aa2b3a3ab2904fb9e5cdbecc643ddb3647212781c4ae3" +dependencies = [ + "windows-implement", + "windows-interface", + "windows-link", + "windows-result", + "windows-strings", +] + +[[package]] +name = "windows-implement" +version = "0.60.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "a47fddd13af08290e67f4acabf4b459f647552718f683a7b415d290ac744a836" +dependencies = [ + "proc-macro2", + "quote", + "syn", +] + +[[package]] +name = "windows-interface" +version = "0.59.1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "bd9211b69f8dcdfa817bfd14bf1c97c9188afa36f4750130fcdf3f400eca9fa8" +dependencies = [ + "proc-macro2", + "quote", + "syn", +] + +[[package]] +name = "windows-link" +version = "0.1.3" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "5e6ad25900d524eaabdbbb96d20b4311e1e7ae1699af4fb28c17ae66c80d798a" + +[[package]] +name = "windows-result" +version = "0.3.4" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "56f42bd332cc6c8eac5af113fc0c1fd6a8fd2aa08a0119358686e5160d0586c6" +dependencies = [ + "windows-link", +] + +[[package]] +name = "windows-strings" +version = "0.4.2" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "56e6c93f3a0c3b36176cb1327a4958a0353d5d166c2a35cb268ace15e91d3b57" +dependencies = [ + "windows-link", +] + +[[package]] +name = "windows-sys" +version = "0.45.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "75283be5efb2831d37ea142365f009c02ec203cd29a3ebecbc093d52315b66d0" +dependencies = [ + "windows-targets 0.42.2", +] + +[[package]] +name = "windows-sys" +version = "0.52.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "282be5f36a8ce781fad8c8ae18fa3f9beff57ec1b52cb3de0789201425d9a33d" +dependencies = [ + "windows-targets 0.52.6", +] + +[[package]] +name = "windows-sys" +version = "0.59.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "1e38bc4d79ed67fd075bcc251a1c39b32a1776bbe92e5bef1f0bf1f8c531853b" +dependencies = [ + "windows-targets 0.52.6", +] + +[[package]] +name = "windows-sys" +version = "0.60.2" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "f2f500e4d28234f72040990ec9d39e3a6b950f9f22d3dba18416c35882612bcb" +dependencies = [ + "windows-targets 0.53.3", +] + +[[package]] +name = "windows-targets" +version = "0.42.2" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "8e5180c00cd44c9b1c88adb3693291f1cd93605ded80c250a75d472756b4d071" +dependencies = [ + "windows_aarch64_gnullvm 0.42.2", + "windows_aarch64_msvc 0.42.2", + "windows_i686_gnu 0.42.2", + "windows_i686_msvc 0.42.2", + "windows_x86_64_gnu 0.42.2", + "windows_x86_64_gnullvm 0.42.2", + "windows_x86_64_msvc 0.42.2", +] + +[[package]] +name = "windows-targets" +version = "0.52.6" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "9b724f72796e036ab90c1021d4780d4d3d648aca59e491e6b98e725b84e99973" +dependencies = [ + "windows_aarch64_gnullvm 0.52.6", + "windows_aarch64_msvc 0.52.6", + "windows_i686_gnu 0.52.6", + "windows_i686_gnullvm 0.52.6", + "windows_i686_msvc 0.52.6", + "windows_x86_64_gnu 0.52.6", + "windows_x86_64_gnullvm 0.52.6", + "windows_x86_64_msvc 0.52.6", +] + +[[package]] +name = "windows-targets" +version = "0.53.3" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "d5fe6031c4041849d7c496a8ded650796e7b6ecc19df1a431c1a363342e5dc91" +dependencies = [ + "windows-link", + "windows_aarch64_gnullvm 0.53.0", + "windows_aarch64_msvc 0.53.0", + "windows_i686_gnu 0.53.0", + "windows_i686_gnullvm 0.53.0", + "windows_i686_msvc 0.53.0", + "windows_x86_64_gnu 0.53.0", + "windows_x86_64_gnullvm 0.53.0", + "windows_x86_64_msvc 0.53.0", +] + +[[package]] +name = "windows_aarch64_gnullvm" +version = "0.42.2" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "597a5118570b68bc08d8d59125332c54f1ba9d9adeedeef5b99b02ba2b0698f8" + +[[package]] +name = "windows_aarch64_gnullvm" +version = "0.52.6" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "32a4622180e7a0ec044bb555404c800bc9fd9ec262ec147edd5989ccd0c02cd3" + +[[package]] +name = "windows_aarch64_gnullvm" +version = "0.53.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "86b8d5f90ddd19cb4a147a5fa63ca848db3df085e25fee3cc10b39b6eebae764" + +[[package]] +name = "windows_aarch64_msvc" +version = "0.42.2" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "e08e8864a60f06ef0d0ff4ba04124db8b0fb3be5776a5cd47641e942e58c4d43" + +[[package]] +name = "windows_aarch64_msvc" +version = "0.52.6" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "09ec2a7bb152e2252b53fa7803150007879548bc709c039df7627cabbd05d469" + +[[package]] +name = "windows_aarch64_msvc" +version = "0.53.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "c7651a1f62a11b8cbd5e0d42526e55f2c99886c77e007179efff86c2b137e66c" + +[[package]] +name = "windows_i686_gnu" +version = "0.42.2" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "c61d927d8da41da96a81f029489353e68739737d3beca43145c8afec9a31a84f" + +[[package]] +name = "windows_i686_gnu" +version = "0.52.6" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "8e9b5ad5ab802e97eb8e295ac6720e509ee4c243f69d781394014ebfe8bbfa0b" + +[[package]] +name = "windows_i686_gnu" +version = "0.53.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "c1dc67659d35f387f5f6c479dc4e28f1d4bb90ddd1a5d3da2e5d97b42d6272c3" + +[[package]] +name = "windows_i686_gnullvm" +version = "0.52.6" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "0eee52d38c090b3caa76c563b86c3a4bd71ef1a819287c19d586d7334ae8ed66" + +[[package]] +name = "windows_i686_gnullvm" +version = "0.53.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "9ce6ccbdedbf6d6354471319e781c0dfef054c81fbc7cf83f338a4296c0cae11" + +[[package]] +name = "windows_i686_msvc" +version = "0.42.2" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "44d840b6ec649f480a41c8d80f9c65108b92d89345dd94027bfe06ac444d1060" + +[[package]] +name = "windows_i686_msvc" +version = "0.52.6" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "240948bc05c5e7c6dabba28bf89d89ffce3e303022809e73deaefe4f6ec56c66" + +[[package]] +name = "windows_i686_msvc" +version = "0.53.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "581fee95406bb13382d2f65cd4a908ca7b1e4c2f1917f143ba16efe98a589b5d" + +[[package]] +name = "windows_x86_64_gnu" +version = "0.42.2" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "8de912b8b8feb55c064867cf047dda097f92d51efad5b491dfb98f6bbb70cb36" + +[[package]] +name = "windows_x86_64_gnu" +version = "0.52.6" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "147a5c80aabfbf0c7d901cb5895d1de30ef2907eb21fbbab29ca94c5b08b1a78" + +[[package]] +name = "windows_x86_64_gnu" +version = "0.53.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "2e55b5ac9ea33f2fc1716d1742db15574fd6fc8dadc51caab1c16a3d3b4190ba" + +[[package]] +name = "windows_x86_64_gnullvm" +version = "0.42.2" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "26d41b46a36d453748aedef1486d5c7a85db22e56aff34643984ea85514e94a3" + +[[package]] +name = "windows_x86_64_gnullvm" +version = "0.52.6" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "24d5b23dc417412679681396f2b49f3de8c1473deb516bd34410872eff51ed0d" + +[[package]] +name = "windows_x86_64_gnullvm" +version = "0.53.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "0a6e035dd0599267ce1ee132e51c27dd29437f63325753051e71dd9e42406c57" + +[[package]] +name = "windows_x86_64_msvc" +version = "0.42.2" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "9aec5da331524158c6d1a4ac0ab1541149c0b9505fde06423b02f5ef0106b9f0" + +[[package]] +name = "windows_x86_64_msvc" +version = "0.52.6" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "589f6da84c646204747d1270a2a5661ea66ed1cced2631d546fdfb155959f9ec" + +[[package]] +name = "windows_x86_64_msvc" +version = "0.53.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "271414315aff87387382ec3d271b52d7ae78726f5d44ac98b4f4030c91880486" + +[[package]] +name = "winnow" +version = "0.7.13" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "21a0236b59786fed61e2a80582dd500fe61f18b5dca67a4a067d0bc9039339cf" +dependencies = [ + "memchr", +] + +[[package]] +name = "wit-bindgen-rt" +version = "0.39.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "6f42320e61fe2cfd34354ecb597f86f413484a798ba44a8ca1165c58d42da6c1" +dependencies = [ + "bitflags", +] + +[[package]] +name = "writeable" +version = "0.6.1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "ea2f10b9bb0928dfb1b42b65e1f9e36f7f54dbdf08457afefb38afcdec4fa2bb" + +[[package]] +name = "yoke" +version = "0.8.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "5f41bb01b8226ef4bfd589436a297c53d118f65921786300e427be8d487695cc" +dependencies = [ + "serde", + "stable_deref_trait", + "yoke-derive", + "zerofrom", +] + +[[package]] +name = "yoke-derive" +version = "0.8.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "38da3c9736e16c5d3c8c597a9aaa5d1fa565d0532ae05e27c24aa62fb32c0ab6" +dependencies = [ + "proc-macro2", + "quote", + "syn", + "synstructure", +] + +[[package]] +name = "zerocopy" +version = "0.8.26" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "1039dd0d3c310cf05de012d8a39ff557cb0d23087fd44cad61df08fc31907a2f" +dependencies = [ + "zerocopy-derive", +] + +[[package]] +name = "zerocopy-derive" +version = "0.8.26" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "9ecf5b4cc5364572d7f4c329661bcc82724222973f2cab6f050a4e5c22f75181" +dependencies = [ + "proc-macro2", + "quote", + "syn", +] + +[[package]] +name = "zerofrom" +version = "0.1.6" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "50cc42e0333e05660c3587f3bf9d0478688e15d870fab3346451ce7f8c9fbea5" +dependencies = [ + "zerofrom-derive", +] + +[[package]] +name = "zerofrom-derive" +version = "0.1.6" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "d71e5d6e06ab090c67b5e44993ec16b72dcbaabc526db883a360057678b48502" +dependencies = [ + "proc-macro2", + "quote", + "syn", + "synstructure", +] + +[[package]] +name = "zeroize" +version = "1.8.1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "ced3678a2879b30306d323f4542626697a464a97c0a07c9aebf7ebca65cd4dde" + +[[package]] +name = "zerotrie" +version = "0.2.2" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "36f0bbd478583f79edad978b407914f61b2972f5af6fa089686016be8f9af595" +dependencies = [ + "displaydoc", + "yoke", + "zerofrom", +] + +[[package]] +name = "zerovec" +version = "0.11.4" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "e7aa2bd55086f1ab526693ecbe444205da57e25f4489879da80635a46d90e73b" +dependencies = [ + "yoke", + "zerofrom", + "zerovec-derive", +] + +[[package]] +name = "zerovec-derive" +version = "0.11.1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "5b96237efa0c878c64bd89c436f661be4e46b2f3eff1ebb976f7ef2321d2f58f" +dependencies = [ + "proc-macro2", + "quote", + "syn", +] + +[[package]] +name = "zinit-client" +version = "0.4.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "4121c3ba22f1b3ccc4546de32072c9530c7e2735b734641ada5280ac422ac9cd" +dependencies = [ + "async-stream", + "async-trait", + "chrono", + "futures", + "rand", + "serde", + "serde_json", + "thiserror", + "tokio", + "tracing", +] diff --git a/clients/openrpc/Cargo.toml b/clients/openrpc/Cargo.toml new file mode 100644 index 0000000..4fe6e3c --- /dev/null +++ b/clients/openrpc/Cargo.toml @@ -0,0 +1,82 @@ +[package] +name = "hero-supervisor-openrpc-client" +version = "0.1.0" +edition = "2021" +description = "OpenRPC client for Hero Supervisor" +license = "MIT OR Apache-2.0" + +[lib] +crate-type = ["cdylib", "rlib"] + +[dependencies] +# Common dependencies for both native and WASM +serde = { version = "1.0", features = ["derive"] } +serde_json = "1.0" +thiserror = "1.0" +log = "0.4" +uuid = { version = "1.0", features = ["v4", "serde"] } + +# Native JSON-RPC client (not WASM compatible) +[target.'cfg(not(target_arch = "wasm32"))'.dependencies] +jsonrpsee = { version = "0.24", features = ["http-client", "macros"] } +tokio = { version = "1.0", features = ["full"] } +hero-supervisor = { path = "../.." } +env_logger = "0.11" + +# WASM-specific dependencies +[target.'cfg(target_arch = "wasm32")'.dependencies] +wasm-bindgen = "0.2" +wasm-bindgen-futures = "0.4" +js-sys = "0.3" +web-sys = { version = "0.3", features = [ + "console", + "Request", + "RequestInit", + "RequestMode", + "Response", + "Headers", + "Window", +] } +console_log = "1.0" +getrandom = { version = "0.2", features = ["js"] } + +[target.'cfg(target_arch = "wasm32")'.dev-dependencies] +wasm-bindgen-test = "0.3" + +# UUID for job IDs (native) +[target.'cfg(not(target_arch = "wasm32"))'.dependencies.uuid] +version = "1.0" +features = ["v4", "serde"] + +# Time handling (native) +[target.'cfg(not(target_arch = "wasm32"))'.dependencies.chrono] +version = "0.4" +features = ["serde"] + +# WASM-compatible dependencies (already defined above) + +[target.'cfg(target_arch = "wasm32")'.dependencies.chrono] +version = "0.4" +features = ["serde", "wasmbind"] + +[target.'cfg(target_arch = "wasm32")'.dependencies.uuid] +version = "1.0" +features = ["v4", "serde", "js"] + +# Collections +indexmap = "2.0" + +# Interactive CLI +crossterm = "0.27" +ratatui = "0.28" + +# Command line parsing +clap = { version = "4.0", features = ["derive"] } + +[[bin]] +name = "openrpc-cli" +path = "cmd/main.rs" + +[dev-dependencies] +# Testing utilities +tokio-test = "0.4" diff --git a/clients/openrpc/README.md b/clients/openrpc/README.md new file mode 100644 index 0000000..79a5c47 --- /dev/null +++ b/clients/openrpc/README.md @@ -0,0 +1,196 @@ +# Hero Supervisor OpenRPC Client + +A Rust client library for interacting with the Hero Supervisor OpenRPC server. This crate provides a simple, async interface for managing actors and jobs remotely. + +## Features + +- **Async API**: Built on `tokio` and `jsonrpsee` for high-performance async operations +- **Type Safety**: Full Rust type safety with serde serialization/deserialization +- **Job Builder**: Fluent API for creating jobs with validation +- **Comprehensive Coverage**: All supervisor operations available via client +- **Error Handling**: Detailed error types with proper error propagation + +## Installation + +Add this to your `Cargo.toml`: + +```toml +[dependencies] +hero-supervisor-openrpc-client = "0.1.0" +tokio = { version = "1.0", features = ["full"] } +``` + +## Quick Start + +```rust +use hero_supervisor_openrpc_client::{ + SupervisorClient, RunnerConfig, RunnerType, ProcessManagerType, JobBuilder, JobType +}; +use std::path::PathBuf; +use std::time::Duration; + +#[tokio::main] +async fn main() -> Result<(), Box> { + // Create a client + let client = SupervisorClient::new("http://127.0.0.1:3030")?; + + // Add a runner + let config = RunnerConfig { + actor_id: "my_actor".to_string(), + runner_type: RunnerType::OSISRunner, + binary_path: PathBuf::from("/path/to/actor/binary"), + db_path: "/path/to/db".to_string(), + redis_url: "redis://localhost:6379".to_string(), + }; + + client.add_runner(config, ProcessManagerType::Simple).await?; + + // Start the runner + client.start_runner("my_actor").await?; + + // Create and queue a job + let job = JobBuilder::new() + .caller_id("my_client") + .context_id("example_context") + .payload("print('Hello from Hero Supervisor!');") + .job_type(JobType::OSIS) + .runner_name("my_actor") + .timeout(Duration::from_secs(60)) + .build()?; + + client.queue_job_to_runner("my_actor", job).await?; + + // Check runner status + let status = client.get_runner_status("my_actor").await?; + println!("Runner status: {:?}", status); + + // List all runners + let runners = client.list_runners().await?; + println!("Active runners: {:?}", runners); + + Ok(()) +} +``` + +## API Reference + +### Client Creation + +```rust +let client = SupervisorClient::new("http://127.0.0.1:3030")?; +``` + +### Runner Management + +```rust +// Add a runner +client.add_runner(config, ProcessManagerType::Simple).await?; + +// Remove a runner +client.remove_runner("actor_id").await?; + +// List all runners +let runners = client.list_runners().await?; + +// Start/stop runners +client.start_runner("actor_id").await?; +client.stop_runner("actor_id", false).await?; // force = false + +// Get runner status +let status = client.get_runner_status("actor_id").await?; + +// Get runner logs +let logs = client.get_runner_logs("actor_id", Some(100), false).await?; +``` + +### Job Management + +```rust +// Create a job using the builder +let job = JobBuilder::new() + .caller_id("client_id") + .context_id("context_id") + .payload("script_content") + .job_type(JobType::OSIS) + .runner_name("target_actor") + .timeout(Duration::from_secs(300)) + .env_var("KEY", "value") + .build()?; + +// Queue the job +client.queue_job_to_runner("actor_id", job).await?; +``` + +### Bulk Operations + +```rust +// Start all runners +let results = client.start_all().await?; + +// Stop all runners +let results = client.stop_all(false).await?; // force = false + +// Get status of all runners +let statuses = client.get_all_runner_status().await?; +``` + +## Types + +### RunnerType + +- `SALRunner` - System abstraction layer operations +- `OSISRunner` - Operating system interface operations +- `VRunner` - Virtualization operations +- `PyRunner` - Python-based actors + +### JobType + +- `SAL` - SAL job type +- `OSIS` - OSIS job type +- `V` - V job type +- `Python` - Python job type + +### ProcessManagerType + +- `Simple` - Direct process spawning +- `Tmux(String)` - Tmux session-based management + +### ProcessStatus + +- `Running` - Process is active +- `Stopped` - Process is stopped +- `Failed` - Process failed +- `Unknown` - Status unknown + +## Error Handling + +The client uses the `ClientError` enum for error handling: + +```rust +use hero_supervisor_openrpc_client::ClientError; + +match client.start_runner("actor_id").await { + Ok(()) => println!("Runner started successfully"), + Err(ClientError::JsonRpc(e)) => println!("JSON-RPC error: {}", e), + Err(ClientError::Server { message }) => println!("Server error: {}", message), + Err(e) => println!("Other error: {}", e), +} +``` + +## Examples + +See the `examples/` directory for complete usage examples: + +- `basic_client.rs` - Basic client usage +- `job_management.rs` - Job creation and management +- `runner_lifecycle.rs` - Complete runner lifecycle management + +## Requirements + +- Rust 1.70+ +- Hero Supervisor server running with OpenRPC feature enabled +- Network access to the supervisor server + +## License + +Licensed under either of Apache License, Version 2.0 or MIT license at your option. diff --git a/clients/openrpc/build-wasm.sh b/clients/openrpc/build-wasm.sh new file mode 100755 index 0000000..81b3d6d --- /dev/null +++ b/clients/openrpc/build-wasm.sh @@ -0,0 +1,29 @@ +#!/bin/bash + +# Build script for WASM-compatible OpenRPC client + +set -e + +echo "Building WASM OpenRPC client..." + +# Check if wasm-pack is installed +if ! command -v wasm-pack &> /dev/null; then + echo "wasm-pack is not installed. Installing..." + curl https://rustwasm.github.io/wasm-pack/installer/init.sh -sSf | sh +fi + +# Build the WASM package +echo "Building WASM package..." +wasm-pack build --target web --out-dir pkg-wasm + +echo "WASM build complete! Package available in pkg-wasm/" +echo "" +echo "To use in a web project:" +echo "1. Copy the pkg-wasm directory to your web project" +echo "2. Import the module in your JavaScript:" +echo " import init, { WasmSupervisorClient, create_client, create_job } from './pkg-wasm/hero_supervisor_openrpc_client_wasm.js';" +echo "3. Initialize the WASM module:" +echo " await init();" +echo "4. Create and use the client:" +echo " const client = create_client('http://localhost:3030');" +echo " const runners = await client.list_runners();" diff --git a/clients/openrpc/cmd/main.rs b/clients/openrpc/cmd/main.rs new file mode 100644 index 0000000..b61b8c6 --- /dev/null +++ b/clients/openrpc/cmd/main.rs @@ -0,0 +1,872 @@ +//! Interactive CLI for Hero Supervisor OpenRPC Client +//! +//! This CLI provides an interactive interface to explore and test OpenRPC methods +//! with arrow key navigation, parameter input, and response display. + +use clap::Parser; +use crossterm::{ + event::{self, DisableMouseCapture, EnableMouseCapture, Event, KeyCode, KeyEventKind}, + execute, + terminal::{disable_raw_mode, enable_raw_mode, EnterAlternateScreen, LeaveAlternateScreen}, +}; +use ratatui::{ + backend::CrosstermBackend, + layout::{Alignment, Constraint, Direction, Layout, Margin, Rect}, + style::{Color, Modifier, Style}, + text::{Line, Span, Text}, + widgets::{Block, Borders, List, ListItem, ListState, Paragraph, Wrap}, + Frame, Terminal, +}; +use serde_json::json; +use std::io; +use chrono; + +use hero_supervisor_openrpc_client::{SupervisorClient, RunnerConfig, RunnerType, ProcessManagerType}; +use std::path::PathBuf; + +#[derive(Parser)] +#[command(name = "openrpc-cli")] +#[command(about = "Interactive CLI for Hero Supervisor OpenRPC")] +struct Cli { + /// OpenRPC server URL + #[arg(short, long, default_value = "http://127.0.0.1:3030")] + url: String, +} + +#[derive(Debug, Clone)] +struct RpcMethod { + name: String, + description: String, + params: Vec, +} + +#[derive(Debug, Clone)] +struct RpcParam { + name: String, + param_type: String, + required: bool, + description: String, +} + +struct App { + client: SupervisorClient, + methods: Vec, + list_state: ListState, + current_screen: Screen, + selected_method: Option, + param_inputs: Vec, + current_param_index: usize, + response: Option, + error_message: Option, +} + +#[derive(Debug, PartialEq)] +enum Screen { + MethodList, + ParamInput, + Response, +} + +impl App { + async fn new(url: String) -> Result> { + let client = SupervisorClient::new(&url)?; + + // Test connection to OpenRPC server using the standard rpc.discover method + // This is the proper OpenRPC way to test server connectivity and discover available methods + let discovery_result = client.discover().await; + match discovery_result { + Ok(discovery_info) => { + println!("✓ Connected to OpenRPC server at {}", url); + if let Some(info) = discovery_info.get("info") { + if let Some(title) = info.get("title").and_then(|t| t.as_str()) { + println!(" Server: {}", title); + } + if let Some(version) = info.get("version").and_then(|v| v.as_str()) { + println!(" Version: {}", version); + } + } + } + Err(e) => { + return Err(format!("Failed to connect to OpenRPC server at {}: {}\nMake sure the supervisor is running with OpenRPC enabled.", url, e).into()); + } + } + + let methods = vec![ + RpcMethod { + name: "list_runners".to_string(), + description: "List all registered runners".to_string(), + params: vec![], + }, + RpcMethod { + name: "register_runner".to_string(), + description: "Register a new runner to the supervisor with secret authentication".to_string(), + params: vec![ + RpcParam { + name: "secret".to_string(), + param_type: "String".to_string(), + required: true, + description: "Secret required for runner registration".to_string(), + }, + RpcParam { + name: "name".to_string(), + param_type: "String".to_string(), + required: true, + description: "Name of the runner".to_string(), + }, + RpcParam { + name: "queue".to_string(), + param_type: "String".to_string(), + required: true, + description: "Queue name for the runner to listen to".to_string(), + }, + ], + }, + RpcMethod { + name: "run_job".to_string(), + description: "Run a job on the appropriate runner".to_string(), + params: vec![ + RpcParam { + name: "secret".to_string(), + param_type: "String".to_string(), + required: true, + description: "Secret required for job execution".to_string(), + }, + RpcParam { + name: "job_id".to_string(), + param_type: "String".to_string(), + required: true, + description: "Job ID".to_string(), + }, + RpcParam { + name: "runner_name".to_string(), + param_type: "String".to_string(), + required: true, + description: "Name of the runner to execute the job".to_string(), + }, + RpcParam { + name: "payload".to_string(), + param_type: "String".to_string(), + required: true, + description: "Job payload/script content".to_string(), + }, + ], + }, + RpcMethod { + name: "remove_runner".to_string(), + description: "Remove a runner from the supervisor".to_string(), + params: vec![ + RpcParam { + name: "actor_id".to_string(), + param_type: "String".to_string(), + required: true, + description: "ID of the runner to remove".to_string(), + }, + ], + }, + RpcMethod { + name: "start_runner".to_string(), + description: "Start a specific runner".to_string(), + params: vec![ + RpcParam { + name: "actor_id".to_string(), + param_type: "String".to_string(), + required: true, + description: "ID of the runner to start".to_string(), + }, + ], + }, + RpcMethod { + name: "stop_runner".to_string(), + description: "Stop a specific runner".to_string(), + params: vec![ + RpcParam { + name: "actor_id".to_string(), + param_type: "String".to_string(), + required: true, + description: "ID of the runner to stop".to_string(), + }, + RpcParam { + name: "force".to_string(), + param_type: "bool".to_string(), + required: true, + description: "Whether to force stop the runner".to_string(), + }, + ], + }, + RpcMethod { + name: "get_runner_status".to_string(), + description: "Get the status of a specific runner".to_string(), + params: vec![ + RpcParam { + name: "actor_id".to_string(), + param_type: "String".to_string(), + required: true, + description: "ID of the runner".to_string(), + }, + ], + }, + RpcMethod { + name: "get_all_runner_status".to_string(), + description: "Get status of all runners".to_string(), + params: vec![], + }, + RpcMethod { + name: "start_all".to_string(), + description: "Start all runners".to_string(), + params: vec![], + }, + RpcMethod { + name: "stop_all".to_string(), + description: "Stop all runners".to_string(), + params: vec![ + RpcParam { + name: "force".to_string(), + param_type: "bool".to_string(), + required: true, + description: "Whether to force stop all runners".to_string(), + }, + ], + }, + RpcMethod { + name: "get_all_status".to_string(), + description: "Get status of all components".to_string(), + params: vec![], + }, + ]; + + let mut list_state = ListState::default(); + list_state.select(Some(0)); + + Ok(App { + client, + methods, + list_state, + current_screen: Screen::MethodList, + selected_method: None, + param_inputs: vec![], + current_param_index: 0, + response: None, + error_message: None, + }) + } + + fn next_method(&mut self) { + let i = match self.list_state.selected() { + Some(i) => { + if i >= self.methods.len() - 1 { + 0 + } else { + i + 1 + } + } + None => 0, + }; + self.list_state.select(Some(i)); + } + + fn previous_method(&mut self) { + let i = match self.list_state.selected() { + Some(i) => { + if i == 0 { + self.methods.len() - 1 + } else { + i - 1 + } + } + None => 0, + }; + self.list_state.select(Some(i)); + } + + fn select_method(&mut self) { + if let Some(i) = self.list_state.selected() { + let method = self.methods[i].clone(); + if method.params.is_empty() { + // No parameters needed, call directly + self.selected_method = Some(method); + self.current_screen = Screen::Response; + } else { + // Parameters needed, go to input screen + self.selected_method = Some(method.clone()); + self.param_inputs = vec!["".to_string(); method.params.len()]; + self.current_param_index = 0; + self.current_screen = Screen::ParamInput; + } + } + } + + fn next_param(&mut self) { + if let Some(method) = &self.selected_method { + if self.current_param_index < method.params.len() - 1 { + self.current_param_index += 1; + } + } + } + + fn previous_param(&mut self) { + if self.current_param_index > 0 { + self.current_param_index -= 1; + } + } + + fn add_char_to_current_param(&mut self, c: char) { + if self.current_param_index < self.param_inputs.len() { + self.param_inputs[self.current_param_index].push(c); + } + } + + fn remove_char_from_current_param(&mut self) { + if self.current_param_index < self.param_inputs.len() { + self.param_inputs[self.current_param_index].pop(); + } + } + + async fn execute_method(&mut self) { + if let Some(method) = &self.selected_method { + self.error_message = None; + self.response = None; + + // Build parameters + let mut params = json!({}); + + if !method.params.is_empty() { + for (i, param) in method.params.iter().enumerate() { + let input = &self.param_inputs[i]; + if input.is_empty() && param.required { + self.error_message = Some(format!("Required parameter '{}' is empty", param.name)); + return; + } + + if !input.is_empty() { + let value = match param.param_type.as_str() { + "bool" => { + match input.to_lowercase().as_str() { + "true" | "1" | "yes" => json!(true), + "false" | "0" | "no" => json!(false), + _ => { + self.error_message = Some(format!("Invalid boolean value for '{}': {}", param.name, input)); + return; + } + } + } + "i32" | "i64" | "u32" | "u64" => { + match input.parse::() { + Ok(n) => json!(n), + Err(_) => { + self.error_message = Some(format!("Invalid number for '{}': {}", param.name, input)); + return; + } + } + } + _ => json!(input), + }; + + if method.name == "register_runner" { + // Special handling for register_runner method + match param.name.as_str() { + "secret" => params["secret"] = value, + "name" => params["name"] = value, + "queue" => params["queue"] = value, + _ => {} + } + } else if method.name == "run_job" { + // Special handling for run_job method + match param.name.as_str() { + "secret" => params["secret"] = value, + "job_id" => params["job_id"] = value, + "runner_name" => params["runner_name"] = value, + "payload" => params["payload"] = value, + _ => {} + } + } else { + params[¶m.name] = value; + } + } + } + } + + // Execute the method + let result: Result = match method.name.as_str() { + "list_runners" => { + match self.client.list_runners().await { + Ok(response) => { + match serde_json::to_value(response) { + Ok(value) => Ok(value), + Err(e) => Err(hero_supervisor_openrpc_client::ClientError::from(e)), + } + }, + Err(e) => Err(e), + } + } + "get_all_runner_status" => { + match self.client.get_all_runner_status().await { + Ok(response) => { + match serde_json::to_value(response) { + Ok(value) => Ok(value), + Err(e) => Err(hero_supervisor_openrpc_client::ClientError::from(e)), + } + }, + Err(e) => Err(e), + } + } + "start_all" => { + match self.client.start_all().await { + Ok(response) => { + match serde_json::to_value(response) { + Ok(value) => Ok(value), + Err(e) => Err(hero_supervisor_openrpc_client::ClientError::from(e)), + } + }, + Err(e) => Err(e), + } + } + "get_all_status" => { + match self.client.get_all_status().await { + Ok(response) => { + match serde_json::to_value(response) { + Ok(value) => Ok(value), + Err(e) => Err(hero_supervisor_openrpc_client::ClientError::from(e)), + } + }, + Err(e) => Err(e), + } + } + "stop_all" => { + let force = params.get("force").and_then(|v| v.as_bool()).unwrap_or(false); + match self.client.stop_all(force).await { + Ok(response) => { + match serde_json::to_value(response) { + Ok(value) => Ok(value), + Err(e) => Err(hero_supervisor_openrpc_client::ClientError::from(e)), + } + }, + Err(e) => Err(e), + } + } + "start_runner" => { + if let Some(actor_id) = params.get("actor_id").and_then(|v| v.as_str()) { + match self.client.start_runner(actor_id).await { + Ok(response) => { + match serde_json::to_value(response) { + Ok(value) => Ok(value), + Err(e) => Err(hero_supervisor_openrpc_client::ClientError::from(e)), + } + }, + Err(e) => Err(e), + } + } else { + Err(hero_supervisor_openrpc_client::ClientError::from( + serde_json::Error::io(std::io::Error::new(std::io::ErrorKind::InvalidInput, "Missing actor_id parameter")) + )) + } + } + "stop_runner" => { + if let (Some(actor_id), Some(force)) = ( + params.get("actor_id").and_then(|v| v.as_str()), + params.get("force").and_then(|v| v.as_bool()) + ) { + match self.client.stop_runner(actor_id, force).await { + Ok(response) => { + match serde_json::to_value(response) { + Ok(value) => Ok(value), + Err(e) => Err(hero_supervisor_openrpc_client::ClientError::from(e)), + } + }, + Err(e) => Err(e), + } + } else { + Err(hero_supervisor_openrpc_client::ClientError::from( + serde_json::Error::io(std::io::Error::new(std::io::ErrorKind::InvalidInput, "Missing parameters")) + )) + } + } + "remove_runner" => { + if let Some(actor_id) = params.get("actor_id").and_then(|v| v.as_str()) { + match self.client.remove_runner(actor_id).await { + Ok(response) => { + match serde_json::to_value(response) { + Ok(value) => Ok(value), + Err(e) => Err(hero_supervisor_openrpc_client::ClientError::from(e)), + } + }, + Err(e) => Err(e), + } + } else { + Err(hero_supervisor_openrpc_client::ClientError::from( + serde_json::Error::io(std::io::Error::new(std::io::ErrorKind::InvalidInput, "Missing actor_id parameter")) + )) + } + } + "get_runner_status" => { + if let Some(actor_id) = params.get("actor_id").and_then(|v| v.as_str()) { + match self.client.get_runner_status(actor_id).await { + Ok(response) => { + match serde_json::to_value(response) { + Ok(value) => Ok(value), + Err(e) => Err(hero_supervisor_openrpc_client::ClientError::from(e)), + } + }, + Err(e) => Err(e), + } + } else { + Err(hero_supervisor_openrpc_client::ClientError::from( + serde_json::Error::io(std::io::Error::new(std::io::ErrorKind::InvalidInput, "Missing actor_id parameter")) + )) + } + } + "register_runner" => { + if let (Some(secret), Some(name), Some(queue)) = ( + params.get("secret").and_then(|v| v.as_str()), + params.get("name").and_then(|v| v.as_str()), + params.get("queue").and_then(|v| v.as_str()) + ) { + match self.client.register_runner(secret, name, queue).await { + Ok(response) => { + match serde_json::to_value(response) { + Ok(value) => Ok(value), + Err(e) => Err(hero_supervisor_openrpc_client::ClientError::from(e)), + } + }, + Err(e) => Err(e), + } + } else { + Err(hero_supervisor_openrpc_client::ClientError::from( + serde_json::Error::io(std::io::Error::new(std::io::ErrorKind::InvalidInput, "Missing required parameters: secret, name, queue")) + )) + } + } + "run_job" => { + if let (Some(secret), Some(job_id), Some(runner_name), Some(payload)) = ( + params.get("secret").and_then(|v| v.as_str()), + params.get("job_id").and_then(|v| v.as_str()), + params.get("runner_name").and_then(|v| v.as_str()), + params.get("payload").and_then(|v| v.as_str()) + ) { + // Create a job object + let job = serde_json::json!({ + "id": job_id, + "caller_id": "cli_user", + "context_id": "cli_context", + "payload": payload, + "job_type": "SAL", + "runner_name": runner_name, + "timeout": 30000000000u64, // 30 seconds in nanoseconds + "env_vars": {}, + "created_at": chrono::Utc::now().to_rfc3339(), + "updated_at": chrono::Utc::now().to_rfc3339() + }); + + match self.client.run_job(secret, job).await { + Ok(response) => { + match serde_json::to_value(response) { + Ok(value) => Ok(value), + Err(e) => Err(hero_supervisor_openrpc_client::ClientError::from(e)), + } + }, + Err(e) => Err(e), + } + } else { + Err(hero_supervisor_openrpc_client::ClientError::from( + serde_json::Error::io(std::io::Error::new(std::io::ErrorKind::InvalidInput, "Missing required parameters: secret, job_id, runner_name, payload")) + )) + } + } + _ => Err(hero_supervisor_openrpc_client::ClientError::from( + serde_json::Error::io(std::io::Error::new(std::io::ErrorKind::InvalidInput, "Method not implemented in CLI")) + )), + }; + + match result { + Ok(response) => { + self.response = Some(format!("{:#}", response)); + } + Err(e) => { + self.error_message = Some(format!("Error: {}", e)); + } + } + + self.current_screen = Screen::Response; + } + } + + fn back_to_methods(&mut self) { + self.current_screen = Screen::MethodList; + self.selected_method = None; + self.param_inputs.clear(); + self.current_param_index = 0; + self.response = None; + self.error_message = None; + } +} + +fn ui(f: &mut Frame, app: &mut App) { + match app.current_screen { + Screen::MethodList => draw_method_list(f, app), + Screen::ParamInput => draw_param_input(f, app), + Screen::Response => draw_response(f, app), + } +} + +fn draw_method_list(f: &mut Frame, app: &mut App) { + let chunks = Layout::default() + .direction(Direction::Vertical) + .margin(1) + .constraints([Constraint::Min(0)].as_ref()) + .split(f.area()); + + let items: Vec = app + .methods + .iter() + .map(|method| { + let content = vec![Line::from(vec![ + Span::styled(&method.name, Style::default().fg(Color::Yellow)), + Span::raw(" - "), + Span::raw(&method.description), + ])]; + ListItem::new(content) + }) + .collect(); + + let items = List::new(items) + .block( + Block::default() + .borders(Borders::ALL) + .title("OpenRPC Methods (↑↓ to navigate, Enter to select, q to quit)"), + ) + .highlight_style( + Style::default() + .bg(Color::LightGreen) + .fg(Color::Black) + .add_modifier(Modifier::BOLD), + ) + .highlight_symbol(">> "); + + f.render_stateful_widget(items, chunks[0], &mut app.list_state); +} + +fn draw_param_input(f: &mut Frame, app: &mut App) { + if let Some(method) = &app.selected_method { + let chunks = Layout::default() + .direction(Direction::Vertical) + .margin(1) + .constraints([ + Constraint::Length(3), + Constraint::Min(0), + Constraint::Length(3), + ]) + .split(f.area()); + + // Title + let title = Paragraph::new(format!("Parameters for: {}", method.name)) + .block(Block::default().borders(Borders::ALL).title("Method")); + f.render_widget(title, chunks[0]); + + // Parameters - create proper form layout with separate label and input areas + let param_chunks = Layout::default() + .direction(Direction::Vertical) + .constraints(vec![Constraint::Length(5); method.params.len()]) + .split(chunks[1]); + + for (i, param) in method.params.iter().enumerate() { + let is_current = i == app.current_param_index; + + // Split each parameter into label and input areas + let param_layout = Layout::default() + .direction(Direction::Vertical) + .constraints([Constraint::Length(2), Constraint::Length(3)]) + .split(param_chunks[i]); + + // Parameter label and description + let label_style = if is_current { + Style::default().fg(Color::Yellow).add_modifier(Modifier::BOLD) + } else { + Style::default().fg(Color::White) + }; + + let label_text = vec![ + Line::from(vec![ + Span::styled(¶m.name, label_style), + Span::raw(if param.required { " (required)" } else { " (optional)" }), + Span::raw(format!(" [{}]", param.param_type)), + ]), + Line::from(Span::styled(¶m.description, Style::default().fg(Color::Gray))), + ]; + + let label_widget = Paragraph::new(label_text) + .block(Block::default().borders(Borders::NONE)); + f.render_widget(label_widget, param_layout[0]); + + // Input field + let empty_string = String::new(); + let input_value = app.param_inputs.get(i).unwrap_or(&empty_string); + + let input_display = if is_current { + if input_value.is_empty() { + "█".to_string() // Show cursor when active and empty + } else { + format!("{}█", input_value) // Show cursor at end when active + } + } else { + if input_value.is_empty() { + " ".to_string() // Empty space for inactive empty fields + } else { + input_value.clone() + } + }; + + let input_style = if is_current { + Style::default().fg(Color::Black).bg(Color::Cyan) + } else { + Style::default().fg(Color::White).bg(Color::DarkGray) + }; + + let border_style = if is_current { + Style::default().fg(Color::Green).add_modifier(Modifier::BOLD) + } else { + Style::default().fg(Color::Gray) + }; + + let input_widget = Paragraph::new(Line::from(Span::styled(input_display, input_style))) + .block( + Block::default() + .borders(Borders::ALL) + .border_style(border_style) + .title(if is_current { " INPUT " } else { "" }), + ); + + f.render_widget(input_widget, param_layout[1]); + } + + // Instructions + let instructions = Paragraph::new("↑↓ to navigate params, type to edit, Enter to execute, Esc to go back") + .block(Block::default().borders(Borders::ALL).title("Instructions")); + f.render_widget(instructions, chunks[2]); + } +} + +fn draw_response(f: &mut Frame, app: &mut App) { + let chunks = Layout::default() + .direction(Direction::Vertical) + .margin(1) + .constraints([ + Constraint::Length(3), + Constraint::Min(0), + Constraint::Length(3), + ]) + .split(f.area()); + + // Title + let method_name = app.selected_method.as_ref().map(|m| m.name.as_str()).unwrap_or("Unknown"); + let title = Paragraph::new(format!("Response for: {}", method_name)) + .block(Block::default().borders(Borders::ALL).title("Response")); + f.render_widget(title, chunks[0]); + + // Response content + let content = if let Some(error) = &app.error_message { + Text::from(error.clone()).style(Style::default().fg(Color::Red)) + } else if let Some(response) = &app.response { + Text::from(response.clone()).style(Style::default().fg(Color::Green)) + } else { + Text::from("Executing...").style(Style::default().fg(Color::Yellow)) + }; + + let response_widget = Paragraph::new(content) + .block(Block::default().borders(Borders::ALL)) + .wrap(Wrap { trim: true }); + f.render_widget(response_widget, chunks[1]); + + // Instructions + let instructions = Paragraph::new("Esc to go back to methods") + .block(Block::default().borders(Borders::ALL).title("Instructions")); + f.render_widget(instructions, chunks[2]); +} + +#[tokio::main] +async fn main() -> Result<(), Box> { + let cli = Cli::parse(); + + // Setup terminal + enable_raw_mode()?; + let mut stdout = io::stdout(); + execute!(stdout, EnterAlternateScreen, EnableMouseCapture)?; + let backend = CrosstermBackend::new(stdout); + let mut terminal = Terminal::new(backend)?; + + // Create app + let mut app = match App::new(cli.url).await { + Ok(app) => app, + Err(e) => { + // Cleanup terminal before showing error + disable_raw_mode()?; + execute!( + terminal.backend_mut(), + LeaveAlternateScreen, + DisableMouseCapture + )?; + terminal.show_cursor()?; + + eprintln!("Failed to connect to OpenRPC server: {}", e); + eprintln!("Make sure the supervisor is running with OpenRPC enabled."); + std::process::exit(1); + } + }; + + // Main loop + loop { + terminal.draw(|f| ui(f, &mut app))?; + + if let Event::Key(key) = event::read()? { + if key.kind == KeyEventKind::Press { + match app.current_screen { + Screen::MethodList => { + match key.code { + KeyCode::Char('q') => break, + KeyCode::Down => app.next_method(), + KeyCode::Up => app.previous_method(), + KeyCode::Enter => { + app.select_method(); + // If the selected method has no parameters, execute it immediately + if let Some(method) = &app.selected_method { + if method.params.is_empty() { + app.execute_method().await; + } + } + }, + _ => {} + } + } + Screen::ParamInput => { + match key.code { + KeyCode::Esc => app.back_to_methods(), + KeyCode::Up => app.previous_param(), + KeyCode::Down => app.next_param(), + KeyCode::Enter => { + app.execute_method().await; + } + KeyCode::Backspace => app.remove_char_from_current_param(), + KeyCode::Char(c) => app.add_char_to_current_param(c), + _ => {} + } + } + Screen::Response => { + match key.code { + KeyCode::Esc => app.back_to_methods(), + _ => {} + } + } + } + } + } + } + + // Restore terminal + disable_raw_mode()?; + execute!( + terminal.backend_mut(), + LeaveAlternateScreen, + DisableMouseCapture + )?; + terminal.show_cursor()?; + + Ok(()) +} diff --git a/clients/openrpc/example-wasm.html b/clients/openrpc/example-wasm.html new file mode 100644 index 0000000..8885975 --- /dev/null +++ b/clients/openrpc/example-wasm.html @@ -0,0 +1,202 @@ + + + + + Hero Supervisor WASM OpenRPC Client Example + + + +

Hero Supervisor WASM OpenRPC Client

+ +
+

Connection

+ + +
+
+ +
+

Runner Management

+ +
+ +

Register Runner

+ + + + +
+
+ +
+

Job Execution

+ + + + + +
+
+ + + + diff --git a/clients/openrpc/src/lib.rs b/clients/openrpc/src/lib.rs new file mode 100644 index 0000000..14c82f7 --- /dev/null +++ b/clients/openrpc/src/lib.rs @@ -0,0 +1,1037 @@ +//! OpenRPC client for Hero Supervisor +//! +//! This crate provides a client library for interacting with the Hero Supervisor +//! OpenRPC server. It offers a simple, async interface for managing actors and jobs. +//! +//! ## Features +//! +//! - **Native client**: Full-featured client for native Rust applications +//! - **WASM client**: Browser-compatible client using fetch APIs +//! +//! ## Usage +//! +//! ### Native Client +//! ```rust +//! use hero_supervisor_openrpc_client::SupervisorClient; +//! +//! let client = SupervisorClient::new("http://localhost:3030")?; +//! let runners = client.list_runners().await?; +//! ``` +//! +//! ### WASM Client +//! ```rust +//! use hero_supervisor_openrpc_client::wasm::WasmSupervisorClient; +//! +//! let client = WasmSupervisorClient::new("http://localhost:3030".to_string()); +//! let runners = client.list_runners().await?; +//! ``` + +use serde::{Deserialize, Serialize}; +use std::collections::HashMap; +use std::time::Duration; +use thiserror::Error; +use serde_json; +use uuid::Uuid; + +// Import types from the main supervisor crate +#[cfg(not(target_arch = "wasm32"))] +use hero_supervisor::RunnerStatus; + +#[cfg(target_arch = "wasm32")] +use wasm_bindgen::prelude::*; + +#[cfg(target_arch = "wasm32")] +use std::path::PathBuf; + +// WASM-compatible client module +#[cfg(target_arch = "wasm32")] +pub mod wasm; + +// Re-export WASM types for convenience +#[cfg(target_arch = "wasm32")] +pub use wasm::{WasmSupervisorClient, WasmJob, WasmJobType, WasmRunnerType}; + +// Native client dependencies +#[cfg(not(target_arch = "wasm32"))] +use jsonrpsee::{ + core::client::ClientT, + http_client::{HttpClient, HttpClientBuilder}, + rpc_params, +}; + +#[cfg(not(target_arch = "wasm32"))] +use std::path::PathBuf; + +/// Client for communicating with Hero Supervisor OpenRPC server +#[cfg(not(target_arch = "wasm32"))] +pub struct SupervisorClient { + client: HttpClient, + server_url: String, +} + +/// Error types for client operations +#[cfg(not(target_arch = "wasm32"))] +#[derive(Error, Debug)] +pub enum ClientError { + #[error("JSON-RPC error: {0}")] + JsonRpc(#[from] jsonrpsee::core::ClientError), + + #[error("Serialization error: {0}")] + Serialization(#[from] serde_json::Error), + + #[error("HTTP client error: {0}")] + Http(String), + + #[error("Server error: {message}")] + Server { message: String }, +} + +/// Error types for WASM client operations +#[cfg(target_arch = "wasm32")] +#[derive(Error, Debug)] +pub enum ClientError { + #[error("JSON-RPC error: {0}")] + JsonRpc(String), + + #[error("Serialization error: {0}")] + Serialization(#[from] serde_json::Error), + + #[error("HTTP client error: {0}")] + Http(String), + + #[error("Server error: {message}")] + Server { message: String }, + + #[error("JavaScript error: {0}")] + JavaScript(String), + + #[error("Network error: {0}")] + Network(String), +} + +// Implement From for jsonrpsee ClientError for WASM +#[cfg(target_arch = "wasm32")] +impl From for ClientError { + fn from(js_val: wasm_bindgen::JsValue) -> Self { + ClientError::JavaScript(format!("{:?}", js_val)) + } +} + +/// Result type for client operations +pub type ClientResult = Result; + +/// Types of runners supported by the supervisor +#[derive(Debug, Clone, PartialEq, Serialize, Deserialize)] +pub enum RunnerType { + /// SAL Runner for system abstraction layer operations + SALRunner, + /// OSIS Runner for operating system interface operations + OSISRunner, + /// V Runner for virtualization operations + VRunner, + /// Python Runner for Python-based actors + PyRunner, +} + +/// Process manager type for a runner +#[derive(Debug, Clone, PartialEq, Serialize, Deserialize)] +pub enum ProcessManagerType { + /// Simple process manager for direct process spawning + Simple, + /// Tmux process manager for session-based management + Tmux(String), // session name +} + +/// Configuration for an actor runner +#[derive(Debug, Clone, Serialize, Deserialize)] +pub struct RunnerConfig { + /// Unique identifier for the actor + pub actor_id: String, + /// Type of runner + pub runner_type: RunnerType, + /// Path to the actor binary + pub binary_path: PathBuf, + /// Database path for the actor + pub db_path: String, + /// Redis URL for job queue + pub redis_url: String, +} + +/// Job type enumeration that maps to runner types +#[derive(Debug, Clone, PartialEq, Serialize, Deserialize)] +pub enum JobType { + /// SAL job type + SAL, + /// OSIS job type + OSIS, + /// V job type + V, + /// Python job type + Python, +} + +/// Job status enumeration +#[derive(Debug, Clone, PartialEq, Serialize, Deserialize)] +pub enum JobStatus { + /// Job has been created but not yet dispatched + Created, + /// Job has been dispatched to a worker queue + Dispatched, + /// Job is currently being executed + Started, + /// Job completed successfully + Finished, + /// Job completed with an error + Error, +} + +/// Job structure for creating and managing jobs +#[derive(Debug, Clone, Serialize, Deserialize)] +pub struct Job { + /// Unique job identifier + pub id: String, + /// ID of the caller/client that created this job + pub caller_id: String, + /// Context ID for grouping related jobs + pub context_id: String, + /// Script content or payload to execute + pub payload: String, + /// Type of job (determines which actor will process it) + pub job_type: JobType, + /// Name of the specific runner/actor to execute this job + pub runner_name: String, + /// Current status of the job + pub status: JobStatus, + /// Timestamp when the job was created + pub created_at: String, + /// Timestamp when the job was last updated + pub updated_at: String, + /// Job execution timeout + pub timeout: Duration, + /// Environment variables for job execution + pub env_vars: HashMap, +} + +/// Process status wrapper for OpenRPC serialization (matches server response) +#[derive(Debug, Clone, PartialEq, Serialize, Deserialize)] +pub enum ProcessStatusWrapper { + Running, + Stopped, + Starting, + Stopping, + Error(String), +} + +/// Log information wrapper for OpenRPC serialization (matches server response) +#[derive(Debug, Clone, Serialize, Deserialize)] +pub struct LogInfoWrapper { + pub timestamp: String, + pub level: String, + pub message: String, +} + +/// Supervisor information response containing secret counts and server details +#[derive(Debug, Clone, Serialize, Deserialize)] +pub struct SupervisorInfo { + pub server_url: String, + pub admin_secrets_count: usize, + pub user_secrets_count: usize, + pub register_secrets_count: usize, + pub runners_count: usize, +} + +/// Type aliases for compatibility +#[cfg(target_arch = "wasm32")] +pub type ProcessStatus = ProcessStatusWrapper; +#[cfg(target_arch = "wasm32")] +pub type LogInfo = LogInfoWrapper; + +/// Re-export types from supervisor crate for native builds +#[cfg(not(target_arch = "wasm32"))] +pub use hero_supervisor::{LogInfo, RunnerStatus as ProcessStatus}; + +#[cfg(not(target_arch = "wasm32"))] +impl SupervisorClient { + /// Create a new supervisor client + pub fn new(server_url: impl Into) -> ClientResult { + let server_url = server_url.into(); + + let client = HttpClientBuilder::default() + .request_timeout(Duration::from_secs(30)) + .build(&server_url) + .map_err(|e| ClientError::Http(e.to_string()))?; + + Ok(Self { + client, + server_url, + }) + } + + /// Get the server URL + pub fn server_url(&self) -> &str { + &self.server_url + } + + /// Test connection using OpenRPC discovery method + /// This calls the standard `rpc.discover` method that should be available on any OpenRPC server + pub async fn discover(&self) -> ClientResult { + let result: serde_json::Value = self + .client + .request("rpc.discover", rpc_params![]) + .await.map_err(|e| ClientError::JsonRpc(e))?; + Ok(result) + } + + /// Register a new runner to the supervisor with secret authentication + pub async fn register_runner( + &self, + secret: &str, + name: &str, + queue: &str, + ) -> ClientResult<()> { + let _: () = self + .client + .request( + "register_runner", + rpc_params![secret, name, queue], + ) + .await.map_err(|e| ClientError::JsonRpc(e))?; + Ok(()) + } + + /// Run a job on the appropriate runner + pub async fn run_job( + &self, + secret: &str, + job: serde_json::Value, + ) -> ClientResult> { + let result: Option = self + .client + .request("run_job", rpc_params![secret, job]) + .await.map_err(|e| ClientError::JsonRpc(e))?; + Ok(result) + } + + /// Remove a runner from the supervisor + pub async fn remove_runner(&self, actor_id: &str) -> ClientResult<()> { + let _: () = self + .client + .request("remove_runner", rpc_params![actor_id]) + .await.map_err(|e| ClientError::JsonRpc(e))?; + Ok(()) + } + + /// List all runner IDs + pub async fn list_runners(&self) -> ClientResult> { + let runners: Vec = self + .client + .request("list_runners", rpc_params![]) + .await.map_err(|e| ClientError::JsonRpc(e))?; + Ok(runners) + } + + /// Start a specific runner + pub async fn start_runner(&self, actor_id: &str) -> ClientResult<()> { + let _: () = self + .client + .request("start_runner", rpc_params![actor_id]) + .await.map_err(|e| ClientError::JsonRpc(e))?; + Ok(()) + } + + /// Stop a specific runner + pub async fn stop_runner(&self, actor_id: &str, force: bool) -> ClientResult<()> { + let _: () = self + .client + .request("stop_runner", rpc_params![actor_id, force]) + .await.map_err(|e| ClientError::JsonRpc(e))?; + Ok(()) + } + + /// Get status of a specific runner + pub async fn get_runner_status(&self, actor_id: &str) -> ClientResult { + #[cfg(target_arch = "wasm32")] + { + let status: ProcessStatusWrapper = self + .client + .request("get_runner_status", rpc_params![actor_id]) + .await.map_err(|e| ClientError::JsonRpc(e))?; + Ok(status) + } + #[cfg(not(target_arch = "wasm32"))] + { + let status: ProcessStatusWrapper = self + .client + .request("get_runner_status", rpc_params![actor_id]) + .await.map_err(|e| ClientError::JsonRpc(e))?; + // Convert wrapper to internal type for native builds + let internal_status = match status { + ProcessStatusWrapper::Running => RunnerStatus::Running, + ProcessStatusWrapper::Stopped => RunnerStatus::Stopped, + ProcessStatusWrapper::Starting => RunnerStatus::Starting, + ProcessStatusWrapper::Stopping => RunnerStatus::Stopping, + ProcessStatusWrapper::Error(msg) => RunnerStatus::Error(msg), + }; + Ok(internal_status) + } + } + + /// Get logs for a specific runner + pub async fn get_runner_logs( + &self, + actor_id: &str, + lines: Option, + follow: bool, + ) -> ClientResult> { + #[cfg(target_arch = "wasm32")] + { + let logs: Vec = self + .client + .request("get_runner_logs", rpc_params![actor_id, lines, follow]) + .await.map_err(|e| ClientError::JsonRpc(e))?; + Ok(logs) + } + #[cfg(not(target_arch = "wasm32"))] + { + let logs: Vec = self + .client + .request("get_runner_logs", rpc_params![actor_id, lines, follow]) + .await.map_err(|e| ClientError::JsonRpc(e))?; + // Convert wrapper to internal type for native builds + let internal_logs = logs.into_iter().map(|log| hero_supervisor::LogInfo { + timestamp: log.timestamp, + level: log.level, + message: log.message, + }).collect(); + Ok(internal_logs) + } + } + + /// Queue a job to a specific runner + pub async fn queue_job_to_runner(&self, runner_name: &str, job: Job) -> ClientResult<()> { + let params = serde_json::json!({ + "runner_name": runner_name, + "job": job + }); + + let _: () = self + .client + .request("queue_job_to_runner", rpc_params![params]) + .await.map_err(|e| ClientError::JsonRpc(e))?; + Ok(()) + } + + /// Queue a job to a specific runner and wait for the result + /// This implements the proper Hero job protocol with BLPOP on reply queue + pub async fn queue_and_wait(&self, runner_name: &str, job: Job, timeout_secs: u64) -> ClientResult> { + let params = serde_json::json!({ + "runner_name": runner_name, + "job": job, + "timeout_secs": timeout_secs + }); + + let result: Option = self + .client + .request("queue_and_wait", rpc_params![params]) + .await.map_err(|e| ClientError::JsonRpc(e))?; + Ok(result) + } + + /// Get job result by job ID + pub async fn get_job_result(&self, job_id: &str) -> ClientResult> { + let result: Option = self + .client + .request("get_job_result", rpc_params![job_id]) + .await.map_err(|e| ClientError::JsonRpc(e))?; + Ok(result) + } + + /// Get status of all runners + pub async fn get_all_runner_status(&self) -> ClientResult> { + let statuses: Vec<(String, ProcessStatusWrapper)> = self + .client + .request("get_all_runner_status", rpc_params![]) + .await.map_err(|e| ClientError::JsonRpc(e))?; + + #[cfg(target_arch = "wasm32")] + { + Ok(statuses) + } + #[cfg(not(target_arch = "wasm32"))] + { + // Convert wrapper to internal type for native builds + let internal_statuses = statuses.into_iter().map(|(name, status)| { + let internal_status = match status { + ProcessStatusWrapper::Running => RunnerStatus::Running, + ProcessStatusWrapper::Stopped => RunnerStatus::Stopped, + ProcessStatusWrapper::Starting => RunnerStatus::Starting, + ProcessStatusWrapper::Stopping => RunnerStatus::Stopping, + ProcessStatusWrapper::Error(msg) => RunnerStatus::Error(msg), + }; + (name, internal_status) + }).collect(); + Ok(internal_statuses) + } + } + + /// Start all runners + pub async fn start_all(&self) -> ClientResult> { + let results: Vec<(String, bool)> = self + .client + .request("start_all", rpc_params![]) + .await.map_err(|e| ClientError::JsonRpc(e))?; + Ok(results) + } + + /// Stop all runners + pub async fn stop_all(&self, force: bool) -> ClientResult> { + let results: Vec<(String, bool)> = self + .client + .request("stop_all", rpc_params![force]) + .await.map_err(|e| ClientError::JsonRpc(e))?; + Ok(results) + } + + /// Get status of all runners (alternative method) + pub async fn get_all_status(&self) -> ClientResult> { + let statuses: Vec<(String, ProcessStatusWrapper)> = self + .client + .request("get_all_status", rpc_params![]) + .await.map_err(|e| ClientError::JsonRpc(e))?; + + #[cfg(target_arch = "wasm32")] + { + Ok(statuses) + } + #[cfg(not(target_arch = "wasm32"))] + { + // Convert wrapper to internal type for native builds + let internal_statuses = statuses.into_iter().map(|(name, status)| { + let internal_status = match status { + ProcessStatusWrapper::Running => RunnerStatus::Running, + ProcessStatusWrapper::Stopped => RunnerStatus::Stopped, + ProcessStatusWrapper::Starting => RunnerStatus::Starting, + ProcessStatusWrapper::Stopping => RunnerStatus::Stopping, + ProcessStatusWrapper::Error(msg) => RunnerStatus::Error(msg), + }; + (name, internal_status) + }).collect(); + Ok(internal_statuses) + } + } + + /// Add a secret to the supervisor + pub async fn add_secret( + &self, + admin_secret: &str, + secret_type: &str, + secret_value: &str, + ) -> ClientResult<()> { + let params = serde_json::json!({ + "admin_secret": admin_secret, + "secret_type": secret_type, + "secret_value": secret_value + }); + + let _: () = self + .client + .request("add_secret", rpc_params![params]) + .await.map_err(|e| ClientError::JsonRpc(e))?; + Ok(()) + } + + /// Remove a secret from the supervisor + pub async fn remove_secret( + &self, + admin_secret: &str, + secret_type: &str, + secret_value: &str, + ) -> ClientResult<()> { + let params = serde_json::json!({ + "admin_secret": admin_secret, + "secret_type": secret_type, + "secret_value": secret_value + }); + + let _: () = self + .client + .request("remove_secret", rpc_params![params]) + .await.map_err(|e| ClientError::JsonRpc(e))?; + Ok(()) + } + + /// List secrets (returns supervisor info including secret counts) + pub async fn list_secrets(&self, admin_secret: &str) -> ClientResult { + let params = serde_json::json!({ + "admin_secret": admin_secret + }); + + let info: SupervisorInfo = self + .client + .request("list_secrets", rpc_params![params]) + .await.map_err(|e| ClientError::JsonRpc(e))?; + Ok(info) + } + + /// Get supervisor information including secret counts + pub async fn get_supervisor_info(&self, admin_secret: &str) -> ClientResult { + let info: SupervisorInfo = self + .client + .request("get_supervisor_info", rpc_params![admin_secret]) + .await.map_err(|e| ClientError::JsonRpc(e))?; + Ok(info) + } +} + +/// Builder for creating jobs with a fluent API +pub struct JobBuilder { + caller_id: String, + context_id: String, + payload: String, + job_type: JobType, + runner_name: String, + timeout: Duration, + env_vars: HashMap, +} + +impl JobBuilder { + /// Create a new job builder + pub fn new() -> Self { + Self { + caller_id: "".to_string(), + context_id: "".to_string(), + payload: "".to_string(), + job_type: JobType::SAL, // default + runner_name: "".to_string(), + timeout: Duration::from_secs(300), // 5 minutes default + env_vars: HashMap::new(), + } + } + + /// Set the caller ID for this job + pub fn caller_id(mut self, caller_id: impl Into) -> Self { + self.caller_id = caller_id.into(); + self + } + + /// Set the context ID for this job + pub fn context_id(mut self, context_id: impl Into) -> Self { + self.context_id = context_id.into(); + self + } + + /// Set the payload (script content) for this job + pub fn payload(mut self, payload: impl Into) -> Self { + self.payload = payload.into(); + self + } + + /// Set the job type + pub fn job_type(mut self, job_type: JobType) -> Self { + self.job_type = job_type; + self + } + + /// Set the runner name for this job + pub fn runner_name(mut self, runner_name: impl Into) -> Self { + self.runner_name = runner_name.into(); + self + } + + /// Set the timeout for job execution + pub fn timeout(mut self, timeout: Duration) -> Self { + self.timeout = timeout; + self + } + + /// Set a single environment variable + pub fn env_var(mut self, key: impl Into, value: impl Into) -> Self { + self.env_vars.insert(key.into(), value.into()); + self + } + + /// Set multiple environment variables from a HashMap + pub fn env_vars(mut self, env_vars: HashMap) -> Self { + self.env_vars = env_vars; + self + } + + /// Build the job + pub fn build(self) -> ClientResult { + if self.caller_id.is_empty() { + return Err(ClientError::Server { + message: "caller_id is required".to_string(), + }); + } + if self.context_id.is_empty() { + return Err(ClientError::Server { + message: "context_id is required".to_string(), + }); + } + if self.payload.is_empty() { + return Err(ClientError::Server { + message: "payload is required".to_string(), + }); + } + if self.runner_name.is_empty() { + return Err(ClientError::Server { + message: "runner_name is required".to_string(), + }); + } + + let now = chrono::Utc::now().to_rfc3339(); + + Ok(Job { + id: Uuid::new_v4().to_string(), + caller_id: self.caller_id, + context_id: self.context_id, + payload: self.payload, + job_type: self.job_type, + runner_name: self.runner_name, + status: JobStatus::Created, + created_at: now.clone(), + updated_at: now, + timeout: self.timeout, + env_vars: self.env_vars, + }) + } +} + +impl Default for JobBuilder { + fn default() -> Self { + Self::new() + } +} + +#[cfg(test)] +mod tests { + use super::*; + + #[test] + fn test_client_creation() { + let client = SupervisorClient::new("http://127.0.0.1:3030"); + assert!(client.is_ok()); + + let client = client.unwrap(); + assert_eq!(client.server_url(), "http://127.0.0.1:3030"); + } + + #[test] + fn test_job_builder() { + let job = JobBuilder::new() + .caller_id("test_client") + .context_id("test_context") + .payload("print('Hello, World!');") + .job_type(JobType::OSIS) + .runner_name("test_runner") + .timeout(Duration::from_secs(60)) + .env_var("TEST_VAR", "test_value") + .build(); + + assert!(job.is_ok()); + let job = job.unwrap(); + + assert_eq!(job.caller_id, "test_client"); + assert_eq!(job.context_id, "test_context"); + assert_eq!(job.payload, "print('Hello, World!');"); + assert_eq!(job.job_type, JobType::OSIS); + assert_eq!(job.runner_name, "test_runner"); + assert_eq!(job.timeout, Duration::from_secs(60)); + assert_eq!(job.env_vars.get("TEST_VAR"), Some(&"test_value".to_string())); + assert_eq!(job.status, JobStatus::Created); + } + + #[test] + fn test_job_builder_validation() { + // Missing caller_id + let result = JobBuilder::new() + .context_id("test") + .payload("test") + .runner_name("test") + .build(); + assert!(result.is_err()); + + // Missing context_id + let result = JobBuilder::new() + .caller_id("test") + .payload("test") + .runner_name("test") + .build(); + assert!(result.is_err()); + + // Missing payload + let result = JobBuilder::new() + .caller_id("test") + .context_id("test") + .runner_name("test") + .build(); + assert!(result.is_err()); + + // Missing runner_name + let result = JobBuilder::new() + .caller_id("test") + .context_id("test") + .payload("test") + .build(); + assert!(result.is_err()); + } +} + +#[cfg(test)] +mod client_tests { + use super::*; + + #[cfg(not(target_arch = "wasm32"))] + mod native_tests { + use super::*; + + #[test] + fn test_client_creation() { + let client = SupervisorClient::new("http://localhost:3030"); + assert!(client.is_ok()); + let client = client.unwrap(); + assert_eq!(client.server_url(), "http://localhost:3030"); + } + + #[test] + fn test_client_creation_invalid_url() { + let client = SupervisorClient::new("invalid-url"); + // HTTP client builder validates URLs and should fail on invalid ones + assert!(client.is_err()); + } + + #[test] + fn test_process_status_wrapper_serialization() { + let status = ProcessStatusWrapper::Running; + let serialized = serde_json::to_string(&status).unwrap(); + assert_eq!(serialized, "\"Running\""); + + let status = ProcessStatusWrapper::Error("test error".to_string()); + let serialized = serde_json::to_string(&status).unwrap(); + assert!(serialized.contains("Error")); + assert!(serialized.contains("test error")); + } + + #[test] + fn test_log_info_wrapper_serialization() { + let log = LogInfoWrapper { + timestamp: "2023-01-01T00:00:00Z".to_string(), + level: "INFO".to_string(), + message: "test message".to_string(), + }; + + let serialized = serde_json::to_string(&log).unwrap(); + assert!(serialized.contains("2023-01-01T00:00:00Z")); + assert!(serialized.contains("INFO")); + assert!(serialized.contains("test message")); + + let deserialized: LogInfoWrapper = serde_json::from_str(&serialized).unwrap(); + assert_eq!(deserialized.timestamp, log.timestamp); + assert_eq!(deserialized.level, log.level); + assert_eq!(deserialized.message, log.message); + } + + #[test] + fn test_runner_type_serialization() { + let runner_type = RunnerType::SALRunner; + let serialized = serde_json::to_string(&runner_type).unwrap(); + assert_eq!(serialized, "\"SALRunner\""); + + let deserialized: RunnerType = serde_json::from_str(&serialized).unwrap(); + assert_eq!(deserialized, RunnerType::SALRunner); + } + + #[test] + fn test_job_type_conversion() { + assert_eq!(JobType::SAL, JobType::SAL); + assert_eq!(JobType::OSIS, JobType::OSIS); + assert_eq!(JobType::V, JobType::V); + assert_eq!(JobType::Python, JobType::Python); + } + + #[test] + fn test_job_status_serialization() { + let status = JobStatus::Started; + let serialized = serde_json::to_string(&status).unwrap(); + assert_eq!(serialized, "\"Started\""); + + let deserialized: JobStatus = serde_json::from_str(&serialized).unwrap(); + assert_eq!(deserialized, JobStatus::Started); + } + } + + #[cfg(target_arch = "wasm32")] + mod wasm_tests { + use super::*; + use wasm_bindgen_test::*; + + wasm_bindgen_test_configure!(run_in_browser); + + #[wasm_bindgen_test] + fn test_wasm_client_creation() { + let client = crate::wasm::WasmSupervisorClient::new("http://localhost:3030".to_string()); + assert_eq!(client.server_url(), "http://localhost:3030"); + } + + #[wasm_bindgen_test] + fn test_wasm_job_creation() { + let job = crate::wasm::WasmJob::new( + "test-id".to_string(), + "test payload".to_string(), + "SAL".to_string(), + "test-runner".to_string(), + ); + + assert_eq!(job.id(), "test-id"); + assert_eq!(job.payload(), "test payload"); + assert_eq!(job.job_type(), "SAL"); + assert_eq!(job.runner_name(), "test-runner"); + assert_eq!(job.caller_id(), "wasm_client"); + assert_eq!(job.context_id(), "wasm_context"); + assert_eq!(job.timeout_secs(), 30); + } + + #[wasm_bindgen_test] + fn test_wasm_job_setters() { + let mut job = crate::wasm::WasmJob::new( + "test-id".to_string(), + "test payload".to_string(), + "SAL".to_string(), + "test-runner".to_string(), + ); + + job.set_caller_id("custom-caller".to_string()); + job.set_context_id("custom-context".to_string()); + job.set_timeout_secs(60); + job.set_env_vars("{\"KEY\":\"VALUE\"}".to_string()); + + assert_eq!(job.caller_id(), "custom-caller"); + assert_eq!(job.context_id(), "custom-context"); + assert_eq!(job.timeout_secs(), 60); + assert_eq!(job.env_vars(), "{\"KEY\":\"VALUE\"}"); + } + + #[wasm_bindgen_test] + fn test_wasm_job_id_generation() { + let mut job = crate::wasm::WasmJob::new( + "original-id".to_string(), + "test payload".to_string(), + "SAL".to_string(), + "test-runner".to_string(), + ); + + let original_id = job.id(); + job.generate_id(); + let new_id = job.id(); + + assert_ne!(original_id, new_id); + assert!(new_id.len() > 0); + } + + #[wasm_bindgen_test] + fn test_create_job_function() { + let job = crate::wasm::create_job( + "func-test-id".to_string(), + "func test payload".to_string(), + "OSIS".to_string(), + "func-test-runner".to_string(), + ); + + assert_eq!(job.id(), "func-test-id"); + assert_eq!(job.payload(), "func test payload"); + assert_eq!(job.job_type(), "OSIS"); + assert_eq!(job.runner_name(), "func-test-runner"); + } + + #[wasm_bindgen_test] + fn test_wasm_job_type_enum() { + use crate::wasm::WasmJobType; + + // Test that enum variants exist and can be created + let sal = WasmJobType::SAL; + let osis = WasmJobType::OSIS; + let v = WasmJobType::V; + + // Test equality + assert_eq!(sal, WasmJobType::SAL); + assert_eq!(osis, WasmJobType::OSIS); + assert_eq!(v, WasmJobType::V); + + // Test inequality + assert_ne!(sal, osis); + assert_ne!(osis, v); + assert_ne!(v, sal); + } + } + + // Common tests that work on both native and WASM + #[test] + fn test_process_status_wrapper_variants() { + let running = ProcessStatusWrapper::Running; + let stopped = ProcessStatusWrapper::Stopped; + let starting = ProcessStatusWrapper::Starting; + let stopping = ProcessStatusWrapper::Stopping; + let error = ProcessStatusWrapper::Error("test".to_string()); + + // Test that all variants can be created + assert_eq!(running, ProcessStatusWrapper::Running); + assert_eq!(stopped, ProcessStatusWrapper::Stopped); + assert_eq!(starting, ProcessStatusWrapper::Starting); + assert_eq!(stopping, ProcessStatusWrapper::Stopping); + + if let ProcessStatusWrapper::Error(msg) = error { + assert_eq!(msg, "test"); + } else { + panic!("Expected Error variant"); + } + } + + #[test] + fn test_job_type_variants() { + assert_eq!(JobType::SAL, JobType::SAL); + assert_eq!(JobType::OSIS, JobType::OSIS); + assert_eq!(JobType::V, JobType::V); + assert_eq!(JobType::Python, JobType::Python); + + assert_ne!(JobType::SAL, JobType::OSIS); + assert_ne!(JobType::OSIS, JobType::V); + assert_ne!(JobType::V, JobType::Python); + } + + #[test] + fn test_job_status_variants() { + assert_eq!(JobStatus::Created, JobStatus::Created); + assert_eq!(JobStatus::Dispatched, JobStatus::Dispatched); + assert_eq!(JobStatus::Started, JobStatus::Started); + assert_eq!(JobStatus::Finished, JobStatus::Finished); + assert_eq!(JobStatus::Error, JobStatus::Error); + + assert_ne!(JobStatus::Created, JobStatus::Dispatched); + assert_ne!(JobStatus::Started, JobStatus::Finished); + } + + #[test] + fn test_runner_type_variants() { + assert_eq!(RunnerType::SALRunner, RunnerType::SALRunner); + assert_eq!(RunnerType::OSISRunner, RunnerType::OSISRunner); + assert_eq!(RunnerType::VRunner, RunnerType::VRunner); + assert_eq!(RunnerType::PyRunner, RunnerType::PyRunner); + + assert_ne!(RunnerType::SALRunner, RunnerType::OSISRunner); + assert_ne!(RunnerType::VRunner, RunnerType::PyRunner); + } + + #[test] + fn test_process_manager_type_variants() { + let simple = ProcessManagerType::Simple; + let tmux = ProcessManagerType::Tmux("test-session".to_string()); + + assert_eq!(simple, ProcessManagerType::Simple); + + if let ProcessManagerType::Tmux(session) = tmux { + assert_eq!(session, "test-session"); + } else { + panic!("Expected Tmux variant"); + } + } +} diff --git a/clients/openrpc/src/wasm.rs b/clients/openrpc/src/wasm.rs new file mode 100644 index 0000000..32da08d --- /dev/null +++ b/clients/openrpc/src/wasm.rs @@ -0,0 +1,668 @@ +//! WASM-compatible OpenRPC client for Hero Supervisor +//! +//! This module provides a WASM-compatible client library for interacting with the Hero Supervisor +//! OpenRPC server using browser-native fetch APIs. + +use wasm_bindgen::prelude::*; +use wasm_bindgen_futures::JsFuture; +use web_sys::{Request, RequestInit, RequestMode, Response, Headers}; +use serde::{Deserialize, Serialize}; +// use std::collections::HashMap; // Unused +use thiserror::Error; +use uuid::Uuid; +// use js_sys::Promise; // Unused + +/// WASM-compatible client for communicating with Hero Supervisor OpenRPC server +#[wasm_bindgen] +pub struct WasmSupervisorClient { + server_url: String, +} + +/// Error types for WASM client operations +#[derive(Error, Debug)] +pub enum WasmClientError { + #[error("Network error: {0}")] + Network(String), + + #[error("Serialization error: {0}")] + Serialization(#[from] serde_json::Error), + + #[error("JavaScript error: {0}")] + JavaScript(String), + + #[error("Server error: {message}")] + Server { message: String }, + + #[error("Invalid response format")] + InvalidResponse, +} + +/// Result type for WASM client operations +pub type WasmClientResult = Result; + +/// JSON-RPC request structure +#[derive(Serialize)] +struct JsonRpcRequest { + jsonrpc: String, + method: String, + params: serde_json::Value, + id: u32, +} + +/// JSON-RPC response structure +#[derive(Deserialize)] +struct JsonRpcResponse { + jsonrpc: String, + #[serde(skip_serializing_if = "Option::is_none")] + result: Option, + #[serde(skip_serializing_if = "Option::is_none")] + error: Option, + id: u32, +} + +/// JSON-RPC error structure +#[derive(Deserialize)] +struct JsonRpcError { + code: i32, + message: String, + #[serde(skip_serializing_if = "Option::is_none")] + data: Option, +} + +/// Types of runners supported by the supervisor +#[derive(Debug, Clone, PartialEq, Serialize, Deserialize)] +#[wasm_bindgen] +pub enum WasmRunnerType { + SALRunner, + OSISRunner, + VRunner, +} + +/// Job type enumeration that maps to runner types +#[derive(Debug, Clone, PartialEq, Serialize, Deserialize)] +#[wasm_bindgen] +pub enum WasmJobType { + SAL, + OSIS, + V, +} + +/// Job structure for creating and managing jobs +#[derive(Debug, Clone, Serialize, Deserialize)] +#[wasm_bindgen] +pub struct WasmJob { + id: String, + caller_id: String, + context_id: String, + payload: String, + runner_name: String, + executor: String, + timeout_secs: u64, + env_vars: String, // JSON string of HashMap + created_at: String, + updated_at: String, +} + +#[wasm_bindgen] +impl WasmSupervisorClient { + /// Create a new WASM supervisor client + #[wasm_bindgen(constructor)] + pub fn new(server_url: String) -> Self { + console_log::init_with_level(log::Level::Info).ok(); + Self { server_url } + } + + /// Get the server URL + #[wasm_bindgen(getter)] + pub fn server_url(&self) -> String { + self.server_url.clone() + } + + /// Test connection using OpenRPC discovery method + pub async fn discover(&self) -> Result { + let result = self.call_method("rpc.discover", serde_json::Value::Null).await; + match result { + Ok(value) => Ok(wasm_bindgen::JsValue::from_str(&value.to_string())), + Err(e) => Err(JsValue::from_str(&e.to_string())), + } + } + + /// Register a new runner to the supervisor with secret authentication + pub async fn register_runner(&self, secret: &str, name: &str, queue: &str) -> Result { + let params = serde_json::json!([{ + "secret": secret, + "name": name, + "queue": queue + }]); + + match self.call_method("register_runner", params).await { + Ok(result) => { + // Extract the runner name from the result + if let Some(runner_name) = result.as_str() { + Ok(runner_name.to_string()) + } else { + Err(JsValue::from_str("Invalid response format: expected runner name")) + } + }, + Err(e) => Err(JsValue::from_str(&e.to_string())), + } + } + + /// Create a job (fire-and-forget, non-blocking) + #[wasm_bindgen] + pub async fn create_job(&self, secret: String, job: WasmJob) -> Result { + // Backend expects RunJobParams struct with secret and job fields - wrap in array like register_runner + let params = serde_json::json!([{ + "secret": secret, + "job": { + "id": job.id, + "caller_id": job.caller_id, + "context_id": job.context_id, + "payload": job.payload, + "runner_name": job.runner_name, + "executor": job.executor, + "timeout": { + "secs": job.timeout_secs, + "nanos": 0 + }, + "env_vars": serde_json::from_str::(&job.env_vars).unwrap_or(serde_json::json!({})), + "created_at": job.created_at, + "updated_at": job.updated_at + } + }]); + + match self.call_method("create_job", params).await { + Ok(result) => { + if let Some(job_id) = result.as_str() { + Ok(job_id.to_string()) + } else { + Ok(result.to_string()) + } + } + Err(e) => Err(JsValue::from_str(&format!("Failed to create job: {:?}", e))) + } + } + + /// Run a job on a specific runner (blocking, returns result) + #[wasm_bindgen] + pub async fn run_job(&self, secret: String, job: WasmJob) -> Result { + // Backend expects RunJobParams struct with secret and job fields - wrap in array like register_runner + let params = serde_json::json!([{ + "secret": secret, + "job": { + "id": job.id, + "caller_id": job.caller_id, + "context_id": job.context_id, + "payload": job.payload, + "runner_name": job.runner_name, + "executor": job.executor, + "timeout": { + "secs": job.timeout_secs, + "nanos": 0 + }, + "env_vars": serde_json::from_str::(&job.env_vars).unwrap_or(serde_json::json!({})), + "created_at": job.created_at, + "updated_at": job.updated_at + } + }]); + + match self.call_method("run_job", params).await { + Ok(result) => { + if let Some(result_str) = result.as_str() { + Ok(result_str.to_string()) + } else { + Ok(result.to_string()) + } + }, + Err(e) => Err(JsValue::from_str(&e.to_string())), + } + } + + /// List all runner IDs + pub async fn list_runners(&self) -> Result, JsValue> { + match self.call_method("list_runners", serde_json::Value::Null).await { + Ok(result) => { + if let Ok(runners) = serde_json::from_value::>(result) { + Ok(runners) + } else { + Err(JsValue::from_str("Invalid response format for list_runners")) + } + }, + Err(e) => Err(JsValue::from_str(&e.to_string())), + } + } + + /// List all job IDs from Redis + pub async fn list_jobs(&self) -> Result, JsValue> { + match self.call_method("list_jobs", serde_json::Value::Null).await { + Ok(result) => { + if let Ok(jobs) = serde_json::from_value::>(result) { + Ok(jobs) + } else { + Err(JsValue::from_str("Invalid response format for list_jobs")) + } + }, + Err(e) => Err(JsValue::from_str(&e.to_string())), + } + } + + /// Get a job by job ID + pub async fn get_job(&self, job_id: &str) -> Result { + let params = serde_json::json!([job_id]); + match self.call_method("get_job", params).await { + Ok(result) => { + // Convert the Job result to WasmJob + if let Ok(job_value) = serde_json::from_value::(result) { + // Extract fields from the job + let id = job_value.get("id").and_then(|v| v.as_str()).unwrap_or("").to_string(); + let caller_id = job_value.get("caller_id").and_then(|v| v.as_str()).unwrap_or("").to_string(); + let context_id = job_value.get("context_id").and_then(|v| v.as_str()).unwrap_or("").to_string(); + let payload = job_value.get("payload").and_then(|v| v.as_str()).unwrap_or("").to_string(); + let runner_name = job_value.get("runner_name").and_then(|v| v.as_str()).unwrap_or("").to_string(); + let executor = job_value.get("executor").and_then(|v| v.as_str()).unwrap_or("").to_string(); + let timeout_secs = job_value.get("timeout").and_then(|v| v.get("secs")).and_then(|v| v.as_u64()).unwrap_or(30); + let env_vars = job_value.get("env_vars").map(|v| v.to_string()).unwrap_or_else(|| "{}".to_string()); + let created_at = job_value.get("created_at").and_then(|v| v.as_str()).unwrap_or("").to_string(); + let updated_at = job_value.get("updated_at").and_then(|v| v.as_str()).unwrap_or("").to_string(); + + Ok(WasmJob { + id, + caller_id, + context_id, + payload, + runner_name, + executor, + timeout_secs, + env_vars, + created_at, + updated_at, + }) + } else { + Err(JsValue::from_str("Invalid response format for get_job")) + } + }, + Err(e) => Err(JsValue::from_str(&e.to_string())), + } + } + + /// Ping a runner by dispatching a ping job to its queue + #[wasm_bindgen] + pub async fn ping_runner(&self, runner_id: &str) -> Result { + let params = serde_json::json!([runner_id]); + + match self.call_method("ping_runner", params).await { + Ok(result) => { + if let Some(job_id) = result.as_str() { + Ok(job_id.to_string()) + } else { + Ok(result.to_string()) + } + } + Err(e) => Err(JsValue::from_str(&format!("Failed to ping runner: {:?}", e))) + } + } + + /// Stop a job by ID + #[wasm_bindgen] + pub async fn stop_job(&self, job_id: &str) -> Result<(), JsValue> { + let params = serde_json::json!([job_id]); + + match self.call_method("stop_job", params).await { + Ok(_) => Ok(()), + Err(e) => Err(JsValue::from_str(&format!("Failed to stop job: {:?}", e))) + } + } + + /// Delete a job by ID + #[wasm_bindgen] + pub async fn delete_job(&self, job_id: &str) -> Result<(), JsValue> { + let params = serde_json::json!([job_id]); + + match self.call_method("delete_job", params).await { + Ok(_) => Ok(()), + Err(e) => Err(JsValue::from_str(&format!("Failed to delete job: {:?}", e))) + } + } + + /// Remove a runner from the supervisor + pub async fn remove_runner(&self, actor_id: &str) -> Result<(), JsValue> { + let params = serde_json::json!([actor_id]); + match self.call_method("remove_runner", params).await { + Ok(_) => Ok(()), + Err(e) => Err(JsValue::from_str(&e.to_string())), + } + } + + /// Start a specific runner + pub async fn start_runner(&self, actor_id: &str) -> Result<(), JsValue> { + let params = serde_json::json!([actor_id]); + match self.call_method("start_runner", params).await { + Ok(_) => Ok(()), + Err(e) => Err(JsValue::from_str(&e.to_string())), + } + } + + /// Stop a specific runner + pub async fn stop_runner(&self, actor_id: &str, force: bool) -> Result<(), JsValue> { + let params = serde_json::json!([actor_id, force]); + self.call_method("stop_runner", params) + .await + .map_err(|e| JsValue::from_str(&e.to_string()))?; + Ok(()) + } + + /// Get a specific runner by ID + pub async fn get_runner(&self, actor_id: &str) -> Result { + let params = serde_json::json!([actor_id]); + let result = self.call_method("get_runner", params) + .await + .map_err(|e| JsValue::from_str(&e.to_string()))?; + // Convert the serde_json::Value to a JsValue via string serialization + let json_string = serde_json::to_string(&result) + .map_err(|e| JsValue::from_str(&e.to_string()))?; + Ok(js_sys::JSON::parse(&json_string) + .map_err(|e| JsValue::from_str("Failed to parse JSON"))?) + } + + /// Add a secret to the supervisor + pub async fn add_secret(&self, admin_secret: &str, secret_type: &str, secret_value: &str) -> Result<(), JsValue> { + let params = serde_json::json!([{ + "admin_secret": admin_secret, + "secret_type": secret_type, + "secret_value": secret_value + }]); + match self.call_method("add_secret", params).await { + Ok(_) => Ok(()), + Err(e) => Err(JsValue::from_str(&e.to_string())), + } + } + + /// Remove a secret from the supervisor + pub async fn remove_secret(&self, admin_secret: &str, secret_type: &str, secret_value: &str) -> Result<(), JsValue> { + let params = serde_json::json!([{ + "admin_secret": admin_secret, + "secret_type": secret_type, + "secret_value": secret_value + }]); + match self.call_method("remove_secret", params).await { + Ok(_) => Ok(()), + Err(e) => Err(JsValue::from_str(&e.to_string())), + } + } + + /// List secrets (returns supervisor info including secret counts) + pub async fn list_secrets(&self, admin_secret: &str) -> Result { + let params = serde_json::json!([{ + "admin_secret": admin_secret + }]); + match self.call_method("list_secrets", params).await { + Ok(result) => { + // Convert serde_json::Value to JsValue + let result_str = serde_json::to_string(&result) + .map_err(|e| JsValue::from_str(&e.to_string()))?; + Ok(js_sys::JSON::parse(&result_str) + .map_err(|e| JsValue::from_str(&format!("JSON parse error: {:?}", e)))?) + }, + Err(e) => Err(JsValue::from_str(&e.to_string())), + } + } + + /// Get supervisor information including secret counts + pub async fn get_supervisor_info(&self, admin_secret: &str) -> Result { + let params = serde_json::json!({ + "admin_secret": admin_secret + }); + + match self.call_method("get_supervisor_info", params).await { + Ok(result) => { + let result_str = serde_json::to_string(&result) + .map_err(|e| JsValue::from_str(&format!("Serialization error: {:?}", e)))?; + Ok(js_sys::JSON::parse(&result_str) + .map_err(|e| JsValue::from_str(&format!("JSON parse error: {:?}", e)))?) + }, + Err(e) => Err(JsValue::from_str(&format!("Failed to get supervisor info: {:?}", e))), + } + } + + /// List admin secrets (returns actual secret values) + pub async fn list_admin_secrets(&self, admin_secret: &str) -> Result, JsValue> { + let params = serde_json::json!({ + "admin_secret": admin_secret + }); + + match self.call_method("list_admin_secrets", params).await { + Ok(result) => { + let secrets: Vec = serde_json::from_value(result) + .map_err(|e| JsValue::from_str(&format!("Failed to parse admin secrets: {:?}", e)))?; + Ok(secrets) + }, + Err(e) => Err(JsValue::from_str(&format!("Failed to list admin secrets: {:?}", e))), + } + } + + /// List user secrets (returns actual secret values) + pub async fn list_user_secrets(&self, admin_secret: &str) -> Result, JsValue> { + let params = serde_json::json!({ + "admin_secret": admin_secret + }); + + match self.call_method("list_user_secrets", params).await { + Ok(result) => { + let secrets: Vec = serde_json::from_value(result) + .map_err(|e| JsValue::from_str(&format!("Failed to parse user secrets: {:?}", e)))?; + Ok(secrets) + }, + Err(e) => Err(JsValue::from_str(&format!("Failed to list user secrets: {:?}", e))), + } + } + + /// List register secrets (returns actual secret values) + pub async fn list_register_secrets(&self, admin_secret: &str) -> Result, JsValue> { + let params = serde_json::json!({ + "admin_secret": admin_secret + }); + + match self.call_method("list_register_secrets", params).await { + Ok(result) => { + let secrets: Vec = serde_json::from_value(result) + .map_err(|e| JsValue::from_str(&format!("Failed to parse register secrets: {:?}", e)))?; + Ok(secrets) + }, + Err(e) => Err(JsValue::from_str(&format!("Failed to list register secrets: {:?}", e))), + } + } +} + +#[wasm_bindgen] +impl WasmJob { + /// Create a new job with default values + #[wasm_bindgen(constructor)] + pub fn new(id: String, payload: String, executor: String, runner_name: String) -> Self { + let now = js_sys::Date::new_0().to_iso_string().as_string().unwrap(); + Self { + id, + caller_id: "wasm_client".to_string(), + context_id: "wasm_context".to_string(), + payload, + runner_name, + executor, + timeout_secs: 30, + env_vars: "{}".to_string(), + created_at: now.clone(), + updated_at: now, + } + } + + /// Set the caller ID + #[wasm_bindgen(setter)] + pub fn set_caller_id(&mut self, caller_id: String) { + self.caller_id = caller_id; + } + + /// Set the context ID + #[wasm_bindgen(setter)] + pub fn set_context_id(&mut self, context_id: String) { + self.context_id = context_id; + } + + /// Set the timeout in seconds + #[wasm_bindgen(setter)] + pub fn set_timeout_secs(&mut self, timeout_secs: u64) { + self.timeout_secs = timeout_secs; + } + + /// Set environment variables as JSON string + #[wasm_bindgen(setter)] + pub fn set_env_vars(&mut self, env_vars: String) { + self.env_vars = env_vars; + } + + /// Generate a new UUID for the job + #[wasm_bindgen] + pub fn generate_id(&mut self) { + self.id = Uuid::new_v4().to_string(); + } + + /// Get the job ID + #[wasm_bindgen(getter)] + pub fn id(&self) -> String { + self.id.clone() + } + + /// Get the caller ID + #[wasm_bindgen(getter)] + pub fn caller_id(&self) -> String { + self.caller_id.clone() + } + + /// Get the context ID + #[wasm_bindgen(getter)] + pub fn context_id(&self) -> String { + self.context_id.clone() + } + + /// Get the payload + #[wasm_bindgen(getter)] + pub fn payload(&self) -> String { + self.payload.clone() + } + + /// Get the job type + #[wasm_bindgen(getter)] + pub fn executor(&self) -> String { + self.executor.clone() + } + + /// Get the runner name + #[wasm_bindgen(getter)] + pub fn runner_name(&self) -> String { + self.runner_name.clone() + } + + /// Get the timeout in seconds + #[wasm_bindgen(getter)] + pub fn timeout_secs(&self) -> u64 { + self.timeout_secs + } + + /// Get the environment variables as JSON string + #[wasm_bindgen(getter)] + pub fn env_vars(&self) -> String { + self.env_vars.clone() + } + + /// Get the created timestamp + #[wasm_bindgen(getter)] + pub fn created_at(&self) -> String { + self.created_at.clone() + } + + /// Get the updated timestamp + #[wasm_bindgen(getter)] + pub fn updated_at(&self) -> String { + self.updated_at.clone() + } +} + +impl WasmSupervisorClient { + /// Internal method to make JSON-RPC calls + async fn call_method(&self, method: &str, params: serde_json::Value) -> WasmClientResult { + let request = JsonRpcRequest { + jsonrpc: "2.0".to_string(), + method: method.to_string(), + params, + id: 1, + }; + + let body = serde_json::to_string(&request)?; + + // Create headers + let headers = Headers::new().map_err(|e| WasmClientError::JavaScript(format!("{:?}", e)))?; + headers.set("Content-Type", "application/json") + .map_err(|e| WasmClientError::JavaScript(format!("{:?}", e)))?; + + // Create request init + let opts = RequestInit::new(); + opts.set_method("POST"); + opts.set_headers(&headers); + opts.set_body(&JsValue::from_str(&body)); + opts.set_mode(RequestMode::Cors); + + // Create request + let request = Request::new_with_str_and_init(&self.server_url, &opts) + .map_err(|e| WasmClientError::JavaScript(format!("{:?}", e)))?; + + // Get window and fetch + let window = web_sys::window().ok_or_else(|| WasmClientError::JavaScript("No window object".to_string()))?; + let resp_value = JsFuture::from(window.fetch_with_request(&request)).await + .map_err(|e| WasmClientError::Network(format!("{:?}", e)))?; + + // Convert to Response + let resp: Response = resp_value.dyn_into() + .map_err(|e| WasmClientError::JavaScript(format!("{:?}", e)))?; + + // Check if response is ok + if !resp.ok() { + return Err(WasmClientError::Network(format!("HTTP {}: {}", resp.status(), resp.status_text()))); + } + + // Get response text + let text_promise = resp.text() + .map_err(|e| WasmClientError::JavaScript(format!("{:?}", e)))?; + let text_value = JsFuture::from(text_promise).await + .map_err(|e| WasmClientError::Network(format!("{:?}", e)))?; + let text = text_value.as_string() + .ok_or_else(|| WasmClientError::InvalidResponse)?; + + // Parse JSON-RPC response + let response: JsonRpcResponse = serde_json::from_str(&text)?; + + if let Some(error) = response.error { + return Err(WasmClientError::Server { + message: format!("{}: {}", error.code, error.message), + }); + } + + // For void methods, null result is valid + Ok(response.result.unwrap_or(serde_json::Value::Null)) + } +} + +/// Initialize the WASM client library (call manually if needed) +pub fn init() { + console_log::init_with_level(log::Level::Info).ok(); + log::info!("Hero Supervisor WASM OpenRPC Client initialized"); +} + +/// Utility function to create a job from JavaScript +/// Create a new job (convenience function for JavaScript) +#[wasm_bindgen] +pub fn create_job(id: String, payload: String, executor: String, runner_name: String) -> WasmJob { + WasmJob::new(id, payload, executor, runner_name) +} + +/// Utility function to create a client from JavaScript +#[wasm_bindgen] +pub fn create_client(server_url: String) -> WasmSupervisorClient { + WasmSupervisorClient::new(server_url) +} diff --git a/cmd/supervisor.rs b/cmd/supervisor.rs new file mode 100644 index 0000000..263097b --- /dev/null +++ b/cmd/supervisor.rs @@ -0,0 +1,106 @@ +//! # Hero Supervisor Binary +//! +//! Main supervisor binary that manages multiple actors and listens to jobs over Redis. +//! The supervisor builds with actor configuration, starts actors, and dispatches jobs +//! to the appropriate runners based on the job's runner_name field. + + + +use hero_supervisor::{SupervisorApp, SupervisorBuilder}; +use clap::Parser; +use log::{info, error}; +use std::path::PathBuf; + + + + +/// Command line arguments for the supervisor +#[derive(Parser, Debug)] +#[command(name = "supervisor")] +#[command(about = "Hero Supervisor - manages multiple actors and dispatches jobs")] +struct Args { + /// Path to the configuration TOML file + #[arg(short, long, value_name = "FILE")] + config: Option, + + /// Redis URL for job queue + #[arg(long, default_value = "redis://localhost:6379")] + redis_url: String, + + /// Namespace for Redis keys + #[arg(long, default_value = "")] + namespace: String, + + /// Admin secrets (can be specified multiple times) + #[arg(long = "admin-secret", value_name = "SECRET")] + admin_secrets: Vec, + + /// User secrets (can be specified multiple times) + #[arg(long = "user-secret", value_name = "SECRET")] + user_secrets: Vec, + + /// Register secrets (can be specified multiple times) + #[arg(long = "register-secret", value_name = "SECRET")] + register_secrets: Vec, + + /// OpenRPC server bind address + #[arg(long, default_value = "127.0.0.1")] + bind_address: String, + + /// OpenRPC server port + #[arg(long, default_value = "3030")] + port: u16, +} + +#[tokio::main] +async fn main() -> Result<(), Box> { + // Initialize logging + env_logger::init(); + + info!("Starting Hero Supervisor"); + + // Parse command line arguments + let args = Args::parse(); + + + + // Create and initialize supervisor using builder pattern + let mut builder = SupervisorBuilder::new() + .redis_url(&args.redis_url) + .namespace(&args.namespace); + + // Add secrets from CLI arguments + if !args.admin_secrets.is_empty() { + info!("Adding {} admin secret(s)", args.admin_secrets.len()); + builder = builder.admin_secrets(args.admin_secrets); + } + + if !args.user_secrets.is_empty() { + info!("Adding {} user secret(s)", args.user_secrets.len()); + builder = builder.user_secrets(args.user_secrets); + } + + if !args.register_secrets.is_empty() { + info!("Adding {} register secret(s)", args.register_secrets.len()); + builder = builder.register_secrets(args.register_secrets); + } + + let supervisor = match args.config { + Some(_config_path) => { + info!("Loading configuration from config file not yet implemented"); + // For now, use CLI configuration + builder.build().await? + } + None => { + info!("Using CLI configuration"); + builder.build().await? + } + }; + + let mut app = SupervisorApp::new(supervisor, args.bind_address, args.port); + + // Start the complete supervisor application + app.start().await?; + + Ok(()) +} diff --git a/docs/openrpc.json b/docs/openrpc.json new file mode 100644 index 0000000..ead89b5 --- /dev/null +++ b/docs/openrpc.json @@ -0,0 +1,213 @@ +{ + "openrpc": "1.3.2", + "info": { + "title": "Hero Supervisor OpenRPC API", + "version": "1.0.0", + "description": "OpenRPC API for managing Hero Supervisor runners and jobs" + }, + "methods": [ + { + "name": "list_runners", + "description": "List all registered runners", + "params": [], + "result": { + "name": "runners", + "schema": { + "type": "array", + "items": { "type": "string" } + } + } + }, + { + "name": "register_runner", + "description": "Register a new runner to the supervisor with secret authentication", + "params": [ + { + "name": "params", + "schema": { + "type": "object", + "properties": { + "secret": { "type": "string" }, + "name": { "type": "string" }, + "queue": { "type": "string" } + }, + "required": ["secret", "name", "queue"] + } + } + ], + "result": { + "name": "result", + "schema": { "type": "null" } + } + }, + { + "name": "run_job", + "description": "Run a job on the appropriate runner", + "params": [ + { + "name": "params", + "schema": { + "type": "object", + "properties": { + "secret": { "type": "string" }, + "job": { + "type": "object", + "properties": { + "id": { "type": "string" }, + "caller_id": { "type": "string" }, + "context_id": { "type": "string" }, + "payload": { "type": "string" }, + "job_type": { "type": "string" }, + "runner_name": { "type": "string" }, + "timeout": { "type": "number" }, + "env_vars": { "type": "object" }, + "created_at": { "type": "string" }, + "updated_at": { "type": "string" } + }, + "required": ["id", "caller_id", "context_id", "payload", "job_type", "runner_name", "timeout", "env_vars", "created_at", "updated_at"] + } + }, + "required": ["secret", "job"] + } + } + ], + "result": { + "name": "result", + "schema": { + "type": ["string", "null"] + } + } + }, + { + "name": "remove_runner", + "description": "Remove a runner from the supervisor", + "params": [ + { + "name": "actor_id", + "schema": { "type": "string" } + } + ], + "result": { + "name": "result", + "schema": { "type": "null" } + } + }, + { + "name": "start_runner", + "description": "Start a specific runner", + "params": [ + { + "name": "actor_id", + "schema": { "type": "string" } + } + ], + "result": { + "name": "result", + "schema": { "type": "null" } + } + }, + { + "name": "stop_runner", + "description": "Stop a specific runner", + "params": [ + { + "name": "actor_id", + "schema": { "type": "string" } + }, + { + "name": "force", + "schema": { "type": "boolean" } + } + ], + "result": { + "name": "result", + "schema": { "type": "null" } + } + }, + { + "name": "get_runner_status", + "description": "Get the status of a specific runner", + "params": [ + { + "name": "actor_id", + "schema": { "type": "string" } + } + ], + "result": { + "name": "status", + "schema": { "type": "object" } + } + }, + { + "name": "get_all_runner_status", + "description": "Get status of all runners", + "params": [], + "result": { + "name": "statuses", + "schema": { + "type": "array", + "items": { "type": "object" } + } + } + }, + { + "name": "start_all", + "description": "Start all runners", + "params": [], + "result": { + "name": "results", + "schema": { + "type": "array", + "items": { + "type": "array", + "items": { "type": "string" } + } + } + } + }, + { + "name": "stop_all", + "description": "Stop all runners", + "params": [ + { + "name": "force", + "schema": { "type": "boolean" } + } + ], + "result": { + "name": "results", + "schema": { + "type": "array", + "items": { + "type": "array", + "items": { "type": "string" } + } + } + } + }, + { + "name": "get_all_status", + "description": "Get status of all runners (alternative format)", + "params": [], + "result": { + "name": "statuses", + "schema": { + "type": "array", + "items": { + "type": "array", + "items": { "type": "string" } + } + } + } + }, + { + "name": "rpc.discover", + "description": "OpenRPC discovery method - returns the OpenRPC document describing this API", + "params": [], + "result": { + "name": "openrpc_document", + "schema": { "type": "object" } + } + } + ] +} diff --git a/examples/basic_openrpc_client.rs b/examples/basic_openrpc_client.rs new file mode 100644 index 0000000..9926dbe --- /dev/null +++ b/examples/basic_openrpc_client.rs @@ -0,0 +1,290 @@ +//! Comprehensive OpenRPC Example for Hero Supervisor +//! +//! This example demonstrates the complete OpenRPC workflow: +//! 1. Automatically starting a Hero Supervisor with OpenRPC server using escargot +//! 2. Building and using a mock runner binary +//! 3. Connecting with the OpenRPC client +//! 4. Managing runners (add, start, stop, remove) +//! 5. Creating and queuing jobs +//! 6. Monitoring job execution and verifying results +//! 7. Bulk operations and status monitoring +//! 8. Gracefully shutting down the supervisor +//! +//! To run this example: +//! `cargo run --example basic_openrpc_client` +//! +//! This example is completely self-contained and will start/stop the supervisor automatically. + +use hero_supervisor_openrpc_client::{ + SupervisorClient, RunnerConfig, RunnerType, ProcessManagerType, + JobBuilder, JobType, ClientError +}; +use std::time::Duration; +use escargot::CargoBuild; +use std::process::Stdio; +use tokio::time::sleep; + +#[tokio::main] +async fn main() -> Result<(), Box> { + // env_logger::init(); // Commented out to avoid version conflicts + + println!("🚀 Comprehensive OpenRPC Example for Hero Supervisor"); + println!("===================================================="); + + // Build the supervisor with OpenRPC feature (force rebuild to avoid escargot caching) + println!("\n🔨 Force rebuilding supervisor with OpenRPC feature..."); + + // Clear target directory to force fresh build + let _ = std::process::Command::new("cargo") + .arg("clean") + .output(); + + let supervisor_binary = CargoBuild::new() + .bin("supervisor") + .features("openrpc") + .current_release() + .run()?; + + println!("✅ Supervisor binary built successfully"); + + // Build the mock runner binary + println!("\n🔨 Building mock runner binary..."); + let mock_runner_binary = CargoBuild::new() + .example("mock_runner") + .current_release() + .run()?; + + println!("✅ Mock runner binary built successfully"); + + // Start the supervisor process + println!("\n🚀 Starting supervisor with OpenRPC server..."); + let mut supervisor_process = supervisor_binary + .command() + .stdout(Stdio::piped()) + .stderr(Stdio::piped()) + .spawn()?; + + println!("✅ Supervisor process started (PID: {})", supervisor_process.id()); + + // Wait for the server to start up + println!("\n⏳ Waiting for OpenRPC server to start..."); + sleep(Duration::from_secs(5)).await; + + // Create client + let client = SupervisorClient::new("http://127.0.0.1:3030")?; + println!("✅ Client created for: {}", client.server_url()); + + // Test connectivity with retries + println!("\n🔍 Testing server connectivity..."); + let mut connection_attempts = 0; + let max_attempts = 10; + + loop { + connection_attempts += 1; + match client.list_runners().await { + Ok(runners) => { + println!("✅ Server is responsive"); + println!("📋 Current runners: {:?}", runners); + break; + } + Err(e) if connection_attempts < max_attempts => { + println!("⏳ Attempt {}/{}: Server not ready yet, retrying...", connection_attempts, max_attempts); + sleep(Duration::from_secs(1)).await; + continue; + } + Err(e) => { + eprintln!("❌ Failed to connect to server after {} attempts: {}", max_attempts, e); + // Clean up the supervisor process before returning + let _ = supervisor_process.kill(); + return Err(e.into()); + } + } + } + + // Add a simple runner using the mock runner binary + let config = RunnerConfig { + actor_id: "basic_example_actor".to_string(), + runner_type: RunnerType::OSISRunner, + binary_path: mock_runner_binary.path().to_path_buf(), + db_path: "/tmp/example_db".to_string(), + redis_url: "redis://localhost:6379".to_string(), + }; + + println!("➕ Adding runner: {}", config.actor_id); + client.add_runner(config, ProcessManagerType::Simple).await?; + + // Start the runner + println!("▶️ Starting runner..."); + client.start_runner("basic_example_actor").await?; + + // Check status + let status = client.get_runner_status("basic_example_actor").await?; + println!("📊 Runner status: {:?}", status); + + // Create and queue multiple jobs to demonstrate functionality + let jobs = vec![ + ("Hello World", "print('Hello from comprehensive OpenRPC example!');"), + ("Math Calculation", "let result = 42 * 2; print(`The answer is: ${result}`);"), + ("Current Time", "print('Job executed at: ' + new Date().toISOString());"), + ]; + + let mut job_ids = Vec::new(); + + for (description, payload) in jobs { + let job = JobBuilder::new() + .caller_id("comprehensive_client") + .context_id("demo") + .payload(payload) + .job_type(JobType::OSIS) + .runner_name("basic_example_actor") + .timeout(Duration::from_secs(30)) + .build()?; + + println!("📤 Queuing job '{}': {}", description, job.id); + client.queue_job_to_runner("basic_example_actor", job.clone()).await?; + job_ids.push((job.id, description.to_string())); + + // Small delay between jobs + sleep(Duration::from_millis(500)).await; + } + + // Demonstrate synchronous job execution using polling approach + // (Note: queue_and_wait OpenRPC method registration needs debugging) + println!("\n🎯 Demonstrating synchronous job execution with result verification..."); + + let sync_jobs = vec![ + ("Synchronous Hello", "print('Hello from synchronous execution!');"), + ("Synchronous Math", "let result = 123 + 456; print(`Calculation result: ${result}`);"), + ("Synchronous Status", "print('Job processed with result verification');"), + ]; + + for (description, payload) in sync_jobs { + let job = JobBuilder::new() + .caller_id("sync_client") + .context_id("sync_demo") + .payload(payload) + .job_type(JobType::OSIS) + .runner_name("basic_example_actor") + .timeout(Duration::from_secs(30)) + .build()?; + + println!("🚀 Executing '{}' with result verification...", description); + let job_id = job.id.clone(); + + // Queue the job + client.queue_job_to_runner("basic_example_actor", job).await?; + + // Poll for completion with timeout + let mut attempts = 0; + let max_attempts = 20; // 10 seconds with 500ms intervals + let mut result = None; + + while attempts < max_attempts { + match client.get_job_result(&job_id).await { + Ok(Some(job_result)) => { + result = Some(job_result); + break; + } + Ok(None) => { + // Job not finished yet, wait and retry + sleep(Duration::from_millis(500)).await; + attempts += 1; + } + Err(e) => { + println!("⚠️ Error getting result for job {}: {}", job_id, e); + break; + } + } + } + + match result { + Some(job_result) => { + println!("✅ Job '{}' completed successfully!", description); + println!(" 📋 Job ID: {}", job_id); + println!(" 📤 Result: {}", job_result); + } + None => { + println!("⏰ Job '{}' did not complete within timeout", description); + } + } + + // Small delay between jobs + sleep(Duration::from_millis(500)).await; + } + + // Demonstrate bulk operations and status monitoring + println!("\n📊 Demonstrating bulk operations and status monitoring..."); + + // Get all runner statuses + println!("📋 Getting all runner statuses..."); + match client.get_all_runner_status().await { + Ok(statuses) => { + println!("✅ Runner statuses:"); + for (runner_id, status) in statuses { + println!(" - {}: {:?}", runner_id, status); + } + } + Err(e) => println!("❌ Failed to get runner statuses: {}", e), + } + + // List all runners one more time + println!("\n📋 Final runner list:"); + match client.list_runners().await { + Ok(runners) => { + println!("✅ Active runners: {:?}", runners); + } + Err(e) => println!("❌ Failed to list runners: {}", e), + } + + // Stop and remove runner + println!("\n⏹️ Stopping runner..."); + client.stop_runner("basic_example_actor", false).await?; + + println!("🗑️ Removing runner..."); + client.remove_runner("basic_example_actor").await?; + + // Final verification + println!("\n🔍 Final verification - listing remaining runners..."); + match client.list_runners().await { + Ok(runners) => { + if runners.contains(&"basic_example_actor".to_string()) { + println!("⚠️ Runner still present: {:?}", runners); + } else { + println!("✅ Runner successfully removed. Remaining runners: {:?}", runners); + } + } + Err(e) => println!("❌ Failed to verify runner removal: {}", e), + } + + // Gracefully shutdown the supervisor process + println!("\n🛑 Shutting down supervisor process..."); + match supervisor_process.kill() { + Ok(()) => { + println!("✅ Supervisor process terminated successfully"); + // Wait for the process to fully exit + match supervisor_process.wait() { + Ok(status) => println!("✅ Supervisor exited with status: {}", status), + Err(e) => println!("⚠️ Error waiting for supervisor exit: {}", e), + } + } + Err(e) => println!("⚠️ Error terminating supervisor: {}", e), + } + + println!("\n🎉 Comprehensive OpenRPC Example Complete!"); + println!("=========================================="); + println!("✅ Successfully demonstrated:"); + println!(" - Automatic supervisor startup with escargot"); + println!(" - Mock runner binary integration"); + println!(" - OpenRPC client connectivity with retry logic"); + println!(" - Runner management (add, start, stop, remove)"); + println!(" - Asynchronous job creation and queuing"); + println!(" - Synchronous job execution with result polling"); + println!(" - Job result verification from Redis job hash"); + println!(" - Bulk operations and status monitoring"); + println!(" - Graceful cleanup and supervisor shutdown"); + println!("\n🎯 The Hero Supervisor OpenRPC integration is fully functional!"); + println!("📝 Note: queue_and_wait method implemented but OpenRPC registration needs debugging"); + println!("🚀 Both async job queuing and sync result polling patterns work perfectly!"); + + Ok(()) +} diff --git a/examples/mock_runner.rs b/examples/mock_runner.rs new file mode 100644 index 0000000..a19d6ea --- /dev/null +++ b/examples/mock_runner.rs @@ -0,0 +1,163 @@ +//! Mock Runner Binary for Testing OpenRPC Examples +//! +//! This is a simple mock runner that simulates an actor binary for testing +//! the Hero Supervisor OpenRPC integration. It connects to Redis, listens for +//! jobs using the proper Hero job queue system, and echoes the job payload. +//! +//! Usage: +//! ```bash +//! cargo run --example mock_runner -- --actor-id test_actor --db-path /tmp/test_db --redis-url redis://localhost:6379 +//! ``` + +use std::env; +use std::time::Duration; +use tokio::time::sleep; +use redis::AsyncCommands; +use hero_supervisor::{ + job::{Job, JobStatus, JobType, keys}, +}; + +#[derive(Debug, Clone)] +pub struct MockRunnerConfig { + pub actor_id: String, + pub db_path: String, + pub redis_url: String, +} + +impl MockRunnerConfig { + pub fn from_args() -> Result> { + let args: Vec = env::args().collect(); + + let mut actor_id = None; + let mut db_path = None; + let mut redis_url = None; + + let mut i = 1; + while i < args.len() { + match args[i].as_str() { + "--actor-id" => { + if i + 1 < args.len() { + actor_id = Some(args[i + 1].clone()); + i += 2; + } else { + return Err("Missing value for --actor-id".into()); + } + } + "--redis-url" => { + if i + 1 < args.len() { + redis_url = Some(args[i + 1].clone()); + i += 2; + } else { + return Err("Missing value for --redis-url".into()); + } + } + _ => i += 1, + } + } + + Ok(MockRunnerConfig { + actor_id: actor_id.ok_or("Missing required --actor-id argument")?, + db_path: db_path.ok_or("Missing required --db-path argument")?, + redis_url: redis_url.unwrap_or_else(|| "redis://localhost:6379".to_string()), + }) + } +} + +pub struct MockRunner { + config: MockRunnerConfig, + redis_client: redis::Client, +} + +impl MockRunner { + pub fn new(config: MockRunnerConfig) -> Result> { + let redis_client = redis::Client::open(config.redis_url.clone())?; + + Ok(MockRunner { + config, + redis_client, + }) + } + + pub async fn run(&self) -> Result<(), Box> { + println!("🤖 Mock Runner '{}' starting...", self.config.actor_id); + println!("📂 DB Path: {}", self.config.db_path); + println!("🔗 Redis URL: {}", self.config.redis_url); + + let mut conn = self.redis_client.get_multiplexed_async_connection().await?; + + // Use the proper Hero job queue key for this actor instance + // Format: hero:q:work:type:{job_type}:group:{group}:inst:{instance} + let work_queue_key = keys::work_instance(&JobType::OSIS, "default", &self.config.actor_id); + + println!("👂 Listening for jobs on queue: {}", work_queue_key); + + loop { + // Try to pop a job ID from the work queue using the Hero protocol + let result: redis::RedisResult> = conn.lpop(&work_queue_key, None).await; + + match result { + Ok(Some(job_id)) => { + println!("📨 Received job ID: {}", job_id); + if let Err(e) = self.process_job(&mut conn, &job_id).await { + eprintln!("❌ Error processing job {}: {}", job_id, e); + // Mark job as error + if let Err(e2) = Job::set_error(&mut conn, &job_id, &format!("Processing error: {}", e)).await { + eprintln!("❌ Failed to set job error status: {}", e2); + } + } + } + Ok(None) => { + // No jobs available, wait a bit + sleep(Duration::from_millis(100)).await; + } + Err(e) => { + eprintln!("❌ Redis error: {}", e); + sleep(Duration::from_secs(1)).await; + } + } + } + } + + async fn process_job(&self, conn: &mut redis::aio::MultiplexedConnection, job_id: &str) -> Result<(), Box> { + // Load the job from Redis using the Hero job system + let job = Job::load_from_redis(conn, job_id).await?; + + println!("📝 Processing job: {}", job.id); + println!("📝 Caller: {}", job.caller_id); + println!("📝 Context: {}", job.context_id); + println!("📝 Payload: {}", job.payload); + println!("📝 Job Type: {:?}", job.job_type); + + // Mark job as started + Job::update_status(conn, job_id, JobStatus::Started).await?; + println!("🚀 Job {} marked as Started", job_id); + + // Simulate processing time + sleep(Duration::from_millis(500)).await; + + // Echo the payload (simulate job execution) + let output = format!("echo: {}", job.payload); + println!("📤 Output: {}", output); + + // Set the job result + Job::set_result(conn, job_id, &output).await?; + + // Mark job as finished + Job::update_status(conn, job_id, JobStatus::Finished).await?; + println!("✅ Job {} completed successfully", job_id); + + Ok(()) + } +} + +#[tokio::main] +async fn main() -> Result<(), Box> { + // Parse command line arguments + let config = MockRunnerConfig::from_args()?; + + // Create and run the mock runner + let runner = MockRunner::new(config)?; + runner.run().await?; + + Ok(()) +} diff --git a/examples/supervisor/README.md b/examples/supervisor/README.md new file mode 100644 index 0000000..e4834a1 --- /dev/null +++ b/examples/supervisor/README.md @@ -0,0 +1,108 @@ +# Hero Supervisor Example + +This example demonstrates how to configure and run the Hero Supervisor with multiple actors using a TOML configuration file. + +## Files + +- `config.toml` - Example supervisor configuration with multiple actors +- `run_supervisor.sh` - Shell script to build and run the supervisor with the example config +- `run_supervisor.rs` - Rust script using escargot to build and run the supervisor +- `README.md` - This documentation file + +## Configuration + +The `config.toml` file defines: + +- **Redis connection**: URL for the Redis server used for job queuing +- **Database path**: Local path for supervisor state storage +- **Job queue key**: Redis key for the supervisor job queue +- **Actors**: List of actor configurations with: + - `name`: Unique identifier for the actor + - `runner_type`: Type of runner ("SAL", "OSIS", "V", "Python") + - `binary_path`: Path to the actor binary + - `process_manager`: Process management type ("simple" or "tmux") + +## Prerequisites + +1. **Redis Server**: Ensure Redis is running on `localhost:6379` (or update the config) +2. **Actor Binaries**: Build the required actor binaries referenced in the config: + ```bash + # Build SAL worker + cd ../../sal + cargo build --bin sal_worker + + # Build OSIS and system workers + cd ../../worker + cargo build --bin osis + cargo build --bin system + ``` + +## Running the Example + +### Option 1: Shell Script (Recommended) + +```bash +./run_supervisor.sh +``` + +### Option 2: Rust Script with Escargot + +```bash +cargo +nightly -Zscript run_supervisor.rs +``` + +### Option 3: Manual Build and Run + +```bash +# Build the supervisor +cd ../../../supervisor +cargo build --bin supervisor --features cli + +# Run with config +./target/debug/supervisor --config ../baobab/examples/supervisor/config.toml +``` + +## Usage + +Once running, the supervisor will: + +1. Load the configuration from `config.toml` +2. Initialize and start all configured actors +3. Listen for jobs on the Redis queue (`hero:supervisor:jobs`) +4. Dispatch jobs to appropriate actors based on the `runner_name` field +5. Monitor actor health and status + +## Testing + +You can test the supervisor by dispatching jobs to the Redis queue: + +```bash +# Using redis-cli to add a test job +redis-cli LPUSH "hero:supervisor:jobs" '{"id":"test-123","runner_name":"sal_actor_1","script":"print(\"Hello from SAL actor!\")"}' +``` + +## Stopping + +Use `Ctrl+C` to gracefully shutdown the supervisor. It will: + +1. Stop accepting new jobs +2. Wait for running jobs to complete +3. Shutdown all managed actors +4. Clean up resources + +## Customization + +Modify `config.toml` to: + +- Add more actors +- Change binary paths to match your build locations +- Update Redis connection settings +- Configure different process managers per actor +- Adjust database and queue settings + +## Troubleshooting + +- **Redis Connection**: Ensure Redis is running and accessible +- **Binary Paths**: Verify all actor binary paths exist and are executable +- **Permissions**: Ensure the supervisor has permission to create the database directory +- **Ports**: Check that Redis port (6379) is not blocked by firewall diff --git a/examples/supervisor/config.toml b/examples/supervisor/config.toml new file mode 100644 index 0000000..e255335 --- /dev/null +++ b/examples/supervisor/config.toml @@ -0,0 +1,18 @@ +# Hero Supervisor Configuration +# This configuration defines the Redis connection, database path, and actors to manage + +# Redis connection URL +redis_url = "redis://localhost:6379" + +# Database path for supervisor state +db_path = "/tmp/supervisor_example_db" + +# Job queue key for supervisor jobs +job_queue_key = "hero:supervisor:jobs" + +# Actor configurations +[[actors]] +name = "sal_actor_1" +runner_type = "SAL" +binary_path = "cargo run /Users/timurgordon/code/git.ourworld.tf/herocode/supervisor/examples/mock_runner.rs" +process_manager = "tmux" \ No newline at end of file diff --git a/examples/supervisor/run_supervisor.rs b/examples/supervisor/run_supervisor.rs new file mode 100644 index 0000000..4b5983e --- /dev/null +++ b/examples/supervisor/run_supervisor.rs @@ -0,0 +1,70 @@ +#!/usr/bin/env cargo +nightly -Zscript +//! ```cargo +//! [dependencies] +//! escargot = "0.5" +//! tokio = { version = "1.0", features = ["full"] } +//! log = "0.4" +//! env_logger = "0.10" +//! ``` + +use escargot::CargoBuild; +use std::process::Command; +use log::{info, error}; + +#[tokio::main] +async fn main() -> Result<(), Box> { + // Initialize logging + env_logger::init(); + + info!("Building and running Hero Supervisor with example configuration"); + + // Get the current directory (when running as cargo example, this is the crate root) + let current_dir = std::env::current_dir()?; + info!("Current directory: {}", current_dir.display()); + + // Path to the supervisor crate (current directory when running as example) + let supervisor_crate_path = current_dir.clone(); + + // Path to the config file (in examples/supervisor subdirectory) + let config_path = current_dir.join("examples/supervisor/config.toml"); + + if !config_path.exists() { + error!("Config file not found: {}", config_path.display()); + return Err("Config file not found".into()); + } + + info!("Using config file: {}", config_path.display()); + + // Build the supervisor binary using escargot + info!("Building supervisor binary..."); + let supervisor_bin = CargoBuild::new() + .bin("supervisor") + .manifest_path(supervisor_crate_path.join("Cargo.toml")) + .features("cli") + .run()?; + + info!("Supervisor binary built successfully"); + + // Run the supervisor with the config file + info!("Starting supervisor with config: {}", config_path.display()); + + let mut cmd = Command::new(supervisor_bin.path()); + cmd.arg("--config") + .arg(&config_path); + + // Add environment variables for better logging + cmd.env("RUST_LOG", "info"); + + info!("Executing: {:?}", cmd); + + // Execute the supervisor + let status = cmd.status()?; + + if status.success() { + info!("Supervisor completed successfully"); + } else { + error!("Supervisor exited with status: {}", status); + } + + Ok(()) +} diff --git a/examples/supervisor/run_supervisor.sh b/examples/supervisor/run_supervisor.sh new file mode 100755 index 0000000..25111f1 --- /dev/null +++ b/examples/supervisor/run_supervisor.sh @@ -0,0 +1,52 @@ +#!/bin/bash + +# Hero Supervisor Example Runner +# This script builds and runs the supervisor binary with the example configuration + +set -e + +# Get the directory of this script +SCRIPT_DIR="$(cd "$(dirname "${BASH_SOURCE[0]}")" && pwd)" +SUPERVISOR_DIR="$SCRIPT_DIR/../../../supervisor" +CONFIG_FILE="$SCRIPT_DIR/config.toml" + +echo "🚀 Building and running Hero Supervisor with example configuration" +echo "📁 Script directory: $SCRIPT_DIR" +echo "🔧 Supervisor crate: $SUPERVISOR_DIR" +echo "⚙️ Config file: $CONFIG_FILE" + +# Check if config file exists +if [ ! -f "$CONFIG_FILE" ]; then + echo "❌ Config file not found: $CONFIG_FILE" + exit 1 +fi + +# Check if supervisor directory exists +if [ ! -d "$SUPERVISOR_DIR" ]; then + echo "❌ Supervisor directory not found: $SUPERVISOR_DIR" + exit 1 +fi + +# Build the supervisor binary +echo "🔨 Building supervisor binary..." +cd "$SUPERVISOR_DIR" +cargo build --bin supervisor --features cli + +# Check if build was successful +if [ $? -ne 0 ]; then + echo "❌ Failed to build supervisor binary" + exit 1 +fi + +echo "✅ Supervisor binary built successfully" + +# Run the supervisor with the config file +echo "🎯 Starting supervisor with config: $CONFIG_FILE" +echo "📝 Use Ctrl+C to stop the supervisor" +echo "" + +# Set environment variables for better logging +export RUST_LOG=info + +# Execute the supervisor +exec "$SUPERVISOR_DIR/target/debug/supervisor" --config "$CONFIG_FILE" diff --git a/examples/test_openrpc_methods.rs b/examples/test_openrpc_methods.rs new file mode 100644 index 0000000..4cb76fa --- /dev/null +++ b/examples/test_openrpc_methods.rs @@ -0,0 +1,59 @@ +//! Test to verify OpenRPC method registration + +use hero_supervisor_openrpc_client::SupervisorClient; +use tokio::time::{sleep, Duration}; + +#[tokio::main] +async fn main() -> Result<(), Box> { + println!("🔍 Testing OpenRPC method registration"); + + // Start a local supervisor with OpenRPC (assume it's running) + println!("📡 Connecting to OpenRPC server..."); + let client = SupervisorClient::new("http://127.0.0.1:3030").await?; + + // Test basic methods first + println!("🧪 Testing basic methods..."); + + // Test list_runners (should work) + match client.list_runners().await { + Ok(runners) => println!("✅ list_runners works: {:?}", runners), + Err(e) => println!("❌ list_runners failed: {}", e), + } + + // Test get_all_runner_status (might have serialization issues) + match client.get_all_runner_status().await { + Ok(statuses) => println!("✅ get_all_runner_status works: {} runners", statuses.len()), + Err(e) => println!("❌ get_all_runner_status failed: {}", e), + } + + // Test the new queue_and_wait method + println!("🎯 Testing queue_and_wait method..."); + + // Create a simple test job + use hero_supervisor::job::{JobBuilder, JobType}; + let job = JobBuilder::new() + .caller_id("test_client") + .context_id("method_test") + .payload("print('Testing queue_and_wait method registration');") + .job_type(JobType::OSIS) + .runner_name("osis_actor") // Use existing runner + .timeout(Duration::from_secs(10)) + .build()?; + + match client.queue_and_wait("osis_actor", job, 10).await { + Ok(Some(result)) => println!("✅ queue_and_wait works! Result: {}", result), + Ok(None) => println!("⏰ queue_and_wait timed out"), + Err(e) => { + println!("❌ queue_and_wait failed: {}", e); + + // Check if it's a MethodNotFound error + if e.to_string().contains("Method not found") { + println!("🔍 Method not found - this suggests trait registration issue"); + } + } + } + + println!("🏁 OpenRPC method test completed"); + + Ok(()) +} diff --git a/examples/test_queue_and_wait.rs b/examples/test_queue_and_wait.rs new file mode 100644 index 0000000..ee31f51 --- /dev/null +++ b/examples/test_queue_and_wait.rs @@ -0,0 +1,70 @@ +//! Simple test for the queue_and_wait functionality + +use hero_supervisor::{ + supervisor::{Supervisor, ProcessManagerType}, + runner::RunnerConfig, + job::{JobBuilder, JobType}, +}; +use std::time::Duration; +use std::path::PathBuf; + +#[tokio::main] +async fn main() -> Result<(), Box> { + println!("🧪 Testing queue_and_wait functionality directly"); + + // Create supervisor + let mut supervisor = Supervisor::new(); + + // Create a runner config + let config = RunnerConfig::new( + "test_actor".to_string(), + hero_supervisor::runner::RunnerType::OSISRunner, + PathBuf::from("./target/debug/examples/mock_runner"), + "/tmp/test_db".to_string(), + "redis://localhost:6379".to_string(), + ); + + // Add runner + println!("➕ Adding test runner..."); + supervisor.add_runner(config, ProcessManagerType::Simple).await?; + + // Start runner + println!("▶️ Starting test runner..."); + supervisor.start_runner("test_actor").await?; + + // Create a test job + let job = JobBuilder::new() + .caller_id("test_client") + .context_id("direct_test") + .payload("print('Direct queue_and_wait test!');") + .job_type(JobType::OSIS) + .runner_name("test_actor") + .timeout(Duration::from_secs(10)) + .build()?; + + println!("🚀 Testing queue_and_wait directly..."); + println!("📋 Job ID: {}", job.id); + + // Test queue_and_wait directly + match supervisor.queue_and_wait("test_actor", job, 10).await { + Ok(Some(result)) => { + println!("✅ queue_and_wait succeeded!"); + println!("📤 Result: {}", result); + } + Ok(None) => { + println!("⏰ queue_and_wait timed out"); + } + Err(e) => { + println!("❌ queue_and_wait failed: {}", e); + } + } + + // Cleanup + println!("🧹 Cleaning up..."); + supervisor.stop_runner("test_actor", false).await?; + supervisor.remove_runner("test_actor").await?; + + println!("✅ Direct test completed!"); + + Ok(()) +} diff --git a/examples/test_register_runner.rs b/examples/test_register_runner.rs new file mode 100644 index 0000000..b8cf35a --- /dev/null +++ b/examples/test_register_runner.rs @@ -0,0 +1,46 @@ +//! Test program for register_runner functionality with secret authentication + +use hero_supervisor::{SupervisorApp}; +use log::info; +use tokio; + +#[tokio::main] +async fn main() -> Result<(), Box> { + env_logger::init(); + + info!("Starting supervisor with test secrets..."); + + // Create supervisor app with test secrets + let mut app = SupervisorApp::builder() + .redis_url("redis://localhost:6379") + .db_path("/tmp/hero_test_db") + .queue_key("hero:test_queue") + .admin_secret("admin123") + .register_secret("register456") + .user_secret("user789") + .build() + .await?; + + info!("Supervisor configured with secrets:"); + info!(" Admin secrets: {:?}", app.supervisor.admin_secrets()); + info!(" Register secrets: {:?}", app.supervisor.register_secrets()); + info!(" User secrets: {:?}", app.supervisor.user_secrets()); + + // Start OpenRPC server + let supervisor_arc = std::sync::Arc::new(tokio::sync::Mutex::new(app.supervisor.clone())); + + info!("Starting OpenRPC server..."); + hero_supervisor::openrpc::start_openrpc_servers(supervisor_arc).await?; + + info!("Supervisor is running with OpenRPC server on http://127.0.0.1:3030"); + info!("Test secrets configured:"); + info!(" Admin secret: admin123"); + info!(" Register secret: register456"); + info!(" User secret: user789"); + info!("Press Ctrl+C to stop..."); + + // Keep running + loop { + tokio::time::sleep(tokio::time::Duration::from_secs(1)).await; + } +} diff --git a/scripts/build.sh b/scripts/build.sh new file mode 100755 index 0000000..3aa973a --- /dev/null +++ b/scripts/build.sh @@ -0,0 +1,42 @@ +#!/bin/bash +set -e + +SCRIPT_DIR=$(cd "$(dirname "${BASH_SOURCE[0]}")" && pwd) + +# Defaults +OUTDIR="" +RELEASE=0 +CARGO_ARGS="" + +usage() { + cat < Output directory (passed to cargo --dist) + --cargo-args "..." Extra arguments forwarded to cargo build + -h, --help Show this help +EOF +} + +# Parse args +while [[ $# -gt 0 ]]; do + case "$1" in + --release) RELEASE=1; shift;; + --outdir) OUTDIR="$2"; shift 2;; + --cargo-args) CARGO_ARGS="$2"; shift 2;; + -h|--help) usage; exit 0;; + *) echo "❌ Unknown option: $1"; echo; usage; exit 1;; + esac +done + +"$SCRIPT_DIR/install.sh" + +set -x +cmd=(cargo build) +if [[ $RELEASE -eq 1 ]]; then cmd+=(--release); fi +if [[ -n "$OUTDIR" ]]; then cmd+=(--dist "$OUTDIR"); fi +if [[ -n "$CARGO_ARGS" ]]; then cmd+=($CARGO_ARGS); fi +"${cmd[@]}" +set +x \ No newline at end of file diff --git a/scripts/environment.sh b/scripts/environment.sh new file mode 100644 index 0000000..6bcc08f --- /dev/null +++ b/scripts/environment.sh @@ -0,0 +1,76 @@ +#!/usr/bin/env bash +set -euo pipefail + +# This script prepares the dev environment and (when sourced) exports env vars. +# Usage: +# source ./scripts/environment.sh # export env vars to current shell +# ./scripts/environment.sh # runs setup checks; prints sourcing hint + +SCRIPT_DIR=$(cd "$(dirname "${BASH_SOURCE[0]}")" && pwd) +REPO_ROOT=$(cd "$SCRIPT_DIR/.." && pwd) +cd "$REPO_ROOT" + +# --- Helper: print next steps ------------------------------------------------- +print_next_steps() { + echo "" + echo "Next steps:" + echo " 1) Start server (in ../server): cargo run -- --from-env --verbose" + echo " 2) Start portal: ./scripts/start.sh (or ./scripts/start.sh --port 8088)" + echo " 3) Dev (Trunk): set -a; source .env; set +a; trunk serve" +} + +# --- Ensure .env exists (key=value style) ------------------------------------- +if [ ! -f ".env" ]; then + echo "📝 Creating .env file..." + cat > .env << EOF +# Portal Client Configuration +# This file configures the frontend portal app + +## Export-style so that 'source .env' exports to current shell + +# API Key for server authentication (must match one of the API_KEYS in the server .env) +export API_KEY=dev_key_123 + +# Optional: Override server API base URL (defaults to http://127.0.0.1:3001/api) +# Example: API_URL=http://localhost:3001/api +# export API_URL= +EOF + echo "✅ Created .env file with default API key" +else + echo "✅ .env file already exists" +fi + +# --- Install prerequisites ---------------------------------------------------- +if ! command -v trunk >/dev/null 2>&1; then + echo "📦 Installing trunk..." + cargo install trunk +else + echo "✅ trunk is installed" +fi + +if ! rustup target list --installed | grep -q "wasm32-unknown-unknown"; then + echo "🔧 Adding wasm32-unknown-unknown target..." + rustup target add wasm32-unknown-unknown +else + echo "✅ wasm32-unknown-unknown target present" +fi + +# --- Detect if sourced vs executed -------------------------------------------- +# Works for bash and zsh +is_sourced=false +# shellcheck disable=SC2296 +if [ -n "${ZSH_EVAL_CONTEXT:-}" ]; then + case $ZSH_EVAL_CONTEXT in *:file:*) is_sourced=true;; esac +elif [ -n "${BASH_SOURCE:-}" ] && [ "${BASH_SOURCE[0]}" != "$0" ]; then + is_sourced=true +fi + +if $is_sourced; then + echo "🔐 Sourcing .env (export-style) into current shell..." + # shellcheck disable=SC1091 + source .env + echo "✅ Environment exported (API_KEY, optional API_URL)" +else + echo "ℹ️ Run 'source ./scripts/environment.sh' or 'source .env' to export env vars to your shell." + print_next_steps +fi diff --git a/scripts/install.sh b/scripts/install.sh new file mode 100755 index 0000000..a78cf9f --- /dev/null +++ b/scripts/install.sh @@ -0,0 +1,8 @@ +#!/bin/bash +set -e + +SCRIPT_DIR=$(cd "$(dirname "${BASH_SOURCE[0]}")" && pwd) +ROOT_DIR=$(cd "$SCRIPT_DIR/.." && pwd) + +pushd "$ROOT_DIR" +cargo update \ No newline at end of file diff --git a/scripts/release.sh b/scripts/release.sh new file mode 100755 index 0000000..e7daa4d --- /dev/null +++ b/scripts/release.sh @@ -0,0 +1,161 @@ +#!/bin/bash +# release.sh - Build optimized WASM and serve with Caddy + Brotli compression +set -e + +############################################################################### +# Freezone Portal Release Script +# - Builds the WASM app with trunk in release mode +# - Optionally optimizes .wasm with wasm-opt (-Oz, strip) +# - Precompresses assets with gzip and brotli for efficient static serving +# - Generates a manifest (manifest.json) with sizes and SHA-256 checksums +# +# Usage: +# ./release.sh [--outdir dist] [--no-opt] [--compress] [--no-manifest] +# [--trunk-args "--public-url /portal/"] +# +# Notes: +# - Precompression is OFF by default; enable with --compress +# - Only modifies files within the output directory (default: dist) +# - Non-destructive to your source tree +############################################################################### + +set -u + +SCRIPT_DIR=$(cd "$(dirname "${BASH_SOURCE[0]}")" && pwd) +PROJECT_DIR=$(cd "$SCRIPT_DIR/.." && pwd) +BUILD_SCRIPT="$SCRIPT_DIR/build.sh" + +# Defaults +OUTDIR="dist" +DO_OPT=1 +DO_COMPRESS=0 +DO_MANIFEST=1 +TRUNK_ARGS="" + +usage() { + cat < Output directory (default: dist) + --no-opt Skip wasm-opt optimization + --compress Enable gzip/brotli precompression + --no-manifest Skip manifest generation + --trunk-args "..." Extra arguments forwarded to trunk build + -h, --help Show this help + +Examples: + $(basename "$0") --outdir dist --trunk-args "--public-url /" + $(basename "$0") --no-opt --no-compress +EOF +} + +# Parse args +while [[ $# -gt 0 ]]; do + case "$1" in + --outdir) + OUTDIR="$2"; shift 2;; + --no-opt) + DO_OPT=0; shift;; + --compress) + DO_COMPRESS=1; shift;; + --no-manifest) + DO_MANIFEST=0; shift;; + --trunk-args) + TRUNK_ARGS="$2"; shift 2;; + -h|--help) + usage; exit 0;; + *) + echo "❌ Unknown option: $1"; echo; usage; exit 1;; + esac +done + +# Tool checks +if [[ ! -x "$BUILD_SCRIPT" ]]; then + echo "❌ build.sh not found or not executable at: $BUILD_SCRIPT" + echo " Ensure portal/scripts/build.sh exists and is chmod +x." + exit 1 +fi +if ! command -v trunk >/dev/null 2>&1; then + echo "❌ trunk not found. Install with: cargo install trunk"; exit 1; +fi + +HAS_WASM_OPT=0 +if command -v wasm-opt >/dev/null 2>&1; then HAS_WASM_OPT=1; fi +if [[ $DO_OPT -eq 1 && $HAS_WASM_OPT -eq 0 ]]; then + echo "⚠️ wasm-opt not found. Skipping WASM optimization." + DO_OPT=0 +fi + +if [[ $DO_COMPRESS -eq 1 ]]; then + if ! command -v gzip >/dev/null 2>&1; then + echo "⚠️ gzip not found. Skipping gzip compression."; GZIP_OK=0; else GZIP_OK=1; fi + if ! command -v brotli >/dev/null 2>&1; then + echo "⚠️ brotli not found. Skipping brotli compression."; BR_OK=0; else BR_OK=1; fi +else + GZIP_OK=0; BR_OK=0 +fi + +echo "🔧 Building optimized WASM bundle (via build.sh)..." +set -x +"$BUILD_SCRIPT" --release --outdir "$OUTDIR" ${TRUNK_ARGS:+--trunk-args "$TRUNK_ARGS"} +set +x + +DIST_DIR="$PROJECT_DIR/$OUTDIR" +if [[ ! -d "$DIST_DIR" ]]; then + echo "❌ Build failed: output directory not found: $DIST_DIR"; exit 1; +fi + +# Optimize .wasm files +if [[ $DO_OPT -eq 1 && $HAS_WASM_OPT -eq 1 ]]; then + echo "🛠️ Optimizing WASM with wasm-opt (-Oz, strip)..." + while IFS= read -r -d '' wasm; do + echo " • $(basename "$wasm")" + tmp="$wasm.opt" + wasm-opt -Oz --strip-dwarf "$wasm" -o "$tmp" + mv "$tmp" "$wasm" + done < <(find "$DIST_DIR" -type f -name "*.wasm" -print0) +fi + +# Precompress assets +if [[ $DO_COMPRESS -eq 1 ]]; then + echo "🗜️ Precompressing assets (gzip/brotli)..." + while IFS= read -r -d '' f; do + if [[ $GZIP_OK -eq 1 ]]; then + gzip -kf9 "$f" + fi + if [[ $BR_OK -eq 1 ]]; then + brotli -f -q 11 "$f" + fi + done < <(find "$DIST_DIR" -type f \( -name "*.wasm" -o -name "*.js" -o -name "*.css" \) -print0) +fi + +# Manifest with sizes and SHA-256 +if [[ $DO_MANIFEST -eq 1 ]]; then + echo "🧾 Generating manifest.json (sizes, sha256)..." + manifest="$DIST_DIR/manifest.json" + echo "{" > "$manifest" + first=1 + while IFS= read -r -d '' f; do + rel="${f#"$DIST_DIR/"}" + size=$(stat -f%z "$f" 2>/dev/null || stat -c%s "$f") + if command -v shasum >/dev/null 2>&1; then + hash=$(shasum -a 256 "$f" | awk '{print $1}') + else + hash=$(openssl dgst -sha256 -r "$f" | awk '{print $1}') + fi + [[ $first -eq 1 ]] || echo "," >> "$manifest" + first=0 + printf " \"%s\": { \"bytes\": %s, \"sha256\": \"%s\" }" "$rel" "$size" "$hash" >> "$manifest" + done < <(find "$DIST_DIR" -type f ! -name "manifest.json" -print0 | sort -z) + echo "\n}" >> "$manifest" +fi + +echo "📦 Checking bundle sizes ($OUTDIR)..." +if [ -d "$OUTDIR" ]; then + echo "Bundle sizes:" + find "$OUTDIR" -name "*.wasm" -exec ls -lh {} \; | awk '{print " WASM: " $5 " - " $9}' + find "$OUTDIR" -name "*.js" -exec ls -lh {} \; | awk '{print " JS: " $5 " - " $9}' + find "$OUTDIR" -name "*.css" -exec ls -lh {} \; | awk '{print " CSS: " $5 " - " $9}' + echo "" +fi diff --git a/scripts/run.sh b/scripts/run.sh new file mode 100755 index 0000000..0f4c3cf --- /dev/null +++ b/scripts/run.sh @@ -0,0 +1 @@ +cargo run \ No newline at end of file diff --git a/scripts/test.sh b/scripts/test.sh new file mode 100755 index 0000000..293f1de --- /dev/null +++ b/scripts/test.sh @@ -0,0 +1,8 @@ +#!/bin/bash +# serve.sh - Build optimized WASM and serve with Caddy + Brotli compression +set -e + +SCRIPT_DIR=$(cd "$(dirname "${BASH_SOURCE[0]}")" && pwd) + +cargo check +cargo test \ No newline at end of file diff --git a/src/app.rs b/src/app.rs new file mode 100644 index 0000000..94376b1 --- /dev/null +++ b/src/app.rs @@ -0,0 +1,166 @@ +//! # Hero Supervisor Application +//! +//! Simplified supervisor application that wraps a built Supervisor instance. +//! Use SupervisorBuilder to construct the supervisor with all configuration, +//! then pass it to SupervisorApp for runtime management. + +use crate::Supervisor; +use crate::openrpc::start_openrpc_servers; +use log::{info, error, debug}; +use std::sync::Arc; +use tokio::sync::Mutex; + +/// Main supervisor application +pub struct SupervisorApp { + pub supervisor: Supervisor, + pub bind_address: String, + pub port: u16, +} + +impl SupervisorApp { + /// Create a new supervisor application with a built supervisor + pub fn new(supervisor: Supervisor, bind_address: String, port: u16) -> Self { + Self { + supervisor, + bind_address, + port, + } + } + + /// Start the complete supervisor application + /// This method handles the entire application lifecycle: + /// - Starts all configured runners + /// - Launches the OpenRPC server + /// - Sets up graceful shutdown handling + /// - Keeps the application running + pub async fn start(&mut self) -> Result<(), Box> { + info!("Starting Hero Supervisor Application"); + + // Start all configured runners + self.start_all().await?; + + // Start OpenRPC server + self.start_openrpc_server().await?; + + // Set up graceful shutdown + self.setup_graceful_shutdown().await; + + // Keep the application running + info!("Supervisor is running. Press Ctrl+C to shutdown."); + self.run_main_loop().await; + + Ok(()) + } + + /// Start the OpenRPC server + async fn start_openrpc_server(&self) -> Result<(), Box> { + info!("Starting OpenRPC server..."); + + let supervisor_for_openrpc = Arc::new(Mutex::new(self.supervisor.clone())); + let bind_address = self.bind_address.clone(); + let port = self.port; + + // Start the OpenRPC server in a background task + let server_handle = tokio::spawn(async move { + if let Err(e) = start_openrpc_servers(supervisor_for_openrpc, &bind_address, port).await { + error!("OpenRPC server error: {}", e); + } + }); + + // Give the server a moment to start + tokio::time::sleep(tokio::time::Duration::from_millis(100)).await; + info!("OpenRPC server started successfully"); + + // Store the handle for potential cleanup (we could add this to the struct if needed) + std::mem::forget(server_handle); // For now, let it run in background + + Ok(()) + } + + /// Set up graceful shutdown handling + async fn setup_graceful_shutdown(&self) { + tokio::spawn(async move { + tokio::signal::ctrl_c().await.expect("Failed to listen for ctrl+c"); + info!("Received shutdown signal"); + std::process::exit(0); + }); + } + + /// Main application loop + async fn run_main_loop(&self) { + // Keep the main thread alive + loop { + tokio::time::sleep(tokio::time::Duration::from_secs(1)).await; + } + } + + /// Start all configured runners + pub async fn start_all(&mut self) -> Result<(), Box> { + info!("Starting all runners"); + + let results = self.supervisor.start_all().await; + let mut failed_count = 0; + + for (runner_id, result) in results { + match result { + Ok(_) => info!("Runner {} started successfully", runner_id), + Err(e) => { + error!("Failed to start runner {}: {}", runner_id, e); + failed_count += 1; + } + } + } + + if failed_count == 0 { + info!("All runners started successfully"); + } else { + error!("Failed to start {} runners", failed_count); + } + + Ok(()) + } + + /// Stop all configured runners + pub async fn stop_all(&mut self, force: bool) -> Result<(), Box> { + info!("Stopping all runners (force: {})", force); + + let results = self.supervisor.stop_all(force).await; + let mut failed_count = 0; + + for (runner_id, result) in results { + match result { + Ok(_) => info!("Runner {} stopped successfully", runner_id), + Err(e) => { + error!("Failed to stop runner {}: {}", runner_id, e); + failed_count += 1; + } + } + } + + if failed_count == 0 { + info!("All runners stopped successfully"); + } else { + error!("Failed to stop {} runners", failed_count); + } + + Ok(()) + } + + /// Get status of all runners + pub async fn get_status(&self) -> Result, Box> { + debug!("Getting status of all runners"); + + let statuses = self.supervisor.get_all_runner_status().await + .map_err(|e| Box::new(e) as Box)?; + + let status_strings: Vec<(String, String)> = statuses + .into_iter() + .map(|(runner_id, status)| { + let status_str = format!("{:?}", status); + (runner_id, status_str) + }) + .collect(); + + Ok(status_strings) + } +} diff --git a/src/client.rs b/src/client.rs new file mode 100644 index 0000000..02ae334 --- /dev/null +++ b/src/client.rs @@ -0,0 +1,327 @@ +//! Main supervisor implementation for managing multiple actor runners. + +use chrono::Utc; +use redis::AsyncCommands; +use sal_service_manager::{ProcessManager, SimpleProcessManager, TmuxProcessManager}; +use std::collections::HashMap; +use std::path::PathBuf; +use std::sync::Arc; +use tokio::sync::Mutex; + +use crate::{runner::{LogInfo, Runner, RunnerConfig, RunnerError, RunnerResult, RunnerStatus}, JobError, job::JobStatus}; +use crate::{job::Job}; + +#[cfg(feature = "admin")] +use supervisor_admin_server::{AdminSupervisor, RunnerConfigInfo, JobInfo}; + +/// Process manager type for a runner +#[derive(Debug, Clone)] +pub enum ProcessManagerType { + /// Simple process manager for direct process spawning + Simple, + /// Tmux process manager for session-based management + Tmux(String), // session name +} + +/// Main supervisor that manages multiple runners +#[derive(Clone)] +pub struct Client { + redis_client: redis::Client, + /// Namespace for queue keys + namespace: String, +} + +pub struct ClientBuilder { + /// Redis URL for connection + redis_url: String, + /// Namespace for queue keys + namespace: String, +} + +impl ClientBuilder { + /// Create a new supervisor builder + pub fn new() -> Self { + Self { + redis_url: "redis://localhost:6379".to_string(), + namespace: "".to_string(), + } + } + + /// Set the Redis URL + pub fn redis_url>(mut self, url: S) -> Self { + self.redis_url = url.into(); + self + } + + /// Set the namespace for queue keys + pub fn namespace>(mut self, namespace: S) -> Self { + self.namespace = namespace.into(); + self + } + + /// Build the supervisor + pub async fn build(self) -> RunnerResult { + // Create Redis client + let redis_client = redis::Client::open(self.redis_url.as_str()) + .map_err(|e| RunnerError::ConfigError { + reason: format!("Invalid Redis URL: {}", e), + })?; + + Ok(Client { + redis_client, + namespace: self.namespace, + }) + } +} + +impl Default for Client { + fn default() -> Self { + // Note: Default implementation creates an empty supervisor + // Use Supervisor::builder() for proper initialization + Self { + redis_client: redis::Client::open("redis://localhost:6379").unwrap(), + namespace: "".to_string(), + } + } +} + +impl Client { + /// Create a new supervisor builder + pub fn builder() -> ClientBuilder { + ClientBuilder::new() + } + + /// List all job IDs from Redis + pub async fn list_jobs(&self) -> RunnerResult> { + let mut conn = self.redis_client + .get_multiplexed_async_connection() + .await + .map_err(|e| RunnerError::RedisError { source: e })?; + + let keys: Vec = conn.keys(format!("{}:*", &self.jobs_key())).await?; + let job_ids: Vec = keys + .into_iter() + .filter_map(|key| { + if key.starts_with(&format!("{}:", self.jobs_key())) { + key.strip_prefix(&format!("{}:", self.jobs_key())) + .map(|s| s.to_string()) + } else { + None + } + }) + .collect(); + + Ok(job_ids) + } + + fn jobs_key(&self) -> String { + if self.namespace.is_empty() { + format!("job") + } else { + format!("{}:job", self.namespace) + } + } + + pub fn job_key(&self, job_id: &str) -> String { + if self.namespace.is_empty() { + format!("job:{}", job_id) + } else { + format!("{}:job:{}", self.namespace, job_id) + } + } + + pub fn job_reply_key(&self, job_id: &str) -> String { + if self.namespace.is_empty() { + format!("reply:{}", job_id) + } else { + format!("{}:reply:{}", self.namespace, job_id) + } + } + + /// Set job error in Redis + pub async fn set_error(&self, + job_id: &str, + error: &str, + ) -> Result<(), JobError> { + let job_key = self.job_key(job_id); + let now = Utc::now(); + + let mut conn = self.redis_client + .get_multiplexed_async_connection() + .await + .map_err(|e| JobError:: Redis(e))?; + + conn.hset_multiple(&job_key, &[ + ("error", error), + ("status", JobStatus::Error.as_str()), + ("updated_at", &now.to_rfc3339()), + ]).await + .map_err(|e| JobError::Redis(e))?; + + Ok(()) + } + + /// Set job status in Redis + pub async fn set_job_status(&self, + job_id: &str, + status: JobStatus, + ) -> Result<(), JobError> { + let job_key = self.job_key(job_id); + let now = Utc::now(); + + let mut conn = self.redis_client + .get_multiplexed_async_connection() + .await + .map_err(|e| JobError:: Redis(e))?; + + conn.hset_multiple(&job_key, &[ + ("status", status.as_str()), + ("updated_at", &now.to_rfc3339()), + ]).await + .map_err(|e| JobError::Redis(e))?; + Ok(()) + } + + /// Get job status from Redis + pub async fn get_status( + &self, + job_id: &str, + ) -> Result { + let mut conn = self.redis_client + .get_multiplexed_async_connection() + .await + .map_err(|e| JobError:: Redis(e))?; + + let status_str: Option = conn.hget(&self.job_key(job_id), "status").await?; + + match status_str { + Some(s) => JobStatus::from_str(&s).ok_or_else(|| JobError::InvalidStatus(s)), + None => Err(JobError::NotFound(job_id.to_string())), + } + } + + /// Delete job from Redis + pub async fn delete_from_redis( + &self, + job_id: &str, + ) -> Result<(), JobError> { + let mut conn = self.redis_client + .get_multiplexed_async_connection() + .await + .map_err(|e| JobError:: Redis(e))?; + + let job_key = self.job_key(job_id); + let _: () = conn.del(&job_key).await?; + Ok(()) + } + + /// Store this job in Redis + pub async fn store_job_in_redis(&self, job: &Job) -> Result<(), JobError> { + let mut conn = self.redis_client + .get_multiplexed_async_connection() + .await + .map_err(|e| JobError:: Redis(e))?; + + let job_key = self.job_key(&job.id); + + // Serialize the job data + let job_data = serde_json::to_string(job)?; + + // Store job data in Redis hash + let _: () = conn.hset_multiple(&job_key, &[ + ("data", job_data), + ("status", JobStatus::Dispatched.as_str().to_string()), + ("created_at", job.created_at.to_rfc3339()), + ("updated_at", job.updated_at.to_rfc3339()), + ]).await?; + + // Set TTL for the job (24 hours) + let _: () = conn.expire(&job_key, 86400).await?; + + Ok(()) + } + + /// Load a job from Redis by ID + pub async fn load_job_from_redis( + &self, + job_id: &str, + ) -> Result { + let job_key = self.job_key(job_id); + + let mut conn = self.redis_client + .get_multiplexed_async_connection() + .await + .map_err(|e| JobError:: Redis(e))?; + + // Get job data from Redis + let job_data: Option = conn.hget(&job_key, "data").await?; + + match job_data { + Some(data) => { + let job: Job = serde_json::from_str(&data)?; + Ok(job) + } + None => Err(JobError::NotFound(job_id.to_string())), + } + } + + /// Delete a job by ID + pub async fn delete_job(&mut self, job_id: &str) -> RunnerResult<()> { + use redis::AsyncCommands; + + let mut conn = self.redis_client.get_multiplexed_async_connection().await + .map_err(|e| JobError:: Redis(e))?; + + let job_key = self.job_key(job_id); + let deleted_count: i32 = conn.del(&job_key).await + .map_err(|e| RunnerError::QueueError { + actor_id: job_id.to_string(), + reason: format!("Failed to delete job: {}", e), + })?; + + if deleted_count == 0 { + return Err(RunnerError::QueueError { + actor_id: job_id.to_string(), + reason: format!("Job '{}' not found or already deleted", job_id), + }); + } + + Ok(()) + } + + /// Set job result in Redis + pub async fn set_result( + &self, + job_id: &str, + result: &str, + ) -> Result<(), JobError> { + let job_key = self.job_key(&job_id); + let now = Utc::now(); + let mut conn = self.redis_client + .get_multiplexed_async_connection() + .await + .map_err(|e| JobError:: Redis(e))?; + let _: () = conn.hset_multiple(&job_key, &[ + ("result", result), + ("status", JobStatus::Finished.as_str()), + ("updated_at", &now.to_rfc3339()), + ]).await?; + + Ok(()) + } + + /// Get job result from Redis + pub async fn get_result( + &self, + job_id: &str, + ) -> Result, JobError> { + let job_key = self.job_key(job_id); + let mut conn = self.redis_client + .get_multiplexed_async_connection() + .await + .map_err(|e| JobError:: Redis(e))?; + let result: Option = conn.hget(&job_key, "result").await?; + Ok(result) + } + +} \ No newline at end of file diff --git a/src/job.rs b/src/job.rs new file mode 100644 index 0000000..981c0ab --- /dev/null +++ b/src/job.rs @@ -0,0 +1,220 @@ +use chrono::Utc; +use serde::{Deserialize, Serialize}; +use std::collections::HashMap; +use std::time::Duration; +use uuid::Uuid; +use redis::AsyncCommands; +use thiserror::Error; + +/// Job status enumeration +#[derive(Debug, Clone, PartialEq, Serialize, Deserialize)] +pub enum JobStatus { + Dispatched, + WaitingForPrerequisites, + Started, + Error, + Stopping, + Finished, +} + +impl JobStatus { + pub fn as_str(&self) -> &'static str { + match self { + JobStatus::Dispatched => "dispatched", + JobStatus::WaitingForPrerequisites => "waiting_for_prerequisites", + JobStatus::Started => "started", + JobStatus::Error => "error", + JobStatus::Stopping => "stopping", + JobStatus::Finished => "finished", + } + } + + pub fn from_str(s: &str) -> Option { + match s { + "dispatched" => Some(JobStatus::Dispatched), + "waiting_for_prerequisites" => Some(JobStatus::WaitingForPrerequisites), + "started" => Some(JobStatus::Started), + "error" => Some(JobStatus::Error), + "stopping" => Some(JobStatus::Stopping), + "finished" => Some(JobStatus::Finished), + _ => None, + } + } +} + +/// Representation of a script execution request. +/// +/// This structure contains all the information needed to execute a script +/// on a actor service, including the script content, dependencies, and metadata. +#[derive(Debug, Clone, Serialize, Deserialize)] +pub struct Job { + pub id: String, + pub caller_id: String, + pub context_id: String, + pub payload: String, + pub runner_name: String, // name of the runner to execute this job + pub executor: String, // name of the executor the runner will use to execute this job + pub timeout: Duration, + pub env_vars: HashMap, // environment variables for script execution + pub created_at: chrono::DateTime, + pub updated_at: chrono::DateTime, +} + +/// Error types for job operations +#[derive(Error, Debug)] +pub enum JobError { + #[error("Redis error: {0}")] + Redis(#[from] redis::RedisError), + #[error("Serialization error: {0}")] + Serialization(#[from] serde_json::Error), + #[error("Job not found: {0}")] + NotFound(String), + #[error("Invalid job status: {0}")] + InvalidStatus(String), + #[error("Timeout error: {0}")] + Timeout(String), + #[error("Invalid job data: {0}")] + InvalidData(String), +} + +impl Job { + /// Create a new job with the given parameters + pub fn new( + caller_id: String, + context_id: String, + payload: String, + runner_name: String, + executor: String, + ) -> Self { + let now = Utc::now(); + Self { + id: Uuid::new_v4().to_string(), + caller_id, + context_id, + payload, + runner_name, + executor, + timeout: Duration::from_secs(300), // 5 minutes default + env_vars: HashMap::new(), + created_at: now, + updated_at: now, + } + } +} + +/// Builder for constructing job execution requests. +pub struct JobBuilder { + caller_id: String, + context_id: String, + payload: String, + runner_name: String, + executor: String, + timeout: Duration, + env_vars: HashMap, +} + +impl JobBuilder { + pub fn new() -> Self { + Self { + caller_id: "".to_string(), + context_id: "".to_string(), + payload: "".to_string(), + runner_name: "".to_string(), + executor: "".to_string(), + timeout: Duration::from_secs(300), // 5 minutes default + env_vars: HashMap::new(), + } + } + + /// Set the caller ID for this job + pub fn caller_id(mut self, caller_id: &str) -> Self { + self.caller_id = caller_id.to_string(); + self + } + + /// Set the context ID for this job + pub fn context_id(mut self, context_id: &str) -> Self { + self.context_id = context_id.to_string(); + self + } + + /// Set the payload (script content) for this job + pub fn payload(mut self, payload: &str) -> Self { + self.payload = payload.to_string(); + self + } + + /// Set the runner name for this job + pub fn runner_name(mut self, runner_name: &str) -> Self { + self.runner_name = runner_name.to_string(); + self + } + + /// Set the executor for this job + pub fn executor(mut self, executor: &str) -> Self { + self.executor = executor.to_string(); + self + } + + /// Set the timeout for job execution + pub fn timeout(mut self, timeout: Duration) -> Self { + self.timeout = timeout; + self + } + + /// Set a single environment variable + pub fn env_var(mut self, key: &str, value: &str) -> Self { + self.env_vars.insert(key.to_string(), value.to_string()); + self + } + + /// Set multiple environment variables from a HashMap + pub fn env_vars(mut self, env_vars: HashMap) -> Self { + self.env_vars = env_vars; + self + } + + /// Clear all environment variables + pub fn clear_env_vars(mut self) -> Self { + self.env_vars.clear(); + self + } + + /// Build the job + pub fn build(self) -> Result { + if self.caller_id.is_empty() { + return Err(JobError::InvalidData("caller_id is required".to_string())); + } + if self.context_id.is_empty() { + return Err(JobError::InvalidData("context_id is required".to_string())); + } + if self.payload.is_empty() { + return Err(JobError::InvalidData("payload is required".to_string())); + } + if self.runner_name.is_empty() { + return Err(JobError::InvalidData("runner_name is required".to_string())); + } + if self.executor.is_empty() { + return Err(JobError::InvalidData("executor is required".to_string())); + } + + let mut job = Job::new( + self.caller_id, + self.context_id, + self.payload, + self.runner_name, + self.executor, + ); + + job.timeout = self.timeout; + job.env_vars = self.env_vars; + + Ok(job) + } +} + +impl Default for JobBuilder { + fn default() -> Self { + Self::new() + } +} diff --git a/src/lib.rs b/src/lib.rs new file mode 100644 index 0000000..ce1ca71 --- /dev/null +++ b/src/lib.rs @@ -0,0 +1,21 @@ +//! Hero Supervisor - Actor management for the Hero ecosystem. +//! +//! See README.md for detailed documentation and usage examples. + +pub mod runner; +pub mod supervisor; +pub mod job; +pub mod client; +pub mod app; + +// OpenRPC server module +pub mod openrpc; + +// Re-export main types for convenience +pub use runner::{ + LogInfo, Runner, RunnerConfig, RunnerResult, RunnerStatus, +}; +pub use sal_service_manager::{ProcessManager, SimpleProcessManager, TmuxProcessManager}; +pub use supervisor::{Supervisor, SupervisorBuilder, ProcessManagerType}; +pub use job::{Job, JobBuilder, JobStatus, JobError}; +pub use app::SupervisorApp; diff --git a/src/openrpc.rs b/src/openrpc.rs new file mode 100644 index 0000000..d35fd62 --- /dev/null +++ b/src/openrpc.rs @@ -0,0 +1,829 @@ +//! OpenRPC server implementation. + +use jsonrpsee::{ + core::{RpcResult, async_trait}, + proc_macros::rpc, + server::{Server, ServerHandle}, + types::{ErrorObject, ErrorObjectOwned}, +}; +use tower_http::cors::{CorsLayer, Any}; + +use anyhow; +use log::{debug, info, error}; + +use crate::supervisor::Supervisor; +use crate::runner::{Runner, RunnerError}; +use crate::job::Job; +use crate::ProcessManagerType; +use sal_service_manager::{ProcessStatus, LogInfo}; +use serde::{Deserialize, Serialize}; + +use std::net::SocketAddr; +use std::path::PathBuf; +use std::sync::Arc; +use std::fs; +use tokio::sync::Mutex; + +/// Load OpenRPC specification from docs/openrpc.json +fn load_openrpc_spec() -> Result> { + // Try to find the openrpc.json file relative to the current working directory + let possible_paths = [ + "docs/openrpc.json", + "../docs/openrpc.json", + "../../docs/openrpc.json", + "./supervisor/docs/openrpc.json", + ]; + + for path in &possible_paths { + if let Ok(content) = fs::read_to_string(path) { + match serde_json::from_str(&content) { + Ok(spec) => { + debug!("Loaded OpenRPC specification from: {}", path); + return Ok(spec); + } + Err(e) => { + error!("Failed to parse OpenRPC JSON from {}: {}", path, e); + } + } + } + } + + Err("Could not find or parse docs/openrpc.json".into()) +} + +/// Helper function to convert RunnerError to RPC error +fn runner_error_to_rpc_error(err: RunnerError) -> ErrorObject<'static> { + ErrorObject::owned( + -32603, // Internal error code + format!("Supervisor error: {}", err), + None::<()>, + ) +} + +/// Helper function to create invalid params error +fn invalid_params_error(msg: &str) -> ErrorObject<'static> { + ErrorObject::owned( + -32602, // Invalid params error code + format!("Invalid parameters: {}", msg), + None::<()>, + ) +} + +/// Request parameters for registering a new runner +#[derive(Debug, Deserialize, Serialize)] +pub struct RegisterRunnerParams { + pub secret: String, + pub name: String, + pub queue: String, +} + +/// Request parameters for running a job +#[derive(Debug, Deserialize, Serialize)] +pub struct RunJobParams { + pub secret: String, + pub job: Job, +} + +/// Request parameters for adding a new runner +#[derive(Debug, Deserialize, Serialize)] +pub struct AddRunnerParams { + pub actor_id: String, + pub binary_path: String, + pub db_path: String, + pub redis_url: String, + pub process_manager_type: String, // "simple" or "tmux" + pub tmux_session_name: Option, // required if process_manager_type is "tmux" +} + +/// Request parameters for queuing a job +#[derive(Debug, Deserialize, Serialize)] +pub struct QueueJobParams { + pub runner_name: String, + pub job: Job, +} + +/// Request parameters for queue and wait operation +#[derive(Debug, Deserialize, Serialize)] +pub struct QueueAndWaitParams { + pub runner_name: String, + pub job: Job, + pub timeout_secs: u64, +} + +/// Request parameters for getting runner logs +#[derive(Debug, Deserialize, Serialize)] +pub struct GetLogsParams { + pub actor_id: String, + pub lines: Option, + pub follow: bool, +} + +/// Request parameters for adding secrets +#[derive(Debug, Deserialize, Serialize)] +pub struct AddSecretParams { + pub admin_secret: String, + pub secret_type: String, // "admin", "user", or "register" + pub secret_value: String, +} + +/// Request parameters for removing secrets +#[derive(Debug, Deserialize, Serialize)] +pub struct RemoveSecretParams { + pub admin_secret: String, + pub secret_type: String, // "admin", "user", or "register" + pub secret_value: String, +} + +/// Request parameters for listing secrets +#[derive(Debug, Deserialize, Serialize)] +pub struct ListSecretsParams { + pub admin_secret: String, +} + +/// Serializable wrapper for ProcessStatus +#[derive(Debug, Serialize, Clone)] +pub enum ProcessStatusWrapper { + Running, + Stopped, + Starting, + Stopping, + Error(String), +} + +impl From for ProcessStatusWrapper { + fn from(status: ProcessStatus) -> Self { + match status { + ProcessStatus::Running => ProcessStatusWrapper::Running, + ProcessStatus::Stopped => ProcessStatusWrapper::Stopped, + ProcessStatus::Starting => ProcessStatusWrapper::Starting, + ProcessStatus::Stopping => ProcessStatusWrapper::Stopping, + ProcessStatus::Error(msg) => ProcessStatusWrapper::Error(msg), + } + } +} + +// Note: RunnerStatus is just an alias for ProcessStatus, so we don't need a separate impl + +/// Serializable wrapper for Runner +#[derive(Debug, Serialize, Clone)] +pub struct RunnerWrapper { + pub id: String, + pub name: String, + pub command: String, + pub redis_url: String, +} + +impl From<&Runner> for RunnerWrapper { + fn from(runner: &Runner) -> Self { + RunnerWrapper { + id: runner.id.clone(), + name: runner.name.clone(), + command: runner.command.to_string_lossy().to_string(), + redis_url: runner.redis_url.clone(), + } + } +} + +/// Serializable wrapper for LogInfo +#[derive(Debug, Serialize, Clone)] +pub struct LogInfoWrapper { + pub timestamp: String, + pub level: String, + pub message: String, +} + +impl From for LogInfoWrapper { + fn from(log: LogInfo) -> Self { + LogInfoWrapper { + timestamp: log.timestamp, + level: log.level, + message: log.message, + } + } +} + +impl From for LogInfoWrapper { + fn from(log: crate::runner::LogInfo) -> Self { + LogInfoWrapper { + timestamp: log.timestamp, + level: log.level, + message: log.message, + } + } +} + +/// Response for runner status queries +#[derive(Debug, Serialize, Clone)] +pub struct RunnerStatusResponse { + pub actor_id: String, + pub status: ProcessStatusWrapper, +} + +/// Response for supervisor info +#[derive(Debug, Serialize, Clone)] +pub struct SupervisorInfoResponse { + pub server_url: String, + pub admin_secrets_count: usize, + pub user_secrets_count: usize, + pub register_secrets_count: usize, + pub runners_count: usize, +} + +/// OpenRPC trait defining all supervisor methods +#[rpc(server)] +pub trait SupervisorRpc { + /// Register a new runner with secret-based authentication + #[method(name = "register_runner")] + async fn register_runner(&self, params: RegisterRunnerParams) -> RpcResult; + + /// Create a job (fire-and-forget, non-blocking) + #[method(name = "create_job")] + async fn create_job(&self, params: RunJobParams) -> RpcResult; + + /// Run a job on the appropriate runner (blocking, returns result) + #[method(name = "run_job")] + async fn run_job(&self, params: RunJobParams) -> RpcResult>; + + /// Remove a runner from the supervisor + #[method(name = "remove_runner")] + async fn remove_runner(&self, actor_id: String) -> RpcResult<()>; + + /// List all runner IDs + #[method(name = "list_runners")] + async fn list_runners(&self) -> RpcResult>; + + /// Start a specific runner + #[method(name = "start_runner")] + async fn start_runner(&self, actor_id: String) -> RpcResult<()>; + + /// Stop a specific runner + #[method(name = "stop_runner")] + async fn stop_runner(&self, actor_id: String, force: bool) -> RpcResult<()>; + + /// Get a specific runner by ID + #[method(name = "get_runner")] + async fn get_runner(&self, actor_id: String) -> RpcResult; + + /// Get the status of a specific runner + #[method(name = "get_runner_status")] + async fn get_runner_status(&self, actor_id: String) -> RpcResult; + + /// Get logs for a specific runner + #[method(name = "get_runner_logs")] + async fn get_runner_logs(&self, params: GetLogsParams) -> RpcResult>; + + /// Queue a job to a specific runner + #[method(name = "queue_job_to_runner")] + async fn queue_job_to_runner(&self, params: QueueJobParams) -> RpcResult<()>; + + /// List all job IDs from Redis + #[method(name = "list_jobs")] + async fn list_jobs(&self) -> RpcResult>; + + /// Get a job by job ID + #[method(name = "get_job")] + async fn get_job(&self, job_id: String) -> RpcResult; + + /// Ping a runner (dispatch a ping job) + #[method(name = "ping_runner")] + async fn ping_runner(&self, runner_id: String) -> RpcResult; + + /// Stop a job + #[method(name = "stop_job")] + async fn stop_job(&self, job_id: String) -> RpcResult<()>; + + /// Delete a job + #[method(name = "delete_job")] + async fn delete_job(&self, job_id: String) -> RpcResult<()>; + + /// Queue a job to a specific runner and wait for the result + #[method(name = "queue_and_wait")] + async fn queue_and_wait(&self, params: QueueAndWaitParams) -> RpcResult>; + + /// Get status of all runners + #[method(name = "get_all_runner_status")] + async fn get_all_runner_status(&self) -> RpcResult>; + + /// Start all runners + #[method(name = "start_all")] + async fn start_all(&self) -> RpcResult>; + + /// Stop all runners + #[method(name = "stop_all")] + async fn stop_all(&self, force: bool) -> RpcResult>; + + /// Get status of all runners (alternative format) + #[method(name = "get_all_status")] + async fn get_all_status(&self) -> RpcResult>; + + /// Add a secret to the supervisor (admin, user, or register) + #[method(name = "add_secret")] + async fn add_secret(&self, params: AddSecretParams) -> RpcResult<()>; + + /// Remove a secret from the supervisor + #[method(name = "remove_secret")] + async fn remove_secret(&self, params: RemoveSecretParams) -> RpcResult<()>; + + /// List all secrets (returns counts only for security) + #[method(name = "list_secrets")] + async fn list_secrets(&self, params: ListSecretsParams) -> RpcResult; + + /// List admin secrets (returns actual secret values) + #[method(name = "list_admin_secrets")] + async fn list_admin_secrets(&self, admin_secret: String) -> RpcResult>; + + /// List user secrets (returns actual secret values) + #[method(name = "list_user_secrets")] + async fn list_user_secrets(&self, admin_secret: String) -> RpcResult>; + + /// List register secrets (returns actual secret values) + #[method(name = "list_register_secrets")] + async fn list_register_secrets(&self, admin_secret: String) -> RpcResult>; + + /// Get supervisor information and statistics + #[method(name = "get_supervisor_info")] + async fn get_supervisor_info(&self, admin_secret: String) -> RpcResult; + + /// OpenRPC discovery method - returns the OpenRPC document describing this API + #[method(name = "rpc.discover")] + async fn rpc_discover(&self) -> RpcResult; +} + +/// Helper function to parse process manager type from string +fn parse_process_manager_type(pm_type: &str, session_name: Option) -> Result> { + match pm_type.to_lowercase().as_str() { + "simple" => Ok(ProcessManagerType::Simple), + "tmux" => { + let session = session_name.unwrap_or_else(|| "default_session".to_string()); + Ok(ProcessManagerType::Tmux(session)) + }, + _ => Err(invalid_params_error(&format!( + "Invalid process manager type: {}. Must be 'simple' or 'tmux'", + pm_type + ))), + } +} + +/// Direct RPC implementation on Arc> +/// This eliminates the need for a wrapper struct +#[async_trait] +impl SupervisorRpcServer for Arc> { + async fn register_runner(&self, params: RegisterRunnerParams) -> RpcResult { + debug!("OpenRPC request: register_runner with params: {:?}", params); + + let mut supervisor = self.lock().await; + supervisor + .register_runner(¶ms.secret, ¶ms.name, ¶ms.queue) + .await + .map_err(runner_error_to_rpc_error)?; + + // Return the runner name that was registered + Ok(params.name) + } + + async fn create_job(&self, params: RunJobParams) -> RpcResult { + debug!("OpenRPC request: create_job with params: {:?}", params); + + let mut supervisor = self.lock().await; + let job_id = supervisor + .create_job(¶ms.secret, params.job) + .await + .map_err(runner_error_to_rpc_error)?; + + Ok(job_id) + } + + async fn run_job(&self, params: RunJobParams) -> RpcResult> { + debug!("OpenRPC request: run_job with params: {:?}", params); + + let mut supervisor = self.lock().await; + supervisor + .run_job(¶ms.secret, params.job) + .await + .map_err(runner_error_to_rpc_error) + } + + async fn remove_runner(&self, actor_id: String) -> RpcResult<()> { + debug!("OpenRPC request: remove_runner with actor_id: {}", actor_id); + let mut supervisor = self.lock().await; + supervisor + .remove_runner(&actor_id) + .await + .map_err(runner_error_to_rpc_error) + } + + async fn list_runners(&self) -> RpcResult> { + debug!("OpenRPC request: list_runners"); + let supervisor = self.lock().await; + Ok(supervisor.list_runners().into_iter().map(|s| s.to_string()).collect()) + } + + async fn start_runner(&self, actor_id: String) -> RpcResult<()> { + debug!("OpenRPC request: start_runner with actor_id: {}", actor_id); + let mut supervisor = self.lock().await; + supervisor + .start_runner(&actor_id) + .await + .map_err(runner_error_to_rpc_error) + } + + async fn stop_runner(&self, actor_id: String, force: bool) -> RpcResult<()> { + debug!("OpenRPC request: stop_runner with actor_id: {}, force: {}", actor_id, force); + let mut supervisor = self.lock().await; + supervisor + .stop_runner(&actor_id, force) + .await + .map_err(runner_error_to_rpc_error) + } + + async fn get_runner(&self, actor_id: String) -> RpcResult { + debug!("OpenRPC request: get_runner with actor_id: {}", actor_id); + let supervisor = self.lock().await; + match supervisor.get_runner(&actor_id) { + Some(runner) => Ok(RunnerWrapper::from(runner)), + None => Err(ErrorObjectOwned::owned(-32000, format!("Runner not found: {}", actor_id), None::<()>)), + } + } + + async fn get_runner_status(&self, actor_id: String) -> RpcResult { + debug!("OpenRPC request: get_runner_status with actor_id: {}", actor_id); + let supervisor = self.lock().await; + let status = supervisor + .get_runner_status(&actor_id) + .await + .map_err(runner_error_to_rpc_error)?; + Ok(status.into()) + } + + async fn get_runner_logs(&self, params: GetLogsParams) -> RpcResult> { + debug!("OpenRPC request: get_runner_logs with params: {:?}", params); + let supervisor = self.lock().await; + let logs = supervisor + .get_runner_logs(¶ms.actor_id, params.lines, params.follow) + .await + .map_err(runner_error_to_rpc_error)?; + Ok(logs.into_iter().map(LogInfoWrapper::from).collect()) + } + + async fn queue_job_to_runner(&self, params: QueueJobParams) -> RpcResult<()> { + debug!("OpenRPC request: queue_job_to_runner with params: {:?}", params); + let mut supervisor = self.lock().await; + supervisor + .queue_job_to_runner(¶ms.runner_name, params.job) + .await + .map_err(runner_error_to_rpc_error) + } + + async fn list_jobs(&self) -> RpcResult> { + debug!("OpenRPC request: list_jobs"); + let supervisor = self.lock().await; + supervisor + .list_jobs() + .await + .map_err(runner_error_to_rpc_error) + } + + async fn get_job(&self, job_id: String) -> RpcResult { + debug!("OpenRPC request: get_job with job_id: {}", job_id); + let supervisor = self.lock().await; + supervisor + .get_job(&job_id) + .await + .map_err(runner_error_to_rpc_error) + } + + async fn ping_runner(&self, runner_id: String) -> RpcResult { + debug!("OpenRPC request: ping_runner with runner_id: {}", runner_id); + let mut supervisor = self.lock().await; + supervisor + .ping_runner(&runner_id) + .await + .map_err(runner_error_to_rpc_error) + } + + async fn stop_job(&self, job_id: String) -> RpcResult<()> { + debug!("OpenRPC request: stop_job with job_id: {}", job_id); + let mut supervisor = self.lock().await; + supervisor + .stop_job(&job_id) + .await + .map_err(runner_error_to_rpc_error) + } + + async fn delete_job(&self, job_id: String) -> RpcResult<()> { + debug!("OpenRPC request: delete_job with job_id: {}", job_id); + let mut supervisor = self.lock().await; + supervisor + .delete_job(&job_id) + .await + .map_err(runner_error_to_rpc_error) + } + + async fn queue_and_wait(&self, params: QueueAndWaitParams) -> RpcResult> { + debug!("OpenRPC request: queue_and_wait with params: {:?}", params); + let mut supervisor = self.lock().await; + supervisor + .queue_and_wait(¶ms.runner_name, params.job, params.timeout_secs) + .await + .map_err(runner_error_to_rpc_error) + } + + async fn get_all_runner_status(&self) -> RpcResult> { + debug!("OpenRPC request: get_all_runner_status"); + let supervisor = self.lock().await; + let statuses = supervisor.get_all_runner_status().await + .map_err(runner_error_to_rpc_error)?; + Ok(statuses + .into_iter() + .map(|(actor_id, status)| RunnerStatusResponse { + actor_id, + status: ProcessStatusWrapper::from(status), + }) + .collect()) + } + + async fn start_all(&self) -> RpcResult> { + debug!("OpenRPC request: start_all"); + let mut supervisor = self.lock().await; + let results = supervisor.start_all().await; + Ok(results + .into_iter() + .map(|(actor_id, result)| { + let status = match result { + Ok(_) => "Success".to_string(), + Err(e) => format!("Error: {}", e), + }; + (actor_id, status) + }) + .collect()) + } + + async fn stop_all(&self, force: bool) -> RpcResult> { + debug!("OpenRPC request: stop_all with force: {}", force); + let mut supervisor = self.lock().await; + let results = supervisor.stop_all(force).await; + Ok(results + .into_iter() + .map(|(actor_id, result)| { + let status = match result { + Ok(_) => "Success".to_string(), + Err(e) => format!("Error: {}", e), + }; + (actor_id, status) + }) + .collect()) + } + + async fn get_all_status(&self) -> RpcResult> { + debug!("OpenRPC request: get_all_status"); + let supervisor = self.lock().await; + let statuses = supervisor.get_all_runner_status().await + .map_err(runner_error_to_rpc_error)?; + Ok(statuses + .into_iter() + .map(|(actor_id, status)| { + let status_str = format!("{:?}", status); + (actor_id, status_str) + }) + .collect()) + } + + async fn add_secret(&self, params: AddSecretParams) -> RpcResult<()> { + debug!("OpenRPC request: add_secret, type: {}", params.secret_type); + let mut supervisor = self.lock().await; + + // Verify admin secret + if !supervisor.has_admin_secret(¶ms.admin_secret) { + return Err(ErrorObject::owned(-32602, "Invalid admin secret", None::<()>)); + } + + match params.secret_type.as_str() { + "admin" => { + supervisor.add_admin_secret(params.secret_value); + } + "user" => { + supervisor.add_user_secret(params.secret_value); + } + "register" => { + supervisor.add_register_secret(params.secret_value); + } + _ => { + return Err(ErrorObject::owned(-32602, "Invalid secret type. Must be 'admin', 'user', or 'register'", None::<()>)); + } + } + + Ok(()) + } + + async fn remove_secret(&self, params: RemoveSecretParams) -> RpcResult<()> { + debug!("OpenRPC request: remove_secret, type: {}", params.secret_type); + let mut supervisor = self.lock().await; + + // Verify admin secret + if !supervisor.has_admin_secret(¶ms.admin_secret) { + return Err(ErrorObject::owned(-32602, "Invalid admin secret", None::<()>)); + } + + match params.secret_type.as_str() { + "admin" => { + supervisor.remove_admin_secret(¶ms.secret_value); + } + "user" => { + supervisor.remove_user_secret(¶ms.secret_value); + } + "register" => { + supervisor.remove_register_secret(¶ms.secret_value); + } + _ => { + return Err(ErrorObject::owned(-32602, "Invalid secret type. Must be 'admin', 'user', or 'register'", None::<()>)); + } + } + + Ok(()) + } + + async fn list_secrets(&self, params: ListSecretsParams) -> RpcResult { + debug!("OpenRPC request: list_secrets"); + let supervisor = self.lock().await; + + // Verify admin secret + if !supervisor.has_admin_secret(¶ms.admin_secret) { + return Err(ErrorObject::owned(-32602, "Invalid admin secret", None::<()>)); + } + + Ok(SupervisorInfoResponse { + server_url: "http://127.0.0.1:3030".to_string(), + admin_secrets_count: supervisor.admin_secrets_count(), + user_secrets_count: supervisor.user_secrets_count(), + register_secrets_count: supervisor.register_secrets_count(), + runners_count: supervisor.runners_count(), + }) + } + + async fn list_admin_secrets(&self, admin_secret: String) -> RpcResult> { + debug!("OpenRPC request: list_admin_secrets"); + let supervisor = self.lock().await; + + // Verify admin secret + if !supervisor.has_admin_secret(&admin_secret) { + return Err(ErrorObject::owned(-32602, "Invalid admin secret", None::<()>)); + } + + Ok(supervisor.get_admin_secrets()) + } + + async fn list_user_secrets(&self, admin_secret: String) -> RpcResult> { + debug!("OpenRPC request: list_user_secrets"); + let supervisor = self.lock().await; + + // Verify admin secret + if !supervisor.has_admin_secret(&admin_secret) { + return Err(ErrorObject::owned(-32602, "Invalid admin secret", None::<()>)); + } + + Ok(supervisor.get_user_secrets()) + } + + async fn list_register_secrets(&self, admin_secret: String) -> RpcResult> { + debug!("OpenRPC request: list_register_secrets"); + let supervisor = self.lock().await; + + // Verify admin secret + if !supervisor.has_admin_secret(&admin_secret) { + return Err(ErrorObject::owned(-32602, "Invalid admin secret", None::<()>)); + } + + Ok(supervisor.get_register_secrets()) + } + + async fn get_supervisor_info(&self, admin_secret: String) -> RpcResult { + debug!("OpenRPC request: get_supervisor_info"); + let supervisor = self.lock().await; + + // Verify admin secret + if !supervisor.has_admin_secret(&admin_secret) { + return Err(ErrorObject::owned(-32602, "Invalid admin secret", None::<()>)); + } + + Ok(SupervisorInfoResponse { + server_url: "http://127.0.0.1:3030".to_string(), + admin_secrets_count: supervisor.admin_secrets_count(), + user_secrets_count: supervisor.user_secrets_count(), + register_secrets_count: supervisor.register_secrets_count(), + runners_count: supervisor.runners_count(), + }) + } + + async fn rpc_discover(&self) -> RpcResult { + debug!("OpenRPC request: rpc.discover"); + + // Read OpenRPC specification from docs/openrpc.json + match load_openrpc_spec() { + Ok(spec) => Ok(spec), + Err(e) => { + error!("Failed to load OpenRPC specification: {}", e); + // Fallback to a minimal spec if file loading fails + Ok(serde_json::json!({ + "openrpc": "1.3.2", + "info": { + "title": "Hero Supervisor OpenRPC API", + "version": "1.0.0", + "description": "OpenRPC API for managing Hero Supervisor runners and jobs" + }, + "methods": [], + "error": "Failed to load full specification" + })) + } + } + } +} + +/// Start the OpenRPC server with a default supervisor +pub async fn start_server(addr: SocketAddr) -> anyhow::Result { + let supervisor = Arc::new(Mutex::new(Supervisor::default())); + start_server_with_supervisor(addr, supervisor).await +} + +/// Start the OpenRPC server with an existing supervisor instance +pub async fn start_server_with_supervisor( + addr: SocketAddr, + supervisor: Arc>, +) -> anyhow::Result { + let server = Server::builder().build(addr).await?; + let handle = server.start(supervisor.into_rpc()); + Ok(handle) +} + +/// Start HTTP OpenRPC server (Unix socket support would require additional dependencies) +pub async fn start_http_openrpc_server( + supervisor: Arc>, + bind_address: &str, + port: u16, +) -> anyhow::Result { + let http_addr: SocketAddr = format!("{}:{}", bind_address, port).parse()?; + + // Configure CORS to allow requests from the admin UI + let cors = CorsLayer::new() + .allow_origin(Any) + .allow_headers(Any) + .allow_methods(Any); + + // Start HTTP server with CORS + let http_server = Server::builder() + .set_http_middleware(tower::ServiceBuilder::new().layer(cors)) + .build(http_addr) + .await?; + let http_handle = http_server.start(supervisor.into_rpc()); + + info!("OpenRPC HTTP server running at http://{} with CORS enabled", http_addr); + + Ok(http_handle) +} + +/// Simplified server startup function for supervisor binary +pub async fn start_openrpc_servers( + supervisor: Arc>, + bind_address: &str, + port: u16, +) -> Result<(), Box> { + let bind_address = bind_address.to_string(); + tokio::spawn(async move { + match start_http_openrpc_server(supervisor, &bind_address, port).await { + Ok(http_handle) => { + info!("OpenRPC server started successfully"); + // Keep the server running + http_handle.stopped().await; + error!("OpenRPC server stopped unexpectedly"); + } + Err(e) => { + error!("Failed to start OpenRPC server: {}", e); + } + } + }); + + // Give the server a moment to start up + tokio::time::sleep(tokio::time::Duration::from_millis(500)).await; + + Ok(()) +} + +#[cfg(test)] +mod tests { + use super::*; + + #[test] + fn test_supervisor_rpc_creation() { + let _rpc = SupervisorRpcImpl::new(); + // Just test that we can create the RPC implementation + } + + #[cfg(feature = "openrpc")] + #[test] + fn test_process_manager_type_parsing() { + assert!(SupervisorRpcImpl::parse_process_manager_type("simple").is_ok()); + assert!(SupervisorRpcImpl::parse_process_manager_type("tmux").is_ok()); + assert!(SupervisorRpcImpl::parse_process_manager_type("Simple").is_ok()); + assert!(SupervisorRpcImpl::parse_process_manager_type("TMUX").is_ok()); + assert!(SupervisorRpcImpl::parse_process_manager_type("invalid").is_err()); + } +} diff --git a/src/runner.rs b/src/runner.rs new file mode 100644 index 0000000..5540795 --- /dev/null +++ b/src/runner.rs @@ -0,0 +1,234 @@ +//! Runner implementation for actor process management. + +use crate::job::{Job}; +use log::{debug, info}; +use redis::AsyncCommands; +use sal_service_manager::{ProcessManager, ProcessManagerError as ServiceProcessManagerError, ProcessStatus, ProcessConfig}; +use std::path::PathBuf; +use std::sync::Arc; +use tokio::sync::Mutex; + +/// Represents the current status of an actor/runner (alias for ProcessStatus) +pub type RunnerStatus = ProcessStatus; + +/// Log information structure +#[derive(Debug, Clone)] +pub struct LogInfo { + pub timestamp: String, + pub level: String, + pub message: String, +} + +/// Runner configuration and state (merged from RunnerConfig) +#[derive(Debug, Clone)] +pub struct Runner { + /// Unique identifier for the runner + pub id: String, + pub name: String, + pub namespace: String, + /// Path to the actor binary + pub command: PathBuf, // Command to run runner by, used only if supervisor is used to run runners + /// Redis URL for job queue + pub redis_url: String, +} + +impl Runner { + /// Create a new runner from configuration + pub fn from_config(config: RunnerConfig) -> Self { + Self { + id: config.id, + name: config.name, + namespace: config.namespace, + command: config.command, + redis_url: config.redis_url, + } + } + + /// Create a new runner with the given parameters + pub fn new( + id: String, + name: String, + namespace: String, + command: PathBuf, + redis_url: String, + ) -> Self { + Self { + id, + name, + namespace, + command, + redis_url, + } + } + + /// Get the queue key for this runner with the given namespace + pub fn get_queue(&self) -> String { + if self.namespace == "" { + format!("runner:{}", self.name) + } else { + format!("{}:runner:{}", self.namespace, self.name) + } + } +} + +/// Result type for runner operations +pub type RunnerResult = Result; + +/// Errors that can occur during runner operations +#[derive(Debug, thiserror::Error)] +pub enum RunnerError { + #[error("Actor '{actor_id}' not found")] + ActorNotFound { actor_id: String }, + + #[error("Actor '{actor_id}' is already running")] + ActorAlreadyRunning { actor_id: String }, + + #[error("Actor '{actor_id}' is not running")] + ActorNotRunning { actor_id: String }, + + #[error("Failed to start actor '{actor_id}': {reason}")] + StartupFailed { actor_id: String, reason: String }, + + #[error("Failed to stop actor '{actor_id}': {reason}")] + StopFailed { actor_id: String, reason: String }, + + #[error("Timeout waiting for actor '{actor_id}' to start")] + StartupTimeout { actor_id: String }, + + #[error("Job queue error for actor '{actor_id}': {reason}")] + QueueError { actor_id: String, reason: String }, + + #[error("Process manager error: {source}")] + ProcessManagerError { + #[from] + source: ServiceProcessManagerError, + }, + + #[error("Configuration error: {reason}")] + ConfigError { reason: String }, + + #[error("Invalid secret: {0}")] + InvalidSecret(String), + + #[error("IO error: {source}")] + IoError { + #[from] + source: std::io::Error, + }, + + #[error("Redis error: {source}")] + RedisError { + #[from] + source: redis::RedisError, + }, + + #[error("Job error: {source}")] + JobError { + #[from] + source: crate::JobError, + }, +} + +/// Convert Runner to ProcessConfig +pub fn runner_to_process_config(config: &Runner) -> ProcessConfig { + ProcessConfig::new(config.id.clone(), config.command.clone()) + .with_arg("--id".to_string()) + .with_arg(config.id.clone()) + .with_arg("--redis-url".to_string()) + .with_arg(config.redis_url.clone()) +} + +// Type alias for backward compatibility +pub type RunnerConfig = Runner; + +#[cfg(test)] +mod tests { + use super::*; + use sal_service_manager::{ProcessManagerError, SimpleProcessManager}; + use std::collections::HashMap; + + #[derive(Debug)] + struct MockProcessManager { + processes: HashMap, + } + + impl MockProcessManager { + fn new() -> Self { + Self { + processes: HashMap::new(), + } + } + } + + #[async_trait::async_trait] + impl ProcessManager for MockProcessManager { + async fn start_process(&mut self, config: &ProcessConfig) -> Result<(), ProcessManagerError> { + self.processes.insert(config.id.clone(), ProcessStatus::Running); + Ok(()) + } + + async fn stop_process(&mut self, process_id: &str, _force: bool) -> Result<(), ProcessManagerError> { + self.processes.insert(process_id.to_string(), ProcessStatus::Stopped); + Ok(()) + } + + async fn process_status(&self, process_id: &str) -> Result { + Ok(self.processes.get(process_id).cloned().unwrap_or(ProcessStatus::Stopped)) + } + + async fn process_logs(&self, _process_id: &str, _lines: Option, _follow: bool) -> Result, ProcessManagerError> { + Ok(vec![]) + } + + async fn health_check(&self) -> Result<(), ProcessManagerError> { + Ok(()) + } + + async fn list_processes(&self) -> Result, ProcessManagerError> { + Ok(self.processes.keys().cloned().collect()) + } + } + + #[test] + fn test_runner_creation() { + let runner = Runner::new( + "test_actor".to_string(), + "test_runner".to_string(), + "".to_string(), + PathBuf::from("/path/to/binary"), + "redis://localhost:6379".to_string(), + ); + + assert_eq!(runner.id, "test_actor"); + assert_eq!(runner.name, "test_runner"); + assert_eq!(runner.command, PathBuf::from("/path/to/binary")); + assert_eq!(runner.redis_url, "redis://localhost:6379"); + } + + #[test] + fn test_runner_get_queue() { + let runner = Runner::new( + "test_actor".to_string(), + "test_runner".to_string(), + "".to_string(), + PathBuf::from("/path/to/binary"), + "redis://localhost:6379".to_string(), + ); + + let queue_key = runner.get_queue(); + assert_eq!(queue_key, "runner:test_runner"); + } + + #[test] + fn test_runner_error_types() { + let error = RunnerError::ActorNotFound { + actor_id: "test".to_string(), + }; + assert!(error.to_string().contains("test")); + + let error = RunnerError::ActorAlreadyRunning { + actor_id: "test".to_string(), + }; + assert!(error.to_string().contains("already running")); + } +} diff --git a/src/supervisor.rs b/src/supervisor.rs new file mode 100644 index 0000000..baf73b7 --- /dev/null +++ b/src/supervisor.rs @@ -0,0 +1,777 @@ +//! Main supervisor implementation for managing multiple actor runners. + +use chrono::Utc; +use sal_service_manager::{ProcessManager, SimpleProcessManager, TmuxProcessManager}; +use std::collections::HashMap; +use std::path::PathBuf; +use std::sync::Arc; +use tokio::sync::Mutex; + +use crate::{client::{Client, ClientBuilder}, job::JobStatus, runner::{LogInfo, Runner, RunnerConfig, RunnerError, RunnerResult, RunnerStatus}, JobError}; +use crate::{job::Job}; + +#[cfg(feature = "admin")] +use supervisor_admin_server::{AdminSupervisor, RunnerConfigInfo, JobInfo}; + +/// Process manager type for a runner +#[derive(Debug, Clone)] +pub enum ProcessManagerType { + /// Simple process manager for direct process spawning + Simple, + /// Tmux process manager for session-based management + Tmux(String), // session name +} + +/// Main supervisor that manages multiple runners +#[derive(Clone)] +pub struct Supervisor { + /// Map of runner name to runner configuration + runners: HashMap, + /// Shared process manager for all runners + process_manager: Arc>, + /// Shared Redis client for all runners + redis_client: redis::Client, + /// Namespace for queue keys + namespace: String, + /// Admin secrets for full access + admin_secrets: Vec, + /// User secrets for limited access + user_secrets: Vec, + /// Register secrets for runner registration + register_secrets: Vec, + client: Client, +} + +pub struct SupervisorBuilder { + /// Map of runner name to runner configuration + runners: HashMap, + /// Redis URL for connection + redis_url: String, + /// Process manager type + process_manager_type: ProcessManagerType, + /// Namespace for queue keys + namespace: String, + /// Admin secrets for full access + admin_secrets: Vec, + /// User secrets for limited access + user_secrets: Vec, + /// Register secrets for runner registration + register_secrets: Vec, + client_builder: ClientBuilder, +} + +impl SupervisorBuilder { + /// Create a new supervisor builder + pub fn new() -> Self { + Self { + runners: HashMap::new(), + redis_url: "redis://localhost:6379".to_string(), + process_manager_type: ProcessManagerType::Simple, + namespace: "".to_string(), + admin_secrets: Vec::new(), + user_secrets: Vec::new(), + register_secrets: Vec::new(), + client_builder: ClientBuilder::new(), + } + } + + /// Set the Redis URL + pub fn redis_url>(mut self, url: S) -> Self { + let url_string = url.into(); + self.redis_url = url_string.clone(); + self.client_builder = self.client_builder.redis_url(url_string); + self + } + + /// Set the process manager type + pub fn process_manager(mut self, pm_type: ProcessManagerType) -> Self { + self.process_manager_type = pm_type; + self + } + + /// Set the namespace for queue keys + pub fn namespace>(mut self, namespace: S) -> Self { + let namespace_string = namespace.into(); + self.namespace = namespace_string.clone(); + self.client_builder = self.client_builder.namespace(namespace_string); + self + } + + /// Add an admin secret + pub fn add_admin_secret>(mut self, secret: S) -> Self { + self.admin_secrets.push(secret.into()); + self + } + + /// Add multiple admin secrets + pub fn admin_secrets(mut self, secrets: I) -> Self + where + I: IntoIterator, + S: Into, + { + self.admin_secrets.extend(secrets.into_iter().map(|s| s.into())); + self + } + + /// Add a user secret + pub fn add_user_secret>(mut self, secret: S) -> Self { + self.user_secrets.push(secret.into()); + self + } + + /// Add multiple user secrets + pub fn user_secrets(mut self, secrets: I) -> Self + where + I: IntoIterator, + S: Into, + { + self.user_secrets.extend(secrets.into_iter().map(|s| s.into())); + self + } + + /// Add a register secret + pub fn add_register_secret>(mut self, secret: S) -> Self { + self.register_secrets.push(secret.into()); + self + } + + /// Add multiple register secrets + pub fn register_secrets(mut self, secrets: I) -> Self + where + I: IntoIterator, + S: Into, + { + self.register_secrets.extend(secrets.into_iter().map(|s| s.into())); + self + } + + /// Add a runner to the supervisor + pub fn add_runner(mut self, runner: Runner) -> Self { + self.runners.insert(runner.id.clone(), runner); + self + } + + /// Build the supervisor + pub async fn build(self) -> RunnerResult { + // Create process manager based on type + let process_manager: Arc> = match &self.process_manager_type { + ProcessManagerType::Simple => { + Arc::new(Mutex::new(SimpleProcessManager::new())) + } + ProcessManagerType::Tmux(session_name) => { + Arc::new(Mutex::new(TmuxProcessManager::new(session_name.clone()))) + } + }; + + // Create Redis client + let redis_client = redis::Client::open(self.redis_url.as_str()) + .map_err(|e| RunnerError::ConfigError { + reason: format!("Invalid Redis URL: {}", e), + })?; + + Ok(Supervisor { + client: self.client_builder.build().await.unwrap(), + runners: self.runners, + process_manager, + redis_client, + namespace: self.namespace, + admin_secrets: self.admin_secrets, + user_secrets: self.user_secrets, + register_secrets: self.register_secrets, + }) + } +} + +impl Supervisor { + /// Create a new supervisor builder + pub fn builder() -> SupervisorBuilder { + SupervisorBuilder::new() + } + + /// Add a new runner to the supervisor + pub async fn add_runner( + &mut self, + config: RunnerConfig, + ) -> RunnerResult<()> { + // Runner is now just the config + let runner = Runner::from_config(config.clone()); + + self.runners.insert(config.id.clone(), runner); + Ok(()) + } + + /// Register a new runner with secret-based authentication + pub async fn register_runner(&mut self, secret: &str, name: &str, queue: &str) -> RunnerResult<()> { + // Check if the secret is valid (admin or register secret) + if !self.admin_secrets.contains(&secret.to_string()) && + !self.register_secrets.contains(&secret.to_string()) { + return Err(RunnerError::InvalidSecret("Invalid secret for runner registration".to_string())); + } + + // Create a basic runner config for the named runner + let config = RunnerConfig { + id: name.to_string(), // Use the provided name as actor_id + name: name.to_string(), // Use the provided name as actor_id + namespace: self.namespace.clone(), + command: PathBuf::from("/tmp/mock_runner"), // Default path + redis_url: "redis://localhost:6379".to_string(), + }; + + // Add the runner using existing logic + self.add_runner(config).await + } + + /// Create a job (fire-and-forget, non-blocking) with secret-based authentication + pub async fn create_job(&mut self, secret: &str, job: crate::job::Job) -> RunnerResult { + // Check if the secret is valid (admin or user secret) + if !self.admin_secrets.contains(&secret.to_string()) && + !self.user_secrets.contains(&secret.to_string()) { + return Err(RunnerError::InvalidSecret("Invalid secret for job creation".to_string())); + } + + // Find the runner by name + let runner_name = job.runner_name.clone(); + let job_id = job.id.clone(); // Store job ID before moving job + if let Some(_runner) = self.runners.get(&runner_name) { + // Use the supervisor's queue_job_to_runner method (fire-and-forget) + self.queue_job_to_runner(&runner_name, job).await?; + Ok(job_id) // Return the job ID immediately + } else { + Err(RunnerError::ActorNotFound { + actor_id: job.runner_name.clone(), + }) + } + } + + /// Run a job on the appropriate runner with secret-based authentication + /// This is a synchronous operation that queues the job, waits for the result, and returns it + pub async fn run_job(&mut self, secret: &str, job: crate::job::Job) -> RunnerResult> { + // Check if the secret is valid (admin or user secret) + if !self.admin_secrets.contains(&secret.to_string()) && + !self.user_secrets.contains(&secret.to_string()) { + return Err(RunnerError::InvalidSecret("Invalid secret for job execution".to_string())); + } + + // Find the runner by name + let runner_name = job.runner_name.clone(); + if let Some(_runner) = self.runners.get(&runner_name) { + // Use the synchronous queue_and_wait method with a reasonable timeout (30 seconds) + self.queue_and_wait(&runner_name, job, 30).await + } else { + Err(RunnerError::ActorNotFound { + actor_id: job.runner_name.clone(), + }) + } + } + + /// Remove a runner from the supervisor + pub async fn remove_runner(&mut self, actor_id: &str) -> RunnerResult<()> { + if let Some(_instance) = self.runners.remove(actor_id) { + // Runner is removed from the map, which will drop the Arc + // and eventually clean up the runner when no more references exist + } + Ok(()) + } + + /// Get a runner by actor ID + pub fn get_runner(&self, actor_id: &str) -> Option<&Runner> { + self.runners.get(actor_id) + } + + /// Get a job by job ID from Redis + pub async fn get_job(&self, job_id: &str) -> RunnerResult { + use redis::AsyncCommands; + + let mut conn = self.redis_client.get_multiplexed_async_connection().await + .map_err(|e| RunnerError::RedisError { + source: e + })?; + + self.client.load_job_from_redis(job_id).await + .map_err(|e| RunnerError::QueueError { + actor_id: job_id.to_string(), + reason: format!("Failed to load job: {}", e), + }) + } + + /// Ping a runner by dispatching a ping job to its queue + pub async fn ping_runner(&mut self, runner_id: &str) -> RunnerResult { + use crate::job::{Job, JobBuilder}; + use std::time::Duration; + + // Check if runner exists + if !self.runners.contains_key(runner_id) { + return Err(RunnerError::ActorNotFound { + actor_id: runner_id.to_string(), + }); + } + + // Create a ping job + let ping_job = JobBuilder::new() + .caller_id("supervisor_ping") + .context_id("ping_context") + .payload("ping") + .runner_name(runner_id) + .executor("ping") + .timeout(Duration::from_secs(10)) + .build() + .map_err(|e| RunnerError::QueueError { + actor_id: runner_id.to_string(), + reason: format!("Failed to create ping job: {}", e), + })?; + + // Queue the ping job + let job_id = ping_job.id.clone(); + self.queue_job_to_runner(runner_id, ping_job).await?; + + Ok(job_id) + } + + /// Stop a job by ID + pub async fn stop_job(&mut self, job_id: &str) -> RunnerResult<()> { + use redis::AsyncCommands; + + // For now, we'll implement a basic stop by removing the job from Redis + // In a more sophisticated implementation, you might send a stop signal to the runner + let mut conn = self.redis_client.get_multiplexed_async_connection().await + .map_err(|e| RunnerError::QueueError { + actor_id: job_id.to_string(), + reason: format!("Failed to connect to Redis: {}", e), + })?; + + let job_key = self.client.set_job_status(job_id, JobStatus::Stopping).await; + + Ok(()) + } + + /// Delete a job by ID + pub async fn delete_job(&mut self, job_id: &str) -> RunnerResult<()> { + self.client.delete_job(&job_id).await + } + + /// List all managed runners + pub fn list_runners(&self) -> Vec<&str> { + self.runners.keys().map(|s| s.as_str()).collect() + } + + /// Start a specific runner + pub async fn start_runner(&mut self, actor_id: &str) -> RunnerResult<()> { + use crate::runner::runner_to_process_config; + use log::info; + + if let Some(runner) = self.runners.get(actor_id) { + info!("Starting actor {}", runner.id); + + let process_config = runner_to_process_config(runner); + let mut pm = self.process_manager.lock().await; + pm.start_process(&process_config).await?; + + info!("Successfully started actor {}", runner.id); + Ok(()) + } else { + Err(RunnerError::ActorNotFound { + actor_id: actor_id.to_string(), + }) + } + } + + /// Stop a specific runner + pub async fn stop_runner(&mut self, actor_id: &str, force: bool) -> RunnerResult<()> { + use log::info; + + if let Some(runner) = self.runners.get(actor_id) { + info!("Stopping actor {}", runner.id); + + let mut pm = self.process_manager.lock().await; + pm.stop_process(&runner.id, force).await?; + + info!("Successfully stopped actor {}", runner.id); + Ok(()) + } else { + Err(RunnerError::ActorNotFound { + actor_id: actor_id.to_string(), + }) + } + } + + /// Get status of a specific runner + pub async fn get_runner_status(&self, actor_id: &str) -> RunnerResult { + if let Some(runner) = self.runners.get(actor_id) { + let pm = self.process_manager.lock().await; + let status = pm.process_status(&runner.id).await?; + Ok(status) + } else { + Err(RunnerError::ActorNotFound { + actor_id: actor_id.to_string(), + }) + } + } + + /// Get logs from a specific runner + pub async fn get_runner_logs( + &self, + actor_id: &str, + lines: Option, + follow: bool, + ) -> RunnerResult> { + if let Some(runner) = self.runners.get(actor_id) { + let pm = self.process_manager.lock().await; + let logs = pm.process_logs(&runner.id, lines, follow).await?; + + // Convert sal_service_manager::LogInfo to our LogInfo + let converted_logs = logs.into_iter().map(|log| LogInfo { + timestamp: log.timestamp, + level: log.level, + message: log.message, + }).collect(); + + Ok(converted_logs) + } else { + Err(RunnerError::ActorNotFound { + actor_id: actor_id.to_string(), + }) + } + } + + /// Queue a job to a specific runner by name + pub async fn queue_job_to_runner(&mut self, runner_name: &str, job: crate::job::Job) -> RunnerResult<()> { + use redis::AsyncCommands; + use log::{debug, info}; + + if let Some(runner) = self.runners.get(runner_name) { + debug!("Queuing job {} for actor {}", job.id, runner.id); + + let mut conn = self.redis_client.get_multiplexed_async_connection().await + .map_err(|e| RunnerError::QueueError { + actor_id: runner.id.clone(), + reason: format!("Failed to connect to Redis: {}", e), + })?; + + // Store the job in Redis first + self.client.store_job_in_redis(&job).await + .map_err(|e| RunnerError::QueueError { + actor_id: runner.id.clone(), + reason: format!("Failed to store job: {}", e), + })?; + + // Use the runner's get_queue method with our namespace + let queue_key = runner.get_queue(); + + let _: () = conn.lpush(&queue_key, &job.id).await + .map_err(|e| RunnerError::QueueError { + actor_id: runner.id.clone(), + reason: format!("Failed to queue job: {}", e), + })?; + + info!("Job {} queued successfully for actor {} on queue {}", job.id, runner.id, queue_key); + Ok(()) + } else { + Err(RunnerError::ActorNotFound { + actor_id: runner_name.to_string(), + }) + } + } + + /// Queue a job to a specific runner and wait for the result + /// This implements the proper Hero job protocol: + /// 1. Queue the job to the runner + /// 2. BLPOP on the reply queue for this job + /// 3. Get the job result from the job hash + /// 4. Return the complete result + pub async fn queue_and_wait(&mut self, runner_name: &str, job: crate::job::Job, timeout_secs: u64) -> RunnerResult> { + use redis::AsyncCommands; + + let job_id = job.id.clone(); + + // First queue the job + self.queue_job_to_runner(runner_name, job).await?; + + // Get Redis connection from the supervisor (shared Redis client) + let _runner = self.runners.get(runner_name) + .ok_or_else(|| RunnerError::ActorNotFound { + actor_id: runner_name.to_string(), + })?; + + let mut conn = self.redis_client.get_multiplexed_async_connection().await + .map_err(|e| RunnerError::RedisError { + source: e + })?; + + // BLPOP on the reply queue for this specific job + let reply_key = self.client.job_reply_key(&job_id); + let result: Option> = conn.blpop(&reply_key, timeout_secs as f64).await + .map_err(|e| RunnerError::RedisError { + source: e + })?; + + match result { + Some(reply_data) => { + // Reply received, now get the job result from the job hash + let job_key = self.client.job_key(&job_id); + let job_result: Option = conn.hget(&job_key, "result").await + .map_err(|e| RunnerError::RedisError { + source: e + })?; + + Ok(job_result) + } + None => { + // Timeout occurred + Ok(None) + } + } + } + + /// Get status of all runners + pub async fn get_all_runner_status(&self) -> RunnerResult> { + let mut results = Vec::new(); + + for (actor_id, instance) in &self.runners { + match self.get_runner_status(actor_id).await { + Ok(status) => results.push((actor_id.clone(), status)), + Err(_) => { + use sal_service_manager::ProcessStatus; + results.push((actor_id.clone(), ProcessStatus::Stopped)); + } + } + } + + Ok(results) + } + + /// Start all runners + pub async fn start_all(&mut self) -> Vec<(String, RunnerResult<()>)> { + let mut results = Vec::new(); + let actor_ids: Vec = self.runners.keys().cloned().collect(); + + for actor_id in actor_ids { + let result = self.start_runner(&actor_id).await; + results.push((actor_id, result)); + } + + results + } + + /// Stop all runners + pub async fn stop_all(&mut self, force: bool) -> Vec<(String, RunnerResult<()>)> { + let mut results = Vec::new(); + let actor_ids: Vec = self.runners.keys().cloned().collect(); + + for actor_id in actor_ids { + let result = self.stop_runner(&actor_id, force).await; + results.push((actor_id, result)); + } + + results + } + + /// Get status of all runners + pub async fn get_all_status(&self) -> Vec<(String, RunnerResult)> { + let mut results = Vec::new(); + + for (actor_id, _instance) in &self.runners { + let result = self.get_runner_status(actor_id).await; + results.push((actor_id.clone(), result)); + } + + results + } + + /// Add an admin secret + pub fn add_admin_secret(&mut self, secret: String) { + if !self.admin_secrets.contains(&secret) { + self.admin_secrets.push(secret); + } + } + + /// Remove an admin secret + pub fn remove_admin_secret(&mut self, secret: &str) -> bool { + if let Some(pos) = self.admin_secrets.iter().position(|x| x == secret) { + self.admin_secrets.remove(pos); + true + } else { + false + } + } + + /// Check if admin secret exists + pub fn has_admin_secret(&self, secret: &str) -> bool { + self.admin_secrets.contains(&secret.to_string()) + } + + /// Get admin secrets count + pub fn admin_secrets_count(&self) -> usize { + self.admin_secrets.len() + } + + /// Add a user secret + pub fn add_user_secret(&mut self, secret: String) { + if !self.user_secrets.contains(&secret) { + self.user_secrets.push(secret); + } + } + + /// Remove a user secret + pub fn remove_user_secret(&mut self, secret: &str) -> bool { + if let Some(pos) = self.user_secrets.iter().position(|x| x == secret) { + self.user_secrets.remove(pos); + true + } else { + false + } + } + + /// Check if user secret exists + pub fn has_user_secret(&self, secret: &str) -> bool { + self.user_secrets.contains(&secret.to_string()) + } + + /// Get user secrets count + pub fn user_secrets_count(&self) -> usize { + self.user_secrets.len() + } + + /// Add a register secret + pub fn add_register_secret(&mut self, secret: String) { + if !self.register_secrets.contains(&secret) { + self.register_secrets.push(secret); + } + } + + /// Remove a register secret + pub fn remove_register_secret(&mut self, secret: &str) -> bool { + if let Some(pos) = self.register_secrets.iter().position(|x| x == secret) { + self.register_secrets.remove(pos); + true + } else { + false + } + } + + /// Check if register secret exists + pub fn has_register_secret(&self, secret: &str) -> bool { + self.register_secrets.contains(&secret.to_string()) + } + + /// Get register secrets count + pub fn register_secrets_count(&self) -> usize { + self.register_secrets.len() + } + + /// List all job IDs from Redis + pub async fn list_jobs(&self) -> RunnerResult> { + self.client.list_jobs().await + } + + /// Get runners count + pub fn runners_count(&self) -> usize { + self.runners.len() + } + + /// Get admin secrets (returns cloned vector for security) + pub fn get_admin_secrets(&self) -> Vec { + self.admin_secrets.clone() + } + + /// Get user secrets (returns cloned vector for security) + pub fn get_user_secrets(&self) -> Vec { + self.user_secrets.clone() + } + + /// Get register secrets (returns cloned vector for security) + pub fn get_register_secrets(&self) -> Vec { + self.register_secrets.clone() + } +} + +impl Default for Supervisor { + fn default() -> Self { + // Note: Default implementation creates an empty supervisor + // Use Supervisor::builder() for proper initialization + Self { + runners: HashMap::new(), + process_manager: Arc::new(Mutex::new(SimpleProcessManager::new())), + redis_client: redis::Client::open("redis://localhost:6379").unwrap(), + namespace: "".to_string(), + admin_secrets: Vec::new(), + user_secrets: Vec::new(), + register_secrets: Vec::new(), + client: Client::default(), + } + } +} + +mod tests { + use super::*; + use std::path::PathBuf; + use sal_service_manager::SimpleProcessManager; + + #[tokio::test] + async fn test_supervisor_creation() { + let supervisor = Supervisor::builder() + .redis_url("redis://localhost:6379") + .build() + .await + .unwrap(); + assert_eq!(supervisor.list_runners().len(), 0); + } + + #[tokio::test] + async fn test_add_runner() { + use std::path::PathBuf; + + let config = RunnerConfig::new( + "test_actor".to_string(), + "test_actor".to_string(), + "".to_string(), + PathBuf::from("/usr/bin/test_actor"), + "redis://localhost:6379".to_string(), + ); + + let runner = Runner::from_config(config.clone()); + let mut supervisor = Supervisor::builder() + .redis_url("redis://localhost:6379") + .add_runner(runner) + .build() + .await + .unwrap(); + + assert_eq!(supervisor.list_runners().len(), 1); + } + + #[tokio::test] + async fn test_add_multiple_runners() { + use std::path::PathBuf; + + let config1 = RunnerConfig::new( + "sal_actor".to_string(), + "sal_actor".to_string(), + "".to_string(), + PathBuf::from("/usr/bin/sal_actor"), + "redis://localhost:6379".to_string(), + ); + + let config2 = RunnerConfig::new( + "osis_actor".to_string(), + "osis_actor".to_string(), + "".to_string(), + PathBuf::from("/usr/bin/osis_actor"), + "redis://localhost:6379".to_string(), + ); + + let runner1 = Runner::from_config(config1); + let runner2 = Runner::from_config(config2); + + let supervisor = Supervisor::builder() + .redis_url("redis://localhost:6379") + .add_runner(runner1) + .add_runner(runner2) + .build() + .await + .unwrap(); + + assert_eq!(supervisor.list_runners().len(), 2); + assert!(supervisor.get_runner("sal_actor").is_some()); + assert!(supervisor.get_runner("osis_actor").is_some()); + } +}