...
This commit is contained in:
143
pkg2_dont_use/heroscript/handlerfactory/README.md
Normal file
143
pkg2_dont_use/heroscript/handlerfactory/README.md
Normal 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.
|
95
pkg2_dont_use/heroscript/handlerfactory/core/base.go
Normal file
95
pkg2_dont_use/heroscript/handlerfactory/core/base.go
Normal 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
|
||||
}
|
145
pkg2_dont_use/heroscript/handlerfactory/core/handlerfactory.go
Normal file
145
pkg2_dont_use/heroscript/handlerfactory/core/handlerfactory.go
Normal 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())
|
||||
}
|
629
pkg2_dont_use/heroscript/handlerfactory/core/telnetserver.go
Normal file
629
pkg2_dont_use/heroscript/handlerfactory/core/telnetserver.go
Normal 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()
|
||||
}
|
@@ -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
|
||||
}
|
@@ -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())
|
||||
}
|
@@ -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
|
||||
}
|
@@ -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)
|
||||
}
|
@@ -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()
|
||||
}
|
@@ -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)
|
||||
}
|
@@ -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\"")
|
||||
}
|
@@ -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`
|
||||
}
|
117
pkg2_dont_use/heroscript/handlerfactory/rustclients/Cargo.lock
generated
Normal file
117
pkg2_dont_use/heroscript/handlerfactory/rustclients/Cargo.lock
generated
Normal 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"
|
@@ -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"
|
@@ -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(())
|
||||
}
|
@@ -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(())
|
||||
}
|
@@ -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)
|
||||
}
|
||||
}
|
242
pkg2_dont_use/heroscript/handlerfactory/rustclients/src/lib.rs
Normal file
242
pkg2_dont_use/heroscript/handlerfactory/rustclients/src/lib.rs
Normal 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)
|
||||
}
|
||||
}
|
@@ -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)
|
||||
}
|
||||
}
|
Reference in New Issue
Block a user