package daemon import ( "fmt" "log" "sync" "time" "git.dws.rip/DWS/onyx/internal/core" "github.com/fsnotify/fsnotify" ) // Daemon manages the filesystem watching and automatic snapshot creation type Daemon struct { repo *core.OnyxRepository watcher *fsnotify.Watcher ticker *time.Ticker debounce time.Duration shutdown chan bool mu sync.Mutex isRunning bool // Debouncing state pendingChanges bool lastChangeTime time.Time } // Config holds daemon configuration options type Config struct { // Debounce duration for filesystem events (default: 500ms) Debounce time.Duration // Ticker interval for periodic checks (default: 1 second) TickerInterval time.Duration // Repository root path RepoPath string } // DefaultConfig returns the default daemon configuration func DefaultConfig() *Config { return &Config{ Debounce: 500 * time.Millisecond, TickerInterval: 1 * time.Second, } } // New creates a new Daemon instance func New(repo *core.OnyxRepository, config *Config) (*Daemon, error) { if config == nil { config = DefaultConfig() } watcher, err := fsnotify.NewWatcher() if err != nil { return nil, fmt.Errorf("failed to create filesystem watcher: %w", err) } return &Daemon{ repo: repo, watcher: watcher, ticker: time.NewTicker(config.TickerInterval), debounce: config.Debounce, shutdown: make(chan bool), isRunning: false, pendingChanges: false, }, nil } // Start begins the daemon's main loop func (d *Daemon) Start() error { d.mu.Lock() if d.isRunning { d.mu.Unlock() return fmt.Errorf("daemon is already running") } d.isRunning = true d.mu.Unlock() // Set up filesystem watchers if err := d.setupWatchers(); err != nil { d.isRunning = false return fmt.Errorf("failed to setup watchers: %w", err) } log.Println("Onyx daemon started") // Run the main event loop go d.run() return nil } // Stop gracefully shuts down the daemon func (d *Daemon) Stop() error { d.mu.Lock() if !d.isRunning { d.mu.Unlock() return fmt.Errorf("daemon is not running") } d.mu.Unlock() log.Println("Stopping Onyx daemon...") // Signal shutdown close(d.shutdown) // Clean up resources d.ticker.Stop() if err := d.watcher.Close(); err != nil { return fmt.Errorf("failed to close watcher: %w", err) } d.mu.Lock() d.isRunning = false d.mu.Unlock() log.Println("Onyx daemon stopped") return nil } // IsRunning returns whether the daemon is currently running func (d *Daemon) IsRunning() bool { d.mu.Lock() defer d.mu.Unlock() return d.isRunning } // run is the main event loop for the daemon func (d *Daemon) run() { for { select { case <-d.shutdown: return case event, ok := <-d.watcher.Events: if !ok { return } d.handleFileEvent(event) case err, ok := <-d.watcher.Errors: if !ok { return } log.Printf("Watcher error: %v", err) case <-d.ticker.C: d.processDebounced() } } } // handleFileEvent processes a filesystem event func (d *Daemon) handleFileEvent(event fsnotify.Event) { // Ignore events for .git and .onx directories if shouldIgnorePath(event.Name) { return } // Mark that we have pending changes d.mu.Lock() d.pendingChanges = true d.lastChangeTime = time.Now() d.mu.Unlock() log.Printf("File change detected: %s [%s]", event.Name, event.Op) } // processDebounced checks if enough time has passed since the last change // and creates a snapshot if needed func (d *Daemon) processDebounced() { d.mu.Lock() hasPending := d.pendingChanges timeSinceChange := time.Since(d.lastChangeTime) d.mu.Unlock() if hasPending && timeSinceChange >= d.debounce { d.mu.Lock() d.pendingChanges = false d.mu.Unlock() log.Println("Creating automatic snapshot...") if err := d.CreateSnapshot(); err != nil { log.Printf("Failed to create snapshot: %v", err) } else { log.Println("Snapshot created successfully") } } }