varaverk: replace User Scripts dependency with native job tracking
run_job.sh: new wrapper called by scheduler cron for every job. Writes /var/log/varaverk/<id>.json (status/start/end/exit/pid) and /var/log/varaverk/<id>.log (latest run output, overwritten). Maps exit 0→ok, 1→warn, 2+→error so watchdog "took action" runs show warn not error. scheduler: cron now calls bash run_job.sh instead of inline bash -c. Added vv_job_stat_path(). LOG_DIR moved to config.php (shared). monitor: vv_scripts_status() now reads /var/log/varaverk/*.json and */*.json — no User Scripts tmpScripts dependency at all. Stale running detection via /proc/<pid> check. Returns error_count in addition to running/ok/warn counts. pages/monitor.php: added error pill (red ✗), error icon/color in list, duration shown next to age for each script entry.
This commit is contained in:
@@ -7,6 +7,7 @@ define('PLUGIN_CFG', '/boot/config/plugins/varaverk/varaverk.cfg');
|
|||||||
$_vv_cfg = @parse_ini_file(PLUGIN_CFG) ?: [];
|
$_vv_cfg = @parse_ini_file(PLUGIN_CFG) ?: [];
|
||||||
define('SCRIPTS_DIR', $_vv_cfg['SCRIPTS_DIR'] ?? '/mnt/user/appdata/unraid_scripts');
|
define('SCRIPTS_DIR', $_vv_cfg['SCRIPTS_DIR'] ?? '/mnt/user/appdata/unraid_scripts');
|
||||||
define('CONF_DIR', SCRIPTS_DIR . '/Configurations');
|
define('CONF_DIR', SCRIPTS_DIR . '/Configurations');
|
||||||
|
define('LOG_DIR', '/var/log/varaverk');
|
||||||
unset($_vv_cfg);
|
unset($_vv_cfg);
|
||||||
|
|
||||||
function vv_get_hostname(): string {
|
function vv_get_hostname(): string {
|
||||||
|
|||||||
@@ -648,37 +648,35 @@ function vv_log_tail(string $path, int $lines): string {
|
|||||||
}
|
}
|
||||||
|
|
||||||
function vv_scripts_status(): array {
|
function vv_scripts_status(): array {
|
||||||
$tmpBase = '/tmp/user.scripts/tmpScripts';
|
$logDir = LOG_DIR;
|
||||||
$runDir = '/tmp/user.scripts/running';
|
$statFiles = array_merge(
|
||||||
|
glob("$logDir/*.json") ?: [],
|
||||||
$running = [];
|
glob("$logDir/*/*.json") ?: []
|
||||||
foreach (glob($runDir . '/*') ?: [] as $f) {
|
);
|
||||||
$running[basename($f)] = true;
|
|
||||||
}
|
|
||||||
|
|
||||||
$scripts = [];
|
$scripts = [];
|
||||||
foreach (glob($tmpBase . '/*/') ?: [] as $dir) {
|
foreach ($statFiles as $statFile) {
|
||||||
$name = basename(rtrim($dir, '/'));
|
$stat = json_decode(@file_get_contents($statFile) ?: '{}', true) ?: [];
|
||||||
$logPath = $dir . 'log.txt';
|
$status = $stat['status'] ?? 'unknown';
|
||||||
$ts = @filemtime($logPath);
|
|
||||||
if (!$ts) continue;
|
|
||||||
|
|
||||||
$isRunning = isset($running[$name]);
|
// Stale running — PID gone (crash or reboot with no cleanup)
|
||||||
|
if ($status === 'running' && !empty($stat['pid'])) {
|
||||||
if ($isRunning) {
|
if (!file_exists("/proc/{$stat['pid']}")) $status = 'error';
|
||||||
$status = 'running';
|
|
||||||
} else {
|
|
||||||
$tail = vv_log_tail($logPath, 15);
|
|
||||||
$finished = stripos($tail, 'Script Finished') !== false;
|
|
||||||
if ($finished) {
|
|
||||||
$hasWarn = (bool)preg_match('/permission denied|command not found|no such file|: error[\s:]/i', $tail);
|
|
||||||
$status = $hasWarn ? 'warn' : 'ok';
|
|
||||||
} else {
|
|
||||||
$status = 'unknown';
|
|
||||||
}
|
|
||||||
}
|
}
|
||||||
|
|
||||||
$scripts[] = ['name' => $name, 'last_ts' => $ts, 'status' => $status, 'running' => $isRunning];
|
$id = $stat['id'] ?? basename($statFile, '.json');
|
||||||
|
$name = basename(preg_replace('/\.sh$/', '', $id));
|
||||||
|
$ts = (int)($stat['end'] ?? $stat['start'] ?? @filemtime($statFile) ?: 0);
|
||||||
|
|
||||||
|
$scripts[] = [
|
||||||
|
'name' => $name,
|
||||||
|
'last_ts' => $ts,
|
||||||
|
'status' => $status,
|
||||||
|
'running' => $status === 'running',
|
||||||
|
'exit' => $stat['exit'] ?? null,
|
||||||
|
'duration' => isset($stat['start'], $stat['end'])
|
||||||
|
? (int)$stat['end'] - (int)$stat['start'] : null,
|
||||||
|
];
|
||||||
}
|
}
|
||||||
|
|
||||||
usort($scripts, fn($a, $b) => ($b['last_ts'] ?? 0) <=> ($a['last_ts'] ?? 0));
|
usort($scripts, fn($a, $b) => ($b['last_ts'] ?? 0) <=> ($a['last_ts'] ?? 0));
|
||||||
@@ -689,5 +687,6 @@ function vv_scripts_status(): array {
|
|||||||
'running_count' => count(array_filter($scripts, fn($s) => $s['status'] === 'running')),
|
'running_count' => count(array_filter($scripts, fn($s) => $s['status'] === 'running')),
|
||||||
'ok_count' => count(array_filter($scripts, fn($s) => $s['status'] === 'ok')),
|
'ok_count' => count(array_filter($scripts, fn($s) => $s['status'] === 'ok')),
|
||||||
'warn_count' => count(array_filter($scripts, fn($s) => $s['status'] === 'warn')),
|
'warn_count' => count(array_filter($scripts, fn($s) => $s['status'] === 'warn')),
|
||||||
|
'error_count' => count(array_filter($scripts, fn($s) => $s['status'] === 'error')),
|
||||||
];
|
];
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -39,15 +39,18 @@ function vv_job_flags(string $id): string {
|
|||||||
return !empty($schedule[$id]['log_enabled']) ? '--log' : '';
|
return !empty($schedule[$id]['log_enabled']) ? '--log' : '';
|
||||||
}
|
}
|
||||||
|
|
||||||
define('LOG_DIR', '/var/log/varaverk');
|
|
||||||
|
|
||||||
function vv_job_log_path(string $id): string {
|
function vv_job_log_path(string $id): string {
|
||||||
return LOG_DIR . '/' . preg_replace('/\.sh$/', '.log', $id);
|
return LOG_DIR . '/' . preg_replace('/\.sh$/', '.log', $id);
|
||||||
}
|
}
|
||||||
|
|
||||||
|
function vv_job_stat_path(string $id): string {
|
||||||
|
return LOG_DIR . '/' . preg_replace('/\.sh$/', '.json', $id);
|
||||||
|
}
|
||||||
|
|
||||||
function vv_cron_rebuild(array $schedule): bool {
|
function vv_cron_rebuild(array $schedule): bool {
|
||||||
if (!is_dir(LOG_DIR)) mkdir(LOG_DIR, 0755, true);
|
if (!is_dir(LOG_DIR)) mkdir(LOG_DIR, 0755, true);
|
||||||
|
|
||||||
|
$runner = dirname(__DIR__) . '/run_job.sh';
|
||||||
$lines = ["# Varaverk — managed by plugin, do not edit manually"];
|
$lines = ["# Varaverk — managed by plugin, do not edit manually"];
|
||||||
$lines[] = "# Regenerated: " . date('Y-m-d H:i:s');
|
$lines[] = "# Regenerated: " . date('Y-m-d H:i:s');
|
||||||
$lines[] = "";
|
$lines[] = "";
|
||||||
@@ -56,11 +59,9 @@ function vv_cron_rebuild(array $schedule): bool {
|
|||||||
foreach ($schedule as $entry) {
|
foreach ($schedule as $entry) {
|
||||||
if (empty($entry['enabled']) || empty($entry['cron']) || empty($entry['id'])) continue;
|
if (empty($entry['enabled']) || empty($entry['cron']) || empty($entry['id'])) continue;
|
||||||
$script = "$scriptsDir/{$entry['id']}";
|
$script = "$scriptsDir/{$entry['id']}";
|
||||||
$logFile = vv_job_log_path($entry['id']);
|
|
||||||
$logDir = dirname($logFile);
|
|
||||||
if (!is_dir($logDir)) mkdir($logDir, 0755, true);
|
|
||||||
$flags = !empty($entry['log_enabled']) ? ' --log' : '';
|
$flags = !empty($entry['log_enabled']) ? ' --log' : '';
|
||||||
$lines[] = "{$entry['cron']} bash -c 'echo; echo \"── \$(date \"+%Y-%m-%d %H:%M:%S\") ──────────────────────\"; bash \"$script\"$flags' >> \"$logFile\" 2>&1";
|
$id = $entry['id'];
|
||||||
|
$lines[] = "{$entry['cron']} bash \"$runner\" \"$id\" \"$script\"$flags";
|
||||||
}
|
}
|
||||||
$lines[] = "";
|
$lines[] = "";
|
||||||
|
|
||||||
|
|||||||
@@ -341,6 +341,7 @@ function vvRenderScripts() {
|
|||||||
const running = sc.running_count ?? 0;
|
const running = sc.running_count ?? 0;
|
||||||
const ok = sc.ok_count ?? 0;
|
const ok = sc.ok_count ?? 0;
|
||||||
const warn = sc.warn_count ?? 0;
|
const warn = sc.warn_count ?? 0;
|
||||||
|
const errors = sc.error_count ?? 0;
|
||||||
const now = Math.floor(Date.now() / 1000);
|
const now = Math.floor(Date.now() / 1000);
|
||||||
|
|
||||||
function vvScriptPill(type, count, color, bg, border, icon) {
|
function vvScriptPill(type, count, color, bg, border, icon) {
|
||||||
@@ -354,7 +355,8 @@ function vvRenderScripts() {
|
|||||||
if (running > 0) html += vvScriptPill('running', running, '#4fc3f7', '#0a2233', '#1e4060', '●');
|
if (running > 0) html += vvScriptPill('running', running, '#4fc3f7', '#0a2233', '#1e4060', '●');
|
||||||
if (ok > 0) html += vvScriptPill('ok', ok, '#4caf50', '#0a1f0a', '#1a3a1a', '✓');
|
if (ok > 0) html += vvScriptPill('ok', ok, '#4caf50', '#0a1f0a', '#1a3a1a', '✓');
|
||||||
if (warn > 0) html += vvScriptPill('warn', warn, '#ff9800', '#1f1200', '#3a2200', '!');
|
if (warn > 0) html += vvScriptPill('warn', warn, '#ff9800', '#1f1200', '#3a2200', '!');
|
||||||
if (!running && !ok && !warn) html += `<span style="font-size:10px;color:#555;">No recent runs</span>`;
|
if (errors > 0) html += vvScriptPill('error', errors, '#f44336', '#2a0a0a', '#5a1a1a', '✗');
|
||||||
|
if (!running && !ok && !warn && !errors) html += `<span style="font-size:10px;color:#555;">No recent runs</span>`;
|
||||||
html += `</div>`;
|
html += `</div>`;
|
||||||
|
|
||||||
const filtered = vvScriptsFilter ? scList.filter(s => s.status === vvScriptsFilter) : scList;
|
const filtered = vvScriptsFilter ? scList.filter(s => s.status === vvScriptsFilter) : scList;
|
||||||
@@ -362,13 +364,14 @@ function vvRenderScripts() {
|
|||||||
filtered.forEach(s => {
|
filtered.forEach(s => {
|
||||||
const diff = now - (s.last_ts ?? now);
|
const diff = now - (s.last_ts ?? now);
|
||||||
const ago = diff < 60 ? diff + 's' : diff < 3600 ? Math.floor(diff / 60) + 'm' : diff < 86400 ? Math.floor(diff / 3600) + 'h' : Math.floor(diff / 86400) + 'd';
|
const ago = diff < 60 ? diff + 's' : diff < 3600 ? Math.floor(diff / 60) + 'm' : diff < 86400 ? Math.floor(diff / 3600) + 'h' : Math.floor(diff / 86400) + 'd';
|
||||||
const icon = s.status === 'running' ? '●' : s.status === 'ok' ? '✓' : s.status === 'warn' ? '!' : '?';
|
const icon = s.status === 'running' ? '●' : s.status === 'ok' ? '✓' : s.status === 'warn' ? '!' : s.status === 'error' ? '✗' : '?';
|
||||||
const color = s.status === 'running' ? '#4fc3f7' : s.status === 'ok' ? '#4caf50' : s.status === 'warn' ? '#ff9800' : '#555';
|
const color = s.status === 'running' ? '#4fc3f7' : s.status === 'ok' ? '#4caf50' : s.status === 'warn' ? '#ff9800' : s.status === 'error' ? '#f44336' : '#555';
|
||||||
const name = s.name.length > 24 ? s.name.slice(0, 23) + '…' : s.name;
|
const name = s.name.length > 24 ? s.name.slice(0, 23) + '…' : s.name;
|
||||||
|
const dur = s.duration != null ? ` ${s.duration}s` : '';
|
||||||
listHtml += `<div style="display:flex;align-items:center;gap:5px;margin-bottom:4px;font-size:11px;">
|
listHtml += `<div style="display:flex;align-items:center;gap:5px;margin-bottom:4px;font-size:11px;">
|
||||||
<span style="color:${color};flex-shrink:0;width:10px;text-align:center;">${icon}</span>
|
<span style="color:${color};flex-shrink:0;width:10px;text-align:center;">${icon}</span>
|
||||||
<span style="color:#888;flex:1;white-space:nowrap;overflow:hidden;text-overflow:ellipsis;">${name}</span>
|
<span style="color:#888;flex:1;white-space:nowrap;overflow:hidden;text-overflow:ellipsis;">${name}</span>
|
||||||
<span style="color:#444;font-size:10px;flex-shrink:0;">${ago}</span>
|
<span style="color:#444;font-size:10px;flex-shrink:0;">${ago}${dur}</span>
|
||||||
</div>`;
|
</div>`;
|
||||||
});
|
});
|
||||||
|
|
||||||
|
|||||||
@@ -0,0 +1,39 @@
|
|||||||
|
#!/bin/bash
|
||||||
|
# Varaverk job runner — wraps script execution with JSON status tracking.
|
||||||
|
# Called by /etc/cron.d/varaverk for every scheduled job.
|
||||||
|
#
|
||||||
|
# Usage: bash run_job.sh <job_id> <script_path> [flags...]
|
||||||
|
#
|
||||||
|
# Writes: /var/log/varaverk/<id>.json — status, timestamps, exit code, pid
|
||||||
|
# /var/log/varaverk/<id>.log — latest run output (overwritten each run)
|
||||||
|
#
|
||||||
|
# Status values: running → ok (exit 0) | warn (exit 1) | error (exit 2+)
|
||||||
|
|
||||||
|
JOB_ID="$1"
|
||||||
|
SCRIPT="$2"
|
||||||
|
shift 2
|
||||||
|
|
||||||
|
LOG_DIR="/var/log/varaverk"
|
||||||
|
BASE="${JOB_ID%.sh}"
|
||||||
|
LOG_FILE="$LOG_DIR/$BASE.log"
|
||||||
|
STAT_FILE="$LOG_DIR/$BASE.json"
|
||||||
|
|
||||||
|
mkdir -p "$(dirname "$LOG_FILE")" "$(dirname "$STAT_FILE")"
|
||||||
|
|
||||||
|
START=$(date +%s)
|
||||||
|
printf '{"id":"%s","status":"running","start":%s,"pid":%s}\n' \
|
||||||
|
"$JOB_ID" "$START" "$$" > "$STAT_FILE"
|
||||||
|
|
||||||
|
bash "$SCRIPT" "$@" > "$LOG_FILE" 2>&1
|
||||||
|
EC=$?
|
||||||
|
|
||||||
|
END=$(date +%s)
|
||||||
|
if [ "$EC" -eq 0 ]; then STATUS="ok"
|
||||||
|
elif [ "$EC" -eq 1 ]; then STATUS="warn"
|
||||||
|
else STATUS="error"
|
||||||
|
fi
|
||||||
|
|
||||||
|
printf '{"id":"%s","status":"%s","start":%s,"end":%s,"exit":%s}\n' \
|
||||||
|
"$JOB_ID" "$STATUS" "$START" "$END" "$EC" > "$STAT_FILE"
|
||||||
|
|
||||||
|
exit $EC
|
||||||
Reference in New Issue
Block a user