<?php class DebugClient { // free port will be selected automatically by the operating system protected $port = 0; private $tmpDir; protected $socket; protected $php; protected $ppipes; public function getPort() { return $this->port; } public function setPort($port) { $this->port = $port; } protected function getIPAddress() { return "127.0.0.1"; } protected function getAddress() { return "tcp://" . $this->getIPAddress() . ":" . $this->getPort(); } public function __construct() { $envTmpDir = getenv('TEST_TMP_DIR'); $this->tmpDir = $envTmpDir !== false ? $envTmpDir : sys_get_temp_dir(); } private function open( &$errno, &$errstr ) { $socket = @stream_socket_server( $this->getAddress(), $errno, $errstr ); if ( $socket ) { $name = stream_socket_get_name( $socket, false ); $name = explode( ":", $name ); $this->port = array_pop( $name ); } return $socket; } private function launchPhp( &$pipes, $filename, array $ini_options = [] ) { @unlink( $this->tmpDir . '/error-output.txt' ); @unlink( $this->tmpDir . '/remote_log.txt' ); $descriptorspec = array( 0 => array( 'pipe', 'r' ), 1 => array( 'pipe', 'w' ), 2 => array( 'file', $this->tmpDir . '/error-output.txt', 'a' ) ); $default_options = array( "xdebug.mode" => "debug", "xdebug.start_with_request" => "'yes'", "xdebug.client_host" => $this->getIPAddress(), "xdebug.client_port" => $this->getPort(), ); $options = (getenv('TEST_PHP_ARGS') ?: ''); $ini_options = array_merge( $default_options, $ini_options ); foreach ( $ini_options as $key => $value ) { $options .= " -d{$key}=$value"; } $php = getenv( 'TEST_PHP_EXECUTABLE' ); $cmd = "{$php} $options {$filename} >{$this->tmpDir}/php-stdout.txt 2>{$this->tmpDir}/php-stderr.txt"; $cwd = dirname( __FILE__ ); $process = proc_open( $cmd, $descriptorspec, $pipes, $cwd ); return $process; } function fixFilePath( $m ) { preg_match( '@.*/(.*\.inc)@', $m[2], $fm ); if ( !isset( $fm[1] ) ) { $fm[1] = ''; } return " {$m[1]}=\"file://{$fm[1]}\""; } function doRead( $conn, string $transaction_id = null ) { stream_set_timeout( $conn, 3 ); do { $trans_id = null; $length = 0; while ( "\0" !== ( $char = fgetc($conn) ) ) { if ( $char === false ) { echo "read a false for $transaction_id" . PHP_EOL; return null; } if ( !is_numeric($char) ) { echo "read a non-number for $transaction_id" . PHP_EOL; return null; } $length = $length * 10 + (int)$char; } $read = ''; while ( 0 < $length ) { $data = fread( $conn, $length ); if ( $data === false ) { echo "read a false for $transaction_id" . PHP_EOL; return null; } $length -= strlen( $data ); $read .= $data; } $char = fgetc( $conn ); if ( $char !== "\0" ) { echo 'must end with \0' . PHP_EOL; } // sanitize $read = preg_replace( '@\s(appid|id)="\d+?"@', ' \\1=""', $read ); $read = preg_replace( '@\s(xdebug:language_version)="[^"]+?"@', ' \\1=""', $read ); $read = preg_replace( '@(engine\sversion)="[^"]+?"@', '\\1=""', $read ); $read = preg_replace( '@(2002-20[0-9]{2})@', '2002-2099', $read ); $read = preg_replace_callback( '@\s(fileuri|filename)="file:///(.+?)"@', 'self::fixFilePath', $read ); echo $read, "\n\n"; if ( preg_match( '@<stream xmlns="urn.debugger_protocol_v1" xmlns:xdebug@', $read ) ) { continue; } if ( preg_match( '@<notify xmlns="urn.debugger_protocol_v1" xmlns:xdebug@', $read ) ) { continue; } $matches = []; if ( preg_match( '@transaction_id="(?P<transaction_id>\d+)"@', $read, $matches ) ) { $trans_id = $matches['transaction_id'] ?? null; } } while ( $trans_id !== $transaction_id ); } function start( $filename, array $ini_options = [], array $options = []) { $filename = realpath( $filename ); $this->socket = $this->open( $errno, $errstr ); if ( $this->socket === false ) { echo "Could not create socket server - already in use?\n"; echo "Error: {$errstr}, errno: {$errno}\n"; echo "Address: {$this->getAddress()}\n"; return false; } $this->php = $this->launchPhp( $this->ppipes, $filename, $ini_options ); $conn = @stream_socket_accept( $this->socket, isset( $options['timeout'] ) ? $options['timeout'] : 20 ); if ( $conn === false ) { echo @file_get_contents( $this->tmpDir . '/php-stdout.txt' ), "\n"; echo @file_get_contents( $this->tmpDir . '/php-stderr.txt' ), "\n"; echo @file_get_contents( $this->tmpDir . '/error-output.txt' ), "\n"; echo @file_get_contents( $this->tmpDir . '/remote_log.txt' ), "\n"; proc_close( $this->php ); return false; } return $conn; } function stop( $conn ) { fclose( $conn ); fclose( $this->ppipes[0] ); fclose( $this->ppipes[1] ); fclose( $this->socket ); proc_close( $this->php ); // echo @file_get_contents( $this->tmpDir . '/php-stderr.txt' ), "\n"; // echo @file_get_contents( $this->tmpDir . '/error-output.txt' ), "\n"; } function sendCommand( $conn, $command, $transaction_id ) { // inject identifier $parts = explode( ' ', $command, 2 ); if ( count($parts) == 1 ) { $command = $parts[0] . " -i $transaction_id"; } else { $command = $parts[0] . " -i $transaction_id " . $parts[1]; } $sanitised = $command; $sanitised = preg_replace( '@\sfile://.*[/\\\\](.*\.inc)\s@', ' file://\\1 ', $sanitised ); echo "-> ", $sanitised, "\n"; fwrite( $conn, $command . "\0" ); } function runTest( $filename, array $commands, array $ini_options = [], array $options = [] ) { $conn = $this->start( $filename, $ini_options, $options ); if ( $conn === false ) { return; } $i = 1; // read header $this->doRead( $conn ); foreach ( $commands as $command ) { $this->sendCommand( $conn, $command, $i ); $this->doRead( $conn, (string)$i ); $i++; } $this->stop( $conn ); } } class DebugClientIPv6 extends DebugClient { protected function getIPAddress() { return "::1"; } protected function getAddress() { return "tcp://[" . $this->getIPAddress() . "]:" . $this->getPort(); } public static function isSupported( &$errno, &$errstr ) { $socket = @stream_socket_server( "tcp://[::1]:0", $errno, $errstr ); if ( $socket === false ) { return false; } fclose( $socket ); return true; } } function dbgpRunFile( $data, $commands, array $ini_options = [], array $options = [] ) { if ( isset( $options['ipv'] ) && $options['ipv'] == 6 ) { $t = new DebugClientIPv6(); } else { $t = new DebugClient(); } $t->runTest( $data, $commands, $ini_options, $options ); } ?>