Update git remote URL from git.ourworld.tf to git.threefold.info

This commit is contained in:
despiegk 2025-06-15 16:21:00 +02:00
parent ae6966edb2
commit fab2d199d1
13 changed files with 195 additions and 2056 deletions

124
examples/zinit_client_example.vsh Executable file
View File

@ -0,0 +1,124 @@
#!/usr/bin/env -S v -n -w -cg -gc none -cc tcc -d use_openssl -enable-globals run
import freeflowuniverse.heroweb.clients.zinit
import x.json2
// Create a new Zinit client with the default socket path
mut client := zinit.new_default_client()
println('Connected to Zinit via OpenRPC')
// Example 1: Get the OpenRPC API specification
println('\n=== Getting API Specification ===')
api_spec_response := client.rpc_discover() or {
println('Error getting API spec: ${err}')
return
}
println('API Specification (first 100 chars): ${json2.encode(api_spec_response.spec)[..100]}...')
// Example 2: List all services
println('\n=== Listing Services ===')
service_list_response := client.service_list() or {
println('Error listing services: ${err}')
return
}
println('Services:')
for name, state in service_list_response.services {
println('- ${name}: ${state}')
}
// Example 3: Get detailed status of a service (if any exist)
if service_list_response.services.len > 0 {
service_name := service_list_response.services.keys()[0]
println('\n=== Getting Status for Service: ${service_name} ===')
status := client.service_status(service_name) or {
println('Error getting status: ${err}')
return
}
println('Service Status:')
println('- Name: ${status.name}')
println('- PID: ${status.pid}')
println('- State: ${status.state}')
println('- Target: ${status.target}')
println('- Dependencies:')
for dep_name, dep_state in status.after {
println(' - ${dep_name}: ${dep_state}')
}
// Example 4: Get service stats
println('\n=== Getting Stats for Service: ${service_name} ===')
stats := client.service_stats(service_name) or {
println('Error getting stats: ${err}')
println('Note: Stats are only available for running services')
return
}
println('Service Stats:')
println('- Memory Usage: ${stats.memory_usage} bytes (${zinit.format_memory_usage(stats.memory_usage)})')
println('- CPU Usage: ${stats.cpu_usage}% (${zinit.format_cpu_usage(stats.cpu_usage)})')
if stats.children.len > 0 {
println('- Child Processes:')
for child in stats.children {
println(' - PID: ${child.pid}, Memory: ${zinit.format_memory_usage(child.memory_usage)}, CPU: ${zinit.format_cpu_usage(child.cpu_usage)}')
}
}
// Example 5: Get logs for a service
println('\n=== Getting Logs for Service: ${service_name} ===')
logs_response := client.stream_current_logs(service_name) or {
println('Error getting logs: ${err}')
return
}
println('Service logs:')
for log in logs_response.logs {
println('- ${log}')
}
} else {
println('\nNo services found to query')
}
// Example 6: Create a new service (commented out for safety)
/*
println('\n=== Creating a New Service ===')
new_service_config := zinit.ServiceConfig{
exec: '/bin/echo "Hello from Zinit"'
oneshot: true
after: []string{}
log: zinit.log_stdout
env: {
'ENV_VAR': 'value'
}
}
create_response := client.service_create('example_service', new_service_config) or {
println('Error creating service: ${err}')
return
}
println('Service created: ${create_response.path}')
// Start the service
client.service_start('example_service') or {
println('Error starting service: ${err}')
return
}
println('Service started')
// Delete the service when done
client.service_stop('example_service') or {
println('Error stopping service: ${err}')
return
}
client.service_forget('example_service') or {
println('Error forgetting service: ${err}')
return
}
delete_response := client.service_delete('example_service') or {
println('Error deleting service: ${err}')
return
}
println('Service deleted: ${delete_response.result}')
*/
println('\nZinit OpenRPC client example completed')

66
examples/zinit_rpc_example.vsh Executable file
View File

@ -0,0 +1,66 @@
#!/usr/bin/env -S v -n -w -cg -gc none -cc tcc -d use_openssl -enable-globals run
import freeflowuniverse.herolib.schemas.jsonrpc
import freeflowuniverse.herolib.clients.zinit
import json
// We'll use the ServiceStatusResponse from the zinit module
// No need to define our own struct
// Create a client using the Unix socket transport
mut cl := jsonrpc.new_unix_socket_client("/tmp/zinit.sock")
// Example 1: Discover the API using rpc_discover
// Create a request for rpc_discover method with empty parameters
discover_request := jsonrpc.new_request_generic('rpc.discover', []string{})
// Send the request and receive the OpenRPC specification as a JSON string
println('Sending rpc_discover request...')
println('This will return the OpenRPC specification for the API')
// Use map[string]string for the result to avoid json2.Any issues
api_spec_raw := cl.send[[]string, string](discover_request)!
// Parse the JSON string manually
println('API Specification (raw):')
println(api_spec_raw)
// Example 2: List all services
// Create a request for service_list method with empty parameters
list_request := jsonrpc.new_request_generic('service_list', []string{})
// Send the request and receive a map of service names to states
println('\nSending service_list request...')
service_list := cl.send[[]string, map[string]string](list_request)!
// Display the service list
println('Service List:')
println(service_list)
// Example 3: Get status of a specific service
// First, check if we have any services to query
if service_list.len > 0 {
// Get the first service name from the list
service_name := service_list.keys()[0]
// Create a request for service_status method with the service name as parameter
// The parameter for service_status is a single string (service name)
status_request := jsonrpc.new_request_generic('service_status', service_name)
// Send the request and receive a ServiceStatusResponse object
println('\nSending service_status request for service: $service_name')
service_status := cl.send[string, zinit.ServiceStatusResponse](status_request)!
// Display the service status details
println('Service Status:')
println('- Name: ${service_status.name}')
println('- PID: ${service_status.pid}')
println('- State: ${service_status.state}')
println('- Target: ${service_status.target}')
println('- Dependencies:')
for dep_name, dep_state in service_status.after {
println(' - $dep_name: $dep_state')
}
} else {
println('\nNo services found to query status')
}

View File

View File

@ -7,13 +7,13 @@ pub:
} }
// get_all_processes returns all processes in the system // get_all_processes returns all processes in the system
pub fn (pc &ProcessController) get_all_processes() []Process { pub fn (pc &ProcessController) get_all_processes() ![]Process {
// For now using fake data, will be replaced with openrpc calls // For now using fake data, will be replaced with openrpc calls
return get_all_processes() return get_all_processes()
} }
// get_process_by_pid returns a specific process by PID // get_process_by_pid returns a specific process by PID
pub fn (pc &ProcessController) get_process_by_pid(pid int) ?Process { pub fn (pc &ProcessController) get_process_by_pid(pid int) !Process {
processes := get_all_processes() processes := get_all_processes()
for process in processes { for process in processes {
if process.pid == pid { if process.pid == pid {

View File

@ -1,4 +1,6 @@
module controllers module controllers
import freeflowuniverse.herolib.schemas.openrpc
// OpenRPCController handles OpenRPC-related operations // OpenRPCController handles OpenRPC-related operations
pub struct OpenRPCController { pub struct OpenRPCController {
pub: pub:
@ -12,7 +14,7 @@ pub fn (oc &OpenRPCController) get_all_specs() []OpenRPCSpec {
} }
// get_spec_by_name returns a specific OpenRPC specification by name // get_spec_by_name returns a specific OpenRPC specification by name
pub fn (oc &OpenRPCController) get_spec_by_name(name string) ?OpenRPCSpec { pub fn (oc &OpenRPCController) get_spec_by_name(name string) !openrpc.OpenRPC {
specs := get_all_openrpc_specs() specs := get_all_openrpc_specs()
for spec in specs { for spec in specs {
if spec.title.to_lower() == name.to_lower() { if spec.title.to_lower() == name.to_lower() {

View File

@ -1,406 +0,0 @@
# Zinit Client Module
A well-documented V (Vlang) client library for interacting with the Zinit service manager via JSON-RPC over Unix socket.
## Overview
This module provides a complete client implementation for the Zinit JSON-RPC API, allowing you to manage services, monitor system resources, and control the Zinit service manager programmatically.
## Features
- **Service Management**: Start, stop, monitor, and forget services
- **Service Configuration**: Create, delete, and retrieve service configurations
- **System Control**: Shutdown, reboot, and HTTP server management
- **Monitoring**: Get service status, statistics, and resource usage
- **Logging**: Stream current logs and subscribe to log updates
- **Error Handling**: Comprehensive error handling with detailed error messages
- **Type Safety**: Strongly typed structs for all API responses
- **Connection Management**: Automatic connection handling with proper cleanup
## Installation
Simply import the module in your V project:
```v
import zinit
```
## Quick Start
```v
import zinit
fn main() {
// Create a client with default socket path (/tmp/zinit.sock)
mut client := zinit.new_default_client()
// Or specify a custom socket path
// mut client := zinit.new_client('/custom/path/to/zinit.sock')
defer {
client.disconnect()
}
// List all services
services := client.service_list() or {
eprintln('Error: ${err}')
return
}
for name, state in services {
println('${name}: ${state}')
}
}
```
## API Reference
### Client Creation
#### `new_client(socket_path string) &Client`
Creates a new zinit client with a custom socket path.
#### `new_default_client() &Client`
Creates a new zinit client with the default socket path (`/tmp/zinit.sock`).
### Connection Management
#### `connect() !`
Establishes a connection to the zinit Unix socket. Called automatically by API methods.
#### `disconnect()`
Closes the connection to the zinit Unix socket. Should be called when done with the client.
### Service Management
#### `service_list() !map[string]string`
Lists all services managed by Zinit.
**Returns**: A map of service names to their current states.
```v
services := client.service_list()!
for name, state in services {
println('${name}: ${state}')
}
```
#### `service_status(name string) !ServiceStatus`
Shows detailed status information for a specific service.
**Parameters**:
- `name`: The name of the service
**Returns**: `ServiceStatus` struct with detailed information.
```v
status := client.service_status('redis')!
println('Service: ${status.name}')
println('State: ${status.state}')
println('PID: ${status.pid}')
```
#### `service_start(name string) !`
Starts a service.
**Parameters**:
- `name`: The name of the service to start
#### `service_stop(name string) !`
Stops a service.
**Parameters**:
- `name`: The name of the service to stop
#### `service_monitor(name string) !`
Starts monitoring a service. The service configuration is loaded from the config directory.
**Parameters**:
- `name`: The name of the service to monitor
#### `service_forget(name string) !`
Stops monitoring a service. You can only forget a stopped service.
**Parameters**:
- `name`: The name of the service to forget
#### `service_kill(name string, signal string) !`
Sends a signal to a running service.
**Parameters**:
- `name`: The name of the service to send the signal to
- `signal`: The signal to send (e.g., "SIGTERM", "SIGKILL")
### Service Configuration
#### `service_create(name string, config ServiceConfig) !string`
Creates a new service configuration file.
**Parameters**:
- `name`: The name of the service to create
- `config`: The service configuration
**Returns**: Result message.
```v
config := zinit.ServiceConfig{
exec: '/usr/bin/redis-server'
oneshot: false
log: 'stdout'
env: {
'REDIS_PORT': '6379'
}
shutdown_timeout: 30
}
result := client.service_create('redis', config)!
println('Service created: ${result}')
```
#### `service_delete(name string) !string`
Deletes a service configuration file.
**Parameters**:
- `name`: The name of the service to delete
**Returns**: Result message.
#### `service_get(name string) !ServiceConfig`
Gets a service configuration file.
**Parameters**:
- `name`: The name of the service to get
**Returns**: `ServiceConfig` struct with the service configuration.
### Service Statistics
#### `service_stats(name string) !ServiceStats`
Gets memory and CPU usage statistics for a service.
**Parameters**:
- `name`: The name of the service to get stats for
**Returns**: `ServiceStats` struct with usage information.
```v
stats := client.service_stats('redis')!
println('Memory Usage: ${stats.memory_usage / 1024 / 1024} MB')
println('CPU Usage: ${stats.cpu_usage}%')
```
### System Operations
#### `system_shutdown() !`
Stops all services and powers off the system.
⚠️ **Warning**: This will actually shut down the system!
#### `system_reboot() !`
Stops all services and reboots the system.
⚠️ **Warning**: This will actually reboot the system!
#### `system_start_http_server(address string) !string`
Starts an HTTP/RPC server at the specified address.
**Parameters**:
- `address`: The network address to bind the server to (e.g., "127.0.0.1:8080")
**Returns**: Result message.
#### `system_stop_http_server() !`
Stops the HTTP/RPC server if running.
### Logging
#### `stream_current_logs(name ?string) ![]string`
Gets current logs from zinit and monitored services.
**Parameters**:
- `name`: Optional service name filter. If provided, only logs from this service will be returned.
**Returns**: Array of log strings.
```v
// Get all logs
all_logs := client.stream_current_logs(none)!
// Get logs for a specific service
redis_logs := client.stream_current_logs('redis')!
```
#### `stream_subscribe_logs(name ?string) !string`
Subscribes to log messages generated by zinit and monitored services.
**Parameters**:
- `name`: Optional service name filter.
**Returns**: A single log message.
**Note**: For continuous streaming, call this method repeatedly.
### API Discovery
#### `rpc_discover() !map[string]interface{}`
Returns the OpenRPC specification for the API.
**Returns**: The complete OpenRPC specification as a map.
## Data Types
### ServiceConfig
Represents the configuration for a zinit service.
```v
struct ServiceConfig {
pub mut:
exec string // Command to run
oneshot bool // Whether the service should be restarted
after []string // Services that must be running before this one starts
log string // How to handle service output (null, ring, stdout)
env map[string]string // Environment variables for the service
shutdown_timeout int // Maximum time to wait for service to stop during shutdown
}
```
### ServiceStatus
Represents the detailed status information for a service.
```v
struct ServiceStatus {
pub mut:
name string // Service name
pid int // Process ID of the running service (if running)
state string // Current state of the service (Running, Success, Error, etc.)
target string // Target state of the service (Up, Down)
after map[string]string // Dependencies of the service and their states
}
```
### ServiceStats
Represents memory and CPU usage statistics for a service.
```v
struct ServiceStats {
pub mut:
name string // Service name
pid int // Process ID of the service
memory_usage i64 // Memory usage in bytes
cpu_usage f64 // CPU usage as a percentage (0-100)
children []ChildStats // Stats for child processes
}
```
### ChildStats
Represents statistics for a child process.
```v
struct ChildStats {
pub mut:
pid int // Process ID of the child process
memory_usage i64 // Memory usage in bytes
cpu_usage f64 // CPU usage as a percentage (0-100)
}
```
## Error Handling
The client provides comprehensive error handling through V's error system. All API methods that can fail return a result type (`!`).
### ZinitError
Custom error type for zinit-specific errors.
```v
struct ZinitError {
pub mut:
code int // Error code
message string // Error message
data string // Additional error data
}
```
Common error codes:
- `-32000`: Service not found
- `-32001`: Service already monitored
- `-32002`: Service is up
- `-32003`: Service is down
- `-32004`: Invalid signal
- `-32005`: Config error
- `-32006`: Shutting down
- `-32007`: Service already exists
- `-32008`: Service file error
## Examples
See `example.v` for comprehensive usage examples covering all API methods.
### Basic Service Management
```v
import zinit
fn manage_service() ! {
mut client := zinit.new_default_client()
defer { client.disconnect() }
// Create a service
config := zinit.ServiceConfig{
exec: '/usr/bin/nginx'
oneshot: false
log: 'stdout'
after: ['network']
}
client.service_create('nginx', config)!
client.service_monitor('nginx')!
client.service_start('nginx')!
// Check status
status := client.service_status('nginx')!
println('Nginx is ${status.state}')
// Get statistics
stats := client.service_stats('nginx')!
println('Memory: ${stats.memory_usage / 1024 / 1024} MB')
}
```
### Log Monitoring
```v
import zinit
import time
fn monitor_logs() ! {
mut client := zinit.new_default_client()
defer { client.disconnect() }
// Get current logs
logs := client.stream_current_logs(none)!
for log in logs {
println(log)
}
// Subscribe to new logs (simplified example)
for i in 0..10 {
log_entry := client.stream_subscribe_logs(none) or { continue }
println('New log: ${log_entry}')
time.sleep(1 * time.second)
}
}
```
## Thread Safety
The client is not thread-safe. If you need to use it from multiple threads, you should create separate client instances or implement your own synchronization.
## Requirements
- V compiler
- Unix-like operating system (for Unix socket support)
- Running Zinit service manager
## License
This module follows the same license as the parent project.

View File

@ -1,211 +0,0 @@
module zinit
import net.unix
import json
import time
// service_status shows detailed status information for a specific service
// name: the name of the service
pub fn (mut c Client) service_status(name string) !ServiceStatus {
params := [name]
response := c.send_request('service_status', params)!
result_map := response.result as map[string]interface{}
mut after_map := map[string]string{}
if after_raw := result_map['after'] {
if after_obj := after_raw as map[string]interface{} {
for key, value in after_obj {
after_map[key] = value.str()
}
}
}
return ServiceStatus{
name: result_map['name'] or { '' }.str()
pid: result_map['pid'] or { 0 }.int()
state: result_map['state'] or { '' }.str()
target: result_map['target'] or { '' }.str()
after: after_map
}
}
// service_start starts a service
// name: the name of the service to start
pub fn (mut c Client) service_start(name string) ! {
params := [name]
c.send_request('service_start', params)!
}
// service_stop stops a service
// name: the name of the service to stop
pub fn (mut c Client) service_stop(name string) ! {
params := [name]
c.send_request('service_stop', params)!
}
// service_monitor starts monitoring a service
// The service configuration is loaded from the config directory
// name: the name of the service to monitor
pub fn (mut c Client) service_monitor(name string) ! {
params := [name]
c.send_request('service_monitor', params)!
}
// service_forget stops monitoring a service
// You can only forget a stopped service
// name: the name of the service to forget
pub fn (mut c Client) service_forget(name string) ! {
params := [name]
c.send_request('service_forget', params)!
}
// service_kill sends a signal to a running service
// name: the name of the service to send the signal to
// signal: the signal to send (e.g., SIGTERM, SIGKILL)
pub fn (mut c Client) service_kill(name string, signal string) ! {
params := [name, signal]
c.send_request('service_kill', params)!
}
// service_create creates a new service configuration file
// name: the name of the service to create
// config: the service configuration
pub fn (mut c Client) service_create(name string, config ServiceConfig) !string {
params := [name, config]
response := c.send_request('service_create', params)!
return response.result.str()
}
// service_delete deletes a service configuration file
// name: the name of the service to delete
pub fn (mut c Client) service_delete(name string) !string {
params := [name]
response := c.send_request('service_delete', params)!
return response.result.str()
}
// service_get gets a service configuration file
// name: the name of the service to get
pub fn (mut c Client) service_get(name string) !ServiceConfig {
params := [name]
response := c.send_request('service_get', params)!
result_map := response.result as map[string]interface{}
mut after_list := []string{}
if after_raw := result_map['after'] {
if after_array := after_raw as []interface{} {
for item in after_array {
after_list << item.str()
}
}
}
mut env_map := map[string]string{}
if env_raw := result_map['env'] {
if env_obj := env_raw as map[string]interface{} {
for key, value in env_obj {
env_map[key] = value.str()
}
}
}
return ServiceConfig{
exec: result_map['exec'] or { '' }.str()
oneshot: result_map['oneshot'] or { false }.bool()
after: after_list
log: result_map['log'] or { '' }.str()
env: env_map
shutdown_timeout: result_map['shutdown_timeout'] or { 0 }.int()
}
}
// service_stats gets memory and CPU usage statistics for a service
// name: the name of the service to get stats for
pub fn (mut c Client) service_stats(name string) !ServiceStats {
params := [name]
response := c.send_request('service_stats', params)!
result_map := response.result as map[string]interface{}
mut children_list := []ChildStats{}
if children_raw := result_map['children'] {
if children_array := children_raw as []interface{} {
for child_raw in children_array {
if child_map := child_raw as map[string]interface{} {
children_list << ChildStats{
pid: child_map['pid'] or { 0 }.int()
memory_usage: child_map['memory_usage'] or { i64(0) }.i64()
cpu_usage: child_map['cpu_usage'] or { 0.0 }.f64()
}
}
}
}
}
return ServiceStats{
name: result_map['name'] or { '' }.str()
pid: result_map['pid'] or { 0 }.int()
memory_usage: result_map['memory_usage'] or { i64(0) }.i64()
cpu_usage: result_map['cpu_usage'] or { 0.0 }.f64()
children: children_list
}
}
// system_shutdown stops all services and powers off the system
pub fn (mut c Client) system_shutdown() ! {
c.send_request('system_shutdown', []interface{})!
}
// system_reboot stops all services and reboots the system
pub fn (mut c Client) system_reboot() ! {
c.send_request('system_reboot', []interface{})!
}
// system_start_http_server starts an HTTP/RPC server at the specified address
// address: the network address to bind the server to (e.g., '127.0.0.1:8080')
pub fn (mut c Client) system_start_http_server(address string) !string {
params := [address]
response := c.send_request('system_start_http_server', params)!
return response.result.str()
}
// system_stop_http_server stops the HTTP/RPC server if running
pub fn (mut c Client) system_stop_http_server() ! {
c.send_request('system_stop_http_server', []interface{})!
}
// stream_current_logs gets current logs from zinit and monitored services
// name: optional service name filter. If provided, only logs from this service will be returned
pub fn (mut c Client) stream_current_logs(name ?string) ![]string {
mut params := []interface{}
if service_name := name {
params << service_name
}
response := c.send_request('stream_currentLogs', params)!
if logs_array := response.result as []interface{} {
mut logs := []string{}
for log_entry in logs_array {
logs << log_entry.str()
}
return logs
}
return []string{}
}
// stream_subscribe_logs subscribes to log messages generated by zinit and monitored services
// name: optional service name filter. If provided, only logs from this service will be returned
// Note: This method returns a single log message. For continuous streaming, call this method repeatedly
pub fn (mut c Client) stream_subscribe_logs(name ?string) !string {
mut params := []interface{}
if service_name := name {
params << service_name
}
response := c.send_request('stream_subscribeLogs', params)!
return response.result.str()
}

View File

@ -1,873 +0,0 @@
{
"openrpc": "1.2.6",
"info": {
"version": "1.0.0",
"title": "Zinit JSON-RPC API",
"description": "JSON-RPC 2.0 API for controlling and querying Zinit services",
"license": {
"name": "MIT"
}
},
"servers": [
{
"name": "Unix Socket",
"url": "unix:///tmp/zinit.sock"
}
],
"methods": [
{
"name": "rpc_discover",
"description": "Returns the OpenRPC specification for the API",
"params": [],
"result": {
"name": "OpenRPCSpec",
"description": "The OpenRPC specification",
"schema": {
"type": "object"
}
},
"examples": [
{
"name": "Get API specification",
"params": [],
"result": {
"name": "OpenRPCSpecResult",
"value": {
"openrpc": "1.2.6",
"info": {
"version": "1.0.0",
"title": "Zinit JSON-RPC API"
}
}
}
}
]
},
{
"name": "service_list",
"description": "Lists all services managed by Zinit",
"params": [],
"result": {
"name": "ServiceList",
"description": "A map of service names to their current states",
"schema": {
"type": "object",
"additionalProperties": {
"type": "string",
"description": "Service state (Running, Success, Error, etc.)"
}
}
},
"examples": [
{
"name": "List all services",
"params": [],
"result": {
"name": "ServiceListResult",
"value": {
"service1": "Running",
"service2": "Success",
"service3": "Error"
}
}
}
]
},
{
"name": "service_status",
"description": "Shows detailed status information for a specific service",
"params": [
{
"name": "name",
"description": "The name of the service",
"required": true,
"schema": {
"type": "string"
}
}
],
"result": {
"name": "ServiceStatus",
"description": "Detailed status information for the service",
"schema": {
"type": "object",
"properties": {
"name": {
"type": "string",
"description": "Service name"
},
"pid": {
"type": "integer",
"description": "Process ID of the running service (if running)"
},
"state": {
"type": "string",
"description": "Current state of the service (Running, Success, Error, etc.)"
},
"target": {
"type": "string",
"description": "Target state of the service (Up, Down)"
},
"after": {
"type": "object",
"description": "Dependencies of the service and their states",
"additionalProperties": {
"type": "string",
"description": "State of the dependency"
}
}
}
}
},
"examples": [
{
"name": "Get status of redis service",
"params": [
{
"name": "name",
"value": "redis"
}
],
"result": {
"name": "ServiceStatusResult",
"value": {
"name": "redis",
"pid": 1234,
"state": "Running",
"target": "Up",
"after": {
"dependency1": "Success",
"dependency2": "Running"
}
}
}
}
],
"errors": [
{
"code": -32000,
"message": "Service not found",
"data": "service name \"unknown\" unknown"
}
]
},
{
"name": "service_start",
"description": "Starts a service",
"params": [
{
"name": "name",
"description": "The name of the service to start",
"required": true,
"schema": {
"type": "string"
}
}
],
"result": {
"name": "StartResult",
"description": "Result of the start operation",
"schema": {
"type": "null"
}
},
"examples": [
{
"name": "Start redis service",
"params": [
{
"name": "name",
"value": "redis"
}
],
"result": {
"name": "StartResult",
"value": null
}
}
],
"errors": [
{
"code": -32000,
"message": "Service not found",
"data": "service name \"unknown\" unknown"
}
]
},
{
"name": "service_stop",
"description": "Stops a service",
"params": [
{
"name": "name",
"description": "The name of the service to stop",
"required": true,
"schema": {
"type": "string"
}
}
],
"result": {
"name": "StopResult",
"description": "Result of the stop operation",
"schema": {
"type": "null"
}
},
"examples": [
{
"name": "Stop redis service",
"params": [
{
"name": "name",
"value": "redis"
}
],
"result": {
"name": "StopResult",
"value": null
}
}
],
"errors": [
{
"code": -32000,
"message": "Service not found",
"data": "service name \"unknown\" unknown"
},
{
"code": -32003,
"message": "Service is down",
"data": "service \"redis\" is down"
}
]
},
{
"name": "service_monitor",
"description": "Starts monitoring a service. The service configuration is loaded from the config directory.",
"params": [
{
"name": "name",
"description": "The name of the service to monitor",
"required": true,
"schema": {
"type": "string"
}
}
],
"result": {
"name": "MonitorResult",
"description": "Result of the monitor operation",
"schema": {
"type": "null"
}
},
"examples": [
{
"name": "Monitor redis service",
"params": [
{
"name": "name",
"value": "redis"
}
],
"result": {
"name": "MonitorResult",
"value": null
}
}
],
"errors": [
{
"code": -32001,
"message": "Service already monitored",
"data": "service \"redis\" already monitored"
},
{
"code": -32005,
"message": "Config error",
"data": "failed to load service configuration"
}
]
},
{
"name": "service_forget",
"description": "Stops monitoring a service. You can only forget a stopped service.",
"params": [
{
"name": "name",
"description": "The name of the service to forget",
"required": true,
"schema": {
"type": "string"
}
}
],
"result": {
"name": "ForgetResult",
"description": "Result of the forget operation",
"schema": {
"type": "null"
}
},
"examples": [
{
"name": "Forget redis service",
"params": [
{
"name": "name",
"value": "redis"
}
],
"result": {
"name": "ForgetResult",
"value": null
}
}
],
"errors": [
{
"code": -32000,
"message": "Service not found",
"data": "service name \"unknown\" unknown"
},
{
"code": -32002,
"message": "Service is up",
"data": "service \"redis\" is up"
}
]
},
{
"name": "service_kill",
"description": "Sends a signal to a running service",
"params": [
{
"name": "name",
"description": "The name of the service to send the signal to",
"required": true,
"schema": {
"type": "string"
}
},
{
"name": "signal",
"description": "The signal to send (e.g., SIGTERM, SIGKILL)",
"required": true,
"schema": {
"type": "string"
}
}
],
"result": {
"name": "KillResult",
"description": "Result of the kill operation",
"schema": {
"type": "null"
}
},
"examples": [
{
"name": "Send SIGTERM to redis service",
"params": [
{
"name": "name",
"value": "redis"
},
{
"name": "signal",
"value": "SIGTERM"
}
],
"result": {
"name": "KillResult",
"value": null
}
}
],
"errors": [
{
"code": -32000,
"message": "Service not found",
"data": "service name \"unknown\" unknown"
},
{
"code": -32003,
"message": "Service is down",
"data": "service \"redis\" is down"
},
{
"code": -32004,
"message": "Invalid signal",
"data": "invalid signal: INVALID"
}
]
},
{
"name": "system_shutdown",
"description": "Stops all services and powers off the system",
"params": [],
"result": {
"name": "ShutdownResult",
"description": "Result of the shutdown operation",
"schema": {
"type": "null"
}
},
"examples": [
{
"name": "Shutdown the system",
"params": [],
"result": {
"name": "ShutdownResult",
"value": null
}
}
],
"errors": [
{
"code": -32006,
"message": "Shutting down",
"data": "system is already shutting down"
}
]
},
{
"name": "system_reboot",
"description": "Stops all services and reboots the system",
"params": [],
"result": {
"name": "RebootResult",
"description": "Result of the reboot operation",
"schema": {
"type": "null"
}
},
"examples": [
{
"name": "Reboot the system",
"params": [],
"result": {
"name": "RebootResult",
"value": null
}
}
],
"errors": [
{
"code": -32006,
"message": "Shutting down",
"data": "system is already shutting down"
}
]
},
{
"name": "service_create",
"description": "Creates a new service configuration file",
"params": [
{
"name": "name",
"description": "The name of the service to create",
"required": true,
"schema": {
"type": "string"
}
},
{
"name": "content",
"description": "The service configuration content",
"required": true,
"schema": {
"type": "object",
"properties": {
"exec": {
"type": "string",
"description": "Command to run"
},
"oneshot": {
"type": "boolean",
"description": "Whether the service should be restarted"
},
"after": {
"type": "array",
"items": {
"type": "string"
},
"description": "Services that must be running before this one starts"
},
"log": {
"type": "string",
"enum": ["null", "ring", "stdout"],
"description": "How to handle service output"
},
"env": {
"type": "object",
"additionalProperties": {
"type": "string"
},
"description": "Environment variables for the service"
},
"shutdown_timeout": {
"type": "integer",
"description": "Maximum time to wait for service to stop during shutdown"
}
}
}
}
],
"result": {
"name": "CreateServiceResult",
"description": "Result of the create operation",
"schema": {
"type": "string"
}
},
"errors": [
{
"code": -32007,
"message": "Service already exists",
"data": "Service 'name' already exists"
},
{
"code": -32008,
"message": "Service file error",
"data": "Failed to create service file"
}
]
},
{
"name": "service_delete",
"description": "Deletes a service configuration file",
"params": [
{
"name": "name",
"description": "The name of the service to delete",
"required": true,
"schema": {
"type": "string"
}
}
],
"result": {
"name": "DeleteServiceResult",
"description": "Result of the delete operation",
"schema": {
"type": "string"
}
},
"errors": [
{
"code": -32000,
"message": "Service not found",
"data": "Service 'name' not found"
},
{
"code": -32008,
"message": "Service file error",
"data": "Failed to delete service file"
}
]
},
{
"name": "service_get",
"description": "Gets a service configuration file",
"params": [
{
"name": "name",
"description": "The name of the service to get",
"required": true,
"schema": {
"type": "string"
}
}
],
"result": {
"name": "GetServiceResult",
"description": "The service configuration",
"schema": {
"type": "object"
}
},
"errors": [
{
"code": -32000,
"message": "Service not found",
"data": "Service 'name' not found"
},
{
"code": -32008,
"message": "Service file error",
"data": "Failed to read service file"
}
]
},
{
"name": "service_stats",
"description": "Get memory and CPU usage statistics for a service",
"params": [
{
"name": "name",
"description": "The name of the service to get stats for",
"required": true,
"schema": {
"type": "string"
}
}
],
"result": {
"name": "ServiceStats",
"description": "Memory and CPU usage statistics for the service",
"schema": {
"type": "object",
"properties": {
"name": {
"type": "string",
"description": "Service name"
},
"pid": {
"type": "integer",
"description": "Process ID of the service"
},
"memory_usage": {
"type": "integer",
"description": "Memory usage in bytes"
},
"cpu_usage": {
"type": "number",
"description": "CPU usage as a percentage (0-100)"
},
"children": {
"type": "array",
"description": "Stats for child processes",
"items": {
"type": "object",
"properties": {
"pid": {
"type": "integer",
"description": "Process ID of the child process"
},
"memory_usage": {
"type": "integer",
"description": "Memory usage in bytes"
},
"cpu_usage": {
"type": "number",
"description": "CPU usage as a percentage (0-100)"
}
}
}
}
}
}
},
"examples": [
{
"name": "Get stats for redis service",
"params": [
{
"name": "name",
"value": "redis"
}
],
"result": {
"name": "ServiceStatsResult",
"value": {
"name": "redis",
"pid": 1234,
"memory_usage": 10485760,
"cpu_usage": 2.5,
"children": [
{
"pid": 1235,
"memory_usage": 5242880,
"cpu_usage": 1.2
}
]
}
}
}
],
"errors": [
{
"code": -32000,
"message": "Service not found",
"data": "service name \"unknown\" unknown"
},
{
"code": -32003,
"message": "Service is down",
"data": "service \"redis\" is down"
}
]
},
{
"name": "system_start_http_server",
"description": "Start an HTTP/RPC server at the specified address",
"params": [
{
"name": "address",
"description": "The network address to bind the server to (e.g., '127.0.0.1:8080')",
"required": true,
"schema": {
"type": "string"
}
}
],
"result": {
"name": "StartHttpServerResult",
"description": "Result of the start HTTP server operation",
"schema": {
"type": "string"
}
},
"examples": [
{
"name": "Start HTTP server on localhost:8080",
"params": [
{
"name": "address",
"value": "127.0.0.1:8080"
}
],
"result": {
"name": "StartHttpServerResult",
"value": "HTTP server started at 127.0.0.1:8080"
}
}
],
"errors": [
{
"code": -32602,
"message": "Invalid address",
"data": "Invalid network address format"
}
]
},
{
"name": "system_stop_http_server",
"description": "Stop the HTTP/RPC server if running",
"params": [],
"result": {
"name": "StopHttpServerResult",
"description": "Result of the stop HTTP server operation",
"schema": {
"type": "null"
}
},
"examples": [
{
"name": "Stop the HTTP server",
"params": [],
"result": {
"name": "StopHttpServerResult",
"value": null
}
}
],
"errors": [
{
"code": -32602,
"message": "Server not running",
"data": "No HTTP server is currently running"
}
]
},
{
"name": "stream_currentLogs",
"description": "Get current logs from zinit and monitored services",
"params": [
{
"name": "name",
"description": "Optional service name filter. If provided, only logs from this service will be returned",
"required": false,
"schema": {
"type": "string"
}
}
],
"result": {
"name": "LogsResult",
"description": "Array of log strings",
"schema": {
"type": "array",
"items": {
"type": "string"
}
}
},
"examples": [
{
"name": "Get all logs",
"params": [],
"result": {
"name": "LogsResult",
"value": [
"2023-01-01T12:00:00 redis: Starting service",
"2023-01-01T12:00:01 nginx: Starting service"
]
}
},
{
"name": "Get logs for a specific service",
"params": [
{
"name": "name",
"value": "redis"
}
],
"result": {
"name": "LogsResult",
"value": [
"2023-01-01T12:00:00 redis: Starting service",
"2023-01-01T12:00:02 redis: Service started"
]
}
}
]
},
{
"name": "stream_subscribeLogs",
"description": "Subscribe to log messages generated by zinit and monitored services",
"params": [
{
"name": "name",
"description": "Optional service name filter. If provided, only logs from this service will be returned",
"required": false,
"schema": {
"type": "string"
}
}
],
"result": {
"name": "LogSubscription",
"description": "A subscription to log messages",
"schema": {
"type": "string"
}
},
"examples": [
{
"name": "Subscribe to all logs",
"params": [],
"result": {
"name": "LogSubscription",
"value": "2023-01-01T12:00:00 redis: Service started"
}
},
{
"name": "Subscribe to filtered logs",
"params": [
{
"name": "name",
"value": "redis"
}
],
"result": {
"name": "LogSubscription",
"value": "2023-01-01T12:00:00 redis: Service started"
}
}
]
}
]
}

View File

@ -1,161 +0,0 @@
module zinit
import freeflowuniverse.herolib.schemas.jsonrpc
import net.unix
import time
import json
// UnixSocketTransport implements the jsonrpc.IRPCTransportClient interface for Unix domain sockets
struct UnixSocketTransport {
mut:
socket_path string
}
// new_unix_socket_transport creates a new Unix socket transport
fn new_unix_socket_transport(socket_path string) &UnixSocketTransport {
return &UnixSocketTransport{
socket_path: socket_path
}
}
// send implements the jsonrpc.IRPCTransportClient interface
fn (mut t UnixSocketTransport) send(request string, params jsonrpc.SendParams) !string {
// Create a Unix domain socket client
mut socket := unix.connect_stream(t.socket_path)!
defer { socket.close() or {} }
// Set timeout if specified
if params.timeout > 0 {
socket.set_read_timeout(params.timeout * time.second)
socket.set_write_timeout(params.timeout * time.second)
}
// Send the request
socket.write_string(request + '\n')!
// Read the response
mut response := ''
mut buf := []u8{len: 4096}
for {
bytes_read := socket.read(mut buf)!
if bytes_read <= 0 {
break
}
response += buf[..bytes_read].bytestr()
// Check if we've received a complete JSON response
if response.ends_with('}') {
break
}
}
return response
}
// Client provides a client interface to the zinit JSON-RPC API over Unix socket
@[heap]
pub struct Client {
mut:
socket_path string
rpc_client &jsonrpc.Client
request_id int
}
// new_client creates a new zinit client instance
// socket_path: path to the Unix socket (default: /tmp/zinit.sock)
pub fn new_client(socket_path string) &Client {
mut transport := new_unix_socket_transport(socket_path)
mut rpc_client := jsonrpc.new_client(transport)
return &Client{
socket_path: socket_path
rpc_client: rpc_client
request_id: 0
}
}
// new_default_client creates a new zinit client with default socket path
pub fn new_default_client() &Client {
return new_client('/tmp/zinit.sock')
}
// rpc_discover returns the OpenRPC specification for the API
pub fn (mut c Client) rpc_discover() !map[string]json.Any {
send_params := jsonrpc.SendParams{
timeout: 30
retry: 1
}
request := jsonrpc.new_request_generic('rpc_discover', []string{})
result := c.rpc_client.send[[]string, map[string]json.Any](request, send_params)!
return result
}
// service_list lists all services managed by Zinit
// Returns a map of service names to their current states
pub fn (mut c Client) service_list() !map[string]string {
send_params := jsonrpc.SendParams{
timeout: 30
retry: 1
}
request := jsonrpc.new_request_generic('service_list', []string{})
result := c.rpc_client.send[[]string, map[string]string](request, send_params)!
return result
}
// service_status shows detailed status information for a specific service
// name: the name of the service
pub fn (mut c Client) service_status(name string) !ServiceStatus {
send_params := jsonrpc.SendParams{
timeout: 30
retry: 1
}
request := jsonrpc.new_request_generic('service_status', [name])
result_map := c.rpc_client.send[[string], map[string]json.Any](request, send_params)!
mut after_map := map[string]string{}
if after_raw := result_map['after'] {
if after_raw is map[string]json.Any {
for key, value in after_raw {
after_map[key] = value.str()
}
}
}
return ServiceStatus{
name: result_map['name'].str()
pid: result_map['pid'].int()
state: result_map['state'].str()
target: result_map['target'].str()
after: after_map
}
}
// service_start starts a service
// name: the name of the service to start
pub fn (mut c Client) service_start(name string) ! {
send_params := jsonrpc.SendParams{
timeout: 30
retry: 1
}
request := jsonrpc.new_request_generic('service_start', [name])
c.rpc_client.send[[string], json.Any](request, send_params)!
}
// service_stop stops a service
// name: the name of the service to stop
pub fn (mut c Client) service_stop(name string) ! {
send_params := jsonrpc.SendParams{
timeout: 30
retry: 1
}
request := jsonrpc.new_request_generic('service_stop', [name])
c.rpc_client.send[[string], json.Any](request, send_params)!
}

View File

@ -1,204 +0,0 @@
module zinit
import time
// Basic tests for the zinit client module
// Note: These tests require a running zinit instance at /tmp/zinit.sock
// test_client_creation tests basic client creation
fn test_client_creation() {
// Test default client creation
client1 := new_default_client()
assert client1.socket_path == '/tmp/zinit.sock'
// Test custom client creation
client2 := new_client('/custom/path/zinit.sock')
assert client2.socket_path == '/custom/path/zinit.sock'
println(' Client creation tests passed')
}
// test_service_config tests ServiceConfig struct
fn test_service_config() {
config := ServiceConfig{
exec: '/usr/bin/test'
oneshot: true
after: ['dependency1', 'dependency2']
log: 'stdout'
env: {
'TEST_VAR': 'test_value'
'PATH': '/usr/bin:/bin'
}
shutdown_timeout: 30
}
assert config.exec == '/usr/bin/test'
assert config.oneshot == true
assert config.after.len == 2
assert config.after[0] == 'dependency1'
assert config.env['TEST_VAR'] == 'test_value'
assert config.shutdown_timeout == 30
println(' ServiceConfig tests passed')
}
// test_service_status tests ServiceStatus struct
fn test_service_status() {
mut after_map := map[string]string{}
after_map['dep1'] = 'Running'
after_map['dep2'] = 'Success'
status := ServiceStatus{
name: 'test_service'
pid: 1234
state: 'Running'
target: 'Up'
after: after_map
}
assert status.name == 'test_service'
assert status.pid == 1234
assert status.state == 'Running'
assert status.target == 'Up'
assert status.after['dep1'] == 'Running'
println(' ServiceStatus tests passed')
}
// test_service_stats tests ServiceStats and ChildStats structs
fn test_service_stats() {
child1 := ChildStats{
pid: 1235
memory_usage: 1024 * 1024 // 1MB
cpu_usage: 2.5
}
child2 := ChildStats{
pid: 1236
memory_usage: 2 * 1024 * 1024 // 2MB
cpu_usage: 1.2
}
stats := ServiceStats{
name: 'test_service'
pid: 1234
memory_usage: 10 * 1024 * 1024 // 10MB
cpu_usage: 5.0
children: [child1, child2]
}
assert stats.name == 'test_service'
assert stats.pid == 1234
assert stats.memory_usage == 10 * 1024 * 1024
assert stats.cpu_usage == 5.0
assert stats.children.len == 2
assert stats.children[0].pid == 1235
assert stats.children[1].memory_usage == 2 * 1024 * 1024
println(' ServiceStats tests passed')
}
// test_zinit_error tests ZinitError struct and error message
fn test_zinit_error() {
error := ZinitError{
code: -32000
message: 'Service not found'
data: 'service name "unknown" unknown'
}
assert error.code == -32000
assert error.message == 'Service not found'
assert error.data == 'service name "unknown" unknown'
error_msg := error.msg()
assert error_msg.contains('Zinit Error -32000')
assert error_msg.contains('Service not found')
assert error_msg.contains('service name "unknown" unknown')
println(' ZinitError tests passed')
}
// test_connection_handling tests connection management (without actual connection)
fn test_connection_handling() {
mut client := new_default_client()
// Initially no connection
assert client.conn == none
// Test disconnect on non-connected client (should not panic)
client.disconnect()
assert client.conn == none
println(' Connection handling tests passed')
}
// integration_test_basic performs basic integration tests if zinit is available
fn integration_test_basic() {
mut client := new_default_client()
defer {
client.disconnect()
}
println('Running integration tests (requires running zinit)...')
// Test RPC discovery
spec := client.rpc_discover() or {
println(' Integration test skipped: zinit not available (${err})')
return
}
println(' RPC discovery successful')
// Test service list
services := client.service_list() or {
println(' Service list failed: ${err}')
return
}
println(' Service list successful (${services.len} services)')
// If there are services, test getting status of the first one
if services.len > 0 {
service_name := services.keys()[0]
status := client.service_status(service_name) or {
println(' Could not get status for ${service_name}: ${err}')
return
}
println(' Service status for ${service_name}: ${status.state}')
}
// Test getting current logs
logs := client.stream_current_logs(none) or {
println(' Could not get logs: ${err}')
return
}
println(' Log streaming successful (${logs.len} log entries)')
println(' All integration tests passed')
}
// run_all_tests runs all test functions
pub fn run_all_tests() {
println('Running Zinit Client Tests...\n')
// Unit tests
test_client_creation()
test_service_config()
test_service_status()
test_service_stats()
test_zinit_error()
test_connection_handling()
println('\n--- Unit Tests Complete ---\n')
// Integration tests (optional, requires running zinit)
integration_test_basic()
println('\n--- All Tests Complete ---')
}
// main function for running tests directly
fn main() {
run_all_tests()
}

View File

@ -1,53 +0,0 @@
module zinit
// ServiceConfig represents the configuration for a zinit service
pub struct ServiceConfig {
pub mut:
exec string // Command to run
oneshot bool // Whether the service should be restarted
after []string // Services that must be running before this one starts
log string // How to handle service output (null, ring, stdout)
env map[string]string // Environment variables for the service
shutdown_timeout int // Maximum time to wait for service to stop during shutdown
}
// ServiceStatus represents the detailed status information for a service
pub struct ServiceStatus {
pub mut:
name string // Service name
pid int // Process ID of the running service (if running)
state string // Current state of the service (Running, Success, Error, etc.)
target string // Target state of the service (Up, Down)
after map[string]string // Dependencies of the service and their states
}
// ServiceStats represents memory and CPU usage statistics for a service
pub struct ServiceStats {
pub mut:
name string // Service name
pid int // Process ID of the service
memory_usage i64 // Memory usage in bytes
cpu_usage f64 // CPU usage as a percentage (0-100)
children []ChildStats // Stats for child processes
}
// ChildStats represents statistics for a child process
pub struct ChildStats {
pub mut:
pid int // Process ID of the child process
memory_usage i64 // Memory usage in bytes
cpu_usage f64 // CPU usage as a percentage (0-100)
}
// ZinitError represents an error returned by the zinit API
pub struct ZinitError {
pub mut:
code int // Error code
message string // Error message
data string // Additional error data
}
// Error implements the error interface for ZinitError
pub fn (e ZinitError) msg() string {
return 'Zinit Error ${e.code}: ${e.message} - ${e.data}'
}

View File

@ -1,145 +0,0 @@
module zinit
// Zinit Client Module
//
// This module provides a comprehensive V (Vlang) client library for interacting
// with the Zinit service manager via JSON-RPC over Unix socket.
//
// The module includes:
// - Complete type definitions for all API structures
// - Full client implementation with all OpenRPC methods
// - Comprehensive error handling
// - Connection management
// - Well-documented API with examples
//
// Usage:
// import zinit
//
// mut client := zinit.new_default_client()
// defer { client.disconnect() }
//
// services := client.service_list()!
// for name, state in services {
// println('${name}: ${state}')
// }
//
// For detailed documentation, see README.md
// For usage examples, see example.v
// For tests, see test.v
// Module version information
pub const (
version = '1.0.0'
author = 'Hero Code'
license = 'MIT'
)
// Default socket path for zinit
pub const default_socket_path = '/tmp/zinit.sock'
// Common service states
pub const (
state_running = 'Running'
state_success = 'Success'
state_error = 'Error'
state_stopped = 'Stopped'
state_failed = 'Failed'
)
// Common service targets
pub const (
target_up = 'Up'
target_down = 'Down'
)
// Common log types
pub const (
log_null = 'null'
log_ring = 'ring'
log_stdout = 'stdout'
)
// Common signals
pub const (
signal_term = 'SIGTERM'
signal_kill = 'SIGKILL'
signal_hup = 'SIGHUP'
signal_usr1 = 'SIGUSR1'
signal_usr2 = 'SIGUSR2'
)
// JSON-RPC error codes as defined in the OpenRPC specification
pub const (
error_service_not_found = -32000
error_service_already_monitored = -32001
error_service_is_up = -32002
error_service_is_down = -32003
error_invalid_signal = -32004
error_config_error = -32005
error_shutting_down = -32006
error_service_already_exists = -32007
error_service_file_error = -32008
)
// Helper function to create a basic service configuration
pub fn new_service_config(exec string) ServiceConfig {
return ServiceConfig{
exec: exec
oneshot: false
log: log_stdout
env: map[string]string{}
shutdown_timeout: 30
}
}
// Helper function to create a oneshot service configuration
pub fn new_oneshot_service_config(exec string) ServiceConfig {
return ServiceConfig{
exec: exec
oneshot: true
log: log_stdout
env: map[string]string{}
shutdown_timeout: 30
}
}
// Helper function to check if an error is a specific zinit error code
pub fn is_zinit_error_code(err IError, code int) bool {
if zinit_err := err as ZinitError {
return zinit_err.code == code
}
return false
}
// Helper function to check if service is not found error
pub fn is_service_not_found_error(err IError) bool {
return is_zinit_error_code(err, error_service_not_found)
}
// Helper function to check if service is already monitored error
pub fn is_service_already_monitored_error(err IError) bool {
return is_zinit_error_code(err, error_service_already_monitored)
}
// Helper function to check if service is down error
pub fn is_service_down_error(err IError) bool {
return is_zinit_error_code(err, error_service_is_down)
}
// Helper function to format memory usage in human-readable format
pub fn format_memory_usage(bytes i64) string {
if bytes < 1024 {
return '${bytes} B'
} else if bytes < 1024 * 1024 {
return '${bytes / 1024} KB'
} else if bytes < 1024 * 1024 * 1024 {
return '${bytes / 1024 / 1024} MB'
} else {
return '${bytes / 1024 / 1024 / 1024} GB'
}
}
// Helper function to format CPU usage
pub fn format_cpu_usage(cpu_percent f64) string {
return '${cpu_percent:.1f}%'
}