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

写在前面

插件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初始化

一.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?

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

  // 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初始化的loader部分)。。。VS Code团队的路数狂野得很哪

三.插件机制初始化流程

之前在VS Code启动流程的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.tscreateContents()方法与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进程间通信

参考资料

API注入机制及插件启动流程_VSCode插件开发笔记2》上有1条评论

发表评论

电子邮件地址不会被公开。 必填项已用*标注

*

code