Skip to content

Commit 4ac1d65

Browse files
ondrejmirtesclaude
andcommitted
Introduce ProcessPromise interface for the PHPStan Pro worker
ProcessPromise becomes an interface describing the fixer:worker process as seen by FixerApplication; the existing react/child-process implementation is renamed to SpawnedProcessPromise. FixerApplication creates it through a factory method. Sole implementation for now — a pcntl_fork() based one follows. Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>
1 parent 639b4d4 commit 4ac1d65

3 files changed

Lines changed: 134 additions & 89 deletions

File tree

src/Command/FixerApplication.php

Lines changed: 25 additions & 10 deletions
Original file line numberDiff line numberDiff line change
@@ -28,6 +28,7 @@
2828
use PHPStan\Process\ProcessCrashedException;
2929
use PHPStan\Process\ProcessHelper;
3030
use PHPStan\Process\ProcessPromise;
31+
use PHPStan\Process\SpawnedProcessPromise;
3132
use PHPStan\ShouldNotHappenException;
3233
use React\ChildProcess\Process;
3334
use React\EventLoop\LoopInterface;
@@ -446,16 +447,7 @@ private function analyse(
446447
});
447448
});
448449

449-
$process = new ProcessPromise($loop, ProcessHelper::getWorkerCommand(
450-
$mainScript,
451-
'fixer:worker',
452-
$projectConfigFile,
453-
[
454-
'--server-port',
455-
(string) $serverPort,
456-
],
457-
$input,
458-
));
450+
$process = $this->createProcessPromise($loop, $mainScript, $projectConfigFile, $input, $serverPort);
459451
$this->processInProgress = $process->run();
460452

461453
$this->processInProgress->then(function () use ($server): void {
@@ -566,4 +558,27 @@ private function getStubFiles(): array
566558
return $stubFiles;
567559
}
568560

561+
/**
562+
* @param int<0, 65535> $serverPort
563+
*/
564+
private function createProcessPromise(
565+
LoopInterface $loop,
566+
string $mainScript,
567+
?string $projectConfigFile,
568+
InputInterface $input,
569+
int $serverPort,
570+
): ProcessPromise
571+
{
572+
return new SpawnedProcessPromise($loop, ProcessHelper::getWorkerCommand(
573+
$mainScript,
574+
'fixer:worker',
575+
$projectConfigFile,
576+
[
577+
'--server-port',
578+
(string) $serverPort,
579+
],
580+
$input,
581+
));
582+
}
583+
569584
}

src/Process/ProcessPromise.php

Lines changed: 11 additions & 79 deletions
Original file line numberDiff line numberDiff line change
@@ -2,91 +2,23 @@
22

33
namespace PHPStan\Process;
44

5-
use PHPStan\ShouldNotHappenException;
6-
use React\ChildProcess\Process;
7-
use React\EventLoop\LoopInterface;
8-
use React\Promise\Deferred;
95
use React\Promise\PromiseInterface;
10-
use function fclose;
11-
use function rewind;
12-
use function stream_get_contents;
13-
use function tmpfile;
146

15-
final class ProcessPromise
7+
/**
8+
* A PHPStan Pro analysis worker as seen by FixerApplication.
9+
*
10+
* Implementations differ only in how the worker process comes to life:
11+
* SpawnedProcessPromise spawns a fresh PHP process via react/child-process,
12+
* ForkedProcessPromise forks the already-booted main process via pcntl_fork().
13+
* Both yield a promise that resolves on success and rejects with
14+
* ProcessCrashedException / ProcessCanceledException otherwise.
15+
*/
16+
interface ProcessPromise
1617
{
1718

18-
/** @var Deferred<string> */
19-
private Deferred $deferred;
20-
21-
private ?Process $process = null;
22-
23-
private bool $canceled = false;
24-
25-
public function __construct(private LoopInterface $loop, private string $command)
26-
{
27-
$this->deferred = new Deferred(function (): void {
28-
$this->cancel();
29-
});
30-
}
31-
3219
/**
3320
* @return PromiseInterface<string>
3421
*/
35-
public function run(): PromiseInterface
36-
{
37-
$tmpStdOutResource = tmpfile();
38-
if ($tmpStdOutResource === false) {
39-
throw new ShouldNotHappenException('Failed creating temp file for stdout.');
40-
}
41-
$tmpStdErrResource = tmpfile();
42-
if ($tmpStdErrResource === false) {
43-
throw new ShouldNotHappenException('Failed creating temp file for stderr.');
44-
}
45-
46-
$this->process = new Process($this->command, fds: [
47-
1 => $tmpStdOutResource,
48-
2 => $tmpStdErrResource,
49-
]);
50-
$this->process->start($this->loop);
51-
52-
$this->process->on('exit', function ($exitCode) use ($tmpStdOutResource, $tmpStdErrResource): void {
53-
if ($this->canceled) {
54-
fclose($tmpStdOutResource);
55-
fclose($tmpStdErrResource);
56-
return;
57-
}
58-
rewind($tmpStdOutResource);
59-
$stdOut = stream_get_contents($tmpStdOutResource);
60-
fclose($tmpStdOutResource);
61-
62-
rewind($tmpStdErrResource);
63-
$stdErr = stream_get_contents($tmpStdErrResource);
64-
fclose($tmpStdErrResource);
65-
66-
if ($exitCode === null) {
67-
$this->deferred->reject(new ProcessCrashedException($stdOut . $stdErr));
68-
return;
69-
}
70-
71-
if ($exitCode === 0) {
72-
$this->deferred->resolve($stdOut);
73-
return;
74-
}
75-
76-
$this->deferred->reject(new ProcessCrashedException($stdOut . $stdErr));
77-
});
78-
79-
return $this->deferred->promise();
80-
}
81-
82-
private function cancel(): void
83-
{
84-
if ($this->process === null) {
85-
throw new ShouldNotHappenException('Cancelling process before running');
86-
}
87-
$this->canceled = true;
88-
$this->process->terminate();
89-
$this->deferred->reject(new ProcessCanceledException());
90-
}
22+
public function run(): PromiseInterface;
9123

9224
}
Lines changed: 98 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,98 @@
1+
<?php declare(strict_types = 1);
2+
3+
namespace PHPStan\Process;
4+
5+
use PHPStan\ShouldNotHappenException;
6+
use React\ChildProcess\Process;
7+
use React\EventLoop\LoopInterface;
8+
use React\Promise\Deferred;
9+
use React\Promise\PromiseInterface;
10+
use function fclose;
11+
use function rewind;
12+
use function stream_get_contents;
13+
use function tmpfile;
14+
15+
/**
16+
* ProcessPromise backed by a freshly spawned PHP process (react/child-process).
17+
*
18+
* @see ForkedProcessPromise for the pcntl_fork()-based alternative that skips
19+
* the per-worker application re-boot.
20+
*/
21+
final class SpawnedProcessPromise implements ProcessPromise
22+
{
23+
24+
/** @var Deferred<string> */
25+
private Deferred $deferred;
26+
27+
private ?Process $process = null;
28+
29+
private bool $canceled = false;
30+
31+
public function __construct(private LoopInterface $loop, private string $command)
32+
{
33+
$this->deferred = new Deferred(function (): void {
34+
$this->cancel();
35+
});
36+
}
37+
38+
/**
39+
* @return PromiseInterface<string>
40+
*/
41+
public function run(): PromiseInterface
42+
{
43+
$tmpStdOutResource = tmpfile();
44+
if ($tmpStdOutResource === false) {
45+
throw new ShouldNotHappenException('Failed creating temp file for stdout.');
46+
}
47+
$tmpStdErrResource = tmpfile();
48+
if ($tmpStdErrResource === false) {
49+
throw new ShouldNotHappenException('Failed creating temp file for stderr.');
50+
}
51+
52+
$this->process = new Process($this->command, fds: [
53+
1 => $tmpStdOutResource,
54+
2 => $tmpStdErrResource,
55+
]);
56+
$this->process->start($this->loop);
57+
58+
$this->process->on('exit', function ($exitCode) use ($tmpStdOutResource, $tmpStdErrResource): void {
59+
if ($this->canceled) {
60+
fclose($tmpStdOutResource);
61+
fclose($tmpStdErrResource);
62+
return;
63+
}
64+
rewind($tmpStdOutResource);
65+
$stdOut = stream_get_contents($tmpStdOutResource);
66+
fclose($tmpStdOutResource);
67+
68+
rewind($tmpStdErrResource);
69+
$stdErr = stream_get_contents($tmpStdErrResource);
70+
fclose($tmpStdErrResource);
71+
72+
if ($exitCode === null) {
73+
$this->deferred->reject(new ProcessCrashedException($stdOut . $stdErr));
74+
return;
75+
}
76+
77+
if ($exitCode === 0) {
78+
$this->deferred->resolve($stdOut);
79+
return;
80+
}
81+
82+
$this->deferred->reject(new ProcessCrashedException($stdOut . $stdErr));
83+
});
84+
85+
return $this->deferred->promise();
86+
}
87+
88+
private function cancel(): void
89+
{
90+
if ($this->process === null) {
91+
throw new ShouldNotHappenException('Cancelling process before running');
92+
}
93+
$this->canceled = true;
94+
$this->process->terminate();
95+
$this->deferred->reject(new ProcessCanceledException());
96+
}
97+
98+
}

0 commit comments

Comments
 (0)