Add plugin system

This commit is contained in:
Asher 2020-07-28 15:06:15 -05:00
parent 1c8eede1aa
commit bac948ea6f
No known key found for this signature in database
GPG Key ID: D63C1EF81242354A
6 changed files with 67 additions and 22 deletions

1
.gitignore vendored
View File

@ -10,3 +10,4 @@ release-gcp/
release-images/ release-images/
node_modules node_modules
node-* node-*
/plugins

View File

@ -1306,17 +1306,16 @@ index 0000000000..56331ff1fc
+require('../../bootstrap-amd').load('vs/server/entry'); +require('../../bootstrap-amd').load('vs/server/entry');
diff --git a/src/vs/server/ipc.d.ts b/src/vs/server/ipc.d.ts diff --git a/src/vs/server/ipc.d.ts b/src/vs/server/ipc.d.ts
new file mode 100644 new file mode 100644
index 0000000000..0a9c95d50e index 0000000000..5cc3e1f0f4
--- /dev/null --- /dev/null
+++ b/src/vs/server/ipc.d.ts +++ 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 + * External interfaces for integration into code-server over IPC. No vs imports
+ * should be made in this file. + * should be made in this file.
+ */ + */
+export interface Options { +export interface Options {
+ base: string + base: string
+ commit: string
+ disableTelemetry: boolean + disableTelemetry: boolean
+} +}
+ +

View File

@ -200,8 +200,6 @@ export class VscodeHttpProvider extends HttpProvider {
.replace(`"{{WORKBENCH_WEB_CONFIGURATION}}"`, `'${JSON.stringify(options.workbenchWebConfiguration)}'`) .replace(`"{{WORKBENCH_WEB_CONFIGURATION}}"`, `'${JSON.stringify(options.workbenchWebConfiguration)}'`)
.replace(`"{{NLS_CONFIGURATION}}"`, `'${JSON.stringify(options.nlsConfiguration)}'`) .replace(`"{{NLS_CONFIGURATION}}"`, `'${JSON.stringify(options.nlsConfiguration)}'`)
return this.replaceTemplates<Options>(route, response, { return this.replaceTemplates<Options>(route, response, {
base: this.base(route),
commit: this.options.commit,
disableTelemetry: !!this.args["disable-telemetry"], disableTelemetry: !!this.args["disable-telemetry"],
}) })
} }

View File

@ -9,7 +9,8 @@ import { UpdateHttpProvider } from "./app/update"
import { VscodeHttpProvider } from "./app/vscode" import { VscodeHttpProvider } from "./app/vscode"
import { Args, bindAddrFromAllSources, optionDescriptions, parse, readConfigFile, setDefaults } from "./cli" import { Args, bindAddrFromAllSources, optionDescriptions, parse, readConfigFile, setDefaults } from "./cli"
import { AuthType, HttpServer, HttpServerOptions } from "./http" 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" import { ipcMain, wrap } from "./wrapper"
process.on("uncaughtException", (error) => { process.on("uncaughtException", (error) => {
@ -77,6 +78,8 @@ const main = async (args: Args, cliArgs: Args, configArgs: Args): Promise<void>
httpServer.registerHttpProvider("/login", LoginHttpProvider, args.config!, envPassword) httpServer.registerHttpProvider("/login", LoginHttpProvider, args.config!, envPassword)
httpServer.registerHttpProvider("/static", StaticHttpProvider) httpServer.registerHttpProvider("/static", StaticHttpProvider)
await loadPlugins(httpServer, args)
ipcMain().onDispose(() => { ipcMain().onDispose(() => {
httpServer.dispose().then((errors) => { httpServer.dispose().then((errors) => {
errors.forEach((error) => logger.error(error.message)) errors.forEach((error) => logger.error(error.message))

View File

@ -235,30 +235,22 @@ export abstract class HttpProvider {
/** /**
* Replace common templates strings. * Replace common templates strings.
*/ */
protected replaceTemplates(route: Route, response: HttpStringFileResponse, sessionId?: string): HttpStringFileResponse
protected replaceTemplates<T extends object>( protected replaceTemplates<T extends object>(
route: Route, route: Route,
response: HttpStringFileResponse, response: HttpStringFileResponse,
options: T, extraOptions?: Omit<T, "base" | "csStaticBase" | "logLevel">,
): HttpStringFileResponse
protected replaceTemplates(
route: Route,
response: HttpStringFileResponse,
sessionIdOrOptions?: string | object,
): HttpStringFileResponse { ): HttpStringFileResponse {
if (typeof sessionIdOrOptions === "undefined" || typeof sessionIdOrOptions === "string") { const options: Options = {
sessionIdOrOptions = { base: this.base(route),
base: this.base(route), commit: this.options.commit,
commit: this.options.commit, logLevel: logger.level,
logLevel: logger.level, ...extraOptions,
sessionID: sessionIdOrOptions,
} as Options
} }
response.content = response.content response.content = response.content
.replace(/{{COMMIT}}/g, this.options.commit) .replace(/{{COMMIT}}/g, this.options.commit)
.replace(/{{TO}}/g, Array.isArray(route.query.to) ? route.query.to[0] : route.query.to || "/dashboard") .replace(/{{TO}}/g, Array.isArray(route.query.to) ? route.query.to[0] : route.query.to || "/dashboard")
.replace(/{{BASE}}/g, this.base(route)) .replace(/{{BASE}}/g, this.base(route))
.replace(/"{{OPTIONS}}"/, `'${JSON.stringify(sessionIdOrOptions)}'`) .replace(/"{{OPTIONS}}"/, `'${JSON.stringify(options)}'`)
return response return response
} }
@ -664,7 +656,7 @@ export class HttpServer {
e = new HttpError("Not found", HttpCode.NotFound) e = new HttpError("Not found", HttpCode.NotFound)
} }
const code = typeof e.code === "number" ? e.code : HttpCode.ServerError 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) { if (code >= HttpCode.ServerError) {
logger.error(error.stack) logger.error(error.stack)
} }

52
src/node/plugin.ts Normal file
View File

@ -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<void> => {
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<void> => {
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<void> => {
try {
await _loadPlugins(httpServer, args)
} catch (error) {
if (error.code !== "ENOENT") {
logger.warn(error.message)
}
}
}