This commit is contained in:
2025-05-23 15:40:41 +04:00
parent 0e545e56de
commit 532cda72d3
126 changed files with 0 additions and 0 deletions

View File

@@ -0,0 +1,127 @@
package api
import (
"fmt"
"time"
"git.ourworld.tf/herocode/heroagent/pkg/system/stats"
"github.com/gofiber/fiber/v2"
)
// UptimeProvider defines an interface for getting system uptime
type UptimeProvider interface {
GetUptime() string
}
// AdminHandler handles admin-related API routes
type AdminHandler struct {
uptimeProvider UptimeProvider
statsManager *stats.StatsManager
}
// NewAdminHandler creates a new AdminHandler
func NewAdminHandler(uptimeProvider UptimeProvider, statsManager *stats.StatsManager) *AdminHandler {
// If statsManager is nil, create a new one with default settings
if statsManager == nil {
var err error
statsManager, err = stats.NewStatsManagerWithDefaults()
if err != nil {
// Log the error but continue with nil statsManager
fmt.Printf("Error creating StatsManager: %v\n", err)
}
}
return &AdminHandler{
uptimeProvider: uptimeProvider,
statsManager: statsManager,
}
}
// RegisterRoutes registers all admin API routes
func (h *AdminHandler) RegisterRoutes(app *fiber.App) {
// API endpoints
admin := app.Group("/api")
// @Summary Get hardware stats
// @Description Get hardware statistics in JSON format
// @Tags admin
// @Accept json
// @Produce json
// @Success 200 {object} map[string]interface{}
// @Failure 500 {object} ErrorResponse
// @Router /api/hardware-stats [get]
admin.Get("/hardware-stats", h.getHardwareStatsJSON)
// @Summary Get process stats
// @Description Get process statistics in JSON format
// @Tags admin
// @Accept json
// @Produce json
// @Success 200 {object} map[string]interface{}
// @Failure 500 {object} ErrorResponse
// @Router /api/process-stats [get]
admin.Get("/process-stats", h.getProcessStatsJSON)
}
// getProcessStatsJSON returns process statistics in JSON format for API consumption
func (h *AdminHandler) getProcessStatsJSON(c *fiber.Ctx) error {
// Get process stats from the StatsManager (limit to top 30 processes)
var processData *stats.ProcessStats
var err error
if h.statsManager != nil {
processData, err = h.statsManager.GetProcessStats(30)
} else {
// Fallback to direct function call if StatsManager is not available
processData, err = stats.GetProcessStats(30)
}
if err != nil {
return c.Status(fiber.StatusInternalServerError).JSON(fiber.Map{
"success": false,
"error": "Failed to get process stats: " + err.Error(),
})
}
// Convert to []fiber.Map for JSON response
processStats := make([]fiber.Map, len(processData.Processes))
for i, proc := range processData.Processes {
processStats[i] = fiber.Map{
"pid": proc.PID,
"name": proc.Name,
"status": proc.Status,
"cpu_percent": proc.CPUPercent,
"memory_mb": proc.MemoryMB,
"create_time_str": proc.CreateTime,
"is_current": proc.IsCurrent,
}
}
// Return JSON response
return c.JSON(fiber.Map{
"success": true,
"processes": processStats,
"timestamp": time.Now().Unix(),
})
}
// getHardwareStatsJSON returns hardware stats in JSON format for API consumption
func (h *AdminHandler) getHardwareStatsJSON(c *fiber.Ctx) error {
// Get hardware stats from the StatsManager
var hardwareStats map[string]interface{}
if h.statsManager != nil {
hardwareStats = h.statsManager.GetHardwareStatsJSON()
} else {
// Fallback to direct function call if StatsManager is not available
hardwareStats = stats.GetHardwareStatsJSON()
}
// Convert to fiber.Map for JSON response
response := fiber.Map{
"success": true,
}
for k, v := range hardwareStats {
response[k] = v
}
// Return JSON response
return c.JSON(response)
}

View File

@@ -0,0 +1,149 @@
package api
import (
"time"
"git.ourworld.tf/herocode/heroagent/pkg/sal/executor"
"github.com/gofiber/fiber/v2"
)
// ExecutorHandler handles executor-related API endpoints
type ExecutorHandler struct {
executor *executor.Executor
}
// NewExecutorHandler creates a new executor handler
func NewExecutorHandler(exec *executor.Executor) *ExecutorHandler {
return &ExecutorHandler{
executor: exec,
}
}
// RegisterRoutes registers executor routes to the fiber app
func (h *ExecutorHandler) RegisterRoutes(app *fiber.App) {
group := app.Group("/api/executor")
// @Summary Execute a command
// @Description Execute a command and return a job ID
// @Tags executor
// @Accept json
// @Produce json
// @Param command body ExecuteCommandRequest true "Command to execute"
// @Success 200 {object} ExecuteCommandResponse
// @Failure 400 {object} ErrorResponse
// @Router /api/executor/execute [post]
group.Post("/execute", h.executeCommand)
// @Summary List all jobs
// @Description Get a list of all command execution jobs
// @Tags executor
// @Produce json
// @Success 200 {array} JobResponse
// @Router /api/executor/jobs [get]
group.Get("/jobs", h.listJobs)
// @Summary Get job details
// @Description Get details of a specific job by ID
// @Tags executor
// @Produce json
// @Param id path string true "Job ID"
// @Success 200 {object} JobResponse
// @Failure 404 {object} ErrorResponse
// @Router /api/executor/jobs/{id} [get]
group.Get("/jobs/:id", h.getJob)
}
// @Summary Execute a command
// @Description Execute a command and return a job ID
// @Tags executor
// @Accept json
// @Produce json
// @Param command body ExecuteCommandRequest true "Command to execute"
// @Success 200 {object} ExecuteCommandResponse
// @Failure 400 {object} ErrorResponse
// @Router /api/executor/execute [post]
func (h *ExecutorHandler) executeCommand(c *fiber.Ctx) error {
var req ExecuteCommandRequest
if err := c.BodyParser(&req); err != nil {
return c.Status(fiber.StatusBadRequest).JSON(ErrorResponse{
Error: "Invalid request: " + err.Error(),
})
}
jobID, err := h.executor.ExecuteCommand(req.Command, req.Args)
if err != nil {
return c.Status(fiber.StatusInternalServerError).JSON(ErrorResponse{
Error: "Failed to execute command: " + err.Error(),
})
}
return c.JSON(ExecuteCommandResponse{
JobID: jobID,
})
}
// @Summary List all jobs
// @Description Get a list of all command execution jobs
// @Tags executor
// @Produce json
// @Success 200 {array} JobResponse
// @Router /api/executor/jobs [get]
func (h *ExecutorHandler) listJobs(c *fiber.Ctx) error {
jobs := h.executor.ListJobs()
response := make([]JobResponse, 0, len(jobs))
for _, job := range jobs {
var endTime time.Time
if job.Status == "completed" || job.Status == "failed" {
endTime = job.EndTime
}
response = append(response, JobResponse{
ID: job.ID,
Command: job.Command,
Args: job.Args,
StartTime: job.StartTime,
EndTime: endTime,
Status: job.Status,
Output: job.Output,
Error: job.Error,
})
}
return c.JSON(response)
}
// @Summary Get job details
// @Description Get details of a specific job by ID
// @Tags executor
// @Produce json
// @Param id path string true "Job ID"
// @Success 200 {object} JobResponse
// @Failure 404 {object} ErrorResponse
// @Router /api/executor/jobs/{id} [get]
func (h *ExecutorHandler) getJob(c *fiber.Ctx) error {
jobID := c.Params("id")
job, err := h.executor.GetJob(jobID)
if err != nil {
return c.Status(fiber.StatusNotFound).JSON(ErrorResponse{
Error: err.Error(),
})
}
var endTime time.Time
if job.Status == "completed" || job.Status == "failed" {
endTime = job.EndTime
}
return c.JSON(JobResponse{
ID: job.ID,
Command: job.Command,
Args: job.Args,
StartTime: job.StartTime,
EndTime: endTime,
Status: job.Status,
Output: job.Output,
Error: job.Error,
})
}

View File

@@ -0,0 +1,112 @@
package api
import (
"strings"
"github.com/CloudyKit/jet/v6"
"github.com/gofiber/fiber/v2"
)
// JetTemplateRequest represents the request body for the checkjet endpoint
type JetTemplateRequest struct {
Template string `json:"template"`
}
// JetTemplateResponse represents the response for the checkjet endpoint
type JetTemplateResponse struct {
Valid bool `json:"valid"`
Message string `json:"message,omitempty"`
Error string `json:"error,omitempty"`
}
// JetHandler handles Jet template-related API endpoints
type JetHandler struct {
// No dependencies needed for this handler
}
// NewJetHandler creates a new Jet template handler
func NewJetHandler() *JetHandler {
return &JetHandler{}
}
// RegisterRoutes registers Jet template routes to the fiber app
func (h *JetHandler) RegisterRoutes(app *fiber.App) {
// Create a group for Jet API endpoints
jetGroup := app.Group("/api/jet")
// Register the checkjet endpoint
jetGroup.Post("/validate", h.validateTemplate)
}
// @Summary Validate a Jet template
// @Description Validates a Jet template and returns detailed error information if invalid
// @Tags jet
// @Accept json
// @Produce json
// @Param template body JetTemplateRequest true "Jet template to validate"
// @Success 200 {object} JetTemplateResponse
// @Failure 400 {object} map[string]interface{}
// @Router /api/jet/validate [post]
func (h *JetHandler) validateTemplate(c *fiber.Ctx) error {
var req JetTemplateRequest
if err := c.BodyParser(&req); err != nil {
return c.Status(fiber.StatusBadRequest).JSON(fiber.Map{
"success": false,
"error": "Invalid request: " + err.Error(),
})
}
if req.Template == "" {
return c.Status(fiber.StatusBadRequest).JSON(fiber.Map{
"success": false,
"error": "Template cannot be empty",
})
}
// Create a temporary in-memory loader for the template
loader := jet.NewInMemLoader()
// Add the template to the loader
loader.Set("test.jet", req.Template)
// Create a new Jet set with the loader and enable development mode for better error reporting
set := jet.NewSet(loader, jet.InDevelopmentMode())
// Get the template to parse it
_, err := set.GetTemplate("test.jet")
// Check if the template is valid
if err != nil {
// Extract meaningful error information
errMsg := err.Error()
// Ignore errors related to extended or included files not found
// These aren't syntax errors but dependency errors we want to ignore
if strings.Contains(errMsg, "no template") ||
strings.Contains(errMsg, "unable to locate template") ||
strings.Contains(errMsg, "template not found") ||
strings.Contains(errMsg, "extends|import") ||
strings.Contains(errMsg, "could not be found") ||
strings.Contains(errMsg, "template /") {
// Still valid since it's only a dependency error, not a syntax error
return c.JSON(fiber.Map{
"success": true,
"valid": true,
"message": "Template syntax is valid (ignoring extends/include errors)",
})
}
return c.JSON(fiber.Map{
"success": false,
"valid": false,
"error": errMsg,
})
}
// If no error, the template is valid
return c.JSON(fiber.Map{
"success": true,
"valid": true,
"message": "Template is valid",
})
}

View File

@@ -0,0 +1,74 @@
// Package api contains API handlers for HeroLauncher
package api
// @title HeroLauncher API
// @version 1.0
// @description API for HeroLauncher - a modular service manager
// @termsOfService http://swagger.io/terms/
// @contact.name API Support
// @contact.email support@freeflowuniverse.org
// @license.name Apache 2.0
// @license.url http://www.apache.org/licenses/LICENSE-2.0.html
// @host localhost:9001
// @BasePath /api
// @schemes http https
// This file exists solely to provide Swagger documentation
// and to ensure all API handlers are included in the documentation
// AdminHandler handles admin-related API routes
// @Router /api/hardware-stats [get]
// @Router /api/process-stats [get]
// ServiceHandler handles service-related API routes
// @Router /api/services/running [get]
// @Router /api/services/start [post]
// @Router /api/services/stop [post]
// @Router /api/services/restart [post]
// @Router /api/services/delete [post]
// @Router /api/services/logs [post]
// @Router /admin/services/ [get]
// @Router /admin/services/data [get]
// @Router /admin/services/running [get]
// @Router /admin/services/start [post]
// @Router /admin/services/stop [post]
// @Router /admin/services/restart [post]
// @Router /admin/services/delete [post]
// @Router /admin/services/logs [post]
// ExecutorHandler handles command execution API routes
// @Router /api/executor/execute [post]
// @Router /api/executor/jobs [get]
// @Router /api/executor/jobs/{id} [get]
// JetHandler handles Jet template API routes
// @Router /api/jet/validate [post]
// RedisHandler handles Redis API routes
// @Router /api/redis/set [post]
// @Router /api/redis/get/{key} [get]
// @Router /api/redis/del/{key} [delete]
// @Router /api/redis/keys/{pattern} [get]
// @Router /api/redis/hset [post]
// @Router /api/redis/hget/{key}/{field} [get]
// @Router /api/redis/hdel [post]
// @Router /api/redis/hkeys/{key} [get]
// @Router /api/redis/hgetall/{key} [get]
// JobHandler handles HeroJobs API routes
// @Router /api/jobs/submit [post]
// @Router /api/jobs/get/{id} [get]
// @Router /api/jobs/delete/{id} [delete]
// @Router /api/jobs/list [get]
// @Router /api/jobs/queue/size [get]
// @Router /api/jobs/queue/empty [post]
// @Router /api/jobs/queue/get [get]
// @Router /api/jobs/create [post]
// @Router /admin/jobs/submit [post]
// @Router /admin/jobs/get/{id} [get]
// @Router /admin/jobs/delete/{id} [delete]
// @Router /admin/jobs/list [get]
// @Router /admin/jobs/queue/size [get]
// @Router /admin/jobs/queue/empty [post]
// @Router /admin/jobs/queue/get [get]
// @Router /admin/jobs/create [post]

View File

@@ -0,0 +1,105 @@
package api
import "time"
// ErrorResponse represents an error response
type ErrorResponse struct {
Error string `json:"error"`
}
// Executor Models
// ExecuteCommandRequest represents a request to execute a command
type ExecuteCommandRequest struct {
Command string `json:"command"`
Args []string `json:"args"`
}
// ExecuteCommandResponse represents the response from executing a command
type ExecuteCommandResponse struct {
JobID string `json:"job_id"`
}
// JobResponse represents a job response
type JobResponse struct {
ID string `json:"id"`
Command string `json:"command"`
Args []string `json:"args"`
StartTime time.Time `json:"start_time"`
EndTime time.Time `json:"end_time"`
Status string `json:"status"`
Output string `json:"output"`
Error string `json:"error"`
}
// Redis Models
// SetKeyRequest represents a request to set a key
type SetKeyRequest struct {
Key string `json:"key"`
Value string `json:"value"`
ExpirationSeconds int `json:"expiration_seconds"`
}
// SetKeyResponse represents the response from setting a key
type SetKeyResponse struct {
Success bool `json:"success"`
}
// GetKeyResponse represents the response from getting a key
type GetKeyResponse struct {
Value string `json:"value"`
}
// DeleteKeyResponse represents the response from deleting a key
type DeleteKeyResponse struct {
Count int `json:"count"`
}
// GetKeysResponse represents the response from getting keys
type GetKeysResponse struct {
Keys []string `json:"keys"`
}
// HSetKeyRequest represents a request to set a hash field
type HSetKeyRequest struct {
Key string `json:"key"`
Field string `json:"field"`
Value string `json:"value"`
}
// HSetKeyResponse represents the response from setting a hash field
type HSetKeyResponse struct {
Added bool `json:"added"`
}
// HGetKeyResponse represents the response from getting a hash field
type HGetKeyResponse struct {
Value string `json:"value"`
}
// HDelKeyRequest represents a request to delete hash fields
type HDelKeyRequest struct {
Key string `json:"key"`
Fields []string `json:"fields"`
}
// HDelKeyResponse represents the response from deleting hash fields
type HDelKeyResponse struct {
Count int `json:"count"`
}
// HKeysResponse represents the response from getting hash keys
type HKeysResponse struct {
Fields []string `json:"fields"`
}
// HLenResponse represents the response from getting hash length
type HLenResponse struct {
Length int `json:"length"`
}
// IncrKeyResponse represents the response from incrementing a key
type IncrKeyResponse struct {
Value int64 `json:"value"`
}

View File

@@ -0,0 +1,544 @@
package api
import (
"encoding/json"
"fmt"
"log"
"strconv"
"time"
"git.ourworld.tf/herocode/heroagent/pkg/processmanager"
"git.ourworld.tf/herocode/heroagent/pkg/processmanager/interfaces"
"git.ourworld.tf/herocode/heroagent/pkg/processmanager/interfaces/openrpc"
"github.com/gofiber/fiber/v2"
)
// ProcessDisplayInfo represents information about a process for display purposes
type ProcessDisplayInfo struct {
ID string `json:"id"`
Name string `json:"name"`
Status string `json:"status"`
Uptime string `json:"uptime"`
StartTime string `json:"start_time"`
CPU string `json:"cpu"`
Memory string `json:"memory"`
}
// ConvertToDisplayInfo converts a ProcessInfo from the processmanager package to ProcessDisplayInfo
func ConvertToDisplayInfo(info *processmanager.ProcessInfo) ProcessDisplayInfo {
// Calculate uptime from start time
uptime := formatUptime(time.Since(info.StartTime))
return ProcessDisplayInfo{
ID: fmt.Sprintf("%d", info.PID),
Name: info.Name,
Status: string(info.Status),
Uptime: uptime,
StartTime: info.StartTime.Format("2006-01-02 15:04:05"),
CPU: fmt.Sprintf("%.2f%%", info.CPUPercent),
Memory: fmt.Sprintf("%.2f MB", info.MemoryMB),
}
}
// ServiceHandler handles service-related API routes
type ServiceHandler struct {
client *openrpc.Client
logger *log.Logger
}
// default number of log lines to retrieve - use a high value to essentially show all logs
const DefaultLogLines = 10000
// NewServiceHandler creates a new service handler with the provided socket path and secret
func NewServiceHandler(socketPath, secret string, logger *log.Logger) *ServiceHandler {
fmt.Printf("DEBUG: Creating new api.ServiceHandler with socket path: %s and secret: %s\n", socketPath, secret)
return &ServiceHandler{
client: openrpc.NewClient(socketPath, secret),
logger: logger,
}
}
// RegisterRoutes registers service API routes
func (h *ServiceHandler) RegisterRoutes(app *fiber.App) {
// Register common routes to both API and admin groups
serviceRoutes := func(group fiber.Router) {
group.Get("/running", h.getRunningServices)
group.Post("/start", h.startService)
group.Post("/stop", h.stopService)
group.Post("/restart", h.restartService)
group.Post("/delete", h.deleteService)
group.Post("/logs", h.getProcessLogs)
}
// Apply common routes to API group
apiServices := app.Group("/api/services")
serviceRoutes(apiServices)
// Apply common routes to admin group and add admin-specific routes
adminServices := app.Group("/admin/services")
serviceRoutes(adminServices)
// Admin-only routes
adminServices.Get("/", h.getServicesPage)
adminServices.Get("/data", h.getServicesData)
}
// getProcessList gets a list of processes from the process manager
// TODO: add swagger annotations
func (h *ServiceHandler) getProcessList() ([]ProcessDisplayInfo, error) {
// Debug: Log the function entry
h.logger.Printf("Entering getProcessList() function")
fmt.Printf("DEBUG: API getProcessList called using client: %p\n", h.client)
// Get the list of processes via the client
result, err := h.client.ListProcesses("json")
if err != nil {
h.logger.Printf("Error listing processes: %v", err)
return nil, err
}
// Convert the result to a slice of ProcessStatus
processStatuses, ok := result.([]interfaces.ProcessStatus)
if !ok {
// Try to handle the result as a map or other structure
h.logger.Printf("Warning: unexpected result type from ListProcesses, trying alternative parsing")
// Try to convert the result to JSON and then parse it
resultJSON, err := json.Marshal(result)
if err != nil {
h.logger.Printf("Error marshaling result to JSON: %v", err)
return nil, fmt.Errorf("failed to marshal result: %w", err)
}
var processStatuses []interfaces.ProcessStatus
if err := json.Unmarshal(resultJSON, &processStatuses); err != nil {
h.logger.Printf("Error unmarshaling result to ProcessStatus: %v", err)
return nil, fmt.Errorf("failed to unmarshal process list result: %w", err)
}
// Convert to display info format
displayInfoList := make([]ProcessDisplayInfo, 0, len(processStatuses))
for _, proc := range processStatuses {
// Calculate uptime based on start time
uptime := formatUptime(time.Since(proc.StartTime))
displayInfo := ProcessDisplayInfo{
ID: fmt.Sprintf("%d", proc.PID),
Name: proc.Name,
Status: string(proc.Status),
Uptime: uptime,
StartTime: proc.StartTime.Format("2006-01-02 15:04:05"),
CPU: fmt.Sprintf("%.2f%%", proc.CPUPercent),
Memory: fmt.Sprintf("%.2f MB", proc.MemoryMB),
}
displayInfoList = append(displayInfoList, displayInfo)
}
// Debug: Log the number of processes
h.logger.Printf("Found %d processes", len(displayInfoList))
return displayInfoList, nil
}
// Convert to display info format
displayInfoList := make([]ProcessDisplayInfo, 0, len(processStatuses))
for _, proc := range processStatuses {
// Calculate uptime based on start time
uptime := formatUptime(time.Since(proc.StartTime))
displayInfo := ProcessDisplayInfo{
ID: fmt.Sprintf("%d", proc.PID),
Name: proc.Name,
Status: string(proc.Status),
Uptime: uptime,
StartTime: proc.StartTime.Format("2006-01-02 15:04:05"),
CPU: fmt.Sprintf("%.2f%%", proc.CPUPercent),
Memory: fmt.Sprintf("%.2f MB", proc.MemoryMB),
}
displayInfoList = append(displayInfoList, displayInfo)
}
// Debug: Log the number of processes
h.logger.Printf("Found %d processes", len(displayInfoList))
return displayInfoList, nil
}
// formatUptime formats a duration as a human-readable uptime string
func formatUptime(duration time.Duration) string {
totalSeconds := int(duration.Seconds())
days := totalSeconds / (24 * 3600)
hours := (totalSeconds % (24 * 3600)) / 3600
minutes := (totalSeconds % 3600) / 60
seconds := totalSeconds % 60
if days > 0 {
return fmt.Sprintf("%d days, %d hours", days, hours)
} else if hours > 0 {
return fmt.Sprintf("%d hours, %d minutes", hours, minutes)
} else if minutes > 0 {
return fmt.Sprintf("%d minutes, %d seconds", minutes, seconds)
} else {
return fmt.Sprintf("%d seconds", seconds)
}
}
// @Summary Start a service
// @Description Start a new service with the given name and command
// @Tags services
// @Accept x-www-form-urlencoded
// @Produce json
// @Param name formData string true "Service name"
// @Param command formData string true "Command to run"
// @Success 200 {object} map[string]interface{}
// @Failure 400 {object} map[string]string
// @Failure 500 {object} map[string]string
// @Router /api/services/start [post]
// @Router /admin/services/start [post]
func (h *ServiceHandler) startService(c *fiber.Ctx) error {
// Get form values
name := c.FormValue("name")
command := c.FormValue("command")
// Validate inputs
if name == "" || command == "" {
return c.Status(fiber.StatusBadRequest).JSON(fiber.Map{
"success": false,
"error": "Name and command are required",
})
}
// Start the process with default values
// logEnabled=true, deadline=0 (no deadline), no cron, no jobID
fmt.Printf("DEBUG: API startService called for '%s' using client: %p\n", name, h.client)
result, err := h.client.StartProcess(name, command, true, 0, "", "")
if err != nil {
return c.Status(fiber.StatusInternalServerError).JSON(fiber.Map{
"success": false,
"error": fmt.Sprintf("Failed to start service: %v", err),
})
}
// Check if the result indicates success
if !result.Success {
return c.Status(fiber.StatusInternalServerError).JSON(fiber.Map{
"success": false,
"error": result.Message,
})
}
// Get the PID from the result
pid := result.PID
return c.JSON(fiber.Map{
"success": true,
"message": fmt.Sprintf("Service '%s' started with PID %d", name, pid),
"pid": pid,
})
}
// @Summary Stop a service
// @Description Stop a running service by name
// @Tags services
// @Accept x-www-form-urlencoded
// @Produce json
// @Param name formData string true "Service name"
// @Success 200 {object} map[string]interface{}
// @Failure 400 {object} map[string]string
// @Failure 500 {object} map[string]string
// @Router /api/services/stop [post]
// @Router /admin/services/stop [post]
// stopService stops a service
func (h *ServiceHandler) stopService(c *fiber.Ctx) error {
// Get form values
name := c.FormValue("name")
// For backward compatibility, try ID field if name is empty
if name == "" {
name = c.FormValue("id")
if name == "" {
return c.Status(fiber.StatusBadRequest).JSON(fiber.Map{
"success": false,
"error": "Process name is required",
})
}
}
// Log the stop request
h.logger.Printf("Stopping process with name: %s", name)
// Stop the process
fmt.Printf("DEBUG: API stopService called for '%s' using client: %p\n", name, h.client)
result, err := h.client.StopProcess(name)
if err != nil {
h.logger.Printf("Error stopping process: %v", err)
return c.Status(fiber.StatusInternalServerError).JSON(fiber.Map{
"success": false,
"error": fmt.Sprintf("Failed to stop service: %v", err),
})
}
// Check if the result indicates success
if !result.Success {
return c.Status(fiber.StatusInternalServerError).JSON(fiber.Map{
"success": false,
"error": result.Message,
})
}
return c.JSON(fiber.Map{
"success": true,
"message": fmt.Sprintf("Service '%s' stopped successfully", name),
})
}
// @Summary Restart a service
// @Description Restart a running service by name
// @Tags services
// @Accept x-www-form-urlencoded
// @Produce json
// @Param name formData string true "Service name"
// @Success 200 {object} map[string]interface{}
// @Failure 400 {object} map[string]string
// @Failure 500 {object} map[string]string
// @Router /api/services/restart [post]
// @Router /admin/services/restart [post]
// restartService restarts a service
func (h *ServiceHandler) restartService(c *fiber.Ctx) error {
// Get form values
name := c.FormValue("name")
// For backward compatibility, try ID field if name is empty
if name == "" {
name = c.FormValue("id")
if name == "" {
return c.Status(fiber.StatusBadRequest).JSON(fiber.Map{
"success": false,
"error": "Process name is required",
})
}
}
// Log the restart request
h.logger.Printf("Restarting process with name: %s", name)
// Restart the process
fmt.Printf("DEBUG: API restartService called for '%s' using client: %p\n", name, h.client)
result, err := h.client.RestartProcess(name)
if err != nil {
h.logger.Printf("Error restarting process: %v", err)
return c.Status(fiber.StatusInternalServerError).JSON(fiber.Map{
"success": false,
"error": fmt.Sprintf("Failed to restart service: %v", err),
})
}
// Check if the result indicates success
if !result.Success {
return c.Status(fiber.StatusInternalServerError).JSON(fiber.Map{
"success": false,
"error": result.Message,
})
}
return c.JSON(fiber.Map{
"success": true,
"message": fmt.Sprintf("Service '%s' restarted successfully", name),
})
}
// @Summary Delete a service
// @Description Delete a service by name
// @Tags services
// @Accept x-www-form-urlencoded
// @Produce json
// @Param name formData string true "Service name"
// @Success 200 {object} map[string]interface{}
// @Failure 400 {object} map[string]string
// @Failure 500 {object} map[string]string
// @Router /api/services/delete [post]
// @Router /admin/services/delete [post]
// deleteService deletes a service
func (h *ServiceHandler) deleteService(c *fiber.Ctx) error {
// Get form values
name := c.FormValue("name")
// Validate inputs
if name == "" {
return c.Status(fiber.StatusBadRequest).JSON(fiber.Map{
"success": false,
"error": "Service name is required",
})
}
// Debug: Log the delete request
h.logger.Printf("Deleting process with name: %s", name)
// Delete the process
fmt.Printf("DEBUG: API deleteService called for '%s' using client: %p\n", name, h.client)
result, err := h.client.DeleteProcess(name)
if err != nil {
return c.Status(fiber.StatusInternalServerError).JSON(fiber.Map{
"success": false,
"error": fmt.Sprintf("Failed to delete service: %v", err),
})
}
// Check if the result indicates success
if !result.Success {
return c.Status(fiber.StatusInternalServerError).JSON(fiber.Map{
"success": false,
"error": result.Message,
})
}
return c.JSON(fiber.Map{
"success": true,
"message": fmt.Sprintf("Service '%s' deleted successfully", name),
})
}
// @Summary Get running services
// @Description Get a list of all currently running services
// @Tags services
// @Accept json
// @Produce json
// @Success 200 {object} map[string][]ProcessDisplayInfo
// @Failure 500 {object} map[string]string
// @Router /api/services/running [get]
// @Router /admin/services/running [get]
func (h *ServiceHandler) getRunningServices(c *fiber.Ctx) error {
// Get the list of processes
processes, err := h.getProcessList()
if err != nil {
h.logger.Printf("Error getting process list: %v", err)
return c.Status(fiber.StatusInternalServerError).JSON(fiber.Map{
"success": false,
"error": fmt.Sprintf("Failed to get process list: %v", err),
})
}
// Filter to only include running processes
runningProcesses := make([]ProcessDisplayInfo, 0)
for _, proc := range processes {
if proc.Status == "running" {
runningProcesses = append(runningProcesses, proc)
}
}
// Return the processes as JSON
return c.JSON(fiber.Map{
"success": true,
"services": runningProcesses,
"processes": processes, // Keep for backward compatibility
})
}
// @Summary Get process logs
// @Description Get logs for a specific process
// @Tags services
// @Accept x-www-form-urlencoded
// @Produce json
// @Param name formData string true "Service name"
// @Param lines formData integer false "Number of log lines to retrieve"
// @Success 200 {object} map[string]string
// @Failure 400 {object} map[string]string
// @Failure 500 {object} map[string]string
// @Router /api/services/logs [post]
// @Router /admin/services/logs [post]
// getProcessLogs retrieves logs for a specific process
func (h *ServiceHandler) getProcessLogs(c *fiber.Ctx) error {
// Get form values
name := c.FormValue("name")
// For backward compatibility, try ID field if name is empty
if name == "" {
name = c.FormValue("id")
if name == "" {
return c.Status(fiber.StatusBadRequest).JSON(fiber.Map{
"success": false,
"error": "Process name is required",
})
}
}
// Get the number of lines to retrieve
linesStr := c.FormValue("lines")
lines := DefaultLogLines
if linesStr != "" {
if parsedLines, err := strconv.Atoi(linesStr); err == nil && parsedLines > 0 {
lines = parsedLines
}
}
// Log the request
h.logger.Printf("Getting logs for process: %s (lines: %d)", name, lines)
// Get logs
fmt.Printf("DEBUG: API getProcessLogs called for '%s' using client: %p\n", name, h.client)
logs, err := h.client.GetProcessLogs(name, lines)
if err != nil {
h.logger.Printf("Error getting process logs: %v", err)
return c.Status(fiber.StatusInternalServerError).JSON(fiber.Map{
"success": false,
"error": fmt.Sprintf("Failed to get logs: %v", err),
})
}
return c.JSON(fiber.Map{
"success": true,
"logs": logs,
})
}
// @Summary Get services page
// @Description Get the services management page
// @Tags admin
// @Produce html
// @Success 200 {string} string "HTML content"
// @Failure 500 {object} map[string]string
// @Router /admin/services/ [get]
// getServicesPage renders the services page
func (h *ServiceHandler) getServicesPage(c *fiber.Ctx) error {
// Get processes to display on the initial page load
processes, _ := h.getProcessList()
// Check if client is properly initialized
var warning string
if h.client == nil {
warning = "Process manager client is not properly initialized."
h.logger.Printf("Warning: %s", warning)
}
return c.Render("admin/services", fiber.Map{
"title": "Services",
"processes": processes,
"warning": warning,
})
}
// @Summary Get services data
// @Description Get services data for AJAX updates
// @Tags admin
// @Produce html
// @Success 200 {string} string "HTML content"
// @Failure 500 {object} map[string]string
// @Router /admin/services/data [get]
// getServicesData returns only the services fragment for AJAX updates
func (h *ServiceHandler) getServicesData(c *fiber.Ctx) error {
// Get processes
processes, _ := h.getProcessList()
// Check if client is properly initialized
var warning string
if h.client == nil {
warning = "Process manager client is not properly initialized."
h.logger.Printf("Warning: %s", warning)
}
// Return the fragment with process data and optional warning
return c.Render("admin/services_fragment", fiber.Map{
"processes": processes,
"warning": warning,
"layout": "",
})
}

View File

@@ -0,0 +1,449 @@
package api
import (
"context"
"time"
"github.com/gofiber/fiber/v2"
"github.com/redis/go-redis/v9"
)
// RedisHandler handles Redis-related API endpoints
type RedisHandler struct {
redisClient *redis.Client
}
// NewRedisHandler creates a new Redis handler
func NewRedisHandler(redisAddr string, isUnixSocket bool) *RedisHandler {
// Determine network type
networkType := "tcp"
if isUnixSocket {
networkType = "unix"
}
// Create Redis client
client := redis.NewClient(&redis.Options{
Network: networkType,
Addr: redisAddr,
DB: 0,
DialTimeout: 5 * time.Second,
ReadTimeout: 5 * time.Second,
WriteTimeout: 5 * time.Second,
})
return &RedisHandler{
redisClient: client,
}
}
// RegisterRoutes registers Redis routes to the fiber app
func (h *RedisHandler) RegisterRoutes(app *fiber.App) {
group := app.Group("/api/redis")
// @Summary Set a Redis key
// @Description Set a key-value pair in Redis with optional expiration
// @Tags redis
// @Accept json
// @Produce json
// @Param request body SetKeyRequest true "Key-value data"
// @Success 200 {object} SetKeyResponse
// @Failure 400 {object} ErrorResponse
// @Failure 500 {object} ErrorResponse
// @Router /api/redis/set [post]
group.Post("/set", h.setKey)
// @Summary Get a Redis key
// @Description Get a value by key from Redis
// @Tags redis
// @Produce json
// @Param key path string true "Key to retrieve"
// @Success 200 {object} GetKeyResponse
// @Failure 400 {object} ErrorResponse
// @Failure 404 {object} ErrorResponse
// @Failure 500 {object} ErrorResponse
// @Router /api/redis/get/{key} [get]
group.Get("/get/:key", h.getKey)
// @Summary Delete a Redis key
// @Description Delete a key from Redis
// @Tags redis
// @Produce json
// @Param key path string true "Key to delete"
// @Success 200 {object} DeleteKeyResponse
// @Failure 400 {object} ErrorResponse
// @Failure 500 {object} ErrorResponse
// @Router /api/redis/del/{key} [delete]
group.Delete("/del/:key", h.deleteKey)
// @Summary Get Redis keys by pattern
// @Description Get keys matching a pattern from Redis
// @Tags redis
// @Produce json
// @Param pattern path string true "Pattern to match keys"
// @Success 200 {object} GetKeysResponse
// @Failure 500 {object} ErrorResponse
// @Router /api/redis/keys/{pattern} [get]
group.Get("/keys/:pattern", h.getKeys)
// @Summary Set hash fields
// @Description Set one or more fields in a Redis hash
// @Tags redis
// @Accept json
// @Produce json
// @Param request body HSetKeyRequest true "Hash field data"
// @Success 200 {object} HSetKeyResponse
// @Failure 400 {object} ErrorResponse
// @Failure 500 {object} ErrorResponse
// @Router /api/redis/hset [post]
group.Post("/hset", h.hsetKey)
// @Summary Get hash field
// @Description Get a field from a Redis hash
// @Tags redis
// @Produce json
// @Param key path string true "Hash key"
// @Param field path string true "Field to retrieve"
// @Success 200 {object} HGetKeyResponse
// @Failure 400 {object} ErrorResponse
// @Failure 404 {object} ErrorResponse
// @Failure 500 {object} ErrorResponse
// @Router /api/redis/hget/{key}/{field} [get]
group.Get("/hget/:key/:field", h.hgetKey)
// @Summary Delete hash fields
// @Description Delete one or more fields from a Redis hash
// @Tags redis
// @Accept json
// @Produce json
// @Param request body HDelKeyRequest true "Fields to delete"
// @Success 200 {object} HDelKeyResponse
// @Failure 400 {object} ErrorResponse
// @Failure 500 {object} ErrorResponse
// @Router /api/redis/hdel [post]
group.Post("/hdel", h.hdelKey)
// @Summary Get hash fields
// @Description Get all field names in a Redis hash
// @Tags redis
// @Produce json
// @Param key path string true "Hash key"
// @Success 200 {object} HKeysResponse
// @Failure 400 {object} ErrorResponse
// @Failure 500 {object} ErrorResponse
// @Router /api/redis/hkeys/{key} [get]
group.Get("/hkeys/:key", h.hkeysKey)
// @Summary Get all hash fields and values
// @Description Get all fields and values in a Redis hash
// @Tags redis
// @Produce json
// @Param key path string true "Hash key"
// @Success 200 {object} map[string]string
// @Failure 400 {object} ErrorResponse
// @Failure 500 {object} ErrorResponse
// @Router /api/redis/hgetall/{key} [get]
group.Get("/hgetall/:key", h.hgetallKey)
}
// setKey sets a key-value pair in Redis
func (h *RedisHandler) setKey(c *fiber.Ctx) error {
// Parse request
var req struct {
Key string `json:"key"`
Value string `json:"value"`
Expires int `json:"expires,omitempty"` // Expiration in seconds, optional
}
if err := c.BodyParser(&req); err != nil {
return c.Status(fiber.StatusBadRequest).JSON(fiber.Map{
"success": false,
"error": "Invalid request format: " + err.Error(),
})
}
// Validate required fields
if req.Key == "" || req.Value == "" {
return c.Status(fiber.StatusBadRequest).JSON(fiber.Map{
"success": false,
"error": "Key and value are required",
})
}
ctx := context.Background()
var err error
// Set with or without expiration
if req.Expires > 0 {
err = h.redisClient.Set(ctx, req.Key, req.Value, time.Duration(req.Expires)*time.Second).Err()
} else {
err = h.redisClient.Set(ctx, req.Key, req.Value, 0).Err()
}
if err != nil {
return c.Status(fiber.StatusInternalServerError).JSON(fiber.Map{
"success": false,
"error": "Failed to set key: " + err.Error(),
})
}
return c.JSON(fiber.Map{
"success": true,
"message": "Key set successfully",
})
}
// getKey retrieves a value by key from Redis
func (h *RedisHandler) getKey(c *fiber.Ctx) error {
key := c.Params("key")
if key == "" {
return c.Status(fiber.StatusBadRequest).JSON(fiber.Map{
"success": false,
"error": "Key is required",
})
}
ctx := context.Background()
val, err := h.redisClient.Get(ctx, key).Result()
if err == redis.Nil {
return c.Status(fiber.StatusNotFound).JSON(fiber.Map{
"success": false,
"error": "Key not found",
})
} else if err != nil {
return c.Status(fiber.StatusInternalServerError).JSON(fiber.Map{
"success": false,
"error": "Failed to get key: " + err.Error(),
})
}
return c.JSON(fiber.Map{
"success": true,
"key": key,
"value": val,
})
}
// deleteKey deletes a key from Redis
func (h *RedisHandler) deleteKey(c *fiber.Ctx) error {
key := c.Params("key")
if key == "" {
return c.Status(fiber.StatusBadRequest).JSON(fiber.Map{
"success": false,
"error": "Key is required",
})
}
ctx := context.Background()
result, err := h.redisClient.Del(ctx, key).Result()
if err != nil {
return c.Status(fiber.StatusInternalServerError).JSON(fiber.Map{
"success": false,
"error": "Failed to delete key: " + err.Error(),
})
}
return c.JSON(fiber.Map{
"success": true,
"deleted": result > 0,
"count": result,
})
}
// getKeys retrieves keys matching a pattern from Redis
func (h *RedisHandler) getKeys(c *fiber.Ctx) error {
pattern := c.Params("pattern", "*")
ctx := context.Background()
keys, err := h.redisClient.Keys(ctx, pattern).Result()
if err != nil {
return c.Status(fiber.StatusInternalServerError).JSON(fiber.Map{
"success": false,
"error": "Failed to get keys: " + err.Error(),
})
}
return c.JSON(fiber.Map{
"success": true,
"keys": keys,
"count": len(keys),
})
}
// hsetKey sets a field in a hash stored at key
func (h *RedisHandler) hsetKey(c *fiber.Ctx) error {
// Parse request
var req struct {
Key string `json:"key"`
Fields map[string]string `json:"fields"`
}
if err := c.BodyParser(&req); err != nil {
return c.Status(fiber.StatusBadRequest).JSON(fiber.Map{
"success": false,
"error": "Invalid request format: " + err.Error(),
})
}
// Validate required fields
if req.Key == "" || len(req.Fields) == 0 {
return c.Status(fiber.StatusBadRequest).JSON(fiber.Map{
"success": false,
"error": "Key and at least one field are required",
})
}
ctx := context.Background()
totalAdded := 0
// Use HSet to set multiple fields at once
for field, value := range req.Fields {
added, err := h.redisClient.HSet(ctx, req.Key, field, value).Result()
if err != nil {
return c.Status(fiber.StatusInternalServerError).JSON(fiber.Map{
"success": false,
"error": "Failed to set hash field: " + err.Error(),
})
}
totalAdded += int(added)
}
return c.JSON(fiber.Map{
"success": true,
"added": totalAdded,
})
}
// hgetKey retrieves a field from a hash stored at key
func (h *RedisHandler) hgetKey(c *fiber.Ctx) error {
key := c.Params("key")
field := c.Params("field")
if key == "" || field == "" {
return c.Status(fiber.StatusBadRequest).JSON(fiber.Map{
"success": false,
"error": "Key and field are required",
})
}
ctx := context.Background()
val, err := h.redisClient.HGet(ctx, key, field).Result()
if err == redis.Nil {
return c.Status(fiber.StatusNotFound).JSON(fiber.Map{
"success": false,
"error": "Field not found in hash",
})
} else if err != nil {
return c.Status(fiber.StatusInternalServerError).JSON(fiber.Map{
"success": false,
"error": "Failed to get hash field: " + err.Error(),
})
}
return c.JSON(fiber.Map{
"success": true,
"key": key,
"field": field,
"value": val,
})
}
// hdelKey deletes fields from a hash stored at key
func (h *RedisHandler) hdelKey(c *fiber.Ctx) error {
// Parse request
var req struct {
Key string `json:"key"`
Fields []string `json:"fields"`
}
if err := c.BodyParser(&req); err != nil {
return c.Status(fiber.StatusBadRequest).JSON(fiber.Map{
"success": false,
"error": "Invalid request format: " + err.Error(),
})
}
// Validate required fields
if req.Key == "" || len(req.Fields) == 0 {
return c.Status(fiber.StatusBadRequest).JSON(fiber.Map{
"success": false,
"error": "Key and at least one field are required",
})
}
ctx := context.Background()
fields := make([]string, len(req.Fields))
copy(fields, req.Fields)
removed, err := h.redisClient.HDel(ctx, req.Key, fields...).Result()
if err != nil {
return c.Status(fiber.StatusInternalServerError).JSON(fiber.Map{
"success": false,
"error": "Failed to delete hash fields: " + err.Error(),
})
}
return c.JSON(fiber.Map{
"success": true,
"removed": removed,
})
}
// hkeysKey retrieves all field names in a hash stored at key
func (h *RedisHandler) hkeysKey(c *fiber.Ctx) error {
key := c.Params("key")
if key == "" {
return c.Status(fiber.StatusBadRequest).JSON(fiber.Map{
"success": false,
"error": "Key is required",
})
}
ctx := context.Background()
fields, err := h.redisClient.HKeys(ctx, key).Result()
if err != nil {
return c.Status(fiber.StatusInternalServerError).JSON(fiber.Map{
"success": false,
"error": "Failed to get hash keys: " + err.Error(),
})
}
return c.JSON(fiber.Map{
"success": true,
"key": key,
"fields": fields,
"count": len(fields),
})
}
// hgetallKey retrieves all fields and values in a hash stored at key
func (h *RedisHandler) hgetallKey(c *fiber.Ctx) error {
key := c.Params("key")
if key == "" {
return c.Status(fiber.StatusBadRequest).JSON(fiber.Map{
"success": false,
"error": "Key is required",
})
}
ctx := context.Background()
values, err := h.redisClient.HGetAll(ctx, key).Result()
if err != nil {
return c.Status(fiber.StatusInternalServerError).JSON(fiber.Map{
"success": false,
"error": "Failed to get hash: " + err.Error(),
})
}
return c.JSON(fiber.Map{
"success": true,
"key": key,
"hash": values,
"count": len(values),
})
}

View File

@@ -0,0 +1,57 @@
package tests
import (
"bytes"
"encoding/json"
"net/http"
"net/http/httptest"
"testing"
"github.com/gofiber/fiber/v2"
"github.com/stretchr/testify/assert"
)
// TestSetup represents the common test setup
type TestSetup struct {
App *fiber.App
Assert *assert.Assertions
}
// NewTestSetup creates a new test setup
func NewTestSetup(t *testing.T) *TestSetup {
return &TestSetup{
App: fiber.New(),
Assert: assert.New(t),
}
}
// PerformRequest performs an HTTP request and returns the response
func (ts *TestSetup) PerformRequest(method, path string, body interface{}) *http.Response {
// Convert body to JSON if it's not nil
var reqBody *bytes.Buffer
if body != nil {
jsonBody, _ := json.Marshal(body)
reqBody = bytes.NewBuffer(jsonBody)
} else {
reqBody = bytes.NewBuffer(nil)
}
// Create a new HTTP request
req := httptest.NewRequest(method, path, reqBody)
req.Header.Set("Content-Type", "application/json")
// Perform the request
resp, _ := ts.App.Test(req)
return resp
}
// AssertStatusCode asserts that the response has the expected status code
func (ts *TestSetup) AssertStatusCode(resp *http.Response, expected int) {
ts.Assert.Equal(expected, resp.StatusCode, "Expected status code %d but got %d", expected, resp.StatusCode)
}
// ParseResponseBody parses the response body into the given struct
func (ts *TestSetup) ParseResponseBody(resp *http.Response, v interface{}) {
defer resp.Body.Close()
ts.Assert.NoError(json.NewDecoder(resp.Body).Decode(v), "Failed to parse response body")
}