🚀 在 VS Code 中

偵錯工具擴充功能

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,此 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 結構描述建構 $refdefinition 不受支援。
  • VS Code 建立的初始 launch.json 的預設偵錯組態。
  • 使用者可以新增至 launch.json 檔案的偵錯組態程式碼片段。
  • 可以在偵錯組態中使用的變數宣告。

您可以在 contributes.breakpointscontributes.debuggers 參考資料中找到更多資訊。

除了上述純粹的宣告式貢獻之外,偵錯工具擴充功能 API 還啟用了以下基於程式碼的功能

  • 針對 VS Code 建立的初始 launch.json 動態產生的預設偵錯組態。
  • 動態判斷要使用的偵錯配接器。
  • 在偵錯組態傳遞至偵錯配接器之前,先驗證或修改偵錯組態。
  • 與偵錯配接器通訊。
  • 將訊息傳送至偵錯主控台。

在本文件的其餘部分,我們將說明如何開發偵錯工具擴充功能。

Mock Debug 擴充功能

由於從頭開始建立偵錯配接器對於本教學課程來說有點繁重,因此我們將從一個簡單的 DA 開始,我們已將其建立為教育性的「偵錯配接器入門套件」。它稱為 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

在 VS Code 中開啟專案資料夾 vscode-mock-debug

套件中有什麼?

  • package.json 是 mock-debug 擴充功能的資訊清單
    • 它列出了 mock-debug 擴充功能的貢獻。
    • compilewatch 指令碼用於將 TypeScript 原始碼轉譯到 out 資料夾中,並監看後續的原始碼修改。
    • 相依性 vscode-debugprotocolvscode-debugadaptervscode-debugadapter-testsupport 是 NPM 模組,可簡化基於節點的偵錯配接器的開發。
  • src/mockRuntime.ts 是一個具有簡單偵錯 API 的模擬執行階段。
  • 將執行階段調整為偵錯配接器通訊協定的程式碼位於 src/mockDebug.ts 中。您可以在這裡找到各種 DAP 要求的處理常式。
  • 由於偵錯工具擴充功能的實作位於偵錯配接器中,因此完全不需要擴充功能程式碼 (即在擴充功能主機處理程序中執行的程式碼)。但是,Mock Debug 有一個小的 src/extension.ts,因為它說明了可以在偵錯工具擴充功能的擴充功能程式碼中完成哪些操作。

現在,透過選取 [擴充功能] 啟動組態並按下 F5 來建置並啟動 Mock Debug 擴充功能。最初,這會將 TypeScript 來源完整轉譯到 out 資料夾中。完整建置後,會啟動監看器工作,以轉譯您所做的任何變更。

轉譯來源之後,會出現一個標示為「[擴充功能開發主機]」的新 VS Code 視窗,其中 Mock Debug 擴充功能現在以偵錯模式執行。從該視窗開啟您的 mock test 專案和 readme.md 檔案,使用 'F5' 啟動偵錯工作階段,然後逐步執行它

Debugging Extension and Server

由於您是以偵錯模式執行擴充功能,因此您現在可以在 src/extension.ts 中設定和命中中斷點,但正如我上面提到的,擴充功能中沒有太多有趣的程式碼在執行。有趣的程式碼在偵錯配接器中執行,偵錯配接器是一個獨立的處理程序。

為了偵錯偵錯配接器本身,我們必須以偵錯模式執行它。最簡單的方法是以伺服器模式執行偵錯配接器,並設定 VS Code 以連接到它。在您的 VS Code vscode-mock-debug 專案中,從下拉式選單中選取 [伺服器] 啟動組態,然後按下綠色的 [啟動] 按鈕。

由於我們已經有一個用於擴充功能的現有偵錯工作階段,因此 VS Code 偵錯工具 UI 現在進入多工作階段模式,這表示在 [呼叫堆疊] 檢視中會顯示兩個偵錯工作階段 [擴充功能] 和 [伺服器] 的名稱

Debugging Extension and Server

現在我們可以同時偵錯擴充功能和 DA。更快到達這裡的方法是使用 [擴充功能 + 伺服器] 啟動組態,它會自動啟動這兩個工作階段。

偵錯擴充功能和 DA 的另一種更簡單的方法可以在下方找到。

在檔案 src/mockDebug.ts 中方法 launchRequest(...) 的開頭設定一個中斷點,並在最後一個步驟中將 debugServer 屬性新增至您的 mock test 啟動組態,以將 mock 偵錯工具設定為連接到 DA 伺服器,連接埠為 4711

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

如果您現在啟動此偵錯組態,VS Code 不會將 mock 偵錯配接器作為獨立的處理程序啟動,而是直接連接到已在執行之伺服器的本機連接埠 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 下引入了一個偵錯工具。使用者可以在啟動組態中參考此類型。選用屬性 label 可用於在 UI 中顯示偵錯類型時給予其一個友善的名稱。

由於偵錯工具擴充功能使用偵錯配接器,因此偵錯配接器的程式碼的相對路徑會作為 program 屬性給定。為了使擴充功能成為獨立的,應用程式必須位於擴充功能資料夾內。依照慣例,我們將此應用程式保留在名為 outbin 的資料夾中,但您可以自由使用不同的名稱。

由於 VS Code 在不同的平台上執行,因此我們必須確保 DA 程式也支援不同的平台。為此,我們有以下選項

  1. 如果程式是以平台獨立的方式實作的,例如在所有支援平台上可用的執行階段上執行的程式,則您可以透過 runtime 屬性指定此執行階段。截至今日,VS Code 支援 nodemono 執行階段。我們上面的 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,它實作為一個 mono 應用程式,在 macOS 和 Linux 上需要執行階段,但在 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.json 中靜態定義 launch.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 的初始偵錯組態,例如基於工作區中可用的某些內容資訊。
  • 啟動組態可以在用於啟動新的偵錯工作階段之前解析 (或修改)。這允許根據工作區中可用的資訊填入預設值。存在兩種解析方法:resolveDebugConfiguration 在變數在啟動組態中取代之前呼叫,resolveDebugConfigurationWithSubstitutedVariables 在所有變數都已取代之後呼叫。如果驗證邏輯將額外的變數插入到偵錯組態中,則必須使用前者。如果驗證邏輯需要存取所有偵錯組態屬性的最終值,則必須使用後者。

src/extension.ts 中的 MockConfigurationProvider 實作 resolveDebugConfiguration 以偵測在沒有 launch.json 存在但活動編輯器中開啟了 Markdown 檔案時啟動偵錯工作階段的情況。這是使用者在編輯器中開啟檔案且只想對其進行偵錯而不建立 launch.json 的典型案例。

偵錯組態提供者透過 vscode.debug.registerDebugConfigurationProvider 針對特定偵錯類型註冊,通常在擴充功能的 activate 函式中。為了確保 DebugConfigurationProvider 盡早註冊,必須在偵錯功能一使用時就啟動擴充功能。這可以透過在 package.json 中為 onDebug 事件設定擴充功能啟動來輕鬆實現

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

此萬用字元 onDebug 會在任何偵錯功能一使用時觸發。只要擴充功能的啟動成本不高 (即不會在其啟動序列中花費大量時間),這就可以正常運作。如果偵錯工具擴充功能的啟動成本很高 (例如,由於啟動語言伺服器),則 onDebug 啟動事件可能會對其他偵錯工具擴充功能產生負面影響,因為它觸發得較早,並且未考慮特定的偵錯類型。

昂貴的偵錯工具擴充功能的更好方法是使用更細微的啟動事件

  • onDebugInitialConfigurations 在呼叫 DebugConfigurationProviderprovideDebugConfigurations 方法之前觸發。
  • onDebugResolve:type 在呼叫指定類型的 DebugConfigurationProviderresolveDebugConfigurationresolveDebugConfigurationWithSubstitutedVariables 方法之前觸發。

經驗法則: 如果偵錯工具擴充功能的啟動成本不高,請使用 onDebug。如果啟動成本很高,請使用 onDebugInitialConfigurations 和/或 onDebugResolve,具體取決於 DebugConfigurationProvider 是否實作了對應的方法 provideDebugConfigurations 和/或 resolveDebugConfiguration

發佈您的偵錯工具擴充功能

建立偵錯工具擴充功能後,您可以將其發佈到市集

  • 更新 package.json 中的屬性,以反映偵錯工具擴充功能的命名和用途。
  • 依照發佈擴充功能中所述上傳到市集。

開發偵錯工具擴充功能的替代方法

正如我們所看到的,開發偵錯工具擴充功能通常涉及在兩個平行工作階段中偵錯擴充功能和偵錯配接器。如上所述,VS Code 非常好地支援此功能,但如果擴充功能和偵錯配接器都是可以在一個偵錯工作階段中偵錯的程式,則開發可能會更容易。

事實上,只要您的偵錯配接器是以 TypeScript/JavaScript 實作的,這種方法就很容易做到。基本概念是在擴充功能內直接執行偵錯配接器,並讓 VS Code 連接到它,而不是為每個工作階段啟動新的外部偵錯配接器。

為此,VS Code 提供了擴充功能 API 來控制偵錯配接器的建立和執行方式。DebugAdapterDescriptorFactory 有一個方法 createDebugAdapterDescriptor,當偵錯工作階段啟動且需要偵錯配接器時,VS Code 會呼叫此方法。此方法必須傳回一個描述器物件 (DebugAdapterDescriptor),用於描述偵錯配接器的執行方式。

如今,VS Code 支援三種不同的偵錯配接器執行方式,因此提供三種不同的描述器類型

  • 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 顯示了 三種類型 DebugAdapterDescriptorFactory 的範例,以及它們如何針對 'mock' 偵錯類型註冊。要使用的執行模式可以透過將全域變數 runMode 設定為可能值 externalserverinline 之一來選取。

對於開發而言,inlineserver 模式特別有用,因為它們允許在單一處理程序中偵錯擴充功能和偵錯配接器。