Compare commits

..

3 Commits

Author SHA1 Message Date
Maxime Van Hees
0e63efda61 fixed getters and setters + more idiomatic error handling 2025-07-10 11:52:45 +02:00
Maxime Van Hees
568a5b0a49 buildah and nerdctl fixes 2025-07-09 16:27:11 +02:00
Maxime Van Hees
d431705501 Solved multiple Buildah-related issues in the Buildah SAL + fixed test suite for Buildah 2025-07-07 16:45:06 +02:00
449 changed files with 1451 additions and 29340 deletions

View File

@ -1,227 +0,0 @@
name: Publish SAL Crates
on:
release:
types: [published]
workflow_dispatch:
inputs:
version:
description: 'Version to publish (e.g., 0.1.0)'
required: true
type: string
dry_run:
description: 'Dry run (do not actually publish)'
required: false
type: boolean
default: false
env:
CARGO_TERM_COLOR: always
jobs:
publish:
name: Publish to crates.io
runs-on: ubuntu-latest
steps:
- name: Checkout repository
uses: actions/checkout@v4
with:
fetch-depth: 0
- name: Install Rust toolchain
uses: dtolnay/rust-toolchain@stable
with:
toolchain: stable
- name: Cache Cargo dependencies
uses: actions/cache@v4
with:
path: |
~/.cargo/bin/
~/.cargo/registry/index/
~/.cargo/registry/cache/
~/.cargo/git/db/
target/
key: ${{ runner.os }}-cargo-${{ hashFiles('**/Cargo.lock') }}
restore-keys: |
${{ runner.os }}-cargo-
- name: Install cargo-edit for version management
run: cargo install cargo-edit
- name: Set version from release tag
if: github.event_name == 'release'
run: |
VERSION=${GITHUB_REF#refs/tags/v}
echo "PUBLISH_VERSION=$VERSION" >> $GITHUB_ENV
echo "Publishing version: $VERSION"
- name: Set version from workflow input
if: github.event_name == 'workflow_dispatch'
run: |
echo "PUBLISH_VERSION=${{ github.event.inputs.version }}" >> $GITHUB_ENV
echo "Publishing version: ${{ github.event.inputs.version }}"
- name: Update version in all crates
run: |
echo "Updating version to $PUBLISH_VERSION"
# Update root Cargo.toml
cargo set-version $PUBLISH_VERSION
# Update each crate
CRATES=(os process text net git vault kubernetes virt redisclient postgresclient zinit_client mycelium rhai)
for crate in "${CRATES[@]}"; do
if [ -d "$crate" ]; then
cd "$crate"
cargo set-version $PUBLISH_VERSION
cd ..
echo "Updated $crate to version $PUBLISH_VERSION"
fi
done
- name: Run tests
run: cargo test --workspace --verbose
- name: Check formatting
run: cargo fmt --all -- --check
- name: Run clippy
run: cargo clippy --workspace --all-targets --all-features -- -D warnings
- name: Dry run publish (check packages)
run: |
echo "Checking all packages can be published..."
CRATES=(os process text net git vault kubernetes virt redisclient postgresclient zinit_client mycelium rhai)
for crate in "${CRATES[@]}"; do
if [ -d "$crate" ]; then
echo "Checking $crate..."
cd "$crate"
cargo publish --dry-run
cd ..
fi
done
echo "Checking main crate..."
cargo publish --dry-run
- name: Publish crates (dry run)
if: github.event.inputs.dry_run == 'true'
run: |
echo "🔍 DRY RUN MODE - Would publish the following crates:"
echo "Individual crates: sal-os, sal-process, sal-text, sal-net, sal-git, sal-vault, sal-kubernetes, sal-virt, sal-redisclient, sal-postgresclient, sal-zinit-client, sal-mycelium, sal-rhai"
echo "Meta-crate: sal"
echo "Version: $PUBLISH_VERSION"
- name: Publish individual crates
if: github.event.inputs.dry_run != 'true'
env:
CARGO_REGISTRY_TOKEN: ${{ secrets.CARGO_REGISTRY_TOKEN }}
run: |
echo "Publishing individual crates..."
# Crates in dependency order
CRATES=(os process text net git vault kubernetes virt redisclient postgresclient zinit_client mycelium rhai)
for crate in "${CRATES[@]}"; do
if [ -d "$crate" ]; then
echo "Publishing sal-$crate..."
cd "$crate"
# Retry logic for transient failures
for attempt in 1 2 3; do
if cargo publish --token $CARGO_REGISTRY_TOKEN; then
echo "✅ sal-$crate published successfully"
break
else
if [ $attempt -eq 3 ]; then
echo "❌ Failed to publish sal-$crate after 3 attempts"
exit 1
else
echo "⚠️ Attempt $attempt failed, retrying in 30 seconds..."
sleep 30
fi
fi
done
cd ..
# Wait for crates.io to process
if [ "$crate" != "rhai" ]; then
echo "⏳ Waiting 30 seconds for crates.io to process..."
sleep 30
fi
fi
done
- name: Publish main crate
if: github.event.inputs.dry_run != 'true'
env:
CARGO_REGISTRY_TOKEN: ${{ secrets.CARGO_REGISTRY_TOKEN }}
run: |
echo "Publishing main sal crate..."
# Wait a bit longer before publishing the meta-crate
echo "⏳ Waiting 60 seconds for all individual crates to be available..."
sleep 60
# Retry logic for the main crate
for attempt in 1 2 3; do
if cargo publish --token $CARGO_REGISTRY_TOKEN; then
echo "✅ Main sal crate published successfully"
break
else
if [ $attempt -eq 3 ]; then
echo "❌ Failed to publish main sal crate after 3 attempts"
exit 1
else
echo "⚠️ Attempt $attempt failed, retrying in 60 seconds..."
sleep 60
fi
fi
done
- name: Create summary
if: always()
run: |
echo "## 📦 SAL Publishing Summary" >> $GITHUB_STEP_SUMMARY
echo "" >> $GITHUB_STEP_SUMMARY
echo "**Version:** $PUBLISH_VERSION" >> $GITHUB_STEP_SUMMARY
echo "**Trigger:** ${{ github.event_name }}" >> $GITHUB_STEP_SUMMARY
if [ "${{ github.event.inputs.dry_run }}" == "true" ]; then
echo "**Mode:** Dry Run" >> $GITHUB_STEP_SUMMARY
else
echo "**Mode:** Live Publishing" >> $GITHUB_STEP_SUMMARY
fi
echo "" >> $GITHUB_STEP_SUMMARY
echo "### Published Crates" >> $GITHUB_STEP_SUMMARY
echo "" >> $GITHUB_STEP_SUMMARY
echo "- sal-os" >> $GITHUB_STEP_SUMMARY
echo "- sal-process" >> $GITHUB_STEP_SUMMARY
echo "- sal-text" >> $GITHUB_STEP_SUMMARY
echo "- sal-net" >> $GITHUB_STEP_SUMMARY
echo "- sal-git" >> $GITHUB_STEP_SUMMARY
echo "- sal-vault" >> $GITHUB_STEP_SUMMARY
echo "- sal-kubernetes" >> $GITHUB_STEP_SUMMARY
echo "- sal-virt" >> $GITHUB_STEP_SUMMARY
echo "- sal-redisclient" >> $GITHUB_STEP_SUMMARY
echo "- sal-postgresclient" >> $GITHUB_STEP_SUMMARY
echo "- sal-zinit-client" >> $GITHUB_STEP_SUMMARY
echo "- sal-mycelium" >> $GITHUB_STEP_SUMMARY
echo "- sal-rhai" >> $GITHUB_STEP_SUMMARY
echo "- sal (meta-crate)" >> $GITHUB_STEP_SUMMARY
echo "" >> $GITHUB_STEP_SUMMARY
echo "### Usage" >> $GITHUB_STEP_SUMMARY
echo "" >> $GITHUB_STEP_SUMMARY
echo '```bash' >> $GITHUB_STEP_SUMMARY
echo "# Individual crates" >> $GITHUB_STEP_SUMMARY
echo "cargo add sal-os sal-process sal-text" >> $GITHUB_STEP_SUMMARY
echo "" >> $GITHUB_STEP_SUMMARY
echo "# Meta-crate with features" >> $GITHUB_STEP_SUMMARY
echo "cargo add sal --features core" >> $GITHUB_STEP_SUMMARY
echo "cargo add sal --features all" >> $GITHUB_STEP_SUMMARY
echo '```' >> $GITHUB_STEP_SUMMARY

View File

@ -1,233 +0,0 @@
name: Test Publishing Setup
on:
push:
branches: [ main, master ]
paths:
- '**/Cargo.toml'
- 'scripts/publish-all.sh'
- '.github/workflows/publish.yml'
pull_request:
branches: [ main, master ]
paths:
- '**/Cargo.toml'
- 'scripts/publish-all.sh'
- '.github/workflows/publish.yml'
workflow_dispatch:
env:
CARGO_TERM_COLOR: always
jobs:
test-publish-setup:
name: Test Publishing Setup
runs-on: ubuntu-latest
steps:
- name: Checkout repository
uses: actions/checkout@v4
- name: Install Rust toolchain
uses: dtolnay/rust-toolchain@stable
with:
toolchain: stable
- name: Cache Cargo dependencies
uses: actions/cache@v4
with:
path: |
~/.cargo/bin/
~/.cargo/registry/index/
~/.cargo/registry/cache/
~/.cargo/git/db/
target/
key: ${{ runner.os }}-cargo-publish-test-${{ hashFiles('**/Cargo.lock') }}
restore-keys: |
${{ runner.os }}-cargo-publish-test-
${{ runner.os }}-cargo-
- name: Install cargo-edit
run: cargo install cargo-edit
- name: Test workspace structure
run: |
echo "Testing workspace structure..."
# Check that all expected crates exist
EXPECTED_CRATES=(os process text net git vault kubernetes virt redisclient postgresclient zinit_client mycelium rhai herodo)
for crate in "${EXPECTED_CRATES[@]}"; do
if [ -d "$crate" ] && [ -f "$crate/Cargo.toml" ]; then
echo "✅ $crate exists"
else
echo "❌ $crate missing or invalid"
exit 1
fi
done
- name: Test feature configuration
run: |
echo "Testing feature configuration..."
# Test that features work correctly
cargo check --features os
cargo check --features process
cargo check --features text
cargo check --features net
cargo check --features git
cargo check --features vault
cargo check --features kubernetes
cargo check --features virt
cargo check --features redisclient
cargo check --features postgresclient
cargo check --features zinit_client
cargo check --features mycelium
cargo check --features rhai
echo "✅ All individual features work"
# Test feature groups
cargo check --features core
cargo check --features clients
cargo check --features infrastructure
cargo check --features scripting
echo "✅ All feature groups work"
# Test all features
cargo check --features all
echo "✅ All features together work"
- name: Test dry-run publishing
run: |
echo "Testing dry-run publishing..."
# Test each individual crate can be packaged
CRATES=(os process text net git vault kubernetes virt redisclient postgresclient zinit_client mycelium rhai)
for crate in "${CRATES[@]}"; do
echo "Testing sal-$crate..."
cd "$crate"
cargo publish --dry-run
cd ..
echo "✅ sal-$crate can be published"
done
# Test main crate
echo "Testing main sal crate..."
cargo publish --dry-run
echo "✅ Main sal crate can be published"
- name: Test publishing script
run: |
echo "Testing publishing script..."
# Make script executable
chmod +x scripts/publish-all.sh
# Test dry run
./scripts/publish-all.sh --dry-run --version 0.1.0-test
echo "✅ Publishing script works"
- name: Test version consistency
run: |
echo "Testing version consistency..."
# Get version from root Cargo.toml
ROOT_VERSION=$(grep '^version = ' Cargo.toml | head -1 | sed 's/version = "\(.*\)"/\1/')
echo "Root version: $ROOT_VERSION"
# Check all crates have the same version
CRATES=(os process text net git vault kubernetes virt redisclient postgresclient zinit_client mycelium rhai herodo)
for crate in "${CRATES[@]}"; do
if [ -f "$crate/Cargo.toml" ]; then
CRATE_VERSION=$(grep '^version = ' "$crate/Cargo.toml" | head -1 | sed 's/version = "\(.*\)"/\1/')
if [ "$CRATE_VERSION" = "$ROOT_VERSION" ]; then
echo "✅ $crate version matches: $CRATE_VERSION"
else
echo "❌ $crate version mismatch: $CRATE_VERSION (expected $ROOT_VERSION)"
exit 1
fi
fi
done
- name: Test metadata completeness
run: |
echo "Testing metadata completeness..."
# Check that all crates have required metadata
CRATES=(os process text net git vault kubernetes virt redisclient postgresclient zinit_client mycelium rhai)
for crate in "${CRATES[@]}"; do
echo "Checking sal-$crate metadata..."
cd "$crate"
# Check required fields exist
if ! grep -q '^name = "sal-' Cargo.toml; then
echo "❌ $crate missing or incorrect name"
exit 1
fi
if ! grep -q '^description = ' Cargo.toml; then
echo "❌ $crate missing description"
exit 1
fi
if ! grep -q '^repository = ' Cargo.toml; then
echo "❌ $crate missing repository"
exit 1
fi
if ! grep -q '^license = ' Cargo.toml; then
echo "❌ $crate missing license"
exit 1
fi
echo "✅ sal-$crate metadata complete"
cd ..
done
- name: Test dependency resolution
run: |
echo "Testing dependency resolution..."
# Test that all workspace dependencies resolve correctly
cargo tree --workspace > /dev/null
echo "✅ All dependencies resolve correctly"
# Test that there are no dependency conflicts
cargo check --workspace
echo "✅ No dependency conflicts"
- name: Generate publishing report
if: always()
run: |
echo "## 🧪 Publishing Setup Test Report" >> $GITHUB_STEP_SUMMARY
echo "" >> $GITHUB_STEP_SUMMARY
echo "### ✅ Tests Passed" >> $GITHUB_STEP_SUMMARY
echo "" >> $GITHUB_STEP_SUMMARY
echo "- Workspace structure validation" >> $GITHUB_STEP_SUMMARY
echo "- Feature configuration testing" >> $GITHUB_STEP_SUMMARY
echo "- Dry-run publishing simulation" >> $GITHUB_STEP_SUMMARY
echo "- Publishing script validation" >> $GITHUB_STEP_SUMMARY
echo "- Version consistency check" >> $GITHUB_STEP_SUMMARY
echo "- Metadata completeness verification" >> $GITHUB_STEP_SUMMARY
echo "- Dependency resolution testing" >> $GITHUB_STEP_SUMMARY
echo "" >> $GITHUB_STEP_SUMMARY
echo "### 📦 Ready for Publishing" >> $GITHUB_STEP_SUMMARY
echo "" >> $GITHUB_STEP_SUMMARY
echo "All SAL crates are ready for publishing to crates.io!" >> $GITHUB_STEP_SUMMARY
echo "" >> $GITHUB_STEP_SUMMARY
echo "**Individual Crates:** 13 modules" >> $GITHUB_STEP_SUMMARY
echo "**Meta-crate:** sal with optional features" >> $GITHUB_STEP_SUMMARY
echo "**Binary:** herodo script executor" >> $GITHUB_STEP_SUMMARY
echo "" >> $GITHUB_STEP_SUMMARY
echo "### 🚀 Next Steps" >> $GITHUB_STEP_SUMMARY
echo "" >> $GITHUB_STEP_SUMMARY
echo "1. Create a release tag (e.g., v0.1.0)" >> $GITHUB_STEP_SUMMARY
echo "2. The publish workflow will automatically trigger" >> $GITHUB_STEP_SUMMARY
echo "3. All crates will be published to crates.io" >> $GITHUB_STEP_SUMMARY
echo "4. Users can install with: \`cargo add sal-os\` or \`cargo add sal --features all\`" >> $GITHUB_STEP_SUMMARY

2
.gitignore vendored
View File

@ -62,5 +62,3 @@ docusaurus.config.ts
sidebars.ts
tsconfig.json
Cargo.toml.bak
for_augment

View File

@ -11,26 +11,7 @@ categories = ["os", "filesystem", "api-bindings"]
readme = "README.md"
[workspace]
members = [
"packages/clients/myceliumclient",
"packages/clients/postgresclient",
"packages/clients/redisclient",
"packages/clients/zinitclient",
"packages/core/net",
"packages/core/text",
"packages/crypt/vault",
"packages/data/ourdb",
"packages/data/radixtree",
"packages/data/tst",
"packages/system/git",
"packages/system/kubernetes",
"packages/system/os",
"packages/system/process",
"packages/system/virt",
"rhai",
"herodo",
"packages/clients/hetznerclient",
]
members = [".", "vault", "git", "redisclient", "mycelium", "text", "os", "net", "zinit_client", "process", "virt", "postgresclient", "rhai", "herodo"]
resolver = "2"
[workspace.metadata]
@ -51,7 +32,7 @@ log = "0.4"
once_cell = "1.18.0"
rand = "0.8.5"
regex = "1.8.1"
reqwest = { version = "0.12.15", features = ["json", "blocking"] }
reqwest = { version = "0.12.15", features = ["json"] }
rhai = { version = "1.12.0", features = ["sync"] }
serde = { version = "1.0", features = ["derive"] }
serde_json = "1.0"
@ -72,10 +53,6 @@ chacha20poly1305 = "0.10.1"
k256 = { version = "0.13.4", features = ["ecdsa", "ecdh"] }
sha2 = "0.10.7"
hex = "0.4"
bincode = { version = "2.0.1", features = ["serde"] }
pbkdf2 = "0.12.2"
getrandom = { version = "0.3.3", features = ["wasm_js"] }
tera = "1.19.0"
# Ethereum dependencies
ethers = { version = "2.0.7", features = ["legacy"] }
@ -89,110 +66,21 @@ windows = { version = "0.61.1", features = [
] }
# Specialized dependencies
zinit-client = "0.4.0"
zinit-client = "0.3.0"
urlencoding = "2.1.3"
tokio-test = "0.4.4"
kube = { version = "0.95.0", features = ["client", "config", "derive"] }
k8s-openapi = { version = "0.23.0", features = ["latest"] }
tokio-retry = "0.3.0"
governor = "0.6.3"
tower = { version = "0.5.2", features = ["timeout", "limit"] }
serde_yaml = "0.9"
postgres-types = "0.2.5"
r2d2 = "0.8.10"
# SAL dependencies
sal-git = { path = "packages/system/git" }
sal-kubernetes = { path = "packages/system/kubernetes" }
sal-redisclient = { path = "packages/clients/redisclient" }
sal-mycelium = { path = "packages/clients/myceliumclient" }
sal-hetzner = { path = "packages/clients/hetznerclient" }
sal-text = { path = "packages/core/text" }
sal-os = { path = "packages/system/os" }
sal-net = { path = "packages/core/net" }
sal-zinit-client = { path = "packages/clients/zinitclient" }
sal-process = { path = "packages/system/process" }
sal-virt = { path = "packages/system/virt" }
sal-postgresclient = { path = "packages/clients/postgresclient" }
sal-vault = { path = "packages/crypt/vault" }
sal-rhai = { path = "rhai" }
sal-service-manager = { path = "_archive/service_manager" }
[dependencies]
thiserror = { workspace = true }
tokio = { workspace = true }
# Optional dependencies - users can choose which modules to include
sal-git = { workspace = true, optional = true }
sal-kubernetes = { workspace = true, optional = true }
sal-redisclient = { workspace = true, optional = true }
sal-mycelium = { workspace = true, optional = true }
sal-hetzner = { workspace = true, optional = true }
sal-text = { workspace = true, optional = true }
sal-os = { workspace = true, optional = true }
sal-net = { workspace = true, optional = true }
sal-zinit-client = { workspace = true, optional = true }
sal-process = { workspace = true, optional = true }
sal-virt = { workspace = true, optional = true }
sal-postgresclient = { workspace = true, optional = true }
sal-vault = { workspace = true, optional = true }
sal-rhai = { workspace = true, optional = true }
sal-service-manager = { workspace = true, optional = true }
[features]
default = []
# Individual module features
git = ["dep:sal-git"]
kubernetes = ["dep:sal-kubernetes"]
redisclient = ["dep:sal-redisclient"]
mycelium = ["dep:sal-mycelium"]
hetzner = ["dep:sal-hetzner"]
text = ["dep:sal-text"]
os = ["dep:sal-os"]
net = ["dep:sal-net"]
zinit_client = ["dep:sal-zinit-client"]
process = ["dep:sal-process"]
virt = ["dep:sal-virt"]
postgresclient = ["dep:sal-postgresclient"]
vault = ["dep:sal-vault"]
rhai = ["dep:sal-rhai"]
# service_manager is removed as it's not a direct member anymore
# Convenience feature groups
core = ["os", "process", "text", "net"]
clients = ["redisclient", "postgresclient", "zinit_client", "mycelium", "hetzner"]
infrastructure = ["git", "vault", "kubernetes", "virt"]
scripting = ["rhai"]
all = [
"git",
"kubernetes",
"redisclient",
"mycelium",
"hetzner",
"text",
"os",
"net",
"zinit_client",
"process",
"virt",
"postgresclient",
"vault",
"rhai",
]
# Examples
[[example]]
name = "postgres_cluster"
path = "examples/kubernetes/clusters/postgres.rs"
required-features = ["kubernetes"]
[[example]]
name = "redis_cluster"
path = "examples/kubernetes/clusters/redis.rs"
required-features = ["kubernetes"]
[[example]]
name = "generic_cluster"
path = "examples/kubernetes/clusters/generic.rs"
required-features = ["kubernetes"]
thiserror = "2.0.12" # For error handling in the main Error enum
sal-git = { path = "git" }
sal-redisclient = { path = "redisclient" }
sal-mycelium = { path = "mycelium" }
sal-text = { path = "text" }
sal-os = { path = "os" }
sal-net = { path = "net" }
sal-zinit-client = { path = "zinit_client" }
sal-process = { path = "process" }
sal-virt = { path = "virt" }
sal-postgresclient = { path = "postgresclient" }
sal-vault = { path = "vault" }
sal-rhai = { path = "rhai" }

View File

@ -1,239 +0,0 @@
# SAL Publishing Guide
This guide explains how to publish SAL crates to crates.io and how users can consume them.
## 🎯 Publishing Strategy
SAL uses a **modular publishing approach** where each module is published as an individual crate. This allows users to install only the functionality they need, reducing compilation time and binary size.
## 📦 Crate Structure
### Individual Crates
Each SAL module is published as a separate crate:
| Crate Name | Description | Category |
|------------|-------------|----------|
| `sal-os` | Operating system operations | Core |
| `sal-process` | Process management | Core |
| `sal-text` | Text processing utilities | Core |
| `sal-net` | Network operations | Core |
| `sal-git` | Git repository management | Infrastructure |
| `sal-vault` | Cryptographic operations | Infrastructure |
| `sal-kubernetes` | Kubernetes cluster management | Infrastructure |
| `sal-virt` | Virtualization tools (Buildah, nerdctl) | Infrastructure |
| `sal-redisclient` | Redis database client | Clients |
| `sal-postgresclient` | PostgreSQL database client | Clients |
| `sal-zinit-client` | Zinit process supervisor client | Clients |
| `sal-mycelium` | Mycelium network client | Clients |
| `sal-rhai` | Rhai scripting integration | Scripting |
### Meta-crate
The main `sal` crate serves as a meta-crate that re-exports all modules with optional features:
```toml
[dependencies]
sal = { version = "0.1.0", features = ["os", "process", "text"] }
```
## 🚀 Publishing Process
### Prerequisites
1. **Crates.io Account**: Ensure you have a crates.io account and API token
2. **Repository Access**: Ensure the repository URL is accessible
3. **Version Consistency**: All crates should use the same version number
### Publishing Individual Crates
Each crate can be published independently:
```bash
# Publish core modules
cd os && cargo publish
cd ../process && cargo publish
cd ../text && cargo publish
cd ../net && cargo publish
# Publish infrastructure modules
cd ../git && cargo publish
cd ../vault && cargo publish
cd ../kubernetes && cargo publish
cd ../virt && cargo publish
# Publish client modules
cd ../redisclient && cargo publish
cd ../postgresclient && cargo publish
cd ../zinit_client && cargo publish
cd ../mycelium && cargo publish
# Publish scripting module
cd ../rhai && cargo publish
# Finally, publish the meta-crate
cd .. && cargo publish
```
### Automated Publishing
Use the comprehensive publishing script:
```bash
# Test the publishing process (safe)
./scripts/publish-all.sh --dry-run --version 0.1.0
# Actually publish to crates.io
./scripts/publish-all.sh --version 0.1.0
```
The script handles:
- ✅ **Dependency order** - Publishes crates in correct dependency order
- ✅ **Path dependencies** - Automatically updates path deps to version deps
- ✅ **Rate limiting** - Waits between publishes to avoid rate limits
- ✅ **Error handling** - Stops on failures with clear error messages
- ✅ **Dry run mode** - Test without actually publishing
## 👥 User Consumption
### Installation Options
#### Option 1: Individual Crates (Recommended)
Users install only what they need:
```bash
# Core functionality
cargo add sal-os sal-process sal-text sal-net
# Database operations
cargo add sal-redisclient sal-postgresclient
# Infrastructure management
cargo add sal-git sal-vault sal-kubernetes
# Service integration
cargo add sal-zinit-client sal-mycelium
# Scripting
cargo add sal-rhai
```
**Usage:**
```rust
use sal_os::fs;
use sal_process::run;
use sal_git::GitManager;
fn main() -> Result<(), Box<dyn std::error::Error>> {
let files = fs::list_files(".")?;
let result = run::command("echo hello")?;
let git = GitManager::new(".")?;
Ok(())
}
```
#### Option 2: Meta-crate with Features
Users can use the main crate with selective features:
```bash
# Specific modules
cargo add sal --features os,process,text
# Feature groups
cargo add sal --features core # os, process, text, net
cargo add sal --features clients # redisclient, postgresclient, zinit_client, mycelium
cargo add sal --features infrastructure # git, vault, kubernetes, virt
cargo add sal --features scripting # rhai
# Everything
cargo add sal --features all
```
**Usage:**
```rust
// Cargo.toml: sal = { version = "0.1.0", features = ["os", "process", "git"] }
use sal::os::fs;
use sal::process::run;
use sal::git::GitManager;
fn main() -> Result<(), Box<dyn std::error::Error>> {
let files = fs::list_files(".")?;
let result = run::command("echo hello")?;
let git = GitManager::new(".")?;
Ok(())
}
```
### Feature Groups
The meta-crate provides convenient feature groups:
- **`core`**: Essential system operations (os, process, text, net)
- **`clients`**: Database and service clients (redisclient, postgresclient, zinit_client, mycelium)
- **`infrastructure`**: Infrastructure management tools (git, vault, kubernetes, virt)
- **`scripting`**: Rhai scripting support (rhai)
- **`all`**: Everything included
## 📋 Version Management
### Semantic Versioning
All SAL crates follow semantic versioning:
- **Major version**: Breaking API changes
- **Minor version**: New features, backward compatible
- **Patch version**: Bug fixes, backward compatible
### Synchronized Releases
All crates are released with the same version number to ensure compatibility:
```toml
# All crates use the same version
sal-os = "0.1.0"
sal-process = "0.1.0"
sal-git = "0.1.0"
# etc.
```
## 🔧 Maintenance
### Updating Dependencies
When updating dependencies:
1. Update `Cargo.toml` in the workspace root
2. Update individual crate dependencies if needed
3. Test all crates: `cargo test --workspace`
4. Publish with incremented version numbers
### Adding New Modules
To add a new SAL module:
1. Create the new crate directory
2. Add to workspace members in root `Cargo.toml`
3. Add optional dependency in root `Cargo.toml`
4. Add feature flag in root `Cargo.toml`
5. Add conditional re-export in `src/lib.rs`
6. Update documentation
## 🎉 Benefits
### For Users
- **Minimal Dependencies**: Install only what you need
- **Faster Builds**: Smaller dependency trees compile faster
- **Smaller Binaries**: Reduced binary size
- **Clear Dependencies**: Explicit about what functionality is used
### For Maintainers
- **Independent Releases**: Can release individual crates as needed
- **Focused Testing**: Test individual modules in isolation
- **Clear Ownership**: Each crate has clear responsibility
- **Easier Maintenance**: Smaller, focused codebases
This publishing strategy provides the best of both worlds: modularity for users who want minimal dependencies, and convenience for users who prefer a single crate with features.

298
README.md
View File

@ -1,136 +1,228 @@
# Herocode Herolib Rust Repository
# SAL (System Abstraction Layer)
## Overview
**Version: 0.1.0**
This repository contains the **Herocode Herolib** Rust library and a collection of scripts, examples, and utilities for building, testing, and publishing the SAL (System Abstraction Layer) crates. The repository includes:
SAL is a comprehensive Rust library designed to provide a unified and simplified interface for a wide array of system-level operations and interactions. It abstracts platform-specific details, enabling developers to write robust, cross-platform code with greater ease. SAL also includes `herodo`, a powerful command-line tool for executing Rhai scripts that leverage SAL's capabilities for automation and system management tasks.
- **Rust crates** for various system components (e.g., `os`, `process`, `text`, `git`, `vault`, `kubernetes`, etc.).
- **Rhai scripts** and test suites for each crate.
- **Utility scripts** to automate common development tasks.
## 🏗️ **Cargo Workspace Structure**
## Scripts
SAL is organized as a **Cargo workspace** with 16 specialized crates:
The repository provides three primary helper scripts located in the repository root:
- **Root Package**: `sal` - Umbrella crate that re-exports all modules
- **13 Library Crates**: Specialized SAL modules (git, text, os, net, etc.)
- **1 Binary Crate**: `herodo` - Rhai script execution engine
- **1 Integration Crate**: `rhai` - Rhai scripting integration layer
| Script | Description | Typical Usage |
|--------|-------------|--------------|
| `scripts/publish-all.sh` | Publishes all SAL crates to **crates.io** in the correct dependency order. Handles version bumping, dependency updates, dryrun mode, and ratelimiting. | `./scripts/publish-all.sh [--dry-run] [--wait <seconds>] [--version <ver>]` |
| `build_herodo.sh` | Builds the `herodo` binary from the `herodo` package and optionally runs a specified Rhai script. | `./build_herodo.sh [script_name]` |
| `run_rhai_tests.sh` | Executes all Rhai test suites across the repository, logging results and providing a summary. | `./run_rhai_tests.sh` |
This workspace structure provides excellent build performance, dependency management, and maintainability.
Below are detailed usage instructions for each script.
### **🚀 Workspace Benefits**
- **Unified Dependency Management**: Shared dependencies across all crates with consistent versions
- **Optimized Build Performance**: Parallel compilation and shared build artifacts
- **Simplified Testing**: Run tests across all modules with a single command
- **Modular Architecture**: Each module is independently maintainable while sharing common infrastructure
- **Production Ready**: 100% test coverage with comprehensive Rhai integration tests
---
## Core Features
## 1. `scripts/publish-all.sh`
SAL offers a broad spectrum of functionalities, including:
### Purpose
- **System Operations**: File and directory management, environment variable access, system information retrieval, and OS-specific commands.
- **Process Management**: Create, monitor, control, and interact with system processes.
- **Containerization Tools**:
- Integration with **Buildah** for building OCI/Docker-compatible container images.
- Integration with **nerdctl** for managing containers (run, stop, list, build, etc.).
- **Version Control**: Programmatic interaction with Git repositories (clone, commit, push, pull, status, etc.).
- **Database Clients**:
- **Redis**: Robust client for interacting with Redis servers.
- **PostgreSQL**: Client for executing queries and managing PostgreSQL databases.
- **Scripting Engine**: In-built support for the **Rhai** scripting language, allowing SAL functionalities to be scripted and automated, primarily through the `herodo` tool.
- **Networking & Services**:
- **Mycelium**: Tools for Mycelium network peer management and message passing.
- **Zinit**: Client for interacting with the Zinit process supervision system.
- **RFS (Remote/Virtual Filesystem)**: Mount, manage, pack, and unpack various types of filesystems (local, SSH, S3, WebDAV).
- **Text Processing**: A suite of utilities for text manipulation, formatting, and regular expressions.
- **Cryptography (`vault`)**: Functions for common cryptographic operations.
- Publishes each SAL crate in the correct dependency order.
- Updates crate versions (if `--version` is supplied).
- Updates path dependencies to version dependencies before publishing.
- Supports **dryrun** mode to preview actions without publishing.
- Handles ratelimiting between crate publishes.
## `herodo`: The SAL Scripting Tool
### Options
| Option | Description |
|--------|-------------|
| `--dry-run` | Shows what would be published without actually publishing. |
| `--wait <seconds>` | Wait time between publishes (default: 15s). |
| `--version <ver>` | Set a new version for all crates (updates `Cargo.toml` files). |
| `-h, --help` | Show help message. |
### Example Usage
```bash
# Dry run no crates will be published
./scripts/publish-all.sh --dry-run
# Publish with a custom wait time and version bump
./scripts/publish-all.sh --wait 30 --version 1.2.3
# Normal publish (no dryrun)
./scripts/publish-all.sh
```
### Notes
- Must be run from the repository root (where `Cargo.toml` lives).
- Requires `cargo` and a loggedin `cargo` session (`cargo login`).
- The script automatically updates dependencies in each crates `Cargo.toml` to use the new version before publishing.
---
## 2. `build_herodo.sh`
### Purpose
- Builds the `herodo` binary from the `herodo` package.
- Copies the binary to a systemwide location (`/usr/local/bin`) if run as root, otherwise to `~/hero/bin`.
- Optionally runs a specified Rhai script after building.
`herodo` is a command-line utility bundled with SAL that executes Rhai scripts. It empowers users to automate tasks and orchestrate complex workflows by leveraging SAL's diverse modules directly from scripts.
### Usage
```bash
# Build only
./build_herodo.sh
# Execute a single Rhai script
herodo script.rhai
# Build and run a specific Rhai script (e.g., `example`):
./build_herodo.sh example
# Execute a script with arguments
herodo script.rhai arg1 arg2
# Execute all .rhai scripts in a directory
herodo /path/to/scripts/
```
### Details
If a directory is provided, `herodo` will execute all `.rhai` scripts within that directory (and its subdirectories) in alphabetical order.
- The script changes to its own directory, builds the `herodo` crate (`cargo build`), and copies the binary.
- If a script name is provided, it looks for the script in:
- `src/rhaiexamples/<name>.rhai`
- `src/herodo/scripts/<name>.rhai`
- If the script is not found, the script exits with an error.
### Scriptable SAL Modules via `herodo`
---
The following SAL modules and functionalities are exposed to the Rhai scripting environment through `herodo`:
## 3. `run_rhai_tests.sh`
- **OS (`os`)**: Comprehensive file system operations, file downloading & installation, and system package management. [Documentation](os/README.md)
- **Process (`process`)**: Robust command and script execution, plus process management (listing, finding, killing, checking command existence). [Documentation](process/README.md)
- **Text (`text`)**: String manipulation, prefixing, path/name fixing, text replacement, and templating. [Documentation](text/README.md)
- **Net (`net`)**: Network operations, HTTP requests, and connectivity utilities. [Documentation](net/README.md)
- **Git (`git`)**: High-level repository management and generic Git command execution with Redis-backed authentication (clone, pull, push, commit, etc.). [Documentation](git/README.md)
- **Vault (`vault`)**: Cryptographic operations, keypair management, encryption, decryption, hashing, etc. [Documentation](vault/README.md)
- **Redis Client (`redisclient`)**: Execute Redis commands (`redis_get`, `redis_set`, `redis_execute`, etc.). [Documentation](redisclient/README.md)
- **PostgreSQL Client (`postgresclient`)**: Execute SQL queries against PostgreSQL databases. [Documentation](postgresclient/README.md)
- **Zinit (`zinit_client`)**: Client for Zinit process supervisor (service management, logs). [Documentation](zinit_client/README.md)
- **Mycelium (`mycelium`)**: Client for Mycelium decentralized networking API (node info, peer management, messaging). [Documentation](mycelium/README.md)
- **Virtualization (`virt`)**:
- **Buildah**: OCI/Docker image building functions. [Documentation](virt/README.md)
- **nerdctl**: Container lifecycle management (`nerdctl_run`, `nerdctl_stop`, `nerdctl_images`, `nerdctl_image_build`, etc.)
- **RFS**: Mount various filesystems (local, SSH, S3, etc.), pack/unpack filesystem layers.
### Purpose
### Example `herodo` Rhai Script
- Runs **all** Rhai test suites across the repository.
- Supports both the legacy `rhai_tests` directory and the newer `*/tests/rhai` layout.
- Logs output to `run_rhai_tests.log` and prints a summary.
```rhai
// file: /opt/scripts/example_task.rhai
### Usage
// OS operations
println("Checking for /tmp/my_app_data...");
if !exist("/tmp/my_app_data") {
mkdir("/tmp/my_app_data");
println("Created directory /tmp/my_app_data");
}
// Redis operations
println("Setting Redis key 'app_status' to 'running'");
redis_set("app_status", "running");
let status = redis_get("app_status");
println("Current app_status from Redis: " + status);
// Process execution
println("Listing files in /tmp:");
let output = run("ls -la /tmp");
println(output.stdout);
println("Script finished.");
```
Run with: `herodo /opt/scripts/example_task.rhai`
For more examples, check the individual module test directories (e.g., `text/tests/rhai/`, `os/tests/rhai/`, etc.) in this repository.
## Using SAL as a Rust Library
Add SAL as a dependency to your `Cargo.toml`:
```toml
[dependencies]
sal = "0.1.0" # Or the latest version
```
### Rust Example: Using Redis Client
```rust
use sal::redisclient::{get_global_client, execute_cmd_with_args};
use redis::RedisResult;
async fn example_redis_interaction() -> RedisResult<()> {
// Get a connection from the global pool
let mut conn = get_global_client().await?.get_async_connection().await?;
// Set a value
execute_cmd_with_args(&mut conn, "SET", vec!["my_key", "my_value"]).await?;
println!("Set 'my_key' to 'my_value'");
// Get a value
let value: String = execute_cmd_with_args(&mut conn, "GET", vec!["my_key"]).await?;
println!("Retrieved value for 'my_key': {}", value);
Ok(())
}
#[tokio::main]
async fn main() {
if let Err(e) = example_redis_interaction().await {
eprintln!("Redis Error: {}", e);
}
}
```
*(Note: The Redis client API might have evolved; please refer to `src/redisclient/mod.rs` and its documentation for the most current usage.)*
## 📦 **Workspace Modules Overview**
SAL is organized as a Cargo workspace with the following crates:
### **Core Library Modules**
- **`sal-os`**: Core OS interactions, file system operations, environment access
- **`sal-process`**: Process creation, management, and control
- **`sal-text`**: Utilities for text processing and manipulation
- **`sal-net`**: Network operations, HTTP requests, and connectivity utilities
### **Integration Modules**
- **`sal-git`**: Git repository management and operations
- **`sal-vault`**: Cryptographic functions and keypair management
- **`sal-rhai`**: Integration layer for the Rhai scripting engine, used by `herodo`
### **Client Modules**
- **`sal-redisclient`**: Client for Redis database interactions
- **`sal-postgresclient`**: Client for PostgreSQL database interactions
- **`sal-zinit-client`**: Client for Zinit process supervisor
- **`sal-mycelium`**: Client for Mycelium network operations
### **Specialized Modules**
- **`sal-virt`**: Virtualization-related utilities (buildah, nerdctl, rfs)
### **Root Package & Binary**
- **`sal`**: Root umbrella crate that re-exports all modules
- **`herodo`**: Command-line binary for executing Rhai scripts
## 🔨 **Building SAL**
Build the entire workspace (all crates) using Cargo:
```bash
# Run all tests
# Build all workspace members
cargo build --workspace
# Build for release
cargo build --workspace --release
# Build specific crate
cargo build -p sal-text
cargo build -p herodo
```
The `herodo` executable will be located at `target/debug/herodo` or `target/release/herodo`.
## 🧪 **Running Tests**
### **Rust Unit Tests**
```bash
# Run all workspace tests
cargo test --workspace
# Run tests for specific crate
cargo test -p sal-text
cargo test -p sal-os
# Run only library tests (faster)
cargo test --workspace --lib
```
### **Rhai Integration Tests**
Run comprehensive Rhai script tests that exercise `herodo` and SAL's scripted functionalities:
```bash
# Run all Rhai integration tests (16 modules)
./run_rhai_tests.sh
# Results: 16/16 modules pass with 100% success rate
```
### Output
- Colored console output for readability.
- Log file (`run_rhai_tests.log`) contains full output for later review.
- Summary includes total modules, passed, and failed counts.
- Exit code `0` if all tests pass, `1` otherwise.
---
## General Development Workflow
1. **Build**: Use `build_herodo.sh` to compile the `herodo` binary.
2. **Test**: Run `run_rhai_tests.sh` to ensure all Rhai scripts pass.
3. **Publish**: When ready to release, use `scripts/publish-all.sh` (with `--dry-run` first to verify).
## Prerequisites
- **Rust toolchain** (`cargo`, `rustc`) installed.
- **Rhai** interpreter (`herodo`) built and available.
- **Git** for version control.
- **Cargo login** for publishing to crates.io.
The Rhai tests validate real-world functionality across all SAL modules and provide comprehensive integration testing.
## License
See `LICENSE` for details.
---
**Happy coding!**
SAL is licensed under the Apache License 2.0. See the [LICENSE](LICENSE) file for details.

View File

@ -1,43 +0,0 @@
[package]
name = "sal-service-manager"
version = "0.1.0"
edition = "2021"
authors = ["PlanetFirst <info@incubaid.com>"]
description = "SAL Service Manager - Cross-platform service management for dynamic worker deployment"
repository = "https://git.threefold.info/herocode/sal"
license = "Apache-2.0"
[dependencies]
# Use workspace dependencies for consistency
thiserror = "1.0"
tokio = { workspace = true }
log = { workspace = true }
serde = { workspace = true }
serde_json = { workspace = true }
futures = { workspace = true }
once_cell = { workspace = true }
# Use base zinit-client instead of SAL wrapper
zinit-client = { version = "0.4.0" }
# Optional Rhai integration
rhai = { workspace = true, optional = true }
[target.'cfg(target_os = "macos")'.dependencies]
# macOS-specific dependencies for launchctl
plist = "1.6"
[features]
default = ["zinit"]
zinit = []
rhai = ["dep:rhai"]
# Enable zinit feature for tests
[dev-dependencies]
tokio-test = "0.4"
rhai = { workspace = true }
tempfile = { workspace = true }
env_logger = "0.10"
[[test]]
name = "zinit_integration_tests"
required-features = ["zinit"]

View File

@ -1,198 +0,0 @@
# SAL Service Manager
[![Crates.io](https://img.shields.io/crates/v/sal-service-manager.svg)](https://crates.io/crates/sal-service-manager)
[![Documentation](https://docs.rs/sal-service-manager/badge.svg)](https://docs.rs/sal-service-manager)
A cross-platform service management library for the System Abstraction Layer (SAL). This crate provides a unified interface for managing system services across different platforms, enabling dynamic deployment of workers and services.
## Features
- **Cross-platform service management** - Unified API across macOS and Linux
- **Dynamic worker deployment** - Perfect for circle workers and on-demand services
- **Platform-specific implementations**:
- **macOS**: Uses `launchctl` with plist management
- **Linux**: Uses `zinit` for lightweight service management (systemd also available)
- **Complete lifecycle management** - Start, stop, restart, status monitoring, and log retrieval
- **Service configuration** - Environment variables, working directories, auto-restart
- **Production-ready** - Comprehensive error handling and resource management
## Usage
Add this to your `Cargo.toml`:
```toml
[dependencies]
sal-service-manager = "0.1.0"
```
Or use it as part of the SAL ecosystem:
```toml
[dependencies]
sal = { version = "0.1.0", features = ["service_manager"] }
```
## Primary Use Case: Dynamic Circle Worker Management
This service manager was designed specifically for dynamic deployment of circle workers in freezone environments. When a new resident registers, you can instantly launch a dedicated circle worker:
```rust,no_run
use sal_service_manager::{create_service_manager, ServiceConfig};
use std::collections::HashMap;
// New resident registration triggers worker creation
fn deploy_circle_worker(resident_id: &str) -> Result<(), Box<dyn std::error::Error>> {
let manager = create_service_manager();
let mut env = HashMap::new();
env.insert("RESIDENT_ID".to_string(), resident_id.to_string());
env.insert("WORKER_TYPE".to_string(), "circle".to_string());
let config = ServiceConfig {
name: format!("circle-worker-{}", resident_id),
binary_path: "/usr/bin/circle-worker".to_string(),
args: vec!["--resident".to_string(), resident_id.to_string()],
working_directory: Some("/var/lib/circle-workers".to_string()),
environment: env,
auto_restart: true,
};
// Deploy the worker
manager.start(&config)?;
println!("✅ Circle worker deployed for resident: {}", resident_id);
Ok(())
}
```
## Basic Usage Example
Here is an example of the core service management API:
```rust,no_run
use sal_service_manager::{create_service_manager, ServiceConfig};
use std::collections::HashMap;
fn main() -> Result<(), Box<dyn std::error::Error>> {
let service_manager = create_service_manager();
let config = ServiceConfig {
name: "my-service".to_string(),
binary_path: "/usr/local/bin/my-service-executable".to_string(),
args: vec!["--config".to_string(), "/etc/my-service.conf".to_string()],
working_directory: Some("/var/tmp".to_string()),
environment: HashMap::new(),
auto_restart: true,
};
// Start a new service
service_manager.start(&config)?;
// Get the status of the service
let status = service_manager.status("my-service")?;
println!("Service status: {:?}", status);
// Stop the service
service_manager.stop("my-service")?;
Ok(())
}
```
## Examples
Comprehensive examples are available in the SAL examples directory:
### Circle Worker Manager Example
The primary use case - dynamically launching circle workers for new freezone residents:
```bash
# Run the circle worker management example
herodo examples/service_manager/circle_worker_manager.rhai
```
This example demonstrates:
- Creating service configurations for circle workers
- Complete service lifecycle management
- Error handling and status monitoring
- Service cleanup and removal
### Basic Usage Example
A simpler example showing the core API:
```bash
# Run the basic usage example
herodo examples/service_manager/basic_usage.rhai
```
See `examples/service_manager/README.md` for detailed documentation.
## Testing
Run the test suite:
```bash
cargo test -p sal-service-manager
```
For Rhai integration tests:
```bash
cargo test -p sal-service-manager --features rhai
```
### Testing with Herodo
To test the service manager with real Rhai scripts using herodo, first build herodo:
```bash
./build_herodo.sh
```
Then run Rhai scripts that use the service manager:
```bash
herodo your_service_script.rhai
```
## Prerequisites
### Linux (zinit/systemd)
The service manager automatically discovers running zinit servers and falls back to systemd if none are found.
**For zinit (recommended):**
```bash
# Start zinit with default socket
zinit -s /tmp/zinit.sock init
# Or with a custom socket path
zinit -s /var/run/zinit.sock init
```
**Socket Discovery:**
The service manager will automatically find running zinit servers by checking:
1. `ZINIT_SOCKET_PATH` environment variable (if set)
2. Common socket locations: `/var/run/zinit.sock`, `/tmp/zinit.sock`, `/run/zinit.sock`, `./zinit.sock`
**Custom socket path:**
```bash
# Set custom socket path
export ZINIT_SOCKET_PATH=/your/custom/path/zinit.sock
```
**Systemd fallback:**
If no zinit server is detected, the service manager automatically falls back to systemd.
### macOS (launchctl)
No additional setup required - uses the built-in launchctl system.
## Platform Support
- **macOS**: Full support using `launchctl` for service management
- **Linux**: Full support using `zinit` for service management (systemd also available as alternative)
- **Windows**: Not currently supported

View File

@ -1,47 +0,0 @@
# Service Manager Examples
This directory contains examples demonstrating the usage of the `sal-service-manager` crate.
## Running Examples
To run any example, use the following command structure from the `service_manager` crate's root directory:
```sh
cargo run --example <EXAMPLE_NAME>
```
---
### 1. `simple_service`
This example demonstrates the ideal, clean lifecycle of a service using the separated `create` and `start` steps.
**Behavior:**
1. Creates a new service definition.
2. Starts the newly created service.
3. Checks its status to confirm it's running.
4. Stops the service.
5. Checks its status again to confirm it's stopped.
6. Removes the service definition.
**Run it:**
```sh
cargo run --example simple_service
```
### 2. `service_spaghetti`
This example demonstrates how the service manager handles "messy" or improper sequences of operations, showcasing its error handling and robustness.
**Behavior:**
1. Creates a service.
2. Starts the service.
3. Tries to start the **same service again** (which should fail as it's already running).
4. Removes the service **without stopping it first** (the manager should handle this gracefully).
5. Tries to stop the **already removed** service (which should fail).
6. Tries to remove the service **again** (which should also fail).
**Run it:**
```sh
cargo run --example service_spaghetti
```

View File

@ -1,109 +0,0 @@
//! service_spaghetti - An example of messy service management.
//!
//! This example demonstrates how the service manager behaves when commands
//! are issued in a less-than-ideal order, such as starting a service that's
//! already running or removing a service that hasn't been stopped.
use sal_service_manager::{create_service_manager, ServiceConfig};
use std::collections::HashMap;
use std::thread;
use std::time::Duration;
fn main() {
// Initialize logging to see socket discovery in action
env_logger::init();
let manager = match create_service_manager() {
Ok(manager) => manager,
Err(e) => {
eprintln!("Error: Failed to create service manager: {}", e);
return;
}
};
let service_name = "com.herocode.examples.spaghetti";
let service_config = ServiceConfig {
name: service_name.to_string(),
binary_path: "/bin/sh".to_string(),
args: vec![
"-c".to_string(),
"while true; do echo 'Spaghetti service is running...'; sleep 5; done".to_string(),
],
working_directory: None,
environment: HashMap::new(),
auto_restart: false,
};
println!("--- Service Spaghetti Example ---");
println!("This example demonstrates messy, error-prone service management.");
// Cleanup from previous runs to ensure a clean slate
if let Ok(true) = manager.exists(service_name) {
println!(
"\nService '{}' found from a previous run. Cleaning up first.",
service_name
);
let _ = manager.stop(service_name);
let _ = manager.remove(service_name);
println!("Cleanup complete.");
}
// 1. Start the service (creates and starts in one step)
println!("\n1. Starting the service for the first time...");
match manager.start(&service_config) {
Ok(()) => println!(" -> Success: Service '{}' started.", service_name),
Err(e) => {
eprintln!(
" -> Error: Failed to start service: {}. Halting example.",
e
);
return;
}
}
thread::sleep(Duration::from_secs(2));
// 2. Try to start the service again while it's already running
println!("\n2. Trying to start the *same service* again...");
match manager.start(&service_config) {
Ok(()) => println!(" -> Unexpected Success: Service started again."),
Err(e) => eprintln!(
" -> Expected Error: {}. The manager should detect it is already running.",
e
),
}
// 3. Let it run for a bit
println!("\n3. Letting the service run for 5 seconds...");
thread::sleep(Duration::from_secs(5));
// 4. Remove the service without stopping it first
// The `remove` function is designed to stop the service if it's running.
println!("\n4. Removing the service without explicitly stopping it first...");
match manager.remove(service_name) {
Ok(()) => println!(" -> Success: Service was stopped and removed."),
Err(e) => eprintln!(" -> Error: Failed to remove service: {}", e),
}
// 5. Try to stop the service after it has been removed
println!("\n5. Trying to stop the service that was just removed...");
match manager.stop(service_name) {
Ok(()) => println!(" -> Unexpected Success: Stopped a removed service."),
Err(e) => eprintln!(
" -> Expected Error: {}. The manager knows the service is gone.",
e
),
}
// 6. Try to remove the service again
println!("\n6. Trying to remove the service again...");
match manager.remove(service_name) {
Ok(()) => println!(" -> Unexpected Success: Removed a non-existent service."),
Err(e) => eprintln!(
" -> Expected Error: {}. The manager correctly reports it's not found.",
e
),
}
println!("\n--- Spaghetti Example Finished ---");
}

View File

@ -1,110 +0,0 @@
use sal_service_manager::{create_service_manager, ServiceConfig};
use std::collections::HashMap;
use std::thread;
use std::time::Duration;
fn main() {
// Initialize logging to see socket discovery in action
env_logger::init();
// 1. Create a service manager for the current platform
let manager = match create_service_manager() {
Ok(manager) => manager,
Err(e) => {
eprintln!("Error: Failed to create service manager: {}", e);
return;
}
};
// 2. Define the configuration for our new service
let service_name = "com.herocode.examples.simpleservice";
let service_config = ServiceConfig {
name: service_name.to_string(),
// A simple command that runs in a loop
binary_path: "/bin/sh".to_string(),
args: vec![
"-c".to_string(),
"while true; do echo 'Simple service is running...'; date; sleep 5; done".to_string(),
],
working_directory: None,
environment: HashMap::new(),
auto_restart: false,
};
println!("--- Service Manager Example ---");
// Cleanup from previous runs, if necessary
if let Ok(true) = manager.exists(service_name) {
println!(
"Service '{}' already exists. Cleaning up before starting.",
service_name
);
if let Err(e) = manager.stop(service_name) {
println!(
"Note: could not stop existing service (it might not be running): {}",
e
);
}
if let Err(e) = manager.remove(service_name) {
eprintln!("Error: failed to remove existing service: {}", e);
return;
}
println!("Cleanup complete.");
}
// 3. Start the service (creates and starts in one step)
println!("\n1. Starting service: '{}'", service_name);
match manager.start(&service_config) {
Ok(()) => println!("Service '{}' started successfully.", service_name),
Err(e) => {
eprintln!("Error: Failed to start service '{}': {}", service_name, e);
return;
}
}
// Give it a moment to run
println!("\nWaiting for 2 seconds for the service to initialize...");
thread::sleep(Duration::from_secs(2));
// 4. Check the status of the service
println!("\n2. Checking service status...");
match manager.status(service_name) {
Ok(status) => println!("Service status: {:?}", status),
Err(e) => eprintln!(
"Error: Failed to get status for service '{}': {}",
service_name, e
),
}
println!("\nLetting the service run for 10 seconds. Check logs if you can.");
thread::sleep(Duration::from_secs(10));
// 5. Stop the service
println!("\n3. Stopping service: '{}'", service_name);
match manager.stop(service_name) {
Ok(()) => println!("Service '{}' stopped successfully.", service_name),
Err(e) => eprintln!("Error: Failed to stop service '{}': {}", service_name, e),
}
println!("\nWaiting for 2 seconds for the service to stop...");
thread::sleep(Duration::from_secs(2));
// Check status again
println!("\n4. Checking status after stopping...");
match manager.status(service_name) {
Ok(status) => println!("Service status: {:?}", status),
Err(e) => eprintln!(
"Error: Failed to get status for service '{}': {}",
service_name, e
),
}
// 6. Remove the service
println!("\n5. Removing service: '{}'", service_name);
match manager.remove(service_name) {
Ok(()) => println!("Service '{}' removed successfully.", service_name),
Err(e) => eprintln!("Error: Failed to remove service '{}': {}", service_name, e),
}
println!("\n--- Example Finished ---");
}

View File

@ -1,47 +0,0 @@
//! Socket Discovery Test
//!
//! This example demonstrates the zinit socket discovery functionality.
//! It shows how the service manager finds available zinit sockets.
use sal_service_manager::create_service_manager;
fn main() {
// Initialize logging to see socket discovery in action
env_logger::init();
println!("=== Zinit Socket Discovery Test ===");
println!("This test demonstrates how the service manager discovers zinit sockets.");
println!();
// Test environment variable
if let Ok(socket_path) = std::env::var("ZINIT_SOCKET_PATH") {
println!("🔍 ZINIT_SOCKET_PATH environment variable set to: {}", socket_path);
} else {
println!("🔍 ZINIT_SOCKET_PATH environment variable not set");
}
println!();
println!("🚀 Creating service manager...");
match create_service_manager() {
Ok(_manager) => {
println!("✅ Service manager created successfully!");
#[cfg(target_os = "macos")]
println!("📱 Platform: macOS - Using launchctl");
#[cfg(target_os = "linux")]
println!("🐧 Platform: Linux - Check logs above for socket discovery details");
}
Err(e) => {
println!("❌ Failed to create service manager: {}", e);
}
}
println!();
println!("=== Test Complete ===");
println!();
println!("To test zinit socket discovery on Linux:");
println!("1. Start zinit: zinit -s /tmp/zinit.sock init");
println!("2. Run with logging: RUST_LOG=debug cargo run --example socket_discovery_test -p sal-service-manager");
println!("3. Or set custom path: ZINIT_SOCKET_PATH=/custom/path.sock RUST_LOG=debug cargo run --example socket_discovery_test -p sal-service-manager");
}

View File

@ -1,301 +0,0 @@
use std::collections::HashMap;
use thiserror::Error;
#[derive(Error, Debug)]
pub enum ServiceManagerError {
#[error("Service '{0}' not found")]
ServiceNotFound(String),
#[error("Service '{0}' already exists")]
ServiceAlreadyExists(String),
#[error("Failed to start service '{0}': {1}")]
StartFailed(String, String),
#[error("Failed to stop service '{0}': {1}")]
StopFailed(String, String),
#[error("Failed to restart service '{0}': {1}")]
RestartFailed(String, String),
#[error("Failed to get logs for service '{0}': {1}")]
LogsFailed(String, String),
#[error("IO error: {0}")]
IoError(#[from] std::io::Error),
#[error("Service manager error: {0}")]
Other(String),
}
#[derive(Debug, Clone)]
pub struct ServiceConfig {
pub name: String,
pub binary_path: String,
pub args: Vec<String>,
pub working_directory: Option<String>,
pub environment: HashMap<String, String>,
pub auto_restart: bool,
}
#[derive(Debug, Clone, PartialEq)]
pub enum ServiceStatus {
Running,
Stopped,
Failed,
Unknown,
}
pub trait ServiceManager: Send + Sync {
/// Check if a service exists
fn exists(&self, service_name: &str) -> Result<bool, ServiceManagerError>;
/// Start a service with the given configuration
fn start(&self, config: &ServiceConfig) -> Result<(), ServiceManagerError>;
/// Start an existing service by name (load existing plist/config)
fn start_existing(&self, service_name: &str) -> Result<(), ServiceManagerError>;
/// Start a service and wait for confirmation that it's running or failed
fn start_and_confirm(
&self,
config: &ServiceConfig,
timeout_secs: u64,
) -> Result<(), ServiceManagerError>;
/// Start an existing service and wait for confirmation that it's running or failed
fn start_existing_and_confirm(
&self,
service_name: &str,
timeout_secs: u64,
) -> Result<(), ServiceManagerError>;
/// Stop a service by name
fn stop(&self, service_name: &str) -> Result<(), ServiceManagerError>;
/// Restart a service by name
fn restart(&self, service_name: &str) -> Result<(), ServiceManagerError>;
/// Get the status of a service
fn status(&self, service_name: &str) -> Result<ServiceStatus, ServiceManagerError>;
/// Get logs for a service
fn logs(&self, service_name: &str, lines: Option<usize>)
-> Result<String, ServiceManagerError>;
/// List all managed services
fn list(&self) -> Result<Vec<String>, ServiceManagerError>;
/// Remove a service configuration (stop if running)
fn remove(&self, service_name: &str) -> Result<(), ServiceManagerError>;
}
// Platform-specific implementations
#[cfg(target_os = "macos")]
mod launchctl;
#[cfg(target_os = "macos")]
pub use launchctl::LaunchctlServiceManager;
#[cfg(target_os = "linux")]
mod systemd;
#[cfg(target_os = "linux")]
pub use systemd::SystemdServiceManager;
mod zinit;
pub use zinit::ZinitServiceManager;
#[cfg(feature = "rhai")]
pub mod rhai;
/// Discover available zinit socket paths
///
/// This function checks for zinit sockets in the following order:
/// 1. Environment variable ZINIT_SOCKET_PATH (if set)
/// 2. Common socket locations with connectivity testing
///
/// # Returns
///
/// Returns the first working socket path found, or None if no working zinit server is detected.
#[cfg(target_os = "linux")]
fn discover_zinit_socket() -> Option<String> {
// First check environment variable
if let Ok(env_socket_path) = std::env::var("ZINIT_SOCKET_PATH") {
log::debug!("Checking ZINIT_SOCKET_PATH: {}", env_socket_path);
if test_zinit_socket(&env_socket_path) {
log::info!(
"Using zinit socket from ZINIT_SOCKET_PATH: {}",
env_socket_path
);
return Some(env_socket_path);
} else {
log::warn!(
"ZINIT_SOCKET_PATH specified but socket is not accessible: {}",
env_socket_path
);
}
}
// Try common socket locations
let common_paths = [
"/var/run/zinit.sock",
"/tmp/zinit.sock",
"/run/zinit.sock",
"./zinit.sock",
];
log::debug!("Discovering zinit socket from common locations...");
for path in &common_paths {
log::debug!("Testing socket path: {}", path);
if test_zinit_socket(path) {
log::info!("Found working zinit socket at: {}", path);
return Some(path.to_string());
}
}
log::debug!("No working zinit socket found");
None
}
/// Test if a zinit socket is accessible and responsive
///
/// This function attempts to create a ZinitServiceManager and perform a basic
/// connectivity test by listing services.
#[cfg(target_os = "linux")]
fn test_zinit_socket(socket_path: &str) -> bool {
// Check if socket file exists first
if !std::path::Path::new(socket_path).exists() {
log::debug!("Socket file does not exist: {}", socket_path);
return false;
}
// Try to create a manager and test basic connectivity
match ZinitServiceManager::new(socket_path) {
Ok(manager) => {
// Test basic connectivity by trying to list services
match manager.list() {
Ok(_) => {
log::debug!("Socket {} is responsive", socket_path);
true
}
Err(e) => {
log::debug!("Socket {} exists but not responsive: {}", socket_path, e);
false
}
}
}
Err(e) => {
log::debug!("Failed to create manager for socket {}: {}", socket_path, e);
false
}
}
}
/// Create a service manager appropriate for the current platform
///
/// - On macOS: Uses launchctl for service management
/// - On Linux: Uses zinit for service management with systemd fallback
///
/// # Returns
///
/// Returns a Result containing the service manager or an error if initialization fails.
/// On Linux, it first tries to discover a working zinit socket. If no zinit server is found,
/// it will fall back to systemd.
///
/// # Environment Variables
///
/// - `ZINIT_SOCKET_PATH`: Specifies the zinit socket path (Linux only)
///
/// # Errors
///
/// Returns `ServiceManagerError` if:
/// - The platform is not supported (Windows, etc.)
/// - Service manager initialization fails on all available backends
pub fn create_service_manager() -> Result<Box<dyn ServiceManager>, ServiceManagerError> {
#[cfg(target_os = "macos")]
{
Ok(Box::new(LaunchctlServiceManager::new()))
}
#[cfg(target_os = "linux")]
{
// Try to discover a working zinit socket
if let Some(socket_path) = discover_zinit_socket() {
match ZinitServiceManager::new(&socket_path) {
Ok(zinit_manager) => {
log::info!("Using zinit service manager with socket: {}", socket_path);
return Ok(Box::new(zinit_manager));
}
Err(zinit_error) => {
log::warn!(
"Failed to create zinit manager for discovered socket {}: {}",
socket_path,
zinit_error
);
}
}
} else {
log::info!("No running zinit server detected. To use zinit, start it with: zinit -s /tmp/zinit.sock init");
}
// Fallback to systemd
log::info!("Falling back to systemd service manager");
Ok(Box::new(SystemdServiceManager::new()))
}
#[cfg(not(any(target_os = "macos", target_os = "linux")))]
{
Err(ServiceManagerError::Other(
"Service manager not implemented for this platform".to_string(),
))
}
}
/// Create a service manager for zinit with a custom socket path
///
/// This is useful when zinit is running with a non-default socket path
pub fn create_zinit_service_manager(
socket_path: &str,
) -> Result<Box<dyn ServiceManager>, ServiceManagerError> {
Ok(Box::new(ZinitServiceManager::new(socket_path)?))
}
/// Create a service manager for systemd (Linux alternative)
///
/// This creates a systemd-based service manager as an alternative to zinit on Linux
#[cfg(target_os = "linux")]
pub fn create_systemd_service_manager() -> Box<dyn ServiceManager> {
Box::new(SystemdServiceManager::new())
}
#[cfg(test)]
mod tests {
use super::*;
#[test]
fn test_create_service_manager() {
// This test ensures the service manager can be created without panicking
let result = create_service_manager();
assert!(result.is_ok(), "Service manager creation should succeed");
}
#[cfg(target_os = "linux")]
#[test]
fn test_socket_discovery_with_env_var() {
// Test that environment variable is respected
std::env::set_var("ZINIT_SOCKET_PATH", "/test/path.sock");
// The discover function should check the env var first
// Since the socket doesn't exist, it should return None, but we can't test
// the actual discovery logic without a real socket
std::env::remove_var("ZINIT_SOCKET_PATH");
}
#[cfg(target_os = "linux")]
#[test]
fn test_socket_discovery_without_env_var() {
// Ensure env var is not set
std::env::remove_var("ZINIT_SOCKET_PATH");
// The discover function should try common paths
// Since no zinit is running, it should return None
let result = discover_zinit_socket();
// This is expected to be None in test environment
assert!(
result.is_none(),
"Should return None when no zinit server is running"
);
}
}

View File

@ -1,256 +0,0 @@
//! Rhai integration for the service manager module
//!
//! This module provides Rhai scripting support for service management operations.
use crate::{create_service_manager, ServiceConfig, ServiceManager};
use rhai::{Engine, EvalAltResult, Map};
use std::collections::HashMap;
use std::sync::Arc;
/// A wrapper around ServiceManager that can be used in Rhai
#[derive(Clone)]
pub struct RhaiServiceManager {
inner: Arc<Box<dyn ServiceManager>>,
}
impl RhaiServiceManager {
pub fn new() -> Result<Self, Box<EvalAltResult>> {
let manager = create_service_manager()
.map_err(|e| format!("Failed to create service manager: {}", e))?;
Ok(Self {
inner: Arc::new(manager),
})
}
}
/// Register the service manager module with a Rhai engine
pub fn register_service_manager_module(engine: &mut Engine) -> Result<(), Box<EvalAltResult>> {
// Factory function to create service manager
engine.register_type::<RhaiServiceManager>();
engine.register_fn(
"create_service_manager",
|| -> Result<RhaiServiceManager, Box<EvalAltResult>> { RhaiServiceManager::new() },
);
// Service management functions
engine.register_fn(
"start",
|manager: &mut RhaiServiceManager, config: Map| -> Result<(), Box<EvalAltResult>> {
let service_config = map_to_service_config(config)?;
manager
.inner
.start(&service_config)
.map_err(|e| format!("Failed to start service: {}", e).into())
},
);
engine.register_fn(
"stop",
|manager: &mut RhaiServiceManager,
service_name: String|
-> Result<(), Box<EvalAltResult>> {
manager
.inner
.stop(&service_name)
.map_err(|e| format!("Failed to stop service: {}", e).into())
},
);
engine.register_fn(
"restart",
|manager: &mut RhaiServiceManager,
service_name: String|
-> Result<(), Box<EvalAltResult>> {
manager
.inner
.restart(&service_name)
.map_err(|e| format!("Failed to restart service: {}", e).into())
},
);
engine.register_fn(
"status",
|manager: &mut RhaiServiceManager,
service_name: String|
-> Result<String, Box<EvalAltResult>> {
let status = manager
.inner
.status(&service_name)
.map_err(|e| format!("Failed to get service status: {}", e))?;
Ok(format!("{:?}", status))
},
);
engine.register_fn(
"logs",
|manager: &mut RhaiServiceManager,
service_name: String,
lines: i64|
-> Result<String, Box<EvalAltResult>> {
let lines_opt = if lines > 0 {
Some(lines as usize)
} else {
None
};
manager
.inner
.logs(&service_name, lines_opt)
.map_err(|e| format!("Failed to get service logs: {}", e).into())
},
);
engine.register_fn(
"list",
|manager: &mut RhaiServiceManager| -> Result<Vec<String>, Box<EvalAltResult>> {
manager
.inner
.list()
.map_err(|e| format!("Failed to list services: {}", e).into())
},
);
engine.register_fn(
"remove",
|manager: &mut RhaiServiceManager,
service_name: String|
-> Result<(), Box<EvalAltResult>> {
manager
.inner
.remove(&service_name)
.map_err(|e| format!("Failed to remove service: {}", e).into())
},
);
engine.register_fn(
"exists",
|manager: &mut RhaiServiceManager,
service_name: String|
-> Result<bool, Box<EvalAltResult>> {
manager
.inner
.exists(&service_name)
.map_err(|e| format!("Failed to check if service exists: {}", e).into())
},
);
engine.register_fn(
"start_and_confirm",
|manager: &mut RhaiServiceManager,
config: Map,
timeout_secs: i64|
-> Result<(), Box<EvalAltResult>> {
let service_config = map_to_service_config(config)?;
let timeout = if timeout_secs > 0 {
timeout_secs as u64
} else {
30
};
manager
.inner
.start_and_confirm(&service_config, timeout)
.map_err(|e| format!("Failed to start and confirm service: {}", e).into())
},
);
engine.register_fn(
"start_existing_and_confirm",
|manager: &mut RhaiServiceManager,
service_name: String,
timeout_secs: i64|
-> Result<(), Box<EvalAltResult>> {
let timeout = if timeout_secs > 0 {
timeout_secs as u64
} else {
30
};
manager
.inner
.start_existing_and_confirm(&service_name, timeout)
.map_err(|e| format!("Failed to start existing service and confirm: {}", e).into())
},
);
Ok(())
}
/// Convert a Rhai Map to a ServiceConfig
fn map_to_service_config(map: Map) -> Result<ServiceConfig, Box<EvalAltResult>> {
let name = map
.get("name")
.and_then(|v| v.clone().into_string().ok())
.ok_or("Service config must have a 'name' field")?;
let binary_path = map
.get("binary_path")
.and_then(|v| v.clone().into_string().ok())
.ok_or("Service config must have a 'binary_path' field")?;
let args = map
.get("args")
.and_then(|v| v.clone().try_cast::<rhai::Array>())
.map(|arr| {
arr.into_iter()
.filter_map(|v| v.into_string().ok())
.collect::<Vec<String>>()
})
.unwrap_or_default();
let working_directory = map
.get("working_directory")
.and_then(|v| v.clone().into_string().ok());
let environment = map
.get("environment")
.and_then(|v| v.clone().try_cast::<Map>())
.map(|env_map| {
env_map
.into_iter()
.filter_map(|(k, v)| v.into_string().ok().map(|val| (k.to_string(), val)))
.collect::<HashMap<String, String>>()
})
.unwrap_or_default();
let auto_restart = map
.get("auto_restart")
.and_then(|v| v.as_bool().ok())
.unwrap_or(false);
Ok(ServiceConfig {
name,
binary_path,
args,
working_directory,
environment,
auto_restart,
})
}
#[cfg(test)]
mod tests {
use super::*;
use rhai::{Engine, Map};
#[test]
fn test_register_service_manager_module() {
let mut engine = Engine::new();
register_service_manager_module(&mut engine).unwrap();
// Test that the functions are registered
// Note: Rhai doesn't expose a public API to check if functions are registered
// So we'll just verify the module registration doesn't panic
assert!(true);
}
#[test]
fn test_map_to_service_config() {
let mut map = Map::new();
map.insert("name".into(), "test-service".into());
map.insert("binary_path".into(), "/bin/echo".into());
map.insert("auto_restart".into(), true.into());
let config = map_to_service_config(map).unwrap();
assert_eq!(config.name, "test-service");
assert_eq!(config.binary_path, "/bin/echo");
assert_eq!(config.auto_restart, true);
}
}

View File

@ -1,434 +0,0 @@
use crate::{ServiceConfig, ServiceManager, ServiceManagerError, ServiceStatus};
use std::fs;
use std::path::PathBuf;
use std::process::Command;
#[derive(Debug)]
pub struct SystemdServiceManager {
service_prefix: String,
user_mode: bool,
}
impl SystemdServiceManager {
pub fn new() -> Self {
Self {
service_prefix: "sal".to_string(),
user_mode: true, // Default to user services for safety
}
}
pub fn new_system() -> Self {
Self {
service_prefix: "sal".to_string(),
user_mode: false, // System-wide services (requires root)
}
}
fn get_service_name(&self, service_name: &str) -> String {
format!("{}-{}.service", self.service_prefix, service_name)
}
fn get_unit_file_path(&self, service_name: &str) -> PathBuf {
let service_file = self.get_service_name(service_name);
if self.user_mode {
// User service directory
let home = std::env::var("HOME").unwrap_or_else(|_| "/tmp".to_string());
PathBuf::from(home)
.join(".config")
.join("systemd")
.join("user")
.join(service_file)
} else {
// System service directory
PathBuf::from("/etc/systemd/system").join(service_file)
}
}
fn run_systemctl(&self, args: &[&str]) -> Result<String, ServiceManagerError> {
let mut cmd = Command::new("systemctl");
if self.user_mode {
cmd.arg("--user");
}
cmd.args(args);
let output = cmd
.output()
.map_err(|e| ServiceManagerError::Other(format!("Failed to run systemctl: {}", e)))?;
if !output.status.success() {
let stderr = String::from_utf8_lossy(&output.stderr);
return Err(ServiceManagerError::Other(format!(
"systemctl command failed: {}",
stderr
)));
}
Ok(String::from_utf8_lossy(&output.stdout).to_string())
}
fn create_unit_file(&self, config: &ServiceConfig) -> Result<(), ServiceManagerError> {
let unit_path = self.get_unit_file_path(&config.name);
// Ensure the directory exists
if let Some(parent) = unit_path.parent() {
fs::create_dir_all(parent).map_err(|e| {
ServiceManagerError::Other(format!("Failed to create unit directory: {}", e))
})?;
}
// Create the unit file content
let mut unit_content = String::new();
unit_content.push_str("[Unit]\n");
unit_content.push_str(&format!("Description={} service\n", config.name));
unit_content.push_str("After=network.target\n\n");
unit_content.push_str("[Service]\n");
unit_content.push_str("Type=simple\n");
// Build the ExecStart command
let mut exec_start = config.binary_path.clone();
for arg in &config.args {
exec_start.push(' ');
exec_start.push_str(arg);
}
unit_content.push_str(&format!("ExecStart={}\n", exec_start));
if let Some(working_dir) = &config.working_directory {
unit_content.push_str(&format!("WorkingDirectory={}\n", working_dir));
}
// Add environment variables
for (key, value) in &config.environment {
unit_content.push_str(&format!("Environment=\"{}={}\"\n", key, value));
}
if config.auto_restart {
unit_content.push_str("Restart=always\n");
unit_content.push_str("RestartSec=5\n");
}
unit_content.push_str("\n[Install]\n");
unit_content.push_str("WantedBy=default.target\n");
// Write the unit file
fs::write(&unit_path, unit_content)
.map_err(|e| ServiceManagerError::Other(format!("Failed to write unit file: {}", e)))?;
// Reload systemd to pick up the new unit file
self.run_systemctl(&["daemon-reload"])?;
Ok(())
}
}
impl ServiceManager for SystemdServiceManager {
fn exists(&self, service_name: &str) -> Result<bool, ServiceManagerError> {
let unit_path = self.get_unit_file_path(service_name);
Ok(unit_path.exists())
}
fn start(&self, config: &ServiceConfig) -> Result<(), ServiceManagerError> {
let service_name = self.get_service_name(&config.name);
// Check if service already exists and is running
if self.exists(&config.name)? {
match self.status(&config.name)? {
ServiceStatus::Running => {
return Err(ServiceManagerError::ServiceAlreadyExists(
config.name.clone(),
));
}
_ => {
// Service exists but not running, we can start it
}
}
} else {
// Create the unit file
self.create_unit_file(config)?;
}
// Enable and start the service
self.run_systemctl(&["enable", &service_name])
.map_err(|e| ServiceManagerError::StartFailed(config.name.clone(), e.to_string()))?;
self.run_systemctl(&["start", &service_name])
.map_err(|e| ServiceManagerError::StartFailed(config.name.clone(), e.to_string()))?;
Ok(())
}
fn start_existing(&self, service_name: &str) -> Result<(), ServiceManagerError> {
let service_unit = self.get_service_name(service_name);
// Check if unit file exists
if !self.exists(service_name)? {
return Err(ServiceManagerError::ServiceNotFound(
service_name.to_string(),
));
}
// Check if already running
match self.status(service_name)? {
ServiceStatus::Running => {
return Ok(()); // Already running, nothing to do
}
_ => {
// Start the service
self.run_systemctl(&["start", &service_unit]).map_err(|e| {
ServiceManagerError::StartFailed(service_name.to_string(), e.to_string())
})?;
}
}
Ok(())
}
fn start_and_confirm(
&self,
config: &ServiceConfig,
timeout_secs: u64,
) -> Result<(), ServiceManagerError> {
// Start the service first
self.start(config)?;
// Wait for confirmation with timeout
let start_time = std::time::Instant::now();
let timeout_duration = std::time::Duration::from_secs(timeout_secs);
while start_time.elapsed() < timeout_duration {
match self.status(&config.name) {
Ok(ServiceStatus::Running) => return Ok(()),
Ok(ServiceStatus::Failed) => {
return Err(ServiceManagerError::StartFailed(
config.name.clone(),
"Service failed to start".to_string(),
));
}
Ok(_) => {
// Still starting, wait a bit
std::thread::sleep(std::time::Duration::from_millis(100));
}
Err(_) => {
// Service might not exist yet, wait a bit
std::thread::sleep(std::time::Duration::from_millis(100));
}
}
}
Err(ServiceManagerError::StartFailed(
config.name.clone(),
format!("Service did not start within {} seconds", timeout_secs),
))
}
fn start_existing_and_confirm(
&self,
service_name: &str,
timeout_secs: u64,
) -> Result<(), ServiceManagerError> {
// Start the existing service first
self.start_existing(service_name)?;
// Wait for confirmation with timeout
let start_time = std::time::Instant::now();
let timeout_duration = std::time::Duration::from_secs(timeout_secs);
while start_time.elapsed() < timeout_duration {
match self.status(service_name) {
Ok(ServiceStatus::Running) => return Ok(()),
Ok(ServiceStatus::Failed) => {
return Err(ServiceManagerError::StartFailed(
service_name.to_string(),
"Service failed to start".to_string(),
));
}
Ok(_) => {
// Still starting, wait a bit
std::thread::sleep(std::time::Duration::from_millis(100));
}
Err(_) => {
// Service might not exist yet, wait a bit
std::thread::sleep(std::time::Duration::from_millis(100));
}
}
}
Err(ServiceManagerError::StartFailed(
service_name.to_string(),
format!("Service did not start within {} seconds", timeout_secs),
))
}
fn stop(&self, service_name: &str) -> Result<(), ServiceManagerError> {
let service_unit = self.get_service_name(service_name);
// Check if service exists
if !self.exists(service_name)? {
return Err(ServiceManagerError::ServiceNotFound(
service_name.to_string(),
));
}
// Stop the service
self.run_systemctl(&["stop", &service_unit]).map_err(|e| {
ServiceManagerError::StopFailed(service_name.to_string(), e.to_string())
})?;
Ok(())
}
fn restart(&self, service_name: &str) -> Result<(), ServiceManagerError> {
let service_unit = self.get_service_name(service_name);
// Check if service exists
if !self.exists(service_name)? {
return Err(ServiceManagerError::ServiceNotFound(
service_name.to_string(),
));
}
// Restart the service
self.run_systemctl(&["restart", &service_unit])
.map_err(|e| {
ServiceManagerError::RestartFailed(service_name.to_string(), e.to_string())
})?;
Ok(())
}
fn status(&self, service_name: &str) -> Result<ServiceStatus, ServiceManagerError> {
let service_unit = self.get_service_name(service_name);
// Check if service exists
if !self.exists(service_name)? {
return Err(ServiceManagerError::ServiceNotFound(
service_name.to_string(),
));
}
// Get service status
let output = self
.run_systemctl(&["is-active", &service_unit])
.unwrap_or_else(|_| "unknown".to_string());
let status = match output.trim() {
"active" => ServiceStatus::Running,
"inactive" => ServiceStatus::Stopped,
"failed" => ServiceStatus::Failed,
_ => ServiceStatus::Unknown,
};
Ok(status)
}
fn logs(
&self,
service_name: &str,
lines: Option<usize>,
) -> Result<String, ServiceManagerError> {
let service_unit = self.get_service_name(service_name);
// Check if service exists
if !self.exists(service_name)? {
return Err(ServiceManagerError::ServiceNotFound(
service_name.to_string(),
));
}
// Build journalctl command
let mut args = vec!["--unit", &service_unit, "--no-pager"];
let lines_arg;
if let Some(n) = lines {
lines_arg = format!("--lines={}", n);
args.push(&lines_arg);
}
// Use journalctl to get logs
let mut cmd = std::process::Command::new("journalctl");
if self.user_mode {
cmd.arg("--user");
}
cmd.args(&args);
let output = cmd.output().map_err(|e| {
ServiceManagerError::LogsFailed(
service_name.to_string(),
format!("Failed to run journalctl: {}", e),
)
})?;
if !output.status.success() {
let stderr = String::from_utf8_lossy(&output.stderr);
return Err(ServiceManagerError::LogsFailed(
service_name.to_string(),
format!("journalctl command failed: {}", stderr),
));
}
Ok(String::from_utf8_lossy(&output.stdout).to_string())
}
fn list(&self) -> Result<Vec<String>, ServiceManagerError> {
// List all services with our prefix
let output =
self.run_systemctl(&["list-units", "--type=service", "--all", "--no-pager"])?;
let mut services = Vec::new();
for line in output.lines() {
if line.contains(&format!("{}-", self.service_prefix)) {
// Extract service name from the line
if let Some(unit_name) = line.split_whitespace().next() {
if let Some(service_name) = unit_name.strip_suffix(".service") {
if let Some(name) =
service_name.strip_prefix(&format!("{}-", self.service_prefix))
{
services.push(name.to_string());
}
}
}
}
}
Ok(services)
}
fn remove(&self, service_name: &str) -> Result<(), ServiceManagerError> {
let service_unit = self.get_service_name(service_name);
// Check if service exists
if !self.exists(service_name)? {
return Err(ServiceManagerError::ServiceNotFound(
service_name.to_string(),
));
}
// Try to stop the service first, but don't fail if it's already stopped
if let Err(e) = self.stop(service_name) {
log::warn!(
"Failed to stop service '{}' before removal: {}",
service_name,
e
);
}
// Disable the service
if let Err(e) = self.run_systemctl(&["disable", &service_unit]) {
log::warn!("Failed to disable service '{}': {}", service_name, e);
}
// Remove the unit file
let unit_path = self.get_unit_file_path(service_name);
if unit_path.exists() {
std::fs::remove_file(&unit_path).map_err(|e| {
ServiceManagerError::Other(format!("Failed to remove unit file: {}", e))
})?;
}
// Reload systemd to pick up the changes
self.run_systemctl(&["daemon-reload"])?;
Ok(())
}
}

View File

@ -1,379 +0,0 @@
use crate::{ServiceConfig, ServiceManager, ServiceManagerError, ServiceStatus};
use once_cell::sync::Lazy;
use serde_json::json;
use std::sync::Arc;
use std::time::Duration;
use tokio::runtime::Runtime;
use tokio::time::timeout;
use zinit_client::{ServiceStatus as ZinitServiceStatus, ZinitClient, ZinitError};
// Shared runtime for async operations - production-safe initialization
static ASYNC_RUNTIME: Lazy<Option<Runtime>> = Lazy::new(|| Runtime::new().ok());
/// Get the async runtime, creating a temporary one if the static runtime failed
fn get_runtime() -> Result<Runtime, ServiceManagerError> {
// Try to use the static runtime first
if let Some(_runtime) = ASYNC_RUNTIME.as_ref() {
// We can't return a reference to the static runtime because we need ownership
// for block_on, so we create a new one. This is a reasonable trade-off for safety.
Runtime::new().map_err(|e| {
ServiceManagerError::Other(format!("Failed to create async runtime: {}", e))
})
} else {
// Static runtime failed, try to create a new one
Runtime::new().map_err(|e| {
ServiceManagerError::Other(format!("Failed to create async runtime: {}", e))
})
}
}
pub struct ZinitServiceManager {
client: Arc<ZinitClient>,
}
impl ZinitServiceManager {
pub fn new(socket_path: &str) -> Result<Self, ServiceManagerError> {
// Create the base zinit client directly
let client = Arc::new(ZinitClient::new(socket_path));
Ok(ZinitServiceManager { client })
}
/// Execute an async operation using the shared runtime or current context
fn execute_async<F, T>(&self, operation: F) -> Result<T, ServiceManagerError>
where
F: std::future::Future<Output = Result<T, ZinitError>> + Send + 'static,
T: Send + 'static,
{
// Check if we're already in a tokio runtime context
if let Ok(_handle) = tokio::runtime::Handle::try_current() {
// We're in an async context, use spawn_blocking to avoid nested runtime
let result = std::thread::spawn(
move || -> Result<Result<T, ZinitError>, ServiceManagerError> {
let rt = Runtime::new().map_err(|e| {
ServiceManagerError::Other(format!("Failed to create runtime: {}", e))
})?;
Ok(rt.block_on(operation))
},
)
.join()
.map_err(|_| ServiceManagerError::Other("Thread join failed".to_string()))?;
result?.map_err(|e| ServiceManagerError::Other(e.to_string()))
} else {
// No current runtime, use production-safe runtime
let runtime = get_runtime()?;
runtime
.block_on(operation)
.map_err(|e| ServiceManagerError::Other(e.to_string()))
}
}
/// Execute an async operation with timeout using the shared runtime or current context
fn execute_async_with_timeout<F, T>(
&self,
operation: F,
timeout_secs: u64,
) -> Result<T, ServiceManagerError>
where
F: std::future::Future<Output = Result<T, ZinitError>> + Send + 'static,
T: Send + 'static,
{
let timeout_duration = Duration::from_secs(timeout_secs);
let timeout_op = timeout(timeout_duration, operation);
// Check if we're already in a tokio runtime context
if let Ok(_handle) = tokio::runtime::Handle::try_current() {
// We're in an async context, use spawn_blocking to avoid nested runtime
let result = std::thread::spawn(move || {
let rt = tokio::runtime::Runtime::new().unwrap();
rt.block_on(timeout_op)
})
.join()
.map_err(|_| ServiceManagerError::Other("Thread join failed".to_string()))?;
result
.map_err(|_| {
ServiceManagerError::Other(format!(
"Operation timed out after {} seconds",
timeout_secs
))
})?
.map_err(|e| ServiceManagerError::Other(e.to_string()))
} else {
// No current runtime, use production-safe runtime
let runtime = get_runtime()?;
runtime
.block_on(timeout_op)
.map_err(|_| {
ServiceManagerError::Other(format!(
"Operation timed out after {} seconds",
timeout_secs
))
})?
.map_err(|e| ServiceManagerError::Other(e.to_string()))
}
}
}
impl ServiceManager for ZinitServiceManager {
fn exists(&self, service_name: &str) -> Result<bool, ServiceManagerError> {
let status_res = self.status(service_name);
match status_res {
Ok(_) => Ok(true),
Err(ServiceManagerError::ServiceNotFound(_)) => Ok(false),
Err(e) => Err(e),
}
}
fn start(&self, config: &ServiceConfig) -> Result<(), ServiceManagerError> {
// Build the exec command with args
let mut exec_command = config.binary_path.clone();
if !config.args.is_empty() {
exec_command.push(' ');
exec_command.push_str(&config.args.join(" "));
}
// Create zinit-compatible service configuration
let mut service_config = json!({
"exec": exec_command,
"oneshot": !config.auto_restart, // zinit uses oneshot, not restart
"env": config.environment,
});
// Add optional fields if present
if let Some(ref working_dir) = config.working_directory {
// Zinit doesn't support working_directory directly, so we need to modify the exec command
let cd_command = format!("cd {} && {}", working_dir, exec_command);
service_config["exec"] = json!(cd_command);
}
let client = Arc::clone(&self.client);
let service_name = config.name.clone();
self.execute_async(
async move { client.create_service(&service_name, service_config).await },
)
.map_err(|e| ServiceManagerError::StartFailed(config.name.clone(), e.to_string()))?;
self.start_existing(&config.name)
}
fn start_existing(&self, service_name: &str) -> Result<(), ServiceManagerError> {
let client = Arc::clone(&self.client);
let service_name_owned = service_name.to_string();
let service_name_for_error = service_name.to_string();
self.execute_async(async move { client.start(&service_name_owned).await })
.map_err(|e| ServiceManagerError::StartFailed(service_name_for_error, e.to_string()))
}
fn start_and_confirm(
&self,
config: &ServiceConfig,
timeout_secs: u64,
) -> Result<(), ServiceManagerError> {
// Start the service first
self.start(config)?;
// Wait for confirmation with timeout using the shared runtime
self.execute_async_with_timeout(
async move {
let start_time = std::time::Instant::now();
let timeout_duration = Duration::from_secs(timeout_secs);
while start_time.elapsed() < timeout_duration {
// We need to call status in a blocking way from within the async context
// For now, we'll use a simple polling approach
tokio::time::sleep(Duration::from_millis(100)).await;
}
// Return a timeout error that will be handled by execute_async_with_timeout
// Use a generic error since we don't know the exact ZinitError variants
Err(ZinitError::from(std::io::Error::new(
std::io::ErrorKind::TimedOut,
"Timeout waiting for service confirmation",
)))
},
timeout_secs,
)?;
// Check final status
match self.status(&config.name)? {
ServiceStatus::Running => Ok(()),
ServiceStatus::Failed => Err(ServiceManagerError::StartFailed(
config.name.clone(),
"Service failed to start".to_string(),
)),
_ => Err(ServiceManagerError::StartFailed(
config.name.clone(),
format!("Service did not start within {} seconds", timeout_secs),
)),
}
}
fn start_existing_and_confirm(
&self,
service_name: &str,
timeout_secs: u64,
) -> Result<(), ServiceManagerError> {
// Start the existing service first
self.start_existing(service_name)?;
// Wait for confirmation with timeout using the shared runtime
self.execute_async_with_timeout(
async move {
let start_time = std::time::Instant::now();
let timeout_duration = Duration::from_secs(timeout_secs);
while start_time.elapsed() < timeout_duration {
tokio::time::sleep(Duration::from_millis(100)).await;
}
// Return a timeout error that will be handled by execute_async_with_timeout
// Use a generic error since we don't know the exact ZinitError variants
Err(ZinitError::from(std::io::Error::new(
std::io::ErrorKind::TimedOut,
"Timeout waiting for service confirmation",
)))
},
timeout_secs,
)?;
// Check final status
match self.status(service_name)? {
ServiceStatus::Running => Ok(()),
ServiceStatus::Failed => Err(ServiceManagerError::StartFailed(
service_name.to_string(),
"Service failed to start".to_string(),
)),
_ => Err(ServiceManagerError::StartFailed(
service_name.to_string(),
format!("Service did not start within {} seconds", timeout_secs),
)),
}
}
fn stop(&self, service_name: &str) -> Result<(), ServiceManagerError> {
let client = Arc::clone(&self.client);
let service_name_owned = service_name.to_string();
let service_name_for_error = service_name.to_string();
self.execute_async(async move { client.stop(&service_name_owned).await })
.map_err(|e| ServiceManagerError::StopFailed(service_name_for_error, e.to_string()))
}
fn restart(&self, service_name: &str) -> Result<(), ServiceManagerError> {
let client = Arc::clone(&self.client);
let service_name_owned = service_name.to_string();
let service_name_for_error = service_name.to_string();
self.execute_async(async move { client.restart(&service_name_owned).await })
.map_err(|e| ServiceManagerError::RestartFailed(service_name_for_error, e.to_string()))
}
fn status(&self, service_name: &str) -> Result<ServiceStatus, ServiceManagerError> {
let client = Arc::clone(&self.client);
let service_name_owned = service_name.to_string();
let service_name_for_error = service_name.to_string();
let status: ZinitServiceStatus = self
.execute_async(async move { client.status(&service_name_owned).await })
.map_err(|e| {
// Check if this is a "service not found" error
if e.to_string().contains("not found") || e.to_string().contains("does not exist") {
ServiceManagerError::ServiceNotFound(service_name_for_error)
} else {
ServiceManagerError::Other(e.to_string())
}
})?;
// ServiceStatus is a struct with fields, not an enum
// We need to check the state field to determine the status
// Convert ServiceState to string and match on that
let state_str = format!("{:?}", status.state).to_lowercase();
let service_status = match state_str.as_str() {
s if s.contains("running") => crate::ServiceStatus::Running,
s if s.contains("stopped") => crate::ServiceStatus::Stopped,
s if s.contains("failed") => crate::ServiceStatus::Failed,
_ => crate::ServiceStatus::Unknown,
};
Ok(service_status)
}
fn logs(
&self,
service_name: &str,
_lines: Option<usize>,
) -> Result<String, ServiceManagerError> {
// The logs method takes (follow: bool, filter: Option<impl AsRef<str>>)
let client = Arc::clone(&self.client);
let service_name_owned = service_name.to_string();
let logs = self
.execute_async(async move {
use futures::StreamExt;
use tokio::time::{timeout, Duration};
let mut log_stream = client
.logs(false, Some(service_name_owned.as_str()))
.await?;
let mut logs = Vec::new();
// Collect logs from the stream with a reasonable limit
let mut count = 0;
const MAX_LOGS: usize = 100;
const LOG_TIMEOUT: Duration = Duration::from_secs(5);
// Use timeout to prevent hanging
let result = timeout(LOG_TIMEOUT, async {
while let Some(log_result) = log_stream.next().await {
match log_result {
Ok(log_entry) => {
logs.push(format!("{:?}", log_entry));
count += 1;
if count >= MAX_LOGS {
break;
}
}
Err(_) => break,
}
}
})
.await;
// Handle timeout - this is not an error, just means no more logs available
if result.is_err() {
log::debug!(
"Log reading timed out after {} seconds, returning {} logs",
LOG_TIMEOUT.as_secs(),
logs.len()
);
}
Ok::<Vec<String>, ZinitError>(logs)
})
.map_err(|e| {
ServiceManagerError::LogsFailed(service_name.to_string(), e.to_string())
})?;
Ok(logs.join("\n"))
}
fn list(&self) -> Result<Vec<String>, ServiceManagerError> {
let client = Arc::clone(&self.client);
let services = self
.execute_async(async move { client.list().await })
.map_err(|e| ServiceManagerError::Other(e.to_string()))?;
Ok(services.keys().cloned().collect())
}
fn remove(&self, service_name: &str) -> Result<(), ServiceManagerError> {
// Try to stop the service first, but don't fail if it's already stopped or doesn't exist
if let Err(e) = self.stop(service_name) {
// Log the error but continue with removal
log::warn!(
"Failed to stop service '{}' before removal: {}",
service_name,
e
);
}
let client = Arc::clone(&self.client);
let service_name = service_name.to_string();
self.execute_async(async move { client.delete_service(&service_name).await })
.map_err(|e| ServiceManagerError::Other(e.to_string()))
}
}

View File

@ -1,243 +0,0 @@
use sal_service_manager::{create_service_manager, ServiceConfig, ServiceManager};
use std::collections::HashMap;
#[test]
fn test_create_service_manager() {
// Test that the factory function creates the appropriate service manager for the platform
let manager = create_service_manager().expect("Failed to create service manager");
// Test basic functionality - should be able to call methods without panicking
let list_result = manager.list();
// The result might be an error (if no service system is available), but it shouldn't panic
match list_result {
Ok(services) => {
println!(
"✓ Service manager created successfully, found {} services",
services.len()
);
}
Err(e) => {
println!("✓ Service manager created, but got expected error: {}", e);
// This is expected on systems without the appropriate service manager
}
}
}
#[test]
fn test_service_config_creation() {
// Test creating various service configurations
let basic_config = ServiceConfig {
name: "test-service".to_string(),
binary_path: "/usr/bin/echo".to_string(),
args: vec!["hello".to_string(), "world".to_string()],
working_directory: None,
environment: HashMap::new(),
auto_restart: false,
};
assert_eq!(basic_config.name, "test-service");
assert_eq!(basic_config.binary_path, "/usr/bin/echo");
assert_eq!(basic_config.args.len(), 2);
assert_eq!(basic_config.args[0], "hello");
assert_eq!(basic_config.args[1], "world");
assert!(basic_config.working_directory.is_none());
assert!(basic_config.environment.is_empty());
assert!(!basic_config.auto_restart);
println!("✓ Basic service config created successfully");
// Test config with environment variables
let mut env = HashMap::new();
env.insert("PATH".to_string(), "/usr/bin:/bin".to_string());
env.insert("HOME".to_string(), "/tmp".to_string());
let env_config = ServiceConfig {
name: "env-service".to_string(),
binary_path: "/usr/bin/env".to_string(),
args: vec![],
working_directory: Some("/tmp".to_string()),
environment: env.clone(),
auto_restart: true,
};
assert_eq!(env_config.name, "env-service");
assert_eq!(env_config.binary_path, "/usr/bin/env");
assert!(env_config.args.is_empty());
assert_eq!(env_config.working_directory, Some("/tmp".to_string()));
assert_eq!(env_config.environment.len(), 2);
assert_eq!(
env_config.environment.get("PATH"),
Some(&"/usr/bin:/bin".to_string())
);
assert_eq!(
env_config.environment.get("HOME"),
Some(&"/tmp".to_string())
);
assert!(env_config.auto_restart);
println!("✓ Environment service config created successfully");
}
#[test]
fn test_service_config_clone() {
// Test that ServiceConfig can be cloned
let original_config = ServiceConfig {
name: "original".to_string(),
binary_path: "/bin/sh".to_string(),
args: vec!["-c".to_string(), "echo test".to_string()],
working_directory: Some("/home".to_string()),
environment: {
let mut env = HashMap::new();
env.insert("TEST".to_string(), "value".to_string());
env
},
auto_restart: true,
};
let cloned_config = original_config.clone();
assert_eq!(original_config.name, cloned_config.name);
assert_eq!(original_config.binary_path, cloned_config.binary_path);
assert_eq!(original_config.args, cloned_config.args);
assert_eq!(
original_config.working_directory,
cloned_config.working_directory
);
assert_eq!(original_config.environment, cloned_config.environment);
assert_eq!(original_config.auto_restart, cloned_config.auto_restart);
println!("✓ Service config cloning works correctly");
}
#[cfg(target_os = "macos")]
#[test]
fn test_macos_service_manager() {
use sal_service_manager::LaunchctlServiceManager;
// Test creating macOS-specific service manager
let manager = LaunchctlServiceManager::new();
// Test basic functionality
let list_result = manager.list();
match list_result {
Ok(services) => {
println!(
"✓ macOS LaunchctlServiceManager created successfully, found {} services",
services.len()
);
}
Err(e) => {
println!(
"✓ macOS LaunchctlServiceManager created, but got expected error: {}",
e
);
}
}
}
#[cfg(target_os = "linux")]
#[test]
fn test_linux_service_manager() {
use sal_service_manager::SystemdServiceManager;
// Test creating Linux-specific service manager
let manager = SystemdServiceManager::new();
// Test basic functionality
let list_result = manager.list();
match list_result {
Ok(services) => {
println!(
"✓ Linux SystemdServiceManager created successfully, found {} services",
services.len()
);
}
Err(e) => {
println!(
"✓ Linux SystemdServiceManager created, but got expected error: {}",
e
);
}
}
}
#[test]
fn test_service_status_debug() {
use sal_service_manager::ServiceStatus;
// Test that ServiceStatus can be debugged and cloned
let statuses = vec![
ServiceStatus::Running,
ServiceStatus::Stopped,
ServiceStatus::Failed,
ServiceStatus::Unknown,
];
for status in &statuses {
let cloned = status.clone();
let debug_str = format!("{:?}", status);
assert!(!debug_str.is_empty());
assert_eq!(status, &cloned);
println!(
"✓ ServiceStatus::{:?} debug and clone work correctly",
status
);
}
}
#[test]
fn test_service_manager_error_debug() {
use sal_service_manager::ServiceManagerError;
// Test that ServiceManagerError can be debugged and displayed
let errors = vec![
ServiceManagerError::ServiceNotFound("test".to_string()),
ServiceManagerError::ServiceAlreadyExists("test".to_string()),
ServiceManagerError::StartFailed("test".to_string(), "reason".to_string()),
ServiceManagerError::StopFailed("test".to_string(), "reason".to_string()),
ServiceManagerError::RestartFailed("test".to_string(), "reason".to_string()),
ServiceManagerError::LogsFailed("test".to_string(), "reason".to_string()),
ServiceManagerError::Other("generic error".to_string()),
];
for error in &errors {
let debug_str = format!("{:?}", error);
let display_str = format!("{}", error);
assert!(!debug_str.is_empty());
assert!(!display_str.is_empty());
println!("✓ Error debug: {:?}", error);
println!("✓ Error display: {}", error);
}
}
#[test]
fn test_service_manager_trait_object() {
// Test that we can use ServiceManager as a trait object
let manager: Box<dyn ServiceManager> =
create_service_manager().expect("Failed to create service manager");
// Test that we can call methods through the trait object
let list_result = manager.list();
match list_result {
Ok(services) => {
println!("✓ Trait object works, found {} services", services.len());
}
Err(e) => {
println!("✓ Trait object works, got expected error: {}", e);
}
}
// Test exists method
let exists_result = manager.exists("non-existent-service");
match exists_result {
Ok(false) => println!("✓ Trait object exists method works correctly"),
Ok(true) => println!("⚠ Unexpectedly found non-existent service"),
Err(_) => println!("✓ Trait object exists method works (with error)"),
}
}

View File

@ -1,177 +0,0 @@
// Service lifecycle management test script
// This script tests REAL complete service lifecycle scenarios
print("=== Service Lifecycle Management Test ===");
// Create service manager
let manager = create_service_manager();
print("✓ Service manager created");
// Test configuration - real services for testing
let test_services = [
#{
name: "lifecycle-test-1",
binary_path: "/bin/echo",
args: ["Lifecycle test 1"],
working_directory: "/tmp",
environment: #{},
auto_restart: false
},
#{
name: "lifecycle-test-2",
binary_path: "/bin/echo",
args: ["Lifecycle test 2"],
working_directory: "/tmp",
environment: #{ "TEST_VAR": "test_value" },
auto_restart: false
}
];
let total_tests = 0;
let passed_tests = 0;
// Test 1: Service Creation and Start
print("\n1. Testing service creation and start...");
for service_config in test_services {
print(`\nStarting service: ${service_config.name}`);
try {
start(manager, service_config);
print(` ✓ Service ${service_config.name} started successfully`);
passed_tests += 1;
} catch(e) {
print(` ✗ Service ${service_config.name} start failed: ${e}`);
}
total_tests += 1;
}
// Test 2: Service Existence Check
print("\n2. Testing service existence checks...");
for service_config in test_services {
print(`\nChecking existence of: ${service_config.name}`);
try {
let service_exists = exists(manager, service_config.name);
if service_exists {
print(` ✓ Service ${service_config.name} exists: ${service_exists}`);
passed_tests += 1;
} else {
print(` ✗ Service ${service_config.name} doesn't exist after start`);
}
} catch(e) {
print(` ✗ Existence check failed for ${service_config.name}: ${e}`);
}
total_tests += 1;
}
// Test 3: Status Check
print("\n3. Testing status checks...");
for service_config in test_services {
print(`\nChecking status of: ${service_config.name}`);
try {
let service_status = status(manager, service_config.name);
print(` ✓ Service ${service_config.name} status: ${service_status}`);
passed_tests += 1;
} catch(e) {
print(` ✗ Status check failed for ${service_config.name}: ${e}`);
}
total_tests += 1;
}
// Test 4: Service List Check
print("\n4. Testing service list...");
try {
let services = list(manager);
print(` ✓ Service list retrieved (${services.len()} services)`);
// Check if our test services are in the list
for service_config in test_services {
let found = false;
for service in services {
if service.contains(service_config.name) {
found = true;
print(` ✓ Found ${service_config.name} in list`);
break;
}
}
if !found {
print(` ⚠ ${service_config.name} not found in service list`);
}
}
passed_tests += 1;
} catch(e) {
print(` ✗ Service list failed: ${e}`);
}
total_tests += 1;
// Test 5: Service Stop
print("\n5. Testing service stop...");
for service_config in test_services {
print(`\nStopping service: ${service_config.name}`);
try {
stop(manager, service_config.name);
print(` ✓ Service ${service_config.name} stopped successfully`);
passed_tests += 1;
} catch(e) {
print(` ✗ Service ${service_config.name} stop failed: ${e}`);
}
total_tests += 1;
}
// Test 6: Service Removal
print("\n6. Testing service removal...");
for service_config in test_services {
print(`\nRemoving service: ${service_config.name}`);
try {
remove(manager, service_config.name);
print(` ✓ Service ${service_config.name} removed successfully`);
passed_tests += 1;
} catch(e) {
print(` ✗ Service ${service_config.name} removal failed: ${e}`);
}
total_tests += 1;
}
// Test 7: Cleanup Verification
print("\n7. Testing cleanup verification...");
for service_config in test_services {
print(`\nVerifying removal of: ${service_config.name}`);
try {
let exists_after_remove = exists(manager, service_config.name);
if !exists_after_remove {
print(` ✓ Service ${service_config.name} correctly doesn't exist after removal`);
passed_tests += 1;
} else {
print(` ✗ Service ${service_config.name} still exists after removal`);
}
} catch(e) {
print(` ✗ Cleanup verification failed for ${service_config.name}: ${e}`);
}
total_tests += 1;
}
// Test Summary
print("\n=== Lifecycle Test Summary ===");
print(`Services tested: ${test_services.len()}`);
print(`Total operations: ${total_tests}`);
print(`Successful operations: ${passed_tests}`);
print(`Failed operations: ${total_tests - passed_tests}`);
print(`Success rate: ${(passed_tests * 100) / total_tests}%`);
if passed_tests == total_tests {
print("\n🎉 All lifecycle tests passed!");
print("Service manager is working correctly across all scenarios.");
} else {
print(`\n⚠ ${total_tests - passed_tests} test(s) failed`);
print("Some service manager operations need attention.");
}
print("\n=== Service Lifecycle Test Complete ===");
// Return test results
#{
summary: #{
total_tests: total_tests,
passed_tests: passed_tests,
success_rate: (passed_tests * 100) / total_tests,
services_tested: test_services.len()
}
}

View File

@ -1,218 +0,0 @@
// Basic service manager functionality test script
// This script tests the REAL service manager through Rhai integration
print("=== Service Manager Basic Functionality Test ===");
// Test configuration
let test_service_name = "rhai-test-service";
let test_binary = "/bin/echo";
let test_args = ["Hello from Rhai service manager test"];
print(`Testing service: ${test_service_name}`);
print(`Binary: ${test_binary}`);
print(`Args: ${test_args}`);
// Test results tracking
let test_results = #{
creation: "NOT_RUN",
exists_before: "NOT_RUN",
start: "NOT_RUN",
exists_after: "NOT_RUN",
status: "NOT_RUN",
list: "NOT_RUN",
stop: "NOT_RUN",
remove: "NOT_RUN",
cleanup: "NOT_RUN"
};
let passed_tests = 0;
let total_tests = 0;
// Test 1: Service Manager Creation
print("\n1. Testing service manager creation...");
try {
let manager = create_service_manager();
print("✓ Service manager created successfully");
test_results["creation"] = "PASS";
passed_tests += 1;
total_tests += 1;
} catch(e) {
print(`✗ Service manager creation failed: ${e}`);
test_results["creation"] = "FAIL";
total_tests += 1;
// Return early if we can't create the manager
return test_results;
}
// Create the service manager for all subsequent tests
let manager = create_service_manager();
// Test 2: Check if service exists before creation
print("\n2. Testing service existence check (before creation)...");
try {
let exists_before = exists(manager, test_service_name);
print(`✓ Service existence check: ${exists_before}`);
if !exists_before {
print("✓ Service correctly doesn't exist before creation");
test_results["exists_before"] = "PASS";
passed_tests += 1;
} else {
print("⚠ Service unexpectedly exists before creation");
test_results["exists_before"] = "WARN";
}
total_tests += 1;
} catch(e) {
print(`✗ Service existence check failed: ${e}`);
test_results["exists_before"] = "FAIL";
total_tests += 1;
}
// Test 3: Start the service
print("\n3. Testing service start...");
try {
// Create a service configuration object
let service_config = #{
name: test_service_name,
binary_path: test_binary,
args: test_args,
working_directory: "/tmp",
environment: #{},
auto_restart: false
};
start(manager, service_config);
print("✓ Service started successfully");
test_results["start"] = "PASS";
passed_tests += 1;
total_tests += 1;
} catch(e) {
print(`✗ Service start failed: ${e}`);
test_results["start"] = "FAIL";
total_tests += 1;
}
// Test 4: Check if service exists after creation
print("\n4. Testing service existence check (after creation)...");
try {
let exists_after = exists(manager, test_service_name);
print(`✓ Service existence check: ${exists_after}`);
if exists_after {
print("✓ Service correctly exists after creation");
test_results["exists_after"] = "PASS";
passed_tests += 1;
} else {
print("✗ Service doesn't exist after creation");
test_results["exists_after"] = "FAIL";
}
total_tests += 1;
} catch(e) {
print(`✗ Service existence check failed: ${e}`);
test_results["exists_after"] = "FAIL";
total_tests += 1;
}
// Test 5: Check service status
print("\n5. Testing service status...");
try {
let service_status = status(manager, test_service_name);
print(`✓ Service status: ${service_status}`);
test_results["status"] = "PASS";
passed_tests += 1;
total_tests += 1;
} catch(e) {
print(`✗ Service status check failed: ${e}`);
test_results["status"] = "FAIL";
total_tests += 1;
}
// Test 6: List services
print("\n6. Testing service list...");
try {
let services = list(manager);
print("✓ Service list retrieved");
// Skip service search due to Rhai type constraints with Vec iteration
print(" ⚠️ Skipping service search due to Rhai type constraints");
test_results["list"] = "PASS";
passed_tests += 1;
total_tests += 1;
} catch(e) {
print(`✗ Service list failed: ${e}`);
test_results["list"] = "FAIL";
total_tests += 1;
}
// Test 7: Stop the service
print("\n7. Testing service stop...");
try {
stop(manager, test_service_name);
print(`✓ Service stopped: ${test_service_name}`);
test_results["stop"] = "PASS";
passed_tests += 1;
total_tests += 1;
} catch(e) {
print(`✗ Service stop failed: ${e}`);
test_results["stop"] = "FAIL";
total_tests += 1;
}
// Test 8: Remove the service
print("\n8. Testing service remove...");
try {
remove(manager, test_service_name);
print(`✓ Service removed: ${test_service_name}`);
test_results["remove"] = "PASS";
passed_tests += 1;
total_tests += 1;
} catch(e) {
print(`✗ Service remove failed: ${e}`);
test_results["remove"] = "FAIL";
total_tests += 1;
}
// Test 9: Verify cleanup
print("\n9. Testing cleanup verification...");
try {
let exists_after_remove = exists(manager, test_service_name);
if !exists_after_remove {
print("✓ Service correctly doesn't exist after removal");
test_results["cleanup"] = "PASS";
passed_tests += 1;
} else {
print("✗ Service still exists after removal");
test_results["cleanup"] = "FAIL";
}
total_tests += 1;
} catch(e) {
print(`✗ Cleanup verification failed: ${e}`);
test_results["cleanup"] = "FAIL";
total_tests += 1;
}
// Test Summary
print("\n=== Test Summary ===");
print(`Total tests: ${total_tests}`);
print(`Passed: ${passed_tests}`);
print(`Failed: ${total_tests - passed_tests}`);
print(`Success rate: ${(passed_tests * 100) / total_tests}%`);
print("\nDetailed Results:");
for test_name in test_results.keys() {
let result = test_results[test_name];
let status_icon = if result == "PASS" { "✓" } else if result == "FAIL" { "✗" } else { "⚠" };
print(` ${status_icon} ${test_name}: ${result}`);
}
if passed_tests == total_tests {
print("\n🎉 All tests passed!");
} else {
print(`\n⚠ ${total_tests - passed_tests} test(s) failed`);
}
print("\n=== Service Manager Basic Test Complete ===");
// Return test results for potential use by calling code
test_results

View File

@ -1,252 +0,0 @@
use rhai::{Engine, EvalAltResult};
use std::fs;
use std::path::Path;
/// Helper function to create a Rhai engine for service manager testing
fn create_service_manager_engine() -> Result<Engine, Box<EvalAltResult>> {
#[cfg(feature = "rhai")]
{
let mut engine = Engine::new();
// Register the service manager module for real testing
sal_service_manager::rhai::register_service_manager_module(&mut engine)?;
Ok(engine)
}
#[cfg(not(feature = "rhai"))]
{
Ok(Engine::new())
}
}
/// Helper function to run a Rhai script file
fn run_rhai_script(script_path: &str) -> Result<rhai::Dynamic, Box<EvalAltResult>> {
let engine = create_service_manager_engine()?;
// Read the script file
let script_content = fs::read_to_string(script_path)
.map_err(|e| format!("Failed to read script file {}: {}", script_path, e))?;
// Execute the script
engine.eval::<rhai::Dynamic>(&script_content)
}
#[test]
fn test_rhai_service_manager_basic() {
let script_path = "tests/rhai/service_manager_basic.rhai";
if !Path::new(script_path).exists() {
println!("⚠ Skipping test: Rhai script not found at {}", script_path);
return;
}
println!("Running Rhai service manager basic test...");
match run_rhai_script(script_path) {
Ok(result) => {
println!("✓ Rhai basic test completed successfully");
// Try to extract test results if the script returns them
if let Some(map) = result.try_cast::<rhai::Map>() {
println!("Test results received from Rhai script:");
for (key, value) in map.iter() {
println!(" {}: {:?}", key, value);
}
// Check if all tests passed
let all_passed = map.values().all(|v| {
if let Some(s) = v.clone().try_cast::<String>() {
s == "PASS"
} else {
false
}
});
if all_passed {
println!("✓ All Rhai tests reported as PASS");
} else {
println!("⚠ Some Rhai tests did not pass");
}
}
}
Err(e) => {
println!("✗ Rhai basic test failed: {}", e);
assert!(false, "Rhai script execution failed: {}", e);
}
}
}
#[test]
fn test_rhai_service_lifecycle() {
let script_path = "tests/rhai/service_lifecycle.rhai";
if !Path::new(script_path).exists() {
println!("⚠ Skipping test: Rhai script not found at {}", script_path);
return;
}
println!("Running Rhai service lifecycle test...");
match run_rhai_script(script_path) {
Ok(result) => {
println!("✓ Rhai lifecycle test completed successfully");
// Try to extract test results if the script returns them
if let Some(map) = result.try_cast::<rhai::Map>() {
println!("Lifecycle test results received from Rhai script:");
// Extract summary if available
if let Some(summary) = map.get("summary") {
if let Some(summary_map) = summary.clone().try_cast::<rhai::Map>() {
println!("Summary:");
for (key, value) in summary_map.iter() {
println!(" {}: {:?}", key, value);
}
}
}
// Extract performance metrics if available
if let Some(performance) = map.get("performance") {
if let Some(perf_map) = performance.clone().try_cast::<rhai::Map>() {
println!("Performance:");
for (key, value) in perf_map.iter() {
println!(" {}: {:?}", key, value);
}
}
}
}
}
Err(e) => {
println!("✗ Rhai lifecycle test failed: {}", e);
assert!(false, "Rhai script execution failed: {}", e);
}
}
}
#[test]
fn test_rhai_engine_functionality() {
println!("Testing basic Rhai engine functionality...");
let engine = create_service_manager_engine().expect("Failed to create Rhai engine");
// Test basic Rhai functionality
let test_script = r#"
let test_results = #{
basic_math: 2 + 2 == 4,
string_ops: "hello".len() == 5,
array_ops: [1, 2, 3].len() == 3,
map_ops: #{ a: 1, b: 2 }.len() == 2
};
let all_passed = true;
for result in test_results.values() {
if !result {
all_passed = false;
break;
}
}
#{
results: test_results,
all_passed: all_passed
}
"#;
match engine.eval::<rhai::Dynamic>(test_script) {
Ok(result) => {
if let Some(map) = result.try_cast::<rhai::Map>() {
if let Some(all_passed) = map.get("all_passed") {
if let Some(passed) = all_passed.clone().try_cast::<bool>() {
if passed {
println!("✓ All basic Rhai functionality tests passed");
} else {
println!("✗ Some basic Rhai functionality tests failed");
assert!(false, "Basic Rhai tests failed");
}
}
}
if let Some(results) = map.get("results") {
if let Some(results_map) = results.clone().try_cast::<rhai::Map>() {
println!("Detailed results:");
for (test_name, result) in results_map.iter() {
let status = if let Some(passed) = result.clone().try_cast::<bool>() {
if passed {
""
} else {
""
}
} else {
"?"
};
println!(" {} {}: {:?}", status, test_name, result);
}
}
}
}
}
Err(e) => {
println!("✗ Basic Rhai functionality test failed: {}", e);
assert!(false, "Basic Rhai test failed: {}", e);
}
}
}
#[test]
fn test_rhai_script_error_handling() {
println!("Testing Rhai error handling...");
let engine = create_service_manager_engine().expect("Failed to create Rhai engine");
// Test script with intentional error
let error_script = r#"
let result = "test";
result.non_existent_method(); // This should cause an error
"#;
match engine.eval::<rhai::Dynamic>(error_script) {
Ok(_) => {
println!("⚠ Expected error but script succeeded");
assert!(
false,
"Error handling test failed - expected error but got success"
);
}
Err(e) => {
println!("✓ Error correctly caught: {}", e);
// Verify it's the expected type of error
assert!(e.to_string().contains("method") || e.to_string().contains("function"));
}
}
}
#[test]
fn test_rhai_script_files_exist() {
println!("Checking that Rhai test scripts exist...");
let script_files = [
"tests/rhai/service_manager_basic.rhai",
"tests/rhai/service_lifecycle.rhai",
];
for script_file in &script_files {
if Path::new(script_file).exists() {
println!("✓ Found script: {}", script_file);
// Verify the file is readable and not empty
match fs::read_to_string(script_file) {
Ok(content) => {
if content.trim().is_empty() {
assert!(false, "Script file {} is empty", script_file);
}
println!(" Content length: {} characters", content.len());
}
Err(e) => {
assert!(false, "Failed to read script file {}: {}", script_file, e);
}
}
} else {
assert!(false, "Required script file not found: {}", script_file);
}
}
println!("✓ All required Rhai script files exist and are readable");
}

View File

@ -1,317 +0,0 @@
use sal_service_manager::{
ServiceConfig, ServiceManager, ServiceManagerError, ServiceStatus, ZinitServiceManager,
};
use std::collections::HashMap;
use std::time::Duration;
use tokio::time::sleep;
/// Helper function to find an available Zinit socket path
async fn get_available_socket_path() -> Option<String> {
let socket_paths = [
"/var/run/zinit.sock",
"/tmp/zinit.sock",
"/run/zinit.sock",
"./zinit.sock",
];
for path in &socket_paths {
// Try to create a ZinitServiceManager to test connectivity
if let Ok(manager) = ZinitServiceManager::new(path) {
// Test if we can list services (basic connectivity test)
if manager.list().is_ok() {
println!("✓ Found working Zinit socket at: {}", path);
return Some(path.to_string());
}
}
}
None
}
/// Helper function to clean up test services
async fn cleanup_test_service(manager: &dyn ServiceManager, service_name: &str) {
let _ = manager.stop(service_name);
let _ = manager.remove(service_name);
}
#[tokio::test]
async fn test_zinit_service_manager_creation() {
if let Some(socket_path) = get_available_socket_path().await {
let manager = ZinitServiceManager::new(&socket_path);
assert!(
manager.is_ok(),
"Should be able to create ZinitServiceManager"
);
let manager = manager.unwrap();
// Test basic connectivity by listing services
let list_result = manager.list();
assert!(list_result.is_ok(), "Should be able to list services");
println!("✓ ZinitServiceManager created successfully");
} else {
println!("⚠ Skipping test_zinit_service_manager_creation: No Zinit socket available");
}
}
#[tokio::test]
async fn test_service_lifecycle() {
if let Some(socket_path) = get_available_socket_path().await {
let manager = ZinitServiceManager::new(&socket_path).expect("Failed to create manager");
let service_name = "test-lifecycle-service";
// Clean up any existing service first
cleanup_test_service(&manager, service_name).await;
let config = ServiceConfig {
name: service_name.to_string(),
binary_path: "echo".to_string(),
args: vec!["Hello from lifecycle test".to_string()],
working_directory: Some("/tmp".to_string()),
environment: HashMap::new(),
auto_restart: false,
};
// Test service creation and start
println!("Testing service creation and start...");
let start_result = manager.start(&config);
match start_result {
Ok(_) => {
println!("✓ Service started successfully");
// Wait a bit for the service to run
sleep(Duration::from_millis(500)).await;
// Test service exists
let exists = manager.exists(service_name);
assert!(exists.is_ok(), "Should be able to check if service exists");
if let Ok(true) = exists {
println!("✓ Service exists check passed");
// Test service status
let status_result = manager.status(service_name);
match status_result {
Ok(status) => {
println!("✓ Service status: {:?}", status);
assert!(
matches!(status, ServiceStatus::Running | ServiceStatus::Stopped),
"Service should be running or stopped (for oneshot)"
);
}
Err(e) => println!("⚠ Status check failed: {}", e),
}
// Test service logs
let logs_result = manager.logs(service_name, None);
match logs_result {
Ok(logs) => {
println!("✓ Retrieved logs: {}", logs.len());
// For echo command, we should have some output
assert!(
!logs.is_empty() || logs.contains("Hello"),
"Should have log output"
);
}
Err(e) => println!("⚠ Logs retrieval failed: {}", e),
}
// Test service list
let list_result = manager.list();
match list_result {
Ok(services) => {
println!("✓ Listed {} services", services.len());
assert!(
services.contains(&service_name.to_string()),
"Service should appear in list"
);
}
Err(e) => println!("⚠ List services failed: {}", e),
}
}
// Test service stop
println!("Testing service stop...");
let stop_result = manager.stop(service_name);
match stop_result {
Ok(_) => println!("✓ Service stopped successfully"),
Err(e) => println!("⚠ Stop failed: {}", e),
}
// Test service removal
println!("Testing service removal...");
let remove_result = manager.remove(service_name);
match remove_result {
Ok(_) => println!("✓ Service removed successfully"),
Err(e) => println!("⚠ Remove failed: {}", e),
}
}
Err(e) => {
println!("⚠ Service creation/start failed: {}", e);
// This might be expected if zinit doesn't allow service creation
}
}
// Final cleanup
cleanup_test_service(&manager, service_name).await;
} else {
println!("⚠ Skipping test_service_lifecycle: No Zinit socket available");
}
}
#[tokio::test]
async fn test_service_start_and_confirm() {
if let Some(socket_path) = get_available_socket_path().await {
let manager = ZinitServiceManager::new(&socket_path).expect("Failed to create manager");
let service_name = "test-start-confirm-service";
// Clean up any existing service first
cleanup_test_service(&manager, service_name).await;
let config = ServiceConfig {
name: service_name.to_string(),
binary_path: "sleep".to_string(),
args: vec!["5".to_string()], // Sleep for 5 seconds
working_directory: Some("/tmp".to_string()),
environment: HashMap::new(),
auto_restart: false,
};
// Test start_and_confirm with timeout
println!("Testing start_and_confirm with timeout...");
let start_result = manager.start_and_confirm(&config, 10);
match start_result {
Ok(_) => {
println!("✓ Service started and confirmed successfully");
// Verify it's actually running
let status = manager.status(service_name);
match status {
Ok(ServiceStatus::Running) => println!("✓ Service confirmed running"),
Ok(other_status) => {
println!("⚠ Service in unexpected state: {:?}", other_status)
}
Err(e) => println!("⚠ Status check failed: {}", e),
}
}
Err(e) => {
println!("⚠ start_and_confirm failed: {}", e);
}
}
// Test start_existing_and_confirm
println!("Testing start_existing_and_confirm...");
let start_existing_result = manager.start_existing_and_confirm(service_name, 5);
match start_existing_result {
Ok(_) => println!("✓ start_existing_and_confirm succeeded"),
Err(e) => println!("⚠ start_existing_and_confirm failed: {}", e),
}
// Cleanup
cleanup_test_service(&manager, service_name).await;
} else {
println!("⚠ Skipping test_service_start_and_confirm: No Zinit socket available");
}
}
#[tokio::test]
async fn test_service_restart() {
if let Some(socket_path) = get_available_socket_path().await {
let manager = ZinitServiceManager::new(&socket_path).expect("Failed to create manager");
let service_name = "test-restart-service";
// Clean up any existing service first
cleanup_test_service(&manager, service_name).await;
let config = ServiceConfig {
name: service_name.to_string(),
binary_path: "echo".to_string(),
args: vec!["Restart test".to_string()],
working_directory: Some("/tmp".to_string()),
environment: HashMap::new(),
auto_restart: true, // Enable auto-restart for this test
};
// Start the service first
let start_result = manager.start(&config);
if start_result.is_ok() {
// Wait for service to be established
sleep(Duration::from_millis(1000)).await;
// Test restart
println!("Testing service restart...");
let restart_result = manager.restart(service_name);
match restart_result {
Ok(_) => {
println!("✓ Service restarted successfully");
// Wait and check status
sleep(Duration::from_millis(500)).await;
let status_result = manager.status(service_name);
match status_result {
Ok(status) => {
println!("✓ Service state after restart: {:?}", status);
}
Err(e) => println!("⚠ Status check after restart failed: {}", e),
}
}
Err(e) => {
println!("⚠ Restart failed: {}", e);
}
}
}
// Cleanup
cleanup_test_service(&manager, service_name).await;
} else {
println!("⚠ Skipping test_service_restart: No Zinit socket available");
}
}
#[tokio::test]
async fn test_error_handling() {
if let Some(socket_path) = get_available_socket_path().await {
let manager = ZinitServiceManager::new(&socket_path).expect("Failed to create manager");
// Test operations on non-existent service
let non_existent_service = "non-existent-service-12345";
// Test status of non-existent service
let status_result = manager.status(non_existent_service);
match status_result {
Err(ServiceManagerError::ServiceNotFound(_)) => {
println!("✓ Correctly returned ServiceNotFound for non-existent service");
}
Err(other_error) => {
println!(
"⚠ Got different error for non-existent service: {}",
other_error
);
}
Ok(_) => {
println!("⚠ Unexpectedly found non-existent service");
}
}
// Test exists for non-existent service
let exists_result = manager.exists(non_existent_service);
match exists_result {
Ok(false) => println!("✓ Correctly reported non-existent service as not existing"),
Ok(true) => println!("⚠ Incorrectly reported non-existent service as existing"),
Err(e) => println!("⚠ Error checking existence: {}", e),
}
// Test stop of non-existent service
let stop_result = manager.stop(non_existent_service);
match stop_result {
Err(_) => println!("✓ Correctly failed to stop non-existent service"),
Ok(_) => println!("⚠ Unexpectedly succeeded in stopping non-existent service"),
}
println!("✓ Error handling tests completed");
} else {
println!("⚠ Skipping test_error_handling: No Zinit socket available");
}
}

View File

View File

@ -1,76 +1,64 @@
# SAL Vault Examples
# Hero Vault Cryptography Examples
This directory contains examples demonstrating the SAL Vault functionality.
This directory contains examples demonstrating the Hero Vault cryptography functionality integrated into the SAL project.
## Overview
SAL Vault provides secure key management and cryptographic operations including:
Hero Vault provides cryptographic operations including:
- Vault creation and management
- KeySpace operations (encrypted key-value stores)
- Symmetric key generation and operations
- Asymmetric key operations (signing and verification)
- Secure key derivation from passwords
- Key space management (creation, loading, encryption, decryption)
- Keypair management (creation, selection, listing)
- Digital signatures (signing and verification)
- Symmetric encryption (key generation, encryption, decryption)
- Ethereum wallet functionality
- Smart contract interactions
- Key-value store with encryption
## Current Status
## Example Files
⚠️ **Note**: The vault module is currently being updated to use Lee's implementation.
The Rhai scripting integration is temporarily disabled while we adapt the examples
to work with the new vault API.
- `example.rhai` - Basic example demonstrating key management, signing, and encryption
- `advanced_example.rhai` - Advanced example with error handling, conditional logic, and more complex operations
- `key_persistence_example.rhai` - Demonstrates creating and saving a key space to disk
- `load_existing_space.rhai` - Shows how to load a previously created key space and use its keypairs
- `contract_example.rhai` - Demonstrates loading a contract ABI and interacting with smart contracts
- `agung_send_transaction.rhai` - Demonstrates sending native tokens on the Agung network
- `agung_contract_with_args.rhai` - Shows how to interact with contracts with arguments on Agung
## Available Operations
## Running the Examples
- **Vault Management**: Create and manage vault instances
- **KeySpace Operations**: Open encrypted key-value stores within vaults
- **Symmetric Encryption**: Generate keys and encrypt/decrypt data
- **Asymmetric Operations**: Create keypairs, sign messages, verify signatures
You can run the examples using the `herodo` tool that comes with the SAL project:
## Example Files (Legacy - Sameh's Implementation)
```bash
# Run a single example
herodo --path example.rhai
⚠️ **These examples are currently archived and use the previous vault implementation**:
- `_archive/example.rhai` - Basic example demonstrating key management, signing, and encryption
- `_archive/advanced_example.rhai` - Advanced example with error handling and complex operations
- `_archive/key_persistence_example.rhai` - Demonstrates creating and saving a key space to disk
- `_archive/load_existing_space.rhai` - Shows how to load a previously created key space
- `_archive/contract_example.rhai` - Demonstrates smart contract interactions (Ethereum)
- `_archive/agung_send_transaction.rhai` - Demonstrates Ethereum transactions on Agung network
- `_archive/agung_contract_with_args.rhai` - Shows contract interactions with arguments
## Current Implementation (Lee's Vault)
The current vault implementation provides:
```rust
// Create a new vault
let vault = Vault::new(&path).await?;
// Open an encrypted keyspace
let keyspace = vault.open_keyspace("my_space", "password").await?;
// Perform cryptographic operations
// (API documentation coming soon)
# Run all examples using the provided script
./run_examples.sh
```
## Migration Status
## Key Space Storage
- ✅ **Vault Core**: Lee's implementation is active
- ✅ **Archive**: Sameh's implementation preserved in `vault/_archive/`
- ⏳ **Rhai Integration**: Being developed for Lee's implementation
- ⏳ **Examples**: Will be updated to use Lee's API
- ❌ **Ethereum Features**: Not available in Lee's implementation
Key spaces are stored in the `~/.hero-vault/key-spaces/` directory by default. Each key space is stored in a separate JSON file named after the key space (e.g., `my_space.json`).
## Ethereum Functionality
The Hero Vault module provides comprehensive Ethereum wallet functionality:
- Creating and managing wallets for different networks
- Sending ETH transactions
- Checking balances
- Interacting with smart contracts (read and write functions)
- Support for multiple networks (Ethereum, Gnosis, Peaq, Agung, etc.)
## Security
The vault uses:
Key spaces are encrypted with ChaCha20Poly1305 using a key derived from the provided password. The encryption ensures that the key material is secure at rest.
- **ChaCha20Poly1305** for symmetric encryption
- **Password-based key derivation** for keyspace encryption
- **Secure key storage** with proper isolation
## Best Practices
## Next Steps
1. **Rhai Integration**: Implement Rhai bindings for Lee's vault
2. **New Examples**: Create examples using Lee's simpler API
3. **Documentation**: Complete API documentation for Lee's implementation
4. **Migration Guide**: Provide guidance for users migrating from Sameh's implementation
1. **Use Strong Passwords**: Since the security of your key spaces depends on the strength of your passwords, use strong, unique passwords.
2. **Backup Key Spaces**: Regularly backup your key spaces directory to prevent data loss.
3. **Script Organization**: Split your scripts into logical units, with separate scripts for key creation and key usage.
4. **Error Handling**: Always check the return values of functions to ensure operations succeeded before proceeding.
5. **Network Selection**: When working with Ethereum functionality, be explicit about which network you're targeting to avoid confusion.
6. **Gas Management**: For Ethereum transactions, consider gas costs and set appropriate gas limits.

View File

@ -1,72 +0,0 @@
//! Basic Kubernetes operations example
//!
//! This script demonstrates basic Kubernetes operations using the SAL Kubernetes module.
//!
//! Prerequisites:
//! - A running Kubernetes cluster
//! - Valid kubeconfig file or in-cluster configuration
//! - Appropriate permissions for the operations
//!
//! Usage:
//! herodo examples/kubernetes/basic_operations.rhai
print("=== SAL Kubernetes Basic Operations Example ===");
// Create a KubernetesManager for the default namespace
print("Creating KubernetesManager for 'default' namespace...");
let km = kubernetes_manager_new("default");
print("✓ KubernetesManager created for namespace: " + namespace(km));
// List all pods in the namespace
print("\n--- Listing Pods ---");
let pods = pods_list(km);
print("Found " + pods.len() + " pods in the namespace:");
for pod in pods {
print(" - " + pod);
}
// List all services in the namespace
print("\n--- Listing Services ---");
let services = services_list(km);
print("Found " + services.len() + " services in the namespace:");
for service in services {
print(" - " + service);
}
// List all deployments in the namespace
print("\n--- Listing Deployments ---");
let deployments = deployments_list(km);
print("Found " + deployments.len() + " deployments in the namespace:");
for deployment in deployments {
print(" - " + deployment);
}
// Get resource counts
print("\n--- Resource Counts ---");
let counts = resource_counts(km);
print("Resource counts in namespace '" + namespace(km) + "':");
for resource_type in counts.keys() {
print(" " + resource_type + ": " + counts[resource_type]);
}
// List all namespaces (cluster-wide operation)
print("\n--- Listing All Namespaces ---");
let namespaces = namespaces_list(km);
print("Found " + namespaces.len() + " namespaces in the cluster:");
for ns in namespaces {
print(" - " + ns);
}
// Check if specific namespaces exist
print("\n--- Checking Namespace Existence ---");
let test_namespaces = ["default", "kube-system", "non-existent-namespace"];
for ns in test_namespaces {
let exists = namespace_exists(km, ns);
if exists {
print("✓ Namespace '" + ns + "' exists");
} else {
print("✗ Namespace '" + ns + "' does not exist");
}
}
print("\n=== Example completed successfully! ===");

View File

@ -1,134 +0,0 @@
//! Generic Application Deployment Example
//!
//! This example shows how to deploy any containerized application using the
//! KubernetesManager convenience methods. This works for any Docker image.
use sal_kubernetes::KubernetesManager;
use std::collections::HashMap;
#[tokio::main]
async fn main() -> Result<(), Box<dyn std::error::Error>> {
// Create Kubernetes manager
let km = KubernetesManager::new("default").await?;
// Clean up any existing resources first
println!("=== Cleaning up existing resources ===");
let apps_to_clean = ["web-server", "node-app", "mongodb"];
for app in &apps_to_clean {
match km.deployment_delete(app).await {
Ok(_) => println!("✓ Deleted existing deployment: {}", app),
Err(_) => println!("✓ No existing deployment to delete: {}", app),
}
match km.service_delete(app).await {
Ok(_) => println!("✓ Deleted existing service: {}", app),
Err(_) => println!("✓ No existing service to delete: {}", app),
}
}
// Example 1: Simple web server deployment
println!("\n=== Example 1: Simple Nginx Web Server ===");
km.deploy_application("web-server", "nginx:latest", 2, 80, None, None)
.await?;
println!("✅ Nginx web server deployed!");
// Example 2: Node.js application with labels
println!("\n=== Example 2: Node.js Application ===");
let mut node_labels = HashMap::new();
node_labels.insert("app".to_string(), "node-app".to_string());
node_labels.insert("tier".to_string(), "backend".to_string());
node_labels.insert("environment".to_string(), "production".to_string());
// Configure Node.js environment variables
let mut node_env_vars = HashMap::new();
node_env_vars.insert("NODE_ENV".to_string(), "production".to_string());
node_env_vars.insert("PORT".to_string(), "3000".to_string());
node_env_vars.insert("LOG_LEVEL".to_string(), "info".to_string());
node_env_vars.insert("MAX_CONNECTIONS".to_string(), "1000".to_string());
km.deploy_application(
"node-app", // name
"node:18-alpine", // image
3, // replicas - scale to 3 instances
3000, // port
Some(node_labels), // labels
Some(node_env_vars), // environment variables
)
.await?;
println!("✅ Node.js application deployed!");
// Example 3: Database deployment (any database)
println!("\n=== Example 3: MongoDB Database ===");
let mut mongo_labels = HashMap::new();
mongo_labels.insert("app".to_string(), "mongodb".to_string());
mongo_labels.insert("type".to_string(), "database".to_string());
mongo_labels.insert("engine".to_string(), "mongodb".to_string());
// Configure MongoDB environment variables
let mut mongo_env_vars = HashMap::new();
mongo_env_vars.insert(
"MONGO_INITDB_ROOT_USERNAME".to_string(),
"admin".to_string(),
);
mongo_env_vars.insert(
"MONGO_INITDB_ROOT_PASSWORD".to_string(),
"mongopassword".to_string(),
);
mongo_env_vars.insert("MONGO_INITDB_DATABASE".to_string(), "myapp".to_string());
km.deploy_application(
"mongodb", // name
"mongo:6.0", // image
1, // replicas - single instance for simplicity
27017, // port
Some(mongo_labels), // labels
Some(mongo_env_vars), // environment variables
)
.await?;
println!("✅ MongoDB deployed!");
// Check status of all deployments
println!("\n=== Checking Deployment Status ===");
let deployments = km.deployments_list().await?;
for deployment in &deployments {
if let Some(name) = &deployment.metadata.name {
let total_replicas = deployment
.spec
.as_ref()
.and_then(|s| s.replicas)
.unwrap_or(0);
let ready_replicas = deployment
.status
.as_ref()
.and_then(|s| s.ready_replicas)
.unwrap_or(0);
println!(
"{}: {}/{} replicas ready",
name, ready_replicas, total_replicas
);
}
}
println!("\n🎉 All deployments completed!");
println!("\n💡 Key Points:");
println!(" • Any Docker image can be deployed using this simple interface");
println!(" • Use labels to organize and identify your applications");
println!(
" • The same method works for databases, web servers, APIs, and any containerized app"
);
println!(" • For advanced configuration, use the individual KubernetesManager methods");
println!(
" • Environment variables and resource limits can be added via direct Kubernetes API"
);
Ok(())
}

View File

@ -1,79 +0,0 @@
//! PostgreSQL Cluster Deployment Example (Rhai)
//!
//! This script shows how to deploy a PostgreSQL cluster using Rhai scripting
//! with the KubernetesManager convenience methods.
print("=== PostgreSQL Cluster Deployment ===");
// Create Kubernetes manager for the database namespace
print("Creating Kubernetes manager for 'database' namespace...");
let km = kubernetes_manager_new("database");
print("✓ Kubernetes manager created");
// Create the namespace if it doesn't exist
print("Creating namespace 'database' if it doesn't exist...");
try {
create_namespace(km, "database");
print("✓ Namespace 'database' created");
} catch(e) {
if e.to_string().contains("already exists") {
print("✓ Namespace 'database' already exists");
} else {
print("⚠️ Warning: " + e);
}
}
// Clean up any existing resources first
print("\nCleaning up any existing PostgreSQL resources...");
try {
delete_deployment(km, "postgres-cluster");
print("✓ Deleted existing deployment");
} catch(e) {
print("✓ No existing deployment to delete");
}
try {
delete_service(km, "postgres-cluster");
print("✓ Deleted existing service");
} catch(e) {
print("✓ No existing service to delete");
}
// Create PostgreSQL cluster using the convenience method
print("\nDeploying PostgreSQL cluster...");
try {
// Deploy PostgreSQL using the convenience method
let result = deploy_application(km, "postgres-cluster", "postgres:15", 2, 5432, #{
"app": "postgres-cluster",
"type": "database",
"engine": "postgresql"
}, #{
"POSTGRES_DB": "myapp",
"POSTGRES_USER": "postgres",
"POSTGRES_PASSWORD": "secretpassword",
"PGDATA": "/var/lib/postgresql/data/pgdata"
});
print("✓ " + result);
print("\n✅ PostgreSQL cluster deployed successfully!");
print("\n📋 Connection Information:");
print(" Host: postgres-cluster.database.svc.cluster.local");
print(" Port: 5432");
print(" Database: postgres (default)");
print(" Username: postgres (default)");
print("\n🔧 To connect from another pod:");
print(" psql -h postgres-cluster.database.svc.cluster.local -U postgres");
print("\n💡 Next steps:");
print(" • Set POSTGRES_PASSWORD environment variable");
print(" • Configure persistent storage");
print(" • Set up backup and monitoring");
} catch(e) {
print("❌ Failed to deploy PostgreSQL cluster: " + e);
}
print("\n=== Deployment Complete ===");

View File

@ -1,112 +0,0 @@
//! PostgreSQL Cluster Deployment Example
//!
//! This example shows how to deploy a PostgreSQL cluster using the
//! KubernetesManager convenience methods.
use sal_kubernetes::KubernetesManager;
use std::collections::HashMap;
#[tokio::main]
async fn main() -> Result<(), Box<dyn std::error::Error>> {
// Create Kubernetes manager for the database namespace
let km = KubernetesManager::new("database").await?;
// Create the namespace if it doesn't exist
println!("Creating namespace 'database' if it doesn't exist...");
match km.namespace_create("database").await {
Ok(_) => println!("✓ Namespace 'database' created"),
Err(e) => {
if e.to_string().contains("already exists") {
println!("✓ Namespace 'database' already exists");
} else {
return Err(e.into());
}
}
}
// Clean up any existing resources first
println!("Cleaning up any existing PostgreSQL resources...");
match km.deployment_delete("postgres-cluster").await {
Ok(_) => println!("✓ Deleted existing deployment"),
Err(_) => println!("✓ No existing deployment to delete"),
}
match km.service_delete("postgres-cluster").await {
Ok(_) => println!("✓ Deleted existing service"),
Err(_) => println!("✓ No existing service to delete"),
}
// Configure PostgreSQL-specific labels
let mut labels = HashMap::new();
labels.insert("app".to_string(), "postgres-cluster".to_string());
labels.insert("type".to_string(), "database".to_string());
labels.insert("engine".to_string(), "postgresql".to_string());
// Configure PostgreSQL environment variables
let mut env_vars = HashMap::new();
env_vars.insert("POSTGRES_DB".to_string(), "myapp".to_string());
env_vars.insert("POSTGRES_USER".to_string(), "postgres".to_string());
env_vars.insert(
"POSTGRES_PASSWORD".to_string(),
"secretpassword".to_string(),
);
env_vars.insert(
"PGDATA".to_string(),
"/var/lib/postgresql/data/pgdata".to_string(),
);
// Deploy the PostgreSQL cluster using the convenience method
println!("Deploying PostgreSQL cluster...");
km.deploy_application(
"postgres-cluster", // name
"postgres:15", // image
2, // replicas (1 master + 1 replica)
5432, // port
Some(labels), // labels
Some(env_vars), // environment variables
)
.await?;
println!("✅ PostgreSQL cluster deployed successfully!");
// Check deployment status
let deployments = km.deployments_list().await?;
let postgres_deployment = deployments
.iter()
.find(|d| d.metadata.name.as_ref() == Some(&"postgres-cluster".to_string()));
if let Some(deployment) = postgres_deployment {
let total_replicas = deployment
.spec
.as_ref()
.and_then(|s| s.replicas)
.unwrap_or(0);
let ready_replicas = deployment
.status
.as_ref()
.and_then(|s| s.ready_replicas)
.unwrap_or(0);
println!(
"Deployment status: {}/{} replicas ready",
ready_replicas, total_replicas
);
}
println!("\n📋 Connection Information:");
println!(" Host: postgres-cluster.database.svc.cluster.local");
println!(" Port: 5432");
println!(" Database: postgres (default)");
println!(" Username: postgres (default)");
println!(" Password: Set POSTGRES_PASSWORD environment variable");
println!("\n🔧 To connect from another pod:");
println!(" psql -h postgres-cluster.database.svc.cluster.local -U postgres");
println!("\n💡 Next steps:");
println!(" • Set environment variables for database credentials");
println!(" • Add persistent volume claims for data storage");
println!(" • Configure backup and monitoring");
Ok(())
}

View File

@ -1,79 +0,0 @@
//! Redis Cluster Deployment Example (Rhai)
//!
//! This script shows how to deploy a Redis cluster using Rhai scripting
//! with the KubernetesManager convenience methods.
print("=== Redis Cluster Deployment ===");
// Create Kubernetes manager for the cache namespace
print("Creating Kubernetes manager for 'cache' namespace...");
let km = kubernetes_manager_new("cache");
print("✓ Kubernetes manager created");
// Create the namespace if it doesn't exist
print("Creating namespace 'cache' if it doesn't exist...");
try {
create_namespace(km, "cache");
print("✓ Namespace 'cache' created");
} catch(e) {
if e.to_string().contains("already exists") {
print("✓ Namespace 'cache' already exists");
} else {
print("⚠️ Warning: " + e);
}
}
// Clean up any existing resources first
print("\nCleaning up any existing Redis resources...");
try {
delete_deployment(km, "redis-cluster");
print("✓ Deleted existing deployment");
} catch(e) {
print("✓ No existing deployment to delete");
}
try {
delete_service(km, "redis-cluster");
print("✓ Deleted existing service");
} catch(e) {
print("✓ No existing service to delete");
}
// Create Redis cluster using the convenience method
print("\nDeploying Redis cluster...");
try {
// Deploy Redis using the convenience method
let result = deploy_application(km, "redis-cluster", "redis:7-alpine", 3, 6379, #{
"app": "redis-cluster",
"type": "cache",
"engine": "redis"
}, #{
"REDIS_PASSWORD": "redispassword",
"REDIS_PORT": "6379",
"REDIS_DATABASES": "16",
"REDIS_MAXMEMORY": "256mb",
"REDIS_MAXMEMORY_POLICY": "allkeys-lru"
});
print("✓ " + result);
print("\n✅ Redis cluster deployed successfully!");
print("\n📋 Connection Information:");
print(" Host: redis-cluster.cache.svc.cluster.local");
print(" Port: 6379");
print("\n🔧 To connect from another pod:");
print(" redis-cli -h redis-cluster.cache.svc.cluster.local");
print("\n💡 Next steps:");
print(" • Configure Redis authentication");
print(" • Set up Redis clustering configuration");
print(" • Add persistent storage");
print(" • Configure memory policies");
} catch(e) {
print("❌ Failed to deploy Redis cluster: " + e);
}
print("\n=== Deployment Complete ===");

View File

@ -1,109 +0,0 @@
//! Redis Cluster Deployment Example
//!
//! This example shows how to deploy a Redis cluster using the
//! KubernetesManager convenience methods.
use sal_kubernetes::KubernetesManager;
use std::collections::HashMap;
#[tokio::main]
async fn main() -> Result<(), Box<dyn std::error::Error>> {
// Create Kubernetes manager for the cache namespace
let km = KubernetesManager::new("cache").await?;
// Create the namespace if it doesn't exist
println!("Creating namespace 'cache' if it doesn't exist...");
match km.namespace_create("cache").await {
Ok(_) => println!("✓ Namespace 'cache' created"),
Err(e) => {
if e.to_string().contains("already exists") {
println!("✓ Namespace 'cache' already exists");
} else {
return Err(e.into());
}
}
}
// Clean up any existing resources first
println!("Cleaning up any existing Redis resources...");
match km.deployment_delete("redis-cluster").await {
Ok(_) => println!("✓ Deleted existing deployment"),
Err(_) => println!("✓ No existing deployment to delete"),
}
match km.service_delete("redis-cluster").await {
Ok(_) => println!("✓ Deleted existing service"),
Err(_) => println!("✓ No existing service to delete"),
}
// Configure Redis-specific labels
let mut labels = HashMap::new();
labels.insert("app".to_string(), "redis-cluster".to_string());
labels.insert("type".to_string(), "cache".to_string());
labels.insert("engine".to_string(), "redis".to_string());
// Configure Redis environment variables
let mut env_vars = HashMap::new();
env_vars.insert("REDIS_PASSWORD".to_string(), "redispassword".to_string());
env_vars.insert("REDIS_PORT".to_string(), "6379".to_string());
env_vars.insert("REDIS_DATABASES".to_string(), "16".to_string());
env_vars.insert("REDIS_MAXMEMORY".to_string(), "256mb".to_string());
env_vars.insert(
"REDIS_MAXMEMORY_POLICY".to_string(),
"allkeys-lru".to_string(),
);
// Deploy the Redis cluster using the convenience method
println!("Deploying Redis cluster...");
km.deploy_application(
"redis-cluster", // name
"redis:7-alpine", // image
3, // replicas (Redis cluster nodes)
6379, // port
Some(labels), // labels
Some(env_vars), // environment variables
)
.await?;
println!("✅ Redis cluster deployed successfully!");
// Check deployment status
let deployments = km.deployments_list().await?;
let redis_deployment = deployments
.iter()
.find(|d| d.metadata.name.as_ref() == Some(&"redis-cluster".to_string()));
if let Some(deployment) = redis_deployment {
let total_replicas = deployment
.spec
.as_ref()
.and_then(|s| s.replicas)
.unwrap_or(0);
let ready_replicas = deployment
.status
.as_ref()
.and_then(|s| s.ready_replicas)
.unwrap_or(0);
println!(
"Deployment status: {}/{} replicas ready",
ready_replicas, total_replicas
);
}
println!("\n📋 Connection Information:");
println!(" Host: redis-cluster.cache.svc.cluster.local");
println!(" Port: 6379");
println!(" Password: Configure REDIS_PASSWORD environment variable");
println!("\n🔧 To connect from another pod:");
println!(" redis-cli -h redis-cluster.cache.svc.cluster.local");
println!("\n💡 Next steps:");
println!(" • Configure Redis authentication with environment variables");
println!(" • Set up Redis clustering configuration");
println!(" • Add persistent volume claims for data persistence");
println!(" • Configure memory limits and eviction policies");
Ok(())
}

View File

@ -1,208 +0,0 @@
//! Multi-namespace Kubernetes operations example
//!
//! This script demonstrates working with multiple namespaces and comparing resources across them.
//!
//! Prerequisites:
//! - A running Kubernetes cluster
//! - Valid kubeconfig file or in-cluster configuration
//! - Appropriate permissions for the operations
//!
//! Usage:
//! herodo examples/kubernetes/multi_namespace_operations.rhai
print("=== SAL Kubernetes Multi-Namespace Operations Example ===");
// Define namespaces to work with
let target_namespaces = ["default", "kube-system"];
let managers = #{};
print("Creating managers for multiple namespaces...");
// Create managers for each namespace
for ns in target_namespaces {
try {
let km = kubernetes_manager_new(ns);
managers[ns] = km;
print("✓ Created manager for namespace: " + ns);
} catch(e) {
print("✗ Failed to create manager for " + ns + ": " + e);
}
}
// Function to safely get resource counts
fn get_safe_counts(km) {
try {
return resource_counts(km);
} catch(e) {
print(" Warning: Could not get resource counts - " + e);
return #{};
}
}
// Function to safely get pod list
fn get_safe_pods(km) {
try {
return pods_list(km);
} catch(e) {
print(" Warning: Could not list pods - " + e);
return [];
}
}
// Compare resource counts across namespaces
print("\n--- Resource Comparison Across Namespaces ---");
let total_resources = #{};
for ns in target_namespaces {
if ns in managers {
let km = managers[ns];
print("\nNamespace: " + ns);
let counts = get_safe_counts(km);
for resource_type in counts.keys() {
let count = counts[resource_type];
print(" " + resource_type + ": " + count);
// Accumulate totals
if resource_type in total_resources {
total_resources[resource_type] = total_resources[resource_type] + count;
} else {
total_resources[resource_type] = count;
}
}
}
}
print("\n--- Total Resources Across All Namespaces ---");
for resource_type in total_resources.keys() {
print("Total " + resource_type + ": " + total_resources[resource_type]);
}
// Find namespaces with the most resources
print("\n--- Namespace Resource Analysis ---");
let namespace_totals = #{};
for ns in target_namespaces {
if ns in managers {
let km = managers[ns];
let counts = get_safe_counts(km);
let total = 0;
for resource_type in counts.keys() {
total = total + counts[resource_type];
}
namespace_totals[ns] = total;
print("Namespace '" + ns + "' has " + total + " total resources");
}
}
// Find the busiest namespace
let busiest_ns = "";
let max_resources = 0;
for ns in namespace_totals.keys() {
if namespace_totals[ns] > max_resources {
max_resources = namespace_totals[ns];
busiest_ns = ns;
}
}
if busiest_ns != "" {
print("🏆 Busiest namespace: '" + busiest_ns + "' with " + max_resources + " resources");
}
// Detailed pod analysis
print("\n--- Pod Analysis Across Namespaces ---");
let all_pods = [];
for ns in target_namespaces {
if ns in managers {
let km = managers[ns];
let pods = get_safe_pods(km);
print("\nNamespace '" + ns + "' pods:");
if pods.len() == 0 {
print(" (no pods)");
} else {
for pod in pods {
print(" - " + pod);
all_pods.push(ns + "/" + pod);
}
}
}
}
print("\n--- All Pods Summary ---");
print("Total pods across all namespaces: " + all_pods.len());
// Look for common pod name patterns
print("\n--- Pod Name Pattern Analysis ---");
let patterns = #{
"system": 0,
"kube": 0,
"coredns": 0,
"proxy": 0,
"controller": 0
};
for pod_full_name in all_pods {
let pod_name = pod_full_name.to_lower();
for pattern in patterns.keys() {
if pod_name.contains(pattern) {
patterns[pattern] = patterns[pattern] + 1;
}
}
}
print("Common pod name patterns found:");
for pattern in patterns.keys() {
if patterns[pattern] > 0 {
print(" '" + pattern + "': " + patterns[pattern] + " pods");
}
}
// Namespace health check
print("\n--- Namespace Health Check ---");
for ns in target_namespaces {
if ns in managers {
let km = managers[ns];
print("\nChecking namespace: " + ns);
// Check if namespace exists (should always be true for our managers)
let exists = namespace_exists(km, ns);
if exists {
print(" ✓ Namespace exists and is accessible");
} else {
print(" ✗ Namespace existence check failed");
}
// Try to get resource counts as a health indicator
let counts = get_safe_counts(km);
if counts.len() > 0 {
print(" ✓ Can access resources (" + counts.len() + " resource types)");
} else {
print(" ⚠ No resources found or access limited");
}
}
}
// Create a summary report
print("\n--- Summary Report ---");
print("Namespaces analyzed: " + target_namespaces.len());
print("Total unique resource types: " + total_resources.len());
let grand_total = 0;
for resource_type in total_resources.keys() {
grand_total = grand_total + total_resources[resource_type];
}
print("Grand total resources: " + grand_total);
print("\nResource breakdown:");
for resource_type in total_resources.keys() {
let count = total_resources[resource_type];
let percentage = (count * 100) / grand_total;
print(" " + resource_type + ": " + count + " (" + percentage + "%)");
}
print("\n=== Multi-namespace operations example completed! ===");

View File

@ -1,95 +0,0 @@
//! Kubernetes namespace management example
//!
//! This script demonstrates namespace creation and management operations.
//!
//! Prerequisites:
//! - A running Kubernetes cluster
//! - Valid kubeconfig file or in-cluster configuration
//! - Permissions to create and manage namespaces
//!
//! Usage:
//! herodo examples/kubernetes/namespace_management.rhai
print("=== SAL Kubernetes Namespace Management Example ===");
// Create a KubernetesManager
let km = kubernetes_manager_new("default");
print("Created KubernetesManager for namespace: " + namespace(km));
// Define test namespace names
let test_namespaces = [
"sal-test-namespace-1",
"sal-test-namespace-2",
"sal-example-app"
];
print("\n--- Creating Test Namespaces ---");
for ns in test_namespaces {
print("Creating namespace: " + ns);
try {
namespace_create(km, ns);
print("✓ Successfully created namespace: " + ns);
} catch(e) {
print("✗ Failed to create namespace " + ns + ": " + e);
}
}
// Wait a moment for namespaces to be created
print("\nWaiting for namespaces to be ready...");
// Verify namespaces were created
print("\n--- Verifying Namespace Creation ---");
for ns in test_namespaces {
let exists = namespace_exists(km, ns);
if exists {
print("✓ Namespace '" + ns + "' exists");
} else {
print("✗ Namespace '" + ns + "' was not found");
}
}
// List all namespaces to see our new ones
print("\n--- Current Namespaces ---");
let all_namespaces = namespaces_list(km);
print("Total namespaces in cluster: " + all_namespaces.len());
for ns in all_namespaces {
if ns.starts_with("sal-") {
print(" 🔹 " + ns + " (created by this example)");
} else {
print(" - " + ns);
}
}
// Test idempotent creation (creating the same namespace again)
print("\n--- Testing Idempotent Creation ---");
let test_ns = test_namespaces[0];
print("Attempting to create existing namespace: " + test_ns);
try {
namespace_create(km, test_ns);
print("✓ Idempotent creation successful (no error for existing namespace)");
} catch(e) {
print("✗ Unexpected error during idempotent creation: " + e);
}
// Create managers for the new namespaces and check their properties
print("\n--- Creating Managers for New Namespaces ---");
for ns in test_namespaces {
try {
let ns_km = kubernetes_manager_new(ns);
print("✓ Created manager for namespace: " + namespace(ns_km));
// Get resource counts for the new namespace (should be mostly empty)
let counts = resource_counts(ns_km);
print(" Resource counts: " + counts);
} catch(e) {
print("✗ Failed to create manager for " + ns + ": " + e);
}
}
print("\n--- Cleanup Instructions ---");
print("To clean up the test namespaces created by this example, run:");
for ns in test_namespaces {
print(" kubectl delete namespace " + ns);
}
print("\n=== Namespace management example completed! ===");

View File

@ -1,157 +0,0 @@
//! Kubernetes pattern-based deletion example
//!
//! This script demonstrates how to use PCRE patterns to delete multiple resources.
//!
//! ⚠️ WARNING: This example includes actual deletion operations!
//! ⚠️ Only run this in a test environment!
//!
//! Prerequisites:
//! - A running Kubernetes cluster (preferably a test cluster)
//! - Valid kubeconfig file or in-cluster configuration
//! - Permissions to delete resources
//!
//! Usage:
//! herodo examples/kubernetes/pattern_deletion.rhai
print("=== SAL Kubernetes Pattern Deletion Example ===");
print("⚠️ WARNING: This example will delete resources matching patterns!");
print("⚠️ Only run this in a test environment!");
// Create a KubernetesManager for a test namespace
let test_namespace = "sal-pattern-test";
let km = kubernetes_manager_new("default");
print("\nCreating test namespace: " + test_namespace);
try {
namespace_create(km, test_namespace);
print("✓ Test namespace created");
} catch(e) {
print("Note: " + e);
}
// Switch to the test namespace
let test_km = kubernetes_manager_new(test_namespace);
print("Switched to namespace: " + namespace(test_km));
// Show current resources before any operations
print("\n--- Current Resources in Test Namespace ---");
let counts = resource_counts(test_km);
print("Resource counts before operations:");
for resource_type in counts.keys() {
print(" " + resource_type + ": " + counts[resource_type]);
}
// List current pods to see what we're working with
let current_pods = pods_list(test_km);
print("\nCurrent pods in namespace:");
if current_pods.len() == 0 {
print(" (no pods found)");
} else {
for pod in current_pods {
print(" - " + pod);
}
}
// Demonstrate pattern matching without deletion first
print("\n--- Pattern Matching Demo (Dry Run) ---");
let test_patterns = [
"test-.*", // Match anything starting with "test-"
".*-temp$", // Match anything ending with "-temp"
"demo-pod-.*", // Match demo pods
"nginx-.*", // Match nginx pods
"app-[0-9]+", // Match app-1, app-2, etc.
];
for pattern in test_patterns {
print("Testing pattern: '" + pattern + "'");
// Check which pods would match this pattern
let matching_pods = [];
for pod in current_pods {
// Simple pattern matching simulation (Rhai doesn't have regex, so this is illustrative)
if pod.contains("test") && pattern == "test-.*" {
matching_pods.push(pod);
} else if pod.contains("temp") && pattern == ".*-temp$" {
matching_pods.push(pod);
} else if pod.contains("demo") && pattern == "demo-pod-.*" {
matching_pods.push(pod);
} else if pod.contains("nginx") && pattern == "nginx-.*" {
matching_pods.push(pod);
}
}
print(" Would match " + matching_pods.len() + " pods: " + matching_pods);
}
// Example of safe deletion patterns
print("\n--- Safe Deletion Examples ---");
print("These patterns are designed to be safe for testing:");
let safe_patterns = [
"test-example-.*", // Very specific test resources
"sal-demo-.*", // SAL demo resources
"temp-resource-.*", // Temporary resources
];
for pattern in safe_patterns {
print("\nTesting safe pattern: '" + pattern + "'");
try {
// This will actually attempt deletion, but should be safe in a test environment
let deleted_count = delete(test_km, pattern);
print("✓ Pattern '" + pattern + "' matched and deleted " + deleted_count + " resources");
} catch(e) {
print("Note: Pattern '" + pattern + "' - " + e);
}
}
// Show resources after deletion attempts
print("\n--- Resources After Deletion Attempts ---");
let final_counts = resource_counts(test_km);
print("Final resource counts:");
for resource_type in final_counts.keys() {
print(" " + resource_type + ": " + final_counts[resource_type]);
}
// Example of individual resource deletion
print("\n--- Individual Resource Deletion Examples ---");
print("These functions delete specific resources by name:");
// These are examples - they will fail if the resources don't exist, which is expected
let example_deletions = [
["pod", "test-pod-example"],
["service", "test-service-example"],
["deployment", "test-deployment-example"],
];
for deletion in example_deletions {
let resource_type = deletion[0];
let resource_name = deletion[1];
print("Attempting to delete " + resource_type + ": " + resource_name);
try {
if resource_type == "pod" {
pod_delete(test_km, resource_name);
} else if resource_type == "service" {
service_delete(test_km, resource_name);
} else if resource_type == "deployment" {
deployment_delete(test_km, resource_name);
}
print("✓ Successfully deleted " + resource_type + ": " + resource_name);
} catch(e) {
print("Note: " + resource_type + " '" + resource_name + "' - " + e);
}
}
print("\n--- Best Practices for Pattern Deletion ---");
print("1. Always test patterns in a safe environment first");
print("2. Use specific patterns rather than broad ones");
print("3. Consider using dry-run approaches when possible");
print("4. Have backups or be able to recreate resources");
print("5. Use descriptive naming conventions for easier pattern matching");
print("\n--- Cleanup ---");
print("To clean up the test namespace:");
print(" kubectl delete namespace " + test_namespace);
print("\n=== Pattern deletion example completed! ===");

View File

@ -1,33 +0,0 @@
//! Test Kubernetes module registration
//!
//! This script tests that the Kubernetes module is properly registered
//! and available in the Rhai environment.
print("=== Testing Kubernetes Module Registration ===");
// Test that we can reference the kubernetes functions
print("Testing function registration...");
// These should not error even if we can't connect to a cluster
let functions_to_test = [
"kubernetes_manager_new",
"pods_list",
"services_list",
"deployments_list",
"delete",
"namespace_create",
"namespace_exists",
"resource_counts",
"pod_delete",
"service_delete",
"deployment_delete",
"namespace"
];
for func_name in functions_to_test {
print("✓ Function '" + func_name + "' is available");
}
print("\n=== All Kubernetes functions are properly registered! ===");
print("Note: To test actual functionality, you need a running Kubernetes cluster.");
print("See other examples in this directory for real cluster operations.");

View File

@ -1,7 +1,6 @@
// Example of using the network modules in SAL through Rhai
// Shows TCP port checking, HTTP URL validation, and SSH command execution
// Function to print section header
fn section(title) {
print("\n");
@ -20,14 +19,14 @@ let host = "localhost";
let port = 22;
print(`Checking if port ${port} is open on ${host}...`);
let is_open = tcp.check_port(host, port);
print(`Port ${port} is ${if is_open { "open" } else { "closed" }}`);
print(`Port ${port} is ${is_open ? "open" : "closed"}`);
// Check multiple ports
let ports = [22, 80, 443];
print(`Checking multiple ports on ${host}...`);
let port_results = tcp.check_ports(host, ports);
for result in port_results {
print(`Port ${result.port} is ${if result.is_open { "open" } else { "closed" }}`);
print(`Port ${result.port} is ${result.is_open ? "open" : "closed"}`);
}
// HTTP connectivity checks
@ -40,7 +39,7 @@ let http = net::new_http_connector();
let url = "https://www.example.com";
print(`Checking if ${url} is reachable...`);
let is_reachable = http.check_url(url);
print(`${url} is ${if is_reachable { "reachable" } else { "unreachable" }}`);
print(`${url} is ${is_reachable ? "reachable" : "unreachable"}`);
// Check the status code of a URL
print(`Checking status code of ${url}...`);
@ -69,7 +68,7 @@ if is_open {
let ssh = net::new_ssh_builder()
.host("localhost")
.port(22)
.user(if os::get_env("USER") != () { os::get_env("USER") } else { "root" })
.user(os::get_env("USER") || "root")
.timeout(10)
.build();

View File

@ -1,7 +1,7 @@
print("Running a basic command using run().execute()...");
print("Running a basic command using run().do()...");
// Execute a simple command
let result = run("echo Hello from run_basic!").execute();
let result = run("echo Hello from run_basic!").do();
// Print the command result
print(`Command: echo Hello from run_basic!`);
@ -13,6 +13,6 @@ print(`Stderr:\n${result.stderr}`);
// Example of a command that might fail (if 'nonexistent_command' doesn't exist)
// This will halt execution by default because ignore_error() is not used.
// print("Running a command that will fail (and should halt)...");
// let fail_result = run("nonexistent_command").execute(); // This line will cause the script to halt if the command doesn't exist
// let fail_result = run("nonexistent_command").do(); // This line will cause the script to halt if the command doesn't exist
print("Basic run() example finished.");

View File

@ -2,7 +2,7 @@ print("Running a command that will fail, but ignoring the error...");
// Run a command that exits with a non-zero code (will fail)
// Using .ignore_error() prevents the script from halting
let result = run("exit 1").ignore_error().execute();
let result = run("exit 1").ignore_error().do();
print(`Command finished.`);
print(`Success: ${result.success}`); // This should be false
@ -22,7 +22,7 @@ print("\nScript continued execution after the potentially failing command.");
// Example of a command that might fail due to OS error (e.g., command not found)
// This *might* still halt depending on how the underlying Rust function handles it,
// as ignore_error() primarily prevents halting on *command* non-zero exit codes.
// let os_error_result = run("nonexistent_command_123").ignore_error().execute();
// let os_error_result = run("nonexistent_command_123").ignore_error().do();
// print(`OS Error Command Success: ${os_error_result.success}`);
// print(`OS Error Command Exit Code: ${os_error_result.code}`);

View File

@ -1,4 +1,4 @@
print("Running a command using run().log().execute()...");
print("Running a command using run().log().do()...");
// The .log() method will print the command string to the console before execution.
// This is useful for debugging or tracing which commands are being run.

View File

@ -1,8 +1,8 @@
print("Running a command using run().silent().execute()...\n");
print("Running a command using run().silent().do()...\n");
// This command will print to standard output and standard error
// However, because .silent() is used, the output will not appear in the console directly
let result = run("echo 'This should be silent stdout.'; echo 'This should be silent stderr.' >&2; exit 0").silent().execute();
let result = run("echo 'This should be silent stdout.'; echo 'This should be silent stderr.' >&2; exit 0").silent().do();
// The output is still captured in the CommandResult
print(`Command finished.`);
@ -12,7 +12,7 @@ print(`Captured Stdout:\\n${result.stdout}`);
print(`Captured Stderr:\\n${result.stderr}`);
// Example of a silent command that fails (but won't halt because we only suppress output)
// let fail_result = run("echo 'This is silent failure stderr.' >&2; exit 1").silent().execute();
// let fail_result = run("echo 'This is silent failure stderr.' >&2; exit 1").silent().do();
// print(`Failed command finished (silent):`);
// print(`Success: ${fail_result.success}`);
// print(`Exit Code: ${fail_result.code}`);

View File

@ -1,116 +0,0 @@
# Service Manager Examples
This directory contains examples demonstrating the SAL service manager functionality for dynamically launching and managing services across platforms.
## Overview
The service manager provides a unified interface for managing system services:
- **macOS**: Uses `launchctl` for service management
- **Linux**: Uses `zinit` for service management (systemd also available as alternative)
## Examples
### 1. Circle Worker Manager (`circle_worker_manager.rhai`)
**Primary Use Case**: Demonstrates dynamic circle worker management for freezone residents.
This example shows:
- Creating service configurations for circle workers
- Complete service lifecycle management (start, stop, restart, remove)
- Status monitoring and log retrieval
- Error handling and cleanup
```bash
# Run the circle worker management example
herodo examples/service_manager/circle_worker_manager.rhai
```
### 2. Basic Usage (`basic_usage.rhai`)
**Learning Example**: Simple demonstration of the core service manager API.
This example covers:
- Creating and configuring services
- Starting and stopping services
- Checking service status
- Listing managed services
- Retrieving service logs
```bash
# Run the basic usage example
herodo examples/service_manager/basic_usage.rhai
```
## Prerequisites
### Linux (zinit)
Make sure zinit is installed and running:
```bash
# Start zinit with default socket
zinit -s /tmp/zinit.sock init
```
### macOS (launchctl)
No additional setup required - uses the built-in launchctl system.
## Service Manager API
The service manager provides these key functions:
- `create_service_manager()` - Create platform-appropriate service manager
- `start(manager, config)` - Start a new service
- `stop(manager, service_name)` - Stop a running service
- `restart(manager, service_name)` - Restart a service
- `status(manager, service_name)` - Get service status
- `logs(manager, service_name, lines)` - Retrieve service logs
- `list(manager)` - List all managed services
- `remove(manager, service_name)` - Remove a service
- `exists(manager, service_name)` - Check if service exists
- `start_and_confirm(manager, config, timeout)` - Start with confirmation
## Service Configuration
Services are configured using a map with these fields:
```rhai
let config = #{
name: "my-service", // Service name
binary_path: "/usr/bin/my-app", // Executable path
args: ["--config", "/etc/my-app.conf"], // Command arguments
working_directory: "/var/lib/my-app", // Working directory (optional)
environment: #{ // Environment variables
"VAR1": "value1",
"VAR2": "value2"
},
auto_restart: true // Auto-restart on failure
};
```
## Real-World Usage
The circle worker example demonstrates the exact use case requested by the team:
> "We want to be able to launch circle workers dynamically. For instance when someone registers to the freezone, we need to be able to launch a circle worker for the new resident."
The service manager enables:
1. **Dynamic service creation** - Create services on-demand for new residents
2. **Cross-platform support** - Works on both macOS and Linux
3. **Lifecycle management** - Full control over service lifecycle
4. **Monitoring and logging** - Track service status and retrieve logs
5. **Cleanup** - Proper service removal when no longer needed
## Error Handling
All service manager functions can throw errors. Use try-catch blocks for robust error handling:
```rhai
try {
sm::start(manager, config);
print("✅ Service started successfully");
} catch (error) {
print(`❌ Failed to start service: ${error}`);
}
```

View File

@ -1,85 +0,0 @@
// Basic Service Manager Usage Example
//
// This example demonstrates the basic API of the service manager.
// It works on both macOS (launchctl) and Linux (zinit/systemd).
//
// Prerequisites:
//
// Linux: The service manager will automatically discover running zinit servers
// or fall back to systemd. To use zinit, start it with:
// zinit -s /tmp/zinit.sock init
//
// You can also specify a custom socket path:
// export ZINIT_SOCKET_PATH=/your/custom/path/zinit.sock
//
// macOS: No additional setup required (uses launchctl).
//
// Usage:
// herodo examples/service_manager/basic_usage.rhai
// Service Manager Basic Usage Example
// This example uses the SAL service manager through Rhai integration
print("🚀 Basic Service Manager Usage Example");
print("======================================");
// Create a service manager for the current platform
let manager = create_service_manager();
print("🍎 Using service manager for current platform");
// Create a simple service configuration
let config = #{
name: "example-service",
binary_path: "/bin/echo",
args: ["Hello from service manager!"],
working_directory: "/tmp",
environment: #{
"EXAMPLE_VAR": "hello_world"
},
auto_restart: false
};
print("\n📝 Service Configuration:");
print(` Name: ${config.name}`);
print(` Binary: ${config.binary_path}`);
print(` Args: ${config.args}`);
// Start the service
print("\n🚀 Starting service...");
start(manager, config);
print("✅ Service started successfully");
// Check service status
print("\n📊 Checking service status...");
let status = status(manager, "example-service");
print(`Status: ${status}`);
// List all services
print("\n📋 Listing all managed services...");
let services = list(manager);
print(`Found ${services.len()} services:`);
for service in services {
print(` - ${service}`);
}
// Get service logs
print("\n📄 Getting service logs...");
let logs = logs(manager, "example-service", 5);
if logs.trim() == "" {
print("No logs available");
} else {
print(`Logs:\n${logs}`);
}
// Stop the service
print("\n🛑 Stopping service...");
stop(manager, "example-service");
print("✅ Service stopped");
// Remove the service
print("\n🗑 Removing service...");
remove(manager, "example-service");
print("✅ Service removed");
print("\n🎉 Example completed successfully!");

View File

@ -1,141 +0,0 @@
// Circle Worker Manager Example
//
// This example demonstrates how to use the service manager to dynamically launch
// circle workers for new freezone residents. This is the primary use case requested
// by the team.
//
// Usage:
//
// On macOS (uses launchctl):
// herodo examples/service_manager/circle_worker_manager.rhai
//
// On Linux (uses zinit - requires zinit to be running):
// First start zinit: zinit -s /tmp/zinit.sock init
// herodo examples/service_manager/circle_worker_manager.rhai
// Circle Worker Manager Example
// This example uses the SAL service manager through Rhai integration
print("🚀 Circle Worker Manager Example");
print("=================================");
// Create the appropriate service manager for the current platform
let service_manager = create_service_manager();
print("✅ Created service manager for current platform");
// Simulate a new freezone resident registration
let resident_id = "resident_12345";
let worker_name = `circle-worker-${resident_id}`;
print(`\n📝 New freezone resident registered: ${resident_id}`);
print(`🔧 Creating circle worker service: ${worker_name}`);
// Create service configuration for the circle worker
let config = #{
name: worker_name,
binary_path: "/bin/sh",
args: [
"-c",
`echo 'Circle worker for ${resident_id} starting...'; sleep 30; echo 'Circle worker for ${resident_id} completed'`
],
working_directory: "/tmp",
environment: #{
"RESIDENT_ID": resident_id,
"WORKER_TYPE": "circle",
"LOG_LEVEL": "info"
},
auto_restart: true
};
print("📋 Service configuration created:");
print(` Name: ${config.name}`);
print(` Binary: ${config.binary_path}`);
print(` Args: ${config.args}`);
print(` Auto-restart: ${config.auto_restart}`);
print(`\n🔄 Demonstrating service lifecycle for: ${worker_name}`);
// 1. Check if service already exists
print("\n1⃣ Checking if service exists...");
if exists(service_manager, worker_name) {
print("⚠️ Service already exists, removing it first...");
remove(service_manager, worker_name);
print("🗑️ Existing service removed");
} else {
print("✅ Service doesn't exist, ready to create");
}
// 2. Start the service
print("\n2⃣ Starting the circle worker service...");
start(service_manager, config);
print("✅ Service started successfully");
// 3. Check service status
print("\n3⃣ Checking service status...");
let status = status(service_manager, worker_name);
print(`📊 Service status: ${status}`);
// 4. List all services to show our service is there
print("\n4⃣ Listing all managed services...");
let services = list(service_manager);
print(`📋 Managed services (${services.len()}):`);
for service in services {
let marker = if service == worker_name { "👉" } else { " " };
print(` ${marker} ${service}`);
}
// 5. Wait a moment and check status again
print("\n5⃣ Waiting 3 seconds and checking status again...");
sleep(3000); // 3 seconds in milliseconds
let status = status(service_manager, worker_name);
print(`📊 Service status after 3s: ${status}`);
// 6. Get service logs
print("\n6⃣ Retrieving service logs...");
let logs = logs(service_manager, worker_name, 10);
if logs.trim() == "" {
print("📄 No logs available yet (this is normal for new services)");
} else {
print("📄 Recent logs:");
let log_lines = logs.split('\n');
for i in 0..5 {
if i < log_lines.len() {
print(` ${log_lines[i]}`);
}
}
}
// 7. Demonstrate start_and_confirm with timeout
print("\n7⃣ Testing start_and_confirm (should succeed quickly since already running)...");
start_and_confirm(service_manager, config, 5);
print("✅ Service confirmed running within timeout");
// 8. Stop the service
print("\n8⃣ Stopping the service...");
stop(service_manager, worker_name);
print("🛑 Service stopped");
// 9. Check status after stopping
print("\n9⃣ Checking status after stop...");
let status = status(service_manager, worker_name);
print(`📊 Service status after stop: ${status}`);
// 10. Restart the service
print("\n🔟 Restarting the service...");
restart(service_manager, worker_name);
print("🔄 Service restarted successfully");
// 11. Final cleanup
print("\n🧹 Cleaning up - removing the service...");
remove(service_manager, worker_name);
print("🗑️ Service removed successfully");
// 12. Verify removal
print("\n✅ Verifying service removal...");
if !exists(service_manager, worker_name) {
print("✅ Service successfully removed");
} else {
print("⚠️ Service still exists after removal");
}
print("\n🎉 Circle worker management demonstration complete!");

View File

@ -10,12 +10,12 @@ license = "Apache-2.0"
[dependencies]
# Use workspace dependencies for consistency
regex = { workspace = true }
redis = { workspace = true }
serde = { workspace = true }
serde_json = { workspace = true }
rhai = { workspace = true }
log = { workspace = true }
url = { workspace = true }
redis = { workspace = true }
[dev-dependencies]
tempfile = { workspace = true }

View File

@ -1,18 +1,9 @@
# SAL Git Package (`sal-git`)
# SAL `git` Module
The `sal-git` package provides comprehensive functionalities for interacting with Git repositories. It offers both high-level abstractions for common Git workflows and a flexible executor for running arbitrary Git commands with integrated authentication.
The `git` module in SAL provides comprehensive functionalities for interacting with Git repositories. It offers both high-level abstractions for common Git workflows and a flexible executor for running arbitrary Git commands with integrated authentication.
This module is central to SAL's capabilities for managing source code, enabling automation of development tasks, and integrating with version control systems.
## Installation
Add this to your `Cargo.toml`:
```toml
[dependencies]
sal-git = "0.1.0"
```
## Core Components
The module is primarily composed of two main parts:

View File

@ -18,8 +18,8 @@ path = "src/main.rs"
env_logger = { workspace = true }
rhai = { workspace = true }
# SAL library for Rhai module registration (with all features for herodo)
sal = { path = "..", features = ["all"] }
# SAL library for Rhai module registration
sal = { path = ".." }
[dev-dependencies]
tempfile = { workspace = true }

View File

@ -15,32 +15,14 @@ Herodo is a command-line utility that executes Rhai scripts with full access to
## Installation
### Build and Install
Build the herodo binary:
```bash
git clone https://github.com/PlanetFirst/sal.git
cd sal
./build_herodo.sh
cd herodo
cargo build --release
```
This script will:
- Build herodo in debug mode
- Install it to `~/hero/bin/herodo` (non-root) or `/usr/local/bin/herodo` (root)
- Make it available in your PATH
**Note**: If using the non-root installation, make sure `~/hero/bin` is in your PATH:
```bash
export PATH="$HOME/hero/bin:$PATH"
```
### Install from crates.io (Coming Soon)
```bash
# This will be available once herodo is published to crates.io
cargo install herodo
```
**Note**: `herodo` is not yet published to crates.io due to publishing rate limits. It will be available soon.
The executable will be available at `target/release/herodo`.
## Usage

View File

@ -3,7 +3,7 @@
//! This library loads the Rhai engine, registers all SAL modules,
//! and executes Rhai scripts from a specified directory in sorted order.
use rhai::{Engine, Scope};
use rhai::Engine;
use std::error::Error;
use std::fs;
use std::path::{Path, PathBuf};
@ -29,17 +29,6 @@ pub fn run(script_path: &str) -> Result<(), Box<dyn Error>> {
// Create a new Rhai engine
let mut engine = Engine::new();
// TODO: if we create a scope here we could clean up all the different functionsand types regsitered wit the engine
// We should generalize the way we add things to the scope for each module sepeartely
let mut scope = Scope::new();
// TODO: this should be done for the other clients as well (but not here of course, in each module)
let hetzner_client = sal::hetzner::api::Client::new(sal::hetzner::config::Config::from_env().unwrap());
scope.push("hetzner", hetzner_client);
// This makes it easy to call e.g. `hetzner.get_server()` or `mycelium.get_connected_peers()`
// --> without the need of manually created a client for each one first
// --> could be conditionally compiled to only use those who we need (we only push the things to the scope that we actually need to run the script)
// Register println function for output
engine.register_fn("println", |s: &str| println!("{}", s));
@ -89,20 +78,19 @@ pub fn run(script_path: &str) -> Result<(), Box<dyn Error>> {
let script = fs::read_to_string(&script_file)?;
// Execute the script
// match engine.eval::<rhai::Dynamic>(&script) {
// Ok(result) => {
// println!("Script executed successfully");
// if !result.is_unit() {
// println!("Result: {}", result);
// }
// }
// Err(err) => {
// eprintln!("Error executing script: {}", err);
// // Exit with error code when a script fails
// process::exit(1);
// }
// }
engine.run_with_scope(&mut scope, &script)?;
match engine.eval::<rhai::Dynamic>(&script) {
Ok(result) => {
println!("Script executed successfully");
if !result.is_unit() {
println!("Result: {}", result);
}
}
Err(err) => {
eprintln!("Error executing script: {}", err);
// Exit with error code when a script fails
process::exit(1);
}
}
}
println!("\nAll scripts executed successfully!");

View File

@ -9,22 +9,22 @@ license = "Apache-2.0"
[dependencies]
# HTTP client for async requests
reqwest = { workspace = true }
reqwest = { version = "0.12.15", features = ["json"] }
# JSON handling
serde_json = { workspace = true }
serde_json = "1.0"
# Base64 encoding/decoding for message payloads
base64 = { workspace = true }
base64 = "0.22.1"
# Async runtime
tokio = { workspace = true }
tokio = { version = "1.45.0", features = ["full"] }
# Rhai scripting support
rhai = { workspace = true }
rhai = { version = "1.12.0", features = ["sync"] }
# Logging
log = { workspace = true }
log = "0.4"
# URL encoding for API parameters
urlencoding = { workspace = true }
urlencoding = "2.1.3"
[dev-dependencies]
# For async testing
tokio-test = { workspace = true }
tokio-test = "0.4.4"
# For temporary files in tests
tempfile = { workspace = true }
tempfile = "3.5"

View File

@ -1,16 +1,7 @@
# SAL Mycelium (`sal-mycelium`)
# SAL Mycelium
A Rust client library for interacting with Mycelium node's HTTP API, with Rhai scripting support.
## Installation
Add this to your `Cargo.toml`:
```toml
[dependencies]
sal-mycelium = "0.1.0"
```
## Overview
SAL Mycelium provides async HTTP client functionality for managing Mycelium nodes, including:

View File

@ -10,7 +10,7 @@ keywords = ["network", "tcp", "http", "ssh", "connectivity"]
categories = ["network-programming", "api-bindings"]
[dependencies]
anyhow = { workspace = true }
tokio = { workspace = true }
reqwest = { workspace = true, features = ["json", "blocking"] }
rhai = { workspace = true }
anyhow = "1.0.98"
tokio = { version = "1.0", features = ["full"] }
reqwest = { version = "0.12", features = ["json", "blocking"] }
rhai = "1.19.0"

View File

@ -1,16 +1,7 @@
# SAL Network Package (`sal-net`)
# SAL Network Package
Network connectivity utilities for TCP, HTTP, and SSH operations.
## Installation
Add this to your `Cargo.toml`:
```toml
[dependencies]
sal-net = "0.1.0"
```
## Overview
The `sal-net` package provides a comprehensive set of network connectivity tools for the SAL (System Abstraction Layer) ecosystem. It includes utilities for TCP port checking, HTTP/HTTPS connectivity testing, and SSH command execution.

View File

@ -14,8 +14,7 @@ categories = ["os", "filesystem", "api-bindings"]
dirs = { workspace = true }
glob = { workspace = true }
libc = { workspace = true }
anyhow = {workspace = true}
reqwest = {workspace = true}
# Error handling
thiserror = { workspace = true }

Some files were not shown because too many files have changed in this diff Show More