參加你附近的 ,瞭解 VS Code 中的 AI 輔助開發。

偵錯程式擴充套件

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` 參考中找到更多資訊。

除了上述純宣告性貢獻之外,除錯擴充套件 API 還支援以下基於程式碼的功能:

  • VS Code 建立的初始 launch.json 的動態生成的預設除錯配置。
  • 動態確定要使用的除錯介面卡。
  • 在將除錯配置傳遞給除錯介面卡之前進行驗證或修改。
  • 與除錯介面卡通訊。
  • 向除錯控制檯傳送訊息。

本文件的其餘部分將介紹如何開發偵錯程式擴充套件。

模擬除錯擴充套件

由於從頭開始建立一個除錯介面卡對於本教程來說有點繁重,我們將從一個簡單的 DA 開始,我們將其建立為一個教育性的“除錯介面卡入門工具包”。它被稱為 *模擬除錯*,因為它不與真正的偵錯程式通訊,而是模擬一個。模擬除錯模擬了一個偵錯程式,並支援單步執行、繼續、斷點、異常和變數訪問,但它沒有連線到任何真正的偵錯程式。

在深入瞭解模擬除錯的開發設定之前,讓我們首先從 VS Code Marketplace 安裝一個預構建版本並試用它:

  • 切換到“擴充套件”檢視並輸入“mock”搜尋 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 的原始碼並在 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 擴充套件的貢獻。
    • `compile` 和 `watch` 指令碼用於將 TypeScript 原始碼轉換為 `out` 資料夾並監視後續的原始碼修改。
    • 依賴項 `vscode-debugprotocol`、`vscode-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 擴充套件現在以除錯模式執行。從該視窗中開啟帶有 `readme.md` 檔案的 `mock test` 專案,使用“F5”啟動除錯會話,然後單步執行它。

Debugging Extension and Server

由於您在除錯模式下執行擴充套件,因此現在可以在 `src/extension.ts` 中設定並命中斷點,但正如我上面提到的,在擴充套件中執行的有趣程式碼不多。有趣的程式碼在除錯介面卡中執行,這是一個單獨的程序。

為了除錯除錯介面卡本身,我們必須在除錯模式下執行它。最簡單的方法是讓除錯介面卡以 *伺服器模式* 執行,並配置 VS Code 連線到它。在您的 VS Code vscode-mock-debug 專案中,從下拉選單中選擇啟動配置 **Server**,然後按下綠色的啟動按鈕。

由於我們已經有一個活動的擴充套件除錯會話,VS Code 偵錯程式 UI 現在進入 *多會話* 模式,這透過在 CALL STACK 檢視中看到兩個除錯會話 **Extension** 和 **Server** 的名稱來表示。

Debugging Extension and Server

現在我們能夠同時除錯擴充套件和 DA。更快的方法是使用 **Extension + Server** 啟動配置,它會自動啟動兩個會話。

除錯擴充套件和 DA 的另一種甚至更簡單的方法可以在下方找到。

在檔案 `src/mockDebug.ts` 中的方法 `launchRequest(...)` 的開頭設定一個斷點,最後一步是配置模擬偵錯程式以連線到 DA 伺服器,方法是為您的模擬測試啟動配置新增一個埠 `4711` 的 `debugServer` 屬性。

{
  "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.ts` 和 `src/mockRuntime.ts` 中的模擬實現。這涉及理解和實現除錯介面卡協議。有關此內容的更多詳細資訊可以在此處找到。

偵錯程式擴充套件的 package.json 結構

除了提供除錯介面卡的特定於偵錯程式的實現之外,偵錯程式擴充套件還需要一個 `package.json` 檔案來貢獻各種與除錯相關的貢獻點。

因此,讓我們仔細看看 Mock Debug 的 `package.json`。

像每個 VS Code 擴充套件一樣,`package.json` 聲明瞭擴充套件的基本屬性 **name**、**publisher** 和 **version**。使用 **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** 部分。在這裡,一個偵錯程式以除錯 **型別** `mock` 引入。使用者可以在啟動配置中引用此型別。可選屬性 **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,它作為 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` 在呼叫 `DebugConfigurationProvider` 的 `provideDebugConfigurations` 方法之前觸發。
  • `onDebugResolve:type` 在呼叫指定型別的 `DebugConfigurationProvider` 的 `resolveDebugConfiguration` 或 `resolveDebugConfigurationWithSubstitutedVariables` 方法之前觸發。

**經驗法則:** 如果除錯擴充套件的啟用成本低,請使用 `onDebug`。如果成本高,則根據 `DebugConfigurationProvider` 是否實現相應的 `provideDebugConfigurations` 和/或 `resolveDebugConfiguration` 方法,使用 `onDebugInitialConfigurations` 和/或 `onDebugResolve`。

釋出您的偵錯程式擴充套件

建立偵錯程式擴充套件後,您可以將其釋出到市場:

  • 更新 `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` 設定為 `external`、`server` 或 `inline` 中的一個來選擇要使用的執行模式。

對於開發,`inline` 和 `server` 模式特別有用,因為它們允許在單個程序中除錯擴充套件和除錯介面卡。