デバッガー拡張機能

Visual Studio Code のデバッグアーキテクチャを使用すると、拡張機能作成者は既存のデバッガーを VS Code に簡単に統合でき、同時にすべてのデバッガーで共通のユーザーインターフェイスを利用できます。

VS Code には、組み込みのデバッガー拡張機能として Node.js デバッガー拡張機能が付属しており、これは VS Code がサポートする多くのデバッガー機能を示す優れた例となっています。

VS Code Debug Features

このスクリーンショットは、以下のデバッグ機能を示しています。

  1. デバッグ構成管理。
  2. 開始/停止およびステップ実行のためのデバッグ操作。
  3. ソース、関数、条件付き、インラインの各ブレークポイント、およびログポイント。
  4. マルチスレッドおよびマルチプロセスサポートを含むスタックトレース。
  5. ビューやホバーでの複雑なデータ構造のナビゲーション。
  6. ホバーまたはソース内にインライン表示される変数。
  7. ウォッチ式の管理。
  8. オートコンプリート付きの対話型評価が可能なデバッグコンソール。

このドキュメントでは、あらゆるデバッガーを VS Code で動作させるためのデバッガー拡張機能の作成方法について説明します。

VS Code のデバッグアーキテクチャ

VS Code は、デバッガーのバックエンドと通信するために導入された抽象プロトコルに基づいて、汎用的な(言語に依存しない)デバッガー UI を実装しています。通常、デバッガーはこのプロトコルを直接実装していないため、デバッガーをプロトコルに「適合」させるための中間層が必要です。この中間層は、通常、デバッガーと通信する独立したプロセスです。

VS Code Debug Architecture

私たちはこの中間層をデバッグアダプター(または略して DA)と呼び、DA と VS Code の間で使われる抽象プロトコルをデバッグアダプタープロトコル(略して DAP)と呼びます。デバッグアダプタープロトコルは VS Code から独立しているため、独自のウェブサイトを持っており、そこで導入と概要、詳細な仕様、および既知の実装やサポートツールのリストを確認できます。DAP の歴史と背景にある動機については、こちらのブログ記事で説明されています。

デバッグアダプターは VS Code から独立しており、他の開発ツールでも使用できるため、拡張機能とコントリビューションポイントに基づく VS Code の拡張機能アーキテクチャとは一致しません。

そのため、VS Code は debuggers というコントリビューションポイントを提供しており、ここで特定のデバッグタイプ(例:Node.js デバッガーの場合は node)の下にデバッグアダプターを登録できます。ユーザーがそのタイプのデバッグセッションを開始するたびに、VS Code は登録された DA を起動します。

したがって、最も最小限の形式では、デバッガー拡張機能とはデバッグアダプターの実装を宣言的に指定するものであり、拡張機能は追加のコードを含まないデバッグアダプターのパッケージングコンテナに過ぎません。

VS Code Debug Architecture 2

より実用的なデバッガー拡張機能は、以下の宣言的項目の一部またはすべてを VS Code に提供します。

  • デバッガーがサポートする言語のリスト。VS Code はこれらの言語に対してブレークポイントを設定する UI を有効にします。
  • デバッガーによって導入されるデバッグ構成属性の JSON スキーマ。VS Code はこのスキーマを使用して launch.json エディター内の構成を検証し、IntelliSense を提供します。なお、JSON スキーマの $ref および definition 構造はサポートされていないことに注意してください。
  • VS Code によって作成される初期 launch.json のためのデフォルトデバッグ構成。
  • ユーザーが launch.json ファイルに追加できるデバッグ構成スニペット。
  • デバッグ構成で使用できる変数の宣言。

詳細については、contributes.breakpoints および contributes.debuggers のリファレンスを参照してください。

上記の純粋に宣言的なコントリビューションに加え、Debug Extension API は以下のコードベースの機能を実現します。

  • VS Code によって作成される初期 launch.json のために動的に生成されるデフォルトデバッグ構成。
  • 使用するデバッグアダプターを動的に決定する。
  • デバッグアダプターに渡される前に、デバッグ構成を検証または変更する。
  • デバッグアダプターと通信する。
  • デバッグコンソールにメッセージを送信する。

このドキュメントの後半では、デバッガー拡張機能の開発方法を示します。

Mock Debug 拡張機能

ゼロからデバッグアダプターを作成するのはこのチュートリアルでは少し重いため、学習用の「デバッグアダプター・スターターキット」として作成したシンプルな DA から始めます。これは、実際のデバッガーと対話するのではなく、デバッガーを模倣(mock)するため「Mock Debug」と呼ばれます。Mock Debug はデバッガーをシミュレートし、ステップ実行、続行、ブレークポイント、例外、変数アクセスをサポートしますが、実際のデバッガーには接続されていません。

mock-debug の開発セットアップに入る前に、まずは VS Code Marketplace からビルド済みバージョンをインストールして触ってみましょう。

  • 拡張機能ビューレットに切り替え、「mock」と入力して Mock Debug 拡張機能を検索します。
  • 拡張機能を「インストール」し、「リロード」します。

Mock Debug を試すには

  • 新しい空のフォルダー mock test を作成し、VS Code で開きます。
  • readme.md ファイルを作成し、任意のテキストを数行入力します。
  • 「実行とデバッグ」ビュー (⇧⌘D (Windows, Linux Ctrl+Shift+D)) に切り替え、launch.json ファイルを作成するリンクを選択します。
  • VS Code で「デバッガー」を選択してデフォルトの起動構成を作成するよう求められるので、「Mock Debug」を選びます。
  • 緑色の開始ボタンを押し、Enter キーを押して提案された readme.md ファイルを確定します。

デバッグセッションが開始され、readme ファイルを「ステップ実行」したり、ブレークポイントを設定して到達させたり、例外に遭遇させたり(行内に exception という単語がある場合)できるようになります。

Mock Debugger running

Mock Debug を自身の開発の出発点として使用する前に、まずビルド済みバージョンをアンインストールすることをお勧めします。

  • 拡張機能ビューレットに切り替え、Mock Debug 拡張機能の歯車アイコンをクリックします。
  • 「アンインストール」を実行し、ウィンドウを「リロード」します。

Mock Debug の開発セットアップ

それでは、Mock Debug のソースを取得して、VS Code 内で開発を始めましょう。

git clone https://github.com/microsoft/vscode-mock-debug.git
cd vscode-mock-debug
yarn

プロジェクトフォルダー vscode-mock-debug を VS Code で開きます。

パッケージの中身は?

  • package.json は mock-debug 拡張機能のマニフェストです。
    • ここには mock-debug 拡張機能のコントリビューションがリストされています。
    • compile および watch スクリプトは、TypeScript ソースを out フォルダーにトランスパイルし、その後のソースの変更を監視するために使用されます。
    • 依存関係である vscode-debugprotocolvscode-debugadapter、および vscode-debugadapter-testsupport は、Node ベースのデバッグアダプターの開発を簡素化する NPM モジュールです。
  • src/mockRuntime.ts は、シンプルなデバッグ API を持つモックランタイムです。
  • ランタイムをデバッグアダプタープロトコルに適応させるコードは src/mockDebug.ts に存在します。ここには、DAP のさまざまなリクエストに対するハンドラーが記述されています。
  • デバッガー拡張機能の実装はデバッグアダプター内で行われるため、拡張機能コード(拡張機能ホストプロセスで実行されるコード)を持つ必要は全くありません。しかし、Mock Debug には src/extension.ts という小さなファイルがあります。これはデバッガー拡張機能の拡張機能コード内で何ができるかを示すためです。

Extension 起動構成を選択して F5 を押し、Mock Debug 拡張機能をビルドして起動します。最初、これは TypeScript ソースを out フォルダーへ完全にトランスパイルします。フルビルド後、変更を自動的にトランスパイルするウォッチャータスクが開始されます。

ソースのトランスパイル後、「[Extension Development Host]」というラベルが付いた新しい VS Code ウィンドウが現れ、Mock Debug 拡張機能がデバッグモードで実行されます。そのウィンドウから mock test プロジェクトと readme.md ファイルを開き、「F5」でデバッグセッションを開始してステップ実行してみてください。

Debugging Extension and Server

デバッグモードで拡張機能を実行しているため、src/extension.ts 内にブレークポイントを設定して到達させることも可能ですが、前述の通り、拡張機能内で実行されている興味深いコードはほとんどありません。興味深いコードは、別のプロセスであるデバッグアダプター内で実行されます。

デバッグアダプター自体をデバッグするには、デバッグモードで実行する必要があります。これは、デバッグアダプターをサーバーモードで実行し、VS Code をそれに接続するように構成するのが最も簡単です。VS Code の vscode-mock-debug プロジェクトで、ドロップダウンメニューから起動構成 Server を選択し、緑色の開始ボタンを押します。

すでに拡張機能のアクティブなデバッグセッションがあるため、VS Code のデバッガー UI はマルチセッションモードに入ります。これは、CALL STACK ビューに ExtensionServer という2つのデバッグセッション名が表示されることで確認できます。

Debugging Extension and Server

これで拡張機能と DA を同時にデバッグできるようになりました。ここへ到達するより速い方法は、Extension + Server 起動構成を使用することです。これにより、両方のセッションが自動的に起動します。

拡張機能と DA をデバッグするための、さらに簡単な別のアプローチを以下に示します。

ファイル src/mockDebug.ts のメソッド launchRequest(...) の先頭にブレークポイントを設定します。最後のステップとして、ポート 4711 用の debugServer 属性を mock test の起動構成に追加し、モックデバッガーを DA サーバーに接続するように構成します。

{
  "version": "0.2.0",
  "configurations": [
    {
      "type": "mock",
      "request": "launch",
      "name": "mock test",
      "program": "${workspaceFolder}/readme.md",
      "stopOnEntry": true,
      "debugServer": 4711
    }
  ]
}

このデバッグ構成を起動すると、VS Code はモックデバッグアダプターを別のプロセスとして起動するのではなく、既に実行中のサーバーのローカルポート 4711 に直接接続するため、launchRequest 内のブレークポイントに到達するはずです。

このセットアップにより、Mock Debug の編集、トランスパイル、デバッグが容易に行えるようになります。

しかし、ここからが本当の仕事です。src/mockDebug.tssrc/mockRuntime.ts のデバッグアダプターのモック実装を、「実際の」デバッガーやランタイムと対話するコードに置き換える必要があります。これには、デバッグアダプタープロトコルを理解して実装することが含まれます。詳細についてはこちらを参照してください。

デバッガー拡張機能の package.json の構造

デバッガー特有のデバッグアダプター実装を提供するだけでなく、デバッガー拡張機能はさまざまなデバッグ関連のコントリビューションポイントに寄与する package.json を必要とします。

Mock Debug の package.json を詳しく見てみましょう。

すべての VS Code 拡張機能と同様に、package.json は拡張機能の基本プロパティである namepublisherversion を宣言します。categories フィールドを使用して、VS Code 拡張機能マーケットプレイスで拡張機能を見つけやすくします。

{
  "name": "mock-debug",
  "displayName": "Mock Debug",
  "version": "0.24.0",
  "publisher": "...",
  "description": "Starter extension for developing debug adapters for VS Code.",
  "author": {
    "name": "...",
    "email": "..."
  },
  "engines": {
    "vscode": "^1.17.0",
    "node": "^7.9.0"
  },
  "icon": "images/mock-debug-icon.png",
  "categories": ["Debuggers"],

  "contributes": {
    "breakpoints": [{ "language": "markdown" }],
    "debuggers": [
      {
        "type": "mock",
        "label": "Mock Debug",

        "program": "./out/mockDebug.js",
        "runtime": "node",

        "configurationAttributes": {
          "launch": {
            "required": ["program"],
            "properties": {
              "program": {
                "type": "string",
                "description": "Absolute path to a text file.",
                "default": "${workspaceFolder}/${command:AskForProgramName}"
              },
              "stopOnEntry": {
                "type": "boolean",
                "description": "Automatically stop after launch.",
                "default": true
              }
            }
          }
        },

        "initialConfigurations": [
          {
            "type": "mock",
            "request": "launch",
            "name": "Ask for file name",
            "program": "${workspaceFolder}/${command:AskForProgramName}",
            "stopOnEntry": true
          }
        ],

        "configurationSnippets": [
          {
            "label": "Mock Debug: Launch",
            "description": "A new configuration for launching a mock debug program",
            "body": {
              "type": "mock",
              "request": "launch",
              "name": "${2:Launch Program}",
              "program": "^\"\\${workspaceFolder}/${1:Program}\""
            }
          }
        ],

        "variables": {
          "AskForProgramName": "extension.mock-debug.getProgramName"
        }
      }
    ]
  },

  "activationEvents": ["onDebug", "onCommand:extension.mock-debug.getProgramName"]
}

次に、デバッグ拡張機能固有のコントリビューションを含む contributes セクションを見てみます。

まず、breakpoints コントリビューションポイントを使用して、ブレークポイントの設定が有効になる言語をリストします。これがないと、Markdown ファイルにブレークポイントを設定することはできません。

次は debuggers セクションです。ここでは、デバッグ type mock の下に1つのデバッガーが導入されています。ユーザーは起動構成でこのタイプを参照できます。オプション属性の label を使用すると、UI に表示されるときにデバッグタイプにわかりやすい名前を付けることができます。

デバッグ拡張機能はデバッグアダプターを使用するため、コードへの相対パスが program 属性として指定されます。拡張機能を自己完結させるため、アプリケーションは拡張機能フォルダー内に存在する必要があります。慣例として、このアプリケーションは out または bin というフォルダー内に保持しますが、別の名前を使用することも自由です。

VS Code はさまざまなプラットフォームで動作するため、DA プログラムもそれらのプラットフォームをサポートしていることを確認する必要があります。そのために以下のオプションがあります。

  1. プログラムがプラットフォームに依存しない方法で実装されている場合(例:サポートされているすべてのプラットフォームで利用可能なランタイム上で実行されるプログラム)、runtime 属性を通じてこのランタイムを指定できます。現時点で、VS Code は node および mono ランタイムをサポートしています。上記の Mock デバッグアダプターはこのアプローチを使用しています。

  2. DA 実装がプラットフォームごとに異なる実行ファイルを必要とする場合、program 属性を次のようにプラットフォームごとに修飾できます。

    "debuggers": [{
        "type": "gdb",
        "windows": {
            "program": "./bin/gdbDebug.exe",
        },
        "osx": {
            "program": "./bin/gdbDebug.sh",
        },
        "linux": {
            "program": "./bin/gdbDebug.sh",
        }
    }]
    
  3. 両方のアプローチを組み合わせることも可能です。以下の例は Mono DA のもので、macOS と Linux ではランタイムを必要とする mono アプリケーションとして実装されていますが、Windows では不要です。

    "debuggers": [{
        "type": "mono",
        "program": "./bin/monoDebug.exe",
        "osx": {
            "runtime": "mono"
        },
        "linux": {
            "runtime": "mono"
        }
    }]
    

configurationAttributes は、このデバッガーで利用可能な launch.json 属性のスキーマを宣言します。このスキーマは、launch.json の検証、および起動構成を編集する際の IntelliSense やホバーヘルプのサポートに使用されます。

initialConfigurations は、このデバッガーのためのデフォルトの launch.json の初期内容を定義します。この情報は、プロジェクトに launch.json が存在せず、ユーザーがデバッグセッションを開始したり、実行とデバッグビューの launch.json ファイルを作成するリンクを選択したりしたときに使用されます。この場合、VS Code はユーザーにデバッグ環境を選択させ、対応する launch.json を作成します。

Debugger Quickpick

package.jsonlaunch.json の初期内容を静的に定義する代わりに、DebugConfigurationProvider を実装して初期構成を動的に計算することも可能です(詳細は以下の「DebugConfigurationProvider の使用」セクションを参照)。

configurationSnippets は、launch.json を編集する際に IntelliSense に表示される起動構成スニペットを定義します。慣例として、スニペットの label 属性にはデバッグ環境名をプレフィックスとして付け、多くのスニペット候補の中で明確に識別できるようにします。

variables コントリビューションは、「変数」を「コマンド」にバインドします。これらの変数は ${command:xyz} 構文を使用して起動構成で使用でき、デバッグセッションが開始されると、バインドされたコマンドから返される値に置換されます。

コマンドの実装は拡張機能内にあり、UI を持たない単純な式から、拡張機能 API で利用可能な UI 機能に基づいた高度な機能まで多岐にわたります。Mock Debug は AskForProgramName という変数を extension.mock-debug.getProgramName コマンドにバインドします。src/extension.ts におけるこのコマンドの実装は、showInputBox を使用してユーザーにプログラム名を入力させます。

vscode.commands.registerCommand('extension.mock-debug.getProgramName', config => {
  return vscode.window.showInputBox({
    placeHolder: 'Please enter the name of a markdown file in the workspace folder',
    value: 'readme.md'
  });
});

この変数は、起動構成の文字列型値の中で ${command:AskForProgramName} として使用できるようになりました。

DebugConfigurationProvider の使用

package.json でのデバッグコントリビューションの静的な定義だけでは不十分な場合、DebugConfigurationProvider を使用してデバッグ拡張機能の以下の側面を動的に制御できます。

  • 新しく作成された launch.json の初期デバッグ構成を、ワークスペースで利用可能なコンテキスト情報に基づいて動的に生成できます。
  • 起動構成は、新しいデバッグセッションを開始するために使用される前に「解決(解決)」または修正できます。これにより、ワークスペースで利用可能な情報に基づいてデフォルト値を埋めることができます。2つの「解決」メソッドが存在します。resolveDebugConfiguration は起動構成の変数が置換される前に呼び出され、resolveDebugConfigurationWithSubstitutedVariables はすべての変数が置換された後に呼び出されます。検証ロジックがデバッグ構成に追加の変数を挿入する場合は前者が、検証ロジックがすべてのデバッグ構成属性の最終的な値にアクセスする必要がある場合は後者が使用されなければなりません。

src/extension.ts 内の MockConfigurationProviderresolveDebugConfiguration を実装し、launch.json が存在しない状態でデバッグセッションが開始されたものの、アクティブなエディターで Markdown ファイルが開かれている場合を検知します。これは、ユーザーがエディターでファイルを開いていて、launch.json を作成せずにデバッグしたいという典型的なシナリオです。

デバッグ構成プロバイダーは、vscode.debug.registerDebugConfigurationProvider を通じて特定のデバッグタイプに登録されます(通常は拡張機能の activate 関数内)。DebugConfigurationProvider が十分に早く登録されるように、拡張機能はデバッグ機能が使用されるとすぐにアクティブ化される必要があります。これは、package.jsononDebug イベントの拡張機能アクティブ化を構成することで簡単に実現できます。

"activationEvents": [
    "onDebug",
    // ...
],

このすべてを網羅する onDebug は、デバッグ機能が使用されるとすぐにトリガーされます。これは、拡張機能の起動コストが低い(つまり、起動シーケンスに多くの時間を費やさない)限り、うまく機能します。デバッグ拡張機能の起動が重い場合(例えば言語サーバーを開始するためなど)、onDebug アクティブ化イベントは他のデバッグ拡張機能に悪影響を与える可能性があります。なぜなら、これはかなり早い段階でトリガーされ、特定のデバッグタイプを考慮しないためです。

重いデバッグ拡張機能に対するより良いアプローチは、より細かいアクティブ化イベントを使用することです。

  • onDebugInitialConfigurations は、DebugConfigurationProviderprovideDebugConfigurations メソッドが呼び出される直前に発生します。
  • onDebugResolve:type は、指定されたタイプの DebugConfigurationProviderresolveDebugConfiguration または resolveDebugConfigurationWithSubstitutedVariables メソッドが呼び出される直前に発生します。

経験則: デバッグ拡張機能のアクティブ化が軽い場合は onDebug を使用してください。重い場合は、DebugConfigurationProvider が対応する provideDebugConfigurations および resolveDebugConfiguration メソッドを実装しているかどうかに応じて、onDebugInitialConfigurationsonDebugResolve を使用してください。

デバッガー拡張機能の公開

デバッガー拡張機能を作成したら、マーケットプレイスに公開できます。

  • package.json 内の属性を更新し、デバッガー拡張機能の名前と目的を反映させます。
  • 拡張機能の公開に記載されている手順で、マーケットプレイスにアップロードします。

デバッガー拡張機能開発の代替アプローチ

見てきたように、デバッガー拡張機能の開発には通常、拡張機能とデバッグアダプターの両方を2つの並行セッションでデバッグする必要があります。上述の通り、VS Code はこれをうまくサポートしていますが、拡張機能とデバッグアダプターが1つのプログラムとして1つのデバッグセッションでデバッグできれば、開発はもっと簡単になるかもしれません。

このアプローチは、デバッグアダプターが TypeScript/JavaScript で実装されている限り、実際に簡単に行うことができます。基本的な考え方は、デバッグアダプターを拡張機能内で直接実行し、セッションごとに新しい外部デバッグアダプターを起動する代わりに、VS Code にそこに接続させることです。

このために、VS Code はデバッグアダプターの作成と実行方法を制御する拡張機能 API を提供しています。DebugAdapterDescriptorFactory には createDebugAdapterDescriptor メソッドがあり、デバッグセッションが開始されデバッグアダプターが必要になると VS Code によって呼び出されます。このメソッドは、デバッグアダプターの実行方法を記述するオブジェクト(DebugAdapterDescriptor)を返す必要があります。

現在、VS Code はデバッグアダプターを実行するための3つの異なる方法をサポートしており、したがって3つの異なる記述子タイプを提供しています。

  • DebugAdapterExecutable: このオブジェクトは、パス、オプションの引数、およびランタイムを持つ外部実行ファイルとしてのデバッグアダプターを記述します。実行ファイルはデバッグアダプタープロトコルを実装し、stdin/stdout を介して通信する必要があります。これは VS Code のデフォルトの動作モードであり、DebugAdapterDescriptorFactory が明示的に登録されていない場合、VS Code はこの記述子を package.json の対応する値とともに自動的に使用します。
  • DebugAdapterServer: このオブジェクトは、特定のローカルまたはリモートポートを介して通信するサーバーとして実行されるデバッグアダプターを記述します。vscode-debugadapter npm モジュールに基づくデバッグアダプター実装は、このサーバーモードを自動的にサポートします。
  • DebugAdapterInlineImplementation: このオブジェクトは、vscode.DebugAdapter インターフェイスを実装する JavaScript または Typescript オブジェクトとしてのデバッグアダプターを記述します。vscode-debugadapter npm モジュールのバージョン 1.38-pre.4 以降に基づくデバッグアダプター実装は、このインターフェイスを自動的に実装します。

Mock Debug は、3種類の DebugAdapterDescriptorFactories の例と、それらがどのように 'mock' デバッグタイプに対して登録されているかを示しています。使用する実行モードは、グローバル変数 runMode を設定することで、可能な値 externalserver、または inline のいずれかを選択できます。

開発においては、inline モードと server モードが特に便利です。なぜなら、単一のプロセス内で拡張機能とデバッグアダプターの両方をデバッグできるためです。

© . This site is unofficial and not affiliated with Microsoft.