Code Coverage
 
Lines
Functions and Methods
Classes and Traits
Total
92.59% covered (success)
92.59%
25 / 27
66.67% covered (warning)
66.67%
2 / 3
CRAP
0.00% covered (danger)
0.00%
0 / 1
WorkerStarter
92.59% covered (success)
92.59%
25 / 27
66.67% covered (warning)
66.67%
2 / 3
5.01
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
 startWorker
92.00% covered (success)
92.00%
23 / 25
0.00% covered (danger)
0.00%
0 / 1
3.00
 createProcess
100.00% covered (success)
100.00%
1 / 1
100.00% covered (success)
100.00%
1 / 1
1
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
11use OCA\Libresign\Vendor\Symfony\Component\Process\Process;
12use OCP\IBinaryFinder;
13
14/** Starts a detached worker. The caller MUST hold the local registry lock. */
15class WorkerStarter {
16    public function __construct(
17        private IBinaryFinder $binaryFinder,
18        private WorkerConfiguration $workerConfiguration,
19    ) {
20    }
21
22    public function startWorker(string $pool = WorkerConfiguration::SIGNING): int {
23        $php = $this->binaryFinder->findBinaryPath('php');
24        if ($php === false) {
25            throw new \RuntimeException('PHP CLI is unavailable');
26        }
27        $classes = match ($pool) {
28            WorkerConfiguration::SIGNING => $this->workerConfiguration->getSigningJobClasses(),
29            WorkerConfiguration::NOTIFICATION => $this->workerConfiguration->getNotificationJobClasses(),
30            default => throw new \InvalidArgumentException('Unknown local worker pool'),
31        };
32        $command = [
33            $php,
34            \OC::$SERVERROOT . '/occ',
35            'background-job:worker',
36            ...$classes,
37            '--stop_after=30m',
38            '--interval=0',
39        ];
40        $process = $this->createProcess($command);
41        // Request that Symfony detach the child from the HTTP request lifetime.
42        $process->setOptions(['create_new_console' => true]);
43        // The HTTP request cannot keep reading child stdout/stderr. Detached
44        // pipes would close when Symfony destroys the Process object, and a
45        // worker writing progress to stdout could then terminate on SIGPIPE.
46        $process->disableOutput();
47        $process->setTimeout(null);
48        $process->start();
49        $pid = (int)$process->getPid();
50        if ($pid <= 0) {
51            throw new \RuntimeException('Worker was started without a valid process identifier');
52        }
53        return $pid;
54    }
55
56    protected function createProcess(array $command): Process {
57        return new Process($command);
58    }
59}