Skip to content

Commit 3d8d544

Browse files
committed
Refactor VS Code routes to match others
1 parent 323a1f3 commit 3d8d544

File tree

2 files changed

+163
-182
lines changed

2 files changed

+163
-182
lines changed

src/node/routes/index.ts

+4-6
Original file line numberDiff line numberDiff line change
@@ -25,7 +25,7 @@ import * as login from "./login"
2525
import * as logout from "./logout"
2626
import * as pathProxy from "./pathProxy"
2727
import * as update from "./update"
28-
import { CodeServerRouteWrapper } from "./vscode"
28+
import * as vscode from "./vscode"
2929

3030
/**
3131
* Register all routes and middleware.
@@ -170,12 +170,10 @@ export const register = async (app: App, args: DefaultedArgs): Promise<Disposabl
170170

171171
app.router.use("/update", update.router)
172172

173-
const vsServerRouteHandler = new CodeServerRouteWrapper()
174-
175173
// Note that the root route is replaced in Coder Enterprise by the plugin API.
176174
for (const routePrefix of ["/vscode", "/"]) {
177-
app.router.use(routePrefix, vsServerRouteHandler.router)
178-
app.wsRouter.use(routePrefix, vsServerRouteHandler.wsRouter)
175+
app.router.use(routePrefix, vscode.router)
176+
app.wsRouter.use(routePrefix, vscode.wsRouter.router)
179177
}
180178

181179
app.router.use(() => {
@@ -188,6 +186,6 @@ export const register = async (app: App, args: DefaultedArgs): Promise<Disposabl
188186
return () => {
189187
heart.dispose()
190188
pluginApi?.dispose()
191-
vsServerRouteHandler.dispose()
189+
vscode.dispose()
192190
}
193191
}

src/node/routes/vscode.ts

+159-176
Original file line numberDiff line numberDiff line change
@@ -14,203 +14,186 @@ import { SocketProxyProvider } from "../socket"
1414
import { isFile, loadAMDModule } from "../util"
1515
import { Router as WsRouter } from "../wsRouter"
1616

17+
export const router = express.Router()
18+
19+
export const wsRouter = WsRouter()
20+
1721
/**
18-
* This is the API of Code's web client server. code-server delegates requests
19-
* to Code here.
22+
* The API of VS Code's web client server. code-server delegates requests to VS
23+
* Code here.
24+
*
25+
* @see ../../../lib/vscode/src/vs/server/node/server.main.ts:72
2026
*/
21-
export interface IServerAPI {
27+
export interface IVSCodeServerAPI {
2228
handleRequest(req: http.IncomingMessage, res: http.ServerResponse): Promise<void>
2329
handleUpgrade(req: http.IncomingMessage, socket: net.Socket): void
2430
handleServerError(err: Error): void
2531
dispose(): void
2632
}
2733

28-
// Types for ../../../lib/vscode/src/vs/server/node/server.main.ts:72.
29-
export type CreateServer = (address: string | net.AddressInfo | null, args: CodeArgs) => Promise<IServerAPI>
30-
31-
export class CodeServerRouteWrapper {
32-
/** Assigned in `ensureCodeServerLoaded` */
33-
private _codeServerMain!: IServerAPI
34-
private _wsRouterWrapper = WsRouter()
35-
private _socketProxyProvider = new SocketProxyProvider()
36-
public router = express.Router()
37-
private mintKeyPromise: Promise<Buffer> | undefined
34+
// See ../../../lib/vscode/src/vs/server/node/server.main.ts:72.
35+
export type CreateServer = (address: string | net.AddressInfo | null, args: CodeArgs) => Promise<IVSCodeServerAPI>
3836

39-
public get wsRouter() {
40-
return this._wsRouterWrapper.router
41-
}
37+
// The VS Code server is dynamically loaded in when a request is made to this
38+
// router by `ensureCodeServerLoaded`.
39+
let vscodeServer: IVSCodeServerAPI | undefined
4240

43-
//#region Route Handlers
44-
45-
private manifest: express.Handler = async (req, res, next) => {
46-
const appName = req.args["app-name"] || "code-server"
47-
res.writeHead(200, { "Content-Type": "application/manifest+json" })
48-
49-
return res.end(
50-
replaceTemplates(
51-
req,
52-
JSON.stringify(
53-
{
54-
name: appName,
55-
short_name: appName,
56-
start_url: ".",
57-
display: "fullscreen",
58-
display_override: ["window-controls-overlay"],
59-
description: "Run Code on a remote server.",
60-
icons: [192, 512].map((size) => ({
61-
src: `{{BASE}}/_static/src/browser/media/pwa-icon-${size}.png`,
62-
type: "image/png",
63-
sizes: `${size}x${size}`,
64-
})),
65-
},
66-
null,
67-
2,
68-
),
69-
),
70-
)
41+
/**
42+
* Ensure the VS Code server is loaded.
43+
*/
44+
export const ensureVSCodeLoaded = async (
45+
req: express.Request,
46+
_: express.Response,
47+
next: express.NextFunction,
48+
): Promise<void> => {
49+
if (vscodeServer) {
50+
return next()
7151
}
72-
73-
private mintKey: express.Handler = async (req, res, next) => {
74-
if (!this.mintKeyPromise) {
75-
this.mintKeyPromise = new Promise(async (resolve) => {
76-
const keyPath = path.join(req.args["user-data-dir"], "serve-web-key-half")
77-
logger.debug(`Reading server web key half from ${keyPath}`)
78-
try {
79-
resolve(await fs.readFile(keyPath))
80-
return
81-
} catch (error: any) {
82-
if (error.code !== "ENOENT") {
83-
logError(logger, `read ${keyPath}`, error)
84-
}
85-
}
86-
// VS Code wants 256 bits.
87-
const key = crypto.randomBytes(32)
88-
try {
89-
await fs.writeFile(keyPath, key)
90-
} catch (error: any) {
91-
logError(logger, `write ${keyPath}`, error)
92-
}
93-
resolve(key)
94-
})
52+
// See ../../../lib/vscode/src/vs/server/node/server.main.ts:72.
53+
const createVSServer = await loadAMDModule<CreateServer>("vs/server/node/server.main", "createServer")
54+
try {
55+
vscodeServer = await createVSServer(null, {
56+
...(await toCodeArgs(req.args)),
57+
"without-connection-token": true,
58+
})
59+
} catch (error) {
60+
logError(logger, "CodeServerRouteWrapper", error)
61+
if (isDevMode) {
62+
return next(new Error((error instanceof Error ? error.message : error) + " (VS Code may still be compiling)"))
9563
}
96-
const key = await this.mintKeyPromise
97-
res.end(key)
64+
return next(error)
9865
}
66+
return next()
67+
}
9968

100-
private $root: express.Handler = async (req, res, next) => {
101-
const isAuthenticated = await authenticated(req)
102-
const NO_FOLDER_OR_WORKSPACE_QUERY = !req.query.folder && !req.query.workspace
103-
// Ew means the workspace was closed so clear the last folder/workspace.
104-
const FOLDER_OR_WORKSPACE_WAS_CLOSED = req.query.ew
105-
106-
if (!isAuthenticated) {
107-
const to = self(req)
108-
return redirect(req, res, "login", {
109-
to: to !== "/" ? to : undefined,
110-
})
111-
}
112-
113-
if (NO_FOLDER_OR_WORKSPACE_QUERY && !FOLDER_OR_WORKSPACE_WAS_CLOSED) {
114-
const settings = await req.settings.read()
115-
const lastOpened = settings.query || {}
116-
// This flag disables the last opened behavior
117-
const IGNORE_LAST_OPENED = req.args["ignore-last-opened"]
118-
const HAS_LAST_OPENED_FOLDER_OR_WORKSPACE = lastOpened.folder || lastOpened.workspace
119-
const HAS_FOLDER_OR_WORKSPACE_FROM_CLI = req.args._.length > 0
120-
const to = self(req)
121-
122-
let folder = undefined
123-
let workspace = undefined
124-
125-
// Redirect to the last folder/workspace if nothing else is opened.
126-
if (HAS_LAST_OPENED_FOLDER_OR_WORKSPACE && !IGNORE_LAST_OPENED) {
127-
folder = lastOpened.folder
128-
workspace = lastOpened.workspace
129-
} else if (HAS_FOLDER_OR_WORKSPACE_FROM_CLI) {
130-
const lastEntry = path.resolve(req.args._[req.args._.length - 1])
131-
const entryIsFile = await isFile(lastEntry)
132-
const IS_WORKSPACE_FILE = entryIsFile && path.extname(lastEntry) === ".code-workspace"
133-
134-
if (IS_WORKSPACE_FILE) {
135-
workspace = lastEntry
136-
} else if (!entryIsFile) {
137-
folder = lastEntry
138-
}
139-
}
140-
141-
if (folder || workspace) {
142-
return redirect(req, res, to, {
143-
folder,
144-
workspace,
145-
})
146-
}
147-
}
148-
149-
// Store the query parameters so we can use them on the next load. This
150-
// also allows users to create functionality around query parameters.
151-
await req.settings.write({ query: req.query })
152-
153-
next()
154-
}
155-
156-
private $proxyRequest: express.Handler = async (req, res, next) => {
157-
this._codeServerMain.handleRequest(req, res)
158-
}
159-
160-
private $proxyWebsocket = async (req: WebsocketRequest) => {
161-
const wrappedSocket = await this._socketProxyProvider.createProxy(req.ws)
162-
// This should actually accept a duplex stream but it seems Code has not
163-
// been updated to match the Node 16 types so cast for now. There does not
164-
// appear to be any code specific to sockets so this should be fine.
165-
this._codeServerMain.handleUpgrade(req, wrappedSocket as net.Socket)
166-
167-
req.ws.resume()
69+
router.get("/", ensureVSCodeLoaded, async (req, res, next) => {
70+
const isAuthenticated = await authenticated(req)
71+
const NO_FOLDER_OR_WORKSPACE_QUERY = !req.query.folder && !req.query.workspace
72+
// Ew means the workspace was closed so clear the last folder/workspace.
73+
const FOLDER_OR_WORKSPACE_WAS_CLOSED = req.query.ew
74+
75+
if (!isAuthenticated) {
76+
const to = self(req)
77+
return redirect(req, res, "login", {
78+
to: to !== "/" ? to : undefined,
79+
})
16880
}
16981

170-
//#endregion
171-
172-
/**
173-
* Fetches a code server instance asynchronously to avoid an initial memory overhead.
174-
*/
175-
private ensureCodeServerLoaded: express.Handler = async (req, _res, next) => {
176-
if (this._codeServerMain) {
177-
// Already loaded...
178-
return next()
82+
if (NO_FOLDER_OR_WORKSPACE_QUERY && !FOLDER_OR_WORKSPACE_WAS_CLOSED) {
83+
const settings = await req.settings.read()
84+
const lastOpened = settings.query || {}
85+
// This flag disables the last opened behavior
86+
const IGNORE_LAST_OPENED = req.args["ignore-last-opened"]
87+
const HAS_LAST_OPENED_FOLDER_OR_WORKSPACE = lastOpened.folder || lastOpened.workspace
88+
const HAS_FOLDER_OR_WORKSPACE_FROM_CLI = req.args._.length > 0
89+
const to = self(req)
90+
91+
let folder = undefined
92+
let workspace = undefined
93+
94+
// Redirect to the last folder/workspace if nothing else is opened.
95+
if (HAS_LAST_OPENED_FOLDER_OR_WORKSPACE && !IGNORE_LAST_OPENED) {
96+
folder = lastOpened.folder
97+
workspace = lastOpened.workspace
98+
} else if (HAS_FOLDER_OR_WORKSPACE_FROM_CLI) {
99+
const lastEntry = path.resolve(req.args._[req.args._.length - 1])
100+
const entryIsFile = await isFile(lastEntry)
101+
const IS_WORKSPACE_FILE = entryIsFile && path.extname(lastEntry) === ".code-workspace"
102+
103+
if (IS_WORKSPACE_FILE) {
104+
workspace = lastEntry
105+
} else if (!entryIsFile) {
106+
folder = lastEntry
107+
}
179108
}
180109

181-
// Create the server...
182-
183-
const { args } = req
184-
185-
// See ../../../lib/vscode/src/vs/server/node/server.main.ts:72.
186-
const createVSServer = await loadAMDModule<CreateServer>("vs/server/node/server.main", "createServer")
187-
188-
try {
189-
this._codeServerMain = await createVSServer(null, {
190-
...(await toCodeArgs(args)),
191-
"without-connection-token": true,
110+
if (folder || workspace) {
111+
return redirect(req, res, to, {
112+
folder,
113+
workspace,
192114
})
193-
} catch (error) {
194-
logError(logger, "CodeServerRouteWrapper", error)
195-
if (isDevMode) {
196-
return next(new Error((error instanceof Error ? error.message : error) + " (VS Code may still be compiling)"))
197-
}
198-
return next(error)
199115
}
200-
201-
return next()
202-
}
203-
204-
constructor() {
205-
this.router.get("/", this.ensureCodeServerLoaded, this.$root)
206-
this.router.get("/manifest.json", this.manifest)
207-
this.router.post("/mint-key", this.mintKey)
208-
this.router.all(/.*/, ensureAuthenticated, this.ensureCodeServerLoaded, this.$proxyRequest)
209-
this._wsRouterWrapper.ws(/.*/, ensureOrigin, ensureAuthenticated, this.ensureCodeServerLoaded, this.$proxyWebsocket)
210116
}
211117

212-
dispose() {
213-
this._codeServerMain?.dispose()
214-
this._socketProxyProvider.stop()
118+
// Store the query parameters so we can use them on the next load. This
119+
// also allows users to create functionality around query parameters.
120+
await req.settings.write({ query: req.query })
121+
122+
next()
123+
})
124+
125+
router.get("/manifest.json", async (req, res) => {
126+
const appName = req.args["app-name"] || "code-server"
127+
res.writeHead(200, { "Content-Type": "application/manifest+json" })
128+
129+
return res.end(
130+
replaceTemplates(
131+
req,
132+
JSON.stringify(
133+
{
134+
name: appName,
135+
short_name: appName,
136+
start_url: ".",
137+
display: "fullscreen",
138+
display_override: ["window-controls-overlay"],
139+
description: "Run Code on a remote server.",
140+
icons: [192, 512].map((size) => ({
141+
src: `{{BASE}}/_static/src/browser/media/pwa-icon-${size}.png`,
142+
type: "image/png",
143+
sizes: `${size}x${size}`,
144+
})),
145+
},
146+
null,
147+
2,
148+
),
149+
),
150+
)
151+
})
152+
153+
let mintKeyPromise: Promise<Buffer> | undefined
154+
router.post("/mint-key", async (req, res) => {
155+
if (!mintKeyPromise) {
156+
mintKeyPromise = new Promise(async (resolve) => {
157+
const keyPath = path.join(req.args["user-data-dir"], "serve-web-key-half")
158+
logger.debug(`Reading server web key half from ${keyPath}`)
159+
try {
160+
resolve(await fs.readFile(keyPath))
161+
return
162+
} catch (error: any) {
163+
if (error.code !== "ENOENT") {
164+
logError(logger, `read ${keyPath}`, error)
165+
}
166+
}
167+
// VS Code wants 256 bits.
168+
const key = crypto.randomBytes(32)
169+
try {
170+
await fs.writeFile(keyPath, key)
171+
} catch (error: any) {
172+
logError(logger, `write ${keyPath}`, error)
173+
}
174+
resolve(key)
175+
})
215176
}
177+
const key = await mintKeyPromise
178+
res.end(key)
179+
})
180+
181+
router.all(/.*/, ensureAuthenticated, ensureVSCodeLoaded, async (req, res) => {
182+
vscodeServer!.handleRequest(req, res)
183+
})
184+
185+
const socketProxyProvider = new SocketProxyProvider()
186+
wsRouter.ws(/.*/, ensureOrigin, ensureAuthenticated, ensureVSCodeLoaded, async (req: WebsocketRequest) => {
187+
const wrappedSocket = await socketProxyProvider.createProxy(req.ws)
188+
// This should actually accept a duplex stream but it seems Code has not
189+
// been updated to match the Node 16 types so cast for now. There does not
190+
// appear to be any code specific to sockets so this should be fine.
191+
vscodeServer!.handleUpgrade(req, wrappedSocket as net.Socket)
192+
193+
req.ws.resume()
194+
})
195+
196+
export function dispose() {
197+
vscodeServer?.dispose()
198+
socketProxyProvider.stop()
216199
}

0 commit comments

Comments
 (0)