diff --git a/src/Process/ForkedChildTerminator.php b/src/Process/ForkedChildTerminator.php index 0452f589d89..0ccfb2a97c9 100644 --- a/src/Process/ForkedChildTerminator.php +++ b/src/Process/ForkedChildTerminator.php @@ -2,8 +2,8 @@ namespace PHPStan\Process; +use PHPStan\Turbo\ForkedChild; use PHPStan\Turbo\TurboExtensionEnabler; -use PHPStanTurbo\Runtime; use function register_shutdown_function; /** @@ -25,9 +25,10 @@ * Everything the parent reads from the child, the results over the socket * and the captured output, has gone through unbuffered fds by then. * - * PHP itself has no _exit(); the turbo extension provides it, and fork mode - * from a phar requires the extension anyway (see ForkParallelChecker). A - * source checkout forking without it keeps the plain exit(). + * PHP itself has no _exit(); the turbo extension provides it + * (ForkedChild::exitImmediately()), and fork mode from a phar requires the + * extension anyway (see ForkParallelChecker). A source checkout forking + * without it keeps the plain exit(). */ final class ForkedChildTerminator { @@ -39,7 +40,7 @@ public static function install(): void } register_shutdown_function(static function (): void { - Runtime::exitImmediately(); + ForkedChild::exitImmediately(); }); } diff --git a/src/Turbo/ForkedChild.php b/src/Turbo/ForkedChild.php new file mode 100644 index 00000000000..d7b86e9e696 --- /dev/null +++ b/src/Turbo/ForkedChild.php @@ -0,0 +1,30 @@ + +#else +#include +#endif + +static zend_class_entry *pt_ce_forked_child = nullptr; + +/* {{{ registration */ + +#include "reg.h" + +PT_MINIT_REGISTRATION(pt_register_forked_child) +{ + reg::Class cls("PHPStan\\Turbo\\ForkedChild"); + ptdecl::ForkedChild::declareClass(cls); + ptdecl::ForkedChild::declareProperties(cls); + + /* exitImmediately() — _exit() for a pcntl_fork()ed worker; + * ForkedChildTerminator registers it as the child's last shutdown + * function. + * + * A forked child inherits the whole parent process, every loaded + * extension with whatever background threads it started included — but + * fork() copies only the calling thread. PHP's exit() then runs + * destructors and each extension's module shutdown, and an extension + * whose shutdown waits for its threads to check out (ext-grpc's + * grpc_shutdown() without grpc.enable_fork_support, for one) waits + * forever for threads the child never had: the worker has delivered its + * results, the parent keeps polling waitpid(), and the run hangs at 100%. + * A forked child that does not exec() must end with _exit() — no + * destructors, no module shutdown, no atexit handlers; that teardown is + * the parent's. The shutdown functions have run by then, so the crash + * report (ForkedChildCrashReporter) is written, and everything the parent + * reads — the results over the socket, the captured output — went + * through unbuffered fds. + * + * The status is the engine's: what exit() was given, 255 after a fatal + * error, 0 otherwise. */ + cls.method(sigs::exitImmediately, [](INTERNAL_FUNCTION_PARAMETERS) { + ZEND_PARSE_PARAMETERS_NONE(); + + _exit(EG(exit_status)); + }); + + cls.shadow(&pt_ce_forked_child); +} + +/* }}} */ diff --git a/turbo-ext/src/generated/ForkedChild.h b/turbo-ext/src/generated/ForkedChild.h new file mode 100644 index 00000000000..79f871a2662 --- /dev/null +++ b/turbo-ext/src/generated/ForkedChild.h @@ -0,0 +1,39 @@ +/* Generated by turbo-ext/bin/generate-declarations.php from + * src/Turbo/ForkedChild.php — do not edit. */ + +#ifndef PHPSTANTURBO_GENERATED_FORKED_CHILD_H +#define PHPSTANTURBO_GENERATED_FORKED_CHILD_H + +#include "../reg.h" + +namespace ptdecl::ForkedChild { + +inline void declareClass(reg::Class &cls) +{ + cls.final(); +} + +/* the properties the class declares itself, in declaration order (a used trait's come from its registrar) */ +inline void declareProperties(reg::Class &cls) +{ + (void) cls; +} + +/* the string and parameter tables the signatures below index into (see reg::Sig) */ +namespace sigtab { +inline constexpr char strings[] = + "exitImmediately"; /* 0 */ +inline constexpr reg::PackedArg args[] = { + reg::packed(0, 0), /* placeholder: C++ has no empty arrays */ +}; +using Sig = reg::Sig; +} // namespace sigtab + +/* the signatures of the methods the class declares itself (a used trait's are in the trait's header) */ +namespace sig { +inline constexpr sigtab::Sig exitImmediately = { { 0 /* exitImmediately */, 0, 0, 0, reg::NoArg, ZEND_ACC_PUBLIC | ZEND_ACC_STATIC } }; +} // namespace sig + +} // namespace ptdecl::ForkedChild + +#endif diff --git a/turbo-ext/src/main.cpp b/turbo-ext/src/main.cpp index bd8ac04380d..53220507058 100644 --- a/turbo-ext/src/main.cpp +++ b/turbo-ext/src/main.cpp @@ -15,13 +15,6 @@ #include "Engine.h" #include "version.h" -#ifdef PHP_WIN32 -#include -#else -#include -#endif - - /* PHPStanTurbo\Runtime::configure() — cold-path configuration entry point. * TurboExtensionEnabler passes the generated class map (derived from the * ReferencedByTurboExtension attributes) that the native code resolves @@ -107,33 +100,6 @@ static void ZEND_FASTCALL runtimeTrustTypesUnder(INTERNAL_FUNCTION_PARAMETERS) RETURN_BOOL(pt_trusted_types_set_prefix(prefix)); } -/* PHPStanTurbo\Runtime::exitImmediately() — _exit() for a pcntl_fork()ed - * worker; ForkedChildTerminator registers it as the child's last shutdown - * function. - * - * A forked child inherits the whole parent process, every loaded extension - * with whatever background threads it started included — but fork() copies - * only the calling thread. PHP's exit() then runs destructors and each - * extension's module shutdown, and an extension whose shutdown waits for - * its threads to check out (ext-grpc's grpc_shutdown() without - * grpc.enable_fork_support, for one) waits forever for threads the child - * never had: the worker has delivered its results, the parent keeps - * polling waitpid(), and the run hangs at 100%. A forked child that does - * not exec() must end with _exit() — no destructors, no module shutdown, no - * atexit handlers; that teardown is the parent's. The shutdown functions - * have run by then, so the crash report (ForkedChildCrashReporter) is - * written, and everything the parent reads — the results over the socket, - * the captured output — went through unbuffered fds. - * - * The status is the engine's: what exit() was given, 255 after a fatal - * error, 0 otherwise. */ -static void ZEND_FASTCALL runtimeExitImmediately(INTERNAL_FUNCTION_PARAMETERS) -{ - ZEND_PARSE_PARAMETERS_NONE(); - - _exit(EG(exit_status)); -} - /* Whether the shared core loaded next to this library carries the same * version: a mismatched pair (a torn update of the distributed files) must * never run together — the extension then registers nothing and reports @@ -201,7 +167,6 @@ static PHP_MINIT_FUNCTION(phpstan_turbo) runtime.method("isShadowing", reg::PublicStatic, 0, {}, runtimeIsShadowing, &returnsBool); runtime.method("enablePharForkGuard", reg::PublicStatic, 1, { reg::stringArg("pharPath") }, runtimeEnablePharForkGuard); runtime.method("trustTypesUnder", reg::PublicStatic, 1, { reg::stringArg("prefix") }, runtimeTrustTypesUnder, &returnsBool); - runtime.method("exitImmediately", reg::PublicStatic, 0, {}, runtimeExitImmediately); runtime.register_(); pt_minit_registrations_run(); diff --git a/turbo-ext/tests/exit-immediately.php b/turbo-ext/tests/exit-immediately.php index eefb96e2afd..b6f0697ca90 100644 --- a/turbo-ext/tests/exit-immediately.php +++ b/turbo-ext/tests/exit-immediately.php @@ -1,7 +1,7 @@ $exited, 'status' => $status, 'markers' => $written]; } -$control = forkAndExit(false); +$control = forkAndExit(null); printf("control (exit): exited %s, markers %s\n", $control['exited'] ? 'yes' : 'no', json_encode($control['markers'])); -$terminated = forkAndExit(true); -printf("exitImmediately(): exited %s, status %s, markers %s\n", $terminated['exited'] ? 'yes' : 'no', var_export($terminated['status'], true), json_encode($terminated['markers'])); +$terminated = forkAndExit(\PHPStanTurbo\ForkedChild::class); +printf("native: exited %s, status %s, markers %s\n", $terminated['exited'] ? 'yes' : 'no', var_export($terminated['status'], true), json_encode($terminated['markers'])); + +$twinThrew = false; +try { + \PHPStan\Turbo\ForkedChild::exitImmediately(); +} catch (\PHPStan\Turbo\TurboNotActiveException) { + $twinThrew = true; +} +printf("PHP twin: threw TurboNotActiveException %s\n", $twinThrew ? 'yes' : 'no'); if ($control['exited'] || $control['markers'] !== "shutdown\ndestructor\n") { 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 fwrite(STDERR, "FAIL: unexpected teardown ran in the child\n"); exit(1); } +if (!$twinThrew) { + fwrite(STDERR, "FAIL: the PHP twin's exitImmediately() did not throw TurboNotActiveException\n"); + exit(1); +} echo "ALL OK\n"; diff --git a/turbo-ext/tests/smoke.php b/turbo-ext/tests/smoke.php index a26c086997e..db61783b772 100644 --- a/turbo-ext/tests/smoke.php +++ b/turbo-ext/tests/smoke.php @@ -4659,6 +4659,7 @@ public function getComments(): array \PHPStan\Cache\ArenaCache::class => 'arena-smoke.php', \PHPStan\Parser\ParserRunner::class => 'parser-corpus.php', \PHPStan\Reflection\BetterReflection\SourceLocator\PhpFileCleaner::class => 'php-file-cleaner-corpus.php', + \PHPStan\Turbo\ForkedChild::class => 'exit-immediately.php', \PHPStan\Reflection\BetterReflection\SourceLocator\SymbolFinderInFiles::class => 'symbol-finder-corpus.php', \PHPStan\Analyser\ExprHandler\Helper\EarlyTerminatingCallHelper::class => 'walk-trace.php', \PHPStan\Analyser\ExprHandler\Helper\MethodCallReturnTypeHelper::class => 'walk-trace.php',