Code Coverage
 
Lines
Functions and Methods
Classes and Traits
Total
92.31% covered (success)
92.31%
24 / 26
75.00% covered (warning)
75.00%
3 / 4
CRAP
0.00% covered (danger)
0.00%
0 / 1
LocalWorkerProcess
92.31% covered (success)
92.31%
24 / 26
75.00% covered (warning)
75.00%
3 / 4
20.18
0.00% covered (danger)
0.00%
0 / 1
 __construct
100.00% covered (success)
100.00%
1 / 1
100.00% covered (success)
100.00%
1 / 1
1
 isSupported
100.00% covered (success)
100.00%
4 / 4
100.00% covered (success)
100.00%
1 / 1
5
 startTime
80.00% covered (warning)
80.00%
8 / 10
0.00% covered (danger)
0.00%
0 / 1
4.13
 isRunning
100.00% covered (success)
100.00%
11 / 11
100.00% covered (success)
100.00%
1 / 1
10
1<?php
2
3declare(strict_types=1);
4/**
5 * SPDX-FileCopyrightText: 2026 LibreCode coop and contributors
6 * SPDX-License-Identifier: AGPL-3.0-or-later
7 */
8
9namespace OCA\Libresign\Service\Worker;
10
11/** Linux process identity is local to a PID namespace, never a cluster lock. */
12class 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}