Compare commits
	
		
			3 Commits
		
	
	
		
			network_se
			...
			developmen
		
	
	| Author | SHA1 | Date | |
|---|---|---|---|
|  | 0e63efda61 | ||
|  | 568a5b0a49 | ||
|  | d431705501 | 
							
								
								
									
										227
									
								
								.github/workflows/publish.yml
									
									
									
									
										vendored
									
									
								
							
							
						
						
									
										227
									
								
								.github/workflows/publish.yml
									
									
									
									
										vendored
									
									
								
							| @@ -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 |  | ||||||
							
								
								
									
										233
									
								
								.github/workflows/test-publish.yml
									
									
									
									
										vendored
									
									
								
							
							
						
						
									
										233
									
								
								.github/workflows/test-publish.yml
									
									
									
									
										vendored
									
									
								
							| @@ -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
									
									
								
							
							
						
						
									
										2
									
								
								.gitignore
									
									
									
									
										vendored
									
									
								
							| @@ -62,5 +62,3 @@ docusaurus.config.ts | |||||||
| sidebars.ts | sidebars.ts | ||||||
|  |  | ||||||
| tsconfig.json | tsconfig.json | ||||||
| Cargo.toml.bak |  | ||||||
| for_augment |  | ||||||
							
								
								
									
										145
									
								
								Cargo.toml
									
									
									
									
									
								
							
							
						
						
									
										145
									
								
								Cargo.toml
									
									
									
									
									
								
							| @@ -11,27 +11,7 @@ categories = ["os", "filesystem", "api-bindings"] | |||||||
| readme = "README.md" | readme = "README.md" | ||||||
|  |  | ||||||
| [workspace] | [workspace] | ||||||
| members = [ | members = [".", "vault", "git", "redisclient", "mycelium", "text", "os", "net", "zinit_client", "process", "virt", "postgresclient", "rhai", "herodo"] | ||||||
|     "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", |  | ||||||
|     "rhailib", |  | ||||||
|     "herodo", |  | ||||||
|     "packages/clients/hetznerclient", |  | ||||||
| ] |  | ||||||
| resolver = "2" | resolver = "2" | ||||||
|  |  | ||||||
| [workspace.metadata] | [workspace.metadata] | ||||||
| @@ -52,7 +32,7 @@ log = "0.4" | |||||||
| once_cell = "1.18.0" | once_cell = "1.18.0" | ||||||
| rand = "0.8.5" | rand = "0.8.5" | ||||||
| regex = "1.8.1" | 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"] } | rhai = { version = "1.12.0", features = ["sync"] } | ||||||
| serde = { version = "1.0", features = ["derive"] } | serde = { version = "1.0", features = ["derive"] } | ||||||
| serde_json = "1.0" | serde_json = "1.0" | ||||||
| @@ -73,10 +53,6 @@ chacha20poly1305 = "0.10.1" | |||||||
| k256 = { version = "0.13.4", features = ["ecdsa", "ecdh"] } | k256 = { version = "0.13.4", features = ["ecdsa", "ecdh"] } | ||||||
| sha2 = "0.10.7" | sha2 = "0.10.7" | ||||||
| hex = "0.4" | 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 | # Ethereum dependencies | ||||||
| ethers = { version = "2.0.7", features = ["legacy"] } | ethers = { version = "2.0.7", features = ["legacy"] } | ||||||
| @@ -90,110 +66,21 @@ windows = { version = "0.61.1", features = [ | |||||||
| ] } | ] } | ||||||
|  |  | ||||||
| # Specialized dependencies | # Specialized dependencies | ||||||
| zinit-client = "0.4.0" | zinit-client = "0.3.0" | ||||||
| urlencoding = "2.1.3" | urlencoding = "2.1.3" | ||||||
| tokio-test = "0.4.4" | 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] | [dependencies] | ||||||
| thiserror = { workspace = true } | thiserror = "2.0.12" # For error handling in the main Error enum | ||||||
| tokio = { workspace = true } | sal-git = { path = "git" } | ||||||
|  | sal-redisclient = { path = "redisclient" } | ||||||
| # Optional dependencies - users can choose which modules to include | sal-mycelium = { path = "mycelium" } | ||||||
| sal-git = { workspace = true, optional = true } | sal-text = { path = "text" } | ||||||
| sal-kubernetes = { workspace = true, optional = true } | sal-os = { path = "os" } | ||||||
| sal-redisclient = { workspace = true, optional = true } | sal-net = { path = "net" } | ||||||
| sal-mycelium = { workspace = true, optional = true } | sal-zinit-client = { path = "zinit_client" } | ||||||
| sal-hetzner = { workspace = true, optional = true } | sal-process = { path = "process" } | ||||||
| sal-text = { workspace = true, optional = true } | sal-virt = { path = "virt" } | ||||||
| sal-os = { workspace = true, optional = true } | sal-postgresclient = { path = "postgresclient" } | ||||||
| sal-net = { workspace = true, optional = true } | sal-vault = { path = "vault" } | ||||||
| sal-zinit-client = { workspace = true, optional = true } | sal-rhai = { path = "rhai" } | ||||||
| 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"] |  | ||||||
|   | |||||||
							
								
								
									
										239
									
								
								PUBLISHING.md
									
									
									
									
									
								
							
							
						
						
									
										239
									
								
								PUBLISHING.md
									
									
									
									
									
								
							| @@ -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
									
									
									
									
									
								
							
							
						
						
									
										298
									
								
								README.md
									
									
									
									
									
								
							| @@ -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.). | ## 🏗️ **Cargo Workspace Structure** | ||||||
| - **Rhai scripts** and test suites for each crate. |  | ||||||
| - **Utility scripts** to automate common development tasks. |  | ||||||
|  |  | ||||||
| ## 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 | | This workspace structure provides excellent build performance, dependency management, and maintainability. | ||||||
| |--------|-------------|--------------| |  | ||||||
| | `scripts/publish-all.sh` | Publishes all SAL crates to **crates.io** in the correct dependency order. Handles version bumping, dependency updates, dry‑run mode, and rate‑limiting. | `./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` | |  | ||||||
|  |  | ||||||
| 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. | ## `herodo`: The SAL Scripting Tool | ||||||
| - Updates crate versions (if `--version` is supplied). |  | ||||||
| - Updates path dependencies to version dependencies before publishing. |  | ||||||
| - Supports **dry‑run** mode to preview actions without publishing. |  | ||||||
| - Handles rate‑limiting between crate publishes. |  | ||||||
|  |  | ||||||
| ### Options | `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. | ||||||
|  |  | ||||||
| | Option | Description | |  | ||||||
| |--------|-------------| |  | ||||||
| | `--dry-run` | Shows what would be published without actually publishing. | |  | ||||||
| | `--wait <seconds>` | Wait time between publishes (default: 15 s). | |  | ||||||
| | `--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 dry‑run) |  | ||||||
| ./scripts/publish-all.sh |  | ||||||
| ``` |  | ||||||
|  |  | ||||||
| ### Notes |  | ||||||
|  |  | ||||||
| - Must be run from the repository root (where `Cargo.toml` lives). |  | ||||||
| - Requires `cargo` and a logged‑in `cargo` session (`cargo login`). |  | ||||||
| - The script automatically updates dependencies in each crate’s `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 system‑wide location (`/usr/local/bin`) if run as root, otherwise to `~/hero/bin`. |  | ||||||
| - Optionally runs a specified Rhai script after building. |  | ||||||
|  |  | ||||||
| ### Usage | ### Usage | ||||||
|  |  | ||||||
| ```bash | ```bash | ||||||
| # Build only | # Execute a single Rhai script | ||||||
| ./build_herodo.sh | herodo script.rhai | ||||||
|  |  | ||||||
| # Build and run a specific Rhai script (e.g., `example`): | # Execute a script with arguments | ||||||
| ./build_herodo.sh example | 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. | ### Scriptable SAL Modules via `herodo` | ||||||
| - 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. |  | ||||||
|  |  | ||||||
| --- | 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. | ```rhai | ||||||
| - Supports both the legacy `rhai_tests` directory and the newer `*/tests/rhai` layout. | // file: /opt/scripts/example_task.rhai | ||||||
| - Logs output to `run_rhai_tests.log` and prints a summary. |  | ||||||
|  |  | ||||||
| ### 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 | ```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 | ./run_rhai_tests.sh | ||||||
|  |  | ||||||
|  | # Results: 16/16 modules pass with 100% success rate | ||||||
| ``` | ``` | ||||||
|  |  | ||||||
| ### Output | The Rhai tests validate real-world functionality across all SAL modules and provide comprehensive integration testing. | ||||||
|  |  | ||||||
| - 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. |  | ||||||
|  |  | ||||||
| ## License | ## License | ||||||
|  |  | ||||||
| See `LICENSE` for details. | SAL is licensed under the Apache License 2.0. See the [LICENSE](LICENSE) file for details. | ||||||
|  |  | ||||||
| --- |  | ||||||
|  |  | ||||||
| **Happy coding!** |  | ||||||
|   | |||||||
| @@ -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"] |  | ||||||
| @@ -1,198 +0,0 @@ | |||||||
| # SAL Service Manager |  | ||||||
|  |  | ||||||
| [](https://crates.io/crates/sal-service-manager) |  | ||||||
| [](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 |  | ||||||
| @@ -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 |  | ||||||
| ``` |  | ||||||
| @@ -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 ---"); |  | ||||||
| } |  | ||||||
| @@ -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 ---"); |  | ||||||
| } |  | ||||||
| @@ -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"); |  | ||||||
| } |  | ||||||
| @@ -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" |  | ||||||
|         ); |  | ||||||
|     } |  | ||||||
| } |  | ||||||
| @@ -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); |  | ||||||
|     } |  | ||||||
| } |  | ||||||
| @@ -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(()) |  | ||||||
|     } |  | ||||||
| } |  | ||||||
| @@ -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())) |  | ||||||
|     } |  | ||||||
| } |  | ||||||
| @@ -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)"), |  | ||||||
|     } |  | ||||||
| } |  | ||||||
| @@ -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() |  | ||||||
|     } |  | ||||||
| } |  | ||||||
| @@ -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 |  | ||||||
| @@ -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"); |  | ||||||
| } |  | ||||||
| @@ -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"); |  | ||||||
|     } |  | ||||||
| } |  | ||||||
| @@ -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 | ## Overview | ||||||
|  |  | ||||||
| SAL Vault provides secure key management and cryptographic operations including: | Hero Vault provides cryptographic operations including: | ||||||
|  |  | ||||||
| - Vault creation and management | - Key space management (creation, loading, encryption, decryption) | ||||||
| - KeySpace operations (encrypted key-value stores) | - Keypair management (creation, selection, listing) | ||||||
| - Symmetric key generation and operations | - Digital signatures (signing and verification) | ||||||
| - Asymmetric key operations (signing and verification) | - Symmetric encryption (key generation, encryption, decryption) | ||||||
| - Secure key derivation from passwords | - 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. | - `example.rhai` - Basic example demonstrating key management, signing, and encryption | ||||||
| The Rhai scripting integration is temporarily disabled while we adapt the examples | - `advanced_example.rhai` - Advanced example with error handling, conditional logic, and more complex operations | ||||||
| to work with the new vault API. | - `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 | You can run the examples using the `herodo` tool that comes with the SAL project: | ||||||
| - **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 |  | ||||||
|  |  | ||||||
| ## 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**: | # Run all examples using the provided script | ||||||
|  | ./run_examples.sh | ||||||
| - `_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) |  | ||||||
| ``` | ``` | ||||||
|  |  | ||||||
| ## Migration Status | ## Key Space Storage | ||||||
|  |  | ||||||
| - ✅ **Vault Core**: Lee's implementation is active | 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`). | ||||||
| - ✅ **Archive**: Sameh's implementation preserved in `vault/_archive/` |  | ||||||
| - ⏳ **Rhai Integration**: Being developed for Lee's implementation | ## Ethereum Functionality | ||||||
| - ⏳ **Examples**: Will be updated to use Lee's API |  | ||||||
| - ❌ **Ethereum Features**: Not available in Lee's implementation | 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 | ## 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 | ## Best Practices | ||||||
| - **Password-based key derivation** for keyspace encryption |  | ||||||
| - **Secure key storage** with proper isolation |  | ||||||
|  |  | ||||||
| ## Next Steps | 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. | ||||||
| 1. **Rhai Integration**: Implement Rhai bindings for Lee's vault | 3. **Script Organization**: Split your scripts into logical units, with separate scripts for key creation and key usage. | ||||||
| 2. **New Examples**: Create examples using Lee's simpler API | 4. **Error Handling**: Always check the return values of functions to ensure operations succeeded before proceeding. | ||||||
| 3. **Documentation**: Complete API documentation for Lee's implementation | 5. **Network Selection**: When working with Ethereum functionality, be explicit about which network you're targeting to avoid confusion. | ||||||
| 4. **Migration Guide**: Provide guidance for users migrating from Sameh's implementation | 6. **Gas Management**: For Ethereum transactions, consider gas costs and set appropriate gas limits. | ||||||
|   | |||||||
| @@ -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! ==="); |  | ||||||
| @@ -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(()) |  | ||||||
| } |  | ||||||
| @@ -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 ==="); |  | ||||||
| @@ -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(()) |  | ||||||
| } |  | ||||||
| @@ -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 ==="); |  | ||||||
| @@ -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(()) |  | ||||||
| } |  | ||||||
| @@ -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! ==="); |  | ||||||
| @@ -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! ==="); |  | ||||||
| @@ -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! ==="); |  | ||||||
| @@ -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."); |  | ||||||
| @@ -1,7 +1,6 @@ | |||||||
| // Example of using the network modules in SAL through Rhai | // Example of using the network modules in SAL through Rhai | ||||||
| // Shows TCP port checking, HTTP URL validation, and SSH command execution | // Shows TCP port checking, HTTP URL validation, and SSH command execution | ||||||
|  |  | ||||||
|  |  | ||||||
| // Function to print section header | // Function to print section header | ||||||
| fn section(title) { | fn section(title) { | ||||||
|     print("\n"); |     print("\n"); | ||||||
| @@ -20,14 +19,14 @@ let host = "localhost"; | |||||||
| let port = 22; | let port = 22; | ||||||
| print(`Checking if port ${port} is open on ${host}...`); | print(`Checking if port ${port} is open on ${host}...`); | ||||||
| let is_open = tcp.check_port(host, port); | 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 | // Check multiple ports | ||||||
| let ports = [22, 80, 443]; | let ports = [22, 80, 443]; | ||||||
| print(`Checking multiple ports on ${host}...`); | print(`Checking multiple ports on ${host}...`); | ||||||
| let port_results = tcp.check_ports(host, ports); | let port_results = tcp.check_ports(host, ports); | ||||||
| for result in port_results { | 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 | // HTTP connectivity checks | ||||||
| @@ -40,7 +39,7 @@ let http = net::new_http_connector(); | |||||||
| let url = "https://www.example.com"; | let url = "https://www.example.com"; | ||||||
| print(`Checking if ${url} is reachable...`); | print(`Checking if ${url} is reachable...`); | ||||||
| let is_reachable = http.check_url(url); | 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 | // Check the status code of a URL | ||||||
| print(`Checking status code of ${url}...`); | print(`Checking status code of ${url}...`); | ||||||
| @@ -69,7 +68,7 @@ if is_open { | |||||||
|     let ssh = net::new_ssh_builder() |     let ssh = net::new_ssh_builder() | ||||||
|         .host("localhost") |         .host("localhost") | ||||||
|         .port(22) |         .port(22) | ||||||
|         .user(if os::get_env("USER") != () { os::get_env("USER") } else { "root" }) |         .user(os::get_env("USER") || "root") | ||||||
|         .timeout(10) |         .timeout(10) | ||||||
|         .build(); |         .build(); | ||||||
|      |      | ||||||
|   | |||||||
| @@ -1,7 +1,7 @@ | |||||||
| print("Running a basic command using run().execute()..."); | print("Running a basic command using run().do()..."); | ||||||
|  |  | ||||||
| // Execute a simple command | // 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 the command result | ||||||
| print(`Command: echo Hello from run_basic!`); | 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) | // 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. | // This will halt execution by default because ignore_error() is not used. | ||||||
| // print("Running a command that will fail (and should halt)..."); | // 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."); | print("Basic run() example finished."); | ||||||
| @@ -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) | // Run a command that exits with a non-zero code (will fail) | ||||||
| // Using .ignore_error() prevents the script from halting | // 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(`Command finished.`); | ||||||
| print(`Success: ${result.success}`); // This should be false | 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) | // 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, | // 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. | // 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 Success: ${os_error_result.success}`); | ||||||
| // print(`OS Error Command Exit Code: ${os_error_result.code}`); | // print(`OS Error Command Exit Code: ${os_error_result.code}`); | ||||||
|  |  | ||||||
|   | |||||||
| @@ -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. | // 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. | // This is useful for debugging or tracing which commands are being run. | ||||||
|   | |||||||
| @@ -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 | // This command will print to standard output and standard error | ||||||
| // However, because .silent() is used, the output will not appear in the console directly | // 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 | // The output is still captured in the CommandResult | ||||||
| print(`Command finished.`); | print(`Command finished.`); | ||||||
| @@ -12,7 +12,7 @@ print(`Captured Stdout:\\n${result.stdout}`); | |||||||
| print(`Captured Stderr:\\n${result.stderr}`); | print(`Captured Stderr:\\n${result.stderr}`); | ||||||
|  |  | ||||||
| // Example of a silent command that fails (but won't halt because we only suppress output) | // 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(`Failed command finished (silent):`); | ||||||
| // print(`Success: ${fail_result.success}`); | // print(`Success: ${fail_result.success}`); | ||||||
| // print(`Exit Code: ${fail_result.code}`); | // print(`Exit Code: ${fail_result.code}`); | ||||||
|   | |||||||
| @@ -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}`); |  | ||||||
| } |  | ||||||
| ``` |  | ||||||
| @@ -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!"); |  | ||||||
| @@ -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!"); |  | ||||||
| @@ -10,12 +10,12 @@ license = "Apache-2.0" | |||||||
| [dependencies] | [dependencies] | ||||||
| # Use workspace dependencies for consistency | # Use workspace dependencies for consistency | ||||||
| regex = { workspace = true } | regex = { workspace = true } | ||||||
|  | redis = { workspace = true } | ||||||
| serde = { workspace = true } | serde = { workspace = true } | ||||||
| serde_json = { workspace = true } | serde_json = { workspace = true } | ||||||
| rhai = { workspace = true } | rhai = { workspace = true } | ||||||
| log = { workspace = true } | log = { workspace = true } | ||||||
| url = { workspace = true } | url = { workspace = true } | ||||||
| redis = { workspace = true } |  | ||||||
| 
 | 
 | ||||||
| [dev-dependencies] | [dev-dependencies] | ||||||
| tempfile = { workspace = true } | tempfile = { workspace = true } | ||||||
| @@ -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. | 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 | ## Core Components | ||||||
| 
 | 
 | ||||||
| The module is primarily composed of two main parts: | The module is primarily composed of two main parts: | ||||||
| @@ -18,8 +18,8 @@ path = "src/main.rs" | |||||||
| env_logger = { workspace = true } | env_logger = { workspace = true } | ||||||
| rhai = { workspace = true } | rhai = { workspace = true } | ||||||
|  |  | ||||||
| # SAL library for Rhai module registration (with all features for herodo) | # SAL library for Rhai module registration | ||||||
| sal = { path = "..", features = ["all"] } | sal = { path = ".." } | ||||||
|  |  | ||||||
| [dev-dependencies] | [dev-dependencies] | ||||||
| tempfile = { workspace = true } | tempfile = { workspace = true } | ||||||
|   | |||||||
| @@ -15,32 +15,14 @@ Herodo is a command-line utility that executes Rhai scripts with full access to | |||||||
|  |  | ||||||
| ## Installation | ## Installation | ||||||
|  |  | ||||||
| ### Build and Install | Build the herodo binary: | ||||||
|  |  | ||||||
| ```bash | ```bash | ||||||
| git clone https://github.com/PlanetFirst/sal.git | cd herodo | ||||||
| cd sal | cargo build --release | ||||||
| ./build_herodo.sh |  | ||||||
| ``` | ``` | ||||||
|  |  | ||||||
| This script will: | The executable will be available at `target/release/herodo`. | ||||||
| - 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. |  | ||||||
|  |  | ||||||
| ## Usage | ## Usage | ||||||
|  |  | ||||||
|   | |||||||
| @@ -3,7 +3,7 @@ | |||||||
| //! This library loads the Rhai engine, registers all SAL modules, | //! This library loads the Rhai engine, registers all SAL modules, | ||||||
| //! and executes Rhai scripts from a specified directory in sorted order. | //! and executes Rhai scripts from a specified directory in sorted order. | ||||||
|  |  | ||||||
| use rhai::{Engine, Scope}; | use rhai::Engine; | ||||||
| use std::error::Error; | use std::error::Error; | ||||||
| use std::fs; | use std::fs; | ||||||
| use std::path::{Path, PathBuf}; | use std::path::{Path, PathBuf}; | ||||||
| @@ -30,19 +30,6 @@ pub fn run(script_path: &str) -> Result<(), Box<dyn Error>> { | |||||||
|     // Create a new Rhai engine |     // Create a new Rhai engine | ||||||
|     let mut engine = Engine::new(); |     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(); |  | ||||||
|     // Conditionally add Hetzner client only when env config is present |  | ||||||
|     if let Ok(cfg) = sal::hetzner::config::Config::from_env() { |  | ||||||
|         let hetzner_client = sal::hetzner::api::Client::new(cfg); |  | ||||||
|         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 |     // Register println function for output | ||||||
|     engine.register_fn("println", |s: &str| println!("{}", s)); |     engine.register_fn("println", |s: &str| println!("{}", s)); | ||||||
|  |  | ||||||
| @@ -91,20 +78,19 @@ pub fn run(script_path: &str) -> Result<(), Box<dyn Error>> { | |||||||
|         let script = fs::read_to_string(&script_file)?; |         let script = fs::read_to_string(&script_file)?; | ||||||
|  |  | ||||||
|         // Execute the script |         // Execute the script | ||||||
|         // match engine.eval::<rhai::Dynamic>(&script) { |         match engine.eval::<rhai::Dynamic>(&script) { | ||||||
|         //     Ok(result) => { |             Ok(result) => { | ||||||
|         //         println!("Script executed successfully"); |                 println!("Script executed successfully"); | ||||||
|         //         if !result.is_unit() { |                 if !result.is_unit() { | ||||||
|         //             println!("Result: {}", result); |                     println!("Result: {}", result); | ||||||
|         //         } |                 } | ||||||
|         //     } |             } | ||||||
|         //     Err(err) => { |             Err(err) => { | ||||||
|         //         eprintln!("Error executing script: {}", err); |                 eprintln!("Error executing script: {}", err); | ||||||
|         //         // Exit with error code when a script fails |                 // Exit with error code when a script fails | ||||||
|         //         process::exit(1); |                 process::exit(1); | ||||||
|         //     } |             } | ||||||
|         // } |         } | ||||||
|         engine.run_with_scope(&mut scope, &script)?; |  | ||||||
|     } |     } | ||||||
|  |  | ||||||
|     println!("\nAll scripts executed successfully!"); |     println!("\nAll scripts executed successfully!"); | ||||||
|   | |||||||
| @@ -9,22 +9,22 @@ license = "Apache-2.0" | |||||||
| 
 | 
 | ||||||
| [dependencies] | [dependencies] | ||||||
| # HTTP client for async requests | # HTTP client for async requests | ||||||
| reqwest = { workspace = true } | reqwest = { version = "0.12.15", features = ["json"] } | ||||||
| # JSON handling | # JSON handling | ||||||
| serde_json = { workspace = true } | serde_json = "1.0" | ||||||
| # Base64 encoding/decoding for message payloads | # Base64 encoding/decoding for message payloads | ||||||
| base64 = { workspace = true } | base64 = "0.22.1" | ||||||
| # Async runtime | # Async runtime | ||||||
| tokio = { workspace = true } | tokio = { version = "1.45.0", features = ["full"] } | ||||||
| # Rhai scripting support | # Rhai scripting support | ||||||
| rhai = { workspace = true } | rhai = { version = "1.12.0", features = ["sync"] } | ||||||
| # Logging | # Logging | ||||||
| log = { workspace = true } | log = "0.4" | ||||||
| # URL encoding for API parameters | # URL encoding for API parameters | ||||||
| urlencoding = { workspace = true } | urlencoding = "2.1.3" | ||||||
| 
 | 
 | ||||||
| [dev-dependencies] | [dev-dependencies] | ||||||
| # For async testing | # For async testing | ||||||
| tokio-test = { workspace = true } | tokio-test = "0.4.4" | ||||||
| # For temporary files in tests | # For temporary files in tests | ||||||
| tempfile = { workspace = true } | tempfile = "3.5" | ||||||
| @@ -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. | 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 | ## Overview | ||||||
| 
 | 
 | ||||||
| SAL Mycelium provides async HTTP client functionality for managing Mycelium nodes, including: | SAL Mycelium provides async HTTP client functionality for managing Mycelium nodes, including: | ||||||
| @@ -10,7 +10,7 @@ keywords = ["network", "tcp", "http", "ssh", "connectivity"] | |||||||
| categories = ["network-programming", "api-bindings"] | categories = ["network-programming", "api-bindings"] | ||||||
| 
 | 
 | ||||||
| [dependencies] | [dependencies] | ||||||
| anyhow = { workspace = true } | anyhow = "1.0.98" | ||||||
| tokio = { workspace = true } | tokio = { version = "1.0", features = ["full"] } | ||||||
| reqwest = { workspace = true, features = ["json", "blocking"] } | reqwest = { version = "0.12", features = ["json", "blocking"] } | ||||||
| rhai = { workspace = true } | rhai = "1.19.0" | ||||||
| @@ -1,16 +1,7 @@ | |||||||
| # SAL Network Package (`sal-net`) | # SAL Network Package | ||||||
| 
 | 
 | ||||||
| Network connectivity utilities for TCP, HTTP, and SSH operations. | Network connectivity utilities for TCP, HTTP, and SSH operations. | ||||||
| 
 | 
 | ||||||
| ## Installation |  | ||||||
| 
 |  | ||||||
| Add this to your `Cargo.toml`: |  | ||||||
| 
 |  | ||||||
| ```toml |  | ||||||
| [dependencies] |  | ||||||
| sal-net = "0.1.0" |  | ||||||
| ``` |  | ||||||
| 
 |  | ||||||
| ## Overview | ## 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. | 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. | ||||||
| @@ -14,8 +14,7 @@ categories = ["os", "filesystem", "api-bindings"] | |||||||
| dirs = { workspace = true } | dirs = { workspace = true } | ||||||
| glob = { workspace = true } | glob = { workspace = true } | ||||||
| libc = { workspace = true } | libc = { workspace = true } | ||||||
| anyhow = {workspace = true} | 
 | ||||||
| reqwest = {workspace = true} |  | ||||||
| # Error handling | # Error handling | ||||||
| thiserror = { workspace = true } | thiserror = { workspace = true } | ||||||
| 
 | 
 | ||||||
Some files were not shown because too many files have changed in this diff Show More
		Reference in New Issue
	
	Block a user