서문에
플러그인 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를 반환 (캐시 우선). 모듈은 1 회만 로드되며, 모듈 로드 속도를 향상. 캐시를 우회하고 싶다면,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 {
// 각 확장 기능은 자신의 api 구현을 얻음
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);
}
// 파일명에서 확장 기능 id 와 확장 기능용 api 획득
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;
}
// 기본 구현으로 폴백
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(() => {
// extensionHost 의 생성과 확장 기능 스캔 지연
// workbench 실행 후까지
// 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'
})
};
// 현재 프로세스의 fork 로서 Extension Host 실행
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 에 발을 들입니다:
// 사물 설정
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`);
});
}
// "eager" 激活 확장 기능 처리
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 窗體) 1 개만 존재하며, 모든 플러그인이 해당 프로세스에서 실행됩니다.각 플러그인이 독립된 프로세스라는 것은 아닙니다
주의, 플러그인 호스트 프로세스는보통의 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 를 통해 완료되며, 구체적으로:
// 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 프로세스 간 통신/) 참조
아직 댓글이 없습니다