|
2 | 2 |
|
3 | 3 | namespace PHPStan\Command; |
4 | 4 |
|
5 | | -use Clue\React\NDJson\Decoder; |
6 | | -use Clue\React\NDJson\Encoder; |
7 | 5 | use Override; |
8 | | -use PHPStan\Analyser\FileAnalyser; |
9 | | -use PHPStan\Analyser\InternalError; |
10 | | -use PHPStan\Analyser\NodeScopeResolver; |
11 | | -use PHPStan\Collectors\Registry as CollectorRegistry; |
12 | | -use PHPStan\DependencyInjection\Container; |
13 | 6 | use PHPStan\File\PathNotFoundException; |
14 | | -use PHPStan\Rules\Registry as RuleRegistry; |
| 7 | +use PHPStan\Parallel\WorkerRunner; |
15 | 8 | use PHPStan\ShouldNotHappenException; |
16 | | -use React\EventLoop\StreamSelectLoop; |
17 | | -use React\Socket\ConnectionInterface; |
18 | | -use React\Socket\TcpConnector; |
19 | | -use React\Stream\ReadableStreamInterface; |
20 | | -use React\Stream\WritableStreamInterface; |
21 | 9 | use Symfony\Component\Console\Command\Command; |
22 | 10 | use Symfony\Component\Console\Input\InputArgument; |
23 | 11 | use Symfony\Component\Console\Input\InputInterface; |
24 | 12 | use Symfony\Component\Console\Input\InputOption; |
25 | 13 | use Symfony\Component\Console\Output\OutputInterface; |
26 | | -use Throwable; |
27 | | -use function array_fill_keys; |
28 | | -use function array_filter; |
29 | | -use function array_merge; |
30 | | -use function array_unshift; |
31 | | -use function array_values; |
32 | | -use function defined; |
33 | 14 | use function is_array; |
34 | 15 | use function is_bool; |
35 | 16 | use function is_string; |
36 | | -use function memory_get_peak_usage; |
37 | 17 | use function sprintf; |
38 | 18 |
|
39 | 19 | final class WorkerCommand extends Command |
40 | 20 | { |
41 | 21 |
|
42 | 22 | private const NAME = 'worker'; |
43 | 23 |
|
44 | | - private int $errorCount = 0; |
45 | | - |
46 | 24 | /** |
47 | 25 | * @param string[] $composerAutoloaderProjectPaths |
48 | 26 | */ |
@@ -122,196 +100,30 @@ protected function execute(InputInterface $input, OutputInterface $output): int |
122 | 100 | } catch (InceptionNotSuccessfulException $e) { |
123 | 101 | return 1; |
124 | 102 | } |
125 | | - $loop = new StreamSelectLoop(); |
126 | 103 |
|
127 | 104 | $container = $inceptionResult->getContainer(); |
128 | 105 |
|
129 | 106 | try { |
130 | 107 | [$analysedFiles] = $inceptionResult->getFiles(); |
131 | | - $analysedFiles = $this->switchTmpFile($analysedFiles, $insteadOfFile, $tmpFile); |
132 | 108 | } catch (PathNotFoundException $e) { |
133 | 109 | $inceptionResult->getErrorOutput()->writeLineFormatted(sprintf('<error>%s</error>', $e->getMessage())); |
134 | 110 | return 1; |
135 | 111 | } catch (InceptionNotSuccessfulException) { |
136 | 112 | return 1; |
137 | 113 | } |
138 | 114 |
|
139 | | - $nodeScopeResolver = $container->getByType(NodeScopeResolver::class); |
140 | | - $nodeScopeResolver->setAnalysedFiles($analysedFiles); |
141 | | - |
142 | | - $analysedFiles = array_fill_keys($analysedFiles, true); |
143 | | - |
144 | | - $tcpConnector = new TcpConnector($loop); |
145 | | - $tcpConnector->connect(sprintf('127.0.0.1:%d', (int) $port))->then(function (ConnectionInterface $connection) use ($container, $identifier, $output, $analysedFiles, $tmpFile, $insteadOfFile): void { |
146 | | - // phpcs:disable SlevomatCodingStandard.Namespaces.ReferenceUsedNamesOnly |
147 | | - $jsonInvalidUtf8Ignore = defined('JSON_INVALID_UTF8_IGNORE') ? JSON_INVALID_UTF8_IGNORE : 0; |
148 | | - // phpcs:enable |
149 | | - $out = new Encoder($connection, $jsonInvalidUtf8Ignore); |
150 | | - $in = new Decoder($connection, true, options: $jsonInvalidUtf8Ignore, maxlength: $container->getParameter('parallel')['buffer']); |
151 | | - $out->write(['action' => 'hello', 'identifier' => $identifier]); |
152 | | - $this->runWorker($container, $out, $in, $output, $analysedFiles, $tmpFile, $insteadOfFile); |
153 | | - }); |
154 | | - |
155 | | - $loop->run(); |
156 | | - |
157 | | - if ($this->errorCount > 0) { |
158 | | - return 1; |
159 | | - } |
160 | | - |
161 | | - return 0; |
162 | | - } |
163 | | - |
164 | | - /** |
165 | | - * @param array<string, true> $analysedFiles |
166 | | - */ |
167 | | - private function runWorker( |
168 | | - Container $container, |
169 | | - WritableStreamInterface $out, |
170 | | - ReadableStreamInterface $in, |
171 | | - OutputInterface $output, |
172 | | - array $analysedFiles, |
173 | | - ?string $tmpFile, |
174 | | - ?string $insteadOfFile, |
175 | | - ): void |
176 | | - { |
177 | | - $handleError = function (Throwable $error) use ($out, $output): void { |
178 | | - $this->errorCount++; |
179 | | - $output->writeln(sprintf('Error: %s', $error->getMessage())); |
180 | | - $out->write([ |
181 | | - 'action' => 'result', |
182 | | - 'result' => [ |
183 | | - 'errors' => [], |
184 | | - 'internalErrors' => [ |
185 | | - new InternalError( |
186 | | - $error->getMessage(), |
187 | | - 'communicating with main process in parallel worker', |
188 | | - InternalError::prepareTrace($error), |
189 | | - $error->getTraceAsString(), |
190 | | - shouldReportBug: true, |
191 | | - ), |
192 | | - ], |
193 | | - 'filteredPhpErrors' => [], |
194 | | - 'allPhpErrors' => [], |
195 | | - 'locallyIgnoredErrors' => [], |
196 | | - 'linesToIgnore' => [], |
197 | | - 'unmatchedLineIgnores' => [], |
198 | | - 'collectedData' => [], |
199 | | - 'memoryUsage' => memory_get_peak_usage(true), |
200 | | - 'dependencies' => [], |
201 | | - 'exportedNodes' => [], |
202 | | - 'files' => [], |
203 | | - 'internalErrorsCount' => 1, |
204 | | - ], |
205 | | - ]); |
206 | | - $out->end(); |
207 | | - }; |
208 | | - $out->on('error', $handleError); |
209 | | - $fileAnalyser = $container->getByType(FileAnalyser::class); |
210 | | - $ruleRegistry = $container->getByType(RuleRegistry::class); |
211 | | - $collectorRegistry = $container->getByType(CollectorRegistry::class); |
212 | | - $in->on('data', static function (array $json) use ($fileAnalyser, $ruleRegistry, $collectorRegistry, $out, $analysedFiles, $tmpFile, $insteadOfFile): void { |
213 | | - $action = $json['action']; |
214 | | - if ($action !== 'analyse') { |
215 | | - return; |
216 | | - } |
217 | | - |
218 | | - $internalErrorsCount = 0; |
219 | | - $files = $json['files']; |
220 | | - $errors = []; |
221 | | - $internalErrors = []; |
222 | | - $filteredPhpErrors = []; |
223 | | - $allPhpErrors = []; |
224 | | - $locallyIgnoredErrors = []; |
225 | | - $linesToIgnore = []; |
226 | | - $unmatchedLineIgnores = []; |
227 | | - $collectedData = []; |
228 | | - $dependencies = []; |
229 | | - $usedTraitDependencies = []; |
230 | | - $exportedNodes = []; |
231 | | - $processedFiles = []; |
232 | | - foreach ($files as $file) { |
233 | | - try { |
234 | | - if ($file === $insteadOfFile) { |
235 | | - $file = $tmpFile; |
236 | | - } |
237 | | - $fileAnalyserResult = $fileAnalyser->analyseFile($file, $analysedFiles, $ruleRegistry, $collectorRegistry, null); |
238 | | - $fileErrors = $fileAnalyserResult->getErrors(); |
239 | | - $filteredPhpErrors = array_merge($filteredPhpErrors, $fileAnalyserResult->getFilteredPhpErrors()); |
240 | | - $allPhpErrors = array_merge($allPhpErrors, $fileAnalyserResult->getAllPhpErrors()); |
241 | | - $linesToIgnore[$file] = $fileAnalyserResult->getLinesToIgnore(); |
242 | | - $unmatchedLineIgnores[$file] = $fileAnalyserResult->getUnmatchedLineIgnores(); |
243 | | - $dependencies[$file] = $fileAnalyserResult->getDependencies(); |
244 | | - $usedTraitDependencies[$file] = $fileAnalyserResult->getUsedTraitDependencies(); |
245 | | - $exportedNodes[$file] = $fileAnalyserResult->getExportedNodes(); |
246 | | - $processedFiles = array_merge($processedFiles, $fileAnalyserResult->getProcessedFiles()); |
247 | | - foreach ($fileErrors as $fileError) { |
248 | | - $errors[] = $fileError; |
249 | | - } |
250 | | - foreach ($fileAnalyserResult->getLocallyIgnoredErrors() as $locallyIgnoredError) { |
251 | | - $locallyIgnoredErrors[] = $locallyIgnoredError; |
252 | | - } |
253 | | - foreach ($fileAnalyserResult->getCollectedData() as $collectedFile => $dataPerCollector) { |
254 | | - foreach ($dataPerCollector as $collectorType => $collectorData) { |
255 | | - foreach ($collectorData as $data) { |
256 | | - $collectedData[$collectedFile][$collectorType][] = $data; |
257 | | - } |
258 | | - } |
259 | | - } |
260 | | - } catch (Throwable $t) { |
261 | | - $internalErrorsCount++; |
262 | | - $internalErrors[] = new InternalError( |
263 | | - $t->getMessage(), |
264 | | - sprintf('analysing file %s', $file), |
265 | | - InternalError::prepareTrace($t), |
266 | | - $t->getTraceAsString(), |
267 | | - shouldReportBug: true, |
268 | | - ); |
269 | | - } |
270 | | - } |
271 | | - |
272 | | - $out->write([ |
273 | | - 'action' => 'result', |
274 | | - 'result' => [ |
275 | | - 'errors' => $errors, |
276 | | - 'internalErrors' => $internalErrors, |
277 | | - 'filteredPhpErrors' => $filteredPhpErrors, |
278 | | - 'allPhpErrors' => $allPhpErrors, |
279 | | - 'locallyIgnoredErrors' => $locallyIgnoredErrors, |
280 | | - 'linesToIgnore' => $linesToIgnore, |
281 | | - 'unmatchedLineIgnores' => $unmatchedLineIgnores, |
282 | | - 'collectedData' => $collectedData, |
283 | | - 'memoryUsage' => memory_get_peak_usage(true), |
284 | | - 'dependencies' => $dependencies, |
285 | | - 'usedTraitDependencies' => $usedTraitDependencies, |
286 | | - 'exportedNodes' => $exportedNodes, |
287 | | - 'files' => $files, |
288 | | - 'processedFiles' => $processedFiles, |
289 | | - 'internalErrorsCount' => $internalErrorsCount, |
290 | | - ]]); |
291 | | - }); |
292 | | - $in->on('error', $handleError); |
293 | | - } |
294 | | - |
295 | | - /** |
296 | | - * @param string[] $analysedFiles |
297 | | - * @return string[] |
298 | | - */ |
299 | | - private function switchTmpFile( |
300 | | - array $analysedFiles, |
301 | | - ?string $insteadOfFile, |
302 | | - ?string $tmpFile, |
303 | | - ): array |
304 | | - { |
305 | | - if ($insteadOfFile === null) { |
306 | | - return $analysedFiles; |
307 | | - } |
308 | | - $analysedFiles = array_values(array_filter($analysedFiles, static fn (string $file): bool => $file !== $insteadOfFile)); |
309 | | - |
310 | | - if ($tmpFile !== null) { |
311 | | - array_unshift($analysedFiles, $tmpFile); |
312 | | - } |
313 | | - |
314 | | - return $analysedFiles; |
| 115 | + // Everything after the boot lives in WorkerRunner so a pcntl_fork()-ed |
| 116 | + // child can reuse it without re-booting (see ParallelAnalyser). |
| 117 | + $workerRunner = $container->getByType(WorkerRunner::class); |
| 118 | + |
| 119 | + return $workerRunner->run( |
| 120 | + $output, |
| 121 | + $analysedFiles, |
| 122 | + (int) $port, |
| 123 | + $identifier, |
| 124 | + $tmpFile, |
| 125 | + $insteadOfFile, |
| 126 | + ); |
315 | 127 | } |
316 | 128 |
|
317 | 129 | } |
0 commit comments