Compare commits
1 Commits
| Author | SHA1 | Date | |
|---|---|---|---|
|
|
f621a74062 |
1
.gitignore
vendored
1
.gitignore
vendored
@@ -10,4 +10,3 @@ release-gcp/
|
||||
release-images/
|
||||
node_modules
|
||||
node-*
|
||||
/plugins
|
||||
|
||||
@@ -18,7 +18,7 @@ main() {
|
||||
fi
|
||||
|
||||
parcel build \
|
||||
--public-url "." \
|
||||
--public-url "/static/$(git rev-parse HEAD)/dist" \
|
||||
--out-dir dist \
|
||||
$([[ $MINIFY ]] || echo --no-minify) \
|
||||
src/browser/register.ts \
|
||||
|
||||
@@ -21,12 +21,6 @@ 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() {
|
||||
|
||||
@@ -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..3c0703b717
|
||||
index 0000000000..8fb2a87303
|
||||
--- /dev/null
|
||||
+++ b/src/vs/server/browser/client.ts
|
||||
@@ -0,0 +1,189 @@
|
||||
@@ -0,0 +1,208 @@
|
||||
+import { Emitter } from 'vs/base/common/event';
|
||||
+import { URI } from 'vs/base/common/uri';
|
||||
+import { localize } from 'vs/nls';
|
||||
@@ -761,12 +761,31 @@ index 0000000000..3c0703b717
|
||||
+};
|
||||
+
|
||||
+/**
|
||||
+ * Get options embedded in the HTML.
|
||||
+ * Get options embedded in the HTML from the server.
|
||||
+ */
|
||||
+export const getOptions = <T extends Options>(): T => {
|
||||
+ if (typeof document === "undefined") {
|
||||
+ return {} as T;
|
||||
+ }
|
||||
+ const el = document.getElementById("coder-options");
|
||||
+ try {
|
||||
+ return JSON.parse(document.getElementById("coder-options")!.getAttribute("data-settings")!);
|
||||
+ 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),
|
||||
+ };
|
||||
+ } catch (error) {
|
||||
+ console.warn(error);
|
||||
+ return {} as T;
|
||||
+ }
|
||||
+};
|
||||
@@ -1287,15 +1306,17 @@ 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..7e1cd270c8
|
||||
index 0000000000..0a9c95d50e
|
||||
--- /dev/null
|
||||
+++ b/src/vs/server/ipc.d.ts
|
||||
@@ -0,0 +1,115 @@
|
||||
@@ -0,0 +1,117 @@
|
||||
+/**
|
||||
+ * 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
|
||||
+}
|
||||
+
|
||||
|
||||
@@ -37,7 +37,6 @@ 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 => {
|
||||
@@ -49,10 +48,6 @@ class Watcher {
|
||||
tsc.removeAllListeners()
|
||||
tsc.kill()
|
||||
|
||||
Watcher.log("killing plugin")
|
||||
plugin.removeAllListeners()
|
||||
plugin.kill()
|
||||
|
||||
if (server) {
|
||||
Watcher.log("killing server")
|
||||
server.removeAllListeners()
|
||||
@@ -74,10 +69,6 @@ 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)
|
||||
@@ -91,7 +82,6 @@ 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 = [
|
||||
@@ -150,16 +140,6 @@ 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 {
|
||||
@@ -170,7 +150,7 @@ class Watcher {
|
||||
cacheDir: path.join(this.rootPath, ".cache"),
|
||||
minify: !!process.env.MINIFY,
|
||||
logLevel: 1,
|
||||
publicUrl: ".",
|
||||
publicUrl: "/static/development/dist",
|
||||
},
|
||||
)
|
||||
}
|
||||
|
||||
@@ -7,32 +7,32 @@
|
||||
"description": "Run editors on a remote server.",
|
||||
"icons": [
|
||||
{
|
||||
"src": "{{CS_STATIC_BASE}}/src/browser/media/pwa-icon-96.png",
|
||||
"src": "{{BASE}}/static/{{COMMIT}}/src/browser/media/pwa-icon-96.png",
|
||||
"type": "image/png",
|
||||
"sizes": "96x96"
|
||||
},
|
||||
{
|
||||
"src": "{{CS_STATIC_BASE}}/src/browser/media/pwa-icon-128.png",
|
||||
"src": "{{BASE}}/static/{{COMMIT}}/src/browser/media/pwa-icon-128.png",
|
||||
"type": "image/png",
|
||||
"sizes": "128x128"
|
||||
},
|
||||
{
|
||||
"src": "{{CS_STATIC_BASE}}/src/browser/media/pwa-icon-192.png",
|
||||
"src": "{{BASE}}/static/{{COMMIT}}/src/browser/media/pwa-icon-192.png",
|
||||
"type": "image/png",
|
||||
"sizes": "192x192"
|
||||
},
|
||||
{
|
||||
"src": "{{CS_STATIC_BASE}}/src/browser/media/pwa-icon-256.png",
|
||||
"src": "{{BASE}}/static/{{COMMIT}}/src/browser/media/pwa-icon-256.png",
|
||||
"type": "image/png",
|
||||
"sizes": "256x256"
|
||||
},
|
||||
{
|
||||
"src": "{{CS_STATIC_BASE}}/src/browser/media/pwa-icon-384.png",
|
||||
"src": "{{BASE}}/static/{{COMMIT}}/src/browser/media/pwa-icon-384.png",
|
||||
"type": "image/png",
|
||||
"sizes": "384x384"
|
||||
},
|
||||
{
|
||||
"src": "{{CS_STATIC_BASE}}/src/browser/media/pwa-icon-512.png",
|
||||
"src": "{{BASE}}/static/{{COMMIT}}/src/browser/media/pwa-icon-512.png",
|
||||
"type": "image/png",
|
||||
"sizes": "512x512"
|
||||
}
|
||||
|
||||
@@ -11,10 +11,14 @@
|
||||
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="{{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" />
|
||||
<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" />
|
||||
<meta id="coder-options" data-settings="{{OPTIONS}}" />
|
||||
</head>
|
||||
<body>
|
||||
@@ -29,6 +33,6 @@
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
<script data-cfasync="false" src="{{CS_STATIC_BASE}}/dist/register.js"></script>
|
||||
<script data-cfasync="false" src="{{BASE}}/static/{{COMMIT}}/dist/register.js"></script>
|
||||
</body>
|
||||
</html>
|
||||
|
||||
@@ -11,10 +11,14 @@
|
||||
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="{{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" />
|
||||
<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" />
|
||||
<meta id="coder-options" data-settings="{{OPTIONS}}" />
|
||||
</head>
|
||||
<body>
|
||||
@@ -46,7 +50,7 @@
|
||||
</div>
|
||||
</div>
|
||||
</body>
|
||||
<script data-cfasync="false" src="{{CS_STATIC_BASE}}/dist/register.js"></script>
|
||||
<script data-cfasync="false" src="{{BASE}}/static/{{COMMIT}}/dist/register.js"></script>
|
||||
<script>
|
||||
const parts = window.location.pathname.replace(/^\//g, "").split("/")
|
||||
parts[parts.length - 1] = "{{BASE}}"
|
||||
|
||||
@@ -24,17 +24,21 @@
|
||||
<meta id="vscode-remote-nls-configuration" data-settings="{{NLS_CONFIGURATION}}" />
|
||||
|
||||
<!-- Workbench Icon/Manifest/CSS -->
|
||||
<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="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"
|
||||
/>
|
||||
<!-- PROD_ONLY
|
||||
<link data-name="vs/workbench/workbench.web.api" rel="stylesheet" href="{{CS_STATIC_BASE}}/lib/vscode/out/vs/workbench/workbench.web.api.css">
|
||||
<link data-name="vs/workbench/workbench.web.api" rel="stylesheet" href="{{BASE}}/static/{{COMMIT}}/lib/vscode/out/vs/workbench/workbench.web.api.css">
|
||||
END_PROD_ONLY -->
|
||||
<link rel="apple-touch-icon" href="{{CS_STATIC_BASE}}/src/browser/media/pwa-icon-384.png" />
|
||||
<link rel="apple-touch-icon" href="{{BASE}}/static/{{COMMIT}}/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="{{CS_STATIC_BASE}}/lib/vscode/node_modules/semver-umd/lib/semver-umd.js">
|
||||
<link rel="prefetch" href="{{BASE}}/static/{{COMMIT}}/lib/vscode/node_modules/semver-umd/lib/semver-umd.js">
|
||||
END_PROD_ONLY -->
|
||||
|
||||
<meta id="coder-options" data-settings="{{OPTIONS}}" />
|
||||
@@ -44,6 +48,10 @@
|
||||
|
||||
<!-- 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"))
|
||||
@@ -56,7 +64,7 @@
|
||||
}
|
||||
// FIXME: Only works if path separators are /.
|
||||
const path = nlsConfig._resolvedLanguagePackCoreLocation + "/" + bundle.replace(/\//g, "!") + ".nls.json"
|
||||
fetch(`{{BASE}}/resource/?path=${encodeURIComponent(path)}`)
|
||||
fetch(`${url.href}/resource/?path=${encodeURIComponent(path)}`)
|
||||
.then((response) => response.json())
|
||||
.then((json) => {
|
||||
bundles[bundle] = json
|
||||
@@ -69,26 +77,26 @@
|
||||
/* Probably fine. */
|
||||
}
|
||||
self.require = {
|
||||
baseUrl: "{{CS_STATIC_BASE}}/lib/vscode/out",
|
||||
baseUrl: `${staticBase}/out`,
|
||||
paths: {
|
||||
"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`,
|
||||
"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`,
|
||||
},
|
||||
"vs/nls": nlsConfig,
|
||||
}
|
||||
</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>
|
||||
<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>
|
||||
<!-- PROD_ONLY
|
||||
<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>
|
||||
<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>
|
||||
END_PROD_ONLY -->
|
||||
<script>
|
||||
require(["vs/code/browser/workbench/workbench"], function () {})
|
||||
|
||||
@@ -7,7 +7,7 @@ import "./pages/global.css"
|
||||
import "./pages/login.css"
|
||||
|
||||
if ("serviceWorker" in navigator) {
|
||||
const path = normalize(`${options.csStaticBase}/dist/serviceWorker.js`)
|
||||
const path = normalize(`${options.base}/static/${options.commit}/dist/serviceWorker.js`)
|
||||
navigator.serviceWorker
|
||||
.register(path, {
|
||||
scope: options.base || "/",
|
||||
|
||||
@@ -1,21 +1,19 @@
|
||||
import { Callback } from "./types"
|
||||
|
||||
export interface Disposable {
|
||||
dispose(): void
|
||||
}
|
||||
|
||||
export interface Event<T> {
|
||||
(listener: Callback<T>): Disposable
|
||||
(listener: (value: T) => void): Disposable
|
||||
}
|
||||
|
||||
/**
|
||||
* Emitter typecasts for a single event type.
|
||||
*/
|
||||
export class Emitter<T> {
|
||||
private listeners: Array<Callback<T>> = []
|
||||
private listeners: Array<(value: T) => void> = []
|
||||
|
||||
public get event(): Event<T> {
|
||||
return (cb: Callback<T>): Disposable => {
|
||||
return (cb: (value: T) => void): Disposable => {
|
||||
this.listeners.push(cb)
|
||||
|
||||
return {
|
||||
|
||||
@@ -9,7 +9,7 @@ export enum HttpCode {
|
||||
}
|
||||
|
||||
export class HttpError extends Error {
|
||||
public constructor(message: string, public readonly code: number, public readonly details?: object) {
|
||||
constructor(message: string, public readonly code: number) {
|
||||
super(message)
|
||||
this.name = this.constructor.name
|
||||
}
|
||||
|
||||
@@ -1 +0,0 @@
|
||||
export type Callback<T, R = void> = (t: T) => R
|
||||
@@ -2,8 +2,9 @@ import { logger, field } from "@coder/logger"
|
||||
|
||||
export interface Options {
|
||||
base: string
|
||||
csStaticBase: string
|
||||
commit: string
|
||||
logLevel: number
|
||||
pid?: number
|
||||
}
|
||||
|
||||
/**
|
||||
@@ -15,11 +16,7 @@ export const split = (str: string, delimiter: string): [string, string] => {
|
||||
return index !== -1 ? [str.substring(0, index).trim(), str.substring(index + 1)] : [str, ""]
|
||||
}
|
||||
|
||||
/**
|
||||
* Appends an 's' to the provided string if count is greater than one;
|
||||
* otherwise the string is returned
|
||||
*/
|
||||
export const plural = (count: number, str: string): string => (count === 1 ? str : `${str}s`)
|
||||
export const plural = (count: number): string => (count === 1 ? "" : "s")
|
||||
|
||||
export const generateUuid = (length = 24): string => {
|
||||
const possible = "0123456789ABCDEFGHIJKLMNOPQRSTUVWXYZabcdefghijklmnopqrstuvwxyz"
|
||||
@@ -43,28 +40,21 @@ 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 {
|
||||
options = JSON.parse(document.getElementById("coder-options")!.getAttribute("data-settings")!)
|
||||
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)
|
||||
} catch (error) {
|
||||
options = {} as T
|
||||
}
|
||||
@@ -78,10 +68,15 @@ export const getOptions = <T extends Options>(): T => {
|
||||
}
|
||||
}
|
||||
|
||||
logger.level = options.logLevel
|
||||
|
||||
options.base = resolveBase(options.base)
|
||||
options.csStaticBase = resolveBase(options.csStaticBase)
|
||||
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.debug("got options", field("options", options))
|
||||
|
||||
|
||||
@@ -8,9 +8,10 @@ import { HttpProvider, HttpResponse, Route } from "../http"
|
||||
import { pathToFsPath } from "../util"
|
||||
|
||||
/**
|
||||
* 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.
|
||||
* 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.
|
||||
*/
|
||||
export class StaticHttpProvider extends HttpProvider {
|
||||
public async handleRequest(route: Route, request: http.IncomingMessage): Promise<HttpResponse> {
|
||||
@@ -21,7 +22,7 @@ export class StaticHttpProvider extends HttpProvider {
|
||||
return this.getTarredResource(request, pathToFsPath(route.query.tar))
|
||||
}
|
||||
|
||||
const response = await this.getReplacedResource(request, route)
|
||||
const response = await this.getReplacedResource(route)
|
||||
if (!this.isDev) {
|
||||
response.cache = true
|
||||
}
|
||||
@@ -31,25 +32,17 @@ export class StaticHttpProvider extends HttpProvider {
|
||||
/**
|
||||
* Return a resource with variables replaced where necessary.
|
||||
*/
|
||||
protected async getReplacedResource(request: http.IncomingMessage, route: Route): Promise<HttpResponse> {
|
||||
protected async getReplacedResource(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(resourcePath)
|
||||
const response = await this.getUtf8Resource(this.rootPath, ...split)
|
||||
return this.replaceTemplates(route, response)
|
||||
}
|
||||
}
|
||||
return this.getResource(resourcePath)
|
||||
return this.getResource(this.rootPath, ...split)
|
||||
}
|
||||
|
||||
/**
|
||||
|
||||
@@ -200,6 +200,8 @@ 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"],
|
||||
})
|
||||
}
|
||||
|
||||
@@ -9,10 +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 { loadPlugins } from "./plugin"
|
||||
import { generateCertificate, hash, humanPath, open } from "./util"
|
||||
import { generateCertificate, hash, open, humanPath } from "./util"
|
||||
import { ipcMain, wrap } from "./wrapper"
|
||||
import { plural } from "../common/util"
|
||||
|
||||
process.on("uncaughtException", (error) => {
|
||||
logger.error(`Uncaught exception: ${error.message}`)
|
||||
@@ -79,8 +77,6 @@ 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))
|
||||
@@ -114,7 +110,7 @@ const main = async (args: Args, cliArgs: Args, configArgs: Args): Promise<void>
|
||||
}
|
||||
|
||||
if (httpServer.proxyDomains.size > 0) {
|
||||
logger.info(` - ${plural(httpServer.proxyDomains.size, "Proxying the following domain")}:`)
|
||||
logger.info(` - Proxying the following domain${httpServer.proxyDomains.size === 1 ? "" : "s"}:`)
|
||||
httpServer.proxyDomains.forEach((domain) => logger.info(` - *.${domain}`))
|
||||
}
|
||||
|
||||
|
||||
@@ -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,23 +235,30 @@ 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,
|
||||
extraOptions?: Omit<T, "base" | "csStaticBase" | "logLevel">,
|
||||
options: T,
|
||||
): HttpStringFileResponse
|
||||
protected replaceTemplates(
|
||||
route: Route,
|
||||
response: HttpStringFileResponse,
|
||||
sessionIdOrOptions?: string | object,
|
||||
): HttpStringFileResponse {
|
||||
const base = this.base(route)
|
||||
const options: Options = {
|
||||
base,
|
||||
csStaticBase: base + "/static/" + this.options.commit + this.rootPath,
|
||||
logLevel: logger.level,
|
||||
...extraOptions,
|
||||
if (typeof sessionIdOrOptions === "undefined" || typeof sessionIdOrOptions === "string") {
|
||||
sessionIdOrOptions = {
|
||||
base: this.base(route),
|
||||
commit: this.options.commit,
|
||||
logLevel: logger.level,
|
||||
sessionID: sessionIdOrOptions,
|
||||
} as Options
|
||||
}
|
||||
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, options.base)
|
||||
.replace(/{{CS_STATIC_BASE}}/g, options.csStaticBase)
|
||||
.replace(/"{{OPTIONS}}"/, `'${JSON.stringify(options)}'`)
|
||||
.replace(/{{BASE}}/g, this.base(route))
|
||||
.replace(/"{{OPTIONS}}"/, `'${JSON.stringify(sessionIdOrOptions)}'`)
|
||||
return response
|
||||
}
|
||||
|
||||
@@ -474,7 +481,7 @@ export class HttpServer {
|
||||
this.proxyDomains = new Set((options.proxyDomains || []).map((d) => d.replace(/^\*\./, "")))
|
||||
this.heart = new Heart(path.join(paths.data, "heartbeat"), async () => {
|
||||
const connections = await this.getConnections()
|
||||
logger.trace(plural(connections, `${connections} active connection`))
|
||||
logger.trace(`${connections} active connection${plural(connections)}`)
|
||||
return connections !== 0
|
||||
})
|
||||
this.protocol = this.options.cert ? "https" : "http"
|
||||
@@ -657,7 +664,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), field("error", error))
|
||||
logger.debug("Request error", field("url", request.url), field("code", code))
|
||||
if (code >= HttpCode.ServerError) {
|
||||
logger.error(error.stack)
|
||||
}
|
||||
|
||||
@@ -1,60 +0,0 @@
|
||||
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)
|
||||
}
|
||||
}
|
||||
@@ -32,7 +32,7 @@ export class IpcMain {
|
||||
public readonly onMessage = this._onMessage.event
|
||||
private readonly _onDispose = new Emitter<NodeJS.Signals | undefined>()
|
||||
public readonly onDispose = this._onDispose.event
|
||||
public readonly processExit: (code?: number) => never
|
||||
public readonly exit: (code?: number) => never
|
||||
|
||||
public constructor(public readonly parentPid?: number) {
|
||||
process.on("SIGINT", () => this._onDispose.emit("SIGINT"))
|
||||
@@ -40,7 +40,7 @@ export class IpcMain {
|
||||
process.on("exit", () => this._onDispose.emit(undefined))
|
||||
|
||||
// Ensure we control when the process exits.
|
||||
this.processExit = process.exit
|
||||
this.exit = process.exit
|
||||
process.exit = function (code?: number) {
|
||||
logger.warn(`process.exit() was prevented: ${code || "unknown code"}.`)
|
||||
} as (code?: number) => never
|
||||
@@ -71,14 +71,6 @@ export class IpcMain {
|
||||
}
|
||||
}
|
||||
|
||||
public exit(error?: number | ProcessError): never {
|
||||
if (error && typeof error !== "number") {
|
||||
this.processExit(typeof error.code === "number" ? error.code : 1)
|
||||
} else {
|
||||
this.processExit(error)
|
||||
}
|
||||
}
|
||||
|
||||
public handshake(child?: cp.ChildProcess): Promise<void> {
|
||||
return new Promise((resolve, reject) => {
|
||||
const target = child || process
|
||||
@@ -169,37 +161,28 @@ export class WrapperProcess {
|
||||
}
|
||||
})
|
||||
|
||||
ipcMain().onMessage((message) => {
|
||||
ipcMain().onMessage(async (message) => {
|
||||
switch (message.type) {
|
||||
case "relaunch":
|
||||
logger.info(`Relaunching: ${this.currentVersion} -> ${message.version}`)
|
||||
this.currentVersion = message.version
|
||||
this.relaunch()
|
||||
this.started = undefined
|
||||
if (this.process) {
|
||||
this.process.removeAllListeners()
|
||||
this.process.kill()
|
||||
}
|
||||
try {
|
||||
await this.start()
|
||||
} catch (error) {
|
||||
logger.error(error.message)
|
||||
ipcMain().exit(typeof error.code === "number" ? error.code : 1)
|
||||
}
|
||||
break
|
||||
default:
|
||||
logger.error(`Unrecognized message ${message}`)
|
||||
break
|
||||
}
|
||||
})
|
||||
|
||||
process.on("SIGUSR1", async () => {
|
||||
logger.info("Received SIGUSR1; hotswapping")
|
||||
this.relaunch()
|
||||
})
|
||||
}
|
||||
|
||||
private async relaunch(): Promise<void> {
|
||||
this.started = undefined
|
||||
if (this.process) {
|
||||
this.process.removeAllListeners()
|
||||
this.process.kill()
|
||||
}
|
||||
try {
|
||||
await this.start()
|
||||
} catch (error) {
|
||||
logger.error(error.message)
|
||||
ipcMain().exit(typeof error.code === "number" ? error.code : 1)
|
||||
}
|
||||
}
|
||||
|
||||
public start(): Promise<void> {
|
||||
@@ -261,13 +244,13 @@ export const wrap = (fn: () => Promise<void>): void => {
|
||||
.then(() => fn())
|
||||
.catch((error: ProcessError): void => {
|
||||
logger.error(error.message)
|
||||
ipcMain().exit(error)
|
||||
ipcMain().exit(typeof error.code === "number" ? error.code : 1)
|
||||
})
|
||||
} else {
|
||||
const wrapper = new WrapperProcess(require("../../package.json").version)
|
||||
wrapper.start().catch((error) => {
|
||||
logger.error(error.message)
|
||||
ipcMain().exit(error)
|
||||
ipcMain().exit(typeof error.code === "number" ? error.code : 1)
|
||||
})
|
||||
}
|
||||
}
|
||||
|
||||
Reference in New Issue
Block a user