reaches the rsync and the ssh beneath it. Signalling the pid alone // would reap the wrapper and leave the transfer running with no record pointing at it. // // The recorded pid is verified to still be that job before it is signalled. // A stale json from a run killed by a reboot can name a pid the kernel has since reused. // /proc//cmdline is checked for the seed script's own path first, so at worst this // refuses to stop something; it cannot kill an unrelated process. // // Starting is delegated, not duplicated. // run_job.sh already refuses to start a job that is running, and media_seed.sh takes its // own lock. This endpoint does not re-implement either check — it dispatches and reports // what the record then says. // // REQUEST // POST action=start dispatch the seed detached // POST action=stop terminate a running seed // // RESPONSE // {"ok":true,"status":"running"} start: the job record went live within the wait window // {"ok":true,"stopped":true} stop: the process group was signalled // {"ok":false,"error":string} wrong method, unknown action, or nothing to act on // // DEPENDS ON // Rsync/media_seed.sh the job itself // Plugin/unraid/run_job.sh the wrapper that writes the record // include/config.php SCRIPTS_DIR // ═══════════════════════════════════════════════════════════════════════════════════════════════ header('Content-Type: application/json'); require_once dirname(__DIR__) . '/include/config.php'; if ($_SERVER['REQUEST_METHOD'] !== 'POST') { echo json_encode(['ok' => false, 'error' => 'POST only']); exit; } $action = $_POST['action'] ?? ''; $dir = rtrim(SCRIPTS_DIR, '/'); $script = $dir . '/Rsync/media_seed.sh'; $runner = $dir . '/Plugin/unraid/run_job.sh'; $stat = '/var/log/varaverk/Rsync/media_seed.json'; if ($action === 'start') { if (!is_file($script) || !is_file($runner)) { echo json_encode(['ok' => false, 'error' => 'media_seed.sh not found on this host']); exit; } $cmd = 'setsid /bin/bash ' . escapeshellarg($runner) . ' ' . escapeshellarg('Rsync/media_seed.sh') . ' ' . escapeshellarg($script) . ' --manual >/dev/null 2>&1 true, 'status' => 'running']); exit; } } usleep(500000); } echo json_encode(['ok' => false, 'error' => 'Seed did not start — check Rsync/media_seed.log']); exit; } if ($action === 'stop') { if (!is_file($stat)) { echo json_encode(['ok' => false, 'error' => 'No seed has been run on this host']); exit; } $j = json_decode((string)file_get_contents($stat), true); $pid = (int)($j['pid'] ?? 0); if (($j['status'] ?? '') !== 'running' || !$pid || !is_dir("/proc/$pid")) { echo json_encode(['ok' => false, 'error' => 'Seed is not running']); exit; } // See "The recorded pid is verified" above — cmdline is NUL-separated, so the script path // is matched against the raw bytes rather than a split. $cmdline = @file_get_contents("/proc/$pid/cmdline") ?: ''; if (strpos($cmdline, 'media_seed.sh') === false) { echo json_encode(['ok' => false, 'error' => 'Recorded PID is no longer the seed — record is stale']); exit; } shell_exec('kill -TERM -' . $pid . ' 2>/dev/null'); usleep(400000); if (is_dir("/proc/$pid")) shell_exec('kill -KILL -' . $pid . ' 2>/dev/null'); echo json_encode(['ok' => true, 'stopped' => true]); exit; } echo json_encode(['ok' => false, 'error' => 'Unknown action']);