Squashed 'components/zinit/' content from commit 1b76c06

git-subtree-dir: components/zinit
git-subtree-split: 1b76c062fe31d552d1b7b23484ce163995a81482
This commit is contained in:
2025-08-16 21:12:16 +02:00
commit 2fda71af11
48 changed files with 11203 additions and 0 deletions

2
.cargo/config.toml Normal file
View File

@@ -0,0 +1,2 @@
[target.aarch64-unknown-linux-musl]
linker = "aarch64-linux-musl-gcc"

134
.github/workflows/release.yaml vendored Normal file
View 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
.github/workflows/rust.yml vendored Normal file
View 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
.gitignore vendored Normal file
View File

@@ -0,0 +1,2 @@
/target
**/*.rs.bk

2717
Cargo.lock generated Normal file

File diff suppressed because it is too large Load Diff

50
Cargo.toml Normal file
View 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
LICENSE Normal file
View 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
Makefile Normal file
View 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
README.md Normal file
View File

@@ -0,0 +1,77 @@
# Zinit [![Rust](https://github.com/threefoldtech/zinit/actions/workflows/rust.yml/badge.svg)](https://github.com/threefoldtech/zinit/actions/workflows/rust.yml)
Zinit is a lightweight PID 1 replacement inspired by runit, written in Rust using Tokio for async I/O. It provides both a Unix socket interface and an HTTP API for interacting with the process manager.
### Key Features
- **Service Management**: Ensures configured services are up and running at all times
- **Dependency Handling**: Supports service dependencies for proper startup ordering
- **Simple Control Interface**: Provides an intuitive CLI to add, start, stop, and monitor services
- **Container Support**: Can run in container mode with appropriate signal handling
- **Configurable Logging**: Multiple logging options including ringbuffer and stdout
## Installation
```bash
curl https://raw.githubusercontent.com/threefoldtech/zinit/refs/heads/master/install.sh | bash
#to install & run
curl https://raw.githubusercontent.com/threefoldtech/zinit/refs/heads/master/install_run.sh | bash
```
Click [here](docs/installation.md) for more information on how to install Zinit.
## Usage
### Process Manager (zinit)
```bash
# Run zinit in init mode
zinit init --config /etc/zinit/ --socket /var/run/zinit.sock
# List services
zinit list
# Start a service
zinit start <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
docker/Dockerfile Normal file
View File

@@ -0,0 +1,6 @@
FROM ubuntu:18.04
RUN mkdir -p /etc/zinit
ADD zinit /sbin/zinit
ENTRYPOINT ["/sbin/zinit", "init"]

256
docs/cmd.md Normal file
View 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
docs/installation.md Normal file
View 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
docs/osx_cross_compile.md Normal file
View 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
docs/services.md Normal file
View 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

View 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
docs/stats.md Normal file
View 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
example/example.sh Executable file
View 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
install.sh Executable file
View 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
install_run.sh Executable file
View 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
openrpc.json Normal file
View 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
osx_build.sh Executable file
View 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
release_zinit.sh Executable file
View 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
src/app/api.rs Normal file
View 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
src/app/mod.rs Normal file
View 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
src/app/rpc.rs Normal file
View 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
src/bin/testapp.rs Normal file
View 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
src/lib.rs Normal file
View 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
src/main.rs Normal file
View 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
src/manager/buffer.rs Normal file
View 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
src/manager/mod.rs Normal file
View 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
src/testapp/main.rs Normal file
View 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
src/testapp/mod.rs Normal file
View 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
src/zinit/config.rs Normal file
View 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
src/zinit/errors.rs Normal file
View 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
src/zinit/lifecycle.rs Normal file
View 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
src/zinit/mod.rs Normal file
View 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
src/zinit/ord.rs Normal file
View 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
src/zinit/service.rs Normal file
View 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
src/zinit/state.rs Normal file
View 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
src/zinit/types.rs Normal file
View 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
stop.sh Executable file
View 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
zinit-client/Cargo.toml Normal file
View 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
zinit-client/README.md Normal file
View 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.

View 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(())
}

View 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
zinit-client/src/lib.rs Normal file
View 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(),
))
}
}

View 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
zinit.json Normal file
View 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"
}
}
]
}
]
}