Compare commits
24 Commits
| Author | SHA1 | Date | |
|---|---|---|---|
|
|
d8568ebaa9 | ||
|
|
f7790c9719 | ||
|
|
150d37868a | ||
|
|
8590f80c31 | ||
|
|
d6d24966be | ||
|
|
751a5ea3ad | ||
|
|
de568d446b | ||
|
|
7d02f34f71 | ||
|
|
2fad8a2a58 | ||
|
|
a0ff2014c3 | ||
|
|
8d03c22cb0 | ||
|
|
6e27869c09 | ||
|
|
934c8d4eb6 | ||
|
|
9b979ac869 | ||
|
|
3badf6bf7b | ||
|
|
10c2b956ac | ||
|
|
543d64268d | ||
|
|
fd36f8c168 | ||
|
|
c78d164948 | ||
|
|
4dd2c86cca | ||
|
|
42467b3e66 | ||
|
|
361e7103ea | ||
|
|
bac948ea6f | ||
|
|
1c8eede1aa |
@@ -23,6 +23,9 @@ rules:
|
|||||||
no-dupe-class-members: off
|
no-dupe-class-members: off
|
||||||
"@typescript-eslint/no-use-before-define": off
|
"@typescript-eslint/no-use-before-define": off
|
||||||
"@typescript-eslint/no-non-null-assertion": off
|
"@typescript-eslint/no-non-null-assertion": off
|
||||||
|
eqeqeq: error
|
||||||
|
import/order:
|
||||||
|
[error, { alphabetize: { order: "asc" }, groups: [["builtin", "external", "internal"], "parent", "sibling"] }]
|
||||||
|
|
||||||
settings:
|
settings:
|
||||||
# Does not work with CommonJS unfortunately.
|
# Does not work with CommonJS unfortunately.
|
||||||
|
|||||||
1
.gitignore
vendored
1
.gitignore
vendored
@@ -10,3 +10,4 @@ release-gcp/
|
|||||||
release-images/
|
release-images/
|
||||||
node_modules
|
node_modules
|
||||||
node-*
|
node-*
|
||||||
|
/plugins
|
||||||
|
|||||||
@@ -9,7 +9,8 @@ MINIFY=${MINIFY-true}
|
|||||||
main() {
|
main() {
|
||||||
cd "$(dirname "${0}")/../.."
|
cd "$(dirname "${0}")/../.."
|
||||||
|
|
||||||
tsc --outDir out --tsBuildInfoFile .cache/out.tsbuildinfo
|
tsc
|
||||||
|
|
||||||
# If out/node/entry.js does not already have the shebang,
|
# If out/node/entry.js does not already have the shebang,
|
||||||
# we make sure to add it and make it executable.
|
# we make sure to add it and make it executable.
|
||||||
if ! grep -q -m1 "^#!/usr/bin/env node" out/node/entry.js; then
|
if ! grep -q -m1 "^#!/usr/bin/env node" out/node/entry.js; then
|
||||||
@@ -18,11 +19,13 @@ main() {
|
|||||||
fi
|
fi
|
||||||
|
|
||||||
parcel build \
|
parcel build \
|
||||||
--public-url "/static/$(git rev-parse HEAD)/dist" \
|
--public-url "." \
|
||||||
--out-dir dist \
|
--out-dir dist \
|
||||||
$([[ $MINIFY ]] || echo --no-minify) \
|
$([[ $MINIFY ]] || echo --no-minify) \
|
||||||
src/browser/register.ts \
|
src/browser/register.ts \
|
||||||
src/browser/serviceWorker.ts
|
src/browser/serviceWorker.ts \
|
||||||
|
src/browser/pages/login.ts \
|
||||||
|
src/browser/pages/vscode.ts
|
||||||
}
|
}
|
||||||
|
|
||||||
main "$@"
|
main "$@"
|
||||||
|
|||||||
@@ -21,6 +21,12 @@ main() {
|
|||||||
rsync README.md "$RELEASE_PATH"
|
rsync README.md "$RELEASE_PATH"
|
||||||
rsync LICENSE.txt "$RELEASE_PATH"
|
rsync LICENSE.txt "$RELEASE_PATH"
|
||||||
rsync ./lib/vscode/ThirdPartyNotices.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() {
|
bundle_code_server() {
|
||||||
|
|||||||
@@ -722,10 +722,10 @@ index eab8591492..26668701f7 100644
|
|||||||
options.logService.error(`${logPrefix} socketFactory.connect() failed. Error:`);
|
options.logService.error(`${logPrefix} socketFactory.connect() failed. Error:`);
|
||||||
diff --git a/src/vs/server/browser/client.ts b/src/vs/server/browser/client.ts
|
diff --git a/src/vs/server/browser/client.ts b/src/vs/server/browser/client.ts
|
||||||
new file mode 100644
|
new file mode 100644
|
||||||
index 0000000000..8fb2a87303
|
index 0000000000..3c0703b717
|
||||||
--- /dev/null
|
--- /dev/null
|
||||||
+++ b/src/vs/server/browser/client.ts
|
+++ b/src/vs/server/browser/client.ts
|
||||||
@@ -0,0 +1,208 @@
|
@@ -0,0 +1,189 @@
|
||||||
+import { Emitter } from 'vs/base/common/event';
|
+import { Emitter } from 'vs/base/common/event';
|
||||||
+import { URI } from 'vs/base/common/uri';
|
+import { URI } from 'vs/base/common/uri';
|
||||||
+import { localize } from 'vs/nls';
|
+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 => {
|
+export const getOptions = <T extends Options>(): T => {
|
||||||
+ if (typeof document === "undefined") {
|
|
||||||
+ return {} as T;
|
|
||||||
+ }
|
|
||||||
+ const el = document.getElementById("coder-options");
|
|
||||||
+ try {
|
+ try {
|
||||||
+ if (!el) {
|
+ return JSON.parse(document.getElementById("coder-options")!.getAttribute("data-settings")!);
|
||||||
+ 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) {
|
+ } catch (error) {
|
||||||
+ console.warn(error);
|
|
||||||
+ return {} as T;
|
+ return {} as T;
|
||||||
+ }
|
+ }
|
||||||
+};
|
+};
|
||||||
@@ -1306,17 +1287,15 @@ 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..7e1cd270c8
|
||||||
--- /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,115 @@
|
||||||
+/**
|
+/**
|
||||||
+ * 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
|
|
||||||
+ commit: string
|
|
||||||
+ disableTelemetry: boolean
|
+ disableTelemetry: boolean
|
||||||
+}
|
+}
|
||||||
+
|
+
|
||||||
|
|||||||
@@ -37,6 +37,7 @@ class Watcher {
|
|||||||
|
|
||||||
const vscode = cp.spawn("yarn", ["watch"], { cwd: this.vscodeSourcePath })
|
const vscode = cp.spawn("yarn", ["watch"], { cwd: this.vscodeSourcePath })
|
||||||
const tsc = cp.spawn("tsc", ["--watch", "--pretty", "--preserveWatchOutput"], { cwd: this.rootPath })
|
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 bundler = this.createBundler()
|
||||||
|
|
||||||
const cleanup = (code?: number | null): void => {
|
const cleanup = (code?: number | null): void => {
|
||||||
@@ -48,6 +49,10 @@ class Watcher {
|
|||||||
tsc.removeAllListeners()
|
tsc.removeAllListeners()
|
||||||
tsc.kill()
|
tsc.kill()
|
||||||
|
|
||||||
|
Watcher.log("killing plugin")
|
||||||
|
plugin.removeAllListeners()
|
||||||
|
plugin.kill()
|
||||||
|
|
||||||
if (server) {
|
if (server) {
|
||||||
Watcher.log("killing server")
|
Watcher.log("killing server")
|
||||||
server.removeAllListeners()
|
server.removeAllListeners()
|
||||||
@@ -69,6 +74,10 @@ class Watcher {
|
|||||||
Watcher.log("tsc terminated unexpectedly")
|
Watcher.log("tsc terminated unexpectedly")
|
||||||
cleanup(code)
|
cleanup(code)
|
||||||
})
|
})
|
||||||
|
plugin.on("exit", (code) => {
|
||||||
|
Watcher.log("plugin terminated unexpectedly")
|
||||||
|
cleanup(code)
|
||||||
|
})
|
||||||
const bundle = bundler.bundle().catch(() => {
|
const bundle = bundler.bundle().catch(() => {
|
||||||
Watcher.log("parcel watcher terminated unexpectedly")
|
Watcher.log("parcel watcher terminated unexpectedly")
|
||||||
cleanup(1)
|
cleanup(1)
|
||||||
@@ -82,6 +91,7 @@ class Watcher {
|
|||||||
|
|
||||||
vscode.stderr.on("data", (d) => process.stderr.write(d))
|
vscode.stderr.on("data", (d) => process.stderr.write(d))
|
||||||
tsc.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
|
// From https://github.com/chalk/ansi-regex
|
||||||
const pattern = [
|
const pattern = [
|
||||||
@@ -140,17 +150,32 @@ class Watcher {
|
|||||||
bundle.then(restartServer)
|
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 {
|
private createBundler(out = "dist"): Bundler {
|
||||||
return new Bundler(
|
return new Bundler(
|
||||||
[path.join(this.rootPath, "src/browser/register.ts"), path.join(this.rootPath, "src/browser/serviceWorker.ts")],
|
[
|
||||||
|
path.join(this.rootPath, "src/browser/register.ts"),
|
||||||
|
path.join(this.rootPath, "src/browser/serviceWorker.ts"),
|
||||||
|
path.join(this.rootPath, "src/browser/pages/login.ts"),
|
||||||
|
path.join(this.rootPath, "src/browser/pages/vscode.ts"),
|
||||||
|
],
|
||||||
{
|
{
|
||||||
outDir: path.join(this.rootPath, out),
|
outDir: path.join(this.rootPath, out),
|
||||||
cacheDir: path.join(this.rootPath, ".cache"),
|
cacheDir: path.join(this.rootPath, ".cache"),
|
||||||
minify: !!process.env.MINIFY,
|
minify: !!process.env.MINIFY,
|
||||||
logLevel: 1,
|
logLevel: 1,
|
||||||
publicUrl: "/static/development/dist",
|
publicUrl: ".",
|
||||||
},
|
},
|
||||||
)
|
)
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -19,7 +19,7 @@ Please refer to [VS Code's prerequisites](https://github.com/Microsoft/vscode/wi
|
|||||||
Differences:
|
Differences:
|
||||||
|
|
||||||
- We require a minimum of node v12 but later versions should work.
|
- We require a minimum of node v12 but later versions should work.
|
||||||
- We use [fnpm](https://github.com/goreleaser/nfpm) to build `.deb` and `.rpm` packages.
|
- We use [nfpm](https://github.com/goreleaser/nfpm) to build `.deb` and `.rpm` packages.
|
||||||
- We use [jq](https://stedolan.github.io/jq/) to build code-server releases.
|
- We use [jq](https://stedolan.github.io/jq/) to build code-server releases.
|
||||||
- The [CI container](../ci/images/debian8/Dockerfile) is a useful reference for all our dependencies.
|
- The [CI container](../ci/images/debian8/Dockerfile) is a useful reference for all our dependencies.
|
||||||
|
|
||||||
|
|||||||
15
doc/FAQ.md
15
doc/FAQ.md
@@ -86,9 +86,20 @@ If you have your own marketplace that implements the VS Code Extension Gallery A
|
|||||||
point code-server to it by setting `$SERVICE_URL` and `$ITEM_URL`. These correspond directly
|
point code-server to it by setting `$SERVICE_URL` and `$ITEM_URL`. These correspond directly
|
||||||
to `serviceUrl` and `itemUrl` in VS Code's `product.json`.
|
to `serviceUrl` and `itemUrl` in VS Code's `product.json`.
|
||||||
|
|
||||||
|
e.g. to use [open-vsx.org](https://open-vsx.org):
|
||||||
|
|
||||||
|
```bash
|
||||||
|
export SERVICE_URL=https://open-vsx.org/vscode/gallery
|
||||||
|
export ITEM_URL=https://open-vsx.org/vscode/item
|
||||||
|
```
|
||||||
|
|
||||||
While you can technically use Microsoft's marketplace with these, please do not do so as it
|
While you can technically use Microsoft's marketplace with these, please do not do so as it
|
||||||
is against their terms of use. See [above](#differences-compared-to-vs-code). These variables
|
is against their terms of use. See [above](#differences-compared-to-vs-code) and this
|
||||||
are most valuable to our enterprise customers for whom we have a self hosted marketplace product.
|
discussion regarding the use of the Microsoft URLs in forks:
|
||||||
|
|
||||||
|
https://github.com/microsoft/vscode/issues/31168#issue-244533026
|
||||||
|
|
||||||
|
These variables are most valuable to our enterprise customers for whom we have a self hosted marketplace product.
|
||||||
|
|
||||||
## Where are extensions stored?
|
## Where are extensions stored?
|
||||||
|
|
||||||
|
|||||||
@@ -7,32 +7,32 @@
|
|||||||
"description": "Run editors on a remote server.",
|
"description": "Run editors on a remote server.",
|
||||||
"icons": [
|
"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",
|
"type": "image/png",
|
||||||
"sizes": "96x96"
|
"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",
|
"type": "image/png",
|
||||||
"sizes": "128x128"
|
"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",
|
"type": "image/png",
|
||||||
"sizes": "192x192"
|
"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",
|
"type": "image/png",
|
||||||
"sizes": "256x256"
|
"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",
|
"type": "image/png",
|
||||||
"sizes": "384x384"
|
"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",
|
"type": "image/png",
|
||||||
"sizes": "512x512"
|
"sizes": "512x512"
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -11,14 +11,10 @@
|
|||||||
content="style-src 'self'; manifest-src 'self'; img-src 'self' data:; font-src 'self' data:;"
|
content="style-src 'self'; manifest-src 'self'; img-src 'self' data:; font-src 'self' data:;"
|
||||||
/>
|
/>
|
||||||
<title>{{ERROR_TITLE}} - code-server</title>
|
<title>{{ERROR_TITLE}} - code-server</title>
|
||||||
<link rel="icon" href="{{BASE}}/static/{{COMMIT}}/src/browser/media/favicon.ico" type="image/x-icon" />
|
<link rel="icon" href="{{CS_STATIC_BASE}}/src/browser/media/favicon.ico" type="image/x-icon" />
|
||||||
<link
|
<link rel="manifest" href="{{CS_STATIC_BASE}}/src/browser/media/manifest.json" crossorigin="use-credentials" />
|
||||||
rel="manifest"
|
<link rel="apple-touch-icon" href="{{CS_STATIC_BASE}}/src/browser/media/pwa-icon-384.png" />
|
||||||
href="{{BASE}}/static/{{COMMIT}}/src/browser/media/manifest.json"
|
<link href="{{CS_STATIC_BASE}}/dist/register.css" rel="stylesheet" />
|
||||||
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}}" />
|
<meta id="coder-options" data-settings="{{OPTIONS}}" />
|
||||||
</head>
|
</head>
|
||||||
<body>
|
<body>
|
||||||
@@ -33,6 +29,6 @@
|
|||||||
</div>
|
</div>
|
||||||
</div>
|
</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>
|
</body>
|
||||||
</html>
|
</html>
|
||||||
|
|||||||
@@ -11,14 +11,10 @@
|
|||||||
content="style-src 'self'; script-src 'self' 'unsafe-inline'; manifest-src 'self'; img-src 'self' data:; font-src 'self' data:;"
|
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>
|
<title>code-server login</title>
|
||||||
<link rel="icon" href="{{BASE}}/static/{{COMMIT}}/src/browser/media/favicon.ico" type="image/x-icon" />
|
<link rel="icon" href="{{CS_STATIC_BASE}}/src/browser/media/favicon.ico" type="image/x-icon" />
|
||||||
<link
|
<link rel="manifest" href="{{CS_STATIC_BASE}}/src/browser/media/manifest.json" crossorigin="use-credentials" />
|
||||||
rel="manifest"
|
<link rel="apple-touch-icon" href="{{CS_STATIC_BASE}}/src/browser/media/pwa-icon-384.png" />
|
||||||
href="{{BASE}}/static/{{COMMIT}}/src/browser/media/manifest.json"
|
<link href="{{CS_STATIC_BASE}}/dist/register.css" rel="stylesheet" />
|
||||||
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}}" />
|
<meta id="coder-options" data-settings="{{OPTIONS}}" />
|
||||||
</head>
|
</head>
|
||||||
<body>
|
<body>
|
||||||
@@ -50,11 +46,6 @@
|
|||||||
</div>
|
</div>
|
||||||
</div>
|
</div>
|
||||||
</body>
|
</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>
|
<script data-cfasync="false" src="{{CS_STATIC_BASE}}/dist/login.js"></script>
|
||||||
const parts = window.location.pathname.replace(/^\//g, "").split("/")
|
|
||||||
parts[parts.length - 1] = "{{BASE}}"
|
|
||||||
const url = new URL(window.location.origin + "/" + parts.join("/"))
|
|
||||||
document.getElementById("base").value = url.pathname
|
|
||||||
</script>
|
|
||||||
</html>
|
</html>
|
||||||
|
|||||||
7
src/browser/pages/login.ts
Normal file
7
src/browser/pages/login.ts
Normal file
@@ -0,0 +1,7 @@
|
|||||||
|
import { getOptions } from "../../common/util"
|
||||||
|
|
||||||
|
const options = getOptions()
|
||||||
|
const el = document.getElementById("base") as HTMLInputElement
|
||||||
|
if (el) {
|
||||||
|
el.value = options.base
|
||||||
|
}
|
||||||
@@ -24,21 +24,17 @@
|
|||||||
<meta id="vscode-remote-nls-configuration" data-settings="{{NLS_CONFIGURATION}}" />
|
<meta id="vscode-remote-nls-configuration" data-settings="{{NLS_CONFIGURATION}}" />
|
||||||
|
|
||||||
<!-- Workbench Icon/Manifest/CSS -->
|
<!-- Workbench Icon/Manifest/CSS -->
|
||||||
<link rel="icon" href="{{BASE}}/static/{{COMMIT}}/src/browser/media/favicon.ico" type="image/x-icon" />
|
<link rel="icon" href="{{CS_STATIC_BASE}}/src/browser/media/favicon.ico" type="image/x-icon" />
|
||||||
<link
|
<link rel="manifest" href="{{CS_STATIC_BASE}}/src/browser/media/manifest.json" crossorigin="use-credentials" />
|
||||||
rel="manifest"
|
|
||||||
href="{{BASE}}/static/{{COMMIT}}/src/browser/media/manifest.json"
|
|
||||||
crossorigin="use-credentials"
|
|
||||||
/>
|
|
||||||
<!-- PROD_ONLY
|
<!-- 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 -->
|
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" />
|
<meta name="apple-mobile-web-app-capable" content="yes" />
|
||||||
|
|
||||||
<!-- Prefetch to avoid waterfall -->
|
<!-- Prefetch to avoid waterfall -->
|
||||||
<!-- PROD_ONLY
|
<!-- 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 -->
|
END_PROD_ONLY -->
|
||||||
|
|
||||||
<meta id="coder-options" data-settings="{{OPTIONS}}" />
|
<meta id="coder-options" data-settings="{{OPTIONS}}" />
|
||||||
@@ -47,65 +43,14 @@
|
|||||||
<body aria-label=""></body>
|
<body aria-label=""></body>
|
||||||
|
|
||||||
<!-- Startup (do not modify order of script tags!) -->
|
<!-- Startup (do not modify order of script tags!) -->
|
||||||
<script>
|
<script data-cfasync="false" src="{{CS_STATIC_BASE}}/dist/pages/vscode.js"></script>
|
||||||
const parts = window.location.pathname.replace(/^\//g, "").split("/")
|
<script data-cfasync="false" src="{{CS_STATIC_BASE}}/dist/register.js"></script>
|
||||||
parts[parts.length - 1] = "{{BASE}}"
|
<script data-cfasync="false" src="{{CS_STATIC_BASE}}/lib/vscode/out/vs/loader.js"></script>
|
||||||
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"))
|
|
||||||
if (nlsConfig._resolvedLanguagePackCoreLocation) {
|
|
||||||
const bundles = Object.create(null)
|
|
||||||
nlsConfig.loadBundle = (bundle, language, cb) => {
|
|
||||||
let result = bundles[bundle]
|
|
||||||
if (result) {
|
|
||||||
return cb(undefined, result)
|
|
||||||
}
|
|
||||||
// FIXME: Only works if path separators are /.
|
|
||||||
const path = nlsConfig._resolvedLanguagePackCoreLocation + "/" + bundle.replace(/\//g, "!") + ".nls.json"
|
|
||||||
fetch(`${url.href}/resource/?path=${encodeURIComponent(path)}`)
|
|
||||||
.then((response) => response.json())
|
|
||||||
.then((json) => {
|
|
||||||
bundles[bundle] = json
|
|
||||||
cb(undefined, json)
|
|
||||||
})
|
|
||||||
.catch(cb)
|
|
||||||
}
|
|
||||||
}
|
|
||||||
} catch (error) {
|
|
||||||
/* Probably fine. */
|
|
||||||
}
|
|
||||||
self.require = {
|
|
||||||
baseUrl: `${staticBase}/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`,
|
|
||||||
},
|
|
||||||
"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>
|
|
||||||
<!-- PROD_ONLY
|
<!-- 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="{{CS_STATIC_BASE}}/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.js"></script>
|
||||||
END_PROD_ONLY -->
|
END_PROD_ONLY -->
|
||||||
<script>
|
<script>
|
||||||
require(["vs/code/browser/workbench/workbench"], function () {})
|
require(["vs/code/browser/workbench/workbench"], function () {})
|
||||||
</script>
|
</script>
|
||||||
<script>
|
|
||||||
try {
|
|
||||||
document.body.style.background = JSON.parse(localStorage.getItem("colorThemeData")).colorMap["editor.background"]
|
|
||||||
} catch (error) {
|
|
||||||
// Oh well.
|
|
||||||
}
|
|
||||||
</script>
|
|
||||||
</html>
|
</html>
|
||||||
|
|||||||
53
src/browser/pages/vscode.ts
Normal file
53
src/browser/pages/vscode.ts
Normal file
@@ -0,0 +1,53 @@
|
|||||||
|
import { getOptions } from "../../common/util"
|
||||||
|
|
||||||
|
const options = getOptions()
|
||||||
|
|
||||||
|
// TODO: Add proper types.
|
||||||
|
/* eslint-disable @typescript-eslint/no-explicit-any */
|
||||||
|
|
||||||
|
let nlsConfig: any
|
||||||
|
try {
|
||||||
|
nlsConfig = JSON.parse(document.getElementById("vscode-remote-nls-configuration")!.getAttribute("data-settings")!)
|
||||||
|
if (nlsConfig._resolvedLanguagePackCoreLocation) {
|
||||||
|
const bundles = Object.create(null)
|
||||||
|
nlsConfig.loadBundle = (bundle: any, _language: any, cb: any): void => {
|
||||||
|
const result = bundles[bundle]
|
||||||
|
if (result) {
|
||||||
|
return cb(undefined, result)
|
||||||
|
}
|
||||||
|
// FIXME: Only works if path separators are /.
|
||||||
|
const path = nlsConfig._resolvedLanguagePackCoreLocation + "/" + bundle.replace(/\//g, "!") + ".nls.json"
|
||||||
|
fetch(`{{BASE}}/resource/?path=${encodeURIComponent(path)}`)
|
||||||
|
.then((response) => response.json())
|
||||||
|
.then((json) => {
|
||||||
|
bundles[bundle] = json
|
||||||
|
cb(undefined, json)
|
||||||
|
})
|
||||||
|
.catch(cb)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
} catch (error) {
|
||||||
|
/* Probably fine. */
|
||||||
|
}
|
||||||
|
|
||||||
|
;(self.require as any) = {
|
||||||
|
baseUrl: `${options.csStaticBase}/lib/vscode/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`,
|
||||||
|
},
|
||||||
|
"vs/nls": nlsConfig,
|
||||||
|
}
|
||||||
|
|
||||||
|
try {
|
||||||
|
document.body.style.background = JSON.parse(localStorage.getItem("colorThemeData")!).colorMap["editor.background"]
|
||||||
|
} catch (error) {
|
||||||
|
// Oh well.
|
||||||
|
}
|
||||||
@@ -7,7 +7,7 @@ import "./pages/global.css"
|
|||||||
import "./pages/login.css"
|
import "./pages/login.css"
|
||||||
|
|
||||||
if ("serviceWorker" in navigator) {
|
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
|
navigator.serviceWorker
|
||||||
.register(path, {
|
.register(path, {
|
||||||
scope: options.base || "/",
|
scope: options.base || "/",
|
||||||
|
|||||||
@@ -1,19 +1,21 @@
|
|||||||
|
import { Callback } from "./types"
|
||||||
|
|
||||||
export interface Disposable {
|
export interface Disposable {
|
||||||
dispose(): void
|
dispose(): void
|
||||||
}
|
}
|
||||||
|
|
||||||
export interface Event<T> {
|
export interface Event<T> {
|
||||||
(listener: (value: T) => void): Disposable
|
(listener: Callback<T>): Disposable
|
||||||
}
|
}
|
||||||
|
|
||||||
/**
|
/**
|
||||||
* Emitter typecasts for a single event type.
|
* Emitter typecasts for a single event type.
|
||||||
*/
|
*/
|
||||||
export class Emitter<T> {
|
export class Emitter<T> {
|
||||||
private listeners: Array<(value: T) => void> = []
|
private listeners: Array<Callback<T>> = []
|
||||||
|
|
||||||
public get event(): Event<T> {
|
public get event(): Event<T> {
|
||||||
return (cb: (value: T) => void): Disposable => {
|
return (cb: Callback<T>): Disposable => {
|
||||||
this.listeners.push(cb)
|
this.listeners.push(cb)
|
||||||
|
|
||||||
return {
|
return {
|
||||||
|
|||||||
1
src/common/types.ts
Normal file
1
src/common/types.ts
Normal file
@@ -0,0 +1 @@
|
|||||||
|
export type Callback<T, R = void> = (t: T) => R
|
||||||
@@ -2,9 +2,8 @@ import { logger, field } from "@coder/logger"
|
|||||||
|
|
||||||
export interface Options {
|
export interface Options {
|
||||||
base: string
|
base: string
|
||||||
commit: string
|
csStaticBase: string
|
||||||
logLevel: number
|
logLevel: number
|
||||||
pid?: number
|
|
||||||
}
|
}
|
||||||
|
|
||||||
/**
|
/**
|
||||||
@@ -16,7 +15,11 @@ export const split = (str: string, delimiter: string): [string, string] => {
|
|||||||
return index !== -1 ? [str.substring(0, index).trim(), str.substring(index + 1)] : [str, ""]
|
return index !== -1 ? [str.substring(0, index).trim(), str.substring(index + 1)] : [str, ""]
|
||||||
}
|
}
|
||||||
|
|
||||||
export const plural = (count: number): string => (count === 1 ? "" : "s")
|
/**
|
||||||
|
* 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 generateUuid = (length = 24): string => {
|
export const generateUuid = (length = 24): string => {
|
||||||
const possible = "0123456789ABCDEFGHIJKLMNOPQRSTUVWXYZabcdefghijklmnopqrstuvwxyz"
|
const possible = "0123456789ABCDEFGHIJKLMNOPQRSTUVWXYZabcdefghijklmnopqrstuvwxyz"
|
||||||
@@ -40,21 +43,28 @@ export const trimSlashes = (url: string): string => {
|
|||||||
return url.replace(/^\/+|\/+$/g, "")
|
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.
|
* Get options embedded in the HTML or query params.
|
||||||
*/
|
*/
|
||||||
export const getOptions = <T extends Options>(): T => {
|
export const getOptions = <T extends Options>(): T => {
|
||||||
let options: T
|
let options: T
|
||||||
try {
|
try {
|
||||||
const el = document.getElementById("coder-options")
|
options = 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")
|
|
||||||
}
|
|
||||||
options = JSON.parse(value)
|
|
||||||
} catch (error) {
|
} catch (error) {
|
||||||
options = {} as T
|
options = {} as T
|
||||||
}
|
}
|
||||||
@@ -68,15 +78,10 @@ export const getOptions = <T extends Options>(): T => {
|
|||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
if (typeof options.logLevel !== "undefined") {
|
|
||||||
logger.level = options.logLevel
|
logger.level = options.logLevel
|
||||||
}
|
|
||||||
if (options.base) {
|
options.base = resolveBase(options.base)
|
||||||
const parts = location.pathname.replace(/^\//g, "").split("/")
|
options.csStaticBase = resolveBase(options.csStaticBase)
|
||||||
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))
|
logger.debug("got options", field("options", options))
|
||||||
|
|
||||||
|
|||||||
@@ -8,10 +8,9 @@ import { HttpProvider, HttpResponse, Route } from "../http"
|
|||||||
import { pathToFsPath } from "../util"
|
import { pathToFsPath } from "../util"
|
||||||
|
|
||||||
/**
|
/**
|
||||||
* Static file HTTP provider. Regular static requests (the path is the request
|
* Static file HTTP provider. Static requests do not require authentication if
|
||||||
* itself) do not require authentication and they only allow access to resources
|
* the resource is in the application's directory except requests to serve a
|
||||||
* within the application. Requests for tars (the path is in a query parameter)
|
* directory as a tar which always requires authentication.
|
||||||
* do require permissions and can access any directory.
|
|
||||||
*/
|
*/
|
||||||
export class StaticHttpProvider extends HttpProvider {
|
export class StaticHttpProvider extends HttpProvider {
|
||||||
public async handleRequest(route: Route, request: http.IncomingMessage): Promise<HttpResponse> {
|
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))
|
return this.getTarredResource(request, pathToFsPath(route.query.tar))
|
||||||
}
|
}
|
||||||
|
|
||||||
const response = await this.getReplacedResource(route)
|
const response = await this.getReplacedResource(request, route)
|
||||||
if (!this.isDev) {
|
if (!this.isDev) {
|
||||||
response.cache = true
|
response.cache = true
|
||||||
}
|
}
|
||||||
@@ -32,17 +31,25 @@ export class StaticHttpProvider extends HttpProvider {
|
|||||||
/**
|
/**
|
||||||
* Return a resource with variables replaced where necessary.
|
* 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).
|
// The first part is always the commit (for caching purposes).
|
||||||
const split = route.requestPath.split("/").slice(1)
|
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]) {
|
switch (split[split.length - 1]) {
|
||||||
case "manifest.json": {
|
case "manifest.json": {
|
||||||
const response = await this.getUtf8Resource(this.rootPath, ...split)
|
const response = await this.getUtf8Resource(resourcePath)
|
||||||
return this.replaceTemplates(route, response)
|
return this.replaceTemplates(route, response)
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
return this.getResource(this.rootPath, ...split)
|
return this.getResource(resourcePath)
|
||||||
}
|
}
|
||||||
|
|
||||||
/**
|
/**
|
||||||
|
|||||||
@@ -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"],
|
||||||
})
|
})
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -172,7 +172,7 @@ export const parse = (
|
|||||||
const arg = argv[i]
|
const arg = argv[i]
|
||||||
|
|
||||||
// -- signals the end of option parsing.
|
// -- signals the end of option parsing.
|
||||||
if (!ended && arg == "--") {
|
if (!ended && arg === "--") {
|
||||||
ended = true
|
ended = true
|
||||||
continue
|
continue
|
||||||
}
|
}
|
||||||
@@ -220,7 +220,7 @@ export const parse = (
|
|||||||
throw error(`--${key} requires a value`)
|
throw error(`--${key} requires a value`)
|
||||||
}
|
}
|
||||||
|
|
||||||
if (option.type == OptionalString && value == "false") {
|
if (option.type === OptionalString && value === "false") {
|
||||||
continue
|
continue
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|||||||
@@ -9,8 +9,10 @@ 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"
|
||||||
|
import { plural } from "../common/util"
|
||||||
|
|
||||||
process.on("uncaughtException", (error) => {
|
process.on("uncaughtException", (error) => {
|
||||||
logger.error(`Uncaught exception: ${error.message}`)
|
logger.error(`Uncaught exception: ${error.message}`)
|
||||||
@@ -77,6 +79,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))
|
||||||
@@ -110,7 +114,7 @@ const main = async (args: Args, cliArgs: Args, configArgs: Args): Promise<void>
|
|||||||
}
|
}
|
||||||
|
|
||||||
if (httpServer.proxyDomains.size > 0) {
|
if (httpServer.proxyDomains.size > 0) {
|
||||||
logger.info(` - Proxying the following domain${httpServer.proxyDomains.size === 1 ? "" : "s"}:`)
|
logger.info(` - ${plural(httpServer.proxyDomains.size, "Proxying the following domain")}:`)
|
||||||
httpServer.proxyDomains.forEach((domain) => logger.info(` - *.${domain}`))
|
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
|
* Get the base relative to the provided route. For each slash we need to go
|
||||||
* up a directory. For example:
|
* up a directory. For example:
|
||||||
* / => ./
|
* / => .
|
||||||
* /foo => ./
|
* /foo => .
|
||||||
* /foo/ => ./../
|
* /foo/ => ./..
|
||||||
* /foo/bar => ./../
|
* /foo/bar => ./..
|
||||||
* /foo/bar/ => ./../../
|
* /foo/bar/ => ./../..
|
||||||
*/
|
*/
|
||||||
public base(route: Route): string {
|
public base(route: Route): string {
|
||||||
const depth = (route.originalPath.match(/\//g) || []).length
|
const depth = (route.originalPath.match(/\//g) || []).length
|
||||||
@@ -235,30 +235,23 @@ 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 base = this.base(route)
|
||||||
sessionIdOrOptions = {
|
const options: Options = {
|
||||||
base: this.base(route),
|
base,
|
||||||
commit: this.options.commit,
|
csStaticBase: base + "/static/" + this.options.commit + this.rootPath,
|
||||||
logLevel: logger.level,
|
logLevel: logger.level,
|
||||||
sessionID: sessionIdOrOptions,
|
...extraOptions,
|
||||||
} as Options
|
|
||||||
}
|
}
|
||||||
response.content = response.content
|
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(/{{TO}}/g, Array.isArray(route.query.to) ? route.query.to[0] : route.query.to || "/dashboard")
|
||||||
.replace(/{{BASE}}/g, this.base(route))
|
.replace(/{{BASE}}/g, options.base)
|
||||||
.replace(/"{{OPTIONS}}"/, `'${JSON.stringify(sessionIdOrOptions)}'`)
|
.replace(/{{CS_STATIC_BASE}}/g, options.csStaticBase)
|
||||||
|
.replace(/"{{OPTIONS}}"/, `'${JSON.stringify(options)}'`)
|
||||||
return response
|
return response
|
||||||
}
|
}
|
||||||
|
|
||||||
@@ -481,7 +474,7 @@ export class HttpServer {
|
|||||||
this.proxyDomains = new Set((options.proxyDomains || []).map((d) => d.replace(/^\*\./, "")))
|
this.proxyDomains = new Set((options.proxyDomains || []).map((d) => d.replace(/^\*\./, "")))
|
||||||
this.heart = new Heart(path.join(paths.data, "heartbeat"), async () => {
|
this.heart = new Heart(path.join(paths.data, "heartbeat"), async () => {
|
||||||
const connections = await this.getConnections()
|
const connections = await this.getConnections()
|
||||||
logger.trace(`${connections} active connection${plural(connections)}`)
|
logger.trace(plural(connections, `${connections} active connection`))
|
||||||
return connections !== 0
|
return connections !== 0
|
||||||
})
|
})
|
||||||
this.protocol = this.options.cert ? "https" : "http"
|
this.protocol = this.options.cert ? "https" : "http"
|
||||||
@@ -664,7 +657,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)
|
||||||
}
|
}
|
||||||
@@ -875,6 +868,7 @@ export class HttpServer {
|
|||||||
// isn't setting the host header to match the access domain.
|
// isn't setting the host header to match the access domain.
|
||||||
host === "localhost"
|
host === "localhost"
|
||||||
) {
|
) {
|
||||||
|
logger.debug("no valid cookie doman", field("host", host))
|
||||||
return undefined
|
return undefined
|
||||||
}
|
}
|
||||||
|
|
||||||
@@ -884,6 +878,7 @@ export class HttpServer {
|
|||||||
}
|
}
|
||||||
})
|
})
|
||||||
|
|
||||||
|
logger.debug("got cookie doman", field("host", host))
|
||||||
return host ? `Domain=${host}` : undefined
|
return host ? `Domain=${host}` : undefined
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|||||||
60
src/node/plugin.ts
Normal file
60
src/node/plugin.ts
Normal 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)
|
||||||
|
}
|
||||||
|
}
|
||||||
@@ -1,8 +1,8 @@
|
|||||||
|
import { logger } from "@coder/logger"
|
||||||
import * as fs from "fs-extra"
|
import * as fs from "fs-extra"
|
||||||
import * as path from "path"
|
import * as path from "path"
|
||||||
import { extend, paths } from "./util"
|
|
||||||
import { logger } from "@coder/logger"
|
|
||||||
import { Route } from "./http"
|
import { Route } from "./http"
|
||||||
|
import { paths } from "./util"
|
||||||
|
|
||||||
export type Settings = { [key: string]: Settings | string | boolean | number }
|
export type Settings = { [key: string]: Settings | string | boolean | number }
|
||||||
|
|
||||||
@@ -30,12 +30,12 @@ export class SettingsProvider<T> {
|
|||||||
|
|
||||||
/**
|
/**
|
||||||
* Write settings combined with current settings. On failure log a warning.
|
* Write settings combined with current settings. On failure log a warning.
|
||||||
* Settings can be shallow or deep merged.
|
* Settings will be merged shallowly.
|
||||||
*/
|
*/
|
||||||
public async write(settings: Partial<T>, shallow = true): Promise<void> {
|
public async write(settings: Partial<T>): Promise<void> {
|
||||||
try {
|
try {
|
||||||
const oldSettings = await this.read()
|
const oldSettings = await this.read()
|
||||||
const nextSettings = shallow ? Object.assign({}, oldSettings, settings) : extend(oldSettings, settings)
|
const nextSettings = { ...oldSettings, ...settings }
|
||||||
await fs.writeFile(this.settingsPath, JSON.stringify(nextSettings, null, 2))
|
await fs.writeFile(this.settingsPath, JSON.stringify(nextSettings, null, 2))
|
||||||
} catch (error) {
|
} catch (error) {
|
||||||
logger.warn(error.message)
|
logger.warn(error.message)
|
||||||
|
|||||||
@@ -1,10 +1,10 @@
|
|||||||
import * as cp from "child_process"
|
import * as cp from "child_process"
|
||||||
import * as crypto from "crypto"
|
import * as crypto from "crypto"
|
||||||
|
import envPaths from "env-paths"
|
||||||
import * as fs from "fs-extra"
|
import * as fs from "fs-extra"
|
||||||
import * as os from "os"
|
import * as os from "os"
|
||||||
import * as path from "path"
|
import * as path from "path"
|
||||||
import * as util from "util"
|
import * as util from "util"
|
||||||
import envPaths from "env-paths"
|
|
||||||
import xdgBasedir from "xdg-basedir"
|
import xdgBasedir from "xdg-basedir"
|
||||||
|
|
||||||
export const tmpdir = path.join(os.tmpdir(), "code-server")
|
export const tmpdir = path.join(os.tmpdir(), "code-server")
|
||||||
@@ -199,25 +199,6 @@ export const isObject = <T extends object>(obj: T): obj is T => {
|
|||||||
return !Array.isArray(obj) && typeof obj === "object" && obj !== null
|
return !Array.isArray(obj) && typeof obj === "object" && obj !== null
|
||||||
}
|
}
|
||||||
|
|
||||||
/**
|
|
||||||
* Extend a with b and return a new object. Properties with objects will be
|
|
||||||
* recursively merged while all other properties are just overwritten.
|
|
||||||
*/
|
|
||||||
export function extend<A, B>(a: A, b: B): A & B
|
|
||||||
// eslint-disable-next-line @typescript-eslint/no-explicit-any
|
|
||||||
export function extend(...args: any[]): any {
|
|
||||||
const c = {} as any // eslint-disable-line @typescript-eslint/no-explicit-any
|
|
||||||
for (const obj of args) {
|
|
||||||
if (!isObject(obj)) {
|
|
||||||
continue
|
|
||||||
}
|
|
||||||
for (const key in obj) {
|
|
||||||
c[key] = isObject(obj[key]) ? extend(c[key], obj[key]) : obj[key]
|
|
||||||
}
|
|
||||||
}
|
|
||||||
return c
|
|
||||||
}
|
|
||||||
|
|
||||||
/**
|
/**
|
||||||
* Taken from vs/base/common/charCode.ts. Copied for now instead of importing so
|
* Taken from vs/base/common/charCode.ts. Copied for now instead of importing so
|
||||||
* we don't have to set up a `vs` alias to be able to import with types (since
|
* we don't have to set up a `vs` alias to be able to import with types (since
|
||||||
|
|||||||
@@ -6,8 +6,8 @@ import * as net from "net"
|
|||||||
import * as path from "path"
|
import * as path from "path"
|
||||||
import * as tls from "tls"
|
import * as tls from "tls"
|
||||||
import { Emitter } from "../src/common/emitter"
|
import { Emitter } from "../src/common/emitter"
|
||||||
import { generateCertificate, tmpdir } from "../src/node/util"
|
|
||||||
import { SocketProxyProvider } from "../src/node/socket"
|
import { SocketProxyProvider } from "../src/node/socket"
|
||||||
|
import { generateCertificate, tmpdir } from "../src/node/util"
|
||||||
|
|
||||||
describe("SocketProxyProvider", () => {
|
describe("SocketProxyProvider", () => {
|
||||||
const provider = new SocketProxyProvider()
|
const provider = new SocketProxyProvider()
|
||||||
|
|||||||
@@ -1,43 +1,7 @@
|
|||||||
import * as assert from "assert"
|
import * as assert from "assert"
|
||||||
import { normalize } from "../src/common/util"
|
import { normalize } from "../src/common/util"
|
||||||
import { extend } from "../src/node/util"
|
|
||||||
|
|
||||||
describe("util", () => {
|
describe("util", () => {
|
||||||
describe("extend", () => {
|
|
||||||
it("should extend", () => {
|
|
||||||
const a = { foo: { bar: 0, baz: 2 }, garply: 4, waldo: 6 }
|
|
||||||
const b = { foo: { bar: 1, qux: 3 }, garply: "5", fred: 7 }
|
|
||||||
const extended = extend(a, b)
|
|
||||||
assert.deepEqual(extended, {
|
|
||||||
foo: { bar: 1, baz: 2, qux: 3 },
|
|
||||||
garply: "5",
|
|
||||||
waldo: 6,
|
|
||||||
fred: 7,
|
|
||||||
})
|
|
||||||
})
|
|
||||||
|
|
||||||
it("should make deep copies of the original objects", () => {
|
|
||||||
const a = { foo: 0, bar: { frobnozzle: 2 }, mumble: { qux: { thud: 4 } } }
|
|
||||||
const b = { foo: 1, bar: { chad: 3 } }
|
|
||||||
const extended = extend(a, b)
|
|
||||||
assert.notEqual(a.bar, extended.bar)
|
|
||||||
assert.notEqual(b.bar, extended.bar)
|
|
||||||
assert.notEqual(a.mumble, extended.mumble)
|
|
||||||
assert.notEqual(a.mumble.qux, extended.mumble.qux)
|
|
||||||
})
|
|
||||||
|
|
||||||
it("should handle mismatch in type", () => {
|
|
||||||
const a = { foo: { bar: 0, baz: 2, qux: { mumble: 11 } }, garply: 4, waldo: { thud: 10 } }
|
|
||||||
const b = { foo: { bar: [1], baz: { plugh: 8 }, qux: 12 }, garply: { nox: 9 }, waldo: 7 }
|
|
||||||
const extended = extend(a, b)
|
|
||||||
assert.deepEqual(extended, {
|
|
||||||
foo: { bar: [1], baz: { plugh: 8 }, qux: 12 },
|
|
||||||
garply: { nox: 9 },
|
|
||||||
waldo: 7,
|
|
||||||
})
|
|
||||||
})
|
|
||||||
})
|
|
||||||
|
|
||||||
describe("normalize", () => {
|
describe("normalize", () => {
|
||||||
it("should remove multiple slashes", () => {
|
it("should remove multiple slashes", () => {
|
||||||
assert.equal(normalize("//foo//bar//baz///mumble"), "/foo/bar/baz/mumble")
|
assert.equal(normalize("//foo//bar//baz///mumble"), "/foo/bar/baz/mumble")
|
||||||
|
|||||||
@@ -8,17 +8,15 @@
|
|||||||
"noUnusedLocals": true,
|
"noUnusedLocals": true,
|
||||||
"forceConsistentCasingInFileNames": true,
|
"forceConsistentCasingInFileNames": true,
|
||||||
"outDir": "./out",
|
"outDir": "./out",
|
||||||
"allowJs": false,
|
|
||||||
"jsx": "react",
|
|
||||||
"declaration": true,
|
"declaration": true,
|
||||||
"experimentalDecorators": true,
|
"experimentalDecorators": true,
|
||||||
"esModuleInterop": true,
|
"esModuleInterop": true,
|
||||||
"allowSyntheticDefaultImports": true,
|
"allowSyntheticDefaultImports": true,
|
||||||
"sourceMap": true,
|
"sourceMap": true,
|
||||||
"tsBuildInfoFile": "./.tsbuildinfo",
|
"tsBuildInfoFile": "./.cache/tsbuildinfo",
|
||||||
"incremental": true,
|
"incremental": true,
|
||||||
"rootDir": "./src",
|
"rootDir": "./src",
|
||||||
"typeRoots": ["./node_modules/@types", "./typings"]
|
"typeRoots": ["./node_modules/@types", "./typings"]
|
||||||
},
|
},
|
||||||
"include": ["./src/**/*.ts", "./src/**/*.tsx"]
|
"include": ["./src/**/*.ts"]
|
||||||
}
|
}
|
||||||
|
|||||||
Reference in New Issue
Block a user