101c2a01f1
It seems reaching into lib/vscode for the types caused tsc to establish watches that caused it to restart over and over while vscode was building. The strategy used here is to symlink it instead which is the same thing we do for the proxy agent.
169 lines
5.1 KiB
TypeScript
169 lines
5.1 KiB
TypeScript
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<cp.ChildProcess>
|
|
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<void> {
|
|
this.socketProvider.stop()
|
|
if (this._vscode) {
|
|
const vscode = await this._vscode
|
|
vscode.removeAllListeners()
|
|
vscode.kill()
|
|
this._vscode = undefined
|
|
}
|
|
}
|
|
|
|
public async initialize(
|
|
options: Omit<ipc.VscodeOptions, "startPath">,
|
|
query: ipc.Query,
|
|
): Promise<ipc.WorkbenchOptions> {
|
|
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<ipc.VscodeMessage, ipc.OptionsMessage>(
|
|
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<cp.ChildProcess> {
|
|
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<ipc.VscodeMessage, ipc.ReadyMessage>(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<void> {
|
|
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<ipc.StartPath | undefined> {
|
|
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
|
|
}
|
|
}
|