This commit is contained in:
2025-04-23 04:18:28 +02:00
parent 10a7d9bb6b
commit a16ac8f627
276 changed files with 85166 additions and 1 deletions

View File

@@ -0,0 +1,85 @@
# Process Manager OpenRPC Example
This example demonstrates how to use the Process Manager OpenRPC interface to interact with the process manager. It provides a complete working example of both server and client implementations, with a mock process manager for testing purposes.
## Overview
The example includes:
1. A server that exposes the process manager functionality via OpenRPC over a Unix socket
2. A client that connects to the server and performs various operations
3. A mock process manager implementation for testing without requiring actual processes
## Structure
- `main.go`: The main entry point that runs both the server and client in the same process
- `mock_processmanager.go`: A mock implementation of the `ProcessManagerInterface` that simulates process operations
- `example_client.go`: Client implementation demonstrating how to use the OpenRPC client to interact with the process manager
## How to Run
```bash
# From the heroagent root directory
go run ./pkg/processmanager/examples/openrpc
```
The example will:
1. Start a server using a Unix socket at `/tmp/process-manager.sock`
2. Run a series of client operations against the server
3. Display the results of each operation
4. Clean up and shut down when complete
## What It Demonstrates
This example shows:
1. How to initialize and start an OpenRPC server for the process manager
2. How to create and use an OpenRPC client to interact with the process manager
3. How to perform common operations like:
- Starting a process (`StartProcess`)
- Getting process status (`GetProcessStatus`)
- Listing all processes (`ListProcesses`)
- Getting process logs (`GetProcessLogs`)
- Restarting a process (`RestartProcess`)
- Stopping a process (`StopProcess`)
- Deleting a process (`DeleteProcess`)
4. How to handle the response types returned by each operation
5. Proper error handling for RPC operations
## Notes
- This example uses a mock process manager implementation for demonstration purposes
- In a real application, you would use the actual process manager implementation
- The server and client run in the same process for simplicity, but they could be in separate processes
- The Unix socket communication can be replaced with other transport mechanisms if needed
## Implementation Details
### Server
The server is implemented using the `openrpc.Server` type, which wraps the OpenRPC manager and Unix socket server. It:
1. Creates a Unix socket at the specified path
2. Registers handlers for each RPC method
3. Authenticates requests using a secret
4. Marshals/unmarshals JSON-RPC requests and responses
### Client
The client is implemented using the `openrpc.Client` type, which provides methods for each operation. It:
1. Connects to the Unix socket
2. Sends JSON-RPC requests with the appropriate parameters
3. Handles authentication using the secret
4. Parses the responses into appropriate result types
### Mock Process Manager
The mock process manager implements the `interfaces.ProcessManagerInterface` and simulates:
1. Process creation and management
2. Status tracking
3. Log collection
4. Error handling
This allows testing the OpenRPC interface without requiring actual processes to be run.

View File

@@ -0,0 +1,149 @@
package main
import (
"fmt"
"log"
"time"
"github.com/freeflowuniverse/heroagent/pkg/processmanager/interfaces"
"github.com/freeflowuniverse/heroagent/pkg/processmanager/interfaces/openrpc"
)
// RunClientExample runs a complete example of using the process manager OpenRPC client
func RunClientExample(socketPath, secret string) error {
// Create a new client
client := openrpc.NewClient(socketPath, secret)
log.Println("🚀 Starting example process...")
// Start a process
result, err := client.StartProcess("example-process", "sleep 60", true, 0, "", "")
if err != nil {
return fmt.Errorf("failed to start process: %w", err)
}
log.Printf("Start result: success=%v, message=%s, PID=%d", result.Success, result.Message, result.PID)
// Wait a bit for the process to start
time.Sleep(500 * time.Millisecond)
log.Println("📊 Getting process status...")
// Get the process status
status, err := client.GetProcessStatus("example-process", "json")
if err != nil {
return fmt.Errorf("failed to get process status: %w", err)
}
printProcessStatus(status.(interfaces.ProcessStatus))
log.Println("📋 Listing all processes...")
// List all processes
processList, err := client.ListProcesses("json")
if err != nil {
return fmt.Errorf("failed to list processes: %w", err)
}
// For simplicity in this example, just log that we got a response
log.Printf("Got process list response: %T", processList)
// Try to handle the response in a more robust way
switch v := processList.(type) {
case []interface{}:
log.Printf("Found %d processes", len(v))
for i, p := range v {
log.Printf("Process %d: %T", i, p)
if processMap, ok := p.(map[string]interface{}); ok {
log.Printf(" Name: %v", processMap["name"])
log.Printf(" Command: %v", processMap["command"])
log.Printf(" Status: %v", processMap["status"])
}
}
case map[string]interface{}:
log.Printf("Process list is a map with %d entries", len(v))
for k, val := range v {
log.Printf(" %s: %T", k, val)
}
default:
log.Printf("Process list is of unexpected type: %T", processList)
}
log.Println("📜 Getting process logs...")
// Get process logs
logResult, err := client.GetProcessLogs("example-process", 10)
if err != nil {
return fmt.Errorf("failed to get process logs: %w", err)
}
log.Printf("Process logs: success=%v, message=%s", logResult.Success, logResult.Message)
log.Printf("Logs:\n%s", logResult.Logs)
log.Println("🔄 Restarting process...")
// Restart the process
restartResult, err := client.RestartProcess("example-process")
if err != nil {
return fmt.Errorf("failed to restart process: %w", err)
}
log.Printf("Restart result: success=%v, message=%s, PID=%d", restartResult.Success, restartResult.Message, restartResult.PID)
// Wait a bit for the process to restart
time.Sleep(500 * time.Millisecond)
// Get the process status after restart
status, err = client.GetProcessStatus("example-process", "json")
if err != nil {
return fmt.Errorf("failed to get process status after restart: %w", err)
}
log.Println("Process status after restart:")
printProcessStatus(status.(interfaces.ProcessStatus))
log.Println("⏹️ Stopping process...")
// Stop the process
stopResult, err := client.StopProcess("example-process")
if err != nil {
return fmt.Errorf("failed to stop process: %w", err)
}
log.Printf("Stop result: success=%v, message=%s", stopResult.Success, stopResult.Message)
// Wait a bit for the process to stop
time.Sleep(500 * time.Millisecond)
// Get the process status after stop
status, err = client.GetProcessStatus("example-process", "json")
if err != nil {
return fmt.Errorf("failed to get process status after stop: %w", err)
}
log.Println("Process status after stop:")
printProcessStatus(status.(interfaces.ProcessStatus))
log.Println("🗑️ Deleting process...")
// Delete the process
deleteResult, err := client.DeleteProcess("example-process")
if err != nil {
return fmt.Errorf("failed to delete process: %w", err)
}
log.Printf("Delete result: success=%v, message=%s", deleteResult.Success, deleteResult.Message)
// Try to get the process status after delete (should fail)
_, err = client.GetProcessStatus("example-process", "json")
if err != nil {
log.Printf("Expected error after deletion: %v", err)
} else {
return fmt.Errorf("process still exists after deletion")
}
log.Println("✅ Example completed successfully!")
return nil
}
// printProcessStatus prints the status of a process
func printProcessStatus(status interfaces.ProcessStatus) {
log.Printf("Process: %s", status.Name)
log.Printf(" Command: %s", status.Command)
log.Printf(" Status: %s", status.Status)
log.Printf(" PID: %d", status.PID)
if status.CPUPercent > 0 {
log.Printf(" CPU: %.2f%%", status.CPUPercent)
}
if status.MemoryMB > 0 {
log.Printf(" Memory: %.2f MB", status.MemoryMB)
}
if !status.StartTime.IsZero() {
log.Printf(" Started: %s", status.StartTime.Format(time.RFC3339))
}
}

View File

@@ -0,0 +1,75 @@
package main
import (
"log"
"os"
"os/signal"
"syscall"
"time"
"github.com/freeflowuniverse/heroagent/pkg/processmanager/interfaces/openrpc"
)
func main() {
log.Println("Starting Process Manager OpenRPC Example")
// Use /tmp directory for the socket as it's more reliable for Unix sockets
// Define the socket path
socketPath := "/tmp/process-manager.sock"
// Remove the socket file if it already exists
if _, err := os.Stat(socketPath); err == nil {
if err := os.Remove(socketPath); err != nil {
log.Fatalf("Failed to remove existing socket file: %v", err)
}
}
log.Printf("Using socket path: %s", socketPath)
// Create a mock process manager
mockPM := NewMockProcessManager()
// Create and start the server
server, err := openrpc.NewServer(mockPM, socketPath)
if err != nil {
log.Fatalf("Failed to create server: %v", err)
}
// Start the server
err = server.Start()
if err != nil {
log.Fatalf("Failed to start server: %v", err)
}
log.Println("Server started successfully")
// Set up signal handling for graceful shutdown
sigChan := make(chan os.Signal, 1)
signal.Notify(sigChan, syscall.SIGINT, syscall.SIGTERM)
// Wait a bit for the server to start
time.Sleep(1 * time.Second)
// Run the client example in a goroutine
errChan := make(chan error, 1)
go func() {
err := RunClientExample(socketPath, mockPM.GetSecret())
errChan <- err
}()
// Wait for the client to finish or for a signal
select {
case err := <-errChan:
if err != nil {
log.Printf("Client example failed: %v", err)
}
case sig := <-sigChan:
log.Printf("Received signal: %v", sig)
}
// Stop the server
log.Println("Stopping server...")
err = server.Stop()
if err != nil {
log.Printf("Failed to stop server: %v", err)
}
log.Println("Example completed")
}

View File

@@ -0,0 +1,195 @@
package main
import (
"fmt"
"strings"
"sync"
"time"
"github.com/freeflowuniverse/heroagent/pkg/processmanager"
"github.com/freeflowuniverse/heroagent/pkg/processmanager/interfaces"
)
// MockProcessManager implements the interfaces.ProcessManagerInterface for testing purposes
type MockProcessManager struct {
processes map[string]*processmanager.ProcessInfo
logs map[string][]string
mutex sync.RWMutex
secret string
}
// Ensure MockProcessManager implements interfaces.ProcessManagerInterface
var _ interfaces.ProcessManagerInterface = (*MockProcessManager)(nil)
// NewMockProcessManager creates a new mock process manager
func NewMockProcessManager() *MockProcessManager {
return &MockProcessManager{
processes: make(map[string]*processmanager.ProcessInfo),
logs: make(map[string][]string),
secret: "mock-secret",
}
}
// StartProcess starts a new process
func (m *MockProcessManager) StartProcess(name, command string, logEnabled bool, deadline int, cron, jobID string) error {
m.mutex.Lock()
defer m.mutex.Unlock()
if _, exists := m.processes[name]; exists {
return fmt.Errorf("process %s already exists", name)
}
process := &processmanager.ProcessInfo{
Name: name,
Command: command,
PID: 12345, // Mock PID
Status: processmanager.ProcessStatusRunning,
CPUPercent: 0.5,
MemoryMB: 10.0,
StartTime: time.Now(),
LogEnabled: logEnabled,
Cron: cron,
JobID: jobID,
Deadline: deadline,
}
m.processes[name] = process
m.logs[name] = []string{
fmt.Sprintf("[%s] Process started: %s", time.Now().Format(time.RFC3339), command),
}
return nil
}
// StopProcess stops a running process
func (m *MockProcessManager) StopProcess(name string) error {
m.mutex.Lock()
defer m.mutex.Unlock()
process, exists := m.processes[name]
if !exists {
return fmt.Errorf("process %s does not exist", name)
}
if process.Status != processmanager.ProcessStatusRunning {
return fmt.Errorf("process %s is not running", name)
}
process.Status = processmanager.ProcessStatusStopped
m.logs[name] = append(m.logs[name], fmt.Sprintf("[%s] Process stopped", time.Now().Format(time.RFC3339)))
return nil
}
// RestartProcess restarts a process
func (m *MockProcessManager) RestartProcess(name string) error {
m.mutex.Lock()
defer m.mutex.Unlock()
process, exists := m.processes[name]
if !exists {
return fmt.Errorf("process %s does not exist", name)
}
process.Status = processmanager.ProcessStatusRunning
process.StartTime = time.Now()
m.logs[name] = append(m.logs[name], fmt.Sprintf("[%s] Process restarted", time.Now().Format(time.RFC3339)))
return nil
}
// DeleteProcess deletes a process
func (m *MockProcessManager) DeleteProcess(name string) error {
m.mutex.Lock()
defer m.mutex.Unlock()
if _, exists := m.processes[name]; !exists {
return fmt.Errorf("process %s does not exist", name)
}
delete(m.processes, name)
delete(m.logs, name)
return nil
}
// GetProcessStatus gets the status of a process
func (m *MockProcessManager) GetProcessStatus(name string) (*processmanager.ProcessInfo, error) {
m.mutex.RLock()
defer m.mutex.RUnlock()
process, exists := m.processes[name]
if !exists {
return nil, fmt.Errorf("process %s does not exist", name)
}
// Return a copy to avoid race conditions
return &processmanager.ProcessInfo{
Name: process.Name,
Command: process.Command,
PID: process.PID,
Status: process.Status,
CPUPercent: process.CPUPercent,
MemoryMB: process.MemoryMB,
StartTime: process.StartTime,
LogEnabled: process.LogEnabled,
Cron: process.Cron,
JobID: process.JobID,
Deadline: process.Deadline,
}, nil
}
// ListProcesses lists all processes
func (m *MockProcessManager) ListProcesses() []*processmanager.ProcessInfo {
m.mutex.RLock()
defer m.mutex.RUnlock()
processes := make([]*processmanager.ProcessInfo, 0, len(m.processes))
for _, process := range m.processes {
// Create a copy to avoid race conditions
processes = append(processes, &processmanager.ProcessInfo{
Name: process.Name,
Command: process.Command,
PID: process.PID,
Status: process.Status,
CPUPercent: process.CPUPercent,
MemoryMB: process.MemoryMB,
StartTime: process.StartTime,
LogEnabled: process.LogEnabled,
Cron: process.Cron,
JobID: process.JobID,
Deadline: process.Deadline,
})
}
return processes
}
// GetProcessLogs gets the logs for a process
func (m *MockProcessManager) GetProcessLogs(name string, maxLines int) (string, error) {
m.mutex.RLock()
defer m.mutex.RUnlock()
logs, exists := m.logs[name]
if !exists {
return "", fmt.Errorf("logs for process %s do not exist", name)
}
if maxLines <= 0 || maxLines > len(logs) {
return strings.Join(logs, "\n"), nil
}
return strings.Join(logs[len(logs)-maxLines:], "\n"), nil
}
// SetLogsBasePath sets the base path for logs (mock implementation does nothing)
func (m *MockProcessManager) SetLogsBasePath(path string) {
// No-op for mock
}
// GetSecret returns the authentication secret
func (m *MockProcessManager) GetSecret() string {
return m.secret
}