偵錯工具延伸模組

Visual Studio Code 的偵錯架構讓擴充功能開發人員能夠輕鬆地將現有的偵錯工具整合到 VS Code 中,同時為所有偵錯工具提供統一的使用者介面。

VS Code 內建一個偵錯工具擴充功能,即 Node.js 偵錯工具擴充功能,它是 VS Code 支援的眾多偵錯功能絕佳的展示範例。

VS Code Debug Features

此截圖展示了以下偵錯功能:

  1. 偵錯組態管理。
  2. 用於啟動/停止與逐步執行的偵錯動作。
  3. 原始碼、函式、條件式、行內中斷點,以及記錄點 (log points)。
  4. 堆疊追蹤,包含多執行緒與多處理程序支援。
  5. 在檢視與懸浮視窗中瀏覽複雜的資料結構。
  6. 顯示於懸浮視窗或直接內嵌於原始碼中的變數值。
  7. 管理監看運算式。
  8. 用於互動式評估且具備自動完成功能的偵錯主控台。

本文件將協助您建立一個偵錯工具擴充功能,讓任何偵錯工具都能在 VS Code 中運作。

VS Code 的偵錯架構

VS Code 實作了一個通用的(不限程式語言)偵錯 UI,它是基於我們所引入的一套抽象協定來與偵錯工具後端進行通訊。由於偵錯工具通常不會直接實作此協定,因此需要一個中間層來將偵錯工具「適配」(adapt) 到該協定。這個中間層通常是一個與偵錯工具進行通訊的獨立處理程序。

VS Code Debug Architecture

我們將此中間層稱為偵錯轉接器 (Debug Adapter)(簡稱 DA),而在 DA 與 VS Code 之間所使用的抽象協定則稱為偵錯轉接器協定 (Debug Adapter Protocol)(簡稱 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 語法。
  • 用於初始 launch.json(由 VS Code 建立)的預設偵錯組態。
  • 使用者可新增至 launch.json 檔案中的偵錯組態程式碼片段 (snippets)。
  • 可在偵錯組態中使用的變數宣告。

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

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

  • 為初始 launch.json 動態產生預設偵錯組態(由 VS Code 建立)。
  • 動態決定所使用的偵錯轉接器。
  • 在偵錯組態傳遞給偵錯轉接器之前進行驗證或修改。
  • 與偵錯轉接器進行通訊。
  • 傳送訊息至偵錯主控台。

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

Mock Debug 擴充功能

由於從零開始建立偵錯轉接器對於本教學來說負擔較重,我們將從一個我們為教育目的建立的「偵錯轉接器入門套件」開始。它被稱為 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 是簡化 Node.js 環境偵錯轉接器開發的 NPM 模組。
  • src/mockRuntime.ts 是一個具有簡單偵錯 API 的 mock 執行時期。
  • 將執行時期適配到偵錯轉接器協定的程式碼位於 src/mockDebug.ts。您可以在此找到處理 DAP 各種請求的處理函式。
  • 由於偵錯工具擴充功能的實作位於偵錯轉接器中,因此根本不需要擴充功能程式碼(即執行於擴充功能主機處理程序中的程式碼)。不過,Mock Debug 有一個小型的 src/extension.ts,因為它說明了在偵錯工具擴充功能的擴充功能程式碼中可以做到什麼。

現在,透過選取 Extension 啟動組態並按下 F5 來建置並啟動 Mock Debug 擴充功能。最初,這會將 TypeScript 原始碼完整轉譯至 out 資料夾。完整建置後,會啟動一個監控工作 (watcher task),負責轉譯您所做的任何變更。

轉譯原始碼後,會出現一個標記為 "[Extension Development Host]" 的新 VS Code 視窗,Mock Debug 擴充功能現已在偵錯模式下執行。從該視窗開啟您的 mock test 專案與 readme.md 檔案,按 'F5' 啟動偵錯工作階段,然後進行逐步執行。

Debugging Extension and Server

由於您正在偵錯模式下執行擴充功能,現在可以設定並觸發 src/extension.ts 中的中斷點,但如上所述,擴充功能中並無太多有趣的程式碼在執行。有趣的程式碼執行於偵錯轉接器中,這是一個獨立的處理程序。

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

由於我們已經有一個作用中的擴充功能偵錯工作階段,VS Code 偵錯 UI 現在進入多工作階段 (multi session) 模式,這可以從「呼叫堆疊」檢視中看到 ExtensionServer 兩個偵錯工作階段名稱得到證實。

Debugging Extension and Server

現在我們能夠同時偵錯擴充功能與 DA。達到此目的的一種更快方法是使用 Extension + Server 啟動組態,它會自動啟動兩個工作階段。

另一種更簡單的偵錯擴充功能與 DA 的方法可參考下方說明

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

{
  "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 中的 mock 實作替換為與「真實」偵錯工具或執行時期對話的程式碼。這涉及理解並實作偵錯轉接器協定。關於此處的詳細資訊可參考此處

偵錯工具擴充功能 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 啟用事件可能會對其他偵錯擴充功能產生負面影響,因為它觸發得相當早,且未考慮特定偵錯類型。

對於昂貴的偵錯擴充功能,較好的做法是使用更細緻的啟用事件:

  • onDebugInitialConfigurationsDebugConfigurationProviderprovideDebugConfigurations 方法被呼叫前觸發。
  • onDebugResolve:type 在針對指定類型的 DebugConfigurationProviderresolveDebugConfigurationresolveDebugConfigurationWithSubstitutedVariables 方法被呼叫前觸發。

經驗法則:如果偵錯擴充功能的啟用成本低,請使用 onDebug。如果成本昂貴,請根據 DebugConfigurationProvider 是否實作對應的 provideDebugConfigurations 和/或 resolveDebugConfiguration 方法,選擇使用 onDebugInitialConfigurations 和/或 onDebugResolve

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

建立偵錯工具擴充功能後,您可以將其發佈至 Marketplace:

  • 更新 package.json 中的屬性,以反映您偵錯工具擴充功能的命名與目的。
  • 依照 發佈擴充功能 中的說明上傳至 Marketplace。

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

如我們所見,開發偵錯工具擴充功能通常涉及在兩個平行工作階段中分別偵錯擴充功能與偵錯轉接器。如上所述,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 物件。基於 1.38-pre.4 或更新版本 vscode-debugadapter npm 模組的偵錯轉接器實作會自動實作該介面。

Mock Debug 顯示了 三種類型的 DebugAdapterDescriptorFactories 範例,以及它們針對 'mock' 偵錯類型註冊的方式。使用的執行模式可以透過將全域變數 runMode 設定externalserverinline 其中之一來選擇。

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

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