API注入机制及插件启动流程_VSCode插件开发笔记2

2019-06-12 14:35:28 浏览数 (1)

写在前面

插件Helloworld有一种示例用法:

代码语言:javascript复制
// 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初始化

一.require

寻着蛛丝马迹,先看引入一个Node模块时发生了什么?

Node通过require(name)函数来加载模块,传入模块名name,返回Module实例,大致过程如下:

  1. name参数通过Module._resolveFilename()方法映射到完整文件路径
  2. 如果cache[fullName]存在,就返回cache[fullName].exports(优先走缓存),一个模块只加载一次,从而提高模块加载速度。不想走缓存的话,可以在require(name)之前把cache[fullName]delete掉,例如delete require.cache[require.resolve('./my-module.js')]
  3. 否则,加载相应文件中的源码,并进行预处理(模块级变量注入),见Module.prototype.load
  4. 最后,编译(执行)转换过的源码,返回module.exports的值,见Module.prototype._compile

P.S.关于模块缓存的更多信息,请查看node.js require() cache – possible to invalidate?

看一个简单场景,假设有两个源码文件:

代码语言:javascript复制
  // my-modue.js
module.exports = 'my-modue';// index.js
const m = require('./my-module.js');

执行入口文件第一行require('./my-modue.js')的大致过程为:

代码语言:javascript复制
// 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()具体如下:

代码语言:javascript复制
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是这样:

代码语言:javascript复制
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];
};

简单梳理下,其实整个过程的核心工作相当于:

代码语言:javascript复制
// 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:

代码语言:javascript复制
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,很容易发现做过手脚的地方:

代码语言:javascript复制
// 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初始化的loader部分)。。。VS Code团队的路数狂野得很哪

三.插件机制初始化流程

之前在VS Code启动流程的UI布局部分提到:

代码语言:javascript复制
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开始着手界面的创建,以及界面与功能服务的对接。上次只关注了主启动流程相关的部分,这次看看插件机制的初始化流程

插件机制初始化相关文件递进关系:

代码语言:javascript复制
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.tscreateContents()方法与ExtensionService有关,主要内容如下:

代码语言:javascript复制
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,关键部分如下:

代码语言:javascript复制
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,关键部分如下:

代码语言:javascript复制
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得到的子进程入口是:

代码语言:javascript复制
// URI.parse(require.toUrl('bootstrap')).fsPath
// 经toUrl转换对应到
// out/bootstrap

src/bootstrap.js,关键部分如下:

代码语言:javascript复制
require('./bootstrap-amd').bootstrap(process.env['AMD_ENTRYPOINT']);

先绕出再回来,是为了走loader执行入口文件:

代码语言:javascript复制
var loader = require('./vs/loader');
exports.bootstrap = function (entrypoint) {
 loader([entrypoint], function () { }, function (err) { console.error(err); });
};

那么现在,踏进入口src/vs/workbench/node/extensionHostProcess.ts

代码语言:javascript复制
// setup things
const extensionHostMain = new ExtensionHostMain(renderer.rpcProtocol, renderer.initData);
onTerminate = () => extensionHostMain.terminate();
return extensionHostMain.start();

又转到了ExtensionHostMain,对应源码文件为src/vs/workbench/node/extensionHostMain.ts

代码语言:javascript复制
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.想要了解具体过程的话,可以看这两个文件:

代码语言:javascript复制
src/vs/workbench/api/node/extHostExtensionService.ts
src/vs/workbench/api/node/extHostExtensionActivator.ts

篇幅限制,我们跳过繁琐的依赖处理环节,直接看加载插件pkg.main入口文件的部分:

代码语言:javascript复制
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

代码语言:javascript复制
// 环境变量
ELECTRON_RUN_AS_NODE: '1'

所以不能在插件运行环境使用require('electron').BrowserWindow.getAllWindows()曲线改UI

P.S.关于插件定制UI能力的讨论,见access electron API from vscode extension

进程间通信方式

代码语言:javascript复制
      <Electron IPC>
Main ---------------- Renderer
|
|
| <Child Process IPC>
|
|
Extension Host

其中,Extension Host与Main之间的通信是通过fork()内置的IPC来完成的,具体如下:

代码语言:javascript复制
// 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进程间通信

参考资料

  • Microsoft/vscode v1.19.3
  • Hacking Node require

0 人点赞