The response was discarded, so a refused stop looked like a completed one, and the payload the card redraws from is cached for 300s with no invalidation — the container carried on showing as running until the once-a-minute writer caught up. Stop now confirms; start still does not.
130 lines
6.6 KiB
PHP
130 lines
6.6 KiB
PHP
<?php
|
|
// ═══════════════════════════════════════════════════════════════════════════════════════════════
|
|
// PURPOSE
|
|
// Container update worker. Pulls one image, decides whether it actually changed, and
|
|
// recreates the container if it did — reporting progress through a job file the docker tab
|
|
// polls.
|
|
//
|
|
// OPERATIONAL MODEL
|
|
// Not an HTTP endpoint. This runs as a detached CLI process, spawned by docker_action.php,
|
|
// because a pull can take minutes and no web request should be held open for it. It lives
|
|
// under api/ because it is part of that endpoint's implementation, not because it is
|
|
// reachable over HTTP — and it refuses to run if it ever is.
|
|
//
|
|
// Progress is a file, not a return value. The parent request returns immediately with a job
|
|
// id; this process writes the current state to that file as it goes, and the page polls it.
|
|
// The file is the only channel between the two.
|
|
//
|
|
// Called as: php docker_pull_worker.php <name> <jobFile> <oldId> <image> <rebuild>
|
|
//
|
|
// 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);
|
|
|
|
// Only for vv_cache_clear() below. Required after the CLI guard, so a stray web request is turned
|
|
// away before this process loads anything at all.
|
|
require_once dirname(__DIR__) . '/include/config.php';
|
|
|
|
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;
|
|
}
|
|
|
|
// The container was stopped and started to get here whether or not the rebuild reported success,
|
|
// so the cached container list is out of date either way.
|
|
vv_cache_clear('monitor');
|
|
|
|
jw($jobFile, $rc === 0
|
|
? ['ok' => true, 'status' => 'done', 'updated' => true, 'message' => 'Updated and rebuilt']
|
|
: ['ok' => false, 'status' => 'done', 'error' => 'Rebuild failed after pull']
|
|
);
|