milestone 2 complete
This commit is contained in:
190
internal/daemon/daemon.go
Normal file
190
internal/daemon/daemon.go
Normal file
@ -0,0 +1,190 @@
|
||||
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")
|
||||
}
|
||||
}
|
||||
}
|
Reference in New Issue
Block a user