commit 2fda71af117a90da5f496d8bb8105f0ee9e07420 Author: Jan De Landtsheer Date: Sat Aug 16 21:12:16 2025 +0200 Squashed 'components/zinit/' content from commit 1b76c06 git-subtree-dir: components/zinit git-subtree-split: 1b76c062fe31d552d1b7b23484ce163995a81482 diff --git a/.cargo/config.toml b/.cargo/config.toml new file mode 100644 index 0000000..a076ee4 --- /dev/null +++ b/.cargo/config.toml @@ -0,0 +1,2 @@ +[target.aarch64-unknown-linux-musl] +linker = "aarch64-linux-musl-gcc" diff --git a/.github/workflows/release.yaml b/.github/workflows/release.yaml new file mode 100644 index 0000000..8356753 --- /dev/null +++ b/.github/workflows/release.yaml @@ -0,0 +1,134 @@ +on: + push: + # Sequence of patterns matched against refs/tags + tags: + - "v*" # Push events to matching v*, i.e. v1.0, v20.15.10 + +name: Create Release + +jobs: + build: + name: Build and Release + runs-on: ${{ matrix.os }} + permissions: + contents: write + strategy: + fail-fast: false # Continue with other builds if one fails + matrix: + include: + # Linux builds + - os: ubuntu-latest + target: x86_64-unknown-linux-musl + binary_name: zinit-linux-x86_64 + # macOS builds + - os: macos-latest + target: x86_64-apple-darwin + binary_name: zinit-macos-x86_64 + - os: macos-latest + target: aarch64-apple-darwin + binary_name: zinit-macos-aarch64 + steps: + - name: Checkout code + uses: actions/checkout@v4 + with: + fetch-depth: 0 # Fetch all history for proper versioning + + # Cache Rust dependencies + - name: Cache Rust dependencies + uses: actions/cache@v3 + with: + path: | + ~/.cargo/registry + ~/.cargo/git + target + key: ${{ runner.os }}-${{ matrix.target }}-cargo-${{ hashFiles('**/Cargo.lock') }} + restore-keys: | + ${{ runner.os }}-${{ matrix.target }}-cargo- + + - name: Setup build environment (macOS) + if: matrix.os == 'macos-latest' + run: | + # Install required build tools for macOS + brew install llvm + + # For cross-compilation to Apple Silicon when on Intel + if [[ "${{ matrix.target }}" == "aarch64-apple-darwin" ]]; then + rustup target add aarch64-apple-darwin + fi + + - name: Install Rust toolchain + uses: actions-rs/toolchain@v1 + with: + toolchain: stable + target: ${{ matrix.target }} + override: true + profile: minimal # Minimal components for faster installation + + - name: Install MUSL tools (Linux) + if: matrix.os == 'ubuntu-latest' && contains(matrix.target, 'musl') + run: | + sudo apt-get update + sudo apt-get install -y musl-tools musl-dev + + - name: Build release + env: + CC: ${{ matrix.os == 'macos-latest' && 'clang' || '' }} + CXX: ${{ matrix.os == 'macos-latest' && 'clang++' || '' }} + MACOSX_DEPLOYMENT_TARGET: '10.12' + run: | + # Add special flags for Apple Silicon builds + if [[ "${{ matrix.target }}" == "aarch64-apple-darwin" ]]; then + export RUSTFLAGS="-C target-feature=+crt-static" + fi + + cargo build --release --target=${{ matrix.target }} --verbose + + # Verify binary exists + if [ ! -f "target/${{ matrix.target }}/release/zinit" ]; then + echo "::error::Binary not found at target/${{ matrix.target }}/release/zinit" + exit 1 + fi + + - name: Strip binary (Linux) + if: matrix.os == 'ubuntu-latest' + run: | + strip target/${{ matrix.target }}/release/zinit + + - name: Strip binary (macOS) + if: matrix.os == 'macos-latest' + run: | + strip -x target/${{ matrix.target }}/release/zinit || true + + - name: Rename binary + run: | + cp target/${{ matrix.target }}/release/zinit ${{ matrix.binary_name }} + + # Verify binary was copied successfully + if [ ! -f "${{ matrix.binary_name }}" ]; then + echo "::error::Binary not copied successfully to ${{ matrix.binary_name }}" + exit 1 + fi + + # Show binary info for debugging + echo "Binary details for ${{ matrix.binary_name }}:" + ls -la ${{ matrix.binary_name }} + file ${{ matrix.binary_name }} || true + + # Upload artifacts even if the release step fails + - name: Upload artifacts + uses: actions/upload-artifact@v4 + with: + name: ${{ matrix.binary_name }} + path: ${{ matrix.binary_name }} + retention-days: 5 + + - name: Upload Release Assets + uses: softprops/action-gh-release@v2 + with: + files: ${{ matrix.binary_name }} + name: Release ${{ github.ref_name }} + draft: false + prerelease: false + fail_on_unmatched_files: false + env: + GITHUB_TOKEN: ${{ secrets.GITHUB_TOKEN }} diff --git a/.github/workflows/rust.yml b/.github/workflows/rust.yml new file mode 100644 index 0000000..e9c12cb --- /dev/null +++ b/.github/workflows/rust.yml @@ -0,0 +1,36 @@ +name: Rust + +on: [push] + +jobs: + build: + runs-on: ubuntu-latest + steps: + - uses: actions/checkout@v1 + name: Checkout code + with: + fetch-depth: 1 + - uses: actions-rs/toolchain@v1 + name: Install toolchain + with: + toolchain: stable + target: x86_64-unknown-linux-musl + - uses: actions-rs/cargo@v1 + name: Check formatting + with: + command: fmt + args: -- --check + - uses: actions-rs/cargo@v1 + name: Run tests (ahm!) + with: + command: test + args: --verbose + - uses: actions-rs/cargo@v1 + name: Run clippy + with: + command: clippy + - uses: actions-rs/cargo@v1 + name: Build + with: + command: build + args: --verbose diff --git a/.gitignore b/.gitignore new file mode 100644 index 0000000..f0e3bca --- /dev/null +++ b/.gitignore @@ -0,0 +1,2 @@ +/target +**/*.rs.bk \ No newline at end of file diff --git a/Cargo.lock b/Cargo.lock new file mode 100644 index 0000000..bf7dfc6 --- /dev/null +++ b/Cargo.lock @@ -0,0 +1,2717 @@ +# This file is automatically @generated by Cargo. +# It is not intended for manual editing. +version = 4 + +[[package]] +name = "addr2line" +version = "0.24.2" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "dfbe277e56a376000877090da837660b4427aad530e3028d44e0bffe4f89a1c1" +dependencies = [ + "gimli", +] + +[[package]] +name = "adler2" +version = "2.0.1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "320119579fcad9c21884f5c4861d16174d0e06250625266f50fe6898340abefa" + +[[package]] +name = "ansi_term" +version = "0.12.1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "d52a9bb7ec0cf484c551830a7ce27bd20d67eac647e1befb56b0be4ee39a55d2" +dependencies = [ + "winapi", +] + +[[package]] +name = "anyhow" +version = "1.0.98" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "e16d2d3311acee920a9eb8d33b8cbc1787ce4a264e85f964c2404b969bdcd487" + +[[package]] +name = "async-trait" +version = "0.1.88" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "e539d3fca749fcee5236ab05e93a52867dd549cc157c8cb7f99595f3cedffdb5" +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 = "atty" +version = "0.2.14" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "d9b39be18770d11421cdb1b9947a45dd3f37e93092cbf377614828a319d5fee8" +dependencies = [ + "hermit-abi", + "libc", + "winapi", +] + +[[package]] +name = "autocfg" +version = "1.5.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "c08606f8c3cbf4ce6ec8e28fb0014a2c086708fe954eaa885384a6165172e7e8" + +[[package]] +name = "backtrace" +version = "0.3.75" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "6806a6321ec58106fea15becdad98371e28d92ccbc7c8f1b3b6dd724fe8f1002" +dependencies = [ + "addr2line", + "cfg-if", + "libc", + "miniz_oxide", + "object", + "rustc-demangle", + "windows-targets 0.52.6", +] + +[[package]] +name = "base64" +version = "0.22.1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "72b3254f16251a8381aa12e40e3c4d2f0199f8c6508fbecb9d91f575e0fbb8c6" + +[[package]] +name = "bitflags" +version = "1.3.2" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "bef38d45163c2f1dde094a7dfd33ccf595c92905c8f8f4fdc18d06fb1037718a" + +[[package]] +name = "bitflags" +version = "2.9.1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "1b8e56985ec62d17e9c1001dc89c88ecd7dc08e47eba5ec7c29c7b5eeecde967" + +[[package]] +name = "block-buffer" +version = "0.10.4" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "3078c7629b62d3f0439517fa394996acacc5cbc91c5a20d8c658e77abd503a71" +dependencies = [ + "generic-array", +] + +[[package]] +name = "bumpalo" +version = "3.19.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "46c5e41b57b8bba42a04676d81cb89e9ee8e859a1a66f80a5a72e1cb76b34d43" + +[[package]] +name = "bytes" +version = "1.10.1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "d71b6127be86fdcfddb610f7182ac57211d4b18a3e9c82eb2d17662f2227ad6a" + +[[package]] +name = "cc" +version = "1.2.30" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "deec109607ca693028562ed836a5f1c4b8bd77755c4e132fc5ce11b0b6211ae7" +dependencies = [ + "shlex", +] + +[[package]] +name = "cesu8" +version = "1.1.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "6d43a04d8753f35258c91f8ec639f792891f748a1edbd759cf1dcea3382ad83c" + +[[package]] +name = "cfg-if" +version = "1.0.1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "9555578bc9e57714c812a1f84e4fc5b4d21fcb063490c624de019f7464c91268" + +[[package]] +name = "clap" +version = "2.34.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "a0610544180c38b88101fecf2dd634b174a62eef6946f84dfc6a7127512b381c" +dependencies = [ + "ansi_term", + "atty", + "bitflags 1.3.2", + "strsim", + "textwrap", + "unicode-width", + "vec_map", +] + +[[package]] +name = "combine" +version = "4.6.7" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "ba5a308b75df32fe02788e748662718f03fde005016435c444eea572398219fd" +dependencies = [ + "bytes", + "memchr", +] + +[[package]] +name = "command-group" +version = "1.0.8" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "f7a8a86f409b4a59df3a3e4bee2de0b83f1755fdd2a25e3a9684c396fc4bed2c" +dependencies = [ + "nix", + "winapi", +] + +[[package]] +name = "core-foundation" +version = "0.10.1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "b2a6cd9ae233e7f62ba4e9353e81a88df7fc8a5987b8d445b4d90c879bd156f6" +dependencies = [ + "core-foundation-sys", + "libc", +] + +[[package]] +name = "core-foundation-sys" +version = "0.8.7" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "773648b94d0e5d620f64f280777445740e61fe701025087ec8b57f45c791888b" + +[[package]] +name = "cpufeatures" +version = "0.2.17" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "59ed5838eebb26a2bb2e58f6d5b5316989ae9d08bab10e0e6d103e656d1b0280" +dependencies = [ + "libc", +] + +[[package]] +name = "crossbeam-deque" +version = "0.8.6" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "9dd111b7b7f7d55b72c0a6ae361660ee5853c9af73f70c3c2ef6858b950e2e51" +dependencies = [ + "crossbeam-epoch", + "crossbeam-utils", +] + +[[package]] +name = "crossbeam-epoch" +version = "0.9.18" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "5b82ac4a3c2ca9c3460964f020e1402edd5753411d7737aa39c3714ad1b5420e" +dependencies = [ + "crossbeam-utils", +] + +[[package]] +name = "crossbeam-utils" +version = "0.8.21" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "d0a5c400df2834b80a4c3327b3aad3a4c4cd4de0629063962b03235697506a28" + +[[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 = "dirs" +version = "5.0.1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "44c45a9d03d6676652bcb5e724c7e988de1acad23a711b5217ab9cbecbec2225" +dependencies = [ + "dirs-sys", +] + +[[package]] +name = "dirs-sys" +version = "0.4.1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "520f05a5cbd335fae5a99ff7a6ab8627577660ee5cfd6a94a6a929b52ff0321c" +dependencies = [ + "libc", + "option-ext", + "redox_users", + "windows-sys 0.48.0", +] + +[[package]] +name = "displaydoc" +version = "0.2.5" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "97369cbbc041bc366949bc74d34658d6cda5621039731c6310521892a3a20ae0" +dependencies = [ + "proc-macro2", + "quote", + "syn", +] + +[[package]] +name = "doctest-file" +version = "1.0.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "aac81fa3e28d21450aa4d2ac065992ba96a1d7303efbce51a95f4fd175b67562" + +[[package]] +name = "either" +version = "1.15.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "48c757948c5ede0e46177b7add2e67155f70e33c07fea8284df6576da70b3719" + +[[package]] +name = "equivalent" +version = "1.0.2" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "877a4ace8713b0bcf2a4e7eec82529c029f1d0619886d18145fea96c3ffe5c0f" + +[[package]] +name = "errno" +version = "0.3.13" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "778e2ac28f6c47af28e4907f13ffd1e1ddbd400980a9abd7c8df189bf578a5ad" +dependencies = [ + "libc", + "windows-sys 0.60.2", +] + +[[package]] +name = "fastrand" +version = "2.3.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "37909eebbb50d72f9059c3b6d82c0463f2ff062c9e95845c43a6c9c0355411be" + +[[package]] +name = "fern" +version = "0.6.2" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "d9f0c14694cbd524c8720dd69b0e3179344f04ebb5f90f2e4a440c6ea3b2f1ee" +dependencies = [ + "log", +] + +[[package]] +name = "fnv" +version = "1.0.7" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "3f9eec918d3f24069decb9af1554cad7c880e2da24a9afd88aca000531ab82c1" + +[[package]] +name = "form_urlencoded" +version = "1.2.1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "e13624c2627564efccf4934284bdd98cbaa14e79b0b5a141218e507b3a823456" +dependencies = [ + "percent-encoding", +] + +[[package]] +name = "futures" +version = "0.3.31" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "65bc07b1a8bc7c85c5f2e110c476c7389b4554ba72af57d8445ea63a576b0876" +dependencies = [ + "futures-channel", + "futures-core", + "futures-executor", + "futures-io", + "futures-sink", + "futures-task", + "futures-util", +] + +[[package]] +name = "futures-channel" +version = "0.3.31" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "2dff15bf788c671c1934e366d07e30c1814a8ef514e1af724a602e8a2fbe1b10" +dependencies = [ + "futures-core", + "futures-sink", +] + +[[package]] +name = "futures-core" +version = "0.3.31" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "05f29059c0c2090612e8d742178b0580d2dc940c837851ad723096f87af6663e" + +[[package]] +name = "futures-executor" +version = "0.3.31" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "1e28d1d997f585e54aebc3f97d39e72338912123a67330d723fdbb564d646c9f" +dependencies = [ + "futures-core", + "futures-task", + "futures-util", +] + +[[package]] +name = "futures-io" +version = "0.3.31" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "9e5c1b78ca4aae1ac06c48a526a655760685149f0d465d21f37abfe57ce075c6" + +[[package]] +name = "futures-macro" +version = "0.3.31" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "162ee34ebcb7c64a8abebc059ce0fee27c2262618d7b60ed8faf72fef13c3650" +dependencies = [ + "proc-macro2", + "quote", + "syn", +] + +[[package]] +name = "futures-sink" +version = "0.3.31" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "e575fab7d1e0dcb8d0c7bcf9a63ee213816ab51902e6d244a95819acacf1d4f7" + +[[package]] +name = "futures-task" +version = "0.3.31" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "f90f7dce0722e95104fcb095585910c0977252f286e354b5e3bd38902cd99988" + +[[package]] +name = "futures-timer" +version = "3.0.3" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "f288b0a4f20f9a56b5d1da57e2227c661b7b16168e2f72365f57b63326e29b24" +dependencies = [ + "gloo-timers", + "send_wrapper", +] + +[[package]] +name = "futures-util" +version = "0.3.31" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "9fa08315bb612088cc391249efdc3bc77536f16c91f6cf495e6fbe85b20a4a81" +dependencies = [ + "futures-channel", + "futures-core", + "futures-io", + "futures-macro", + "futures-sink", + "futures-task", + "memchr", + "pin-project-lite", + "pin-utils", + "slab", +] + +[[package]] +name = "generic-array" +version = "0.14.7" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "85649ca51fd72272d7821adaf274ad91c288277713d9c18820d8499a7ff69e9a" +dependencies = [ + "typenum", + "version_check", +] + +[[package]] +name = "getrandom" +version = "0.2.16" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "335ff9f135e4384c8150d6f27c6daed433577f86b4750418338c01a1a2528592" +dependencies = [ + "cfg-if", + "libc", + "wasi 0.11.1+wasi-snapshot-preview1", +] + +[[package]] +name = "getrandom" +version = "0.3.3" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "26145e563e54f2cadc477553f1ec5ee650b00862f0a58bcd12cbdc5f0ea2d2f4" +dependencies = [ + "cfg-if", + "libc", + "r-efi", + "wasi 0.14.2+wasi-0.2.4", +] + +[[package]] +name = "gimli" +version = "0.31.1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "07e28edb80900c19c28f1072f2e8aeca7fa06b23cd4169cefe1af5aa3260783f" + +[[package]] +name = "git-version" +version = "0.3.9" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "1ad568aa3db0fcbc81f2f116137f263d7304f512a1209b35b85150d3ef88ad19" +dependencies = [ + "git-version-macro", +] + +[[package]] +name = "git-version-macro" +version = "0.3.9" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "53010ccb100b96a67bc32c0175f0ed1426b31b655d562898e57325f81c023ac0" +dependencies = [ + "proc-macro2", + "quote", + "syn", +] + +[[package]] +name = "gloo-net" +version = "0.6.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "c06f627b1a58ca3d42b45d6104bf1e1a03799df472df00988b6ba21accc10580" +dependencies = [ + "futures-channel", + "futures-core", + "futures-sink", + "gloo-utils", + "http", + "js-sys", + "pin-project", + "serde", + "serde_json", + "thiserror 1.0.69", + "wasm-bindgen", + "wasm-bindgen-futures", + "web-sys", +] + +[[package]] +name = "gloo-timers" +version = "0.2.6" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "9b995a66bb87bebce9a0f4a95aed01daca4872c050bfcb21653361c03bc35e5c" +dependencies = [ + "futures-channel", + "futures-core", + "js-sys", + "wasm-bindgen", +] + +[[package]] +name = "gloo-utils" +version = "0.2.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "0b5555354113b18c547c1d3a98fbf7fb32a9ff4f6fa112ce823a21641a0ba3aa" +dependencies = [ + "js-sys", + "serde", + "serde_json", + "wasm-bindgen", + "web-sys", +] + +[[package]] +name = "h2" +version = "0.4.11" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "17da50a276f1e01e0ba6c029e47b7100754904ee8a278f886546e98575380785" +dependencies = [ + "atomic-waker", + "bytes", + "fnv", + "futures-core", + "futures-sink", + "http", + "indexmap 2.10.0", + "slab", + "tokio", + "tokio-util", + "tracing", +] + +[[package]] +name = "hashbrown" +version = "0.12.3" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "8a9ee70c43aaf417c914396645a0fa852624801b24ebb7ae78fe8272889ac888" + +[[package]] +name = "hashbrown" +version = "0.15.4" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "5971ac85611da7067dbfcabef3c70ebb5606018acd9e2a3903a0da507521e0d5" + +[[package]] +name = "heck" +version = "0.5.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "2304e00983f87ffb38b55b444b5e3b60a884b5d30c0fca7d82fe33449bbe55ea" + +[[package]] +name = "hermit-abi" +version = "0.1.19" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "62b467343b94ba476dcb2500d242dadbb39557df889310ac77c5d99100aaac33" +dependencies = [ + "libc", +] + +[[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.6.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "cc2b571658e38e0c01b1fdca3bbbe93c00d3d71693ff2770043f8c29bc7d6f80" +dependencies = [ + "bytes", + "futures-channel", + "futures-util", + "h2", + "http", + "http-body", + "httparse", + "httpdate", + "itoa", + "pin-project-lite", + "smallvec", + "tokio", + "want", +] + +[[package]] +name = "hyper-rustls" +version = "0.27.7" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "e3c93eb611681b207e1fe55d5a71ecf91572ec8a6705cdb6857f7d8d5242cf58" +dependencies = [ + "http", + "hyper", + "hyper-util", + "log", + "rustls", + "rustls-pki-types", + "tokio", + "tokio-rustls", + "tower-service", +] + +[[package]] +name = "hyper-util" +version = "0.1.16" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "8d9b05277c7e8da2c93a568989bb6207bef0112e8d17df7a6eda4a3cf143bc5e" +dependencies = [ + "bytes", + "futures-channel", + "futures-core", + "futures-util", + "http", + "http-body", + "hyper", + "libc", + "pin-project-lite", + "socket2 0.6.0", + "tokio", + "tower-service", + "tracing", +] + +[[package]] +name = "icu_collections" +version = "2.0.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "200072f5d0e3614556f94a9930d5dc3e0662a652823904c3a75dc3b0af7fee47" +dependencies = [ + "displaydoc", + "potential_utf", + "yoke", + "zerofrom", + "zerovec", +] + +[[package]] +name = "icu_locale_core" +version = "2.0.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "0cde2700ccaed3872079a65fb1a78f6c0a36c91570f28755dda67bc8f7d9f00a" +dependencies = [ + "displaydoc", + "litemap", + "tinystr", + "writeable", + "zerovec", +] + +[[package]] +name = "icu_normalizer" +version = "2.0.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "436880e8e18df4d7bbc06d58432329d6458cc84531f7ac5f024e93deadb37979" +dependencies = [ + "displaydoc", + "icu_collections", + "icu_normalizer_data", + "icu_properties", + "icu_provider", + "smallvec", + "zerovec", +] + +[[package]] +name = "icu_normalizer_data" +version = "2.0.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "00210d6893afc98edb752b664b8890f0ef174c8adbb8d0be9710fa66fbbf72d3" + +[[package]] +name = "icu_properties" +version = "2.0.1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "016c619c1eeb94efb86809b015c58f479963de65bdb6253345c1a1276f22e32b" +dependencies = [ + "displaydoc", + "icu_collections", + "icu_locale_core", + "icu_properties_data", + "icu_provider", + "potential_utf", + "zerotrie", + "zerovec", +] + +[[package]] +name = "icu_properties_data" +version = "2.0.1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "298459143998310acd25ffe6810ed544932242d3f07083eee1084d83a71bd632" + +[[package]] +name = "icu_provider" +version = "2.0.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "03c80da27b5f4187909049ee2d72f276f0d9f99a42c306bd0131ecfe04d8e5af" +dependencies = [ + "displaydoc", + "icu_locale_core", + "stable_deref_trait", + "tinystr", + "writeable", + "yoke", + "zerofrom", + "zerotrie", + "zerovec", +] + +[[package]] +name = "idna" +version = "1.0.3" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "686f825264d630750a544639377bae737628043f20d38bbc029e8f29ea968a7e" +dependencies = [ + "idna_adapter", + "smallvec", + "utf8_iter", +] + +[[package]] +name = "idna_adapter" +version = "1.2.1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "3acae9609540aa318d1bc588455225fb2085b9ed0c4f6bd0d9d5bcd86f1a0344" +dependencies = [ + "icu_normalizer", + "icu_properties", +] + +[[package]] +name = "indexmap" +version = "1.9.3" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "bd070e393353796e801d209ad339e89596eb4c8d430d18ede6a1cced8fafbd99" +dependencies = [ + "autocfg", + "hashbrown 0.12.3", +] + +[[package]] +name = "indexmap" +version = "2.10.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "fe4cd85333e22411419a0bcae1297d25e58c9443848b11dc6a86fefe8c78a661" +dependencies = [ + "equivalent", + "hashbrown 0.15.4", +] + +[[package]] +name = "interprocess" +version = "2.2.3" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "d941b405bd2322993887859a8ee6ac9134945a24ec5ec763a8a962fc64dfec2d" +dependencies = [ + "doctest-file", + "futures-core", + "libc", + "recvmsg", + "tokio", + "widestring", + "windows-sys 0.52.0", +] + +[[package]] +name = "io-uring" +version = "0.7.9" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "d93587f37623a1a17d94ef2bc9ada592f5465fe7732084ab7beefabe5c77c0c4" +dependencies = [ + "bitflags 2.9.1", + "cfg-if", + "libc", +] + +[[package]] +name = "itoa" +version = "1.0.15" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "4a5f13b858c8d314ee3e8f639011f7ccefe71f97f96e50151fb991f267928e2c" + +[[package]] +name = "jni" +version = "0.21.1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "1a87aa2bb7d2af34197c04845522473242e1aa17c12f4935d5856491a7fb8c97" +dependencies = [ + "cesu8", + "cfg-if", + "combine", + "jni-sys", + "log", + "thiserror 1.0.69", + "walkdir", + "windows-sys 0.45.0", +] + +[[package]] +name = "jni-sys" +version = "0.3.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "8eaf4bc02d17cbdd7ff4c7438cafcdf7fb9a4613313ad11b4f8fefe7d3fa0130" + +[[package]] +name = "js-sys" +version = "0.3.77" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "1cfaf33c695fc6e08064efbc1f72ec937429614f25eef83af942d0e227c3a28f" +dependencies = [ + "once_cell", + "wasm-bindgen", +] + +[[package]] +name = "jsonrpsee" +version = "0.25.1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "1fba77a59c4c644fd48732367624d1bcf6f409f9c9a286fbc71d2f1fc0b2ea16" +dependencies = [ + "jsonrpsee-client-transport", + "jsonrpsee-core", + "jsonrpsee-http-client", + "jsonrpsee-proc-macros", + "jsonrpsee-server", + "jsonrpsee-types", + "jsonrpsee-wasm-client", + "jsonrpsee-ws-client", + "tokio", + "tracing", +] + +[[package]] +name = "jsonrpsee-client-transport" +version = "0.25.1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "a2a320a3f1464e4094f780c4d48413acd786ce5627aaaecfac9e9c7431d13ae1" +dependencies = [ + "base64", + "futures-channel", + "futures-util", + "gloo-net", + "http", + "jsonrpsee-core", + "pin-project", + "rustls", + "rustls-pki-types", + "rustls-platform-verifier", + "soketto", + "thiserror 2.0.12", + "tokio", + "tokio-rustls", + "tokio-util", + "tracing", + "url", +] + +[[package]] +name = "jsonrpsee-core" +version = "0.25.1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "693c93cbb7db25f4108ed121304b671a36002c2db67dff2ee4391a688c738547" +dependencies = [ + "async-trait", + "bytes", + "futures-timer", + "futures-util", + "http", + "http-body", + "http-body-util", + "jsonrpsee-types", + "parking_lot", + "pin-project", + "rand 0.9.2", + "rustc-hash", + "serde", + "serde_json", + "thiserror 2.0.12", + "tokio", + "tokio-stream", + "tower", + "tracing", + "wasm-bindgen-futures", +] + +[[package]] +name = "jsonrpsee-http-client" +version = "0.25.1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "6962d2bd295f75e97dd328891e58fce166894b974c1f7ce2e7597f02eeceb791" +dependencies = [ + "base64", + "http-body", + "hyper", + "hyper-rustls", + "hyper-util", + "jsonrpsee-core", + "jsonrpsee-types", + "rustls", + "rustls-platform-verifier", + "serde", + "serde_json", + "thiserror 2.0.12", + "tokio", + "tower", + "url", +] + +[[package]] +name = "jsonrpsee-proc-macros" +version = "0.25.1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "2fa4f5daed39f982a1bb9d15449a28347490ad42b212f8eaa2a2a344a0dce9e9" +dependencies = [ + "heck", + "proc-macro-crate", + "proc-macro2", + "quote", + "syn", +] + +[[package]] +name = "jsonrpsee-server" +version = "0.25.1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "d38b0bcf407ac68d241f90e2d46041e6a06988f97fe1721fb80b91c42584fae6" +dependencies = [ + "futures-util", + "http", + "http-body", + "http-body-util", + "hyper", + "hyper-util", + "jsonrpsee-core", + "jsonrpsee-types", + "pin-project", + "route-recognizer", + "serde", + "serde_json", + "soketto", + "thiserror 2.0.12", + "tokio", + "tokio-stream", + "tokio-util", + "tower", + "tracing", +] + +[[package]] +name = "jsonrpsee-types" +version = "0.25.1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "66df7256371c45621b3b7d2fb23aea923d577616b9c0e9c0b950a6ea5c2be0ca" +dependencies = [ + "http", + "serde", + "serde_json", + "thiserror 2.0.12", +] + +[[package]] +name = "jsonrpsee-wasm-client" +version = "0.25.1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "6b67695cbcf4653f39f8f8738925547e0e23fd9fe315bccf951097b9f6a38781" +dependencies = [ + "jsonrpsee-client-transport", + "jsonrpsee-core", + "jsonrpsee-types", + "tower", +] + +[[package]] +name = "jsonrpsee-ws-client" +version = "0.25.1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "2da2694c9ff271a9d3ebfe520f6b36820e85133a51be77a3cb549fd615095261" +dependencies = [ + "http", + "jsonrpsee-client-transport", + "jsonrpsee-core", + "jsonrpsee-types", + "tower", + "url", +] + +[[package]] +name = "libc" +version = "0.2.174" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "1171693293099992e19cddea4e8b849964e9846f4acee11b3948bcc337be8776" + +[[package]] +name = "libredox" +version = "0.1.6" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "4488594b9328dee448adb906d8b126d9b7deb7cf5c22161ee591610bb1be83c0" +dependencies = [ + "bitflags 2.9.1", + "libc", +] + +[[package]] +name = "linked-hash-map" +version = "0.5.6" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "0717cef1bc8b636c6e1c1bbdefc09e6322da8a9321966e8928ef80d20f7f770f" + +[[package]] +name = "linux-raw-sys" +version = "0.9.4" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "cd945864f07fe9f5371a27ad7b52a172b4b499999f1d97574c9fa68373937e12" + +[[package]] +name = "litemap" +version = "0.8.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "241eaef5fd12c88705a01fc1066c48c4b36e0dd4377dcdc7ec3942cea7a69956" + +[[package]] +name = "lock_api" +version = "0.4.13" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "96936507f153605bddfcda068dd804796c84324ed2510809e5b2a624c81da765" +dependencies = [ + "autocfg", + "scopeguard", +] + +[[package]] +name = "log" +version = "0.4.27" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "13dc2df351e3202783a1fe0d44375f7295ffb4049267b0f3018346dc122a1d94" + +[[package]] +name = "memchr" +version = "2.7.5" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "32a282da65faaf38286cf3be983213fcf1d2e2a58700e808f83f4ea9a4804bc0" + +[[package]] +name = "memoffset" +version = "0.6.5" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "5aa361d4faea93603064a027415f07bd8e1d5c88c9fbf68bf56a285428fd79ce" +dependencies = [ + "autocfg", +] + +[[package]] +name = "miniz_oxide" +version = "0.8.9" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "1fa76a2c86f704bdb222d66965fb3d63269ce38518b83cb0575fca855ebb6316" +dependencies = [ + "adler2", +] + +[[package]] +name = "mio" +version = "1.0.4" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "78bed444cc8a2160f01cbcf811ef18cac863ad68ae8ca62092e8db51d51c761c" +dependencies = [ + "libc", + "wasi 0.11.1+wasi-snapshot-preview1", + "windows-sys 0.59.0", +] + +[[package]] +name = "nix" +version = "0.22.3" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "e4916f159ed8e5de0082076562152a76b7a1f64a01fd9d1e0fea002c37624faf" +dependencies = [ + "bitflags 1.3.2", + "cc", + "cfg-if", + "libc", + "memoffset", +] + +[[package]] +name = "ntapi" +version = "0.4.1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "e8a3895c6391c39d7fe7ebc444a87eb2991b2a0bc718fdabd071eec617fc68e4" +dependencies = [ + "winapi", +] + +[[package]] +name = "object" +version = "0.36.7" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "62948e14d923ea95ea2c7c86c71013138b66525b86bdc08d2dcc262bdb497b87" +dependencies = [ + "memchr", +] + +[[package]] +name = "once_cell" +version = "1.21.3" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "42f5e15c9953c5e4ccceeb2e7382a716482c34515315f7b03532b8b4e8393d2d" + +[[package]] +name = "openssl-probe" +version = "0.1.6" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "d05e27ee213611ffe7d6348b942e8f942b37114c00cc03cec254295a4a17852e" + +[[package]] +name = "option-ext" +version = "0.2.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "04744f49eae99ab78e0d5c0b603ab218f515ea8cfe5a456d7629ad883a3b6e7d" + +[[package]] +name = "parking_lot" +version = "0.12.4" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "70d58bf43669b5795d1576d0641cfb6fbb2057bf629506267a92807158584a13" +dependencies = [ + "lock_api", + "parking_lot_core", +] + +[[package]] +name = "parking_lot_core" +version = "0.9.11" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "bc838d2a56b5b1a6c25f55575dfc605fabb63bb2365f6c2353ef9159aa69e4a5" +dependencies = [ + "cfg-if", + "libc", + "redox_syscall", + "smallvec", + "windows-targets 0.52.6", +] + +[[package]] +name = "percent-encoding" +version = "2.3.1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "e3148f5046208a5d56bcfc03053e3ca6334e51da8dfb19b6cdc8b306fae3283e" + +[[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 = "potential_utf" +version = "0.1.2" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "e5a7c30837279ca13e7c867e9e40053bc68740f988cb07f7ca6df43cc734b585" +dependencies = [ + "zerovec", +] + +[[package]] +name = "ppv-lite86" +version = "0.2.21" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "85eae3c4ed2f50dcfe72643da4befc30deadb458a9b590d720cde2f2b1e97da9" +dependencies = [ + "zerocopy", +] + +[[package]] +name = "proc-macro-crate" +version = "3.3.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "edce586971a4dfaa28950c6f18ed55e0406c1ab88bbce2c6f6293a7aaba73d35" +dependencies = [ + "toml_edit", +] + +[[package]] +name = "proc-macro2" +version = "1.0.95" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "02b3e5e68a3a1a02aad3ec490a98007cbc13c37cbe84a3cd7b8e406d76e7f778" +dependencies = [ + "unicode-ident", +] + +[[package]] +name = "quote" +version = "1.0.40" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "1885c039570dc00dcb4ff087a89e185fd56bae234ddc7f056a945bf36467248d" +dependencies = [ + "proc-macro2", +] + +[[package]] +name = "r-efi" +version = "5.3.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "69cdb34c158ceb288df11e18b4bd39de994f6657d83847bdffdbd7f346754b0f" + +[[package]] +name = "rand" +version = "0.8.5" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "34af8d1a0e25924bc5b7c43c079c942339d8f0a8b57c39049bef581b46327404" +dependencies = [ + "libc", + "rand_chacha 0.3.1", + "rand_core 0.6.4", +] + +[[package]] +name = "rand" +version = "0.9.2" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "6db2770f06117d490610c7488547d543617b21bfa07796d7a12f6f1bd53850d1" +dependencies = [ + "rand_chacha 0.9.0", + "rand_core 0.9.3", +] + +[[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 0.6.4", +] + +[[package]] +name = "rand_chacha" +version = "0.9.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "d3022b5f1df60f26e1ffddd6c66e8aa15de382ae63b3a0c1bfc0e4d3e3f325cb" +dependencies = [ + "ppv-lite86", + "rand_core 0.9.3", +] + +[[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 = "rand_core" +version = "0.9.3" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "99d9a13982dcf210057a8a78572b2217b667c3beacbf3a0d8b454f6f82837d38" +dependencies = [ + "getrandom 0.3.3", +] + +[[package]] +name = "rayon" +version = "1.10.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "b418a60154510ca1a002a752ca9714984e21e4241e804d32555251faf8b78ffa" +dependencies = [ + "either", + "rayon-core", +] + +[[package]] +name = "rayon-core" +version = "1.12.1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "1465873a3dfdaa8ae7cb14b4383657caab0b3e8a0aa9ae8e04b044854c8dfce2" +dependencies = [ + "crossbeam-deque", + "crossbeam-utils", +] + +[[package]] +name = "recvmsg" +version = "1.0.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "d3edd4d5d42c92f0a659926464d4cce56b562761267ecf0f469d85b7de384175" + +[[package]] +name = "redox_syscall" +version = "0.5.15" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "7e8af0dde094006011e6a740d4879319439489813bd0bcdc7d821beaeeff48ec" +dependencies = [ + "bitflags 2.9.1", +] + +[[package]] +name = "redox_users" +version = "0.4.6" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "ba009ff324d1fc1b900bd1fdb31564febe58a8ccc8a6fdbb93b543d33b13ca43" +dependencies = [ + "getrandom 0.2.16", + "libredox", + "thiserror 1.0.69", +] + +[[package]] +name = "reth-ipc" +version = "1.6.0" +source = "git+https://github.com/paradigmxyz/reth#876e964cbcbd85ade6fdec40bb35f08690332854" +dependencies = [ + "bytes", + "futures", + "futures-util", + "interprocess", + "jsonrpsee", + "pin-project", + "serde_json", + "thiserror 2.0.12", + "tokio", + "tokio-stream", + "tokio-util", + "tower", + "tracing", +] + +[[package]] +name = "ring" +version = "0.17.14" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "a4689e6c2294d81e88dc6261c768b63bc4fcdb852be6d1352498b114f61383b7" +dependencies = [ + "cc", + "cfg-if", + "getrandom 0.2.16", + "libc", + "untrusted", + "windows-sys 0.52.0", +] + +[[package]] +name = "route-recognizer" +version = "0.3.1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "afab94fb28594581f62d981211a9a4d53cc8130bbcbbb89a0440d9b8e81a7746" + +[[package]] +name = "rustc-demangle" +version = "0.1.25" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "989e6739f80c4ad5b13e0fd7fe89531180375b18520cc8c82080e4dc4035b84f" + +[[package]] +name = "rustc-hash" +version = "2.1.1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "357703d41365b4b27c590e3ed91eabb1b663f07c4c084095e60cbed4362dff0d" + +[[package]] +name = "rustix" +version = "1.0.8" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "11181fbabf243db407ef8df94a6ce0b2f9a733bd8be4ad02b4eda9602296cac8" +dependencies = [ + "bitflags 2.9.1", + "errno", + "libc", + "linux-raw-sys", + "windows-sys 0.60.2", +] + +[[package]] +name = "rustls" +version = "0.23.29" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "2491382039b29b9b11ff08b76ff6c97cf287671dbb74f0be44bda389fffe9bd1" +dependencies = [ + "log", + "once_cell", + "ring", + "rustls-pki-types", + "rustls-webpki", + "subtle", + "zeroize", +] + +[[package]] +name = "rustls-native-certs" +version = "0.8.1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "7fcff2dd52b58a8d98a70243663a0d234c4e2b79235637849d15913394a247d3" +dependencies = [ + "openssl-probe", + "rustls-pki-types", + "schannel", + "security-framework", +] + +[[package]] +name = "rustls-pki-types" +version = "1.12.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "229a4a4c221013e7e1f1a043678c5cc39fe5171437c88fb47151a21e6f5b5c79" +dependencies = [ + "zeroize", +] + +[[package]] +name = "rustls-platform-verifier" +version = "0.5.3" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "19787cda76408ec5404443dc8b31795c87cd8fec49762dc75fa727740d34acc1" +dependencies = [ + "core-foundation", + "core-foundation-sys", + "jni", + "log", + "once_cell", + "rustls", + "rustls-native-certs", + "rustls-platform-verifier-android", + "rustls-webpki", + "security-framework", + "security-framework-sys", + "webpki-root-certs 0.26.11", + "windows-sys 0.59.0", +] + +[[package]] +name = "rustls-platform-verifier-android" +version = "0.1.1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "f87165f0995f63a9fbeea62b64d10b4d9d8e78ec6d7d51fb2125fda7bb36788f" + +[[package]] +name = "rustls-webpki" +version = "0.103.4" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "0a17884ae0c1b773f1ccd2bd4a8c72f16da897310a98b0e84bf349ad5ead92fc" +dependencies = [ + "ring", + "rustls-pki-types", + "untrusted", +] + +[[package]] +name = "rustversion" +version = "1.0.21" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "8a0d197bd2c9dc6e53b84da9556a69ba4cdfab8619eb41a8bd1cc2027a0f6b1d" + +[[package]] +name = "ryu" +version = "1.0.20" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "28d3b2b1366ec20994f1fd18c3c594f05c5dd4bc44d8bb0c1c632c8d6829481f" + +[[package]] +name = "same-file" +version = "1.0.6" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "93fc1dc3aaa9bfed95e02e6eadabb4baf7e3078b0bd1b4d7b6b0b68378900502" +dependencies = [ + "winapi-util", +] + +[[package]] +name = "schannel" +version = "0.1.27" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "1f29ebaa345f945cec9fbbc532eb307f0fdad8161f281b6369539c8d84876b3d" +dependencies = [ + "windows-sys 0.59.0", +] + +[[package]] +name = "scopeguard" +version = "1.2.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "94143f37725109f92c262ed2cf5e59bce7498c01bcc1502d7b9afe439a4e9f49" + +[[package]] +name = "security-framework" +version = "3.2.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "271720403f46ca04f7ba6f55d438f8bd878d6b8ca0a1046e8228c4145bcbb316" +dependencies = [ + "bitflags 2.9.1", + "core-foundation", + "core-foundation-sys", + "libc", + "security-framework-sys", +] + +[[package]] +name = "security-framework-sys" +version = "2.14.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "49db231d56a190491cb4aeda9527f1ad45345af50b0851622a7adb8c03b01c32" +dependencies = [ + "core-foundation-sys", + "libc", +] + +[[package]] +name = "send_wrapper" +version = "0.4.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "f638d531eccd6e23b980caf34876660d38e265409d8e99b397ab71eb3612fad0" + +[[package]] +name = "serde" +version = "1.0.219" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "5f0e2c6ed6606019b4e29e69dbaba95b11854410e5347d525002456dbbb786b6" +dependencies = [ + "serde_derive", +] + +[[package]] +name = "serde_derive" +version = "1.0.219" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "5b0276cf7f2c73365f7157c8123c21cd9a50fbbd844757af28ca1f5925fc2a00" +dependencies = [ + "proc-macro2", + "quote", + "syn", +] + +[[package]] +name = "serde_json" +version = "1.0.141" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "30b9eff21ebe718216c6ec64e1d9ac57087aad11efc64e32002bce4a0d4c03d3" +dependencies = [ + "itoa", + "memchr", + "ryu", + "serde", +] + +[[package]] +name = "serde_yaml" +version = "0.8.26" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "578a7433b776b56a35785ed5ce9a7e777ac0598aac5a6dd1b4b18a307c7fc71b" +dependencies = [ + "indexmap 1.9.3", + "ryu", + "serde", + "yaml-rust", +] + +[[package]] +name = "sha1" +version = "0.10.6" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "e3bf829a2d51ab4a5ddf1352d8470c140cadc8301b2ae1789db023f01cedd6ba" +dependencies = [ + "cfg-if", + "cpufeatures", + "digest", +] + +[[package]] +name = "shlex" +version = "1.3.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "0fda2ff0d084019ba4d7c6f371c95d8fd75ce3524c3cb8fb653a3023f6323e64" + +[[package]] +name = "signal-hook-registry" +version = "1.4.5" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "9203b8055f63a2a00e2f593bb0510367fe707d7ff1e5c872de2f537b339e5410" +dependencies = [ + "libc", +] + +[[package]] +name = "slab" +version = "0.4.10" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "04dc19736151f35336d325007ac991178d504a119863a2fcb3758cdb5e52c50d" + +[[package]] +name = "smallvec" +version = "1.15.1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "67b1b7a3b5fe4f1376887184045fcf45c69e92af734b7aaddc05fb777b6fbd03" + +[[package]] +name = "socket2" +version = "0.5.10" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "e22376abed350d73dd1cd119b57ffccad95b4e585a7cda43e286245ce23c0678" +dependencies = [ + "libc", + "windows-sys 0.52.0", +] + +[[package]] +name = "socket2" +version = "0.6.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "233504af464074f9d066d7b5416c5f9b894a5862a6506e306f7b816cdd6f1807" +dependencies = [ + "libc", + "windows-sys 0.59.0", +] + +[[package]] +name = "soketto" +version = "0.8.1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "2e859df029d160cb88608f5d7df7fb4753fd20fdfb4de5644f3d8b8440841721" +dependencies = [ + "base64", + "bytes", + "futures", + "http", + "httparse", + "log", + "rand 0.8.5", + "sha1", +] + +[[package]] +name = "stable_deref_trait" +version = "1.2.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "a8f112729512f8e442d81f95a8a7ddf2b7c6b8a1a6f509a95864142b30cab2d3" + +[[package]] +name = "strsim" +version = "0.8.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "8ea5119cdb4c55b55d432abb513a0429384878c15dde60cc77b1c99de1a95a6a" + +[[package]] +name = "subtle" +version = "2.6.1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "13c2bddecc57b384dee18652358fb23172facb8a2c51ccc10d74c157bdea3292" + +[[package]] +name = "syn" +version = "2.0.104" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "17b6f705963418cdb9927482fa304bc562ece2fdd4f616084c50b7023b435a40" +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 = "synstructure" +version = "0.13.2" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "728a70f3dbaf5bab7f0c4b1ac8d7ae5ea60a4b5549c8a5914361c99147a709d2" +dependencies = [ + "proc-macro2", + "quote", + "syn", +] + +[[package]] +name = "sysinfo" +version = "0.29.11" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "cd727fc423c2060f6c92d9534cef765c65a6ed3f428a03d7def74a8c4348e666" +dependencies = [ + "cfg-if", + "core-foundation-sys", + "libc", + "ntapi", + "once_cell", + "rayon", + "winapi", +] + +[[package]] +name = "tempfile" +version = "3.20.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "e8a64e3985349f2441a1a9ef0b853f869006c3855f2cda6862a94d26ebb9d6a1" +dependencies = [ + "fastrand", + "getrandom 0.3.3", + "once_cell", + "rustix", + "windows-sys 0.59.0", +] + +[[package]] +name = "textwrap" +version = "0.11.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "d326610f408c7a4eb6f51c37c330e496b08506c9457c9d34287ecc38809fb060" +dependencies = [ + "unicode-width", +] + +[[package]] +name = "thiserror" +version = "1.0.69" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "b6aaf5339b578ea85b50e080feb250a3e8ae8cfcdff9a461c9ec2904bc923f52" +dependencies = [ + "thiserror-impl 1.0.69", +] + +[[package]] +name = "thiserror" +version = "2.0.12" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "567b8a2dae586314f7be2a752ec7474332959c6460e02bde30d702a66d488708" +dependencies = [ + "thiserror-impl 2.0.12", +] + +[[package]] +name = "thiserror-impl" +version = "1.0.69" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "4fee6c4efc90059e10f81e6d42c60a18f76588c3d74cb83a0b242a2b6c7504c1" +dependencies = [ + "proc-macro2", + "quote", + "syn", +] + +[[package]] +name = "thiserror-impl" +version = "2.0.12" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "7f7cf42b4507d8ea322120659672cf1b9dbb93f8f2d4ecfd6e51350ff5b17a1d" +dependencies = [ + "proc-macro2", + "quote", + "syn", +] + +[[package]] +name = "tinystr" +version = "0.8.1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "5d4f6d1145dcb577acf783d4e601bc1d76a13337bb54e6233add580b07344c8b" +dependencies = [ + "displaydoc", + "zerovec", +] + +[[package]] +name = "tokio" +version = "1.46.1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "0cc3a2344dafbe23a245241fe8b09735b521110d30fcefbbd5feb1797ca35d17" +dependencies = [ + "backtrace", + "bytes", + "io-uring", + "libc", + "mio", + "parking_lot", + "pin-project-lite", + "signal-hook-registry", + "slab", + "socket2 0.5.10", + "tokio-macros", + "windows-sys 0.52.0", +] + +[[package]] +name = "tokio-macros" +version = "2.5.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "6e06d43f1345a3bcd39f6a56dbb7dcab2ba47e68e8ac134855e7e2bdbaf8cab8" +dependencies = [ + "proc-macro2", + "quote", + "syn", +] + +[[package]] +name = "tokio-rustls" +version = "0.26.2" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "8e727b36a1a0e8b74c376ac2211e40c2c8af09fb4013c60d910495810f008e9b" +dependencies = [ + "rustls", + "tokio", +] + +[[package]] +name = "tokio-stream" +version = "0.1.17" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "eca58d7bba4a75707817a2c44174253f9236b2d5fbd055602e9d5c07c139a047" +dependencies = [ + "futures-core", + "pin-project-lite", + "tokio", + "tokio-util", +] + +[[package]] +name = "tokio-util" +version = "0.7.15" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "66a539a9ad6d5d281510d5bd368c973d636c02dbf8a67300bfb6b950696ad7df" +dependencies = [ + "bytes", + "futures-core", + "futures-io", + "futures-sink", + "pin-project-lite", + "tokio", +] + +[[package]] +name = "toml_datetime" +version = "0.6.11" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "22cddaf88f4fbc13c51aebbf5f8eceb5c7c5a9da2ac40a13519eb5b0a0e8f11c" + +[[package]] +name = "toml_edit" +version = "0.22.27" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "41fe8c660ae4257887cf66394862d21dbca4a6ddd26f04a3560410406a2f819a" +dependencies = [ + "indexmap 2.10.0", + "toml_datetime", + "winnow", +] + +[[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-http" +version = "0.5.2" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "1e9cd434a998747dd2c4276bc96ee2e0c7a2eadf3cae88e52be55a05fa9053f5" +dependencies = [ + "bitflags 2.9.1", + "bytes", + "http", + "http-body", + "http-body-util", + "pin-project-lite", + "tower-layer", + "tower-service", +] + +[[package]] +name = "tower-layer" +version = "0.3.3" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "121c2a6cda46980bb0fcd1647ffaf6cd3fc79a013de288782836f6df9c48780e" + +[[package]] +name = "tower-service" +version = "0.3.3" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "8df9b6e13f2d32c91b9bd719c00d1958837bc7dec474d94952798cc8e69eeec3" + +[[package]] +name = "tracing" +version = "0.1.41" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "784e0ac535deb450455cbfa28a6f0df145ea1bb7ae51b821cf5e7927fdcfbdd0" +dependencies = [ + "pin-project-lite", + "tracing-attributes", + "tracing-core", +] + +[[package]] +name = "tracing-attributes" +version = "0.1.30" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "81383ab64e72a7a8b8e13130c49e3dab29def6d0c7d76a03087b3cf71c5c6903" +dependencies = [ + "proc-macro2", + "quote", + "syn", +] + +[[package]] +name = "tracing-core" +version = "0.1.34" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "b9d12581f227e93f094d3af2ae690a574abb8a2b9b7a96e7cfe9647b2b617678" +dependencies = [ + "once_cell", +] + +[[package]] +name = "try-lock" +version = "0.2.5" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "e421abadd41a4225275504ea4d6566923418b7f05506fbc9c0fe86ba7396114b" + +[[package]] +name = "typenum" +version = "1.18.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "1dccffe3ce07af9386bfd29e80c0ab1a8205a2fc34e4bcd40364df902cfa8f3f" + +[[package]] +name = "unicode-ident" +version = "1.0.18" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "5a5f39404a5da50712a4c1eecf25e90dd62b613502b7e925fd4e4d19b5c96512" + +[[package]] +name = "unicode-width" +version = "0.1.14" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "7dd6e30e90baa6f72411720665d41d89b9a3d039dc45b8faea1ddd07f617f6af" + +[[package]] +name = "untrusted" +version = "0.9.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "8ecb6da28b8a351d773b68d5825ac39017e680750f980f3a1a85cd8dd28a47c1" + +[[package]] +name = "url" +version = "2.5.4" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "32f8b686cadd1473f4bd0117a5d28d36b1ade384ea9b5069a1c40aefed7fda60" +dependencies = [ + "form_urlencoded", + "idna", + "percent-encoding", +] + +[[package]] +name = "utf8_iter" +version = "1.0.4" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "b6c140620e7ffbb22c2dee59cafe6084a59b5ffc27a8859a5f0d494b5d52b6be" + +[[package]] +name = "vec_map" +version = "0.8.2" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "f1bddf1187be692e79c5ffeab891132dfb0f236ed36a43c7ed39f1165ee20191" + +[[package]] +name = "version_check" +version = "0.9.5" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "0b928f33d975fc6ad9f86c8f283853ad26bdd5b10b7f1542aa2fa15e2289105a" + +[[package]] +name = "walkdir" +version = "2.5.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "29790946404f91d9c5d06f9874efddea1dc06c5efe94541a7d6863108e3a5e4b" +dependencies = [ + "same-file", + "winapi-util", +] + +[[package]] +name = "want" +version = "0.3.1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "bfa7760aed19e106de2c7c0b581b509f2f25d3dacaf737cb82ac61bc6d760b0e" +dependencies = [ + "try-lock", +] + +[[package]] +name = "wasi" +version = "0.11.1+wasi-snapshot-preview1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "ccf3ec651a847eb01de73ccad15eb7d99f80485de043efb2f370cd654f4ea44b" + +[[package]] +name = "wasi" +version = "0.14.2+wasi-0.2.4" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "9683f9a5a998d873c0d21fcbe3c083009670149a8fab228644b8bd36b2c48cb3" +dependencies = [ + "wit-bindgen-rt", +] + +[[package]] +name = "wasm-bindgen" +version = "0.2.100" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "1edc8929d7499fc4e8f0be2262a241556cfc54a0bea223790e71446f2aab1ef5" +dependencies = [ + "cfg-if", + "once_cell", + "rustversion", + "wasm-bindgen-macro", +] + +[[package]] +name = "wasm-bindgen-backend" +version = "0.2.100" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "2f0a0651a5c2bc21487bde11ee802ccaf4c51935d0d3d42a6101f98161700bc6" +dependencies = [ + "bumpalo", + "log", + "proc-macro2", + "quote", + "syn", + "wasm-bindgen-shared", +] + +[[package]] +name = "wasm-bindgen-futures" +version = "0.4.50" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "555d470ec0bc3bb57890405e5d4322cc9ea83cebb085523ced7be4144dac1e61" +dependencies = [ + "cfg-if", + "js-sys", + "once_cell", + "wasm-bindgen", + "web-sys", +] + +[[package]] +name = "wasm-bindgen-macro" +version = "0.2.100" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "7fe63fc6d09ed3792bd0897b314f53de8e16568c2b3f7982f468c0bf9bd0b407" +dependencies = [ + "quote", + "wasm-bindgen-macro-support", +] + +[[package]] +name = "wasm-bindgen-macro-support" +version = "0.2.100" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "8ae87ea40c9f689fc23f209965b6fb8a99ad69aeeb0231408be24920604395de" +dependencies = [ + "proc-macro2", + "quote", + "syn", + "wasm-bindgen-backend", + "wasm-bindgen-shared", +] + +[[package]] +name = "wasm-bindgen-shared" +version = "0.2.100" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "1a05d73b933a847d6cccdda8f838a22ff101ad9bf93e33684f39c1f5f0eece3d" +dependencies = [ + "unicode-ident", +] + +[[package]] +name = "web-sys" +version = "0.3.77" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "33b6dd2ef9186f1f2072e409e99cd22a975331a6b3591b12c764e0e55c60d5d2" +dependencies = [ + "js-sys", + "wasm-bindgen", +] + +[[package]] +name = "webpki-root-certs" +version = "0.26.11" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "75c7f0ef91146ebfb530314f5f1d24528d7f0767efbfd31dce919275413e393e" +dependencies = [ + "webpki-root-certs 1.0.2", +] + +[[package]] +name = "webpki-root-certs" +version = "1.0.2" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "4e4ffd8df1c57e87c325000a3d6ef93db75279dc3a231125aac571650f22b12a" +dependencies = [ + "rustls-pki-types", +] + +[[package]] +name = "widestring" +version = "1.2.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "dd7cf3379ca1aac9eea11fba24fd7e315d621f8dfe35c8d7d2be8b793726e07d" + +[[package]] +name = "winapi" +version = "0.3.9" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "5c839a674fcd7a98952e593242ea400abe93992746761e38641405d28b00f419" +dependencies = [ + "winapi-i686-pc-windows-gnu", + "winapi-x86_64-pc-windows-gnu", +] + +[[package]] +name = "winapi-i686-pc-windows-gnu" +version = "0.4.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "ac3b87c63620426dd9b991e5ce0329eff545bccbbb34f3be09ff6fb6ab51b7b6" + +[[package]] +name = "winapi-util" +version = "0.1.9" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "cf221c93e13a30d793f7645a0e7762c55d169dbb0a49671918a2319d289b10bb" +dependencies = [ + "windows-sys 0.59.0", +] + +[[package]] +name = "winapi-x86_64-pc-windows-gnu" +version = "0.4.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "712e227841d057c1ee1cd2fb22fa7e5a5461ae8e48fa2ca79ec42cfc1931183f" + +[[package]] +name = "windows-sys" +version = "0.45.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "75283be5efb2831d37ea142365f009c02ec203cd29a3ebecbc093d52315b66d0" +dependencies = [ + "windows-targets 0.42.2", +] + +[[package]] +name = "windows-sys" +version = "0.48.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "677d2418bec65e3338edb076e806bc1ec15693c5d0104683f2efe857f61056a9" +dependencies = [ + "windows-targets 0.48.5", +] + +[[package]] +name = "windows-sys" +version = "0.52.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "282be5f36a8ce781fad8c8ae18fa3f9beff57ec1b52cb3de0789201425d9a33d" +dependencies = [ + "windows-targets 0.52.6", +] + +[[package]] +name = "windows-sys" +version = "0.59.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "1e38bc4d79ed67fd075bcc251a1c39b32a1776bbe92e5bef1f0bf1f8c531853b" +dependencies = [ + "windows-targets 0.52.6", +] + +[[package]] +name = "windows-sys" +version = "0.60.2" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "f2f500e4d28234f72040990ec9d39e3a6b950f9f22d3dba18416c35882612bcb" +dependencies = [ + "windows-targets 0.53.2", +] + +[[package]] +name = "windows-targets" +version = "0.42.2" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "8e5180c00cd44c9b1c88adb3693291f1cd93605ded80c250a75d472756b4d071" +dependencies = [ + "windows_aarch64_gnullvm 0.42.2", + "windows_aarch64_msvc 0.42.2", + "windows_i686_gnu 0.42.2", + "windows_i686_msvc 0.42.2", + "windows_x86_64_gnu 0.42.2", + "windows_x86_64_gnullvm 0.42.2", + "windows_x86_64_msvc 0.42.2", +] + +[[package]] +name = "windows-targets" +version = "0.48.5" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "9a2fa6e2155d7247be68c096456083145c183cbbbc2764150dda45a87197940c" +dependencies = [ + "windows_aarch64_gnullvm 0.48.5", + "windows_aarch64_msvc 0.48.5", + "windows_i686_gnu 0.48.5", + "windows_i686_msvc 0.48.5", + "windows_x86_64_gnu 0.48.5", + "windows_x86_64_gnullvm 0.48.5", + "windows_x86_64_msvc 0.48.5", +] + +[[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.2" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "c66f69fcc9ce11da9966ddb31a40968cad001c5bedeb5c2b82ede4253ab48aef" +dependencies = [ + "windows_aarch64_gnullvm 0.53.0", + "windows_aarch64_msvc 0.53.0", + "windows_i686_gnu 0.53.0", + "windows_i686_gnullvm 0.53.0", + "windows_i686_msvc 0.53.0", + "windows_x86_64_gnu 0.53.0", + "windows_x86_64_gnullvm 0.53.0", + "windows_x86_64_msvc 0.53.0", +] + +[[package]] +name = "windows_aarch64_gnullvm" +version = "0.42.2" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "597a5118570b68bc08d8d59125332c54f1ba9d9adeedeef5b99b02ba2b0698f8" + +[[package]] +name = "windows_aarch64_gnullvm" +version = "0.48.5" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "2b38e32f0abccf9987a4e3079dfb67dcd799fb61361e53e2882c3cbaf0d905d8" + +[[package]] +name = "windows_aarch64_gnullvm" +version = "0.52.6" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "32a4622180e7a0ec044bb555404c800bc9fd9ec262ec147edd5989ccd0c02cd3" + +[[package]] +name = "windows_aarch64_gnullvm" +version = "0.53.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "86b8d5f90ddd19cb4a147a5fa63ca848db3df085e25fee3cc10b39b6eebae764" + +[[package]] +name = "windows_aarch64_msvc" +version = "0.42.2" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "e08e8864a60f06ef0d0ff4ba04124db8b0fb3be5776a5cd47641e942e58c4d43" + +[[package]] +name = "windows_aarch64_msvc" +version = "0.48.5" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "dc35310971f3b2dbbf3f0690a219f40e2d9afcf64f9ab7cc1be722937c26b4bc" + +[[package]] +name = "windows_aarch64_msvc" +version = "0.52.6" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "09ec2a7bb152e2252b53fa7803150007879548bc709c039df7627cabbd05d469" + +[[package]] +name = "windows_aarch64_msvc" +version = "0.53.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "c7651a1f62a11b8cbd5e0d42526e55f2c99886c77e007179efff86c2b137e66c" + +[[package]] +name = "windows_i686_gnu" +version = "0.42.2" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "c61d927d8da41da96a81f029489353e68739737d3beca43145c8afec9a31a84f" + +[[package]] +name = "windows_i686_gnu" +version = "0.48.5" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "a75915e7def60c94dcef72200b9a8e58e5091744960da64ec734a6c6e9b3743e" + +[[package]] +name = "windows_i686_gnu" +version = "0.52.6" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "8e9b5ad5ab802e97eb8e295ac6720e509ee4c243f69d781394014ebfe8bbfa0b" + +[[package]] +name = "windows_i686_gnu" +version = "0.53.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "c1dc67659d35f387f5f6c479dc4e28f1d4bb90ddd1a5d3da2e5d97b42d6272c3" + +[[package]] +name = "windows_i686_gnullvm" +version = "0.52.6" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "0eee52d38c090b3caa76c563b86c3a4bd71ef1a819287c19d586d7334ae8ed66" + +[[package]] +name = "windows_i686_gnullvm" +version = "0.53.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "9ce6ccbdedbf6d6354471319e781c0dfef054c81fbc7cf83f338a4296c0cae11" + +[[package]] +name = "windows_i686_msvc" +version = "0.42.2" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "44d840b6ec649f480a41c8d80f9c65108b92d89345dd94027bfe06ac444d1060" + +[[package]] +name = "windows_i686_msvc" +version = "0.48.5" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "8f55c233f70c4b27f66c523580f78f1004e8b5a8b659e05a4eb49d4166cca406" + +[[package]] +name = "windows_i686_msvc" +version = "0.52.6" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "240948bc05c5e7c6dabba28bf89d89ffce3e303022809e73deaefe4f6ec56c66" + +[[package]] +name = "windows_i686_msvc" +version = "0.53.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "581fee95406bb13382d2f65cd4a908ca7b1e4c2f1917f143ba16efe98a589b5d" + +[[package]] +name = "windows_x86_64_gnu" +version = "0.42.2" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "8de912b8b8feb55c064867cf047dda097f92d51efad5b491dfb98f6bbb70cb36" + +[[package]] +name = "windows_x86_64_gnu" +version = "0.48.5" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "53d40abd2583d23e4718fddf1ebec84dbff8381c07cae67ff7768bbf19c6718e" + +[[package]] +name = "windows_x86_64_gnu" +version = "0.52.6" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "147a5c80aabfbf0c7d901cb5895d1de30ef2907eb21fbbab29ca94c5b08b1a78" + +[[package]] +name = "windows_x86_64_gnu" +version = "0.53.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "2e55b5ac9ea33f2fc1716d1742db15574fd6fc8dadc51caab1c16a3d3b4190ba" + +[[package]] +name = "windows_x86_64_gnullvm" +version = "0.42.2" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "26d41b46a36d453748aedef1486d5c7a85db22e56aff34643984ea85514e94a3" + +[[package]] +name = "windows_x86_64_gnullvm" +version = "0.48.5" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "0b7b52767868a23d5bab768e390dc5f5c55825b6d30b86c844ff2dc7414044cc" + +[[package]] +name = "windows_x86_64_gnullvm" +version = "0.52.6" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "24d5b23dc417412679681396f2b49f3de8c1473deb516bd34410872eff51ed0d" + +[[package]] +name = "windows_x86_64_gnullvm" +version = "0.53.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "0a6e035dd0599267ce1ee132e51c27dd29437f63325753051e71dd9e42406c57" + +[[package]] +name = "windows_x86_64_msvc" +version = "0.42.2" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "9aec5da331524158c6d1a4ac0ab1541149c0b9505fde06423b02f5ef0106b9f0" + +[[package]] +name = "windows_x86_64_msvc" +version = "0.48.5" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "ed94fce61571a4006852b7389a063ab983c02eb1bb37b47f8272ce92d06d9538" + +[[package]] +name = "windows_x86_64_msvc" +version = "0.52.6" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "589f6da84c646204747d1270a2a5661ea66ed1cced2631d546fdfb155959f9ec" + +[[package]] +name = "windows_x86_64_msvc" +version = "0.53.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "271414315aff87387382ec3d271b52d7ae78726f5d44ac98b4f4030c91880486" + +[[package]] +name = "winnow" +version = "0.7.12" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "f3edebf492c8125044983378ecb5766203ad3b4c2f7a922bd7dd207f6d443e95" +dependencies = [ + "memchr", +] + +[[package]] +name = "wit-bindgen-rt" +version = "0.39.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "6f42320e61fe2cfd34354ecb597f86f413484a798ba44a8ca1165c58d42da6c1" +dependencies = [ + "bitflags 2.9.1", +] + +[[package]] +name = "writeable" +version = "0.6.1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "ea2f10b9bb0928dfb1b42b65e1f9e36f7f54dbdf08457afefb38afcdec4fa2bb" + +[[package]] +name = "yaml-rust" +version = "0.4.5" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "56c1936c4cc7a1c9ab21a1ebb602eb942ba868cbd44a99cb7cdc5892335e1c85" +dependencies = [ + "linked-hash-map", +] + +[[package]] +name = "yoke" +version = "0.8.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "5f41bb01b8226ef4bfd589436a297c53d118f65921786300e427be8d487695cc" +dependencies = [ + "serde", + "stable_deref_trait", + "yoke-derive", + "zerofrom", +] + +[[package]] +name = "yoke-derive" +version = "0.8.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "38da3c9736e16c5d3c8c597a9aaa5d1fa565d0532ae05e27c24aa62fb32c0ab6" +dependencies = [ + "proc-macro2", + "quote", + "syn", + "synstructure", +] + +[[package]] +name = "zerocopy" +version = "0.8.26" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "1039dd0d3c310cf05de012d8a39ff557cb0d23087fd44cad61df08fc31907a2f" +dependencies = [ + "zerocopy-derive", +] + +[[package]] +name = "zerocopy-derive" +version = "0.8.26" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "9ecf5b4cc5364572d7f4c329661bcc82724222973f2cab6f050a4e5c22f75181" +dependencies = [ + "proc-macro2", + "quote", + "syn", +] + +[[package]] +name = "zerofrom" +version = "0.1.6" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "50cc42e0333e05660c3587f3bf9d0478688e15d870fab3346451ce7f8c9fbea5" +dependencies = [ + "zerofrom-derive", +] + +[[package]] +name = "zerofrom-derive" +version = "0.1.6" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "d71e5d6e06ab090c67b5e44993ec16b72dcbaabc526db883a360057678b48502" +dependencies = [ + "proc-macro2", + "quote", + "syn", + "synstructure", +] + +[[package]] +name = "zeroize" +version = "1.8.1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "ced3678a2879b30306d323f4542626697a464a97c0a07c9aebf7ebca65cd4dde" + +[[package]] +name = "zerotrie" +version = "0.2.2" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "36f0bbd478583f79edad978b407914f61b2972f5af6fa089686016be8f9af595" +dependencies = [ + "displaydoc", + "yoke", + "zerofrom", +] + +[[package]] +name = "zerovec" +version = "0.11.2" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "4a05eb080e015ba39cc9e23bbe5e7fb04d5fb040350f99f34e338d5fdd294428" +dependencies = [ + "yoke", + "zerofrom", + "zerovec-derive", +] + +[[package]] +name = "zerovec-derive" +version = "0.11.1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "5b96237efa0c878c64bd89c436f661be4e46b2f3eff1ebb976f7ef2321d2f58f" +dependencies = [ + "proc-macro2", + "quote", + "syn", +] + +[[package]] +name = "zinit" +version = "0.2.0" +dependencies = [ + "anyhow", + "async-trait", + "bytes", + "clap", + "command-group", + "dirs", + "fern", + "git-version", + "hyper", + "jsonrpsee", + "log", + "memchr", + "nix", + "reth-ipc", + "serde", + "serde_json", + "serde_yaml", + "shlex", + "sysinfo", + "tempfile", + "thiserror 1.0.69", + "tokio", + "tokio-stream", + "tower", + "tower-http", +] + +[[package]] +name = "zinit-client" +version = "0.1.0" +dependencies = [ + "anyhow", + "async-trait", + "jsonrpsee", + "log", + "reth-ipc", + "serde", + "serde_json", + "thiserror 1.0.69", + "tokio", +] diff --git a/Cargo.toml b/Cargo.toml new file mode 100644 index 0000000..2901ca7 --- /dev/null +++ b/Cargo.toml @@ -0,0 +1,50 @@ +[workspace] +members = [ + ".", + "zinit-client" +] + +[package] +name = "zinit" +version = "0.2.0" +edition = "2018" + +# See more keys and their definitions at https://doc.rust-lang.org/cargo/reference/manifest.html + +[dependencies] +anyhow = "1.0" +tokio = { version = "1.44.1", features = ["full"] } +tokio-stream = { version = "0.1.17", features = ["sync"] } +shlex ="1.1" +nix = "0.22.1" +serde = { version = "1.0", features = ["derive"] } +serde_yaml = "0.8" +serde_json = "1.0" +fern = "0.6" +log = "0.4" +thiserror = "1.0" +clap = "2.33" +git-version = "0.3.5" +command-group = "1.0.8" +dirs = "5.0" +hyper = "1.6" +# axum = { version = "0.7.4", features = ["http1"] } +bytes = "1.0" +jsonrpsee = { version = "0.25.1", features = ["server", "client", "macros"] } +memchr = "2.5.0" +async-trait = "0.1.88" +reth-ipc = { git = "https://github.com/paradigmxyz/reth", package = "reth-ipc" } +tower-http = { version = "0.5", features = ["cors"] } +tower = "0.5.2" +sysinfo = "0.29.10" + +[dev-dependencies] +tokio = { version = "1.14.0", features = ["full", "test-util"] } +tempfile = "3.3.0" +[lib] +name = "zinit" +path = "src/lib.rs" + +[[bin]] +name = "zinit" +path = "src/main.rs" diff --git a/LICENSE b/LICENSE new file mode 100644 index 0000000..88b9a96 --- /dev/null +++ b/LICENSE @@ -0,0 +1,201 @@ + Apache License + Version 2.0, January 2004 + http://www.apache.org/licenses/ + + TERMS AND CONDITIONS FOR USE, REPRODUCTION, AND DISTRIBUTION + + 1. Definitions. + + "License" shall mean the terms and conditions for use, reproduction, + and distribution as defined by Sections 1 through 9 of this document. + + "Licensor" shall mean the copyright owner or entity authorized by + the copyright owner that is granting the License. + + "Legal Entity" shall mean the union of the acting entity and all + other entities that control, are controlled by, or are under common + control with that entity. For the purposes of this definition, + "control" means (i) the power, direct or indirect, to cause the + direction or management of such entity, whether by contract or + otherwise, or (ii) ownership of fifty percent (50%) or more of the + outstanding shares, or (iii) beneficial ownership of such entity. + + "You" (or "Your") shall mean an individual or Legal Entity + exercising permissions granted by this License. + + "Source" form shall mean the preferred form for making modifications, + including but not limited to software source code, documentation + source, and configuration files. + + "Object" form shall mean any form resulting from mechanical + transformation or translation of a Source form, including but + not limited to compiled object code, generated documentation, + and conversions to other media types. + + "Work" shall mean the work of authorship, whether in Source or + Object form, made available under the License, as indicated by a + copyright notice that is included in or attached to the work + (an example is provided in the Appendix below). + + "Derivative Works" shall mean any work, whether in Source or Object + form, that is based on (or derived from) the Work and for which the + editorial revisions, annotations, elaborations, or other modifications + represent, as a whole, an original work of authorship. For the purposes + of this License, Derivative Works shall not include works that remain + separable from, or merely link (or bind by name) to the interfaces of, + the Work and Derivative Works thereof. + + "Contribution" shall mean any work of authorship, including + the original version of the Work and any modifications or additions + to that Work or Derivative Works thereof, that is intentionally + submitted to Licensor for inclusion in the Work by the copyright owner + or by an individual or Legal Entity authorized to submit on behalf of + the copyright owner. For the purposes of this definition, "submitted" + means any form of electronic, verbal, or written communication sent + to the Licensor or its representatives, including but not limited to + communication on electronic mailing lists, source code control systems, + and issue tracking systems that are managed by, or on behalf of, the + Licensor for the purpose of discussing and improving the Work, but + excluding communication that is conspicuously marked or otherwise + designated in writing by the copyright owner as "Not a Contribution." + + "Contributor" shall mean Licensor and any individual or Legal Entity + on behalf of whom a Contribution has been received by Licensor and + subsequently incorporated within the Work. + + 2. Grant of Copyright License. Subject to the terms and conditions of + this License, each Contributor hereby grants to You a perpetual, + worldwide, non-exclusive, no-charge, royalty-free, irrevocable + copyright license to reproduce, prepare Derivative Works of, + publicly display, publicly perform, sublicense, and distribute the + Work and such Derivative Works in Source or Object form. + + 3. Grant of Patent License. Subject to the terms and conditions of + this License, each Contributor hereby grants to You a perpetual, + worldwide, non-exclusive, no-charge, royalty-free, irrevocable + (except as stated in this section) patent license to make, have made, + use, offer to sell, sell, import, and otherwise transfer the Work, + where such license applies only to those patent claims licensable + by such Contributor that are necessarily infringed by their + Contribution(s) alone or by combination of their Contribution(s) + with the Work to which such Contribution(s) was submitted. If You + institute patent litigation against any entity (including a + cross-claim or counterclaim in a lawsuit) alleging that the Work + or a Contribution incorporated within the Work constitutes direct + or contributory patent infringement, then any patent licenses + granted to You under this License for that Work shall terminate + as of the date such litigation is filed. + + 4. Redistribution. You may reproduce and distribute copies of the + Work or Derivative Works thereof in any medium, with or without + modifications, and in Source or Object form, provided that You + meet the following conditions: + + (a) You must give any other recipients of the Work or + Derivative Works a copy of this License; and + + (b) You must cause any modified files to carry prominent notices + stating that You changed the files; and + + (c) You must retain, in the Source form of any Derivative Works + that You distribute, all copyright, patent, trademark, and + attribution notices from the Source form of the Work, + excluding those notices that do not pertain to any part of + the Derivative Works; and + + (d) If the Work includes a "NOTICE" text file as part of its + distribution, then any Derivative Works that You distribute must + include a readable copy of the attribution notices contained + within such NOTICE file, excluding those notices that do not + pertain to any part of the Derivative Works, in at least one + of the following places: within a NOTICE text file distributed + as part of the Derivative Works; within the Source form or + documentation, if provided along with the Derivative Works; or, + within a display generated by the Derivative Works, if and + wherever such third-party notices normally appear. The contents + of the NOTICE file are for informational purposes only and + do not modify the License. You may add Your own attribution + notices within Derivative Works that You distribute, alongside + or as an addendum to the NOTICE text from the Work, provided + that such additional attribution notices cannot be construed + as modifying the License. + + You may add Your own copyright statement to Your modifications and + may provide additional or different license terms and conditions + for use, reproduction, or distribution of Your modifications, or + for any such Derivative Works as a whole, provided Your use, + reproduction, and distribution of the Work otherwise complies with + the conditions stated in this License. + + 5. Submission of Contributions. Unless You explicitly state otherwise, + any Contribution intentionally submitted for inclusion in the Work + by You to the Licensor shall be under the terms and conditions of + this License, without any additional terms or conditions. + Notwithstanding the above, nothing herein shall supersede or modify + the terms of any separate license agreement you may have executed + with Licensor regarding such Contributions. + + 6. Trademarks. This License does not grant permission to use the trade + names, trademarks, service marks, or product names of the Licensor, + except as required for reasonable and customary use in describing the + origin of the Work and reproducing the content of the NOTICE file. + + 7. Disclaimer of Warranty. Unless required by applicable law or + agreed to in writing, Licensor provides the Work (and each + Contributor provides its Contributions) on an "AS IS" BASIS, + WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or + implied, including, without limitation, any warranties or conditions + of TITLE, NON-INFRINGEMENT, MERCHANTABILITY, or FITNESS FOR A + PARTICULAR PURPOSE. You are solely responsible for determining the + appropriateness of using or redistributing the Work and assume any + risks associated with Your exercise of permissions under this License. + + 8. Limitation of Liability. In no event and under no legal theory, + whether in tort (including negligence), contract, or otherwise, + unless required by applicable law (such as deliberate and grossly + negligent acts) or agreed to in writing, shall any Contributor be + liable to You for damages, including any direct, indirect, special, + incidental, or consequential damages of any character arising as a + result of this License or out of the use or inability to use the + Work (including but not limited to damages for loss of goodwill, + work stoppage, computer failure or malfunction, or any and all + other commercial damages or losses), even if such Contributor + has been advised of the possibility of such damages. + + 9. Accepting Warranty or Additional Liability. While redistributing + the Work or Derivative Works thereof, You may choose to offer, + and charge a fee for, acceptance of support, warranty, indemnity, + or other liability obligations and/or rights consistent with this + License. However, in accepting such obligations, You may act only + on Your own behalf and on Your sole responsibility, not on behalf + of any other Contributor, and only if You agree to indemnify, + defend, and hold each Contributor harmless for any liability + incurred by, or claims asserted against, such Contributor by reason + of your accepting any such warranty or additional liability. + + END OF TERMS AND CONDITIONS + + APPENDIX: How to apply the Apache License to your work. + + To apply the Apache License to your work, attach the following + boilerplate notice, with the fields enclosed by brackets "[]" + replaced with your own identifying information. (Don't include + the brackets!) The text should be enclosed in the appropriate + comment syntax for the file format. We also recommend that a + file or class name and description of purpose be included on the + same "printed page" as the copyright notice for easier + identification within third-party archives. + + Copyright TF TECH NV (Belgium) + + Licensed under the Apache License, Version 2.0 (the "License"); + you may not use this file except in compliance with the License. + You may obtain a copy of the License at + + http://www.apache.org/licenses/LICENSE-2.0 + + Unless required by applicable law or agreed to in writing, software + distributed under the License is distributed on an "AS IS" BASIS, + WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + See the License for the specific language governing permissions and + limitations under the License. diff --git a/Makefile b/Makefile new file mode 100644 index 0000000..1698d4b --- /dev/null +++ b/Makefile @@ -0,0 +1,30 @@ +default: release + +docker: release + docker build -f docker/Dockerfile -t zinit-ubuntu:18.04 target/x86_64-unknown-linux-musl/release + +prepare: + rustup target add x86_64-unknown-linux-musl + +release: prepare + cargo build --release --target=x86_64-unknown-linux-musl + +release-aarch64-musl: prepare-aarch64-musl + cargo build --release --target=aarch64-unknown-linux-musl + +prepare-aarch64-musl: + rustup target add aarch64-unknown-linux-musl + +# Build for macOS (both Intel and Apple Silicon) +release-macos: + cargo build --release + +# Install to ~/hero/bin (if it exists) +install-macos: release-macos + @if [ -d ~/hero/bin ]; then \ + cp target/release/zinit ~/hero/bin; \ + echo "Installed zinit to ~/hero/bin"; \ + else \ + echo "~/hero/bin directory not found. Please create it or specify a different installation path."; \ + exit 1; \ + fi diff --git a/README.md b/README.md new file mode 100644 index 0000000..472ca12 --- /dev/null +++ b/README.md @@ -0,0 +1,77 @@ +# Zinit [![Rust](https://github.com/threefoldtech/zinit/actions/workflows/rust.yml/badge.svg)](https://github.com/threefoldtech/zinit/actions/workflows/rust.yml) + +Zinit is a lightweight PID 1 replacement inspired by runit, written in Rust using Tokio for async I/O. It provides both a Unix socket interface and an HTTP API for interacting with the process manager. + +### Key Features + +- **Service Management**: Ensures configured services are up and running at all times +- **Dependency Handling**: Supports service dependencies for proper startup ordering +- **Simple Control Interface**: Provides an intuitive CLI to add, start, stop, and monitor services +- **Container Support**: Can run in container mode with appropriate signal handling +- **Configurable Logging**: Multiple logging options including ringbuffer and stdout + +## Installation + +```bash +curl https://raw.githubusercontent.com/threefoldtech/zinit/refs/heads/master/install.sh | bash + +#to install & run +curl https://raw.githubusercontent.com/threefoldtech/zinit/refs/heads/master/install_run.sh | bash +``` + +Click [here](docs/installation.md) for more information on how to install Zinit. + +## Usage + +### Process Manager (zinit) + +```bash +# Run zinit in init mode +zinit init --config /etc/zinit/ --socket /var/run/zinit.sock + +# List services +zinit list + +# Start a service +zinit start + +# Stop a service +zinit stop +``` + +```bash +# Start the HTTP proxy on the default port (8080) +zinit proxy +``` + +More information about all the available commands can be found [here](docs/cmd.md). + +### Service Configuration + +Zinit uses YAML files for service configuration. Here's a basic example: + +```yaml +# Service configuration (e.g., /etc/zinit/myservice.yaml) +exec: "/usr/bin/myservice --option value" # Command to run (required) +test: "/usr/bin/check-myservice" # Health check command (optional) +oneshot: false # Whether to restart on exit (default: false) +after: # Services that must be running first (optional) + - dependency1 + - dependency2 +``` + +For more information on how to configure service files, see the [service file reference](docs/services.md) documentation. + +### JSON-RPC API + +The HTTP proxy provides a JSON-RPC 2.0 API for interacting with Zinit. You can send JSON-RPC requests to the HTTP endpoint you provided to the proxy: + +```bash +curl -X POST -H "Content-Type: application/json" -d '{"jsonrpc":"2.0","id":1,"method":"service_list","params":{}}' http://localhost:8080/ +``` + +See the [OpenRPC specs](openrpc.json) for more information about available RPC calls to interact with Zinit. + +## License + +See [LICENSE](LICENSE) file for details. diff --git a/docker/Dockerfile b/docker/Dockerfile new file mode 100644 index 0000000..3406490 --- /dev/null +++ b/docker/Dockerfile @@ -0,0 +1,6 @@ +FROM ubuntu:18.04 + +RUN mkdir -p /etc/zinit +ADD zinit /sbin/zinit + +ENTRYPOINT ["/sbin/zinit", "init"] \ No newline at end of file diff --git a/docs/cmd.md b/docs/cmd.md new file mode 100644 index 0000000..b4e7381 --- /dev/null +++ b/docs/cmd.md @@ -0,0 +1,256 @@ +# Zinit Command Line Reference + +This document provides a comprehensive reference for all Zinit command line options and commands. + +## Command Structure + +Zinit uses a command-based CLI with the following general structure: + +```bash +zinit [FLAGS] [OPTIONS] [SUBCOMMAND] +``` + +## Global Flags and Options + +These flags and options apply to all Zinit commands: + +| Flag/Option | Description | +|-------------|-------------| +| `-d, --debug` | Run in debug mode with increased verbosity | +| `-h, --help` | Display help information | +| `-V, --version` | Display version information | +| `-s, --socket ` | Path to Unix socket (default: `/var/run/zinit.sock`) | + +## Subcommands + +### Main Mode + +#### `init` + +Run Zinit in init mode, starting and maintaining configured services. + +```bash +zinit init [FLAGS] [OPTIONS] +``` + +**Flags:** +- `--container`: Run in container mode, exiting on signal instead of rebooting + +**Options:** +- `-c, --config `: Service configurations directory (default: `/etc/zinit/`) +- `-b, --buffer `: Buffer size (in lines) to keep service logs (default: `2000`) + +**Example:** +```bash +# Run in init mode with custom config directory +zinit init -c /opt/services/ + +# Run in container mode +zinit init --container +``` + +### Service Management + +#### `list` + +Display a quick view of all currently known services and their status. + +```bash +zinit list +``` + +**Output:** +A JSON object with service names as keys and their status as values. + +**Example:** +```bash +# List all services +zinit list +``` + +#### `status` + +Show detailed status information for a specific service. + +```bash +zinit status +``` + +**Arguments:** +- ``: Name of the service to show status for + +**Example:** +```bash +# Check status of redis service +zinit status redis +``` + +#### `start` + +Start a service. Has no effect if the service is already running. + +```bash +zinit start +``` + +**Arguments:** +- ``: Name of the service to start + +**Example:** +```bash +# Start the nginx service +zinit start nginx +``` + +#### `stop` + +Stop a service. Sets the target state to "down" and sends the stop signal. + +```bash +zinit stop +``` + +**Arguments:** +- ``: Name of the service to stop + +**Example:** +```bash +# Stop the redis service +zinit stop redis +``` + +#### `restart` + +Restart a service. If it fails to stop, it will be killed and then started again. + +```bash +zinit restart +``` + +**Arguments:** +- ``: Name of the service to restart + +**Example:** +```bash +# Restart the web service +zinit restart web +``` + +#### `monitor` + +Start monitoring a service. The configuration is loaded from the server's config directory. + +```bash +zinit monitor +``` + +**Arguments:** +- ``: Name of the service to monitor + +**Example:** +```bash +# Monitor the database service +zinit monitor database +``` + +#### `forget` + +Remove a service from monitoring. You can only forget a stopped service. + +```bash +zinit forget +``` + +**Arguments:** +- ``: Name of the service to forget + +**Example:** +```bash +# Forget the backup service +zinit forget backup +``` + +#### `kill` + +Send a signal to a running service. + +```bash +zinit kill +``` + +**Arguments:** +- ``: Name of the service to send signal to +- ``: Signal name (e.g., SIGTERM, SIGKILL, SIGINT) + +**Example:** +```bash +# Send SIGTERM to the redis service +zinit kill redis SIGTERM + +# Send SIGKILL to force terminate a service +zinit kill stuck-service SIGKILL +``` + +### System Operations + +#### `shutdown` + +Stop all services in dependency order and power off the system. + +```bash +zinit shutdown +``` + +**Example:** +```bash +# Shutdown the system +zinit shutdown +``` + +#### `reboot` + +Stop all services in dependency order and reboot the system. + +```bash +zinit reboot +``` + +**Example:** +```bash +# Reboot the system +zinit reboot +``` + +### Logging + +#### `log` + +View service logs from the Zinit ring buffer. + +```bash +zinit log [FLAGS] [FILTER] +``` + +**Flags:** +- `-s, --snapshot`: If set, log prints current buffer without following + +**Arguments:** +- `[FILTER]`: Optional service name to filter logs for + +**Examples:** +```bash +# View logs for all services and follow new logs +zinit log + +# View current logs for the nginx service without following +zinit log -s nginx +``` + +## Exit Codes + +Zinit commands return the following exit codes: + +| Code | Description | +|------|-------------| +| 0 | Success | +| 1 | Error (with error message printed to stderr) | diff --git a/docs/installation.md b/docs/installation.md new file mode 100644 index 0000000..9092587 --- /dev/null +++ b/docs/installation.md @@ -0,0 +1,197 @@ +# Installing Zinit + +This guide provides detailed instructions for installing Zinit on various platforms. + +## System Requirements + +Zinit has minimal system requirements: + +- Linux-based operating system +- Root access (for running as init system) + +## Pre-built Binaries + +If pre-built binaries are available for your system, you can install them directly: + +```bash +# Download the binary (replace with actual URL) +wget https://github.com/threefoldtech/zinit/releases/download/vX.Y.Z/zinit-x86_64-unknown-linux-musl + +# Make it executable +chmod +x zinit-x86_64-unknown-linux-musl + +# Move to a location in your PATH +sudo mv zinit-x86_64-unknown-linux-musl /usr/local/bin/zinit +``` + +## Building from Source + +### Prerequisites + +To build Zinit from source, you'll need: + +- Rust toolchain (1.46.0 or later recommended) +- musl and musl-tools packages +- GNU Make + +#### Install Rust + +If you don't have Rust installed, use rustup: + +```bash +curl --proto '=https' --tlsv1.2 -sSf https://sh.rustup.rs | sh +source $HOME/.cargo/env +``` + +#### Install musl development tools + +On Debian/Ubuntu: + +```bash +sudo apt update +sudo apt install musl musl-tools +``` + +On Fedora: + +```bash +sudo dnf install musl musl-devel +``` + +On Alpine Linux (musl is already the default libc): + +```bash +apk add build-base +``` + +### Build Process + +1. Clone the repository: + +```bash +git clone https://github.com/threefoldtech/zinit.git +cd zinit +``` + +2. Build using make: + +```bash +make +``` + +This will create a statically linked binary at `target/x86_64-unknown-linux-musl/release/zinit`. + +3. Install the binary: + +```bash +sudo cp target/x86_64-unknown-linux-musl/release/zinit /usr/local/bin/ +``` + +### Development Build + +For development or debugging: + +```bash +make dev +``` + +## Docker Installation + +### Using the Provided Dockerfile + +Zinit includes a test Docker image: + +```bash +# Build the Docker image +make docker + +# Run the container +docker run -dt --device=/dev/kmsg:/dev/kmsg:rw zinit +``` +> Don't forget to port-forward a port to get access to the Zinit proxy using the `-p XXXX:YYYY` flag when running the container. + +### Custom Docker Setup + +To create your own Dockerfile with Zinit: + +```dockerfile +FROM alpine:latest + +# Install dependencies if needed +RUN apk add --no-cache bash curl + +# Copy the zinit binary +COPY zinit /usr/local/bin/zinit +RUN chmod +x /usr/local/bin/zinit + +# Create configuration directory +RUN mkdir -p /etc/zinit + +# Add your service configurations +COPY services/*.yaml /etc/zinit/ + +# Set zinit as the entrypoint +ENTRYPOINT ["/usr/local/bin/zinit", "init", "--container"] +``` + +## Using Zinit as the Init System + +To use Zinit as the init system (PID 1) on a Linux system: + +### On a Standard Linux System + +1. Install Zinit as described above +2. Create your service configurations in `/etc/zinit/` +3. Configure your bootloader to use zinit as init + +For GRUB, add `init=/usr/local/bin/zinit` to the kernel command line: + +```bash +# Edit GRUB configuration +sudo nano /etc/default/grub + +# Add init parameter to GRUB_CMDLINE_LINUX +# Example: +# GRUB_CMDLINE_LINUX="init=/usr/local/bin/zinit" + +# Update GRUB +sudo update-grub +``` + +### In a Container Environment + +For containers, simply set Zinit as the entrypoint: + +```bash +docker run -dt --device=/dev/kmsg:/dev/kmsg:rw \ + --entrypoint /usr/local/bin/zinit \ + your-image init --container +``` + +## First-time Setup + +After installation, you'll need to create a basic configuration: + +1. Create the configuration directory: + +```bash +sudo mkdir -p /etc/zinit +``` + +2. Create a simple service configuration: + +```bash +cat << EOF | sudo tee /etc/zinit/hello.yaml +exec: "echo 'Hello from Zinit!'" +oneshot: true +EOF +``` + +3. Test Zinit without running as init: + +```bash +# For testing only - doesn't replace system init +sudo zinit init +``` + +If all is working correctly, you should see Zinit start and run your service. \ No newline at end of file diff --git a/docs/osx_cross_compile.md b/docs/osx_cross_compile.md new file mode 100644 index 0000000..0132566 --- /dev/null +++ b/docs/osx_cross_compile.md @@ -0,0 +1,78 @@ +# macOS Guide for Zinit + +This guide covers both building Zinit natively on macOS and cross-compiling from macOS to Linux targets. + +## Building Zinit Natively on macOS + +Zinit can now be built and run directly on macOS. The code has been updated to handle platform-specific differences between Linux and macOS. + +### Building for macOS + +```bash +# Build a release version for macOS +make release-macos + +# Install to ~/hero/bin (if it exists) +make install-macos +``` + +The native macOS build provides most of Zinit's functionality, with the following limitations: +- System reboot and shutdown operations are not supported (they will exit the process instead) +- Some Linux-specific features are disabled + +## Cross-Compilation from macOS to Linux + +This section outlines the steps to set up your macOS environment for cross-compiling Rust projects to the `aarch64-unknown-linux-musl` target. This is particularly useful for building binaries that can run on ARM-based Linux systems (e.g., Raspberry Pi, AWS Graviton) using musl libc. + +## Prerequisites + +* Homebrew (https://brew.sh/) installed on your macOS system. +* Rust and Cargo installed (e.g., via `rustup`). + +## Step 1: Install the `aarch64-linux-musl-gcc` Toolchain + +The `aarch64-linux-musl-gcc` toolchain is required for linking when cross-compiling to `aarch64-unknown-linux-musl`. You can install it using Homebrew: + +```bash +brew install messense/macos-cross-toolchains/aarch64-linux-musl-cross +``` + +## Step 2: Link `musl-gcc` + +Some build scripts or tools might look for `musl-gcc`. To ensure compatibility, create a symbolic link: + +```bash +sudo ln -s /opt/homebrew/bin/aarch64-linux-musl-gcc /opt/homebrew/bin/musl-gcc +``` + +You might be prompted for your system password to complete this operation. + +## Step 3: Add the Rust Target + +Add the `aarch64-unknown-linux-musl` target to your Rust toolchain: + +```bash +rustup target add aarch64-unknown-linux-musl +``` + +## Step 4: Build Your Project + +Now you can build your Rust project for the `aarch64-unknown-linux-musl` target using Cargo: + +```bash +cargo build --release --target aarch64-unknown-linux-musl +``` + +Alternatively, if you are using the provided `Makefile`, you can use the new target: + +```bash +make release-aarch64-musl +``` + +This will produce a release binary located in `target/aarch64-unknown-linux-musl/release/`. + +## Step 5: copy to osx hero bin + +```bash +cp target/aarch64-unknown-linux-musl/release/zinit ~/hero/bin +``` \ No newline at end of file diff --git a/docs/services.md b/docs/services.md new file mode 100644 index 0000000..1e02c36 --- /dev/null +++ b/docs/services.md @@ -0,0 +1,217 @@ +# Service Configuration Format + +This document describes the structure and options for Zinit service configuration files. + +## File Format + +Zinit uses YAML files for service configuration. Each service has its own configuration file stored in the Zinit configuration directory (default: `/etc/zinit`). + +### File Naming and Location + +- **Location**: `/etc/zinit/` (default, can be changed with `-c` flag) + - on osx `~/hero/cfg/zinit` +- **Naming**: `.yaml` + +For example: +- `/etc/zinit/nginx.yaml` +- `/etc/zinit/redis.yaml` + +## Configuration Schema + +Service configuration files use the following schema: + +```yaml +# Command to run (required) +exec: "command line to start service" + +# Command to test if service is running (optional) +test: "command line to test service" + +# Whether the service should be restarted (optional, default: false) +oneshot: true|false + +# Maximum time to wait for service to stop during shutdown (optional, default: 10) +shutdown_timeout: 30 + +# Services that must be running before this one starts (optional) +after: + - service1_name + - service2_name + +# Signals configuration (optional) +signal: + stop: SIGKILL # signal sent on 'stop' action (default: SIGTERM) + +# Log handling configuration (optional, default: ring) +log: null|ring|stdout + +# Environment variables for the service (optional) +env: + KEY1: "VALUE1" + KEY2: "VALUE2" + +# Working directory for the service (optional) +dir: "/path/to/working/directory" +``` + +## Configuration Options + +### Required Fields + +| Field | Description | +|-------|-------------| +| `exec` | Command line to execute when starting the service | + +### Optional Fields + +| Field | Type | Default | Description | +|-------|------|---------|-------------| +| `test` | String | - | Command to determine if service is running | +| `oneshot` | Boolean | `false` | If true, service won't be restarted after exit | +| `shutdown_timeout` | Integer | 10 | Seconds to wait for service to stop during shutdown | +| `after` | String[] | `[]` | List of services that must be running first | +| `signal.stop` | String | `"sigterm"` | Signal to send when stopping the service | +| `log` | Enum | `ring` | How to handle service output (null, ring, stdout) | +| `env` | Object | `{}` | Environment variables to pass to the service | +| `dir` | String | `""` | Working directory for the service | + +## Field Details + +### exec + +The command to run when starting the service. This is the only required field in the configuration. + +```yaml +exec: "/usr/bin/redis-server --port 6379" +``` + +Shell-style commands are supported: + +```yaml +exec: "sh -c 'echo Starting service && /usr/local/bin/myservice'" +``` + +### test + +Command that tests whether the service is running properly. Zinit runs this command periodically until it succeeds (exit code 0), at which point the service is considered running. + +```yaml +test: "redis-cli -p 6379 PING" +``` + +If no test command is provided, the service is considered running as soon as it's started. + +### oneshot + +When set to `true`, the service will not be automatically restarted when it exits. This is useful for initialization tasks or commands that should run only once. + +```yaml +oneshot: true +``` + +Services that depend on a oneshot service will start only after the oneshot service has exited successfully. + +### shutdown_timeout + +How long (in seconds) to wait for the service to stop during system shutdown before giving up: + +```yaml +shutdown_timeout: 30 # Wait up to 30 seconds +``` + +### after + +List of service names that must be running (or completed successfully for oneshot services) before this service starts: + +```yaml +after: + - networking + - database +``` + +### signal + +Custom signals to use for operations. Currently, only the `stop` signal is configurable: + +```yaml +signal: + stop: SIGKILL # Use SIGKILL instead of default SIGTERM +``` + +Valid signal names follow the standard UNIX signal naming (SIGTERM, SIGKILL, SIGINT, etc). + +### log + +How to handle stdout/stderr output from the service: + +```yaml +log: stdout # Print output to zinit's stdout +``` + +Options: +- `null`: Ignore all service output (like redirecting to /dev/null) +- `ring`: Store logs in the kernel ring buffer with service name prefix (default) +- `stdout`: Send service output to zinit's stdout + +> **Note**: To use `ring` inside Docker, make sure to add the `kmsg` device: +> ``` +> docker run -dt --device=/dev/kmsg:/dev/kmsg:rw zinit +> ``` + +### env + +Additional environment variables for the service. These are added to the existing environment: + +```yaml +env: + PORT: "8080" + DEBUG: "true" + NODE_ENV: "production" +``` + +### dir + +Working directory for the service process: + +```yaml +dir: "/var/lib/myservice" +``` + +If not specified, the process inherits zinit's working directory. + +## Example Configurations + +### Web Server + +```yaml +exec: "/usr/bin/nginx -g 'daemon off;'" +test: "curl -s http://localhost > /dev/null" +after: + - networking +log: stdout +``` + +### Database Initialization + +```yaml +exec: "sh -c 'echo Creating database schema && /usr/bin/db-migrate'" +oneshot: true +dir: "/opt/myapp" +env: + DB_HOST: "localhost" + DB_USER: "admin" +``` + +### Application with Dependencies + +```yaml +exec: "/usr/bin/myapp --config /etc/myapp.conf" +test: "curl -s http://localhost:8080/health > /dev/null" +after: + - database + - cache +signal: + stop: SIGINT # Use SIGINT for graceful shutdown +env: + PORT: "8080" +shutdown_timeout: 20 \ No newline at end of file diff --git a/docs/shutdown_improvement_plan.md b/docs/shutdown_improvement_plan.md new file mode 100644 index 0000000..a32fa06 --- /dev/null +++ b/docs/shutdown_improvement_plan.md @@ -0,0 +1,366 @@ +# Zinit Shutdown Functionality Improvement Plan + +## Current Issues + +1. **Incomplete Child Process Termination**: When services are stopped, child processes may remain running. +2. **Lack of Verification**: There's no verification that all processes are actually terminated. +3. **Improper Graceful Shutdown**: Zinit doesn't wait for all processes to terminate before exiting. + +## Solution Overview + +We'll implement a robust shutdown mechanism that: +1. Uses our stats functionality to detect all child processes +2. Properly manages process groups +3. Verifies all processes are terminated before Zinit exits + +## Implementation Plan + +```mermaid +flowchart TD + A[Enhance stop method] --> B[Improve kill_process_tree] + B --> C[Add process verification] + C --> D[Implement graceful shutdown] + + A1[Use stats to detect child processes] --> A + A2[Send signals to all processes] --> A + A3[Implement cascading termination] --> A + + B1[Ensure proper process group handling] --> B + B2[Add timeout and escalation logic] --> B + + C1[Create verification mechanism] --> C + C2[Add polling for process existence] --> C + + D1[Wait for all processes to terminate] --> D + D2[Add cleanup of resources] --> D + D3[Implement clean exit] --> D +``` + +## Detailed Implementation Steps + +### 1. Enhance the `stop` Method in `LifecycleManager` + +```rust +pub async fn stop>(&self, name: S) -> Result<()> { + // Get service information + let table = self.services.read().await; + let service = table.get(name.as_ref()) + .ok_or_else(|| ZInitError::unknown_service(name.as_ref()))?; + + let mut service = service.write().await; + service.set_target(Target::Down); + + // Get the main process PID + let pid = service.pid; + if pid.as_raw() == 0 { + return Ok(()); + } + + // Get the signal to use + let signal = signal::Signal::from_str(&service.service.signal.stop.to_uppercase()) + .map_err(|err| anyhow::anyhow!("unknown stop signal: {}", err))?; + + // Release the lock before potentially long-running operations + drop(service); + drop(table); + + // Get all child processes using our stats functionality + let children = self.get_child_process_stats(pid.as_raw()).await?; + + // First try to stop the process group + let _ = self.pm.signal(pid, signal); + + // Wait a short time for processes to terminate gracefully + sleep(std::time::Duration::from_millis(500)).await; + + // Check if processes are still running and use SIGKILL if needed + self.ensure_processes_terminated(pid.as_raw(), &children).await?; + + Ok(()) +} +``` + +### 2. Add a New `ensure_processes_terminated` Method + +```rust +async fn ensure_processes_terminated(&self, parent_pid: i32, children: &[ProcessStats]) -> Result<()> { + // Check if parent is still running + let parent_running = self.is_process_running(parent_pid).await?; + + // If parent is still running, send SIGKILL + if parent_running { + debug!("Process {} still running after SIGTERM, sending SIGKILL", parent_pid); + let _ = self.pm.signal(Pid::from_raw(parent_pid), signal::Signal::SIGKILL); + } + + // Check and kill any remaining child processes + for child in children { + if self.is_process_running(child.pid).await? { + debug!("Child process {} still running, sending SIGKILL", child.pid); + let _ = signal::kill(Pid::from_raw(child.pid), signal::Signal::SIGKILL); + } + } + + // Verify all processes are gone + let mut retries = 5; + while retries > 0 { + let mut all_terminated = true; + + // Check parent + if self.is_process_running(parent_pid).await? { + all_terminated = false; + } + + // Check children + for child in children { + if self.is_process_running(child.pid).await? { + all_terminated = false; + break; + } + } + + if all_terminated { + return Ok(()); + } + + // Wait before retrying + sleep(std::time::Duration::from_millis(100)).await; + retries -= 1; + } + + // If we get here, some processes might still be running + warn!("Some processes may still be running after shutdown attempts"); + Ok(()) +} +``` + +### 3. Add a Helper Method to Check if a Process is Running + +```rust +async fn is_process_running(&self, pid: i32) -> Result { + // Use sysinfo to check if process exists + let mut system = System::new(); + let sys_pid = sysinfo::Pid::from(pid as usize); + system.refresh_process(sys_pid); + + Ok(system.process(sys_pid).is_some()) +} +``` + +### 4. Improve the `kill_process_tree` Method + +```rust +#[cfg(target_os = "linux")] +async fn kill_process_tree( + &self, + mut dag: ProcessDAG, + mut state_channels: HashMap>, + mut shutdown_timeouts: HashMap, +) -> Result<()> { + let (tx, mut rx) = mpsc::unbounded_channel(); + tx.send(DUMMY_ROOT.into())?; + + let mut count = dag.count; + while let Some(name) = rx.recv().await { + debug!("{} has been killed (or was inactive) adding its children", name); + + for child in dag.adj.get(&name).unwrap_or(&Vec::new()) { + let child_indegree: &mut u32 = dag.indegree.entry(child.clone()).or_default(); + *child_indegree -= 1; + + debug!("decrementing child {} indegree to {}", child, child_indegree); + + if *child_indegree == 0 { + let watcher = state_channels.remove(child); + if watcher.is_none() { + // not an active service + tx.send(child.to_string())?; + continue; + } + + let shutdown_timeout = shutdown_timeouts.remove(child); + let lifecycle = self.clone_lifecycle(); + + // Spawn a task to kill the service and wait for it to terminate + let kill_task = tokio::spawn(Self::kill_wait_enhanced( + lifecycle, + child.to_string(), + tx.clone(), + watcher.unwrap(), + shutdown_timeout.unwrap_or(config::DEFAULT_SHUTDOWN_TIMEOUT), + )); + + // Add a timeout to ensure we don't wait forever + let _ = tokio::time::timeout( + std::time::Duration::from_secs(shutdown_timeout.unwrap_or(config::DEFAULT_SHUTDOWN_TIMEOUT) + 2), + kill_task + ).await; + } + } + + count -= 1; + if count == 0 { + break; + } + } + + // Final verification that all processes are gone + self.verify_all_processes_terminated().await?; + + Ok(()) +} +``` + +### 5. Add an Enhanced `kill_wait` Method + +```rust +#[cfg(target_os = "linux")] +async fn kill_wait_enhanced( + self, + name: String, + ch: mpsc::UnboundedSender, + mut rx: Watcher, + shutdown_timeout: u64, +) { + debug!("kill_wait {}", name); + + // Try to stop the service gracefully + let stop_result = self.stop(name.clone()).await; + + // Wait for the service to become inactive or timeout + let fut = timeout( + std::time::Duration::from_secs(shutdown_timeout), + async move { + while let Some(state) = rx.next().await { + if !state.is_active() { + return; + } + } + }, + ); + + match stop_result { + Ok(_) => { + let _ = fut.await; + } + Err(e) => error!("couldn't stop service {}: {}", name.clone(), e), + } + + // Verify the service is actually stopped + if let Ok(status) = self.status(&name).await { + if status.pid != 0 { + // Service is still running, try to kill it + let _ = self.kill(&name, signal::Signal::SIGKILL).await; + } + } + + debug!("sending to the death channel {}", name.clone()); + if let Err(e) = ch.send(name.clone()) { + error!( + "error: couldn't send the service {} to the shutdown loop: {}", + name, e + ); + } +} +``` + +### 6. Add a Method to Verify All Processes are Terminated + +```rust +async fn verify_all_processes_terminated(&self) -> Result<()> { + // Get all services + let table = self.services.read().await; + + // Check each service + for (name, service) in table.iter() { + let service = service.read().await; + let pid = service.pid.as_raw(); + + // Skip services with no PID + if pid == 0 { + continue; + } + + // Check if the main process is still running + if self.is_process_running(pid).await? { + warn!("Service {} (PID {}) is still running after shutdown", name, pid); + + // Try to kill it with SIGKILL + let _ = signal::kill(Pid::from_raw(pid), signal::Signal::SIGKILL); + } + + // Check for child processes + if let Ok(children) = self.get_child_process_stats(pid).await { + for child in children { + if self.is_process_running(child.pid).await? { + warn!("Child process {} of service {} is still running after shutdown", + child.pid, name); + + // Try to kill it with SIGKILL + let _ = signal::kill(Pid::from_raw(child.pid), signal::Signal::SIGKILL); + } + } + } + } + + Ok(()) +} +``` + +### 7. Update the `shutdown` and `reboot` Methods + +```rust +pub async fn shutdown(&self) -> Result<()> { + info!("shutting down"); + + // Set the shutdown flag + *self.shutdown.write().await = true; + + #[cfg(target_os = "linux")] + { + // Power off using our enhanced method + let result = self.power(RebootMode::RB_POWER_OFF).await; + + // Final verification before exit + self.verify_all_processes_terminated().await?; + + return result; + } + + #[cfg(not(target_os = "linux"))] + { + // Stop all services + let services = self.list().await?; + for service in services { + let _ = self.stop(&service).await; + } + + // Verify all processes are terminated + self.verify_all_processes_terminated().await?; + + if self.container { + std::process::exit(0); + } else { + info!("System shutdown not supported on this platform"); + std::process::exit(0); + } + } +} +``` + +## Testing Plan + +1. **Basic Service Termination**: Test that a simple service is properly terminated +2. **Child Process Termination**: Test that a service with child processes has all processes terminated +3. **Graceful Shutdown**: Test that Zinit exits cleanly after all services are stopped +4. **Edge Cases**: + - Test with services that spawn many child processes + - Test with services that spawn child processes that change their process group + - Test with services that ignore SIGTERM + +## Implementation Timeline + +1. **Phase 1**: Enhance the `stop` method and add the helper methods (1-2 hours) +2. **Phase 2**: Improve the `kill_process_tree` and `kill_wait` methods (1-2 hours) +3. **Phase 3**: Update the `shutdown` and `reboot` methods (1 hour) +4. **Phase 4**: Testing and debugging (2-3 hours) \ No newline at end of file diff --git a/docs/stats.md b/docs/stats.md new file mode 100644 index 0000000..57b883b --- /dev/null +++ b/docs/stats.md @@ -0,0 +1,125 @@ +# Service Stats Functionality + +This document describes the stats functionality in Zinit, which provides memory and CPU usage information for services and their child processes. + +## Overview + +The stats functionality allows you to monitor the resource usage of services managed by Zinit. It provides information about: + +- Memory usage (in bytes) +- CPU usage (as a percentage) +- Child processes and their resource usage + +This is particularly useful for monitoring system resources and identifying services that might be consuming excessive resources. + +## Command Line Usage + +To get stats for a service using the command line: + +```bash +zinit stats +``` + +Example: +```bash +zinit stats nginx +``` + +This will output YAML-formatted stats information: + +```yaml +name: nginx +pid: 1234 +memory_usage: 10485760 # Memory usage in bytes (10MB) +cpu_usage: 2.5 # CPU usage as percentage +children: # Stats for child processes + - pid: 1235 + memory_usage: 5242880 + cpu_usage: 1.2 + - pid: 1236 + memory_usage: 4194304 + cpu_usage: 0.8 +``` + +## JSON-RPC API + +The stats functionality is also available through the JSON-RPC API: + +### Method: `service_stats` + +Get memory and CPU usage statistics for a service. + +**Parameters:** +- `name` (string, required): The name of the service to get stats for + +**Returns:** +- Object containing stats information: + - `name` (string): Service name + - `pid` (integer): Process ID of the service + - `memory_usage` (integer): Memory usage in bytes + - `cpu_usage` (number): CPU usage as a percentage (0-100) + - `children` (array): Stats for child processes + - Each child has: + - `pid` (integer): Process ID of the child process + - `memory_usage` (integer): Memory usage in bytes + - `cpu_usage` (number): CPU usage as a percentage (0-100) + +**Example Request:** +```json +{ + "jsonrpc": "2.0", + "id": 1, + "method": "service_stats", + "params": { + "name": "nginx" + } +} +``` + +**Example Response:** +```json +{ + "jsonrpc": "2.0", + "id": 1, + "result": { + "name": "nginx", + "pid": 1234, + "memory_usage": 10485760, + "cpu_usage": 2.5, + "children": [ + { + "pid": 1235, + "memory_usage": 5242880, + "cpu_usage": 1.2 + }, + { + "pid": 1236, + "memory_usage": 4194304, + "cpu_usage": 0.8 + } + ] + } +} +``` + +**Possible Errors:** +- `-32000`: Service not found +- `-32003`: Service is down + +## Implementation Details + +The stats functionality works by: + +1. Reading process information from `/proc//` directories on Linux systems +2. Calculating memory usage from `/proc//status` (VmRSS field) +3. Calculating CPU usage by sampling `/proc//stat` over a short interval +4. Identifying child processes by checking the PPid field in `/proc//status` + +On non-Linux systems, the functionality provides placeholder values as the `/proc` filesystem is specific to Linux. + +## Notes + +- Memory usage is reported in bytes +- CPU usage is reported as a percentage (0-100) +- The service must be running to get stats (otherwise an error is returned) +- Child processes are identified by their parent PID matching the service's PID \ No newline at end of file diff --git a/example/example.sh b/example/example.sh new file mode 100755 index 0000000..b08ede6 --- /dev/null +++ b/example/example.sh @@ -0,0 +1,104 @@ +#!/bin/bash + +# Determine the zinit binary path +ZINIT_BIN="./target/release/zinit" # Assuming zinit is built in release mode in the current directory + +# Determine the configuration directory based on OS +if [[ "$(uname)" == "Darwin" ]]; then + # macOS + ZINIT_CONFIG_DIR="$HOME/hero/cfg/zinit" +else + # Linux or other + ZINIT_CONFIG_DIR="/etc/zinit" +fi + +SERVICE_NAME="test_service" +CPU_SERVICE_NAME="cpu_test_service" +SERVICE_FILE="$ZINIT_CONFIG_DIR/$SERVICE_NAME.yaml" +CPU_SERVICE_FILE="$ZINIT_CONFIG_DIR/$CPU_SERVICE_NAME.yaml" + +echo "--- Zinit Example Script ---" +echo "Zinit binary path: $ZINIT_BIN" +echo "Zinit config directory: $ZINIT_CONFIG_DIR" + +# Step 1: Ensure zinit config directory exists +echo "Ensuring zinit config directory exists..." +mkdir -p "$ZINIT_CONFIG_DIR" +if [ $? -ne 0 ]; then + echo "Error: Failed to create config directory $ZINIT_CONFIG_DIR. Exiting." + exit 1 +fi +echo "Config directory $ZINIT_CONFIG_DIR is ready." + +# Step 2: Check if zinit daemon is running, if not, start it in background +echo "Checking if zinit daemon is running..." +if "$ZINIT_BIN" list > /dev/null 2>&1; then + echo "Zinit daemon is already running." +else + echo "Zinit daemon not running. Starting it in background..." + # Start zinit init in a new process group to avoid it being killed by script exit + # and redirecting output to /dev/null + nohup "$ZINIT_BIN" init > /dev/null 2>&1 & + ZINIT_PID=$! + echo "Zinit daemon started with PID: $ZINIT_PID" + sleep 2 # Give zinit a moment to start up and create the socket + if ! "$ZINIT_BIN" list > /dev/null 2>&1; then + echo "Error: Zinit daemon failed to start. Exiting." + exit 1 + fi + echo "Zinit daemon successfully started." +fi + +# Step 3: Create sample zinit service files +echo "Creating sample service file: $SERVICE_FILE" +cat < "$SERVICE_FILE" +name: $SERVICE_NAME +exec: /bin/bash -c "while true; do echo 'Hello from $SERVICE_NAME!'; sleep 5; done" +log: stdout +EOF + +if [ $? -ne 0 ]; then + echo "Error: Failed to create service file $SERVICE_FILE. Exiting." + exit 1 +fi +echo "Service file created." + +# Create a CPU-intensive service with child processes +echo "Creating CPU-intensive service file: $CPU_SERVICE_FILE" +cat < "$CPU_SERVICE_FILE" +name: $CPU_SERVICE_NAME +exec: /bin/bash -c "for i in {1..3}; do (yes > /dev/null &) ; done; while true; do sleep 10; done" +log: stdout +EOF + +if [ $? -ne 0 ]; then + echo "Error: Failed to create CPU service file $CPU_SERVICE_FILE. Exiting." + exit 1 +fi +echo "CPU service file created." + +# Step 4: Tell zinit to monitor the new services +echo "Telling zinit to monitor the services..." +"$ZINIT_BIN" monitor "$SERVICE_NAME" +"$ZINIT_BIN" monitor "$CPU_SERVICE_NAME" + +# Step 5: List services to verify the new service is recognized +echo "Listing zinit services to verify..." +"$ZINIT_BIN" list + +# Step 6: Show stats for the CPU-intensive service +echo "Waiting for services to start and generate some stats..." +sleep 5 +echo "Getting stats for $CPU_SERVICE_NAME..." +"$ZINIT_BIN" stats "$CPU_SERVICE_NAME" + +# # Step 7: Clean up (optional, but good for examples) +# echo "Cleaning up: stopping and forgetting services..." +# "$ZINIT_BIN" stop "$SERVICE_NAME" > /dev/null 2>&1 +# "$ZINIT_BIN" forget "$SERVICE_NAME" > /dev/null 2>&1 +# "$ZINIT_BIN" stop "$CPU_SERVICE_NAME" > /dev/null 2>&1 +# "$ZINIT_BIN" forget "$CPU_SERVICE_NAME" > /dev/null 2>&1 +# rm -f "$SERVICE_FILE" "$CPU_SERVICE_FILE" +# echo "Cleanup complete." + +echo "--- Script Finished ---" diff --git a/install.sh b/install.sh new file mode 100755 index 0000000..86a9b6d --- /dev/null +++ b/install.sh @@ -0,0 +1,153 @@ +#!/bin/bash +set -e + +# Colors for output +RED='\033[0;31m' +GREEN='\033[0;32m' +YELLOW='\033[0;33m' +NC='\033[0m' # No Color + +echo -e "${GREEN}stop zinit...${NC}" +rm -f /tmp/stop.sh +curl -fsSL https://raw.githubusercontent.com/threefoldtech/zinit/refs/heads/master/stop.sh > /tmp/stop.sh +bash /tmp/stop.sh + + +# GitHub repository information +GITHUB_REPO="threefoldtech/zinit" + +# Get the latest version from GitHub API +echo -e "${YELLOW}Fetching latest version information...${NC}" +if command -v curl &> /dev/null; then + VERSION=$(curl -s "https://api.github.com/repos/${GITHUB_REPO}/releases/latest" | grep -o '"tag_name": "[^"]*' | grep -o '[^"]*$') +elif command -v wget &> /dev/null; then + VERSION=$(wget -qO- "https://api.github.com/repos/${GITHUB_REPO}/releases/latest" | grep -o '"tag_name": "[^"]*' | grep -o '[^"]*$') +else + echo -e "${RED}Neither curl nor wget found. Please install one of them and try again.${NC}" + exit 1 +fi + +if [ -z "$VERSION" ]; then + echo -e "${RED}Failed to fetch the latest version. Please check your internet connection.${NC}" + exit 1 +fi + +echo -e "${GREEN}Latest version: ${VERSION}${NC}" +DOWNLOAD_URL="https://github.com/${GITHUB_REPO}/releases/download/${VERSION}" +MIN_SIZE_BYTES=2000000 # 2MB in bytes + +echo -e "${GREEN}Installing zinit ${VERSION}...${NC}" + +# Create temporary directory +TMP_DIR=$(mktemp -d) +trap 'rm -rf "$TMP_DIR"' EXIT + +# Detect OS and architecture +OS=$(uname -s | tr '[:upper:]' '[:lower:]') +ARCH=$(uname -m) + +# Map architecture names +if [ "$ARCH" = "x86_64" ]; then + ARCH_NAME="x86_64" +elif [ "$ARCH" = "aarch64" ] || [ "$ARCH" = "arm64" ]; then + ARCH_NAME="aarch64" +else + echo -e "${RED}Unsupported architecture: $ARCH${NC}" + exit 1 +fi + +# Determine binary name based on OS and architecture +if [ "$OS" = "linux" ]; then + if [ "$ARCH_NAME" = "x86_64" ]; then + BINARY_NAME="zinit-linux-x86_64" + else + echo -e "${RED}Unsupported Linux architecture: $ARCH${NC}" + exit 1 + fi +elif [ "$OS" = "darwin" ]; then + if [ "$ARCH_NAME" = "x86_64" ]; then + BINARY_NAME="zinit-macos-x86_64" + elif [ "$ARCH_NAME" = "aarch64" ]; then + BINARY_NAME="zinit-macos-aarch64" + else + echo -e "${RED}Unsupported macOS architecture: $ARCH${NC}" + exit 1 + fi +else + echo -e "${RED}Unsupported operating system: $OS${NC}" + exit 1 +fi + +# Download URL +DOWNLOAD_PATH="${DOWNLOAD_URL}/${BINARY_NAME}" +LOCAL_PATH="${TMP_DIR}/${BINARY_NAME}" + +echo -e "${YELLOW}Detected: $OS on $ARCH_NAME${NC}" +echo -e "${YELLOW}Downloading from: $DOWNLOAD_PATH${NC}" + +# Download the binary +if command -v curl &> /dev/null; then + curl -L -o "$LOCAL_PATH" "$DOWNLOAD_PATH" +elif command -v wget &> /dev/null; then + wget -O "$LOCAL_PATH" "$DOWNLOAD_PATH" +else + echo -e "${RED}Neither curl nor wget found. Please install one of them and try again.${NC}" + exit 1 +fi + +# Check file size +FILE_SIZE=$(stat -f%z "$LOCAL_PATH" 2>/dev/null || stat -c%s "$LOCAL_PATH" 2>/dev/null) +if [ "$FILE_SIZE" -lt "$MIN_SIZE_BYTES" ]; then + echo -e "${RED}Downloaded file is too small (${FILE_SIZE} bytes). Expected at least ${MIN_SIZE_BYTES} bytes.${NC}" + echo -e "${RED}This might indicate a failed or incomplete download.${NC}" + exit 1 +fi + +echo -e "${GREEN}Download successful. File size: $(echo "$FILE_SIZE / 1000000" | bc -l | xargs printf "%.2f") MB${NC}" + +# Make the binary executable +chmod +x "$LOCAL_PATH" + +# Determine installation directory +if [ "$OS" = "darwin" ]; then + # macOS - install to ~/hero/bin/ + INSTALL_DIR="$HOME/hero/bin" +else + # Linux - install to /usr/local/bin/ if running as root, otherwise to ~/.local/bin/ + if [ "$(id -u)" -eq 0 ]; then + INSTALL_DIR="/usr/local/bin" + else + INSTALL_DIR="$HOME/.local/bin" + # Ensure ~/.local/bin exists and is in PATH + mkdir -p "$INSTALL_DIR" + if [[ ":$PATH:" != *":$INSTALL_DIR:"* ]]; then + echo -e "${YELLOW}Adding $INSTALL_DIR to your PATH. You may need to restart your terminal.${NC}" + if [ -f "$HOME/.bashrc" ]; then + echo "export PATH=\"\$PATH:$INSTALL_DIR\"" >> "$HOME/.bashrc" + fi + if [ -f "$HOME/.zshrc" ]; then + echo "export PATH=\"\$PATH:$INSTALL_DIR\"" >> "$HOME/.zshrc" + fi + fi + fi +fi + +# Create installation directory if it doesn't exist +mkdir -p "$INSTALL_DIR" + +# Copy the binary to the installation directory +cp "$LOCAL_PATH" "$INSTALL_DIR/zinit" +echo -e "${GREEN}Installed zinit to $INSTALL_DIR/zinit${NC}" + +# Test the installation +echo -e "${YELLOW}Testing installation...${NC}" +if "$INSTALL_DIR/zinit" --help &> /dev/null; then + echo -e "${GREEN}Installation successful! You can now use 'zinit' command.${NC}" + echo -e "${YELLOW}Example usage: zinit --help${NC}" + "$INSTALL_DIR/zinit" --help | head -n 5 +else + echo -e "${RED}Installation test failed. Please check the error messages above.${NC}" + exit 1 +fi + +echo -e "${GREEN}zinit ${VERSION} has been successfully installed!${NC}" \ No newline at end of file diff --git a/install_run.sh b/install_run.sh new file mode 100755 index 0000000..c86634a --- /dev/null +++ b/install_run.sh @@ -0,0 +1,50 @@ +#!/bin/bash +set -e + +# Colors for output +RED='\033[0;31m' +GREEN='\033[0;32m' +YELLOW='\033[0;33m' +NC='\033[0m' # No Color + +# Function to check if zinit is running +is_zinit_running() { + if zinit list &>/dev/null; then + return 0 # Command successful, zinit is running + else + return 1 # Command failed, zinit is not running + fi +} + +echo -e "${GREEN}Starting zinit installation and setup...${NC}" +# Download and execute install.sh +echo -e "${YELLOW}Downloading and executing install.sh...${NC}" +curl -fsSL https://raw.githubusercontent.com/threefoldtech/zinit/refs/heads/master/install.sh | bash + +echo -e "${GREEN}install zinit...${NC}" +rm -f /tmp/install.sh +curl -fsSL https://raw.githubusercontent.com/threefoldtech/zinit/refs/heads/master/install.sh > /tmp/install.sh +bash /tmp/install.sh + + +# Launch zinit in the background +echo -e "${GREEN}Starting zinit in the background...${NC}" +zinit & + +# Give it a moment to start +sleep 1 + +# Verify zinit is running +if is_zinit_running; then + echo -e "${GREEN}Zinit is now running in the background.${NC}" + echo -e "${YELLOW}You can manage services with:${NC}" + echo -e " ${YELLOW}$ZINIT_PATH list${NC} - List all services" + echo -e " ${YELLOW}$ZINIT_PATH status${NC} - Show status of all services" + echo -e " ${YELLOW}$ZINIT_PATH monitor${NC} - Monitor services in real-time" + echo -e " ${YELLOW}$ZINIT_PATH shutdown${NC} - Shutdown zinit when needed" +else + echo -e "${RED}Failed to start zinit. Please check for errors above.${NC}" + exit 1 +fi + +echo -e "${GREEN}Zinit installation and startup complete!${NC}" \ No newline at end of file diff --git a/openrpc.json b/openrpc.json new file mode 100644 index 0000000..6e3e3ee --- /dev/null +++ b/openrpc.json @@ -0,0 +1,873 @@ +{ + "openrpc": "1.2.6", + "info": { + "version": "1.0.0", + "title": "Zinit JSON-RPC API", + "description": "JSON-RPC 2.0 API for controlling and querying Zinit services", + "license": { + "name": "MIT" + } + }, + "servers": [ + { + "name": "Unix Socket", + "url": "unix:///tmp/zinit.sock" + } + ], + "methods": [ + { + "name": "rpc.discover", + "description": "Returns the OpenRPC specification for the API", + "params": [], + "result": { + "name": "OpenRPCSpec", + "description": "The OpenRPC specification", + "schema": { + "type": "object" + } + }, + "examples": [ + { + "name": "Get API specification", + "params": [], + "result": { + "name": "OpenRPCSpecResult", + "value": { + "openrpc": "1.2.6", + "info": { + "version": "1.0.0", + "title": "Zinit JSON-RPC API" + } + } + } + } + ] + }, + { + "name": "service_list", + "description": "Lists all services managed by Zinit", + "params": [], + "result": { + "name": "ServiceList", + "description": "A map of service names to their current states", + "schema": { + "type": "object", + "additionalProperties": { + "type": "string", + "description": "Service state (Running, Success, Error, etc.)" + } + } + }, + "examples": [ + { + "name": "List all services", + "params": [], + "result": { + "name": "ServiceListResult", + "value": { + "service1": "Running", + "service2": "Success", + "service3": "Error" + } + } + } + ] + }, + { + "name": "service_status", + "description": "Shows detailed status information for a specific service", + "params": [ + { + "name": "name", + "description": "The name of the service", + "required": true, + "schema": { + "type": "string" + } + } + ], + "result": { + "name": "ServiceStatus", + "description": "Detailed status information for the service", + "schema": { + "type": "object", + "properties": { + "name": { + "type": "string", + "description": "Service name" + }, + "pid": { + "type": "integer", + "description": "Process ID of the running service (if running)" + }, + "state": { + "type": "string", + "description": "Current state of the service (Running, Success, Error, etc.)" + }, + "target": { + "type": "string", + "description": "Target state of the service (Up, Down)" + }, + "after": { + "type": "object", + "description": "Dependencies of the service and their states", + "additionalProperties": { + "type": "string", + "description": "State of the dependency" + } + } + } + } + }, + "examples": [ + { + "name": "Get status of redis service", + "params": [ + { + "name": "name", + "value": "redis" + } + ], + "result": { + "name": "ServiceStatusResult", + "value": { + "name": "redis", + "pid": 1234, + "state": "Running", + "target": "Up", + "after": { + "dependency1": "Success", + "dependency2": "Running" + } + } + } + } + ], + "errors": [ + { + "code": -32000, + "message": "Service not found", + "data": "service name \"unknown\" unknown" + } + ] + }, + { + "name": "service_start", + "description": "Starts a service", + "params": [ + { + "name": "name", + "description": "The name of the service to start", + "required": true, + "schema": { + "type": "string" + } + } + ], + "result": { + "name": "StartResult", + "description": "Result of the start operation", + "schema": { + "type": "null" + } + }, + "examples": [ + { + "name": "Start redis service", + "params": [ + { + "name": "name", + "value": "redis" + } + ], + "result": { + "name": "StartResult", + "value": null + } + } + ], + "errors": [ + { + "code": -32000, + "message": "Service not found", + "data": "service name \"unknown\" unknown" + } + ] + }, + { + "name": "service_stop", + "description": "Stops a service", + "params": [ + { + "name": "name", + "description": "The name of the service to stop", + "required": true, + "schema": { + "type": "string" + } + } + ], + "result": { + "name": "StopResult", + "description": "Result of the stop operation", + "schema": { + "type": "null" + } + }, + "examples": [ + { + "name": "Stop redis service", + "params": [ + { + "name": "name", + "value": "redis" + } + ], + "result": { + "name": "StopResult", + "value": null + } + } + ], + "errors": [ + { + "code": -32000, + "message": "Service not found", + "data": "service name \"unknown\" unknown" + }, + { + "code": -32003, + "message": "Service is down", + "data": "service \"redis\" is down" + } + ] + }, + { + "name": "service_monitor", + "description": "Starts monitoring a service. The service configuration is loaded from the config directory.", + "params": [ + { + "name": "name", + "description": "The name of the service to monitor", + "required": true, + "schema": { + "type": "string" + } + } + ], + "result": { + "name": "MonitorResult", + "description": "Result of the monitor operation", + "schema": { + "type": "null" + } + }, + "examples": [ + { + "name": "Monitor redis service", + "params": [ + { + "name": "name", + "value": "redis" + } + ], + "result": { + "name": "MonitorResult", + "value": null + } + } + ], + "errors": [ + { + "code": -32001, + "message": "Service already monitored", + "data": "service \"redis\" already monitored" + }, + { + "code": -32005, + "message": "Config error", + "data": "failed to load service configuration" + } + ] + }, + { + "name": "service_forget", + "description": "Stops monitoring a service. You can only forget a stopped service.", + "params": [ + { + "name": "name", + "description": "The name of the service to forget", + "required": true, + "schema": { + "type": "string" + } + } + ], + "result": { + "name": "ForgetResult", + "description": "Result of the forget operation", + "schema": { + "type": "null" + } + }, + "examples": [ + { + "name": "Forget redis service", + "params": [ + { + "name": "name", + "value": "redis" + } + ], + "result": { + "name": "ForgetResult", + "value": null + } + } + ], + "errors": [ + { + "code": -32000, + "message": "Service not found", + "data": "service name \"unknown\" unknown" + }, + { + "code": -32002, + "message": "Service is up", + "data": "service \"redis\" is up" + } + ] + }, + { + "name": "service_kill", + "description": "Sends a signal to a running service", + "params": [ + { + "name": "name", + "description": "The name of the service to send the signal to", + "required": true, + "schema": { + "type": "string" + } + }, + { + "name": "signal", + "description": "The signal to send (e.g., SIGTERM, SIGKILL)", + "required": true, + "schema": { + "type": "string" + } + } + ], + "result": { + "name": "KillResult", + "description": "Result of the kill operation", + "schema": { + "type": "null" + } + }, + "examples": [ + { + "name": "Send SIGTERM to redis service", + "params": [ + { + "name": "name", + "value": "redis" + }, + { + "name": "signal", + "value": "SIGTERM" + } + ], + "result": { + "name": "KillResult", + "value": null + } + } + ], + "errors": [ + { + "code": -32000, + "message": "Service not found", + "data": "service name \"unknown\" unknown" + }, + { + "code": -32003, + "message": "Service is down", + "data": "service \"redis\" is down" + }, + { + "code": -32004, + "message": "Invalid signal", + "data": "invalid signal: INVALID" + } + ] + }, + { + "name": "system_shutdown", + "description": "Stops all services and powers off the system", + "params": [], + "result": { + "name": "ShutdownResult", + "description": "Result of the shutdown operation", + "schema": { + "type": "null" + } + }, + "examples": [ + { + "name": "Shutdown the system", + "params": [], + "result": { + "name": "ShutdownResult", + "value": null + } + } + ], + "errors": [ + { + "code": -32006, + "message": "Shutting down", + "data": "system is already shutting down" + } + ] + }, + { + "name": "system_reboot", + "description": "Stops all services and reboots the system", + "params": [], + "result": { + "name": "RebootResult", + "description": "Result of the reboot operation", + "schema": { + "type": "null" + } + }, + "examples": [ + { + "name": "Reboot the system", + "params": [], + "result": { + "name": "RebootResult", + "value": null + } + } + ], + "errors": [ + { + "code": -32006, + "message": "Shutting down", + "data": "system is already shutting down" + } + ] + }, + { + "name": "service_create", + "description": "Creates a new service configuration file", + "params": [ + { + "name": "name", + "description": "The name of the service to create", + "required": true, + "schema": { + "type": "string" + } + }, + { + "name": "content", + "description": "The service configuration content", + "required": true, + "schema": { + "type": "object", + "properties": { + "exec": { + "type": "string", + "description": "Command to run" + }, + "oneshot": { + "type": "boolean", + "description": "Whether the service should be restarted" + }, + "after": { + "type": "array", + "items": { + "type": "string" + }, + "description": "Services that must be running before this one starts" + }, + "log": { + "type": "string", + "enum": ["null", "ring", "stdout"], + "description": "How to handle service output" + }, + "env": { + "type": "object", + "additionalProperties": { + "type": "string" + }, + "description": "Environment variables for the service" + }, + "shutdown_timeout": { + "type": "integer", + "description": "Maximum time to wait for service to stop during shutdown" + } + } + } + } + ], + "result": { + "name": "CreateServiceResult", + "description": "Result of the create operation", + "schema": { + "type": "string" + } + }, + "errors": [ + { + "code": -32007, + "message": "Service already exists", + "data": "Service 'name' already exists" + }, + { + "code": -32008, + "message": "Service file error", + "data": "Failed to create service file" + } + ] + }, + { + "name": "service_delete", + "description": "Deletes a service configuration file", + "params": [ + { + "name": "name", + "description": "The name of the service to delete", + "required": true, + "schema": { + "type": "string" + } + } + ], + "result": { + "name": "DeleteServiceResult", + "description": "Result of the delete operation", + "schema": { + "type": "string" + } + }, + "errors": [ + { + "code": -32000, + "message": "Service not found", + "data": "Service 'name' not found" + }, + { + "code": -32008, + "message": "Service file error", + "data": "Failed to delete service file" + } + ] + }, + { + "name": "service_get", + "description": "Gets a service configuration file", + "params": [ + { + "name": "name", + "description": "The name of the service to get", + "required": true, + "schema": { + "type": "string" + } + } + ], + "result": { + "name": "GetServiceResult", + "description": "The service configuration", + "schema": { + "type": "object" + } + }, + "errors": [ + { + "code": -32000, + "message": "Service not found", + "data": "Service 'name' not found" + }, + { + "code": -32008, + "message": "Service file error", + "data": "Failed to read service file" + } + ] + }, + { + "name": "service_stats", + "description": "Get memory and CPU usage statistics for a service", + "params": [ + { + "name": "name", + "description": "The name of the service to get stats for", + "required": true, + "schema": { + "type": "string" + } + } + ], + "result": { + "name": "ServiceStats", + "description": "Memory and CPU usage statistics for the service", + "schema": { + "type": "object", + "properties": { + "name": { + "type": "string", + "description": "Service name" + }, + "pid": { + "type": "integer", + "description": "Process ID of the service" + }, + "memory_usage": { + "type": "integer", + "description": "Memory usage in bytes" + }, + "cpu_usage": { + "type": "number", + "description": "CPU usage as a percentage (0-100)" + }, + "children": { + "type": "array", + "description": "Stats for child processes", + "items": { + "type": "object", + "properties": { + "pid": { + "type": "integer", + "description": "Process ID of the child process" + }, + "memory_usage": { + "type": "integer", + "description": "Memory usage in bytes" + }, + "cpu_usage": { + "type": "number", + "description": "CPU usage as a percentage (0-100)" + } + } + } + } + } + } + }, + "examples": [ + { + "name": "Get stats for redis service", + "params": [ + { + "name": "name", + "value": "redis" + } + ], + "result": { + "name": "ServiceStatsResult", + "value": { + "name": "redis", + "pid": 1234, + "memory_usage": 10485760, + "cpu_usage": 2.5, + "children": [ + { + "pid": 1235, + "memory_usage": 5242880, + "cpu_usage": 1.2 + } + ] + } + } + } + ], + "errors": [ + { + "code": -32000, + "message": "Service not found", + "data": "service name \"unknown\" unknown" + }, + { + "code": -32003, + "message": "Service is down", + "data": "service \"redis\" is down" + } + ] + }, + { + "name": "system_start_http_server", + "description": "Start an HTTP/RPC server at the specified address", + "params": [ + { + "name": "address", + "description": "The network address to bind the server to (e.g., '127.0.0.1:8080')", + "required": true, + "schema": { + "type": "string" + } + } + ], + "result": { + "name": "StartHttpServerResult", + "description": "Result of the start HTTP server operation", + "schema": { + "type": "string" + } + }, + "examples": [ + { + "name": "Start HTTP server on localhost:8080", + "params": [ + { + "name": "address", + "value": "127.0.0.1:8080" + } + ], + "result": { + "name": "StartHttpServerResult", + "value": "HTTP server started at 127.0.0.1:8080" + } + } + ], + "errors": [ + { + "code": -32602, + "message": "Invalid address", + "data": "Invalid network address format" + } + ] + }, + { + "name": "system_stop_http_server", + "description": "Stop the HTTP/RPC server if running", + "params": [], + "result": { + "name": "StopHttpServerResult", + "description": "Result of the stop HTTP server operation", + "schema": { + "type": "null" + } + }, + "examples": [ + { + "name": "Stop the HTTP server", + "params": [], + "result": { + "name": "StopHttpServerResult", + "value": null + } + } + ], + "errors": [ + { + "code": -32602, + "message": "Server not running", + "data": "No HTTP server is currently running" + } + ] + }, + { + "name": "stream_currentLogs", + "description": "Get current logs from zinit and monitored services", + "params": [ + { + "name": "name", + "description": "Optional service name filter. If provided, only logs from this service will be returned", + "required": false, + "schema": { + "type": "string" + } + } + ], + "result": { + "name": "LogsResult", + "description": "Array of log strings", + "schema": { + "type": "array", + "items": { + "type": "string" + } + } + }, + "examples": [ + { + "name": "Get all logs", + "params": [], + "result": { + "name": "LogsResult", + "value": [ + "2023-01-01T12:00:00 redis: Starting service", + "2023-01-01T12:00:01 nginx: Starting service" + ] + } + }, + { + "name": "Get logs for a specific service", + "params": [ + { + "name": "name", + "value": "redis" + } + ], + "result": { + "name": "LogsResult", + "value": [ + "2023-01-01T12:00:00 redis: Starting service", + "2023-01-01T12:00:02 redis: Service started" + ] + } + } + ] + }, + { + "name": "stream_subscribeLogs", + "description": "Subscribe to log messages generated by zinit and monitored services", + "params": [ + { + "name": "name", + "description": "Optional service name filter. If provided, only logs from this service will be returned", + "required": false, + "schema": { + "type": "string" + } + } + ], + "result": { + "name": "LogSubscription", + "description": "A subscription to log messages", + "schema": { + "type": "string" + } + }, + "examples": [ + { + "name": "Subscribe to all logs", + "params": [], + "result": { + "name": "LogSubscription", + "value": "2023-01-01T12:00:00 redis: Service started" + } + }, + { + "name": "Subscribe to filtered logs", + "params": [ + { + "name": "name", + "value": "redis" + } + ], + "result": { + "name": "LogSubscription", + "value": "2023-01-01T12:00:00 redis: Service started" + } + } + ] + } + ] +} \ No newline at end of file diff --git a/osx_build.sh b/osx_build.sh new file mode 100755 index 0000000..a6a4ca9 --- /dev/null +++ b/osx_build.sh @@ -0,0 +1,57 @@ +#!/bin/bash + +# Jump to the directory of the script +cd "$(dirname "$0")" + +./stop.sh + +# Build the project +echo "Building zinit..." +cargo build --release + +if [ $? -ne 0 ]; then + echo "Build failed!" + exit 1 +fi + +# Copy the binary +echo "Copying zinit binary to ~/hero/bin..." +cp ./target/release/zinit ~/hero/bin + +if [ $? -ne 0 ]; then + echo "Failed to copy binary!" + exit 1 +fi + +# Ensure config directory exists +echo "Ensuring config directory exists..." +mkdir -p ~/hero/cfg/zinit + +# Start zinit in init mode (daemon) in background +echo "Starting zinit daemon in background..." +~/hero/bin/zinit init -c ~/hero/cfg/zinit & +ZINIT_PID=$! + +# Wait a moment for zinit to start and create the socket +sleep 5 + +# Check if zinit is running +if kill -0 $ZINIT_PID 2>/dev/null; then + echo "Zinit daemon started successfully with PID: $ZINIT_PID" + + # Test with zinit list + echo "Testing zinit list command..." + ~/hero/bin/zinit list + + if [ $? -eq 0 ]; then + echo "Zinit is working correctly!" + else + echo "Warning: zinit list command failed, but zinit daemon is running" + echo "This might be normal if no services are configured yet." + fi +else + echo "Failed to start zinit daemon!" + exit 1 +fi + +echo "Build and setup completed successfully!" diff --git a/release_zinit.sh b/release_zinit.sh new file mode 100755 index 0000000..6969c58 --- /dev/null +++ b/release_zinit.sh @@ -0,0 +1,101 @@ +#!/bin/bash + +# Navigate to the zinit project directory +cd /Users/despiegk/code/github/threefoldtech/zinit + +# Check if we're in the right directory +if [ ! -f "Cargo.toml" ]; then + echo "Error: Not in zinit project directory" + exit 1 +fi + +# Function to get the latest tag from Git +get_latest_tag() { + # Fetch all tags from remote + git fetch --tags origin 2>/dev/null + + # Get the latest tag using version sorting + local latest_tag=$(git tag -l "v*" | sort -V | tail -n 1) + + if [ -z "$latest_tag" ]; then + echo "v0.0.0" + else + echo "$latest_tag" + fi +} + +# Function to increment version +increment_version() { + local version=$1 + # Remove 'v' prefix if present + version=${version#v} + + # Split version into parts + IFS='.' read -ra PARTS <<< "$version" + major=${PARTS[0]:-0} + minor=${PARTS[1]:-0} + patch=${PARTS[2]:-0} + + # Increment patch (maintenance) version + patch=$((patch + 1)) + + echo "v${major}.${minor}.${patch}" +} + +echo "🔍 Checking latest tag..." +latest_tag=$(get_latest_tag) +echo "Latest tag: $latest_tag" + +new_version=$(increment_version "$latest_tag") +echo "New version: $new_version" + +# Confirm with user +read -p "Create release $new_version? (y/N): " -n 1 -r +echo +if [[ ! $REPLY =~ ^[Yy]$ ]]; then + echo "Release cancelled" + exit 0 +fi + +# Check if tag already exists locally and remove it +if git tag -l | grep -q "^$new_version$"; then + echo "⚠️ Local tag $new_version already exists, removing it..." + git tag -d "$new_version" +fi + +# Make sure we're on the right branch and up to date +echo "🔄 Updating repository..." +git fetch origin + +# Get current branch name +current_branch=$(git branch --show-current) + +# If we're not on main or master, try to checkout one of them +if [[ "$current_branch" != "main" && "$current_branch" != "master" ]]; then + echo "Current branch: $current_branch" + if git show-ref --verify --quiet refs/heads/main; then + echo "Switching to main branch..." + git checkout main + current_branch="main" + elif git show-ref --verify --quiet refs/heads/master; then + echo "Switching to master branch..." + git checkout master + current_branch="master" + else + echo "⚠️ Neither main nor master branch found, staying on current branch: $current_branch" + fi +fi + +echo "Pulling latest changes from $current_branch..." +git pull origin "$current_branch" + +# Create and push the tag +echo "🏷️ Creating tag $new_version..." +git tag "$new_version" + +echo "🚀 Pushing tag to trigger release..." +git push origin "$new_version" + +echo "✅ Release $new_version has been triggered!" +echo "🔗 Check the release at: https://github.com/threefoldtech/zinit/releases" +echo "🔗 Monitor the build at: https://github.com/threefoldtech/zinit/actions" diff --git a/src/app/api.rs b/src/app/api.rs new file mode 100644 index 0000000..1315702 --- /dev/null +++ b/src/app/api.rs @@ -0,0 +1,139 @@ +use super::rpc::{ + ZinitLoggingApiServer, ZinitRpcApiServer, ZinitServiceApiServer, ZinitSystemApiServer, +}; +use crate::zinit::ZInit; +use anyhow::{bail, Context, Result}; +use jsonrpsee::server::ServerHandle; +use reth_ipc::server::Builder; +use serde::{Deserialize, Serialize}; +use serde_json::Value; +use std::collections::HashMap; +use std::sync::Arc; +use tokio::sync::Mutex; +use tower_http::cors::{AllowHeaders, AllowMethods}; +use tower_http::cors::{Any, CorsLayer}; + +#[derive(Clone, Debug, Deserialize, Serialize)] +#[serde(rename_all = "lowercase")] +struct ZinitResponse { + pub state: ZinitState, + pub body: Value, +} + +#[derive(Clone, Debug, Deserialize, Serialize)] +#[serde(rename_all = "lowercase")] +enum ZinitState { + Ok, + Error, +} + +#[derive(Clone, Debug, Deserialize, Serialize)] +#[serde(rename_all = "lowercase")] +pub struct Status { + pub name: String, + pub pid: u32, + pub state: String, + pub target: String, + pub after: HashMap, +} + +/// Service stats information +#[derive(Clone, Debug, Deserialize, Serialize)] +#[serde(rename_all = "lowercase")] +pub struct Stats { + pub name: String, + pub pid: u32, + pub memory_usage: u64, + pub cpu_usage: f32, + pub children: Vec, +} + +/// Child process stats information +#[derive(Clone, Debug, Deserialize, Serialize)] +#[serde(rename_all = "lowercase")] +pub struct ChildStats { + pub pid: u32, + pub memory_usage: u64, + pub cpu_usage: f32, +} + +pub struct ApiServer { + _handle: ServerHandle, +} + +#[derive(Clone)] +pub struct Api { + pub zinit: ZInit, + pub http_server_handle: Arc>>, +} + +impl Api { + pub fn new(zinit: ZInit) -> Api { + Api { + zinit, + http_server_handle: Arc::new(Mutex::new(None)), + } + } + + pub async fn serve(&self, endpoint: String) -> Result { + let server = Builder::default().build(endpoint); + let mut module = ZinitRpcApiServer::into_rpc(self.clone()); + module.merge(ZinitSystemApiServer::into_rpc(self.clone()))?; + module.merge(ZinitServiceApiServer::into_rpc(self.clone()))?; + module.merge(ZinitLoggingApiServer::into_rpc(self.clone()))?; + + let _handle = server.start(module).await?; + + Ok(ApiServer { _handle }) + } + + /// Start an HTTP/RPC server at a specified address + pub async fn start_http_server(&self, address: String) -> Result { + // Parse the address string + let socket_addr = address + .parse::() + .context("Failed to parse socket address")?; + + let cors = CorsLayer::new() + // Allow `POST` when accessing the resource + .allow_methods(AllowMethods::any()) + // Allow requests from any origin + .allow_origin(Any) + .allow_headers(AllowHeaders::any()); + let middleware = tower::ServiceBuilder::new().layer(cors); + + // Create the JSON-RPC server with CORS support + let server_rpc = jsonrpsee::server::ServerBuilder::default() + .set_http_middleware(middleware) + .build(socket_addr) + .await?; + + // Create and merge all API modules + let mut rpc_module = ZinitRpcApiServer::into_rpc(self.clone()); + rpc_module.merge(ZinitSystemApiServer::into_rpc(self.clone()))?; + rpc_module.merge(ZinitServiceApiServer::into_rpc(self.clone()))?; + rpc_module.merge(ZinitLoggingApiServer::into_rpc(self.clone()))?; + + // Start the server + let handle = server_rpc.start(rpc_module); + + // Store the handle + let mut http_handle = self.http_server_handle.lock().await; + *http_handle = Some(handle); + + Ok(format!("HTTP/RPC server started at {}", address)) + } + + /// Stop the HTTP/RPC server if running + pub async fn stop_http_server(&self) -> Result<()> { + let mut http_handle = self.http_server_handle.lock().await; + + if http_handle.is_some() { + // The handle is automatically dropped, which should stop the server + *http_handle = None; + Ok(()) + } else { + bail!("No HTTP/RPC server is currently running") + } + } +} diff --git a/src/app/mod.rs b/src/app/mod.rs new file mode 100644 index 0000000..45c5b94 --- /dev/null +++ b/src/app/mod.rs @@ -0,0 +1,265 @@ +pub mod api; +pub mod rpc; + +use crate::zinit; +use anyhow::{Context, Result}; +use api::ApiServer; +use reth_ipc::client::IpcClientBuilder; +use rpc::ZinitLoggingApiClient; +use rpc::ZinitServiceApiClient; +use rpc::ZinitSystemApiClient; +use serde_yaml as encoder; +use std::net::ToSocketAddrs; +use std::path::{Path, PathBuf}; +use tokio::fs; +use tokio::signal; +use tokio::time; +use tokio_stream::wrappers::ReceiverStream; +use tokio_stream::Stream; + +fn logger(level: log::LevelFilter) -> Result<()> { + let logger = fern::Dispatch::new() + .format(|out, message, record| { + out.finish(format_args!( + "zinit: {} ({}) {}", + record.level(), + record.target(), + message + )) + }) + .level(level) + .chain(std::io::stdout()); + let logger = match std::fs::OpenOptions::new().write(true).open("/dev/kmsg") { + Ok(file) => logger.chain(file), + Err(_err) => logger, + }; + logger.apply()?; + + Ok(()) +} + +fn absolute>(p: P) -> Result { + let p = p.as_ref(); + let result = if p.is_absolute() { + p.to_path_buf() + } else { + let mut current = std::env::current_dir()?; + current.push(p); + current + }; + + Ok(result) +} + +pub async fn init( + cap: usize, + config: &str, + socket: &str, + container: bool, + debug: bool, +) -> Result { + fs::create_dir_all(config) + .await + .with_context(|| format!("failed to create config directory '{}'", config))?; + if let Err(err) = logger(if debug { + log::LevelFilter::Debug + } else { + log::LevelFilter::Info + }) { + eprintln!("failed to setup logging: {}", err); + } + + let config = absolute(Path::new(config)).context("failed to get config dire absolute path")?; + let socket_path = + absolute(Path::new(socket)).context("failed to get socket file absolute path")?; + + if let Some(dir) = socket_path.parent() { + fs::create_dir_all(dir) + .await + .with_context(|| format!("failed to create directory {:?}", dir))?; + } + + let _ = fs::remove_file(&socket).await; + + debug!("switching to home dir: {}", config.display()); + std::env::set_current_dir(&config).with_context(|| { + format!( + "failed to switch working directory to '{}'", + config.display() + ) + })?; + + let init = zinit::ZInit::new(cap, container); + + init.serve(); + + let services = zinit::config::load_dir(&config)?; + for (k, v) in services { + if let Err(err) = init.monitor(&k, v).await { + error!("failed to monitor service {}: {}", k, err); + }; + } + let a = api::Api::new(init); + a.serve(socket.into()).await +} + +pub async fn list(socket: &str) -> Result<()> { + let client = IpcClientBuilder::default().build(socket.into()).await?; + let results = client.list().await?; + encoder::to_writer(std::io::stdout(), &results)?; + Ok(()) +} + +pub async fn shutdown(socket: &str) -> Result<()> { + let client = IpcClientBuilder::default().build(socket.into()).await?; + client.shutdown().await?; + Ok(()) +} + +pub async fn reboot(socket: &str) -> Result<()> { + let client = IpcClientBuilder::default().build(socket.into()).await?; + client.reboot().await?; + Ok(()) +} + +pub async fn status(socket: &str, name: String) -> Result<()> { + let client = IpcClientBuilder::default().build(socket.into()).await?; + let results = client.status(name).await?; + encoder::to_writer(std::io::stdout(), &results)?; + Ok(()) +} + +pub async fn start(socket: &str, name: String) -> Result<()> { + let client = IpcClientBuilder::default().build(socket.into()).await?; + client.start(name).await?; + Ok(()) +} + +pub async fn stop(socket: &str, name: String) -> Result<()> { + let client = IpcClientBuilder::default().build(socket.into()).await?; + client.stop(name).await?; + Ok(()) +} + +pub async fn restart(socket: &str, name: String) -> Result<()> { + let client = IpcClientBuilder::default().build(socket.into()).await?; + client.stop(name.clone()).await?; + //pull status + for _ in 0..20 { + let result = client.status(name.clone()).await?; + if result.pid == 0 && result.target == "Down" { + client.start(name.clone()).await?; + return Ok(()); + } + time::sleep(std::time::Duration::from_secs(1)).await; + } + // process not stopped try to kill it + client.kill(name.clone(), "SIGKILL".into()).await?; + client.start(name).await?; + Ok(()) +} + +pub async fn forget(socket: &str, name: String) -> Result<()> { + let client = IpcClientBuilder::default().build(socket.into()).await?; + client.forget(name).await?; + Ok(()) +} + +pub async fn monitor(socket: &str, name: String) -> Result<()> { + let client = IpcClientBuilder::default().build(socket.into()).await?; + client.monitor(name).await?; + Ok(()) +} + +pub async fn kill(socket: &str, name: String, signal: String) -> Result<()> { + let client = IpcClientBuilder::default().build(socket.into()).await?; + client.kill(name, signal).await?; + Ok(()) +} + +pub async fn stats(socket: &str, name: String) -> Result<()> { + let client = IpcClientBuilder::default().build(socket.into()).await?; + let results = client.stats(name).await?; + encoder::to_writer(std::io::stdout(), &results)?; + Ok(()) +} + +pub async fn logs( + socket: &str, + filter: Option, + follow: bool, +) -> Result + Unpin> { + let client = IpcClientBuilder::default().build(socket.into()).await?; + if let Some(ref filter) = filter { + client.status(filter.clone()).await?; + } + let logs = client.logs(filter.clone()).await?; + let (tx, rx) = tokio::sync::mpsc::channel(2000); + + let logs_sub = if follow { + Some(client.log_subscribe(filter).await?) + } else { + None + }; + tokio::task::spawn(async move { + for log in logs { + if tx.send(log).await.is_err() { + if let Some(logs_sub) = logs_sub { + let _ = logs_sub.unsubscribe().await; + } + // error means receiver is dead, so just quit + return; + } + } + let Some(mut logs_sub) = logs_sub else { return }; + loop { + match logs_sub.next().await { + Some(Ok(log)) => { + if tx.send(log).await.is_err() { + let _ = logs_sub.unsubscribe().await; + return; + } + } + Some(Err(e)) => { + log::error!("Failed to get new log from subscription: {e}"); + return; + } + _ => return, + } + } + }); + + Ok(ReceiverStream::new(rx)) +} + +/// Start an HTTP/RPC proxy server for the Zinit API at the specified address +pub async fn proxy(sock: &str, address: String) -> Result<()> { + // Parse the socket address + let _socket_addr = address + .to_socket_addrs() + .context("Failed to parse socket address")? + .next() + .context("No valid socket address found")?; + + println!("Starting HTTP/RPC server on {}", address); + println!("Connecting to Zinit daemon at {}", sock); + + // Connect to the existing Zinit daemon through the Unix socket + let client = IpcClientBuilder::default().build(sock.into()).await?; + + // Issue an RPC call to start the HTTP server on the specified address + let result = client.start_http_server(address.clone()).await?; + + println!("{}", result); + println!("Press Ctrl+C to stop"); + + // Wait for Ctrl+C to shutdown + signal::ctrl_c().await?; + + // Shutdown the HTTP server + client.stop_http_server().await?; + + println!("HTTP/RPC server stopped"); + + Ok(()) +} diff --git a/src/app/rpc.rs b/src/app/rpc.rs new file mode 100644 index 0000000..199e709 --- /dev/null +++ b/src/app/rpc.rs @@ -0,0 +1,426 @@ +use crate::app::api::{ChildStats, Stats, Status}; +use crate::zinit::config; +use async_trait::async_trait; +use jsonrpsee::core::{RpcResult, SubscriptionResult}; +use jsonrpsee::proc_macros::rpc; +use jsonrpsee::types::{ErrorCode, ErrorObjectOwned}; +use jsonrpsee::PendingSubscriptionSink; +use serde_json::{Map, Value}; +use std::collections::HashMap; +use std::str::FromStr; +use tokio_stream::StreamExt; + +use super::api::Api; + +// Custom error codes for Zinit +const SERVICE_NOT_FOUND: i32 = -32000; +const SERVICE_IS_UP: i32 = -32002; +const SHUTTING_DOWN: i32 = -32006; +const SERVICE_ALREADY_EXISTS: i32 = -32007; +const SERVICE_FILE_ERROR: i32 = -32008; + +// Include the OpenRPC specification +const OPENRPC_SPEC: &str = include_str!("../../openrpc.json"); + +/// RPC methods for discovery. +#[rpc(server, client)] +pub trait ZinitRpcApi { + /// Returns the OpenRPC specification as a string. + #[method(name = "rpc.discover")] + async fn discover(&self) -> RpcResult; +} + +#[async_trait] +impl ZinitRpcApiServer for Api { + async fn discover(&self) -> RpcResult { + Ok(OPENRPC_SPEC.to_string()) + } +} + +/// RPC methods for service management. +#[rpc(server, client, namespace = "service")] +pub trait ZinitServiceApi { + /// List all monitored services and their current state. + /// Returns a map where keys are service names and values are state strings. + #[method(name = "list")] + async fn list(&self) -> RpcResult>; + + /// Get the detailed status of a specific service. + #[method(name = "status")] + async fn status(&self, name: String) -> RpcResult; + + /// Start a specific service. + #[method(name = "start")] + async fn start(&self, name: String) -> RpcResult<()>; + + /// Stop a specific service. + #[method(name = "stop")] + async fn stop(&self, name: String) -> RpcResult<()>; + + /// Load and monitor a new service from its configuration file (e.g., "service_name.yaml"). + #[method(name = "monitor")] + async fn monitor(&self, name: String) -> RpcResult<()>; + + /// Stop monitoring a service and remove it from management. + #[method(name = "forget")] + async fn forget(&self, name: String) -> RpcResult<()>; + + /// Send a signal (e.g., "SIGTERM", "SIGKILL") to a specific service process. + #[method(name = "kill")] + async fn kill(&self, name: String, signal: String) -> RpcResult<()>; + + /// Create a new service configuration file (e.g., "service_name.yaml") + /// with the provided content (JSON map representing YAML structure). + /// Returns a success message string. + #[method(name = "create")] + async fn create(&self, name: String, content: Map) -> RpcResult; + + /// Delete a service configuration file. + /// Returns a success message string. + #[method(name = "delete")] + async fn delete(&self, name: String) -> RpcResult; + + /// Get the content of a service configuration file as a JSON Value. + #[method(name = "get")] + async fn get(&self, name: String) -> RpcResult; + + /// Get memory and CPU usage statistics for a service. + #[method(name = "stats")] + async fn stats(&self, name: String) -> RpcResult; +} + +#[async_trait] +impl ZinitServiceApiServer for Api { + async fn list(&self) -> RpcResult> { + let services = self + .zinit + .list() + .await + .map_err(|_| ErrorObjectOwned::from(ErrorCode::InternalError))?; + + let mut map: HashMap = HashMap::new(); + for service in services { + let state = self + .zinit + .status(&service) + .await + .map_err(|_| ErrorObjectOwned::from(ErrorCode::InternalError))?; + map.insert(service, format!("{:?}", state.state)); + } + Ok(map) + } + + async fn status(&self, name: String) -> RpcResult { + let status = self + .zinit + .status(&name) + .await + .map_err(|_| ErrorObjectOwned::from(ErrorCode::InternalError))?; + + let result = Status { + name: name.clone(), + pid: status.pid.as_raw() as u32, + state: format!("{:?}", status.state), + target: format!("{:?}", status.target), + after: { + let mut after = HashMap::new(); + for service in status.service.after { + let status = match self.zinit.status(&service).await { + Ok(dep) => dep.state, + Err(_) => crate::zinit::State::Unknown, + }; + after.insert(service, format!("{:?}", status)); + } + after + }, + }; + + Ok(result) + } + + async fn start(&self, name: String) -> RpcResult<()> { + self.zinit + .start(name) + .await + .map_err(|_| ErrorObjectOwned::from(ErrorCode::ServerError(SERVICE_IS_UP))) + } + + async fn stop(&self, name: String) -> RpcResult<()> { + self.zinit + .stop(name) + .await + .map_err(|_| ErrorObjectOwned::from(ErrorCode::InternalError)) + } + + async fn monitor(&self, name: String) -> RpcResult<()> { + if let Ok((name_str, service)) = config::load(format!("{}.yaml", name)) + .map_err(|_| ErrorObjectOwned::from(ErrorCode::InternalError)) + { + self.zinit + .monitor(name_str, service) + .await + .map_err(|_| ErrorObjectOwned::from(ErrorCode::InternalError)) + } else { + Err(ErrorObjectOwned::from(ErrorCode::InternalError)) + } + } + + async fn forget(&self, name: String) -> RpcResult<()> { + self.zinit + .forget(name) + .await + .map_err(|_| ErrorObjectOwned::from(ErrorCode::InternalError)) + } + + async fn kill(&self, name: String, signal: String) -> RpcResult<()> { + if let Ok(sig) = nix::sys::signal::Signal::from_str(&signal.to_uppercase()) { + self.zinit + .kill(name, sig) + .await + .map_err(|_e| ErrorObjectOwned::from(ErrorCode::InternalError)) + } else { + Err(ErrorObjectOwned::from(ErrorCode::InternalError)) + } + } + + async fn create(&self, name: String, content: Map) -> RpcResult { + use std::fs; + use std::io::Write; + use std::path::PathBuf; + + // Validate service name (no path traversal, valid characters) + if name.contains('/') || name.contains('\\') || name.contains('.') { + return Err(ErrorObjectOwned::from(ErrorCode::InternalError)); + } + + // Construct the file path + let file_path = PathBuf::from(format!("{}.yaml", name)); + + // Check if the service file already exists + if file_path.exists() { + return Err(ErrorObjectOwned::from(ErrorCode::ServerError( + SERVICE_ALREADY_EXISTS, + ))); + } + + // Convert the JSON content to YAML + let yaml_content = serde_yaml::to_string(&content) + .map_err(|_| ErrorObjectOwned::from(ErrorCode::InternalError))?; + + // Write the YAML content to the file + let mut file = fs::File::create(&file_path) + .map_err(|_| ErrorObjectOwned::from(ErrorCode::ServerError(SERVICE_FILE_ERROR)))?; + + file.write_all(yaml_content.as_bytes()) + .map_err(|_| ErrorObjectOwned::from(ErrorCode::ServerError(SERVICE_FILE_ERROR)))?; + + Ok(format!("Service '{}' created successfully", name)) + } + + async fn delete(&self, name: String) -> RpcResult { + use std::fs; + use std::path::PathBuf; + + // Validate service name (no path traversal, valid characters) + if name.contains('/') || name.contains('\\') || name.contains('.') { + return Err(ErrorObjectOwned::from(ErrorCode::InternalError)); + } + + // Construct the file path + let file_path = PathBuf::from(format!("{}.yaml", name)); + + // Check if the service file exists + if !file_path.exists() { + return Err(ErrorObjectOwned::from(ErrorCode::ServerError( + SERVICE_NOT_FOUND, + ))); + } + + // Delete the file + fs::remove_file(&file_path) + .map_err(|_| ErrorObjectOwned::from(ErrorCode::ServerError(SERVICE_FILE_ERROR)))?; + + Ok(format!("Service '{}' deleted successfully", name)) + } + + async fn get(&self, name: String) -> RpcResult { + use std::fs; + use std::path::PathBuf; + + // Validate service name (no path traversal, valid characters) + if name.contains('/') || name.contains('\\') || name.contains('.') { + return Err(ErrorObjectOwned::from(ErrorCode::InternalError)); + } + + // Construct the file path + let file_path = PathBuf::from(format!("{}.yaml", name)); + + // Check if the service file exists + if !file_path.exists() { + return Err(ErrorObjectOwned::from(ErrorCode::ServerError( + SERVICE_NOT_FOUND, + ))); + } + + // Read the file content + let yaml_content = fs::read_to_string(&file_path) + .map_err(|_| ErrorObjectOwned::from(ErrorCode::ServerError(SERVICE_FILE_ERROR)))?; + + // Parse YAML to JSON + let yaml_value: serde_yaml::Value = serde_yaml::from_str(&yaml_content) + .map_err(|_| ErrorObjectOwned::from(ErrorCode::InternalError))?; + + // Convert YAML value to JSON value + let json_value = serde_json::to_value(yaml_value) + .map_err(|_| ErrorObjectOwned::from(ErrorCode::InternalError))?; + + Ok(json_value) + } + + async fn stats(&self, name: String) -> RpcResult { + let stats = self + .zinit + .stats(&name) + .await + .map_err(|_| ErrorObjectOwned::from(ErrorCode::InternalError))?; + + let result = Stats { + name: name.clone(), + pid: stats.pid as u32, + memory_usage: stats.memory_usage, + cpu_usage: stats.cpu_usage, + children: stats + .children + .into_iter() + .map(|child| ChildStats { + pid: child.pid as u32, + memory_usage: child.memory_usage, + cpu_usage: child.cpu_usage, + }) + .collect(), + }; + + Ok(result) + } +} + +/// RPC methods for system-level operations. +#[rpc(server, client, namespace = "system")] +pub trait ZinitSystemApi { + /// Initiate system shutdown process. + #[method(name = "shutdown")] + async fn shutdown(&self) -> RpcResult<()>; + + /// Initiate system reboot process. + #[method(name = "reboot")] + async fn reboot(&self) -> RpcResult<()>; + + /// Start an HTTP/RPC server at the specified address + #[method(name = "start_http_server")] + async fn start_http_server(&self, address: String) -> RpcResult; + + /// Stop the HTTP/RPC server if running + #[method(name = "stop_http_server")] + async fn stop_http_server(&self) -> RpcResult<()>; +} + +#[async_trait] +impl ZinitSystemApiServer for Api { + async fn shutdown(&self) -> RpcResult<()> { + self.zinit + .shutdown() + .await + .map_err(|_e| ErrorObjectOwned::from(ErrorCode::ServerError(SHUTTING_DOWN))) + } + + async fn reboot(&self) -> RpcResult<()> { + self.zinit + .reboot() + .await + .map_err(|_| ErrorObjectOwned::from(ErrorCode::InternalError)) + } + + async fn start_http_server(&self, address: String) -> RpcResult { + // Call the method from the API implementation + match crate::app::api::Api::start_http_server(self, address).await { + Ok(result) => Ok(result), + Err(_) => Err(ErrorObjectOwned::from(ErrorCode::InternalError)), + } + } + + async fn stop_http_server(&self) -> RpcResult<()> { + // Call the method from the API implementation + match crate::app::api::Api::stop_http_server(self).await { + Ok(_) => Ok(()), + Err(_) => Err(ErrorObjectOwned::from(ErrorCode::InternalError)), + } + } +} + +/// RPC subscription methods for streaming data. +#[rpc(server, client, namespace = "stream")] +pub trait ZinitLoggingApi { + #[method(name = "currentLogs")] + async fn logs(&self, name: Option) -> RpcResult>; + /// Subscribe to log messages generated by zinit and monitored services. + /// An optional filter can be provided to only receive logs containing the filter string. + /// The subscription returns a stream of log lines (String). + #[subscription(name = "subscribeLogs", item = String)] + async fn log_subscribe(&self, filter: Option) -> SubscriptionResult; +} + +#[async_trait] +impl ZinitLoggingApiServer for Api { + async fn logs(&self, name: Option) -> RpcResult> { + let filter = name.map(|n| format!("{n}:")); + Ok( + tokio_stream::wrappers::ReceiverStream::new(self.zinit.logs(true, false).await) + .filter_map(|l| { + if let Some(ref filter) = filter { + if l[4..].starts_with(filter) { + Some(l.to_string()) + } else { + None + } + } else { + Some(l.to_string()) + } + }) + .collect() + .await, + ) + } + + async fn log_subscribe( + &self, + sink: PendingSubscriptionSink, + name: Option, + ) -> SubscriptionResult { + let sink = sink.accept().await?; + let filter = name.map(|n| format!("{n}:")); + let mut stream = + tokio_stream::wrappers::ReceiverStream::new(self.zinit.logs(false, true).await) + .filter_map(|l| { + if let Some(ref filter) = filter { + if l[4..].starts_with(filter) { + Some(l.to_string()) + } else { + None + } + } else { + Some(l.to_string()) + } + }); + while let Some(log) = stream.next().await { + if sink + .send(serde_json::value::to_raw_value(&log)?) + .await + .is_err() + { + break; + } + } + + Ok(()) + } +} diff --git a/src/bin/testapp.rs b/src/bin/testapp.rs new file mode 100644 index 0000000..0f50582 --- /dev/null +++ b/src/bin/testapp.rs @@ -0,0 +1,172 @@ +#[tokio::main] +async fn main() { + println!("hello from testapp"); +} + +// extern crate zinit; + +// use anyhow::Result; +// use serde_json::json; +// use std::env; +// use tokio::time::{sleep, Duration}; + +// use zinit::testapp; + +// #[tokio::main] +// async fn main() -> Result<()> { +// // Define paths for socket and config +// let temp_dir = env::temp_dir(); +// let socket_path = temp_dir +// .join("zinit-test.sock") +// .to_str() +// .unwrap() +// .to_string(); +// let config_dir = temp_dir +// .join("zinit-test-config") +// .to_str() +// .unwrap() +// .to_string(); + +// println!("Starting zinit with socket at: {}", socket_path); +// println!("Using config directory: {}", config_dir); + +// // Start zinit in the background +// testapp::start_zinit(&socket_path, &config_dir).await?; + +// // Wait for zinit to initialize +// sleep(Duration::from_secs(2)).await; + +// // Create a client to communicate with zinit +// let client = Client::new(&socket_path); + +// // Create service configurations +// println!("Creating service configurations..."); + +// // Create a find service +// testapp::create_service_config( +// &config_dir, +// "find-service", +// "find / -name \"*.txt\" -type f", +// ) +// .await?; + +// // Create a sleep service with echo +// testapp::create_service_config( +// &config_dir, +// "sleep-service", +// "sh -c 'echo Starting sleep; sleep 30; echo Finished sleep'", +// ) +// .await?; + +// // Wait for zinit to load the configurations +// sleep(Duration::from_secs(1)).await; + +// // Tell zinit to monitor our services +// println!("Monitoring services..."); +// client.monitor("find-service").await?; +// client.monitor("sleep-service").await?; + +// // List all services +// println!("\nListing all services:"); +// let services = client.list().await?; +// for (name, status) in services { +// println!("Service: {} - Status: {}", name, status); +// } + +// // Start the find service +// println!("\nStarting find-service..."); +// client.start("find-service").await?; + +// // Wait a bit and check status +// sleep(Duration::from_secs(2)).await; +// let status = client.status("find-service").await?; +// println!("find-service status: {:?}", status); + +// // Start the sleep service +// println!("\nStarting sleep-service..."); +// client.start("sleep-service").await?; + +// // Wait a bit and check status +// sleep(Duration::from_secs(2)).await; +// let status = client.status("sleep-service").await?; +// println!("sleep-service status: {:?}", status); + +// // Stop the find service +// println!("\nStopping find-service..."); +// client.stop("find-service").await?; + +// // Wait a bit and check status +// sleep(Duration::from_secs(2)).await; +// let status = client.status("find-service").await?; +// println!("find-service status after stopping: {:?}", status); + +// // Kill the sleep service with SIGTERM +// println!("\nKilling sleep-service with SIGTERM..."); +// client.kill("sleep-service", "SIGTERM").await?; + +// // Wait a bit and check status +// sleep(Duration::from_secs(2)).await; +// let status = client.status("sleep-service").await?; +// println!("sleep-service status after killing: {:?}", status); + +// // Cleanup - forget services +// println!("\nForgetting services..."); +// if status.pid == 0 { +// // Only forget if it's not running +// client.forget("sleep-service").await?; +// } +// client.forget("find-service").await?; + +// // Demonstrate service file operations +// println!("\nDemonstrating service file operations..."); + +// // Create a new service using the API +// println!("Creating a new service via API..."); +// let service_content = json!({ +// "exec": "echo 'Hello from API-created service'", +// "oneshot": true, +// "log": "stdout" +// }) +// .as_object() +// .unwrap() +// .clone(); + +// let result = client +// .create_service("api-service", service_content) +// .await?; +// println!("Create service result: {}", result); + +// // Get the service configuration +// println!("\nGetting service configuration..."); +// let config = client.get_service("api-service").await?; +// println!( +// "Service configuration: {}", +// serde_json::to_string_pretty(&config)? +// ); + +// // Monitor and start the new service +// println!("\nMonitoring and starting the new service..."); +// client.monitor("api-service").await?; +// client.start("api-service").await?; + +// // Wait a bit and check status +// sleep(Duration::from_secs(2)).await; +// let status = client.status("api-service").await?; +// println!("api-service status: {:?}", status); + +// // Delete the service +// println!("\nDeleting the service..."); +// if status.pid == 0 { +// // Only forget if it's not running +// client.forget("api-service").await?; +// let result = client.delete_service("api-service").await?; +// println!("Delete service result: {}", result); +// } + +// // Shutdown zinit +// println!("\nShutting down zinit..."); +// client.shutdown().await?; + +// println!("\nTest completed successfully!"); +// Ok(()) +// } diff --git a/src/lib.rs b/src/lib.rs new file mode 100644 index 0000000..b03e821 --- /dev/null +++ b/src/lib.rs @@ -0,0 +1,11 @@ +extern crate serde; +#[macro_use] +extern crate anyhow; +#[macro_use] +extern crate log; +extern crate tokio; + +pub mod app; +pub mod manager; +pub mod testapp; +pub mod zinit; diff --git a/src/main.rs b/src/main.rs new file mode 100644 index 0000000..62d09aa --- /dev/null +++ b/src/main.rs @@ -0,0 +1,281 @@ +extern crate zinit; + +use anyhow::Result; +use clap::{App, Arg, SubCommand}; +use git_version::git_version; + +use tokio_stream::StreamExt; +use zinit::app; + +const GIT_VERSION: &str = git_version!(args = ["--tags", "--always", "--dirty=-modified"]); + +#[tokio::main] +async fn main() -> Result<()> { + let matches = App::new("zinit") + .author("ThreeFold Tech, https://github.com/threefoldtech") + .version(GIT_VERSION) + .about("A runit replacement") + .arg(Arg::with_name("socket").value_name("SOCKET").short("s").long("socket").default_value("/tmp/zinit.sock").help("path to unix socket")) + .arg(Arg::with_name("debug").short("d").long("debug").help("run in debug mode")) + .subcommand( + SubCommand::with_name("init") + .arg( + Arg::with_name("config") + .value_name("DIR") + .short("c") + .long("config") + .help("service configurations directory"), + ) + .arg( + Arg::with_name("buffer") + .value_name("BUFFER") + .short("b") + .long("buffer") + .help("buffer size (in lines) to keep services logs") + .default_value("2000") + ) + .arg(Arg::with_name("container").long("container").help("run in container mode, shutdown on signal")) + .about("run in init mode, start and maintain configured services"), + ) + .subcommand( + SubCommand::with_name("list") + .about("quick view of current known services and their status"), + ) + .subcommand( + SubCommand::with_name("shutdown") + .about("stop all services and power off"), + ) + .subcommand( + SubCommand::with_name("reboot") + .about("stop all services and reboot"), + ) + .subcommand( + SubCommand::with_name("status") + .arg( + Arg::with_name("service") + .value_name("SERVICE") + .required(true) + .help("service name"), + ) + .about("show detailed service status"), + ) + .subcommand( + SubCommand::with_name("stop") + .arg( + Arg::with_name("service") + .value_name("SERVICE") + .required(true) + .help("service name"), + ) + .about("stop service"), + ) + .subcommand( + SubCommand::with_name("start") + .arg( + Arg::with_name("service") + .value_name("SERVICE") + .required(true) + .help("service name"), + ) + .about("start service. has no effect if the service is already running"), + ) + .subcommand( + SubCommand::with_name("forget") + .arg( + Arg::with_name("service") + .value_name("SERVICE") + .required(true) + .help("service name"), + ) + .about("forget a service. you can only forget a stopped service"), + ) + .subcommand( + SubCommand::with_name("monitor") + .arg( + Arg::with_name("service") + .value_name("SERVICE") + .required(true) + .help("service name"), + ) + .about("start monitoring a service. configuration is loaded from server config directory"), + ) + .subcommand( + SubCommand::with_name("log") + .arg( + Arg::with_name("snapshot") + .short("s") + .long("snapshot") + .required(false) + .help("if set log prints current buffer without following") + ) + .arg( + Arg::with_name("filter") + .value_name("FILTER") + .required(false) + .help("an optional 'exact' service name") + ) + .about("view services logs from zinit ring buffer"), + ) + .subcommand( + SubCommand::with_name("kill") + .arg( + Arg::with_name("service") + .value_name("SERVICE") + .required(true) + .help("service name"), + ) + .arg( + Arg::with_name("signal") + .value_name("SIGNAL") + .required(true) + .default_value("SIGTERM") + .help("signal name (example: SIGTERM)"), + ) + .about("send a signal to a running service."), + ) + .subcommand( + SubCommand::with_name("restart") + .arg( + Arg::with_name("service") + .value_name("SERVICE") + .required(true) + .help("service name"), + ) + .about("restart a service."), + ) + .subcommand( + SubCommand::with_name("stats") + .arg( + Arg::with_name("service") + .value_name("SERVICE") + .required(true) + .help("service name"), + ) + .about("show memory and CPU usage statistics for a service"), + ) + .subcommand( + SubCommand::with_name("proxy") + .arg( + Arg::with_name("address") + .value_name("ADDRESS") + .short("a") + .long("address") + .default_value("127.0.0.1:8080") + .help("address to bind the HTTP/RPC server to"), + ) + .about("start an HTTP/RPC proxy for Zinit API"), + ) + .get_matches(); + + use dirs; // Add this import + + let socket = matches.value_of("socket").unwrap(); + let debug = matches.is_present("debug"); + + let config_path = if let Some(config_arg) = matches.value_of("config") { + config_arg.to_string() + } else { + #[cfg(target_os = "macos")] + { + let home_dir = dirs::home_dir() + .ok_or_else(|| anyhow::anyhow!("Could not determine home directory"))?; + home_dir + .join("hero") + .join("cfg") + .join("zinit") + .to_str() + .ok_or_else(|| anyhow::anyhow!("Invalid path for config directory"))? + .to_string() + } + #[cfg(not(target_os = "macos"))] + { + "/etc/zinit/".to_string() + } + }; + + let result = match matches.subcommand() { + ("init", Some(matches)) => { + let _server = app::init( + matches.value_of("buffer").unwrap().parse().unwrap(), + &config_path, // Use the determined config_path + socket, + matches.is_present("container"), + debug, + ) + .await?; + tokio::signal::ctrl_c().await?; + Ok(()) + } + ("list", _) => app::list(socket).await, + ("shutdown", _) => app::shutdown(socket).await, + ("reboot", _) => app::reboot(socket).await, + // ("log", Some(matches)) => app::log(matches.value_of("filter")), + ("status", Some(matches)) => { + app::status(socket, matches.value_of("service").unwrap().to_string()).await + } + ("stop", Some(matches)) => { + app::stop(socket, matches.value_of("service").unwrap().to_string()).await + } + ("start", Some(matches)) => { + app::start(socket, matches.value_of("service").unwrap().to_string()).await + } + ("forget", Some(matches)) => { + app::forget(socket, matches.value_of("service").unwrap().to_string()).await + } + ("monitor", Some(matches)) => { + app::monitor(socket, matches.value_of("service").unwrap().to_string()).await + } + ("kill", Some(matches)) => { + app::kill( + socket, + matches.value_of("service").unwrap().to_string(), + matches.value_of("signal").unwrap().to_string(), + ) + .await + } + ("log", Some(matches)) => { + let mut stream = app::logs( + socket, + matches.value_of("filter").map(|s| s.to_string()), + !matches.is_present("snapshot"), + ) + .await?; + + loop { + tokio::select! { + item = stream.next() => { + match item { + Some(log_entry) => { + println!("{}", log_entry); + }, + None => break + } + } + _ = tokio::signal::ctrl_c() => { + break + } + } + } + + Ok(()) + } + ("restart", Some(matches)) => { + app::restart(socket, matches.value_of("service").unwrap().to_string()).await + } + ("stats", Some(matches)) => { + app::stats(socket, matches.value_of("service").unwrap().to_string()).await + } + ("proxy", Some(matches)) => { + app::proxy(socket, matches.value_of("address").unwrap().to_string()).await + } + _ => app::list(socket).await, // default command + }; + + match result { + Ok(_) => Ok(()), + Err(e) => { + eprintln!("{:#}", e); + std::process::exit(1); + } + } +} diff --git a/src/manager/buffer.rs b/src/manager/buffer.rs new file mode 100644 index 0000000..2e79e66 --- /dev/null +++ b/src/manager/buffer.rs @@ -0,0 +1,149 @@ +use anyhow::Result; +use std::sync::Arc; +use tokio::sync::broadcast; +use tokio::sync::broadcast::error::RecvError; +use tokio::sync::{mpsc, Mutex}; + +struct Buffer { + inner: Vec, + at: usize, +} + +impl Buffer { + pub fn new(cap: usize) -> Buffer { + Buffer { + inner: Vec::with_capacity(cap), + at: 0, + } + } + + fn len(&self) -> usize { + self.inner.len() + } + + pub fn cap(&self) -> usize { + self.inner.capacity() + } + + pub fn push(&mut self, o: T) { + if self.len() < self.cap() { + self.inner.push(o); + } else { + self.inner[self.at] = o; + } + + self.at = (self.at + 1) % self.cap(); + } +} + +impl<'a, T: 'a> IntoIterator for &'a Buffer { + type IntoIter = BufferIter<'a, T>; + type Item = &'a T; + + fn into_iter(self) -> Self::IntoIter { + let (second, first) = self.inner.split_at(self.at); + + BufferIter { + first, + second, + index: 0, + } + } +} + +pub struct BufferIter<'a, T> { + first: &'a [T], + second: &'a [T], + index: usize, +} + +impl<'a, T> Iterator for BufferIter<'a, T> { + type Item = &'a T; + + fn next(&mut self) -> Option { + let index = self.index; + self.index += 1; + if index < self.first.len() { + Some(&self.first[index]) + } else if index - self.first.len() < self.second.len() { + Some(&self.second[index - self.first.len()]) + } else { + None + } + } +} + +pub type Logs = mpsc::Receiver>; + +#[derive(Clone)] +pub struct Ring { + buffer: Arc>>>, + sender: broadcast::Sender>, +} + +impl Ring { + pub fn new(cap: usize) -> Ring { + let (tx, _) = broadcast::channel(100); + Ring { + buffer: Arc::new(Mutex::new(Buffer::new(cap))), + sender: tx, + } + } + + pub async fn push(&self, line: String) -> Result<()> { + let line = Arc::new(line.clone()); + self.buffer.lock().await.push(Arc::clone(&line)); + self.sender.send(line)?; + Ok(()) + } + + /// stream returns a continues stream that first receive + /// a snapshot of the current buffer. + /// then if follow is true the logs stream will remain + /// open and fed each received line forever until the + /// received closed the channel from its end. + pub async fn stream(&self, existing_logs: bool, follow: bool) -> Logs { + let (tx, stream) = mpsc::channel::>(100); + let mut rx = self.sender.subscribe(); + + let buffer = if existing_logs { + // Get current exisiting logs + self.buffer + .lock() + .await + .into_iter() + .cloned() + .collect::>() + } else { + // Don't care about existing logs + vec![] + }; + + tokio::spawn(async move { + for item in buffer { + let _ = tx.send(Arc::clone(&item)).await; + } + + if !follow { + return; + } + + loop { + let line = match rx.recv().await { + Ok(line) => line, + Err(RecvError::Closed) => break, + Err(RecvError::Lagged(n)) => { + Arc::new(format!("[-] zinit: {} lines dropped", n)) + } + }; + + if tx.send(line).await.is_err() { + // client disconnected. + break; + } + } + }); + + stream + } +} diff --git a/src/manager/mod.rs b/src/manager/mod.rs new file mode 100644 index 0000000..28a506d --- /dev/null +++ b/src/manager/mod.rs @@ -0,0 +1,253 @@ +use std::collections::HashMap; + +use anyhow::{Context, Result}; +use command_group::CommandGroup; +use nix::sys::signal; +use nix::sys::wait::{self, WaitStatus}; +use nix::unistd::Pid; +use std::fs::File as StdFile; +use std::os::unix::io::FromRawFd; +use std::os::unix::io::IntoRawFd; +use std::process::Command; +use std::process::Stdio; +use std::sync::Arc; +use tokio::fs::File; +use tokio::io::AsyncBufReadExt; +use tokio::io::BufReader; +use tokio::signal::unix; +use tokio::sync::oneshot; +use tokio::sync::Mutex; + +mod buffer; +pub use buffer::Logs; + +pub struct Process { + cmd: String, + env: HashMap, + cwd: String, +} +type WaitChannel = oneshot::Receiver; + +pub struct Child { + pub pid: Pid, + ch: WaitChannel, +} + +impl Child { + pub fn new(pid: Pid, ch: WaitChannel) -> Child { + Child { pid, ch } + } + + pub async fn wait(self) -> Result { + Ok(self.ch.await?) + } +} + +type Handler = oneshot::Sender; + +impl Process { + pub fn new>(cmd: S, cwd: S, env: Option>) -> Process { + let env = env.unwrap_or_default(); + + Process { + env, + cmd: cmd.into(), + cwd: cwd.into(), + } + } +} + +#[derive(Clone)] +pub enum Log { + None, + Stdout, + Ring(String), +} + +#[derive(Clone)] +pub struct ProcessManager { + table: Arc>>, + ring: buffer::Ring, + env: Environ, +} + +impl ProcessManager { + pub fn new(cap: usize) -> ProcessManager { + ProcessManager { + table: Arc::new(Mutex::new(HashMap::new())), + ring: buffer::Ring::new(cap), + env: Environ::new(), + } + } + + fn wait_process() -> Vec { + let mut statuses: Vec = Vec::new(); + loop { + let status = match wait::waitpid(Option::None, Some(wait::WaitPidFlag::WNOHANG)) { + Ok(status) => status, + Err(_) => { + return statuses; + } + }; + match status { + WaitStatus::StillAlive => break, + _ => statuses.push(status), + } + } + statuses + } + + pub fn start(&self) { + let table = Arc::clone(&self.table); + let mut signals = match unix::signal(unix::SignalKind::child()) { + Ok(s) => s, + Err(err) => { + panic!("failed to bind to signals: {}", err); + } + }; + + tokio::spawn(async move { + loop { + signals.recv().await; + let mut table = table.lock().await; + for exited in Self::wait_process() { + if let Some(pid) = exited.pid() { + if let Some(sender) = table.remove(&pid) { + if sender.send(exited).is_err() { + debug!("failed to send exit state to process: {}", pid); + } + } + } + } + } + }); + } + + fn sink(&self, file: File, prefix: String) { + let ring = self.ring.clone(); + let reader = BufReader::new(file); + + tokio::spawn(async move { + let mut lines = reader.lines(); + while let Ok(line) = lines.next_line().await { + let _ = match line { + Some(line) => ring.push(format!("{}: {}", prefix, line)).await, + None => break, + }; + } + }); + } + + pub async fn stream(&self, existing_logs: bool, follow: bool) -> Logs { + self.ring.stream(existing_logs, follow).await + } + + pub fn signal(&self, pid: Pid, sig: signal::Signal) -> Result<()> { + Ok(signal::killpg(pid, sig)?) + } + + pub async fn run(&self, cmd: Process, log: Log) -> Result { + let args = shlex::split(&cmd.cmd).context("failed to parse command")?; + if args.is_empty() { + bail!("invalid command"); + } + + let mut child = Command::new(&args[0]); + + let child = if !cmd.cwd.is_empty() { + child.current_dir(&cmd.cwd) + } else { + child.current_dir("/") + }; + + let child = child.args(&args[1..]).envs(&self.env.0).envs(cmd.env); + + let child = match log { + Log::None => child.stdout(Stdio::null()).stderr(Stdio::null()), + Log::Ring(_) => child.stdout(Stdio::piped()).stderr(Stdio::piped()), + _ => child, // default to inherit + }; + + let mut table = self.table.lock().await; + + let mut child = child + .group_spawn() + .context("failed to spawn command")? + .into_inner(); + + if let Log::Ring(prefix) = log { + let _ = self + .ring + .push(format!("[-] {}: ------------ [start] ------------", prefix)) + .await; + + if let Some(out) = child.stdout.take() { + let out = File::from_std(unsafe { StdFile::from_raw_fd(out.into_raw_fd()) }); + self.sink(out, format!("[+] {}", prefix)) + } + + if let Some(out) = child.stderr.take() { + let out = File::from_std(unsafe { StdFile::from_raw_fd(out.into_raw_fd()) }); + self.sink(out, format!("[-] {}", prefix)) + } + } + + let (tx, rx) = oneshot::channel(); + + let id = child.id(); + + let pid = Pid::from_raw(id as i32); + table.insert(pid, tx); + + Ok(Child::new(pid, rx)) + } +} + +#[derive(Clone)] +struct Environ(HashMap); + +impl Environ { + fn new() -> Environ { + let env = match Environ::parse("/etc/environment") { + Ok(r) => r, + Err(err) => { + error!("failed to load /etc/environment file: {}", err); + HashMap::new() + } + }; + + Environ(env) + } + + fn parse

(p: P) -> Result, std::io::Error> + where + P: AsRef, + { + let mut m = HashMap::new(); + let txt = match std::fs::read_to_string(p) { + Ok(txt) => txt, + Err(err) if err.kind() == std::io::ErrorKind::NotFound => { + info!("skipping /etc/environment file because it does not exist"); + "".into() + } + Err(err) => return Err(err), + }; + + for line in txt.lines() { + let line = line.trim(); + if line.starts_with('#') { + continue; + } + let parts: Vec<&str> = line.splitn(2, '=').collect(); + let key = String::from(parts[0]); + let value = match parts.len() { + 2 => String::from(parts[1]), + _ => String::default(), + }; + //m.into_iter() + m.insert(key, value); + } + + Ok(m) + } +} diff --git a/src/testapp/main.rs b/src/testapp/main.rs new file mode 100644 index 0000000..4a0fda5 --- /dev/null +++ b/src/testapp/main.rs @@ -0,0 +1,264 @@ +use anyhow::{Context, Result}; +use std::path::Path; +use tokio::time::{sleep, Duration}; +use std::env; +use tokio::process::Command; +use tokio::fs; +use std::process::Stdio; +use serde::{Deserialize, Serialize}; +use serde_json; +use tokio::net::UnixStream; +use tokio::io::{AsyncBufReadExt, AsyncReadExt, AsyncWriteExt, BufStream}; +use std::collections::HashMap; + +#[derive(Clone, Debug, Deserialize, Serialize)] +#[serde(rename_all = "lowercase")] +struct Response { + pub state: State, + pub body: serde_json::Value, +} + +#[derive(Clone, Debug, Deserialize, Serialize)] +#[serde(rename_all = "lowercase")] +enum State { + Ok, + Error, +} + +#[derive(Clone, Debug, Deserialize, Serialize)] +#[serde(rename_all = "lowercase")] +struct Status { + pub name: String, + pub pid: u32, + pub state: String, + pub target: String, + pub after: HashMap, +} + +struct Client { + socket: String, +} + +impl Client { + pub fn new(socket: &str) -> Client { + Client { + socket: socket.to_string(), + } + } + + async fn connect(&self) -> Result { + UnixStream::connect(&self.socket).await.with_context(|| { + format!( + "failed to connect to '{}'. is zinit listening on that socket?", + self.socket + ) + }) + } + + async fn command(&self, c: &str) -> Result { + let mut con = BufStream::new(self.connect().await?); + + let _ = con.write(c.as_bytes()).await?; + let _ = con.write(b"\n").await?; + con.flush().await?; + + let mut data = String::new(); + con.read_to_string(&mut data).await?; + + let response: Response = serde_json::from_str(&data)?; + + match response.state { + State::Ok => Ok(response.body), + State::Error => { + let err: String = serde_json::from_value(response.body)?; + anyhow::bail!(err) + } + } + } + + pub async fn list(&self) -> Result> { + let response = self.command("list").await?; + Ok(serde_json::from_value(response)?) + } + + pub async fn status>(&self, name: S) -> Result { + let response = self.command(&format!("status {}", name.as_ref())).await?; + Ok(serde_json::from_value(response)?) + } + + pub async fn start>(&self, name: S) -> Result<()> { + self.command(&format!("start {}", name.as_ref())).await?; + Ok(()) + } + + pub async fn stop>(&self, name: S) -> Result<()> { + self.command(&format!("stop {}", name.as_ref())).await?; + Ok(()) + } + + pub async fn forget>(&self, name: S) -> Result<()> { + self.command(&format!("forget {}", name.as_ref())).await?; + Ok(()) + } + + pub async fn monitor>(&self, name: S) -> Result<()> { + self.command(&format!("monitor {}", name.as_ref())).await?; + Ok(()) + } + + pub async fn kill>(&self, name: S, sig: S) -> Result<()> { + self.command(&format!("kill {} {}", name.as_ref(), sig.as_ref())) + .await?; + Ok(()) + } + + pub async fn shutdown(&self) -> Result<()> { + self.command("shutdown").await?; + Ok(()) + } +} + +async fn start_zinit(socket_path: &str, config_dir: &str) -> Result<()> { + // Create a temporary config directory if it doesn't exist + let config_path = Path::new(config_dir); + if !config_path.exists() { + fs::create_dir_all(config_path).await?; + } + + // Start zinit in the background + let mut cmd = Command::new("zinit"); + cmd.arg("--socket") + .arg(socket_path) + .arg("init") + .arg("--config") + .arg(config_dir) + .stdout(Stdio::piped()) + .stderr(Stdio::piped()); + + let child = cmd.spawn()?; + + // Give zinit some time to start up + sleep(Duration::from_secs(1)).await; + + println!("Zinit started with PID: {:?}", child.id()); + + Ok(()) +} + +async fn create_service_config(config_dir: &str, name: &str, command: &str) -> Result<()> { + let config_path = format!("{}/{}.yaml", config_dir, name); + let config_content = format!( + r#"exec: {} +oneshot: false +shutdown_timeout: 10 +after: [] +signal: + stop: sigterm +log: ring +env: {{}} +dir: / +"#, + command + ); + + fs::write(config_path, config_content).await?; + Ok(()) +} + +#[tokio::main] +async fn main() -> Result<()> { + // Define paths for socket and config + let temp_dir = env::temp_dir(); + let socket_path = temp_dir.join("zinit-test.sock").to_str().unwrap().to_string(); + let config_dir = temp_dir.join("zinit-test-config").to_str().unwrap().to_string(); + + println!("Starting zinit with socket at: {}", socket_path); + println!("Using config directory: {}", config_dir); + + // Start zinit in the background + start_zinit(&socket_path, &config_dir).await?; + + // Wait for zinit to initialize + sleep(Duration::from_secs(2)).await; + + // Create a client to communicate with zinit + let client = Client::new(&socket_path); + + // Create service configurations + println!("Creating service configurations..."); + + // Create a find service + create_service_config(&config_dir, "find-service", "find / -name \"*.txt\" -type f").await?; + + // Create a sleep service with echo + create_service_config( + &config_dir, + "sleep-service", + "sh -c 'echo Starting sleep; sleep 30; echo Finished sleep'" + ).await?; + + // Wait for zinit to load the configurations + sleep(Duration::from_secs(1)).await; + + // Tell zinit to monitor our services + println!("Monitoring services..."); + client.monitor("find-service").await?; + client.monitor("sleep-service").await?; + + // List all services + println!("\nListing all services:"); + let services = client.list().await?; + for (name, status) in services { + println!("Service: {} - Status: {}", name, status); + } + + // Start the find service + println!("\nStarting find-service..."); + client.start("find-service").await?; + + // Wait a bit and check status + sleep(Duration::from_secs(2)).await; + let status = client.status("find-service").await?; + println!("find-service status: {:?}", status); + + // Start the sleep service + println!("\nStarting sleep-service..."); + client.start("sleep-service").await?; + + // Wait a bit and check status + sleep(Duration::from_secs(2)).await; + let status = client.status("sleep-service").await?; + println!("sleep-service status: {:?}", status); + + // Stop the find service + println!("\nStopping find-service..."); + client.stop("find-service").await?; + + // Wait a bit and check status + sleep(Duration::from_secs(2)).await; + let status = client.status("find-service").await?; + println!("find-service status after stopping: {:?}", status); + + // Kill the sleep service with SIGTERM + println!("\nKilling sleep-service with SIGTERM..."); + client.kill("sleep-service", "SIGTERM").await?; + + // Wait a bit and check status + sleep(Duration::from_secs(2)).await; + let status = client.status("sleep-service").await?; + println!("sleep-service status after killing: {:?}", status); + + // Cleanup - forget services + println!("\nForgetting services..."); + if status.pid == 0 { // Only forget if it's not running + client.forget("sleep-service").await?; + } + client.forget("find-service").await?; + + // Shutdown zinit + println!("\nShutting down zinit..."); + client.shutdown().await?; + + println!("\nTest completed successfully!"); + Ok(()) +} \ No newline at end of file diff --git a/src/testapp/mod.rs b/src/testapp/mod.rs new file mode 100644 index 0000000..32bf1fd --- /dev/null +++ b/src/testapp/mod.rs @@ -0,0 +1,57 @@ +use anyhow::Result; +use std::env; +use std::path::Path; +use std::process::Stdio; +use tokio::process::Command; +use tokio::time::{sleep, Duration}; + +pub async fn start_zinit(socket_path: &str, config_dir: &str) -> Result<()> { + // Create a temporary config directory if it doesn't exist + let config_path = Path::new(config_dir); + if !config_path.exists() { + tokio::fs::create_dir_all(config_path).await?; + } + + // Get the path to the zinit binary (use the one we just built) + let zinit_path = env::current_dir()?.join("target/debug/zinit"); + println!("Using zinit binary at: {}", zinit_path.display()); + + // Start zinit in the background + let mut cmd = Command::new(zinit_path); + cmd.arg("--socket") + .arg(socket_path) + .arg("init") + .arg("--config") + .arg(config_dir) + .stdout(Stdio::piped()) + .stderr(Stdio::piped()); + + let child = cmd.spawn()?; + + // Give zinit some time to start up + sleep(Duration::from_secs(1)).await; + + println!("Zinit started with PID: {:?}", child.id()); + + Ok(()) +} + +pub async fn create_service_config(config_dir: &str, name: &str, command: &str) -> Result<()> { + let config_path = format!("{}/{}.yaml", config_dir, name); + let config_content = format!( + r#"exec: {} +oneshot: false +shutdown_timeout: 10 +after: [] +signal: + stop: sigterm +log: ring +env: {{}} +dir: / +"#, + command + ); + + tokio::fs::write(config_path, config_content).await?; + Ok(()) +} diff --git a/src/zinit/config.rs b/src/zinit/config.rs new file mode 100644 index 0000000..f706c47 --- /dev/null +++ b/src/zinit/config.rs @@ -0,0 +1,119 @@ +use anyhow::Result; +use serde::{Deserialize, Serialize}; +use serde_yaml as yaml; +use std::collections::HashMap; +use std::ffi::OsStr; +use std::fs::{self, File}; +use std::path::Path; +pub type Services = HashMap; + +pub const DEFAULT_SHUTDOWN_TIMEOUT: u64 = 10; // in seconds + +#[derive(Clone, Debug, Serialize, Deserialize)] +#[serde(default)] +pub struct Signal { + pub stop: String, +} + +impl Default for Signal { + fn default() -> Self { + Signal { + stop: String::from("sigterm"), + } + } +} + +#[derive(Default, Clone, Debug, Deserialize)] +#[serde(rename_all = "lowercase")] +pub enum Log { + None, + #[default] + Ring, + Stdout, +} + +fn default_shutdown_timeout_fn() -> u64 { + DEFAULT_SHUTDOWN_TIMEOUT +} +#[derive(Clone, Debug, Default, Deserialize)] +#[serde(default)] +pub struct Service { + /// command to run + pub exec: String, + /// test command (optional) + #[serde(default)] + pub test: String, + #[serde(rename = "oneshot")] + pub one_shot: bool, + #[serde(default = "default_shutdown_timeout_fn")] + pub shutdown_timeout: u64, + pub after: Vec, + pub signal: Signal, + pub log: Log, + pub env: HashMap, + pub dir: String, +} + +impl Service { + pub fn validate(&self) -> Result<()> { + use nix::sys::signal::Signal; + use std::str::FromStr; + if self.exec.is_empty() { + bail!("missing exec directive"); + } + + Signal::from_str(&self.signal.stop.to_uppercase())?; + + Ok(()) + } +} +/// load loads a single file +pub fn load>(t: T) -> Result<(String, Service)> { + let p = t.as_ref(); + //todo: can't find a way to shorten this down. + let name = match p.file_stem() { + Some(name) => match name.to_str() { + Some(name) => name, + None => bail!("invalid file name: {}", p.to_str().unwrap()), + }, + None => bail!("invalid file name: {}", p.to_str().unwrap()), + }; + + let file = File::open(p)?; + let service: Service = yaml::from_reader(&file)?; + service.validate()?; + Ok((String::from(name), service)) +} + +/// walks over a directory and load all configuration files. +/// the callback is called with any error that is encountered on loading +/// a file, the callback can decide to either ignore the file, or stop +/// the directory walking +pub fn load_dir>(p: T) -> Result { + let mut services: Services = HashMap::new(); + + for entry in fs::read_dir(p)? { + let entry = entry?; + if !entry.file_type()?.is_file() { + continue; + } + + let fp = entry.path(); + + if !matches!(fp.extension(), Some(ext) if ext == OsStr::new("yaml")) { + continue; + } + + let (name, service) = match load(&fp) { + Ok(content) => content, + Err(err) => { + error!("failed to load config file {:?}: {}", fp, err); + continue; + } + }; + + services.insert(name, service); + } + + Ok(services) +} diff --git a/src/zinit/errors.rs b/src/zinit/errors.rs new file mode 100644 index 0000000..72c60ee --- /dev/null +++ b/src/zinit/errors.rs @@ -0,0 +1,80 @@ +use thiserror::Error; + +/// Errors that can occur in the zinit module +#[derive(Error, Debug)] +pub enum ZInitError { + /// Service name is unknown + #[error("service name {name:?} unknown")] + UnknownService { name: String }, + + /// Service is already being monitored + #[error("service {name:?} already monitored")] + ServiceAlreadyMonitored { name: String }, + + /// Service is up and running + #[error("service {name:?} is up")] + ServiceIsUp { name: String }, + + /// Service is down and not running + #[error("service {name:?} is down")] + ServiceIsDown { name: String }, + + /// Zinit is shutting down + #[error("zinit is shutting down")] + ShuttingDown, + + /// Invalid state transition + #[error("service state transition error: {message}")] + InvalidStateTransition { message: String }, + + /// Dependency error + #[error("dependency error: {message}")] + DependencyError { message: String }, + + /// Process error + #[error("process error: {message}")] + ProcessError { message: String }, +} + +impl ZInitError { + /// Create a new UnknownService error + pub fn unknown_service>(name: S) -> Self { + ZInitError::UnknownService { name: name.into() } + } + + /// Create a new ServiceAlreadyMonitored error + pub fn service_already_monitored>(name: S) -> Self { + ZInitError::ServiceAlreadyMonitored { name: name.into() } + } + + /// Create a new ServiceIsUp error + pub fn service_is_up>(name: S) -> Self { + ZInitError::ServiceIsUp { name: name.into() } + } + + /// Create a new ServiceIsDown error + pub fn service_is_down>(name: S) -> Self { + ZInitError::ServiceIsDown { name: name.into() } + } + + /// Create a new InvalidStateTransition error + pub fn invalid_state_transition>(message: S) -> Self { + ZInitError::InvalidStateTransition { + message: message.into(), + } + } + + /// Create a new DependencyError error + pub fn dependency_error>(message: S) -> Self { + ZInitError::DependencyError { + message: message.into(), + } + } + + /// Create a new ProcessError error + pub fn process_error>(message: S) -> Self { + ZInitError::ProcessError { + message: message.into(), + } + } +} diff --git a/src/zinit/lifecycle.rs b/src/zinit/lifecycle.rs new file mode 100644 index 0000000..adc99ea --- /dev/null +++ b/src/zinit/lifecycle.rs @@ -0,0 +1,970 @@ +use crate::manager::{Log, Logs, Process, ProcessManager}; +use crate::zinit::config; +use crate::zinit::errors::ZInitError; +#[cfg(target_os = "linux")] +use crate::zinit::ord::{service_dependency_order, ProcessDAG, DUMMY_ROOT}; +use crate::zinit::service::ZInitService; +use crate::zinit::state::{State, Target}; +#[cfg(target_os = "linux")] +use crate::zinit::types::Watcher; +use crate::zinit::types::{ProcessStats, ServiceStats, ServiceTable}; +use std::collections::HashMap; +use sysinfo::{self, PidExt, ProcessExt, System, SystemExt}; + +// Define a local extension trait for WaitStatus +trait WaitStatusExt { + fn success(&self) -> bool; +} + +impl WaitStatusExt for WaitStatus { + fn success(&self) -> bool { + matches!(self, WaitStatus::Exited(_, 0)) + } +} +use anyhow::Result; +#[cfg(target_os = "linux")] +use nix::sys::reboot::RebootMode; +use nix::sys::signal; +use nix::sys::wait::WaitStatus; +use nix::unistd::Pid; +use std::str::FromStr; +use std::sync::Arc; +#[cfg(target_os = "linux")] +use tokio::sync::mpsc; +use tokio::sync::{Notify, RwLock}; +use tokio::time::sleep; +#[cfg(target_os = "linux")] +use tokio::time::timeout; +#[cfg(target_os = "linux")] +use tokio_stream::StreamExt; + +/// Manages the lifecycle of services +#[derive(Clone)] +pub struct LifecycleManager { + /// Process manager for spawning and managing processes + pm: ProcessManager, + + /// Table of services + services: Arc>, + + /// Notification for service state changes + notify: Arc, + + /// Whether the system is shutting down + shutdown: Arc>, + + /// Whether running in container mode + container: bool, +} + +impl LifecycleManager { + /// Create a new lifecycle manager + pub fn new( + pm: ProcessManager, + services: Arc>, + notify: Arc, + shutdown: Arc>, + container: bool, + ) -> Self { + Self { + pm, + services, + notify, + shutdown, + container, + } + } + + /// Get a reference to the process manager + pub fn process_manager(&self) -> &ProcessManager { + &self.pm + } + + /// Check if running in container mode + pub fn is_container_mode(&self) -> bool { + self.container + } + + /// Get logs from the process manager + pub async fn logs(&self, existing_logs: bool, follow: bool) -> Logs { + self.pm.stream(existing_logs, follow).await + } + + /// Monitor a service + pub async fn monitor>(&self, name: S, service: config::Service) -> Result<()> { + if *self.shutdown.read().await { + return Err(ZInitError::ShuttingDown.into()); + } + + let name = name.into(); + let mut services = self.services.write().await; + + if services.contains_key(&name) { + return Err(ZInitError::service_already_monitored(name).into()); + } + + let service = Arc::new(RwLock::new(ZInitService::new(service, State::Unknown))); + services.insert(name.clone(), Arc::clone(&service)); + + let lifecycle = self.clone_lifecycle(); + debug!("service '{}' monitored", name); + tokio::spawn(lifecycle.watch_service(name, service)); + + Ok(()) + } + + /// Get the status of a service + pub async fn status>( + &self, + name: S, + ) -> Result { + let table = self.services.read().await; + let service = table + .get(name.as_ref()) + .ok_or_else(|| ZInitError::unknown_service(name.as_ref()))?; + + let service = service.read().await.status(); + Ok(service) + } + + /// Start a service + pub async fn start>(&self, name: S) -> Result<()> { + if *self.shutdown.read().await { + return Err(ZInitError::ShuttingDown.into()); + } + + self.set_service_state(name.as_ref(), None, Some(Target::Up)) + .await; + + let table = self.services.read().await; + let service = table + .get(name.as_ref()) + .ok_or_else(|| ZInitError::unknown_service(name.as_ref()))?; + + let lifecycle = self.clone_lifecycle(); + tokio::spawn(lifecycle.watch_service(name.as_ref().into(), Arc::clone(service))); + + Ok(()) + } + + /// Stop a service + pub async fn stop>(&self, name: S) -> Result<()> { + // Get service information + let table = self.services.read().await; + let service = table + .get(name.as_ref()) + .ok_or_else(|| ZInitError::unknown_service(name.as_ref()))?; + + let mut service = service.write().await; + service.set_target(Target::Down); + + // Get the main process PID + let pid = service.pid; + if pid.as_raw() == 0 { + return Ok(()); + } + + // Get the signal to use + let signal = signal::Signal::from_str(&service.service.signal.stop.to_uppercase()) + .map_err(|err| anyhow::anyhow!("unknown stop signal: {}", err))?; + + // Release the lock before potentially long-running operations + drop(service); + drop(table); + + // Get all child processes using our stats functionality + let children = self.get_child_process_stats(pid.as_raw()).await?; + + // First try to stop the process group + let _ = self.pm.signal(pid, signal); + + // Wait a short time for processes to terminate gracefully + sleep(std::time::Duration::from_millis(500)).await; + + // Check if processes are still running and use SIGKILL if needed + self.ensure_processes_terminated(pid.as_raw(), &children) + .await?; + + Ok(()) + } + + /// Ensure that a process and its children are terminated + async fn ensure_processes_terminated( + &self, + parent_pid: i32, + children: &[ProcessStats], + ) -> Result<()> { + // Check if parent is still running + let parent_running = self.is_process_running(parent_pid).await?; + + // If parent is still running, send SIGKILL + if parent_running { + debug!( + "Process {} still running after SIGTERM, sending SIGKILL", + parent_pid + ); + let _ = self + .pm + .signal(Pid::from_raw(parent_pid), signal::Signal::SIGKILL); + } + + // Check and kill any remaining child processes + for child in children { + if self.is_process_running(child.pid).await? { + debug!("Child process {} still running, sending SIGKILL", child.pid); + let _ = signal::kill(Pid::from_raw(child.pid), signal::Signal::SIGKILL); + } + } + + // Verify all processes are gone + let mut retries = 5; + while retries > 0 { + let mut all_terminated = true; + + // Check parent + if self.is_process_running(parent_pid).await? { + all_terminated = false; + } + + // Check children + for child in children { + if self.is_process_running(child.pid).await? { + all_terminated = false; + break; + } + } + + if all_terminated { + return Ok(()); + } + + // Wait before retrying + sleep(std::time::Duration::from_millis(100)).await; + retries -= 1; + } + + // If we get here, some processes might still be running + warn!("Some processes may still be running after shutdown attempts"); + Ok(()) + } + + /// Check if a process is running + async fn is_process_running(&self, pid: i32) -> Result { + // Use sysinfo to check if process exists + let mut system = System::new(); + let sys_pid = sysinfo::Pid::from(pid as usize); + system.refresh_process(sys_pid); + + Ok(system.process(sys_pid).is_some()) + } + + /// Forget a service + pub async fn forget>(&self, name: S) -> Result<()> { + let mut table = self.services.write().await; + let service = table + .get(name.as_ref()) + .ok_or_else(|| ZInitError::unknown_service(name.as_ref()))?; + + let service = service.read().await; + if service.target == Target::Up || service.pid != Pid::from_raw(0) { + return Err(ZInitError::service_is_up(name.as_ref()).into()); + } + + drop(service); + table.remove(name.as_ref()); + + Ok(()) + } + + /// Send a signal to a service + pub async fn kill>(&self, name: S, signal: signal::Signal) -> Result<()> { + let table = self.services.read().await; + let service = table + .get(name.as_ref()) + .ok_or_else(|| ZInitError::unknown_service(name.as_ref()))?; + + let service = service.read().await; + if service.pid == Pid::from_raw(0) { + return Err(ZInitError::service_is_down(name.as_ref()).into()); + } + + self.pm.signal(service.pid, signal) + } + + /// List all services + pub async fn list(&self) -> Result> { + let table = self.services.read().await; + Ok(table.keys().map(|k| k.into()).collect()) + } + + /// Get stats for a service (memory and CPU usage) + pub async fn stats>(&self, name: S) -> Result { + let table = self.services.read().await; + let service = table + .get(name.as_ref()) + .ok_or_else(|| ZInitError::unknown_service(name.as_ref()))?; + + let service = service.read().await; + if service.pid.as_raw() == 0 { + return Err(ZInitError::service_is_down(name.as_ref()).into()); + } + + // Get stats for the main process + let pid = service.pid.as_raw(); + let (memory_usage, cpu_usage) = self.get_process_stats(pid).await?; + + // Get stats for child processes + let children = self.get_child_process_stats(pid).await?; + + Ok(ServiceStats { + memory_usage, + cpu_usage, + pid, + children, + }) + } + + /// Get memory and CPU usage for a process + async fn get_process_stats(&self, pid: i32) -> Result<(u64, f32)> { + // Create a new System instance with all information + let mut system = System::new_all(); + + // Convert i32 pid to sysinfo::Pid + let sys_pid = sysinfo::Pid::from(pid as usize); + + // Make sure we're refreshing CPU information + system.refresh_cpu(); + system.refresh_processes(); + + // First refresh to get initial CPU values + system.refresh_all(); + + // Wait longer for CPU measurement (500ms instead of 100ms) + sleep(std::time::Duration::from_millis(500)).await; + + // Refresh again to get updated CPU values + system.refresh_cpu(); + system.refresh_processes(); + system.refresh_all(); + + // Get the process + if let Some(process) = system.process(sys_pid) { + // Get memory in bytes + let memory_usage = process.memory(); + + // Get CPU usage as percentage + let cpu_usage = process.cpu_usage(); + + Ok((memory_usage, cpu_usage)) + } else { + // Process not found + Ok((0, 0.0)) + } + } + + /// Get stats for child processes + async fn get_child_process_stats(&self, parent_pid: i32) -> Result> { + // Create a new System instance with all processes information + let mut system = System::new_all(); + + // Make sure we're refreshing CPU information + system.refresh_cpu(); + system.refresh_processes(); + system.refresh_all(); + + // Convert i32 pid to sysinfo::Pid + let sys_pid = sysinfo::Pid::from(parent_pid as usize); + + // Wait longer for CPU measurement (500ms instead of 100ms) + sleep(std::time::Duration::from_millis(500)).await; + + // Refresh all system information to get updated CPU values + system.refresh_cpu(); + system.refresh_processes(); + system.refresh_all(); + + let mut children = Vec::new(); + + // Recursively collect all descendant PIDs + let mut descendant_pids = Vec::new(); + self.collect_descendants(sys_pid, &system.processes(), &mut descendant_pids); + + // Get stats for each child process + for &child_pid in &descendant_pids { + if let Some(process) = system.process(child_pid) { + children.push(ProcessStats { + pid: child_pid.as_u32() as i32, + memory_usage: process.memory(), + cpu_usage: process.cpu_usage(), + }); + } + } + + Ok(children) + } + + /// Recursively collect all descendant PIDs of a process + fn collect_descendants( + &self, + pid: sysinfo::Pid, + procs: &HashMap, + out: &mut Vec, + ) { + for (&child_pid, proc) in procs.iter() { + if proc.parent() == Some(pid) { + out.push(child_pid); + self.collect_descendants(child_pid, procs, out); + } + } + } + + /// Verify that all processes are terminated + async fn verify_all_processes_terminated(&self) -> Result<()> { + // Get all services + let table = self.services.read().await; + + // Check each service + for (name, service) in table.iter() { + let service = service.read().await; + let pid = service.pid.as_raw(); + + // Skip services with no PID + if pid == 0 { + continue; + } + + // Check if the main process is still running + if self.is_process_running(pid).await? { + warn!( + "Service {} (PID {}) is still running after shutdown", + name, pid + ); + + // Try to kill it with SIGKILL + let _ = signal::kill(Pid::from_raw(pid), signal::Signal::SIGKILL); + } + + // Check for child processes + if let Ok(children) = self.get_child_process_stats(pid).await { + for child in children { + if self.is_process_running(child.pid).await? { + warn!( + "Child process {} of service {} is still running after shutdown", + child.pid, name + ); + + // Try to kill it with SIGKILL + let _ = signal::kill(Pid::from_raw(child.pid), signal::Signal::SIGKILL); + } + } + } + } + + Ok(()) + } + + /// Shutdown the system + pub async fn shutdown(&self) -> Result<()> { + info!("shutting down"); + + // Set the shutdown flag + *self.shutdown.write().await = true; + + #[cfg(target_os = "linux")] + { + // Power off using our enhanced method + let result = self.power(RebootMode::RB_POWER_OFF).await; + + // Final verification before exit + self.verify_all_processes_terminated().await?; + + return result; + } + + #[cfg(not(target_os = "linux"))] + { + // Stop all services + let services = self.list().await?; + for service in services { + let _ = self.stop(&service).await; + } + + // Verify all processes are terminated + self.verify_all_processes_terminated().await?; + + if self.container { + std::process::exit(0); + } else { + info!("System shutdown not supported on this platform"); + std::process::exit(0); + } + } + } + + /// Reboot the system + pub async fn reboot(&self) -> Result<()> { + info!("rebooting"); + + // Set the shutdown flag + *self.shutdown.write().await = true; + + #[cfg(target_os = "linux")] + { + // Reboot using our enhanced method + let result = self.power(RebootMode::RB_AUTOBOOT).await; + + // Final verification before exit + self.verify_all_processes_terminated().await?; + + return result; + } + + #[cfg(not(target_os = "linux"))] + { + // Stop all services + let services = self.list().await?; + for service in services { + let _ = self.stop(&service).await; + } + + // Verify all processes are terminated + self.verify_all_processes_terminated().await?; + + if self.container { + std::process::exit(0); + } else { + info!("System reboot not supported on this platform"); + std::process::exit(0); + } + } + } + + /// Power off or reboot the system + #[cfg(target_os = "linux")] + async fn power(&self, mode: RebootMode) -> Result<()> { + *self.shutdown.write().await = true; + + let mut state_channels: HashMap> = HashMap::new(); + let mut shutdown_timeouts: HashMap = HashMap::new(); + + let table = self.services.read().await; + for (name, service) in table.iter() { + let service = service.read().await; + if service.is_active() { + info!("service '{}' is scheduled for a shutdown", name); + state_channels.insert(name.into(), service.state_watcher()); + shutdown_timeouts.insert(name.into(), service.service.shutdown_timeout); + } + } + + drop(table); + let dag = service_dependency_order(self.services.clone()).await; + self.kill_process_tree(dag, state_channels, shutdown_timeouts) + .await?; + + // On Linux, we can use sync and reboot + nix::unistd::sync(); + if self.container { + std::process::exit(0); + } else { + nix::sys::reboot::reboot(mode)?; + } + + Ok(()) + } + + /// Kill processes in dependency order + #[cfg(target_os = "linux")] + async fn kill_process_tree( + &self, + mut dag: ProcessDAG, + mut state_channels: HashMap>, + mut shutdown_timeouts: HashMap, + ) -> Result<()> { + let (tx, mut rx) = mpsc::unbounded_channel(); + tx.send(DUMMY_ROOT.into())?; + + let mut count = dag.count; + while let Some(name) = rx.recv().await { + debug!( + "{} has been killed (or was inactive) adding its children", + name + ); + + for child in dag.adj.get(&name).unwrap_or(&Vec::new()) { + let child_indegree: &mut u32 = dag.indegree.entry(child.clone()).or_default(); + *child_indegree -= 1; + + debug!( + "decrementing child {} indegree to {}", + child, child_indegree + ); + + if *child_indegree == 0 { + let watcher = state_channels.remove(child); + if watcher.is_none() { + // not an active service + tx.send(child.to_string())?; + continue; + } + + let shutdown_timeout = shutdown_timeouts.remove(child); + let lifecycle = self.clone_lifecycle(); + + // Spawn a task to kill the service and wait for it to terminate + let kill_task = tokio::spawn(Self::kill_wait_enhanced( + lifecycle, + child.to_string(), + tx.clone(), + watcher.unwrap(), + shutdown_timeout.unwrap_or(config::DEFAULT_SHUTDOWN_TIMEOUT), + )); + + // Add a timeout to ensure we don't wait forever + let _ = tokio::time::timeout( + std::time::Duration::from_secs( + shutdown_timeout.unwrap_or(config::DEFAULT_SHUTDOWN_TIMEOUT) + 2, + ), + kill_task, + ) + .await; + } + } + + count -= 1; + if count == 0 { + break; + } + } + + // Final verification that all processes are gone + self.verify_all_processes_terminated().await?; + + Ok(()) + } + + /// Enhanced version of kill_wait that ensures processes are terminated + #[cfg(target_os = "linux")] + async fn kill_wait_enhanced( + self, + name: String, + ch: mpsc::UnboundedSender, + mut rx: Watcher, + shutdown_timeout: u64, + ) { + debug!("kill_wait {}", name); + + // Try to stop the service gracefully + let stop_result = self.stop(name.clone()).await; + + // Wait for the service to become inactive or timeout + let fut = timeout( + std::time::Duration::from_secs(shutdown_timeout), + async move { + while let Some(state) = rx.next().await { + if !state.is_active() { + return; + } + } + }, + ); + + match stop_result { + Ok(_) => { + let _ = fut.await; + } + Err(e) => error!("couldn't stop service {}: {}", name.clone(), e), + } + + // Verify the service is actually stopped + if let Ok(status) = self.status(&name).await { + if status.pid != Pid::from_raw(0) { + // Service is still running, try to kill it + let _ = self.kill(&name, signal::Signal::SIGKILL).await; + } + } + + debug!("sending to the death channel {}", name.clone()); + if let Err(e) = ch.send(name.clone()) { + error!( + "error: couldn't send the service {} to the shutdown loop: {}", + name, e + ); + } + } + + /// Original kill_wait for backward compatibility + #[cfg(target_os = "linux")] + async fn kill_wait( + self, + name: String, + ch: mpsc::UnboundedSender, + rx: Watcher, + shutdown_timeout: u64, + ) { + Self::kill_wait_enhanced(self, name, ch, rx, shutdown_timeout).await + } + + /// Check if a service can be scheduled + async fn can_schedule(&self, service: &config::Service) -> bool { + let mut can = true; + let table = self.services.read().await; + + for dep in service.after.iter() { + can = match table.get(dep) { + Some(ps) => { + let ps = ps.read().await; + debug!( + "- service {} is {:?} oneshot: {}", + dep, + ps.get_state(), + ps.service.one_shot + ); + + match ps.get_state() { + State::Running if !ps.service.one_shot => true, + State::Success => true, + _ => false, + } + } + // depending on an undefined service. This still can be resolved later + // by monitoring the dependency in the future. + None => false, + }; + + // if state is blocked, we can break the loop + if !can { + break; + } + } + + can + } + + /// Set the state and/or target of a service + async fn set_service_state(&self, name: &str, state: Option, target: Option) { + let table = self.services.read().await; + let service = match table.get(name) { + Some(service) => service, + None => return, + }; + + let mut service = service.write().await; + if let Some(state) = state { + service.force_set_state(state); + } + + if let Some(target) = target { + service.set_target(target); + } + } + + /// Test if a service is running correctly + async fn test_service>(&self, name: S, cfg: &config::Service) -> Result { + if cfg.test.is_empty() { + return Ok(true); + } + + let log = match cfg.log { + config::Log::None => Log::None, + config::Log::Stdout => Log::Stdout, + config::Log::Ring => Log::Ring(format!("{}/test", name.as_ref())), + }; + + let test = self + .pm + .run( + Process::new(&cfg.test, &cfg.dir, Some(cfg.env.clone())), + log.clone(), + ) + .await?; + + let status = test.wait().await?; + Ok(status.success()) + } + + /// Run the test loop for a service + async fn test_loop(self, name: String, cfg: config::Service) { + loop { + let result = self.test_service(&name, &cfg).await; + + match result { + Ok(result) => { + if result { + self.set_service_state(&name, Some(State::Running), None) + .await; + // release + self.notify.notify_waiters(); + return; + } + // wait before we try again + sleep(std::time::Duration::from_secs(2)).await; + } + Err(_) => { + self.set_service_state(&name, Some(State::TestFailure), None) + .await; + } + } + } + } + + /// Watch a service and manage its lifecycle + async fn watch_service(self, name: String, input: Arc>) { + let name = name.clone(); + + let mut service = input.write().await; + if service.target == Target::Down { + debug!("service '{}' target is down", name); + return; + } + + if service.scheduled { + debug!("service '{}' already scheduled", name); + return; + } + + service.scheduled = true; + drop(service); + + loop { + let name = name.clone(); + + let service = input.read().await; + // early check if service is down, so we don't have to do extra checks + if service.target == Target::Down { + // we check target in loop in case service have + // been set down. + break; + } + + let config = service.service.clone(); + // we need to spawn this service now, but is it ready? + // are all dependent services are running? + + // so we drop the table to give other services + // chance to acquire the lock and schedule themselves + drop(service); + + 'checks: loop { + let sig = self.notify.notified(); + debug!("checking {} if it can schedule", name); + if self.can_schedule(&config).await { + debug!("service {} can schedule", name); + break 'checks; + } + + self.set_service_state(&name, Some(State::Blocked), None) + .await; + // don't even care if i am lagging + // as long i am notified that some services status + // has changed + debug!("service {} is blocked, waiting release", name); + sig.await; + } + + let log = match config.log { + config::Log::None => Log::None, + config::Log::Stdout => Log::Stdout, + config::Log::Ring => Log::Ring(name.clone()), + }; + + let mut service = input.write().await; + // we check again in case target has changed. Since we had to release the lock + // earlier to not block locking on this service (for example if a stop was called) + // while the service was waiting for dependencies. + // the lock is kept until the spawning and the update of the pid. + if service.target == Target::Down { + // we check target in loop in case service have + // been set down. + break; + } + + let child = self + .pm + .run( + Process::new(&config.exec, &config.dir, Some(config.env.clone())), + log.clone(), + ) + .await; + + let child = match child { + Ok(child) => { + service.force_set_state(State::Spawned); + service.set_pid(child.pid); + child + } + Err(err) => { + // so, spawning failed. and nothing we can do about it + // this can be duo to a bad command or exe not found. + // set service to failure. + error!("service {} failed to start: {}", name, err); + service.force_set_state(State::Failure); + break; + } + }; + + if config.one_shot { + service.force_set_state(State::Running); + } + + // we don't lock here because this can take forever + // to finish. so we allow other operation on the service (for example) + // status and stop operations. + drop(service); + + let mut handler = None; + if !config.one_shot { + let lifecycle = self.clone_lifecycle(); + handler = Some(tokio::spawn( + lifecycle.test_loop(name.clone(), config.clone()), + )); + } + + let result = child.wait().await; + if let Some(handler) = handler { + handler.abort(); + } + + let mut service = input.write().await; + service.clear_pid(); + + match result { + Err(err) => { + error!("failed to read service '{}' status: {}", name, err); + service.force_set_state(State::Unknown); + } + Ok(status) => { + service.force_set_state(if status.success() { + State::Success + } else { + State::Error(status) + }); + } + }; + + drop(service); + if config.one_shot { + // we don't need to restart the service anymore + self.notify.notify_waiters(); + break; + } + + // we trying again in 2 seconds + sleep(std::time::Duration::from_secs(2)).await; + } + + let mut service = input.write().await; + service.scheduled = false; + } + + /// Clone the lifecycle manager + pub fn clone_lifecycle(&self) -> Self { + Self { + pm: self.pm.clone(), + services: Arc::clone(&self.services), + notify: Arc::clone(&self.notify), + shutdown: Arc::clone(&self.shutdown), + container: self.container, + } + } +} diff --git a/src/zinit/mod.rs b/src/zinit/mod.rs new file mode 100644 index 0000000..676eb4f --- /dev/null +++ b/src/zinit/mod.rs @@ -0,0 +1,119 @@ +pub mod config; +pub mod errors; +pub mod lifecycle; +pub mod ord; +pub mod service; +pub mod state; +pub mod types; + +// Re-export commonly used items +pub use service::ZInitStatus; +pub use state::State; +pub use types::{ProcessStats, ServiceStats}; + +use crate::manager::{Logs, ProcessManager}; +use anyhow::Result; +use nix::sys::signal; +use std::sync::Arc; +use tokio::sync::{Notify, RwLock}; + +/// Main ZInit service manager +#[derive(Clone)] +pub struct ZInit { + /// Lifecycle manager for service management + lifecycle: lifecycle::LifecycleManager, +} + +impl ZInit { + /// Create a new ZInit instance + pub fn new(cap: usize, container: bool) -> ZInit { + let pm = ProcessManager::new(cap); + let services = Arc::new(RwLock::new(types::ServiceTable::new())); + let notify = Arc::new(Notify::new()); + let shutdown = Arc::new(RwLock::new(false)); + + let lifecycle = lifecycle::LifecycleManager::new(pm, services, notify, shutdown, container); + + ZInit { lifecycle } + } + + /// Start the service manager + pub fn serve(&self) { + self.lifecycle.process_manager().start(); + if self.lifecycle.is_container_mode() { + let lifecycle = self.lifecycle.clone_lifecycle(); + tokio::spawn(async move { + use tokio::signal::unix; + + let mut term = unix::signal(unix::SignalKind::terminate()).unwrap(); + let mut int = unix::signal(unix::SignalKind::interrupt()).unwrap(); + let mut hup = unix::signal(unix::SignalKind::hangup()).unwrap(); + + tokio::select! { + _ = term.recv() => {}, + _ = int.recv() => {}, + _ = hup.recv() => {}, + }; + + debug!("shutdown signal received"); + let _ = lifecycle.shutdown().await; + }); + } + } + + /// Get logs from the process manager + /// `existing_logs` TODO: + pub async fn logs(&self, existing_logs: bool, follow: bool) -> Logs { + self.lifecycle.logs(existing_logs, follow).await + } + + /// Monitor a service + pub async fn monitor>(&self, name: S, service: config::Service) -> Result<()> { + self.lifecycle.monitor(name, service).await + } + + /// Get the status of a service + pub async fn status>(&self, name: S) -> Result { + self.lifecycle.status(name).await + } + + /// Start a service + pub async fn start>(&self, name: S) -> Result<()> { + self.lifecycle.start(name).await + } + + /// Stop a service + pub async fn stop>(&self, name: S) -> Result<()> { + self.lifecycle.stop(name).await + } + + /// Forget a service + pub async fn forget>(&self, name: S) -> Result<()> { + self.lifecycle.forget(name).await + } + + /// Send a signal to a service + pub async fn kill>(&self, name: S, signal: signal::Signal) -> Result<()> { + self.lifecycle.kill(name, signal).await + } + + /// List all services + pub async fn list(&self) -> Result> { + self.lifecycle.list().await + } + + /// Shutdown the system + pub async fn shutdown(&self) -> Result<()> { + self.lifecycle.shutdown().await + } + + /// Reboot the system + pub async fn reboot(&self) -> Result<()> { + self.lifecycle.reboot().await + } + + /// Get stats for a service (memory and CPU usage) + pub async fn stats>(&self, name: S) -> Result { + self.lifecycle.stats(name).await + } +} diff --git a/src/zinit/ord.rs b/src/zinit/ord.rs new file mode 100644 index 0000000..75c9dae --- /dev/null +++ b/src/zinit/ord.rs @@ -0,0 +1,37 @@ +use crate::zinit::types::ServiceTable; +use std::collections::HashMap; +use std::sync::Arc; +use tokio::sync::RwLock; +pub const DUMMY_ROOT: &str = ""; +pub struct ProcessDAG { + pub adj: HashMap>, + pub indegree: HashMap, + /// number of services including the dummy root + pub count: u32, +} +pub async fn service_dependency_order(services: Arc>) -> ProcessDAG { + let mut children: HashMap> = HashMap::new(); + let mut indegree: HashMap = HashMap::new(); + let table = services.read().await; + for (name, service) in table.iter() { + let service = service.read().await; + for child in service.service.after.iter() { + children.entry(name.into()).or_default().push(child.into()); + *indegree.entry(child.into()).or_insert(0) += 1; + } + } + let mut heads: Vec = Vec::new(); + for (name, _) in table.iter() { + if *indegree.get::(name).unwrap_or(&0) == 0 { + heads.push(name.into()); + // add edges from the dummy root to the heads + *indegree.entry(name.into()).or_insert(0) += 1; + } + } + children.insert(DUMMY_ROOT.to_string(), heads); + ProcessDAG { + adj: children, + indegree, + count: table.len() as u32 + 1, + } +} diff --git a/src/zinit/service.rs b/src/zinit/service.rs new file mode 100644 index 0000000..823a4ea --- /dev/null +++ b/src/zinit/service.rs @@ -0,0 +1,126 @@ +use crate::zinit::config; +use crate::zinit::state::{State, Target}; +use crate::zinit::types::Watched; +use anyhow::{Context, Result}; +use nix::unistd::Pid; + +/// Represents a service managed by ZInit +pub struct ZInitService { + /// Process ID of the running service + pub pid: Pid, + + /// Service configuration + pub service: config::Service, + + /// Target state of the service (up, down) + pub target: Target, + + /// Whether the service is scheduled for execution + pub scheduled: bool, + + /// Current state of the service + state: Watched, +} + +/// Status information for a service +pub struct ZInitStatus { + /// Process ID of the running service + pub pid: Pid, + + /// Service configuration + pub service: config::Service, + + /// Target state of the service (up, down) + pub target: Target, + + /// Whether the service is scheduled for execution + pub scheduled: bool, + + /// Current state of the service + pub state: State, +} + +impl ZInitService { + /// Create a new service with the given configuration and initial state + pub fn new(service: config::Service, state: State) -> ZInitService { + ZInitService { + pid: Pid::from_raw(0), + state: Watched::new(state), + service, + target: Target::Up, + scheduled: false, + } + } + + /// Get the current status of the service + pub fn status(&self) -> ZInitStatus { + ZInitStatus { + pid: self.pid, + state: self.state.get().clone(), + service: self.service.clone(), + target: self.target.clone(), + scheduled: self.scheduled, + } + } + + /// Set the state of the service, validating the state transition + pub fn set_state(&mut self, state: State) -> Result<()> { + let current_state = self.state.get().clone(); + let new_state = current_state + .transition_to(state) + .context("Failed to transition service state")?; + + self.state.set(new_state); + Ok(()) + } + + /// Set the state of the service without validation + pub fn force_set_state(&mut self, state: State) { + self.state.set(state); + } + + /// Set the target state of the service + pub fn set_target(&mut self, target: Target) { + self.target = target; + } + + /// Get the current state of the service + pub fn get_state(&self) -> &State { + self.state.get() + } + + /// Get a watcher for the service state + pub fn state_watcher(&self) -> crate::zinit::types::Watcher { + self.state.watcher() + } + + /// Check if the service is active (running or in progress) + pub fn is_active(&self) -> bool { + self.state.get().is_active() + } + + /// Check if the service is in a terminal state (success or failure) + pub fn is_terminal(&self) -> bool { + self.state.get().is_terminal() + } + + /// Set the process ID of the service + pub fn set_pid(&mut self, pid: Pid) { + self.pid = pid; + } + + /// Clear the process ID of the service + pub fn clear_pid(&mut self) { + self.pid = Pid::from_raw(0); + } + + /// Check if the service is running + pub fn is_running(&self) -> bool { + self.pid.as_raw() != 0 && self.state.get().is_active() + } + + /// Check if the service is a one-shot service + pub fn is_one_shot(&self) -> bool { + self.service.one_shot + } +} diff --git a/src/zinit/state.rs b/src/zinit/state.rs new file mode 100644 index 0000000..7e9fa8b --- /dev/null +++ b/src/zinit/state.rs @@ -0,0 +1,106 @@ +use crate::zinit::errors::ZInitError; +use anyhow::Result; +use nix::sys::wait::WaitStatus; + +/// Target state for a service +#[derive(Clone, Debug, PartialEq)] +pub enum Target { + /// Service should be running + Up, + /// Service should be stopped + Down, +} + +/// Service state +#[derive(Debug, PartialEq, Clone)] +pub enum State { + /// Service is in an unknown state + Unknown, + + /// Blocked means one or more dependencies hasn't been met yet. Service can stay in + /// this state as long as at least one dependency is not in either Running, or Success + Blocked, + + /// Service has been started, but it didn't exit yet, or we didn't run the test command. + Spawned, + + /// Service has been started, and test command passed. + Running, + + /// Service has exited with success state, only one-shot can stay in this state + Success, + + /// Service exited with this error, only one-shot can stay in this state + Error(WaitStatus), + + /// The service test command failed, this might (or might not) be replaced + /// with an Error state later on once the service process itself exits + TestFailure, + + /// Failure means the service has failed to spawn in a way that retrying + /// won't help, like command line parsing error or failed to fork + Failure, +} + +impl State { + /// Validate if a transition from the current state to the new state is valid + pub fn can_transition_to(&self, new_state: &State) -> bool { + match (self, new_state) { + // From Unknown state, any transition is valid + (State::Unknown, _) => true, + + // From Blocked state + (State::Blocked, State::Spawned) => true, + (State::Blocked, State::Failure) => true, + + // From Spawned state + (State::Spawned, State::Running) => true, + (State::Spawned, State::TestFailure) => true, + (State::Spawned, State::Error(_)) => true, + (State::Spawned, State::Success) => true, + + // From Running state + (State::Running, State::Success) => true, + (State::Running, State::Error(_)) => true, + + // To Unknown or Blocked state is always valid + (_, State::Unknown) => true, + (_, State::Blocked) => true, + + // Any other transition is invalid + _ => false, + } + } + + /// Attempt to transition to a new state, validating the transition + pub fn transition_to(&self, new_state: State) -> Result { + if self.can_transition_to(&new_state) { + Ok(new_state) + } else { + Err(ZInitError::invalid_state_transition(format!( + "Invalid transition from {:?} to {:?}", + self, new_state + ))) + } + } + + /// Check if the state is considered "active" (running or in progress) + pub fn is_active(&self) -> bool { + matches!(self, State::Running | State::Spawned) + } + + /// Check if the state is considered "terminal" (success or failure) + pub fn is_terminal(&self) -> bool { + matches!(self, State::Success | State::Error(_) | State::Failure) + } + + /// Check if the state is considered "successful" + pub fn is_successful(&self) -> bool { + matches!(self, State::Success | State::Running) + } + + /// Check if the state is considered "failed" + pub fn is_failed(&self) -> bool { + matches!(self, State::Error(_) | State::Failure | State::TestFailure) + } +} diff --git a/src/zinit/types.rs b/src/zinit/types.rs new file mode 100644 index 0000000..5a5c7af --- /dev/null +++ b/src/zinit/types.rs @@ -0,0 +1,89 @@ +use nix::sys::wait::WaitStatus; +use serde::{Deserialize, Serialize}; +use std::collections::HashMap; +use std::sync::Arc; +use tokio::sync::watch; +use tokio::sync::RwLock; +use tokio_stream::wrappers::WatchStream; + +/// Stats information for a service +#[derive(Debug, Clone, Serialize, Deserialize)] +pub struct ServiceStats { + /// Memory usage in bytes + pub memory_usage: u64, + + /// CPU usage as a percentage (0-100) + pub cpu_usage: f32, + + /// Process ID of the service + pub pid: i32, + + /// Child process stats if any + pub children: Vec, +} + +/// Stats for an individual process +#[derive(Debug, Clone, Serialize, Deserialize)] +pub struct ProcessStats { + /// Process ID + pub pid: i32, + + /// Memory usage in bytes + pub memory_usage: u64, + + /// CPU usage as a percentage (0-100) + pub cpu_usage: f32, +} + +/// Extension trait for WaitStatus to check if a process exited successfully +pub trait WaitStatusExt { + fn success(&self) -> bool; +} + +impl WaitStatusExt for WaitStatus { + fn success(&self) -> bool { + matches!(self, WaitStatus::Exited(_, code) if *code == 0) + } +} + +/// Type alias for a service table mapping service names to service instances +pub type ServiceTable = HashMap>>; + +/// Type alias for a watch stream +pub type Watcher = WatchStream>; + +/// A wrapper around a value that can be watched for changes +pub struct Watched { + v: Arc, + tx: watch::Sender>, +} + +impl Watched +where + T: Send + Sync + 'static, +{ + /// Create a new watched value + pub fn new(v: T) -> Self { + let v = Arc::new(v); + let (tx, _) = watch::channel(Arc::clone(&v)); + Self { v, tx } + } + + /// Set the value and notify watchers + pub fn set(&mut self, v: T) { + let v = Arc::new(v); + self.v = Arc::clone(&v); + // update the value even when there are no receivers + self.tx.send_replace(v); + } + + /// Get a reference to the current value + pub fn get(&self) -> &T { + &self.v + } + + /// Create a watcher for this value + pub fn watcher(&self) -> Watcher { + WatchStream::new(self.tx.subscribe()) + } +} diff --git a/stop.sh b/stop.sh new file mode 100755 index 0000000..f8b42ff --- /dev/null +++ b/stop.sh @@ -0,0 +1,42 @@ +#!/bin/bash +set -e + +# Colors for output +RED='\033[0;31m' +GREEN='\033[0;32m' +YELLOW='\033[0;33m' +NC='\033[0m' # No Color + +echo -e "${GREEN}Stopping zinit...${NC}" + +# Function to check if zinit is running +is_zinit_running() { + pgrep -f "zinit" > /dev/null + return $? +} + +# Try to shutdown zinit gracefully if it's running +if is_zinit_running; then + echo -e "${YELLOW}Zinit is already running. Attempting graceful shutdown...${NC}" + zinit shutdown || true + + # Give it a moment to shut down + sleep 2 + + # Check if it's still running + if is_zinit_running; then + echo -e "${YELLOW}Zinit is still running. Attempting to kill the process...${NC}" + pkill -f "zinit$" || true + sleep 1 + fi +else + echo -e "${YELLOW}No existing zinit process found.${NC}" +fi + +# Double-check no zinit is running +if is_zinit_running; then + echo -e "${RED}Warning: Could not terminate existing zinit process. You may need to manually kill it.${NC}" + ps aux | grep "zinit" | grep -v grep +else + echo -e "${GREEN}No zinit process is running. Ready to start a new instance.${NC}" +fi diff --git a/zinit-client/Cargo.toml b/zinit-client/Cargo.toml new file mode 100644 index 0000000..76afd6b --- /dev/null +++ b/zinit-client/Cargo.toml @@ -0,0 +1,26 @@ +[package] +name = "zinit-client" +version = "0.1.0" +edition = "2021" +description = "A client library for interacting with Zinit process manager" +license = "Apache 2.0" +authors = ["ThreeFold Tech, https://github.com/threefoldtech"] + +[dependencies] +anyhow = "1.0" +async-trait = "0.1.88" +jsonrpsee = { version = "0.25.1", features = ["macros", "http-client", "ws-client"] } +reth-ipc = { git = "https://github.com/paradigmxyz/reth", package = "reth-ipc" } +tokio = { version = "1.14.0", features = ["full"] } +serde = { version = "1.0", features = ["derive"] } +serde_json = "1.0" +thiserror = "1.0" +log = "0.4" + +[[example]] +name = "basic_usage" +path = "examples/basic_usage.rs" + +[[example]] +name = "http_client" +path = "examples/http_client.rs" diff --git a/zinit-client/README.md b/zinit-client/README.md new file mode 100644 index 0000000..d527625 --- /dev/null +++ b/zinit-client/README.md @@ -0,0 +1,123 @@ +# Zinit Client Library + +A simple Rust client library for interacting with the Zinit process manager. + +## Features + +- Connect to Zinit via Unix socket or HTTP +- Manage services (start, stop, restart, monitor) +- Query service status and information +- Create and delete service configurations +- System operations (shutdown, reboot) + +## Installation + +Add this to your `Cargo.toml`: + +```toml +[dependencies] +zinit-client = "0.1.0" +``` + +## Usage + +### Creating a Client + +You can create a client using either Unix socket or HTTP transport: + +```rust +use zinit_client::Client; + +// Using Unix socket (local only) +let client = Client::unix_socket("/var/run/zinit.sock"); + +// Using HTTP (works for remote Zinit instances) +let client = Client::http("http://localhost:8080"); +``` + +### Service Management + +```rust +// List all services +let services = client.list().await?; +for (name, state) in services { + println!("{}: {}", name, state); +} + +// Get status of a specific service +let status = client.status("my-service").await?; +println!("PID: {}, State: {}", status.pid, status.state); + +// Start a service +client.start("my-service").await?; + +// Stop a service +client.stop("my-service").await?; + +// Restart a service +client.restart("my-service").await?; + +// Monitor a service +client.monitor("my-service").await?; + +// Forget a service +client.forget("my-service").await?; + +// Send a signal to a service +client.kill("my-service", "SIGTERM").await?; +``` + +### Service Configuration + +```rust +use serde_json::json; + +// Create a new service +let config = json!({ + "exec": "nginx", + "oneshot": false, + "after": ["network"] +}).as_object().unwrap().clone(); + +client.create_service("nginx", config).await?; + +// Get service configuration +let config = client.get_service("nginx").await?; +println!("Config: {:?}", config); + +// Delete a service +client.delete_service("nginx").await?; +``` + +### System Operations + +```rust +// Shutdown the system +client.shutdown().await?; + +// Reboot the system +client.reboot().await?; +``` + +## Error Handling + +The library provides a `ClientError` enum for handling errors: + +```rust +match client.status("non-existent-service").await { + Ok(status) => println!("Service status: {}", status.state), + Err(e) => match e { + ClientError::ServiceNotFound(_) => println!("Service not found"), + ClientError::ConnectionError(_) => println!("Failed to connect to Zinit"), + _ => println!("Other error: {}", e), + }, +} +``` + +## Examples + +See the [examples](examples) directory for complete usage examples. + +## License + +This project is licensed under the MIT License. \ No newline at end of file diff --git a/zinit-client/examples/basic_usage.rs b/zinit-client/examples/basic_usage.rs new file mode 100644 index 0000000..c83e0e9 --- /dev/null +++ b/zinit-client/examples/basic_usage.rs @@ -0,0 +1,50 @@ +use anyhow::Result; +use zinit_client::Client; + +#[tokio::main] +async fn main() -> Result<()> { + // Create a client using Unix socket transport + let client = Client::unix_socket("/var/run/zinit.sock").await?; + + // List all services + let services = client.list().await?; + println!("Services:"); + for (name, state) in services { + println!("{}: {}", name, state); + } + + // Get a specific service status + let service_name = "example-service"; + match client.status(service_name).await { + Ok(status) => { + println!("\nService: {}", status.name); + println!("PID: {}", status.pid); + println!("State: {}", status.state); + println!("Target: {}", status.target); + println!("After:"); + for (dep, state) in status.after { + println!(" {}: {}", dep, state); + } + } + Err(e) => eprintln!("Failed to get status: {}", e), + } + + // Try to start a service + match client.start(service_name).await { + Ok(_) => println!("\nService started successfully"), + Err(e) => eprintln!("Failed to start service: {}", e), + } + + // Get logs for the service + match client.logs(Some(service_name.to_string())).await { + Ok(logs) => { + println!("\nLogs:"); + for log in logs { + println!("{}", log); + } + } + Err(e) => eprintln!("Failed to get logs: {}", e), + } + + Ok(()) +} diff --git a/zinit-client/examples/http_client.rs b/zinit-client/examples/http_client.rs new file mode 100644 index 0000000..a8395e1 --- /dev/null +++ b/zinit-client/examples/http_client.rs @@ -0,0 +1,78 @@ +use anyhow::Result; +use serde_json::json; +use zinit_client::Client; + +#[tokio::main] +async fn main() -> Result<()> { + // Create a client using HTTP transport + let client = Client::http("http://localhost:8080").await?; + + // Create a new service + let service_name = "example-http-service"; + let service_config = json!({ + "exec": "echo 'Hello from HTTP service'", + "oneshot": true, + "after": ["network"] + }) + .as_object() + .unwrap() + .clone(); + + match client.create_service(service_name, service_config).await { + Ok(msg) => println!("Service created: {}", msg), + Err(e) => eprintln!("Failed to create service: {}", e), + } + + // Start the HTTP/RPC server on a specific address + match client.start_http_server("0.0.0.0:8081").await { + Ok(msg) => println!("HTTP server status: {}", msg), + Err(e) => eprintln!("Failed to start HTTP server: {}", e), + } + + // List all services + let services = client.list().await?; + println!("\nServices:"); + for (name, state) in services { + println!("{}: {}", name, state); + } + + // Monitor the service + match client.monitor(service_name).await { + Ok(_) => println!("\nService is now monitored"), + Err(e) => eprintln!("Failed to monitor service: {}", e), + } + + // Start the service + match client.start(service_name).await { + Ok(_) => println!("Service started successfully"), + Err(e) => eprintln!("Failed to start service: {}", e), + } + + // Get logs + let logs = client.logs(Some(service_name.to_string())).await?; + println!("\nLogs:"); + for log in logs { + println!("{}", log); + } + + // Clean up - forget the service + println!("\nCleaning up..."); + match client.forget(service_name).await { + Ok(_) => println!("Service has been forgotten"), + Err(e) => eprintln!("Failed to forget service: {}", e), + } + + // Clean up - delete the service configuration + match client.delete_service(service_name).await { + Ok(msg) => println!("{}", msg), + Err(e) => eprintln!("Failed to delete service: {}", e), + } + + // Stop the HTTP/RPC server + match client.stop_http_server().await { + Ok(_) => println!("HTTP server stopped"), + Err(e) => eprintln!("Failed to stop HTTP server: {}", e), + } + + Ok(()) +} diff --git a/zinit-client/src/lib.rs b/zinit-client/src/lib.rs new file mode 100644 index 0000000..b01fef6 --- /dev/null +++ b/zinit-client/src/lib.rs @@ -0,0 +1,450 @@ +//! A client library for interacting with the Zinit process manager. +//! +//! This library provides a simple API for communicating with a Zinit daemon +//! via either Unix socket (using reth-ipc) or HTTP (using jsonrpsee). +use jsonrpsee::core::client::ClientT; +use jsonrpsee::core::client::Error as RpcError; +use jsonrpsee::http_client::{HttpClient, HttpClientBuilder}; +use jsonrpsee::rpc_params; +use reth_ipc::client::IpcClientBuilder; +use serde::{Deserialize, Serialize}; +use serde_json::{Map, Value}; +use std::collections::HashMap; +use thiserror::Error; + +/// Error type for client operations +#[derive(Error, Debug)] +pub enum ClientError { + #[error("connection error: {0}")] + ConnectionError(String), + + #[error("service not found: {0}")] + ServiceNotFound(String), + + #[error("service is already up: {0}")] + ServiceIsUp(String), + + #[error("system is shutting down")] + ShuttingDown, + + #[error("service already exists: {0}")] + ServiceAlreadyExists(String), + + #[error("service file error: {0}")] + ServiceFileError(String), + + #[error("rpc error: {0}")] + RpcError(String), + + #[error("unknown error: {0}")] + UnknownError(String), +} + +impl From for ClientError { + fn from(err: RpcError) -> Self { + // Parse the error code if available + if let RpcError::Call(err) = &err { + match err.code() { + -32000 => return ClientError::ServiceNotFound(err.message().to_string()), + -32002 => return ClientError::ServiceIsUp(err.message().to_string()), + -32006 => return ClientError::ShuttingDown, + -32007 => return ClientError::ServiceAlreadyExists(err.message().to_string()), + -32008 => return ClientError::ServiceFileError(err.message().to_string()), + _ => {} + } + } + + match err { + RpcError::Transport(_) => ClientError::ConnectionError(err.to_string()), + _ => ClientError::RpcError(err.to_string()), + } + } +} + +/// Service status information +#[derive(Debug, Clone, Deserialize, Serialize)] +pub struct Status { + pub name: String, + pub pid: u32, + pub state: String, + pub target: String, + pub after: HashMap, +} + +/// Child process stats information +#[derive(Debug, Clone, Deserialize, Serialize)] +pub struct ChildStats { + pub pid: u32, + pub memory_usage: u64, + pub cpu_usage: f32, +} + +/// Service stats information +#[derive(Debug, Clone, Deserialize, Serialize)] +pub struct Stats { + pub name: String, + pub pid: u32, + pub memory_usage: u64, + pub cpu_usage: f32, + pub children: Vec, +} + +/// Client implementation for communicating with Zinit +pub enum Client { + Ipc(String), // Socket path + Http(HttpClient), +} + +impl Client { + /// Create a new client using Unix socket transport + pub async fn unix_socket>(path: P) -> Result { + Ok(Client::Ipc(path.as_ref().to_string_lossy().to_string())) + } + + /// Create a new client using HTTP transport + pub async fn http>(url: S) -> Result { + let client = HttpClientBuilder::default() + .build(url.as_ref()) + .map_err(|e| ClientError::ConnectionError(e.to_string()))?; + + Ok(Client::Http(client)) + } + + // Helper to get IPC client + async fn get_ipc_client(&self) -> Result { + match self { + Client::Ipc(path) => IpcClientBuilder::default() + .build(path) + .await + .map_err(|e| ClientError::ConnectionError(e.to_string())), + _ => Err(ClientError::UnknownError("Not an IPC client".to_string())), + } + } + + // Service API Methods + + /// List all monitored services and their current state + pub async fn list(&self) -> Result, ClientError> { + match self { + Client::Ipc(_) => { + let client = self.get_ipc_client().await?; + client + .request("service_list", rpc_params![]) + .await + .map_err(Into::into) + } + Client::Http(client) => client + .request("service_list", rpc_params![]) + .await + .map_err(Into::into), + } + } + + /// Get the detailed status of a specific service + pub async fn status(&self, name: impl AsRef) -> Result { + let name = name.as_ref().to_string(); + match self { + Client::Ipc(_) => { + let client = self.get_ipc_client().await?; + client + .request("service_status", rpc_params![name]) + .await + .map_err(Into::into) + } + Client::Http(client) => client + .request("service_status", rpc_params![name]) + .await + .map_err(Into::into), + } + } + + /// Start a specific service + pub async fn start(&self, name: impl AsRef) -> Result<(), ClientError> { + let name = name.as_ref().to_string(); + match self { + Client::Ipc(_) => { + let client = self.get_ipc_client().await?; + client + .request("service_start", rpc_params![name]) + .await + .map_err(Into::into) + } + Client::Http(client) => client + .request("service_start", rpc_params![name]) + .await + .map_err(Into::into), + } + } + + /// Stop a specific service + pub async fn stop(&self, name: impl AsRef) -> Result<(), ClientError> { + let name = name.as_ref().to_string(); + match self { + Client::Ipc(_) => { + let client = self.get_ipc_client().await?; + client + .request("service_stop", rpc_params![name]) + .await + .map_err(Into::into) + } + Client::Http(client) => client + .request("service_stop", rpc_params![name]) + .await + .map_err(Into::into), + } + } + + /// Restart a service + pub async fn restart(&self, name: impl AsRef) -> Result<(), ClientError> { + let name = name.as_ref().to_string(); + // First stop the service + self.stop(&name).await?; + + // Poll the service status until it's stopped + for _ in 0..20 { + let status = self.status(&name).await?; + if status.pid == 0 && status.target == "Down" { + return self.start(&name).await; + } + tokio::time::sleep(std::time::Duration::from_secs(1)).await; + } + + // Process not stopped, try to kill it + self.kill(&name, "SIGKILL").await?; + self.start(&name).await + } + + /// Load and monitor a new service from its configuration file + pub async fn monitor(&self, name: impl AsRef) -> Result<(), ClientError> { + let name = name.as_ref().to_string(); + match self { + Client::Ipc(_) => { + let client = self.get_ipc_client().await?; + client + .request("service_monitor", rpc_params![name]) + .await + .map_err(Into::into) + } + Client::Http(client) => client + .request("service_monitor", rpc_params![name]) + .await + .map_err(Into::into), + } + } + + /// Stop monitoring a service and remove it from management + pub async fn forget(&self, name: impl AsRef) -> Result<(), ClientError> { + let name = name.as_ref().to_string(); + match self { + Client::Ipc(_) => { + let client = self.get_ipc_client().await?; + client + .request("service_forget", rpc_params![name]) + .await + .map_err(Into::into) + } + Client::Http(client) => client + .request("service_forget", rpc_params![name]) + .await + .map_err(Into::into), + } + } + + /// Send a signal to a specific service process + pub async fn kill( + &self, + name: impl AsRef, + signal: impl AsRef, + ) -> Result<(), ClientError> { + let name = name.as_ref().to_string(); + let signal = signal.as_ref().to_string(); + match self { + Client::Ipc(_) => { + let client = self.get_ipc_client().await?; + client + .request("service_kill", rpc_params![name, signal]) + .await + .map_err(Into::into) + } + Client::Http(client) => client + .request("service_kill", rpc_params![name, signal]) + .await + .map_err(Into::into), + } + } + + /// Create a new service configuration + pub async fn create_service( + &self, + name: impl AsRef, + content: Map, + ) -> Result { + let name = name.as_ref().to_string(); + match self { + Client::Ipc(_) => { + let client = self.get_ipc_client().await?; + client + .request("service_create", rpc_params![name, content]) + .await + .map_err(Into::into) + } + Client::Http(client) => client + .request("service_create", rpc_params![name, content]) + .await + .map_err(Into::into), + } + } + + /// Delete a service configuration + pub async fn delete_service(&self, name: impl AsRef) -> Result { + let name = name.as_ref().to_string(); + match self { + Client::Ipc(_) => { + let client = self.get_ipc_client().await?; + client + .request("service_delete", rpc_params![name]) + .await + .map_err(Into::into) + } + Client::Http(client) => client + .request("service_delete", rpc_params![name]) + .await + .map_err(Into::into), + } + } + + /// Get a service configuration + pub async fn get_service(&self, name: impl AsRef) -> Result { + let name = name.as_ref().to_string(); + match self { + Client::Ipc(_) => { + let client = self.get_ipc_client().await?; + client + .request("service_get", rpc_params![name]) + .await + .map_err(Into::into) + } + Client::Http(client) => client + .request("service_get", rpc_params![name]) + .await + .map_err(Into::into), + } + } + + /// Get memory and CPU usage statistics for a service + pub async fn stats(&self, name: impl AsRef) -> Result { + let name = name.as_ref().to_string(); + match self { + Client::Ipc(_) => { + let client = self.get_ipc_client().await?; + client + .request("service_stats", rpc_params![name]) + .await + .map_err(Into::into) + } + Client::Http(client) => client + .request("service_stats", rpc_params![name]) + .await + .map_err(Into::into), + } + } + + // System API Methods + + /// Initiate system shutdown + pub async fn shutdown(&self) -> Result<(), ClientError> { + match self { + Client::Ipc(_) => { + let client = self.get_ipc_client().await?; + client + .request("system_shutdown", rpc_params![]) + .await + .map_err(Into::into) + } + Client::Http(client) => client + .request("system_shutdown", rpc_params![]) + .await + .map_err(Into::into), + } + } + + /// Initiate system reboot + pub async fn reboot(&self) -> Result<(), ClientError> { + match self { + Client::Ipc(_) => { + let client = self.get_ipc_client().await?; + client + .request("system_reboot", rpc_params![]) + .await + .map_err(Into::into) + } + Client::Http(client) => client + .request("system_reboot", rpc_params![]) + .await + .map_err(Into::into), + } + } + + /// Start HTTP/RPC server + pub async fn start_http_server(&self, address: impl AsRef) -> Result { + let address = address.as_ref().to_string(); + match self { + Client::Ipc(_) => { + let client = self.get_ipc_client().await?; + client + .request("system_start_http_server", rpc_params![address]) + .await + .map_err(Into::into) + } + Client::Http(client) => client + .request("system_start_http_server", rpc_params![address]) + .await + .map_err(Into::into), + } + } + + /// Stop HTTP/RPC server + pub async fn stop_http_server(&self) -> Result<(), ClientError> { + match self { + Client::Ipc(_) => { + let client = self.get_ipc_client().await?; + client + .request("system_stop_http_server", rpc_params![]) + .await + .map_err(Into::into) + } + Client::Http(client) => client + .request("system_stop_http_server", rpc_params![]) + .await + .map_err(Into::into), + } + } + + // Logging API Methods + + /// Get current logs + pub async fn logs(&self, filter: Option) -> Result, ClientError> { + match self { + Client::Ipc(_) => { + let client = self.get_ipc_client().await?; + client + .request("stream_currentLogs", rpc_params![filter]) + .await + .map_err(Into::into) + } + Client::Http(client) => client + .request("stream_currentLogs", rpc_params![filter]) + .await + .map_err(Into::into), + } + } + + /// Subscribe to logs + /// + /// Note: This method is not fully implemented yet. For now, it will return an error. + pub async fn log_subscribe(&self, _filter: Option) -> Result<(), ClientError> { + Err(ClientError::UnknownError( + "Log subscription not implemented yet".to_string(), + )) + } +} diff --git a/zinit-client/tests/integration_test.rs b/zinit-client/tests/integration_test.rs new file mode 100644 index 0000000..e9718cc --- /dev/null +++ b/zinit-client/tests/integration_test.rs @@ -0,0 +1,66 @@ +use std::env; +use zinit_client::{Client, ClientError}; + +#[tokio::test] +async fn test_connection_error() { + // Try to connect to a non-existent socket + let result = Client::unix_socket("/non/existent/socket").await; + assert!(result.is_ok()); // Just creating the client succeeds + + // Trying to make a request should fail + if let Ok(client) = result { + let list_result = client.list().await; + assert!(matches!(list_result, Err(ClientError::ConnectionError(_)))); + } +} + +#[tokio::test] +async fn test_http_connection_error() { + // Try to connect to a non-existent HTTP endpoint + let result = Client::http("http://localhost:12345").await; + // This should succeed as we're just creating the client, not making a request + assert!(result.is_ok()); + + // Try to make a request which should fail + if let Ok(client) = result { + let list_result = client.list().await; + assert!(matches!(list_result, Err(ClientError::ConnectionError(_)))); + } +} + +// This test only runs if ZINIT_SOCKET is set in the environment +// and points to a valid Zinit socket +#[tokio::test] +#[ignore] +async fn test_live_connection() { + let socket_path = match env::var("ZINIT_SOCKET") { + Ok(path) => path, + Err(_) => { + println!("ZINIT_SOCKET not set, skipping live test"); + return; + } + }; + + let client = match Client::unix_socket(&socket_path).await { + Ok(client) => client, + Err(e) => { + panic!( + "Failed to connect to Zinit socket at {}: {}", + socket_path, e + ); + } + }; + + // Test listing services + let services = client.list().await.expect("Failed to list services"); + println!("Found {} services", services.len()); + + // If there are services, test getting status of the first one + if let Some((service_name, _)) = services.iter().next() { + let status = client + .status(service_name) + .await + .expect("Failed to get service status"); + println!("Service {} has PID {}", service_name, status.pid); + } +} diff --git a/zinit.json b/zinit.json new file mode 100644 index 0000000..57e1cbe --- /dev/null +++ b/zinit.json @@ -0,0 +1,873 @@ +{ + "openrpc": "1.2.6", + "info": { + "version": "1.0.0", + "title": "Zinit JSON-RPC API", + "description": "JSON-RPC 2.0 API for controlling and querying Zinit services", + "license": { + "name": "MIT" + } + }, + "servers": [ + { + "name": "Unix Socket", + "url": "unix:///var/run/zinit.sock" + } + ], + "methods": [ + { + "name": "rpc_discover", + "description": "Returns the OpenRPC specification for the API", + "params": [], + "result": { + "name": "OpenRPCSpec", + "description": "The OpenRPC specification", + "schema": { + "type": "object" + } + }, + "examples": [ + { + "name": "Get API specification", + "params": [], + "result": { + "name": "OpenRPCSpecResult", + "value": { + "openrpc": "1.2.6", + "info": { + "version": "1.0.0", + "title": "Zinit JSON-RPC API" + } + } + } + } + ] + }, + { + "name": "service_list", + "description": "Lists all services managed by Zinit", + "params": [], + "result": { + "name": "ServiceList", + "description": "A map of service names to their current states", + "schema": { + "type": "object", + "additionalProperties": { + "type": "string", + "description": "Service state (Running, Success, Error, etc.)" + } + } + }, + "examples": [ + { + "name": "List all services", + "params": [], + "result": { + "name": "ServiceListResult", + "value": { + "service1": "Running", + "service2": "Success", + "service3": "Error" + } + } + } + ] + }, + { + "name": "service_status", + "description": "Shows detailed status information for a specific service", + "params": [ + { + "name": "name", + "description": "The name of the service", + "required": true, + "schema": { + "type": "string" + } + } + ], + "result": { + "name": "ServiceStatus", + "description": "Detailed status information for the service", + "schema": { + "type": "object", + "properties": { + "name": { + "type": "string", + "description": "Service name" + }, + "pid": { + "type": "integer", + "description": "Process ID of the running service (if running)" + }, + "state": { + "type": "string", + "description": "Current state of the service (Running, Success, Error, etc.)" + }, + "target": { + "type": "string", + "description": "Target state of the service (Up, Down)" + }, + "after": { + "type": "object", + "description": "Dependencies of the service and their states", + "additionalProperties": { + "type": "string", + "description": "State of the dependency" + } + } + } + } + }, + "examples": [ + { + "name": "Get status of redis service", + "params": [ + { + "name": "name", + "value": "redis" + } + ], + "result": { + "name": "ServiceStatusResult", + "value": { + "name": "redis", + "pid": 1234, + "state": "Running", + "target": "Up", + "after": { + "dependency1": "Success", + "dependency2": "Running" + } + } + } + } + ], + "errors": [ + { + "code": -32000, + "message": "Service not found", + "data": "service name \"unknown\" unknown" + } + ] + }, + { + "name": "service_start", + "description": "Starts a service", + "params": [ + { + "name": "name", + "description": "The name of the service to start", + "required": true, + "schema": { + "type": "string" + } + } + ], + "result": { + "name": "StartResult", + "description": "Result of the start operation", + "schema": { + "type": "null" + } + }, + "examples": [ + { + "name": "Start redis service", + "params": [ + { + "name": "name", + "value": "redis" + } + ], + "result": { + "name": "StartResult", + "value": null + } + } + ], + "errors": [ + { + "code": -32000, + "message": "Service not found", + "data": "service name \"unknown\" unknown" + } + ] + }, + { + "name": "service_stop", + "description": "Stops a service", + "params": [ + { + "name": "name", + "description": "The name of the service to stop", + "required": true, + "schema": { + "type": "string" + } + } + ], + "result": { + "name": "StopResult", + "description": "Result of the stop operation", + "schema": { + "type": "null" + } + }, + "examples": [ + { + "name": "Stop redis service", + "params": [ + { + "name": "name", + "value": "redis" + } + ], + "result": { + "name": "StopResult", + "value": null + } + } + ], + "errors": [ + { + "code": -32000, + "message": "Service not found", + "data": "service name \"unknown\" unknown" + }, + { + "code": -32003, + "message": "Service is down", + "data": "service \"redis\" is down" + } + ] + }, + { + "name": "service_monitor", + "description": "Starts monitoring a service. The service configuration is loaded from the config directory.", + "params": [ + { + "name": "name", + "description": "The name of the service to monitor", + "required": true, + "schema": { + "type": "string" + } + } + ], + "result": { + "name": "MonitorResult", + "description": "Result of the monitor operation", + "schema": { + "type": "null" + } + }, + "examples": [ + { + "name": "Monitor redis service", + "params": [ + { + "name": "name", + "value": "redis" + } + ], + "result": { + "name": "MonitorResult", + "value": null + } + } + ], + "errors": [ + { + "code": -32001, + "message": "Service already monitored", + "data": "service \"redis\" already monitored" + }, + { + "code": -32005, + "message": "Config error", + "data": "failed to load service configuration" + } + ] + }, + { + "name": "service_forget", + "description": "Stops monitoring a service. You can only forget a stopped service.", + "params": [ + { + "name": "name", + "description": "The name of the service to forget", + "required": true, + "schema": { + "type": "string" + } + } + ], + "result": { + "name": "ForgetResult", + "description": "Result of the forget operation", + "schema": { + "type": "null" + } + }, + "examples": [ + { + "name": "Forget redis service", + "params": [ + { + "name": "name", + "value": "redis" + } + ], + "result": { + "name": "ForgetResult", + "value": null + } + } + ], + "errors": [ + { + "code": -32000, + "message": "Service not found", + "data": "service name \"unknown\" unknown" + }, + { + "code": -32002, + "message": "Service is up", + "data": "service \"redis\" is up" + } + ] + }, + { + "name": "service_kill", + "description": "Sends a signal to a running service", + "params": [ + { + "name": "name", + "description": "The name of the service to send the signal to", + "required": true, + "schema": { + "type": "string" + } + }, + { + "name": "signal", + "description": "The signal to send (e.g., SIGTERM, SIGKILL)", + "required": true, + "schema": { + "type": "string" + } + } + ], + "result": { + "name": "KillResult", + "description": "Result of the kill operation", + "schema": { + "type": "null" + } + }, + "examples": [ + { + "name": "Send SIGTERM to redis service", + "params": [ + { + "name": "name", + "value": "redis" + }, + { + "name": "signal", + "value": "SIGTERM" + } + ], + "result": { + "name": "KillResult", + "value": null + } + } + ], + "errors": [ + { + "code": -32000, + "message": "Service not found", + "data": "service name \"unknown\" unknown" + }, + { + "code": -32003, + "message": "Service is down", + "data": "service \"redis\" is down" + }, + { + "code": -32004, + "message": "Invalid signal", + "data": "invalid signal: INVALID" + } + ] + }, + { + "name": "system_shutdown", + "description": "Stops all services and powers off the system", + "params": [], + "result": { + "name": "ShutdownResult", + "description": "Result of the shutdown operation", + "schema": { + "type": "null" + } + }, + "examples": [ + { + "name": "Shutdown the system", + "params": [], + "result": { + "name": "ShutdownResult", + "value": null + } + } + ], + "errors": [ + { + "code": -32006, + "message": "Shutting down", + "data": "system is already shutting down" + } + ] + }, + { + "name": "system_reboot", + "description": "Stops all services and reboots the system", + "params": [], + "result": { + "name": "RebootResult", + "description": "Result of the reboot operation", + "schema": { + "type": "null" + } + }, + "examples": [ + { + "name": "Reboot the system", + "params": [], + "result": { + "name": "RebootResult", + "value": null + } + } + ], + "errors": [ + { + "code": -32006, + "message": "Shutting down", + "data": "system is already shutting down" + } + ] + }, + { + "name": "service_create", + "description": "Creates a new service configuration file", + "params": [ + { + "name": "name", + "description": "The name of the service to create", + "required": true, + "schema": { + "type": "string" + } + }, + { + "name": "content", + "description": "The service configuration content", + "required": true, + "schema": { + "type": "object", + "properties": { + "exec": { + "type": "string", + "description": "Command to run" + }, + "oneshot": { + "type": "boolean", + "description": "Whether the service should be restarted" + }, + "after": { + "type": "array", + "items": { + "type": "string" + }, + "description": "Services that must be running before this one starts" + }, + "log": { + "type": "string", + "enum": ["null", "ring", "stdout"], + "description": "How to handle service output" + }, + "env": { + "type": "object", + "additionalProperties": { + "type": "string" + }, + "description": "Environment variables for the service" + }, + "shutdown_timeout": { + "type": "integer", + "description": "Maximum time to wait for service to stop during shutdown" + } + } + } + } + ], + "result": { + "name": "CreateServiceResult", + "description": "Result of the create operation", + "schema": { + "type": "string" + } + }, + "errors": [ + { + "code": -32007, + "message": "Service already exists", + "data": "Service 'name' already exists" + }, + { + "code": -32008, + "message": "Service file error", + "data": "Failed to create service file" + } + ] + }, + { + "name": "service_delete", + "description": "Deletes a service configuration file", + "params": [ + { + "name": "name", + "description": "The name of the service to delete", + "required": true, + "schema": { + "type": "string" + } + } + ], + "result": { + "name": "DeleteServiceResult", + "description": "Result of the delete operation", + "schema": { + "type": "string" + } + }, + "errors": [ + { + "code": -32000, + "message": "Service not found", + "data": "Service 'name' not found" + }, + { + "code": -32008, + "message": "Service file error", + "data": "Failed to delete service file" + } + ] + }, + { + "name": "service_get", + "description": "Gets a service configuration file", + "params": [ + { + "name": "name", + "description": "The name of the service to get", + "required": true, + "schema": { + "type": "string" + } + } + ], + "result": { + "name": "GetServiceResult", + "description": "The service configuration", + "schema": { + "type": "object" + } + }, + "errors": [ + { + "code": -32000, + "message": "Service not found", + "data": "Service 'name' not found" + }, + { + "code": -32008, + "message": "Service file error", + "data": "Failed to read service file" + } + ] + }, + { + "name": "service_stats", + "description": "Get memory and CPU usage statistics for a service", + "params": [ + { + "name": "name", + "description": "The name of the service to get stats for", + "required": true, + "schema": { + "type": "string" + } + } + ], + "result": { + "name": "ServiceStats", + "description": "Memory and CPU usage statistics for the service", + "schema": { + "type": "object", + "properties": { + "name": { + "type": "string", + "description": "Service name" + }, + "pid": { + "type": "integer", + "description": "Process ID of the service" + }, + "memory_usage": { + "type": "integer", + "description": "Memory usage in bytes" + }, + "cpu_usage": { + "type": "number", + "description": "CPU usage as a percentage (0-100)" + }, + "children": { + "type": "array", + "description": "Stats for child processes", + "items": { + "type": "object", + "properties": { + "pid": { + "type": "integer", + "description": "Process ID of the child process" + }, + "memory_usage": { + "type": "integer", + "description": "Memory usage in bytes" + }, + "cpu_usage": { + "type": "number", + "description": "CPU usage as a percentage (0-100)" + } + } + } + } + } + } + }, + "examples": [ + { + "name": "Get stats for redis service", + "params": [ + { + "name": "name", + "value": "redis" + } + ], + "result": { + "name": "ServiceStatsResult", + "value": { + "name": "redis", + "pid": 1234, + "memory_usage": 10485760, + "cpu_usage": 2.5, + "children": [ + { + "pid": 1235, + "memory_usage": 5242880, + "cpu_usage": 1.2 + } + ] + } + } + } + ], + "errors": [ + { + "code": -32000, + "message": "Service not found", + "data": "service name \"unknown\" unknown" + }, + { + "code": -32003, + "message": "Service is down", + "data": "service \"redis\" is down" + } + ] + }, + { + "name": "system_start_http_server", + "description": "Start an HTTP/RPC server at the specified address", + "params": [ + { + "name": "address", + "description": "The network address to bind the server to (e.g., '127.0.0.1:8080')", + "required": true, + "schema": { + "type": "string" + } + } + ], + "result": { + "name": "StartHttpServerResult", + "description": "Result of the start HTTP server operation", + "schema": { + "type": "string" + } + }, + "examples": [ + { + "name": "Start HTTP server on localhost:8080", + "params": [ + { + "name": "address", + "value": "127.0.0.1:8080" + } + ], + "result": { + "name": "StartHttpServerResult", + "value": "HTTP server started at 127.0.0.1:8080" + } + } + ], + "errors": [ + { + "code": -32602, + "message": "Invalid address", + "data": "Invalid network address format" + } + ] + }, + { + "name": "system_stop_http_server", + "description": "Stop the HTTP/RPC server if running", + "params": [], + "result": { + "name": "StopHttpServerResult", + "description": "Result of the stop HTTP server operation", + "schema": { + "type": "null" + } + }, + "examples": [ + { + "name": "Stop the HTTP server", + "params": [], + "result": { + "name": "StopHttpServerResult", + "value": null + } + } + ], + "errors": [ + { + "code": -32602, + "message": "Server not running", + "data": "No HTTP server is currently running" + } + ] + }, + { + "name": "stream_currentLogs", + "description": "Get current logs from zinit and monitored services", + "params": [ + { + "name": "name", + "description": "Optional service name filter. If provided, only logs from this service will be returned", + "required": false, + "schema": { + "type": "string" + } + } + ], + "result": { + "name": "LogsResult", + "description": "Array of log strings", + "schema": { + "type": "array", + "items": { + "type": "string" + } + } + }, + "examples": [ + { + "name": "Get all logs", + "params": [], + "result": { + "name": "LogsResult", + "value": [ + "2023-01-01T12:00:00 redis: Starting service", + "2023-01-01T12:00:01 nginx: Starting service" + ] + } + }, + { + "name": "Get logs for a specific service", + "params": [ + { + "name": "name", + "value": "redis" + } + ], + "result": { + "name": "LogsResult", + "value": [ + "2023-01-01T12:00:00 redis: Starting service", + "2023-01-01T12:00:02 redis: Service started" + ] + } + } + ] + }, + { + "name": "stream_subscribeLogs", + "description": "Subscribe to log messages generated by zinit and monitored services", + "params": [ + { + "name": "name", + "description": "Optional service name filter. If provided, only logs from this service will be returned", + "required": false, + "schema": { + "type": "string" + } + } + ], + "result": { + "name": "LogSubscription", + "description": "A subscription to log messages", + "schema": { + "type": "string" + } + }, + "examples": [ + { + "name": "Subscribe to all logs", + "params": [], + "result": { + "name": "LogSubscription", + "value": "2023-01-01T12:00:00 redis: Service started" + } + }, + { + "name": "Subscribe to filtered logs", + "params": [ + { + "name": "name", + "value": "redis" + } + ], + "result": { + "name": "LogSubscription", + "value": "2023-01-01T12:00:00 redis: Service started" + } + } + ] + } + ] +} \ No newline at end of file