动态

详情 返回 返回

在 VSCode 中實現 Jupyter Debug Adapter - 动态 详情

説起 VSCode 中廣受好評的功能,必須算上其優秀的調試(debug)功能,它擁有豐富的功能和直觀的用户界面。

更為難得的是,VSCode 為這套調試架構實現的插件化機制,使得我們可以很方便地為不同的自定義語言和框架實現調試功能,並具有統一且通用的用户界面。

20240625155834

本文將以 Jupyter 接入 VSCode 調試的功能為例,介紹如何在 VSCode 中實現 Jupyter Debug Adapter。

在 VSCode 中註冊 Debug Adapter Protocol

(https://code.visualstudio.com/api/extension-guides/debugger-extension)

從 VSCode 的相關介紹中我們可以看到,VSCode 的 Debug Adapter 是一個獨立的進程,它負責處理 IDE 和調試器之間的通信。而調試功能正是通過 Debug Adapter Protocol(DAP)來實現的,它是一個標準的調試協議,用於在 IDE 和調試器之間進行通信。

而為了給 VSCode 掛載自定義的調試器,我們可以通過 registerDebugAdapterDescriptorFactory 這個 API 來註冊我們的 Debug Adapter。由於 Jupyter 是基於 Python 語言的,因此這裏的 debugType 選擇 python。

之後我們就啓動 debugpy 後在 vscode 中設置好 launch.json 入口,調試普通 python 代碼了。

如果你想了解更多關於 Debug Adapter 的內容,可以參考 VSCode 官方文檔。

在 DAP 中,要實現一套完成的 debugger 流程,要求我們需要實現一些基本的功能,如:

  1. initialize:初始化調試器。
  2. setBreakpoints:設置斷點。
  3. variables/stackTrace/threads:獲取相關變量、調用棧等信息。
  4. stepInto/stepOut/stepOver:單步調試。
  5. break statement:中斷調試。

接下來我們就以 Jupyter 為例,看看如果要實現其他語言的 debug 接入,應該做哪些工作。

實現 Jupyter Debug Protocol

通過翻閲 Jupyter 文檔,我們可以知道,若要實現調試功能,其連接的 kernel 裏必須要支持 Jupyter Debug Protocol,這也是我們主要需要實現的地方。

我們可以通過以下的流程圖來了解 debug 從發起到結束的全過程:

20240625145827

  1. Initialize 階段。IDE 會向 kernel 發送 initialize_request,這個 request 將會幫我們創建一個新的 channel 用來交換調試信息。
  2. Attach 階段。該請求將會幫我們將建立好的 debug adaptor channel 與當前的 IDE 進行綁定,將在這裏負責 debug 交互的全過程。
  3. Configuring breakpoints and exception behavior 階段。這一部分是將 IDE 中設置的斷點等相關信息傳遞給 kernel,以便 kernel 能夠在適當的時候中斷。

    其中,setBreakpoints 等將會傳遞設置斷點的行/函數/異常處理等信息,dumpCell 是將單元格內的內容或狀態信息傳遞給 kernel。

    值得注意的是,這裏所有的請求 msg_type 將以 debug_event 或者 status 發出而不再是 request

    隨着 configureationDone 的出現,標誌着客户端配置過程的結束。

  4. Execute request 階段。這一步與正常的代碼執行完全一致,發出代碼執行請求,最終收穫到執行結果。

    但此時,如果我們設置了 breakpoint,該過程將在執行到斷點處時被一個內容為 stoppeddebug_event 給暫時 block。

  5. Pausing and extract context 階段。

    當 kernel 收到 stoppeddebug_event 後,將會暫停當前的執行,此時會發送一系列channel typecontrolcommand,將當前的各種相關 context 傳遞給 IDE。

    其中 variables 是獲取當前 code 中的變量信息,包括其名稱類型等。stackTrace 幫助 IDE 獲取當前的調用棧信息。scopes 則是獲取函數或變量的作用域信息。threads 則是獲取當前處理 debug 功能所處的線程信息。

  6. Dispose 階段。當所有斷點都被跳過後,之前的 execute_request 將被執行完成返回 execute_reply。至此 debug 流程結束。此時我們需要讓 IDE 發送 disconnect request 來關閉當前的 debug adaptor channel。如果我們想正確執行完整的 debug 生命週期。無論是否異常結束,都需要執行 disconnect request

瞭解了 Jupyter Debug Protocol 的全流程後,我們就可以開始着手為 VSCode 的交互實現做準備了。

利用 Jupyterlab 庫為溝通 kernel 提供 API 支持

上面我們簡要介紹了一下 Jupyter 調試過程的原理,而事實上我們並不需要完全從零開始實現 Jupyter Debug Protocol,因為開源社區裏已經有了很多現成的庫可以幫助我們實現這一功能。

Jupyterlab 是一個為 Jupyter 打造的第一方開發環境工具庫,它提供了豐富的 API 支持,可以幫助我們更方便地與 kernel 進行交互。

在 Jupyterlab 中,我們可以通過 jupyterlab/debugger 這個插件來實現對 Jupyter Debug Protocol 的支持。它提供了一套完整的調試功能,包括設置斷點、單步調試、查看變量等。

因此,我們真正需要實現的觸發事件與交互邏輯也就變得更為清晰了。只需要處理好 debugging 的這幾個實現即可:

  1. debugging 的開始與終止事件
  2. 斷點的設置與清除
  3. 代碼執行
  4. 變量、調用棧等的查看
  5. 單步調試(包括stepInto/stepOut/stepOver

因此,我們可以得出基於 jupyterlab 的調試器實現的基本流程:

20240625161141

實現 Debugging Manager 完成對調試器的管理

到了最終代碼實現的階段了。我們需要實現一個 Debugging Manager,用於通過registerDebugAdapterDescriptorFactory 註冊給 VSCode,管理調試器的啓動、停止、斷點設置等操作。

具體的工程實現方案可以有很多,具體就不展開了,這裏只 po 一下 vscode-jupyter 的實現方案:

20240625160558

vscode-jupyter 通過額外增加了 KernelDebugAdapter 類實現了 debug_event 消息的收發,通過 DebugCellController 類來管理單元格的 debug 執行信息,實現了不同消息走不同 Controller 的分離。

總結

通過本文的介紹,我們可以瞭解 VSCode 的 Debug Adapter 的實現原理,並以 Jupyter 為例,成功在 VSCode 中實現 Jupyter Debug Adapter,並實現完全的調試能力。

得益於 VSCode 靈活的調試注入能力,我們可以方便地為更多的語言,甚至是一些自定義框架實現調試能力,這可能可以為更多的小眾語言或框架的開發者帶來工作效率上的幫助。

Add a new 评论

Some HTML is okay.