Merge commit '2fda71af117a90da5f496d8bb8105f0ee9e07420' as 'components/zinit'
This commit is contained in:
2
components/zinit/.cargo/config.toml
Normal file
2
components/zinit/.cargo/config.toml
Normal file
@@ -0,0 +1,2 @@
|
|||||||
|
[target.aarch64-unknown-linux-musl]
|
||||||
|
linker = "aarch64-linux-musl-gcc"
|
||||||
134
components/zinit/.github/workflows/release.yaml
vendored
Normal file
134
components/zinit/.github/workflows/release.yaml
vendored
Normal file
@@ -0,0 +1,134 @@
|
|||||||
|
on:
|
||||||
|
push:
|
||||||
|
# Sequence of patterns matched against refs/tags
|
||||||
|
tags:
|
||||||
|
- "v*" # Push events to matching v*, i.e. v1.0, v20.15.10
|
||||||
|
|
||||||
|
name: Create Release
|
||||||
|
|
||||||
|
jobs:
|
||||||
|
build:
|
||||||
|
name: Build and Release
|
||||||
|
runs-on: ${{ matrix.os }}
|
||||||
|
permissions:
|
||||||
|
contents: write
|
||||||
|
strategy:
|
||||||
|
fail-fast: false # Continue with other builds if one fails
|
||||||
|
matrix:
|
||||||
|
include:
|
||||||
|
# Linux builds
|
||||||
|
- os: ubuntu-latest
|
||||||
|
target: x86_64-unknown-linux-musl
|
||||||
|
binary_name: zinit-linux-x86_64
|
||||||
|
# macOS builds
|
||||||
|
- os: macos-latest
|
||||||
|
target: x86_64-apple-darwin
|
||||||
|
binary_name: zinit-macos-x86_64
|
||||||
|
- os: macos-latest
|
||||||
|
target: aarch64-apple-darwin
|
||||||
|
binary_name: zinit-macos-aarch64
|
||||||
|
steps:
|
||||||
|
- name: Checkout code
|
||||||
|
uses: actions/checkout@v4
|
||||||
|
with:
|
||||||
|
fetch-depth: 0 # Fetch all history for proper versioning
|
||||||
|
|
||||||
|
# Cache Rust dependencies
|
||||||
|
- name: Cache Rust dependencies
|
||||||
|
uses: actions/cache@v3
|
||||||
|
with:
|
||||||
|
path: |
|
||||||
|
~/.cargo/registry
|
||||||
|
~/.cargo/git
|
||||||
|
target
|
||||||
|
key: ${{ runner.os }}-${{ matrix.target }}-cargo-${{ hashFiles('**/Cargo.lock') }}
|
||||||
|
restore-keys: |
|
||||||
|
${{ runner.os }}-${{ matrix.target }}-cargo-
|
||||||
|
|
||||||
|
- name: Setup build environment (macOS)
|
||||||
|
if: matrix.os == 'macos-latest'
|
||||||
|
run: |
|
||||||
|
# Install required build tools for macOS
|
||||||
|
brew install llvm
|
||||||
|
|
||||||
|
# For cross-compilation to Apple Silicon when on Intel
|
||||||
|
if [[ "${{ matrix.target }}" == "aarch64-apple-darwin" ]]; then
|
||||||
|
rustup target add aarch64-apple-darwin
|
||||||
|
fi
|
||||||
|
|
||||||
|
- name: Install Rust toolchain
|
||||||
|
uses: actions-rs/toolchain@v1
|
||||||
|
with:
|
||||||
|
toolchain: stable
|
||||||
|
target: ${{ matrix.target }}
|
||||||
|
override: true
|
||||||
|
profile: minimal # Minimal components for faster installation
|
||||||
|
|
||||||
|
- name: Install MUSL tools (Linux)
|
||||||
|
if: matrix.os == 'ubuntu-latest' && contains(matrix.target, 'musl')
|
||||||
|
run: |
|
||||||
|
sudo apt-get update
|
||||||
|
sudo apt-get install -y musl-tools musl-dev
|
||||||
|
|
||||||
|
- name: Build release
|
||||||
|
env:
|
||||||
|
CC: ${{ matrix.os == 'macos-latest' && 'clang' || '' }}
|
||||||
|
CXX: ${{ matrix.os == 'macos-latest' && 'clang++' || '' }}
|
||||||
|
MACOSX_DEPLOYMENT_TARGET: '10.12'
|
||||||
|
run: |
|
||||||
|
# Add special flags for Apple Silicon builds
|
||||||
|
if [[ "${{ matrix.target }}" == "aarch64-apple-darwin" ]]; then
|
||||||
|
export RUSTFLAGS="-C target-feature=+crt-static"
|
||||||
|
fi
|
||||||
|
|
||||||
|
cargo build --release --target=${{ matrix.target }} --verbose
|
||||||
|
|
||||||
|
# Verify binary exists
|
||||||
|
if [ ! -f "target/${{ matrix.target }}/release/zinit" ]; then
|
||||||
|
echo "::error::Binary not found at target/${{ matrix.target }}/release/zinit"
|
||||||
|
exit 1
|
||||||
|
fi
|
||||||
|
|
||||||
|
- name: Strip binary (Linux)
|
||||||
|
if: matrix.os == 'ubuntu-latest'
|
||||||
|
run: |
|
||||||
|
strip target/${{ matrix.target }}/release/zinit
|
||||||
|
|
||||||
|
- name: Strip binary (macOS)
|
||||||
|
if: matrix.os == 'macos-latest'
|
||||||
|
run: |
|
||||||
|
strip -x target/${{ matrix.target }}/release/zinit || true
|
||||||
|
|
||||||
|
- name: Rename binary
|
||||||
|
run: |
|
||||||
|
cp target/${{ matrix.target }}/release/zinit ${{ matrix.binary_name }}
|
||||||
|
|
||||||
|
# Verify binary was copied successfully
|
||||||
|
if [ ! -f "${{ matrix.binary_name }}" ]; then
|
||||||
|
echo "::error::Binary not copied successfully to ${{ matrix.binary_name }}"
|
||||||
|
exit 1
|
||||||
|
fi
|
||||||
|
|
||||||
|
# Show binary info for debugging
|
||||||
|
echo "Binary details for ${{ matrix.binary_name }}:"
|
||||||
|
ls -la ${{ matrix.binary_name }}
|
||||||
|
file ${{ matrix.binary_name }} || true
|
||||||
|
|
||||||
|
# Upload artifacts even if the release step fails
|
||||||
|
- name: Upload artifacts
|
||||||
|
uses: actions/upload-artifact@v4
|
||||||
|
with:
|
||||||
|
name: ${{ matrix.binary_name }}
|
||||||
|
path: ${{ matrix.binary_name }}
|
||||||
|
retention-days: 5
|
||||||
|
|
||||||
|
- name: Upload Release Assets
|
||||||
|
uses: softprops/action-gh-release@v2
|
||||||
|
with:
|
||||||
|
files: ${{ matrix.binary_name }}
|
||||||
|
name: Release ${{ github.ref_name }}
|
||||||
|
draft: false
|
||||||
|
prerelease: false
|
||||||
|
fail_on_unmatched_files: false
|
||||||
|
env:
|
||||||
|
GITHUB_TOKEN: ${{ secrets.GITHUB_TOKEN }}
|
||||||
36
components/zinit/.github/workflows/rust.yml
vendored
Normal file
36
components/zinit/.github/workflows/rust.yml
vendored
Normal file
@@ -0,0 +1,36 @@
|
|||||||
|
name: Rust
|
||||||
|
|
||||||
|
on: [push]
|
||||||
|
|
||||||
|
jobs:
|
||||||
|
build:
|
||||||
|
runs-on: ubuntu-latest
|
||||||
|
steps:
|
||||||
|
- uses: actions/checkout@v1
|
||||||
|
name: Checkout code
|
||||||
|
with:
|
||||||
|
fetch-depth: 1
|
||||||
|
- uses: actions-rs/toolchain@v1
|
||||||
|
name: Install toolchain
|
||||||
|
with:
|
||||||
|
toolchain: stable
|
||||||
|
target: x86_64-unknown-linux-musl
|
||||||
|
- uses: actions-rs/cargo@v1
|
||||||
|
name: Check formatting
|
||||||
|
with:
|
||||||
|
command: fmt
|
||||||
|
args: -- --check
|
||||||
|
- uses: actions-rs/cargo@v1
|
||||||
|
name: Run tests (ahm!)
|
||||||
|
with:
|
||||||
|
command: test
|
||||||
|
args: --verbose
|
||||||
|
- uses: actions-rs/cargo@v1
|
||||||
|
name: Run clippy
|
||||||
|
with:
|
||||||
|
command: clippy
|
||||||
|
- uses: actions-rs/cargo@v1
|
||||||
|
name: Build
|
||||||
|
with:
|
||||||
|
command: build
|
||||||
|
args: --verbose
|
||||||
2
components/zinit/.gitignore
vendored
Normal file
2
components/zinit/.gitignore
vendored
Normal file
@@ -0,0 +1,2 @@
|
|||||||
|
/target
|
||||||
|
**/*.rs.bk
|
||||||
2717
components/zinit/Cargo.lock
generated
Normal file
2717
components/zinit/Cargo.lock
generated
Normal file
File diff suppressed because it is too large
Load Diff
50
components/zinit/Cargo.toml
Normal file
50
components/zinit/Cargo.toml
Normal file
@@ -0,0 +1,50 @@
|
|||||||
|
[workspace]
|
||||||
|
members = [
|
||||||
|
".",
|
||||||
|
"zinit-client"
|
||||||
|
]
|
||||||
|
|
||||||
|
[package]
|
||||||
|
name = "zinit"
|
||||||
|
version = "0.2.0"
|
||||||
|
edition = "2018"
|
||||||
|
|
||||||
|
# See more keys and their definitions at https://doc.rust-lang.org/cargo/reference/manifest.html
|
||||||
|
|
||||||
|
[dependencies]
|
||||||
|
anyhow = "1.0"
|
||||||
|
tokio = { version = "1.44.1", features = ["full"] }
|
||||||
|
tokio-stream = { version = "0.1.17", features = ["sync"] }
|
||||||
|
shlex ="1.1"
|
||||||
|
nix = "0.22.1"
|
||||||
|
serde = { version = "1.0", features = ["derive"] }
|
||||||
|
serde_yaml = "0.8"
|
||||||
|
serde_json = "1.0"
|
||||||
|
fern = "0.6"
|
||||||
|
log = "0.4"
|
||||||
|
thiserror = "1.0"
|
||||||
|
clap = "2.33"
|
||||||
|
git-version = "0.3.5"
|
||||||
|
command-group = "1.0.8"
|
||||||
|
dirs = "5.0"
|
||||||
|
hyper = "1.6"
|
||||||
|
# axum = { version = "0.7.4", features = ["http1"] }
|
||||||
|
bytes = "1.0"
|
||||||
|
jsonrpsee = { version = "0.25.1", features = ["server", "client", "macros"] }
|
||||||
|
memchr = "2.5.0"
|
||||||
|
async-trait = "0.1.88"
|
||||||
|
reth-ipc = { git = "https://github.com/paradigmxyz/reth", package = "reth-ipc" }
|
||||||
|
tower-http = { version = "0.5", features = ["cors"] }
|
||||||
|
tower = "0.5.2"
|
||||||
|
sysinfo = "0.29.10"
|
||||||
|
|
||||||
|
[dev-dependencies]
|
||||||
|
tokio = { version = "1.14.0", features = ["full", "test-util"] }
|
||||||
|
tempfile = "3.3.0"
|
||||||
|
[lib]
|
||||||
|
name = "zinit"
|
||||||
|
path = "src/lib.rs"
|
||||||
|
|
||||||
|
[[bin]]
|
||||||
|
name = "zinit"
|
||||||
|
path = "src/main.rs"
|
||||||
201
components/zinit/LICENSE
Normal file
201
components/zinit/LICENSE
Normal file
@@ -0,0 +1,201 @@
|
|||||||
|
Apache License
|
||||||
|
Version 2.0, January 2004
|
||||||
|
http://www.apache.org/licenses/
|
||||||
|
|
||||||
|
TERMS AND CONDITIONS FOR USE, REPRODUCTION, AND DISTRIBUTION
|
||||||
|
|
||||||
|
1. Definitions.
|
||||||
|
|
||||||
|
"License" shall mean the terms and conditions for use, reproduction,
|
||||||
|
and distribution as defined by Sections 1 through 9 of this document.
|
||||||
|
|
||||||
|
"Licensor" shall mean the copyright owner or entity authorized by
|
||||||
|
the copyright owner that is granting the License.
|
||||||
|
|
||||||
|
"Legal Entity" shall mean the union of the acting entity and all
|
||||||
|
other entities that control, are controlled by, or are under common
|
||||||
|
control with that entity. For the purposes of this definition,
|
||||||
|
"control" means (i) the power, direct or indirect, to cause the
|
||||||
|
direction or management of such entity, whether by contract or
|
||||||
|
otherwise, or (ii) ownership of fifty percent (50%) or more of the
|
||||||
|
outstanding shares, or (iii) beneficial ownership of such entity.
|
||||||
|
|
||||||
|
"You" (or "Your") shall mean an individual or Legal Entity
|
||||||
|
exercising permissions granted by this License.
|
||||||
|
|
||||||
|
"Source" form shall mean the preferred form for making modifications,
|
||||||
|
including but not limited to software source code, documentation
|
||||||
|
source, and configuration files.
|
||||||
|
|
||||||
|
"Object" form shall mean any form resulting from mechanical
|
||||||
|
transformation or translation of a Source form, including but
|
||||||
|
not limited to compiled object code, generated documentation,
|
||||||
|
and conversions to other media types.
|
||||||
|
|
||||||
|
"Work" shall mean the work of authorship, whether in Source or
|
||||||
|
Object form, made available under the License, as indicated by a
|
||||||
|
copyright notice that is included in or attached to the work
|
||||||
|
(an example is provided in the Appendix below).
|
||||||
|
|
||||||
|
"Derivative Works" shall mean any work, whether in Source or Object
|
||||||
|
form, that is based on (or derived from) the Work and for which the
|
||||||
|
editorial revisions, annotations, elaborations, or other modifications
|
||||||
|
represent, as a whole, an original work of authorship. For the purposes
|
||||||
|
of this License, Derivative Works shall not include works that remain
|
||||||
|
separable from, or merely link (or bind by name) to the interfaces of,
|
||||||
|
the Work and Derivative Works thereof.
|
||||||
|
|
||||||
|
"Contribution" shall mean any work of authorship, including
|
||||||
|
the original version of the Work and any modifications or additions
|
||||||
|
to that Work or Derivative Works thereof, that is intentionally
|
||||||
|
submitted to Licensor for inclusion in the Work by the copyright owner
|
||||||
|
or by an individual or Legal Entity authorized to submit on behalf of
|
||||||
|
the copyright owner. For the purposes of this definition, "submitted"
|
||||||
|
means any form of electronic, verbal, or written communication sent
|
||||||
|
to the Licensor or its representatives, including but not limited to
|
||||||
|
communication on electronic mailing lists, source code control systems,
|
||||||
|
and issue tracking systems that are managed by, or on behalf of, the
|
||||||
|
Licensor for the purpose of discussing and improving the Work, but
|
||||||
|
excluding communication that is conspicuously marked or otherwise
|
||||||
|
designated in writing by the copyright owner as "Not a Contribution."
|
||||||
|
|
||||||
|
"Contributor" shall mean Licensor and any individual or Legal Entity
|
||||||
|
on behalf of whom a Contribution has been received by Licensor and
|
||||||
|
subsequently incorporated within the Work.
|
||||||
|
|
||||||
|
2. Grant of Copyright License. Subject to the terms and conditions of
|
||||||
|
this License, each Contributor hereby grants to You a perpetual,
|
||||||
|
worldwide, non-exclusive, no-charge, royalty-free, irrevocable
|
||||||
|
copyright license to reproduce, prepare Derivative Works of,
|
||||||
|
publicly display, publicly perform, sublicense, and distribute the
|
||||||
|
Work and such Derivative Works in Source or Object form.
|
||||||
|
|
||||||
|
3. Grant of Patent License. Subject to the terms and conditions of
|
||||||
|
this License, each Contributor hereby grants to You a perpetual,
|
||||||
|
worldwide, non-exclusive, no-charge, royalty-free, irrevocable
|
||||||
|
(except as stated in this section) patent license to make, have made,
|
||||||
|
use, offer to sell, sell, import, and otherwise transfer the Work,
|
||||||
|
where such license applies only to those patent claims licensable
|
||||||
|
by such Contributor that are necessarily infringed by their
|
||||||
|
Contribution(s) alone or by combination of their Contribution(s)
|
||||||
|
with the Work to which such Contribution(s) was submitted. If You
|
||||||
|
institute patent litigation against any entity (including a
|
||||||
|
cross-claim or counterclaim in a lawsuit) alleging that the Work
|
||||||
|
or a Contribution incorporated within the Work constitutes direct
|
||||||
|
or contributory patent infringement, then any patent licenses
|
||||||
|
granted to You under this License for that Work shall terminate
|
||||||
|
as of the date such litigation is filed.
|
||||||
|
|
||||||
|
4. Redistribution. You may reproduce and distribute copies of the
|
||||||
|
Work or Derivative Works thereof in any medium, with or without
|
||||||
|
modifications, and in Source or Object form, provided that You
|
||||||
|
meet the following conditions:
|
||||||
|
|
||||||
|
(a) You must give any other recipients of the Work or
|
||||||
|
Derivative Works a copy of this License; and
|
||||||
|
|
||||||
|
(b) You must cause any modified files to carry prominent notices
|
||||||
|
stating that You changed the files; and
|
||||||
|
|
||||||
|
(c) You must retain, in the Source form of any Derivative Works
|
||||||
|
that You distribute, all copyright, patent, trademark, and
|
||||||
|
attribution notices from the Source form of the Work,
|
||||||
|
excluding those notices that do not pertain to any part of
|
||||||
|
the Derivative Works; and
|
||||||
|
|
||||||
|
(d) If the Work includes a "NOTICE" text file as part of its
|
||||||
|
distribution, then any Derivative Works that You distribute must
|
||||||
|
include a readable copy of the attribution notices contained
|
||||||
|
within such NOTICE file, excluding those notices that do not
|
||||||
|
pertain to any part of the Derivative Works, in at least one
|
||||||
|
of the following places: within a NOTICE text file distributed
|
||||||
|
as part of the Derivative Works; within the Source form or
|
||||||
|
documentation, if provided along with the Derivative Works; or,
|
||||||
|
within a display generated by the Derivative Works, if and
|
||||||
|
wherever such third-party notices normally appear. The contents
|
||||||
|
of the NOTICE file are for informational purposes only and
|
||||||
|
do not modify the License. You may add Your own attribution
|
||||||
|
notices within Derivative Works that You distribute, alongside
|
||||||
|
or as an addendum to the NOTICE text from the Work, provided
|
||||||
|
that such additional attribution notices cannot be construed
|
||||||
|
as modifying the License.
|
||||||
|
|
||||||
|
You may add Your own copyright statement to Your modifications and
|
||||||
|
may provide additional or different license terms and conditions
|
||||||
|
for use, reproduction, or distribution of Your modifications, or
|
||||||
|
for any such Derivative Works as a whole, provided Your use,
|
||||||
|
reproduction, and distribution of the Work otherwise complies with
|
||||||
|
the conditions stated in this License.
|
||||||
|
|
||||||
|
5. Submission of Contributions. Unless You explicitly state otherwise,
|
||||||
|
any Contribution intentionally submitted for inclusion in the Work
|
||||||
|
by You to the Licensor shall be under the terms and conditions of
|
||||||
|
this License, without any additional terms or conditions.
|
||||||
|
Notwithstanding the above, nothing herein shall supersede or modify
|
||||||
|
the terms of any separate license agreement you may have executed
|
||||||
|
with Licensor regarding such Contributions.
|
||||||
|
|
||||||
|
6. Trademarks. This License does not grant permission to use the trade
|
||||||
|
names, trademarks, service marks, or product names of the Licensor,
|
||||||
|
except as required for reasonable and customary use in describing the
|
||||||
|
origin of the Work and reproducing the content of the NOTICE file.
|
||||||
|
|
||||||
|
7. Disclaimer of Warranty. Unless required by applicable law or
|
||||||
|
agreed to in writing, Licensor provides the Work (and each
|
||||||
|
Contributor provides its Contributions) on an "AS IS" BASIS,
|
||||||
|
WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or
|
||||||
|
implied, including, without limitation, any warranties or conditions
|
||||||
|
of TITLE, NON-INFRINGEMENT, MERCHANTABILITY, or FITNESS FOR A
|
||||||
|
PARTICULAR PURPOSE. You are solely responsible for determining the
|
||||||
|
appropriateness of using or redistributing the Work and assume any
|
||||||
|
risks associated with Your exercise of permissions under this License.
|
||||||
|
|
||||||
|
8. Limitation of Liability. In no event and under no legal theory,
|
||||||
|
whether in tort (including negligence), contract, or otherwise,
|
||||||
|
unless required by applicable law (such as deliberate and grossly
|
||||||
|
negligent acts) or agreed to in writing, shall any Contributor be
|
||||||
|
liable to You for damages, including any direct, indirect, special,
|
||||||
|
incidental, or consequential damages of any character arising as a
|
||||||
|
result of this License or out of the use or inability to use the
|
||||||
|
Work (including but not limited to damages for loss of goodwill,
|
||||||
|
work stoppage, computer failure or malfunction, or any and all
|
||||||
|
other commercial damages or losses), even if such Contributor
|
||||||
|
has been advised of the possibility of such damages.
|
||||||
|
|
||||||
|
9. Accepting Warranty or Additional Liability. While redistributing
|
||||||
|
the Work or Derivative Works thereof, You may choose to offer,
|
||||||
|
and charge a fee for, acceptance of support, warranty, indemnity,
|
||||||
|
or other liability obligations and/or rights consistent with this
|
||||||
|
License. However, in accepting such obligations, You may act only
|
||||||
|
on Your own behalf and on Your sole responsibility, not on behalf
|
||||||
|
of any other Contributor, and only if You agree to indemnify,
|
||||||
|
defend, and hold each Contributor harmless for any liability
|
||||||
|
incurred by, or claims asserted against, such Contributor by reason
|
||||||
|
of your accepting any such warranty or additional liability.
|
||||||
|
|
||||||
|
END OF TERMS AND CONDITIONS
|
||||||
|
|
||||||
|
APPENDIX: How to apply the Apache License to your work.
|
||||||
|
|
||||||
|
To apply the Apache License to your work, attach the following
|
||||||
|
boilerplate notice, with the fields enclosed by brackets "[]"
|
||||||
|
replaced with your own identifying information. (Don't include
|
||||||
|
the brackets!) The text should be enclosed in the appropriate
|
||||||
|
comment syntax for the file format. We also recommend that a
|
||||||
|
file or class name and description of purpose be included on the
|
||||||
|
same "printed page" as the copyright notice for easier
|
||||||
|
identification within third-party archives.
|
||||||
|
|
||||||
|
Copyright TF TECH NV (Belgium)
|
||||||
|
|
||||||
|
Licensed under the Apache License, Version 2.0 (the "License");
|
||||||
|
you may not use this file except in compliance with the License.
|
||||||
|
You may obtain a copy of the License at
|
||||||
|
|
||||||
|
http://www.apache.org/licenses/LICENSE-2.0
|
||||||
|
|
||||||
|
Unless required by applicable law or agreed to in writing, software
|
||||||
|
distributed under the License is distributed on an "AS IS" BASIS,
|
||||||
|
WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
|
||||||
|
See the License for the specific language governing permissions and
|
||||||
|
limitations under the License.
|
||||||
30
components/zinit/Makefile
Normal file
30
components/zinit/Makefile
Normal file
@@ -0,0 +1,30 @@
|
|||||||
|
default: release
|
||||||
|
|
||||||
|
docker: release
|
||||||
|
docker build -f docker/Dockerfile -t zinit-ubuntu:18.04 target/x86_64-unknown-linux-musl/release
|
||||||
|
|
||||||
|
prepare:
|
||||||
|
rustup target add x86_64-unknown-linux-musl
|
||||||
|
|
||||||
|
release: prepare
|
||||||
|
cargo build --release --target=x86_64-unknown-linux-musl
|
||||||
|
|
||||||
|
release-aarch64-musl: prepare-aarch64-musl
|
||||||
|
cargo build --release --target=aarch64-unknown-linux-musl
|
||||||
|
|
||||||
|
prepare-aarch64-musl:
|
||||||
|
rustup target add aarch64-unknown-linux-musl
|
||||||
|
|
||||||
|
# Build for macOS (both Intel and Apple Silicon)
|
||||||
|
release-macos:
|
||||||
|
cargo build --release
|
||||||
|
|
||||||
|
# Install to ~/hero/bin (if it exists)
|
||||||
|
install-macos: release-macos
|
||||||
|
@if [ -d ~/hero/bin ]; then \
|
||||||
|
cp target/release/zinit ~/hero/bin; \
|
||||||
|
echo "Installed zinit to ~/hero/bin"; \
|
||||||
|
else \
|
||||||
|
echo "~/hero/bin directory not found. Please create it or specify a different installation path."; \
|
||||||
|
exit 1; \
|
||||||
|
fi
|
||||||
77
components/zinit/README.md
Normal file
77
components/zinit/README.md
Normal file
@@ -0,0 +1,77 @@
|
|||||||
|
# Zinit [](https://github.com/threefoldtech/zinit/actions/workflows/rust.yml)
|
||||||
|
|
||||||
|
Zinit is a lightweight PID 1 replacement inspired by runit, written in Rust using Tokio for async I/O. It provides both a Unix socket interface and an HTTP API for interacting with the process manager.
|
||||||
|
|
||||||
|
### Key Features
|
||||||
|
|
||||||
|
- **Service Management**: Ensures configured services are up and running at all times
|
||||||
|
- **Dependency Handling**: Supports service dependencies for proper startup ordering
|
||||||
|
- **Simple Control Interface**: Provides an intuitive CLI to add, start, stop, and monitor services
|
||||||
|
- **Container Support**: Can run in container mode with appropriate signal handling
|
||||||
|
- **Configurable Logging**: Multiple logging options including ringbuffer and stdout
|
||||||
|
|
||||||
|
## Installation
|
||||||
|
|
||||||
|
```bash
|
||||||
|
curl https://raw.githubusercontent.com/threefoldtech/zinit/refs/heads/master/install.sh | bash
|
||||||
|
|
||||||
|
#to install & run
|
||||||
|
curl https://raw.githubusercontent.com/threefoldtech/zinit/refs/heads/master/install_run.sh | bash
|
||||||
|
```
|
||||||
|
|
||||||
|
Click [here](docs/installation.md) for more information on how to install Zinit.
|
||||||
|
|
||||||
|
## Usage
|
||||||
|
|
||||||
|
### Process Manager (zinit)
|
||||||
|
|
||||||
|
```bash
|
||||||
|
# Run zinit in init mode
|
||||||
|
zinit init --config /etc/zinit/ --socket /var/run/zinit.sock
|
||||||
|
|
||||||
|
# List services
|
||||||
|
zinit list
|
||||||
|
|
||||||
|
# Start a service
|
||||||
|
zinit start <service-name>
|
||||||
|
|
||||||
|
# Stop a service
|
||||||
|
zinit stop <service-name>
|
||||||
|
```
|
||||||
|
|
||||||
|
```bash
|
||||||
|
# Start the HTTP proxy on the default port (8080)
|
||||||
|
zinit proxy
|
||||||
|
```
|
||||||
|
|
||||||
|
More information about all the available commands can be found [here](docs/cmd.md).
|
||||||
|
|
||||||
|
### Service Configuration
|
||||||
|
|
||||||
|
Zinit uses YAML files for service configuration. Here's a basic example:
|
||||||
|
|
||||||
|
```yaml
|
||||||
|
# Service configuration (e.g., /etc/zinit/myservice.yaml)
|
||||||
|
exec: "/usr/bin/myservice --option value" # Command to run (required)
|
||||||
|
test: "/usr/bin/check-myservice" # Health check command (optional)
|
||||||
|
oneshot: false # Whether to restart on exit (default: false)
|
||||||
|
after: # Services that must be running first (optional)
|
||||||
|
- dependency1
|
||||||
|
- dependency2
|
||||||
|
```
|
||||||
|
|
||||||
|
For more information on how to configure service files, see the [service file reference](docs/services.md) documentation.
|
||||||
|
|
||||||
|
### JSON-RPC API
|
||||||
|
|
||||||
|
The HTTP proxy provides a JSON-RPC 2.0 API for interacting with Zinit. You can send JSON-RPC requests to the HTTP endpoint you provided to the proxy:
|
||||||
|
|
||||||
|
```bash
|
||||||
|
curl -X POST -H "Content-Type: application/json" -d '{"jsonrpc":"2.0","id":1,"method":"service_list","params":{}}' http://localhost:8080/
|
||||||
|
```
|
||||||
|
|
||||||
|
See the [OpenRPC specs](openrpc.json) for more information about available RPC calls to interact with Zinit.
|
||||||
|
|
||||||
|
## License
|
||||||
|
|
||||||
|
See [LICENSE](LICENSE) file for details.
|
||||||
6
components/zinit/docker/Dockerfile
Normal file
6
components/zinit/docker/Dockerfile
Normal file
@@ -0,0 +1,6 @@
|
|||||||
|
FROM ubuntu:18.04
|
||||||
|
|
||||||
|
RUN mkdir -p /etc/zinit
|
||||||
|
ADD zinit /sbin/zinit
|
||||||
|
|
||||||
|
ENTRYPOINT ["/sbin/zinit", "init"]
|
||||||
256
components/zinit/docs/cmd.md
Normal file
256
components/zinit/docs/cmd.md
Normal file
@@ -0,0 +1,256 @@
|
|||||||
|
# Zinit Command Line Reference
|
||||||
|
|
||||||
|
This document provides a comprehensive reference for all Zinit command line options and commands.
|
||||||
|
|
||||||
|
## Command Structure
|
||||||
|
|
||||||
|
Zinit uses a command-based CLI with the following general structure:
|
||||||
|
|
||||||
|
```bash
|
||||||
|
zinit [FLAGS] [OPTIONS] [SUBCOMMAND]
|
||||||
|
```
|
||||||
|
|
||||||
|
## Global Flags and Options
|
||||||
|
|
||||||
|
These flags and options apply to all Zinit commands:
|
||||||
|
|
||||||
|
| Flag/Option | Description |
|
||||||
|
|-------------|-------------|
|
||||||
|
| `-d, --debug` | Run in debug mode with increased verbosity |
|
||||||
|
| `-h, --help` | Display help information |
|
||||||
|
| `-V, --version` | Display version information |
|
||||||
|
| `-s, --socket <PATH>` | Path to Unix socket (default: `/var/run/zinit.sock`) |
|
||||||
|
|
||||||
|
## Subcommands
|
||||||
|
|
||||||
|
### Main Mode
|
||||||
|
|
||||||
|
#### `init`
|
||||||
|
|
||||||
|
Run Zinit in init mode, starting and maintaining configured services.
|
||||||
|
|
||||||
|
```bash
|
||||||
|
zinit init [FLAGS] [OPTIONS]
|
||||||
|
```
|
||||||
|
|
||||||
|
**Flags:**
|
||||||
|
- `--container`: Run in container mode, exiting on signal instead of rebooting
|
||||||
|
|
||||||
|
**Options:**
|
||||||
|
- `-c, --config <DIR>`: Service configurations directory (default: `/etc/zinit/`)
|
||||||
|
- `-b, --buffer <SIZE>`: Buffer size (in lines) to keep service logs (default: `2000`)
|
||||||
|
|
||||||
|
**Example:**
|
||||||
|
```bash
|
||||||
|
# Run in init mode with custom config directory
|
||||||
|
zinit init -c /opt/services/
|
||||||
|
|
||||||
|
# Run in container mode
|
||||||
|
zinit init --container
|
||||||
|
```
|
||||||
|
|
||||||
|
### Service Management
|
||||||
|
|
||||||
|
#### `list`
|
||||||
|
|
||||||
|
Display a quick view of all currently known services and their status.
|
||||||
|
|
||||||
|
```bash
|
||||||
|
zinit list
|
||||||
|
```
|
||||||
|
|
||||||
|
**Output:**
|
||||||
|
A JSON object with service names as keys and their status as values.
|
||||||
|
|
||||||
|
**Example:**
|
||||||
|
```bash
|
||||||
|
# List all services
|
||||||
|
zinit list
|
||||||
|
```
|
||||||
|
|
||||||
|
#### `status`
|
||||||
|
|
||||||
|
Show detailed status information for a specific service.
|
||||||
|
|
||||||
|
```bash
|
||||||
|
zinit status <SERVICE>
|
||||||
|
```
|
||||||
|
|
||||||
|
**Arguments:**
|
||||||
|
- `<SERVICE>`: Name of the service to show status for
|
||||||
|
|
||||||
|
**Example:**
|
||||||
|
```bash
|
||||||
|
# Check status of redis service
|
||||||
|
zinit status redis
|
||||||
|
```
|
||||||
|
|
||||||
|
#### `start`
|
||||||
|
|
||||||
|
Start a service. Has no effect if the service is already running.
|
||||||
|
|
||||||
|
```bash
|
||||||
|
zinit start <SERVICE>
|
||||||
|
```
|
||||||
|
|
||||||
|
**Arguments:**
|
||||||
|
- `<SERVICE>`: Name of the service to start
|
||||||
|
|
||||||
|
**Example:**
|
||||||
|
```bash
|
||||||
|
# Start the nginx service
|
||||||
|
zinit start nginx
|
||||||
|
```
|
||||||
|
|
||||||
|
#### `stop`
|
||||||
|
|
||||||
|
Stop a service. Sets the target state to "down" and sends the stop signal.
|
||||||
|
|
||||||
|
```bash
|
||||||
|
zinit stop <SERVICE>
|
||||||
|
```
|
||||||
|
|
||||||
|
**Arguments:**
|
||||||
|
- `<SERVICE>`: Name of the service to stop
|
||||||
|
|
||||||
|
**Example:**
|
||||||
|
```bash
|
||||||
|
# Stop the redis service
|
||||||
|
zinit stop redis
|
||||||
|
```
|
||||||
|
|
||||||
|
#### `restart`
|
||||||
|
|
||||||
|
Restart a service. If it fails to stop, it will be killed and then started again.
|
||||||
|
|
||||||
|
```bash
|
||||||
|
zinit restart <SERVICE>
|
||||||
|
```
|
||||||
|
|
||||||
|
**Arguments:**
|
||||||
|
- `<SERVICE>`: Name of the service to restart
|
||||||
|
|
||||||
|
**Example:**
|
||||||
|
```bash
|
||||||
|
# Restart the web service
|
||||||
|
zinit restart web
|
||||||
|
```
|
||||||
|
|
||||||
|
#### `monitor`
|
||||||
|
|
||||||
|
Start monitoring a service. The configuration is loaded from the server's config directory.
|
||||||
|
|
||||||
|
```bash
|
||||||
|
zinit monitor <SERVICE>
|
||||||
|
```
|
||||||
|
|
||||||
|
**Arguments:**
|
||||||
|
- `<SERVICE>`: Name of the service to monitor
|
||||||
|
|
||||||
|
**Example:**
|
||||||
|
```bash
|
||||||
|
# Monitor the database service
|
||||||
|
zinit monitor database
|
||||||
|
```
|
||||||
|
|
||||||
|
#### `forget`
|
||||||
|
|
||||||
|
Remove a service from monitoring. You can only forget a stopped service.
|
||||||
|
|
||||||
|
```bash
|
||||||
|
zinit forget <SERVICE>
|
||||||
|
```
|
||||||
|
|
||||||
|
**Arguments:**
|
||||||
|
- `<SERVICE>`: Name of the service to forget
|
||||||
|
|
||||||
|
**Example:**
|
||||||
|
```bash
|
||||||
|
# Forget the backup service
|
||||||
|
zinit forget backup
|
||||||
|
```
|
||||||
|
|
||||||
|
#### `kill`
|
||||||
|
|
||||||
|
Send a signal to a running service.
|
||||||
|
|
||||||
|
```bash
|
||||||
|
zinit kill <SERVICE> <SIGNAL>
|
||||||
|
```
|
||||||
|
|
||||||
|
**Arguments:**
|
||||||
|
- `<SERVICE>`: Name of the service to send signal to
|
||||||
|
- `<SIGNAL>`: Signal name (e.g., SIGTERM, SIGKILL, SIGINT)
|
||||||
|
|
||||||
|
**Example:**
|
||||||
|
```bash
|
||||||
|
# Send SIGTERM to the redis service
|
||||||
|
zinit kill redis SIGTERM
|
||||||
|
|
||||||
|
# Send SIGKILL to force terminate a service
|
||||||
|
zinit kill stuck-service SIGKILL
|
||||||
|
```
|
||||||
|
|
||||||
|
### System Operations
|
||||||
|
|
||||||
|
#### `shutdown`
|
||||||
|
|
||||||
|
Stop all services in dependency order and power off the system.
|
||||||
|
|
||||||
|
```bash
|
||||||
|
zinit shutdown
|
||||||
|
```
|
||||||
|
|
||||||
|
**Example:**
|
||||||
|
```bash
|
||||||
|
# Shutdown the system
|
||||||
|
zinit shutdown
|
||||||
|
```
|
||||||
|
|
||||||
|
#### `reboot`
|
||||||
|
|
||||||
|
Stop all services in dependency order and reboot the system.
|
||||||
|
|
||||||
|
```bash
|
||||||
|
zinit reboot
|
||||||
|
```
|
||||||
|
|
||||||
|
**Example:**
|
||||||
|
```bash
|
||||||
|
# Reboot the system
|
||||||
|
zinit reboot
|
||||||
|
```
|
||||||
|
|
||||||
|
### Logging
|
||||||
|
|
||||||
|
#### `log`
|
||||||
|
|
||||||
|
View service logs from the Zinit ring buffer.
|
||||||
|
|
||||||
|
```bash
|
||||||
|
zinit log [FLAGS] [FILTER]
|
||||||
|
```
|
||||||
|
|
||||||
|
**Flags:**
|
||||||
|
- `-s, --snapshot`: If set, log prints current buffer without following
|
||||||
|
|
||||||
|
**Arguments:**
|
||||||
|
- `[FILTER]`: Optional service name to filter logs for
|
||||||
|
|
||||||
|
**Examples:**
|
||||||
|
```bash
|
||||||
|
# View logs for all services and follow new logs
|
||||||
|
zinit log
|
||||||
|
|
||||||
|
# View current logs for the nginx service without following
|
||||||
|
zinit log -s nginx
|
||||||
|
```
|
||||||
|
|
||||||
|
## Exit Codes
|
||||||
|
|
||||||
|
Zinit commands return the following exit codes:
|
||||||
|
|
||||||
|
| Code | Description |
|
||||||
|
|------|-------------|
|
||||||
|
| 0 | Success |
|
||||||
|
| 1 | Error (with error message printed to stderr) |
|
||||||
197
components/zinit/docs/installation.md
Normal file
197
components/zinit/docs/installation.md
Normal file
@@ -0,0 +1,197 @@
|
|||||||
|
# Installing Zinit
|
||||||
|
|
||||||
|
This guide provides detailed instructions for installing Zinit on various platforms.
|
||||||
|
|
||||||
|
## System Requirements
|
||||||
|
|
||||||
|
Zinit has minimal system requirements:
|
||||||
|
|
||||||
|
- Linux-based operating system
|
||||||
|
- Root access (for running as init system)
|
||||||
|
|
||||||
|
## Pre-built Binaries
|
||||||
|
|
||||||
|
If pre-built binaries are available for your system, you can install them directly:
|
||||||
|
|
||||||
|
```bash
|
||||||
|
# Download the binary (replace with actual URL)
|
||||||
|
wget https://github.com/threefoldtech/zinit/releases/download/vX.Y.Z/zinit-x86_64-unknown-linux-musl
|
||||||
|
|
||||||
|
# Make it executable
|
||||||
|
chmod +x zinit-x86_64-unknown-linux-musl
|
||||||
|
|
||||||
|
# Move to a location in your PATH
|
||||||
|
sudo mv zinit-x86_64-unknown-linux-musl /usr/local/bin/zinit
|
||||||
|
```
|
||||||
|
|
||||||
|
## Building from Source
|
||||||
|
|
||||||
|
### Prerequisites
|
||||||
|
|
||||||
|
To build Zinit from source, you'll need:
|
||||||
|
|
||||||
|
- Rust toolchain (1.46.0 or later recommended)
|
||||||
|
- musl and musl-tools packages
|
||||||
|
- GNU Make
|
||||||
|
|
||||||
|
#### Install Rust
|
||||||
|
|
||||||
|
If you don't have Rust installed, use rustup:
|
||||||
|
|
||||||
|
```bash
|
||||||
|
curl --proto '=https' --tlsv1.2 -sSf https://sh.rustup.rs | sh
|
||||||
|
source $HOME/.cargo/env
|
||||||
|
```
|
||||||
|
|
||||||
|
#### Install musl development tools
|
||||||
|
|
||||||
|
On Debian/Ubuntu:
|
||||||
|
|
||||||
|
```bash
|
||||||
|
sudo apt update
|
||||||
|
sudo apt install musl musl-tools
|
||||||
|
```
|
||||||
|
|
||||||
|
On Fedora:
|
||||||
|
|
||||||
|
```bash
|
||||||
|
sudo dnf install musl musl-devel
|
||||||
|
```
|
||||||
|
|
||||||
|
On Alpine Linux (musl is already the default libc):
|
||||||
|
|
||||||
|
```bash
|
||||||
|
apk add build-base
|
||||||
|
```
|
||||||
|
|
||||||
|
### Build Process
|
||||||
|
|
||||||
|
1. Clone the repository:
|
||||||
|
|
||||||
|
```bash
|
||||||
|
git clone https://github.com/threefoldtech/zinit.git
|
||||||
|
cd zinit
|
||||||
|
```
|
||||||
|
|
||||||
|
2. Build using make:
|
||||||
|
|
||||||
|
```bash
|
||||||
|
make
|
||||||
|
```
|
||||||
|
|
||||||
|
This will create a statically linked binary at `target/x86_64-unknown-linux-musl/release/zinit`.
|
||||||
|
|
||||||
|
3. Install the binary:
|
||||||
|
|
||||||
|
```bash
|
||||||
|
sudo cp target/x86_64-unknown-linux-musl/release/zinit /usr/local/bin/
|
||||||
|
```
|
||||||
|
|
||||||
|
### Development Build
|
||||||
|
|
||||||
|
For development or debugging:
|
||||||
|
|
||||||
|
```bash
|
||||||
|
make dev
|
||||||
|
```
|
||||||
|
|
||||||
|
## Docker Installation
|
||||||
|
|
||||||
|
### Using the Provided Dockerfile
|
||||||
|
|
||||||
|
Zinit includes a test Docker image:
|
||||||
|
|
||||||
|
```bash
|
||||||
|
# Build the Docker image
|
||||||
|
make docker
|
||||||
|
|
||||||
|
# Run the container
|
||||||
|
docker run -dt --device=/dev/kmsg:/dev/kmsg:rw zinit
|
||||||
|
```
|
||||||
|
> Don't forget to port-forward a port to get access to the Zinit proxy using the `-p XXXX:YYYY` flag when running the container.
|
||||||
|
|
||||||
|
### Custom Docker Setup
|
||||||
|
|
||||||
|
To create your own Dockerfile with Zinit:
|
||||||
|
|
||||||
|
```dockerfile
|
||||||
|
FROM alpine:latest
|
||||||
|
|
||||||
|
# Install dependencies if needed
|
||||||
|
RUN apk add --no-cache bash curl
|
||||||
|
|
||||||
|
# Copy the zinit binary
|
||||||
|
COPY zinit /usr/local/bin/zinit
|
||||||
|
RUN chmod +x /usr/local/bin/zinit
|
||||||
|
|
||||||
|
# Create configuration directory
|
||||||
|
RUN mkdir -p /etc/zinit
|
||||||
|
|
||||||
|
# Add your service configurations
|
||||||
|
COPY services/*.yaml /etc/zinit/
|
||||||
|
|
||||||
|
# Set zinit as the entrypoint
|
||||||
|
ENTRYPOINT ["/usr/local/bin/zinit", "init", "--container"]
|
||||||
|
```
|
||||||
|
|
||||||
|
## Using Zinit as the Init System
|
||||||
|
|
||||||
|
To use Zinit as the init system (PID 1) on a Linux system:
|
||||||
|
|
||||||
|
### On a Standard Linux System
|
||||||
|
|
||||||
|
1. Install Zinit as described above
|
||||||
|
2. Create your service configurations in `/etc/zinit/`
|
||||||
|
3. Configure your bootloader to use zinit as init
|
||||||
|
|
||||||
|
For GRUB, add `init=/usr/local/bin/zinit` to the kernel command line:
|
||||||
|
|
||||||
|
```bash
|
||||||
|
# Edit GRUB configuration
|
||||||
|
sudo nano /etc/default/grub
|
||||||
|
|
||||||
|
# Add init parameter to GRUB_CMDLINE_LINUX
|
||||||
|
# Example:
|
||||||
|
# GRUB_CMDLINE_LINUX="init=/usr/local/bin/zinit"
|
||||||
|
|
||||||
|
# Update GRUB
|
||||||
|
sudo update-grub
|
||||||
|
```
|
||||||
|
|
||||||
|
### In a Container Environment
|
||||||
|
|
||||||
|
For containers, simply set Zinit as the entrypoint:
|
||||||
|
|
||||||
|
```bash
|
||||||
|
docker run -dt --device=/dev/kmsg:/dev/kmsg:rw \
|
||||||
|
--entrypoint /usr/local/bin/zinit \
|
||||||
|
your-image init --container
|
||||||
|
```
|
||||||
|
|
||||||
|
## First-time Setup
|
||||||
|
|
||||||
|
After installation, you'll need to create a basic configuration:
|
||||||
|
|
||||||
|
1. Create the configuration directory:
|
||||||
|
|
||||||
|
```bash
|
||||||
|
sudo mkdir -p /etc/zinit
|
||||||
|
```
|
||||||
|
|
||||||
|
2. Create a simple service configuration:
|
||||||
|
|
||||||
|
```bash
|
||||||
|
cat << EOF | sudo tee /etc/zinit/hello.yaml
|
||||||
|
exec: "echo 'Hello from Zinit!'"
|
||||||
|
oneshot: true
|
||||||
|
EOF
|
||||||
|
```
|
||||||
|
|
||||||
|
3. Test Zinit without running as init:
|
||||||
|
|
||||||
|
```bash
|
||||||
|
# For testing only - doesn't replace system init
|
||||||
|
sudo zinit init
|
||||||
|
```
|
||||||
|
|
||||||
|
If all is working correctly, you should see Zinit start and run your service.
|
||||||
78
components/zinit/docs/osx_cross_compile.md
Normal file
78
components/zinit/docs/osx_cross_compile.md
Normal file
@@ -0,0 +1,78 @@
|
|||||||
|
# macOS Guide for Zinit
|
||||||
|
|
||||||
|
This guide covers both building Zinit natively on macOS and cross-compiling from macOS to Linux targets.
|
||||||
|
|
||||||
|
## Building Zinit Natively on macOS
|
||||||
|
|
||||||
|
Zinit can now be built and run directly on macOS. The code has been updated to handle platform-specific differences between Linux and macOS.
|
||||||
|
|
||||||
|
### Building for macOS
|
||||||
|
|
||||||
|
```bash
|
||||||
|
# Build a release version for macOS
|
||||||
|
make release-macos
|
||||||
|
|
||||||
|
# Install to ~/hero/bin (if it exists)
|
||||||
|
make install-macos
|
||||||
|
```
|
||||||
|
|
||||||
|
The native macOS build provides most of Zinit's functionality, with the following limitations:
|
||||||
|
- System reboot and shutdown operations are not supported (they will exit the process instead)
|
||||||
|
- Some Linux-specific features are disabled
|
||||||
|
|
||||||
|
## Cross-Compilation from macOS to Linux
|
||||||
|
|
||||||
|
This section outlines the steps to set up your macOS environment for cross-compiling Rust projects to the `aarch64-unknown-linux-musl` target. This is particularly useful for building binaries that can run on ARM-based Linux systems (e.g., Raspberry Pi, AWS Graviton) using musl libc.
|
||||||
|
|
||||||
|
## Prerequisites
|
||||||
|
|
||||||
|
* Homebrew (https://brew.sh/) installed on your macOS system.
|
||||||
|
* Rust and Cargo installed (e.g., via `rustup`).
|
||||||
|
|
||||||
|
## Step 1: Install the `aarch64-linux-musl-gcc` Toolchain
|
||||||
|
|
||||||
|
The `aarch64-linux-musl-gcc` toolchain is required for linking when cross-compiling to `aarch64-unknown-linux-musl`. You can install it using Homebrew:
|
||||||
|
|
||||||
|
```bash
|
||||||
|
brew install messense/macos-cross-toolchains/aarch64-linux-musl-cross
|
||||||
|
```
|
||||||
|
|
||||||
|
## Step 2: Link `musl-gcc`
|
||||||
|
|
||||||
|
Some build scripts or tools might look for `musl-gcc`. To ensure compatibility, create a symbolic link:
|
||||||
|
|
||||||
|
```bash
|
||||||
|
sudo ln -s /opt/homebrew/bin/aarch64-linux-musl-gcc /opt/homebrew/bin/musl-gcc
|
||||||
|
```
|
||||||
|
|
||||||
|
You might be prompted for your system password to complete this operation.
|
||||||
|
|
||||||
|
## Step 3: Add the Rust Target
|
||||||
|
|
||||||
|
Add the `aarch64-unknown-linux-musl` target to your Rust toolchain:
|
||||||
|
|
||||||
|
```bash
|
||||||
|
rustup target add aarch64-unknown-linux-musl
|
||||||
|
```
|
||||||
|
|
||||||
|
## Step 4: Build Your Project
|
||||||
|
|
||||||
|
Now you can build your Rust project for the `aarch64-unknown-linux-musl` target using Cargo:
|
||||||
|
|
||||||
|
```bash
|
||||||
|
cargo build --release --target aarch64-unknown-linux-musl
|
||||||
|
```
|
||||||
|
|
||||||
|
Alternatively, if you are using the provided `Makefile`, you can use the new target:
|
||||||
|
|
||||||
|
```bash
|
||||||
|
make release-aarch64-musl
|
||||||
|
```
|
||||||
|
|
||||||
|
This will produce a release binary located in `target/aarch64-unknown-linux-musl/release/`.
|
||||||
|
|
||||||
|
## Step 5: copy to osx hero bin
|
||||||
|
|
||||||
|
```bash
|
||||||
|
cp target/aarch64-unknown-linux-musl/release/zinit ~/hero/bin
|
||||||
|
```
|
||||||
217
components/zinit/docs/services.md
Normal file
217
components/zinit/docs/services.md
Normal file
@@ -0,0 +1,217 @@
|
|||||||
|
# Service Configuration Format
|
||||||
|
|
||||||
|
This document describes the structure and options for Zinit service configuration files.
|
||||||
|
|
||||||
|
## File Format
|
||||||
|
|
||||||
|
Zinit uses YAML files for service configuration. Each service has its own configuration file stored in the Zinit configuration directory (default: `/etc/zinit`).
|
||||||
|
|
||||||
|
### File Naming and Location
|
||||||
|
|
||||||
|
- **Location**: `/etc/zinit/` (default, can be changed with `-c` flag)
|
||||||
|
- on osx `~/hero/cfg/zinit`
|
||||||
|
- **Naming**: `<service-name>.yaml`
|
||||||
|
|
||||||
|
For example:
|
||||||
|
- `/etc/zinit/nginx.yaml`
|
||||||
|
- `/etc/zinit/redis.yaml`
|
||||||
|
|
||||||
|
## Configuration Schema
|
||||||
|
|
||||||
|
Service configuration files use the following schema:
|
||||||
|
|
||||||
|
```yaml
|
||||||
|
# Command to run (required)
|
||||||
|
exec: "command line to start service"
|
||||||
|
|
||||||
|
# Command to test if service is running (optional)
|
||||||
|
test: "command line to test service"
|
||||||
|
|
||||||
|
# Whether the service should be restarted (optional, default: false)
|
||||||
|
oneshot: true|false
|
||||||
|
|
||||||
|
# Maximum time to wait for service to stop during shutdown (optional, default: 10)
|
||||||
|
shutdown_timeout: 30
|
||||||
|
|
||||||
|
# Services that must be running before this one starts (optional)
|
||||||
|
after:
|
||||||
|
- service1_name
|
||||||
|
- service2_name
|
||||||
|
|
||||||
|
# Signals configuration (optional)
|
||||||
|
signal:
|
||||||
|
stop: SIGKILL # signal sent on 'stop' action (default: SIGTERM)
|
||||||
|
|
||||||
|
# Log handling configuration (optional, default: ring)
|
||||||
|
log: null|ring|stdout
|
||||||
|
|
||||||
|
# Environment variables for the service (optional)
|
||||||
|
env:
|
||||||
|
KEY1: "VALUE1"
|
||||||
|
KEY2: "VALUE2"
|
||||||
|
|
||||||
|
# Working directory for the service (optional)
|
||||||
|
dir: "/path/to/working/directory"
|
||||||
|
```
|
||||||
|
|
||||||
|
## Configuration Options
|
||||||
|
|
||||||
|
### Required Fields
|
||||||
|
|
||||||
|
| Field | Description |
|
||||||
|
|-------|-------------|
|
||||||
|
| `exec` | Command line to execute when starting the service |
|
||||||
|
|
||||||
|
### Optional Fields
|
||||||
|
|
||||||
|
| Field | Type | Default | Description |
|
||||||
|
|-------|------|---------|-------------|
|
||||||
|
| `test` | String | - | Command to determine if service is running |
|
||||||
|
| `oneshot` | Boolean | `false` | If true, service won't be restarted after exit |
|
||||||
|
| `shutdown_timeout` | Integer | 10 | Seconds to wait for service to stop during shutdown |
|
||||||
|
| `after` | String[] | `[]` | List of services that must be running first |
|
||||||
|
| `signal.stop` | String | `"sigterm"` | Signal to send when stopping the service |
|
||||||
|
| `log` | Enum | `ring` | How to handle service output (null, ring, stdout) |
|
||||||
|
| `env` | Object | `{}` | Environment variables to pass to the service |
|
||||||
|
| `dir` | String | `""` | Working directory for the service |
|
||||||
|
|
||||||
|
## Field Details
|
||||||
|
|
||||||
|
### exec
|
||||||
|
|
||||||
|
The command to run when starting the service. This is the only required field in the configuration.
|
||||||
|
|
||||||
|
```yaml
|
||||||
|
exec: "/usr/bin/redis-server --port 6379"
|
||||||
|
```
|
||||||
|
|
||||||
|
Shell-style commands are supported:
|
||||||
|
|
||||||
|
```yaml
|
||||||
|
exec: "sh -c 'echo Starting service && /usr/local/bin/myservice'"
|
||||||
|
```
|
||||||
|
|
||||||
|
### test
|
||||||
|
|
||||||
|
Command that tests whether the service is running properly. Zinit runs this command periodically until it succeeds (exit code 0), at which point the service is considered running.
|
||||||
|
|
||||||
|
```yaml
|
||||||
|
test: "redis-cli -p 6379 PING"
|
||||||
|
```
|
||||||
|
|
||||||
|
If no test command is provided, the service is considered running as soon as it's started.
|
||||||
|
|
||||||
|
### oneshot
|
||||||
|
|
||||||
|
When set to `true`, the service will not be automatically restarted when it exits. This is useful for initialization tasks or commands that should run only once.
|
||||||
|
|
||||||
|
```yaml
|
||||||
|
oneshot: true
|
||||||
|
```
|
||||||
|
|
||||||
|
Services that depend on a oneshot service will start only after the oneshot service has exited successfully.
|
||||||
|
|
||||||
|
### shutdown_timeout
|
||||||
|
|
||||||
|
How long (in seconds) to wait for the service to stop during system shutdown before giving up:
|
||||||
|
|
||||||
|
```yaml
|
||||||
|
shutdown_timeout: 30 # Wait up to 30 seconds
|
||||||
|
```
|
||||||
|
|
||||||
|
### after
|
||||||
|
|
||||||
|
List of service names that must be running (or completed successfully for oneshot services) before this service starts:
|
||||||
|
|
||||||
|
```yaml
|
||||||
|
after:
|
||||||
|
- networking
|
||||||
|
- database
|
||||||
|
```
|
||||||
|
|
||||||
|
### signal
|
||||||
|
|
||||||
|
Custom signals to use for operations. Currently, only the `stop` signal is configurable:
|
||||||
|
|
||||||
|
```yaml
|
||||||
|
signal:
|
||||||
|
stop: SIGKILL # Use SIGKILL instead of default SIGTERM
|
||||||
|
```
|
||||||
|
|
||||||
|
Valid signal names follow the standard UNIX signal naming (SIGTERM, SIGKILL, SIGINT, etc).
|
||||||
|
|
||||||
|
### log
|
||||||
|
|
||||||
|
How to handle stdout/stderr output from the service:
|
||||||
|
|
||||||
|
```yaml
|
||||||
|
log: stdout # Print output to zinit's stdout
|
||||||
|
```
|
||||||
|
|
||||||
|
Options:
|
||||||
|
- `null`: Ignore all service output (like redirecting to /dev/null)
|
||||||
|
- `ring`: Store logs in the kernel ring buffer with service name prefix (default)
|
||||||
|
- `stdout`: Send service output to zinit's stdout
|
||||||
|
|
||||||
|
> **Note**: To use `ring` inside Docker, make sure to add the `kmsg` device:
|
||||||
|
> ```
|
||||||
|
> docker run -dt --device=/dev/kmsg:/dev/kmsg:rw zinit
|
||||||
|
> ```
|
||||||
|
|
||||||
|
### env
|
||||||
|
|
||||||
|
Additional environment variables for the service. These are added to the existing environment:
|
||||||
|
|
||||||
|
```yaml
|
||||||
|
env:
|
||||||
|
PORT: "8080"
|
||||||
|
DEBUG: "true"
|
||||||
|
NODE_ENV: "production"
|
||||||
|
```
|
||||||
|
|
||||||
|
### dir
|
||||||
|
|
||||||
|
Working directory for the service process:
|
||||||
|
|
||||||
|
```yaml
|
||||||
|
dir: "/var/lib/myservice"
|
||||||
|
```
|
||||||
|
|
||||||
|
If not specified, the process inherits zinit's working directory.
|
||||||
|
|
||||||
|
## Example Configurations
|
||||||
|
|
||||||
|
### Web Server
|
||||||
|
|
||||||
|
```yaml
|
||||||
|
exec: "/usr/bin/nginx -g 'daemon off;'"
|
||||||
|
test: "curl -s http://localhost > /dev/null"
|
||||||
|
after:
|
||||||
|
- networking
|
||||||
|
log: stdout
|
||||||
|
```
|
||||||
|
|
||||||
|
### Database Initialization
|
||||||
|
|
||||||
|
```yaml
|
||||||
|
exec: "sh -c 'echo Creating database schema && /usr/bin/db-migrate'"
|
||||||
|
oneshot: true
|
||||||
|
dir: "/opt/myapp"
|
||||||
|
env:
|
||||||
|
DB_HOST: "localhost"
|
||||||
|
DB_USER: "admin"
|
||||||
|
```
|
||||||
|
|
||||||
|
### Application with Dependencies
|
||||||
|
|
||||||
|
```yaml
|
||||||
|
exec: "/usr/bin/myapp --config /etc/myapp.conf"
|
||||||
|
test: "curl -s http://localhost:8080/health > /dev/null"
|
||||||
|
after:
|
||||||
|
- database
|
||||||
|
- cache
|
||||||
|
signal:
|
||||||
|
stop: SIGINT # Use SIGINT for graceful shutdown
|
||||||
|
env:
|
||||||
|
PORT: "8080"
|
||||||
|
shutdown_timeout: 20
|
||||||
366
components/zinit/docs/shutdown_improvement_plan.md
Normal file
366
components/zinit/docs/shutdown_improvement_plan.md
Normal file
@@ -0,0 +1,366 @@
|
|||||||
|
# Zinit Shutdown Functionality Improvement Plan
|
||||||
|
|
||||||
|
## Current Issues
|
||||||
|
|
||||||
|
1. **Incomplete Child Process Termination**: When services are stopped, child processes may remain running.
|
||||||
|
2. **Lack of Verification**: There's no verification that all processes are actually terminated.
|
||||||
|
3. **Improper Graceful Shutdown**: Zinit doesn't wait for all processes to terminate before exiting.
|
||||||
|
|
||||||
|
## Solution Overview
|
||||||
|
|
||||||
|
We'll implement a robust shutdown mechanism that:
|
||||||
|
1. Uses our stats functionality to detect all child processes
|
||||||
|
2. Properly manages process groups
|
||||||
|
3. Verifies all processes are terminated before Zinit exits
|
||||||
|
|
||||||
|
## Implementation Plan
|
||||||
|
|
||||||
|
```mermaid
|
||||||
|
flowchart TD
|
||||||
|
A[Enhance stop method] --> B[Improve kill_process_tree]
|
||||||
|
B --> C[Add process verification]
|
||||||
|
C --> D[Implement graceful shutdown]
|
||||||
|
|
||||||
|
A1[Use stats to detect child processes] --> A
|
||||||
|
A2[Send signals to all processes] --> A
|
||||||
|
A3[Implement cascading termination] --> A
|
||||||
|
|
||||||
|
B1[Ensure proper process group handling] --> B
|
||||||
|
B2[Add timeout and escalation logic] --> B
|
||||||
|
|
||||||
|
C1[Create verification mechanism] --> C
|
||||||
|
C2[Add polling for process existence] --> C
|
||||||
|
|
||||||
|
D1[Wait for all processes to terminate] --> D
|
||||||
|
D2[Add cleanup of resources] --> D
|
||||||
|
D3[Implement clean exit] --> D
|
||||||
|
```
|
||||||
|
|
||||||
|
## Detailed Implementation Steps
|
||||||
|
|
||||||
|
### 1. Enhance the `stop` Method in `LifecycleManager`
|
||||||
|
|
||||||
|
```rust
|
||||||
|
pub async fn stop<S: AsRef<str>>(&self, name: S) -> Result<()> {
|
||||||
|
// Get service information
|
||||||
|
let table = self.services.read().await;
|
||||||
|
let service = table.get(name.as_ref())
|
||||||
|
.ok_or_else(|| ZInitError::unknown_service(name.as_ref()))?;
|
||||||
|
|
||||||
|
let mut service = service.write().await;
|
||||||
|
service.set_target(Target::Down);
|
||||||
|
|
||||||
|
// Get the main process PID
|
||||||
|
let pid = service.pid;
|
||||||
|
if pid.as_raw() == 0 {
|
||||||
|
return Ok(());
|
||||||
|
}
|
||||||
|
|
||||||
|
// Get the signal to use
|
||||||
|
let signal = signal::Signal::from_str(&service.service.signal.stop.to_uppercase())
|
||||||
|
.map_err(|err| anyhow::anyhow!("unknown stop signal: {}", err))?;
|
||||||
|
|
||||||
|
// Release the lock before potentially long-running operations
|
||||||
|
drop(service);
|
||||||
|
drop(table);
|
||||||
|
|
||||||
|
// Get all child processes using our stats functionality
|
||||||
|
let children = self.get_child_process_stats(pid.as_raw()).await?;
|
||||||
|
|
||||||
|
// First try to stop the process group
|
||||||
|
let _ = self.pm.signal(pid, signal);
|
||||||
|
|
||||||
|
// Wait a short time for processes to terminate gracefully
|
||||||
|
sleep(std::time::Duration::from_millis(500)).await;
|
||||||
|
|
||||||
|
// Check if processes are still running and use SIGKILL if needed
|
||||||
|
self.ensure_processes_terminated(pid.as_raw(), &children).await?;
|
||||||
|
|
||||||
|
Ok(())
|
||||||
|
}
|
||||||
|
```
|
||||||
|
|
||||||
|
### 2. Add a New `ensure_processes_terminated` Method
|
||||||
|
|
||||||
|
```rust
|
||||||
|
async fn ensure_processes_terminated(&self, parent_pid: i32, children: &[ProcessStats]) -> Result<()> {
|
||||||
|
// Check if parent is still running
|
||||||
|
let parent_running = self.is_process_running(parent_pid).await?;
|
||||||
|
|
||||||
|
// If parent is still running, send SIGKILL
|
||||||
|
if parent_running {
|
||||||
|
debug!("Process {} still running after SIGTERM, sending SIGKILL", parent_pid);
|
||||||
|
let _ = self.pm.signal(Pid::from_raw(parent_pid), signal::Signal::SIGKILL);
|
||||||
|
}
|
||||||
|
|
||||||
|
// Check and kill any remaining child processes
|
||||||
|
for child in children {
|
||||||
|
if self.is_process_running(child.pid).await? {
|
||||||
|
debug!("Child process {} still running, sending SIGKILL", child.pid);
|
||||||
|
let _ = signal::kill(Pid::from_raw(child.pid), signal::Signal::SIGKILL);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
// Verify all processes are gone
|
||||||
|
let mut retries = 5;
|
||||||
|
while retries > 0 {
|
||||||
|
let mut all_terminated = true;
|
||||||
|
|
||||||
|
// Check parent
|
||||||
|
if self.is_process_running(parent_pid).await? {
|
||||||
|
all_terminated = false;
|
||||||
|
}
|
||||||
|
|
||||||
|
// Check children
|
||||||
|
for child in children {
|
||||||
|
if self.is_process_running(child.pid).await? {
|
||||||
|
all_terminated = false;
|
||||||
|
break;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
if all_terminated {
|
||||||
|
return Ok(());
|
||||||
|
}
|
||||||
|
|
||||||
|
// Wait before retrying
|
||||||
|
sleep(std::time::Duration::from_millis(100)).await;
|
||||||
|
retries -= 1;
|
||||||
|
}
|
||||||
|
|
||||||
|
// If we get here, some processes might still be running
|
||||||
|
warn!("Some processes may still be running after shutdown attempts");
|
||||||
|
Ok(())
|
||||||
|
}
|
||||||
|
```
|
||||||
|
|
||||||
|
### 3. Add a Helper Method to Check if a Process is Running
|
||||||
|
|
||||||
|
```rust
|
||||||
|
async fn is_process_running(&self, pid: i32) -> Result<bool> {
|
||||||
|
// Use sysinfo to check if process exists
|
||||||
|
let mut system = System::new();
|
||||||
|
let sys_pid = sysinfo::Pid::from(pid as usize);
|
||||||
|
system.refresh_process(sys_pid);
|
||||||
|
|
||||||
|
Ok(system.process(sys_pid).is_some())
|
||||||
|
}
|
||||||
|
```
|
||||||
|
|
||||||
|
### 4. Improve the `kill_process_tree` Method
|
||||||
|
|
||||||
|
```rust
|
||||||
|
#[cfg(target_os = "linux")]
|
||||||
|
async fn kill_process_tree(
|
||||||
|
&self,
|
||||||
|
mut dag: ProcessDAG,
|
||||||
|
mut state_channels: HashMap<String, Watcher<State>>,
|
||||||
|
mut shutdown_timeouts: HashMap<String, u64>,
|
||||||
|
) -> Result<()> {
|
||||||
|
let (tx, mut rx) = mpsc::unbounded_channel();
|
||||||
|
tx.send(DUMMY_ROOT.into())?;
|
||||||
|
|
||||||
|
let mut count = dag.count;
|
||||||
|
while let Some(name) = rx.recv().await {
|
||||||
|
debug!("{} has been killed (or was inactive) adding its children", name);
|
||||||
|
|
||||||
|
for child in dag.adj.get(&name).unwrap_or(&Vec::new()) {
|
||||||
|
let child_indegree: &mut u32 = dag.indegree.entry(child.clone()).or_default();
|
||||||
|
*child_indegree -= 1;
|
||||||
|
|
||||||
|
debug!("decrementing child {} indegree to {}", child, child_indegree);
|
||||||
|
|
||||||
|
if *child_indegree == 0 {
|
||||||
|
let watcher = state_channels.remove(child);
|
||||||
|
if watcher.is_none() {
|
||||||
|
// not an active service
|
||||||
|
tx.send(child.to_string())?;
|
||||||
|
continue;
|
||||||
|
}
|
||||||
|
|
||||||
|
let shutdown_timeout = shutdown_timeouts.remove(child);
|
||||||
|
let lifecycle = self.clone_lifecycle();
|
||||||
|
|
||||||
|
// Spawn a task to kill the service and wait for it to terminate
|
||||||
|
let kill_task = tokio::spawn(Self::kill_wait_enhanced(
|
||||||
|
lifecycle,
|
||||||
|
child.to_string(),
|
||||||
|
tx.clone(),
|
||||||
|
watcher.unwrap(),
|
||||||
|
shutdown_timeout.unwrap_or(config::DEFAULT_SHUTDOWN_TIMEOUT),
|
||||||
|
));
|
||||||
|
|
||||||
|
// Add a timeout to ensure we don't wait forever
|
||||||
|
let _ = tokio::time::timeout(
|
||||||
|
std::time::Duration::from_secs(shutdown_timeout.unwrap_or(config::DEFAULT_SHUTDOWN_TIMEOUT) + 2),
|
||||||
|
kill_task
|
||||||
|
).await;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
count -= 1;
|
||||||
|
if count == 0 {
|
||||||
|
break;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
// Final verification that all processes are gone
|
||||||
|
self.verify_all_processes_terminated().await?;
|
||||||
|
|
||||||
|
Ok(())
|
||||||
|
}
|
||||||
|
```
|
||||||
|
|
||||||
|
### 5. Add an Enhanced `kill_wait` Method
|
||||||
|
|
||||||
|
```rust
|
||||||
|
#[cfg(target_os = "linux")]
|
||||||
|
async fn kill_wait_enhanced(
|
||||||
|
self,
|
||||||
|
name: String,
|
||||||
|
ch: mpsc::UnboundedSender<String>,
|
||||||
|
mut rx: Watcher<State>,
|
||||||
|
shutdown_timeout: u64,
|
||||||
|
) {
|
||||||
|
debug!("kill_wait {}", name);
|
||||||
|
|
||||||
|
// Try to stop the service gracefully
|
||||||
|
let stop_result = self.stop(name.clone()).await;
|
||||||
|
|
||||||
|
// Wait for the service to become inactive or timeout
|
||||||
|
let fut = timeout(
|
||||||
|
std::time::Duration::from_secs(shutdown_timeout),
|
||||||
|
async move {
|
||||||
|
while let Some(state) = rx.next().await {
|
||||||
|
if !state.is_active() {
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
},
|
||||||
|
);
|
||||||
|
|
||||||
|
match stop_result {
|
||||||
|
Ok(_) => {
|
||||||
|
let _ = fut.await;
|
||||||
|
}
|
||||||
|
Err(e) => error!("couldn't stop service {}: {}", name.clone(), e),
|
||||||
|
}
|
||||||
|
|
||||||
|
// Verify the service is actually stopped
|
||||||
|
if let Ok(status) = self.status(&name).await {
|
||||||
|
if status.pid != 0 {
|
||||||
|
// Service is still running, try to kill it
|
||||||
|
let _ = self.kill(&name, signal::Signal::SIGKILL).await;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
debug!("sending to the death channel {}", name.clone());
|
||||||
|
if let Err(e) = ch.send(name.clone()) {
|
||||||
|
error!(
|
||||||
|
"error: couldn't send the service {} to the shutdown loop: {}",
|
||||||
|
name, e
|
||||||
|
);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
```
|
||||||
|
|
||||||
|
### 6. Add a Method to Verify All Processes are Terminated
|
||||||
|
|
||||||
|
```rust
|
||||||
|
async fn verify_all_processes_terminated(&self) -> Result<()> {
|
||||||
|
// Get all services
|
||||||
|
let table = self.services.read().await;
|
||||||
|
|
||||||
|
// Check each service
|
||||||
|
for (name, service) in table.iter() {
|
||||||
|
let service = service.read().await;
|
||||||
|
let pid = service.pid.as_raw();
|
||||||
|
|
||||||
|
// Skip services with no PID
|
||||||
|
if pid == 0 {
|
||||||
|
continue;
|
||||||
|
}
|
||||||
|
|
||||||
|
// Check if the main process is still running
|
||||||
|
if self.is_process_running(pid).await? {
|
||||||
|
warn!("Service {} (PID {}) is still running after shutdown", name, pid);
|
||||||
|
|
||||||
|
// Try to kill it with SIGKILL
|
||||||
|
let _ = signal::kill(Pid::from_raw(pid), signal::Signal::SIGKILL);
|
||||||
|
}
|
||||||
|
|
||||||
|
// Check for child processes
|
||||||
|
if let Ok(children) = self.get_child_process_stats(pid).await {
|
||||||
|
for child in children {
|
||||||
|
if self.is_process_running(child.pid).await? {
|
||||||
|
warn!("Child process {} of service {} is still running after shutdown",
|
||||||
|
child.pid, name);
|
||||||
|
|
||||||
|
// Try to kill it with SIGKILL
|
||||||
|
let _ = signal::kill(Pid::from_raw(child.pid), signal::Signal::SIGKILL);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
Ok(())
|
||||||
|
}
|
||||||
|
```
|
||||||
|
|
||||||
|
### 7. Update the `shutdown` and `reboot` Methods
|
||||||
|
|
||||||
|
```rust
|
||||||
|
pub async fn shutdown(&self) -> Result<()> {
|
||||||
|
info!("shutting down");
|
||||||
|
|
||||||
|
// Set the shutdown flag
|
||||||
|
*self.shutdown.write().await = true;
|
||||||
|
|
||||||
|
#[cfg(target_os = "linux")]
|
||||||
|
{
|
||||||
|
// Power off using our enhanced method
|
||||||
|
let result = self.power(RebootMode::RB_POWER_OFF).await;
|
||||||
|
|
||||||
|
// Final verification before exit
|
||||||
|
self.verify_all_processes_terminated().await?;
|
||||||
|
|
||||||
|
return result;
|
||||||
|
}
|
||||||
|
|
||||||
|
#[cfg(not(target_os = "linux"))]
|
||||||
|
{
|
||||||
|
// Stop all services
|
||||||
|
let services = self.list().await?;
|
||||||
|
for service in services {
|
||||||
|
let _ = self.stop(&service).await;
|
||||||
|
}
|
||||||
|
|
||||||
|
// Verify all processes are terminated
|
||||||
|
self.verify_all_processes_terminated().await?;
|
||||||
|
|
||||||
|
if self.container {
|
||||||
|
std::process::exit(0);
|
||||||
|
} else {
|
||||||
|
info!("System shutdown not supported on this platform");
|
||||||
|
std::process::exit(0);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
```
|
||||||
|
|
||||||
|
## Testing Plan
|
||||||
|
|
||||||
|
1. **Basic Service Termination**: Test that a simple service is properly terminated
|
||||||
|
2. **Child Process Termination**: Test that a service with child processes has all processes terminated
|
||||||
|
3. **Graceful Shutdown**: Test that Zinit exits cleanly after all services are stopped
|
||||||
|
4. **Edge Cases**:
|
||||||
|
- Test with services that spawn many child processes
|
||||||
|
- Test with services that spawn child processes that change their process group
|
||||||
|
- Test with services that ignore SIGTERM
|
||||||
|
|
||||||
|
## Implementation Timeline
|
||||||
|
|
||||||
|
1. **Phase 1**: Enhance the `stop` method and add the helper methods (1-2 hours)
|
||||||
|
2. **Phase 2**: Improve the `kill_process_tree` and `kill_wait` methods (1-2 hours)
|
||||||
|
3. **Phase 3**: Update the `shutdown` and `reboot` methods (1 hour)
|
||||||
|
4. **Phase 4**: Testing and debugging (2-3 hours)
|
||||||
125
components/zinit/docs/stats.md
Normal file
125
components/zinit/docs/stats.md
Normal file
@@ -0,0 +1,125 @@
|
|||||||
|
# Service Stats Functionality
|
||||||
|
|
||||||
|
This document describes the stats functionality in Zinit, which provides memory and CPU usage information for services and their child processes.
|
||||||
|
|
||||||
|
## Overview
|
||||||
|
|
||||||
|
The stats functionality allows you to monitor the resource usage of services managed by Zinit. It provides information about:
|
||||||
|
|
||||||
|
- Memory usage (in bytes)
|
||||||
|
- CPU usage (as a percentage)
|
||||||
|
- Child processes and their resource usage
|
||||||
|
|
||||||
|
This is particularly useful for monitoring system resources and identifying services that might be consuming excessive resources.
|
||||||
|
|
||||||
|
## Command Line Usage
|
||||||
|
|
||||||
|
To get stats for a service using the command line:
|
||||||
|
|
||||||
|
```bash
|
||||||
|
zinit stats <service-name>
|
||||||
|
```
|
||||||
|
|
||||||
|
Example:
|
||||||
|
```bash
|
||||||
|
zinit stats nginx
|
||||||
|
```
|
||||||
|
|
||||||
|
This will output YAML-formatted stats information:
|
||||||
|
|
||||||
|
```yaml
|
||||||
|
name: nginx
|
||||||
|
pid: 1234
|
||||||
|
memory_usage: 10485760 # Memory usage in bytes (10MB)
|
||||||
|
cpu_usage: 2.5 # CPU usage as percentage
|
||||||
|
children: # Stats for child processes
|
||||||
|
- pid: 1235
|
||||||
|
memory_usage: 5242880
|
||||||
|
cpu_usage: 1.2
|
||||||
|
- pid: 1236
|
||||||
|
memory_usage: 4194304
|
||||||
|
cpu_usage: 0.8
|
||||||
|
```
|
||||||
|
|
||||||
|
## JSON-RPC API
|
||||||
|
|
||||||
|
The stats functionality is also available through the JSON-RPC API:
|
||||||
|
|
||||||
|
### Method: `service_stats`
|
||||||
|
|
||||||
|
Get memory and CPU usage statistics for a service.
|
||||||
|
|
||||||
|
**Parameters:**
|
||||||
|
- `name` (string, required): The name of the service to get stats for
|
||||||
|
|
||||||
|
**Returns:**
|
||||||
|
- Object containing stats information:
|
||||||
|
- `name` (string): Service name
|
||||||
|
- `pid` (integer): Process ID of the service
|
||||||
|
- `memory_usage` (integer): Memory usage in bytes
|
||||||
|
- `cpu_usage` (number): CPU usage as a percentage (0-100)
|
||||||
|
- `children` (array): Stats for child processes
|
||||||
|
- Each child has:
|
||||||
|
- `pid` (integer): Process ID of the child process
|
||||||
|
- `memory_usage` (integer): Memory usage in bytes
|
||||||
|
- `cpu_usage` (number): CPU usage as a percentage (0-100)
|
||||||
|
|
||||||
|
**Example Request:**
|
||||||
|
```json
|
||||||
|
{
|
||||||
|
"jsonrpc": "2.0",
|
||||||
|
"id": 1,
|
||||||
|
"method": "service_stats",
|
||||||
|
"params": {
|
||||||
|
"name": "nginx"
|
||||||
|
}
|
||||||
|
}
|
||||||
|
```
|
||||||
|
|
||||||
|
**Example Response:**
|
||||||
|
```json
|
||||||
|
{
|
||||||
|
"jsonrpc": "2.0",
|
||||||
|
"id": 1,
|
||||||
|
"result": {
|
||||||
|
"name": "nginx",
|
||||||
|
"pid": 1234,
|
||||||
|
"memory_usage": 10485760,
|
||||||
|
"cpu_usage": 2.5,
|
||||||
|
"children": [
|
||||||
|
{
|
||||||
|
"pid": 1235,
|
||||||
|
"memory_usage": 5242880,
|
||||||
|
"cpu_usage": 1.2
|
||||||
|
},
|
||||||
|
{
|
||||||
|
"pid": 1236,
|
||||||
|
"memory_usage": 4194304,
|
||||||
|
"cpu_usage": 0.8
|
||||||
|
}
|
||||||
|
]
|
||||||
|
}
|
||||||
|
}
|
||||||
|
```
|
||||||
|
|
||||||
|
**Possible Errors:**
|
||||||
|
- `-32000`: Service not found
|
||||||
|
- `-32003`: Service is down
|
||||||
|
|
||||||
|
## Implementation Details
|
||||||
|
|
||||||
|
The stats functionality works by:
|
||||||
|
|
||||||
|
1. Reading process information from `/proc/<pid>/` directories on Linux systems
|
||||||
|
2. Calculating memory usage from `/proc/<pid>/status` (VmRSS field)
|
||||||
|
3. Calculating CPU usage by sampling `/proc/<pid>/stat` over a short interval
|
||||||
|
4. Identifying child processes by checking the PPid field in `/proc/<pid>/status`
|
||||||
|
|
||||||
|
On non-Linux systems, the functionality provides placeholder values as the `/proc` filesystem is specific to Linux.
|
||||||
|
|
||||||
|
## Notes
|
||||||
|
|
||||||
|
- Memory usage is reported in bytes
|
||||||
|
- CPU usage is reported as a percentage (0-100)
|
||||||
|
- The service must be running to get stats (otherwise an error is returned)
|
||||||
|
- Child processes are identified by their parent PID matching the service's PID
|
||||||
104
components/zinit/example/example.sh
Executable file
104
components/zinit/example/example.sh
Executable file
@@ -0,0 +1,104 @@
|
|||||||
|
#!/bin/bash
|
||||||
|
|
||||||
|
# Determine the zinit binary path
|
||||||
|
ZINIT_BIN="./target/release/zinit" # Assuming zinit is built in release mode in the current directory
|
||||||
|
|
||||||
|
# Determine the configuration directory based on OS
|
||||||
|
if [[ "$(uname)" == "Darwin" ]]; then
|
||||||
|
# macOS
|
||||||
|
ZINIT_CONFIG_DIR="$HOME/hero/cfg/zinit"
|
||||||
|
else
|
||||||
|
# Linux or other
|
||||||
|
ZINIT_CONFIG_DIR="/etc/zinit"
|
||||||
|
fi
|
||||||
|
|
||||||
|
SERVICE_NAME="test_service"
|
||||||
|
CPU_SERVICE_NAME="cpu_test_service"
|
||||||
|
SERVICE_FILE="$ZINIT_CONFIG_DIR/$SERVICE_NAME.yaml"
|
||||||
|
CPU_SERVICE_FILE="$ZINIT_CONFIG_DIR/$CPU_SERVICE_NAME.yaml"
|
||||||
|
|
||||||
|
echo "--- Zinit Example Script ---"
|
||||||
|
echo "Zinit binary path: $ZINIT_BIN"
|
||||||
|
echo "Zinit config directory: $ZINIT_CONFIG_DIR"
|
||||||
|
|
||||||
|
# Step 1: Ensure zinit config directory exists
|
||||||
|
echo "Ensuring zinit config directory exists..."
|
||||||
|
mkdir -p "$ZINIT_CONFIG_DIR"
|
||||||
|
if [ $? -ne 0 ]; then
|
||||||
|
echo "Error: Failed to create config directory $ZINIT_CONFIG_DIR. Exiting."
|
||||||
|
exit 1
|
||||||
|
fi
|
||||||
|
echo "Config directory $ZINIT_CONFIG_DIR is ready."
|
||||||
|
|
||||||
|
# Step 2: Check if zinit daemon is running, if not, start it in background
|
||||||
|
echo "Checking if zinit daemon is running..."
|
||||||
|
if "$ZINIT_BIN" list > /dev/null 2>&1; then
|
||||||
|
echo "Zinit daemon is already running."
|
||||||
|
else
|
||||||
|
echo "Zinit daemon not running. Starting it in background..."
|
||||||
|
# Start zinit init in a new process group to avoid it being killed by script exit
|
||||||
|
# and redirecting output to /dev/null
|
||||||
|
nohup "$ZINIT_BIN" init > /dev/null 2>&1 &
|
||||||
|
ZINIT_PID=$!
|
||||||
|
echo "Zinit daemon started with PID: $ZINIT_PID"
|
||||||
|
sleep 2 # Give zinit a moment to start up and create the socket
|
||||||
|
if ! "$ZINIT_BIN" list > /dev/null 2>&1; then
|
||||||
|
echo "Error: Zinit daemon failed to start. Exiting."
|
||||||
|
exit 1
|
||||||
|
fi
|
||||||
|
echo "Zinit daemon successfully started."
|
||||||
|
fi
|
||||||
|
|
||||||
|
# Step 3: Create sample zinit service files
|
||||||
|
echo "Creating sample service file: $SERVICE_FILE"
|
||||||
|
cat <<EOF > "$SERVICE_FILE"
|
||||||
|
name: $SERVICE_NAME
|
||||||
|
exec: /bin/bash -c "while true; do echo 'Hello from $SERVICE_NAME!'; sleep 5; done"
|
||||||
|
log: stdout
|
||||||
|
EOF
|
||||||
|
|
||||||
|
if [ $? -ne 0 ]; then
|
||||||
|
echo "Error: Failed to create service file $SERVICE_FILE. Exiting."
|
||||||
|
exit 1
|
||||||
|
fi
|
||||||
|
echo "Service file created."
|
||||||
|
|
||||||
|
# Create a CPU-intensive service with child processes
|
||||||
|
echo "Creating CPU-intensive service file: $CPU_SERVICE_FILE"
|
||||||
|
cat <<EOF > "$CPU_SERVICE_FILE"
|
||||||
|
name: $CPU_SERVICE_NAME
|
||||||
|
exec: /bin/bash -c "for i in {1..3}; do (yes > /dev/null &) ; done; while true; do sleep 10; done"
|
||||||
|
log: stdout
|
||||||
|
EOF
|
||||||
|
|
||||||
|
if [ $? -ne 0 ]; then
|
||||||
|
echo "Error: Failed to create CPU service file $CPU_SERVICE_FILE. Exiting."
|
||||||
|
exit 1
|
||||||
|
fi
|
||||||
|
echo "CPU service file created."
|
||||||
|
|
||||||
|
# Step 4: Tell zinit to monitor the new services
|
||||||
|
echo "Telling zinit to monitor the services..."
|
||||||
|
"$ZINIT_BIN" monitor "$SERVICE_NAME"
|
||||||
|
"$ZINIT_BIN" monitor "$CPU_SERVICE_NAME"
|
||||||
|
|
||||||
|
# Step 5: List services to verify the new service is recognized
|
||||||
|
echo "Listing zinit services to verify..."
|
||||||
|
"$ZINIT_BIN" list
|
||||||
|
|
||||||
|
# Step 6: Show stats for the CPU-intensive service
|
||||||
|
echo "Waiting for services to start and generate some stats..."
|
||||||
|
sleep 5
|
||||||
|
echo "Getting stats for $CPU_SERVICE_NAME..."
|
||||||
|
"$ZINIT_BIN" stats "$CPU_SERVICE_NAME"
|
||||||
|
|
||||||
|
# # Step 7: Clean up (optional, but good for examples)
|
||||||
|
# echo "Cleaning up: stopping and forgetting services..."
|
||||||
|
# "$ZINIT_BIN" stop "$SERVICE_NAME" > /dev/null 2>&1
|
||||||
|
# "$ZINIT_BIN" forget "$SERVICE_NAME" > /dev/null 2>&1
|
||||||
|
# "$ZINIT_BIN" stop "$CPU_SERVICE_NAME" > /dev/null 2>&1
|
||||||
|
# "$ZINIT_BIN" forget "$CPU_SERVICE_NAME" > /dev/null 2>&1
|
||||||
|
# rm -f "$SERVICE_FILE" "$CPU_SERVICE_FILE"
|
||||||
|
# echo "Cleanup complete."
|
||||||
|
|
||||||
|
echo "--- Script Finished ---"
|
||||||
153
components/zinit/install.sh
Executable file
153
components/zinit/install.sh
Executable file
@@ -0,0 +1,153 @@
|
|||||||
|
#!/bin/bash
|
||||||
|
set -e
|
||||||
|
|
||||||
|
# Colors for output
|
||||||
|
RED='\033[0;31m'
|
||||||
|
GREEN='\033[0;32m'
|
||||||
|
YELLOW='\033[0;33m'
|
||||||
|
NC='\033[0m' # No Color
|
||||||
|
|
||||||
|
echo -e "${GREEN}stop zinit...${NC}"
|
||||||
|
rm -f /tmp/stop.sh
|
||||||
|
curl -fsSL https://raw.githubusercontent.com/threefoldtech/zinit/refs/heads/master/stop.sh > /tmp/stop.sh
|
||||||
|
bash /tmp/stop.sh
|
||||||
|
|
||||||
|
|
||||||
|
# GitHub repository information
|
||||||
|
GITHUB_REPO="threefoldtech/zinit"
|
||||||
|
|
||||||
|
# Get the latest version from GitHub API
|
||||||
|
echo -e "${YELLOW}Fetching latest version information...${NC}"
|
||||||
|
if command -v curl &> /dev/null; then
|
||||||
|
VERSION=$(curl -s "https://api.github.com/repos/${GITHUB_REPO}/releases/latest" | grep -o '"tag_name": "[^"]*' | grep -o '[^"]*$')
|
||||||
|
elif command -v wget &> /dev/null; then
|
||||||
|
VERSION=$(wget -qO- "https://api.github.com/repos/${GITHUB_REPO}/releases/latest" | grep -o '"tag_name": "[^"]*' | grep -o '[^"]*$')
|
||||||
|
else
|
||||||
|
echo -e "${RED}Neither curl nor wget found. Please install one of them and try again.${NC}"
|
||||||
|
exit 1
|
||||||
|
fi
|
||||||
|
|
||||||
|
if [ -z "$VERSION" ]; then
|
||||||
|
echo -e "${RED}Failed to fetch the latest version. Please check your internet connection.${NC}"
|
||||||
|
exit 1
|
||||||
|
fi
|
||||||
|
|
||||||
|
echo -e "${GREEN}Latest version: ${VERSION}${NC}"
|
||||||
|
DOWNLOAD_URL="https://github.com/${GITHUB_REPO}/releases/download/${VERSION}"
|
||||||
|
MIN_SIZE_BYTES=2000000 # 2MB in bytes
|
||||||
|
|
||||||
|
echo -e "${GREEN}Installing zinit ${VERSION}...${NC}"
|
||||||
|
|
||||||
|
# Create temporary directory
|
||||||
|
TMP_DIR=$(mktemp -d)
|
||||||
|
trap 'rm -rf "$TMP_DIR"' EXIT
|
||||||
|
|
||||||
|
# Detect OS and architecture
|
||||||
|
OS=$(uname -s | tr '[:upper:]' '[:lower:]')
|
||||||
|
ARCH=$(uname -m)
|
||||||
|
|
||||||
|
# Map architecture names
|
||||||
|
if [ "$ARCH" = "x86_64" ]; then
|
||||||
|
ARCH_NAME="x86_64"
|
||||||
|
elif [ "$ARCH" = "aarch64" ] || [ "$ARCH" = "arm64" ]; then
|
||||||
|
ARCH_NAME="aarch64"
|
||||||
|
else
|
||||||
|
echo -e "${RED}Unsupported architecture: $ARCH${NC}"
|
||||||
|
exit 1
|
||||||
|
fi
|
||||||
|
|
||||||
|
# Determine binary name based on OS and architecture
|
||||||
|
if [ "$OS" = "linux" ]; then
|
||||||
|
if [ "$ARCH_NAME" = "x86_64" ]; then
|
||||||
|
BINARY_NAME="zinit-linux-x86_64"
|
||||||
|
else
|
||||||
|
echo -e "${RED}Unsupported Linux architecture: $ARCH${NC}"
|
||||||
|
exit 1
|
||||||
|
fi
|
||||||
|
elif [ "$OS" = "darwin" ]; then
|
||||||
|
if [ "$ARCH_NAME" = "x86_64" ]; then
|
||||||
|
BINARY_NAME="zinit-macos-x86_64"
|
||||||
|
elif [ "$ARCH_NAME" = "aarch64" ]; then
|
||||||
|
BINARY_NAME="zinit-macos-aarch64"
|
||||||
|
else
|
||||||
|
echo -e "${RED}Unsupported macOS architecture: $ARCH${NC}"
|
||||||
|
exit 1
|
||||||
|
fi
|
||||||
|
else
|
||||||
|
echo -e "${RED}Unsupported operating system: $OS${NC}"
|
||||||
|
exit 1
|
||||||
|
fi
|
||||||
|
|
||||||
|
# Download URL
|
||||||
|
DOWNLOAD_PATH="${DOWNLOAD_URL}/${BINARY_NAME}"
|
||||||
|
LOCAL_PATH="${TMP_DIR}/${BINARY_NAME}"
|
||||||
|
|
||||||
|
echo -e "${YELLOW}Detected: $OS on $ARCH_NAME${NC}"
|
||||||
|
echo -e "${YELLOW}Downloading from: $DOWNLOAD_PATH${NC}"
|
||||||
|
|
||||||
|
# Download the binary
|
||||||
|
if command -v curl &> /dev/null; then
|
||||||
|
curl -L -o "$LOCAL_PATH" "$DOWNLOAD_PATH"
|
||||||
|
elif command -v wget &> /dev/null; then
|
||||||
|
wget -O "$LOCAL_PATH" "$DOWNLOAD_PATH"
|
||||||
|
else
|
||||||
|
echo -e "${RED}Neither curl nor wget found. Please install one of them and try again.${NC}"
|
||||||
|
exit 1
|
||||||
|
fi
|
||||||
|
|
||||||
|
# Check file size
|
||||||
|
FILE_SIZE=$(stat -f%z "$LOCAL_PATH" 2>/dev/null || stat -c%s "$LOCAL_PATH" 2>/dev/null)
|
||||||
|
if [ "$FILE_SIZE" -lt "$MIN_SIZE_BYTES" ]; then
|
||||||
|
echo -e "${RED}Downloaded file is too small (${FILE_SIZE} bytes). Expected at least ${MIN_SIZE_BYTES} bytes.${NC}"
|
||||||
|
echo -e "${RED}This might indicate a failed or incomplete download.${NC}"
|
||||||
|
exit 1
|
||||||
|
fi
|
||||||
|
|
||||||
|
echo -e "${GREEN}Download successful. File size: $(echo "$FILE_SIZE / 1000000" | bc -l | xargs printf "%.2f") MB${NC}"
|
||||||
|
|
||||||
|
# Make the binary executable
|
||||||
|
chmod +x "$LOCAL_PATH"
|
||||||
|
|
||||||
|
# Determine installation directory
|
||||||
|
if [ "$OS" = "darwin" ]; then
|
||||||
|
# macOS - install to ~/hero/bin/
|
||||||
|
INSTALL_DIR="$HOME/hero/bin"
|
||||||
|
else
|
||||||
|
# Linux - install to /usr/local/bin/ if running as root, otherwise to ~/.local/bin/
|
||||||
|
if [ "$(id -u)" -eq 0 ]; then
|
||||||
|
INSTALL_DIR="/usr/local/bin"
|
||||||
|
else
|
||||||
|
INSTALL_DIR="$HOME/.local/bin"
|
||||||
|
# Ensure ~/.local/bin exists and is in PATH
|
||||||
|
mkdir -p "$INSTALL_DIR"
|
||||||
|
if [[ ":$PATH:" != *":$INSTALL_DIR:"* ]]; then
|
||||||
|
echo -e "${YELLOW}Adding $INSTALL_DIR to your PATH. You may need to restart your terminal.${NC}"
|
||||||
|
if [ -f "$HOME/.bashrc" ]; then
|
||||||
|
echo "export PATH=\"\$PATH:$INSTALL_DIR\"" >> "$HOME/.bashrc"
|
||||||
|
fi
|
||||||
|
if [ -f "$HOME/.zshrc" ]; then
|
||||||
|
echo "export PATH=\"\$PATH:$INSTALL_DIR\"" >> "$HOME/.zshrc"
|
||||||
|
fi
|
||||||
|
fi
|
||||||
|
fi
|
||||||
|
fi
|
||||||
|
|
||||||
|
# Create installation directory if it doesn't exist
|
||||||
|
mkdir -p "$INSTALL_DIR"
|
||||||
|
|
||||||
|
# Copy the binary to the installation directory
|
||||||
|
cp "$LOCAL_PATH" "$INSTALL_DIR/zinit"
|
||||||
|
echo -e "${GREEN}Installed zinit to $INSTALL_DIR/zinit${NC}"
|
||||||
|
|
||||||
|
# Test the installation
|
||||||
|
echo -e "${YELLOW}Testing installation...${NC}"
|
||||||
|
if "$INSTALL_DIR/zinit" --help &> /dev/null; then
|
||||||
|
echo -e "${GREEN}Installation successful! You can now use 'zinit' command.${NC}"
|
||||||
|
echo -e "${YELLOW}Example usage: zinit --help${NC}"
|
||||||
|
"$INSTALL_DIR/zinit" --help | head -n 5
|
||||||
|
else
|
||||||
|
echo -e "${RED}Installation test failed. Please check the error messages above.${NC}"
|
||||||
|
exit 1
|
||||||
|
fi
|
||||||
|
|
||||||
|
echo -e "${GREEN}zinit ${VERSION} has been successfully installed!${NC}"
|
||||||
50
components/zinit/install_run.sh
Executable file
50
components/zinit/install_run.sh
Executable file
@@ -0,0 +1,50 @@
|
|||||||
|
#!/bin/bash
|
||||||
|
set -e
|
||||||
|
|
||||||
|
# Colors for output
|
||||||
|
RED='\033[0;31m'
|
||||||
|
GREEN='\033[0;32m'
|
||||||
|
YELLOW='\033[0;33m'
|
||||||
|
NC='\033[0m' # No Color
|
||||||
|
|
||||||
|
# Function to check if zinit is running
|
||||||
|
is_zinit_running() {
|
||||||
|
if zinit list &>/dev/null; then
|
||||||
|
return 0 # Command successful, zinit is running
|
||||||
|
else
|
||||||
|
return 1 # Command failed, zinit is not running
|
||||||
|
fi
|
||||||
|
}
|
||||||
|
|
||||||
|
echo -e "${GREEN}Starting zinit installation and setup...${NC}"
|
||||||
|
# Download and execute install.sh
|
||||||
|
echo -e "${YELLOW}Downloading and executing install.sh...${NC}"
|
||||||
|
curl -fsSL https://raw.githubusercontent.com/threefoldtech/zinit/refs/heads/master/install.sh | bash
|
||||||
|
|
||||||
|
echo -e "${GREEN}install zinit...${NC}"
|
||||||
|
rm -f /tmp/install.sh
|
||||||
|
curl -fsSL https://raw.githubusercontent.com/threefoldtech/zinit/refs/heads/master/install.sh > /tmp/install.sh
|
||||||
|
bash /tmp/install.sh
|
||||||
|
|
||||||
|
|
||||||
|
# Launch zinit in the background
|
||||||
|
echo -e "${GREEN}Starting zinit in the background...${NC}"
|
||||||
|
zinit &
|
||||||
|
|
||||||
|
# Give it a moment to start
|
||||||
|
sleep 1
|
||||||
|
|
||||||
|
# Verify zinit is running
|
||||||
|
if is_zinit_running; then
|
||||||
|
echo -e "${GREEN}Zinit is now running in the background.${NC}"
|
||||||
|
echo -e "${YELLOW}You can manage services with:${NC}"
|
||||||
|
echo -e " ${YELLOW}$ZINIT_PATH list${NC} - List all services"
|
||||||
|
echo -e " ${YELLOW}$ZINIT_PATH status${NC} - Show status of all services"
|
||||||
|
echo -e " ${YELLOW}$ZINIT_PATH monitor${NC} - Monitor services in real-time"
|
||||||
|
echo -e " ${YELLOW}$ZINIT_PATH shutdown${NC} - Shutdown zinit when needed"
|
||||||
|
else
|
||||||
|
echo -e "${RED}Failed to start zinit. Please check for errors above.${NC}"
|
||||||
|
exit 1
|
||||||
|
fi
|
||||||
|
|
||||||
|
echo -e "${GREEN}Zinit installation and startup complete!${NC}"
|
||||||
873
components/zinit/openrpc.json
Normal file
873
components/zinit/openrpc.json
Normal file
@@ -0,0 +1,873 @@
|
|||||||
|
{
|
||||||
|
"openrpc": "1.2.6",
|
||||||
|
"info": {
|
||||||
|
"version": "1.0.0",
|
||||||
|
"title": "Zinit JSON-RPC API",
|
||||||
|
"description": "JSON-RPC 2.0 API for controlling and querying Zinit services",
|
||||||
|
"license": {
|
||||||
|
"name": "MIT"
|
||||||
|
}
|
||||||
|
},
|
||||||
|
"servers": [
|
||||||
|
{
|
||||||
|
"name": "Unix Socket",
|
||||||
|
"url": "unix:///tmp/zinit.sock"
|
||||||
|
}
|
||||||
|
],
|
||||||
|
"methods": [
|
||||||
|
{
|
||||||
|
"name": "rpc.discover",
|
||||||
|
"description": "Returns the OpenRPC specification for the API",
|
||||||
|
"params": [],
|
||||||
|
"result": {
|
||||||
|
"name": "OpenRPCSpec",
|
||||||
|
"description": "The OpenRPC specification",
|
||||||
|
"schema": {
|
||||||
|
"type": "object"
|
||||||
|
}
|
||||||
|
},
|
||||||
|
"examples": [
|
||||||
|
{
|
||||||
|
"name": "Get API specification",
|
||||||
|
"params": [],
|
||||||
|
"result": {
|
||||||
|
"name": "OpenRPCSpecResult",
|
||||||
|
"value": {
|
||||||
|
"openrpc": "1.2.6",
|
||||||
|
"info": {
|
||||||
|
"version": "1.0.0",
|
||||||
|
"title": "Zinit JSON-RPC API"
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
]
|
||||||
|
},
|
||||||
|
{
|
||||||
|
"name": "service_list",
|
||||||
|
"description": "Lists all services managed by Zinit",
|
||||||
|
"params": [],
|
||||||
|
"result": {
|
||||||
|
"name": "ServiceList",
|
||||||
|
"description": "A map of service names to their current states",
|
||||||
|
"schema": {
|
||||||
|
"type": "object",
|
||||||
|
"additionalProperties": {
|
||||||
|
"type": "string",
|
||||||
|
"description": "Service state (Running, Success, Error, etc.)"
|
||||||
|
}
|
||||||
|
}
|
||||||
|
},
|
||||||
|
"examples": [
|
||||||
|
{
|
||||||
|
"name": "List all services",
|
||||||
|
"params": [],
|
||||||
|
"result": {
|
||||||
|
"name": "ServiceListResult",
|
||||||
|
"value": {
|
||||||
|
"service1": "Running",
|
||||||
|
"service2": "Success",
|
||||||
|
"service3": "Error"
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
]
|
||||||
|
},
|
||||||
|
{
|
||||||
|
"name": "service_status",
|
||||||
|
"description": "Shows detailed status information for a specific service",
|
||||||
|
"params": [
|
||||||
|
{
|
||||||
|
"name": "name",
|
||||||
|
"description": "The name of the service",
|
||||||
|
"required": true,
|
||||||
|
"schema": {
|
||||||
|
"type": "string"
|
||||||
|
}
|
||||||
|
}
|
||||||
|
],
|
||||||
|
"result": {
|
||||||
|
"name": "ServiceStatus",
|
||||||
|
"description": "Detailed status information for the service",
|
||||||
|
"schema": {
|
||||||
|
"type": "object",
|
||||||
|
"properties": {
|
||||||
|
"name": {
|
||||||
|
"type": "string",
|
||||||
|
"description": "Service name"
|
||||||
|
},
|
||||||
|
"pid": {
|
||||||
|
"type": "integer",
|
||||||
|
"description": "Process ID of the running service (if running)"
|
||||||
|
},
|
||||||
|
"state": {
|
||||||
|
"type": "string",
|
||||||
|
"description": "Current state of the service (Running, Success, Error, etc.)"
|
||||||
|
},
|
||||||
|
"target": {
|
||||||
|
"type": "string",
|
||||||
|
"description": "Target state of the service (Up, Down)"
|
||||||
|
},
|
||||||
|
"after": {
|
||||||
|
"type": "object",
|
||||||
|
"description": "Dependencies of the service and their states",
|
||||||
|
"additionalProperties": {
|
||||||
|
"type": "string",
|
||||||
|
"description": "State of the dependency"
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
},
|
||||||
|
"examples": [
|
||||||
|
{
|
||||||
|
"name": "Get status of redis service",
|
||||||
|
"params": [
|
||||||
|
{
|
||||||
|
"name": "name",
|
||||||
|
"value": "redis"
|
||||||
|
}
|
||||||
|
],
|
||||||
|
"result": {
|
||||||
|
"name": "ServiceStatusResult",
|
||||||
|
"value": {
|
||||||
|
"name": "redis",
|
||||||
|
"pid": 1234,
|
||||||
|
"state": "Running",
|
||||||
|
"target": "Up",
|
||||||
|
"after": {
|
||||||
|
"dependency1": "Success",
|
||||||
|
"dependency2": "Running"
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
],
|
||||||
|
"errors": [
|
||||||
|
{
|
||||||
|
"code": -32000,
|
||||||
|
"message": "Service not found",
|
||||||
|
"data": "service name \"unknown\" unknown"
|
||||||
|
}
|
||||||
|
]
|
||||||
|
},
|
||||||
|
{
|
||||||
|
"name": "service_start",
|
||||||
|
"description": "Starts a service",
|
||||||
|
"params": [
|
||||||
|
{
|
||||||
|
"name": "name",
|
||||||
|
"description": "The name of the service to start",
|
||||||
|
"required": true,
|
||||||
|
"schema": {
|
||||||
|
"type": "string"
|
||||||
|
}
|
||||||
|
}
|
||||||
|
],
|
||||||
|
"result": {
|
||||||
|
"name": "StartResult",
|
||||||
|
"description": "Result of the start operation",
|
||||||
|
"schema": {
|
||||||
|
"type": "null"
|
||||||
|
}
|
||||||
|
},
|
||||||
|
"examples": [
|
||||||
|
{
|
||||||
|
"name": "Start redis service",
|
||||||
|
"params": [
|
||||||
|
{
|
||||||
|
"name": "name",
|
||||||
|
"value": "redis"
|
||||||
|
}
|
||||||
|
],
|
||||||
|
"result": {
|
||||||
|
"name": "StartResult",
|
||||||
|
"value": null
|
||||||
|
}
|
||||||
|
}
|
||||||
|
],
|
||||||
|
"errors": [
|
||||||
|
{
|
||||||
|
"code": -32000,
|
||||||
|
"message": "Service not found",
|
||||||
|
"data": "service name \"unknown\" unknown"
|
||||||
|
}
|
||||||
|
]
|
||||||
|
},
|
||||||
|
{
|
||||||
|
"name": "service_stop",
|
||||||
|
"description": "Stops a service",
|
||||||
|
"params": [
|
||||||
|
{
|
||||||
|
"name": "name",
|
||||||
|
"description": "The name of the service to stop",
|
||||||
|
"required": true,
|
||||||
|
"schema": {
|
||||||
|
"type": "string"
|
||||||
|
}
|
||||||
|
}
|
||||||
|
],
|
||||||
|
"result": {
|
||||||
|
"name": "StopResult",
|
||||||
|
"description": "Result of the stop operation",
|
||||||
|
"schema": {
|
||||||
|
"type": "null"
|
||||||
|
}
|
||||||
|
},
|
||||||
|
"examples": [
|
||||||
|
{
|
||||||
|
"name": "Stop redis service",
|
||||||
|
"params": [
|
||||||
|
{
|
||||||
|
"name": "name",
|
||||||
|
"value": "redis"
|
||||||
|
}
|
||||||
|
],
|
||||||
|
"result": {
|
||||||
|
"name": "StopResult",
|
||||||
|
"value": null
|
||||||
|
}
|
||||||
|
}
|
||||||
|
],
|
||||||
|
"errors": [
|
||||||
|
{
|
||||||
|
"code": -32000,
|
||||||
|
"message": "Service not found",
|
||||||
|
"data": "service name \"unknown\" unknown"
|
||||||
|
},
|
||||||
|
{
|
||||||
|
"code": -32003,
|
||||||
|
"message": "Service is down",
|
||||||
|
"data": "service \"redis\" is down"
|
||||||
|
}
|
||||||
|
]
|
||||||
|
},
|
||||||
|
{
|
||||||
|
"name": "service_monitor",
|
||||||
|
"description": "Starts monitoring a service. The service configuration is loaded from the config directory.",
|
||||||
|
"params": [
|
||||||
|
{
|
||||||
|
"name": "name",
|
||||||
|
"description": "The name of the service to monitor",
|
||||||
|
"required": true,
|
||||||
|
"schema": {
|
||||||
|
"type": "string"
|
||||||
|
}
|
||||||
|
}
|
||||||
|
],
|
||||||
|
"result": {
|
||||||
|
"name": "MonitorResult",
|
||||||
|
"description": "Result of the monitor operation",
|
||||||
|
"schema": {
|
||||||
|
"type": "null"
|
||||||
|
}
|
||||||
|
},
|
||||||
|
"examples": [
|
||||||
|
{
|
||||||
|
"name": "Monitor redis service",
|
||||||
|
"params": [
|
||||||
|
{
|
||||||
|
"name": "name",
|
||||||
|
"value": "redis"
|
||||||
|
}
|
||||||
|
],
|
||||||
|
"result": {
|
||||||
|
"name": "MonitorResult",
|
||||||
|
"value": null
|
||||||
|
}
|
||||||
|
}
|
||||||
|
],
|
||||||
|
"errors": [
|
||||||
|
{
|
||||||
|
"code": -32001,
|
||||||
|
"message": "Service already monitored",
|
||||||
|
"data": "service \"redis\" already monitored"
|
||||||
|
},
|
||||||
|
{
|
||||||
|
"code": -32005,
|
||||||
|
"message": "Config error",
|
||||||
|
"data": "failed to load service configuration"
|
||||||
|
}
|
||||||
|
]
|
||||||
|
},
|
||||||
|
{
|
||||||
|
"name": "service_forget",
|
||||||
|
"description": "Stops monitoring a service. You can only forget a stopped service.",
|
||||||
|
"params": [
|
||||||
|
{
|
||||||
|
"name": "name",
|
||||||
|
"description": "The name of the service to forget",
|
||||||
|
"required": true,
|
||||||
|
"schema": {
|
||||||
|
"type": "string"
|
||||||
|
}
|
||||||
|
}
|
||||||
|
],
|
||||||
|
"result": {
|
||||||
|
"name": "ForgetResult",
|
||||||
|
"description": "Result of the forget operation",
|
||||||
|
"schema": {
|
||||||
|
"type": "null"
|
||||||
|
}
|
||||||
|
},
|
||||||
|
"examples": [
|
||||||
|
{
|
||||||
|
"name": "Forget redis service",
|
||||||
|
"params": [
|
||||||
|
{
|
||||||
|
"name": "name",
|
||||||
|
"value": "redis"
|
||||||
|
}
|
||||||
|
],
|
||||||
|
"result": {
|
||||||
|
"name": "ForgetResult",
|
||||||
|
"value": null
|
||||||
|
}
|
||||||
|
}
|
||||||
|
],
|
||||||
|
"errors": [
|
||||||
|
{
|
||||||
|
"code": -32000,
|
||||||
|
"message": "Service not found",
|
||||||
|
"data": "service name \"unknown\" unknown"
|
||||||
|
},
|
||||||
|
{
|
||||||
|
"code": -32002,
|
||||||
|
"message": "Service is up",
|
||||||
|
"data": "service \"redis\" is up"
|
||||||
|
}
|
||||||
|
]
|
||||||
|
},
|
||||||
|
{
|
||||||
|
"name": "service_kill",
|
||||||
|
"description": "Sends a signal to a running service",
|
||||||
|
"params": [
|
||||||
|
{
|
||||||
|
"name": "name",
|
||||||
|
"description": "The name of the service to send the signal to",
|
||||||
|
"required": true,
|
||||||
|
"schema": {
|
||||||
|
"type": "string"
|
||||||
|
}
|
||||||
|
},
|
||||||
|
{
|
||||||
|
"name": "signal",
|
||||||
|
"description": "The signal to send (e.g., SIGTERM, SIGKILL)",
|
||||||
|
"required": true,
|
||||||
|
"schema": {
|
||||||
|
"type": "string"
|
||||||
|
}
|
||||||
|
}
|
||||||
|
],
|
||||||
|
"result": {
|
||||||
|
"name": "KillResult",
|
||||||
|
"description": "Result of the kill operation",
|
||||||
|
"schema": {
|
||||||
|
"type": "null"
|
||||||
|
}
|
||||||
|
},
|
||||||
|
"examples": [
|
||||||
|
{
|
||||||
|
"name": "Send SIGTERM to redis service",
|
||||||
|
"params": [
|
||||||
|
{
|
||||||
|
"name": "name",
|
||||||
|
"value": "redis"
|
||||||
|
},
|
||||||
|
{
|
||||||
|
"name": "signal",
|
||||||
|
"value": "SIGTERM"
|
||||||
|
}
|
||||||
|
],
|
||||||
|
"result": {
|
||||||
|
"name": "KillResult",
|
||||||
|
"value": null
|
||||||
|
}
|
||||||
|
}
|
||||||
|
],
|
||||||
|
"errors": [
|
||||||
|
{
|
||||||
|
"code": -32000,
|
||||||
|
"message": "Service not found",
|
||||||
|
"data": "service name \"unknown\" unknown"
|
||||||
|
},
|
||||||
|
{
|
||||||
|
"code": -32003,
|
||||||
|
"message": "Service is down",
|
||||||
|
"data": "service \"redis\" is down"
|
||||||
|
},
|
||||||
|
{
|
||||||
|
"code": -32004,
|
||||||
|
"message": "Invalid signal",
|
||||||
|
"data": "invalid signal: INVALID"
|
||||||
|
}
|
||||||
|
]
|
||||||
|
},
|
||||||
|
{
|
||||||
|
"name": "system_shutdown",
|
||||||
|
"description": "Stops all services and powers off the system",
|
||||||
|
"params": [],
|
||||||
|
"result": {
|
||||||
|
"name": "ShutdownResult",
|
||||||
|
"description": "Result of the shutdown operation",
|
||||||
|
"schema": {
|
||||||
|
"type": "null"
|
||||||
|
}
|
||||||
|
},
|
||||||
|
"examples": [
|
||||||
|
{
|
||||||
|
"name": "Shutdown the system",
|
||||||
|
"params": [],
|
||||||
|
"result": {
|
||||||
|
"name": "ShutdownResult",
|
||||||
|
"value": null
|
||||||
|
}
|
||||||
|
}
|
||||||
|
],
|
||||||
|
"errors": [
|
||||||
|
{
|
||||||
|
"code": -32006,
|
||||||
|
"message": "Shutting down",
|
||||||
|
"data": "system is already shutting down"
|
||||||
|
}
|
||||||
|
]
|
||||||
|
},
|
||||||
|
{
|
||||||
|
"name": "system_reboot",
|
||||||
|
"description": "Stops all services and reboots the system",
|
||||||
|
"params": [],
|
||||||
|
"result": {
|
||||||
|
"name": "RebootResult",
|
||||||
|
"description": "Result of the reboot operation",
|
||||||
|
"schema": {
|
||||||
|
"type": "null"
|
||||||
|
}
|
||||||
|
},
|
||||||
|
"examples": [
|
||||||
|
{
|
||||||
|
"name": "Reboot the system",
|
||||||
|
"params": [],
|
||||||
|
"result": {
|
||||||
|
"name": "RebootResult",
|
||||||
|
"value": null
|
||||||
|
}
|
||||||
|
}
|
||||||
|
],
|
||||||
|
"errors": [
|
||||||
|
{
|
||||||
|
"code": -32006,
|
||||||
|
"message": "Shutting down",
|
||||||
|
"data": "system is already shutting down"
|
||||||
|
}
|
||||||
|
]
|
||||||
|
},
|
||||||
|
{
|
||||||
|
"name": "service_create",
|
||||||
|
"description": "Creates a new service configuration file",
|
||||||
|
"params": [
|
||||||
|
{
|
||||||
|
"name": "name",
|
||||||
|
"description": "The name of the service to create",
|
||||||
|
"required": true,
|
||||||
|
"schema": {
|
||||||
|
"type": "string"
|
||||||
|
}
|
||||||
|
},
|
||||||
|
{
|
||||||
|
"name": "content",
|
||||||
|
"description": "The service configuration content",
|
||||||
|
"required": true,
|
||||||
|
"schema": {
|
||||||
|
"type": "object",
|
||||||
|
"properties": {
|
||||||
|
"exec": {
|
||||||
|
"type": "string",
|
||||||
|
"description": "Command to run"
|
||||||
|
},
|
||||||
|
"oneshot": {
|
||||||
|
"type": "boolean",
|
||||||
|
"description": "Whether the service should be restarted"
|
||||||
|
},
|
||||||
|
"after": {
|
||||||
|
"type": "array",
|
||||||
|
"items": {
|
||||||
|
"type": "string"
|
||||||
|
},
|
||||||
|
"description": "Services that must be running before this one starts"
|
||||||
|
},
|
||||||
|
"log": {
|
||||||
|
"type": "string",
|
||||||
|
"enum": ["null", "ring", "stdout"],
|
||||||
|
"description": "How to handle service output"
|
||||||
|
},
|
||||||
|
"env": {
|
||||||
|
"type": "object",
|
||||||
|
"additionalProperties": {
|
||||||
|
"type": "string"
|
||||||
|
},
|
||||||
|
"description": "Environment variables for the service"
|
||||||
|
},
|
||||||
|
"shutdown_timeout": {
|
||||||
|
"type": "integer",
|
||||||
|
"description": "Maximum time to wait for service to stop during shutdown"
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
],
|
||||||
|
"result": {
|
||||||
|
"name": "CreateServiceResult",
|
||||||
|
"description": "Result of the create operation",
|
||||||
|
"schema": {
|
||||||
|
"type": "string"
|
||||||
|
}
|
||||||
|
},
|
||||||
|
"errors": [
|
||||||
|
{
|
||||||
|
"code": -32007,
|
||||||
|
"message": "Service already exists",
|
||||||
|
"data": "Service 'name' already exists"
|
||||||
|
},
|
||||||
|
{
|
||||||
|
"code": -32008,
|
||||||
|
"message": "Service file error",
|
||||||
|
"data": "Failed to create service file"
|
||||||
|
}
|
||||||
|
]
|
||||||
|
},
|
||||||
|
{
|
||||||
|
"name": "service_delete",
|
||||||
|
"description": "Deletes a service configuration file",
|
||||||
|
"params": [
|
||||||
|
{
|
||||||
|
"name": "name",
|
||||||
|
"description": "The name of the service to delete",
|
||||||
|
"required": true,
|
||||||
|
"schema": {
|
||||||
|
"type": "string"
|
||||||
|
}
|
||||||
|
}
|
||||||
|
],
|
||||||
|
"result": {
|
||||||
|
"name": "DeleteServiceResult",
|
||||||
|
"description": "Result of the delete operation",
|
||||||
|
"schema": {
|
||||||
|
"type": "string"
|
||||||
|
}
|
||||||
|
},
|
||||||
|
"errors": [
|
||||||
|
{
|
||||||
|
"code": -32000,
|
||||||
|
"message": "Service not found",
|
||||||
|
"data": "Service 'name' not found"
|
||||||
|
},
|
||||||
|
{
|
||||||
|
"code": -32008,
|
||||||
|
"message": "Service file error",
|
||||||
|
"data": "Failed to delete service file"
|
||||||
|
}
|
||||||
|
]
|
||||||
|
},
|
||||||
|
{
|
||||||
|
"name": "service_get",
|
||||||
|
"description": "Gets a service configuration file",
|
||||||
|
"params": [
|
||||||
|
{
|
||||||
|
"name": "name",
|
||||||
|
"description": "The name of the service to get",
|
||||||
|
"required": true,
|
||||||
|
"schema": {
|
||||||
|
"type": "string"
|
||||||
|
}
|
||||||
|
}
|
||||||
|
],
|
||||||
|
"result": {
|
||||||
|
"name": "GetServiceResult",
|
||||||
|
"description": "The service configuration",
|
||||||
|
"schema": {
|
||||||
|
"type": "object"
|
||||||
|
}
|
||||||
|
},
|
||||||
|
"errors": [
|
||||||
|
{
|
||||||
|
"code": -32000,
|
||||||
|
"message": "Service not found",
|
||||||
|
"data": "Service 'name' not found"
|
||||||
|
},
|
||||||
|
{
|
||||||
|
"code": -32008,
|
||||||
|
"message": "Service file error",
|
||||||
|
"data": "Failed to read service file"
|
||||||
|
}
|
||||||
|
]
|
||||||
|
},
|
||||||
|
{
|
||||||
|
"name": "service_stats",
|
||||||
|
"description": "Get memory and CPU usage statistics for a service",
|
||||||
|
"params": [
|
||||||
|
{
|
||||||
|
"name": "name",
|
||||||
|
"description": "The name of the service to get stats for",
|
||||||
|
"required": true,
|
||||||
|
"schema": {
|
||||||
|
"type": "string"
|
||||||
|
}
|
||||||
|
}
|
||||||
|
],
|
||||||
|
"result": {
|
||||||
|
"name": "ServiceStats",
|
||||||
|
"description": "Memory and CPU usage statistics for the service",
|
||||||
|
"schema": {
|
||||||
|
"type": "object",
|
||||||
|
"properties": {
|
||||||
|
"name": {
|
||||||
|
"type": "string",
|
||||||
|
"description": "Service name"
|
||||||
|
},
|
||||||
|
"pid": {
|
||||||
|
"type": "integer",
|
||||||
|
"description": "Process ID of the service"
|
||||||
|
},
|
||||||
|
"memory_usage": {
|
||||||
|
"type": "integer",
|
||||||
|
"description": "Memory usage in bytes"
|
||||||
|
},
|
||||||
|
"cpu_usage": {
|
||||||
|
"type": "number",
|
||||||
|
"description": "CPU usage as a percentage (0-100)"
|
||||||
|
},
|
||||||
|
"children": {
|
||||||
|
"type": "array",
|
||||||
|
"description": "Stats for child processes",
|
||||||
|
"items": {
|
||||||
|
"type": "object",
|
||||||
|
"properties": {
|
||||||
|
"pid": {
|
||||||
|
"type": "integer",
|
||||||
|
"description": "Process ID of the child process"
|
||||||
|
},
|
||||||
|
"memory_usage": {
|
||||||
|
"type": "integer",
|
||||||
|
"description": "Memory usage in bytes"
|
||||||
|
},
|
||||||
|
"cpu_usage": {
|
||||||
|
"type": "number",
|
||||||
|
"description": "CPU usage as a percentage (0-100)"
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
},
|
||||||
|
"examples": [
|
||||||
|
{
|
||||||
|
"name": "Get stats for redis service",
|
||||||
|
"params": [
|
||||||
|
{
|
||||||
|
"name": "name",
|
||||||
|
"value": "redis"
|
||||||
|
}
|
||||||
|
],
|
||||||
|
"result": {
|
||||||
|
"name": "ServiceStatsResult",
|
||||||
|
"value": {
|
||||||
|
"name": "redis",
|
||||||
|
"pid": 1234,
|
||||||
|
"memory_usage": 10485760,
|
||||||
|
"cpu_usage": 2.5,
|
||||||
|
"children": [
|
||||||
|
{
|
||||||
|
"pid": 1235,
|
||||||
|
"memory_usage": 5242880,
|
||||||
|
"cpu_usage": 1.2
|
||||||
|
}
|
||||||
|
]
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
],
|
||||||
|
"errors": [
|
||||||
|
{
|
||||||
|
"code": -32000,
|
||||||
|
"message": "Service not found",
|
||||||
|
"data": "service name \"unknown\" unknown"
|
||||||
|
},
|
||||||
|
{
|
||||||
|
"code": -32003,
|
||||||
|
"message": "Service is down",
|
||||||
|
"data": "service \"redis\" is down"
|
||||||
|
}
|
||||||
|
]
|
||||||
|
},
|
||||||
|
{
|
||||||
|
"name": "system_start_http_server",
|
||||||
|
"description": "Start an HTTP/RPC server at the specified address",
|
||||||
|
"params": [
|
||||||
|
{
|
||||||
|
"name": "address",
|
||||||
|
"description": "The network address to bind the server to (e.g., '127.0.0.1:8080')",
|
||||||
|
"required": true,
|
||||||
|
"schema": {
|
||||||
|
"type": "string"
|
||||||
|
}
|
||||||
|
}
|
||||||
|
],
|
||||||
|
"result": {
|
||||||
|
"name": "StartHttpServerResult",
|
||||||
|
"description": "Result of the start HTTP server operation",
|
||||||
|
"schema": {
|
||||||
|
"type": "string"
|
||||||
|
}
|
||||||
|
},
|
||||||
|
"examples": [
|
||||||
|
{
|
||||||
|
"name": "Start HTTP server on localhost:8080",
|
||||||
|
"params": [
|
||||||
|
{
|
||||||
|
"name": "address",
|
||||||
|
"value": "127.0.0.1:8080"
|
||||||
|
}
|
||||||
|
],
|
||||||
|
"result": {
|
||||||
|
"name": "StartHttpServerResult",
|
||||||
|
"value": "HTTP server started at 127.0.0.1:8080"
|
||||||
|
}
|
||||||
|
}
|
||||||
|
],
|
||||||
|
"errors": [
|
||||||
|
{
|
||||||
|
"code": -32602,
|
||||||
|
"message": "Invalid address",
|
||||||
|
"data": "Invalid network address format"
|
||||||
|
}
|
||||||
|
]
|
||||||
|
},
|
||||||
|
{
|
||||||
|
"name": "system_stop_http_server",
|
||||||
|
"description": "Stop the HTTP/RPC server if running",
|
||||||
|
"params": [],
|
||||||
|
"result": {
|
||||||
|
"name": "StopHttpServerResult",
|
||||||
|
"description": "Result of the stop HTTP server operation",
|
||||||
|
"schema": {
|
||||||
|
"type": "null"
|
||||||
|
}
|
||||||
|
},
|
||||||
|
"examples": [
|
||||||
|
{
|
||||||
|
"name": "Stop the HTTP server",
|
||||||
|
"params": [],
|
||||||
|
"result": {
|
||||||
|
"name": "StopHttpServerResult",
|
||||||
|
"value": null
|
||||||
|
}
|
||||||
|
}
|
||||||
|
],
|
||||||
|
"errors": [
|
||||||
|
{
|
||||||
|
"code": -32602,
|
||||||
|
"message": "Server not running",
|
||||||
|
"data": "No HTTP server is currently running"
|
||||||
|
}
|
||||||
|
]
|
||||||
|
},
|
||||||
|
{
|
||||||
|
"name": "stream_currentLogs",
|
||||||
|
"description": "Get current logs from zinit and monitored services",
|
||||||
|
"params": [
|
||||||
|
{
|
||||||
|
"name": "name",
|
||||||
|
"description": "Optional service name filter. If provided, only logs from this service will be returned",
|
||||||
|
"required": false,
|
||||||
|
"schema": {
|
||||||
|
"type": "string"
|
||||||
|
}
|
||||||
|
}
|
||||||
|
],
|
||||||
|
"result": {
|
||||||
|
"name": "LogsResult",
|
||||||
|
"description": "Array of log strings",
|
||||||
|
"schema": {
|
||||||
|
"type": "array",
|
||||||
|
"items": {
|
||||||
|
"type": "string"
|
||||||
|
}
|
||||||
|
}
|
||||||
|
},
|
||||||
|
"examples": [
|
||||||
|
{
|
||||||
|
"name": "Get all logs",
|
||||||
|
"params": [],
|
||||||
|
"result": {
|
||||||
|
"name": "LogsResult",
|
||||||
|
"value": [
|
||||||
|
"2023-01-01T12:00:00 redis: Starting service",
|
||||||
|
"2023-01-01T12:00:01 nginx: Starting service"
|
||||||
|
]
|
||||||
|
}
|
||||||
|
},
|
||||||
|
{
|
||||||
|
"name": "Get logs for a specific service",
|
||||||
|
"params": [
|
||||||
|
{
|
||||||
|
"name": "name",
|
||||||
|
"value": "redis"
|
||||||
|
}
|
||||||
|
],
|
||||||
|
"result": {
|
||||||
|
"name": "LogsResult",
|
||||||
|
"value": [
|
||||||
|
"2023-01-01T12:00:00 redis: Starting service",
|
||||||
|
"2023-01-01T12:00:02 redis: Service started"
|
||||||
|
]
|
||||||
|
}
|
||||||
|
}
|
||||||
|
]
|
||||||
|
},
|
||||||
|
{
|
||||||
|
"name": "stream_subscribeLogs",
|
||||||
|
"description": "Subscribe to log messages generated by zinit and monitored services",
|
||||||
|
"params": [
|
||||||
|
{
|
||||||
|
"name": "name",
|
||||||
|
"description": "Optional service name filter. If provided, only logs from this service will be returned",
|
||||||
|
"required": false,
|
||||||
|
"schema": {
|
||||||
|
"type": "string"
|
||||||
|
}
|
||||||
|
}
|
||||||
|
],
|
||||||
|
"result": {
|
||||||
|
"name": "LogSubscription",
|
||||||
|
"description": "A subscription to log messages",
|
||||||
|
"schema": {
|
||||||
|
"type": "string"
|
||||||
|
}
|
||||||
|
},
|
||||||
|
"examples": [
|
||||||
|
{
|
||||||
|
"name": "Subscribe to all logs",
|
||||||
|
"params": [],
|
||||||
|
"result": {
|
||||||
|
"name": "LogSubscription",
|
||||||
|
"value": "2023-01-01T12:00:00 redis: Service started"
|
||||||
|
}
|
||||||
|
},
|
||||||
|
{
|
||||||
|
"name": "Subscribe to filtered logs",
|
||||||
|
"params": [
|
||||||
|
{
|
||||||
|
"name": "name",
|
||||||
|
"value": "redis"
|
||||||
|
}
|
||||||
|
],
|
||||||
|
"result": {
|
||||||
|
"name": "LogSubscription",
|
||||||
|
"value": "2023-01-01T12:00:00 redis: Service started"
|
||||||
|
}
|
||||||
|
}
|
||||||
|
]
|
||||||
|
}
|
||||||
|
]
|
||||||
|
}
|
||||||
57
components/zinit/osx_build.sh
Executable file
57
components/zinit/osx_build.sh
Executable file
@@ -0,0 +1,57 @@
|
|||||||
|
#!/bin/bash
|
||||||
|
|
||||||
|
# Jump to the directory of the script
|
||||||
|
cd "$(dirname "$0")"
|
||||||
|
|
||||||
|
./stop.sh
|
||||||
|
|
||||||
|
# Build the project
|
||||||
|
echo "Building zinit..."
|
||||||
|
cargo build --release
|
||||||
|
|
||||||
|
if [ $? -ne 0 ]; then
|
||||||
|
echo "Build failed!"
|
||||||
|
exit 1
|
||||||
|
fi
|
||||||
|
|
||||||
|
# Copy the binary
|
||||||
|
echo "Copying zinit binary to ~/hero/bin..."
|
||||||
|
cp ./target/release/zinit ~/hero/bin
|
||||||
|
|
||||||
|
if [ $? -ne 0 ]; then
|
||||||
|
echo "Failed to copy binary!"
|
||||||
|
exit 1
|
||||||
|
fi
|
||||||
|
|
||||||
|
# Ensure config directory exists
|
||||||
|
echo "Ensuring config directory exists..."
|
||||||
|
mkdir -p ~/hero/cfg/zinit
|
||||||
|
|
||||||
|
# Start zinit in init mode (daemon) in background
|
||||||
|
echo "Starting zinit daemon in background..."
|
||||||
|
~/hero/bin/zinit init -c ~/hero/cfg/zinit &
|
||||||
|
ZINIT_PID=$!
|
||||||
|
|
||||||
|
# Wait a moment for zinit to start and create the socket
|
||||||
|
sleep 5
|
||||||
|
|
||||||
|
# Check if zinit is running
|
||||||
|
if kill -0 $ZINIT_PID 2>/dev/null; then
|
||||||
|
echo "Zinit daemon started successfully with PID: $ZINIT_PID"
|
||||||
|
|
||||||
|
# Test with zinit list
|
||||||
|
echo "Testing zinit list command..."
|
||||||
|
~/hero/bin/zinit list
|
||||||
|
|
||||||
|
if [ $? -eq 0 ]; then
|
||||||
|
echo "Zinit is working correctly!"
|
||||||
|
else
|
||||||
|
echo "Warning: zinit list command failed, but zinit daemon is running"
|
||||||
|
echo "This might be normal if no services are configured yet."
|
||||||
|
fi
|
||||||
|
else
|
||||||
|
echo "Failed to start zinit daemon!"
|
||||||
|
exit 1
|
||||||
|
fi
|
||||||
|
|
||||||
|
echo "Build and setup completed successfully!"
|
||||||
101
components/zinit/release_zinit.sh
Executable file
101
components/zinit/release_zinit.sh
Executable file
@@ -0,0 +1,101 @@
|
|||||||
|
#!/bin/bash
|
||||||
|
|
||||||
|
# Navigate to the zinit project directory
|
||||||
|
cd /Users/despiegk/code/github/threefoldtech/zinit
|
||||||
|
|
||||||
|
# Check if we're in the right directory
|
||||||
|
if [ ! -f "Cargo.toml" ]; then
|
||||||
|
echo "Error: Not in zinit project directory"
|
||||||
|
exit 1
|
||||||
|
fi
|
||||||
|
|
||||||
|
# Function to get the latest tag from Git
|
||||||
|
get_latest_tag() {
|
||||||
|
# Fetch all tags from remote
|
||||||
|
git fetch --tags origin 2>/dev/null
|
||||||
|
|
||||||
|
# Get the latest tag using version sorting
|
||||||
|
local latest_tag=$(git tag -l "v*" | sort -V | tail -n 1)
|
||||||
|
|
||||||
|
if [ -z "$latest_tag" ]; then
|
||||||
|
echo "v0.0.0"
|
||||||
|
else
|
||||||
|
echo "$latest_tag"
|
||||||
|
fi
|
||||||
|
}
|
||||||
|
|
||||||
|
# Function to increment version
|
||||||
|
increment_version() {
|
||||||
|
local version=$1
|
||||||
|
# Remove 'v' prefix if present
|
||||||
|
version=${version#v}
|
||||||
|
|
||||||
|
# Split version into parts
|
||||||
|
IFS='.' read -ra PARTS <<< "$version"
|
||||||
|
major=${PARTS[0]:-0}
|
||||||
|
minor=${PARTS[1]:-0}
|
||||||
|
patch=${PARTS[2]:-0}
|
||||||
|
|
||||||
|
# Increment patch (maintenance) version
|
||||||
|
patch=$((patch + 1))
|
||||||
|
|
||||||
|
echo "v${major}.${minor}.${patch}"
|
||||||
|
}
|
||||||
|
|
||||||
|
echo "🔍 Checking latest tag..."
|
||||||
|
latest_tag=$(get_latest_tag)
|
||||||
|
echo "Latest tag: $latest_tag"
|
||||||
|
|
||||||
|
new_version=$(increment_version "$latest_tag")
|
||||||
|
echo "New version: $new_version"
|
||||||
|
|
||||||
|
# Confirm with user
|
||||||
|
read -p "Create release $new_version? (y/N): " -n 1 -r
|
||||||
|
echo
|
||||||
|
if [[ ! $REPLY =~ ^[Yy]$ ]]; then
|
||||||
|
echo "Release cancelled"
|
||||||
|
exit 0
|
||||||
|
fi
|
||||||
|
|
||||||
|
# Check if tag already exists locally and remove it
|
||||||
|
if git tag -l | grep -q "^$new_version$"; then
|
||||||
|
echo "⚠️ Local tag $new_version already exists, removing it..."
|
||||||
|
git tag -d "$new_version"
|
||||||
|
fi
|
||||||
|
|
||||||
|
# Make sure we're on the right branch and up to date
|
||||||
|
echo "🔄 Updating repository..."
|
||||||
|
git fetch origin
|
||||||
|
|
||||||
|
# Get current branch name
|
||||||
|
current_branch=$(git branch --show-current)
|
||||||
|
|
||||||
|
# If we're not on main or master, try to checkout one of them
|
||||||
|
if [[ "$current_branch" != "main" && "$current_branch" != "master" ]]; then
|
||||||
|
echo "Current branch: $current_branch"
|
||||||
|
if git show-ref --verify --quiet refs/heads/main; then
|
||||||
|
echo "Switching to main branch..."
|
||||||
|
git checkout main
|
||||||
|
current_branch="main"
|
||||||
|
elif git show-ref --verify --quiet refs/heads/master; then
|
||||||
|
echo "Switching to master branch..."
|
||||||
|
git checkout master
|
||||||
|
current_branch="master"
|
||||||
|
else
|
||||||
|
echo "⚠️ Neither main nor master branch found, staying on current branch: $current_branch"
|
||||||
|
fi
|
||||||
|
fi
|
||||||
|
|
||||||
|
echo "Pulling latest changes from $current_branch..."
|
||||||
|
git pull origin "$current_branch"
|
||||||
|
|
||||||
|
# Create and push the tag
|
||||||
|
echo "🏷️ Creating tag $new_version..."
|
||||||
|
git tag "$new_version"
|
||||||
|
|
||||||
|
echo "🚀 Pushing tag to trigger release..."
|
||||||
|
git push origin "$new_version"
|
||||||
|
|
||||||
|
echo "✅ Release $new_version has been triggered!"
|
||||||
|
echo "🔗 Check the release at: https://github.com/threefoldtech/zinit/releases"
|
||||||
|
echo "🔗 Monitor the build at: https://github.com/threefoldtech/zinit/actions"
|
||||||
139
components/zinit/src/app/api.rs
Normal file
139
components/zinit/src/app/api.rs
Normal file
@@ -0,0 +1,139 @@
|
|||||||
|
use super::rpc::{
|
||||||
|
ZinitLoggingApiServer, ZinitRpcApiServer, ZinitServiceApiServer, ZinitSystemApiServer,
|
||||||
|
};
|
||||||
|
use crate::zinit::ZInit;
|
||||||
|
use anyhow::{bail, Context, Result};
|
||||||
|
use jsonrpsee::server::ServerHandle;
|
||||||
|
use reth_ipc::server::Builder;
|
||||||
|
use serde::{Deserialize, Serialize};
|
||||||
|
use serde_json::Value;
|
||||||
|
use std::collections::HashMap;
|
||||||
|
use std::sync::Arc;
|
||||||
|
use tokio::sync::Mutex;
|
||||||
|
use tower_http::cors::{AllowHeaders, AllowMethods};
|
||||||
|
use tower_http::cors::{Any, CorsLayer};
|
||||||
|
|
||||||
|
#[derive(Clone, Debug, Deserialize, Serialize)]
|
||||||
|
#[serde(rename_all = "lowercase")]
|
||||||
|
struct ZinitResponse {
|
||||||
|
pub state: ZinitState,
|
||||||
|
pub body: Value,
|
||||||
|
}
|
||||||
|
|
||||||
|
#[derive(Clone, Debug, Deserialize, Serialize)]
|
||||||
|
#[serde(rename_all = "lowercase")]
|
||||||
|
enum ZinitState {
|
||||||
|
Ok,
|
||||||
|
Error,
|
||||||
|
}
|
||||||
|
|
||||||
|
#[derive(Clone, Debug, Deserialize, Serialize)]
|
||||||
|
#[serde(rename_all = "lowercase")]
|
||||||
|
pub struct Status {
|
||||||
|
pub name: String,
|
||||||
|
pub pid: u32,
|
||||||
|
pub state: String,
|
||||||
|
pub target: String,
|
||||||
|
pub after: HashMap<String, String>,
|
||||||
|
}
|
||||||
|
|
||||||
|
/// Service stats information
|
||||||
|
#[derive(Clone, Debug, Deserialize, Serialize)]
|
||||||
|
#[serde(rename_all = "lowercase")]
|
||||||
|
pub struct Stats {
|
||||||
|
pub name: String,
|
||||||
|
pub pid: u32,
|
||||||
|
pub memory_usage: u64,
|
||||||
|
pub cpu_usage: f32,
|
||||||
|
pub children: Vec<ChildStats>,
|
||||||
|
}
|
||||||
|
|
||||||
|
/// Child process stats information
|
||||||
|
#[derive(Clone, Debug, Deserialize, Serialize)]
|
||||||
|
#[serde(rename_all = "lowercase")]
|
||||||
|
pub struct ChildStats {
|
||||||
|
pub pid: u32,
|
||||||
|
pub memory_usage: u64,
|
||||||
|
pub cpu_usage: f32,
|
||||||
|
}
|
||||||
|
|
||||||
|
pub struct ApiServer {
|
||||||
|
_handle: ServerHandle,
|
||||||
|
}
|
||||||
|
|
||||||
|
#[derive(Clone)]
|
||||||
|
pub struct Api {
|
||||||
|
pub zinit: ZInit,
|
||||||
|
pub http_server_handle: Arc<Mutex<Option<jsonrpsee::server::ServerHandle>>>,
|
||||||
|
}
|
||||||
|
|
||||||
|
impl Api {
|
||||||
|
pub fn new(zinit: ZInit) -> Api {
|
||||||
|
Api {
|
||||||
|
zinit,
|
||||||
|
http_server_handle: Arc::new(Mutex::new(None)),
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
pub async fn serve(&self, endpoint: String) -> Result<ApiServer> {
|
||||||
|
let server = Builder::default().build(endpoint);
|
||||||
|
let mut module = ZinitRpcApiServer::into_rpc(self.clone());
|
||||||
|
module.merge(ZinitSystemApiServer::into_rpc(self.clone()))?;
|
||||||
|
module.merge(ZinitServiceApiServer::into_rpc(self.clone()))?;
|
||||||
|
module.merge(ZinitLoggingApiServer::into_rpc(self.clone()))?;
|
||||||
|
|
||||||
|
let _handle = server.start(module).await?;
|
||||||
|
|
||||||
|
Ok(ApiServer { _handle })
|
||||||
|
}
|
||||||
|
|
||||||
|
/// Start an HTTP/RPC server at a specified address
|
||||||
|
pub async fn start_http_server(&self, address: String) -> Result<String> {
|
||||||
|
// Parse the address string
|
||||||
|
let socket_addr = address
|
||||||
|
.parse::<std::net::SocketAddr>()
|
||||||
|
.context("Failed to parse socket address")?;
|
||||||
|
|
||||||
|
let cors = CorsLayer::new()
|
||||||
|
// Allow `POST` when accessing the resource
|
||||||
|
.allow_methods(AllowMethods::any())
|
||||||
|
// Allow requests from any origin
|
||||||
|
.allow_origin(Any)
|
||||||
|
.allow_headers(AllowHeaders::any());
|
||||||
|
let middleware = tower::ServiceBuilder::new().layer(cors);
|
||||||
|
|
||||||
|
// Create the JSON-RPC server with CORS support
|
||||||
|
let server_rpc = jsonrpsee::server::ServerBuilder::default()
|
||||||
|
.set_http_middleware(middleware)
|
||||||
|
.build(socket_addr)
|
||||||
|
.await?;
|
||||||
|
|
||||||
|
// Create and merge all API modules
|
||||||
|
let mut rpc_module = ZinitRpcApiServer::into_rpc(self.clone());
|
||||||
|
rpc_module.merge(ZinitSystemApiServer::into_rpc(self.clone()))?;
|
||||||
|
rpc_module.merge(ZinitServiceApiServer::into_rpc(self.clone()))?;
|
||||||
|
rpc_module.merge(ZinitLoggingApiServer::into_rpc(self.clone()))?;
|
||||||
|
|
||||||
|
// Start the server
|
||||||
|
let handle = server_rpc.start(rpc_module);
|
||||||
|
|
||||||
|
// Store the handle
|
||||||
|
let mut http_handle = self.http_server_handle.lock().await;
|
||||||
|
*http_handle = Some(handle);
|
||||||
|
|
||||||
|
Ok(format!("HTTP/RPC server started at {}", address))
|
||||||
|
}
|
||||||
|
|
||||||
|
/// Stop the HTTP/RPC server if running
|
||||||
|
pub async fn stop_http_server(&self) -> Result<()> {
|
||||||
|
let mut http_handle = self.http_server_handle.lock().await;
|
||||||
|
|
||||||
|
if http_handle.is_some() {
|
||||||
|
// The handle is automatically dropped, which should stop the server
|
||||||
|
*http_handle = None;
|
||||||
|
Ok(())
|
||||||
|
} else {
|
||||||
|
bail!("No HTTP/RPC server is currently running")
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
265
components/zinit/src/app/mod.rs
Normal file
265
components/zinit/src/app/mod.rs
Normal file
@@ -0,0 +1,265 @@
|
|||||||
|
pub mod api;
|
||||||
|
pub mod rpc;
|
||||||
|
|
||||||
|
use crate::zinit;
|
||||||
|
use anyhow::{Context, Result};
|
||||||
|
use api::ApiServer;
|
||||||
|
use reth_ipc::client::IpcClientBuilder;
|
||||||
|
use rpc::ZinitLoggingApiClient;
|
||||||
|
use rpc::ZinitServiceApiClient;
|
||||||
|
use rpc::ZinitSystemApiClient;
|
||||||
|
use serde_yaml as encoder;
|
||||||
|
use std::net::ToSocketAddrs;
|
||||||
|
use std::path::{Path, PathBuf};
|
||||||
|
use tokio::fs;
|
||||||
|
use tokio::signal;
|
||||||
|
use tokio::time;
|
||||||
|
use tokio_stream::wrappers::ReceiverStream;
|
||||||
|
use tokio_stream::Stream;
|
||||||
|
|
||||||
|
fn logger(level: log::LevelFilter) -> Result<()> {
|
||||||
|
let logger = fern::Dispatch::new()
|
||||||
|
.format(|out, message, record| {
|
||||||
|
out.finish(format_args!(
|
||||||
|
"zinit: {} ({}) {}",
|
||||||
|
record.level(),
|
||||||
|
record.target(),
|
||||||
|
message
|
||||||
|
))
|
||||||
|
})
|
||||||
|
.level(level)
|
||||||
|
.chain(std::io::stdout());
|
||||||
|
let logger = match std::fs::OpenOptions::new().write(true).open("/dev/kmsg") {
|
||||||
|
Ok(file) => logger.chain(file),
|
||||||
|
Err(_err) => logger,
|
||||||
|
};
|
||||||
|
logger.apply()?;
|
||||||
|
|
||||||
|
Ok(())
|
||||||
|
}
|
||||||
|
|
||||||
|
fn absolute<P: AsRef<Path>>(p: P) -> Result<PathBuf> {
|
||||||
|
let p = p.as_ref();
|
||||||
|
let result = if p.is_absolute() {
|
||||||
|
p.to_path_buf()
|
||||||
|
} else {
|
||||||
|
let mut current = std::env::current_dir()?;
|
||||||
|
current.push(p);
|
||||||
|
current
|
||||||
|
};
|
||||||
|
|
||||||
|
Ok(result)
|
||||||
|
}
|
||||||
|
|
||||||
|
pub async fn init(
|
||||||
|
cap: usize,
|
||||||
|
config: &str,
|
||||||
|
socket: &str,
|
||||||
|
container: bool,
|
||||||
|
debug: bool,
|
||||||
|
) -> Result<ApiServer> {
|
||||||
|
fs::create_dir_all(config)
|
||||||
|
.await
|
||||||
|
.with_context(|| format!("failed to create config directory '{}'", config))?;
|
||||||
|
if let Err(err) = logger(if debug {
|
||||||
|
log::LevelFilter::Debug
|
||||||
|
} else {
|
||||||
|
log::LevelFilter::Info
|
||||||
|
}) {
|
||||||
|
eprintln!("failed to setup logging: {}", err);
|
||||||
|
}
|
||||||
|
|
||||||
|
let config = absolute(Path::new(config)).context("failed to get config dire absolute path")?;
|
||||||
|
let socket_path =
|
||||||
|
absolute(Path::new(socket)).context("failed to get socket file absolute path")?;
|
||||||
|
|
||||||
|
if let Some(dir) = socket_path.parent() {
|
||||||
|
fs::create_dir_all(dir)
|
||||||
|
.await
|
||||||
|
.with_context(|| format!("failed to create directory {:?}", dir))?;
|
||||||
|
}
|
||||||
|
|
||||||
|
let _ = fs::remove_file(&socket).await;
|
||||||
|
|
||||||
|
debug!("switching to home dir: {}", config.display());
|
||||||
|
std::env::set_current_dir(&config).with_context(|| {
|
||||||
|
format!(
|
||||||
|
"failed to switch working directory to '{}'",
|
||||||
|
config.display()
|
||||||
|
)
|
||||||
|
})?;
|
||||||
|
|
||||||
|
let init = zinit::ZInit::new(cap, container);
|
||||||
|
|
||||||
|
init.serve();
|
||||||
|
|
||||||
|
let services = zinit::config::load_dir(&config)?;
|
||||||
|
for (k, v) in services {
|
||||||
|
if let Err(err) = init.monitor(&k, v).await {
|
||||||
|
error!("failed to monitor service {}: {}", k, err);
|
||||||
|
};
|
||||||
|
}
|
||||||
|
let a = api::Api::new(init);
|
||||||
|
a.serve(socket.into()).await
|
||||||
|
}
|
||||||
|
|
||||||
|
pub async fn list(socket: &str) -> Result<()> {
|
||||||
|
let client = IpcClientBuilder::default().build(socket.into()).await?;
|
||||||
|
let results = client.list().await?;
|
||||||
|
encoder::to_writer(std::io::stdout(), &results)?;
|
||||||
|
Ok(())
|
||||||
|
}
|
||||||
|
|
||||||
|
pub async fn shutdown(socket: &str) -> Result<()> {
|
||||||
|
let client = IpcClientBuilder::default().build(socket.into()).await?;
|
||||||
|
client.shutdown().await?;
|
||||||
|
Ok(())
|
||||||
|
}
|
||||||
|
|
||||||
|
pub async fn reboot(socket: &str) -> Result<()> {
|
||||||
|
let client = IpcClientBuilder::default().build(socket.into()).await?;
|
||||||
|
client.reboot().await?;
|
||||||
|
Ok(())
|
||||||
|
}
|
||||||
|
|
||||||
|
pub async fn status(socket: &str, name: String) -> Result<()> {
|
||||||
|
let client = IpcClientBuilder::default().build(socket.into()).await?;
|
||||||
|
let results = client.status(name).await?;
|
||||||
|
encoder::to_writer(std::io::stdout(), &results)?;
|
||||||
|
Ok(())
|
||||||
|
}
|
||||||
|
|
||||||
|
pub async fn start(socket: &str, name: String) -> Result<()> {
|
||||||
|
let client = IpcClientBuilder::default().build(socket.into()).await?;
|
||||||
|
client.start(name).await?;
|
||||||
|
Ok(())
|
||||||
|
}
|
||||||
|
|
||||||
|
pub async fn stop(socket: &str, name: String) -> Result<()> {
|
||||||
|
let client = IpcClientBuilder::default().build(socket.into()).await?;
|
||||||
|
client.stop(name).await?;
|
||||||
|
Ok(())
|
||||||
|
}
|
||||||
|
|
||||||
|
pub async fn restart(socket: &str, name: String) -> Result<()> {
|
||||||
|
let client = IpcClientBuilder::default().build(socket.into()).await?;
|
||||||
|
client.stop(name.clone()).await?;
|
||||||
|
//pull status
|
||||||
|
for _ in 0..20 {
|
||||||
|
let result = client.status(name.clone()).await?;
|
||||||
|
if result.pid == 0 && result.target == "Down" {
|
||||||
|
client.start(name.clone()).await?;
|
||||||
|
return Ok(());
|
||||||
|
}
|
||||||
|
time::sleep(std::time::Duration::from_secs(1)).await;
|
||||||
|
}
|
||||||
|
// process not stopped try to kill it
|
||||||
|
client.kill(name.clone(), "SIGKILL".into()).await?;
|
||||||
|
client.start(name).await?;
|
||||||
|
Ok(())
|
||||||
|
}
|
||||||
|
|
||||||
|
pub async fn forget(socket: &str, name: String) -> Result<()> {
|
||||||
|
let client = IpcClientBuilder::default().build(socket.into()).await?;
|
||||||
|
client.forget(name).await?;
|
||||||
|
Ok(())
|
||||||
|
}
|
||||||
|
|
||||||
|
pub async fn monitor(socket: &str, name: String) -> Result<()> {
|
||||||
|
let client = IpcClientBuilder::default().build(socket.into()).await?;
|
||||||
|
client.monitor(name).await?;
|
||||||
|
Ok(())
|
||||||
|
}
|
||||||
|
|
||||||
|
pub async fn kill(socket: &str, name: String, signal: String) -> Result<()> {
|
||||||
|
let client = IpcClientBuilder::default().build(socket.into()).await?;
|
||||||
|
client.kill(name, signal).await?;
|
||||||
|
Ok(())
|
||||||
|
}
|
||||||
|
|
||||||
|
pub async fn stats(socket: &str, name: String) -> Result<()> {
|
||||||
|
let client = IpcClientBuilder::default().build(socket.into()).await?;
|
||||||
|
let results = client.stats(name).await?;
|
||||||
|
encoder::to_writer(std::io::stdout(), &results)?;
|
||||||
|
Ok(())
|
||||||
|
}
|
||||||
|
|
||||||
|
pub async fn logs(
|
||||||
|
socket: &str,
|
||||||
|
filter: Option<String>,
|
||||||
|
follow: bool,
|
||||||
|
) -> Result<impl Stream<Item = String> + Unpin> {
|
||||||
|
let client = IpcClientBuilder::default().build(socket.into()).await?;
|
||||||
|
if let Some(ref filter) = filter {
|
||||||
|
client.status(filter.clone()).await?;
|
||||||
|
}
|
||||||
|
let logs = client.logs(filter.clone()).await?;
|
||||||
|
let (tx, rx) = tokio::sync::mpsc::channel(2000);
|
||||||
|
|
||||||
|
let logs_sub = if follow {
|
||||||
|
Some(client.log_subscribe(filter).await?)
|
||||||
|
} else {
|
||||||
|
None
|
||||||
|
};
|
||||||
|
tokio::task::spawn(async move {
|
||||||
|
for log in logs {
|
||||||
|
if tx.send(log).await.is_err() {
|
||||||
|
if let Some(logs_sub) = logs_sub {
|
||||||
|
let _ = logs_sub.unsubscribe().await;
|
||||||
|
}
|
||||||
|
// error means receiver is dead, so just quit
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
let Some(mut logs_sub) = logs_sub else { return };
|
||||||
|
loop {
|
||||||
|
match logs_sub.next().await {
|
||||||
|
Some(Ok(log)) => {
|
||||||
|
if tx.send(log).await.is_err() {
|
||||||
|
let _ = logs_sub.unsubscribe().await;
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
Some(Err(e)) => {
|
||||||
|
log::error!("Failed to get new log from subscription: {e}");
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
_ => return,
|
||||||
|
}
|
||||||
|
}
|
||||||
|
});
|
||||||
|
|
||||||
|
Ok(ReceiverStream::new(rx))
|
||||||
|
}
|
||||||
|
|
||||||
|
/// Start an HTTP/RPC proxy server for the Zinit API at the specified address
|
||||||
|
pub async fn proxy(sock: &str, address: String) -> Result<()> {
|
||||||
|
// Parse the socket address
|
||||||
|
let _socket_addr = address
|
||||||
|
.to_socket_addrs()
|
||||||
|
.context("Failed to parse socket address")?
|
||||||
|
.next()
|
||||||
|
.context("No valid socket address found")?;
|
||||||
|
|
||||||
|
println!("Starting HTTP/RPC server on {}", address);
|
||||||
|
println!("Connecting to Zinit daemon at {}", sock);
|
||||||
|
|
||||||
|
// Connect to the existing Zinit daemon through the Unix socket
|
||||||
|
let client = IpcClientBuilder::default().build(sock.into()).await?;
|
||||||
|
|
||||||
|
// Issue an RPC call to start the HTTP server on the specified address
|
||||||
|
let result = client.start_http_server(address.clone()).await?;
|
||||||
|
|
||||||
|
println!("{}", result);
|
||||||
|
println!("Press Ctrl+C to stop");
|
||||||
|
|
||||||
|
// Wait for Ctrl+C to shutdown
|
||||||
|
signal::ctrl_c().await?;
|
||||||
|
|
||||||
|
// Shutdown the HTTP server
|
||||||
|
client.stop_http_server().await?;
|
||||||
|
|
||||||
|
println!("HTTP/RPC server stopped");
|
||||||
|
|
||||||
|
Ok(())
|
||||||
|
}
|
||||||
426
components/zinit/src/app/rpc.rs
Normal file
426
components/zinit/src/app/rpc.rs
Normal file
@@ -0,0 +1,426 @@
|
|||||||
|
use crate::app::api::{ChildStats, Stats, Status};
|
||||||
|
use crate::zinit::config;
|
||||||
|
use async_trait::async_trait;
|
||||||
|
use jsonrpsee::core::{RpcResult, SubscriptionResult};
|
||||||
|
use jsonrpsee::proc_macros::rpc;
|
||||||
|
use jsonrpsee::types::{ErrorCode, ErrorObjectOwned};
|
||||||
|
use jsonrpsee::PendingSubscriptionSink;
|
||||||
|
use serde_json::{Map, Value};
|
||||||
|
use std::collections::HashMap;
|
||||||
|
use std::str::FromStr;
|
||||||
|
use tokio_stream::StreamExt;
|
||||||
|
|
||||||
|
use super::api::Api;
|
||||||
|
|
||||||
|
// Custom error codes for Zinit
|
||||||
|
const SERVICE_NOT_FOUND: i32 = -32000;
|
||||||
|
const SERVICE_IS_UP: i32 = -32002;
|
||||||
|
const SHUTTING_DOWN: i32 = -32006;
|
||||||
|
const SERVICE_ALREADY_EXISTS: i32 = -32007;
|
||||||
|
const SERVICE_FILE_ERROR: i32 = -32008;
|
||||||
|
|
||||||
|
// Include the OpenRPC specification
|
||||||
|
const OPENRPC_SPEC: &str = include_str!("../../openrpc.json");
|
||||||
|
|
||||||
|
/// RPC methods for discovery.
|
||||||
|
#[rpc(server, client)]
|
||||||
|
pub trait ZinitRpcApi {
|
||||||
|
/// Returns the OpenRPC specification as a string.
|
||||||
|
#[method(name = "rpc.discover")]
|
||||||
|
async fn discover(&self) -> RpcResult<String>;
|
||||||
|
}
|
||||||
|
|
||||||
|
#[async_trait]
|
||||||
|
impl ZinitRpcApiServer for Api {
|
||||||
|
async fn discover(&self) -> RpcResult<String> {
|
||||||
|
Ok(OPENRPC_SPEC.to_string())
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
/// RPC methods for service management.
|
||||||
|
#[rpc(server, client, namespace = "service")]
|
||||||
|
pub trait ZinitServiceApi {
|
||||||
|
/// List all monitored services and their current state.
|
||||||
|
/// Returns a map where keys are service names and values are state strings.
|
||||||
|
#[method(name = "list")]
|
||||||
|
async fn list(&self) -> RpcResult<HashMap<String, String>>;
|
||||||
|
|
||||||
|
/// Get the detailed status of a specific service.
|
||||||
|
#[method(name = "status")]
|
||||||
|
async fn status(&self, name: String) -> RpcResult<Status>;
|
||||||
|
|
||||||
|
/// Start a specific service.
|
||||||
|
#[method(name = "start")]
|
||||||
|
async fn start(&self, name: String) -> RpcResult<()>;
|
||||||
|
|
||||||
|
/// Stop a specific service.
|
||||||
|
#[method(name = "stop")]
|
||||||
|
async fn stop(&self, name: String) -> RpcResult<()>;
|
||||||
|
|
||||||
|
/// Load and monitor a new service from its configuration file (e.g., "service_name.yaml").
|
||||||
|
#[method(name = "monitor")]
|
||||||
|
async fn monitor(&self, name: String) -> RpcResult<()>;
|
||||||
|
|
||||||
|
/// Stop monitoring a service and remove it from management.
|
||||||
|
#[method(name = "forget")]
|
||||||
|
async fn forget(&self, name: String) -> RpcResult<()>;
|
||||||
|
|
||||||
|
/// Send a signal (e.g., "SIGTERM", "SIGKILL") to a specific service process.
|
||||||
|
#[method(name = "kill")]
|
||||||
|
async fn kill(&self, name: String, signal: String) -> RpcResult<()>;
|
||||||
|
|
||||||
|
/// Create a new service configuration file (e.g., "service_name.yaml")
|
||||||
|
/// with the provided content (JSON map representing YAML structure).
|
||||||
|
/// Returns a success message string.
|
||||||
|
#[method(name = "create")]
|
||||||
|
async fn create(&self, name: String, content: Map<String, Value>) -> RpcResult<String>;
|
||||||
|
|
||||||
|
/// Delete a service configuration file.
|
||||||
|
/// Returns a success message string.
|
||||||
|
#[method(name = "delete")]
|
||||||
|
async fn delete(&self, name: String) -> RpcResult<String>;
|
||||||
|
|
||||||
|
/// Get the content of a service configuration file as a JSON Value.
|
||||||
|
#[method(name = "get")]
|
||||||
|
async fn get(&self, name: String) -> RpcResult<Value>;
|
||||||
|
|
||||||
|
/// Get memory and CPU usage statistics for a service.
|
||||||
|
#[method(name = "stats")]
|
||||||
|
async fn stats(&self, name: String) -> RpcResult<Stats>;
|
||||||
|
}
|
||||||
|
|
||||||
|
#[async_trait]
|
||||||
|
impl ZinitServiceApiServer for Api {
|
||||||
|
async fn list(&self) -> RpcResult<HashMap<String, String>> {
|
||||||
|
let services = self
|
||||||
|
.zinit
|
||||||
|
.list()
|
||||||
|
.await
|
||||||
|
.map_err(|_| ErrorObjectOwned::from(ErrorCode::InternalError))?;
|
||||||
|
|
||||||
|
let mut map: HashMap<String, String> = HashMap::new();
|
||||||
|
for service in services {
|
||||||
|
let state = self
|
||||||
|
.zinit
|
||||||
|
.status(&service)
|
||||||
|
.await
|
||||||
|
.map_err(|_| ErrorObjectOwned::from(ErrorCode::InternalError))?;
|
||||||
|
map.insert(service, format!("{:?}", state.state));
|
||||||
|
}
|
||||||
|
Ok(map)
|
||||||
|
}
|
||||||
|
|
||||||
|
async fn status(&self, name: String) -> RpcResult<Status> {
|
||||||
|
let status = self
|
||||||
|
.zinit
|
||||||
|
.status(&name)
|
||||||
|
.await
|
||||||
|
.map_err(|_| ErrorObjectOwned::from(ErrorCode::InternalError))?;
|
||||||
|
|
||||||
|
let result = Status {
|
||||||
|
name: name.clone(),
|
||||||
|
pid: status.pid.as_raw() as u32,
|
||||||
|
state: format!("{:?}", status.state),
|
||||||
|
target: format!("{:?}", status.target),
|
||||||
|
after: {
|
||||||
|
let mut after = HashMap::new();
|
||||||
|
for service in status.service.after {
|
||||||
|
let status = match self.zinit.status(&service).await {
|
||||||
|
Ok(dep) => dep.state,
|
||||||
|
Err(_) => crate::zinit::State::Unknown,
|
||||||
|
};
|
||||||
|
after.insert(service, format!("{:?}", status));
|
||||||
|
}
|
||||||
|
after
|
||||||
|
},
|
||||||
|
};
|
||||||
|
|
||||||
|
Ok(result)
|
||||||
|
}
|
||||||
|
|
||||||
|
async fn start(&self, name: String) -> RpcResult<()> {
|
||||||
|
self.zinit
|
||||||
|
.start(name)
|
||||||
|
.await
|
||||||
|
.map_err(|_| ErrorObjectOwned::from(ErrorCode::ServerError(SERVICE_IS_UP)))
|
||||||
|
}
|
||||||
|
|
||||||
|
async fn stop(&self, name: String) -> RpcResult<()> {
|
||||||
|
self.zinit
|
||||||
|
.stop(name)
|
||||||
|
.await
|
||||||
|
.map_err(|_| ErrorObjectOwned::from(ErrorCode::InternalError))
|
||||||
|
}
|
||||||
|
|
||||||
|
async fn monitor(&self, name: String) -> RpcResult<()> {
|
||||||
|
if let Ok((name_str, service)) = config::load(format!("{}.yaml", name))
|
||||||
|
.map_err(|_| ErrorObjectOwned::from(ErrorCode::InternalError))
|
||||||
|
{
|
||||||
|
self.zinit
|
||||||
|
.monitor(name_str, service)
|
||||||
|
.await
|
||||||
|
.map_err(|_| ErrorObjectOwned::from(ErrorCode::InternalError))
|
||||||
|
} else {
|
||||||
|
Err(ErrorObjectOwned::from(ErrorCode::InternalError))
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
async fn forget(&self, name: String) -> RpcResult<()> {
|
||||||
|
self.zinit
|
||||||
|
.forget(name)
|
||||||
|
.await
|
||||||
|
.map_err(|_| ErrorObjectOwned::from(ErrorCode::InternalError))
|
||||||
|
}
|
||||||
|
|
||||||
|
async fn kill(&self, name: String, signal: String) -> RpcResult<()> {
|
||||||
|
if let Ok(sig) = nix::sys::signal::Signal::from_str(&signal.to_uppercase()) {
|
||||||
|
self.zinit
|
||||||
|
.kill(name, sig)
|
||||||
|
.await
|
||||||
|
.map_err(|_e| ErrorObjectOwned::from(ErrorCode::InternalError))
|
||||||
|
} else {
|
||||||
|
Err(ErrorObjectOwned::from(ErrorCode::InternalError))
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
async fn create(&self, name: String, content: Map<String, Value>) -> RpcResult<String> {
|
||||||
|
use std::fs;
|
||||||
|
use std::io::Write;
|
||||||
|
use std::path::PathBuf;
|
||||||
|
|
||||||
|
// Validate service name (no path traversal, valid characters)
|
||||||
|
if name.contains('/') || name.contains('\\') || name.contains('.') {
|
||||||
|
return Err(ErrorObjectOwned::from(ErrorCode::InternalError));
|
||||||
|
}
|
||||||
|
|
||||||
|
// Construct the file path
|
||||||
|
let file_path = PathBuf::from(format!("{}.yaml", name));
|
||||||
|
|
||||||
|
// Check if the service file already exists
|
||||||
|
if file_path.exists() {
|
||||||
|
return Err(ErrorObjectOwned::from(ErrorCode::ServerError(
|
||||||
|
SERVICE_ALREADY_EXISTS,
|
||||||
|
)));
|
||||||
|
}
|
||||||
|
|
||||||
|
// Convert the JSON content to YAML
|
||||||
|
let yaml_content = serde_yaml::to_string(&content)
|
||||||
|
.map_err(|_| ErrorObjectOwned::from(ErrorCode::InternalError))?;
|
||||||
|
|
||||||
|
// Write the YAML content to the file
|
||||||
|
let mut file = fs::File::create(&file_path)
|
||||||
|
.map_err(|_| ErrorObjectOwned::from(ErrorCode::ServerError(SERVICE_FILE_ERROR)))?;
|
||||||
|
|
||||||
|
file.write_all(yaml_content.as_bytes())
|
||||||
|
.map_err(|_| ErrorObjectOwned::from(ErrorCode::ServerError(SERVICE_FILE_ERROR)))?;
|
||||||
|
|
||||||
|
Ok(format!("Service '{}' created successfully", name))
|
||||||
|
}
|
||||||
|
|
||||||
|
async fn delete(&self, name: String) -> RpcResult<String> {
|
||||||
|
use std::fs;
|
||||||
|
use std::path::PathBuf;
|
||||||
|
|
||||||
|
// Validate service name (no path traversal, valid characters)
|
||||||
|
if name.contains('/') || name.contains('\\') || name.contains('.') {
|
||||||
|
return Err(ErrorObjectOwned::from(ErrorCode::InternalError));
|
||||||
|
}
|
||||||
|
|
||||||
|
// Construct the file path
|
||||||
|
let file_path = PathBuf::from(format!("{}.yaml", name));
|
||||||
|
|
||||||
|
// Check if the service file exists
|
||||||
|
if !file_path.exists() {
|
||||||
|
return Err(ErrorObjectOwned::from(ErrorCode::ServerError(
|
||||||
|
SERVICE_NOT_FOUND,
|
||||||
|
)));
|
||||||
|
}
|
||||||
|
|
||||||
|
// Delete the file
|
||||||
|
fs::remove_file(&file_path)
|
||||||
|
.map_err(|_| ErrorObjectOwned::from(ErrorCode::ServerError(SERVICE_FILE_ERROR)))?;
|
||||||
|
|
||||||
|
Ok(format!("Service '{}' deleted successfully", name))
|
||||||
|
}
|
||||||
|
|
||||||
|
async fn get(&self, name: String) -> RpcResult<Value> {
|
||||||
|
use std::fs;
|
||||||
|
use std::path::PathBuf;
|
||||||
|
|
||||||
|
// Validate service name (no path traversal, valid characters)
|
||||||
|
if name.contains('/') || name.contains('\\') || name.contains('.') {
|
||||||
|
return Err(ErrorObjectOwned::from(ErrorCode::InternalError));
|
||||||
|
}
|
||||||
|
|
||||||
|
// Construct the file path
|
||||||
|
let file_path = PathBuf::from(format!("{}.yaml", name));
|
||||||
|
|
||||||
|
// Check if the service file exists
|
||||||
|
if !file_path.exists() {
|
||||||
|
return Err(ErrorObjectOwned::from(ErrorCode::ServerError(
|
||||||
|
SERVICE_NOT_FOUND,
|
||||||
|
)));
|
||||||
|
}
|
||||||
|
|
||||||
|
// Read the file content
|
||||||
|
let yaml_content = fs::read_to_string(&file_path)
|
||||||
|
.map_err(|_| ErrorObjectOwned::from(ErrorCode::ServerError(SERVICE_FILE_ERROR)))?;
|
||||||
|
|
||||||
|
// Parse YAML to JSON
|
||||||
|
let yaml_value: serde_yaml::Value = serde_yaml::from_str(&yaml_content)
|
||||||
|
.map_err(|_| ErrorObjectOwned::from(ErrorCode::InternalError))?;
|
||||||
|
|
||||||
|
// Convert YAML value to JSON value
|
||||||
|
let json_value = serde_json::to_value(yaml_value)
|
||||||
|
.map_err(|_| ErrorObjectOwned::from(ErrorCode::InternalError))?;
|
||||||
|
|
||||||
|
Ok(json_value)
|
||||||
|
}
|
||||||
|
|
||||||
|
async fn stats(&self, name: String) -> RpcResult<Stats> {
|
||||||
|
let stats = self
|
||||||
|
.zinit
|
||||||
|
.stats(&name)
|
||||||
|
.await
|
||||||
|
.map_err(|_| ErrorObjectOwned::from(ErrorCode::InternalError))?;
|
||||||
|
|
||||||
|
let result = Stats {
|
||||||
|
name: name.clone(),
|
||||||
|
pid: stats.pid as u32,
|
||||||
|
memory_usage: stats.memory_usage,
|
||||||
|
cpu_usage: stats.cpu_usage,
|
||||||
|
children: stats
|
||||||
|
.children
|
||||||
|
.into_iter()
|
||||||
|
.map(|child| ChildStats {
|
||||||
|
pid: child.pid as u32,
|
||||||
|
memory_usage: child.memory_usage,
|
||||||
|
cpu_usage: child.cpu_usage,
|
||||||
|
})
|
||||||
|
.collect(),
|
||||||
|
};
|
||||||
|
|
||||||
|
Ok(result)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
/// RPC methods for system-level operations.
|
||||||
|
#[rpc(server, client, namespace = "system")]
|
||||||
|
pub trait ZinitSystemApi {
|
||||||
|
/// Initiate system shutdown process.
|
||||||
|
#[method(name = "shutdown")]
|
||||||
|
async fn shutdown(&self) -> RpcResult<()>;
|
||||||
|
|
||||||
|
/// Initiate system reboot process.
|
||||||
|
#[method(name = "reboot")]
|
||||||
|
async fn reboot(&self) -> RpcResult<()>;
|
||||||
|
|
||||||
|
/// Start an HTTP/RPC server at the specified address
|
||||||
|
#[method(name = "start_http_server")]
|
||||||
|
async fn start_http_server(&self, address: String) -> RpcResult<String>;
|
||||||
|
|
||||||
|
/// Stop the HTTP/RPC server if running
|
||||||
|
#[method(name = "stop_http_server")]
|
||||||
|
async fn stop_http_server(&self) -> RpcResult<()>;
|
||||||
|
}
|
||||||
|
|
||||||
|
#[async_trait]
|
||||||
|
impl ZinitSystemApiServer for Api {
|
||||||
|
async fn shutdown(&self) -> RpcResult<()> {
|
||||||
|
self.zinit
|
||||||
|
.shutdown()
|
||||||
|
.await
|
||||||
|
.map_err(|_e| ErrorObjectOwned::from(ErrorCode::ServerError(SHUTTING_DOWN)))
|
||||||
|
}
|
||||||
|
|
||||||
|
async fn reboot(&self) -> RpcResult<()> {
|
||||||
|
self.zinit
|
||||||
|
.reboot()
|
||||||
|
.await
|
||||||
|
.map_err(|_| ErrorObjectOwned::from(ErrorCode::InternalError))
|
||||||
|
}
|
||||||
|
|
||||||
|
async fn start_http_server(&self, address: String) -> RpcResult<String> {
|
||||||
|
// Call the method from the API implementation
|
||||||
|
match crate::app::api::Api::start_http_server(self, address).await {
|
||||||
|
Ok(result) => Ok(result),
|
||||||
|
Err(_) => Err(ErrorObjectOwned::from(ErrorCode::InternalError)),
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
async fn stop_http_server(&self) -> RpcResult<()> {
|
||||||
|
// Call the method from the API implementation
|
||||||
|
match crate::app::api::Api::stop_http_server(self).await {
|
||||||
|
Ok(_) => Ok(()),
|
||||||
|
Err(_) => Err(ErrorObjectOwned::from(ErrorCode::InternalError)),
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
/// RPC subscription methods for streaming data.
|
||||||
|
#[rpc(server, client, namespace = "stream")]
|
||||||
|
pub trait ZinitLoggingApi {
|
||||||
|
#[method(name = "currentLogs")]
|
||||||
|
async fn logs(&self, name: Option<String>) -> RpcResult<Vec<String>>;
|
||||||
|
/// Subscribe to log messages generated by zinit and monitored services.
|
||||||
|
/// An optional filter can be provided to only receive logs containing the filter string.
|
||||||
|
/// The subscription returns a stream of log lines (String).
|
||||||
|
#[subscription(name = "subscribeLogs", item = String)]
|
||||||
|
async fn log_subscribe(&self, filter: Option<String>) -> SubscriptionResult;
|
||||||
|
}
|
||||||
|
|
||||||
|
#[async_trait]
|
||||||
|
impl ZinitLoggingApiServer for Api {
|
||||||
|
async fn logs(&self, name: Option<String>) -> RpcResult<Vec<String>> {
|
||||||
|
let filter = name.map(|n| format!("{n}:"));
|
||||||
|
Ok(
|
||||||
|
tokio_stream::wrappers::ReceiverStream::new(self.zinit.logs(true, false).await)
|
||||||
|
.filter_map(|l| {
|
||||||
|
if let Some(ref filter) = filter {
|
||||||
|
if l[4..].starts_with(filter) {
|
||||||
|
Some(l.to_string())
|
||||||
|
} else {
|
||||||
|
None
|
||||||
|
}
|
||||||
|
} else {
|
||||||
|
Some(l.to_string())
|
||||||
|
}
|
||||||
|
})
|
||||||
|
.collect()
|
||||||
|
.await,
|
||||||
|
)
|
||||||
|
}
|
||||||
|
|
||||||
|
async fn log_subscribe(
|
||||||
|
&self,
|
||||||
|
sink: PendingSubscriptionSink,
|
||||||
|
name: Option<String>,
|
||||||
|
) -> SubscriptionResult {
|
||||||
|
let sink = sink.accept().await?;
|
||||||
|
let filter = name.map(|n| format!("{n}:"));
|
||||||
|
let mut stream =
|
||||||
|
tokio_stream::wrappers::ReceiverStream::new(self.zinit.logs(false, true).await)
|
||||||
|
.filter_map(|l| {
|
||||||
|
if let Some(ref filter) = filter {
|
||||||
|
if l[4..].starts_with(filter) {
|
||||||
|
Some(l.to_string())
|
||||||
|
} else {
|
||||||
|
None
|
||||||
|
}
|
||||||
|
} else {
|
||||||
|
Some(l.to_string())
|
||||||
|
}
|
||||||
|
});
|
||||||
|
while let Some(log) = stream.next().await {
|
||||||
|
if sink
|
||||||
|
.send(serde_json::value::to_raw_value(&log)?)
|
||||||
|
.await
|
||||||
|
.is_err()
|
||||||
|
{
|
||||||
|
break;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
Ok(())
|
||||||
|
}
|
||||||
|
}
|
||||||
172
components/zinit/src/bin/testapp.rs
Normal file
172
components/zinit/src/bin/testapp.rs
Normal file
@@ -0,0 +1,172 @@
|
|||||||
|
#[tokio::main]
|
||||||
|
async fn main() {
|
||||||
|
println!("hello from testapp");
|
||||||
|
}
|
||||||
|
|
||||||
|
// extern crate zinit;
|
||||||
|
|
||||||
|
// use anyhow::Result;
|
||||||
|
// use serde_json::json;
|
||||||
|
// use std::env;
|
||||||
|
// use tokio::time::{sleep, Duration};
|
||||||
|
|
||||||
|
// use zinit::testapp;
|
||||||
|
|
||||||
|
// #[tokio::main]
|
||||||
|
// async fn main() -> Result<()> {
|
||||||
|
// // Define paths for socket and config
|
||||||
|
// let temp_dir = env::temp_dir();
|
||||||
|
// let socket_path = temp_dir
|
||||||
|
// .join("zinit-test.sock")
|
||||||
|
// .to_str()
|
||||||
|
// .unwrap()
|
||||||
|
// .to_string();
|
||||||
|
// let config_dir = temp_dir
|
||||||
|
// .join("zinit-test-config")
|
||||||
|
// .to_str()
|
||||||
|
// .unwrap()
|
||||||
|
// .to_string();
|
||||||
|
|
||||||
|
// println!("Starting zinit with socket at: {}", socket_path);
|
||||||
|
// println!("Using config directory: {}", config_dir);
|
||||||
|
|
||||||
|
// // Start zinit in the background
|
||||||
|
// testapp::start_zinit(&socket_path, &config_dir).await?;
|
||||||
|
|
||||||
|
// // Wait for zinit to initialize
|
||||||
|
// sleep(Duration::from_secs(2)).await;
|
||||||
|
|
||||||
|
// // Create a client to communicate with zinit
|
||||||
|
// let client = Client::new(&socket_path);
|
||||||
|
|
||||||
|
// // Create service configurations
|
||||||
|
// println!("Creating service configurations...");
|
||||||
|
|
||||||
|
// // Create a find service
|
||||||
|
// testapp::create_service_config(
|
||||||
|
// &config_dir,
|
||||||
|
// "find-service",
|
||||||
|
// "find / -name \"*.txt\" -type f",
|
||||||
|
// )
|
||||||
|
// .await?;
|
||||||
|
|
||||||
|
// // Create a sleep service with echo
|
||||||
|
// testapp::create_service_config(
|
||||||
|
// &config_dir,
|
||||||
|
// "sleep-service",
|
||||||
|
// "sh -c 'echo Starting sleep; sleep 30; echo Finished sleep'",
|
||||||
|
// )
|
||||||
|
// .await?;
|
||||||
|
|
||||||
|
// // Wait for zinit to load the configurations
|
||||||
|
// sleep(Duration::from_secs(1)).await;
|
||||||
|
|
||||||
|
// // Tell zinit to monitor our services
|
||||||
|
// println!("Monitoring services...");
|
||||||
|
// client.monitor("find-service").await?;
|
||||||
|
// client.monitor("sleep-service").await?;
|
||||||
|
|
||||||
|
// // List all services
|
||||||
|
// println!("\nListing all services:");
|
||||||
|
// let services = client.list().await?;
|
||||||
|
// for (name, status) in services {
|
||||||
|
// println!("Service: {} - Status: {}", name, status);
|
||||||
|
// }
|
||||||
|
|
||||||
|
// // Start the find service
|
||||||
|
// println!("\nStarting find-service...");
|
||||||
|
// client.start("find-service").await?;
|
||||||
|
|
||||||
|
// // Wait a bit and check status
|
||||||
|
// sleep(Duration::from_secs(2)).await;
|
||||||
|
// let status = client.status("find-service").await?;
|
||||||
|
// println!("find-service status: {:?}", status);
|
||||||
|
|
||||||
|
// // Start the sleep service
|
||||||
|
// println!("\nStarting sleep-service...");
|
||||||
|
// client.start("sleep-service").await?;
|
||||||
|
|
||||||
|
// // Wait a bit and check status
|
||||||
|
// sleep(Duration::from_secs(2)).await;
|
||||||
|
// let status = client.status("sleep-service").await?;
|
||||||
|
// println!("sleep-service status: {:?}", status);
|
||||||
|
|
||||||
|
// // Stop the find service
|
||||||
|
// println!("\nStopping find-service...");
|
||||||
|
// client.stop("find-service").await?;
|
||||||
|
|
||||||
|
// // Wait a bit and check status
|
||||||
|
// sleep(Duration::from_secs(2)).await;
|
||||||
|
// let status = client.status("find-service").await?;
|
||||||
|
// println!("find-service status after stopping: {:?}", status);
|
||||||
|
|
||||||
|
// // Kill the sleep service with SIGTERM
|
||||||
|
// println!("\nKilling sleep-service with SIGTERM...");
|
||||||
|
// client.kill("sleep-service", "SIGTERM").await?;
|
||||||
|
|
||||||
|
// // Wait a bit and check status
|
||||||
|
// sleep(Duration::from_secs(2)).await;
|
||||||
|
// let status = client.status("sleep-service").await?;
|
||||||
|
// println!("sleep-service status after killing: {:?}", status);
|
||||||
|
|
||||||
|
// // Cleanup - forget services
|
||||||
|
// println!("\nForgetting services...");
|
||||||
|
// if status.pid == 0 {
|
||||||
|
// // Only forget if it's not running
|
||||||
|
// client.forget("sleep-service").await?;
|
||||||
|
// }
|
||||||
|
// client.forget("find-service").await?;
|
||||||
|
|
||||||
|
// // Demonstrate service file operations
|
||||||
|
// println!("\nDemonstrating service file operations...");
|
||||||
|
|
||||||
|
// // Create a new service using the API
|
||||||
|
// println!("Creating a new service via API...");
|
||||||
|
// let service_content = json!({
|
||||||
|
// "exec": "echo 'Hello from API-created service'",
|
||||||
|
// "oneshot": true,
|
||||||
|
// "log": "stdout"
|
||||||
|
// })
|
||||||
|
// .as_object()
|
||||||
|
// .unwrap()
|
||||||
|
// .clone();
|
||||||
|
|
||||||
|
// let result = client
|
||||||
|
// .create_service("api-service", service_content)
|
||||||
|
// .await?;
|
||||||
|
// println!("Create service result: {}", result);
|
||||||
|
|
||||||
|
// // Get the service configuration
|
||||||
|
// println!("\nGetting service configuration...");
|
||||||
|
// let config = client.get_service("api-service").await?;
|
||||||
|
// println!(
|
||||||
|
// "Service configuration: {}",
|
||||||
|
// serde_json::to_string_pretty(&config)?
|
||||||
|
// );
|
||||||
|
|
||||||
|
// // Monitor and start the new service
|
||||||
|
// println!("\nMonitoring and starting the new service...");
|
||||||
|
// client.monitor("api-service").await?;
|
||||||
|
// client.start("api-service").await?;
|
||||||
|
|
||||||
|
// // Wait a bit and check status
|
||||||
|
// sleep(Duration::from_secs(2)).await;
|
||||||
|
// let status = client.status("api-service").await?;
|
||||||
|
// println!("api-service status: {:?}", status);
|
||||||
|
|
||||||
|
// // Delete the service
|
||||||
|
// println!("\nDeleting the service...");
|
||||||
|
// if status.pid == 0 {
|
||||||
|
// // Only forget if it's not running
|
||||||
|
// client.forget("api-service").await?;
|
||||||
|
// let result = client.delete_service("api-service").await?;
|
||||||
|
// println!("Delete service result: {}", result);
|
||||||
|
// }
|
||||||
|
|
||||||
|
// // Shutdown zinit
|
||||||
|
// println!("\nShutting down zinit...");
|
||||||
|
// client.shutdown().await?;
|
||||||
|
|
||||||
|
// println!("\nTest completed successfully!");
|
||||||
|
// Ok(())
|
||||||
|
// }
|
||||||
11
components/zinit/src/lib.rs
Normal file
11
components/zinit/src/lib.rs
Normal file
@@ -0,0 +1,11 @@
|
|||||||
|
extern crate serde;
|
||||||
|
#[macro_use]
|
||||||
|
extern crate anyhow;
|
||||||
|
#[macro_use]
|
||||||
|
extern crate log;
|
||||||
|
extern crate tokio;
|
||||||
|
|
||||||
|
pub mod app;
|
||||||
|
pub mod manager;
|
||||||
|
pub mod testapp;
|
||||||
|
pub mod zinit;
|
||||||
281
components/zinit/src/main.rs
Normal file
281
components/zinit/src/main.rs
Normal file
@@ -0,0 +1,281 @@
|
|||||||
|
extern crate zinit;
|
||||||
|
|
||||||
|
use anyhow::Result;
|
||||||
|
use clap::{App, Arg, SubCommand};
|
||||||
|
use git_version::git_version;
|
||||||
|
|
||||||
|
use tokio_stream::StreamExt;
|
||||||
|
use zinit::app;
|
||||||
|
|
||||||
|
const GIT_VERSION: &str = git_version!(args = ["--tags", "--always", "--dirty=-modified"]);
|
||||||
|
|
||||||
|
#[tokio::main]
|
||||||
|
async fn main() -> Result<()> {
|
||||||
|
let matches = App::new("zinit")
|
||||||
|
.author("ThreeFold Tech, https://github.com/threefoldtech")
|
||||||
|
.version(GIT_VERSION)
|
||||||
|
.about("A runit replacement")
|
||||||
|
.arg(Arg::with_name("socket").value_name("SOCKET").short("s").long("socket").default_value("/tmp/zinit.sock").help("path to unix socket"))
|
||||||
|
.arg(Arg::with_name("debug").short("d").long("debug").help("run in debug mode"))
|
||||||
|
.subcommand(
|
||||||
|
SubCommand::with_name("init")
|
||||||
|
.arg(
|
||||||
|
Arg::with_name("config")
|
||||||
|
.value_name("DIR")
|
||||||
|
.short("c")
|
||||||
|
.long("config")
|
||||||
|
.help("service configurations directory"),
|
||||||
|
)
|
||||||
|
.arg(
|
||||||
|
Arg::with_name("buffer")
|
||||||
|
.value_name("BUFFER")
|
||||||
|
.short("b")
|
||||||
|
.long("buffer")
|
||||||
|
.help("buffer size (in lines) to keep services logs")
|
||||||
|
.default_value("2000")
|
||||||
|
)
|
||||||
|
.arg(Arg::with_name("container").long("container").help("run in container mode, shutdown on signal"))
|
||||||
|
.about("run in init mode, start and maintain configured services"),
|
||||||
|
)
|
||||||
|
.subcommand(
|
||||||
|
SubCommand::with_name("list")
|
||||||
|
.about("quick view of current known services and their status"),
|
||||||
|
)
|
||||||
|
.subcommand(
|
||||||
|
SubCommand::with_name("shutdown")
|
||||||
|
.about("stop all services and power off"),
|
||||||
|
)
|
||||||
|
.subcommand(
|
||||||
|
SubCommand::with_name("reboot")
|
||||||
|
.about("stop all services and reboot"),
|
||||||
|
)
|
||||||
|
.subcommand(
|
||||||
|
SubCommand::with_name("status")
|
||||||
|
.arg(
|
||||||
|
Arg::with_name("service")
|
||||||
|
.value_name("SERVICE")
|
||||||
|
.required(true)
|
||||||
|
.help("service name"),
|
||||||
|
)
|
||||||
|
.about("show detailed service status"),
|
||||||
|
)
|
||||||
|
.subcommand(
|
||||||
|
SubCommand::with_name("stop")
|
||||||
|
.arg(
|
||||||
|
Arg::with_name("service")
|
||||||
|
.value_name("SERVICE")
|
||||||
|
.required(true)
|
||||||
|
.help("service name"),
|
||||||
|
)
|
||||||
|
.about("stop service"),
|
||||||
|
)
|
||||||
|
.subcommand(
|
||||||
|
SubCommand::with_name("start")
|
||||||
|
.arg(
|
||||||
|
Arg::with_name("service")
|
||||||
|
.value_name("SERVICE")
|
||||||
|
.required(true)
|
||||||
|
.help("service name"),
|
||||||
|
)
|
||||||
|
.about("start service. has no effect if the service is already running"),
|
||||||
|
)
|
||||||
|
.subcommand(
|
||||||
|
SubCommand::with_name("forget")
|
||||||
|
.arg(
|
||||||
|
Arg::with_name("service")
|
||||||
|
.value_name("SERVICE")
|
||||||
|
.required(true)
|
||||||
|
.help("service name"),
|
||||||
|
)
|
||||||
|
.about("forget a service. you can only forget a stopped service"),
|
||||||
|
)
|
||||||
|
.subcommand(
|
||||||
|
SubCommand::with_name("monitor")
|
||||||
|
.arg(
|
||||||
|
Arg::with_name("service")
|
||||||
|
.value_name("SERVICE")
|
||||||
|
.required(true)
|
||||||
|
.help("service name"),
|
||||||
|
)
|
||||||
|
.about("start monitoring a service. configuration is loaded from server config directory"),
|
||||||
|
)
|
||||||
|
.subcommand(
|
||||||
|
SubCommand::with_name("log")
|
||||||
|
.arg(
|
||||||
|
Arg::with_name("snapshot")
|
||||||
|
.short("s")
|
||||||
|
.long("snapshot")
|
||||||
|
.required(false)
|
||||||
|
.help("if set log prints current buffer without following")
|
||||||
|
)
|
||||||
|
.arg(
|
||||||
|
Arg::with_name("filter")
|
||||||
|
.value_name("FILTER")
|
||||||
|
.required(false)
|
||||||
|
.help("an optional 'exact' service name")
|
||||||
|
)
|
||||||
|
.about("view services logs from zinit ring buffer"),
|
||||||
|
)
|
||||||
|
.subcommand(
|
||||||
|
SubCommand::with_name("kill")
|
||||||
|
.arg(
|
||||||
|
Arg::with_name("service")
|
||||||
|
.value_name("SERVICE")
|
||||||
|
.required(true)
|
||||||
|
.help("service name"),
|
||||||
|
)
|
||||||
|
.arg(
|
||||||
|
Arg::with_name("signal")
|
||||||
|
.value_name("SIGNAL")
|
||||||
|
.required(true)
|
||||||
|
.default_value("SIGTERM")
|
||||||
|
.help("signal name (example: SIGTERM)"),
|
||||||
|
)
|
||||||
|
.about("send a signal to a running service."),
|
||||||
|
)
|
||||||
|
.subcommand(
|
||||||
|
SubCommand::with_name("restart")
|
||||||
|
.arg(
|
||||||
|
Arg::with_name("service")
|
||||||
|
.value_name("SERVICE")
|
||||||
|
.required(true)
|
||||||
|
.help("service name"),
|
||||||
|
)
|
||||||
|
.about("restart a service."),
|
||||||
|
)
|
||||||
|
.subcommand(
|
||||||
|
SubCommand::with_name("stats")
|
||||||
|
.arg(
|
||||||
|
Arg::with_name("service")
|
||||||
|
.value_name("SERVICE")
|
||||||
|
.required(true)
|
||||||
|
.help("service name"),
|
||||||
|
)
|
||||||
|
.about("show memory and CPU usage statistics for a service"),
|
||||||
|
)
|
||||||
|
.subcommand(
|
||||||
|
SubCommand::with_name("proxy")
|
||||||
|
.arg(
|
||||||
|
Arg::with_name("address")
|
||||||
|
.value_name("ADDRESS")
|
||||||
|
.short("a")
|
||||||
|
.long("address")
|
||||||
|
.default_value("127.0.0.1:8080")
|
||||||
|
.help("address to bind the HTTP/RPC server to"),
|
||||||
|
)
|
||||||
|
.about("start an HTTP/RPC proxy for Zinit API"),
|
||||||
|
)
|
||||||
|
.get_matches();
|
||||||
|
|
||||||
|
use dirs; // Add this import
|
||||||
|
|
||||||
|
let socket = matches.value_of("socket").unwrap();
|
||||||
|
let debug = matches.is_present("debug");
|
||||||
|
|
||||||
|
let config_path = if let Some(config_arg) = matches.value_of("config") {
|
||||||
|
config_arg.to_string()
|
||||||
|
} else {
|
||||||
|
#[cfg(target_os = "macos")]
|
||||||
|
{
|
||||||
|
let home_dir = dirs::home_dir()
|
||||||
|
.ok_or_else(|| anyhow::anyhow!("Could not determine home directory"))?;
|
||||||
|
home_dir
|
||||||
|
.join("hero")
|
||||||
|
.join("cfg")
|
||||||
|
.join("zinit")
|
||||||
|
.to_str()
|
||||||
|
.ok_or_else(|| anyhow::anyhow!("Invalid path for config directory"))?
|
||||||
|
.to_string()
|
||||||
|
}
|
||||||
|
#[cfg(not(target_os = "macos"))]
|
||||||
|
{
|
||||||
|
"/etc/zinit/".to_string()
|
||||||
|
}
|
||||||
|
};
|
||||||
|
|
||||||
|
let result = match matches.subcommand() {
|
||||||
|
("init", Some(matches)) => {
|
||||||
|
let _server = app::init(
|
||||||
|
matches.value_of("buffer").unwrap().parse().unwrap(),
|
||||||
|
&config_path, // Use the determined config_path
|
||||||
|
socket,
|
||||||
|
matches.is_present("container"),
|
||||||
|
debug,
|
||||||
|
)
|
||||||
|
.await?;
|
||||||
|
tokio::signal::ctrl_c().await?;
|
||||||
|
Ok(())
|
||||||
|
}
|
||||||
|
("list", _) => app::list(socket).await,
|
||||||
|
("shutdown", _) => app::shutdown(socket).await,
|
||||||
|
("reboot", _) => app::reboot(socket).await,
|
||||||
|
// ("log", Some(matches)) => app::log(matches.value_of("filter")),
|
||||||
|
("status", Some(matches)) => {
|
||||||
|
app::status(socket, matches.value_of("service").unwrap().to_string()).await
|
||||||
|
}
|
||||||
|
("stop", Some(matches)) => {
|
||||||
|
app::stop(socket, matches.value_of("service").unwrap().to_string()).await
|
||||||
|
}
|
||||||
|
("start", Some(matches)) => {
|
||||||
|
app::start(socket, matches.value_of("service").unwrap().to_string()).await
|
||||||
|
}
|
||||||
|
("forget", Some(matches)) => {
|
||||||
|
app::forget(socket, matches.value_of("service").unwrap().to_string()).await
|
||||||
|
}
|
||||||
|
("monitor", Some(matches)) => {
|
||||||
|
app::monitor(socket, matches.value_of("service").unwrap().to_string()).await
|
||||||
|
}
|
||||||
|
("kill", Some(matches)) => {
|
||||||
|
app::kill(
|
||||||
|
socket,
|
||||||
|
matches.value_of("service").unwrap().to_string(),
|
||||||
|
matches.value_of("signal").unwrap().to_string(),
|
||||||
|
)
|
||||||
|
.await
|
||||||
|
}
|
||||||
|
("log", Some(matches)) => {
|
||||||
|
let mut stream = app::logs(
|
||||||
|
socket,
|
||||||
|
matches.value_of("filter").map(|s| s.to_string()),
|
||||||
|
!matches.is_present("snapshot"),
|
||||||
|
)
|
||||||
|
.await?;
|
||||||
|
|
||||||
|
loop {
|
||||||
|
tokio::select! {
|
||||||
|
item = stream.next() => {
|
||||||
|
match item {
|
||||||
|
Some(log_entry) => {
|
||||||
|
println!("{}", log_entry);
|
||||||
|
},
|
||||||
|
None => break
|
||||||
|
}
|
||||||
|
}
|
||||||
|
_ = tokio::signal::ctrl_c() => {
|
||||||
|
break
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
Ok(())
|
||||||
|
}
|
||||||
|
("restart", Some(matches)) => {
|
||||||
|
app::restart(socket, matches.value_of("service").unwrap().to_string()).await
|
||||||
|
}
|
||||||
|
("stats", Some(matches)) => {
|
||||||
|
app::stats(socket, matches.value_of("service").unwrap().to_string()).await
|
||||||
|
}
|
||||||
|
("proxy", Some(matches)) => {
|
||||||
|
app::proxy(socket, matches.value_of("address").unwrap().to_string()).await
|
||||||
|
}
|
||||||
|
_ => app::list(socket).await, // default command
|
||||||
|
};
|
||||||
|
|
||||||
|
match result {
|
||||||
|
Ok(_) => Ok(()),
|
||||||
|
Err(e) => {
|
||||||
|
eprintln!("{:#}", e);
|
||||||
|
std::process::exit(1);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
149
components/zinit/src/manager/buffer.rs
Normal file
149
components/zinit/src/manager/buffer.rs
Normal file
@@ -0,0 +1,149 @@
|
|||||||
|
use anyhow::Result;
|
||||||
|
use std::sync::Arc;
|
||||||
|
use tokio::sync::broadcast;
|
||||||
|
use tokio::sync::broadcast::error::RecvError;
|
||||||
|
use tokio::sync::{mpsc, Mutex};
|
||||||
|
|
||||||
|
struct Buffer<T> {
|
||||||
|
inner: Vec<T>,
|
||||||
|
at: usize,
|
||||||
|
}
|
||||||
|
|
||||||
|
impl<T: Clone> Buffer<T> {
|
||||||
|
pub fn new(cap: usize) -> Buffer<T> {
|
||||||
|
Buffer {
|
||||||
|
inner: Vec::with_capacity(cap),
|
||||||
|
at: 0,
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
fn len(&self) -> usize {
|
||||||
|
self.inner.len()
|
||||||
|
}
|
||||||
|
|
||||||
|
pub fn cap(&self) -> usize {
|
||||||
|
self.inner.capacity()
|
||||||
|
}
|
||||||
|
|
||||||
|
pub fn push(&mut self, o: T) {
|
||||||
|
if self.len() < self.cap() {
|
||||||
|
self.inner.push(o);
|
||||||
|
} else {
|
||||||
|
self.inner[self.at] = o;
|
||||||
|
}
|
||||||
|
|
||||||
|
self.at = (self.at + 1) % self.cap();
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
impl<'a, T: 'a> IntoIterator for &'a Buffer<T> {
|
||||||
|
type IntoIter = BufferIter<'a, T>;
|
||||||
|
type Item = &'a T;
|
||||||
|
|
||||||
|
fn into_iter(self) -> Self::IntoIter {
|
||||||
|
let (second, first) = self.inner.split_at(self.at);
|
||||||
|
|
||||||
|
BufferIter {
|
||||||
|
first,
|
||||||
|
second,
|
||||||
|
index: 0,
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
pub struct BufferIter<'a, T> {
|
||||||
|
first: &'a [T],
|
||||||
|
second: &'a [T],
|
||||||
|
index: usize,
|
||||||
|
}
|
||||||
|
|
||||||
|
impl<'a, T> Iterator for BufferIter<'a, T> {
|
||||||
|
type Item = &'a T;
|
||||||
|
|
||||||
|
fn next(&mut self) -> Option<Self::Item> {
|
||||||
|
let index = self.index;
|
||||||
|
self.index += 1;
|
||||||
|
if index < self.first.len() {
|
||||||
|
Some(&self.first[index])
|
||||||
|
} else if index - self.first.len() < self.second.len() {
|
||||||
|
Some(&self.second[index - self.first.len()])
|
||||||
|
} else {
|
||||||
|
None
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
pub type Logs = mpsc::Receiver<Arc<String>>;
|
||||||
|
|
||||||
|
#[derive(Clone)]
|
||||||
|
pub struct Ring {
|
||||||
|
buffer: Arc<Mutex<Buffer<Arc<String>>>>,
|
||||||
|
sender: broadcast::Sender<Arc<String>>,
|
||||||
|
}
|
||||||
|
|
||||||
|
impl Ring {
|
||||||
|
pub fn new(cap: usize) -> Ring {
|
||||||
|
let (tx, _) = broadcast::channel(100);
|
||||||
|
Ring {
|
||||||
|
buffer: Arc::new(Mutex::new(Buffer::new(cap))),
|
||||||
|
sender: tx,
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
pub async fn push(&self, line: String) -> Result<()> {
|
||||||
|
let line = Arc::new(line.clone());
|
||||||
|
self.buffer.lock().await.push(Arc::clone(&line));
|
||||||
|
self.sender.send(line)?;
|
||||||
|
Ok(())
|
||||||
|
}
|
||||||
|
|
||||||
|
/// stream returns a continues stream that first receive
|
||||||
|
/// a snapshot of the current buffer.
|
||||||
|
/// then if follow is true the logs stream will remain
|
||||||
|
/// open and fed each received line forever until the
|
||||||
|
/// received closed the channel from its end.
|
||||||
|
pub async fn stream(&self, existing_logs: bool, follow: bool) -> Logs {
|
||||||
|
let (tx, stream) = mpsc::channel::<Arc<String>>(100);
|
||||||
|
let mut rx = self.sender.subscribe();
|
||||||
|
|
||||||
|
let buffer = if existing_logs {
|
||||||
|
// Get current exisiting logs
|
||||||
|
self.buffer
|
||||||
|
.lock()
|
||||||
|
.await
|
||||||
|
.into_iter()
|
||||||
|
.cloned()
|
||||||
|
.collect::<Vec<_>>()
|
||||||
|
} else {
|
||||||
|
// Don't care about existing logs
|
||||||
|
vec![]
|
||||||
|
};
|
||||||
|
|
||||||
|
tokio::spawn(async move {
|
||||||
|
for item in buffer {
|
||||||
|
let _ = tx.send(Arc::clone(&item)).await;
|
||||||
|
}
|
||||||
|
|
||||||
|
if !follow {
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
|
||||||
|
loop {
|
||||||
|
let line = match rx.recv().await {
|
||||||
|
Ok(line) => line,
|
||||||
|
Err(RecvError::Closed) => break,
|
||||||
|
Err(RecvError::Lagged(n)) => {
|
||||||
|
Arc::new(format!("[-] zinit: {} lines dropped", n))
|
||||||
|
}
|
||||||
|
};
|
||||||
|
|
||||||
|
if tx.send(line).await.is_err() {
|
||||||
|
// client disconnected.
|
||||||
|
break;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
});
|
||||||
|
|
||||||
|
stream
|
||||||
|
}
|
||||||
|
}
|
||||||
253
components/zinit/src/manager/mod.rs
Normal file
253
components/zinit/src/manager/mod.rs
Normal file
@@ -0,0 +1,253 @@
|
|||||||
|
use std::collections::HashMap;
|
||||||
|
|
||||||
|
use anyhow::{Context, Result};
|
||||||
|
use command_group::CommandGroup;
|
||||||
|
use nix::sys::signal;
|
||||||
|
use nix::sys::wait::{self, WaitStatus};
|
||||||
|
use nix::unistd::Pid;
|
||||||
|
use std::fs::File as StdFile;
|
||||||
|
use std::os::unix::io::FromRawFd;
|
||||||
|
use std::os::unix::io::IntoRawFd;
|
||||||
|
use std::process::Command;
|
||||||
|
use std::process::Stdio;
|
||||||
|
use std::sync::Arc;
|
||||||
|
use tokio::fs::File;
|
||||||
|
use tokio::io::AsyncBufReadExt;
|
||||||
|
use tokio::io::BufReader;
|
||||||
|
use tokio::signal::unix;
|
||||||
|
use tokio::sync::oneshot;
|
||||||
|
use tokio::sync::Mutex;
|
||||||
|
|
||||||
|
mod buffer;
|
||||||
|
pub use buffer::Logs;
|
||||||
|
|
||||||
|
pub struct Process {
|
||||||
|
cmd: String,
|
||||||
|
env: HashMap<String, String>,
|
||||||
|
cwd: String,
|
||||||
|
}
|
||||||
|
type WaitChannel = oneshot::Receiver<WaitStatus>;
|
||||||
|
|
||||||
|
pub struct Child {
|
||||||
|
pub pid: Pid,
|
||||||
|
ch: WaitChannel,
|
||||||
|
}
|
||||||
|
|
||||||
|
impl Child {
|
||||||
|
pub fn new(pid: Pid, ch: WaitChannel) -> Child {
|
||||||
|
Child { pid, ch }
|
||||||
|
}
|
||||||
|
|
||||||
|
pub async fn wait(self) -> Result<WaitStatus> {
|
||||||
|
Ok(self.ch.await?)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
type Handler = oneshot::Sender<WaitStatus>;
|
||||||
|
|
||||||
|
impl Process {
|
||||||
|
pub fn new<S: Into<String>>(cmd: S, cwd: S, env: Option<HashMap<String, String>>) -> Process {
|
||||||
|
let env = env.unwrap_or_default();
|
||||||
|
|
||||||
|
Process {
|
||||||
|
env,
|
||||||
|
cmd: cmd.into(),
|
||||||
|
cwd: cwd.into(),
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
#[derive(Clone)]
|
||||||
|
pub enum Log {
|
||||||
|
None,
|
||||||
|
Stdout,
|
||||||
|
Ring(String),
|
||||||
|
}
|
||||||
|
|
||||||
|
#[derive(Clone)]
|
||||||
|
pub struct ProcessManager {
|
||||||
|
table: Arc<Mutex<HashMap<Pid, Handler>>>,
|
||||||
|
ring: buffer::Ring,
|
||||||
|
env: Environ,
|
||||||
|
}
|
||||||
|
|
||||||
|
impl ProcessManager {
|
||||||
|
pub fn new(cap: usize) -> ProcessManager {
|
||||||
|
ProcessManager {
|
||||||
|
table: Arc::new(Mutex::new(HashMap::new())),
|
||||||
|
ring: buffer::Ring::new(cap),
|
||||||
|
env: Environ::new(),
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
fn wait_process() -> Vec<WaitStatus> {
|
||||||
|
let mut statuses: Vec<WaitStatus> = Vec::new();
|
||||||
|
loop {
|
||||||
|
let status = match wait::waitpid(Option::None, Some(wait::WaitPidFlag::WNOHANG)) {
|
||||||
|
Ok(status) => status,
|
||||||
|
Err(_) => {
|
||||||
|
return statuses;
|
||||||
|
}
|
||||||
|
};
|
||||||
|
match status {
|
||||||
|
WaitStatus::StillAlive => break,
|
||||||
|
_ => statuses.push(status),
|
||||||
|
}
|
||||||
|
}
|
||||||
|
statuses
|
||||||
|
}
|
||||||
|
|
||||||
|
pub fn start(&self) {
|
||||||
|
let table = Arc::clone(&self.table);
|
||||||
|
let mut signals = match unix::signal(unix::SignalKind::child()) {
|
||||||
|
Ok(s) => s,
|
||||||
|
Err(err) => {
|
||||||
|
panic!("failed to bind to signals: {}", err);
|
||||||
|
}
|
||||||
|
};
|
||||||
|
|
||||||
|
tokio::spawn(async move {
|
||||||
|
loop {
|
||||||
|
signals.recv().await;
|
||||||
|
let mut table = table.lock().await;
|
||||||
|
for exited in Self::wait_process() {
|
||||||
|
if let Some(pid) = exited.pid() {
|
||||||
|
if let Some(sender) = table.remove(&pid) {
|
||||||
|
if sender.send(exited).is_err() {
|
||||||
|
debug!("failed to send exit state to process: {}", pid);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
});
|
||||||
|
}
|
||||||
|
|
||||||
|
fn sink(&self, file: File, prefix: String) {
|
||||||
|
let ring = self.ring.clone();
|
||||||
|
let reader = BufReader::new(file);
|
||||||
|
|
||||||
|
tokio::spawn(async move {
|
||||||
|
let mut lines = reader.lines();
|
||||||
|
while let Ok(line) = lines.next_line().await {
|
||||||
|
let _ = match line {
|
||||||
|
Some(line) => ring.push(format!("{}: {}", prefix, line)).await,
|
||||||
|
None => break,
|
||||||
|
};
|
||||||
|
}
|
||||||
|
});
|
||||||
|
}
|
||||||
|
|
||||||
|
pub async fn stream(&self, existing_logs: bool, follow: bool) -> Logs {
|
||||||
|
self.ring.stream(existing_logs, follow).await
|
||||||
|
}
|
||||||
|
|
||||||
|
pub fn signal(&self, pid: Pid, sig: signal::Signal) -> Result<()> {
|
||||||
|
Ok(signal::killpg(pid, sig)?)
|
||||||
|
}
|
||||||
|
|
||||||
|
pub async fn run(&self, cmd: Process, log: Log) -> Result<Child> {
|
||||||
|
let args = shlex::split(&cmd.cmd).context("failed to parse command")?;
|
||||||
|
if args.is_empty() {
|
||||||
|
bail!("invalid command");
|
||||||
|
}
|
||||||
|
|
||||||
|
let mut child = Command::new(&args[0]);
|
||||||
|
|
||||||
|
let child = if !cmd.cwd.is_empty() {
|
||||||
|
child.current_dir(&cmd.cwd)
|
||||||
|
} else {
|
||||||
|
child.current_dir("/")
|
||||||
|
};
|
||||||
|
|
||||||
|
let child = child.args(&args[1..]).envs(&self.env.0).envs(cmd.env);
|
||||||
|
|
||||||
|
let child = match log {
|
||||||
|
Log::None => child.stdout(Stdio::null()).stderr(Stdio::null()),
|
||||||
|
Log::Ring(_) => child.stdout(Stdio::piped()).stderr(Stdio::piped()),
|
||||||
|
_ => child, // default to inherit
|
||||||
|
};
|
||||||
|
|
||||||
|
let mut table = self.table.lock().await;
|
||||||
|
|
||||||
|
let mut child = child
|
||||||
|
.group_spawn()
|
||||||
|
.context("failed to spawn command")?
|
||||||
|
.into_inner();
|
||||||
|
|
||||||
|
if let Log::Ring(prefix) = log {
|
||||||
|
let _ = self
|
||||||
|
.ring
|
||||||
|
.push(format!("[-] {}: ------------ [start] ------------", prefix))
|
||||||
|
.await;
|
||||||
|
|
||||||
|
if let Some(out) = child.stdout.take() {
|
||||||
|
let out = File::from_std(unsafe { StdFile::from_raw_fd(out.into_raw_fd()) });
|
||||||
|
self.sink(out, format!("[+] {}", prefix))
|
||||||
|
}
|
||||||
|
|
||||||
|
if let Some(out) = child.stderr.take() {
|
||||||
|
let out = File::from_std(unsafe { StdFile::from_raw_fd(out.into_raw_fd()) });
|
||||||
|
self.sink(out, format!("[-] {}", prefix))
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
let (tx, rx) = oneshot::channel();
|
||||||
|
|
||||||
|
let id = child.id();
|
||||||
|
|
||||||
|
let pid = Pid::from_raw(id as i32);
|
||||||
|
table.insert(pid, tx);
|
||||||
|
|
||||||
|
Ok(Child::new(pid, rx))
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
#[derive(Clone)]
|
||||||
|
struct Environ(HashMap<String, String>);
|
||||||
|
|
||||||
|
impl Environ {
|
||||||
|
fn new() -> Environ {
|
||||||
|
let env = match Environ::parse("/etc/environment") {
|
||||||
|
Ok(r) => r,
|
||||||
|
Err(err) => {
|
||||||
|
error!("failed to load /etc/environment file: {}", err);
|
||||||
|
HashMap::new()
|
||||||
|
}
|
||||||
|
};
|
||||||
|
|
||||||
|
Environ(env)
|
||||||
|
}
|
||||||
|
|
||||||
|
fn parse<P>(p: P) -> Result<HashMap<String, String>, std::io::Error>
|
||||||
|
where
|
||||||
|
P: AsRef<std::path::Path>,
|
||||||
|
{
|
||||||
|
let mut m = HashMap::new();
|
||||||
|
let txt = match std::fs::read_to_string(p) {
|
||||||
|
Ok(txt) => txt,
|
||||||
|
Err(err) if err.kind() == std::io::ErrorKind::NotFound => {
|
||||||
|
info!("skipping /etc/environment file because it does not exist");
|
||||||
|
"".into()
|
||||||
|
}
|
||||||
|
Err(err) => return Err(err),
|
||||||
|
};
|
||||||
|
|
||||||
|
for line in txt.lines() {
|
||||||
|
let line = line.trim();
|
||||||
|
if line.starts_with('#') {
|
||||||
|
continue;
|
||||||
|
}
|
||||||
|
let parts: Vec<&str> = line.splitn(2, '=').collect();
|
||||||
|
let key = String::from(parts[0]);
|
||||||
|
let value = match parts.len() {
|
||||||
|
2 => String::from(parts[1]),
|
||||||
|
_ => String::default(),
|
||||||
|
};
|
||||||
|
//m.into_iter()
|
||||||
|
m.insert(key, value);
|
||||||
|
}
|
||||||
|
|
||||||
|
Ok(m)
|
||||||
|
}
|
||||||
|
}
|
||||||
264
components/zinit/src/testapp/main.rs
Normal file
264
components/zinit/src/testapp/main.rs
Normal file
@@ -0,0 +1,264 @@
|
|||||||
|
use anyhow::{Context, Result};
|
||||||
|
use std::path::Path;
|
||||||
|
use tokio::time::{sleep, Duration};
|
||||||
|
use std::env;
|
||||||
|
use tokio::process::Command;
|
||||||
|
use tokio::fs;
|
||||||
|
use std::process::Stdio;
|
||||||
|
use serde::{Deserialize, Serialize};
|
||||||
|
use serde_json;
|
||||||
|
use tokio::net::UnixStream;
|
||||||
|
use tokio::io::{AsyncBufReadExt, AsyncReadExt, AsyncWriteExt, BufStream};
|
||||||
|
use std::collections::HashMap;
|
||||||
|
|
||||||
|
#[derive(Clone, Debug, Deserialize, Serialize)]
|
||||||
|
#[serde(rename_all = "lowercase")]
|
||||||
|
struct Response {
|
||||||
|
pub state: State,
|
||||||
|
pub body: serde_json::Value,
|
||||||
|
}
|
||||||
|
|
||||||
|
#[derive(Clone, Debug, Deserialize, Serialize)]
|
||||||
|
#[serde(rename_all = "lowercase")]
|
||||||
|
enum State {
|
||||||
|
Ok,
|
||||||
|
Error,
|
||||||
|
}
|
||||||
|
|
||||||
|
#[derive(Clone, Debug, Deserialize, Serialize)]
|
||||||
|
#[serde(rename_all = "lowercase")]
|
||||||
|
struct Status {
|
||||||
|
pub name: String,
|
||||||
|
pub pid: u32,
|
||||||
|
pub state: String,
|
||||||
|
pub target: String,
|
||||||
|
pub after: HashMap<String, String>,
|
||||||
|
}
|
||||||
|
|
||||||
|
struct Client {
|
||||||
|
socket: String,
|
||||||
|
}
|
||||||
|
|
||||||
|
impl Client {
|
||||||
|
pub fn new(socket: &str) -> Client {
|
||||||
|
Client {
|
||||||
|
socket: socket.to_string(),
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
async fn connect(&self) -> Result<UnixStream> {
|
||||||
|
UnixStream::connect(&self.socket).await.with_context(|| {
|
||||||
|
format!(
|
||||||
|
"failed to connect to '{}'. is zinit listening on that socket?",
|
||||||
|
self.socket
|
||||||
|
)
|
||||||
|
})
|
||||||
|
}
|
||||||
|
|
||||||
|
async fn command(&self, c: &str) -> Result<serde_json::Value> {
|
||||||
|
let mut con = BufStream::new(self.connect().await?);
|
||||||
|
|
||||||
|
let _ = con.write(c.as_bytes()).await?;
|
||||||
|
let _ = con.write(b"\n").await?;
|
||||||
|
con.flush().await?;
|
||||||
|
|
||||||
|
let mut data = String::new();
|
||||||
|
con.read_to_string(&mut data).await?;
|
||||||
|
|
||||||
|
let response: Response = serde_json::from_str(&data)?;
|
||||||
|
|
||||||
|
match response.state {
|
||||||
|
State::Ok => Ok(response.body),
|
||||||
|
State::Error => {
|
||||||
|
let err: String = serde_json::from_value(response.body)?;
|
||||||
|
anyhow::bail!(err)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
pub async fn list(&self) -> Result<HashMap<String, String>> {
|
||||||
|
let response = self.command("list").await?;
|
||||||
|
Ok(serde_json::from_value(response)?)
|
||||||
|
}
|
||||||
|
|
||||||
|
pub async fn status<S: AsRef<str>>(&self, name: S) -> Result<Status> {
|
||||||
|
let response = self.command(&format!("status {}", name.as_ref())).await?;
|
||||||
|
Ok(serde_json::from_value(response)?)
|
||||||
|
}
|
||||||
|
|
||||||
|
pub async fn start<S: AsRef<str>>(&self, name: S) -> Result<()> {
|
||||||
|
self.command(&format!("start {}", name.as_ref())).await?;
|
||||||
|
Ok(())
|
||||||
|
}
|
||||||
|
|
||||||
|
pub async fn stop<S: AsRef<str>>(&self, name: S) -> Result<()> {
|
||||||
|
self.command(&format!("stop {}", name.as_ref())).await?;
|
||||||
|
Ok(())
|
||||||
|
}
|
||||||
|
|
||||||
|
pub async fn forget<S: AsRef<str>>(&self, name: S) -> Result<()> {
|
||||||
|
self.command(&format!("forget {}", name.as_ref())).await?;
|
||||||
|
Ok(())
|
||||||
|
}
|
||||||
|
|
||||||
|
pub async fn monitor<S: AsRef<str>>(&self, name: S) -> Result<()> {
|
||||||
|
self.command(&format!("monitor {}", name.as_ref())).await?;
|
||||||
|
Ok(())
|
||||||
|
}
|
||||||
|
|
||||||
|
pub async fn kill<S: AsRef<str>>(&self, name: S, sig: S) -> Result<()> {
|
||||||
|
self.command(&format!("kill {} {}", name.as_ref(), sig.as_ref()))
|
||||||
|
.await?;
|
||||||
|
Ok(())
|
||||||
|
}
|
||||||
|
|
||||||
|
pub async fn shutdown(&self) -> Result<()> {
|
||||||
|
self.command("shutdown").await?;
|
||||||
|
Ok(())
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
async fn start_zinit(socket_path: &str, config_dir: &str) -> Result<()> {
|
||||||
|
// Create a temporary config directory if it doesn't exist
|
||||||
|
let config_path = Path::new(config_dir);
|
||||||
|
if !config_path.exists() {
|
||||||
|
fs::create_dir_all(config_path).await?;
|
||||||
|
}
|
||||||
|
|
||||||
|
// Start zinit in the background
|
||||||
|
let mut cmd = Command::new("zinit");
|
||||||
|
cmd.arg("--socket")
|
||||||
|
.arg(socket_path)
|
||||||
|
.arg("init")
|
||||||
|
.arg("--config")
|
||||||
|
.arg(config_dir)
|
||||||
|
.stdout(Stdio::piped())
|
||||||
|
.stderr(Stdio::piped());
|
||||||
|
|
||||||
|
let child = cmd.spawn()?;
|
||||||
|
|
||||||
|
// Give zinit some time to start up
|
||||||
|
sleep(Duration::from_secs(1)).await;
|
||||||
|
|
||||||
|
println!("Zinit started with PID: {:?}", child.id());
|
||||||
|
|
||||||
|
Ok(())
|
||||||
|
}
|
||||||
|
|
||||||
|
async fn create_service_config(config_dir: &str, name: &str, command: &str) -> Result<()> {
|
||||||
|
let config_path = format!("{}/{}.yaml", config_dir, name);
|
||||||
|
let config_content = format!(
|
||||||
|
r#"exec: {}
|
||||||
|
oneshot: false
|
||||||
|
shutdown_timeout: 10
|
||||||
|
after: []
|
||||||
|
signal:
|
||||||
|
stop: sigterm
|
||||||
|
log: ring
|
||||||
|
env: {{}}
|
||||||
|
dir: /
|
||||||
|
"#,
|
||||||
|
command
|
||||||
|
);
|
||||||
|
|
||||||
|
fs::write(config_path, config_content).await?;
|
||||||
|
Ok(())
|
||||||
|
}
|
||||||
|
|
||||||
|
#[tokio::main]
|
||||||
|
async fn main() -> Result<()> {
|
||||||
|
// Define paths for socket and config
|
||||||
|
let temp_dir = env::temp_dir();
|
||||||
|
let socket_path = temp_dir.join("zinit-test.sock").to_str().unwrap().to_string();
|
||||||
|
let config_dir = temp_dir.join("zinit-test-config").to_str().unwrap().to_string();
|
||||||
|
|
||||||
|
println!("Starting zinit with socket at: {}", socket_path);
|
||||||
|
println!("Using config directory: {}", config_dir);
|
||||||
|
|
||||||
|
// Start zinit in the background
|
||||||
|
start_zinit(&socket_path, &config_dir).await?;
|
||||||
|
|
||||||
|
// Wait for zinit to initialize
|
||||||
|
sleep(Duration::from_secs(2)).await;
|
||||||
|
|
||||||
|
// Create a client to communicate with zinit
|
||||||
|
let client = Client::new(&socket_path);
|
||||||
|
|
||||||
|
// Create service configurations
|
||||||
|
println!("Creating service configurations...");
|
||||||
|
|
||||||
|
// Create a find service
|
||||||
|
create_service_config(&config_dir, "find-service", "find / -name \"*.txt\" -type f").await?;
|
||||||
|
|
||||||
|
// Create a sleep service with echo
|
||||||
|
create_service_config(
|
||||||
|
&config_dir,
|
||||||
|
"sleep-service",
|
||||||
|
"sh -c 'echo Starting sleep; sleep 30; echo Finished sleep'"
|
||||||
|
).await?;
|
||||||
|
|
||||||
|
// Wait for zinit to load the configurations
|
||||||
|
sleep(Duration::from_secs(1)).await;
|
||||||
|
|
||||||
|
// Tell zinit to monitor our services
|
||||||
|
println!("Monitoring services...");
|
||||||
|
client.monitor("find-service").await?;
|
||||||
|
client.monitor("sleep-service").await?;
|
||||||
|
|
||||||
|
// List all services
|
||||||
|
println!("\nListing all services:");
|
||||||
|
let services = client.list().await?;
|
||||||
|
for (name, status) in services {
|
||||||
|
println!("Service: {} - Status: {}", name, status);
|
||||||
|
}
|
||||||
|
|
||||||
|
// Start the find service
|
||||||
|
println!("\nStarting find-service...");
|
||||||
|
client.start("find-service").await?;
|
||||||
|
|
||||||
|
// Wait a bit and check status
|
||||||
|
sleep(Duration::from_secs(2)).await;
|
||||||
|
let status = client.status("find-service").await?;
|
||||||
|
println!("find-service status: {:?}", status);
|
||||||
|
|
||||||
|
// Start the sleep service
|
||||||
|
println!("\nStarting sleep-service...");
|
||||||
|
client.start("sleep-service").await?;
|
||||||
|
|
||||||
|
// Wait a bit and check status
|
||||||
|
sleep(Duration::from_secs(2)).await;
|
||||||
|
let status = client.status("sleep-service").await?;
|
||||||
|
println!("sleep-service status: {:?}", status);
|
||||||
|
|
||||||
|
// Stop the find service
|
||||||
|
println!("\nStopping find-service...");
|
||||||
|
client.stop("find-service").await?;
|
||||||
|
|
||||||
|
// Wait a bit and check status
|
||||||
|
sleep(Duration::from_secs(2)).await;
|
||||||
|
let status = client.status("find-service").await?;
|
||||||
|
println!("find-service status after stopping: {:?}", status);
|
||||||
|
|
||||||
|
// Kill the sleep service with SIGTERM
|
||||||
|
println!("\nKilling sleep-service with SIGTERM...");
|
||||||
|
client.kill("sleep-service", "SIGTERM").await?;
|
||||||
|
|
||||||
|
// Wait a bit and check status
|
||||||
|
sleep(Duration::from_secs(2)).await;
|
||||||
|
let status = client.status("sleep-service").await?;
|
||||||
|
println!("sleep-service status after killing: {:?}", status);
|
||||||
|
|
||||||
|
// Cleanup - forget services
|
||||||
|
println!("\nForgetting services...");
|
||||||
|
if status.pid == 0 { // Only forget if it's not running
|
||||||
|
client.forget("sleep-service").await?;
|
||||||
|
}
|
||||||
|
client.forget("find-service").await?;
|
||||||
|
|
||||||
|
// Shutdown zinit
|
||||||
|
println!("\nShutting down zinit...");
|
||||||
|
client.shutdown().await?;
|
||||||
|
|
||||||
|
println!("\nTest completed successfully!");
|
||||||
|
Ok(())
|
||||||
|
}
|
||||||
57
components/zinit/src/testapp/mod.rs
Normal file
57
components/zinit/src/testapp/mod.rs
Normal file
@@ -0,0 +1,57 @@
|
|||||||
|
use anyhow::Result;
|
||||||
|
use std::env;
|
||||||
|
use std::path::Path;
|
||||||
|
use std::process::Stdio;
|
||||||
|
use tokio::process::Command;
|
||||||
|
use tokio::time::{sleep, Duration};
|
||||||
|
|
||||||
|
pub async fn start_zinit(socket_path: &str, config_dir: &str) -> Result<()> {
|
||||||
|
// Create a temporary config directory if it doesn't exist
|
||||||
|
let config_path = Path::new(config_dir);
|
||||||
|
if !config_path.exists() {
|
||||||
|
tokio::fs::create_dir_all(config_path).await?;
|
||||||
|
}
|
||||||
|
|
||||||
|
// Get the path to the zinit binary (use the one we just built)
|
||||||
|
let zinit_path = env::current_dir()?.join("target/debug/zinit");
|
||||||
|
println!("Using zinit binary at: {}", zinit_path.display());
|
||||||
|
|
||||||
|
// Start zinit in the background
|
||||||
|
let mut cmd = Command::new(zinit_path);
|
||||||
|
cmd.arg("--socket")
|
||||||
|
.arg(socket_path)
|
||||||
|
.arg("init")
|
||||||
|
.arg("--config")
|
||||||
|
.arg(config_dir)
|
||||||
|
.stdout(Stdio::piped())
|
||||||
|
.stderr(Stdio::piped());
|
||||||
|
|
||||||
|
let child = cmd.spawn()?;
|
||||||
|
|
||||||
|
// Give zinit some time to start up
|
||||||
|
sleep(Duration::from_secs(1)).await;
|
||||||
|
|
||||||
|
println!("Zinit started with PID: {:?}", child.id());
|
||||||
|
|
||||||
|
Ok(())
|
||||||
|
}
|
||||||
|
|
||||||
|
pub async fn create_service_config(config_dir: &str, name: &str, command: &str) -> Result<()> {
|
||||||
|
let config_path = format!("{}/{}.yaml", config_dir, name);
|
||||||
|
let config_content = format!(
|
||||||
|
r#"exec: {}
|
||||||
|
oneshot: false
|
||||||
|
shutdown_timeout: 10
|
||||||
|
after: []
|
||||||
|
signal:
|
||||||
|
stop: sigterm
|
||||||
|
log: ring
|
||||||
|
env: {{}}
|
||||||
|
dir: /
|
||||||
|
"#,
|
||||||
|
command
|
||||||
|
);
|
||||||
|
|
||||||
|
tokio::fs::write(config_path, config_content).await?;
|
||||||
|
Ok(())
|
||||||
|
}
|
||||||
119
components/zinit/src/zinit/config.rs
Normal file
119
components/zinit/src/zinit/config.rs
Normal file
@@ -0,0 +1,119 @@
|
|||||||
|
use anyhow::Result;
|
||||||
|
use serde::{Deserialize, Serialize};
|
||||||
|
use serde_yaml as yaml;
|
||||||
|
use std::collections::HashMap;
|
||||||
|
use std::ffi::OsStr;
|
||||||
|
use std::fs::{self, File};
|
||||||
|
use std::path::Path;
|
||||||
|
pub type Services = HashMap<String, Service>;
|
||||||
|
|
||||||
|
pub const DEFAULT_SHUTDOWN_TIMEOUT: u64 = 10; // in seconds
|
||||||
|
|
||||||
|
#[derive(Clone, Debug, Serialize, Deserialize)]
|
||||||
|
#[serde(default)]
|
||||||
|
pub struct Signal {
|
||||||
|
pub stop: String,
|
||||||
|
}
|
||||||
|
|
||||||
|
impl Default for Signal {
|
||||||
|
fn default() -> Self {
|
||||||
|
Signal {
|
||||||
|
stop: String::from("sigterm"),
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
#[derive(Default, Clone, Debug, Deserialize)]
|
||||||
|
#[serde(rename_all = "lowercase")]
|
||||||
|
pub enum Log {
|
||||||
|
None,
|
||||||
|
#[default]
|
||||||
|
Ring,
|
||||||
|
Stdout,
|
||||||
|
}
|
||||||
|
|
||||||
|
fn default_shutdown_timeout_fn() -> u64 {
|
||||||
|
DEFAULT_SHUTDOWN_TIMEOUT
|
||||||
|
}
|
||||||
|
#[derive(Clone, Debug, Default, Deserialize)]
|
||||||
|
#[serde(default)]
|
||||||
|
pub struct Service {
|
||||||
|
/// command to run
|
||||||
|
pub exec: String,
|
||||||
|
/// test command (optional)
|
||||||
|
#[serde(default)]
|
||||||
|
pub test: String,
|
||||||
|
#[serde(rename = "oneshot")]
|
||||||
|
pub one_shot: bool,
|
||||||
|
#[serde(default = "default_shutdown_timeout_fn")]
|
||||||
|
pub shutdown_timeout: u64,
|
||||||
|
pub after: Vec<String>,
|
||||||
|
pub signal: Signal,
|
||||||
|
pub log: Log,
|
||||||
|
pub env: HashMap<String, String>,
|
||||||
|
pub dir: String,
|
||||||
|
}
|
||||||
|
|
||||||
|
impl Service {
|
||||||
|
pub fn validate(&self) -> Result<()> {
|
||||||
|
use nix::sys::signal::Signal;
|
||||||
|
use std::str::FromStr;
|
||||||
|
if self.exec.is_empty() {
|
||||||
|
bail!("missing exec directive");
|
||||||
|
}
|
||||||
|
|
||||||
|
Signal::from_str(&self.signal.stop.to_uppercase())?;
|
||||||
|
|
||||||
|
Ok(())
|
||||||
|
}
|
||||||
|
}
|
||||||
|
/// load loads a single file
|
||||||
|
pub fn load<T: AsRef<Path>>(t: T) -> Result<(String, Service)> {
|
||||||
|
let p = t.as_ref();
|
||||||
|
//todo: can't find a way to shorten this down.
|
||||||
|
let name = match p.file_stem() {
|
||||||
|
Some(name) => match name.to_str() {
|
||||||
|
Some(name) => name,
|
||||||
|
None => bail!("invalid file name: {}", p.to_str().unwrap()),
|
||||||
|
},
|
||||||
|
None => bail!("invalid file name: {}", p.to_str().unwrap()),
|
||||||
|
};
|
||||||
|
|
||||||
|
let file = File::open(p)?;
|
||||||
|
let service: Service = yaml::from_reader(&file)?;
|
||||||
|
service.validate()?;
|
||||||
|
Ok((String::from(name), service))
|
||||||
|
}
|
||||||
|
|
||||||
|
/// walks over a directory and load all configuration files.
|
||||||
|
/// the callback is called with any error that is encountered on loading
|
||||||
|
/// a file, the callback can decide to either ignore the file, or stop
|
||||||
|
/// the directory walking
|
||||||
|
pub fn load_dir<T: AsRef<Path>>(p: T) -> Result<Services> {
|
||||||
|
let mut services: Services = HashMap::new();
|
||||||
|
|
||||||
|
for entry in fs::read_dir(p)? {
|
||||||
|
let entry = entry?;
|
||||||
|
if !entry.file_type()?.is_file() {
|
||||||
|
continue;
|
||||||
|
}
|
||||||
|
|
||||||
|
let fp = entry.path();
|
||||||
|
|
||||||
|
if !matches!(fp.extension(), Some(ext) if ext == OsStr::new("yaml")) {
|
||||||
|
continue;
|
||||||
|
}
|
||||||
|
|
||||||
|
let (name, service) = match load(&fp) {
|
||||||
|
Ok(content) => content,
|
||||||
|
Err(err) => {
|
||||||
|
error!("failed to load config file {:?}: {}", fp, err);
|
||||||
|
continue;
|
||||||
|
}
|
||||||
|
};
|
||||||
|
|
||||||
|
services.insert(name, service);
|
||||||
|
}
|
||||||
|
|
||||||
|
Ok(services)
|
||||||
|
}
|
||||||
80
components/zinit/src/zinit/errors.rs
Normal file
80
components/zinit/src/zinit/errors.rs
Normal file
@@ -0,0 +1,80 @@
|
|||||||
|
use thiserror::Error;
|
||||||
|
|
||||||
|
/// Errors that can occur in the zinit module
|
||||||
|
#[derive(Error, Debug)]
|
||||||
|
pub enum ZInitError {
|
||||||
|
/// Service name is unknown
|
||||||
|
#[error("service name {name:?} unknown")]
|
||||||
|
UnknownService { name: String },
|
||||||
|
|
||||||
|
/// Service is already being monitored
|
||||||
|
#[error("service {name:?} already monitored")]
|
||||||
|
ServiceAlreadyMonitored { name: String },
|
||||||
|
|
||||||
|
/// Service is up and running
|
||||||
|
#[error("service {name:?} is up")]
|
||||||
|
ServiceIsUp { name: String },
|
||||||
|
|
||||||
|
/// Service is down and not running
|
||||||
|
#[error("service {name:?} is down")]
|
||||||
|
ServiceIsDown { name: String },
|
||||||
|
|
||||||
|
/// Zinit is shutting down
|
||||||
|
#[error("zinit is shutting down")]
|
||||||
|
ShuttingDown,
|
||||||
|
|
||||||
|
/// Invalid state transition
|
||||||
|
#[error("service state transition error: {message}")]
|
||||||
|
InvalidStateTransition { message: String },
|
||||||
|
|
||||||
|
/// Dependency error
|
||||||
|
#[error("dependency error: {message}")]
|
||||||
|
DependencyError { message: String },
|
||||||
|
|
||||||
|
/// Process error
|
||||||
|
#[error("process error: {message}")]
|
||||||
|
ProcessError { message: String },
|
||||||
|
}
|
||||||
|
|
||||||
|
impl ZInitError {
|
||||||
|
/// Create a new UnknownService error
|
||||||
|
pub fn unknown_service<S: Into<String>>(name: S) -> Self {
|
||||||
|
ZInitError::UnknownService { name: name.into() }
|
||||||
|
}
|
||||||
|
|
||||||
|
/// Create a new ServiceAlreadyMonitored error
|
||||||
|
pub fn service_already_monitored<S: Into<String>>(name: S) -> Self {
|
||||||
|
ZInitError::ServiceAlreadyMonitored { name: name.into() }
|
||||||
|
}
|
||||||
|
|
||||||
|
/// Create a new ServiceIsUp error
|
||||||
|
pub fn service_is_up<S: Into<String>>(name: S) -> Self {
|
||||||
|
ZInitError::ServiceIsUp { name: name.into() }
|
||||||
|
}
|
||||||
|
|
||||||
|
/// Create a new ServiceIsDown error
|
||||||
|
pub fn service_is_down<S: Into<String>>(name: S) -> Self {
|
||||||
|
ZInitError::ServiceIsDown { name: name.into() }
|
||||||
|
}
|
||||||
|
|
||||||
|
/// Create a new InvalidStateTransition error
|
||||||
|
pub fn invalid_state_transition<S: Into<String>>(message: S) -> Self {
|
||||||
|
ZInitError::InvalidStateTransition {
|
||||||
|
message: message.into(),
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
/// Create a new DependencyError error
|
||||||
|
pub fn dependency_error<S: Into<String>>(message: S) -> Self {
|
||||||
|
ZInitError::DependencyError {
|
||||||
|
message: message.into(),
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
/// Create a new ProcessError error
|
||||||
|
pub fn process_error<S: Into<String>>(message: S) -> Self {
|
||||||
|
ZInitError::ProcessError {
|
||||||
|
message: message.into(),
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
970
components/zinit/src/zinit/lifecycle.rs
Normal file
970
components/zinit/src/zinit/lifecycle.rs
Normal file
@@ -0,0 +1,970 @@
|
|||||||
|
use crate::manager::{Log, Logs, Process, ProcessManager};
|
||||||
|
use crate::zinit::config;
|
||||||
|
use crate::zinit::errors::ZInitError;
|
||||||
|
#[cfg(target_os = "linux")]
|
||||||
|
use crate::zinit::ord::{service_dependency_order, ProcessDAG, DUMMY_ROOT};
|
||||||
|
use crate::zinit::service::ZInitService;
|
||||||
|
use crate::zinit::state::{State, Target};
|
||||||
|
#[cfg(target_os = "linux")]
|
||||||
|
use crate::zinit::types::Watcher;
|
||||||
|
use crate::zinit::types::{ProcessStats, ServiceStats, ServiceTable};
|
||||||
|
use std::collections::HashMap;
|
||||||
|
use sysinfo::{self, PidExt, ProcessExt, System, SystemExt};
|
||||||
|
|
||||||
|
// Define a local extension trait for WaitStatus
|
||||||
|
trait WaitStatusExt {
|
||||||
|
fn success(&self) -> bool;
|
||||||
|
}
|
||||||
|
|
||||||
|
impl WaitStatusExt for WaitStatus {
|
||||||
|
fn success(&self) -> bool {
|
||||||
|
matches!(self, WaitStatus::Exited(_, 0))
|
||||||
|
}
|
||||||
|
}
|
||||||
|
use anyhow::Result;
|
||||||
|
#[cfg(target_os = "linux")]
|
||||||
|
use nix::sys::reboot::RebootMode;
|
||||||
|
use nix::sys::signal;
|
||||||
|
use nix::sys::wait::WaitStatus;
|
||||||
|
use nix::unistd::Pid;
|
||||||
|
use std::str::FromStr;
|
||||||
|
use std::sync::Arc;
|
||||||
|
#[cfg(target_os = "linux")]
|
||||||
|
use tokio::sync::mpsc;
|
||||||
|
use tokio::sync::{Notify, RwLock};
|
||||||
|
use tokio::time::sleep;
|
||||||
|
#[cfg(target_os = "linux")]
|
||||||
|
use tokio::time::timeout;
|
||||||
|
#[cfg(target_os = "linux")]
|
||||||
|
use tokio_stream::StreamExt;
|
||||||
|
|
||||||
|
/// Manages the lifecycle of services
|
||||||
|
#[derive(Clone)]
|
||||||
|
pub struct LifecycleManager {
|
||||||
|
/// Process manager for spawning and managing processes
|
||||||
|
pm: ProcessManager,
|
||||||
|
|
||||||
|
/// Table of services
|
||||||
|
services: Arc<RwLock<ServiceTable>>,
|
||||||
|
|
||||||
|
/// Notification for service state changes
|
||||||
|
notify: Arc<Notify>,
|
||||||
|
|
||||||
|
/// Whether the system is shutting down
|
||||||
|
shutdown: Arc<RwLock<bool>>,
|
||||||
|
|
||||||
|
/// Whether running in container mode
|
||||||
|
container: bool,
|
||||||
|
}
|
||||||
|
|
||||||
|
impl LifecycleManager {
|
||||||
|
/// Create a new lifecycle manager
|
||||||
|
pub fn new(
|
||||||
|
pm: ProcessManager,
|
||||||
|
services: Arc<RwLock<ServiceTable>>,
|
||||||
|
notify: Arc<Notify>,
|
||||||
|
shutdown: Arc<RwLock<bool>>,
|
||||||
|
container: bool,
|
||||||
|
) -> Self {
|
||||||
|
Self {
|
||||||
|
pm,
|
||||||
|
services,
|
||||||
|
notify,
|
||||||
|
shutdown,
|
||||||
|
container,
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
/// Get a reference to the process manager
|
||||||
|
pub fn process_manager(&self) -> &ProcessManager {
|
||||||
|
&self.pm
|
||||||
|
}
|
||||||
|
|
||||||
|
/// Check if running in container mode
|
||||||
|
pub fn is_container_mode(&self) -> bool {
|
||||||
|
self.container
|
||||||
|
}
|
||||||
|
|
||||||
|
/// Get logs from the process manager
|
||||||
|
pub async fn logs(&self, existing_logs: bool, follow: bool) -> Logs {
|
||||||
|
self.pm.stream(existing_logs, follow).await
|
||||||
|
}
|
||||||
|
|
||||||
|
/// Monitor a service
|
||||||
|
pub async fn monitor<S: Into<String>>(&self, name: S, service: config::Service) -> Result<()> {
|
||||||
|
if *self.shutdown.read().await {
|
||||||
|
return Err(ZInitError::ShuttingDown.into());
|
||||||
|
}
|
||||||
|
|
||||||
|
let name = name.into();
|
||||||
|
let mut services = self.services.write().await;
|
||||||
|
|
||||||
|
if services.contains_key(&name) {
|
||||||
|
return Err(ZInitError::service_already_monitored(name).into());
|
||||||
|
}
|
||||||
|
|
||||||
|
let service = Arc::new(RwLock::new(ZInitService::new(service, State::Unknown)));
|
||||||
|
services.insert(name.clone(), Arc::clone(&service));
|
||||||
|
|
||||||
|
let lifecycle = self.clone_lifecycle();
|
||||||
|
debug!("service '{}' monitored", name);
|
||||||
|
tokio::spawn(lifecycle.watch_service(name, service));
|
||||||
|
|
||||||
|
Ok(())
|
||||||
|
}
|
||||||
|
|
||||||
|
/// Get the status of a service
|
||||||
|
pub async fn status<S: AsRef<str>>(
|
||||||
|
&self,
|
||||||
|
name: S,
|
||||||
|
) -> Result<crate::zinit::service::ZInitStatus> {
|
||||||
|
let table = self.services.read().await;
|
||||||
|
let service = table
|
||||||
|
.get(name.as_ref())
|
||||||
|
.ok_or_else(|| ZInitError::unknown_service(name.as_ref()))?;
|
||||||
|
|
||||||
|
let service = service.read().await.status();
|
||||||
|
Ok(service)
|
||||||
|
}
|
||||||
|
|
||||||
|
/// Start a service
|
||||||
|
pub async fn start<S: AsRef<str>>(&self, name: S) -> Result<()> {
|
||||||
|
if *self.shutdown.read().await {
|
||||||
|
return Err(ZInitError::ShuttingDown.into());
|
||||||
|
}
|
||||||
|
|
||||||
|
self.set_service_state(name.as_ref(), None, Some(Target::Up))
|
||||||
|
.await;
|
||||||
|
|
||||||
|
let table = self.services.read().await;
|
||||||
|
let service = table
|
||||||
|
.get(name.as_ref())
|
||||||
|
.ok_or_else(|| ZInitError::unknown_service(name.as_ref()))?;
|
||||||
|
|
||||||
|
let lifecycle = self.clone_lifecycle();
|
||||||
|
tokio::spawn(lifecycle.watch_service(name.as_ref().into(), Arc::clone(service)));
|
||||||
|
|
||||||
|
Ok(())
|
||||||
|
}
|
||||||
|
|
||||||
|
/// Stop a service
|
||||||
|
pub async fn stop<S: AsRef<str>>(&self, name: S) -> Result<()> {
|
||||||
|
// Get service information
|
||||||
|
let table = self.services.read().await;
|
||||||
|
let service = table
|
||||||
|
.get(name.as_ref())
|
||||||
|
.ok_or_else(|| ZInitError::unknown_service(name.as_ref()))?;
|
||||||
|
|
||||||
|
let mut service = service.write().await;
|
||||||
|
service.set_target(Target::Down);
|
||||||
|
|
||||||
|
// Get the main process PID
|
||||||
|
let pid = service.pid;
|
||||||
|
if pid.as_raw() == 0 {
|
||||||
|
return Ok(());
|
||||||
|
}
|
||||||
|
|
||||||
|
// Get the signal to use
|
||||||
|
let signal = signal::Signal::from_str(&service.service.signal.stop.to_uppercase())
|
||||||
|
.map_err(|err| anyhow::anyhow!("unknown stop signal: {}", err))?;
|
||||||
|
|
||||||
|
// Release the lock before potentially long-running operations
|
||||||
|
drop(service);
|
||||||
|
drop(table);
|
||||||
|
|
||||||
|
// Get all child processes using our stats functionality
|
||||||
|
let children = self.get_child_process_stats(pid.as_raw()).await?;
|
||||||
|
|
||||||
|
// First try to stop the process group
|
||||||
|
let _ = self.pm.signal(pid, signal);
|
||||||
|
|
||||||
|
// Wait a short time for processes to terminate gracefully
|
||||||
|
sleep(std::time::Duration::from_millis(500)).await;
|
||||||
|
|
||||||
|
// Check if processes are still running and use SIGKILL if needed
|
||||||
|
self.ensure_processes_terminated(pid.as_raw(), &children)
|
||||||
|
.await?;
|
||||||
|
|
||||||
|
Ok(())
|
||||||
|
}
|
||||||
|
|
||||||
|
/// Ensure that a process and its children are terminated
|
||||||
|
async fn ensure_processes_terminated(
|
||||||
|
&self,
|
||||||
|
parent_pid: i32,
|
||||||
|
children: &[ProcessStats],
|
||||||
|
) -> Result<()> {
|
||||||
|
// Check if parent is still running
|
||||||
|
let parent_running = self.is_process_running(parent_pid).await?;
|
||||||
|
|
||||||
|
// If parent is still running, send SIGKILL
|
||||||
|
if parent_running {
|
||||||
|
debug!(
|
||||||
|
"Process {} still running after SIGTERM, sending SIGKILL",
|
||||||
|
parent_pid
|
||||||
|
);
|
||||||
|
let _ = self
|
||||||
|
.pm
|
||||||
|
.signal(Pid::from_raw(parent_pid), signal::Signal::SIGKILL);
|
||||||
|
}
|
||||||
|
|
||||||
|
// Check and kill any remaining child processes
|
||||||
|
for child in children {
|
||||||
|
if self.is_process_running(child.pid).await? {
|
||||||
|
debug!("Child process {} still running, sending SIGKILL", child.pid);
|
||||||
|
let _ = signal::kill(Pid::from_raw(child.pid), signal::Signal::SIGKILL);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
// Verify all processes are gone
|
||||||
|
let mut retries = 5;
|
||||||
|
while retries > 0 {
|
||||||
|
let mut all_terminated = true;
|
||||||
|
|
||||||
|
// Check parent
|
||||||
|
if self.is_process_running(parent_pid).await? {
|
||||||
|
all_terminated = false;
|
||||||
|
}
|
||||||
|
|
||||||
|
// Check children
|
||||||
|
for child in children {
|
||||||
|
if self.is_process_running(child.pid).await? {
|
||||||
|
all_terminated = false;
|
||||||
|
break;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
if all_terminated {
|
||||||
|
return Ok(());
|
||||||
|
}
|
||||||
|
|
||||||
|
// Wait before retrying
|
||||||
|
sleep(std::time::Duration::from_millis(100)).await;
|
||||||
|
retries -= 1;
|
||||||
|
}
|
||||||
|
|
||||||
|
// If we get here, some processes might still be running
|
||||||
|
warn!("Some processes may still be running after shutdown attempts");
|
||||||
|
Ok(())
|
||||||
|
}
|
||||||
|
|
||||||
|
/// Check if a process is running
|
||||||
|
async fn is_process_running(&self, pid: i32) -> Result<bool> {
|
||||||
|
// Use sysinfo to check if process exists
|
||||||
|
let mut system = System::new();
|
||||||
|
let sys_pid = sysinfo::Pid::from(pid as usize);
|
||||||
|
system.refresh_process(sys_pid);
|
||||||
|
|
||||||
|
Ok(system.process(sys_pid).is_some())
|
||||||
|
}
|
||||||
|
|
||||||
|
/// Forget a service
|
||||||
|
pub async fn forget<S: AsRef<str>>(&self, name: S) -> Result<()> {
|
||||||
|
let mut table = self.services.write().await;
|
||||||
|
let service = table
|
||||||
|
.get(name.as_ref())
|
||||||
|
.ok_or_else(|| ZInitError::unknown_service(name.as_ref()))?;
|
||||||
|
|
||||||
|
let service = service.read().await;
|
||||||
|
if service.target == Target::Up || service.pid != Pid::from_raw(0) {
|
||||||
|
return Err(ZInitError::service_is_up(name.as_ref()).into());
|
||||||
|
}
|
||||||
|
|
||||||
|
drop(service);
|
||||||
|
table.remove(name.as_ref());
|
||||||
|
|
||||||
|
Ok(())
|
||||||
|
}
|
||||||
|
|
||||||
|
/// Send a signal to a service
|
||||||
|
pub async fn kill<S: AsRef<str>>(&self, name: S, signal: signal::Signal) -> Result<()> {
|
||||||
|
let table = self.services.read().await;
|
||||||
|
let service = table
|
||||||
|
.get(name.as_ref())
|
||||||
|
.ok_or_else(|| ZInitError::unknown_service(name.as_ref()))?;
|
||||||
|
|
||||||
|
let service = service.read().await;
|
||||||
|
if service.pid == Pid::from_raw(0) {
|
||||||
|
return Err(ZInitError::service_is_down(name.as_ref()).into());
|
||||||
|
}
|
||||||
|
|
||||||
|
self.pm.signal(service.pid, signal)
|
||||||
|
}
|
||||||
|
|
||||||
|
/// List all services
|
||||||
|
pub async fn list(&self) -> Result<Vec<String>> {
|
||||||
|
let table = self.services.read().await;
|
||||||
|
Ok(table.keys().map(|k| k.into()).collect())
|
||||||
|
}
|
||||||
|
|
||||||
|
/// Get stats for a service (memory and CPU usage)
|
||||||
|
pub async fn stats<S: AsRef<str>>(&self, name: S) -> Result<ServiceStats> {
|
||||||
|
let table = self.services.read().await;
|
||||||
|
let service = table
|
||||||
|
.get(name.as_ref())
|
||||||
|
.ok_or_else(|| ZInitError::unknown_service(name.as_ref()))?;
|
||||||
|
|
||||||
|
let service = service.read().await;
|
||||||
|
if service.pid.as_raw() == 0 {
|
||||||
|
return Err(ZInitError::service_is_down(name.as_ref()).into());
|
||||||
|
}
|
||||||
|
|
||||||
|
// Get stats for the main process
|
||||||
|
let pid = service.pid.as_raw();
|
||||||
|
let (memory_usage, cpu_usage) = self.get_process_stats(pid).await?;
|
||||||
|
|
||||||
|
// Get stats for child processes
|
||||||
|
let children = self.get_child_process_stats(pid).await?;
|
||||||
|
|
||||||
|
Ok(ServiceStats {
|
||||||
|
memory_usage,
|
||||||
|
cpu_usage,
|
||||||
|
pid,
|
||||||
|
children,
|
||||||
|
})
|
||||||
|
}
|
||||||
|
|
||||||
|
/// Get memory and CPU usage for a process
|
||||||
|
async fn get_process_stats(&self, pid: i32) -> Result<(u64, f32)> {
|
||||||
|
// Create a new System instance with all information
|
||||||
|
let mut system = System::new_all();
|
||||||
|
|
||||||
|
// Convert i32 pid to sysinfo::Pid
|
||||||
|
let sys_pid = sysinfo::Pid::from(pid as usize);
|
||||||
|
|
||||||
|
// Make sure we're refreshing CPU information
|
||||||
|
system.refresh_cpu();
|
||||||
|
system.refresh_processes();
|
||||||
|
|
||||||
|
// First refresh to get initial CPU values
|
||||||
|
system.refresh_all();
|
||||||
|
|
||||||
|
// Wait longer for CPU measurement (500ms instead of 100ms)
|
||||||
|
sleep(std::time::Duration::from_millis(500)).await;
|
||||||
|
|
||||||
|
// Refresh again to get updated CPU values
|
||||||
|
system.refresh_cpu();
|
||||||
|
system.refresh_processes();
|
||||||
|
system.refresh_all();
|
||||||
|
|
||||||
|
// Get the process
|
||||||
|
if let Some(process) = system.process(sys_pid) {
|
||||||
|
// Get memory in bytes
|
||||||
|
let memory_usage = process.memory();
|
||||||
|
|
||||||
|
// Get CPU usage as percentage
|
||||||
|
let cpu_usage = process.cpu_usage();
|
||||||
|
|
||||||
|
Ok((memory_usage, cpu_usage))
|
||||||
|
} else {
|
||||||
|
// Process not found
|
||||||
|
Ok((0, 0.0))
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
/// Get stats for child processes
|
||||||
|
async fn get_child_process_stats(&self, parent_pid: i32) -> Result<Vec<ProcessStats>> {
|
||||||
|
// Create a new System instance with all processes information
|
||||||
|
let mut system = System::new_all();
|
||||||
|
|
||||||
|
// Make sure we're refreshing CPU information
|
||||||
|
system.refresh_cpu();
|
||||||
|
system.refresh_processes();
|
||||||
|
system.refresh_all();
|
||||||
|
|
||||||
|
// Convert i32 pid to sysinfo::Pid
|
||||||
|
let sys_pid = sysinfo::Pid::from(parent_pid as usize);
|
||||||
|
|
||||||
|
// Wait longer for CPU measurement (500ms instead of 100ms)
|
||||||
|
sleep(std::time::Duration::from_millis(500)).await;
|
||||||
|
|
||||||
|
// Refresh all system information to get updated CPU values
|
||||||
|
system.refresh_cpu();
|
||||||
|
system.refresh_processes();
|
||||||
|
system.refresh_all();
|
||||||
|
|
||||||
|
let mut children = Vec::new();
|
||||||
|
|
||||||
|
// Recursively collect all descendant PIDs
|
||||||
|
let mut descendant_pids = Vec::new();
|
||||||
|
self.collect_descendants(sys_pid, &system.processes(), &mut descendant_pids);
|
||||||
|
|
||||||
|
// Get stats for each child process
|
||||||
|
for &child_pid in &descendant_pids {
|
||||||
|
if let Some(process) = system.process(child_pid) {
|
||||||
|
children.push(ProcessStats {
|
||||||
|
pid: child_pid.as_u32() as i32,
|
||||||
|
memory_usage: process.memory(),
|
||||||
|
cpu_usage: process.cpu_usage(),
|
||||||
|
});
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
Ok(children)
|
||||||
|
}
|
||||||
|
|
||||||
|
/// Recursively collect all descendant PIDs of a process
|
||||||
|
fn collect_descendants(
|
||||||
|
&self,
|
||||||
|
pid: sysinfo::Pid,
|
||||||
|
procs: &HashMap<sysinfo::Pid, sysinfo::Process>,
|
||||||
|
out: &mut Vec<sysinfo::Pid>,
|
||||||
|
) {
|
||||||
|
for (&child_pid, proc) in procs.iter() {
|
||||||
|
if proc.parent() == Some(pid) {
|
||||||
|
out.push(child_pid);
|
||||||
|
self.collect_descendants(child_pid, procs, out);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
/// Verify that all processes are terminated
|
||||||
|
async fn verify_all_processes_terminated(&self) -> Result<()> {
|
||||||
|
// Get all services
|
||||||
|
let table = self.services.read().await;
|
||||||
|
|
||||||
|
// Check each service
|
||||||
|
for (name, service) in table.iter() {
|
||||||
|
let service = service.read().await;
|
||||||
|
let pid = service.pid.as_raw();
|
||||||
|
|
||||||
|
// Skip services with no PID
|
||||||
|
if pid == 0 {
|
||||||
|
continue;
|
||||||
|
}
|
||||||
|
|
||||||
|
// Check if the main process is still running
|
||||||
|
if self.is_process_running(pid).await? {
|
||||||
|
warn!(
|
||||||
|
"Service {} (PID {}) is still running after shutdown",
|
||||||
|
name, pid
|
||||||
|
);
|
||||||
|
|
||||||
|
// Try to kill it with SIGKILL
|
||||||
|
let _ = signal::kill(Pid::from_raw(pid), signal::Signal::SIGKILL);
|
||||||
|
}
|
||||||
|
|
||||||
|
// Check for child processes
|
||||||
|
if let Ok(children) = self.get_child_process_stats(pid).await {
|
||||||
|
for child in children {
|
||||||
|
if self.is_process_running(child.pid).await? {
|
||||||
|
warn!(
|
||||||
|
"Child process {} of service {} is still running after shutdown",
|
||||||
|
child.pid, name
|
||||||
|
);
|
||||||
|
|
||||||
|
// Try to kill it with SIGKILL
|
||||||
|
let _ = signal::kill(Pid::from_raw(child.pid), signal::Signal::SIGKILL);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
Ok(())
|
||||||
|
}
|
||||||
|
|
||||||
|
/// Shutdown the system
|
||||||
|
pub async fn shutdown(&self) -> Result<()> {
|
||||||
|
info!("shutting down");
|
||||||
|
|
||||||
|
// Set the shutdown flag
|
||||||
|
*self.shutdown.write().await = true;
|
||||||
|
|
||||||
|
#[cfg(target_os = "linux")]
|
||||||
|
{
|
||||||
|
// Power off using our enhanced method
|
||||||
|
let result = self.power(RebootMode::RB_POWER_OFF).await;
|
||||||
|
|
||||||
|
// Final verification before exit
|
||||||
|
self.verify_all_processes_terminated().await?;
|
||||||
|
|
||||||
|
return result;
|
||||||
|
}
|
||||||
|
|
||||||
|
#[cfg(not(target_os = "linux"))]
|
||||||
|
{
|
||||||
|
// Stop all services
|
||||||
|
let services = self.list().await?;
|
||||||
|
for service in services {
|
||||||
|
let _ = self.stop(&service).await;
|
||||||
|
}
|
||||||
|
|
||||||
|
// Verify all processes are terminated
|
||||||
|
self.verify_all_processes_terminated().await?;
|
||||||
|
|
||||||
|
if self.container {
|
||||||
|
std::process::exit(0);
|
||||||
|
} else {
|
||||||
|
info!("System shutdown not supported on this platform");
|
||||||
|
std::process::exit(0);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
/// Reboot the system
|
||||||
|
pub async fn reboot(&self) -> Result<()> {
|
||||||
|
info!("rebooting");
|
||||||
|
|
||||||
|
// Set the shutdown flag
|
||||||
|
*self.shutdown.write().await = true;
|
||||||
|
|
||||||
|
#[cfg(target_os = "linux")]
|
||||||
|
{
|
||||||
|
// Reboot using our enhanced method
|
||||||
|
let result = self.power(RebootMode::RB_AUTOBOOT).await;
|
||||||
|
|
||||||
|
// Final verification before exit
|
||||||
|
self.verify_all_processes_terminated().await?;
|
||||||
|
|
||||||
|
return result;
|
||||||
|
}
|
||||||
|
|
||||||
|
#[cfg(not(target_os = "linux"))]
|
||||||
|
{
|
||||||
|
// Stop all services
|
||||||
|
let services = self.list().await?;
|
||||||
|
for service in services {
|
||||||
|
let _ = self.stop(&service).await;
|
||||||
|
}
|
||||||
|
|
||||||
|
// Verify all processes are terminated
|
||||||
|
self.verify_all_processes_terminated().await?;
|
||||||
|
|
||||||
|
if self.container {
|
||||||
|
std::process::exit(0);
|
||||||
|
} else {
|
||||||
|
info!("System reboot not supported on this platform");
|
||||||
|
std::process::exit(0);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
/// Power off or reboot the system
|
||||||
|
#[cfg(target_os = "linux")]
|
||||||
|
async fn power(&self, mode: RebootMode) -> Result<()> {
|
||||||
|
*self.shutdown.write().await = true;
|
||||||
|
|
||||||
|
let mut state_channels: HashMap<String, Watcher<State>> = HashMap::new();
|
||||||
|
let mut shutdown_timeouts: HashMap<String, u64> = HashMap::new();
|
||||||
|
|
||||||
|
let table = self.services.read().await;
|
||||||
|
for (name, service) in table.iter() {
|
||||||
|
let service = service.read().await;
|
||||||
|
if service.is_active() {
|
||||||
|
info!("service '{}' is scheduled for a shutdown", name);
|
||||||
|
state_channels.insert(name.into(), service.state_watcher());
|
||||||
|
shutdown_timeouts.insert(name.into(), service.service.shutdown_timeout);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
drop(table);
|
||||||
|
let dag = service_dependency_order(self.services.clone()).await;
|
||||||
|
self.kill_process_tree(dag, state_channels, shutdown_timeouts)
|
||||||
|
.await?;
|
||||||
|
|
||||||
|
// On Linux, we can use sync and reboot
|
||||||
|
nix::unistd::sync();
|
||||||
|
if self.container {
|
||||||
|
std::process::exit(0);
|
||||||
|
} else {
|
||||||
|
nix::sys::reboot::reboot(mode)?;
|
||||||
|
}
|
||||||
|
|
||||||
|
Ok(())
|
||||||
|
}
|
||||||
|
|
||||||
|
/// Kill processes in dependency order
|
||||||
|
#[cfg(target_os = "linux")]
|
||||||
|
async fn kill_process_tree(
|
||||||
|
&self,
|
||||||
|
mut dag: ProcessDAG,
|
||||||
|
mut state_channels: HashMap<String, Watcher<State>>,
|
||||||
|
mut shutdown_timeouts: HashMap<String, u64>,
|
||||||
|
) -> Result<()> {
|
||||||
|
let (tx, mut rx) = mpsc::unbounded_channel();
|
||||||
|
tx.send(DUMMY_ROOT.into())?;
|
||||||
|
|
||||||
|
let mut count = dag.count;
|
||||||
|
while let Some(name) = rx.recv().await {
|
||||||
|
debug!(
|
||||||
|
"{} has been killed (or was inactive) adding its children",
|
||||||
|
name
|
||||||
|
);
|
||||||
|
|
||||||
|
for child in dag.adj.get(&name).unwrap_or(&Vec::new()) {
|
||||||
|
let child_indegree: &mut u32 = dag.indegree.entry(child.clone()).or_default();
|
||||||
|
*child_indegree -= 1;
|
||||||
|
|
||||||
|
debug!(
|
||||||
|
"decrementing child {} indegree to {}",
|
||||||
|
child, child_indegree
|
||||||
|
);
|
||||||
|
|
||||||
|
if *child_indegree == 0 {
|
||||||
|
let watcher = state_channels.remove(child);
|
||||||
|
if watcher.is_none() {
|
||||||
|
// not an active service
|
||||||
|
tx.send(child.to_string())?;
|
||||||
|
continue;
|
||||||
|
}
|
||||||
|
|
||||||
|
let shutdown_timeout = shutdown_timeouts.remove(child);
|
||||||
|
let lifecycle = self.clone_lifecycle();
|
||||||
|
|
||||||
|
// Spawn a task to kill the service and wait for it to terminate
|
||||||
|
let kill_task = tokio::spawn(Self::kill_wait_enhanced(
|
||||||
|
lifecycle,
|
||||||
|
child.to_string(),
|
||||||
|
tx.clone(),
|
||||||
|
watcher.unwrap(),
|
||||||
|
shutdown_timeout.unwrap_or(config::DEFAULT_SHUTDOWN_TIMEOUT),
|
||||||
|
));
|
||||||
|
|
||||||
|
// Add a timeout to ensure we don't wait forever
|
||||||
|
let _ = tokio::time::timeout(
|
||||||
|
std::time::Duration::from_secs(
|
||||||
|
shutdown_timeout.unwrap_or(config::DEFAULT_SHUTDOWN_TIMEOUT) + 2,
|
||||||
|
),
|
||||||
|
kill_task,
|
||||||
|
)
|
||||||
|
.await;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
count -= 1;
|
||||||
|
if count == 0 {
|
||||||
|
break;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
// Final verification that all processes are gone
|
||||||
|
self.verify_all_processes_terminated().await?;
|
||||||
|
|
||||||
|
Ok(())
|
||||||
|
}
|
||||||
|
|
||||||
|
/// Enhanced version of kill_wait that ensures processes are terminated
|
||||||
|
#[cfg(target_os = "linux")]
|
||||||
|
async fn kill_wait_enhanced(
|
||||||
|
self,
|
||||||
|
name: String,
|
||||||
|
ch: mpsc::UnboundedSender<String>,
|
||||||
|
mut rx: Watcher<State>,
|
||||||
|
shutdown_timeout: u64,
|
||||||
|
) {
|
||||||
|
debug!("kill_wait {}", name);
|
||||||
|
|
||||||
|
// Try to stop the service gracefully
|
||||||
|
let stop_result = self.stop(name.clone()).await;
|
||||||
|
|
||||||
|
// Wait for the service to become inactive or timeout
|
||||||
|
let fut = timeout(
|
||||||
|
std::time::Duration::from_secs(shutdown_timeout),
|
||||||
|
async move {
|
||||||
|
while let Some(state) = rx.next().await {
|
||||||
|
if !state.is_active() {
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
},
|
||||||
|
);
|
||||||
|
|
||||||
|
match stop_result {
|
||||||
|
Ok(_) => {
|
||||||
|
let _ = fut.await;
|
||||||
|
}
|
||||||
|
Err(e) => error!("couldn't stop service {}: {}", name.clone(), e),
|
||||||
|
}
|
||||||
|
|
||||||
|
// Verify the service is actually stopped
|
||||||
|
if let Ok(status) = self.status(&name).await {
|
||||||
|
if status.pid != Pid::from_raw(0) {
|
||||||
|
// Service is still running, try to kill it
|
||||||
|
let _ = self.kill(&name, signal::Signal::SIGKILL).await;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
debug!("sending to the death channel {}", name.clone());
|
||||||
|
if let Err(e) = ch.send(name.clone()) {
|
||||||
|
error!(
|
||||||
|
"error: couldn't send the service {} to the shutdown loop: {}",
|
||||||
|
name, e
|
||||||
|
);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
/// Original kill_wait for backward compatibility
|
||||||
|
#[cfg(target_os = "linux")]
|
||||||
|
async fn kill_wait(
|
||||||
|
self,
|
||||||
|
name: String,
|
||||||
|
ch: mpsc::UnboundedSender<String>,
|
||||||
|
rx: Watcher<State>,
|
||||||
|
shutdown_timeout: u64,
|
||||||
|
) {
|
||||||
|
Self::kill_wait_enhanced(self, name, ch, rx, shutdown_timeout).await
|
||||||
|
}
|
||||||
|
|
||||||
|
/// Check if a service can be scheduled
|
||||||
|
async fn can_schedule(&self, service: &config::Service) -> bool {
|
||||||
|
let mut can = true;
|
||||||
|
let table = self.services.read().await;
|
||||||
|
|
||||||
|
for dep in service.after.iter() {
|
||||||
|
can = match table.get(dep) {
|
||||||
|
Some(ps) => {
|
||||||
|
let ps = ps.read().await;
|
||||||
|
debug!(
|
||||||
|
"- service {} is {:?} oneshot: {}",
|
||||||
|
dep,
|
||||||
|
ps.get_state(),
|
||||||
|
ps.service.one_shot
|
||||||
|
);
|
||||||
|
|
||||||
|
match ps.get_state() {
|
||||||
|
State::Running if !ps.service.one_shot => true,
|
||||||
|
State::Success => true,
|
||||||
|
_ => false,
|
||||||
|
}
|
||||||
|
}
|
||||||
|
// depending on an undefined service. This still can be resolved later
|
||||||
|
// by monitoring the dependency in the future.
|
||||||
|
None => false,
|
||||||
|
};
|
||||||
|
|
||||||
|
// if state is blocked, we can break the loop
|
||||||
|
if !can {
|
||||||
|
break;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
can
|
||||||
|
}
|
||||||
|
|
||||||
|
/// Set the state and/or target of a service
|
||||||
|
async fn set_service_state(&self, name: &str, state: Option<State>, target: Option<Target>) {
|
||||||
|
let table = self.services.read().await;
|
||||||
|
let service = match table.get(name) {
|
||||||
|
Some(service) => service,
|
||||||
|
None => return,
|
||||||
|
};
|
||||||
|
|
||||||
|
let mut service = service.write().await;
|
||||||
|
if let Some(state) = state {
|
||||||
|
service.force_set_state(state);
|
||||||
|
}
|
||||||
|
|
||||||
|
if let Some(target) = target {
|
||||||
|
service.set_target(target);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
/// Test if a service is running correctly
|
||||||
|
async fn test_service<S: AsRef<str>>(&self, name: S, cfg: &config::Service) -> Result<bool> {
|
||||||
|
if cfg.test.is_empty() {
|
||||||
|
return Ok(true);
|
||||||
|
}
|
||||||
|
|
||||||
|
let log = match cfg.log {
|
||||||
|
config::Log::None => Log::None,
|
||||||
|
config::Log::Stdout => Log::Stdout,
|
||||||
|
config::Log::Ring => Log::Ring(format!("{}/test", name.as_ref())),
|
||||||
|
};
|
||||||
|
|
||||||
|
let test = self
|
||||||
|
.pm
|
||||||
|
.run(
|
||||||
|
Process::new(&cfg.test, &cfg.dir, Some(cfg.env.clone())),
|
||||||
|
log.clone(),
|
||||||
|
)
|
||||||
|
.await?;
|
||||||
|
|
||||||
|
let status = test.wait().await?;
|
||||||
|
Ok(status.success())
|
||||||
|
}
|
||||||
|
|
||||||
|
/// Run the test loop for a service
|
||||||
|
async fn test_loop(self, name: String, cfg: config::Service) {
|
||||||
|
loop {
|
||||||
|
let result = self.test_service(&name, &cfg).await;
|
||||||
|
|
||||||
|
match result {
|
||||||
|
Ok(result) => {
|
||||||
|
if result {
|
||||||
|
self.set_service_state(&name, Some(State::Running), None)
|
||||||
|
.await;
|
||||||
|
// release
|
||||||
|
self.notify.notify_waiters();
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
// wait before we try again
|
||||||
|
sleep(std::time::Duration::from_secs(2)).await;
|
||||||
|
}
|
||||||
|
Err(_) => {
|
||||||
|
self.set_service_state(&name, Some(State::TestFailure), None)
|
||||||
|
.await;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
/// Watch a service and manage its lifecycle
|
||||||
|
async fn watch_service(self, name: String, input: Arc<RwLock<ZInitService>>) {
|
||||||
|
let name = name.clone();
|
||||||
|
|
||||||
|
let mut service = input.write().await;
|
||||||
|
if service.target == Target::Down {
|
||||||
|
debug!("service '{}' target is down", name);
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
|
||||||
|
if service.scheduled {
|
||||||
|
debug!("service '{}' already scheduled", name);
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
|
||||||
|
service.scheduled = true;
|
||||||
|
drop(service);
|
||||||
|
|
||||||
|
loop {
|
||||||
|
let name = name.clone();
|
||||||
|
|
||||||
|
let service = input.read().await;
|
||||||
|
// early check if service is down, so we don't have to do extra checks
|
||||||
|
if service.target == Target::Down {
|
||||||
|
// we check target in loop in case service have
|
||||||
|
// been set down.
|
||||||
|
break;
|
||||||
|
}
|
||||||
|
|
||||||
|
let config = service.service.clone();
|
||||||
|
// we need to spawn this service now, but is it ready?
|
||||||
|
// are all dependent services are running?
|
||||||
|
|
||||||
|
// so we drop the table to give other services
|
||||||
|
// chance to acquire the lock and schedule themselves
|
||||||
|
drop(service);
|
||||||
|
|
||||||
|
'checks: loop {
|
||||||
|
let sig = self.notify.notified();
|
||||||
|
debug!("checking {} if it can schedule", name);
|
||||||
|
if self.can_schedule(&config).await {
|
||||||
|
debug!("service {} can schedule", name);
|
||||||
|
break 'checks;
|
||||||
|
}
|
||||||
|
|
||||||
|
self.set_service_state(&name, Some(State::Blocked), None)
|
||||||
|
.await;
|
||||||
|
// don't even care if i am lagging
|
||||||
|
// as long i am notified that some services status
|
||||||
|
// has changed
|
||||||
|
debug!("service {} is blocked, waiting release", name);
|
||||||
|
sig.await;
|
||||||
|
}
|
||||||
|
|
||||||
|
let log = match config.log {
|
||||||
|
config::Log::None => Log::None,
|
||||||
|
config::Log::Stdout => Log::Stdout,
|
||||||
|
config::Log::Ring => Log::Ring(name.clone()),
|
||||||
|
};
|
||||||
|
|
||||||
|
let mut service = input.write().await;
|
||||||
|
// we check again in case target has changed. Since we had to release the lock
|
||||||
|
// earlier to not block locking on this service (for example if a stop was called)
|
||||||
|
// while the service was waiting for dependencies.
|
||||||
|
// the lock is kept until the spawning and the update of the pid.
|
||||||
|
if service.target == Target::Down {
|
||||||
|
// we check target in loop in case service have
|
||||||
|
// been set down.
|
||||||
|
break;
|
||||||
|
}
|
||||||
|
|
||||||
|
let child = self
|
||||||
|
.pm
|
||||||
|
.run(
|
||||||
|
Process::new(&config.exec, &config.dir, Some(config.env.clone())),
|
||||||
|
log.clone(),
|
||||||
|
)
|
||||||
|
.await;
|
||||||
|
|
||||||
|
let child = match child {
|
||||||
|
Ok(child) => {
|
||||||
|
service.force_set_state(State::Spawned);
|
||||||
|
service.set_pid(child.pid);
|
||||||
|
child
|
||||||
|
}
|
||||||
|
Err(err) => {
|
||||||
|
// so, spawning failed. and nothing we can do about it
|
||||||
|
// this can be duo to a bad command or exe not found.
|
||||||
|
// set service to failure.
|
||||||
|
error!("service {} failed to start: {}", name, err);
|
||||||
|
service.force_set_state(State::Failure);
|
||||||
|
break;
|
||||||
|
}
|
||||||
|
};
|
||||||
|
|
||||||
|
if config.one_shot {
|
||||||
|
service.force_set_state(State::Running);
|
||||||
|
}
|
||||||
|
|
||||||
|
// we don't lock here because this can take forever
|
||||||
|
// to finish. so we allow other operation on the service (for example)
|
||||||
|
// status and stop operations.
|
||||||
|
drop(service);
|
||||||
|
|
||||||
|
let mut handler = None;
|
||||||
|
if !config.one_shot {
|
||||||
|
let lifecycle = self.clone_lifecycle();
|
||||||
|
handler = Some(tokio::spawn(
|
||||||
|
lifecycle.test_loop(name.clone(), config.clone()),
|
||||||
|
));
|
||||||
|
}
|
||||||
|
|
||||||
|
let result = child.wait().await;
|
||||||
|
if let Some(handler) = handler {
|
||||||
|
handler.abort();
|
||||||
|
}
|
||||||
|
|
||||||
|
let mut service = input.write().await;
|
||||||
|
service.clear_pid();
|
||||||
|
|
||||||
|
match result {
|
||||||
|
Err(err) => {
|
||||||
|
error!("failed to read service '{}' status: {}", name, err);
|
||||||
|
service.force_set_state(State::Unknown);
|
||||||
|
}
|
||||||
|
Ok(status) => {
|
||||||
|
service.force_set_state(if status.success() {
|
||||||
|
State::Success
|
||||||
|
} else {
|
||||||
|
State::Error(status)
|
||||||
|
});
|
||||||
|
}
|
||||||
|
};
|
||||||
|
|
||||||
|
drop(service);
|
||||||
|
if config.one_shot {
|
||||||
|
// we don't need to restart the service anymore
|
||||||
|
self.notify.notify_waiters();
|
||||||
|
break;
|
||||||
|
}
|
||||||
|
|
||||||
|
// we trying again in 2 seconds
|
||||||
|
sleep(std::time::Duration::from_secs(2)).await;
|
||||||
|
}
|
||||||
|
|
||||||
|
let mut service = input.write().await;
|
||||||
|
service.scheduled = false;
|
||||||
|
}
|
||||||
|
|
||||||
|
/// Clone the lifecycle manager
|
||||||
|
pub fn clone_lifecycle(&self) -> Self {
|
||||||
|
Self {
|
||||||
|
pm: self.pm.clone(),
|
||||||
|
services: Arc::clone(&self.services),
|
||||||
|
notify: Arc::clone(&self.notify),
|
||||||
|
shutdown: Arc::clone(&self.shutdown),
|
||||||
|
container: self.container,
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
119
components/zinit/src/zinit/mod.rs
Normal file
119
components/zinit/src/zinit/mod.rs
Normal file
@@ -0,0 +1,119 @@
|
|||||||
|
pub mod config;
|
||||||
|
pub mod errors;
|
||||||
|
pub mod lifecycle;
|
||||||
|
pub mod ord;
|
||||||
|
pub mod service;
|
||||||
|
pub mod state;
|
||||||
|
pub mod types;
|
||||||
|
|
||||||
|
// Re-export commonly used items
|
||||||
|
pub use service::ZInitStatus;
|
||||||
|
pub use state::State;
|
||||||
|
pub use types::{ProcessStats, ServiceStats};
|
||||||
|
|
||||||
|
use crate::manager::{Logs, ProcessManager};
|
||||||
|
use anyhow::Result;
|
||||||
|
use nix::sys::signal;
|
||||||
|
use std::sync::Arc;
|
||||||
|
use tokio::sync::{Notify, RwLock};
|
||||||
|
|
||||||
|
/// Main ZInit service manager
|
||||||
|
#[derive(Clone)]
|
||||||
|
pub struct ZInit {
|
||||||
|
/// Lifecycle manager for service management
|
||||||
|
lifecycle: lifecycle::LifecycleManager,
|
||||||
|
}
|
||||||
|
|
||||||
|
impl ZInit {
|
||||||
|
/// Create a new ZInit instance
|
||||||
|
pub fn new(cap: usize, container: bool) -> ZInit {
|
||||||
|
let pm = ProcessManager::new(cap);
|
||||||
|
let services = Arc::new(RwLock::new(types::ServiceTable::new()));
|
||||||
|
let notify = Arc::new(Notify::new());
|
||||||
|
let shutdown = Arc::new(RwLock::new(false));
|
||||||
|
|
||||||
|
let lifecycle = lifecycle::LifecycleManager::new(pm, services, notify, shutdown, container);
|
||||||
|
|
||||||
|
ZInit { lifecycle }
|
||||||
|
}
|
||||||
|
|
||||||
|
/// Start the service manager
|
||||||
|
pub fn serve(&self) {
|
||||||
|
self.lifecycle.process_manager().start();
|
||||||
|
if self.lifecycle.is_container_mode() {
|
||||||
|
let lifecycle = self.lifecycle.clone_lifecycle();
|
||||||
|
tokio::spawn(async move {
|
||||||
|
use tokio::signal::unix;
|
||||||
|
|
||||||
|
let mut term = unix::signal(unix::SignalKind::terminate()).unwrap();
|
||||||
|
let mut int = unix::signal(unix::SignalKind::interrupt()).unwrap();
|
||||||
|
let mut hup = unix::signal(unix::SignalKind::hangup()).unwrap();
|
||||||
|
|
||||||
|
tokio::select! {
|
||||||
|
_ = term.recv() => {},
|
||||||
|
_ = int.recv() => {},
|
||||||
|
_ = hup.recv() => {},
|
||||||
|
};
|
||||||
|
|
||||||
|
debug!("shutdown signal received");
|
||||||
|
let _ = lifecycle.shutdown().await;
|
||||||
|
});
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
/// Get logs from the process manager
|
||||||
|
/// `existing_logs` TODO:
|
||||||
|
pub async fn logs(&self, existing_logs: bool, follow: bool) -> Logs {
|
||||||
|
self.lifecycle.logs(existing_logs, follow).await
|
||||||
|
}
|
||||||
|
|
||||||
|
/// Monitor a service
|
||||||
|
pub async fn monitor<S: Into<String>>(&self, name: S, service: config::Service) -> Result<()> {
|
||||||
|
self.lifecycle.monitor(name, service).await
|
||||||
|
}
|
||||||
|
|
||||||
|
/// Get the status of a service
|
||||||
|
pub async fn status<S: AsRef<str>>(&self, name: S) -> Result<ZInitStatus> {
|
||||||
|
self.lifecycle.status(name).await
|
||||||
|
}
|
||||||
|
|
||||||
|
/// Start a service
|
||||||
|
pub async fn start<S: AsRef<str>>(&self, name: S) -> Result<()> {
|
||||||
|
self.lifecycle.start(name).await
|
||||||
|
}
|
||||||
|
|
||||||
|
/// Stop a service
|
||||||
|
pub async fn stop<S: AsRef<str>>(&self, name: S) -> Result<()> {
|
||||||
|
self.lifecycle.stop(name).await
|
||||||
|
}
|
||||||
|
|
||||||
|
/// Forget a service
|
||||||
|
pub async fn forget<S: AsRef<str>>(&self, name: S) -> Result<()> {
|
||||||
|
self.lifecycle.forget(name).await
|
||||||
|
}
|
||||||
|
|
||||||
|
/// Send a signal to a service
|
||||||
|
pub async fn kill<S: AsRef<str>>(&self, name: S, signal: signal::Signal) -> Result<()> {
|
||||||
|
self.lifecycle.kill(name, signal).await
|
||||||
|
}
|
||||||
|
|
||||||
|
/// List all services
|
||||||
|
pub async fn list(&self) -> Result<Vec<String>> {
|
||||||
|
self.lifecycle.list().await
|
||||||
|
}
|
||||||
|
|
||||||
|
/// Shutdown the system
|
||||||
|
pub async fn shutdown(&self) -> Result<()> {
|
||||||
|
self.lifecycle.shutdown().await
|
||||||
|
}
|
||||||
|
|
||||||
|
/// Reboot the system
|
||||||
|
pub async fn reboot(&self) -> Result<()> {
|
||||||
|
self.lifecycle.reboot().await
|
||||||
|
}
|
||||||
|
|
||||||
|
/// Get stats for a service (memory and CPU usage)
|
||||||
|
pub async fn stats<S: AsRef<str>>(&self, name: S) -> Result<ServiceStats> {
|
||||||
|
self.lifecycle.stats(name).await
|
||||||
|
}
|
||||||
|
}
|
||||||
37
components/zinit/src/zinit/ord.rs
Normal file
37
components/zinit/src/zinit/ord.rs
Normal file
@@ -0,0 +1,37 @@
|
|||||||
|
use crate::zinit::types::ServiceTable;
|
||||||
|
use std::collections::HashMap;
|
||||||
|
use std::sync::Arc;
|
||||||
|
use tokio::sync::RwLock;
|
||||||
|
pub const DUMMY_ROOT: &str = "";
|
||||||
|
pub struct ProcessDAG {
|
||||||
|
pub adj: HashMap<String, Vec<String>>,
|
||||||
|
pub indegree: HashMap<String, u32>,
|
||||||
|
/// number of services including the dummy root
|
||||||
|
pub count: u32,
|
||||||
|
}
|
||||||
|
pub async fn service_dependency_order(services: Arc<RwLock<ServiceTable>>) -> ProcessDAG {
|
||||||
|
let mut children: HashMap<String, Vec<String>> = HashMap::new();
|
||||||
|
let mut indegree: HashMap<String, u32> = HashMap::new();
|
||||||
|
let table = services.read().await;
|
||||||
|
for (name, service) in table.iter() {
|
||||||
|
let service = service.read().await;
|
||||||
|
for child in service.service.after.iter() {
|
||||||
|
children.entry(name.into()).or_default().push(child.into());
|
||||||
|
*indegree.entry(child.into()).or_insert(0) += 1;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
let mut heads: Vec<String> = Vec::new();
|
||||||
|
for (name, _) in table.iter() {
|
||||||
|
if *indegree.get::<str>(name).unwrap_or(&0) == 0 {
|
||||||
|
heads.push(name.into());
|
||||||
|
// add edges from the dummy root to the heads
|
||||||
|
*indegree.entry(name.into()).or_insert(0) += 1;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
children.insert(DUMMY_ROOT.to_string(), heads);
|
||||||
|
ProcessDAG {
|
||||||
|
adj: children,
|
||||||
|
indegree,
|
||||||
|
count: table.len() as u32 + 1,
|
||||||
|
}
|
||||||
|
}
|
||||||
126
components/zinit/src/zinit/service.rs
Normal file
126
components/zinit/src/zinit/service.rs
Normal file
@@ -0,0 +1,126 @@
|
|||||||
|
use crate::zinit::config;
|
||||||
|
use crate::zinit::state::{State, Target};
|
||||||
|
use crate::zinit::types::Watched;
|
||||||
|
use anyhow::{Context, Result};
|
||||||
|
use nix::unistd::Pid;
|
||||||
|
|
||||||
|
/// Represents a service managed by ZInit
|
||||||
|
pub struct ZInitService {
|
||||||
|
/// Process ID of the running service
|
||||||
|
pub pid: Pid,
|
||||||
|
|
||||||
|
/// Service configuration
|
||||||
|
pub service: config::Service,
|
||||||
|
|
||||||
|
/// Target state of the service (up, down)
|
||||||
|
pub target: Target,
|
||||||
|
|
||||||
|
/// Whether the service is scheduled for execution
|
||||||
|
pub scheduled: bool,
|
||||||
|
|
||||||
|
/// Current state of the service
|
||||||
|
state: Watched<State>,
|
||||||
|
}
|
||||||
|
|
||||||
|
/// Status information for a service
|
||||||
|
pub struct ZInitStatus {
|
||||||
|
/// Process ID of the running service
|
||||||
|
pub pid: Pid,
|
||||||
|
|
||||||
|
/// Service configuration
|
||||||
|
pub service: config::Service,
|
||||||
|
|
||||||
|
/// Target state of the service (up, down)
|
||||||
|
pub target: Target,
|
||||||
|
|
||||||
|
/// Whether the service is scheduled for execution
|
||||||
|
pub scheduled: bool,
|
||||||
|
|
||||||
|
/// Current state of the service
|
||||||
|
pub state: State,
|
||||||
|
}
|
||||||
|
|
||||||
|
impl ZInitService {
|
||||||
|
/// Create a new service with the given configuration and initial state
|
||||||
|
pub fn new(service: config::Service, state: State) -> ZInitService {
|
||||||
|
ZInitService {
|
||||||
|
pid: Pid::from_raw(0),
|
||||||
|
state: Watched::new(state),
|
||||||
|
service,
|
||||||
|
target: Target::Up,
|
||||||
|
scheduled: false,
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
/// Get the current status of the service
|
||||||
|
pub fn status(&self) -> ZInitStatus {
|
||||||
|
ZInitStatus {
|
||||||
|
pid: self.pid,
|
||||||
|
state: self.state.get().clone(),
|
||||||
|
service: self.service.clone(),
|
||||||
|
target: self.target.clone(),
|
||||||
|
scheduled: self.scheduled,
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
/// Set the state of the service, validating the state transition
|
||||||
|
pub fn set_state(&mut self, state: State) -> Result<()> {
|
||||||
|
let current_state = self.state.get().clone();
|
||||||
|
let new_state = current_state
|
||||||
|
.transition_to(state)
|
||||||
|
.context("Failed to transition service state")?;
|
||||||
|
|
||||||
|
self.state.set(new_state);
|
||||||
|
Ok(())
|
||||||
|
}
|
||||||
|
|
||||||
|
/// Set the state of the service without validation
|
||||||
|
pub fn force_set_state(&mut self, state: State) {
|
||||||
|
self.state.set(state);
|
||||||
|
}
|
||||||
|
|
||||||
|
/// Set the target state of the service
|
||||||
|
pub fn set_target(&mut self, target: Target) {
|
||||||
|
self.target = target;
|
||||||
|
}
|
||||||
|
|
||||||
|
/// Get the current state of the service
|
||||||
|
pub fn get_state(&self) -> &State {
|
||||||
|
self.state.get()
|
||||||
|
}
|
||||||
|
|
||||||
|
/// Get a watcher for the service state
|
||||||
|
pub fn state_watcher(&self) -> crate::zinit::types::Watcher<State> {
|
||||||
|
self.state.watcher()
|
||||||
|
}
|
||||||
|
|
||||||
|
/// Check if the service is active (running or in progress)
|
||||||
|
pub fn is_active(&self) -> bool {
|
||||||
|
self.state.get().is_active()
|
||||||
|
}
|
||||||
|
|
||||||
|
/// Check if the service is in a terminal state (success or failure)
|
||||||
|
pub fn is_terminal(&self) -> bool {
|
||||||
|
self.state.get().is_terminal()
|
||||||
|
}
|
||||||
|
|
||||||
|
/// Set the process ID of the service
|
||||||
|
pub fn set_pid(&mut self, pid: Pid) {
|
||||||
|
self.pid = pid;
|
||||||
|
}
|
||||||
|
|
||||||
|
/// Clear the process ID of the service
|
||||||
|
pub fn clear_pid(&mut self) {
|
||||||
|
self.pid = Pid::from_raw(0);
|
||||||
|
}
|
||||||
|
|
||||||
|
/// Check if the service is running
|
||||||
|
pub fn is_running(&self) -> bool {
|
||||||
|
self.pid.as_raw() != 0 && self.state.get().is_active()
|
||||||
|
}
|
||||||
|
|
||||||
|
/// Check if the service is a one-shot service
|
||||||
|
pub fn is_one_shot(&self) -> bool {
|
||||||
|
self.service.one_shot
|
||||||
|
}
|
||||||
|
}
|
||||||
106
components/zinit/src/zinit/state.rs
Normal file
106
components/zinit/src/zinit/state.rs
Normal file
@@ -0,0 +1,106 @@
|
|||||||
|
use crate::zinit::errors::ZInitError;
|
||||||
|
use anyhow::Result;
|
||||||
|
use nix::sys::wait::WaitStatus;
|
||||||
|
|
||||||
|
/// Target state for a service
|
||||||
|
#[derive(Clone, Debug, PartialEq)]
|
||||||
|
pub enum Target {
|
||||||
|
/// Service should be running
|
||||||
|
Up,
|
||||||
|
/// Service should be stopped
|
||||||
|
Down,
|
||||||
|
}
|
||||||
|
|
||||||
|
/// Service state
|
||||||
|
#[derive(Debug, PartialEq, Clone)]
|
||||||
|
pub enum State {
|
||||||
|
/// Service is in an unknown state
|
||||||
|
Unknown,
|
||||||
|
|
||||||
|
/// Blocked means one or more dependencies hasn't been met yet. Service can stay in
|
||||||
|
/// this state as long as at least one dependency is not in either Running, or Success
|
||||||
|
Blocked,
|
||||||
|
|
||||||
|
/// Service has been started, but it didn't exit yet, or we didn't run the test command.
|
||||||
|
Spawned,
|
||||||
|
|
||||||
|
/// Service has been started, and test command passed.
|
||||||
|
Running,
|
||||||
|
|
||||||
|
/// Service has exited with success state, only one-shot can stay in this state
|
||||||
|
Success,
|
||||||
|
|
||||||
|
/// Service exited with this error, only one-shot can stay in this state
|
||||||
|
Error(WaitStatus),
|
||||||
|
|
||||||
|
/// The service test command failed, this might (or might not) be replaced
|
||||||
|
/// with an Error state later on once the service process itself exits
|
||||||
|
TestFailure,
|
||||||
|
|
||||||
|
/// Failure means the service has failed to spawn in a way that retrying
|
||||||
|
/// won't help, like command line parsing error or failed to fork
|
||||||
|
Failure,
|
||||||
|
}
|
||||||
|
|
||||||
|
impl State {
|
||||||
|
/// Validate if a transition from the current state to the new state is valid
|
||||||
|
pub fn can_transition_to(&self, new_state: &State) -> bool {
|
||||||
|
match (self, new_state) {
|
||||||
|
// From Unknown state, any transition is valid
|
||||||
|
(State::Unknown, _) => true,
|
||||||
|
|
||||||
|
// From Blocked state
|
||||||
|
(State::Blocked, State::Spawned) => true,
|
||||||
|
(State::Blocked, State::Failure) => true,
|
||||||
|
|
||||||
|
// From Spawned state
|
||||||
|
(State::Spawned, State::Running) => true,
|
||||||
|
(State::Spawned, State::TestFailure) => true,
|
||||||
|
(State::Spawned, State::Error(_)) => true,
|
||||||
|
(State::Spawned, State::Success) => true,
|
||||||
|
|
||||||
|
// From Running state
|
||||||
|
(State::Running, State::Success) => true,
|
||||||
|
(State::Running, State::Error(_)) => true,
|
||||||
|
|
||||||
|
// To Unknown or Blocked state is always valid
|
||||||
|
(_, State::Unknown) => true,
|
||||||
|
(_, State::Blocked) => true,
|
||||||
|
|
||||||
|
// Any other transition is invalid
|
||||||
|
_ => false,
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
/// Attempt to transition to a new state, validating the transition
|
||||||
|
pub fn transition_to(&self, new_state: State) -> Result<State, ZInitError> {
|
||||||
|
if self.can_transition_to(&new_state) {
|
||||||
|
Ok(new_state)
|
||||||
|
} else {
|
||||||
|
Err(ZInitError::invalid_state_transition(format!(
|
||||||
|
"Invalid transition from {:?} to {:?}",
|
||||||
|
self, new_state
|
||||||
|
)))
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
/// Check if the state is considered "active" (running or in progress)
|
||||||
|
pub fn is_active(&self) -> bool {
|
||||||
|
matches!(self, State::Running | State::Spawned)
|
||||||
|
}
|
||||||
|
|
||||||
|
/// Check if the state is considered "terminal" (success or failure)
|
||||||
|
pub fn is_terminal(&self) -> bool {
|
||||||
|
matches!(self, State::Success | State::Error(_) | State::Failure)
|
||||||
|
}
|
||||||
|
|
||||||
|
/// Check if the state is considered "successful"
|
||||||
|
pub fn is_successful(&self) -> bool {
|
||||||
|
matches!(self, State::Success | State::Running)
|
||||||
|
}
|
||||||
|
|
||||||
|
/// Check if the state is considered "failed"
|
||||||
|
pub fn is_failed(&self) -> bool {
|
||||||
|
matches!(self, State::Error(_) | State::Failure | State::TestFailure)
|
||||||
|
}
|
||||||
|
}
|
||||||
89
components/zinit/src/zinit/types.rs
Normal file
89
components/zinit/src/zinit/types.rs
Normal file
@@ -0,0 +1,89 @@
|
|||||||
|
use nix::sys::wait::WaitStatus;
|
||||||
|
use serde::{Deserialize, Serialize};
|
||||||
|
use std::collections::HashMap;
|
||||||
|
use std::sync::Arc;
|
||||||
|
use tokio::sync::watch;
|
||||||
|
use tokio::sync::RwLock;
|
||||||
|
use tokio_stream::wrappers::WatchStream;
|
||||||
|
|
||||||
|
/// Stats information for a service
|
||||||
|
#[derive(Debug, Clone, Serialize, Deserialize)]
|
||||||
|
pub struct ServiceStats {
|
||||||
|
/// Memory usage in bytes
|
||||||
|
pub memory_usage: u64,
|
||||||
|
|
||||||
|
/// CPU usage as a percentage (0-100)
|
||||||
|
pub cpu_usage: f32,
|
||||||
|
|
||||||
|
/// Process ID of the service
|
||||||
|
pub pid: i32,
|
||||||
|
|
||||||
|
/// Child process stats if any
|
||||||
|
pub children: Vec<ProcessStats>,
|
||||||
|
}
|
||||||
|
|
||||||
|
/// Stats for an individual process
|
||||||
|
#[derive(Debug, Clone, Serialize, Deserialize)]
|
||||||
|
pub struct ProcessStats {
|
||||||
|
/// Process ID
|
||||||
|
pub pid: i32,
|
||||||
|
|
||||||
|
/// Memory usage in bytes
|
||||||
|
pub memory_usage: u64,
|
||||||
|
|
||||||
|
/// CPU usage as a percentage (0-100)
|
||||||
|
pub cpu_usage: f32,
|
||||||
|
}
|
||||||
|
|
||||||
|
/// Extension trait for WaitStatus to check if a process exited successfully
|
||||||
|
pub trait WaitStatusExt {
|
||||||
|
fn success(&self) -> bool;
|
||||||
|
}
|
||||||
|
|
||||||
|
impl WaitStatusExt for WaitStatus {
|
||||||
|
fn success(&self) -> bool {
|
||||||
|
matches!(self, WaitStatus::Exited(_, code) if *code == 0)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
/// Type alias for a service table mapping service names to service instances
|
||||||
|
pub type ServiceTable = HashMap<String, Arc<RwLock<crate::zinit::service::ZInitService>>>;
|
||||||
|
|
||||||
|
/// Type alias for a watch stream
|
||||||
|
pub type Watcher<T> = WatchStream<Arc<T>>;
|
||||||
|
|
||||||
|
/// A wrapper around a value that can be watched for changes
|
||||||
|
pub struct Watched<T> {
|
||||||
|
v: Arc<T>,
|
||||||
|
tx: watch::Sender<Arc<T>>,
|
||||||
|
}
|
||||||
|
|
||||||
|
impl<T> Watched<T>
|
||||||
|
where
|
||||||
|
T: Send + Sync + 'static,
|
||||||
|
{
|
||||||
|
/// Create a new watched value
|
||||||
|
pub fn new(v: T) -> Self {
|
||||||
|
let v = Arc::new(v);
|
||||||
|
let (tx, _) = watch::channel(Arc::clone(&v));
|
||||||
|
Self { v, tx }
|
||||||
|
}
|
||||||
|
|
||||||
|
/// Set the value and notify watchers
|
||||||
|
pub fn set(&mut self, v: T) {
|
||||||
|
let v = Arc::new(v);
|
||||||
|
self.v = Arc::clone(&v);
|
||||||
|
// update the value even when there are no receivers
|
||||||
|
self.tx.send_replace(v);
|
||||||
|
}
|
||||||
|
|
||||||
|
/// Get a reference to the current value
|
||||||
|
pub fn get(&self) -> &T {
|
||||||
|
&self.v
|
||||||
|
}
|
||||||
|
|
||||||
|
/// Create a watcher for this value
|
||||||
|
pub fn watcher(&self) -> Watcher<T> {
|
||||||
|
WatchStream::new(self.tx.subscribe())
|
||||||
|
}
|
||||||
|
}
|
||||||
42
components/zinit/stop.sh
Executable file
42
components/zinit/stop.sh
Executable file
@@ -0,0 +1,42 @@
|
|||||||
|
#!/bin/bash
|
||||||
|
set -e
|
||||||
|
|
||||||
|
# Colors for output
|
||||||
|
RED='\033[0;31m'
|
||||||
|
GREEN='\033[0;32m'
|
||||||
|
YELLOW='\033[0;33m'
|
||||||
|
NC='\033[0m' # No Color
|
||||||
|
|
||||||
|
echo -e "${GREEN}Stopping zinit...${NC}"
|
||||||
|
|
||||||
|
# Function to check if zinit is running
|
||||||
|
is_zinit_running() {
|
||||||
|
pgrep -f "zinit" > /dev/null
|
||||||
|
return $?
|
||||||
|
}
|
||||||
|
|
||||||
|
# Try to shutdown zinit gracefully if it's running
|
||||||
|
if is_zinit_running; then
|
||||||
|
echo -e "${YELLOW}Zinit is already running. Attempting graceful shutdown...${NC}"
|
||||||
|
zinit shutdown || true
|
||||||
|
|
||||||
|
# Give it a moment to shut down
|
||||||
|
sleep 2
|
||||||
|
|
||||||
|
# Check if it's still running
|
||||||
|
if is_zinit_running; then
|
||||||
|
echo -e "${YELLOW}Zinit is still running. Attempting to kill the process...${NC}"
|
||||||
|
pkill -f "zinit$" || true
|
||||||
|
sleep 1
|
||||||
|
fi
|
||||||
|
else
|
||||||
|
echo -e "${YELLOW}No existing zinit process found.${NC}"
|
||||||
|
fi
|
||||||
|
|
||||||
|
# Double-check no zinit is running
|
||||||
|
if is_zinit_running; then
|
||||||
|
echo -e "${RED}Warning: Could not terminate existing zinit process. You may need to manually kill it.${NC}"
|
||||||
|
ps aux | grep "zinit" | grep -v grep
|
||||||
|
else
|
||||||
|
echo -e "${GREEN}No zinit process is running. Ready to start a new instance.${NC}"
|
||||||
|
fi
|
||||||
26
components/zinit/zinit-client/Cargo.toml
Normal file
26
components/zinit/zinit-client/Cargo.toml
Normal file
@@ -0,0 +1,26 @@
|
|||||||
|
[package]
|
||||||
|
name = "zinit-client"
|
||||||
|
version = "0.1.0"
|
||||||
|
edition = "2021"
|
||||||
|
description = "A client library for interacting with Zinit process manager"
|
||||||
|
license = "Apache 2.0"
|
||||||
|
authors = ["ThreeFold Tech, https://github.com/threefoldtech"]
|
||||||
|
|
||||||
|
[dependencies]
|
||||||
|
anyhow = "1.0"
|
||||||
|
async-trait = "0.1.88"
|
||||||
|
jsonrpsee = { version = "0.25.1", features = ["macros", "http-client", "ws-client"] }
|
||||||
|
reth-ipc = { git = "https://github.com/paradigmxyz/reth", package = "reth-ipc" }
|
||||||
|
tokio = { version = "1.14.0", features = ["full"] }
|
||||||
|
serde = { version = "1.0", features = ["derive"] }
|
||||||
|
serde_json = "1.0"
|
||||||
|
thiserror = "1.0"
|
||||||
|
log = "0.4"
|
||||||
|
|
||||||
|
[[example]]
|
||||||
|
name = "basic_usage"
|
||||||
|
path = "examples/basic_usage.rs"
|
||||||
|
|
||||||
|
[[example]]
|
||||||
|
name = "http_client"
|
||||||
|
path = "examples/http_client.rs"
|
||||||
123
components/zinit/zinit-client/README.md
Normal file
123
components/zinit/zinit-client/README.md
Normal file
@@ -0,0 +1,123 @@
|
|||||||
|
# Zinit Client Library
|
||||||
|
|
||||||
|
A simple Rust client library for interacting with the Zinit process manager.
|
||||||
|
|
||||||
|
## Features
|
||||||
|
|
||||||
|
- Connect to Zinit via Unix socket or HTTP
|
||||||
|
- Manage services (start, stop, restart, monitor)
|
||||||
|
- Query service status and information
|
||||||
|
- Create and delete service configurations
|
||||||
|
- System operations (shutdown, reboot)
|
||||||
|
|
||||||
|
## Installation
|
||||||
|
|
||||||
|
Add this to your `Cargo.toml`:
|
||||||
|
|
||||||
|
```toml
|
||||||
|
[dependencies]
|
||||||
|
zinit-client = "0.1.0"
|
||||||
|
```
|
||||||
|
|
||||||
|
## Usage
|
||||||
|
|
||||||
|
### Creating a Client
|
||||||
|
|
||||||
|
You can create a client using either Unix socket or HTTP transport:
|
||||||
|
|
||||||
|
```rust
|
||||||
|
use zinit_client::Client;
|
||||||
|
|
||||||
|
// Using Unix socket (local only)
|
||||||
|
let client = Client::unix_socket("/var/run/zinit.sock");
|
||||||
|
|
||||||
|
// Using HTTP (works for remote Zinit instances)
|
||||||
|
let client = Client::http("http://localhost:8080");
|
||||||
|
```
|
||||||
|
|
||||||
|
### Service Management
|
||||||
|
|
||||||
|
```rust
|
||||||
|
// List all services
|
||||||
|
let services = client.list().await?;
|
||||||
|
for (name, state) in services {
|
||||||
|
println!("{}: {}", name, state);
|
||||||
|
}
|
||||||
|
|
||||||
|
// Get status of a specific service
|
||||||
|
let status = client.status("my-service").await?;
|
||||||
|
println!("PID: {}, State: {}", status.pid, status.state);
|
||||||
|
|
||||||
|
// Start a service
|
||||||
|
client.start("my-service").await?;
|
||||||
|
|
||||||
|
// Stop a service
|
||||||
|
client.stop("my-service").await?;
|
||||||
|
|
||||||
|
// Restart a service
|
||||||
|
client.restart("my-service").await?;
|
||||||
|
|
||||||
|
// Monitor a service
|
||||||
|
client.monitor("my-service").await?;
|
||||||
|
|
||||||
|
// Forget a service
|
||||||
|
client.forget("my-service").await?;
|
||||||
|
|
||||||
|
// Send a signal to a service
|
||||||
|
client.kill("my-service", "SIGTERM").await?;
|
||||||
|
```
|
||||||
|
|
||||||
|
### Service Configuration
|
||||||
|
|
||||||
|
```rust
|
||||||
|
use serde_json::json;
|
||||||
|
|
||||||
|
// Create a new service
|
||||||
|
let config = json!({
|
||||||
|
"exec": "nginx",
|
||||||
|
"oneshot": false,
|
||||||
|
"after": ["network"]
|
||||||
|
}).as_object().unwrap().clone();
|
||||||
|
|
||||||
|
client.create_service("nginx", config).await?;
|
||||||
|
|
||||||
|
// Get service configuration
|
||||||
|
let config = client.get_service("nginx").await?;
|
||||||
|
println!("Config: {:?}", config);
|
||||||
|
|
||||||
|
// Delete a service
|
||||||
|
client.delete_service("nginx").await?;
|
||||||
|
```
|
||||||
|
|
||||||
|
### System Operations
|
||||||
|
|
||||||
|
```rust
|
||||||
|
// Shutdown the system
|
||||||
|
client.shutdown().await?;
|
||||||
|
|
||||||
|
// Reboot the system
|
||||||
|
client.reboot().await?;
|
||||||
|
```
|
||||||
|
|
||||||
|
## Error Handling
|
||||||
|
|
||||||
|
The library provides a `ClientError` enum for handling errors:
|
||||||
|
|
||||||
|
```rust
|
||||||
|
match client.status("non-existent-service").await {
|
||||||
|
Ok(status) => println!("Service status: {}", status.state),
|
||||||
|
Err(e) => match e {
|
||||||
|
ClientError::ServiceNotFound(_) => println!("Service not found"),
|
||||||
|
ClientError::ConnectionError(_) => println!("Failed to connect to Zinit"),
|
||||||
|
_ => println!("Other error: {}", e),
|
||||||
|
},
|
||||||
|
}
|
||||||
|
```
|
||||||
|
|
||||||
|
## Examples
|
||||||
|
|
||||||
|
See the [examples](examples) directory for complete usage examples.
|
||||||
|
|
||||||
|
## License
|
||||||
|
|
||||||
|
This project is licensed under the MIT License.
|
||||||
50
components/zinit/zinit-client/examples/basic_usage.rs
Normal file
50
components/zinit/zinit-client/examples/basic_usage.rs
Normal file
@@ -0,0 +1,50 @@
|
|||||||
|
use anyhow::Result;
|
||||||
|
use zinit_client::Client;
|
||||||
|
|
||||||
|
#[tokio::main]
|
||||||
|
async fn main() -> Result<()> {
|
||||||
|
// Create a client using Unix socket transport
|
||||||
|
let client = Client::unix_socket("/var/run/zinit.sock").await?;
|
||||||
|
|
||||||
|
// List all services
|
||||||
|
let services = client.list().await?;
|
||||||
|
println!("Services:");
|
||||||
|
for (name, state) in services {
|
||||||
|
println!("{}: {}", name, state);
|
||||||
|
}
|
||||||
|
|
||||||
|
// Get a specific service status
|
||||||
|
let service_name = "example-service";
|
||||||
|
match client.status(service_name).await {
|
||||||
|
Ok(status) => {
|
||||||
|
println!("\nService: {}", status.name);
|
||||||
|
println!("PID: {}", status.pid);
|
||||||
|
println!("State: {}", status.state);
|
||||||
|
println!("Target: {}", status.target);
|
||||||
|
println!("After:");
|
||||||
|
for (dep, state) in status.after {
|
||||||
|
println!(" {}: {}", dep, state);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
Err(e) => eprintln!("Failed to get status: {}", e),
|
||||||
|
}
|
||||||
|
|
||||||
|
// Try to start a service
|
||||||
|
match client.start(service_name).await {
|
||||||
|
Ok(_) => println!("\nService started successfully"),
|
||||||
|
Err(e) => eprintln!("Failed to start service: {}", e),
|
||||||
|
}
|
||||||
|
|
||||||
|
// Get logs for the service
|
||||||
|
match client.logs(Some(service_name.to_string())).await {
|
||||||
|
Ok(logs) => {
|
||||||
|
println!("\nLogs:");
|
||||||
|
for log in logs {
|
||||||
|
println!("{}", log);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
Err(e) => eprintln!("Failed to get logs: {}", e),
|
||||||
|
}
|
||||||
|
|
||||||
|
Ok(())
|
||||||
|
}
|
||||||
78
components/zinit/zinit-client/examples/http_client.rs
Normal file
78
components/zinit/zinit-client/examples/http_client.rs
Normal file
@@ -0,0 +1,78 @@
|
|||||||
|
use anyhow::Result;
|
||||||
|
use serde_json::json;
|
||||||
|
use zinit_client::Client;
|
||||||
|
|
||||||
|
#[tokio::main]
|
||||||
|
async fn main() -> Result<()> {
|
||||||
|
// Create a client using HTTP transport
|
||||||
|
let client = Client::http("http://localhost:8080").await?;
|
||||||
|
|
||||||
|
// Create a new service
|
||||||
|
let service_name = "example-http-service";
|
||||||
|
let service_config = json!({
|
||||||
|
"exec": "echo 'Hello from HTTP service'",
|
||||||
|
"oneshot": true,
|
||||||
|
"after": ["network"]
|
||||||
|
})
|
||||||
|
.as_object()
|
||||||
|
.unwrap()
|
||||||
|
.clone();
|
||||||
|
|
||||||
|
match client.create_service(service_name, service_config).await {
|
||||||
|
Ok(msg) => println!("Service created: {}", msg),
|
||||||
|
Err(e) => eprintln!("Failed to create service: {}", e),
|
||||||
|
}
|
||||||
|
|
||||||
|
// Start the HTTP/RPC server on a specific address
|
||||||
|
match client.start_http_server("0.0.0.0:8081").await {
|
||||||
|
Ok(msg) => println!("HTTP server status: {}", msg),
|
||||||
|
Err(e) => eprintln!("Failed to start HTTP server: {}", e),
|
||||||
|
}
|
||||||
|
|
||||||
|
// List all services
|
||||||
|
let services = client.list().await?;
|
||||||
|
println!("\nServices:");
|
||||||
|
for (name, state) in services {
|
||||||
|
println!("{}: {}", name, state);
|
||||||
|
}
|
||||||
|
|
||||||
|
// Monitor the service
|
||||||
|
match client.monitor(service_name).await {
|
||||||
|
Ok(_) => println!("\nService is now monitored"),
|
||||||
|
Err(e) => eprintln!("Failed to monitor service: {}", e),
|
||||||
|
}
|
||||||
|
|
||||||
|
// Start the service
|
||||||
|
match client.start(service_name).await {
|
||||||
|
Ok(_) => println!("Service started successfully"),
|
||||||
|
Err(e) => eprintln!("Failed to start service: {}", e),
|
||||||
|
}
|
||||||
|
|
||||||
|
// Get logs
|
||||||
|
let logs = client.logs(Some(service_name.to_string())).await?;
|
||||||
|
println!("\nLogs:");
|
||||||
|
for log in logs {
|
||||||
|
println!("{}", log);
|
||||||
|
}
|
||||||
|
|
||||||
|
// Clean up - forget the service
|
||||||
|
println!("\nCleaning up...");
|
||||||
|
match client.forget(service_name).await {
|
||||||
|
Ok(_) => println!("Service has been forgotten"),
|
||||||
|
Err(e) => eprintln!("Failed to forget service: {}", e),
|
||||||
|
}
|
||||||
|
|
||||||
|
// Clean up - delete the service configuration
|
||||||
|
match client.delete_service(service_name).await {
|
||||||
|
Ok(msg) => println!("{}", msg),
|
||||||
|
Err(e) => eprintln!("Failed to delete service: {}", e),
|
||||||
|
}
|
||||||
|
|
||||||
|
// Stop the HTTP/RPC server
|
||||||
|
match client.stop_http_server().await {
|
||||||
|
Ok(_) => println!("HTTP server stopped"),
|
||||||
|
Err(e) => eprintln!("Failed to stop HTTP server: {}", e),
|
||||||
|
}
|
||||||
|
|
||||||
|
Ok(())
|
||||||
|
}
|
||||||
450
components/zinit/zinit-client/src/lib.rs
Normal file
450
components/zinit/zinit-client/src/lib.rs
Normal file
@@ -0,0 +1,450 @@
|
|||||||
|
//! A client library for interacting with the Zinit process manager.
|
||||||
|
//!
|
||||||
|
//! This library provides a simple API for communicating with a Zinit daemon
|
||||||
|
//! via either Unix socket (using reth-ipc) or HTTP (using jsonrpsee).
|
||||||
|
use jsonrpsee::core::client::ClientT;
|
||||||
|
use jsonrpsee::core::client::Error as RpcError;
|
||||||
|
use jsonrpsee::http_client::{HttpClient, HttpClientBuilder};
|
||||||
|
use jsonrpsee::rpc_params;
|
||||||
|
use reth_ipc::client::IpcClientBuilder;
|
||||||
|
use serde::{Deserialize, Serialize};
|
||||||
|
use serde_json::{Map, Value};
|
||||||
|
use std::collections::HashMap;
|
||||||
|
use thiserror::Error;
|
||||||
|
|
||||||
|
/// Error type for client operations
|
||||||
|
#[derive(Error, Debug)]
|
||||||
|
pub enum ClientError {
|
||||||
|
#[error("connection error: {0}")]
|
||||||
|
ConnectionError(String),
|
||||||
|
|
||||||
|
#[error("service not found: {0}")]
|
||||||
|
ServiceNotFound(String),
|
||||||
|
|
||||||
|
#[error("service is already up: {0}")]
|
||||||
|
ServiceIsUp(String),
|
||||||
|
|
||||||
|
#[error("system is shutting down")]
|
||||||
|
ShuttingDown,
|
||||||
|
|
||||||
|
#[error("service already exists: {0}")]
|
||||||
|
ServiceAlreadyExists(String),
|
||||||
|
|
||||||
|
#[error("service file error: {0}")]
|
||||||
|
ServiceFileError(String),
|
||||||
|
|
||||||
|
#[error("rpc error: {0}")]
|
||||||
|
RpcError(String),
|
||||||
|
|
||||||
|
#[error("unknown error: {0}")]
|
||||||
|
UnknownError(String),
|
||||||
|
}
|
||||||
|
|
||||||
|
impl From<RpcError> for ClientError {
|
||||||
|
fn from(err: RpcError) -> Self {
|
||||||
|
// Parse the error code if available
|
||||||
|
if let RpcError::Call(err) = &err {
|
||||||
|
match err.code() {
|
||||||
|
-32000 => return ClientError::ServiceNotFound(err.message().to_string()),
|
||||||
|
-32002 => return ClientError::ServiceIsUp(err.message().to_string()),
|
||||||
|
-32006 => return ClientError::ShuttingDown,
|
||||||
|
-32007 => return ClientError::ServiceAlreadyExists(err.message().to_string()),
|
||||||
|
-32008 => return ClientError::ServiceFileError(err.message().to_string()),
|
||||||
|
_ => {}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
match err {
|
||||||
|
RpcError::Transport(_) => ClientError::ConnectionError(err.to_string()),
|
||||||
|
_ => ClientError::RpcError(err.to_string()),
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
/// Service status information
|
||||||
|
#[derive(Debug, Clone, Deserialize, Serialize)]
|
||||||
|
pub struct Status {
|
||||||
|
pub name: String,
|
||||||
|
pub pid: u32,
|
||||||
|
pub state: String,
|
||||||
|
pub target: String,
|
||||||
|
pub after: HashMap<String, String>,
|
||||||
|
}
|
||||||
|
|
||||||
|
/// Child process stats information
|
||||||
|
#[derive(Debug, Clone, Deserialize, Serialize)]
|
||||||
|
pub struct ChildStats {
|
||||||
|
pub pid: u32,
|
||||||
|
pub memory_usage: u64,
|
||||||
|
pub cpu_usage: f32,
|
||||||
|
}
|
||||||
|
|
||||||
|
/// Service stats information
|
||||||
|
#[derive(Debug, Clone, Deserialize, Serialize)]
|
||||||
|
pub struct Stats {
|
||||||
|
pub name: String,
|
||||||
|
pub pid: u32,
|
||||||
|
pub memory_usage: u64,
|
||||||
|
pub cpu_usage: f32,
|
||||||
|
pub children: Vec<ChildStats>,
|
||||||
|
}
|
||||||
|
|
||||||
|
/// Client implementation for communicating with Zinit
|
||||||
|
pub enum Client {
|
||||||
|
Ipc(String), // Socket path
|
||||||
|
Http(HttpClient),
|
||||||
|
}
|
||||||
|
|
||||||
|
impl Client {
|
||||||
|
/// Create a new client using Unix socket transport
|
||||||
|
pub async fn unix_socket<P: AsRef<std::path::Path>>(path: P) -> Result<Self, ClientError> {
|
||||||
|
Ok(Client::Ipc(path.as_ref().to_string_lossy().to_string()))
|
||||||
|
}
|
||||||
|
|
||||||
|
/// Create a new client using HTTP transport
|
||||||
|
pub async fn http<S: AsRef<str>>(url: S) -> Result<Self, ClientError> {
|
||||||
|
let client = HttpClientBuilder::default()
|
||||||
|
.build(url.as_ref())
|
||||||
|
.map_err(|e| ClientError::ConnectionError(e.to_string()))?;
|
||||||
|
|
||||||
|
Ok(Client::Http(client))
|
||||||
|
}
|
||||||
|
|
||||||
|
// Helper to get IPC client
|
||||||
|
async fn get_ipc_client(&self) -> Result<impl ClientT, ClientError> {
|
||||||
|
match self {
|
||||||
|
Client::Ipc(path) => IpcClientBuilder::default()
|
||||||
|
.build(path)
|
||||||
|
.await
|
||||||
|
.map_err(|e| ClientError::ConnectionError(e.to_string())),
|
||||||
|
_ => Err(ClientError::UnknownError("Not an IPC client".to_string())),
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
// Service API Methods
|
||||||
|
|
||||||
|
/// List all monitored services and their current state
|
||||||
|
pub async fn list(&self) -> Result<HashMap<String, String>, ClientError> {
|
||||||
|
match self {
|
||||||
|
Client::Ipc(_) => {
|
||||||
|
let client = self.get_ipc_client().await?;
|
||||||
|
client
|
||||||
|
.request("service_list", rpc_params![])
|
||||||
|
.await
|
||||||
|
.map_err(Into::into)
|
||||||
|
}
|
||||||
|
Client::Http(client) => client
|
||||||
|
.request("service_list", rpc_params![])
|
||||||
|
.await
|
||||||
|
.map_err(Into::into),
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
/// Get the detailed status of a specific service
|
||||||
|
pub async fn status(&self, name: impl AsRef<str>) -> Result<Status, ClientError> {
|
||||||
|
let name = name.as_ref().to_string();
|
||||||
|
match self {
|
||||||
|
Client::Ipc(_) => {
|
||||||
|
let client = self.get_ipc_client().await?;
|
||||||
|
client
|
||||||
|
.request("service_status", rpc_params![name])
|
||||||
|
.await
|
||||||
|
.map_err(Into::into)
|
||||||
|
}
|
||||||
|
Client::Http(client) => client
|
||||||
|
.request("service_status", rpc_params![name])
|
||||||
|
.await
|
||||||
|
.map_err(Into::into),
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
/// Start a specific service
|
||||||
|
pub async fn start(&self, name: impl AsRef<str>) -> Result<(), ClientError> {
|
||||||
|
let name = name.as_ref().to_string();
|
||||||
|
match self {
|
||||||
|
Client::Ipc(_) => {
|
||||||
|
let client = self.get_ipc_client().await?;
|
||||||
|
client
|
||||||
|
.request("service_start", rpc_params![name])
|
||||||
|
.await
|
||||||
|
.map_err(Into::into)
|
||||||
|
}
|
||||||
|
Client::Http(client) => client
|
||||||
|
.request("service_start", rpc_params![name])
|
||||||
|
.await
|
||||||
|
.map_err(Into::into),
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
/// Stop a specific service
|
||||||
|
pub async fn stop(&self, name: impl AsRef<str>) -> Result<(), ClientError> {
|
||||||
|
let name = name.as_ref().to_string();
|
||||||
|
match self {
|
||||||
|
Client::Ipc(_) => {
|
||||||
|
let client = self.get_ipc_client().await?;
|
||||||
|
client
|
||||||
|
.request("service_stop", rpc_params![name])
|
||||||
|
.await
|
||||||
|
.map_err(Into::into)
|
||||||
|
}
|
||||||
|
Client::Http(client) => client
|
||||||
|
.request("service_stop", rpc_params![name])
|
||||||
|
.await
|
||||||
|
.map_err(Into::into),
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
/// Restart a service
|
||||||
|
pub async fn restart(&self, name: impl AsRef<str>) -> Result<(), ClientError> {
|
||||||
|
let name = name.as_ref().to_string();
|
||||||
|
// First stop the service
|
||||||
|
self.stop(&name).await?;
|
||||||
|
|
||||||
|
// Poll the service status until it's stopped
|
||||||
|
for _ in 0..20 {
|
||||||
|
let status = self.status(&name).await?;
|
||||||
|
if status.pid == 0 && status.target == "Down" {
|
||||||
|
return self.start(&name).await;
|
||||||
|
}
|
||||||
|
tokio::time::sleep(std::time::Duration::from_secs(1)).await;
|
||||||
|
}
|
||||||
|
|
||||||
|
// Process not stopped, try to kill it
|
||||||
|
self.kill(&name, "SIGKILL").await?;
|
||||||
|
self.start(&name).await
|
||||||
|
}
|
||||||
|
|
||||||
|
/// Load and monitor a new service from its configuration file
|
||||||
|
pub async fn monitor(&self, name: impl AsRef<str>) -> Result<(), ClientError> {
|
||||||
|
let name = name.as_ref().to_string();
|
||||||
|
match self {
|
||||||
|
Client::Ipc(_) => {
|
||||||
|
let client = self.get_ipc_client().await?;
|
||||||
|
client
|
||||||
|
.request("service_monitor", rpc_params![name])
|
||||||
|
.await
|
||||||
|
.map_err(Into::into)
|
||||||
|
}
|
||||||
|
Client::Http(client) => client
|
||||||
|
.request("service_monitor", rpc_params![name])
|
||||||
|
.await
|
||||||
|
.map_err(Into::into),
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
/// Stop monitoring a service and remove it from management
|
||||||
|
pub async fn forget(&self, name: impl AsRef<str>) -> Result<(), ClientError> {
|
||||||
|
let name = name.as_ref().to_string();
|
||||||
|
match self {
|
||||||
|
Client::Ipc(_) => {
|
||||||
|
let client = self.get_ipc_client().await?;
|
||||||
|
client
|
||||||
|
.request("service_forget", rpc_params![name])
|
||||||
|
.await
|
||||||
|
.map_err(Into::into)
|
||||||
|
}
|
||||||
|
Client::Http(client) => client
|
||||||
|
.request("service_forget", rpc_params![name])
|
||||||
|
.await
|
||||||
|
.map_err(Into::into),
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
/// Send a signal to a specific service process
|
||||||
|
pub async fn kill(
|
||||||
|
&self,
|
||||||
|
name: impl AsRef<str>,
|
||||||
|
signal: impl AsRef<str>,
|
||||||
|
) -> Result<(), ClientError> {
|
||||||
|
let name = name.as_ref().to_string();
|
||||||
|
let signal = signal.as_ref().to_string();
|
||||||
|
match self {
|
||||||
|
Client::Ipc(_) => {
|
||||||
|
let client = self.get_ipc_client().await?;
|
||||||
|
client
|
||||||
|
.request("service_kill", rpc_params![name, signal])
|
||||||
|
.await
|
||||||
|
.map_err(Into::into)
|
||||||
|
}
|
||||||
|
Client::Http(client) => client
|
||||||
|
.request("service_kill", rpc_params![name, signal])
|
||||||
|
.await
|
||||||
|
.map_err(Into::into),
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
/// Create a new service configuration
|
||||||
|
pub async fn create_service(
|
||||||
|
&self,
|
||||||
|
name: impl AsRef<str>,
|
||||||
|
content: Map<String, Value>,
|
||||||
|
) -> Result<String, ClientError> {
|
||||||
|
let name = name.as_ref().to_string();
|
||||||
|
match self {
|
||||||
|
Client::Ipc(_) => {
|
||||||
|
let client = self.get_ipc_client().await?;
|
||||||
|
client
|
||||||
|
.request("service_create", rpc_params![name, content])
|
||||||
|
.await
|
||||||
|
.map_err(Into::into)
|
||||||
|
}
|
||||||
|
Client::Http(client) => client
|
||||||
|
.request("service_create", rpc_params![name, content])
|
||||||
|
.await
|
||||||
|
.map_err(Into::into),
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
/// Delete a service configuration
|
||||||
|
pub async fn delete_service(&self, name: impl AsRef<str>) -> Result<String, ClientError> {
|
||||||
|
let name = name.as_ref().to_string();
|
||||||
|
match self {
|
||||||
|
Client::Ipc(_) => {
|
||||||
|
let client = self.get_ipc_client().await?;
|
||||||
|
client
|
||||||
|
.request("service_delete", rpc_params![name])
|
||||||
|
.await
|
||||||
|
.map_err(Into::into)
|
||||||
|
}
|
||||||
|
Client::Http(client) => client
|
||||||
|
.request("service_delete", rpc_params![name])
|
||||||
|
.await
|
||||||
|
.map_err(Into::into),
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
/// Get a service configuration
|
||||||
|
pub async fn get_service(&self, name: impl AsRef<str>) -> Result<Value, ClientError> {
|
||||||
|
let name = name.as_ref().to_string();
|
||||||
|
match self {
|
||||||
|
Client::Ipc(_) => {
|
||||||
|
let client = self.get_ipc_client().await?;
|
||||||
|
client
|
||||||
|
.request("service_get", rpc_params![name])
|
||||||
|
.await
|
||||||
|
.map_err(Into::into)
|
||||||
|
}
|
||||||
|
Client::Http(client) => client
|
||||||
|
.request("service_get", rpc_params![name])
|
||||||
|
.await
|
||||||
|
.map_err(Into::into),
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
/// Get memory and CPU usage statistics for a service
|
||||||
|
pub async fn stats(&self, name: impl AsRef<str>) -> Result<Stats, ClientError> {
|
||||||
|
let name = name.as_ref().to_string();
|
||||||
|
match self {
|
||||||
|
Client::Ipc(_) => {
|
||||||
|
let client = self.get_ipc_client().await?;
|
||||||
|
client
|
||||||
|
.request("service_stats", rpc_params![name])
|
||||||
|
.await
|
||||||
|
.map_err(Into::into)
|
||||||
|
}
|
||||||
|
Client::Http(client) => client
|
||||||
|
.request("service_stats", rpc_params![name])
|
||||||
|
.await
|
||||||
|
.map_err(Into::into),
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
// System API Methods
|
||||||
|
|
||||||
|
/// Initiate system shutdown
|
||||||
|
pub async fn shutdown(&self) -> Result<(), ClientError> {
|
||||||
|
match self {
|
||||||
|
Client::Ipc(_) => {
|
||||||
|
let client = self.get_ipc_client().await?;
|
||||||
|
client
|
||||||
|
.request("system_shutdown", rpc_params![])
|
||||||
|
.await
|
||||||
|
.map_err(Into::into)
|
||||||
|
}
|
||||||
|
Client::Http(client) => client
|
||||||
|
.request("system_shutdown", rpc_params![])
|
||||||
|
.await
|
||||||
|
.map_err(Into::into),
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
/// Initiate system reboot
|
||||||
|
pub async fn reboot(&self) -> Result<(), ClientError> {
|
||||||
|
match self {
|
||||||
|
Client::Ipc(_) => {
|
||||||
|
let client = self.get_ipc_client().await?;
|
||||||
|
client
|
||||||
|
.request("system_reboot", rpc_params![])
|
||||||
|
.await
|
||||||
|
.map_err(Into::into)
|
||||||
|
}
|
||||||
|
Client::Http(client) => client
|
||||||
|
.request("system_reboot", rpc_params![])
|
||||||
|
.await
|
||||||
|
.map_err(Into::into),
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
/// Start HTTP/RPC server
|
||||||
|
pub async fn start_http_server(&self, address: impl AsRef<str>) -> Result<String, ClientError> {
|
||||||
|
let address = address.as_ref().to_string();
|
||||||
|
match self {
|
||||||
|
Client::Ipc(_) => {
|
||||||
|
let client = self.get_ipc_client().await?;
|
||||||
|
client
|
||||||
|
.request("system_start_http_server", rpc_params![address])
|
||||||
|
.await
|
||||||
|
.map_err(Into::into)
|
||||||
|
}
|
||||||
|
Client::Http(client) => client
|
||||||
|
.request("system_start_http_server", rpc_params![address])
|
||||||
|
.await
|
||||||
|
.map_err(Into::into),
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
/// Stop HTTP/RPC server
|
||||||
|
pub async fn stop_http_server(&self) -> Result<(), ClientError> {
|
||||||
|
match self {
|
||||||
|
Client::Ipc(_) => {
|
||||||
|
let client = self.get_ipc_client().await?;
|
||||||
|
client
|
||||||
|
.request("system_stop_http_server", rpc_params![])
|
||||||
|
.await
|
||||||
|
.map_err(Into::into)
|
||||||
|
}
|
||||||
|
Client::Http(client) => client
|
||||||
|
.request("system_stop_http_server", rpc_params![])
|
||||||
|
.await
|
||||||
|
.map_err(Into::into),
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
// Logging API Methods
|
||||||
|
|
||||||
|
/// Get current logs
|
||||||
|
pub async fn logs(&self, filter: Option<String>) -> Result<Vec<String>, ClientError> {
|
||||||
|
match self {
|
||||||
|
Client::Ipc(_) => {
|
||||||
|
let client = self.get_ipc_client().await?;
|
||||||
|
client
|
||||||
|
.request("stream_currentLogs", rpc_params![filter])
|
||||||
|
.await
|
||||||
|
.map_err(Into::into)
|
||||||
|
}
|
||||||
|
Client::Http(client) => client
|
||||||
|
.request("stream_currentLogs", rpc_params![filter])
|
||||||
|
.await
|
||||||
|
.map_err(Into::into),
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
/// Subscribe to logs
|
||||||
|
///
|
||||||
|
/// Note: This method is not fully implemented yet. For now, it will return an error.
|
||||||
|
pub async fn log_subscribe(&self, _filter: Option<String>) -> Result<(), ClientError> {
|
||||||
|
Err(ClientError::UnknownError(
|
||||||
|
"Log subscription not implemented yet".to_string(),
|
||||||
|
))
|
||||||
|
}
|
||||||
|
}
|
||||||
66
components/zinit/zinit-client/tests/integration_test.rs
Normal file
66
components/zinit/zinit-client/tests/integration_test.rs
Normal file
@@ -0,0 +1,66 @@
|
|||||||
|
use std::env;
|
||||||
|
use zinit_client::{Client, ClientError};
|
||||||
|
|
||||||
|
#[tokio::test]
|
||||||
|
async fn test_connection_error() {
|
||||||
|
// Try to connect to a non-existent socket
|
||||||
|
let result = Client::unix_socket("/non/existent/socket").await;
|
||||||
|
assert!(result.is_ok()); // Just creating the client succeeds
|
||||||
|
|
||||||
|
// Trying to make a request should fail
|
||||||
|
if let Ok(client) = result {
|
||||||
|
let list_result = client.list().await;
|
||||||
|
assert!(matches!(list_result, Err(ClientError::ConnectionError(_))));
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
#[tokio::test]
|
||||||
|
async fn test_http_connection_error() {
|
||||||
|
// Try to connect to a non-existent HTTP endpoint
|
||||||
|
let result = Client::http("http://localhost:12345").await;
|
||||||
|
// This should succeed as we're just creating the client, not making a request
|
||||||
|
assert!(result.is_ok());
|
||||||
|
|
||||||
|
// Try to make a request which should fail
|
||||||
|
if let Ok(client) = result {
|
||||||
|
let list_result = client.list().await;
|
||||||
|
assert!(matches!(list_result, Err(ClientError::ConnectionError(_))));
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
// This test only runs if ZINIT_SOCKET is set in the environment
|
||||||
|
// and points to a valid Zinit socket
|
||||||
|
#[tokio::test]
|
||||||
|
#[ignore]
|
||||||
|
async fn test_live_connection() {
|
||||||
|
let socket_path = match env::var("ZINIT_SOCKET") {
|
||||||
|
Ok(path) => path,
|
||||||
|
Err(_) => {
|
||||||
|
println!("ZINIT_SOCKET not set, skipping live test");
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
};
|
||||||
|
|
||||||
|
let client = match Client::unix_socket(&socket_path).await {
|
||||||
|
Ok(client) => client,
|
||||||
|
Err(e) => {
|
||||||
|
panic!(
|
||||||
|
"Failed to connect to Zinit socket at {}: {}",
|
||||||
|
socket_path, e
|
||||||
|
);
|
||||||
|
}
|
||||||
|
};
|
||||||
|
|
||||||
|
// Test listing services
|
||||||
|
let services = client.list().await.expect("Failed to list services");
|
||||||
|
println!("Found {} services", services.len());
|
||||||
|
|
||||||
|
// If there are services, test getting status of the first one
|
||||||
|
if let Some((service_name, _)) = services.iter().next() {
|
||||||
|
let status = client
|
||||||
|
.status(service_name)
|
||||||
|
.await
|
||||||
|
.expect("Failed to get service status");
|
||||||
|
println!("Service {} has PID {}", service_name, status.pid);
|
||||||
|
}
|
||||||
|
}
|
||||||
873
components/zinit/zinit.json
Normal file
873
components/zinit/zinit.json
Normal file
@@ -0,0 +1,873 @@
|
|||||||
|
{
|
||||||
|
"openrpc": "1.2.6",
|
||||||
|
"info": {
|
||||||
|
"version": "1.0.0",
|
||||||
|
"title": "Zinit JSON-RPC API",
|
||||||
|
"description": "JSON-RPC 2.0 API for controlling and querying Zinit services",
|
||||||
|
"license": {
|
||||||
|
"name": "MIT"
|
||||||
|
}
|
||||||
|
},
|
||||||
|
"servers": [
|
||||||
|
{
|
||||||
|
"name": "Unix Socket",
|
||||||
|
"url": "unix:///var/run/zinit.sock"
|
||||||
|
}
|
||||||
|
],
|
||||||
|
"methods": [
|
||||||
|
{
|
||||||
|
"name": "rpc_discover",
|
||||||
|
"description": "Returns the OpenRPC specification for the API",
|
||||||
|
"params": [],
|
||||||
|
"result": {
|
||||||
|
"name": "OpenRPCSpec",
|
||||||
|
"description": "The OpenRPC specification",
|
||||||
|
"schema": {
|
||||||
|
"type": "object"
|
||||||
|
}
|
||||||
|
},
|
||||||
|
"examples": [
|
||||||
|
{
|
||||||
|
"name": "Get API specification",
|
||||||
|
"params": [],
|
||||||
|
"result": {
|
||||||
|
"name": "OpenRPCSpecResult",
|
||||||
|
"value": {
|
||||||
|
"openrpc": "1.2.6",
|
||||||
|
"info": {
|
||||||
|
"version": "1.0.0",
|
||||||
|
"title": "Zinit JSON-RPC API"
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
]
|
||||||
|
},
|
||||||
|
{
|
||||||
|
"name": "service_list",
|
||||||
|
"description": "Lists all services managed by Zinit",
|
||||||
|
"params": [],
|
||||||
|
"result": {
|
||||||
|
"name": "ServiceList",
|
||||||
|
"description": "A map of service names to their current states",
|
||||||
|
"schema": {
|
||||||
|
"type": "object",
|
||||||
|
"additionalProperties": {
|
||||||
|
"type": "string",
|
||||||
|
"description": "Service state (Running, Success, Error, etc.)"
|
||||||
|
}
|
||||||
|
}
|
||||||
|
},
|
||||||
|
"examples": [
|
||||||
|
{
|
||||||
|
"name": "List all services",
|
||||||
|
"params": [],
|
||||||
|
"result": {
|
||||||
|
"name": "ServiceListResult",
|
||||||
|
"value": {
|
||||||
|
"service1": "Running",
|
||||||
|
"service2": "Success",
|
||||||
|
"service3": "Error"
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
]
|
||||||
|
},
|
||||||
|
{
|
||||||
|
"name": "service_status",
|
||||||
|
"description": "Shows detailed status information for a specific service",
|
||||||
|
"params": [
|
||||||
|
{
|
||||||
|
"name": "name",
|
||||||
|
"description": "The name of the service",
|
||||||
|
"required": true,
|
||||||
|
"schema": {
|
||||||
|
"type": "string"
|
||||||
|
}
|
||||||
|
}
|
||||||
|
],
|
||||||
|
"result": {
|
||||||
|
"name": "ServiceStatus",
|
||||||
|
"description": "Detailed status information for the service",
|
||||||
|
"schema": {
|
||||||
|
"type": "object",
|
||||||
|
"properties": {
|
||||||
|
"name": {
|
||||||
|
"type": "string",
|
||||||
|
"description": "Service name"
|
||||||
|
},
|
||||||
|
"pid": {
|
||||||
|
"type": "integer",
|
||||||
|
"description": "Process ID of the running service (if running)"
|
||||||
|
},
|
||||||
|
"state": {
|
||||||
|
"type": "string",
|
||||||
|
"description": "Current state of the service (Running, Success, Error, etc.)"
|
||||||
|
},
|
||||||
|
"target": {
|
||||||
|
"type": "string",
|
||||||
|
"description": "Target state of the service (Up, Down)"
|
||||||
|
},
|
||||||
|
"after": {
|
||||||
|
"type": "object",
|
||||||
|
"description": "Dependencies of the service and their states",
|
||||||
|
"additionalProperties": {
|
||||||
|
"type": "string",
|
||||||
|
"description": "State of the dependency"
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
},
|
||||||
|
"examples": [
|
||||||
|
{
|
||||||
|
"name": "Get status of redis service",
|
||||||
|
"params": [
|
||||||
|
{
|
||||||
|
"name": "name",
|
||||||
|
"value": "redis"
|
||||||
|
}
|
||||||
|
],
|
||||||
|
"result": {
|
||||||
|
"name": "ServiceStatusResult",
|
||||||
|
"value": {
|
||||||
|
"name": "redis",
|
||||||
|
"pid": 1234,
|
||||||
|
"state": "Running",
|
||||||
|
"target": "Up",
|
||||||
|
"after": {
|
||||||
|
"dependency1": "Success",
|
||||||
|
"dependency2": "Running"
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
],
|
||||||
|
"errors": [
|
||||||
|
{
|
||||||
|
"code": -32000,
|
||||||
|
"message": "Service not found",
|
||||||
|
"data": "service name \"unknown\" unknown"
|
||||||
|
}
|
||||||
|
]
|
||||||
|
},
|
||||||
|
{
|
||||||
|
"name": "service_start",
|
||||||
|
"description": "Starts a service",
|
||||||
|
"params": [
|
||||||
|
{
|
||||||
|
"name": "name",
|
||||||
|
"description": "The name of the service to start",
|
||||||
|
"required": true,
|
||||||
|
"schema": {
|
||||||
|
"type": "string"
|
||||||
|
}
|
||||||
|
}
|
||||||
|
],
|
||||||
|
"result": {
|
||||||
|
"name": "StartResult",
|
||||||
|
"description": "Result of the start operation",
|
||||||
|
"schema": {
|
||||||
|
"type": "null"
|
||||||
|
}
|
||||||
|
},
|
||||||
|
"examples": [
|
||||||
|
{
|
||||||
|
"name": "Start redis service",
|
||||||
|
"params": [
|
||||||
|
{
|
||||||
|
"name": "name",
|
||||||
|
"value": "redis"
|
||||||
|
}
|
||||||
|
],
|
||||||
|
"result": {
|
||||||
|
"name": "StartResult",
|
||||||
|
"value": null
|
||||||
|
}
|
||||||
|
}
|
||||||
|
],
|
||||||
|
"errors": [
|
||||||
|
{
|
||||||
|
"code": -32000,
|
||||||
|
"message": "Service not found",
|
||||||
|
"data": "service name \"unknown\" unknown"
|
||||||
|
}
|
||||||
|
]
|
||||||
|
},
|
||||||
|
{
|
||||||
|
"name": "service_stop",
|
||||||
|
"description": "Stops a service",
|
||||||
|
"params": [
|
||||||
|
{
|
||||||
|
"name": "name",
|
||||||
|
"description": "The name of the service to stop",
|
||||||
|
"required": true,
|
||||||
|
"schema": {
|
||||||
|
"type": "string"
|
||||||
|
}
|
||||||
|
}
|
||||||
|
],
|
||||||
|
"result": {
|
||||||
|
"name": "StopResult",
|
||||||
|
"description": "Result of the stop operation",
|
||||||
|
"schema": {
|
||||||
|
"type": "null"
|
||||||
|
}
|
||||||
|
},
|
||||||
|
"examples": [
|
||||||
|
{
|
||||||
|
"name": "Stop redis service",
|
||||||
|
"params": [
|
||||||
|
{
|
||||||
|
"name": "name",
|
||||||
|
"value": "redis"
|
||||||
|
}
|
||||||
|
],
|
||||||
|
"result": {
|
||||||
|
"name": "StopResult",
|
||||||
|
"value": null
|
||||||
|
}
|
||||||
|
}
|
||||||
|
],
|
||||||
|
"errors": [
|
||||||
|
{
|
||||||
|
"code": -32000,
|
||||||
|
"message": "Service not found",
|
||||||
|
"data": "service name \"unknown\" unknown"
|
||||||
|
},
|
||||||
|
{
|
||||||
|
"code": -32003,
|
||||||
|
"message": "Service is down",
|
||||||
|
"data": "service \"redis\" is down"
|
||||||
|
}
|
||||||
|
]
|
||||||
|
},
|
||||||
|
{
|
||||||
|
"name": "service_monitor",
|
||||||
|
"description": "Starts monitoring a service. The service configuration is loaded from the config directory.",
|
||||||
|
"params": [
|
||||||
|
{
|
||||||
|
"name": "name",
|
||||||
|
"description": "The name of the service to monitor",
|
||||||
|
"required": true,
|
||||||
|
"schema": {
|
||||||
|
"type": "string"
|
||||||
|
}
|
||||||
|
}
|
||||||
|
],
|
||||||
|
"result": {
|
||||||
|
"name": "MonitorResult",
|
||||||
|
"description": "Result of the monitor operation",
|
||||||
|
"schema": {
|
||||||
|
"type": "null"
|
||||||
|
}
|
||||||
|
},
|
||||||
|
"examples": [
|
||||||
|
{
|
||||||
|
"name": "Monitor redis service",
|
||||||
|
"params": [
|
||||||
|
{
|
||||||
|
"name": "name",
|
||||||
|
"value": "redis"
|
||||||
|
}
|
||||||
|
],
|
||||||
|
"result": {
|
||||||
|
"name": "MonitorResult",
|
||||||
|
"value": null
|
||||||
|
}
|
||||||
|
}
|
||||||
|
],
|
||||||
|
"errors": [
|
||||||
|
{
|
||||||
|
"code": -32001,
|
||||||
|
"message": "Service already monitored",
|
||||||
|
"data": "service \"redis\" already monitored"
|
||||||
|
},
|
||||||
|
{
|
||||||
|
"code": -32005,
|
||||||
|
"message": "Config error",
|
||||||
|
"data": "failed to load service configuration"
|
||||||
|
}
|
||||||
|
]
|
||||||
|
},
|
||||||
|
{
|
||||||
|
"name": "service_forget",
|
||||||
|
"description": "Stops monitoring a service. You can only forget a stopped service.",
|
||||||
|
"params": [
|
||||||
|
{
|
||||||
|
"name": "name",
|
||||||
|
"description": "The name of the service to forget",
|
||||||
|
"required": true,
|
||||||
|
"schema": {
|
||||||
|
"type": "string"
|
||||||
|
}
|
||||||
|
}
|
||||||
|
],
|
||||||
|
"result": {
|
||||||
|
"name": "ForgetResult",
|
||||||
|
"description": "Result of the forget operation",
|
||||||
|
"schema": {
|
||||||
|
"type": "null"
|
||||||
|
}
|
||||||
|
},
|
||||||
|
"examples": [
|
||||||
|
{
|
||||||
|
"name": "Forget redis service",
|
||||||
|
"params": [
|
||||||
|
{
|
||||||
|
"name": "name",
|
||||||
|
"value": "redis"
|
||||||
|
}
|
||||||
|
],
|
||||||
|
"result": {
|
||||||
|
"name": "ForgetResult",
|
||||||
|
"value": null
|
||||||
|
}
|
||||||
|
}
|
||||||
|
],
|
||||||
|
"errors": [
|
||||||
|
{
|
||||||
|
"code": -32000,
|
||||||
|
"message": "Service not found",
|
||||||
|
"data": "service name \"unknown\" unknown"
|
||||||
|
},
|
||||||
|
{
|
||||||
|
"code": -32002,
|
||||||
|
"message": "Service is up",
|
||||||
|
"data": "service \"redis\" is up"
|
||||||
|
}
|
||||||
|
]
|
||||||
|
},
|
||||||
|
{
|
||||||
|
"name": "service_kill",
|
||||||
|
"description": "Sends a signal to a running service",
|
||||||
|
"params": [
|
||||||
|
{
|
||||||
|
"name": "name",
|
||||||
|
"description": "The name of the service to send the signal to",
|
||||||
|
"required": true,
|
||||||
|
"schema": {
|
||||||
|
"type": "string"
|
||||||
|
}
|
||||||
|
},
|
||||||
|
{
|
||||||
|
"name": "signal",
|
||||||
|
"description": "The signal to send (e.g., SIGTERM, SIGKILL)",
|
||||||
|
"required": true,
|
||||||
|
"schema": {
|
||||||
|
"type": "string"
|
||||||
|
}
|
||||||
|
}
|
||||||
|
],
|
||||||
|
"result": {
|
||||||
|
"name": "KillResult",
|
||||||
|
"description": "Result of the kill operation",
|
||||||
|
"schema": {
|
||||||
|
"type": "null"
|
||||||
|
}
|
||||||
|
},
|
||||||
|
"examples": [
|
||||||
|
{
|
||||||
|
"name": "Send SIGTERM to redis service",
|
||||||
|
"params": [
|
||||||
|
{
|
||||||
|
"name": "name",
|
||||||
|
"value": "redis"
|
||||||
|
},
|
||||||
|
{
|
||||||
|
"name": "signal",
|
||||||
|
"value": "SIGTERM"
|
||||||
|
}
|
||||||
|
],
|
||||||
|
"result": {
|
||||||
|
"name": "KillResult",
|
||||||
|
"value": null
|
||||||
|
}
|
||||||
|
}
|
||||||
|
],
|
||||||
|
"errors": [
|
||||||
|
{
|
||||||
|
"code": -32000,
|
||||||
|
"message": "Service not found",
|
||||||
|
"data": "service name \"unknown\" unknown"
|
||||||
|
},
|
||||||
|
{
|
||||||
|
"code": -32003,
|
||||||
|
"message": "Service is down",
|
||||||
|
"data": "service \"redis\" is down"
|
||||||
|
},
|
||||||
|
{
|
||||||
|
"code": -32004,
|
||||||
|
"message": "Invalid signal",
|
||||||
|
"data": "invalid signal: INVALID"
|
||||||
|
}
|
||||||
|
]
|
||||||
|
},
|
||||||
|
{
|
||||||
|
"name": "system_shutdown",
|
||||||
|
"description": "Stops all services and powers off the system",
|
||||||
|
"params": [],
|
||||||
|
"result": {
|
||||||
|
"name": "ShutdownResult",
|
||||||
|
"description": "Result of the shutdown operation",
|
||||||
|
"schema": {
|
||||||
|
"type": "null"
|
||||||
|
}
|
||||||
|
},
|
||||||
|
"examples": [
|
||||||
|
{
|
||||||
|
"name": "Shutdown the system",
|
||||||
|
"params": [],
|
||||||
|
"result": {
|
||||||
|
"name": "ShutdownResult",
|
||||||
|
"value": null
|
||||||
|
}
|
||||||
|
}
|
||||||
|
],
|
||||||
|
"errors": [
|
||||||
|
{
|
||||||
|
"code": -32006,
|
||||||
|
"message": "Shutting down",
|
||||||
|
"data": "system is already shutting down"
|
||||||
|
}
|
||||||
|
]
|
||||||
|
},
|
||||||
|
{
|
||||||
|
"name": "system_reboot",
|
||||||
|
"description": "Stops all services and reboots the system",
|
||||||
|
"params": [],
|
||||||
|
"result": {
|
||||||
|
"name": "RebootResult",
|
||||||
|
"description": "Result of the reboot operation",
|
||||||
|
"schema": {
|
||||||
|
"type": "null"
|
||||||
|
}
|
||||||
|
},
|
||||||
|
"examples": [
|
||||||
|
{
|
||||||
|
"name": "Reboot the system",
|
||||||
|
"params": [],
|
||||||
|
"result": {
|
||||||
|
"name": "RebootResult",
|
||||||
|
"value": null
|
||||||
|
}
|
||||||
|
}
|
||||||
|
],
|
||||||
|
"errors": [
|
||||||
|
{
|
||||||
|
"code": -32006,
|
||||||
|
"message": "Shutting down",
|
||||||
|
"data": "system is already shutting down"
|
||||||
|
}
|
||||||
|
]
|
||||||
|
},
|
||||||
|
{
|
||||||
|
"name": "service_create",
|
||||||
|
"description": "Creates a new service configuration file",
|
||||||
|
"params": [
|
||||||
|
{
|
||||||
|
"name": "name",
|
||||||
|
"description": "The name of the service to create",
|
||||||
|
"required": true,
|
||||||
|
"schema": {
|
||||||
|
"type": "string"
|
||||||
|
}
|
||||||
|
},
|
||||||
|
{
|
||||||
|
"name": "content",
|
||||||
|
"description": "The service configuration content",
|
||||||
|
"required": true,
|
||||||
|
"schema": {
|
||||||
|
"type": "object",
|
||||||
|
"properties": {
|
||||||
|
"exec": {
|
||||||
|
"type": "string",
|
||||||
|
"description": "Command to run"
|
||||||
|
},
|
||||||
|
"oneshot": {
|
||||||
|
"type": "boolean",
|
||||||
|
"description": "Whether the service should be restarted"
|
||||||
|
},
|
||||||
|
"after": {
|
||||||
|
"type": "array",
|
||||||
|
"items": {
|
||||||
|
"type": "string"
|
||||||
|
},
|
||||||
|
"description": "Services that must be running before this one starts"
|
||||||
|
},
|
||||||
|
"log": {
|
||||||
|
"type": "string",
|
||||||
|
"enum": ["null", "ring", "stdout"],
|
||||||
|
"description": "How to handle service output"
|
||||||
|
},
|
||||||
|
"env": {
|
||||||
|
"type": "object",
|
||||||
|
"additionalProperties": {
|
||||||
|
"type": "string"
|
||||||
|
},
|
||||||
|
"description": "Environment variables for the service"
|
||||||
|
},
|
||||||
|
"shutdown_timeout": {
|
||||||
|
"type": "integer",
|
||||||
|
"description": "Maximum time to wait for service to stop during shutdown"
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
],
|
||||||
|
"result": {
|
||||||
|
"name": "CreateServiceResult",
|
||||||
|
"description": "Result of the create operation",
|
||||||
|
"schema": {
|
||||||
|
"type": "string"
|
||||||
|
}
|
||||||
|
},
|
||||||
|
"errors": [
|
||||||
|
{
|
||||||
|
"code": -32007,
|
||||||
|
"message": "Service already exists",
|
||||||
|
"data": "Service 'name' already exists"
|
||||||
|
},
|
||||||
|
{
|
||||||
|
"code": -32008,
|
||||||
|
"message": "Service file error",
|
||||||
|
"data": "Failed to create service file"
|
||||||
|
}
|
||||||
|
]
|
||||||
|
},
|
||||||
|
{
|
||||||
|
"name": "service_delete",
|
||||||
|
"description": "Deletes a service configuration file",
|
||||||
|
"params": [
|
||||||
|
{
|
||||||
|
"name": "name",
|
||||||
|
"description": "The name of the service to delete",
|
||||||
|
"required": true,
|
||||||
|
"schema": {
|
||||||
|
"type": "string"
|
||||||
|
}
|
||||||
|
}
|
||||||
|
],
|
||||||
|
"result": {
|
||||||
|
"name": "DeleteServiceResult",
|
||||||
|
"description": "Result of the delete operation",
|
||||||
|
"schema": {
|
||||||
|
"type": "string"
|
||||||
|
}
|
||||||
|
},
|
||||||
|
"errors": [
|
||||||
|
{
|
||||||
|
"code": -32000,
|
||||||
|
"message": "Service not found",
|
||||||
|
"data": "Service 'name' not found"
|
||||||
|
},
|
||||||
|
{
|
||||||
|
"code": -32008,
|
||||||
|
"message": "Service file error",
|
||||||
|
"data": "Failed to delete service file"
|
||||||
|
}
|
||||||
|
]
|
||||||
|
},
|
||||||
|
{
|
||||||
|
"name": "service_get",
|
||||||
|
"description": "Gets a service configuration file",
|
||||||
|
"params": [
|
||||||
|
{
|
||||||
|
"name": "name",
|
||||||
|
"description": "The name of the service to get",
|
||||||
|
"required": true,
|
||||||
|
"schema": {
|
||||||
|
"type": "string"
|
||||||
|
}
|
||||||
|
}
|
||||||
|
],
|
||||||
|
"result": {
|
||||||
|
"name": "GetServiceResult",
|
||||||
|
"description": "The service configuration",
|
||||||
|
"schema": {
|
||||||
|
"type": "object"
|
||||||
|
}
|
||||||
|
},
|
||||||
|
"errors": [
|
||||||
|
{
|
||||||
|
"code": -32000,
|
||||||
|
"message": "Service not found",
|
||||||
|
"data": "Service 'name' not found"
|
||||||
|
},
|
||||||
|
{
|
||||||
|
"code": -32008,
|
||||||
|
"message": "Service file error",
|
||||||
|
"data": "Failed to read service file"
|
||||||
|
}
|
||||||
|
]
|
||||||
|
},
|
||||||
|
{
|
||||||
|
"name": "service_stats",
|
||||||
|
"description": "Get memory and CPU usage statistics for a service",
|
||||||
|
"params": [
|
||||||
|
{
|
||||||
|
"name": "name",
|
||||||
|
"description": "The name of the service to get stats for",
|
||||||
|
"required": true,
|
||||||
|
"schema": {
|
||||||
|
"type": "string"
|
||||||
|
}
|
||||||
|
}
|
||||||
|
],
|
||||||
|
"result": {
|
||||||
|
"name": "ServiceStats",
|
||||||
|
"description": "Memory and CPU usage statistics for the service",
|
||||||
|
"schema": {
|
||||||
|
"type": "object",
|
||||||
|
"properties": {
|
||||||
|
"name": {
|
||||||
|
"type": "string",
|
||||||
|
"description": "Service name"
|
||||||
|
},
|
||||||
|
"pid": {
|
||||||
|
"type": "integer",
|
||||||
|
"description": "Process ID of the service"
|
||||||
|
},
|
||||||
|
"memory_usage": {
|
||||||
|
"type": "integer",
|
||||||
|
"description": "Memory usage in bytes"
|
||||||
|
},
|
||||||
|
"cpu_usage": {
|
||||||
|
"type": "number",
|
||||||
|
"description": "CPU usage as a percentage (0-100)"
|
||||||
|
},
|
||||||
|
"children": {
|
||||||
|
"type": "array",
|
||||||
|
"description": "Stats for child processes",
|
||||||
|
"items": {
|
||||||
|
"type": "object",
|
||||||
|
"properties": {
|
||||||
|
"pid": {
|
||||||
|
"type": "integer",
|
||||||
|
"description": "Process ID of the child process"
|
||||||
|
},
|
||||||
|
"memory_usage": {
|
||||||
|
"type": "integer",
|
||||||
|
"description": "Memory usage in bytes"
|
||||||
|
},
|
||||||
|
"cpu_usage": {
|
||||||
|
"type": "number",
|
||||||
|
"description": "CPU usage as a percentage (0-100)"
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
},
|
||||||
|
"examples": [
|
||||||
|
{
|
||||||
|
"name": "Get stats for redis service",
|
||||||
|
"params": [
|
||||||
|
{
|
||||||
|
"name": "name",
|
||||||
|
"value": "redis"
|
||||||
|
}
|
||||||
|
],
|
||||||
|
"result": {
|
||||||
|
"name": "ServiceStatsResult",
|
||||||
|
"value": {
|
||||||
|
"name": "redis",
|
||||||
|
"pid": 1234,
|
||||||
|
"memory_usage": 10485760,
|
||||||
|
"cpu_usage": 2.5,
|
||||||
|
"children": [
|
||||||
|
{
|
||||||
|
"pid": 1235,
|
||||||
|
"memory_usage": 5242880,
|
||||||
|
"cpu_usage": 1.2
|
||||||
|
}
|
||||||
|
]
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
],
|
||||||
|
"errors": [
|
||||||
|
{
|
||||||
|
"code": -32000,
|
||||||
|
"message": "Service not found",
|
||||||
|
"data": "service name \"unknown\" unknown"
|
||||||
|
},
|
||||||
|
{
|
||||||
|
"code": -32003,
|
||||||
|
"message": "Service is down",
|
||||||
|
"data": "service \"redis\" is down"
|
||||||
|
}
|
||||||
|
]
|
||||||
|
},
|
||||||
|
{
|
||||||
|
"name": "system_start_http_server",
|
||||||
|
"description": "Start an HTTP/RPC server at the specified address",
|
||||||
|
"params": [
|
||||||
|
{
|
||||||
|
"name": "address",
|
||||||
|
"description": "The network address to bind the server to (e.g., '127.0.0.1:8080')",
|
||||||
|
"required": true,
|
||||||
|
"schema": {
|
||||||
|
"type": "string"
|
||||||
|
}
|
||||||
|
}
|
||||||
|
],
|
||||||
|
"result": {
|
||||||
|
"name": "StartHttpServerResult",
|
||||||
|
"description": "Result of the start HTTP server operation",
|
||||||
|
"schema": {
|
||||||
|
"type": "string"
|
||||||
|
}
|
||||||
|
},
|
||||||
|
"examples": [
|
||||||
|
{
|
||||||
|
"name": "Start HTTP server on localhost:8080",
|
||||||
|
"params": [
|
||||||
|
{
|
||||||
|
"name": "address",
|
||||||
|
"value": "127.0.0.1:8080"
|
||||||
|
}
|
||||||
|
],
|
||||||
|
"result": {
|
||||||
|
"name": "StartHttpServerResult",
|
||||||
|
"value": "HTTP server started at 127.0.0.1:8080"
|
||||||
|
}
|
||||||
|
}
|
||||||
|
],
|
||||||
|
"errors": [
|
||||||
|
{
|
||||||
|
"code": -32602,
|
||||||
|
"message": "Invalid address",
|
||||||
|
"data": "Invalid network address format"
|
||||||
|
}
|
||||||
|
]
|
||||||
|
},
|
||||||
|
{
|
||||||
|
"name": "system_stop_http_server",
|
||||||
|
"description": "Stop the HTTP/RPC server if running",
|
||||||
|
"params": [],
|
||||||
|
"result": {
|
||||||
|
"name": "StopHttpServerResult",
|
||||||
|
"description": "Result of the stop HTTP server operation",
|
||||||
|
"schema": {
|
||||||
|
"type": "null"
|
||||||
|
}
|
||||||
|
},
|
||||||
|
"examples": [
|
||||||
|
{
|
||||||
|
"name": "Stop the HTTP server",
|
||||||
|
"params": [],
|
||||||
|
"result": {
|
||||||
|
"name": "StopHttpServerResult",
|
||||||
|
"value": null
|
||||||
|
}
|
||||||
|
}
|
||||||
|
],
|
||||||
|
"errors": [
|
||||||
|
{
|
||||||
|
"code": -32602,
|
||||||
|
"message": "Server not running",
|
||||||
|
"data": "No HTTP server is currently running"
|
||||||
|
}
|
||||||
|
]
|
||||||
|
},
|
||||||
|
{
|
||||||
|
"name": "stream_currentLogs",
|
||||||
|
"description": "Get current logs from zinit and monitored services",
|
||||||
|
"params": [
|
||||||
|
{
|
||||||
|
"name": "name",
|
||||||
|
"description": "Optional service name filter. If provided, only logs from this service will be returned",
|
||||||
|
"required": false,
|
||||||
|
"schema": {
|
||||||
|
"type": "string"
|
||||||
|
}
|
||||||
|
}
|
||||||
|
],
|
||||||
|
"result": {
|
||||||
|
"name": "LogsResult",
|
||||||
|
"description": "Array of log strings",
|
||||||
|
"schema": {
|
||||||
|
"type": "array",
|
||||||
|
"items": {
|
||||||
|
"type": "string"
|
||||||
|
}
|
||||||
|
}
|
||||||
|
},
|
||||||
|
"examples": [
|
||||||
|
{
|
||||||
|
"name": "Get all logs",
|
||||||
|
"params": [],
|
||||||
|
"result": {
|
||||||
|
"name": "LogsResult",
|
||||||
|
"value": [
|
||||||
|
"2023-01-01T12:00:00 redis: Starting service",
|
||||||
|
"2023-01-01T12:00:01 nginx: Starting service"
|
||||||
|
]
|
||||||
|
}
|
||||||
|
},
|
||||||
|
{
|
||||||
|
"name": "Get logs for a specific service",
|
||||||
|
"params": [
|
||||||
|
{
|
||||||
|
"name": "name",
|
||||||
|
"value": "redis"
|
||||||
|
}
|
||||||
|
],
|
||||||
|
"result": {
|
||||||
|
"name": "LogsResult",
|
||||||
|
"value": [
|
||||||
|
"2023-01-01T12:00:00 redis: Starting service",
|
||||||
|
"2023-01-01T12:00:02 redis: Service started"
|
||||||
|
]
|
||||||
|
}
|
||||||
|
}
|
||||||
|
]
|
||||||
|
},
|
||||||
|
{
|
||||||
|
"name": "stream_subscribeLogs",
|
||||||
|
"description": "Subscribe to log messages generated by zinit and monitored services",
|
||||||
|
"params": [
|
||||||
|
{
|
||||||
|
"name": "name",
|
||||||
|
"description": "Optional service name filter. If provided, only logs from this service will be returned",
|
||||||
|
"required": false,
|
||||||
|
"schema": {
|
||||||
|
"type": "string"
|
||||||
|
}
|
||||||
|
}
|
||||||
|
],
|
||||||
|
"result": {
|
||||||
|
"name": "LogSubscription",
|
||||||
|
"description": "A subscription to log messages",
|
||||||
|
"schema": {
|
||||||
|
"type": "string"
|
||||||
|
}
|
||||||
|
},
|
||||||
|
"examples": [
|
||||||
|
{
|
||||||
|
"name": "Subscribe to all logs",
|
||||||
|
"params": [],
|
||||||
|
"result": {
|
||||||
|
"name": "LogSubscription",
|
||||||
|
"value": "2023-01-01T12:00:00 redis: Service started"
|
||||||
|
}
|
||||||
|
},
|
||||||
|
{
|
||||||
|
"name": "Subscribe to filtered logs",
|
||||||
|
"params": [
|
||||||
|
{
|
||||||
|
"name": "name",
|
||||||
|
"value": "redis"
|
||||||
|
}
|
||||||
|
],
|
||||||
|
"result": {
|
||||||
|
"name": "LogSubscription",
|
||||||
|
"value": "2023-01-01T12:00:00 redis: Service started"
|
||||||
|
}
|
||||||
|
}
|
||||||
|
]
|
||||||
|
}
|
||||||
|
]
|
||||||
|
}
|
||||||
Reference in New Issue
Block a user