|
2 | 2 |
|
3 | 3 | namespace PHPStan\Parallel; |
4 | 4 |
|
5 | | -use PHPStan\ShouldNotHappenException; |
6 | | -use React\EventLoop\LoopInterface; |
7 | | -use React\EventLoop\TimerInterface; |
8 | 5 | use React\Stream\ReadableStreamInterface; |
9 | 6 | use React\Stream\WritableStreamInterface; |
10 | 7 | use Throwable; |
11 | | -use function fclose; |
12 | | -use function rewind; |
13 | | -use function sprintf; |
14 | | -use function stream_get_contents; |
15 | | -use function tmpfile; |
16 | 8 |
|
17 | | -final class Process |
| 9 | +/** |
| 10 | + * A parallel analysis worker as seen by ParallelAnalyser / ProcessPool. |
| 11 | + * |
| 12 | + * Implementations differ only in how the worker process comes to life: |
| 13 | + * SpawnedProcess spawns a fresh PHP process via react/child-process, |
| 14 | + * ForkedProcess forks the already-booted main process via pcntl_fork(). Both |
| 15 | + * then speak the same TCP + NDJSON protocol, so request()/quit()/ |
| 16 | + * bindConnection() behave identically and live in ProcessBase. |
| 17 | + */ |
| 18 | +interface Process |
18 | 19 | { |
19 | 20 |
|
20 | | - private \React\ChildProcess\Process $process; |
21 | | - |
22 | | - private ?WritableStreamInterface $in = null; |
23 | | - |
24 | | - /** @var resource */ |
25 | | - private $stdOut; |
26 | | - |
27 | | - /** @var resource */ |
28 | | - private $stdErr; |
29 | | - |
30 | | - /** @var callable(mixed[] $json) : void */ |
31 | | - private $onData; |
32 | | - |
33 | | - /** @var callable(Throwable $exception): void */ |
34 | | - private $onError; |
35 | | - |
36 | | - private ?TimerInterface $timer = null; |
37 | | - |
38 | | - public function __construct( |
39 | | - private string $command, |
40 | | - private LoopInterface $loop, |
41 | | - private float $timeoutSeconds, |
42 | | - ) |
43 | | - { |
44 | | - } |
45 | | - |
46 | 21 | /** |
47 | 22 | * @param callable(mixed[] $json) : void $onData |
48 | 23 | * @param callable(Throwable $exception): void $onError |
49 | 24 | * @param callable(?int $exitCode, string $output) : void $onExit |
50 | 25 | */ |
51 | | - public function start(callable $onData, callable $onError, callable $onExit): void |
52 | | - { |
53 | | - $tmpStdOut = tmpfile(); |
54 | | - if ($tmpStdOut === false) { |
55 | | - throw new ShouldNotHappenException('Failed creating temp file for stdout.'); |
56 | | - } |
57 | | - $tmpStdErr = tmpfile(); |
58 | | - if ($tmpStdErr === false) { |
59 | | - throw new ShouldNotHappenException('Failed creating temp file for stderr.'); |
60 | | - } |
61 | | - $this->stdOut = $tmpStdOut; |
62 | | - $this->stdErr = $tmpStdErr; |
63 | | - $this->process = new \React\ChildProcess\Process($this->command, fds: [ |
64 | | - 1 => $this->stdOut, |
65 | | - 2 => $this->stdErr, |
66 | | - ]); |
67 | | - $this->process->start($this->loop); |
68 | | - $this->onData = $onData; |
69 | | - $this->onError = $onError; |
70 | | - $this->process->on('exit', function ($exitCode) use ($onExit): void { |
71 | | - $this->cancelTimer(); |
72 | | - |
73 | | - $output = ''; |
74 | | - rewind($this->stdOut); |
75 | | - $output .= stream_get_contents($this->stdOut); |
76 | | - |
77 | | - rewind($this->stdErr); |
78 | | - $output .= stream_get_contents($this->stdErr); |
79 | | - |
80 | | - $onExit($exitCode, $output); |
81 | | - fclose($this->stdOut); |
82 | | - fclose($this->stdErr); |
83 | | - }); |
84 | | - } |
85 | | - |
86 | | - private function cancelTimer(): void |
87 | | - { |
88 | | - if ($this->timer === null) { |
89 | | - return; |
90 | | - } |
91 | | - |
92 | | - $this->loop->cancelTimer($this->timer); |
93 | | - $this->timer = null; |
94 | | - } |
| 26 | + public function start(callable $onData, callable $onError, callable $onExit): void; |
95 | 27 |
|
96 | 28 | /** |
97 | 29 | * @param mixed[] $data |
98 | 30 | */ |
99 | | - public function request(array $data): void |
100 | | - { |
101 | | - $this->cancelTimer(); |
102 | | - if ($this->in === null) { |
103 | | - throw new ShouldNotHappenException(); |
104 | | - } |
105 | | - $this->in->write($data); |
106 | | - $this->timer = $this->loop->addTimer($this->timeoutSeconds, function (): void { |
107 | | - $onError = $this->onError; |
108 | | - $onError(new ProcessTimedOutException(sprintf('Child process timed out after %.1f seconds. Try making it longer with parallel.processTimeout setting.', $this->timeoutSeconds))); |
109 | | - }); |
110 | | - } |
111 | | - |
112 | | - public function quit(): void |
113 | | - { |
114 | | - $this->cancelTimer(); |
115 | | - if (!$this->process->isRunning()) { |
116 | | - return; |
117 | | - } |
118 | | - |
119 | | - foreach ($this->process->pipes as $pipe) { |
120 | | - $pipe->close(); |
121 | | - } |
122 | | - |
123 | | - if ($this->in === null) { |
124 | | - return; |
125 | | - } |
126 | | - |
127 | | - $this->in->end(); |
128 | | - } |
| 31 | + public function request(array $data): void; |
129 | 32 |
|
130 | | - public function bindConnection(ReadableStreamInterface $out, WritableStreamInterface $in): void |
131 | | - { |
132 | | - $out->on('data', function (array $json): void { |
133 | | - $this->cancelTimer(); |
134 | | - if ($json['action'] !== 'result') { |
135 | | - return; |
136 | | - } |
| 33 | + public function quit(): void; |
137 | 34 |
|
138 | | - $onData = $this->onData; |
139 | | - $onData($json['result']); |
140 | | - }); |
141 | | - $this->in = $in; |
142 | | - $out->on('error', function (Throwable $error): void { |
143 | | - $onError = $this->onError; |
144 | | - $onError($error); |
145 | | - }); |
146 | | - $in->on('error', function (Throwable $error): void { |
147 | | - $onError = $this->onError; |
148 | | - $onError($error); |
149 | | - }); |
150 | | - } |
| 35 | + public function bindConnection(ReadableStreamInterface $out, WritableStreamInterface $in): void; |
151 | 36 |
|
152 | 37 | } |
0 commit comments