Code Coverage |
||||||||||
Lines |
Functions and Methods |
Classes and Traits |
||||||||
| Total | |
92.31% |
24 / 26 |
|
75.00% |
3 / 4 |
CRAP | |
0.00% |
0 / 1 |
| LocalWorkerProcess | |
92.31% |
24 / 26 |
|
75.00% |
3 / 4 |
20.18 | |
0.00% |
0 / 1 |
| __construct | |
100.00% |
1 / 1 |
|
100.00% |
1 / 1 |
1 | |||
| isSupported | |
100.00% |
4 / 4 |
|
100.00% |
1 / 1 |
5 | |||
| startTime | |
80.00% |
8 / 10 |
|
0.00% |
0 / 1 |
4.13 | |||
| isRunning | |
100.00% |
11 / 11 |
|
100.00% |
1 / 1 |
10 | |||
| 1 | <?php |
| 2 | |
| 3 | declare(strict_types=1); |
| 4 | /** |
| 5 | * SPDX-FileCopyrightText: 2026 LibreCode coop and contributors |
| 6 | * SPDX-License-Identifier: AGPL-3.0-or-later |
| 7 | */ |
| 8 | |
| 9 | namespace OCA\Libresign\Service\Worker; |
| 10 | |
| 11 | /** Linux process identity is local to a PID namespace, never a cluster lock. */ |
| 12 | class LocalWorkerProcess { |
| 13 | public function __construct( |
| 14 | private string $procRoot = '/proc', |
| 15 | private ?string $expectedOccPath = null, |
| 16 | ) { |
| 17 | } |
| 18 | |
| 19 | public function isSupported(): bool { |
| 20 | // Avoid spawning workers we cannot identify in later requests when |
| 21 | // procfs is restricted (for example through PHP open_basedir). |
| 22 | return PHP_OS_FAMILY === 'Linux' && function_exists('flock') |
| 23 | && is_dir($this->procRoot) |
| 24 | && @is_readable($this->procRoot . '/self/stat') |
| 25 | && @is_readable($this->procRoot . '/self/cmdline'); |
| 26 | } |
| 27 | |
| 28 | public function startTime(int $pid): ?string { |
| 29 | if ($pid <= 0) { |
| 30 | return null; |
| 31 | } |
| 32 | $stat = @file_get_contents($this->procRoot . '/' . $pid . '/stat'); |
| 33 | if (!is_string($stat)) { |
| 34 | return null; |
| 35 | } |
| 36 | $close = strrpos($stat, ')'); |
| 37 | if ($close === false) { |
| 38 | return null; |
| 39 | } |
| 40 | $fields = preg_split('/\s+/', trim(substr($stat, $close + 1))); |
| 41 | // Linux proc stat field 22 is index 19 after state (field 3). |
| 42 | return $fields[19] ?? null; |
| 43 | } |
| 44 | |
| 45 | /** @param list<class-string> $jobClasses */ |
| 46 | public function isRunning(int $pid, string $startTime, array $jobClasses = []): bool { |
| 47 | if ($startTime === '' || $this->startTime($pid) !== $startTime |
| 48 | || (function_exists('posix_geteuid') && @fileowner($this->procRoot . '/' . $pid) !== posix_geteuid())) { |
| 49 | return false; |
| 50 | } |
| 51 | $cmd = @file_get_contents($this->procRoot . '/' . $pid . '/cmdline'); |
| 52 | if (!is_string($cmd) || $cmd === '') { |
| 53 | return false; |
| 54 | } |
| 55 | $argv = explode("\0", trim($cmd, "\0")); |
| 56 | // A PID is not a worker identity: ensure it is the OCC command for this |
| 57 | // Nextcloud instance, not another process named occ on the same host. |
| 58 | $expectedOcc = $this->expectedOccPath ?? \OC::$SERVERROOT . '/occ'; |
| 59 | // WorkerStarter always passes [php, this Nextcloud's occ, command]. |
| 60 | // Arbitrary process arguments must not look like a running worker. |
| 61 | return ($argv[1] ?? null) === $expectedOcc |
| 62 | && ($argv[2] ?? null) === 'background-job:worker' |
| 63 | && ($jobClasses === [] || array_slice($argv, 3) === [...$jobClasses, '--stop_after=30m', '--interval=0']); |
| 64 | } |
| 65 | } |