package syncthing import ( "log" "os/exec" "sync" "time" ) // Process manages the Syncthing child process lifecycle. type Process struct { mu sync.Mutex cmd *exec.Cmd stopCh chan struct{} wg sync.WaitGroup running bool restarts int } // NewProcess creates a new Syncthing process manager. func NewProcess() *Process { return &Process{ stopCh: make(chan struct{}), } } // Start launches Syncthing and monitors it (restarts on crash). func (p *Process) Start() error { p.mu.Lock() if p.running { p.mu.Unlock() return nil } p.running = true p.mu.Unlock() p.wg.Add(1) go p.supervise() return nil } // Stop terminates the Syncthing process. func (p *Process) Stop() { p.mu.Lock() if !p.running { p.mu.Unlock() return } p.running = false p.mu.Unlock() close(p.stopCh) p.mu.Lock() cmd := p.cmd p.mu.Unlock() if cmd != nil && cmd.Process != nil { _ = cmd.Process.Kill() } p.wg.Wait() } // IsRunning returns whether the process is currently running. func (p *Process) IsRunning() bool { p.mu.Lock() defer p.mu.Unlock() return p.running && p.cmd != nil } func (p *Process) supervise() { defer p.wg.Done() backoff := 2 * time.Second maxBackoff := 30 * time.Second for { select { case <-p.stopCh: return default: } cmd := createSyncthingCmd() if cmd == nil { log.Println("syncthing binary not found in PATH") select { case <-p.stopCh: return case <-time.After(maxBackoff): } continue } p.mu.Lock() p.cmd = cmd p.mu.Unlock() log.Printf("starting syncthing (pid will follow)") err := cmd.Start() if err != nil { log.Printf("failed to start syncthing: %v", err) select { case <-p.stopCh: return case <-time.After(backoff): } continue } log.Printf("syncthing started (pid %d)", cmd.Process.Pid) err = cmd.Wait() p.mu.Lock() p.cmd = nil running := p.running p.restarts++ p.mu.Unlock() if !running { return } if err != nil { log.Printf("syncthing exited: %v (will restart in %v)", err, backoff) } else { log.Printf("syncthing exited normally (will restart in %v)", backoff) } select { case <-p.stopCh: return case <-time.After(backoff): } backoff *= 2 if backoff > maxBackoff { backoff = maxBackoff } } } func findSyncthing() string { path, err := exec.LookPath("syncthing") if err != nil { return "" } return path }