在理解了 VS Code 的通信机制后,咱们能够着手剖析 VS Code Server 中各模块的实现以及设计思路了。

<!-- more -->

VSCode Server 模块设计

通过之前的介绍咱们能够理解到,VS Code 的能力是前后端拆散的,这使得 remote server 的革新实现变得简略。

通过这一张架构图,咱们能够直观的看到在 VS Code 中,前后端能力职责的划分。

能够看出,除了多数的一些像本地文件上传,语法高亮、主题设置等能力,一些重依赖多过程通信、OS反对,语言编译的能力都被设计在了 Server 端中,以保障 Client 端足够的轻量简洁,能够运行在 Web 这样的轻环境中。

在本篇中,我来带大家浅要剖析 Server 端的几个重要模块的设计思路与实现。

Remote File System 设计

Remote File System 负责解决文件系统的读写操作,同时还须要解决文件系统的变动事件,以便于客户端可能实时更新文件系统的变动。在 VSCode 中,它封装了一层 Virtual file system 来实现对不同文件系统的兼容管制。

这一部分是 VSCode Server 的外围中最容易实现的局部。它实质上就是使依赖古代浏览器的 File_System_Access_API 来实现的(强制在 HTTPS 下应用)。

async function getTheFile() {  // open file picker  [fileHandle] = await window.showOpenFilePicker(pickerOpts);  // get file contents  const fileData = await fileHandle.getFile();}

具体的代码申明地位见 FileSystemProvider。

这里应用 vscode-vfs 这个库来实现虚构文件系统。这是一个 URI 计划,它注册了 File System Provider,并且该文件系统上的资源将由应用该模式的 URI 示意(例如vscode-vfs://vscode/package.json)。

因而,间接关上近程存储库也得以实现,例如 Github Codespaces 的关上就是这样实现的。

应用 vscode-vfs://github/microsoft/vscode, 通过拜访 https://github.com/microsoft/vscode,就可能在不进行 git clone的状况下,间接关上我的项目文件夹了。

实例化后,全局都能够通过传入 RuntimeEnvironment,通过 runtime.fs 来拜访与调用。

async stat(uri: string): Promise<FileStat> {    if (fileFs && uri.startsWith('file:')) {        return fileFs.stat(uri);    }    const res = await connection.sendRequest(FsStatRequest.type, uri.toString());    return res;}readDirectory(uri: string): Promise<[string, FileType][]> {    if (fileFs && uri.startsWith('file:')) {        return fileFs.readDirectory(uri);    }    return connection.sendRequest(FsReadDirRequest.type, uri.toString());}

当然,对于不反对这套 API 的浏览器来说,关上时会检测接口,弹出正告。

至于解决形式,之前说过,VSCode 的 server 端是同构的,server 天然也能提供本地文件系统反对,仍能够通过浏览器的上传 API 来实现。

Remote Terminal Process 设计

这里实际上是复用了VSCode 之前推出的 Remote-Server extension 能力,通过 SSH 隧道的形式,将终端的输入输出流转发到近程服务器上。(再一次阐明了为什么强制要求在HTTPS下应用)

还记得咱们之前提到过的,Channel 为通信的最小单元吗?VSCode ServerRemote Terminal 就是通过一个 RemoteTerminalChannel 来实现的。

通过监听与触发不同的事件(如onExecuteCommand, sendCommandResult),来实现对 Remote Terminal 的不同行为的信息同步。

    async call(ctx: RemoteAgentConnectionContext, command: string, args?: any): Promise<any> {        switch (command) {            case '$restartPtyHost': return this._ptyService.restartPtyHost?.apply(this._ptyService, args);            case '$createProcess': {                const uriTransformer = createURITransformer(ctx.remoteAuthority);                return this._createProcess(uriTransformer, <ICreateTerminalProcessArguments>args);            }            case '$attachToProcess': return this._ptyService.attachToProcess.apply(this._ptyService, args);            case '$detachFromProcess': return this._ptyService.detachFromProcess.apply(this._ptyService, args);            case '$listProcesses': return this._ptyService.listProcesses.apply(this._ptyService, args);            case '$orphanQuestionReply': return this._ptyService.orphanQuestionReply.apply(this._ptyService, args);            case '$acceptPtyHostResolvedVariables': return this._ptyService.acceptPtyHostResolvedVariables?.apply(this._ptyService, args);            case '$start': return this._ptyService.start.apply(this._ptyService, args);            case '$input': return this._ptyService.input.apply(this._ptyService, args);            case '$acknowledgeDataEvent': return this._ptyService.acknowledgeDataEvent.apply(this._ptyService, args);            case '$shutdown': return this._ptyService.shutdown.apply(this._ptyService, args);            case '$resize': return this._ptyService.resize.apply(this._ptyService, args);            case '$getInitialCwd': return this._ptyService.getInitialCwd.apply(this._ptyService, args);            case '$getCwd': return this._ptyService.getCwd.apply(this._ptyService, args);            case '$processBinary': return this._ptyService.processBinary.apply(this._ptyService, args);            case '$sendCommandResult': return this._sendCommandResult(args[0], args[1], args[2]);            case '$installAutoReply': return this._ptyService.installAutoReply.apply(this._ptyService, args);            case '$uninstallAllAutoReplies': return this._ptyService.uninstallAllAutoReplies.apply(this._ptyService, args);            case '$getDefaultSystemShell': return this._getDefaultSystemShell.apply(this, args);            case '$getProfiles': return this._getProfiles.apply(this, args);            case '$getEnvironment': return this._getEnvironment();            case '$getWslPath': return this._getWslPath(args[0]);            case '$getTerminalLayoutInfo': return this._ptyService.getTerminalLayoutInfo(<IGetTerminalLayoutInfoArgs>args);            case '$setTerminalLayoutInfo': return this._ptyService.setTerminalLayoutInfo(<ISetTerminalLayoutInfoArgs>args);            case '$serializeTerminalState': return this._ptyService.serializeTerminalState.apply(this._ptyService, args);            case '$reviveTerminalProcesses': return this._ptyService.reviveTerminalProcesses.apply(this._ptyService, args);            case '$getRevivedPtyNewId': return this._ptyService.getRevivedPtyNewId.apply(this._ptyService, args);            case '$setUnicodeVersion': return this._ptyService.setUnicodeVersion.apply(this._ptyService, args);            case '$reduceConnectionGraceTime': return this._reduceConnectionGraceTime();            case '$updateIcon': return this._ptyService.updateIcon.apply(this._ptyService, args);            case '$updateTitle': return this._ptyService.updateTitle.apply(this._ptyService, args);            case '$updateProperty': return this._ptyService.updateProperty.apply(this._ptyService, args);            case '$refreshProperty': return this._ptyService.refreshProperty.apply(this._ptyService, args);            case '$requestDetachInstance': return this._ptyService.requestDetachInstance(args[0], args[1]);            case '$acceptDetachedInstance': return this._ptyService.acceptDetachInstanceReply(args[0], args[1]);            case '$freePortKillProcess': return this._ptyService.freePortKillProcess?.apply(this._ptyService, args);        }        throw new Error(`IPC Command ${command} not found`);    }    listen(_: any, event: string, arg: any): Event<any> {        switch (event) {            case '$onPtyHostExitEvent': return this._ptyService.onPtyHostExit || Event.None;            case '$onPtyHostStartEvent': return this._ptyService.onPtyHostStart || Event.None;            case '$onPtyHostUnresponsiveEvent': return this._ptyService.onPtyHostUnresponsive || Event.None;            case '$onPtyHostResponsiveEvent': return this._ptyService.onPtyHostResponsive || Event.None;            case '$onPtyHostRequestResolveVariablesEvent': return this._ptyService.onPtyHostRequestResolveVariables || Event.None;            case '$onProcessDataEvent': return this._ptyService.onProcessData;            case '$onProcessReadyEvent': return this._ptyService.onProcessReady;            case '$onProcessExitEvent': return this._ptyService.onProcessExit;            case '$onProcessReplayEvent': return this._ptyService.onProcessReplay;            case '$onProcessOrphanQuestion': return this._ptyService.onProcessOrphanQuestion;            case '$onExecuteCommand': return this.onExecuteCommand;            case '$onDidRequestDetach': return this._ptyService.onDidRequestDetach || Event.None;            case '$onDidChangeProperty': return this._ptyService.onDidChangeProperty;            default:                break;        }        throw new Error('Not supported');    }

Extension Processes 设计

存储地位

VSCode Server 会将通过 code-server --install-extension <extension id> 命令装置的 extensions 存储在 $XDG_DATA_HOME/code-server/extensions 下。

用户配置信息存储在本地的~/.vscode 下,应用官网的 Settings Sync 插件进行配置漫游。

插件分类

VSCode 将插件分为了 UI ExtensionWorkspace Extension 两种,通过 extensionKind 字段进行指定。

如果不波及到 Node.js 调用的简略插件,是纯申明性质的代码的话(例如 Themes、key-binding,或者能间接利用客户端 API 能笼罩能力的插件等),则能够定义为 UI Extension,间接在客户端中执行,服务端只保留插件的配置信息,无需进行通信。

这也是为什么 vscode.dev 中(截至目前地位,该网页不蕴含 Server 能力),所有的主题、包含例如 TS、Python、Markdown、HTML 等语言的文件补全、语法高亮、括号着色都是能够失常应用的起因。因为在架构上,这些能力都是由客户端的内置插件(语言补全等相干个性是通过专门编写的 web worker thread 旁路执行)提供的,通过 VSCode API 间接进行调用。

但如果性能波及到运行时的零碎级调用,则须要被定义为Workspace Extension,它能够齐全拜访源码、文件系统、以及大部分 OS API

Workspace Extension 须要装置在服务端,并须要在插件中显式申明。

体现在编码标准上,咱们须要为插件我的项目的 package.json 文件中增加 main 的 entrypoint,以执行服务端插件调用,而 UI Extension 的 entrypoint 应用 browser 示意。

{    ...    "main": "./dist/node/extension.js",    "browser": "./dist/browser/extension.js",    "capabilities": {        "virtualWorkspaces": true    }    ...}

逻辑上,插件须要依据 web 端与 server 端离开编写,并自行做好兼容。