Full Syncthing tray wrapper with: - System tray with 5 icon states (idle/syncing/paused/error/disconnected) - Syncthing REST API client with auto-discovered API key - Long-polling event listener for real-time status - Transfer rate monitoring, folder tracking, recent files, conflict counting - Full context menu with folders, recent files, settings toggles - Embedded admin panel binary (webview, requires CGO) - OS notifications via beeep (per-event configurable) - Syncthing process management with auto-restart - Cross-platform installer with autostart - CI pipeline for Linux (.deb + .tar.gz) and Windows (.zip) Co-Authored-By: Claude Opus 4.6 <noreply@anthropic.com>
This commit is contained in:
149
internal/syncthing/process.go
Normal file
149
internal/syncthing/process.go
Normal file
@@ -0,0 +1,149 @@
|
||||
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
|
||||
}
|
||||
Reference in New Issue
Block a user