mirror of
https://github.com/coder/code-server.git
synced 2026-05-28 07:59:34 +00:00
Implement new structure
This commit is contained in:
59
src/node/vscode/README.md
Normal file
59
src/node/vscode/README.md
Normal 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 extension’s 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
204
src/node/vscode/server.ts
Normal 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)}'`),
|
||||
}
|
||||
}
|
||||
}
|
||||
Reference in New Issue
Block a user