code-server/src/node/app/vscode.ts

253 lines
8.7 KiB
TypeScript
Raw Normal View History

2020-02-05 02:27:46 +07:00
import { field, logger } from "@coder/logger"
import * as cp from "child_process"
import * as crypto from "crypto"
import * as fs from "fs-extra"
2020-02-05 02:27:46 +07:00
import * as http from "http"
import * as net from "net"
import * as path from "path"
import * as url from "url"
2020-02-05 02:27:46 +07:00
import {
CodeServerMessage,
Options,
StartPath,
2020-02-05 02:27:46 +07:00
VscodeMessage,
VscodeOptions,
WorkbenchOptions,
} from "../../../lib/vscode/src/vs/server/ipc"
import { HttpCode, HttpError } from "../../common/http"
2020-02-05 02:27:46 +07:00
import { generateUuid } from "../../common/util"
import { Args } from "../cli"
2020-02-06 06:30:09 +07:00
import { HttpProvider, HttpProviderOptions, HttpResponse, Route } from "../http"
import { settings } from "../settings"
2020-02-05 02:27:46 +07:00
export class VscodeHttpProvider extends HttpProvider {
private readonly serverRootPath: string
private readonly vsRootPath: string
private _vscode?: Promise<cp.ChildProcess>
public constructor(options: HttpProviderOptions, private readonly args: Args) {
2020-02-05 02:27:46 +07:00
super(options)
this.vsRootPath = path.resolve(this.rootPath, "lib/vscode")
this.serverRootPath = path.join(this.vsRootPath, "out/vs/server")
}
public get running(): boolean {
return !!this._vscode
}
public async dispose(): Promise<void> {
if (this._vscode) {
const vscode = await this._vscode
vscode.removeAllListeners()
this._vscode = undefined
vscode.kill()
}
}
2020-02-05 02:27:46 +07:00
private async initialize(options: VscodeOptions): Promise<WorkbenchOptions> {
const id = generateUuid()
const vscode = await this.fork()
logger.debug("setting up vs code...")
2020-02-05 02:27:46 +07:00
return new Promise<WorkbenchOptions>((resolve, reject) => {
vscode.once("message", (message: VscodeMessage) => {
logger.debug("got message from vs code", field("message", message))
2020-02-05 02:27:46 +07:00
return message.type === "options" && message.id === id
? resolve(message.options)
: reject(new Error("Unexpected response during initialization"))
})
vscode.once("error", reject)
vscode.once("exit", (code) => reject(new Error(`VS Code exited unexpectedly with code ${code}`)))
this.send({ type: "init", id, options }, vscode)
})
}
private fork(): Promise<cp.ChildProcess> {
if (!this._vscode) {
logger.debug("forking vs code...")
2020-02-05 02:27:46 +07:00
const vscode = cp.fork(path.join(this.serverRootPath, "fork"))
vscode.on("error", (error) => {
logger.error(error.message)
this._vscode = undefined
})
vscode.on("exit", (code) => {
logger.error(`VS Code exited unexpectedly with code ${code}`)
this._vscode = undefined
})
this._vscode = new Promise((resolve, reject) => {
vscode.once("message", (message: VscodeMessage) => {
logger.debug("got message from vs code", field("message", message))
2020-02-05 02:27:46 +07:00
return message.type === "ready"
? resolve(vscode)
: reject(new Error("Unexpected response waiting for ready response"))
})
vscode.once("error", reject)
vscode.once("exit", (code) => reject(new Error(`VS Code exited unexpectedly with code ${code}`)))
})
}
return this._vscode
}
2020-03-12 04:00:39 +07:00
public async handleWebSocket(route: Route, request: http.IncomingMessage, socket: net.Socket): Promise<void> {
2020-02-05 02:27:46 +07:00
if (!this.authenticated(request)) {
throw new Error("not authenticated")
}
// VS Code expects a raw socket. It will handle all the web socket frames.
// We just need to handle the initial upgrade.
// This magic value is specified by the websocket spec.
const magic = "258EAFA5-E914-47DA-95CA-C5AB0DC85B11"
const reply = crypto
.createHash("sha1")
.update(request.headers["sec-websocket-key"] + magic)
.digest("base64")
socket.write(
[
"HTTP/1.1 101 Switching Protocols",
"Upgrade: websocket",
"Connection: Upgrade",
`Sec-WebSocket-Accept: ${reply}`,
2020-02-15 07:46:00 +07:00
].join("\r\n") + "\r\n\r\n",
2020-02-05 02:27:46 +07:00
)
const vscode = await this._vscode
2020-02-06 06:30:09 +07:00
this.send({ type: "socket", query: route.query }, vscode, socket)
2020-02-05 02:27:46 +07:00
}
private send(message: CodeServerMessage, vscode?: cp.ChildProcess, socket?: net.Socket): void {
if (!vscode || vscode.killed) {
throw new Error("vscode is not running")
}
vscode.send(message, socket)
}
2020-03-03 01:43:02 +07:00
public async handleRequest(route: Route, request: http.IncomingMessage): Promise<HttpResponse> {
this.ensureMethod(request)
2020-02-06 06:30:09 +07:00
switch (route.base) {
2020-02-05 02:27:46 +07:00
case "/":
if (!this.isRoot(route)) {
throw new HttpError("Not found", HttpCode.NotFound)
} else if (!this.authenticated(request)) {
return { redirect: "/login", query: { to: this.options.base } }
}
try {
2020-02-06 06:30:09 +07:00
return await this.getRoot(request, route)
} catch (error) {
const message = `<div>VS Code failed to load.</div> ${
this.isDev
? `<div>It might not have finished compiling.</div>` +
`Check for <code>Finished <span class="success">compilation</span></code> in the output.`
: ""
} <br><br>${error}`
return this.getErrorRoot(route, "VS Code failed to load", "500", message)
}
}
this.ensureAuthenticated(request)
switch (route.base) {
2020-02-05 02:27:46 +07:00
case "/resource":
case "/vscode-remote-resource":
2020-02-06 06:30:09 +07:00
if (typeof route.query.path === "string") {
return this.getResource(route.query.path)
2020-02-05 02:27:46 +07:00
}
break
case "/webview":
2020-02-06 06:30:09 +07:00
if (/^\/vscode-resource/.test(route.requestPath)) {
return this.getResource(route.requestPath.replace(/^\/vscode-resource(\/file)?/, ""))
2020-02-05 02:27:46 +07:00
}
2020-02-06 06:30:09 +07:00
return this.getResource(this.vsRootPath, "out/vs/workbench/contrib/webview/browser/pre", route.requestPath)
2020-02-05 02:27:46 +07:00
}
2020-03-03 01:43:02 +07:00
throw new HttpError("Not found", HttpCode.NotFound)
2020-02-05 02:27:46 +07:00
}
2020-02-06 06:30:09 +07:00
private async getRoot(request: http.IncomingMessage, route: Route): Promise<HttpResponse> {
const remoteAuthority = request.headers.host as string
const { lastVisited } = await settings.read()
const startPath = await this.getFirstValidPath(
[
{ url: route.query.workspace, workspace: true },
{ url: route.query.folder, workspace: false },
this.args._ && this.args._.length > 0 ? { url: path.resolve(this.args._[this.args._.length - 1]) } : undefined,
lastVisited,
],
2020-02-15 07:46:00 +07:00
remoteAuthority,
)
2020-02-05 02:27:46 +07:00
const [response, options] = await Promise.all([
await this.getUtf8Resource(this.rootPath, "src/browser/pages/vscode.html"),
2020-02-05 02:27:46 +07:00
this.initialize({
args: this.args,
remoteAuthority,
startPath,
2020-02-05 02:27:46 +07:00
}),
])
if (startPath) {
settings.write({
lastVisited: startPath,
2020-02-05 02:27:46 +07:00
})
}
if (!this.isDev) {
response.content = response.content.replace(/<!-- PROD_ONLY/g, "").replace(/END_PROD_ONLY -->/g, "")
}
response.content = response.content
.replace(`"{{REMOTE_USER_DATA_URI}}"`, `'${JSON.stringify(options.remoteUserDataUri)}'`)
.replace(`"{{PRODUCT_CONFIGURATION}}"`, `'${JSON.stringify(options.productConfiguration)}'`)
.replace(`"{{WORKBENCH_WEB_CONFIGURATION}}"`, `'${JSON.stringify(options.workbenchWebConfiguration)}'`)
.replace(`"{{NLS_CONFIGURATION}}"`, `'${JSON.stringify(options.nlsConfiguration)}'`)
return this.replaceTemplates<Options>(route, response, {
base: this.base(route),
commit: this.options.commit,
disableTelemetry: !!this.args["disable-telemetry"],
})
2020-02-05 02:27:46 +07:00
}
/**
* Choose the first valid path. If `workspace` is undefined then either a
* workspace or a directory are acceptable. Otherwise it must be a file if a
* workspace or a directory otherwise.
*/
private async getFirstValidPath(
startPaths: Array<{ url?: string | string[]; workspace?: boolean } | undefined>,
2020-02-15 07:46:00 +07:00
remoteAuthority: string,
): Promise<StartPath | undefined> {
for (let i = 0; i < startPaths.length; ++i) {
const startPath = startPaths[i]
if (!startPath) {
continue
}
const paths = typeof startPath.url === "string" ? [startPath.url] : startPath.url || []
for (let j = 0; j < paths.length; ++j) {
const uri = url.parse(paths[j])
try {
if (!uri.pathname) {
throw new Error(`${paths[j]} is not a valid URL`)
}
const stat = await fs.stat(uri.pathname)
if (typeof startPath.workspace === "undefined" || startPath.workspace !== stat.isDirectory()) {
return {
url: url.format({
protocol: uri.protocol || "vscode-remote",
hostname: remoteAuthority.split(":")[0],
port: remoteAuthority.split(":")[1],
pathname: uri.pathname,
slashes: true,
}),
workspace: !stat.isDirectory(),
}
}
} catch (error) {
logger.warn(error.message)
}
}
}
return undefined
}
2020-02-05 02:27:46 +07:00
}