CI / Lint (push) Failing after 14m46s
CI / Test (push) Failing after 4m23s
CI / Build (darwin/amd64) (push) Skipped
CI / Build (linux/amd64) (push) Skipped
CI / Build (windows/amd64) (push) Skipped
CI / Build (darwin/arm64) (push) Skipped
CI / Build (linux/arm64) (push) Skipped
CI / Build (windows/arm64) (push) Skipped
CI / Release (push) Skipped
121 lines
2.9 KiB
Go
121 lines
2.9 KiB
Go
// Package daemon orchestrates the main clip-sync loop: poll the local clipboard,
|
|
// broadcast changes to peers, and receive incoming clips via the HTTP server.
|
|
package daemon
|
|
|
|
import (
|
|
"context"
|
|
"log"
|
|
"os"
|
|
"sync"
|
|
"time"
|
|
|
|
"git.dracodev.net/Projets/clip-sync/internal/clipboard"
|
|
"git.dracodev.net/Projets/clip-sync/internal/config"
|
|
"git.dracodev.net/Projets/clip-sync/internal/dedup"
|
|
"git.dracodev.net/Projets/clip-sync/internal/peer"
|
|
"git.dracodev.net/Projets/clip-sync/internal/server"
|
|
)
|
|
|
|
// Daemon is the main clip-sync process.
|
|
type Daemon struct {
|
|
cfg *config.Config
|
|
clipboard clipboard.Clipboard
|
|
filter *dedup.Filter
|
|
server *server.Server
|
|
broadcaster *peer.Broadcaster
|
|
}
|
|
|
|
// New creates a Daemon from the given configuration.
|
|
func New(cfg *config.Config) (*Daemon, error) {
|
|
cb, err := clipboard.New()
|
|
if err != nil {
|
|
return nil, err
|
|
}
|
|
|
|
return newWithClipboard(cfg, cb)
|
|
}
|
|
|
|
// newWithClipboard creates a Daemon with a pre-existing clipboard (used by tests).
|
|
func newWithClipboard(cfg *config.Config, cb clipboard.Clipboard) (*Daemon, error) {
|
|
return newWithClipboardOrigin(cfg, cb, "")
|
|
}
|
|
|
|
// newWithClipboardOrigin creates a Daemon with a pre-existing clipboard and explicit origin.
|
|
func newWithClipboardOrigin(cfg *config.Config, cb clipboard.Clipboard, origin string) (*Daemon, error) {
|
|
if origin == "" {
|
|
var err error
|
|
origin, err = os.Hostname()
|
|
if err != nil {
|
|
return nil, err
|
|
}
|
|
}
|
|
|
|
filter := dedup.NewFilter(origin)
|
|
broadcaster := peer.NewBroadcaster(cfg.Peers, origin)
|
|
srv := server.New(cfg.ListenAddr(), cb, filter)
|
|
|
|
return &Daemon{
|
|
cfg: cfg,
|
|
clipboard: cb,
|
|
filter: filter,
|
|
server: srv,
|
|
broadcaster: broadcaster,
|
|
}, nil
|
|
}
|
|
|
|
// Run starts the daemon loop. It blocks until ctx is cancelled.
|
|
func (d *Daemon) Run(ctx context.Context) error {
|
|
// Start the HTTP server in a background goroutine.
|
|
var wg sync.WaitGroup
|
|
wg.Add(1)
|
|
go func() {
|
|
defer wg.Done()
|
|
log.Printf("clip-sync: listening on %s", d.cfg.ListenAddr())
|
|
if err := d.server.ListenAndServe(); err != nil {
|
|
// ListenAndServe returns http.ErrServerClosed on graceful shutdown.
|
|
if ctx.Err() == nil {
|
|
log.Printf("clip-sync: server error: %v", err)
|
|
}
|
|
}
|
|
}()
|
|
|
|
// Main clipboard polling loop.
|
|
ticker := time.NewTicker(d.cfg.PollInterval())
|
|
defer ticker.Stop()
|
|
|
|
var lastText string
|
|
|
|
log.Printf("clip-sync: polling clipboard every %v, %d peer(s) configured",
|
|
d.cfg.PollInterval(), len(d.cfg.Peers))
|
|
|
|
for {
|
|
select {
|
|
case <-ctx.Done():
|
|
d.server.Close()
|
|
wg.Wait()
|
|
return ctx.Err()
|
|
|
|
case <-ticker.C:
|
|
text, err := d.clipboard.Read()
|
|
if err != nil {
|
|
log.Printf("clip-sync: clipboard read: %v", err)
|
|
continue
|
|
}
|
|
|
|
if text == "" || text == lastText {
|
|
continue
|
|
}
|
|
|
|
lastText = text
|
|
|
|
// Record our write so the filter can block echoes.
|
|
d.filter.MarkWritten(text)
|
|
|
|
// Broadcast to all peers.
|
|
if len(d.cfg.Peers) > 0 {
|
|
d.broadcaster.Broadcast(text)
|
|
}
|
|
}
|
|
}
|
|
}
|