545 lines
16 KiB
Go
545 lines
16 KiB
Go
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": "",
|
|
})
|
|
}
|