Varaverk: rsync flag toggle + stop button
- Rsync/rsync.sh children in Advanced now show as conf_flag type when the parent orch controls a *_RSYNC_ENABLED tier flag; toggle writes true/false to master.conf instead of comment/uncommenting a SCRIPTS array entry - Added vv_conf_flag_value() and vv_conf_flag_set() helpers in scheduler.php - Added api/flag_toggle.php endpoint (validates *_RSYNC_ENABLED pattern) - Added api/stop.php: kills process group, sweeps stuck locks, updates stat - vv-flag-badge CSS (amber, monospace) to distinguish from event/script rows - vvSaveChild() routes conf_flag children to flag_toggle.php unconditionally - vvApplyOrchState() keeps conf_flag toggle at real flag value; disables when orch off - vvSaveAll() skips conf_flag children (immediate-save only)
This commit is contained in:
@@ -0,0 +1,14 @@
|
||||
<?php
|
||||
header('Content-Type: application/json');
|
||||
require_once dirname(__DIR__) . '/include/scheduler.php';
|
||||
|
||||
$name = trim($_POST['name'] ?? '');
|
||||
$enabled = ($_POST['enabled'] ?? '0') === '1';
|
||||
|
||||
if (!$name || !preg_match('/^[A-Z_]+_RSYNC_ENABLED$/', $name)) {
|
||||
echo json_encode(['ok' => 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']);
|
||||
@@ -0,0 +1,91 @@
|
||||
<?php
|
||||
header('Content-Type: application/json');
|
||||
require_once dirname(__DIR__) . '/include/scheduler.php';
|
||||
|
||||
$id = trim($_POST['id'] ?? '');
|
||||
|
||||
if (!$id || !preg_match('/^[a-zA-Z0-9_.\/\-]+\.sh$/', $id) || str_contains($id, '..')) {
|
||||
echo json_encode(['ok' => 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,
|
||||
]);
|
||||
@@ -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; }
|
||||
|
||||
@@ -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;
|
||||
}
|
||||
|
||||
@@ -71,6 +71,34 @@ foreach ($tree as $orch) {
|
||||
<?php if (!empty($orch['children'])): ?>
|
||||
<div class="vv-children" style="display:none;">
|
||||
<?php foreach ($orch['children'] as $child): $cid = htmlspecialchars($child['id']); ?>
|
||||
<?php if (($child['type'] ?? 'script') === 'conf_flag'): ?>
|
||||
<div class="vv-script" data-id="<?= $cid ?>"
|
||||
data-type="conf_flag"
|
||||
data-flag-name="<?= htmlspecialchars($child['flag_name']) ?>"
|
||||
data-flag-value="<?= !empty($child['flag_value']) ? '1' : '0' ?>">
|
||||
<div class="vv-job-row">
|
||||
<label class="vv-toggle" title="Toggle <?= htmlspecialchars($child['flag_name']) ?> in master.conf">
|
||||
<input type="checkbox" class="vv-enabled"
|
||||
<?= !empty($child['flag_value']) ? 'checked' : '' ?>
|
||||
onchange="vvSaveChild(this)">
|
||||
<span class="vv-slider"></span>
|
||||
</label>
|
||||
<span class="vv-job-label"><?= htmlspecialchars($child['label']) ?></span>
|
||||
<span class="vv-flag-badge"><?= htmlspecialchars($child['flag_name']) ?></span>
|
||||
<span class="vv-save-check"></span>
|
||||
</div>
|
||||
<?php if (!empty($child['desc'])): ?>
|
||||
<div class="vv-job-desc" title="<?= htmlspecialchars($child['desc']) ?>"><?= htmlspecialchars($child['desc']) ?></div>
|
||||
<?php endif; ?>
|
||||
<div class="vv-job-actions">
|
||||
<button class="vv-btn-sm vv-run-btn" onclick="vvRunJob(this)">▶ Run</button>
|
||||
<button class="vv-btn-sm vv-dry-btn" onclick="vvDryRun(this)">▶ Dry Run</button>
|
||||
<?php $childLog = vv_job_log_path($child['id']); ?>
|
||||
<button class="vv-btn-sm vv-log-btn<?= (file_exists($childLog) && filesize($childLog) > 0) ? ' vv-has-log' : '' ?>" onclick="vvSelectLog(this)">Log</button>
|
||||
<span class="vv-job-dot"></span>
|
||||
</div>
|
||||
</div>
|
||||
<?php else: ?>
|
||||
<div class="vv-script" data-id="<?= $cid ?>"
|
||||
data-conf-managed="<?= $child['conf_managed'] ? '1' : '0' ?>"
|
||||
data-conf-enabled="<?= $child['conf_enabled'] === true ? '1' : ($child['conf_enabled'] === false ? '0' : '') ?>">
|
||||
@@ -106,6 +134,7 @@ foreach ($tree as $orch) {
|
||||
<span class="vv-job-dot"></span>
|
||||
</div>
|
||||
</div>
|
||||
<?php endif; ?>
|
||||
<?php endforeach; ?>
|
||||
</div>
|
||||
<?php endif; ?>
|
||||
@@ -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
|
||||
|
||||
Reference in New Issue
Block a user