共计 7226 个字符,预计需要花费 19 分钟才能阅读完成。
在理解了 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 Server
的 Remote 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 Extension
与 Workspace 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 端离开编写,并自行做好兼容。