Compare commits

..

1 Commits

Author SHA1 Message Date
Anmol Sethi
16cbbcb99f Bundle systemd system unit
systemd's user units are buggy on certain versions
and do not linger by default.

Closes #1771
Closes #1673
Closes #1882
Closes #1861
2020-08-04 07:17:59 -04:00
27 changed files with 183 additions and 258 deletions

1
.gitignore vendored
View File

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

View File

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

View File

@@ -21,12 +21,6 @@ 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() {

View File

@@ -0,0 +1,11 @@
[Unit]
Description=code-server
After=network.target
[Service]
Type=exec
ExecStart=/usr/bin/code-server
Restart=always
[Install]
WantedBy=default.target

View File

@@ -6,6 +6,7 @@ After=network.target
Type=exec Type=exec
ExecStart=/usr/bin/code-server ExecStart=/usr/bin/code-server
Restart=always Restart=always
User=%i
[Install] [Install]
WantedBy=default.target WantedBy=default.target

View File

@@ -12,5 +12,6 @@ homepage: "https://github.com/cdr/code-server"
license: "MIT" license: "MIT"
files: files:
./ci/build/code-server-nfpm.sh: /usr/bin/code-server ./ci/build/code-server-nfpm.sh: /usr/bin/code-server
./ci/build/code-server.service: /usr/lib/systemd/user/code-server.service ./ci/build/code-server.service: /usr/lib/systemd/system/code-server.service
./ci/build/code-server-user.service: /usr/lib/systemd/user/code-server.service
./release-standalone/**/*: "/usr/lib/code-server/" ./release-standalone/**/*: "/usr/lib/code-server/"

View File

@@ -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..3c0703b717 index 0000000000..8fb2a87303
--- /dev/null --- /dev/null
+++ b/src/vs/server/browser/client.ts +++ b/src/vs/server/browser/client.ts
@@ -0,0 +1,189 @@ @@ -0,0 +1,208 @@
+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,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 => { +export const getOptions = <T extends Options>(): T => {
+ if (typeof document === "undefined") {
+ return {} as T;
+ }
+ const el = document.getElementById("coder-options");
+ try { + 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) { + } catch (error) {
+ console.warn(error);
+ return {} as T; + return {} as T;
+ } + }
+}; +};
@@ -1287,15 +1306,17 @@ 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..7e1cd270c8 index 0000000000..0a9c95d50e
--- /dev/null --- /dev/null
+++ b/src/vs/server/ipc.d.ts +++ 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 + * 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
+} +}
+ +

View File

@@ -37,7 +37,6 @@ 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 => {
@@ -49,10 +48,6 @@ 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()
@@ -74,10 +69,6 @@ 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)
@@ -91,7 +82,6 @@ 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 = [
@@ -150,16 +140,6 @@ 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 {
@@ -170,7 +150,7 @@ class Watcher {
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: ".", publicUrl: "/static/development/dist",
}, },
) )
} }

View File

@@ -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 [nfpm](https://github.com/goreleaser/nfpm) to build `.deb` and `.rpm` packages. - We use [fnpm](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.

View File

@@ -86,20 +86,9 @@ 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) and this is against their terms of use. See [above](#differences-compared-to-vs-code). These variables
discussion regarding the use of the Microsoft URLs in forks: are most valuable to our enterprise customers for whom we have a self hosted marketplace product.
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?

View File

@@ -131,7 +131,7 @@ sed -i.bak 's/auth: password/auth: none/' ~/.config/code-server/config.yaml
Restart `code-server` with (assuming you followed the guide): Restart `code-server` with (assuming you followed the guide):
```bash ```bash
systemctl --user restart code-server sudo systemctl restart code-server@$USER
``` ```
Now forward local port 8080 to `127.0.0.1:8080` on the remote instance. Now forward local port 8080 to `127.0.0.1:8080` on the remote instance.
@@ -277,7 +277,7 @@ sudo setcap cap_net_bind_service=+ep /usr/lib/code-server/lib/node
Assuming you have been following the guide, restart `code-server` with: Assuming you have been following the guide, restart `code-server` with:
```bash ```bash
systemctl --user restart code-server sudo systemctl restart code-server@$USER
``` ```
Edit your instance and checkmark the allow HTTPS traffic option. Edit your instance and checkmark the allow HTTPS traffic option.
@@ -295,7 +295,7 @@ Edit the `password` field in the `code-server` config file at `~/.config/code-se
and then restart `code-server` with: and then restart `code-server` with:
```bash ```bash
systemctl --user restart code-server sudo systemctl restart code-server@$USER
``` ```
### How do I securely access development web services? ### How do I securely access development web services?

View File

@@ -81,7 +81,7 @@ commands presented in the rest of this document.
```bash ```bash
curl -fOL https://github.com/cdr/code-server/releases/download/v3.4.1/code-server_3.4.1_amd64.deb curl -fOL https://github.com/cdr/code-server/releases/download/v3.4.1/code-server_3.4.1_amd64.deb
sudo dpkg -i code-server_3.4.1_amd64.deb sudo dpkg -i code-server_3.4.1_amd64.deb
systemctl --user enable --now code-server sudo systemctl enable --now code-server@$USER
# Now visit http://127.0.0.1:8080. Your password is in ~/.config/code-server/config.yaml # Now visit http://127.0.0.1:8080. Your password is in ~/.config/code-server/config.yaml
``` ```
@@ -90,7 +90,7 @@ systemctl --user enable --now code-server
```bash ```bash
curl -fOL https://github.com/cdr/code-server/releases/download/v3.4.1/code-server-3.4.1-amd64.rpm curl -fOL https://github.com/cdr/code-server/releases/download/v3.4.1/code-server-3.4.1-amd64.rpm
sudo rpm -i code-server-3.4.1-amd64.rpm sudo rpm -i code-server-3.4.1-amd64.rpm
systemctl --user enable --now code-server sudo systemctl enable --now code-server@$USER
# Now visit http://127.0.0.1:8080. Your password is in ~/.config/code-server/config.yaml # Now visit http://127.0.0.1:8080. Your password is in ~/.config/code-server/config.yaml
``` ```
@@ -99,7 +99,7 @@ systemctl --user enable --now code-server
```bash ```bash
# Installs code-server from the AUR using yay. # Installs code-server from the AUR using yay.
yay -S code-server yay -S code-server
systemctl --user enable --now code-server sudo systemctl enable --now code-server@$USER
# Now visit http://127.0.0.1:8080. Your password is in ~/.config/code-server/config.yaml # Now visit http://127.0.0.1:8080. Your password is in ~/.config/code-server/config.yaml
``` ```
@@ -108,7 +108,7 @@ systemctl --user enable --now code-server
git clone https://aur.archlinux.org/code-server.git git clone https://aur.archlinux.org/code-server.git
cd code-server cd code-server
makepkg -si makepkg -si
systemctl --user enable --now code-server sudo systemctl enable --now code-server@$USER
# Now visit http://127.0.0.1:8080. Your password is in ~/.config/code-server/config.yaml # Now visit http://127.0.0.1:8080. Your password is in ~/.config/code-server/config.yaml
``` ```

View File

@@ -84,7 +84,7 @@ echo_systemd_postinstall() {
echoh echoh
cath << EOF cath << EOF
To have systemd start code-server now and restart on boot: To have systemd start code-server now and restart on boot:
systemctl --user enable --now code-server sudo systemctl enable --now code-server@$USER
Or, if you don't want/need a background service you can run: Or, if you don't want/need a background service you can run:
code-server code-server
EOF EOF

View File

@@ -7,32 +7,32 @@
"description": "Run editors on a remote server.", "description": "Run editors on a remote server.",
"icons": [ "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", "type": "image/png",
"sizes": "96x96" "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", "type": "image/png",
"sizes": "128x128" "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", "type": "image/png",
"sizes": "192x192" "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", "type": "image/png",
"sizes": "256x256" "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", "type": "image/png",
"sizes": "384x384" "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", "type": "image/png",
"sizes": "512x512" "sizes": "512x512"
} }

View File

@@ -11,10 +11,14 @@
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="{{CS_STATIC_BASE}}/src/browser/media/favicon.ico" type="image/x-icon" /> <link rel="icon" href="{{BASE}}/static/{{COMMIT}}/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
<link rel="apple-touch-icon" href="{{CS_STATIC_BASE}}/src/browser/media/pwa-icon-384.png" /> rel="manifest"
<link href="{{CS_STATIC_BASE}}/dist/register.css" rel="stylesheet" /> 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}}" /> <meta id="coder-options" data-settings="{{OPTIONS}}" />
</head> </head>
<body> <body>
@@ -29,6 +33,6 @@
</div> </div>
</div> </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> </body>
</html> </html>

View File

@@ -11,10 +11,14 @@
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="{{CS_STATIC_BASE}}/src/browser/media/favicon.ico" type="image/x-icon" /> <link rel="icon" href="{{BASE}}/static/{{COMMIT}}/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
<link rel="apple-touch-icon" href="{{CS_STATIC_BASE}}/src/browser/media/pwa-icon-384.png" /> rel="manifest"
<link href="{{CS_STATIC_BASE}}/dist/register.css" rel="stylesheet" /> 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}}" /> <meta id="coder-options" data-settings="{{OPTIONS}}" />
</head> </head>
<body> <body>
@@ -46,7 +50,7 @@
</div> </div>
</div> </div>
</body> </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> <script>
const parts = window.location.pathname.replace(/^\//g, "").split("/") const parts = window.location.pathname.replace(/^\//g, "").split("/")
parts[parts.length - 1] = "{{BASE}}" parts[parts.length - 1] = "{{BASE}}"

View File

@@ -24,17 +24,21 @@
<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="{{CS_STATIC_BASE}}/src/browser/media/favicon.ico" type="image/x-icon" /> <link rel="icon" href="{{BASE}}/static/{{COMMIT}}/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="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="{{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 --> 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" /> <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="{{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 --> END_PROD_ONLY -->
<meta id="coder-options" data-settings="{{OPTIONS}}" /> <meta id="coder-options" data-settings="{{OPTIONS}}" />
@@ -44,6 +48,10 @@
<!-- Startup (do not modify order of script tags!) --> <!-- Startup (do not modify order of script tags!) -->
<script> <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 let nlsConfig
try { try {
nlsConfig = JSON.parse(document.getElementById("vscode-remote-nls-configuration").getAttribute("data-settings")) nlsConfig = JSON.parse(document.getElementById("vscode-remote-nls-configuration").getAttribute("data-settings"))
@@ -56,7 +64,7 @@
} }
// FIXME: Only works if path separators are /. // FIXME: Only works if path separators are /.
const path = nlsConfig._resolvedLanguagePackCoreLocation + "/" + bundle.replace(/\//g, "!") + ".nls.json" 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((response) => response.json())
.then((json) => { .then((json) => {
bundles[bundle] = json bundles[bundle] = json
@@ -69,26 +77,26 @@
/* Probably fine. */ /* Probably fine. */
} }
self.require = { self.require = {
baseUrl: "{{CS_STATIC_BASE}}/lib/vscode/out", baseUrl: `${staticBase}/out`,
paths: { paths: {
"vscode-textmate": `../node_modules/vscode-textmate/release/main`, "vscode-textmate": `${staticBase}/node_modules/vscode-textmate/release/main`,
"vscode-oniguruma": `../node_modules/vscode-oniguruma/release/main`, "vscode-oniguruma": `${staticBase}/node_modules/vscode-oniguruma/release/main`,
xterm: `../node_modules/xterm/lib/xterm.js`, xterm: `${staticBase}/node_modules/xterm/lib/xterm.js`,
"xterm-addon-search": `../node_modules/xterm-addon-search/lib/xterm-addon-search.js`, "xterm-addon-search": `${staticBase}/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-unicode11": `${staticBase}/node_modules/xterm-addon-unicode11/lib/xterm-addon-unicode11.js`,
"xterm-addon-webgl": `../node_modules/xterm-addon-webgl/lib/xterm-addon-webgl.js`, "xterm-addon-webgl": `${staticBase}/node_modules/xterm-addon-webgl/lib/xterm-addon-webgl.js`,
"semver-umd": `../node_modules/semver-umd/lib/semver-umd.js`, "semver-umd": `${staticBase}/node_modules/semver-umd/lib/semver-umd.js`,
"iconv-lite-umd": `../node_modules/iconv-lite-umd/lib/iconv-lite-umd.js`, "iconv-lite-umd": `${staticBase}/node_modules/iconv-lite-umd/lib/iconv-lite-umd.js`,
jschardet: `../node_modules/jschardet/dist/jschardet.min.js`, jschardet: `${staticBase}/node_modules/jschardet/dist/jschardet.min.js`,
}, },
"vs/nls": nlsConfig, "vs/nls": nlsConfig,
} }
</script> </script>
<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 data-cfasync="false" src="{{CS_STATIC_BASE}}/lib/vscode/out/vs/loader.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="{{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.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.js"></script>
END_PROD_ONLY --> END_PROD_ONLY -->
<script> <script>
require(["vs/code/browser/workbench/workbench"], function () {}) require(["vs/code/browser/workbench/workbench"], function () {})

View File

@@ -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.csStaticBase}/dist/serviceWorker.js`) const path = normalize(`${options.base}/static/${options.commit}/dist/serviceWorker.js`)
navigator.serviceWorker navigator.serviceWorker
.register(path, { .register(path, {
scope: options.base || "/", scope: options.base || "/",

View File

@@ -1,21 +1,19 @@
import { Callback } from "./types"
export interface Disposable { export interface Disposable {
dispose(): void dispose(): void
} }
export interface Event<T> { export interface Event<T> {
(listener: Callback<T>): Disposable (listener: (value: T) => void): 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<Callback<T>> = [] private listeners: Array<(value: T) => void> = []
public get event(): Event<T> { public get event(): Event<T> {
return (cb: Callback<T>): Disposable => { return (cb: (value: T) => void): Disposable => {
this.listeners.push(cb) this.listeners.push(cb)
return { return {

View File

@@ -1 +0,0 @@
export type Callback<T, R = void> = (t: T) => R

View File

@@ -2,8 +2,9 @@ import { logger, field } from "@coder/logger"
export interface Options { export interface Options {
base: string base: string
csStaticBase: string commit: string
logLevel: number 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, ""] 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"
@@ -43,28 +40,21 @@ 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 {
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) { } catch (error) {
options = {} as T options = {} as T
} }
@@ -78,10 +68,15 @@ export const getOptions = <T extends Options>(): T => {
} }
} }
if (typeof options.logLevel !== "undefined") {
logger.level = options.logLevel logger.level = options.logLevel
}
options.base = resolveBase(options.base) if (options.base) {
options.csStaticBase = resolveBase(options.csStaticBase) 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)) logger.debug("got options", field("options", options))

View File

@@ -8,9 +8,10 @@ import { HttpProvider, HttpResponse, Route } from "../http"
import { pathToFsPath } from "../util" import { pathToFsPath } from "../util"
/** /**
* Static file HTTP provider. Static requests do not require authentication if * Static file HTTP provider. Regular static requests (the path is the request
* the resource is in the application's directory except requests to serve a * itself) do not require authentication and they only allow access to resources
* directory as a tar which always requires authentication. * 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 { 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> {
@@ -21,7 +22,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(request, route) const response = await this.getReplacedResource(route)
if (!this.isDev) { if (!this.isDev) {
response.cache = true response.cache = true
} }
@@ -31,25 +32,17 @@ export class StaticHttpProvider extends HttpProvider {
/** /**
* Return a resource with variables replaced where necessary. * 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). // 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(resourcePath) const response = await this.getUtf8Resource(this.rootPath, ...split)
return this.replaceTemplates(route, response) return this.replaceTemplates(route, response)
} }
} }
return this.getResource(resourcePath) return this.getResource(this.rootPath, ...split)
} }
/** /**

View File

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

View File

@@ -9,10 +9,8 @@ import { UpdateHttpProvider } from "./app/update"
import { VscodeHttpProvider } from "./app/vscode" import { VscodeHttpProvider } from "./app/vscode"
import { Args, bindAddrFromAllSources, optionDescriptions, parse, readConfigFile, setDefaults } from "./cli" import { Args, bindAddrFromAllSources, optionDescriptions, parse, readConfigFile, setDefaults } from "./cli"
import { AuthType, HttpServer, HttpServerOptions } from "./http" import { AuthType, HttpServer, HttpServerOptions } from "./http"
import { loadPlugins } from "./plugin" import { generateCertificate, hash, open, humanPath } from "./util"
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}`)
@@ -79,8 +77,6 @@ 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))
@@ -114,7 +110,7 @@ const main = async (args: Args, cliArgs: Args, configArgs: Args): Promise<void>
} }
if (httpServer.proxyDomains.size > 0) { 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}`)) httpServer.proxyDomains.forEach((domain) => logger.info(` - *.${domain}`))
} }

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 * 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,23 +235,30 @@ 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,
extraOptions?: Omit<T, "base" | "csStaticBase" | "logLevel">, options: T,
): HttpStringFileResponse
protected replaceTemplates(
route: Route,
response: HttpStringFileResponse,
sessionIdOrOptions?: string | object,
): HttpStringFileResponse { ): HttpStringFileResponse {
const base = this.base(route) if (typeof sessionIdOrOptions === "undefined" || typeof sessionIdOrOptions === "string") {
const options: Options = { sessionIdOrOptions = {
base, base: this.base(route),
csStaticBase: base + "/static/" + this.options.commit + this.rootPath, commit: this.options.commit,
logLevel: logger.level, logLevel: logger.level,
...extraOptions, sessionID: sessionIdOrOptions,
} as Options
} }
response.content = response.content response.content = response.content
.replace(/{{COMMIT}}/g, this.options.commit)
.replace(/{{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, options.base) .replace(/{{BASE}}/g, this.base(route))
.replace(/{{CS_STATIC_BASE}}/g, options.csStaticBase) .replace(/"{{OPTIONS}}"/, `'${JSON.stringify(sessionIdOrOptions)}'`)
.replace(/"{{OPTIONS}}"/, `'${JSON.stringify(options)}'`)
return response return response
} }
@@ -474,7 +481,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(plural(connections, `${connections} active connection`)) logger.trace(`${connections} active connection${plural(connections)}`)
return connections !== 0 return connections !== 0
}) })
this.protocol = this.options.cert ? "https" : "http" this.protocol = this.options.cert ? "https" : "http"
@@ -657,7 +664,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), field("error", error)) logger.debug("Request error", field("url", request.url), field("code", code))
if (code >= HttpCode.ServerError) { if (code >= HttpCode.ServerError) {
logger.error(error.stack) logger.error(error.stack)
} }

View File

@@ -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)
}
}

View File

@@ -32,7 +32,7 @@ export class IpcMain {
public readonly onMessage = this._onMessage.event public readonly onMessage = this._onMessage.event
private readonly _onDispose = new Emitter<NodeJS.Signals | undefined>() private readonly _onDispose = new Emitter<NodeJS.Signals | undefined>()
public readonly onDispose = this._onDispose.event public readonly onDispose = this._onDispose.event
public readonly processExit: (code?: number) => never public readonly exit: (code?: number) => never
public constructor(public readonly parentPid?: number) { public constructor(public readonly parentPid?: number) {
process.on("SIGINT", () => this._onDispose.emit("SIGINT")) process.on("SIGINT", () => this._onDispose.emit("SIGINT"))
@@ -40,7 +40,7 @@ export class IpcMain {
process.on("exit", () => this._onDispose.emit(undefined)) process.on("exit", () => this._onDispose.emit(undefined))
// Ensure we control when the process exits. // Ensure we control when the process exits.
this.processExit = process.exit this.exit = process.exit
process.exit = function (code?: number) { process.exit = function (code?: number) {
logger.warn(`process.exit() was prevented: ${code || "unknown code"}.`) logger.warn(`process.exit() was prevented: ${code || "unknown code"}.`)
} as (code?: number) => never } 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> { public handshake(child?: cp.ChildProcess): Promise<void> {
return new Promise((resolve, reject) => { return new Promise((resolve, reject) => {
const target = child || process const target = child || process
@@ -169,26 +161,11 @@ export class WrapperProcess {
} }
}) })
ipcMain().onMessage((message) => { ipcMain().onMessage(async (message) => {
switch (message.type) { switch (message.type) {
case "relaunch": case "relaunch":
logger.info(`Relaunching: ${this.currentVersion} -> ${message.version}`) logger.info(`Relaunching: ${this.currentVersion} -> ${message.version}`)
this.currentVersion = message.version this.currentVersion = message.version
this.relaunch()
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 this.started = undefined
if (this.process) { if (this.process) {
this.process.removeAllListeners() this.process.removeAllListeners()
@@ -200,6 +177,12 @@ export class WrapperProcess {
logger.error(error.message) logger.error(error.message)
ipcMain().exit(typeof error.code === "number" ? error.code : 1) ipcMain().exit(typeof error.code === "number" ? error.code : 1)
} }
break
default:
logger.error(`Unrecognized message ${message}`)
break
}
})
} }
public start(): Promise<void> { public start(): Promise<void> {
@@ -261,13 +244,13 @@ export const wrap = (fn: () => Promise<void>): void => {
.then(() => fn()) .then(() => fn())
.catch((error: ProcessError): void => { .catch((error: ProcessError): void => {
logger.error(error.message) logger.error(error.message)
ipcMain().exit(error) ipcMain().exit(typeof error.code === "number" ? error.code : 1)
}) })
} else { } else {
const wrapper = new WrapperProcess(require("../../package.json").version) const wrapper = new WrapperProcess(require("../../package.json").version)
wrapper.start().catch((error) => { wrapper.start().catch((error) => {
logger.error(error.message) logger.error(error.message)
ipcMain().exit(error) ipcMain().exit(typeof error.code === "number" ? error.code : 1)
}) })
} }
} }