Merge pull request #1934 from cdr/plugin

Add plugin system for adding http endpoints
This commit is contained in:
Asher 2020-08-13 16:59:44 -05:00 committed by GitHub
commit 7d02f34f71
No known key found for this signature in database
GPG Key ID: 4AEE18F83AFDEB23
16 changed files with 188 additions and 136 deletions

1
.gitignore vendored
View File

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

View File

@ -18,7 +18,7 @@ main() {
fi
parcel build \
--public-url "/static/$(git rev-parse HEAD)/dist" \
--public-url "." \
--out-dir dist \
$([[ $MINIFY ]] || echo --no-minify) \
src/browser/register.ts \

View File

@ -21,6 +21,12 @@ main() {
rsync README.md "$RELEASE_PATH"
rsync LICENSE.txt "$RELEASE_PATH"
rsync ./lib/vscode/ThirdPartyNotices.txt "$RELEASE_PATH"
# code-server exports types which can be imported and used by plugins. Those
# types import ipc.d.ts but it isn't included in the final vscode build so
# we'll copy it ourselves here.
mkdir -p "$RELEASE_PATH/lib/vscode/src/vs/server"
rsync ./lib/vscode/src/vs/server/ipc.d.ts "$RELEASE_PATH/lib/vscode/src/vs/server"
}
bundle_code_server() {

View File

@ -722,10 +722,10 @@ index eab8591492..26668701f7 100644
options.logService.error(`${logPrefix} socketFactory.connect() failed. Error:`);
diff --git a/src/vs/server/browser/client.ts b/src/vs/server/browser/client.ts
new file mode 100644
index 0000000000..8fb2a87303
index 0000000000..3c0703b717
--- /dev/null
+++ b/src/vs/server/browser/client.ts
@@ -0,0 +1,208 @@
@@ -0,0 +1,189 @@
+import { Emitter } from 'vs/base/common/event';
+import { URI } from 'vs/base/common/uri';
+import { localize } from 'vs/nls';
@ -761,31 +761,12 @@ index 0000000000..8fb2a87303
+};
+
+/**
+ * Get options embedded in the HTML from the server.
+ * Get options embedded in the HTML.
+ */
+export const getOptions = <T extends Options>(): T => {
+ if (typeof document === "undefined") {
+ return {} as T;
+ }
+ const el = document.getElementById("coder-options");
+ try {
+ if (!el) {
+ throw new Error("no options element");
+ }
+ const value = el.getAttribute("data-settings");
+ if (!value) {
+ throw new Error("no options value");
+ }
+ const options = JSON.parse(value);
+ const parts = window.location.pathname.replace(/^\//g, "").split("/");
+ parts[parts.length - 1] = options.base;
+ const url = new URL(window.location.origin + "/" + parts.join("/"));
+ return {
+ ...options,
+ base: normalize(url.pathname, true),
+ };
+ return JSON.parse(document.getElementById("coder-options")!.getAttribute("data-settings")!);
+ } catch (error) {
+ console.warn(error);
+ return {} as T;
+ }
+};
@ -1306,17 +1287,15 @@ 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..7e1cd270c8
--- /dev/null
+++ b/src/vs/server/ipc.d.ts
@@ -0,0 +1,117 @@
@@ -0,0 +1,115 @@
+/**
+ * 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
+}
+

View File

@ -37,6 +37,7 @@ class Watcher {
const vscode = cp.spawn("yarn", ["watch"], { cwd: this.vscodeSourcePath })
const tsc = cp.spawn("tsc", ["--watch", "--pretty", "--preserveWatchOutput"], { cwd: this.rootPath })
const plugin = cp.spawn("yarn", ["build", "--watch"], { cwd: process.env.PLUGIN_DIR })
const bundler = this.createBundler()
const cleanup = (code?: number | null): void => {
@ -48,6 +49,10 @@ class Watcher {
tsc.removeAllListeners()
tsc.kill()
Watcher.log("killing plugin")
plugin.removeAllListeners()
plugin.kill()
if (server) {
Watcher.log("killing server")
server.removeAllListeners()
@ -69,6 +74,10 @@ class Watcher {
Watcher.log("tsc terminated unexpectedly")
cleanup(code)
})
plugin.on("exit", (code) => {
Watcher.log("plugin terminated unexpectedly")
cleanup(code)
})
const bundle = bundler.bundle().catch(() => {
Watcher.log("parcel watcher terminated unexpectedly")
cleanup(1)
@ -82,6 +91,7 @@ class Watcher {
vscode.stderr.on("data", (d) => process.stderr.write(d))
tsc.stderr.on("data", (d) => process.stderr.write(d))
plugin.stderr.on("data", (d) => process.stderr.write(d))
// From https://github.com/chalk/ansi-regex
const pattern = [
@ -140,6 +150,16 @@ class Watcher {
bundle.then(restartServer)
}
})
onLine(plugin, (line, original) => {
// tsc outputs blank lines; skip them.
if (line !== "") {
console.log("[plugin]", original)
}
if (line.includes("Watching for file changes")) {
bundle.then(restartServer)
}
})
}
private createBundler(out = "dist"): Bundler {
@ -150,7 +170,7 @@ class Watcher {
cacheDir: path.join(this.rootPath, ".cache"),
minify: !!process.env.MINIFY,
logLevel: 1,
publicUrl: "/static/development/dist",
publicUrl: ".",
},
)
}

View File

@ -7,32 +7,32 @@
"description": "Run editors on a remote server.",
"icons": [
{
"src": "{{BASE}}/static/{{COMMIT}}/src/browser/media/pwa-icon-96.png",
"src": "{{CS_STATIC_BASE}}/src/browser/media/pwa-icon-96.png",
"type": "image/png",
"sizes": "96x96"
},
{
"src": "{{BASE}}/static/{{COMMIT}}/src/browser/media/pwa-icon-128.png",
"src": "{{CS_STATIC_BASE}}/src/browser/media/pwa-icon-128.png",
"type": "image/png",
"sizes": "128x128"
},
{
"src": "{{BASE}}/static/{{COMMIT}}/src/browser/media/pwa-icon-192.png",
"src": "{{CS_STATIC_BASE}}/src/browser/media/pwa-icon-192.png",
"type": "image/png",
"sizes": "192x192"
},
{
"src": "{{BASE}}/static/{{COMMIT}}/src/browser/media/pwa-icon-256.png",
"src": "{{CS_STATIC_BASE}}/src/browser/media/pwa-icon-256.png",
"type": "image/png",
"sizes": "256x256"
},
{
"src": "{{BASE}}/static/{{COMMIT}}/src/browser/media/pwa-icon-384.png",
"src": "{{CS_STATIC_BASE}}/src/browser/media/pwa-icon-384.png",
"type": "image/png",
"sizes": "384x384"
},
{
"src": "{{BASE}}/static/{{COMMIT}}/src/browser/media/pwa-icon-512.png",
"src": "{{CS_STATIC_BASE}}/src/browser/media/pwa-icon-512.png",
"type": "image/png",
"sizes": "512x512"
}

View File

@ -11,14 +11,10 @@
content="style-src 'self'; manifest-src 'self'; img-src 'self' data:; font-src 'self' data:;"
/>
<title>{{ERROR_TITLE}} - code-server</title>
<link rel="icon" href="{{BASE}}/static/{{COMMIT}}/src/browser/media/favicon.ico" type="image/x-icon" />
<link
rel="manifest"
href="{{BASE}}/static/{{COMMIT}}/src/browser/media/manifest.json"
crossorigin="use-credentials"
/>
<link rel="apple-touch-icon" href="{{BASE}}/static/{{COMMIT}}/src/browser/media/pwa-icon-384.png" />
<link href="{{BASE}}/static/{{COMMIT}}/dist/register.css" rel="stylesheet" />
<link rel="icon" href="{{CS_STATIC_BASE}}/src/browser/media/favicon.ico" type="image/x-icon" />
<link rel="manifest" href="{{CS_STATIC_BASE}}/src/browser/media/manifest.json" crossorigin="use-credentials" />
<link rel="apple-touch-icon" href="{{CS_STATIC_BASE}}/src/browser/media/pwa-icon-384.png" />
<link href="{{CS_STATIC_BASE}}/dist/register.css" rel="stylesheet" />
<meta id="coder-options" data-settings="{{OPTIONS}}" />
</head>
<body>
@ -33,6 +29,6 @@
</div>
</div>
</div>
<script data-cfasync="false" src="{{BASE}}/static/{{COMMIT}}/dist/register.js"></script>
<script data-cfasync="false" src="{{CS_STATIC_BASE}}/dist/register.js"></script>
</body>
</html>

View File

@ -11,14 +11,10 @@
content="style-src 'self'; script-src 'self' 'unsafe-inline'; manifest-src 'self'; img-src 'self' data:; font-src 'self' data:;"
/>
<title>code-server login</title>
<link rel="icon" href="{{BASE}}/static/{{COMMIT}}/src/browser/media/favicon.ico" type="image/x-icon" />
<link
rel="manifest"
href="{{BASE}}/static/{{COMMIT}}/src/browser/media/manifest.json"
crossorigin="use-credentials"
/>
<link rel="apple-touch-icon" href="{{BASE}}/static/{{COMMIT}}/src/browser/media/pwa-icon-384.png" />
<link href="{{BASE}}/static/{{COMMIT}}/dist/register.css" rel="stylesheet" />
<link rel="icon" href="{{CS_STATIC_BASE}}/src/browser/media/favicon.ico" type="image/x-icon" />
<link rel="manifest" href="{{CS_STATIC_BASE}}/src/browser/media/manifest.json" crossorigin="use-credentials" />
<link rel="apple-touch-icon" href="{{CS_STATIC_BASE}}/src/browser/media/pwa-icon-384.png" />
<link href="{{CS_STATIC_BASE}}/dist/register.css" rel="stylesheet" />
<meta id="coder-options" data-settings="{{OPTIONS}}" />
</head>
<body>
@ -50,7 +46,7 @@
</div>
</div>
</body>
<script data-cfasync="false" src="{{BASE}}/static/{{COMMIT}}/dist/register.js"></script>
<script data-cfasync="false" src="{{CS_STATIC_BASE}}/dist/register.js"></script>
<script>
const parts = window.location.pathname.replace(/^\//g, "").split("/")
parts[parts.length - 1] = "{{BASE}}"

View File

@ -24,21 +24,17 @@
<meta id="vscode-remote-nls-configuration" data-settings="{{NLS_CONFIGURATION}}" />
<!-- Workbench Icon/Manifest/CSS -->
<link rel="icon" href="{{BASE}}/static/{{COMMIT}}/src/browser/media/favicon.ico" type="image/x-icon" />
<link
rel="manifest"
href="{{BASE}}/static/{{COMMIT}}/src/browser/media/manifest.json"
crossorigin="use-credentials"
/>
<link rel="icon" href="{{CS_STATIC_BASE}}/src/browser/media/favicon.ico" type="image/x-icon" />
<link rel="manifest" href="{{CS_STATIC_BASE}}/src/browser/media/manifest.json" crossorigin="use-credentials" />
<!-- PROD_ONLY
<link data-name="vs/workbench/workbench.web.api" rel="stylesheet" href="{{BASE}}/static/{{COMMIT}}/lib/vscode/out/vs/workbench/workbench.web.api.css">
<link data-name="vs/workbench/workbench.web.api" rel="stylesheet" href="{{CS_STATIC_BASE}}/lib/vscode/out/vs/workbench/workbench.web.api.css">
END_PROD_ONLY -->
<link rel="apple-touch-icon" href="{{BASE}}/static/{{COMMIT}}/src/browser/media/pwa-icon-384.png" />
<link rel="apple-touch-icon" href="{{CS_STATIC_BASE}}/src/browser/media/pwa-icon-384.png" />
<meta name="apple-mobile-web-app-capable" content="yes" />
<!-- Prefetch to avoid waterfall -->
<!-- PROD_ONLY
<link rel="prefetch" href="{{BASE}}/static/{{COMMIT}}/lib/vscode/node_modules/semver-umd/lib/semver-umd.js">
<link rel="prefetch" href="{{CS_STATIC_BASE}}/lib/vscode/node_modules/semver-umd/lib/semver-umd.js">
END_PROD_ONLY -->
<meta id="coder-options" data-settings="{{OPTIONS}}" />
@ -48,10 +44,6 @@
<!-- Startup (do not modify order of script tags!) -->
<script>
const parts = window.location.pathname.replace(/^\//g, "").split("/")
parts[parts.length - 1] = "{{BASE}}"
const url = new URL(window.location.origin + "/" + parts.join("/"))
const staticBase = url.href.replace(/\/+$/, "") + "/static/{{COMMIT}}/lib/vscode"
let nlsConfig
try {
nlsConfig = JSON.parse(document.getElementById("vscode-remote-nls-configuration").getAttribute("data-settings"))
@ -64,7 +56,7 @@
}
// FIXME: Only works if path separators are /.
const path = nlsConfig._resolvedLanguagePackCoreLocation + "/" + bundle.replace(/\//g, "!") + ".nls.json"
fetch(`${url.href}/resource/?path=${encodeURIComponent(path)}`)
fetch(`{{BASE}}/resource/?path=${encodeURIComponent(path)}`)
.then((response) => response.json())
.then((json) => {
bundles[bundle] = json
@ -77,26 +69,26 @@
/* Probably fine. */
}
self.require = {
baseUrl: `${staticBase}/out`,
baseUrl: "{{CS_STATIC_BASE}}/lib/vscode/out",
paths: {
"vscode-textmate": `${staticBase}/node_modules/vscode-textmate/release/main`,
"vscode-oniguruma": `${staticBase}/node_modules/vscode-oniguruma/release/main`,
xterm: `${staticBase}/node_modules/xterm/lib/xterm.js`,
"xterm-addon-search": `${staticBase}/node_modules/xterm-addon-search/lib/xterm-addon-search.js`,
"xterm-addon-unicode11": `${staticBase}/node_modules/xterm-addon-unicode11/lib/xterm-addon-unicode11.js`,
"xterm-addon-webgl": `${staticBase}/node_modules/xterm-addon-webgl/lib/xterm-addon-webgl.js`,
"semver-umd": `${staticBase}/node_modules/semver-umd/lib/semver-umd.js`,
"iconv-lite-umd": `${staticBase}/node_modules/iconv-lite-umd/lib/iconv-lite-umd.js`,
jschardet: `${staticBase}/node_modules/jschardet/dist/jschardet.min.js`,
"vscode-textmate": `../node_modules/vscode-textmate/release/main`,
"vscode-oniguruma": `../node_modules/vscode-oniguruma/release/main`,
xterm: `../node_modules/xterm/lib/xterm.js`,
"xterm-addon-search": `../node_modules/xterm-addon-search/lib/xterm-addon-search.js`,
"xterm-addon-unicode11": `../node_modules/xterm-addon-unicode11/lib/xterm-addon-unicode11.js`,
"xterm-addon-webgl": `../node_modules/xterm-addon-webgl/lib/xterm-addon-webgl.js`,
"semver-umd": `../node_modules/semver-umd/lib/semver-umd.js`,
"iconv-lite-umd": `../node_modules/iconv-lite-umd/lib/iconv-lite-umd.js`,
jschardet: `../node_modules/jschardet/dist/jschardet.min.js`,
},
"vs/nls": nlsConfig,
}
</script>
<script data-cfasync="false" src="{{BASE}}/static/{{COMMIT}}/dist/register.js"></script>
<script data-cfasync="false" src="{{BASE}}/static/{{COMMIT}}/lib/vscode/out/vs/loader.js"></script>
<script data-cfasync="false" src="{{CS_STATIC_BASE}}/dist/register.js"></script>
<script data-cfasync="false" src="{{CS_STATIC_BASE}}/lib/vscode/out/vs/loader.js"></script>
<!-- PROD_ONLY
<script data-cfasync="false" src="{{BASE}}/static/{{COMMIT}}/lib/vscode/out/vs/workbench/workbench.web.api.nls.js"></script>
<script data-cfasync="false" src="{{BASE}}/static/{{COMMIT}}/lib/vscode/out/vs/workbench/workbench.web.api.js"></script>
<script data-cfasync="false" src="{{CS_STATIC_BASE}}/lib/vscode/out/vs/workbench/workbench.web.api.nls.js"></script>
<script data-cfasync="false" src="{{CS_STATIC_BASE}}/lib/vscode/out/vs/workbench/workbench.web.api.js"></script>
END_PROD_ONLY -->
<script>
require(["vs/code/browser/workbench/workbench"], function () {})

View File

@ -7,7 +7,7 @@ import "./pages/global.css"
import "./pages/login.css"
if ("serviceWorker" in navigator) {
const path = normalize(`${options.base}/static/${options.commit}/dist/serviceWorker.js`)
const path = normalize(`${options.csStaticBase}/dist/serviceWorker.js`)
navigator.serviceWorker
.register(path, {
scope: options.base || "/",

View File

@ -2,9 +2,8 @@ import { logger, field } from "@coder/logger"
export interface Options {
base: string
commit: string
csStaticBase: string
logLevel: number
pid?: number
}
/**
@ -44,21 +43,28 @@ export const trimSlashes = (url: string): string => {
return url.replace(/^\/+|\/+$/g, "")
}
/**
* Resolve a relative base against the window location. This is used for
* anything that doesn't work with a relative path.
*/
export const resolveBase = (base?: string): string => {
// After resolving the base will either start with / or be an empty string.
if (!base || base.startsWith("/")) {
return base ?? ""
}
const parts = location.pathname.split("/")
parts[parts.length - 1] = base
const url = new URL(location.origin + "/" + parts.join("/"))
return normalize(url.pathname)
}
/**
* Get options embedded in the HTML or query params.
*/
export const getOptions = <T extends Options>(): T => {
let options: T
try {
const el = document.getElementById("coder-options")
if (!el) {
throw new Error("no options element")
}
const value = el.getAttribute("data-settings")
if (!value) {
throw new Error("no options value")
}
options = JSON.parse(value)
options = JSON.parse(document.getElementById("coder-options")!.getAttribute("data-settings")!)
} catch (error) {
options = {} as T
}
@ -72,15 +78,10 @@ export const getOptions = <T extends Options>(): T => {
}
}
if (typeof options.logLevel !== "undefined") {
logger.level = options.logLevel
}
if (options.base) {
const parts = location.pathname.replace(/^\//g, "").split("/")
parts[parts.length - 1] = options.base
const url = new URL(location.origin + "/" + parts.join("/"))
options.base = normalize(url.pathname, true)
}
logger.level = options.logLevel
options.base = resolveBase(options.base)
options.csStaticBase = resolveBase(options.csStaticBase)
logger.debug("got options", field("options", options))

View File

@ -8,10 +8,9 @@ import { HttpProvider, HttpResponse, Route } from "../http"
import { pathToFsPath } from "../util"
/**
* Static file HTTP provider. Regular static requests (the path is the request
* itself) do not require authentication and they only allow access to resources
* within the application. Requests for tars (the path is in a query parameter)
* do require permissions and can access any directory.
* Static file HTTP provider. Static requests do not require authentication if
* the resource is in the application's directory except requests to serve a
* directory as a tar which always requires authentication.
*/
export class StaticHttpProvider extends HttpProvider {
public async handleRequest(route: Route, request: http.IncomingMessage): Promise<HttpResponse> {
@ -22,7 +21,7 @@ export class StaticHttpProvider extends HttpProvider {
return this.getTarredResource(request, pathToFsPath(route.query.tar))
}
const response = await this.getReplacedResource(route)
const response = await this.getReplacedResource(request, route)
if (!this.isDev) {
response.cache = true
}
@ -32,17 +31,25 @@ export class StaticHttpProvider extends HttpProvider {
/**
* Return a resource with variables replaced where necessary.
*/
protected async getReplacedResource(route: Route): Promise<HttpResponse> {
protected async getReplacedResource(request: http.IncomingMessage, route: Route): Promise<HttpResponse> {
// The first part is always the commit (for caching purposes).
const split = route.requestPath.split("/").slice(1)
const resourcePath = path.resolve("/", ...split)
// Make sure it's in code-server or a plugin.
const validPaths = [this.rootPath, process.env.PLUGIN_DIR]
if (!validPaths.find((p) => p && resourcePath.startsWith(p))) {
this.ensureAuthenticated(request)
}
switch (split[split.length - 1]) {
case "manifest.json": {
const response = await this.getUtf8Resource(this.rootPath, ...split)
const response = await this.getUtf8Resource(resourcePath)
return this.replaceTemplates(route, response)
}
}
return this.getResource(this.rootPath, ...split)
return this.getResource(resourcePath)
}
/**

View File

@ -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<Options>(route, response, {
base: this.base(route),
commit: this.options.commit,
disableTelemetry: !!this.args["disable-telemetry"],
})
}

View File

@ -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"
import { plural } from "../common/util"
@ -78,6 +79,8 @@ const main = async (args: Args, cliArgs: Args, configArgs: Args): Promise<void>
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))

View File

@ -209,11 +209,11 @@ export abstract class HttpProvider {
/**
* Get the base relative to the provided route. For each slash we need to go
* up a directory. For example:
* / => ./
* /foo => ./
* /foo/ => ./../
* /foo/bar => ./../
* /foo/bar/ => ./../../
* / => .
* /foo => .
* /foo/ => ./..
* /foo/bar => ./..
* /foo/bar/ => ./../..
*/
public base(route: Route): string {
const depth = (route.originalPath.match(/\//g) || []).length
@ -235,30 +235,23 @@ export abstract class HttpProvider {
/**
* Replace common templates strings.
*/
protected replaceTemplates(route: Route, response: HttpStringFileResponse, sessionId?: string): HttpStringFileResponse
protected replaceTemplates<T extends object>(
route: Route,
response: HttpStringFileResponse,
options: T,
): HttpStringFileResponse
protected replaceTemplates(
route: Route,
response: HttpStringFileResponse,
sessionIdOrOptions?: string | object,
extraOptions?: Omit<T, "base" | "csStaticBase" | "logLevel">,
): 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 base = this.base(route)
const options: Options = {
base,
csStaticBase: base + "/static/" + this.options.commit + this.rootPath,
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(/{{BASE}}/g, options.base)
.replace(/{{CS_STATIC_BASE}}/g, options.csStaticBase)
.replace(/"{{OPTIONS}}"/, `'${JSON.stringify(options)}'`)
return response
}
@ -664,7 +657,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)
}

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

@ -0,0 +1,60 @@
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
}
/**
* Intercept imports so we can inject code-server when the plugin tries to
* import it.
*/
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.error(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)
}
}
if (process.env.PLUGIN_DIR) {
await loadPlugin(process.env.PLUGIN_DIR, httpServer, args)
}
}