forked from phpstan/phpstan-src
-
Notifications
You must be signed in to change notification settings - Fork 0
Expand file tree
/
Copy pathForkedProcess.php
More file actions
151 lines (130 loc) · 3.95 KB
/
Copy pathForkedProcess.php
File metadata and controls
151 lines (130 loc) · 3.95 KB
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
59
60
61
62
63
64
65
66
67
68
69
70
71
72
73
74
75
76
77
78
79
80
81
82
83
84
85
86
87
88
89
90
91
92
93
94
95
96
97
98
99
100
101
102
103
104
105
106
107
108
109
110
111
112
113
114
115
116
117
118
119
120
121
122
123
124
125
126
127
128
129
130
131
132
133
134
135
136
137
138
139
140
141
142
143
144
145
146
147
148
149
150
151
<?php declare(strict_types = 1);
namespace PHPStan\Parallel;
use PHPStan\ShouldNotHappenException;
use React\EventLoop\LoopInterface;
use React\EventLoop\TimerInterface;
use React\Socket\TcpServer;
use Symfony\Component\Console\Output\StreamOutput;
use Throwable;
use function fclose;
use function pcntl_fork;
use function pcntl_waitpid;
use function pcntl_wexitstatus;
use function pcntl_wifexited;
use function rewind;
use function stream_get_contents;
use function tmpfile;
use const WNOHANG;
/**
* Parallel worker backed by pcntl_fork(): the worker is forked from the
* already-booted main process, so it inherits the DI container for free and
* skips the application re-boot that a {@see SpawnedProcess} pays.
*
* The forked child still talks to ParallelAnalyser over the same TCP + NDJSON
* protocol — only the process-creation mechanism differs.
*/
final class ForkedProcess extends ProcessBase
{
private const WAITPID_POLL_INTERVAL = 0.01;
/** @var resource|null */
private $stdOut = null;
private ?TimerInterface $waitTimer = null;
/**
* @param string[] $analysedFiles
*/
public function __construct(
LoopInterface $loop,
float $timeoutSeconds,
private WorkerRunner $workerRunner,
private TcpServer $server,
private int $serverPort,
private string $identifier,
private array $analysedFiles,
private ?string $tmpFile,
private ?string $insteadOfFile,
)
{
parent::__construct($loop, $timeoutSeconds);
}
/**
* @param callable(mixed[] $json) : void $onData
* @param callable(Throwable $exception): void $onError
* @param callable(?int $exitCode, string $output) : void $onExit
*/
public function start(callable $onData, callable $onError, callable $onExit): void
{
$this->setCallbacks($onData, $onError);
// Created before the fork so the parent can read what the child wrote.
$tmpStdOut = tmpfile();
if ($tmpStdOut === false) {
throw new ShouldNotHappenException('Failed creating temp file for stdout.');
}
$this->stdOut = $tmpStdOut;
$pid = pcntl_fork();
if ($pid === -1) {
fclose($this->stdOut);
$this->stdOut = null;
// Deferred so it runs after ParallelAnalyser has attached this
// process to the pool — otherwise tryQuitProcess() would no-op.
$this->loop->futureTick(static function () use ($onExit): void {
$onExit(null, 'pcntl_fork() failed.');
});
return;
}
if ($pid === 0) {
// Child: drop the inherited listening socket immediately, then run
// the worker on its own fresh event loop and never return.
$this->server->close();
$output = new StreamOutput($this->stdOut);
$exitCode = $this->workerRunner->run(
$output,
$this->analysedFiles,
$this->serverPort,
$this->identifier,
$this->tmpFile,
$this->insteadOfFile,
);
exit($exitCode);
}
// Parent: poll for the child to exit and report it through $onExit.
$this->waitTimer = $this->loop->addPeriodicTimer(self::WAITPID_POLL_INTERVAL, function () use ($pid, $onExit): void {
$status = 0;
$result = pcntl_waitpid($pid, $status, WNOHANG);
if ($result === 0) {
return;
}
$this->cancelWaitTimer();
$this->cancelTimer();
$exitCode = null;
if ($result > 0 && pcntl_wifexited($status)) {
$exitStatus = pcntl_wexitstatus($status);
if ($exitStatus !== false) {
$exitCode = $exitStatus;
}
}
$output = '';
if ($this->stdOut !== null) {
rewind($this->stdOut);
$output = (string) stream_get_contents($this->stdOut);
fclose($this->stdOut);
$this->stdOut = null;
}
$onExit($exitCode, $output);
});
}
public function quit(): void
{
// Ending the connection makes the child's event loop drain and the
// child exit; the waitpid poll timer must keep running until then so
// the child is actually reaped (otherwise: zombie + hang).
$this->endConnection();
}
private function cancelWaitTimer(): void
{
if ($this->waitTimer === null) {
return;
}
$this->loop->cancelTimer($this->waitTimer);
$this->waitTimer = null;
}
}