2018-12-23

Xdebug, DBGPで簡易リモートデバッガを作る

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はざっくり以下のような仕様になっています

ステップ実行はstep_overコマンドで実行します。
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

このエントリーをはてなブックマークに追加