22
33namespace PHPStan \Turbo ;
44
5+ use function explode ;
56use function extension_loaded ;
67use function function_exists ;
78use function get_cfg_var ;
9+ use function in_array ;
810use function ini_get ;
911use function is_string ;
1012use function max ;
1113use function pcntl_exec ;
1214use function php_ini_loaded_file ;
15+ use function strtolower ;
16+ use function trim ;
1317use const PHP_BINARY ;
1418
1519/**
16- * Restarts the main PHPStan process via pcntl_exec() with the distributed
17- * turbo extension loaded through -d extension=.
20+ * Restarts the main PHPStan process via pcntl_exec() when the process it
21+ * was started as is not the one PHPStan wants to run — for either of two
22+ * reasons, checked independently.
1823 *
19- * Parallel analysis prefers pcntl_fork()ed workers over spawned ones (see
24+ * The distributed turbo extension is available but not loaded. Parallel
25+ * analysis prefers pcntl_fork()ed workers over spawned ones (see
2026 * ForkParallelChecker) — a forked worker inherits the booted process instead
2127 * of paying a full re-boot. But a forked worker also inherits the parent's
2228 * loaded extensions: the spawn-time `-d extension=` injection
2329 * (ProcessHelper) cannot reach it, and dl() cannot load a binary from next
24- * to the phar. So when the extension binary is available but not loaded,
25- * the whole process is re-executed with it before anything else runs —
26- * forked workers then inherit the extension, its stub shadowing, and (when
27- * running from a phar) the phar-fork-guard that keeps phar:// reads safe
28- * across fork.
30+ * to the phar. So the whole process is re-executed with the extension
31+ * before anything else runs — forked workers then inherit the extension,
32+ * its stub shadowing, and (when running from a phar) the phar-fork-guard
33+ * that keeps phar:// reads safe across fork.
2934 *
30- * The restarted process also gets OPcache activated — with JIT pinned off,
31- * whatever the user's ini says — so forked workers share the parent's warm
32- * opcode cache (see resolveOpcacheArgs()).
35+ * The OPcache configuration in effect is not the one PHPStan wants (see
36+ * resolveOpcacheArgs()) — usually because OPcache is dormant on CLI, or has
37+ * JIT on. The restarted process runs with it activated, JIT pinned off, and
38+ * forked workers share the parent's warm opcode cache. This reason stands
39+ * on its own so that a php.ini which already loads turbo (a source checkout
40+ * with a locally built extension, say) does not leave OPcache dormant just
41+ * because there is no extension left to restart for.
3342 *
34- * The restart carries a marker ini entry with the extension path. It doubles
35- * as the retry stop (a binary that failed to load leaves the extension
36- * unloaded — without the marker the restart would loop) and tells
43+ * The restart carries two marker ini entries: RESTARTED_INI is the retry
44+ * stop (a binary that failed to load, or an OPcache that could not start,
45+ * would otherwise restart forever), and EXTENSION_PATH_INI tells
3746 * TurboExtensionSelector that spawned workers still need the -d flag
3847 * (command-line -d flags, unlike php.ini, are not inherited).
3948 */
@@ -42,6 +51,9 @@ final class TurboProcessRestarter
4251
4352 public const EXTENSION_PATH_INI = 'phpstan.turboExtensionPath ' ;
4453
54+ /** Set by the restart so the restarted process never restarts again */
55+ public const RESTARTED_INI = 'phpstan.restarted ' ;
56+
4557 /** Shared memory reserved (not touched) for the opcode cache, in MB — see resolveOpcacheArgs() for the sizing */
4658 private const OPCACHE_MEMORY_CONSUMPTION_MB_LIMIT = 256 ;
4759
@@ -80,11 +92,10 @@ public static function getRestartExtensionPath(): ?string
8092 */
8193 public static function restartIfSuitable (array $ argv ): void
8294 {
83- if (extension_loaded ('phpstan_turbo ' )) {
84- return ;
85- }
86- if (self ::getRestartExtensionPath () !== null ) {
87- // already restarted and the extension still did not load
95+ if (get_cfg_var (self ::RESTARTED_INI ) !== false ) {
96+ // already restarted — whatever did not take effect (a binary that
97+ // failed to load, an OPcache that could not start) will not on a
98+ // second try either
8899 return ;
89100 }
90101 if (
@@ -100,8 +111,12 @@ public static function restartIfSuitable(array $argv): void
100111 return ;
101112 }
102113
103- $ extensionPath = TurboExtensionSelector ::findExtension ();
104- if ($ extensionPath === null ) {
114+ $ extensionPath = extension_loaded ('phpstan_turbo ' ) ? null : TurboExtensionSelector ::findExtension ();
115+ $ opcacheArgs = self ::getOpcacheArgs ();
116+ if (
117+ $ extensionPath === null
118+ && !self ::resolveOpcacheRestartNeeded ($ opcacheArgs , self ::getCurrentIniValues ($ opcacheArgs ))
119+ ) {
105120 return ;
106121 }
107122
@@ -113,20 +128,82 @@ public static function restartIfSuitable(array $argv): void
113128 }
114129 $ args [] = '-d ' ;
115130 $ args [] = 'memory_limit= ' . ini_get ('memory_limit ' );
116- foreach (self :: getOpcacheArgs () as $ opcacheArg ) {
131+ foreach ($ opcacheArgs as $ opcacheArg ) {
117132 $ args [] = '-d ' ;
118133 $ args [] = $ opcacheArg ;
119134 }
135+ if ($ extensionPath !== null ) {
136+ $ args [] = '-d ' ;
137+ $ args [] = 'extension= ' . $ extensionPath ;
138+ $ args [] = '-d ' ;
139+ $ args [] = self ::EXTENSION_PATH_INI . '= ' . $ extensionPath ;
140+ }
120141 $ args [] = '-d ' ;
121- $ args [] = 'extension= ' . $ extensionPath ;
122- $ args [] = '-d ' ;
123- $ args [] = self ::EXTENSION_PATH_INI . '= ' . $ extensionPath ;
142+ $ args [] = self ::RESTARTED_INI . '=1 ' ;
124143 foreach ($ argv as $ arg ) {
125144 $ args [] = $ arg ;
126145 }
127146
128147 pcntl_exec (PHP_BINARY , $ args );
129- // pcntl_exec() returns only on failure — continue without the extension
148+ // pcntl_exec() returns only on failure — continue as we are
149+ }
150+
151+ /**
152+ * Whether the OPcache configuration the restart would set differs from
153+ * the one in effect — the second reason to restart, independent of turbo:
154+ * with the extension already loaded through php.ini there would be
155+ * nothing else to restart for, and OPcache would stay dormant.
156+ *
157+ * @param list<string> $opcacheArgs `name=value` entries, see resolveOpcacheArgs()
158+ * @param array<string, string|false> $currentIniValues ini_get() of each of those names
159+ */
160+ public static function resolveOpcacheRestartNeeded (array $ opcacheArgs , array $ currentIniValues ): bool
161+ {
162+ foreach ($ opcacheArgs as $ opcacheArg ) {
163+ [$ name , $ value ] = explode ('= ' , $ opcacheArg , 2 );
164+ $ current = $ currentIniValues [$ name ] ?? false ;
165+ if ($ current === false ) {
166+ // unknown directive on this PHP (opcache.jit before 8.0) — nothing a restart could change
167+ continue ;
168+ }
169+ if (self ::normalizeIniValue ($ current ) !== self ::normalizeIniValue ($ value )) {
170+ return true ;
171+ }
172+ }
173+
174+ return false ;
175+ }
176+
177+ /**
178+ * @param list<string> $opcacheArgs
179+ * @return array<string, string|false>
180+ */
181+ private static function getCurrentIniValues (array $ opcacheArgs ): array
182+ {
183+ $ values = [];
184+ foreach ($ opcacheArgs as $ opcacheArg ) {
185+ $ name = explode ('= ' , $ opcacheArg , 2 )[0 ];
186+ $ values [$ name ] = ini_get ($ name );
187+ }
188+
189+ return $ values ;
190+ }
191+
192+ /**
193+ * The ini parser stores booleans as "1" / "" for php.ini and -d values
194+ * alike; ini_set()-style spellings are folded the same way.
195+ */
196+ private static function normalizeIniValue (string $ value ): string
197+ {
198+ $ value = strtolower (trim ($ value ));
199+ if (in_array ($ value , ['' , '0 ' , 'off ' , 'false ' , 'no ' , 'none ' ], true )) {
200+ return '0 ' ;
201+ }
202+ if (in_array ($ value , ['1 ' , 'on ' , 'true ' , 'yes ' ], true )) {
203+ return '1 ' ;
204+ }
205+
206+ return $ value ;
130207 }
131208
132209 /**
0 commit comments