Friday, March 27, 2009

PHP CLI XDebug client

This shows how to create a simple XDebug client in PHP.
When the client is started, it waits for a PHP script to start, and then begins to take input
from the user.



The source formatting doesn't look too great, but it's readable, I guess...



We start off with the actual server that listens to port 9000:


#!/usr/bin/php

/*
* Xdebug settings in php.ini
*
* xdebug.remote_autostart=on
* xdebug.remote_enable=on
* xdebug.remote_handler=dbgp
* xdebug.remote_mode=req
* xdebug.remote_host=localhost
* xdebug.remote_port=9000
*/

require_once 'Zend/Loader.php';
Zend_Loader::registerAutoload();

// PEAR
require_once 'Net/Server.php';

require_once 'lib/CLIDebug.php';

$server = &Net_Server::create('sequential', 'localhost', 9000);

$server->setCallbackObject(new CLIDebug());
$server->setEndCharacter("\0");

$server->setDebugMode(false);

$server->start();

?>


Our XDebug client is added via the "setCallbackObject" method, and
looks like this:



/**
* Description of CLIDebug
*
* @author Morten Amundsen
*/
class CLIDebug extends Net_Server_Handler
{
private $_clientid = 0;
private $_appid = 0;
private $_app = '';
private $_status = '';

private $_currline = 0;

/**
*
* @param int $client
* @param string $data
* @return boolean
*/
public function onReceiveData($client = 0, $data = '')
{
$this->_client = $client;

if (is_numeric(trim($data)) or !trim($data)) return true;

$xml = simplexml_load_string($data);

if ($xml) {
if (!empty($xml['appid']) and !empty($xml['fileuri'])) {
$this->_appid = $xml['appid'];
$this->_app = $xml['fileuri'];

$this->_printHelp();
}

$this->_status = (string) $xml['status'];

if ($this->_status == 'stopping') {
$this->_server->closeConnection();
die("Done.\n");
}

$command = (string) $xml['command'];

switch ($command) {
case 'source':
$code = (string) $xml;
$code = base64_decode($code);
echo $code."\n";
break;
case 'step_over':
case 'step_into':
$xdebug = $xml->children('http://xdebug.org/dbgp/xdebug');

$this->_printFileAndLine($xdebug);

if ($this->_cmdSourceLineXdebug($xdebug)) {
return true;
}
default:
}

flush();

} else {
return true;
}

do {
echo ">";

flush();

while (($cmd = $this->waitForCommand()) === false);
} while (!$this->_executeCommand($cmd));
}

/**
*
* @return mixed
*/
public function waitForCommand()
{
if ($fh = fopen("php://STDIN", 'r')) {
$cmd = fgets($fh, 100);
fclose($fh);

return trim($cmd);
} else {
echo "Unable to connect to php://STDIN\n";
return false;
}
}

/**
*
* @return string Command
*/
private function _executeCommand($cmd)
{
$cmd = trim(strtolower($cmd));

switch ($cmd) {
case 'o':
case 'step_over':
return $this->_cmdStepOver();
break;
case 'i':
case 'step_into':
return $this->_cmdStepInto();
break;
case 'h':
case 'help':
$this->_printHelp();
return false;
break;
case 'q':
case 'quit':
$this->_server->closeConnection();
die("Quitting.\n");
default:
echo "Unknown command: ".$cmd."\n";
return false;
}

return true;
}

/**
*
* @return boolean
*/
private function _cmdStepOver()
{
$cmd = "step_over -i {$this->_appid}\0";
$this->_server->sendData($this->_clientid, $cmd);

return true;
}

/**
*
* @return boolean
*/
private function _cmdStepInto()
{
$cmd = "step_into -i {$this->_appid}\0";
$this->_server->sendData($this->_clientid, $cmd);

return true;
}

/**
*
* @param SimpleXMLElement $xdebug
* @return boolean
*/
private function _cmdSourceLineXdebug($xdebug)
{
$attr = $xdebug->attributes();

if ($attr and isset($attr['lineno'])) {

$line = (string) $attr['lineno'];
$file = (string) $attr['filename'];

$cmd = "source -i {$this->_appid} -b {$line} -e {$line} -f {$file}\0";
$this->_server->sendData($this->_clientid, $cmd);

return true;
} else {
return false;
}
}

/**
*
* @param SimpleXMLElement $xdebug
* @return boolean
*/
private function _cmdSourceLines($file, $begin, $end)
{
$cmd = "source -i {$this->_appid} -b {$begin} -e {$end} -f {$file}\0";
$this->_server->sendData($this->_clientid, $cmd);

return true;
}

/**
*
* @param SimpleXMLElement $xdebug
*/
private function _printFileAndLine($xdebug)
{
$attr = $xdebug->attributes();

$this->_app = (string) $attr['filename'];

$line = (string) $attr['lineno'];
$this->_currline = $line;

echo "File: ".(string) $this->_app . " - Line: " . (string) $attr['lineno']." (".$this->_status.")\n";
}

/**
*
*/
private function _printHelp()
{
echo "i|step_into\n\tsteps to the next statement, if there is a function call\n\tinvolved it will break on the first statement in that function\n";
echo "o|step_over\n\tsteps to the next statement, if there is a function call\n\ton the line from which the step_over is issued then the debugger engine will stop at the\n\tstatement after the function call in the same\n\tscope as from where the command was issued\n";
echo "h|help\n\tThis help text\n";
echo "q|quit\n\tQuit\n";
flush();
}
}
?>


Now it's only a matter of filling in the blanks with the rest of the DBGP protocol.

0 comments:

Post a Comment