├── .cargo └── config.toml ├── .github └── workflows │ ├── release.yaml │ └── rust.yml ├── .gitignore ├── Cargo.lock ├── Cargo.toml ├── LICENSE ├── Makefile ├── README.md ├── docker └── Dockerfile ├── docs ├── cmd.md ├── installation.md ├── osx_cross_compile.md ├── services.md ├── shutdown_improvement_plan.md └── stats.md ├── example └── example.sh ├── install.sh ├── install_run.sh ├── openrpc.json ├── osx_build.sh ├── release_zinit.sh ├── src ├── app │ ├── api.rs │ ├── mod.rs │ └── rpc.rs ├── bin │ └── testapp.rs ├── lib.rs ├── main.rs ├── manager │ ├── buffer.rs │ └── mod.rs ├── testapp │ ├── main.rs │ └── mod.rs └── zinit │ ├── config.rs │ ├── errors.rs │ ├── lifecycle.rs │ ├── mod.rs │ ├── ord.rs │ ├── service.rs │ ├── state.rs │ └── types.rs ├── stop.sh ├── zinit-client ├── Cargo.toml ├── README.md ├── examples │ ├── basic_usage.rs │ └── http_client.rs ├── src │ └── lib.rs └── tests │ └── integration_test.rs └── zinit.json /.cargo/config.toml: -------------------------------------------------------------------------------- 1 | [target.aarch64-unknown-linux-musl] 2 | linker = "aarch64-linux-musl-gcc" 3 | -------------------------------------------------------------------------------- /.github/workflows/release.yaml: -------------------------------------------------------------------------------- 1 | on: 2 | push: 3 | # Sequence of patterns matched against refs/tags 4 | tags: 5 | - "v*" # Push events to matching v*, i.e. v1.0, v20.15.10 6 | 7 | name: Create Release 8 | 9 | jobs: 10 | build: 11 | name: Build and Release 12 | runs-on: ${{ matrix.os }} 13 | permissions: 14 | contents: write 15 | strategy: 16 | fail-fast: false # Continue with other builds if one fails 17 | matrix: 18 | include: 19 | # Linux builds 20 | - os: ubuntu-latest 21 | target: x86_64-unknown-linux-musl 22 | binary_name: zinit-linux-x86_64 23 | # macOS builds 24 | - os: macos-latest 25 | target: x86_64-apple-darwin 26 | binary_name: zinit-macos-x86_64 27 | - os: macos-latest 28 | target: aarch64-apple-darwin 29 | binary_name: zinit-macos-aarch64 30 | steps: 31 | - name: Checkout code 32 | uses: actions/checkout@v4 33 | with: 34 | fetch-depth: 0 # Fetch all history for proper versioning 35 | 36 | # Cache Rust dependencies 37 | - name: Cache Rust dependencies 38 | uses: actions/cache@v3 39 | with: 40 | path: | 41 | ~/.cargo/registry 42 | ~/.cargo/git 43 | target 44 | key: ${{ runner.os }}-${{ matrix.target }}-cargo-${{ hashFiles('**/Cargo.lock') }} 45 | restore-keys: | 46 | ${{ runner.os }}-${{ matrix.target }}-cargo- 47 | 48 | - name: Setup build environment (macOS) 49 | if: matrix.os == 'macos-latest' 50 | run: | 51 | # Install required build tools for macOS 52 | brew install llvm 53 | 54 | # For cross-compilation to Apple Silicon when on Intel 55 | if [[ "${{ matrix.target }}" == "aarch64-apple-darwin" ]]; then 56 | rustup target add aarch64-apple-darwin 57 | fi 58 | 59 | - name: Install Rust toolchain 60 | uses: actions-rs/toolchain@v1 61 | with: 62 | toolchain: stable 63 | target: ${{ matrix.target }} 64 | override: true 65 | profile: minimal # Minimal components for faster installation 66 | 67 | - name: Install MUSL tools (Linux) 68 | if: matrix.os == 'ubuntu-latest' && contains(matrix.target, 'musl') 69 | run: | 70 | sudo apt-get update 71 | sudo apt-get install -y musl-tools musl-dev 72 | 73 | - name: Build release 74 | env: 75 | CC: ${{ matrix.os == 'macos-latest' && 'clang' || '' }} 76 | CXX: ${{ matrix.os == 'macos-latest' && 'clang++' || '' }} 77 | MACOSX_DEPLOYMENT_TARGET: '10.12' 78 | run: | 79 | # Add special flags for Apple Silicon builds 80 | if [[ "${{ matrix.target }}" == "aarch64-apple-darwin" ]]; then 81 | export RUSTFLAGS="-C target-feature=+crt-static" 82 | fi 83 | 84 | cargo build --release --target=${{ matrix.target }} --verbose 85 | 86 | # Verify binary exists 87 | if [ ! -f "target/${{ matrix.target }}/release/zinit" ]; then 88 | echo "::error::Binary not found at target/${{ matrix.target }}/release/zinit" 89 | exit 1 90 | fi 91 | 92 | - name: Strip binary (Linux) 93 | if: matrix.os == 'ubuntu-latest' 94 | run: | 95 | strip target/${{ matrix.target }}/release/zinit 96 | 97 | - name: Strip binary (macOS) 98 | if: matrix.os == 'macos-latest' 99 | run: | 100 | strip -x target/${{ matrix.target }}/release/zinit || true 101 | 102 | - name: Rename binary 103 | run: | 104 | cp target/${{ matrix.target }}/release/zinit ${{ matrix.binary_name }} 105 | 106 | # Verify binary was copied successfully 107 | if [ ! -f "${{ matrix.binary_name }}" ]; then 108 | echo "::error::Binary not copied successfully to ${{ matrix.binary_name }}" 109 | exit 1 110 | fi 111 | 112 | # Show binary info for debugging 113 | echo "Binary details for ${{ matrix.binary_name }}:" 114 | ls -la ${{ matrix.binary_name }} 115 | file ${{ matrix.binary_name }} || true 116 | 117 | # Upload artifacts even if the release step fails 118 | - name: Upload artifacts 119 | uses: actions/upload-artifact@v4 120 | with: 121 | name: ${{ matrix.binary_name }} 122 | path: ${{ matrix.binary_name }} 123 | retention-days: 5 124 | 125 | - name: Upload Release Assets 126 | uses: softprops/action-gh-release@v2 127 | with: 128 | files: ${{ matrix.binary_name }} 129 | name: Release ${{ github.ref_name }} 130 | draft: false 131 | prerelease: false 132 | fail_on_unmatched_files: false 133 | env: 134 | GITHUB_TOKEN: ${{ secrets.GITHUB_TOKEN }} 135 | -------------------------------------------------------------------------------- /.github/workflows/rust.yml: -------------------------------------------------------------------------------- 1 | name: Rust 2 | 3 | on: [push] 4 | 5 | jobs: 6 | build: 7 | runs-on: ubuntu-latest 8 | steps: 9 | - uses: actions/checkout@v1 10 | name: Checkout code 11 | with: 12 | fetch-depth: 1 13 | - uses: actions-rs/toolchain@v1 14 | name: Install toolchain 15 | with: 16 | toolchain: stable 17 | target: x86_64-unknown-linux-musl 18 | - uses: actions-rs/cargo@v1 19 | name: Check formatting 20 | with: 21 | command: fmt 22 | args: -- --check 23 | - uses: actions-rs/cargo@v1 24 | name: Run tests (ahm!) 25 | with: 26 | command: test 27 | args: --verbose 28 | - uses: actions-rs/cargo@v1 29 | name: Run clippy 30 | with: 31 | command: clippy 32 | - uses: actions-rs/cargo@v1 33 | name: Build 34 | with: 35 | command: build 36 | args: --verbose 37 | -------------------------------------------------------------------------------- /.gitignore: -------------------------------------------------------------------------------- 1 | /target 2 | **/*.rs.bk -------------------------------------------------------------------------------- /Cargo.toml: -------------------------------------------------------------------------------- 1 | [workspace] 2 | members = [ 3 | ".", 4 | "zinit-client" 5 | ] 6 | 7 | [package] 8 | name = "zinit" 9 | version = "0.2.0" 10 | edition = "2018" 11 | 12 | # See more keys and their definitions at https://doc.rust-lang.org/cargo/reference/manifest.html 13 | 14 | [dependencies] 15 | anyhow = "1.0" 16 | tokio = { version = "1.44.1", features = ["full"] } 17 | tokio-stream = { version = "0.1.17", features = ["sync"] } 18 | shlex ="1.1" 19 | nix = "0.22.1" 20 | serde = { version = "1.0", features = ["derive"] } 21 | serde_yaml = "0.8" 22 | serde_json = "1.0" 23 | fern = "0.6" 24 | log = "0.4" 25 | thiserror = "1.0" 26 | clap = "2.33" 27 | git-version = "0.3.5" 28 | command-group = "1.0.8" 29 | dirs = "5.0" 30 | hyper = "1.6" 31 | # axum = { version = "0.7.4", features = ["http1"] } 32 | bytes = "1.0" 33 | jsonrpsee = { version = "0.24.9", features = ["server", "client", "macros"] } 34 | memchr = "2.5.0" 35 | async-trait = "0.1.88" 36 | reth-ipc = { git = "https://github.com/paradigmxyz/reth", package = "reth-ipc" } 37 | tower-http = { version = "0.5", features = ["cors"] } 38 | tower = "0.4" 39 | sysinfo = "0.29.10" 40 | 41 | [dev-dependencies] 42 | tokio = { version = "1.14.0", features = ["full", "test-util"] } 43 | tempfile = "3.3.0" 44 | [lib] 45 | name = "zinit" 46 | path = "src/lib.rs" 47 | 48 | [[bin]] 49 | name = "zinit" 50 | path = "src/main.rs" 51 | -------------------------------------------------------------------------------- /LICENSE: -------------------------------------------------------------------------------- 1 | Apache License 2 | Version 2.0, January 2004 3 | http://www.apache.org/licenses/ 4 | 5 | TERMS AND CONDITIONS FOR USE, REPRODUCTION, AND DISTRIBUTION 6 | 7 | 1. Definitions. 8 | 9 | "License" shall mean the terms and conditions for use, reproduction, 10 | and distribution as defined by Sections 1 through 9 of this document. 11 | 12 | "Licensor" shall mean the copyright owner or entity authorized by 13 | the copyright owner that is granting the License. 14 | 15 | "Legal Entity" shall mean the union of the acting entity and all 16 | other entities that control, are controlled by, or are under common 17 | control with that entity. For the purposes of this definition, 18 | "control" means (i) the power, direct or indirect, to cause the 19 | direction or management of such entity, whether by contract or 20 | otherwise, or (ii) ownership of fifty percent (50%) or more of the 21 | outstanding shares, or (iii) beneficial ownership of such entity. 22 | 23 | "You" (or "Your") shall mean an individual or Legal Entity 24 | exercising permissions granted by this License. 25 | 26 | "Source" form shall mean the preferred form for making modifications, 27 | including but not limited to software source code, documentation 28 | source, and configuration files. 29 | 30 | "Object" form shall mean any form resulting from mechanical 31 | transformation or translation of a Source form, including but 32 | not limited to compiled object code, generated documentation, 33 | and conversions to other media types. 34 | 35 | "Work" shall mean the work of authorship, whether in Source or 36 | Object form, made available under the License, as indicated by a 37 | copyright notice that is included in or attached to the work 38 | (an example is provided in the Appendix below). 39 | 40 | "Derivative Works" shall mean any work, whether in Source or Object 41 | form, that is based on (or derived from) the Work and for which the 42 | editorial revisions, annotations, elaborations, or other modifications 43 | represent, as a whole, an original work of authorship. For the purposes 44 | of this License, Derivative Works shall not include works that remain 45 | separable from, or merely link (or bind by name) to the interfaces of, 46 | the Work and Derivative Works thereof. 47 | 48 | "Contribution" shall mean any work of authorship, including 49 | the original version of the Work and any modifications or additions 50 | to that Work or Derivative Works thereof, that is intentionally 51 | submitted to Licensor for inclusion in the Work by the copyright owner 52 | or by an individual or Legal Entity authorized to submit on behalf of 53 | the copyright owner. For the purposes of this definition, "submitted" 54 | means any form of electronic, verbal, or written communication sent 55 | to the Licensor or its representatives, including but not limited to 56 | communication on electronic mailing lists, source code control systems, 57 | and issue tracking systems that are managed by, or on behalf of, the 58 | Licensor for the purpose of discussing and improving the Work, but 59 | excluding communication that is conspicuously marked or otherwise 60 | designated in writing by the copyright owner as "Not a Contribution." 61 | 62 | "Contributor" shall mean Licensor and any individual or Legal Entity 63 | on behalf of whom a Contribution has been received by Licensor and 64 | subsequently incorporated within the Work. 65 | 66 | 2. Grant of Copyright License. Subject to the terms and conditions of 67 | this License, each Contributor hereby grants to You a perpetual, 68 | worldwide, non-exclusive, no-charge, royalty-free, irrevocable 69 | copyright license to reproduce, prepare Derivative Works of, 70 | publicly display, publicly perform, sublicense, and distribute the 71 | Work and such Derivative Works in Source or Object form. 72 | 73 | 3. Grant of Patent License. Subject to the terms and conditions of 74 | this License, each Contributor hereby grants to You a perpetual, 75 | worldwide, non-exclusive, no-charge, royalty-free, irrevocable 76 | (except as stated in this section) patent license to make, have made, 77 | use, offer to sell, sell, import, and otherwise transfer the Work, 78 | where such license applies only to those patent claims licensable 79 | by such Contributor that are necessarily infringed by their 80 | Contribution(s) alone or by combination of their Contribution(s) 81 | with the Work to which such Contribution(s) was submitted. If You 82 | institute patent litigation against any entity (including a 83 | cross-claim or counterclaim in a lawsuit) alleging that the Work 84 | or a Contribution incorporated within the Work constitutes direct 85 | or contributory patent infringement, then any patent licenses 86 | granted to You under this License for that Work shall terminate 87 | as of the date such litigation is filed. 88 | 89 | 4. Redistribution. You may reproduce and distribute copies of the 90 | Work or Derivative Works thereof in any medium, with or without 91 | modifications, and in Source or Object form, provided that You 92 | meet the following conditions: 93 | 94 | (a) You must give any other recipients of the Work or 95 | Derivative Works a copy of this License; and 96 | 97 | (b) You must cause any modified files to carry prominent notices 98 | stating that You changed the files; and 99 | 100 | (c) You must retain, in the Source form of any Derivative Works 101 | that You distribute, all copyright, patent, trademark, and 102 | attribution notices from the Source form of the Work, 103 | excluding those notices that do not pertain to any part of 104 | the Derivative Works; and 105 | 106 | (d) If the Work includes a "NOTICE" text file as part of its 107 | distribution, then any Derivative Works that You distribute must 108 | include a readable copy of the attribution notices contained 109 | within such NOTICE file, excluding those notices that do not 110 | pertain to any part of the Derivative Works, in at least one 111 | of the following places: within a NOTICE text file distributed 112 | as part of the Derivative Works; within the Source form or 113 | documentation, if provided along with the Derivative Works; or, 114 | within a display generated by the Derivative Works, if and 115 | wherever such third-party notices normally appear. The contents 116 | of the NOTICE file are for informational purposes only and 117 | do not modify the License. You may add Your own attribution 118 | notices within Derivative Works that You distribute, alongside 119 | or as an addendum to the NOTICE text from the Work, provided 120 | that such additional attribution notices cannot be construed 121 | as modifying the License. 122 | 123 | You may add Your own copyright statement to Your modifications and 124 | may provide additional or different license terms and conditions 125 | for use, reproduction, or distribution of Your modifications, or 126 | for any such Derivative Works as a whole, provided Your use, 127 | reproduction, and distribution of the Work otherwise complies with 128 | the conditions stated in this License. 129 | 130 | 5. Submission of Contributions. Unless You explicitly state otherwise, 131 | any Contribution intentionally submitted for inclusion in the Work 132 | by You to the Licensor shall be under the terms and conditions of 133 | this License, without any additional terms or conditions. 134 | Notwithstanding the above, nothing herein shall supersede or modify 135 | the terms of any separate license agreement you may have executed 136 | with Licensor regarding such Contributions. 137 | 138 | 6. Trademarks. This License does not grant permission to use the trade 139 | names, trademarks, service marks, or product names of the Licensor, 140 | except as required for reasonable and customary use in describing the 141 | origin of the Work and reproducing the content of the NOTICE file. 142 | 143 | 7. Disclaimer of Warranty. Unless required by applicable law or 144 | agreed to in writing, Licensor provides the Work (and each 145 | Contributor provides its Contributions) on an "AS IS" BASIS, 146 | WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or 147 | implied, including, without limitation, any warranties or conditions 148 | of TITLE, NON-INFRINGEMENT, MERCHANTABILITY, or FITNESS FOR A 149 | PARTICULAR PURPOSE. You are solely responsible for determining the 150 | appropriateness of using or redistributing the Work and assume any 151 | risks associated with Your exercise of permissions under this License. 152 | 153 | 8. Limitation of Liability. In no event and under no legal theory, 154 | whether in tort (including negligence), contract, or otherwise, 155 | unless required by applicable law (such as deliberate and grossly 156 | negligent acts) or agreed to in writing, shall any Contributor be 157 | liable to You for damages, including any direct, indirect, special, 158 | incidental, or consequential damages of any character arising as a 159 | result of this License or out of the use or inability to use the 160 | Work (including but not limited to damages for loss of goodwill, 161 | work stoppage, computer failure or malfunction, or any and all 162 | other commercial damages or losses), even if such Contributor 163 | has been advised of the possibility of such damages. 164 | 165 | 9. Accepting Warranty or Additional Liability. While redistributing 166 | the Work or Derivative Works thereof, You may choose to offer, 167 | and charge a fee for, acceptance of support, warranty, indemnity, 168 | or other liability obligations and/or rights consistent with this 169 | License. However, in accepting such obligations, You may act only 170 | on Your own behalf and on Your sole responsibility, not on behalf 171 | of any other Contributor, and only if You agree to indemnify, 172 | defend, and hold each Contributor harmless for any liability 173 | incurred by, or claims asserted against, such Contributor by reason 174 | of your accepting any such warranty or additional liability. 175 | 176 | END OF TERMS AND CONDITIONS 177 | 178 | APPENDIX: How to apply the Apache License to your work. 179 | 180 | To apply the Apache License to your work, attach the following 181 | boilerplate notice, with the fields enclosed by brackets "[]" 182 | replaced with your own identifying information. (Don't include 183 | the brackets!) The text should be enclosed in the appropriate 184 | comment syntax for the file format. We also recommend that a 185 | file or class name and description of purpose be included on the 186 | same "printed page" as the copyright notice for easier 187 | identification within third-party archives. 188 | 189 | Copyright TF TECH NV (Belgium) 190 | 191 | Licensed under the Apache License, Version 2.0 (the "License"); 192 | you may not use this file except in compliance with the License. 193 | You may obtain a copy of the License at 194 | 195 | http://www.apache.org/licenses/LICENSE-2.0 196 | 197 | Unless required by applicable law or agreed to in writing, software 198 | distributed under the License is distributed on an "AS IS" BASIS, 199 | WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. 200 | See the License for the specific language governing permissions and 201 | limitations under the License. 202 | -------------------------------------------------------------------------------- /Makefile: -------------------------------------------------------------------------------- 1 | default: release 2 | 3 | docker: release 4 | docker build -f docker/Dockerfile -t zinit-ubuntu:18.04 target/x86_64-unknown-linux-musl/release 5 | 6 | prepare: 7 | rustup target add x86_64-unknown-linux-musl 8 | 9 | release: prepare 10 | cargo build --release --target=x86_64-unknown-linux-musl 11 | 12 | release-aarch64-musl: prepare-aarch64-musl 13 | cargo build --release --target=aarch64-unknown-linux-musl 14 | 15 | prepare-aarch64-musl: 16 | rustup target add aarch64-unknown-linux-musl 17 | 18 | # Build for macOS (both Intel and Apple Silicon) 19 | release-macos: 20 | cargo build --release 21 | 22 | # Install to ~/hero/bin (if it exists) 23 | install-macos: release-macos 24 | @if [ -d ~/hero/bin ]; then \ 25 | cp target/release/zinit ~/hero/bin; \ 26 | echo "Installed zinit to ~/hero/bin"; \ 27 | else \ 28 | echo "~/hero/bin directory not found. Please create it or specify a different installation path."; \ 29 | exit 1; \ 30 | fi 31 | -------------------------------------------------------------------------------- /README.md: -------------------------------------------------------------------------------- 1 | # Zinit [![Rust](https://github.com/threefoldtech/zinit/actions/workflows/rust.yml/badge.svg)](https://github.com/threefoldtech/zinit/actions/workflows/rust.yml) 2 | 3 | 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. 4 | 5 | ### Key Features 6 | 7 | - **Service Management**: Ensures configured services are up and running at all times 8 | - **Dependency Handling**: Supports service dependencies for proper startup ordering 9 | - **Simple Control Interface**: Provides an intuitive CLI to add, start, stop, and monitor services 10 | - **Container Support**: Can run in container mode with appropriate signal handling 11 | - **Configurable Logging**: Multiple logging options including ringbuffer and stdout 12 | 13 | ## Installation 14 | 15 | ```bash 16 | curl https://raw.githubusercontent.com/threefoldtech/zinit/refs/heads/master/install.sh | bash 17 | 18 | #to install & run 19 | curl https://raw.githubusercontent.com/threefoldtech/zinit/refs/heads/master/install_run.sh | bash 20 | ``` 21 | 22 | Click [here](docs/installation.md) for more information on how to install Zinit. 23 | 24 | ## Usage 25 | 26 | ### Process Manager (zinit) 27 | 28 | ```bash 29 | # Run zinit in init mode 30 | zinit init --config /etc/zinit/ --socket /var/run/zinit.sock 31 | 32 | # List services 33 | zinit list 34 | 35 | # Start a service 36 | zinit start 37 | 38 | # Stop a service 39 | zinit stop 40 | ``` 41 | 42 | ```bash 43 | # Start the HTTP proxy on the default port (8080) 44 | zinit proxy 45 | ``` 46 | 47 | More information about all the available commands can be found [here](docs/cmd.md). 48 | 49 | ### Service Configuration 50 | 51 | Zinit uses YAML files for service configuration. Here's a basic example: 52 | 53 | ```yaml 54 | # Service configuration (e.g., /etc/zinit/myservice.yaml) 55 | exec: "/usr/bin/myservice --option value" # Command to run (required) 56 | test: "/usr/bin/check-myservice" # Health check command (optional) 57 | oneshot: false # Whether to restart on exit (default: false) 58 | after: # Services that must be running first (optional) 59 | - dependency1 60 | - dependency2 61 | ``` 62 | 63 | For more information on how to configure service files, see the [service file reference](docs/services.md) documentation. 64 | 65 | ### JSON-RPC API 66 | 67 | 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: 68 | 69 | ```bash 70 | curl -X POST -H "Content-Type: application/json" -d '{"jsonrpc":"2.0","id":1,"method":"service_list","params":{}}' http://localhost:8080/ 71 | ``` 72 | 73 | See the [OpenRPC specs](openrpc.json) for more information about available RPC calls to interact with Zinit. 74 | 75 | ## License 76 | 77 | See [LICENSE](LICENSE) file for details. 78 | -------------------------------------------------------------------------------- /docker/Dockerfile: -------------------------------------------------------------------------------- 1 | FROM ubuntu:18.04 2 | 3 | RUN mkdir -p /etc/zinit 4 | ADD zinit /sbin/zinit 5 | 6 | ENTRYPOINT ["/sbin/zinit", "init"] -------------------------------------------------------------------------------- /docs/cmd.md: -------------------------------------------------------------------------------- 1 | # Zinit Command Line Reference 2 | 3 | This document provides a comprehensive reference for all Zinit command line options and commands. 4 | 5 | ## Command Structure 6 | 7 | Zinit uses a command-based CLI with the following general structure: 8 | 9 | ```bash 10 | zinit [FLAGS] [OPTIONS] [SUBCOMMAND] 11 | ``` 12 | 13 | ## Global Flags and Options 14 | 15 | These flags and options apply to all Zinit commands: 16 | 17 | | Flag/Option | Description | 18 | |-------------|-------------| 19 | | `-d, --debug` | Run in debug mode with increased verbosity | 20 | | `-h, --help` | Display help information | 21 | | `-V, --version` | Display version information | 22 | | `-s, --socket ` | Path to Unix socket (default: `/var/run/zinit.sock`) | 23 | 24 | ## Subcommands 25 | 26 | ### Main Mode 27 | 28 | #### `init` 29 | 30 | Run Zinit in init mode, starting and maintaining configured services. 31 | 32 | ```bash 33 | zinit init [FLAGS] [OPTIONS] 34 | ``` 35 | 36 | **Flags:** 37 | - `--container`: Run in container mode, exiting on signal instead of rebooting 38 | 39 | **Options:** 40 | - `-c, --config `: Service configurations directory (default: `/etc/zinit/`) 41 | - `-b, --buffer `: Buffer size (in lines) to keep service logs (default: `2000`) 42 | 43 | **Example:** 44 | ```bash 45 | # Run in init mode with custom config directory 46 | zinit init -c /opt/services/ 47 | 48 | # Run in container mode 49 | zinit init --container 50 | ``` 51 | 52 | ### Service Management 53 | 54 | #### `list` 55 | 56 | Display a quick view of all currently known services and their status. 57 | 58 | ```bash 59 | zinit list 60 | ``` 61 | 62 | **Output:** 63 | A JSON object with service names as keys and their status as values. 64 | 65 | **Example:** 66 | ```bash 67 | # List all services 68 | zinit list 69 | ``` 70 | 71 | #### `status` 72 | 73 | Show detailed status information for a specific service. 74 | 75 | ```bash 76 | zinit status 77 | ``` 78 | 79 | **Arguments:** 80 | - ``: Name of the service to show status for 81 | 82 | **Example:** 83 | ```bash 84 | # Check status of redis service 85 | zinit status redis 86 | ``` 87 | 88 | #### `start` 89 | 90 | Start a service. Has no effect if the service is already running. 91 | 92 | ```bash 93 | zinit start 94 | ``` 95 | 96 | **Arguments:** 97 | - ``: Name of the service to start 98 | 99 | **Example:** 100 | ```bash 101 | # Start the nginx service 102 | zinit start nginx 103 | ``` 104 | 105 | #### `stop` 106 | 107 | Stop a service. Sets the target state to "down" and sends the stop signal. 108 | 109 | ```bash 110 | zinit stop 111 | ``` 112 | 113 | **Arguments:** 114 | - ``: Name of the service to stop 115 | 116 | **Example:** 117 | ```bash 118 | # Stop the redis service 119 | zinit stop redis 120 | ``` 121 | 122 | #### `restart` 123 | 124 | Restart a service. If it fails to stop, it will be killed and then started again. 125 | 126 | ```bash 127 | zinit restart 128 | ``` 129 | 130 | **Arguments:** 131 | - ``: Name of the service to restart 132 | 133 | **Example:** 134 | ```bash 135 | # Restart the web service 136 | zinit restart web 137 | ``` 138 | 139 | #### `monitor` 140 | 141 | Start monitoring a service. The configuration is loaded from the server's config directory. 142 | 143 | ```bash 144 | zinit monitor 145 | ``` 146 | 147 | **Arguments:** 148 | - ``: Name of the service to monitor 149 | 150 | **Example:** 151 | ```bash 152 | # Monitor the database service 153 | zinit monitor database 154 | ``` 155 | 156 | #### `forget` 157 | 158 | Remove a service from monitoring. You can only forget a stopped service. 159 | 160 | ```bash 161 | zinit forget 162 | ``` 163 | 164 | **Arguments:** 165 | - ``: Name of the service to forget 166 | 167 | **Example:** 168 | ```bash 169 | # Forget the backup service 170 | zinit forget backup 171 | ``` 172 | 173 | #### `kill` 174 | 175 | Send a signal to a running service. 176 | 177 | ```bash 178 | zinit kill 179 | ``` 180 | 181 | **Arguments:** 182 | - ``: Name of the service to send signal to 183 | - ``: Signal name (e.g., SIGTERM, SIGKILL, SIGINT) 184 | 185 | **Example:** 186 | ```bash 187 | # Send SIGTERM to the redis service 188 | zinit kill redis SIGTERM 189 | 190 | # Send SIGKILL to force terminate a service 191 | zinit kill stuck-service SIGKILL 192 | ``` 193 | 194 | ### System Operations 195 | 196 | #### `shutdown` 197 | 198 | Stop all services in dependency order and power off the system. 199 | 200 | ```bash 201 | zinit shutdown 202 | ``` 203 | 204 | **Example:** 205 | ```bash 206 | # Shutdown the system 207 | zinit shutdown 208 | ``` 209 | 210 | #### `reboot` 211 | 212 | Stop all services in dependency order and reboot the system. 213 | 214 | ```bash 215 | zinit reboot 216 | ``` 217 | 218 | **Example:** 219 | ```bash 220 | # Reboot the system 221 | zinit reboot 222 | ``` 223 | 224 | ### Logging 225 | 226 | #### `log` 227 | 228 | View service logs from the Zinit ring buffer. 229 | 230 | ```bash 231 | zinit log [FLAGS] [FILTER] 232 | ``` 233 | 234 | **Flags:** 235 | - `-s, --snapshot`: If set, log prints current buffer without following 236 | 237 | **Arguments:** 238 | - `[FILTER]`: Optional service name to filter logs for 239 | 240 | **Examples:** 241 | ```bash 242 | # View logs for all services and follow new logs 243 | zinit log 244 | 245 | # View current logs for the nginx service without following 246 | zinit log -s nginx 247 | ``` 248 | 249 | ## Exit Codes 250 | 251 | Zinit commands return the following exit codes: 252 | 253 | | Code | Description | 254 | |------|-------------| 255 | | 0 | Success | 256 | | 1 | Error (with error message printed to stderr) | 257 | -------------------------------------------------------------------------------- /docs/installation.md: -------------------------------------------------------------------------------- 1 | # Installing Zinit 2 | 3 | This guide provides detailed instructions for installing Zinit on various platforms. 4 | 5 | ## System Requirements 6 | 7 | Zinit has minimal system requirements: 8 | 9 | - Linux-based operating system 10 | - Root access (for running as init system) 11 | 12 | ## Pre-built Binaries 13 | 14 | If pre-built binaries are available for your system, you can install them directly: 15 | 16 | ```bash 17 | # Download the binary (replace with actual URL) 18 | wget https://github.com/threefoldtech/zinit/releases/download/vX.Y.Z/zinit-x86_64-unknown-linux-musl 19 | 20 | # Make it executable 21 | chmod +x zinit-x86_64-unknown-linux-musl 22 | 23 | # Move to a location in your PATH 24 | sudo mv zinit-x86_64-unknown-linux-musl /usr/local/bin/zinit 25 | ``` 26 | 27 | ## Building from Source 28 | 29 | ### Prerequisites 30 | 31 | To build Zinit from source, you'll need: 32 | 33 | - Rust toolchain (1.46.0 or later recommended) 34 | - musl and musl-tools packages 35 | - GNU Make 36 | 37 | #### Install Rust 38 | 39 | If you don't have Rust installed, use rustup: 40 | 41 | ```bash 42 | curl --proto '=https' --tlsv1.2 -sSf https://sh.rustup.rs | sh 43 | source $HOME/.cargo/env 44 | ``` 45 | 46 | #### Install musl development tools 47 | 48 | On Debian/Ubuntu: 49 | 50 | ```bash 51 | sudo apt update 52 | sudo apt install musl musl-tools 53 | ``` 54 | 55 | On Fedora: 56 | 57 | ```bash 58 | sudo dnf install musl musl-devel 59 | ``` 60 | 61 | On Alpine Linux (musl is already the default libc): 62 | 63 | ```bash 64 | apk add build-base 65 | ``` 66 | 67 | ### Build Process 68 | 69 | 1. Clone the repository: 70 | 71 | ```bash 72 | git clone https://github.com/threefoldtech/zinit.git 73 | cd zinit 74 | ``` 75 | 76 | 2. Build using make: 77 | 78 | ```bash 79 | make 80 | ``` 81 | 82 | This will create a statically linked binary at `target/x86_64-unknown-linux-musl/release/zinit`. 83 | 84 | 3. Install the binary: 85 | 86 | ```bash 87 | sudo cp target/x86_64-unknown-linux-musl/release/zinit /usr/local/bin/ 88 | ``` 89 | 90 | ### Development Build 91 | 92 | For development or debugging: 93 | 94 | ```bash 95 | make dev 96 | ``` 97 | 98 | ## Docker Installation 99 | 100 | ### Using the Provided Dockerfile 101 | 102 | Zinit includes a test Docker image: 103 | 104 | ```bash 105 | # Build the Docker image 106 | make docker 107 | 108 | # Run the container 109 | docker run -dt --device=/dev/kmsg:/dev/kmsg:rw zinit 110 | ``` 111 | > 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. 112 | 113 | ### Custom Docker Setup 114 | 115 | To create your own Dockerfile with Zinit: 116 | 117 | ```dockerfile 118 | FROM alpine:latest 119 | 120 | # Install dependencies if needed 121 | RUN apk add --no-cache bash curl 122 | 123 | # Copy the zinit binary 124 | COPY zinit /usr/local/bin/zinit 125 | RUN chmod +x /usr/local/bin/zinit 126 | 127 | # Create configuration directory 128 | RUN mkdir -p /etc/zinit 129 | 130 | # Add your service configurations 131 | COPY services/*.yaml /etc/zinit/ 132 | 133 | # Set zinit as the entrypoint 134 | ENTRYPOINT ["/usr/local/bin/zinit", "init", "--container"] 135 | ``` 136 | 137 | ## Using Zinit as the Init System 138 | 139 | To use Zinit as the init system (PID 1) on a Linux system: 140 | 141 | ### On a Standard Linux System 142 | 143 | 1. Install Zinit as described above 144 | 2. Create your service configurations in `/etc/zinit/` 145 | 3. Configure your bootloader to use zinit as init 146 | 147 | For GRUB, add `init=/usr/local/bin/zinit` to the kernel command line: 148 | 149 | ```bash 150 | # Edit GRUB configuration 151 | sudo nano /etc/default/grub 152 | 153 | # Add init parameter to GRUB_CMDLINE_LINUX 154 | # Example: 155 | # GRUB_CMDLINE_LINUX="init=/usr/local/bin/zinit" 156 | 157 | # Update GRUB 158 | sudo update-grub 159 | ``` 160 | 161 | ### In a Container Environment 162 | 163 | For containers, simply set Zinit as the entrypoint: 164 | 165 | ```bash 166 | docker run -dt --device=/dev/kmsg:/dev/kmsg:rw \ 167 | --entrypoint /usr/local/bin/zinit \ 168 | your-image init --container 169 | ``` 170 | 171 | ## First-time Setup 172 | 173 | After installation, you'll need to create a basic configuration: 174 | 175 | 1. Create the configuration directory: 176 | 177 | ```bash 178 | sudo mkdir -p /etc/zinit 179 | ``` 180 | 181 | 2. Create a simple service configuration: 182 | 183 | ```bash 184 | cat << EOF | sudo tee /etc/zinit/hello.yaml 185 | exec: "echo 'Hello from Zinit!'" 186 | oneshot: true 187 | EOF 188 | ``` 189 | 190 | 3. Test Zinit without running as init: 191 | 192 | ```bash 193 | # For testing only - doesn't replace system init 194 | sudo zinit init 195 | ``` 196 | 197 | If all is working correctly, you should see Zinit start and run your service. -------------------------------------------------------------------------------- /docs/osx_cross_compile.md: -------------------------------------------------------------------------------- 1 | # macOS Guide for Zinit 2 | 3 | This guide covers both building Zinit natively on macOS and cross-compiling from macOS to Linux targets. 4 | 5 | ## Building Zinit Natively on macOS 6 | 7 | Zinit can now be built and run directly on macOS. The code has been updated to handle platform-specific differences between Linux and macOS. 8 | 9 | ### Building for macOS 10 | 11 | ```bash 12 | # Build a release version for macOS 13 | make release-macos 14 | 15 | # Install to ~/hero/bin (if it exists) 16 | make install-macos 17 | ``` 18 | 19 | The native macOS build provides most of Zinit's functionality, with the following limitations: 20 | - System reboot and shutdown operations are not supported (they will exit the process instead) 21 | - Some Linux-specific features are disabled 22 | 23 | ## Cross-Compilation from macOS to Linux 24 | 25 | 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. 26 | 27 | ## Prerequisites 28 | 29 | * Homebrew (https://brew.sh/) installed on your macOS system. 30 | * Rust and Cargo installed (e.g., via `rustup`). 31 | 32 | ## Step 1: Install the `aarch64-linux-musl-gcc` Toolchain 33 | 34 | The `aarch64-linux-musl-gcc` toolchain is required for linking when cross-compiling to `aarch64-unknown-linux-musl`. You can install it using Homebrew: 35 | 36 | ```bash 37 | brew install messense/macos-cross-toolchains/aarch64-linux-musl-cross 38 | ``` 39 | 40 | ## Step 2: Link `musl-gcc` 41 | 42 | Some build scripts or tools might look for `musl-gcc`. To ensure compatibility, create a symbolic link: 43 | 44 | ```bash 45 | sudo ln -s /opt/homebrew/bin/aarch64-linux-musl-gcc /opt/homebrew/bin/musl-gcc 46 | ``` 47 | 48 | You might be prompted for your system password to complete this operation. 49 | 50 | ## Step 3: Add the Rust Target 51 | 52 | Add the `aarch64-unknown-linux-musl` target to your Rust toolchain: 53 | 54 | ```bash 55 | rustup target add aarch64-unknown-linux-musl 56 | ``` 57 | 58 | ## Step 4: Build Your Project 59 | 60 | Now you can build your Rust project for the `aarch64-unknown-linux-musl` target using Cargo: 61 | 62 | ```bash 63 | cargo build --release --target aarch64-unknown-linux-musl 64 | ``` 65 | 66 | Alternatively, if you are using the provided `Makefile`, you can use the new target: 67 | 68 | ```bash 69 | make release-aarch64-musl 70 | ``` 71 | 72 | This will produce a release binary located in `target/aarch64-unknown-linux-musl/release/`. 73 | 74 | ## Step 5: copy to osx hero bin 75 | 76 | ```bash 77 | cp target/aarch64-unknown-linux-musl/release/zinit ~/hero/bin 78 | ``` -------------------------------------------------------------------------------- /docs/services.md: -------------------------------------------------------------------------------- 1 | # Service Configuration Format 2 | 3 | This document describes the structure and options for Zinit service configuration files. 4 | 5 | ## File Format 6 | 7 | Zinit uses YAML files for service configuration. Each service has its own configuration file stored in the Zinit configuration directory (default: `/etc/zinit`). 8 | 9 | ### File Naming and Location 10 | 11 | - **Location**: `/etc/zinit/` (default, can be changed with `-c` flag) 12 | - on osx `~/hero/cfg/zinit` 13 | - **Naming**: `.yaml` 14 | 15 | For example: 16 | - `/etc/zinit/nginx.yaml` 17 | - `/etc/zinit/redis.yaml` 18 | 19 | ## Configuration Schema 20 | 21 | Service configuration files use the following schema: 22 | 23 | ```yaml 24 | # Command to run (required) 25 | exec: "command line to start service" 26 | 27 | # Command to test if service is running (optional) 28 | test: "command line to test service" 29 | 30 | # Whether the service should be restarted (optional, default: false) 31 | oneshot: true|false 32 | 33 | # Maximum time to wait for service to stop during shutdown (optional, default: 10) 34 | shutdown_timeout: 30 35 | 36 | # Services that must be running before this one starts (optional) 37 | after: 38 | - service1_name 39 | - service2_name 40 | 41 | # Signals configuration (optional) 42 | signal: 43 | stop: SIGKILL # signal sent on 'stop' action (default: SIGTERM) 44 | 45 | # Log handling configuration (optional, default: ring) 46 | log: null|ring|stdout 47 | 48 | # Environment variables for the service (optional) 49 | env: 50 | KEY1: "VALUE1" 51 | KEY2: "VALUE2" 52 | 53 | # Working directory for the service (optional) 54 | dir: "/path/to/working/directory" 55 | ``` 56 | 57 | ## Configuration Options 58 | 59 | ### Required Fields 60 | 61 | | Field | Description | 62 | |-------|-------------| 63 | | `exec` | Command line to execute when starting the service | 64 | 65 | ### Optional Fields 66 | 67 | | Field | Type | Default | Description | 68 | |-------|------|---------|-------------| 69 | | `test` | String | - | Command to determine if service is running | 70 | | `oneshot` | Boolean | `false` | If true, service won't be restarted after exit | 71 | | `shutdown_timeout` | Integer | 10 | Seconds to wait for service to stop during shutdown | 72 | | `after` | String[] | `[]` | List of services that must be running first | 73 | | `signal.stop` | String | `"sigterm"` | Signal to send when stopping the service | 74 | | `log` | Enum | `ring` | How to handle service output (null, ring, stdout) | 75 | | `env` | Object | `{}` | Environment variables to pass to the service | 76 | | `dir` | String | `""` | Working directory for the service | 77 | 78 | ## Field Details 79 | 80 | ### exec 81 | 82 | The command to run when starting the service. This is the only required field in the configuration. 83 | 84 | ```yaml 85 | exec: "/usr/bin/redis-server --port 6379" 86 | ``` 87 | 88 | Shell-style commands are supported: 89 | 90 | ```yaml 91 | exec: "sh -c 'echo Starting service && /usr/local/bin/myservice'" 92 | ``` 93 | 94 | ### test 95 | 96 | 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. 97 | 98 | ```yaml 99 | test: "redis-cli -p 6379 PING" 100 | ``` 101 | 102 | If no test command is provided, the service is considered running as soon as it's started. 103 | 104 | ### oneshot 105 | 106 | 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. 107 | 108 | ```yaml 109 | oneshot: true 110 | ``` 111 | 112 | Services that depend on a oneshot service will start only after the oneshot service has exited successfully. 113 | 114 | ### shutdown_timeout 115 | 116 | How long (in seconds) to wait for the service to stop during system shutdown before giving up: 117 | 118 | ```yaml 119 | shutdown_timeout: 30 # Wait up to 30 seconds 120 | ``` 121 | 122 | ### after 123 | 124 | List of service names that must be running (or completed successfully for oneshot services) before this service starts: 125 | 126 | ```yaml 127 | after: 128 | - networking 129 | - database 130 | ``` 131 | 132 | ### signal 133 | 134 | Custom signals to use for operations. Currently, only the `stop` signal is configurable: 135 | 136 | ```yaml 137 | signal: 138 | stop: SIGKILL # Use SIGKILL instead of default SIGTERM 139 | ``` 140 | 141 | Valid signal names follow the standard UNIX signal naming (SIGTERM, SIGKILL, SIGINT, etc). 142 | 143 | ### log 144 | 145 | How to handle stdout/stderr output from the service: 146 | 147 | ```yaml 148 | log: stdout # Print output to zinit's stdout 149 | ``` 150 | 151 | Options: 152 | - `null`: Ignore all service output (like redirecting to /dev/null) 153 | - `ring`: Store logs in the kernel ring buffer with service name prefix (default) 154 | - `stdout`: Send service output to zinit's stdout 155 | 156 | > **Note**: To use `ring` inside Docker, make sure to add the `kmsg` device: 157 | > ``` 158 | > docker run -dt --device=/dev/kmsg:/dev/kmsg:rw zinit 159 | > ``` 160 | 161 | ### env 162 | 163 | Additional environment variables for the service. These are added to the existing environment: 164 | 165 | ```yaml 166 | env: 167 | PORT: "8080" 168 | DEBUG: "true" 169 | NODE_ENV: "production" 170 | ``` 171 | 172 | ### dir 173 | 174 | Working directory for the service process: 175 | 176 | ```yaml 177 | dir: "/var/lib/myservice" 178 | ``` 179 | 180 | If not specified, the process inherits zinit's working directory. 181 | 182 | ## Example Configurations 183 | 184 | ### Web Server 185 | 186 | ```yaml 187 | exec: "/usr/bin/nginx -g 'daemon off;'" 188 | test: "curl -s http://localhost > /dev/null" 189 | after: 190 | - networking 191 | log: stdout 192 | ``` 193 | 194 | ### Database Initialization 195 | 196 | ```yaml 197 | exec: "sh -c 'echo Creating database schema && /usr/bin/db-migrate'" 198 | oneshot: true 199 | dir: "/opt/myapp" 200 | env: 201 | DB_HOST: "localhost" 202 | DB_USER: "admin" 203 | ``` 204 | 205 | ### Application with Dependencies 206 | 207 | ```yaml 208 | exec: "/usr/bin/myapp --config /etc/myapp.conf" 209 | test: "curl -s http://localhost:8080/health > /dev/null" 210 | after: 211 | - database 212 | - cache 213 | signal: 214 | stop: SIGINT # Use SIGINT for graceful shutdown 215 | env: 216 | PORT: "8080" 217 | shutdown_timeout: 20 -------------------------------------------------------------------------------- /docs/shutdown_improvement_plan.md: -------------------------------------------------------------------------------- 1 | # Zinit Shutdown Functionality Improvement Plan 2 | 3 | ## Current Issues 4 | 5 | 1. **Incomplete Child Process Termination**: When services are stopped, child processes may remain running. 6 | 2. **Lack of Verification**: There's no verification that all processes are actually terminated. 7 | 3. **Improper Graceful Shutdown**: Zinit doesn't wait for all processes to terminate before exiting. 8 | 9 | ## Solution Overview 10 | 11 | We'll implement a robust shutdown mechanism that: 12 | 1. Uses our stats functionality to detect all child processes 13 | 2. Properly manages process groups 14 | 3. Verifies all processes are terminated before Zinit exits 15 | 16 | ## Implementation Plan 17 | 18 | ```mermaid 19 | flowchart TD 20 | A[Enhance stop method] --> B[Improve kill_process_tree] 21 | B --> C[Add process verification] 22 | C --> D[Implement graceful shutdown] 23 | 24 | A1[Use stats to detect child processes] --> A 25 | A2[Send signals to all processes] --> A 26 | A3[Implement cascading termination] --> A 27 | 28 | B1[Ensure proper process group handling] --> B 29 | B2[Add timeout and escalation logic] --> B 30 | 31 | C1[Create verification mechanism] --> C 32 | C2[Add polling for process existence] --> C 33 | 34 | D1[Wait for all processes to terminate] --> D 35 | D2[Add cleanup of resources] --> D 36 | D3[Implement clean exit] --> D 37 | ``` 38 | 39 | ## Detailed Implementation Steps 40 | 41 | ### 1. Enhance the `stop` Method in `LifecycleManager` 42 | 43 | ```rust 44 | pub async fn stop>(&self, name: S) -> Result<()> { 45 | // Get service information 46 | let table = self.services.read().await; 47 | let service = table.get(name.as_ref()) 48 | .ok_or_else(|| ZInitError::unknown_service(name.as_ref()))?; 49 | 50 | let mut service = service.write().await; 51 | service.set_target(Target::Down); 52 | 53 | // Get the main process PID 54 | let pid = service.pid; 55 | if pid.as_raw() == 0 { 56 | return Ok(()); 57 | } 58 | 59 | // Get the signal to use 60 | let signal = signal::Signal::from_str(&service.service.signal.stop.to_uppercase()) 61 | .map_err(|err| anyhow::anyhow!("unknown stop signal: {}", err))?; 62 | 63 | // Release the lock before potentially long-running operations 64 | drop(service); 65 | drop(table); 66 | 67 | // Get all child processes using our stats functionality 68 | let children = self.get_child_process_stats(pid.as_raw()).await?; 69 | 70 | // First try to stop the process group 71 | let _ = self.pm.signal(pid, signal); 72 | 73 | // Wait a short time for processes to terminate gracefully 74 | sleep(std::time::Duration::from_millis(500)).await; 75 | 76 | // Check if processes are still running and use SIGKILL if needed 77 | self.ensure_processes_terminated(pid.as_raw(), &children).await?; 78 | 79 | Ok(()) 80 | } 81 | ``` 82 | 83 | ### 2. Add a New `ensure_processes_terminated` Method 84 | 85 | ```rust 86 | async fn ensure_processes_terminated(&self, parent_pid: i32, children: &[ProcessStats]) -> Result<()> { 87 | // Check if parent is still running 88 | let parent_running = self.is_process_running(parent_pid).await?; 89 | 90 | // If parent is still running, send SIGKILL 91 | if parent_running { 92 | debug!("Process {} still running after SIGTERM, sending SIGKILL", parent_pid); 93 | let _ = self.pm.signal(Pid::from_raw(parent_pid), signal::Signal::SIGKILL); 94 | } 95 | 96 | // Check and kill any remaining child processes 97 | for child in children { 98 | if self.is_process_running(child.pid).await? { 99 | debug!("Child process {} still running, sending SIGKILL", child.pid); 100 | let _ = signal::kill(Pid::from_raw(child.pid), signal::Signal::SIGKILL); 101 | } 102 | } 103 | 104 | // Verify all processes are gone 105 | let mut retries = 5; 106 | while retries > 0 { 107 | let mut all_terminated = true; 108 | 109 | // Check parent 110 | if self.is_process_running(parent_pid).await? { 111 | all_terminated = false; 112 | } 113 | 114 | // Check children 115 | for child in children { 116 | if self.is_process_running(child.pid).await? { 117 | all_terminated = false; 118 | break; 119 | } 120 | } 121 | 122 | if all_terminated { 123 | return Ok(()); 124 | } 125 | 126 | // Wait before retrying 127 | sleep(std::time::Duration::from_millis(100)).await; 128 | retries -= 1; 129 | } 130 | 131 | // If we get here, some processes might still be running 132 | warn!("Some processes may still be running after shutdown attempts"); 133 | Ok(()) 134 | } 135 | ``` 136 | 137 | ### 3. Add a Helper Method to Check if a Process is Running 138 | 139 | ```rust 140 | async fn is_process_running(&self, pid: i32) -> Result { 141 | // Use sysinfo to check if process exists 142 | let mut system = System::new(); 143 | let sys_pid = sysinfo::Pid::from(pid as usize); 144 | system.refresh_process(sys_pid); 145 | 146 | Ok(system.process(sys_pid).is_some()) 147 | } 148 | ``` 149 | 150 | ### 4. Improve the `kill_process_tree` Method 151 | 152 | ```rust 153 | #[cfg(target_os = "linux")] 154 | async fn kill_process_tree( 155 | &self, 156 | mut dag: ProcessDAG, 157 | mut state_channels: HashMap>, 158 | mut shutdown_timeouts: HashMap, 159 | ) -> Result<()> { 160 | let (tx, mut rx) = mpsc::unbounded_channel(); 161 | tx.send(DUMMY_ROOT.into())?; 162 | 163 | let mut count = dag.count; 164 | while let Some(name) = rx.recv().await { 165 | debug!("{} has been killed (or was inactive) adding its children", name); 166 | 167 | for child in dag.adj.get(&name).unwrap_or(&Vec::new()) { 168 | let child_indegree: &mut u32 = dag.indegree.entry(child.clone()).or_default(); 169 | *child_indegree -= 1; 170 | 171 | debug!("decrementing child {} indegree to {}", child, child_indegree); 172 | 173 | if *child_indegree == 0 { 174 | let watcher = state_channels.remove(child); 175 | if watcher.is_none() { 176 | // not an active service 177 | tx.send(child.to_string())?; 178 | continue; 179 | } 180 | 181 | let shutdown_timeout = shutdown_timeouts.remove(child); 182 | let lifecycle = self.clone_lifecycle(); 183 | 184 | // Spawn a task to kill the service and wait for it to terminate 185 | let kill_task = tokio::spawn(Self::kill_wait_enhanced( 186 | lifecycle, 187 | child.to_string(), 188 | tx.clone(), 189 | watcher.unwrap(), 190 | shutdown_timeout.unwrap_or(config::DEFAULT_SHUTDOWN_TIMEOUT), 191 | )); 192 | 193 | // Add a timeout to ensure we don't wait forever 194 | let _ = tokio::time::timeout( 195 | std::time::Duration::from_secs(shutdown_timeout.unwrap_or(config::DEFAULT_SHUTDOWN_TIMEOUT) + 2), 196 | kill_task 197 | ).await; 198 | } 199 | } 200 | 201 | count -= 1; 202 | if count == 0 { 203 | break; 204 | } 205 | } 206 | 207 | // Final verification that all processes are gone 208 | self.verify_all_processes_terminated().await?; 209 | 210 | Ok(()) 211 | } 212 | ``` 213 | 214 | ### 5. Add an Enhanced `kill_wait` Method 215 | 216 | ```rust 217 | #[cfg(target_os = "linux")] 218 | async fn kill_wait_enhanced( 219 | self, 220 | name: String, 221 | ch: mpsc::UnboundedSender, 222 | mut rx: Watcher, 223 | shutdown_timeout: u64, 224 | ) { 225 | debug!("kill_wait {}", name); 226 | 227 | // Try to stop the service gracefully 228 | let stop_result = self.stop(name.clone()).await; 229 | 230 | // Wait for the service to become inactive or timeout 231 | let fut = timeout( 232 | std::time::Duration::from_secs(shutdown_timeout), 233 | async move { 234 | while let Some(state) = rx.next().await { 235 | if !state.is_active() { 236 | return; 237 | } 238 | } 239 | }, 240 | ); 241 | 242 | match stop_result { 243 | Ok(_) => { 244 | let _ = fut.await; 245 | } 246 | Err(e) => error!("couldn't stop service {}: {}", name.clone(), e), 247 | } 248 | 249 | // Verify the service is actually stopped 250 | if let Ok(status) = self.status(&name).await { 251 | if status.pid != 0 { 252 | // Service is still running, try to kill it 253 | let _ = self.kill(&name, signal::Signal::SIGKILL).await; 254 | } 255 | } 256 | 257 | debug!("sending to the death channel {}", name.clone()); 258 | if let Err(e) = ch.send(name.clone()) { 259 | error!( 260 | "error: couldn't send the service {} to the shutdown loop: {}", 261 | name, e 262 | ); 263 | } 264 | } 265 | ``` 266 | 267 | ### 6. Add a Method to Verify All Processes are Terminated 268 | 269 | ```rust 270 | async fn verify_all_processes_terminated(&self) -> Result<()> { 271 | // Get all services 272 | let table = self.services.read().await; 273 | 274 | // Check each service 275 | for (name, service) in table.iter() { 276 | let service = service.read().await; 277 | let pid = service.pid.as_raw(); 278 | 279 | // Skip services with no PID 280 | if pid == 0 { 281 | continue; 282 | } 283 | 284 | // Check if the main process is still running 285 | if self.is_process_running(pid).await? { 286 | warn!("Service {} (PID {}) is still running after shutdown", name, pid); 287 | 288 | // Try to kill it with SIGKILL 289 | let _ = signal::kill(Pid::from_raw(pid), signal::Signal::SIGKILL); 290 | } 291 | 292 | // Check for child processes 293 | if let Ok(children) = self.get_child_process_stats(pid).await { 294 | for child in children { 295 | if self.is_process_running(child.pid).await? { 296 | warn!("Child process {} of service {} is still running after shutdown", 297 | child.pid, name); 298 | 299 | // Try to kill it with SIGKILL 300 | let _ = signal::kill(Pid::from_raw(child.pid), signal::Signal::SIGKILL); 301 | } 302 | } 303 | } 304 | } 305 | 306 | Ok(()) 307 | } 308 | ``` 309 | 310 | ### 7. Update the `shutdown` and `reboot` Methods 311 | 312 | ```rust 313 | pub async fn shutdown(&self) -> Result<()> { 314 | info!("shutting down"); 315 | 316 | // Set the shutdown flag 317 | *self.shutdown.write().await = true; 318 | 319 | #[cfg(target_os = "linux")] 320 | { 321 | // Power off using our enhanced method 322 | let result = self.power(RebootMode::RB_POWER_OFF).await; 323 | 324 | // Final verification before exit 325 | self.verify_all_processes_terminated().await?; 326 | 327 | return result; 328 | } 329 | 330 | #[cfg(not(target_os = "linux"))] 331 | { 332 | // Stop all services 333 | let services = self.list().await?; 334 | for service in services { 335 | let _ = self.stop(&service).await; 336 | } 337 | 338 | // Verify all processes are terminated 339 | self.verify_all_processes_terminated().await?; 340 | 341 | if self.container { 342 | std::process::exit(0); 343 | } else { 344 | info!("System shutdown not supported on this platform"); 345 | std::process::exit(0); 346 | } 347 | } 348 | } 349 | ``` 350 | 351 | ## Testing Plan 352 | 353 | 1. **Basic Service Termination**: Test that a simple service is properly terminated 354 | 2. **Child Process Termination**: Test that a service with child processes has all processes terminated 355 | 3. **Graceful Shutdown**: Test that Zinit exits cleanly after all services are stopped 356 | 4. **Edge Cases**: 357 | - Test with services that spawn many child processes 358 | - Test with services that spawn child processes that change their process group 359 | - Test with services that ignore SIGTERM 360 | 361 | ## Implementation Timeline 362 | 363 | 1. **Phase 1**: Enhance the `stop` method and add the helper methods (1-2 hours) 364 | 2. **Phase 2**: Improve the `kill_process_tree` and `kill_wait` methods (1-2 hours) 365 | 3. **Phase 3**: Update the `shutdown` and `reboot` methods (1 hour) 366 | 4. **Phase 4**: Testing and debugging (2-3 hours) -------------------------------------------------------------------------------- /docs/stats.md: -------------------------------------------------------------------------------- 1 | # Service Stats Functionality 2 | 3 | This document describes the stats functionality in Zinit, which provides memory and CPU usage information for services and their child processes. 4 | 5 | ## Overview 6 | 7 | The stats functionality allows you to monitor the resource usage of services managed by Zinit. It provides information about: 8 | 9 | - Memory usage (in bytes) 10 | - CPU usage (as a percentage) 11 | - Child processes and their resource usage 12 | 13 | This is particularly useful for monitoring system resources and identifying services that might be consuming excessive resources. 14 | 15 | ## Command Line Usage 16 | 17 | To get stats for a service using the command line: 18 | 19 | ```bash 20 | zinit stats 21 | ``` 22 | 23 | Example: 24 | ```bash 25 | zinit stats nginx 26 | ``` 27 | 28 | This will output YAML-formatted stats information: 29 | 30 | ```yaml 31 | name: nginx 32 | pid: 1234 33 | memory_usage: 10485760 # Memory usage in bytes (10MB) 34 | cpu_usage: 2.5 # CPU usage as percentage 35 | children: # Stats for child processes 36 | - pid: 1235 37 | memory_usage: 5242880 38 | cpu_usage: 1.2 39 | - pid: 1236 40 | memory_usage: 4194304 41 | cpu_usage: 0.8 42 | ``` 43 | 44 | ## JSON-RPC API 45 | 46 | The stats functionality is also available through the JSON-RPC API: 47 | 48 | ### Method: `service_stats` 49 | 50 | Get memory and CPU usage statistics for a service. 51 | 52 | **Parameters:** 53 | - `name` (string, required): The name of the service to get stats for 54 | 55 | **Returns:** 56 | - Object containing stats information: 57 | - `name` (string): Service name 58 | - `pid` (integer): Process ID of the service 59 | - `memory_usage` (integer): Memory usage in bytes 60 | - `cpu_usage` (number): CPU usage as a percentage (0-100) 61 | - `children` (array): Stats for child processes 62 | - Each child has: 63 | - `pid` (integer): Process ID of the child process 64 | - `memory_usage` (integer): Memory usage in bytes 65 | - `cpu_usage` (number): CPU usage as a percentage (0-100) 66 | 67 | **Example Request:** 68 | ```json 69 | { 70 | "jsonrpc": "2.0", 71 | "id": 1, 72 | "method": "service_stats", 73 | "params": { 74 | "name": "nginx" 75 | } 76 | } 77 | ``` 78 | 79 | **Example Response:** 80 | ```json 81 | { 82 | "jsonrpc": "2.0", 83 | "id": 1, 84 | "result": { 85 | "name": "nginx", 86 | "pid": 1234, 87 | "memory_usage": 10485760, 88 | "cpu_usage": 2.5, 89 | "children": [ 90 | { 91 | "pid": 1235, 92 | "memory_usage": 5242880, 93 | "cpu_usage": 1.2 94 | }, 95 | { 96 | "pid": 1236, 97 | "memory_usage": 4194304, 98 | "cpu_usage": 0.8 99 | } 100 | ] 101 | } 102 | } 103 | ``` 104 | 105 | **Possible Errors:** 106 | - `-32000`: Service not found 107 | - `-32003`: Service is down 108 | 109 | ## Implementation Details 110 | 111 | The stats functionality works by: 112 | 113 | 1. Reading process information from `/proc//` directories on Linux systems 114 | 2. Calculating memory usage from `/proc//status` (VmRSS field) 115 | 3. Calculating CPU usage by sampling `/proc//stat` over a short interval 116 | 4. Identifying child processes by checking the PPid field in `/proc//status` 117 | 118 | On non-Linux systems, the functionality provides placeholder values as the `/proc` filesystem is specific to Linux. 119 | 120 | ## Notes 121 | 122 | - Memory usage is reported in bytes 123 | - CPU usage is reported as a percentage (0-100) 124 | - The service must be running to get stats (otherwise an error is returned) 125 | - Child processes are identified by their parent PID matching the service's PID -------------------------------------------------------------------------------- /example/example.sh: -------------------------------------------------------------------------------- 1 | #!/bin/bash 2 | 3 | # Determine the zinit binary path 4 | ZINIT_BIN="./target/release/zinit" # Assuming zinit is built in release mode in the current directory 5 | 6 | # Determine the configuration directory based on OS 7 | if [[ "$(uname)" == "Darwin" ]]; then 8 | # macOS 9 | ZINIT_CONFIG_DIR="$HOME/hero/cfg/zinit" 10 | else 11 | # Linux or other 12 | ZINIT_CONFIG_DIR="/etc/zinit" 13 | fi 14 | 15 | SERVICE_NAME="test_service" 16 | CPU_SERVICE_NAME="cpu_test_service" 17 | SERVICE_FILE="$ZINIT_CONFIG_DIR/$SERVICE_NAME.yaml" 18 | CPU_SERVICE_FILE="$ZINIT_CONFIG_DIR/$CPU_SERVICE_NAME.yaml" 19 | 20 | echo "--- Zinit Example Script ---" 21 | echo "Zinit binary path: $ZINIT_BIN" 22 | echo "Zinit config directory: $ZINIT_CONFIG_DIR" 23 | 24 | # Step 1: Ensure zinit config directory exists 25 | echo "Ensuring zinit config directory exists..." 26 | mkdir -p "$ZINIT_CONFIG_DIR" 27 | if [ $? -ne 0 ]; then 28 | echo "Error: Failed to create config directory $ZINIT_CONFIG_DIR. Exiting." 29 | exit 1 30 | fi 31 | echo "Config directory $ZINIT_CONFIG_DIR is ready." 32 | 33 | # Step 2: Check if zinit daemon is running, if not, start it in background 34 | echo "Checking if zinit daemon is running..." 35 | if "$ZINIT_BIN" list > /dev/null 2>&1; then 36 | echo "Zinit daemon is already running." 37 | else 38 | echo "Zinit daemon not running. Starting it in background..." 39 | # Start zinit init in a new process group to avoid it being killed by script exit 40 | # and redirecting output to /dev/null 41 | nohup "$ZINIT_BIN" init > /dev/null 2>&1 & 42 | ZINIT_PID=$! 43 | echo "Zinit daemon started with PID: $ZINIT_PID" 44 | sleep 2 # Give zinit a moment to start up and create the socket 45 | if ! "$ZINIT_BIN" list > /dev/null 2>&1; then 46 | echo "Error: Zinit daemon failed to start. Exiting." 47 | exit 1 48 | fi 49 | echo "Zinit daemon successfully started." 50 | fi 51 | 52 | # Step 3: Create sample zinit service files 53 | echo "Creating sample service file: $SERVICE_FILE" 54 | cat < "$SERVICE_FILE" 55 | name: $SERVICE_NAME 56 | exec: /bin/bash -c "while true; do echo 'Hello from $SERVICE_NAME!'; sleep 5; done" 57 | log: stdout 58 | EOF 59 | 60 | if [ $? -ne 0 ]; then 61 | echo "Error: Failed to create service file $SERVICE_FILE. Exiting." 62 | exit 1 63 | fi 64 | echo "Service file created." 65 | 66 | # Create a CPU-intensive service with child processes 67 | echo "Creating CPU-intensive service file: $CPU_SERVICE_FILE" 68 | cat < "$CPU_SERVICE_FILE" 69 | name: $CPU_SERVICE_NAME 70 | exec: /bin/bash -c "for i in {1..3}; do (yes > /dev/null &) ; done; while true; do sleep 10; done" 71 | log: stdout 72 | EOF 73 | 74 | if [ $? -ne 0 ]; then 75 | echo "Error: Failed to create CPU service file $CPU_SERVICE_FILE. Exiting." 76 | exit 1 77 | fi 78 | echo "CPU service file created." 79 | 80 | # Step 4: Tell zinit to monitor the new services 81 | echo "Telling zinit to monitor the services..." 82 | "$ZINIT_BIN" monitor "$SERVICE_NAME" 83 | "$ZINIT_BIN" monitor "$CPU_SERVICE_NAME" 84 | 85 | # Step 5: List services to verify the new service is recognized 86 | echo "Listing zinit services to verify..." 87 | "$ZINIT_BIN" list 88 | 89 | # Step 6: Show stats for the CPU-intensive service 90 | echo "Waiting for services to start and generate some stats..." 91 | sleep 5 92 | echo "Getting stats for $CPU_SERVICE_NAME..." 93 | "$ZINIT_BIN" stats "$CPU_SERVICE_NAME" 94 | 95 | # # Step 7: Clean up (optional, but good for examples) 96 | # echo "Cleaning up: stopping and forgetting services..." 97 | # "$ZINIT_BIN" stop "$SERVICE_NAME" > /dev/null 2>&1 98 | # "$ZINIT_BIN" forget "$SERVICE_NAME" > /dev/null 2>&1 99 | # "$ZINIT_BIN" stop "$CPU_SERVICE_NAME" > /dev/null 2>&1 100 | # "$ZINIT_BIN" forget "$CPU_SERVICE_NAME" > /dev/null 2>&1 101 | # rm -f "$SERVICE_FILE" "$CPU_SERVICE_FILE" 102 | # echo "Cleanup complete." 103 | 104 | echo "--- Script Finished ---" 105 | -------------------------------------------------------------------------------- /install.sh: -------------------------------------------------------------------------------- 1 | #!/bin/bash 2 | set -e 3 | 4 | # Colors for output 5 | RED='\033[0;31m' 6 | GREEN='\033[0;32m' 7 | YELLOW='\033[0;33m' 8 | NC='\033[0m' # No Color 9 | 10 | echo -e "${GREEN}stop zinit...${NC}" 11 | rm -f /tmp/stop.sh 12 | curl -fsSL https://raw.githubusercontent.com/threefoldtech/zinit/refs/heads/master/stop.sh > /tmp/stop.sh 13 | bash /tmp/stop.sh 14 | 15 | 16 | # GitHub repository information 17 | GITHUB_REPO="threefoldtech/zinit" 18 | 19 | # Get the latest version from GitHub API 20 | echo -e "${YELLOW}Fetching latest version information...${NC}" 21 | if command -v curl &> /dev/null; then 22 | VERSION=$(curl -s "https://api.github.com/repos/${GITHUB_REPO}/releases/latest" | grep -o '"tag_name": "[^"]*' | grep -o '[^"]*$') 23 | elif command -v wget &> /dev/null; then 24 | VERSION=$(wget -qO- "https://api.github.com/repos/${GITHUB_REPO}/releases/latest" | grep -o '"tag_name": "[^"]*' | grep -o '[^"]*$') 25 | else 26 | echo -e "${RED}Neither curl nor wget found. Please install one of them and try again.${NC}" 27 | exit 1 28 | fi 29 | 30 | if [ -z "$VERSION" ]; then 31 | echo -e "${RED}Failed to fetch the latest version. Please check your internet connection.${NC}" 32 | exit 1 33 | fi 34 | 35 | echo -e "${GREEN}Latest version: ${VERSION}${NC}" 36 | DOWNLOAD_URL="https://github.com/${GITHUB_REPO}/releases/download/${VERSION}" 37 | MIN_SIZE_BYTES=2000000 # 2MB in bytes 38 | 39 | echo -e "${GREEN}Installing zinit ${VERSION}...${NC}" 40 | 41 | # Create temporary directory 42 | TMP_DIR=$(mktemp -d) 43 | trap 'rm -rf "$TMP_DIR"' EXIT 44 | 45 | # Detect OS and architecture 46 | OS=$(uname -s | tr '[:upper:]' '[:lower:]') 47 | ARCH=$(uname -m) 48 | 49 | # Map architecture names 50 | if [ "$ARCH" = "x86_64" ]; then 51 | ARCH_NAME="x86_64" 52 | elif [ "$ARCH" = "aarch64" ] || [ "$ARCH" = "arm64" ]; then 53 | ARCH_NAME="aarch64" 54 | else 55 | echo -e "${RED}Unsupported architecture: $ARCH${NC}" 56 | exit 1 57 | fi 58 | 59 | # Determine binary name based on OS and architecture 60 | if [ "$OS" = "linux" ]; then 61 | if [ "$ARCH_NAME" = "x86_64" ]; then 62 | BINARY_NAME="zinit-linux-x86_64" 63 | else 64 | echo -e "${RED}Unsupported Linux architecture: $ARCH${NC}" 65 | exit 1 66 | fi 67 | elif [ "$OS" = "darwin" ]; then 68 | if [ "$ARCH_NAME" = "x86_64" ]; then 69 | BINARY_NAME="zinit-macos-x86_64" 70 | elif [ "$ARCH_NAME" = "aarch64" ]; then 71 | BINARY_NAME="zinit-macos-aarch64" 72 | else 73 | echo -e "${RED}Unsupported macOS architecture: $ARCH${NC}" 74 | exit 1 75 | fi 76 | else 77 | echo -e "${RED}Unsupported operating system: $OS${NC}" 78 | exit 1 79 | fi 80 | 81 | # Download URL 82 | DOWNLOAD_PATH="${DOWNLOAD_URL}/${BINARY_NAME}" 83 | LOCAL_PATH="${TMP_DIR}/${BINARY_NAME}" 84 | 85 | echo -e "${YELLOW}Detected: $OS on $ARCH_NAME${NC}" 86 | echo -e "${YELLOW}Downloading from: $DOWNLOAD_PATH${NC}" 87 | 88 | # Download the binary 89 | if command -v curl &> /dev/null; then 90 | curl -L -o "$LOCAL_PATH" "$DOWNLOAD_PATH" 91 | elif command -v wget &> /dev/null; then 92 | wget -O "$LOCAL_PATH" "$DOWNLOAD_PATH" 93 | else 94 | echo -e "${RED}Neither curl nor wget found. Please install one of them and try again.${NC}" 95 | exit 1 96 | fi 97 | 98 | # Check file size 99 | FILE_SIZE=$(stat -f%z "$LOCAL_PATH" 2>/dev/null || stat -c%s "$LOCAL_PATH" 2>/dev/null) 100 | if [ "$FILE_SIZE" -lt "$MIN_SIZE_BYTES" ]; then 101 | echo -e "${RED}Downloaded file is too small (${FILE_SIZE} bytes). Expected at least ${MIN_SIZE_BYTES} bytes.${NC}" 102 | echo -e "${RED}This might indicate a failed or incomplete download.${NC}" 103 | exit 1 104 | fi 105 | 106 | echo -e "${GREEN}Download successful. File size: $(echo "$FILE_SIZE / 1000000" | bc -l | xargs printf "%.2f") MB${NC}" 107 | 108 | # Make the binary executable 109 | chmod +x "$LOCAL_PATH" 110 | 111 | # Determine installation directory 112 | if [ "$OS" = "darwin" ]; then 113 | # macOS - install to ~/hero/bin/ 114 | INSTALL_DIR="$HOME/hero/bin" 115 | else 116 | # Linux - install to /usr/local/bin/ if running as root, otherwise to ~/.local/bin/ 117 | if [ "$(id -u)" -eq 0 ]; then 118 | INSTALL_DIR="/usr/local/bin" 119 | else 120 | INSTALL_DIR="$HOME/.local/bin" 121 | # Ensure ~/.local/bin exists and is in PATH 122 | mkdir -p "$INSTALL_DIR" 123 | if [[ ":$PATH:" != *":$INSTALL_DIR:"* ]]; then 124 | echo -e "${YELLOW}Adding $INSTALL_DIR to your PATH. You may need to restart your terminal.${NC}" 125 | if [ -f "$HOME/.bashrc" ]; then 126 | echo "export PATH=\"\$PATH:$INSTALL_DIR\"" >> "$HOME/.bashrc" 127 | fi 128 | if [ -f "$HOME/.zshrc" ]; then 129 | echo "export PATH=\"\$PATH:$INSTALL_DIR\"" >> "$HOME/.zshrc" 130 | fi 131 | fi 132 | fi 133 | fi 134 | 135 | # Create installation directory if it doesn't exist 136 | mkdir -p "$INSTALL_DIR" 137 | 138 | # Copy the binary to the installation directory 139 | cp "$LOCAL_PATH" "$INSTALL_DIR/zinit" 140 | echo -e "${GREEN}Installed zinit to $INSTALL_DIR/zinit${NC}" 141 | 142 | # Test the installation 143 | echo -e "${YELLOW}Testing installation...${NC}" 144 | if "$INSTALL_DIR/zinit" --help &> /dev/null; then 145 | echo -e "${GREEN}Installation successful! You can now use 'zinit' command.${NC}" 146 | echo -e "${YELLOW}Example usage: zinit --help${NC}" 147 | "$INSTALL_DIR/zinit" --help | head -n 5 148 | else 149 | echo -e "${RED}Installation test failed. Please check the error messages above.${NC}" 150 | exit 1 151 | fi 152 | 153 | echo -e "${GREEN}zinit ${VERSION} has been successfully installed!${NC}" -------------------------------------------------------------------------------- /install_run.sh: -------------------------------------------------------------------------------- 1 | #!/bin/bash 2 | set -e 3 | 4 | # Colors for output 5 | RED='\033[0;31m' 6 | GREEN='\033[0;32m' 7 | YELLOW='\033[0;33m' 8 | NC='\033[0m' # No Color 9 | 10 | # Function to check if zinit is running 11 | is_zinit_running() { 12 | if zinit list &>/dev/null; then 13 | return 0 # Command successful, zinit is running 14 | else 15 | return 1 # Command failed, zinit is not running 16 | fi 17 | } 18 | 19 | echo -e "${GREEN}Starting zinit installation and setup...${NC}" 20 | # Download and execute install.sh 21 | echo -e "${YELLOW}Downloading and executing install.sh...${NC}" 22 | curl -fsSL https://raw.githubusercontent.com/threefoldtech/zinit/refs/heads/master/install.sh | bash 23 | 24 | echo -e "${GREEN}install zinit...${NC}" 25 | rm -f /tmp/install.sh 26 | curl -fsSL https://raw.githubusercontent.com/threefoldtech/zinit/refs/heads/master/install.sh > /tmp/install.sh 27 | bash /tmp/install.sh 28 | 29 | 30 | # Launch zinit in the background 31 | echo -e "${GREEN}Starting zinit in the background...${NC}" 32 | zinit & 33 | 34 | # Give it a moment to start 35 | sleep 1 36 | 37 | # Verify zinit is running 38 | if is_zinit_running; then 39 | echo -e "${GREEN}Zinit is now running in the background.${NC}" 40 | echo -e "${YELLOW}You can manage services with:${NC}" 41 | echo -e " ${YELLOW}$ZINIT_PATH list${NC} - List all services" 42 | echo -e " ${YELLOW}$ZINIT_PATH status${NC} - Show status of all services" 43 | echo -e " ${YELLOW}$ZINIT_PATH monitor${NC} - Monitor services in real-time" 44 | echo -e " ${YELLOW}$ZINIT_PATH shutdown${NC} - Shutdown zinit when needed" 45 | else 46 | echo -e "${RED}Failed to start zinit. Please check for errors above.${NC}" 47 | exit 1 48 | fi 49 | 50 | echo -e "${GREEN}Zinit installation and startup complete!${NC}" -------------------------------------------------------------------------------- /openrpc.json: -------------------------------------------------------------------------------- 1 | { 2 | "openrpc": "1.2.6", 3 | "info": { 4 | "version": "1.0.0", 5 | "title": "Zinit JSON-RPC API", 6 | "description": "JSON-RPC 2.0 API for controlling and querying Zinit services", 7 | "license": { 8 | "name": "MIT" 9 | } 10 | }, 11 | "servers": [ 12 | { 13 | "name": "Unix Socket", 14 | "url": "unix:///tmp/zinit.sock" 15 | } 16 | ], 17 | "methods": [ 18 | { 19 | "name": "rpc.discover", 20 | "description": "Returns the OpenRPC specification for the API", 21 | "params": [], 22 | "result": { 23 | "name": "OpenRPCSpec", 24 | "description": "The OpenRPC specification", 25 | "schema": { 26 | "type": "object" 27 | } 28 | }, 29 | "examples": [ 30 | { 31 | "name": "Get API specification", 32 | "params": [], 33 | "result": { 34 | "name": "OpenRPCSpecResult", 35 | "value": { 36 | "openrpc": "1.2.6", 37 | "info": { 38 | "version": "1.0.0", 39 | "title": "Zinit JSON-RPC API" 40 | } 41 | } 42 | } 43 | } 44 | ] 45 | }, 46 | { 47 | "name": "service_list", 48 | "description": "Lists all services managed by Zinit", 49 | "params": [], 50 | "result": { 51 | "name": "ServiceList", 52 | "description": "A map of service names to their current states", 53 | "schema": { 54 | "type": "object", 55 | "additionalProperties": { 56 | "type": "string", 57 | "description": "Service state (Running, Success, Error, etc.)" 58 | } 59 | } 60 | }, 61 | "examples": [ 62 | { 63 | "name": "List all services", 64 | "params": [], 65 | "result": { 66 | "name": "ServiceListResult", 67 | "value": { 68 | "service1": "Running", 69 | "service2": "Success", 70 | "service3": "Error" 71 | } 72 | } 73 | } 74 | ] 75 | }, 76 | { 77 | "name": "service_status", 78 | "description": "Shows detailed status information for a specific service", 79 | "params": [ 80 | { 81 | "name": "name", 82 | "description": "The name of the service", 83 | "required": true, 84 | "schema": { 85 | "type": "string" 86 | } 87 | } 88 | ], 89 | "result": { 90 | "name": "ServiceStatus", 91 | "description": "Detailed status information for the service", 92 | "schema": { 93 | "type": "object", 94 | "properties": { 95 | "name": { 96 | "type": "string", 97 | "description": "Service name" 98 | }, 99 | "pid": { 100 | "type": "integer", 101 | "description": "Process ID of the running service (if running)" 102 | }, 103 | "state": { 104 | "type": "string", 105 | "description": "Current state of the service (Running, Success, Error, etc.)" 106 | }, 107 | "target": { 108 | "type": "string", 109 | "description": "Target state of the service (Up, Down)" 110 | }, 111 | "after": { 112 | "type": "object", 113 | "description": "Dependencies of the service and their states", 114 | "additionalProperties": { 115 | "type": "string", 116 | "description": "State of the dependency" 117 | } 118 | } 119 | } 120 | } 121 | }, 122 | "examples": [ 123 | { 124 | "name": "Get status of redis service", 125 | "params": [ 126 | { 127 | "name": "name", 128 | "value": "redis" 129 | } 130 | ], 131 | "result": { 132 | "name": "ServiceStatusResult", 133 | "value": { 134 | "name": "redis", 135 | "pid": 1234, 136 | "state": "Running", 137 | "target": "Up", 138 | "after": { 139 | "dependency1": "Success", 140 | "dependency2": "Running" 141 | } 142 | } 143 | } 144 | } 145 | ], 146 | "errors": [ 147 | { 148 | "code": -32000, 149 | "message": "Service not found", 150 | "data": "service name \"unknown\" unknown" 151 | } 152 | ] 153 | }, 154 | { 155 | "name": "service_start", 156 | "description": "Starts a service", 157 | "params": [ 158 | { 159 | "name": "name", 160 | "description": "The name of the service to start", 161 | "required": true, 162 | "schema": { 163 | "type": "string" 164 | } 165 | } 166 | ], 167 | "result": { 168 | "name": "StartResult", 169 | "description": "Result of the start operation", 170 | "schema": { 171 | "type": "null" 172 | } 173 | }, 174 | "examples": [ 175 | { 176 | "name": "Start redis service", 177 | "params": [ 178 | { 179 | "name": "name", 180 | "value": "redis" 181 | } 182 | ], 183 | "result": { 184 | "name": "StartResult", 185 | "value": null 186 | } 187 | } 188 | ], 189 | "errors": [ 190 | { 191 | "code": -32000, 192 | "message": "Service not found", 193 | "data": "service name \"unknown\" unknown" 194 | } 195 | ] 196 | }, 197 | { 198 | "name": "service_stop", 199 | "description": "Stops a service", 200 | "params": [ 201 | { 202 | "name": "name", 203 | "description": "The name of the service to stop", 204 | "required": true, 205 | "schema": { 206 | "type": "string" 207 | } 208 | } 209 | ], 210 | "result": { 211 | "name": "StopResult", 212 | "description": "Result of the stop operation", 213 | "schema": { 214 | "type": "null" 215 | } 216 | }, 217 | "examples": [ 218 | { 219 | "name": "Stop redis service", 220 | "params": [ 221 | { 222 | "name": "name", 223 | "value": "redis" 224 | } 225 | ], 226 | "result": { 227 | "name": "StopResult", 228 | "value": null 229 | } 230 | } 231 | ], 232 | "errors": [ 233 | { 234 | "code": -32000, 235 | "message": "Service not found", 236 | "data": "service name \"unknown\" unknown" 237 | }, 238 | { 239 | "code": -32003, 240 | "message": "Service is down", 241 | "data": "service \"redis\" is down" 242 | } 243 | ] 244 | }, 245 | { 246 | "name": "service_monitor", 247 | "description": "Starts monitoring a service. The service configuration is loaded from the config directory.", 248 | "params": [ 249 | { 250 | "name": "name", 251 | "description": "The name of the service to monitor", 252 | "required": true, 253 | "schema": { 254 | "type": "string" 255 | } 256 | } 257 | ], 258 | "result": { 259 | "name": "MonitorResult", 260 | "description": "Result of the monitor operation", 261 | "schema": { 262 | "type": "null" 263 | } 264 | }, 265 | "examples": [ 266 | { 267 | "name": "Monitor redis service", 268 | "params": [ 269 | { 270 | "name": "name", 271 | "value": "redis" 272 | } 273 | ], 274 | "result": { 275 | "name": "MonitorResult", 276 | "value": null 277 | } 278 | } 279 | ], 280 | "errors": [ 281 | { 282 | "code": -32001, 283 | "message": "Service already monitored", 284 | "data": "service \"redis\" already monitored" 285 | }, 286 | { 287 | "code": -32005, 288 | "message": "Config error", 289 | "data": "failed to load service configuration" 290 | } 291 | ] 292 | }, 293 | { 294 | "name": "service_forget", 295 | "description": "Stops monitoring a service. You can only forget a stopped service.", 296 | "params": [ 297 | { 298 | "name": "name", 299 | "description": "The name of the service to forget", 300 | "required": true, 301 | "schema": { 302 | "type": "string" 303 | } 304 | } 305 | ], 306 | "result": { 307 | "name": "ForgetResult", 308 | "description": "Result of the forget operation", 309 | "schema": { 310 | "type": "null" 311 | } 312 | }, 313 | "examples": [ 314 | { 315 | "name": "Forget redis service", 316 | "params": [ 317 | { 318 | "name": "name", 319 | "value": "redis" 320 | } 321 | ], 322 | "result": { 323 | "name": "ForgetResult", 324 | "value": null 325 | } 326 | } 327 | ], 328 | "errors": [ 329 | { 330 | "code": -32000, 331 | "message": "Service not found", 332 | "data": "service name \"unknown\" unknown" 333 | }, 334 | { 335 | "code": -32002, 336 | "message": "Service is up", 337 | "data": "service \"redis\" is up" 338 | } 339 | ] 340 | }, 341 | { 342 | "name": "service_kill", 343 | "description": "Sends a signal to a running service", 344 | "params": [ 345 | { 346 | "name": "name", 347 | "description": "The name of the service to send the signal to", 348 | "required": true, 349 | "schema": { 350 | "type": "string" 351 | } 352 | }, 353 | { 354 | "name": "signal", 355 | "description": "The signal to send (e.g., SIGTERM, SIGKILL)", 356 | "required": true, 357 | "schema": { 358 | "type": "string" 359 | } 360 | } 361 | ], 362 | "result": { 363 | "name": "KillResult", 364 | "description": "Result of the kill operation", 365 | "schema": { 366 | "type": "null" 367 | } 368 | }, 369 | "examples": [ 370 | { 371 | "name": "Send SIGTERM to redis service", 372 | "params": [ 373 | { 374 | "name": "name", 375 | "value": "redis" 376 | }, 377 | { 378 | "name": "signal", 379 | "value": "SIGTERM" 380 | } 381 | ], 382 | "result": { 383 | "name": "KillResult", 384 | "value": null 385 | } 386 | } 387 | ], 388 | "errors": [ 389 | { 390 | "code": -32000, 391 | "message": "Service not found", 392 | "data": "service name \"unknown\" unknown" 393 | }, 394 | { 395 | "code": -32003, 396 | "message": "Service is down", 397 | "data": "service \"redis\" is down" 398 | }, 399 | { 400 | "code": -32004, 401 | "message": "Invalid signal", 402 | "data": "invalid signal: INVALID" 403 | } 404 | ] 405 | }, 406 | { 407 | "name": "system_shutdown", 408 | "description": "Stops all services and powers off the system", 409 | "params": [], 410 | "result": { 411 | "name": "ShutdownResult", 412 | "description": "Result of the shutdown operation", 413 | "schema": { 414 | "type": "null" 415 | } 416 | }, 417 | "examples": [ 418 | { 419 | "name": "Shutdown the system", 420 | "params": [], 421 | "result": { 422 | "name": "ShutdownResult", 423 | "value": null 424 | } 425 | } 426 | ], 427 | "errors": [ 428 | { 429 | "code": -32006, 430 | "message": "Shutting down", 431 | "data": "system is already shutting down" 432 | } 433 | ] 434 | }, 435 | { 436 | "name": "system_reboot", 437 | "description": "Stops all services and reboots the system", 438 | "params": [], 439 | "result": { 440 | "name": "RebootResult", 441 | "description": "Result of the reboot operation", 442 | "schema": { 443 | "type": "null" 444 | } 445 | }, 446 | "examples": [ 447 | { 448 | "name": "Reboot the system", 449 | "params": [], 450 | "result": { 451 | "name": "RebootResult", 452 | "value": null 453 | } 454 | } 455 | ], 456 | "errors": [ 457 | { 458 | "code": -32006, 459 | "message": "Shutting down", 460 | "data": "system is already shutting down" 461 | } 462 | ] 463 | }, 464 | { 465 | "name": "service_create", 466 | "description": "Creates a new service configuration file", 467 | "params": [ 468 | { 469 | "name": "name", 470 | "description": "The name of the service to create", 471 | "required": true, 472 | "schema": { 473 | "type": "string" 474 | } 475 | }, 476 | { 477 | "name": "content", 478 | "description": "The service configuration content", 479 | "required": true, 480 | "schema": { 481 | "type": "object", 482 | "properties": { 483 | "exec": { 484 | "type": "string", 485 | "description": "Command to run" 486 | }, 487 | "oneshot": { 488 | "type": "boolean", 489 | "description": "Whether the service should be restarted" 490 | }, 491 | "after": { 492 | "type": "array", 493 | "items": { 494 | "type": "string" 495 | }, 496 | "description": "Services that must be running before this one starts" 497 | }, 498 | "log": { 499 | "type": "string", 500 | "enum": ["null", "ring", "stdout"], 501 | "description": "How to handle service output" 502 | }, 503 | "env": { 504 | "type": "object", 505 | "additionalProperties": { 506 | "type": "string" 507 | }, 508 | "description": "Environment variables for the service" 509 | }, 510 | "shutdown_timeout": { 511 | "type": "integer", 512 | "description": "Maximum time to wait for service to stop during shutdown" 513 | } 514 | } 515 | } 516 | } 517 | ], 518 | "result": { 519 | "name": "CreateServiceResult", 520 | "description": "Result of the create operation", 521 | "schema": { 522 | "type": "string" 523 | } 524 | }, 525 | "errors": [ 526 | { 527 | "code": -32007, 528 | "message": "Service already exists", 529 | "data": "Service 'name' already exists" 530 | }, 531 | { 532 | "code": -32008, 533 | "message": "Service file error", 534 | "data": "Failed to create service file" 535 | } 536 | ] 537 | }, 538 | { 539 | "name": "service_delete", 540 | "description": "Deletes a service configuration file", 541 | "params": [ 542 | { 543 | "name": "name", 544 | "description": "The name of the service to delete", 545 | "required": true, 546 | "schema": { 547 | "type": "string" 548 | } 549 | } 550 | ], 551 | "result": { 552 | "name": "DeleteServiceResult", 553 | "description": "Result of the delete operation", 554 | "schema": { 555 | "type": "string" 556 | } 557 | }, 558 | "errors": [ 559 | { 560 | "code": -32000, 561 | "message": "Service not found", 562 | "data": "Service 'name' not found" 563 | }, 564 | { 565 | "code": -32008, 566 | "message": "Service file error", 567 | "data": "Failed to delete service file" 568 | } 569 | ] 570 | }, 571 | { 572 | "name": "service_get", 573 | "description": "Gets a service configuration file", 574 | "params": [ 575 | { 576 | "name": "name", 577 | "description": "The name of the service to get", 578 | "required": true, 579 | "schema": { 580 | "type": "string" 581 | } 582 | } 583 | ], 584 | "result": { 585 | "name": "GetServiceResult", 586 | "description": "The service configuration", 587 | "schema": { 588 | "type": "object" 589 | } 590 | }, 591 | "errors": [ 592 | { 593 | "code": -32000, 594 | "message": "Service not found", 595 | "data": "Service 'name' not found" 596 | }, 597 | { 598 | "code": -32008, 599 | "message": "Service file error", 600 | "data": "Failed to read service file" 601 | } 602 | ] 603 | }, 604 | { 605 | "name": "service_stats", 606 | "description": "Get memory and CPU usage statistics for a service", 607 | "params": [ 608 | { 609 | "name": "name", 610 | "description": "The name of the service to get stats for", 611 | "required": true, 612 | "schema": { 613 | "type": "string" 614 | } 615 | } 616 | ], 617 | "result": { 618 | "name": "ServiceStats", 619 | "description": "Memory and CPU usage statistics for the service", 620 | "schema": { 621 | "type": "object", 622 | "properties": { 623 | "name": { 624 | "type": "string", 625 | "description": "Service name" 626 | }, 627 | "pid": { 628 | "type": "integer", 629 | "description": "Process ID of the service" 630 | }, 631 | "memory_usage": { 632 | "type": "integer", 633 | "description": "Memory usage in bytes" 634 | }, 635 | "cpu_usage": { 636 | "type": "number", 637 | "description": "CPU usage as a percentage (0-100)" 638 | }, 639 | "children": { 640 | "type": "array", 641 | "description": "Stats for child processes", 642 | "items": { 643 | "type": "object", 644 | "properties": { 645 | "pid": { 646 | "type": "integer", 647 | "description": "Process ID of the child process" 648 | }, 649 | "memory_usage": { 650 | "type": "integer", 651 | "description": "Memory usage in bytes" 652 | }, 653 | "cpu_usage": { 654 | "type": "number", 655 | "description": "CPU usage as a percentage (0-100)" 656 | } 657 | } 658 | } 659 | } 660 | } 661 | } 662 | }, 663 | "examples": [ 664 | { 665 | "name": "Get stats for redis service", 666 | "params": [ 667 | { 668 | "name": "name", 669 | "value": "redis" 670 | } 671 | ], 672 | "result": { 673 | "name": "ServiceStatsResult", 674 | "value": { 675 | "name": "redis", 676 | "pid": 1234, 677 | "memory_usage": 10485760, 678 | "cpu_usage": 2.5, 679 | "children": [ 680 | { 681 | "pid": 1235, 682 | "memory_usage": 5242880, 683 | "cpu_usage": 1.2 684 | } 685 | ] 686 | } 687 | } 688 | } 689 | ], 690 | "errors": [ 691 | { 692 | "code": -32000, 693 | "message": "Service not found", 694 | "data": "service name \"unknown\" unknown" 695 | }, 696 | { 697 | "code": -32003, 698 | "message": "Service is down", 699 | "data": "service \"redis\" is down" 700 | } 701 | ] 702 | }, 703 | { 704 | "name": "system_start_http_server", 705 | "description": "Start an HTTP/RPC server at the specified address", 706 | "params": [ 707 | { 708 | "name": "address", 709 | "description": "The network address to bind the server to (e.g., '127.0.0.1:8080')", 710 | "required": true, 711 | "schema": { 712 | "type": "string" 713 | } 714 | } 715 | ], 716 | "result": { 717 | "name": "StartHttpServerResult", 718 | "description": "Result of the start HTTP server operation", 719 | "schema": { 720 | "type": "string" 721 | } 722 | }, 723 | "examples": [ 724 | { 725 | "name": "Start HTTP server on localhost:8080", 726 | "params": [ 727 | { 728 | "name": "address", 729 | "value": "127.0.0.1:8080" 730 | } 731 | ], 732 | "result": { 733 | "name": "StartHttpServerResult", 734 | "value": "HTTP server started at 127.0.0.1:8080" 735 | } 736 | } 737 | ], 738 | "errors": [ 739 | { 740 | "code": -32602, 741 | "message": "Invalid address", 742 | "data": "Invalid network address format" 743 | } 744 | ] 745 | }, 746 | { 747 | "name": "system_stop_http_server", 748 | "description": "Stop the HTTP/RPC server if running", 749 | "params": [], 750 | "result": { 751 | "name": "StopHttpServerResult", 752 | "description": "Result of the stop HTTP server operation", 753 | "schema": { 754 | "type": "null" 755 | } 756 | }, 757 | "examples": [ 758 | { 759 | "name": "Stop the HTTP server", 760 | "params": [], 761 | "result": { 762 | "name": "StopHttpServerResult", 763 | "value": null 764 | } 765 | } 766 | ], 767 | "errors": [ 768 | { 769 | "code": -32602, 770 | "message": "Server not running", 771 | "data": "No HTTP server is currently running" 772 | } 773 | ] 774 | }, 775 | { 776 | "name": "stream_currentLogs", 777 | "description": "Get current logs from zinit and monitored services", 778 | "params": [ 779 | { 780 | "name": "name", 781 | "description": "Optional service name filter. If provided, only logs from this service will be returned", 782 | "required": false, 783 | "schema": { 784 | "type": "string" 785 | } 786 | } 787 | ], 788 | "result": { 789 | "name": "LogsResult", 790 | "description": "Array of log strings", 791 | "schema": { 792 | "type": "array", 793 | "items": { 794 | "type": "string" 795 | } 796 | } 797 | }, 798 | "examples": [ 799 | { 800 | "name": "Get all logs", 801 | "params": [], 802 | "result": { 803 | "name": "LogsResult", 804 | "value": [ 805 | "2023-01-01T12:00:00 redis: Starting service", 806 | "2023-01-01T12:00:01 nginx: Starting service" 807 | ] 808 | } 809 | }, 810 | { 811 | "name": "Get logs for a specific service", 812 | "params": [ 813 | { 814 | "name": "name", 815 | "value": "redis" 816 | } 817 | ], 818 | "result": { 819 | "name": "LogsResult", 820 | "value": [ 821 | "2023-01-01T12:00:00 redis: Starting service", 822 | "2023-01-01T12:00:02 redis: Service started" 823 | ] 824 | } 825 | } 826 | ] 827 | }, 828 | { 829 | "name": "stream_subscribeLogs", 830 | "description": "Subscribe to log messages generated by zinit and monitored services", 831 | "params": [ 832 | { 833 | "name": "name", 834 | "description": "Optional service name filter. If provided, only logs from this service will be returned", 835 | "required": false, 836 | "schema": { 837 | "type": "string" 838 | } 839 | } 840 | ], 841 | "result": { 842 | "name": "LogSubscription", 843 | "description": "A subscription to log messages", 844 | "schema": { 845 | "type": "string" 846 | } 847 | }, 848 | "examples": [ 849 | { 850 | "name": "Subscribe to all logs", 851 | "params": [], 852 | "result": { 853 | "name": "LogSubscription", 854 | "value": "2023-01-01T12:00:00 redis: Service started" 855 | } 856 | }, 857 | { 858 | "name": "Subscribe to filtered logs", 859 | "params": [ 860 | { 861 | "name": "name", 862 | "value": "redis" 863 | } 864 | ], 865 | "result": { 866 | "name": "LogSubscription", 867 | "value": "2023-01-01T12:00:00 redis: Service started" 868 | } 869 | } 870 | ] 871 | } 872 | ] 873 | } -------------------------------------------------------------------------------- /osx_build.sh: -------------------------------------------------------------------------------- 1 | #!/bin/bash 2 | 3 | # Jump to the directory of the script 4 | cd "$(dirname "$0")" 5 | 6 | ./stop.sh 7 | 8 | # Build the project 9 | echo "Building zinit..." 10 | cargo build --release 11 | 12 | if [ $? -ne 0 ]; then 13 | echo "Build failed!" 14 | exit 1 15 | fi 16 | 17 | # Copy the binary 18 | echo "Copying zinit binary to ~/hero/bin..." 19 | cp ./target/release/zinit ~/hero/bin 20 | 21 | if [ $? -ne 0 ]; then 22 | echo "Failed to copy binary!" 23 | exit 1 24 | fi 25 | 26 | # Ensure config directory exists 27 | echo "Ensuring config directory exists..." 28 | mkdir -p ~/hero/cfg/zinit 29 | 30 | # Start zinit in init mode (daemon) in background 31 | echo "Starting zinit daemon in background..." 32 | ~/hero/bin/zinit init -c ~/hero/cfg/zinit & 33 | ZINIT_PID=$! 34 | 35 | # Wait a moment for zinit to start and create the socket 36 | sleep 5 37 | 38 | # Check if zinit is running 39 | if kill -0 $ZINIT_PID 2>/dev/null; then 40 | echo "Zinit daemon started successfully with PID: $ZINIT_PID" 41 | 42 | # Test with zinit list 43 | echo "Testing zinit list command..." 44 | ~/hero/bin/zinit list 45 | 46 | if [ $? -eq 0 ]; then 47 | echo "Zinit is working correctly!" 48 | else 49 | echo "Warning: zinit list command failed, but zinit daemon is running" 50 | echo "This might be normal if no services are configured yet." 51 | fi 52 | else 53 | echo "Failed to start zinit daemon!" 54 | exit 1 55 | fi 56 | 57 | echo "Build and setup completed successfully!" 58 | -------------------------------------------------------------------------------- /release_zinit.sh: -------------------------------------------------------------------------------- 1 | #!/bin/bash 2 | 3 | # Navigate to the zinit project directory 4 | cd /Users/despiegk/code/github/threefoldtech/zinit 5 | 6 | # Check if we're in the right directory 7 | if [ ! -f "Cargo.toml" ]; then 8 | echo "Error: Not in zinit project directory" 9 | exit 1 10 | fi 11 | 12 | # Function to get the latest tag from Git 13 | get_latest_tag() { 14 | # Fetch all tags from remote 15 | git fetch --tags origin 2>/dev/null 16 | 17 | # Get the latest tag using version sorting 18 | local latest_tag=$(git tag -l "v*" | sort -V | tail -n 1) 19 | 20 | if [ -z "$latest_tag" ]; then 21 | echo "v0.0.0" 22 | else 23 | echo "$latest_tag" 24 | fi 25 | } 26 | 27 | # Function to increment version 28 | increment_version() { 29 | local version=$1 30 | # Remove 'v' prefix if present 31 | version=${version#v} 32 | 33 | # Split version into parts 34 | IFS='.' read -ra PARTS <<< "$version" 35 | major=${PARTS[0]:-0} 36 | minor=${PARTS[1]:-0} 37 | patch=${PARTS[2]:-0} 38 | 39 | # Increment patch (maintenance) version 40 | patch=$((patch + 1)) 41 | 42 | echo "v${major}.${minor}.${patch}" 43 | } 44 | 45 | echo "🔍 Checking latest tag..." 46 | latest_tag=$(get_latest_tag) 47 | echo "Latest tag: $latest_tag" 48 | 49 | new_version=$(increment_version "$latest_tag") 50 | echo "New version: $new_version" 51 | 52 | # Confirm with user 53 | read -p "Create release $new_version? (y/N): " -n 1 -r 54 | echo 55 | if [[ ! $REPLY =~ ^[Yy]$ ]]; then 56 | echo "Release cancelled" 57 | exit 0 58 | fi 59 | 60 | # Check if tag already exists locally and remove it 61 | if git tag -l | grep -q "^$new_version$"; then 62 | echo "⚠️ Local tag $new_version already exists, removing it..." 63 | git tag -d "$new_version" 64 | fi 65 | 66 | # Make sure we're on the right branch and up to date 67 | echo "🔄 Updating repository..." 68 | git fetch origin 69 | 70 | # Get current branch name 71 | current_branch=$(git branch --show-current) 72 | 73 | # If we're not on main or master, try to checkout one of them 74 | if [[ "$current_branch" != "main" && "$current_branch" != "master" ]]; then 75 | echo "Current branch: $current_branch" 76 | if git show-ref --verify --quiet refs/heads/main; then 77 | echo "Switching to main branch..." 78 | git checkout main 79 | current_branch="main" 80 | elif git show-ref --verify --quiet refs/heads/master; then 81 | echo "Switching to master branch..." 82 | git checkout master 83 | current_branch="master" 84 | else 85 | echo "⚠️ Neither main nor master branch found, staying on current branch: $current_branch" 86 | fi 87 | fi 88 | 89 | echo "Pulling latest changes from $current_branch..." 90 | git pull origin "$current_branch" 91 | 92 | # Create and push the tag 93 | echo "🏷️ Creating tag $new_version..." 94 | git tag "$new_version" 95 | 96 | echo "🚀 Pushing tag to trigger release..." 97 | git push origin "$new_version" 98 | 99 | echo "✅ Release $new_version has been triggered!" 100 | echo "🔗 Check the release at: https://github.com/threefoldtech/zinit/releases" 101 | echo "🔗 Monitor the build at: https://github.com/threefoldtech/zinit/actions" 102 | -------------------------------------------------------------------------------- /src/app/api.rs: -------------------------------------------------------------------------------- 1 | use super::rpc::{ 2 | ZinitLoggingApiServer, ZinitRpcApiServer, ZinitServiceApiServer, ZinitSystemApiServer, 3 | }; 4 | use crate::zinit::ZInit; 5 | use anyhow::{bail, Context, Result}; 6 | use jsonrpsee::server::ServerHandle; 7 | use reth_ipc::server::Builder; 8 | use serde::{Deserialize, Serialize}; 9 | use serde_json::Value; 10 | use std::collections::HashMap; 11 | use std::sync::Arc; 12 | use tokio::sync::Mutex; 13 | use tower_http::cors::{AllowHeaders, AllowMethods}; 14 | use tower_http::cors::{Any, CorsLayer}; 15 | 16 | #[derive(Clone, Debug, Deserialize, Serialize)] 17 | #[serde(rename_all = "lowercase")] 18 | struct ZinitResponse { 19 | pub state: ZinitState, 20 | pub body: Value, 21 | } 22 | 23 | #[derive(Clone, Debug, Deserialize, Serialize)] 24 | #[serde(rename_all = "lowercase")] 25 | enum ZinitState { 26 | Ok, 27 | Error, 28 | } 29 | 30 | #[derive(Clone, Debug, Deserialize, Serialize)] 31 | #[serde(rename_all = "lowercase")] 32 | pub struct Status { 33 | pub name: String, 34 | pub pid: u32, 35 | pub state: String, 36 | pub target: String, 37 | pub after: HashMap, 38 | } 39 | 40 | /// Service stats information 41 | #[derive(Clone, Debug, Deserialize, Serialize)] 42 | #[serde(rename_all = "lowercase")] 43 | pub struct Stats { 44 | pub name: String, 45 | pub pid: u32, 46 | pub memory_usage: u64, 47 | pub cpu_usage: f32, 48 | pub children: Vec, 49 | } 50 | 51 | /// Child process stats information 52 | #[derive(Clone, Debug, Deserialize, Serialize)] 53 | #[serde(rename_all = "lowercase")] 54 | pub struct ChildStats { 55 | pub pid: u32, 56 | pub memory_usage: u64, 57 | pub cpu_usage: f32, 58 | } 59 | 60 | pub struct ApiServer { 61 | _handle: ServerHandle, 62 | } 63 | 64 | #[derive(Clone)] 65 | pub struct Api { 66 | pub zinit: ZInit, 67 | pub http_server_handle: Arc>>, 68 | } 69 | 70 | impl Api { 71 | pub fn new(zinit: ZInit) -> Api { 72 | Api { 73 | zinit, 74 | http_server_handle: Arc::new(Mutex::new(None)), 75 | } 76 | } 77 | 78 | pub async fn serve(&self, endpoint: String) -> Result { 79 | let server = Builder::default().build(endpoint); 80 | let mut module = ZinitRpcApiServer::into_rpc(self.clone()); 81 | module.merge(ZinitSystemApiServer::into_rpc(self.clone()))?; 82 | module.merge(ZinitServiceApiServer::into_rpc(self.clone()))?; 83 | module.merge(ZinitLoggingApiServer::into_rpc(self.clone()))?; 84 | 85 | let _handle = server.start(module).await?; 86 | 87 | Ok(ApiServer { _handle }) 88 | } 89 | 90 | /// Start an HTTP/RPC server at a specified address 91 | pub async fn start_http_server(&self, address: String) -> Result { 92 | // Parse the address string 93 | let socket_addr = address 94 | .parse::() 95 | .context("Failed to parse socket address")?; 96 | 97 | let cors = CorsLayer::new() 98 | // Allow `POST` when accessing the resource 99 | .allow_methods(AllowMethods::any()) 100 | // Allow requests from any origin 101 | .allow_origin(Any) 102 | .allow_headers(AllowHeaders::any()); 103 | let middleware = tower::ServiceBuilder::new().layer(cors); 104 | 105 | // Create the JSON-RPC server with CORS support 106 | let server_rpc = jsonrpsee::server::ServerBuilder::default() 107 | .set_http_middleware(middleware) 108 | .build(socket_addr) 109 | .await?; 110 | 111 | // Create and merge all API modules 112 | let mut rpc_module = ZinitRpcApiServer::into_rpc(self.clone()); 113 | rpc_module.merge(ZinitSystemApiServer::into_rpc(self.clone()))?; 114 | rpc_module.merge(ZinitServiceApiServer::into_rpc(self.clone()))?; 115 | rpc_module.merge(ZinitLoggingApiServer::into_rpc(self.clone()))?; 116 | 117 | // Start the server 118 | let handle = server_rpc.start(rpc_module); 119 | 120 | // Store the handle 121 | let mut http_handle = self.http_server_handle.lock().await; 122 | *http_handle = Some(handle); 123 | 124 | Ok(format!("HTTP/RPC server started at {}", address)) 125 | } 126 | 127 | /// Stop the HTTP/RPC server if running 128 | pub async fn stop_http_server(&self) -> Result<()> { 129 | let mut http_handle = self.http_server_handle.lock().await; 130 | 131 | if http_handle.is_some() { 132 | // The handle is automatically dropped, which should stop the server 133 | *http_handle = None; 134 | Ok(()) 135 | } else { 136 | bail!("No HTTP/RPC server is currently running") 137 | } 138 | } 139 | } 140 | -------------------------------------------------------------------------------- /src/app/mod.rs: -------------------------------------------------------------------------------- 1 | pub mod api; 2 | pub mod rpc; 3 | 4 | use crate::zinit; 5 | use anyhow::{Context, Result}; 6 | use api::ApiServer; 7 | use reth_ipc::client::IpcClientBuilder; 8 | use rpc::ZinitLoggingApiClient; 9 | use rpc::ZinitServiceApiClient; 10 | use rpc::ZinitSystemApiClient; 11 | use serde_yaml as encoder; 12 | use std::net::ToSocketAddrs; 13 | use std::path::{Path, PathBuf}; 14 | use tokio::fs; 15 | use tokio::signal; 16 | use tokio::time; 17 | use tokio_stream::wrappers::ReceiverStream; 18 | use tokio_stream::Stream; 19 | 20 | fn logger(level: log::LevelFilter) -> Result<()> { 21 | let logger = fern::Dispatch::new() 22 | .format(|out, message, record| { 23 | out.finish(format_args!( 24 | "zinit: {} ({}) {}", 25 | record.level(), 26 | record.target(), 27 | message 28 | )) 29 | }) 30 | .level(level) 31 | .chain(std::io::stdout()); 32 | let logger = match std::fs::OpenOptions::new().write(true).open("/dev/kmsg") { 33 | Ok(file) => logger.chain(file), 34 | Err(_err) => logger, 35 | }; 36 | logger.apply()?; 37 | 38 | Ok(()) 39 | } 40 | 41 | fn absolute>(p: P) -> Result { 42 | let p = p.as_ref(); 43 | let result = if p.is_absolute() { 44 | p.to_path_buf() 45 | } else { 46 | let mut current = std::env::current_dir()?; 47 | current.push(p); 48 | current 49 | }; 50 | 51 | Ok(result) 52 | } 53 | 54 | pub async fn init( 55 | cap: usize, 56 | config: &str, 57 | socket: &str, 58 | container: bool, 59 | debug: bool, 60 | ) -> Result { 61 | fs::create_dir_all(config) 62 | .await 63 | .with_context(|| format!("failed to create config directory '{}'", config))?; 64 | if let Err(err) = logger(if debug { 65 | log::LevelFilter::Debug 66 | } else { 67 | log::LevelFilter::Info 68 | }) { 69 | eprintln!("failed to setup logging: {}", err); 70 | } 71 | 72 | let config = absolute(Path::new(config)).context("failed to get config dire absolute path")?; 73 | let socket_path = 74 | absolute(Path::new(socket)).context("failed to get socket file absolute path")?; 75 | 76 | if let Some(dir) = socket_path.parent() { 77 | fs::create_dir_all(dir) 78 | .await 79 | .with_context(|| format!("failed to create directory {:?}", dir))?; 80 | } 81 | 82 | let _ = fs::remove_file(&socket).await; 83 | 84 | debug!("switching to home dir: {}", config.display()); 85 | std::env::set_current_dir(&config).with_context(|| { 86 | format!( 87 | "failed to switch working directory to '{}'", 88 | config.display() 89 | ) 90 | })?; 91 | 92 | let init = zinit::ZInit::new(cap, container); 93 | 94 | init.serve(); 95 | 96 | let services = zinit::config::load_dir(&config)?; 97 | for (k, v) in services { 98 | if let Err(err) = init.monitor(&k, v).await { 99 | error!("failed to monitor service {}: {}", k, err); 100 | }; 101 | } 102 | let a = api::Api::new(init); 103 | a.serve(socket.into()).await 104 | } 105 | 106 | pub async fn list(socket: &str) -> Result<()> { 107 | let client = IpcClientBuilder::default().build(socket.into()).await?; 108 | let results = client.list().await?; 109 | encoder::to_writer(std::io::stdout(), &results)?; 110 | Ok(()) 111 | } 112 | 113 | pub async fn shutdown(socket: &str) -> Result<()> { 114 | let client = IpcClientBuilder::default().build(socket.into()).await?; 115 | client.shutdown().await?; 116 | Ok(()) 117 | } 118 | 119 | pub async fn reboot(socket: &str) -> Result<()> { 120 | let client = IpcClientBuilder::default().build(socket.into()).await?; 121 | client.reboot().await?; 122 | Ok(()) 123 | } 124 | 125 | pub async fn status(socket: &str, name: String) -> Result<()> { 126 | let client = IpcClientBuilder::default().build(socket.into()).await?; 127 | let results = client.status(name).await?; 128 | encoder::to_writer(std::io::stdout(), &results)?; 129 | Ok(()) 130 | } 131 | 132 | pub async fn start(socket: &str, name: String) -> Result<()> { 133 | let client = IpcClientBuilder::default().build(socket.into()).await?; 134 | client.start(name).await?; 135 | Ok(()) 136 | } 137 | 138 | pub async fn stop(socket: &str, name: String) -> Result<()> { 139 | let client = IpcClientBuilder::default().build(socket.into()).await?; 140 | client.stop(name).await?; 141 | Ok(()) 142 | } 143 | 144 | pub async fn restart(socket: &str, name: String) -> Result<()> { 145 | let client = IpcClientBuilder::default().build(socket.into()).await?; 146 | client.stop(name.clone()).await?; 147 | //pull status 148 | for _ in 0..20 { 149 | let result = client.status(name.clone()).await?; 150 | if result.pid == 0 && result.target == "Down" { 151 | client.start(name.clone()).await?; 152 | return Ok(()); 153 | } 154 | time::sleep(std::time::Duration::from_secs(1)).await; 155 | } 156 | // process not stopped try to kill it 157 | client.kill(name.clone(), "SIGKILL".into()).await?; 158 | client.start(name).await?; 159 | Ok(()) 160 | } 161 | 162 | pub async fn forget(socket: &str, name: String) -> Result<()> { 163 | let client = IpcClientBuilder::default().build(socket.into()).await?; 164 | client.forget(name).await?; 165 | Ok(()) 166 | } 167 | 168 | pub async fn monitor(socket: &str, name: String) -> Result<()> { 169 | let client = IpcClientBuilder::default().build(socket.into()).await?; 170 | client.monitor(name).await?; 171 | Ok(()) 172 | } 173 | 174 | pub async fn kill(socket: &str, name: String, signal: String) -> Result<()> { 175 | let client = IpcClientBuilder::default().build(socket.into()).await?; 176 | client.kill(name, signal).await?; 177 | Ok(()) 178 | } 179 | 180 | pub async fn stats(socket: &str, name: String) -> Result<()> { 181 | let client = IpcClientBuilder::default().build(socket.into()).await?; 182 | let results = client.stats(name).await?; 183 | encoder::to_writer(std::io::stdout(), &results)?; 184 | Ok(()) 185 | } 186 | 187 | pub async fn logs( 188 | socket: &str, 189 | filter: Option, 190 | follow: bool, 191 | ) -> Result + Unpin> { 192 | let client = IpcClientBuilder::default().build(socket.into()).await?; 193 | if let Some(ref filter) = filter { 194 | client.status(filter.clone()).await?; 195 | } 196 | let logs = client.logs(filter.clone()).await?; 197 | let (tx, rx) = tokio::sync::mpsc::channel(2000); 198 | 199 | let logs_sub = if follow { 200 | Some(client.log_subscribe(filter).await?) 201 | } else { 202 | None 203 | }; 204 | tokio::task::spawn(async move { 205 | for log in logs { 206 | if tx.send(log).await.is_err() { 207 | if let Some(logs_sub) = logs_sub { 208 | let _ = logs_sub.unsubscribe().await; 209 | } 210 | // error means receiver is dead, so just quit 211 | return; 212 | } 213 | } 214 | let Some(mut logs_sub) = logs_sub else { return }; 215 | loop { 216 | match logs_sub.next().await { 217 | Some(Ok(log)) => { 218 | if tx.send(log).await.is_err() { 219 | let _ = logs_sub.unsubscribe().await; 220 | return; 221 | } 222 | } 223 | Some(Err(e)) => { 224 | log::error!("Failed to get new log from subscription: {e}"); 225 | return; 226 | } 227 | _ => return, 228 | } 229 | } 230 | }); 231 | 232 | Ok(ReceiverStream::new(rx)) 233 | } 234 | 235 | /// Start an HTTP/RPC proxy server for the Zinit API at the specified address 236 | pub async fn proxy(sock: &str, address: String) -> Result<()> { 237 | // Parse the socket address 238 | let _socket_addr = address 239 | .to_socket_addrs() 240 | .context("Failed to parse socket address")? 241 | .next() 242 | .context("No valid socket address found")?; 243 | 244 | println!("Starting HTTP/RPC server on {}", address); 245 | println!("Connecting to Zinit daemon at {}", sock); 246 | 247 | // Connect to the existing Zinit daemon through the Unix socket 248 | let client = IpcClientBuilder::default().build(sock.into()).await?; 249 | 250 | // Issue an RPC call to start the HTTP server on the specified address 251 | let result = client.start_http_server(address.clone()).await?; 252 | 253 | println!("{}", result); 254 | println!("Press Ctrl+C to stop"); 255 | 256 | // Wait for Ctrl+C to shutdown 257 | signal::ctrl_c().await?; 258 | 259 | // Shutdown the HTTP server 260 | client.stop_http_server().await?; 261 | 262 | println!("HTTP/RPC server stopped"); 263 | 264 | Ok(()) 265 | } 266 | -------------------------------------------------------------------------------- /src/app/rpc.rs: -------------------------------------------------------------------------------- 1 | use crate::app::api::{ChildStats, Stats, Status}; 2 | use crate::zinit::config; 3 | use async_trait::async_trait; 4 | use jsonrpsee::core::{RpcResult, SubscriptionResult}; 5 | use jsonrpsee::proc_macros::rpc; 6 | use jsonrpsee::types::{ErrorCode, ErrorObjectOwned}; 7 | use jsonrpsee::PendingSubscriptionSink; 8 | use serde_json::{Map, Value}; 9 | use std::collections::HashMap; 10 | use std::str::FromStr; 11 | use tokio_stream::StreamExt; 12 | 13 | use super::api::Api; 14 | 15 | // Custom error codes for Zinit 16 | const SERVICE_NOT_FOUND: i32 = -32000; 17 | const SERVICE_IS_UP: i32 = -32002; 18 | const SHUTTING_DOWN: i32 = -32006; 19 | const SERVICE_ALREADY_EXISTS: i32 = -32007; 20 | const SERVICE_FILE_ERROR: i32 = -32008; 21 | 22 | // Include the OpenRPC specification 23 | const OPENRPC_SPEC: &str = include_str!("../../openrpc.json"); 24 | 25 | /// RPC methods for discovery. 26 | #[rpc(server, client)] 27 | pub trait ZinitRpcApi { 28 | /// Returns the OpenRPC specification as a string. 29 | #[method(name = "rpc.discover")] 30 | async fn discover(&self) -> RpcResult; 31 | } 32 | 33 | #[async_trait] 34 | impl ZinitRpcApiServer for Api { 35 | async fn discover(&self) -> RpcResult { 36 | Ok(OPENRPC_SPEC.to_string()) 37 | } 38 | } 39 | 40 | /// RPC methods for service management. 41 | #[rpc(server, client, namespace = "service")] 42 | pub trait ZinitServiceApi { 43 | /// List all monitored services and their current state. 44 | /// Returns a map where keys are service names and values are state strings. 45 | #[method(name = "list")] 46 | async fn list(&self) -> RpcResult>; 47 | 48 | /// Get the detailed status of a specific service. 49 | #[method(name = "status")] 50 | async fn status(&self, name: String) -> RpcResult; 51 | 52 | /// Start a specific service. 53 | #[method(name = "start")] 54 | async fn start(&self, name: String) -> RpcResult<()>; 55 | 56 | /// Stop a specific service. 57 | #[method(name = "stop")] 58 | async fn stop(&self, name: String) -> RpcResult<()>; 59 | 60 | /// Load and monitor a new service from its configuration file (e.g., "service_name.yaml"). 61 | #[method(name = "monitor")] 62 | async fn monitor(&self, name: String) -> RpcResult<()>; 63 | 64 | /// Stop monitoring a service and remove it from management. 65 | #[method(name = "forget")] 66 | async fn forget(&self, name: String) -> RpcResult<()>; 67 | 68 | /// Send a signal (e.g., "SIGTERM", "SIGKILL") to a specific service process. 69 | #[method(name = "kill")] 70 | async fn kill(&self, name: String, signal: String) -> RpcResult<()>; 71 | 72 | /// Create a new service configuration file (e.g., "service_name.yaml") 73 | /// with the provided content (JSON map representing YAML structure). 74 | /// Returns a success message string. 75 | #[method(name = "create")] 76 | async fn create(&self, name: String, content: Map) -> RpcResult; 77 | 78 | /// Delete a service configuration file. 79 | /// Returns a success message string. 80 | #[method(name = "delete")] 81 | async fn delete(&self, name: String) -> RpcResult; 82 | 83 | /// Get the content of a service configuration file as a JSON Value. 84 | #[method(name = "get")] 85 | async fn get(&self, name: String) -> RpcResult; 86 | 87 | /// Get memory and CPU usage statistics for a service. 88 | #[method(name = "stats")] 89 | async fn stats(&self, name: String) -> RpcResult; 90 | } 91 | 92 | #[async_trait] 93 | impl ZinitServiceApiServer for Api { 94 | async fn list(&self) -> RpcResult> { 95 | let services = self 96 | .zinit 97 | .list() 98 | .await 99 | .map_err(|_| ErrorObjectOwned::from(ErrorCode::InternalError))?; 100 | 101 | let mut map: HashMap = HashMap::new(); 102 | for service in services { 103 | let state = self 104 | .zinit 105 | .status(&service) 106 | .await 107 | .map_err(|_| ErrorObjectOwned::from(ErrorCode::InternalError))?; 108 | map.insert(service, format!("{:?}", state.state)); 109 | } 110 | Ok(map) 111 | } 112 | 113 | async fn status(&self, name: String) -> RpcResult { 114 | let status = self 115 | .zinit 116 | .status(&name) 117 | .await 118 | .map_err(|_| ErrorObjectOwned::from(ErrorCode::InternalError))?; 119 | 120 | let result = Status { 121 | name: name.clone(), 122 | pid: status.pid.as_raw() as u32, 123 | state: format!("{:?}", status.state), 124 | target: format!("{:?}", status.target), 125 | after: { 126 | let mut after = HashMap::new(); 127 | for service in status.service.after { 128 | let status = match self.zinit.status(&service).await { 129 | Ok(dep) => dep.state, 130 | Err(_) => crate::zinit::State::Unknown, 131 | }; 132 | after.insert(service, format!("{:?}", status)); 133 | } 134 | after 135 | }, 136 | }; 137 | 138 | Ok(result) 139 | } 140 | 141 | async fn start(&self, name: String) -> RpcResult<()> { 142 | self.zinit 143 | .start(name) 144 | .await 145 | .map_err(|_| ErrorObjectOwned::from(ErrorCode::ServerError(SERVICE_IS_UP))) 146 | } 147 | 148 | async fn stop(&self, name: String) -> RpcResult<()> { 149 | self.zinit 150 | .stop(name) 151 | .await 152 | .map_err(|_| ErrorObjectOwned::from(ErrorCode::InternalError)) 153 | } 154 | 155 | async fn monitor(&self, name: String) -> RpcResult<()> { 156 | if let Ok((name_str, service)) = config::load(format!("{}.yaml", name)) 157 | .map_err(|_| ErrorObjectOwned::from(ErrorCode::InternalError)) 158 | { 159 | self.zinit 160 | .monitor(name_str, service) 161 | .await 162 | .map_err(|_| ErrorObjectOwned::from(ErrorCode::InternalError)) 163 | } else { 164 | Err(ErrorObjectOwned::from(ErrorCode::InternalError)) 165 | } 166 | } 167 | 168 | async fn forget(&self, name: String) -> RpcResult<()> { 169 | self.zinit 170 | .forget(name) 171 | .await 172 | .map_err(|_| ErrorObjectOwned::from(ErrorCode::InternalError)) 173 | } 174 | 175 | async fn kill(&self, name: String, signal: String) -> RpcResult<()> { 176 | if let Ok(sig) = nix::sys::signal::Signal::from_str(&signal.to_uppercase()) { 177 | self.zinit 178 | .kill(name, sig) 179 | .await 180 | .map_err(|_e| ErrorObjectOwned::from(ErrorCode::InternalError)) 181 | } else { 182 | Err(ErrorObjectOwned::from(ErrorCode::InternalError)) 183 | } 184 | } 185 | 186 | async fn create(&self, name: String, content: Map) -> RpcResult { 187 | use std::fs; 188 | use std::io::Write; 189 | use std::path::PathBuf; 190 | 191 | // Validate service name (no path traversal, valid characters) 192 | if name.contains('/') || name.contains('\\') || name.contains('.') { 193 | return Err(ErrorObjectOwned::from(ErrorCode::InternalError)); 194 | } 195 | 196 | // Construct the file path 197 | let file_path = PathBuf::from(format!("{}.yaml", name)); 198 | 199 | // Check if the service file already exists 200 | if file_path.exists() { 201 | return Err(ErrorObjectOwned::from(ErrorCode::ServerError( 202 | SERVICE_ALREADY_EXISTS, 203 | ))); 204 | } 205 | 206 | // Convert the JSON content to YAML 207 | let yaml_content = serde_yaml::to_string(&content) 208 | .map_err(|_| ErrorObjectOwned::from(ErrorCode::InternalError))?; 209 | 210 | // Write the YAML content to the file 211 | let mut file = fs::File::create(&file_path) 212 | .map_err(|_| ErrorObjectOwned::from(ErrorCode::ServerError(SERVICE_FILE_ERROR)))?; 213 | 214 | file.write_all(yaml_content.as_bytes()) 215 | .map_err(|_| ErrorObjectOwned::from(ErrorCode::ServerError(SERVICE_FILE_ERROR)))?; 216 | 217 | Ok(format!("Service '{}' created successfully", name)) 218 | } 219 | 220 | async fn delete(&self, name: String) -> RpcResult { 221 | use std::fs; 222 | use std::path::PathBuf; 223 | 224 | // Validate service name (no path traversal, valid characters) 225 | if name.contains('/') || name.contains('\\') || name.contains('.') { 226 | return Err(ErrorObjectOwned::from(ErrorCode::InternalError)); 227 | } 228 | 229 | // Construct the file path 230 | let file_path = PathBuf::from(format!("{}.yaml", name)); 231 | 232 | // Check if the service file exists 233 | if !file_path.exists() { 234 | return Err(ErrorObjectOwned::from(ErrorCode::ServerError( 235 | SERVICE_NOT_FOUND, 236 | ))); 237 | } 238 | 239 | // Delete the file 240 | fs::remove_file(&file_path) 241 | .map_err(|_| ErrorObjectOwned::from(ErrorCode::ServerError(SERVICE_FILE_ERROR)))?; 242 | 243 | Ok(format!("Service '{}' deleted successfully", name)) 244 | } 245 | 246 | async fn get(&self, name: String) -> RpcResult { 247 | use std::fs; 248 | use std::path::PathBuf; 249 | 250 | // Validate service name (no path traversal, valid characters) 251 | if name.contains('/') || name.contains('\\') || name.contains('.') { 252 | return Err(ErrorObjectOwned::from(ErrorCode::InternalError)); 253 | } 254 | 255 | // Construct the file path 256 | let file_path = PathBuf::from(format!("{}.yaml", name)); 257 | 258 | // Check if the service file exists 259 | if !file_path.exists() { 260 | return Err(ErrorObjectOwned::from(ErrorCode::ServerError( 261 | SERVICE_NOT_FOUND, 262 | ))); 263 | } 264 | 265 | // Read the file content 266 | let yaml_content = fs::read_to_string(&file_path) 267 | .map_err(|_| ErrorObjectOwned::from(ErrorCode::ServerError(SERVICE_FILE_ERROR)))?; 268 | 269 | // Parse YAML to JSON 270 | let yaml_value: serde_yaml::Value = serde_yaml::from_str(&yaml_content) 271 | .map_err(|_| ErrorObjectOwned::from(ErrorCode::InternalError))?; 272 | 273 | // Convert YAML value to JSON value 274 | let json_value = serde_json::to_value(yaml_value) 275 | .map_err(|_| ErrorObjectOwned::from(ErrorCode::InternalError))?; 276 | 277 | Ok(json_value) 278 | } 279 | 280 | async fn stats(&self, name: String) -> RpcResult { 281 | let stats = self 282 | .zinit 283 | .stats(&name) 284 | .await 285 | .map_err(|_| ErrorObjectOwned::from(ErrorCode::InternalError))?; 286 | 287 | let result = Stats { 288 | name: name.clone(), 289 | pid: stats.pid as u32, 290 | memory_usage: stats.memory_usage, 291 | cpu_usage: stats.cpu_usage, 292 | children: stats 293 | .children 294 | .into_iter() 295 | .map(|child| ChildStats { 296 | pid: child.pid as u32, 297 | memory_usage: child.memory_usage, 298 | cpu_usage: child.cpu_usage, 299 | }) 300 | .collect(), 301 | }; 302 | 303 | Ok(result) 304 | } 305 | } 306 | 307 | /// RPC methods for system-level operations. 308 | #[rpc(server, client, namespace = "system")] 309 | pub trait ZinitSystemApi { 310 | /// Initiate system shutdown process. 311 | #[method(name = "shutdown")] 312 | async fn shutdown(&self) -> RpcResult<()>; 313 | 314 | /// Initiate system reboot process. 315 | #[method(name = "reboot")] 316 | async fn reboot(&self) -> RpcResult<()>; 317 | 318 | /// Start an HTTP/RPC server at the specified address 319 | #[method(name = "start_http_server")] 320 | async fn start_http_server(&self, address: String) -> RpcResult; 321 | 322 | /// Stop the HTTP/RPC server if running 323 | #[method(name = "stop_http_server")] 324 | async fn stop_http_server(&self) -> RpcResult<()>; 325 | } 326 | 327 | #[async_trait] 328 | impl ZinitSystemApiServer for Api { 329 | async fn shutdown(&self) -> RpcResult<()> { 330 | self.zinit 331 | .shutdown() 332 | .await 333 | .map_err(|_e| ErrorObjectOwned::from(ErrorCode::ServerError(SHUTTING_DOWN))) 334 | } 335 | 336 | async fn reboot(&self) -> RpcResult<()> { 337 | self.zinit 338 | .reboot() 339 | .await 340 | .map_err(|_| ErrorObjectOwned::from(ErrorCode::InternalError)) 341 | } 342 | 343 | async fn start_http_server(&self, address: String) -> RpcResult { 344 | // Call the method from the API implementation 345 | match crate::app::api::Api::start_http_server(self, address).await { 346 | Ok(result) => Ok(result), 347 | Err(_) => Err(ErrorObjectOwned::from(ErrorCode::InternalError)), 348 | } 349 | } 350 | 351 | async fn stop_http_server(&self) -> RpcResult<()> { 352 | // Call the method from the API implementation 353 | match crate::app::api::Api::stop_http_server(self).await { 354 | Ok(_) => Ok(()), 355 | Err(_) => Err(ErrorObjectOwned::from(ErrorCode::InternalError)), 356 | } 357 | } 358 | } 359 | 360 | /// RPC subscription methods for streaming data. 361 | #[rpc(server, client, namespace = "stream")] 362 | pub trait ZinitLoggingApi { 363 | #[method(name = "currentLogs")] 364 | async fn logs(&self, name: Option) -> RpcResult>; 365 | /// Subscribe to log messages generated by zinit and monitored services. 366 | /// An optional filter can be provided to only receive logs containing the filter string. 367 | /// The subscription returns a stream of log lines (String). 368 | #[subscription(name = "subscribeLogs", item = String)] 369 | async fn log_subscribe(&self, filter: Option) -> SubscriptionResult; 370 | } 371 | 372 | #[async_trait] 373 | impl ZinitLoggingApiServer for Api { 374 | async fn logs(&self, name: Option) -> RpcResult> { 375 | let filter = name.map(|n| format!("{n}:")); 376 | Ok( 377 | tokio_stream::wrappers::ReceiverStream::new(self.zinit.logs(true, false).await) 378 | .filter_map(|l| { 379 | if let Some(ref filter) = filter { 380 | if l[4..].starts_with(filter) { 381 | Some(l.to_string()) 382 | } else { 383 | None 384 | } 385 | } else { 386 | Some(l.to_string()) 387 | } 388 | }) 389 | .collect() 390 | .await, 391 | ) 392 | } 393 | 394 | async fn log_subscribe( 395 | &self, 396 | sink: PendingSubscriptionSink, 397 | name: Option, 398 | ) -> SubscriptionResult { 399 | let sink = sink.accept().await?; 400 | let filter = name.map(|n| format!("{n}:")); 401 | let mut stream = 402 | tokio_stream::wrappers::ReceiverStream::new(self.zinit.logs(false, true).await) 403 | .filter_map(|l| { 404 | if let Some(ref filter) = filter { 405 | if l[4..].starts_with(filter) { 406 | Some(l.to_string()) 407 | } else { 408 | None 409 | } 410 | } else { 411 | Some(l.to_string()) 412 | } 413 | }); 414 | while let Some(log) = stream.next().await { 415 | if sink.send(log.into()).await.is_err() { 416 | break; 417 | } 418 | } 419 | 420 | Ok(()) 421 | } 422 | } 423 | -------------------------------------------------------------------------------- /src/bin/testapp.rs: -------------------------------------------------------------------------------- 1 | #[tokio::main] 2 | async fn main() { 3 | println!("hello from testapp"); 4 | } 5 | 6 | // extern crate zinit; 7 | 8 | // use anyhow::Result; 9 | // use serde_json::json; 10 | // use std::env; 11 | // use tokio::time::{sleep, Duration}; 12 | 13 | // use zinit::testapp; 14 | 15 | // #[tokio::main] 16 | // async fn main() -> Result<()> { 17 | // // Define paths for socket and config 18 | // let temp_dir = env::temp_dir(); 19 | // let socket_path = temp_dir 20 | // .join("zinit-test.sock") 21 | // .to_str() 22 | // .unwrap() 23 | // .to_string(); 24 | // let config_dir = temp_dir 25 | // .join("zinit-test-config") 26 | // .to_str() 27 | // .unwrap() 28 | // .to_string(); 29 | 30 | // println!("Starting zinit with socket at: {}", socket_path); 31 | // println!("Using config directory: {}", config_dir); 32 | 33 | // // Start zinit in the background 34 | // testapp::start_zinit(&socket_path, &config_dir).await?; 35 | 36 | // // Wait for zinit to initialize 37 | // sleep(Duration::from_secs(2)).await; 38 | 39 | // // Create a client to communicate with zinit 40 | // let client = Client::new(&socket_path); 41 | 42 | // // Create service configurations 43 | // println!("Creating service configurations..."); 44 | 45 | // // Create a find service 46 | // testapp::create_service_config( 47 | // &config_dir, 48 | // "find-service", 49 | // "find / -name \"*.txt\" -type f", 50 | // ) 51 | // .await?; 52 | 53 | // // Create a sleep service with echo 54 | // testapp::create_service_config( 55 | // &config_dir, 56 | // "sleep-service", 57 | // "sh -c 'echo Starting sleep; sleep 30; echo Finished sleep'", 58 | // ) 59 | // .await?; 60 | 61 | // // Wait for zinit to load the configurations 62 | // sleep(Duration::from_secs(1)).await; 63 | 64 | // // Tell zinit to monitor our services 65 | // println!("Monitoring services..."); 66 | // client.monitor("find-service").await?; 67 | // client.monitor("sleep-service").await?; 68 | 69 | // // List all services 70 | // println!("\nListing all services:"); 71 | // let services = client.list().await?; 72 | // for (name, status) in services { 73 | // println!("Service: {} - Status: {}", name, status); 74 | // } 75 | 76 | // // Start the find service 77 | // println!("\nStarting find-service..."); 78 | // client.start("find-service").await?; 79 | 80 | // // Wait a bit and check status 81 | // sleep(Duration::from_secs(2)).await; 82 | // let status = client.status("find-service").await?; 83 | // println!("find-service status: {:?}", status); 84 | 85 | // // Start the sleep service 86 | // println!("\nStarting sleep-service..."); 87 | // client.start("sleep-service").await?; 88 | 89 | // // Wait a bit and check status 90 | // sleep(Duration::from_secs(2)).await; 91 | // let status = client.status("sleep-service").await?; 92 | // println!("sleep-service status: {:?}", status); 93 | 94 | // // Stop the find service 95 | // println!("\nStopping find-service..."); 96 | // client.stop("find-service").await?; 97 | 98 | // // Wait a bit and check status 99 | // sleep(Duration::from_secs(2)).await; 100 | // let status = client.status("find-service").await?; 101 | // println!("find-service status after stopping: {:?}", status); 102 | 103 | // // Kill the sleep service with SIGTERM 104 | // println!("\nKilling sleep-service with SIGTERM..."); 105 | // client.kill("sleep-service", "SIGTERM").await?; 106 | 107 | // // Wait a bit and check status 108 | // sleep(Duration::from_secs(2)).await; 109 | // let status = client.status("sleep-service").await?; 110 | // println!("sleep-service status after killing: {:?}", status); 111 | 112 | // // Cleanup - forget services 113 | // println!("\nForgetting services..."); 114 | // if status.pid == 0 { 115 | // // Only forget if it's not running 116 | // client.forget("sleep-service").await?; 117 | // } 118 | // client.forget("find-service").await?; 119 | 120 | // // Demonstrate service file operations 121 | // println!("\nDemonstrating service file operations..."); 122 | 123 | // // Create a new service using the API 124 | // println!("Creating a new service via API..."); 125 | // let service_content = json!({ 126 | // "exec": "echo 'Hello from API-created service'", 127 | // "oneshot": true, 128 | // "log": "stdout" 129 | // }) 130 | // .as_object() 131 | // .unwrap() 132 | // .clone(); 133 | 134 | // let result = client 135 | // .create_service("api-service", service_content) 136 | // .await?; 137 | // println!("Create service result: {}", result); 138 | 139 | // // Get the service configuration 140 | // println!("\nGetting service configuration..."); 141 | // let config = client.get_service("api-service").await?; 142 | // println!( 143 | // "Service configuration: {}", 144 | // serde_json::to_string_pretty(&config)? 145 | // ); 146 | 147 | // // Monitor and start the new service 148 | // println!("\nMonitoring and starting the new service..."); 149 | // client.monitor("api-service").await?; 150 | // client.start("api-service").await?; 151 | 152 | // // Wait a bit and check status 153 | // sleep(Duration::from_secs(2)).await; 154 | // let status = client.status("api-service").await?; 155 | // println!("api-service status: {:?}", status); 156 | 157 | // // Delete the service 158 | // println!("\nDeleting the service..."); 159 | // if status.pid == 0 { 160 | // // Only forget if it's not running 161 | // client.forget("api-service").await?; 162 | // let result = client.delete_service("api-service").await?; 163 | // println!("Delete service result: {}", result); 164 | // } 165 | 166 | // // Shutdown zinit 167 | // println!("\nShutting down zinit..."); 168 | // client.shutdown().await?; 169 | 170 | // println!("\nTest completed successfully!"); 171 | // Ok(()) 172 | // } 173 | -------------------------------------------------------------------------------- /src/lib.rs: -------------------------------------------------------------------------------- 1 | extern crate serde; 2 | #[macro_use] 3 | extern crate anyhow; 4 | #[macro_use] 5 | extern crate log; 6 | extern crate tokio; 7 | 8 | pub mod app; 9 | pub mod manager; 10 | pub mod testapp; 11 | pub mod zinit; 12 | -------------------------------------------------------------------------------- /src/main.rs: -------------------------------------------------------------------------------- 1 | extern crate zinit; 2 | 3 | use anyhow::Result; 4 | use clap::{App, Arg, SubCommand}; 5 | use git_version::git_version; 6 | 7 | use tokio_stream::StreamExt; 8 | use zinit::app; 9 | 10 | const GIT_VERSION: &str = git_version!(args = ["--tags", "--always", "--dirty=-modified"]); 11 | 12 | #[tokio::main] 13 | async fn main() -> Result<()> { 14 | let matches = App::new("zinit") 15 | .author("ThreeFold Tech, https://github.com/threefoldtech") 16 | .version(GIT_VERSION) 17 | .about("A runit replacement") 18 | .arg(Arg::with_name("socket").value_name("SOCKET").short("s").long("socket").default_value("/tmp/zinit.sock").help("path to unix socket")) 19 | .arg(Arg::with_name("debug").short("d").long("debug").help("run in debug mode")) 20 | .subcommand( 21 | SubCommand::with_name("init") 22 | .arg( 23 | Arg::with_name("config") 24 | .value_name("DIR") 25 | .short("c") 26 | .long("config") 27 | .help("service configurations directory"), 28 | ) 29 | .arg( 30 | Arg::with_name("buffer") 31 | .value_name("BUFFER") 32 | .short("b") 33 | .long("buffer") 34 | .help("buffer size (in lines) to keep services logs") 35 | .default_value("2000") 36 | ) 37 | .arg(Arg::with_name("container").long("container").help("run in container mode, shutdown on signal")) 38 | .about("run in init mode, start and maintain configured services"), 39 | ) 40 | .subcommand( 41 | SubCommand::with_name("list") 42 | .about("quick view of current known services and their status"), 43 | ) 44 | .subcommand( 45 | SubCommand::with_name("shutdown") 46 | .about("stop all services and power off"), 47 | ) 48 | .subcommand( 49 | SubCommand::with_name("reboot") 50 | .about("stop all services and reboot"), 51 | ) 52 | .subcommand( 53 | SubCommand::with_name("status") 54 | .arg( 55 | Arg::with_name("service") 56 | .value_name("SERVICE") 57 | .required(true) 58 | .help("service name"), 59 | ) 60 | .about("show detailed service status"), 61 | ) 62 | .subcommand( 63 | SubCommand::with_name("stop") 64 | .arg( 65 | Arg::with_name("service") 66 | .value_name("SERVICE") 67 | .required(true) 68 | .help("service name"), 69 | ) 70 | .about("stop service"), 71 | ) 72 | .subcommand( 73 | SubCommand::with_name("start") 74 | .arg( 75 | Arg::with_name("service") 76 | .value_name("SERVICE") 77 | .required(true) 78 | .help("service name"), 79 | ) 80 | .about("start service. has no effect if the service is already running"), 81 | ) 82 | .subcommand( 83 | SubCommand::with_name("forget") 84 | .arg( 85 | Arg::with_name("service") 86 | .value_name("SERVICE") 87 | .required(true) 88 | .help("service name"), 89 | ) 90 | .about("forget a service. you can only forget a stopped service"), 91 | ) 92 | .subcommand( 93 | SubCommand::with_name("monitor") 94 | .arg( 95 | Arg::with_name("service") 96 | .value_name("SERVICE") 97 | .required(true) 98 | .help("service name"), 99 | ) 100 | .about("start monitoring a service. configuration is loaded from server config directory"), 101 | ) 102 | .subcommand( 103 | SubCommand::with_name("log") 104 | .arg( 105 | Arg::with_name("snapshot") 106 | .short("s") 107 | .long("snapshot") 108 | .required(false) 109 | .help("if set log prints current buffer without following") 110 | ) 111 | .arg( 112 | Arg::with_name("filter") 113 | .value_name("FILTER") 114 | .required(false) 115 | .help("an optional 'exact' service name") 116 | ) 117 | .about("view services logs from zinit ring buffer"), 118 | ) 119 | .subcommand( 120 | SubCommand::with_name("kill") 121 | .arg( 122 | Arg::with_name("service") 123 | .value_name("SERVICE") 124 | .required(true) 125 | .help("service name"), 126 | ) 127 | .arg( 128 | Arg::with_name("signal") 129 | .value_name("SIGNAL") 130 | .required(true) 131 | .default_value("SIGTERM") 132 | .help("signal name (example: SIGTERM)"), 133 | ) 134 | .about("send a signal to a running service."), 135 | ) 136 | .subcommand( 137 | SubCommand::with_name("restart") 138 | .arg( 139 | Arg::with_name("service") 140 | .value_name("SERVICE") 141 | .required(true) 142 | .help("service name"), 143 | ) 144 | .about("restart a service."), 145 | ) 146 | .subcommand( 147 | SubCommand::with_name("stats") 148 | .arg( 149 | Arg::with_name("service") 150 | .value_name("SERVICE") 151 | .required(true) 152 | .help("service name"), 153 | ) 154 | .about("show memory and CPU usage statistics for a service"), 155 | ) 156 | .subcommand( 157 | SubCommand::with_name("proxy") 158 | .arg( 159 | Arg::with_name("address") 160 | .value_name("ADDRESS") 161 | .short("a") 162 | .long("address") 163 | .default_value("127.0.0.1:8080") 164 | .help("address to bind the HTTP/RPC server to"), 165 | ) 166 | .about("start an HTTP/RPC proxy for Zinit API"), 167 | ) 168 | .get_matches(); 169 | 170 | use dirs; // Add this import 171 | 172 | let socket = matches.value_of("socket").unwrap(); 173 | let debug = matches.is_present("debug"); 174 | 175 | let config_path = if let Some(config_arg) = matches.value_of("config") { 176 | config_arg.to_string() 177 | } else { 178 | #[cfg(target_os = "macos")] 179 | { 180 | let home_dir = dirs::home_dir() 181 | .ok_or_else(|| anyhow::anyhow!("Could not determine home directory"))?; 182 | home_dir 183 | .join("hero") 184 | .join("cfg") 185 | .join("zinit") 186 | .to_str() 187 | .ok_or_else(|| anyhow::anyhow!("Invalid path for config directory"))? 188 | .to_string() 189 | } 190 | #[cfg(not(target_os = "macos"))] 191 | { 192 | "/etc/zinit/".to_string() 193 | } 194 | }; 195 | 196 | let result = match matches.subcommand() { 197 | ("init", Some(matches)) => { 198 | let _server = app::init( 199 | matches.value_of("buffer").unwrap().parse().unwrap(), 200 | &config_path, // Use the determined config_path 201 | socket, 202 | matches.is_present("container"), 203 | debug, 204 | ) 205 | .await?; 206 | tokio::signal::ctrl_c().await?; 207 | Ok(()) 208 | } 209 | ("list", _) => app::list(socket).await, 210 | ("shutdown", _) => app::shutdown(socket).await, 211 | ("reboot", _) => app::reboot(socket).await, 212 | // ("log", Some(matches)) => app::log(matches.value_of("filter")), 213 | ("status", Some(matches)) => { 214 | app::status(socket, matches.value_of("service").unwrap().to_string()).await 215 | } 216 | ("stop", Some(matches)) => { 217 | app::stop(socket, matches.value_of("service").unwrap().to_string()).await 218 | } 219 | ("start", Some(matches)) => { 220 | app::start(socket, matches.value_of("service").unwrap().to_string()).await 221 | } 222 | ("forget", Some(matches)) => { 223 | app::forget(socket, matches.value_of("service").unwrap().to_string()).await 224 | } 225 | ("monitor", Some(matches)) => { 226 | app::monitor(socket, matches.value_of("service").unwrap().to_string()).await 227 | } 228 | ("kill", Some(matches)) => { 229 | app::kill( 230 | socket, 231 | matches.value_of("service").unwrap().to_string(), 232 | matches.value_of("signal").unwrap().to_string(), 233 | ) 234 | .await 235 | } 236 | ("log", Some(matches)) => { 237 | let mut stream = app::logs( 238 | socket, 239 | matches.value_of("filter").map(|s| s.to_string()), 240 | !matches.is_present("snapshot"), 241 | ) 242 | .await?; 243 | 244 | loop { 245 | tokio::select! { 246 | item = stream.next() => { 247 | match item { 248 | Some(log_entry) => { 249 | println!("{}", log_entry); 250 | }, 251 | None => break 252 | } 253 | } 254 | _ = tokio::signal::ctrl_c() => { 255 | break 256 | } 257 | } 258 | } 259 | 260 | Ok(()) 261 | } 262 | ("restart", Some(matches)) => { 263 | app::restart(socket, matches.value_of("service").unwrap().to_string()).await 264 | } 265 | ("stats", Some(matches)) => { 266 | app::stats(socket, matches.value_of("service").unwrap().to_string()).await 267 | } 268 | ("proxy", Some(matches)) => { 269 | app::proxy(socket, matches.value_of("address").unwrap().to_string()).await 270 | } 271 | _ => app::list(socket).await, // default command 272 | }; 273 | 274 | match result { 275 | Ok(_) => Ok(()), 276 | Err(e) => { 277 | eprintln!("{:#}", e); 278 | std::process::exit(1); 279 | } 280 | } 281 | } 282 | -------------------------------------------------------------------------------- /src/manager/buffer.rs: -------------------------------------------------------------------------------- 1 | use anyhow::Result; 2 | use std::sync::Arc; 3 | use tokio::sync::broadcast; 4 | use tokio::sync::broadcast::error::RecvError; 5 | use tokio::sync::{mpsc, Mutex}; 6 | 7 | struct Buffer { 8 | inner: Vec, 9 | at: usize, 10 | } 11 | 12 | impl Buffer { 13 | pub fn new(cap: usize) -> Buffer { 14 | Buffer { 15 | inner: Vec::with_capacity(cap), 16 | at: 0, 17 | } 18 | } 19 | 20 | fn len(&self) -> usize { 21 | self.inner.len() 22 | } 23 | 24 | pub fn cap(&self) -> usize { 25 | self.inner.capacity() 26 | } 27 | 28 | pub fn push(&mut self, o: T) { 29 | if self.len() < self.cap() { 30 | self.inner.push(o); 31 | } else { 32 | self.inner[self.at] = o; 33 | } 34 | 35 | self.at = (self.at + 1) % self.cap(); 36 | } 37 | } 38 | 39 | impl<'a, T: 'a> IntoIterator for &'a Buffer { 40 | type IntoIter = BufferIter<'a, T>; 41 | type Item = &'a T; 42 | 43 | fn into_iter(self) -> Self::IntoIter { 44 | let (second, first) = self.inner.split_at(self.at); 45 | 46 | BufferIter { 47 | first, 48 | second, 49 | index: 0, 50 | } 51 | } 52 | } 53 | 54 | pub struct BufferIter<'a, T> { 55 | first: &'a [T], 56 | second: &'a [T], 57 | index: usize, 58 | } 59 | 60 | impl<'a, T> Iterator for BufferIter<'a, T> { 61 | type Item = &'a T; 62 | 63 | fn next(&mut self) -> Option { 64 | let index = self.index; 65 | self.index += 1; 66 | if index < self.first.len() { 67 | Some(&self.first[index]) 68 | } else if index - self.first.len() < self.second.len() { 69 | Some(&self.second[index - self.first.len()]) 70 | } else { 71 | None 72 | } 73 | } 74 | } 75 | 76 | pub type Logs = mpsc::Receiver>; 77 | 78 | #[derive(Clone)] 79 | pub struct Ring { 80 | buffer: Arc>>>, 81 | sender: broadcast::Sender>, 82 | } 83 | 84 | impl Ring { 85 | pub fn new(cap: usize) -> Ring { 86 | let (tx, _) = broadcast::channel(100); 87 | Ring { 88 | buffer: Arc::new(Mutex::new(Buffer::new(cap))), 89 | sender: tx, 90 | } 91 | } 92 | 93 | pub async fn push(&self, line: String) -> Result<()> { 94 | let line = Arc::new(line.clone()); 95 | self.buffer.lock().await.push(Arc::clone(&line)); 96 | self.sender.send(line)?; 97 | Ok(()) 98 | } 99 | 100 | /// stream returns a continues stream that first receive 101 | /// a snapshot of the current buffer. 102 | /// then if follow is true the logs stream will remain 103 | /// open and fed each received line forever until the 104 | /// received closed the channel from its end. 105 | pub async fn stream(&self, existing_logs: bool, follow: bool) -> Logs { 106 | let (tx, stream) = mpsc::channel::>(100); 107 | let mut rx = self.sender.subscribe(); 108 | 109 | let buffer = if existing_logs { 110 | // Get current exisiting logs 111 | self.buffer 112 | .lock() 113 | .await 114 | .into_iter() 115 | .cloned() 116 | .collect::>() 117 | } else { 118 | // Don't care about existing logs 119 | vec![] 120 | }; 121 | 122 | tokio::spawn(async move { 123 | for item in buffer { 124 | let _ = tx.send(Arc::clone(&item)).await; 125 | } 126 | 127 | if !follow { 128 | return; 129 | } 130 | 131 | loop { 132 | let line = match rx.recv().await { 133 | Ok(line) => line, 134 | Err(RecvError::Closed) => break, 135 | Err(RecvError::Lagged(n)) => { 136 | Arc::new(format!("[-] zinit: {} lines dropped", n)) 137 | } 138 | }; 139 | 140 | if tx.send(line).await.is_err() { 141 | // client disconnected. 142 | break; 143 | } 144 | } 145 | }); 146 | 147 | stream 148 | } 149 | } 150 | -------------------------------------------------------------------------------- /src/manager/mod.rs: -------------------------------------------------------------------------------- 1 | use std::collections::HashMap; 2 | 3 | use anyhow::{Context, Result}; 4 | use command_group::CommandGroup; 5 | use nix::sys::signal; 6 | use nix::sys::wait::{self, WaitStatus}; 7 | use nix::unistd::Pid; 8 | use std::fs::File as StdFile; 9 | use std::os::unix::io::FromRawFd; 10 | use std::os::unix::io::IntoRawFd; 11 | use std::process::Command; 12 | use std::process::Stdio; 13 | use std::sync::Arc; 14 | use tokio::fs::File; 15 | use tokio::io::AsyncBufReadExt; 16 | use tokio::io::BufReader; 17 | use tokio::signal::unix; 18 | use tokio::sync::oneshot; 19 | use tokio::sync::Mutex; 20 | 21 | mod buffer; 22 | pub use buffer::Logs; 23 | 24 | pub struct Process { 25 | cmd: String, 26 | env: HashMap, 27 | cwd: String, 28 | } 29 | type WaitChannel = oneshot::Receiver; 30 | 31 | pub struct Child { 32 | pub pid: Pid, 33 | ch: WaitChannel, 34 | } 35 | 36 | impl Child { 37 | pub fn new(pid: Pid, ch: WaitChannel) -> Child { 38 | Child { pid, ch } 39 | } 40 | 41 | pub async fn wait(self) -> Result { 42 | Ok(self.ch.await?) 43 | } 44 | } 45 | 46 | type Handler = oneshot::Sender; 47 | 48 | impl Process { 49 | pub fn new>(cmd: S, cwd: S, env: Option>) -> Process { 50 | let env = env.unwrap_or_default(); 51 | 52 | Process { 53 | env, 54 | cmd: cmd.into(), 55 | cwd: cwd.into(), 56 | } 57 | } 58 | } 59 | 60 | #[derive(Clone)] 61 | pub enum Log { 62 | None, 63 | Stdout, 64 | Ring(String), 65 | } 66 | 67 | #[derive(Clone)] 68 | pub struct ProcessManager { 69 | table: Arc>>, 70 | ring: buffer::Ring, 71 | env: Environ, 72 | } 73 | 74 | impl ProcessManager { 75 | pub fn new(cap: usize) -> ProcessManager { 76 | ProcessManager { 77 | table: Arc::new(Mutex::new(HashMap::new())), 78 | ring: buffer::Ring::new(cap), 79 | env: Environ::new(), 80 | } 81 | } 82 | 83 | fn wait_process() -> Vec { 84 | let mut statuses: Vec = Vec::new(); 85 | loop { 86 | let status = match wait::waitpid(Option::None, Some(wait::WaitPidFlag::WNOHANG)) { 87 | Ok(status) => status, 88 | Err(_) => { 89 | return statuses; 90 | } 91 | }; 92 | match status { 93 | WaitStatus::StillAlive => break, 94 | _ => statuses.push(status), 95 | } 96 | } 97 | statuses 98 | } 99 | 100 | pub fn start(&self) { 101 | let table = Arc::clone(&self.table); 102 | let mut signals = match unix::signal(unix::SignalKind::child()) { 103 | Ok(s) => s, 104 | Err(err) => { 105 | panic!("failed to bind to signals: {}", err); 106 | } 107 | }; 108 | 109 | tokio::spawn(async move { 110 | loop { 111 | signals.recv().await; 112 | let mut table = table.lock().await; 113 | for exited in Self::wait_process() { 114 | if let Some(pid) = exited.pid() { 115 | if let Some(sender) = table.remove(&pid) { 116 | if sender.send(exited).is_err() { 117 | debug!("failed to send exit state to process: {}", pid); 118 | } 119 | } 120 | } 121 | } 122 | } 123 | }); 124 | } 125 | 126 | fn sink(&self, file: File, prefix: String) { 127 | let ring = self.ring.clone(); 128 | let reader = BufReader::new(file); 129 | 130 | tokio::spawn(async move { 131 | let mut lines = reader.lines(); 132 | while let Ok(line) = lines.next_line().await { 133 | let _ = match line { 134 | Some(line) => ring.push(format!("{}: {}", prefix, line)).await, 135 | None => break, 136 | }; 137 | } 138 | }); 139 | } 140 | 141 | pub async fn stream(&self, existing_logs: bool, follow: bool) -> Logs { 142 | self.ring.stream(existing_logs, follow).await 143 | } 144 | 145 | pub fn signal(&self, pid: Pid, sig: signal::Signal) -> Result<()> { 146 | Ok(signal::killpg(pid, sig)?) 147 | } 148 | 149 | pub async fn run(&self, cmd: Process, log: Log) -> Result { 150 | let args = shlex::split(&cmd.cmd).context("failed to parse command")?; 151 | if args.is_empty() { 152 | bail!("invalid command"); 153 | } 154 | 155 | let mut child = Command::new(&args[0]); 156 | 157 | let child = if !cmd.cwd.is_empty() { 158 | child.current_dir(&cmd.cwd) 159 | } else { 160 | child.current_dir("/") 161 | }; 162 | 163 | let child = child.args(&args[1..]).envs(&self.env.0).envs(cmd.env); 164 | 165 | let child = match log { 166 | Log::None => child.stdout(Stdio::null()).stderr(Stdio::null()), 167 | Log::Ring(_) => child.stdout(Stdio::piped()).stderr(Stdio::piped()), 168 | _ => child, // default to inherit 169 | }; 170 | 171 | let mut table = self.table.lock().await; 172 | 173 | let mut child = child 174 | .group_spawn() 175 | .context("failed to spawn command")? 176 | .into_inner(); 177 | 178 | if let Log::Ring(prefix) = log { 179 | let _ = self 180 | .ring 181 | .push(format!("[-] {}: ------------ [start] ------------", prefix)) 182 | .await; 183 | 184 | if let Some(out) = child.stdout.take() { 185 | let out = File::from_std(unsafe { StdFile::from_raw_fd(out.into_raw_fd()) }); 186 | self.sink(out, format!("[+] {}", prefix)) 187 | } 188 | 189 | if let Some(out) = child.stderr.take() { 190 | let out = File::from_std(unsafe { StdFile::from_raw_fd(out.into_raw_fd()) }); 191 | self.sink(out, format!("[-] {}", prefix)) 192 | } 193 | } 194 | 195 | let (tx, rx) = oneshot::channel(); 196 | 197 | let id = child.id(); 198 | 199 | let pid = Pid::from_raw(id as i32); 200 | table.insert(pid, tx); 201 | 202 | Ok(Child::new(pid, rx)) 203 | } 204 | } 205 | 206 | #[derive(Clone)] 207 | struct Environ(HashMap); 208 | 209 | impl Environ { 210 | fn new() -> Environ { 211 | let env = match Environ::parse("/etc/environment") { 212 | Ok(r) => r, 213 | Err(err) => { 214 | error!("failed to load /etc/environment file: {}", err); 215 | HashMap::new() 216 | } 217 | }; 218 | 219 | Environ(env) 220 | } 221 | 222 | fn parse

(p: P) -> Result, std::io::Error> 223 | where 224 | P: AsRef, 225 | { 226 | let mut m = HashMap::new(); 227 | let txt = match std::fs::read_to_string(p) { 228 | Ok(txt) => txt, 229 | Err(err) if err.kind() == std::io::ErrorKind::NotFound => { 230 | info!("skipping /etc/environment file because it does not exist"); 231 | "".into() 232 | } 233 | Err(err) => return Err(err), 234 | }; 235 | 236 | for line in txt.lines() { 237 | let line = line.trim(); 238 | if line.starts_with('#') { 239 | continue; 240 | } 241 | let parts: Vec<&str> = line.splitn(2, '=').collect(); 242 | let key = String::from(parts[0]); 243 | let value = match parts.len() { 244 | 2 => String::from(parts[1]), 245 | _ => String::default(), 246 | }; 247 | //m.into_iter() 248 | m.insert(key, value); 249 | } 250 | 251 | Ok(m) 252 | } 253 | } 254 | -------------------------------------------------------------------------------- /src/testapp/main.rs: -------------------------------------------------------------------------------- 1 | use anyhow::{Context, Result}; 2 | use std::path::Path; 3 | use tokio::time::{sleep, Duration}; 4 | use std::env; 5 | use tokio::process::Command; 6 | use tokio::fs; 7 | use std::process::Stdio; 8 | use serde::{Deserialize, Serialize}; 9 | use serde_json; 10 | use tokio::net::UnixStream; 11 | use tokio::io::{AsyncBufReadExt, AsyncReadExt, AsyncWriteExt, BufStream}; 12 | use std::collections::HashMap; 13 | 14 | #[derive(Clone, Debug, Deserialize, Serialize)] 15 | #[serde(rename_all = "lowercase")] 16 | struct Response { 17 | pub state: State, 18 | pub body: serde_json::Value, 19 | } 20 | 21 | #[derive(Clone, Debug, Deserialize, Serialize)] 22 | #[serde(rename_all = "lowercase")] 23 | enum State { 24 | Ok, 25 | Error, 26 | } 27 | 28 | #[derive(Clone, Debug, Deserialize, Serialize)] 29 | #[serde(rename_all = "lowercase")] 30 | struct Status { 31 | pub name: String, 32 | pub pid: u32, 33 | pub state: String, 34 | pub target: String, 35 | pub after: HashMap, 36 | } 37 | 38 | struct Client { 39 | socket: String, 40 | } 41 | 42 | impl Client { 43 | pub fn new(socket: &str) -> Client { 44 | Client { 45 | socket: socket.to_string(), 46 | } 47 | } 48 | 49 | async fn connect(&self) -> Result { 50 | UnixStream::connect(&self.socket).await.with_context(|| { 51 | format!( 52 | "failed to connect to '{}'. is zinit listening on that socket?", 53 | self.socket 54 | ) 55 | }) 56 | } 57 | 58 | async fn command(&self, c: &str) -> Result { 59 | let mut con = BufStream::new(self.connect().await?); 60 | 61 | let _ = con.write(c.as_bytes()).await?; 62 | let _ = con.write(b"\n").await?; 63 | con.flush().await?; 64 | 65 | let mut data = String::new(); 66 | con.read_to_string(&mut data).await?; 67 | 68 | let response: Response = serde_json::from_str(&data)?; 69 | 70 | match response.state { 71 | State::Ok => Ok(response.body), 72 | State::Error => { 73 | let err: String = serde_json::from_value(response.body)?; 74 | anyhow::bail!(err) 75 | } 76 | } 77 | } 78 | 79 | pub async fn list(&self) -> Result> { 80 | let response = self.command("list").await?; 81 | Ok(serde_json::from_value(response)?) 82 | } 83 | 84 | pub async fn status>(&self, name: S) -> Result { 85 | let response = self.command(&format!("status {}", name.as_ref())).await?; 86 | Ok(serde_json::from_value(response)?) 87 | } 88 | 89 | pub async fn start>(&self, name: S) -> Result<()> { 90 | self.command(&format!("start {}", name.as_ref())).await?; 91 | Ok(()) 92 | } 93 | 94 | pub async fn stop>(&self, name: S) -> Result<()> { 95 | self.command(&format!("stop {}", name.as_ref())).await?; 96 | Ok(()) 97 | } 98 | 99 | pub async fn forget>(&self, name: S) -> Result<()> { 100 | self.command(&format!("forget {}", name.as_ref())).await?; 101 | Ok(()) 102 | } 103 | 104 | pub async fn monitor>(&self, name: S) -> Result<()> { 105 | self.command(&format!("monitor {}", name.as_ref())).await?; 106 | Ok(()) 107 | } 108 | 109 | pub async fn kill>(&self, name: S, sig: S) -> Result<()> { 110 | self.command(&format!("kill {} {}", name.as_ref(), sig.as_ref())) 111 | .await?; 112 | Ok(()) 113 | } 114 | 115 | pub async fn shutdown(&self) -> Result<()> { 116 | self.command("shutdown").await?; 117 | Ok(()) 118 | } 119 | } 120 | 121 | async fn start_zinit(socket_path: &str, config_dir: &str) -> Result<()> { 122 | // Create a temporary config directory if it doesn't exist 123 | let config_path = Path::new(config_dir); 124 | if !config_path.exists() { 125 | fs::create_dir_all(config_path).await?; 126 | } 127 | 128 | // Start zinit in the background 129 | let mut cmd = Command::new("zinit"); 130 | cmd.arg("--socket") 131 | .arg(socket_path) 132 | .arg("init") 133 | .arg("--config") 134 | .arg(config_dir) 135 | .stdout(Stdio::piped()) 136 | .stderr(Stdio::piped()); 137 | 138 | let child = cmd.spawn()?; 139 | 140 | // Give zinit some time to start up 141 | sleep(Duration::from_secs(1)).await; 142 | 143 | println!("Zinit started with PID: {:?}", child.id()); 144 | 145 | Ok(()) 146 | } 147 | 148 | async fn create_service_config(config_dir: &str, name: &str, command: &str) -> Result<()> { 149 | let config_path = format!("{}/{}.yaml", config_dir, name); 150 | let config_content = format!( 151 | r#"exec: {} 152 | oneshot: false 153 | shutdown_timeout: 10 154 | after: [] 155 | signal: 156 | stop: sigterm 157 | log: ring 158 | env: {{}} 159 | dir: / 160 | "#, 161 | command 162 | ); 163 | 164 | fs::write(config_path, config_content).await?; 165 | Ok(()) 166 | } 167 | 168 | #[tokio::main] 169 | async fn main() -> Result<()> { 170 | // Define paths for socket and config 171 | let temp_dir = env::temp_dir(); 172 | let socket_path = temp_dir.join("zinit-test.sock").to_str().unwrap().to_string(); 173 | let config_dir = temp_dir.join("zinit-test-config").to_str().unwrap().to_string(); 174 | 175 | println!("Starting zinit with socket at: {}", socket_path); 176 | println!("Using config directory: {}", config_dir); 177 | 178 | // Start zinit in the background 179 | start_zinit(&socket_path, &config_dir).await?; 180 | 181 | // Wait for zinit to initialize 182 | sleep(Duration::from_secs(2)).await; 183 | 184 | // Create a client to communicate with zinit 185 | let client = Client::new(&socket_path); 186 | 187 | // Create service configurations 188 | println!("Creating service configurations..."); 189 | 190 | // Create a find service 191 | create_service_config(&config_dir, "find-service", "find / -name \"*.txt\" -type f").await?; 192 | 193 | // Create a sleep service with echo 194 | create_service_config( 195 | &config_dir, 196 | "sleep-service", 197 | "sh -c 'echo Starting sleep; sleep 30; echo Finished sleep'" 198 | ).await?; 199 | 200 | // Wait for zinit to load the configurations 201 | sleep(Duration::from_secs(1)).await; 202 | 203 | // Tell zinit to monitor our services 204 | println!("Monitoring services..."); 205 | client.monitor("find-service").await?; 206 | client.monitor("sleep-service").await?; 207 | 208 | // List all services 209 | println!("\nListing all services:"); 210 | let services = client.list().await?; 211 | for (name, status) in services { 212 | println!("Service: {} - Status: {}", name, status); 213 | } 214 | 215 | // Start the find service 216 | println!("\nStarting find-service..."); 217 | client.start("find-service").await?; 218 | 219 | // Wait a bit and check status 220 | sleep(Duration::from_secs(2)).await; 221 | let status = client.status("find-service").await?; 222 | println!("find-service status: {:?}", status); 223 | 224 | // Start the sleep service 225 | println!("\nStarting sleep-service..."); 226 | client.start("sleep-service").await?; 227 | 228 | // Wait a bit and check status 229 | sleep(Duration::from_secs(2)).await; 230 | let status = client.status("sleep-service").await?; 231 | println!("sleep-service status: {:?}", status); 232 | 233 | // Stop the find service 234 | println!("\nStopping find-service..."); 235 | client.stop("find-service").await?; 236 | 237 | // Wait a bit and check status 238 | sleep(Duration::from_secs(2)).await; 239 | let status = client.status("find-service").await?; 240 | println!("find-service status after stopping: {:?}", status); 241 | 242 | // Kill the sleep service with SIGTERM 243 | println!("\nKilling sleep-service with SIGTERM..."); 244 | client.kill("sleep-service", "SIGTERM").await?; 245 | 246 | // Wait a bit and check status 247 | sleep(Duration::from_secs(2)).await; 248 | let status = client.status("sleep-service").await?; 249 | println!("sleep-service status after killing: {:?}", status); 250 | 251 | // Cleanup - forget services 252 | println!("\nForgetting services..."); 253 | if status.pid == 0 { // Only forget if it's not running 254 | client.forget("sleep-service").await?; 255 | } 256 | client.forget("find-service").await?; 257 | 258 | // Shutdown zinit 259 | println!("\nShutting down zinit..."); 260 | client.shutdown().await?; 261 | 262 | println!("\nTest completed successfully!"); 263 | Ok(()) 264 | } -------------------------------------------------------------------------------- /src/testapp/mod.rs: -------------------------------------------------------------------------------- 1 | use anyhow::Result; 2 | use std::env; 3 | use std::path::Path; 4 | use std::process::Stdio; 5 | use tokio::process::Command; 6 | use tokio::time::{sleep, Duration}; 7 | 8 | pub async fn start_zinit(socket_path: &str, config_dir: &str) -> Result<()> { 9 | // Create a temporary config directory if it doesn't exist 10 | let config_path = Path::new(config_dir); 11 | if !config_path.exists() { 12 | tokio::fs::create_dir_all(config_path).await?; 13 | } 14 | 15 | // Get the path to the zinit binary (use the one we just built) 16 | let zinit_path = env::current_dir()?.join("target/debug/zinit"); 17 | println!("Using zinit binary at: {}", zinit_path.display()); 18 | 19 | // Start zinit in the background 20 | let mut cmd = Command::new(zinit_path); 21 | cmd.arg("--socket") 22 | .arg(socket_path) 23 | .arg("init") 24 | .arg("--config") 25 | .arg(config_dir) 26 | .stdout(Stdio::piped()) 27 | .stderr(Stdio::piped()); 28 | 29 | let child = cmd.spawn()?; 30 | 31 | // Give zinit some time to start up 32 | sleep(Duration::from_secs(1)).await; 33 | 34 | println!("Zinit started with PID: {:?}", child.id()); 35 | 36 | Ok(()) 37 | } 38 | 39 | pub async fn create_service_config(config_dir: &str, name: &str, command: &str) -> Result<()> { 40 | let config_path = format!("{}/{}.yaml", config_dir, name); 41 | let config_content = format!( 42 | r#"exec: {} 43 | oneshot: false 44 | shutdown_timeout: 10 45 | after: [] 46 | signal: 47 | stop: sigterm 48 | log: ring 49 | env: {{}} 50 | dir: / 51 | "#, 52 | command 53 | ); 54 | 55 | tokio::fs::write(config_path, config_content).await?; 56 | Ok(()) 57 | } 58 | -------------------------------------------------------------------------------- /src/zinit/config.rs: -------------------------------------------------------------------------------- 1 | use anyhow::Result; 2 | use serde::{Deserialize, Serialize}; 3 | use serde_yaml as yaml; 4 | use std::collections::HashMap; 5 | use std::ffi::OsStr; 6 | use std::fs::{self, File}; 7 | use std::path::Path; 8 | pub type Services = HashMap; 9 | 10 | pub const DEFAULT_SHUTDOWN_TIMEOUT: u64 = 10; // in seconds 11 | 12 | #[derive(Clone, Debug, Serialize, Deserialize)] 13 | #[serde(default)] 14 | pub struct Signal { 15 | pub stop: String, 16 | } 17 | 18 | impl Default for Signal { 19 | fn default() -> Self { 20 | Signal { 21 | stop: String::from("sigterm"), 22 | } 23 | } 24 | } 25 | 26 | #[derive(Default, Clone, Debug, Deserialize)] 27 | #[serde(rename_all = "lowercase")] 28 | pub enum Log { 29 | None, 30 | #[default] 31 | Ring, 32 | Stdout, 33 | } 34 | 35 | fn default_shutdown_timeout_fn() -> u64 { 36 | DEFAULT_SHUTDOWN_TIMEOUT 37 | } 38 | #[derive(Clone, Debug, Default, Deserialize)] 39 | #[serde(default)] 40 | pub struct Service { 41 | /// command to run 42 | pub exec: String, 43 | /// test command (optional) 44 | #[serde(default)] 45 | pub test: String, 46 | #[serde(rename = "oneshot")] 47 | pub one_shot: bool, 48 | #[serde(default = "default_shutdown_timeout_fn")] 49 | pub shutdown_timeout: u64, 50 | pub after: Vec, 51 | pub signal: Signal, 52 | pub log: Log, 53 | pub env: HashMap, 54 | pub dir: String, 55 | } 56 | 57 | impl Service { 58 | pub fn validate(&self) -> Result<()> { 59 | use nix::sys::signal::Signal; 60 | use std::str::FromStr; 61 | if self.exec.is_empty() { 62 | bail!("missing exec directive"); 63 | } 64 | 65 | Signal::from_str(&self.signal.stop.to_uppercase())?; 66 | 67 | Ok(()) 68 | } 69 | } 70 | /// load loads a single file 71 | pub fn load>(t: T) -> Result<(String, Service)> { 72 | let p = t.as_ref(); 73 | //todo: can't find a way to shorten this down. 74 | let name = match p.file_stem() { 75 | Some(name) => match name.to_str() { 76 | Some(name) => name, 77 | None => bail!("invalid file name: {}", p.to_str().unwrap()), 78 | }, 79 | None => bail!("invalid file name: {}", p.to_str().unwrap()), 80 | }; 81 | 82 | let file = File::open(p)?; 83 | let service: Service = yaml::from_reader(&file)?; 84 | service.validate()?; 85 | Ok((String::from(name), service)) 86 | } 87 | 88 | /// walks over a directory and load all configuration files. 89 | /// the callback is called with any error that is encountered on loading 90 | /// a file, the callback can decide to either ignore the file, or stop 91 | /// the directory walking 92 | pub fn load_dir>(p: T) -> Result { 93 | let mut services: Services = HashMap::new(); 94 | 95 | for entry in fs::read_dir(p)? { 96 | let entry = entry?; 97 | if !entry.file_type()?.is_file() { 98 | continue; 99 | } 100 | 101 | let fp = entry.path(); 102 | 103 | if !matches!(fp.extension(), Some(ext) if ext == OsStr::new("yaml")) { 104 | continue; 105 | } 106 | 107 | let (name, service) = match load(&fp) { 108 | Ok(content) => content, 109 | Err(err) => { 110 | error!("failed to load config file {:?}: {}", fp, err); 111 | continue; 112 | } 113 | }; 114 | 115 | services.insert(name, service); 116 | } 117 | 118 | Ok(services) 119 | } 120 | -------------------------------------------------------------------------------- /src/zinit/errors.rs: -------------------------------------------------------------------------------- 1 | use thiserror::Error; 2 | 3 | /// Errors that can occur in the zinit module 4 | #[derive(Error, Debug)] 5 | pub enum ZInitError { 6 | /// Service name is unknown 7 | #[error("service name {name:?} unknown")] 8 | UnknownService { name: String }, 9 | 10 | /// Service is already being monitored 11 | #[error("service {name:?} already monitored")] 12 | ServiceAlreadyMonitored { name: String }, 13 | 14 | /// Service is up and running 15 | #[error("service {name:?} is up")] 16 | ServiceIsUp { name: String }, 17 | 18 | /// Service is down and not running 19 | #[error("service {name:?} is down")] 20 | ServiceIsDown { name: String }, 21 | 22 | /// Zinit is shutting down 23 | #[error("zinit is shutting down")] 24 | ShuttingDown, 25 | 26 | /// Invalid state transition 27 | #[error("service state transition error: {message}")] 28 | InvalidStateTransition { message: String }, 29 | 30 | /// Dependency error 31 | #[error("dependency error: {message}")] 32 | DependencyError { message: String }, 33 | 34 | /// Process error 35 | #[error("process error: {message}")] 36 | ProcessError { message: String }, 37 | } 38 | 39 | impl ZInitError { 40 | /// Create a new UnknownService error 41 | pub fn unknown_service>(name: S) -> Self { 42 | ZInitError::UnknownService { name: name.into() } 43 | } 44 | 45 | /// Create a new ServiceAlreadyMonitored error 46 | pub fn service_already_monitored>(name: S) -> Self { 47 | ZInitError::ServiceAlreadyMonitored { name: name.into() } 48 | } 49 | 50 | /// Create a new ServiceIsUp error 51 | pub fn service_is_up>(name: S) -> Self { 52 | ZInitError::ServiceIsUp { name: name.into() } 53 | } 54 | 55 | /// Create a new ServiceIsDown error 56 | pub fn service_is_down>(name: S) -> Self { 57 | ZInitError::ServiceIsDown { name: name.into() } 58 | } 59 | 60 | /// Create a new InvalidStateTransition error 61 | pub fn invalid_state_transition>(message: S) -> Self { 62 | ZInitError::InvalidStateTransition { 63 | message: message.into(), 64 | } 65 | } 66 | 67 | /// Create a new DependencyError error 68 | pub fn dependency_error>(message: S) -> Self { 69 | ZInitError::DependencyError { 70 | message: message.into(), 71 | } 72 | } 73 | 74 | /// Create a new ProcessError error 75 | pub fn process_error>(message: S) -> Self { 76 | ZInitError::ProcessError { 77 | message: message.into(), 78 | } 79 | } 80 | } 81 | -------------------------------------------------------------------------------- /src/zinit/mod.rs: -------------------------------------------------------------------------------- 1 | pub mod config; 2 | pub mod errors; 3 | pub mod lifecycle; 4 | pub mod ord; 5 | pub mod service; 6 | pub mod state; 7 | pub mod types; 8 | 9 | // Re-export commonly used items 10 | pub use service::ZInitStatus; 11 | pub use state::State; 12 | pub use types::{ProcessStats, ServiceStats}; 13 | 14 | use crate::manager::{Logs, ProcessManager}; 15 | use anyhow::Result; 16 | use nix::sys::signal; 17 | use std::sync::Arc; 18 | use tokio::sync::{Notify, RwLock}; 19 | 20 | /// Main ZInit service manager 21 | #[derive(Clone)] 22 | pub struct ZInit { 23 | /// Lifecycle manager for service management 24 | lifecycle: lifecycle::LifecycleManager, 25 | } 26 | 27 | impl ZInit { 28 | /// Create a new ZInit instance 29 | pub fn new(cap: usize, container: bool) -> ZInit { 30 | let pm = ProcessManager::new(cap); 31 | let services = Arc::new(RwLock::new(types::ServiceTable::new())); 32 | let notify = Arc::new(Notify::new()); 33 | let shutdown = Arc::new(RwLock::new(false)); 34 | 35 | let lifecycle = lifecycle::LifecycleManager::new(pm, services, notify, shutdown, container); 36 | 37 | ZInit { lifecycle } 38 | } 39 | 40 | /// Start the service manager 41 | pub fn serve(&self) { 42 | self.lifecycle.process_manager().start(); 43 | if self.lifecycle.is_container_mode() { 44 | let lifecycle = self.lifecycle.clone_lifecycle(); 45 | tokio::spawn(async move { 46 | use tokio::signal::unix; 47 | 48 | let mut term = unix::signal(unix::SignalKind::terminate()).unwrap(); 49 | let mut int = unix::signal(unix::SignalKind::interrupt()).unwrap(); 50 | let mut hup = unix::signal(unix::SignalKind::hangup()).unwrap(); 51 | 52 | tokio::select! { 53 | _ = term.recv() => {}, 54 | _ = int.recv() => {}, 55 | _ = hup.recv() => {}, 56 | }; 57 | 58 | debug!("shutdown signal received"); 59 | let _ = lifecycle.shutdown().await; 60 | }); 61 | } 62 | } 63 | 64 | /// Get logs from the process manager 65 | /// `existing_logs` TODO: 66 | pub async fn logs(&self, existing_logs: bool, follow: bool) -> Logs { 67 | self.lifecycle.logs(existing_logs, follow).await 68 | } 69 | 70 | /// Monitor a service 71 | pub async fn monitor>(&self, name: S, service: config::Service) -> Result<()> { 72 | self.lifecycle.monitor(name, service).await 73 | } 74 | 75 | /// Get the status of a service 76 | pub async fn status>(&self, name: S) -> Result { 77 | self.lifecycle.status(name).await 78 | } 79 | 80 | /// Start a service 81 | pub async fn start>(&self, name: S) -> Result<()> { 82 | self.lifecycle.start(name).await 83 | } 84 | 85 | /// Stop a service 86 | pub async fn stop>(&self, name: S) -> Result<()> { 87 | self.lifecycle.stop(name).await 88 | } 89 | 90 | /// Forget a service 91 | pub async fn forget>(&self, name: S) -> Result<()> { 92 | self.lifecycle.forget(name).await 93 | } 94 | 95 | /// Send a signal to a service 96 | pub async fn kill>(&self, name: S, signal: signal::Signal) -> Result<()> { 97 | self.lifecycle.kill(name, signal).await 98 | } 99 | 100 | /// List all services 101 | pub async fn list(&self) -> Result> { 102 | self.lifecycle.list().await 103 | } 104 | 105 | /// Shutdown the system 106 | pub async fn shutdown(&self) -> Result<()> { 107 | self.lifecycle.shutdown().await 108 | } 109 | 110 | /// Reboot the system 111 | pub async fn reboot(&self) -> Result<()> { 112 | self.lifecycle.reboot().await 113 | } 114 | 115 | /// Get stats for a service (memory and CPU usage) 116 | pub async fn stats>(&self, name: S) -> Result { 117 | self.lifecycle.stats(name).await 118 | } 119 | } 120 | -------------------------------------------------------------------------------- /src/zinit/ord.rs: -------------------------------------------------------------------------------- 1 | use crate::zinit::types::ServiceTable; 2 | use std::collections::HashMap; 3 | use std::sync::Arc; 4 | use tokio::sync::RwLock; 5 | pub const DUMMY_ROOT: &str = ""; 6 | pub struct ProcessDAG { 7 | pub adj: HashMap>, 8 | pub indegree: HashMap, 9 | /// number of services including the dummy root 10 | pub count: u32, 11 | } 12 | pub async fn service_dependency_order(services: Arc>) -> ProcessDAG { 13 | let mut children: HashMap> = HashMap::new(); 14 | let mut indegree: HashMap = HashMap::new(); 15 | let table = services.read().await; 16 | for (name, service) in table.iter() { 17 | let service = service.read().await; 18 | for child in service.service.after.iter() { 19 | children.entry(name.into()).or_default().push(child.into()); 20 | *indegree.entry(child.into()).or_insert(0) += 1; 21 | } 22 | } 23 | let mut heads: Vec = Vec::new(); 24 | for (name, _) in table.iter() { 25 | if *indegree.get::(name).unwrap_or(&0) == 0 { 26 | heads.push(name.into()); 27 | // add edges from the dummy root to the heads 28 | *indegree.entry(name.into()).or_insert(0) += 1; 29 | } 30 | } 31 | children.insert(DUMMY_ROOT.to_string(), heads); 32 | ProcessDAG { 33 | adj: children, 34 | indegree, 35 | count: table.len() as u32 + 1, 36 | } 37 | } 38 | -------------------------------------------------------------------------------- /src/zinit/service.rs: -------------------------------------------------------------------------------- 1 | use crate::zinit::config; 2 | use crate::zinit::state::{State, Target}; 3 | use crate::zinit::types::Watched; 4 | use anyhow::{Context, Result}; 5 | use nix::unistd::Pid; 6 | 7 | /// Represents a service managed by ZInit 8 | pub struct ZInitService { 9 | /// Process ID of the running service 10 | pub pid: Pid, 11 | 12 | /// Service configuration 13 | pub service: config::Service, 14 | 15 | /// Target state of the service (up, down) 16 | pub target: Target, 17 | 18 | /// Whether the service is scheduled for execution 19 | pub scheduled: bool, 20 | 21 | /// Current state of the service 22 | state: Watched, 23 | } 24 | 25 | /// Status information for a service 26 | pub struct ZInitStatus { 27 | /// Process ID of the running service 28 | pub pid: Pid, 29 | 30 | /// Service configuration 31 | pub service: config::Service, 32 | 33 | /// Target state of the service (up, down) 34 | pub target: Target, 35 | 36 | /// Whether the service is scheduled for execution 37 | pub scheduled: bool, 38 | 39 | /// Current state of the service 40 | pub state: State, 41 | } 42 | 43 | impl ZInitService { 44 | /// Create a new service with the given configuration and initial state 45 | pub fn new(service: config::Service, state: State) -> ZInitService { 46 | ZInitService { 47 | pid: Pid::from_raw(0), 48 | state: Watched::new(state), 49 | service, 50 | target: Target::Up, 51 | scheduled: false, 52 | } 53 | } 54 | 55 | /// Get the current status of the service 56 | pub fn status(&self) -> ZInitStatus { 57 | ZInitStatus { 58 | pid: self.pid, 59 | state: self.state.get().clone(), 60 | service: self.service.clone(), 61 | target: self.target.clone(), 62 | scheduled: self.scheduled, 63 | } 64 | } 65 | 66 | /// Set the state of the service, validating the state transition 67 | pub fn set_state(&mut self, state: State) -> Result<()> { 68 | let current_state = self.state.get().clone(); 69 | let new_state = current_state 70 | .transition_to(state) 71 | .context("Failed to transition service state")?; 72 | 73 | self.state.set(new_state); 74 | Ok(()) 75 | } 76 | 77 | /// Set the state of the service without validation 78 | pub fn force_set_state(&mut self, state: State) { 79 | self.state.set(state); 80 | } 81 | 82 | /// Set the target state of the service 83 | pub fn set_target(&mut self, target: Target) { 84 | self.target = target; 85 | } 86 | 87 | /// Get the current state of the service 88 | pub fn get_state(&self) -> &State { 89 | self.state.get() 90 | } 91 | 92 | /// Get a watcher for the service state 93 | pub fn state_watcher(&self) -> crate::zinit::types::Watcher { 94 | self.state.watcher() 95 | } 96 | 97 | /// Check if the service is active (running or in progress) 98 | pub fn is_active(&self) -> bool { 99 | self.state.get().is_active() 100 | } 101 | 102 | /// Check if the service is in a terminal state (success or failure) 103 | pub fn is_terminal(&self) -> bool { 104 | self.state.get().is_terminal() 105 | } 106 | 107 | /// Set the process ID of the service 108 | pub fn set_pid(&mut self, pid: Pid) { 109 | self.pid = pid; 110 | } 111 | 112 | /// Clear the process ID of the service 113 | pub fn clear_pid(&mut self) { 114 | self.pid = Pid::from_raw(0); 115 | } 116 | 117 | /// Check if the service is running 118 | pub fn is_running(&self) -> bool { 119 | self.pid.as_raw() != 0 && self.state.get().is_active() 120 | } 121 | 122 | /// Check if the service is a one-shot service 123 | pub fn is_one_shot(&self) -> bool { 124 | self.service.one_shot 125 | } 126 | } 127 | -------------------------------------------------------------------------------- /src/zinit/state.rs: -------------------------------------------------------------------------------- 1 | use crate::zinit::errors::ZInitError; 2 | use anyhow::Result; 3 | use nix::sys::wait::WaitStatus; 4 | 5 | /// Target state for a service 6 | #[derive(Clone, Debug, PartialEq)] 7 | pub enum Target { 8 | /// Service should be running 9 | Up, 10 | /// Service should be stopped 11 | Down, 12 | } 13 | 14 | /// Service state 15 | #[derive(Debug, PartialEq, Clone)] 16 | pub enum State { 17 | /// Service is in an unknown state 18 | Unknown, 19 | 20 | /// Blocked means one or more dependencies hasn't been met yet. Service can stay in 21 | /// this state as long as at least one dependency is not in either Running, or Success 22 | Blocked, 23 | 24 | /// Service has been started, but it didn't exit yet, or we didn't run the test command. 25 | Spawned, 26 | 27 | /// Service has been started, and test command passed. 28 | Running, 29 | 30 | /// Service has exited with success state, only one-shot can stay in this state 31 | Success, 32 | 33 | /// Service exited with this error, only one-shot can stay in this state 34 | Error(WaitStatus), 35 | 36 | /// The service test command failed, this might (or might not) be replaced 37 | /// with an Error state later on once the service process itself exits 38 | TestFailure, 39 | 40 | /// Failure means the service has failed to spawn in a way that retrying 41 | /// won't help, like command line parsing error or failed to fork 42 | Failure, 43 | } 44 | 45 | impl State { 46 | /// Validate if a transition from the current state to the new state is valid 47 | pub fn can_transition_to(&self, new_state: &State) -> bool { 48 | match (self, new_state) { 49 | // From Unknown state, any transition is valid 50 | (State::Unknown, _) => true, 51 | 52 | // From Blocked state 53 | (State::Blocked, State::Spawned) => true, 54 | (State::Blocked, State::Failure) => true, 55 | 56 | // From Spawned state 57 | (State::Spawned, State::Running) => true, 58 | (State::Spawned, State::TestFailure) => true, 59 | (State::Spawned, State::Error(_)) => true, 60 | (State::Spawned, State::Success) => true, 61 | 62 | // From Running state 63 | (State::Running, State::Success) => true, 64 | (State::Running, State::Error(_)) => true, 65 | 66 | // To Unknown or Blocked state is always valid 67 | (_, State::Unknown) => true, 68 | (_, State::Blocked) => true, 69 | 70 | // Any other transition is invalid 71 | _ => false, 72 | } 73 | } 74 | 75 | /// Attempt to transition to a new state, validating the transition 76 | pub fn transition_to(&self, new_state: State) -> Result { 77 | if self.can_transition_to(&new_state) { 78 | Ok(new_state) 79 | } else { 80 | Err(ZInitError::invalid_state_transition(format!( 81 | "Invalid transition from {:?} to {:?}", 82 | self, new_state 83 | ))) 84 | } 85 | } 86 | 87 | /// Check if the state is considered "active" (running or in progress) 88 | pub fn is_active(&self) -> bool { 89 | matches!(self, State::Running | State::Spawned) 90 | } 91 | 92 | /// Check if the state is considered "terminal" (success or failure) 93 | pub fn is_terminal(&self) -> bool { 94 | matches!(self, State::Success | State::Error(_) | State::Failure) 95 | } 96 | 97 | /// Check if the state is considered "successful" 98 | pub fn is_successful(&self) -> bool { 99 | matches!(self, State::Success | State::Running) 100 | } 101 | 102 | /// Check if the state is considered "failed" 103 | pub fn is_failed(&self) -> bool { 104 | matches!(self, State::Error(_) | State::Failure | State::TestFailure) 105 | } 106 | } 107 | -------------------------------------------------------------------------------- /src/zinit/types.rs: -------------------------------------------------------------------------------- 1 | use nix::sys::wait::WaitStatus; 2 | use serde::{Deserialize, Serialize}; 3 | use std::collections::HashMap; 4 | use std::sync::Arc; 5 | use tokio::sync::watch; 6 | use tokio::sync::RwLock; 7 | use tokio_stream::wrappers::WatchStream; 8 | 9 | /// Stats information for a service 10 | #[derive(Debug, Clone, Serialize, Deserialize)] 11 | pub struct ServiceStats { 12 | /// Memory usage in bytes 13 | pub memory_usage: u64, 14 | 15 | /// CPU usage as a percentage (0-100) 16 | pub cpu_usage: f32, 17 | 18 | /// Process ID of the service 19 | pub pid: i32, 20 | 21 | /// Child process stats if any 22 | pub children: Vec, 23 | } 24 | 25 | /// Stats for an individual process 26 | #[derive(Debug, Clone, Serialize, Deserialize)] 27 | pub struct ProcessStats { 28 | /// Process ID 29 | pub pid: i32, 30 | 31 | /// Memory usage in bytes 32 | pub memory_usage: u64, 33 | 34 | /// CPU usage as a percentage (0-100) 35 | pub cpu_usage: f32, 36 | } 37 | 38 | /// Extension trait for WaitStatus to check if a process exited successfully 39 | pub trait WaitStatusExt { 40 | fn success(&self) -> bool; 41 | } 42 | 43 | impl WaitStatusExt for WaitStatus { 44 | fn success(&self) -> bool { 45 | matches!(self, WaitStatus::Exited(_, code) if *code == 0) 46 | } 47 | } 48 | 49 | /// Type alias for a service table mapping service names to service instances 50 | pub type ServiceTable = HashMap>>; 51 | 52 | /// Type alias for a watch stream 53 | pub type Watcher = WatchStream>; 54 | 55 | /// A wrapper around a value that can be watched for changes 56 | pub struct Watched { 57 | v: Arc, 58 | tx: watch::Sender>, 59 | } 60 | 61 | impl Watched 62 | where 63 | T: Send + Sync + 'static, 64 | { 65 | /// Create a new watched value 66 | pub fn new(v: T) -> Self { 67 | let v = Arc::new(v); 68 | let (tx, _) = watch::channel(Arc::clone(&v)); 69 | Self { v, tx } 70 | } 71 | 72 | /// Set the value and notify watchers 73 | pub fn set(&mut self, v: T) { 74 | let v = Arc::new(v); 75 | self.v = Arc::clone(&v); 76 | // update the value even when there are no receivers 77 | self.tx.send_replace(v); 78 | } 79 | 80 | /// Get a reference to the current value 81 | pub fn get(&self) -> &T { 82 | &self.v 83 | } 84 | 85 | /// Create a watcher for this value 86 | pub fn watcher(&self) -> Watcher { 87 | WatchStream::new(self.tx.subscribe()) 88 | } 89 | } 90 | -------------------------------------------------------------------------------- /stop.sh: -------------------------------------------------------------------------------- 1 | #!/bin/bash 2 | set -e 3 | 4 | # Colors for output 5 | RED='\033[0;31m' 6 | GREEN='\033[0;32m' 7 | YELLOW='\033[0;33m' 8 | NC='\033[0m' # No Color 9 | 10 | echo -e "${GREEN}Stopping zinit...${NC}" 11 | 12 | # Function to check if zinit is running 13 | is_zinit_running() { 14 | pgrep -f "zinit" > /dev/null 15 | return $? 16 | } 17 | 18 | # Try to shutdown zinit gracefully if it's running 19 | if is_zinit_running; then 20 | echo -e "${YELLOW}Zinit is already running. Attempting graceful shutdown...${NC}" 21 | zinit shutdown || true 22 | 23 | # Give it a moment to shut down 24 | sleep 2 25 | 26 | # Check if it's still running 27 | if is_zinit_running; then 28 | echo -e "${YELLOW}Zinit is still running. Attempting to kill the process...${NC}" 29 | pkill -f "zinit$" || true 30 | sleep 1 31 | fi 32 | else 33 | echo -e "${YELLOW}No existing zinit process found.${NC}" 34 | fi 35 | 36 | # Double-check no zinit is running 37 | if is_zinit_running; then 38 | echo -e "${RED}Warning: Could not terminate existing zinit process. You may need to manually kill it.${NC}" 39 | ps aux | grep "zinit" | grep -v grep 40 | else 41 | echo -e "${GREEN}No zinit process is running. Ready to start a new instance.${NC}" 42 | fi 43 | -------------------------------------------------------------------------------- /zinit-client/Cargo.toml: -------------------------------------------------------------------------------- 1 | [package] 2 | name = "zinit-client" 3 | version = "0.1.0" 4 | edition = "2021" 5 | description = "A client library for interacting with Zinit process manager" 6 | license = "Apache 2.0" 7 | authors = ["ThreeFold Tech, https://github.com/threefoldtech"] 8 | 9 | [dependencies] 10 | anyhow = "1.0" 11 | async-trait = "0.1.88" 12 | jsonrpsee = { version = "0.25.1", features = ["macros", "http-client", "ws-client"] } 13 | reth-ipc = { git = "https://github.com/paradigmxyz/reth", package = "reth-ipc" } 14 | tokio = { version = "1.14.0", features = ["full"] } 15 | serde = { version = "1.0", features = ["derive"] } 16 | serde_json = "1.0" 17 | thiserror = "1.0" 18 | log = "0.4" 19 | 20 | [[example]] 21 | name = "basic_usage" 22 | path = "examples/basic_usage.rs" 23 | 24 | [[example]] 25 | name = "http_client" 26 | path = "examples/http_client.rs" 27 | -------------------------------------------------------------------------------- /zinit-client/README.md: -------------------------------------------------------------------------------- 1 | # Zinit Client Library 2 | 3 | A simple Rust client library for interacting with the Zinit process manager. 4 | 5 | ## Features 6 | 7 | - Connect to Zinit via Unix socket or HTTP 8 | - Manage services (start, stop, restart, monitor) 9 | - Query service status and information 10 | - Create and delete service configurations 11 | - System operations (shutdown, reboot) 12 | 13 | ## Installation 14 | 15 | Add this to your `Cargo.toml`: 16 | 17 | ```toml 18 | [dependencies] 19 | zinit-client = "0.1.0" 20 | ``` 21 | 22 | ## Usage 23 | 24 | ### Creating a Client 25 | 26 | You can create a client using either Unix socket or HTTP transport: 27 | 28 | ```rust 29 | use zinit_client::Client; 30 | 31 | // Using Unix socket (local only) 32 | let client = Client::unix_socket("/var/run/zinit.sock"); 33 | 34 | // Using HTTP (works for remote Zinit instances) 35 | let client = Client::http("http://localhost:8080"); 36 | ``` 37 | 38 | ### Service Management 39 | 40 | ```rust 41 | // List all services 42 | let services = client.list().await?; 43 | for (name, state) in services { 44 | println!("{}: {}", name, state); 45 | } 46 | 47 | // Get status of a specific service 48 | let status = client.status("my-service").await?; 49 | println!("PID: {}, State: {}", status.pid, status.state); 50 | 51 | // Start a service 52 | client.start("my-service").await?; 53 | 54 | // Stop a service 55 | client.stop("my-service").await?; 56 | 57 | // Restart a service 58 | client.restart("my-service").await?; 59 | 60 | // Monitor a service 61 | client.monitor("my-service").await?; 62 | 63 | // Forget a service 64 | client.forget("my-service").await?; 65 | 66 | // Send a signal to a service 67 | client.kill("my-service", "SIGTERM").await?; 68 | ``` 69 | 70 | ### Service Configuration 71 | 72 | ```rust 73 | use serde_json::json; 74 | 75 | // Create a new service 76 | let config = json!({ 77 | "exec": "nginx", 78 | "oneshot": false, 79 | "after": ["network"] 80 | }).as_object().unwrap().clone(); 81 | 82 | client.create_service("nginx", config).await?; 83 | 84 | // Get service configuration 85 | let config = client.get_service("nginx").await?; 86 | println!("Config: {:?}", config); 87 | 88 | // Delete a service 89 | client.delete_service("nginx").await?; 90 | ``` 91 | 92 | ### System Operations 93 | 94 | ```rust 95 | // Shutdown the system 96 | client.shutdown().await?; 97 | 98 | // Reboot the system 99 | client.reboot().await?; 100 | ``` 101 | 102 | ## Error Handling 103 | 104 | The library provides a `ClientError` enum for handling errors: 105 | 106 | ```rust 107 | match client.status("non-existent-service").await { 108 | Ok(status) => println!("Service status: {}", status.state), 109 | Err(e) => match e { 110 | ClientError::ServiceNotFound(_) => println!("Service not found"), 111 | ClientError::ConnectionError(_) => println!("Failed to connect to Zinit"), 112 | _ => println!("Other error: {}", e), 113 | }, 114 | } 115 | ``` 116 | 117 | ## Examples 118 | 119 | See the [examples](examples) directory for complete usage examples. 120 | 121 | ## License 122 | 123 | This project is licensed under the MIT License. -------------------------------------------------------------------------------- /zinit-client/examples/basic_usage.rs: -------------------------------------------------------------------------------- 1 | use anyhow::Result; 2 | use zinit_client::Client; 3 | 4 | #[tokio::main] 5 | async fn main() -> Result<()> { 6 | // Create a client using Unix socket transport 7 | let client = Client::unix_socket("/var/run/zinit.sock").await?; 8 | 9 | // List all services 10 | let services = client.list().await?; 11 | println!("Services:"); 12 | for (name, state) in services { 13 | println!("{}: {}", name, state); 14 | } 15 | 16 | // Get a specific service status 17 | let service_name = "example-service"; 18 | match client.status(service_name).await { 19 | Ok(status) => { 20 | println!("\nService: {}", status.name); 21 | println!("PID: {}", status.pid); 22 | println!("State: {}", status.state); 23 | println!("Target: {}", status.target); 24 | println!("After:"); 25 | for (dep, state) in status.after { 26 | println!(" {}: {}", dep, state); 27 | } 28 | } 29 | Err(e) => eprintln!("Failed to get status: {}", e), 30 | } 31 | 32 | // Try to start a service 33 | match client.start(service_name).await { 34 | Ok(_) => println!("\nService started successfully"), 35 | Err(e) => eprintln!("Failed to start service: {}", e), 36 | } 37 | 38 | // Get logs for the service 39 | match client.logs(Some(service_name.to_string())).await { 40 | Ok(logs) => { 41 | println!("\nLogs:"); 42 | for log in logs { 43 | println!("{}", log); 44 | } 45 | } 46 | Err(e) => eprintln!("Failed to get logs: {}", e), 47 | } 48 | 49 | Ok(()) 50 | } 51 | -------------------------------------------------------------------------------- /zinit-client/examples/http_client.rs: -------------------------------------------------------------------------------- 1 | use anyhow::Result; 2 | use serde_json::json; 3 | use zinit_client::Client; 4 | 5 | #[tokio::main] 6 | async fn main() -> Result<()> { 7 | // Create a client using HTTP transport 8 | let client = Client::http("http://localhost:8080").await?; 9 | 10 | // Create a new service 11 | let service_name = "example-http-service"; 12 | let service_config = json!({ 13 | "exec": "echo 'Hello from HTTP service'", 14 | "oneshot": true, 15 | "after": ["network"] 16 | }) 17 | .as_object() 18 | .unwrap() 19 | .clone(); 20 | 21 | match client.create_service(service_name, service_config).await { 22 | Ok(msg) => println!("Service created: {}", msg), 23 | Err(e) => eprintln!("Failed to create service: {}", e), 24 | } 25 | 26 | // Start the HTTP/RPC server on a specific address 27 | match client.start_http_server("0.0.0.0:8081").await { 28 | Ok(msg) => println!("HTTP server status: {}", msg), 29 | Err(e) => eprintln!("Failed to start HTTP server: {}", e), 30 | } 31 | 32 | // List all services 33 | let services = client.list().await?; 34 | println!("\nServices:"); 35 | for (name, state) in services { 36 | println!("{}: {}", name, state); 37 | } 38 | 39 | // Monitor the service 40 | match client.monitor(service_name).await { 41 | Ok(_) => println!("\nService is now monitored"), 42 | Err(e) => eprintln!("Failed to monitor service: {}", e), 43 | } 44 | 45 | // Start the service 46 | match client.start(service_name).await { 47 | Ok(_) => println!("Service started successfully"), 48 | Err(e) => eprintln!("Failed to start service: {}", e), 49 | } 50 | 51 | // Get logs 52 | let logs = client.logs(Some(service_name.to_string())).await?; 53 | println!("\nLogs:"); 54 | for log in logs { 55 | println!("{}", log); 56 | } 57 | 58 | // Clean up - forget the service 59 | println!("\nCleaning up..."); 60 | match client.forget(service_name).await { 61 | Ok(_) => println!("Service has been forgotten"), 62 | Err(e) => eprintln!("Failed to forget service: {}", e), 63 | } 64 | 65 | // Clean up - delete the service configuration 66 | match client.delete_service(service_name).await { 67 | Ok(msg) => println!("{}", msg), 68 | Err(e) => eprintln!("Failed to delete service: {}", e), 69 | } 70 | 71 | // Stop the HTTP/RPC server 72 | match client.stop_http_server().await { 73 | Ok(_) => println!("HTTP server stopped"), 74 | Err(e) => eprintln!("Failed to stop HTTP server: {}", e), 75 | } 76 | 77 | Ok(()) 78 | } 79 | -------------------------------------------------------------------------------- /zinit-client/src/lib.rs: -------------------------------------------------------------------------------- 1 | //! A client library for interacting with the Zinit process manager. 2 | //! 3 | //! This library provides a simple API for communicating with a Zinit daemon 4 | //! via either Unix socket (using reth-ipc) or HTTP (using jsonrpsee). 5 | use jsonrpsee::core::client::ClientT; 6 | use jsonrpsee::core::client::Error as RpcError; 7 | use jsonrpsee::http_client::{HttpClient, HttpClientBuilder}; 8 | use jsonrpsee::rpc_params; 9 | use reth_ipc::client::IpcClientBuilder; 10 | use serde::{Deserialize, Serialize}; 11 | use serde_json::{Map, Value}; 12 | use std::collections::HashMap; 13 | use thiserror::Error; 14 | 15 | /// Error type for client operations 16 | #[derive(Error, Debug)] 17 | pub enum ClientError { 18 | #[error("connection error: {0}")] 19 | ConnectionError(String), 20 | 21 | #[error("service not found: {0}")] 22 | ServiceNotFound(String), 23 | 24 | #[error("service is already up: {0}")] 25 | ServiceIsUp(String), 26 | 27 | #[error("system is shutting down")] 28 | ShuttingDown, 29 | 30 | #[error("service already exists: {0}")] 31 | ServiceAlreadyExists(String), 32 | 33 | #[error("service file error: {0}")] 34 | ServiceFileError(String), 35 | 36 | #[error("rpc error: {0}")] 37 | RpcError(String), 38 | 39 | #[error("unknown error: {0}")] 40 | UnknownError(String), 41 | } 42 | 43 | impl From for ClientError { 44 | fn from(err: RpcError) -> Self { 45 | // Parse the error code if available 46 | if let RpcError::Call(err) = &err { 47 | match err.code() { 48 | -32000 => return ClientError::ServiceNotFound(err.message().to_string()), 49 | -32002 => return ClientError::ServiceIsUp(err.message().to_string()), 50 | -32006 => return ClientError::ShuttingDown, 51 | -32007 => return ClientError::ServiceAlreadyExists(err.message().to_string()), 52 | -32008 => return ClientError::ServiceFileError(err.message().to_string()), 53 | _ => {} 54 | } 55 | } 56 | 57 | match err { 58 | RpcError::Transport(_) => ClientError::ConnectionError(err.to_string()), 59 | _ => ClientError::RpcError(err.to_string()), 60 | } 61 | } 62 | } 63 | 64 | /// Service status information 65 | #[derive(Debug, Clone, Deserialize, Serialize)] 66 | pub struct Status { 67 | pub name: String, 68 | pub pid: u32, 69 | pub state: String, 70 | pub target: String, 71 | pub after: HashMap, 72 | } 73 | 74 | /// Child process stats information 75 | #[derive(Debug, Clone, Deserialize, Serialize)] 76 | pub struct ChildStats { 77 | pub pid: u32, 78 | pub memory_usage: u64, 79 | pub cpu_usage: f32, 80 | } 81 | 82 | /// Service stats information 83 | #[derive(Debug, Clone, Deserialize, Serialize)] 84 | pub struct Stats { 85 | pub name: String, 86 | pub pid: u32, 87 | pub memory_usage: u64, 88 | pub cpu_usage: f32, 89 | pub children: Vec, 90 | } 91 | 92 | /// Client implementation for communicating with Zinit 93 | pub enum Client { 94 | Ipc(String), // Socket path 95 | Http(HttpClient), 96 | } 97 | 98 | impl Client { 99 | /// Create a new client using Unix socket transport 100 | pub async fn unix_socket>(path: P) -> Result { 101 | Ok(Client::Ipc(path.as_ref().to_string_lossy().to_string())) 102 | } 103 | 104 | /// Create a new client using HTTP transport 105 | pub async fn http>(url: S) -> Result { 106 | let client = HttpClientBuilder::default() 107 | .build(url.as_ref()) 108 | .map_err(|e| ClientError::ConnectionError(e.to_string()))?; 109 | 110 | Ok(Client::Http(client)) 111 | } 112 | 113 | // Helper to get IPC client 114 | async fn get_ipc_client(&self) -> Result { 115 | match self { 116 | Client::Ipc(path) => IpcClientBuilder::default() 117 | .build(path) 118 | .await 119 | .map_err(|e| ClientError::ConnectionError(e.to_string())), 120 | _ => Err(ClientError::UnknownError("Not an IPC client".to_string())), 121 | } 122 | } 123 | 124 | // Service API Methods 125 | 126 | /// List all monitored services and their current state 127 | pub async fn list(&self) -> Result, ClientError> { 128 | match self { 129 | Client::Ipc(_) => { 130 | let client = self.get_ipc_client().await?; 131 | client 132 | .request("service_list", rpc_params![]) 133 | .await 134 | .map_err(Into::into) 135 | } 136 | Client::Http(client) => client 137 | .request("service_list", rpc_params![]) 138 | .await 139 | .map_err(Into::into), 140 | } 141 | } 142 | 143 | /// Get the detailed status of a specific service 144 | pub async fn status(&self, name: impl AsRef) -> Result { 145 | let name = name.as_ref().to_string(); 146 | match self { 147 | Client::Ipc(_) => { 148 | let client = self.get_ipc_client().await?; 149 | client 150 | .request("service_status", rpc_params![name]) 151 | .await 152 | .map_err(Into::into) 153 | } 154 | Client::Http(client) => client 155 | .request("service_status", rpc_params![name]) 156 | .await 157 | .map_err(Into::into), 158 | } 159 | } 160 | 161 | /// Start a specific service 162 | pub async fn start(&self, name: impl AsRef) -> Result<(), ClientError> { 163 | let name = name.as_ref().to_string(); 164 | match self { 165 | Client::Ipc(_) => { 166 | let client = self.get_ipc_client().await?; 167 | client 168 | .request("service_start", rpc_params![name]) 169 | .await 170 | .map_err(Into::into) 171 | } 172 | Client::Http(client) => client 173 | .request("service_start", rpc_params![name]) 174 | .await 175 | .map_err(Into::into), 176 | } 177 | } 178 | 179 | /// Stop a specific service 180 | pub async fn stop(&self, name: impl AsRef) -> Result<(), ClientError> { 181 | let name = name.as_ref().to_string(); 182 | match self { 183 | Client::Ipc(_) => { 184 | let client = self.get_ipc_client().await?; 185 | client 186 | .request("service_stop", rpc_params![name]) 187 | .await 188 | .map_err(Into::into) 189 | } 190 | Client::Http(client) => client 191 | .request("service_stop", rpc_params![name]) 192 | .await 193 | .map_err(Into::into), 194 | } 195 | } 196 | 197 | /// Restart a service 198 | pub async fn restart(&self, name: impl AsRef) -> Result<(), ClientError> { 199 | let name = name.as_ref().to_string(); 200 | // First stop the service 201 | self.stop(&name).await?; 202 | 203 | // Poll the service status until it's stopped 204 | for _ in 0..20 { 205 | let status = self.status(&name).await?; 206 | if status.pid == 0 && status.target == "Down" { 207 | return self.start(&name).await; 208 | } 209 | tokio::time::sleep(std::time::Duration::from_secs(1)).await; 210 | } 211 | 212 | // Process not stopped, try to kill it 213 | self.kill(&name, "SIGKILL").await?; 214 | self.start(&name).await 215 | } 216 | 217 | /// Load and monitor a new service from its configuration file 218 | pub async fn monitor(&self, name: impl AsRef) -> Result<(), ClientError> { 219 | let name = name.as_ref().to_string(); 220 | match self { 221 | Client::Ipc(_) => { 222 | let client = self.get_ipc_client().await?; 223 | client 224 | .request("service_monitor", rpc_params![name]) 225 | .await 226 | .map_err(Into::into) 227 | } 228 | Client::Http(client) => client 229 | .request("service_monitor", rpc_params![name]) 230 | .await 231 | .map_err(Into::into), 232 | } 233 | } 234 | 235 | /// Stop monitoring a service and remove it from management 236 | pub async fn forget(&self, name: impl AsRef) -> Result<(), ClientError> { 237 | let name = name.as_ref().to_string(); 238 | match self { 239 | Client::Ipc(_) => { 240 | let client = self.get_ipc_client().await?; 241 | client 242 | .request("service_forget", rpc_params![name]) 243 | .await 244 | .map_err(Into::into) 245 | } 246 | Client::Http(client) => client 247 | .request("service_forget", rpc_params![name]) 248 | .await 249 | .map_err(Into::into), 250 | } 251 | } 252 | 253 | /// Send a signal to a specific service process 254 | pub async fn kill( 255 | &self, 256 | name: impl AsRef, 257 | signal: impl AsRef, 258 | ) -> Result<(), ClientError> { 259 | let name = name.as_ref().to_string(); 260 | let signal = signal.as_ref().to_string(); 261 | match self { 262 | Client::Ipc(_) => { 263 | let client = self.get_ipc_client().await?; 264 | client 265 | .request("service_kill", rpc_params![name, signal]) 266 | .await 267 | .map_err(Into::into) 268 | } 269 | Client::Http(client) => client 270 | .request("service_kill", rpc_params![name, signal]) 271 | .await 272 | .map_err(Into::into), 273 | } 274 | } 275 | 276 | /// Create a new service configuration 277 | pub async fn create_service( 278 | &self, 279 | name: impl AsRef, 280 | content: Map, 281 | ) -> Result { 282 | let name = name.as_ref().to_string(); 283 | match self { 284 | Client::Ipc(_) => { 285 | let client = self.get_ipc_client().await?; 286 | client 287 | .request("service_create", rpc_params![name, content]) 288 | .await 289 | .map_err(Into::into) 290 | } 291 | Client::Http(client) => client 292 | .request("service_create", rpc_params![name, content]) 293 | .await 294 | .map_err(Into::into), 295 | } 296 | } 297 | 298 | /// Delete a service configuration 299 | pub async fn delete_service(&self, name: impl AsRef) -> Result { 300 | let name = name.as_ref().to_string(); 301 | match self { 302 | Client::Ipc(_) => { 303 | let client = self.get_ipc_client().await?; 304 | client 305 | .request("service_delete", rpc_params![name]) 306 | .await 307 | .map_err(Into::into) 308 | } 309 | Client::Http(client) => client 310 | .request("service_delete", rpc_params![name]) 311 | .await 312 | .map_err(Into::into), 313 | } 314 | } 315 | 316 | /// Get a service configuration 317 | pub async fn get_service(&self, name: impl AsRef) -> Result { 318 | let name = name.as_ref().to_string(); 319 | match self { 320 | Client::Ipc(_) => { 321 | let client = self.get_ipc_client().await?; 322 | client 323 | .request("service_get", rpc_params![name]) 324 | .await 325 | .map_err(Into::into) 326 | } 327 | Client::Http(client) => client 328 | .request("service_get", rpc_params![name]) 329 | .await 330 | .map_err(Into::into), 331 | } 332 | } 333 | 334 | /// Get memory and CPU usage statistics for a service 335 | pub async fn stats(&self, name: impl AsRef) -> Result { 336 | let name = name.as_ref().to_string(); 337 | match self { 338 | Client::Ipc(_) => { 339 | let client = self.get_ipc_client().await?; 340 | client 341 | .request("service_stats", rpc_params![name]) 342 | .await 343 | .map_err(Into::into) 344 | } 345 | Client::Http(client) => client 346 | .request("service_stats", rpc_params![name]) 347 | .await 348 | .map_err(Into::into), 349 | } 350 | } 351 | 352 | // System API Methods 353 | 354 | /// Initiate system shutdown 355 | pub async fn shutdown(&self) -> Result<(), ClientError> { 356 | match self { 357 | Client::Ipc(_) => { 358 | let client = self.get_ipc_client().await?; 359 | client 360 | .request("system_shutdown", rpc_params![]) 361 | .await 362 | .map_err(Into::into) 363 | } 364 | Client::Http(client) => client 365 | .request("system_shutdown", rpc_params![]) 366 | .await 367 | .map_err(Into::into), 368 | } 369 | } 370 | 371 | /// Initiate system reboot 372 | pub async fn reboot(&self) -> Result<(), ClientError> { 373 | match self { 374 | Client::Ipc(_) => { 375 | let client = self.get_ipc_client().await?; 376 | client 377 | .request("system_reboot", rpc_params![]) 378 | .await 379 | .map_err(Into::into) 380 | } 381 | Client::Http(client) => client 382 | .request("system_reboot", rpc_params![]) 383 | .await 384 | .map_err(Into::into), 385 | } 386 | } 387 | 388 | /// Start HTTP/RPC server 389 | pub async fn start_http_server(&self, address: impl AsRef) -> Result { 390 | let address = address.as_ref().to_string(); 391 | match self { 392 | Client::Ipc(_) => { 393 | let client = self.get_ipc_client().await?; 394 | client 395 | .request("system_start_http_server", rpc_params![address]) 396 | .await 397 | .map_err(Into::into) 398 | } 399 | Client::Http(client) => client 400 | .request("system_start_http_server", rpc_params![address]) 401 | .await 402 | .map_err(Into::into), 403 | } 404 | } 405 | 406 | /// Stop HTTP/RPC server 407 | pub async fn stop_http_server(&self) -> Result<(), ClientError> { 408 | match self { 409 | Client::Ipc(_) => { 410 | let client = self.get_ipc_client().await?; 411 | client 412 | .request("system_stop_http_server", rpc_params![]) 413 | .await 414 | .map_err(Into::into) 415 | } 416 | Client::Http(client) => client 417 | .request("system_stop_http_server", rpc_params![]) 418 | .await 419 | .map_err(Into::into), 420 | } 421 | } 422 | 423 | // Logging API Methods 424 | 425 | /// Get current logs 426 | pub async fn logs(&self, filter: Option) -> Result, ClientError> { 427 | match self { 428 | Client::Ipc(_) => { 429 | let client = self.get_ipc_client().await?; 430 | client 431 | .request("stream_currentLogs", rpc_params![filter]) 432 | .await 433 | .map_err(Into::into) 434 | } 435 | Client::Http(client) => client 436 | .request("stream_currentLogs", rpc_params![filter]) 437 | .await 438 | .map_err(Into::into), 439 | } 440 | } 441 | 442 | /// Subscribe to logs 443 | /// 444 | /// Note: This method is not fully implemented yet. For now, it will return an error. 445 | pub async fn log_subscribe(&self, _filter: Option) -> Result<(), ClientError> { 446 | Err(ClientError::UnknownError( 447 | "Log subscription not implemented yet".to_string(), 448 | )) 449 | } 450 | } 451 | -------------------------------------------------------------------------------- /zinit-client/tests/integration_test.rs: -------------------------------------------------------------------------------- 1 | use std::env; 2 | use zinit_client::{Client, ClientError}; 3 | 4 | #[tokio::test] 5 | async fn test_connection_error() { 6 | // Try to connect to a non-existent socket 7 | let result = Client::unix_socket("/non/existent/socket").await; 8 | assert!(result.is_ok()); // Just creating the client succeeds 9 | 10 | // Trying to make a request should fail 11 | if let Ok(client) = result { 12 | let list_result = client.list().await; 13 | assert!(matches!(list_result, Err(ClientError::ConnectionError(_)))); 14 | } 15 | } 16 | 17 | #[tokio::test] 18 | async fn test_http_connection_error() { 19 | // Try to connect to a non-existent HTTP endpoint 20 | let result = Client::http("http://localhost:12345").await; 21 | // This should succeed as we're just creating the client, not making a request 22 | assert!(result.is_ok()); 23 | 24 | // Try to make a request which should fail 25 | if let Ok(client) = result { 26 | let list_result = client.list().await; 27 | assert!(matches!(list_result, Err(ClientError::ConnectionError(_)))); 28 | } 29 | } 30 | 31 | // This test only runs if ZINIT_SOCKET is set in the environment 32 | // and points to a valid Zinit socket 33 | #[tokio::test] 34 | #[ignore] 35 | async fn test_live_connection() { 36 | let socket_path = match env::var("ZINIT_SOCKET") { 37 | Ok(path) => path, 38 | Err(_) => { 39 | println!("ZINIT_SOCKET not set, skipping live test"); 40 | return; 41 | } 42 | }; 43 | 44 | let client = match Client::unix_socket(&socket_path).await { 45 | Ok(client) => client, 46 | Err(e) => { 47 | panic!( 48 | "Failed to connect to Zinit socket at {}: {}", 49 | socket_path, e 50 | ); 51 | } 52 | }; 53 | 54 | // Test listing services 55 | let services = client.list().await.expect("Failed to list services"); 56 | println!("Found {} services", services.len()); 57 | 58 | // If there are services, test getting status of the first one 59 | if let Some((service_name, _)) = services.iter().next() { 60 | let status = client 61 | .status(service_name) 62 | .await 63 | .expect("Failed to get service status"); 64 | println!("Service {} has PID {}", service_name, status.pid); 65 | } 66 | } 67 | --------------------------------------------------------------------------------