package main
import (
"context"
"fmt"
"log"
"os"
"os/exec"
"os/signal"
"syscall"
"time"
"github.com/msenol/gorev/internal/api"
"github.com/msenol/gorev/internal/config"
"github.com/msenol/gorev/internal/daemon"
"github.com/spf13/cobra"
)
func createDaemonCommand() *cobra.Command {
var daemonPort string
var detach bool
var serverMode string
var dbPath string
cmd := &cobra.Command{
Use: "daemon",
Short: "Run Gorev as background daemon process",
Long: `Start Gorev daemon server that provides:
- HTTP REST API for workspace management
- WebSocket for real-time updates (future)
- MCP proxy connection endpoint
- Shared database connection pool
The daemon runs as a single process that multiple MCP clients can connect to,
eliminating port conflicts and reducing resource usage.
Server Modes:
- local: Each workspace uses its own .gorev/gorev.db (default for local dev)
- centralized: Single database with workspace_id isolation (for Docker/remote)`,
Example: ` # Start daemon in foreground (local mode - default)
gorev daemon
# Start daemon in centralized mode (for Docker/remote)
gorev daemon --mode=centralized --db-path=/data/gorev.db
# Start daemon in background (detached)
gorev daemon --detach
# Start daemon on custom port
gorev daemon --port 5083
# Check daemon status
curl http://localhost:5082/api/health`,
RunE: func(cmd *cobra.Command, args []string) error {
// Setup server configuration
cfg := config.DefaultConfig()
if serverMode != "" {
cfg.Mode = config.ServerMode(serverMode)
}
if dbPath != "" {
cfg.CentralizedDBPath = dbPath
}
cfg.Port = daemonPort
cfg.AllowLocalPaths = cfg.Mode == config.ModeLocal
config.SetGlobalConfig(cfg)
if detach {
return runDetachedDaemon(daemonPort, serverMode, dbPath)
}
return runDaemon(daemonPort)
},
}
cmd.Flags().StringVar(&daemonPort, "port", "5082", "Daemon HTTP API port")
cmd.Flags().BoolVar(&detach, "detach", false, "Run as background process (daemon)")
cmd.Flags().StringVar(&serverMode, "mode", "", "Server mode: local (default) or centralized")
cmd.Flags().StringVar(&dbPath, "db-path", "", "Database path (for centralized mode)")
return cmd
}
func runDaemon(port string) error {
cfg := config.GetGlobalConfig()
log.Printf("š Starting Gorev Daemon on port %s (mode: %s)...", port, cfg.Mode)
// Create lock file
if err := daemon.CreateLockFile(os.Getpid(), port, version); err != nil {
return fmt.Errorf("failed to create lock file: %w", err)
}
defer func() {
if err := daemon.RemoveLockFile(); err != nil {
log.Printf("Warning: Failed to remove lock file: %v", err)
}
}()
// Initialize workspace manager (multi-workspace support)
workspaceManager := api.NewWorkspaceManager()
// Set embedded migrations
migrationsFS, err := getEmbeddedMigrationsFS()
if err != nil {
return fmt.Errorf("failed to get embedded migrations: %w", err)
}
workspaceManager.SetMigrationsFS(migrationsFS)
// Create API server (pure multi-workspace, no legacy single workspace)
apiServer := api.NewAPIServer(port, nil) // nil for legacy isYonetici
// Serve static files (Web UI)
if err := api.ServeStaticFiles(apiServer.App(), WebDistFS); err != nil {
log.Printf("Warning: Failed to serve web UI: %v", err)
}
// Setup signal handling for graceful shutdown
sigChan := make(chan os.Signal, 1)
signal.Notify(sigChan, syscall.SIGINT, syscall.SIGTERM)
// Start server in background
errChan := make(chan error, 1)
go func() {
if err := apiServer.Start(); err != nil {
errChan <- fmt.Errorf("daemon server failed: %w", err)
}
}()
log.Printf("ā
Gorev Daemon started successfully")
log.Printf("š¦ Mode: %s", cfg.Mode)
if cfg.Mode == config.ModeCentralized {
log.Printf("š¾ Database: %s", cfg.CentralizedDBPath)
} else {
log.Printf("š¾ Database: Per-workspace (.gorev/gorev.db)")
}
log.Printf("š± Web UI: http://localhost:%s", port)
log.Printf("š§ API: http://localhost:%s/api/v1", port)
log.Printf("š WebSocket: ws://localhost:%s/ws (future)", port)
log.Printf("š Lock file: %s", daemon.GetLockFilePath())
log.Printf("\nPress Ctrl+C to stop daemon")
// Wait for shutdown signal or error
select {
case sig := <-sigChan:
log.Printf("\nš Received signal %v, shutting down gracefully...", sig)
case err := <-errChan:
log.Printf("\nā Server error: %v", err)
return err
}
// Graceful shutdown
ctx, cancel := context.WithTimeout(context.Background(), 30*time.Second)
defer cancel()
if err := apiServer.Shutdown(ctx); err != nil {
return fmt.Errorf("daemon shutdown failed: %w", err)
}
log.Printf("ā
Daemon stopped successfully")
return nil
}
func runDetachedDaemon(port, mode, dbPath string) error {
// Get current executable path
exePath, err := os.Executable()
if err != nil {
return fmt.Errorf("failed to get executable path: %w", err)
}
// Build command arguments
args := []string{"daemon", "--port", port}
if mode != "" {
args = append(args, "--mode", mode)
}
if dbPath != "" {
args = append(args, "--db-path", dbPath)
}
// Fork process and run in background
cmd := exec.Command(exePath, args...)
cmd.Stdout = nil
cmd.Stderr = nil
cmd.Stdin = nil
// Start detached process
if err := cmd.Start(); err != nil {
return fmt.Errorf("failed to start daemon process: %w", err)
}
// Wait a moment for daemon to initialize
time.Sleep(500 * time.Millisecond)
// Verify daemon is running
daemonURL := fmt.Sprintf("http://localhost:%s", port)
if err := daemon.WaitForDaemon(daemonURL, 10*time.Second); err != nil {
return fmt.Errorf("daemon failed to start: %w", err)
}
modeStr := mode
if modeStr == "" {
modeStr = "local"
}
log.Printf("ā
Daemon started in background")
log.Printf("š PID: %d", cmd.Process.Pid)
log.Printf("š URL: %s", daemonURL)
log.Printf("š¦ Mode: %s", modeStr)
log.Printf("š¾ Lock file: %s", daemon.GetLockFilePath())
log.Printf("\nUse 'curl %s/api/health' to check status", daemonURL)
return nil
}
// stopDaemonCommand creates a command to stop running daemon
func createDaemonStopCommand() *cobra.Command {
return &cobra.Command{
Use: "daemon-stop",
Short: "Stop running Gorev daemon",
Long: "Gracefully stop the running Gorev daemon process",
RunE: func(cmd *cobra.Command, args []string) error {
lock, err := daemon.ReadLockFile()
if err != nil {
return fmt.Errorf("no daemon running: %w", err)
}
log.Printf("š Stopping daemon (PID: %d)...", lock.PID)
// Send SIGTERM for graceful shutdown
process, err := os.FindProcess(lock.PID)
if err != nil {
return fmt.Errorf("failed to find daemon process: %w", err)
}
if err := process.Signal(syscall.SIGTERM); err != nil {
return fmt.Errorf("failed to send stop signal: %w", err)
}
// Wait for process to exit
for i := 0; i < 30; i++ {
if !daemon.IsProcessRunning(lock.PID) {
log.Printf("ā
Daemon stopped successfully")
return nil
}
time.Sleep(1 * time.Second)
}
// Force kill if still running
log.Printf("ā ļø Daemon did not stop gracefully, forcing kill...")
if err := process.Kill(); err != nil {
return fmt.Errorf("failed to kill daemon: %w", err)
}
log.Printf("ā
Daemon forcefully stopped")
return nil
},
}
}
// statusDaemonCommand creates a command to check daemon status
func createDaemonStatusCommand() *cobra.Command {
return &cobra.Command{
Use: "daemon-status",
Short: "Check Gorev daemon status",
Long: "Display status and information about running Gorev daemon",
RunE: func(cmd *cobra.Command, args []string) error {
lock, err := daemon.GetDaemonInfo()
if err != nil {
fmt.Println("ā Daemon Status: NOT RUNNING")
fmt.Printf(" Reason: %v\n", err)
return nil
}
fmt.Println("ā
Daemon Status: RUNNING")
fmt.Printf(" PID: %d\n", lock.PID)
fmt.Printf(" URL: %s\n", lock.DaemonURL)
fmt.Printf(" Port: %s\n", lock.Port)
fmt.Printf(" Version: %s\n", lock.Version)
fmt.Printf(" Started: %s\n", lock.StartTime.Format(time.RFC3339))
fmt.Printf(" Uptime: %s\n", time.Since(lock.StartTime).Round(time.Second))
fmt.Printf(" Lock File: %s\n", daemon.GetLockFilePath())
return nil
},
}
}