Archived
1
0
This repository has been archived on 2024-09-09. You can view files and clone it, but cannot push or open issues or pull requests.
code-server/src/node/vscode.ts
Asher 101c2a01f1
Fix tsc watch restarting when it shouldn't
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.
2021-04-01 10:58:56 -05:00

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
}
}