diff --git a/.gitignore b/.gitignore index ea8c4bf..f3283df 100644 --- a/.gitignore +++ b/.gitignore @@ -1 +1,2 @@ /target +**/generated/*.rs diff --git a/Cargo.lock b/Cargo.lock index 9353994..c415d93 100644 --- a/Cargo.lock +++ b/Cargo.lock @@ -3,5 +3,2026 @@ version = 4 [[package]] -name = "ciron" +name = "aho-corasick" +version = "1.1.4" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "ddd31a130427c27518df266943a5308ed92d4b226cc639f5a8f1002816174301" +dependencies = [ + "memchr", +] + +[[package]] +name = "anstream" +version = "0.6.21" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "43d5b281e737544384e969a5ccad3f1cdd24b48086a0fc1b2a5262a26b8f4f4a" +dependencies = [ + "anstyle", + "anstyle-parse", + "anstyle-query", + "anstyle-wincon", + "colorchoice", + "is_terminal_polyfill", + "utf8parse", +] + +[[package]] +name = "anstyle" +version = "1.0.13" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "5192cca8006f1fd4f7237516f40fa183bb07f8fbdfedaa0036de5ea9b0b45e78" + +[[package]] +name = "anstyle-parse" +version = "0.2.7" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "4e7644824f0aa2c7b9384579234ef10eb7efb6a0deb83f9630a49594dd9c15c2" +dependencies = [ + "utf8parse", +] + +[[package]] +name = "anstyle-query" +version = "1.1.4" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "9e231f6134f61b71076a3eab506c379d4f36122f2af15a9ff04415ea4c3339e2" +dependencies = [ + "windows-sys 0.60.2", +] + +[[package]] +name = "anstyle-wincon" +version = "3.0.10" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "3e0633414522a32ffaac8ac6cc8f748e090c5717661fddeea04219e2344f5f2a" +dependencies = [ + "anstyle", + "once_cell_polyfill", + "windows-sys 0.60.2", +] + +[[package]] +name = "anyhow" +version = "1.0.100" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "a23eb6b1614318a8071c9b2521f36b424b2c83db5eb3a0fead4a6c0809af6e61" + +[[package]] +name = "arraydeque" +version = "0.5.1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "7d902e3d592a523def97af8f317b08ce16b7ab854c1985a0c671e6f15cebc236" + +[[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.2", + "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.21.7" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "9d297deb1925b89f2ccc13d7635fa0714f12c87adce1c75356b39ca9b7178567" + +[[package]] +name = "base64" +version = "0.22.1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "72b3254f16251a8381aa12e40e3c4d2f0199f8c6508fbecb9d91f575e0fbb8c6" + +[[package]] +name = "bitflags" +version = "2.10.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "812e12b5285cc515a9c72a5c1d3b6d46a19dac5acfef5265968c166106e31dd3" +dependencies = [ + "serde_core", +] + +[[package]] +name = "block-buffer" +version = "0.10.4" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "3078c7629b62d3f0439517fa394996acacc5cbc91c5a20d8c658e77abd503a71" +dependencies = [ + "generic-array", +] + +[[package]] +name = "bytes" +version = "1.10.1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "d71b6127be86fdcfddb610f7182ac57211d4b18a3e9c82eb2d17662f2227ad6a" + +[[package]] +name = "cfg-if" +version = "1.0.4" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "9330f8b2ff13f34540b44e946ef35111825727b38d33286ef986142615121801" + +[[package]] +name = "cfg_aliases" +version = "0.2.1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "613afe47fcd5fac7ccf1db93babcb082c5994d996f20b8b159f2ad1658eb5724" + +[[package]] +name = "ciron-common" version = "0.1.0" +dependencies = [ + "anyhow", + "config", + "prost", + "prost-types", + "serde", + "serde_json", + "tokio", + "tokio-vsock 0.7.2", + "tonic", + "tonic-build", +] + +[[package]] +name = "cironctl" +version = "0.1.0" +dependencies = [ + "anyhow", + "ciron-common", + "clap", + "hyper-util", + "prost", + "tokio", + "tokio-stream", + "tokio-vsock 0.5.0", + "tonic", + "tower 0.4.13", +] + +[[package]] +name = "cirond" +version = "0.1.0" +dependencies = [ + "anyhow", + "async-stream", + "ciron-common", + "clap", + "nix 0.27.1", + "prost", + "prost-types", + "serde", + "serde_json", + "shell-words", + "tokio", + "tokio-stream", + "tokio-vsock 0.5.0", + "tonic", + "tracing", + "tracing-subscriber", +] + +[[package]] +name = "clap" +version = "4.5.51" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "4c26d721170e0295f191a69bd9a1f93efcdb0aff38684b61ab5750468972e5f5" +dependencies = [ + "clap_builder", + "clap_derive", +] + +[[package]] +name = "clap_builder" +version = "4.5.51" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "75835f0c7bf681bfd05abe44e965760fea999a5286c6eb2d59883634fd02011a" +dependencies = [ + "anstream", + "anstyle", + "clap_lex", + "strsim", +] + +[[package]] +name = "clap_derive" +version = "4.5.49" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "2a0b5487afeab2deb2ff4e03a807ad1a03ac532ff5a2cee5d86884440c7f7671" +dependencies = [ + "heck", + "proc-macro2", + "quote", + "syn", +] + +[[package]] +name = "clap_lex" +version = "0.7.6" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "a1d728cc89cf3aee9ff92b05e62b19ee65a02b5702cff7d5a377e32c6ae29d8d" + +[[package]] +name = "colorchoice" +version = "1.0.4" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "b05b61dc5112cbb17e4b6cd61790d9845d13888356391624cbe7e41efeac1e75" + +[[package]] +name = "config" +version = "0.15.18" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "180e549344080374f9b32ed41bf3b6b57885ff6a289367b3dbc10eea8acc1918" +dependencies = [ + "async-trait", + "convert_case", + "json5", + "pathdiff", + "ron", + "rust-ini", + "serde-untagged", + "serde_core", + "serde_json", + "toml", + "winnow", + "yaml-rust2", +] + +[[package]] +name = "const-random" +version = "0.1.18" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "87e00182fe74b066627d63b85fd550ac2998d4b0bd86bfed477a0ae4c7c71359" +dependencies = [ + "const-random-macro", +] + +[[package]] +name = "const-random-macro" +version = "0.1.16" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "f9d839f2a20b0aee515dc581a6172f2321f96cab76c1a38a4c584a194955390e" +dependencies = [ + "getrandom 0.2.16", + "once_cell", + "tiny-keccak", +] + +[[package]] +name = "convert_case" +version = "0.6.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "ec182b0ca2f35d8fc196cf3404988fd8b8c739a4d270ff118a398feb0cbec1ca" +dependencies = [ + "unicode-segmentation", +] + +[[package]] +name = "cpufeatures" +version = "0.2.17" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "59ed5838eebb26a2bb2e58f6d5b5316989ae9d08bab10e0e6d103e656d1b0280" +dependencies = [ + "libc", +] + +[[package]] +name = "crunchy" +version = "0.2.4" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "460fbee9c2c2f33933d720630a6a0bac33ba7053db5344fac858d4b8952d77d5" + +[[package]] +name = "crypto-common" +version = "0.1.6" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "1bfb12502f3fc46cca1bb51ac28df9d618d813cdc3d2f25b9fe775a34af26bb3" +dependencies = [ + "generic-array", + "typenum", +] + +[[package]] +name = "digest" +version = "0.10.7" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "9ed9a281f7bc9b7576e61468ba615a66a5c8cfdff42420a70aa82701a3b1e292" +dependencies = [ + "block-buffer", + "crypto-common", +] + +[[package]] +name = "dlv-list" +version = "0.5.2" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "442039f5147480ba31067cb00ada1adae6892028e40e45fc5de7b7df6dcc1b5f" +dependencies = [ + "const-random", +] + +[[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 = "erased-serde" +version = "0.4.8" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "259d404d09818dec19332e31d94558aeb442fea04c817006456c24b5460bbd4b" +dependencies = [ + "serde", + "serde_core", + "typeid", +] + +[[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 = "fixedbitset" +version = "0.5.7" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "1d674e81391d1e1ab681a28d99df07927c6d4aa5b027d7da16ba32d1d21ecd99" + +[[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 = "futures" +version = "0.3.31" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "65bc07b1a8bc7c85c5f2e110c476c7389b4554ba72af57d8445ea63a576b0876" +dependencies = [ + "futures-channel", + "futures-core", + "futures-executor", + "futures-io", + "futures-sink", + "futures-task", + "futures-util", +] + +[[package]] +name = "futures-channel" +version = "0.3.31" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "2dff15bf788c671c1934e366d07e30c1814a8ef514e1af724a602e8a2fbe1b10" +dependencies = [ + "futures-core", + "futures-sink", +] + +[[package]] +name = "futures-core" +version = "0.3.31" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "05f29059c0c2090612e8d742178b0580d2dc940c837851ad723096f87af6663e" + +[[package]] +name = "futures-executor" +version = "0.3.31" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "1e28d1d997f585e54aebc3f97d39e72338912123a67330d723fdbb564d646c9f" +dependencies = [ + "futures-core", + "futures-task", + "futures-util", +] + +[[package]] +name = "futures-io" +version = "0.3.31" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "9e5c1b78ca4aae1ac06c48a526a655760685149f0d465d21f37abfe57ce075c6" + +[[package]] +name = "futures-macro" +version = "0.3.31" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "162ee34ebcb7c64a8abebc059ce0fee27c2262618d7b60ed8faf72fef13c3650" +dependencies = [ + "proc-macro2", + "quote", + "syn", +] + +[[package]] +name = "futures-sink" +version = "0.3.31" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "e575fab7d1e0dcb8d0c7bcf9a63ee213816ab51902e6d244a95819acacf1d4f7" + +[[package]] +name = "futures-task" +version = "0.3.31" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "f90f7dce0722e95104fcb095585910c0977252f286e354b5e3bd38902cd99988" + +[[package]] +name = "futures-util" +version = "0.3.31" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "9fa08315bb612088cc391249efdc3bc77536f16c91f6cf495e6fbe85b20a4a81" +dependencies = [ + "futures-channel", + "futures-core", + "futures-io", + "futures-macro", + "futures-sink", + "futures-task", + "memchr", + "pin-project-lite", + "pin-utils", + "slab", +] + +[[package]] +name = "generic-array" +version = "0.14.9" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "4bb6743198531e02858aeaea5398fcc883e71851fcbcb5a2f773e2fb6cb1edf2" +dependencies = [ + "typenum", + "version_check", +] + +[[package]] +name = "getrandom" +version = "0.2.16" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "335ff9f135e4384c8150d6f27c6daed433577f86b4750418338c01a1a2528592" +dependencies = [ + "cfg-if", + "libc", + "wasi", +] + +[[package]] +name = "getrandom" +version = "0.3.4" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "899def5c37c4fd7b2664648c28120ecec138e4d395b459e5ca34f9cce2dd77fd" +dependencies = [ + "cfg-if", + "libc", + "r-efi", + "wasip2", +] + +[[package]] +name = "h2" +version = "0.4.12" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "f3c0b69cfcb4e1b9f1bf2f53f95f766e4661169728ec61cd3fe5a0166f2d1386" +dependencies = [ + "atomic-waker", + "bytes", + "fnv", + "futures-core", + "futures-sink", + "http", + "indexmap 2.12.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.14.5" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "e5274423e17b7c9fc20b6e7e208532f9b19825d82dfd615708b70edd83df41f1" + +[[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.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "5419bdc4f6a9207fbeba6d11b604d481addf78ecd10c11ad51e76c2f6482748d" + +[[package]] +name = "hashlink" +version = "0.10.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "7382cf6263419f2d8df38c55d7da83da5c18aef87fc7a7fc1fb1e344edfe14c1" +dependencies = [ + "hashbrown 0.15.5", +] + +[[package]] +name = "heck" +version = "0.5.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "2304e00983f87ffb38b55b444b5e3b60a884b5d30c0fca7d82fe33449bbe55ea" + +[[package]] +name = "http" +version = "1.3.1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "f4a85d31aea989eead29a3aaf9e1115a180df8282431156e533de47660892565" +dependencies = [ + "bytes", + "fnv", + "itoa", +] + +[[package]] +name = "http-body" +version = "1.0.1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "1efedce1fb8e6913f23e0c92de8e62cd5b772a67e7b3946df930a62566c93184" +dependencies = [ + "bytes", + "http", +] + +[[package]] +name = "http-body-util" +version = "0.1.3" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "b021d93e26becf5dc7e1b75b1bed1fd93124b374ceb73f43d4d4eafec896a64a" +dependencies = [ + "bytes", + "futures-core", + "http", + "http-body", + "pin-project-lite", +] + +[[package]] +name = "httparse" +version = "1.10.1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "6dbf3de79e51f3d586ab4cb9d5c3e2c14aa28ed23d180cf89b4df0454a69cc87" + +[[package]] +name = "httpdate" +version = "1.0.3" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "df3b46402a9d5adb4c86a0cf463f42e19994e3ee891101b1841f30a545cb49a9" + +[[package]] +name = "hyper" +version = "1.7.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "eb3aa54a13a0dfe7fbe3a59e0c76093041720fdc77b110cc0fc260fafb4dc51e" +dependencies = [ + "atomic-waker", + "bytes", + "futures-channel", + "futures-core", + "h2", + "http", + "http-body", + "httparse", + "httpdate", + "itoa", + "pin-project-lite", + "pin-utils", + "smallvec", + "tokio", + "want", +] + +[[package]] +name = "hyper-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-util" +version = "0.1.17" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "3c6995591a8f1380fcb4ba966a252a4b29188d51d2b89e3a252f5305be65aea8" +dependencies = [ + "bytes", + "futures-channel", + "futures-core", + "futures-util", + "http", + "http-body", + "hyper", + "libc", + "pin-project-lite", + "socket2 0.6.1", + "tokio", + "tower-service", + "tracing", +] + +[[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", +] + +[[package]] +name = "indexmap" +version = "2.12.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "6717a8d2a5a929a1a2eb43a12812498ed141a0bcfb7e8f7844fbdbe4303bba9f" +dependencies = [ + "equivalent", + "hashbrown 0.16.0", +] + +[[package]] +name = "is_terminal_polyfill" +version = "1.70.2" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "a6cb138bb79a146c1bd460005623e142ef0181e3d0219cb493e02f7d08a35695" + +[[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.15" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "4a5f13b858c8d314ee3e8f639011f7ccefe71f97f96e50151fb991f267928e2c" + +[[package]] +name = "json5" +version = "0.4.1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "96b0db21af676c1ce64250b5f40f3ce2cf27e4e47cb91ed91eb6fe9350b430c1" +dependencies = [ + "pest", + "pest_derive", + "serde", +] + +[[package]] +name = "lazy_static" +version = "1.5.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "bbd2bcb4c963f2ddae06a2efc7e9f3591312473c50c6685e1f298068316e66fe" + +[[package]] +name = "libc" +version = "0.2.177" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "2874a2af47a2325c2001a6e6fad9b16a53b802102b528163885171cf92b15976" + +[[package]] +name = "linux-raw-sys" +version = "0.11.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "df1d3c3b53da64cf5760482273a98e575c651a67eec7f77df96b5b642de8f039" + +[[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.28" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "34080505efa8e45a4b816c349525ebe327ceaa8559756f0356cba97ef3bf7432" + +[[package]] +name = "matchit" +version = "0.7.3" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "0e7465ac9959cc2b1404e8e2367b43684a6d13790fe23056cc8c6c5a6b7bcb94" + +[[package]] +name = "memchr" +version = "2.7.6" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "f52b00d39961fc5b2736ea853c9cc86238e165017a493d1d5c8eac6bdc4cc273" + +[[package]] +name = "memoffset" +version = "0.9.1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "488016bfae457b036d996092f6cb448677611ce4449e970ceaf42695203f218a" +dependencies = [ + "autocfg", +] + +[[package]] +name = "mime" +version = "0.3.17" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "6877bb514081ee2a7ff5ef9de3281f14a4dd4bceac4c09388074a6b5df8a139a" + +[[package]] +name = "mio" +version = "1.1.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "69d83b0086dc8ecf3ce9ae2874b2d1290252e2a30720bea58a5c6639b0092873" +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 = "nix" +version = "0.27.1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "2eb04e9c688eff1c89d72b407f168cf79bb9e867a9d3323ed6c01519eb9cc053" +dependencies = [ + "bitflags", + "cfg-if", + "libc", + "memoffset", +] + +[[package]] +name = "nix" +version = "0.29.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "71e2746dc3a24dd78b3cfcb7be93368c6de9963d30f43a6a73998a9cf4b17b46" +dependencies = [ + "bitflags", + "cfg-if", + "cfg_aliases", + "libc", + "memoffset", +] + +[[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 = "once_cell" +version = "1.21.3" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "42f5e15c9953c5e4ccceeb2e7382a716482c34515315f7b03532b8b4e8393d2d" + +[[package]] +name = "once_cell_polyfill" +version = "1.70.2" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "384b8ab6d37215f3c5301a95a4accb5d64aa607f1fcb26a11b5303878451b4fe" + +[[package]] +name = "ordered-multimap" +version = "0.7.3" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "49203cdcae0030493bad186b28da2fa25645fa276a51b6fec8010d281e02ef79" +dependencies = [ + "dlv-list", + "hashbrown 0.14.5", +] + +[[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", + "smallvec", + "windows-link", +] + +[[package]] +name = "pathdiff" +version = "0.2.3" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "df94ce210e5bc13cb6651479fa48d14f601d9858cfe0467f43ae157023b938d3" + +[[package]] +name = "percent-encoding" +version = "2.3.2" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "9b4f627cb1b25917193a259e49bdad08f671f8d9708acfd5fe0a8c1455d87220" + +[[package]] +name = "pest" +version = "2.8.3" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "989e7521a040efde50c3ab6bbadafbe15ab6dc042686926be59ac35d74607df4" +dependencies = [ + "memchr", + "ucd-trie", +] + +[[package]] +name = "pest_derive" +version = "2.8.3" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "187da9a3030dbafabbbfb20cb323b976dc7b7ce91fcd84f2f74d6e31d378e2de" +dependencies = [ + "pest", + "pest_generator", +] + +[[package]] +name = "pest_generator" +version = "2.8.3" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "49b401d98f5757ebe97a26085998d6c0eecec4995cad6ab7fc30ffdf4b052843" +dependencies = [ + "pest", + "pest_meta", + "proc-macro2", + "quote", + "syn", +] + +[[package]] +name = "pest_meta" +version = "2.8.3" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "72f27a2cfee9f9039c4d86faa5af122a0ac3851441a34865b8a043b46be0065a" +dependencies = [ + "pest", + "sha2", +] + +[[package]] +name = "petgraph" +version = "0.7.1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "3672b37090dbd86368a4145bc067582552b29c27377cad4e0a306c97f9bd7772" +dependencies = [ + "fixedbitset", + "indexmap 2.12.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 = "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.103" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "5ee95bc4ef87b8d5ba32e8b7714ccc834865276eab0aed5c9958d00ec45f49e8" +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.41" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "ce25767e7b499d1b604768e7cde645d14cc8584231ea6b295e9c9eb22c02e1d1" +dependencies = [ + "proc-macro2", +] + +[[package]] +name = "r-efi" +version = "5.3.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "69cdb34c158ceb288df11e18b4bd39de994f6657d83847bdffdbd7f346754b0f" + +[[package]] +name = "rand" +version = "0.8.5" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "34af8d1a0e25924bc5b7c43c079c942339d8f0a8b57c39049bef581b46327404" +dependencies = [ + "libc", + "rand_chacha", + "rand_core", +] + +[[package]] +name = "rand_chacha" +version = "0.3.1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "e6c10a63a0fa32252be49d21e7709d4d4baf8d231c2dbce1eaa8141b9b127d88" +dependencies = [ + "ppv-lite86", + "rand_core", +] + +[[package]] +name = "rand_core" +version = "0.6.4" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "ec0be4795e2f6a28069bec0b5ff3e2ac9bafc99e6a9a7dc3547996c5c816922c" +dependencies = [ + "getrandom 0.2.16", +] + +[[package]] +name = "redox_syscall" +version = "0.5.18" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "ed2bf2547551a7053d6fdfafda3f938979645c44812fbfcda098faae3f1a362d" +dependencies = [ + "bitflags", +] + +[[package]] +name = "regex" +version = "1.12.2" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "843bc0191f75f3e22651ae5f1e72939ab2f72a4bc30fa80a066bd66edefc24d4" +dependencies = [ + "aho-corasick", + "memchr", + "regex-automata", + "regex-syntax", +] + +[[package]] +name = "regex-automata" +version = "0.4.13" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "5276caf25ac86c8d810222b3dbb938e512c55c6831a10f3e6ed1c93b84041f1c" +dependencies = [ + "aho-corasick", + "memchr", + "regex-syntax", +] + +[[package]] +name = "regex-syntax" +version = "0.8.8" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "7a2d987857b319362043e95f5353c0535c1f58eec5336fdfcf626430af7def58" + +[[package]] +name = "ron" +version = "0.8.1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "b91f7eff05f748767f183df4320a63d6936e9c6107d97c9e6bdd9784f4289c94" +dependencies = [ + "base64 0.21.7", + "bitflags", + "serde", + "serde_derive", +] + +[[package]] +name = "rust-ini" +version = "0.21.3" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "796e8d2b6696392a43bea58116b667fb4c29727dc5abd27d6acf338bb4f688c7" +dependencies = [ + "cfg-if", + "ordered-multimap", +] + +[[package]] +name = "rustix" +version = "1.1.2" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "cd15f8a2c5551a84d56efdc1cd049089e409ac19a3072d5037a17fd70719ff3e" +dependencies = [ + "bitflags", + "errno", + "libc", + "linux-raw-sys", + "windows-sys 0.61.2", +] + +[[package]] +name = "rustversion" +version = "1.0.22" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "b39cdef0fa800fc44525c84ccb54a029961a8215f9619753635a9c0d2538d46d" + +[[package]] +name = "ryu" +version = "1.0.20" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "28d3b2b1366ec20994f1fd18c3c594f05c5dd4bc44d8bb0c1c632c8d6829481f" + +[[package]] +name = "scopeguard" +version = "1.2.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "94143f37725109f92c262ed2cf5e59bce7498c01bcc1502d7b9afe439a4e9f49" + +[[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-untagged" +version = "0.1.9" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "f9faf48a4a2d2693be24c6289dbe26552776eb7737074e6722891fadbe6c5058" +dependencies = [ + "erased-serde", + "serde", + "serde_core", + "typeid", +] + +[[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.145" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "402a6f66d8c709116cf22f558eab210f5a50187f702eb4d7e5ef38d9a7f1c79c" +dependencies = [ + "itoa", + "memchr", + "ryu", + "serde", + "serde_core", +] + +[[package]] +name = "serde_spanned" +version = "1.0.3" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "e24345aa0fe688594e73770a5f6d1b216508b4f93484c0026d521acd30134392" +dependencies = [ + "serde_core", +] + +[[package]] +name = "sha2" +version = "0.10.9" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "a7507d819769d01a365ab707794a4084392c824f54a7a6a7862f8c3d0892b283" +dependencies = [ + "cfg-if", + "cpufeatures", + "digest", +] + +[[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 = "shell-words" +version = "1.1.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "24188a676b6ae68c3b2cb3a01be17fbf7240ce009799bb56d5b1409051e78fde" + +[[package]] +name = "signal-hook-registry" +version = "1.4.6" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "b2a4719bff48cee6b39d12c020eeb490953ad2443b7055bd0b21fca26bd8c28b" +dependencies = [ + "libc", +] + +[[package]] +name = "slab" +version = "0.4.11" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "7a2ae44ef20feb57a68b23d846850f861394c2e02dc425a50098ae8c90267589" + +[[package]] +name = "smallvec" +version = "1.15.1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "67b1b7a3b5fe4f1376887184045fcf45c69e92af734b7aaddc05fb777b6fbd03" + +[[package]] +name = "socket2" +version = "0.5.10" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "e22376abed350d73dd1cd119b57ffccad95b4e585a7cda43e286245ce23c0678" +dependencies = [ + "libc", + "windows-sys 0.52.0", +] + +[[package]] +name = "socket2" +version = "0.6.1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "17129e116933cf371d018bb80ae557e889637989d8638274fb25622827b03881" +dependencies = [ + "libc", + "windows-sys 0.60.2", +] + +[[package]] +name = "strsim" +version = "0.11.1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "7da8b5736845d9f2fcb837ea5d9e2628564b3b043a70948a3f0b778838c5fb4f" + +[[package]] +name = "syn" +version = "2.0.108" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "da58917d35242480a05c2897064da0a80589a2a0476c9a3f2fdc83b53502e917" +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" + +[[package]] +name = "tempfile" +version = "3.23.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "2d31c77bdf42a745371d260a26ca7163f1e0924b64afa0b688e61b5a9fa02f16" +dependencies = [ + "fastrand", + "getrandom 0.3.4", + "once_cell", + "rustix", + "windows-sys 0.61.2", +] + +[[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 = "tiny-keccak" +version = "2.0.2" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "2c9d3793400a45f954c52e73d068316d76b6f4e36977e3fcebb13a2721e80237" +dependencies = [ + "crunchy", +] + +[[package]] +name = "tokio" +version = "1.48.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "ff360e02eab121e0bc37a2d3b4d4dc622e6eda3a8e5253d5435ecf5bd4c68408" +dependencies = [ + "bytes", + "libc", + "mio", + "parking_lot", + "pin-project-lite", + "signal-hook-registry", + "socket2 0.6.1", + "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-stream" +version = "0.1.17" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "eca58d7bba4a75707817a2c44174253f9236b2d5fbd055602e9d5c07c139a047" +dependencies = [ + "futures-core", + "pin-project-lite", + "tokio", +] + +[[package]] +name = "tokio-util" +version = "0.7.16" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "14307c986784f72ef81c89db7d9e28d6ac26d16213b109ea501696195e6e3ce5" +dependencies = [ + "bytes", + "futures-core", + "futures-sink", + "pin-project-lite", + "tokio", +] + +[[package]] +name = "tokio-vsock" +version = "0.5.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "2e336ac4b36df625d5429a735dd5847732fe5f62010e3ce0c50f3705d44730f8" +dependencies = [ + "bytes", + "futures", + "libc", + "tokio", + "vsock 0.4.0", +] + +[[package]] +name = "tokio-vsock" +version = "0.7.2" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "8b319ef9394889dab2e1b4f0085b45ba11d0c79dc9d1a9d1afc057d009d0f1c7" +dependencies = [ + "bytes", + "futures", + "libc", + "tokio", + "vsock 0.5.1", +] + +[[package]] +name = "toml" +version = "0.9.8" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "f0dc8b1fb61449e27716ec0e1bdf0f6b8f3e8f6b05391e8497b8b6d7804ea6d8" +dependencies = [ + "serde_core", + "serde_spanned", + "toml_datetime", + "toml_parser", + "winnow", +] + +[[package]] +name = "toml_datetime" +version = "0.7.3" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "f2cdb639ebbc97961c51720f858597f7f24c4fc295327923af55b74c3c724533" +dependencies = [ + "serde_core", +] + +[[package]] +name = "toml_parser" +version = "1.0.4" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "c0cbe268d35bdb4bb5a56a2de88d0ad0eb70af5384a99d648cd4b3d04039800e" +dependencies = [ + "winnow", +] + +[[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 0.22.1", + "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.2" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "d039ad9159c98b70ecfd540b2573b97f7f52c3e8d9f8ad57a24b916a536975f9" +dependencies = [ + "futures-core", + "futures-util", + "pin-project-lite", + "sync_wrapper", + "tower-layer", + "tower-service", +] + +[[package]] +name = "tower-layer" +version = "0.3.3" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "121c2a6cda46980bb0fcd1647ffaf6cd3fc79a013de288782836f6df9c48780e" + +[[package]] +name = "tower-service" +version = "0.3.3" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "8df9b6e13f2d32c91b9bd719c00d1958837bc7dec474d94952798cc8e69eeec3" + +[[package]] +name = "tracing" +version = "0.1.41" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "784e0ac535deb450455cbfa28a6f0df145ea1bb7ae51b821cf5e7927fdcfbdd0" +dependencies = [ + "log", + "pin-project-lite", + "tracing-attributes", + "tracing-core", +] + +[[package]] +name = "tracing-attributes" +version = "0.1.30" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "81383ab64e72a7a8b8e13130c49e3dab29def6d0c7d76a03087b3cf71c5c6903" +dependencies = [ + "proc-macro2", + "quote", + "syn", +] + +[[package]] +name = "tracing-core" +version = "0.1.34" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "b9d12581f227e93f094d3af2ae690a574abb8a2b9b7a96e7cfe9647b2b617678" +dependencies = [ + "once_cell", + "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-subscriber" +version = "0.3.20" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "2054a14f5307d601f88daf0553e1cbf472acc4f2c51afab632431cdcd72124d5" +dependencies = [ + "nu-ansi-term", + "sharded-slab", + "smallvec", + "thread_local", + "tracing-core", + "tracing-log", +] + +[[package]] +name = "try-lock" +version = "0.2.5" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "e421abadd41a4225275504ea4d6566923418b7f05506fbc9c0fe86ba7396114b" + +[[package]] +name = "typeid" +version = "1.0.3" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "bc7d623258602320d5c55d1bc22793b57daff0ec7efc270ea7d55ce1d5f5471c" + +[[package]] +name = "typenum" +version = "1.19.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "562d481066bde0658276a35467c4af00bdc6ee726305698a55b86e61d7ad82bb" + +[[package]] +name = "ucd-trie" +version = "0.1.7" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "2896d95c02a80c6d6a5d6e953d479f5ddf2dfdb6a244441010e373ac0fb88971" + +[[package]] +name = "unicode-ident" +version = "1.0.20" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "462eeb75aeb73aea900253ce739c8e18a67423fadf006037cd3ff27e82748a06" + +[[package]] +name = "unicode-segmentation" +version = "1.12.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "f6ccf251212114b54433ec949fd6a7841275f9ada20dddd2f29e9ceea4501493" + +[[package]] +name = "utf8parse" +version = "0.2.2" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "06abde3611657adf66d383f00b093d7faecc7fa57071cce2578660c9f1010821" + +[[package]] +name = "valuable" +version = "0.1.1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "ba73ea9cf16a25df0c8caa16c51acb937d5712a8429db78a3ee29d5dcacd3a65" + +[[package]] +name = "version_check" +version = "0.9.5" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "0b928f33d975fc6ad9f86c8f283853ad26bdd5b10b7f1542aa2fa15e2289105a" + +[[package]] +name = "vsock" +version = "0.4.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "2dfb6e7a74830912f1f4a7655227c9ded1ea4e9136676311fedf54bedb412f35" +dependencies = [ + "libc", + "nix 0.27.1", +] + +[[package]] +name = "vsock" +version = "0.5.1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "4e8b4d00e672f147fc86a09738fadb1445bd1c0a40542378dfb82909deeee688" +dependencies = [ + "libc", + "nix 0.29.0", +] + +[[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.1+wasi-0.2.4" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "0562428422c63773dad2c345a1882263bbf4d65cf3f42e90921f787ef5ad58e7" +dependencies = [ + "wit-bindgen", +] + +[[package]] +name = "windows-link" +version = "0.2.1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "f0805222e57f7521d6a62e36fa9163bc891acd422f971defe97d64e70d0a4fe5" + +[[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 = "winnow" +version = "0.7.13" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "21a0236b59786fed61e2a80582dd500fe61f18b5dca67a4a067d0bc9039339cf" +dependencies = [ + "memchr", +] + +[[package]] +name = "wit-bindgen" +version = "0.46.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "f17a85883d4e6d00e8a97c586de764dabcc06133f7f1d55dce5cdc070ad7fe59" + +[[package]] +name = "yaml-rust2" +version = "0.10.4" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "2462ea039c445496d8793d052e13787f2b90e750b833afee748e601c17621ed9" +dependencies = [ + "arraydeque", + "encoding_rs", + "hashlink", +] + +[[package]] +name = "zerocopy" +version = "0.8.27" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "0894878a5fa3edfd6da3f88c4805f4c8558e2b996227a3d864f47fe11e38282c" +dependencies = [ + "zerocopy-derive", +] + +[[package]] +name = "zerocopy-derive" +version = "0.8.27" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "88d2b8d9c68ad2b9e4340d7832716a4d21a22a1154777ad56ea55c51a9cf3831" +dependencies = [ + "proc-macro2", + "quote", + "syn", +] diff --git a/Cargo.toml b/Cargo.toml index 5c2e490..b875173 100644 --- a/Cargo.toml +++ b/Cargo.toml @@ -1,6 +1,27 @@ -[package] -name = "ciron" +[workspace] +members = ["cirond", "cironctl", "ciron-common"] +resolver = "2" + +[workspace.package] version = "0.1.0" edition = "2024" +authors = ["Zaptoss"] +license = "MIT" + +[workspace.dependencies] +anyhow = "1.0.100" +config = "0.15.18" +serde = { version = "1.0.228", features = ["derive"] } +serde_json = "1.0" +tokio = { version = "1", features = ["full"] } +tokio-stream = "0.1" +tracing = "0.1" +tracing-subscriber = "0.3" +clap = { version = "4", features = ["derive"] } +shell-words = "1.1" -[dependencies] +# gRPC dependencies +tonic = "0.12" +tonic-build = "0.12" +prost = "0.13" +prost-types = "0.13" diff --git a/README.md b/README.md new file mode 100644 index 0000000..52d2b66 --- /dev/null +++ b/README.md @@ -0,0 +1,120 @@ +# Ciron - Process Manager Daemon + +A lightweight process manager daemon written in Rust. Ciron allows you to manage, monitor, and automatically restart processes. + +Inspired by [Supervisor](https://supervisord.org/) and designed for cloud-native environments with support for microVMs and vsock communication. + +## Use Cases + +While it may seem counterintuitive to run multiple processes in a single container (containers are typically designed for one process), Ciron excels in specific scenarios: + +- **CTF Competitions**: Package complete challenge environments (web server + database + services) in a single container +- **MicroVMs**: Manage multiple services in lightweight VMs like Firecracker with native vsock support for minimal overhead +- **Legacy Applications**: Run traditional applications that bundle multiple services (e.g., Apache + PHP-FPM) + +## Components + +- **cirond**: The daemon that manages processes +- **cironctl**: CLI client to control the daemon +- **ciron-common**: Shared library with gRPC definitions and utilities + +## Building + +```bash +cargo build --release +``` + +## Running + +Start the daemon: +```bash +./target/release/cirond -c ciron.toml +``` + +Control processes: +```bash +./target/release/cironctl -t unix:///tmp/cirond.sock status +./target/release/cironctl -t inet://127.0.0.1:50051 start web +./target/release/cironctl -t vsock://2:50051 stop sleep +``` + +## Running in Docker + +Build the Docker image: +```bash +docker build -t cirond:latest . +``` + +Run the daemon in a container: +```bash +docker run --rm -p 50051:50051 \ + -v $(pwd)/ciron.docker.toml:/etc/ciron/ciron.toml \ + -v $(pwd)/loop_app.sh:/opt/loop_app.sh \ + cirond:latest +``` + +Control processes from host: +```bash +./target/debug/cironctl -t inet://127.0.0.1:50051 status +``` + +## Examples + +See the `examples/` directory for complete working examples: + +- **[basic](examples/basic/)** - Simple shell commands to get started +- **[nginx-webapp](examples/nginx-webapp/)** - Multi-process setup with Nginx and Python web application + +## TODO + +- [ ] Add comprehensive logging with log rotation +- [ ] Implement process resource limits (CPU, memory) +- [ ] Add process dependency management +- [ ] Implement health checks for monitored processes +- [ ] Support for process groups +- [ ] Signal handling for graceful shutdown +- [ ] Process output capture and log management +- [ ] Web UI for process monitoring +- [ ] `systemd` integration +- [ ] Configuration hot-reload +- [ ] Add unit and integration tests +- [ ] Create Firecracker/microVM testing environment for vsock +- [ ] Documentation improvements and examples + +## Configuration Example +``` +log_level = "info" + +# Transport configuration +[transport] +enable_unix = true +# Unix socket path (default: /tmp/cirond.sock) +unix_socket_path = "/tmp/cirond.sock" +# Enable inet socket (default: false) +enable_inet = false +# Inet address (default: 127.0.0.1:50051) +inet_address = "127.0.0.1:50051" +# Enable vsock (default: false, Linux only) +enable_vsock = false +# Vsock CID (Context ID) - use 2 for host, or specific VM CID +vsock_cid = 2 +# Vsock port +vsock_port = 50051 + +[program.web] +command = "./webapp" +autostart = true +restart = "always" + +[program.web.env] +PORT = "8080" +DEBUG = "true" + +[program.sleep] +command = "sleep 5" +autostart = true +restart = "on-failure" +``` + + + diff --git a/ciron-common/Cargo.toml b/ciron-common/Cargo.toml new file mode 100644 index 0000000..c59a9c7 --- /dev/null +++ b/ciron-common/Cargo.toml @@ -0,0 +1,20 @@ +[package] +name = "ciron-common" +version.workspace = true +edition.workspace = true +authors.workspace = true +license.workspace = true + +[dependencies] +anyhow.workspace = true +config.workspace = true +serde.workspace = true +serde_json.workspace = true +tonic.workspace = true +prost.workspace = true +prost-types.workspace = true +tokio.workspace = true +tokio-vsock = "0.7.2" + +[build-dependencies] +tonic-build.workspace = true diff --git a/ciron-common/build.rs b/ciron-common/build.rs new file mode 100644 index 0000000..f1c9d5d --- /dev/null +++ b/ciron-common/build.rs @@ -0,0 +1,14 @@ +fn main() -> Result<(), Box> { + std::fs::create_dir_all("src/generated")?; + + tonic_build::configure() + .build_server(true) + .build_client(true) + .out_dir("src/generated") + .compile_protos( + &["../proto/ciron.proto", "../proto/errors.proto"], + &["../proto"], + )?; + + Ok(()) +} diff --git a/ciron-common/src/config.rs b/ciron-common/src/config.rs new file mode 100644 index 0000000..4694e26 --- /dev/null +++ b/ciron-common/src/config.rs @@ -0,0 +1,85 @@ +use anyhow::Result; +use config::{Config, File}; +use serde::{Deserialize, Serialize}; +use std::collections::HashMap; + +#[derive(Debug, Deserialize, Serialize)] +pub struct GlobalConfig { + #[serde(default)] + pub log_level: Option, + #[serde(default)] + pub transport: TransportConfig, + pub program: HashMap, +} + +#[derive(Debug, Deserialize, Serialize)] +pub struct TransportConfig { + #[serde(default = "default_true")] + pub enable_unix: bool, + #[serde(default = "default_unix_socket_path")] + pub unix_socket_path: String, + #[serde(default)] + pub enable_inet: bool, + #[serde(default = "default_inet_address")] + pub inet_address: String, + #[serde(default)] + pub enable_vsock: bool, + #[serde(default = "default_vsock_cid")] + pub vsock_cid: u32, + #[serde(default = "default_vsock_port")] + pub vsock_port: u32, +} + +impl Default for TransportConfig { + fn default() -> Self { + Self { + enable_unix: true, + unix_socket_path: default_unix_socket_path(), + enable_inet: false, + inet_address: default_inet_address(), + enable_vsock: false, + vsock_cid: default_vsock_cid(), + vsock_port: default_vsock_port(), + } + } +} + +fn default_true() -> bool { + true +} + +fn default_unix_socket_path() -> String { + "/tmp/cirond.sock".to_string() +} + +fn default_inet_address() -> String { + "127.0.0.1:50051".to_string() +} + +fn default_vsock_cid() -> u32 { + u32::MAX // All CIDs +} + +fn default_vsock_port() -> u32 { + 50051 +} + +#[derive(Debug, Deserialize, Serialize)] +pub struct ProgramConfig { + pub command: String, + #[serde(default)] + pub autostart: bool, + #[serde(default)] + pub restart: Option, + #[serde(default)] + pub env: Option>, +} + +pub fn load_config(path: &str) -> Result { + let settings = Config::builder() + .add_source(File::with_name(path)) + .build()?; + + let config: GlobalConfig = settings.try_deserialize()?; + Ok(config) +} diff --git a/ciron-common/src/lib.rs b/ciron-common/src/lib.rs new file mode 100644 index 0000000..17a29e8 --- /dev/null +++ b/ciron-common/src/lib.rs @@ -0,0 +1,16 @@ +pub mod config; +pub mod transport; + +// Generated gRPC code +pub mod generated { + pub mod ciron_v1 { + include!("generated/ciron.v1.rs"); + } +} + +pub use config::{GlobalConfig, ProgramConfig, TransportConfig, load_config}; +pub use transport::Transport; + +pub use generated::ciron_v1::ciron_daemon_client::CironDaemonClient; +pub use generated::ciron_v1::ciron_daemon_server::{CironDaemon, CironDaemonServer}; +pub use generated::ciron_v1::*; diff --git a/ciron-common/src/transport.rs b/ciron-common/src/transport.rs new file mode 100644 index 0000000..269a1f4 --- /dev/null +++ b/ciron-common/src/transport.rs @@ -0,0 +1,117 @@ +use anyhow::{Context, Result}; +use std::path::PathBuf; +use tokio_vsock::VMADDR_CID_ANY; + +#[derive(Debug, Clone)] +pub enum Transport { + Inet { host: String, port: u16 }, + Unix { path: PathBuf }, + Vsock { cid: u32, port: u32 }, +} + +impl Transport { + pub fn default_inet() -> Self { + Transport::Inet { + host: "127.0.0.1".to_string(), + port: 50051, + } + } + + pub fn default_unix() -> Self { + Transport::Unix { + path: PathBuf::from("/tmp/cirond.sock"), + } + } + + pub fn default_vsock() -> Self { + Transport::Vsock { + cid: VMADDR_CID_ANY, + port: 50051, + } + } + + /// Parse transport from string + /// + /// Formats: + /// - `inet://host:port` or `tcp://host:port` + /// - `unix:///path/to/socket` + /// - `vsock://cid:port` + pub fn parse(s: &str) -> Result { + if let Some(addr) = s + .strip_prefix("inet://") + .or_else(|| s.strip_prefix("tcp://")) + { + let parts: Vec<&str> = addr.split(':').collect(); + if parts.len() != 2 { + anyhow::bail!("Invalid inet address format. Expected host:port"); + } + let host = parts[0].to_string(); + let port = parts[1].parse().context("Invalid port number")?; + Ok(Transport::Inet { host, port }) + } else if let Some(path) = s.strip_prefix("unix://") { + Ok(Transport::Unix { + path: PathBuf::from(path), + }) + } else if let Some(addr) = s.strip_prefix("vsock://") { + let parts: Vec<&str> = addr.split(':').collect(); + if parts.len() != 2 { + anyhow::bail!("Invalid vsock address format. Expected cid:port"); + } + let cid = parts[0].parse().context("Invalid CID")?; + let port = parts[1].parse().context("Invalid port")?; + Ok(Transport::Vsock { cid, port }) + } else { + anyhow::bail!("Unknown transport type. Use inet://, unix://, or vsock:// prefix") + } + } +} + +impl std::fmt::Display for Transport { + fn fmt(&self, f: &mut std::fmt::Formatter<'_>) -> std::fmt::Result { + match self { + Transport::Inet { host, port } => write!(f, "inet://{}:{}", host, port), + Transport::Unix { path } => write!(f, "unix://{}", path.display()), + Transport::Vsock { cid, port } => write!(f, "vsock://{}:{}", cid, port), + } + } +} + +#[cfg(test)] +mod tests { + use super::*; + + #[test] + fn test_parse_inet() { + let transport = Transport::parse("inet://127.0.0.1:8080").unwrap(); + match transport { + Transport::Inet { host, port } => { + assert_eq!(host, "127.0.0.1"); + assert_eq!(port, 8080); + } + _ => panic!("Expected Inet transport"), + } + } + + #[test] + fn test_parse_unix() { + let transport = Transport::parse("unix:///tmp/test.sock").unwrap(); + match transport { + Transport::Unix { path } => { + assert_eq!(path, PathBuf::from("/tmp/test.sock")); + } + _ => panic!("Expected Unix transport"), + } + } + + #[test] + fn test_parse_vsock() { + let transport = Transport::parse("vsock://2:9000").unwrap(); + match transport { + Transport::Vsock { cid, port } => { + assert_eq!(cid, 2); + assert_eq!(port, 9000); + } + _ => panic!("Expected Vsock transport"), + } + } +} diff --git a/ciron.toml b/ciron.toml new file mode 100644 index 0000000..94535c0 --- /dev/null +++ b/ciron.toml @@ -0,0 +1,15 @@ +log_level = "info" + +[transport] +enable_unix = true +unix_socket_path = "/tmp/cirond.sock" +enable_inet = false +inet_address = "127.0.0.1:50051" +enable_vsock = false +vsock_cid = 2 +vsock_port = 50051 + +[program.sleep] +command = "sleep 5" +autostart = true +restart = "on-failure" diff --git a/cironctl/Cargo.toml b/cironctl/Cargo.toml new file mode 100644 index 0000000..0a43e8b --- /dev/null +++ b/cironctl/Cargo.toml @@ -0,0 +1,24 @@ +[package] +name = "cironctl" +version.workspace = true +edition.workspace = true +authors.workspace = true +license.workspace = true + +[[bin]] +name = "cironctl" +path = "src/main.rs" + +[dependencies] +ciron-common = { path = "../ciron-common" } +anyhow = "1.0" +clap = { version = "4.0", features = ["derive"] } +tokio = { version = "1", features = ["full"] } +tonic = "0.12" +prost = "0.13" +tokio-stream = "0.1" +tower = "0.4" +hyper-util = { version = "0.1", features = ["tokio"] } + +[target.'cfg(target_os = "linux")'.dependencies] +tokio-vsock = "0.5" diff --git a/cironctl/src/main.rs b/cironctl/src/main.rs new file mode 100644 index 0000000..b442f15 --- /dev/null +++ b/cironctl/src/main.rs @@ -0,0 +1,235 @@ +use anyhow::{Context, Result}; +use ciron_common::{ + CironDaemonClient, GetStatusRequest, RestartProcessRequest, StartProcessRequest, + StopProcessRequest, Transport, +}; +use clap::{Parser, Subcommand}; + +#[cfg(unix)] +use {hyper_util::rt::TokioIo, tokio::net::UnixStream, tower::service_fn}; + +#[cfg(target_os = "linux")] +use tokio_vsock::{VsockAddr, VsockStream}; + +#[derive(Parser)] +#[command(name = "cironctl")] +#[command(about = "Control utility for Cirond daemon", long_about = None)] +#[command(version)] +struct Cli { + #[arg(short, long, default_value = "unix:///tmp/cirond.sock")] + transport: String, + + #[command(subcommand)] + command: Commands, +} + +#[derive(Subcommand)] +enum Commands { + /// Show status of all processes + Status, + + /// Start a process + Start { name: String }, + + /// Stop a process + Stop { + name: String, + #[arg(short, long)] + force: bool, + }, + + /// Restart a process + Restart { name: String }, + + /// Show logs of a process + Logs { + name: String, + #[arg(short, long)] + follow: bool, + /// Number of lines to show + #[arg(short, long, default_value = "50")] + lines: i32, + }, + + /// Reload daemon configuration + Reload, + + /// Stop all processes and shutdown daemon + Shutdown { + #[arg(short, long, default_value = "true")] + graceful: bool, + + /// Timeout in seconds + #[arg(short, long)] + timeout: Option, + }, +} + +#[tokio::main] +async fn main() -> Result<()> { + let cli = Cli::parse(); + + // Parse transport + let transport = + Transport::parse(&cli.transport).context("Failed to parse transport address")?; + + // Connect to daemon based on transport type + let mut client = match transport { + Transport::Inet { ref host, port } => { + let uri = format!("http://{}:{}", host, port); + CironDaemonClient::connect(uri) + .await + .context("Failed to connect to cirond. Is the daemon running?")? + } + #[cfg(unix)] + Transport::Unix { ref path } => { + let path = path.clone(); + + // Create a channel that connects via Unix socket + let channel = tonic::transport::Endpoint::try_from("http://[::]:50051")? + .connect_with_connector(service_fn(move |_: tonic::transport::Uri| { + let path = path.clone(); + async move { + UnixStream::connect(path) + .await + .map(TokioIo::new) + .map_err(std::io::Error::other) + } + })) + .await + .context("Failed to connect to cirond via Unix socket. Is the daemon running?")?; + + CironDaemonClient::new(channel) + } + #[cfg(not(unix))] + Transport::Unix { .. } => { + anyhow::bail!("Unix sockets not supported on this platform"); + } + #[cfg(target_os = "linux")] + Transport::Vsock { cid, port } => { + // Create a channel that connects via Vsock + let channel = tonic::transport::Endpoint::try_from("http://[::]:50051")? + .connect_with_connector(service_fn(move |_: tonic::transport::Uri| async move { + let addr = VsockAddr::new(cid, port); + VsockStream::connect(addr) + .await + .map(TokioIo::new) + .map_err(std::io::Error::other) + })) + .await + .context("Failed to connect to cirond via Vsock. Is the daemon running?")?; + + CironDaemonClient::new(channel) + } + #[cfg(not(target_os = "linux"))] + Transport::Vsock { .. } => { + anyhow::bail!("Vsock transport not supported on this platform (Linux only)"); + } + }; + + match cli.command { + Commands::Status => { + let response = client + .get_status(GetStatusRequest {}) + .await + .context("Failed to get status")?; + + let resp = response.into_inner(); + + println!("Cirond Status"); + println!("─────────────────────────────────────────"); + + if let Some(daemon_info) = resp.daemon_info { + println!("Version: {}", daemon_info.version); + println!("Uptime: {}s", daemon_info.uptime_seconds); + println!("Config: {}", daemon_info.config_file); + println!( + "Processes: {} total, {} running, {} stopped", + daemon_info.total_processes, + daemon_info.running_processes, + daemon_info.stopped_processes + ); + println!(); + } + + println!("Processes:"); + for process in resp.processes { + let state_str = match process.state { + 1 => "STOPPED", + 2 => "STARTING", + 3 => "RUNNING", + 4 => "STOPPING", + 5 => "FAILED", + 6 => "EXITED", + _ => "UNKNOWN", + }; + + println!(" {:20} {}", process.name, state_str); + + if let Some(pid) = process.pid { + println!(" PID: {}", pid); + } + } + + Ok(()) + } + Commands::Start { name } => { + println!("Starting process: {}", name); + + let response = client + .start_process(StartProcessRequest { name: name.clone() }) + .await + .context("Failed to start process")?; + + let resp = response.into_inner(); + + if resp.success { + println!("Success: {}", resp.message); + } else { + println!("Failed: {}", resp.message); + } + + Ok(()) + } + Commands::Stop { name, force } => { + println!("Stopping process: {}", name); + + let response = client + .stop_process(StopProcessRequest { + name: name.clone(), + force, + }) + .await + .context("Failed to stop process")?; + + let resp = response.into_inner(); + + if resp.success { + println!("Success: {}", resp.message); + } else { + println!("Failed: {}", resp.message); + } + + Ok(()) + } + Commands::Restart { name } => { + println!("Restarting process: {}", name); + + let response = client + .restart_process(RestartProcessRequest { name: name.clone() }) + .await + .context("Failed to restart process")?; + + let resp = response.into_inner(); + + if resp.success { + println!("Success: {}", resp.message); + } else { + println!("Failed: {}", resp.message); + } + + Ok(()) + } + _ => unimplemented!(), + } +} diff --git a/cirond/Cargo.toml b/cirond/Cargo.toml new file mode 100644 index 0000000..8458824 --- /dev/null +++ b/cirond/Cargo.toml @@ -0,0 +1,32 @@ +[package] +name = "cirond" +version.workspace = true +edition.workspace = true +authors.workspace = true +license.workspace = true + +[[bin]] +name = "cirond" +path = "src/main.rs" + +[dependencies] +ciron-common = { path = "../ciron-common" } +anyhow.workspace = true +tokio.workspace = true +tokio-stream.workspace = true +tracing.workspace = true +tracing-subscriber.workspace = true +clap.workspace = true +serde.workspace = true +serde_json.workspace = true +tonic.workspace = true +prost.workspace = true +prost-types.workspace = true +shell-words.workspace = true + +[target.'cfg(unix)'.dependencies] +nix = { version = "0.27", features = ["signal"] } + +[target.'cfg(target_os = "linux")'.dependencies] +tokio-vsock = "0.5" +async-stream = "0.3" diff --git a/cirond/src/grpc_server.rs b/cirond/src/grpc_server.rs new file mode 100644 index 0000000..8b9c91a --- /dev/null +++ b/cirond/src/grpc_server.rs @@ -0,0 +1,305 @@ +use ciron_common::{ + CironDaemon, DaemonInfo, GetLogsRequest, GetProcessStatusRequest, GetStatusRequest, + GetStatusResponse, LogEntry, ProcessConfig, ProcessEvent as ProtoProcessEvent, ProcessState, + ProcessStatus as ProtoProcessStatus, ReloadConfigRequest, ReloadConfigResponse, + RestartProcessRequest, RestartProcessResponse, ShutdownRequest, ShutdownResponse, + StartProcessRequest, StartProcessResponse, StopProcessRequest, StopProcessResponse, + StreamEventsRequest, TransportInfo, TransportType, +}; +use std::sync::Arc; +use std::time::SystemTime; +use tokio::sync::RwLock; +use tonic::{Request, Response, Status}; +use tracing::{error, info}; + +use crate::process::ProcessManager; + +pub struct CironDaemonService { + manager: Arc>, + start_time: SystemTime, + version: String, + config_file: String, + transport: String, +} + +impl CironDaemonService { + pub fn new( + manager: Arc>, + config_file: String, + transport: String, + ) -> Self { + Self { + manager, + start_time: SystemTime::now(), + version: env!("CARGO_PKG_VERSION").to_string(), + config_file, + transport, + } + } + + fn get_uptime_seconds(&self) -> i32 { + self.start_time + .elapsed() + .map(|d| d.as_secs() as i32) + .unwrap_or(0) + } + + fn parse_transport_info(&self) -> TransportInfo { + let (transport_type, address) = + if self.transport.starts_with("inet://") || self.transport.starts_with("tcp://") { + (TransportType::Inet, self.transport.clone()) + } else if self.transport.starts_with("unix://") { + (TransportType::Unix, self.transport.clone()) + } else if self.transport.starts_with("vsock://") { + (TransportType::Vsock, self.transport.clone()) + } else { + (TransportType::Unspecified, self.transport.clone()) + }; + + TransportInfo { + r#type: transport_type as i32, + address, + } + } +} + +#[tonic::async_trait] +impl CironDaemon for CironDaemonService { + async fn get_status( + &self, + _request: Request, + ) -> Result, Status> { + info!("Received GetStatus request"); + + let (processes, daemon_info) = { + let manager = self.manager.read().await; + let processes = manager.list_processes(); + + let running_count = processes.iter().filter(|(_, running)| *running).count(); + let stopped_count = processes.len() - running_count; + + let daemon_info = DaemonInfo { + version: self.version.clone(), + started_at: self + .start_time + .duration_since(SystemTime::UNIX_EPOCH) + .map(|d| d.as_secs().to_string()) + .unwrap_or_default(), + uptime_seconds: self.get_uptime_seconds(), + config_file: self.config_file.clone(), + total_processes: processes.len() as i32, + running_processes: running_count as i32, + stopped_processes: stopped_count as i32, + transport: Some(self.parse_transport_info()), + }; + + (processes, daemon_info) + }; + + let process_statuses: Vec = processes + .into_iter() + .map(|(name, running)| ProtoProcessStatus { + name: name.clone(), + state: if running { + ProcessState::Running + } else { + ProcessState::Stopped + } as i32, + pid: None, // TODO: get actual PID + started_at: None, + uptime_seconds: None, + restart_count: None, + config: Some(ProcessConfig { + command: String::new(), // TODO: get from config + autostart: false, + restart_policy: None, + env: Default::default(), + working_directory: None, + user: None, + group: None, + }), + }) + .collect(); + + Ok(Response::new(GetStatusResponse { + processes: process_statuses, + daemon_info: Some(daemon_info), + })) + } + + async fn get_process_status( + &self, + request: Request, + ) -> Result, Status> { + let name = &request.into_inner().name; + info!("Received GetProcessStatus request for: {}", name); + + let manager = self.manager.read().await; + let processes = manager.list_processes(); + + let process = processes + .iter() + .find(|(n, _)| n == name) + .ok_or_else(|| Status::not_found(format!("Process '{}' not found", name)))?; + + Ok(Response::new(ProtoProcessStatus { + name: name.clone(), + state: if process.1 { + ProcessState::Running + } else { + ProcessState::Stopped + } as i32, + pid: None, + started_at: None, + uptime_seconds: None, + restart_count: None, + config: Some(ProcessConfig { + command: String::new(), + autostart: false, + restart_policy: None, + env: Default::default(), + working_directory: None, + user: None, + group: None, + }), + })) + } + + async fn start_process( + &self, + request: Request, + ) -> Result, Status> { + let name = request.into_inner().name; + info!("Received StartProcess request for: {}", name); + + let mut manager = self.manager.write().await; + + match manager.start_process(&name).await { + Ok(_) => { + info!("Successfully started process: {}", name); + Ok(Response::new(StartProcessResponse { + success: true, + message: format!("Process '{}' started successfully", name), + status: Some(ProtoProcessStatus { + name: name.clone(), + state: ProcessState::Running as i32, + pid: None, + started_at: None, + uptime_seconds: None, + restart_count: None, + config: None, + }), + })) + } + Err(e) => { + error!("Failed to start process {}: {}", name, e); + Ok(Response::new(StartProcessResponse { + success: false, + message: format!("Failed to start process '{}': {}", name, e), + status: None, + })) + } + } + } + + async fn stop_process( + &self, + request: Request, + ) -> Result, Status> { + let req = request.into_inner(); + let name = req.name; + info!( + "Received StopProcess request for: {} (force: {})", + name, req.force + ); + + let mut manager = self.manager.write().await; + + match manager.stop_process(&name).await { + Ok(_) => { + info!("Successfully stopped process: {}", name); + Ok(Response::new(StopProcessResponse { + success: true, + message: format!("Process '{}' stopped successfully", name), + })) + } + Err(e) => { + error!("Failed to stop process {}: {}", name, e); + Ok(Response::new(StopProcessResponse { + success: false, + message: format!("Failed to stop process '{}': {}", name, e), + })) + } + } + } + + async fn restart_process( + &self, + request: Request, + ) -> Result, Status> { + let name = request.into_inner().name; + info!("Received RestartProcess request for: {}", name); + + let mut manager = self.manager.write().await; + + match manager.restart_process(&name).await { + Ok(_) => { + info!("Successfully restarted process: {}", name); + Ok(Response::new(RestartProcessResponse { + success: true, + message: format!("Process '{}' restarted successfully", name), + status: Some(ProtoProcessStatus { + name: name.clone(), + state: ProcessState::Running as i32, + pid: None, + started_at: None, + uptime_seconds: None, + restart_count: None, + config: None, + }), + })) + } + Err(e) => { + error!("Failed to restart process {}: {}", name, e); + Ok(Response::new(RestartProcessResponse { + success: false, + message: format!("Failed to restart process '{}': {}", name, e), + status: None, + })) + } + } + } + + type GetLogsStream = tokio_stream::wrappers::ReceiverStream>; + + async fn get_logs( + &self, + _request: Request, + ) -> Result, Status> { + unimplemented!() + } + + async fn reload_config( + &self, + _request: Request, + ) -> Result, Status> { + unimplemented!() + } + + async fn shutdown( + &self, + _request: Request, + ) -> Result, Status> { + unimplemented!() + } + + type StreamEventsStream = + tokio_stream::wrappers::ReceiverStream>; + + async fn stream_events( + &self, + _request: Request, + ) -> Result, Status> { + unimplemented!() + } +} diff --git a/cirond/src/main.rs b/cirond/src/main.rs new file mode 100644 index 0000000..14602b1 --- /dev/null +++ b/cirond/src/main.rs @@ -0,0 +1,347 @@ +mod grpc_server; +mod process; + +use ciron_common::{CironDaemonServer, Transport, load_config}; +use clap::Parser; +use grpc_server::CironDaemonService; +use process::ProcessManager; +use std::path::PathBuf; +use std::sync::Arc; +use tokio::sync::RwLock; +use tonic::transport::Server; +use tracing::{Level, error, info}; +use tracing_subscriber::FmtSubscriber; + +#[cfg(unix)] +use {tokio::net::UnixListener, tokio_stream::wrappers::UnixListenerStream}; + +#[cfg(target_os = "linux")] +use { + std::pin::Pin, + std::task::{Context, Poll}, + tokio_vsock::{VMADDR_CID_ANY, VsockAddr, VsockListener, VsockStream}, + tonic::transport::server::Connected, +}; + +// Wrapper to implement Connected trait for VsockStream +#[cfg(target_os = "linux")] +struct VsockConnection { + stream: VsockStream, +} + +#[cfg(target_os = "linux")] +impl Connected for VsockConnection { + type ConnectInfo = (); + + fn connect_info(&self) -> Self::ConnectInfo {} +} + +#[cfg(target_os = "linux")] +impl tokio::io::AsyncRead for VsockConnection { + fn poll_read( + mut self: Pin<&mut Self>, + cx: &mut Context<'_>, + buf: &mut tokio::io::ReadBuf<'_>, + ) -> Poll> { + Pin::new(&mut self.stream).poll_read(cx, buf) + } +} + +#[cfg(target_os = "linux")] +impl tokio::io::AsyncWrite for VsockConnection { + fn poll_write( + mut self: Pin<&mut Self>, + cx: &mut Context<'_>, + buf: &[u8], + ) -> Poll> { + Pin::new(&mut self.stream).poll_write(cx, buf) + } + + fn poll_flush(mut self: Pin<&mut Self>, cx: &mut Context<'_>) -> Poll> { + Pin::new(&mut self.stream).poll_flush(cx) + } + + fn poll_shutdown(mut self: Pin<&mut Self>, cx: &mut Context<'_>) -> Poll> { + Pin::new(&mut self.stream).poll_shutdown(cx) + } +} + +#[derive(Parser)] +#[command(name = "cirond")] +#[command(about = "Ciron process manager daemon", long_about = None)] +struct Cli { + /// Path to configuration file + #[arg(short, long, default_value = "ciron.toml")] + config: String, + + /// Run in foreground (don't daemonize) + #[arg(short, long)] + foreground: bool, + + /// Override transport address (inet://host:port, unix:///path, vsock://cid:port) + /// If not specified, uses transport config from config file + #[arg(short, long)] + transport: Option, +} + +#[tokio::main] +async fn main() -> anyhow::Result<()> { + let cli = Cli::parse(); + + // Initialize logging + let subscriber = FmtSubscriber::builder() + .with_max_level(Level::INFO) + .finish(); + tracing::subscriber::set_global_default(subscriber).expect("Failed to set tracing subscriber"); + + info!("Starting Cirond process manager daemon"); + info!("Loading configuration from: {}", cli.config); + + // Load configuration + let config = load_config(&cli.config)?; + info!("Configuration loaded successfully"); + info!("Log level: {:?}", config.log_level); + info!("Programs configured: {}", config.program.len()); + + // Determine transport to use + let transports = if let Some(transport_str) = &cli.transport { + info!("Using transport from CLI: {}", transport_str); + vec![Transport::parse(transport_str)?] + } else { + let mut transports = Vec::new(); + + if config.transport.enable_unix { + #[cfg(unix)] + { + transports.push(Transport::Unix { + path: PathBuf::from(&config.transport.unix_socket_path), + }); + info!("Unix socket enabled: {}", config.transport.unix_socket_path); + } + #[cfg(not(unix))] + { + info!("Unix socket requested but not available on this platform"); + } + } + + if config.transport.enable_inet { + let parts: Vec<&str> = config.transport.inet_address.split(':').collect(); + if parts.len() == 2 { + transports.push(Transport::Inet { + host: parts[0].to_string(), + port: parts[1].parse()?, + }); + info!("Inet socket enabled: {}", config.transport.inet_address); + } + } + + if config.transport.enable_vsock { + #[cfg(target_os = "linux")] + { + transports.push(Transport::Vsock { + cid: config.transport.vsock_cid, + port: config.transport.vsock_port, + }); + info!( + "Vsock enabled: CID={}, port={}", + config.transport.vsock_cid, config.transport.vsock_port + ); + } + #[cfg(not(target_os = "linux"))] + { + info!("Vsock requested but not available on this platform (Linux only)"); + } + } + + if transports.is_empty() { + error!("No transports enabled in configuration"); + anyhow::bail!("At least one transport must be enabled"); + } + + transports + }; + + // Create process manager and load configuration + let manager = ProcessManager::new(); + let manager = Arc::new(RwLock::new(manager)); + + { + let mut mgr = manager.write().await; + mgr.load_from_config(config); + } + + // Start all autostart processes + { + let mut mgr = manager.write().await; + mgr.start_all_autostart().await?; + } + + // List running processes + { + let mgr = manager.read().await; + let processes = mgr.list_processes(); + info!("Process status:"); + for (name, running) in processes { + info!( + " {} - {}", + name, + if running { "RUNNING" } else { "STOPPED" } + ); + } + } + + // Setup Ctrl+C handler + let (shutdown_tx, shutdown_rx) = tokio::sync::broadcast::channel::<()>(1); + + // Spawn Ctrl+C listener + let shutdown_tx_clone = shutdown_tx.clone(); + tokio::spawn(async move { + match tokio::signal::ctrl_c().await { + Ok(()) => { + info!("Received Ctrl+C signal"); + let _ = shutdown_tx_clone.send(()); + } + Err(err) => { + error!("Error setting up Ctrl+C handler: {}", err); + } + } + }); + + // Clone manager for event loop + let event_manager = manager.clone(); + + // Spawn event processing task that checks periodically + let event_loop = tokio::spawn(async move { + loop { + tokio::time::sleep(tokio::time::Duration::from_millis(100)).await; + + let mut mgr = event_manager.write().await; + // Process all pending events + while mgr.handle_single_event().await {} + drop(mgr); + } + }); + + // Start gRPC servers for each transport + let mut server_handles = Vec::new(); + + for transport in transports { + let manager_clone = manager.clone(); + let config_path = cli.config.clone(); + let transport_str = transport.to_string(); + let mut shutdown_rx_clone = shutdown_rx.resubscribe(); + + let handle = tokio::spawn(async move { + let grpc_service = + CironDaemonService::new(manager_clone, config_path, transport_str.clone()); + + let result: anyhow::Result<()> = match transport { + Transport::Inet { host, port } => { + let addr = format!("{}:{}", host, port).parse()?; + info!("Starting gRPC server on: {}", addr); + + Server::builder() + .add_service(CironDaemonServer::new(grpc_service)) + .serve_with_shutdown(addr, async move { + let _ = shutdown_rx_clone.recv().await; + }) + .await + .map_err(|e| anyhow::anyhow!("Server error: {}", e)) + } + #[cfg(unix)] + Transport::Unix { path } => { + // Remove existing socket file if it exists + let _ = std::fs::remove_file(&path); + + info!("Starting gRPC server on Unix socket: {}", path.display()); + + let uds = UnixListener::bind(&path)?; + let uds_stream = UnixListenerStream::new(uds); + + Server::builder() + .add_service(CironDaemonServer::new(grpc_service)) + .serve_with_incoming_shutdown(uds_stream, async move { + let _ = shutdown_rx_clone.recv().await; + }) + .await + .map_err(|e| anyhow::anyhow!("Server error: {}", e)) + } + #[cfg(not(unix))] + Transport::Unix { .. } => Err(anyhow::anyhow!( + "Unix sockets not supported on this platform" + )), + #[cfg(target_os = "linux")] + Transport::Vsock { cid, port } => { + info!("Starting gRPC server on Vsock: CID={}, port={}", cid, port); + + // For server, we listen on VMADDR_CID_ANY to accept connections from any CID + let addr = VsockAddr::new(VMADDR_CID_ANY, port); + let mut listener = VsockListener::bind(addr)?; + + // Create a stream of incoming connections + let incoming = async_stream::stream! { + loop { + match listener.accept().await { + Ok((stream, addr)) => { + info!("Accepted vsock connection from {:?}", addr); + yield Ok::<_, std::io::Error>(VsockConnection { stream }); + } + Err(e) => { + error!("Error accepting vsock connection: {}", e); + yield Err(e); + } + } + } + }; + + Server::builder() + .add_service(CironDaemonServer::new(grpc_service)) + .serve_with_incoming_shutdown(incoming, async move { + let _ = shutdown_rx_clone.recv().await; + }) + .await + .map_err(|e| anyhow::anyhow!("Server error: {}", e)) + } + #[cfg(not(target_os = "linux"))] + Transport::Vsock { .. } => Err(anyhow::anyhow!( + "Vsock not supported on this platform (Linux only)" + )), + }; + + result + }); + + server_handles.push(handle); + } + + // Wait for all servers to complete + for handle in server_handles { + match handle.await { + Ok(Ok(())) => { + info!("Server stopped successfully"); + } + Ok(Err(e)) => { + error!("Server error: {}", e); + } + Err(e) => { + error!("Server task error: {}", e); + } + } + } + + info!("All gRPC servers stopped"); + + // Stop all processes + info!("Stopping all processes..."); + { + let mut mgr = manager.write().await; + mgr.shutdown(); // Close event channel first + mgr.stop_all().await; + } + + // Abort event loop + event_loop.abort(); + + info!("Cirond daemon stopped"); + Ok(()) +} diff --git a/cirond/src/process.rs b/cirond/src/process.rs new file mode 100644 index 0000000..06e92b5 --- /dev/null +++ b/cirond/src/process.rs @@ -0,0 +1,282 @@ +use anyhow::{Context, Result}; +use ciron_common::{GlobalConfig, ProgramConfig}; +use std::collections::HashMap; +use std::process::Stdio; +use tokio::process::Command; +use tokio::sync::mpsc; +use tokio::task::JoinHandle; +use tracing::{error, info, warn}; + +#[derive(Debug, Clone)] +pub enum ProcessEvent { + Started(String), + Exited(String, i32), + Failed(String, String), + RestartRequested(String), +} + +pub struct ProcessManager { + processes: HashMap, + event_tx: mpsc::UnboundedSender, + event_rx: mpsc::UnboundedReceiver, +} + +struct ManagedProcess { + _name: String, + config: ProgramConfig, + monitor_handle: Option>, + pid: Option, + running: bool, +} + +impl ProcessManager { + pub fn new() -> Self { + let (event_tx, event_rx) = mpsc::unbounded_channel(); + Self { + processes: HashMap::new(), + event_tx, + event_rx, + } + } + + pub fn load_from_config(&mut self, config: GlobalConfig) { + for (name, program_config) in config.program { + info!("Loaded program configuration: {}", name); + self.processes.insert( + name.clone(), + ManagedProcess { + _name: name, + config: program_config, + monitor_handle: None, + pid: None, + running: false, + }, + ); + } + } + + pub async fn start_all_autostart(&mut self) -> Result<()> { + let programs_to_start: Vec = self + .processes + .iter() + .filter(|(_, p)| p.config.autostart) + .map(|(name, _)| name.clone()) + .collect(); + + for name in programs_to_start { + if let Err(e) = self.start_process(&name).await { + error!("Failed to start autostart program {}: {}", name, e); + } + } + + Ok(()) + } + + pub async fn start_process(&mut self, name: &str) -> Result<()> { + let process = self + .processes + .get_mut(name) + .context(format!("Program {} not found", name))?; + + if process.running { + warn!("Process {} is already running", name); + return Ok(()); + } + + info!("Starting process: {}", name); + + // Parse command and arguments using shell-words for proper quote handling + let parts = shell_words::split(&process.config.command) + .context(format!("Failed to parse command for {}", name))?; + + if parts.is_empty() { + return Err(anyhow::anyhow!("Empty command for {}", name)); + } + + let mut cmd = Command::new(&parts[0]); + if parts.len() > 1 { + cmd.args(&parts[1..]); + } + + // Set environment variables if specified + if let Some(env) = &process.config.env { + for (key, value) in env { + cmd.env(key, value); + } + } + + cmd.stdout(Stdio::piped()) + .stderr(Stdio::piped()) + .stdin(Stdio::null()); + + let mut child = cmd + .spawn() + .context(format!("Failed to spawn process {}", name))?; + + let pid = child.id(); + info!("Process {} started with PID: {:?}", name, pid); + + // Store the PID + process.pid = pid; + + let _ = self.event_tx.send(ProcessEvent::Started(name.to_string())); + + // Start monitoring the process + let name_clone = name.to_string(); + let event_tx = self.event_tx.clone(); + + let monitor_handle = tokio::spawn(async move { + match child.wait().await { + Ok(status) => { + let code = status.code().unwrap_or(-1); + info!("Process {} exited with code: {}", name_clone, code); + let _ = event_tx.send(ProcessEvent::Exited(name_clone.clone(), code)); + } + Err(e) => { + error!("Error waiting for process {}: {}", name_clone, e); + let _ = event_tx.send(ProcessEvent::Failed(name_clone.clone(), e.to_string())); + } + } + }); + + process.monitor_handle = Some(monitor_handle); + process.running = true; + + Ok(()) + } + + pub async fn stop_process(&mut self, name: &str) -> Result<()> { + let process = self + .processes + .get_mut(name) + .context(format!("Program {} not found", name))?; + + if !process.running { + warn!("Process {} is not running", name); + return Ok(()); + } + + info!("Stopping process: {}", name); + + // Send SIGTERM to the process + if let Some(pid) = process.pid { + #[cfg(unix)] + { + use nix::sys::signal::{Signal, kill}; + use nix::unistd::Pid; + + match kill(Pid::from_raw(pid as i32), Signal::SIGTERM) { + Ok(_) => info!("Sent SIGTERM to process {} (PID: {})", name, pid), + Err(e) => warn!( + "Failed to send SIGTERM to process {} (PID: {}): {}", + name, pid, e + ), + } + } + + #[cfg(not(unix))] + { + warn!("Signal handling not implemented for non-Unix systems"); + } + } + + // Cancel the monitor task + if let Some(handle) = process.monitor_handle.take() { + handle.abort(); + } + + process.running = false; + process.pid = None; + + Ok(()) + } + + pub async fn restart_process(&mut self, name: &str) -> Result<()> { + info!("Restarting process: {}", name); + self.stop_process(name).await?; + tokio::time::sleep(tokio::time::Duration::from_secs(1)).await; + self.start_process(name).await?; + Ok(()) + } + + pub async fn handle_single_event(&mut self) -> bool { + match self.event_rx.try_recv() { + Ok(event) => { + match event { + ProcessEvent::Started(name) => { + info!("Event: Process {} started", name); + } + ProcessEvent::Exited(name, code) => { + info!("Event: Process {} exited with code {}", name, code); + + // Mark process as not running + if let Some(process) = self.processes.get_mut(&name) { + process.running = false; + + // Handle restart policy + let should_restart = match process.config.restart.as_deref() { + Some("always") => true, + Some("on-failure") => code != 0, + _ => false, + }; + + if should_restart { + info!( + "Scheduling restart for process {} due to restart policy", + name + ); + let event_tx = self.event_tx.clone(); + let name_clone = name.clone(); + tokio::spawn(async move { + tokio::time::sleep(tokio::time::Duration::from_secs(2)).await; + let _ = + event_tx.send(ProcessEvent::RestartRequested(name_clone)); + }); + } + } + } + ProcessEvent::Failed(name, error) => { + error!("Event: Process {} failed: {}", name, error); + if let Some(process) = self.processes.get_mut(&name) { + process.running = false; + } + } + ProcessEvent::RestartRequested(name) => { + info!("Event: Restart requested for {}", name); + if let Err(e) = self.start_process(&name).await { + error!("Failed to restart process {}: {}", name, e); + } + } + } + true + } + Err(tokio::sync::mpsc::error::TryRecvError::Empty) => false, + Err(tokio::sync::mpsc::error::TryRecvError::Disconnected) => { + info!("Event channel closed"); + false + } + } + } + + pub fn list_processes(&self) -> Vec<(String, bool)> { + self.processes + .iter() + .map(|(name, process)| (name.clone(), process.running)) + .collect() + } + + pub async fn stop_all(&mut self) { + info!("Stopping all processes"); + let names: Vec = self.processes.keys().cloned().collect(); + for name in names { + if let Err(e) = self.stop_process(&name).await { + error!("Failed to stop process {}: {}", name, e); + } + } + } + + pub fn shutdown(&mut self) { + info!("Shutting down process manager"); + self.event_rx.close(); + } +} diff --git a/examples/basic/README.md b/examples/basic/README.md new file mode 100644 index 0000000..fb05a45 --- /dev/null +++ b/examples/basic/README.md @@ -0,0 +1,52 @@ +# Basic Example + +This example demonstrates basic usage of Ciron with shell commands. + +## Running + +1. Start the daemon: +```bash +../../target/release/cirond -c ciron.toml +``` + +2. Check status: +```bash +../../target/release/cironctl -t unix:///tmp/cirond.sock status +``` + +You should see: +``` +Process: hello - Status: RUNNING +Process: counter - Status: RUNNING +``` + +3. Control processes: +```bash +# Stop a process +../../target/release/cironctl -t unix:///tmp/cirond.sock stop hello + +# Check status again +../../target/release/cironctl -t unix:///tmp/cirond.sock status + +# Start a process +../../target/release/cironctl -t unix:///tmp/cirond.sock start hello + +# Restart a process +../../target/release/cironctl -t unix:///tmp/cirond.sock restart counter +``` + +## What's Happening + +- **hello**: Prints "Hello from Ciron" with timestamp every 2 seconds +- **counter**: Counts from 0 upwards, printing every 3 seconds +- Both processes use shell commands with proper quote handling +- `restart = "always"` means the process will always restart when it exits +- `restart = "on-failure"` means it only restarts if it exits with non-zero code + +## Notes + +Commands are parsed with proper shell quote handling, so you can use: +- Single quotes: `'text'` +- Double quotes: `"text"` +- Shell variables: `$var` or `$(command)` +- Complex shell syntax diff --git a/examples/basic/ciron.toml b/examples/basic/ciron.toml new file mode 100644 index 0000000..6638464 --- /dev/null +++ b/examples/basic/ciron.toml @@ -0,0 +1,21 @@ +log_level = "info" + +# Transport configuration +[transport] +enable_unix = true +unix_socket_path = "/tmp/cirond.sock" +enable_inet = false +inet_address = "127.0.0.1:50051" +enable_vsock = false +vsock_cid = 2 +vsock_port = 50051 + +[program.hello] +command = "sh -c 'while true; do echo \"Hello from Ciron at $(date)\"; sleep 2; done'" +autostart = true +restart = "always" + +[program.counter] +command = "sh -c 'i=0; while true; do echo \"Count: $i\"; i=$((i+1)); sleep 3; done'" +autostart = true +restart = "on-failure" diff --git a/examples/nginx-webapp/Dockerfile b/examples/nginx-webapp/Dockerfile new file mode 100644 index 0000000..899669d --- /dev/null +++ b/examples/nginx-webapp/Dockerfile @@ -0,0 +1,37 @@ +FROM rust:1.86 AS builder + +RUN apt-get update && apt-get install -y \ + pkg-config \ + libssl-dev \ + protobuf-compiler \ + && rm -rf /var/lib/apt/lists/* + +WORKDIR /build + +COPY Cargo.toml Cargo.lock ./ +COPY ciron-common ./ciron-common +COPY cirond ./cirond +COPY cironctl ./cironctl +COPY proto ./proto + +RUN cargo build --release --bin cirond + +FROM debian:bookworm-slim +RUN apt-get update && apt-get install -y \ + python3 \ + nginx \ + && rm -rf /var/lib/apt/lists/* + +COPY --from=builder /build/target/release/cirond /usr/local/bin/cirond + +COPY examples/nginx-webapp/app.py /app/app.py +COPY examples/nginx-webapp/nginx.conf /etc/nginx/nginx.conf +COPY examples/nginx-webapp/ciron.toml /etc/ciron/ciron.toml + +WORKDIR /app + +RUN chmod +x /app/app.py + +EXPOSE 80 50051 + +CMD ["cirond", "-c", "/etc/ciron/ciron.toml"] diff --git a/examples/nginx-webapp/README.md b/examples/nginx-webapp/README.md new file mode 100644 index 0000000..ca8aa82 --- /dev/null +++ b/examples/nginx-webapp/README.md @@ -0,0 +1,57 @@ +# Nginx + Web Application Example + +This example demonstrates managing multiple processes: a Python web application and Nginx as a reverse proxy, all running inside a Docker container. + +## Architecture + +``` +Client -> Docker Container + | + +-> Cirond (PID 1) + | + +-> Nginx (port 80) -> Python App (port 8080) +``` + +## Running with Docker + +1. Build the Docker image (from project root): +```bash +docker build -f examples/nginx-webapp/Dockerfile -t ciron-nginx-example . +``` + +2. Run the container: +```bash +docker run --rm -p 8080:80 -p 50051:50051 ciron-nginx-example +``` + +3. Test the application: +```bash +# Access through Nginx +curl http://localhost:8080 + +# Health check +curl http://localhost:8080/health +``` + +4. Control processes from host: +```bash +# Build cironctl if not already built +cargo build --release --bin cironctl + +# Check status +./target/release/cironctl -t inet://127.0.0.1:50051 status + +# Stop nginx +./target/release/cironctl -t inet://127.0.0.1:50051 stop nginx + +# Restart webapp +./target/release/cironctl -t inet://127.0.0.1:50051 restart webapp +``` + +## What's Happening + +- **cirond**: Runs as PID 1 in the container, managing both processes +- **webapp**: Python HTTP server on port 8080 +- **nginx**: Nginx reverse proxy on port 80, forwarding to Python app +- Both processes automatically restart if they crash +- You can control processes via **cironctl** from outside the container diff --git a/examples/nginx-webapp/app.py b/examples/nginx-webapp/app.py new file mode 100644 index 0000000..db1fe31 --- /dev/null +++ b/examples/nginx-webapp/app.py @@ -0,0 +1,29 @@ +#!/usr/bin/env python3 +"""Simple HTTP server for demonstration.""" +import http.server +import socketserver +import os + +PORT = int(os.getenv('PORT', 8080)) + +class MyHandler(http.server.SimpleHTTPRequestHandler): + def do_GET(self): + self.send_response(200) + self.send_header('Content-type', 'text/html') + self.end_headers() + html = f""" + + + Ciron Demo + +

Hello from Ciron managed app!

+

This Python app is managed by Ciron process manager.

+

Running on port {PORT}

+ + + """ + self.wfile.write(html.encode()) + +with socketserver.TCPServer(("", PORT), MyHandler) as httpd: + print(f"Serving on port {PORT}") + httpd.serve_forever() diff --git a/examples/nginx-webapp/ciron.toml b/examples/nginx-webapp/ciron.toml new file mode 100644 index 0000000..2682250 --- /dev/null +++ b/examples/nginx-webapp/ciron.toml @@ -0,0 +1,27 @@ +log_level = "info" + +# Transport configuration +[transport] +enable_unix = true +unix_socket_path = "/tmp/cirond.sock" +enable_inet = true +inet_address = "0.0.0.0:50051" +enable_vsock = false +vsock_cid = 2 +vsock_port = 50051 + +# Backend Python application +[program.webapp] +command = "python3 /app/app.py" +autostart = true +restart = "always" + +[program.webapp.env] +PORT = "8080" +PYTHONUNBUFFERED = "1" + +# Nginx reverse proxy +[program.nginx] +command = "nginx -c /etc/nginx/nginx.conf -g 'daemon off;'" +autostart = true +restart = "always" diff --git a/examples/nginx-webapp/nginx.conf b/examples/nginx-webapp/nginx.conf new file mode 100644 index 0000000..caa7b23 --- /dev/null +++ b/examples/nginx-webapp/nginx.conf @@ -0,0 +1,25 @@ +events { + worker_connections 1024; +} + +http { + upstream backend { + server 127.0.0.1:8080; + } + + server { + listen 80; + server_name localhost; + + location / { + proxy_pass http://backend; + proxy_set_header Host $host; + proxy_set_header X-Real-IP $remote_addr; + } + + location /health { + return 200 "OK\n"; + add_header Content-Type text/plain; + } + } +} diff --git a/proto/ciron.proto b/proto/ciron.proto new file mode 100644 index 0000000..6d60299 --- /dev/null +++ b/proto/ciron.proto @@ -0,0 +1,199 @@ +syntax = "proto3"; + +package ciron.v1; + +// Ciron Daemon Service +// Provides API for managing processes through cirond +service CironDaemon { + // Get status of all processes + rpc GetStatus(GetStatusRequest) returns (GetStatusResponse); + + // Get status of a specific process + rpc GetProcessStatus(GetProcessStatusRequest) returns (ProcessStatus); + + // Start a process + rpc StartProcess(StartProcessRequest) returns (StartProcessResponse); + + // Stop a process + rpc StopProcess(StopProcessRequest) returns (StopProcessResponse); + + // Restart a process + rpc RestartProcess(RestartProcessRequest) returns (RestartProcessResponse); + + // Get logs of a process + rpc GetLogs(GetLogsRequest) returns (stream LogEntry); + + // Reload daemon configuration + rpc ReloadConfig(ReloadConfigRequest) returns (ReloadConfigResponse); + + // Shutdown daemon + rpc Shutdown(ShutdownRequest) returns (ShutdownResponse); + + // Stream process events + rpc StreamEvents(StreamEventsRequest) returns (stream ProcessEvent); +} + +// Request/Response messages + +message GetStatusRequest {} + +message GetStatusResponse { + repeated ProcessStatus processes = 1; + DaemonInfo daemon_info = 2; +} + +message GetProcessStatusRequest { + string name = 1; +} + +message StartProcessRequest { + string name = 1; +} + +message StartProcessResponse { + bool success = 1; + string message = 2; + ProcessStatus status = 3; +} + +message StopProcessRequest { + string name = 1; + bool force = 2; // Force kill if true +} + +message StopProcessResponse { + bool success = 1; + string message = 2; +} + +message RestartProcessRequest { + string name = 1; +} + +message RestartProcessResponse { + bool success = 1; + string message = 2; + ProcessStatus status = 3; +} + +message GetLogsRequest { + string name = 1; + bool follow = 2; + int32 lines = 3; // Number of lines to retrieve (0 = all) + optional string since = 4; // Timestamp to start from +} + +message LogEntry { + string timestamp = 1; + LogLevel level = 2; + string source = 3; // stdout or stderr + string message = 4; +} + +enum LogLevel { + LOG_LEVEL_UNSPECIFIED = 0; + LOG_LEVEL_TRACE = 1; + LOG_LEVEL_DEBUG = 2; + LOG_LEVEL_INFO = 3; + LOG_LEVEL_WARN = 4; + LOG_LEVEL_ERROR = 5; +} + +message ReloadConfigRequest {} + +message ReloadConfigResponse { + bool success = 1; + string message = 2; + repeated string added_processes = 3; + repeated string removed_processes = 4; + repeated string updated_processes = 5; +} + +message ShutdownRequest { + bool graceful = 1; + optional int32 timeout_seconds = 2; +} + +message ShutdownResponse { + bool success = 1; + string message = 2; +} + +message StreamEventsRequest { + repeated string process_names = 1; // Empty = all processes +} + +message ProcessEvent { + string process_name = 1; + ProcessEventType event_type = 2; + string timestamp = 3; + optional int32 exit_code = 4; + optional int32 pid = 5; + optional string message = 6; +} + +enum ProcessEventType { + PROCESS_EVENT_TYPE_UNSPECIFIED = 0; + PROCESS_EVENT_TYPE_STARTING = 1; + PROCESS_EVENT_TYPE_STARTED = 2; + PROCESS_EVENT_TYPE_STOPPING = 3; + PROCESS_EVENT_TYPE_STOPPED = 4; + PROCESS_EVENT_TYPE_EXITED = 5; + PROCESS_EVENT_TYPE_FAILED = 6; + PROCESS_EVENT_TYPE_RESTARTING = 7; +} + +// Data models + +message ProcessStatus { + string name = 1; + ProcessState state = 2; + optional int32 pid = 3; + optional string started_at = 4; + optional int32 uptime_seconds = 5; + optional int32 restart_count = 6; + ProcessConfig config = 7; +} + +enum ProcessState { + PROCESS_STATE_UNSPECIFIED = 0; + PROCESS_STATE_STOPPED = 1; + PROCESS_STATE_STARTING = 2; + PROCESS_STATE_RUNNING = 3; + PROCESS_STATE_STOPPING = 4; + PROCESS_STATE_FAILED = 5; + PROCESS_STATE_EXITED = 6; +} + +message ProcessConfig { + string command = 1; + bool autostart = 2; + optional string restart_policy = 3; + map env = 4; + optional string working_directory = 5; + optional string user = 6; + optional string group = 7; +} + +message DaemonInfo { + string version = 1; + string started_at = 2; + int32 uptime_seconds = 3; + string config_file = 4; + int32 total_processes = 5; + int32 running_processes = 6; + int32 stopped_processes = 7; + TransportInfo transport = 8; +} + +message TransportInfo { + TransportType type = 1; + string address = 2; +} + +enum TransportType { + TRANSPORT_TYPE_UNSPECIFIED = 0; + TRANSPORT_TYPE_INET = 1; // TCP/IP socket + TRANSPORT_TYPE_UNIX = 2; // Unix domain socket + TRANSPORT_TYPE_VSOCK = 3; // VM sockets +} diff --git a/proto/errors.proto b/proto/errors.proto new file mode 100644 index 0000000..169ab58 --- /dev/null +++ b/proto/errors.proto @@ -0,0 +1,29 @@ +syntax = "proto3"; + +package ciron.v1; + +// Common error types for Ciron API + +message ErrorDetails { + ErrorCode code = 1; + string message = 2; + repeated ErrorDetail details = 3; +} + +enum ErrorCode { + ERROR_CODE_UNSPECIFIED = 0; + ERROR_CODE_INTERNAL = 1; + ERROR_CODE_NOT_FOUND = 2; + ERROR_CODE_ALREADY_EXISTS = 3; + ERROR_CODE_INVALID_ARGUMENT = 4; + ERROR_CODE_PERMISSION_DENIED = 5; + ERROR_CODE_TIMEOUT = 6; + ERROR_CODE_UNAVAILABLE = 7; + ERROR_CODE_FAILED_PRECONDITION = 8; + ERROR_CODE_RESOURCE_EXHAUSTED = 9; +} + +message ErrorDetail { + string field = 1; + string description = 2; +} diff --git a/src/main.rs b/src/main.rs deleted file mode 100644 index e7a11a9..0000000 --- a/src/main.rs +++ /dev/null @@ -1,3 +0,0 @@ -fn main() { - println!("Hello, world!"); -}