|
230 | 230 | echo date('Y-m-d H:i:s') . " Auto-failed: job #{$zj['id']} ({$zj['task_type']}) — running >24h on online agent \"{$zj['agent_name']}\"\n"; |
231 | 231 | } |
232 | 232 |
|
| 233 | +// Step 2b-wol: Wake-on-LAN — queued backup work for sleeping clients (#326). |
| 234 | +// SchedulerService queues due backups for offline agents when WoL is enabled |
| 235 | +// (instead of marking the schedule missed). This step sends a magic packet |
| 236 | +// burst every minute until the agent's heartbeat brings it online (the job |
| 237 | +// then dispatches normally) or the per-client timeout expires, at which |
| 238 | +// point the job fails with a clear error. Only works when the BBS server |
| 239 | +// is on the same network as the client. |
| 240 | +$wolJobs = $db->fetchAll(" |
| 241 | + SELECT bj.id, bj.queued_at, bj.status_message, bj.backup_plan_id, |
| 242 | + a.id AS agent_id, a.name AS agent_name, a.ip_address, |
| 243 | + a.wol_mac, a.mac_address, a.wol_broadcast, a.wol_timeout_minutes |
| 244 | + FROM backup_jobs bj |
| 245 | + JOIN agents a ON a.id = bj.agent_id |
| 246 | + WHERE bj.status = 'queued' AND bj.task_type = 'backup' |
| 247 | + AND a.wol_enabled = 1 AND a.status = 'offline' |
| 248 | +"); |
| 249 | +foreach ($wolJobs as $wj) { |
| 250 | + $wolFail = function (string $error) use ($db, $wj, &$notificationService) { |
| 251 | + $db->update('backup_jobs', [ |
| 252 | + 'status' => 'failed', |
| 253 | + 'completed_at' => date('Y-m-d H:i:s'), |
| 254 | + 'error_log' => $error, |
| 255 | + ], 'id = ?', [$wj['id']]); |
| 256 | + $db->insert('server_log', [ |
| 257 | + 'agent_id' => $wj['agent_id'], |
| 258 | + 'backup_job_id' => $wj['id'], |
| 259 | + 'level' => 'error', |
| 260 | + 'message' => "Job #{$wj['id']} failed — {$error} (client \"{$wj['agent_name']}\")", |
| 261 | + ]); |
| 262 | + $notificationService = $notificationService ?? new NotificationService(); |
| 263 | + $notificationService->notify( |
| 264 | + 'backup_failed', |
| 265 | + $wj['agent_id'], |
| 266 | + $wj['backup_plan_id'] ? (int) $wj['backup_plan_id'] : null, |
| 267 | + "Backup failed on client \"{$wj['agent_name']}\" — {$error}", |
| 268 | + 'critical' |
| 269 | + ); |
| 270 | + echo date('Y-m-d H:i:s') . " Failed: job #{$wj['id']} — {$error}\n"; |
| 271 | + }; |
| 272 | + |
| 273 | + $mac = $wj['wol_mac'] ?: $wj['mac_address']; |
| 274 | + if (empty($mac)) { |
| 275 | + $wolFail('Wake-on-LAN is enabled but no MAC address is configured for this client'); |
| 276 | + continue; |
| 277 | + } |
| 278 | + |
| 279 | + $timeoutSecs = max(1, (int) $wj['wol_timeout_minutes']) * 60; |
| 280 | + $elapsed = time() - strtotime($wj['queued_at']); |
| 281 | + if ($elapsed > $timeoutSecs) { |
| 282 | + $wolFail("Client did not wake within {$wj['wol_timeout_minutes']} minute(s) after Wake-on-LAN"); |
| 283 | + continue; |
| 284 | + } |
| 285 | + |
| 286 | + $broadcast = $wj['wol_broadcast'] |
| 287 | + ?: \BBS\Services\WakeOnLanService::defaultBroadcast($wj['ip_address']) |
| 288 | + ?: '255.255.255.255'; |
| 289 | + $sent = \BBS\Services\WakeOnLanService::send($mac, $broadcast); |
| 290 | + |
| 291 | + $remaining = (int) ceil(($timeoutSecs - $elapsed) / 60); |
| 292 | + $db->update('backup_jobs', [ |
| 293 | + 'status_message' => $sent |
| 294 | + ? "Wake-on-LAN sent — waiting for client to wake ({$remaining}m left)" |
| 295 | + : "Wake-on-LAN send failed — retrying ({$remaining}m left)", |
| 296 | + ], 'id = ?', [$wj['id']]); |
| 297 | + |
| 298 | + if (empty($wj['status_message'])) { |
| 299 | + // First attempt for this job — log once, not every minute |
| 300 | + $db->insert('server_log', [ |
| 301 | + 'agent_id' => $wj['agent_id'], |
| 302 | + 'backup_job_id' => $wj['id'], |
| 303 | + 'level' => 'info', |
| 304 | + 'message' => "Wake-on-LAN magic packet sent to {$mac} via {$broadcast} for client \"{$wj['agent_name']}\"", |
| 305 | + ]); |
| 306 | + } |
| 307 | + echo date('Y-m-d H:i:s') . " WoL: magic packet " . ($sent ? 'sent' : 'send FAILED') . " to {$mac} via {$broadcast} (job #{$wj['id']}, {$remaining}m left)\n"; |
| 308 | +} |
| 309 | + |
233 | 310 | // Step 2c: Fail stale management tasks (update_borg, update_agent) after 7 days |
234 | 311 | // unpicked. These are excluded from Step 2 so they don't fail the moment the |
235 | 312 | // client's laptop goes to sleep, but we still need a safety valve — if an agent |
|
0 commit comments