8e08c73f60
Full-stack Go monolith with embedded React frontend for orchestrating rsync-over-SSH file synchronization with Wake-on-LAN support. Features: - JWT auth (HS256) with bcrypt password hashing - CRUD for machines (with WoL config) and sync_pairs - Ed25519 SSH key generation and known_hosts management - WoL magic packet sender + TCP-connect waiter with backoff - Sync engine: rsync subprocess, per-pair job queue, progress parsing - Homebrew cron parser for scheduled syncs - SSE stream for live job status (queued/waking_up/running/success/failed) - React+TS+Vite+Tailwind SPA embedded via embed.FS - Debian packaging with systemd unit, postinst/prerm/postrm Tech stack: - Go 1.22+ (CGO_ENABLED=0, pure SQLite via modernc.org/sqlite) - chi router for HTTP API - TypeScript + React 18 + Tailwind CSS frontend - Cross-compiled to Linux amd64 for Proxmox LXC deployment Tests: wol (MAC parsing, magic packet), syncengine/queue, scheduler/cron
69 lines
1.5 KiB
Go
69 lines
1.5 KiB
Go
package wol
|
|
|
|
import (
|
|
"context"
|
|
"fmt"
|
|
"net"
|
|
"os/exec"
|
|
"time"
|
|
)
|
|
|
|
var ErrTimeout = fmt.Errorf("timeout waiting for machine to respond")
|
|
|
|
func WaitUntilReady(ctx context.Context, host string, sshPort int, maxWait, checkInterval time.Duration, usePing bool) error {
|
|
deadline := time.Now().Add(maxWait)
|
|
interval := checkInterval
|
|
|
|
ticker := time.NewTicker(checkInterval)
|
|
defer ticker.Stop()
|
|
|
|
for {
|
|
select {
|
|
case <-ctx.Done():
|
|
return ctx.Err()
|
|
case <-ticker.C:
|
|
}
|
|
|
|
addr := fmt.Sprintf("%s:%d", host, sshPort)
|
|
dialer := net.Dialer{Timeout: 3 * time.Second}
|
|
conn, err := dialer.DialContext(ctx, "tcp", addr)
|
|
if err == nil {
|
|
conn.Close()
|
|
return nil
|
|
}
|
|
|
|
if usePing {
|
|
cmd := exec.CommandContext(ctx, "ping", "-c", "1", "-W", "1", host)
|
|
if err := cmd.Run(); err == nil {
|
|
return nil
|
|
}
|
|
}
|
|
|
|
if time.Now().After(deadline) {
|
|
return ErrTimeout
|
|
}
|
|
|
|
elapsed := time.Since(time.Now().Add(-maxWait))
|
|
if elapsed > maxWait/2 && interval < 10*time.Second {
|
|
interval = interval * 3 / 2
|
|
if interval > 10*time.Second {
|
|
interval = 10 * time.Second
|
|
}
|
|
ticker.Reset(interval)
|
|
}
|
|
}
|
|
}
|
|
|
|
func IsHostReachable(host string, port int, timeout time.Duration) bool {
|
|
ctx, cancel := context.WithTimeout(context.Background(), timeout)
|
|
defer cancel()
|
|
addr := fmt.Sprintf("%s:%d", host, port)
|
|
dialer := net.Dialer{Timeout: timeout}
|
|
conn, err := dialer.DialContext(ctx, "tcp", addr)
|
|
if err == nil {
|
|
conn.Close()
|
|
return true
|
|
}
|
|
return false
|
|
}
|