This repository has been archived on 2025-08-04. You can view files and clone it, but cannot push or open issues or pull requests.
heroagent_go_old/_pkg2_dont_use/heroagent/api/processmanager.go

545 lines
16 KiB
Go

package api
import (
"encoding/json"
"fmt"
"log"
"strconv"
"time"
"git.threefold.info/herocode/heroagent/pkg/processmanager"
"git.threefold.info/herocode/heroagent/pkg/processmanager/interfaces"
"git.threefold.info/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": "",
})
}