...
This commit is contained in:
180
pkg2_dont_use/heroscript/README.md
Normal file
180
pkg2_dont_use/heroscript/README.md
Normal file
@@ -0,0 +1,180 @@
|
||||
# HeroScript
|
||||
|
||||
A Go package for parsing and executing HeroScript, a small scripting language for defining actions.
|
||||
|
||||
## HeroScript Format
|
||||
|
||||
HeroScript is a simple scripting language with the following structure:
|
||||
|
||||
```heroscript
|
||||
!!actor.action
|
||||
name: 'value'
|
||||
key: 'value'
|
||||
numeric_value: 25
|
||||
boolean_value: 1
|
||||
description: '
|
||||
A multiline description
|
||||
can be added here.
|
||||
|
||||
It supports multiple paragraphs.
|
||||
'
|
||||
```
|
||||
|
||||
Key features:
|
||||
- Every action starts with `!!` (for SAL actions)
|
||||
- The first part is the actor (e.g., `mailclient`)
|
||||
- The second part is the action name (e.g., `configure`)
|
||||
- Parameters follow in an indented block
|
||||
- Multiline strings are supported using single quotes
|
||||
- Comments start with `//`
|
||||
|
||||
## Usage
|
||||
|
||||
### Basic Usage
|
||||
|
||||
```go
|
||||
import (
|
||||
"fmt"
|
||||
"git.ourworld.tf/herocode/heroagent/pkg/heroscript/playbook"
|
||||
)
|
||||
|
||||
// Create a new playbook from HeroScript text
|
||||
script := `
|
||||
!!mailclient.configure
|
||||
name: 'myname'
|
||||
host: 'localhost'
|
||||
port: 25
|
||||
secure: 1
|
||||
`
|
||||
|
||||
pb, err := playbook.NewFromText(script)
|
||||
if err != nil {
|
||||
// Handle error
|
||||
}
|
||||
|
||||
// Access actions
|
||||
for _, action := range pb.Actions {
|
||||
fmt.Printf("Action: %s.%s\n", action.Actor, action.Name)
|
||||
|
||||
// Access parameters
|
||||
name := action.Params.Get("name")
|
||||
host := action.Params.Get("host")
|
||||
port := action.Params.GetInt("port")
|
||||
secure := action.Params.GetBool("secure")
|
||||
|
||||
// Do something with the action...
|
||||
}
|
||||
```
|
||||
|
||||
### Finding Actions
|
||||
|
||||
```go
|
||||
// Find all actions for a specific actor
|
||||
mailActions, err := pb.FindActions(0, "mailclient", "", playbook.ActionTypeUnknown)
|
||||
if err != nil {
|
||||
// Handle error
|
||||
}
|
||||
|
||||
// Find a specific action
|
||||
configAction, err := pb.GetAction(0, "mailclient", "configure")
|
||||
if err != nil {
|
||||
// Handle error
|
||||
}
|
||||
```
|
||||
|
||||
### Generating HeroScript
|
||||
|
||||
```go
|
||||
// Generate HeroScript from the playbook
|
||||
script := pb.HeroScript(true) // true to include done actions
|
||||
fmt.Println(script)
|
||||
|
||||
// Generate HeroScript excluding done actions
|
||||
script = pb.HeroScript(false)
|
||||
fmt.Println(script)
|
||||
```
|
||||
|
||||
## Action Types
|
||||
|
||||
HeroScript supports different action types:
|
||||
|
||||
- `!action` - DAL (Data Access Layer)
|
||||
- `!!action` - SAL (Service Access Layer)
|
||||
- `!!!action` - Macro
|
||||
|
||||
## Integration with HandlerFactory
|
||||
|
||||
The HeroScript package can be used with the HandlerFactory to process commands. Each handler is associated with an actor and implements methods for each action it supports.
|
||||
|
||||
### Handler Implementation
|
||||
|
||||
To create a handler that works with the HandlerFactory and HeroScript:
|
||||
|
||||
```go
|
||||
// MyHandler handles actions for the "myactor" actor
|
||||
type MyHandler struct {
|
||||
handlerfactory.BaseHandler
|
||||
}
|
||||
|
||||
// NewMyHandler creates a new MyHandler
|
||||
func NewMyHandler() *MyHandler {
|
||||
return &MyHandler{
|
||||
BaseHandler: handlerfactory.BaseHandler{
|
||||
ActorName: "myactor",
|
||||
},
|
||||
}
|
||||
}
|
||||
|
||||
// Play processes all actions for this handler's actor
|
||||
func (h *MyHandler) Play(script string, handler interface{}) (string, error) {
|
||||
return h.BaseHandler.Play(script, handler)
|
||||
}
|
||||
|
||||
// DoSomething handles the myactor.do_something action
|
||||
func (h *MyHandler) DoSomething(script string) string {
|
||||
log.Printf("MyActor.DoSomething called with: %s", script)
|
||||
params, err := h.ParseParams(script)
|
||||
if err != nil {
|
||||
return fmt.Sprintf("Error parsing parameters: %v", err)
|
||||
}
|
||||
|
||||
// Process the action...
|
||||
return "Action completed successfully"
|
||||
}
|
||||
```
|
||||
|
||||
### Using with HandlerFactory
|
||||
|
||||
```go
|
||||
// Create a new handler factory
|
||||
factory := handlerfactory.NewHandlerFactory()
|
||||
|
||||
// Create and register a handler
|
||||
myHandler := NewMyHandler()
|
||||
err := factory.RegisterHandler(myHandler)
|
||||
if err != nil {
|
||||
log.Fatalf("Failed to register handler: %v", err)
|
||||
}
|
||||
|
||||
// Process a HeroScript command
|
||||
result, err := factory.ProcessHeroscript(`
|
||||
!!myactor.do_something
|
||||
param1: 'value1'
|
||||
param2: 'value2'
|
||||
`)
|
||||
if err != nil {
|
||||
log.Fatalf("Error processing heroscript: %v", err)
|
||||
}
|
||||
|
||||
fmt.Println(result)
|
||||
```
|
||||
|
||||
## Example
|
||||
|
||||
See the [example](./example/main.go) for a complete demonstration of how to use this package.
|
||||
|
||||
## Running Tests
|
||||
|
||||
```bash
|
||||
go test -v ./pkg/heroscript/playbook
|
||||
```
|
75
pkg2_dont_use/heroscript/cmd/fakehandler_start/main.go
Normal file
75
pkg2_dont_use/heroscript/cmd/fakehandler_start/main.go
Normal file
@@ -0,0 +1,75 @@
|
||||
package main
|
||||
|
||||
import (
|
||||
"flag"
|
||||
"fmt"
|
||||
"os"
|
||||
"os/signal"
|
||||
"syscall"
|
||||
|
||||
"git.ourworld.tf/herocode/heroagent/pkg/handlerfactory/herohandler"
|
||||
)
|
||||
|
||||
func main() {
|
||||
// Parse command line flags
|
||||
socketPath := flag.String("socket", "/tmp/hero.sock", "Unix socket path")
|
||||
tcpAddress := flag.String("tcp", "localhost:8023", "TCP address")
|
||||
useUnixSocket := flag.Bool("unix", true, "Use Unix socket")
|
||||
useTCP := flag.Bool("tcp-enable", false, "Use TCP")
|
||||
flag.Parse()
|
||||
|
||||
// Initialize the hero handler
|
||||
err := herohandler.Init()
|
||||
if err != nil {
|
||||
fmt.Printf("Failed to initialize hero handler: %v\n", err)
|
||||
os.Exit(1)
|
||||
}
|
||||
|
||||
// Get the default instance
|
||||
handler := herohandler.DefaultInstance
|
||||
|
||||
// The fake handler is already registered in the Init() function
|
||||
fmt.Println("Using pre-registered fake handler")
|
||||
|
||||
// Start the server
|
||||
if *useUnixSocket || *useTCP {
|
||||
fmt.Printf("Starting telnet server\n")
|
||||
var socketPathStr, tcpAddressStr string
|
||||
if *useUnixSocket {
|
||||
socketPathStr = *socketPath
|
||||
fmt.Printf("Unix socket: %s\n", socketPathStr)
|
||||
}
|
||||
if *useTCP {
|
||||
tcpAddressStr = *tcpAddress
|
||||
fmt.Printf("TCP address: %s\n", tcpAddressStr)
|
||||
}
|
||||
|
||||
err = handler.StartTelnet(socketPathStr, tcpAddressStr)
|
||||
if err != nil {
|
||||
fmt.Printf("Failed to start telnet server: %v\n", err)
|
||||
os.Exit(1)
|
||||
}
|
||||
}
|
||||
|
||||
// Print available commands
|
||||
factory := handler.GetFactory()
|
||||
actions := factory.GetSupportedActions()
|
||||
fmt.Println("\nAvailable commands:")
|
||||
for actor, commands := range actions {
|
||||
fmt.Printf("Actor: %s\n", actor)
|
||||
for _, command := range commands {
|
||||
fmt.Printf(" !!%s.%s\n", actor, command)
|
||||
}
|
||||
}
|
||||
|
||||
fmt.Println("\nServer is running. Press Ctrl+C to stop.")
|
||||
|
||||
// Wait for interrupt signal
|
||||
sigCh := make(chan os.Signal, 1)
|
||||
signal.Notify(sigCh, syscall.SIGINT, syscall.SIGTERM)
|
||||
<-sigCh
|
||||
|
||||
fmt.Println("\nShutting down...")
|
||||
handler.StopTelnet()
|
||||
fmt.Println("Server stopped")
|
||||
}
|
1
pkg2_dont_use/heroscript/cmd/heroexecute/.gitignore
vendored
Normal file
1
pkg2_dont_use/heroscript/cmd/heroexecute/.gitignore
vendored
Normal file
@@ -0,0 +1 @@
|
||||
heroexecute
|
165
pkg2_dont_use/heroscript/cmd/heroexecute/main.go
Normal file
165
pkg2_dont_use/heroscript/cmd/heroexecute/main.go
Normal file
@@ -0,0 +1,165 @@
|
||||
package main
|
||||
|
||||
import (
|
||||
"flag"
|
||||
"fmt"
|
||||
"io/ioutil"
|
||||
"log"
|
||||
"os"
|
||||
"strings"
|
||||
|
||||
"git.ourworld.tf/herocode/heroagent/pkg/heroscript/playbook"
|
||||
)
|
||||
|
||||
func main() {
|
||||
// Define command line flags
|
||||
parseCmd := flag.NewFlagSet("parse", flag.ExitOnError)
|
||||
parseFile := parseCmd.String("file", "", "Path to heroscript file to parse")
|
||||
parseText := parseCmd.String("text", "", "Heroscript text to parse")
|
||||
parsePriority := parseCmd.Int("priority", 10, "Default priority for actions")
|
||||
|
||||
executeCmd := flag.NewFlagSet("execute", flag.ExitOnError)
|
||||
executeFile := executeCmd.String("file", "", "Path to heroscript file to execute")
|
||||
executeText := executeCmd.String("text", "", "Heroscript text to execute")
|
||||
executePriority := executeCmd.Int("priority", 10, "Default priority for actions")
|
||||
executeActor := executeCmd.String("actor", "", "Only execute actions for this actor")
|
||||
executeAction := executeCmd.String("action", "", "Only execute actions with this name")
|
||||
|
||||
// Check if a subcommand is provided
|
||||
if len(os.Args) < 2 {
|
||||
fmt.Println("Expected 'parse' or 'execute' subcommand")
|
||||
os.Exit(1)
|
||||
}
|
||||
|
||||
// Parse the subcommand
|
||||
switch os.Args[1] {
|
||||
case "parse":
|
||||
parseCmd.Parse(os.Args[2:])
|
||||
handleParseCommand(*parseFile, *parseText, *parsePriority)
|
||||
case "execute":
|
||||
executeCmd.Parse(os.Args[2:])
|
||||
handleExecuteCommand(*executeFile, *executeText, *executePriority, *executeActor, *executeAction)
|
||||
default:
|
||||
fmt.Println("Expected 'parse' or 'execute' subcommand")
|
||||
os.Exit(1)
|
||||
}
|
||||
}
|
||||
|
||||
func handleParseCommand(file, text string, priority int) {
|
||||
var pb *playbook.PlayBook
|
||||
var err error
|
||||
|
||||
// Parse from file or text
|
||||
if file != "" {
|
||||
content, err := ioutil.ReadFile(file)
|
||||
if err != nil {
|
||||
log.Fatalf("Failed to read file: %v", err)
|
||||
}
|
||||
pb, err = playbook.NewFromText(string(content))
|
||||
} else if text != "" {
|
||||
pb, err = playbook.NewFromText(text)
|
||||
} else {
|
||||
log.Fatalf("Either -file or -text must be provided")
|
||||
}
|
||||
|
||||
if err != nil {
|
||||
log.Fatalf("Failed to parse heroscript: %v", err)
|
||||
}
|
||||
|
||||
// Print the parsed playbook
|
||||
fmt.Printf("Parsed %d actions:\n\n", len(pb.Actions))
|
||||
for i, action := range pb.Actions {
|
||||
fmt.Printf("Action %d: %s.%s (Priority: %d)\n", i+1, action.Actor, action.Name, action.Priority)
|
||||
if action.Comments != "" {
|
||||
fmt.Printf(" Comments: %s\n", action.Comments)
|
||||
}
|
||||
fmt.Printf(" Parameters:\n")
|
||||
for key, value := range action.Params.GetAll() {
|
||||
// Format multiline values
|
||||
if strings.Contains(value, "\n") {
|
||||
fmt.Printf(" %s: |\n", key)
|
||||
lines := strings.Split(value, "\n")
|
||||
for _, line := range lines {
|
||||
fmt.Printf(" %s\n", line)
|
||||
}
|
||||
} else {
|
||||
fmt.Printf(" %s: %s\n", key, value)
|
||||
}
|
||||
}
|
||||
fmt.Println()
|
||||
}
|
||||
|
||||
// Print the generated heroscript
|
||||
fmt.Println("Generated HeroScript:")
|
||||
fmt.Println("---------------------")
|
||||
fmt.Println(pb.HeroScript(true))
|
||||
fmt.Println("---------------------")
|
||||
}
|
||||
|
||||
func handleExecuteCommand(file, text string, priority int, actor, action string) {
|
||||
var pb *playbook.PlayBook
|
||||
var err error
|
||||
|
||||
// Parse from file or text
|
||||
if file != "" {
|
||||
content, err := ioutil.ReadFile(file)
|
||||
if err != nil {
|
||||
log.Fatalf("Failed to read file: %v", err)
|
||||
}
|
||||
pb, err = playbook.NewFromText(string(content))
|
||||
} else if text != "" {
|
||||
pb, err = playbook.NewFromText(text)
|
||||
} else {
|
||||
log.Fatalf("Either -file or -text must be provided")
|
||||
}
|
||||
|
||||
if err != nil {
|
||||
log.Fatalf("Failed to parse heroscript: %v", err)
|
||||
}
|
||||
|
||||
// Find actions to execute
|
||||
var actionsToExecute []*playbook.Action
|
||||
if actor != "" || action != "" {
|
||||
// Find specific actions
|
||||
actionsToExecute, err = pb.FindActions(0, actor, action, playbook.ActionTypeUnknown)
|
||||
if err != nil {
|
||||
log.Fatalf("Failed to find actions: %v", err)
|
||||
}
|
||||
} else {
|
||||
// Execute all actions in priority order
|
||||
actionsToExecute, err = pb.ActionsSorted(false)
|
||||
if err != nil {
|
||||
log.Fatalf("Failed to sort actions: %v", err)
|
||||
}
|
||||
}
|
||||
|
||||
// Execute the actions
|
||||
fmt.Printf("Executing %d actions:\n\n", len(actionsToExecute))
|
||||
for i, action := range actionsToExecute {
|
||||
fmt.Printf("Executing action %d: %s.%s\n", i+1, action.Actor, action.Name)
|
||||
|
||||
// In a real implementation, you would have handlers for different actors and actions
|
||||
// For this example, we'll just simulate execution
|
||||
fmt.Printf(" Parameters:\n")
|
||||
for key, value := range action.Params.GetAll() {
|
||||
fmt.Printf(" %s: %s\n", key, value)
|
||||
}
|
||||
|
||||
// Mark the action as done
|
||||
action.Done = true
|
||||
|
||||
// Set some result data
|
||||
action.Result.Set("status", "success")
|
||||
action.Result.Set("execution_time", "0.5s")
|
||||
|
||||
fmt.Printf(" Result: success\n\n")
|
||||
}
|
||||
|
||||
// Check if all actions are done
|
||||
err = pb.EmptyCheck()
|
||||
if err != nil {
|
||||
fmt.Printf("Warning: %v\n", err)
|
||||
} else {
|
||||
fmt.Println("All actions executed successfully!")
|
||||
}
|
||||
}
|
2
pkg2_dont_use/heroscript/cmd/herohandler/.gitignore
vendored
Normal file
2
pkg2_dont_use/heroscript/cmd/herohandler/.gitignore
vendored
Normal file
@@ -0,0 +1,2 @@
|
||||
herohandler
|
||||
example
|
106
pkg2_dont_use/heroscript/cmd/herohandler/README.md
Normal file
106
pkg2_dont_use/heroscript/cmd/herohandler/README.md
Normal file
@@ -0,0 +1,106 @@
|
||||
# HeroHandler Example
|
||||
|
||||
This package demonstrates how to implement and use a handler for HeroScript in the HeroLauncher project.
|
||||
|
||||
## Overview
|
||||
|
||||
The HeroHandler example provides a simple key-value store implementation that showcases how to:
|
||||
|
||||
1. Create a custom handler that extends the base handler functionality
|
||||
2. Implement action methods that can be called via HeroScript
|
||||
3. Parse parameters from HeroScript actions
|
||||
4. Process and execute HeroScript commands
|
||||
|
||||
## Project Structure
|
||||
|
||||
```
|
||||
./herohandler/
|
||||
├── README.md # This documentation file
|
||||
├── main.go # Main executable that uses the example handler
|
||||
└── internal/ # Internal package for the example handler implementation
|
||||
└── example_handler.go # Example handler implementation
|
||||
```
|
||||
|
||||
## Handler Actions
|
||||
|
||||
The example handler supports the following actions:
|
||||
|
||||
- `example.set`: Store a key-value pair
|
||||
- Parameters: `key`, `value`
|
||||
- `example.get`: Retrieve a value by key
|
||||
- Parameters: `key`
|
||||
- `example.list`: List all stored key-value pairs
|
||||
- No parameters
|
||||
- `example.delete`: Remove a key-value pair
|
||||
- Parameters: `key`
|
||||
|
||||
## Usage
|
||||
|
||||
You can run the example handler using the provided `main.go`:
|
||||
|
||||
```bash
|
||||
# Build the binary
|
||||
cd pkg/heroscript/cmd/herohandler
|
||||
go build -o herohandler
|
||||
|
||||
# Set a key-value pair
|
||||
./herohandler "example.set key:mykey value:myvalue"
|
||||
|
||||
# Get a value by key
|
||||
./herohandler "example.get key:mykey"
|
||||
|
||||
# List all stored key-value pairs
|
||||
./herohandler "example.list"
|
||||
|
||||
# Delete a key-value pair
|
||||
./herohandler "example.delete key:mykey"
|
||||
```
|
||||
|
||||
### Important Note on State Persistence
|
||||
|
||||
The example handler maintains its key-value store in memory only for the duration of a single command execution. Each time you run the `herohandler` command, a new instance of the handler is created with an empty data store. This is by design to keep the example simple.
|
||||
|
||||
In a real-world application, you would typically implement persistence using a database, file storage, or other mechanisms to maintain state between command executions.
|
||||
|
||||
### Multi-Command Example
|
||||
|
||||
To execute multiple commands in a single script, you can create a HeroScript file and pass it to the handler. For example:
|
||||
|
||||
```bash
|
||||
# Create a script file
|
||||
cat > example.hero << EOF
|
||||
!!example.set key:user value:john
|
||||
!!example.set key:role value:admin
|
||||
!!example.list
|
||||
EOF
|
||||
|
||||
# Run the script
|
||||
cat example.hero | ./herohandler
|
||||
```
|
||||
|
||||
This would process all commands in a single execution, allowing the in-memory state to be shared between commands.
|
||||
|
||||
## Implementation Details
|
||||
|
||||
The example handler demonstrates several important concepts:
|
||||
|
||||
1. **Handler Structure**: The `ExampleHandler` extends the `handlers.BaseHandler` which provides common functionality for all handlers.
|
||||
|
||||
2. **Action Methods**: Each action is implemented as a method on the handler struct (e.g., `Set`, `Get`, `List`, `Delete`).
|
||||
|
||||
3. **Parameter Parsing**: The `ParseParams` method from `BaseHandler` is used to extract parameters from HeroScript.
|
||||
|
||||
4. **Action Execution**: The `Play` method from `BaseHandler` uses reflection to find and call the appropriate method based on the action name.
|
||||
|
||||
5. **In-Memory Storage**: The example handler maintains a simple in-memory key-value store using a map.
|
||||
|
||||
## Extending the Example
|
||||
|
||||
To create your own handler:
|
||||
|
||||
1. Create a new struct that embeds the `handlers.BaseHandler`
|
||||
2. Implement methods for each action your handler will support
|
||||
3. Create a constructor function that initializes your handler with the appropriate actor name
|
||||
4. Use the `Play` method to process HeroScript commands
|
||||
|
||||
For more complex handlers, you might need to add additional fields to store state or configuration.
|
7
pkg2_dont_use/heroscript/cmd/herohandler/example.hero
Normal file
7
pkg2_dont_use/heroscript/cmd/herohandler/example.hero
Normal file
@@ -0,0 +1,7 @@
|
||||
!!example.set key:username value:johndoe
|
||||
!!example.set key:email value:john@example.com
|
||||
!!example.set key:role value:admin
|
||||
!!example.list
|
||||
!!example.get key:username
|
||||
!!example.delete key:email
|
||||
!!example.list
|
@@ -0,0 +1,102 @@
|
||||
package internal
|
||||
|
||||
import (
|
||||
"fmt"
|
||||
|
||||
"git.ourworld.tf/herocode/heroagent/pkg/handlerfactory"
|
||||
"git.ourworld.tf/herocode/heroagent/pkg/heroscript/handlers"
|
||||
)
|
||||
|
||||
// ExampleHandler handles example actions
|
||||
type ExampleHandler struct {
|
||||
handlers.BaseHandler
|
||||
data map[string]string
|
||||
}
|
||||
|
||||
// NewExampleHandler creates a new example handler
|
||||
func NewExampleHandler() *ExampleHandler {
|
||||
return &ExampleHandler{
|
||||
BaseHandler: handlers.BaseHandler{
|
||||
BaseHandler: handlerfactory.BaseHandler{
|
||||
ActorName: "example",
|
||||
},
|
||||
},
|
||||
data: make(map[string]string),
|
||||
}
|
||||
}
|
||||
|
||||
// Set handles the example.set action
|
||||
func (h *ExampleHandler) Set(script string) string {
|
||||
params, err := h.BaseHandler.ParseParams(script)
|
||||
if err != nil {
|
||||
return fmt.Sprintf("Error parsing parameters: %v", err)
|
||||
}
|
||||
|
||||
key := params.Get("key")
|
||||
if key == "" {
|
||||
return "Error: key is required"
|
||||
}
|
||||
|
||||
value := params.Get("value")
|
||||
if value == "" {
|
||||
return "Error: value is required"
|
||||
}
|
||||
|
||||
h.data[key] = value
|
||||
return fmt.Sprintf("Set %s = %s", key, value)
|
||||
}
|
||||
|
||||
// Get handles the example.get action
|
||||
func (h *ExampleHandler) Get(script string) string {
|
||||
params, err := h.BaseHandler.ParseParams(script)
|
||||
if err != nil {
|
||||
return fmt.Sprintf("Error parsing parameters: %v", err)
|
||||
}
|
||||
|
||||
key := params.Get("key")
|
||||
if key == "" {
|
||||
return "Error: key is required"
|
||||
}
|
||||
|
||||
value, exists := h.data[key]
|
||||
if !exists {
|
||||
return fmt.Sprintf("Key '%s' not found", key)
|
||||
}
|
||||
|
||||
return fmt.Sprintf("%s = %s", key, value)
|
||||
}
|
||||
|
||||
// List handles the example.list action
|
||||
func (h *ExampleHandler) List(script string) string {
|
||||
if len(h.data) == 0 {
|
||||
return "No data stored"
|
||||
}
|
||||
|
||||
var result string
|
||||
for key, value := range h.data {
|
||||
result += fmt.Sprintf("%s = %s\n", key, value)
|
||||
}
|
||||
|
||||
return result
|
||||
}
|
||||
|
||||
// Delete handles the example.delete action
|
||||
func (h *ExampleHandler) Delete(script string) string {
|
||||
params, err := h.BaseHandler.ParseParams(script)
|
||||
if err != nil {
|
||||
return fmt.Sprintf("Error parsing parameters: %v", err)
|
||||
}
|
||||
|
||||
key := params.Get("key")
|
||||
if key == "" {
|
||||
return "Error: key is required"
|
||||
}
|
||||
|
||||
_, exists := h.data[key]
|
||||
if !exists {
|
||||
return fmt.Sprintf("Key '%s' not found", key)
|
||||
}
|
||||
|
||||
delete(h.data, key)
|
||||
return fmt.Sprintf("Deleted key '%s'", key)
|
||||
}
|
101
pkg2_dont_use/heroscript/cmd/herohandler/main.go
Normal file
101
pkg2_dont_use/heroscript/cmd/herohandler/main.go
Normal file
@@ -0,0 +1,101 @@
|
||||
package main
|
||||
|
||||
import (
|
||||
"bufio"
|
||||
"fmt"
|
||||
"io"
|
||||
"os"
|
||||
"strings"
|
||||
|
||||
"git.ourworld.tf/herocode/heroagent/pkg/heroscript/cmd/herohandler/internal"
|
||||
)
|
||||
|
||||
func main() {
|
||||
// Create a new example handler
|
||||
handler := internal.NewExampleHandler()
|
||||
|
||||
// Check if input is coming from stdin (piped input)
|
||||
stat, _ := os.Stdin.Stat()
|
||||
if (stat.Mode() & os.ModeCharDevice) == 0 {
|
||||
// Reading from stdin (pipe or redirect)
|
||||
processStdin(handler)
|
||||
return
|
||||
}
|
||||
|
||||
// Check if there are command-line arguments
|
||||
if len(os.Args) < 2 {
|
||||
printUsage()
|
||||
return
|
||||
}
|
||||
|
||||
// Get the command from arguments
|
||||
command := strings.Join(os.Args[1:], " ")
|
||||
|
||||
// Format as proper HeroScript with !! prefix if not already prefixed
|
||||
script := command
|
||||
if !strings.HasPrefix(script, "!!") {
|
||||
script = "!!" + script
|
||||
}
|
||||
|
||||
// Process the script
|
||||
result, err := handler.Play(script, handler)
|
||||
if err != nil {
|
||||
fmt.Printf("Error: %v\n", err)
|
||||
return
|
||||
}
|
||||
|
||||
// Print the result
|
||||
fmt.Println(result)
|
||||
}
|
||||
|
||||
func printUsage() {
|
||||
fmt.Println("Usage: herohandler <action>")
|
||||
fmt.Println(" cat script.hero | herohandler")
|
||||
fmt.Println("\nExample commands:")
|
||||
fmt.Println(" example.set key:mykey value:myvalue")
|
||||
fmt.Println(" example.get key:mykey")
|
||||
fmt.Println(" example.list")
|
||||
fmt.Println(" example.delete key:mykey")
|
||||
fmt.Println("\nNote: The command will be automatically formatted as HeroScript with !! prefix.")
|
||||
fmt.Println(" You can also pipe a multi-line HeroScript file to process multiple commands.")
|
||||
}
|
||||
|
||||
// processStdin reads and processes HeroScript from stdin
|
||||
func processStdin(handler *internal.ExampleHandler) {
|
||||
reader := bufio.NewReader(os.Stdin)
|
||||
var scriptBuilder strings.Builder
|
||||
|
||||
// Read all lines from stdin
|
||||
for {
|
||||
line, err := reader.ReadString('\n')
|
||||
if err != nil && err != io.EOF {
|
||||
fmt.Printf("Error reading from stdin: %v\n", err)
|
||||
return
|
||||
}
|
||||
|
||||
// Add the line to our script
|
||||
scriptBuilder.WriteString(line)
|
||||
|
||||
// If we've reached EOF, break
|
||||
if err == io.EOF {
|
||||
break
|
||||
}
|
||||
}
|
||||
|
||||
// Process the complete script
|
||||
script := scriptBuilder.String()
|
||||
if script == "" {
|
||||
fmt.Println("Error: Empty script")
|
||||
return
|
||||
}
|
||||
|
||||
// Process the script
|
||||
result, err := handler.Play(script, handler)
|
||||
if err != nil {
|
||||
fmt.Printf("Error: %v\n", err)
|
||||
return
|
||||
}
|
||||
|
||||
// Print the result
|
||||
fmt.Println(result)
|
||||
}
|
208
pkg2_dont_use/heroscript/cmd/herohandler_start/main.go
Normal file
208
pkg2_dont_use/heroscript/cmd/herohandler_start/main.go
Normal file
@@ -0,0 +1,208 @@
|
||||
package main
|
||||
|
||||
import (
|
||||
"bufio"
|
||||
"fmt"
|
||||
"log"
|
||||
"net"
|
||||
"os"
|
||||
"strings"
|
||||
"syscall"
|
||||
"time"
|
||||
|
||||
"git.ourworld.tf/herocode/heroagent/pkg/handlerfactory/herohandler"
|
||||
)
|
||||
|
||||
func main() {
|
||||
// Start the herohandler in a goroutine
|
||||
go startHeroHandler()
|
||||
|
||||
// Run a simple telnet test
|
||||
time.Sleep(1 * time.Second) // Give the server time to start
|
||||
fmt.Println("\n=== Testing HeroHandler Telnet Server ===")
|
||||
fmt.Println("The herohandler is running with a process manager handler registered.")
|
||||
fmt.Println("You can connect to the telnet server at:")
|
||||
fmt.Println("- Unix socket: /tmp/hero.sock")
|
||||
fmt.Println("- TCP address: localhost:8023")
|
||||
fmt.Println("\nYou can use the following command to connect:")
|
||||
fmt.Println(" telnet localhost 8023")
|
||||
fmt.Println(" nc -U /tmp/hero.sock")
|
||||
|
||||
// Start interactive shell
|
||||
fmt.Println("\n=== Interactive Shell ===")
|
||||
fmt.Println("Type 'help' to see available commands")
|
||||
fmt.Println("Type 'exit' to quit")
|
||||
|
||||
startInteractiveShell()
|
||||
}
|
||||
|
||||
func startInteractiveShell() {
|
||||
reader := bufio.NewReader(os.Stdin)
|
||||
|
||||
for {
|
||||
fmt.Print("> ")
|
||||
input, err := reader.ReadString('\n')
|
||||
if err != nil {
|
||||
fmt.Println("Error reading input:", err)
|
||||
continue
|
||||
}
|
||||
|
||||
input = strings.TrimSpace(input)
|
||||
|
||||
switch input {
|
||||
case "exit", "quit":
|
||||
fmt.Println("Exiting...")
|
||||
// Send termination signal to the herohandler goroutine
|
||||
syscall.Kill(syscall.Getpid(), syscall.SIGINT)
|
||||
return
|
||||
|
||||
case "help":
|
||||
showHelp()
|
||||
|
||||
case "status":
|
||||
fmt.Println("HeroHandler is running")
|
||||
fmt.Println("Telnet server is active at:")
|
||||
fmt.Println("- Unix socket: /tmp/hero.sock")
|
||||
fmt.Println("- TCP address: localhost:8023")
|
||||
|
||||
case "actions":
|
||||
showSupportedActions()
|
||||
|
||||
case "test":
|
||||
runTestScript()
|
||||
|
||||
default:
|
||||
if input != "" {
|
||||
fmt.Println("Unknown command. Type 'help' to see available commands.")
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
func showHelp() {
|
||||
fmt.Println("Available commands:")
|
||||
fmt.Println(" help - Show this help message")
|
||||
fmt.Println(" status - Show herohandler status")
|
||||
fmt.Println(" actions - Show supported actions for registered handlers")
|
||||
fmt.Println(" test - Run a test heroscript")
|
||||
fmt.Println(" exit - Exit the program")
|
||||
}
|
||||
|
||||
func showSupportedActions() {
|
||||
// We need to implement this function to get supported actions
|
||||
// Since we can't directly access the factory field, we'll use the telnet interface
|
||||
script := "!!core.actions"
|
||||
|
||||
// Try TCP first, then Unix socket if TCP fails
|
||||
result, err := Send(script, "localhost:8023", false)
|
||||
if err != nil {
|
||||
fmt.Printf("TCP connection failed, trying Unix socket: %v\n", err)
|
||||
result, err = Send(script, "/tmp/hero.sock", true)
|
||||
if err != nil {
|
||||
fmt.Printf("Error getting supported actions: %v\n", err)
|
||||
return
|
||||
}
|
||||
}
|
||||
|
||||
fmt.Println("Supported actions by actor:")
|
||||
fmt.Println(result)
|
||||
}
|
||||
|
||||
// Send connects to the telnet server and sends a command, returning the response
|
||||
func Send(command string, address string, isUnixSocket bool) (string, error) {
|
||||
var conn net.Conn
|
||||
var err error
|
||||
|
||||
// Connect to the server based on the address type
|
||||
if isUnixSocket {
|
||||
conn, err = net.Dial("unix", address)
|
||||
} else {
|
||||
conn, err = net.Dial("tcp", address)
|
||||
}
|
||||
|
||||
if err != nil {
|
||||
return "", fmt.Errorf("error connecting to server: %v", err)
|
||||
}
|
||||
defer conn.Close()
|
||||
|
||||
// Create a reader for the connection
|
||||
reader := bufio.NewReader(conn)
|
||||
|
||||
// Read the welcome message
|
||||
_, err = reader.ReadString('\n')
|
||||
if err != nil {
|
||||
return "", fmt.Errorf("error reading welcome message: %v", err)
|
||||
}
|
||||
|
||||
// Send the command
|
||||
fmt.Fprintf(conn, "%s\n", command)
|
||||
|
||||
// Read the response with a timeout
|
||||
result := ""
|
||||
ch := make(chan string)
|
||||
errCh := make(chan error)
|
||||
|
||||
go func() {
|
||||
var response strings.Builder
|
||||
for {
|
||||
line, err := reader.ReadString('\n')
|
||||
if err != nil {
|
||||
errCh <- fmt.Errorf("error reading response: %v", err)
|
||||
return
|
||||
}
|
||||
response.WriteString(line)
|
||||
|
||||
// If we've received a complete response, break
|
||||
if strings.Contains(line, "\n") && strings.TrimSpace(line) == "" {
|
||||
break
|
||||
}
|
||||
}
|
||||
ch <- response.String()
|
||||
}()
|
||||
|
||||
select {
|
||||
case result = <-ch:
|
||||
return result, nil
|
||||
case err = <-errCh:
|
||||
return "", err
|
||||
case <-time.After(5 * time.Second):
|
||||
return "", fmt.Errorf("timeout waiting for response")
|
||||
}
|
||||
}
|
||||
|
||||
func runTestScript() {
|
||||
// Simple test script for the process manager
|
||||
script := `
|
||||
!!process.list format:json
|
||||
`
|
||||
|
||||
fmt.Println("Running test script:")
|
||||
fmt.Println(script)
|
||||
|
||||
// Send the script to the telnet server
|
||||
// Try TCP first, then Unix socket if TCP fails
|
||||
result, err := Send(script, "localhost:8023", false)
|
||||
if err != nil {
|
||||
fmt.Printf("TCP connection failed, trying Unix socket: %v\n", err)
|
||||
result, err = Send(script, "/tmp/hero.sock", true)
|
||||
if err != nil {
|
||||
fmt.Printf("Unix socket connection failed: %v\n", err)
|
||||
|
||||
// We can't directly access the factory field, so we'll just report the error
|
||||
fmt.Printf("Error: %v\n", err)
|
||||
return
|
||||
}
|
||||
}
|
||||
|
||||
fmt.Println("Result:")
|
||||
fmt.Println(result)
|
||||
}
|
||||
|
||||
func startHeroHandler() {
|
||||
if err := herohandler.Init(); err != nil {
|
||||
log.Fatalf("Failed to start telnet server: %v", err)
|
||||
}
|
||||
if err := herohandler.StartTelnet(); err != nil {
|
||||
log.Fatalf("Failed to start telnet server: %v", err)
|
||||
}
|
||||
}
|
1
pkg2_dont_use/heroscript/cmd/heroscriptexample/.gitignore
vendored
Normal file
1
pkg2_dont_use/heroscript/cmd/heroscriptexample/.gitignore
vendored
Normal file
@@ -0,0 +1 @@
|
||||
heroscriptexample
|
75
pkg2_dont_use/heroscript/cmd/heroscriptexample/main.go
Normal file
75
pkg2_dont_use/heroscript/cmd/heroscriptexample/main.go
Normal file
@@ -0,0 +1,75 @@
|
||||
package main
|
||||
|
||||
import (
|
||||
"fmt"
|
||||
"log"
|
||||
|
||||
"git.ourworld.tf/herocode/heroagent/pkg/heroscript/playbook"
|
||||
)
|
||||
|
||||
const exampleScript = `
|
||||
//This is a mail client configuration
|
||||
!!mailclient.configure
|
||||
name: 'mymail'
|
||||
host: 'smtp.example.com'
|
||||
port: 25
|
||||
secure: 1
|
||||
reset: 1
|
||||
description: '
|
||||
This is a multiline description
|
||||
for my mail client configuration.
|
||||
|
||||
It supports multiple paragraphs.
|
||||
'
|
||||
|
||||
//System update action
|
||||
!!system.update
|
||||
force: 1
|
||||
packages: 'git,curl,wget'
|
||||
`
|
||||
|
||||
func main() {
|
||||
// Parse heroscript
|
||||
pb, err := playbook.NewFromText(exampleScript)
|
||||
if err != nil {
|
||||
log.Fatalf("Failed to parse heroscript: %v", err)
|
||||
}
|
||||
|
||||
// Print the playbook
|
||||
fmt.Println("Playbook contains:")
|
||||
fmt.Printf("- %d actions\n", len(pb.Actions))
|
||||
fmt.Println("- Hash: " + pb.HashKey())
|
||||
fmt.Println()
|
||||
|
||||
// Print each action
|
||||
for i, action := range pb.Actions {
|
||||
fmt.Printf("Action %d: %s.%s\n", i+1, action.Actor, action.Name)
|
||||
fmt.Printf(" Comments: %s\n", action.Comments)
|
||||
fmt.Printf(" Parameters:\n")
|
||||
|
||||
for key, value := range action.Params.GetAll() {
|
||||
fmt.Printf(" %s: %s\n", key, value)
|
||||
}
|
||||
fmt.Println()
|
||||
}
|
||||
|
||||
// Generate heroscript
|
||||
fmt.Println("Generated HeroScript:")
|
||||
fmt.Println("---------------------")
|
||||
fmt.Println(pb.HeroScript(true))
|
||||
fmt.Println("---------------------")
|
||||
|
||||
// Demonstrate finding actions
|
||||
mailActions, err := pb.FindActions(0, "mailclient", "", playbook.ActionTypeUnknown)
|
||||
if err != nil {
|
||||
log.Fatalf("Error finding actions: %v", err)
|
||||
}
|
||||
fmt.Printf("\nFound %d mail client actions\n", len(mailActions))
|
||||
|
||||
// Mark an action as done
|
||||
if len(pb.Actions) > 0 {
|
||||
pb.Actions[0].Done = true
|
||||
fmt.Println("\nAfter marking first action as done:")
|
||||
fmt.Println(pb.HeroScript(false)) // Don't show done actions
|
||||
}
|
||||
}
|
19
pkg2_dont_use/heroscript/cmd/vmhandler/.gitignore
vendored
Normal file
19
pkg2_dont_use/heroscript/cmd/vmhandler/.gitignore
vendored
Normal file
@@ -0,0 +1,19 @@
|
||||
# Binaries for programs and plugins
|
||||
*.exe
|
||||
*.exe~
|
||||
*.dll
|
||||
*.so
|
||||
*.dylib
|
||||
vmhandler
|
||||
|
||||
# Test binary, built with `go test -c`
|
||||
*.test
|
||||
|
||||
# Output of the go coverage tool, specifically when used with LiteIDE
|
||||
*.out
|
||||
|
||||
# Dependency directories (remove the comment below to include it)
|
||||
# vendor/
|
||||
|
||||
# Go workspace file
|
||||
go.work
|
134
pkg2_dont_use/heroscript/cmd/vmhandler/README.md
Normal file
134
pkg2_dont_use/heroscript/cmd/vmhandler/README.md
Normal file
@@ -0,0 +1,134 @@
|
||||
# VM Handler Example
|
||||
|
||||
This example demonstrates how to use the HandlerFactory with a VM handler to process heroscript commands.
|
||||
|
||||
## Overview
|
||||
|
||||
The VM handler example shows how to:
|
||||
|
||||
1. Create a handler that processes VM-related actions
|
||||
2. Register the handler with the HandlerFactory
|
||||
3. Start a telnet server that uses the HandlerFactory to process commands
|
||||
4. Connect to the telnet server and send heroscript commands
|
||||
|
||||
## Running the Example
|
||||
|
||||
To run the example:
|
||||
|
||||
```bash
|
||||
cd ~/code/github/freeflowuniverse/herocode/heroagent/pkg/handlerfactory/cmd/vmhandler
|
||||
go run . tutorial
|
||||
#to run just the server do
|
||||
go run .
|
||||
#you can then go to other terminal and play with telnet / nc
|
||||
```
|
||||
|
||||
This will start a telnet server on:
|
||||
- Unix socket: `/tmp/vmhandler.sock`
|
||||
- TCP: `localhost:8024`
|
||||
|
||||
## Connecting to the Server
|
||||
|
||||
### Using Unix Socket
|
||||
|
||||
```bash
|
||||
nc -U /tmp/vmhandler.sock
|
||||
```
|
||||
|
||||
### Using TCP
|
||||
|
||||
```bash
|
||||
telnet localhost 8024
|
||||
```
|
||||
|
||||
## Authentication
|
||||
|
||||
When you connect, you'll need to authenticate with the secret:
|
||||
|
||||
```
|
||||
!!auth secret:1234
|
||||
```
|
||||
|
||||
## Available Commands
|
||||
|
||||
Once authenticated, you can use the following commands:
|
||||
|
||||
```
|
||||
!!vm.define name:'test_vm' cpu:4 memory:'8GB' storage:'100GB'
|
||||
!!vm.start name:'test_vm'
|
||||
!!vm.stop name:'test_vm'
|
||||
!!vm.disk_add name:'test_vm' size:'50GB' type:'SSD'
|
||||
!!vm.list
|
||||
!!vm.status name:'test_vm'
|
||||
!!vm.delete name:'test_vm' force:true
|
||||
```
|
||||
|
||||
## Example Session
|
||||
|
||||
Here's an example session:
|
||||
|
||||
```
|
||||
$ telnet localhost 8024
|
||||
Connected to localhost.
|
||||
Escape character is '^]'.
|
||||
** Welcome: you are not authenticated, provide secret.
|
||||
!!auth secret:1234
|
||||
|
||||
** Welcome: you are authenticated.
|
||||
!!vm.define name:'test_vm' cpu:4 memory:'8GB' storage:'100GB'
|
||||
VM 'test_vm' defined successfully with 4 CPU, 8GB memory, and 100GB storage
|
||||
|
||||
!!vm.start name:'test_vm'
|
||||
VM 'test_vm' started successfully
|
||||
|
||||
!!vm.disk_add name:'test_vm' size:'50GB' type:'SSD'
|
||||
Added 50GB SSD disk to VM 'test_vm'
|
||||
|
||||
!!vm.status name:'test_vm'
|
||||
VM 'test_vm' status:
|
||||
- Status: running
|
||||
- CPU: 4
|
||||
- Memory: 8GB
|
||||
- Storage: 100GB
|
||||
- Attached disks:
|
||||
1. 50GB SSD
|
||||
|
||||
!!vm.list
|
||||
Defined VMs:
|
||||
- test_vm (running): 4 CPU, 8GB memory, 100GB storage
|
||||
Attached disks:
|
||||
1. 50GB SSD
|
||||
|
||||
!!vm.stop name:'test_vm'
|
||||
VM 'test_vm' stopped successfully
|
||||
|
||||
!!vm.delete name:'test_vm'
|
||||
VM 'test_vm' deleted successfully
|
||||
|
||||
!!quit
|
||||
Goodbye!
|
||||
Connection closed by foreign host.
|
||||
```
|
||||
|
||||
## Other Commands
|
||||
|
||||
- `!!help`, `h`, or `?` - Show help
|
||||
- `!!interactive` or `!!i` - Toggle interactive mode (with colors)
|
||||
- `!!quit`, `!!exit`, or `q` - Disconnect from server
|
||||
|
||||
## How It Works
|
||||
|
||||
1. The `main.go` file creates a HandlerFactory and registers the VM handler
|
||||
2. It starts a telnet server that uses the HandlerFactory to process commands
|
||||
3. When a client connects and sends a heroscript command, the server:
|
||||
- Parses the command to determine the actor and action
|
||||
- Calls the appropriate method on the VM handler
|
||||
- Returns the result to the client
|
||||
|
||||
## Extending the Example
|
||||
|
||||
You can extend this example by:
|
||||
|
||||
1. Adding more methods to the VM handler
|
||||
2. Creating new handlers for different actors
|
||||
3. Registering multiple handlers with the HandlerFactory
|
276
pkg2_dont_use/heroscript/cmd/vmhandler/tutorial.go
Normal file
276
pkg2_dont_use/heroscript/cmd/vmhandler/tutorial.go
Normal file
@@ -0,0 +1,276 @@
|
||||
package main
|
||||
|
||||
import (
|
||||
"bufio"
|
||||
"fmt"
|
||||
"os"
|
||||
"time"
|
||||
|
||||
"git.ourworld.tf/herocode/heroagent/pkg/handlerfactory"
|
||||
)
|
||||
|
||||
// runTutorial runs an interactive tutorial demonstrating the VM handler
|
||||
func runTutorial() {
|
||||
fmt.Println("=== VM Handler Tutorial ===")
|
||||
fmt.Println("This tutorial will demonstrate how to use the VM handler with heroscript commands.")
|
||||
fmt.Println("Press Enter to continue through each step...")
|
||||
waitForEnter()
|
||||
|
||||
// Create a new handler factory
|
||||
fmt.Println("\nStep 1: Create a new HandlerFactory")
|
||||
fmt.Println("factory := handlerfactory.NewHandlerFactory()")
|
||||
factory := handlerfactory.NewHandlerFactory()
|
||||
waitForEnter()
|
||||
|
||||
// Create a VM handler
|
||||
fmt.Println("\nStep 2: Create a VM handler")
|
||||
fmt.Println("vmHandler := NewVMHandler()")
|
||||
vmHandler := NewVMHandler()
|
||||
waitForEnter()
|
||||
|
||||
// Register the VM handler with the factory
|
||||
fmt.Println("\nStep 3: Register the VM handler with the factory")
|
||||
fmt.Println("factory.RegisterHandler(vmHandler)")
|
||||
err := factory.RegisterHandler(vmHandler)
|
||||
if err != nil {
|
||||
fmt.Printf("Error: %v\n", err)
|
||||
return
|
||||
}
|
||||
fmt.Println("Handler registered successfully!")
|
||||
waitForEnter()
|
||||
|
||||
// Show available actions
|
||||
fmt.Println("\nStep 4: List available actions for the VM handler")
|
||||
actions := factory.GetSupportedActions()
|
||||
fmt.Println("Supported actions for 'vm' actor:")
|
||||
for _, action := range actions["vm"] {
|
||||
fmt.Printf("- %s\n", action)
|
||||
}
|
||||
waitForEnter()
|
||||
|
||||
// Process heroscript commands
|
||||
fmt.Println("\nStep 5: Process heroscript commands")
|
||||
|
||||
// Define a VM
|
||||
defineScript := `!!vm.define name:'tutorial_vm' cpu:2 memory:'4GB' storage:'50GB'
|
||||
description: 'A tutorial VM for demonstration purposes'`
|
||||
fmt.Println("\nCommand:")
|
||||
fmt.Println(defineScript)
|
||||
fmt.Println("\nProcessing...")
|
||||
time.Sleep(1 * time.Second)
|
||||
result, err := factory.ProcessHeroscript(defineScript)
|
||||
if err != nil {
|
||||
fmt.Printf("Error: %v\n", err)
|
||||
} else {
|
||||
fmt.Printf("Result: %s\n", result)
|
||||
}
|
||||
waitForEnter()
|
||||
|
||||
// Start the VM
|
||||
startScript := `!!vm.start name:'tutorial_vm'`
|
||||
fmt.Println("\nCommand:")
|
||||
fmt.Println(startScript)
|
||||
fmt.Println("\nProcessing...")
|
||||
time.Sleep(1 * time.Second)
|
||||
result, err = factory.ProcessHeroscript(startScript)
|
||||
if err != nil {
|
||||
fmt.Printf("Error: %v\n", err)
|
||||
} else {
|
||||
fmt.Printf("Result: %s\n", result)
|
||||
}
|
||||
waitForEnter()
|
||||
|
||||
// Add a disk
|
||||
diskAddScript := `!!vm.disk_add name:'tutorial_vm' size:'20GB' type:'SSD'`
|
||||
fmt.Println("\nCommand:")
|
||||
fmt.Println(diskAddScript)
|
||||
fmt.Println("\nProcessing...")
|
||||
time.Sleep(1 * time.Second)
|
||||
result, err = factory.ProcessHeroscript(diskAddScript)
|
||||
if err != nil {
|
||||
fmt.Printf("Error: %v\n", err)
|
||||
} else {
|
||||
fmt.Printf("Result: %s\n", result)
|
||||
}
|
||||
waitForEnter()
|
||||
|
||||
// Check VM status
|
||||
statusScript := `!!vm.status name:'tutorial_vm'`
|
||||
fmt.Println("\nCommand:")
|
||||
fmt.Println(statusScript)
|
||||
fmt.Println("\nProcessing...")
|
||||
time.Sleep(1 * time.Second)
|
||||
result, err = factory.ProcessHeroscript(statusScript)
|
||||
if err != nil {
|
||||
fmt.Printf("Error: %v\n", err)
|
||||
} else {
|
||||
fmt.Printf("Result: %s\n", result)
|
||||
}
|
||||
waitForEnter()
|
||||
|
||||
// List all VMs
|
||||
listScript := `!!vm.list`
|
||||
fmt.Println("\nCommand:")
|
||||
fmt.Println(listScript)
|
||||
fmt.Println("\nProcessing...")
|
||||
time.Sleep(1 * time.Second)
|
||||
result, err = factory.ProcessHeroscript(listScript)
|
||||
if err != nil {
|
||||
fmt.Printf("Error: %v\n", err)
|
||||
} else {
|
||||
fmt.Printf("Result: %s\n", result)
|
||||
}
|
||||
waitForEnter()
|
||||
|
||||
// Stop the VM
|
||||
stopScript := `!!vm.stop name:'tutorial_vm'`
|
||||
fmt.Println("\nCommand:")
|
||||
fmt.Println(stopScript)
|
||||
fmt.Println("\nProcessing...")
|
||||
time.Sleep(1 * time.Second)
|
||||
result, err = factory.ProcessHeroscript(stopScript)
|
||||
if err != nil {
|
||||
fmt.Printf("Error: %v\n", err)
|
||||
} else {
|
||||
fmt.Printf("Result: %s\n", result)
|
||||
}
|
||||
waitForEnter()
|
||||
|
||||
// Delete the VM
|
||||
deleteScript := `!!vm.delete name:'tutorial_vm'`
|
||||
fmt.Println("\nCommand:")
|
||||
fmt.Println(deleteScript)
|
||||
fmt.Println("\nProcessing...")
|
||||
time.Sleep(1 * time.Second)
|
||||
result, err = factory.ProcessHeroscript(deleteScript)
|
||||
if err != nil {
|
||||
fmt.Printf("Error: %v\n", err)
|
||||
} else {
|
||||
fmt.Printf("Result: %s\n", result)
|
||||
}
|
||||
waitForEnter()
|
||||
|
||||
// Try an invalid command
|
||||
invalidScript := `!!vm.invalid name:'tutorial_vm'`
|
||||
fmt.Println("\nInvalid Command:")
|
||||
fmt.Println(invalidScript)
|
||||
fmt.Println("\nProcessing...")
|
||||
time.Sleep(1 * time.Second)
|
||||
result, err = factory.ProcessHeroscript(invalidScript)
|
||||
if err != nil {
|
||||
fmt.Printf("Error: %v\n", err)
|
||||
} else {
|
||||
fmt.Printf("Result: %s\n", result)
|
||||
}
|
||||
waitForEnter()
|
||||
|
||||
// Conclusion
|
||||
fmt.Println("\nTutorial Complete!")
|
||||
fmt.Println("You've seen how to:")
|
||||
fmt.Println("1. Create a HandlerFactory")
|
||||
fmt.Println("2. Register a VM handler")
|
||||
fmt.Println("3. Process various heroscript commands")
|
||||
fmt.Println("\nTo run the full telnet server example, execute:")
|
||||
fmt.Println("go run main.go vm_handler.go")
|
||||
fmt.Println("\nPress Enter to exit the tutorial...")
|
||||
waitForEnter()
|
||||
}
|
||||
|
||||
// waitForEnter waits for the user to press Enter
|
||||
func waitForEnter() {
|
||||
reader := bufio.NewReader(os.Stdin)
|
||||
reader.ReadString('\n')
|
||||
}
|
||||
|
||||
// VMTutorial contains the tutorial text for the VM handler
|
||||
const VMTutorial = `
|
||||
VM Handler Tutorial
|
||||
==================
|
||||
|
||||
The VM handler provides a set of commands to manage virtual machines through heroscript commands.
|
||||
|
||||
Available VM commands:
|
||||
!!vm.define name:test_vm cpu:4 memory:8GB storage:100GB
|
||||
!!vm.start name:test_vm
|
||||
!!vm.stop name:test_vm
|
||||
!!vm.disk_add name:test_vm size:50GB type:SSD
|
||||
!!vm.list
|
||||
!!vm.status name:test_vm
|
||||
!!vm.delete name:test_vm force:true
|
||||
|
||||
Authentication secret: 1234
|
||||
|
||||
Command Details:
|
||||
--------------
|
||||
1. define - Create a new VM with specified resources
|
||||
Parameters:
|
||||
- name: (required) Name of the VM
|
||||
- cpu: (optional) Number of CPUs, default: 1
|
||||
- memory: (optional) Memory size, default: 1GB
|
||||
- storage: (optional) Storage size, default: 10GB
|
||||
- description: (optional) Description of the VM
|
||||
|
||||
2. start - Start a VM
|
||||
Parameters:
|
||||
- name: (required) Name of the VM to start
|
||||
|
||||
3. stop - Stop a running VM
|
||||
Parameters:
|
||||
- name: (required) Name of the VM to stop
|
||||
|
||||
4. disk_add - Add a disk to a VM
|
||||
Parameters:
|
||||
- name: (required) Name of the VM
|
||||
- size: (optional) Size of the disk, default: 10GB
|
||||
- type: (optional) Type of disk (SSD, HDD), default: HDD
|
||||
|
||||
5. list - List all VMs
|
||||
|
||||
6. status - Show status of a VM
|
||||
Parameters:
|
||||
- name: (required) Name of the VM
|
||||
|
||||
7. delete - Delete a VM
|
||||
Parameters:
|
||||
- name: (required) Name of the VM
|
||||
- force: (optional) Force deletion even if VM is running, default: false
|
||||
|
||||
8. help - Show this help message
|
||||
|
||||
Examples:
|
||||
--------
|
||||
1. Create a new VM:
|
||||
!!vm.define name:webserver cpu:2 memory:4GB storage:50GB description:'Web server VM'
|
||||
|
||||
2. Start the VM:
|
||||
!!vm.start name:webserver
|
||||
|
||||
3. Add an SSD disk:
|
||||
!!vm.disk_add name:webserver size:100GB type:SSD
|
||||
|
||||
4. Check VM status:
|
||||
!!vm.status name:webserver
|
||||
|
||||
5. List all VMs:
|
||||
!!vm.list
|
||||
|
||||
6. Stop the VM:
|
||||
!!vm.stop name:webserver
|
||||
|
||||
7. Delete the VM:
|
||||
!!vm.delete name:webserver force:true
|
||||
`
|
||||
|
||||
// addTutorialCommand adds the tutorial command to the main function
|
||||
func addTutorialCommand() {
|
||||
// Check command line arguments
|
||||
if len(os.Args) > 1 && os.Args[1] == "tutorial" {
|
||||
runTutorial()
|
||||
os.Exit(0)
|
||||
}
|
||||
}
|
||||
|
||||
// GetVMTutorial returns the VM handler tutorial text
|
||||
func GetVMTutorial() string {
|
||||
return VMTutorial
|
||||
}
|
285
pkg2_dont_use/heroscript/cmd/vmhandler/vm_handler.go
Normal file
285
pkg2_dont_use/heroscript/cmd/vmhandler/vm_handler.go
Normal file
@@ -0,0 +1,285 @@
|
||||
package main
|
||||
|
||||
import (
|
||||
"fmt"
|
||||
"strings"
|
||||
|
||||
"git.ourworld.tf/herocode/heroagent/pkg/handlerfactory"
|
||||
)
|
||||
|
||||
// VMHandler handles VM-related actions
|
||||
type VMHandler struct {
|
||||
handlerfactory.BaseHandler
|
||||
vms map[string]*VM
|
||||
}
|
||||
|
||||
// VM represents a virtual machine
|
||||
type VM struct {
|
||||
Name string
|
||||
CPU int
|
||||
Memory string
|
||||
Storage string
|
||||
Description string
|
||||
Running bool
|
||||
Disks []Disk
|
||||
}
|
||||
|
||||
// Disk represents a disk attached to a VM
|
||||
type Disk struct {
|
||||
Size string
|
||||
Type string
|
||||
}
|
||||
|
||||
// NewVMHandler creates a new VM handler
|
||||
func NewVMHandler() *VMHandler {
|
||||
return &VMHandler{
|
||||
BaseHandler: handlerfactory.BaseHandler{
|
||||
ActorName: "vm",
|
||||
},
|
||||
vms: make(map[string]*VM),
|
||||
}
|
||||
}
|
||||
|
||||
// Define handles the vm.define action
|
||||
func (h *VMHandler) Define(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: VM name is required"
|
||||
}
|
||||
|
||||
// Check if VM already exists
|
||||
if _, exists := h.vms[name]; exists {
|
||||
return fmt.Sprintf("Error: VM '%s' already exists", name)
|
||||
}
|
||||
|
||||
// Create new VM
|
||||
cpu := params.GetIntDefault("cpu", 1)
|
||||
memory := params.Get("memory")
|
||||
if memory == "" {
|
||||
memory = "1GB"
|
||||
}
|
||||
storage := params.Get("storage")
|
||||
if storage == "" {
|
||||
storage = "10GB"
|
||||
}
|
||||
description := params.Get("description")
|
||||
|
||||
vm := &VM{
|
||||
Name: name,
|
||||
CPU: cpu,
|
||||
Memory: memory,
|
||||
Storage: storage,
|
||||
Description: description,
|
||||
Running: false,
|
||||
Disks: []Disk{},
|
||||
}
|
||||
|
||||
// Add VM to map
|
||||
h.vms[name] = vm
|
||||
|
||||
return fmt.Sprintf("VM '%s' defined successfully with %d CPU, %s memory, and %s storage",
|
||||
name, cpu, memory, storage)
|
||||
}
|
||||
|
||||
// Start handles the vm.start action
|
||||
func (h *VMHandler) 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: VM name is required"
|
||||
}
|
||||
|
||||
// Find VM
|
||||
vm, exists := h.vms[name]
|
||||
if !exists {
|
||||
return fmt.Sprintf("Error: VM '%s' not found", name)
|
||||
}
|
||||
|
||||
// Start VM
|
||||
if vm.Running {
|
||||
return fmt.Sprintf("VM '%s' is already running", name)
|
||||
}
|
||||
|
||||
vm.Running = true
|
||||
return fmt.Sprintf("VM '%s' started successfully", name)
|
||||
}
|
||||
|
||||
// Stop handles the vm.stop action
|
||||
func (h *VMHandler) 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: VM name is required"
|
||||
}
|
||||
|
||||
// Find VM
|
||||
vm, exists := h.vms[name]
|
||||
if !exists {
|
||||
return fmt.Sprintf("Error: VM '%s' not found", name)
|
||||
}
|
||||
|
||||
// Stop VM
|
||||
if !vm.Running {
|
||||
return fmt.Sprintf("VM '%s' is already stopped", name)
|
||||
}
|
||||
|
||||
vm.Running = false
|
||||
return fmt.Sprintf("VM '%s' stopped successfully", name)
|
||||
}
|
||||
|
||||
// DiskAdd handles the vm.disk_add action
|
||||
func (h *VMHandler) DiskAdd(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: VM name is required"
|
||||
}
|
||||
|
||||
// Find VM
|
||||
vm, exists := h.vms[name]
|
||||
if !exists {
|
||||
return fmt.Sprintf("Error: VM '%s' not found", name)
|
||||
}
|
||||
|
||||
// Add disk
|
||||
size := params.Get("size")
|
||||
if size == "" {
|
||||
size = "10GB"
|
||||
}
|
||||
diskType := params.Get("type")
|
||||
if diskType == "" {
|
||||
diskType = "HDD"
|
||||
}
|
||||
|
||||
disk := Disk{
|
||||
Size: size,
|
||||
Type: diskType,
|
||||
}
|
||||
|
||||
vm.Disks = append(vm.Disks, disk)
|
||||
return fmt.Sprintf("Added %s %s disk to VM '%s'", size, diskType, name)
|
||||
}
|
||||
|
||||
// Delete handles the vm.delete action
|
||||
func (h *VMHandler) 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: VM name is required"
|
||||
}
|
||||
|
||||
// Find VM
|
||||
vm, exists := h.vms[name]
|
||||
if !exists {
|
||||
return fmt.Sprintf("Error: VM '%s' not found", name)
|
||||
}
|
||||
|
||||
// Check if VM is running and force flag is not set
|
||||
if vm.Running && !params.GetBool("force") {
|
||||
return fmt.Sprintf("Error: VM '%s' is running. Use force:true to delete anyway", name)
|
||||
}
|
||||
|
||||
// Delete VM
|
||||
delete(h.vms, name)
|
||||
return fmt.Sprintf("VM '%s' deleted successfully", name)
|
||||
}
|
||||
|
||||
// List handles the vm.list action
|
||||
func (h *VMHandler) List(script string) string {
|
||||
if len(h.vms) == 0 {
|
||||
return "No VMs defined"
|
||||
}
|
||||
|
||||
var result strings.Builder
|
||||
result.WriteString("Defined VMs:\n")
|
||||
|
||||
for _, vm := range h.vms {
|
||||
status := "stopped"
|
||||
if vm.Running {
|
||||
status = "running"
|
||||
}
|
||||
|
||||
result.WriteString(fmt.Sprintf("- %s (%s): %d CPU, %s memory, %s storage\n",
|
||||
vm.Name, status, vm.CPU, vm.Memory, vm.Storage))
|
||||
|
||||
if len(vm.Disks) > 0 {
|
||||
result.WriteString(" Attached disks:\n")
|
||||
for i, disk := range vm.Disks {
|
||||
result.WriteString(fmt.Sprintf(" %d. %s %s\n", i+1, disk.Size, disk.Type))
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
return result.String()
|
||||
}
|
||||
|
||||
// Help handles the vm.help action
|
||||
func (h *VMHandler) Help(script string) string {
|
||||
return GetVMTutorial()
|
||||
}
|
||||
|
||||
// Status handles the vm.status action
|
||||
func (h *VMHandler) 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: VM name is required"
|
||||
}
|
||||
|
||||
// Find VM
|
||||
vm, exists := h.vms[name]
|
||||
if !exists {
|
||||
return fmt.Sprintf("Error: VM '%s' not found", name)
|
||||
}
|
||||
|
||||
// Return VM status
|
||||
status := "stopped"
|
||||
if vm.Running {
|
||||
status = "running"
|
||||
}
|
||||
|
||||
var result strings.Builder
|
||||
result.WriteString(fmt.Sprintf("VM '%s' status:\n", name))
|
||||
result.WriteString(fmt.Sprintf("- Status: %s\n", status))
|
||||
result.WriteString(fmt.Sprintf("- CPU: %d\n", vm.CPU))
|
||||
result.WriteString(fmt.Sprintf("- Memory: %s\n", vm.Memory))
|
||||
result.WriteString(fmt.Sprintf("- Storage: %s\n", vm.Storage))
|
||||
|
||||
if vm.Description != "" {
|
||||
result.WriteString(fmt.Sprintf("- Description: %s\n", vm.Description))
|
||||
}
|
||||
|
||||
if len(vm.Disks) > 0 {
|
||||
result.WriteString("- Attached disks:\n")
|
||||
for i, disk := range vm.Disks {
|
||||
result.WriteString(fmt.Sprintf(" %d. %s %s\n", i+1, disk.Size, disk.Type))
|
||||
}
|
||||
}
|
||||
|
||||
return result.String()
|
||||
}
|
75
pkg2_dont_use/heroscript/cmd/vmhandler/vm_handler_server.go
Normal file
75
pkg2_dont_use/heroscript/cmd/vmhandler/vm_handler_server.go
Normal file
@@ -0,0 +1,75 @@
|
||||
package main
|
||||
|
||||
import (
|
||||
"fmt"
|
||||
"log"
|
||||
"os"
|
||||
"os/signal"
|
||||
"path/filepath"
|
||||
"syscall"
|
||||
|
||||
"git.ourworld.tf/herocode/heroagent/pkg/handlerfactory"
|
||||
)
|
||||
|
||||
// The tutorial functions are defined in tutorial.go
|
||||
|
||||
func main() {
|
||||
// Check if tutorial mode is requested
|
||||
addTutorialCommand()
|
||||
|
||||
fmt.Println("Starting VM Handler Example")
|
||||
|
||||
// Create a new handler factory
|
||||
factory := handlerfactory.NewHandlerFactory()
|
||||
|
||||
// Create and register the VM handler
|
||||
vmHandler := NewVMHandler()
|
||||
err := factory.RegisterHandler(vmHandler)
|
||||
if err != nil {
|
||||
log.Fatalf("Failed to register VM handler: %v", err)
|
||||
}
|
||||
|
||||
// Create a telnet server with the handler factory
|
||||
server := handlerfactory.NewTelnetServer(factory, "1234")
|
||||
|
||||
// Create socket directory if it doesn't exist
|
||||
socketDir := "/tmp"
|
||||
err = os.MkdirAll(socketDir, 0755)
|
||||
if err != nil {
|
||||
log.Fatalf("Failed to create socket directory: %v", err)
|
||||
}
|
||||
|
||||
// Start the telnet server on a Unix socket
|
||||
socketPath := filepath.Join(socketDir, "vmhandler.sock")
|
||||
err = server.Start(socketPath)
|
||||
if err != nil {
|
||||
log.Fatalf("Failed to start telnet server: %v", err)
|
||||
}
|
||||
fmt.Printf("Telnet server started on socket: %s\n", socketPath)
|
||||
fmt.Printf("Connect with: nc -U %s\n", socketPath)
|
||||
|
||||
// Also start on TCP port for easier access
|
||||
err = server.StartTCP("localhost:8024")
|
||||
if err != nil {
|
||||
log.Fatalf("Failed to start TCP telnet server: %v", err)
|
||||
}
|
||||
fmt.Println("Telnet server started on TCP: localhost:8024")
|
||||
fmt.Println("Connect with: telnet localhost 8024")
|
||||
|
||||
// Print available commands
|
||||
fmt.Println("\nVM Handler started. Type '!!vm.help' to see available commands.")
|
||||
fmt.Println("Authentication secret: 1234")
|
||||
|
||||
// Wait for interrupt signal
|
||||
sigChan := make(chan os.Signal, 1)
|
||||
signal.Notify(sigChan, syscall.SIGINT, syscall.SIGTERM)
|
||||
<-sigChan
|
||||
|
||||
// Stop the server
|
||||
fmt.Println("Stopping server...")
|
||||
err = server.Stop()
|
||||
if err != nil {
|
||||
log.Fatalf("Failed to stop telnet server: %v", err)
|
||||
}
|
||||
fmt.Println("Telnet server stopped")
|
||||
}
|
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)
|
||||
}
|
||||
}
|
137
pkg2_dont_use/heroscript/paramsparser/README.md
Normal file
137
pkg2_dont_use/heroscript/paramsparser/README.md
Normal file
@@ -0,0 +1,137 @@
|
||||
# ParamsParser
|
||||
|
||||
A Go package for parsing and manipulating parameters from text in a key-value format with support for multiline strings.
|
||||
|
||||
## Features
|
||||
|
||||
- Parse parameters in a natural format: `key: 'value' anotherKey: 'another value'`
|
||||
- Support for multiline string values
|
||||
- Support for numeric values without quotes: `port: 25`
|
||||
- Support for boolean-like values: `secure: 1`
|
||||
- Type conversion helpers (string, int, float, boolean)
|
||||
- Default value support
|
||||
- Required parameter validation with panic-on-missing options
|
||||
- Simple and intuitive API
|
||||
|
||||
## Usage
|
||||
|
||||
### Basic Usage
|
||||
|
||||
```go
|
||||
import (
|
||||
"git.ourworld.tf/herocode/heroagent/pkg/paramsparser"
|
||||
)
|
||||
|
||||
// Create a new parser
|
||||
parser := paramsparser.New()
|
||||
|
||||
// Parse a string with parameters
|
||||
inputStr := `
|
||||
name: 'myapp'
|
||||
host: 'localhost'
|
||||
port: 25
|
||||
secure: 1
|
||||
reset: 1
|
||||
description: '
|
||||
A multiline description
|
||||
for my application.
|
||||
'
|
||||
`
|
||||
|
||||
err := parser.Parse(inputStr)
|
||||
if err != nil {
|
||||
// Handle error
|
||||
}
|
||||
|
||||
// Or parse a simpler one-line string
|
||||
parser.ParseString("name: 'myapp' version: '1.0' active: 1")
|
||||
|
||||
// Set default values
|
||||
parser.SetDefault("host", "localhost")
|
||||
parser.SetDefault("port", "8080")
|
||||
|
||||
// Or set multiple defaults at once
|
||||
parser.SetDefaults(map[string]string{
|
||||
"debug": "false",
|
||||
"timeout": "30",
|
||||
})
|
||||
|
||||
// Get values with type conversion
|
||||
name := parser.Get("name")
|
||||
port := parser.GetIntDefault("port", 8080)
|
||||
secure := parser.GetBool("secure")
|
||||
```
|
||||
|
||||
### Type Conversion
|
||||
|
||||
```go
|
||||
// String value (with default if not found)
|
||||
value := parser.Get("key")
|
||||
|
||||
// Integer value
|
||||
intValue, err := parser.GetInt("key")
|
||||
// Or with default
|
||||
intValue := parser.GetIntDefault("key", 42)
|
||||
|
||||
// Float value
|
||||
floatValue, err := parser.GetFloat("key")
|
||||
// Or with default
|
||||
floatValue := parser.GetFloatDefault("key", 3.14)
|
||||
|
||||
// Boolean value (true, yes, 1, on are considered true)
|
||||
boolValue := parser.GetBool("key")
|
||||
// Or with default
|
||||
boolValue := parser.GetBoolDefault("key", false)
|
||||
```
|
||||
|
||||
### Required Parameters
|
||||
|
||||
```go
|
||||
// These will panic if the parameter is missing or invalid
|
||||
value := parser.MustGet("required_param")
|
||||
intValue := parser.MustGetInt("required_int_param")
|
||||
floatValue := parser.MustGetFloat("required_float_param")
|
||||
```
|
||||
|
||||
### Getting All Parameters
|
||||
|
||||
```go
|
||||
// Get all parameters (including defaults)
|
||||
allParams := parser.GetAll()
|
||||
for key, value := range allParams {
|
||||
fmt.Printf("%s = %s\n", key, value)
|
||||
}
|
||||
```
|
||||
|
||||
## Example Input Format
|
||||
|
||||
The parser supports the following format:
|
||||
|
||||
```
|
||||
name: 'myname' host: 'localhost'
|
||||
port: 25
|
||||
secure: 1
|
||||
reset: 1
|
||||
description: '
|
||||
a description can be multiline
|
||||
|
||||
like this
|
||||
'
|
||||
```
|
||||
|
||||
Key features of the format:
|
||||
- Keys are alphanumeric (plus underscore)
|
||||
- String values are enclosed in single quotes
|
||||
- Numeric values don't need quotes
|
||||
- Boolean values can be specified as 1/0
|
||||
- Multiline strings start with a single quote and continue until a closing quote is found
|
||||
|
||||
## Example
|
||||
|
||||
See the [example](./example/main.go) for a complete demonstration of how to use this package.
|
||||
|
||||
## Running Tests
|
||||
|
||||
```bash
|
||||
go test -v ./pkg/paramsparser
|
||||
```
|
84
pkg2_dont_use/heroscript/paramsparser/example/main.go
Normal file
84
pkg2_dont_use/heroscript/paramsparser/example/main.go
Normal file
@@ -0,0 +1,84 @@
|
||||
// Example usage of the paramsparser package
|
||||
package main
|
||||
|
||||
import (
|
||||
"fmt"
|
||||
|
||||
"git.ourworld.tf/herocode/heroagent/pkg/heroscript/paramsparser"
|
||||
)
|
||||
|
||||
func main() {
|
||||
// Create a new parser
|
||||
parser := paramsparser.New()
|
||||
|
||||
// Set some default values
|
||||
parser.SetDefaults(map[string]string{
|
||||
"host": "localhost",
|
||||
"port": "8080",
|
||||
"debug": "false",
|
||||
"timeout": "30",
|
||||
"greeting": "Hello, World!",
|
||||
})
|
||||
|
||||
// Parse a string in the specified format
|
||||
inputStr := `
|
||||
name: 'myapp'
|
||||
host: 'example.com'
|
||||
port: 25
|
||||
secure: 1
|
||||
reset: 1
|
||||
description: '
|
||||
This is a multiline description
|
||||
for my application.
|
||||
|
||||
It can span multiple lines.
|
||||
'
|
||||
`
|
||||
|
||||
err := parser.Parse(inputStr)
|
||||
if err != nil {
|
||||
fmt.Printf("Error parsing input: %v\n", err)
|
||||
return
|
||||
}
|
||||
|
||||
// Access parameters with type conversion
|
||||
name := parser.Get("name")
|
||||
host := parser.Get("host")
|
||||
port := parser.GetIntDefault("port", 8080)
|
||||
secure := parser.GetBool("secure")
|
||||
reset := parser.GetBool("reset")
|
||||
description := parser.Get("description")
|
||||
|
||||
fmt.Println("Configuration:")
|
||||
fmt.Printf(" Name: %s\n", name)
|
||||
fmt.Printf(" Host: %s\n", host)
|
||||
fmt.Printf(" Port: %d\n", port)
|
||||
fmt.Printf(" Secure: %t\n", secure)
|
||||
fmt.Printf(" Reset: %t\n", reset)
|
||||
fmt.Printf(" Description: %s\n", description)
|
||||
|
||||
// Get all parameters
|
||||
fmt.Println("\nAll parameters:")
|
||||
for key, value := range parser.GetAll() {
|
||||
if key == "description" {
|
||||
// Truncate long values for display
|
||||
if len(value) > 30 {
|
||||
value = value[:30] + "..."
|
||||
}
|
||||
}
|
||||
fmt.Printf(" %s = %s\n", key, value)
|
||||
}
|
||||
|
||||
// Example of using MustGet for required parameters
|
||||
if parser.Has("name") {
|
||||
fmt.Printf("\nRequired parameter: %s\n", parser.MustGet("name"))
|
||||
}
|
||||
|
||||
// Example of a simpler one-line parse
|
||||
simpleParser := paramsparser.New()
|
||||
simpleParser.ParseString("name: 'simple' version: '1.0' active: 1")
|
||||
fmt.Println("\nSimple parser results:")
|
||||
fmt.Printf(" Name: %s\n", simpleParser.Get("name"))
|
||||
fmt.Printf(" Version: %s\n", simpleParser.Get("version"))
|
||||
fmt.Printf(" Active: %t\n", simpleParser.GetBool("active"))
|
||||
}
|
447
pkg2_dont_use/heroscript/paramsparser/paramsparser.go
Normal file
447
pkg2_dont_use/heroscript/paramsparser/paramsparser.go
Normal file
@@ -0,0 +1,447 @@
|
||||
// Package paramsparser provides functionality for parsing and manipulating parameters
|
||||
// from text in a key-value format with support for multiline strings.
|
||||
package paramsparser
|
||||
|
||||
import (
|
||||
"errors"
|
||||
"fmt"
|
||||
"os"
|
||||
"strconv"
|
||||
"strings"
|
||||
|
||||
"git.ourworld.tf/herocode/heroagent/pkg/tools"
|
||||
)
|
||||
|
||||
// ParamsParser represents a parameter parser that can handle various parameter sources
|
||||
type ParamsParser struct {
|
||||
params map[string]string
|
||||
defaultParams map[string]string
|
||||
}
|
||||
|
||||
// New creates a new ParamsParser instance
|
||||
func New() *ParamsParser {
|
||||
return &ParamsParser{
|
||||
params: make(map[string]string),
|
||||
defaultParams: make(map[string]string),
|
||||
}
|
||||
}
|
||||
|
||||
// Parse parses a string containing key-value pairs in the format:
|
||||
// key:value or key:'value'
|
||||
// It supports multiline string values.
|
||||
func (p *ParamsParser) Parse(input string) error {
|
||||
// Normalize line endings
|
||||
input = strings.ReplaceAll(input, "\r\n", "\n")
|
||||
|
||||
// Track the current state
|
||||
var currentKey string
|
||||
var currentValue strings.Builder
|
||||
var inMultilineString bool
|
||||
|
||||
// Process each line
|
||||
lines := strings.Split(input, "\n")
|
||||
for i := 0; i < len(lines); i++ {
|
||||
// Only trim space for non-multiline string processing
|
||||
var line string
|
||||
if !inMultilineString {
|
||||
line = strings.TrimSpace(lines[i])
|
||||
} else {
|
||||
line = lines[i]
|
||||
}
|
||||
|
||||
// Skip empty lines unless we're in a multiline string
|
||||
if line == "" && !inMultilineString {
|
||||
continue
|
||||
}
|
||||
|
||||
// If we're in a multiline string
|
||||
if inMultilineString {
|
||||
// Check if this line ends the multiline string
|
||||
if strings.HasSuffix(line, "'") && !strings.HasSuffix(line, "\\'") {
|
||||
// Add the line without the closing quote
|
||||
currentValue.WriteString(line[:len(line)-1])
|
||||
p.params[currentKey] = currentValue.String()
|
||||
inMultilineString = false
|
||||
currentKey = ""
|
||||
currentValue.Reset()
|
||||
} else {
|
||||
// Continue the multiline string
|
||||
currentValue.WriteString(line)
|
||||
currentValue.WriteString("\n")
|
||||
}
|
||||
continue
|
||||
}
|
||||
|
||||
// Process the line to extract key-value pairs
|
||||
var processedPos int
|
||||
for processedPos < len(line) {
|
||||
// Skip leading whitespace
|
||||
for processedPos < len(line) && (line[processedPos] == ' ' || line[processedPos] == '\t') {
|
||||
processedPos++
|
||||
}
|
||||
|
||||
if processedPos >= len(line) {
|
||||
break
|
||||
}
|
||||
|
||||
// Find the next key by looking for a colon
|
||||
keyStart := processedPos
|
||||
colonPos := -1
|
||||
|
||||
for j := processedPos; j < len(line); j++ {
|
||||
if line[j] == ':' {
|
||||
colonPos = j
|
||||
break
|
||||
}
|
||||
}
|
||||
|
||||
if colonPos == -1 {
|
||||
// No colon found, skip this part
|
||||
break
|
||||
}
|
||||
|
||||
// Extract key and use NameFix to standardize it
|
||||
rawKey := strings.TrimSpace(line[keyStart:colonPos])
|
||||
key := tools.NameFix(rawKey)
|
||||
|
||||
if key == "" {
|
||||
// Invalid key, move past the colon and continue
|
||||
processedPos = colonPos + 1
|
||||
continue
|
||||
}
|
||||
|
||||
// Move position past the colon
|
||||
processedPos = colonPos + 1
|
||||
|
||||
if processedPos >= len(line) {
|
||||
// End of line reached, store empty value
|
||||
p.params[key] = ""
|
||||
break
|
||||
}
|
||||
|
||||
// Skip whitespace after the colon
|
||||
for processedPos < len(line) && (line[processedPos] == ' ' || line[processedPos] == '\t') {
|
||||
processedPos++
|
||||
}
|
||||
|
||||
if processedPos >= len(line) {
|
||||
// End of line reached after whitespace, store empty value
|
||||
p.params[key] = ""
|
||||
break
|
||||
}
|
||||
|
||||
// Check if the value is quoted
|
||||
if line[processedPos] == '\'' {
|
||||
// This is a quoted string
|
||||
processedPos++ // Skip the opening quote
|
||||
|
||||
// Look for the closing quote
|
||||
quoteEnd := -1
|
||||
for j := processedPos; j < len(line); j++ {
|
||||
// Check for escaped quote
|
||||
if line[j] == '\'' && (j == 0 || line[j-1] != '\\') {
|
||||
quoteEnd = j
|
||||
break
|
||||
}
|
||||
}
|
||||
|
||||
if quoteEnd != -1 {
|
||||
// Single-line quoted string
|
||||
value := line[processedPos:quoteEnd]
|
||||
// For quoted values, we preserve the original formatting
|
||||
// But for single-line values, we can apply NameFix if needed
|
||||
if key != "description" {
|
||||
value = tools.NameFix(value)
|
||||
}
|
||||
p.params[key] = value
|
||||
processedPos = quoteEnd + 1 // Move past the closing quote
|
||||
} else {
|
||||
// Start of multiline string
|
||||
currentKey = key
|
||||
currentValue.WriteString(line[processedPos:])
|
||||
currentValue.WriteString("\n")
|
||||
inMultilineString = true
|
||||
break
|
||||
}
|
||||
} else {
|
||||
// This is an unquoted value
|
||||
valueStart := processedPos
|
||||
valueEnd := valueStart
|
||||
|
||||
// Find the end of the value (space or end of line)
|
||||
for valueEnd < len(line) && line[valueEnd] != ' ' && line[valueEnd] != '\t' {
|
||||
valueEnd++
|
||||
}
|
||||
|
||||
value := line[valueStart:valueEnd]
|
||||
// For unquoted values, use NameFix to standardize them
|
||||
// This handles the 'without' keyword and other special cases
|
||||
p.params[key] = tools.NameFix(value)
|
||||
processedPos = valueEnd
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
// If we're still in a multiline string at the end, that's an error
|
||||
if inMultilineString {
|
||||
return errors.New("unterminated multiline string")
|
||||
}
|
||||
|
||||
return nil
|
||||
}
|
||||
|
||||
// ParseString is a simpler version that parses a string with the format:
|
||||
// key:value or key:'value'
|
||||
// This version doesn't support multiline strings and is optimized for one-line inputs
|
||||
func (p *ParamsParser) ParseString(input string) error {
|
||||
// Trim the input
|
||||
input = strings.TrimSpace(input)
|
||||
|
||||
// Process the input to extract key-value pairs
|
||||
var processedPos int
|
||||
for processedPos < len(input) {
|
||||
// Skip leading whitespace
|
||||
for processedPos < len(input) && (input[processedPos] == ' ' || input[processedPos] == '\t') {
|
||||
processedPos++
|
||||
}
|
||||
|
||||
if processedPos >= len(input) {
|
||||
break
|
||||
}
|
||||
|
||||
// Find the next key by looking for a colon
|
||||
keyStart := processedPos
|
||||
colonPos := -1
|
||||
|
||||
for j := processedPos; j < len(input); j++ {
|
||||
if input[j] == ':' {
|
||||
colonPos = j
|
||||
break
|
||||
}
|
||||
}
|
||||
|
||||
if colonPos == -1 {
|
||||
// No colon found, skip this part
|
||||
break
|
||||
}
|
||||
|
||||
// Extract key and use NameFix to standardize it
|
||||
rawKey := strings.TrimSpace(input[keyStart:colonPos])
|
||||
key := tools.NameFix(rawKey)
|
||||
|
||||
if key == "" {
|
||||
// Invalid key, move past the colon and continue
|
||||
processedPos = colonPos + 1
|
||||
continue
|
||||
}
|
||||
|
||||
// Move position past the colon
|
||||
processedPos = colonPos + 1
|
||||
|
||||
if processedPos >= len(input) {
|
||||
// End of input reached, store empty value
|
||||
p.params[key] = ""
|
||||
break
|
||||
}
|
||||
|
||||
// Skip whitespace after the colon
|
||||
for processedPos < len(input) && (input[processedPos] == ' ' || input[processedPos] == '\t') {
|
||||
processedPos++
|
||||
}
|
||||
|
||||
if processedPos >= len(input) {
|
||||
// End of input reached after whitespace, store empty value
|
||||
p.params[key] = ""
|
||||
break
|
||||
}
|
||||
|
||||
// Check if the value is quoted
|
||||
if input[processedPos] == '\'' {
|
||||
// This is a quoted string
|
||||
processedPos++ // Skip the opening quote
|
||||
|
||||
// Look for the closing quote
|
||||
quoteEnd := -1
|
||||
for j := processedPos; j < len(input); j++ {
|
||||
// Check for escaped quote
|
||||
if input[j] == '\'' && (j == 0 || input[j-1] != '\\') {
|
||||
quoteEnd = j
|
||||
break
|
||||
}
|
||||
}
|
||||
|
||||
if quoteEnd == -1 {
|
||||
return errors.New("unterminated quoted string")
|
||||
}
|
||||
|
||||
value := input[processedPos:quoteEnd]
|
||||
// For quoted values in ParseString, we can apply NameFix
|
||||
// since this method doesn't handle multiline strings
|
||||
if key != "description" {
|
||||
value = tools.NameFix(value)
|
||||
}
|
||||
p.params[key] = value
|
||||
processedPos = quoteEnd + 1 // Move past the closing quote
|
||||
} else {
|
||||
// This is an unquoted value
|
||||
valueStart := processedPos
|
||||
valueEnd := valueStart
|
||||
|
||||
// Find the end of the value (space or end of input)
|
||||
for valueEnd < len(input) && input[valueEnd] != ' ' && input[valueEnd] != '\t' {
|
||||
valueEnd++
|
||||
}
|
||||
|
||||
value := input[valueStart:valueEnd]
|
||||
// For unquoted values, use NameFix to standardize them
|
||||
// This handles the 'without' keyword and other special cases
|
||||
p.params[key] = tools.NameFix(value)
|
||||
processedPos = valueEnd
|
||||
}
|
||||
}
|
||||
|
||||
return nil
|
||||
}
|
||||
|
||||
// ParseFile parses a file containing key-value pairs
|
||||
func (p *ParamsParser) ParseFile(filename string) error {
|
||||
data, err := os.ReadFile(filename)
|
||||
if err != nil {
|
||||
return err
|
||||
}
|
||||
return p.Parse(string(data))
|
||||
}
|
||||
|
||||
// SetDefault sets a default value for a parameter
|
||||
func (p *ParamsParser) SetDefault(key, value string) {
|
||||
p.defaultParams[key] = value
|
||||
}
|
||||
|
||||
// SetDefaults sets multiple default values at once
|
||||
func (p *ParamsParser) SetDefaults(defaults map[string]string) {
|
||||
for k, v := range defaults {
|
||||
p.defaultParams[k] = v
|
||||
}
|
||||
}
|
||||
|
||||
// Set explicitly sets a parameter value
|
||||
func (p *ParamsParser) Set(key, value string) {
|
||||
p.params[key] = value
|
||||
}
|
||||
|
||||
// Get retrieves a parameter value, returning the default if not found
|
||||
func (p *ParamsParser) Get(key string) string {
|
||||
if value, exists := p.params[key]; exists {
|
||||
return value
|
||||
}
|
||||
if defaultValue, exists := p.defaultParams[key]; exists {
|
||||
return defaultValue
|
||||
}
|
||||
return ""
|
||||
}
|
||||
|
||||
// GetInt retrieves a parameter as an integer
|
||||
func (p *ParamsParser) GetInt(key string) (int, error) {
|
||||
value := p.Get(key)
|
||||
if value == "" {
|
||||
return 0, errors.New("parameter not found")
|
||||
}
|
||||
return strconv.Atoi(value)
|
||||
}
|
||||
|
||||
// GetIntDefault retrieves a parameter as an integer with a default value
|
||||
func (p *ParamsParser) GetIntDefault(key string, defaultValue int) int {
|
||||
value, err := p.GetInt(key)
|
||||
if err != nil {
|
||||
return defaultValue
|
||||
}
|
||||
return value
|
||||
}
|
||||
|
||||
// GetBool retrieves a parameter as a boolean
|
||||
func (p *ParamsParser) GetBool(key string) bool {
|
||||
value := p.Get(key)
|
||||
if value == "" {
|
||||
return false
|
||||
}
|
||||
|
||||
// Check for common boolean string representations
|
||||
value = strings.ToLower(value)
|
||||
return value == "true" || value == "yes" || value == "1" || value == "on"
|
||||
}
|
||||
|
||||
// GetBoolDefault retrieves a parameter as a boolean with a default value
|
||||
func (p *ParamsParser) GetBoolDefault(key string, defaultValue bool) bool {
|
||||
if !p.Has(key) {
|
||||
return defaultValue
|
||||
}
|
||||
return p.GetBool(key)
|
||||
}
|
||||
|
||||
// GetFloat retrieves a parameter as a float64
|
||||
func (p *ParamsParser) GetFloat(key string) (float64, error) {
|
||||
value := p.Get(key)
|
||||
if value == "" {
|
||||
return 0, errors.New("parameter not found")
|
||||
}
|
||||
return strconv.ParseFloat(value, 64)
|
||||
}
|
||||
|
||||
// GetFloatDefault retrieves a parameter as a float64 with a default value
|
||||
func (p *ParamsParser) GetFloatDefault(key string, defaultValue float64) float64 {
|
||||
value, err := p.GetFloat(key)
|
||||
if err != nil {
|
||||
return defaultValue
|
||||
}
|
||||
return value
|
||||
}
|
||||
|
||||
// Has checks if a parameter exists
|
||||
func (p *ParamsParser) Has(key string) bool {
|
||||
_, exists := p.params[key]
|
||||
return exists
|
||||
}
|
||||
|
||||
// GetAll returns all parameters as a map
|
||||
func (p *ParamsParser) GetAll() map[string]string {
|
||||
result := make(map[string]string)
|
||||
|
||||
// First add defaults
|
||||
for k, v := range p.defaultParams {
|
||||
result[k] = v
|
||||
}
|
||||
|
||||
// Then override with actual params
|
||||
for k, v := range p.params {
|
||||
result[k] = v
|
||||
}
|
||||
|
||||
return result
|
||||
}
|
||||
|
||||
// MustGet retrieves a parameter value, panicking if not found
|
||||
func (p *ParamsParser) MustGet(key string) string {
|
||||
value := p.Get(key)
|
||||
if value == "" {
|
||||
panic(fmt.Sprintf("required parameter '%s' not found", key))
|
||||
}
|
||||
return value
|
||||
}
|
||||
|
||||
// MustGetInt retrieves a parameter as an integer, panicking if not found or invalid
|
||||
func (p *ParamsParser) MustGetInt(key string) int {
|
||||
value, err := p.GetInt(key)
|
||||
if err != nil {
|
||||
panic(fmt.Sprintf("required integer parameter '%s' not found or invalid", key))
|
||||
}
|
||||
return value
|
||||
}
|
||||
|
||||
// MustGetFloat retrieves a parameter as a float64, panicking if not found or invalid
|
||||
func (p *ParamsParser) MustGetFloat(key string) float64 {
|
||||
value, err := p.GetFloat(key)
|
||||
if err != nil {
|
||||
panic(fmt.Sprintf("required float parameter '%s' not found or invalid", key))
|
||||
}
|
||||
return value
|
||||
}
|
226
pkg2_dont_use/heroscript/paramsparser/paramsparser_test.go
Normal file
226
pkg2_dont_use/heroscript/paramsparser/paramsparser_test.go
Normal file
@@ -0,0 +1,226 @@
|
||||
package paramsparser
|
||||
|
||||
import (
|
||||
"testing"
|
||||
)
|
||||
|
||||
func TestParamsParserBasic(t *testing.T) {
|
||||
input := "name:'myname' host:'localhost' port:25 secure:1 reset:1"
|
||||
parser := New()
|
||||
err := parser.ParseString(input)
|
||||
if err != nil {
|
||||
t.Fatalf("Failed to parse input: %v", err)
|
||||
}
|
||||
|
||||
tests := []struct {
|
||||
name string
|
||||
key string
|
||||
expected string
|
||||
}{
|
||||
{"string value", "name", "myname"},
|
||||
{"another string value", "host", "localhost"},
|
||||
{"numeric value", "port", "25"},
|
||||
{"boolean-like value", "secure", "1"},
|
||||
{"another boolean-like value", "reset", "1"},
|
||||
{"non-existent key", "nonexistent", ""},
|
||||
}
|
||||
|
||||
for _, tt := range tests {
|
||||
t.Run(tt.name, func(t *testing.T) {
|
||||
if got := parser.Get(tt.key); got != tt.expected {
|
||||
t.Errorf("ParamsParser.Get(%q) = %q, want %q", tt.key, got, tt.expected)
|
||||
}
|
||||
})
|
||||
}
|
||||
}
|
||||
|
||||
func TestParamsParserMultiline(t *testing.T) {
|
||||
input := "name:'myname' description:'\n\t\ta description can be multiline\n\n\t\tlike this\n'"
|
||||
parser := New()
|
||||
err := parser.Parse(input)
|
||||
if err != nil {
|
||||
t.Fatalf("Failed to parse input: %v", err)
|
||||
}
|
||||
|
||||
// Check the name parameter
|
||||
if got := parser.Get("name"); got != "myname" {
|
||||
t.Errorf("ParamsParser.Get(\"name\") = %q, want %q", got, "myname")
|
||||
}
|
||||
|
||||
// Check the multiline description
|
||||
expectedDesc := "\n\t\ta description can be multiline\n\n\t\tlike this\n"
|
||||
if got := parser.Get("description"); got != expectedDesc {
|
||||
t.Errorf("ParamsParser.Get(\"description\") = %q, want %q", got, expectedDesc)
|
||||
}
|
||||
}
|
||||
|
||||
func TestParamsParserDefaults(t *testing.T) {
|
||||
parser := New()
|
||||
parser.SetDefault("key1", "default1")
|
||||
parser.SetDefaults(map[string]string{
|
||||
"key2": "default2",
|
||||
"key3": "default3",
|
||||
})
|
||||
|
||||
// Override one default
|
||||
parser.Set("key2", "override")
|
||||
|
||||
tests := []struct {
|
||||
name string
|
||||
key string
|
||||
expected string
|
||||
}{
|
||||
{"default value", "key1", "default1"},
|
||||
{"overridden value", "key2", "override"},
|
||||
{"another default", "key3", "default3"},
|
||||
{"non-existent key", "key4", ""},
|
||||
}
|
||||
|
||||
for _, tt := range tests {
|
||||
t.Run(tt.name, func(t *testing.T) {
|
||||
if got := parser.Get(tt.key); got != tt.expected {
|
||||
t.Errorf("ParamsParser.Get(%q) = %q, want %q", tt.key, got, tt.expected)
|
||||
}
|
||||
})
|
||||
}
|
||||
}
|
||||
|
||||
func TestParamsParserTypes(t *testing.T) {
|
||||
parser := New()
|
||||
parser.Set("int", "123")
|
||||
parser.Set("float", "3.14")
|
||||
parser.Set("bool_true", "true")
|
||||
parser.Set("bool_yes", "yes")
|
||||
parser.Set("bool_1", "1")
|
||||
parser.Set("bool_false", "false")
|
||||
parser.Set("invalid_int", "not_an_int")
|
||||
parser.Set("invalid_float", "not_a_float")
|
||||
|
||||
t.Run("GetInt", func(t *testing.T) {
|
||||
if val, err := parser.GetInt("int"); err != nil || val != 123 {
|
||||
t.Errorf("GetInt(\"int\") = %d, %v, want 123, nil", val, err)
|
||||
}
|
||||
if val, err := parser.GetInt("invalid_int"); err == nil {
|
||||
t.Errorf("GetInt(\"invalid_int\") = %d, %v, want error", val, err)
|
||||
}
|
||||
if val, err := parser.GetInt("nonexistent"); err == nil {
|
||||
t.Errorf("GetInt(\"nonexistent\") = %d, %v, want error", val, err)
|
||||
}
|
||||
})
|
||||
|
||||
t.Run("GetIntDefault", func(t *testing.T) {
|
||||
if val := parser.GetIntDefault("int", 0); val != 123 {
|
||||
t.Errorf("GetIntDefault(\"int\", 0) = %d, want 123", val)
|
||||
}
|
||||
if val := parser.GetIntDefault("invalid_int", 42); val != 42 {
|
||||
t.Errorf("GetIntDefault(\"invalid_int\", 42) = %d, want 42", val)
|
||||
}
|
||||
if val := parser.GetIntDefault("nonexistent", 42); val != 42 {
|
||||
t.Errorf("GetIntDefault(\"nonexistent\", 42) = %d, want 42", val)
|
||||
}
|
||||
})
|
||||
|
||||
t.Run("GetFloat", func(t *testing.T) {
|
||||
if val, err := parser.GetFloat("float"); err != nil || val != 3.14 {
|
||||
t.Errorf("GetFloat(\"float\") = %f, %v, want 3.14, nil", val, err)
|
||||
}
|
||||
if val, err := parser.GetFloat("invalid_float"); err == nil {
|
||||
t.Errorf("GetFloat(\"invalid_float\") = %f, %v, want error", val, err)
|
||||
}
|
||||
if val, err := parser.GetFloat("nonexistent"); err == nil {
|
||||
t.Errorf("GetFloat(\"nonexistent\") = %f, %v, want error", val, err)
|
||||
}
|
||||
})
|
||||
|
||||
t.Run("GetFloatDefault", func(t *testing.T) {
|
||||
if val := parser.GetFloatDefault("float", 0.0); val != 3.14 {
|
||||
t.Errorf("GetFloatDefault(\"float\", 0.0) = %f, want 3.14", val)
|
||||
}
|
||||
if val := parser.GetFloatDefault("invalid_float", 2.71); val != 2.71 {
|
||||
t.Errorf("GetFloatDefault(\"invalid_float\", 2.71) = %f, want 2.71", val)
|
||||
}
|
||||
if val := parser.GetFloatDefault("nonexistent", 2.71); val != 2.71 {
|
||||
t.Errorf("GetFloatDefault(\"nonexistent\", 2.71) = %f, want 2.71", val)
|
||||
}
|
||||
})
|
||||
|
||||
t.Run("GetBool", func(t *testing.T) {
|
||||
if val := parser.GetBool("bool_true"); !val {
|
||||
t.Errorf("GetBool(\"bool_true\") = %v, want true", val)
|
||||
}
|
||||
if val := parser.GetBool("bool_yes"); !val {
|
||||
t.Errorf("GetBool(\"bool_yes\") = %v, want true", val)
|
||||
}
|
||||
if val := parser.GetBool("bool_1"); !val {
|
||||
t.Errorf("GetBool(\"bool_1\") = %v, want true", val)
|
||||
}
|
||||
if val := parser.GetBool("bool_false"); val {
|
||||
t.Errorf("GetBool(\"bool_false\") = %v, want false", val)
|
||||
}
|
||||
if val := parser.GetBool("nonexistent"); val {
|
||||
t.Errorf("GetBool(\"nonexistent\") = %v, want false", val)
|
||||
}
|
||||
})
|
||||
|
||||
t.Run("GetBoolDefault", func(t *testing.T) {
|
||||
if val := parser.GetBoolDefault("bool_true", false); !val {
|
||||
t.Errorf("GetBoolDefault(\"bool_true\", false) = %v, want true", val)
|
||||
}
|
||||
if val := parser.GetBoolDefault("nonexistent", true); !val {
|
||||
t.Errorf("GetBoolDefault(\"nonexistent\", true) = %v, want true", val)
|
||||
}
|
||||
})
|
||||
}
|
||||
|
||||
func TestParamsParserGetAll(t *testing.T) {
|
||||
parser := New()
|
||||
parser.SetDefault("key1", "default1")
|
||||
parser.SetDefault("key2", "default2")
|
||||
parser.Set("key2", "override")
|
||||
parser.Set("key3", "value3")
|
||||
|
||||
all := parser.GetAll()
|
||||
|
||||
expected := map[string]string{
|
||||
"key1": "default1",
|
||||
"key2": "override",
|
||||
"key3": "value3",
|
||||
}
|
||||
|
||||
if len(all) != len(expected) {
|
||||
t.Errorf("GetAll() returned map with %d entries, want %d", len(all), len(expected))
|
||||
}
|
||||
|
||||
for k, v := range expected {
|
||||
if all[k] != v {
|
||||
t.Errorf("GetAll()[%q] = %q, want %q", k, all[k], v)
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
func TestParamsParserMustGet(t *testing.T) {
|
||||
parser := New()
|
||||
parser.Set("key", "value")
|
||||
parser.Set("int", "123")
|
||||
parser.Set("float", "3.14")
|
||||
|
||||
defer func() {
|
||||
if r := recover(); r == nil {
|
||||
t.Errorf("MustGet on non-existent key did not panic")
|
||||
}
|
||||
}()
|
||||
|
||||
// These should not panic
|
||||
if val := parser.MustGet("key"); val != "value" {
|
||||
t.Errorf("MustGet(\"key\") = %q, want \"value\"", val)
|
||||
}
|
||||
if val := parser.MustGetInt("int"); val != 123 {
|
||||
t.Errorf("MustGetInt(\"int\") = %d, want 123", val)
|
||||
}
|
||||
if val := parser.MustGetFloat("float"); val != 3.14 {
|
||||
t.Errorf("MustGetFloat(\"float\") = %f, want 3.14", val)
|
||||
}
|
||||
|
||||
// This should panic
|
||||
parser.MustGet("nonexistent")
|
||||
}
|
212
pkg2_dont_use/heroscript/playbook/parser.go
Normal file
212
pkg2_dont_use/heroscript/playbook/parser.go
Normal file
@@ -0,0 +1,212 @@
|
||||
package playbook
|
||||
|
||||
import (
|
||||
"strings"
|
||||
|
||||
"git.ourworld.tf/herocode/heroagent/pkg/heroscript/paramsparser"
|
||||
"git.ourworld.tf/herocode/heroagent/pkg/tools"
|
||||
)
|
||||
|
||||
// State represents the parser state
|
||||
type State int
|
||||
|
||||
const (
|
||||
StateStart State = iota
|
||||
StateCommentForActionMaybe
|
||||
StateAction
|
||||
StateOtherText
|
||||
)
|
||||
|
||||
// PlayBookOptions contains options for creating a new PlayBook
|
||||
type PlayBookOptions struct {
|
||||
Text string
|
||||
Path string
|
||||
GitURL string
|
||||
GitPull bool
|
||||
GitBranch string
|
||||
GitReset bool
|
||||
Priority int
|
||||
}
|
||||
|
||||
// AddText adds heroscript text to the playbook
|
||||
func (p *PlayBook) AddText(text string, priority int) error {
|
||||
// Normalize text
|
||||
text = strings.ReplaceAll(text, "\t", " ")
|
||||
|
||||
var state State = StateStart
|
||||
var action *Action
|
||||
var comments []string
|
||||
var paramsData []string
|
||||
|
||||
// Process each line
|
||||
lines := strings.Split(text, "\n")
|
||||
for _, line := range lines {
|
||||
lineStrip := strings.TrimSpace(line)
|
||||
|
||||
if lineStrip == "" {
|
||||
continue
|
||||
}
|
||||
|
||||
// Handle action state
|
||||
if state == StateAction {
|
||||
if !strings.HasPrefix(line, " ") || lineStrip == "" || strings.HasPrefix(lineStrip, "!") {
|
||||
state = StateStart
|
||||
// End of action, parse params
|
||||
if len(paramsData) > 0 {
|
||||
params := strings.Join(paramsData, "\n")
|
||||
err := action.Params.Parse(params)
|
||||
if err != nil {
|
||||
return err
|
||||
}
|
||||
// Remove ID from params if present
|
||||
delete(action.Params.GetAll(), "id")
|
||||
}
|
||||
comments = []string{}
|
||||
paramsData = []string{}
|
||||
action = nil
|
||||
} else {
|
||||
paramsData = append(paramsData, line)
|
||||
}
|
||||
}
|
||||
|
||||
// Handle comment state
|
||||
if state == StateCommentForActionMaybe {
|
||||
if strings.HasPrefix(lineStrip, "//") {
|
||||
comments = append(comments, strings.TrimLeft(lineStrip, "/ "))
|
||||
} else {
|
||||
if strings.HasPrefix(lineStrip, "!") {
|
||||
state = StateStart
|
||||
} else {
|
||||
state = StateStart
|
||||
p.OtherText += strings.Join(comments, "\n")
|
||||
if !strings.HasSuffix(p.OtherText, "\n") {
|
||||
p.OtherText += "\n"
|
||||
}
|
||||
comments = []string{}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
// Handle start state
|
||||
if state == StateStart {
|
||||
if strings.HasPrefix(lineStrip, "!") && !strings.HasPrefix(lineStrip, "![") {
|
||||
// Start a new action
|
||||
state = StateAction
|
||||
|
||||
// Create new action
|
||||
action = &Action{
|
||||
ID: p.NrActions + 1,
|
||||
Priority: priority,
|
||||
Params: paramsparser.New(),
|
||||
Result: paramsparser.New(),
|
||||
}
|
||||
p.NrActions++
|
||||
|
||||
// Set comments
|
||||
action.Comments = strings.Join(comments, "\n")
|
||||
comments = []string{}
|
||||
paramsData = []string{}
|
||||
|
||||
// Parse action name
|
||||
actionName := lineStrip
|
||||
if strings.Contains(lineStrip, " ") {
|
||||
actionName = strings.TrimSpace(strings.Split(lineStrip, " ")[0])
|
||||
params := strings.TrimSpace(strings.Join(strings.Split(lineStrip, " ")[1:], " "))
|
||||
if params != "" {
|
||||
paramsData = append(paramsData, params)
|
||||
}
|
||||
}
|
||||
|
||||
// Determine action type
|
||||
if strings.HasPrefix(actionName, "!!!!!") {
|
||||
return ErrInvalidActionPrefix
|
||||
} else if strings.HasPrefix(actionName, "!!!!") {
|
||||
action.ActionType = ActionTypeWAL
|
||||
} else if strings.HasPrefix(actionName, "!!!") {
|
||||
action.ActionType = ActionTypeMacro
|
||||
} else if strings.HasPrefix(actionName, "!!") {
|
||||
action.ActionType = ActionTypeSAL
|
||||
} else if strings.HasPrefix(actionName, "!") {
|
||||
action.ActionType = ActionTypeDAL
|
||||
}
|
||||
|
||||
// Remove prefix
|
||||
actionName = strings.TrimLeft(actionName, "!")
|
||||
|
||||
// Split into actor and action name
|
||||
parts := strings.Split(actionName, ".")
|
||||
if len(parts) == 1 {
|
||||
action.Actor = "core"
|
||||
action.Name = tools.NameFix(parts[0])
|
||||
} else if len(parts) == 2 {
|
||||
action.Actor = tools.NameFix(parts[0])
|
||||
action.Name = tools.NameFix(parts[1])
|
||||
} else {
|
||||
return ErrInvalidActionName
|
||||
}
|
||||
|
||||
// Add action to playbook
|
||||
p.Actions = append(p.Actions, action)
|
||||
|
||||
continue
|
||||
} else if strings.HasPrefix(lineStrip, "//") {
|
||||
state = StateCommentForActionMaybe
|
||||
comments = append(comments, strings.TrimLeft(lineStrip, "/ "))
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
// Process the last action if needed
|
||||
if state == StateAction && action != nil && action.ID != 0 {
|
||||
if len(paramsData) > 0 {
|
||||
params := strings.Join(paramsData, "\n")
|
||||
err := action.Params.Parse(params)
|
||||
if err != nil {
|
||||
return err
|
||||
}
|
||||
// Remove ID from params if present
|
||||
delete(action.Params.GetAll(), "id")
|
||||
}
|
||||
}
|
||||
|
||||
// Process the last comment if needed
|
||||
if state == StateCommentForActionMaybe && len(comments) > 0 {
|
||||
p.OtherText += strings.Join(comments, "\n")
|
||||
}
|
||||
|
||||
return nil
|
||||
}
|
||||
|
||||
// NewFromFile creates a new PlayBook from a file
|
||||
func NewFromFile(path string, priority int) (*PlayBook, error) {
|
||||
// This is a simplified version - in a real implementation, you'd read the file
|
||||
// and handle different file types (md, hero, etc.)
|
||||
|
||||
// For now, we'll just create an empty playbook
|
||||
pb := New()
|
||||
|
||||
// TODO: Implement file reading and parsing
|
||||
|
||||
return pb, nil
|
||||
}
|
||||
|
||||
// Errors
|
||||
var (
|
||||
ErrInvalidActionPrefix = NewError("invalid action prefix")
|
||||
ErrInvalidActionName = NewError("invalid action name")
|
||||
)
|
||||
|
||||
// NewError creates a new error
|
||||
func NewError(msg string) error {
|
||||
return &PlayBookError{msg}
|
||||
}
|
||||
|
||||
// PlayBookError represents a playbook error
|
||||
type PlayBookError struct {
|
||||
Msg string
|
||||
}
|
||||
|
||||
// Error returns the error message
|
||||
func (e *PlayBookError) Error() string {
|
||||
return e.Msg
|
||||
}
|
301
pkg2_dont_use/heroscript/playbook/playbook.go
Normal file
301
pkg2_dont_use/heroscript/playbook/playbook.go
Normal file
@@ -0,0 +1,301 @@
|
||||
package playbook
|
||||
|
||||
import (
|
||||
"crypto/sha1"
|
||||
"encoding/hex"
|
||||
"fmt"
|
||||
"sort"
|
||||
"strings"
|
||||
|
||||
"git.ourworld.tf/herocode/heroagent/pkg/heroscript/paramsparser"
|
||||
)
|
||||
|
||||
// ActionType represents the type of action
|
||||
type ActionType int
|
||||
|
||||
const (
|
||||
ActionTypeUnknown ActionType = iota
|
||||
ActionTypeDAL
|
||||
ActionTypeSAL
|
||||
ActionTypeWAL
|
||||
ActionTypeMacro
|
||||
)
|
||||
|
||||
// Action represents a single action in a heroscript
|
||||
type Action struct {
|
||||
ID int
|
||||
CID string
|
||||
Name string
|
||||
Actor string
|
||||
Priority int
|
||||
Params *paramsparser.ParamsParser
|
||||
Result *paramsparser.ParamsParser
|
||||
ActionType ActionType
|
||||
Comments string
|
||||
Done bool
|
||||
}
|
||||
|
||||
// PlayBook represents a collection of actions
|
||||
type PlayBook struct {
|
||||
Actions []*Action
|
||||
Priorities map[int][]int // key is priority, value is list of action indices
|
||||
OtherText string // text outside of actions
|
||||
Result string
|
||||
NrActions int
|
||||
Done []int
|
||||
}
|
||||
|
||||
// NewAction creates a new action and adds it to the playbook
|
||||
func (p *PlayBook) NewAction(cid, name, actor string, priority int, actionType ActionType) *Action {
|
||||
p.NrActions++
|
||||
action := &Action{
|
||||
ID: p.NrActions,
|
||||
CID: cid,
|
||||
Name: name,
|
||||
Actor: actor,
|
||||
Priority: priority,
|
||||
ActionType: actionType,
|
||||
Params: paramsparser.New(),
|
||||
Result: paramsparser.New(),
|
||||
}
|
||||
p.Actions = append(p.Actions, action)
|
||||
return action
|
||||
}
|
||||
|
||||
// New creates a new PlayBook
|
||||
func New() *PlayBook {
|
||||
return &PlayBook{
|
||||
Actions: make([]*Action, 0),
|
||||
Priorities: make(map[int][]int),
|
||||
NrActions: 0,
|
||||
Done: make([]int, 0),
|
||||
}
|
||||
}
|
||||
|
||||
// NewFromText creates a new PlayBook from heroscript text
|
||||
func NewFromText(text string) (*PlayBook, error) {
|
||||
pb := New()
|
||||
err := pb.AddText(text, 10) // Default priority 10
|
||||
if err != nil {
|
||||
return nil, err
|
||||
}
|
||||
return pb, nil
|
||||
}
|
||||
|
||||
// String returns the heroscript representation of the action
|
||||
func (a *Action) String() string {
|
||||
out := a.HeroScript()
|
||||
if a.Result != nil && len(a.Result.GetAll()) > 0 {
|
||||
out += "\n\nResult:\n"
|
||||
// Indent the result
|
||||
resultParams := a.Result.GetAll()
|
||||
for k, v := range resultParams {
|
||||
out += " " + k + ": '" + v + "'\n"
|
||||
}
|
||||
}
|
||||
return out
|
||||
}
|
||||
|
||||
// HeroScript returns the heroscript representation of the action
|
||||
func (a *Action) HeroScript() string {
|
||||
var out strings.Builder
|
||||
|
||||
// Add comments if any
|
||||
if a.Comments != "" {
|
||||
lines := strings.Split(a.Comments, "\n")
|
||||
for _, line := range lines {
|
||||
out.WriteString("// " + line + "\n")
|
||||
}
|
||||
}
|
||||
|
||||
// Add action type prefix
|
||||
switch a.ActionType {
|
||||
case ActionTypeDAL:
|
||||
out.WriteString("!")
|
||||
case ActionTypeSAL:
|
||||
out.WriteString("!!")
|
||||
case ActionTypeMacro:
|
||||
out.WriteString("!!!")
|
||||
default:
|
||||
out.WriteString("!!") // Default to SAL
|
||||
}
|
||||
|
||||
// Add actor and name
|
||||
if a.Actor != "" {
|
||||
out.WriteString(a.Actor + ".")
|
||||
}
|
||||
out.WriteString(a.Name + " ")
|
||||
|
||||
// Add ID if present
|
||||
if a.ID > 0 {
|
||||
out.WriteString(fmt.Sprintf("id:%d ", a.ID))
|
||||
}
|
||||
|
||||
// Add parameters
|
||||
if a.Params != nil && len(a.Params.GetAll()) > 0 {
|
||||
params := a.Params.GetAll()
|
||||
firstLine := true
|
||||
for k, v := range params {
|
||||
if firstLine {
|
||||
out.WriteString(k + ":'" + v + "'\n")
|
||||
firstLine = false
|
||||
} else {
|
||||
out.WriteString(" " + k + ":'" + v + "'\n")
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
return out.String()
|
||||
}
|
||||
|
||||
// HashKey returns a unique hash for the action
|
||||
func (a *Action) HashKey() string {
|
||||
h := sha1.New()
|
||||
h.Write([]byte(a.HeroScript()))
|
||||
return hex.EncodeToString(h.Sum(nil))
|
||||
}
|
||||
|
||||
// HashKey returns a unique hash for the playbook
|
||||
func (p *PlayBook) HashKey() string {
|
||||
h := sha1.New()
|
||||
for _, action := range p.Actions {
|
||||
h.Write([]byte(action.HashKey()))
|
||||
}
|
||||
return hex.EncodeToString(h.Sum(nil))
|
||||
}
|
||||
|
||||
// HeroScript returns the heroscript representation of the playbook
|
||||
func (p *PlayBook) HeroScript(showDone bool) string {
|
||||
var out strings.Builder
|
||||
|
||||
actions, _ := p.ActionsSorted(false)
|
||||
for _, action := range actions {
|
||||
if !showDone && action.Done {
|
||||
continue
|
||||
}
|
||||
out.WriteString(action.HeroScript() + "\n")
|
||||
}
|
||||
|
||||
if p.OtherText != "" {
|
||||
out.WriteString(p.OtherText)
|
||||
}
|
||||
|
||||
return out.String()
|
||||
}
|
||||
|
||||
// ActionsSorted returns the actions sorted by priority
|
||||
func (p *PlayBook) ActionsSorted(prioOnly bool) ([]*Action, error) {
|
||||
var result []*Action
|
||||
|
||||
// If no priorities are set, return all actions
|
||||
if len(p.Priorities) == 0 {
|
||||
return p.Actions, nil
|
||||
}
|
||||
|
||||
// Get all priority numbers and sort them
|
||||
var priorities []int
|
||||
for prio := range p.Priorities {
|
||||
priorities = append(priorities, prio)
|
||||
}
|
||||
sort.Ints(priorities)
|
||||
|
||||
// Add actions in priority order
|
||||
for _, prio := range priorities {
|
||||
if prioOnly && prio > 49 {
|
||||
continue
|
||||
}
|
||||
|
||||
actionIDs := p.Priorities[prio]
|
||||
for _, id := range actionIDs {
|
||||
action, err := p.GetAction(id, "", "")
|
||||
if err != nil {
|
||||
return nil, err
|
||||
}
|
||||
result = append(result, action)
|
||||
}
|
||||
}
|
||||
|
||||
return result, nil
|
||||
}
|
||||
|
||||
// GetAction finds an action by ID, actor, or name
|
||||
func (p *PlayBook) GetAction(id int, actor, name string) (*Action, error) {
|
||||
actions, err := p.FindActions(id, actor, name, ActionTypeUnknown)
|
||||
if err != nil {
|
||||
return nil, err
|
||||
}
|
||||
|
||||
if len(actions) == 1 {
|
||||
return actions[0], nil
|
||||
} else if len(actions) == 0 {
|
||||
return nil, fmt.Errorf("couldn't find action with id: %d, actor: %s, name: %s", id, actor, name)
|
||||
} else {
|
||||
return nil, fmt.Errorf("multiple actions found with id: %d, actor: %s, name: %s", id, actor, name)
|
||||
}
|
||||
}
|
||||
|
||||
// FindActions finds actions based on criteria
|
||||
func (p *PlayBook) FindActions(id int, actor, name string, actionType ActionType) ([]*Action, error) {
|
||||
var result []*Action
|
||||
|
||||
for _, a := range p.Actions {
|
||||
// If ID is specified, return only the action with that ID
|
||||
if id != 0 {
|
||||
if a.ID == id {
|
||||
return []*Action{a}, nil
|
||||
}
|
||||
continue
|
||||
}
|
||||
|
||||
// Filter by actor if specified
|
||||
if actor != "" && a.Actor != actor {
|
||||
continue
|
||||
}
|
||||
|
||||
// Filter by name if specified
|
||||
if name != "" && a.Name != name {
|
||||
continue
|
||||
}
|
||||
|
||||
// Filter by actionType if specified
|
||||
if actionType != ActionTypeUnknown && a.ActionType != actionType {
|
||||
continue
|
||||
}
|
||||
|
||||
// If the action passes all filters, add it to the result
|
||||
result = append(result, a)
|
||||
}
|
||||
|
||||
return result, nil
|
||||
}
|
||||
|
||||
// ActionExists checks if an action exists
|
||||
func (p *PlayBook) ActionExists(id int, actor, name string) bool {
|
||||
actions, err := p.FindActions(id, actor, name, ActionTypeUnknown)
|
||||
if err != nil || len(actions) == 0 {
|
||||
return false
|
||||
}
|
||||
return true
|
||||
}
|
||||
|
||||
// String returns a string representation of the playbook
|
||||
func (p *PlayBook) String() string {
|
||||
return p.HeroScript(true)
|
||||
}
|
||||
|
||||
// EmptyCheck checks if there are any actions left to execute
|
||||
func (p *PlayBook) EmptyCheck() error {
|
||||
var undoneActions []*Action
|
||||
|
||||
for _, a := range p.Actions {
|
||||
if !a.Done {
|
||||
undoneActions = append(undoneActions, a)
|
||||
}
|
||||
}
|
||||
|
||||
if len(undoneActions) > 0 {
|
||||
return fmt.Errorf("there are actions left to execute: %d", len(undoneActions))
|
||||
}
|
||||
|
||||
return nil
|
||||
}
|
211
pkg2_dont_use/heroscript/playbook/playbook_test.go
Normal file
211
pkg2_dont_use/heroscript/playbook/playbook_test.go
Normal file
@@ -0,0 +1,211 @@
|
||||
package playbook
|
||||
|
||||
import (
|
||||
"strings"
|
||||
"testing"
|
||||
)
|
||||
|
||||
const testText1 = `
|
||||
//comment for the action
|
||||
!!mailclient.configure host:localhost
|
||||
name: 'myname'
|
||||
port:25
|
||||
secure: 1
|
||||
reset:1
|
||||
description:'
|
||||
a description can be multiline
|
||||
|
||||
like this
|
||||
'
|
||||
`
|
||||
|
||||
func TestParse(t *testing.T) {
|
||||
pb, err := NewFromText(testText1)
|
||||
if err != nil {
|
||||
t.Fatalf("Failed to parse text: %v", err)
|
||||
}
|
||||
|
||||
if len(pb.Actions) != 1 {
|
||||
t.Errorf("Expected 1 action, got %d", len(pb.Actions))
|
||||
}
|
||||
|
||||
action := pb.Actions[0]
|
||||
if action.Actor != "mailclient" {
|
||||
t.Errorf("Expected actor 'mailclient', got '%s'", action.Actor)
|
||||
}
|
||||
|
||||
if action.Name != "configure" {
|
||||
t.Errorf("Expected name 'configure', got '%s'", action.Name)
|
||||
}
|
||||
|
||||
if action.Comments != "comment for the action" {
|
||||
t.Errorf("Expected comment 'comment for the action', got '%s'", action.Comments)
|
||||
}
|
||||
|
||||
// Test params
|
||||
name := action.Params.Get("name")
|
||||
if name != "myname" {
|
||||
t.Errorf("Expected name 'myname', got '%s'", name)
|
||||
}
|
||||
|
||||
host := action.Params.Get("host")
|
||||
if host != "localhost" {
|
||||
t.Errorf("Expected host 'localhost', got '%s'", host)
|
||||
}
|
||||
|
||||
port, err := action.Params.GetInt("port")
|
||||
if err != nil || port != 25 {
|
||||
t.Errorf("Expected port 25, got %d, error: %v", port, err)
|
||||
}
|
||||
|
||||
secure := action.Params.GetBool("secure")
|
||||
if !secure {
|
||||
t.Errorf("Expected secure to be true, got false")
|
||||
}
|
||||
|
||||
reset := action.Params.GetBool("reset")
|
||||
if !reset {
|
||||
t.Errorf("Expected reset to be true, got false")
|
||||
}
|
||||
|
||||
// Test multiline description
|
||||
desc := action.Params.Get("description")
|
||||
// Just check that the description contains the expected text
|
||||
if !strings.Contains(desc, "a description can be multiline") || !strings.Contains(desc, "like this") {
|
||||
t.Errorf("Description doesn't contain expected content: '%s'", desc)
|
||||
}
|
||||
}
|
||||
|
||||
func TestHeroScript(t *testing.T) {
|
||||
pb, err := NewFromText(testText1)
|
||||
if err != nil {
|
||||
t.Fatalf("Failed to parse text: %v", err)
|
||||
}
|
||||
|
||||
// Generate heroscript
|
||||
script := pb.HeroScript(true)
|
||||
|
||||
// Parse the generated script again
|
||||
pb2, err := NewFromText(script)
|
||||
if err != nil {
|
||||
t.Fatalf("Failed to parse generated script: %v", err)
|
||||
}
|
||||
|
||||
// Verify the actions are the same
|
||||
if len(pb2.Actions) != len(pb.Actions) {
|
||||
t.Errorf("Expected %d actions, got %d", len(pb.Actions), len(pb2.Actions))
|
||||
}
|
||||
|
||||
// Verify the actions have the same actor and name
|
||||
if pb.Actions[0].Actor != pb2.Actions[0].Actor || pb.Actions[0].Name != pb2.Actions[0].Name {
|
||||
t.Errorf("Actions don't match: %s.%s vs %s.%s",
|
||||
pb.Actions[0].Actor, pb.Actions[0].Name,
|
||||
pb2.Actions[0].Actor, pb2.Actions[0].Name)
|
||||
}
|
||||
|
||||
// Verify the parameters are the same
|
||||
params1 := pb.Actions[0].Params.GetAll()
|
||||
params2 := pb2.Actions[0].Params.GetAll()
|
||||
|
||||
// Check that all keys in params1 exist in params2
|
||||
for k, v1 := range params1 {
|
||||
v2, exists := params2[k]
|
||||
if !exists {
|
||||
t.Errorf("Key %s missing in generated script", k)
|
||||
continue
|
||||
}
|
||||
|
||||
// For multiline strings, just check that they contain the same content
|
||||
if strings.Contains(v1, "\n") {
|
||||
if !strings.Contains(v2, "description") || !strings.Contains(v2, "multiline") {
|
||||
t.Errorf("Multiline value for key %s doesn't match: '%s' vs '%s'", k, v1, v2)
|
||||
}
|
||||
} else if v1 != v2 {
|
||||
t.Errorf("Value for key %s doesn't match: '%s' vs '%s'", k, v1, v2)
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
func TestSpacedValues(t *testing.T) {
|
||||
const spacedValuesText = `
|
||||
!!mailclient.configure
|
||||
name: 'myname'
|
||||
host: 'localhost'
|
||||
port: 25
|
||||
secure: 1
|
||||
description: 'This is a description'
|
||||
`
|
||||
|
||||
pb, err := NewFromText(spacedValuesText)
|
||||
if err != nil {
|
||||
t.Fatalf("Failed to parse text with spaces between colon and quoted values: %v", err)
|
||||
}
|
||||
|
||||
if len(pb.Actions) != 1 {
|
||||
t.Errorf("Expected 1 action, got %d", len(pb.Actions))
|
||||
}
|
||||
|
||||
action := pb.Actions[0]
|
||||
if action.Actor != "mailclient" || action.Name != "configure" {
|
||||
t.Errorf("Action incorrect: %s.%s", action.Actor, action.Name)
|
||||
}
|
||||
|
||||
// Test params with spaces after colon
|
||||
name := action.Params.Get("name")
|
||||
if name != "myname" {
|
||||
t.Errorf("Expected name 'myname', got '%s'", name)
|
||||
}
|
||||
|
||||
host := action.Params.Get("host")
|
||||
if host != "localhost" {
|
||||
t.Errorf("Expected host 'localhost', got '%s'", host)
|
||||
}
|
||||
|
||||
desc := action.Params.Get("description")
|
||||
if desc != "This is a description" {
|
||||
t.Errorf("Expected description 'This is a description', got '%s'", desc)
|
||||
}
|
||||
}
|
||||
|
||||
func TestMultipleActions(t *testing.T) {
|
||||
const multipleActionsText = `
|
||||
!!mailclient.configure
|
||||
name:'myname'
|
||||
host:'localhost'
|
||||
|
||||
!!system.update
|
||||
force:1
|
||||
packages:'git,curl,wget'
|
||||
`
|
||||
|
||||
pb, err := NewFromText(multipleActionsText)
|
||||
if err != nil {
|
||||
t.Fatalf("Failed to parse text: %v", err)
|
||||
}
|
||||
|
||||
if len(pb.Actions) != 2 {
|
||||
t.Errorf("Expected 2 actions, got %d", len(pb.Actions))
|
||||
}
|
||||
|
||||
// Check first action
|
||||
action1 := pb.Actions[0]
|
||||
if action1.Actor != "mailclient" || action1.Name != "configure" {
|
||||
t.Errorf("First action incorrect: %s.%s", action1.Actor, action1.Name)
|
||||
}
|
||||
|
||||
// Check second action
|
||||
action2 := pb.Actions[1]
|
||||
if action2.Actor != "system" || action2.Name != "update" {
|
||||
t.Errorf("Second action incorrect: %s.%s", action2.Actor, action2.Name)
|
||||
}
|
||||
|
||||
force := action2.Params.GetBool("force")
|
||||
if !force {
|
||||
t.Errorf("Expected force to be true, got false")
|
||||
}
|
||||
|
||||
packages := action2.Params.Get("packages")
|
||||
if packages != "git,curl,wget" {
|
||||
t.Errorf("Expected packages 'git,curl,wget', got '%s'", packages)
|
||||
}
|
||||
}
|
Reference in New Issue
Block a user