heroagent/pkg/heroagent/factory.go
2025-04-23 04:18:28 +02:00

419 lines
13 KiB
Go

package heroagent
import (
"fmt"
"log"
"net"
"os"
"os/exec"
"os/signal"
"path/filepath"
"runtime"
"syscall"
"time"
"github.com/freeflowuniverse/heroagent/pkg/heroagent/api"
"github.com/freeflowuniverse/heroagent/pkg/heroagent/handlers"
"github.com/freeflowuniverse/heroagent/pkg/heroagent/pages"
"github.com/freeflowuniverse/heroagent/pkg/processmanager"
"github.com/freeflowuniverse/heroagent/pkg/sal/executor"
"github.com/freeflowuniverse/heroagent/pkg/servers/redisserver"
"github.com/freeflowuniverse/heroagent/pkg/system/stats"
// "github.com/freeflowuniverse/heroagent/pkg/vfs/interfaces"
// "github.com/freeflowuniverse/heroagent/pkg/vfs/interfaces/mock"
"github.com/gofiber/fiber/v2"
"github.com/gofiber/fiber/v2/middleware/cors"
"github.com/gofiber/fiber/v2/middleware/logger"
"github.com/gofiber/fiber/v2/middleware/recover"
"github.com/gofiber/template/jet/v2"
)
// Config holds the configuration for the HeroLauncher server
type Config struct {
Port string
RedisTCPPort string
RedisSocketPath string
TemplatesPath string
StaticFilesPath string
PMSocketPath string // ProcessManager socket path
PMSecret string // ProcessManager authentication secret
HJSocketPath string // HeroJobs socket path
}
// DefaultConfig returns a default configuration for the HeroLauncher server
func DefaultConfig() Config {
// Get the absolute path to the project root
_, filename, _, _ := runtime.Caller(0)
projectRoot := filepath.Join(filepath.Dir(filename), "../..")
// Check for PORT environment variable
port := os.Getenv("PORT")
if port == "" {
port = "9021" // Default port if not specified
}
return Config{
Port: port,
RedisTCPPort: "6379",
RedisSocketPath: "/tmp/heroagent_new.sock",
PMSocketPath: "/tmp/processmanager.sock", // Default ProcessManager socket path
PMSecret: "1234", // Default ProcessManager secret
HJSocketPath: "/tmp/herojobs.sock", // Default HeroJobs socket path
TemplatesPath: filepath.Join(projectRoot, "pkg/heroagent/web/templates"),
StaticFilesPath: filepath.Join(projectRoot, "pkg/heroagent/web/static"),
}
}
// HeroLauncher represents the main application
type HeroLauncher struct {
app *fiber.App
redisServer *redisserver.Server
executorService *executor.Executor
pm *processmanager.ProcessManager
pmProcess *os.Process // Process for the process manager
hjProcess *os.Process // Process for the HeroJobs server
// vfsManager interfaces.VFSManager // VFS manager implementation
config Config
startTime time.Time
}
// New creates a new instance of HeroLauncher with the provided configuration
func New(config Config) *HeroLauncher {
// Initialize modules
redisServer := redisserver.NewServer(redisserver.ServerConfig{
TCPPort: config.RedisTCPPort,
UnixSocketPath: config.RedisSocketPath,
})
executorService := executor.NewExecutor()
// Initialize process manager directly
pm := processmanager.NewProcessManager()
// Set the shared logs path for process manager
sharedLogsPath := filepath.Join(os.TempDir(), "heroagent_logs")
pm.SetLogsBasePath(sharedLogsPath)
// // Initialize VFS manager and client
// vfsManager := mock.NewMockVFSManager() // Using mock implementation for now
// Initialize template engine with debugging enabled
// Use absolute path for templates to avoid path resolution issues
absTemplatePath, err := filepath.Abs(config.TemplatesPath)
if err != nil {
log.Fatalf("Failed to get absolute path for templates: %v", err)
}
engine := jet.New(absTemplatePath, ".jet")
engine.Debug(true) // Enable debug mode to see template errors
// Reload templates on each render in development
engine.Reload(true)
// Initialize Fiber app
app := fiber.New(fiber.Config{
Views: engine,
ErrorHandler: func(c *fiber.Ctx, err error) error {
return c.Status(fiber.StatusInternalServerError).JSON(api.ErrorResponse{
Error: err.Error(),
})
},
})
// Middleware
app.Use(logger.New())
app.Use(recover.New())
app.Use(cors.New())
// Static files - serve all directories with proper paths
app.Static("/", config.StaticFilesPath)
app.Static("/css", config.StaticFilesPath+"/css")
app.Static("/js", config.StaticFilesPath+"/js")
app.Static("/img", config.StaticFilesPath+"/img")
app.Static("/favicon.ico", config.StaticFilesPath+"/favicon.ico")
// Create HeroLauncher instance
hl := &HeroLauncher{
app: app,
redisServer: redisServer,
executorService: executorService,
pm: pm,
// vfsManager: vfsManager,
config: config,
startTime: time.Now(),
}
// Initialize and register route handlers
hl.setupRoutes()
return hl
}
// setupRoutes initializes and registers all route handlers
func (hl *HeroLauncher) setupRoutes() {
// Initialize StatsManager
statsManager, err := stats.NewStatsManagerWithDefaults()
if err != nil {
log.Printf("Warning: Failed to initialize StatsManager: %v\n", err)
statsManager = nil
}
// Initialize API handlers
apiAdminHandler := api.NewAdminHandler(hl, statsManager)
apiServiceHandler := api.NewServiceHandler(hl.config.PMSocketPath, hl.config.PMSecret, log.Default())
// Initialize Page handlers
pageAdminHandler := pages.NewAdminHandler(hl, statsManager, hl.config.PMSocketPath, hl.config.PMSecret)
pageServiceHandler := pages.NewServiceHandler(hl.config.PMSocketPath, hl.config.PMSecret, log.Default())
// Initialize Jobs page handler
pageJobHandler, err := pages.NewJobHandler(hl.config.HJSocketPath, log.Default())
if err != nil {
log.Printf("Warning: Failed to initialize Jobs page handler: %v\n", err)
}
// Initialize JobHandler
jobHandler, err := handlers.NewJobHandler(hl.config.HJSocketPath, log.Default())
if err != nil {
log.Printf("Warning: Failed to initialize JobHandler: %v\n", err)
} else {
// Register Job routes
jobHandler.RegisterRoutes(hl.app)
}
// Register API routes
apiAdminHandler.RegisterRoutes(hl.app)
apiServiceHandler.RegisterRoutes(hl.app)
// Register Page routes
pageAdminHandler.RegisterRoutes(hl.app)
pageServiceHandler.RegisterRoutes(hl.app)
// Register Jobs page routes if handler was initialized successfully
if pageJobHandler != nil {
pageJobHandler.RegisterRoutes(hl.app)
}
// TODO: Move these to appropriate API or pages packages
executorHandler := api.NewExecutorHandler(hl.executorService)
//vfsHandler := routesold.NewVFSHandler(hl.vfsClient, log.Default())
// Create new API handlers
redisAddr := "localhost:" + hl.config.RedisTCPPort
redisHandler := api.NewRedisHandler(redisAddr, false)
jetHandler := api.NewJetHandler()
// Register legacy routes (to be migrated)
executorHandler.RegisterRoutes(hl.app)
//vfsHandler.RegisterRoutes(hl.app)
// Register new API routes
redisHandler.RegisterRoutes(hl.app)
jetHandler.RegisterRoutes(hl.app)
}
// GetUptime returns the uptime of the HeroLauncher server as a formatted string
func (hl *HeroLauncher) GetUptime() string {
// Calculate uptime based on the server's start time
uptimeDuration := time.Since(hl.startTime)
// Use more precise calculation for the uptime
totalSeconds := int(uptimeDuration.Seconds())
days := totalSeconds / (24 * 3600)
hours := (totalSeconds % (24 * 3600)) / 3600
minutes := (totalSeconds % 3600) / 60
seconds := totalSeconds % 60
// Format the uptime string based on the duration
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)
}
}
// startProcessManager starts the process manager as a background process
func (hl *HeroLauncher) startProcessManager() error {
_, filename, _, _ := runtime.Caller(0)
projectRoot := filepath.Join(filepath.Dir(filename), "../..")
processManagerPath := filepath.Join(projectRoot, "cmd/processmanager/main.go")
log.Printf("Starting process manager from: %s", processManagerPath)
// Check if processmanager is already running by testing the socket
if _, err := os.Stat(hl.config.PMSocketPath); err == nil {
// Try to connect to the socket to verify it's working
conn, err := net.Dial("unix", hl.config.PMSocketPath)
if err == nil {
// Socket is valid and we can connect to it
conn.Close()
log.Printf("Found existing process manager socket, using it instead of starting a new one")
return nil
}
// If socket exists but we can't connect, assume it's stale
log.Printf("Found existing socket, but can't connect to it: %v", err)
log.Printf("Removing stale socket and starting a new process manager")
_ = os.Remove(hl.config.PMSocketPath)
}
// Define shared logs path
sharedLogsPath := filepath.Join(os.TempDir(), "heroagent_logs")
// Ensure the logs directory exists
if err := os.MkdirAll(sharedLogsPath, 0755); err != nil {
log.Printf("Warning: Failed to create logs directory: %v", err)
}
// Start the process manager with the shared logs path
cmd := exec.Command("go", "run", processManagerPath,
"-socket", hl.config.PMSocketPath,
"-secret", hl.config.PMSecret,
"-logs", sharedLogsPath)
cmd.Stdout = os.Stdout
cmd.Stderr = os.Stderr
err := cmd.Start()
if err != nil {
return fmt.Errorf("failed to start process manager: %v", err)
}
hl.pmProcess = cmd.Process
log.Printf("Started process manager with PID: %d", cmd.Process.Pid)
// Wait for the process manager to start up
timeout := time.After(5 * time.Second)
ticker := time.NewTicker(100 * time.Millisecond)
defer ticker.Stop()
for {
select {
case <-ticker.C:
// Check if the socket exists
if _, err := os.Stat(hl.config.PMSocketPath); err == nil {
// If socket exists, assume process manager is running
log.Printf("Process manager is up and running")
return nil
}
case <-timeout:
return fmt.Errorf("timeout waiting for process manager to start")
}
}
}
// startHeroJobs starts the HeroJobs server as a background process
func (hl *HeroLauncher) startHeroJobs() error {
_, filename, _, _ := runtime.Caller(0)
projectRoot := filepath.Join(filepath.Dir(filename), "../..")
heroJobsPath := filepath.Join(projectRoot, "cmd/herojobs/main.go")
log.Printf("Starting HeroJobs from: %s", heroJobsPath)
// Check if HeroJobs is already running by testing the socket
if _, err := os.Stat(hl.config.HJSocketPath); err == nil {
// Try to connect to the socket to verify it's working
conn, err := net.Dial("unix", hl.config.HJSocketPath)
if err == nil {
// Socket is valid and we can connect to it
conn.Close()
log.Printf("Found existing HeroJobs socket, using it instead of starting a new one")
return nil
}
// If socket exists but we can't connect, assume it's stale
log.Printf("Found existing HeroJobs socket, but can't connect to it: %v", err)
log.Printf("Removing stale socket and starting a new HeroJobs server")
_ = os.Remove(hl.config.HJSocketPath)
}
// Define shared logs path
sharedLogsPath := filepath.Join(os.TempDir(), "heroagent_logs/jobs")
// Ensure the logs directory exists
if err := os.MkdirAll(sharedLogsPath, 0755); err != nil {
log.Printf("Warning: Failed to create logs directory: %v", err)
}
// Start HeroJobs with the shared logs path
cmd := exec.Command("go", "run", heroJobsPath,
"-socket", hl.config.HJSocketPath,
"-logs", sharedLogsPath)
cmd.Stdout = os.Stdout
cmd.Stderr = os.Stderr
err := cmd.Start()
if err != nil {
return fmt.Errorf("failed to start HeroJobs: %v", err)
}
// Store the process reference for graceful shutdown
hl.hjProcess = cmd.Process
log.Printf("Started HeroJobs with PID: %d", cmd.Process.Pid)
// Wait for HeroJobs to start up
timeout := time.After(5 * time.Second)
ticker := time.NewTicker(100 * time.Millisecond)
defer ticker.Stop()
for {
select {
case <-ticker.C:
// Check if the socket exists
if _, err := os.Stat(hl.config.HJSocketPath); err == nil {
// If socket exists, assume HeroJobs is running
log.Printf("HeroJobs is up and running")
return nil
}
case <-timeout:
return fmt.Errorf("timeout waiting for HeroJobs to start")
}
}
}
// Start starts the HeroLauncher server
func (hl *HeroLauncher) Start() error {
// Start the process manager first
err := hl.startProcessManager()
if err != nil {
log.Printf("Warning: Failed to start process manager: %v", err)
// Continue anyway, we'll just show warnings in the UI
}
// Start HeroJobs
err = hl.startHeroJobs()
if err != nil {
log.Printf("Warning: Failed to start HeroJobs: %v", err)
// Continue anyway, we'll just show warnings in the UI
}
// Setup graceful shutdown
c := make(chan os.Signal, 1)
signal.Notify(c, os.Interrupt, syscall.SIGTERM)
go func() {
<-c
log.Println("Shutting down server...")
// Kill the process manager if we started it
if hl.pmProcess != nil {
log.Println("Stopping process manager...")
_ = hl.pmProcess.Kill()
}
// Kill the HeroJobs server if we started it
if hl.hjProcess != nil {
log.Println("Stopping HeroJobs server...")
_ = hl.hjProcess.Kill()
}
_ = hl.app.Shutdown()
}()
// Start server
log.Printf("Starting server on :%s", hl.config.Port)
return hl.app.Listen(":" + hl.config.Port)
}