2020-11-19 01:05:24 +07:00
|
|
|
import { logger } from "@coder/logger"
|
2020-10-21 06:05:58 +07:00
|
|
|
import * as cp from "child_process"
|
|
|
|
import * as net from "net"
|
|
|
|
import * as path from "path"
|
|
|
|
import * as ipc from "../../lib/vscode/src/vs/server/ipc"
|
|
|
|
import { arrayify, generateUuid } from "../common/util"
|
|
|
|
import { rootPath } from "./constants"
|
|
|
|
import { settings } from "./settings"
|
2020-11-11 06:24:07 +07:00
|
|
|
import { SocketProxyProvider } from "./socket"
|
2020-11-04 03:40:06 +07:00
|
|
|
import { isFile } from "./util"
|
2020-11-19 01:05:24 +07:00
|
|
|
import { onMessage, wrapper } from "./wrapper"
|
2020-10-21 06:05:58 +07:00
|
|
|
|
|
|
|
export class VscodeProvider {
|
|
|
|
public readonly serverRootPath: string
|
|
|
|
public readonly vsRootPath: string
|
|
|
|
private _vscode?: Promise<cp.ChildProcess>
|
2020-11-11 06:24:07 +07:00
|
|
|
private readonly socketProvider = new SocketProxyProvider()
|
2020-10-21 06:05:58 +07:00
|
|
|
|
|
|
|
public constructor() {
|
|
|
|
this.vsRootPath = path.resolve(rootPath, "lib/vscode")
|
|
|
|
this.serverRootPath = path.join(this.vsRootPath, "out/vs/server")
|
2020-11-19 00:43:25 +07:00
|
|
|
wrapper.onDispose(() => this.dispose())
|
2020-10-21 06:05:58 +07:00
|
|
|
}
|
|
|
|
|
|
|
|
public async dispose(): Promise<void> {
|
2020-11-11 06:24:07 +07:00
|
|
|
this.socketProvider.stop()
|
2020-10-21 06:05:58 +07:00
|
|
|
if (this._vscode) {
|
|
|
|
const vscode = await this._vscode
|
|
|
|
vscode.removeAllListeners()
|
|
|
|
vscode.kill()
|
2020-11-04 03:36:27 +07:00
|
|
|
this._vscode = undefined
|
2020-10-21 06:05:58 +07:00
|
|
|
}
|
|
|
|
}
|
|
|
|
|
|
|
|
public async initialize(
|
|
|
|
options: Omit<ipc.VscodeOptions, "startPath">,
|
|
|
|
query: ipc.Query,
|
|
|
|
): Promise<ipc.WorkbenchOptions> {
|
|
|
|
const { lastVisited } = await settings.read()
|
2021-01-18 23:29:18 +07:00
|
|
|
let startPath = await this.getFirstPath([
|
2020-10-21 06:05:58 +07:00
|
|
|
{ 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,
|
2021-01-18 23:29:18 +07:00
|
|
|
!options.args["ignore-last-opened"] ? lastVisited : undefined,
|
2020-10-21 06:05:58 +07:00
|
|
|
])
|
|
|
|
|
2021-01-18 23:29:18 +07:00
|
|
|
if (query.ew) {
|
|
|
|
startPath = undefined
|
|
|
|
}
|
|
|
|
|
2020-10-21 06:05:58 +07:00
|
|
|
settings.write({
|
|
|
|
lastVisited: startPath,
|
|
|
|
query,
|
|
|
|
})
|
|
|
|
|
|
|
|
const id = generateUuid()
|
|
|
|
const vscode = await this.fork()
|
|
|
|
|
|
|
|
logger.debug("setting up vs code...")
|
2020-11-04 03:54:27 +07:00
|
|
|
|
2020-11-11 04:46:53 +07:00
|
|
|
this.send(
|
|
|
|
{
|
|
|
|
type: "init",
|
|
|
|
id,
|
|
|
|
options: {
|
|
|
|
...options,
|
|
|
|
startPath,
|
2020-10-21 06:05:58 +07:00
|
|
|
},
|
2020-11-11 04:46:53 +07:00
|
|
|
},
|
|
|
|
vscode,
|
|
|
|
)
|
|
|
|
|
2020-11-19 01:05:24 +07:00
|
|
|
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
|
|
|
|
},
|
|
|
|
)
|
2020-11-11 04:46:53 +07:00
|
|
|
|
|
|
|
return message.options
|
2020-10-21 06:05:58 +07:00
|
|
|
}
|
|
|
|
|
|
|
|
private fork(): Promise<cp.ChildProcess> {
|
2020-11-04 03:42:37 +07:00
|
|
|
if (this._vscode) {
|
|
|
|
return this._vscode
|
|
|
|
}
|
2020-10-21 06:05:58 +07:00
|
|
|
|
2020-11-04 03:42:37 +07:00
|
|
|
logger.debug("forking vs code...")
|
|
|
|
const vscode = cp.fork(path.join(this.serverRootPath, "fork"))
|
2020-11-11 04:46:53 +07:00
|
|
|
|
|
|
|
const dispose = () => {
|
|
|
|
vscode.removeAllListeners()
|
|
|
|
vscode.kill()
|
2020-11-04 03:42:37 +07:00
|
|
|
this._vscode = undefined
|
2020-11-11 04:46:53 +07:00
|
|
|
}
|
|
|
|
|
|
|
|
vscode.on("error", (error: Error) => {
|
|
|
|
logger.error(error.message)
|
|
|
|
if (error.stack) {
|
|
|
|
logger.debug(error.stack)
|
|
|
|
}
|
|
|
|
dispose()
|
2020-11-04 03:42:37 +07:00
|
|
|
})
|
2020-11-11 04:46:53 +07:00
|
|
|
|
2020-11-04 03:42:37 +07:00
|
|
|
vscode.on("exit", (code) => {
|
|
|
|
logger.error(`VS Code exited unexpectedly with code ${code}`)
|
2020-11-11 04:46:53 +07:00
|
|
|
dispose()
|
2020-11-04 03:42:37 +07:00
|
|
|
})
|
|
|
|
|
2020-11-19 01:05:24 +07:00
|
|
|
this._vscode = onMessage<ipc.VscodeMessage, ipc.ReadyMessage>(vscode, (message): message is ipc.ReadyMessage => {
|
2020-11-11 04:46:53 +07:00
|
|
|
return message.type === "ready"
|
|
|
|
}).then(() => vscode)
|
|
|
|
|
|
|
|
return this._vscode
|
|
|
|
}
|
2020-11-04 03:54:27 +07:00
|
|
|
|
2020-10-21 06:05:58 +07:00
|
|
|
/**
|
|
|
|
* VS Code expects a raw socket. It will handle all the web socket frames.
|
|
|
|
*/
|
2021-03-03 05:42:25 +07:00
|
|
|
public async sendWebsocket(socket: net.Socket, query: ipc.Query, permessageDeflate: boolean): Promise<void> {
|
2020-10-21 06:05:58 +07:00
|
|
|
const vscode = await this._vscode
|
2020-11-11 06:24:07 +07:00
|
|
|
// 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)
|
2021-03-03 05:42:25 +07:00
|
|
|
this.send({ type: "socket", query, permessageDeflate }, vscode, socketProxy)
|
2020-10-21 06:05:58 +07:00
|
|
|
}
|
|
|
|
|
|
|
|
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)
|
|
|
|
}
|
|
|
|
|
|
|
|
/**
|
2020-11-11 06:02:39 +07:00
|
|
|
* 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.
|
2020-10-21 06:05:58 +07:00
|
|
|
*/
|
|
|
|
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
|
|
|
|
}
|
|
|
|
}
|