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