diff --git a/.github/actions/e2e-mac/action.yml b/.github/actions/e2e-mac/action.yml index e69f2930..33768eec 100644 --- a/.github/actions/e2e-mac/action.yml +++ b/.github/actions/e2e-mac/action.yml @@ -27,13 +27,13 @@ runs: prism mock -h localhost -p 8020 test_resource/studio.yaml > studio.txt 2>&1 & sleep 5 - ${{ inputs.binary-path }} > log.txt 2>&1 & + ${{ inputs.binary-path }} controller > log.txt 2>&1 & sleep 5 cd e2e cargo test - pgrep -f nodex-agent | xargs kill -SIGINT + pgrep -f "nodex-agent controller" | xargs kill -SIGINT sleep 5 if !grep -q "SIGINT" log.txt; then @@ -41,7 +41,7 @@ runs: exit 1 fi - pids=$(pgrep -f nodex-agent || true) + pids=$(pgrep -f "nodex-agent controller" || true) # When executing pgrep, if the process does not exist, it exits; therefore, a solution for that is needed. if [ -z "$pids" ]; then echo "Process not found, as expected." diff --git a/.github/actions/e2e-with-docker/action.yml b/.github/actions/e2e-with-docker/action.yml index 2635d190..88f06206 100644 --- a/.github/actions/e2e-with-docker/action.yml +++ b/.github/actions/e2e-with-docker/action.yml @@ -34,7 +34,7 @@ runs: shell: bash run: | container_id=$(docker compose -f test_resource/compose.yaml -f test_resource/overrides/${{ inputs.docker-image }}.yaml --profile e2e ps -q e2e_agent) - pid=$(docker exec $container_id pgrep -f nodex-agent) + pid=$(docker exec $container_id pgrep -f "nodex-agent controller") docker exec $container_id kill -SIGINT $pid sleep 3 @@ -53,7 +53,7 @@ runs: if: steps.check_logs_for_sigint.outputs.stopped_found == 'true' run: | container_id=$(docker compose -f test_resource/compose.yaml -f test_resource/overrides/${{ inputs.docker-image }}.yaml --profile e2e ps -q e2e_agent) - if ! docker exec $container_id pgrep -f /tmp/nodex-agent; then + if ! docker exec $container_id pgrep -f "/tmp/nodex-agent controller"; then echo "Process not found, as expected." else echo "Process is still running, which is not expected." @@ -70,7 +70,7 @@ runs: shell: bash run: | container_id=$(docker compose -f test_resource/compose.yaml -f test_resource/overrides/${{ inputs.docker-image }}.yaml --profile e2e ps -q e2e_agent) - pid=$(docker exec $container_id pgrep -f nodex-agent) + pid=$(docker exec $container_id pgrep -f "nodex-agent controller") docker exec $container_id kill -SIGTERM $pid sleep 3 @@ -89,7 +89,7 @@ runs: if: steps.check_logs_for_sigterm.outputs.stopped_found == 'true' run: | container_id=$(docker compose -f test_resource/compose.yaml -f test_resource/overrides/${{ inputs.docker-image }}.yaml --profile e2e ps -q e2e_agent) - if ! docker exec $container_id pgrep -f /tmp/nodex-agent; then + if ! docker exec $container_id pgrep -f "/tmp/nodex-agent controller"; then echo "Process not found, as expected." else echo "Process is still running, which is not expected." diff --git a/Cargo.lock b/Cargo.lock index 296c558c..c24afbe2 100644 --- a/Cargo.lock +++ b/Cargo.lock @@ -2,190 +2,6 @@ # It is not intended for manual editing. version = 4 -[[package]] -name = "actix-codec" -version = "0.5.2" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "5f7b0a21988c1bf877cf4759ef5ddaac04c1c9fe808c9142ecb78ba97d97a28a" -dependencies = [ - "bitflags", - "bytes", - "futures-core", - "futures-sink", - "memchr", - "pin-project-lite", - "tokio", - "tokio-util", - "tracing", -] - -[[package]] -name = "actix-http" -version = "3.9.0" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "d48f96fc3003717aeb9856ca3d02a8c7de502667ad76eeacd830b48d2e91fac4" -dependencies = [ - "actix-codec", - "actix-rt", - "actix-service", - "actix-utils", - "ahash", - "base64 0.22.1", - "bitflags", - "brotli", - "bytes", - "bytestring", - "derive_more", - "encoding_rs", - "flate2", - "futures-core", - "h2", - "http 0.2.12", - "httparse", - "httpdate", - "itoa", - "language-tags", - "local-channel", - "mime", - "percent-encoding", - "pin-project-lite", - "rand 0.8.5", - "sha1", - "smallvec", - "tokio", - "tokio-util", - "tracing", - "zstd", -] - -[[package]] -name = "actix-macros" -version = "0.2.4" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "e01ed3140b2f8d422c68afa1ed2e85d996ea619c988ac834d255db32138655cb" -dependencies = [ - "quote", - "syn 2.0.87", -] - -[[package]] -name = "actix-router" -version = "0.5.3" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "13d324164c51f63867b57e73ba5936ea151b8a41a1d23d1031eeb9f70d0236f8" -dependencies = [ - "bytestring", - "cfg-if", - "http 0.2.12", - "regex", - "regex-lite", - "serde", - "tracing", -] - -[[package]] -name = "actix-rt" -version = "2.10.0" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "24eda4e2a6e042aa4e55ac438a2ae052d3b5da0ecf83d7411e1a368946925208" -dependencies = [ - "actix-macros", - "futures-core", - "tokio", -] - -[[package]] -name = "actix-server" -version = "2.5.0" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "7ca2549781d8dd6d75c40cf6b6051260a2cc2f3c62343d761a969a0640646894" -dependencies = [ - "actix-rt", - "actix-service", - "actix-utils", - "futures-core", - "futures-util", - "mio", - "socket2", - "tokio", - "tracing", -] - -[[package]] -name = "actix-service" -version = "2.0.2" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "3b894941f818cfdc7ccc4b9e60fa7e53b5042a2e8567270f9147d5591893373a" -dependencies = [ - "futures-core", - "paste", - "pin-project-lite", -] - -[[package]] -name = "actix-utils" -version = "3.0.1" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "88a1dcdff1466e3c2488e1cb5c36a71822750ad43839937f85d2f4d9f8b705d8" -dependencies = [ - "local-waker", - "pin-project-lite", -] - -[[package]] -name = "actix-web" -version = "4.9.0" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "9180d76e5cc7ccbc4d60a506f2c727730b154010262df5b910eb17dbe4b8cb38" -dependencies = [ - "actix-codec", - "actix-http", - "actix-macros", - "actix-router", - "actix-rt", - "actix-server", - "actix-service", - "actix-utils", - "actix-web-codegen", - "ahash", - "bytes", - "bytestring", - "cfg-if", - "cookie", - "derive_more", - "encoding_rs", - "futures-core", - "futures-util", - "impl-more", - "itoa", - "language-tags", - "log", - "mime", - "once_cell", - "pin-project-lite", - "regex", - "regex-lite", - "serde", - "serde_json", - "serde_urlencoded", - "smallvec", - "socket2", - "time", - "url", -] - -[[package]] -name = "actix-web-codegen" -version = "4.3.0" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "f591380e2e68490b5dfaf1dd1aa0ebe78d84ba7067078512b4ea6e4492d622b8" -dependencies = [ - "actix-router", - "proc-macro2", - "quote", - "syn 2.0.87", -] - [[package]] name = "addr2line" version = "0.24.2" @@ -218,7 +34,7 @@ checksum = "9e8b47f52ea9bae42228d07ec09eb676433d7c4ed1ebdf0f1d1c29ed446f1ab8" dependencies = [ "cfg-if", "cipher 0.3.0", - "cpufeatures 0.2.14", + "cpufeatures 0.2.16", "opaque-debug 0.3.1", ] @@ -230,7 +46,7 @@ checksum = "b169f7a6d4742236a0a00c541b845991d0ac43e546831af1249753ab4c3aa3a0" dependencies = [ "cfg-if", "cipher 0.4.4", - "cpufeatures 0.2.14", + "cpufeatures 0.2.16", ] [[package]] @@ -251,54 +67,46 @@ dependencies = [ name = "agent" version = "3.4.0" dependencies = [ - "actix-rt", - "actix-web", "anyhow", + "async-trait", + "axum", "bytes", "chrono", "clap", + "controller", "cuid", - "daemonize", "dirs 5.0.1", "dotenvy", - "env_logger 0.11.5", + "fs2", "hex", "hmac 0.12.1", "home-config", + "http-body-util", + "hyper 1.5.2", + "hyper-util", "log", "mac_address", "nix 0.29.0", "protocol", "reqwest", "rstest", - "rumqttc", "serde", "serde_json", "sha2 0.10.8", "shadow-rs", "sysinfo", - "thiserror", + "thiserror 1.0.69", "tokio", + "tokio-util", + "tower 0.4.13", "trait-variant", "url", "uuid", + "validator", "windows 0.58.0", "zip", ] -[[package]] -name = "ahash" -version = "0.8.11" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "e89da841a80418a9b391ebaea17f5c112ffaaa96f621d2c285b5174da76b9011" -dependencies = [ - "cfg-if", - "getrandom 0.2.15", - "once_cell", - "version_check", - "zerocopy", -] - [[package]] name = "aho-corasick" version = "1.1.3" @@ -308,21 +116,6 @@ dependencies = [ "memchr", ] -[[package]] -name = "alloc-no-stdlib" -version = "2.0.4" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "cc7bb162ec39d46ab1ca8c77bf72e890535becd1751bb45f64c597edb4c8c6b3" - -[[package]] -name = "alloc-stdlib" -version = "0.2.2" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "94fb8275041c72129eb51b7d0322c29b8387a0386127718b096429201a5d6ece" -dependencies = [ - "alloc-no-stdlib", -] - [[package]] name = "android-tzdata" version = "0.1.1" @@ -389,9 +182,9 @@ dependencies = [ [[package]] name = "anyhow" -version = "1.0.94" +version = "1.0.95" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "c1fd03a028ef38ba2276dce7e33fcd6369c158a1bca17946c4b1b701891c1ff7" +checksum = "34ac096ce696dc2fcabef30516bb13c0a68a11d30131d3df6f04711467681b04" [[package]] name = "arbitrary" @@ -408,6 +201,213 @@ version = "0.3.9" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "76a2e8124351fda1ef8aaaa3bbd7ebbcb486bbcd4225aca0aa0d84bb2db8fecb" +[[package]] +name = "ascii-canvas" +version = "3.0.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "8824ecca2e851cec16968d54a01dd372ef8f95b244fb84b84e70128be347c3c6" +dependencies = [ + "term", +] + +[[package]] +name = "assert-json-diff" +version = "2.0.2" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "47e4f2b81832e72834d7518d8487a0396a28cc408186a2e8854c0f98011faf12" +dependencies = [ + "serde", + "serde_json", +] + +[[package]] +name = "async-attributes" +version = "1.1.2" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "a3203e79f4dd9bdda415ed03cf14dae5a2bf775c683a00f94e9cd1faf0f596e5" +dependencies = [ + "quote", + "syn 1.0.109", +] + +[[package]] +name = "async-channel" +version = "1.9.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "81953c529336010edd6d8e358f886d9581267795c61b19475b71314bffa46d35" +dependencies = [ + "concurrent-queue", + "event-listener 2.5.3", + "futures-core", +] + +[[package]] +name = "async-channel" +version = "2.3.1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "89b47800b0be77592da0afd425cc03468052844aff33b84e33cc696f64e77b6a" +dependencies = [ + "concurrent-queue", + "event-listener-strategy", + "futures-core", + "pin-project-lite", +] + +[[package]] +name = "async-executor" +version = "1.13.1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "30ca9a001c1e8ba5149f91a74362376cc6bc5b919d92d988668657bd570bdcec" +dependencies = [ + "async-task", + "concurrent-queue", + "fastrand", + "futures-lite", + "slab", +] + +[[package]] +name = "async-global-executor" +version = "2.4.1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "05b1b633a2115cd122d73b955eadd9916c18c8f510ec9cd1686404c60ad1c29c" +dependencies = [ + "async-channel 2.3.1", + "async-executor", + "async-io", + "async-lock", + "blocking", + "futures-lite", + "once_cell", +] + +[[package]] +name = "async-io" +version = "2.4.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "43a2b323ccce0a1d90b449fd71f2a06ca7faa7c54c2751f06c9bd851fc061059" +dependencies = [ + "async-lock", + "cfg-if", + "concurrent-queue", + "futures-io", + "futures-lite", + "parking", + "polling", + "rustix", + "slab", + "tracing", + "windows-sys 0.59.0", +] + +[[package]] +name = "async-lock" +version = "3.4.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "ff6e472cdea888a4bd64f342f09b3f50e1886d32afe8df3d663c01140b811b18" +dependencies = [ + "event-listener 5.3.1", + "event-listener-strategy", + "pin-project-lite", +] + +[[package]] +name = "async-object-pool" +version = "0.1.5" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "333c456b97c3f2d50604e8b2624253b7f787208cb72eb75e64b0ad11b221652c" +dependencies = [ + "async-std", +] + +[[package]] +name = "async-process" +version = "2.3.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "63255f1dc2381611000436537bbedfe83183faa303a5a0edaf191edef06526bb" +dependencies = [ + "async-channel 2.3.1", + "async-io", + "async-lock", + "async-signal", + "async-task", + "blocking", + "cfg-if", + "event-listener 5.3.1", + "futures-lite", + "rustix", + "tracing", +] + +[[package]] +name = "async-signal" +version = "0.2.10" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "637e00349800c0bdf8bfc21ebbc0b6524abea702b0da4168ac00d070d0c0b9f3" +dependencies = [ + "async-io", + "async-lock", + "atomic-waker", + "cfg-if", + "futures-core", + "futures-io", + "rustix", + "signal-hook-registry", + "slab", + "windows-sys 0.59.0", +] + +[[package]] +name = "async-std" +version = "1.13.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "c634475f29802fde2b8f0b505b1bd00dfe4df7d4a000f0b36f7671197d5c3615" +dependencies = [ + "async-attributes", + "async-channel 1.9.0", + "async-global-executor", + "async-io", + "async-lock", + "async-process", + "crossbeam-utils", + "futures-channel", + "futures-core", + "futures-io", + "futures-lite", + "gloo-timers", + "kv-log-macro", + "log", + "memchr", + "once_cell", + "pin-project-lite", + "pin-utils", + "slab", + "wasm-bindgen-futures", +] + +[[package]] +name = "async-task" +version = "4.7.1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "8b75356056920673b02621b35afd0f7dda9306d03c79a30f5c56c44cf256e3de" + +[[package]] +name = "async-trait" +version = "0.1.83" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "721cae7de5c34fbb2acd27e21e6d2cf7b886dce0c27388d46c4e6c47ea4318dd" +dependencies = [ + "proc-macro2", + "quote", + "syn 2.0.91", +] + +[[package]] +name = "atomic-waker" +version = "1.1.2" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "1505bd5d3d116872e7271a6d4e16d81d0c8570876c8de68093a09ac269d8aac0" + [[package]] name = "atty" version = "0.2.14" @@ -425,6 +425,73 @@ version = "1.4.0" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "ace50bade8e6234aa140d9a2f552bbee1db4d353f69b8217bc503490fc1a9f26" +[[package]] +name = "axum" +version = "0.7.9" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "edca88bc138befd0323b20752846e6587272d3b03b0343c8ea28a6f819e6e71f" +dependencies = [ + "async-trait", + "axum-core", + "axum-macros", + "bytes", + "futures-util", + "http 1.2.0", + "http-body 1.0.1", + "http-body-util", + "hyper 1.5.2", + "hyper-util", + "itoa", + "matchit", + "memchr", + "mime", + "percent-encoding", + "pin-project-lite", + "rustversion", + "serde", + "serde_json", + "serde_path_to_error", + "serde_urlencoded", + "sync_wrapper", + "tokio", + "tower 0.5.2", + "tower-layer", + "tower-service", + "tracing", +] + +[[package]] +name = "axum-core" +version = "0.4.5" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "09f2bd6146b97ae3359fa0cc6d6b376d9539582c7b4220f041a33ec24c226199" +dependencies = [ + "async-trait", + "bytes", + "futures-util", + "http 1.2.0", + "http-body 1.0.1", + "http-body-util", + "mime", + "pin-project-lite", + "rustversion", + "sync_wrapper", + "tower-layer", + "tower-service", + "tracing", +] + +[[package]] +name = "axum-macros" +version = "0.4.2" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "57d123550fa8d071b7255cb0cc04dc302baa6c8c4a79f55701552684d8399bce" +dependencies = [ + "proc-macro2", + "quote", + "syn 2.0.91", +] + [[package]] name = "backtrace" version = "0.3.74" @@ -469,25 +536,76 @@ source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "9e1b586273c5702936fe7b7d6896644d8be71e6314cfe09d3167c95f712589e8" [[package]] -name = "base64" -version = "0.22.1" +name = "base64" +version = "0.21.7" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "9d297deb1925b89f2ccc13d7635fa0714f12c87adce1c75356b39ca9b7178567" + +[[package]] +name = "base64" +version = "0.22.1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "72b3254f16251a8381aa12e40e3c4d2f0199f8c6508fbecb9d91f575e0fbb8c6" + +[[package]] +name = "base64-url" +version = "1.4.13" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "67a99c239d0c7e77c85dddfa9cebce48704b3c49550fcd3b84dd637e4484899f" +dependencies = [ + "base64 0.13.1", +] + +[[package]] +name = "base64ct" +version = "1.6.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "8c3c1a368f70d6cf7302d78f8f7093da241fb8e8807c05cc9e51a125895a6d5b" + +[[package]] +name = "basic-cookies" +version = "0.1.5" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "67bd8fd42c16bdb08688243dc5f0cc117a3ca9efeeaba3a345a18a6159ad96f7" +dependencies = [ + "lalrpop", + "lalrpop-util", + "regex", +] + +[[package]] +name = "bin" +version = "3.4.0" +dependencies = [ + "agent", + "chrono", + "clap", + "controller", + "env_logger 0.11.6", + "log", + "shadow-rs", +] + +[[package]] +name = "bit-set" +version = "0.5.3" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "72b3254f16251a8381aa12e40e3c4d2f0199f8c6508fbecb9d91f575e0fbb8c6" +checksum = "0700ddab506f33b20a03b13996eccd309a48e5ff77d0d95926aa0210fb4e95f1" +dependencies = [ + "bit-vec", +] [[package]] -name = "base64-url" -version = "1.4.13" +name = "bit-vec" +version = "0.6.3" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "67a99c239d0c7e77c85dddfa9cebce48704b3c49550fcd3b84dd637e4484899f" -dependencies = [ - "base64 0.13.1", -] +checksum = "349f9b6a179ed607305526ca489b34ad0a41aed5f7980fa90eb03160b69598fb" [[package]] -name = "base64ct" -version = "1.6.0" +name = "bitflags" +version = "1.3.2" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "8c3c1a368f70d6cf7302d78f8f7093da241fb8e8807c05cc9e51a125895a6d5b" +checksum = "bef38d45163c2f1dde094a7dfd33ccf595c92905c8f8f4fdc18d06fb1037718a" [[package]] name = "bitflags" @@ -535,24 +653,16 @@ dependencies = [ ] [[package]] -name = "brotli" -version = "6.0.0" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "74f7971dbd9326d58187408ab83117d8ac1bb9c17b085fdacd1cf2f598719b6b" -dependencies = [ - "alloc-no-stdlib", - "alloc-stdlib", - "brotli-decompressor", -] - -[[package]] -name = "brotli-decompressor" -version = "4.0.1" +name = "blocking" +version = "1.6.1" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "9a45bd2e4095a8b518033b128020dd4a55aab1c0a381ba4404a472630f4bc362" +checksum = "703f41c54fc768e63e091340b424302bb1c29ef4aa0c7f10fe849dfb114d29ea" dependencies = [ - "alloc-no-stdlib", - "alloc-stdlib", + "async-channel 2.3.1", + "async-task", + "futures-io", + "futures-lite", + "piper", ] [[package]] @@ -579,15 +689,6 @@ version = "1.9.0" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "325918d6fe32f23b19878fe4b34794ae41fc19ddbe53b10571a4874d44ffd39b" -[[package]] -name = "bytestring" -version = "1.3.1" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "74d80203ea6b29df88012294f62733de21cfeab47f17b41af3a38bc30a03ee72" -dependencies = [ - "bytes", -] - [[package]] name = "bzip2" version = "0.4.4" @@ -611,9 +712,9 @@ dependencies = [ [[package]] name = "cc" -version = "1.1.36" +version = "1.2.5" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "baee610e9452a8f6f0a1b6194ec09ff9e2d85dea54432acdae41aa0761c95d70" +checksum = "c31a0499c1dc64f458ad13872de75c0eb7e3fdb0e67964610c914b034fc5956e" dependencies = [ "jobserver", "libc", @@ -665,9 +766,9 @@ dependencies = [ [[package]] name = "chrono" -version = "0.4.38" +version = "0.4.39" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "a21f936df1771bf62b77f047b726c4625ff2e8aa607c01ec06e5a05bd8463401" +checksum = "7e36cc9d416881d2e24f9a963be5fb1cd90966419ac844274161d10488b3e825" dependencies = [ "android-tzdata", "iana-time-zone", @@ -728,7 +829,7 @@ dependencies = [ "heck", "proc-macro2", "quote", - "syn 2.0.87", + "syn 2.0.91", ] [[package]] @@ -743,6 +844,25 @@ version = "1.0.3" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "5b63caa9aa9397e2d9480a9b13673856c78d8ac123288526c37d7839f2a86990" +[[package]] +name = "colored" +version = "2.2.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "117725a109d387c937a1533ce01b450cbde6b88abceea8473c4d7a85853cda3c" +dependencies = [ + "lazy_static", + "windows-sys 0.59.0", +] + +[[package]] +name = "concurrent-queue" +version = "2.5.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "4ca0197aee26d1ae37445ee532fefce43251d24cc7c166799f4d46817f1d3973" +dependencies = [ + "crossbeam-utils", +] + [[package]] name = "const-oid" version = "0.6.2" @@ -763,18 +883,18 @@ checksum = "373e9fafaa20882876db20562275ff58d50e0caa2590077fe7ce7bef90211d0d" [[package]] name = "const_format" -version = "0.2.33" +version = "0.2.34" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "50c655d81ff1114fb0dcdea9225ea9f0cc712a6f8d189378e82bdf62a473a64b" +checksum = "126f97965c8ad46d6d9163268ff28432e8f6a1196a55578867832e3049df63dd" dependencies = [ "const_format_proc_macros", ] [[package]] name = "const_format_proc_macros" -version = "0.2.33" +version = "0.2.34" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "eff1a44b93f47b1bac19a27932f5c591e43d1ba357ee4f61526c8a25603f0eb1" +checksum = "1d57c2eccfb16dbac1f4e61e206105db5820c9d26c3c472bc17c774259ef7744" dependencies = [ "proc-macro2", "quote", @@ -788,27 +908,50 @@ source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "7c74b8349d32d297c9134b8c88677813a227df8f779daa29bfc29c183fe3dca6" [[package]] -name = "convert_case" -version = "0.4.0" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "6245d59a3e82a7fc217c5828a6692dbc6dfb63a0c8c90495621f7b9d79704a0e" - -[[package]] -name = "cookie" -version = "0.16.2" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "e859cd57d0710d9e06c381b550c06e76992472a8c6d527aecd2fc673dcc231fb" +name = "controller" +version = "3.4.0" dependencies = [ - "percent-encoding", - "time", - "version_check", + "bytes", + "chrono", + "dirs 5.0.1", + "env_logger 0.11.6", + "filename", + "filetime", + "flate2", + "fs2", + "glob", + "http-body-util", + "httpmock", + "hyper 1.5.2", + "hyper-util", + "hyperlocal", + "lazy_static", + "libc", + "log", + "mockito", + "nix 0.29.0", + "notify", + "reqwest", + "semver", + "serde", + "serde_json", + "serde_yaml", + "serial_test", + "shadow-rs", + "tar", + "tempfile", + "thiserror 1.0.69", + "tokio", + "trait-variant", + "users", + "zip", ] [[package]] name = "core-foundation" -version = "0.9.4" +version = "0.10.0" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "91e195e091a93c46f7102ec7818a2aa394e1e1771c3ab4825963fa03e45afb8f" +checksum = "b55271e5c8c478ad3f38ad24ef34923091e0548492a266d19b3c0b4d82574c63" dependencies = [ "core-foundation-sys", "libc", @@ -831,9 +974,9 @@ dependencies = [ [[package]] name = "cpufeatures" -version = "0.2.14" +version = "0.2.16" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "608697df725056feaccfa42cffdaeeec3fccc4ffc38358ecd19b243e716a78e0" +checksum = "16b80225097f2e5ae4e7179dd2266824648f3e2f49d9134d584b76389d31c4c3" dependencies = [ "libc", ] @@ -864,9 +1007,9 @@ dependencies = [ [[package]] name = "crossbeam-deque" -version = "0.8.5" +version = "0.8.6" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "613f8cc01fe9cf1a3eb3d7f488fd2fa8388403e97039e2f73692932e291a770d" +checksum = "9dd111b7b7f7d55b72c0a6ae361660ee5853c9af73f70c3c2ef6858b950e2e51" dependencies = [ "crossbeam-epoch", "crossbeam-utils", @@ -883,9 +1026,15 @@ dependencies = [ [[package]] name = "crossbeam-utils" -version = "0.8.20" +version = "0.8.21" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "d0a5c400df2834b80a4c3327b3aad3a4c4cd4de0629063962b03235697506a28" + +[[package]] +name = "crunchy" +version = "0.2.2" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "22ec99545bb0ed0ea7bb9b8e1e9122ea386ff8a48c0922e43f36d45ab09e0e80" +checksum = "7a81dae078cea95a014a339291cec439d2f232ebe854a9d672b796c6afafa9b7" [[package]] name = "crypto-bigint" @@ -995,7 +1144,7 @@ source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "97fb8b7c4503de7d6ae7b42ab72a5a59857b4c937ec27a3d4539dba95b5ab2be" dependencies = [ "cfg-if", - "cpufeatures 0.2.14", + "cpufeatures 0.2.16", "curve25519-dalek-derive", "fiat-crypto", "rustc_version", @@ -1011,16 +1160,42 @@ checksum = "f46882e17999c6cc590af592290432be3bce0428cb0d5f8b6715e4dc7b383eb3" dependencies = [ "proc-macro2", "quote", - "syn 2.0.87", + "syn 2.0.91", ] [[package]] -name = "daemonize" -version = "0.5.0" +name = "darling" +version = "0.20.10" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "ab8bfdaacb3c887a54d41bdf48d3af8873b3f5566469f8ba21b92057509f116e" +checksum = "6f63b86c8a8826a49b8c21f08a2d07338eec8d900540f8630dc76284be802989" dependencies = [ - "libc", + "darling_core", + "darling_macro", +] + +[[package]] +name = "darling_core" +version = "0.20.10" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "95133861a8032aaea082871032f5815eb9e98cef03fa916ab4500513994df9e5" +dependencies = [ + "fnv", + "ident_case", + "proc-macro2", + "quote", + "strsim", + "syn 2.0.91", +] + +[[package]] +name = "darling_macro" +version = "0.20.10" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "d336a2a514f6ccccaa3e09b02d41d35330c07ddf03a62165fcec10bb561c7806" +dependencies = [ + "darling_core", + "quote", + "syn 2.0.91", ] [[package]] @@ -1071,20 +1246,7 @@ checksum = "30542c1ad912e0e3d22a1935c290e12e8a29d704a420177a31faad4a601a0800" dependencies = [ "proc-macro2", "quote", - "syn 2.0.87", -] - -[[package]] -name = "derive_more" -version = "0.99.18" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "5f33878137e4dafd7fa914ad4e259e18a4e8e532b9617a2d0150262bf53abfce" -dependencies = [ - "convert_case", - "proc-macro2", - "quote", - "rustc_version", - "syn 2.0.87", + "syn 2.0.91", ] [[package]] @@ -1112,7 +1274,7 @@ dependencies = [ "serde", "serde_json", "sha2 0.8.2", - "thiserror", + "thiserror 1.0.69", "uuid", "x25519-dalek 1.1.1", ] @@ -1165,6 +1327,16 @@ dependencies = [ "dirs-sys 0.4.1", ] +[[package]] +name = "dirs-next" +version = "2.0.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "b98cf8ebf19c3d1b223e151f99a4f9f0690dca41414773390fc824184ac833e1" +dependencies = [ + "cfg-if", + "dirs-sys-next", +] + [[package]] name = "dirs-sys" version = "0.3.7" @@ -1188,6 +1360,17 @@ dependencies = [ "windows-sys 0.48.0", ] +[[package]] +name = "dirs-sys-next" +version = "0.1.2" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "4ebda144c4fe02d1f7ea1a7d9641b6fc6b580adcfa024ae48797ecdeb6825b4d" +dependencies = [ + "libc", + "redox_users", + "winapi", +] + [[package]] name = "displaydoc" version = "0.2.5" @@ -1196,7 +1379,7 @@ checksum = "97369cbbc041bc366949bc74d34658d6cda5621039731c6310521892a3a20ae0" dependencies = [ "proc-macro2", "quote", - "syn 2.0.87", + "syn 2.0.91", ] [[package]] @@ -1299,19 +1482,19 @@ dependencies = [ ] [[package]] -name = "encoding_rs" -version = "0.8.35" +name = "ena" +version = "0.14.3" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "75030f3c4f45dafd7586dd6780965a8c7e8e285a5ecb86713e63a79c5b2766f3" +checksum = "3d248bdd43ce613d87415282f69b9bb99d947d290b10962dd6c56233312c2ad5" dependencies = [ - "cfg-if", + "log", ] [[package]] name = "env_filter" -version = "0.1.2" +version = "0.1.3" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "4f2c92ceda6ceec50f43169f9ee8424fe2db276791afde7b2cd8bc084cb376ab" +checksum = "186e05a59d4c50738528153b83b0b0194d3a29507dfec16eccd4b342903397d0" dependencies = [ "log", "regex", @@ -1332,9 +1515,9 @@ dependencies = [ [[package]] name = "env_logger" -version = "0.11.5" +version = "0.11.6" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "e13fa619b91fb2381732789fc5de83b45675e882f66623b7d8cb4f643017018d" +checksum = "dcaee3d8e3cfc3fd92428d477bc97fc29ec8716d180c0d74c643bb26166660e0" dependencies = [ "anstream", "anstyle", @@ -1349,6 +1532,43 @@ version = "1.0.1" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "5443807d6dff69373d433ab9ef5378ad8df50ca6298caf15de6e52e24aaf54d5" +[[package]] +name = "errno" +version = "0.3.10" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "33d852cb9b869c2a9b3df2f71a3074817f01e1844f839a144f5fcef059a4eb5d" +dependencies = [ + "libc", + "windows-sys 0.59.0", +] + +[[package]] +name = "event-listener" +version = "2.5.3" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "0206175f82b8d6bf6652ff7d71a1e27fd2e4efde587fd368662814d6ec1d9ce0" + +[[package]] +name = "event-listener" +version = "5.3.1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "6032be9bd27023a771701cc49f9f053c751055f71efb2e0ae5c15809093675ba" +dependencies = [ + "concurrent-queue", + "parking", + "pin-project-lite", +] + +[[package]] +name = "event-listener-strategy" +version = "0.5.3" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "3c3e4e0dd3673c1139bf041f3008816d9cf2946bbfac2945c09e523b8d7b05b2" +dependencies = [ + "event-listener 5.3.1", + "pin-project-lite", +] + [[package]] name = "failure" version = "0.1.8" @@ -1377,6 +1597,12 @@ version = "0.1.2" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "e88a8acf291dafb59c2d96e8f59828f3838bb1a70398823ade51a84de6a6deed" +[[package]] +name = "fastrand" +version = "2.3.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "37909eebbb50d72f9059c3b6d82c0463f2ff062c9e95845c43a6c9c0355411be" + [[package]] name = "ff" version = "0.10.1" @@ -1404,24 +1630,41 @@ source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "28dea519a9695b9977216879a3ebfddf92f1c08c05d984f8996aecd6ecdc811d" [[package]] -name = "flate2" -version = "1.0.34" +name = "filename" +version = "0.1.1" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "a1b589b4dc103969ad3cf85c950899926ec64300a1a46d76c03a6072957036f0" +checksum = "63e4df03effebdf9cfa31a663f569fd96cc7e206184b0d4dcd388dc490f7ebe8" dependencies = [ - "crc32fast", - "miniz_oxide", + "libc", + "winapi", ] [[package]] -name = "flume" -version = "0.11.1" +name = "filetime" +version = "0.2.25" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "da0e4dd2a88388a1f4ccc7c9ce104604dab68d9f408dc34cd45823d5a9069095" +checksum = "35c0522e981e68cbfa8c3f978441a5f34b30b96e146b33cd3359176b50fe8586" dependencies = [ - "futures-core", - "futures-sink", - "spin", + "cfg-if", + "libc", + "libredox", + "windows-sys 0.59.0", +] + +[[package]] +name = "fixedbitset" +version = "0.4.2" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "0ce7134b9999ecaf8bcd65542e436736ef32ddca1b3e06094cb6ec5755203b80" + +[[package]] +name = "flate2" +version = "1.0.35" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "c936bfdafb507ebbf50b8074c54fa31c5be9a1e7e5f467dd659697041407d07c" +dependencies = [ + "crc32fast", + "miniz_oxide", ] [[package]] @@ -1439,6 +1682,25 @@ dependencies = [ "percent-encoding", ] +[[package]] +name = "fs2" +version = "0.4.3" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "9564fc758e15025b46aa6643b1b77d047d1a56a1aea6e01002ac0c7026876213" +dependencies = [ + "libc", + "winapi", +] + +[[package]] +name = "fsevent-sys" +version = "4.1.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "76ee7a02da4d231650c7cea31349b889be2f45ddb3ef3032d2ec8185f6313fd2" +dependencies = [ + "libc", +] + [[package]] name = "futures" version = "0.3.31" @@ -1487,6 +1749,19 @@ version = "0.3.31" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "9e5c1b78ca4aae1ac06c48a526a655760685149f0d465d21f37abfe57ce075c6" +[[package]] +name = "futures-lite" +version = "2.5.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "cef40d21ae2c515b51041df9ed313ed21e572df340ea58a922a0aefe7e8891a1" +dependencies = [ + "fastrand", + "futures-core", + "futures-io", + "parking", + "pin-project-lite", +] + [[package]] name = "futures-macro" version = "0.3.31" @@ -1495,7 +1770,7 @@ checksum = "162ee34ebcb7c64a8abebc059ce0fee27c2262618d7b60ed8faf72fef13c3650" dependencies = [ "proc-macro2", "quote", - "syn 2.0.87", + "syn 2.0.91", ] [[package]] @@ -1600,7 +1875,7 @@ version = "0.19.0" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "b903b73e45dc0c6c596f2d37eccece7c1c8bb6e4407b001096387c63d0d93724" dependencies = [ - "bitflags", + "bitflags 2.6.0", "libc", "libgit2-sys", "log", @@ -1613,6 +1888,18 @@ version = "0.3.1" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "d2fabcfbdc87f4758337ca535fb41a6d701b65693ce38287d856d1674551ec9b" +[[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 = "group" version = "0.10.0" @@ -1637,16 +1924,16 @@ dependencies = [ [[package]] name = "h2" -version = "0.3.26" +version = "0.4.7" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "81fe527a889e1532da5c525686d96d4c2e74cdd345badf8dfef9f6b39dd5f5e8" +checksum = "ccae279728d634d083c00f6099cb58f01cc99c145b84b8be2f6c74618d79922e" dependencies = [ + "atomic-waker", "bytes", "fnv", "futures-core", "futures-sink", - "futures-util", - "http 0.2.12", + "http 1.2.0", "indexmap", "slab", "tokio", @@ -1656,9 +1943,9 @@ dependencies = [ [[package]] name = "hashbrown" -version = "0.15.1" +version = "0.15.2" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "3a9bfc1af68b1726ea47d3d5109de126281def866b33970e10fbab11b5dafab3" +checksum = "bf151400ff0baff5465007dd2f3e717f3fe502074ca563069ce3a6629d07b289" [[package]] name = "heck" @@ -1677,9 +1964,9 @@ dependencies = [ [[package]] name = "hermit-abi" -version = "0.3.9" +version = "0.4.0" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "d231dfb89cfffdbc30e7fc41579ed6066ad03abda9e567ccafae602b97ec5024" +checksum = "fbf6a919d6cf397374f7dfeeea91d974c7c0a7221d0d0f4f20d859d329e53fcc" [[package]] name = "hex" @@ -1764,6 +2051,17 @@ dependencies = [ "itoa", ] +[[package]] +name = "http-body" +version = "0.4.6" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "7ceab25649e9960c0311ea418d17bee82c0dcec1bd053b5f9a66e265a693bed2" +dependencies = [ + "bytes", + "http 0.2.12", + "pin-project-lite", +] + [[package]] name = "http-body" version = "1.0.1" @@ -1783,7 +2081,7 @@ dependencies = [ "bytes", "futures-util", "http 1.2.0", - "http-body", + "http-body 1.0.1", "pin-project-lite", ] @@ -1799,6 +2097,34 @@ version = "1.0.3" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "df3b46402a9d5adb4c86a0cf463f42e19994e3ee891101b1841f30a545cb49a9" +[[package]] +name = "httpmock" +version = "0.7.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "08ec9586ee0910472dec1a1f0f8acf52f0fdde93aea74d70d4a3107b4be0fd5b" +dependencies = [ + "assert-json-diff", + "async-object-pool", + "async-std", + "async-trait", + "base64 0.21.7", + "basic-cookies", + "crossbeam-utils", + "form_urlencoded", + "futures-util", + "hyper 0.14.32", + "lazy_static", + "levenshtein", + "log", + "regex", + "serde", + "serde_json", + "serde_regex", + "similar", + "tokio", + "url", +] + [[package]] name = "humantime" version = "2.1.0" @@ -1807,16 +2133,41 @@ checksum = "9a3a5bfb195931eeb336b2a7b4d761daec841b97f947d34394601737a7bba5e4" [[package]] name = "hyper" -version = "1.5.0" +version = "0.14.32" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "41dfc780fdec9373c01bae43289ea34c972e40ee3c9f6b3c8801a35f35586ce7" +dependencies = [ + "bytes", + "futures-channel", + "futures-core", + "futures-util", + "http 0.2.12", + "http-body 0.4.6", + "httparse", + "httpdate", + "itoa", + "pin-project-lite", + "socket2", + "tokio", + "tower-service", + "tracing", + "want", +] + +[[package]] +name = "hyper" +version = "1.5.2" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "bbbff0a806a4728c99295b254c8838933b5b082d75e3cb70c8dab21fdfbcfa9a" +checksum = "256fb8d4bd6413123cc9d91832d78325c48ff41677595be797d90f42969beae0" dependencies = [ "bytes", "futures-channel", "futures-util", + "h2", "http 1.2.0", - "http-body", + "http-body 1.0.1", "httparse", + "httpdate", "itoa", "pin-project-lite", "smallvec", @@ -1826,19 +2177,19 @@ dependencies = [ [[package]] name = "hyper-rustls" -version = "0.27.3" +version = "0.27.5" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "08afdbb5c31130e3034af566421053ab03787c640246a446327f550d11bcb333" +checksum = "2d191583f3da1305256f22463b9bb0471acad48a4e534a5218b9963e9c1f59b2" dependencies = [ "futures-util", "http 1.2.0", - "hyper", + "hyper 1.5.2", "hyper-util", - "rustls 0.23.16", - "rustls-native-certs 0.8.0", + "rustls", + "rustls-native-certs", "rustls-pki-types", "tokio", - "tokio-rustls 0.26.0", + "tokio-rustls", "tower-service", ] @@ -1852,8 +2203,8 @@ dependencies = [ "futures-channel", "futures-util", "http 1.2.0", - "http-body", - "hyper", + "http-body 1.0.1", + "hyper 1.5.2", "pin-project-lite", "socket2", "tokio", @@ -1861,6 +2212,20 @@ dependencies = [ "tracing", ] +[[package]] +name = "hyperlocal" +version = "0.9.0-alpha" +source = "git+https://github.com/softprops/hyperlocal.git?rev=34dc857#34dc8579d74f96b68ddbd55582c76019ae18cfdc" +dependencies = [ + "hex", + "http-body-util", + "hyper 1.5.2", + "hyper-util", + "pin-project-lite", + "tokio", + "tower-service", +] + [[package]] name = "iana-time-zone" version = "0.1.61" @@ -1999,7 +2364,23 @@ checksum = "1ec89e9337638ecdc08744df490b221a7399bf8d164eb52a665454e60e075ad6" dependencies = [ "proc-macro2", "quote", - "syn 2.0.87", + "syn 2.0.91", +] + +[[package]] +name = "ident_case" +version = "1.0.1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "b9e0384b61958566e926dc50660321d12159025e767c18e043daf26b70104c39" + +[[package]] +name = "idna" +version = "0.5.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "634d9b1461af396cad843f47fdba5597a4f9e6ddd4bfb6ff5d85028c25cb12f6" +dependencies = [ + "unicode-bidi", + "unicode-normalization", ] [[package]] @@ -2023,22 +2404,36 @@ dependencies = [ "icu_properties", ] -[[package]] -name = "impl-more" -version = "0.1.8" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "aae21c3177a27788957044151cc2800043d127acaa460a47ebb9b84dfa2c6aa0" - [[package]] name = "indexmap" -version = "2.6.0" +version = "2.7.0" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "707907fe3c25f5424cce2cb7e1cbcafee6bdbe735ca90ef77c29e84591e5b9da" +checksum = "62f822373a4fe84d4bb149bf54e584a7f4abec90e072ed49cda0edea5b95471f" dependencies = [ "equivalent", "hashbrown", ] +[[package]] +name = "inotify" +version = "0.10.2" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "fdd168d97690d0b8c412d6b6c10360277f4d7ee495c5d0d5d5fe0854923255cc" +dependencies = [ + "bitflags 1.3.2", + "inotify-sys", + "libc", +] + +[[package]] +name = "inotify-sys" +version = "0.1.5" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "e05c02b5e89bff3b946cedeca278abc628fe811e604f027c45a8aa3cf793d0eb" +dependencies = [ + "libc", +] + [[package]] name = "inout" version = "0.1.3" @@ -2048,6 +2443,15 @@ dependencies = [ "generic-array 0.14.7", ] +[[package]] +name = "instant" +version = "0.1.13" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "e0242819d153cba4b4b05a5a8f2a7e9bbf97b6055b2a002b395c96b5ff3c0222" +dependencies = [ + "cfg-if", +] + [[package]] name = "ipnet" version = "2.10.1" @@ -2056,9 +2460,9 @@ checksum = "ddc24109865250148c2e0f3d25d4f0f479571723792d3802153c60922a4fb708" [[package]] name = "is_debug" -version = "1.0.1" +version = "1.0.2" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "06d198e9919d9822d5f7083ba8530e04de87841eaf21ead9af8f2304efd57c89" +checksum = "e8ea828c9d6638a5bd3d8b14e37502b4d56cae910ccf8a5b7f51c7a0eb1d0508" [[package]] name = "is_terminal_polyfill" @@ -2066,11 +2470,20 @@ version = "1.70.1" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "7943c866cc5cd64cbc25b2e01621d07fa8eb2a1a23160ee81ce38704e97b8ecf" +[[package]] +name = "itertools" +version = "0.11.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "b1c173a5686ce8bfa551b3563d0c2170bf24ca44da99c7ca4bfdab5418c3fe57" +dependencies = [ + "either", +] + [[package]] name = "itoa" -version = "1.0.11" +version = "1.0.14" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "49f1f14873335454500d59611f1cf4a4b0f786f9ac11f4312a78e4cf2566695b" +checksum = "d75a2a4b1b190afb6f5425f10f6a8f959d2ea0b9c2b1d79553551850539e4674" [[package]] name = "jobserver" @@ -2083,10 +2496,11 @@ dependencies = [ [[package]] name = "js-sys" -version = "0.3.72" +version = "0.3.76" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "6a88f1bda2bd75b0452a14784937d796722fdebfe50df998aeb3f0b7603019a9" +checksum = "6717b6b5b077764fb5966237269cb3c64edddde4b14ce42647430a78ced9e7b7" dependencies = [ + "once_cell", "wasm-bindgen", ] @@ -2123,14 +2537,80 @@ version = "0.1.5" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "ecc2af9a1119c51f12a14607e783cb977bde58bc069ff0c3da1095e635d70654" dependencies = [ - "cpufeatures 0.2.14", + "cpufeatures 0.2.16", +] + +[[package]] +name = "kqueue" +version = "1.0.8" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "7447f1ca1b7b563588a205fe93dea8df60fd981423a768bc1c0ded35ed147d0c" +dependencies = [ + "kqueue-sys", + "libc", +] + +[[package]] +name = "kqueue-sys" +version = "1.0.4" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "ed9625ffda8729b85e45cf04090035ac368927b8cebc34898e7c120f52e4838b" +dependencies = [ + "bitflags 1.3.2", + "libc", +] + +[[package]] +name = "kv-log-macro" +version = "1.0.7" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "0de8b303297635ad57c9f5059fd9cee7a47f8e8daa09df0fcd07dd39fb22977f" +dependencies = [ + "log", +] + +[[package]] +name = "lalrpop" +version = "0.20.2" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "55cb077ad656299f160924eb2912aa147d7339ea7d69e1b5517326fdcec3c1ca" +dependencies = [ + "ascii-canvas", + "bit-set", + "ena", + "itertools", + "lalrpop-util", + "petgraph", + "pico-args", + "regex", + "regex-syntax", + "string_cache", + "term", + "tiny-keccak", + "unicode-xid", + "walkdir", +] + +[[package]] +name = "lalrpop-util" +version = "0.20.2" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "507460a910eb7b32ee961886ff48539633b788a36b65692b95f225b844c82553" +dependencies = [ + "regex-automata", ] [[package]] -name = "language-tags" -version = "0.3.2" +name = "lazy_static" +version = "1.5.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "bbd2bcb4c963f2ddae06a2efc7e9f3591312473c50c6685e1f298068316e66fe" + +[[package]] +name = "levenshtein" +version = "1.0.5" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "d4345964bb142484797b161f473a503a434de77149dd8c7427788c6e13379388" +checksum = "db13adb97ab515a3691f56e4dbab09283d0b86cb45abd991d8634a9d6f501760" [[package]] name = "libaes" @@ -2140,9 +2620,9 @@ checksum = "884609db30d6c40a6bd8e0e22b0b5002546e4dcb53295d33fc700b1644b8a656" [[package]] name = "libc" -version = "0.2.161" +version = "0.2.169" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "8e9489c2807c139ffd9c1794f4af0ebe86a828db53ecdc7fea2111d0fed085d1" +checksum = "b5aba8db14291edd000dfcc4d620c7ebfb122c613afb886ca8803fa4e128a20a" [[package]] name = "libgit2-sys" @@ -2162,8 +2642,9 @@ version = "0.1.3" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "c0ff37bd590ca25063e35af745c343cb7a0271906fb7b37e4813e8f79f00268d" dependencies = [ - "bitflags", + "bitflags 2.6.0", "libc", + "redox_syscall", ] [[package]] @@ -2179,27 +2660,16 @@ dependencies = [ ] [[package]] -name = "litemap" -version = "0.7.3" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "643cb0b8d4fcc284004d5fd0d67ccf61dfffadb7f75e1e71bc420f4688a3a704" - -[[package]] -name = "local-channel" -version = "0.1.5" +name = "linux-raw-sys" +version = "0.4.14" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "b6cbc85e69b8df4b8bb8b89ec634e7189099cea8927a276b7384ce5488e53ec8" -dependencies = [ - "futures-core", - "futures-sink", - "local-waker", -] +checksum = "78b3ae25bc7c8c38cec158d1f2757ee79e9b3740fbc7ccf0e59e4b08d793fa89" [[package]] -name = "local-waker" -version = "0.1.4" +name = "litemap" +version = "0.7.4" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "4d873d7c67ce09b42110d801813efbc9364414e356be9935700d368351657487" +checksum = "4ee93343901ab17bd981295f2cf0026d4ad018c7c31ba84549a4ddbb47a45104" [[package]] name = "lock_api" @@ -2222,6 +2692,9 @@ name = "log" version = "0.4.22" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "a7a70ba024b9dc04c27ea2f0c0548feb474ec5c54bba33a7f72f873a39d07b24" +dependencies = [ + "value-bag", +] [[package]] name = "lzma-rs" @@ -2243,6 +2716,12 @@ dependencies = [ "winapi", ] +[[package]] +name = "matchit" +version = "0.7.3" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "0e7465ac9959cc2b1404e8e2367b43684a6d13790fe23056cc8c6c5a6b7bcb94" + [[package]] name = "memchr" version = "2.7.4" @@ -2266,33 +2745,62 @@ checksum = "6877bb514081ee2a7ff5ef9de3281f14a4dd4bceac4c09388074a6b5df8a139a" [[package]] name = "miniz_oxide" -version = "0.8.0" +version = "0.8.2" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "e2d80299ef12ff69b16a84bb182e3b9df68b5a91574d3d4fa6e41b65deec4df1" +checksum = "4ffbe83022cedc1d264172192511ae958937694cd57ce297164951b8b3568394" dependencies = [ "adler2", ] [[package]] name = "mio" -version = "1.0.2" +version = "1.0.3" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "80e04d1dcff3aae0704555fe5fee3bcfaf3d1fdf8a7e521d5b9d2b42acb52cec" +checksum = "2886843bf800fba2e3377cff24abf6379b4c4d5c6681eaf9ea5b0d15090450bd" dependencies = [ - "hermit-abi 0.3.9", "libc", "log", "wasi 0.11.0+wasi-snapshot-preview1", "windows-sys 0.52.0", ] +[[package]] +name = "mockito" +version = "1.6.1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "652cd6d169a36eaf9d1e6bce1a221130439a966d7f27858af66a33a66e9c4ee2" +dependencies = [ + "assert-json-diff", + "bytes", + "colored", + "futures-util", + "http 1.2.0", + "http-body 1.0.1", + "http-body-util", + "hyper 1.5.2", + "hyper-util", + "log", + "rand 0.8.5", + "regex", + "serde_json", + "serde_urlencoded", + "similar", + "tokio", +] + +[[package]] +name = "new_debug_unreachable" +version = "1.0.6" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "650eef8c711430f1a879fdd01d4745a7deea475becfb90269c06775983bbf086" + [[package]] name = "nix" version = "0.28.0" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "ab2156c4fce2f8df6c499cc1c763e4394b7482525bf2a9701c9d79d215f519e4" dependencies = [ - "bitflags", + "bitflags 2.6.0", "cfg-if", "cfg_aliases 0.1.1", "libc", @@ -2305,10 +2813,39 @@ version = "0.29.0" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "71e2746dc3a24dd78b3cfcb7be93368c6de9963d30f43a6a73998a9cf4b17b46" dependencies = [ - "bitflags", + "bitflags 2.6.0", "cfg-if", "cfg_aliases 0.2.1", "libc", + "memoffset", +] + +[[package]] +name = "notify" +version = "7.0.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "c533b4c39709f9ba5005d8002048266593c1cfaf3c5f0739d5b8ab0c6c504009" +dependencies = [ + "bitflags 2.6.0", + "filetime", + "fsevent-sys", + "inotify", + "kqueue", + "libc", + "log", + "mio", + "notify-types", + "walkdir", + "windows-sys 0.52.0", +] + +[[package]] +name = "notify-types" +version = "1.0.1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "585d3cb5e12e01aed9e8a1f70d5c6b5e86fe2a6e48fc8cd0b3e0b8df6f6eb174" +dependencies = [ + "instant", ] [[package]] @@ -2410,9 +2947,9 @@ dependencies = [ [[package]] name = "object" -version = "0.36.5" +version = "0.36.7" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "aedf0a2d09c573ed1d8d85b30c119153926a2b36dce0ab28322c09a117a4683e" +checksum = "62948e14d923ea95ea2c7c86c71013138b66525b86bdc08d2dcc262bdb497b87" dependencies = [ "memchr", ] @@ -2458,6 +2995,12 @@ dependencies = [ "sha2 0.9.9", ] +[[package]] +name = "parking" +version = "2.2.1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "f38d5652c16fde515bb1ecef450ab0f6a219d619a7274976324d5e377f7dceba" + [[package]] name = "parking_lot" version = "0.12.3" @@ -2503,6 +3046,51 @@ version = "2.3.1" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "e3148f5046208a5d56bcfc03053e3ca6334e51da8dfb19b6cdc8b306fae3283e" +[[package]] +name = "petgraph" +version = "0.6.5" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "b4c5cc86750666a3ed20bdaf5ca2a0344f9c67674cae0515bec2da16fbaa47db" +dependencies = [ + "fixedbitset", + "indexmap", +] + +[[package]] +name = "phf_shared" +version = "0.10.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "b6796ad771acdc0123d2a88dc428b5e38ef24456743ddb1744ed628f9815c096" +dependencies = [ + "siphasher", +] + +[[package]] +name = "pico-args" +version = "0.5.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "5be167a7af36ee22fe3115051bc51f6e6c7054c9348e28deb4f49bd6f705a315" + +[[package]] +name = "pin-project" +version = "1.1.7" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "be57f64e946e500c8ee36ef6331845d40a93055567ec57e8fae13efd33759b95" +dependencies = [ + "pin-project-internal", +] + +[[package]] +name = "pin-project-internal" +version = "1.1.7" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "3c0f5fad0874fc7abcd4d750e76917eaebbecaa2c20bde22e1dbeeba8beb758c" +dependencies = [ + "proc-macro2", + "quote", + "syn 2.0.91", +] + [[package]] name = "pin-project-lite" version = "0.2.15" @@ -2515,6 +3103,17 @@ version = "0.1.0" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "8b870d8c151b6f2fb93e84a13146138f05d02ed11c7e7c54f8826aaaf7c9f184" +[[package]] +name = "piper" +version = "0.2.4" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "96c8c490f422ef9a4efd2cb5b42b76c8613d7e7dfc1caf667b8a3350a5acc066" +dependencies = [ + "atomic-waker", + "fastrand", + "futures-io", +] + [[package]] name = "pkcs8" version = "0.7.6" @@ -2541,13 +3140,28 @@ version = "0.3.31" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "953ec861398dccce10c670dfeaf3ec4911ca479e9c02154b3a215178c5f566f2" +[[package]] +name = "polling" +version = "3.7.4" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "a604568c3202727d1507653cb121dbd627a58684eb09a820fd746bee38b4442f" +dependencies = [ + "cfg-if", + "concurrent-queue", + "hermit-abi 0.4.0", + "pin-project-lite", + "rustix", + "tracing", + "windows-sys 0.59.0", +] + [[package]] name = "poly1305" version = "0.7.2" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "048aeb476be11a4b6ca432ca569e375810de9294ae78f4774e78ea98a9246ede" dependencies = [ - "cpufeatures 0.2.14", + "cpufeatures 0.2.16", "opaque-debug 0.3.1", "universal-hash", ] @@ -2559,7 +3173,7 @@ source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "8419d2b623c7c0896ff2d5d96e2cb4ede590fed28fcc34934f4c33c036e620a1" dependencies = [ "cfg-if", - "cpufeatures 0.2.14", + "cpufeatures 0.2.16", "opaque-debug 0.3.1", "universal-hash", ] @@ -2579,6 +3193,12 @@ dependencies = [ "zerocopy", ] +[[package]] +name = "precomputed-hash" +version = "0.1.1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "925383efa346730478fb4838dbe9137d2a47675ad789c546d150a6e1dd4ab31c" + [[package]] name = "proc-macro-crate" version = "3.2.0" @@ -2588,11 +3208,35 @@ dependencies = [ "toml_edit", ] +[[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.89" +version = "1.0.92" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "f139b0662de085916d1fb67d2b4169d1addddda1919e696f3252b740b629986e" +checksum = "37d3544b3f2748c54e147655edb5025752e2303145b5aefb3c3ea2c78b973bb0" dependencies = [ "unicode-ident", ] @@ -2601,7 +3245,6 @@ dependencies = [ name = "protocol" version = "0.2.0" dependencies = [ - "actix-rt", "chrono", "cuid", "data-encoding", @@ -2615,7 +3258,8 @@ dependencies = [ "serde_jcs", "serde_json", "sha2 0.10.8", - "thiserror", + "thiserror 1.0.69", + "tokio", "trait-variant", "x25519-dalek 2.0.1", "zeroize", @@ -2623,44 +3267,47 @@ dependencies = [ [[package]] name = "quinn" -version = "0.11.5" +version = "0.11.6" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "8c7c5fdde3cdae7203427dc4f0a68fe0ed09833edc525a03456b153b79828684" +checksum = "62e96808277ec6f97351a2380e6c25114bc9e67037775464979f3037c92d05ef" dependencies = [ "bytes", "pin-project-lite", "quinn-proto", "quinn-udp", "rustc-hash", - "rustls 0.23.16", + "rustls", "socket2", - "thiserror", + "thiserror 2.0.9", "tokio", "tracing", ] [[package]] name = "quinn-proto" -version = "0.11.8" +version = "0.11.9" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "fadfaed2cd7f389d0161bb73eeb07b7b78f8691047a6f3e73caaeae55310a4a6" +checksum = "a2fe5ef3495d7d2e377ff17b1a8ce2ee2ec2a18cde8b6ad6619d65d0701c135d" dependencies = [ "bytes", + "getrandom 0.2.15", "rand 0.8.5", "ring", "rustc-hash", - "rustls 0.23.16", + "rustls", + "rustls-pki-types", "slab", - "thiserror", + "thiserror 2.0.9", "tinyvec", "tracing", + "web-time", ] [[package]] name = "quinn-udp" -version = "0.5.7" +version = "0.5.9" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "7d5a626c6807713b15cac82a6acaccd6043c9a5408c24baae07611fec3f243da" +checksum = "1c40286217b4ba3a71d644d752e6a0b71f13f1b6a2c5311acfcbe0c2418ed904" dependencies = [ "cfg_aliases 0.2.1", "libc", @@ -2672,9 +3319,9 @@ dependencies = [ [[package]] name = "quote" -version = "1.0.37" +version = "1.0.38" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "b5b9d34b8991d19d98081b46eacdd8eb58c6f2b201139f7c5f643cc155a633af" +checksum = "0e4dccaaaf89514f546c693ddc140f729f958c247918a13380cccc6078391acc" dependencies = [ "proc-macro2", ] @@ -2772,11 +3419,11 @@ dependencies = [ [[package]] name = "redox_syscall" -version = "0.5.7" +version = "0.5.8" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "9b6dfecf2c74bce2466cabf93f6664d6998a69eb21e39f4207930065b27b771f" +checksum = "03a862b389f93e68874fbf580b9de08dd02facb9a788ebadaf4a3fd33cf58834" dependencies = [ - "bitflags", + "bitflags 2.6.0", ] [[package]] @@ -2787,7 +3434,7 @@ checksum = "ba009ff324d1fc1b900bd1fdb31564febe58a8ccc8a6fdbb93b543d33b13ca43" dependencies = [ "getrandom 0.2.15", "libredox", - "thiserror", + "thiserror 1.0.69", ] [[package]] @@ -2804,21 +3451,15 @@ dependencies = [ [[package]] name = "regex-automata" -version = "0.4.8" +version = "0.4.9" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "368758f23274712b504848e9d5a6f010445cc8b87a7cdb4d7cbee666c1288da3" +checksum = "809e8dc61f6de73b46c85f4c96486310fe304c434cfa43669d7b40f711150908" dependencies = [ "aho-corasick", "memchr", "regex-syntax", ] -[[package]] -name = "regex-lite" -version = "0.1.6" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "53a49587ad06b26609c52e423de037e7f57f20d53535d66e08c695f347df952a" - [[package]] name = "regex-syntax" version = "0.8.5" @@ -2842,9 +3483,9 @@ dependencies = [ "futures-core", "futures-util", "http 1.2.0", - "http-body", + "http-body 1.0.1", "http-body-util", - "hyper", + "hyper 1.5.2", "hyper-rustls", "hyper-util", "ipnet", @@ -2855,8 +3496,8 @@ dependencies = [ "percent-encoding", "pin-project-lite", "quinn", - "rustls 0.23.16", - "rustls-native-certs 0.8.0", + "rustls", + "rustls-native-certs", "rustls-pemfile", "rustls-pki-types", "serde", @@ -2864,7 +3505,7 @@ dependencies = [ "serde_urlencoded", "sync_wrapper", "tokio", - "tokio-rustls 0.26.0", + "tokio-rustls", "tower-service", "url", "wasm-bindgen", @@ -2924,26 +3565,8 @@ dependencies = [ "regex", "relative-path", "rustc_version", - "syn 2.0.87", - "unicode-ident", -] - -[[package]] -name = "rumqttc" -version = "0.24.0" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "e1568e15fab2d546f940ed3a21f48bbbd1c494c90c99c4481339364a497f94a9" -dependencies = [ - "bytes", - "flume", - "futures-util", - "log", - "rustls-native-certs 0.7.3", - "rustls-pemfile", - "rustls-webpki", - "thiserror", - "tokio", - "tokio-rustls 0.25.0", + "syn 2.0.91", + "unicode-ident", ] [[package]] @@ -2954,9 +3577,9 @@ checksum = "719b953e2095829ee67db738b3bfa9fa368c94900df327b3f07fe6e794d2fe1f" [[package]] name = "rustc-hash" -version = "2.0.0" +version = "2.1.0" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "583034fd73374156e66797ed8e5b0d5690409c9226b22d87cb7f19821c05d152" +checksum = "c7fb8039b3032c191086b10f11f319a6e99e1e82889c5cc6046f515c9db1d497" [[package]] name = "rustc_version" @@ -2968,24 +3591,23 @@ dependencies = [ ] [[package]] -name = "rustls" -version = "0.22.4" +name = "rustix" +version = "0.38.42" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "bf4ef73721ac7bcd79b2b315da7779d8fc09718c6b3d2d1b2d94850eb8c18432" +checksum = "f93dc38ecbab2eb790ff964bb77fa94faf256fd3e73285fd7ba0903b76bedb85" dependencies = [ - "log", - "ring", - "rustls-pki-types", - "rustls-webpki", - "subtle", - "zeroize", + "bitflags 2.6.0", + "errno", + "libc", + "linux-raw-sys", + "windows-sys 0.59.0", ] [[package]] name = "rustls" -version = "0.23.16" +version = "0.23.20" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "eee87ff5d9b36712a58574e12e9f0ea80f915a5b0ac518d322b24a465617925e" +checksum = "5065c3f250cbd332cd894be57c40fa52387247659b14a2d6041d121547903b1b" dependencies = [ "once_cell", "ring", @@ -2997,25 +3619,11 @@ dependencies = [ [[package]] name = "rustls-native-certs" -version = "0.7.3" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "e5bfb394eeed242e909609f56089eecfe5fda225042e8b171791b9c95f5931e5" -dependencies = [ - "openssl-probe", - "rustls-pemfile", - "rustls-pki-types", - "schannel", - "security-framework", -] - -[[package]] -name = "rustls-native-certs" -version = "0.8.0" +version = "0.8.1" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "fcaf18a4f2be7326cd874a5fa579fae794320a0f388d365dca7e480e55f83f8a" +checksum = "7fcff2dd52b58a8d98a70243663a0d234c4e2b79235637849d15913394a247d3" dependencies = [ "openssl-probe", - "rustls-pemfile", "rustls-pki-types", "schannel", "security-framework", @@ -3032,9 +3640,12 @@ dependencies = [ [[package]] name = "rustls-pki-types" -version = "1.10.0" +version = "1.10.1" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "16f1201b3c9a7ee8039bcadc17b7e605e2945b27eee7631788c1bd2b0643674b" +checksum = "d2bf47e6ff922db3825eb750c4e2ff784c6ff8fb9e13046ef6a1d1c5401b0b37" +dependencies = [ + "web-time", +] [[package]] name = "rustls-webpki" @@ -3047,6 +3658,12 @@ dependencies = [ "untrusted", ] +[[package]] +name = "rustversion" +version = "1.0.18" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "0e819f2bc632f285be6d7cd36e25940d45b2391dd6d9b939e79de557f7014248" + [[package]] name = "ryu" version = "1.0.18" @@ -3059,11 +3676,29 @@ version = "0.2.2" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "6518fc26bced4d53678a22d6e423e9d8716377def84545fe328236e3af070e7f" +[[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 = "scc" +version = "2.2.6" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "94b13f8ea6177672c49d12ed964cca44836f59621981b04a3e26b87e675181de" +dependencies = [ + "sdd", +] + [[package]] name = "schannel" -version = "0.1.26" +version = "0.1.27" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "01227be5826fa0690321a2ba6c5cd57a19cf3f6a09e76973b58e61de6ab9d1c1" +checksum = "1f29ebaa345f945cec9fbbc532eb307f0fdad8161f281b6369539c8d84876b3d" dependencies = [ "windows-sys 0.59.0", ] @@ -3074,6 +3709,12 @@ version = "1.2.0" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "94143f37725109f92c262ed2cf5e59bce7498c01bcc1502d7b9afe439a4e9f49" +[[package]] +name = "sdd" +version = "3.0.5" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "478f121bb72bbf63c52c93011ea1791dca40140dfe13f8336c4c5ac952c33aa9" + [[package]] name = "sec1" version = "0.7.3" @@ -3091,11 +3732,11 @@ dependencies = [ [[package]] name = "security-framework" -version = "2.11.1" +version = "3.1.0" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "897b2245f0b511c87893af39b033e5ca9cce68824c4d7e7630b5a1d339658d02" +checksum = "81d3f8c9bfcc3cbb6b0179eb57042d75b1582bdc65c3cb95f3fa999509c03cbc" dependencies = [ - "bitflags", + "bitflags 2.6.0", "core-foundation", "core-foundation-sys", "libc", @@ -3104,9 +3745,9 @@ dependencies = [ [[package]] name = "security-framework-sys" -version = "2.12.0" +version = "2.13.0" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "ea4a292869320c0272d7bc55a5a6aafaff59b4f63404a003887b679a2e05b4b6" +checksum = "1863fd3768cd83c56a7f60faa4dc0d403f1b6df0a38c3c25f44b7894e45370d5" dependencies = [ "core-foundation-sys", "libc", @@ -3114,28 +3755,31 @@ dependencies = [ [[package]] name = "semver" -version = "1.0.23" +version = "1.0.24" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "61697e0a1c7e512e84a621326239844a24d8207b4669b41bc18b32ea5cbf988b" +checksum = "3cb6eb87a131f756572d7fb904f6e7b68633f09cca868c5df1c4b8d1a694bbba" +dependencies = [ + "serde", +] [[package]] name = "serde" -version = "1.0.215" +version = "1.0.216" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "6513c1ad0b11a9376da888e3e0baa0077f1aed55c17f50e7b2397136129fb88f" +checksum = "0b9781016e935a97e8beecf0c933758c97a5520d32930e460142b4cd80c6338e" dependencies = [ "serde_derive", ] [[package]] name = "serde_derive" -version = "1.0.215" +version = "1.0.216" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "ad1e866f866923f252f05c889987993144fb74e722403468a4ebd70c3cd756c0" +checksum = "46f859dbbf73865c6627ed570e78961cd3ac92407a2d117204c49232485da55e" dependencies = [ "proc-macro2", "quote", - "syn 2.0.87", + "syn 2.0.91", ] [[package]] @@ -3151,9 +3795,9 @@ dependencies = [ [[package]] name = "serde_json" -version = "1.0.133" +version = "1.0.134" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "c7fceb2473b9166b2294ef05efcb65a3db80803f0b03ef86a5fc88a2b85ee377" +checksum = "d00f4175c42ee48b15416f6193a959ba3a0d67fc699a0db9ad12df9f83991c7d" dependencies = [ "itoa", "memchr", @@ -3161,6 +3805,26 @@ dependencies = [ "serde", ] +[[package]] +name = "serde_path_to_error" +version = "0.1.16" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "af99884400da37c88f5e9146b7f1fd0fbcae8f6eec4e9da38b67d05486f814a6" +dependencies = [ + "itoa", + "serde", +] + +[[package]] +name = "serde_regex" +version = "1.1.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "a8136f1a4ea815d7eac4101cfd0b16dc0cb5e1fe1b8609dfd728058656b7badf" +dependencies = [ + "regex", + "serde", +] + [[package]] name = "serde_urlencoded" version = "0.7.1" @@ -3196,6 +3860,31 @@ dependencies = [ "serde", ] +[[package]] +name = "serial_test" +version = "3.2.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "1b258109f244e1d6891bf1053a55d63a5cd4f8f4c30cf9a1280989f80e7a1fa9" +dependencies = [ + "futures", + "log", + "once_cell", + "parking_lot", + "scc", + "serial_test_derive", +] + +[[package]] +name = "serial_test_derive" +version = "3.2.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "5d69265a08751de7844521fd15003ae0a888e035773ba05695c5c759a6f89eef" +dependencies = [ + "proc-macro2", + "quote", + "syn 2.0.91", +] + [[package]] name = "sha1" version = "0.10.6" @@ -3203,7 +3892,7 @@ source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "e3bf829a2d51ab4a5ddf1352d8470c140cadc8301b2ae1789db023f01cedd6ba" dependencies = [ "cfg-if", - "cpufeatures 0.2.14", + "cpufeatures 0.2.16", "digest 0.10.7", ] @@ -3227,7 +3916,7 @@ checksum = "4d58a1e1bf39749807d89cf2d98ac2dfa0ff1cb3faa38fbb64dd88ac8013d800" dependencies = [ "block-buffer 0.9.0", "cfg-if", - "cpufeatures 0.2.14", + "cpufeatures 0.2.16", "digest 0.9.0", "opaque-debug 0.3.1", ] @@ -3239,7 +3928,7 @@ source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "793db75ad2bcafc3ffa7c68b215fee268f537982cd901d132f89c6343f3a3dc8" dependencies = [ "cfg-if", - "cpufeatures 0.2.14", + "cpufeatures 0.2.16", "digest 0.10.7", ] @@ -3255,9 +3944,9 @@ dependencies = [ [[package]] name = "shadow-rs" -version = "0.36.0" +version = "0.36.1" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "58cfcd0643497a9f780502063aecbcc4a3212cbe4948fd25ee8fd179c2cf9a18" +checksum = "cd2f59f8b166e94269530e0f47323c8b2a5b2d82ef90363cc7ce1e517e063f78" dependencies = [ "const_format", "git2", @@ -3307,6 +3996,18 @@ version = "0.3.7" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "d66dc143e6b11c1eddc06d5c423cfc97062865baf299914ab64caa38182078fe" +[[package]] +name = "similar" +version = "2.6.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "1de1d4f81173b03af4c0cbed3c898f6bff5b870e4a7f5d6f4057d62a7a4b686e" + +[[package]] +name = "siphasher" +version = "0.3.11" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "38b58827f4464d87d377d175e90bf58eb00fd8716ff0a62f80356b5e61555d0d" + [[package]] name = "slab" version = "0.4.9" @@ -3324,9 +4025,9 @@ checksum = "3c5e1a9a646d36c3599cd173a41282daf47c44583ad367b8e6837255952e5c67" [[package]] name = "socket2" -version = "0.5.7" +version = "0.5.8" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "ce305eb0b4296696835b71df73eb912e0f1ffd2556a501fcede6e0c50349191c" +checksum = "c970269d99b64e60ec3bd6ad27270092a5394c4e309314b18ae3fe575695fbe8" dependencies = [ "libc", "windows-sys 0.52.0", @@ -3337,9 +4038,6 @@ name = "spin" version = "0.9.8" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "6980e8d7511241f8acf4aebddbb1ff938df5eebe98691418c4468d0b72a96a67" -dependencies = [ - "lock_api", -] [[package]] name = "spki" @@ -3366,6 +4064,19 @@ version = "1.2.0" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "a8f112729512f8e442d81f95a8a7ddf2b7c6b8a1a6f509a95864142b30cab2d3" +[[package]] +name = "string_cache" +version = "0.8.7" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "f91138e76242f575eb1d3b38b4f1362f10d3a43f47d182a5b359af488a02293b" +dependencies = [ + "new_debug_unreachable", + "once_cell", + "parking_lot", + "phf_shared", + "precomputed-hash", +] + [[package]] name = "strsim" version = "0.11.1" @@ -3391,9 +4102,9 @@ dependencies = [ [[package]] name = "syn" -version = "2.0.87" +version = "2.0.91" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "25aa4ce346d03a6dcd68dd8b4010bcb74e54e62c90c573f394c46eae99aba32d" +checksum = "d53cbcb5a243bd33b7858b1d7f4aca2153490815872d86d955d6ea29f743c035" dependencies = [ "proc-macro2", "quote", @@ -3402,9 +4113,9 @@ dependencies = [ [[package]] name = "sync_wrapper" -version = "1.0.1" +version = "1.0.2" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "a7065abeca94b6a8a577f9bd45aa0867a2238b74e8eb67cf10d492bc39351394" +checksum = "0bf256ce5efdfa370213c1dabab5935a12e49f2c58d15e9eac2870d3b4f27263" dependencies = [ "futures-core", ] @@ -3429,7 +4140,7 @@ checksum = "c8af7666ab7b6390ab78131fb5b0fce11d6b7a6951602017c35fa82800708971" dependencies = [ "proc-macro2", "quote", - "syn 2.0.87", + "syn 2.0.91", ] [[package]] @@ -3447,6 +4158,41 @@ dependencies = [ "windows 0.52.0", ] +[[package]] +name = "tar" +version = "0.4.43" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "c65998313f8e17d0d553d28f91a0df93e4dbbbf770279c7bc21ca0f09ea1a1f6" +dependencies = [ + "filetime", + "libc", + "xattr", +] + +[[package]] +name = "tempfile" +version = "3.14.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "28cce251fcbc87fac86a866eeb0d6c2d536fc16d06f184bb61aeae11aa4cee0c" +dependencies = [ + "cfg-if", + "fastrand", + "once_cell", + "rustix", + "windows-sys 0.59.0", +] + +[[package]] +name = "term" +version = "0.7.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "c59df8ac95d96ff9bede18eb7300b0fda5e5d8d90960e76f8e14ae765eedbf1f" +dependencies = [ + "dirs-next", + "rustversion", + "winapi", +] + [[package]] name = "termcolor" version = "1.4.1" @@ -3462,7 +4208,16 @@ version = "1.0.69" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "b6aaf5339b578ea85b50e080feb250a3e8ae8cfcdff9a461c9ec2904bc923f52" dependencies = [ - "thiserror-impl", + "thiserror-impl 1.0.69", +] + +[[package]] +name = "thiserror" +version = "2.0.9" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "f072643fd0190df67a8bab670c20ef5d8737177d6ac6b2e9a236cb096206b2cc" +dependencies = [ + "thiserror-impl 2.0.9", ] [[package]] @@ -3473,14 +4228,25 @@ checksum = "4fee6c4efc90059e10f81e6d42c60a18f76588c3d74cb83a0b242a2b6c7504c1" dependencies = [ "proc-macro2", "quote", - "syn 2.0.87", + "syn 2.0.91", +] + +[[package]] +name = "thiserror-impl" +version = "2.0.9" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "7b50fa271071aae2e6ee85f842e2e28ba8cd2c5fb67f11fcb1fd70b276f9e7d4" +dependencies = [ + "proc-macro2", + "quote", + "syn 2.0.91", ] [[package]] name = "time" -version = "0.3.36" +version = "0.3.37" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "5dfd88e563464686c916c7e46e623e520ddc6d79fa6641390f2e3fa86e83e885" +checksum = "35e7868883861bd0e56d9ac6efcaaca0d6d5d82a2a7ec8209ff492c07cf37b21" dependencies = [ "deranged", "itoa", @@ -3501,14 +4267,23 @@ checksum = "ef927ca75afb808a4d64dd374f00a2adf8d0fcff8e7b184af886c3c87ec4a3f3" [[package]] name = "time-macros" -version = "0.2.18" +version = "0.2.19" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "3f252a68540fde3a3877aeea552b832b40ab9a69e318efd078774a01ddee1ccf" +checksum = "2834e6017e3e5e4b9834939793b282bc03b37a3336245fa820e35e233e2a85de" dependencies = [ "num-conv", "time-core", ] +[[package]] +name = "tiny-keccak" +version = "2.0.2" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "2c9d3793400a45f954c52e73d068316d76b6f4e36977e3fcebb13a2721e80237" +dependencies = [ + "crunchy", +] + [[package]] name = "tinystr" version = "0.7.6" @@ -3521,9 +4296,9 @@ dependencies = [ [[package]] name = "tinyvec" -version = "1.8.0" +version = "1.8.1" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "445e881f4f6d382d5f27c034e25eb92edd7c784ceab92a0937db7f2e9471b938" +checksum = "022db8904dfa342efe721985167e9fcd16c29b226db4397ed752a761cfce81e8" dependencies = [ "tinyvec_macros", ] @@ -3560,36 +4335,24 @@ checksum = "693d596312e88961bc67d7f1f97af8a70227d9f90c31bba5806eec004978d752" dependencies = [ "proc-macro2", "quote", - "syn 2.0.87", -] - -[[package]] -name = "tokio-rustls" -version = "0.25.0" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "775e0c0f0adb3a2f22a00c4745d728b479985fc15ee7ca6a2608388c5569860f" -dependencies = [ - "rustls 0.22.4", - "rustls-pki-types", - "tokio", + "syn 2.0.91", ] [[package]] name = "tokio-rustls" -version = "0.26.0" +version = "0.26.1" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "0c7bc40d0e5a97695bb96e27995cd3a08538541b0a846f65bba7a359f36700d4" +checksum = "5f6d0975eaace0cf0fcadee4e4aaa5da15b5c079146f2cffb67c113be122bf37" dependencies = [ - "rustls 0.23.16", - "rustls-pki-types", + "rustls", "tokio", ] [[package]] name = "tokio-util" -version = "0.7.12" +version = "0.7.13" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "61e7c3654c13bcd040d4a03abee2c75b1d14a37b423cf5a813ceae1cc903ec6a" +checksum = "d7fcaa8d55a2bdd6b83ace262b016eca0d79ee02818c5c1bcdf0305114081078" dependencies = [ "bytes", "futures-core", @@ -3624,6 +4387,43 @@ dependencies = [ "winnow", ] +[[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" +version = "0.5.2" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "d039ad9159c98b70ecfd540b2573b97f7f52c3e8d9f8ad57a24b916a536975f9" +dependencies = [ + "futures-core", + "futures-util", + "pin-project-lite", + "sync_wrapper", + "tokio", + "tower-layer", + "tower-service", + "tracing", +] + +[[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" @@ -3632,9 +4432,9 @@ checksum = "8df9b6e13f2d32c91b9bd719c00d1958837bc7dec474d94952798cc8e69eeec3" [[package]] name = "tracing" -version = "0.1.40" +version = "0.1.41" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "c3523ab5a71916ccf420eebdf5521fcef02141234bbc0b8a49f2fdc4544364ef" +checksum = "784e0ac535deb450455cbfa28a6f0df145ea1bb7ae51b821cf5e7927fdcfbdd0" dependencies = [ "log", "pin-project-lite", @@ -3643,9 +4443,9 @@ dependencies = [ [[package]] name = "tracing-core" -version = "0.1.32" +version = "0.1.33" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "c06d3da6113f116aaee68e4d601191614c9053067f9ab7f6edbcb161237daa54" +checksum = "e672c95779cf947c5311f83787af4fa8fffd12fb27e4993211a84bdfd9610f9c" dependencies = [ "once_cell", ] @@ -3658,7 +4458,7 @@ checksum = "70977707304198400eb4835a78f6a9f928bf41bba420deb8fdb175cd965d77a7" dependencies = [ "proc-macro2", "quote", - "syn 2.0.87", + "syn 2.0.91", ] [[package]] @@ -3702,11 +4502,26 @@ dependencies = [ "tz-rs", ] +[[package]] +name = "unicode-bidi" +version = "0.3.18" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "5c1cb5db39152898a79168971543b1cb5020dff7fe43c8dc468b0885f5e29df5" + [[package]] name = "unicode-ident" -version = "1.0.13" +version = "1.0.14" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "adb9e6ca4f869e1180728b7950e35922a7fc6397f7b641499e8f3ef06e50dc83" + +[[package]] +name = "unicode-normalization" +version = "0.1.24" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "e91b56cd4cadaeb79bbf1a5645f6b4f8dc5bde8834ad5894a8db35fda9efa1fe" +checksum = "5033c97c4262335cded6d6fc3e5c18ab755e1a3dc96376350f3d8e9f009ad956" +dependencies = [ + "tinyvec", +] [[package]] name = "unicode-xid" @@ -3743,10 +4558,20 @@ source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "32f8b686cadd1473f4bd0117a5d28d36b1ade384ea9b5069a1c40aefed7fda60" dependencies = [ "form_urlencoded", - "idna", + "idna 1.0.3", "percent-encoding", ] +[[package]] +name = "users" +version = "0.11.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "24cc0f6d6f267b73e5a2cadf007ba8f9bc39c6a6f9666f8cf25ea809a153b032" +dependencies = [ + "libc", + "log", +] + [[package]] name = "utf16_iter" version = "1.0.5" @@ -3785,9 +4610,45 @@ checksum = "6b91f57fe13a38d0ce9e28a03463d8d3c2468ed03d75375110ec71d93b449a08" dependencies = [ "proc-macro2", "quote", - "syn 2.0.87", + "syn 2.0.91", +] + +[[package]] +name = "validator" +version = "0.18.1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "db79c75af171630a3148bd3e6d7c4f42b6a9a014c2945bc5ed0020cbb8d9478e" +dependencies = [ + "idna 0.5.0", + "once_cell", + "regex", + "serde", + "serde_derive", + "serde_json", + "url", + "validator_derive", +] + +[[package]] +name = "validator_derive" +version = "0.18.2" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "df0bcf92720c40105ac4b2dda2a4ea3aa717d4d6a862cc217da653a4bd5c6b10" +dependencies = [ + "darling", + "once_cell", + "proc-macro-error", + "proc-macro2", + "quote", + "syn 2.0.91", ] +[[package]] +name = "value-bag" +version = "1.10.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "3ef4c4aa54d5d05a279399bfa921ec387b7aba77caf7a682ae8d86785b8fdad2" + [[package]] name = "vcpkg" version = "0.2.15" @@ -3800,6 +4661,16 @@ 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" @@ -3823,9 +4694,9 @@ checksum = "9c8d87e72b64a3b4db28d11ce29237c246188f4f51057d65a7eab63b7987e423" [[package]] name = "wasm-bindgen" -version = "0.2.95" +version = "0.2.99" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "128d1e363af62632b8eb57219c8fd7877144af57558fb2ef0368d0087bddeb2e" +checksum = "a474f6281d1d70c17ae7aa6a613c87fce69a127e2624002df63dcb39d6cf6396" dependencies = [ "cfg-if", "once_cell", @@ -3834,36 +4705,36 @@ dependencies = [ [[package]] name = "wasm-bindgen-backend" -version = "0.2.95" +version = "0.2.99" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "cb6dd4d3ca0ddffd1dd1c9c04f94b868c37ff5fac97c30b97cff2d74fce3a358" +checksum = "5f89bb38646b4f81674e8f5c3fb81b562be1fd936d84320f3264486418519c79" dependencies = [ "bumpalo", "log", - "once_cell", "proc-macro2", "quote", - "syn 2.0.87", + "syn 2.0.91", "wasm-bindgen-shared", ] [[package]] name = "wasm-bindgen-futures" -version = "0.4.45" +version = "0.4.49" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "cc7ec4f8827a71586374db3e87abdb5a2bb3a15afed140221307c3ec06b1f63b" +checksum = "38176d9b44ea84e9184eff0bc34cc167ed044f816accfe5922e54d84cf48eca2" dependencies = [ "cfg-if", "js-sys", + "once_cell", "wasm-bindgen", "web-sys", ] [[package]] name = "wasm-bindgen-macro" -version = "0.2.95" +version = "0.2.99" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "e79384be7f8f5a9dd5d7167216f022090cf1f9ec128e6e6a482a2cb5c5422c56" +checksum = "2cc6181fd9a7492eef6fef1f33961e3695e4579b9872a6f7c83aee556666d4fe" dependencies = [ "quote", "wasm-bindgen-macro-support", @@ -3871,28 +4742,38 @@ dependencies = [ [[package]] name = "wasm-bindgen-macro-support" -version = "0.2.95" +version = "0.2.99" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "26c6ab57572f7a24a4985830b120de1594465e5d500f24afe89e16b4e833ef68" +checksum = "30d7a95b763d3c45903ed6c81f156801839e5ee968bb07e534c44df0fcd330c2" dependencies = [ "proc-macro2", "quote", - "syn 2.0.87", + "syn 2.0.91", "wasm-bindgen-backend", "wasm-bindgen-shared", ] [[package]] name = "wasm-bindgen-shared" -version = "0.2.95" +version = "0.2.99" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "65fc09f10666a9f147042251e0dda9c18f166ff7de300607007e96bdebc1068d" +checksum = "943aab3fdaaa029a6e0271b35ea10b72b943135afe9bffca82384098ad0e06a6" [[package]] name = "web-sys" -version = "0.3.72" +version = "0.3.76" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "04dd7223427d52553d3702c004d3b2fe07c148165faa56313cb00211e31c12bc" +dependencies = [ + "js-sys", + "wasm-bindgen", +] + +[[package]] +name = "web-time" +version = "1.1.0" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "f6488b90108c040df0fe62fa815cbdee25124641df01814dd7282749234c6112" +checksum = "5a6580f308b1fad9207618087a65c04e7a10bc77e02c8e84e9b00dd4b12fa0bb" dependencies = [ "js-sys", "wasm-bindgen", @@ -3979,7 +4860,7 @@ checksum = "2bbd5b46c938e506ecbce286b6628a02171d56153ba733b6c741fc627ec9579b" dependencies = [ "proc-macro2", "quote", - "syn 2.0.87", + "syn 2.0.91", ] [[package]] @@ -3990,7 +4871,7 @@ checksum = "053c4c462dc91d3b1504c6fe5a726dd15e216ba718e84a0e46a88fbe5ded3515" dependencies = [ "proc-macro2", "quote", - "syn 2.0.87", + "syn 2.0.91", ] [[package]] @@ -4215,11 +5096,22 @@ dependencies = [ "zeroize", ] +[[package]] +name = "xattr" +version = "1.3.1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "8da84f1a25939b27f6820d92aed108f83ff920fdf11a7b19366c27c4cda81d4f" +dependencies = [ + "libc", + "linux-raw-sys", + "rustix", +] + [[package]] name = "yoke" -version = "0.7.4" +version = "0.7.5" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "6c5b1314b079b0930c31e3af543d8ee1757b1951ae1e1565ec704403a7240ca5" +checksum = "120e6aef9aa629e3d4f52dc8cc43a015c7724194c97dfaf45180d2daf2b77f40" dependencies = [ "serde", "stable_deref_trait", @@ -4229,13 +5121,13 @@ dependencies = [ [[package]] name = "yoke-derive" -version = "0.7.4" +version = "0.7.5" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "28cc31741b18cb6f1d5ff12f5b7523e3d6eb0852bbbad19d73905511d9849b95" +checksum = "2380878cad4ac9aac1e2435f3eb4020e8374b5f13c296cb75b4620ff8e229154" dependencies = [ "proc-macro2", "quote", - "syn 2.0.87", + "syn 2.0.91", "synstructure 0.13.1", ] @@ -4257,27 +5149,27 @@ checksum = "fa4f8080344d4671fb4e831a13ad1e68092748387dfc4f55e356242fae12ce3e" dependencies = [ "proc-macro2", "quote", - "syn 2.0.87", + "syn 2.0.91", ] [[package]] name = "zerofrom" -version = "0.1.4" +version = "0.1.5" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "91ec111ce797d0e0784a1116d0ddcdbea84322cd79e5d5ad173daeba4f93ab55" +checksum = "cff3ee08c995dee1859d998dea82f7374f2826091dd9cd47def953cae446cd2e" dependencies = [ "zerofrom-derive", ] [[package]] name = "zerofrom-derive" -version = "0.1.4" +version = "0.1.5" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "0ea7b4a3637ea8669cedf0f1fd5c286a17f3de97b8dd5a70a6c167a1730e63a5" +checksum = "595eed982f7d355beb85837f651fa22e90b3c044842dc7f2c2842c086f295808" dependencies = [ "proc-macro2", "quote", - "syn 2.0.87", + "syn 2.0.91", "synstructure 0.13.1", ] @@ -4298,7 +5190,7 @@ checksum = "ce36e65b0d2999d2aafac989fb249189a141aee1f53c612c1f37d72631959f69" dependencies = [ "proc-macro2", "quote", - "syn 2.0.87", + "syn 2.0.91", ] [[package]] @@ -4320,14 +5212,14 @@ checksum = "6eafa6dfb17584ea3e2bd6e76e0cc15ad7af12b09abdd1ca55961bed9b1063c6" dependencies = [ "proc-macro2", "quote", - "syn 2.0.87", + "syn 2.0.91", ] [[package]] name = "zip" -version = "2.2.0" +version = "2.2.2" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "dc5e4288ea4057ae23afc69a4472434a87a2495cafce6632fd1c4ec9f5cf3494" +checksum = "ae9c1ea7b3a5e1f4b922ff856a129881167511563dc219869afe3787fc0c1a45" dependencies = [ "aes 0.8.4", "arbitrary", @@ -4345,7 +5237,7 @@ dependencies = [ "pbkdf2", "rand 0.8.5", "sha1", - "thiserror", + "thiserror 2.0.9", "time", "zeroize", "zopfli", diff --git a/Cargo.toml b/Cargo.toml index 59c900fc..ebc82aff 100644 --- a/Cargo.toml +++ b/Cargo.toml @@ -1,6 +1,6 @@ [workspace] exclude = ["e2e"] -members = ["agent", "protocol"] +members = ["agent", "bin", "controller", "protocol"] resolver = "2" [workspace.package] @@ -17,15 +17,17 @@ repository = "https://github.com/nodecross/nodex" version = "3.4.0" [workspace.dependencies] -actix-rt = "2.9.0" -actix-web = "4.9.0" agent = { path = "./agent" } anyhow = "1.0.94" bytes = "1.9.0" chrono = { version = "0.4", features = ["serde"] } +clap = { version = "4.5.23", features = ["cargo", "derive"] } +controller = { path = "./controller" } cuid = "1.3.2" data-encoding = "2.6.0" +dirs = "5.0.1" env_logger = { version = "0.11.3", features = ["color"] } +fs2 = "0.4" hex = "0.4.3" hmac = "0.12.1" http = "1.2.0" @@ -39,11 +41,11 @@ reqwest = { version = "0.12", features = [ "rustls-tls-native-roots", ], default-features = false } rstest = "0.21.0" -rumqttc = "0.24.0" serde = { version = "1.0.215", features = ["derive"] } serde_jcs = "0.1.0" serde_json = "1.0.133" sha2 = "0.10.8" +shadow-rs = "0.36.0" sysinfo = "0.30.13" thiserror = "1.0.69" tokio = { version = "1.42.0", features = ["full"] } diff --git a/agent/Cargo.toml b/agent/Cargo.toml index 22bf4361..0bd809c9 100644 --- a/agent/Cargo.toml +++ b/agent/Cargo.toml @@ -5,39 +5,41 @@ edition = { workspace = true } name = "agent" version = { workspace = true } -[[bin]] -name = "nodex-agent" -path = "src/main.rs" - [dependencies] -actix-rt = { workspace = true } -actix-web = { workspace = true } anyhow = { workspace = true } +async-trait = "0.1.83" +axum = { version = "0.7.9", features = ["macros"] } bytes = { workspace = true } chrono = { workspace = true } -clap = { version = "4.5.23", features = ["cargo", "derive"] } +clap = { workspace = true } +controller = { workspace = true } cuid = { workspace = true } -dirs = "5.0.1" +dirs = { workspace = true } dotenvy = "0.15.7" -env_logger = { workspace = true } +fs2 = { workspace = true } hex = { workspace = true } hmac = { workspace = true } home-config = { version = "0.6.0", features = ["json", "toml", "yaml"] } +http-body-util = "0.1.2" +hyper = { version = "1.5.2", features = ["client"] } +hyper-util = "0.1.10" log = { workspace = true } mac_address = { workspace = true } protocol = { workspace = true } reqwest = { workspace = true } -rumqttc = { workspace = true } serde = { workspace = true } serde_json = { workspace = true } sha2 = { workspace = true } -shadow-rs = "0.36.0" +shadow-rs = { workspace = true } sysinfo = { workspace = true } thiserror = { workspace = true } tokio = { workspace = true } +tokio-util = "0.7.13" +tower = { version = "0.4", features = ["util"] } trait-variant = { workspace = true } url = { workspace = true } uuid = { workspace = true } +validator = { version = "0.18", features = ["derive"] } zip = { workspace = true } [target.'cfg(windows)'.dependencies] @@ -47,7 +49,6 @@ windows = { version = "0.58.0", features = [ ] } [target.'cfg(unix)'.dependencies] -daemonize = "0.5.0" nix = { version = "0.29.0", features = ["signal"] } [build-dependencies] diff --git a/agent/src/cli.rs b/agent/src/cli.rs new file mode 100644 index 00000000..a18500fd --- /dev/null +++ b/agent/src/cli.rs @@ -0,0 +1,37 @@ +use clap::{Parser, Subcommand}; + +#[derive(Parser, Debug, Default)] +pub struct AgentOptions { + #[arg(long, help = "Enable configuration")] + pub config: bool, + + #[command(subcommand)] + pub command: Option, +} + +#[derive(Subcommand, Debug)] +pub enum AgentCommands { + #[command(about = "help for DID")] + Did, + #[command(about = "help for Network")] + Network { + #[command(subcommand)] + command: NetworkSubCommands, + }, +} + +#[derive(Subcommand, Debug)] +pub enum NetworkSubCommands { + #[command(about = "Set a network configuration")] + Set { + #[arg(short, long)] + key: String, + #[arg(short, long)] + value: String, + }, + #[command(about = "Get a network configuration")] + Get { + #[arg(short, long)] + key: String, + }, +} diff --git a/agent/src/errors.rs b/agent/src/controllers/errors.rs similarity index 75% rename from agent/src/errors.rs rename to agent/src/controllers/errors.rs index ff9823f5..a9ba05b4 100644 --- a/agent/src/errors.rs +++ b/agent/src/controllers/errors.rs @@ -1,10 +1,11 @@ -use actix_web::HttpResponse; -use actix_web::{error, http::StatusCode}; +use axum::extract::Json; +use axum::http::StatusCode; +use axum::response::{IntoResponse, Response}; use serde::Serialize; use std::convert::From; use thiserror::Error; -#[derive(Serialize, Clone, Copy, Debug, Error)] +#[derive(Clone, Copy, Debug, Error, Serialize)] pub enum AgentErrorCode { #[error("binary_url is required")] VersionNoBinaryUrl = 1001, @@ -111,60 +112,31 @@ pub enum AgentErrorCode { MessageActivityConflict = 6001, } -#[derive(Serialize, Debug)] -pub struct AgentError { - code: u16, - message: String, -} - -impl std::fmt::Display for AgentError { - fn fmt(&self, f: &mut std::fmt::Formatter) -> std::fmt::Result { - write!(f, "code: {}, message: {}", self.code, self.message) - } -} - -impl AgentError { - pub fn new(code: AgentErrorCode) -> Self { - Self { - code: code as u16, - message: format!("{}", code), - } - } -} -impl From<&AgentError> for HttpResponse { - fn from(error: &AgentError) -> Self { - let code = error.code; +impl From for StatusCode { + fn from(code: AgentErrorCode) -> Self { + let code = code as u16; if (1000..2000).contains(&code) { - HttpResponse::BadRequest().json(error) + StatusCode::BAD_REQUEST } else if (2000..3000).contains(&code) { - HttpResponse::Forbidden().json(error) + StatusCode::FORBIDDEN } else if (3000..4000).contains(&code) { - HttpResponse::Unauthorized().json(error) + StatusCode::UNAUTHORIZED } else if (4000..5000).contains(&code) { - HttpResponse::NotFound().json(error) + StatusCode::NOT_FOUND } else if (5000..6000).contains(&code) { - HttpResponse::InternalServerError().json(error) + StatusCode::INTERNAL_SERVER_ERROR } else if (6000..6100).contains(&code) { - HttpResponse::Conflict().json(error) + StatusCode::CONFLICT } else { - HttpResponse::InternalServerError().json(error) + StatusCode::INTERNAL_SERVER_ERROR } } } -impl From for AgentError { - fn from(code: AgentErrorCode) -> Self { - AgentError::new(code) - } -} - -impl error::ResponseError for AgentError { - fn error_response(&self) -> HttpResponse { - self.into() - } - - fn status_code(&self) -> StatusCode { - let res: HttpResponse = self.into(); - res.status() +impl IntoResponse for AgentErrorCode { + fn into_response(self) -> Response { + let code: StatusCode = self.into(); + let value = Json(serde_json::json!({"code": self as u16, "message": format!("{}", self)})); + (code, value).into_response() } } diff --git a/agent/src/controllers/internal/network.rs b/agent/src/controllers/internal/network.rs index 00574d3c..339f6d27 100644 --- a/agent/src/controllers/internal/network.rs +++ b/agent/src/controllers/internal/network.rs @@ -1,8 +1,5 @@ -use crate::{ - errors::{AgentError, AgentErrorCode}, - services::studio::Studio, -}; -use actix_web::{web, HttpRequest, HttpResponse}; +use crate::{controllers::errors::AgentErrorCode, services::studio::Studio}; +use axum::extract::Json; use serde::{Deserialize, Serialize}; use serde_json::Value; @@ -13,12 +10,11 @@ pub struct MessageContainer { } pub async fn handler( - _req: HttpRequest, - web::Json(_): web::Json, -) -> actix_web::Result { + Json(_): Json, +) -> Result, AgentErrorCode> { let studio = Studio::new(); match studio.network().await { - Ok(_) => Ok(HttpResponse::Ok().json("ok")), + Ok(_) => Ok(Json("ok")), Err(e) => { log::error!("{:?}", e); Err(AgentErrorCode::NetworkInternal)? diff --git a/agent/src/controllers/internal/version.rs b/agent/src/controllers/internal/version.rs index 739771b7..caaee9e5 100644 --- a/agent/src/controllers/internal/version.rs +++ b/agent/src/controllers/internal/version.rs @@ -1,11 +1,7 @@ -use crate::{ - errors::{AgentError, AgentErrorCode}, - services::nodex::NodeX, -}; -use actix_web::{web, HttpRequest, HttpResponse}; +use crate::{controllers::errors::AgentErrorCode, services::nodex::NodeX}; +use axum::extract::Json; use serde::{Deserialize, Serialize}; -use serde_json::{json, Value}; -use std::path::PathBuf; +use serde_json::Value; // NOTE: POST /internal/version #[derive(Deserialize, Serialize)] @@ -13,26 +9,23 @@ pub struct MessageContainer { message: Value, } -pub async fn handler_get(_req: HttpRequest) -> actix_web::Result { - let current_version = env!("CARGO_PKG_VERSION"); - Ok(HttpResponse::Ok().json(json!({ "version": current_version }))) +pub async fn handler_get() -> Json { + Json(serde_json::json!({ "version": env!("CARGO_PKG_VERSION")})) } pub async fn handler_update( - _req: HttpRequest, - web::Json(json): web::Json, -) -> actix_web::Result { + Json(json): Json, +) -> Result, AgentErrorCode> { let binary_url = match json.message["binary_url"].as_str() { Some(url) => url, None => Err(AgentErrorCode::VersionNoBinaryUrl)?, }; - let path = match json.message["path"].as_str() { - Some(p) => p, - None => Err(AgentErrorCode::VersionNoPath)?, - }; let nodex = NodeX::new(); - match nodex.update_version(binary_url, PathBuf::from(path)).await { - Ok(_) => Ok(HttpResponse::Ok().json("ok")), - Err(_) => Err(AgentErrorCode::VersionInternal)?, + match nodex.update_version(binary_url).await { + Ok(_) => Ok(Json("ok")), + Err(e) => { + log::error!("{}", e); + Err(AgentErrorCode::VersionInternal)? + } } } diff --git a/agent/src/controllers/mod.rs b/agent/src/controllers/mod.rs index 52c4b48e..e2364313 100644 --- a/agent/src/controllers/mod.rs +++ b/agent/src/controllers/mod.rs @@ -1,2 +1,3 @@ +mod errors; pub mod internal; pub mod public; diff --git a/agent/src/controllers/public/nodex_create_didcomm_message.rs b/agent/src/controllers/public/nodex_create_didcomm_message.rs index 683dda6c..1112a14c 100644 --- a/agent/src/controllers/public/nodex_create_didcomm_message.rs +++ b/agent/src/controllers/public/nodex_create_didcomm_message.rs @@ -1,15 +1,12 @@ -use actix_web::{web, HttpRequest, HttpResponse}; -use chrono::Utc; -use serde::{Deserialize, Serialize}; - -use protocol::didcomm::encrypted::DidCommEncryptedServiceGenerateError as S; - -use crate::errors::{AgentError, AgentErrorCode}; +use super::utils; +use crate::controllers::errors::AgentErrorCode; use crate::nodex::utils::did_accessor::DidAccessorImpl; use crate::usecase::didcomm_message_usecase::GenerateDidcommMessageUseCaseError as U; use crate::{services::studio::Studio, usecase::didcomm_message_usecase::DidcommMessageUseCase}; - -use super::utils; +use axum::extract::Json; +use chrono::Utc; +use protocol::didcomm::encrypted::DidCommEncryptedServiceGenerateError as S; +use serde::{Deserialize, Serialize}; // NOTE: POST /create-didcomm-message #[derive(Deserialize, Serialize)] @@ -22,10 +19,7 @@ pub struct MessageContainer { operation_tag: String, } -pub async fn handler( - _req: HttpRequest, - web::Json(json): web::Json, -) -> actix_web::Result { +pub async fn handler(Json(json): Json) -> Result { if json.destination_did.is_empty() { Err(AgentErrorCode::CreateDidCommMessageNoDestinationDid)? } @@ -45,7 +39,7 @@ pub async fn handler( .generate(json.destination_did, json.message, json.operation_tag, now) .await { - Ok(v) => Ok(HttpResponse::Ok().body(v)), + Ok(v) => Ok(v), Err(e) => match e { U::MessageActivity(e) => Err(utils::handle_status(e)), U::ServiceGenerate(S::DidDocNotFound(target)) => { diff --git a/agent/src/controllers/public/nodex_create_identifier.rs b/agent/src/controllers/public/nodex_create_identifier.rs index aafe5e1c..f3ab8fdc 100644 --- a/agent/src/controllers/public/nodex_create_identifier.rs +++ b/agent/src/controllers/public/nodex_create_identifier.rs @@ -1,17 +1,12 @@ -use actix_web::{HttpRequest, HttpResponse}; -use serde::{Deserialize, Serialize}; +use crate::controllers::errors::AgentErrorCode; +use axum::extract::Json; +use protocol::did::sidetree::payload::DidResolutionResponse; -use crate::errors::{AgentError, AgentErrorCode}; - -// NOTE: POST /identifiers -#[derive(Deserialize, Serialize)] -struct MessageContainer {} - -pub async fn handler(_req: HttpRequest) -> actix_web::Result { +pub async fn handler() -> Result, AgentErrorCode> { let service = crate::services::nodex::NodeX::new(); match service.create_identifier().await { - Ok(v) => Ok(HttpResponse::Ok().json(&v)), + Ok(v) => Ok(Json(v)), Err(e) => { log::error!("{:?}", e); Err(AgentErrorCode::CreateIdentifierInternal)? diff --git a/agent/src/controllers/public/nodex_create_verifiable_message.rs b/agent/src/controllers/public/nodex_create_verifiable_message.rs index a0279917..341e7990 100644 --- a/agent/src/controllers/public/nodex_create_verifiable_message.rs +++ b/agent/src/controllers/public/nodex_create_verifiable_message.rs @@ -1,15 +1,13 @@ -use actix_web::{web, HttpRequest, HttpResponse}; -use chrono::Utc; -use serde::{Deserialize, Serialize}; - -use crate::errors::{AgentError, AgentErrorCode}; +use super::utils; +use crate::controllers::errors::AgentErrorCode; use crate::nodex::utils::did_accessor::DidAccessorImpl; use crate::usecase::verifiable_message_usecase::CreateVerifiableMessageUseCaseError as U; use crate::{ services::studio::Studio, usecase::verifiable_message_usecase::VerifiableMessageUseCase, }; - -use super::utils; +use axum::extract::Json; +use chrono::Utc; +use serde::{Deserialize, Serialize}; // NOTE: POST /create-verifiable-message #[derive(Deserialize, Serialize)] @@ -22,10 +20,7 @@ pub struct MessageContainer { operation_tag: String, } -pub async fn handler( - _req: HttpRequest, - web::Json(json): web::Json, -) -> actix_web::Result { +pub async fn handler(Json(json): Json) -> Result { if json.destination_did.is_empty() { Err(AgentErrorCode::CreateVerifiableMessageNoDestinationDid)? } @@ -45,7 +40,7 @@ pub async fn handler( .generate(json.destination_did, json.message, json.operation_tag, now) .await { - Ok(v) => Ok(HttpResponse::Ok().body(v)), + Ok(v) => Ok(v), Err(e) => match e { U::MessageActivity(e) => Err(utils::handle_status(e)), U::DestinationNotFound(e) => { diff --git a/agent/src/controllers/public/nodex_find_identifier.rs b/agent/src/controllers/public/nodex_find_identifier.rs index c88ac355..b2287121 100644 --- a/agent/src/controllers/public/nodex_find_identifier.rs +++ b/agent/src/controllers/public/nodex_find_identifier.rs @@ -1,20 +1,14 @@ -use actix_web::{web, HttpRequest, HttpResponse}; -use serde::{Deserialize, Serialize}; - -use crate::errors::{AgentError, AgentErrorCode}; - -// NOTE: GET /identifiers/${ did } -#[derive(Deserialize, Serialize)] -struct MessageContainer {} +use crate::controllers::errors::AgentErrorCode; +use axum::extract::{Json, Path}; +use protocol::did::sidetree::payload::DidResolutionResponse; pub async fn handler( - _req: HttpRequest, - did: web::Path, -) -> actix_web::Result { + did: Path, +) -> Result>, AgentErrorCode> { let service = crate::services::nodex::NodeX::new(); match service.find_identifier(&did).await { - Ok(v) => Ok(HttpResponse::Ok().json(&v)), + Ok(v) => Ok(Json(v)), Err(e) => { log::error!("{:?}", e); Err(AgentErrorCode::FindIdentifierInternal)? diff --git a/agent/src/controllers/public/nodex_receive.rs b/agent/src/controllers/public/nodex_receive.rs index c8d53947..80f7eaf5 100644 --- a/agent/src/controllers/public/nodex_receive.rs +++ b/agent/src/controllers/public/nodex_receive.rs @@ -1,15 +1,13 @@ -use anyhow::anyhow; - -use serde::{Deserialize, Serialize}; -use serde_json; -use std::{env, path::PathBuf, sync::Arc, time::Duration}; -use tokio::sync::Notify; - -use protocol::didcomm::encrypted::DidCommEncryptedService; - use crate::nodex::utils::did_accessor::{DidAccessor, DidAccessorImpl}; use crate::services::nodex::NodeX; use crate::services::studio::{MessageResponse, Studio}; +use anyhow::anyhow; +use controller::validator::network::can_connect_to_download_server; +use protocol::didcomm::encrypted::DidCommEncryptedService; +use serde::{Deserialize, Serialize}; +use serde_json; +use std::time::Duration; +use tokio_util::sync::CancellationToken; #[derive(Deserialize)] enum OperationType { @@ -88,24 +86,16 @@ impl MessageReceiveUsecase { let binary_url = container["binary_url"] .as_str() .ok_or(anyhow!("the container doesn't have binary_url"))?; - - let tmp_path = { - #[cfg(unix)] - { - PathBuf::from("/tmp/nodex-agent") - } - #[cfg(windows)] - { - PathBuf::from("C:\\Temp\\nodex-agent") - } - }; - let exe_path = env::current_exe()?; - let working_dir = exe_path - .parent() - .map(|p| p.to_path_buf()) - .unwrap_or(tmp_path); - - self.agent.update_version(binary_url, working_dir).await?; + if !can_connect_to_download_server("https://github.com").await { + log::error!("Not connected to the Internet"); + anyhow::bail!("Not connected to the Internet"); + } else if !binary_url.starts_with( + "https://github.com/nodecross/nodex/releases/download/", + ) { + log::error!("Invalid url"); + anyhow::bail!("Invalid url"); + } + self.agent.update_version(binary_url).await?; } Ok(OperationType::UpdateNetworkJson) => { self.studio.network().await?; @@ -133,7 +123,7 @@ impl MessageReceiveUsecase { } } -pub async fn polling_task(shutdown_notify: Arc) { +pub async fn polling_task(shutdown_token: CancellationToken) { log::info!("Polling task is started"); let usecase = MessageReceiveUsecase::new(); @@ -147,7 +137,7 @@ pub async fn polling_task(shutdown_notify: Arc) { Err(e) => log::error!("Error: {:?}", e), } } - _ = shutdown_notify.notified() => { + _ = shutdown_token.cancelled() => { break; }, } diff --git a/agent/src/controllers/public/nodex_verify_didcomm_message.rs b/agent/src/controllers/public/nodex_verify_didcomm_message.rs index 96f20cec..0d3ccf23 100644 --- a/agent/src/controllers/public/nodex_verify_didcomm_message.rs +++ b/agent/src/controllers/public/nodex_verify_didcomm_message.rs @@ -1,11 +1,5 @@ -use actix_web::{web, HttpRequest, HttpResponse}; -use chrono::Utc; -use serde::{Deserialize, Serialize}; - -use protocol::didcomm::encrypted::DidCommEncryptedServiceVerifyError as S; -use protocol::didcomm::types::DidCommMessage; - -use crate::errors::{AgentError, AgentErrorCode}; +use super::utils; +use crate::controllers::errors::AgentErrorCode; use crate::nodex::utils::did_accessor::DidAccessorImpl; use crate::{ services::studio::Studio, @@ -13,8 +7,12 @@ use crate::{ DidcommMessageUseCase, VerifyDidcommMessageUseCaseError as U, }, }; - -use super::utils; +use axum::extract::Json; +use chrono::Utc; +use protocol::didcomm::encrypted::DidCommEncryptedServiceVerifyError as S; +use protocol::didcomm::types::DidCommMessage; +use protocol::verifiable_credentials::types::VerifiableCredentials; +use serde::{Deserialize, Serialize}; // NOTE: POST /verify-verifiable-message #[derive(Deserialize, Serialize)] @@ -24,9 +22,8 @@ pub struct MessageContainer { } pub async fn handler( - _req: HttpRequest, - web::Json(json): web::Json, -) -> actix_web::Result { + Json(json): Json, +) -> Result, AgentErrorCode> { let now = Utc::now(); let usecase = @@ -38,7 +35,7 @@ pub async fn handler( Err(AgentErrorCode::VerifyDidcommMessageJsonError)? } Ok(message) => match usecase.verify(message, now).await { - Ok(v) => Ok(HttpResponse::Ok().json(v)), + Ok(v) => Ok(Json(v)), Err(e) => match e { U::MessageActivity(e) => Err(utils::handle_status(e)), U::NotAddressedToMe => { diff --git a/agent/src/controllers/public/nodex_verify_verifiable_message.rs b/agent/src/controllers/public/nodex_verify_verifiable_message.rs index aafaf08b..3ba43767 100644 --- a/agent/src/controllers/public/nodex_verify_verifiable_message.rs +++ b/agent/src/controllers/public/nodex_verify_verifiable_message.rs @@ -1,30 +1,26 @@ -use actix_web::{web, HttpRequest, HttpResponse}; -use chrono::Utc; -use serde::{Deserialize, Serialize}; - -use protocol::verifiable_credentials::did_vc::DidVcServiceVerifyError as S; -use protocol::verifiable_credentials::types::VerifiableCredentials; - -use crate::errors::{AgentError, AgentErrorCode}; +use super::utils; +use crate::controllers::errors::AgentErrorCode; use crate::nodex::utils::did_accessor::DidAccessorImpl; use crate::usecase::verifiable_message_usecase::VerifyVerifiableMessageUseCaseError as U; use crate::{ services::studio::Studio, usecase::verifiable_message_usecase::VerifiableMessageUseCase, }; - -use super::utils; +use axum::extract::Json; +use chrono::Utc; +use protocol::verifiable_credentials::did_vc::DidVcServiceVerifyError as S; +use protocol::verifiable_credentials::types::VerifiableCredentials; +use serde::{Deserialize, Serialize}; // NOTE: POST /verify-verifiable-message -#[derive(Deserialize, Serialize)] +#[derive(Clone, Deserialize, Serialize)] pub struct MessageContainer { #[serde(default)] message: String, } pub async fn handler( - _req: HttpRequest, - web::Json(json): web::Json, -) -> actix_web::Result { + Json(json): Json, +) -> Result, AgentErrorCode> { let now = Utc::now(); let repo = utils::did_repository(); @@ -37,7 +33,7 @@ pub async fn handler( Err(AgentErrorCode::VerifyVerifiableMessageJsonError)? } Ok(vc) => match usecase.verify(vc, now).await { - Ok(v) => Ok(HttpResponse::Ok().json(v)), + Ok(v) => Ok(Json(v)), Err(e) => match e { U::MessageActivity(e) => Err(utils::handle_status(e)), U::DidVcServiceVerify(S::VerifyFailed(e)) => { diff --git a/agent/src/controllers/public/send_attribute.rs b/agent/src/controllers/public/send_attribute.rs index 800b3ba7..a8d7627b 100644 --- a/agent/src/controllers/public/send_attribute.rs +++ b/agent/src/controllers/public/send_attribute.rs @@ -1,11 +1,9 @@ -use actix_web::{web, HttpRequest, HttpResponse}; -use serde::{Deserialize, Serialize}; - use crate::{ - errors::{AgentError, AgentErrorCode}, - repository::attribute_repository::AttributeStoreRequest, + controllers::errors::AgentErrorCode, repository::attribute_repository::AttributeStoreRequest, usecase::attribute_usecase::AttributeUsecase, }; +use axum::extract::Json; +use serde::{Deserialize, Serialize}; #[derive(Deserialize, Serialize)] pub struct MessageContainer { @@ -15,10 +13,7 @@ pub struct MessageContainer { value: String, } -pub async fn handler( - _req: HttpRequest, - web::Json(json): web::Json, -) -> actix_web::Result { +pub async fn handler(Json(json): Json) -> Result<(), AgentErrorCode> { if json.key_name.is_empty() { Err(AgentErrorCode::SendAttributeNoKeyName)? } @@ -36,7 +31,7 @@ pub async fn handler( { Ok(_) => { log::info!("save attribute"); - Ok(HttpResponse::NoContent().finish()) + Ok(()) } Err(e) => { log::error!("{:?}", e); diff --git a/agent/src/controllers/public/send_custom_metric.rs b/agent/src/controllers/public/send_custom_metric.rs index 2de91f37..cf23260e 100644 --- a/agent/src/controllers/public/send_custom_metric.rs +++ b/agent/src/controllers/public/send_custom_metric.rs @@ -1,13 +1,11 @@ use super::utils::milliseconds_to_time; -use actix_web::{web, HttpRequest, HttpResponse}; - -use serde::{Deserialize, Serialize}; - use crate::{ - errors::{AgentError, AgentErrorCode}, + controllers::errors::AgentErrorCode, repository::custom_metric_repository::CustomMetricStoreRequest, usecase::custom_metric_usecase::CustomMetricUsecase, }; +use axum::extract::Json; +use serde::{Deserialize, Serialize}; #[derive(Deserialize, Serialize)] pub struct MessageContainer { @@ -18,10 +16,7 @@ pub struct MessageContainer { occurred_at: u64, } -pub async fn handler( - _req: HttpRequest, - web::Json(json): web::Json>, -) -> actix_web::Result { +pub async fn handler(Json(json): Json>) -> Result<(), AgentErrorCode> { let metrics = json .iter() .map(|m| { @@ -38,14 +33,13 @@ pub async fn handler( occurred_at, }) }) - .collect::, AgentErrorCode>>() - .map_err(AgentError::new)?; + .collect::, AgentErrorCode>>()?; let usecase = CustomMetricUsecase::new(); match usecase.save(metrics).await { Ok(_) => { log::info!("sent custom metrics"); - Ok(HttpResponse::NoContent().finish()) + Ok(()) } Err(e) => { log::error!("{:?}", e); diff --git a/agent/src/controllers/public/send_event.rs b/agent/src/controllers/public/send_event.rs index f008863c..d3e0cc5f 100644 --- a/agent/src/controllers/public/send_event.rs +++ b/agent/src/controllers/public/send_event.rs @@ -1,14 +1,10 @@ -use actix_web::{web, HttpRequest, HttpResponse}; - -use serde::{Deserialize, Serialize}; - +use super::utils::milliseconds_to_time; use crate::{ - errors::{AgentError, AgentErrorCode}, - repository::event_repository::EventStoreRequest, + controllers::errors::AgentErrorCode, repository::event_repository::EventStoreRequest, usecase::event_usecase::EventUsecase, }; - -use super::utils::milliseconds_to_time; +use axum::extract::Json; +use serde::{Deserialize, Serialize}; #[derive(Deserialize, Serialize)] pub struct MessageContainer { @@ -20,10 +16,7 @@ pub struct MessageContainer { occurred_at: u64, } -pub async fn handler( - _req: HttpRequest, - web::Json(json): web::Json, -) -> actix_web::Result { +pub async fn handler(Json(json): Json) -> Result<(), AgentErrorCode> { if json.key.is_empty() { Err(AgentErrorCode::SendEventNoKey)? } @@ -45,7 +38,7 @@ pub async fn handler( { Ok(_) => { log::info!("save event"); - Ok(HttpResponse::NoContent().finish()) + Ok(()) } Err(e) => { log::error!("{:?}", e); diff --git a/agent/src/controllers/public/utils.rs b/agent/src/controllers/public/utils.rs index 9bdfce18..ba683729 100644 --- a/agent/src/controllers/public/utils.rs +++ b/agent/src/controllers/public/utils.rs @@ -1,11 +1,10 @@ -use anyhow::Context as _; -use chrono::{DateTime, Utc}; -use protocol::did::did_repository::DidRepositoryImpl; - -use crate::errors::{AgentError, AgentErrorCode}; +use crate::controllers::errors::AgentErrorCode; use crate::nodex::utils::sidetree_client::SideTreeClient; use crate::repository::message_activity_repository::MessageActivityHttpError; use crate::server_config; +use anyhow::Context as _; +use chrono::{DateTime, Utc}; +use protocol::did::did_repository::DidRepositoryImpl; pub fn did_repository() -> DidRepositoryImpl { let server_config = server_config(); @@ -15,29 +14,29 @@ pub fn did_repository() -> DidRepositoryImpl { DidRepositoryImpl::new(sidetree_client) } -pub fn handle_status(e: MessageActivityHttpError) -> AgentError { +pub fn handle_status(e: MessageActivityHttpError) -> AgentErrorCode { match e { MessageActivityHttpError::BadRequest(message) => { log::warn!("Bad Request: {}", message); - AgentErrorCode::MessageActivityBadRequest.into() + AgentErrorCode::MessageActivityBadRequest } MessageActivityHttpError::Forbidden(message) => { log::warn!("Forbidden: {}", message); - AgentErrorCode::MessageActivityForbidden.into() + AgentErrorCode::MessageActivityForbidden } MessageActivityHttpError::Unauthorized(message) => { log::warn!("Unauthorized: {}", message); - AgentErrorCode::MessageActivityUnauthorized.into() + AgentErrorCode::MessageActivityUnauthorized } MessageActivityHttpError::NotFound(message) => { log::warn!("Not Found: {}", message); - AgentErrorCode::MessageActivityNotFound.into() + AgentErrorCode::MessageActivityNotFound } MessageActivityHttpError::Conflict(message) => { log::warn!("Conflict: {}", message); - AgentErrorCode::MessageActivityConflict.into() + AgentErrorCode::MessageActivityConflict } - _ => AgentErrorCode::MessageActivityInternal.into(), + _ => AgentErrorCode::MessageActivityInternal, } } diff --git a/agent/src/handlers/mod.rs b/agent/src/handlers/mod.rs deleted file mode 100644 index f1cdb09a..00000000 --- a/agent/src/handlers/mod.rs +++ /dev/null @@ -1,39 +0,0 @@ -use serde_json::Value; -use tokio::sync::oneshot; - -pub mod sender; - -type Responder = oneshot::Sender; - -#[derive(Debug)] -pub enum Command { - Send { value: Value, resp: Responder }, -} - -#[allow(dead_code)] -#[trait_variant::make(Send)] -pub trait TransferClient: Sync { - async fn send(&self, value: Value) -> anyhow::Result; -} - -pub struct MqttClient { - sender: tokio::sync::mpsc::Sender, -} - -impl MqttClient { - pub fn new(sender: tokio::sync::mpsc::Sender) -> Self { - MqttClient { sender } - } -} - -impl TransferClient for MqttClient { - async fn send(&self, value: Value) -> anyhow::Result { - let (tx, rx) = oneshot::channel(); - - let command = Command::Send { value, resp: tx }; - - self.sender.send(command).await?; - - Ok(rx.await?) - } -} diff --git a/agent/src/handlers/receiver.rs b/agent/src/handlers/receiver.rs deleted file mode 100644 index 36b0d668..00000000 --- a/agent/src/handlers/receiver.rs +++ /dev/null @@ -1,47 +0,0 @@ -use rumqttc::{Event, EventLoop, Packet}; -use serde::{Deserialize, Serialize}; -use std::collections::HashMap; -use std::sync::{atomic::AtomicBool, Arc}; -use tokio::sync::RwLock; - -pub async fn handler( - shutdown_marker: Arc, - mut eventloop: EventLoop, - db: Arc>>, -) { - #[derive(Debug, Serialize, Deserialize)] - struct Response { - received_id: String, - } - - log::info!("start receiver"); - - while let Ok(notification) = eventloop.poll().await { - if shutdown_marker.load(std::sync::atomic::Ordering::SeqCst) { - break; - } - - match notification { - Event::Incoming(v) => { - if let Packet::Publish(v) = v { - if let Ok(payload) = serde_json::from_slice::(&v.payload) { - let mut keys = Vec::::new(); - - db.read().await.keys().enumerate().for_each(|v| { - keys.push(v.1.to_string()); - }); - - let item = keys.iter().find(|v| v.to_string() == payload.received_id); - - if let Some(v) = item { - let _ = db.write().await.insert(v.to_string(), true); - } - }; - } - } - Event::Outgoing(_) => {} - } - } - - log::info!("stop receiver"); -} diff --git a/agent/src/handlers/sender.rs b/agent/src/handlers/sender.rs deleted file mode 100644 index 74df1168..00000000 --- a/agent/src/handlers/sender.rs +++ /dev/null @@ -1,69 +0,0 @@ -use rumqttc::{AsyncClient, QoS}; -use serde_json::{json, Value}; -use std::collections::HashMap; -use std::sync::Arc; -use tokio::sync::{mpsc::Receiver, RwLock}; -use tokio::time::{sleep, Duration, Instant}; - -use super::Command; - -pub async fn handler( - mut rx: Receiver, - client: AsyncClient, - db: Arc>>, - topic: String, -) { - log::info!("start sender"); - - while let Some(cmd) = rx.recv().await { - match cmd { - Command::Send { value, resp } => { - let id = cuid::cuid2(); - - let payload: Value = json!({ - "id": id, - "value": value, - }); - - if (client - .publish( - topic.to_string(), - QoS::AtLeastOnce, - false, - payload.to_string().as_bytes(), - ) - .await) - .is_ok() - { - db.write().await.insert(id.clone(), false); - - let start = Instant::now(); - let threshold = Duration::from_secs(15); - - loop { - if threshold < start.elapsed() { - _ = resp.send(false); - break; - } - - match db.read().await.get(&id) { - Some(v) => { - if *v { - _ = resp.send(true); - break; - } - } - None => { - continue; - } - } - - sleep(Duration::from_secs(1)).await; - } - } - } - } - } - - log::info!("stop sender"); -} diff --git a/agent/src/lib.rs b/agent/src/lib.rs new file mode 100644 index 00000000..a6a21887 --- /dev/null +++ b/agent/src/lib.rs @@ -0,0 +1,214 @@ +use crate::controllers::public::nodex_receive; +use cli::AgentCommands; +use dotenvy::dotenv; +use mac_address::get_mac_address; +use nodex::utils::UnwrapLog; +use services::metrics::{MetricsInMemoryCacheService, MetricsWatchService}; +use services::nodex::NodeX; +use services::studio::Studio; +use std::env; +use std::fs; +use tokio::task::JoinSet; +use tokio_util::sync::CancellationToken; +use usecase::metric_usecase::MetricUsecase; +pub mod cli; +mod config; +mod controllers; +mod network; +mod nodex; +mod repository; +mod server; +mod services; +mod usecase; +pub use crate::config::app_config; +pub use crate::config::server_config; +pub use crate::network::network_config; + +#[tokio::main] +pub async fn run(controlled: bool, options: &cli::AgentOptions) -> std::io::Result<()> { + dotenv().ok(); + + #[cfg(windows)] + server::windows::kill_other_self_process(); + + { + let config = app_config(); + let config = config.lock(); + config.write().unwrap_log(); + } + + let home_dir = dirs::home_dir().unwrap(); + let config_dir = home_dir.join(".nodex"); + let logs_dir = config_dir.clone().join("logs"); + + fs::create_dir_all(&logs_dir).unwrap_log(); + + // NOTE: generate Key Chain + let node_x = NodeX::new(); + let device_did = node_x.create_identifier().await.unwrap(); + + if options.config { + use_cli(options.command.as_ref(), device_did.did_document.id.clone()); + return Ok(()); + } + + studio_initialize(device_did.did_document.id.clone()).await; + send_device_info().await; + + let shutdown_token = CancellationToken::new(); + let mut tasks = JoinSet::new(); + + let cache_repository = + MetricsInMemoryCacheService::new(app_config().lock().get_metric_cache_capacity()); + let cache_repository_cloned = cache_repository.clone(); + let shutdown_token_cloned = shutdown_token.clone(); + tasks.spawn(async move { + let mut metric_usecase = MetricUsecase::new( + Studio::new(), + MetricsWatchService::new(), + app_config(), + cache_repository_cloned, + shutdown_token_cloned, + ); + metric_usecase.collect_task().await + }); + let shutdown_token_cloned = shutdown_token.clone(); + tasks.spawn(async move { + let mut metric_usecase = MetricUsecase::new( + Studio::new(), + MetricsWatchService::new(), + app_config(), + cache_repository, + shutdown_token_cloned, + ); + metric_usecase.send_task().await + }); + tasks.spawn(nodex_receive::polling_task(shutdown_token.clone())); + + // NOTE: booting... + #[cfg(unix)] + { + let runtime_dir = config_dir.clone().join("run"); + fs::create_dir_all(&runtime_dir).unwrap_log(); + let nodex_path = runtime_dir.clone().join("nodex.sock"); + let listener = if !controlled { + controller::unix_utils::remove_file_if_exists(&nodex_path); + tokio::net::UnixListener::bind(&nodex_path)? + } else { + server::unix::recieve_listener(&nodex_path)? + }; + let fd = std::os::unix::io::AsRawFd::as_raw_fd(&listener); + let server = server::unix::make_uds_server(server::make_router(), listener); + let server = + server::unix::wrap_with_signal_handler(server, shutdown_token, fd, &nodex_path); + let (server, _) = tokio::join!(server.join_all(), tasks.join_all()); + server.into_iter().collect::, _>>()?; + }; + + #[cfg(windows)] + { + let port_str = + env::var("NODEX_SERVER_PORT").expect("NODEX_SERVER_PORT must be set and valid."); + let port = server::windows::validate_port(&port_str).expect("Invalid port number."); + let router = server::make_router(); + let server = server::windows::new_web_server(port, router).await?; + let _ = tokio::join!(server, tasks.join_all()); + }; + Ok(()) +} + +fn use_cli(command: Option<&AgentCommands>, did: String) { + let network_config = crate::network_config(); + let mut network_config = network_config.lock(); + const SECRET_KEY: &str = "secret_key"; + const PROJECT_DID: &str = "project_did"; + + if let Some(command) = command { + match command { + AgentCommands::Did {} => { + println!("Node ID: {}", did); + } + AgentCommands::Network { command } => match command { + cli::NetworkSubCommands::Set { key, value } => match key.as_str() { + SECRET_KEY => { + network_config.save_secret_key(value); + log::info!("Network {} is set", SECRET_KEY); + } + PROJECT_DID => { + network_config.save_project_did(value); + log::info!("Network {} is set", PROJECT_DID); + } + _ => { + log::info!("key is not found"); + } + }, + cli::NetworkSubCommands::Get { key } => match key.as_str() { + SECRET_KEY => { + if let Some(v) = network_config.get_secret_key() { + println!("Network {}: {}", SECRET_KEY, v); + return; + }; + log::info!("Network {} is not set", SECRET_KEY); + } + PROJECT_DID => { + if let Some(v) = network_config.get_project_did() { + log::info!("Network {}: {}", PROJECT_DID, v); + return; + }; + log::info!("Network {} is not set", PROJECT_DID); + } + _ => { + log::info!("key is not found"); + } + }, + }, + } + } +} + +async fn studio_initialize(my_did: String) { + let project_did = { + let network = network_config(); + let network_config = network.lock(); + + // NOTE: check network secret_key and project_did + network_config + .get_secret_key() + .ok_or("Network secret_key is not set. Please set secret_key use cli") + .unwrap_log(); + network_config + .get_project_did() + .expect("Network project_did is not set. Please set project_did use cli") + }; + + let studio = Studio::new(); + studio + .register_device(my_did, project_did) + .await + .unwrap_log(); +} + +async fn send_device_info() { + const VERSION: &str = env!("CARGO_PKG_VERSION"); + const OS: &str = env::consts::OS; + let mac_address: String = match get_mac_address() { + Ok(Some(ma)) => ma.to_string(), + _ => String::from("No MAC address found."), + }; + + let project_did = network_config() + .lock() + .get_project_did() + .expect("Failed to get project_did"); + + let studio = Studio::new(); + studio + .send_device_info( + project_did, + mac_address, + VERSION.to_string(), + OS.to_string(), + ) + .await + .unwrap_log(); +} diff --git a/agent/src/main.rs b/agent/src/main.rs deleted file mode 100644 index ba350426..00000000 --- a/agent/src/main.rs +++ /dev/null @@ -1,459 +0,0 @@ -extern crate env_logger; - -use crate::controllers::public::nodex_receive; -use anyhow::anyhow; -use clap::{Parser, Subcommand}; -use dotenvy::dotenv; -use handlers::Command; -use handlers::MqttClient; -use mac_address::get_mac_address; -#[cfg(unix)] -use nix::{ - sys::signal::{kill, Signal}, - unistd::Pid, -}; -use rumqttc::{AsyncClient, MqttOptions, QoS}; -use services::metrics::{MetricsInMemoryCacheService, MetricsWatchService}; -use services::nodex::NodeX; -use services::studio::Studio; -use shadow_rs::shadow; -use std::env; -#[cfg(unix)] -use std::os::unix::fs::PermissionsExt; -use std::sync::atomic::{AtomicBool, Ordering}; -use std::{collections::HashMap, fs, sync::Arc}; -use sysinfo::{get_current_pid, System}; -use tokio::sync::mpsc; -use tokio::sync::Notify; -use tokio::sync::RwLock; -use tokio::time::Duration; - -#[cfg(windows)] -use windows::Win32::{ - Foundation::{CloseHandle, GetLastError}, - System::Threading::{OpenProcess, TerminateProcess, PROCESS_TERMINATE}, -}; - -use nodex::utils::UnwrapLog; -use usecase::metric_usecase::MetricUsecase; - -mod config; -mod controllers; -mod errors; -mod handlers; -mod network; -mod nodex; -mod repository; -mod server; -mod services; -mod usecase; - -pub use crate::config::app_config; -pub use crate::config::server_config; -pub use crate::network::network_config; - -shadow!(build); - -#[derive(Parser, Debug)] -#[clap(name = "nodex-agent")] -#[clap( - version = shadow_rs::formatcp!("v{} ({} {})\n{} @ {}", build::PKG_VERSION, build::SHORT_COMMIT, build::BUILD_TIME_3339, build::RUST_VERSION, build::BUILD_TARGET), - about, - long_about = None -)] -struct Cli { - #[clap(long)] - config: bool, - - #[command(subcommand)] - command: Option, -} - -#[derive(Debug, Subcommand)] -enum Commands { - #[command(about = "help for did")] - Did {}, - #[command(about = "help for network")] - Network { - #[command(subcommand)] - command: NetworkSubCommands, - }, -} - -#[derive(Debug, Subcommand)] -enum NetworkSubCommands { - #[command(about = "help for Set")] - Set { - #[arg(short, long)] - key: String, - #[arg(short, long)] - value: String, - }, - #[command(about = "help for Get")] - Get { - #[arg(short, long)] - key: String, - }, -} - -#[tokio::main] -async fn main() -> std::io::Result<()> { - dotenv().ok(); - - let cli = Cli::parse(); - - std::env::set_var("RUST_LOG", "info"); - log_init(); - kill_other_self_process(); - - let studio_did_topic = "nodex/did:nodex:test:EiCW6eklabBIrkTMHFpBln7574xmZlbMakWSCNtBWcunDg"; - - { - let config = app_config(); - let config = config.lock(); - config.write().unwrap_log(); - } - - let home_dir = dirs::home_dir().unwrap(); - let config_dir = home_dir.join(".nodex"); - let logs_dir = config_dir.clone().join("logs"); - - fs::create_dir_all(&logs_dir).unwrap_log(); - - // NOTE: generate Key Chain - let node_x = NodeX::new(); - let device_did = node_x.create_identifier().await.unwrap(); - - if cli.config { - use_cli(cli.command, device_did.did_document.id.clone()); - return Ok(()); - } - - studio_initialize(device_did.did_document.id.clone()).await; - send_device_info().await; - - // NOTE: connect mqtt server - let mqtt_host = "demo-mqtt.getnodex.io"; - let mqtt_port = 1883; - let mqtt_client_id = cuid::cuid2(); - - let did_id = device_did.did_document.id; - let mqtt_topic = format!("nodex/{}", did_id); - - let mut mqtt_options = MqttOptions::new(&mqtt_client_id, mqtt_host, mqtt_port); - mqtt_options.set_clean_session(true); - mqtt_options.set_keep_alive(Duration::from_secs(5)); - - let (client, _eventloop) = AsyncClient::new(mqtt_options, 10); - - client - .subscribe(studio_did_topic, QoS::ExactlyOnce) - .await - .unwrap(); - log::info!("subscribed: {}", studio_did_topic); - - let shutdown_notify = Arc::new(Notify::new()); - - let cache_repository = - MetricsInMemoryCacheService::new(app_config().lock().get_metric_cache_capacity()); - let collect_task = { - let mut metric_usecase = MetricUsecase::new( - Studio::new(), - MetricsWatchService::new(), - app_config(), - cache_repository.clone(), - Arc::clone(&shutdown_notify), - ); - tokio::spawn(async move { metric_usecase.collect_task().await }) - }; - - let send_task = { - let mut metric_usecase = MetricUsecase::new( - Studio::new(), - MetricsWatchService::new(), - app_config(), - cache_repository, - Arc::clone(&shutdown_notify), - ); - tokio::spawn(async move { metric_usecase.send_task().await }) - }; - - // NOTE: booting... - let (tx, rx) = mpsc::channel::(32); - let db = Arc::new(RwLock::new(HashMap::::new())); - - let transfer_client = MqttClient::new(tx); - - #[cfg(unix)] - let server = { - let runtime_dir = config_dir.clone().join("run"); - fs::create_dir_all(&runtime_dir).unwrap_log(); - let sock_path = runtime_dir.clone().join("nodex.sock"); - - let uds_server = server::new_uds_server(&sock_path, transfer_client); - let permissions = fs::Permissions::from_mode(0o766); - fs::set_permissions(&sock_path, permissions)?; - - uds_server - }; - - #[cfg(windows)] - let server = { - let port_str = - env::var("NODEX_SERVER_PORT").expect("NODEX_SERVER_PORT must be set and valid."); - let port = validate_port(&port_str).expect("Invalid port number."); - server::new_web_server(port, transfer_client) - }; - - let server_handle = server.handle(); - - let message_polling_task = - tokio::spawn(nodex_receive::polling_task(Arc::clone(&shutdown_notify))); - - let server_task = tokio::spawn(server); - let sender_task = tokio::spawn(handlers::sender::handler( - rx, - client, - Arc::clone(&db), - mqtt_topic, - )); - - let should_stop = Arc::new(AtomicBool::new(false)); - let shutdown = tokio::spawn(async move { - handle_signals(should_stop.clone()).await; - - let server_stop = server_handle.stop(true); - shutdown_notify.notify_waiters(); - server_stop.await; - - log::info!("Agent has been successfully stopped."); - }); - - let _ = tokio::try_join!( - server_task, - sender_task, - message_polling_task, - collect_task, - send_task, - shutdown - ) - .unwrap_log(); - Ok(()) -} - -#[cfg(windows)] -fn validate_port(port_str: &str) -> Result { - match port_str.parse::() { - Ok(port) if (1024..=65535).contains(&port) => Ok(port), - _ => Err("Port number must be an integer between 1024 and 65535.".to_string()), - } -} - -#[cfg(unix)] -async fn handle_signals(should_stop: Arc) { - use tokio::signal::unix::{signal, SignalKind}; - - let ctrl_c = tokio::signal::ctrl_c(); - let mut sigterm = signal(SignalKind::terminate()).expect("Failed to bind to SIGTERM"); - - tokio::select! { - _ = ctrl_c => { - log::info!("Received SIGINT"); - should_stop.store(true, Ordering::Relaxed); - }, - _ = sigterm.recv() => { - log::info!("Received SIGTERM"); - should_stop.store(true, Ordering::Relaxed); - }, - } -} - -#[cfg(windows)] -async fn handle_signals(should_stop: Arc) { - tokio::signal::ctrl_c() - .await - .expect("Failed to listen for Ctrl+C"); - log::info!("Received Ctrl+C"); - should_stop.store(true, Ordering::Relaxed); -} - -fn use_cli(command: Option, did: String) { - let network_config = crate::network_config(); - let mut network_config = network_config.lock(); - const SECRET_KEY: &str = "secret_key"; - const PROJECT_DID: &str = "project_did"; - - if let Some(command) = command { - match command { - Commands::Did {} => { - println!("Node ID: {}", did); - } - Commands::Network { command } => match command { - NetworkSubCommands::Set { key, value } => match &*key { - SECRET_KEY => { - network_config.save_secret_key(&value); - log::info!("Network {} is set", SECRET_KEY); - } - PROJECT_DID => { - network_config.save_project_did(&value); - log::info!("Network {} is set", PROJECT_DID); - } - _ => { - log::info!("key is not found"); - } - }, - NetworkSubCommands::Get { key } => match &*key { - SECRET_KEY => { - if let Some(v) = network_config.get_secret_key() { - println!("Network {}: {}", SECRET_KEY, v); - return; - }; - log::info!("Network {} is not set", SECRET_KEY); - } - PROJECT_DID => { - if let Some(v) = network_config.get_project_did() { - log::info!("Network {}: {}", PROJECT_DID, v); - return; - }; - log::info!("Network {} is not set", PROJECT_DID); - } - _ => { - log::info!("key is not found"); - } - }, - }, - } - } -} - -async fn studio_initialize(my_did: String) { - let project_did = { - let network = network_config(); - let network_config = network.lock(); - - // NOTE: check network secret_key and project_did - network_config - .get_secret_key() - .ok_or("Network secret_key is not set. Please set secret_key use cli") - .unwrap_log(); - network_config - .get_project_did() - .expect("Network project_did is not set. Please set project_did use cli") - }; - - let studio = Studio::new(); - studio - .register_device(my_did, project_did) - .await - .unwrap_log(); -} - -async fn send_device_info() { - const VERSION: &str = env!("CARGO_PKG_VERSION"); - const OS: &str = env::consts::OS; - let mac_address: String = match get_mac_address() { - Ok(Some(ma)) => ma.to_string(), - _ => String::from("No MAC address found."), - }; - - let project_did = network_config() - .lock() - .get_project_did() - .expect("Failed to get project_did"); - - let studio = Studio::new(); - studio - .send_device_info( - project_did, - mac_address, - VERSION.to_string(), - OS.to_string(), - ) - .await - .unwrap_log(); -} - -fn log_init() { - let mut builder = env_logger::Builder::from_default_env(); - builder.format(|buf, record| { - use std::io::Write; - writeln!( - buf, - "{} [{}] - {} - {} - {}:{}", - chrono::Utc::now().to_rfc3339(), - record.level(), - record.target(), - record.args(), - record.file().unwrap_or(""), - record.line().unwrap_or(0), - ) - }); - builder.init(); -} - -fn kill_other_self_process() { - let current_pid = get_current_pid().unwrap_log(); - let mut system = System::new_all(); - system.refresh_all(); - - #[cfg(unix)] - let process_name = { "nodex-agent" }; - #[cfg(windows)] - let process_name = { "nodex-agent.exe" }; - - for process in system.processes_by_exact_name(process_name) { - if current_pid == process.pid() { - continue; - } - if process.parent() == Some(current_pid) { - continue; - } - - let pid = process.pid().as_u32(); - if let Err(e) = kill_process(pid) { - log::error!("Failed to kill process with PID: {}. Error: {:?}", pid, e); - } - } -} -#[cfg(unix)] -fn kill_process(pid: u32) -> Result<(), anyhow::Error> { - kill(Pid::from_raw(pid as i32), Signal::SIGTERM).map_err(|e| { - anyhow!( - "Failed to kill nodex process with PID: {}. Error: {}", - pid, - e - ) - })?; - log::info!("nodex Process with PID: {} killed successfully.", pid); - Ok(()) -} - -#[cfg(windows)] -fn kill_process(pid: u32) -> Result<(), anyhow::Error> { - unsafe { - let handle = OpenProcess(PROCESS_TERMINATE, false, pid)?; - if handle.is_invalid() { - return Err(anyhow!( - "Failed to open process with PID: {}. Invalid handle.", - pid - )); - } - - match TerminateProcess(handle, 1) { - Ok(_) => { - log::info!("nodex Process with PID: {} killed successfully.", pid); - } - Err(e) => { - CloseHandle(handle); - return Err(anyhow!( - "Failed to terminate process with PID: {}. Error: {:?}", - pid, - GetLastError() - )); - } - }; - CloseHandle(handle); - } - - Ok(()) -} diff --git a/agent/src/nodex/utils/studio_client.rs b/agent/src/nodex/utils/studio_client.rs index 0bfeb4f2..a24ec217 100644 --- a/agent/src/nodex/utils/studio_client.rs +++ b/agent/src/nodex/utils/studio_client.rs @@ -220,7 +220,7 @@ pub mod tests { origin: String, } - #[actix_rt::test] + #[tokio::test] #[ignore] async fn it_should_success_post() { let client_config: StudioClientConfig = StudioClientConfig { @@ -245,7 +245,7 @@ pub mod tests { assert!(!json.origin.is_empty()); } - #[actix_rt::test] + #[tokio::test] #[ignore] async fn it_should_success_put() { let client_config: StudioClientConfig = StudioClientConfig { diff --git a/agent/src/server.rs b/agent/src/server.rs index 0123150c..fd014863 100644 --- a/agent/src/server.rs +++ b/agent/src/server.rs @@ -1,107 +1,265 @@ -use crate::{controllers, handlers::TransferClient}; -use actix_web::{dev::Server, middleware, web, App, HttpServer}; -use std::path::PathBuf; -use tokio::sync::Mutex as TokioMutex; - -#[allow(dead_code)] -pub struct Context { - pub sender: TokioMutex, -} +use crate::controllers; +use axum::{ + routing::{get, post}, + Router, +}; #[cfg(unix)] -pub fn new_uds_server(sock_path: &PathBuf, sender: C) -> Server { - let context = web::Data::new(Context { - sender: TokioMutex::new(sender), - }); +pub mod unix { + use axum::http::Request; + use axum::Router; + use controller::unix_utils::{ + convention_of_meta_uds_path, recv_fd, remove_file_if_exists, send_fd, + }; + use hyper::body::Incoming; + use hyper_util::{ + rt::{TokioExecutor, TokioIo}, + server, + }; + use std::convert::Infallible; + use std::os::unix::fs::PermissionsExt; + use std::os::unix::io::{AsRawFd, FromRawFd, RawFd}; + use std::path::Path; + use tokio::net::{UnixListener, UnixStream}; + use tokio::signal::unix::{signal, SignalKind}; + use tokio::task::JoinSet; + use tokio_util::sync::CancellationToken; + use tower::Service; + + fn unwrap_infallible(result: Result) -> T { + match result { + Ok(value) => value, + Err(err) => match err {}, + } + } + + pub fn recieve_listener(uds_path: impl AsRef) -> std::io::Result { + let meta_uds_path = convention_of_meta_uds_path(&uds_path)?; + remove_file_if_exists(&meta_uds_path); + let sock = std::os::unix::net::UnixListener::bind(&meta_uds_path)?; + let permissions = std::fs::Permissions::from_mode(0o766); + std::fs::set_permissions(&meta_uds_path, permissions)?; + let (stream, _) = sock.accept()?; + let fd = recv_fd(stream.as_raw_fd())?; + remove_file_if_exists(&meta_uds_path); + let uds = match fd { + Some(fd) => { + let listener = + unsafe { std::os::unix::net::UnixListener::from_raw_fd(fd as RawFd) }; + UnixListener::from_std(listener)? + } + None => { + remove_file_if_exists(&uds_path); + UnixListener::bind(&uds_path)? + } + }; + Ok(uds) + } - HttpServer::new(move || { - App::new() - .wrap(middleware::DefaultHeaders::new().add(("x-version", "0.1.0"))) - .wrap(middleware::Compress::default()) - .wrap(middleware::Logger::default()) - .configure(config_app(&context)) - }) - .bind_uds(sock_path) - .unwrap() - .workers(1) - .run() + pub async fn make_uds_server(router: Router, uds: UnixListener) -> std::io::Result<()> { + // https://github.com/tokio-rs/axum/blob/main/examples/unix-domain-socket/src/main.rs + let mut app = router.into_make_service(); + loop { + let (socket, _remote_addr) = uds.accept().await?; + let tower_service = unwrap_infallible(app.call(&socket).await); + tokio::spawn(async move { + let socket = TokioIo::new(socket); + let hyper_service = + hyper::service::service_fn(move |request: Request| { + tower_service.clone().call(request) + }); + if let Err(err) = server::conn::auto::Builder::new(TokioExecutor::new()) + .serve_connection_with_upgrades(socket, hyper_service) + .await + { + log::error!("failed to serve connection: {}", err); + } + }); + } + } + + pub fn wrap_with_signal_handler( + server: impl std::future::Future> + Send + 'static, + token: CancellationToken, + fd: RawFd, + uds_path: impl AsRef, + ) -> JoinSet> { + let mut set = JoinSet::new(); + let cloned_token = token.clone(); + let tasks = async move { + tokio::select! { + _ = cloned_token.cancelled() => Ok(()), + res = server => res, + } + }; + set.spawn(tasks); + let uds_path = uds_path.as_ref().to_owned(); + set.spawn(async move { + let ctrl_c = tokio::signal::ctrl_c(); + let mut sigterm = signal(SignalKind::terminate())?; + let mut sigusr1 = signal(SignalKind::user_defined1())?; + tokio::select! { + _ = ctrl_c => { + log::info!("Received Ctrl+C"); + token.cancel(); + Ok(()) + }, + _ = sigterm.recv() => { + log::info!("Received SIGTERM"); + token.cancel(); + Ok(()) + }, + _ = sigusr1.recv() => { + log::info!("Received SIGUSR1"); + let send_sock_path = convention_of_meta_uds_path(&uds_path)?; + let () = controller::unix_utils::wait_until_file_created(&send_sock_path) + .map_err(|e| std::io::Error::new(std::io::ErrorKind::NotFound, format!("{}", e)))?; + let stream = loop { + match UnixStream::connect(&send_sock_path).await { + Ok(stream) => break stream, + Err(err) if err.kind() == std::io::ErrorKind::ConnectionRefused => { + // Wait for bind + tokio::time::sleep(std::time::Duration::from_millis(5)).await; + continue; + } + Err(err) => return Err(err), + } + }; + send_fd(stream.as_raw_fd(), Some(fd))?; + token.cancel(); + Ok(()) + } + } + }); + set + } } #[cfg(windows)] -pub fn new_web_server(port: u16, sender: C) -> Server { - let context = web::Data::new(Context { - sender: TokioMutex::new(sender), - }); +pub mod windows { + use crate::nodex::utils::UnwrapLog; + use anyhow::anyhow; + use axum::Router; + use std::future::IntoFuture; + use sysinfo::{get_current_pid, System}; + use windows::Win32::{ + Foundation::{CloseHandle, GetLastError}, + System::Threading::{OpenProcess, TerminateProcess, PROCESS_TERMINATE}, + }; - HttpServer::new(move || { - App::new() - .wrap(middleware::DefaultHeaders::new().add(("x-version", "0.1.0"))) - .wrap(middleware::Compress::default()) - .wrap(middleware::Logger::default()) - .configure(config_app(&context)) - }) - .bind(format!("127.0.0.1:{}", port)) - .unwrap() - .workers(1) - .run() -} + pub async fn new_web_server( + port: u16, + router: Router, + ) -> Result>, std::io::Error> { + // run our app with hyper, listening globally on port 3000 + let listener = tokio::net::TcpListener::bind(format!("127.0.0.1:{}", port)).await?; + Ok(axum::serve(listener, router).into_future()) + } -fn config_app( - context: &web::Data>, -) -> impl Fn(&mut web::ServiceConfig) + '_ { - move |cfg: &mut web::ServiceConfig| { - cfg.app_data(context.clone()) - .route( - "/identifiers", - web::post().to(controllers::public::nodex_create_identifier::handler), - ) - .route( - "/identifiers/{did}", - web::get().to(controllers::public::nodex_find_identifier::handler), - ) - .route( - "/create-verifiable-message", - web::post().to(controllers::public::nodex_create_verifiable_message::handler), - ) - .route( - "/verify-verifiable-message", - web::post().to(controllers::public::nodex_verify_verifiable_message::handler), - ) - .route( - "/create-didcomm-message", - web::post().to(controllers::public::nodex_create_didcomm_message::handler), - ) - .route( - "/verify-didcomm-message", - web::post().to(controllers::public::nodex_verify_didcomm_message::handler), - ) - .route( - "/events", - web::post().to(controllers::public::send_event::handler), - ) - .route( - "/custom-metrics", - web::post().to(controllers::public::send_custom_metric::handler), - ) - .route( - "/attributes", - web::post().to(controllers::public::send_attribute::handler), - ) - // NOTE: Internal (Private) Routes - .service( - web::scope("/internal") - .route( - "/version/get", - web::get().to(controllers::internal::version::handler_get), - ) - .route( - "/version/update", - web::post().to(controllers::internal::version::handler_update), - ) - .route( - "/network", - web::post().to(controllers::internal::network::handler), - ), - ); + pub fn validate_port(port_str: &str) -> Result { + match port_str.parse::() { + Ok(port) if (1024..=65535).contains(&port) => Ok(port), + _ => Err("Port number must be an integer between 1024 and 65535.".to_string()), + } } + + pub fn kill_other_self_process() { + let current_pid = get_current_pid().unwrap_log(); + let mut system = System::new_all(); + system.refresh_all(); + + let process_name = { "nodex-agent.exe" }; + for process in system.processes_by_exact_name(process_name) { + if current_pid == process.pid() { + continue; + } + if process.parent() == Some(current_pid) { + continue; + } + + let pid = process.pid().as_u32(); + if let Err(e) = kill_process(pid) { + log::error!("Failed to kill process with PID: {}. Error: {:?}", pid, e); + } + } + } + + fn kill_process(pid: u32) -> Result<(), anyhow::Error> { + unsafe { + let handle = OpenProcess(PROCESS_TERMINATE, false, pid)?; + if handle.is_invalid() { + return Err(anyhow!( + "Failed to open process with PID: {}. Invalid handle.", + pid + )); + } + + match TerminateProcess(handle, 1) { + Ok(_) => { + log::info!("nodex Process with PID: {} killed successfully.", pid); + } + Err(e) => { + CloseHandle(handle); + return Err(anyhow!( + "Failed to terminate process with PID: {}. Error: {:?}", + pid, + GetLastError() + )); + } + }; + CloseHandle(handle); + } + + Ok(()) + } +} + +pub fn make_router() -> Router { + Router::new() + .route( + "/identifiers", + post(controllers::public::nodex_create_identifier::handler), + ) + .route( + "/identifiers/:did", + get(controllers::public::nodex_find_identifier::handler), + ) + .route( + "/create-verifiable-message", + post(controllers::public::nodex_create_verifiable_message::handler), + ) + .route( + "/verify-verifiable-message", + post(controllers::public::nodex_verify_verifiable_message::handler), + ) + .route( + "/create-didcomm-message", + post(controllers::public::nodex_create_didcomm_message::handler), + ) + .route( + "/verify-didcomm-message", + post(controllers::public::nodex_verify_didcomm_message::handler), + ) + .route("/events", post(controllers::public::send_event::handler)) + .route( + "/custom-metrics", + post(controllers::public::send_custom_metric::handler), + ) + .route( + "/attributes", + post(controllers::public::send_attribute::handler), + ) + // NOTE: Internal (Private) Routes + .route( + "/internal/version/get", + get(controllers::internal::version::handler_get), + ) + .route( + "/internal/version/update", + post(controllers::internal::version::handler_update), + ) + .route( + "/internal/network", + post(controllers::internal::network::handler), + ) } diff --git a/agent/src/services/nodex.rs b/agent/src/services/nodex.rs index a158ce12..ad79724f 100644 --- a/agent/src/services/nodex.rs +++ b/agent/src/services/nodex.rs @@ -3,19 +3,21 @@ use crate::nodex::keyring; use crate::nodex::utils::sidetree_client::SideTreeClient; use crate::{app_config, server_config}; use anyhow; -use bytes::Bytes; +use controller::managers::{ + resource::ResourceManagerTrait, + runtime::{RuntimeManagerImpl, RuntimeManagerWithoutAsync, State}, +}; +use controller::validator::storage::check_storage; use protocol::did::did_repository::{DidRepository, DidRepositoryImpl}; use protocol::did::sidetree::payload::DidResolutionResponse; -use std::{ - fs, - io::Cursor, - path::{Path, PathBuf}, - process::Command, -}; -use zip::ZipArchive; -#[cfg(unix)] -use daemonize::Daemonize; +#[cfg(windows)] +mod windows_imports { + pub use controller::managers::resource::WindowsResourceManager; +} + +#[cfg(windows)] +use windows_imports::*; pub struct NodeX { did_repository: DidRepositoryImpl, @@ -68,84 +70,43 @@ impl NodeX { Ok(res) } - pub async fn update_version( - &self, - binary_url: &str, - output_path: PathBuf, - ) -> anyhow::Result<()> { - anyhow::ensure!( - binary_url.starts_with("https://github.com/nodecross/nodex/releases/download/"), - "Invalid url" - ); - - #[cfg(unix)] - let agent_filename = { "nodex-agent" }; + pub async fn update_version(&self, binary_url: &str) -> anyhow::Result<()> { #[cfg(windows)] - let agent_filename = { "nodex-agent.exe" }; - - let agent_path = output_path.join(agent_filename); - - let response = reqwest::get(binary_url).await?; - let content = response.bytes().await?; - if PathBuf::from(&agent_path).exists() { - fs::remove_file(&agent_path)?; + { + unimplemented!(); } - self.extract_zip(content, &output_path)?; - - self.run_agent(&agent_path)?; - - Ok(()) - } - fn extract_zip(&self, archive_data: Bytes, output_path: &Path) -> anyhow::Result<()> { - let cursor = Cursor::new(archive_data); - let mut archive = ZipArchive::new(cursor)?; - - for i in 0..archive.len() { - let mut file = archive.by_index(i)?; - let file_path = output_path.join(file.mangled_name()); - - if file.is_file() { - if let Some(parent) = file_path.parent() { - std::fs::create_dir_all(parent)?; - } - let mut output_file = fs::File::create(&file_path)?; - std::io::copy(&mut file, &mut output_file)?; - } else if file.is_dir() { - std::fs::create_dir_all(&file_path)?; + #[cfg(unix)] + { + let handler = + controller::managers::mmap_storage::MmapHandler::new("nodex_runtime_info")?; + let mut runtime_manager = RuntimeManagerImpl::new_by_agent( + handler, + controller::managers::unix_process_manager::UnixProcessManager, + ); + let agent_path = &runtime_manager.get_runtime_info()?.exec_path; + let output_path = agent_path + .parent() + .ok_or(anyhow::anyhow!("Failed to get path of parent directory"))?; + if !check_storage(output_path) { + log::error!("Not enough storage space: {:?}", output_path); + anyhow::bail!("Not enough storage space"); } - } - - Ok(()) - } - - #[cfg(unix)] - fn run_agent(&self, agent_path: &Path) -> anyhow::Result<()> { - Command::new("chmod").arg("+x").arg(agent_path).status()?; - - let daemonize = Daemonize::new(); - daemonize.start().expect("Failed to update nodex process"); - std::process::Command::new(agent_path) - .spawn() - .expect("Failed to execute command") - .wait()?; - Ok(()) - } + let resource_manager = + controller::managers::resource::UnixResourceManager::new(agent_path); - #[cfg(windows)] - fn run_agent(&self, agent_path: &Path) -> anyhow::Result<()> { - let agent_path_str = agent_path - .to_str() - .ok_or_else(|| anyhow::anyhow!("Failed to convert agent_path to string"))?; + resource_manager.backup().map_err(|e| { + log::error!("Failed to backup: {}", e); + anyhow::anyhow!(e) + })?; - let status = Command::new("cmd") - .args(&["/C", "start", agent_path_str]) - .status()?; + resource_manager + .download_update_resources(binary_url, Some(output_path)) + .await + .map_err(|e| anyhow::anyhow!(e))?; - if !status.success() { - eprintln!("Command execution failed with status: {}", status); - } else { - println!("Started child process"); + runtime_manager.launch_controller(agent_path)?; + runtime_manager.update_state(State::Update)?; } Ok(()) diff --git a/agent/src/usecase/metric_usecase.rs b/agent/src/usecase/metric_usecase.rs index 914c04e9..67c8a1f8 100644 --- a/agent/src/usecase/metric_usecase.rs +++ b/agent/src/usecase/metric_usecase.rs @@ -2,8 +2,8 @@ use crate::config::SingletonAppConfig; use crate::repository::metric_repository::{ MetricStoreRepository, MetricsCacheRepository, MetricsWatchRepository, }; -use std::{sync::Arc, time::Duration}; -use tokio::sync::Notify; +use std::time::Duration; +use tokio_util::sync::CancellationToken; pub struct MetricUsecase where @@ -15,7 +15,7 @@ where watch_repository: W, config: Box, cache_repository: C, - shutdown_notify: Arc, + shutdown_token: CancellationToken, } impl MetricUsecase @@ -29,14 +29,14 @@ where watch_repository: W, config: Box, cache_repository: C, - shutdown_notify: Arc, + shutdown_token: CancellationToken, ) -> Self { MetricUsecase { store_repository, watch_repository, config, cache_repository, - shutdown_notify, + shutdown_token, } } @@ -52,7 +52,7 @@ where } log::info!("collected metrics"); } - _ = self.shutdown_notify.notified() => { + _ = self.shutdown_token.cancelled() => { break; }, } @@ -79,7 +79,7 @@ where Err(e) => log::error!("failed to send metric{:?}", e), } } - _ = self.shutdown_notify.notified() => { + _ = self.shutdown_token.cancelled() => { break; }, } @@ -127,31 +127,31 @@ mod tests { #[tokio::test] async fn test_collect_task() { - let notify = Arc::new(Notify::new()); - let notify_clone = notify.clone(); + let token = CancellationToken::new(); + let cloned_token = token.clone(); let mut usecase = MetricUsecase { store_repository: MockMetricStoreRepository {}, watch_repository: MockMetricWatchRepository {}, config: app_config(), cache_repository: MetricsInMemoryCacheService::new(1 << 16), - shutdown_notify: notify_clone, + shutdown_token: cloned_token, }; - notify.notify_one(); + token.cancel(); usecase.collect_task().await; } #[tokio::test] async fn test_send_task() { - let notify = Arc::new(Notify::new()); - let notify_clone = notify.clone(); + let token = CancellationToken::new(); + let cloned_token = token.clone(); let mut usecase = MetricUsecase { store_repository: MockMetricStoreRepository {}, watch_repository: MockMetricWatchRepository {}, config: app_config(), cache_repository: MetricsInMemoryCacheService::new(1 << 16), - shutdown_notify: notify_clone, + shutdown_token: cloned_token, }; - notify.notify_one(); + token.cancel(); usecase.send_task().await; } } diff --git a/bin/Cargo.toml b/bin/Cargo.toml new file mode 100644 index 00000000..19cb3a20 --- /dev/null +++ b/bin/Cargo.toml @@ -0,0 +1,32 @@ +[package] +authors.workspace = true +build = "build.rs" +categories.workspace = true +description.workspace = true +documentation.workspace = true +edition.workspace = true +homepage.workspace = true +keywords.workspace = true +license-file.workspace = true +name = "bin" +readme.workspace = true +repository.workspace = true +version.workspace = true + +[[bin]] +name = "nodex-agent" +path = "src/main.rs" + +# See more keys and their definitions at https://doc.rust-lang.org/cargo/reference/manifest.html + +[dependencies] +agent = { path = "../agent" } +chrono = { workspace = true } +clap = { workspace = true } +controller = { path = "../controller" } +env_logger = { workspace = true } +log = { workspace = true } +shadow-rs = { workspace = true } + +[build-dependencies] +shadow-rs = { workspace = true } diff --git a/bin/build.rs b/bin/build.rs new file mode 100644 index 00000000..4a0dfc45 --- /dev/null +++ b/bin/build.rs @@ -0,0 +1,3 @@ +fn main() -> shadow_rs::SdResult<()> { + shadow_rs::new() +} diff --git a/bin/src/main.rs b/bin/src/main.rs new file mode 100644 index 00000000..32aa37d7 --- /dev/null +++ b/bin/src/main.rs @@ -0,0 +1,65 @@ +extern crate env_logger; +use clap::{Parser, Subcommand}; +use shadow_rs::shadow; + +shadow!(build); + +#[derive(Parser, Debug)] +#[clap( + name = "nodex-agent", + version = shadow_rs::formatcp!("v{} ({} {})\n{} @ {}", build::PKG_VERSION, build::SHORT_COMMIT, build::BUILD_TIME_3339, build::RUST_VERSION, build::BUILD_TARGET), + about, + long_about = None +)] +struct Cli { + #[command(subcommand)] + command: Option, + + #[clap(flatten)] + agent_options: agent::cli::AgentOptions, +} + +#[derive(Subcommand, Debug)] +enum Commands { + Controller, + Controlled, +} + +fn log_init() { + let mut builder = env_logger::Builder::from_default_env(); + builder.format(|buf, record| { + use std::io::Write; + writeln!( + buf, + "{} [{}] - {} - {} - {}:{}", + chrono::Utc::now().to_rfc3339(), + record.level(), + record.target(), + record.args(), + record.file().unwrap_or(""), + record.line().unwrap_or(0), + ) + }); + builder.init(); +} + +fn main() { + std::env::set_var("RUST_LOG", "info"); + log_init(); + let cli = Cli::parse(); + + if let Some(Commands::Controller) = &cli.command { + #[cfg(unix)] + let _ = controller::run(); + #[cfg(not(unix))] + log::error!("Controller is not supported on this platform."); + } else { + let controlled = cli.command.map(|_| true).unwrap_or(false); + let options = if cli.agent_options.config || cli.agent_options.command.is_some() { + cli.agent_options + } else { + agent::cli::AgentOptions::default() + }; + let _ = agent::run(controlled, &options); + } +} diff --git a/controller/Cargo.toml b/controller/Cargo.toml new file mode 100644 index 00000000..1399ff7f --- /dev/null +++ b/controller/Cargo.toml @@ -0,0 +1,62 @@ +[package] +authors.workspace = true +categories.workspace = true +description.workspace = true +documentation.workspace = true +edition.workspace = true +homepage.workspace = true +keywords.workspace = true +license-file.workspace = true +name = "controller" +readme.workspace = true +repository.workspace = true +version.workspace = true + +# See more keys and their definitions at https://doc.rust-lang.org/cargo/reference/manifest.html + +[dependencies] +bytes = { workspace = true } +chrono = { workspace = true } +dirs = { workspace = true } +env_logger = { workspace = true } +flate2 = "1.0.34" +fs2 = { workspace = true } +glob = "0.3.1" +http-body-util = { version = "0.1" } +hyper = { version = "1.2", features = ["client", "http1"] } +hyper-util = { version = "0.1.3", features = [ + "client-legacy", + "http1", + "tokio", +] } +lazy_static = "1.4" +log = { workspace = true } +nix = { version = "0.29", features = ["feature", "fs", "mman", "process", "signal", "socket", "uio"] } +notify = "7.0.0" +reqwest = { workspace = true } +semver = { version = "1.0.23", features = ["serde"] } +serde = { workspace = true } +serde_json = { workspace = true } +serde_yaml = "0.9.34" +shadow-rs = { workspace = true } +tar = "0.4.43" +thiserror = { workspace = true } +tokio = { workspace = true } +trait-variant = { workspace = true } +zip = { workspace = true } + +[target.'cfg(unix)'.dependencies] +hyperlocal = { git = "https://github.com/softprops/hyperlocal.git", rev = "34dc857" } +users = "0.11.0" + +[build-dependencies] +shadow-rs = { workspace = true } + +[dev-dependencies] +filename = "0.1.1" +filetime = "0.2.25" +httpmock = "0.7.0" +libc = "0.2.167" +mockito = "1.6.1" +serial_test = "3.2.0" +tempfile = "3.14.0" diff --git a/controller/build.rs b/controller/build.rs new file mode 100644 index 00000000..4a0dfc45 --- /dev/null +++ b/controller/build.rs @@ -0,0 +1,3 @@ +fn main() -> shadow_rs::SdResult<()> { + shadow_rs::new() +} diff --git a/controller/src/config.rs b/controller/src/config.rs new file mode 100644 index 00000000..21eb3002 --- /dev/null +++ b/controller/src/config.rs @@ -0,0 +1,43 @@ +use lazy_static::lazy_static; +use std::fs; +use std::path::PathBuf; +use std::sync::Mutex; + +pub struct Config { + pub config_dir: PathBuf, + #[allow(dead_code)] + pub nodex_dir: PathBuf, + #[allow(dead_code)] + pub runtime_dir: PathBuf, + pub uds_path: PathBuf, +} + +impl Config { + pub fn new() -> Self { + let home_dir = dirs::home_dir().expect("Failed to get home directory"); + let config_dir = home_dir.join(".config").join("nodex"); + fs::create_dir_all(&config_dir).expect("Failed to create config directory"); + + let nodex_dir = home_dir.join(".nodex"); + let runtime_dir = nodex_dir.join("run"); + + fs::create_dir_all(&runtime_dir).expect("Failed to create runtime directory"); + + let sock_path = runtime_dir.join("nodex.sock"); + + Config { + config_dir, + nodex_dir, + runtime_dir, + uds_path: sock_path, + } + } +} + +lazy_static! { + static ref CONFIG: Mutex = Mutex::new(Config::new()); +} + +pub fn get_config() -> &'static Mutex { + &CONFIG +} diff --git a/controller/src/lib.rs b/controller/src/lib.rs new file mode 100644 index 00000000..6af33538 --- /dev/null +++ b/controller/src/lib.rs @@ -0,0 +1,111 @@ +use crate::config::get_config; +use crate::managers::runtime::{ProcessManager, RuntimeInfoStorage, RuntimeManagerImpl}; +use crate::state::handler::handle_state; +use std::sync::Arc; +use tokio::sync::Mutex; +#[cfg(unix)] +type ProcessManagerImpl = crate::managers::unix_process_manager::UnixProcessManager; + +#[cfg(windows)] +type ProcessManagerImpl = crate::managers::windows_process_manager::WindowsProcessManager; + +mod config; +pub mod managers; +pub mod state; +#[cfg(unix)] +pub mod unix_utils; +pub mod validator; + +#[tokio::main] +pub async fn run() -> std::io::Result<()> { + #[cfg(unix)] + let handler = crate::managers::mmap_storage::MmapHandler::new("nodex_runtime_info") + .expect("Failed to create MmapHandler"); + #[cfg(windows)] + let handler = { + let path = get_config() + .lock() + .unwrap() + .runtime_dir + .join("runtime_info.json"); + crate::managers::file_storage::FileHandler::new(path).expect("Failed to create FileHandler") + }; + let uds_path = get_config().lock().unwrap().uds_path.clone(); + let (runtime_manager, mut state_rx) = + RuntimeManagerImpl::new_by_controller(handler, ProcessManagerImpl {}, uds_path) + .expect("Failed to create RuntimeManager"); + + let runtime_manager = Arc::new(Mutex::new(runtime_manager)); + let shutdown_handle = tokio::spawn(handle_signals(runtime_manager.clone())); + + tokio::spawn(async move { + let mut description = "Initial state"; + while { + let current_state = *state_rx.borrow(); + log::info!("Worker: {}: {:?}", description, current_state); + { + let mut _runtime_manager = runtime_manager.lock().await; + if let Err(e) = handle_state(current_state, &mut *_runtime_manager).await { + log::error!("Worker: Failed to handle {}: {}", description, e); + } + } + description = "State change"; + state_rx.changed().await.is_ok() + } {} + }); + + let _ = shutdown_handle.await; + log::info!("Shutdown handler completed successfully."); + + Ok(()) +} + +#[cfg(unix)] +pub async fn handle_signals(runtime_manager: Arc>>) +where + H: RuntimeInfoStorage + Send + Sync + 'static, + P: ProcessManager + Send + Sync + 'static, +{ + use tokio::signal::unix::{signal, SignalKind}; + + let ctrl_c = tokio::signal::ctrl_c(); + let mut sigterm = signal(SignalKind::terminate()).expect("Failed to bind to SIGTERM"); + let mut sigabrt = signal(SignalKind::user_defined1()).expect("Failed to bind to SIGABRT"); + let mut sigint = signal(SignalKind::quit()).expect("Failed to bind to SIGINT"); + + // We have the following as a convention. + // - Only the controller terminates with SIGTERM. + // - SIGUSR1 is sent to the Agent by SIGINT etc. The Agent that receives SIGUSR1 sends fd of the Unix domain socket. + tokio::select! { + _ = sigint.recv() => { + if let Err(e) = runtime_manager.lock().await.cleanup_all() { + log::error!("Failed to handle sigint: {}", e); + } + }, + _ = ctrl_c => { + if let Err(e) = runtime_manager.lock().await.cleanup_all() { + log::error!("Failed to handle CTRL+C: {}", e); + } + }, + _ = sigterm.recv() => { + log::info!("Received SIGTERM. Gracefully stopping application."); + // Just to be sure + let _ = runtime_manager.lock().await.cleanup(); + }, + _ = sigabrt.recv() => { + if let Err(e) = runtime_manager.lock().await.cleanup_all() { + log::error!("Failed to handle SIGABRT: {}", e); + } + } + } + log::info!("All processes have been successfully terminated."); +} + +#[cfg(windows)] +pub async fn handle_signals(runtime_manager: Arc>>) +where + H: RuntimeInfoStorage + Send + Sync + 'static, + P: ProcessManager + Send + Sync + 'static, +{ + unimplemented!("implemented for Windows."); +} diff --git a/controller/src/managers/file_storage.rs b/controller/src/managers/file_storage.rs new file mode 100644 index 00000000..7d24ceb2 --- /dev/null +++ b/controller/src/managers/file_storage.rs @@ -0,0 +1,215 @@ +use super::runtime::{RuntimeError, RuntimeInfo, RuntimeInfoStorage, State}; +use fs2::FileExt; +use std::fs::{File, OpenOptions}; +use std::io::{Read, Seek, Write}; +use std::path::PathBuf; + +#[derive(Debug)] +pub struct FileHandler { + file: File, +} + +impl RuntimeInfoStorage for FileHandler { + fn read(&mut self) -> Result { + let mut content = String::new(); + self.file + .read_to_string(&mut content) + .map_err(RuntimeError::FileRead)?; + self.file + .seek(std::io::SeekFrom::Start(0)) + .map_err(RuntimeError::FileRead)?; + if content.trim().is_empty() { + // We assume that the file is empty means that it is the first execution. + let process_infos = [None, None, None, None]; + return Ok(RuntimeInfo { + state: State::Idle, + process_infos, + exec_path: std::env::current_exe().map_err(RuntimeError::FailedCurrentExe)?, + }); + } + serde_json::from_str(&content).map_err(RuntimeError::JsonDeserialize) + } + + fn apply_with_lock(&mut self, operation: F) -> Result<(), RuntimeError> + where + F: FnOnce(&mut RuntimeInfo) -> Result<(), RuntimeError>, + { + self.file + .lock_exclusive() + .map_err(self.handle_err(RuntimeError::FileLock))?; + + let mut runtime_info = self.read().map_err(self.handle_err_id())?; + + operation(&mut runtime_info).map_err(self.handle_err_id())?; + + self.write_locked(&runtime_info) + .map_err(self.handle_err_id())?; + self.file.unlock().map_err(RuntimeError::FileUnlock)?; + + Ok(()) + } +} + +impl FileHandler { + pub fn new(path: PathBuf) -> Result { + let file = OpenOptions::new() + .read(true) + .write(true) + .create(true) + .truncate(false) + .open(&path) + .map_err(RuntimeError::FileOpen)?; + Ok(FileHandler { file }) + } + + fn handle_err_id(&mut self) -> impl Fn(RuntimeError) -> RuntimeError + '_ { + self.handle_err(|x| x) + } + + fn handle_err<'a, E>( + &'a mut self, + error: impl Fn(E) -> RuntimeError + 'a, + ) -> impl Fn(E) -> RuntimeError + 'a { + move |e| { + let res = self.file.unlock().map_err(RuntimeError::FileUnlock); + if let Err(res) = res { + return res; + } + error(e) + } + } + + fn write_locked(&mut self, runtime_info: &RuntimeInfo) -> Result<(), RuntimeError> { + let json_data = + serde_json::to_string_pretty(runtime_info).map_err(RuntimeError::JsonSerialize)?; + + self.file.set_len(0).map_err(RuntimeError::FileWrite)?; + + self.file + .seek(std::io::SeekFrom::Start(0)) + .map_err(RuntimeError::FileWrite)?; + + self.file + .write_all(json_data.as_bytes()) + .map_err(RuntimeError::FileWrite)?; + + self.file.flush().map_err(RuntimeError::FileWrite)?; + + self.file + .seek(std::io::SeekFrom::Start(0)) + .map_err(RuntimeError::FileWrite)?; + + log::info!("File written successfully"); + Ok(()) + } +} + +#[cfg(all(test, unix))] +mod tests { + use super::*; + use crate::managers::runtime::{ + FeatType, ProcessInfo, RuntimeInfo, RuntimeManagerImpl, RuntimeManagerWithoutAsync, + }; + use crate::managers::unix_process_manager::UnixProcessManager; + use std::fs::File; + use tempfile::tempdir; + + #[test] + fn test_read_write_runtime_info() { + let initial_runtime_info = RuntimeInfo { + state: State::Update, + process_infos: [None, None, None, None], + exec_path: std::env::current_exe().unwrap(), + }; + let tempdir = tempdir().expect("Failed to create temporary directory"); + let temp_file_path = tempdir.path().join("runtime_info.json"); + let mut file_handler = FileHandler::new(temp_file_path.clone()).unwrap(); + + file_handler + .apply_with_lock(|runtime_info| { + *runtime_info = initial_runtime_info.clone(); + Ok(()) + }) + .unwrap(); + + let read_runtime_info = file_handler.read().unwrap(); + assert_eq!(read_runtime_info, initial_runtime_info); + } + + #[test] + fn test_update_state() { + let temp_dir = tempdir().expect("Failed to create temporary directory"); + let temp_file_path = temp_dir.path().join("runtime_info.json"); + File::create(&temp_file_path).expect("Failed to create temporary runtime_info.json"); + let file_handler = FileHandler::new(temp_file_path.clone()).unwrap(); + let mut runtime_manager = + RuntimeManagerImpl::new_by_agent(file_handler, UnixProcessManager); + + runtime_manager + .update_state_without_send(State::Update) + .unwrap(); + + let state = runtime_manager.get_runtime_info().unwrap().state; + + assert_eq!(state, State::Update); + } + + #[test] + fn test_cleanup_process_info() { + let temp_dir = tempdir().expect("Failed to create temporary directory"); + let temp_file_path = temp_dir.path().join("runtime_info.json"); + File::create(&temp_file_path).expect("Failed to create temporary runtime_info.json"); + + let process_info = ProcessInfo::new((1 << 22) + 1, FeatType::Agent); + let runtime_info = RuntimeInfo { + state: State::Idle, + process_infos: [Some(process_info.clone()), None, None, None], + exec_path: std::env::current_exe().unwrap(), + }; + let mut file_handler = FileHandler::new(temp_file_path.clone()).unwrap(); + file_handler.write_locked(&runtime_info).unwrap(); + + let mut runtime_manager = RuntimeManagerImpl::new_by_controller( + file_handler, + UnixProcessManager, + "/tmp/nodex.sock", + ) + .unwrap() + .0; + + let process_infos: Vec<_> = runtime_manager + .get_runtime_info() + .unwrap() + .process_infos + .into_iter() + .flatten() + .collect(); + assert!(!process_infos.contains(&process_info)); + } + + // TODO: Fix fork bomb + // #[tokio::test] + // async fn test_launch_and_terminate_agent() { + // let temp_dir = tempfile::tempdir().unwrap(); + // let temp_file_path = temp_dir.path().join("runtime_info.json"); + // File::create(&temp_file_path).expect("Failed to create temporary runtime_info.json"); + // let uds_path = temp_dir.path().join("test_socket"); + // let file_handler = FileHandler::new(temp_file_path.clone()).unwrap(); + // let mut manager = RuntimeManagerImpl::new_by_controller( + // file_handler, + // UnixProcessManager, + // uds_path, + // ) + // .unwrap() + // .0; + + // let process_info = manager.launch_agent(false); + // assert!(process_info.is_ok(), "Agent launch should succeed"); + + // let process_info = process_info.unwrap(); + // assert!( + // manager.kill_process(&process_info).is_ok(), + // "Agent termination should succeed" + // ); + // } +} diff --git a/controller/src/managers/mmap_storage.rs b/controller/src/managers/mmap_storage.rs new file mode 100644 index 00000000..a53d18a3 --- /dev/null +++ b/controller/src/managers/mmap_storage.rs @@ -0,0 +1,294 @@ +use super::runtime::{RuntimeError, RuntimeInfo, RuntimeInfoStorage, State}; +use nix::errno::Errno; +use nix::fcntl::OFlag; +use nix::sys::mman::{ + mlock, mmap, msync, munlock, munmap, shm_open, shm_unlink, MapFlags, MsFlags, ProtFlags, +}; +use nix::sys::stat::Mode; +use nix::unistd::ftruncate; +use std::io::Write; +use std::ops::{Deref, DerefMut}; +use std::path::{Path, PathBuf}; + +pub struct MmapHandler { + name: PathBuf, + ptr: core::ptr::NonNull, + len: core::num::NonZeroUsize, +} + +impl Deref for MmapHandler { + type Target = [u8]; + + #[inline] + fn deref(&self) -> &[u8] { + unsafe { std::slice::from_raw_parts(self.ptr.as_ptr() as *const u8, self.len.into()) } + } +} + +impl DerefMut for MmapHandler { + #[inline] + fn deref_mut(&mut self) -> &mut [u8] { + unsafe { std::slice::from_raw_parts_mut(self.ptr.as_ptr() as *mut u8, self.len.into()) } + } +} + +impl AsRef<[u8]> for MmapHandler { + #[inline] + fn as_ref(&self) -> &[u8] { + self.deref() + } +} + +impl AsMut<[u8]> for MmapHandler { + #[inline] + fn as_mut(&mut self) -> &mut [u8] { + self.deref_mut() + } +} + +impl std::fmt::Debug for MmapHandler { + fn fmt(&self, fmt: &mut std::fmt::Formatter) -> std::fmt::Result { + fmt.debug_struct("MmapHandler") + .field("ptr", &self.ptr) + .field("len", &self.len) + .finish() + } +} + +unsafe impl Send for MmapHandler {} +unsafe impl Sync for MmapHandler {} + +#[inline] +fn _e2e(e: Errno) -> std::io::Error { + std::io::Error::from_raw_os_error(e as core::ffi::c_int) +} + +impl MmapHandler { + // ref: https://stackoverflow.com/questions/62320764/how-to-create-shared-memory-after-fork + pub fn new(name: impl AsRef) -> Result { + // We assume that data is sufficiently small. + let length = core::num::NonZero::new(10000).unwrap(); + // Open without creation + let fd = shm_open(name.as_ref(), OFlag::O_RDWR, Mode::S_IRUSR | Mode::S_IWUSR); + let fd = match fd { + Ok(fd) => fd, + Err(Errno::ENOENT) => { + let fd = shm_open( + name.as_ref(), + OFlag::O_CREAT | OFlag::O_EXCL | OFlag::O_RDWR, + Mode::S_IRUSR | Mode::S_IWUSR, + ) + .map_err(_e2e) + .map_err(RuntimeError::FileOpen)?; + // We must truncate size of shared memory at the time of initial creation. + ftruncate(&fd, Into::::into(length) as i64) + .map_err(_e2e) + .map_err(RuntimeError::FileOpen)?; + fd + } + Err(err) => return Err(RuntimeError::FileOpen(_e2e(err))), + }; + let ptr = unsafe { + mmap( + None, + length, + ProtFlags::PROT_READ | ProtFlags::PROT_WRITE, + MapFlags::MAP_NORESERVE | MapFlags::MAP_SHARED, + fd, + 0, + ) + .map_err(_e2e) + .map_err(RuntimeError::FileOpen)? + }; + Ok(MmapHandler { + ptr, + len: length, + name: name.as_ref().to_path_buf(), + }) + } + + pub fn close(self) -> Result<(), RuntimeError> { + unsafe { + munmap(self.ptr, self.len.into()) + .map_err(_e2e) + .map_err(RuntimeError::FileRemove)?; + shm_unlink(&self.name) + .map_err(_e2e) + .map_err(RuntimeError::FileRemove)?; + } + Ok(()) + } + + fn lock(&self) -> Result<(), RuntimeError> { + unsafe { + mlock(self.ptr, self.len.into()) + .map_err(_e2e) + .map_err(RuntimeError::FileLock) + } + } + + fn unlock(&self) -> Result<(), RuntimeError> { + unsafe { + munlock(self.ptr, self.len.into()) + .map_err(_e2e) + .map_err(RuntimeError::FileUnlock) + } + } + + fn flush(&self) -> Result<(), RuntimeError> { + unsafe { + msync(self.ptr, self.len.into(), MsFlags::MS_SYNC) + .map_err(_e2e) + .map_err(RuntimeError::FileWrite) + } + } + + fn handle_err<'a, E>( + &'a mut self, + error: impl Fn(E) -> RuntimeError + 'a, + ) -> impl Fn(E) -> RuntimeError + 'a { + move |e| { + let res = self.unlock(); + if let Err(res) = res { + return res; + } + error(e) + } + } + + fn handle_err_id(&mut self) -> impl Fn(RuntimeError) -> RuntimeError + '_ { + self.handle_err(|x| x) + } + + fn write_locked(&mut self, runtime_info: &RuntimeInfo) -> Result<(), RuntimeError> { + let json_data = serde_json::to_string(runtime_info).map_err(RuntimeError::JsonSerialize)?; + let mut json_data = json_data.into_bytes(); + json_data.push(0); + (&mut self[..]) + .write(&json_data) + .map_err(RuntimeError::FileWrite)?; + self.flush()?; + log::info!("File written successfully"); + Ok(()) + } +} + +impl RuntimeInfoStorage for MmapHandler { + fn read(&mut self) -> Result { + self.lock()?; + let cstr = std::ffi::CStr::from_bytes_until_nul(self) + .ok() + .and_then(|s| s.to_str().ok()) + .ok_or(std::io::Error::new( + std::io::ErrorKind::InvalidData, + "Failed to read runtime info", + )) + .map_err(RuntimeError::FileRead); + self.unlock()?; + let cstr = cstr?.trim(); + if cstr.is_empty() { + // We assume that memmap is empty means that it is the first execution. + let process_infos = [None, None, None, None]; + return Ok(RuntimeInfo { + state: State::Idle, + process_infos, + exec_path: std::env::current_exe().map_err(RuntimeError::FailedCurrentExe)?, + }); + } + serde_json::from_str(cstr).map_err(RuntimeError::JsonDeserialize) + } + + fn apply_with_lock(&mut self, operation: F) -> Result<(), RuntimeError> + where + F: FnOnce(&mut RuntimeInfo) -> Result<(), RuntimeError>, + { + self.lock()?; + let mut runtime_info = self.read().map_err(self.handle_err_id())?; + + operation(&mut runtime_info).map_err(self.handle_err_id())?; + + self.write_locked(&runtime_info) + .map_err(self.handle_err_id())?; + self.unlock()?; + Ok(()) + } +} + +#[cfg(all(test, unix))] +mod tests { + use super::*; + use crate::managers::runtime::{ + FeatType, ProcessInfo, RuntimeInfo, RuntimeManagerImpl, RuntimeManagerWithoutAsync, + }; + use crate::managers::unix_process_manager::UnixProcessManager; + + #[test] + fn test_read_write_runtime_info() { + let initial_runtime_info = RuntimeInfo { + state: State::Idle, + process_infos: [None, None, None, None], + exec_path: std::env::current_exe().unwrap(), + }; + + let mut mmap_handler = MmapHandler::new("test_shm").unwrap(); + + mmap_handler + .apply_with_lock(|runtime_info| { + *runtime_info = initial_runtime_info.clone(); + Ok(()) + }) + .unwrap(); + + let read_runtime_info = mmap_handler.read().unwrap(); + assert_eq!(read_runtime_info, initial_runtime_info); + mmap_handler.close().unwrap(); + } + + #[test] + fn test_update_state() { + let mmap_handler = MmapHandler::new("test_shm_state").unwrap(); + let mut runtime_manager = + RuntimeManagerImpl::new_by_agent(mmap_handler, UnixProcessManager); + + runtime_manager + .update_state_without_send(State::Update) + .unwrap(); + + let state = runtime_manager.get_runtime_info().unwrap().state; + + assert_eq!(state, State::Update); + MmapHandler::new("test_shm_state").unwrap().close().unwrap(); + } + + #[test] + fn test_cleanup_process_info() { + let process_info = ProcessInfo::new((1 << 22) + 1, FeatType::Agent); + let runtime_info = RuntimeInfo { + state: State::Idle, + process_infos: [Some(process_info.clone()), None, None, None], + exec_path: std::env::current_exe().unwrap(), + }; + let mut mmap_handler = MmapHandler::new("test_cleanup_process_info_shm").unwrap(); + mmap_handler.write_locked(&runtime_info).unwrap(); + let mut runtime_manager = RuntimeManagerImpl::new_by_controller( + mmap_handler, + UnixProcessManager, + "/tmp/nodex.sock", + ) + .unwrap() + .0; + + let process_infos: Vec<_> = runtime_manager + .get_runtime_info() + .unwrap() + .process_infos + .into_iter() + .flatten() + .collect(); + assert!(!process_infos.contains(&process_info)); + MmapHandler::new("test_cleanup_process_info_shm") + .unwrap() + .close() + .unwrap(); + } +} diff --git a/controller/src/managers/mod.rs b/controller/src/managers/mod.rs new file mode 100644 index 00000000..92c4e623 --- /dev/null +++ b/controller/src/managers/mod.rs @@ -0,0 +1,9 @@ +pub mod file_storage; +#[cfg(unix)] +pub mod mmap_storage; +pub mod resource; +pub mod runtime; +#[cfg(unix)] +pub mod unix_process_manager; +#[cfg(windows)] +pub mod windows_process_manager; diff --git a/controller/src/managers/resource.rs b/controller/src/managers/resource.rs new file mode 100644 index 00000000..4a4bed7c --- /dev/null +++ b/controller/src/managers/resource.rs @@ -0,0 +1,677 @@ +use crate::config::get_config; +use bytes::Bytes; +use flate2::{read::GzDecoder, write::GzEncoder, Compression}; +use glob::glob; +use std::{ + fs::{self, File}, + io::{self, Cursor}, + path::{Path, PathBuf}, + time::SystemTime, +}; +use tar::{Archive, Builder, Header}; +#[cfg(unix)] +use users::{get_current_gid, get_current_uid}; +use zip::{result::ZipError, ZipArchive}; + +#[derive(Debug, thiserror::Error)] +pub enum ResourceError { + #[error("Failed to download the file from {0}")] + DownloadFailed(String), + #[error("Failed to write to output path: {0}")] + IoError(#[from] io::Error), + #[error("Failed to extract zip file")] + ZipError(#[from] ZipError), + #[error("Failed to create tarball: {0}")] + TarError(String), + #[error("Failed to delete files in {0}")] + RemoveFailed(String), + #[error("Rollback failed: {0}")] + RollbackFailed(String), +} + +// ref: https://stackoverflow.com/questions/26958489/how-to-copy-a-folder-recursively-in-rust +fn copy_dir_all(src: impl AsRef, dst: impl AsRef) -> io::Result<()> { + if !fs::metadata(&src)?.is_dir() { + if !fs::exists(&dst)? { + fs::copy(&src, &dst)?; + } else if fs::metadata(&dst)?.is_dir() { + let name = src + .as_ref() + .file_name() + .ok_or(io::Error::new(io::ErrorKind::IsADirectory, "Invalid path"))?; + fs::copy(&src, dst.as_ref().join(name))?; + } else { + fs::copy(&src, &dst)?; + } + return Ok(()); + } + + fs::create_dir_all(&dst)?; + for entry in fs::read_dir(src)? { + let entry = entry?; + let ty = entry.file_type()?; + if ty.is_dir() { + copy_dir_all(entry.path(), dst.as_ref().join(entry.file_name()))?; + } else { + fs::copy(entry.path(), dst.as_ref().join(entry.file_name()))?; + } + } + Ok(()) +} + +#[trait_variant::make(Send)] +pub trait ResourceManagerTrait: Send + Sync { + fn backup(&self) -> Result<(), ResourceError>; + + fn rollback(&self, backup_file: &Path) -> Result<(), ResourceError>; + + fn tmp_path(&self) -> &PathBuf; + + fn agent_path(&self) -> &PathBuf; + + async fn download_update_resources( + &self, + binary_url: &str, + output_path: Option + Send>, + ) -> Result<(), ResourceError> { + async move { + let output_path = output_path.map(|x| x.as_ref().to_path_buf()); + let download_path = output_path.as_ref().unwrap_or(self.tmp_path()); + + let response = reqwest::get(binary_url) + .await + .map_err(|_| ResourceError::DownloadFailed(binary_url.to_string()))?; + let content = response + .bytes() + .await + .map_err(|_| ResourceError::DownloadFailed(binary_url.to_string()))?; + + self.extract_zip(content, download_path)?; + Ok(()) + } + } + + fn get_paths_to_backup(&self) -> Result, ResourceError> { + let config = get_config().lock().unwrap(); + Ok(vec![self.agent_path().clone(), config.config_dir.clone()]) + } + + fn collect_downloaded_bundles(&self) -> Vec { + let pattern = self + .tmp_path() + .join("bundles") + .join("*.yml") + .to_string_lossy() + .into_owned(); + + match glob(&pattern) { + Ok(paths) => paths.filter_map(Result::ok).collect(), + Err(_) => Vec::new(), + } + } + + fn get_latest_backup(&self) -> Option { + fs::read_dir(self.tmp_path()) + .ok()? + .filter_map(|entry| entry.ok().map(|e| e.path())) + .filter(|path| { + path.is_file() && path.extension().and_then(|ext| ext.to_str()) == Some("gz") + }) + .max_by_key(|path| { + path.metadata() + .and_then(|meta| meta.modified()) + .unwrap_or(SystemTime::UNIX_EPOCH) + }) + } + + fn extract_zip(&self, archive_data: Bytes, output_path: &Path) -> Result<(), ResourceError> { + let cursor = Cursor::new(archive_data); + let mut archive = ZipArchive::new(cursor)?; + + for i in 0..archive.len() { + let mut file = archive.by_index(i)?; + let file_path = output_path.join(file.mangled_name()); + + if file.is_file() { + if let Some(parent) = file_path.parent() { + fs::create_dir_all(parent)?; + } + let _ = fs::remove_file(&file_path); + let mut output_file = File::create(&file_path)?; + io::copy(&mut file, &mut output_file)?; + #[cfg(unix)] + if let Some(file_name) = file_path.file_name() { + if file_name == "nodex-agent" { + crate::unix_utils::change_to_executable(&file_path)?; + } + } + } else if file.is_dir() { + fs::create_dir_all(&file_path)?; + } + } + + Ok(()) + } + + fn remove_directory(&self, path: &Path) -> Result<(), io::Error> { + if !path.exists() { + return Ok(()); + } + + if path.is_dir() { + fs::remove_dir_all(path).map_err(|e| { + io::Error::new( + io::ErrorKind::PermissionDenied, + format!("Failed to remove directory {:?}: {}", path, e), + ) + })?; + } else { + fs::remove_file(path).map_err(|e| { + io::Error::new( + io::ErrorKind::PermissionDenied, + format!("Failed to remove file {:?}: {}", path, e), + ) + })?; + } + Ok(()) + } + + fn remove(&self) -> Result<(), ResourceError> { + for entry in fs::read_dir(self.tmp_path()) + .map_err(|e| ResourceError::RemoveFailed(format!("Failed to read directory: {}", e)))? + { + let entry = entry.map_err(|e| { + ResourceError::RemoveFailed(format!("Failed to access entry: {}", e)) + })?; + let entry_path = entry.path(); + + self.remove_directory(&entry_path).map_err(|e| { + ResourceError::RemoveFailed(format!( + "Failed to remove path {:?}: {}", + entry_path, e + )) + })?; + } + Ok(()) + } +} + +#[cfg(unix)] +pub struct UnixResourceManager { + tmp_path: PathBuf, + agent_path: PathBuf, +} + +#[cfg(unix)] +impl ResourceManagerTrait for UnixResourceManager { + fn tmp_path(&self) -> &PathBuf { + &self.tmp_path + } + + fn agent_path(&self) -> &PathBuf { + &self.agent_path + } + + fn backup(&self) -> Result<(), ResourceError> { + let paths_to_backup = self.get_paths_to_backup()?; + let metadata = self.generate_metadata(&paths_to_backup)?; + let tar_gz_path = self.create_tar_gz_with_metadata(&metadata)?; + log::info!("Backup created successfully at {:?}", tar_gz_path); + Ok(()) + } + + fn rollback(&self, backup_file: &Path) -> Result<(), ResourceError> { + let temp_dir = self.extract_tar_to_temp(backup_file)?; + // Might be safer to check for the existence of config.json and binary + let metadata = self.read_metadata(&temp_dir)?; + self.move_files_to_original_paths(&temp_dir, &metadata)?; + + log::info!("Rollback completed successfully from {:?}", backup_file); + Ok(()) + } +} + +#[cfg(unix)] +impl UnixResourceManager { + pub fn new(agent_path: impl AsRef) -> Self { + let tmp_path = if PathBuf::from("/home/nodex/").exists() { + PathBuf::from("/home/nodex/tmp") + } else if PathBuf::from("/tmp/nodex").exists() || fs::create_dir_all("/tmp/nodex").is_ok() { + PathBuf::from("/tmp/nodex") + } else { + PathBuf::from("/tmp") + }; + + if !tmp_path.exists() { + fs::create_dir_all(&tmp_path).expect("Failed to create tmp dir"); + } + + Self { + tmp_path, + agent_path: agent_path.as_ref().into(), + } + } + + fn generate_metadata( + &self, + src_paths: &[PathBuf], + ) -> Result, ResourceError> { + src_paths + .iter() + .map(|path| { + let relative_path = path.strip_prefix("/").unwrap_or(path).to_path_buf(); + Ok((path.clone(), relative_path)) + }) + .collect() + } + + fn create_tar_gz_with_metadata( + &self, + metadata: &[(PathBuf, PathBuf)], + ) -> Result { + let timestamp = SystemTime::now() + .duration_since(SystemTime::UNIX_EPOCH) + .map_err(|e| { + ResourceError::TarError(format!("Failed to get current timestamp: {}", e)) + })? + .as_secs(); + + let dest_path = self + .tmp_path + .join(format!("nodex_backup_{}.tar.gz", timestamp)); + + let tar_gz_file = File::create(&dest_path) + .map_err(|e| ResourceError::IoError(io::Error::new(io::ErrorKind::Other, e)))?; + let mut encoder = GzEncoder::new(tar_gz_file, Compression::default()); + { + let mut tar_builder = Builder::new(&mut encoder); + + self.add_files_to_tar(&mut tar_builder, metadata)?; + self.add_metadata_to_tar(&mut tar_builder, metadata, timestamp)?; + tar_builder + .finish() + .map_err(|e| ResourceError::TarError(format!("Failed to finish tarball: {}", e)))?; + } + + encoder.try_finish().map_err(|e| { + ResourceError::TarError(format!("Failed to finalize tar.gz file: {}", e)) + })?; + + Ok(dest_path) + } + + fn add_files_to_tar( + &self, + tar_builder: &mut Builder, + metadata: &[(PathBuf, PathBuf)], + ) -> Result<(), ResourceError> { + for (original_path, relative_path) in metadata { + if original_path.is_dir() { + tar_builder + .append_dir_all(relative_path, original_path) + .map_err(|e| { + ResourceError::TarError(format!( + "Failed to append directory {:?}: {}", + original_path, e + )) + })?; + } else if original_path.is_file() { + tar_builder + .append_path_with_name(original_path, relative_path) + .map_err(|e| { + ResourceError::TarError(format!( + "Failed to append file {:?}: {}", + original_path, e + )) + })?; + } + } + Ok(()) + } + + fn add_metadata_to_tar( + &self, + tar_builder: &mut Builder, + metadata: &[(PathBuf, PathBuf)], + timestamp: u64, + ) -> Result<(), ResourceError> { + let uid = get_current_uid(); + let gid = get_current_gid(); + + let metadata: Vec<_> = metadata + .iter() + .map(|(x, y)| (x.as_path().to_str(), y.as_path().to_str())) + .collect(); + let metadata_json = serde_json::to_string(&metadata) + .map_err(|e| ResourceError::TarError(format!("Failed to serialize metadata: {}", e)))?; + + let mut header = Header::new_gnu(); + header + .set_path("backup_metadata.json") + .map_err(|e| ResourceError::TarError(format!("Failed to set header path: {}", e)))?; + header.set_size(metadata_json.len() as u64); + header.set_mode(0o644); + header.set_mtime(timestamp); + header.set_uid(uid as u64); + header.set_gid(gid as u64); + header.set_cksum(); + + tar_builder + .append_data( + &mut header, + "backup_metadata.json", + metadata_json.as_bytes(), + ) + .map_err(|e| ResourceError::TarError(format!("Failed to add metadata: {}", e)))?; + + Ok(()) + } + + fn extract_tar_to_temp(&self, backup_file: &Path) -> Result { + let file = File::open(backup_file).map_err(|e| { + ResourceError::RollbackFailed(format!( + "Failed to open backup file {:?}: {}", + backup_file, e + )) + })?; + let decompressed = GzDecoder::new(file); + let mut archive = Archive::new(decompressed); + + let temp_dir = PathBuf::from("/tmp/restore_temp"); + std::fs::create_dir_all(&temp_dir).map_err(|e| { + ResourceError::RollbackFailed(format!( + "Failed to create temp directory {:?}: {}", + temp_dir, e + )) + })?; + + archive.unpack(&temp_dir).map_err(|e| { + ResourceError::RollbackFailed(format!( + "Failed to unpack backup archive to temp directory {:?}: {}", + temp_dir, e + )) + })?; + + Ok(temp_dir) + } + + fn read_metadata(&self, temp_dir: &Path) -> Result, ResourceError> { + let metadata_file = temp_dir.join("backup_metadata.json"); + let metadata_contents = std::fs::read_to_string(&metadata_file).map_err(|e| { + ResourceError::RollbackFailed(format!( + "Failed to read metadata file {:?}: {}", + metadata_file, e + )) + })?; + let metadata = serde_json::from_str(&metadata_contents).map_err(|e| { + ResourceError::RollbackFailed(format!( + "Failed to parse metadata file {:?}: {}", + metadata_file, e + )) + })?; + Ok(metadata) + } + + fn move_files_to_original_paths( + &self, + temp_dir: &Path, + metadata: &[(PathBuf, PathBuf)], + ) -> Result<(), ResourceError> { + for (original_path, relative_path) in metadata { + let temp_path = temp_dir.join(relative_path); + if temp_path.exists() { + if original_path.exists() { + self.remove_directory(original_path).map_err(|e| { + ResourceError::RollbackFailed(format!( + "Failed to remove existing path {:?}: {}", + original_path, e + )) + })?; + } + // fs::rename does not work with another partition + copy_dir_all(&temp_path, original_path).map_err(|e| { + ResourceError::RollbackFailed(format!( + "Failed to move file from {:?} to {:?}: {}", + temp_path, original_path, e + )) + })?; + } + } + Ok(()) + } +} + +#[cfg(windows)] +pub struct WindowsResourceManager { + tmp_path: PathBuf, +} + +#[cfg(windows)] +impl ResourceManagerTrait for WindowsResourceManager { + fn tmp_path(&self) -> &PathBuf { + &self.tmp_path + } + + fn agent_path(&self) -> &PathBuf { + unimplemented!() + } + + fn backup(&self) -> Result<(), ResourceError> { + unimplemented!() + } + + fn rollback(&self, backup_file: &Path) -> Result<(), ResourceError> { + unimplemented!() + } +} + +#[cfg(windows)] +impl WindowsResourceManager { + pub fn new() -> Self { + // provisional implementation + let tmp_path = PathBuf::from("C:\\Temp\\nodex-agent"); + Self { tmp_path } + } +} + +#[cfg(windows)] +impl Default for WindowsResourceManager { + fn default() -> Self { + Self::new() + } +} + +#[cfg(all(test, unix))] +mod tests { + use super::*; + use filetime; + use mockito; + use std::fs::{self, File}; + use std::io::Write; + use std::time::{Duration, SystemTime}; + use tempfile::{tempdir, NamedTempFile}; + use zip::{ + write::{ExtendedFileOptions, FileOptions}, + CompressionMethod, ZipWriter, + }; + + impl Default for UnixResourceManager { + fn default() -> Self { + Self::new(std::env::current_exe().unwrap()) + } + } + + fn create_sample_zip() -> NamedTempFile { + let file = NamedTempFile::new().unwrap(); + let mut zip = ZipWriter::new(file.reopen().unwrap()); + + let options: FileOptions = FileOptions::default() + .compression_method(CompressionMethod::Stored) + .unix_permissions(0o644); + + zip.start_file("sample.txt", options).unwrap(); + zip.write_all(b"This is a test file.").unwrap(); + zip.finish().unwrap(); + + file + } + + #[tokio::test] + async fn test_download_update_resources() { + let sample_zip = create_sample_zip(); + let zip_data = fs::read(sample_zip.path()).unwrap(); + + let mut server = mockito::Server::new_async().await; + let path = "/test.zip"; + let _mock = server + .mock("GET", path) + .with_status(200) + .with_header("content-type", "application/zip") + .with_body(zip_data) + .create(); + + let resource_manager = UnixResourceManager::default(); + let temp_dir = tempdir().unwrap(); + let output_path = temp_dir.path().to_path_buf(); + + let url = server.url() + path; + let result = resource_manager + .download_update_resources(&url, Some(&output_path)) + .await; + + assert!( + result.is_ok(), + "Expected download_update_resources to succeed" + ); + + let extracted_file = output_path.join("sample.txt"); + assert!(extracted_file.exists(), "Expected extracted file to exist"); + + let content = fs::read_to_string(extracted_file).unwrap(); + assert_eq!( + content.trim(), + "This is a test file.", + "File content mismatch" + ); + } + + #[test] + fn test_collect_downloaded_bundles() { + let temp_dir = tempdir().unwrap(); + let bundles_dir = temp_dir.path().join("bundles"); + fs::create_dir_all(&bundles_dir).unwrap(); + + let bundle_file = bundles_dir.join("bundle1.yml"); + File::create(&bundle_file).unwrap(); + + let resource_manager = UnixResourceManager { + tmp_path: temp_dir.path().to_path_buf(), + ..Default::default() + }; + + let collected_bundles = resource_manager.collect_downloaded_bundles(); + + assert_eq!( + collected_bundles.len(), + 1, + "Expected exactly one bundle file" + ); + assert_eq!( + collected_bundles[0], bundle_file, + "Unexpected bundle file path" + ); + } + + #[test] + fn test_get_latest_backup() { + let temp_dir = tempdir().unwrap(); + + let old_file = temp_dir.path().join("old_backup.gz"); + let new_file = temp_dir.path().join("new_backup.gz"); + + File::create(&old_file).unwrap(); + File::create(&new_file).unwrap(); + let new_time = SystemTime::now(); + let old_time = new_time - Duration::from_secs(60); + + filetime::set_file_mtime(&old_file, filetime::FileTime::from_system_time(old_time)) + .unwrap(); + filetime::set_file_mtime(&new_file, filetime::FileTime::from_system_time(new_time)) + .unwrap(); + + let resource_manager = UnixResourceManager { + tmp_path: temp_dir.path().to_path_buf(), + ..Default::default() + }; + + let latest_backup = resource_manager.get_latest_backup(); + + assert_eq!( + latest_backup, + Some(new_file), + "Expected new_backup.gz to be the latest" + ); + } + + #[test] + fn test_backup() { + let temp_dir = tempdir().unwrap(); + let resource_manager = UnixResourceManager { + tmp_path: temp_dir.path().to_path_buf(), + ..Default::default() + }; + + let result = resource_manager.backup(); + assert!(result.is_ok(), "Expected backup to succeed"); + + let backups: Vec<_> = fs::read_dir(temp_dir.path()) + .unwrap() + .filter_map(|entry| entry.ok()) + .filter(|entry| entry.path().extension().and_then(|e| e.to_str()) == Some("gz")) + .collect(); + + assert_eq!(backups.len(), 1, "Expected exactly one backup file"); + } + + #[test] + fn test_rollback() { + let temp_dir = tempdir().unwrap(); + let resource_manager = UnixResourceManager { + tmp_path: temp_dir.path().to_path_buf(), + ..Default::default() + }; + + let _ = resource_manager.backup(); + let latest_backup = resource_manager.get_latest_backup(); + + assert!(latest_backup.is_some(), "Expected a backup to exist"); + if let Some(backup) = latest_backup { + let result: Result<(), ResourceError> = resource_manager.rollback(&backup); + println!("Result: {:?}", result); + assert!(result.is_ok(), "Expected rollback to succeed"); + } + } + + #[test] + fn test_remove() { + let temp_dir = tempdir().unwrap(); + + let resource_manager = UnixResourceManager { + tmp_path: temp_dir.path().to_path_buf(), + ..Default::default() + }; + + let dummy_file = temp_dir.path().join("dummy_file.txt"); + File::create(&dummy_file).unwrap(); + + assert!( + dummy_file.exists(), + "Dummy file should exist before removal" + ); + + let result = resource_manager.remove(); + assert!(result.is_ok(), "Expected remove to succeed"); + + assert!(!dummy_file.exists(), "Dummy file should be removed"); + } +} diff --git a/controller/src/managers/runtime.rs b/controller/src/managers/runtime.rs new file mode 100644 index 00000000..efba380c --- /dev/null +++ b/controller/src/managers/runtime.rs @@ -0,0 +1,562 @@ +use crate::validator::process::{is_manage_by_systemd, is_manage_socket_activation}; +use chrono::{DateTime, FixedOffset, Utc}; +use semver::Version; +use serde::{Deserialize, Serialize}; +use std::path::{Path, PathBuf}; +use tokio::sync::watch; + +#[derive(Debug, Serialize, Deserialize, PartialEq, Eq, Clone)] +pub struct RuntimeInfo { + pub state: State, + pub process_infos: [Option; 4], + pub exec_path: PathBuf, +} + +#[derive(Debug, Serialize, Deserialize, PartialEq, Eq, Clone, Copy)] +pub enum State { + Idle, + Update, + Rollback, +} + +#[derive(Debug, Serialize, Deserialize, PartialEq, Eq, Clone)] +pub struct ProcessInfo { + pub process_id: u32, + pub executed_at: DateTime, + pub version: Version, + pub feat_type: FeatType, +} + +#[derive(Debug, Serialize, Deserialize, PartialEq, Eq, Clone)] +pub enum FeatType { + Agent, + Controller, +} + +pub enum NodexSignal { + Terminate, + SendFd, +} + +pub trait ProcessManager: Clone { + fn is_running(&self, process_id: u32) -> bool; + fn spawn_process(&self, cmd: impl AsRef, args: &[&str]) -> Result; + fn kill_process(&self, process_id: u32, signal: NodexSignal) -> Result<(), std::io::Error>; +} + +#[derive(Debug, thiserror::Error)] +pub enum RuntimeError { + #[error("Failed to open file: {0}")] + FileOpen(#[source] std::io::Error), + #[error("Failed to read file: {0}")] + FileRead(#[source] std::io::Error), + #[error("Failed to write data to file: {0}")] + FileWrite(#[source] std::io::Error), + #[error("Failed to remove file: {0}")] + FileRemove(#[source] std::io::Error), + #[error("Failed to acquire exclusive file lock: {0}")] + FileLock(#[source] std::io::Error), + #[error("Failed to unlock file: {0}")] + FileUnlock(#[source] std::io::Error), + #[error("Failed to serialize runtime info to JSON: {0}")] + JsonSerialize(#[source] serde_json::Error), + #[error("Failed to deserialize runtime info from JSON: {0}")] + JsonDeserialize(#[source] serde_json::Error), + #[error("Failed to kill process")] + Kill(std::io::Error), + #[error("Failed to kill processes")] + Kills(Vec), + #[error("Failed to create command: {0}")] + Command(#[source] std::io::Error), + #[error("Failed to fork: {0}")] + Fork(#[source] std::io::Error), + #[error("failed to know path of self exe: {0}")] + FailedCurrentExe(#[source] std::io::Error), + #[error("Controller already running")] + AlreadyExistController, + #[error(transparent)] + SemVer(#[from] semver::Error), + #[cfg(unix)] + #[error("Failed to bind UDS: {0}")] + BindUdsError(#[source] std::io::Error), + #[cfg(unix)] + #[error("Failed to watch UDS: {0}")] + WatchUdsError(#[source] notify::Error), + #[cfg(unix)] + #[error("Failed to get fd from systemd: {0}")] + GetFd(#[from] crate::unix_utils::GetFdError), + #[cfg(unix)] + #[error("Request failed: {0}")] + Request(#[from] crate::unix_utils::GetRequestError), + #[cfg(unix)] + #[error("Failed to get meta uds path")] + PathConvention, +} + +pub trait RuntimeInfoStorage: std::fmt::Debug { + fn read(&mut self) -> Result; + fn apply_with_lock(&mut self, operation: F) -> Result<(), RuntimeError> + where + F: FnOnce(&mut RuntimeInfo) -> Result<(), RuntimeError>; +} + +#[derive(Debug, Deserialize)] +struct VersionResponse { + pub version: String, +} + +pub trait RuntimeManagerWithoutAsync { + fn launch_agent(&mut self, is_first: bool) -> Result; + + fn launch_controller( + &mut self, + new_controller_path: impl AsRef, + ) -> Result<(), RuntimeError>; + + fn get_runtime_info(&mut self) -> Result; + + fn update_state_without_send(&mut self, state: State) -> Result<(), RuntimeError>; + + fn update_state(&mut self, state: State) -> Result<(), RuntimeError>; + + fn kill_process(&mut self, process_info: &ProcessInfo) -> Result<(), RuntimeError>; + + fn kill_other_agents(&mut self, target: u32) -> Result<(), RuntimeError>; +} + +#[trait_variant::make(Send)] +pub trait RuntimeManager: RuntimeManagerWithoutAsync { + async fn get_version(&self) -> Result; +} + +#[derive(Debug, Clone)] +pub struct RuntimeManagerImpl +where + H: RuntimeInfoStorage, + P: ProcessManager, +{ + self_pid: u32, + file_handler: H, + process_manager: P, + uds_path: PathBuf, + meta_uds_path: PathBuf, + state_sender: watch::Sender, +} + +impl RuntimeManager for RuntimeManagerImpl +where + H: RuntimeInfoStorage + Sync + Send, + P: ProcessManager + Sync + Send, +{ + async fn get_version(&self) -> Result { + #[cfg(unix)] + let version_response: VersionResponse = + crate::unix_utils::get_request(&self.uds_path, "/internal/version/get").await?; + #[cfg(windows)] + let version_response = VersionResponse { + version: "9.9.9".to_string(), + }; + Ok(Version::parse(&version_response.version)?) + } +} + +impl RuntimeManagerWithoutAsync for RuntimeManagerImpl +where + H: RuntimeInfoStorage, + P: ProcessManager, +{ + fn launch_agent(&mut self, is_first: bool) -> Result { + #[cfg(unix)] + if is_first { + if !is_manage_socket_activation() && self.uds_path.exists() { + log::warn!("UDS file already exists, removing: {:?}", self.uds_path); + let _ = std::fs::remove_file(&self.uds_path); + } + if self.meta_uds_path.exists() { + log::warn!( + "UDS file already exists, removing: {:?}", + self.meta_uds_path + ); + let _ = std::fs::remove_file(&self.meta_uds_path); + } + } + let current_exe = &self.get_runtime_info()?.exec_path; + let child = self + .process_manager + .spawn_process(current_exe, &["controlled"]) + .map_err(RuntimeError::Fork)?; + + #[cfg(unix)] + if is_first { + let listener = if is_manage_by_systemd() && is_manage_socket_activation() { + Some(crate::unix_utils::get_fd_from_systemd()?) + } else { + None + }; + let () = crate::unix_utils::wait_until_file_created(&self.meta_uds_path) + .map_err(RuntimeError::WatchUdsError)?; + let stream = loop { + match std::os::unix::net::UnixStream::connect(&self.meta_uds_path) { + Ok(stream) => break stream, + Err(err) if err.kind() == std::io::ErrorKind::ConnectionRefused => { + // Wait for bind + std::thread::sleep(std::time::Duration::from_millis(5)); + continue; + } + Err(err) => return Err(RuntimeError::BindUdsError(err)), + } + }; + let stream = std::os::unix::io::AsRawFd::as_raw_fd(&stream); + crate::unix_utils::send_fd(stream, listener) + .map_err(|e| RuntimeError::BindUdsError(e.into()))?; + } + let process_info = ProcessInfo::new(child, FeatType::Agent); + self.add_process_info(process_info.clone())?; + Ok(process_info) + } + + fn get_runtime_info(&mut self) -> Result { + self.file_handler.read() + } + + fn kill_process(&mut self, process_info: &ProcessInfo) -> Result<(), RuntimeError> { + let signal = if process_info.feat_type == FeatType::Agent { + NodexSignal::SendFd + } else { + NodexSignal::Terminate + }; + self.process_manager + .kill_process(process_info.process_id, signal) + .map_err(RuntimeError::Kill)?; + self.remove_process_info(process_info.process_id)?; + Ok(()) + } + + fn update_state_without_send(&mut self, state: State) -> Result<(), RuntimeError> { + self.file_handler.apply_with_lock(|runtime_info| { + runtime_info.state = state; + Ok(()) + }) + } + + fn update_state(&mut self, state: State) -> Result<(), RuntimeError> { + self.update_state_without_send(state)?; + let _ = self.state_sender.send(state); + Ok(()) + } + + fn kill_other_agents(&mut self, target: u32) -> Result<(), RuntimeError> { + self.kill_others(target, Some(FeatType::Agent)) + } + + fn launch_controller( + &mut self, + new_controller_path: impl AsRef, + ) -> Result<(), RuntimeError> { + self.kill_others(self.self_pid, None)?; + if is_manage_by_systemd() { + return Ok(()); + } + let child = self + .process_manager + .spawn_process(new_controller_path, &["controller"]) + .map_err(RuntimeError::Fork)?; + log::info!("Parent process launched child with PID: {}", child); + Ok(()) + } +} + +impl RuntimeManagerImpl +where + H: RuntimeInfoStorage, + P: ProcessManager, +{ + pub fn new_by_controller( + mut file_handler: H, + process_manager: P, + uds_path: impl AsRef, + ) -> Result<(Self, watch::Receiver), RuntimeError> { + let (state_sender, state_receiver) = watch::channel(file_handler.read()?.state); + #[cfg(unix)] + let meta_uds_path = crate::unix_utils::convention_of_meta_uds_path(&uds_path) + .map_err(|_| RuntimeError::PathConvention)?; + #[cfg(windows)] + let meta_uds_path = PathBuf::from(""); + let self_pid = std::process::id(); + let mut runtime_manager = RuntimeManagerImpl { + self_pid, + file_handler, + state_sender, + process_manager, + uds_path: uds_path.as_ref().into(), + meta_uds_path, + }; + // We assume that caller is controller. + runtime_manager.cleanup_process_info()?; + let runtime_info = runtime_manager.file_handler.read()?; + let controller_processes: Vec<_> = runtime_info + .filter_by_feat(FeatType::Controller) + .filter(|process_info| process_info.process_id != self_pid) + .collect(); + if !controller_processes.is_empty() { + return Err(RuntimeError::AlreadyExistController); + } + let self_info = ProcessInfo::new(self_pid, FeatType::Controller); + runtime_manager.add_process_info(self_info)?; + Ok((runtime_manager, state_receiver)) + } + + pub fn new_by_agent(file_handler: H, process_manager: P) -> Self { + // We assume that caller is agent. + // dummy channel + let (state_sender, _) = watch::channel(State::Idle); + RuntimeManagerImpl { + self_pid: std::process::id(), + file_handler, + state_sender, + process_manager, + uds_path: "".into(), + meta_uds_path: "".into(), + } + } + + fn add_process_info(&mut self, process_info: ProcessInfo) -> Result<(), RuntimeError> { + self.file_handler + .apply_with_lock(|runtime_info| runtime_info.add_process_info(process_info)) + } + + fn remove_process_info(&mut self, process_id: u32) -> Result<(), RuntimeError> { + self.file_handler + .apply_with_lock(|runtime_info| runtime_info.remove_process_info(process_id)) + } + + // Kill all related processes + pub fn cleanup_all(&mut self) -> Result<(), RuntimeError> { + #[cfg(unix)] + { + crate::unix_utils::remove_file_if_exists(&self.uds_path); + crate::unix_utils::remove_file_if_exists(&self.meta_uds_path); + } + let process_manager = &self.process_manager; + self.file_handler.apply_with_lock(move |runtime_info| { + let mut errs = vec![]; + for info in runtime_info.process_infos.iter_mut() { + if let Some(info) = info { + if let Err(err) = + process_manager.kill_process(info.process_id, NodexSignal::Terminate) + { + errs.push(RuntimeError::Kill(err)); + } + } + *info = None; + } + runtime_info.state = State::Idle; + if errs.is_empty() { + Ok(()) + } else { + Err(RuntimeError::Kills(errs)) + } + }) + } + + pub fn cleanup(&mut self) -> Result<(), RuntimeError> { + self.remove_process_info(self.self_pid) + } + + fn cleanup_process_info(&mut self) -> Result<(), RuntimeError> { + let process_manager = &self.process_manager; + self.file_handler.apply_with_lock(|runtime_info| { + for process_info in runtime_info.process_infos.iter_mut() { + if let Some(ref p) = process_info { + if !process_manager.is_running(p.process_id) { + *process_info = None; + } + } + } + Ok(()) + }) + } + + fn kill_others( + &mut self, + target: u32, + feat_type: Option, + ) -> Result<(), RuntimeError> { + let (_oks, errs): (Vec<_>, Vec<_>) = self + .file_handler + .read()? + .process_infos + .into_iter() + .flatten() + .filter(|process_info| process_info.process_id != target) + .filter(|p| { + feat_type + .as_ref() + .map(|f| p.feat_type == *f) + .unwrap_or(true) + }) + .map(move |process_info| self.kill_process(&process_info)) + .partition(Result::is_ok); + if errs.is_empty() { + Ok(()) + } else { + Err(RuntimeError::Kills( + errs.into_iter().map(Result::unwrap_err).collect(), + )) + } + } +} + +impl ProcessInfo { + pub fn new(process_id: u32, feat_type: FeatType) -> Self { + let now = Utc::now().with_timezone(&FixedOffset::east_opt(9 * 3600).unwrap()); + let version = Version::parse(env!("CARGO_PKG_VERSION")).unwrap(); + ProcessInfo { + process_id, + executed_at: now, + version, + feat_type, + } + } +} + +impl RuntimeInfo { + pub fn add_process_info(&mut self, process_info: ProcessInfo) -> Result<(), RuntimeError> { + for info in self.process_infos.iter_mut() { + if info.is_none() { + *info = Some(process_info); + return Ok(()); + } + } + Err(RuntimeError::FileWrite(std::io::Error::new( + std::io::ErrorKind::StorageFull, + "Failed to add process_info", + ))) + } + pub fn remove_process_info(&mut self, process_id: u32) -> Result<(), RuntimeError> { + let pid = process_id; + let mut i = None; + for (j, info) in self.process_infos.iter_mut().enumerate() { + match info.as_ref() { + Some(ProcessInfo { process_id, .. }) if pid == *process_id => { + *info = None; + i = Some(j); + break; + } + _ => continue, + } + } + if let Some(i) = i { + self.process_infos[i..].rotate_left(1); + Ok(()) + } else { + Err(RuntimeError::FileWrite(std::io::Error::new( + std::io::ErrorKind::StorageFull, + "Failed to remove process_info", + ))) + } + } + + pub fn find_process_info(&self, process_id: u32) -> Option<&ProcessInfo> { + self.process_infos + .iter() + .flatten() + .find(|p| p.process_id == process_id) + } + + pub fn filter_by_feat(&self, feat_type: FeatType) -> impl Iterator { + self.process_infos + .iter() + .flatten() + .filter(move |process_info| process_info.feat_type == feat_type) + } + + pub fn is_agent_running(&self) -> bool { + let is_not_empty = self + .filter_by_feat(FeatType::Agent) + .peekable() + .peek() + .is_some(); + is_not_empty + } +} + +#[cfg(all(test, unix))] +mod tests { + use super::*; + + #[test] + fn test_add_process_info() { + let mut runtime_info = RuntimeInfo { + state: State::Idle, + process_infos: [None, None, None, None], + exec_path: std::env::current_exe().unwrap(), + }; + + let process_info = ProcessInfo::new(12345, FeatType::Agent); + runtime_info.add_process_info(process_info.clone()).unwrap(); + + assert_eq!( + runtime_info.process_infos, + [Some(process_info), None, None, None] + ); + } + + #[test] + fn test_remove_process_info() { + let mut runtime_info = RuntimeInfo { + state: State::Idle, + process_infos: [None, None, None, None], + exec_path: std::env::current_exe().unwrap(), + }; + + let process_info1 = ProcessInfo::new(12345, FeatType::Agent); + let process_info2 = ProcessInfo::new(67890, FeatType::Controller); + + runtime_info + .add_process_info(process_info1.clone()) + .unwrap(); + runtime_info + .add_process_info(process_info2.clone()) + .unwrap(); + + runtime_info.remove_process_info(12345).unwrap(); + + assert_eq!( + runtime_info.process_infos, + [Some(process_info2), None, None, None] + ); + } + + #[test] + fn test_filter_process_infos() { + let mut runtime_info = RuntimeInfo { + state: State::Idle, + process_infos: [None, None, None, None], + exec_path: std::env::current_exe().unwrap(), + }; + + let process_info1 = ProcessInfo::new(12345, FeatType::Agent); + let process_info2 = ProcessInfo::new(67890, FeatType::Controller); + + runtime_info + .add_process_info(process_info1.clone()) + .unwrap(); + runtime_info + .add_process_info(process_info2.clone()) + .unwrap(); + + let agents: Vec<_> = runtime_info.filter_by_feat(FeatType::Agent).collect(); + assert_eq!(agents.len(), 1); + assert_eq!(agents[0].process_id, 12345); + + let controllers: Vec<_> = runtime_info.filter_by_feat(FeatType::Controller).collect(); + assert_eq!(controllers.len(), 1); + assert_eq!(controllers[0].process_id, 67890); + } + + #[test] + fn test_version_format() { + assert!(Version::parse(env!("CARGO_PKG_VERSION")).is_ok()); + } +} diff --git a/controller/src/managers/unix_process_manager.rs b/controller/src/managers/unix_process_manager.rs new file mode 100644 index 00000000..5d3ed959 --- /dev/null +++ b/controller/src/managers/unix_process_manager.rs @@ -0,0 +1,53 @@ +use super::runtime::{NodexSignal, ProcessManager}; +use nix::{ + sys::signal::{self, Signal}, + unistd::{execvp, fork, setsid, ForkResult, Pid}, +}; +use std::ffi::CString; +use std::path::Path; + +#[derive(Clone)] +pub struct UnixProcessManager; + +#[inline] +fn nule_to_ioe(e: std::ffi::NulError) -> std::io::Error { + std::io::Error::new(std::io::ErrorKind::InvalidInput, e) +} + +impl ProcessManager for UnixProcessManager { + fn is_running(&self, process_id: u32) -> bool { + let pid = Pid::from_raw(process_id as i32); + match signal::kill(pid, None) { + Ok(()) => true, + Err(_) => false, + } + } + fn spawn_process(&self, cmd: impl AsRef, args: &[&str]) -> Result { + let cmd = CString::new(cmd.as_ref().to_string_lossy().as_ref()).map_err(nule_to_ioe)?; + let args: Result, _> = args + .iter() + .map(|arg| CString::new(*arg).map_err(nule_to_ioe)) + .collect(); + let mut args = args?; + args.splice(0..0, vec![cmd.clone()]); + + match unsafe { fork() } { + Ok(ForkResult::Parent { child }) => Ok(child.as_raw() as _), + Ok(ForkResult::Child) => { + setsid().map_err(|e| std::io::Error::new(std::io::ErrorKind::Other, e))?; + execvp(&cmd, &args) + .map_err(|e| std::io::Error::new(std::io::ErrorKind::Other, e))?; + unreachable!(); + } + Err(e) => Err(std::io::Error::new(std::io::ErrorKind::Other, e)), + } + } + fn kill_process(&self, process_id: u32, signal: NodexSignal) -> Result<(), std::io::Error> { + let signal = match signal { + NodexSignal::SendFd => Signal::SIGUSR1, + NodexSignal::Terminate => Signal::SIGTERM, + }; + signal::kill(Pid::from_raw(process_id as i32), signal) + .map_err(|e| std::io::Error::from_raw_os_error(e as _)) + } +} diff --git a/controller/src/managers/windows_process_manager.rs b/controller/src/managers/windows_process_manager.rs new file mode 100644 index 00000000..ead84d70 --- /dev/null +++ b/controller/src/managers/windows_process_manager.rs @@ -0,0 +1,17 @@ +use super::runtime::{NodexSignal, ProcessManager}; +use std::path::Path; + +#[derive(Clone)] +pub struct WindowsProcessManager; + +impl ProcessManager for WindowsProcessManager { + fn is_running(&self, process_id: u32) -> bool { + unimplemented!() + } + fn spawn_process(&self, cmd: impl AsRef, args: &[&str]) -> Result { + unimplemented!() + } + fn kill_process(&self, process_id: u32, signal: NodexSignal) -> Result<(), std::io::Error> { + unimplemented!() + } +} diff --git a/controller/src/state/handler.rs b/controller/src/state/handler.rs new file mode 100644 index 00000000..baa601d8 --- /dev/null +++ b/controller/src/state/handler.rs @@ -0,0 +1,47 @@ +use crate::managers::runtime::{RuntimeError, RuntimeManager, State}; +use crate::state::{idle, rollback, update}; + +#[cfg(unix)] +use crate::managers::resource::UnixResourceManager; + +#[cfg(windows)] +use crate::managers::resource::WindowsResourceManager; + +#[derive(Debug, thiserror::Error)] +pub enum StateHandlerError { + #[error("update failed: {0}")] + Update(#[from] update::UpdateError), + #[error("rollback failed: {0}")] + Rollback(#[from] rollback::RollbackError), + #[error("default failed: {0}")] + Idle(#[from] idle::IdleError), + #[error("failed to get runtime info: {0}")] + RuntimeInfo(#[from] RuntimeError), +} + +pub async fn handle_state( + state: State, + runtime_manager: &mut R, +) -> Result<(), StateHandlerError> { + let agent_path = runtime_manager.get_runtime_info()?.exec_path; + #[cfg(unix)] + let resource_manager = UnixResourceManager::new(agent_path); + #[cfg(windows)] + let resource_manager = WindowsResourceManager::new(); + + match state { + State::Update => { + update::execute(&resource_manager, runtime_manager).await?; + // ERASE: test for rollback + // runtime_manager.update_state(crate::managers::runtime::State::Rollback)?; + } + State::Rollback => { + rollback::execute(&resource_manager, runtime_manager).await?; + } + State::Idle => { + idle::execute(runtime_manager).await?; + } + } + + Ok(()) +} diff --git a/controller/src/state/idle.rs b/controller/src/state/idle.rs new file mode 100644 index 00000000..73065e24 --- /dev/null +++ b/controller/src/state/idle.rs @@ -0,0 +1,65 @@ +use crate::managers::runtime::{RuntimeError, RuntimeManager}; + +#[derive(Debug, thiserror::Error)] +pub enum IdleError { + #[error("failed to get runtime info: {0}")] + RuntimeError(#[from] RuntimeError), +} + +pub async fn execute(runtime_manager: &mut T) -> Result<(), IdleError> { + if !runtime_manager.get_runtime_info()?.is_agent_running() { + let _process_info = runtime_manager.launch_agent(true)?; + } else { + log::error!("Agent already running"); + } + log::info!("No state change required."); + Ok(()) +} + +#[cfg(all(test, unix))] +mod tests { + use super::super::tests::MockRuntimeManager; + use super::*; + use crate::managers::runtime::{FeatType, ProcessInfo, RuntimeInfo, State}; + + #[tokio::test] + async fn test_execute_with_no_running_agents() { + let runtime_info = RuntimeInfo { + state: State::Idle, + process_infos: [None, None, None, None], + exec_path: std::env::current_exe().unwrap(), + }; + let mut runtime_manager = MockRuntimeManager::new(runtime_info); + + let result = execute(&mut runtime_manager).await; + assert!(result.is_ok(), "Expected Ok result, got {:?}", result); + + let process_infos: Vec<_> = runtime_manager + .runtime_info + .process_infos + .iter() + .flatten() + .collect(); + assert_eq!(process_infos.len(), 1); + assert_eq!(process_infos[0].feat_type, FeatType::Agent); + assert_eq!(process_infos[0].process_id, 1); + } + + #[tokio::test] + async fn test_execute_with_one_running_agent() { + let runtime_info = RuntimeInfo { + state: State::Idle, + process_infos: [ + Some(ProcessInfo::new(12345, FeatType::Agent)), + None, + None, + None, + ], + exec_path: std::env::current_exe().unwrap(), + }; + let mut runtime_manager = MockRuntimeManager::new(runtime_info); + + let result = execute(&mut runtime_manager).await; + assert!(result.is_ok(), "Expected Ok result, got {:?}", result); + } +} diff --git a/controller/src/state/mod.rs b/controller/src/state/mod.rs new file mode 100644 index 00000000..4043d306 --- /dev/null +++ b/controller/src/state/mod.rs @@ -0,0 +1,157 @@ +pub mod handler; +mod idle; +pub mod rollback; +pub mod update; + +#[cfg(all(test, unix))] +mod tests { + use crate::managers::{ + resource::{ResourceError, ResourceManagerTrait}, + runtime::{ + FeatType, ProcessInfo, RuntimeError, RuntimeInfo, RuntimeManager, + RuntimeManagerWithoutAsync, State, + }, + }; + use chrono::{FixedOffset, Utc}; + use semver::Version; + use std::path::{Path, PathBuf}; + use std::sync::Mutex as StdMutex; + + pub struct MockRuntimeManager { + pub response_version: Version, + pub runtime_info: RuntimeInfo, + } + + impl MockRuntimeManager { + pub fn new(runtime_info: RuntimeInfo) -> Self { + let current_version = Version::parse(env!("CARGO_PKG_VERSION")).unwrap(); + Self { + response_version: current_version, + runtime_info, + } + } + } + + impl RuntimeManagerWithoutAsync for MockRuntimeManager { + fn launch_agent(&mut self, _is_first: bool) -> Result { + let now = Utc::now().with_timezone(&FixedOffset::east_opt(9 * 3600).unwrap()); + let process_info = ProcessInfo { + process_id: 1, + feat_type: FeatType::Agent, + version: self.response_version.clone(), + executed_at: now, + }; + let _ = self.runtime_info.add_process_info(process_info.clone()); + Ok(process_info) + } + + fn launch_controller( + &mut self, + _new_controller_path: impl AsRef, + ) -> Result<(), RuntimeError> { + Ok(()) + } + + fn get_runtime_info(&mut self) -> Result { + Ok(self.runtime_info.clone()) + } + + fn update_state_without_send(&mut self, state: State) -> Result<(), RuntimeError> { + self.runtime_info.state = state; + Ok(()) + } + + fn update_state(&mut self, state: State) -> Result<(), RuntimeError> { + self.runtime_info.state = state; + Ok(()) + } + + fn kill_process(&mut self, _process_info: &ProcessInfo) -> Result<(), RuntimeError> { + unimplemented!(); + } + + fn kill_other_agents(&mut self, _target: u32) -> Result<(), RuntimeError> { + for p in self + .runtime_info + .process_infos + .iter_mut() + .filter(|p| p.as_ref().map(|q| &q.version) != Some(&self.response_version)) + { + *p = None; + } + Ok(()) + } + } + + impl RuntimeManager for MockRuntimeManager { + async fn get_version(&self) -> Result { + Ok(self.response_version.clone()) + } + } + + pub struct MockResourceManager { + bundles: Vec, + pub rollback_called: StdMutex, + pub remove_called: StdMutex, + } + + impl MockResourceManager { + pub fn new(bundles: Vec) -> Self { + Self { + bundles, + remove_called: StdMutex::new(false), + rollback_called: StdMutex::new(false), + } + } + } + + impl ResourceManagerTrait for MockResourceManager { + fn backup(&self) -> Result<(), ResourceError> { + unimplemented!() + } + + fn rollback(&self, _backup_file: &std::path::Path) -> Result<(), ResourceError> { + let mut called = self.rollback_called.lock().unwrap(); + *called = true; + Ok(()) + } + + fn agent_path(&self) -> &PathBuf { + unimplemented!() + } + + fn tmp_path(&self) -> &PathBuf { + unimplemented!() + } + + fn get_paths_to_backup(&self) -> Result, ResourceError> { + unimplemented!() + } + + fn collect_downloaded_bundles(&self) -> Vec { + self.bundles.clone() + } + + fn get_latest_backup(&self) -> Option { + self.bundles.first().cloned() + } + + fn extract_zip( + &self, + _archive_data: bytes::Bytes, + _output_path: &std::path::Path, + ) -> Result<(), ResourceError> { + unimplemented!() + } + + fn remove_directory(&self, _path: &std::path::Path) -> Result<(), std::io::Error> { + Ok(()) + } + + fn remove(&self) -> Result<(), ResourceError> { + let mut called = self.remove_called.lock().unwrap(); + *called = true; + Ok(()) + } + } +} diff --git a/controller/src/state/rollback/mod.rs b/controller/src/state/rollback/mod.rs new file mode 100644 index 00000000..956f7abe --- /dev/null +++ b/controller/src/state/rollback/mod.rs @@ -0,0 +1,119 @@ +use crate::managers::{ + resource::{ResourceError, ResourceManagerTrait}, + runtime::{RuntimeError, RuntimeManager}, +}; + +#[cfg(unix)] +pub use nix::{ + sys::signal::{self, Signal}, + unistd::Pid, +}; + +#[derive(Debug, thiserror::Error)] +pub enum RollbackError { + #[error("Failed to find backup")] + BackupNotFound, + #[error("resource operation failed: {0}")] + ResourceError(#[from] ResourceError), + #[error("failed to get runtime info: {0}")] + RuntimeError(#[from] RuntimeError), + #[error("failed to kill process: {0}")] + FailedKillOwnProcess(String), + #[error("Failed to get current executable path: {0}")] + CurrentExecutablePathError(#[source] std::io::Error), +} + +pub async fn execute<'a, R, T>( + resource_manager: &'a R, + runtime_manager: &'a mut T, +) -> Result<(), RollbackError> +where + R: ResourceManagerTrait, + T: RuntimeManager, +{ + log::info!("Starting rollback"); + + let latest_backup = resource_manager.get_latest_backup(); + match latest_backup { + Some(backup_file) => { + let agent_path = runtime_manager.get_runtime_info()?.exec_path; + log::info!("Found backup: {}", backup_file.display()); + resource_manager.rollback(&backup_file)?; + if let Err(err) = resource_manager.remove() { + log::error!("Failed to remove files {}", err); + } + runtime_manager.update_state_without_send(crate::managers::runtime::State::Idle)?; + runtime_manager.launch_controller(agent_path)?; + log::info!("Rollback completed"); + + #[cfg(not(test))] // failed test by kill own process + { + log::info!("Restarting controller by SIGTERM"); + let runtime_info = runtime_manager.get_runtime_info()?; + let self_info = runtime_info.find_process_info(std::process::id()).ok_or( + RollbackError::FailedKillOwnProcess("Failed to find self info".into()), + )?; + runtime_manager.kill_process(self_info)?; + } + Ok(()) + } + None => Err(RollbackError::BackupNotFound), + } +} + +#[cfg(all(test, unix))] +mod tests { + use super::super::tests::{MockResourceManager, MockRuntimeManager}; + use super::*; + use crate::managers::runtime::{RuntimeInfo, RuntimeManagerWithoutAsync, State}; + use tempfile::tempdir; + + #[tokio::test] + async fn test_execute_with_backup() { + let temp_dir = tempdir().expect("Failed to create temporary directory"); + let backup_file = temp_dir.path().join("backup.tar.gz"); + let resource = MockResourceManager::new(vec![backup_file]); + let runtime_info = RuntimeInfo { + state: State::Rollback, + process_infos: [None, None, None, None], + exec_path: "".into(), + }; + let mut runtime = MockRuntimeManager::new(runtime_info); + + let result = execute(&resource, &mut runtime).await; + assert!(result.is_ok()); + + let state = runtime.get_runtime_info().unwrap().state; + assert_eq!( + state, + State::Idle, + "update_state should be called with State::Init" + ); + + { + let rollback_called = *resource.rollback_called.lock().unwrap(); + assert!(rollback_called, "rollback should be called"); + let remove_called = *resource.remove_called.lock().unwrap(); + assert!(remove_called, "remove should be called"); + } + } + + #[tokio::test] + async fn test_execute_without_backup() { + let resource = MockResourceManager::new(vec![]); + let runtime_info = RuntimeInfo { + state: State::Rollback, + process_infos: [None, None, None, None], + exec_path: "".into(), + }; + let mut runtime = MockRuntimeManager::new(runtime_info); + + let result = execute(&resource, &mut runtime).await; + assert!(result.is_err()); + + match result { + Err(RollbackError::BackupNotFound) => {} + _ => panic!("Should return BackupNotFound error"), + } + } +} diff --git a/controller/src/state/update/mod.rs b/controller/src/state/update/mod.rs new file mode 100644 index 00000000..1853b6f8 --- /dev/null +++ b/controller/src/state/update/mod.rs @@ -0,0 +1,384 @@ +pub mod tasks; +use crate::managers::{ + resource::{ResourceError, ResourceManagerTrait}, + runtime::{FeatType, RuntimeError, RuntimeManager, State}, +}; +use crate::state::update::tasks::{UpdateAction, UpdateActionError}; +use semver::Version; +use serde_yaml::Error as SerdeYamlError; +use std::fs; +use std::path::PathBuf; +use std::time::Duration; +use tokio::time::{self, Instant}; + +#[derive(Debug, thiserror::Error)] +pub enum UpdateError { + #[error("Failed to find bundle")] + BundleNotFound, + #[error("Invalid version format")] + InvalidVersionFormat, + #[error("update action error: {0}")] + UpdateActionFailed(#[from] UpdateActionError), + #[error("Failed to read YAML file: {0}")] + YamlReadFailed(#[from] std::io::Error), + #[error("Failed to parse YAML: {0}")] + YamlParseFailed(#[source] SerdeYamlError), + #[error("Failed to update state: {0}")] + UpdateStateFailed(#[source] RuntimeError), + #[error("Failed to Agent version check: {0}")] + AgentVersionCheckFailed(String), + #[error("runtime operation failed: {0}")] + RuntimeError(#[from] RuntimeError), + #[error("resource operation failed: {0}")] + ResourceError(#[from] ResourceError), + #[error("Agent not running")] + AgentNotRunning, +} + +impl UpdateError { + pub fn required_restore_state(&self) -> bool { + !matches!(self, UpdateError::AgentNotRunning) + } + + pub fn requires_rollback(&self) -> bool { + !matches!( + self, + UpdateError::ResourceError(ResourceError::RemoveFailed(_)) + ) + } +} +fn get_target_state(update_error: &UpdateError) -> Option { + if update_error.requires_rollback() { + Some(State::Rollback) + } else if update_error.required_restore_state() { + Some(State::Idle) + } else { + None + } +} + +fn parse_bundles(bundles: &[PathBuf]) -> Result, UpdateError> { + bundles + .iter() + .map(|bundle| { + let yaml_content = fs::read_to_string(bundle)?; + let update_action: UpdateAction = + serde_yaml::from_str(&yaml_content).map_err(UpdateError::YamlParseFailed)?; + Ok(update_action) + }) + .collect() +} + +fn extract_pending_update_actions<'b>( + update_actions: &'b [UpdateAction], + current_controller_version: &Version, + current_agent_version: &Version, +) -> Result, UpdateError> { + let pending_actions: Vec<&'b UpdateAction> = update_actions + .iter() + .filter_map(|action| { + let target_version = Version::parse(&action.version).ok()?; + if *current_controller_version >= target_version + && target_version > *current_agent_version + { + Some(action) + } else { + None + } + }) + .collect(); + + Ok(pending_actions) +} + +async fn monitor_agent_version<'a, R: RuntimeManager>( + runtime_manager: &'a R, + expected_version: &Version, +) -> Result<(), UpdateError> { + let timeout = Duration::from_secs(180); + let interval = Duration::from_secs(3); + + let start = Instant::now(); + let mut interval_timer = time::interval(interval); + + while start.elapsed() < timeout { + interval_timer.tick().await; + + let version = runtime_manager.get_version().await.map_err(|e| { + log::error!("Error occurred during version check: {}", e); + UpdateError::AgentVersionCheckFailed(e.to_string()) + })?; + + if version == *expected_version { + log::info!("Expected version received: {}", expected_version); + return Ok(()); + } else { + log::info!("Version did not match expected value."); + } + } + + Err(UpdateError::AgentVersionCheckFailed(format!( + "Expected version '{}' was not received within {:?}.", + expected_version, timeout + ))) +} + +pub async fn execute<'a, R, T>( + resource_manager: &'a R, + runtime_manager: &'a mut T, +) -> Result<(), UpdateError> +where + R: ResourceManagerTrait, + T: RuntimeManager, +{ + log::info!("Starting update"); + + let res: Result<(), UpdateError> = async { + let current_version = Version::parse(env!("CARGO_PKG_VERSION")) + .map_err(|_| UpdateError::InvalidVersionFormat)?; + let runtime_info = runtime_manager.get_runtime_info()?; + if !runtime_info.is_agent_running() { + return Err(UpdateError::AgentNotRunning); + } + let current_running_agent = runtime_info.filter_by_feat(FeatType::Agent).next().unwrap(); + let bundles = resource_manager.collect_downloaded_bundles(); + let update_actions = parse_bundles(&bundles)?; + let pending_update_actions = extract_pending_update_actions( + &update_actions, + ¤t_version, + ¤t_running_agent.version, + )?; + for action in pending_update_actions { + action.handle()?; + } + // launch new version agent + let latest = runtime_manager.launch_agent(false)?; + // terminate old version agents + runtime_manager.kill_other_agents(latest.process_id)?; + monitor_agent_version(runtime_manager, ¤t_version).await?; + // if you test for rollback, comment out a follow line. + resource_manager.remove()?; + Ok(()) + } + .await; + + match res { + Ok(()) => runtime_manager.update_state(crate::managers::runtime::State::Idle)?, + Err(update_error) => { + if let Some(target_state) = get_target_state(&update_error) { + runtime_manager.update_state(target_state)?; + } + return Err(update_error); + } + } + + log::info!("Update completed"); + + Ok(()) +} + +#[cfg(all(test, unix))] +mod tests { + use super::super::tests::{MockResourceManager, MockRuntimeManager}; + use super::*; + use crate::managers::runtime::{FeatType, ProcessInfo, RuntimeInfo, State}; + use crate::state::update::tasks::{Task, UpdateAction}; + use chrono::{FixedOffset, Utc}; + use std::io::Write; + use std::path::PathBuf; + use tempfile::{tempdir, TempDir}; + + #[tokio::test] + async fn test_execute_with_empty_bundles() { + let current_version = Version::parse(env!("CARGO_PKG_VERSION")).unwrap(); + let runtime_info = RuntimeInfo { + state: State::Update, + process_infos: [ + Some(ProcessInfo { + process_id: 2, + feat_type: FeatType::Controller, + version: current_version.clone(), + executed_at: Utc::now() + .with_timezone(&FixedOffset::east_opt(9 * 3600).unwrap()), + }), + Some(ProcessInfo { + process_id: 3, + feat_type: FeatType::Agent, + version: Version::parse("0.0.1").unwrap(), + executed_at: Utc::now() + .with_timezone(&FixedOffset::east_opt(9 * 3600).unwrap()), + }), + None, + None, + ], + exec_path: "".into(), + }; + let mut runtime = MockRuntimeManager { + response_version: current_version.clone(), + runtime_info, + }; + let resource = MockResourceManager::new(vec![]); + + let result = execute(&resource, &mut runtime).await; + assert!(result.is_ok(), "Update should succeed"); + + let runtime_info: Vec<_> = runtime + .runtime_info + .process_infos + .iter() + .flatten() + .collect(); + assert_eq!(runtime_info.len(), 2); + assert_eq!(runtime_info[0].process_id, 2); + assert_eq!(runtime_info[0].feat_type, FeatType::Controller); + assert_eq!(runtime_info[0].version, current_version.clone()); + assert_eq!(runtime_info[1].process_id, 1); + assert_eq!(runtime_info[1].feat_type, FeatType::Agent); + assert_eq!(runtime_info[1].version, current_version); + } + + fn create_test_file(path: &str, content: &str) -> std::io::Result<()> { + let mut file = fs::File::create(path)?; + file.write_all(content.as_bytes())?; + Ok(()) + } + + #[tokio::test] + async fn test_execute_with_bundles() { + let current_version = Version::parse(env!("CARGO_PKG_VERSION")).unwrap(); + let runtime_info = RuntimeInfo { + state: State::Update, + process_infos: [ + Some(ProcessInfo { + process_id: 2, + feat_type: FeatType::Controller, + version: current_version.clone(), + executed_at: Utc::now() + .with_timezone(&FixedOffset::east_opt(9 * 3600).unwrap()), + }), + Some(ProcessInfo { + process_id: 3, + feat_type: FeatType::Agent, + version: Version::parse("0.0.1").unwrap(), + executed_at: Utc::now() + .with_timezone(&FixedOffset::east_opt(9 * 3600).unwrap()), + }), + None, + None, + ], + exec_path: "".into(), + }; + + let mut runtime = MockRuntimeManager { + response_version: current_version.clone(), + runtime_info, + }; + + // setup bundles + let source_path = "/tmp/source.txt"; + create_test_file(source_path, "This is source1").expect("Failed to create source1.txt"); + let dest_path = "/tmp/dest"; + + let tasks = vec![Task::Move { + description: "Move file".to_string(), + src: source_path.to_string(), + dest: dest_path.to_string(), + }]; + + let action = UpdateAction { + version: current_version.to_string(), + description: "Test move tasks".to_string(), + tasks, + }; + + let _temp_dir = tempdir().expect("Failed to create temporary directory"); + let yaml_str = serde_yaml::to_string(&action).expect("Failed to serialize action to YAML"); + let bundle_path = _temp_dir.path().join("test_bundle.yaml"); + fs::write(&bundle_path, &yaml_str).expect("Failed to write YAML to file"); + + let resource = MockResourceManager::new(vec![bundle_path]); + + let result = execute(&resource, &mut runtime).await; + assert!(result.is_ok(), "Update should succeed"); + } + + #[tokio::test] + async fn test_execute_without_running_agent() { + let current_version = Version::parse(env!("CARGO_PKG_VERSION")).unwrap(); + let runtime_info = RuntimeInfo { + state: State::Update, + process_infos: [None, None, None, None], + exec_path: "".into(), + }; + let mut runtime = MockRuntimeManager { + response_version: current_version, + runtime_info, + }; + let resource = MockResourceManager::new(vec![]); + + let result = execute(&resource, &mut runtime).await; + assert!( + matches!(result, Err(UpdateError::AgentNotRunning)), + "Should fail with AgentNotRunning" + ); + } + + #[tokio::test] + async fn test_extract_pending_update_actions() { + let current_version = Version::parse(env!("CARGO_PKG_VERSION")).unwrap(); + let _temp_dir = tempdir().expect("Failed to create temporary directory"); + + fn setup_bundle(temp_dir: &TempDir, file_name: &str, version: String) -> PathBuf { + let source_path = "/tmp/source.txt"; + create_test_file(source_path, "This is source1").expect("Failed to create source1.txt"); + let dest_path = "/tmp/dest"; + + let tasks = vec![Task::Move { + description: "Move file".to_string(), + src: source_path.to_string(), + dest: dest_path.to_string(), + }]; + + let action = UpdateAction { + version, + description: "Test move tasks".to_string(), + tasks, + }; + + let yaml_str = + serde_yaml::to_string(&action).expect("Failed to serialize action to YAML"); + let bundle_path = temp_dir.path().join(file_name); + fs::write(&bundle_path, &yaml_str).expect("Failed to write YAML to file"); + + bundle_path + } + let agent_version = Version::parse("1.0.0").unwrap(); + let bundle1 = setup_bundle(&_temp_dir, "bundle1.yml", current_version.to_string()); + let mut cloned_current_version = current_version.clone(); + cloned_current_version.patch += 1; + let bundle2 = setup_bundle( + &_temp_dir, + "bundle2.yml", + cloned_current_version.to_string(), + ); + let bundle3 = setup_bundle(&_temp_dir, "bundle3.yml", agent_version.to_string()); + let bundle4 = setup_bundle(&_temp_dir, "bundle4.yml", "1.5.0".to_string()); + + let bundles = vec![bundle1, bundle2, bundle3, bundle4]; + + let update_actions = parse_bundles(&bundles).unwrap(); + let result = + extract_pending_update_actions(&update_actions, ¤t_version, &agent_version); + + assert!(result.is_ok(), "Update should succeed"); + let pending_update_actions = result.unwrap(); + assert!( + pending_update_actions.len() == 2, + "Update should have one action" + ); + + let expected_versions = [current_version.to_string(), "1.5.0".to_string()]; + assert!(expected_versions.contains(&pending_update_actions[0].version)); + assert!(expected_versions.contains(&pending_update_actions[1].version)); + } +} diff --git a/controller/src/state/update/tasks/mod.rs b/controller/src/state/update/tasks/mod.rs new file mode 100644 index 00000000..57ce98fb --- /dev/null +++ b/controller/src/state/update/tasks/mod.rs @@ -0,0 +1,281 @@ +mod move_resource; +mod update_json; + +use crate::state::update::tasks::{move_resource::MoveResourceError, update_json::UpdateJsonError}; +use serde::{Deserialize, Serialize}; + +#[derive(Debug, Deserialize, Serialize)] +pub struct UpdateAction { + pub version: String, + pub description: String, + pub tasks: Vec, +} + +#[derive(Debug, Deserialize, Serialize, Clone)] +#[serde(tag = "action")] +pub enum Task { + Move { + description: String, + src: String, + dest: String, + }, + UpdateJson { + description: String, + file: String, + field: String, + value: String, + }, +} + +#[derive(Debug, thiserror::Error)] +pub enum UpdateActionError { + #[error("Move task failed: {0}")] + Move(#[from] MoveResourceError), + #[error("Update JSON operation failed: {0}")] + UpdateJson(#[from] UpdateJsonError), +} + +impl UpdateAction { + pub fn handle(&self) -> Result<(), UpdateActionError> { + for task in &self.tasks { + match task { + Task::Move { src, dest, .. } => { + move_resource::run(src, dest)?; + } + Task::UpdateJson { + file, field, value, .. + } => { + update_json::run(file, field, value)?; + } + }; + } + Ok(()) + } +} + +#[cfg(test)] +mod tests { + use super::*; + use std::fs; + use std::io::Write; + use std::path::PathBuf; + + mod mock_move_resource { + use super::*; + #[allow(dead_code)] + pub fn run(src: &str, _dest: &str) -> Result<(), MoveResourceError> { + if src == "error" { + Err(MoveResourceError::SourceNotFoundError(PathBuf::from(src))) + } else { + Ok(()) + } + } + } + + mod mock_update_json { + use super::*; + #[allow(dead_code)] + pub fn run(file: &str, _field: &str, _value: &str) -> Result<(), UpdateJsonError> { + if file == "error.json" { + Err(UpdateJsonError::InvalidFieldPath( + "invalid_field".to_string(), + )) + } else { + Ok(()) + } + } + } + + fn create_test_file(path: &str, content: &str) -> std::io::Result<()> { + let mut file = fs::File::create(path)?; + file.write_all(content.as_bytes())?; + Ok(()) + } + + fn cleanup_test_file(path: &str) { + let _ = fs::remove_file(path); + } + + #[test] + fn test_handle_successful_move_tasks() { + let source1_path = "/tmp/source1.txt"; + let source2_path = "/tmp/source2.txt"; + + let dest1_path = "/tmp/dest1"; + let dest2_path = "/tmp/dest2"; + + create_test_file(source1_path, "This is source1").expect("Failed to create source1.txt"); + create_test_file(source2_path, "This is source2").expect("Failed to create source2.txt"); + + let tasks = vec![ + Task::Move { + description: "Move file 1".to_string(), + src: source1_path.to_string(), + dest: dest1_path.to_string(), + }, + Task::Move { + description: "Move file 2".to_string(), + src: source2_path.to_string(), + dest: dest2_path.to_string(), + }, + ]; + + let action = UpdateAction { + version: "1.0.0".to_string(), + description: "Test move tasks".to_string(), + tasks, + }; + + let result = action.handle(); + + assert!( + result.is_ok(), + "Expected successful execution, but got: {:?}", + result + ); + + assert!( + fs::metadata(source1_path).is_err(), + "Source1 file should have been moved" + ); + assert!( + fs::metadata(source2_path).is_err(), + "Source2 file should have been moved" + ); + assert!( + fs::metadata(dest1_path).is_ok(), + "Destination1 file should exist" + ); + assert!( + fs::metadata(dest2_path).is_ok(), + "Destination2 file should exist" + ); + + cleanup_test_file(dest1_path); + cleanup_test_file(dest2_path); + } + + #[test] + fn test_handle_successful_update_json_tasks() { + let source1_path = "/tmp/test1.json"; + let source2_path = "/tmp/test2.json"; + + create_test_file( + source1_path, + r#"{"key1": "old_value1", "key3": "unchanged"}"#, + ) + .expect("Failed to create test1.json"); + create_test_file( + source2_path, + r#"{"key2": "old_value2", "key4": "unchanged"}"#, + ) + .expect("Failed to create test2.json"); + + let tasks = vec![ + Task::UpdateJson { + description: "Update field 1".to_string(), + file: source1_path.to_string(), + field: "key1".to_string(), + value: "value1".to_string(), + }, + Task::UpdateJson { + description: "Update field 2".to_string(), + file: source2_path.to_string(), + field: "key2".to_string(), + value: "value2".to_string(), + }, + ]; + + let action = UpdateAction { + version: "1.0.0".to_string(), + description: "Test update JSON tasks".to_string(), + tasks, + }; + + let result = action.handle(); + assert!( + result.is_ok(), + "Expected successful execution, but got: {:?}", + result + ); + + let updated_content1 = + std::fs::read_to_string(source1_path).expect("Failed to read test1.json"); + let updated_content2 = + std::fs::read_to_string(source2_path).expect("Failed to read test2.json"); + + let json1: serde_json::Value = + serde_json::from_str(&updated_content1).expect("Failed to parse test1.json"); + let json2: serde_json::Value = + serde_json::from_str(&updated_content2).expect("Failed to parse test2.json"); + + assert_eq!(json1["key1"], "value1", "key1 should be updated to value1"); + assert_eq!(json1["key3"], "unchanged", "key3 should remain unchanged"); + + assert_eq!(json2["key2"], "value2", "key2 should be updated to value2"); + assert_eq!(json2["key4"], "unchanged", "key4 should remain unchanged"); + + cleanup_test_file(source1_path); + cleanup_test_file(source2_path); + } + + #[test] + fn test_handle_move_task_error() { + let tasks = vec![ + Task::Move { + description: "Move valid file".to_string(), + src: "/tmp/source1.txt".to_string(), + dest: "/tmp/dest1".to_string(), + }, + Task::Move { + description: "Move invalid file".to_string(), + src: "error".to_string(), + dest: "/tmp/dest2".to_string(), + }, + ]; + + let action = UpdateAction { + version: "1.0.0".to_string(), + description: "Test move task error".to_string(), + tasks, + }; + + let result = action.handle(); + assert!( + matches!(result, Err(UpdateActionError::Move(_))), + "Expected Move error, but got: {:?}", + result + ); + } + + #[test] + fn test_handle_update_json_task_error() { + let tasks = vec![ + Task::UpdateJson { + description: "Update valid JSON".to_string(), + file: "/tmp/test1.json".to_string(), + field: "key1".to_string(), + value: "value1".to_string(), + }, + Task::UpdateJson { + description: "Update invalid JSON".to_string(), + file: "error.json".to_string(), + field: "key2".to_string(), + value: "value2".to_string(), + }, + ]; + + let action = UpdateAction { + version: "1.0.0".to_string(), + description: "Test update JSON task error".to_string(), + tasks, + }; + + let result = action.handle(); + assert!( + matches!(result, Err(UpdateActionError::UpdateJson(_))), + "Expected UpdateJson error, but got: {:?}", + result + ); + } +} diff --git a/controller/src/state/update/tasks/move_resource.rs b/controller/src/state/update/tasks/move_resource.rs new file mode 100644 index 00000000..e2ffa526 --- /dev/null +++ b/controller/src/state/update/tasks/move_resource.rs @@ -0,0 +1,171 @@ +use std::fs; +use std::path::{Path, PathBuf}; + +#[derive(Debug, thiserror::Error)] +pub enum MoveResourceError { + #[error("Source file '{0}' not found or is not a file")] + SourceNotFoundError(PathBuf), + #[error("Destination directory '{0}' does not exist and could not be created: {1}")] + DestinationCreationError(PathBuf, #[source] std::io::Error), + #[error("Destination path '{0}' is not a directory")] + DestinationNotDirectoryError(PathBuf), + #[error("Invalid source file name for '{0}'")] + InvalidSourceFileName(PathBuf), + #[error("Failed to move file from '{0}' to '{1}': {2}")] + FileMoveError(PathBuf, PathBuf, #[source] std::io::Error), +} + +pub fn run(src: &String, dest: &String) -> Result<(), MoveResourceError> { + let src_path = Path::new(src).to_path_buf(); + if !src_path.exists() { + return Err(MoveResourceError::SourceNotFoundError(src_path)); + } else if src_path.is_dir() { + return Err(MoveResourceError::InvalidSourceFileName(src_path)); + } + + let dest_path = Path::new(dest).to_path_buf(); + if !dest_path.exists() { + log::info!( + "Destination directory does not exist. Creating directory: {}", + dest + ); + fs::create_dir_all(&dest_path) + .map_err(|e| MoveResourceError::DestinationCreationError(dest_path.clone(), e))?; + } else if !dest_path.is_dir() { + return Err(MoveResourceError::DestinationNotDirectoryError(dest_path)); + } + + let file_name = src_path + .file_name() + .ok_or_else(|| MoveResourceError::InvalidSourceFileName(src_path.clone()))?; + let dest_file_path = dest_path.join(file_name); + + log::info!("Moving file from {} to {}", src, dest_file_path.display()); + + fs::rename(&src_path, &dest_file_path) + .map_err(|e| MoveResourceError::FileMoveError(src_path, dest_file_path, e))?; + + Ok(()) +} + +#[cfg(test)] +mod tests { + use super::*; + use std::fs::File; + use tempfile::tempdir; + + #[test] + fn test_run_success() { + let temp_dir = tempdir().expect("Failed to create temporary directory"); + let src_file_path = temp_dir.path().join("source.txt"); + let dest_dir_path = temp_dir.path().join("destination"); + + File::create(&src_file_path).expect("Failed to create source file"); + + let result = run( + &src_file_path.to_string_lossy().to_string(), + &dest_dir_path.to_string_lossy().to_string(), + ); + + assert!( + result.is_ok(), + "Expected run to succeed, but got error: {:?}", + result + ); + + let dest_file_path = dest_dir_path.join("source.txt"); + assert!( + dest_file_path.exists(), + "Expected file to be moved to {:?}, but it does not exist", + dest_file_path + ); + + assert!( + !src_file_path.exists(), + "Expected source file to be removed, but it still exists" + ); + } + + #[test] + fn test_source_not_found_error() { + let temp_dir = tempdir().expect("Failed to create temporary directory"); + let src_file_path = temp_dir.path().join("non_existent.txt"); + let dest_dir_path = temp_dir.path().join("destination"); + + let result = run( + &src_file_path.to_string_lossy().to_string(), + &dest_dir_path.to_string_lossy().to_string(), + ); + + assert!( + matches!(result, Err(MoveResourceError::SourceNotFoundError(_))), + "Expected SourceNotFoundError, but got: {:?}", + result + ); + } + + #[test] + fn test_destination_creation_error() { + let temp_dir = tempdir().expect("Failed to create temporary directory"); + let src_file_path = temp_dir.path().join("source.txt"); + + File::create(&src_file_path).expect("Failed to create source file"); + + let dest_dir_path = "/invalid/destination/directory"; + + let result = run( + &src_file_path.to_string_lossy().to_string(), + &dest_dir_path.to_string(), + ); + + assert!( + matches!( + result, + Err(MoveResourceError::DestinationCreationError(_, _)) + ), + "Expected DestinationCreationError, but got: {:?}", + result + ); + } + + #[test] + fn test_destination_not_directory_error() { + let temp_dir = tempdir().expect("Failed to create temporary directory"); + let src_file_path = temp_dir.path().join("source.txt"); + let dest_file_path = temp_dir.path().join("not_a_directory.txt"); + + File::create(&src_file_path).expect("Failed to create source file"); + File::create(&dest_file_path).expect("Failed to create destination file"); + + let result = run( + &src_file_path.to_string_lossy().to_string(), + &dest_file_path.to_string_lossy().to_string(), + ); + + assert!( + matches!( + result, + Err(MoveResourceError::DestinationNotDirectoryError(_)) + ), + "Expected DestinationNotDirectoryError, but got: {:?}", + result + ); + } + + #[test] + fn test_invalid_source_file_name_error() { + let temp_dir = tempdir().expect("Failed to create temporary directory"); + let dest_dir_path = temp_dir.path().join("destination"); + + let result = run( + &temp_dir.path().to_string_lossy().to_string(), + &dest_dir_path.to_string_lossy().to_string(), + ); + + assert!( + matches!(result, Err(MoveResourceError::InvalidSourceFileName(_))), + "Expected InvalidSourceFileName, but got: {:?}", + result + ); + } +} diff --git a/controller/src/state/update/tasks/update_json.rs b/controller/src/state/update/tasks/update_json.rs new file mode 100644 index 00000000..f2cf110a --- /dev/null +++ b/controller/src/state/update/tasks/update_json.rs @@ -0,0 +1,238 @@ +use serde_json::{error::Error as SerdeError, Value}; +use std::fs; + +#[derive(Debug, thiserror::Error)] +pub enum UpdateJsonError { + #[error("Failed to read JSON file '{0}': {1}")] + FileReadError(String, #[source] std::io::Error), + #[error("Failed to parse JSON in file '{0}': {1}")] + JsonParseError(String, #[source] SerdeError), + #[error("Invalid field path '{0}'")] + InvalidFieldPath(String), + #[error("Failed to write JSON file '{0}': {1}")] + FileWriteError(String, #[source] std::io::Error), +} + +pub fn run(file: &String, field: &String, value: &String) -> Result<(), UpdateJsonError> { + // Array updates are not supported. + // It's unclear whether the operation is an addition or a completely new write. + log::info!( + "Updating JSON file '{}' field '{}' with value '{}'", + file, + field, + value + ); + + let file_content = fs::read_to_string(file) + .map_err(|e| UpdateJsonError::FileReadError(file.to_string(), e))?; + + let mut json_data: Value = serde_json::from_str(&file_content) + .map_err(|e| UpdateJsonError::JsonParseError(file.to_string(), e))?; + + let parts: Vec<&str> = field.split('.').collect(); + let mut current = &mut json_data; + for part in &parts[..parts.len() - 1] { + current = current + .get_mut(part) + .ok_or_else(|| UpdateJsonError::InvalidFieldPath(field.to_string()))?; + } + + current[parts.last().unwrap()] = Value::String(value.to_string()); + + fs::write( + file, + serde_json::to_string_pretty(&json_data) + .map_err(|e| UpdateJsonError::JsonParseError(file.to_string(), e))?, + ) + .map_err(|e| UpdateJsonError::FileWriteError(file.to_string(), e))?; + + Ok(()) +} + +#[cfg(test)] +mod tests { + use super::*; + use std::fs; + use std::os::unix::fs::PermissionsExt; + use tempfile::tempdir; + + #[test] + fn test_creates_nested_structure() { + let temp_dir = tempdir().unwrap(); + let file_path = temp_dir.path().join("test.json"); + fs::write(&file_path, r#"{"key1": {"key2": "value"}}"#).unwrap(); + + let field = "key1.key2".to_string(); + let value = "new_value".to_string(); + let file_path_str = file_path.to_str().unwrap().to_string(); + + let result = run(&file_path_str, &field, &value); + assert!( + result.is_ok(), + "Expected run to succeed, but got an error: {:?}", + result + ); + + let updated_content = fs::read_to_string(&file_path).unwrap(); + let expected_content = r#"{ + "key1": { + "key2": "new_value" + } +}"#; + assert_eq!( + updated_content.trim(), + expected_content, + "File content mismatch" + ); + } + + #[test] + fn test_array_value() { + let temp_dir = tempdir().unwrap(); + let file_path = temp_dir.path().join("test.json"); + fs::write(&file_path, r#"{"key1": {"key2": ["item1", "item2"]}}"#).unwrap(); + + let field = "key1.key2".to_string(); + let value = "new_item".to_string(); + let file_path_str = file_path.to_str().unwrap().to_string(); + + let result = run(&file_path_str, &field, &value); + assert!( + result.is_ok(), + "Expected run to succeed, but got an error: {:?}", + result + ); + + let updated_content = fs::read_to_string(&file_path).unwrap(); + let expected_content = r#"{ + "key1": { + "key2": "new_item" + } +}"#; + assert_eq!( + updated_content.trim(), + expected_content, + "File content mismatch" + ); + } + + #[test] + fn test_adds_new_field_to_existing_object() { + let temp_dir = tempdir().unwrap(); + let file_path = temp_dir.path().join("test.json"); + fs::write(&file_path, r#"{"key1": {"other_key1": "value1"}}"#).unwrap(); + + let field = "key1.other_key2".to_string(); + let value = "new_value".to_string(); + let file_path_str = file_path.to_str().unwrap().to_string(); + + let result = run(&file_path_str, &field, &value); + assert!( + result.is_ok(), + "Expected run to succeed, but got an error: {:?}", + result + ); + + let updated_content = fs::read_to_string(&file_path).unwrap(); + let expected_content = r#"{ + "key1": { + "other_key1": "value1", + "other_key2": "new_value" + } +}"#; + assert_eq!( + updated_content.trim(), + expected_content, + "File content mismatch" + ); + } + + #[test] + fn test_handles_write_error_by_permission() { + let temp_dir = tempdir().unwrap(); + let file_path = temp_dir.path().join("test.json"); + fs::write(&file_path, r#"{"key1": "value1"}"#).unwrap(); + + let permissions = fs::Permissions::from_mode(0o444); + fs::set_permissions(&file_path, permissions).unwrap(); + + let field = "key1".to_string(); + let value = "new_value".to_string(); + let file_path_str = file_path.to_str().unwrap().to_string(); + + let result = run(&file_path_str, &field, &value); + assert!( + matches!(result, Err(UpdateJsonError::FileWriteError(_, _))), + "Expected FileWriteError, but got: {:?}", + result + ); + + fs::set_permissions(&file_path, fs::Permissions::from_mode(0o644)).unwrap(); + } + + #[test] + fn test_invalid_json_file() { + let temp_dir = tempdir().unwrap(); + let file_path = temp_dir.path().join("invalid.json"); + fs::write(&file_path, "not a json").unwrap(); + + let field = "key1".to_string(); + let value = "new_value".to_string(); + let file_path_str = file_path.to_str().unwrap().to_string(); + + let result = run(&file_path_str, &field, &value); + assert!( + matches!(result, Err(UpdateJsonError::JsonParseError(_, _))), + "Expected JsonParseError, but got: {:?}", + result + ); + } + + #[test] + fn test_empty_json_file() { + let temp_dir = tempdir().unwrap(); + let file_path = temp_dir.path().join("empty.json"); + fs::write(&file_path, "").unwrap(); + + let field = "key1".to_string(); + let value = "new_value".to_string(); + let file_path_str = file_path.to_str().unwrap().to_string(); + + let result = run(&file_path_str, &field, &value); + assert!( + matches!(result, Err(UpdateJsonError::JsonParseError(_, _))), + "Expected JsonParseError for empty file, but got: {:?}", + result + ); + } + + #[test] + fn test_updates_existing_value() { + let temp_dir = tempdir().unwrap(); + let file_path = temp_dir.path().join("test.json"); + fs::write(&file_path, r#"{"key1": {"key2": "old_value"}}"#).unwrap(); + + let field = "key1.key2".to_string(); + let value = "new_value".to_string(); + let file_path_str = file_path.to_str().unwrap().to_string(); + + let result = run(&file_path_str, &field, &value); + assert!( + result.is_ok(), + "Expected run to succeed, but got an error: {:?}", + result + ); + + let updated_content = fs::read_to_string(&file_path).unwrap(); + let expected_content = r#"{ + "key1": { + "key2": "new_value" + } +}"#; + assert_eq!( + updated_content.trim(), + expected_content, + "File content mismatch" + ); + } +} diff --git a/controller/src/unix_utils.rs b/controller/src/unix_utils.rs new file mode 100644 index 00000000..3e98749c --- /dev/null +++ b/controller/src/unix_utils.rs @@ -0,0 +1,214 @@ +use bytes::Bytes; +use http_body_util::{BodyExt, Full}; +use hyper::{body::Incoming, Response}; +use hyper_util::client::legacy::{Client, Error as LegacyClientError}; +use hyperlocal::{UnixClientExt, UnixConnector, Uri}; +use nix::sys::socket::{recvmsg, sendmsg, ControlMessage, ControlMessageOwned, MsgFlags}; +use notify::event::{AccessKind, AccessMode, CreateKind, MetadataKind, ModifyKind}; +use notify::{Config, Event, EventKind, RecommendedWatcher, RecursiveMode, Watcher}; +use serde::de::DeserializeOwned; +use std::env; +use std::fs::set_permissions; +use std::io::{IoSlice, IoSliceMut}; +use std::os::unix::fs::PermissionsExt; +use std::os::unix::io::RawFd; +use std::path::{Path, PathBuf}; + +pub fn convention_of_meta_uds_path(uds: impl AsRef) -> std::io::Result { + let parent = uds.as_ref().parent().ok_or(std::io::Error::new( + std::io::ErrorKind::NotFound, + "Failed to get path of unix domain socket", + ))?; + let base_name = + uds.as_ref() + .file_name() + .and_then(|x| x.to_str()) + .ok_or(std::io::Error::new( + std::io::ErrorKind::NotFound, + "Failed to get path of unix domain socket", + ))?; + Ok(parent.join(format!("meta_{}", base_name))) +} + +pub fn send_fd(tx: RawFd, fd: Option) -> nix::Result<()> { + match fd { + Some(fd) => { + let iov = [IoSlice::new(&[0u8; 1])]; + let fds = [fd]; + let cmsg = ControlMessage::ScmRights(&fds); + sendmsg::<()>(tx, &iov, &[cmsg], MsgFlags::empty(), None)?; + } + None => { + let iov = [IoSlice::new(&[1u8; 1])]; + let fds = []; + let cmsg = ControlMessage::ScmRights(&fds); + sendmsg::<()>(tx, &iov, &[cmsg], MsgFlags::empty(), None)?; + } + }; + Ok(()) +} + +pub fn recv_fd(socket: RawFd) -> nix::Result> { + let mut buf = [0u8; 1]; + let mut iov = [IoSliceMut::new(&mut buf)]; + let mut space = nix::cmsg_space!([RawFd; 1]); + let msg = recvmsg::<()>(socket, &mut iov, Some(&mut space), MsgFlags::empty())?; + let buf = msg.iovs().next().ok_or(nix::errno::Errno::ENOENT)?; + let is_some = !buf.is_empty() && buf[0] == 1; + if is_some { + return Ok(None); + } else { + let cmsg = msg.cmsgs()?.next().ok_or(nix::errno::Errno::ENOENT)?; + if let ControlMessageOwned::ScmRights(fds) = cmsg { + if !fds.is_empty() { + return Ok(Some(fds[0])); + } + } + } + Err(nix::Error::ENOENT) +} + +pub fn wait_until_file_created(path: impl AsRef) -> notify::Result<()> { + let (tx, rx) = std::sync::mpsc::channel(); + let mut watcher = RecommendedWatcher::new(tx, Config::default())?; + let dir = path + .as_ref() + .parent() + .ok_or(notify::Error::io(std::io::Error::new( + std::io::ErrorKind::NotFound, + "Failed to get parent of watching path", + )))?; + watcher.watch(dir.as_ref(), RecursiveMode::NonRecursive)?; + let path = path.as_ref().to_path_buf(); + if !path.exists() { + for res in rx { + match res? { + // ref: https://docs.rs/notify/latest/notify/#macos-fsevents-and-unowned-files + Event { + kind: EventKind::Modify(ModifyKind::Metadata(MetadataKind::Ownership)), + paths, + .. + } + | Event { + kind: EventKind::Access(AccessKind::Close(AccessMode::Write)), + paths, + .. + } + | Event { + kind: EventKind::Create(CreateKind::File), + paths, + .. + } if paths.contains(&path) => return Ok(()), + _ => continue, + } + } + } + Ok(()) +} + +pub fn remove_file_if_exists(path: impl AsRef) { + if path.as_ref().exists() { + let _ = std::fs::remove_file(path); + } +} + +#[derive(Debug, thiserror::Error)] +pub enum GetRequestError { + #[error("Failed to collect body: {0}")] + CollectBody(#[from] hyper::Error), + #[error("Failed to convert body to string: {0}")] + Utf8(#[from] std::str::Utf8Error), + #[error("Failed to parse JSON response: {0}")] + Json(#[from] serde_json::Error), + #[error("Request failed: {0}")] + RequestFailed(#[from] LegacyClientError), +} + +async fn parse_response_body(response: Response) -> Result +where + T: DeserializeOwned, +{ + let collected_body = response.into_body().collect().await?; + let bytes = collected_body.to_bytes(); + let string_body = std::str::from_utf8(bytes.as_ref())?; + Ok(serde_json::from_str(string_body)?) +} + +pub async fn get_request( + uds_path: impl AsRef, + endpoint: &str, +) -> Result +where + T: serde::de::DeserializeOwned + Send, +{ + let client: Client> = Client::unix(); + let uri = Uri::new(uds_path, endpoint).into(); + let response: Response = client.get(uri).await?; + parse_response_body(response).await +} + +pub fn change_to_executable(path: &Path) -> std::io::Result<()> { + let mut perms = std::fs::metadata(path)?.permissions(); + perms.set_mode(perms.mode() | 0o111); + set_permissions(path, perms) +} + +// NOTE: the LISTEN_FDS is assigned from 3. +// ref: https://manpages.debian.org/testing/libsystemd-dev/sd_listen_fds.3.en.html +static DEFAULT_FD: RawFd = 3; + +#[derive(Debug, thiserror::Error)] +pub enum GetFdError { + #[error("LISTEN_FDS not set or invalid")] + ListenFdsError, + #[error("LISTEN_PID not set or invalid")] + ListenPidError, + #[error("LISTEN_PID ({listen_pid}) does not match current process ID ({current_pid})")] + ListenPidMismatch { listen_pid: i32, current_pid: i32 }, + #[error("No file descriptors passed by systemd.")] + NoFileDescriptors, +} + +pub fn get_fd_from_systemd() -> Result { + let listen_fds = env::var("LISTEN_FDS") + .ok() + .and_then(|x| x.parse::().ok()) + .ok_or(GetFdError::ListenFdsError)?; + + let listen_pid = env::var("LISTEN_PID") + .ok() + .and_then(|x| x.parse::().ok()) + .ok_or(GetFdError::ListenPidError)?; + + let current_pid = std::process::id() as i32; + if listen_pid != current_pid { + return Err(GetFdError::ListenPidMismatch { + listen_pid, + current_pid, + }); + } else if listen_fds <= 0 { + return Err(GetFdError::NoFileDescriptors); + } + Ok(DEFAULT_FD) +} + +#[cfg(all(test, unix))] +mod tests { + use super::*; + use std::env; + + #[test] + fn test_setup_listener_with_systemd_activation() { + env::set_var("LISTEN_FDS", "1"); + env::set_var("LISTEN_PID", std::process::id().to_string()); + + let result = get_fd_from_systemd(); + assert!(result.is_ok(), "Systemd socket activation should succeed"); + let listener_fd = result.unwrap(); + + assert_eq!( + listener_fd, DEFAULT_FD, + "Listener FD should match DEFAULT_FD" + ); + } +} diff --git a/controller/src/validator/mod.rs b/controller/src/validator/mod.rs new file mode 100644 index 00000000..4451d1d0 --- /dev/null +++ b/controller/src/validator/mod.rs @@ -0,0 +1,3 @@ +pub mod network; +pub mod process; +pub mod storage; diff --git a/controller/src/validator/network.rs b/controller/src/validator/network.rs new file mode 100644 index 00000000..fe7741ee --- /dev/null +++ b/controller/src/validator/network.rs @@ -0,0 +1,51 @@ +use reqwest::Client; +use std::time::Duration; + +pub async fn can_connect_to_download_server(url: &str) -> bool { + let client = Client::builder().timeout(Duration::from_secs(5)).build(); + + let client = match client { + Ok(c) => c, + Err(_) => return false, + }; + + match client.get(url).send().await { + Ok(response) => response.status().is_success(), + Err(_) => false, + } +} + +#[cfg(test)] +mod tests { + use super::can_connect_to_download_server; + use httpmock::MockServer; + + #[tokio::test] + async fn test_can_connect_to_download_server() { + let server = MockServer::start(); + + let success_mock = server.mock(|when, then| { + when.method("GET").path("/success"); + then.status(200).body("Success"); + }); + + let failure_mock = server.mock(|when, then| { + when.method("GET").path("/failure"); + then.status(500).body("Internal Server Error"); + }); + + let success_url = format!("{}{}", server.base_url(), "/success"); + let result = can_connect_to_download_server(&success_url).await; + assert!(result, "Expected success for the valid URL"); + success_mock.assert(); + + let failure_url = format!("{}{}", server.base_url(), "/failure"); + let result = can_connect_to_download_server(&failure_url).await; + assert!(!result, "Expected failure for the invalid URL"); + failure_mock.assert(); + + let invalid_url = "http://invalid-url"; + let result = can_connect_to_download_server(invalid_url).await; + assert!(!result, "Expected failure for an unreachable URL"); + } +} diff --git a/controller/src/validator/process.rs b/controller/src/validator/process.rs new file mode 100644 index 00000000..a71f1d72 --- /dev/null +++ b/controller/src/validator/process.rs @@ -0,0 +1,101 @@ +#[cfg(unix)] +use nix::sys::signal; +#[cfg(unix)] +use nix::unistd::Pid; +use std::env; + +pub fn is_manage_by_systemd() -> bool { + env::var("INVOCATION_ID").is_ok() +} + +pub fn is_manage_socket_activation() -> bool { + env::var("LISTEN_PID").is_ok() && env::var("LISTEN_FDS").is_ok() +} + +#[cfg(unix)] +pub fn is_running(process_id: u32) -> bool { + let pid = Pid::from_raw(process_id as i32); + match signal::kill(pid, None) { + Ok(()) => true, + Err(_) => false, + } +} + +#[cfg(windows)] +pub fn is_running(process_id: u32) -> bool { + unimplemented!("implemented for Windows."); +} + +#[cfg(all(test, unix))] +mod tests { + use super::*; + use serial_test::serial; + use std::env; + + #[test] + #[serial] + fn test_is_manage_by_systemd() { + env::set_var("INVOCATION_ID", "dummy_id"); + assert!(is_manage_by_systemd(), "Expected to be managed by systemd"); + + env::remove_var("INVOCATION_ID"); + assert!( + !is_manage_by_systemd(), + "Expected not to be managed by systemd" + ); + } + + #[test] + #[serial] + fn test_is_manage_socket_activation() { + env::set_var("LISTEN_PID", "12345"); + env::set_var("LISTEN_FDS", "2"); + assert!( + is_manage_socket_activation(), + "Expected to be managed by socket activation" + ); + + env::remove_var("LISTEN_PID"); + assert!( + !is_manage_socket_activation(), + "Expected not to be managed by socket activation" + ); + + env::set_var("LISTEN_PID", "12345"); + env::remove_var("LISTEN_FDS"); + assert!( + !is_manage_socket_activation(), + "Expected not to be managed by socket activation" + ); + } + + #[cfg(unix)] + #[tokio::test] + async fn test_is_running() { + use nix::unistd::{fork, ForkResult}; + + match unsafe { fork() } { + Ok(ForkResult::Child) => { + std::process::exit(0); + } + Ok(ForkResult::Parent { child }) => { + assert!( + is_running(child.as_raw() as u32), + "Expected child process to be running" + ); + + let status = + nix::sys::wait::waitpid(child, None).expect("Failed to wait for child process"); + assert!( + !is_running(child.as_raw() as u32), + "Expected child process to have exited, but it is still running" + ); + + if let nix::sys::wait::WaitStatus::Exited(_, exit_code) = status { + assert_eq!(exit_code, 0, "Child process did not exit cleanly"); + } + } + Err(_) => panic!("Fork failed"), + } + } +} diff --git a/controller/src/validator/storage.rs b/controller/src/validator/storage.rs new file mode 100644 index 00000000..9a05d728 --- /dev/null +++ b/controller/src/validator/storage.rs @@ -0,0 +1,102 @@ +use std::fs; +use std::path::Path; + +const BASE_LIMIT: u64 = 50 * 1024 * 1024; +const MIN_FREE_SPACE: u64 = 30 * 1024 * 1024; + +pub fn check_storage(directory: impl AsRef) -> bool { + let dir_path = directory.as_ref().to_path_buf(); + let total_size = calculate_directory_size(&dir_path).unwrap_or(0); + let free_space = BASE_LIMIT.saturating_sub(total_size); + free_space >= MIN_FREE_SPACE +} + +fn calculate_directory_size(dir: impl AsRef) -> Result { + let mut total_size = 0; + + for entry in fs::read_dir(dir)? { + let entry = entry?; + let metadata = entry.metadata()?; + + if metadata.is_file() { + total_size += metadata.len(); + } else if metadata.is_dir() { + total_size += calculate_directory_size(entry.path())?; + } + } + + Ok(total_size) +} + +#[cfg(test)] +mod tests { + use super::*; + use std::io::Write; + use tempfile::tempdir; + + #[test] + fn test_calculate_directory_size_empty() { + let dir = tempdir().unwrap(); + let dir_path = dir.path().to_path_buf(); + let size = calculate_directory_size(&dir_path).unwrap(); + assert_eq!(size, 0, "Empty directory should have size 0"); + } + + #[test] + fn test_calculate_directory_size_with_files() { + let dir = tempdir().unwrap(); + let dir_path = dir.path().to_path_buf(); + + let file_path1 = dir_path.join("file1.txt"); + { + let mut file = std::fs::File::create(&file_path1).unwrap(); + file.write_all(&vec![0u8; 1024]).unwrap(); + } + + let file_path2 = dir_path.join("file2.txt"); + { + let mut file = std::fs::File::create(&file_path2).unwrap(); + file.write_all(&vec![0u8; 2048]).unwrap(); + } + + let sub_dir_path = dir_path.join("subdir"); + std::fs::create_dir_all(&sub_dir_path).unwrap(); + let sub_file_path = sub_dir_path.join("file3.txt"); + { + let mut file = std::fs::File::create(&sub_file_path).unwrap(); + file.write_all(&vec![0u8; 1024]).unwrap(); + } + + let size = calculate_directory_size(&dir_path).unwrap(); + assert_eq!(size, 1024 + 2048 + 1024, "Total size should be 4KB"); + } + + #[test] + fn test_check_storage_sufficient_space() { + let dir = tempdir().unwrap(); + let dir_path = dir.path().to_path_buf(); + + assert!( + check_storage(&dir_path), + "Empty directory should have enough space" + ); + } + + #[test] + fn test_check_storage_insufficient_space() { + let dir = tempdir().unwrap(); + let dir_path = dir.path().to_path_buf(); + + let big_file_path = dir_path.join("bigfile.bin"); + { + let mut file = std::fs::File::create(&big_file_path).unwrap(); + let large_data = vec![0u8; (45 * 1024 * 1024) as usize]; + file.write_all(&large_data).unwrap(); + } + + assert!( + !check_storage(&dir_path), + "Directory with large file should not have enough space" + ); + } +} diff --git a/e2e/src/common/platform_client.rs b/e2e/src/common/platform_client.rs index 795c017d..130ac504 100644 --- a/e2e/src/common/platform_client.rs +++ b/e2e/src/common/platform_client.rs @@ -1,8 +1,6 @@ use http_body_util::BodyExt; -use hyper::{ - body::{Body, Incoming}, - Response, Uri as HyperUri, -}; +use hyper::body::{Body, Incoming}; +use hyper::Response; use hyper_util::client::legacy::Client as LegacyClient; use std::boxed::Box; use std::error::Error as StdError; @@ -26,7 +24,7 @@ pub enum GenericUri { #[cfg(unix)] Unix(HyperLocalUri), #[cfg(windows)] - Http(HyperUri), + Http(hyper::Uri), } impl GenericUri { @@ -47,11 +45,8 @@ pub fn new_uri(url: &str) -> hyper::Uri { { let homedir = dirs::home_dir().expect("Home directory not found"); let socket_path = homedir.join(".nodex/run/nodex.sock"); - let generic_uri = GenericUri::new_unix(socket_path, url); - match generic_uri { - GenericUri::Unix(uri) => uri.into(), - _ => panic!("Invalid URI type"), - } + let GenericUri::Unix(uri) = GenericUri::new_unix(socket_path, url); + uri.into() } #[cfg(windows)] { diff --git a/e2e/tests/test_didcomm_message.rs b/e2e/tests/test_didcomm_message.rs index 9bbec845..547a510e 100644 --- a/e2e/tests/test_didcomm_message.rs +++ b/e2e/tests/test_didcomm_message.rs @@ -1,8 +1,6 @@ -use http_body_util::BodyExt; -use hyper::{body::Incoming, Method, Request, StatusCode}; +use hyper::{Method, Request, StatusCode}; use serde_json::json; use std::fs::read; -use tokio::io::AsyncWriteExt as _; use e2e::common::platform_client::{new_client, new_uri, response_to_string}; @@ -42,7 +40,6 @@ async fn create_didcomm_message_scenario() -> anyhow::Result { } async fn verify_didcomm_message_scenario(input: String) -> anyhow::Result<()> { - let homedir = dirs::home_dir().unwrap(); let client = new_client(); let body = json!({ diff --git a/e2e/tests/test_verifiable_message.rs b/e2e/tests/test_verifiable_message.rs index 1d9e944b..c382056d 100644 --- a/e2e/tests/test_verifiable_message.rs +++ b/e2e/tests/test_verifiable_message.rs @@ -1,8 +1,6 @@ -use http_body_util::BodyExt; -use hyper::{body::Incoming, Method, Request, StatusCode}; +use hyper::{Method, Request, StatusCode}; use serde_json::json; use std::fs::read; -use tokio::io::AsyncWriteExt as _; use e2e::common::platform_client::{new_client, new_uri, response_to_string}; @@ -46,7 +44,6 @@ async fn create_verifiable_message_scenario() -> anyhow::Result { } async fn verify_verifiable_message_scenario(input: String) -> anyhow::Result<()> { - let homedir = dirs::home_dir().unwrap(); let client = new_client(); let body = json!({ diff --git a/examples/python/src/update_version.py b/examples/python/src/update_version.py index 6d32d66f..11ccea6f 100644 --- a/examples/python/src/update_version.py +++ b/examples/python/src/update_version.py @@ -11,7 +11,6 @@ payload = { "message": { "binary_url": "https://example.com/nodex-agent-1.0.0.zip", - "path": "/tmp", } } diff --git a/nodex-agent.deb b/nodex-agent.deb new file mode 100644 index 00000000..cd616d96 Binary files /dev/null and b/nodex-agent.deb differ diff --git a/omnibus/config/software/build-nodex-agent.rb b/omnibus/config/software/build-nodex-agent.rb index 436c6825..989e314d 100644 --- a/omnibus/config/software/build-nodex-agent.rb +++ b/omnibus/config/software/build-nodex-agent.rb @@ -8,6 +8,16 @@ end copy "#{nodex_dir}/agent/*", "#{project_dir}/agent/" + unless Dir.exist?("#{project_dir}/bin") + mkdir "#{project_dir}/bin" + end + copy "#{nodex_dir}/bin/*", "#{project_dir}/bin/" + + unless Dir.exist?("#{project_dir}/controller") + mkdir "#{project_dir}/controller" + end + copy "#{nodex_dir}/controller/*", "#{project_dir}/controller/" + unless Dir.exist?("#{project_dir}/protocol") mkdir "#{project_dir}/protocol" end diff --git a/omnibus/config/software/init-scripts.rb b/omnibus/config/software/init-scripts.rb index 60d30bbb..c9b4a6dd 100644 --- a/omnibus/config/software/init-scripts.rb +++ b/omnibus/config/software/init-scripts.rb @@ -6,10 +6,15 @@ if ohai['platform_family'] == 'debian' etc_dir = "/etc/nodex-agent" systemd_directory = "/lib/systemd/system" + erb source: "systemd.socket.erb", + dest: "#{systemd_directory}/nodex-agent.socket", + mode: 0644, + vars: { install_dir: install_dir, etc_dir: etc_dir } + project.extra_package_file "#{systemd_directory}/nodex-agent.socket" erb source: "systemd.service.erb", - dest: "#{systemd_directory}/nodex-agent.service", - mode: 0644, - vars: { install_dir: install_dir, etc_dir: etc_dir } + dest: "#{systemd_directory}/nodex-agent.service", + mode: 0644, + vars: { install_dir: install_dir, etc_dir: etc_dir } project.extra_package_file "#{systemd_directory}/nodex-agent.service" end end diff --git a/omnibus/config/templates/init-scripts/systemd.service.erb b/omnibus/config/templates/init-scripts/systemd.service.erb index acc5efc3..310e92aa 100644 --- a/omnibus/config/templates/init-scripts/systemd.service.erb +++ b/omnibus/config/templates/init-scripts/systemd.service.erb @@ -3,18 +3,31 @@ Description=Nodex Agent Service Wants=network-online.target After=network-online.target +Requires=nodex-agent.socket +After=nodex-agent.socket [Service] Type=simple # Core service execution settings. -ExecStart=/home/nodex/bin/nodex-agent +ExecStart=/home/nodex/bin/nodex-agent controller + +# Logging settings. +StandardOutput=append:/var/log/nodex-agent.log +StandardError=append:/var/log/nodex-agent-error.log + +# Custom kill signal handling. +KillSignal=SIGINT +KillMode=process + +# Socket Activation settings. +StandardInput=socket # Restart behavior: restarts on failure, with a 10-second delay between attempts. # Limits restarts to 5 attempts within a 300-second period to avoid excessive restarts. -Restart=on-failure +Restart=always RestartSec=10 -StartLimitIntervalSec=300 +StartLimitInterval=300 StartLimitBurst=5 # Runs the service as the 'nodex' user and group, and manages the process ID. @@ -30,6 +43,9 @@ ProtectKernelModules=true ProtectKernelTunables=true ProtectControlGroups=true +# Sets the umask to 0011, which restricts file permissions to 766 for sock file. +UMask=0011 + [Install] # Makes the service start automatically in multi-user mode. WantedBy=multi-user.target diff --git a/omnibus/config/templates/init-scripts/systemd.socket.erb b/omnibus/config/templates/init-scripts/systemd.socket.erb new file mode 100644 index 00000000..6b180f7b --- /dev/null +++ b/omnibus/config/templates/init-scripts/systemd.socket.erb @@ -0,0 +1,12 @@ +[Unit] +Description=Nodex Agent Socket + +[Socket] +ListenStream=/home/nodex/.nodex/run/nodex.sock +# Overridden by the umask setting, but just to be clear +SocketMode=0766 +SocketUser=nodex +SocketGroup=nodex + +[Install] +WantedBy=sockets.target diff --git a/omnibus/images/ubuntu/Dockerfile b/omnibus/images/ubuntu/Dockerfile index 02b8dad7..1814e3dc 100644 --- a/omnibus/images/ubuntu/Dockerfile +++ b/omnibus/images/ubuntu/Dockerfile @@ -26,6 +26,8 @@ RUN gem install bundler RUN curl --proto '=https' --tlsv1.2 -sSf https://sh.rustup.rs | sh -s -- -y ENV PATH="/root/.cargo/bin:${PATH}" +RUN rustup install 1.83.0 && rustup default 1.83.0 + RUN cargo install cross WORKDIR /nodex diff --git a/omnibus/package-scripts/nodex-agent-deb/postinst b/omnibus/package-scripts/nodex-agent-deb/postinst index 3ac124df..1e4272ff 100755 --- a/omnibus/package-scripts/nodex-agent-deb/postinst +++ b/omnibus/package-scripts/nodex-agent-deb/postinst @@ -5,7 +5,8 @@ # INIT_INSTALL_DIR=/opt/nodex-agent -SERVICE_NAME=nodex-agent +SERVICE_NAME=nodex-agent.service +SOCKET_NAME=nodex-agent.socket HOME_DIR=/home/nodex BIN_INSTALL_DIR=$HOME_DIR/bin @@ -41,12 +42,8 @@ else echo "Bin directory ${BIN_INSTALL_DIR} already exists." fi -if [ ! -f "${BIN_INSTALL_DIR}/nodex-agent" ]; then - echo "Moving nodex-agent to bin directory ${BIN_INSTALL_DIR}..." - mv ${INIT_INSTALL_DIR}/bin/nodex-agent ${BIN_INSTALL_DIR} -else - echo "${BIN_INSTALL_DIR}/nodex-agent already exists." -fi +echo "Moving nodex-agent to bin directory ${BIN_INSTALL_DIR}..." +mv ${INIT_INSTALL_DIR}/bin/nodex-agent ${BIN_INSTALL_DIR} if [ ! -d "${HOME_DIR}/.config" ]; then echo "Creating config directory ${HOME_DIR}/.config..." @@ -90,14 +87,18 @@ else fi # Enable and start the service +echo "Enabling and starting socket $SOCKET_NAME" +if command -v systemctl >/dev/null 2>&1; then + systemctl enable $SOCKET_NAME +fi echo "Enabling service $SERVICE_NAME" if command -v systemctl >/dev/null 2>&1; then - systemctl daemon-reload systemctl enable $SERVICE_NAME fi if [ -f "$HOME_DIR/.config/nodex/config.json" ]; then echo "(Re)starting $SERVICE_NAME now..." + systemctl stop $SOCKET_NAME if command -v systemctl >/dev/null 2>&1; then systemctl restart $SERVICE_NAME || true else diff --git a/omnibus/package-scripts/nodex-agent-deb/postrm b/omnibus/package-scripts/nodex-agent-deb/postrm index bb3e31a7..ff639c2d 100755 --- a/omnibus/package-scripts/nodex-agent-deb/postrm +++ b/omnibus/package-scripts/nodex-agent-deb/postrm @@ -3,6 +3,7 @@ INSTALL_DIR=/opt/nodex-agent SERVICE_NAME=nodex-agent +SOCKET_NAME=nodex-agent.socket HOME_DIR=/home/nodex/ set -e @@ -38,6 +39,17 @@ case "$1" in echo "Purging configuration and removing directories..." # Remove the service, if it still exists + if command -v systemctl >/dev/null 2>&1; then + if ! systemctl stop "$SOCKET_NAME"; then + echo "Failed to stop service $SOCKET_NAME" + fi + if ! systemctl disable "$SOCKET_NAME"; then + echo "Failed to disable service $SOCKET_NAME" + fi + if ! systemctl daemon-reload; then + echo "Failed to reload systemctl daemon" + fi + fi if command -v systemctl >/dev/null 2>&1; then if ! systemctl stop "$SERVICE_NAME"; then echo "Failed to stop service $SERVICE_NAME" diff --git a/protocol/Cargo.toml b/protocol/Cargo.toml index df121221..0868d5da 100644 --- a/protocol/Cargo.toml +++ b/protocol/Cargo.toml @@ -25,5 +25,5 @@ x25519-dalek = { workspace = true } zeroize = { workspace = true } [dev-dependencies] -actix-rt = { workspace = true } rand = "0.8.5" +tokio = { version = "1.42.0", features = ["full"] } diff --git a/protocol/src/did/sidetree/payload.rs b/protocol/src/did/sidetree/payload.rs index 7adc5401..08e4a73c 100644 --- a/protocol/src/did/sidetree/payload.rs +++ b/protocol/src/did/sidetree/payload.rs @@ -11,7 +11,7 @@ use crate::{ // TODO: Migrate Sidetree Version -#[derive(Debug, Serialize, Deserialize)] +#[derive(Clone, Debug, Serialize, Deserialize)] pub struct ServiceEndpoint { #[serde(rename = "id")] pub id: String, @@ -26,7 +26,7 @@ pub struct ServiceEndpoint { pub description: Option, } -#[derive(Debug, Serialize, Deserialize, Clone)] +#[derive(Clone, Debug, Serialize, Deserialize)] pub struct DidPublicKey { #[serde(rename = "id")] pub id: String, @@ -41,7 +41,7 @@ pub struct DidPublicKey { pub public_key_jwk: Jwk, } -#[derive(Debug, Serialize, Deserialize)] +#[derive(Clone, Debug, Serialize, Deserialize)] pub struct DidDocument { // TODO: impl parser for mixed type // #[serde(rename = "@context")] @@ -131,13 +131,13 @@ struct DidDeltaObject { update_commitment: String, } -#[derive(Debug, Serialize, Deserialize)] +#[derive(Clone, Debug, Serialize, Deserialize)] struct DidSuffixObject { delta_hash: String, recovery_commitment: String, } -#[derive(Debug, Serialize, Deserialize)] +#[derive(Clone, Debug, Serialize, Deserialize)] pub struct MethodMetadata { #[serde(rename = "published")] pub published: bool, @@ -149,7 +149,7 @@ pub struct MethodMetadata { pub update_commitment: Option, } -#[derive(Debug, Serialize, Deserialize)] +#[derive(Clone, Debug, Serialize, Deserialize)] pub struct DidResolutionResponse { #[serde(rename = "@context")] pub context: String, diff --git a/protocol/src/didcomm/encrypted.rs b/protocol/src/didcomm/encrypted.rs index e17a3022..38c1a42a 100644 --- a/protocol/src/didcomm/encrypted.rs +++ b/protocol/src/didcomm/encrypted.rs @@ -320,7 +320,7 @@ mod tests { verifiable_credentials::types::VerifiableCredentials, }; - #[actix_rt::test] + #[tokio::test] async fn test_generate_and_verify() { let from_did = create_random_did(); let to_did = create_random_did(); @@ -353,7 +353,7 @@ mod tests { use super::*; use crate::did::did_repository::mocks::NoPublicKeyDidRepository; - #[actix_rt::test] + #[tokio::test] async fn test_did_not_found() { let from_did = create_random_did(); let to_did = create_random_did(); @@ -381,7 +381,7 @@ mod tests { } } - #[actix_rt::test] + #[tokio::test] async fn test_did_public_key_not_found() { let from_did = create_random_did(); let to_did = create_random_did(); @@ -436,7 +436,7 @@ mod tests { .unwrap() } - #[actix_rt::test] + #[tokio::test] async fn test_did_not_found() { let from_did = create_random_did(); let to_did = create_random_did(); @@ -471,7 +471,7 @@ mod tests { } } - #[actix_rt::test] + #[tokio::test] async fn test_cannot_steal_message() { let from_did = create_random_did(); let to_did = create_random_did(); @@ -509,7 +509,7 @@ mod tests { } } - #[actix_rt::test] + #[tokio::test] async fn test_did_public_key_not_found() { let from_did = create_random_did(); let to_did = create_random_did(); diff --git a/protocol/src/verifiable_credentials/did_vc.rs b/protocol/src/verifiable_credentials/did_vc.rs index bbb10c06..e83c1fb2 100644 --- a/protocol/src/verifiable_credentials/did_vc.rs +++ b/protocol/src/verifiable_credentials/did_vc.rs @@ -90,7 +90,7 @@ mod tests { keyring::keypair::KeyPairing, }; - #[actix_rt::test] + #[tokio::test] async fn test_generate_and_verify() { let from_did = create_random_did(); @@ -135,7 +135,7 @@ mod tests { service.generate(model, from_keyring).unwrap() } - #[actix_rt::test] + #[tokio::test] async fn test_did_not_found() { let from_did = create_random_did(); @@ -158,7 +158,7 @@ mod tests { } } - #[actix_rt::test] + #[tokio::test] async fn test_public_key_not_found() { let from_did = create_random_did(); @@ -180,7 +180,7 @@ mod tests { } } - #[actix_rt::test] + #[tokio::test] async fn test_verify_failed() { let from_did = create_random_did(); @@ -207,7 +207,7 @@ mod tests { } } - #[actix_rt::test] + #[tokio::test] async fn test_public_key_length_mismatch() { let from_did = create_random_did(); @@ -229,7 +229,7 @@ mod tests { } } - #[actix_rt::test] + #[tokio::test] async fn test_signature_not_verified() { let from_did = create_random_did(); diff --git a/test_resource/compose.yaml b/test_resource/compose.yaml index 035a794b..fbd80a23 100644 --- a/test_resource/compose.yaml +++ b/test_resource/compose.yaml @@ -59,7 +59,7 @@ services: - ./config:/root/.config/nodex/ - agent_socket:/root/.nodex/ - ./nodex-agent:/tmp/nodex-agent - command: /tmp/nodex-agent + command: /tmp/nodex-agent controller environment: NODEX_DID_HTTP_ENDPOINT: http://sidetree_prism:4010 NODEX_DID_ATTACHMENT_LINK: http://sidetree_prism:4010 diff --git a/test_resource/test_update.sh b/test_resource/test_update.sh new file mode 100755 index 00000000..8b7b742e --- /dev/null +++ b/test_resource/test_update.sh @@ -0,0 +1,34 @@ +#!/bin/bash +# clean up +cd "$(dirname "$0")" + +pkill -9 nodex-agent +rm /dev/shm/nodex_runtime_info +rm ~/.nodex/run/nodex.sock +rm ~/.nodex/run/meta_nodex.sock + +mkdir -p /tmp/nodex-deploy/ +pushd /tmp/nodex-deploy/ +python -m http.server 9000 & +server_pid=$! +popd + +sed -i 's/^version.*=.*".*\..*\..*"/version = "3.4.1"/' ../Cargo.toml +cargo build --release +pushd ../target/release/ +zip -r /tmp/nodex-deploy/nodex-agent.zip nodex-agent +popd +sed -i 's/^version.*=.*".*\..*\..*"/version = "3.4.0"/' ../Cargo.toml +cargo build --release +pushd ../target/release/ +./nodex-agent controller & +popd + +sleep 1 +bash -c "while true; do curl -H 'Content-Type:application/json' --unix-socket ~/.nodex/run/nodex.sock localhost/internal/version/get; done" & +get_version_pid=$! +sleep 1 +curl -v -X POST -H 'Content-Type:application/json' -d '{"message":{"binary_url":"http://localhost:9000/nodex-agent.zip"}}' --unix-socket ~/.nodex/run/nodex.sock http://localhost/internal/version/update +sleep 5 +kill $server_pid +kill $get_version_pid