前回の記事では、IDEをサービスとして提供するCode Serverの仕組みについて説明しました。Code Serverがサーバー機能を提供することに加えて、最も重要な役割はIDEカーネルコードをロードすることです。
Code ServerによるVSCodeカーネルのロード
Code ServerがVSCodeカーネルをロードする関連コードを振り返ると、loadAMDModule関数は本質的にrequire関数であり、VSCodeカーネルコード内のout/bootstrap-amdファイルにあるloadメソッドを呼び出しています。その後、VSCodeのネイティブなモジュールロード機構を通じてvs/server/node/server.main下のcreateServerメソッドをロードし、取得したcreateVSServerメソッドが現在のプロセス内でVSCodeカーネルコードをロードする役割を担っています。
これは以前のアーキテクチャ図で説明したように、Code ServerとVSCodeカーネルは論理的には独立していますが、同じプロセス内で動作しており、VSCodeカーネルは実質的にモジュールとしてCode Serverにロードされているという事実を示しています。
src/node/routes/vscode.ts
const createVSServer = await loadAMDModule<CreateServer>("vs/server/node/server.main", "createServer")
this._codeServerMain = await createVSServer(null, {
...(await toCodeArgs(args)),
"without-connection-token": true,
})
export const loadAMDModule = async <T>(amdPath: string, exportName: string): Promise<T> => {
process.env["VSCODE_INJECT_NODE_MODULE_LOOKUP_PATH"] =
process.env["VSCODE_INJECT_NODE_MODULE_LOOKUP_PATH"] || path.join(vsRootPath, "remote", "node_modules")
require(path.join(vsRootPath, "out/bootstrap-node")).injectNodeModuleLookupPath(
process.env["VSCODE_INJECT_NODE_MODULE_LOOKUP_PATH"],
)
const module = await new Promise<AMDModule<T>>((resolve, reject) => {
require(path.join(vsRootPath, "out/bootstrap-amd")).load(amdPath, resolve, reject)
})
return module[exportName] as T
}
VSCodeカーネルのロードプロセス
さて、VSCodeのソースコードに入りましょう。VSCode Serverのロードロジックのエントリーポイントはsrc/vs/server/node/server.main.tsにあり、これは前にCode Serverがロードしたモジュールです。このcreateServerはdoCreateServerメソッドを呼び出し、その具体的な内容はすべてdoCreateServer内にあります。
ここでは、以下の重要な処理が簡略化して行われています:
setupServerServiceでどのサービスコードがロードされるかを明確にし、各サービスを初期化し、後のサービス間の依存注入を準備しますRemoteExtensionHostAgentServerを初期化します。これは最も核心的なクラスで、後で重点的に説明します。このクラスはCode Serverから転送されてくるHTTPリクエストとその後のWebSocket接続を処理し、このクラスを理解すればVSCodeのリモート開発の基本を理解したことになります。
export function createServer(address: string | net.AddressInfo | null): Promise<IServerAPI> {
return doCreateServer(address, args, REMOTE_DATA_FOLDER);
}
export async function createServer(address: string | net.AddressInfo | null, args: ServerParsedArgs, REMOTE_DATA_FOLDER: string): Promise<IServerAPI> {
// 各サービスを初期化し、依存注入を準備
const { socketServer, instantiationService } = await setupServerServices(connectionToken, args, REMOTE_DATA_FOLDER, disposables);
// 本当のサーバー管理クラスRemoteExtensionHostAgentServerを初期化
const remoteExtensionHostAgentServer = instantiationService.createInstance(RemoteExtensionHostAgentServer, socketServer, connectionToken, vsdaMod, hasWebClient);
}
RemoteExtensionHostAgentServer自体のコードは非常に多く、まずインターフェースを見てみましょう。
export interface IServerAPI {
handleRequest(req: http.IncomingMessage, res: http.ServerResponse): Promise<void>;
handleUpgrade(req: http.IncomingMessage, socket: net.Socket): void;
handleServerError(err: Error): void;
dispose(): void;
}
RemoteExtensionHostAgentServerはこのインターフェースを実装しており、今日はインターフェースの機能から説明します。最も重要な2つのことは次の通りです:
handleRequestでHTTPリクエストを処理handleUpgradeでWebSocket接続を処理
class RemoteExtensionHostAgentServer extends Disposable implements IServerAPI {}
フロントエンド静的リソースサーバー
なぜ突然フロントエンドに話が飛んだのでしょうか?少し唐突に感じたかもしれませんが、ここで言いたいのは、前に言及したhandleRequestで処理するフロントエンドのHTTPリクエストの中で最も重要なものがフロントエンドの静的リソースだということです。
つまり、このIDEのバックエンドは同時にフロントエンド静的リソースサーバーの機能も担っており、Tomcatのようなものですが、前後のコードが一つのパッケージにまとめられている(ただし、実行時にはブラウザにフロントエンド、リモートマシンにバックエンドという前後端分離のアーキテクチャです)というわけです。
this._webClientServer.handleはフロントエンドUIが必要とするファイルを返すメソッドです。これを見てみると、最初のリクエスト("/")が来たとき、handleRootメソッドはworkbench.html(VSCodeフロントエンドのロードエントリーファイル)を返します。workbench.html内には以下のような記述があります。
<script src="{{WORKBENCH_WEB_BASE_URL}}/out/vs/code/browser/workbench/workbench.js"></script>
ブラウザがworkbench.htmlをロードした後、スクリプト内のアドレスに基づいて必要なリソースをさらに要求します。このときにhandleStaticメソッドが呼び出され、対応するファイルを読み込んで返します。
// workbench web UI
if (this._webClientServer) {
this._webClientServer.handle(req, res, parsedUrl);
return;
}
async handle(req: http.IncomingMessage, res: http.ServerResponse, parsedUrl: url.UrlWithParsedQuery): Promise<void> {
try {
const pathname = parsedUrl.pathname!;
if (pathname.startsWith(this._staticRoute) && pathname.charCodeAt(this._staticRoute.length) === CharCode.Slash) {
//
return this._handleStatic(req, res, parsedUrl);
}
if (pathname === '/') {
// テンプレートを置換したworkbench.htmlエントリーファイルを返す
return this._handleRoot(req, res, parsedUrl);
}
}
}
ブラウザの開発者ツール(F12キー)のNetworkタブを確認すると、workbench.htmlがリクエストされ、その後workbench.htmlで参照されているJavaScriptファイルがさらにロードされている様子が明確に確認できます。
WebSocketサーバー
フロントエンドリソースのロードがHTTPプロトコル(handleRequest)に依存しているのに対し、その他の機能の実装は基本的にWebSocketプロトコルに頼っています。例えば、ファイルの閲覧・編集、プラグインの実行(コードデバッグ、シンタックスハイライト)などです。WebSocketが特に重要であるため、ここでは専用の章を設けて説明します。現在理解しておくべきことは、RemoteExtensionHostAgentServerでWebSocket接続の確立ロジックが処理されており、メインプロセス用とプラグインプロセス用の2種類のWebSocket接続があるということだけです。