import { logger } from "@coder/logger" import * as cp from "child_process" import * as net from "net" import * as path from "path" import * as ipc from "../../typings/ipc" import { arrayify, generateUuid } from "../common/util" import { rootPath } from "./constants" import { settings } from "./settings" import { SocketProxyProvider } from "./socket" import { isFile } from "./util" import { onMessage, wrapper } from "./wrapper" export class VscodeProvider { public readonly serverRootPath: string public readonly vsRootPath: string private _vscode?: Promise private readonly socketProvider = new SocketProxyProvider() public constructor() { this.vsRootPath = path.resolve(rootPath, "lib/vscode") this.serverRootPath = path.join(this.vsRootPath, "out/vs/server") wrapper.onDispose(() => this.dispose()) } public async dispose(): Promise { this.socketProvider.stop() if (this._vscode) { const vscode = await this._vscode vscode.removeAllListeners() vscode.kill() this._vscode = undefined } } public async initialize( options: Omit, query: ipc.Query, ): Promise { const { lastVisited } = await settings.read() let startPath = await this.getFirstPath([ { url: query.workspace, workspace: true }, { url: query.folder, workspace: false }, options.args._ && options.args._.length > 0 ? { url: path.resolve(options.args._[options.args._.length - 1]) } : undefined, !options.args["ignore-last-opened"] ? lastVisited : undefined, ]) if (query.ew) { startPath = undefined } settings.write({ lastVisited: startPath, query, }) const id = generateUuid() const vscode = await this.fork() logger.debug("setting up vs code...") this.send( { type: "init", id, options: { ...options, startPath, }, }, vscode, ) const message = await onMessage( vscode, (message): message is ipc.OptionsMessage => { // There can be parallel initializations so wait for the right ID. return message.type === "options" && message.id === id }, ) return message.options } private fork(): Promise { if (this._vscode) { return this._vscode } logger.debug("forking vs code...") const vscode = cp.fork(path.join(this.serverRootPath, "fork")) const dispose = () => { vscode.removeAllListeners() vscode.kill() this._vscode = undefined } vscode.on("error", (error: Error) => { logger.error(error.message) if (error.stack) { logger.debug(error.stack) } dispose() }) vscode.on("exit", (code) => { logger.error(`VS Code exited unexpectedly with code ${code}`) dispose() }) this._vscode = onMessage(vscode, (message): message is ipc.ReadyMessage => { return message.type === "ready" }).then(() => vscode) return this._vscode } /** * VS Code expects a raw socket. It will handle all the web socket frames. */ public async sendWebsocket(socket: net.Socket, query: ipc.Query, permessageDeflate: boolean): Promise { const vscode = await this._vscode // TLS sockets cannot be transferred to child processes so we need an // in-between. Non-TLS sockets will be returned as-is. const socketProxy = await this.socketProvider.createProxy(socket) this.send({ type: "socket", query, permessageDeflate }, vscode, socketProxy) } private send(message: ipc.CodeServerMessage, vscode?: cp.ChildProcess, socket?: net.Socket): void { if (!vscode || vscode.killed) { throw new Error("vscode is not running") } vscode.send(message, socket) } /** * Choose the first non-empty path from the provided array. * * Each array item consists of `url` and an optional `workspace` boolean that * indicates whether that url is for a workspace. * * `url` can be a fully qualified URL or just the path portion. * * `url` can also be a query object to make it easier to pass in query * variables directly but anything that isn't a string or string array is not * valid and will be ignored. */ private async getFirstPath( startPaths: Array<{ url?: string | string[] | ipc.Query | ipc.Query[]; workspace?: boolean } | undefined>, ): Promise { for (let i = 0; i < startPaths.length; ++i) { const startPath = startPaths[i] const url = arrayify(startPath && startPath.url).find((p) => !!p) if (startPath && url && typeof url === "string") { return { url, // The only time `workspace` is undefined is for the command-line // argument, in which case it's a path (not a URL) so we can stat it // without having to parse it. workspace: typeof startPath.workspace !== "undefined" ? startPath.workspace : await isFile(url), } } } return undefined } }