Xdebug, DBGPで簡易デバッガを作ってみました。
この記事はPHP Advent Calendar 2018の23日目の記事です。Xdebug, DBGPの仕組みについて
N番煎じくらいだと思いますが、改めて説明しますと、Xdebugが有効になったPHPを実行するとPHP(以下、クライアント)がDBGPプロトコルでリモートサーバに接続します。リモートサーバはクライアントに対してコマンドを送り、クライアントがコマンドに対応する処理を実行し、レスポンスを返すことで対話的にデバッグできるようになっています。実装について
DBGPはTCPなので、まずはTCPサーバを立ち上げます。$socket = stream_socket_server("tcp://0.0.0.0:9000", $errno, $errstr);
if (!$socket) {
echo "${errstr} (${errno})\n";
die('Could not create socket');
}
while (true) {
while ($conn = stream_socket_accept($socket, -1)) {
while(true) {
// ソケットI/Oな処理
}
fclose($conn);
}
}
fclose($socket);
ソケットI/Oな処理の部分ですがDBGPはざっくり以下のような仕様になっています
- リモートサーバ側からクライアントに対してコマンドを送信する
- コマンドは
{コマンド名} -i {トランザクションID} {その他オプション引数}
の形式で送信 - トランザクションIDはリクエスト/レスポンスの対を特定するための識別子(コマンド打つたびにインクリメントして渡せばOKっぽい)
- コマンドは
- 接続元からサーバ側へのレスポンスは
bodyの長さ + ヌルバイト + body
という形式で返され、bodyはXMLで返される - コマンドもレスポンスもヌルバイト(\0)が終端
function sendCommand($conn, $input) {
if (in_array($input, ["n", "next"])) {
$command = "step_over -i $this->transaction_id";
} else if (in_array($input, ["c", "continue"])) {
# ...
}
stream_socket_sendto($conn, "${command}\0");
$this->transaction_id++;
}
コマンドを送信したらレスポンスを取得します
メッセージ取得は以下のように定義しています。終端がヌルバイトになるまでメッセージを読み取ります。
function getMessage($conn) {
$message = "";
while (true) {
$chunk = stream_socket_recvfrom($conn, 1024, 0);
echo $chunk;
$message .= $chunk;
$len = strlen($message);
if ($message[$len - 1] == "\0") {
break;
}
}
return $message;
}
while (true) {
while ($conn = stream_socket_accept($socket, -1)) {
while(true) {
$message = getMessage($conn);
$input = readline(">> ");
sendCommand($conn, $input);
// ...
読み取ったあとはレスポンスに応じたハンドリングを行います。
レスポンスはXMLなのでよしなにパースするのですが、名前空間付きの属性やタグがあるのでsimplexml_load_xxxだとうまくロードできません。XMLが大きくないので、DOMDocument::loadXMLでメモリに読み出す方法が楽だと思います。
preg_match('/^(\d+)\0([\s\S]+)/', $message, $matches);
$dom = DOMDocument::loadXML($matches[2]);
$init = $dom->getElementsByTagName("init");
ステップ実行をした場合はこんな感じで現在のファイル名と行番号が返ってくるのでそれに応じてファイルの中身を表示します(実際にはXMLの前にはXMLのサイズ(数値)+ヌルバイトが入ります)
<?xml version="1.0" encoding="iso-8859-1"?>
<response xmlns="urn:debugger_protocol_v1" xmlns:xdebug="http://xdebug.org/dbgp/xdebug"
command="step_over"
transaction_id="3"
status="break"
reason="ok">
<xdebug:message filename="file:///Users/foo/tmp/php/index.php" lineno="7">
</xdebug:message>
</response>
evalの場合は入力文字列をbase64エンコードして送る必要があります。
$b64encoded = base64_encode($input);
$command = "eval -i $this->transaction_id -- ${b64encoded}";
実行結果が返ってきますが、stringの場合はbase64エンコードされているのでよしなにデコードしつつ表示します。
<?xml version="1.0" encoding="iso-8859-1"?>
<response xmlns="urn:debugger_protocol_v1" xmlns:xdebug="http://xdebug.org/dbgp/xdebug"
command="eval" transaction_id="$8">
<property type="string" size="4" encoding="base64"><![CDATA[aG9nZQ==]]></property>
</response>
オブジェクトの場合はpropertyがネストされて返ってくるのでよしなにパースしてよしなに表示すればOKです(SimpleDebuggerではクラス名しか表示していない手抜き実装)
ということで簡単にデバッガを実装できました〜
参考URL
- https://xdebug.org/docs-dbgp.php : 本家マニュアル
- https://github.com/tacnoman/dephpugger: Xdebug, DBGPを使ったデバッガ。読みやすく、DBGPのやりとりあたりはかなり参考にさせていただきました。