diff --git a/.gitignore b/.gitignore index 424cb9e7..616f9b01 100644 --- a/.gitignore +++ b/.gitignore @@ -10,3 +10,4 @@ release-gcp/ release-images/ node_modules node-* +/plugins diff --git a/ci/dev/vscode.patch b/ci/dev/vscode.patch index 03b9c5f6..9e46e2ec 100644 --- a/ci/dev/vscode.patch +++ b/ci/dev/vscode.patch @@ -1306,17 +1306,16 @@ index 0000000000..56331ff1fc +require('../../bootstrap-amd').load('vs/server/entry'); diff --git a/src/vs/server/ipc.d.ts b/src/vs/server/ipc.d.ts new file mode 100644 -index 0000000000..0a9c95d50e +index 0000000000..5cc3e1f0f4 --- /dev/null +++ b/src/vs/server/ipc.d.ts -@@ -0,0 +1,117 @@ +@@ -0,0 +1,116 @@ +/** + * External interfaces for integration into code-server over IPC. No vs imports + * should be made in this file. + */ +export interface Options { + base: string -+ commit: string + disableTelemetry: boolean +} + diff --git a/src/node/app/vscode.ts b/src/node/app/vscode.ts index 0de9cb3e..ed4f714e 100644 --- a/src/node/app/vscode.ts +++ b/src/node/app/vscode.ts @@ -200,8 +200,6 @@ export class VscodeHttpProvider extends HttpProvider { .replace(`"{{WORKBENCH_WEB_CONFIGURATION}}"`, `'${JSON.stringify(options.workbenchWebConfiguration)}'`) .replace(`"{{NLS_CONFIGURATION}}"`, `'${JSON.stringify(options.nlsConfiguration)}'`) return this.replaceTemplates(route, response, { - base: this.base(route), - commit: this.options.commit, disableTelemetry: !!this.args["disable-telemetry"], }) } diff --git a/src/node/entry.ts b/src/node/entry.ts index a030cb49..4514b589 100644 --- a/src/node/entry.ts +++ b/src/node/entry.ts @@ -9,7 +9,8 @@ import { UpdateHttpProvider } from "./app/update" import { VscodeHttpProvider } from "./app/vscode" import { Args, bindAddrFromAllSources, optionDescriptions, parse, readConfigFile, setDefaults } from "./cli" import { AuthType, HttpServer, HttpServerOptions } from "./http" -import { generateCertificate, hash, open, humanPath } from "./util" +import { loadPlugins } from "./plugin" +import { generateCertificate, hash, humanPath, open } from "./util" import { ipcMain, wrap } from "./wrapper" process.on("uncaughtException", (error) => { @@ -77,6 +78,8 @@ const main = async (args: Args, cliArgs: Args, configArgs: Args): Promise httpServer.registerHttpProvider("/login", LoginHttpProvider, args.config!, envPassword) httpServer.registerHttpProvider("/static", StaticHttpProvider) + await loadPlugins(httpServer, args) + ipcMain().onDispose(() => { httpServer.dispose().then((errors) => { errors.forEach((error) => logger.error(error.message)) diff --git a/src/node/http.ts b/src/node/http.ts index 216ff5a2..d3e8e44a 100644 --- a/src/node/http.ts +++ b/src/node/http.ts @@ -235,30 +235,22 @@ export abstract class HttpProvider { /** * Replace common templates strings. */ - protected replaceTemplates(route: Route, response: HttpStringFileResponse, sessionId?: string): HttpStringFileResponse protected replaceTemplates( route: Route, response: HttpStringFileResponse, - options: T, - ): HttpStringFileResponse - protected replaceTemplates( - route: Route, - response: HttpStringFileResponse, - sessionIdOrOptions?: string | object, + extraOptions?: Omit, ): HttpStringFileResponse { - if (typeof sessionIdOrOptions === "undefined" || typeof sessionIdOrOptions === "string") { - sessionIdOrOptions = { - base: this.base(route), - commit: this.options.commit, - logLevel: logger.level, - sessionID: sessionIdOrOptions, - } as Options + const options: Options = { + base: this.base(route), + commit: this.options.commit, + logLevel: logger.level, + ...extraOptions, } response.content = response.content .replace(/{{COMMIT}}/g, this.options.commit) .replace(/{{TO}}/g, Array.isArray(route.query.to) ? route.query.to[0] : route.query.to || "/dashboard") .replace(/{{BASE}}/g, this.base(route)) - .replace(/"{{OPTIONS}}"/, `'${JSON.stringify(sessionIdOrOptions)}'`) + .replace(/"{{OPTIONS}}"/, `'${JSON.stringify(options)}'`) return response } @@ -664,7 +656,7 @@ export class HttpServer { e = new HttpError("Not found", HttpCode.NotFound) } const code = typeof e.code === "number" ? e.code : HttpCode.ServerError - logger.debug("Request error", field("url", request.url), field("code", code)) + logger.debug("Request error", field("url", request.url), field("code", code), field("error", error)) if (code >= HttpCode.ServerError) { logger.error(error.stack) } diff --git a/src/node/plugin.ts b/src/node/plugin.ts new file mode 100644 index 00000000..0e024be0 --- /dev/null +++ b/src/node/plugin.ts @@ -0,0 +1,52 @@ +import { field, logger } from "@coder/logger" +import * as fs from "fs" +import * as path from "path" +import * as util from "util" +import { Args } from "./cli" +import { HttpServer } from "./http" + +/* eslint-disable @typescript-eslint/no-var-requires */ + +export type Activate = (httpServer: HttpServer, args: Args) => void + +export interface Plugin { + activate: Activate +} + +const originalLoad = require("module")._load +// eslint-disable-next-line @typescript-eslint/no-explicit-any +require("module")._load = function (request: string, parent: object, isMain: boolean): any { + return originalLoad.apply(this, [request.replace(/^code-server/, path.resolve(__dirname, "../..")), parent, isMain]) +} + +const loadPlugin = async (pluginPath: string, httpServer: HttpServer, args: Args): Promise => { + try { + const plugin: Plugin = require(pluginPath) + plugin.activate(httpServer, args) + logger.debug("Loaded plugin", field("name", path.basename(pluginPath))) + } catch (error) { + if (error.code !== "MODULE_NOT_FOUND") { + logger.warn(error.message) + } else { + logger.debug(error.message) + } + } +} + +const _loadPlugins = async (httpServer: HttpServer, args: Args): Promise => { + const pluginPath = path.resolve(__dirname, "../../plugins") + const files = await util.promisify(fs.readdir)(pluginPath, { + withFileTypes: true, + }) + await Promise.all(files.map((file) => loadPlugin(path.join(pluginPath, file.name), httpServer, args))) +} + +export const loadPlugins = async (httpServer: HttpServer, args: Args): Promise => { + try { + await _loadPlugins(httpServer, args) + } catch (error) { + if (error.code !== "ENOENT") { + logger.warn(error.message) + } + } +}