This commit is contained in:
2025-05-23 16:10:49 +04:00
parent 3f01074e3f
commit 29d0d25a3b
133 changed files with 346 additions and 168 deletions

View File

@@ -0,0 +1,143 @@
# Handler Factory Module
The Handler Factory module provides a framework for creating and managing handlers that process HeroScript commands through a telnet interface. It allows for both Unix socket and TCP connections, making it flexible for different use cases.
## Overview
The Handler Factory module consists of several components:
1. **HandlerFactory**: Core component that manages a collection of handlers, each responsible for processing specific actor commands.
2. **TelnetServer**: Provides a telnet interface for interacting with the handlers, supporting both Unix socket and TCP connections.
3. **HeroHandler**: Main handler that initializes and manages the HandlerFactory and TelnetServer.
4. **ProcessManagerHandler**: Example handler implementation that manages processes through HeroScript commands.
## Architecture
The module follows a plugin-based architecture where:
- The `HandlerFactory` maintains a registry of handlers
- Each handler implements the `Handler` interface and is responsible for a specific actor
- The `TelnetServer` provides a communication interface to send HeroScript commands
- HeroScript commands are parsed and routed to the appropriate handler based on the actor name
## Connecting to the Handler Factory
The Handler Factory exposes two interfaces for communication:
1. Unix Socket (default: `/tmp/hero.sock`)
2. TCP Port (default: `localhost:8023`)
to get started
```bash
cd /root/code/github/freeflowuniverse/herocode/heroagent/pkg/handlerfactory/herohandler/cmd
go run .
```
### Using Telnet to Connect
You can use the standard telnet client to connect to the TCP port:
```bash
# Connect to the default TCP port
telnet localhost 8023
# Once connected, you can send HeroScript commands
# For example:
!!process.list
```
### Using Netcat to Connect
Netcat (nc) can be used to connect to both the Unix socket and TCP port:
#### Connecting to TCP Port
```bash
# Connect to the TCP port
nc localhost 8023
# Send HeroScript commands
!!process.list
```
#### Connecting to Unix Socket
```bash
# Connect to the Unix socket
nc -U /tmp/hero.sock
# Send HeroScript commands
!!process.list
```
## HeroScript Command Format
Commands follow the HeroScript format:
```
!!actor.action param1:"value1" param2:"value2"
```
For example:
```
!!process.start name:"web_server" command:"python -m http.server 8080" log:true
!!process.status name:"web_server"
!!process.stop name:"web_server"
```
## Available Commands
The Handler Factory comes with a built-in ProcessManagerHandler that supports the following commands:
- `!!process.start` - Start a new process
- `!!process.stop` - Stop a running process
- `!!process.restart` - Restart a process
- `!!process.delete` - Delete a process
- `!!process.list` - List all processes
- `!!process.status` - Get status of a specific process
- `!!process.logs` - Get logs of a specific process
- `!!process.help` - Show help information
You can get help on available commands by typing `!!help` in the telnet/netcat session.
## Authentication
The telnet server supports optional authentication with secrets. If secrets are provided when starting the server, clients will need to authenticate using one of these secrets before they can execute commands.
## Extending with Custom Handlers
You can extend the functionality by implementing your own handlers:
1. Create a new handler that implements the `Handler` interface
2. Register the handler with the HandlerFactory
3. Access the handler's functionality through the telnet interface
## Example Usage
Here's a complete example of connecting and using the telnet interface:
```bash
# Connect to the telnet server
nc localhost 8023
# List all processes
!!process.list
# Start a new process
!!process.start name:"web_server" command:"python -m http.server 8080"
# Check the status of the process
!!process.status name:"web_server"
# View the logs
!!process.logs name:"web_server" lines:50
# Stop the process
!!process.stop name:"web_server"
```
## Implementation Details
The Handler Factory module is implemented in pure Go and follows the Go project structure conventions. It uses standard Go libraries for networking and does not have external dependencies for its core functionality.

View File

@@ -0,0 +1,95 @@
package core
import (
"fmt"
"reflect"
"strings"
"git.ourworld.tf/herocode/heroagent/pkg/heroscript/paramsparser"
"git.ourworld.tf/herocode/heroagent/pkg/heroscript/playbook"
)
// Handler interface defines methods that all handlers must implement
type Handler interface {
GetActorName() string
Play(script string, handler interface{}) (string, error)
}
// BaseHandler provides common functionality for all handlers
type BaseHandler struct {
ActorName string
}
// GetActorName returns the actor name for this handler
func (h *BaseHandler) GetActorName() string {
return h.ActorName
}
// Play processes all actions for this handler's actor
func (h *BaseHandler) Play(script string, handler interface{}) (string, error) {
pb, err := playbook.NewFromText(script)
if err != nil {
return "", fmt.Errorf("failed to parse heroscript: %v", err)
}
// Find all actions for this actor
actions, err := pb.FindActions(0, h.ActorName, "", playbook.ActionTypeUnknown)
if err != nil {
return "", fmt.Errorf("failed to find actions: %v", err)
}
if len(actions) == 0 {
return "", fmt.Errorf("no actions found for actor: %s", h.ActorName)
}
var results []string
// Process each action
for _, action := range actions {
// Convert action name to method name (e.g., "disk_add" -> "DiskAdd")
methodName := convertToMethodName(action.Name)
// Get the method from the handler
method := reflect.ValueOf(handler).MethodByName(methodName)
if !method.IsValid() {
return "", fmt.Errorf("action not supported: %s.%s", h.ActorName, action.Name)
}
// Call the method with the action's heroscript
actionScript := action.HeroScript()
args := []reflect.Value{reflect.ValueOf(actionScript)}
result := method.Call(args)
// Get the result
if len(result) > 0 {
resultStr := result[0].String()
results = append(results, resultStr)
}
}
return strings.Join(results, "\n"), nil
}
// ParseParams parses parameters from a heroscript action
func (h *BaseHandler) ParseParams(script string) (*paramsparser.ParamsParser, error) {
pb, err := playbook.NewFromText(script)
if err != nil {
return nil, fmt.Errorf("failed to parse heroscript: %v", err)
}
// Get the first action
if len(pb.Actions) == 0 {
return nil, fmt.Errorf("no actions found in script")
}
// Get the first action
action := pb.Actions[0]
// Check if the action is for this handler
if action.Actor != h.ActorName {
return nil, fmt.Errorf("action actor '%s' does not match handler actor '%s'", action.Actor, h.ActorName)
}
// The action already has a ParamsParser, so we can just return it
return action.Params, nil
}

View File

@@ -0,0 +1,145 @@
package core
import (
"fmt"
"reflect"
"strings"
"git.ourworld.tf/herocode/heroagent/pkg/heroscript/playbook"
)
// HandlerFactory manages a collection of handlers
type HandlerFactory struct {
handlers map[string]Handler
}
// NewHandlerFactory creates a new handler factory
func NewHandlerFactory() *HandlerFactory {
return &HandlerFactory{
handlers: make(map[string]Handler),
}
}
// RegisterHandler registers a handler with the factory
func (f *HandlerFactory) RegisterHandler(handler Handler) error {
actorName := handler.GetActorName()
if actorName == "" {
return fmt.Errorf("handler has no actor name")
}
if _, exists := f.handlers[actorName]; exists {
return fmt.Errorf("handler for actor '%s' already registered", actorName)
}
f.handlers[actorName] = handler
return nil
}
// GetHandler returns a handler for the specified actor
func (f *HandlerFactory) GetHandler(actorName string) (Handler, error) {
handler, exists := f.handlers[actorName]
if !exists {
return nil, fmt.Errorf("no handler registered for actor: %s", actorName)
}
return handler, nil
}
// ProcessHeroscript processes a heroscript coming from the RPC server
func (f *HandlerFactory) ProcessHeroscript(script string) (string, error) {
pb, err := playbook.NewFromText(script)
if err != nil {
return "", fmt.Errorf("failed to parse heroscript: %v", err)
}
if len(pb.Actions) == 0 {
return "", fmt.Errorf("no actions found in script")
}
// Group actions by actor
actorActions := make(map[string][]*playbook.Action)
for _, action := range pb.Actions {
actorActions[action.Actor] = append(actorActions[action.Actor], action)
}
var results []string
// Process actions for each actor
for actorName, actions := range actorActions {
handler, err := f.GetHandler(actorName)
if err != nil {
return "", err
}
// Create a playbook with just this actor's actions
actorPB := playbook.New()
for _, action := range actions {
actorAction := actorPB.NewAction(action.CID, action.Name, action.Actor, action.Priority, action.ActionType)
actorAction.Params = action.Params
}
// Process the actions
result, err := handler.Play(actorPB.HeroScript(true), handler)
if err != nil {
return "", err
}
results = append(results, result)
}
return strings.Join(results, "\n"), nil
}
// GetSupportedActions returns a map of supported actions for each registered actor
func (f *HandlerFactory) GetSupportedActions() map[string][]string {
result := make(map[string][]string)
for actorName, handler := range f.handlers {
handlerType := reflect.TypeOf(handler)
// Get all methods of the handler
var methods []string
for i := 0; i < handlerType.NumMethod(); i++ {
method := handlerType.Method(i)
// Skip methods from BaseHandler and other non-action methods
if method.Name == "GetActorName" || method.Name == "Play" || method.Name == "ParseParams" {
continue
}
// Convert method name to action name (e.g., "DiskAdd" -> "disk_add")
actionName := convertToActionName(method.Name)
methods = append(methods, actionName)
}
result[actorName] = methods
}
return result
}
// Helper functions for name conversion
// convertToMethodName converts an action name to a method name
// e.g., "disk_add" -> "DiskAdd"
func convertToMethodName(actionName string) string {
parts := strings.Split(actionName, "_")
for i, part := range parts {
if len(part) > 0 {
parts[i] = strings.ToUpper(part[0:1]) + part[1:]
}
}
return strings.Join(parts, "")
}
// convertToActionName converts a method name to an action name
// e.g., "DiskAdd" -> "disk_add"
func convertToActionName(methodName string) string {
var result strings.Builder
for i, char := range methodName {
if i > 0 && 'A' <= char && char <= 'Z' {
result.WriteRune('_')
}
result.WriteRune(char)
}
return strings.ToLower(result.String())
}

View File

@@ -0,0 +1,629 @@
package core
import (
"bufio"
"context"
"fmt"
"net"
"os"
"os/signal"
"reflect"
"strings"
"sync"
"syscall"
"git.ourworld.tf/herocode/heroagent/pkg/heroscript/playbook"
)
// ANSI color codes for terminal output
const (
ColorReset = "\033[0m"
ColorRed = "\033[31m"
ColorGreen = "\033[32m"
ColorYellow = "\033[33m"
ColorBlue = "\033[34m"
ColorPurple = "\033[35m"
ColorCyan = "\033[36m"
ColorWhite = "\033[37m"
Bold = "\033[1m"
)
// TelnetServer represents a telnet server for processing HeroScript commands
type TelnetServer struct {
factory *HandlerFactory
secrets []string
unixListener net.Listener
tcpListener net.Listener
clients map[net.Conn]bool // map of client connections to authentication status
clientsMutex sync.RWMutex
running bool
ctx context.Context
cancel context.CancelFunc
wg sync.WaitGroup
sigCh chan os.Signal
onShutdown func()
// Map to store client preferences (like json formatting)
clientPrefs map[net.Conn]map[string]bool
prefsMutex sync.RWMutex
}
// NewTelnetServer creates a new telnet server
func NewTelnetServer(factory *HandlerFactory, secrets ...string) *TelnetServer {
ctx, cancel := context.WithCancel(context.Background())
return &TelnetServer{
factory: factory,
secrets: secrets,
clients: make(map[net.Conn]bool),
clientPrefs: make(map[net.Conn]map[string]bool),
running: false,
ctx: ctx,
cancel: cancel,
sigCh: make(chan os.Signal, 1),
onShutdown: func() {},
}
}
// Start starts the telnet server on a Unix socket
func (ts *TelnetServer) Start(socketPath string) error {
// Remove existing socket file if it exists
if err := os.Remove(socketPath); err != nil {
// Ignore error if the file doesn't exist
if !os.IsNotExist(err) {
return fmt.Errorf("failed to remove existing socket: %v", err)
}
}
// Create Unix domain socket
listener, err := net.Listen("unix", socketPath)
if err != nil {
return fmt.Errorf("failed to listen on socket: %v", err)
}
ts.unixListener = listener
ts.running = true
// Accept connections in a goroutine
ts.wg.Add(1)
go ts.acceptConnections(listener)
// Setup signal handling if this is the first listener
if ts.unixListener != nil && ts.tcpListener == nil {
ts.setupSignalHandling()
}
return nil
}
// StartTCP starts the telnet server on a TCP port
func (ts *TelnetServer) StartTCP(address string) error {
// Create TCP listener
listener, err := net.Listen("tcp", address)
if err != nil {
return fmt.Errorf("failed to listen on TCP address: %v", err)
}
ts.tcpListener = listener
ts.running = true
// Accept connections in a goroutine
ts.wg.Add(1)
go ts.acceptConnections(listener)
// Setup signal handling if this is the first listener
if ts.tcpListener != nil && ts.unixListener == nil {
ts.setupSignalHandling()
}
return nil
}
// Stop stops the telnet server
func (ts *TelnetServer) Stop() error {
if !ts.running {
return nil
}
ts.running = false
// Signal all goroutines to stop
ts.cancel()
// Close the listeners
if ts.unixListener != nil {
if err := ts.unixListener.Close(); err != nil {
return fmt.Errorf("failed to close Unix listener: %v", err)
}
}
if ts.tcpListener != nil {
if err := ts.tcpListener.Close(); err != nil {
return fmt.Errorf("failed to close TCP listener: %v", err)
}
}
// Close all client connections
ts.clientsMutex.Lock()
for conn := range ts.clients {
conn.Close()
delete(ts.clients, conn)
}
ts.clientsMutex.Unlock()
// Wait for all goroutines to finish
ts.wg.Wait()
// Call the onShutdown callback if set
if ts.onShutdown != nil {
ts.onShutdown()
}
return nil
}
// acceptConnections accepts incoming connections
func (ts *TelnetServer) acceptConnections(listener net.Listener) {
defer ts.wg.Done()
for {
// Use a separate goroutine to accept connections so we can check for context cancellation
connCh := make(chan net.Conn)
errCh := make(chan error)
go func() {
conn, err := listener.Accept()
if err != nil {
errCh <- err
return
}
connCh <- conn
}()
select {
case <-ts.ctx.Done():
// Context was canceled, exit the loop
return
case conn := <-connCh:
// Handle the connection in a goroutine
ts.wg.Add(1)
go ts.handleConnection(conn)
case err := <-errCh:
if ts.running {
fmt.Printf("Failed to accept connection: %v\n", err)
} else {
// If we're not running, this is expected during shutdown
return
}
}
}
}
// handleConnection handles a client connection
func (ts *TelnetServer) handleConnection(conn net.Conn) {
defer ts.wg.Done()
// Add client to the map (not authenticated yet)
ts.clientsMutex.Lock()
ts.clients[conn] = false
ts.clientsMutex.Unlock()
// Initialize client preferences
ts.prefsMutex.Lock()
ts.clientPrefs[conn] = make(map[string]bool)
ts.prefsMutex.Unlock()
// Ensure client is removed when connection closes
defer func() {
conn.Close()
ts.clientsMutex.Lock()
delete(ts.clients, conn)
ts.clientsMutex.Unlock()
// Also remove client preferences
ts.prefsMutex.Lock()
delete(ts.clientPrefs, conn)
ts.prefsMutex.Unlock()
}()
// Welcome message
if len(ts.secrets) > 0 {
conn.Write([]byte(" ** Welcome: you are not authenticated, please authenticate with !!core.auth secret:'your_secret'\n"))
} else {
conn.Write([]byte(" ** Welcome to HeroLauncher Telnet Server\n ** Note: Press Enter twice after sending heroscript to execute\n"))
}
// Create a scanner for reading input
scanner := bufio.NewScanner(conn)
var heroscriptBuffer strings.Builder
commandHistory := []string{}
historyPos := 0
interactiveMode := true
// Process client input
for scanner.Scan() {
line := scanner.Text()
// Check for Ctrl+C (ASCII value 3)
if line == "\x03" {
conn.Write([]byte("Goodbye!\n"))
return
}
// Check for arrow up (ANSI escape sequence for up arrow: "\x1b[A")
if line == "\x1b[A" && len(commandHistory) > 0 {
if historyPos > 0 {
historyPos--
}
if historyPos < len(commandHistory) {
conn.Write([]byte(commandHistory[historyPos]))
line = commandHistory[historyPos]
}
}
// Handle quit/exit commands
if line == "!!quit" || line == "!!exit" || line == "q" {
conn.Write([]byte("Goodbye!\n"))
return
}
// Handle help command
if line == "!!help" || line == "h" || line == "?" {
helpText := ts.generateHelpText(interactiveMode)
conn.Write([]byte(helpText))
continue
}
// Handle interactive mode toggle
if line == "!!interactive" || line == "!!i" || line == "i" {
interactiveMode = !interactiveMode
if interactiveMode {
// Only use colors in terminal output, not in telnet
fmt.Println(ColorGreen + "Interactive mode enabled for client. Using colors for console output." + ColorReset)
conn.Write([]byte("Interactive mode enabled. Using formatted output.\n"))
} else {
fmt.Println("Interactive mode disabled for client. Plain text console output.")
conn.Write([]byte("Interactive mode disabled. Plain text output.\n"))
}
continue
}
// Handle JSON format toggle
if line == "!!json" {
ts.prefsMutex.Lock()
prefs, exists := ts.clientPrefs[conn]
if !exists {
prefs = make(map[string]bool)
ts.clientPrefs[conn] = prefs
}
// Toggle JSON format preference
currentSetting := prefs["json"]
prefs["json"] = !currentSetting
ts.prefsMutex.Unlock()
if prefs["json"] {
conn.Write([]byte("JSON format will be automatically added to all heroscripts.\n"))
} else {
conn.Write([]byte("JSON format will no longer be automatically added to heroscripts.\n"))
}
continue
}
// Check authentication
isAuthenticated := ts.isClientAuthenticated(conn)
// Handle authentication
if !isAuthenticated {
// Check if this is an auth command
if strings.HasPrefix(strings.TrimSpace(line), "!!core.auth") || strings.HasPrefix(strings.TrimSpace(line), "!!auth") {
pb, err := playbook.NewFromText(line)
if err != nil {
conn.Write([]byte("Authentication syntax error. Use !!core.auth secret:'your_secret'\n"))
continue
}
if len(pb.Actions) > 0 {
action := pb.Actions[0]
// Support both auth.auth and core.auth patterns
validActor := action.Actor == "auth" || action.Actor == "core"
validAction := action.Name == "auth"
if validActor && validAction {
secret := action.Params.Get("secret")
if ts.isValidSecret(secret) {
ts.clientsMutex.Lock()
ts.clients[conn] = true
ts.clientsMutex.Unlock()
conn.Write([]byte(" ** Authentication successful. You can now send commands.\n"))
continue
} else {
conn.Write([]byte("Authentication failed: Invalid secret provided.\n"))
continue
}
}
}
conn.Write([]byte("Invalid authentication format. Use !!core.auth secret:'your_secret'\n"))
} else {
conn.Write([]byte("You must authenticate first. Use !!core.auth secret:'your_secret'\n"))
}
continue
}
// Empty line executes pending command but does not repeat last command
if line == "" {
if heroscriptBuffer.Len() > 0 {
// Execute pending command
commandText := heroscriptBuffer.String()
result := ts.executeHeroscript(commandText, conn, interactiveMode)
conn.Write([]byte(result))
// Add to history
commandHistory = append(commandHistory, commandText)
historyPos = len(commandHistory)
// Reset buffer
heroscriptBuffer.Reset()
}
continue
}
// Add line to heroscript buffer
if heroscriptBuffer.Len() > 0 {
heroscriptBuffer.WriteString("\n")
}
heroscriptBuffer.WriteString(line)
}
// Handle scanner errors
if err := scanner.Err(); err != nil {
fmt.Printf("Error reading from connection: %v\n", err)
}
}
// isClientAuthenticated checks if a client is authenticated
func (ts *TelnetServer) isClientAuthenticated(conn net.Conn) bool {
// If no secrets are configured, authentication is not required
if len(ts.secrets) == 0 {
return true
}
ts.clientsMutex.RLock()
defer ts.clientsMutex.RUnlock()
authenticated, exists := ts.clients[conn]
return exists && authenticated
}
// isValidSecret checks if a secret is valid
func (ts *TelnetServer) isValidSecret(secret string) bool {
for _, validSecret := range ts.secrets {
if secret == validSecret {
return true
}
}
return false
}
// connKey is a type for context keys
type connKey struct{}
// connKeyValue is the key for storing the connection in context
var connKeyValue = connKey{}
// executeHeroscript executes a heroscript and returns the result
func (ts *TelnetServer) executeHeroscript(script string, conn net.Conn, interactive bool) string {
// Check if this connection has JSON formatting enabled
if conn != nil {
ts.prefsMutex.RLock()
prefs, exists := ts.clientPrefs[conn]
ts.prefsMutex.RUnlock()
if exists && prefs["json"] {
// Add format:json if not already present
if !strings.Contains(script, "format:json") {
script = ts.addJsonFormat(script)
}
}
}
if interactive {
// Format the script with colors
formattedScript := formatHeroscript(script)
fmt.Println("Executing heroscript:\n" + formattedScript)
} else {
fmt.Println("Executing heroscript:\n" + script)
}
// Process the heroscript
result, err := ts.factory.ProcessHeroscript(script)
if err != nil {
errorMsg := fmt.Sprintf("Error: %v", err)
if interactive {
// Only use colors in terminal output, not in telnet response
fmt.Println(ColorRed + errorMsg + ColorReset)
}
return errorMsg
}
if interactive {
// Only use colors in terminal output, not in telnet response
fmt.Println(ColorGreen + "Result: " + result + ColorReset)
}
return result
}
// addJsonFormat adds format:json to a heroscript if not already present
func (ts *TelnetServer) addJsonFormat(script string) string {
lines := strings.Split(script, "\n")
for i, line := range lines {
trimmed := strings.TrimSpace(line)
if strings.HasPrefix(trimmed, "!!") {
// Found action line, add format:json if not present
if !strings.Contains(line, "format:") {
lines[i] = line + " format:json"
}
}
}
return strings.Join(lines, "\n")
}
// formatHeroscript formats heroscript with colors for console output only
// This is not used for telnet responses, only for server-side logging
func formatHeroscript(script string) string {
var formatted strings.Builder
lines := strings.Split(script, "\n")
for _, line := range lines {
trimmed := strings.TrimSpace(line)
// Comments
if strings.HasPrefix(trimmed, "//") {
formatted.WriteString(ColorBlue + line + ColorReset + "\n")
continue
}
// Action lines
if strings.HasPrefix(trimmed, "!") {
parts := strings.SplitN(trimmed, " ", 2)
actionPart := parts[0]
// Highlight actor.action
formatted.WriteString(Bold + ColorYellow + actionPart + ColorReset)
// Add the rest of the line
if len(parts) > 1 {
formatted.WriteString(" " + parts[1])
}
formatted.WriteString("\n")
continue
}
// Parameter lines
if strings.Contains(line, ":") {
parts := strings.SplitN(line, ":", 2)
if len(parts) == 2 {
// Parameter name
formatted.WriteString(parts[0] + ":")
// Parameter value
value := parts[1]
if strings.HasPrefix(value, "'") && strings.HasSuffix(value, "'") {
formatted.WriteString(ColorCyan + value + ColorReset + "\n")
} else {
formatted.WriteString(ColorPurple + value + ColorReset + "\n")
}
continue
}
}
// Default formatting
formatted.WriteString(line + "\n")
}
return formatted.String()
}
// generateHelpText generates help text for available commands
// EnableSignalHandling sets up signal handling for graceful shutdown
// This is now deprecated as signal handling is automatically set up when the server starts
// It's kept for backward compatibility
func (ts *TelnetServer) EnableSignalHandling(onShutdown func()) {
// Set the onShutdown callback
ts.onShutdown = onShutdown
// Setup the signal handling
ts.setupSignalHandling()
}
// setupSignalHandling sets up signal handling for graceful shutdown
func (ts *TelnetServer) setupSignalHandling() {
// Reset any previous signal notification
signal.Reset(syscall.SIGINT, syscall.SIGTERM)
// Register for SIGINT and SIGTERM signals
signal.Notify(ts.sigCh, syscall.SIGINT, syscall.SIGTERM)
// Start a goroutine to handle signals
ts.wg.Add(1)
go func() {
defer ts.wg.Done()
// Wait for signal
sig := <-ts.sigCh
// Log that we received a signal
fmt.Printf("Received %s signal, shutting down telnet server...\n", sig)
// Stop the telnet server
if err := ts.Stop(); err != nil {
fmt.Printf("Error stopping telnet server: %v\n", err)
} else {
fmt.Println("Telnet server stopped successfully")
}
// Call the onShutdown callback if set
if ts.onShutdown != nil {
ts.onShutdown()
}
// Exit the program if this was triggered by a signal
os.Exit(0)
}()
}
func (ts *TelnetServer) generateHelpText(interactive bool) string {
var help strings.Builder
// Only use colors in console output, not in telnet
if interactive {
fmt.Println(Bold + ColorCyan + "Generating help text for client" + ColorReset)
}
help.WriteString("Available Commands:\n")
// System commands
help.WriteString(" System Commands:\n")
help.WriteString(" !!help, h, ? - Show this help\n")
help.WriteString(" !!interactive, i - Toggle interactive mode\n")
help.WriteString(" !!json - Toggle automatic JSON formatting for heroscripts\n")
help.WriteString(" !!quit, q - Disconnect\n")
help.WriteString(" !!exit - Disconnect\n")
help.WriteString("\n")
// Authentication
help.WriteString(" Authentication:\n")
help.WriteString(" !!core.auth secret:'your_secret' - Authenticate with a secret\n")
help.WriteString("\n")
// Usage tips
help.WriteString(" Usage Tips:\n")
help.WriteString(" - Enter an empty line to execute a command\n")
help.WriteString(" - Commands can span multiple lines\n")
help.WriteString(" - Use arrow up to access command history\n")
help.WriteString("------------------------------------------------\n\n")
// Handler help sections
help.WriteString("Handler Documentation:\n\n")
// Get all registered handlers
for actorName, handler := range ts.factory.handlers {
// Try to call the Help method on each handler using reflection
handlerValue := reflect.ValueOf(handler)
helpMethod := handlerValue.MethodByName("Help")
if helpMethod.IsValid() {
// Call the Help method
args := []reflect.Value{reflect.ValueOf("")}
result := helpMethod.Call(args)
// Get the result
if len(result) > 0 && result[0].Kind() == reflect.String {
helpText := result[0].String()
help.WriteString(fmt.Sprintf(" %s Handler (%s):\n", strings.Title(actorName), actorName))
help.WriteString(fmt.Sprintf(" %s\n", helpText))
help.WriteString("\n")
}
}
}
return help.String()
}

View File

@@ -0,0 +1,63 @@
package handlers
import (
"fmt"
"git.ourworld.tf/herocode/heroagent/pkg/heroscript/handlerfactory/core"
)
// AuthHandler handles authentication actions
type AuthHandler struct {
BaseHandler
secrets []string
}
// NewAuthHandler creates a new authentication handler
func NewAuthHandler(secrets ...string) *AuthHandler {
return &AuthHandler{
BaseHandler: BaseHandler{
BaseHandler: core.BaseHandler{
ActorName: "auth",
},
},
secrets: secrets,
}
}
// Auth handles the auth.auth action
func (h *AuthHandler) Auth(script string) string {
params, err := h.ParseParams(script)
if err != nil {
return fmt.Sprintf("Error parsing parameters: %v", err)
}
secret := params.Get("secret")
if secret == "" {
return "Error: secret is required"
}
for _, validSecret := range h.secrets {
if secret == validSecret {
return "Authentication successful"
}
}
return "Authentication failed: invalid secret"
}
// AddSecret adds a new secret to the handler
func (h *AuthHandler) AddSecret(secret string) {
h.secrets = append(h.secrets, secret)
}
// RemoveSecret removes a secret from the handler
func (h *AuthHandler) RemoveSecret(secret string) bool {
for i, s := range h.secrets {
if s == secret {
// Remove the secret
h.secrets = append(h.secrets[:i], h.secrets[i+1:]...)
return true
}
}
return false
}

View File

@@ -0,0 +1,103 @@
package handlers
import (
"fmt"
"reflect"
"strings"
"git.ourworld.tf/herocode/heroagent/pkg/heroscript/handlerfactory/core"
"git.ourworld.tf/herocode/heroagent/pkg/heroscript/paramsparser"
"git.ourworld.tf/herocode/heroagent/pkg/heroscript/playbook"
)
// BaseHandler provides common functionality for all handlers
type BaseHandler struct {
core.BaseHandler
}
// Play processes all actions for this handler's actor
func (h *BaseHandler) Play(script string, handler interface{}) (string, error) {
pb, err := playbook.NewFromText(script)
if err != nil {
return "", fmt.Errorf("failed to parse heroscript: %v", err)
}
// Find all actions for this actor
actions, err := pb.FindActions(0, h.GetActorName(), "", playbook.ActionTypeUnknown)
if err != nil {
return "", fmt.Errorf("failed to find actions: %v", err)
}
if len(actions) == 0 {
return "", fmt.Errorf("no actions found for actor: %s", h.GetActorName())
}
var results []string
// Process each action
for _, action := range actions {
// Convert action name to method name (e.g., "disk_add" -> "DiskAdd")
methodName := convertToMethodName(action.Name)
// Get the method from the handler
method := reflect.ValueOf(handler).MethodByName(methodName)
if !method.IsValid() {
return "", fmt.Errorf("action not supported: %s.%s", h.GetActorName(), action.Name)
}
// Call the method with the action's heroscript
actionScript := action.HeroScript()
args := []reflect.Value{reflect.ValueOf(actionScript)}
result := method.Call(args)
// Get the result
if len(result) > 0 {
resultStr := result[0].String()
results = append(results, resultStr)
}
}
return strings.Join(results, "\n"), nil
}
// ParseParams parses parameters from a heroscript action
func (h *BaseHandler) ParseParams(script string) (*paramsparser.ParamsParser, error) {
pb, err := playbook.NewFromText(script)
if err != nil {
return nil, fmt.Errorf("failed to parse heroscript: %v", err)
}
// Get the first action
if len(pb.Actions) == 0 {
return nil, fmt.Errorf("no actions found in script")
}
return pb.Actions[0].Params, nil
}
// Helper functions for name conversion
// convertToMethodName converts an action name to a method name
// e.g., "disk_add" -> "DiskAdd"
func convertToMethodName(actionName string) string {
parts := strings.Split(actionName, "_")
for i, part := range parts {
if len(part) > 0 {
parts[i] = strings.ToUpper(part[0:1]) + part[1:]
}
}
return strings.Join(parts, "")
}
// convertToActionName converts a method name to an action name
// e.g., "DiskAdd" -> "disk_add"
func convertToActionName(methodName string) string {
var result strings.Builder
for i, char := range methodName {
if i > 0 && 'A' <= char && char <= 'Z' {
result.WriteRune('_')
}
result.WriteRune(char)
}
return strings.ToLower(result.String())
}

View File

@@ -0,0 +1,115 @@
package handlers
import (
"fmt"
"strings"
"git.ourworld.tf/herocode/heroagent/pkg/heroscript/handlerfactory/core"
"git.ourworld.tf/herocode/heroagent/pkg/heroscript/playbook"
)
// HandlerFactory manages a collection of handlers for processing HeroScript commands
type HandlerFactory struct {
handlers map[string]core.Handler
}
// NewHandlerFactory creates a new handler factory
func NewHandlerFactory() *HandlerFactory {
return &HandlerFactory{
handlers: make(map[string]core.Handler),
}
}
// RegisterHandler registers a handler with the factory
func (f *HandlerFactory) RegisterHandler(handler core.Handler) error {
actorName := handler.GetActorName()
if actorName == "" {
return fmt.Errorf("handler has no actor name")
}
if _, exists := f.handlers[actorName]; exists {
return fmt.Errorf("handler for actor '%s' already registered", actorName)
}
f.handlers[actorName] = handler
return nil
}
// GetHandler returns a handler for the specified actor
func (f *HandlerFactory) GetHandler(actorName string) (core.Handler, error) {
handler, exists := f.handlers[actorName]
if !exists {
return nil, fmt.Errorf("no handler registered for actor: %s", actorName)
}
return handler, nil
}
// ProcessHeroscript processes a heroscript command
func (f *HandlerFactory) ProcessHeroscript(script string) (string, error) {
pb, err := playbook.NewFromText(script)
if err != nil {
return "", fmt.Errorf("failed to parse heroscript: %v", err)
}
if len(pb.Actions) == 0 {
return "", fmt.Errorf("no actions found in script")
}
// Group actions by actor
actorActions := make(map[string][]*playbook.Action)
for _, action := range pb.Actions {
actorActions[action.Actor] = append(actorActions[action.Actor], action)
}
var results []string
// Process actions for each actor
for actorName, actions := range actorActions {
handler, err := f.GetHandler(actorName)
if err != nil {
return "", err
}
// Create a playbook with just this actor's actions
actorPB := playbook.New()
for _, action := range actions {
actorAction := actorPB.NewAction(action.CID, action.Name, action.Actor, action.Priority, action.ActionType)
actorAction.Params = action.Params
}
// Process the actions
result, err := handler.Play(actorPB.HeroScript(true), handler)
if err != nil {
return "", err
}
results = append(results, result)
}
return strings.Join(results, "\n"), nil
}
// GetSupportedActions returns a map of supported actions for each registered actor
func (f *HandlerFactory) GetSupportedActions() map[string][]string {
result := make(map[string][]string)
for actorName, handler := range f.handlers {
// Get supported actions for this handler
actions, err := getSupportedActions(handler)
if err == nil && len(actions) > 0 {
result[actorName] = actions
}
}
return result
}
// getSupportedActions returns a list of supported actions for a handler
func getSupportedActions(handler core.Handler) ([]string, error) {
// This is a simplified implementation
// In a real implementation, you would use reflection to get all methods
// that match the pattern for action handlers
// For now, we'll return an empty list
return []string{}, nil
}

View File

@@ -0,0 +1,15 @@
package herohandler
import (
"git.ourworld.tf/herocode/heroagent/pkg/heroscript/handlerfactory/core"
)
// GetFactory returns the handler factory
func (h *HeroHandler) GetFactory() *core.HandlerFactory {
return h.factory
}
// RegisterHandler registers a handler with the factory
func (h *HeroHandler) RegisterHandler(handler core.Handler) error {
return h.factory.RegisterHandler(handler)
}

View File

@@ -0,0 +1,39 @@
package main
import (
"log"
"sync"
"git.ourworld.tf/herocode/heroagent/pkg/heroscript/handlerfactory/herohandler"
)
func main() {
// Initialize the herohandler.DefaultInstance
if err := herohandler.Init(); err != nil {
log.Fatalf("Failed to initialize herohandler: %v", err)
}
// Start the telnet server on both Unix socket and TCP
socketPath := "/tmp/hero.sock"
tcpAddress := "localhost:8023"
log.Println("Starting telnet server...")
//if err := herohandler.DefaultInstance.StartTelnet(socketPath, tcpAddress, "1234");
if err := herohandler.DefaultInstance.StartTelnet(socketPath, tcpAddress); err != nil {
log.Fatalf("Failed to start telnet server: %v", err)
}
log.Println("Telnet server started successfully")
// Register a callback for when the server shuts down
herohandler.DefaultInstance.EnableSignalHandling(func() {
log.Println("Server shutdown complete")
})
log.Println("Press Ctrl+C to stop the server")
// Create a WaitGroup that never completes to keep the program running
// The signal handling in the telnet server will handle the shutdown
var wg sync.WaitGroup
wg.Add(1)
wg.Wait()
}

View File

@@ -0,0 +1,94 @@
package herohandler
import (
"fmt"
"log"
"git.ourworld.tf/herocode/heroagent/pkg/heroscript/handlerfactory/core"
// "git.ourworld.tf/herocode/heroagent/pkg/handlerfactory/heroscript/handlerfactory/fakehandler"
"git.ourworld.tf/herocode/heroagent/pkg/heroscript/handlerfactory/processmanagerhandler"
)
// HeroHandler is the main handler factory that manages all registered handlers
type HeroHandler struct {
factory *core.HandlerFactory
telnetServer *core.TelnetServer
}
var (
// DefaultInstance is the default HeroHandler instance
DefaultInstance *HeroHandler
)
// init initializes the default HeroHandler instance
func Init() error {
factory := core.NewHandlerFactory()
DefaultInstance = &HeroHandler{
factory: factory,
telnetServer: core.NewTelnetServer(factory),
}
log.Println("HeroHandler initialized")
// Register the process manager handler
handler := processmanagerhandler.NewProcessManagerHandler()
if handler == nil {
log.Fatalf("Failed to create process manager handler")
}
if err := DefaultInstance.factory.RegisterHandler(handler); err != nil {
log.Fatalf("Failed to register process manager handler: %v", err)
}
return nil
}
func StartTelnet() error {
if err := DefaultInstance.StartTelnet("/tmp/hero.sock", "localhost:8023"); err != nil {
log.Fatalf("Failed to start telnet server: %v", err)
}
return nil
}
// StartTelnet starts the telnet server on both Unix socket and TCP port
func (h *HeroHandler) StartTelnet(socketPath string, tcpAddress string, secrets ...string) error {
// Create a new telnet server with the factory and secrets
h.telnetServer = core.NewTelnetServer(h.factory, secrets...)
// Start Unix socket server
if socketPath != "" {
if err := h.telnetServer.Start(socketPath); err != nil {
return fmt.Errorf("failed to start Unix socket telnet server: %v", err)
}
log.Printf("Telnet server started on Unix socket: %s", socketPath)
}
// Start TCP server
if tcpAddress != "" {
if err := h.telnetServer.StartTCP(tcpAddress); err != nil {
return fmt.Errorf("failed to start TCP telnet server: %v", err)
}
log.Printf("Telnet server started on TCP address: %s", tcpAddress)
}
return nil
}
// StopTelnet stops the telnet server
func (h *HeroHandler) StopTelnet() error {
if h.telnetServer == nil {
return nil
}
return h.telnetServer.Stop()
}
// EnableSignalHandling sets up signal handling for graceful shutdown of the telnet server
func (h *HeroHandler) EnableSignalHandling(onShutdown func()) {
if h.telnetServer == nil {
return
}
h.telnetServer.EnableSignalHandling(onShutdown)
}

View File

@@ -0,0 +1,51 @@
package main
import (
"fmt"
"git.ourworld.tf/herocode/heroagent/pkg/heroscript/playbook"
)
func main() {
// Example of using the process manager handler through heroscript
// Create a new playbook
pb := playbook.New()
// Start a simple process
startAction := pb.NewAction("1", "start", "process", 0, playbook.ActionTypeUnknown)
startAction.Params.Set("name", "example_process")
startAction.Params.Set("command", "ping -c 60 localhost")
startAction.Params.Set("log", "true")
// List all processes
listAction := pb.NewAction("2", "list", "process", 0, playbook.ActionTypeUnknown)
listAction.Params.Set("format", "table")
// Get status of a specific process
statusAction := pb.NewAction("3", "status", "process", 0, playbook.ActionTypeUnknown)
statusAction.Params.Set("name", "example_process")
// Get logs of a specific process
logsAction := pb.NewAction("4", "logs", "process", 0, playbook.ActionTypeUnknown)
logsAction.Params.Set("name", "example_process")
logsAction.Params.Set("lines", "10")
// Stop a process
stopAction := pb.NewAction("5", "stop", "process", 0, playbook.ActionTypeUnknown)
stopAction.Params.Set("name", "example_process")
// Generate the heroscript
script := pb.HeroScript(true)
// Print the script
fmt.Println("=== Example HeroScript for Process Manager ===")
fmt.Println(script)
fmt.Println("============================================")
fmt.Println("To use this script:")
fmt.Println("1. Start the process manager handler server")
fmt.Println("2. Connect to it using: telnet localhost 8025")
fmt.Println("3. Authenticate with: !!auth 1234")
fmt.Println("4. Copy and paste the above script")
fmt.Println("5. Or use individual commands like: !!process.start name:myprocess command:\"sleep 60\"")
}

View File

@@ -0,0 +1,221 @@
package processmanagerhandler
import (
"fmt"
"git.ourworld.tf/herocode/heroagent/pkg/heroscript/handlerfactory/core"
"git.ourworld.tf/herocode/heroagent/pkg/processmanager"
)
// ProcessManagerHandler handles process manager-related actions
type ProcessManagerHandler struct {
core.BaseHandler
pm *processmanager.ProcessManager
}
// NewProcessManagerHandler creates a new process manager handler
func NewProcessManagerHandler() *ProcessManagerHandler {
return &ProcessManagerHandler{
BaseHandler: core.BaseHandler{
ActorName: "process",
},
pm: processmanager.NewProcessManager(), // Empty string as secret was removed from ProcessManager
}
}
// Start handles the process.start action
func (h *ProcessManagerHandler) Start(script string) string {
params, err := h.ParseParams(script)
if err != nil {
return fmt.Sprintf("Error parsing parameters: %v", err)
}
name := params.Get("name")
if name == "" {
return "Error: Process name is required"
}
command := params.Get("command")
if command == "" {
return "Error: Command is required"
}
logEnabled := params.GetBoolDefault("log", true)
deadline := params.GetIntDefault("deadline", 0)
cron := params.Get("cron")
jobID := params.Get("job_id")
err = h.pm.StartProcess(name, command, logEnabled, deadline, cron, jobID)
if err != nil {
return fmt.Sprintf("Error starting process: %v", err)
}
return fmt.Sprintf("Process '%s' started successfully", name)
}
// Stop handles the process.stop action
func (h *ProcessManagerHandler) Stop(script string) string {
params, err := h.ParseParams(script)
if err != nil {
return fmt.Sprintf("Error parsing parameters: %v", err)
}
name := params.Get("name")
if name == "" {
return "Error: Process name is required"
}
err = h.pm.StopProcess(name)
if err != nil {
return fmt.Sprintf("Error stopping process: %v", err)
}
return fmt.Sprintf("Process '%s' stopped successfully", name)
}
// Restart handles the process.restart action
func (h *ProcessManagerHandler) Restart(script string) string {
params, err := h.ParseParams(script)
if err != nil {
return fmt.Sprintf("Error parsing parameters: %v", err)
}
name := params.Get("name")
if name == "" {
return "Error: Process name is required"
}
err = h.pm.RestartProcess(name)
if err != nil {
return fmt.Sprintf("Error restarting process: %v", err)
}
return fmt.Sprintf("Process '%s' restarted successfully", name)
}
// Delete handles the process.delete action
func (h *ProcessManagerHandler) Delete(script string) string {
params, err := h.ParseParams(script)
if err != nil {
return fmt.Sprintf("Error parsing parameters: %v", err)
}
name := params.Get("name")
if name == "" {
return "Error: Process name is required"
}
err = h.pm.DeleteProcess(name)
if err != nil {
return fmt.Sprintf("Error deleting process: %v", err)
}
return fmt.Sprintf("Process '%s' deleted successfully", name)
}
// List handles the process.list action
func (h *ProcessManagerHandler) List(script string) string {
params, err := h.ParseParams(script)
if err != nil {
return fmt.Sprintf("Error parsing parameters: %v", err)
}
processes := h.pm.ListProcesses()
if len(processes) == 0 {
return "No processes found"
}
format := params.Get("format")
if format == "" {
format = "json"
}
output, err := processmanager.FormatProcessList(processes, format)
if err != nil {
return fmt.Sprintf("Error formatting process list: %v", err)
}
return output
}
// Status handles the process.status action
func (h *ProcessManagerHandler) Status(script string) string {
params, err := h.ParseParams(script)
if err != nil {
return fmt.Sprintf("Error parsing parameters: %v", err)
}
name := params.Get("name")
if name == "" {
return "Error: Process name is required"
}
procInfo, err := h.pm.GetProcessStatus(name)
if err != nil {
return fmt.Sprintf("Error getting process status: %v", err)
}
format := params.Get("format")
if format == "" {
format = "json"
}
output, err := processmanager.FormatProcessInfo(procInfo, format)
if err != nil {
return fmt.Sprintf("Error formatting process status: %v", err)
}
return output
}
// Logs handles the process.logs action
func (h *ProcessManagerHandler) Logs(script string) string {
params, err := h.ParseParams(script)
if err != nil {
return fmt.Sprintf("Error parsing parameters: %v", err)
}
name := params.Get("name")
if name == "" {
return "Error: Process name is required"
}
lines := params.GetIntDefault("lines", 100)
logs, err := h.pm.GetProcessLogs(name, lines)
if err != nil {
return fmt.Sprintf("Error getting process logs: %v", err)
}
return logs
}
// SetLogsPath handles the process.set_logs_path action
func (h *ProcessManagerHandler) SetLogsPath(script string) string {
params, err := h.ParseParams(script)
if err != nil {
return fmt.Sprintf("Error parsing parameters: %v", err)
}
path := params.Get("path")
if path == "" {
return "Error: Path is required"
}
h.pm.SetLogsBasePath(path)
return fmt.Sprintf("Process logs path set to '%s'", path)
}
// Help handles the process.help action
func (h *ProcessManagerHandler) Help(script string) string {
return `Process Manager Handler Commands:
process.start name:<name> command:<command> [log:true|false] [deadline:<seconds>] [cron:<cron_expr>] [job_id:<id>]
process.stop name:<name>
process.restart name:<name>
process.delete name:<name>
process.list [format:json|table|text]
process.status name:<name> [format:json|table|text]
process.logs name:<name> [lines:<count>]
process.set_logs_path path:<path>
process.help`
}

View File

@@ -0,0 +1,117 @@
# This file is automatically @generated by Cargo.
# It is not intended for manual editing.
version = 4
[[package]]
name = "itoa"
version = "1.0.15"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "4a5f13b858c8d314ee3e8f639011f7ccefe71f97f96e50151fb991f267928e2c"
[[package]]
name = "memchr"
version = "2.7.4"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "78ca9ab1a0babb1e7d5695e3530886289c18cf2f87ec19a575a0abdce112e3a3"
[[package]]
name = "proc-macro2"
version = "1.0.94"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "a31971752e70b8b2686d7e46ec17fb38dad4051d94024c88df49b667caea9c84"
dependencies = [
"unicode-ident",
]
[[package]]
name = "quote"
version = "1.0.40"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "1885c039570dc00dcb4ff087a89e185fd56bae234ddc7f056a945bf36467248d"
dependencies = [
"proc-macro2",
]
[[package]]
name = "rustclients"
version = "0.1.0"
dependencies = [
"serde",
"serde_json",
"thiserror",
]
[[package]]
name = "ryu"
version = "1.0.20"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "28d3b2b1366ec20994f1fd18c3c594f05c5dd4bc44d8bb0c1c632c8d6829481f"
[[package]]
name = "serde"
version = "1.0.219"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "5f0e2c6ed6606019b4e29e69dbaba95b11854410e5347d525002456dbbb786b6"
dependencies = [
"serde_derive",
]
[[package]]
name = "serde_derive"
version = "1.0.219"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "5b0276cf7f2c73365f7157c8123c21cd9a50fbbd844757af28ca1f5925fc2a00"
dependencies = [
"proc-macro2",
"quote",
"syn",
]
[[package]]
name = "serde_json"
version = "1.0.140"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "20068b6e96dc6c9bd23e01df8827e6c7e1f2fddd43c21810382803c136b99373"
dependencies = [
"itoa",
"memchr",
"ryu",
"serde",
]
[[package]]
name = "syn"
version = "2.0.100"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "b09a44accad81e1ba1cd74a32461ba89dee89095ba17b32f5d03683b1b1fc2a0"
dependencies = [
"proc-macro2",
"quote",
"unicode-ident",
]
[[package]]
name = "thiserror"
version = "1.0.69"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "b6aaf5339b578ea85b50e080feb250a3e8ae8cfcdff9a461c9ec2904bc923f52"
dependencies = [
"thiserror-impl",
]
[[package]]
name = "thiserror-impl"
version = "1.0.69"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "4fee6c4efc90059e10f81e6d42c60a18f76588c3d74cb83a0b242a2b6c7504c1"
dependencies = [
"proc-macro2",
"quote",
"syn",
]
[[package]]
name = "unicode-ident"
version = "1.0.18"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "5a5f39404a5da50712a4c1eecf25e90dd62b613502b7e925fd4e4d19b5c96512"

View File

@@ -0,0 +1,13 @@
[package]
name = "rustclients"
version = "0.1.0"
edition = "2021"
[dependencies]
serde = { version = "1.0", features = ["derive"] }
serde_json = "1.0"
thiserror = "1.0"
[[example]]
name = "fakehandler_example"
path = "examples/fakehandler_example.rs"

View File

@@ -0,0 +1,111 @@
use std::time::Duration;
use std::thread;
use std::io::{Read, Write};
use std::os::unix::net::{UnixListener, UnixStream};
use std::fs;
// Import directly from the lib.rs
use rustclients::FakeHandlerClient;
use rustclients::Result;
// Simple mock server that handles Unix socket connections
fn start_mock_server(socket_path: &str) -> std::thread::JoinHandle<()> {
let socket_path = socket_path.to_string();
thread::spawn(move || {
// Remove the socket file if it exists
let _ = fs::remove_file(&socket_path);
// Create a Unix socket listener
let listener = match UnixListener::bind(&socket_path) {
Ok(listener) => listener,
Err(e) => {
println!("Failed to bind to socket {}: {}", socket_path, e);
return;
}
};
println!("Mock server listening on {}", socket_path);
// Accept connections and handle them
for stream in listener.incoming() {
match stream {
Ok(mut stream) => {
println!("Mock server: Accepted new connection");
// Read from the stream
let mut buffer = [0; 1024];
match stream.read(&mut buffer) {
Ok(n) => {
let request = String::from_utf8_lossy(&buffer[0..n]);
println!("Mock server received: {}", request);
// Send a welcome message first
let welcome = "Welcome to the mock server\n";
let _ = stream.write_all(welcome.as_bytes());
// Send a response
let response = "OK: Command processed\n> ";
let _ = stream.write_all(response.as_bytes());
},
Err(e) => println!("Mock server error reading from stream: {}", e),
}
},
Err(e) => println!("Mock server error accepting connection: {}", e),
}
}
})
}
fn main() -> Result<()> {
// Define the socket path
let socket_path = "/tmp/heroagent/test.sock";
// Start the mock server
println!("Starting mock server...");
let server_handle = start_mock_server(socket_path);
// Give the server time to start
thread::sleep(Duration::from_millis(500));
// Initialize the client
let client = FakeHandlerClient::new(socket_path)
.with_timeout(Duration::from_secs(5));
println!("\n--- Test 1: Making first request ---");
// This should open a new connection
match client.return_success(Some("Test 1")) {
Ok(response) => println!("Response: {}", response),
Err(e) => println!("Error: {}", e),
}
// Wait a moment
thread::sleep(Duration::from_millis(500));
println!("\n--- Test 2: Making second request ---");
// This should open another new connection
match client.return_success(Some("Test 2")) {
Ok(response) => println!("Response: {}", response),
Err(e) => println!("Error: {}", e),
}
// Wait a moment
thread::sleep(Duration::from_millis(500));
println!("\n--- Test 3: Making third request ---");
// This should open yet another new connection
match client.return_success(Some("Test 3")) {
Ok(response) => println!("Response: {}", response),
Err(e) => println!("Error: {}", e),
}
println!("\nTest completed. Check the debug output to verify that a new connection was opened for each request.");
// Clean up
let _ = fs::remove_file(socket_path);
// Wait for the server to finish (in a real application, you might want to signal it to stop)
println!("Waiting for server to finish...");
// In a real application, we would join the server thread here
Ok(())
}

View File

@@ -0,0 +1,100 @@
use std::time::Duration;
// Import directly from the lib.rs
use rustclients::FakeHandlerClient;
use rustclients::Result;
fn main() -> Result<()> {
// Create a new fake handler client
// Replace with the actual socket path used in your environment
let socket_path = "/tmp/heroagent/fakehandler.sock";
// Initialize the client with a timeout
let client = FakeHandlerClient::new(socket_path)
.with_timeout(Duration::from_secs(5));
println!("Connecting to fake handler at {}", socket_path);
// Connect to the server
match client.connect() {
Ok(_) => println!("Successfully connected to fake handler"),
Err(e) => {
eprintln!("Failed to connect: {}", e);
eprintln!("Make sure the fake handler server is running and the socket path is correct");
return Err(e);
}
}
// Test various commands
// 1. Get help information
println!("\n--- Help Information ---");
match client.help() {
Ok(help) => println!("{}", help),
Err(e) => eprintln!("Error getting help: {}", e),
}
// 2. Return success message
println!("\n--- Success Message ---");
match client.return_success(Some("Custom success message")) {
Ok(response) => println!("Success response: {}", response),
Err(e) => eprintln!("Error getting success: {}", e),
}
// 3. Return JSON response
println!("\n--- JSON Response ---");
match client.return_json(Some("JSON message"), Some("success"), Some(200)) {
Ok(response) => println!("JSON response: {:?}", response),
Err(e) => eprintln!("Error getting JSON: {}", e),
}
// 4. Return error message (this will return a ClientError)
println!("\n--- Error Message ---");
match client.return_error(Some("Custom error message")) {
Ok(response) => println!("Error response (unexpected success): {}", response),
Err(e) => eprintln!("Expected error received: {}", e),
}
// 5. Return empty response
println!("\n--- Empty Response ---");
match client.return_empty() {
Ok(response) => println!("Empty response (length: {})", response.len()),
Err(e) => eprintln!("Error getting empty response: {}", e),
}
// 6. Return large response
println!("\n--- Large Response ---");
match client.return_large(Some(10)) {
Ok(response) => {
let lines: Vec<&str> = response.lines().collect();
println!("Large response (first 3 lines of {} total):", lines.len());
for i in 0..std::cmp::min(3, lines.len()) {
println!(" {}", lines[i]);
}
println!(" ...");
},
Err(e) => eprintln!("Error getting large response: {}", e),
}
// 7. Return invalid JSON (will cause a JSON parsing error)
println!("\n--- Invalid JSON ---");
match client.return_invalid_json() {
Ok(response) => println!("Invalid JSON response (unexpected success): {:?}", response),
Err(e) => eprintln!("Expected JSON error received: {}", e),
}
// 8. Return malformed error
println!("\n--- Malformed Error ---");
match client.return_malformed_error() {
Ok(response) => println!("Malformed error response: {}", response),
Err(e) => eprintln!("Error with malformed error: {}", e),
}
// Close the connection
println!("\nClosing connection");
client.close()?;
println!("Example completed successfully");
Ok(())
}

View File

@@ -0,0 +1,134 @@
use serde::{Deserialize, Serialize};
use std::time::Duration;
use crate::{Client, Result, ClientError};
/// Response from the fake handler
#[derive(Debug, Serialize, Deserialize, Default)]
pub struct FakeResponse {
#[serde(default)]
pub message: String,
#[serde(default)]
pub status: String,
#[serde(default)]
pub code: i32,
}
/// Client for the fake handler
pub struct FakeHandlerClient {
client: Client,
}
impl FakeHandlerClient {
/// Create a new fake handler client
pub fn new(socket_path: &str) -> Self {
Self {
client: Client::new(socket_path),
}
}
/// Set the connection timeout
pub fn with_timeout(mut self, timeout: Duration) -> Self {
self.client = self.client.with_timeout(timeout);
self
}
/// Connect to the server
pub fn connect(&self) -> Result<()> {
self.client.connect()
}
/// Close the connection
pub fn close(&self) -> Result<()> {
self.client.close()
}
/// Return a success message
pub fn return_success(&self, message: Option<&str>) -> Result<String> {
let mut script = "!!fake.return_success".to_string();
if let Some(msg) = message {
script.push_str(&format!(" message:'{}'", msg));
}
self.client.send_command(&script)
}
/// Return an error message
pub fn return_error(&self, message: Option<&str>) -> Result<String> {
let mut script = "!!fake.return_error".to_string();
if let Some(msg) = message {
script.push_str(&format!(" message:'{}'", msg));
}
// This will return a ClientError::ServerError with the error message
self.client.send_command(&script)
}
/// Return a JSON response
pub fn return_json(&self, message: Option<&str>, status: Option<&str>, code: Option<i32>) -> Result<FakeResponse> {
let mut script = "!!fake.return_json".to_string();
if let Some(msg) = message {
script.push_str(&format!(" message:'{}'", msg));
}
if let Some(status_val) = status {
script.push_str(&format!(" status:'{}'", status_val));
}
if let Some(code_val) = code {
script.push_str(&format!(" code:{}", code_val));
}
let response = self.client.send_command(&script)?;
// Parse the JSON response
match serde_json::from_str::<FakeResponse>(&response) {
Ok(result) => Ok(result),
Err(e) => Err(ClientError::JsonError(e)),
}
}
/// Return an invalid JSON response
pub fn return_invalid_json(&self) -> Result<FakeResponse> {
let script = "!!fake.return_invalid_json";
let response = self.client.send_command(&script)?;
// This should fail with a JSON parsing error
match serde_json::from_str::<FakeResponse>(&response) {
Ok(result) => Ok(result),
Err(e) => Err(ClientError::JsonError(e)),
}
}
/// Return an empty response
pub fn return_empty(&self) -> Result<String> {
let script = "!!fake.return_empty";
self.client.send_command(&script)
}
/// Return a large response
pub fn return_large(&self, size: Option<i32>) -> Result<String> {
let mut script = "!!fake.return_large".to_string();
if let Some(size_val) = size {
script.push_str(&format!(" size:{}", size_val));
}
self.client.send_command(&script)
}
/// Return a malformed error message
pub fn return_malformed_error(&self) -> Result<String> {
let script = "!!fake.return_malformed_error";
self.client.send_command(&script)
}
/// Get help information
pub fn help(&self) -> Result<String> {
let script = "!!fake.help";
self.client.send_command(&script)
}
}

View File

@@ -0,0 +1,242 @@
use std::io::{Read, Write};
use std::os::unix::net::UnixStream;
use std::time::Duration;
use thiserror::Error;
use std::fmt;
use std::error::Error as StdError;
mod processmanager;
mod fakehandler;
pub use processmanager::ProcessManagerClient;
pub use fakehandler::FakeHandlerClient;
/// Standard error response from the telnet server
#[derive(Debug, Clone)]
pub struct ServerError {
pub message: String,
pub raw_response: String,
}
impl fmt::Display for ServerError {
fn fmt(&self, f: &mut fmt::Formatter<'_>) -> fmt::Result {
write!(f, "{}", self.message)
}
}
impl StdError for ServerError {}
/// Error type for the client
#[derive(Error, Debug)]
pub enum ClientError {
#[error("IO error: {0}")]
IoError(#[from] std::io::Error),
#[error("JSON parsing error: {0}")]
JsonError(#[from] serde_json::Error),
#[error("Connection error: {0}")]
ConnectionError(String),
#[error("Command error: {0}")]
CommandError(String),
#[error("Server error: {0}")]
ServerError(String),
}
pub type Result<T> = std::result::Result<T, ClientError>;
/// A client for connecting to a Unix socket server with improved error handling
pub struct Client {
socket_path: String,
timeout: Duration,
secret: Option<String>,
}
impl Client {
/// Create a new Unix socket client
pub fn new(socket_path: &str) -> Self {
Self {
socket_path: socket_path.to_string(),
timeout: Duration::from_secs(10),
secret: None,
}
}
/// Set the connection timeout
pub fn with_timeout(mut self, timeout: Duration) -> Self {
self.timeout = timeout;
self
}
/// Set the authentication secret
pub fn with_secret(mut self, secret: &str) -> Self {
self.secret = Some(secret.to_string());
self
}
/// Connect to the Unix socket and return the stream
fn connect_socket(&self) -> Result<UnixStream> {
println!("DEBUG: Opening new connection to {}", self.socket_path);
// Connect to the socket
let stream = UnixStream::connect(&self.socket_path)
.map_err(|e| ClientError::ConnectionError(format!("Failed to connect to socket {}: {}", self.socket_path, e)))?;
// Set read timeout
stream.set_read_timeout(Some(self.timeout))?;
stream.set_write_timeout(Some(self.timeout))?;
// Read welcome message
let mut buffer = [0; 4096];
match stream.try_clone()?.read(&mut buffer) {
Ok(n) => {
let welcome = String::from_utf8_lossy(&buffer[0..n]);
if !welcome.contains("Welcome") {
return Err(ClientError::ConnectionError("Invalid welcome message".to_string()));
}
},
Err(e) => {
return Err(ClientError::IoError(e));
}
}
// Authenticate if a secret is provided
if let Some(secret) = &self.secret {
self.authenticate_stream(&stream, secret)?;
}
Ok(stream)
}
/// Authenticate with the server using the provided stream
fn authenticate_stream(&self, stream: &UnixStream, secret: &str) -> Result<()> {
let mut stream_clone = stream.try_clone()?;
let auth_command = format!("auth {}\n\n", secret);
// Send the auth command
stream_clone.write_all(auth_command.as_bytes())
.map_err(|e| ClientError::CommandError(format!("Failed to send auth command: {}", e)))?;
stream_clone.flush()
.map_err(|e| ClientError::CommandError(format!("Failed to flush auth command: {}", e)))?;
// Add a small delay to ensure the server has time to process the command
std::thread::sleep(Duration::from_millis(100));
// Read the response
let mut buffer = [0; 4096];
let n = stream_clone.read(&mut buffer)
.map_err(|e| ClientError::CommandError(format!("Failed to read auth response: {}", e)))?;
if n == 0 {
return Err(ClientError::ConnectionError("Connection closed by server during authentication".to_string()));
}
let response = String::from_utf8_lossy(&buffer[0..n]).to_string();
// Check for authentication success
if response.contains("Authentication successful") || response.contains("authenticated") {
Ok(())
} else {
Err(ClientError::ServerError(format!("Authentication failed: {}", response)))
}
}
/// Send a command to the server and get the response
pub fn send_command(&self, command: &str) -> Result<String> {
// Connect to the socket for this command
let mut stream = self.connect_socket()?;
// Ensure command ends with double newlines to execute it
let command = if command.ends_with("\n\n") {
command.to_string()
} else if command.ends_with('\n') {
format!("{}\n", command)
} else {
format!("{}\n\n", command)
};
// Send the command
stream.write_all(command.as_bytes())
.map_err(|e| ClientError::CommandError(format!("Failed to send command: {}", e)))?;
stream.flush()
.map_err(|e| ClientError::CommandError(format!("Failed to flush command: {}", e)))?;
// Add a small delay to ensure the server has time to process the command
std::thread::sleep(Duration::from_millis(100));
// Read the response
let mut buffer = [0; 8192]; // Use a larger buffer for large responses
let n = stream.read(&mut buffer)
.map_err(|e| ClientError::CommandError(format!("Failed to read response: {}", e)))?;
if n == 0 {
return Err(ClientError::ConnectionError("Connection closed by server".to_string()));
}
let response = String::from_utf8_lossy(&buffer[0..n]).to_string();
// Remove the prompt if present
let response = response.trim_end_matches("> ").trim().to_string();
// Check for standard error format
if response.starts_with("Error:") {
return Err(ClientError::ServerError(response));
}
// Close the connection by dropping the stream
println!("DEBUG: Closing connection to {}", self.socket_path);
drop(stream);
Ok(response)
}
/// Send a command and parse the JSON response
pub fn send_command_json<T: serde::de::DeserializeOwned>(&self, command: &str) -> Result<T> {
let response = self.send_command(command)?;
// If the response is empty, return an error
if response.trim().is_empty() {
return Err(ClientError::CommandError("Empty response".to_string()));
}
// Handle "action not supported" errors specially
if response.contains("action not supported") {
return Err(ClientError::ServerError(response));
}
// Try to parse the JSON response
match serde_json::from_str::<T>(&response) {
Ok(result) => Ok(result),
Err(e) => {
// If parsing fails, check if it's an error message
if response.starts_with("Error:") || response.contains("error") || response.contains("failed") {
Err(ClientError::ServerError(response))
} else {
Err(ClientError::JsonError(e))
}
},
}
}
/// For backward compatibility
pub fn connect(&self) -> Result<()> {
// Just verify we can connect
let stream = self.connect_socket()?;
drop(stream);
Ok(())
}
/// For backward compatibility
pub fn close(&self) -> Result<()> {
// No-op since we don't maintain a persistent connection
Ok(())
}
/// Authenticate with the server - kept for backward compatibility
pub fn authenticate(&self, secret: &str) -> Result<()> {
// Create a temporary connection to authenticate
let stream = self.connect_socket()?;
self.authenticate_stream(&stream, secret)
}
}

View File

@@ -0,0 +1,164 @@
use serde::{Deserialize, Serialize};
use std::time::Duration;
use crate::{Client, Result};
/// Information about a process
#[derive(Debug, Serialize, Deserialize, Default)]
pub struct ProcessInfo {
#[serde(default)]
pub name: String,
#[serde(default)]
pub command: String,
#[serde(default)]
pub status: String,
#[serde(default)]
pub pid: i32,
#[serde(default)]
pub start_time: String,
#[serde(default)]
pub uptime: String,
#[serde(default)]
pub cpu: String,
#[serde(default)]
pub memory: String,
#[serde(default)]
pub cron: Option<String>,
#[serde(default)]
pub job_id: Option<String>,
}
/// Client for the process manager
pub struct ProcessManagerClient {
client: Client,
}
impl ProcessManagerClient {
/// Create a new process manager client
pub fn new(socket_path: &str) -> Self {
Self {
client: Client::new(socket_path),
}
}
/// Set the connection timeout
pub fn with_timeout(mut self, timeout: Duration) -> Self {
self.client = self.client.with_timeout(timeout);
self
}
/// Set the authentication secret
pub fn with_secret(mut self, secret: &str) -> Self {
self.client = self.client.with_secret(secret);
self
}
/// Connect to the server
pub fn connect(&self) -> Result<()> {
self.client.connect()
}
/// Close the connection
pub fn close(&self) -> Result<()> {
self.client.close()
}
/// Start a new process
pub fn start(&self, name: &str, command: &str, log_enabled: bool, deadline: Option<i32>, cron: Option<&str>, job_id: Option<&str>) -> Result<String> {
let mut script = format!("!!process.start name:'{}' command:'{}' log:{}", name, command, log_enabled);
if let Some(deadline_val) = deadline {
script.push_str(&format!(" deadline:{}", deadline_val));
}
if let Some(cron_val) = cron {
script.push_str(&format!(" cron:'{}'", cron_val));
}
if let Some(job_id_val) = job_id {
script.push_str(&format!(" job_id:'{}'", job_id_val));
}
self.client.send_command(&script)
}
/// Stop a running process
pub fn stop(&self, name: &str) -> Result<String> {
let script = format!("!!process.stop name:'{}'", name);
self.client.send_command(&script)
}
/// Restart a process
pub fn restart(&self, name: &str) -> Result<String> {
let script = format!("!!process.restart name:'{}'", name);
self.client.send_command(&script)
}
/// Delete a process
pub fn delete(&self, name: &str) -> Result<String> {
let script = format!("!!process.delete name:'{}'", name);
self.client.send_command(&script)
}
/// List all processes
pub fn list(&self) -> Result<Vec<ProcessInfo>> {
let script = "!!process.list format:'json'";
let response = self.client.send_command(&script)?;
// Handle empty responses
if response.trim().is_empty() {
return Ok(Vec::new());
}
// Try to parse the response as JSON
match serde_json::from_str::<Vec<ProcessInfo>>(&response) {
Ok(processes) => Ok(processes),
Err(_) => {
// If parsing as a list fails, try parsing as a single ProcessInfo
match serde_json::from_str::<ProcessInfo>(&response) {
Ok(process) => Ok(vec![process]),
Err(_) => {
// If both parsing attempts fail, check if it's a "No processes found" message
if response.contains("No processes found") {
Ok(Vec::new())
} else {
// Otherwise, try to send it as JSON
self.client.send_command_json(&script)
}
}
}
}
}
}
/// Get the status of a specific process
pub fn status(&self, name: &str) -> Result<ProcessInfo> {
let script = format!("!!process.status name:'{}' format:'json'", name);
// Use the send_command_json method which handles JSON parsing with better error handling
self.client.send_command_json(&script)
}
/// Get the logs of a specific process
pub fn logs(&self, name: &str, lines: Option<i32>) -> Result<String> {
let mut script = format!("!!process.logs name:'{}'", name);
if let Some(lines_val) = lines {
script.push_str(&format!(" lines:{}", lines_val));
}
self.client.send_command(&script)
}
/// Set the logs path for the process manager
pub fn set_logs_path(&self, path: &str) -> Result<String> {
let script = format!("!!process.set_logs_path path:'{}'", path);
self.client.send_command(&script)
}
/// Get help information for the process manager
pub fn help(&self) -> Result<String> {
let script = "!!process.help";
self.client.send_command(&script)
}
}