mirror of
https://github.com/Vateron-Media/XC_VM.git
synced 2026-09-17 04:01:41 +02:00
The cs-fix auto-typing narrowed the pid parameters of the /proc-based process checks to a non-nullable `int` (read from the `@param int $pid` docblocks). But every one of these methods was written to tolerate a missing pid: each opens with `$pid = (int) $pid;` and a `$pid <= 0` guard, and their callers routinely pass nullable DB columns (`tv_archive_pid`, `monitor_pid`, `vframes_pid`, `streams.pid`, …) and runtime PIDs that can be null. In production this crashed the monitor cron: ProcessManager::isNamedProcessRunning(): Argument #1 ($pid) must be of type int, null given, called in Cli/Commands/MonitorCommand.php:322 Make the pid parameter nullable (`?int`) on the eight affected public checks — isRunning, isNamedProcessRunning, isStreamRunning, producerKind, resourceSample, kill, getProcessAge, isStreamAlive, isMonitorAlive — so the existing `(int) null === 0` cast and guard return the safe "not running" result instead of a fatal TypeError. The docblocks are corrected to `int|null` as well so a future cs-fix run does not re-narrow the types. procExists stays `int` (internal, only reached after the guard). No behaviour change for valid pids.
669 lines
21 KiB
PHP
669 lines
21 KiB
PHP
<?php
|
|
|
|
namespace XcVm\Core\Process;
|
|
|
|
/**
|
|
* Process Manager
|
|
*
|
|
* Centralizes process management: PID checking, killing, cron locks,
|
|
* /proc filesystem inspection. Replaces scattered posix_kill(),
|
|
* shell_exec('ps ...'), and file_exists('/proc/PID') calls.
|
|
*
|
|
* Usage:
|
|
*
|
|
* // Check if a process is running
|
|
* if (ProcessManager::isRunning($pid)) { ... }
|
|
*
|
|
* // Check if a process with specific executable is running
|
|
* if (ProcessManager::isRunning($pid, 'ffmpeg')) { ... }
|
|
*
|
|
* // Check named process (XC_VM[123], Thumbnail[456], etc.)
|
|
* if (ProcessManager::isNamedProcessRunning($pid, 'XC_VM', $streamId, PHP_BIN)) { ... }
|
|
*
|
|
* // Kill a process
|
|
* ProcessManager::kill($pid);
|
|
* ProcessManager::kill($pid, SIGTERM); // graceful
|
|
*
|
|
* // Cron locking
|
|
* ProcessManager::acquireCronLock('/tmp/cron_streams.pid', 1800);
|
|
* // ... do work ...
|
|
* // Lock file cleaned up automatically on exit
|
|
*
|
|
* @package XC_VM_Core_Process
|
|
* @author Divarion_D <https://github.com/Divarion-D>
|
|
* @copyright 2025-2026 Vateron Media
|
|
* @link https://github.com/Vateron-Media/XC_VM
|
|
* @license AGPL-3.0 https://www.gnu.org/licenses/agpl-3.0.html
|
|
*/
|
|
|
|
class ProcessManager {
|
|
/** @var array Static cache for /proc existence checks */
|
|
protected static $procCache = [];
|
|
|
|
/** @var float Cache TTL in seconds */
|
|
protected static $cacheTtl = 1.0;
|
|
|
|
// ───────────────────────────────────────────────────────────
|
|
// Process Checking
|
|
// ───────────────────────────────────────────────────────────
|
|
|
|
/**
|
|
* Check if a process is running via /proc filesystem
|
|
*
|
|
* @param int|null $pid Process ID (null/0 -> not running)
|
|
* @param string|null $exe Expected executable name (e.g., 'ffmpeg', 'php')
|
|
* @return bool
|
|
*/
|
|
public static function isRunning(?int $pid, ?string $exe = null) {
|
|
$pid = (int) $pid;
|
|
|
|
if ($pid <= 0) {
|
|
return false;
|
|
}
|
|
|
|
if (!self::procExists($pid)) {
|
|
return false;
|
|
}
|
|
|
|
// If no exe filter — just check /proc exists
|
|
if ($exe === null) {
|
|
return true;
|
|
}
|
|
|
|
// Check executable matches
|
|
if (!is_readable('/proc/' . $pid . '/exe')) {
|
|
return false;
|
|
}
|
|
|
|
$actualExe = @basename(@readlink('/proc/' . $pid . '/exe'));
|
|
|
|
return strpos($actualExe, basename($exe)) === 0;
|
|
}
|
|
|
|
/**
|
|
* Check if a named process is running (e.g., XC_VM[123])
|
|
*
|
|
* Reads /proc/PID/cmdline and matches against "NAME[ID]" pattern.
|
|
*
|
|
* @param int|null $pid Process ID (null/0 -> not running)
|
|
* @param string $processName Process name prefix (e.g., 'XC_VM', 'Thumbnail', 'TVArchive')
|
|
* @param int|string $identifier Stream/task ID
|
|
* @param string|null $exe Expected executable (default: PHP_BIN)
|
|
* @return bool
|
|
*/
|
|
public static function isNamedProcessRunning(?int $pid, string $processName, int|string $identifier, ?string $exe = null) {
|
|
$pid = (int) $pid;
|
|
|
|
if ($pid <= 0) {
|
|
return false;
|
|
}
|
|
|
|
if ($exe === null && defined('PHP_BIN')) {
|
|
$exe = PHP_BIN;
|
|
}
|
|
|
|
clearstatcache(true);
|
|
|
|
if (!self::procExists($pid)) {
|
|
return false;
|
|
}
|
|
|
|
if ($exe && !is_readable('/proc/' . $pid . '/exe')) {
|
|
return false;
|
|
}
|
|
|
|
if ($exe) {
|
|
$actualExe = @basename(@readlink('/proc/' . $pid . '/exe'));
|
|
if (strpos($actualExe, basename($exe)) !== 0) {
|
|
return false;
|
|
}
|
|
}
|
|
|
|
$cmdline = trim(@file_get_contents('/proc/' . $pid . '/cmdline'));
|
|
$expected = $processName . '[' . $identifier . ']';
|
|
|
|
return $cmdline === $expected;
|
|
}
|
|
|
|
/**
|
|
* Check if a stream (ffmpeg/php) process is running
|
|
*
|
|
* Specialized check for streaming processes that match
|
|
* either ffmpeg with specific stream output files, or PHP processes.
|
|
*
|
|
* @param int|null $pid Process ID (null/0 -> not running)
|
|
* @param int $streamId Stream ID
|
|
* @return bool
|
|
*/
|
|
public static function isStreamRunning(?int $pid, int $streamId) {
|
|
$pid = (int) $pid;
|
|
|
|
if ($pid <= 0) {
|
|
return false;
|
|
}
|
|
|
|
if (!self::procExists($pid) || !is_readable('/proc/' . $pid . '/exe')) {
|
|
return false;
|
|
}
|
|
|
|
$exe = @basename(@readlink('/proc/' . $pid . '/exe'));
|
|
|
|
if (strpos($exe, 'ffmpeg') === 0) {
|
|
$cmdline = trim(@file_get_contents('/proc/' . $pid . '/cmdline'));
|
|
return (stristr($cmdline, '/' . $streamId . '_.m3u8') ||
|
|
stristr($cmdline, '/' . $streamId . '_%d.ts'));
|
|
}
|
|
|
|
// The fanout daemon's native remuxer (`xc_fanout remux … <streams>/<id>_.m3u8`),
|
|
// which produces a copy-only stream in ffmpeg's place. Its own subcommand
|
|
// and this stream's playlist must both be there: the daemon process shares
|
|
// the executable but names no stream playlist.
|
|
if (strpos($exe, 'xc_fanout') === 0) {
|
|
$cmdline = (string) @file_get_contents('/proc/' . $pid . '/cmdline');
|
|
return strpos($cmdline, "\0remux\0") !== false && strpos($cmdline, '/' . $streamId . '_.m3u8') !== false;
|
|
}
|
|
|
|
if (strpos($exe, 'php') === 0) {
|
|
return true;
|
|
}
|
|
|
|
return false;
|
|
}
|
|
|
|
/**
|
|
* What kind of producer a stream's pid is, for the panel's stream list.
|
|
*
|
|
* @param int|null $pid Process ID (null/0 -> not running)
|
|
* @return string|null 'fanout' (xc_fanout remux), 'ffmpeg', 'php' (the LLOD
|
|
* segmenter / loopback relay), or null when it cannot be read.
|
|
*/
|
|
public static function producerKind(?int $pid) {
|
|
$pid = (int) $pid;
|
|
|
|
if ($pid <= 0 || !self::procExists($pid) || !is_readable('/proc/' . $pid . '/exe')) {
|
|
return null;
|
|
}
|
|
|
|
$exe = @basename(@readlink('/proc/' . $pid . '/exe'));
|
|
|
|
if (strpos($exe, 'xc_fanout') === 0) {
|
|
$cmdline = (string) @file_get_contents('/proc/' . $pid . '/cmdline');
|
|
return strpos($cmdline, "\0remux\0") !== false ? 'fanout' : null;
|
|
}
|
|
if (strpos($exe, 'ffmpeg') === 0) {
|
|
return 'ffmpeg';
|
|
}
|
|
if (strpos($exe, 'php') === 0) {
|
|
return 'php';
|
|
}
|
|
|
|
return null;
|
|
}
|
|
|
|
/**
|
|
* One CPU/memory reading for a process, from /proc/PID/stat.
|
|
*
|
|
* CPU is cumulative (the ticks the process has burned since it started), so a
|
|
* percentage needs two readings — see cpuPercent(). Memory is resident set
|
|
* size in bytes, which is the figure that matters for a box running hundreds
|
|
* of encoders: what they actually hold in RAM.
|
|
*
|
|
* @param int|null $pid Process ID (null/0 -> not running)
|
|
* @return array{ticks:int,rss:int,at:float,start:int}|null Null when the process is gone.
|
|
*/
|
|
public static function resourceSample(?int $pid) {
|
|
$pid = (int) $pid;
|
|
|
|
if ($pid <= 1) {
|
|
return null;
|
|
}
|
|
|
|
$stat = @file_get_contents('/proc/' . $pid . '/stat');
|
|
if (!is_string($stat) || $stat === '') {
|
|
return null;
|
|
}
|
|
|
|
// Field 2 (comm) is parenthesised and may itself contain spaces and
|
|
// brackets — "(ffmpeg (x))" is a legal name — so everything before the
|
|
// LAST ')' is skipped and the split starts at field 3 (state).
|
|
$rClose = strrpos($stat, ')');
|
|
if ($rClose === false) {
|
|
return null;
|
|
}
|
|
$rFields = preg_split('/\s+/', trim(substr($stat, $rClose + 1)));
|
|
if (!is_array($rFields) || count($rFields) < 22) {
|
|
return null;
|
|
}
|
|
|
|
// $rFields[$i] is /proc/PID/stat field $i + 3: utime 14, stime 15,
|
|
// starttime 22 (ticks after boot), rss 24.
|
|
return [
|
|
'ticks' => (int) $rFields[11] + (int) $rFields[12],
|
|
'rss' => (int) $rFields[21] * self::pageSize(),
|
|
'at' => microtime(true),
|
|
'start' => (int) $rFields[19],
|
|
];
|
|
}
|
|
|
|
/**
|
|
* CPU use averaged over the process's whole life, in percent of one core —
|
|
* what `ps` reports. The fallback when there is no earlier sample to take a
|
|
* difference against (the first reading of a producer), so a stream shows a
|
|
* figure at once rather than a dash for a whole pass.
|
|
*
|
|
* The age comes from the process's own start time in /proc/PID/stat against
|
|
* /proc/uptime, not from the mtime of /proc/PID: that inode is created when
|
|
* something first looks at the directory, which for a long-running process
|
|
* can be much later than its start.
|
|
*
|
|
* @param array $sample A resourceSample() reading.
|
|
* @return float|null Null when the age cannot be read.
|
|
*/
|
|
public static function cpuPercentSinceStart(array $sample) {
|
|
if (!isset($sample['ticks'], $sample['start'])) {
|
|
return null;
|
|
}
|
|
$rUptime = (float) strtok((string) @file_get_contents('/proc/uptime'), ' ');
|
|
$rAge = $rUptime - ((int) $sample['start'] / 100);
|
|
if ($rUptime <= 0 || $rAge < 1) {
|
|
return null; // a process younger than a second says nothing yet
|
|
}
|
|
|
|
return round(((int) $sample['ticks'] / 100) / $rAge * 100, 1);
|
|
}
|
|
|
|
/**
|
|
* CPU use between two resourceSample() readings, in percent of one core.
|
|
*
|
|
* @param array $now The newer sample.
|
|
* @param array $prev The older one (from the previous pass).
|
|
* @return float|null Null when the pair says nothing: no previous reading, a
|
|
* restarted process (its counter went backwards), or two
|
|
* readings from the same instant.
|
|
*/
|
|
public static function cpuPercent(array $now, array $prev) {
|
|
if (!isset($now['ticks'], $now['at'], $prev['ticks'], $prev['at'])) {
|
|
return null;
|
|
}
|
|
|
|
$rSeconds = (float) $now['at'] - (float) $prev['at'];
|
|
$rTicks = (int) $now['ticks'] - (int) $prev['ticks'];
|
|
if ($rSeconds <= 0 || $rTicks < 0) {
|
|
return null;
|
|
}
|
|
|
|
// /proc reports CPU time in USER_HZ, which is 100 on Linux whatever the
|
|
// kernel's own tick rate — it is part of the /proc ABI, not CONFIG_HZ.
|
|
return round(($rTicks / 100) / $rSeconds * 100, 1);
|
|
}
|
|
|
|
/**
|
|
* The kernel page size, in bytes — /proc/PID/stat counts RSS in pages.
|
|
*
|
|
* Derived from this process's own two views of its memory (statm in pages,
|
|
* status in kB) rather than assumed, because 64K pages are normal on arm64.
|
|
*
|
|
* @return int
|
|
*/
|
|
protected static function pageSize() {
|
|
static $rSize = null;
|
|
if ($rSize !== null) {
|
|
return $rSize;
|
|
}
|
|
|
|
$rSize = 4096;
|
|
$rStatm = @file_get_contents('/proc/self/statm');
|
|
$rStatus = @file_get_contents('/proc/self/status');
|
|
if (is_string($rStatm) && is_string($rStatus) && preg_match('/^VmRSS:\s+(\d+) kB/m', $rStatus, $rMatch)) {
|
|
$rPages = (int) (preg_split('/\s+/', trim($rStatm))[1] ?? 0);
|
|
if ($rPages > 0) {
|
|
// The two files are read a moment apart, so snap the ratio to a
|
|
// real page size instead of trusting it to the byte.
|
|
$rRatio = ((int) $rMatch[1] * 1024) / $rPages;
|
|
foreach ([4096, 8192, 16384, 65536] as $rCandidate) {
|
|
if ($rRatio >= $rCandidate * 0.75 && $rRatio <= $rCandidate * 1.25) {
|
|
$rSize = $rCandidate;
|
|
break;
|
|
}
|
|
}
|
|
}
|
|
}
|
|
|
|
return $rSize;
|
|
}
|
|
|
|
// ───────────────────────────────────────────────────────────
|
|
// Process Control
|
|
// ───────────────────────────────────────────────────────────
|
|
|
|
/**
|
|
* Kill a process by PID
|
|
*
|
|
* @param int|null $pid Process ID (null/0 -> not running)
|
|
* @param int $signal Signal to send (default: SIGKILL = 9)
|
|
* @return bool
|
|
*/
|
|
public static function kill(?int $pid, int $signal = 9) {
|
|
$pid = (int) $pid;
|
|
|
|
if ($pid <= 0) {
|
|
return false;
|
|
}
|
|
|
|
if (!self::procExists($pid)) {
|
|
return false;
|
|
}
|
|
|
|
return posix_kill($pid, $signal);
|
|
}
|
|
|
|
/**
|
|
* Get the age of a process in seconds (how long it has been running).
|
|
*
|
|
* On Linux the mtime of the /proc/PID directory is fixed to the
|
|
* process start time, so `time() - filemtime()` yields the same figure
|
|
* as `ps -o etimes` without shelling out to ps — consistent with the
|
|
* rest of this class reading /proc directly.
|
|
*
|
|
* Used to detect daemons that are still present but wedged (e.g. a
|
|
* watchdog blocked in poll() on a half-open MariaDB socket): a normally
|
|
* short-lived generation that has been alive far too long is stale.
|
|
*
|
|
* @param int|null $pid Process ID (null/0 -> not running)
|
|
* @return int Age in seconds, or -1 if it cannot be determined
|
|
*/
|
|
public static function getProcessAge(?int $pid) {
|
|
$pid = (int) $pid;
|
|
|
|
if ($pid <= 0 || !self::procExists($pid)) {
|
|
return -1;
|
|
}
|
|
|
|
clearstatcache(true, '/proc/' . $pid);
|
|
$rStart = @filemtime('/proc/' . $pid);
|
|
|
|
if ($rStart === false) {
|
|
return -1;
|
|
}
|
|
|
|
$rAge = time() - $rStart;
|
|
|
|
return $rAge > 0 ? $rAge : 0;
|
|
}
|
|
|
|
// ───────────────────────────────────────────────────────────
|
|
// Cron Lock Management
|
|
// ───────────────────────────────────────────────────────────
|
|
|
|
/**
|
|
* Acquire a cron lock (PID file)
|
|
*
|
|
* If a lock file exists with a running process, exits with 'Running...'.
|
|
* If the process is stale (older than $timeout), kills it and takes over.
|
|
* Creates a new lock file with the current PID.
|
|
*
|
|
* This replaces CoreUtilities::checkCron().
|
|
*
|
|
* @param string $lockFile Path to PID lock file
|
|
* @param int $timeout Maximum age in seconds before considering stale (default: 1800 = 30min)
|
|
* @return bool Always returns true (exits on conflict)
|
|
*/
|
|
public static function acquireCronLock(string $lockFile, int $timeout = 1800) {
|
|
if (file_exists($lockFile)) {
|
|
// Read content + mtime up front. A competing cron can remove the lock
|
|
// file between the exists() check and these reads (TOCTOU), which would
|
|
// otherwise emit "failed to open stream" / "stat failed" warnings. If it
|
|
// vanished, fall through and take the lock ourselves.
|
|
$contents = @file_get_contents($lockFile);
|
|
$mtime = @filemtime($lockFile);
|
|
|
|
if ($contents !== false && $mtime !== false) {
|
|
$pid = (int) trim($contents);
|
|
|
|
if (self::procExists($pid)) {
|
|
// Process is running — check if it's stale
|
|
if (time() - $mtime >= $timeout) {
|
|
// Stale — kill and take over
|
|
if ($pid > 0) {
|
|
posix_kill($pid, 9);
|
|
}
|
|
} else {
|
|
// Still fresh — another instance is running
|
|
exit('Running...');
|
|
}
|
|
}
|
|
}
|
|
}
|
|
|
|
// Write our PID
|
|
$lockDir = dirname($lockFile);
|
|
if (!is_dir($lockDir)) {
|
|
@mkdir($lockDir, 0775, true);
|
|
}
|
|
|
|
// When running as root, chown created dirs to xc_vm so other
|
|
// crons (cache, streams, etc.) can write into tmp/ subtree.
|
|
if (posix_geteuid() === 0 && function_exists('posix_getpwnam') && defined('MAIN_HOME')) {
|
|
$rUser = posix_getpwnam('xc_vm');
|
|
if ($rUser) {
|
|
$rMainHome = rtrim(MAIN_HOME, '/');
|
|
$rDir = $lockDir;
|
|
while ($rDir && $rDir !== $rMainHome && strlen($rDir) > strlen($rMainHome)) {
|
|
if (is_dir($rDir) && fileowner($rDir) === 0) {
|
|
@chown($rDir, $rUser['uid']);
|
|
@chgrp($rDir, $rUser['gid']);
|
|
}
|
|
$rDir = dirname($rDir);
|
|
}
|
|
}
|
|
}
|
|
|
|
file_put_contents($lockFile, getmypid());
|
|
|
|
return true;
|
|
}
|
|
|
|
// ───────────────────────────────────────────────────────────
|
|
// Internal Helpers
|
|
// ───────────────────────────────────────────────────────────
|
|
|
|
/**
|
|
* Check if /proc/PID exists with caching
|
|
*
|
|
* @return bool
|
|
*/
|
|
protected static function procExists(int $pid) {
|
|
$now = microtime(true);
|
|
$key = (int) $pid;
|
|
|
|
if (isset(self::$procCache[$key]) && ($now - self::$procCache[$key]['time']) < self::$cacheTtl) {
|
|
return self::$procCache[$key]['exists'];
|
|
}
|
|
|
|
$exists = file_exists('/proc/' . $pid);
|
|
self::$procCache[$key] = ['exists' => $exists, 'time' => $now];
|
|
|
|
return $exists;
|
|
}
|
|
|
|
/**
|
|
* Clear the proc cache
|
|
*
|
|
* Useful before critical checks where stale cache could be dangerous.
|
|
*/
|
|
public static function clearCache() {
|
|
self::$procCache = [];
|
|
}
|
|
|
|
// ───────────────────────────────────────────────────────────
|
|
// Streaming-specific Process Methods
|
|
// Extracted from StreamingUtilities
|
|
// ───────────────────────────────────────────────────────────
|
|
|
|
/**
|
|
* Check if a stream process is alive (simplified cmdline search).
|
|
*
|
|
* Extracted from ProcessManager::isStreamAlive().
|
|
* Searches for $streamID anywhere in /proc/PID/cmdline (case-insensitive).
|
|
*
|
|
* @param int|null $pid Process ID (null/0 -> not running)
|
|
* @param int|string $streamID Stream identifier to search for
|
|
* @return bool
|
|
*/
|
|
public static function isStreamAlive(?int $pid, int|string $streamID) {
|
|
$pid = (int) $pid;
|
|
if ($pid <= 1) {
|
|
return false;
|
|
}
|
|
|
|
if (!self::procExists($pid)) {
|
|
return false;
|
|
}
|
|
|
|
if (!is_link('/proc/' . $pid . '/exe')) {
|
|
return false;
|
|
}
|
|
|
|
static $cache = [];
|
|
$cacheKey = $pid . '|' . $streamID;
|
|
if (isset($cache[$cacheKey]) && $cache[$cacheKey]['time'] > time() - 4) {
|
|
return $cache[$cacheKey]['alive'];
|
|
}
|
|
|
|
$cmd = @file_get_contents('/proc/' . $pid . '/cmdline');
|
|
if ($cmd === false) {
|
|
$alive = false;
|
|
} else {
|
|
$cmd = str_replace("\0", ' ', $cmd);
|
|
$alive = stripos($cmd, $streamID) !== false;
|
|
}
|
|
|
|
$cache[$cacheKey] = ['alive' => $alive, 'time' => time()];
|
|
return $alive;
|
|
}
|
|
|
|
/**
|
|
* Check if a monitor/proxy process is running.
|
|
*
|
|
* Extracted from ProcessManager::isMonitorAlive().
|
|
* Checks for XC_VM[streamID] OR XC_VMProxy[streamID] in cmdline.
|
|
*
|
|
* @param int|null $pid Process ID (null/0 -> not running)
|
|
* @param int|string $streamID Stream identifier
|
|
* @param string|null $exe Expected executable (default: PHP_BIN)
|
|
* @return bool
|
|
*/
|
|
public static function isMonitorAlive(?int $pid, int|string $streamID, ?string $exe = null) {
|
|
$pid = (int) $pid;
|
|
if ($pid <= 0) {
|
|
return false;
|
|
}
|
|
|
|
if ($exe === null && defined('PHP_BIN')) {
|
|
$exe = PHP_BIN;
|
|
}
|
|
|
|
if (!self::procExists($pid)) {
|
|
return false;
|
|
}
|
|
|
|
if (!$exe || !is_readable('/proc/' . $pid . '/exe')) {
|
|
return false;
|
|
}
|
|
|
|
if (strpos(basename(@readlink('/proc/' . $pid . '/exe')), basename($exe)) !== 0) {
|
|
return false;
|
|
}
|
|
|
|
$cmdline = trim(@file_get_contents('/proc/' . $pid . '/cmdline'));
|
|
return ($cmdline == 'XC_VM[' . $streamID . ']');
|
|
}
|
|
|
|
/**
|
|
* Start a stream monitor process in background.
|
|
*
|
|
* Always the PHP watchdog. Stream code calls StreamProcess::startMonitor()
|
|
* instead, which hands the stream to the fanout daemon's supervisor when this
|
|
* server supervises and only falls back to this.
|
|
*
|
|
* @return bool
|
|
*/
|
|
public static function startMonitor(int $streamID, int $restart = 0) {
|
|
shell_exec(PHP_BIN . ' ' . MAIN_HOME . 'console.php monitor ' . intval($streamID) . ' ' . intval($restart) . ' >/dev/null 2>/dev/null &');
|
|
return true;
|
|
}
|
|
|
|
// ───────────────────────────────────────────────────────────
|
|
// Utility
|
|
// ───────────────────────────────────────────────────────────
|
|
|
|
/**
|
|
* Check if an nginx master process is running.
|
|
*
|
|
* Replaces CoreUtilities::isRunning().
|
|
*
|
|
* The master is owned by root on typical installs (workers run as
|
|
* xc_vm), so the scan must not be restricted to the xc_vm user —
|
|
* a false negative here makes cron:servers/cron:streams bail out
|
|
* every run and no daemon ever gets revived.
|
|
*
|
|
* @return bool
|
|
*/
|
|
public static function isNginxRunning() {
|
|
foreach (glob('/proc/*/cmdline') ?: [] as $rCmdFile) {
|
|
$rRaw = @file_get_contents($rCmdFile);
|
|
if ($rRaw && strpos(str_replace("\0", ' ', $rRaw), 'nginx: master') !== false) {
|
|
return true;
|
|
}
|
|
}
|
|
return false;
|
|
}
|
|
|
|
/**
|
|
* Find PIDs of processes whose command line contains one of the given
|
|
* substrings. Reads /proc directly instead of ps|grep pipelines, which
|
|
* match unrelated processes (e.g. ffmpeg's -thread_queue_size satisfied
|
|
* the "queue" daemon check, so the encode queue was never revived).
|
|
*
|
|
* @param array $rTerms Cmdline substrings to match (exact, case-sensitive)
|
|
* @param int $rLimit Stop after this many matches (0 = no limit)
|
|
* @return array<int> Matching PIDs (own PID excluded)
|
|
*/
|
|
public static function findProcessPIDs(array $rTerms, int $rLimit = 0) {
|
|
$rPIDs = [];
|
|
$rSelf = getmypid();
|
|
foreach (glob('/proc/[0-9]*/cmdline') ?: [] as $rCmdFile) {
|
|
$rPID = intval(basename(dirname($rCmdFile)));
|
|
if ($rPID == $rSelf) {
|
|
continue;
|
|
}
|
|
$rRaw = @file_get_contents($rCmdFile);
|
|
if (!$rRaw) {
|
|
continue;
|
|
}
|
|
$rCmd = str_replace("\0", ' ', $rRaw);
|
|
foreach ($rTerms as $rTerm) {
|
|
if (strpos($rCmd, $rTerm) !== false) {
|
|
$rPIDs[] = $rPID;
|
|
if ($rLimit > 0 && count($rPIDs) >= $rLimit) {
|
|
return $rPIDs;
|
|
}
|
|
break;
|
|
}
|
|
}
|
|
}
|
|
return $rPIDs;
|
|
}
|
|
|
|
/**
|
|
* Check whether any process (any user) matches one of the given
|
|
* cmdline substrings.
|
|
*
|
|
* @param array $rTerms Cmdline substrings to match
|
|
* @return bool
|
|
*/
|
|
public static function isAnyProcessRunning(array $rTerms) {
|
|
return count(self::findProcessPIDs($rTerms, 1)) > 0;
|
|
}
|
|
}
|