Skip to content

Commit f064084

Browse files
authored
Move exitImmediately() to ForkedChild, a shadowed class in the API (#6793)
1 parent 2512359 commit f064084

10 files changed

Lines changed: 199 additions & 64 deletions

File tree

‎src/Process/ForkedChildTerminator.php‎

Lines changed: 6 additions & 5 deletions
Original file line numberDiff line numberDiff line change
@@ -2,8 +2,8 @@
22

33
namespace PHPStan\Process;
44

5+
use PHPStan\Turbo\ForkedChild;
56
use PHPStan\Turbo\TurboExtensionEnabler;
6-
use PHPStanTurbo\Runtime;
77
use function register_shutdown_function;
88

99
/**
@@ -25,9 +25,10 @@
2525
* Everything the parent reads from the child, the results over the socket
2626
* and the captured output, has gone through unbuffered fds by then.
2727
*
28-
* PHP itself has no _exit(); the turbo extension provides it, and fork mode
29-
* from a phar requires the extension anyway (see ForkParallelChecker). A
30-
* source checkout forking without it keeps the plain exit().
28+
* PHP itself has no _exit(); the turbo extension provides it
29+
* (ForkedChild::exitImmediately()), and fork mode from a phar requires the
30+
* extension anyway (see ForkParallelChecker). A source checkout forking
31+
* without it keeps the plain exit().
3132
*/
3233
final class ForkedChildTerminator
3334
{
@@ -39,7 +40,7 @@ public static function install(): void
3940
}
4041

4142
register_shutdown_function(static function (): void {
42-
Runtime::exitImmediately();
43+
ForkedChild::exitImmediately();
4344
});
4445
}
4546

‎src/Turbo/ForkedChild.php‎

Lines changed: 30 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,30 @@
1+
<?php declare(strict_types = 1);
2+
3+
namespace PHPStan\Turbo;
4+
5+
/**
6+
* What a pcntl_fork()ed child needs from the turbo extension. With the
7+
* extension active (TurboExtensionEnabler::isActive()), the native class
8+
* replaces this one.
9+
*
10+
* @api
11+
*/
12+
#[ShadowedByTurboExtension(implementation: __DIR__ . '/../../turbo-ext/src/ForkedChild.cpp')]
13+
final class ForkedChild
14+
{
15+
16+
/**
17+
* Ends the child with _exit() and the engine's exit status, skipping
18+
* destructors and the module shutdown of every extension the child
19+
* inherited - one waiting for threads the child never had wedges there.
20+
* Register it as the child's last shutdown function.
21+
*
22+
* @return never
23+
* @throws TurboNotActiveException
24+
*/
25+
public static function exitImmediately()
26+
{
27+
throw new TurboNotActiveException();
28+
}
29+
30+
}
Lines changed: 20 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,20 @@
1+
<?php declare(strict_types = 1);
2+
3+
namespace PHPStan\Turbo;
4+
5+
use Exception;
6+
7+
/**
8+
* Thrown by a method only the active turbo extension implements.
9+
*
10+
* @api
11+
*/
12+
final class TurboNotActiveException extends Exception
13+
{
14+
15+
public function __construct()
16+
{
17+
parent::__construct('The turbo extension is not active.');
18+
}
19+
20+
}

‎turbo-ext/README.md‎

Lines changed: 10 additions & 5 deletions
Original file line numberDiff line numberDiff line change
@@ -110,11 +110,16 @@ hardcoded in the collector), so a renamed class updates the map on the next
110110
autoloader dump. Shadowed classes are never referenced that way — the native
111111
code holds their class entries itself and instantiates them directly.
112112

113-
Two more `Runtime` entry points serve fork mode (see `ForkParallelChecker`):
114-
`enablePharForkGuard()` gives each pcntl_fork()ed worker a private cursor on
115-
the running phar's fd (`PharForkGuard.cpp`), and `exitImmediately()` —
116-
`_exit()` with the engine's exit status — is how `ForkedChildTerminator` ends
117-
a forked worker. A forked child inherits every loaded extension but none of
113+
Fork mode (see `ForkParallelChecker`) uses two more entry points:
114+
`Runtime::enablePharForkGuard()` gives each pcntl_fork()ed worker a private
115+
cursor on the running phar's fd (`PharForkGuard.cpp`), and
116+
`exitImmediately()` — `_exit()` with the engine's exit status — is how
117+
`ForkedChildTerminator` ends a forked worker. `exitImmediately()` lives on
118+
`PHPStan\Turbo\ForkedChild`, a shadowed class that is part of PHPStan's
119+
`@api` (`ForkedChild.cpp`), because a program that forks its own workers
120+
needs it too; its PHP twin throws `TurboNotActiveException`. The `Runtime`
121+
methods cannot move there: `TurboExtensionEnabler` calls them before
122+
shadowing is active. A forked child inherits every loaded extension but none of
118123
their threads, so PHP's full teardown can wedge in a fork-unsafe extension's
119124
module shutdown (ext-grpc without `grpc.enable_fork_support`); a forked child
120125
that does not exec() must `_exit()` instead.

‎turbo-ext/analysis-stub.php‎

Lines changed: 0 additions & 4 deletions
Original file line numberDiff line numberDiff line change
@@ -37,8 +37,4 @@ public static function trustTypesUnder(string $prefix): bool
3737
{
3838
}
3939

40-
public static function exitImmediately(): never
41-
{
42-
}
43-
4440
}

‎turbo-ext/src/ForkedChild.cpp‎

Lines changed: 63 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,63 @@
1+
/*
2+
* PHPStanTurbo\ForkedChild — native implementation of
3+
* PHPStan\Turbo\ForkedChild, what a pcntl_fork()ed child needs from the
4+
* extension, as part of PHPStan's API. Unlike PHPStanTurbo\Runtime
5+
* (main.cpp), which TurboExtensionEnabler calls before shadowing is active,
6+
* this class only exists under its PHP name once activateShadowing() ran;
7+
* the PHP twin throws TurboNotActiveException.
8+
*/
9+
10+
#include "support.h"
11+
#include "generated/ForkedChild.h"
12+
13+
namespace sigs = ptdecl::ForkedChild::sig;
14+
15+
#ifdef PHP_WIN32
16+
#include <process.h>
17+
#else
18+
#include <unistd.h>
19+
#endif
20+
21+
static zend_class_entry *pt_ce_forked_child = nullptr;
22+
23+
/* {{{ registration */
24+
25+
#include "reg.h"
26+
27+
PT_MINIT_REGISTRATION(pt_register_forked_child)
28+
{
29+
reg::Class cls("PHPStan\\Turbo\\ForkedChild");
30+
ptdecl::ForkedChild::declareClass(cls);
31+
ptdecl::ForkedChild::declareProperties(cls);
32+
33+
/* exitImmediately() — _exit() for a pcntl_fork()ed worker;
34+
* ForkedChildTerminator registers it as the child's last shutdown
35+
* function.
36+
*
37+
* A forked child inherits the whole parent process, every loaded
38+
* extension with whatever background threads it started included — but
39+
* fork() copies only the calling thread. PHP's exit() then runs
40+
* destructors and each extension's module shutdown, and an extension
41+
* whose shutdown waits for its threads to check out (ext-grpc's
42+
* grpc_shutdown() without grpc.enable_fork_support, for one) waits
43+
* forever for threads the child never had: the worker has delivered its
44+
* results, the parent keeps polling waitpid(), and the run hangs at 100%.
45+
* A forked child that does not exec() must end with _exit() — no
46+
* destructors, no module shutdown, no atexit handlers; that teardown is
47+
* the parent's. The shutdown functions have run by then, so the crash
48+
* report (ForkedChildCrashReporter) is written, and everything the parent
49+
* reads — the results over the socket, the captured output — went
50+
* through unbuffered fds.
51+
*
52+
* The status is the engine's: what exit() was given, 255 after a fatal
53+
* error, 0 otherwise. */
54+
cls.method(sigs::exitImmediately, [](INTERNAL_FUNCTION_PARAMETERS) {
55+
ZEND_PARSE_PARAMETERS_NONE();
56+
57+
_exit(EG(exit_status));
58+
});
59+
60+
cls.shadow(&pt_ce_forked_child);
61+
}
62+
63+
/* }}} */
Lines changed: 39 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,39 @@
1+
/* Generated by turbo-ext/bin/generate-declarations.php from
2+
* src/Turbo/ForkedChild.php — do not edit. */
3+
4+
#ifndef PHPSTANTURBO_GENERATED_FORKED_CHILD_H
5+
#define PHPSTANTURBO_GENERATED_FORKED_CHILD_H
6+
7+
#include "../reg.h"
8+
9+
namespace ptdecl::ForkedChild {
10+
11+
inline void declareClass(reg::Class &cls)
12+
{
13+
cls.final();
14+
}
15+
16+
/* the properties the class declares itself, in declaration order (a used trait's come from its registrar) */
17+
inline void declareProperties(reg::Class &cls)
18+
{
19+
(void) cls;
20+
}
21+
22+
/* the string and parameter tables the signatures below index into (see reg::Sig) */
23+
namespace sigtab {
24+
inline constexpr char strings[] =
25+
"exitImmediately"; /* 0 */
26+
inline constexpr reg::PackedArg args[] = {
27+
reg::packed(0, 0), /* placeholder: C++ has no empty arrays */
28+
};
29+
using Sig = reg::Sig<strings, args>;
30+
} // namespace sigtab
31+
32+
/* the signatures of the methods the class declares itself (a used trait's are in the trait's header) */
33+
namespace sig {
34+
inline constexpr sigtab::Sig exitImmediately = { { 0 /* exitImmediately */, 0, 0, 0, reg::NoArg, ZEND_ACC_PUBLIC | ZEND_ACC_STATIC } };
35+
} // namespace sig
36+
37+
} // namespace ptdecl::ForkedChild
38+
39+
#endif

‎turbo-ext/src/main.cpp‎

Lines changed: 0 additions & 35 deletions
Original file line numberDiff line numberDiff line change
@@ -15,13 +15,6 @@
1515
#include "Engine.h"
1616
#include "version.h"
1717

18-
#ifdef PHP_WIN32
19-
#include <process.h>
20-
#else
21-
#include <unistd.h>
22-
#endif
23-
24-
2518
/* PHPStanTurbo\Runtime::configure() — cold-path configuration entry point.
2619
* TurboExtensionEnabler passes the generated class map (derived from the
2720
* ReferencedByTurboExtension attributes) that the native code resolves
@@ -107,33 +100,6 @@ static void ZEND_FASTCALL runtimeTrustTypesUnder(INTERNAL_FUNCTION_PARAMETERS)
107100
RETURN_BOOL(pt_trusted_types_set_prefix(prefix));
108101
}
109102

110-
/* PHPStanTurbo\Runtime::exitImmediately() — _exit() for a pcntl_fork()ed
111-
* worker; ForkedChildTerminator registers it as the child's last shutdown
112-
* function.
113-
*
114-
* A forked child inherits the whole parent process, every loaded extension
115-
* with whatever background threads it started included — but fork() copies
116-
* only the calling thread. PHP's exit() then runs destructors and each
117-
* extension's module shutdown, and an extension whose shutdown waits for
118-
* its threads to check out (ext-grpc's grpc_shutdown() without
119-
* grpc.enable_fork_support, for one) waits forever for threads the child
120-
* never had: the worker has delivered its results, the parent keeps
121-
* polling waitpid(), and the run hangs at 100%. A forked child that does
122-
* not exec() must end with _exit() — no destructors, no module shutdown, no
123-
* atexit handlers; that teardown is the parent's. The shutdown functions
124-
* have run by then, so the crash report (ForkedChildCrashReporter) is
125-
* written, and everything the parent reads — the results over the socket,
126-
* the captured output — went through unbuffered fds.
127-
*
128-
* The status is the engine's: what exit() was given, 255 after a fatal
129-
* error, 0 otherwise. */
130-
static void ZEND_FASTCALL runtimeExitImmediately(INTERNAL_FUNCTION_PARAMETERS)
131-
{
132-
ZEND_PARSE_PARAMETERS_NONE();
133-
134-
_exit(EG(exit_status));
135-
}
136-
137103
/* Whether the shared core loaded next to this library carries the same
138104
* version: a mismatched pair (a torn update of the distributed files) must
139105
* never run together — the extension then registers nothing and reports
@@ -201,7 +167,6 @@ static PHP_MINIT_FUNCTION(phpstan_turbo)
201167
runtime.method("isShadowing", reg::PublicStatic, 0, {}, runtimeIsShadowing, &returnsBool);
202168
runtime.method("enablePharForkGuard", reg::PublicStatic, 1, { reg::stringArg("pharPath") }, runtimeEnablePharForkGuard);
203169
runtime.method("trustTypesUnder", reg::PublicStatic, 1, { reg::stringArg("prefix") }, runtimeTrustTypesUnder, &returnsBool);
204-
runtime.method("exitImmediately", reg::PublicStatic, 0, {}, runtimeExitImmediately);
205170
runtime.register_();
206171

207172
pt_minit_registrations_run();

‎turbo-ext/tests/exit-immediately.php‎

Lines changed: 30 additions & 15 deletions
Original file line numberDiff line numberDiff line change
@@ -1,7 +1,7 @@
11
<?php declare(strict_types=1);
22

33
/**
4-
* Test for Runtime::exitImmediately().
4+
* Test for ForkedChild::exitImmediately().
55
*
66
* A pcntl_fork()ed child inherits the parent's whole process, and PHP's
77
* exit() runs the full teardown there: destructors, then every extension's
@@ -17,25 +17,27 @@
1717
* Two passes over the same child:
1818
* 1. control, plain exit() - the child must still be alive at the deadline
1919
* (proves the workload hangs)
20-
* 2. exitImmediately() from a shutdown function - the child must be gone
21-
* well within the deadline, with exit()'s status, the earlier shutdown
22-
* function's marker written and the destructor's not
20+
* 2. the native exitImmediately() from a shutdown function - the child must
21+
* be gone well within the deadline, with exit()'s status, the earlier
22+
* shutdown function's marker written and the destructor's not
23+
*
24+
* The PHP twin, which runs without the active extension, must throw
25+
* TurboNotActiveException instead.
2326
*
2427
* Run: php -d extension=$PWD/phpstan_turbo.so tests/exit-immediately.php
2528
*/
2629

2730
const DEADLINE_SECONDS = 3;
2831
const EXIT_STATUS = 7;
2932

30-
if (!extension_loaded('phpstan_turbo')) {
31-
fwrite(STDERR, "phpstan_turbo extension is not loaded\n");
32-
exit(1);
33-
}
3433
if (!function_exists('pcntl_fork')) {
3534
echo "SKIP: pcntl is not available\n";
3635
exit(0);
3736
}
3837

38+
// declares the native class as PHPStanTurbo\ForkedChild next to the PHP twin
39+
require __DIR__ . '/activate-prefixed.php';
40+
3941
final class WedgedTeardown
4042
{
4143

@@ -57,9 +59,10 @@ public function __destruct()
5759
}
5860

5961
/**
62+
* @param class-string|null $terminator
6063
* @return array{exited: bool, status: int|null, markers: string}
6164
*/
62-
function forkAndExit(bool $terminate): array
65+
function forkAndExit(?string $terminator): array
6366
{
6467
$markers = tmpfile();
6568
if ($markers === false) {
@@ -78,9 +81,9 @@ function forkAndExit(bool $terminate): array
7881
register_shutdown_function(static function () use ($markers): void {
7982
fwrite($markers, "shutdown\n");
8083
});
81-
if ($terminate) {
82-
register_shutdown_function(static function (): void {
83-
PHPStanTurbo\Runtime::exitImmediately();
84+
if ($terminator !== null) {
85+
register_shutdown_function(static function () use ($terminator): void {
86+
$terminator::exitImmediately();
8487
});
8588
}
8689
exit(EXIT_STATUS);
@@ -110,11 +113,19 @@ function forkAndExit(bool $terminate): array
110113
return ['exited' => $exited, 'status' => $status, 'markers' => $written];
111114
}
112115

113-
$control = forkAndExit(false);
116+
$control = forkAndExit(null);
114117
printf("control (exit): exited %s, markers %s\n", $control['exited'] ? 'yes' : 'no', json_encode($control['markers']));
115118

116-
$terminated = forkAndExit(true);
117-
printf("exitImmediately(): exited %s, status %s, markers %s\n", $terminated['exited'] ? 'yes' : 'no', var_export($terminated['status'], true), json_encode($terminated['markers']));
119+
$terminated = forkAndExit(\PHPStanTurbo\ForkedChild::class);
120+
printf("native: exited %s, status %s, markers %s\n", $terminated['exited'] ? 'yes' : 'no', var_export($terminated['status'], true), json_encode($terminated['markers']));
121+
122+
$twinThrew = false;
123+
try {
124+
\PHPStan\Turbo\ForkedChild::exitImmediately();
125+
} catch (\PHPStan\Turbo\TurboNotActiveException) {
126+
$twinThrew = true;
127+
}
128+
printf("PHP twin: threw TurboNotActiveException %s\n", $twinThrew ? 'yes' : 'no');
118129

119130
if ($control['exited'] || $control['markers'] !== "shutdown\ndestructor\n") {
120131
fwrite(STDERR, "FAIL: the control child did not wedge in its destructor - the workload no longer models the hang\n");
@@ -132,5 +143,9 @@ function forkAndExit(bool $terminate): array
132143
fwrite(STDERR, "FAIL: unexpected teardown ran in the child\n");
133144
exit(1);
134145
}
146+
if (!$twinThrew) {
147+
fwrite(STDERR, "FAIL: the PHP twin's exitImmediately() did not throw TurboNotActiveException\n");
148+
exit(1);
149+
}
135150

136151
echo "ALL OK\n";

‎turbo-ext/tests/smoke.php‎

Lines changed: 1 addition & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -4659,6 +4659,7 @@ public function getComments(): array
46594659
\PHPStan\Cache\ArenaCache::class => 'arena-smoke.php',
46604660
\PHPStan\Parser\ParserRunner::class => 'parser-corpus.php',
46614661
\PHPStan\Reflection\BetterReflection\SourceLocator\PhpFileCleaner::class => 'php-file-cleaner-corpus.php',
4662+
\PHPStan\Turbo\ForkedChild::class => 'exit-immediately.php',
46624663
\PHPStan\Reflection\BetterReflection\SourceLocator\SymbolFinderInFiles::class => 'symbol-finder-corpus.php',
46634664
\PHPStan\Analyser\ExprHandler\Helper\EarlyTerminatingCallHelper::class => 'walk-trace.php',
46644665
\PHPStan\Analyser\ExprHandler\Helper\MethodCallReturnTypeHelper::class => 'walk-trace.php',

0 commit comments

Comments
 (0)