Skip to content
New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

Automatically detect the PHP version of the target (WIP) #139

Closed
wants to merge 18 commits into from
Closed
10 changes: 10 additions & 0 deletions src/Command/Inspector/GetTraceCommand.php
Original file line number Diff line number Diff line change
Expand Up @@ -27,6 +27,7 @@
use PhpProfiler\Lib\Elf\Process\ProcessSymbolReaderException;
use PhpProfiler\Lib\Elf\Tls\TlsFinderException;
use PhpProfiler\Lib\PhpProcessReader\PhpGlobalsFinder;
use PhpProfiler\Lib\PhpProcessReader\PhpVersionDetector;
use PhpProfiler\Lib\Process\MemoryReader\MemoryReaderException;
use PhpProfiler\Lib\PhpProcessReader\PhpMemoryReader\ExecutorGlobalsReader;
use PhpProfiler\Lib\Process\ProcessStopper\ProcessStopper;
Expand All @@ -51,6 +52,7 @@ public function __construct(
private ProcessStopper $process_stopper,
private TargetProcessResolver $target_process_resolver,
private RetryingLoopProvider $retrying_loop_provider,
private PhpVersionDetector $php_version_detector,
) {
parent::__construct();
}
Expand Down Expand Up @@ -85,6 +87,14 @@ public function execute(InputInterface $input, OutputInterface $output): int

$process_specifier = $this->target_process_resolver->resolve($target_process_settings);

$version = $this->php_version_detector->tryDetection(
$process_specifier,
$target_php_settings
);
if (!is_null($version)) {
$target_php_settings = $target_php_settings->alterPhpVersion($version);
}

// On targeting ZTS, it's possible that libpthread.so of the target process isn't yet loaded
// at this point. In that case the TLS block can't be located, then the address of EG can't
// be found also. So simply retrying the whole process of finding EG here.
Expand Down
9 changes: 9 additions & 0 deletions src/Inspector/Daemon/Reader/Worker/PhpReaderTraceLoop.php
Original file line number Diff line number Diff line change
Expand Up @@ -20,6 +20,7 @@
use PhpProfiler\Inspector\Settings\TraceLoopSettings\TraceLoopSettings;
use PhpProfiler\Lib\PhpProcessReader\PhpGlobalsFinder;
use PhpProfiler\Lib\PhpProcessReader\PhpMemoryReader\ExecutorGlobalsReader;
use PhpProfiler\Lib\PhpProcessReader\PhpVersionDetector;
use PhpProfiler\Lib\Process\ProcessSpecifier;
use PhpProfiler\Lib\Process\ProcessStopper\ProcessStopper;

Expand All @@ -33,6 +34,7 @@ public function __construct(
private ExecutorGlobalsReader $executor_globals_reader,
private ReaderLoopProvider $reader_loop_provider,
private ProcessStopper $process_stopper,
private PhpVersionDetector $php_version_detector,
) {
}

Expand All @@ -49,6 +51,13 @@ public function run(
TargetPhpSettings $target_php_settings,
GetTraceSettings $get_trace_settings
): Generator {
$version = $this->php_version_detector->tryDetection(
$process_specifier,
$target_php_settings
);
if (!is_null($version)) {
$target_php_settings = $target_php_settings->alterPhpVersion($version);
}
$eg_address = $this->php_globals_finder->findExecutorGlobals($process_specifier, $target_php_settings);

$loop = $this->reader_loop_provider->getMainLoop(
Expand Down
34 changes: 30 additions & 4 deletions src/Inspector/Settings/TargetPhpSettings/TargetPhpSettings.php
Original file line number Diff line number Diff line change
Expand Up @@ -15,6 +15,7 @@

use PhpProfiler\Lib\PhpInternals\ZendTypeReader;

/** @psalm-immutable */
final class TargetPhpSettings
{
public const PHP_REGEX_DEFAULT = '.*/(php(74|7.4|80|8.0)?|php-fpm|libphp[78]?.*\.so)$';
Expand All @@ -23,13 +24,38 @@ final class TargetPhpSettings

/** @param value-of<ZendTypeReader::ALL_SUPPORTED_VERSIONS> $php_version */
public function __construct(
public string $php_regex = self::PHP_REGEX_DEFAULT,
public string $libpthread_regex = self::LIBPTHREAD_REGEX_DEFAULT,
private string $php_regex = self::PHP_REGEX_DEFAULT,
private string $libpthread_regex = self::LIBPTHREAD_REGEX_DEFAULT,
public string $php_version = self::TARGET_PHP_VERSION_DEFAULT,
public ?string $php_path = null,
public ?string $libpthread_path = null
) {
$this->php_regex = '{' . $php_regex . '}';
$this->libpthread_regex = '{' . $libpthread_regex . '}';
}

public function getDelimitedPhpRegex(): string
{
return $this->getDelimitedRegex($this->php_regex);
}

public function getDelimitedLibPthreadRegex(): string
{
return $this->getDelimitedRegex($this->libpthread_regex);
}

private function getDelimitedRegex(string $regex): string
{
return '{' . $regex . '}';
}

/** @param value-of<ZendTypeReader::ALL_SUPPORTED_VERSIONS> $php_version */
public function alterPhpVersion(string $php_version): self
{
return new self(
php_regex: $this->php_regex,
libpthread_regex: $this->libpthread_regex,
php_version: $php_version,
php_path: $this->php_path,
libpthread_path: $this->libpthread_path,
);
}
}
4 changes: 2 additions & 2 deletions src/Lib/PhpProcessReader/PhpGlobalsFinder.php
Original file line number Diff line number Diff line change
Expand Up @@ -69,8 +69,8 @@ public function getSymbolReader(
): ProcessSymbolReaderInterface {
return $this->php_symbol_reader_creator->create(
$process_specifier->pid,
$target_php_settings->php_regex,
$target_php_settings->libpthread_regex,
$target_php_settings->getDelimitedPhpRegex(),
$target_php_settings->getDelimitedLibPthreadRegex(),
$target_php_settings->php_path,
$target_php_settings->libpthread_path
);
Expand Down
90 changes: 90 additions & 0 deletions src/Lib/PhpProcessReader/PhpVersionDetector.php
Original file line number Diff line number Diff line change
@@ -0,0 +1,90 @@
<?php

/**
* This file is part of the sj-i/php-profiler package.
*
* (c) sji <sji@sj-i.dev>
*
* For the full copyright and license information, please view the LICENSE
* file that was distributed with this source code.
*/

declare(strict_types=1);

namespace PhpProfiler\Lib\PhpProcessReader;

use FFI\CPointer;
use PhpProfiler\Inspector\Settings\TargetPhpSettings\TargetPhpSettings;
use PhpProfiler\Lib\PhpInternals\ZendTypeCData;
use PhpProfiler\Lib\PhpInternals\ZendTypeReader;
use PhpProfiler\Lib\PhpInternals\ZendTypeReaderCreator;
use PhpProfiler\Lib\Process\MemoryReader\MemoryReaderInterface;
use PhpProfiler\Lib\Process\ProcessSpecifier;

final class PhpVersionDetector
{
private const VERSION_STRING_CONVERTOR = [
'7.0' => ZendTypeReader::V70,
'7.1' => ZendTypeReader::V71,
'7.2' => ZendTypeReader::V72,
'7.3' => ZendTypeReader::V73,
'7.4' => ZendTypeReader::V74,
'8.0' => ZendTypeReader::V80,
'8.1' => ZendTypeReader::V81,
];


public function __construct(
private PhpSymbolReaderCreator $php_symbol_reader_creator,
private ZendTypeReaderCreator $zend_type_reader_creator,
private MemoryReaderInterface $memory_reader,
) {
}

/** @return null|value-of<ZendTypeReader::ALL_SUPPORTED_VERSIONS> */
public function tryDetection(
ProcessSpecifier $process_specifier,
TargetPhpSettings $target_php_settings,
): ?string {
$try_lists = [
[$target_php_settings->getDelimitedPhpRegex(), $target_php_settings->php_path, 'basic_functions_module'],
['{/opcache.so}', null, 'accel_module_entry'],
['{/xml.so}', null, 'xml_module_entry'],
];
foreach ($try_lists as [$module_regex, $module_path, $module_entry]) {
try {
$php_symbol_reader = $this->php_symbol_reader_creator->create(
$process_specifier->pid,
$module_regex,
$target_php_settings->getDelimitedLibPthreadRegex(),
$module_path,
$target_php_settings->libpthread_path,
);
$basic_functions_module = $php_symbol_reader->read($module_entry)
?? throw new \Exception();

// use default version for reading the definition of zend_module_entry
$zend_type_reader = $this->zend_type_reader_creator->create(
$target_php_settings->php_version
);

/** @var ZendTypeCData<\FFI\PhpInternals\zend_module_entry> $module_entry */
$module_entry = $zend_type_reader->readAs('zend_module_entry', $basic_functions_module);
/** @var CPointer $version_string_pointer */
$version_string_pointer = \FFI::cast('long', $module_entry->typed->version)
?? throw new \Exception();
$version_string_cdata = $this->memory_reader->read(
$process_specifier->pid,
$version_string_pointer->cdata,
3
);

$php_version = \FFI::string($version_string_cdata, 3);
return self::VERSION_STRING_CONVERTOR[$php_version] ?? null;
} catch (\Throwable $e) {
continue;
}
}
return null;
}
}
Original file line number Diff line number Diff line change
Expand Up @@ -30,8 +30,8 @@ public function testFromConsoleInput(): void

$settings = (new TargetPhpSettingsFromConsoleInput())->createSettings($input);

$this->assertSame('{abc}', $settings->php_regex);
$this->assertSame('{def}', $settings->libpthread_regex);
$this->assertSame('{abc}', $settings->getDelimitedPhpRegex());
$this->assertSame('{def}', $settings->getDelimitedLibPthreadRegex());
$this->assertSame('v74', $settings->php_version);
$this->assertSame('ghi', $settings->php_path);
$this->assertSame('jkl', $settings->libpthread_path);
Expand Down
Original file line number Diff line number Diff line change
@@ -0,0 +1,49 @@
<?php

/**
* This file is part of the sj-i/php-profiler package.
*
* (c) sji <sji@sj-i.dev>
*
* For the full copyright and license information, please view the LICENSE
* file that was distributed with this source code.
*/

declare(strict_types=1);

namespace PhpProfiler\Inspector\Settings\TargetPhpSettings;

use PhpProfiler\Lib\PhpInternals\ZendTypeReader;
use PHPUnit\Framework\TestCase;

class TargetPhpSettingsTest extends TestCase
{
public function testAlterPhpVersion(): void
{
$settings = new TargetPhpSettings(
php_version: ZendTypeReader::V74,
);
$settings_altered = $settings->alterPhpVersion(ZendTypeReader::V81);
$this->assertSame($settings->php_path, $settings_altered->php_path);
$this->assertSame($settings->getDelimitedPhpRegex(), $settings_altered->getDelimitedPhpRegex());
$this->assertSame($settings->libpthread_path, $settings_altered->libpthread_path);
$this->assertSame($settings->getDelimitedLibPthreadRegex(), $settings_altered->getDelimitedLibPthreadRegex());
$this->assertSame(ZendTypeReader::V81, $settings_altered->php_version);
}

public function testGetDelimitedPhpRegex(): void
{
$settings = new TargetPhpSettings(
php_regex: 'test',
);
$this->assertSame('{test}', $settings->getDelimitedPhpRegex());
}

public function testGetDelimitedLibPthreadRegex(): void
{
$settings = new TargetPhpSettings(
libpthread_regex: 'test',
);
$this->assertSame('{test}', $settings->getDelimitedLibPthreadRegex());
}
}
97 changes: 97 additions & 0 deletions tests/Lib/PhpProcessReader/PhpVersionDetectorTest.php
Original file line number Diff line number Diff line change
@@ -0,0 +1,97 @@
<?php

/**
* This file is part of the sj-i/php-profiler package.
*
* (c) sji <sji@sj-i.dev>
*
* For the full copyright and license information, please view the LICENSE
* file that was distributed with this source code.
*/

declare(strict_types=1);

namespace PhpProfiler\Lib\PhpProcessReader;

use PhpProfiler\Inspector\Settings\TargetPhpSettings\TargetPhpSettings;
use PhpProfiler\Lib\ByteStream\IntegerByteSequence\LittleEndianReader;
use PhpProfiler\Lib\Elf\Parser\Elf64Parser;
use PhpProfiler\Lib\Elf\Process\ProcessModuleSymbolReaderCreator;
use PhpProfiler\Lib\Elf\SymbolResolver\Elf64SymbolResolverCreator;
use PhpProfiler\Lib\File\NativeFileReader;
use PhpProfiler\Lib\PhpInternals\ZendTypeReaderCreator;
use PhpProfiler\Lib\Process\MemoryMap\ProcessMemoryMapCreator;
use PhpProfiler\Lib\Process\MemoryReader\MemoryReader;
use PhpProfiler\Lib\Process\ProcessSpecifier;
use PHPUnit\Framework\TestCase;

class PhpVersionDetectorTest extends TestCase
{
/** @var resource|null */
private $child = null;

protected function tearDown(): void
{
if (!is_null($this->child)) {
$child_status = proc_get_status($this->child);
if (is_array($child_status)) {
if ($child_status['running']) {
posix_kill($child_status['pid'], SIGKILL);
}
}
}
}

public function testTryDetection()
{
$memory_reader = new MemoryReader();
$php_symbol_reader_creator = new PhpSymbolReaderCreator(
$memory_reader,
new ProcessModuleSymbolReaderCreator(
new Elf64SymbolResolverCreator(
new NativeFileReader(),
new Elf64Parser(
new LittleEndianReader()
)
),
$memory_reader,
),
$process_memory_map_creator = ProcessMemoryMapCreator::create(),
new LittleEndianReader()
);
$php_version_detector = new PhpVersionDetector(
$php_symbol_reader_creator,
new ZendTypeReaderCreator(),
$memory_reader
);

$this->child = proc_open(
[
PHP_BINARY,
'-r',
'fputs(STDOUT, "a\n");fgets(STDIN);'
],
[
['pipe', 'r'],
['pipe', 'w'],
['pipe', 'w']
],
$pipes
);

fgets($pipes[1]);
$child_status = proc_get_status($this->child);

sleep(1);

/** @var int $child_status['pid'] */
$php_version = $php_version_detector->tryDetection(
new ProcessSpecifier($child_status['pid']),
new TargetPhpSettings(
php_regex: PHP_BINARY,
php_path: PHP_BINARY,
)
);
$this->assertIsString($php_version);
}
}
6 changes: 6 additions & 0 deletions tools/stubs/ffi/php.php
Original file line number Diff line number Diff line change
Expand Up @@ -78,3 +78,9 @@ class zend_class_entry extends CData
{
public zend_string $name;
}

class zend_module_entry extends CData
{
public int $version; // pointer
public int $name; // pointer
}