寫在前面
插件 Helloworld 有一種示例用法:
// The module 'vscode' contains the VS Code extensibility API
import * as vscode from 'vscode';
var disposable = vscode.commands.registerCommand('extension.sayHello', () => {
// Display a message box to the user
vscode.window.showInformationMessage('Hello World!');
});
在插件進程環境,可以引入 vscode 模塊訪問插件可用的 API,好奇一點的話,能夠發現 node_modules 下並沒有 vscode 模塊,而且 vscode 模塊名也沒被 define() 過,看起來我們 require 了一個不存在的模塊,那麼,這個東西是哪裡來的?
P.S. 關於 define() 更多信息,請查看 [VS Code 源碼簡析 | Renderer Process 初始化](/articles/vs-code 源碼簡析/#articleHeader9)
一。require
尋著蛛絲馬跡,先看引入一個 Node 模塊時發生了什麼?
Node 通過 require(name) 函數來加載模塊,傳入模塊名 name,返回 Module 實例,大致過程如下:
-
name參數通過Module._resolveFilename()方法映射到完整文件路徑 -
如果
cache[fullName]存在,就返回cache[fullName].exports(優先走緩存),一個模塊只加載一次,從而提高模塊加載速度。不想走緩存的話,可以在require(name)之前把cache[fullName]先delete掉,例如delete require.cache[require.resolve('./my-module.js')] -
否則,加載相應文件中的源碼,並進行預處理(模塊級變量注入),見 Module.prototype.load
-
最後,編譯(執行)轉換過的源碼,返回
module.exports的值,見 Module.prototype._compile
P.S. 關於模塊緩存的更多信息,請查看 node.js require() cache - possible to invalidate?
看一個簡單場景,假設有兩個源碼文件:
// my-modue.js
module.exports = 'my-modue';
// index.js
const m = require('./my-module.js');
執行入口文件第一行 require('./my-modue.js') 的大致過程為:
// module.js
function require(path) {
return mod.require(path);
}
Module.prototype.require = function(path) {
return Module._load(path, this, /* isMain */ false);
}
Module._load = function(request, parent, isMain) {
var filename = Module._resolveFilename(request, parent, isMain);
var module = new Module(filename, parent);
Module._cache[filename] = module;
tryModuleLoad(module, filename);
return module.exports;
}
其中 tryModuleLoad() 具體如下:
function tryModuleLoad(module, filename) {
module.load(filename);
}
Module.prototype.load = function(filename) {
// 向上查找所有能訪問到的 node_modules 目錄
this.paths = Module._nodeModulePaths(path.dirname(filename));
// 按文件擴展名加載模塊
Module._extensions[extension](this, filename);
}
Module._extensions['.js'] = function(module, filename) {
// 讀源碼
var content = fs.readFileSync(filename, 'utf8');
// 編譯(執行)
module._compile(internalModule.stripBOM(content), filename);
}
Module.prototype._compile = function(content, filename) {
// 用 IIFE 包裹模塊源碼,注入模塊級變量,見 NativeModule.wrap()
var wrapper = Module.wrap(content);
// 相當於更安全的 eval(),編譯包好的 function 源碼,得到可執行的 Function 實例
var compiledWrapper = vm.runInThisContext(wrapper, {
filename: filename,
lineOffset: 0,
displayErrors: true
});
var dirname = path.dirname(filename);
// 要注入的模塊級 require() 方法
var require = internalModule.makeRequireFunction(this);
// 注入模塊參數,執行
result = compiledWrapper.call(this.exports, this.exports, require, this, filename, dirname);
// 這個返回值是被丟棄的,沒什麼用,模塊內容由 this.exports 帶出來
return result;
}
包在模塊源碼外面的 IIFE 是這樣:
NativeModule.wrap = function(script) {
// NativeModule.wrapper[0] = "(function (exports, require, module, __filename, __dirname) { "
// NativeModule.wrapper[1] = "\n});"
return NativeModule.wrapper[0] + script + NativeModule.wrapper[1];
};
簡單梳理下,其實整個過程的核心工作相當於:
// 1. 讀文件
const moduleScript = fs.readFileSync(fullFilename, 'utf8');
// 2. 構造模塊(隔離模塊作用域,聲明模塊級變量)
const wrapped = `(function (exports, require, module, __filename, __dirname) {
${moduleScript}
});`;
// 2.5. 編譯得到可執行模塊
const moduleFunction = eval(wrapped);
// 3. 執行(注入模塊級變量值)
let exportsHost = {};
moduleFunction.call(exportsHost, exportsHost);
const m = exportsHost;
那麼,既然 require 是個(模塊級的)局部變量,不方便做手腳(劫持/篡改),那麼一定是對 Module 乾了點什麼,才能夠支持加載不存在的虛擬模塊的
P.S. 別想通過劫持 require('internal/module').makeRequireFunction 工廠方法來篡改 require,因為不允許訪問 internal module:
NativeModule.nonInternalExists = function(id) {
return NativeModule.exists(id) && !NativeModule.isInternal(id);
};
NativeModule.isInternal = function(id) {
return id.startsWith('internal/');
};
在 Module._resolveFilename 時會被當做外人,從外部找,訪問不到我們想要的那個實例
二。extension API 注入
對 require('vscode') 的過程進行 debug,很容易發現做過手腳的地方:
// ref: src/vs/workbench/api/node/extHost.api.impl.ts
function defineAPI(factory: IExtensionApiFactory, extensionPaths: TernarySearchTree<IExtensionDescription>): void {
// each extension is meant to get its own api implementation
const extApiImpl = new Map<string, typeof vscode>();
let defaultApiImpl: typeof vscode;
const node_module = <any>require.__$__nodeRequire('module');
const original = node_module._load;
node_module._load = function load(request, parent, isMain) {
if (request !== 'vscode') {
return original.apply(this, arguments);
}
// get extension id from filename and api for extension
const ext = extensionPaths.findSubstr(parent.filename);
if (ext) {
let apiImpl = extApiImpl.get(ext.id);
if (!apiImpl) {
apiImpl = factory(ext);
extApiImpl.set(ext.id, apiImpl);
}
return apiImpl;
}
// fall back to a default implementation
if (!defaultApiImpl) {
defaultApiImpl = factory(nullExtensionDescription);
}
return defaultApiImpl;
};
}
Module._load() 方法被劫持了,遇到 vscode 返回一個虛擬模塊,叫做 apiImpl。注意,每個插件拿到的 API 都是獨立的(可能是出於插件安全隔離考慮,避免劫持 API 影響其它插件)
P.S. 注意,之所以要 require.__$__nodeRequire('module'),是因為 global.require 已經被劫持過了(見 [VS Code 源碼簡析 | Renderer Process 初始化](/articles/vs-code 源碼簡析/#articleHeader9) 的 loader 部分)。。。VS Code 團隊的路數狂野得很哪
三。插件機制初始化流程
之前在 [VS Code 啟動流程](/articles/vs-code 源碼簡析/#articleHeader9) 的 UI 佈局部分提到:
UI 入口
src/vs/workbench/electron-browser/bootstrap/index.html
src/vs/workbench/electron-browser/bootstrap/index.js
src/vs/workbench/workbench.main js index 文件
src/vs/workbench/electron-browser/main.ts
src/vs/workbench/electron-browser/shell.ts 界面與功能服務的接入點
src/vs/workbench/electron-browser/workbench.ts 創建界面
src/vs/workbench/browser/layout.ts 佈局計算,絕對定位
從創建
WorkbenchShell開始正式進入功能區 UI 佈局,UI 被稱為 Shell,算作用來承載功能的容器(「殼」)
即從 src/vs/workbench/electron-browser/shell.ts 開始著手界面的創建,以及界面與功能服務的對接。上次只關注了主啟動流程相關的部分,這次看看插件機制的初始化流程
插件機制初始化相關文件遞進關係:
src/vs/workbench/electron-browser/shell.ts 界面與功能服務的接入點
src/vs/workbench/services/extensions/electron-browser/extensionService.ts
src/vs/workbench/services/extensions/electron-browser/extensionHost.ts
src/vs/workbench/node/extensionHostProcess.ts
src/vs/workbench/node/extensionHostMain.ts
創建 ExtensionService
src/vs/workbench/electron-browser/shell.ts 的 createContents() 方法與 ExtensionService 有關,主要內容如下:
private createContents(parent: Builder): Builder {
// Instantiation service with services
const [instantiationService, serviceCollection] = this.initServiceCollection(parent.getHTMLElement());
}
private initServiceCollection(container: HTMLElement): [IInstantiationService, ServiceCollection] {
this.extensionService = instantiationService.createInstance(ExtensionService);
serviceCollection.set(IExtensionService, this.extensionService);
}
ExtensionService 來自 src/vs/workbench/services/extensions/electron-browser/extensionService.ts,關鍵部分如下:
lifecycleService.when(LifecyclePhase.Running).then(() => {
// delay extension host creation and extension scanning
// until after workbench is running
// 1. 初始化 extensionHost
this._startExtensionHostProcess([]);
// 2. 掃描已安裝的插件
this._scanAndHandleExtensions();
});
private _startExtensionHostProcess(initialActivationEvents: string[]): void {
// 乾掉已經存在的 ExtensionHost 進程
this._stopExtensionHostProcess();
// 創建並啟動 ExtensionHostProcessWorker
this._extensionHostProcessWorker = this._instantiationService.createInstance(ExtensionHostProcessWorker, this);
this._extensionHostProcessProxy = this._extensionHostProcessWorker.start().then(
//...
);
// 註冊按場景觸發激活的事件(如打開特定文件時才激活插件)
this._extensionHostProcessProxy.then(() => {
initialActivationEvents.forEach((activationEvent) => this.activateByEvent(activationEvent));
});
}
先通過 ExtensionHostProcessWorker 啟動 extensionHost 進程,同時掃描已安裝的插件,等 extensionHost 進程創建��畢之後註冊按需激活的插件(activationEvents 不為 ["*"] 的插件)
啟動 extensionHost 進程
ExtensionHostProcessWorker 來自 src/vs/workbench/services/extensions/electron-browser/extensionHost.ts,關鍵部分如下:
public start(): TPromise<IMessagePassingProtocol> {
const opts = {
env: objects.mixin(objects.deepClone(process.env), {
AMD_ENTRYPOINT: 'vs/workbench/node/extensionHostProcess'
})
};
// Run Extension Host as fork of current process
this._extensionHostProcess = fork(URI.parse(require.toUrl('bootstrap')).fsPath, ['--type=extensionHost'], opts);
}
這個 fork() 看似與 AMD_ENTRYPOINT 沒有聯繫,實際上,fork 得到的子進程入口是:
// URI.parse(require.toUrl('bootstrap')).fsPath
// 經 toUrl 轉換對應到
// out/bootstrap
即 src/bootstrap.js,關鍵部分如下:
require('./bootstrap-amd').bootstrap(process.env['AMD_ENTRYPOINT']);
先繞出再回來,是為了走 loader 執行入口文件:
var loader = require('./vs/loader');
exports.bootstrap = function (entrypoint) {
loader([entrypoint], function () { }, function (err) { console.error(err); });
};
那麼現在,踏進入口 src/vs/workbench/node/extensionHostProcess.ts:
// setup things
const extensionHostMain = new ExtensionHostMain(renderer.rpcProtocol, renderer.initData);
onTerminate = () => extensionHostMain.terminate();
return extensionHostMain.start();
又轉到了 ExtensionHostMain,對應源碼文件為 src/vs/workbench/node/extensionHostMain.ts:
public start(): TPromise<void> {
return this._extensionService.onExtensionAPIReady()
// 啟動最猴急的一批插件
.then(() => this.handleEagerExtensions())
.then(() => this.handleExtensionTests())
.then(() => {
this._logService.info(`eager extensions activated`);
});
}
// Handle "eager" activation extensions
private handleEagerExtensions(): TPromise<void> {
this._extensionService.activateByEvent('*', true).then(null, (err) => {
console.error(err);
});
return this.handleWorkspaceContainsEagerExtensions();
}
到這裡,無條件啟動的插件也激活了,插件機制初始化完成
激活插件
具體的插件激活過程相當繁瑣,因為支持 Extension Pack 型插件(允許插件依賴其它插件),所以激活插件還要處理插件依賴樹,等依賴的所有插件成功激活之後,才激活當前插件
P.S. 想要了解具體過程的話,可以看這兩個文件:
src/vs/workbench/api/node/extHostExtensionService.ts
src/vs/workbench/api/node/extHostExtensionActivator.ts
篇幅限制,我們跳過繁瑣的依賴處理環節,直接看加載插件 pkg.main 入口文件的部分:
private _doActivateExtension() {
// require 加載插件入口文件
loadCommonJSModule(this._logService, extensionDescription.main, activationTimesBuilder),
this._loadExtensionContext(extensionDescription).then(values => {
// 執行其 activate() 方法
return ExtHostExtensionService._callActivate(this._logService, extensionDescription.id, <IExtensionModule>values[0], <IExtensionContext>values[1], activationTimesBuilder);
});
}
// 加載入口文件
function loadCommonJSModule() {
r = require.__$__nodeRequire<T>(modulePath);
return TPromise.as(r);
}
// 執行約定的 activate() 方法
private static _callActivateOptional() {
if (typeof extensionModule.activate === 'function') {
const activateResult: TPromise<IExtensionAPI> = extensionModule.activate.apply(global, [context]);
}
}
直接 node require 執行插件入口文件得到模塊實例,然後 apply 調用其 activate 方法,插件跑起來了
四。進程模型
至此,我們了解到 VS Code 裡至少有 3 個進程:
-
Electron Main Process:App 主進程
-
Electron Renderer Process:UI 進程
-
Extension Host Process:插件宿主進程,給插件提供執行環境
其中 Extension Host Process(每個 VS Code 窗體)只存在一個,所有插件都在該進程執行,而不是每個插件一個獨立進程
注意,插件宿主進程是個普通的 Node 進程(childProcess.fork() 出來的),並不是 Electron 進程,而且被限制了不能使用 electron:
// 環境變量
ELECTRON_RUN_AS_NODE: '1'
所以不能在插件運行環境使用 require('electron').BrowserWindow.getAllWindows() 曲線改 UI
P.S. 關於插件定製 UI 能力的討論,見 access electron API from vscode extension
進程間通信方式
<Electron IPC>
Main ---------------- Renderer
|
|
| <Child Process IPC>
|
|
Extension Host
其中,Extension Host 與 Main 之間的通信是通過 fork() 內置的 IPC 來完成的,具體如下:
// Support logging from extension host
this._extensionHostProcess.on('message', msg => {
if (msg && (<IRemoteConsoleLog>msg).type === '__$console') {
this._logExtensionHostMessage(<IRemoteConsoleLog>msg);
}
});
這裡只是單向通信(插件 -> Main),實際上可以通過 this._extensionHostProcess.send({msg}) 完成另一半(Main -> 插件)
P.S. 關於進程間通信的更多信息,請查看 [Nodejs 進程間通信](/articles/nodejs 進程間通信/)
暫無評論,快來發表你的看法吧