From 218452706cc6f8dd63c3a86f2331a3613e904f7a Mon Sep 17 00:00:00 2001 From: hibna Date: Sat, 21 Feb 2026 15:50:35 +0300 Subject: [PATCH] chore: initial commit for phase04 --- apps/daemon/Cargo.lock | 2849 ++++++++++++++++++++++ apps/daemon/Cargo.toml | 8 + apps/daemon/src/auth.rs | 15 + apps/daemon/src/docker/container.rs | 263 ++ apps/daemon/src/docker/manager.rs | 77 + apps/daemon/src/docker/mod.rs | 4 + apps/daemon/src/error.rs | 52 + apps/daemon/src/filesystem/mod.rs | 3 + apps/daemon/src/filesystem/operations.rs | 129 + apps/daemon/src/grpc/mod.rs | 3 + apps/daemon/src/grpc/service.rs | 511 ++++ apps/daemon/src/main.rs | 100 +- apps/daemon/src/server/manager.rs | 230 ++ apps/daemon/src/server/mod.rs | 5 + apps/daemon/src/server/state.rs | 69 + 15 files changed, 4310 insertions(+), 8 deletions(-) create mode 100644 apps/daemon/Cargo.lock create mode 100644 apps/daemon/src/auth.rs create mode 100644 apps/daemon/src/docker/container.rs create mode 100644 apps/daemon/src/docker/manager.rs create mode 100644 apps/daemon/src/docker/mod.rs create mode 100644 apps/daemon/src/error.rs create mode 100644 apps/daemon/src/filesystem/mod.rs create mode 100644 apps/daemon/src/filesystem/operations.rs create mode 100644 apps/daemon/src/grpc/mod.rs create mode 100644 apps/daemon/src/grpc/service.rs create mode 100644 apps/daemon/src/server/manager.rs create mode 100644 apps/daemon/src/server/mod.rs create mode 100644 apps/daemon/src/server/state.rs diff --git a/apps/daemon/Cargo.lock b/apps/daemon/Cargo.lock new file mode 100644 index 0000000..6dfb803 --- /dev/null +++ b/apps/daemon/Cargo.lock @@ -0,0 +1,2849 @@ +# This file is automatically @generated by Cargo. +# It is not intended for manual editing. +version = 4 + +[[package]] +name = "adler2" +version = "2.0.1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "320119579fcad9c21884f5c4861d16174d0e06250625266f50fe6898340abefa" + +[[package]] +name = "aho-corasick" +version = "1.1.4" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "ddd31a130427c27518df266943a5308ed92d4b226cc639f5a8f1002816174301" +dependencies = [ + "memchr", +] + +[[package]] +name = "android_system_properties" +version = "0.1.5" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "819e7219dbd41043ac279b19830f2efc897156490d7fd6ea916720117ee66311" +dependencies = [ + "libc", +] + +[[package]] +name = "anyhow" +version = "1.0.102" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "7f202df86484c868dbad7eaa557ef785d5c66295e41b460ef922eca0723b842c" + +[[package]] +name = "async-stream" +version = "0.3.6" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "0b5a71a6f37880a80d1d7f19efd781e4b5de42c88f0722cc13bcb6cc2cfe8476" +dependencies = [ + "async-stream-impl", + "futures-core", + "pin-project-lite", +] + +[[package]] +name = "async-stream-impl" +version = "0.3.6" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "c7c24de15d275a1ecfd47a380fb4d5ec9bfe0933f309ed5e705b775596a3574d" +dependencies = [ + "proc-macro2", + "quote", + "syn", +] + +[[package]] +name = "async-trait" +version = "0.1.89" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "9035ad2d096bed7955a320ee7e2230574d28fd3c3a0f186cbea1ff3c7eed5dbb" +dependencies = [ + "proc-macro2", + "quote", + "syn", +] + +[[package]] +name = "atomic-waker" +version = "1.1.2" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "1505bd5d3d116872e7271a6d4e16d81d0c8570876c8de68093a09ac269d8aac0" + +[[package]] +name = "autocfg" +version = "1.5.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "c08606f8c3cbf4ce6ec8e28fb0014a2c086708fe954eaa885384a6165172e7e8" + +[[package]] +name = "axum" +version = "0.7.9" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "edca88bc138befd0323b20752846e6587272d3b03b0343c8ea28a6f819e6e71f" +dependencies = [ + "async-trait", + "axum-core", + "bytes", + "futures-util", + "http", + "http-body", + "http-body-util", + "itoa", + "matchit", + "memchr", + "mime", + "percent-encoding", + "pin-project-lite", + "rustversion", + "serde", + "sync_wrapper", + "tower 0.5.3", + "tower-layer", + "tower-service", +] + +[[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", + "http-body", + "http-body-util", + "mime", + "pin-project-lite", + "rustversion", + "sync_wrapper", + "tower-layer", + "tower-service", +] + +[[package]] +name = "base64" +version = "0.22.1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "72b3254f16251a8381aa12e40e3c4d2f0199f8c6508fbecb9d91f575e0fbb8c6" + +[[package]] +name = "bitflags" +version = "2.11.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "843867be96c8daad0d758b57df9392b6d8d271134fce549de6ce169ff98a92af" + +[[package]] +name = "bollard" +version = "0.18.1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "97ccca1260af6a459d75994ad5acc1651bcabcbdbc41467cc9786519ab854c30" +dependencies = [ + "base64", + "bollard-stubs", + "bytes", + "futures-core", + "futures-util", + "hex", + "http", + "http-body-util", + "hyper", + "hyper-named-pipe", + "hyper-util", + "hyperlocal", + "log", + "pin-project-lite", + "serde", + "serde_derive", + "serde_json", + "serde_repr", + "serde_urlencoded", + "thiserror", + "tokio", + "tokio-util", + "tower-service", + "url", + "winapi", +] + +[[package]] +name = "bollard-stubs" +version = "1.47.1-rc.27.3.1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "3f179cfbddb6e77a5472703d4b30436bff32929c0aa8a9008ecf23d1d3cdd0da" +dependencies = [ + "serde", + "serde_repr", + "serde_with", +] + +[[package]] +name = "bumpalo" +version = "3.20.2" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "5d20789868f4b01b2f2caec9f5c4e0213b41e3e5702a50157d699ae31ced2fcb" + +[[package]] +name = "bytes" +version = "1.11.1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "1e748733b7cbc798e1434b6ac524f0c1ff2ab456fe201501e6497c8417a4fc33" + +[[package]] +name = "cc" +version = "1.2.56" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "aebf35691d1bfb0ac386a69bac2fde4dd276fb618cf8bf4f5318fe285e821bb2" +dependencies = [ + "find-msvc-tools", + "shlex", +] + +[[package]] +name = "cfg-if" +version = "1.0.4" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "9330f8b2ff13f34540b44e946ef35111825727b38d33286ef986142615121801" + +[[package]] +name = "chrono" +version = "0.4.43" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "fac4744fb15ae8337dc853fee7fb3f4e48c0fbaa23d0afe49c447b4fab126118" +dependencies = [ + "iana-time-zone", + "num-traits", + "serde", + "windows-link", +] + +[[package]] +name = "core-foundation" +version = "0.9.4" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "91e195e091a93c46f7102ec7818a2aa394e1e1771c3ab4825963fa03e45afb8f" +dependencies = [ + "core-foundation-sys", + "libc", +] + +[[package]] +name = "core-foundation" +version = "0.10.1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "b2a6cd9ae233e7f62ba4e9353e81a88df7fc8a5987b8d445b4d90c879bd156f6" +dependencies = [ + "core-foundation-sys", + "libc", +] + +[[package]] +name = "core-foundation-sys" +version = "0.8.7" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "773648b94d0e5d620f64f280777445740e61fe701025087ec8b57f45c791888b" + +[[package]] +name = "crc32fast" +version = "1.5.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "9481c1c90cbf2ac953f07c8d4a58aa3945c425b7185c9154d67a65e4230da511" +dependencies = [ + "cfg-if", +] + +[[package]] +name = "deranged" +version = "0.5.7" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "2163a0e204a148662b6b6816d4b5d5668a5f2f8df498ccbd5cd0e864e78fecba" +dependencies = [ + "powerfmt", + "serde_core", +] + +[[package]] +name = "displaydoc" +version = "0.2.5" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "97369cbbc041bc366949bc74d34658d6cda5621039731c6310521892a3a20ae0" +dependencies = [ + "proc-macro2", + "quote", + "syn", +] + +[[package]] +name = "dyn-clone" +version = "1.0.20" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "d0881ea181b1df73ff77ffaaf9c7544ecc11e82fba9b5f27b262a3c73a332555" + +[[package]] +name = "either" +version = "1.15.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "48c757948c5ede0e46177b7add2e67155f70e33c07fea8284df6576da70b3719" + +[[package]] +name = "encoding_rs" +version = "0.8.35" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "75030f3c4f45dafd7586dd6780965a8c7e8e285a5ecb86713e63a79c5b2766f3" +dependencies = [ + "cfg-if", +] + +[[package]] +name = "equivalent" +version = "1.0.2" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "877a4ace8713b0bcf2a4e7eec82529c029f1d0619886d18145fea96c3ffe5c0f" + +[[package]] +name = "errno" +version = "0.3.14" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "39cab71617ae0d63f51a36d69f866391735b51691dbda63cf6f96d042b63efeb" +dependencies = [ + "libc", + "windows-sys 0.61.2", +] + +[[package]] +name = "fastrand" +version = "2.3.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "37909eebbb50d72f9059c3b6d82c0463f2ff062c9e95845c43a6c9c0355411be" + +[[package]] +name = "filetime" +version = "0.2.27" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "f98844151eee8917efc50bd9e8318cb963ae8b297431495d3f758616ea5c57db" +dependencies = [ + "cfg-if", + "libc", + "libredox", +] + +[[package]] +name = "find-msvc-tools" +version = "0.1.9" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "5baebc0774151f905a1a2cc41989300b1e6fbb29aff0ceffa1064fdd3088d582" + +[[package]] +name = "fixedbitset" +version = "0.5.7" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "1d674e81391d1e1ab681a28d99df07927c6d4aa5b027d7da16ba32d1d21ecd99" + +[[package]] +name = "flate2" +version = "1.1.9" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "843fba2746e448b37e26a819579957415c8cef339bf08564fe8b7ddbd959573c" +dependencies = [ + "crc32fast", + "miniz_oxide", +] + +[[package]] +name = "fnv" +version = "1.0.7" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "3f9eec918d3f24069decb9af1554cad7c880e2da24a9afd88aca000531ab82c1" + +[[package]] +name = "foldhash" +version = "0.1.5" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "d9c4f5dac5e15c24eb999c26181a6ca40b39fe946cbe4c263c7209467bc83af2" + +[[package]] +name = "foreign-types" +version = "0.3.2" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "f6f339eb8adc052cd2ca78910fda869aefa38d22d5cb648e6485e4d3fc06f3b1" +dependencies = [ + "foreign-types-shared", +] + +[[package]] +name = "foreign-types-shared" +version = "0.1.1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "00b0228411908ca8685dba7fc2cdd70ec9990a6e753e89b6ac91a84c40fbaf4b" + +[[package]] +name = "form_urlencoded" +version = "1.2.2" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "cb4cb245038516f5f85277875cdaa4f7d2c9a0fa0468de06ed190163b1581fcf" +dependencies = [ + "percent-encoding", +] + +[[package]] +name = "futures" +version = "0.3.32" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "8b147ee9d1f6d097cef9ce628cd2ee62288d963e16fb287bd9286455b241382d" +dependencies = [ + "futures-channel", + "futures-core", + "futures-executor", + "futures-io", + "futures-sink", + "futures-task", + "futures-util", +] + +[[package]] +name = "futures-channel" +version = "0.3.32" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "07bbe89c50d7a535e539b8c17bc0b49bdb77747034daa8087407d655f3f7cc1d" +dependencies = [ + "futures-core", + "futures-sink", +] + +[[package]] +name = "futures-core" +version = "0.3.32" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "7e3450815272ef58cec6d564423f6e755e25379b217b0bc688e295ba24df6b1d" + +[[package]] +name = "futures-executor" +version = "0.3.32" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "baf29c38818342a3b26b5b923639e7b1f4a61fc5e76102d4b1981c6dc7a7579d" +dependencies = [ + "futures-core", + "futures-task", + "futures-util", +] + +[[package]] +name = "futures-io" +version = "0.3.32" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "cecba35d7ad927e23624b22ad55235f2239cfa44fd10428eecbeba6d6a717718" + +[[package]] +name = "futures-macro" +version = "0.3.32" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "e835b70203e41293343137df5c0664546da5745f82ec9b84d40be8336958447b" +dependencies = [ + "proc-macro2", + "quote", + "syn", +] + +[[package]] +name = "futures-sink" +version = "0.3.32" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "c39754e157331b013978ec91992bde1ac089843443c49cbc7f46150b0fad0893" + +[[package]] +name = "futures-task" +version = "0.3.32" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "037711b3d59c33004d3856fbdc83b99d4ff37a24768fa1be9ce3538a1cde4393" + +[[package]] +name = "futures-util" +version = "0.3.32" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "389ca41296e6190b48053de0321d02a77f32f8a5d2461dd38762c0593805c6d6" +dependencies = [ + "futures-channel", + "futures-core", + "futures-io", + "futures-macro", + "futures-sink", + "futures-task", + "memchr", + "pin-project-lite", + "slab", +] + +[[package]] +name = "gamepanel-daemon" +version = "0.1.0" +dependencies = [ + "anyhow", + "bollard", + "flate2", + "futures", + "prost", + "prost-types", + "reqwest", + "serde", + "serde_json", + "serde_yaml", + "tar", + "thiserror", + "tokio", + "tokio-stream", + "tonic", + "tonic-build", + "tracing", + "tracing-subscriber", + "uuid", +] + +[[package]] +name = "getrandom" +version = "0.2.17" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "ff2abc00be7fca6ebc474524697ae276ad847ad0a6b3faa4bcb027e9a4614ad0" +dependencies = [ + "cfg-if", + "libc", + "wasi", +] + +[[package]] +name = "getrandom" +version = "0.4.1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "139ef39800118c7683f2fd3c98c1b23c09ae076556b435f8e9064ae108aaeeec" +dependencies = [ + "cfg-if", + "libc", + "r-efi", + "wasip2", + "wasip3", +] + +[[package]] +name = "h2" +version = "0.4.13" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "2f44da3a8150a6703ed5d34e164b875fd14c2cdab9af1252a9a1020bde2bdc54" +dependencies = [ + "atomic-waker", + "bytes", + "fnv", + "futures-core", + "futures-sink", + "http", + "indexmap 2.13.0", + "slab", + "tokio", + "tokio-util", + "tracing", +] + +[[package]] +name = "hashbrown" +version = "0.12.3" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "8a9ee70c43aaf417c914396645a0fa852624801b24ebb7ae78fe8272889ac888" + +[[package]] +name = "hashbrown" +version = "0.15.5" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "9229cfe53dfd69f0609a49f65461bd93001ea1ef889cd5529dd176593f5338a1" +dependencies = [ + "foldhash", +] + +[[package]] +name = "hashbrown" +version = "0.16.1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "841d1cc9bed7f9236f321df977030373f4a4163ae1a7dbfe1a51a2c1a51d9100" + +[[package]] +name = "heck" +version = "0.5.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "2304e00983f87ffb38b55b444b5e3b60a884b5d30c0fca7d82fe33449bbe55ea" + +[[package]] +name = "hex" +version = "0.4.3" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "7f24254aa9a54b5c858eaee2f5bccdb46aaf0e486a595ed5fd8f86ba55232a70" + +[[package]] +name = "http" +version = "1.4.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "e3ba2a386d7f85a81f119ad7498ebe444d2e22c2af0b86b069416ace48b3311a" +dependencies = [ + "bytes", + "itoa", +] + +[[package]] +name = "http-body" +version = "1.0.1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "1efedce1fb8e6913f23e0c92de8e62cd5b772a67e7b3946df930a62566c93184" +dependencies = [ + "bytes", + "http", +] + +[[package]] +name = "http-body-util" +version = "0.1.3" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "b021d93e26becf5dc7e1b75b1bed1fd93124b374ceb73f43d4d4eafec896a64a" +dependencies = [ + "bytes", + "futures-core", + "http", + "http-body", + "pin-project-lite", +] + +[[package]] +name = "httparse" +version = "1.10.1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "6dbf3de79e51f3d586ab4cb9d5c3e2c14aa28ed23d180cf89b4df0454a69cc87" + +[[package]] +name = "httpdate" +version = "1.0.3" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "df3b46402a9d5adb4c86a0cf463f42e19994e3ee891101b1841f30a545cb49a9" + +[[package]] +name = "hyper" +version = "1.8.1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "2ab2d4f250c3d7b1c9fcdff1cece94ea4e2dfbec68614f7b87cb205f24ca9d11" +dependencies = [ + "atomic-waker", + "bytes", + "futures-channel", + "futures-core", + "h2", + "http", + "http-body", + "httparse", + "httpdate", + "itoa", + "pin-project-lite", + "pin-utils", + "smallvec", + "tokio", + "want", +] + +[[package]] +name = "hyper-named-pipe" +version = "0.1.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "73b7d8abf35697b81a825e386fc151e0d503e8cb5fcb93cc8669c376dfd6f278" +dependencies = [ + "hex", + "hyper", + "hyper-util", + "pin-project-lite", + "tokio", + "tower-service", + "winapi", +] + +[[package]] +name = "hyper-rustls" +version = "0.27.7" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "e3c93eb611681b207e1fe55d5a71ecf91572ec8a6705cdb6857f7d8d5242cf58" +dependencies = [ + "http", + "hyper", + "hyper-util", + "rustls", + "rustls-pki-types", + "tokio", + "tokio-rustls", + "tower-service", +] + +[[package]] +name = "hyper-timeout" +version = "0.5.2" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "2b90d566bffbce6a75bd8b09a05aa8c2cb1fabb6cb348f8840c9e4c90a0d83b0" +dependencies = [ + "hyper", + "hyper-util", + "pin-project-lite", + "tokio", + "tower-service", +] + +[[package]] +name = "hyper-tls" +version = "0.6.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "70206fc6890eaca9fde8a0bf71caa2ddfc9fe045ac9e5c70df101a7dbde866e0" +dependencies = [ + "bytes", + "http-body-util", + "hyper", + "hyper-util", + "native-tls", + "tokio", + "tokio-native-tls", + "tower-service", +] + +[[package]] +name = "hyper-util" +version = "0.1.20" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "96547c2556ec9d12fb1578c4eaf448b04993e7fb79cbaad930a656880a6bdfa0" +dependencies = [ + "base64", + "bytes", + "futures-channel", + "futures-util", + "http", + "http-body", + "hyper", + "ipnet", + "libc", + "percent-encoding", + "pin-project-lite", + "socket2 0.6.2", + "system-configuration", + "tokio", + "tower-service", + "tracing", + "windows-registry", +] + +[[package]] +name = "hyperlocal" +version = "0.9.1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "986c5ce3b994526b3cd75578e62554abd09f0899d6206de48b3e96ab34ccc8c7" +dependencies = [ + "hex", + "http-body-util", + "hyper", + "hyper-util", + "pin-project-lite", + "tokio", + "tower-service", +] + +[[package]] +name = "iana-time-zone" +version = "0.1.65" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "e31bc9ad994ba00e440a8aa5c9ef0ec67d5cb5e5cb0cc7f8b744a35b389cc470" +dependencies = [ + "android_system_properties", + "core-foundation-sys", + "iana-time-zone-haiku", + "js-sys", + "log", + "wasm-bindgen", + "windows-core", +] + +[[package]] +name = "iana-time-zone-haiku" +version = "0.1.2" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "f31827a206f56af32e590ba56d5d2d085f558508192593743f16b2306495269f" +dependencies = [ + "cc", +] + +[[package]] +name = "icu_collections" +version = "2.1.1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "4c6b649701667bbe825c3b7e6388cb521c23d88644678e83c0c4d0a621a34b43" +dependencies = [ + "displaydoc", + "potential_utf", + "yoke", + "zerofrom", + "zerovec", +] + +[[package]] +name = "icu_locale_core" +version = "2.1.1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "edba7861004dd3714265b4db54a3c390e880ab658fec5f7db895fae2046b5bb6" +dependencies = [ + "displaydoc", + "litemap", + "tinystr", + "writeable", + "zerovec", +] + +[[package]] +name = "icu_normalizer" +version = "2.1.1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "5f6c8828b67bf8908d82127b2054ea1b4427ff0230ee9141c54251934ab1b599" +dependencies = [ + "icu_collections", + "icu_normalizer_data", + "icu_properties", + "icu_provider", + "smallvec", + "zerovec", +] + +[[package]] +name = "icu_normalizer_data" +version = "2.1.1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "7aedcccd01fc5fe81e6b489c15b247b8b0690feb23304303a9e560f37efc560a" + +[[package]] +name = "icu_properties" +version = "2.1.2" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "020bfc02fe870ec3a66d93e677ccca0562506e5872c650f893269e08615d74ec" +dependencies = [ + "icu_collections", + "icu_locale_core", + "icu_properties_data", + "icu_provider", + "zerotrie", + "zerovec", +] + +[[package]] +name = "icu_properties_data" +version = "2.1.2" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "616c294cf8d725c6afcd8f55abc17c56464ef6211f9ed59cccffe534129c77af" + +[[package]] +name = "icu_provider" +version = "2.1.1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "85962cf0ce02e1e0a629cc34e7ca3e373ce20dda4c4d7294bbd0bf1fdb59e614" +dependencies = [ + "displaydoc", + "icu_locale_core", + "writeable", + "yoke", + "zerofrom", + "zerotrie", + "zerovec", +] + +[[package]] +name = "id-arena" +version = "2.3.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "3d3067d79b975e8844ca9eb072e16b31c3c1c36928edf9c6789548c524d0d954" + +[[package]] +name = "idna" +version = "1.1.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "3b0875f23caa03898994f6ddc501886a45c7d3d62d04d2d90788d47be1b1e4de" +dependencies = [ + "idna_adapter", + "smallvec", + "utf8_iter", +] + +[[package]] +name = "idna_adapter" +version = "1.2.1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "3acae9609540aa318d1bc588455225fb2085b9ed0c4f6bd0d9d5bcd86f1a0344" +dependencies = [ + "icu_normalizer", + "icu_properties", +] + +[[package]] +name = "indexmap" +version = "1.9.3" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "bd070e393353796e801d209ad339e89596eb4c8d430d18ede6a1cced8fafbd99" +dependencies = [ + "autocfg", + "hashbrown 0.12.3", + "serde", +] + +[[package]] +name = "indexmap" +version = "2.13.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "7714e70437a7dc3ac8eb7e6f8df75fd8eb422675fc7678aff7364301092b1017" +dependencies = [ + "equivalent", + "hashbrown 0.16.1", + "serde", + "serde_core", +] + +[[package]] +name = "ipnet" +version = "2.11.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "469fb0b9cefa57e3ef31275ee7cacb78f2fdca44e4765491884a2b119d4eb130" + +[[package]] +name = "iri-string" +version = "0.7.10" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "c91338f0783edbd6195decb37bae672fd3b165faffb89bf7b9e6942f8b1a731a" +dependencies = [ + "memchr", + "serde", +] + +[[package]] +name = "itertools" +version = "0.14.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "2b192c782037fadd9cfa75548310488aabdbf3d2da73885b31bd0abd03351285" +dependencies = [ + "either", +] + +[[package]] +name = "itoa" +version = "1.0.17" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "92ecc6618181def0457392ccd0ee51198e065e016d1d527a7ac1b6dc7c1f09d2" + +[[package]] +name = "js-sys" +version = "0.3.87" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "93f0862381daaec758576dcc22eb7bbf4d7efd67328553f3b45a412a51a3fb21" +dependencies = [ + "once_cell", + "wasm-bindgen", +] + +[[package]] +name = "lazy_static" +version = "1.5.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "bbd2bcb4c963f2ddae06a2efc7e9f3591312473c50c6685e1f298068316e66fe" + +[[package]] +name = "leb128fmt" +version = "0.1.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "09edd9e8b54e49e587e4f6295a7d29c3ea94d469cb40ab8ca70b288248a81db2" + +[[package]] +name = "libc" +version = "0.2.182" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "6800badb6cb2082ffd7b6a67e6125bb39f18782f793520caee8cb8846be06112" + +[[package]] +name = "libredox" +version = "0.1.12" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "3d0b95e02c851351f877147b7deea7b1afb1df71b63aa5f8270716e0c5720616" +dependencies = [ + "bitflags", + "libc", + "redox_syscall 0.7.1", +] + +[[package]] +name = "linux-raw-sys" +version = "0.11.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "df1d3c3b53da64cf5760482273a98e575c651a67eec7f77df96b5b642de8f039" + +[[package]] +name = "litemap" +version = "0.8.1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "6373607a59f0be73a39b6fe456b8192fcc3585f602af20751600e974dd455e77" + +[[package]] +name = "lock_api" +version = "0.4.14" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "224399e74b87b5f3557511d98dff8b14089b3dadafcab6bb93eab67d3aace965" +dependencies = [ + "scopeguard", +] + +[[package]] +name = "log" +version = "0.4.29" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "5e5032e24019045c762d3c0f28f5b6b8bbf38563a65908389bf7978758920897" + +[[package]] +name = "matchers" +version = "0.2.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "d1525a2a28c7f4fa0fc98bb91ae755d1e2d1505079e05539e35bc876b5d65ae9" +dependencies = [ + "regex-automata", +] + +[[package]] +name = "matchit" +version = "0.7.3" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "0e7465ac9959cc2b1404e8e2367b43684a6d13790fe23056cc8c6c5a6b7bcb94" + +[[package]] +name = "memchr" +version = "2.8.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "f8ca58f447f06ed17d5fc4043ce1b10dd205e060fb3ce5b979b8ed8e59ff3f79" + +[[package]] +name = "mime" +version = "0.3.17" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "6877bb514081ee2a7ff5ef9de3281f14a4dd4bceac4c09388074a6b5df8a139a" + +[[package]] +name = "miniz_oxide" +version = "0.8.9" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "1fa76a2c86f704bdb222d66965fb3d63269ce38518b83cb0575fca855ebb6316" +dependencies = [ + "adler2", + "simd-adler32", +] + +[[package]] +name = "mio" +version = "1.1.1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "a69bcab0ad47271a0234d9422b131806bf3968021e5dc9328caf2d4cd58557fc" +dependencies = [ + "libc", + "wasi", + "windows-sys 0.61.2", +] + +[[package]] +name = "multimap" +version = "0.10.1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "1d87ecb2933e8aeadb3e3a02b828fed80a7528047e68b4f424523a0981a3a084" + +[[package]] +name = "native-tls" +version = "0.2.18" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "465500e14ea162429d264d44189adc38b199b62b1c21eea9f69e4b73cb03bbf2" +dependencies = [ + "libc", + "log", + "openssl", + "openssl-probe", + "openssl-sys", + "schannel", + "security-framework", + "security-framework-sys", + "tempfile", +] + +[[package]] +name = "nu-ansi-term" +version = "0.50.3" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "7957b9740744892f114936ab4a57b3f487491bbeafaf8083688b16841a4240e5" +dependencies = [ + "windows-sys 0.61.2", +] + +[[package]] +name = "num-conv" +version = "0.2.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "cf97ec579c3c42f953ef76dbf8d55ac91fb219dde70e49aa4a6b7d74e9919050" + +[[package]] +name = "num-traits" +version = "0.2.19" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "071dfc062690e90b734c0b2273ce72ad0ffa95f0c74596bc250dcfd960262841" +dependencies = [ + "autocfg", +] + +[[package]] +name = "once_cell" +version = "1.21.3" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "42f5e15c9953c5e4ccceeb2e7382a716482c34515315f7b03532b8b4e8393d2d" + +[[package]] +name = "openssl" +version = "0.10.75" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "08838db121398ad17ab8531ce9de97b244589089e290a384c900cb9ff7434328" +dependencies = [ + "bitflags", + "cfg-if", + "foreign-types", + "libc", + "once_cell", + "openssl-macros", + "openssl-sys", +] + +[[package]] +name = "openssl-macros" +version = "0.1.1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "a948666b637a0f465e8564c73e89d4dde00d72d4d473cc972f390fc3dcee7d9c" +dependencies = [ + "proc-macro2", + "quote", + "syn", +] + +[[package]] +name = "openssl-probe" +version = "0.2.1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "7c87def4c32ab89d880effc9e097653c8da5d6ef28e6b539d313baaacfbafcbe" + +[[package]] +name = "openssl-sys" +version = "0.9.111" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "82cab2d520aa75e3c58898289429321eb788c3106963d0dc886ec7a5f4adc321" +dependencies = [ + "cc", + "libc", + "pkg-config", + "vcpkg", +] + +[[package]] +name = "parking_lot" +version = "0.12.5" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "93857453250e3077bd71ff98b6a65ea6621a19bb0f559a85248955ac12c45a1a" +dependencies = [ + "lock_api", + "parking_lot_core", +] + +[[package]] +name = "parking_lot_core" +version = "0.9.12" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "2621685985a2ebf1c516881c026032ac7deafcda1a2c9b7850dc81e3dfcb64c1" +dependencies = [ + "cfg-if", + "libc", + "redox_syscall 0.5.18", + "smallvec", + "windows-link", +] + +[[package]] +name = "percent-encoding" +version = "2.3.2" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "9b4f627cb1b25917193a259e49bdad08f671f8d9708acfd5fe0a8c1455d87220" + +[[package]] +name = "petgraph" +version = "0.7.1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "3672b37090dbd86368a4145bc067582552b29c27377cad4e0a306c97f9bd7772" +dependencies = [ + "fixedbitset", + "indexmap 2.13.0", +] + +[[package]] +name = "pin-project" +version = "1.1.10" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "677f1add503faace112b9f1373e43e9e054bfdd22ff1a63c1bc485eaec6a6a8a" +dependencies = [ + "pin-project-internal", +] + +[[package]] +name = "pin-project-internal" +version = "1.1.10" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "6e918e4ff8c4549eb882f14b3a4bc8c8bc93de829416eacf579f1207a8fbf861" +dependencies = [ + "proc-macro2", + "quote", + "syn", +] + +[[package]] +name = "pin-project-lite" +version = "0.2.16" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "3b3cff922bd51709b605d9ead9aa71031d81447142d828eb4a6eba76fe619f9b" + +[[package]] +name = "pin-utils" +version = "0.1.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "8b870d8c151b6f2fb93e84a13146138f05d02ed11c7e7c54f8826aaaf7c9f184" + +[[package]] +name = "pkg-config" +version = "0.3.32" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "7edddbd0b52d732b21ad9a5fab5c704c14cd949e5e9a1ec5929a24fded1b904c" + +[[package]] +name = "potential_utf" +version = "0.1.4" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "b73949432f5e2a09657003c25bca5e19a0e9c84f8058ca374f49e0ebe605af77" +dependencies = [ + "zerovec", +] + +[[package]] +name = "powerfmt" +version = "0.2.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "439ee305def115ba05938db6eb1644ff94165c5ab5e9420d1c1bcedbba909391" + +[[package]] +name = "ppv-lite86" +version = "0.2.21" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "85eae3c4ed2f50dcfe72643da4befc30deadb458a9b590d720cde2f2b1e97da9" +dependencies = [ + "zerocopy", +] + +[[package]] +name = "prettyplease" +version = "0.2.37" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "479ca8adacdd7ce8f1fb39ce9ecccbfe93a3f1344b3d0d97f20bc0196208f62b" +dependencies = [ + "proc-macro2", + "syn", +] + +[[package]] +name = "proc-macro2" +version = "1.0.106" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "8fd00f0bb2e90d81d1044c2b32617f68fcb9fa3bb7640c23e9c748e53fb30934" +dependencies = [ + "unicode-ident", +] + +[[package]] +name = "prost" +version = "0.13.5" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "2796faa41db3ec313a31f7624d9286acf277b52de526150b7e69f3debf891ee5" +dependencies = [ + "bytes", + "prost-derive", +] + +[[package]] +name = "prost-build" +version = "0.13.5" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "be769465445e8c1474e9c5dac2018218498557af32d9ed057325ec9a41ae81bf" +dependencies = [ + "heck", + "itertools", + "log", + "multimap", + "once_cell", + "petgraph", + "prettyplease", + "prost", + "prost-types", + "regex", + "syn", + "tempfile", +] + +[[package]] +name = "prost-derive" +version = "0.13.5" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "8a56d757972c98b346a9b766e3f02746cde6dd1cd1d1d563472929fdd74bec4d" +dependencies = [ + "anyhow", + "itertools", + "proc-macro2", + "quote", + "syn", +] + +[[package]] +name = "prost-types" +version = "0.13.5" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "52c2c1bf36ddb1a1c396b3601a3cec27c2462e45f07c386894ec3ccf5332bd16" +dependencies = [ + "prost", +] + +[[package]] +name = "quote" +version = "1.0.44" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "21b2ebcf727b7760c461f091f9f0f539b77b8e87f2fd88131e7f1b433b3cece4" +dependencies = [ + "proc-macro2", +] + +[[package]] +name = "r-efi" +version = "5.3.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "69cdb34c158ceb288df11e18b4bd39de994f6657d83847bdffdbd7f346754b0f" + +[[package]] +name = "rand" +version = "0.8.5" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "34af8d1a0e25924bc5b7c43c079c942339d8f0a8b57c39049bef581b46327404" +dependencies = [ + "libc", + "rand_chacha", + "rand_core", +] + +[[package]] +name = "rand_chacha" +version = "0.3.1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "e6c10a63a0fa32252be49d21e7709d4d4baf8d231c2dbce1eaa8141b9b127d88" +dependencies = [ + "ppv-lite86", + "rand_core", +] + +[[package]] +name = "rand_core" +version = "0.6.4" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "ec0be4795e2f6a28069bec0b5ff3e2ac9bafc99e6a9a7dc3547996c5c816922c" +dependencies = [ + "getrandom 0.2.17", +] + +[[package]] +name = "redox_syscall" +version = "0.5.18" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "ed2bf2547551a7053d6fdfafda3f938979645c44812fbfcda098faae3f1a362d" +dependencies = [ + "bitflags", +] + +[[package]] +name = "redox_syscall" +version = "0.7.1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "35985aa610addc02e24fc232012c86fd11f14111180f902b67e2d5331f8ebf2b" +dependencies = [ + "bitflags", +] + +[[package]] +name = "ref-cast" +version = "1.0.25" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "f354300ae66f76f1c85c5f84693f0ce81d747e2c3f21a45fef496d89c960bf7d" +dependencies = [ + "ref-cast-impl", +] + +[[package]] +name = "ref-cast-impl" +version = "1.0.25" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "b7186006dcb21920990093f30e3dea63b7d6e977bf1256be20c3563a5db070da" +dependencies = [ + "proc-macro2", + "quote", + "syn", +] + +[[package]] +name = "regex" +version = "1.12.3" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "e10754a14b9137dd7b1e3e5b0493cc9171fdd105e0ab477f51b72e7f3ac0e276" +dependencies = [ + "aho-corasick", + "memchr", + "regex-automata", + "regex-syntax", +] + +[[package]] +name = "regex-automata" +version = "0.4.14" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "6e1dd4122fc1595e8162618945476892eefca7b88c52820e74af6262213cae8f" +dependencies = [ + "aho-corasick", + "memchr", + "regex-syntax", +] + +[[package]] +name = "regex-syntax" +version = "0.8.9" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "a96887878f22d7bad8a3b6dc5b7440e0ada9a245242924394987b21cf2210a4c" + +[[package]] +name = "reqwest" +version = "0.12.28" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "eddd3ca559203180a307f12d114c268abf583f59b03cb906fd0b3ff8646c1147" +dependencies = [ + "base64", + "bytes", + "encoding_rs", + "futures-core", + "h2", + "http", + "http-body", + "http-body-util", + "hyper", + "hyper-rustls", + "hyper-tls", + "hyper-util", + "js-sys", + "log", + "mime", + "native-tls", + "percent-encoding", + "pin-project-lite", + "rustls-pki-types", + "serde", + "serde_json", + "serde_urlencoded", + "sync_wrapper", + "tokio", + "tokio-native-tls", + "tower 0.5.3", + "tower-http", + "tower-service", + "url", + "wasm-bindgen", + "wasm-bindgen-futures", + "web-sys", +] + +[[package]] +name = "ring" +version = "0.17.14" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "a4689e6c2294d81e88dc6261c768b63bc4fcdb852be6d1352498b114f61383b7" +dependencies = [ + "cc", + "cfg-if", + "getrandom 0.2.17", + "libc", + "untrusted", + "windows-sys 0.52.0", +] + +[[package]] +name = "rustix" +version = "1.1.3" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "146c9e247ccc180c1f61615433868c99f3de3ae256a30a43b49f67c2d9171f34" +dependencies = [ + "bitflags", + "errno", + "libc", + "linux-raw-sys", + "windows-sys 0.61.2", +] + +[[package]] +name = "rustls" +version = "0.23.36" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "c665f33d38cea657d9614f766881e4d510e0eda4239891eea56b4cadcf01801b" +dependencies = [ + "once_cell", + "rustls-pki-types", + "rustls-webpki", + "subtle", + "zeroize", +] + +[[package]] +name = "rustls-pki-types" +version = "1.14.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "be040f8b0a225e40375822a563fa9524378b9d63112f53e19ffff34df5d33fdd" +dependencies = [ + "zeroize", +] + +[[package]] +name = "rustls-webpki" +version = "0.103.9" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "d7df23109aa6c1567d1c575b9952556388da57401e4ace1d15f79eedad0d8f53" +dependencies = [ + "ring", + "rustls-pki-types", + "untrusted", +] + +[[package]] +name = "rustversion" +version = "1.0.22" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "b39cdef0fa800fc44525c84ccb54a029961a8215f9619753635a9c0d2538d46d" + +[[package]] +name = "ryu" +version = "1.0.23" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "9774ba4a74de5f7b1c1451ed6cd5285a32eddb5cccb8cc655a4e50009e06477f" + +[[package]] +name = "schannel" +version = "0.1.28" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "891d81b926048e76efe18581bf793546b4c0eaf8448d72be8de2bbee5fd166e1" +dependencies = [ + "windows-sys 0.61.2", +] + +[[package]] +name = "schemars" +version = "0.9.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "4cd191f9397d57d581cddd31014772520aa448f65ef991055d7f61582c65165f" +dependencies = [ + "dyn-clone", + "ref-cast", + "serde", + "serde_json", +] + +[[package]] +name = "schemars" +version = "1.2.1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "a2b42f36aa1cd011945615b92222f6bf73c599a102a300334cd7f8dbeec726cc" +dependencies = [ + "dyn-clone", + "ref-cast", + "serde", + "serde_json", +] + +[[package]] +name = "scopeguard" +version = "1.2.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "94143f37725109f92c262ed2cf5e59bce7498c01bcc1502d7b9afe439a4e9f49" + +[[package]] +name = "security-framework" +version = "3.7.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "b7f4bc775c73d9a02cde8bf7b2ec4c9d12743edf609006c7facc23998404cd1d" +dependencies = [ + "bitflags", + "core-foundation 0.10.1", + "core-foundation-sys", + "libc", + "security-framework-sys", +] + +[[package]] +name = "security-framework-sys" +version = "2.17.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "6ce2691df843ecc5d231c0b14ece2acc3efb62c0a398c7e1d875f3983ce020e3" +dependencies = [ + "core-foundation-sys", + "libc", +] + +[[package]] +name = "semver" +version = "1.0.27" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "d767eb0aabc880b29956c35734170f26ed551a859dbd361d140cdbeca61ab1e2" + +[[package]] +name = "serde" +version = "1.0.228" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "9a8e94ea7f378bd32cbbd37198a4a91436180c5bb472411e48b5ec2e2124ae9e" +dependencies = [ + "serde_core", + "serde_derive", +] + +[[package]] +name = "serde_core" +version = "1.0.228" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "41d385c7d4ca58e59fc732af25c3983b67ac852c1a25000afe1175de458b67ad" +dependencies = [ + "serde_derive", +] + +[[package]] +name = "serde_derive" +version = "1.0.228" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "d540f220d3187173da220f885ab66608367b6574e925011a9353e4badda91d79" +dependencies = [ + "proc-macro2", + "quote", + "syn", +] + +[[package]] +name = "serde_json" +version = "1.0.149" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "83fc039473c5595ace860d8c4fafa220ff474b3fc6bfdb4293327f1a37e94d86" +dependencies = [ + "itoa", + "memchr", + "serde", + "serde_core", + "zmij", +] + +[[package]] +name = "serde_repr" +version = "0.1.20" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "175ee3e80ae9982737ca543e96133087cbd9a485eecc3bc4de9c1a37b47ea59c" +dependencies = [ + "proc-macro2", + "quote", + "syn", +] + +[[package]] +name = "serde_urlencoded" +version = "0.7.1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "d3491c14715ca2294c4d6a88f15e84739788c1d030eed8c110436aafdaa2f3fd" +dependencies = [ + "form_urlencoded", + "itoa", + "ryu", + "serde", +] + +[[package]] +name = "serde_with" +version = "3.16.1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "4fa237f2807440d238e0364a218270b98f767a00d3dada77b1c53ae88940e2e7" +dependencies = [ + "base64", + "chrono", + "hex", + "indexmap 1.9.3", + "indexmap 2.13.0", + "schemars 0.9.0", + "schemars 1.2.1", + "serde_core", + "serde_json", + "time", +] + +[[package]] +name = "serde_yaml" +version = "0.9.34+deprecated" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "6a8b1a1a2ebf674015cc02edccce75287f1a0130d394307b36743c2f5d504b47" +dependencies = [ + "indexmap 2.13.0", + "itoa", + "ryu", + "serde", + "unsafe-libyaml", +] + +[[package]] +name = "sharded-slab" +version = "0.1.7" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "f40ca3c46823713e0d4209592e8d6e826aa57e928f09752619fc696c499637f6" +dependencies = [ + "lazy_static", +] + +[[package]] +name = "shlex" +version = "1.3.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "0fda2ff0d084019ba4d7c6f371c95d8fd75ce3524c3cb8fb653a3023f6323e64" + +[[package]] +name = "signal-hook-registry" +version = "1.4.8" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "c4db69cba1110affc0e9f7bcd48bbf87b3f4fc7c61fc9155afd4c469eb3d6c1b" +dependencies = [ + "errno", + "libc", +] + +[[package]] +name = "simd-adler32" +version = "0.3.8" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "e320a6c5ad31d271ad523dcf3ad13e2767ad8b1cb8f047f75a8aeaf8da139da2" + +[[package]] +name = "slab" +version = "0.4.12" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "0c790de23124f9ab44544d7ac05d60440adc586479ce501c1d6d7da3cd8c9cf5" + +[[package]] +name = "smallvec" +version = "1.15.1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "67b1b7a3b5fe4f1376887184045fcf45c69e92af734b7aaddc05fb777b6fbd03" + +[[package]] +name = "socket2" +version = "0.5.10" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "e22376abed350d73dd1cd119b57ffccad95b4e585a7cda43e286245ce23c0678" +dependencies = [ + "libc", + "windows-sys 0.52.0", +] + +[[package]] +name = "socket2" +version = "0.6.2" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "86f4aa3ad99f2088c990dfa82d367e19cb29268ed67c574d10d0a4bfe71f07e0" +dependencies = [ + "libc", + "windows-sys 0.60.2", +] + +[[package]] +name = "stable_deref_trait" +version = "1.2.1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "6ce2be8dc25455e1f91df71bfa12ad37d7af1092ae736f3a6cd0e37bc7810596" + +[[package]] +name = "subtle" +version = "2.6.1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "13c2bddecc57b384dee18652358fb23172facb8a2c51ccc10d74c157bdea3292" + +[[package]] +name = "syn" +version = "2.0.117" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "e665b8803e7b1d2a727f4023456bbbbe74da67099c585258af0ad9c5013b9b99" +dependencies = [ + "proc-macro2", + "quote", + "unicode-ident", +] + +[[package]] +name = "sync_wrapper" +version = "1.0.2" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "0bf256ce5efdfa370213c1dabab5935a12e49f2c58d15e9eac2870d3b4f27263" +dependencies = [ + "futures-core", +] + +[[package]] +name = "synstructure" +version = "0.13.2" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "728a70f3dbaf5bab7f0c4b1ac8d7ae5ea60a4b5549c8a5914361c99147a709d2" +dependencies = [ + "proc-macro2", + "quote", + "syn", +] + +[[package]] +name = "system-configuration" +version = "0.7.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "a13f3d0daba03132c0aa9767f98351b3488edc2c100cda2d2ec2b04f3d8d3c8b" +dependencies = [ + "bitflags", + "core-foundation 0.9.4", + "system-configuration-sys", +] + +[[package]] +name = "system-configuration-sys" +version = "0.6.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "8e1d1b10ced5ca923a1fcb8d03e96b8d3268065d724548c0211415ff6ac6bac4" +dependencies = [ + "core-foundation-sys", + "libc", +] + +[[package]] +name = "tar" +version = "0.4.44" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "1d863878d212c87a19c1a610eb53bb01fe12951c0501cf5a0d65f724914a667a" +dependencies = [ + "filetime", + "libc", + "xattr", +] + +[[package]] +name = "tempfile" +version = "3.25.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "0136791f7c95b1f6dd99f9cc786b91bb81c3800b639b3478e561ddb7be95e5f1" +dependencies = [ + "fastrand", + "getrandom 0.4.1", + "once_cell", + "rustix", + "windows-sys 0.61.2", +] + +[[package]] +name = "thiserror" +version = "2.0.18" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "4288b5bcbc7920c07a1149a35cf9590a2aa808e0bc1eafaade0b80947865fbc4" +dependencies = [ + "thiserror-impl", +] + +[[package]] +name = "thiserror-impl" +version = "2.0.18" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "ebc4ee7f67670e9b64d05fa4253e753e016c6c95ff35b89b7941d6b856dec1d5" +dependencies = [ + "proc-macro2", + "quote", + "syn", +] + +[[package]] +name = "thread_local" +version = "1.1.9" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "f60246a4944f24f6e018aa17cdeffb7818b76356965d03b07d6a9886e8962185" +dependencies = [ + "cfg-if", +] + +[[package]] +name = "time" +version = "0.3.47" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "743bd48c283afc0388f9b8827b976905fb217ad9e647fae3a379a9283c4def2c" +dependencies = [ + "deranged", + "itoa", + "num-conv", + "powerfmt", + "serde_core", + "time-core", + "time-macros", +] + +[[package]] +name = "time-core" +version = "0.1.8" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "7694e1cfe791f8d31026952abf09c69ca6f6fa4e1a1229e18988f06a04a12dca" + +[[package]] +name = "time-macros" +version = "0.2.27" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "2e70e4c5a0e0a8a4823ad65dfe1a6930e4f4d756dcd9dd7939022b5e8c501215" +dependencies = [ + "num-conv", + "time-core", +] + +[[package]] +name = "tinystr" +version = "0.8.2" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "42d3e9c45c09de15d06dd8acf5f4e0e399e85927b7f00711024eb7ae10fa4869" +dependencies = [ + "displaydoc", + "zerovec", +] + +[[package]] +name = "tokio" +version = "1.49.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "72a2903cd7736441aac9df9d7688bd0ce48edccaadf181c3b90be801e81d3d86" +dependencies = [ + "bytes", + "libc", + "mio", + "parking_lot", + "pin-project-lite", + "signal-hook-registry", + "socket2 0.6.2", + "tokio-macros", + "windows-sys 0.61.2", +] + +[[package]] +name = "tokio-macros" +version = "2.6.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "af407857209536a95c8e56f8231ef2c2e2aff839b22e07a1ffcbc617e9db9fa5" +dependencies = [ + "proc-macro2", + "quote", + "syn", +] + +[[package]] +name = "tokio-native-tls" +version = "0.3.1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "bbae76ab933c85776efabc971569dd6119c580d8f5d448769dec1764bf796ef2" +dependencies = [ + "native-tls", + "tokio", +] + +[[package]] +name = "tokio-rustls" +version = "0.26.4" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "1729aa945f29d91ba541258c8df89027d5792d85a8841fb65e8bf0f4ede4ef61" +dependencies = [ + "rustls", + "tokio", +] + +[[package]] +name = "tokio-stream" +version = "0.1.18" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "32da49809aab5c3bc678af03902d4ccddea2a87d028d86392a4b1560c6906c70" +dependencies = [ + "futures-core", + "pin-project-lite", + "tokio", + "tokio-util", +] + +[[package]] +name = "tokio-util" +version = "0.7.18" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "9ae9cec805b01e8fc3fd2fe289f89149a9b66dd16786abd8b19cfa7b48cb0098" +dependencies = [ + "bytes", + "futures-core", + "futures-sink", + "pin-project-lite", + "tokio", +] + +[[package]] +name = "tonic" +version = "0.12.3" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "877c5b330756d856ffcc4553ab34a5684481ade925ecc54bcd1bf02b1d0d4d52" +dependencies = [ + "async-stream", + "async-trait", + "axum", + "base64", + "bytes", + "h2", + "http", + "http-body", + "http-body-util", + "hyper", + "hyper-timeout", + "hyper-util", + "percent-encoding", + "pin-project", + "prost", + "socket2 0.5.10", + "tokio", + "tokio-stream", + "tower 0.4.13", + "tower-layer", + "tower-service", + "tracing", +] + +[[package]] +name = "tonic-build" +version = "0.12.3" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "9557ce109ea773b399c9b9e5dca39294110b74f1f342cb347a80d1fce8c26a11" +dependencies = [ + "prettyplease", + "proc-macro2", + "prost-build", + "prost-types", + "quote", + "syn", +] + +[[package]] +name = "tower" +version = "0.4.13" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "b8fa9be0de6cf49e536ce1851f987bd21a43b771b09473c3549a6c853db37c1c" +dependencies = [ + "futures-core", + "futures-util", + "indexmap 1.9.3", + "pin-project", + "pin-project-lite", + "rand", + "slab", + "tokio", + "tokio-util", + "tower-layer", + "tower-service", + "tracing", +] + +[[package]] +name = "tower" +version = "0.5.3" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "ebe5ef63511595f1344e2d5cfa636d973292adc0eec1f0ad45fae9f0851ab1d4" +dependencies = [ + "futures-core", + "futures-util", + "pin-project-lite", + "sync_wrapper", + "tokio", + "tower-layer", + "tower-service", +] + +[[package]] +name = "tower-http" +version = "0.6.8" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "d4e6559d53cc268e5031cd8429d05415bc4cb4aefc4aa5d6cc35fbf5b924a1f8" +dependencies = [ + "bitflags", + "bytes", + "futures-util", + "http", + "http-body", + "iri-string", + "pin-project-lite", + "tower 0.5.3", + "tower-layer", + "tower-service", +] + +[[package]] +name = "tower-layer" +version = "0.3.3" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "121c2a6cda46980bb0fcd1647ffaf6cd3fc79a013de288782836f6df9c48780e" + +[[package]] +name = "tower-service" +version = "0.3.3" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "8df9b6e13f2d32c91b9bd719c00d1958837bc7dec474d94952798cc8e69eeec3" + +[[package]] +name = "tracing" +version = "0.1.44" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "63e71662fa4b2a2c3a26f570f037eb95bb1f85397f3cd8076caed2f026a6d100" +dependencies = [ + "pin-project-lite", + "tracing-attributes", + "tracing-core", +] + +[[package]] +name = "tracing-attributes" +version = "0.1.31" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "7490cfa5ec963746568740651ac6781f701c9c5ea257c58e057f3ba8cf69e8da" +dependencies = [ + "proc-macro2", + "quote", + "syn", +] + +[[package]] +name = "tracing-core" +version = "0.1.36" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "db97caf9d906fbde555dd62fa95ddba9eecfd14cb388e4f491a66d74cd5fb79a" +dependencies = [ + "once_cell", + "valuable", +] + +[[package]] +name = "tracing-log" +version = "0.2.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "ee855f1f400bd0e5c02d150ae5de3840039a3f54b025156404e34c23c03f47c3" +dependencies = [ + "log", + "once_cell", + "tracing-core", +] + +[[package]] +name = "tracing-serde" +version = "0.2.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "704b1aeb7be0d0a84fc9828cae51dab5970fee5088f83d1dd7ee6f6246fc6ff1" +dependencies = [ + "serde", + "tracing-core", +] + +[[package]] +name = "tracing-subscriber" +version = "0.3.22" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "2f30143827ddab0d256fd843b7a66d164e9f271cfa0dde49142c5ca0ca291f1e" +dependencies = [ + "matchers", + "nu-ansi-term", + "once_cell", + "regex-automata", + "serde", + "serde_json", + "sharded-slab", + "smallvec", + "thread_local", + "tracing", + "tracing-core", + "tracing-log", + "tracing-serde", +] + +[[package]] +name = "try-lock" +version = "0.2.5" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "e421abadd41a4225275504ea4d6566923418b7f05506fbc9c0fe86ba7396114b" + +[[package]] +name = "unicode-ident" +version = "1.0.24" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "e6e4313cd5fcd3dad5cafa179702e2b244f760991f45397d14d4ebf38247da75" + +[[package]] +name = "unicode-xid" +version = "0.2.6" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "ebc1c04c71510c7f702b52b7c350734c9ff1295c464a03335b00bb84fc54f853" + +[[package]] +name = "unsafe-libyaml" +version = "0.2.11" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "673aac59facbab8a9007c7f6108d11f63b603f7cabff99fabf650fea5c32b861" + +[[package]] +name = "untrusted" +version = "0.9.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "8ecb6da28b8a351d773b68d5825ac39017e680750f980f3a1a85cd8dd28a47c1" + +[[package]] +name = "url" +version = "2.5.8" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "ff67a8a4397373c3ef660812acab3268222035010ab8680ec4215f38ba3d0eed" +dependencies = [ + "form_urlencoded", + "idna", + "percent-encoding", + "serde", +] + +[[package]] +name = "utf8_iter" +version = "1.0.4" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "b6c140620e7ffbb22c2dee59cafe6084a59b5ffc27a8859a5f0d494b5d52b6be" + +[[package]] +name = "uuid" +version = "1.21.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "b672338555252d43fd2240c714dc444b8c6fb0a5c5335e65a07bba7742735ddb" +dependencies = [ + "getrandom 0.4.1", + "js-sys", + "wasm-bindgen", +] + +[[package]] +name = "valuable" +version = "0.1.1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "ba73ea9cf16a25df0c8caa16c51acb937d5712a8429db78a3ee29d5dcacd3a65" + +[[package]] +name = "vcpkg" +version = "0.2.15" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "accd4ea62f7bb7a82fe23066fb0957d48ef677f6eeb8215f372f52e48bb32426" + +[[package]] +name = "want" +version = "0.3.1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "bfa7760aed19e106de2c7c0b581b509f2f25d3dacaf737cb82ac61bc6d760b0e" +dependencies = [ + "try-lock", +] + +[[package]] +name = "wasi" +version = "0.11.1+wasi-snapshot-preview1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "ccf3ec651a847eb01de73ccad15eb7d99f80485de043efb2f370cd654f4ea44b" + +[[package]] +name = "wasip2" +version = "1.0.2+wasi-0.2.9" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "9517f9239f02c069db75e65f174b3da828fe5f5b945c4dd26bd25d89c03ebcf5" +dependencies = [ + "wit-bindgen", +] + +[[package]] +name = "wasip3" +version = "0.4.0+wasi-0.3.0-rc-2026-01-06" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "5428f8bf88ea5ddc08faddef2ac4a67e390b88186c703ce6dbd955e1c145aca5" +dependencies = [ + "wit-bindgen", +] + +[[package]] +name = "wasm-bindgen" +version = "0.2.110" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "1de241cdc66a9d91bd84f097039eb140cdc6eec47e0cdbaf9d932a1dd6c35866" +dependencies = [ + "cfg-if", + "once_cell", + "rustversion", + "wasm-bindgen-macro", + "wasm-bindgen-shared", +] + +[[package]] +name = "wasm-bindgen-futures" +version = "0.4.60" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "a42e96ea38f49b191e08a1bab66c7ffdba24b06f9995b39a9dd60222e5b6f1da" +dependencies = [ + "cfg-if", + "futures-util", + "js-sys", + "once_cell", + "wasm-bindgen", + "web-sys", +] + +[[package]] +name = "wasm-bindgen-macro" +version = "0.2.110" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "e12fdf6649048f2e3de6d7d5ff3ced779cdedee0e0baffd7dff5cdfa3abc8a52" +dependencies = [ + "quote", + "wasm-bindgen-macro-support", +] + +[[package]] +name = "wasm-bindgen-macro-support" +version = "0.2.110" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "0e63d1795c565ac3462334c1e396fd46dbf481c40f51f5072c310717bc4fb309" +dependencies = [ + "bumpalo", + "proc-macro2", + "quote", + "syn", + "wasm-bindgen-shared", +] + +[[package]] +name = "wasm-bindgen-shared" +version = "0.2.110" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "e9f9cdac23a5ce71f6bf9f8824898a501e511892791ea2a0c6b8568c68b9cb53" +dependencies = [ + "unicode-ident", +] + +[[package]] +name = "wasm-encoder" +version = "0.244.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "990065f2fe63003fe337b932cfb5e3b80e0b4d0f5ff650e6985b1048f62c8319" +dependencies = [ + "leb128fmt", + "wasmparser", +] + +[[package]] +name = "wasm-metadata" +version = "0.244.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "bb0e353e6a2fbdc176932bbaab493762eb1255a7900fe0fea1a2f96c296cc909" +dependencies = [ + "anyhow", + "indexmap 2.13.0", + "wasm-encoder", + "wasmparser", +] + +[[package]] +name = "wasmparser" +version = "0.244.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "47b807c72e1bac69382b3a6fb3dbe8ea4c0ed87ff5629b8685ae6b9a611028fe" +dependencies = [ + "bitflags", + "hashbrown 0.15.5", + "indexmap 2.13.0", + "semver", +] + +[[package]] +name = "web-sys" +version = "0.3.87" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "f2c7c5718134e770ee62af3b6b4a84518ec10101aad610c024b64d6ff29bb1ff" +dependencies = [ + "js-sys", + "wasm-bindgen", +] + +[[package]] +name = "winapi" +version = "0.3.9" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "5c839a674fcd7a98952e593242ea400abe93992746761e38641405d28b00f419" +dependencies = [ + "winapi-i686-pc-windows-gnu", + "winapi-x86_64-pc-windows-gnu", +] + +[[package]] +name = "winapi-i686-pc-windows-gnu" +version = "0.4.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "ac3b87c63620426dd9b991e5ce0329eff545bccbbb34f3be09ff6fb6ab51b7b6" + +[[package]] +name = "winapi-x86_64-pc-windows-gnu" +version = "0.4.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "712e227841d057c1ee1cd2fb22fa7e5a5461ae8e48fa2ca79ec42cfc1931183f" + +[[package]] +name = "windows-core" +version = "0.62.2" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "b8e83a14d34d0623b51dce9581199302a221863196a1dde71a7663a4c2be9deb" +dependencies = [ + "windows-implement", + "windows-interface", + "windows-link", + "windows-result", + "windows-strings", +] + +[[package]] +name = "windows-implement" +version = "0.60.2" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "053e2e040ab57b9dc951b72c264860db7eb3b0200ba345b4e4c3b14f67855ddf" +dependencies = [ + "proc-macro2", + "quote", + "syn", +] + +[[package]] +name = "windows-interface" +version = "0.59.3" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "3f316c4a2570ba26bbec722032c4099d8c8bc095efccdc15688708623367e358" +dependencies = [ + "proc-macro2", + "quote", + "syn", +] + +[[package]] +name = "windows-link" +version = "0.2.1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "f0805222e57f7521d6a62e36fa9163bc891acd422f971defe97d64e70d0a4fe5" + +[[package]] +name = "windows-registry" +version = "0.6.1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "02752bf7fbdcce7f2a27a742f798510f3e5ad88dbe84871e5168e2120c3d5720" +dependencies = [ + "windows-link", + "windows-result", + "windows-strings", +] + +[[package]] +name = "windows-result" +version = "0.4.1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "7781fa89eaf60850ac3d2da7af8e5242a5ea78d1a11c49bf2910bb5a73853eb5" +dependencies = [ + "windows-link", +] + +[[package]] +name = "windows-strings" +version = "0.5.1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "7837d08f69c77cf6b07689544538e017c1bfcf57e34b4c0ff58e6c2cd3b37091" +dependencies = [ + "windows-link", +] + +[[package]] +name = "windows-sys" +version = "0.52.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "282be5f36a8ce781fad8c8ae18fa3f9beff57ec1b52cb3de0789201425d9a33d" +dependencies = [ + "windows-targets 0.52.6", +] + +[[package]] +name = "windows-sys" +version = "0.60.2" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "f2f500e4d28234f72040990ec9d39e3a6b950f9f22d3dba18416c35882612bcb" +dependencies = [ + "windows-targets 0.53.5", +] + +[[package]] +name = "windows-sys" +version = "0.61.2" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "ae137229bcbd6cdf0f7b80a31df61766145077ddf49416a728b02cb3921ff3fc" +dependencies = [ + "windows-link", +] + +[[package]] +name = "windows-targets" +version = "0.52.6" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "9b724f72796e036ab90c1021d4780d4d3d648aca59e491e6b98e725b84e99973" +dependencies = [ + "windows_aarch64_gnullvm 0.52.6", + "windows_aarch64_msvc 0.52.6", + "windows_i686_gnu 0.52.6", + "windows_i686_gnullvm 0.52.6", + "windows_i686_msvc 0.52.6", + "windows_x86_64_gnu 0.52.6", + "windows_x86_64_gnullvm 0.52.6", + "windows_x86_64_msvc 0.52.6", +] + +[[package]] +name = "windows-targets" +version = "0.53.5" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "4945f9f551b88e0d65f3db0bc25c33b8acea4d9e41163edf90dcd0b19f9069f3" +dependencies = [ + "windows-link", + "windows_aarch64_gnullvm 0.53.1", + "windows_aarch64_msvc 0.53.1", + "windows_i686_gnu 0.53.1", + "windows_i686_gnullvm 0.53.1", + "windows_i686_msvc 0.53.1", + "windows_x86_64_gnu 0.53.1", + "windows_x86_64_gnullvm 0.53.1", + "windows_x86_64_msvc 0.53.1", +] + +[[package]] +name = "windows_aarch64_gnullvm" +version = "0.52.6" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "32a4622180e7a0ec044bb555404c800bc9fd9ec262ec147edd5989ccd0c02cd3" + +[[package]] +name = "windows_aarch64_gnullvm" +version = "0.53.1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "a9d8416fa8b42f5c947f8482c43e7d89e73a173cead56d044f6a56104a6d1b53" + +[[package]] +name = "windows_aarch64_msvc" +version = "0.52.6" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "09ec2a7bb152e2252b53fa7803150007879548bc709c039df7627cabbd05d469" + +[[package]] +name = "windows_aarch64_msvc" +version = "0.53.1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "b9d782e804c2f632e395708e99a94275910eb9100b2114651e04744e9b125006" + +[[package]] +name = "windows_i686_gnu" +version = "0.52.6" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "8e9b5ad5ab802e97eb8e295ac6720e509ee4c243f69d781394014ebfe8bbfa0b" + +[[package]] +name = "windows_i686_gnu" +version = "0.53.1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "960e6da069d81e09becb0ca57a65220ddff016ff2d6af6a223cf372a506593a3" + +[[package]] +name = "windows_i686_gnullvm" +version = "0.52.6" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "0eee52d38c090b3caa76c563b86c3a4bd71ef1a819287c19d586d7334ae8ed66" + +[[package]] +name = "windows_i686_gnullvm" +version = "0.53.1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "fa7359d10048f68ab8b09fa71c3daccfb0e9b559aed648a8f95469c27057180c" + +[[package]] +name = "windows_i686_msvc" +version = "0.52.6" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "240948bc05c5e7c6dabba28bf89d89ffce3e303022809e73deaefe4f6ec56c66" + +[[package]] +name = "windows_i686_msvc" +version = "0.53.1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "1e7ac75179f18232fe9c285163565a57ef8d3c89254a30685b57d83a38d326c2" + +[[package]] +name = "windows_x86_64_gnu" +version = "0.52.6" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "147a5c80aabfbf0c7d901cb5895d1de30ef2907eb21fbbab29ca94c5b08b1a78" + +[[package]] +name = "windows_x86_64_gnu" +version = "0.53.1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "9c3842cdd74a865a8066ab39c8a7a473c0778a3f29370b5fd6b4b9aa7df4a499" + +[[package]] +name = "windows_x86_64_gnullvm" +version = "0.52.6" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "24d5b23dc417412679681396f2b49f3de8c1473deb516bd34410872eff51ed0d" + +[[package]] +name = "windows_x86_64_gnullvm" +version = "0.53.1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "0ffa179e2d07eee8ad8f57493436566c7cc30ac536a3379fdf008f47f6bb7ae1" + +[[package]] +name = "windows_x86_64_msvc" +version = "0.52.6" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "589f6da84c646204747d1270a2a5661ea66ed1cced2631d546fdfb155959f9ec" + +[[package]] +name = "windows_x86_64_msvc" +version = "0.53.1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "d6bbff5f0aada427a1e5a6da5f1f98158182f26556f345ac9e04d36d0ebed650" + +[[package]] +name = "wit-bindgen" +version = "0.51.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "d7249219f66ced02969388cf2bb044a09756a083d0fab1e566056b04d9fbcaa5" +dependencies = [ + "wit-bindgen-rust-macro", +] + +[[package]] +name = "wit-bindgen-core" +version = "0.51.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "ea61de684c3ea68cb082b7a88508a8b27fcc8b797d738bfc99a82facf1d752dc" +dependencies = [ + "anyhow", + "heck", + "wit-parser", +] + +[[package]] +name = "wit-bindgen-rust" +version = "0.51.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "b7c566e0f4b284dd6561c786d9cb0142da491f46a9fbed79ea69cdad5db17f21" +dependencies = [ + "anyhow", + "heck", + "indexmap 2.13.0", + "prettyplease", + "syn", + "wasm-metadata", + "wit-bindgen-core", + "wit-component", +] + +[[package]] +name = "wit-bindgen-rust-macro" +version = "0.51.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "0c0f9bfd77e6a48eccf51359e3ae77140a7f50b1e2ebfe62422d8afdaffab17a" +dependencies = [ + "anyhow", + "prettyplease", + "proc-macro2", + "quote", + "syn", + "wit-bindgen-core", + "wit-bindgen-rust", +] + +[[package]] +name = "wit-component" +version = "0.244.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "9d66ea20e9553b30172b5e831994e35fbde2d165325bec84fc43dbf6f4eb9cb2" +dependencies = [ + "anyhow", + "bitflags", + "indexmap 2.13.0", + "log", + "serde", + "serde_derive", + "serde_json", + "wasm-encoder", + "wasm-metadata", + "wasmparser", + "wit-parser", +] + +[[package]] +name = "wit-parser" +version = "0.244.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "ecc8ac4bc1dc3381b7f59c34f00b67e18f910c2c0f50015669dde7def656a736" +dependencies = [ + "anyhow", + "id-arena", + "indexmap 2.13.0", + "log", + "semver", + "serde", + "serde_derive", + "serde_json", + "unicode-xid", + "wasmparser", +] + +[[package]] +name = "writeable" +version = "0.6.2" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "9edde0db4769d2dc68579893f2306b26c6ecfbe0ef499b013d731b7b9247e0b9" + +[[package]] +name = "xattr" +version = "1.6.1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "32e45ad4206f6d2479085147f02bc2ef834ac85886624a23575ae137c8aa8156" +dependencies = [ + "libc", + "rustix", +] + +[[package]] +name = "yoke" +version = "0.8.1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "72d6e5c6afb84d73944e5cedb052c4680d5657337201555f9f2a16b7406d4954" +dependencies = [ + "stable_deref_trait", + "yoke-derive", + "zerofrom", +] + +[[package]] +name = "yoke-derive" +version = "0.8.1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "b659052874eb698efe5b9e8cf382204678a0086ebf46982b79d6ca3182927e5d" +dependencies = [ + "proc-macro2", + "quote", + "syn", + "synstructure", +] + +[[package]] +name = "zerocopy" +version = "0.8.39" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "db6d35d663eadb6c932438e763b262fe1a70987f9ae936e60158176d710cae4a" +dependencies = [ + "zerocopy-derive", +] + +[[package]] +name = "zerocopy-derive" +version = "0.8.39" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "4122cd3169e94605190e77839c9a40d40ed048d305bfdc146e7df40ab0f3e517" +dependencies = [ + "proc-macro2", + "quote", + "syn", +] + +[[package]] +name = "zerofrom" +version = "0.1.6" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "50cc42e0333e05660c3587f3bf9d0478688e15d870fab3346451ce7f8c9fbea5" +dependencies = [ + "zerofrom-derive", +] + +[[package]] +name = "zerofrom-derive" +version = "0.1.6" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "d71e5d6e06ab090c67b5e44993ec16b72dcbaabc526db883a360057678b48502" +dependencies = [ + "proc-macro2", + "quote", + "syn", + "synstructure", +] + +[[package]] +name = "zeroize" +version = "1.8.2" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "b97154e67e32c85465826e8bcc1c59429aaaf107c1e4a9e53c8d8ccd5eff88d0" + +[[package]] +name = "zerotrie" +version = "0.2.3" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "2a59c17a5562d507e4b54960e8569ebee33bee890c70aa3fe7b97e85a9fd7851" +dependencies = [ + "displaydoc", + "yoke", + "zerofrom", +] + +[[package]] +name = "zerovec" +version = "0.11.5" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "6c28719294829477f525be0186d13efa9a3c602f7ec202ca9e353d310fb9a002" +dependencies = [ + "yoke", + "zerofrom", + "zerovec-derive", +] + +[[package]] +name = "zerovec-derive" +version = "0.11.2" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "eadce39539ca5cb3985590102671f2567e659fca9666581ad3411d59207951f3" +dependencies = [ + "proc-macro2", + "quote", + "syn", +] + +[[package]] +name = "zmij" +version = "1.0.21" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "b8848ee67ecc8aedbaf3e4122217aff892639231befc6a1b58d29fff4c2cabaa" diff --git a/apps/daemon/Cargo.toml b/apps/daemon/Cargo.toml index 75c4b82..35101c4 100644 --- a/apps/daemon/Cargo.toml +++ b/apps/daemon/Cargo.toml @@ -12,6 +12,7 @@ prost-types = "0.13" # Async runtime tokio = { version = "1", features = ["full"] } +tokio-stream = { version = "0.1", features = ["sync"] } # Docker bollard = "0.18" @@ -35,5 +36,12 @@ thiserror = "2" # UUID uuid = { version = "1", features = ["v4"] } +# Async utils +futures = "0.3" + +# Filesystem +tar = "0.4" +flate2 = "1" + [build-dependencies] tonic-build = "0.12" diff --git a/apps/daemon/src/auth.rs b/apps/daemon/src/auth.rs new file mode 100644 index 0000000..5de0aff --- /dev/null +++ b/apps/daemon/src/auth.rs @@ -0,0 +1,15 @@ +use tonic::{Request, Status}; + +/// Validate the daemon token from the gRPC request metadata. +pub fn check_auth(req: &Request<()>, expected_token: &str) -> Result<(), Status> { + let token = req + .metadata() + .get("authorization") + .and_then(|v| v.to_str().ok()) + .and_then(|v| v.strip_prefix("Bearer ")); + + match token { + Some(t) if t == expected_token => Ok(()), + _ => Err(Status::unauthenticated("Invalid or missing daemon token")), + } +} diff --git a/apps/daemon/src/docker/container.rs b/apps/daemon/src/docker/container.rs new file mode 100644 index 0000000..88da499 --- /dev/null +++ b/apps/daemon/src/docker/container.rs @@ -0,0 +1,263 @@ +use std::collections::HashMap; +use std::sync::Arc; +use anyhow::Result; +use bollard::container::{ + Config, CreateContainerOptions, LogsOptions, RemoveContainerOptions, StartContainerOptions, + StopContainerOptions, StatsOptions, Stats, +}; +use bollard::image::CreateImageOptions; +use bollard::models::{HostConfig, PortBinding}; +use futures::StreamExt; +use tracing::info; + +use crate::docker::DockerManager; +use crate::server::ServerSpec; + +/// Container name prefix for all managed game servers. +const CONTAINER_PREFIX: &str = "gp_"; + +pub fn container_name(server_uuid: &str) -> String { + format!("{}{}", CONTAINER_PREFIX, server_uuid) +} + +impl DockerManager { + /// Pull a Docker image if not already present. + pub async fn pull_image(&self, image: &str) -> Result<()> { + info!(image = %image, "Pulling Docker image"); + + let options = CreateImageOptions { + from_image: image, + ..Default::default() + }; + + let mut stream = self.client().create_image(Some(options), None, None); + while let Some(result) = stream.next().await { + match result { + Ok(info) => { + if let Some(status) = &info.status { + tracing::debug!(status = %status, "Image pull progress"); + } + } + Err(e) => return Err(e.into()), + } + } + + info!(image = %image, "Image pulled successfully"); + Ok(()) + } + + /// Create and configure a container for a game server. + pub async fn create_container(&self, spec: &ServerSpec) -> Result { + let name = container_name(&spec.uuid); + + // Build port bindings + let mut port_bindings: HashMap>> = HashMap::new(); + for port_map in &spec.ports { + let container_port = format!("{}/{}", port_map.container_port, port_map.protocol); + port_bindings.insert( + container_port, + Some(vec![PortBinding { + host_ip: Some("0.0.0.0".to_string()), + host_port: Some(port_map.host_port.to_string()), + }]), + ); + } + + // Build exposed ports + let mut exposed_ports: HashMap> = HashMap::new(); + for port_map in &spec.ports { + let container_port = format!("{}/{}", port_map.container_port, port_map.protocol); + exposed_ports.insert(container_port, HashMap::new()); + } + + // Convert env map to Docker format + let env: Vec = spec + .environment + .iter() + .map(|(k, v)| format!("{}={}", k, v)) + .collect(); + + let host_config = HostConfig { + memory: Some(spec.memory_limit), + memory_swap: Some(spec.memory_limit), // no swap + nano_cpus: Some((spec.cpu_limit as i64) * 10_000_000), // cpu_limit=100 means 1 core + port_bindings: Some(port_bindings), + network_mode: Some(self.network_name().to_string()), + binds: Some(vec![format!( + "{}:/data", + spec.data_path.display() + )]), + ..Default::default() + }; + + let config = Config { + image: Some(spec.docker_image.clone()), + hostname: Some(spec.uuid.clone()), + env: Some(env), + exposed_ports: Some(exposed_ports), + host_config: Some(host_config), + working_dir: Some("/data".to_string()), + cmd: if spec.startup_command.is_empty() { + None + } else { + Some( + spec.startup_command + .split_whitespace() + .map(String::from) + .collect(), + ) + }, + tty: Some(true), + attach_stdin: Some(true), + attach_stdout: Some(true), + attach_stderr: Some(true), + open_stdin: Some(true), + ..Default::default() + }; + + let options = CreateContainerOptions { name: name.as_str(), platform: None }; + let response = self.client().create_container(Some(options), config).await?; + + info!(container_id = %response.id, uuid = %spec.uuid, "Container created"); + Ok(response.id) + } + + /// Start a container. + pub async fn start_container(&self, server_uuid: &str) -> Result<()> { + let name = container_name(server_uuid); + self.client() + .start_container(&name, None::>) + .await?; + info!(uuid = %server_uuid, "Container started"); + Ok(()) + } + + /// Stop a container gracefully. + pub async fn stop_container(&self, server_uuid: &str, timeout_secs: i64) -> Result<()> { + let name = container_name(server_uuid); + self.client() + .stop_container( + &name, + Some(StopContainerOptions { + t: timeout_secs, + }), + ) + .await?; + info!(uuid = %server_uuid, "Container stopped"); + Ok(()) + } + + /// Kill a container immediately. + pub async fn kill_container(&self, server_uuid: &str) -> Result<()> { + let name = container_name(server_uuid); + self.client() + .kill_container::(&name, None) + .await?; + info!(uuid = %server_uuid, "Container killed"); + Ok(()) + } + + /// Remove a container and its volumes. + pub async fn remove_container(&self, server_uuid: &str) -> Result<()> { + let name = container_name(server_uuid); + self.client() + .remove_container( + &name, + Some(RemoveContainerOptions { + force: true, + v: true, + ..Default::default() + }), + ) + .await?; + info!(uuid = %server_uuid, "Container removed"); + Ok(()) + } + + /// Get container stats (CPU, memory, network). + pub async fn container_stats( + &self, + server_uuid: &str, + ) -> Result { + let name = container_name(server_uuid); + let mut stream = self.client().stats( + &name, + Some(StatsOptions { + stream: false, + one_shot: true, + ..Default::default() + }), + ); + + match stream.next().await { + Some(Ok(stats)) => Ok(stats), + Some(Err(e)) => Err(e.into()), + None => Err(anyhow::anyhow!("No stats returned")), + } + } + + /// Check if a container exists and return its state. + pub async fn container_state( + &self, + server_uuid: &str, + ) -> Result> { + let name = container_name(server_uuid); + match self.client().inspect_container(&name, None).await { + Ok(info) => { + let state = info + .state + .and_then(|s| s.status) + .map(|s| format!("{:?}", s)); + Ok(state) + } + Err(bollard::errors::Error::DockerResponseServerError { + status_code: 404, .. + }) => Ok(None), + Err(e) => Err(e.into()), + } + } + + /// Stream container logs (stdout + stderr). Returns an owned stream. + pub fn stream_logs( + self: &Arc, + server_uuid: &str, + ) -> impl futures::Stream> + Send + 'static { + let name = container_name(server_uuid); + let options = LogsOptions:: { + follow: true, + stdout: true, + stderr: true, + tail: "100".to_string(), + ..Default::default() + }; + + let client = self.client().clone(); + client.logs(&name, Some(options)).map(|result| { + result.map(|output| output.to_string()) + }) + } + + /// Send a command to a container via exec (attach to stdin). + pub async fn send_command(&self, server_uuid: &str, command: &str) -> Result<()> { + let name = container_name(server_uuid); + + let exec = self + .client() + .create_exec( + &name, + bollard::exec::CreateExecOptions { + cmd: Some(vec!["sh", "-c", &format!("echo '{}' > /proc/1/fd/0", command)]), + attach_stdout: Some(true), + attach_stderr: Some(true), + ..Default::default() + }, + ) + .await?; + + self.client() + .start_exec(&exec.id, None::) + .await?; + + Ok(()) + } +} diff --git a/apps/daemon/src/docker/manager.rs b/apps/daemon/src/docker/manager.rs new file mode 100644 index 0000000..b94839c --- /dev/null +++ b/apps/daemon/src/docker/manager.rs @@ -0,0 +1,77 @@ +use anyhow::Result; +use bollard::Docker; +use bollard::network::CreateNetworkOptions; +use tracing::info; + +use crate::config::DockerConfig; + +/// Manages the Docker client and network setup. +#[derive(Clone)] +pub struct DockerManager { + client: Docker, + network_name: String, +} + +impl DockerManager { + pub async fn new(config: &DockerConfig) -> Result { + let client = Docker::connect_with_socket( + &config.socket, + 120, // timeout + bollard::API_DEFAULT_VERSION, + )?; + + // Verify connection + let version = client.version().await?; + info!( + docker_version = version.version.as_deref().unwrap_or("unknown"), + "Connected to Docker" + ); + + let manager = Self { + client, + network_name: config.network.clone(), + }; + + manager.ensure_network(&config.network_subnet).await?; + + Ok(manager) + } + + pub fn client(&self) -> &Docker { + &self.client + } + + pub fn network_name(&self) -> &str { + &self.network_name + } + + async fn ensure_network(&self, subnet: &str) -> Result<()> { + let networks = self.client.list_networks::(None).await?; + let exists = networks + .iter() + .any(|n| n.name.as_deref() == Some(&self.network_name)); + + if !exists { + info!(network = %self.network_name, "Creating Docker network"); + let ipam_config = bollard::models::IpamConfig { + subnet: Some(subnet.to_string()), + ..Default::default() + }; + let ipam = bollard::models::Ipam { + config: Some(vec![ipam_config]), + ..Default::default() + }; + self.client + .create_network(CreateNetworkOptions { + name: self.network_name.clone(), + driver: "bridge".to_string(), + ipam, + ..Default::default() + }) + .await?; + info!(network = %self.network_name, "Docker network created"); + } + + Ok(()) + } +} diff --git a/apps/daemon/src/docker/mod.rs b/apps/daemon/src/docker/mod.rs new file mode 100644 index 0000000..337e69d --- /dev/null +++ b/apps/daemon/src/docker/mod.rs @@ -0,0 +1,4 @@ +pub mod container; +pub mod manager; + +pub use manager::DockerManager; diff --git a/apps/daemon/src/error.rs b/apps/daemon/src/error.rs new file mode 100644 index 0000000..972f83e --- /dev/null +++ b/apps/daemon/src/error.rs @@ -0,0 +1,52 @@ +use thiserror::Error; + +#[derive(Error, Debug)] +pub enum DaemonError { + #[error("Docker error: {0}")] + Docker(#[from] bollard::errors::Error), + + #[error("Server not found: {0}")] + ServerNotFound(String), + + #[error("Server already exists: {0}")] + ServerAlreadyExists(String), + + #[error("Invalid state transition: {current} -> {requested}")] + InvalidStateTransition { current: String, requested: String }, + + #[error("Filesystem error: {0}")] + Filesystem(String), + + #[error("Path traversal attempt: {0}")] + PathTraversal(String), + + #[error("IO error: {0}")] + Io(#[from] std::io::Error), + + #[error("Authentication failed")] + AuthFailed, + + #[error("{0}")] + Internal(String), +} + +impl From for tonic::Status { + fn from(err: DaemonError) -> Self { + match &err { + DaemonError::ServerNotFound(_) => tonic::Status::not_found(err.to_string()), + DaemonError::ServerAlreadyExists(_) => { + tonic::Status::already_exists(err.to_string()) + } + DaemonError::InvalidStateTransition { .. } => { + tonic::Status::failed_precondition(err.to_string()) + } + DaemonError::PathTraversal(_) => { + tonic::Status::permission_denied(err.to_string()) + } + DaemonError::AuthFailed => { + tonic::Status::unauthenticated(err.to_string()) + } + _ => tonic::Status::internal(err.to_string()), + } + } +} diff --git a/apps/daemon/src/filesystem/mod.rs b/apps/daemon/src/filesystem/mod.rs new file mode 100644 index 0000000..91f0b6a --- /dev/null +++ b/apps/daemon/src/filesystem/mod.rs @@ -0,0 +1,3 @@ +pub mod operations; + +pub use operations::FileSystem; diff --git a/apps/daemon/src/filesystem/operations.rs b/apps/daemon/src/filesystem/operations.rs new file mode 100644 index 0000000..108f9dd --- /dev/null +++ b/apps/daemon/src/filesystem/operations.rs @@ -0,0 +1,129 @@ +use std::path::PathBuf; +use tokio::fs; +use tracing::debug; + +use crate::error::DaemonError; + +/// Filesystem operations with path jail enforcement. +pub struct FileSystem { + root: PathBuf, +} + +impl FileSystem { + pub fn new(root: PathBuf) -> Self { + Self { root } + } + + /// Resolve a relative path within the jail. Prevents path traversal. + fn resolve(&self, relative: &str) -> Result { + let clean = relative.trim_start_matches('/'); + let resolved = self.root.join(clean); + + // Canonicalize both to compare (handle .. and symlinks) + // For non-existent paths, check the parent + let check_path = if resolved.exists() { + resolved.canonicalize().map_err(DaemonError::Io)? + } else { + let parent = resolved + .parent() + .ok_or_else(|| DaemonError::PathTraversal(relative.to_string()))?; + if !parent.exists() { + // Parent doesn't exist either — check the root prefix + let normalized = self.root.join(clean); + if !normalized.starts_with(&self.root) { + return Err(DaemonError::PathTraversal(relative.to_string())); + } + return Ok(normalized); + } + let canonical_parent = parent.canonicalize().map_err(DaemonError::Io)?; + canonical_parent.join(resolved.file_name().unwrap_or_default()) + }; + + let canonical_root = self.root.canonicalize().unwrap_or_else(|_| self.root.clone()); + if !check_path.starts_with(&canonical_root) { + return Err(DaemonError::PathTraversal(relative.to_string())); + } + + Ok(resolved) + } + + /// List files in a directory. + pub async fn list_files(&self, path: &str) -> Result, DaemonError> { + let resolved = self.resolve(path)?; + let mut entries = Vec::new(); + + let mut reader = fs::read_dir(&resolved).await.map_err(DaemonError::Io)?; + while let Some(entry) = reader.next_entry().await.map_err(DaemonError::Io)? { + let metadata = entry.metadata().await.map_err(DaemonError::Io)?; + let name = entry.file_name().to_string_lossy().to_string(); + let relative_path = format!( + "{}/{}", + path.trim_end_matches('/'), + &name + ); + + entries.push(FileEntry { + name, + path: relative_path, + is_directory: metadata.is_dir(), + size: metadata.len() as i64, + modified_at: metadata + .modified() + .ok() + .and_then(|t| t.duration_since(std::time::UNIX_EPOCH).ok()) + .map(|d| d.as_secs() as i64) + .unwrap_or(0), + }); + } + + entries.sort_by(|a, b| { + // Directories first, then by name + b.is_directory.cmp(&a.is_directory).then(a.name.cmp(&b.name)) + }); + + Ok(entries) + } + + /// Read file contents. + pub async fn read_file(&self, path: &str) -> Result, DaemonError> { + let resolved = self.resolve(path)?; + debug!(path = %resolved.display(), "Reading file"); + fs::read(&resolved).await.map_err(DaemonError::Io) + } + + /// Write file contents. + pub async fn write_file(&self, path: &str, data: &[u8]) -> Result<(), DaemonError> { + let resolved = self.resolve(path)?; + + // Ensure parent directory exists + if let Some(parent) = resolved.parent() { + fs::create_dir_all(parent).await.map_err(DaemonError::Io)?; + } + + debug!(path = %resolved.display(), "Writing file"); + fs::write(&resolved, data).await.map_err(DaemonError::Io) + } + + /// Delete files or directories. + pub async fn delete_paths(&self, paths: &[String]) -> Result<(), DaemonError> { + for path in paths { + let resolved = self.resolve(path)?; + if resolved.is_dir() { + fs::remove_dir_all(&resolved).await.map_err(DaemonError::Io)?; + } else { + fs::remove_file(&resolved).await.map_err(DaemonError::Io)?; + } + debug!(path = %resolved.display(), "Deleted"); + } + Ok(()) + } +} + +#[derive(Debug, Clone)] +pub struct FileEntry { + pub name: String, + pub path: String, + pub is_directory: bool, + pub size: i64, + pub modified_at: i64, +} diff --git a/apps/daemon/src/grpc/mod.rs b/apps/daemon/src/grpc/mod.rs new file mode 100644 index 0000000..6e2947a --- /dev/null +++ b/apps/daemon/src/grpc/mod.rs @@ -0,0 +1,3 @@ +pub mod service; + +pub use service::DaemonServiceImpl; diff --git a/apps/daemon/src/grpc/service.rs b/apps/daemon/src/grpc/service.rs new file mode 100644 index 0000000..56d05cf --- /dev/null +++ b/apps/daemon/src/grpc/service.rs @@ -0,0 +1,511 @@ +use std::pin::Pin; +use std::sync::Arc; +use std::time::Instant; + +use futures::StreamExt; +use tokio_stream::wrappers::ReceiverStream; +use tonic::{Request, Response, Status}; +use tracing::{info, error}; + +use crate::server::{ServerManager, PortMap}; +use crate::filesystem::FileSystem; + +// Import generated protobuf types +pub mod pb { + tonic::include_proto!("gamepanel.daemon"); +} + +use pb::daemon_service_server::DaemonService; +use pb::*; + +pub struct DaemonServiceImpl { + server_manager: Arc, + daemon_token: String, + start_time: Instant, +} + +impl DaemonServiceImpl { + pub fn new(server_manager: Arc, daemon_token: String) -> Self { + Self { + server_manager, + daemon_token, + start_time: Instant::now(), + } + } + + fn check_auth(&self, req: &Request) -> Result<(), Status> { + let token = req + .metadata() + .get("authorization") + .and_then(|v| v.to_str().ok()) + .and_then(|v| v.strip_prefix("Bearer ")); + + match token { + Some(t) if t == self.daemon_token => Ok(()), + _ => Err(Status::unauthenticated("Invalid or missing daemon token")), + } + } + + fn get_fs(&self, uuid: &str) -> FileSystem { + let data_path = self.server_manager.data_root().join(uuid); + FileSystem::new(data_path) + } +} + +type GrpcStream = Pin> + Send>>; + +#[tonic::async_trait] +impl DaemonService for DaemonServiceImpl { + // === Node === + + async fn get_node_status( + &self, + request: Request, + ) -> Result, Status> { + self.check_auth(&request)?; + + let servers = self.server_manager.list_servers().await; + let active = servers + .iter() + .filter(|s| s.state.to_string() == "running") + .count(); + + Ok(Response::new(NodeStatus { + version: env!("CARGO_PKG_VERSION").to_string(), + is_healthy: true, + uptime_seconds: self.start_time.elapsed().as_secs() as i64, + active_servers: active as i32, + })) + } + + type StreamNodeStatsStream = GrpcStream; + + async fn stream_node_stats( + &self, + request: Request, + ) -> Result, Status> { + self.check_auth(&request)?; + + let (tx, rx) = tokio::sync::mpsc::channel(32); + + tokio::spawn(async move { + loop { + // Read system stats + let stats = NodeStats { + cpu_percent: 0.0, // TODO: real system stats + memory_used: 0, + memory_total: 0, + disk_used: 0, + disk_total: 0, + }; + if tx.send(Ok(stats)).await.is_err() { + break; + } + tokio::time::sleep(tokio::time::Duration::from_secs(5)).await; + } + }); + + Ok(Response::new(Box::pin(ReceiverStream::new(rx)))) + } + + // === Server Lifecycle === + + async fn create_server( + &self, + request: Request, + ) -> Result, Status> { + self.check_auth(&request)?; + let req = request.into_inner(); + + let ports: Vec = req + .ports + .iter() + .map(|p| PortMap { + host_port: p.host_port as u16, + container_port: p.container_port as u16, + protocol: if p.protocol.is_empty() { + "tcp".to_string() + } else { + p.protocol.clone() + }, + }) + .collect(); + + self.server_manager + .create_server( + req.uuid.clone(), + req.docker_image, + req.memory_limit, + req.disk_limit, + req.cpu_limit, + req.startup_command, + req.environment, + ports, + ) + .await + .map_err(|e| Status::from(e))?; + + Ok(Response::new(ServerResponse { + uuid: req.uuid, + status: "installing".to_string(), + })) + } + + async fn delete_server( + &self, + request: Request, + ) -> Result, Status> { + self.check_auth(&request)?; + let uuid = request.into_inner().uuid; + + self.server_manager + .delete_server(&uuid) + .await + .map_err(Status::from)?; + + Ok(Response::new(Empty {})) + } + + async fn reinstall_server( + &self, + request: Request, + ) -> Result, Status> { + self.check_auth(&request)?; + let uuid = request.into_inner().uuid; + + // Stop and remove, then recreate + let _ = self.server_manager.kill_server(&uuid).await; + // TODO: full reinstall logic + info!(uuid = %uuid, "Reinstall requested (not yet fully implemented)"); + + Ok(Response::new(Empty {})) + } + + // === Power === + + async fn set_power_state( + &self, + request: Request, + ) -> Result, Status> { + self.check_auth(&request)?; + let req = request.into_inner(); + + match req.action() { + PowerAction::Start => { + self.server_manager.start_server(&req.uuid).await.map_err(Status::from)?; + } + PowerAction::Stop => { + self.server_manager.stop_server(&req.uuid).await.map_err(Status::from)?; + } + PowerAction::Restart => { + let _ = self.server_manager.stop_server(&req.uuid).await; + self.server_manager.start_server(&req.uuid).await.map_err(Status::from)?; + } + PowerAction::Kill => { + self.server_manager.kill_server(&req.uuid).await.map_err(Status::from)?; + } + } + + Ok(Response::new(Empty {})) + } + + async fn get_server_status( + &self, + request: Request, + ) -> Result, Status> { + self.check_auth(&request)?; + let uuid = request.into_inner().uuid; + + let spec = self.server_manager.get_server(&uuid).await.map_err(Status::from)?; + + Ok(Response::new(pb::ServerStatus { + uuid: spec.uuid, + state: spec.state.to_string(), + cpu_percent: 0.0, + memory_bytes: 0, + disk_bytes: 0, + network_rx: 0, + network_tx: 0, + uptime_seconds: 0, + })) + } + + // === Console === + + type StreamConsoleStream = GrpcStream; + + async fn stream_console( + &self, + request: Request, + ) -> Result, Status> { + self.check_auth(&request)?; + let uuid = request.into_inner().uuid; + + // Verify server exists + let _ = self.server_manager.get_server(&uuid).await.map_err(Status::from)?; + + let (tx, rx) = tokio::sync::mpsc::channel(256); + let docker = self.server_manager.docker().clone(); + + let uuid_clone = uuid.clone(); + tokio::spawn(async move { + let mut stream = docker.stream_logs(&uuid_clone); + while let Some(result) = stream.next().await { + match result { + Ok(line) => { + let output = ConsoleOutput { + uuid: uuid_clone.clone(), + line, + timestamp: std::time::SystemTime::now() + .duration_since(std::time::UNIX_EPOCH) + .unwrap_or_default() + .as_millis() as i64, + }; + if tx.send(Ok(output)).await.is_err() { + break; + } + } + Err(e) => { + error!(error = %e, "Console stream error"); + break; + } + } + } + }); + + Ok(Response::new(Box::pin(ReceiverStream::new(rx)))) + } + + async fn send_command( + &self, + request: Request, + ) -> Result, Status> { + self.check_auth(&request)?; + let req = request.into_inner(); + + self.server_manager + .docker() + .send_command(&req.uuid, &req.command) + .await + .map_err(|e| Status::internal(e.to_string()))?; + + Ok(Response::new(Empty {})) + } + + // === Files === + + async fn list_files( + &self, + request: Request, + ) -> Result, Status> { + self.check_auth(&request)?; + let req = request.into_inner(); + let fs = self.get_fs(&req.uuid); + + let entries = fs + .list_files(&req.path) + .await + .map_err(|e| Status::from(e))?; + + let files = entries + .into_iter() + .map(|e| FileEntry { + name: e.name, + path: e.path, + is_directory: e.is_directory, + size: e.size, + modified_at: e.modified_at, + mime_type: String::new(), + }) + .collect(); + + Ok(Response::new(FileListResponse { files })) + } + + async fn read_file( + &self, + request: Request, + ) -> Result, Status> { + self.check_auth(&request)?; + let req = request.into_inner(); + let fs = self.get_fs(&req.uuid); + + let data = fs.read_file(&req.path).await.map_err(Status::from)?; + + Ok(Response::new(FileContent { + data, + mime_type: String::new(), + })) + } + + async fn write_file( + &self, + request: Request, + ) -> Result, Status> { + self.check_auth(&request)?; + let req = request.into_inner(); + let fs = self.get_fs(&req.uuid); + + fs.write_file(&req.path, &req.data) + .await + .map_err(Status::from)?; + + Ok(Response::new(Empty {})) + } + + async fn delete_files( + &self, + request: Request, + ) -> Result, Status> { + self.check_auth(&request)?; + let req = request.into_inner(); + let fs = self.get_fs(&req.uuid); + + fs.delete_paths(&req.paths).await.map_err(Status::from)?; + + Ok(Response::new(Empty {})) + } + + async fn compress_files( + &self, + request: Request, + ) -> Result, Status> { + self.check_auth(&request)?; + // TODO: implement compression + Err(Status::unimplemented("Not yet implemented")) + } + + async fn decompress_file( + &self, + request: Request, + ) -> Result, Status> { + self.check_auth(&request)?; + // TODO: implement decompression + Err(Status::unimplemented("Not yet implemented")) + } + + // === Backup === + + async fn create_backup( + &self, + request: Request, + ) -> Result, Status> { + self.check_auth(&request)?; + // TODO: implement backup creation + Err(Status::unimplemented("Not yet implemented")) + } + + async fn restore_backup( + &self, + request: Request, + ) -> Result, Status> { + self.check_auth(&request)?; + // TODO: implement backup restoration + Err(Status::unimplemented("Not yet implemented")) + } + + async fn delete_backup( + &self, + request: Request, + ) -> Result, Status> { + self.check_auth(&request)?; + // TODO: implement backup deletion + Err(Status::unimplemented("Not yet implemented")) + } + + // === Stats === + + type StreamServerStatsStream = GrpcStream; + + async fn stream_server_stats( + &self, + request: Request, + ) -> Result, Status> { + self.check_auth(&request)?; + let uuid = request.into_inner().uuid; + + let _ = self.server_manager.get_server(&uuid).await.map_err(Status::from)?; + + let (tx, rx) = tokio::sync::mpsc::channel(32); + let docker = self.server_manager.docker().clone(); + let uuid_clone = uuid.clone(); + + tokio::spawn(async move { + loop { + match docker.container_stats(&uuid_clone).await { + Ok(stats) => { + let cpu = calculate_cpu_percent(&stats); + let memory = stats.memory_stats.usage.unwrap_or(0) as i64; + + let resource_stats = ServerResourceStats { + uuid: uuid_clone.clone(), + cpu_percent: cpu, + memory_bytes: memory, + disk_bytes: 0, + network_rx: 0, + network_tx: 0, + state: "running".to_string(), + }; + + if tx.send(Ok(resource_stats)).await.is_err() { + break; + } + } + Err(_) => break, + } + + tokio::time::sleep(tokio::time::Duration::from_secs(2)).await; + } + }); + + Ok(Response::new(Box::pin(ReceiverStream::new(rx)))) + } + + // === Install Progress === + + type StreamInstallProgressStream = GrpcStream; + + async fn stream_install_progress( + &self, + request: Request, + ) -> Result, Status> { + self.check_auth(&request)?; + // TODO: implement install progress streaming + let (_tx, rx) = tokio::sync::mpsc::channel(8); + Ok(Response::new(Box::pin(ReceiverStream::new(rx)))) + } + + // === Players === + + async fn get_active_players( + &self, + request: Request, + ) -> Result, Status> { + self.check_auth(&request)?; + // TODO: implement game-specific player queries (RCON) + Ok(Response::new(PlayerList { + players: vec![], + max_players: 0, + })) + } +} + +/// Calculate CPU percentage from Docker stats. +fn calculate_cpu_percent(stats: &bollard::container::Stats) -> f64 { + let cpu_delta = stats.cpu_stats.cpu_usage.total_usage as f64 + - stats.precpu_stats.cpu_usage.total_usage as f64; + + let system_delta = stats.cpu_stats.system_cpu_usage.unwrap_or(0) as f64 + - stats.precpu_stats.system_cpu_usage.unwrap_or(0) as f64; + + let num_cpus = stats + .cpu_stats + .online_cpus + .unwrap_or(1) as f64; + + if system_delta > 0.0 && cpu_delta >= 0.0 { + (cpu_delta / system_delta) * num_cpus * 100.0 + } else { + 0.0 + } +} diff --git a/apps/daemon/src/main.rs b/apps/daemon/src/main.rs index 831b666..a80bda7 100644 --- a/apps/daemon/src/main.rs +++ b/apps/daemon/src/main.rs @@ -1,8 +1,21 @@ +use std::sync::Arc; use anyhow::Result; +use tonic::transport::Server; use tracing::info; use tracing_subscriber::EnvFilter; +mod auth; mod config; +mod docker; +mod error; +mod filesystem; +mod grpc; +mod server; + +use crate::docker::DockerManager; +use crate::grpc::DaemonServiceImpl; +use crate::grpc::service::pb::daemon_service_server::DaemonServiceServer; +use crate::server::ServerManager; #[tokio::main] async fn main() -> Result<()> { @@ -13,20 +26,91 @@ async fn main() -> Result<()> { ) .init(); - info!("GamePanel Daemon starting..."); + info!("GamePanel Daemon v{} starting...", env!("CARGO_PKG_VERSION")); + // Load config let config = config::DaemonConfig::load()?; info!(grpc_port = config.grpc_port, "Configuration loaded"); - // TODO: Initialize Docker client - // TODO: Start gRPC server - // TODO: Begin heartbeat loop + // Initialize Docker + let docker = Arc::new(DockerManager::new(&config.docker).await?); + info!("Docker manager initialized"); - info!("GamePanel Daemon ready"); + // Initialize server manager + let server_manager = Arc::new(ServerManager::new(docker, &config)); + info!("Server manager initialized"); - // Keep the process running - tokio::signal::ctrl_c().await?; - info!("Shutting down..."); + // Create gRPC service + let daemon_service = DaemonServiceImpl::new( + server_manager.clone(), + config.node_token.clone(), + ); + // Start gRPC server + let addr = format!("0.0.0.0:{}", config.grpc_port).parse()?; + info!(addr = %addr, "Starting gRPC server"); + + // Heartbeat task + let api_url = config.api_url.clone(); + let node_token = config.node_token.clone(); + let sm = server_manager.clone(); + tokio::spawn(async move { + heartbeat_loop(&api_url, &node_token, sm).await; + }); + + // Start serving + Server::builder() + .add_service(DaemonServiceServer::new(daemon_service)) + .serve_with_shutdown(addr, async { + tokio::signal::ctrl_c().await.ok(); + info!("Shutdown signal received"); + }) + .await?; + + info!("GamePanel Daemon stopped"); Ok(()) } + +/// Periodically report node status to the panel API. +async fn heartbeat_loop( + api_url: &str, + node_token: &str, + server_manager: Arc, +) { + let client = reqwest::Client::new(); + let heartbeat_url = format!("{}/api/nodes/heartbeat", api_url); + + loop { + tokio::time::sleep(tokio::time::Duration::from_secs(30)).await; + + let servers = server_manager.list_servers().await; + let active = servers + .iter() + .filter(|s| s.state.to_string() == "running") + .count(); + + let payload = serde_json::json!({ + "active_servers": active, + "total_servers": servers.len(), + "version": env!("CARGO_PKG_VERSION"), + }); + + match client + .post(&heartbeat_url) + .bearer_auth(node_token) + .json(&payload) + .send() + .await + { + Ok(resp) if resp.status().is_success() => { + tracing::debug!("Heartbeat sent successfully"); + } + Ok(resp) => { + tracing::warn!(status = %resp.status(), "Heartbeat failed"); + } + Err(e) => { + tracing::warn!(error = %e, "Heartbeat request failed"); + } + } + } +} diff --git a/apps/daemon/src/server/manager.rs b/apps/daemon/src/server/manager.rs new file mode 100644 index 0000000..866765e --- /dev/null +++ b/apps/daemon/src/server/manager.rs @@ -0,0 +1,230 @@ +use std::collections::HashMap; +use std::path::PathBuf; +use std::sync::Arc; +use tokio::sync::RwLock; +use tracing::{info, error, warn}; +use anyhow::Result; + +use crate::config::DaemonConfig; +use crate::docker::DockerManager; +use crate::error::DaemonError; +use super::state::{ServerState, ServerSpec, PortMap}; + +/// Manages all game server instances on this node. +pub struct ServerManager { + servers: Arc>>, + docker: Arc, + data_root: PathBuf, +} + +impl ServerManager { + pub fn new(docker: Arc, config: &DaemonConfig) -> Self { + Self { + servers: Arc::new(RwLock::new(HashMap::new())), + docker, + data_root: config.data_path.clone(), + } + } + + /// Get server spec by UUID. + pub async fn get_server(&self, uuid: &str) -> Result { + let servers = self.servers.read().await; + servers + .get(uuid) + .cloned() + .ok_or_else(|| DaemonError::ServerNotFound(uuid.to_string())) + } + + /// Get all servers. + pub async fn list_servers(&self) -> Vec { + let servers = self.servers.read().await; + servers.values().cloned().collect() + } + + /// Create a new game server. + pub async fn create_server( + &self, + uuid: String, + docker_image: String, + memory_limit: i64, + disk_limit: i64, + cpu_limit: i32, + startup_command: String, + environment: HashMap, + ports: Vec, + ) -> Result<(), DaemonError> { + let mut servers = self.servers.write().await; + if servers.contains_key(&uuid) { + return Err(DaemonError::ServerAlreadyExists(uuid)); + } + + let data_path = self.data_root.join(&uuid); + + // Create data directory + tokio::fs::create_dir_all(&data_path) + .await + .map_err(DaemonError::Io)?; + + let spec = ServerSpec { + uuid: uuid.clone(), + docker_image, + memory_limit, + disk_limit, + cpu_limit, + startup_command, + environment, + ports, + data_path, + state: ServerState::Installing, + container_id: None, + }; + + servers.insert(uuid.clone(), spec); + drop(servers); + + // Install server in background + let docker = self.docker.clone(); + let servers_ref = self.servers.clone(); + tokio::spawn(async move { + if let Err(e) = Self::install_server(docker, servers_ref.clone(), &uuid).await { + error!(uuid = %uuid, error = %e, "Server installation failed"); + let mut servers = servers_ref.write().await; + if let Some(spec) = servers.get_mut(&uuid) { + spec.state = ServerState::Error; + } + } + }); + + Ok(()) + } + + /// Install a server: pull image, create container. + async fn install_server( + docker: Arc, + servers: Arc>>, + uuid: &str, + ) -> Result<()> { + info!(uuid = %uuid, "Starting server installation"); + + let spec = { + let s = servers.read().await; + s.get(uuid).cloned().ok_or_else(|| anyhow::anyhow!("Server not found"))? + }; + + // Pull image + docker.pull_image(&spec.docker_image).await?; + + // Create container + let container_id = docker.create_container(&spec).await?; + + // Update state + let mut s = servers.write().await; + if let Some(server) = s.get_mut(uuid) { + server.container_id = Some(container_id); + server.state = ServerState::Stopped; + } + + info!(uuid = %uuid, "Server installation complete"); + Ok(()) + } + + /// Start a server. + pub async fn start_server(&self, uuid: &str) -> Result<(), DaemonError> { + let mut servers = self.servers.write().await; + let spec = servers + .get_mut(uuid) + .ok_or_else(|| DaemonError::ServerNotFound(uuid.to_string()))?; + + if !spec.can_transition_to(&ServerState::Starting) { + return Err(DaemonError::InvalidStateTransition { + current: spec.state.to_string(), + requested: "starting".to_string(), + }); + } + + spec.state = ServerState::Starting; + drop(servers); + + self.docker.start_container(uuid).await.map_err(|e| { + DaemonError::Internal(format!("Failed to start container: {}", e)) + })?; + + let mut servers = self.servers.write().await; + if let Some(spec) = servers.get_mut(uuid) { + spec.state = ServerState::Running; + } + + Ok(()) + } + + /// Stop a server. + pub async fn stop_server(&self, uuid: &str) -> Result<(), DaemonError> { + let mut servers = self.servers.write().await; + let spec = servers + .get_mut(uuid) + .ok_or_else(|| DaemonError::ServerNotFound(uuid.to_string()))?; + + if !spec.can_transition_to(&ServerState::Stopping) { + return Err(DaemonError::InvalidStateTransition { + current: spec.state.to_string(), + requested: "stopping".to_string(), + }); + } + + spec.state = ServerState::Stopping; + drop(servers); + + self.docker.stop_container(uuid, 30).await.map_err(|e| { + DaemonError::Internal(format!("Failed to stop container: {}", e)) + })?; + + let mut servers = self.servers.write().await; + if let Some(spec) = servers.get_mut(uuid) { + spec.state = ServerState::Stopped; + } + + Ok(()) + } + + /// Kill a server immediately. + pub async fn kill_server(&self, uuid: &str) -> Result<(), DaemonError> { + self.docker.kill_container(uuid).await.map_err(|e| { + DaemonError::Internal(format!("Failed to kill container: {}", e)) + })?; + + let mut servers = self.servers.write().await; + if let Some(spec) = servers.get_mut(uuid) { + spec.state = ServerState::Stopped; + } + + Ok(()) + } + + /// Delete a server and clean up. + pub async fn delete_server(&self, uuid: &str) -> Result<(), DaemonError> { + // Remove container if it exists + if let Err(e) = self.docker.remove_container(uuid).await { + warn!(uuid = %uuid, error = %e, "Failed to remove container (may not exist)"); + } + + // Remove from state + let mut servers = self.servers.write().await; + servers.remove(uuid); + + // Note: data directory is NOT deleted here for safety. + // Admin should explicitly clean up via API or manually. + + info!(uuid = %uuid, "Server deleted"); + Ok(()) + } + + /// Get the Docker manager Arc. + pub fn docker(&self) -> &Arc { + &self.docker + } + + /// Get the data root path. + pub fn data_root(&self) -> &PathBuf { + &self.data_root + } +} diff --git a/apps/daemon/src/server/mod.rs b/apps/daemon/src/server/mod.rs new file mode 100644 index 0000000..240198b --- /dev/null +++ b/apps/daemon/src/server/mod.rs @@ -0,0 +1,5 @@ +pub mod state; +pub mod manager; + +pub use state::{ServerSpec, PortMap}; +pub use manager::ServerManager; diff --git a/apps/daemon/src/server/state.rs b/apps/daemon/src/server/state.rs new file mode 100644 index 0000000..a086b47 --- /dev/null +++ b/apps/daemon/src/server/state.rs @@ -0,0 +1,69 @@ +use serde::{Deserialize, Serialize}; +use std::collections::HashMap; +use std::path::PathBuf; + +#[derive(Debug, Clone, Serialize, Deserialize, PartialEq)] +#[serde(rename_all = "snake_case")] +pub enum ServerState { + Installing, + Stopped, + Starting, + Running, + Stopping, + Error, +} + +impl std::fmt::Display for ServerState { + fn fmt(&self, f: &mut std::fmt::Formatter<'_>) -> std::fmt::Result { + match self { + Self::Installing => write!(f, "installing"), + Self::Stopped => write!(f, "stopped"), + Self::Starting => write!(f, "starting"), + Self::Running => write!(f, "running"), + Self::Stopping => write!(f, "stopping"), + Self::Error => write!(f, "error"), + } + } +} + +#[derive(Debug, Clone, Serialize, Deserialize)] +pub struct PortMap { + pub host_port: u16, + pub container_port: u16, + pub protocol: String, // "tcp" or "udp" +} + +#[derive(Debug, Clone, Serialize, Deserialize)] +pub struct ServerSpec { + pub uuid: String, + pub docker_image: String, + pub memory_limit: i64, // bytes + pub disk_limit: i64, // bytes + pub cpu_limit: i32, // percentage (100 = 1 core) + pub startup_command: String, + pub environment: HashMap, + pub ports: Vec, + pub data_path: PathBuf, + pub state: ServerState, + pub container_id: Option, +} + +impl ServerSpec { + /// Check if the server can transition to the requested state. + pub fn can_transition_to(&self, target: &ServerState) -> bool { + matches!( + (&self.state, target), + (ServerState::Installing, ServerState::Stopped) + | (ServerState::Installing, ServerState::Error) + | (ServerState::Stopped, ServerState::Starting) + | (ServerState::Starting, ServerState::Running) + | (ServerState::Starting, ServerState::Error) + | (ServerState::Running, ServerState::Stopping) + | (ServerState::Running, ServerState::Error) + | (ServerState::Stopping, ServerState::Stopped) + | (ServerState::Stopping, ServerState::Error) + | (ServerState::Error, ServerState::Starting) + | (ServerState::Error, ServerState::Stopped) + ) + } +}