Bump version to 1.0.10
Smart WoL: TCP pre-check before sending magic packet (3s timeout). Machine status updates via SSE: online/offline tracked in DB and broadcast to all connected browser tabs in real-time. - Pre-check: if machine already reachable, skip WoL and mark online - WoL path: send 3 magic packets, wait for SSH, mark online/offline - Backend: setMachineStatus() helper + machine_status SSE event - Frontend: subscribeMachineStatus() SSE helper for Machines + Dashboard - IsReachable() helper in wol package for TCP reachability checks
This commit is contained in:
@@ -26,12 +26,13 @@ type Engine struct {
|
||||
}
|
||||
|
||||
type Event struct {
|
||||
Type string
|
||||
JobID int64
|
||||
Key string
|
||||
Value string
|
||||
Line string
|
||||
Stream string
|
||||
Type string
|
||||
JobID int64
|
||||
MachineID int64
|
||||
Key string
|
||||
Value string
|
||||
Line string
|
||||
Stream string
|
||||
}
|
||||
|
||||
func New(database interface{ SQLDB() *sql.DB }, cfg *config.Config) *Engine {
|
||||
@@ -116,9 +117,6 @@ func (e *Engine) Run(ctx context.Context, jobID int64, pairID int64) error {
|
||||
f.Close()
|
||||
}
|
||||
|
||||
e.setJobStatus(jobID, "waking_up")
|
||||
e.emit(Event{Type: "status", JobID: jobID, Key: "status", Value: "waking_up"})
|
||||
|
||||
var targetMachine *models.Machine
|
||||
var remotePort int
|
||||
if pair.Direction == "pull" && srcMachine != nil {
|
||||
@@ -129,32 +127,52 @@ func (e *Engine) Run(ctx context.Context, jobID int64, pairID int64) error {
|
||||
remotePort = dstMachine.Port
|
||||
}
|
||||
|
||||
if targetMachine != nil && targetMachine.WoLEnabled && targetMachine.MACAddress != nil {
|
||||
mac, err := wol.ParseMAC(*targetMachine.MACAddress)
|
||||
if err == nil {
|
||||
bcast := ""
|
||||
if targetMachine.BroadcastAddr != nil {
|
||||
bcast = *targetMachine.BroadcastAddr
|
||||
}
|
||||
wolErr := wol.Send(mac, bcast)
|
||||
if wolErr != nil {
|
||||
slog.Warn("WoL send failed", "host", targetMachine.Host, "error", wolErr)
|
||||
} else {
|
||||
slog.Info("WoL magic packet sent", "host", targetMachine.Host, "mac", *targetMachine.MACAddress)
|
||||
}
|
||||
wolShouldRun := targetMachine != nil &&
|
||||
targetMachine.WoLEnabled &&
|
||||
targetMachine.MACAddress != nil
|
||||
|
||||
timeout := time.Duration(targetMachine.WakeTimeoutSeconds) * time.Second
|
||||
interval := time.Duration(targetMachine.WakeCheckIntervalSeconds) * time.Second
|
||||
if err := wol.WaitUntilReady(jobCtx, targetMachine.Host, remotePort, timeout, interval); err != nil {
|
||||
e.setJobStatus(jobID, "failed")
|
||||
if wolErr != nil {
|
||||
e.setJobError(jobID, "wol_send_failed", wolErr.Error())
|
||||
e.emit(Event{Type: "status", JobID: jobID, Key: "status", Value: "failed", Line: fmt.Sprintf("WoL send failed: %v", wolErr)})
|
||||
return fmt.Errorf("WoL send failed: %w", wolErr)
|
||||
if wolShouldRun {
|
||||
if wol.IsReachable(jobCtx, targetMachine.Host, remotePort, 3*time.Second) {
|
||||
slog.Info("machine already reachable, skipping WoL", "host", targetMachine.Host)
|
||||
if targetMachine.ID > 0 {
|
||||
e.setMachineStatus(targetMachine.ID, "online")
|
||||
}
|
||||
} else {
|
||||
e.setJobStatus(jobID, "waking_up")
|
||||
e.emit(Event{Type: "status", JobID: jobID, Key: "status", Value: "waking_up"})
|
||||
|
||||
mac, err := wol.ParseMAC(*targetMachine.MACAddress)
|
||||
if err == nil {
|
||||
bcast := ""
|
||||
if targetMachine.BroadcastAddr != nil {
|
||||
bcast = *targetMachine.BroadcastAddr
|
||||
}
|
||||
wolErr := wol.Send(mac, bcast)
|
||||
if wolErr != nil {
|
||||
slog.Warn("WoL send failed", "host", targetMachine.Host, "error", wolErr)
|
||||
} else {
|
||||
slog.Info("WoL magic packet sent", "host", targetMachine.Host, "mac", *targetMachine.MACAddress)
|
||||
}
|
||||
|
||||
timeout := time.Duration(targetMachine.WakeTimeoutSeconds) * time.Second
|
||||
interval := time.Duration(targetMachine.WakeCheckIntervalSeconds) * time.Second
|
||||
if err := wol.WaitUntilReady(jobCtx, targetMachine.Host, remotePort, timeout, interval); err != nil {
|
||||
e.setJobStatus(jobID, "failed")
|
||||
if wolErr != nil {
|
||||
e.setJobError(jobID, "wol_send_failed", wolErr.Error())
|
||||
e.emit(Event{Type: "status", JobID: jobID, Key: "status", Value: "failed", Line: fmt.Sprintf("WoL send failed: %v", wolErr)})
|
||||
return fmt.Errorf("WoL send failed: %w", wolErr)
|
||||
}
|
||||
if targetMachine.ID > 0 {
|
||||
e.setMachineStatus(targetMachine.ID, "offline")
|
||||
}
|
||||
e.setJobError(jobID, "wol_timeout", err.Error())
|
||||
e.emit(Event{Type: "status", JobID: jobID, Key: "status", Value: "failed", Line: err.Error()})
|
||||
return fmt.Errorf("machine not ready: %w", err)
|
||||
}
|
||||
if targetMachine.ID > 0 {
|
||||
e.setMachineStatus(targetMachine.ID, "online")
|
||||
}
|
||||
e.setJobError(jobID, "wol_timeout", err.Error())
|
||||
e.emit(Event{Type: "status", JobID: jobID, Key: "status", Value: "failed", Line: err.Error()})
|
||||
return fmt.Errorf("machine not ready: %w", err)
|
||||
}
|
||||
}
|
||||
}
|
||||
@@ -281,6 +299,21 @@ func (e *Engine) setJobError(jobID int64, code, message string) {
|
||||
jobRepo.SetError(jobID, code, message)
|
||||
}
|
||||
|
||||
func (e *Engine) setMachineStatus(machineID int64, status string) {
|
||||
machineRepo := models.NewMachineRepository(e.db)
|
||||
if err := machineRepo.UpdateStatus(machineID, status); err != nil {
|
||||
slog.Warn("failed to update machine status",
|
||||
"machine_id", machineID, "status", status, "error", err)
|
||||
return
|
||||
}
|
||||
e.emit(Event{
|
||||
Type: "machine_status",
|
||||
MachineID: machineID,
|
||||
Key: "status",
|
||||
Value: status,
|
||||
})
|
||||
}
|
||||
|
||||
func (e *Engine) emit(evt Event) {
|
||||
e.eventBus.Publish(evt)
|
||||
}
|
||||
|
||||
@@ -0,0 +1,39 @@
|
||||
package syncengine
|
||||
|
||||
import (
|
||||
"context"
|
||||
"net"
|
||||
"testing"
|
||||
"time"
|
||||
|
||||
"github.com/syncserver/internal/wol"
|
||||
)
|
||||
|
||||
func TestIsReachable(t *testing.T) {
|
||||
ln, err := net.ListenTCP("tcp4", &net.TCPAddr{IP: net.ParseIP("127.0.0.1"), Port: 0})
|
||||
if err != nil {
|
||||
t.Skipf("skipping test (no TCP listener available): %v", err)
|
||||
}
|
||||
defer ln.Close()
|
||||
addr := ln.Addr().(*net.TCPAddr)
|
||||
|
||||
ctx := context.Background()
|
||||
|
||||
if !wol.IsReachable(ctx, "127.0.0.1", addr.Port, 500*time.Millisecond) {
|
||||
t.Error("expected reachable on listening port")
|
||||
}
|
||||
if wol.IsReachable(ctx, "127.0.0.1", addr.Port+1, 500*time.Millisecond) {
|
||||
t.Error("expected unreachable on non-listening port")
|
||||
}
|
||||
}
|
||||
|
||||
func TestIsReachableTimeout(t *testing.T) {
|
||||
ctx := context.Background()
|
||||
start := time.Now()
|
||||
if wol.IsReachable(ctx, "192.0.2.1", 12345, 500*time.Millisecond) {
|
||||
t.Error("expected unreachable for non-routable IP")
|
||||
}
|
||||
if d := time.Since(start); d < 400*time.Millisecond {
|
||||
t.Errorf("IsReachable returned too early: %v", d)
|
||||
}
|
||||
}
|
||||
Reference in New Issue
Block a user