// // DESIGN PRINCIPLES // Compares image ids, not pull output. // `docker pull` reports success whether or not anything changed. The image id before // and after is the only reliable signal, and it is what decides whether the container // is disturbed at all — an up-to-date container is never restarted. // // Prefers Unraid's own rebuild path. // When a rebuild helper is supplied and executable it is used, because recreating a // container correctly means reapplying its full template — ports, mounts, variables. // The stop/start fallback exists for the case where that helper is unavailable, and is // explicitly the lesser option: it picks up a new image only if the container was // already configured to be recreated on start. // // Every exit writes a terminal state. // Both outcomes end with a job-file write, so the poller always converges. A worker that // died without writing would leave the page spinning indefinitely. // // OPERATIONAL SAFEGUARDS // Refuses to run under a web server. // PHP_SAPI is checked first and a non-CLI invocation is answered with a 404 and no // output. Without that, requesting this file over HTTP would evaluate it with no $argv // at all — and it is a script whose entire job is to stop and restart containers. // // Required arguments are checked before anything runs. // Missing name, job file, or image exits non-zero before the first docker call, so a // malformed spawn cannot pull or restart anything. // // Every value interpolated into a shell command is escaped. // Image, container name and the rebuild helper path all go through escapeshellarg(), // even though they originate from docker_action.php rather than from a request. The // escaping is what stays correct if that caller ever changes. // // The rebuild helper is confirmed executable before it is invoked. // is_executable() gates it, so a missing or non-executable helper falls back to // stop/start rather than failing the update with a shell error. // // Failure is reported as failure. // A non-zero rebuild status writes ok:false with an explicit message. The image has // already been pulled at that point, so silently reporting success would leave a // container running an old image that the page claims was updated. // // Docker output is captured, never echoed. // Every call redirects stderr and the output is discarded or kept locally. This process // has no stdout consumer; writing to it would only risk corrupting the job file if the // two were ever pointed at the same place. // // ARGUMENTS // 1 name container name // 2 jobFile path the progress JSON is written to // 3 oldId image id before the pull, for the changed/unchanged comparison // 4 image image reference to pull // 5 rebuild optional path to Unraid's container rebuild helper // // JOB FILE STATES // {"ok":true,"status":"done","updated":false,"message":"Already up to date"} // {"ok":true,"status":"rebuilding"} // {"ok":true,"status":"done","updated":true,"message":"Updated and rebuilt"} // {"ok":false,"status":"done","error":"Rebuild failed after pull"} // // DEPENDS ON // api/docker_action.php spawns this worker and creates the job file path // docker CLI pull, image inspect, stop, start // ═══════════════════════════════════════════════════════════════════════════════════════════════ // This file stops and starts containers. It is a CLI worker and must never be reachable as a // web request — over HTTP there is no $argv, and every argument below would be undefined. if (PHP_SAPI !== 'cli') { http_response_code(404); exit(1); } [$name, $jobFile, $oldId, $image, $rebuild] = array_slice($argv, 1, 5) + array_fill(0, 5, ''); if (!$name || !$jobFile || !$image) exit(1); function jw(string $f, array $d): void { file_put_contents($f, json_encode($d)); } shell_exec('docker pull ' . escapeshellarg($image) . ' 2>&1'); $rawInfo = shell_exec('docker image inspect ' . escapeshellarg($image) . ' 2>/dev/null') ?: '[]'; $info = json_decode($rawInfo, true) ?: []; $newId = $info[0]['Id'] ?? ''; if ($oldId && $newId && $oldId === $newId) { jw($jobFile, ['ok' => true, 'status' => 'done', 'updated' => false, 'message' => 'Already up to date']); exit; } jw($jobFile, ['ok' => true, 'status' => 'rebuilding']); if ($rebuild && is_executable($rebuild)) { exec(escapeshellarg($rebuild) . ' ' . escapeshellarg($name) . ' 2>&1', $out, $rc); } else { exec('docker stop ' . escapeshellarg($name) . ' 2>&1', $o1, $rc1); exec('docker start ' . escapeshellarg($name) . ' 2>&1', $o2, $rc2); $rc = ($rc1 === 0 && $rc2 === 0) ? 0 : 1; } jw($jobFile, $rc === 0 ? ['ok' => true, 'status' => 'done', 'updated' => true, 'message' => 'Updated and rebuilt'] : ['ok' => false, 'status' => 'done', 'error' => 'Rebuild failed after pull'] );