Some checks failed
Release / build (push) Failing after 19s
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>
150 lines
2.4 KiB
Go
150 lines
2.4 KiB
Go
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
|
|
}
|