從零開始- VSCode 插件運行機制

blank

從零開始- VSCode 插件運行機制

寫這篇文章是因為最近一段時間的工作涉及到Cloud Studio 插件這一塊的內容,舊的插件系統在面向用戶開放後暴露了安全性、擴展性等諸多問題。調研了幾個不同架構下IDE 的插件系統實現( Theia, VSCode 等),也大致閱讀了一遍VSCode 插件系統相關的源碼,在這裡做一個簡單的分享,個人水平有限,如有錯誤之處還請觀眾老爺們指點一下。


從加載一個插件開始

以我們熟悉的vscode-eslint為例,查看源碼會發現入口是extension.ts文件裡的activate函數,它的函數簽名像這樣:

activate(context: ExtensionContext):void

需要了解的一點是, package.json 裡的activationEvents 字段定義了插件的激活事件,考慮到性能問題,我們並不需要一啟動VSCode 就立即激活所有的插件。 activation-events定義了一組事件,當activationEvents字段指定的事件被觸發時才會激活相應的插件。包含了特定語言的文件被打開,或者特定的【命令】被觸發,以及某些視圖被切換甚至是一些自定義命令被觸發等等事件。

例如在vscode-java中,activationEvents字段的值為

"activationEvents":["onLanguage:java","onCommand:java.show.references","onCommand:java.show.implementations","onCommand:java.open.output","onCommand:java.open.serverLog","onCommand:java.execute.workspaceCommand","onCommand:java.projectConfiguration.update","workspaceContains:pom.xml","workspaceContains:build.gradle"]

其中包含languageId 為java 的文件被打開,以及由該插件自定義的幾個JDT 語言服務命令被觸發,和【工作空間】包含pom.xml/buld.gradle 這些事件。在以上事件被觸發時插件將會被激活。

這段邏輯被定義在src/vs/workbench/api/node/extHostExtensionService.ts 中

//由ExtensionHostProcessManager調用並傳入相應事件作為參數public$activateByEvent(activationEvent:string):Thenable<void>{return(this._barrier.wait().then(_=>this._activateByEvent(activationEvent,false)));}/*省略部分代碼*///實例化activatorthis._activator=newExtensionsActivator(this._registry,{/*省略部分代碼*/actualActivateExtension:(extensionDescription:IExtensionDescription,reason:ExtensionActivationReason):Promise<ActivatedExtension>=>{returnthis._activateExtension(extensionDescription,reason);}});//調用ExtensionsActivator的實例activator的方法激活插件private_activateByEvent(activationEvent:string,startup:boolean):Thenable<void>{constreason=newExtensionActivatedByEvent(startup,activationEvent);returnthis._activator.activateByEvent(activationEvent,reason);}

其中ExtensionsActivator 定義在src/vs/workbench/api/node/extHostExtensionActivator.ts 中

exportclassExtensionsActivator{constructor(registry: ExtensionDescriptionRegistry,// 既上文中实例化 activator 传的第二个参数
host: IExtensionsActivatorHost){this._registry=registry;this._host=host;}}

當調用activator.activateByEvent 方法時(既某個事件被觸發),activator 會獲取所有符合該事件的插件並逐一執行extHostExtensionService._activateExtension 方法(也就是activator.actualActivateExtension) ,中間省去獲取上下文,記錄日誌等一通操作後調用了extHostExtensionService._callActivateOptional 靜態方法

/*省略部分代碼*/// extension.ts裡的activate函數if(typeofextensionModule.activate==='function'){try{activationTimesBuilder.activateCallStart();logService.trace(`ExtensionService#_callActivateOptional${extensionId}`);//調用並傳入相關參數constactivateResult:Thenable<IExtensionAPI>=extensionModule.activate.apply(global,[context]);activationTimesBuilder.activateCallStop();activationTimesBuilder.activateResolveStart();returnPromise.resolve(activateResult).then((value)=>{activationTimesBuilder.activateResolveStop();returnvalue;});}catch(err){returnPromise.reject(err);}}

至此,插件被成功激活。

插件如何運行

再來看插件的代碼,插件中需要引入一個叫vscode 的模塊

import*asvscodefrom'vscode';

熟悉TypeScript 的朋友都知道這實際上只是引入了一個vscode.d.ts 類型聲明文件而已,這個文件包含了所有插件可用的API 及類型定義。

這些API 在插件import 時就被注入到了插件的運行環境中,它們定義在源碼src/vs/workbench/api/node/extHost.api.impl.ts 文件createApiFactory 函數中,通過defineAPI 函數統一被注入到插件運行環境。

functiondefineAPI(factory:IExtensionApiFactory,extensionPaths:TernarySearchTree<IExtensionDescription>,extensionRegistry:ExtensionDescriptionRegistry):void{// each extension is meant to get its own api implementationconstextApiImpl=newMap<string,typeofvscode>();letdefaultApiImpl:typeofvscode;//已被全局劫持過的requireconstnode_module=<any>require.__$__nodeRequire('module');constoriginal=node_module._load;//重寫Module.prototype._load方法node_module._load=functionload(request:string,parent:any,isMain:any){//模塊名不是vscode調用原方法返回模塊if(request!=='vscode'){returnoriginal.apply(this,arguments);}//這裡會為每一個插件生成一份獨立的API (為了安全考慮?)constext=extensionPaths.findSubstr(URI.file(parent.filename).fsPath);if(ext){letapiImpl=extApiImpl.get(ext.id);if(!apiImpl){// factory函數會返回所有APIapiImpl=factory(ext,extensionRegistry);extApiImpl.set(ext.id,apiImpl);}returnapiImpl;}/*省略部分代碼*/}}

實際上也很簡單,這裡的require已經被Microsoft/vscode-loader劫持了,所以在插件代碼中所有通過import (運行時會被編譯為require)引入的模塊都會經過這裡,通過這種方式將API注入到了插件執行環境中。

一般我們查看資源管理器或者進程會發現VSCode 創建了很多個子進程,且所有插件都在一個獨立的Extension Host 進程在運行,這是考慮到插件需要在一個與主線程完全隔離的環境下運行,保證安全性。那麼問題來了,我們調用vscode.window.setStatusBarMessage('Hello World') 時是怎麼在編輯器狀態欄插入消息的?前文我們提到所有的API 被定義在extHost.api.impl.ts 文件的createApiFactory 裡,例如vscode.window.setStatusBarMessage 的實現

constwindow:typeofvscode.window={/* 省略部分代码 */setStatusBarMessage(text: string,timeoutOrThenable?: number|Thenable<any>):vscode.Disposable{returnextHostStatusBar.setStatusBarMessage(text,timeoutOrThenable);},/* 省略部分代码 */}

實際調用的是extHostStatusBar.setStatusBarMessage 函數,而extHostStatusBar 則是ExtHostStatusBar 的實例

constextHostStatusBar=newExtHostStatusBar(rpcProtocol);

ExtHostStatusBar 包含了兩個方法createStatusBarEntry 和setStatusBarMessage,createStatusBarEntry 返回了一個ExtHostStatusBarEntry ,它被包裝了一層代理,在ExtHostStatusBar 被實例化化的同時也會產生一個ExtHostStatusBarEntry 實例

exportclassExtHostStatusBar{private_proxy:MainThreadStatusBarShape;private_statusMessage:StatusBarMessage;constructor(mainContext:IMainContext){//獲取代理this._proxy=mainContext.getProxy(MainContext.MainThreadStatusBar);//傳入this, StatusBarMessage中也隨即實例化了一個ExtHostStatusBarEntrythis._statusMessage=newStatusBarMessage(this);}/*省略部分代碼*/}classStatusBarMessage{private_item:StatusBarItem;private_messages:{message:string}[]=[];constructor(statusBar:ExtHostStatusBar){//調用createStatusBarEntrythis._item=statusBar.createStatusBarEntry(void0,ExtHostStatusBarAlignment.Left,Number.MIN_VALUE);}/*省略部分代碼*/}

所以當我們調用setStatusBarMessage 時,先是調用了this._statusMessage.setMessage 方法

// setStatusBarMessage 方法
letd=this._statusMessage.setMessage(text);

而this._statusMessage.setMessage 方法經過層層調用,最終調用了ExtHostStatusBarEntry 實例的update 方法,也就是前面的StatusBarMessage 構造函數中的this._item.update,而這裡就到了重頭戲,update 方法中包含了一個延時為0 的setTimeout :

this._timeoutHandle=setTimeout(()=>{this._timeoutHandle=undefined;// Set to status bar//還記得一開始實例化ExtHostStatusBar中的this._proxy = mainContext.getProxy(MainContext.MainThreadStatusBar);嗎this._proxy.$setEntry(this.id,this._extensionId,this.text,this.tooltip,this.command,this.color,this._alignment===ExtHostStatusBarAlignment.Left?MainThreadStatusBarAlignment.LEFT:MainThreadStatusBarAlignment.RIGHT,this._priority);},0);

這裡的this.proxy 就是ExtHostStatusBar 構造函數中的this.proxy

constructor(mainContext: IMainContext){this._proxy=mainContext.getProxy(MainContext.MainThreadStatusBar);this._statusMessage=newStatusBarMessage(this);}

這裡的IMainContext 其實就是繼承了IRPCProtocol 的一個別名而已,new ExtHostStatusBar 的參數是一個rpcProtocol 實例,它被定義在src/vs/workbench/services/extensions/node/rpcProtocol.ts 中,我們重點看一下getProxy 的實現

//我錯了,這裡才是重頭戲,VSCode源碼太繞了/( ㄒ o ㄒ )/~~publicgetProxy<T>(identifier:ProxyIdentifier<T>):T{//這裡只是根據對應的identifier生成對應的scope而已,插件調用和API的調用一模一樣比較方便一些constrpcId=identifier.nid;//例如StatusBar的identifier.nid就是'MainThreadStatusBar'if(!this._proxies[rpcId]){//緩存中沒有代理則生成新的代理this._proxies[rpcId]=this._createProxy(rpcId);}//返回代理後的對象returnthis._proxies[rpcId];}//創建代理private_createProxy<T>(rpcId:number):T{lethandler={get:(target:any,name:string)=>{// target即表示scope,name即為被調用方法名if(!target[name]&&name.charCodeAt(0)===CharCode.DollarSign){target[name]=(...myArgs:any[])=>{//插件中的API實際被代理到remoteCall,因為這是一個RPC協議returnthis._remoteCall(rpcId,name,myArgs);};}returntarget[name];}};//返回API代理returnnewProxy(Object.create(null),handler);}

_createProxy 返回的是一個代理對象,即它代理了主線程中真正實現這些API 的對象,例如'MainThreadStatusBar' 返回的是一個MainThreadStatusBarShape 類型的代理。

exportinterfaceMainThreadStatusBarShapeextendsIDisposable{$setEntry(id: number,extensionId: string,text: string,tooltip: string,command: string,color: string|ThemeColor,alignment: MainThreadStatusBarAlignment,priority: number):void;$dispose(id: number):void;}

插件API 定義中並沒有實現這個接口,它只需要被主線程中對應的模塊實現即可,前面我們說到setStatusMessage 最終調用了this._proxy.$setEntry。

_remoteCall 裡會調用RPCProcotol 的靜態方法serializeRequest 將rpcId 方法名以及參數序列化成一個Buffer 並發送給主線程。

constmsg=MessageIO.serializeRequest(req,rpcId,methodName,args,!!cancellationToken,this._uriReplacer);// 省略部分代码
this._protocol.send(msg);

關於主線程中接收到消息如何處理其實已經不用多說了,根據rpcId 找到對應的Services 以及方法,傳入參數即可。

在寫這篇文章的同時也在思考如何在瀏覽器與服務器端實現這樣一個插件加載和運行機制,順便寫了一個Demo extensions-example相比VSCode非常非常簡單,只是大致模擬了整個過程而已,實際還有很多需要完善的地方,有興趣的可以參考一下。

What do you think?

Written by marketer

blank

JavaScript 算法之最好、最壞時間複雜度分析

blank

第二屆SEE Conf 2019 精彩回顧(附PPT 及視頻)