XdebugHandler::doRestart()   B
last analyzed

Complexity

Conditions 6
Paths 24

Size

Total Lines 37
Code Lines 23

Duplication

Lines 0
Ratio 0 %

Importance

Changes 0
Metric Value
eloc 23
dl 0
loc 37
rs 8.9297
c 0
b 0
f 0
cc 6
nc 24
nop 1
1
<?php
2
3
/*
4
 * This file is part of composer/xdebug-handler.
5
 *
6
 * (c) Composer <https://github.com/composer>
7
 *
8
 * For the full copyright and license information, please view
9
 * the LICENSE file that was distributed with this source code.
10
 */
11
12
declare(strict_types=1);
13
14
namespace Composer\XdebugHandler;
15
16
use Composer\Pcre\Preg;
17
use Psr\Log\LoggerInterface;
18
19
/**
20
 * @author John Stevenson <[email protected]>
21
 *
22
 * @phpstan-import-type restartData from PhpConfig
23
 */
24
class XdebugHandler
25
{
26
    const SUFFIX_ALLOW = '_ALLOW_XDEBUG';
27
    const SUFFIX_INIS = '_ORIGINAL_INIS';
28
    const RESTART_ID = 'internal';
29
    const RESTART_SETTINGS = 'XDEBUG_HANDLER_SETTINGS';
30
    const DEBUG = 'XDEBUG_HANDLER_DEBUG';
31
32
    /** @var string|null */
33
    protected $tmpIni;
34
35
    /** @var bool */
36
    private static $inRestart;
37
38
    /** @var string */
39
    private static $name;
40
41
    /** @var string|null */
42
    private static $skipped;
43
44
    /** @var bool */
45
    private static $xdebugActive;
46
47
    /** @var string|null */
48
    private static $xdebugMode;
49
50
    /** @var string|null */
51
    private static $xdebugVersion;
52
53
    /** @var bool */
54
    private $cli;
55
56
    /** @var string|null */
57
    private $debug;
58
59
    /** @var string */
60
    private $envAllowXdebug;
61
62
    /** @var string */
63
    private $envOriginalInis;
64
65
    /** @var bool */
66
    private $persistent;
67
68
    /** @var string|null */
69
    private $script;
70
71
    /** @var Status */
72
    private $statusWriter;
73
74
    /**
75
     * Constructor
76
     *
77
     * The $envPrefix is used to create distinct environment variables. It is
78
     * uppercased and prepended to the default base values. For example 'myapp'
79
     * would result in MYAPP_ALLOW_XDEBUG and MYAPP_ORIGINAL_INIS.
80
     *
81
     * @param string $envPrefix Value used in environment variables
82
     * @throws \RuntimeException If the parameter is invalid
83
     */
84
    public function __construct(string $envPrefix)
85
    {
86
        if ($envPrefix === '') {
87
            throw new \RuntimeException('Invalid constructor parameter');
88
        }
89
90
        self::$name = strtoupper($envPrefix);
91
        $this->envAllowXdebug = self::$name.self::SUFFIX_ALLOW;
92
        $this->envOriginalInis = self::$name.self::SUFFIX_INIS;
93
94
        self::setXdebugDetails();
95
        self::$inRestart = false;
96
97
        if ($this->cli = PHP_SAPI === 'cli') {
98
            $this->debug = (string) getenv(self::DEBUG);
99
        }
100
101
        $this->statusWriter = new Status($this->envAllowXdebug, (bool) $this->debug);
102
    }
103
104
    /**
105
     * Activates status message output to a PSR3 logger
106
     */
107
    public function setLogger(LoggerInterface $logger): self
108
    {
109
        $this->statusWriter->setLogger($logger);
110
        return $this;
111
    }
112
113
    /**
114
     * Sets the main script location if it cannot be called from argv
115
     */
116
    public function setMainScript(string $script): self
117
    {
118
        $this->script = $script;
119
        return $this;
120
    }
121
122
    /**
123
     * Persist the settings to keep Xdebug out of sub-processes
124
     */
125
    public function setPersistent(): self
126
    {
127
        $this->persistent = true;
128
        return $this;
129
    }
130
131
    /**
132
     * Checks if Xdebug is loaded and the process needs to be restarted
133
     *
134
     * This behaviour can be disabled by setting the MYAPP_ALLOW_XDEBUG
135
     * environment variable to 1. This variable is used internally so that
136
     * the restarted process is created only once.
137
     */
138
    public function check(): void
139
    {
140
        $this->notify(Status::CHECK, self::$xdebugVersion.'|'.self::$xdebugMode);
141
        $envArgs = explode('|', (string) getenv($this->envAllowXdebug));
142
143
        if (!((bool) $envArgs[0]) && $this->requiresRestart(self::$xdebugActive)) {
144
            // Restart required
145
            $this->notify(Status::RESTART);
146
            $command = $this->prepareRestart();
147
148
            if ($command !== null) {
149
                $this->restart($command);
150
            }
151
            return;
152
        }
153
154
        if (self::RESTART_ID === $envArgs[0] && count($envArgs) === 5) {
155
            // Restarted, so unset environment variable and use saved values
156
            $this->notify(Status::RESTARTED);
157
158
            Process::setEnv($this->envAllowXdebug);
159
            self::$inRestart = true;
160
161
            if (self::$xdebugVersion === null) {
162
                // Skipped version is only set if Xdebug is not loaded
163
                self::$skipped = $envArgs[1];
164
            }
165
166
            $this->tryEnableSignals();
167
168
            // Put restart settings in the environment
169
            $this->setEnvRestartSettings($envArgs);
170
            return;
171
        }
172
173
        $this->notify(Status::NORESTART);
174
        $settings = self::getRestartSettings();
175
176
        if ($settings !== null) {
177
            // Called with existing settings, so sync our settings
178
            $this->syncSettings($settings);
179
        }
180
    }
181
182
    /**
183
     * Returns an array of php.ini locations with at least one entry
184
     *
185
     * The equivalent of calling php_ini_loaded_file then php_ini_scanned_files.
186
     * The loaded ini location is the first entry and may be an empty string.
187
     *
188
     * @return non-empty-list<string>
0 ignored issues
show
Documentation Bug introduced by
The doc comment non-empty-list<string> at position 0 could not be parsed: Unknown type name 'non-empty-list' at position 0 in non-empty-list<string>.
Loading history...
189
     */
190
    public static function getAllIniFiles(): array
191
    {
192
        if (self::$name !== null) {
0 ignored issues
show
introduced by
The condition self::name !== null is always true.
Loading history...
193
            $env = getenv(self::$name.self::SUFFIX_INIS);
194
195
            if (false !== $env) {
196
                return explode(PATH_SEPARATOR, $env);
197
            }
198
        }
199
200
        $paths = [(string) php_ini_loaded_file()];
201
        $scanned = php_ini_scanned_files();
202
203
        if ($scanned !== false) {
204
            $paths = array_merge($paths, array_map('trim', explode(',', $scanned)));
205
        }
206
207
        return $paths;
208
    }
209
210
    /**
211
     * Returns an array of restart settings or null
212
     *
213
     * Settings will be available if the current process was restarted, or
214
     * called with the settings from an existing restart.
215
     *
216
     * @phpstan-return restartData|null
217
     */
218
    public static function getRestartSettings(): ?array
219
    {
220
        $envArgs = explode('|', (string) getenv(self::RESTART_SETTINGS));
221
222
        if (count($envArgs) !== 6
223
            || (!self::$inRestart && php_ini_loaded_file() !== $envArgs[0])) {
224
            return null;
225
        }
226
227
        return [
228
            'tmpIni' => $envArgs[0],
229
            'scannedInis' => (bool) $envArgs[1],
230
            'scanDir' => '*' === $envArgs[2] ? false : $envArgs[2],
231
            'phprc' => '*' === $envArgs[3] ? false : $envArgs[3],
232
            'inis' => explode(PATH_SEPARATOR, $envArgs[4]),
233
            'skipped' => $envArgs[5],
234
        ];
235
    }
236
237
    /**
238
     * Returns the Xdebug version that triggered a successful restart
239
     */
240
    public static function getSkippedVersion(): string
241
    {
242
        return (string) self::$skipped;
243
    }
244
245
    /**
246
     * Returns whether Xdebug is loaded and active
247
     *
248
     * true: if Xdebug is loaded and is running in an active mode.
249
     * false: if Xdebug is not loaded, or it is running with xdebug.mode=off.
250
     */
251
    public static function isXdebugActive(): bool
252
    {
253
        self::setXdebugDetails();
254
        return self::$xdebugActive;
255
    }
256
257
    /**
258
     * Allows an extending class to decide if there should be a restart
259
     *
260
     * The default is to restart if Xdebug is loaded and its mode is not "off".
261
     */
262
    protected function requiresRestart(bool $default): bool
263
    {
264
        return $default;
265
    }
266
267
    /**
268
     * Allows an extending class to access the tmpIni
269
     *
270
     * @param non-empty-list<string> $command
0 ignored issues
show
Documentation Bug introduced by
The doc comment non-empty-list<string> at position 0 could not be parsed: Unknown type name 'non-empty-list' at position 0 in non-empty-list<string>.
Loading history...
271
     */
272
    protected function restart(array $command): void
273
    {
274
        $this->doRestart($command);
275
    }
276
277
    /**
278
     * Executes the restarted command then deletes the tmp ini
279
     *
280
     * @param non-empty-list<string> $command
0 ignored issues
show
Documentation Bug introduced by
The doc comment non-empty-list<string> at position 0 could not be parsed: Unknown type name 'non-empty-list' at position 0 in non-empty-list<string>.
Loading history...
281
     * @phpstan-return never
282
     */
283
    private function doRestart(array $command): void
284
    {
285
        if (PHP_VERSION_ID >= 70400) {
286
            $cmd = $command;
287
            $displayCmd = sprintf('[%s]', implode(', ', $cmd));
288
        } else {
289
            $cmd = Process::escapeShellCommand($command);
290
            if (defined('PHP_WINDOWS_VERSION_BUILD')) {
291
                // Outer quotes required on cmd string below PHP 8
292
                $cmd = '"'.$cmd.'"';
293
            }
294
            $displayCmd = $cmd;
295
        }
296
297
        $this->tryEnableSignals();
298
        $this->notify(Status::RESTARTING, $displayCmd);
299
300
        $process = proc_open($cmd, [], $pipes);
301
        if (is_resource($process)) {
302
            $exitCode = proc_close($process);
303
        }
304
305
        if (!isset($exitCode)) {
306
            // Unlikely that php or the default shell cannot be invoked
307
            $this->notify(Status::ERROR, 'Unable to restart process');
308
            $exitCode = -1;
309
        } else {
310
            $this->notify(Status::INFO, 'Restarted process exited '.$exitCode);
311
        }
312
313
        if ($this->debug === '2') {
314
            $this->notify(Status::INFO, 'Temp ini saved: '.$this->tmpIni);
315
        } else {
316
            @unlink((string) $this->tmpIni);
0 ignored issues
show
Security Best Practice introduced by
It seems like you do not handle an error condition for unlink(). This can introduce security issues, and is generally not recommended. ( Ignorable by Annotation )

If this is a false-positive, you can also ignore this issue in your code via the ignore-unhandled  annotation

316
            /** @scrutinizer ignore-unhandled */ @unlink((string) $this->tmpIni);

If you suppress an error, we recommend checking for the error condition explicitly:

// For example instead of
@mkdir($dir);

// Better use
if (@mkdir($dir) === false) {
    throw new \RuntimeException('The directory '.$dir.' could not be created.');
}
Loading history...
317
        }
318
319
        exit($exitCode);
0 ignored issues
show
Best Practice introduced by
Using exit here is not recommended.

In general, usage of exit should be done with care and only when running in a scripting context like a CLI script.

Loading history...
Comprehensibility Best Practice introduced by
The variable $exitCode does not seem to be defined for all execution paths leading up to this point.
Loading history...
320
    }
321
322
    /**
323
     * Returns the command line array if everything was written for the restart
324
     *
325
     * If any of the following fails (however unlikely) we must return false to
326
     * stop potential recursion:
327
     *   - tmp ini file creation
328
     *   - environment variable creation
329
     *
330
     * @return non-empty-list<string>|null
0 ignored issues
show
Documentation Bug introduced by
The doc comment non-empty-list<string>|null at position 0 could not be parsed: Unknown type name 'non-empty-list' at position 0 in non-empty-list<string>|null.
Loading history...
331
     */
332
    private function prepareRestart(): ?array
333
    {
334
        if (!$this->cli) {
335
            $this->notify(Status::ERROR, 'Unsupported SAPI: '.PHP_SAPI);
336
            return null;
337
        }
338
339
        if (($argv = $this->checkServerArgv()) === null) {
340
            $this->notify(Status::ERROR, '$_SERVER[argv] is not as expected');
341
            return null;
342
        }
343
344
        if (!$this->checkConfiguration($info)) {
345
            $this->notify(Status::ERROR, $info);
346
            return null;
347
        }
348
349
        $mainScript = (string) $this->script;
350
        if (!$this->checkMainScript($mainScript, $argv)) {
351
            $this->notify(Status::ERROR, 'Unable to access main script: '.$mainScript);
352
            return null;
353
        }
354
355
        $tmpDir = sys_get_temp_dir();
356
        $iniError = 'Unable to create temp ini file at: '.$tmpDir;
357
358
        if (($tmpfile = @tempnam($tmpDir, '')) === false) {
359
            $this->notify(Status::ERROR, $iniError);
360
            return null;
361
        }
362
363
        $error = null;
364
        $iniFiles = self::getAllIniFiles();
365
        $scannedInis = count($iniFiles) > 1;
366
367
        if (!$this->writeTmpIni($tmpfile, $iniFiles, $error)) {
368
            $this->notify(Status::ERROR, $error ?? $iniError);
369
            @unlink($tmpfile);
0 ignored issues
show
Security Best Practice introduced by
It seems like you do not handle an error condition for unlink(). This can introduce security issues, and is generally not recommended. ( Ignorable by Annotation )

If this is a false-positive, you can also ignore this issue in your code via the ignore-unhandled  annotation

369
            /** @scrutinizer ignore-unhandled */ @unlink($tmpfile);

If you suppress an error, we recommend checking for the error condition explicitly:

// For example instead of
@mkdir($dir);

// Better use
if (@mkdir($dir) === false) {
    throw new \RuntimeException('The directory '.$dir.' could not be created.');
}
Loading history...
370
            return null;
371
        }
372
373
        if (!$this->setEnvironment($scannedInis, $iniFiles, $tmpfile)) {
374
            $this->notify(Status::ERROR, 'Unable to set environment variables');
375
            @unlink($tmpfile);
376
            return null;
377
        }
378
379
        $this->tmpIni = $tmpfile;
380
381
        return $this->getCommand($argv, $tmpfile, $mainScript);
382
    }
383
384
    /**
385
     * Returns true if the tmp ini file was written
386
     *
387
     * @param non-empty-list<string> $iniFiles All ini files used in the current process
0 ignored issues
show
Documentation Bug introduced by
The doc comment non-empty-list<string> at position 0 could not be parsed: Unknown type name 'non-empty-list' at position 0 in non-empty-list<string>.
Loading history...
388
     */
389
    private function writeTmpIni(string $tmpFile, array $iniFiles, ?string &$error): bool
390
    {
391
        // $iniFiles has at least one item and it may be empty
392
        if ($iniFiles[0] === '') {
393
            array_shift($iniFiles);
394
        }
395
396
        $content = '';
397
        $sectionRegex = '/^\s*\[(?:PATH|HOST)\s*=/mi';
398
        $xdebugRegex = '/^\s*(zend_extension\s*=.*xdebug.*)$/mi';
399
400
        foreach ($iniFiles as $file) {
401
            // Check for inaccessible ini files
402
            if (($data = @file_get_contents($file)) === false) {
403
                $error = 'Unable to read ini: '.$file;
404
                return false;
405
            }
406
            // Check and remove directives after HOST and PATH sections
407
            if (Preg::isMatchWithOffsets($sectionRegex, $data, $matches)) {
408
                $data = substr($data, 0, $matches[0][1]);
409
            }
410
            $content .= Preg::replace($xdebugRegex, ';$1', $data).PHP_EOL;
411
        }
412
413
        // Merge loaded settings into our ini content, if it is valid
414
        $config = parse_ini_string($content);
415
        $loaded = ini_get_all(null, false);
416
417
        if (false === $config || false === $loaded) {
418
            $error = 'Unable to parse ini data';
419
            return false;
420
        }
421
422
        $content .= $this->mergeLoadedConfig($loaded, $config);
423
424
        // Work-around for https://bugs.php.net/bug.php?id=75932
425
        $content .= 'opcache.enable_cli=0'.PHP_EOL;
426
427
        return (bool) @file_put_contents($tmpFile, $content);
428
    }
429
430
    /**
431
     * Returns the command line arguments for the restart
432
     *
433
     * @param non-empty-list<string> $argv
0 ignored issues
show
Documentation Bug introduced by
The doc comment non-empty-list<string> at position 0 could not be parsed: Unknown type name 'non-empty-list' at position 0 in non-empty-list<string>.
Loading history...
434
     * @return non-empty-list<string>
0 ignored issues
show
Documentation Bug introduced by
The doc comment non-empty-list<string> at position 0 could not be parsed: Unknown type name 'non-empty-list' at position 0 in non-empty-list<string>.
Loading history...
435
     */
436
    private function getCommand(array $argv, string $tmpIni, string $mainScript): array
437
    {
438
        $php = [PHP_BINARY];
439
        $args = array_slice($argv, 1);
440
441
        if (!$this->persistent) {
442
            // Use command-line options
443
            array_push($php, '-n', '-c', $tmpIni);
444
        }
445
446
        return array_merge($php, [$mainScript], $args);
447
    }
448
449
    /**
450
     * Returns true if the restart environment variables were set
451
     *
452
     * No need to update $_SERVER since this is set in the restarted process.
453
     *
454
     * @param non-empty-list<string> $iniFiles All ini files used in the current process
0 ignored issues
show
Documentation Bug introduced by
The doc comment non-empty-list<string> at position 0 could not be parsed: Unknown type name 'non-empty-list' at position 0 in non-empty-list<string>.
Loading history...
455
     */
456
    private function setEnvironment(bool $scannedInis, array $iniFiles, string $tmpIni): bool
457
    {
458
        $scanDir = getenv('PHP_INI_SCAN_DIR');
459
        $phprc = getenv('PHPRC');
460
461
        // Make original inis available to restarted process
462
        if (!putenv($this->envOriginalInis.'='.implode(PATH_SEPARATOR, $iniFiles))) {
463
            return false;
464
        }
465
466
        if ($this->persistent) {
467
            // Use the environment to persist the settings
468
            if (!putenv('PHP_INI_SCAN_DIR=') || !putenv('PHPRC='.$tmpIni)) {
469
                return false;
470
            }
471
        }
472
473
        // Flag restarted process and save values for it to use
474
        $envArgs = [
475
            self::RESTART_ID,
476
            self::$xdebugVersion,
477
            (int) $scannedInis,
478
            false === $scanDir ? '*' : $scanDir,
479
            false === $phprc ? '*' : $phprc,
480
        ];
481
482
        return putenv($this->envAllowXdebug.'='.implode('|', $envArgs));
483
    }
484
485
    /**
486
     * Logs status messages
487
     */
488
    private function notify(string $op, ?string $data = null): void
489
    {
490
        $this->statusWriter->report($op, $data);
491
    }
492
493
    /**
494
     * Returns default, changed and command-line ini settings
495
     *
496
     * @param mixed[] $loadedConfig All current ini settings
497
     * @param mixed[] $iniConfig Settings from user ini files
498
     *
499
     */
500
    private function mergeLoadedConfig(array $loadedConfig, array $iniConfig): string
501
    {
502
        $content = '';
503
504
        foreach ($loadedConfig as $name => $value) {
505
            // Value will either be null, string or array (HHVM only)
506
            if (!is_string($value)
507
                || strpos($name, 'xdebug') === 0
508
                || $name === 'apc.mmap_file_mask') {
509
                continue;
510
            }
511
512
            if (!isset($iniConfig[$name]) || $iniConfig[$name] !== $value) {
513
                // Double-quote escape each value
514
                $content .= $name.'="'.addcslashes($value, '\\"').'"'.PHP_EOL;
515
            }
516
        }
517
518
        return $content;
519
    }
520
521
    /**
522
     * Returns true if the script name can be used
523
     *
524
     * @param non-empty-list<string> $argv
0 ignored issues
show
Documentation Bug introduced by
The doc comment non-empty-list<string> at position 0 could not be parsed: Unknown type name 'non-empty-list' at position 0 in non-empty-list<string>.
Loading history...
525
     */
526
    private function checkMainScript(string &$mainScript, array $argv): bool
527
    {
528
        if ($mainScript !== '') {
529
            // Allow an application to set -- for standard input
530
            return file_exists($mainScript) || '--' === $mainScript;
531
        }
532
533
        if (file_exists($mainScript = $argv[0])) {
534
            return true;
535
        }
536
537
        // Use a backtrace to resolve Phar and chdir issues.
538
        $trace = debug_backtrace(DEBUG_BACKTRACE_IGNORE_ARGS);
539
        $main = end($trace);
540
541
        if ($main !== false && isset($main['file'])) {
542
            return file_exists($mainScript = $main['file']);
543
        }
544
545
        return false;
546
    }
547
548
    /**
549
     * Adds restart settings to the environment
550
     *
551
     * @param non-empty-list<string> $envArgs
0 ignored issues
show
Documentation Bug introduced by
The doc comment non-empty-list<string> at position 0 could not be parsed: Unknown type name 'non-empty-list' at position 0 in non-empty-list<string>.
Loading history...
552
     */
553
    private function setEnvRestartSettings(array $envArgs): void
554
    {
555
        $settings = [
556
            php_ini_loaded_file(),
557
            $envArgs[2],
558
            $envArgs[3],
559
            $envArgs[4],
560
            getenv($this->envOriginalInis),
561
            self::$skipped,
562
        ];
563
564
        Process::setEnv(self::RESTART_SETTINGS, implode('|', $settings));
565
    }
566
567
    /**
568
     * Syncs settings and the environment if called with existing settings
569
     *
570
     * @phpstan-param restartData $settings
571
     */
572
    private function syncSettings(array $settings): void
573
    {
574
        if (false === getenv($this->envOriginalInis)) {
575
            // Called by another app, so make original inis available
576
            Process::setEnv($this->envOriginalInis, implode(PATH_SEPARATOR, $settings['inis']));
577
        }
578
579
        self::$skipped = $settings['skipped'];
580
        $this->notify(Status::INFO, 'Process called with existing restart settings');
581
    }
582
583
    /**
584
     * Returns true if there are no known configuration issues
585
     */
586
    private function checkConfiguration(?string &$info): bool
587
    {
588
        if (!function_exists('proc_open')) {
589
            $info = 'proc_open function is disabled';
590
            return false;
591
        }
592
593
        if (!file_exists(PHP_BINARY)) {
594
            $info = 'PHP_BINARY is not available';
595
            return false;
596
        }
597
598
        if (extension_loaded('uopz') && !((bool) ini_get('uopz.disable'))) {
599
            // uopz works at opcode level and disables exit calls
600
            if (function_exists('uopz_allow_exit')) {
601
                @uopz_allow_exit(true);
0 ignored issues
show
Security Best Practice introduced by
It seems like you do not handle an error condition for uopz_allow_exit(). This can introduce security issues, and is generally not recommended. ( Ignorable by Annotation )

If this is a false-positive, you can also ignore this issue in your code via the ignore-unhandled  annotation

601
                /** @scrutinizer ignore-unhandled */ @uopz_allow_exit(true);

If you suppress an error, we recommend checking for the error condition explicitly:

// For example instead of
@mkdir($dir);

// Better use
if (@mkdir($dir) === false) {
    throw new \RuntimeException('The directory '.$dir.' could not be created.');
}
Loading history...
Bug introduced by
Are you sure the usage of uopz_allow_exit(true) is correct as it seems to always return null.

This check looks for function or method calls that always return null and whose return value is used.

class A
{
    function getObject()
    {
        return null;
    }

}

$a = new A();
if ($a->getObject()) {

The method getObject() can return nothing but null, so it makes no sense to use the return value.

The reason is most likely that a function or method is imcomplete or has been reduced for debug purposes.

Loading history...
602
            } else {
603
                $info = 'uopz extension is not compatible';
604
                return false;
605
            }
606
        }
607
608
        // Check UNC paths when using cmd.exe
609
        if (defined('PHP_WINDOWS_VERSION_BUILD') && PHP_VERSION_ID < 70400) {
610
            $workingDir = getcwd();
611
612
            if ($workingDir === false) {
613
                $info = 'unable to determine working directory';
614
                return false;
615
            }
616
617
            if (0 === strpos($workingDir, '\\\\')) {
618
                $info = 'cmd.exe does not support UNC paths: '.$workingDir;
619
                return false;
620
            }
621
        }
622
623
        return true;
624
    }
625
626
    /**
627
     * Enables async signals and control interrupts in the restarted process
628
     *
629
     * Available on Unix PHP 7.1+ with the pcntl extension and Windows PHP 7.4+.
630
     */
631
    private function tryEnableSignals(): void
632
    {
633
        if (function_exists('pcntl_async_signals') && function_exists('pcntl_signal')) {
634
            pcntl_async_signals(true);
635
            $message = 'Async signals enabled';
0 ignored issues
show
Unused Code introduced by
The assignment to $message is dead and can be removed.
Loading history...
636
637
            if (!self::$inRestart) {
638
                // Restarting, so ignore SIGINT in parent
639
                pcntl_signal(SIGINT, SIG_IGN);
640
            } elseif (is_int(pcntl_signal_get_handler(SIGINT))) {
0 ignored issues
show
introduced by
The condition is_int(pcntl_signal_get_...\XdebugHandler\SIGINT)) is always false.
Loading history...
641
                // Restarted, no handler set so force default action
642
                pcntl_signal(SIGINT, SIG_DFL);
643
            }
644
        }
645
646
        if (!self::$inRestart && function_exists('sapi_windows_set_ctrl_handler')) {
647
            // Restarting, so set a handler to ignore CTRL events in the parent.
648
            // This ensures that CTRL+C events will be available in the child
649
            // process without having to enable them there, which is unreliable.
650
            sapi_windows_set_ctrl_handler(function ($evt) {});
0 ignored issues
show
Unused Code introduced by
The parameter $evt is not used and could be removed. ( Ignorable by Annotation )

If this is a false-positive, you can also ignore this issue in your code via the ignore-unused  annotation

650
            sapi_windows_set_ctrl_handler(function (/** @scrutinizer ignore-unused */ $evt) {});

This check looks for parameters that have been defined for a function or method, but which are not used in the method body.

Loading history...
651
        }
652
    }
653
654
    /**
655
     * Returns $_SERVER['argv'] if it is as expected
656
     *
657
     * @return non-empty-list<string>|null
0 ignored issues
show
Documentation Bug introduced by
The doc comment non-empty-list<string>|null at position 0 could not be parsed: Unknown type name 'non-empty-list' at position 0 in non-empty-list<string>|null.
Loading history...
658
     */
659
    private function checkServerArgv(): ?array
660
    {
661
        $result = [];
662
663
        if (isset($_SERVER['argv']) && is_array($_SERVER['argv'])) {
664
            foreach ($_SERVER['argv'] as $value) {
665
                if (!is_string($value)) {
666
                    return null;
667
                }
668
669
                $result[] = $value;
670
            }
671
        }
672
673
        return count($result) > 0 ? $result : null;
674
    }
675
676
    /**
677
     * Sets static properties $xdebugActive, $xdebugVersion and $xdebugMode
678
     */
679
    private static function setXdebugDetails(): void
680
    {
681
        if (self::$xdebugActive !== null) {
0 ignored issues
show
introduced by
The condition self::xdebugActive !== null is always true.
Loading history...
682
            return;
683
        }
684
685
        self::$xdebugActive = false;
686
        if (!extension_loaded('xdebug')) {
687
            return;
688
        }
689
690
        $version = phpversion('xdebug');
691
        self::$xdebugVersion = $version !== false ? $version : 'unknown';
692
693
        if (version_compare(self::$xdebugVersion, '3.1', '>=')) {
694
            $modes = xdebug_info('mode');
0 ignored issues
show
Unused Code introduced by
The call to xdebug_info() has too many arguments starting with 'mode'. ( Ignorable by Annotation )

If this is a false-positive, you can also ignore this issue in your code via the ignore-call  annotation

694
            $modes = /** @scrutinizer ignore-call */ xdebug_info('mode');

This check compares calls to functions or methods with their respective definitions. If the call has more arguments than are defined, it raises an issue.

If a function is defined several times with a different number of parameters, the check may pick up the wrong definition and report false positives. One codebase where this has been known to happen is Wordpress. Please note the @ignore annotation hint above.

Loading history...
695
            self::$xdebugMode = count($modes) === 0 ? 'off' : implode(',', $modes);
696
            self::$xdebugActive = self::$xdebugMode !== 'off';
697
            return;
698
        }
699
700
        // See if xdebug.mode is supported in this version
701
        $iniMode = ini_get('xdebug.mode');
702
        if ($iniMode === false) {
703
            self::$xdebugActive = true;
704
            return;
705
        }
706
707
        // Environment value wins but cannot be empty
708
        $envMode = (string) getenv('XDEBUG_MODE');
709
        if ($envMode !== '') {
710
            self::$xdebugMode = $envMode;
711
        } else {
712
            self::$xdebugMode = $iniMode !== '' ? $iniMode : 'off';
713
        }
714
715
        // An empty comma-separated list is treated as mode 'off'
716
        if (Preg::isMatch('/^,+$/', str_replace(' ', '', self::$xdebugMode))) {
717
            self::$xdebugMode = 'off';
718
        }
719
720
        self::$xdebugActive = self::$xdebugMode !== 'off';
721
    }
722
}
723