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:
2026-07-08 19:56:08 -04:00
parent ad3e879c42
commit a77f84ad34
8 changed files with 166 additions and 35 deletions
+66 -33
View File
@@ -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)
}