Merge commit '2fda71af117a90da5f496d8bb8105f0ee9e07420' as 'components/zinit'
This commit is contained in:
2
components/zinit/.cargo/config.toml
Normal file
2
components/zinit/.cargo/config.toml
Normal file
@@ -0,0 +1,2 @@
|
||||
[target.aarch64-unknown-linux-musl]
|
||||
linker = "aarch64-linux-musl-gcc"
|
||||
134
components/zinit/.github/workflows/release.yaml
vendored
Normal file
134
components/zinit/.github/workflows/release.yaml
vendored
Normal file
@@ -0,0 +1,134 @@
|
||||
on:
|
||||
push:
|
||||
# Sequence of patterns matched against refs/tags
|
||||
tags:
|
||||
- "v*" # Push events to matching v*, i.e. v1.0, v20.15.10
|
||||
|
||||
name: Create Release
|
||||
|
||||
jobs:
|
||||
build:
|
||||
name: Build and Release
|
||||
runs-on: ${{ matrix.os }}
|
||||
permissions:
|
||||
contents: write
|
||||
strategy:
|
||||
fail-fast: false # Continue with other builds if one fails
|
||||
matrix:
|
||||
include:
|
||||
# Linux builds
|
||||
- os: ubuntu-latest
|
||||
target: x86_64-unknown-linux-musl
|
||||
binary_name: zinit-linux-x86_64
|
||||
# macOS builds
|
||||
- os: macos-latest
|
||||
target: x86_64-apple-darwin
|
||||
binary_name: zinit-macos-x86_64
|
||||
- os: macos-latest
|
||||
target: aarch64-apple-darwin
|
||||
binary_name: zinit-macos-aarch64
|
||||
steps:
|
||||
- name: Checkout code
|
||||
uses: actions/checkout@v4
|
||||
with:
|
||||
fetch-depth: 0 # Fetch all history for proper versioning
|
||||
|
||||
# Cache Rust dependencies
|
||||
- name: Cache Rust dependencies
|
||||
uses: actions/cache@v3
|
||||
with:
|
||||
path: |
|
||||
~/.cargo/registry
|
||||
~/.cargo/git
|
||||
target
|
||||
key: ${{ runner.os }}-${{ matrix.target }}-cargo-${{ hashFiles('**/Cargo.lock') }}
|
||||
restore-keys: |
|
||||
${{ runner.os }}-${{ matrix.target }}-cargo-
|
||||
|
||||
- name: Setup build environment (macOS)
|
||||
if: matrix.os == 'macos-latest'
|
||||
run: |
|
||||
# Install required build tools for macOS
|
||||
brew install llvm
|
||||
|
||||
# For cross-compilation to Apple Silicon when on Intel
|
||||
if [[ "${{ matrix.target }}" == "aarch64-apple-darwin" ]]; then
|
||||
rustup target add aarch64-apple-darwin
|
||||
fi
|
||||
|
||||
- name: Install Rust toolchain
|
||||
uses: actions-rs/toolchain@v1
|
||||
with:
|
||||
toolchain: stable
|
||||
target: ${{ matrix.target }}
|
||||
override: true
|
||||
profile: minimal # Minimal components for faster installation
|
||||
|
||||
- name: Install MUSL tools (Linux)
|
||||
if: matrix.os == 'ubuntu-latest' && contains(matrix.target, 'musl')
|
||||
run: |
|
||||
sudo apt-get update
|
||||
sudo apt-get install -y musl-tools musl-dev
|
||||
|
||||
- name: Build release
|
||||
env:
|
||||
CC: ${{ matrix.os == 'macos-latest' && 'clang' || '' }}
|
||||
CXX: ${{ matrix.os == 'macos-latest' && 'clang++' || '' }}
|
||||
MACOSX_DEPLOYMENT_TARGET: '10.12'
|
||||
run: |
|
||||
# Add special flags for Apple Silicon builds
|
||||
if [[ "${{ matrix.target }}" == "aarch64-apple-darwin" ]]; then
|
||||
export RUSTFLAGS="-C target-feature=+crt-static"
|
||||
fi
|
||||
|
||||
cargo build --release --target=${{ matrix.target }} --verbose
|
||||
|
||||
# Verify binary exists
|
||||
if [ ! -f "target/${{ matrix.target }}/release/zinit" ]; then
|
||||
echo "::error::Binary not found at target/${{ matrix.target }}/release/zinit"
|
||||
exit 1
|
||||
fi
|
||||
|
||||
- name: Strip binary (Linux)
|
||||
if: matrix.os == 'ubuntu-latest'
|
||||
run: |
|
||||
strip target/${{ matrix.target }}/release/zinit
|
||||
|
||||
- name: Strip binary (macOS)
|
||||
if: matrix.os == 'macos-latest'
|
||||
run: |
|
||||
strip -x target/${{ matrix.target }}/release/zinit || true
|
||||
|
||||
- name: Rename binary
|
||||
run: |
|
||||
cp target/${{ matrix.target }}/release/zinit ${{ matrix.binary_name }}
|
||||
|
||||
# Verify binary was copied successfully
|
||||
if [ ! -f "${{ matrix.binary_name }}" ]; then
|
||||
echo "::error::Binary not copied successfully to ${{ matrix.binary_name }}"
|
||||
exit 1
|
||||
fi
|
||||
|
||||
# Show binary info for debugging
|
||||
echo "Binary details for ${{ matrix.binary_name }}:"
|
||||
ls -la ${{ matrix.binary_name }}
|
||||
file ${{ matrix.binary_name }} || true
|
||||
|
||||
# Upload artifacts even if the release step fails
|
||||
- name: Upload artifacts
|
||||
uses: actions/upload-artifact@v4
|
||||
with:
|
||||
name: ${{ matrix.binary_name }}
|
||||
path: ${{ matrix.binary_name }}
|
||||
retention-days: 5
|
||||
|
||||
- name: Upload Release Assets
|
||||
uses: softprops/action-gh-release@v2
|
||||
with:
|
||||
files: ${{ matrix.binary_name }}
|
||||
name: Release ${{ github.ref_name }}
|
||||
draft: false
|
||||
prerelease: false
|
||||
fail_on_unmatched_files: false
|
||||
env:
|
||||
GITHUB_TOKEN: ${{ secrets.GITHUB_TOKEN }}
|
||||
36
components/zinit/.github/workflows/rust.yml
vendored
Normal file
36
components/zinit/.github/workflows/rust.yml
vendored
Normal file
@@ -0,0 +1,36 @@
|
||||
name: Rust
|
||||
|
||||
on: [push]
|
||||
|
||||
jobs:
|
||||
build:
|
||||
runs-on: ubuntu-latest
|
||||
steps:
|
||||
- uses: actions/checkout@v1
|
||||
name: Checkout code
|
||||
with:
|
||||
fetch-depth: 1
|
||||
- uses: actions-rs/toolchain@v1
|
||||
name: Install toolchain
|
||||
with:
|
||||
toolchain: stable
|
||||
target: x86_64-unknown-linux-musl
|
||||
- uses: actions-rs/cargo@v1
|
||||
name: Check formatting
|
||||
with:
|
||||
command: fmt
|
||||
args: -- --check
|
||||
- uses: actions-rs/cargo@v1
|
||||
name: Run tests (ahm!)
|
||||
with:
|
||||
command: test
|
||||
args: --verbose
|
||||
- uses: actions-rs/cargo@v1
|
||||
name: Run clippy
|
||||
with:
|
||||
command: clippy
|
||||
- uses: actions-rs/cargo@v1
|
||||
name: Build
|
||||
with:
|
||||
command: build
|
||||
args: --verbose
|
||||
2
components/zinit/.gitignore
vendored
Normal file
2
components/zinit/.gitignore
vendored
Normal file
@@ -0,0 +1,2 @@
|
||||
/target
|
||||
**/*.rs.bk
|
||||
2717
components/zinit/Cargo.lock
generated
Normal file
2717
components/zinit/Cargo.lock
generated
Normal file
File diff suppressed because it is too large
Load Diff
50
components/zinit/Cargo.toml
Normal file
50
components/zinit/Cargo.toml
Normal file
@@ -0,0 +1,50 @@
|
||||
[workspace]
|
||||
members = [
|
||||
".",
|
||||
"zinit-client"
|
||||
]
|
||||
|
||||
[package]
|
||||
name = "zinit"
|
||||
version = "0.2.0"
|
||||
edition = "2018"
|
||||
|
||||
# See more keys and their definitions at https://doc.rust-lang.org/cargo/reference/manifest.html
|
||||
|
||||
[dependencies]
|
||||
anyhow = "1.0"
|
||||
tokio = { version = "1.44.1", features = ["full"] }
|
||||
tokio-stream = { version = "0.1.17", features = ["sync"] }
|
||||
shlex ="1.1"
|
||||
nix = "0.22.1"
|
||||
serde = { version = "1.0", features = ["derive"] }
|
||||
serde_yaml = "0.8"
|
||||
serde_json = "1.0"
|
||||
fern = "0.6"
|
||||
log = "0.4"
|
||||
thiserror = "1.0"
|
||||
clap = "2.33"
|
||||
git-version = "0.3.5"
|
||||
command-group = "1.0.8"
|
||||
dirs = "5.0"
|
||||
hyper = "1.6"
|
||||
# axum = { version = "0.7.4", features = ["http1"] }
|
||||
bytes = "1.0"
|
||||
jsonrpsee = { version = "0.25.1", features = ["server", "client", "macros"] }
|
||||
memchr = "2.5.0"
|
||||
async-trait = "0.1.88"
|
||||
reth-ipc = { git = "https://github.com/paradigmxyz/reth", package = "reth-ipc" }
|
||||
tower-http = { version = "0.5", features = ["cors"] }
|
||||
tower = "0.5.2"
|
||||
sysinfo = "0.29.10"
|
||||
|
||||
[dev-dependencies]
|
||||
tokio = { version = "1.14.0", features = ["full", "test-util"] }
|
||||
tempfile = "3.3.0"
|
||||
[lib]
|
||||
name = "zinit"
|
||||
path = "src/lib.rs"
|
||||
|
||||
[[bin]]
|
||||
name = "zinit"
|
||||
path = "src/main.rs"
|
||||
201
components/zinit/LICENSE
Normal file
201
components/zinit/LICENSE
Normal file
@@ -0,0 +1,201 @@
|
||||
Apache License
|
||||
Version 2.0, January 2004
|
||||
http://www.apache.org/licenses/
|
||||
|
||||
TERMS AND CONDITIONS FOR USE, REPRODUCTION, AND DISTRIBUTION
|
||||
|
||||
1. Definitions.
|
||||
|
||||
"License" shall mean the terms and conditions for use, reproduction,
|
||||
and distribution as defined by Sections 1 through 9 of this document.
|
||||
|
||||
"Licensor" shall mean the copyright owner or entity authorized by
|
||||
the copyright owner that is granting the License.
|
||||
|
||||
"Legal Entity" shall mean the union of the acting entity and all
|
||||
other entities that control, are controlled by, or are under common
|
||||
control with that entity. For the purposes of this definition,
|
||||
"control" means (i) the power, direct or indirect, to cause the
|
||||
direction or management of such entity, whether by contract or
|
||||
otherwise, or (ii) ownership of fifty percent (50%) or more of the
|
||||
outstanding shares, or (iii) beneficial ownership of such entity.
|
||||
|
||||
"You" (or "Your") shall mean an individual or Legal Entity
|
||||
exercising permissions granted by this License.
|
||||
|
||||
"Source" form shall mean the preferred form for making modifications,
|
||||
including but not limited to software source code, documentation
|
||||
source, and configuration files.
|
||||
|
||||
"Object" form shall mean any form resulting from mechanical
|
||||
transformation or translation of a Source form, including but
|
||||
not limited to compiled object code, generated documentation,
|
||||
and conversions to other media types.
|
||||
|
||||
"Work" shall mean the work of authorship, whether in Source or
|
||||
Object form, made available under the License, as indicated by a
|
||||
copyright notice that is included in or attached to the work
|
||||
(an example is provided in the Appendix below).
|
||||
|
||||
"Derivative Works" shall mean any work, whether in Source or Object
|
||||
form, that is based on (or derived from) the Work and for which the
|
||||
editorial revisions, annotations, elaborations, or other modifications
|
||||
represent, as a whole, an original work of authorship. For the purposes
|
||||
of this License, Derivative Works shall not include works that remain
|
||||
separable from, or merely link (or bind by name) to the interfaces of,
|
||||
the Work and Derivative Works thereof.
|
||||
|
||||
"Contribution" shall mean any work of authorship, including
|
||||
the original version of the Work and any modifications or additions
|
||||
to that Work or Derivative Works thereof, that is intentionally
|
||||
submitted to Licensor for inclusion in the Work by the copyright owner
|
||||
or by an individual or Legal Entity authorized to submit on behalf of
|
||||
the copyright owner. For the purposes of this definition, "submitted"
|
||||
means any form of electronic, verbal, or written communication sent
|
||||
to the Licensor or its representatives, including but not limited to
|
||||
communication on electronic mailing lists, source code control systems,
|
||||
and issue tracking systems that are managed by, or on behalf of, the
|
||||
Licensor for the purpose of discussing and improving the Work, but
|
||||
excluding communication that is conspicuously marked or otherwise
|
||||
designated in writing by the copyright owner as "Not a Contribution."
|
||||
|
||||
"Contributor" shall mean Licensor and any individual or Legal Entity
|
||||
on behalf of whom a Contribution has been received by Licensor and
|
||||
subsequently incorporated within the Work.
|
||||
|
||||
2. Grant of Copyright License. Subject to the terms and conditions of
|
||||
this License, each Contributor hereby grants to You a perpetual,
|
||||
worldwide, non-exclusive, no-charge, royalty-free, irrevocable
|
||||
copyright license to reproduce, prepare Derivative Works of,
|
||||
publicly display, publicly perform, sublicense, and distribute the
|
||||
Work and such Derivative Works in Source or Object form.
|
||||
|
||||
3. Grant of Patent License. Subject to the terms and conditions of
|
||||
this License, each Contributor hereby grants to You a perpetual,
|
||||
worldwide, non-exclusive, no-charge, royalty-free, irrevocable
|
||||
(except as stated in this section) patent license to make, have made,
|
||||
use, offer to sell, sell, import, and otherwise transfer the Work,
|
||||
where such license applies only to those patent claims licensable
|
||||
by such Contributor that are necessarily infringed by their
|
||||
Contribution(s) alone or by combination of their Contribution(s)
|
||||
with the Work to which such Contribution(s) was submitted. If You
|
||||
institute patent litigation against any entity (including a
|
||||
cross-claim or counterclaim in a lawsuit) alleging that the Work
|
||||
or a Contribution incorporated within the Work constitutes direct
|
||||
or contributory patent infringement, then any patent licenses
|
||||
granted to You under this License for that Work shall terminate
|
||||
as of the date such litigation is filed.
|
||||
|
||||
4. Redistribution. You may reproduce and distribute copies of the
|
||||
Work or Derivative Works thereof in any medium, with or without
|
||||
modifications, and in Source or Object form, provided that You
|
||||
meet the following conditions:
|
||||
|
||||
(a) You must give any other recipients of the Work or
|
||||
Derivative Works a copy of this License; and
|
||||
|
||||
(b) You must cause any modified files to carry prominent notices
|
||||
stating that You changed the files; and
|
||||
|
||||
(c) You must retain, in the Source form of any Derivative Works
|
||||
that You distribute, all copyright, patent, trademark, and
|
||||
attribution notices from the Source form of the Work,
|
||||
excluding those notices that do not pertain to any part of
|
||||
the Derivative Works; and
|
||||
|
||||
(d) If the Work includes a "NOTICE" text file as part of its
|
||||
distribution, then any Derivative Works that You distribute must
|
||||
include a readable copy of the attribution notices contained
|
||||
within such NOTICE file, excluding those notices that do not
|
||||
pertain to any part of the Derivative Works, in at least one
|
||||
of the following places: within a NOTICE text file distributed
|
||||
as part of the Derivative Works; within the Source form or
|
||||
documentation, if provided along with the Derivative Works; or,
|
||||
within a display generated by the Derivative Works, if and
|
||||
wherever such third-party notices normally appear. The contents
|
||||
of the NOTICE file are for informational purposes only and
|
||||
do not modify the License. You may add Your own attribution
|
||||
notices within Derivative Works that You distribute, alongside
|
||||
or as an addendum to the NOTICE text from the Work, provided
|
||||
that such additional attribution notices cannot be construed
|
||||
as modifying the License.
|
||||
|
||||
You may add Your own copyright statement to Your modifications and
|
||||
may provide additional or different license terms and conditions
|
||||
for use, reproduction, or distribution of Your modifications, or
|
||||
for any such Derivative Works as a whole, provided Your use,
|
||||
reproduction, and distribution of the Work otherwise complies with
|
||||
the conditions stated in this License.
|
||||
|
||||
5. Submission of Contributions. Unless You explicitly state otherwise,
|
||||
any Contribution intentionally submitted for inclusion in the Work
|
||||
by You to the Licensor shall be under the terms and conditions of
|
||||
this License, without any additional terms or conditions.
|
||||
Notwithstanding the above, nothing herein shall supersede or modify
|
||||
the terms of any separate license agreement you may have executed
|
||||
with Licensor regarding such Contributions.
|
||||
|
||||
6. Trademarks. This License does not grant permission to use the trade
|
||||
names, trademarks, service marks, or product names of the Licensor,
|
||||
except as required for reasonable and customary use in describing the
|
||||
origin of the Work and reproducing the content of the NOTICE file.
|
||||
|
||||
7. Disclaimer of Warranty. Unless required by applicable law or
|
||||
agreed to in writing, Licensor provides the Work (and each
|
||||
Contributor provides its Contributions) on an "AS IS" BASIS,
|
||||
WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or
|
||||
implied, including, without limitation, any warranties or conditions
|
||||
of TITLE, NON-INFRINGEMENT, MERCHANTABILITY, or FITNESS FOR A
|
||||
PARTICULAR PURPOSE. You are solely responsible for determining the
|
||||
appropriateness of using or redistributing the Work and assume any
|
||||
risks associated with Your exercise of permissions under this License.
|
||||
|
||||
8. Limitation of Liability. In no event and under no legal theory,
|
||||
whether in tort (including negligence), contract, or otherwise,
|
||||
unless required by applicable law (such as deliberate and grossly
|
||||
negligent acts) or agreed to in writing, shall any Contributor be
|
||||
liable to You for damages, including any direct, indirect, special,
|
||||
incidental, or consequential damages of any character arising as a
|
||||
result of this License or out of the use or inability to use the
|
||||
Work (including but not limited to damages for loss of goodwill,
|
||||
work stoppage, computer failure or malfunction, or any and all
|
||||
other commercial damages or losses), even if such Contributor
|
||||
has been advised of the possibility of such damages.
|
||||
|
||||
9. Accepting Warranty or Additional Liability. While redistributing
|
||||
the Work or Derivative Works thereof, You may choose to offer,
|
||||
and charge a fee for, acceptance of support, warranty, indemnity,
|
||||
or other liability obligations and/or rights consistent with this
|
||||
License. However, in accepting such obligations, You may act only
|
||||
on Your own behalf and on Your sole responsibility, not on behalf
|
||||
of any other Contributor, and only if You agree to indemnify,
|
||||
defend, and hold each Contributor harmless for any liability
|
||||
incurred by, or claims asserted against, such Contributor by reason
|
||||
of your accepting any such warranty or additional liability.
|
||||
|
||||
END OF TERMS AND CONDITIONS
|
||||
|
||||
APPENDIX: How to apply the Apache License to your work.
|
||||
|
||||
To apply the Apache License to your work, attach the following
|
||||
boilerplate notice, with the fields enclosed by brackets "[]"
|
||||
replaced with your own identifying information. (Don't include
|
||||
the brackets!) The text should be enclosed in the appropriate
|
||||
comment syntax for the file format. We also recommend that a
|
||||
file or class name and description of purpose be included on the
|
||||
same "printed page" as the copyright notice for easier
|
||||
identification within third-party archives.
|
||||
|
||||
Copyright TF TECH NV (Belgium)
|
||||
|
||||
Licensed under the Apache License, Version 2.0 (the "License");
|
||||
you may not use this file except in compliance with the License.
|
||||
You may obtain a copy of the License at
|
||||
|
||||
http://www.apache.org/licenses/LICENSE-2.0
|
||||
|
||||
Unless required by applicable law or agreed to in writing, software
|
||||
distributed under the License is distributed on an "AS IS" BASIS,
|
||||
WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
|
||||
See the License for the specific language governing permissions and
|
||||
limitations under the License.
|
||||
30
components/zinit/Makefile
Normal file
30
components/zinit/Makefile
Normal file
@@ -0,0 +1,30 @@
|
||||
default: release
|
||||
|
||||
docker: release
|
||||
docker build -f docker/Dockerfile -t zinit-ubuntu:18.04 target/x86_64-unknown-linux-musl/release
|
||||
|
||||
prepare:
|
||||
rustup target add x86_64-unknown-linux-musl
|
||||
|
||||
release: prepare
|
||||
cargo build --release --target=x86_64-unknown-linux-musl
|
||||
|
||||
release-aarch64-musl: prepare-aarch64-musl
|
||||
cargo build --release --target=aarch64-unknown-linux-musl
|
||||
|
||||
prepare-aarch64-musl:
|
||||
rustup target add aarch64-unknown-linux-musl
|
||||
|
||||
# Build for macOS (both Intel and Apple Silicon)
|
||||
release-macos:
|
||||
cargo build --release
|
||||
|
||||
# Install to ~/hero/bin (if it exists)
|
||||
install-macos: release-macos
|
||||
@if [ -d ~/hero/bin ]; then \
|
||||
cp target/release/zinit ~/hero/bin; \
|
||||
echo "Installed zinit to ~/hero/bin"; \
|
||||
else \
|
||||
echo "~/hero/bin directory not found. Please create it or specify a different installation path."; \
|
||||
exit 1; \
|
||||
fi
|
||||
77
components/zinit/README.md
Normal file
77
components/zinit/README.md
Normal file
@@ -0,0 +1,77 @@
|
||||
# Zinit [](https://github.com/threefoldtech/zinit/actions/workflows/rust.yml)
|
||||
|
||||
Zinit is a lightweight PID 1 replacement inspired by runit, written in Rust using Tokio for async I/O. It provides both a Unix socket interface and an HTTP API for interacting with the process manager.
|
||||
|
||||
### Key Features
|
||||
|
||||
- **Service Management**: Ensures configured services are up and running at all times
|
||||
- **Dependency Handling**: Supports service dependencies for proper startup ordering
|
||||
- **Simple Control Interface**: Provides an intuitive CLI to add, start, stop, and monitor services
|
||||
- **Container Support**: Can run in container mode with appropriate signal handling
|
||||
- **Configurable Logging**: Multiple logging options including ringbuffer and stdout
|
||||
|
||||
## Installation
|
||||
|
||||
```bash
|
||||
curl https://raw.githubusercontent.com/threefoldtech/zinit/refs/heads/master/install.sh | bash
|
||||
|
||||
#to install & run
|
||||
curl https://raw.githubusercontent.com/threefoldtech/zinit/refs/heads/master/install_run.sh | bash
|
||||
```
|
||||
|
||||
Click [here](docs/installation.md) for more information on how to install Zinit.
|
||||
|
||||
## Usage
|
||||
|
||||
### Process Manager (zinit)
|
||||
|
||||
```bash
|
||||
# Run zinit in init mode
|
||||
zinit init --config /etc/zinit/ --socket /var/run/zinit.sock
|
||||
|
||||
# List services
|
||||
zinit list
|
||||
|
||||
# Start a service
|
||||
zinit start <service-name>
|
||||
|
||||
# Stop a service
|
||||
zinit stop <service-name>
|
||||
```
|
||||
|
||||
```bash
|
||||
# Start the HTTP proxy on the default port (8080)
|
||||
zinit proxy
|
||||
```
|
||||
|
||||
More information about all the available commands can be found [here](docs/cmd.md).
|
||||
|
||||
### Service Configuration
|
||||
|
||||
Zinit uses YAML files for service configuration. Here's a basic example:
|
||||
|
||||
```yaml
|
||||
# Service configuration (e.g., /etc/zinit/myservice.yaml)
|
||||
exec: "/usr/bin/myservice --option value" # Command to run (required)
|
||||
test: "/usr/bin/check-myservice" # Health check command (optional)
|
||||
oneshot: false # Whether to restart on exit (default: false)
|
||||
after: # Services that must be running first (optional)
|
||||
- dependency1
|
||||
- dependency2
|
||||
```
|
||||
|
||||
For more information on how to configure service files, see the [service file reference](docs/services.md) documentation.
|
||||
|
||||
### JSON-RPC API
|
||||
|
||||
The HTTP proxy provides a JSON-RPC 2.0 API for interacting with Zinit. You can send JSON-RPC requests to the HTTP endpoint you provided to the proxy:
|
||||
|
||||
```bash
|
||||
curl -X POST -H "Content-Type: application/json" -d '{"jsonrpc":"2.0","id":1,"method":"service_list","params":{}}' http://localhost:8080/
|
||||
```
|
||||
|
||||
See the [OpenRPC specs](openrpc.json) for more information about available RPC calls to interact with Zinit.
|
||||
|
||||
## License
|
||||
|
||||
See [LICENSE](LICENSE) file for details.
|
||||
6
components/zinit/docker/Dockerfile
Normal file
6
components/zinit/docker/Dockerfile
Normal file
@@ -0,0 +1,6 @@
|
||||
FROM ubuntu:18.04
|
||||
|
||||
RUN mkdir -p /etc/zinit
|
||||
ADD zinit /sbin/zinit
|
||||
|
||||
ENTRYPOINT ["/sbin/zinit", "init"]
|
||||
256
components/zinit/docs/cmd.md
Normal file
256
components/zinit/docs/cmd.md
Normal file
@@ -0,0 +1,256 @@
|
||||
# Zinit Command Line Reference
|
||||
|
||||
This document provides a comprehensive reference for all Zinit command line options and commands.
|
||||
|
||||
## Command Structure
|
||||
|
||||
Zinit uses a command-based CLI with the following general structure:
|
||||
|
||||
```bash
|
||||
zinit [FLAGS] [OPTIONS] [SUBCOMMAND]
|
||||
```
|
||||
|
||||
## Global Flags and Options
|
||||
|
||||
These flags and options apply to all Zinit commands:
|
||||
|
||||
| Flag/Option | Description |
|
||||
|-------------|-------------|
|
||||
| `-d, --debug` | Run in debug mode with increased verbosity |
|
||||
| `-h, --help` | Display help information |
|
||||
| `-V, --version` | Display version information |
|
||||
| `-s, --socket <PATH>` | Path to Unix socket (default: `/var/run/zinit.sock`) |
|
||||
|
||||
## Subcommands
|
||||
|
||||
### Main Mode
|
||||
|
||||
#### `init`
|
||||
|
||||
Run Zinit in init mode, starting and maintaining configured services.
|
||||
|
||||
```bash
|
||||
zinit init [FLAGS] [OPTIONS]
|
||||
```
|
||||
|
||||
**Flags:**
|
||||
- `--container`: Run in container mode, exiting on signal instead of rebooting
|
||||
|
||||
**Options:**
|
||||
- `-c, --config <DIR>`: Service configurations directory (default: `/etc/zinit/`)
|
||||
- `-b, --buffer <SIZE>`: Buffer size (in lines) to keep service logs (default: `2000`)
|
||||
|
||||
**Example:**
|
||||
```bash
|
||||
# Run in init mode with custom config directory
|
||||
zinit init -c /opt/services/
|
||||
|
||||
# Run in container mode
|
||||
zinit init --container
|
||||
```
|
||||
|
||||
### Service Management
|
||||
|
||||
#### `list`
|
||||
|
||||
Display a quick view of all currently known services and their status.
|
||||
|
||||
```bash
|
||||
zinit list
|
||||
```
|
||||
|
||||
**Output:**
|
||||
A JSON object with service names as keys and their status as values.
|
||||
|
||||
**Example:**
|
||||
```bash
|
||||
# List all services
|
||||
zinit list
|
||||
```
|
||||
|
||||
#### `status`
|
||||
|
||||
Show detailed status information for a specific service.
|
||||
|
||||
```bash
|
||||
zinit status <SERVICE>
|
||||
```
|
||||
|
||||
**Arguments:**
|
||||
- `<SERVICE>`: Name of the service to show status for
|
||||
|
||||
**Example:**
|
||||
```bash
|
||||
# Check status of redis service
|
||||
zinit status redis
|
||||
```
|
||||
|
||||
#### `start`
|
||||
|
||||
Start a service. Has no effect if the service is already running.
|
||||
|
||||
```bash
|
||||
zinit start <SERVICE>
|
||||
```
|
||||
|
||||
**Arguments:**
|
||||
- `<SERVICE>`: Name of the service to start
|
||||
|
||||
**Example:**
|
||||
```bash
|
||||
# Start the nginx service
|
||||
zinit start nginx
|
||||
```
|
||||
|
||||
#### `stop`
|
||||
|
||||
Stop a service. Sets the target state to "down" and sends the stop signal.
|
||||
|
||||
```bash
|
||||
zinit stop <SERVICE>
|
||||
```
|
||||
|
||||
**Arguments:**
|
||||
- `<SERVICE>`: Name of the service to stop
|
||||
|
||||
**Example:**
|
||||
```bash
|
||||
# Stop the redis service
|
||||
zinit stop redis
|
||||
```
|
||||
|
||||
#### `restart`
|
||||
|
||||
Restart a service. If it fails to stop, it will be killed and then started again.
|
||||
|
||||
```bash
|
||||
zinit restart <SERVICE>
|
||||
```
|
||||
|
||||
**Arguments:**
|
||||
- `<SERVICE>`: Name of the service to restart
|
||||
|
||||
**Example:**
|
||||
```bash
|
||||
# Restart the web service
|
||||
zinit restart web
|
||||
```
|
||||
|
||||
#### `monitor`
|
||||
|
||||
Start monitoring a service. The configuration is loaded from the server's config directory.
|
||||
|
||||
```bash
|
||||
zinit monitor <SERVICE>
|
||||
```
|
||||
|
||||
**Arguments:**
|
||||
- `<SERVICE>`: Name of the service to monitor
|
||||
|
||||
**Example:**
|
||||
```bash
|
||||
# Monitor the database service
|
||||
zinit monitor database
|
||||
```
|
||||
|
||||
#### `forget`
|
||||
|
||||
Remove a service from monitoring. You can only forget a stopped service.
|
||||
|
||||
```bash
|
||||
zinit forget <SERVICE>
|
||||
```
|
||||
|
||||
**Arguments:**
|
||||
- `<SERVICE>`: Name of the service to forget
|
||||
|
||||
**Example:**
|
||||
```bash
|
||||
# Forget the backup service
|
||||
zinit forget backup
|
||||
```
|
||||
|
||||
#### `kill`
|
||||
|
||||
Send a signal to a running service.
|
||||
|
||||
```bash
|
||||
zinit kill <SERVICE> <SIGNAL>
|
||||
```
|
||||
|
||||
**Arguments:**
|
||||
- `<SERVICE>`: Name of the service to send signal to
|
||||
- `<SIGNAL>`: Signal name (e.g., SIGTERM, SIGKILL, SIGINT)
|
||||
|
||||
**Example:**
|
||||
```bash
|
||||
# Send SIGTERM to the redis service
|
||||
zinit kill redis SIGTERM
|
||||
|
||||
# Send SIGKILL to force terminate a service
|
||||
zinit kill stuck-service SIGKILL
|
||||
```
|
||||
|
||||
### System Operations
|
||||
|
||||
#### `shutdown`
|
||||
|
||||
Stop all services in dependency order and power off the system.
|
||||
|
||||
```bash
|
||||
zinit shutdown
|
||||
```
|
||||
|
||||
**Example:**
|
||||
```bash
|
||||
# Shutdown the system
|
||||
zinit shutdown
|
||||
```
|
||||
|
||||
#### `reboot`
|
||||
|
||||
Stop all services in dependency order and reboot the system.
|
||||
|
||||
```bash
|
||||
zinit reboot
|
||||
```
|
||||
|
||||
**Example:**
|
||||
```bash
|
||||
# Reboot the system
|
||||
zinit reboot
|
||||
```
|
||||
|
||||
### Logging
|
||||
|
||||
#### `log`
|
||||
|
||||
View service logs from the Zinit ring buffer.
|
||||
|
||||
```bash
|
||||
zinit log [FLAGS] [FILTER]
|
||||
```
|
||||
|
||||
**Flags:**
|
||||
- `-s, --snapshot`: If set, log prints current buffer without following
|
||||
|
||||
**Arguments:**
|
||||
- `[FILTER]`: Optional service name to filter logs for
|
||||
|
||||
**Examples:**
|
||||
```bash
|
||||
# View logs for all services and follow new logs
|
||||
zinit log
|
||||
|
||||
# View current logs for the nginx service without following
|
||||
zinit log -s nginx
|
||||
```
|
||||
|
||||
## Exit Codes
|
||||
|
||||
Zinit commands return the following exit codes:
|
||||
|
||||
| Code | Description |
|
||||
|------|-------------|
|
||||
| 0 | Success |
|
||||
| 1 | Error (with error message printed to stderr) |
|
||||
197
components/zinit/docs/installation.md
Normal file
197
components/zinit/docs/installation.md
Normal file
@@ -0,0 +1,197 @@
|
||||
# Installing Zinit
|
||||
|
||||
This guide provides detailed instructions for installing Zinit on various platforms.
|
||||
|
||||
## System Requirements
|
||||
|
||||
Zinit has minimal system requirements:
|
||||
|
||||
- Linux-based operating system
|
||||
- Root access (for running as init system)
|
||||
|
||||
## Pre-built Binaries
|
||||
|
||||
If pre-built binaries are available for your system, you can install them directly:
|
||||
|
||||
```bash
|
||||
# Download the binary (replace with actual URL)
|
||||
wget https://github.com/threefoldtech/zinit/releases/download/vX.Y.Z/zinit-x86_64-unknown-linux-musl
|
||||
|
||||
# Make it executable
|
||||
chmod +x zinit-x86_64-unknown-linux-musl
|
||||
|
||||
# Move to a location in your PATH
|
||||
sudo mv zinit-x86_64-unknown-linux-musl /usr/local/bin/zinit
|
||||
```
|
||||
|
||||
## Building from Source
|
||||
|
||||
### Prerequisites
|
||||
|
||||
To build Zinit from source, you'll need:
|
||||
|
||||
- Rust toolchain (1.46.0 or later recommended)
|
||||
- musl and musl-tools packages
|
||||
- GNU Make
|
||||
|
||||
#### Install Rust
|
||||
|
||||
If you don't have Rust installed, use rustup:
|
||||
|
||||
```bash
|
||||
curl --proto '=https' --tlsv1.2 -sSf https://sh.rustup.rs | sh
|
||||
source $HOME/.cargo/env
|
||||
```
|
||||
|
||||
#### Install musl development tools
|
||||
|
||||
On Debian/Ubuntu:
|
||||
|
||||
```bash
|
||||
sudo apt update
|
||||
sudo apt install musl musl-tools
|
||||
```
|
||||
|
||||
On Fedora:
|
||||
|
||||
```bash
|
||||
sudo dnf install musl musl-devel
|
||||
```
|
||||
|
||||
On Alpine Linux (musl is already the default libc):
|
||||
|
||||
```bash
|
||||
apk add build-base
|
||||
```
|
||||
|
||||
### Build Process
|
||||
|
||||
1. Clone the repository:
|
||||
|
||||
```bash
|
||||
git clone https://github.com/threefoldtech/zinit.git
|
||||
cd zinit
|
||||
```
|
||||
|
||||
2. Build using make:
|
||||
|
||||
```bash
|
||||
make
|
||||
```
|
||||
|
||||
This will create a statically linked binary at `target/x86_64-unknown-linux-musl/release/zinit`.
|
||||
|
||||
3. Install the binary:
|
||||
|
||||
```bash
|
||||
sudo cp target/x86_64-unknown-linux-musl/release/zinit /usr/local/bin/
|
||||
```
|
||||
|
||||
### Development Build
|
||||
|
||||
For development or debugging:
|
||||
|
||||
```bash
|
||||
make dev
|
||||
```
|
||||
|
||||
## Docker Installation
|
||||
|
||||
### Using the Provided Dockerfile
|
||||
|
||||
Zinit includes a test Docker image:
|
||||
|
||||
```bash
|
||||
# Build the Docker image
|
||||
make docker
|
||||
|
||||
# Run the container
|
||||
docker run -dt --device=/dev/kmsg:/dev/kmsg:rw zinit
|
||||
```
|
||||
> Don't forget to port-forward a port to get access to the Zinit proxy using the `-p XXXX:YYYY` flag when running the container.
|
||||
|
||||
### Custom Docker Setup
|
||||
|
||||
To create your own Dockerfile with Zinit:
|
||||
|
||||
```dockerfile
|
||||
FROM alpine:latest
|
||||
|
||||
# Install dependencies if needed
|
||||
RUN apk add --no-cache bash curl
|
||||
|
||||
# Copy the zinit binary
|
||||
COPY zinit /usr/local/bin/zinit
|
||||
RUN chmod +x /usr/local/bin/zinit
|
||||
|
||||
# Create configuration directory
|
||||
RUN mkdir -p /etc/zinit
|
||||
|
||||
# Add your service configurations
|
||||
COPY services/*.yaml /etc/zinit/
|
||||
|
||||
# Set zinit as the entrypoint
|
||||
ENTRYPOINT ["/usr/local/bin/zinit", "init", "--container"]
|
||||
```
|
||||
|
||||
## Using Zinit as the Init System
|
||||
|
||||
To use Zinit as the init system (PID 1) on a Linux system:
|
||||
|
||||
### On a Standard Linux System
|
||||
|
||||
1. Install Zinit as described above
|
||||
2. Create your service configurations in `/etc/zinit/`
|
||||
3. Configure your bootloader to use zinit as init
|
||||
|
||||
For GRUB, add `init=/usr/local/bin/zinit` to the kernel command line:
|
||||
|
||||
```bash
|
||||
# Edit GRUB configuration
|
||||
sudo nano /etc/default/grub
|
||||
|
||||
# Add init parameter to GRUB_CMDLINE_LINUX
|
||||
# Example:
|
||||
# GRUB_CMDLINE_LINUX="init=/usr/local/bin/zinit"
|
||||
|
||||
# Update GRUB
|
||||
sudo update-grub
|
||||
```
|
||||
|
||||
### In a Container Environment
|
||||
|
||||
For containers, simply set Zinit as the entrypoint:
|
||||
|
||||
```bash
|
||||
docker run -dt --device=/dev/kmsg:/dev/kmsg:rw \
|
||||
--entrypoint /usr/local/bin/zinit \
|
||||
your-image init --container
|
||||
```
|
||||
|
||||
## First-time Setup
|
||||
|
||||
After installation, you'll need to create a basic configuration:
|
||||
|
||||
1. Create the configuration directory:
|
||||
|
||||
```bash
|
||||
sudo mkdir -p /etc/zinit
|
||||
```
|
||||
|
||||
2. Create a simple service configuration:
|
||||
|
||||
```bash
|
||||
cat << EOF | sudo tee /etc/zinit/hello.yaml
|
||||
exec: "echo 'Hello from Zinit!'"
|
||||
oneshot: true
|
||||
EOF
|
||||
```
|
||||
|
||||
3. Test Zinit without running as init:
|
||||
|
||||
```bash
|
||||
# For testing only - doesn't replace system init
|
||||
sudo zinit init
|
||||
```
|
||||
|
||||
If all is working correctly, you should see Zinit start and run your service.
|
||||
78
components/zinit/docs/osx_cross_compile.md
Normal file
78
components/zinit/docs/osx_cross_compile.md
Normal file
@@ -0,0 +1,78 @@
|
||||
# macOS Guide for Zinit
|
||||
|
||||
This guide covers both building Zinit natively on macOS and cross-compiling from macOS to Linux targets.
|
||||
|
||||
## Building Zinit Natively on macOS
|
||||
|
||||
Zinit can now be built and run directly on macOS. The code has been updated to handle platform-specific differences between Linux and macOS.
|
||||
|
||||
### Building for macOS
|
||||
|
||||
```bash
|
||||
# Build a release version for macOS
|
||||
make release-macos
|
||||
|
||||
# Install to ~/hero/bin (if it exists)
|
||||
make install-macos
|
||||
```
|
||||
|
||||
The native macOS build provides most of Zinit's functionality, with the following limitations:
|
||||
- System reboot and shutdown operations are not supported (they will exit the process instead)
|
||||
- Some Linux-specific features are disabled
|
||||
|
||||
## Cross-Compilation from macOS to Linux
|
||||
|
||||
This section outlines the steps to set up your macOS environment for cross-compiling Rust projects to the `aarch64-unknown-linux-musl` target. This is particularly useful for building binaries that can run on ARM-based Linux systems (e.g., Raspberry Pi, AWS Graviton) using musl libc.
|
||||
|
||||
## Prerequisites
|
||||
|
||||
* Homebrew (https://brew.sh/) installed on your macOS system.
|
||||
* Rust and Cargo installed (e.g., via `rustup`).
|
||||
|
||||
## Step 1: Install the `aarch64-linux-musl-gcc` Toolchain
|
||||
|
||||
The `aarch64-linux-musl-gcc` toolchain is required for linking when cross-compiling to `aarch64-unknown-linux-musl`. You can install it using Homebrew:
|
||||
|
||||
```bash
|
||||
brew install messense/macos-cross-toolchains/aarch64-linux-musl-cross
|
||||
```
|
||||
|
||||
## Step 2: Link `musl-gcc`
|
||||
|
||||
Some build scripts or tools might look for `musl-gcc`. To ensure compatibility, create a symbolic link:
|
||||
|
||||
```bash
|
||||
sudo ln -s /opt/homebrew/bin/aarch64-linux-musl-gcc /opt/homebrew/bin/musl-gcc
|
||||
```
|
||||
|
||||
You might be prompted for your system password to complete this operation.
|
||||
|
||||
## Step 3: Add the Rust Target
|
||||
|
||||
Add the `aarch64-unknown-linux-musl` target to your Rust toolchain:
|
||||
|
||||
```bash
|
||||
rustup target add aarch64-unknown-linux-musl
|
||||
```
|
||||
|
||||
## Step 4: Build Your Project
|
||||
|
||||
Now you can build your Rust project for the `aarch64-unknown-linux-musl` target using Cargo:
|
||||
|
||||
```bash
|
||||
cargo build --release --target aarch64-unknown-linux-musl
|
||||
```
|
||||
|
||||
Alternatively, if you are using the provided `Makefile`, you can use the new target:
|
||||
|
||||
```bash
|
||||
make release-aarch64-musl
|
||||
```
|
||||
|
||||
This will produce a release binary located in `target/aarch64-unknown-linux-musl/release/`.
|
||||
|
||||
## Step 5: copy to osx hero bin
|
||||
|
||||
```bash
|
||||
cp target/aarch64-unknown-linux-musl/release/zinit ~/hero/bin
|
||||
```
|
||||
217
components/zinit/docs/services.md
Normal file
217
components/zinit/docs/services.md
Normal file
@@ -0,0 +1,217 @@
|
||||
# Service Configuration Format
|
||||
|
||||
This document describes the structure and options for Zinit service configuration files.
|
||||
|
||||
## File Format
|
||||
|
||||
Zinit uses YAML files for service configuration. Each service has its own configuration file stored in the Zinit configuration directory (default: `/etc/zinit`).
|
||||
|
||||
### File Naming and Location
|
||||
|
||||
- **Location**: `/etc/zinit/` (default, can be changed with `-c` flag)
|
||||
- on osx `~/hero/cfg/zinit`
|
||||
- **Naming**: `<service-name>.yaml`
|
||||
|
||||
For example:
|
||||
- `/etc/zinit/nginx.yaml`
|
||||
- `/etc/zinit/redis.yaml`
|
||||
|
||||
## Configuration Schema
|
||||
|
||||
Service configuration files use the following schema:
|
||||
|
||||
```yaml
|
||||
# Command to run (required)
|
||||
exec: "command line to start service"
|
||||
|
||||
# Command to test if service is running (optional)
|
||||
test: "command line to test service"
|
||||
|
||||
# Whether the service should be restarted (optional, default: false)
|
||||
oneshot: true|false
|
||||
|
||||
# Maximum time to wait for service to stop during shutdown (optional, default: 10)
|
||||
shutdown_timeout: 30
|
||||
|
||||
# Services that must be running before this one starts (optional)
|
||||
after:
|
||||
- service1_name
|
||||
- service2_name
|
||||
|
||||
# Signals configuration (optional)
|
||||
signal:
|
||||
stop: SIGKILL # signal sent on 'stop' action (default: SIGTERM)
|
||||
|
||||
# Log handling configuration (optional, default: ring)
|
||||
log: null|ring|stdout
|
||||
|
||||
# Environment variables for the service (optional)
|
||||
env:
|
||||
KEY1: "VALUE1"
|
||||
KEY2: "VALUE2"
|
||||
|
||||
# Working directory for the service (optional)
|
||||
dir: "/path/to/working/directory"
|
||||
```
|
||||
|
||||
## Configuration Options
|
||||
|
||||
### Required Fields
|
||||
|
||||
| Field | Description |
|
||||
|-------|-------------|
|
||||
| `exec` | Command line to execute when starting the service |
|
||||
|
||||
### Optional Fields
|
||||
|
||||
| Field | Type | Default | Description |
|
||||
|-------|------|---------|-------------|
|
||||
| `test` | String | - | Command to determine if service is running |
|
||||
| `oneshot` | Boolean | `false` | If true, service won't be restarted after exit |
|
||||
| `shutdown_timeout` | Integer | 10 | Seconds to wait for service to stop during shutdown |
|
||||
| `after` | String[] | `[]` | List of services that must be running first |
|
||||
| `signal.stop` | String | `"sigterm"` | Signal to send when stopping the service |
|
||||
| `log` | Enum | `ring` | How to handle service output (null, ring, stdout) |
|
||||
| `env` | Object | `{}` | Environment variables to pass to the service |
|
||||
| `dir` | String | `""` | Working directory for the service |
|
||||
|
||||
## Field Details
|
||||
|
||||
### exec
|
||||
|
||||
The command to run when starting the service. This is the only required field in the configuration.
|
||||
|
||||
```yaml
|
||||
exec: "/usr/bin/redis-server --port 6379"
|
||||
```
|
||||
|
||||
Shell-style commands are supported:
|
||||
|
||||
```yaml
|
||||
exec: "sh -c 'echo Starting service && /usr/local/bin/myservice'"
|
||||
```
|
||||
|
||||
### test
|
||||
|
||||
Command that tests whether the service is running properly. Zinit runs this command periodically until it succeeds (exit code 0), at which point the service is considered running.
|
||||
|
||||
```yaml
|
||||
test: "redis-cli -p 6379 PING"
|
||||
```
|
||||
|
||||
If no test command is provided, the service is considered running as soon as it's started.
|
||||
|
||||
### oneshot
|
||||
|
||||
When set to `true`, the service will not be automatically restarted when it exits. This is useful for initialization tasks or commands that should run only once.
|
||||
|
||||
```yaml
|
||||
oneshot: true
|
||||
```
|
||||
|
||||
Services that depend on a oneshot service will start only after the oneshot service has exited successfully.
|
||||
|
||||
### shutdown_timeout
|
||||
|
||||
How long (in seconds) to wait for the service to stop during system shutdown before giving up:
|
||||
|
||||
```yaml
|
||||
shutdown_timeout: 30 # Wait up to 30 seconds
|
||||
```
|
||||
|
||||
### after
|
||||
|
||||
List of service names that must be running (or completed successfully for oneshot services) before this service starts:
|
||||
|
||||
```yaml
|
||||
after:
|
||||
- networking
|
||||
- database
|
||||
```
|
||||
|
||||
### signal
|
||||
|
||||
Custom signals to use for operations. Currently, only the `stop` signal is configurable:
|
||||
|
||||
```yaml
|
||||
signal:
|
||||
stop: SIGKILL # Use SIGKILL instead of default SIGTERM
|
||||
```
|
||||
|
||||
Valid signal names follow the standard UNIX signal naming (SIGTERM, SIGKILL, SIGINT, etc).
|
||||
|
||||
### log
|
||||
|
||||
How to handle stdout/stderr output from the service:
|
||||
|
||||
```yaml
|
||||
log: stdout # Print output to zinit's stdout
|
||||
```
|
||||
|
||||
Options:
|
||||
- `null`: Ignore all service output (like redirecting to /dev/null)
|
||||
- `ring`: Store logs in the kernel ring buffer with service name prefix (default)
|
||||
- `stdout`: Send service output to zinit's stdout
|
||||
|
||||
> **Note**: To use `ring` inside Docker, make sure to add the `kmsg` device:
|
||||
> ```
|
||||
> docker run -dt --device=/dev/kmsg:/dev/kmsg:rw zinit
|
||||
> ```
|
||||
|
||||
### env
|
||||
|
||||
Additional environment variables for the service. These are added to the existing environment:
|
||||
|
||||
```yaml
|
||||
env:
|
||||
PORT: "8080"
|
||||
DEBUG: "true"
|
||||
NODE_ENV: "production"
|
||||
```
|
||||
|
||||
### dir
|
||||
|
||||
Working directory for the service process:
|
||||
|
||||
```yaml
|
||||
dir: "/var/lib/myservice"
|
||||
```
|
||||
|
||||
If not specified, the process inherits zinit's working directory.
|
||||
|
||||
## Example Configurations
|
||||
|
||||
### Web Server
|
||||
|
||||
```yaml
|
||||
exec: "/usr/bin/nginx -g 'daemon off;'"
|
||||
test: "curl -s http://localhost > /dev/null"
|
||||
after:
|
||||
- networking
|
||||
log: stdout
|
||||
```
|
||||
|
||||
### Database Initialization
|
||||
|
||||
```yaml
|
||||
exec: "sh -c 'echo Creating database schema && /usr/bin/db-migrate'"
|
||||
oneshot: true
|
||||
dir: "/opt/myapp"
|
||||
env:
|
||||
DB_HOST: "localhost"
|
||||
DB_USER: "admin"
|
||||
```
|
||||
|
||||
### Application with Dependencies
|
||||
|
||||
```yaml
|
||||
exec: "/usr/bin/myapp --config /etc/myapp.conf"
|
||||
test: "curl -s http://localhost:8080/health > /dev/null"
|
||||
after:
|
||||
- database
|
||||
- cache
|
||||
signal:
|
||||
stop: SIGINT # Use SIGINT for graceful shutdown
|
||||
env:
|
||||
PORT: "8080"
|
||||
shutdown_timeout: 20
|
||||
366
components/zinit/docs/shutdown_improvement_plan.md
Normal file
366
components/zinit/docs/shutdown_improvement_plan.md
Normal file
@@ -0,0 +1,366 @@
|
||||
# Zinit Shutdown Functionality Improvement Plan
|
||||
|
||||
## Current Issues
|
||||
|
||||
1. **Incomplete Child Process Termination**: When services are stopped, child processes may remain running.
|
||||
2. **Lack of Verification**: There's no verification that all processes are actually terminated.
|
||||
3. **Improper Graceful Shutdown**: Zinit doesn't wait for all processes to terminate before exiting.
|
||||
|
||||
## Solution Overview
|
||||
|
||||
We'll implement a robust shutdown mechanism that:
|
||||
1. Uses our stats functionality to detect all child processes
|
||||
2. Properly manages process groups
|
||||
3. Verifies all processes are terminated before Zinit exits
|
||||
|
||||
## Implementation Plan
|
||||
|
||||
```mermaid
|
||||
flowchart TD
|
||||
A[Enhance stop method] --> B[Improve kill_process_tree]
|
||||
B --> C[Add process verification]
|
||||
C --> D[Implement graceful shutdown]
|
||||
|
||||
A1[Use stats to detect child processes] --> A
|
||||
A2[Send signals to all processes] --> A
|
||||
A3[Implement cascading termination] --> A
|
||||
|
||||
B1[Ensure proper process group handling] --> B
|
||||
B2[Add timeout and escalation logic] --> B
|
||||
|
||||
C1[Create verification mechanism] --> C
|
||||
C2[Add polling for process existence] --> C
|
||||
|
||||
D1[Wait for all processes to terminate] --> D
|
||||
D2[Add cleanup of resources] --> D
|
||||
D3[Implement clean exit] --> D
|
||||
```
|
||||
|
||||
## Detailed Implementation Steps
|
||||
|
||||
### 1. Enhance the `stop` Method in `LifecycleManager`
|
||||
|
||||
```rust
|
||||
pub async fn stop<S: AsRef<str>>(&self, name: S) -> Result<()> {
|
||||
// Get service information
|
||||
let table = self.services.read().await;
|
||||
let service = table.get(name.as_ref())
|
||||
.ok_or_else(|| ZInitError::unknown_service(name.as_ref()))?;
|
||||
|
||||
let mut service = service.write().await;
|
||||
service.set_target(Target::Down);
|
||||
|
||||
// Get the main process PID
|
||||
let pid = service.pid;
|
||||
if pid.as_raw() == 0 {
|
||||
return Ok(());
|
||||
}
|
||||
|
||||
// Get the signal to use
|
||||
let signal = signal::Signal::from_str(&service.service.signal.stop.to_uppercase())
|
||||
.map_err(|err| anyhow::anyhow!("unknown stop signal: {}", err))?;
|
||||
|
||||
// Release the lock before potentially long-running operations
|
||||
drop(service);
|
||||
drop(table);
|
||||
|
||||
// Get all child processes using our stats functionality
|
||||
let children = self.get_child_process_stats(pid.as_raw()).await?;
|
||||
|
||||
// First try to stop the process group
|
||||
let _ = self.pm.signal(pid, signal);
|
||||
|
||||
// Wait a short time for processes to terminate gracefully
|
||||
sleep(std::time::Duration::from_millis(500)).await;
|
||||
|
||||
// Check if processes are still running and use SIGKILL if needed
|
||||
self.ensure_processes_terminated(pid.as_raw(), &children).await?;
|
||||
|
||||
Ok(())
|
||||
}
|
||||
```
|
||||
|
||||
### 2. Add a New `ensure_processes_terminated` Method
|
||||
|
||||
```rust
|
||||
async fn ensure_processes_terminated(&self, parent_pid: i32, children: &[ProcessStats]) -> Result<()> {
|
||||
// Check if parent is still running
|
||||
let parent_running = self.is_process_running(parent_pid).await?;
|
||||
|
||||
// If parent is still running, send SIGKILL
|
||||
if parent_running {
|
||||
debug!("Process {} still running after SIGTERM, sending SIGKILL", parent_pid);
|
||||
let _ = self.pm.signal(Pid::from_raw(parent_pid), signal::Signal::SIGKILL);
|
||||
}
|
||||
|
||||
// Check and kill any remaining child processes
|
||||
for child in children {
|
||||
if self.is_process_running(child.pid).await? {
|
||||
debug!("Child process {} still running, sending SIGKILL", child.pid);
|
||||
let _ = signal::kill(Pid::from_raw(child.pid), signal::Signal::SIGKILL);
|
||||
}
|
||||
}
|
||||
|
||||
// Verify all processes are gone
|
||||
let mut retries = 5;
|
||||
while retries > 0 {
|
||||
let mut all_terminated = true;
|
||||
|
||||
// Check parent
|
||||
if self.is_process_running(parent_pid).await? {
|
||||
all_terminated = false;
|
||||
}
|
||||
|
||||
// Check children
|
||||
for child in children {
|
||||
if self.is_process_running(child.pid).await? {
|
||||
all_terminated = false;
|
||||
break;
|
||||
}
|
||||
}
|
||||
|
||||
if all_terminated {
|
||||
return Ok(());
|
||||
}
|
||||
|
||||
// Wait before retrying
|
||||
sleep(std::time::Duration::from_millis(100)).await;
|
||||
retries -= 1;
|
||||
}
|
||||
|
||||
// If we get here, some processes might still be running
|
||||
warn!("Some processes may still be running after shutdown attempts");
|
||||
Ok(())
|
||||
}
|
||||
```
|
||||
|
||||
### 3. Add a Helper Method to Check if a Process is Running
|
||||
|
||||
```rust
|
||||
async fn is_process_running(&self, pid: i32) -> Result<bool> {
|
||||
// Use sysinfo to check if process exists
|
||||
let mut system = System::new();
|
||||
let sys_pid = sysinfo::Pid::from(pid as usize);
|
||||
system.refresh_process(sys_pid);
|
||||
|
||||
Ok(system.process(sys_pid).is_some())
|
||||
}
|
||||
```
|
||||
|
||||
### 4. Improve the `kill_process_tree` Method
|
||||
|
||||
```rust
|
||||
#[cfg(target_os = "linux")]
|
||||
async fn kill_process_tree(
|
||||
&self,
|
||||
mut dag: ProcessDAG,
|
||||
mut state_channels: HashMap<String, Watcher<State>>,
|
||||
mut shutdown_timeouts: HashMap<String, u64>,
|
||||
) -> Result<()> {
|
||||
let (tx, mut rx) = mpsc::unbounded_channel();
|
||||
tx.send(DUMMY_ROOT.into())?;
|
||||
|
||||
let mut count = dag.count;
|
||||
while let Some(name) = rx.recv().await {
|
||||
debug!("{} has been killed (or was inactive) adding its children", name);
|
||||
|
||||
for child in dag.adj.get(&name).unwrap_or(&Vec::new()) {
|
||||
let child_indegree: &mut u32 = dag.indegree.entry(child.clone()).or_default();
|
||||
*child_indegree -= 1;
|
||||
|
||||
debug!("decrementing child {} indegree to {}", child, child_indegree);
|
||||
|
||||
if *child_indegree == 0 {
|
||||
let watcher = state_channels.remove(child);
|
||||
if watcher.is_none() {
|
||||
// not an active service
|
||||
tx.send(child.to_string())?;
|
||||
continue;
|
||||
}
|
||||
|
||||
let shutdown_timeout = shutdown_timeouts.remove(child);
|
||||
let lifecycle = self.clone_lifecycle();
|
||||
|
||||
// Spawn a task to kill the service and wait for it to terminate
|
||||
let kill_task = tokio::spawn(Self::kill_wait_enhanced(
|
||||
lifecycle,
|
||||
child.to_string(),
|
||||
tx.clone(),
|
||||
watcher.unwrap(),
|
||||
shutdown_timeout.unwrap_or(config::DEFAULT_SHUTDOWN_TIMEOUT),
|
||||
));
|
||||
|
||||
// Add a timeout to ensure we don't wait forever
|
||||
let _ = tokio::time::timeout(
|
||||
std::time::Duration::from_secs(shutdown_timeout.unwrap_or(config::DEFAULT_SHUTDOWN_TIMEOUT) + 2),
|
||||
kill_task
|
||||
).await;
|
||||
}
|
||||
}
|
||||
|
||||
count -= 1;
|
||||
if count == 0 {
|
||||
break;
|
||||
}
|
||||
}
|
||||
|
||||
// Final verification that all processes are gone
|
||||
self.verify_all_processes_terminated().await?;
|
||||
|
||||
Ok(())
|
||||
}
|
||||
```
|
||||
|
||||
### 5. Add an Enhanced `kill_wait` Method
|
||||
|
||||
```rust
|
||||
#[cfg(target_os = "linux")]
|
||||
async fn kill_wait_enhanced(
|
||||
self,
|
||||
name: String,
|
||||
ch: mpsc::UnboundedSender<String>,
|
||||
mut rx: Watcher<State>,
|
||||
shutdown_timeout: u64,
|
||||
) {
|
||||
debug!("kill_wait {}", name);
|
||||
|
||||
// Try to stop the service gracefully
|
||||
let stop_result = self.stop(name.clone()).await;
|
||||
|
||||
// Wait for the service to become inactive or timeout
|
||||
let fut = timeout(
|
||||
std::time::Duration::from_secs(shutdown_timeout),
|
||||
async move {
|
||||
while let Some(state) = rx.next().await {
|
||||
if !state.is_active() {
|
||||
return;
|
||||
}
|
||||
}
|
||||
},
|
||||
);
|
||||
|
||||
match stop_result {
|
||||
Ok(_) => {
|
||||
let _ = fut.await;
|
||||
}
|
||||
Err(e) => error!("couldn't stop service {}: {}", name.clone(), e),
|
||||
}
|
||||
|
||||
// Verify the service is actually stopped
|
||||
if let Ok(status) = self.status(&name).await {
|
||||
if status.pid != 0 {
|
||||
// Service is still running, try to kill it
|
||||
let _ = self.kill(&name, signal::Signal::SIGKILL).await;
|
||||
}
|
||||
}
|
||||
|
||||
debug!("sending to the death channel {}", name.clone());
|
||||
if let Err(e) = ch.send(name.clone()) {
|
||||
error!(
|
||||
"error: couldn't send the service {} to the shutdown loop: {}",
|
||||
name, e
|
||||
);
|
||||
}
|
||||
}
|
||||
```
|
||||
|
||||
### 6. Add a Method to Verify All Processes are Terminated
|
||||
|
||||
```rust
|
||||
async fn verify_all_processes_terminated(&self) -> Result<()> {
|
||||
// Get all services
|
||||
let table = self.services.read().await;
|
||||
|
||||
// Check each service
|
||||
for (name, service) in table.iter() {
|
||||
let service = service.read().await;
|
||||
let pid = service.pid.as_raw();
|
||||
|
||||
// Skip services with no PID
|
||||
if pid == 0 {
|
||||
continue;
|
||||
}
|
||||
|
||||
// Check if the main process is still running
|
||||
if self.is_process_running(pid).await? {
|
||||
warn!("Service {} (PID {}) is still running after shutdown", name, pid);
|
||||
|
||||
// Try to kill it with SIGKILL
|
||||
let _ = signal::kill(Pid::from_raw(pid), signal::Signal::SIGKILL);
|
||||
}
|
||||
|
||||
// Check for child processes
|
||||
if let Ok(children) = self.get_child_process_stats(pid).await {
|
||||
for child in children {
|
||||
if self.is_process_running(child.pid).await? {
|
||||
warn!("Child process {} of service {} is still running after shutdown",
|
||||
child.pid, name);
|
||||
|
||||
// Try to kill it with SIGKILL
|
||||
let _ = signal::kill(Pid::from_raw(child.pid), signal::Signal::SIGKILL);
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
Ok(())
|
||||
}
|
||||
```
|
||||
|
||||
### 7. Update the `shutdown` and `reboot` Methods
|
||||
|
||||
```rust
|
||||
pub async fn shutdown(&self) -> Result<()> {
|
||||
info!("shutting down");
|
||||
|
||||
// Set the shutdown flag
|
||||
*self.shutdown.write().await = true;
|
||||
|
||||
#[cfg(target_os = "linux")]
|
||||
{
|
||||
// Power off using our enhanced method
|
||||
let result = self.power(RebootMode::RB_POWER_OFF).await;
|
||||
|
||||
// Final verification before exit
|
||||
self.verify_all_processes_terminated().await?;
|
||||
|
||||
return result;
|
||||
}
|
||||
|
||||
#[cfg(not(target_os = "linux"))]
|
||||
{
|
||||
// Stop all services
|
||||
let services = self.list().await?;
|
||||
for service in services {
|
||||
let _ = self.stop(&service).await;
|
||||
}
|
||||
|
||||
// Verify all processes are terminated
|
||||
self.verify_all_processes_terminated().await?;
|
||||
|
||||
if self.container {
|
||||
std::process::exit(0);
|
||||
} else {
|
||||
info!("System shutdown not supported on this platform");
|
||||
std::process::exit(0);
|
||||
}
|
||||
}
|
||||
}
|
||||
```
|
||||
|
||||
## Testing Plan
|
||||
|
||||
1. **Basic Service Termination**: Test that a simple service is properly terminated
|
||||
2. **Child Process Termination**: Test that a service with child processes has all processes terminated
|
||||
3. **Graceful Shutdown**: Test that Zinit exits cleanly after all services are stopped
|
||||
4. **Edge Cases**:
|
||||
- Test with services that spawn many child processes
|
||||
- Test with services that spawn child processes that change their process group
|
||||
- Test with services that ignore SIGTERM
|
||||
|
||||
## Implementation Timeline
|
||||
|
||||
1. **Phase 1**: Enhance the `stop` method and add the helper methods (1-2 hours)
|
||||
2. **Phase 2**: Improve the `kill_process_tree` and `kill_wait` methods (1-2 hours)
|
||||
3. **Phase 3**: Update the `shutdown` and `reboot` methods (1 hour)
|
||||
4. **Phase 4**: Testing and debugging (2-3 hours)
|
||||
125
components/zinit/docs/stats.md
Normal file
125
components/zinit/docs/stats.md
Normal file
@@ -0,0 +1,125 @@
|
||||
# Service Stats Functionality
|
||||
|
||||
This document describes the stats functionality in Zinit, which provides memory and CPU usage information for services and their child processes.
|
||||
|
||||
## Overview
|
||||
|
||||
The stats functionality allows you to monitor the resource usage of services managed by Zinit. It provides information about:
|
||||
|
||||
- Memory usage (in bytes)
|
||||
- CPU usage (as a percentage)
|
||||
- Child processes and their resource usage
|
||||
|
||||
This is particularly useful for monitoring system resources and identifying services that might be consuming excessive resources.
|
||||
|
||||
## Command Line Usage
|
||||
|
||||
To get stats for a service using the command line:
|
||||
|
||||
```bash
|
||||
zinit stats <service-name>
|
||||
```
|
||||
|
||||
Example:
|
||||
```bash
|
||||
zinit stats nginx
|
||||
```
|
||||
|
||||
This will output YAML-formatted stats information:
|
||||
|
||||
```yaml
|
||||
name: nginx
|
||||
pid: 1234
|
||||
memory_usage: 10485760 # Memory usage in bytes (10MB)
|
||||
cpu_usage: 2.5 # CPU usage as percentage
|
||||
children: # Stats for child processes
|
||||
- pid: 1235
|
||||
memory_usage: 5242880
|
||||
cpu_usage: 1.2
|
||||
- pid: 1236
|
||||
memory_usage: 4194304
|
||||
cpu_usage: 0.8
|
||||
```
|
||||
|
||||
## JSON-RPC API
|
||||
|
||||
The stats functionality is also available through the JSON-RPC API:
|
||||
|
||||
### Method: `service_stats`
|
||||
|
||||
Get memory and CPU usage statistics for a service.
|
||||
|
||||
**Parameters:**
|
||||
- `name` (string, required): The name of the service to get stats for
|
||||
|
||||
**Returns:**
|
||||
- Object containing stats information:
|
||||
- `name` (string): Service name
|
||||
- `pid` (integer): Process ID of the service
|
||||
- `memory_usage` (integer): Memory usage in bytes
|
||||
- `cpu_usage` (number): CPU usage as a percentage (0-100)
|
||||
- `children` (array): Stats for child processes
|
||||
- Each child has:
|
||||
- `pid` (integer): Process ID of the child process
|
||||
- `memory_usage` (integer): Memory usage in bytes
|
||||
- `cpu_usage` (number): CPU usage as a percentage (0-100)
|
||||
|
||||
**Example Request:**
|
||||
```json
|
||||
{
|
||||
"jsonrpc": "2.0",
|
||||
"id": 1,
|
||||
"method": "service_stats",
|
||||
"params": {
|
||||
"name": "nginx"
|
||||
}
|
||||
}
|
||||
```
|
||||
|
||||
**Example Response:**
|
||||
```json
|
||||
{
|
||||
"jsonrpc": "2.0",
|
||||
"id": 1,
|
||||
"result": {
|
||||
"name": "nginx",
|
||||
"pid": 1234,
|
||||
"memory_usage": 10485760,
|
||||
"cpu_usage": 2.5,
|
||||
"children": [
|
||||
{
|
||||
"pid": 1235,
|
||||
"memory_usage": 5242880,
|
||||
"cpu_usage": 1.2
|
||||
},
|
||||
{
|
||||
"pid": 1236,
|
||||
"memory_usage": 4194304,
|
||||
"cpu_usage": 0.8
|
||||
}
|
||||
]
|
||||
}
|
||||
}
|
||||
```
|
||||
|
||||
**Possible Errors:**
|
||||
- `-32000`: Service not found
|
||||
- `-32003`: Service is down
|
||||
|
||||
## Implementation Details
|
||||
|
||||
The stats functionality works by:
|
||||
|
||||
1. Reading process information from `/proc/<pid>/` directories on Linux systems
|
||||
2. Calculating memory usage from `/proc/<pid>/status` (VmRSS field)
|
||||
3. Calculating CPU usage by sampling `/proc/<pid>/stat` over a short interval
|
||||
4. Identifying child processes by checking the PPid field in `/proc/<pid>/status`
|
||||
|
||||
On non-Linux systems, the functionality provides placeholder values as the `/proc` filesystem is specific to Linux.
|
||||
|
||||
## Notes
|
||||
|
||||
- Memory usage is reported in bytes
|
||||
- CPU usage is reported as a percentage (0-100)
|
||||
- The service must be running to get stats (otherwise an error is returned)
|
||||
- Child processes are identified by their parent PID matching the service's PID
|
||||
104
components/zinit/example/example.sh
Executable file
104
components/zinit/example/example.sh
Executable file
@@ -0,0 +1,104 @@
|
||||
#!/bin/bash
|
||||
|
||||
# Determine the zinit binary path
|
||||
ZINIT_BIN="./target/release/zinit" # Assuming zinit is built in release mode in the current directory
|
||||
|
||||
# Determine the configuration directory based on OS
|
||||
if [[ "$(uname)" == "Darwin" ]]; then
|
||||
# macOS
|
||||
ZINIT_CONFIG_DIR="$HOME/hero/cfg/zinit"
|
||||
else
|
||||
# Linux or other
|
||||
ZINIT_CONFIG_DIR="/etc/zinit"
|
||||
fi
|
||||
|
||||
SERVICE_NAME="test_service"
|
||||
CPU_SERVICE_NAME="cpu_test_service"
|
||||
SERVICE_FILE="$ZINIT_CONFIG_DIR/$SERVICE_NAME.yaml"
|
||||
CPU_SERVICE_FILE="$ZINIT_CONFIG_DIR/$CPU_SERVICE_NAME.yaml"
|
||||
|
||||
echo "--- Zinit Example Script ---"
|
||||
echo "Zinit binary path: $ZINIT_BIN"
|
||||
echo "Zinit config directory: $ZINIT_CONFIG_DIR"
|
||||
|
||||
# Step 1: Ensure zinit config directory exists
|
||||
echo "Ensuring zinit config directory exists..."
|
||||
mkdir -p "$ZINIT_CONFIG_DIR"
|
||||
if [ $? -ne 0 ]; then
|
||||
echo "Error: Failed to create config directory $ZINIT_CONFIG_DIR. Exiting."
|
||||
exit 1
|
||||
fi
|
||||
echo "Config directory $ZINIT_CONFIG_DIR is ready."
|
||||
|
||||
# Step 2: Check if zinit daemon is running, if not, start it in background
|
||||
echo "Checking if zinit daemon is running..."
|
||||
if "$ZINIT_BIN" list > /dev/null 2>&1; then
|
||||
echo "Zinit daemon is already running."
|
||||
else
|
||||
echo "Zinit daemon not running. Starting it in background..."
|
||||
# Start zinit init in a new process group to avoid it being killed by script exit
|
||||
# and redirecting output to /dev/null
|
||||
nohup "$ZINIT_BIN" init > /dev/null 2>&1 &
|
||||
ZINIT_PID=$!
|
||||
echo "Zinit daemon started with PID: $ZINIT_PID"
|
||||
sleep 2 # Give zinit a moment to start up and create the socket
|
||||
if ! "$ZINIT_BIN" list > /dev/null 2>&1; then
|
||||
echo "Error: Zinit daemon failed to start. Exiting."
|
||||
exit 1
|
||||
fi
|
||||
echo "Zinit daemon successfully started."
|
||||
fi
|
||||
|
||||
# Step 3: Create sample zinit service files
|
||||
echo "Creating sample service file: $SERVICE_FILE"
|
||||
cat <<EOF > "$SERVICE_FILE"
|
||||
name: $SERVICE_NAME
|
||||
exec: /bin/bash -c "while true; do echo 'Hello from $SERVICE_NAME!'; sleep 5; done"
|
||||
log: stdout
|
||||
EOF
|
||||
|
||||
if [ $? -ne 0 ]; then
|
||||
echo "Error: Failed to create service file $SERVICE_FILE. Exiting."
|
||||
exit 1
|
||||
fi
|
||||
echo "Service file created."
|
||||
|
||||
# Create a CPU-intensive service with child processes
|
||||
echo "Creating CPU-intensive service file: $CPU_SERVICE_FILE"
|
||||
cat <<EOF > "$CPU_SERVICE_FILE"
|
||||
name: $CPU_SERVICE_NAME
|
||||
exec: /bin/bash -c "for i in {1..3}; do (yes > /dev/null &) ; done; while true; do sleep 10; done"
|
||||
log: stdout
|
||||
EOF
|
||||
|
||||
if [ $? -ne 0 ]; then
|
||||
echo "Error: Failed to create CPU service file $CPU_SERVICE_FILE. Exiting."
|
||||
exit 1
|
||||
fi
|
||||
echo "CPU service file created."
|
||||
|
||||
# Step 4: Tell zinit to monitor the new services
|
||||
echo "Telling zinit to monitor the services..."
|
||||
"$ZINIT_BIN" monitor "$SERVICE_NAME"
|
||||
"$ZINIT_BIN" monitor "$CPU_SERVICE_NAME"
|
||||
|
||||
# Step 5: List services to verify the new service is recognized
|
||||
echo "Listing zinit services to verify..."
|
||||
"$ZINIT_BIN" list
|
||||
|
||||
# Step 6: Show stats for the CPU-intensive service
|
||||
echo "Waiting for services to start and generate some stats..."
|
||||
sleep 5
|
||||
echo "Getting stats for $CPU_SERVICE_NAME..."
|
||||
"$ZINIT_BIN" stats "$CPU_SERVICE_NAME"
|
||||
|
||||
# # Step 7: Clean up (optional, but good for examples)
|
||||
# echo "Cleaning up: stopping and forgetting services..."
|
||||
# "$ZINIT_BIN" stop "$SERVICE_NAME" > /dev/null 2>&1
|
||||
# "$ZINIT_BIN" forget "$SERVICE_NAME" > /dev/null 2>&1
|
||||
# "$ZINIT_BIN" stop "$CPU_SERVICE_NAME" > /dev/null 2>&1
|
||||
# "$ZINIT_BIN" forget "$CPU_SERVICE_NAME" > /dev/null 2>&1
|
||||
# rm -f "$SERVICE_FILE" "$CPU_SERVICE_FILE"
|
||||
# echo "Cleanup complete."
|
||||
|
||||
echo "--- Script Finished ---"
|
||||
153
components/zinit/install.sh
Executable file
153
components/zinit/install.sh
Executable file
@@ -0,0 +1,153 @@
|
||||
#!/bin/bash
|
||||
set -e
|
||||
|
||||
# Colors for output
|
||||
RED='\033[0;31m'
|
||||
GREEN='\033[0;32m'
|
||||
YELLOW='\033[0;33m'
|
||||
NC='\033[0m' # No Color
|
||||
|
||||
echo -e "${GREEN}stop zinit...${NC}"
|
||||
rm -f /tmp/stop.sh
|
||||
curl -fsSL https://raw.githubusercontent.com/threefoldtech/zinit/refs/heads/master/stop.sh > /tmp/stop.sh
|
||||
bash /tmp/stop.sh
|
||||
|
||||
|
||||
# GitHub repository information
|
||||
GITHUB_REPO="threefoldtech/zinit"
|
||||
|
||||
# Get the latest version from GitHub API
|
||||
echo -e "${YELLOW}Fetching latest version information...${NC}"
|
||||
if command -v curl &> /dev/null; then
|
||||
VERSION=$(curl -s "https://api.github.com/repos/${GITHUB_REPO}/releases/latest" | grep -o '"tag_name": "[^"]*' | grep -o '[^"]*$')
|
||||
elif command -v wget &> /dev/null; then
|
||||
VERSION=$(wget -qO- "https://api.github.com/repos/${GITHUB_REPO}/releases/latest" | grep -o '"tag_name": "[^"]*' | grep -o '[^"]*$')
|
||||
else
|
||||
echo -e "${RED}Neither curl nor wget found. Please install one of them and try again.${NC}"
|
||||
exit 1
|
||||
fi
|
||||
|
||||
if [ -z "$VERSION" ]; then
|
||||
echo -e "${RED}Failed to fetch the latest version. Please check your internet connection.${NC}"
|
||||
exit 1
|
||||
fi
|
||||
|
||||
echo -e "${GREEN}Latest version: ${VERSION}${NC}"
|
||||
DOWNLOAD_URL="https://github.com/${GITHUB_REPO}/releases/download/${VERSION}"
|
||||
MIN_SIZE_BYTES=2000000 # 2MB in bytes
|
||||
|
||||
echo -e "${GREEN}Installing zinit ${VERSION}...${NC}"
|
||||
|
||||
# Create temporary directory
|
||||
TMP_DIR=$(mktemp -d)
|
||||
trap 'rm -rf "$TMP_DIR"' EXIT
|
||||
|
||||
# Detect OS and architecture
|
||||
OS=$(uname -s | tr '[:upper:]' '[:lower:]')
|
||||
ARCH=$(uname -m)
|
||||
|
||||
# Map architecture names
|
||||
if [ "$ARCH" = "x86_64" ]; then
|
||||
ARCH_NAME="x86_64"
|
||||
elif [ "$ARCH" = "aarch64" ] || [ "$ARCH" = "arm64" ]; then
|
||||
ARCH_NAME="aarch64"
|
||||
else
|
||||
echo -e "${RED}Unsupported architecture: $ARCH${NC}"
|
||||
exit 1
|
||||
fi
|
||||
|
||||
# Determine binary name based on OS and architecture
|
||||
if [ "$OS" = "linux" ]; then
|
||||
if [ "$ARCH_NAME" = "x86_64" ]; then
|
||||
BINARY_NAME="zinit-linux-x86_64"
|
||||
else
|
||||
echo -e "${RED}Unsupported Linux architecture: $ARCH${NC}"
|
||||
exit 1
|
||||
fi
|
||||
elif [ "$OS" = "darwin" ]; then
|
||||
if [ "$ARCH_NAME" = "x86_64" ]; then
|
||||
BINARY_NAME="zinit-macos-x86_64"
|
||||
elif [ "$ARCH_NAME" = "aarch64" ]; then
|
||||
BINARY_NAME="zinit-macos-aarch64"
|
||||
else
|
||||
echo -e "${RED}Unsupported macOS architecture: $ARCH${NC}"
|
||||
exit 1
|
||||
fi
|
||||
else
|
||||
echo -e "${RED}Unsupported operating system: $OS${NC}"
|
||||
exit 1
|
||||
fi
|
||||
|
||||
# Download URL
|
||||
DOWNLOAD_PATH="${DOWNLOAD_URL}/${BINARY_NAME}"
|
||||
LOCAL_PATH="${TMP_DIR}/${BINARY_NAME}"
|
||||
|
||||
echo -e "${YELLOW}Detected: $OS on $ARCH_NAME${NC}"
|
||||
echo -e "${YELLOW}Downloading from: $DOWNLOAD_PATH${NC}"
|
||||
|
||||
# Download the binary
|
||||
if command -v curl &> /dev/null; then
|
||||
curl -L -o "$LOCAL_PATH" "$DOWNLOAD_PATH"
|
||||
elif command -v wget &> /dev/null; then
|
||||
wget -O "$LOCAL_PATH" "$DOWNLOAD_PATH"
|
||||
else
|
||||
echo -e "${RED}Neither curl nor wget found. Please install one of them and try again.${NC}"
|
||||
exit 1
|
||||
fi
|
||||
|
||||
# Check file size
|
||||
FILE_SIZE=$(stat -f%z "$LOCAL_PATH" 2>/dev/null || stat -c%s "$LOCAL_PATH" 2>/dev/null)
|
||||
if [ "$FILE_SIZE" -lt "$MIN_SIZE_BYTES" ]; then
|
||||
echo -e "${RED}Downloaded file is too small (${FILE_SIZE} bytes). Expected at least ${MIN_SIZE_BYTES} bytes.${NC}"
|
||||
echo -e "${RED}This might indicate a failed or incomplete download.${NC}"
|
||||
exit 1
|
||||
fi
|
||||
|
||||
echo -e "${GREEN}Download successful. File size: $(echo "$FILE_SIZE / 1000000" | bc -l | xargs printf "%.2f") MB${NC}"
|
||||
|
||||
# Make the binary executable
|
||||
chmod +x "$LOCAL_PATH"
|
||||
|
||||
# Determine installation directory
|
||||
if [ "$OS" = "darwin" ]; then
|
||||
# macOS - install to ~/hero/bin/
|
||||
INSTALL_DIR="$HOME/hero/bin"
|
||||
else
|
||||
# Linux - install to /usr/local/bin/ if running as root, otherwise to ~/.local/bin/
|
||||
if [ "$(id -u)" -eq 0 ]; then
|
||||
INSTALL_DIR="/usr/local/bin"
|
||||
else
|
||||
INSTALL_DIR="$HOME/.local/bin"
|
||||
# Ensure ~/.local/bin exists and is in PATH
|
||||
mkdir -p "$INSTALL_DIR"
|
||||
if [[ ":$PATH:" != *":$INSTALL_DIR:"* ]]; then
|
||||
echo -e "${YELLOW}Adding $INSTALL_DIR to your PATH. You may need to restart your terminal.${NC}"
|
||||
if [ -f "$HOME/.bashrc" ]; then
|
||||
echo "export PATH=\"\$PATH:$INSTALL_DIR\"" >> "$HOME/.bashrc"
|
||||
fi
|
||||
if [ -f "$HOME/.zshrc" ]; then
|
||||
echo "export PATH=\"\$PATH:$INSTALL_DIR\"" >> "$HOME/.zshrc"
|
||||
fi
|
||||
fi
|
||||
fi
|
||||
fi
|
||||
|
||||
# Create installation directory if it doesn't exist
|
||||
mkdir -p "$INSTALL_DIR"
|
||||
|
||||
# Copy the binary to the installation directory
|
||||
cp "$LOCAL_PATH" "$INSTALL_DIR/zinit"
|
||||
echo -e "${GREEN}Installed zinit to $INSTALL_DIR/zinit${NC}"
|
||||
|
||||
# Test the installation
|
||||
echo -e "${YELLOW}Testing installation...${NC}"
|
||||
if "$INSTALL_DIR/zinit" --help &> /dev/null; then
|
||||
echo -e "${GREEN}Installation successful! You can now use 'zinit' command.${NC}"
|
||||
echo -e "${YELLOW}Example usage: zinit --help${NC}"
|
||||
"$INSTALL_DIR/zinit" --help | head -n 5
|
||||
else
|
||||
echo -e "${RED}Installation test failed. Please check the error messages above.${NC}"
|
||||
exit 1
|
||||
fi
|
||||
|
||||
echo -e "${GREEN}zinit ${VERSION} has been successfully installed!${NC}"
|
||||
50
components/zinit/install_run.sh
Executable file
50
components/zinit/install_run.sh
Executable file
@@ -0,0 +1,50 @@
|
||||
#!/bin/bash
|
||||
set -e
|
||||
|
||||
# Colors for output
|
||||
RED='\033[0;31m'
|
||||
GREEN='\033[0;32m'
|
||||
YELLOW='\033[0;33m'
|
||||
NC='\033[0m' # No Color
|
||||
|
||||
# Function to check if zinit is running
|
||||
is_zinit_running() {
|
||||
if zinit list &>/dev/null; then
|
||||
return 0 # Command successful, zinit is running
|
||||
else
|
||||
return 1 # Command failed, zinit is not running
|
||||
fi
|
||||
}
|
||||
|
||||
echo -e "${GREEN}Starting zinit installation and setup...${NC}"
|
||||
# Download and execute install.sh
|
||||
echo -e "${YELLOW}Downloading and executing install.sh...${NC}"
|
||||
curl -fsSL https://raw.githubusercontent.com/threefoldtech/zinit/refs/heads/master/install.sh | bash
|
||||
|
||||
echo -e "${GREEN}install zinit...${NC}"
|
||||
rm -f /tmp/install.sh
|
||||
curl -fsSL https://raw.githubusercontent.com/threefoldtech/zinit/refs/heads/master/install.sh > /tmp/install.sh
|
||||
bash /tmp/install.sh
|
||||
|
||||
|
||||
# Launch zinit in the background
|
||||
echo -e "${GREEN}Starting zinit in the background...${NC}"
|
||||
zinit &
|
||||
|
||||
# Give it a moment to start
|
||||
sleep 1
|
||||
|
||||
# Verify zinit is running
|
||||
if is_zinit_running; then
|
||||
echo -e "${GREEN}Zinit is now running in the background.${NC}"
|
||||
echo -e "${YELLOW}You can manage services with:${NC}"
|
||||
echo -e " ${YELLOW}$ZINIT_PATH list${NC} - List all services"
|
||||
echo -e " ${YELLOW}$ZINIT_PATH status${NC} - Show status of all services"
|
||||
echo -e " ${YELLOW}$ZINIT_PATH monitor${NC} - Monitor services in real-time"
|
||||
echo -e " ${YELLOW}$ZINIT_PATH shutdown${NC} - Shutdown zinit when needed"
|
||||
else
|
||||
echo -e "${RED}Failed to start zinit. Please check for errors above.${NC}"
|
||||
exit 1
|
||||
fi
|
||||
|
||||
echo -e "${GREEN}Zinit installation and startup complete!${NC}"
|
||||
873
components/zinit/openrpc.json
Normal file
873
components/zinit/openrpc.json
Normal file
@@ -0,0 +1,873 @@
|
||||
{
|
||||
"openrpc": "1.2.6",
|
||||
"info": {
|
||||
"version": "1.0.0",
|
||||
"title": "Zinit JSON-RPC API",
|
||||
"description": "JSON-RPC 2.0 API for controlling and querying Zinit services",
|
||||
"license": {
|
||||
"name": "MIT"
|
||||
}
|
||||
},
|
||||
"servers": [
|
||||
{
|
||||
"name": "Unix Socket",
|
||||
"url": "unix:///tmp/zinit.sock"
|
||||
}
|
||||
],
|
||||
"methods": [
|
||||
{
|
||||
"name": "rpc.discover",
|
||||
"description": "Returns the OpenRPC specification for the API",
|
||||
"params": [],
|
||||
"result": {
|
||||
"name": "OpenRPCSpec",
|
||||
"description": "The OpenRPC specification",
|
||||
"schema": {
|
||||
"type": "object"
|
||||
}
|
||||
},
|
||||
"examples": [
|
||||
{
|
||||
"name": "Get API specification",
|
||||
"params": [],
|
||||
"result": {
|
||||
"name": "OpenRPCSpecResult",
|
||||
"value": {
|
||||
"openrpc": "1.2.6",
|
||||
"info": {
|
||||
"version": "1.0.0",
|
||||
"title": "Zinit JSON-RPC API"
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
]
|
||||
},
|
||||
{
|
||||
"name": "service_list",
|
||||
"description": "Lists all services managed by Zinit",
|
||||
"params": [],
|
||||
"result": {
|
||||
"name": "ServiceList",
|
||||
"description": "A map of service names to their current states",
|
||||
"schema": {
|
||||
"type": "object",
|
||||
"additionalProperties": {
|
||||
"type": "string",
|
||||
"description": "Service state (Running, Success, Error, etc.)"
|
||||
}
|
||||
}
|
||||
},
|
||||
"examples": [
|
||||
{
|
||||
"name": "List all services",
|
||||
"params": [],
|
||||
"result": {
|
||||
"name": "ServiceListResult",
|
||||
"value": {
|
||||
"service1": "Running",
|
||||
"service2": "Success",
|
||||
"service3": "Error"
|
||||
}
|
||||
}
|
||||
}
|
||||
]
|
||||
},
|
||||
{
|
||||
"name": "service_status",
|
||||
"description": "Shows detailed status information for a specific service",
|
||||
"params": [
|
||||
{
|
||||
"name": "name",
|
||||
"description": "The name of the service",
|
||||
"required": true,
|
||||
"schema": {
|
||||
"type": "string"
|
||||
}
|
||||
}
|
||||
],
|
||||
"result": {
|
||||
"name": "ServiceStatus",
|
||||
"description": "Detailed status information for the service",
|
||||
"schema": {
|
||||
"type": "object",
|
||||
"properties": {
|
||||
"name": {
|
||||
"type": "string",
|
||||
"description": "Service name"
|
||||
},
|
||||
"pid": {
|
||||
"type": "integer",
|
||||
"description": "Process ID of the running service (if running)"
|
||||
},
|
||||
"state": {
|
||||
"type": "string",
|
||||
"description": "Current state of the service (Running, Success, Error, etc.)"
|
||||
},
|
||||
"target": {
|
||||
"type": "string",
|
||||
"description": "Target state of the service (Up, Down)"
|
||||
},
|
||||
"after": {
|
||||
"type": "object",
|
||||
"description": "Dependencies of the service and their states",
|
||||
"additionalProperties": {
|
||||
"type": "string",
|
||||
"description": "State of the dependency"
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
},
|
||||
"examples": [
|
||||
{
|
||||
"name": "Get status of redis service",
|
||||
"params": [
|
||||
{
|
||||
"name": "name",
|
||||
"value": "redis"
|
||||
}
|
||||
],
|
||||
"result": {
|
||||
"name": "ServiceStatusResult",
|
||||
"value": {
|
||||
"name": "redis",
|
||||
"pid": 1234,
|
||||
"state": "Running",
|
||||
"target": "Up",
|
||||
"after": {
|
||||
"dependency1": "Success",
|
||||
"dependency2": "Running"
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
],
|
||||
"errors": [
|
||||
{
|
||||
"code": -32000,
|
||||
"message": "Service not found",
|
||||
"data": "service name \"unknown\" unknown"
|
||||
}
|
||||
]
|
||||
},
|
||||
{
|
||||
"name": "service_start",
|
||||
"description": "Starts a service",
|
||||
"params": [
|
||||
{
|
||||
"name": "name",
|
||||
"description": "The name of the service to start",
|
||||
"required": true,
|
||||
"schema": {
|
||||
"type": "string"
|
||||
}
|
||||
}
|
||||
],
|
||||
"result": {
|
||||
"name": "StartResult",
|
||||
"description": "Result of the start operation",
|
||||
"schema": {
|
||||
"type": "null"
|
||||
}
|
||||
},
|
||||
"examples": [
|
||||
{
|
||||
"name": "Start redis service",
|
||||
"params": [
|
||||
{
|
||||
"name": "name",
|
||||
"value": "redis"
|
||||
}
|
||||
],
|
||||
"result": {
|
||||
"name": "StartResult",
|
||||
"value": null
|
||||
}
|
||||
}
|
||||
],
|
||||
"errors": [
|
||||
{
|
||||
"code": -32000,
|
||||
"message": "Service not found",
|
||||
"data": "service name \"unknown\" unknown"
|
||||
}
|
||||
]
|
||||
},
|
||||
{
|
||||
"name": "service_stop",
|
||||
"description": "Stops a service",
|
||||
"params": [
|
||||
{
|
||||
"name": "name",
|
||||
"description": "The name of the service to stop",
|
||||
"required": true,
|
||||
"schema": {
|
||||
"type": "string"
|
||||
}
|
||||
}
|
||||
],
|
||||
"result": {
|
||||
"name": "StopResult",
|
||||
"description": "Result of the stop operation",
|
||||
"schema": {
|
||||
"type": "null"
|
||||
}
|
||||
},
|
||||
"examples": [
|
||||
{
|
||||
"name": "Stop redis service",
|
||||
"params": [
|
||||
{
|
||||
"name": "name",
|
||||
"value": "redis"
|
||||
}
|
||||
],
|
||||
"result": {
|
||||
"name": "StopResult",
|
||||
"value": null
|
||||
}
|
||||
}
|
||||
],
|
||||
"errors": [
|
||||
{
|
||||
"code": -32000,
|
||||
"message": "Service not found",
|
||||
"data": "service name \"unknown\" unknown"
|
||||
},
|
||||
{
|
||||
"code": -32003,
|
||||
"message": "Service is down",
|
||||
"data": "service \"redis\" is down"
|
||||
}
|
||||
]
|
||||
},
|
||||
{
|
||||
"name": "service_monitor",
|
||||
"description": "Starts monitoring a service. The service configuration is loaded from the config directory.",
|
||||
"params": [
|
||||
{
|
||||
"name": "name",
|
||||
"description": "The name of the service to monitor",
|
||||
"required": true,
|
||||
"schema": {
|
||||
"type": "string"
|
||||
}
|
||||
}
|
||||
],
|
||||
"result": {
|
||||
"name": "MonitorResult",
|
||||
"description": "Result of the monitor operation",
|
||||
"schema": {
|
||||
"type": "null"
|
||||
}
|
||||
},
|
||||
"examples": [
|
||||
{
|
||||
"name": "Monitor redis service",
|
||||
"params": [
|
||||
{
|
||||
"name": "name",
|
||||
"value": "redis"
|
||||
}
|
||||
],
|
||||
"result": {
|
||||
"name": "MonitorResult",
|
||||
"value": null
|
||||
}
|
||||
}
|
||||
],
|
||||
"errors": [
|
||||
{
|
||||
"code": -32001,
|
||||
"message": "Service already monitored",
|
||||
"data": "service \"redis\" already monitored"
|
||||
},
|
||||
{
|
||||
"code": -32005,
|
||||
"message": "Config error",
|
||||
"data": "failed to load service configuration"
|
||||
}
|
||||
]
|
||||
},
|
||||
{
|
||||
"name": "service_forget",
|
||||
"description": "Stops monitoring a service. You can only forget a stopped service.",
|
||||
"params": [
|
||||
{
|
||||
"name": "name",
|
||||
"description": "The name of the service to forget",
|
||||
"required": true,
|
||||
"schema": {
|
||||
"type": "string"
|
||||
}
|
||||
}
|
||||
],
|
||||
"result": {
|
||||
"name": "ForgetResult",
|
||||
"description": "Result of the forget operation",
|
||||
"schema": {
|
||||
"type": "null"
|
||||
}
|
||||
},
|
||||
"examples": [
|
||||
{
|
||||
"name": "Forget redis service",
|
||||
"params": [
|
||||
{
|
||||
"name": "name",
|
||||
"value": "redis"
|
||||
}
|
||||
],
|
||||
"result": {
|
||||
"name": "ForgetResult",
|
||||
"value": null
|
||||
}
|
||||
}
|
||||
],
|
||||
"errors": [
|
||||
{
|
||||
"code": -32000,
|
||||
"message": "Service not found",
|
||||
"data": "service name \"unknown\" unknown"
|
||||
},
|
||||
{
|
||||
"code": -32002,
|
||||
"message": "Service is up",
|
||||
"data": "service \"redis\" is up"
|
||||
}
|
||||
]
|
||||
},
|
||||
{
|
||||
"name": "service_kill",
|
||||
"description": "Sends a signal to a running service",
|
||||
"params": [
|
||||
{
|
||||
"name": "name",
|
||||
"description": "The name of the service to send the signal to",
|
||||
"required": true,
|
||||
"schema": {
|
||||
"type": "string"
|
||||
}
|
||||
},
|
||||
{
|
||||
"name": "signal",
|
||||
"description": "The signal to send (e.g., SIGTERM, SIGKILL)",
|
||||
"required": true,
|
||||
"schema": {
|
||||
"type": "string"
|
||||
}
|
||||
}
|
||||
],
|
||||
"result": {
|
||||
"name": "KillResult",
|
||||
"description": "Result of the kill operation",
|
||||
"schema": {
|
||||
"type": "null"
|
||||
}
|
||||
},
|
||||
"examples": [
|
||||
{
|
||||
"name": "Send SIGTERM to redis service",
|
||||
"params": [
|
||||
{
|
||||
"name": "name",
|
||||
"value": "redis"
|
||||
},
|
||||
{
|
||||
"name": "signal",
|
||||
"value": "SIGTERM"
|
||||
}
|
||||
],
|
||||
"result": {
|
||||
"name": "KillResult",
|
||||
"value": null
|
||||
}
|
||||
}
|
||||
],
|
||||
"errors": [
|
||||
{
|
||||
"code": -32000,
|
||||
"message": "Service not found",
|
||||
"data": "service name \"unknown\" unknown"
|
||||
},
|
||||
{
|
||||
"code": -32003,
|
||||
"message": "Service is down",
|
||||
"data": "service \"redis\" is down"
|
||||
},
|
||||
{
|
||||
"code": -32004,
|
||||
"message": "Invalid signal",
|
||||
"data": "invalid signal: INVALID"
|
||||
}
|
||||
]
|
||||
},
|
||||
{
|
||||
"name": "system_shutdown",
|
||||
"description": "Stops all services and powers off the system",
|
||||
"params": [],
|
||||
"result": {
|
||||
"name": "ShutdownResult",
|
||||
"description": "Result of the shutdown operation",
|
||||
"schema": {
|
||||
"type": "null"
|
||||
}
|
||||
},
|
||||
"examples": [
|
||||
{
|
||||
"name": "Shutdown the system",
|
||||
"params": [],
|
||||
"result": {
|
||||
"name": "ShutdownResult",
|
||||
"value": null
|
||||
}
|
||||
}
|
||||
],
|
||||
"errors": [
|
||||
{
|
||||
"code": -32006,
|
||||
"message": "Shutting down",
|
||||
"data": "system is already shutting down"
|
||||
}
|
||||
]
|
||||
},
|
||||
{
|
||||
"name": "system_reboot",
|
||||
"description": "Stops all services and reboots the system",
|
||||
"params": [],
|
||||
"result": {
|
||||
"name": "RebootResult",
|
||||
"description": "Result of the reboot operation",
|
||||
"schema": {
|
||||
"type": "null"
|
||||
}
|
||||
},
|
||||
"examples": [
|
||||
{
|
||||
"name": "Reboot the system",
|
||||
"params": [],
|
||||
"result": {
|
||||
"name": "RebootResult",
|
||||
"value": null
|
||||
}
|
||||
}
|
||||
],
|
||||
"errors": [
|
||||
{
|
||||
"code": -32006,
|
||||
"message": "Shutting down",
|
||||
"data": "system is already shutting down"
|
||||
}
|
||||
]
|
||||
},
|
||||
{
|
||||
"name": "service_create",
|
||||
"description": "Creates a new service configuration file",
|
||||
"params": [
|
||||
{
|
||||
"name": "name",
|
||||
"description": "The name of the service to create",
|
||||
"required": true,
|
||||
"schema": {
|
||||
"type": "string"
|
||||
}
|
||||
},
|
||||
{
|
||||
"name": "content",
|
||||
"description": "The service configuration content",
|
||||
"required": true,
|
||||
"schema": {
|
||||
"type": "object",
|
||||
"properties": {
|
||||
"exec": {
|
||||
"type": "string",
|
||||
"description": "Command to run"
|
||||
},
|
||||
"oneshot": {
|
||||
"type": "boolean",
|
||||
"description": "Whether the service should be restarted"
|
||||
},
|
||||
"after": {
|
||||
"type": "array",
|
||||
"items": {
|
||||
"type": "string"
|
||||
},
|
||||
"description": "Services that must be running before this one starts"
|
||||
},
|
||||
"log": {
|
||||
"type": "string",
|
||||
"enum": ["null", "ring", "stdout"],
|
||||
"description": "How to handle service output"
|
||||
},
|
||||
"env": {
|
||||
"type": "object",
|
||||
"additionalProperties": {
|
||||
"type": "string"
|
||||
},
|
||||
"description": "Environment variables for the service"
|
||||
},
|
||||
"shutdown_timeout": {
|
||||
"type": "integer",
|
||||
"description": "Maximum time to wait for service to stop during shutdown"
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
],
|
||||
"result": {
|
||||
"name": "CreateServiceResult",
|
||||
"description": "Result of the create operation",
|
||||
"schema": {
|
||||
"type": "string"
|
||||
}
|
||||
},
|
||||
"errors": [
|
||||
{
|
||||
"code": -32007,
|
||||
"message": "Service already exists",
|
||||
"data": "Service 'name' already exists"
|
||||
},
|
||||
{
|
||||
"code": -32008,
|
||||
"message": "Service file error",
|
||||
"data": "Failed to create service file"
|
||||
}
|
||||
]
|
||||
},
|
||||
{
|
||||
"name": "service_delete",
|
||||
"description": "Deletes a service configuration file",
|
||||
"params": [
|
||||
{
|
||||
"name": "name",
|
||||
"description": "The name of the service to delete",
|
||||
"required": true,
|
||||
"schema": {
|
||||
"type": "string"
|
||||
}
|
||||
}
|
||||
],
|
||||
"result": {
|
||||
"name": "DeleteServiceResult",
|
||||
"description": "Result of the delete operation",
|
||||
"schema": {
|
||||
"type": "string"
|
||||
}
|
||||
},
|
||||
"errors": [
|
||||
{
|
||||
"code": -32000,
|
||||
"message": "Service not found",
|
||||
"data": "Service 'name' not found"
|
||||
},
|
||||
{
|
||||
"code": -32008,
|
||||
"message": "Service file error",
|
||||
"data": "Failed to delete service file"
|
||||
}
|
||||
]
|
||||
},
|
||||
{
|
||||
"name": "service_get",
|
||||
"description": "Gets a service configuration file",
|
||||
"params": [
|
||||
{
|
||||
"name": "name",
|
||||
"description": "The name of the service to get",
|
||||
"required": true,
|
||||
"schema": {
|
||||
"type": "string"
|
||||
}
|
||||
}
|
||||
],
|
||||
"result": {
|
||||
"name": "GetServiceResult",
|
||||
"description": "The service configuration",
|
||||
"schema": {
|
||||
"type": "object"
|
||||
}
|
||||
},
|
||||
"errors": [
|
||||
{
|
||||
"code": -32000,
|
||||
"message": "Service not found",
|
||||
"data": "Service 'name' not found"
|
||||
},
|
||||
{
|
||||
"code": -32008,
|
||||
"message": "Service file error",
|
||||
"data": "Failed to read service file"
|
||||
}
|
||||
]
|
||||
},
|
||||
{
|
||||
"name": "service_stats",
|
||||
"description": "Get memory and CPU usage statistics for a service",
|
||||
"params": [
|
||||
{
|
||||
"name": "name",
|
||||
"description": "The name of the service to get stats for",
|
||||
"required": true,
|
||||
"schema": {
|
||||
"type": "string"
|
||||
}
|
||||
}
|
||||
],
|
||||
"result": {
|
||||
"name": "ServiceStats",
|
||||
"description": "Memory and CPU usage statistics for the service",
|
||||
"schema": {
|
||||
"type": "object",
|
||||
"properties": {
|
||||
"name": {
|
||||
"type": "string",
|
||||
"description": "Service name"
|
||||
},
|
||||
"pid": {
|
||||
"type": "integer",
|
||||
"description": "Process ID of the service"
|
||||
},
|
||||
"memory_usage": {
|
||||
"type": "integer",
|
||||
"description": "Memory usage in bytes"
|
||||
},
|
||||
"cpu_usage": {
|
||||
"type": "number",
|
||||
"description": "CPU usage as a percentage (0-100)"
|
||||
},
|
||||
"children": {
|
||||
"type": "array",
|
||||
"description": "Stats for child processes",
|
||||
"items": {
|
||||
"type": "object",
|
||||
"properties": {
|
||||
"pid": {
|
||||
"type": "integer",
|
||||
"description": "Process ID of the child process"
|
||||
},
|
||||
"memory_usage": {
|
||||
"type": "integer",
|
||||
"description": "Memory usage in bytes"
|
||||
},
|
||||
"cpu_usage": {
|
||||
"type": "number",
|
||||
"description": "CPU usage as a percentage (0-100)"
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
},
|
||||
"examples": [
|
||||
{
|
||||
"name": "Get stats for redis service",
|
||||
"params": [
|
||||
{
|
||||
"name": "name",
|
||||
"value": "redis"
|
||||
}
|
||||
],
|
||||
"result": {
|
||||
"name": "ServiceStatsResult",
|
||||
"value": {
|
||||
"name": "redis",
|
||||
"pid": 1234,
|
||||
"memory_usage": 10485760,
|
||||
"cpu_usage": 2.5,
|
||||
"children": [
|
||||
{
|
||||
"pid": 1235,
|
||||
"memory_usage": 5242880,
|
||||
"cpu_usage": 1.2
|
||||
}
|
||||
]
|
||||
}
|
||||
}
|
||||
}
|
||||
],
|
||||
"errors": [
|
||||
{
|
||||
"code": -32000,
|
||||
"message": "Service not found",
|
||||
"data": "service name \"unknown\" unknown"
|
||||
},
|
||||
{
|
||||
"code": -32003,
|
||||
"message": "Service is down",
|
||||
"data": "service \"redis\" is down"
|
||||
}
|
||||
]
|
||||
},
|
||||
{
|
||||
"name": "system_start_http_server",
|
||||
"description": "Start an HTTP/RPC server at the specified address",
|
||||
"params": [
|
||||
{
|
||||
"name": "address",
|
||||
"description": "The network address to bind the server to (e.g., '127.0.0.1:8080')",
|
||||
"required": true,
|
||||
"schema": {
|
||||
"type": "string"
|
||||
}
|
||||
}
|
||||
],
|
||||
"result": {
|
||||
"name": "StartHttpServerResult",
|
||||
"description": "Result of the start HTTP server operation",
|
||||
"schema": {
|
||||
"type": "string"
|
||||
}
|
||||
},
|
||||
"examples": [
|
||||
{
|
||||
"name": "Start HTTP server on localhost:8080",
|
||||
"params": [
|
||||
{
|
||||
"name": "address",
|
||||
"value": "127.0.0.1:8080"
|
||||
}
|
||||
],
|
||||
"result": {
|
||||
"name": "StartHttpServerResult",
|
||||
"value": "HTTP server started at 127.0.0.1:8080"
|
||||
}
|
||||
}
|
||||
],
|
||||
"errors": [
|
||||
{
|
||||
"code": -32602,
|
||||
"message": "Invalid address",
|
||||
"data": "Invalid network address format"
|
||||
}
|
||||
]
|
||||
},
|
||||
{
|
||||
"name": "system_stop_http_server",
|
||||
"description": "Stop the HTTP/RPC server if running",
|
||||
"params": [],
|
||||
"result": {
|
||||
"name": "StopHttpServerResult",
|
||||
"description": "Result of the stop HTTP server operation",
|
||||
"schema": {
|
||||
"type": "null"
|
||||
}
|
||||
},
|
||||
"examples": [
|
||||
{
|
||||
"name": "Stop the HTTP server",
|
||||
"params": [],
|
||||
"result": {
|
||||
"name": "StopHttpServerResult",
|
||||
"value": null
|
||||
}
|
||||
}
|
||||
],
|
||||
"errors": [
|
||||
{
|
||||
"code": -32602,
|
||||
"message": "Server not running",
|
||||
"data": "No HTTP server is currently running"
|
||||
}
|
||||
]
|
||||
},
|
||||
{
|
||||
"name": "stream_currentLogs",
|
||||
"description": "Get current logs from zinit and monitored services",
|
||||
"params": [
|
||||
{
|
||||
"name": "name",
|
||||
"description": "Optional service name filter. If provided, only logs from this service will be returned",
|
||||
"required": false,
|
||||
"schema": {
|
||||
"type": "string"
|
||||
}
|
||||
}
|
||||
],
|
||||
"result": {
|
||||
"name": "LogsResult",
|
||||
"description": "Array of log strings",
|
||||
"schema": {
|
||||
"type": "array",
|
||||
"items": {
|
||||
"type": "string"
|
||||
}
|
||||
}
|
||||
},
|
||||
"examples": [
|
||||
{
|
||||
"name": "Get all logs",
|
||||
"params": [],
|
||||
"result": {
|
||||
"name": "LogsResult",
|
||||
"value": [
|
||||
"2023-01-01T12:00:00 redis: Starting service",
|
||||
"2023-01-01T12:00:01 nginx: Starting service"
|
||||
]
|
||||
}
|
||||
},
|
||||
{
|
||||
"name": "Get logs for a specific service",
|
||||
"params": [
|
||||
{
|
||||
"name": "name",
|
||||
"value": "redis"
|
||||
}
|
||||
],
|
||||
"result": {
|
||||
"name": "LogsResult",
|
||||
"value": [
|
||||
"2023-01-01T12:00:00 redis: Starting service",
|
||||
"2023-01-01T12:00:02 redis: Service started"
|
||||
]
|
||||
}
|
||||
}
|
||||
]
|
||||
},
|
||||
{
|
||||
"name": "stream_subscribeLogs",
|
||||
"description": "Subscribe to log messages generated by zinit and monitored services",
|
||||
"params": [
|
||||
{
|
||||
"name": "name",
|
||||
"description": "Optional service name filter. If provided, only logs from this service will be returned",
|
||||
"required": false,
|
||||
"schema": {
|
||||
"type": "string"
|
||||
}
|
||||
}
|
||||
],
|
||||
"result": {
|
||||
"name": "LogSubscription",
|
||||
"description": "A subscription to log messages",
|
||||
"schema": {
|
||||
"type": "string"
|
||||
}
|
||||
},
|
||||
"examples": [
|
||||
{
|
||||
"name": "Subscribe to all logs",
|
||||
"params": [],
|
||||
"result": {
|
||||
"name": "LogSubscription",
|
||||
"value": "2023-01-01T12:00:00 redis: Service started"
|
||||
}
|
||||
},
|
||||
{
|
||||
"name": "Subscribe to filtered logs",
|
||||
"params": [
|
||||
{
|
||||
"name": "name",
|
||||
"value": "redis"
|
||||
}
|
||||
],
|
||||
"result": {
|
||||
"name": "LogSubscription",
|
||||
"value": "2023-01-01T12:00:00 redis: Service started"
|
||||
}
|
||||
}
|
||||
]
|
||||
}
|
||||
]
|
||||
}
|
||||
57
components/zinit/osx_build.sh
Executable file
57
components/zinit/osx_build.sh
Executable file
@@ -0,0 +1,57 @@
|
||||
#!/bin/bash
|
||||
|
||||
# Jump to the directory of the script
|
||||
cd "$(dirname "$0")"
|
||||
|
||||
./stop.sh
|
||||
|
||||
# Build the project
|
||||
echo "Building zinit..."
|
||||
cargo build --release
|
||||
|
||||
if [ $? -ne 0 ]; then
|
||||
echo "Build failed!"
|
||||
exit 1
|
||||
fi
|
||||
|
||||
# Copy the binary
|
||||
echo "Copying zinit binary to ~/hero/bin..."
|
||||
cp ./target/release/zinit ~/hero/bin
|
||||
|
||||
if [ $? -ne 0 ]; then
|
||||
echo "Failed to copy binary!"
|
||||
exit 1
|
||||
fi
|
||||
|
||||
# Ensure config directory exists
|
||||
echo "Ensuring config directory exists..."
|
||||
mkdir -p ~/hero/cfg/zinit
|
||||
|
||||
# Start zinit in init mode (daemon) in background
|
||||
echo "Starting zinit daemon in background..."
|
||||
~/hero/bin/zinit init -c ~/hero/cfg/zinit &
|
||||
ZINIT_PID=$!
|
||||
|
||||
# Wait a moment for zinit to start and create the socket
|
||||
sleep 5
|
||||
|
||||
# Check if zinit is running
|
||||
if kill -0 $ZINIT_PID 2>/dev/null; then
|
||||
echo "Zinit daemon started successfully with PID: $ZINIT_PID"
|
||||
|
||||
# Test with zinit list
|
||||
echo "Testing zinit list command..."
|
||||
~/hero/bin/zinit list
|
||||
|
||||
if [ $? -eq 0 ]; then
|
||||
echo "Zinit is working correctly!"
|
||||
else
|
||||
echo "Warning: zinit list command failed, but zinit daemon is running"
|
||||
echo "This might be normal if no services are configured yet."
|
||||
fi
|
||||
else
|
||||
echo "Failed to start zinit daemon!"
|
||||
exit 1
|
||||
fi
|
||||
|
||||
echo "Build and setup completed successfully!"
|
||||
101
components/zinit/release_zinit.sh
Executable file
101
components/zinit/release_zinit.sh
Executable file
@@ -0,0 +1,101 @@
|
||||
#!/bin/bash
|
||||
|
||||
# Navigate to the zinit project directory
|
||||
cd /Users/despiegk/code/github/threefoldtech/zinit
|
||||
|
||||
# Check if we're in the right directory
|
||||
if [ ! -f "Cargo.toml" ]; then
|
||||
echo "Error: Not in zinit project directory"
|
||||
exit 1
|
||||
fi
|
||||
|
||||
# Function to get the latest tag from Git
|
||||
get_latest_tag() {
|
||||
# Fetch all tags from remote
|
||||
git fetch --tags origin 2>/dev/null
|
||||
|
||||
# Get the latest tag using version sorting
|
||||
local latest_tag=$(git tag -l "v*" | sort -V | tail -n 1)
|
||||
|
||||
if [ -z "$latest_tag" ]; then
|
||||
echo "v0.0.0"
|
||||
else
|
||||
echo "$latest_tag"
|
||||
fi
|
||||
}
|
||||
|
||||
# Function to increment version
|
||||
increment_version() {
|
||||
local version=$1
|
||||
# Remove 'v' prefix if present
|
||||
version=${version#v}
|
||||
|
||||
# Split version into parts
|
||||
IFS='.' read -ra PARTS <<< "$version"
|
||||
major=${PARTS[0]:-0}
|
||||
minor=${PARTS[1]:-0}
|
||||
patch=${PARTS[2]:-0}
|
||||
|
||||
# Increment patch (maintenance) version
|
||||
patch=$((patch + 1))
|
||||
|
||||
echo "v${major}.${minor}.${patch}"
|
||||
}
|
||||
|
||||
echo "🔍 Checking latest tag..."
|
||||
latest_tag=$(get_latest_tag)
|
||||
echo "Latest tag: $latest_tag"
|
||||
|
||||
new_version=$(increment_version "$latest_tag")
|
||||
echo "New version: $new_version"
|
||||
|
||||
# Confirm with user
|
||||
read -p "Create release $new_version? (y/N): " -n 1 -r
|
||||
echo
|
||||
if [[ ! $REPLY =~ ^[Yy]$ ]]; then
|
||||
echo "Release cancelled"
|
||||
exit 0
|
||||
fi
|
||||
|
||||
# Check if tag already exists locally and remove it
|
||||
if git tag -l | grep -q "^$new_version$"; then
|
||||
echo "⚠️ Local tag $new_version already exists, removing it..."
|
||||
git tag -d "$new_version"
|
||||
fi
|
||||
|
||||
# Make sure we're on the right branch and up to date
|
||||
echo "🔄 Updating repository..."
|
||||
git fetch origin
|
||||
|
||||
# Get current branch name
|
||||
current_branch=$(git branch --show-current)
|
||||
|
||||
# If we're not on main or master, try to checkout one of them
|
||||
if [[ "$current_branch" != "main" && "$current_branch" != "master" ]]; then
|
||||
echo "Current branch: $current_branch"
|
||||
if git show-ref --verify --quiet refs/heads/main; then
|
||||
echo "Switching to main branch..."
|
||||
git checkout main
|
||||
current_branch="main"
|
||||
elif git show-ref --verify --quiet refs/heads/master; then
|
||||
echo "Switching to master branch..."
|
||||
git checkout master
|
||||
current_branch="master"
|
||||
else
|
||||
echo "⚠️ Neither main nor master branch found, staying on current branch: $current_branch"
|
||||
fi
|
||||
fi
|
||||
|
||||
echo "Pulling latest changes from $current_branch..."
|
||||
git pull origin "$current_branch"
|
||||
|
||||
# Create and push the tag
|
||||
echo "🏷️ Creating tag $new_version..."
|
||||
git tag "$new_version"
|
||||
|
||||
echo "🚀 Pushing tag to trigger release..."
|
||||
git push origin "$new_version"
|
||||
|
||||
echo "✅ Release $new_version has been triggered!"
|
||||
echo "🔗 Check the release at: https://github.com/threefoldtech/zinit/releases"
|
||||
echo "🔗 Monitor the build at: https://github.com/threefoldtech/zinit/actions"
|
||||
139
components/zinit/src/app/api.rs
Normal file
139
components/zinit/src/app/api.rs
Normal file
@@ -0,0 +1,139 @@
|
||||
use super::rpc::{
|
||||
ZinitLoggingApiServer, ZinitRpcApiServer, ZinitServiceApiServer, ZinitSystemApiServer,
|
||||
};
|
||||
use crate::zinit::ZInit;
|
||||
use anyhow::{bail, Context, Result};
|
||||
use jsonrpsee::server::ServerHandle;
|
||||
use reth_ipc::server::Builder;
|
||||
use serde::{Deserialize, Serialize};
|
||||
use serde_json::Value;
|
||||
use std::collections::HashMap;
|
||||
use std::sync::Arc;
|
||||
use tokio::sync::Mutex;
|
||||
use tower_http::cors::{AllowHeaders, AllowMethods};
|
||||
use tower_http::cors::{Any, CorsLayer};
|
||||
|
||||
#[derive(Clone, Debug, Deserialize, Serialize)]
|
||||
#[serde(rename_all = "lowercase")]
|
||||
struct ZinitResponse {
|
||||
pub state: ZinitState,
|
||||
pub body: Value,
|
||||
}
|
||||
|
||||
#[derive(Clone, Debug, Deserialize, Serialize)]
|
||||
#[serde(rename_all = "lowercase")]
|
||||
enum ZinitState {
|
||||
Ok,
|
||||
Error,
|
||||
}
|
||||
|
||||
#[derive(Clone, Debug, Deserialize, Serialize)]
|
||||
#[serde(rename_all = "lowercase")]
|
||||
pub struct Status {
|
||||
pub name: String,
|
||||
pub pid: u32,
|
||||
pub state: String,
|
||||
pub target: String,
|
||||
pub after: HashMap<String, String>,
|
||||
}
|
||||
|
||||
/// Service stats information
|
||||
#[derive(Clone, Debug, Deserialize, Serialize)]
|
||||
#[serde(rename_all = "lowercase")]
|
||||
pub struct Stats {
|
||||
pub name: String,
|
||||
pub pid: u32,
|
||||
pub memory_usage: u64,
|
||||
pub cpu_usage: f32,
|
||||
pub children: Vec<ChildStats>,
|
||||
}
|
||||
|
||||
/// Child process stats information
|
||||
#[derive(Clone, Debug, Deserialize, Serialize)]
|
||||
#[serde(rename_all = "lowercase")]
|
||||
pub struct ChildStats {
|
||||
pub pid: u32,
|
||||
pub memory_usage: u64,
|
||||
pub cpu_usage: f32,
|
||||
}
|
||||
|
||||
pub struct ApiServer {
|
||||
_handle: ServerHandle,
|
||||
}
|
||||
|
||||
#[derive(Clone)]
|
||||
pub struct Api {
|
||||
pub zinit: ZInit,
|
||||
pub http_server_handle: Arc<Mutex<Option<jsonrpsee::server::ServerHandle>>>,
|
||||
}
|
||||
|
||||
impl Api {
|
||||
pub fn new(zinit: ZInit) -> Api {
|
||||
Api {
|
||||
zinit,
|
||||
http_server_handle: Arc::new(Mutex::new(None)),
|
||||
}
|
||||
}
|
||||
|
||||
pub async fn serve(&self, endpoint: String) -> Result<ApiServer> {
|
||||
let server = Builder::default().build(endpoint);
|
||||
let mut module = ZinitRpcApiServer::into_rpc(self.clone());
|
||||
module.merge(ZinitSystemApiServer::into_rpc(self.clone()))?;
|
||||
module.merge(ZinitServiceApiServer::into_rpc(self.clone()))?;
|
||||
module.merge(ZinitLoggingApiServer::into_rpc(self.clone()))?;
|
||||
|
||||
let _handle = server.start(module).await?;
|
||||
|
||||
Ok(ApiServer { _handle })
|
||||
}
|
||||
|
||||
/// Start an HTTP/RPC server at a specified address
|
||||
pub async fn start_http_server(&self, address: String) -> Result<String> {
|
||||
// Parse the address string
|
||||
let socket_addr = address
|
||||
.parse::<std::net::SocketAddr>()
|
||||
.context("Failed to parse socket address")?;
|
||||
|
||||
let cors = CorsLayer::new()
|
||||
// Allow `POST` when accessing the resource
|
||||
.allow_methods(AllowMethods::any())
|
||||
// Allow requests from any origin
|
||||
.allow_origin(Any)
|
||||
.allow_headers(AllowHeaders::any());
|
||||
let middleware = tower::ServiceBuilder::new().layer(cors);
|
||||
|
||||
// Create the JSON-RPC server with CORS support
|
||||
let server_rpc = jsonrpsee::server::ServerBuilder::default()
|
||||
.set_http_middleware(middleware)
|
||||
.build(socket_addr)
|
||||
.await?;
|
||||
|
||||
// Create and merge all API modules
|
||||
let mut rpc_module = ZinitRpcApiServer::into_rpc(self.clone());
|
||||
rpc_module.merge(ZinitSystemApiServer::into_rpc(self.clone()))?;
|
||||
rpc_module.merge(ZinitServiceApiServer::into_rpc(self.clone()))?;
|
||||
rpc_module.merge(ZinitLoggingApiServer::into_rpc(self.clone()))?;
|
||||
|
||||
// Start the server
|
||||
let handle = server_rpc.start(rpc_module);
|
||||
|
||||
// Store the handle
|
||||
let mut http_handle = self.http_server_handle.lock().await;
|
||||
*http_handle = Some(handle);
|
||||
|
||||
Ok(format!("HTTP/RPC server started at {}", address))
|
||||
}
|
||||
|
||||
/// Stop the HTTP/RPC server if running
|
||||
pub async fn stop_http_server(&self) -> Result<()> {
|
||||
let mut http_handle = self.http_server_handle.lock().await;
|
||||
|
||||
if http_handle.is_some() {
|
||||
// The handle is automatically dropped, which should stop the server
|
||||
*http_handle = None;
|
||||
Ok(())
|
||||
} else {
|
||||
bail!("No HTTP/RPC server is currently running")
|
||||
}
|
||||
}
|
||||
}
|
||||
265
components/zinit/src/app/mod.rs
Normal file
265
components/zinit/src/app/mod.rs
Normal file
@@ -0,0 +1,265 @@
|
||||
pub mod api;
|
||||
pub mod rpc;
|
||||
|
||||
use crate::zinit;
|
||||
use anyhow::{Context, Result};
|
||||
use api::ApiServer;
|
||||
use reth_ipc::client::IpcClientBuilder;
|
||||
use rpc::ZinitLoggingApiClient;
|
||||
use rpc::ZinitServiceApiClient;
|
||||
use rpc::ZinitSystemApiClient;
|
||||
use serde_yaml as encoder;
|
||||
use std::net::ToSocketAddrs;
|
||||
use std::path::{Path, PathBuf};
|
||||
use tokio::fs;
|
||||
use tokio::signal;
|
||||
use tokio::time;
|
||||
use tokio_stream::wrappers::ReceiverStream;
|
||||
use tokio_stream::Stream;
|
||||
|
||||
fn logger(level: log::LevelFilter) -> Result<()> {
|
||||
let logger = fern::Dispatch::new()
|
||||
.format(|out, message, record| {
|
||||
out.finish(format_args!(
|
||||
"zinit: {} ({}) {}",
|
||||
record.level(),
|
||||
record.target(),
|
||||
message
|
||||
))
|
||||
})
|
||||
.level(level)
|
||||
.chain(std::io::stdout());
|
||||
let logger = match std::fs::OpenOptions::new().write(true).open("/dev/kmsg") {
|
||||
Ok(file) => logger.chain(file),
|
||||
Err(_err) => logger,
|
||||
};
|
||||
logger.apply()?;
|
||||
|
||||
Ok(())
|
||||
}
|
||||
|
||||
fn absolute<P: AsRef<Path>>(p: P) -> Result<PathBuf> {
|
||||
let p = p.as_ref();
|
||||
let result = if p.is_absolute() {
|
||||
p.to_path_buf()
|
||||
} else {
|
||||
let mut current = std::env::current_dir()?;
|
||||
current.push(p);
|
||||
current
|
||||
};
|
||||
|
||||
Ok(result)
|
||||
}
|
||||
|
||||
pub async fn init(
|
||||
cap: usize,
|
||||
config: &str,
|
||||
socket: &str,
|
||||
container: bool,
|
||||
debug: bool,
|
||||
) -> Result<ApiServer> {
|
||||
fs::create_dir_all(config)
|
||||
.await
|
||||
.with_context(|| format!("failed to create config directory '{}'", config))?;
|
||||
if let Err(err) = logger(if debug {
|
||||
log::LevelFilter::Debug
|
||||
} else {
|
||||
log::LevelFilter::Info
|
||||
}) {
|
||||
eprintln!("failed to setup logging: {}", err);
|
||||
}
|
||||
|
||||
let config = absolute(Path::new(config)).context("failed to get config dire absolute path")?;
|
||||
let socket_path =
|
||||
absolute(Path::new(socket)).context("failed to get socket file absolute path")?;
|
||||
|
||||
if let Some(dir) = socket_path.parent() {
|
||||
fs::create_dir_all(dir)
|
||||
.await
|
||||
.with_context(|| format!("failed to create directory {:?}", dir))?;
|
||||
}
|
||||
|
||||
let _ = fs::remove_file(&socket).await;
|
||||
|
||||
debug!("switching to home dir: {}", config.display());
|
||||
std::env::set_current_dir(&config).with_context(|| {
|
||||
format!(
|
||||
"failed to switch working directory to '{}'",
|
||||
config.display()
|
||||
)
|
||||
})?;
|
||||
|
||||
let init = zinit::ZInit::new(cap, container);
|
||||
|
||||
init.serve();
|
||||
|
||||
let services = zinit::config::load_dir(&config)?;
|
||||
for (k, v) in services {
|
||||
if let Err(err) = init.monitor(&k, v).await {
|
||||
error!("failed to monitor service {}: {}", k, err);
|
||||
};
|
||||
}
|
||||
let a = api::Api::new(init);
|
||||
a.serve(socket.into()).await
|
||||
}
|
||||
|
||||
pub async fn list(socket: &str) -> Result<()> {
|
||||
let client = IpcClientBuilder::default().build(socket.into()).await?;
|
||||
let results = client.list().await?;
|
||||
encoder::to_writer(std::io::stdout(), &results)?;
|
||||
Ok(())
|
||||
}
|
||||
|
||||
pub async fn shutdown(socket: &str) -> Result<()> {
|
||||
let client = IpcClientBuilder::default().build(socket.into()).await?;
|
||||
client.shutdown().await?;
|
||||
Ok(())
|
||||
}
|
||||
|
||||
pub async fn reboot(socket: &str) -> Result<()> {
|
||||
let client = IpcClientBuilder::default().build(socket.into()).await?;
|
||||
client.reboot().await?;
|
||||
Ok(())
|
||||
}
|
||||
|
||||
pub async fn status(socket: &str, name: String) -> Result<()> {
|
||||
let client = IpcClientBuilder::default().build(socket.into()).await?;
|
||||
let results = client.status(name).await?;
|
||||
encoder::to_writer(std::io::stdout(), &results)?;
|
||||
Ok(())
|
||||
}
|
||||
|
||||
pub async fn start(socket: &str, name: String) -> Result<()> {
|
||||
let client = IpcClientBuilder::default().build(socket.into()).await?;
|
||||
client.start(name).await?;
|
||||
Ok(())
|
||||
}
|
||||
|
||||
pub async fn stop(socket: &str, name: String) -> Result<()> {
|
||||
let client = IpcClientBuilder::default().build(socket.into()).await?;
|
||||
client.stop(name).await?;
|
||||
Ok(())
|
||||
}
|
||||
|
||||
pub async fn restart(socket: &str, name: String) -> Result<()> {
|
||||
let client = IpcClientBuilder::default().build(socket.into()).await?;
|
||||
client.stop(name.clone()).await?;
|
||||
//pull status
|
||||
for _ in 0..20 {
|
||||
let result = client.status(name.clone()).await?;
|
||||
if result.pid == 0 && result.target == "Down" {
|
||||
client.start(name.clone()).await?;
|
||||
return Ok(());
|
||||
}
|
||||
time::sleep(std::time::Duration::from_secs(1)).await;
|
||||
}
|
||||
// process not stopped try to kill it
|
||||
client.kill(name.clone(), "SIGKILL".into()).await?;
|
||||
client.start(name).await?;
|
||||
Ok(())
|
||||
}
|
||||
|
||||
pub async fn forget(socket: &str, name: String) -> Result<()> {
|
||||
let client = IpcClientBuilder::default().build(socket.into()).await?;
|
||||
client.forget(name).await?;
|
||||
Ok(())
|
||||
}
|
||||
|
||||
pub async fn monitor(socket: &str, name: String) -> Result<()> {
|
||||
let client = IpcClientBuilder::default().build(socket.into()).await?;
|
||||
client.monitor(name).await?;
|
||||
Ok(())
|
||||
}
|
||||
|
||||
pub async fn kill(socket: &str, name: String, signal: String) -> Result<()> {
|
||||
let client = IpcClientBuilder::default().build(socket.into()).await?;
|
||||
client.kill(name, signal).await?;
|
||||
Ok(())
|
||||
}
|
||||
|
||||
pub async fn stats(socket: &str, name: String) -> Result<()> {
|
||||
let client = IpcClientBuilder::default().build(socket.into()).await?;
|
||||
let results = client.stats(name).await?;
|
||||
encoder::to_writer(std::io::stdout(), &results)?;
|
||||
Ok(())
|
||||
}
|
||||
|
||||
pub async fn logs(
|
||||
socket: &str,
|
||||
filter: Option<String>,
|
||||
follow: bool,
|
||||
) -> Result<impl Stream<Item = String> + Unpin> {
|
||||
let client = IpcClientBuilder::default().build(socket.into()).await?;
|
||||
if let Some(ref filter) = filter {
|
||||
client.status(filter.clone()).await?;
|
||||
}
|
||||
let logs = client.logs(filter.clone()).await?;
|
||||
let (tx, rx) = tokio::sync::mpsc::channel(2000);
|
||||
|
||||
let logs_sub = if follow {
|
||||
Some(client.log_subscribe(filter).await?)
|
||||
} else {
|
||||
None
|
||||
};
|
||||
tokio::task::spawn(async move {
|
||||
for log in logs {
|
||||
if tx.send(log).await.is_err() {
|
||||
if let Some(logs_sub) = logs_sub {
|
||||
let _ = logs_sub.unsubscribe().await;
|
||||
}
|
||||
// error means receiver is dead, so just quit
|
||||
return;
|
||||
}
|
||||
}
|
||||
let Some(mut logs_sub) = logs_sub else { return };
|
||||
loop {
|
||||
match logs_sub.next().await {
|
||||
Some(Ok(log)) => {
|
||||
if tx.send(log).await.is_err() {
|
||||
let _ = logs_sub.unsubscribe().await;
|
||||
return;
|
||||
}
|
||||
}
|
||||
Some(Err(e)) => {
|
||||
log::error!("Failed to get new log from subscription: {e}");
|
||||
return;
|
||||
}
|
||||
_ => return,
|
||||
}
|
||||
}
|
||||
});
|
||||
|
||||
Ok(ReceiverStream::new(rx))
|
||||
}
|
||||
|
||||
/// Start an HTTP/RPC proxy server for the Zinit API at the specified address
|
||||
pub async fn proxy(sock: &str, address: String) -> Result<()> {
|
||||
// Parse the socket address
|
||||
let _socket_addr = address
|
||||
.to_socket_addrs()
|
||||
.context("Failed to parse socket address")?
|
||||
.next()
|
||||
.context("No valid socket address found")?;
|
||||
|
||||
println!("Starting HTTP/RPC server on {}", address);
|
||||
println!("Connecting to Zinit daemon at {}", sock);
|
||||
|
||||
// Connect to the existing Zinit daemon through the Unix socket
|
||||
let client = IpcClientBuilder::default().build(sock.into()).await?;
|
||||
|
||||
// Issue an RPC call to start the HTTP server on the specified address
|
||||
let result = client.start_http_server(address.clone()).await?;
|
||||
|
||||
println!("{}", result);
|
||||
println!("Press Ctrl+C to stop");
|
||||
|
||||
// Wait for Ctrl+C to shutdown
|
||||
signal::ctrl_c().await?;
|
||||
|
||||
// Shutdown the HTTP server
|
||||
client.stop_http_server().await?;
|
||||
|
||||
println!("HTTP/RPC server stopped");
|
||||
|
||||
Ok(())
|
||||
}
|
||||
426
components/zinit/src/app/rpc.rs
Normal file
426
components/zinit/src/app/rpc.rs
Normal file
@@ -0,0 +1,426 @@
|
||||
use crate::app::api::{ChildStats, Stats, Status};
|
||||
use crate::zinit::config;
|
||||
use async_trait::async_trait;
|
||||
use jsonrpsee::core::{RpcResult, SubscriptionResult};
|
||||
use jsonrpsee::proc_macros::rpc;
|
||||
use jsonrpsee::types::{ErrorCode, ErrorObjectOwned};
|
||||
use jsonrpsee::PendingSubscriptionSink;
|
||||
use serde_json::{Map, Value};
|
||||
use std::collections::HashMap;
|
||||
use std::str::FromStr;
|
||||
use tokio_stream::StreamExt;
|
||||
|
||||
use super::api::Api;
|
||||
|
||||
// Custom error codes for Zinit
|
||||
const SERVICE_NOT_FOUND: i32 = -32000;
|
||||
const SERVICE_IS_UP: i32 = -32002;
|
||||
const SHUTTING_DOWN: i32 = -32006;
|
||||
const SERVICE_ALREADY_EXISTS: i32 = -32007;
|
||||
const SERVICE_FILE_ERROR: i32 = -32008;
|
||||
|
||||
// Include the OpenRPC specification
|
||||
const OPENRPC_SPEC: &str = include_str!("../../openrpc.json");
|
||||
|
||||
/// RPC methods for discovery.
|
||||
#[rpc(server, client)]
|
||||
pub trait ZinitRpcApi {
|
||||
/// Returns the OpenRPC specification as a string.
|
||||
#[method(name = "rpc.discover")]
|
||||
async fn discover(&self) -> RpcResult<String>;
|
||||
}
|
||||
|
||||
#[async_trait]
|
||||
impl ZinitRpcApiServer for Api {
|
||||
async fn discover(&self) -> RpcResult<String> {
|
||||
Ok(OPENRPC_SPEC.to_string())
|
||||
}
|
||||
}
|
||||
|
||||
/// RPC methods for service management.
|
||||
#[rpc(server, client, namespace = "service")]
|
||||
pub trait ZinitServiceApi {
|
||||
/// List all monitored services and their current state.
|
||||
/// Returns a map where keys are service names and values are state strings.
|
||||
#[method(name = "list")]
|
||||
async fn list(&self) -> RpcResult<HashMap<String, String>>;
|
||||
|
||||
/// Get the detailed status of a specific service.
|
||||
#[method(name = "status")]
|
||||
async fn status(&self, name: String) -> RpcResult<Status>;
|
||||
|
||||
/// Start a specific service.
|
||||
#[method(name = "start")]
|
||||
async fn start(&self, name: String) -> RpcResult<()>;
|
||||
|
||||
/// Stop a specific service.
|
||||
#[method(name = "stop")]
|
||||
async fn stop(&self, name: String) -> RpcResult<()>;
|
||||
|
||||
/// Load and monitor a new service from its configuration file (e.g., "service_name.yaml").
|
||||
#[method(name = "monitor")]
|
||||
async fn monitor(&self, name: String) -> RpcResult<()>;
|
||||
|
||||
/// Stop monitoring a service and remove it from management.
|
||||
#[method(name = "forget")]
|
||||
async fn forget(&self, name: String) -> RpcResult<()>;
|
||||
|
||||
/// Send a signal (e.g., "SIGTERM", "SIGKILL") to a specific service process.
|
||||
#[method(name = "kill")]
|
||||
async fn kill(&self, name: String, signal: String) -> RpcResult<()>;
|
||||
|
||||
/// Create a new service configuration file (e.g., "service_name.yaml")
|
||||
/// with the provided content (JSON map representing YAML structure).
|
||||
/// Returns a success message string.
|
||||
#[method(name = "create")]
|
||||
async fn create(&self, name: String, content: Map<String, Value>) -> RpcResult<String>;
|
||||
|
||||
/// Delete a service configuration file.
|
||||
/// Returns a success message string.
|
||||
#[method(name = "delete")]
|
||||
async fn delete(&self, name: String) -> RpcResult<String>;
|
||||
|
||||
/// Get the content of a service configuration file as a JSON Value.
|
||||
#[method(name = "get")]
|
||||
async fn get(&self, name: String) -> RpcResult<Value>;
|
||||
|
||||
/// Get memory and CPU usage statistics for a service.
|
||||
#[method(name = "stats")]
|
||||
async fn stats(&self, name: String) -> RpcResult<Stats>;
|
||||
}
|
||||
|
||||
#[async_trait]
|
||||
impl ZinitServiceApiServer for Api {
|
||||
async fn list(&self) -> RpcResult<HashMap<String, String>> {
|
||||
let services = self
|
||||
.zinit
|
||||
.list()
|
||||
.await
|
||||
.map_err(|_| ErrorObjectOwned::from(ErrorCode::InternalError))?;
|
||||
|
||||
let mut map: HashMap<String, String> = HashMap::new();
|
||||
for service in services {
|
||||
let state = self
|
||||
.zinit
|
||||
.status(&service)
|
||||
.await
|
||||
.map_err(|_| ErrorObjectOwned::from(ErrorCode::InternalError))?;
|
||||
map.insert(service, format!("{:?}", state.state));
|
||||
}
|
||||
Ok(map)
|
||||
}
|
||||
|
||||
async fn status(&self, name: String) -> RpcResult<Status> {
|
||||
let status = self
|
||||
.zinit
|
||||
.status(&name)
|
||||
.await
|
||||
.map_err(|_| ErrorObjectOwned::from(ErrorCode::InternalError))?;
|
||||
|
||||
let result = Status {
|
||||
name: name.clone(),
|
||||
pid: status.pid.as_raw() as u32,
|
||||
state: format!("{:?}", status.state),
|
||||
target: format!("{:?}", status.target),
|
||||
after: {
|
||||
let mut after = HashMap::new();
|
||||
for service in status.service.after {
|
||||
let status = match self.zinit.status(&service).await {
|
||||
Ok(dep) => dep.state,
|
||||
Err(_) => crate::zinit::State::Unknown,
|
||||
};
|
||||
after.insert(service, format!("{:?}", status));
|
||||
}
|
||||
after
|
||||
},
|
||||
};
|
||||
|
||||
Ok(result)
|
||||
}
|
||||
|
||||
async fn start(&self, name: String) -> RpcResult<()> {
|
||||
self.zinit
|
||||
.start(name)
|
||||
.await
|
||||
.map_err(|_| ErrorObjectOwned::from(ErrorCode::ServerError(SERVICE_IS_UP)))
|
||||
}
|
||||
|
||||
async fn stop(&self, name: String) -> RpcResult<()> {
|
||||
self.zinit
|
||||
.stop(name)
|
||||
.await
|
||||
.map_err(|_| ErrorObjectOwned::from(ErrorCode::InternalError))
|
||||
}
|
||||
|
||||
async fn monitor(&self, name: String) -> RpcResult<()> {
|
||||
if let Ok((name_str, service)) = config::load(format!("{}.yaml", name))
|
||||
.map_err(|_| ErrorObjectOwned::from(ErrorCode::InternalError))
|
||||
{
|
||||
self.zinit
|
||||
.monitor(name_str, service)
|
||||
.await
|
||||
.map_err(|_| ErrorObjectOwned::from(ErrorCode::InternalError))
|
||||
} else {
|
||||
Err(ErrorObjectOwned::from(ErrorCode::InternalError))
|
||||
}
|
||||
}
|
||||
|
||||
async fn forget(&self, name: String) -> RpcResult<()> {
|
||||
self.zinit
|
||||
.forget(name)
|
||||
.await
|
||||
.map_err(|_| ErrorObjectOwned::from(ErrorCode::InternalError))
|
||||
}
|
||||
|
||||
async fn kill(&self, name: String, signal: String) -> RpcResult<()> {
|
||||
if let Ok(sig) = nix::sys::signal::Signal::from_str(&signal.to_uppercase()) {
|
||||
self.zinit
|
||||
.kill(name, sig)
|
||||
.await
|
||||
.map_err(|_e| ErrorObjectOwned::from(ErrorCode::InternalError))
|
||||
} else {
|
||||
Err(ErrorObjectOwned::from(ErrorCode::InternalError))
|
||||
}
|
||||
}
|
||||
|
||||
async fn create(&self, name: String, content: Map<String, Value>) -> RpcResult<String> {
|
||||
use std::fs;
|
||||
use std::io::Write;
|
||||
use std::path::PathBuf;
|
||||
|
||||
// Validate service name (no path traversal, valid characters)
|
||||
if name.contains('/') || name.contains('\\') || name.contains('.') {
|
||||
return Err(ErrorObjectOwned::from(ErrorCode::InternalError));
|
||||
}
|
||||
|
||||
// Construct the file path
|
||||
let file_path = PathBuf::from(format!("{}.yaml", name));
|
||||
|
||||
// Check if the service file already exists
|
||||
if file_path.exists() {
|
||||
return Err(ErrorObjectOwned::from(ErrorCode::ServerError(
|
||||
SERVICE_ALREADY_EXISTS,
|
||||
)));
|
||||
}
|
||||
|
||||
// Convert the JSON content to YAML
|
||||
let yaml_content = serde_yaml::to_string(&content)
|
||||
.map_err(|_| ErrorObjectOwned::from(ErrorCode::InternalError))?;
|
||||
|
||||
// Write the YAML content to the file
|
||||
let mut file = fs::File::create(&file_path)
|
||||
.map_err(|_| ErrorObjectOwned::from(ErrorCode::ServerError(SERVICE_FILE_ERROR)))?;
|
||||
|
||||
file.write_all(yaml_content.as_bytes())
|
||||
.map_err(|_| ErrorObjectOwned::from(ErrorCode::ServerError(SERVICE_FILE_ERROR)))?;
|
||||
|
||||
Ok(format!("Service '{}' created successfully", name))
|
||||
}
|
||||
|
||||
async fn delete(&self, name: String) -> RpcResult<String> {
|
||||
use std::fs;
|
||||
use std::path::PathBuf;
|
||||
|
||||
// Validate service name (no path traversal, valid characters)
|
||||
if name.contains('/') || name.contains('\\') || name.contains('.') {
|
||||
return Err(ErrorObjectOwned::from(ErrorCode::InternalError));
|
||||
}
|
||||
|
||||
// Construct the file path
|
||||
let file_path = PathBuf::from(format!("{}.yaml", name));
|
||||
|
||||
// Check if the service file exists
|
||||
if !file_path.exists() {
|
||||
return Err(ErrorObjectOwned::from(ErrorCode::ServerError(
|
||||
SERVICE_NOT_FOUND,
|
||||
)));
|
||||
}
|
||||
|
||||
// Delete the file
|
||||
fs::remove_file(&file_path)
|
||||
.map_err(|_| ErrorObjectOwned::from(ErrorCode::ServerError(SERVICE_FILE_ERROR)))?;
|
||||
|
||||
Ok(format!("Service '{}' deleted successfully", name))
|
||||
}
|
||||
|
||||
async fn get(&self, name: String) -> RpcResult<Value> {
|
||||
use std::fs;
|
||||
use std::path::PathBuf;
|
||||
|
||||
// Validate service name (no path traversal, valid characters)
|
||||
if name.contains('/') || name.contains('\\') || name.contains('.') {
|
||||
return Err(ErrorObjectOwned::from(ErrorCode::InternalError));
|
||||
}
|
||||
|
||||
// Construct the file path
|
||||
let file_path = PathBuf::from(format!("{}.yaml", name));
|
||||
|
||||
// Check if the service file exists
|
||||
if !file_path.exists() {
|
||||
return Err(ErrorObjectOwned::from(ErrorCode::ServerError(
|
||||
SERVICE_NOT_FOUND,
|
||||
)));
|
||||
}
|
||||
|
||||
// Read the file content
|
||||
let yaml_content = fs::read_to_string(&file_path)
|
||||
.map_err(|_| ErrorObjectOwned::from(ErrorCode::ServerError(SERVICE_FILE_ERROR)))?;
|
||||
|
||||
// Parse YAML to JSON
|
||||
let yaml_value: serde_yaml::Value = serde_yaml::from_str(&yaml_content)
|
||||
.map_err(|_| ErrorObjectOwned::from(ErrorCode::InternalError))?;
|
||||
|
||||
// Convert YAML value to JSON value
|
||||
let json_value = serde_json::to_value(yaml_value)
|
||||
.map_err(|_| ErrorObjectOwned::from(ErrorCode::InternalError))?;
|
||||
|
||||
Ok(json_value)
|
||||
}
|
||||
|
||||
async fn stats(&self, name: String) -> RpcResult<Stats> {
|
||||
let stats = self
|
||||
.zinit
|
||||
.stats(&name)
|
||||
.await
|
||||
.map_err(|_| ErrorObjectOwned::from(ErrorCode::InternalError))?;
|
||||
|
||||
let result = Stats {
|
||||
name: name.clone(),
|
||||
pid: stats.pid as u32,
|
||||
memory_usage: stats.memory_usage,
|
||||
cpu_usage: stats.cpu_usage,
|
||||
children: stats
|
||||
.children
|
||||
.into_iter()
|
||||
.map(|child| ChildStats {
|
||||
pid: child.pid as u32,
|
||||
memory_usage: child.memory_usage,
|
||||
cpu_usage: child.cpu_usage,
|
||||
})
|
||||
.collect(),
|
||||
};
|
||||
|
||||
Ok(result)
|
||||
}
|
||||
}
|
||||
|
||||
/// RPC methods for system-level operations.
|
||||
#[rpc(server, client, namespace = "system")]
|
||||
pub trait ZinitSystemApi {
|
||||
/// Initiate system shutdown process.
|
||||
#[method(name = "shutdown")]
|
||||
async fn shutdown(&self) -> RpcResult<()>;
|
||||
|
||||
/// Initiate system reboot process.
|
||||
#[method(name = "reboot")]
|
||||
async fn reboot(&self) -> RpcResult<()>;
|
||||
|
||||
/// Start an HTTP/RPC server at the specified address
|
||||
#[method(name = "start_http_server")]
|
||||
async fn start_http_server(&self, address: String) -> RpcResult<String>;
|
||||
|
||||
/// Stop the HTTP/RPC server if running
|
||||
#[method(name = "stop_http_server")]
|
||||
async fn stop_http_server(&self) -> RpcResult<()>;
|
||||
}
|
||||
|
||||
#[async_trait]
|
||||
impl ZinitSystemApiServer for Api {
|
||||
async fn shutdown(&self) -> RpcResult<()> {
|
||||
self.zinit
|
||||
.shutdown()
|
||||
.await
|
||||
.map_err(|_e| ErrorObjectOwned::from(ErrorCode::ServerError(SHUTTING_DOWN)))
|
||||
}
|
||||
|
||||
async fn reboot(&self) -> RpcResult<()> {
|
||||
self.zinit
|
||||
.reboot()
|
||||
.await
|
||||
.map_err(|_| ErrorObjectOwned::from(ErrorCode::InternalError))
|
||||
}
|
||||
|
||||
async fn start_http_server(&self, address: String) -> RpcResult<String> {
|
||||
// Call the method from the API implementation
|
||||
match crate::app::api::Api::start_http_server(self, address).await {
|
||||
Ok(result) => Ok(result),
|
||||
Err(_) => Err(ErrorObjectOwned::from(ErrorCode::InternalError)),
|
||||
}
|
||||
}
|
||||
|
||||
async fn stop_http_server(&self) -> RpcResult<()> {
|
||||
// Call the method from the API implementation
|
||||
match crate::app::api::Api::stop_http_server(self).await {
|
||||
Ok(_) => Ok(()),
|
||||
Err(_) => Err(ErrorObjectOwned::from(ErrorCode::InternalError)),
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
/// RPC subscription methods for streaming data.
|
||||
#[rpc(server, client, namespace = "stream")]
|
||||
pub trait ZinitLoggingApi {
|
||||
#[method(name = "currentLogs")]
|
||||
async fn logs(&self, name: Option<String>) -> RpcResult<Vec<String>>;
|
||||
/// Subscribe to log messages generated by zinit and monitored services.
|
||||
/// An optional filter can be provided to only receive logs containing the filter string.
|
||||
/// The subscription returns a stream of log lines (String).
|
||||
#[subscription(name = "subscribeLogs", item = String)]
|
||||
async fn log_subscribe(&self, filter: Option<String>) -> SubscriptionResult;
|
||||
}
|
||||
|
||||
#[async_trait]
|
||||
impl ZinitLoggingApiServer for Api {
|
||||
async fn logs(&self, name: Option<String>) -> RpcResult<Vec<String>> {
|
||||
let filter = name.map(|n| format!("{n}:"));
|
||||
Ok(
|
||||
tokio_stream::wrappers::ReceiverStream::new(self.zinit.logs(true, false).await)
|
||||
.filter_map(|l| {
|
||||
if let Some(ref filter) = filter {
|
||||
if l[4..].starts_with(filter) {
|
||||
Some(l.to_string())
|
||||
} else {
|
||||
None
|
||||
}
|
||||
} else {
|
||||
Some(l.to_string())
|
||||
}
|
||||
})
|
||||
.collect()
|
||||
.await,
|
||||
)
|
||||
}
|
||||
|
||||
async fn log_subscribe(
|
||||
&self,
|
||||
sink: PendingSubscriptionSink,
|
||||
name: Option<String>,
|
||||
) -> SubscriptionResult {
|
||||
let sink = sink.accept().await?;
|
||||
let filter = name.map(|n| format!("{n}:"));
|
||||
let mut stream =
|
||||
tokio_stream::wrappers::ReceiverStream::new(self.zinit.logs(false, true).await)
|
||||
.filter_map(|l| {
|
||||
if let Some(ref filter) = filter {
|
||||
if l[4..].starts_with(filter) {
|
||||
Some(l.to_string())
|
||||
} else {
|
||||
None
|
||||
}
|
||||
} else {
|
||||
Some(l.to_string())
|
||||
}
|
||||
});
|
||||
while let Some(log) = stream.next().await {
|
||||
if sink
|
||||
.send(serde_json::value::to_raw_value(&log)?)
|
||||
.await
|
||||
.is_err()
|
||||
{
|
||||
break;
|
||||
}
|
||||
}
|
||||
|
||||
Ok(())
|
||||
}
|
||||
}
|
||||
172
components/zinit/src/bin/testapp.rs
Normal file
172
components/zinit/src/bin/testapp.rs
Normal file
@@ -0,0 +1,172 @@
|
||||
#[tokio::main]
|
||||
async fn main() {
|
||||
println!("hello from testapp");
|
||||
}
|
||||
|
||||
// extern crate zinit;
|
||||
|
||||
// use anyhow::Result;
|
||||
// use serde_json::json;
|
||||
// use std::env;
|
||||
// use tokio::time::{sleep, Duration};
|
||||
|
||||
// use zinit::testapp;
|
||||
|
||||
// #[tokio::main]
|
||||
// async fn main() -> Result<()> {
|
||||
// // Define paths for socket and config
|
||||
// let temp_dir = env::temp_dir();
|
||||
// let socket_path = temp_dir
|
||||
// .join("zinit-test.sock")
|
||||
// .to_str()
|
||||
// .unwrap()
|
||||
// .to_string();
|
||||
// let config_dir = temp_dir
|
||||
// .join("zinit-test-config")
|
||||
// .to_str()
|
||||
// .unwrap()
|
||||
// .to_string();
|
||||
|
||||
// println!("Starting zinit with socket at: {}", socket_path);
|
||||
// println!("Using config directory: {}", config_dir);
|
||||
|
||||
// // Start zinit in the background
|
||||
// testapp::start_zinit(&socket_path, &config_dir).await?;
|
||||
|
||||
// // Wait for zinit to initialize
|
||||
// sleep(Duration::from_secs(2)).await;
|
||||
|
||||
// // Create a client to communicate with zinit
|
||||
// let client = Client::new(&socket_path);
|
||||
|
||||
// // Create service configurations
|
||||
// println!("Creating service configurations...");
|
||||
|
||||
// // Create a find service
|
||||
// testapp::create_service_config(
|
||||
// &config_dir,
|
||||
// "find-service",
|
||||
// "find / -name \"*.txt\" -type f",
|
||||
// )
|
||||
// .await?;
|
||||
|
||||
// // Create a sleep service with echo
|
||||
// testapp::create_service_config(
|
||||
// &config_dir,
|
||||
// "sleep-service",
|
||||
// "sh -c 'echo Starting sleep; sleep 30; echo Finished sleep'",
|
||||
// )
|
||||
// .await?;
|
||||
|
||||
// // Wait for zinit to load the configurations
|
||||
// sleep(Duration::from_secs(1)).await;
|
||||
|
||||
// // Tell zinit to monitor our services
|
||||
// println!("Monitoring services...");
|
||||
// client.monitor("find-service").await?;
|
||||
// client.monitor("sleep-service").await?;
|
||||
|
||||
// // List all services
|
||||
// println!("\nListing all services:");
|
||||
// let services = client.list().await?;
|
||||
// for (name, status) in services {
|
||||
// println!("Service: {} - Status: {}", name, status);
|
||||
// }
|
||||
|
||||
// // Start the find service
|
||||
// println!("\nStarting find-service...");
|
||||
// client.start("find-service").await?;
|
||||
|
||||
// // Wait a bit and check status
|
||||
// sleep(Duration::from_secs(2)).await;
|
||||
// let status = client.status("find-service").await?;
|
||||
// println!("find-service status: {:?}", status);
|
||||
|
||||
// // Start the sleep service
|
||||
// println!("\nStarting sleep-service...");
|
||||
// client.start("sleep-service").await?;
|
||||
|
||||
// // Wait a bit and check status
|
||||
// sleep(Duration::from_secs(2)).await;
|
||||
// let status = client.status("sleep-service").await?;
|
||||
// println!("sleep-service status: {:?}", status);
|
||||
|
||||
// // Stop the find service
|
||||
// println!("\nStopping find-service...");
|
||||
// client.stop("find-service").await?;
|
||||
|
||||
// // Wait a bit and check status
|
||||
// sleep(Duration::from_secs(2)).await;
|
||||
// let status = client.status("find-service").await?;
|
||||
// println!("find-service status after stopping: {:?}", status);
|
||||
|
||||
// // Kill the sleep service with SIGTERM
|
||||
// println!("\nKilling sleep-service with SIGTERM...");
|
||||
// client.kill("sleep-service", "SIGTERM").await?;
|
||||
|
||||
// // Wait a bit and check status
|
||||
// sleep(Duration::from_secs(2)).await;
|
||||
// let status = client.status("sleep-service").await?;
|
||||
// println!("sleep-service status after killing: {:?}", status);
|
||||
|
||||
// // Cleanup - forget services
|
||||
// println!("\nForgetting services...");
|
||||
// if status.pid == 0 {
|
||||
// // Only forget if it's not running
|
||||
// client.forget("sleep-service").await?;
|
||||
// }
|
||||
// client.forget("find-service").await?;
|
||||
|
||||
// // Demonstrate service file operations
|
||||
// println!("\nDemonstrating service file operations...");
|
||||
|
||||
// // Create a new service using the API
|
||||
// println!("Creating a new service via API...");
|
||||
// let service_content = json!({
|
||||
// "exec": "echo 'Hello from API-created service'",
|
||||
// "oneshot": true,
|
||||
// "log": "stdout"
|
||||
// })
|
||||
// .as_object()
|
||||
// .unwrap()
|
||||
// .clone();
|
||||
|
||||
// let result = client
|
||||
// .create_service("api-service", service_content)
|
||||
// .await?;
|
||||
// println!("Create service result: {}", result);
|
||||
|
||||
// // Get the service configuration
|
||||
// println!("\nGetting service configuration...");
|
||||
// let config = client.get_service("api-service").await?;
|
||||
// println!(
|
||||
// "Service configuration: {}",
|
||||
// serde_json::to_string_pretty(&config)?
|
||||
// );
|
||||
|
||||
// // Monitor and start the new service
|
||||
// println!("\nMonitoring and starting the new service...");
|
||||
// client.monitor("api-service").await?;
|
||||
// client.start("api-service").await?;
|
||||
|
||||
// // Wait a bit and check status
|
||||
// sleep(Duration::from_secs(2)).await;
|
||||
// let status = client.status("api-service").await?;
|
||||
// println!("api-service status: {:?}", status);
|
||||
|
||||
// // Delete the service
|
||||
// println!("\nDeleting the service...");
|
||||
// if status.pid == 0 {
|
||||
// // Only forget if it's not running
|
||||
// client.forget("api-service").await?;
|
||||
// let result = client.delete_service("api-service").await?;
|
||||
// println!("Delete service result: {}", result);
|
||||
// }
|
||||
|
||||
// // Shutdown zinit
|
||||
// println!("\nShutting down zinit...");
|
||||
// client.shutdown().await?;
|
||||
|
||||
// println!("\nTest completed successfully!");
|
||||
// Ok(())
|
||||
// }
|
||||
11
components/zinit/src/lib.rs
Normal file
11
components/zinit/src/lib.rs
Normal file
@@ -0,0 +1,11 @@
|
||||
extern crate serde;
|
||||
#[macro_use]
|
||||
extern crate anyhow;
|
||||
#[macro_use]
|
||||
extern crate log;
|
||||
extern crate tokio;
|
||||
|
||||
pub mod app;
|
||||
pub mod manager;
|
||||
pub mod testapp;
|
||||
pub mod zinit;
|
||||
281
components/zinit/src/main.rs
Normal file
281
components/zinit/src/main.rs
Normal file
@@ -0,0 +1,281 @@
|
||||
extern crate zinit;
|
||||
|
||||
use anyhow::Result;
|
||||
use clap::{App, Arg, SubCommand};
|
||||
use git_version::git_version;
|
||||
|
||||
use tokio_stream::StreamExt;
|
||||
use zinit::app;
|
||||
|
||||
const GIT_VERSION: &str = git_version!(args = ["--tags", "--always", "--dirty=-modified"]);
|
||||
|
||||
#[tokio::main]
|
||||
async fn main() -> Result<()> {
|
||||
let matches = App::new("zinit")
|
||||
.author("ThreeFold Tech, https://github.com/threefoldtech")
|
||||
.version(GIT_VERSION)
|
||||
.about("A runit replacement")
|
||||
.arg(Arg::with_name("socket").value_name("SOCKET").short("s").long("socket").default_value("/tmp/zinit.sock").help("path to unix socket"))
|
||||
.arg(Arg::with_name("debug").short("d").long("debug").help("run in debug mode"))
|
||||
.subcommand(
|
||||
SubCommand::with_name("init")
|
||||
.arg(
|
||||
Arg::with_name("config")
|
||||
.value_name("DIR")
|
||||
.short("c")
|
||||
.long("config")
|
||||
.help("service configurations directory"),
|
||||
)
|
||||
.arg(
|
||||
Arg::with_name("buffer")
|
||||
.value_name("BUFFER")
|
||||
.short("b")
|
||||
.long("buffer")
|
||||
.help("buffer size (in lines) to keep services logs")
|
||||
.default_value("2000")
|
||||
)
|
||||
.arg(Arg::with_name("container").long("container").help("run in container mode, shutdown on signal"))
|
||||
.about("run in init mode, start and maintain configured services"),
|
||||
)
|
||||
.subcommand(
|
||||
SubCommand::with_name("list")
|
||||
.about("quick view of current known services and their status"),
|
||||
)
|
||||
.subcommand(
|
||||
SubCommand::with_name("shutdown")
|
||||
.about("stop all services and power off"),
|
||||
)
|
||||
.subcommand(
|
||||
SubCommand::with_name("reboot")
|
||||
.about("stop all services and reboot"),
|
||||
)
|
||||
.subcommand(
|
||||
SubCommand::with_name("status")
|
||||
.arg(
|
||||
Arg::with_name("service")
|
||||
.value_name("SERVICE")
|
||||
.required(true)
|
||||
.help("service name"),
|
||||
)
|
||||
.about("show detailed service status"),
|
||||
)
|
||||
.subcommand(
|
||||
SubCommand::with_name("stop")
|
||||
.arg(
|
||||
Arg::with_name("service")
|
||||
.value_name("SERVICE")
|
||||
.required(true)
|
||||
.help("service name"),
|
||||
)
|
||||
.about("stop service"),
|
||||
)
|
||||
.subcommand(
|
||||
SubCommand::with_name("start")
|
||||
.arg(
|
||||
Arg::with_name("service")
|
||||
.value_name("SERVICE")
|
||||
.required(true)
|
||||
.help("service name"),
|
||||
)
|
||||
.about("start service. has no effect if the service is already running"),
|
||||
)
|
||||
.subcommand(
|
||||
SubCommand::with_name("forget")
|
||||
.arg(
|
||||
Arg::with_name("service")
|
||||
.value_name("SERVICE")
|
||||
.required(true)
|
||||
.help("service name"),
|
||||
)
|
||||
.about("forget a service. you can only forget a stopped service"),
|
||||
)
|
||||
.subcommand(
|
||||
SubCommand::with_name("monitor")
|
||||
.arg(
|
||||
Arg::with_name("service")
|
||||
.value_name("SERVICE")
|
||||
.required(true)
|
||||
.help("service name"),
|
||||
)
|
||||
.about("start monitoring a service. configuration is loaded from server config directory"),
|
||||
)
|
||||
.subcommand(
|
||||
SubCommand::with_name("log")
|
||||
.arg(
|
||||
Arg::with_name("snapshot")
|
||||
.short("s")
|
||||
.long("snapshot")
|
||||
.required(false)
|
||||
.help("if set log prints current buffer without following")
|
||||
)
|
||||
.arg(
|
||||
Arg::with_name("filter")
|
||||
.value_name("FILTER")
|
||||
.required(false)
|
||||
.help("an optional 'exact' service name")
|
||||
)
|
||||
.about("view services logs from zinit ring buffer"),
|
||||
)
|
||||
.subcommand(
|
||||
SubCommand::with_name("kill")
|
||||
.arg(
|
||||
Arg::with_name("service")
|
||||
.value_name("SERVICE")
|
||||
.required(true)
|
||||
.help("service name"),
|
||||
)
|
||||
.arg(
|
||||
Arg::with_name("signal")
|
||||
.value_name("SIGNAL")
|
||||
.required(true)
|
||||
.default_value("SIGTERM")
|
||||
.help("signal name (example: SIGTERM)"),
|
||||
)
|
||||
.about("send a signal to a running service."),
|
||||
)
|
||||
.subcommand(
|
||||
SubCommand::with_name("restart")
|
||||
.arg(
|
||||
Arg::with_name("service")
|
||||
.value_name("SERVICE")
|
||||
.required(true)
|
||||
.help("service name"),
|
||||
)
|
||||
.about("restart a service."),
|
||||
)
|
||||
.subcommand(
|
||||
SubCommand::with_name("stats")
|
||||
.arg(
|
||||
Arg::with_name("service")
|
||||
.value_name("SERVICE")
|
||||
.required(true)
|
||||
.help("service name"),
|
||||
)
|
||||
.about("show memory and CPU usage statistics for a service"),
|
||||
)
|
||||
.subcommand(
|
||||
SubCommand::with_name("proxy")
|
||||
.arg(
|
||||
Arg::with_name("address")
|
||||
.value_name("ADDRESS")
|
||||
.short("a")
|
||||
.long("address")
|
||||
.default_value("127.0.0.1:8080")
|
||||
.help("address to bind the HTTP/RPC server to"),
|
||||
)
|
||||
.about("start an HTTP/RPC proxy for Zinit API"),
|
||||
)
|
||||
.get_matches();
|
||||
|
||||
use dirs; // Add this import
|
||||
|
||||
let socket = matches.value_of("socket").unwrap();
|
||||
let debug = matches.is_present("debug");
|
||||
|
||||
let config_path = if let Some(config_arg) = matches.value_of("config") {
|
||||
config_arg.to_string()
|
||||
} else {
|
||||
#[cfg(target_os = "macos")]
|
||||
{
|
||||
let home_dir = dirs::home_dir()
|
||||
.ok_or_else(|| anyhow::anyhow!("Could not determine home directory"))?;
|
||||
home_dir
|
||||
.join("hero")
|
||||
.join("cfg")
|
||||
.join("zinit")
|
||||
.to_str()
|
||||
.ok_or_else(|| anyhow::anyhow!("Invalid path for config directory"))?
|
||||
.to_string()
|
||||
}
|
||||
#[cfg(not(target_os = "macos"))]
|
||||
{
|
||||
"/etc/zinit/".to_string()
|
||||
}
|
||||
};
|
||||
|
||||
let result = match matches.subcommand() {
|
||||
("init", Some(matches)) => {
|
||||
let _server = app::init(
|
||||
matches.value_of("buffer").unwrap().parse().unwrap(),
|
||||
&config_path, // Use the determined config_path
|
||||
socket,
|
||||
matches.is_present("container"),
|
||||
debug,
|
||||
)
|
||||
.await?;
|
||||
tokio::signal::ctrl_c().await?;
|
||||
Ok(())
|
||||
}
|
||||
("list", _) => app::list(socket).await,
|
||||
("shutdown", _) => app::shutdown(socket).await,
|
||||
("reboot", _) => app::reboot(socket).await,
|
||||
// ("log", Some(matches)) => app::log(matches.value_of("filter")),
|
||||
("status", Some(matches)) => {
|
||||
app::status(socket, matches.value_of("service").unwrap().to_string()).await
|
||||
}
|
||||
("stop", Some(matches)) => {
|
||||
app::stop(socket, matches.value_of("service").unwrap().to_string()).await
|
||||
}
|
||||
("start", Some(matches)) => {
|
||||
app::start(socket, matches.value_of("service").unwrap().to_string()).await
|
||||
}
|
||||
("forget", Some(matches)) => {
|
||||
app::forget(socket, matches.value_of("service").unwrap().to_string()).await
|
||||
}
|
||||
("monitor", Some(matches)) => {
|
||||
app::monitor(socket, matches.value_of("service").unwrap().to_string()).await
|
||||
}
|
||||
("kill", Some(matches)) => {
|
||||
app::kill(
|
||||
socket,
|
||||
matches.value_of("service").unwrap().to_string(),
|
||||
matches.value_of("signal").unwrap().to_string(),
|
||||
)
|
||||
.await
|
||||
}
|
||||
("log", Some(matches)) => {
|
||||
let mut stream = app::logs(
|
||||
socket,
|
||||
matches.value_of("filter").map(|s| s.to_string()),
|
||||
!matches.is_present("snapshot"),
|
||||
)
|
||||
.await?;
|
||||
|
||||
loop {
|
||||
tokio::select! {
|
||||
item = stream.next() => {
|
||||
match item {
|
||||
Some(log_entry) => {
|
||||
println!("{}", log_entry);
|
||||
},
|
||||
None => break
|
||||
}
|
||||
}
|
||||
_ = tokio::signal::ctrl_c() => {
|
||||
break
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
Ok(())
|
||||
}
|
||||
("restart", Some(matches)) => {
|
||||
app::restart(socket, matches.value_of("service").unwrap().to_string()).await
|
||||
}
|
||||
("stats", Some(matches)) => {
|
||||
app::stats(socket, matches.value_of("service").unwrap().to_string()).await
|
||||
}
|
||||
("proxy", Some(matches)) => {
|
||||
app::proxy(socket, matches.value_of("address").unwrap().to_string()).await
|
||||
}
|
||||
_ => app::list(socket).await, // default command
|
||||
};
|
||||
|
||||
match result {
|
||||
Ok(_) => Ok(()),
|
||||
Err(e) => {
|
||||
eprintln!("{:#}", e);
|
||||
std::process::exit(1);
|
||||
}
|
||||
}
|
||||
}
|
||||
149
components/zinit/src/manager/buffer.rs
Normal file
149
components/zinit/src/manager/buffer.rs
Normal file
@@ -0,0 +1,149 @@
|
||||
use anyhow::Result;
|
||||
use std::sync::Arc;
|
||||
use tokio::sync::broadcast;
|
||||
use tokio::sync::broadcast::error::RecvError;
|
||||
use tokio::sync::{mpsc, Mutex};
|
||||
|
||||
struct Buffer<T> {
|
||||
inner: Vec<T>,
|
||||
at: usize,
|
||||
}
|
||||
|
||||
impl<T: Clone> Buffer<T> {
|
||||
pub fn new(cap: usize) -> Buffer<T> {
|
||||
Buffer {
|
||||
inner: Vec::with_capacity(cap),
|
||||
at: 0,
|
||||
}
|
||||
}
|
||||
|
||||
fn len(&self) -> usize {
|
||||
self.inner.len()
|
||||
}
|
||||
|
||||
pub fn cap(&self) -> usize {
|
||||
self.inner.capacity()
|
||||
}
|
||||
|
||||
pub fn push(&mut self, o: T) {
|
||||
if self.len() < self.cap() {
|
||||
self.inner.push(o);
|
||||
} else {
|
||||
self.inner[self.at] = o;
|
||||
}
|
||||
|
||||
self.at = (self.at + 1) % self.cap();
|
||||
}
|
||||
}
|
||||
|
||||
impl<'a, T: 'a> IntoIterator for &'a Buffer<T> {
|
||||
type IntoIter = BufferIter<'a, T>;
|
||||
type Item = &'a T;
|
||||
|
||||
fn into_iter(self) -> Self::IntoIter {
|
||||
let (second, first) = self.inner.split_at(self.at);
|
||||
|
||||
BufferIter {
|
||||
first,
|
||||
second,
|
||||
index: 0,
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
pub struct BufferIter<'a, T> {
|
||||
first: &'a [T],
|
||||
second: &'a [T],
|
||||
index: usize,
|
||||
}
|
||||
|
||||
impl<'a, T> Iterator for BufferIter<'a, T> {
|
||||
type Item = &'a T;
|
||||
|
||||
fn next(&mut self) -> Option<Self::Item> {
|
||||
let index = self.index;
|
||||
self.index += 1;
|
||||
if index < self.first.len() {
|
||||
Some(&self.first[index])
|
||||
} else if index - self.first.len() < self.second.len() {
|
||||
Some(&self.second[index - self.first.len()])
|
||||
} else {
|
||||
None
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
pub type Logs = mpsc::Receiver<Arc<String>>;
|
||||
|
||||
#[derive(Clone)]
|
||||
pub struct Ring {
|
||||
buffer: Arc<Mutex<Buffer<Arc<String>>>>,
|
||||
sender: broadcast::Sender<Arc<String>>,
|
||||
}
|
||||
|
||||
impl Ring {
|
||||
pub fn new(cap: usize) -> Ring {
|
||||
let (tx, _) = broadcast::channel(100);
|
||||
Ring {
|
||||
buffer: Arc::new(Mutex::new(Buffer::new(cap))),
|
||||
sender: tx,
|
||||
}
|
||||
}
|
||||
|
||||
pub async fn push(&self, line: String) -> Result<()> {
|
||||
let line = Arc::new(line.clone());
|
||||
self.buffer.lock().await.push(Arc::clone(&line));
|
||||
self.sender.send(line)?;
|
||||
Ok(())
|
||||
}
|
||||
|
||||
/// stream returns a continues stream that first receive
|
||||
/// a snapshot of the current buffer.
|
||||
/// then if follow is true the logs stream will remain
|
||||
/// open and fed each received line forever until the
|
||||
/// received closed the channel from its end.
|
||||
pub async fn stream(&self, existing_logs: bool, follow: bool) -> Logs {
|
||||
let (tx, stream) = mpsc::channel::<Arc<String>>(100);
|
||||
let mut rx = self.sender.subscribe();
|
||||
|
||||
let buffer = if existing_logs {
|
||||
// Get current exisiting logs
|
||||
self.buffer
|
||||
.lock()
|
||||
.await
|
||||
.into_iter()
|
||||
.cloned()
|
||||
.collect::<Vec<_>>()
|
||||
} else {
|
||||
// Don't care about existing logs
|
||||
vec![]
|
||||
};
|
||||
|
||||
tokio::spawn(async move {
|
||||
for item in buffer {
|
||||
let _ = tx.send(Arc::clone(&item)).await;
|
||||
}
|
||||
|
||||
if !follow {
|
||||
return;
|
||||
}
|
||||
|
||||
loop {
|
||||
let line = match rx.recv().await {
|
||||
Ok(line) => line,
|
||||
Err(RecvError::Closed) => break,
|
||||
Err(RecvError::Lagged(n)) => {
|
||||
Arc::new(format!("[-] zinit: {} lines dropped", n))
|
||||
}
|
||||
};
|
||||
|
||||
if tx.send(line).await.is_err() {
|
||||
// client disconnected.
|
||||
break;
|
||||
}
|
||||
}
|
||||
});
|
||||
|
||||
stream
|
||||
}
|
||||
}
|
||||
253
components/zinit/src/manager/mod.rs
Normal file
253
components/zinit/src/manager/mod.rs
Normal file
@@ -0,0 +1,253 @@
|
||||
use std::collections::HashMap;
|
||||
|
||||
use anyhow::{Context, Result};
|
||||
use command_group::CommandGroup;
|
||||
use nix::sys::signal;
|
||||
use nix::sys::wait::{self, WaitStatus};
|
||||
use nix::unistd::Pid;
|
||||
use std::fs::File as StdFile;
|
||||
use std::os::unix::io::FromRawFd;
|
||||
use std::os::unix::io::IntoRawFd;
|
||||
use std::process::Command;
|
||||
use std::process::Stdio;
|
||||
use std::sync::Arc;
|
||||
use tokio::fs::File;
|
||||
use tokio::io::AsyncBufReadExt;
|
||||
use tokio::io::BufReader;
|
||||
use tokio::signal::unix;
|
||||
use tokio::sync::oneshot;
|
||||
use tokio::sync::Mutex;
|
||||
|
||||
mod buffer;
|
||||
pub use buffer::Logs;
|
||||
|
||||
pub struct Process {
|
||||
cmd: String,
|
||||
env: HashMap<String, String>,
|
||||
cwd: String,
|
||||
}
|
||||
type WaitChannel = oneshot::Receiver<WaitStatus>;
|
||||
|
||||
pub struct Child {
|
||||
pub pid: Pid,
|
||||
ch: WaitChannel,
|
||||
}
|
||||
|
||||
impl Child {
|
||||
pub fn new(pid: Pid, ch: WaitChannel) -> Child {
|
||||
Child { pid, ch }
|
||||
}
|
||||
|
||||
pub async fn wait(self) -> Result<WaitStatus> {
|
||||
Ok(self.ch.await?)
|
||||
}
|
||||
}
|
||||
|
||||
type Handler = oneshot::Sender<WaitStatus>;
|
||||
|
||||
impl Process {
|
||||
pub fn new<S: Into<String>>(cmd: S, cwd: S, env: Option<HashMap<String, String>>) -> Process {
|
||||
let env = env.unwrap_or_default();
|
||||
|
||||
Process {
|
||||
env,
|
||||
cmd: cmd.into(),
|
||||
cwd: cwd.into(),
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
#[derive(Clone)]
|
||||
pub enum Log {
|
||||
None,
|
||||
Stdout,
|
||||
Ring(String),
|
||||
}
|
||||
|
||||
#[derive(Clone)]
|
||||
pub struct ProcessManager {
|
||||
table: Arc<Mutex<HashMap<Pid, Handler>>>,
|
||||
ring: buffer::Ring,
|
||||
env: Environ,
|
||||
}
|
||||
|
||||
impl ProcessManager {
|
||||
pub fn new(cap: usize) -> ProcessManager {
|
||||
ProcessManager {
|
||||
table: Arc::new(Mutex::new(HashMap::new())),
|
||||
ring: buffer::Ring::new(cap),
|
||||
env: Environ::new(),
|
||||
}
|
||||
}
|
||||
|
||||
fn wait_process() -> Vec<WaitStatus> {
|
||||
let mut statuses: Vec<WaitStatus> = Vec::new();
|
||||
loop {
|
||||
let status = match wait::waitpid(Option::None, Some(wait::WaitPidFlag::WNOHANG)) {
|
||||
Ok(status) => status,
|
||||
Err(_) => {
|
||||
return statuses;
|
||||
}
|
||||
};
|
||||
match status {
|
||||
WaitStatus::StillAlive => break,
|
||||
_ => statuses.push(status),
|
||||
}
|
||||
}
|
||||
statuses
|
||||
}
|
||||
|
||||
pub fn start(&self) {
|
||||
let table = Arc::clone(&self.table);
|
||||
let mut signals = match unix::signal(unix::SignalKind::child()) {
|
||||
Ok(s) => s,
|
||||
Err(err) => {
|
||||
panic!("failed to bind to signals: {}", err);
|
||||
}
|
||||
};
|
||||
|
||||
tokio::spawn(async move {
|
||||
loop {
|
||||
signals.recv().await;
|
||||
let mut table = table.lock().await;
|
||||
for exited in Self::wait_process() {
|
||||
if let Some(pid) = exited.pid() {
|
||||
if let Some(sender) = table.remove(&pid) {
|
||||
if sender.send(exited).is_err() {
|
||||
debug!("failed to send exit state to process: {}", pid);
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
});
|
||||
}
|
||||
|
||||
fn sink(&self, file: File, prefix: String) {
|
||||
let ring = self.ring.clone();
|
||||
let reader = BufReader::new(file);
|
||||
|
||||
tokio::spawn(async move {
|
||||
let mut lines = reader.lines();
|
||||
while let Ok(line) = lines.next_line().await {
|
||||
let _ = match line {
|
||||
Some(line) => ring.push(format!("{}: {}", prefix, line)).await,
|
||||
None => break,
|
||||
};
|
||||
}
|
||||
});
|
||||
}
|
||||
|
||||
pub async fn stream(&self, existing_logs: bool, follow: bool) -> Logs {
|
||||
self.ring.stream(existing_logs, follow).await
|
||||
}
|
||||
|
||||
pub fn signal(&self, pid: Pid, sig: signal::Signal) -> Result<()> {
|
||||
Ok(signal::killpg(pid, sig)?)
|
||||
}
|
||||
|
||||
pub async fn run(&self, cmd: Process, log: Log) -> Result<Child> {
|
||||
let args = shlex::split(&cmd.cmd).context("failed to parse command")?;
|
||||
if args.is_empty() {
|
||||
bail!("invalid command");
|
||||
}
|
||||
|
||||
let mut child = Command::new(&args[0]);
|
||||
|
||||
let child = if !cmd.cwd.is_empty() {
|
||||
child.current_dir(&cmd.cwd)
|
||||
} else {
|
||||
child.current_dir("/")
|
||||
};
|
||||
|
||||
let child = child.args(&args[1..]).envs(&self.env.0).envs(cmd.env);
|
||||
|
||||
let child = match log {
|
||||
Log::None => child.stdout(Stdio::null()).stderr(Stdio::null()),
|
||||
Log::Ring(_) => child.stdout(Stdio::piped()).stderr(Stdio::piped()),
|
||||
_ => child, // default to inherit
|
||||
};
|
||||
|
||||
let mut table = self.table.lock().await;
|
||||
|
||||
let mut child = child
|
||||
.group_spawn()
|
||||
.context("failed to spawn command")?
|
||||
.into_inner();
|
||||
|
||||
if let Log::Ring(prefix) = log {
|
||||
let _ = self
|
||||
.ring
|
||||
.push(format!("[-] {}: ------------ [start] ------------", prefix))
|
||||
.await;
|
||||
|
||||
if let Some(out) = child.stdout.take() {
|
||||
let out = File::from_std(unsafe { StdFile::from_raw_fd(out.into_raw_fd()) });
|
||||
self.sink(out, format!("[+] {}", prefix))
|
||||
}
|
||||
|
||||
if let Some(out) = child.stderr.take() {
|
||||
let out = File::from_std(unsafe { StdFile::from_raw_fd(out.into_raw_fd()) });
|
||||
self.sink(out, format!("[-] {}", prefix))
|
||||
}
|
||||
}
|
||||
|
||||
let (tx, rx) = oneshot::channel();
|
||||
|
||||
let id = child.id();
|
||||
|
||||
let pid = Pid::from_raw(id as i32);
|
||||
table.insert(pid, tx);
|
||||
|
||||
Ok(Child::new(pid, rx))
|
||||
}
|
||||
}
|
||||
|
||||
#[derive(Clone)]
|
||||
struct Environ(HashMap<String, String>);
|
||||
|
||||
impl Environ {
|
||||
fn new() -> Environ {
|
||||
let env = match Environ::parse("/etc/environment") {
|
||||
Ok(r) => r,
|
||||
Err(err) => {
|
||||
error!("failed to load /etc/environment file: {}", err);
|
||||
HashMap::new()
|
||||
}
|
||||
};
|
||||
|
||||
Environ(env)
|
||||
}
|
||||
|
||||
fn parse<P>(p: P) -> Result<HashMap<String, String>, std::io::Error>
|
||||
where
|
||||
P: AsRef<std::path::Path>,
|
||||
{
|
||||
let mut m = HashMap::new();
|
||||
let txt = match std::fs::read_to_string(p) {
|
||||
Ok(txt) => txt,
|
||||
Err(err) if err.kind() == std::io::ErrorKind::NotFound => {
|
||||
info!("skipping /etc/environment file because it does not exist");
|
||||
"".into()
|
||||
}
|
||||
Err(err) => return Err(err),
|
||||
};
|
||||
|
||||
for line in txt.lines() {
|
||||
let line = line.trim();
|
||||
if line.starts_with('#') {
|
||||
continue;
|
||||
}
|
||||
let parts: Vec<&str> = line.splitn(2, '=').collect();
|
||||
let key = String::from(parts[0]);
|
||||
let value = match parts.len() {
|
||||
2 => String::from(parts[1]),
|
||||
_ => String::default(),
|
||||
};
|
||||
//m.into_iter()
|
||||
m.insert(key, value);
|
||||
}
|
||||
|
||||
Ok(m)
|
||||
}
|
||||
}
|
||||
264
components/zinit/src/testapp/main.rs
Normal file
264
components/zinit/src/testapp/main.rs
Normal file
@@ -0,0 +1,264 @@
|
||||
use anyhow::{Context, Result};
|
||||
use std::path::Path;
|
||||
use tokio::time::{sleep, Duration};
|
||||
use std::env;
|
||||
use tokio::process::Command;
|
||||
use tokio::fs;
|
||||
use std::process::Stdio;
|
||||
use serde::{Deserialize, Serialize};
|
||||
use serde_json;
|
||||
use tokio::net::UnixStream;
|
||||
use tokio::io::{AsyncBufReadExt, AsyncReadExt, AsyncWriteExt, BufStream};
|
||||
use std::collections::HashMap;
|
||||
|
||||
#[derive(Clone, Debug, Deserialize, Serialize)]
|
||||
#[serde(rename_all = "lowercase")]
|
||||
struct Response {
|
||||
pub state: State,
|
||||
pub body: serde_json::Value,
|
||||
}
|
||||
|
||||
#[derive(Clone, Debug, Deserialize, Serialize)]
|
||||
#[serde(rename_all = "lowercase")]
|
||||
enum State {
|
||||
Ok,
|
||||
Error,
|
||||
}
|
||||
|
||||
#[derive(Clone, Debug, Deserialize, Serialize)]
|
||||
#[serde(rename_all = "lowercase")]
|
||||
struct Status {
|
||||
pub name: String,
|
||||
pub pid: u32,
|
||||
pub state: String,
|
||||
pub target: String,
|
||||
pub after: HashMap<String, String>,
|
||||
}
|
||||
|
||||
struct Client {
|
||||
socket: String,
|
||||
}
|
||||
|
||||
impl Client {
|
||||
pub fn new(socket: &str) -> Client {
|
||||
Client {
|
||||
socket: socket.to_string(),
|
||||
}
|
||||
}
|
||||
|
||||
async fn connect(&self) -> Result<UnixStream> {
|
||||
UnixStream::connect(&self.socket).await.with_context(|| {
|
||||
format!(
|
||||
"failed to connect to '{}'. is zinit listening on that socket?",
|
||||
self.socket
|
||||
)
|
||||
})
|
||||
}
|
||||
|
||||
async fn command(&self, c: &str) -> Result<serde_json::Value> {
|
||||
let mut con = BufStream::new(self.connect().await?);
|
||||
|
||||
let _ = con.write(c.as_bytes()).await?;
|
||||
let _ = con.write(b"\n").await?;
|
||||
con.flush().await?;
|
||||
|
||||
let mut data = String::new();
|
||||
con.read_to_string(&mut data).await?;
|
||||
|
||||
let response: Response = serde_json::from_str(&data)?;
|
||||
|
||||
match response.state {
|
||||
State::Ok => Ok(response.body),
|
||||
State::Error => {
|
||||
let err: String = serde_json::from_value(response.body)?;
|
||||
anyhow::bail!(err)
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
pub async fn list(&self) -> Result<HashMap<String, String>> {
|
||||
let response = self.command("list").await?;
|
||||
Ok(serde_json::from_value(response)?)
|
||||
}
|
||||
|
||||
pub async fn status<S: AsRef<str>>(&self, name: S) -> Result<Status> {
|
||||
let response = self.command(&format!("status {}", name.as_ref())).await?;
|
||||
Ok(serde_json::from_value(response)?)
|
||||
}
|
||||
|
||||
pub async fn start<S: AsRef<str>>(&self, name: S) -> Result<()> {
|
||||
self.command(&format!("start {}", name.as_ref())).await?;
|
||||
Ok(())
|
||||
}
|
||||
|
||||
pub async fn stop<S: AsRef<str>>(&self, name: S) -> Result<()> {
|
||||
self.command(&format!("stop {}", name.as_ref())).await?;
|
||||
Ok(())
|
||||
}
|
||||
|
||||
pub async fn forget<S: AsRef<str>>(&self, name: S) -> Result<()> {
|
||||
self.command(&format!("forget {}", name.as_ref())).await?;
|
||||
Ok(())
|
||||
}
|
||||
|
||||
pub async fn monitor<S: AsRef<str>>(&self, name: S) -> Result<()> {
|
||||
self.command(&format!("monitor {}", name.as_ref())).await?;
|
||||
Ok(())
|
||||
}
|
||||
|
||||
pub async fn kill<S: AsRef<str>>(&self, name: S, sig: S) -> Result<()> {
|
||||
self.command(&format!("kill {} {}", name.as_ref(), sig.as_ref()))
|
||||
.await?;
|
||||
Ok(())
|
||||
}
|
||||
|
||||
pub async fn shutdown(&self) -> Result<()> {
|
||||
self.command("shutdown").await?;
|
||||
Ok(())
|
||||
}
|
||||
}
|
||||
|
||||
async fn start_zinit(socket_path: &str, config_dir: &str) -> Result<()> {
|
||||
// Create a temporary config directory if it doesn't exist
|
||||
let config_path = Path::new(config_dir);
|
||||
if !config_path.exists() {
|
||||
fs::create_dir_all(config_path).await?;
|
||||
}
|
||||
|
||||
// Start zinit in the background
|
||||
let mut cmd = Command::new("zinit");
|
||||
cmd.arg("--socket")
|
||||
.arg(socket_path)
|
||||
.arg("init")
|
||||
.arg("--config")
|
||||
.arg(config_dir)
|
||||
.stdout(Stdio::piped())
|
||||
.stderr(Stdio::piped());
|
||||
|
||||
let child = cmd.spawn()?;
|
||||
|
||||
// Give zinit some time to start up
|
||||
sleep(Duration::from_secs(1)).await;
|
||||
|
||||
println!("Zinit started with PID: {:?}", child.id());
|
||||
|
||||
Ok(())
|
||||
}
|
||||
|
||||
async fn create_service_config(config_dir: &str, name: &str, command: &str) -> Result<()> {
|
||||
let config_path = format!("{}/{}.yaml", config_dir, name);
|
||||
let config_content = format!(
|
||||
r#"exec: {}
|
||||
oneshot: false
|
||||
shutdown_timeout: 10
|
||||
after: []
|
||||
signal:
|
||||
stop: sigterm
|
||||
log: ring
|
||||
env: {{}}
|
||||
dir: /
|
||||
"#,
|
||||
command
|
||||
);
|
||||
|
||||
fs::write(config_path, config_content).await?;
|
||||
Ok(())
|
||||
}
|
||||
|
||||
#[tokio::main]
|
||||
async fn main() -> Result<()> {
|
||||
// Define paths for socket and config
|
||||
let temp_dir = env::temp_dir();
|
||||
let socket_path = temp_dir.join("zinit-test.sock").to_str().unwrap().to_string();
|
||||
let config_dir = temp_dir.join("zinit-test-config").to_str().unwrap().to_string();
|
||||
|
||||
println!("Starting zinit with socket at: {}", socket_path);
|
||||
println!("Using config directory: {}", config_dir);
|
||||
|
||||
// Start zinit in the background
|
||||
start_zinit(&socket_path, &config_dir).await?;
|
||||
|
||||
// Wait for zinit to initialize
|
||||
sleep(Duration::from_secs(2)).await;
|
||||
|
||||
// Create a client to communicate with zinit
|
||||
let client = Client::new(&socket_path);
|
||||
|
||||
// Create service configurations
|
||||
println!("Creating service configurations...");
|
||||
|
||||
// Create a find service
|
||||
create_service_config(&config_dir, "find-service", "find / -name \"*.txt\" -type f").await?;
|
||||
|
||||
// Create a sleep service with echo
|
||||
create_service_config(
|
||||
&config_dir,
|
||||
"sleep-service",
|
||||
"sh -c 'echo Starting sleep; sleep 30; echo Finished sleep'"
|
||||
).await?;
|
||||
|
||||
// Wait for zinit to load the configurations
|
||||
sleep(Duration::from_secs(1)).await;
|
||||
|
||||
// Tell zinit to monitor our services
|
||||
println!("Monitoring services...");
|
||||
client.monitor("find-service").await?;
|
||||
client.monitor("sleep-service").await?;
|
||||
|
||||
// List all services
|
||||
println!("\nListing all services:");
|
||||
let services = client.list().await?;
|
||||
for (name, status) in services {
|
||||
println!("Service: {} - Status: {}", name, status);
|
||||
}
|
||||
|
||||
// Start the find service
|
||||
println!("\nStarting find-service...");
|
||||
client.start("find-service").await?;
|
||||
|
||||
// Wait a bit and check status
|
||||
sleep(Duration::from_secs(2)).await;
|
||||
let status = client.status("find-service").await?;
|
||||
println!("find-service status: {:?}", status);
|
||||
|
||||
// Start the sleep service
|
||||
println!("\nStarting sleep-service...");
|
||||
client.start("sleep-service").await?;
|
||||
|
||||
// Wait a bit and check status
|
||||
sleep(Duration::from_secs(2)).await;
|
||||
let status = client.status("sleep-service").await?;
|
||||
println!("sleep-service status: {:?}", status);
|
||||
|
||||
// Stop the find service
|
||||
println!("\nStopping find-service...");
|
||||
client.stop("find-service").await?;
|
||||
|
||||
// Wait a bit and check status
|
||||
sleep(Duration::from_secs(2)).await;
|
||||
let status = client.status("find-service").await?;
|
||||
println!("find-service status after stopping: {:?}", status);
|
||||
|
||||
// Kill the sleep service with SIGTERM
|
||||
println!("\nKilling sleep-service with SIGTERM...");
|
||||
client.kill("sleep-service", "SIGTERM").await?;
|
||||
|
||||
// Wait a bit and check status
|
||||
sleep(Duration::from_secs(2)).await;
|
||||
let status = client.status("sleep-service").await?;
|
||||
println!("sleep-service status after killing: {:?}", status);
|
||||
|
||||
// Cleanup - forget services
|
||||
println!("\nForgetting services...");
|
||||
if status.pid == 0 { // Only forget if it's not running
|
||||
client.forget("sleep-service").await?;
|
||||
}
|
||||
client.forget("find-service").await?;
|
||||
|
||||
// Shutdown zinit
|
||||
println!("\nShutting down zinit...");
|
||||
client.shutdown().await?;
|
||||
|
||||
println!("\nTest completed successfully!");
|
||||
Ok(())
|
||||
}
|
||||
57
components/zinit/src/testapp/mod.rs
Normal file
57
components/zinit/src/testapp/mod.rs
Normal file
@@ -0,0 +1,57 @@
|
||||
use anyhow::Result;
|
||||
use std::env;
|
||||
use std::path::Path;
|
||||
use std::process::Stdio;
|
||||
use tokio::process::Command;
|
||||
use tokio::time::{sleep, Duration};
|
||||
|
||||
pub async fn start_zinit(socket_path: &str, config_dir: &str) -> Result<()> {
|
||||
// Create a temporary config directory if it doesn't exist
|
||||
let config_path = Path::new(config_dir);
|
||||
if !config_path.exists() {
|
||||
tokio::fs::create_dir_all(config_path).await?;
|
||||
}
|
||||
|
||||
// Get the path to the zinit binary (use the one we just built)
|
||||
let zinit_path = env::current_dir()?.join("target/debug/zinit");
|
||||
println!("Using zinit binary at: {}", zinit_path.display());
|
||||
|
||||
// Start zinit in the background
|
||||
let mut cmd = Command::new(zinit_path);
|
||||
cmd.arg("--socket")
|
||||
.arg(socket_path)
|
||||
.arg("init")
|
||||
.arg("--config")
|
||||
.arg(config_dir)
|
||||
.stdout(Stdio::piped())
|
||||
.stderr(Stdio::piped());
|
||||
|
||||
let child = cmd.spawn()?;
|
||||
|
||||
// Give zinit some time to start up
|
||||
sleep(Duration::from_secs(1)).await;
|
||||
|
||||
println!("Zinit started with PID: {:?}", child.id());
|
||||
|
||||
Ok(())
|
||||
}
|
||||
|
||||
pub async fn create_service_config(config_dir: &str, name: &str, command: &str) -> Result<()> {
|
||||
let config_path = format!("{}/{}.yaml", config_dir, name);
|
||||
let config_content = format!(
|
||||
r#"exec: {}
|
||||
oneshot: false
|
||||
shutdown_timeout: 10
|
||||
after: []
|
||||
signal:
|
||||
stop: sigterm
|
||||
log: ring
|
||||
env: {{}}
|
||||
dir: /
|
||||
"#,
|
||||
command
|
||||
);
|
||||
|
||||
tokio::fs::write(config_path, config_content).await?;
|
||||
Ok(())
|
||||
}
|
||||
119
components/zinit/src/zinit/config.rs
Normal file
119
components/zinit/src/zinit/config.rs
Normal file
@@ -0,0 +1,119 @@
|
||||
use anyhow::Result;
|
||||
use serde::{Deserialize, Serialize};
|
||||
use serde_yaml as yaml;
|
||||
use std::collections::HashMap;
|
||||
use std::ffi::OsStr;
|
||||
use std::fs::{self, File};
|
||||
use std::path::Path;
|
||||
pub type Services = HashMap<String, Service>;
|
||||
|
||||
pub const DEFAULT_SHUTDOWN_TIMEOUT: u64 = 10; // in seconds
|
||||
|
||||
#[derive(Clone, Debug, Serialize, Deserialize)]
|
||||
#[serde(default)]
|
||||
pub struct Signal {
|
||||
pub stop: String,
|
||||
}
|
||||
|
||||
impl Default for Signal {
|
||||
fn default() -> Self {
|
||||
Signal {
|
||||
stop: String::from("sigterm"),
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
#[derive(Default, Clone, Debug, Deserialize)]
|
||||
#[serde(rename_all = "lowercase")]
|
||||
pub enum Log {
|
||||
None,
|
||||
#[default]
|
||||
Ring,
|
||||
Stdout,
|
||||
}
|
||||
|
||||
fn default_shutdown_timeout_fn() -> u64 {
|
||||
DEFAULT_SHUTDOWN_TIMEOUT
|
||||
}
|
||||
#[derive(Clone, Debug, Default, Deserialize)]
|
||||
#[serde(default)]
|
||||
pub struct Service {
|
||||
/// command to run
|
||||
pub exec: String,
|
||||
/// test command (optional)
|
||||
#[serde(default)]
|
||||
pub test: String,
|
||||
#[serde(rename = "oneshot")]
|
||||
pub one_shot: bool,
|
||||
#[serde(default = "default_shutdown_timeout_fn")]
|
||||
pub shutdown_timeout: u64,
|
||||
pub after: Vec<String>,
|
||||
pub signal: Signal,
|
||||
pub log: Log,
|
||||
pub env: HashMap<String, String>,
|
||||
pub dir: String,
|
||||
}
|
||||
|
||||
impl Service {
|
||||
pub fn validate(&self) -> Result<()> {
|
||||
use nix::sys::signal::Signal;
|
||||
use std::str::FromStr;
|
||||
if self.exec.is_empty() {
|
||||
bail!("missing exec directive");
|
||||
}
|
||||
|
||||
Signal::from_str(&self.signal.stop.to_uppercase())?;
|
||||
|
||||
Ok(())
|
||||
}
|
||||
}
|
||||
/// load loads a single file
|
||||
pub fn load<T: AsRef<Path>>(t: T) -> Result<(String, Service)> {
|
||||
let p = t.as_ref();
|
||||
//todo: can't find a way to shorten this down.
|
||||
let name = match p.file_stem() {
|
||||
Some(name) => match name.to_str() {
|
||||
Some(name) => name,
|
||||
None => bail!("invalid file name: {}", p.to_str().unwrap()),
|
||||
},
|
||||
None => bail!("invalid file name: {}", p.to_str().unwrap()),
|
||||
};
|
||||
|
||||
let file = File::open(p)?;
|
||||
let service: Service = yaml::from_reader(&file)?;
|
||||
service.validate()?;
|
||||
Ok((String::from(name), service))
|
||||
}
|
||||
|
||||
/// walks over a directory and load all configuration files.
|
||||
/// the callback is called with any error that is encountered on loading
|
||||
/// a file, the callback can decide to either ignore the file, or stop
|
||||
/// the directory walking
|
||||
pub fn load_dir<T: AsRef<Path>>(p: T) -> Result<Services> {
|
||||
let mut services: Services = HashMap::new();
|
||||
|
||||
for entry in fs::read_dir(p)? {
|
||||
let entry = entry?;
|
||||
if !entry.file_type()?.is_file() {
|
||||
continue;
|
||||
}
|
||||
|
||||
let fp = entry.path();
|
||||
|
||||
if !matches!(fp.extension(), Some(ext) if ext == OsStr::new("yaml")) {
|
||||
continue;
|
||||
}
|
||||
|
||||
let (name, service) = match load(&fp) {
|
||||
Ok(content) => content,
|
||||
Err(err) => {
|
||||
error!("failed to load config file {:?}: {}", fp, err);
|
||||
continue;
|
||||
}
|
||||
};
|
||||
|
||||
services.insert(name, service);
|
||||
}
|
||||
|
||||
Ok(services)
|
||||
}
|
||||
80
components/zinit/src/zinit/errors.rs
Normal file
80
components/zinit/src/zinit/errors.rs
Normal file
@@ -0,0 +1,80 @@
|
||||
use thiserror::Error;
|
||||
|
||||
/// Errors that can occur in the zinit module
|
||||
#[derive(Error, Debug)]
|
||||
pub enum ZInitError {
|
||||
/// Service name is unknown
|
||||
#[error("service name {name:?} unknown")]
|
||||
UnknownService { name: String },
|
||||
|
||||
/// Service is already being monitored
|
||||
#[error("service {name:?} already monitored")]
|
||||
ServiceAlreadyMonitored { name: String },
|
||||
|
||||
/// Service is up and running
|
||||
#[error("service {name:?} is up")]
|
||||
ServiceIsUp { name: String },
|
||||
|
||||
/// Service is down and not running
|
||||
#[error("service {name:?} is down")]
|
||||
ServiceIsDown { name: String },
|
||||
|
||||
/// Zinit is shutting down
|
||||
#[error("zinit is shutting down")]
|
||||
ShuttingDown,
|
||||
|
||||
/// Invalid state transition
|
||||
#[error("service state transition error: {message}")]
|
||||
InvalidStateTransition { message: String },
|
||||
|
||||
/// Dependency error
|
||||
#[error("dependency error: {message}")]
|
||||
DependencyError { message: String },
|
||||
|
||||
/// Process error
|
||||
#[error("process error: {message}")]
|
||||
ProcessError { message: String },
|
||||
}
|
||||
|
||||
impl ZInitError {
|
||||
/// Create a new UnknownService error
|
||||
pub fn unknown_service<S: Into<String>>(name: S) -> Self {
|
||||
ZInitError::UnknownService { name: name.into() }
|
||||
}
|
||||
|
||||
/// Create a new ServiceAlreadyMonitored error
|
||||
pub fn service_already_monitored<S: Into<String>>(name: S) -> Self {
|
||||
ZInitError::ServiceAlreadyMonitored { name: name.into() }
|
||||
}
|
||||
|
||||
/// Create a new ServiceIsUp error
|
||||
pub fn service_is_up<S: Into<String>>(name: S) -> Self {
|
||||
ZInitError::ServiceIsUp { name: name.into() }
|
||||
}
|
||||
|
||||
/// Create a new ServiceIsDown error
|
||||
pub fn service_is_down<S: Into<String>>(name: S) -> Self {
|
||||
ZInitError::ServiceIsDown { name: name.into() }
|
||||
}
|
||||
|
||||
/// Create a new InvalidStateTransition error
|
||||
pub fn invalid_state_transition<S: Into<String>>(message: S) -> Self {
|
||||
ZInitError::InvalidStateTransition {
|
||||
message: message.into(),
|
||||
}
|
||||
}
|
||||
|
||||
/// Create a new DependencyError error
|
||||
pub fn dependency_error<S: Into<String>>(message: S) -> Self {
|
||||
ZInitError::DependencyError {
|
||||
message: message.into(),
|
||||
}
|
||||
}
|
||||
|
||||
/// Create a new ProcessError error
|
||||
pub fn process_error<S: Into<String>>(message: S) -> Self {
|
||||
ZInitError::ProcessError {
|
||||
message: message.into(),
|
||||
}
|
||||
}
|
||||
}
|
||||
970
components/zinit/src/zinit/lifecycle.rs
Normal file
970
components/zinit/src/zinit/lifecycle.rs
Normal file
@@ -0,0 +1,970 @@
|
||||
use crate::manager::{Log, Logs, Process, ProcessManager};
|
||||
use crate::zinit::config;
|
||||
use crate::zinit::errors::ZInitError;
|
||||
#[cfg(target_os = "linux")]
|
||||
use crate::zinit::ord::{service_dependency_order, ProcessDAG, DUMMY_ROOT};
|
||||
use crate::zinit::service::ZInitService;
|
||||
use crate::zinit::state::{State, Target};
|
||||
#[cfg(target_os = "linux")]
|
||||
use crate::zinit::types::Watcher;
|
||||
use crate::zinit::types::{ProcessStats, ServiceStats, ServiceTable};
|
||||
use std::collections::HashMap;
|
||||
use sysinfo::{self, PidExt, ProcessExt, System, SystemExt};
|
||||
|
||||
// Define a local extension trait for WaitStatus
|
||||
trait WaitStatusExt {
|
||||
fn success(&self) -> bool;
|
||||
}
|
||||
|
||||
impl WaitStatusExt for WaitStatus {
|
||||
fn success(&self) -> bool {
|
||||
matches!(self, WaitStatus::Exited(_, 0))
|
||||
}
|
||||
}
|
||||
use anyhow::Result;
|
||||
#[cfg(target_os = "linux")]
|
||||
use nix::sys::reboot::RebootMode;
|
||||
use nix::sys::signal;
|
||||
use nix::sys::wait::WaitStatus;
|
||||
use nix::unistd::Pid;
|
||||
use std::str::FromStr;
|
||||
use std::sync::Arc;
|
||||
#[cfg(target_os = "linux")]
|
||||
use tokio::sync::mpsc;
|
||||
use tokio::sync::{Notify, RwLock};
|
||||
use tokio::time::sleep;
|
||||
#[cfg(target_os = "linux")]
|
||||
use tokio::time::timeout;
|
||||
#[cfg(target_os = "linux")]
|
||||
use tokio_stream::StreamExt;
|
||||
|
||||
/// Manages the lifecycle of services
|
||||
#[derive(Clone)]
|
||||
pub struct LifecycleManager {
|
||||
/// Process manager for spawning and managing processes
|
||||
pm: ProcessManager,
|
||||
|
||||
/// Table of services
|
||||
services: Arc<RwLock<ServiceTable>>,
|
||||
|
||||
/// Notification for service state changes
|
||||
notify: Arc<Notify>,
|
||||
|
||||
/// Whether the system is shutting down
|
||||
shutdown: Arc<RwLock<bool>>,
|
||||
|
||||
/// Whether running in container mode
|
||||
container: bool,
|
||||
}
|
||||
|
||||
impl LifecycleManager {
|
||||
/// Create a new lifecycle manager
|
||||
pub fn new(
|
||||
pm: ProcessManager,
|
||||
services: Arc<RwLock<ServiceTable>>,
|
||||
notify: Arc<Notify>,
|
||||
shutdown: Arc<RwLock<bool>>,
|
||||
container: bool,
|
||||
) -> Self {
|
||||
Self {
|
||||
pm,
|
||||
services,
|
||||
notify,
|
||||
shutdown,
|
||||
container,
|
||||
}
|
||||
}
|
||||
|
||||
/// Get a reference to the process manager
|
||||
pub fn process_manager(&self) -> &ProcessManager {
|
||||
&self.pm
|
||||
}
|
||||
|
||||
/// Check if running in container mode
|
||||
pub fn is_container_mode(&self) -> bool {
|
||||
self.container
|
||||
}
|
||||
|
||||
/// Get logs from the process manager
|
||||
pub async fn logs(&self, existing_logs: bool, follow: bool) -> Logs {
|
||||
self.pm.stream(existing_logs, follow).await
|
||||
}
|
||||
|
||||
/// Monitor a service
|
||||
pub async fn monitor<S: Into<String>>(&self, name: S, service: config::Service) -> Result<()> {
|
||||
if *self.shutdown.read().await {
|
||||
return Err(ZInitError::ShuttingDown.into());
|
||||
}
|
||||
|
||||
let name = name.into();
|
||||
let mut services = self.services.write().await;
|
||||
|
||||
if services.contains_key(&name) {
|
||||
return Err(ZInitError::service_already_monitored(name).into());
|
||||
}
|
||||
|
||||
let service = Arc::new(RwLock::new(ZInitService::new(service, State::Unknown)));
|
||||
services.insert(name.clone(), Arc::clone(&service));
|
||||
|
||||
let lifecycle = self.clone_lifecycle();
|
||||
debug!("service '{}' monitored", name);
|
||||
tokio::spawn(lifecycle.watch_service(name, service));
|
||||
|
||||
Ok(())
|
||||
}
|
||||
|
||||
/// Get the status of a service
|
||||
pub async fn status<S: AsRef<str>>(
|
||||
&self,
|
||||
name: S,
|
||||
) -> Result<crate::zinit::service::ZInitStatus> {
|
||||
let table = self.services.read().await;
|
||||
let service = table
|
||||
.get(name.as_ref())
|
||||
.ok_or_else(|| ZInitError::unknown_service(name.as_ref()))?;
|
||||
|
||||
let service = service.read().await.status();
|
||||
Ok(service)
|
||||
}
|
||||
|
||||
/// Start a service
|
||||
pub async fn start<S: AsRef<str>>(&self, name: S) -> Result<()> {
|
||||
if *self.shutdown.read().await {
|
||||
return Err(ZInitError::ShuttingDown.into());
|
||||
}
|
||||
|
||||
self.set_service_state(name.as_ref(), None, Some(Target::Up))
|
||||
.await;
|
||||
|
||||
let table = self.services.read().await;
|
||||
let service = table
|
||||
.get(name.as_ref())
|
||||
.ok_or_else(|| ZInitError::unknown_service(name.as_ref()))?;
|
||||
|
||||
let lifecycle = self.clone_lifecycle();
|
||||
tokio::spawn(lifecycle.watch_service(name.as_ref().into(), Arc::clone(service)));
|
||||
|
||||
Ok(())
|
||||
}
|
||||
|
||||
/// Stop a service
|
||||
pub async fn stop<S: AsRef<str>>(&self, name: S) -> Result<()> {
|
||||
// Get service information
|
||||
let table = self.services.read().await;
|
||||
let service = table
|
||||
.get(name.as_ref())
|
||||
.ok_or_else(|| ZInitError::unknown_service(name.as_ref()))?;
|
||||
|
||||
let mut service = service.write().await;
|
||||
service.set_target(Target::Down);
|
||||
|
||||
// Get the main process PID
|
||||
let pid = service.pid;
|
||||
if pid.as_raw() == 0 {
|
||||
return Ok(());
|
||||
}
|
||||
|
||||
// Get the signal to use
|
||||
let signal = signal::Signal::from_str(&service.service.signal.stop.to_uppercase())
|
||||
.map_err(|err| anyhow::anyhow!("unknown stop signal: {}", err))?;
|
||||
|
||||
// Release the lock before potentially long-running operations
|
||||
drop(service);
|
||||
drop(table);
|
||||
|
||||
// Get all child processes using our stats functionality
|
||||
let children = self.get_child_process_stats(pid.as_raw()).await?;
|
||||
|
||||
// First try to stop the process group
|
||||
let _ = self.pm.signal(pid, signal);
|
||||
|
||||
// Wait a short time for processes to terminate gracefully
|
||||
sleep(std::time::Duration::from_millis(500)).await;
|
||||
|
||||
// Check if processes are still running and use SIGKILL if needed
|
||||
self.ensure_processes_terminated(pid.as_raw(), &children)
|
||||
.await?;
|
||||
|
||||
Ok(())
|
||||
}
|
||||
|
||||
/// Ensure that a process and its children are terminated
|
||||
async fn ensure_processes_terminated(
|
||||
&self,
|
||||
parent_pid: i32,
|
||||
children: &[ProcessStats],
|
||||
) -> Result<()> {
|
||||
// Check if parent is still running
|
||||
let parent_running = self.is_process_running(parent_pid).await?;
|
||||
|
||||
// If parent is still running, send SIGKILL
|
||||
if parent_running {
|
||||
debug!(
|
||||
"Process {} still running after SIGTERM, sending SIGKILL",
|
||||
parent_pid
|
||||
);
|
||||
let _ = self
|
||||
.pm
|
||||
.signal(Pid::from_raw(parent_pid), signal::Signal::SIGKILL);
|
||||
}
|
||||
|
||||
// Check and kill any remaining child processes
|
||||
for child in children {
|
||||
if self.is_process_running(child.pid).await? {
|
||||
debug!("Child process {} still running, sending SIGKILL", child.pid);
|
||||
let _ = signal::kill(Pid::from_raw(child.pid), signal::Signal::SIGKILL);
|
||||
}
|
||||
}
|
||||
|
||||
// Verify all processes are gone
|
||||
let mut retries = 5;
|
||||
while retries > 0 {
|
||||
let mut all_terminated = true;
|
||||
|
||||
// Check parent
|
||||
if self.is_process_running(parent_pid).await? {
|
||||
all_terminated = false;
|
||||
}
|
||||
|
||||
// Check children
|
||||
for child in children {
|
||||
if self.is_process_running(child.pid).await? {
|
||||
all_terminated = false;
|
||||
break;
|
||||
}
|
||||
}
|
||||
|
||||
if all_terminated {
|
||||
return Ok(());
|
||||
}
|
||||
|
||||
// Wait before retrying
|
||||
sleep(std::time::Duration::from_millis(100)).await;
|
||||
retries -= 1;
|
||||
}
|
||||
|
||||
// If we get here, some processes might still be running
|
||||
warn!("Some processes may still be running after shutdown attempts");
|
||||
Ok(())
|
||||
}
|
||||
|
||||
/// Check if a process is running
|
||||
async fn is_process_running(&self, pid: i32) -> Result<bool> {
|
||||
// Use sysinfo to check if process exists
|
||||
let mut system = System::new();
|
||||
let sys_pid = sysinfo::Pid::from(pid as usize);
|
||||
system.refresh_process(sys_pid);
|
||||
|
||||
Ok(system.process(sys_pid).is_some())
|
||||
}
|
||||
|
||||
/// Forget a service
|
||||
pub async fn forget<S: AsRef<str>>(&self, name: S) -> Result<()> {
|
||||
let mut table = self.services.write().await;
|
||||
let service = table
|
||||
.get(name.as_ref())
|
||||
.ok_or_else(|| ZInitError::unknown_service(name.as_ref()))?;
|
||||
|
||||
let service = service.read().await;
|
||||
if service.target == Target::Up || service.pid != Pid::from_raw(0) {
|
||||
return Err(ZInitError::service_is_up(name.as_ref()).into());
|
||||
}
|
||||
|
||||
drop(service);
|
||||
table.remove(name.as_ref());
|
||||
|
||||
Ok(())
|
||||
}
|
||||
|
||||
/// Send a signal to a service
|
||||
pub async fn kill<S: AsRef<str>>(&self, name: S, signal: signal::Signal) -> Result<()> {
|
||||
let table = self.services.read().await;
|
||||
let service = table
|
||||
.get(name.as_ref())
|
||||
.ok_or_else(|| ZInitError::unknown_service(name.as_ref()))?;
|
||||
|
||||
let service = service.read().await;
|
||||
if service.pid == Pid::from_raw(0) {
|
||||
return Err(ZInitError::service_is_down(name.as_ref()).into());
|
||||
}
|
||||
|
||||
self.pm.signal(service.pid, signal)
|
||||
}
|
||||
|
||||
/// List all services
|
||||
pub async fn list(&self) -> Result<Vec<String>> {
|
||||
let table = self.services.read().await;
|
||||
Ok(table.keys().map(|k| k.into()).collect())
|
||||
}
|
||||
|
||||
/// Get stats for a service (memory and CPU usage)
|
||||
pub async fn stats<S: AsRef<str>>(&self, name: S) -> Result<ServiceStats> {
|
||||
let table = self.services.read().await;
|
||||
let service = table
|
||||
.get(name.as_ref())
|
||||
.ok_or_else(|| ZInitError::unknown_service(name.as_ref()))?;
|
||||
|
||||
let service = service.read().await;
|
||||
if service.pid.as_raw() == 0 {
|
||||
return Err(ZInitError::service_is_down(name.as_ref()).into());
|
||||
}
|
||||
|
||||
// Get stats for the main process
|
||||
let pid = service.pid.as_raw();
|
||||
let (memory_usage, cpu_usage) = self.get_process_stats(pid).await?;
|
||||
|
||||
// Get stats for child processes
|
||||
let children = self.get_child_process_stats(pid).await?;
|
||||
|
||||
Ok(ServiceStats {
|
||||
memory_usage,
|
||||
cpu_usage,
|
||||
pid,
|
||||
children,
|
||||
})
|
||||
}
|
||||
|
||||
/// Get memory and CPU usage for a process
|
||||
async fn get_process_stats(&self, pid: i32) -> Result<(u64, f32)> {
|
||||
// Create a new System instance with all information
|
||||
let mut system = System::new_all();
|
||||
|
||||
// Convert i32 pid to sysinfo::Pid
|
||||
let sys_pid = sysinfo::Pid::from(pid as usize);
|
||||
|
||||
// Make sure we're refreshing CPU information
|
||||
system.refresh_cpu();
|
||||
system.refresh_processes();
|
||||
|
||||
// First refresh to get initial CPU values
|
||||
system.refresh_all();
|
||||
|
||||
// Wait longer for CPU measurement (500ms instead of 100ms)
|
||||
sleep(std::time::Duration::from_millis(500)).await;
|
||||
|
||||
// Refresh again to get updated CPU values
|
||||
system.refresh_cpu();
|
||||
system.refresh_processes();
|
||||
system.refresh_all();
|
||||
|
||||
// Get the process
|
||||
if let Some(process) = system.process(sys_pid) {
|
||||
// Get memory in bytes
|
||||
let memory_usage = process.memory();
|
||||
|
||||
// Get CPU usage as percentage
|
||||
let cpu_usage = process.cpu_usage();
|
||||
|
||||
Ok((memory_usage, cpu_usage))
|
||||
} else {
|
||||
// Process not found
|
||||
Ok((0, 0.0))
|
||||
}
|
||||
}
|
||||
|
||||
/// Get stats for child processes
|
||||
async fn get_child_process_stats(&self, parent_pid: i32) -> Result<Vec<ProcessStats>> {
|
||||
// Create a new System instance with all processes information
|
||||
let mut system = System::new_all();
|
||||
|
||||
// Make sure we're refreshing CPU information
|
||||
system.refresh_cpu();
|
||||
system.refresh_processes();
|
||||
system.refresh_all();
|
||||
|
||||
// Convert i32 pid to sysinfo::Pid
|
||||
let sys_pid = sysinfo::Pid::from(parent_pid as usize);
|
||||
|
||||
// Wait longer for CPU measurement (500ms instead of 100ms)
|
||||
sleep(std::time::Duration::from_millis(500)).await;
|
||||
|
||||
// Refresh all system information to get updated CPU values
|
||||
system.refresh_cpu();
|
||||
system.refresh_processes();
|
||||
system.refresh_all();
|
||||
|
||||
let mut children = Vec::new();
|
||||
|
||||
// Recursively collect all descendant PIDs
|
||||
let mut descendant_pids = Vec::new();
|
||||
self.collect_descendants(sys_pid, &system.processes(), &mut descendant_pids);
|
||||
|
||||
// Get stats for each child process
|
||||
for &child_pid in &descendant_pids {
|
||||
if let Some(process) = system.process(child_pid) {
|
||||
children.push(ProcessStats {
|
||||
pid: child_pid.as_u32() as i32,
|
||||
memory_usage: process.memory(),
|
||||
cpu_usage: process.cpu_usage(),
|
||||
});
|
||||
}
|
||||
}
|
||||
|
||||
Ok(children)
|
||||
}
|
||||
|
||||
/// Recursively collect all descendant PIDs of a process
|
||||
fn collect_descendants(
|
||||
&self,
|
||||
pid: sysinfo::Pid,
|
||||
procs: &HashMap<sysinfo::Pid, sysinfo::Process>,
|
||||
out: &mut Vec<sysinfo::Pid>,
|
||||
) {
|
||||
for (&child_pid, proc) in procs.iter() {
|
||||
if proc.parent() == Some(pid) {
|
||||
out.push(child_pid);
|
||||
self.collect_descendants(child_pid, procs, out);
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
/// Verify that all processes are terminated
|
||||
async fn verify_all_processes_terminated(&self) -> Result<()> {
|
||||
// Get all services
|
||||
let table = self.services.read().await;
|
||||
|
||||
// Check each service
|
||||
for (name, service) in table.iter() {
|
||||
let service = service.read().await;
|
||||
let pid = service.pid.as_raw();
|
||||
|
||||
// Skip services with no PID
|
||||
if pid == 0 {
|
||||
continue;
|
||||
}
|
||||
|
||||
// Check if the main process is still running
|
||||
if self.is_process_running(pid).await? {
|
||||
warn!(
|
||||
"Service {} (PID {}) is still running after shutdown",
|
||||
name, pid
|
||||
);
|
||||
|
||||
// Try to kill it with SIGKILL
|
||||
let _ = signal::kill(Pid::from_raw(pid), signal::Signal::SIGKILL);
|
||||
}
|
||||
|
||||
// Check for child processes
|
||||
if let Ok(children) = self.get_child_process_stats(pid).await {
|
||||
for child in children {
|
||||
if self.is_process_running(child.pid).await? {
|
||||
warn!(
|
||||
"Child process {} of service {} is still running after shutdown",
|
||||
child.pid, name
|
||||
);
|
||||
|
||||
// Try to kill it with SIGKILL
|
||||
let _ = signal::kill(Pid::from_raw(child.pid), signal::Signal::SIGKILL);
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
Ok(())
|
||||
}
|
||||
|
||||
/// Shutdown the system
|
||||
pub async fn shutdown(&self) -> Result<()> {
|
||||
info!("shutting down");
|
||||
|
||||
// Set the shutdown flag
|
||||
*self.shutdown.write().await = true;
|
||||
|
||||
#[cfg(target_os = "linux")]
|
||||
{
|
||||
// Power off using our enhanced method
|
||||
let result = self.power(RebootMode::RB_POWER_OFF).await;
|
||||
|
||||
// Final verification before exit
|
||||
self.verify_all_processes_terminated().await?;
|
||||
|
||||
return result;
|
||||
}
|
||||
|
||||
#[cfg(not(target_os = "linux"))]
|
||||
{
|
||||
// Stop all services
|
||||
let services = self.list().await?;
|
||||
for service in services {
|
||||
let _ = self.stop(&service).await;
|
||||
}
|
||||
|
||||
// Verify all processes are terminated
|
||||
self.verify_all_processes_terminated().await?;
|
||||
|
||||
if self.container {
|
||||
std::process::exit(0);
|
||||
} else {
|
||||
info!("System shutdown not supported on this platform");
|
||||
std::process::exit(0);
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
/// Reboot the system
|
||||
pub async fn reboot(&self) -> Result<()> {
|
||||
info!("rebooting");
|
||||
|
||||
// Set the shutdown flag
|
||||
*self.shutdown.write().await = true;
|
||||
|
||||
#[cfg(target_os = "linux")]
|
||||
{
|
||||
// Reboot using our enhanced method
|
||||
let result = self.power(RebootMode::RB_AUTOBOOT).await;
|
||||
|
||||
// Final verification before exit
|
||||
self.verify_all_processes_terminated().await?;
|
||||
|
||||
return result;
|
||||
}
|
||||
|
||||
#[cfg(not(target_os = "linux"))]
|
||||
{
|
||||
// Stop all services
|
||||
let services = self.list().await?;
|
||||
for service in services {
|
||||
let _ = self.stop(&service).await;
|
||||
}
|
||||
|
||||
// Verify all processes are terminated
|
||||
self.verify_all_processes_terminated().await?;
|
||||
|
||||
if self.container {
|
||||
std::process::exit(0);
|
||||
} else {
|
||||
info!("System reboot not supported on this platform");
|
||||
std::process::exit(0);
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
/// Power off or reboot the system
|
||||
#[cfg(target_os = "linux")]
|
||||
async fn power(&self, mode: RebootMode) -> Result<()> {
|
||||
*self.shutdown.write().await = true;
|
||||
|
||||
let mut state_channels: HashMap<String, Watcher<State>> = HashMap::new();
|
||||
let mut shutdown_timeouts: HashMap<String, u64> = HashMap::new();
|
||||
|
||||
let table = self.services.read().await;
|
||||
for (name, service) in table.iter() {
|
||||
let service = service.read().await;
|
||||
if service.is_active() {
|
||||
info!("service '{}' is scheduled for a shutdown", name);
|
||||
state_channels.insert(name.into(), service.state_watcher());
|
||||
shutdown_timeouts.insert(name.into(), service.service.shutdown_timeout);
|
||||
}
|
||||
}
|
||||
|
||||
drop(table);
|
||||
let dag = service_dependency_order(self.services.clone()).await;
|
||||
self.kill_process_tree(dag, state_channels, shutdown_timeouts)
|
||||
.await?;
|
||||
|
||||
// On Linux, we can use sync and reboot
|
||||
nix::unistd::sync();
|
||||
if self.container {
|
||||
std::process::exit(0);
|
||||
} else {
|
||||
nix::sys::reboot::reboot(mode)?;
|
||||
}
|
||||
|
||||
Ok(())
|
||||
}
|
||||
|
||||
/// Kill processes in dependency order
|
||||
#[cfg(target_os = "linux")]
|
||||
async fn kill_process_tree(
|
||||
&self,
|
||||
mut dag: ProcessDAG,
|
||||
mut state_channels: HashMap<String, Watcher<State>>,
|
||||
mut shutdown_timeouts: HashMap<String, u64>,
|
||||
) -> Result<()> {
|
||||
let (tx, mut rx) = mpsc::unbounded_channel();
|
||||
tx.send(DUMMY_ROOT.into())?;
|
||||
|
||||
let mut count = dag.count;
|
||||
while let Some(name) = rx.recv().await {
|
||||
debug!(
|
||||
"{} has been killed (or was inactive) adding its children",
|
||||
name
|
||||
);
|
||||
|
||||
for child in dag.adj.get(&name).unwrap_or(&Vec::new()) {
|
||||
let child_indegree: &mut u32 = dag.indegree.entry(child.clone()).or_default();
|
||||
*child_indegree -= 1;
|
||||
|
||||
debug!(
|
||||
"decrementing child {} indegree to {}",
|
||||
child, child_indegree
|
||||
);
|
||||
|
||||
if *child_indegree == 0 {
|
||||
let watcher = state_channels.remove(child);
|
||||
if watcher.is_none() {
|
||||
// not an active service
|
||||
tx.send(child.to_string())?;
|
||||
continue;
|
||||
}
|
||||
|
||||
let shutdown_timeout = shutdown_timeouts.remove(child);
|
||||
let lifecycle = self.clone_lifecycle();
|
||||
|
||||
// Spawn a task to kill the service and wait for it to terminate
|
||||
let kill_task = tokio::spawn(Self::kill_wait_enhanced(
|
||||
lifecycle,
|
||||
child.to_string(),
|
||||
tx.clone(),
|
||||
watcher.unwrap(),
|
||||
shutdown_timeout.unwrap_or(config::DEFAULT_SHUTDOWN_TIMEOUT),
|
||||
));
|
||||
|
||||
// Add a timeout to ensure we don't wait forever
|
||||
let _ = tokio::time::timeout(
|
||||
std::time::Duration::from_secs(
|
||||
shutdown_timeout.unwrap_or(config::DEFAULT_SHUTDOWN_TIMEOUT) + 2,
|
||||
),
|
||||
kill_task,
|
||||
)
|
||||
.await;
|
||||
}
|
||||
}
|
||||
|
||||
count -= 1;
|
||||
if count == 0 {
|
||||
break;
|
||||
}
|
||||
}
|
||||
|
||||
// Final verification that all processes are gone
|
||||
self.verify_all_processes_terminated().await?;
|
||||
|
||||
Ok(())
|
||||
}
|
||||
|
||||
/// Enhanced version of kill_wait that ensures processes are terminated
|
||||
#[cfg(target_os = "linux")]
|
||||
async fn kill_wait_enhanced(
|
||||
self,
|
||||
name: String,
|
||||
ch: mpsc::UnboundedSender<String>,
|
||||
mut rx: Watcher<State>,
|
||||
shutdown_timeout: u64,
|
||||
) {
|
||||
debug!("kill_wait {}", name);
|
||||
|
||||
// Try to stop the service gracefully
|
||||
let stop_result = self.stop(name.clone()).await;
|
||||
|
||||
// Wait for the service to become inactive or timeout
|
||||
let fut = timeout(
|
||||
std::time::Duration::from_secs(shutdown_timeout),
|
||||
async move {
|
||||
while let Some(state) = rx.next().await {
|
||||
if !state.is_active() {
|
||||
return;
|
||||
}
|
||||
}
|
||||
},
|
||||
);
|
||||
|
||||
match stop_result {
|
||||
Ok(_) => {
|
||||
let _ = fut.await;
|
||||
}
|
||||
Err(e) => error!("couldn't stop service {}: {}", name.clone(), e),
|
||||
}
|
||||
|
||||
// Verify the service is actually stopped
|
||||
if let Ok(status) = self.status(&name).await {
|
||||
if status.pid != Pid::from_raw(0) {
|
||||
// Service is still running, try to kill it
|
||||
let _ = self.kill(&name, signal::Signal::SIGKILL).await;
|
||||
}
|
||||
}
|
||||
|
||||
debug!("sending to the death channel {}", name.clone());
|
||||
if let Err(e) = ch.send(name.clone()) {
|
||||
error!(
|
||||
"error: couldn't send the service {} to the shutdown loop: {}",
|
||||
name, e
|
||||
);
|
||||
}
|
||||
}
|
||||
|
||||
/// Original kill_wait for backward compatibility
|
||||
#[cfg(target_os = "linux")]
|
||||
async fn kill_wait(
|
||||
self,
|
||||
name: String,
|
||||
ch: mpsc::UnboundedSender<String>,
|
||||
rx: Watcher<State>,
|
||||
shutdown_timeout: u64,
|
||||
) {
|
||||
Self::kill_wait_enhanced(self, name, ch, rx, shutdown_timeout).await
|
||||
}
|
||||
|
||||
/// Check if a service can be scheduled
|
||||
async fn can_schedule(&self, service: &config::Service) -> bool {
|
||||
let mut can = true;
|
||||
let table = self.services.read().await;
|
||||
|
||||
for dep in service.after.iter() {
|
||||
can = match table.get(dep) {
|
||||
Some(ps) => {
|
||||
let ps = ps.read().await;
|
||||
debug!(
|
||||
"- service {} is {:?} oneshot: {}",
|
||||
dep,
|
||||
ps.get_state(),
|
||||
ps.service.one_shot
|
||||
);
|
||||
|
||||
match ps.get_state() {
|
||||
State::Running if !ps.service.one_shot => true,
|
||||
State::Success => true,
|
||||
_ => false,
|
||||
}
|
||||
}
|
||||
// depending on an undefined service. This still can be resolved later
|
||||
// by monitoring the dependency in the future.
|
||||
None => false,
|
||||
};
|
||||
|
||||
// if state is blocked, we can break the loop
|
||||
if !can {
|
||||
break;
|
||||
}
|
||||
}
|
||||
|
||||
can
|
||||
}
|
||||
|
||||
/// Set the state and/or target of a service
|
||||
async fn set_service_state(&self, name: &str, state: Option<State>, target: Option<Target>) {
|
||||
let table = self.services.read().await;
|
||||
let service = match table.get(name) {
|
||||
Some(service) => service,
|
||||
None => return,
|
||||
};
|
||||
|
||||
let mut service = service.write().await;
|
||||
if let Some(state) = state {
|
||||
service.force_set_state(state);
|
||||
}
|
||||
|
||||
if let Some(target) = target {
|
||||
service.set_target(target);
|
||||
}
|
||||
}
|
||||
|
||||
/// Test if a service is running correctly
|
||||
async fn test_service<S: AsRef<str>>(&self, name: S, cfg: &config::Service) -> Result<bool> {
|
||||
if cfg.test.is_empty() {
|
||||
return Ok(true);
|
||||
}
|
||||
|
||||
let log = match cfg.log {
|
||||
config::Log::None => Log::None,
|
||||
config::Log::Stdout => Log::Stdout,
|
||||
config::Log::Ring => Log::Ring(format!("{}/test", name.as_ref())),
|
||||
};
|
||||
|
||||
let test = self
|
||||
.pm
|
||||
.run(
|
||||
Process::new(&cfg.test, &cfg.dir, Some(cfg.env.clone())),
|
||||
log.clone(),
|
||||
)
|
||||
.await?;
|
||||
|
||||
let status = test.wait().await?;
|
||||
Ok(status.success())
|
||||
}
|
||||
|
||||
/// Run the test loop for a service
|
||||
async fn test_loop(self, name: String, cfg: config::Service) {
|
||||
loop {
|
||||
let result = self.test_service(&name, &cfg).await;
|
||||
|
||||
match result {
|
||||
Ok(result) => {
|
||||
if result {
|
||||
self.set_service_state(&name, Some(State::Running), None)
|
||||
.await;
|
||||
// release
|
||||
self.notify.notify_waiters();
|
||||
return;
|
||||
}
|
||||
// wait before we try again
|
||||
sleep(std::time::Duration::from_secs(2)).await;
|
||||
}
|
||||
Err(_) => {
|
||||
self.set_service_state(&name, Some(State::TestFailure), None)
|
||||
.await;
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
/// Watch a service and manage its lifecycle
|
||||
async fn watch_service(self, name: String, input: Arc<RwLock<ZInitService>>) {
|
||||
let name = name.clone();
|
||||
|
||||
let mut service = input.write().await;
|
||||
if service.target == Target::Down {
|
||||
debug!("service '{}' target is down", name);
|
||||
return;
|
||||
}
|
||||
|
||||
if service.scheduled {
|
||||
debug!("service '{}' already scheduled", name);
|
||||
return;
|
||||
}
|
||||
|
||||
service.scheduled = true;
|
||||
drop(service);
|
||||
|
||||
loop {
|
||||
let name = name.clone();
|
||||
|
||||
let service = input.read().await;
|
||||
// early check if service is down, so we don't have to do extra checks
|
||||
if service.target == Target::Down {
|
||||
// we check target in loop in case service have
|
||||
// been set down.
|
||||
break;
|
||||
}
|
||||
|
||||
let config = service.service.clone();
|
||||
// we need to spawn this service now, but is it ready?
|
||||
// are all dependent services are running?
|
||||
|
||||
// so we drop the table to give other services
|
||||
// chance to acquire the lock and schedule themselves
|
||||
drop(service);
|
||||
|
||||
'checks: loop {
|
||||
let sig = self.notify.notified();
|
||||
debug!("checking {} if it can schedule", name);
|
||||
if self.can_schedule(&config).await {
|
||||
debug!("service {} can schedule", name);
|
||||
break 'checks;
|
||||
}
|
||||
|
||||
self.set_service_state(&name, Some(State::Blocked), None)
|
||||
.await;
|
||||
// don't even care if i am lagging
|
||||
// as long i am notified that some services status
|
||||
// has changed
|
||||
debug!("service {} is blocked, waiting release", name);
|
||||
sig.await;
|
||||
}
|
||||
|
||||
let log = match config.log {
|
||||
config::Log::None => Log::None,
|
||||
config::Log::Stdout => Log::Stdout,
|
||||
config::Log::Ring => Log::Ring(name.clone()),
|
||||
};
|
||||
|
||||
let mut service = input.write().await;
|
||||
// we check again in case target has changed. Since we had to release the lock
|
||||
// earlier to not block locking on this service (for example if a stop was called)
|
||||
// while the service was waiting for dependencies.
|
||||
// the lock is kept until the spawning and the update of the pid.
|
||||
if service.target == Target::Down {
|
||||
// we check target in loop in case service have
|
||||
// been set down.
|
||||
break;
|
||||
}
|
||||
|
||||
let child = self
|
||||
.pm
|
||||
.run(
|
||||
Process::new(&config.exec, &config.dir, Some(config.env.clone())),
|
||||
log.clone(),
|
||||
)
|
||||
.await;
|
||||
|
||||
let child = match child {
|
||||
Ok(child) => {
|
||||
service.force_set_state(State::Spawned);
|
||||
service.set_pid(child.pid);
|
||||
child
|
||||
}
|
||||
Err(err) => {
|
||||
// so, spawning failed. and nothing we can do about it
|
||||
// this can be duo to a bad command or exe not found.
|
||||
// set service to failure.
|
||||
error!("service {} failed to start: {}", name, err);
|
||||
service.force_set_state(State::Failure);
|
||||
break;
|
||||
}
|
||||
};
|
||||
|
||||
if config.one_shot {
|
||||
service.force_set_state(State::Running);
|
||||
}
|
||||
|
||||
// we don't lock here because this can take forever
|
||||
// to finish. so we allow other operation on the service (for example)
|
||||
// status and stop operations.
|
||||
drop(service);
|
||||
|
||||
let mut handler = None;
|
||||
if !config.one_shot {
|
||||
let lifecycle = self.clone_lifecycle();
|
||||
handler = Some(tokio::spawn(
|
||||
lifecycle.test_loop(name.clone(), config.clone()),
|
||||
));
|
||||
}
|
||||
|
||||
let result = child.wait().await;
|
||||
if let Some(handler) = handler {
|
||||
handler.abort();
|
||||
}
|
||||
|
||||
let mut service = input.write().await;
|
||||
service.clear_pid();
|
||||
|
||||
match result {
|
||||
Err(err) => {
|
||||
error!("failed to read service '{}' status: {}", name, err);
|
||||
service.force_set_state(State::Unknown);
|
||||
}
|
||||
Ok(status) => {
|
||||
service.force_set_state(if status.success() {
|
||||
State::Success
|
||||
} else {
|
||||
State::Error(status)
|
||||
});
|
||||
}
|
||||
};
|
||||
|
||||
drop(service);
|
||||
if config.one_shot {
|
||||
// we don't need to restart the service anymore
|
||||
self.notify.notify_waiters();
|
||||
break;
|
||||
}
|
||||
|
||||
// we trying again in 2 seconds
|
||||
sleep(std::time::Duration::from_secs(2)).await;
|
||||
}
|
||||
|
||||
let mut service = input.write().await;
|
||||
service.scheduled = false;
|
||||
}
|
||||
|
||||
/// Clone the lifecycle manager
|
||||
pub fn clone_lifecycle(&self) -> Self {
|
||||
Self {
|
||||
pm: self.pm.clone(),
|
||||
services: Arc::clone(&self.services),
|
||||
notify: Arc::clone(&self.notify),
|
||||
shutdown: Arc::clone(&self.shutdown),
|
||||
container: self.container,
|
||||
}
|
||||
}
|
||||
}
|
||||
119
components/zinit/src/zinit/mod.rs
Normal file
119
components/zinit/src/zinit/mod.rs
Normal file
@@ -0,0 +1,119 @@
|
||||
pub mod config;
|
||||
pub mod errors;
|
||||
pub mod lifecycle;
|
||||
pub mod ord;
|
||||
pub mod service;
|
||||
pub mod state;
|
||||
pub mod types;
|
||||
|
||||
// Re-export commonly used items
|
||||
pub use service::ZInitStatus;
|
||||
pub use state::State;
|
||||
pub use types::{ProcessStats, ServiceStats};
|
||||
|
||||
use crate::manager::{Logs, ProcessManager};
|
||||
use anyhow::Result;
|
||||
use nix::sys::signal;
|
||||
use std::sync::Arc;
|
||||
use tokio::sync::{Notify, RwLock};
|
||||
|
||||
/// Main ZInit service manager
|
||||
#[derive(Clone)]
|
||||
pub struct ZInit {
|
||||
/// Lifecycle manager for service management
|
||||
lifecycle: lifecycle::LifecycleManager,
|
||||
}
|
||||
|
||||
impl ZInit {
|
||||
/// Create a new ZInit instance
|
||||
pub fn new(cap: usize, container: bool) -> ZInit {
|
||||
let pm = ProcessManager::new(cap);
|
||||
let services = Arc::new(RwLock::new(types::ServiceTable::new()));
|
||||
let notify = Arc::new(Notify::new());
|
||||
let shutdown = Arc::new(RwLock::new(false));
|
||||
|
||||
let lifecycle = lifecycle::LifecycleManager::new(pm, services, notify, shutdown, container);
|
||||
|
||||
ZInit { lifecycle }
|
||||
}
|
||||
|
||||
/// Start the service manager
|
||||
pub fn serve(&self) {
|
||||
self.lifecycle.process_manager().start();
|
||||
if self.lifecycle.is_container_mode() {
|
||||
let lifecycle = self.lifecycle.clone_lifecycle();
|
||||
tokio::spawn(async move {
|
||||
use tokio::signal::unix;
|
||||
|
||||
let mut term = unix::signal(unix::SignalKind::terminate()).unwrap();
|
||||
let mut int = unix::signal(unix::SignalKind::interrupt()).unwrap();
|
||||
let mut hup = unix::signal(unix::SignalKind::hangup()).unwrap();
|
||||
|
||||
tokio::select! {
|
||||
_ = term.recv() => {},
|
||||
_ = int.recv() => {},
|
||||
_ = hup.recv() => {},
|
||||
};
|
||||
|
||||
debug!("shutdown signal received");
|
||||
let _ = lifecycle.shutdown().await;
|
||||
});
|
||||
}
|
||||
}
|
||||
|
||||
/// Get logs from the process manager
|
||||
/// `existing_logs` TODO:
|
||||
pub async fn logs(&self, existing_logs: bool, follow: bool) -> Logs {
|
||||
self.lifecycle.logs(existing_logs, follow).await
|
||||
}
|
||||
|
||||
/// Monitor a service
|
||||
pub async fn monitor<S: Into<String>>(&self, name: S, service: config::Service) -> Result<()> {
|
||||
self.lifecycle.monitor(name, service).await
|
||||
}
|
||||
|
||||
/// Get the status of a service
|
||||
pub async fn status<S: AsRef<str>>(&self, name: S) -> Result<ZInitStatus> {
|
||||
self.lifecycle.status(name).await
|
||||
}
|
||||
|
||||
/// Start a service
|
||||
pub async fn start<S: AsRef<str>>(&self, name: S) -> Result<()> {
|
||||
self.lifecycle.start(name).await
|
||||
}
|
||||
|
||||
/// Stop a service
|
||||
pub async fn stop<S: AsRef<str>>(&self, name: S) -> Result<()> {
|
||||
self.lifecycle.stop(name).await
|
||||
}
|
||||
|
||||
/// Forget a service
|
||||
pub async fn forget<S: AsRef<str>>(&self, name: S) -> Result<()> {
|
||||
self.lifecycle.forget(name).await
|
||||
}
|
||||
|
||||
/// Send a signal to a service
|
||||
pub async fn kill<S: AsRef<str>>(&self, name: S, signal: signal::Signal) -> Result<()> {
|
||||
self.lifecycle.kill(name, signal).await
|
||||
}
|
||||
|
||||
/// List all services
|
||||
pub async fn list(&self) -> Result<Vec<String>> {
|
||||
self.lifecycle.list().await
|
||||
}
|
||||
|
||||
/// Shutdown the system
|
||||
pub async fn shutdown(&self) -> Result<()> {
|
||||
self.lifecycle.shutdown().await
|
||||
}
|
||||
|
||||
/// Reboot the system
|
||||
pub async fn reboot(&self) -> Result<()> {
|
||||
self.lifecycle.reboot().await
|
||||
}
|
||||
|
||||
/// Get stats for a service (memory and CPU usage)
|
||||
pub async fn stats<S: AsRef<str>>(&self, name: S) -> Result<ServiceStats> {
|
||||
self.lifecycle.stats(name).await
|
||||
}
|
||||
}
|
||||
37
components/zinit/src/zinit/ord.rs
Normal file
37
components/zinit/src/zinit/ord.rs
Normal file
@@ -0,0 +1,37 @@
|
||||
use crate::zinit::types::ServiceTable;
|
||||
use std::collections::HashMap;
|
||||
use std::sync::Arc;
|
||||
use tokio::sync::RwLock;
|
||||
pub const DUMMY_ROOT: &str = "";
|
||||
pub struct ProcessDAG {
|
||||
pub adj: HashMap<String, Vec<String>>,
|
||||
pub indegree: HashMap<String, u32>,
|
||||
/// number of services including the dummy root
|
||||
pub count: u32,
|
||||
}
|
||||
pub async fn service_dependency_order(services: Arc<RwLock<ServiceTable>>) -> ProcessDAG {
|
||||
let mut children: HashMap<String, Vec<String>> = HashMap::new();
|
||||
let mut indegree: HashMap<String, u32> = HashMap::new();
|
||||
let table = services.read().await;
|
||||
for (name, service) in table.iter() {
|
||||
let service = service.read().await;
|
||||
for child in service.service.after.iter() {
|
||||
children.entry(name.into()).or_default().push(child.into());
|
||||
*indegree.entry(child.into()).or_insert(0) += 1;
|
||||
}
|
||||
}
|
||||
let mut heads: Vec<String> = Vec::new();
|
||||
for (name, _) in table.iter() {
|
||||
if *indegree.get::<str>(name).unwrap_or(&0) == 0 {
|
||||
heads.push(name.into());
|
||||
// add edges from the dummy root to the heads
|
||||
*indegree.entry(name.into()).or_insert(0) += 1;
|
||||
}
|
||||
}
|
||||
children.insert(DUMMY_ROOT.to_string(), heads);
|
||||
ProcessDAG {
|
||||
adj: children,
|
||||
indegree,
|
||||
count: table.len() as u32 + 1,
|
||||
}
|
||||
}
|
||||
126
components/zinit/src/zinit/service.rs
Normal file
126
components/zinit/src/zinit/service.rs
Normal file
@@ -0,0 +1,126 @@
|
||||
use crate::zinit::config;
|
||||
use crate::zinit::state::{State, Target};
|
||||
use crate::zinit::types::Watched;
|
||||
use anyhow::{Context, Result};
|
||||
use nix::unistd::Pid;
|
||||
|
||||
/// Represents a service managed by ZInit
|
||||
pub struct ZInitService {
|
||||
/// Process ID of the running service
|
||||
pub pid: Pid,
|
||||
|
||||
/// Service configuration
|
||||
pub service: config::Service,
|
||||
|
||||
/// Target state of the service (up, down)
|
||||
pub target: Target,
|
||||
|
||||
/// Whether the service is scheduled for execution
|
||||
pub scheduled: bool,
|
||||
|
||||
/// Current state of the service
|
||||
state: Watched<State>,
|
||||
}
|
||||
|
||||
/// Status information for a service
|
||||
pub struct ZInitStatus {
|
||||
/// Process ID of the running service
|
||||
pub pid: Pid,
|
||||
|
||||
/// Service configuration
|
||||
pub service: config::Service,
|
||||
|
||||
/// Target state of the service (up, down)
|
||||
pub target: Target,
|
||||
|
||||
/// Whether the service is scheduled for execution
|
||||
pub scheduled: bool,
|
||||
|
||||
/// Current state of the service
|
||||
pub state: State,
|
||||
}
|
||||
|
||||
impl ZInitService {
|
||||
/// Create a new service with the given configuration and initial state
|
||||
pub fn new(service: config::Service, state: State) -> ZInitService {
|
||||
ZInitService {
|
||||
pid: Pid::from_raw(0),
|
||||
state: Watched::new(state),
|
||||
service,
|
||||
target: Target::Up,
|
||||
scheduled: false,
|
||||
}
|
||||
}
|
||||
|
||||
/// Get the current status of the service
|
||||
pub fn status(&self) -> ZInitStatus {
|
||||
ZInitStatus {
|
||||
pid: self.pid,
|
||||
state: self.state.get().clone(),
|
||||
service: self.service.clone(),
|
||||
target: self.target.clone(),
|
||||
scheduled: self.scheduled,
|
||||
}
|
||||
}
|
||||
|
||||
/// Set the state of the service, validating the state transition
|
||||
pub fn set_state(&mut self, state: State) -> Result<()> {
|
||||
let current_state = self.state.get().clone();
|
||||
let new_state = current_state
|
||||
.transition_to(state)
|
||||
.context("Failed to transition service state")?;
|
||||
|
||||
self.state.set(new_state);
|
||||
Ok(())
|
||||
}
|
||||
|
||||
/// Set the state of the service without validation
|
||||
pub fn force_set_state(&mut self, state: State) {
|
||||
self.state.set(state);
|
||||
}
|
||||
|
||||
/// Set the target state of the service
|
||||
pub fn set_target(&mut self, target: Target) {
|
||||
self.target = target;
|
||||
}
|
||||
|
||||
/// Get the current state of the service
|
||||
pub fn get_state(&self) -> &State {
|
||||
self.state.get()
|
||||
}
|
||||
|
||||
/// Get a watcher for the service state
|
||||
pub fn state_watcher(&self) -> crate::zinit::types::Watcher<State> {
|
||||
self.state.watcher()
|
||||
}
|
||||
|
||||
/// Check if the service is active (running or in progress)
|
||||
pub fn is_active(&self) -> bool {
|
||||
self.state.get().is_active()
|
||||
}
|
||||
|
||||
/// Check if the service is in a terminal state (success or failure)
|
||||
pub fn is_terminal(&self) -> bool {
|
||||
self.state.get().is_terminal()
|
||||
}
|
||||
|
||||
/// Set the process ID of the service
|
||||
pub fn set_pid(&mut self, pid: Pid) {
|
||||
self.pid = pid;
|
||||
}
|
||||
|
||||
/// Clear the process ID of the service
|
||||
pub fn clear_pid(&mut self) {
|
||||
self.pid = Pid::from_raw(0);
|
||||
}
|
||||
|
||||
/// Check if the service is running
|
||||
pub fn is_running(&self) -> bool {
|
||||
self.pid.as_raw() != 0 && self.state.get().is_active()
|
||||
}
|
||||
|
||||
/// Check if the service is a one-shot service
|
||||
pub fn is_one_shot(&self) -> bool {
|
||||
self.service.one_shot
|
||||
}
|
||||
}
|
||||
106
components/zinit/src/zinit/state.rs
Normal file
106
components/zinit/src/zinit/state.rs
Normal file
@@ -0,0 +1,106 @@
|
||||
use crate::zinit::errors::ZInitError;
|
||||
use anyhow::Result;
|
||||
use nix::sys::wait::WaitStatus;
|
||||
|
||||
/// Target state for a service
|
||||
#[derive(Clone, Debug, PartialEq)]
|
||||
pub enum Target {
|
||||
/// Service should be running
|
||||
Up,
|
||||
/// Service should be stopped
|
||||
Down,
|
||||
}
|
||||
|
||||
/// Service state
|
||||
#[derive(Debug, PartialEq, Clone)]
|
||||
pub enum State {
|
||||
/// Service is in an unknown state
|
||||
Unknown,
|
||||
|
||||
/// Blocked means one or more dependencies hasn't been met yet. Service can stay in
|
||||
/// this state as long as at least one dependency is not in either Running, or Success
|
||||
Blocked,
|
||||
|
||||
/// Service has been started, but it didn't exit yet, or we didn't run the test command.
|
||||
Spawned,
|
||||
|
||||
/// Service has been started, and test command passed.
|
||||
Running,
|
||||
|
||||
/// Service has exited with success state, only one-shot can stay in this state
|
||||
Success,
|
||||
|
||||
/// Service exited with this error, only one-shot can stay in this state
|
||||
Error(WaitStatus),
|
||||
|
||||
/// The service test command failed, this might (or might not) be replaced
|
||||
/// with an Error state later on once the service process itself exits
|
||||
TestFailure,
|
||||
|
||||
/// Failure means the service has failed to spawn in a way that retrying
|
||||
/// won't help, like command line parsing error or failed to fork
|
||||
Failure,
|
||||
}
|
||||
|
||||
impl State {
|
||||
/// Validate if a transition from the current state to the new state is valid
|
||||
pub fn can_transition_to(&self, new_state: &State) -> bool {
|
||||
match (self, new_state) {
|
||||
// From Unknown state, any transition is valid
|
||||
(State::Unknown, _) => true,
|
||||
|
||||
// From Blocked state
|
||||
(State::Blocked, State::Spawned) => true,
|
||||
(State::Blocked, State::Failure) => true,
|
||||
|
||||
// From Spawned state
|
||||
(State::Spawned, State::Running) => true,
|
||||
(State::Spawned, State::TestFailure) => true,
|
||||
(State::Spawned, State::Error(_)) => true,
|
||||
(State::Spawned, State::Success) => true,
|
||||
|
||||
// From Running state
|
||||
(State::Running, State::Success) => true,
|
||||
(State::Running, State::Error(_)) => true,
|
||||
|
||||
// To Unknown or Blocked state is always valid
|
||||
(_, State::Unknown) => true,
|
||||
(_, State::Blocked) => true,
|
||||
|
||||
// Any other transition is invalid
|
||||
_ => false,
|
||||
}
|
||||
}
|
||||
|
||||
/// Attempt to transition to a new state, validating the transition
|
||||
pub fn transition_to(&self, new_state: State) -> Result<State, ZInitError> {
|
||||
if self.can_transition_to(&new_state) {
|
||||
Ok(new_state)
|
||||
} else {
|
||||
Err(ZInitError::invalid_state_transition(format!(
|
||||
"Invalid transition from {:?} to {:?}",
|
||||
self, new_state
|
||||
)))
|
||||
}
|
||||
}
|
||||
|
||||
/// Check if the state is considered "active" (running or in progress)
|
||||
pub fn is_active(&self) -> bool {
|
||||
matches!(self, State::Running | State::Spawned)
|
||||
}
|
||||
|
||||
/// Check if the state is considered "terminal" (success or failure)
|
||||
pub fn is_terminal(&self) -> bool {
|
||||
matches!(self, State::Success | State::Error(_) | State::Failure)
|
||||
}
|
||||
|
||||
/// Check if the state is considered "successful"
|
||||
pub fn is_successful(&self) -> bool {
|
||||
matches!(self, State::Success | State::Running)
|
||||
}
|
||||
|
||||
/// Check if the state is considered "failed"
|
||||
pub fn is_failed(&self) -> bool {
|
||||
matches!(self, State::Error(_) | State::Failure | State::TestFailure)
|
||||
}
|
||||
}
|
||||
89
components/zinit/src/zinit/types.rs
Normal file
89
components/zinit/src/zinit/types.rs
Normal file
@@ -0,0 +1,89 @@
|
||||
use nix::sys::wait::WaitStatus;
|
||||
use serde::{Deserialize, Serialize};
|
||||
use std::collections::HashMap;
|
||||
use std::sync::Arc;
|
||||
use tokio::sync::watch;
|
||||
use tokio::sync::RwLock;
|
||||
use tokio_stream::wrappers::WatchStream;
|
||||
|
||||
/// Stats information for a service
|
||||
#[derive(Debug, Clone, Serialize, Deserialize)]
|
||||
pub struct ServiceStats {
|
||||
/// Memory usage in bytes
|
||||
pub memory_usage: u64,
|
||||
|
||||
/// CPU usage as a percentage (0-100)
|
||||
pub cpu_usage: f32,
|
||||
|
||||
/// Process ID of the service
|
||||
pub pid: i32,
|
||||
|
||||
/// Child process stats if any
|
||||
pub children: Vec<ProcessStats>,
|
||||
}
|
||||
|
||||
/// Stats for an individual process
|
||||
#[derive(Debug, Clone, Serialize, Deserialize)]
|
||||
pub struct ProcessStats {
|
||||
/// Process ID
|
||||
pub pid: i32,
|
||||
|
||||
/// Memory usage in bytes
|
||||
pub memory_usage: u64,
|
||||
|
||||
/// CPU usage as a percentage (0-100)
|
||||
pub cpu_usage: f32,
|
||||
}
|
||||
|
||||
/// Extension trait for WaitStatus to check if a process exited successfully
|
||||
pub trait WaitStatusExt {
|
||||
fn success(&self) -> bool;
|
||||
}
|
||||
|
||||
impl WaitStatusExt for WaitStatus {
|
||||
fn success(&self) -> bool {
|
||||
matches!(self, WaitStatus::Exited(_, code) if *code == 0)
|
||||
}
|
||||
}
|
||||
|
||||
/// Type alias for a service table mapping service names to service instances
|
||||
pub type ServiceTable = HashMap<String, Arc<RwLock<crate::zinit::service::ZInitService>>>;
|
||||
|
||||
/// Type alias for a watch stream
|
||||
pub type Watcher<T> = WatchStream<Arc<T>>;
|
||||
|
||||
/// A wrapper around a value that can be watched for changes
|
||||
pub struct Watched<T> {
|
||||
v: Arc<T>,
|
||||
tx: watch::Sender<Arc<T>>,
|
||||
}
|
||||
|
||||
impl<T> Watched<T>
|
||||
where
|
||||
T: Send + Sync + 'static,
|
||||
{
|
||||
/// Create a new watched value
|
||||
pub fn new(v: T) -> Self {
|
||||
let v = Arc::new(v);
|
||||
let (tx, _) = watch::channel(Arc::clone(&v));
|
||||
Self { v, tx }
|
||||
}
|
||||
|
||||
/// Set the value and notify watchers
|
||||
pub fn set(&mut self, v: T) {
|
||||
let v = Arc::new(v);
|
||||
self.v = Arc::clone(&v);
|
||||
// update the value even when there are no receivers
|
||||
self.tx.send_replace(v);
|
||||
}
|
||||
|
||||
/// Get a reference to the current value
|
||||
pub fn get(&self) -> &T {
|
||||
&self.v
|
||||
}
|
||||
|
||||
/// Create a watcher for this value
|
||||
pub fn watcher(&self) -> Watcher<T> {
|
||||
WatchStream::new(self.tx.subscribe())
|
||||
}
|
||||
}
|
||||
42
components/zinit/stop.sh
Executable file
42
components/zinit/stop.sh
Executable file
@@ -0,0 +1,42 @@
|
||||
#!/bin/bash
|
||||
set -e
|
||||
|
||||
# Colors for output
|
||||
RED='\033[0;31m'
|
||||
GREEN='\033[0;32m'
|
||||
YELLOW='\033[0;33m'
|
||||
NC='\033[0m' # No Color
|
||||
|
||||
echo -e "${GREEN}Stopping zinit...${NC}"
|
||||
|
||||
# Function to check if zinit is running
|
||||
is_zinit_running() {
|
||||
pgrep -f "zinit" > /dev/null
|
||||
return $?
|
||||
}
|
||||
|
||||
# Try to shutdown zinit gracefully if it's running
|
||||
if is_zinit_running; then
|
||||
echo -e "${YELLOW}Zinit is already running. Attempting graceful shutdown...${NC}"
|
||||
zinit shutdown || true
|
||||
|
||||
# Give it a moment to shut down
|
||||
sleep 2
|
||||
|
||||
# Check if it's still running
|
||||
if is_zinit_running; then
|
||||
echo -e "${YELLOW}Zinit is still running. Attempting to kill the process...${NC}"
|
||||
pkill -f "zinit$" || true
|
||||
sleep 1
|
||||
fi
|
||||
else
|
||||
echo -e "${YELLOW}No existing zinit process found.${NC}"
|
||||
fi
|
||||
|
||||
# Double-check no zinit is running
|
||||
if is_zinit_running; then
|
||||
echo -e "${RED}Warning: Could not terminate existing zinit process. You may need to manually kill it.${NC}"
|
||||
ps aux | grep "zinit" | grep -v grep
|
||||
else
|
||||
echo -e "${GREEN}No zinit process is running. Ready to start a new instance.${NC}"
|
||||
fi
|
||||
26
components/zinit/zinit-client/Cargo.toml
Normal file
26
components/zinit/zinit-client/Cargo.toml
Normal file
@@ -0,0 +1,26 @@
|
||||
[package]
|
||||
name = "zinit-client"
|
||||
version = "0.1.0"
|
||||
edition = "2021"
|
||||
description = "A client library for interacting with Zinit process manager"
|
||||
license = "Apache 2.0"
|
||||
authors = ["ThreeFold Tech, https://github.com/threefoldtech"]
|
||||
|
||||
[dependencies]
|
||||
anyhow = "1.0"
|
||||
async-trait = "0.1.88"
|
||||
jsonrpsee = { version = "0.25.1", features = ["macros", "http-client", "ws-client"] }
|
||||
reth-ipc = { git = "https://github.com/paradigmxyz/reth", package = "reth-ipc" }
|
||||
tokio = { version = "1.14.0", features = ["full"] }
|
||||
serde = { version = "1.0", features = ["derive"] }
|
||||
serde_json = "1.0"
|
||||
thiserror = "1.0"
|
||||
log = "0.4"
|
||||
|
||||
[[example]]
|
||||
name = "basic_usage"
|
||||
path = "examples/basic_usage.rs"
|
||||
|
||||
[[example]]
|
||||
name = "http_client"
|
||||
path = "examples/http_client.rs"
|
||||
123
components/zinit/zinit-client/README.md
Normal file
123
components/zinit/zinit-client/README.md
Normal file
@@ -0,0 +1,123 @@
|
||||
# Zinit Client Library
|
||||
|
||||
A simple Rust client library for interacting with the Zinit process manager.
|
||||
|
||||
## Features
|
||||
|
||||
- Connect to Zinit via Unix socket or HTTP
|
||||
- Manage services (start, stop, restart, monitor)
|
||||
- Query service status and information
|
||||
- Create and delete service configurations
|
||||
- System operations (shutdown, reboot)
|
||||
|
||||
## Installation
|
||||
|
||||
Add this to your `Cargo.toml`:
|
||||
|
||||
```toml
|
||||
[dependencies]
|
||||
zinit-client = "0.1.0"
|
||||
```
|
||||
|
||||
## Usage
|
||||
|
||||
### Creating a Client
|
||||
|
||||
You can create a client using either Unix socket or HTTP transport:
|
||||
|
||||
```rust
|
||||
use zinit_client::Client;
|
||||
|
||||
// Using Unix socket (local only)
|
||||
let client = Client::unix_socket("/var/run/zinit.sock");
|
||||
|
||||
// Using HTTP (works for remote Zinit instances)
|
||||
let client = Client::http("http://localhost:8080");
|
||||
```
|
||||
|
||||
### Service Management
|
||||
|
||||
```rust
|
||||
// List all services
|
||||
let services = client.list().await?;
|
||||
for (name, state) in services {
|
||||
println!("{}: {}", name, state);
|
||||
}
|
||||
|
||||
// Get status of a specific service
|
||||
let status = client.status("my-service").await?;
|
||||
println!("PID: {}, State: {}", status.pid, status.state);
|
||||
|
||||
// Start a service
|
||||
client.start("my-service").await?;
|
||||
|
||||
// Stop a service
|
||||
client.stop("my-service").await?;
|
||||
|
||||
// Restart a service
|
||||
client.restart("my-service").await?;
|
||||
|
||||
// Monitor a service
|
||||
client.monitor("my-service").await?;
|
||||
|
||||
// Forget a service
|
||||
client.forget("my-service").await?;
|
||||
|
||||
// Send a signal to a service
|
||||
client.kill("my-service", "SIGTERM").await?;
|
||||
```
|
||||
|
||||
### Service Configuration
|
||||
|
||||
```rust
|
||||
use serde_json::json;
|
||||
|
||||
// Create a new service
|
||||
let config = json!({
|
||||
"exec": "nginx",
|
||||
"oneshot": false,
|
||||
"after": ["network"]
|
||||
}).as_object().unwrap().clone();
|
||||
|
||||
client.create_service("nginx", config).await?;
|
||||
|
||||
// Get service configuration
|
||||
let config = client.get_service("nginx").await?;
|
||||
println!("Config: {:?}", config);
|
||||
|
||||
// Delete a service
|
||||
client.delete_service("nginx").await?;
|
||||
```
|
||||
|
||||
### System Operations
|
||||
|
||||
```rust
|
||||
// Shutdown the system
|
||||
client.shutdown().await?;
|
||||
|
||||
// Reboot the system
|
||||
client.reboot().await?;
|
||||
```
|
||||
|
||||
## Error Handling
|
||||
|
||||
The library provides a `ClientError` enum for handling errors:
|
||||
|
||||
```rust
|
||||
match client.status("non-existent-service").await {
|
||||
Ok(status) => println!("Service status: {}", status.state),
|
||||
Err(e) => match e {
|
||||
ClientError::ServiceNotFound(_) => println!("Service not found"),
|
||||
ClientError::ConnectionError(_) => println!("Failed to connect to Zinit"),
|
||||
_ => println!("Other error: {}", e),
|
||||
},
|
||||
}
|
||||
```
|
||||
|
||||
## Examples
|
||||
|
||||
See the [examples](examples) directory for complete usage examples.
|
||||
|
||||
## License
|
||||
|
||||
This project is licensed under the MIT License.
|
||||
50
components/zinit/zinit-client/examples/basic_usage.rs
Normal file
50
components/zinit/zinit-client/examples/basic_usage.rs
Normal file
@@ -0,0 +1,50 @@
|
||||
use anyhow::Result;
|
||||
use zinit_client::Client;
|
||||
|
||||
#[tokio::main]
|
||||
async fn main() -> Result<()> {
|
||||
// Create a client using Unix socket transport
|
||||
let client = Client::unix_socket("/var/run/zinit.sock").await?;
|
||||
|
||||
// List all services
|
||||
let services = client.list().await?;
|
||||
println!("Services:");
|
||||
for (name, state) in services {
|
||||
println!("{}: {}", name, state);
|
||||
}
|
||||
|
||||
// Get a specific service status
|
||||
let service_name = "example-service";
|
||||
match client.status(service_name).await {
|
||||
Ok(status) => {
|
||||
println!("\nService: {}", status.name);
|
||||
println!("PID: {}", status.pid);
|
||||
println!("State: {}", status.state);
|
||||
println!("Target: {}", status.target);
|
||||
println!("After:");
|
||||
for (dep, state) in status.after {
|
||||
println!(" {}: {}", dep, state);
|
||||
}
|
||||
}
|
||||
Err(e) => eprintln!("Failed to get status: {}", e),
|
||||
}
|
||||
|
||||
// Try to start a service
|
||||
match client.start(service_name).await {
|
||||
Ok(_) => println!("\nService started successfully"),
|
||||
Err(e) => eprintln!("Failed to start service: {}", e),
|
||||
}
|
||||
|
||||
// Get logs for the service
|
||||
match client.logs(Some(service_name.to_string())).await {
|
||||
Ok(logs) => {
|
||||
println!("\nLogs:");
|
||||
for log in logs {
|
||||
println!("{}", log);
|
||||
}
|
||||
}
|
||||
Err(e) => eprintln!("Failed to get logs: {}", e),
|
||||
}
|
||||
|
||||
Ok(())
|
||||
}
|
||||
78
components/zinit/zinit-client/examples/http_client.rs
Normal file
78
components/zinit/zinit-client/examples/http_client.rs
Normal file
@@ -0,0 +1,78 @@
|
||||
use anyhow::Result;
|
||||
use serde_json::json;
|
||||
use zinit_client::Client;
|
||||
|
||||
#[tokio::main]
|
||||
async fn main() -> Result<()> {
|
||||
// Create a client using HTTP transport
|
||||
let client = Client::http("http://localhost:8080").await?;
|
||||
|
||||
// Create a new service
|
||||
let service_name = "example-http-service";
|
||||
let service_config = json!({
|
||||
"exec": "echo 'Hello from HTTP service'",
|
||||
"oneshot": true,
|
||||
"after": ["network"]
|
||||
})
|
||||
.as_object()
|
||||
.unwrap()
|
||||
.clone();
|
||||
|
||||
match client.create_service(service_name, service_config).await {
|
||||
Ok(msg) => println!("Service created: {}", msg),
|
||||
Err(e) => eprintln!("Failed to create service: {}", e),
|
||||
}
|
||||
|
||||
// Start the HTTP/RPC server on a specific address
|
||||
match client.start_http_server("0.0.0.0:8081").await {
|
||||
Ok(msg) => println!("HTTP server status: {}", msg),
|
||||
Err(e) => eprintln!("Failed to start HTTP server: {}", e),
|
||||
}
|
||||
|
||||
// List all services
|
||||
let services = client.list().await?;
|
||||
println!("\nServices:");
|
||||
for (name, state) in services {
|
||||
println!("{}: {}", name, state);
|
||||
}
|
||||
|
||||
// Monitor the service
|
||||
match client.monitor(service_name).await {
|
||||
Ok(_) => println!("\nService is now monitored"),
|
||||
Err(e) => eprintln!("Failed to monitor service: {}", e),
|
||||
}
|
||||
|
||||
// Start the service
|
||||
match client.start(service_name).await {
|
||||
Ok(_) => println!("Service started successfully"),
|
||||
Err(e) => eprintln!("Failed to start service: {}", e),
|
||||
}
|
||||
|
||||
// Get logs
|
||||
let logs = client.logs(Some(service_name.to_string())).await?;
|
||||
println!("\nLogs:");
|
||||
for log in logs {
|
||||
println!("{}", log);
|
||||
}
|
||||
|
||||
// Clean up - forget the service
|
||||
println!("\nCleaning up...");
|
||||
match client.forget(service_name).await {
|
||||
Ok(_) => println!("Service has been forgotten"),
|
||||
Err(e) => eprintln!("Failed to forget service: {}", e),
|
||||
}
|
||||
|
||||
// Clean up - delete the service configuration
|
||||
match client.delete_service(service_name).await {
|
||||
Ok(msg) => println!("{}", msg),
|
||||
Err(e) => eprintln!("Failed to delete service: {}", e),
|
||||
}
|
||||
|
||||
// Stop the HTTP/RPC server
|
||||
match client.stop_http_server().await {
|
||||
Ok(_) => println!("HTTP server stopped"),
|
||||
Err(e) => eprintln!("Failed to stop HTTP server: {}", e),
|
||||
}
|
||||
|
||||
Ok(())
|
||||
}
|
||||
450
components/zinit/zinit-client/src/lib.rs
Normal file
450
components/zinit/zinit-client/src/lib.rs
Normal file
@@ -0,0 +1,450 @@
|
||||
//! A client library for interacting with the Zinit process manager.
|
||||
//!
|
||||
//! This library provides a simple API for communicating with a Zinit daemon
|
||||
//! via either Unix socket (using reth-ipc) or HTTP (using jsonrpsee).
|
||||
use jsonrpsee::core::client::ClientT;
|
||||
use jsonrpsee::core::client::Error as RpcError;
|
||||
use jsonrpsee::http_client::{HttpClient, HttpClientBuilder};
|
||||
use jsonrpsee::rpc_params;
|
||||
use reth_ipc::client::IpcClientBuilder;
|
||||
use serde::{Deserialize, Serialize};
|
||||
use serde_json::{Map, Value};
|
||||
use std::collections::HashMap;
|
||||
use thiserror::Error;
|
||||
|
||||
/// Error type for client operations
|
||||
#[derive(Error, Debug)]
|
||||
pub enum ClientError {
|
||||
#[error("connection error: {0}")]
|
||||
ConnectionError(String),
|
||||
|
||||
#[error("service not found: {0}")]
|
||||
ServiceNotFound(String),
|
||||
|
||||
#[error("service is already up: {0}")]
|
||||
ServiceIsUp(String),
|
||||
|
||||
#[error("system is shutting down")]
|
||||
ShuttingDown,
|
||||
|
||||
#[error("service already exists: {0}")]
|
||||
ServiceAlreadyExists(String),
|
||||
|
||||
#[error("service file error: {0}")]
|
||||
ServiceFileError(String),
|
||||
|
||||
#[error("rpc error: {0}")]
|
||||
RpcError(String),
|
||||
|
||||
#[error("unknown error: {0}")]
|
||||
UnknownError(String),
|
||||
}
|
||||
|
||||
impl From<RpcError> for ClientError {
|
||||
fn from(err: RpcError) -> Self {
|
||||
// Parse the error code if available
|
||||
if let RpcError::Call(err) = &err {
|
||||
match err.code() {
|
||||
-32000 => return ClientError::ServiceNotFound(err.message().to_string()),
|
||||
-32002 => return ClientError::ServiceIsUp(err.message().to_string()),
|
||||
-32006 => return ClientError::ShuttingDown,
|
||||
-32007 => return ClientError::ServiceAlreadyExists(err.message().to_string()),
|
||||
-32008 => return ClientError::ServiceFileError(err.message().to_string()),
|
||||
_ => {}
|
||||
}
|
||||
}
|
||||
|
||||
match err {
|
||||
RpcError::Transport(_) => ClientError::ConnectionError(err.to_string()),
|
||||
_ => ClientError::RpcError(err.to_string()),
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
/// Service status information
|
||||
#[derive(Debug, Clone, Deserialize, Serialize)]
|
||||
pub struct Status {
|
||||
pub name: String,
|
||||
pub pid: u32,
|
||||
pub state: String,
|
||||
pub target: String,
|
||||
pub after: HashMap<String, String>,
|
||||
}
|
||||
|
||||
/// Child process stats information
|
||||
#[derive(Debug, Clone, Deserialize, Serialize)]
|
||||
pub struct ChildStats {
|
||||
pub pid: u32,
|
||||
pub memory_usage: u64,
|
||||
pub cpu_usage: f32,
|
||||
}
|
||||
|
||||
/// Service stats information
|
||||
#[derive(Debug, Clone, Deserialize, Serialize)]
|
||||
pub struct Stats {
|
||||
pub name: String,
|
||||
pub pid: u32,
|
||||
pub memory_usage: u64,
|
||||
pub cpu_usage: f32,
|
||||
pub children: Vec<ChildStats>,
|
||||
}
|
||||
|
||||
/// Client implementation for communicating with Zinit
|
||||
pub enum Client {
|
||||
Ipc(String), // Socket path
|
||||
Http(HttpClient),
|
||||
}
|
||||
|
||||
impl Client {
|
||||
/// Create a new client using Unix socket transport
|
||||
pub async fn unix_socket<P: AsRef<std::path::Path>>(path: P) -> Result<Self, ClientError> {
|
||||
Ok(Client::Ipc(path.as_ref().to_string_lossy().to_string()))
|
||||
}
|
||||
|
||||
/// Create a new client using HTTP transport
|
||||
pub async fn http<S: AsRef<str>>(url: S) -> Result<Self, ClientError> {
|
||||
let client = HttpClientBuilder::default()
|
||||
.build(url.as_ref())
|
||||
.map_err(|e| ClientError::ConnectionError(e.to_string()))?;
|
||||
|
||||
Ok(Client::Http(client))
|
||||
}
|
||||
|
||||
// Helper to get IPC client
|
||||
async fn get_ipc_client(&self) -> Result<impl ClientT, ClientError> {
|
||||
match self {
|
||||
Client::Ipc(path) => IpcClientBuilder::default()
|
||||
.build(path)
|
||||
.await
|
||||
.map_err(|e| ClientError::ConnectionError(e.to_string())),
|
||||
_ => Err(ClientError::UnknownError("Not an IPC client".to_string())),
|
||||
}
|
||||
}
|
||||
|
||||
// Service API Methods
|
||||
|
||||
/// List all monitored services and their current state
|
||||
pub async fn list(&self) -> Result<HashMap<String, String>, ClientError> {
|
||||
match self {
|
||||
Client::Ipc(_) => {
|
||||
let client = self.get_ipc_client().await?;
|
||||
client
|
||||
.request("service_list", rpc_params![])
|
||||
.await
|
||||
.map_err(Into::into)
|
||||
}
|
||||
Client::Http(client) => client
|
||||
.request("service_list", rpc_params![])
|
||||
.await
|
||||
.map_err(Into::into),
|
||||
}
|
||||
}
|
||||
|
||||
/// Get the detailed status of a specific service
|
||||
pub async fn status(&self, name: impl AsRef<str>) -> Result<Status, ClientError> {
|
||||
let name = name.as_ref().to_string();
|
||||
match self {
|
||||
Client::Ipc(_) => {
|
||||
let client = self.get_ipc_client().await?;
|
||||
client
|
||||
.request("service_status", rpc_params![name])
|
||||
.await
|
||||
.map_err(Into::into)
|
||||
}
|
||||
Client::Http(client) => client
|
||||
.request("service_status", rpc_params![name])
|
||||
.await
|
||||
.map_err(Into::into),
|
||||
}
|
||||
}
|
||||
|
||||
/// Start a specific service
|
||||
pub async fn start(&self, name: impl AsRef<str>) -> Result<(), ClientError> {
|
||||
let name = name.as_ref().to_string();
|
||||
match self {
|
||||
Client::Ipc(_) => {
|
||||
let client = self.get_ipc_client().await?;
|
||||
client
|
||||
.request("service_start", rpc_params![name])
|
||||
.await
|
||||
.map_err(Into::into)
|
||||
}
|
||||
Client::Http(client) => client
|
||||
.request("service_start", rpc_params![name])
|
||||
.await
|
||||
.map_err(Into::into),
|
||||
}
|
||||
}
|
||||
|
||||
/// Stop a specific service
|
||||
pub async fn stop(&self, name: impl AsRef<str>) -> Result<(), ClientError> {
|
||||
let name = name.as_ref().to_string();
|
||||
match self {
|
||||
Client::Ipc(_) => {
|
||||
let client = self.get_ipc_client().await?;
|
||||
client
|
||||
.request("service_stop", rpc_params![name])
|
||||
.await
|
||||
.map_err(Into::into)
|
||||
}
|
||||
Client::Http(client) => client
|
||||
.request("service_stop", rpc_params![name])
|
||||
.await
|
||||
.map_err(Into::into),
|
||||
}
|
||||
}
|
||||
|
||||
/// Restart a service
|
||||
pub async fn restart(&self, name: impl AsRef<str>) -> Result<(), ClientError> {
|
||||
let name = name.as_ref().to_string();
|
||||
// First stop the service
|
||||
self.stop(&name).await?;
|
||||
|
||||
// Poll the service status until it's stopped
|
||||
for _ in 0..20 {
|
||||
let status = self.status(&name).await?;
|
||||
if status.pid == 0 && status.target == "Down" {
|
||||
return self.start(&name).await;
|
||||
}
|
||||
tokio::time::sleep(std::time::Duration::from_secs(1)).await;
|
||||
}
|
||||
|
||||
// Process not stopped, try to kill it
|
||||
self.kill(&name, "SIGKILL").await?;
|
||||
self.start(&name).await
|
||||
}
|
||||
|
||||
/// Load and monitor a new service from its configuration file
|
||||
pub async fn monitor(&self, name: impl AsRef<str>) -> Result<(), ClientError> {
|
||||
let name = name.as_ref().to_string();
|
||||
match self {
|
||||
Client::Ipc(_) => {
|
||||
let client = self.get_ipc_client().await?;
|
||||
client
|
||||
.request("service_monitor", rpc_params![name])
|
||||
.await
|
||||
.map_err(Into::into)
|
||||
}
|
||||
Client::Http(client) => client
|
||||
.request("service_monitor", rpc_params![name])
|
||||
.await
|
||||
.map_err(Into::into),
|
||||
}
|
||||
}
|
||||
|
||||
/// Stop monitoring a service and remove it from management
|
||||
pub async fn forget(&self, name: impl AsRef<str>) -> Result<(), ClientError> {
|
||||
let name = name.as_ref().to_string();
|
||||
match self {
|
||||
Client::Ipc(_) => {
|
||||
let client = self.get_ipc_client().await?;
|
||||
client
|
||||
.request("service_forget", rpc_params![name])
|
||||
.await
|
||||
.map_err(Into::into)
|
||||
}
|
||||
Client::Http(client) => client
|
||||
.request("service_forget", rpc_params![name])
|
||||
.await
|
||||
.map_err(Into::into),
|
||||
}
|
||||
}
|
||||
|
||||
/// Send a signal to a specific service process
|
||||
pub async fn kill(
|
||||
&self,
|
||||
name: impl AsRef<str>,
|
||||
signal: impl AsRef<str>,
|
||||
) -> Result<(), ClientError> {
|
||||
let name = name.as_ref().to_string();
|
||||
let signal = signal.as_ref().to_string();
|
||||
match self {
|
||||
Client::Ipc(_) => {
|
||||
let client = self.get_ipc_client().await?;
|
||||
client
|
||||
.request("service_kill", rpc_params![name, signal])
|
||||
.await
|
||||
.map_err(Into::into)
|
||||
}
|
||||
Client::Http(client) => client
|
||||
.request("service_kill", rpc_params![name, signal])
|
||||
.await
|
||||
.map_err(Into::into),
|
||||
}
|
||||
}
|
||||
|
||||
/// Create a new service configuration
|
||||
pub async fn create_service(
|
||||
&self,
|
||||
name: impl AsRef<str>,
|
||||
content: Map<String, Value>,
|
||||
) -> Result<String, ClientError> {
|
||||
let name = name.as_ref().to_string();
|
||||
match self {
|
||||
Client::Ipc(_) => {
|
||||
let client = self.get_ipc_client().await?;
|
||||
client
|
||||
.request("service_create", rpc_params![name, content])
|
||||
.await
|
||||
.map_err(Into::into)
|
||||
}
|
||||
Client::Http(client) => client
|
||||
.request("service_create", rpc_params![name, content])
|
||||
.await
|
||||
.map_err(Into::into),
|
||||
}
|
||||
}
|
||||
|
||||
/// Delete a service configuration
|
||||
pub async fn delete_service(&self, name: impl AsRef<str>) -> Result<String, ClientError> {
|
||||
let name = name.as_ref().to_string();
|
||||
match self {
|
||||
Client::Ipc(_) => {
|
||||
let client = self.get_ipc_client().await?;
|
||||
client
|
||||
.request("service_delete", rpc_params![name])
|
||||
.await
|
||||
.map_err(Into::into)
|
||||
}
|
||||
Client::Http(client) => client
|
||||
.request("service_delete", rpc_params![name])
|
||||
.await
|
||||
.map_err(Into::into),
|
||||
}
|
||||
}
|
||||
|
||||
/// Get a service configuration
|
||||
pub async fn get_service(&self, name: impl AsRef<str>) -> Result<Value, ClientError> {
|
||||
let name = name.as_ref().to_string();
|
||||
match self {
|
||||
Client::Ipc(_) => {
|
||||
let client = self.get_ipc_client().await?;
|
||||
client
|
||||
.request("service_get", rpc_params![name])
|
||||
.await
|
||||
.map_err(Into::into)
|
||||
}
|
||||
Client::Http(client) => client
|
||||
.request("service_get", rpc_params![name])
|
||||
.await
|
||||
.map_err(Into::into),
|
||||
}
|
||||
}
|
||||
|
||||
/// Get memory and CPU usage statistics for a service
|
||||
pub async fn stats(&self, name: impl AsRef<str>) -> Result<Stats, ClientError> {
|
||||
let name = name.as_ref().to_string();
|
||||
match self {
|
||||
Client::Ipc(_) => {
|
||||
let client = self.get_ipc_client().await?;
|
||||
client
|
||||
.request("service_stats", rpc_params![name])
|
||||
.await
|
||||
.map_err(Into::into)
|
||||
}
|
||||
Client::Http(client) => client
|
||||
.request("service_stats", rpc_params![name])
|
||||
.await
|
||||
.map_err(Into::into),
|
||||
}
|
||||
}
|
||||
|
||||
// System API Methods
|
||||
|
||||
/// Initiate system shutdown
|
||||
pub async fn shutdown(&self) -> Result<(), ClientError> {
|
||||
match self {
|
||||
Client::Ipc(_) => {
|
||||
let client = self.get_ipc_client().await?;
|
||||
client
|
||||
.request("system_shutdown", rpc_params![])
|
||||
.await
|
||||
.map_err(Into::into)
|
||||
}
|
||||
Client::Http(client) => client
|
||||
.request("system_shutdown", rpc_params![])
|
||||
.await
|
||||
.map_err(Into::into),
|
||||
}
|
||||
}
|
||||
|
||||
/// Initiate system reboot
|
||||
pub async fn reboot(&self) -> Result<(), ClientError> {
|
||||
match self {
|
||||
Client::Ipc(_) => {
|
||||
let client = self.get_ipc_client().await?;
|
||||
client
|
||||
.request("system_reboot", rpc_params![])
|
||||
.await
|
||||
.map_err(Into::into)
|
||||
}
|
||||
Client::Http(client) => client
|
||||
.request("system_reboot", rpc_params![])
|
||||
.await
|
||||
.map_err(Into::into),
|
||||
}
|
||||
}
|
||||
|
||||
/// Start HTTP/RPC server
|
||||
pub async fn start_http_server(&self, address: impl AsRef<str>) -> Result<String, ClientError> {
|
||||
let address = address.as_ref().to_string();
|
||||
match self {
|
||||
Client::Ipc(_) => {
|
||||
let client = self.get_ipc_client().await?;
|
||||
client
|
||||
.request("system_start_http_server", rpc_params![address])
|
||||
.await
|
||||
.map_err(Into::into)
|
||||
}
|
||||
Client::Http(client) => client
|
||||
.request("system_start_http_server", rpc_params![address])
|
||||
.await
|
||||
.map_err(Into::into),
|
||||
}
|
||||
}
|
||||
|
||||
/// Stop HTTP/RPC server
|
||||
pub async fn stop_http_server(&self) -> Result<(), ClientError> {
|
||||
match self {
|
||||
Client::Ipc(_) => {
|
||||
let client = self.get_ipc_client().await?;
|
||||
client
|
||||
.request("system_stop_http_server", rpc_params![])
|
||||
.await
|
||||
.map_err(Into::into)
|
||||
}
|
||||
Client::Http(client) => client
|
||||
.request("system_stop_http_server", rpc_params![])
|
||||
.await
|
||||
.map_err(Into::into),
|
||||
}
|
||||
}
|
||||
|
||||
// Logging API Methods
|
||||
|
||||
/// Get current logs
|
||||
pub async fn logs(&self, filter: Option<String>) -> Result<Vec<String>, ClientError> {
|
||||
match self {
|
||||
Client::Ipc(_) => {
|
||||
let client = self.get_ipc_client().await?;
|
||||
client
|
||||
.request("stream_currentLogs", rpc_params![filter])
|
||||
.await
|
||||
.map_err(Into::into)
|
||||
}
|
||||
Client::Http(client) => client
|
||||
.request("stream_currentLogs", rpc_params![filter])
|
||||
.await
|
||||
.map_err(Into::into),
|
||||
}
|
||||
}
|
||||
|
||||
/// Subscribe to logs
|
||||
///
|
||||
/// Note: This method is not fully implemented yet. For now, it will return an error.
|
||||
pub async fn log_subscribe(&self, _filter: Option<String>) -> Result<(), ClientError> {
|
||||
Err(ClientError::UnknownError(
|
||||
"Log subscription not implemented yet".to_string(),
|
||||
))
|
||||
}
|
||||
}
|
||||
66
components/zinit/zinit-client/tests/integration_test.rs
Normal file
66
components/zinit/zinit-client/tests/integration_test.rs
Normal file
@@ -0,0 +1,66 @@
|
||||
use std::env;
|
||||
use zinit_client::{Client, ClientError};
|
||||
|
||||
#[tokio::test]
|
||||
async fn test_connection_error() {
|
||||
// Try to connect to a non-existent socket
|
||||
let result = Client::unix_socket("/non/existent/socket").await;
|
||||
assert!(result.is_ok()); // Just creating the client succeeds
|
||||
|
||||
// Trying to make a request should fail
|
||||
if let Ok(client) = result {
|
||||
let list_result = client.list().await;
|
||||
assert!(matches!(list_result, Err(ClientError::ConnectionError(_))));
|
||||
}
|
||||
}
|
||||
|
||||
#[tokio::test]
|
||||
async fn test_http_connection_error() {
|
||||
// Try to connect to a non-existent HTTP endpoint
|
||||
let result = Client::http("http://localhost:12345").await;
|
||||
// This should succeed as we're just creating the client, not making a request
|
||||
assert!(result.is_ok());
|
||||
|
||||
// Try to make a request which should fail
|
||||
if let Ok(client) = result {
|
||||
let list_result = client.list().await;
|
||||
assert!(matches!(list_result, Err(ClientError::ConnectionError(_))));
|
||||
}
|
||||
}
|
||||
|
||||
// This test only runs if ZINIT_SOCKET is set in the environment
|
||||
// and points to a valid Zinit socket
|
||||
#[tokio::test]
|
||||
#[ignore]
|
||||
async fn test_live_connection() {
|
||||
let socket_path = match env::var("ZINIT_SOCKET") {
|
||||
Ok(path) => path,
|
||||
Err(_) => {
|
||||
println!("ZINIT_SOCKET not set, skipping live test");
|
||||
return;
|
||||
}
|
||||
};
|
||||
|
||||
let client = match Client::unix_socket(&socket_path).await {
|
||||
Ok(client) => client,
|
||||
Err(e) => {
|
||||
panic!(
|
||||
"Failed to connect to Zinit socket at {}: {}",
|
||||
socket_path, e
|
||||
);
|
||||
}
|
||||
};
|
||||
|
||||
// Test listing services
|
||||
let services = client.list().await.expect("Failed to list services");
|
||||
println!("Found {} services", services.len());
|
||||
|
||||
// If there are services, test getting status of the first one
|
||||
if let Some((service_name, _)) = services.iter().next() {
|
||||
let status = client
|
||||
.status(service_name)
|
||||
.await
|
||||
.expect("Failed to get service status");
|
||||
println!("Service {} has PID {}", service_name, status.pid);
|
||||
}
|
||||
}
|
||||
873
components/zinit/zinit.json
Normal file
873
components/zinit/zinit.json
Normal file
@@ -0,0 +1,873 @@
|
||||
{
|
||||
"openrpc": "1.2.6",
|
||||
"info": {
|
||||
"version": "1.0.0",
|
||||
"title": "Zinit JSON-RPC API",
|
||||
"description": "JSON-RPC 2.0 API for controlling and querying Zinit services",
|
||||
"license": {
|
||||
"name": "MIT"
|
||||
}
|
||||
},
|
||||
"servers": [
|
||||
{
|
||||
"name": "Unix Socket",
|
||||
"url": "unix:///var/run/zinit.sock"
|
||||
}
|
||||
],
|
||||
"methods": [
|
||||
{
|
||||
"name": "rpc_discover",
|
||||
"description": "Returns the OpenRPC specification for the API",
|
||||
"params": [],
|
||||
"result": {
|
||||
"name": "OpenRPCSpec",
|
||||
"description": "The OpenRPC specification",
|
||||
"schema": {
|
||||
"type": "object"
|
||||
}
|
||||
},
|
||||
"examples": [
|
||||
{
|
||||
"name": "Get API specification",
|
||||
"params": [],
|
||||
"result": {
|
||||
"name": "OpenRPCSpecResult",
|
||||
"value": {
|
||||
"openrpc": "1.2.6",
|
||||
"info": {
|
||||
"version": "1.0.0",
|
||||
"title": "Zinit JSON-RPC API"
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
]
|
||||
},
|
||||
{
|
||||
"name": "service_list",
|
||||
"description": "Lists all services managed by Zinit",
|
||||
"params": [],
|
||||
"result": {
|
||||
"name": "ServiceList",
|
||||
"description": "A map of service names to their current states",
|
||||
"schema": {
|
||||
"type": "object",
|
||||
"additionalProperties": {
|
||||
"type": "string",
|
||||
"description": "Service state (Running, Success, Error, etc.)"
|
||||
}
|
||||
}
|
||||
},
|
||||
"examples": [
|
||||
{
|
||||
"name": "List all services",
|
||||
"params": [],
|
||||
"result": {
|
||||
"name": "ServiceListResult",
|
||||
"value": {
|
||||
"service1": "Running",
|
||||
"service2": "Success",
|
||||
"service3": "Error"
|
||||
}
|
||||
}
|
||||
}
|
||||
]
|
||||
},
|
||||
{
|
||||
"name": "service_status",
|
||||
"description": "Shows detailed status information for a specific service",
|
||||
"params": [
|
||||
{
|
||||
"name": "name",
|
||||
"description": "The name of the service",
|
||||
"required": true,
|
||||
"schema": {
|
||||
"type": "string"
|
||||
}
|
||||
}
|
||||
],
|
||||
"result": {
|
||||
"name": "ServiceStatus",
|
||||
"description": "Detailed status information for the service",
|
||||
"schema": {
|
||||
"type": "object",
|
||||
"properties": {
|
||||
"name": {
|
||||
"type": "string",
|
||||
"description": "Service name"
|
||||
},
|
||||
"pid": {
|
||||
"type": "integer",
|
||||
"description": "Process ID of the running service (if running)"
|
||||
},
|
||||
"state": {
|
||||
"type": "string",
|
||||
"description": "Current state of the service (Running, Success, Error, etc.)"
|
||||
},
|
||||
"target": {
|
||||
"type": "string",
|
||||
"description": "Target state of the service (Up, Down)"
|
||||
},
|
||||
"after": {
|
||||
"type": "object",
|
||||
"description": "Dependencies of the service and their states",
|
||||
"additionalProperties": {
|
||||
"type": "string",
|
||||
"description": "State of the dependency"
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
},
|
||||
"examples": [
|
||||
{
|
||||
"name": "Get status of redis service",
|
||||
"params": [
|
||||
{
|
||||
"name": "name",
|
||||
"value": "redis"
|
||||
}
|
||||
],
|
||||
"result": {
|
||||
"name": "ServiceStatusResult",
|
||||
"value": {
|
||||
"name": "redis",
|
||||
"pid": 1234,
|
||||
"state": "Running",
|
||||
"target": "Up",
|
||||
"after": {
|
||||
"dependency1": "Success",
|
||||
"dependency2": "Running"
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
],
|
||||
"errors": [
|
||||
{
|
||||
"code": -32000,
|
||||
"message": "Service not found",
|
||||
"data": "service name \"unknown\" unknown"
|
||||
}
|
||||
]
|
||||
},
|
||||
{
|
||||
"name": "service_start",
|
||||
"description": "Starts a service",
|
||||
"params": [
|
||||
{
|
||||
"name": "name",
|
||||
"description": "The name of the service to start",
|
||||
"required": true,
|
||||
"schema": {
|
||||
"type": "string"
|
||||
}
|
||||
}
|
||||
],
|
||||
"result": {
|
||||
"name": "StartResult",
|
||||
"description": "Result of the start operation",
|
||||
"schema": {
|
||||
"type": "null"
|
||||
}
|
||||
},
|
||||
"examples": [
|
||||
{
|
||||
"name": "Start redis service",
|
||||
"params": [
|
||||
{
|
||||
"name": "name",
|
||||
"value": "redis"
|
||||
}
|
||||
],
|
||||
"result": {
|
||||
"name": "StartResult",
|
||||
"value": null
|
||||
}
|
||||
}
|
||||
],
|
||||
"errors": [
|
||||
{
|
||||
"code": -32000,
|
||||
"message": "Service not found",
|
||||
"data": "service name \"unknown\" unknown"
|
||||
}
|
||||
]
|
||||
},
|
||||
{
|
||||
"name": "service_stop",
|
||||
"description": "Stops a service",
|
||||
"params": [
|
||||
{
|
||||
"name": "name",
|
||||
"description": "The name of the service to stop",
|
||||
"required": true,
|
||||
"schema": {
|
||||
"type": "string"
|
||||
}
|
||||
}
|
||||
],
|
||||
"result": {
|
||||
"name": "StopResult",
|
||||
"description": "Result of the stop operation",
|
||||
"schema": {
|
||||
"type": "null"
|
||||
}
|
||||
},
|
||||
"examples": [
|
||||
{
|
||||
"name": "Stop redis service",
|
||||
"params": [
|
||||
{
|
||||
"name": "name",
|
||||
"value": "redis"
|
||||
}
|
||||
],
|
||||
"result": {
|
||||
"name": "StopResult",
|
||||
"value": null
|
||||
}
|
||||
}
|
||||
],
|
||||
"errors": [
|
||||
{
|
||||
"code": -32000,
|
||||
"message": "Service not found",
|
||||
"data": "service name \"unknown\" unknown"
|
||||
},
|
||||
{
|
||||
"code": -32003,
|
||||
"message": "Service is down",
|
||||
"data": "service \"redis\" is down"
|
||||
}
|
||||
]
|
||||
},
|
||||
{
|
||||
"name": "service_monitor",
|
||||
"description": "Starts monitoring a service. The service configuration is loaded from the config directory.",
|
||||
"params": [
|
||||
{
|
||||
"name": "name",
|
||||
"description": "The name of the service to monitor",
|
||||
"required": true,
|
||||
"schema": {
|
||||
"type": "string"
|
||||
}
|
||||
}
|
||||
],
|
||||
"result": {
|
||||
"name": "MonitorResult",
|
||||
"description": "Result of the monitor operation",
|
||||
"schema": {
|
||||
"type": "null"
|
||||
}
|
||||
},
|
||||
"examples": [
|
||||
{
|
||||
"name": "Monitor redis service",
|
||||
"params": [
|
||||
{
|
||||
"name": "name",
|
||||
"value": "redis"
|
||||
}
|
||||
],
|
||||
"result": {
|
||||
"name": "MonitorResult",
|
||||
"value": null
|
||||
}
|
||||
}
|
||||
],
|
||||
"errors": [
|
||||
{
|
||||
"code": -32001,
|
||||
"message": "Service already monitored",
|
||||
"data": "service \"redis\" already monitored"
|
||||
},
|
||||
{
|
||||
"code": -32005,
|
||||
"message": "Config error",
|
||||
"data": "failed to load service configuration"
|
||||
}
|
||||
]
|
||||
},
|
||||
{
|
||||
"name": "service_forget",
|
||||
"description": "Stops monitoring a service. You can only forget a stopped service.",
|
||||
"params": [
|
||||
{
|
||||
"name": "name",
|
||||
"description": "The name of the service to forget",
|
||||
"required": true,
|
||||
"schema": {
|
||||
"type": "string"
|
||||
}
|
||||
}
|
||||
],
|
||||
"result": {
|
||||
"name": "ForgetResult",
|
||||
"description": "Result of the forget operation",
|
||||
"schema": {
|
||||
"type": "null"
|
||||
}
|
||||
},
|
||||
"examples": [
|
||||
{
|
||||
"name": "Forget redis service",
|
||||
"params": [
|
||||
{
|
||||
"name": "name",
|
||||
"value": "redis"
|
||||
}
|
||||
],
|
||||
"result": {
|
||||
"name": "ForgetResult",
|
||||
"value": null
|
||||
}
|
||||
}
|
||||
],
|
||||
"errors": [
|
||||
{
|
||||
"code": -32000,
|
||||
"message": "Service not found",
|
||||
"data": "service name \"unknown\" unknown"
|
||||
},
|
||||
{
|
||||
"code": -32002,
|
||||
"message": "Service is up",
|
||||
"data": "service \"redis\" is up"
|
||||
}
|
||||
]
|
||||
},
|
||||
{
|
||||
"name": "service_kill",
|
||||
"description": "Sends a signal to a running service",
|
||||
"params": [
|
||||
{
|
||||
"name": "name",
|
||||
"description": "The name of the service to send the signal to",
|
||||
"required": true,
|
||||
"schema": {
|
||||
"type": "string"
|
||||
}
|
||||
},
|
||||
{
|
||||
"name": "signal",
|
||||
"description": "The signal to send (e.g., SIGTERM, SIGKILL)",
|
||||
"required": true,
|
||||
"schema": {
|
||||
"type": "string"
|
||||
}
|
||||
}
|
||||
],
|
||||
"result": {
|
||||
"name": "KillResult",
|
||||
"description": "Result of the kill operation",
|
||||
"schema": {
|
||||
"type": "null"
|
||||
}
|
||||
},
|
||||
"examples": [
|
||||
{
|
||||
"name": "Send SIGTERM to redis service",
|
||||
"params": [
|
||||
{
|
||||
"name": "name",
|
||||
"value": "redis"
|
||||
},
|
||||
{
|
||||
"name": "signal",
|
||||
"value": "SIGTERM"
|
||||
}
|
||||
],
|
||||
"result": {
|
||||
"name": "KillResult",
|
||||
"value": null
|
||||
}
|
||||
}
|
||||
],
|
||||
"errors": [
|
||||
{
|
||||
"code": -32000,
|
||||
"message": "Service not found",
|
||||
"data": "service name \"unknown\" unknown"
|
||||
},
|
||||
{
|
||||
"code": -32003,
|
||||
"message": "Service is down",
|
||||
"data": "service \"redis\" is down"
|
||||
},
|
||||
{
|
||||
"code": -32004,
|
||||
"message": "Invalid signal",
|
||||
"data": "invalid signal: INVALID"
|
||||
}
|
||||
]
|
||||
},
|
||||
{
|
||||
"name": "system_shutdown",
|
||||
"description": "Stops all services and powers off the system",
|
||||
"params": [],
|
||||
"result": {
|
||||
"name": "ShutdownResult",
|
||||
"description": "Result of the shutdown operation",
|
||||
"schema": {
|
||||
"type": "null"
|
||||
}
|
||||
},
|
||||
"examples": [
|
||||
{
|
||||
"name": "Shutdown the system",
|
||||
"params": [],
|
||||
"result": {
|
||||
"name": "ShutdownResult",
|
||||
"value": null
|
||||
}
|
||||
}
|
||||
],
|
||||
"errors": [
|
||||
{
|
||||
"code": -32006,
|
||||
"message": "Shutting down",
|
||||
"data": "system is already shutting down"
|
||||
}
|
||||
]
|
||||
},
|
||||
{
|
||||
"name": "system_reboot",
|
||||
"description": "Stops all services and reboots the system",
|
||||
"params": [],
|
||||
"result": {
|
||||
"name": "RebootResult",
|
||||
"description": "Result of the reboot operation",
|
||||
"schema": {
|
||||
"type": "null"
|
||||
}
|
||||
},
|
||||
"examples": [
|
||||
{
|
||||
"name": "Reboot the system",
|
||||
"params": [],
|
||||
"result": {
|
||||
"name": "RebootResult",
|
||||
"value": null
|
||||
}
|
||||
}
|
||||
],
|
||||
"errors": [
|
||||
{
|
||||
"code": -32006,
|
||||
"message": "Shutting down",
|
||||
"data": "system is already shutting down"
|
||||
}
|
||||
]
|
||||
},
|
||||
{
|
||||
"name": "service_create",
|
||||
"description": "Creates a new service configuration file",
|
||||
"params": [
|
||||
{
|
||||
"name": "name",
|
||||
"description": "The name of the service to create",
|
||||
"required": true,
|
||||
"schema": {
|
||||
"type": "string"
|
||||
}
|
||||
},
|
||||
{
|
||||
"name": "content",
|
||||
"description": "The service configuration content",
|
||||
"required": true,
|
||||
"schema": {
|
||||
"type": "object",
|
||||
"properties": {
|
||||
"exec": {
|
||||
"type": "string",
|
||||
"description": "Command to run"
|
||||
},
|
||||
"oneshot": {
|
||||
"type": "boolean",
|
||||
"description": "Whether the service should be restarted"
|
||||
},
|
||||
"after": {
|
||||
"type": "array",
|
||||
"items": {
|
||||
"type": "string"
|
||||
},
|
||||
"description": "Services that must be running before this one starts"
|
||||
},
|
||||
"log": {
|
||||
"type": "string",
|
||||
"enum": ["null", "ring", "stdout"],
|
||||
"description": "How to handle service output"
|
||||
},
|
||||
"env": {
|
||||
"type": "object",
|
||||
"additionalProperties": {
|
||||
"type": "string"
|
||||
},
|
||||
"description": "Environment variables for the service"
|
||||
},
|
||||
"shutdown_timeout": {
|
||||
"type": "integer",
|
||||
"description": "Maximum time to wait for service to stop during shutdown"
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
],
|
||||
"result": {
|
||||
"name": "CreateServiceResult",
|
||||
"description": "Result of the create operation",
|
||||
"schema": {
|
||||
"type": "string"
|
||||
}
|
||||
},
|
||||
"errors": [
|
||||
{
|
||||
"code": -32007,
|
||||
"message": "Service already exists",
|
||||
"data": "Service 'name' already exists"
|
||||
},
|
||||
{
|
||||
"code": -32008,
|
||||
"message": "Service file error",
|
||||
"data": "Failed to create service file"
|
||||
}
|
||||
]
|
||||
},
|
||||
{
|
||||
"name": "service_delete",
|
||||
"description": "Deletes a service configuration file",
|
||||
"params": [
|
||||
{
|
||||
"name": "name",
|
||||
"description": "The name of the service to delete",
|
||||
"required": true,
|
||||
"schema": {
|
||||
"type": "string"
|
||||
}
|
||||
}
|
||||
],
|
||||
"result": {
|
||||
"name": "DeleteServiceResult",
|
||||
"description": "Result of the delete operation",
|
||||
"schema": {
|
||||
"type": "string"
|
||||
}
|
||||
},
|
||||
"errors": [
|
||||
{
|
||||
"code": -32000,
|
||||
"message": "Service not found",
|
||||
"data": "Service 'name' not found"
|
||||
},
|
||||
{
|
||||
"code": -32008,
|
||||
"message": "Service file error",
|
||||
"data": "Failed to delete service file"
|
||||
}
|
||||
]
|
||||
},
|
||||
{
|
||||
"name": "service_get",
|
||||
"description": "Gets a service configuration file",
|
||||
"params": [
|
||||
{
|
||||
"name": "name",
|
||||
"description": "The name of the service to get",
|
||||
"required": true,
|
||||
"schema": {
|
||||
"type": "string"
|
||||
}
|
||||
}
|
||||
],
|
||||
"result": {
|
||||
"name": "GetServiceResult",
|
||||
"description": "The service configuration",
|
||||
"schema": {
|
||||
"type": "object"
|
||||
}
|
||||
},
|
||||
"errors": [
|
||||
{
|
||||
"code": -32000,
|
||||
"message": "Service not found",
|
||||
"data": "Service 'name' not found"
|
||||
},
|
||||
{
|
||||
"code": -32008,
|
||||
"message": "Service file error",
|
||||
"data": "Failed to read service file"
|
||||
}
|
||||
]
|
||||
},
|
||||
{
|
||||
"name": "service_stats",
|
||||
"description": "Get memory and CPU usage statistics for a service",
|
||||
"params": [
|
||||
{
|
||||
"name": "name",
|
||||
"description": "The name of the service to get stats for",
|
||||
"required": true,
|
||||
"schema": {
|
||||
"type": "string"
|
||||
}
|
||||
}
|
||||
],
|
||||
"result": {
|
||||
"name": "ServiceStats",
|
||||
"description": "Memory and CPU usage statistics for the service",
|
||||
"schema": {
|
||||
"type": "object",
|
||||
"properties": {
|
||||
"name": {
|
||||
"type": "string",
|
||||
"description": "Service name"
|
||||
},
|
||||
"pid": {
|
||||
"type": "integer",
|
||||
"description": "Process ID of the service"
|
||||
},
|
||||
"memory_usage": {
|
||||
"type": "integer",
|
||||
"description": "Memory usage in bytes"
|
||||
},
|
||||
"cpu_usage": {
|
||||
"type": "number",
|
||||
"description": "CPU usage as a percentage (0-100)"
|
||||
},
|
||||
"children": {
|
||||
"type": "array",
|
||||
"description": "Stats for child processes",
|
||||
"items": {
|
||||
"type": "object",
|
||||
"properties": {
|
||||
"pid": {
|
||||
"type": "integer",
|
||||
"description": "Process ID of the child process"
|
||||
},
|
||||
"memory_usage": {
|
||||
"type": "integer",
|
||||
"description": "Memory usage in bytes"
|
||||
},
|
||||
"cpu_usage": {
|
||||
"type": "number",
|
||||
"description": "CPU usage as a percentage (0-100)"
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
},
|
||||
"examples": [
|
||||
{
|
||||
"name": "Get stats for redis service",
|
||||
"params": [
|
||||
{
|
||||
"name": "name",
|
||||
"value": "redis"
|
||||
}
|
||||
],
|
||||
"result": {
|
||||
"name": "ServiceStatsResult",
|
||||
"value": {
|
||||
"name": "redis",
|
||||
"pid": 1234,
|
||||
"memory_usage": 10485760,
|
||||
"cpu_usage": 2.5,
|
||||
"children": [
|
||||
{
|
||||
"pid": 1235,
|
||||
"memory_usage": 5242880,
|
||||
"cpu_usage": 1.2
|
||||
}
|
||||
]
|
||||
}
|
||||
}
|
||||
}
|
||||
],
|
||||
"errors": [
|
||||
{
|
||||
"code": -32000,
|
||||
"message": "Service not found",
|
||||
"data": "service name \"unknown\" unknown"
|
||||
},
|
||||
{
|
||||
"code": -32003,
|
||||
"message": "Service is down",
|
||||
"data": "service \"redis\" is down"
|
||||
}
|
||||
]
|
||||
},
|
||||
{
|
||||
"name": "system_start_http_server",
|
||||
"description": "Start an HTTP/RPC server at the specified address",
|
||||
"params": [
|
||||
{
|
||||
"name": "address",
|
||||
"description": "The network address to bind the server to (e.g., '127.0.0.1:8080')",
|
||||
"required": true,
|
||||
"schema": {
|
||||
"type": "string"
|
||||
}
|
||||
}
|
||||
],
|
||||
"result": {
|
||||
"name": "StartHttpServerResult",
|
||||
"description": "Result of the start HTTP server operation",
|
||||
"schema": {
|
||||
"type": "string"
|
||||
}
|
||||
},
|
||||
"examples": [
|
||||
{
|
||||
"name": "Start HTTP server on localhost:8080",
|
||||
"params": [
|
||||
{
|
||||
"name": "address",
|
||||
"value": "127.0.0.1:8080"
|
||||
}
|
||||
],
|
||||
"result": {
|
||||
"name": "StartHttpServerResult",
|
||||
"value": "HTTP server started at 127.0.0.1:8080"
|
||||
}
|
||||
}
|
||||
],
|
||||
"errors": [
|
||||
{
|
||||
"code": -32602,
|
||||
"message": "Invalid address",
|
||||
"data": "Invalid network address format"
|
||||
}
|
||||
]
|
||||
},
|
||||
{
|
||||
"name": "system_stop_http_server",
|
||||
"description": "Stop the HTTP/RPC server if running",
|
||||
"params": [],
|
||||
"result": {
|
||||
"name": "StopHttpServerResult",
|
||||
"description": "Result of the stop HTTP server operation",
|
||||
"schema": {
|
||||
"type": "null"
|
||||
}
|
||||
},
|
||||
"examples": [
|
||||
{
|
||||
"name": "Stop the HTTP server",
|
||||
"params": [],
|
||||
"result": {
|
||||
"name": "StopHttpServerResult",
|
||||
"value": null
|
||||
}
|
||||
}
|
||||
],
|
||||
"errors": [
|
||||
{
|
||||
"code": -32602,
|
||||
"message": "Server not running",
|
||||
"data": "No HTTP server is currently running"
|
||||
}
|
||||
]
|
||||
},
|
||||
{
|
||||
"name": "stream_currentLogs",
|
||||
"description": "Get current logs from zinit and monitored services",
|
||||
"params": [
|
||||
{
|
||||
"name": "name",
|
||||
"description": "Optional service name filter. If provided, only logs from this service will be returned",
|
||||
"required": false,
|
||||
"schema": {
|
||||
"type": "string"
|
||||
}
|
||||
}
|
||||
],
|
||||
"result": {
|
||||
"name": "LogsResult",
|
||||
"description": "Array of log strings",
|
||||
"schema": {
|
||||
"type": "array",
|
||||
"items": {
|
||||
"type": "string"
|
||||
}
|
||||
}
|
||||
},
|
||||
"examples": [
|
||||
{
|
||||
"name": "Get all logs",
|
||||
"params": [],
|
||||
"result": {
|
||||
"name": "LogsResult",
|
||||
"value": [
|
||||
"2023-01-01T12:00:00 redis: Starting service",
|
||||
"2023-01-01T12:00:01 nginx: Starting service"
|
||||
]
|
||||
}
|
||||
},
|
||||
{
|
||||
"name": "Get logs for a specific service",
|
||||
"params": [
|
||||
{
|
||||
"name": "name",
|
||||
"value": "redis"
|
||||
}
|
||||
],
|
||||
"result": {
|
||||
"name": "LogsResult",
|
||||
"value": [
|
||||
"2023-01-01T12:00:00 redis: Starting service",
|
||||
"2023-01-01T12:00:02 redis: Service started"
|
||||
]
|
||||
}
|
||||
}
|
||||
]
|
||||
},
|
||||
{
|
||||
"name": "stream_subscribeLogs",
|
||||
"description": "Subscribe to log messages generated by zinit and monitored services",
|
||||
"params": [
|
||||
{
|
||||
"name": "name",
|
||||
"description": "Optional service name filter. If provided, only logs from this service will be returned",
|
||||
"required": false,
|
||||
"schema": {
|
||||
"type": "string"
|
||||
}
|
||||
}
|
||||
],
|
||||
"result": {
|
||||
"name": "LogSubscription",
|
||||
"description": "A subscription to log messages",
|
||||
"schema": {
|
||||
"type": "string"
|
||||
}
|
||||
},
|
||||
"examples": [
|
||||
{
|
||||
"name": "Subscribe to all logs",
|
||||
"params": [],
|
||||
"result": {
|
||||
"name": "LogSubscription",
|
||||
"value": "2023-01-01T12:00:00 redis: Service started"
|
||||
}
|
||||
},
|
||||
{
|
||||
"name": "Subscribe to filtered logs",
|
||||
"params": [
|
||||
{
|
||||
"name": "name",
|
||||
"value": "redis"
|
||||
}
|
||||
],
|
||||
"result": {
|
||||
"name": "LogSubscription",
|
||||
"value": "2023-01-01T12:00:00 redis: Service started"
|
||||
}
|
||||
}
|
||||
]
|
||||
}
|
||||
]
|
||||
}
|
||||
Reference in New Issue
Block a user