diff --git a/Plugin/usr/local/emhttp/plugins/varaverk/api/flag_toggle.php b/Plugin/usr/local/emhttp/plugins/varaverk/api/flag_toggle.php new file mode 100644 index 0000000..ce3eb8a --- /dev/null +++ b/Plugin/usr/local/emhttp/plugins/varaverk/api/flag_toggle.php @@ -0,0 +1,14 @@ + false, 'error' => 'Invalid flag name']); + exit; +} + +$ok = vv_conf_flag_set($name, $enabled); +echo json_encode(['ok' => $ok, 'error' => $ok ? null : 'Failed to write master.conf']); diff --git a/Plugin/usr/local/emhttp/plugins/varaverk/api/stop.php b/Plugin/usr/local/emhttp/plugins/varaverk/api/stop.php new file mode 100644 index 0000000..f8882ca --- /dev/null +++ b/Plugin/usr/local/emhttp/plugins/varaverk/api/stop.php @@ -0,0 +1,91 @@ + false, 'error' => 'Invalid id']); + exit; +} + +$statFile = vv_job_stat_path($id); +if (!file_exists($statFile)) { + echo json_encode(['ok' => false, 'error' => 'No stat file — script may not be running']); + exit; +} + +$stat = json_decode(file_get_contents($statFile) ?: '{}', true) ?: []; + +if (($stat['status'] ?? '') !== 'running') { + echo json_encode(['ok' => true, 'msg' => 'Not running']); + exit; +} + +$pid = (int)($stat['pid'] ?? 0); +if ($pid < 2) { + echo json_encode(['ok' => false, 'error' => 'No valid PID in stat file']); + exit; +} + +// Kill the whole process group so the script and all its children die together. +// pgid is usually the same as the session leader PID from run_job.sh. +$pgid = (int)trim(shell_exec("ps -o pgid= -p $pid 2>/dev/null") ?: '0'); + +if ($pgid > 1) { + shell_exec("kill -TERM -$pgid 2>/dev/null"); +} else { + // Fallback: kill the direct PID and its children + shell_exec("pkill -TERM -P $pid 2>/dev/null"); + shell_exec("kill -TERM $pid 2>/dev/null"); +} + +// Give it up to 3s to exit gracefully +$dead = false; +for ($i = 0; $i < 6; $i++) { + usleep(500000); + if (!file_exists("/proc/$pid")) { $dead = true; break; } +} + +// Force-kill if still alive +if (!$dead) { + if ($pgid > 1) shell_exec("kill -KILL -$pgid 2>/dev/null"); + shell_exec("pkill -KILL -P $pid 2>/dev/null"); + shell_exec("kill -KILL $pid 2>/dev/null"); + usleep(300000); + $dead = !file_exists("/proc/$pid"); +} + +// Clear any lock files in /tmp/unraid_locks whose content matches this PID +$lockDir = '/tmp/unraid_locks'; +$cleared = []; +foreach (glob("$lockDir/*.lock") ?: [] as $lf) { + $content = trim(file_get_contents($lf) ?: ''); + $lockPid = (int)explode(':', $content)[0]; + if ($lockPid === $pid || !file_exists("/proc/$lockPid")) { + @unlink($lf); + $cleared[] = basename($lf); + } +} + +// Also clear by script name in case PID rotated +$scriptBase = basename($id, '.sh'); +$namedLock = "$lockDir/{$scriptBase}.lock"; +if (file_exists($namedLock)) { + @unlink($namedLock); + if (!in_array(basename($namedLock), $cleared)) $cleared[] = basename($namedLock); +} + +// Update stat file +$now = time(); +$stat['status'] = 'stopped'; +$stat['end'] = $now; +$stat['exit'] = -1; +unset($stat['pid']); +file_put_contents($statFile, json_encode($stat)); + +echo json_encode([ + 'ok' => true, + 'killed' => $dead, + 'locks' => $cleared, +]); diff --git a/Plugin/usr/local/emhttp/plugins/varaverk/css/varaverk.css b/Plugin/usr/local/emhttp/plugins/varaverk/css/varaverk.css index 5e8c2d5..f44ea70 100644 --- a/Plugin/usr/local/emhttp/plugins/varaverk/css/varaverk.css +++ b/Plugin/usr/local/emhttp/plugins/varaverk/css/varaverk.css @@ -60,6 +60,9 @@ .vv-event-badge { flex: 0 0 auto; padding: 3px 8px; border-radius: 4px; font-size: 12px; background: #1a3a1a; border: 1px solid #2e6b2e; color: #6fcf6f; white-space: nowrap; font-weight: 500; } +.vv-flag-badge { flex: 0 0 auto; padding: 2px 6px; border-radius: 3px; font-size: 11px; + background: #2e2200; border: 1px solid #6b4e00; color: #d4a017; + white-space: nowrap; font-family: monospace; } .vv-log-label { display: flex; align-items: center; gap: 4px; font-size: 12px; color: #888; cursor: pointer; white-space: nowrap; flex-shrink: 0; } .vv-log-label input { cursor: pointer; accent-color: #4caf50; } diff --git a/Plugin/usr/local/emhttp/plugins/varaverk/include/scheduler.php b/Plugin/usr/local/emhttp/plugins/varaverk/include/scheduler.php index 37c2cb9..3e5e4d3 100644 --- a/Plugin/usr/local/emhttp/plugins/varaverk/include/scheduler.php +++ b/Plugin/usr/local/emhttp/plugins/varaverk/include/scheduler.php @@ -324,6 +324,30 @@ function vv_conf_script_map(): array { return $cache = $map; } +// Read a boolean flag value (e.g. INTERMEDIATE_RSYNC_ENABLED) from master.conf. +function vv_conf_flag_value(string $name): bool { + $conf = file_get_contents(CONF_DIR . '/master.conf') ?: ''; + if (preg_match('/^\s*' . preg_quote($name, '/') . '\s*=\s*(true|false)\s*$/m', $conf, $m)) { + return $m[1] === 'true'; + } + return false; +} + +// Write a boolean flag value to master.conf. +function vv_conf_flag_set(string $name, bool $value): bool { + $confPath = CONF_DIR . '/master.conf'; + $content = file_get_contents($confPath); + if ($content === false) return false; + $val = $value ? 'true' : 'false'; + $new = preg_replace( + '/^(\s*' . preg_quote($name, '/') . '\s*=\s*)(true|false)(\s*(?:#.*)?)$/m', + '${1}' . $val . '${3}', + $content, -1, $count + ); + if (!$count) return false; + return file_put_contents($confPath, $new) !== false; +} + // Comment or uncomment a script's line in the first master.conf array that contains it. function vv_conf_toggle_script(string $rel, bool $enable): bool { $confPath = CONF_DIR . '/master.conf'; @@ -365,6 +389,12 @@ function vv_script_children(string $orchPath, array $schedule): array { $seen = []; $confMap = vv_conf_script_map(); + // Detect which tier rsync flag this orch controls (e.g. "INTERMEDIATE" → INTERMEDIATE_RSYNC_ENABLED) + $rsyncFlagName = null; + if (preg_match('/check_rsync_enabled\s+"([A-Z]+)"/', $content, $rm)) { + $rsyncFlagName = $rm[1] . '_RSYNC_ENABLED'; + } + $addChild = function(string $rel) use ($scriptsDir, $schedule, $confMap, &$children, &$seen) { if (isset($seen[$rel]) || !file_exists("$scriptsDir/$rel")) return; $seen[$rel] = true; @@ -399,5 +429,18 @@ function vv_script_children(string $orchPath, array $schedule): array { } } + // Annotate Rsync/rsync.sh as a conf_flag child if this orch controls a rsync tier flag + if ($rsyncFlagName) { + foreach ($children as &$c) { + if ($c['id'] === 'Rsync/rsync.sh') { + $c['type'] = 'conf_flag'; + $c['flag_name'] = $rsyncFlagName; + $c['flag_value'] = vv_conf_flag_value($rsyncFlagName); + break; + } + } + unset($c); + } + return $children; } diff --git a/Plugin/usr/local/emhttp/plugins/varaverk/pages/scheduler.php b/Plugin/usr/local/emhttp/plugins/varaverk/pages/scheduler.php index 6318175..fde142d 100644 --- a/Plugin/usr/local/emhttp/plugins/varaverk/pages/scheduler.php +++ b/Plugin/usr/local/emhttp/plugins/varaverk/pages/scheduler.php @@ -71,6 +71,34 @@ foreach ($tree as $orch) {
+ @@ -636,13 +665,22 @@ function vvSaveOrch(el) { // Apply visual + interactive state to children based on whether orch is on or off. function vvApplyOrchState(orchCard, orchEnabled) { orchCard.querySelectorAll('.vv-script').forEach(child => { + const toggle = child.querySelector('.vv-enabled'); + const cronInput = child.querySelector('input.vv-cron'); + + // conf_flag children (e.g. Rsync): toggle reflects real flag value, enabled only when orch is on + if (child.dataset.type === 'conf_flag') { + toggle.checked = child.dataset.flagValue === '1'; + toggle.disabled = !orchEnabled; + if (cronInput) { cronInput.disabled = true; cronInput.style.opacity = '0.35'; } + return; + } + const confManaged = child.dataset.confManaged === '1'; const confEnabled = child.dataset.confEnabled === '1'; - const toggle = child.querySelector('.vv-enabled'); - const cronInput = child.querySelector('input.vv-cron'); if (orchEnabled) { - // Orch is god: set toggle from master.conf state; hide independent cron + // Orch is god: set toggle from master.conf state; dim independent cron toggle.checked = confManaged ? confEnabled : false; toggle.disabled = !confManaged; if (cronInput) { cronInput.disabled = true; cronInput.style.opacity = '0.35'; } @@ -661,6 +699,7 @@ function vvApplyOrchState(orchCard, orchEnabled) { } // Child toggle: conf_toggle when orch is on; scheduler save when orch is off. +// conf_flag children always write directly to master.conf regardless of orch state. function vvSaveChild(el) { const child = el.closest('[data-id]'); const orchCard = child.closest('.vv-sched-card'); @@ -668,6 +707,14 @@ function vvSaveChild(el) { const id = child.dataset.id; const enabled = el.checked; + if (child.dataset.type === 'conf_flag') { + const name = child.dataset.flagName; + child.dataset.flagValue = enabled ? '1' : '0'; + vvPost('/plugins/varaverk/api/flag_toggle.php', {name, enabled: enabled ? '1' : '0'}) + .then(d => { if (d.ok) vvFlashSaved(child); }); + return; + } + if (orchOn) { vvPost('/plugins/varaverk/api/conf_toggle.php', {id, enabled: enabled ? '1' : '0'}) .then(d => { if (d.ok) vvFlashSaved(child); }); @@ -682,9 +729,11 @@ function vvSaveChild(el) { function vvSaveAll() { const status = document.getElementById('vv-save-all-status'); // Collect jobs to save: orchs always; children only when their orch is OFF (orch manages them when on). + // conf_flag children are always immediate-save (flag_toggle.php), never batch-saved. const toSave = []; document.querySelectorAll('#vv-sched-left [data-id]').forEach(job => { if (job.classList.contains('vv-script')) { + if (job.dataset.type === 'conf_flag') return; // immediate-save only const orchCard = job.closest('.vv-sched-card'); const orchOn = orchCard?.querySelector('.vv-orch-row .vv-enabled')?.checked ?? false; if (orchOn) return; // child under active orch — cron suppressed, skip