Implement new structure

This commit is contained in:
Asher
2020-02-04 13:27:46 -06:00
parent ef8da3864f
commit b29346ecdf
94 changed files with 12427 additions and 4936 deletions

59
src/node/vscode/README.md Normal file
View File

@@ -0,0 +1,59 @@
Implementation of [VS Code](https://code.visualstudio.com/) remote/web for use
in `code-server`.
## Docker
To debug Golang in VS Code using the
[ms-vscode-go extension](https://marketplace.visualstudio.com/items?itemName=ms-vscode.Go),
you need to add `--security-opt seccomp=unconfined` to your `docker run`
arguments when launching code-server with Docker. See
[#725](https://github.com/cdr/code-server/issues/725) for details.
## Known Issues
- Creating custom VS Code extensions and debugging them doesn't work.
- Extension profiling and tips are currently disabled.
## Extensions
`code-server` does not provide access to the official
[Visual Studio Marketplace](https://marketplace.visualstudio.com/vscode). Instead,
Coder has created a custom extension marketplace that we manage for open-source
extensions. If you want to use an extension with code-server that we do not have
in our marketplace please look for a release in the extensions repository,
contact us to see if we have one in the works or, if you build an extension
locally from open source, you can copy it to the `extensions` folder. If you
build one locally from open-source please contribute it to the project and let
us know so we can give you props! If you have your own custom marketplace, it is
possible to point code-server to it by setting the `SERVICE_URL` and `ITEM_URL`
environment variables.
## Development: upgrading VS Code
We patch VS Code to provide and fix some functionality. As the web portion of VS
Code matures, we'll be able to shrink and maybe even entirely eliminate our
patch. In the meantime, however, upgrading the VS Code version requires ensuring
that the patch still applies and has the intended effects.
If functionality doesn't depend on code from VS Code then it should be moved
into code-server otherwise it should be in the patch.
To generate a new patch, **stage all the changes** you want to be included in
the patch in the VS Code source, then run `yarn patch:generate` in this
directory.
Our changes include:
- Allow multiple extension directories (both user and built-in).
- Modify the loader, websocket, webview, service worker, and asset requests to
use the URL of the page as a base (and TLS if necessary for the websocket).
- Send client-side telemetry through the server.
- Make changing the display language work.
- Make it possible for us to load code on the client.
- Make extensions work in the browser.
- Fix getting permanently disconnected when you sleep or hibernate for a while.
- Make it possible to automatically update the binary.
## Future
- Run VS Code unit tests against our builds to ensure features work as expected.

204
src/node/vscode/server.ts Normal file
View File

@@ -0,0 +1,204 @@
import { field, logger } from "@coder/logger"
import * as cp from "child_process"
import * as crypto from "crypto"
import * as http from "http"
import * as net from "net"
import * as path from "path"
import * as querystring from "querystring"
import {
CodeServerMessage,
Settings,
VscodeMessage,
VscodeOptions,
WorkbenchOptions,
} from "../../../lib/vscode/src/vs/server/ipc"
import { generateUuid } from "../../common/util"
import { HttpProvider, HttpProviderOptions, HttpResponse } from "../http"
import { SettingsProvider } from "../settings"
import { xdgLocalDir } from "../util"
export class VscodeHttpProvider extends HttpProvider {
private readonly serverRootPath: string
private readonly vsRootPath: string
private readonly settings = new SettingsProvider<Settings>(path.join(xdgLocalDir, "coder.json"))
private _vscode?: Promise<cp.ChildProcess>
private workbenchOptions?: WorkbenchOptions
public constructor(private readonly args: string[], options: HttpProviderOptions) {
super(options)
this.vsRootPath = path.resolve(this.rootPath, "lib/vscode")
this.serverRootPath = path.join(this.vsRootPath, "out/vs/server")
}
private async initialize(options: VscodeOptions): Promise<WorkbenchOptions> {
const id = generateUuid()
const vscode = await this.fork()
logger.debug("Setting up VS Code...")
return new Promise<WorkbenchOptions>((resolve, reject) => {
vscode.once("message", (message: VscodeMessage) => {
logger.debug("Got message from VS Code", field("message", message))
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...")
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))
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
}
public async handleWebSocket(
_base: string,
_requestPath: string,
query: querystring.ParsedUrlQuery,
request: http.IncomingMessage,
socket: net.Socket
): Promise<true> {
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}`,
].join("\r\n") + "\r\n\r\n"
)
const vscode = await this._vscode
this.send({ type: "socket", query }, vscode, socket)
return true
}
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)
}
public async handleRequest(
base: string,
requestPath: string,
query: querystring.ParsedUrlQuery,
request: http.IncomingMessage
): Promise<HttpResponse | undefined> {
this.ensureGet(request)
switch (base) {
case "/":
if (!this.authenticated(request)) {
return { redirect: "/login" }
}
return this.getRoot(request, query)
case "/static": {
switch (requestPath) {
case "/out/vs/workbench/services/extensions/worker/extensionHostWorkerMain.js": {
const response = await this.getUtf8Resource(this.vsRootPath, requestPath)
response.content = response.content.replace(
/{{COMMIT}}/g,
this.workbenchOptions ? this.workbenchOptions.commit : ""
)
response.cache = true
return response
}
}
const response = await this.getResource(this.vsRootPath, requestPath)
response.cache = true
return response
}
case "/resource":
case "/vscode-remote-resource":
this.ensureAuthenticated(request)
if (typeof query.path === "string") {
return this.getResource(query.path)
}
break
case "/tar":
this.ensureAuthenticated(request)
if (typeof query.path === "string") {
return this.getTarredResource(query.path)
}
break
case "/webview":
this.ensureAuthenticated(request)
if (/^\/vscode-resource/.test(requestPath)) {
return this.getResource(requestPath.replace(/^\/vscode-resource(\/file)?/, ""))
}
return this.getResource(this.vsRootPath, "out/vs/workbench/contrib/webview/browser/pre", requestPath)
}
return undefined
}
private async getRoot(request: http.IncomingMessage, query: querystring.ParsedUrlQuery): Promise<HttpResponse> {
const settings = await this.settings.read()
const [response, options] = await Promise.all([
this.getUtf8Resource(this.serverRootPath, "browser/workbench.html"),
this.initialize({
args: this.args,
query,
remoteAuthority: request.headers.host as string,
settings,
}),
])
this.workbenchOptions = options
if (options.startPath) {
this.settings.write({
lastVisited: {
path: options.startPath.path,
workspace: options.startPath.workspace,
},
})
}
return {
...response,
content: response.content
.replace(/{{COMMIT}}/g, options.commit)
.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)}'`),
}
}
}