如果初次接触code server,可以看我的code server介绍
如果对于整体架构还不清楚,可以看我的架构分析
在正式开始看代码前,我们再分析code server在整个远程开发方案中的作用。
Code Server在整个远程开发中的作用
1、code server最重要的作用就是server
,也就是提供了服务器能力,能够将远程机器上的ide作为一个服务提供给外部,访问并使用,所以,code server本质是一个基于express框架,nodejs平台的轻量级服务器
2、code server提供了用户登录功能,使得ide即使作为一个服务可供外部访问,依旧可以保证安全性;且只有在用户登录后,才真正去加载vscode server内核代码
3、code server还提供了升级、代理、心跳检测等功能,当然这些都是基础的服务器功能,我们今天也不细说
我们今天就重点解析一下几个方面的代码
1、code server是如何启动的
2、code server是如何把ide作为一个服务提供出去的
3、code server的中间件和路由
4、code server如何启动vscode内核的
由于code server是一个基于express的框架,最好了解express框架再看,我之后也会补上相关文档,这里就不细说框架的知识了
Code Server程序入口
code server的入口文件是src/node/entry.ts
,我们启动code server时候的命令是
./code-server --host 0.0.0.0 --port 9002
这个code-server实际就是个shell脚本,cat命令查看就能知道他本质上就是用node去启动程序,
# root其实code server的根目录,相当于`node .`,这样node会自动去package.json中找定义的入口文件
exec "$ROOT/lib/node" "$ROOT" "$@"
入口文件可以在package.json中看到定义
"main": "out/node/entry.js"
所以我们先从这个文件看,好吧,一来就是个难点,大家能看到有个判断isChild
,这里我们就要叉出去说了,code server实际最终会启动两个nodejs进程
一个是父进程,他的逻辑非常简单,主要用于管理整个软件,启动子进程并且控制其生命周期;与其通信,比如接收子进程的日志输出,并打印到日志文件中
另一个是子进程,父进程启动后会去启动子进程,子进程才是真正的express框架的服务器,也是之后加载vscode server内核代码的进程
那么这里的逻辑就是,判断当前的进程是不是子进程,如果是父进程,就执行wrapper.start
,就是我上面说的,去启动子进程,并与其建立通信
如果是子进程呢,先通过handshake和父进程建立通信,随后执行最重要的方法runCodeServer
启动Code Server服务器
async function entry(): Promise<void> {
// 判断父子进程
if (isChild(wrapper)) {
// 与父进程建立连接
const args = await wrapper.handshake()
wrapper.preventExit()
// 真正地启动code server服务器
const server = await runCodeServer(args)
wrapper.onDispose(() => server.dispose())
return
}
// 中间省略了部分内容,不影响当前分析
// 父进程执行,实际是去启动子进程
return wrapper.start(args)
}
父子进程相关代码在src/node/wrapper.ts
,大概代码是
export class ParentProcess extends Process {}
export class ChildProcess extends Process {}
// 通过instanceof判断是不是子进程
export function isChild(proc: ChildProcess | ParentProcess): proc is ChildProcess {
return proc instanceof ChildProcess
}
Code Server服务器启动
runCodeServer
方法看起来内容很多,实际重要的就几行
1、通过createApp
2、通过register方法注册中间件和路由
export const runCodeServer = async (
args: DefaultedArgs,
): Promise<{ dispose: Disposable["dispose"]; server: http.Server }> => {
const app = await createApp(args)
const disposeRoutes = await register(app, args)
}
咱们先说createApp,非常清晰,就是原生nodejs的http.createServer
启动了服务器,然后监听我们传入的host和port,这样我们的ide就可以作为一个服务对外提供访问啦!
然后一个重要的方法handleUpgrade
就是这里注册了处理websocket连接的中间件,还记得我们架构篇说的,vscode server前后台的通信方式依赖websocket,这个我们之后专门出一篇websocket文档详细说明
export const createApp = async (args: DefaultedArgs): Promise<App> => {
// 如果看源码会发现这里会判断args.cert,这个我们先省略,不影响理解
const server = http.createServer(router)
await listen(server, args)
// 处理websocket的中间件
handleUpgrade(wsRouter, server)
}
// listen方法里面就是server监听,监听我们启动命令传入的host和port
server.listen(opts.port, opts.host.replace(/^\[|\]$/g, ""), onListen)
我们接着说路由和中间件
如果你之前接触过express就很好理解了,没接触过也没事
路由简单来说,就是作为一个服务器提供GET/POST这种接口,供前台去调用,例如下面的router.get
假设我们前台发送请求到后台,http://172.22.22.22:9002/,最后的"/"就会匹配上这个路由,然后执行这里定义的回调函数
中间件呢?中间件也是一个函数通过use()
方法注册到express框架中,注册的先后顺序也会影响执行顺序,比如下面的这段代码,就是先执行router.use
中的方法,进行鉴权,通过了再使用next()
方法到下一步router.get()
中的函数
// 这个中间件就是在请求到达路由前执行的,通过authenticated方法鉴权
router.use(async (req, res, next) => {
const to = (typeof req.query.to === "string" && req.query.to) || "/"
if (await authenticated(req)) {
return redirect(req, res, to, { to: undefined })
}
next()
})
// 通过验证后,通过getRoot返回资源
router.get("/", async (req, res) => {
res.send(await getRoot(req))
})
好了我们说回code server,register
方法里面就是大量处理路由和中间件的逻辑,比如/login的请求路径分发到Login Router,/health的分发到Health Router,每个Router里面又有各自的中间件去处理这些请求
vscode server内核启动
好吧,我们差不多要说code server是如何启动vscode server的内核了,其实也就是我们上面说的路由和中间件
逻辑在这个文件src/node/routes/vscode.ts
在经过了鉴权等路由之后,我们的请求会到达这个路由
this.router.get("/", this.ensureCodeServerLoaded, this.$root)
ensureCodeServerLoaded
就是负责加载vscode代码的中间件
loadAMDModule实际就是原生vscode启动的那一套,实际是一个require引入模块,这里可以先忽略这点
加载后可以获得一个createVSServer方法,他就是真正去启动vscode内核的方法,在之后的文章说明
private ensureCodeServerLoaded: express.Handler = async (req, _res, next) => {
// See ../../../lib/vscode/src/vs/server/node/server.main.ts:72.
const createVSServer = await loadAMDModule<CreateServer>("vs/server/node/server.main", "createServer")
try {
this._codeServerMain = await createVSServer(null, {
...(await toCodeArgs(args)),
"without-connection-token": true,
})
} catch (error) {
logError(logger, "CodeServerRouteWrapper", error)
if (isDevMode) {
return next(new Error((error instanceof Error ? error.message : error) + " (VS Code may still be compiling)"))
}
return next(error)
}
}
export const loadAMDModule = async <T>(amdPath: string, exportName: string): Promise<T> => {
// Set default remote native node modules path, if unset
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
}
好了,现在code server的使命基本完成了,ide已经具备作为一个服务器对外访问的能力啦!
我们下一步解说vscode server内核,还有websocket协议~