Namespace-migration find/replace (XC_VM -> \XC_VM) leaked a backslash into
many plain string literals: CLI echo/log text, the MaxMind/GitHub outbound
User-Agents, the LB tarball temp path, process-match greps, the sysctl and
cron markers, the proxy ini section, and docblocks.
Most are cosmetic or self-consistent, but two carried upgrade hazards on
already-deployed servers, handled with migration guards:
- sysctl marker: writer + first-line check moved to `# XC_VM` together
(one-time benign rewrite on upgrade).
- cron marker: switched to `# XC_VM` and added a purge to both root-crontab
installers that drops lines with either the old `# \XC_VM` or the new
`# XC_VM` marker before re-adding, so upgrades don't duplicate cron jobs.
The xc_vm user crontab already does a full `crontab -r` rebuild.
ProxyCommand was removed in E1, so no process is ever titled XC_VMProxy[<id>]
anymore — the process-name checks in ProcessManager::isMonitorAlive and
StreamProcess::stopStream that matched it can never be true. Remove the dead
references; the real titles (XC_VM[], LLOD[], Loopback[], the m3u8/ts patterns)
are untouched.
Not touched: the live.php non-proxy .ts chase-read (E2) and HLSGenerator
generateHLS / segment.php tmpfs-serving (E3) — those are the active
daemon-reachability fallback (daemon down, re-feed windows, and the
proxy/restreamer/llod2 types not yet canary-validated), so deleting them now
would remove the rollback. Gated on Phase D stability across all types.
Audited the streaming subsystem for symbols with zero real callers (ruled out
dynamic dispatch: command-name strings, routes, #[ListensTo], self::/$this->
internal calls, bare-name string dispatch). Removals, each grep-verified across
all of src/:
- Whole class TS (src/Streaming/TimeshiftClient.php) — superseded by the inline
TS byte-parsing in LLOD/Loopback; only ref left was a stale comment. Drops its
2 require + 3 use lines too.
- StreamUtils::getTSInfo — 0 callers.
- SegmentReader::getLLODSegments — replaced by the LLOD-v3 daemon feed; 0 callers.
- ProcessChecker::isPIDRunning + isPIDsRunning (dead pair) — 0 callers; drops the
now-orphaned CurlClient import.
- ProcessManager: checkPidFile + matchesCmdline (dead pair), killByPattern,
countProcesses, currentPid, releaseCronLock — all 0 callers (acquireCronLock
stays; locks self-release on exit).
- AsyncFileOperations: checkFilesExists, awaitFileExistsAdaptive, awaitFileModified,
getCacheStats, filterExistingFiles — never-wired public helpers, 0 refs.
php -l clean on all touched files; make gates green; no phpstan-baseline entries
reference the removed symbols.
Phase E (ADR 0003), step 1: proxy live streams are now daemon-only. The
daemon owns the proxy puller (registered + off-air-probed in live.php), so
the legacy producer + relay are dead code and are removed:
- delete Cli/Commands/ProxyCommand.php (the XC_VMProxy producer).
- remove StreamProcess::startProxy and ProcessManager::startProxy (only
caller was live.php).
- live.php: drop the startProxy branch in the process block and the
AF_UNIX socket relay in the proxy delivery arm. A proxy stream whose
daemon is unreachable ($rFanout false) now shows not-on-air — the
keepalive restarts the daemon in ~2s — instead of falling back to the
legacy producer.
This removes the proxy fallback: the daemon is required for proxy delivery
(its own crash resilience is the keepalive). Non-proxy legacy stays for now.
PHPStan clean, 443 tests.
Refs ADR 0003 (Phase E).
Fixes five runtime errors seen in production panel logs (v2.3.9):
- HomeController: player home page fatally crashed (count(): null given)
when content/tmdb_popular was absent or corrupt. Guard the read +
unserialize, default movies/series to arrays, null-safe the counts.
- ShutdownHandler: "array offset on null" on every live session close
when $rChannelInfo was null (vod/timeshift or early exit). Use !empty().
- ProcessManager::acquireCronLock: TOCTOU race — a competing cron removed
the lock file between file_exists() and file_get_contents()/filemtime(),
emitting "failed to open stream"/"stat failed". Read content+mtime once
under @, and take the lock if the file vanished.
- AuthRepository::updateCodes: "foreach() argument must be array, null"
when a code's whitelist JSON decoded to null. Cast to (array).
- admin/review.php: "Undefined array key extension" for URLs without an
extension. Coalesce pathinfo()['extension'] to ''.
cron:servers only checked whether a watchdog process existed. A watchdog
blocked in poll() on a half-open MariaDB socket (CLOSE_WAIT) inside its DB
ping still exists, so cron kept trusting it indefinitely: last_check_ago
went stale and the panel marked the node offline while nginx, php-fpm,
redis and active streams were all healthy. Restarting only the watchdog
restored the heartbeat immediately.
Treat an abnormally old watchdog as stale. A normal generation lives only
a few seconds, so any process alive for more than 90s is killed and a
fresh generation is started; a plain presence check alone would keep
trusting the wedged one forever.
Add ProcessManager::getProcessAge(), which derives age from the mtime of
the /proc/PID directory (time() - filemtime) — the same figure as
`ps -o etimes` without shelling out, consistent with the rest of the class
reading /proc directly and with no HZ/CLK_TCK assumption. Reaping uses the
existing ProcessManager::kill() rather than a raw `kill -9`.
Recovery restarts only the watchdog; nginx, php-fpm, redis, ffmpeg and
running streams are untouched.
Two false positives/negatives in process detection kept the panel's
self-healing loop dead on any real installation:
- isNginxRunning() looked for "nginx: master" only among xc_vm-owned
processes, but the master runs as root on typical installs (workers run
as xc_vm), so cron:servers/cron:streams bailed out with "XC_VM not
running..." before reaching the daemon revival block. Now scans
/proc/*/cmdline user-agnostically, same as RootSignalsCronJob.
- The per-daemon "is it alive" checks piped ps through grep by bare words:
every live-stream ffmpeg carries -thread_queue_size/-max_muxing_queue_size
in its command line, so the "queue" check matched any running stream and
the encode queue daemon was never started while at least one channel was
up — created channels sat at "0% DONE" until console.php queue was run by
hand. All seven checks (signals, cache_handler, network, watchdog, queue,
ondemand, scanner) now use ProcessManager::findProcessPIDs(), a /proc
cmdline scan matching the daemon title (XC_VM[...]) or its exact
console.php invocation. Kill branches use the same PID list — the old
bare "ondemand"/"scanner" greps could kill an innocent ffmpeg whose
source URL contained those words.