102 lines
5.0 KiB
PHP
102 lines
5.0 KiB
PHP
<?php
|
|
// ═══════════════════════════════════════════════════════════════════════════════════════════════
|
|
// PURPOSE
|
|
// Job status endpoint. The current run status of every scheduled job, keyed by job id, so
|
|
// the scheduler page can light up running indicators on a poll without the user opening
|
|
// anything.
|
|
//
|
|
// OPERATIONAL MODEL
|
|
// Status is not stored centrally — each job writes its own .json stat file beside its log,
|
|
// and this endpoint reads the schedule to know which ids exist, then collects one file per
|
|
// id. A job with no stat file has never run and is omitted entirely, which is how the page
|
|
// tells "never run" apart from "ran and finished".
|
|
//
|
|
// DESIGN PRINCIPLES
|
|
// The recorded status is cross-checked against the process table.
|
|
// A stat file saying "running" is only believed while /proc/<pid> 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/<pid> 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/<pid>/stat, which is more machinery than an indicator light justifies.
|
|
//
|
|
// REQUEST
|
|
// GET, no parameters
|
|
//
|
|
// RESPONSE
|
|
// {"<job id>":"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 <job>.json stat files written by run_job.sh
|
|
// ═══════════════════════════════════════════════════════════════════════════════════════════════
|
|
header('Content-Type: application/json');
|
|
require_once dirname(__DIR__) . '/include/scheduler.php';
|
|
|
|
// ?id=<Category/name.sh> — 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);
|