Skip to content

Commit 606bf02

Browse files
ondrejmirtesclaude
andcommitted
Extract WorkerRunner from WorkerCommand
Moves everything WorkerCommand does after CommandHelper::begin() — the TCP connect, NDJSON handshake and per-file analysis loop — into a reusable WorkerRunner service. WorkerCommand now only boots and delegates. This makes the post-boot worker logic callable without a re-boot, which the pcntl_fork() path will rely on. Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>
1 parent 1ac7688 commit 606bf02

2 files changed

Lines changed: 260 additions & 201 deletions

File tree

src/Command/WorkerCommand.php

Lines changed: 13 additions & 201 deletions
Original file line numberDiff line numberDiff line change
@@ -2,47 +2,25 @@
22

33
namespace PHPStan\Command;
44

5-
use Clue\React\NDJson\Decoder;
6-
use Clue\React\NDJson\Encoder;
75
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;
136
use PHPStan\File\PathNotFoundException;
14-
use PHPStan\Rules\Registry as RuleRegistry;
7+
use PHPStan\Parallel\WorkerRunner;
158
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;
219
use Symfony\Component\Console\Command\Command;
2210
use Symfony\Component\Console\Input\InputArgument;
2311
use Symfony\Component\Console\Input\InputInterface;
2412
use Symfony\Component\Console\Input\InputOption;
2513
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;
3314
use function is_array;
3415
use function is_bool;
3516
use function is_string;
36-
use function memory_get_peak_usage;
3717
use function sprintf;
3818

3919
final class WorkerCommand extends Command
4020
{
4121

4222
private const NAME = 'worker';
4323

44-
private int $errorCount = 0;
45-
4624
/**
4725
* @param string[] $composerAutoloaderProjectPaths
4826
*/
@@ -122,196 +100,30 @@ protected function execute(InputInterface $input, OutputInterface $output): int
122100
} catch (InceptionNotSuccessfulException $e) {
123101
return 1;
124102
}
125-
$loop = new StreamSelectLoop();
126103

127104
$container = $inceptionResult->getContainer();
128105

129106
try {
130107
[$analysedFiles] = $inceptionResult->getFiles();
131-
$analysedFiles = $this->switchTmpFile($analysedFiles, $insteadOfFile, $tmpFile);
132108
} catch (PathNotFoundException $e) {
133109
$inceptionResult->getErrorOutput()->writeLineFormatted(sprintf('<error>%s</error>', $e->getMessage()));
134110
return 1;
135111
} catch (InceptionNotSuccessfulException) {
136112
return 1;
137113
}
138114

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+
);
315127
}
316128

317129
}

0 commit comments

Comments
 (0)