已触发了一个断点 vs_VSCode源码分析-断点调试
背景
今年年初,有幸參與了阿里集團IDE 共建項目組,打造阿里生態體系內的公共IDE底層,而作為一款面向開發者的IDE,調試能力的支持一定程度上決定著一款IDE的開發體驗;VSCode作為微軟體系下一款當前最熱的IDE開發工具,在調試領域上的探索實踐是很好的學習案例,有道是:借他山之石,逐已身之玉,故本文著力于分析VCode中調試功能的設計與實現,讓后來的人可以較為簡單的理解調試這件事情是如何做到的。
源碼解析
了解VSCode中的實現,最簡單的方式便是直接調試VSCode源碼工程,到VSCode官方github下載對應源碼工程 microsoft/vscode,下面的分析以Tag 0.10.11 版本為例,可跳過該部分直接看下面結論。
調試技巧:在安裝依賴后點擊`調試`按鈕,先點擊`Launch VS Code`,待`VSCode-OSS`啟動后打開一個簡單的調試項目,再點擊`Attach to Extension Host`對ExtensionHost進程進行調試,此時便可針對調試的核心代碼進行調試了。了解調試,很簡單便可以想到先從Electron的render進程著手,搜索Debug相關代碼可以發現,debugViewlet.ts 文件中針對Electron的Render進程的頁面進行了Action注冊及綁定,如下:
// vscode/src/vs/workbench/contrib/debug/browser/debugViewlet.ts@memoize private get startAction(): StartAction {return this._register(this.instantiationService.createInstance(StartAction, StartAction.ID, StartAction.LABEL)); }同時,在后續調試元素創建過程中針對StartDebugActionItem按鈕的action類型綁定對應的this.actionRunner.run(this.action, this.context) 監聽函數, 代碼見:
// vscode/src/vs/workbench/contrib/debug/browser/debugActionItems.ts:77 this.toDispose.push(dom.addDisposableListener(this.start, dom.EventType.CLICK, () => {this.start.blur();this.actionRunner.run(this.action, this.context); }));這里的actionRunner最終執行到的是基于AbstractDebugAction抽象類封裝出來StartAction類,通過workbench.action.debug.start這個ID進行直接的關聯,即當用戶點擊調試開始按鈕時,便會觸發StartAction類中的run方法,執行vs/workbench/contrib/debug/common/debugUtils模塊封裝的startDebugging方法,這里基于debugUtils模塊封裝的意義在于更好的復用于各個模塊,將獲取啟動參數及啟動調試的邏輯抽象到工具類中實現。
接下來便到DebugService中的執行邏輯初始化操作,初始化過程會保證在文檔保存并且插件正常加載之后執行,通過textFileService及extensionService實現,見:
// vscode/src/vs/workbench/contrib/debug/electron-browser/debugService.ts// 保持當前文件 return this.textFileService.saveAll().then(() => this.configurationService.reloadConfiguration(launch ? launch.workspace : undefined ).then(() => {// 等待已安裝的插件注冊完畢return this.extensionService.whenInstalledExtensionsRegistered().then(() => {...}) })在啟動調試進程的時候可能存在復合類型的調試配置,即多task,需要在錯誤檢查后分別啟動,這里不做贅述。
執行完畢,此時便會返回this.createSession(launch, config, noDebug, parentSession);函數的執行結果作為返回值,進到createSession函數,可以發現該函數主要針對調試類型查找對應的debuggers,同時針對配置文件進行處理,調整變量即運行prelaunch任務,代碼見: vscode/src/vs/workbench/contrib/debug/electron-browser/debugService.ts:341。
處理完成后,執行this.doCreateSession(workspace, { resolved: resolvedConfig, unresolved: unresolvedConfig }, parentSession)函數創建新的調試會話,同時對會話進行對應的事件監聽,接下來便是通過this.launchOrAttachToSession(session)方法啟動對應的調試器
// vscode/src/vs/workbench/contrib/debug/electron-browser/debugService.ts private launchOrAttachToSession(session: IDebugSession, focus = true): Promise<void> { // 根據配置類型獲取調試器const dbgr = this.configurationManager.getDebugger(session.configuration.type);// 初始化會話return session.initialize(dbgr!).then(() => {// 會話啟動return session.launchOrAttach(session.configuration).then(() => {if (focus) {this.focusStackFrame(undefined, undefined, session);}});}).then(undefined, err => {// 出現錯誤,會話關閉session.shutdown();return Promise.reject(err);}); }進到 session.initialize方法,隨著startSession方法的調用,render進程會通過JSONRPC方法調用向main進程發送啟動指令
// vscode/src/vs/workbench/api/browser/mainThreadDebugService.ts public startSession(): Promise<void> {return Promise.resolve(this._proxy.$startDASession(this._handle, this._ds.getSessionDto(this._session))); }注: VSCode中帶$符號的調用基本上都為RPC調用,詳細實現可見: vscode/src/vs/workbench/services/extensions/common/rpcProtocol.ts進到main進程的vscode/src/vs/workbench/contrib/debug/node/debugAdapter.ts文件,在startSession方法處打上斷點,可以看到,對于command類型為node的adapter進程,采用cp.fork的方法啟動,其他的采用 cp.spawn的方式啟動,此時會針對進程綁定對應的監聽函數,輸出該輸出的內容,同時連接對應DebugAdapter(后面簡稱DA)的輸入輸出流,見:
// vscode/src/vs/workbench/contrib/debug/node/debugAdapter.ts this.connect(this.serverProcess.stdout, this.serverProcess.stdin);針對客戶端發來的消息,需要通過調用StreamDebugAdapter類下的sendMessage方法進行DAP協議轉換,從DA發送到主進程的消息也需要通過handleData方法進行數據轉換。
基于StreamDebugAdapter有SocketDebugAdapter及ExecutableDebugAdapter兩種實現的封裝,分別實現socket監聽及stdin/stdout兩種方式的通信方式,基于這兩種通信方式基本可以覆蓋所有消息通信場景。接著便是客戶端ready后發送initialize指令,DA返回initialize結果,后續的通信亦同理通過該通道進行。
結論
最終我們可以分析得到如下時序圖:
從時序圖我們可以看出,整個調試的流程無非就是簡單的視圖層到調試進程間的通訊,調試的核心在于在多個調試器中實現了統一的數據傳輸協議,即DAP(Debug Adapter Protocol) 協議。
什么是DAP?
調試適配器協議(DAP)背后的想法是抽象開發工具的調試支持與調試器或運行時通信協議的方式。對于現有的調試器想要去快速去實現這套協議是不現實的,故我們寧愿去實現一個調試的中間層,即一個調試適配器,去使現有的調試器去適應這套調試適配器協議。 調試適配器協議讓開發工具實現通用調試器成為可能,同時對應的調試器也可以通過調試適配器與不同的調試器通信。調試適配器可以在多個開發工具中重復使用,這大大減少了在不同工具中支持新調試器的工作量。上文引用簡單翻譯自[DAP 協議介紹頁](Debug Adapter Protocol),很容易理解,通過實現適配器,讓不同的調試器實現在工具端上的接入達到統一,即由適配器負責去管理上下游消息通信時的數據處理及轉換工作,從多個IDE工具自己去適配調試器,逐漸演變為多個IDE工具去適配同一套調試協議,如下圖所示
圖右可以看出,從左側調試UI消息到達對應調試器(Debugger)中間通過Adaptor層統一進行消息的轉換,一旦調試相關的消息通訊協議達到一定完成度,工具側便可無需進行任何修改支持多個調試器中的調試邏輯。
如何使用DAP?
知道了DAP協議帶來的好處,在開發一款IDE或開發工具時,我們該如何去使用它呢?
以`Node`調試為例,我創建了一個Web版本的Demo工程簡單對DAP協議進行驗證,見 monaco-node-debug-sample,安裝依賴后運行`yarn start`即可運行項目,接下來跟隨我一步步實現一個適配DAP的調試工具;
實現一個例子
視圖層
UI部分我魔改了`Monaco`的Web版本作為界面代碼展示及斷點操作區,同時簡單實現了基本的調試按鈕UI及控制臺,如圖所示:
詳細代碼可見 client.ts
消息通訊層
消息層引入`reconnecting-websocket` 模塊作為websocket鏈接工具,創建DAP專用的通訊渠道,視圖層通過監聽該消息下的信息響應對應的調試操作,將對應的調試指令轉化為視圖可讀的信息(正式項目中可將這層邏輯也下層于Node層實現),如圖所示:
解析上我們只需根據 DebugProtocol 解析我們需要的調試信息即可,這里我們簡單實現一次調試下必要的一些調試信息即可;
服務層
服務層我們需要實現對應在`/dap`路徑下的調試服務器,新建一個對應的 DebugSession 類用于創建調試鏈接,實現如下幾個功能:
1. 接收`initialize`指令,啟動`Debug Adaptor`進程;
2. 接收`Debug Adaptor`進程消息,轉發到視圖層Socket;
3. 接收視圖層消息,轉發至`Debug Adaptor`進程;
因為調試的邏輯基本上均為異步響應,故Demo中沒有實現完整的JSONRPC通訊;
調試進程
調試進程需實現 DebugAdapter 類,用于`Lanunch` 或 `Attach` 調試器,通過消息轉化邏輯將對應的JSON消息轉換為調試器可讀的信息,以Node為例,需要將如下消息:
{"seq": 153,"type": "request","command": "next","arguments": {"threadId": 3} }轉換為`Node Debugger` 可讀的消息:
Content-Length: 119rn rn {"seq": 153,"type": "request","command": "next","arguments": {"threadId": 3} }同時,Debug Adaptor 需要管理與調試器間的進程通訊,所有的調試器均需要在子進程中啟動,并通過進程間通信來實現消息傳遞,基礎的啟動邏輯如下:
調試器引入了VSCode中使用的node-debug2模塊作為調試器,支持Node 7.6+ 版本調試,通過進程中的stream.Writable及stream.Readable接口接口讀寫對應的進程消息實現通信;
以上即可完整實現DAP的調試鏈路;
效果
效果演示如下:
調試器上可以斷點到界面斷點對應的位置,輸出對應的調試堆棧,同時,通過在控制臺中執行`a`變量取值操作,也可以獲取到在Node執行階段對應的值,如圖所示:
完整效果體驗可至, github/monaco-node-debug-sample 下載對應源碼查看。
未來能做什么?
在工具端支持DAP協議,能夠輕松的去適配多個語言環境下的調試場景;在調試器端支持DAP協議,則能讓更多的工具能便捷的接入,達到接入層的統一;
未來我們希望做的事情:
1. 在Web環境中有許多針對頁面的直接調試場景,我們希望從中探索模擬器調試場景,探索IDE在模擬器上是否能達到與網頁調試一樣的調試體驗;
2. 實現Web端與Electron端統一的調試體驗;
3. 支持遠程調試協議,即可通過本地調試界面,鏈接到遠程的調試服務器中進行調試;
4. 支持多個DebugSession調試,同時支持subDebugSession特性;
更多場景,期待留言分享討論~
目前我們正在建設阿里經濟體體系下的IDE底層,歡迎有志之士簡歷至 danwu.wdw@alibaba-inc.com
總結
以上是生活随笔為你收集整理的已触发了一个断点 vs_VSCode源码分析-断点调试的全部內容,希望文章能夠幫你解決所遇到的問題。
- 上一篇: 将.ncm文件转换为.mp3文件
- 下一篇: 填坑-十万个为什么?(13)