still exists. A // runner killed mid-job — OOM, reboot, kill -9 — never gets to write its own failure, // so trusting the file alone would leave the page showing a spinner forever. // // Reports status only. // No timestamps, no output, no exit codes. This is polled frequently and exists to // drive indicator state; the detail views fetch what they need from log.php. // // OPERATIONAL SAFEGUARDS // Read-only. Nothing here starts, stops, or reaps a job. // // Every file read degrades to an empty object. // @file_get_contents with a ?: '{}' fallback and a ?: [] after json_decode, so a stat // file that is unreadable or caught mid-write yields status 'unknown' rather than a // fatal that would blank every indicator on the page. // // A dead runner is reported as error, never as still running. // This is the safe direction to be wrong in: a job wrongly shown as finished prompts // someone to look, whereas one wrongly shown as running is silently ignored forever. // // Known limit: PID reuse. // /proc/ existing does not prove it is still *this* job's process. On a host that // has churned through the pid space a recycled pid could hold a dead job in 'running'. // Accepted rather than fixed — the alternative is a start-time comparison against // /proc//stat, which is more machinery than an indicator light justifies. // // REQUEST // GET, no parameters // // RESPONSE // {"":"running"|"success"|"error"|"unknown", …} // Jobs that have never run are absent from the object entirely. // // DEPENDS ON // include/scheduler.php vv_schedule_load(), vv_job_stat_path() // LOG_DIR .json stat files written by run_job.sh // ═══════════════════════════════════════════════════════════════════════════════════════════════ header('Content-Type: application/json'); require_once dirname(__DIR__) . '/include/scheduler.php'; // ?id= — one job, including jobs that are not on the schedule. // // The loop below only knows about scheduled jobs, so a job like Partnership/partnership_onboard.sh // — launched on demand, never cronned — was invisible to every status caller even though // run_job.sh writes it a stat file like any other. The UI had nothing to poll, which is why // pressing Onboard produced no visible change for the minutes it then ran. if (isset($_GET['id'])) { $id = trim($_GET['id']); if (!preg_match('/^[a-zA-Z0-9_.\/\-]+\.sh$/', $id) || str_contains($id, '..')) { echo json_encode(['ok' => false, 'error' => 'Invalid job id']); exit; } $statFile = vv_job_stat_path($id); if (!file_exists($statFile)) { echo json_encode(['ok' => true, 'id' => $id, 'status' => 'never_run']); exit; } $stat = json_decode(@file_get_contents($statFile) ?: '{}', true) ?: []; $status = $stat['status'] ?? 'unknown'; // Same liveness rule as the loop: a dead runner reports error, never "still running". if ($status === 'running' && !empty($stat['pid']) && !file_exists("/proc/{$stat['pid']}")) { $status = 'error'; } echo json_encode([ 'ok' => true, 'id' => $id, 'status' => $status, 'start' => $stat['start'] ?? null, 'end' => $stat['end'] ?? null, 'exit' => $stat['exit'] ?? null, ]); exit; } $result = []; foreach (array_keys(vv_schedule_load()) as $id) { $statFile = vv_job_stat_path($id); if (!file_exists($statFile)) continue; $stat = json_decode(@file_get_contents($statFile) ?: '{}', true) ?: []; $status = $stat['status'] ?? 'unknown'; if ($status === 'running' && !empty($stat['pid']) && !file_exists("/proc/{$stat['pid']}")) { $status = 'error'; } $result[$id] = $status; } echo json_encode($result);