package commands import ( "fmt" "os" "os/exec" "path/filepath" "strconv" "syscall" "git.dws.rip/DWS/onyx/internal/core" "github.com/spf13/cobra" ) // NewDaemonCmd creates the daemon command with start, stop, and status subcommands func NewDaemonCmd() *cobra.Command { cmd := &cobra.Command{ Use: "daemon", Short: "Manage the Onyx daemon for transparent versioning", Long: `The daemon command controls the Onyx background daemon that monitors your repository for changes and automatically creates snapshots.`, } cmd.AddCommand(newDaemonStartCmd()) cmd.AddCommand(newDaemonStopCmd()) cmd.AddCommand(newDaemonStatusCmd()) return cmd } // newDaemonStartCmd creates the daemon start subcommand func newDaemonStartCmd() *cobra.Command { return &cobra.Command{ Use: "start", Short: "Start the Onyx daemon", Long: `Starts the Onyx daemon in the background to monitor the repository.`, RunE: runDaemonStart, } } // newDaemonStopCmd creates the daemon stop subcommand func newDaemonStopCmd() *cobra.Command { return &cobra.Command{ Use: "stop", Short: "Stop the Onyx daemon", Long: `Gracefully stops the running Onyx daemon.`, RunE: runDaemonStop, } } // newDaemonStatusCmd creates the daemon status subcommand func newDaemonStatusCmd() *cobra.Command { return &cobra.Command{ Use: "status", Short: "Check the Onyx daemon status", Long: `Checks if the Onyx daemon is running and displays its status.`, RunE: runDaemonStatus, } } // runDaemonStart starts the daemon in the background func runDaemonStart(cmd *cobra.Command, args []string) error { // Get current directory cwd, err := os.Getwd() if err != nil { return fmt.Errorf("failed to get current directory: %w", err) } // Check if this is an Onyx repository if !core.IsOnyxRepo(cwd) { return fmt.Errorf("not an Onyx repository") } // Open repository to get .onx path repo, err := core.Open(cwd) if err != nil { return fmt.Errorf("failed to open repository: %w", err) } defer repo.Close() pidFile := filepath.Join(repo.GetOnyxPath(), "daemon.pid") // Check if daemon is already running if isDaemonRunning(pidFile) { return fmt.Errorf("daemon is already running") } // Find the onxd binary onxdPath, err := exec.LookPath("onxd") if err != nil { // Try to find it in the same directory as onx onxPath, err := os.Executable() if err != nil { return fmt.Errorf("failed to locate onxd binary: %w", err) } onxdPath = filepath.Join(filepath.Dir(onxPath), "onxd") if _, err := os.Stat(onxdPath); err != nil { return fmt.Errorf("onxd binary not found. Please ensure it's installed") } } // Start the daemon in the background daemonCmd := exec.Command(onxdPath, "--repo", cwd) daemonCmd.Stdout = nil daemonCmd.Stderr = nil daemonCmd.SysProcAttr = &syscall.SysProcAttr{ Setsid: true, // Create new session } if err := daemonCmd.Start(); err != nil { return fmt.Errorf("failed to start daemon: %w", err) } // Detach the process if err := daemonCmd.Process.Release(); err != nil { return fmt.Errorf("failed to release daemon process: %w", err) } fmt.Println("Onyx daemon started successfully") return nil } // runDaemonStop stops the running daemon func runDaemonStop(cmd *cobra.Command, args []string) error { // Get current directory cwd, err := os.Getwd() if err != nil { return fmt.Errorf("failed to get current directory: %w", err) } // Check if this is an Onyx repository if !core.IsOnyxRepo(cwd) { return fmt.Errorf("not an Onyx repository") } // Open repository to get .onx path repo, err := core.Open(cwd) if err != nil { return fmt.Errorf("failed to open repository: %w", err) } defer repo.Close() pidFile := filepath.Join(repo.GetOnyxPath(), "daemon.pid") // Read PID from file pid, err := readPIDFile(pidFile) if err != nil { if os.IsNotExist(err) { return fmt.Errorf("daemon is not running (no PID file found)") } return fmt.Errorf("failed to read PID file: %w", err) } // Check if process exists process, err := os.FindProcess(pid) if err != nil { return fmt.Errorf("failed to find daemon process: %w", err) } // Send SIGTERM to gracefully stop the daemon if err := process.Signal(syscall.SIGTERM); err != nil { return fmt.Errorf("failed to stop daemon: %w", err) } fmt.Println("Onyx daemon stopped successfully") return nil } // runDaemonStatus checks the daemon status func runDaemonStatus(cmd *cobra.Command, args []string) error { // Get current directory cwd, err := os.Getwd() if err != nil { return fmt.Errorf("failed to get current directory: %w", err) } // Check if this is an Onyx repository if !core.IsOnyxRepo(cwd) { return fmt.Errorf("not an Onyx repository") } // Open repository to get .onx path repo, err := core.Open(cwd) if err != nil { return fmt.Errorf("failed to open repository: %w", err) } defer repo.Close() pidFile := filepath.Join(repo.GetOnyxPath(), "daemon.pid") if isDaemonRunning(pidFile) { pid, _ := readPIDFile(pidFile) fmt.Printf("Onyx daemon is running (PID: %d)\n", pid) } else { fmt.Println("Onyx daemon is not running") } return nil } // isDaemonRunning checks if the daemon is running based on the PID file func isDaemonRunning(pidFile string) bool { pid, err := readPIDFile(pidFile) if err != nil { return false } // Check if process exists process, err := os.FindProcess(pid) if err != nil { return false } // Send signal 0 to check if process is alive err = process.Signal(syscall.Signal(0)) return err == nil } // readPIDFile reads the PID from a file func readPIDFile(path string) (int, error) { data, err := os.ReadFile(path) if err != nil { return 0, err } pid, err := strconv.Atoi(string(data[:len(data)-1])) // Remove trailing newline if err != nil { return 0, fmt.Errorf("invalid PID file: %w", err) } return pid, nil }