Merge pull request #1 from cdr/master

Master
This commit is contained in:
Meng Jun 2020-09-16 14:24:21 +08:00 committed by GitHub
commit ce656a0104
No known key found for this signature in database
GPG Key ID: 4AEE18F83AFDEB23
58 changed files with 2599 additions and 2215 deletions

View File

@ -23,6 +23,13 @@ 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
"@typescript-eslint/ban-types": off
"@typescript-eslint/no-var-requires": off
"@typescript-eslint/explicit-module-boundary-types": off
"@typescript-eslint/no-explicit-any": 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.

5
.github/ISSUE_TEMPLATE/config.yml vendored Normal file
View File

@ -0,0 +1,5 @@
blank_issues_enabled: false
contact_links:
- name: Question
url: https://github.com/cdr/code-server/discussions/new?category_id=22503114
about: Ask the community for help

7
.github/ISSUE_TEMPLATE/doc.md vendored Normal file
View File

@ -0,0 +1,7 @@
---
name: Documentation improvement
about: Suggest a documentation improvement
title: ""
labels: "docs"
assignees: ""
---

View File

@ -1,4 +0,0 @@
<!--
Please file all questions and support requests at https://www.reddit.com/r/codeserver/
The issue tracker is only for bugs and features.
-->

View File

@ -1,4 +1,6 @@
<!-- <!--
Please link to the issue this PR solves. Please link to the issue this PR solves.
If there is no existing issue, please first create one unless the fix is minor. If there is no existing issue, please first create one unless the fix is minor.
Please make sure the base of your PR is the master branch!
--> -->

1
.gitignore vendored
View File

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

View File

@ -11,7 +11,7 @@ Run [VS Code](https://github.com/Microsoft/vscode) on any machine anywhere and a
- Develop on a Linux machine and pick up from any device with a web browser. - Develop on a Linux machine and pick up from any device with a web browser.
- **Server-powered** - **Server-powered**
- Take advantage of large cloud servers to speed up tests, compilations, downloads, and more. - Take advantage of large cloud servers to speed up tests, compilations, downloads, and more.
- Preserve battery life when you're on the go as all intensive tasks runs on your server. - Preserve battery life when you're on the go as all intensive tasks run on your server.
- Make use of a spare computer you have lying around and turn it into a full development environment. - Make use of a spare computer you have lying around and turn it into a full development environment.
## Getting Started ## Getting Started
@ -52,7 +52,7 @@ See [./doc/CONTRIBUTING.md](./doc/CONTRIBUTING.md).
## Hiring ## Hiring
We ([@cdr](https://github.com/cdr)) are looking for a engineers to help maintain We ([@cdr](https://github.com/cdr)) are looking for engineers to help maintain
code-server, innovate on open source and streamline dev workflows. code-server, innovate on open source and streamline dev workflows.
Our main office is in Austin, Texas. Remote is ok as long as Our main office is in Austin, Texas. Remote is ok as long as
@ -60,6 +60,9 @@ you're in North America or Europe.
Please get in [touch](mailto:jobs@coder.com) with your resume/github if interested. Please get in [touch](mailto:jobs@coder.com) with your resume/github if interested.
We're also hiring someone specifically to help maintain code-server.
See the listing [here](https://jobs.lever.co/coder/e40becde-2cbd-4885-9029-e5c7b0a734b8).
## For Organizations ## For Organizations
Visit [our website](https://coder.com) for more information about remote development for your organization or enterprise. Visit [our website](https://coder.com) for more information about remote development for your organization or enterprise.

View File

@ -18,13 +18,15 @@ Make sure you have `$GITHUB_TOKEN` set and [hub](https://github.com/github/hub)
1. Update in `package.json` 1. Update in `package.json`
2. Update in [./doc/install.md](../doc/install.md) 2. Update in [./doc/install.md](../doc/install.md)
2. GitHub actions will generate the `npm-package`, `release-packages` and `release-images` artifacts. 2. GitHub actions will generate the `npm-package`, `release-packages` and `release-images` artifacts.
1. You do not have to wait for these.
3. Run `yarn release:github-draft` to create a GitHub draft release from the template with 3. Run `yarn release:github-draft` to create a GitHub draft release from the template with
the updated version. the updated version.
1. Summarize the major changes in the release notes and link to the relevant issues. 1. Summarize the major changes in the release notes and link to the relevant issues.
4. Wait for the artifacts in step 2 to build. 4. Wait for the artifacts in step 2 to build.
5. Run `yarn release:github-assets` to download the `release-packages` artifact and 5. Run `yarn release:github-assets` to download the `release-packages` artifact.
upload them to the draft release. - It will upload them to the draft release.
6. Run some basic sanity tests on one of the released packages. 6. Run some basic sanity tests on one of the released packages.
- Especially make sure the terminal works fine.
7. Make sure the github release tag is the commit with the artifacts. This is a bug in 7. Make sure the github release tag is the commit with the artifacts. This is a bug in
`hub` where uploading assets in step 5 will break the tag. `hub` where uploading assets in step 5 will break the tag.
8. Publish the release and merge the PR. 8. Publish the release and merge the PR.
@ -36,7 +38,6 @@ Make sure you have `$GITHUB_TOKEN` set and [hub](https://github.com/github/hub)
10. Wait for the npm package to be published. 10. Wait for the npm package to be published.
11. Update the homebrew package. 11. Update the homebrew package.
- Send a pull request to [homebrew-core](https://github.com/Homebrew/homebrew-core) with the URL in the [formula](https://github.com/Homebrew/homebrew-core/blob/master/Formula/code-server.rb) updated. - Send a pull request to [homebrew-core](https://github.com/Homebrew/homebrew-core) with the URL in the [formula](https://github.com/Homebrew/homebrew-core/blob/master/Formula/code-server.rb) updated.
12. Make sure to add a release without the `v` prefix for autoupdate from `3.2.0`.
## dev ## dev

View File

@ -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 "$@"

View File

@ -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() {
@ -31,6 +37,7 @@ bundle_code_server() {
rsync src/browser/media/ "$RELEASE_PATH/src/browser/media" rsync src/browser/media/ "$RELEASE_PATH/src/browser/media"
mkdir -p "$RELEASE_PATH/src/browser/pages" mkdir -p "$RELEASE_PATH/src/browser/pages"
rsync src/browser/pages/*.html "$RELEASE_PATH/src/browser/pages" rsync src/browser/pages/*.html "$RELEASE_PATH/src/browser/pages"
rsync src/browser/robots.txt "$RELEASE_PATH/src/browser"
# Adds the commit to package.json # Adds the commit to package.json
jq --slurp '.[0] * .[1]' package.json <( jq --slurp '.[0] * .[1]' package.json <(

View File

@ -5,16 +5,16 @@ main() {
cd "$(dirname "${0}")/../.." cd "$(dirname "${0}")/../.."
source ./ci/lib.sh source ./ci/lib.sh
rm -Rf \ rm -rf \
out \ out \
release \ release \
release-standalone \ release-standalone \
release-packages \ release-packages \
release-gcp \ release-gcp \
release-images/ \ release-images \
dist \ dist \
.tsbuildinfo \ .cache \
.cache/out.tsbuildinfo node-*
pushd lib/vscode pushd lib/vscode
git clean -xffd git clean -xffd

View File

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

View File

@ -12,5 +12,8 @@ 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
# Only included for backwards compat with previous releases that shipped
# the user service. See #1997
./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

@ -6,7 +6,7 @@ main() {
cd ./lib/vscode cd ./lib/vscode
git add -A git add -A
git diff HEAD > ../../ci/dev/vscode.patch git diff HEAD --full-index > ../../ci/dev/vscode.patch
} }
main "$@" main "$@"

View File

@ -1,13 +0,0 @@
FROM node:12
RUN apt-get update && apt-get install -y \
curl \
iproute2 \
vim \
iptables \
net-tools \
libsecret-1-dev \
libx11-dev \
libxkbfile-dev
CMD ["/bin/bash"]

View File

@ -1,48 +0,0 @@
#!/usr/bin/env bash
set -euo pipefail
# Opens an interactive bash session inside of a docker container
# for improved isolation during development.
# If the container exists it is restarted if necessary, then reused.
main() {
cd "$(dirname "${0}")/../../.."
local container_name=code-server-dev
if docker inspect $container_name &> /dev/null; then
echo "-- Starting container"
docker start "$container_name" > /dev/null
enter
exit 0
fi
build
run
enter
}
enter() {
echo "--- Entering $container_name"
docker exec -it "$container_name" /bin/bash
}
run() {
echo "--- Spawning $container_name"
docker run \
-it \
--name $container_name \
"-v=$PWD:/code-server" \
"-w=/code-server" \
"-p=127.0.0.1:8080:8080" \
$(if [[ -t 0 ]]; then echo -it; fi) \
"$container_name"
}
build() {
echo "--- Building $container_name"
docker build -t $container_name ./ci/dev/image > /dev/null
}
main "$@"

25
ci/dev/image/run.sh Executable file
View File

@ -0,0 +1,25 @@
#!/usr/bin/env bash
set -euo pipefail
main() {
cd "$(dirname "$0")/../../.."
source ./ci/lib.sh
docker run \
-it \
--rm \
-v "$PWD:/src" \
-w /src \
-p 127.0.0.1:8080:8080 \
-u "$(id -u):$(id -g)" \
-e CI \
"$(docker_build ./ci/images/debian8)" \
"$@"
}
docker_build() {
docker build "$@" >&2
docker build -q "$@"
}
main "$@"

File diff suppressed because it is too large Load Diff

View File

@ -37,6 +37,9 @@ 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 = process.env.PLUGIN_DIR
? cp.spawn("yarn", ["build", "--watch"], { cwd: process.env.PLUGIN_DIR })
: undefined
const bundler = this.createBundler() const bundler = this.createBundler()
const cleanup = (code?: number | null): void => { const cleanup = (code?: number | null): void => {
@ -48,6 +51,12 @@ class Watcher {
tsc.removeAllListeners() tsc.removeAllListeners()
tsc.kill() tsc.kill()
if (plugin) {
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 +78,12 @@ class Watcher {
Watcher.log("tsc terminated unexpectedly") Watcher.log("tsc terminated unexpectedly")
cleanup(code) cleanup(code)
}) })
if (plugin) {
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 +97,9 @@ 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))
if (plugin) {
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 +158,34 @@ class Watcher {
bundle.then(restartServer) bundle.then(restartServer)
} }
}) })
if (plugin) {
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: ".",
}, },
) )
} }

View File

@ -1,9 +1,10 @@
FROM centos:7 FROM centos:7
ARG NODE_VERSION=v12.18.3
RUN ARCH="$(uname -m | sed 's/86_64/64/; s/aarch64/arm64/')" && \ RUN ARCH="$(uname -m | sed 's/86_64/64/; s/aarch64/arm64/')" && \
curl -fsSL "https://nodejs.org/dist/v14.4.0/node-v14.4.0-linux-$ARCH.tar.xz" | tar -C /usr/local -xJ && \ curl -fsSL "https://nodejs.org/dist/$NODE_VERSION/node-$NODE_VERSION-linux-$ARCH.tar.xz" | tar -C /usr/local -xJ && \
mv /usr/local/node-v14.4.0-linux-$ARCH /usr/local/node-v14.4.0 mv "/usr/local/node-$NODE_VERSION-linux-$ARCH" "/usr/local/node-$NODE_VERSION"
ENV PATH=/usr/local/node-v14.4.0/bin:$PATH ENV PATH=/usr/local/node-$NODE_VERSION/bin:$PATH
RUN npm install -g yarn RUN npm install -g yarn
RUN yum groupinstall -y 'Development Tools' RUN yum groupinstall -y 'Development Tools'

View File

@ -6,7 +6,7 @@ RUN apt-get update
RUN apt-get install -y curl gnupg RUN apt-get install -y curl gnupg
# Installs node. # Installs node.
RUN curl -fsSL https://deb.nodesource.com/setup_14.x | bash - && \ RUN curl -fsSL https://deb.nodesource.com/setup_12.x | bash - && \
apt-get install -y nodejs apt-get install -y nodejs
# Installs yarn. # Installs yarn.

View File

@ -35,9 +35,13 @@ RUN ARCH="$(dpkg --print-architecture)" && \
printf "user: coder\ngroup: coder\n" > /etc/fixuid/config.yml printf "user: coder\ngroup: coder\n" > /etc/fixuid/config.yml
COPY release-packages/code-server*.deb /tmp/ COPY release-packages/code-server*.deb /tmp/
COPY ci/release-image/entrypoint.sh /usr/bin/entrypoint.sh
RUN dpkg -i /tmp/code-server*$(dpkg --print-architecture).deb && rm /tmp/code-server*.deb RUN dpkg -i /tmp/code-server*$(dpkg --print-architecture).deb && rm /tmp/code-server*.deb
EXPOSE 8080 EXPOSE 8080
USER coder # This way, if someone sets $DOCKER_USER, docker-exec will still work as
# the uid will remain the same. note: only relevant if -u isn't passed to
# docker-run.
USER 1000
WORKDIR /home/coder WORKDIR /home/coder
ENTRYPOINT ["dumb-init", "fixuid", "-q", "/usr/bin/code-server", "--bind-addr", "0.0.0.0:8080", "."] ENTRYPOINT ["/usr/bin/entrypoint.sh", "--bind-addr", "0.0.0.0:8080", "."]

20
ci/release-image/entrypoint.sh Executable file
View File

@ -0,0 +1,20 @@
#!/bin/sh
set -eu
# This isn't set by default.
export USER="$(whoami)"
if [ "${DOCKER_USER-}" != "$USER" ]; then
echo "$DOCKER_USER ALL=(ALL) NOPASSWD:ALL" | sudo tee -a /etc/sudoers.d/nopasswd > /dev/null
# Unfortunately we cannot change $HOME as we cannot move any bind mounts
# nor can we bind mount $HOME into a new home as that requires a privileged container.
sudo usermod --login "$DOCKER_USER" coder
sudo groupmod -n "$DOCKER_USER" coder
export USER="$(whoami)"
sudo sed -i "/coder/d" /etc/sudoers.d/nopasswd
sudo sed -i "s/coder/$DOCKER_USER/g" /etc/fixuid/config.yml
fi
dumb-init fixuid -q /usr/bin/code-server "$@"

View File

@ -4,10 +4,11 @@ set -euo pipefail
main() { main() {
cd "$(dirname "$0")/../.." cd "$(dirname "$0")/../.."
if [[ $OSTYPE == darwin* ]]; then NODE_VERSION=v12.18.3
curl -L https://nodejs.org/dist/v14.4.0/node-v14.4.0-darwin-x64.tar.gz | tar -xz NODE_OS="$(uname | tr '[:upper:]' '[:lower:]')"
PATH="$PWD/node-v14.4.0-darwin-x64/bin:$PATH" NODE_ARCH="$(uname -m | sed 's/86_64/64/; s/aarch64/arm64/')"
fi curl -L "https://nodejs.org/dist/$NODE_VERSION/node-$NODE_VERSION-$NODE_OS-$NODE_ARCH.tar.gz" | tar -xz
PATH="$PWD/node-$NODE_VERSION-$NODE_OS-$NODE_ARCH/bin:$PATH"
# https://github.com/actions/upload-artifact/issues/38 # https://github.com/actions/upload-artifact/issues/38
tar -xzf release-npm-package/package.tar.gz tar -xzf release-npm-package/package.tar.gz

View File

@ -2,6 +2,7 @@
<!-- DON'T EDIT THIS SECTION, INSTEAD RE-RUN doctoc TO UPDATE --> <!-- DON'T EDIT THIS SECTION, INSTEAD RE-RUN doctoc TO UPDATE -->
# Contributing # Contributing
- [Pull Requests](#pull-requests)
- [Requirements](#requirements) - [Requirements](#requirements)
- [Development Workflow](#development-workflow) - [Development Workflow](#development-workflow)
- [Build](#build) - [Build](#build)
@ -12,6 +13,16 @@
- [Detailed CI and build process docs](../ci) - [Detailed CI and build process docs](../ci)
## Pull Requests
Please link to the issue each PR solves.
If there is no existing issue, please first create one unless the fix is minor.
Please make sure the base of your PR is the master branch. We keep the GitHub
default branch the latest release branch to avoid confusion as the
documentation is on GitHub and we don't want users to see docs on unreleased
features.
## Requirements ## Requirements
Please refer to [VS Code's prerequisites](https://github.com/Microsoft/vscode/wiki/How-to-Contribute#prerequisites). Please refer to [VS Code's prerequisites](https://github.com/Microsoft/vscode/wiki/How-to-Contribute#prerequisites).
@ -35,40 +46,57 @@ yarn watch
To develop inside of an isolated docker container: To develop inside of an isolated docker container:
```shell ```shell
./ci/dev/image/exec.sh ./ci/dev/image/run.sh yarn
./ci/dev/image/run.sh yarn vscode
root@12345:/code-server# yarn ./ci/dev/image/run.sh yarn watch
root@12345:/code-server# yarn vscode
root@12345:/code-server# yarn watch
``` ```
Any changes made to the source will be live reloaded. `yarn watch` will live reload changes to the source.
If changes are made to the patch and you've built previously you must manually If changes are made to the patch and you've built previously you must manually
reset VS Code then run `yarn vscode:patch`. reset VS Code then run `yarn vscode:patch`.
## Build ## Build
You can build with:
```shell
./ci/dev/image/run.sh ./ci/steps/release.sh
```
Run your build with:
```
cd release
yarn --production
# Runs the built JavaScript with Node.
node .
```
Build release packages (make sure you run `./ci/steps/release.sh` first):
```
./ci/dev/image/run.sh ./ci/steps/release-packages.sh
# The standalone release is in ./release-standalone
# .deb, .rpm and the standalone archive are in ./release-packages
```
The `release.sh` script is the equivalent of:
```shell ```shell
yarn yarn
yarn vscode yarn vscode
yarn build yarn build
yarn build:vscode yarn build:vscode
yarn release yarn release
cd release
yarn --production
# Runs the built JavaScript with Node.
node .
``` ```
Now you can build release packages with: And `release-packages.sh` is:
``` ```
yarn release:standalone yarn release:standalone
# The standalone release is in ./release-standalone
yarn test:standalone-release yarn test:standalone-release
yarn package yarn package
# .deb, .rpm and the standalone archive are in ./release-packages
``` ```
## Structure ## Structure

View File

@ -19,6 +19,7 @@
- [How does code-server decide what workspace or folder to open?](#how-does-code-server-decide-what-workspace-or-folder-to-open) - [How does code-server decide what workspace or folder to open?](#how-does-code-server-decide-what-workspace-or-folder-to-open)
- [How do I debug issues with code-server?](#how-do-i-debug-issues-with-code-server) - [How do I debug issues with code-server?](#how-do-i-debug-issues-with-code-server)
- [Heartbeat File](#heartbeat-file) - [Heartbeat File](#heartbeat-file)
- [Healthz endpoint](#healthz-endpoint)
- [How does the config file work?](#how-does-the-config-file-work) - [How does the config file work?](#how-does-the-config-file-work)
- [Blank screen on iPad?](#blank-screen-on-ipad) - [Blank screen on iPad?](#blank-screen-on-ipad)
- [Isn't an install script piped into sh insecure?](#isnt-an-install-script-piped-into-sh-insecure) - [Isn't an install script piped into sh insecure?](#isnt-an-install-script-piped-into-sh-insecure)
@ -30,9 +31,7 @@
## Questions? ## Questions?
Please file all questions and support requests at https://www.reddit.com/r/codeserver/. Please file all questions and support requests at https://github.com/cdr/code-server/discussions.
The issue tracker is **only** for bugs and features.
## How can I reuse my VS Code configuration? ## How can I reuse my VS Code configuration?
@ -244,6 +243,20 @@ older than X minutes, kill `code-server`.
[#1636](https://github.com/cdr/code-server/issues/1636) will make the experience here better. [#1636](https://github.com/cdr/code-server/issues/1636) will make the experience here better.
## Healthz endpoint
`code-server` exposes an endpoint at `/healthz` which can be used to check
whether `code-server` is up without triggering a heartbeat. The response will
include a status (`alive` or `expired`) and a timestamp for the last heartbeat
(defaults to `0`). This endpoint does not require authentication.
```json
{
"status": "alive",
"lastHeartbeat": 1599166210566
}
```
## How does the config file work? ## How does the config file work?
When `code-server` starts up, it creates a default config file in `~/.config/code-server/config.yaml` that looks When `code-server` starts up, it creates a default config file in `~/.config/code-server/config.yaml` that looks

View File

@ -131,16 +131,16 @@ 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 by running the following command on your local machine.
Recommended reading: https://help.ubuntu.com/community/SSH/OpenSSH/PortForwarding. Recommended reading: https://help.ubuntu.com/community/SSH/OpenSSH/PortForwarding.
```bash ```bash
# -N disables executing a remote shell # -N disables executing a remote shell
ssh -N -L 8080:127.0.0.1:8080 <instance-ip> ssh -N -L 8080:127.0.0.1:8080 [user]@<instance-ip>
``` ```
Now if you access http://127.0.0.1:8080 locally, you should see `code-server`! Now if you access http://127.0.0.1:8080 locally, you should see `code-server`!
@ -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

@ -79,18 +79,18 @@ commands presented in the rest of this document.
## Debian, Ubuntu ## Debian, Ubuntu
```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.5.0/code-server_3.5.0_amd64.deb
sudo dpkg -i code-server_3.4.1_amd64.deb sudo dpkg -i code-server_3.5.0_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
``` ```
## Fedora, CentOS, RHEL, SUSE ## Fedora, CentOS, RHEL, SUSE
```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.5.0/code-server-3.5.0-amd64.rpm
sudo rpm -i code-server-3.4.1-amd64.rpm sudo rpm -i code-server-3.5.0-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
``` ```
@ -158,10 +158,10 @@ Here is an example script for installing and using a standalone `code-server` re
```bash ```bash
mkdir -p ~/.local/lib ~/.local/bin mkdir -p ~/.local/lib ~/.local/bin
curl -fL https://github.com/cdr/code-server/releases/download/v3.4.1/code-server-3.4.1-linux-amd64.tar.gz \ curl -fL https://github.com/cdr/code-server/releases/download/v3.5.0/code-server-3.5.0-linux-amd64.tar.gz \
| tar -C ~/.local/lib -xz | tar -C ~/.local/lib -xz
mv ~/.local/lib/code-server-3.4.1-linux-amd64 ~/.local/lib/code-server-3.4.1 mv ~/.local/lib/code-server-3.5.0-linux-amd64 ~/.local/lib/code-server-3.5.0
ln -s ~/.local/lib/code-server-3.4.1/bin/code-server ~/.local/bin/code-server ln -s ~/.local/lib/code-server-3.5.0/bin/code-server ~/.local/bin/code-server
PATH="~/.local/bin:$PATH" PATH="~/.local/bin:$PATH"
code-server code-server
# 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
@ -174,9 +174,16 @@ code-server
# It will also mount your current directory into the container as `/home/coder/project` # It will also mount your current directory into the container as `/home/coder/project`
# and forward your UID/GID so that all file system operations occur as your user outside # and forward your UID/GID so that all file system operations occur as your user outside
# the container. # the container.
docker run -it -p 127.0.0.1:8080:8080 \ #
# Your $HOME/.config is mounted at $HOME/.config within the container to ensure you can
# easily access/modify your code-server config in $HOME/.config/code-server/config.json
# outside the container.
mkdir -p ~/.config
docker run -it --name code-server -p 127.0.0.1:8080:8080 \
-v "$HOME/.config:/home/coder/.config" \
-v "$PWD:/home/coder/project" \ -v "$PWD:/home/coder/project" \
-u "$(id -u):$(id -g)" \ -u "$(id -u):$(id -g)" \
-e "DOCKER_USER=$USER" \
codercom/code-server:latest codercom/code-server:latest
``` ```

View File

@ -21,7 +21,9 @@ sudo apt-get install -y \
pkg-config \ pkg-config \
libx11-dev \ libx11-dev \
libxkbfile-dev \ libxkbfile-dev \
libsecret-1-dev libsecret-1-dev \
python3
npm config set python python3
``` ```
## Fedora, CentOS, RHEL ## Fedora, CentOS, RHEL

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

@ -1 +1 @@
Subproject commit 17299e413d5590b14ab0340ea477cdd86ff13daf Subproject commit a0479759d6e9ea56afa657e454193f72aef85bd0

View File

@ -1,7 +1,7 @@
{ {
"name": "code-server", "name": "code-server",
"license": "MIT", "license": "MIT",
"version": "3.4.1", "version": "3.5.0",
"description": "Run VS Code on a remote server.", "description": "Run VS Code on a remote server.",
"homepage": "https://github.com/cdr/code-server", "homepage": "https://github.com/cdr/code-server",
"bugs": { "bugs": {
@ -26,37 +26,37 @@
"lint": "./ci/dev/lint.sh", "lint": "./ci/dev/lint.sh",
"test": "./ci/dev/test.sh", "test": "./ci/dev/test.sh",
"ci": "./ci/dev/ci.sh", "ci": "./ci/dev/ci.sh",
"watch": "NODE_OPTIONS=--max_old_space_size=32384 ts-node ./ci/dev/watch.ts" "watch": "VSCODE_IPC_HOOK_CLI= NODE_OPTIONS=--max_old_space_size=32384 ts-node ./ci/dev/watch.ts"
}, },
"main": "out/node/entry.js", "main": "out/node/entry.js",
"devDependencies": { "devDependencies": {
"@types/fs-extra": "^8.0.1", "@types/fs-extra": "^8.0.1",
"@types/http-proxy": "^1.17.4", "@types/http-proxy": "^1.17.4",
"@types/js-yaml": "^3.12.3", "@types/js-yaml": "^3.12.3",
"@types/mocha": "^5.2.7", "@types/mocha": "^8.0.3",
"@types/node": "^12.12.7", "@types/node": "^12.12.7",
"@types/parcel-bundler": "^1.12.1", "@types/parcel-bundler": "^1.12.1",
"@types/pem": "^1.9.5", "@types/pem": "^1.9.5",
"@types/safe-compare": "^1.1.0", "@types/safe-compare": "^1.1.0",
"@types/semver": "^7.1.0", "@types/semver": "^7.1.0",
"@types/tar-fs": "^1.16.2", "@types/tar-fs": "^2.0.0",
"@types/tar-stream": "^1.6.1", "@types/tar-stream": "^2.1.0",
"@types/ws": "^6.0.4", "@types/ws": "^7.2.6",
"@typescript-eslint/eslint-plugin": "^2.0.0", "@typescript-eslint/eslint-plugin": "^3.10.1",
"@typescript-eslint/parser": "^2.0.0", "@typescript-eslint/parser": "^3.10.1",
"doctoc": "^1.4.0", "doctoc": "^1.4.0",
"eslint": "^6.2.0", "eslint": "^7.7.0",
"eslint-config-prettier": "^6.0.0", "eslint-config-prettier": "^6.0.0",
"eslint-plugin-import": "^2.18.2", "eslint-plugin-import": "^2.18.2",
"eslint-plugin-prettier": "^3.1.0", "eslint-plugin-prettier": "^3.1.0",
"leaked-handles": "^5.2.0", "leaked-handles": "^5.2.0",
"mocha": "^6.2.0", "mocha": "^8.1.2",
"parcel-bundler": "^1.12.4", "parcel-bundler": "^1.12.4",
"prettier": "^2.0.5", "prettier": "^2.0.5",
"stylelint": "^13.0.0", "stylelint": "^13.0.0",
"stylelint-config-recommended": "^3.0.0", "stylelint-config-recommended": "^3.0.0",
"ts-node": "^8.4.1", "ts-node": "^9.0.0",
"typescript": "3.7.2" "typescript": "4.0.2"
}, },
"resolutions": { "resolutions": {
"@types/node": "^12.12.7", "@types/node": "^12.12.7",
@ -66,13 +66,14 @@
"dependencies": { "dependencies": {
"@coder/logger": "1.1.16", "@coder/logger": "1.1.16",
"env-paths": "^2.2.0", "env-paths": "^2.2.0",
"fs-extra": "^8.1.0", "fs-extra": "^9.0.1",
"http-proxy": "^1.18.0", "http-proxy": "^1.18.0",
"httpolyglot": "^0.1.2", "httpolyglot": "^0.1.2",
"js-yaml": "^3.13.1", "js-yaml": "^3.13.1",
"limiter": "^1.1.5", "limiter": "^1.1.5",
"pem": "^1.14.2", "pem": "^1.14.2",
"rotating-file-stream": "^2.1.1", "rotating-file-stream": "^2.1.1",
"safe-buffer": "^5.1.1",
"safe-compare": "^1.1.4", "safe-compare": "^1.1.4",
"semver": "^7.1.3", "semver": "^7.1.3",
"tar": "^6.0.1", "tar": "^6.0.1",

View File

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

View File

@ -11,28 +11,22 @@
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>
<div class="center-container"> <div class="center-container">
<div class="error-display"> <div class="error-display">
<h2 class="header">{{ERROR_HEADER}}</h2> <h2 class="header">{{ERROR_HEADER}}</h2>
<div class="body"> <div class="body">{{ERROR_BODY}}</div>
{{ERROR_BODY}}
</div>
<div class="links"> <div class="links">
<a class="link" href="{{BASE}}{{TO}}">go home</a> <a class="link" href="{{BASE}}{{TO}}">go home</a>
</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>

View File

@ -11,14 +11,10 @@
content="style-src 'self'; script-src 'self' 'unsafe-inline'; manifest-src 'self'; img-src 'self' data:; font-src 'self' data:;" 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/pages/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>

View 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
}

View File

@ -2,6 +2,11 @@
<!DOCTYPE html> <!DOCTYPE html>
<html> <html>
<head> <head>
<script>
globalThis.MonacoPerformanceMarks = globalThis.MonacoPerformanceMarks || []
globalThis.MonacoPerformanceMarks.push("renderer/started", Date.now())
</script>
<meta charset="utf-8" /> <meta charset="utf-8" />
<meta <meta
@ -24,21 +29,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 +48,17 @@
<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 data-cfasync="false" src="{{CS_STATIC_BASE}}/dist/pages/vscode.js"></script>
<script data-cfasync="false" src="{{CS_STATIC_BASE}}/dist/register.js"></script>
<script data-cfasync="false" src="{{CS_STATIC_BASE}}/lib/vscode/out/vs/loader.js"></script>
<script> <script>
const parts = window.location.pathname.replace(/^\//g, "").split("/") globalThis.MonacoPerformanceMarks.push("willLoadWorkbenchMain", Date.now())
parts[parts.length - 1] = "{{BASE}}"
const url = new URL(window.location.origin + "/" + parts.join("/"))
const staticBase = url.href.replace(/\/+$/, "") + "/static/{{COMMIT}}/lib/vscode"
let nlsConfig
try {
nlsConfig = JSON.parse(document.getElementById("vscode-remote-nls-configuration").getAttribute("data-settings"))
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>
<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>

View File

@ -0,0 +1,54 @@
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(`${options.base}/vscode/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`,
recordStats: true,
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.
}

View File

@ -7,10 +7,10 @@ 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 ?? "") + "/",
}) })
.then(() => { .then(() => {
console.log("[Service Worker] registered") console.log("[Service Worker] registered")

2
src/browser/robots.txt Normal file
View File

@ -0,0 +1,2 @@
User-agent: *
Disallow: /

View File

@ -8,17 +8,6 @@ self.addEventListener("activate", (event: any) => {
event.waitUntil((self as any).clients.claim()) event.waitUntil((self as any).clients.claim())
}) })
self.addEventListener("fetch", (event: any) => { self.addEventListener("fetch", () => {
if (!navigator.onLine) { // Without this event handler we won't be recognized as a PWA.
event.respondWith(
new Promise((resolve) => {
resolve(
new Response("OFFLINE", {
status: 200,
statusText: "OK",
}),
)
}),
)
}
}) })

View File

@ -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
} }
/** /**
@ -44,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
} }
@ -72,15 +78,10 @@ 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))

21
src/node/app/health.ts Normal file
View File

@ -0,0 +1,21 @@
import { HttpProvider, HttpResponse, Heart, HttpProviderOptions } from "../http"
/**
* Check the heartbeat.
*/
export class HealthHttpProvider extends HttpProvider {
public constructor(options: HttpProviderOptions, private readonly heart: Heart) {
super(options)
}
public async handleRequest(): Promise<HttpResponse> {
return {
cache: false,
mime: "application/json",
content: {
status: this.heart.alive() ? "alive" : "expired",
lastHeartbeat: this.heart.lastHeartbeat,
},
}
}
}

View File

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

View File

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

View File

@ -45,6 +45,8 @@ export interface Args extends VsArgs {
readonly "proxy-domain"?: string[] readonly "proxy-domain"?: string[]
readonly locale?: string readonly locale?: string
readonly _: string[] readonly _: string[]
readonly "reuse-window"?: boolean
readonly "new-window"?: boolean
} }
interface Option<T> { interface Option<T> {
@ -125,11 +127,31 @@ const options: Options<Required<Args>> = {
"extra-builtin-extensions-dir": { type: "string[]", path: true }, "extra-builtin-extensions-dir": { type: "string[]", path: true },
"list-extensions": { type: "boolean", description: "List installed VS Code extensions." }, "list-extensions": { type: "boolean", description: "List installed VS Code extensions." },
force: { type: "boolean", description: "Avoid prompts when installing VS Code extensions." }, force: { type: "boolean", description: "Avoid prompts when installing VS Code extensions." },
"install-extension": { type: "string[]", description: "Install or update a VS Code extension by id or vsix." }, "install-extension": {
type: "string[]",
description:
"Install or update a VS Code extension by id or vsix. The identifier of an extension is `${publisher}.${name}`. To install a specific version provide `@${version}`. For example: 'vscode.csharp@1.2.3'.",
},
"enable-proposed-api": {
type: "string[]",
description:
"Enable proposed API features for extensions. Can receive one or more extension IDs to enable individually.",
},
"uninstall-extension": { type: "string[]", description: "Uninstall a VS Code extension by id." }, "uninstall-extension": { type: "string[]", description: "Uninstall a VS Code extension by id." },
"show-versions": { type: "boolean", description: "Show VS Code extension versions." }, "show-versions": { type: "boolean", description: "Show VS Code extension versions." },
"proxy-domain": { type: "string[]", description: "Domain used for proxying ports." }, "proxy-domain": { type: "string[]", description: "Domain used for proxying ports." },
"new-window": {
type: "boolean",
short: "n",
description: "Force to open a new window. (use with open-in)",
},
"reuse-window": {
type: "boolean",
short: "r",
description: "Force to open a file or folder in an already opened window. (use with open-in)",
},
locale: { type: "string" }, locale: { type: "string" },
log: { type: LogLevel }, log: { type: LogLevel },
verbose: { type: "boolean", short: "vvv", description: "Enable verbose logging." }, verbose: { type: "boolean", short: "vvv", description: "Enable verbose logging." },
@ -172,7 +194,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 +242,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
} }
@ -348,7 +370,7 @@ export async function readConfigFile(configPath?: string): Promise<Args> {
logger.info(`Wrote default config file to ${humanPath(configPath)}`) logger.info(`Wrote default config file to ${humanPath(configPath)}`)
} }
if (!process.env.CODE_SERVER_PARENT_PID) { if (!process.env.CODE_SERVER_PARENT_PID && !process.env.VSCODE_IPC_HOOK_CLI) {
logger.info(`Using config file ${humanPath(configPath)}`) logger.info(`Using config file ${humanPath(configPath)}`)
} }
@ -356,6 +378,9 @@ export async function readConfigFile(configPath?: string): Promise<Args> {
const config = yaml.safeLoad(configFile.toString(), { const config = yaml.safeLoad(configFile.toString(), {
filename: configPath, filename: configPath,
}) })
if (!config || typeof config === "string") {
throw new Error(`invalid config: ${config}`)
}
// We convert the config file into a set of flags. // We convert the config file into a set of flags.
// This is a temporary measure until we add a proper CLI library. // This is a temporary measure until we add a proper CLI library.

View File

@ -1,7 +1,11 @@
import { field, logger } from "@coder/logger" import { field, logger } from "@coder/logger"
import * as cp from "child_process" import * as cp from "child_process"
import { promises as fs } from "fs"
import http from "http"
import * as path from "path" import * as path from "path"
import { CliMessage } from "../../lib/vscode/src/vs/server/ipc" import { CliMessage, OpenCommandPipeArgs } from "../../lib/vscode/src/vs/server/ipc"
import { plural } from "../common/util"
import { HealthHttpProvider } from "./app/health"
import { LoginHttpProvider } from "./app/login" import { LoginHttpProvider } from "./app/login"
import { ProxyHttpProvider } from "./app/proxy" import { ProxyHttpProvider } from "./app/proxy"
import { StaticHttpProvider } from "./app/static" import { StaticHttpProvider } from "./app/static"
@ -9,9 +13,9 @@ 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 +81,9 @@ const main = async (args: Args, cliArgs: Args, configArgs: Args): Promise<void>
httpServer.registerHttpProvider("/proxy", ProxyHttpProvider) httpServer.registerHttpProvider("/proxy", ProxyHttpProvider)
httpServer.registerHttpProvider("/login", LoginHttpProvider, args.config!, envPassword) httpServer.registerHttpProvider("/login", LoginHttpProvider, args.config!, envPassword)
httpServer.registerHttpProvider("/static", StaticHttpProvider) httpServer.registerHttpProvider("/static", StaticHttpProvider)
httpServer.registerHttpProvider("/healthz", HealthHttpProvider, httpServer.heart)
await loadPlugins(httpServer, args)
ipcMain().onDispose(() => { ipcMain().onDispose(() => {
httpServer.dispose().then((errors) => { httpServer.dispose().then((errors) => {
@ -167,7 +174,7 @@ async function entry(): Promise<void> {
CODE_SERVER_PARENT_PID: process.pid.toString(), CODE_SERVER_PARENT_PID: process.pid.toString(),
}, },
}) })
vscode.once("message", (message) => { vscode.once("message", (message: any) => {
logger.debug("Got message from VS Code", field("message", message)) logger.debug("Got message from VS Code", field("message", message))
if (message.type !== "ready") { if (message.type !== "ready") {
logger.error("Unexpected response waiting for ready response") logger.error("Unexpected response waiting for ready response")
@ -181,6 +188,57 @@ async function entry(): Promise<void> {
process.exit(1) process.exit(1)
}) })
vscode.on("exit", (code) => process.exit(code || 0)) vscode.on("exit", (code) => process.exit(code || 0))
} else if (process.env.VSCODE_IPC_HOOK_CLI) {
const pipeArgs: OpenCommandPipeArgs = {
type: "open",
folderURIs: [],
forceReuseWindow: args["reuse-window"],
forceNewWindow: args["new-window"],
}
const isDir = async (path: string): Promise<boolean> => {
try {
const st = await fs.stat(path)
return st.isDirectory()
} catch (error) {
return false
}
}
for (let i = 0; i < args._.length; i++) {
const fp = path.resolve(args._[i])
if (await isDir(fp)) {
pipeArgs.folderURIs.push(fp)
} else {
if (!pipeArgs.fileURIs) {
pipeArgs.fileURIs = []
}
pipeArgs.fileURIs.push(fp)
}
}
if (pipeArgs.forceNewWindow && pipeArgs.fileURIs && pipeArgs.fileURIs.length > 0) {
logger.error("new-window can only be used with folder paths")
process.exit(1)
}
if (pipeArgs.folderURIs.length === 0 && (!pipeArgs.fileURIs || pipeArgs.fileURIs.length === 0)) {
logger.error("Please specify at least one file or folder argument")
process.exit(1)
}
const vscode = http.request(
{
path: "/",
method: "POST",
socketPath: process.env["VSCODE_IPC_HOOK_CLI"],
},
(res) => {
res.on("data", (message) => {
logger.debug("Got message from VS Code", field("message", message.toString()))
})
},
)
vscode.on("error", (err) => {
logger.debug("Got error from VS Code", field("error", err))
})
vscode.write(JSON.stringify(pipeArgs))
vscode.end()
} else { } else {
wrap(() => main(args, cliArgs, configArgs)) wrap(() => main(args, cliArgs, configArgs))
} }

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,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
} }
@ -296,7 +289,7 @@ export abstract class HttpProvider {
/** /**
* Helper to error if not authorized. * Helper to error if not authorized.
*/ */
protected ensureAuthenticated(request: http.IncomingMessage): void { public ensureAuthenticated(request: http.IncomingMessage): void {
if (!this.authenticated(request)) { if (!this.authenticated(request)) {
throw new HttpError("Unauthorized", HttpCode.Unauthorized) throw new HttpError("Unauthorized", HttpCode.Unauthorized)
} }
@ -403,23 +396,26 @@ export abstract class HttpProvider {
export class Heart { export class Heart {
private heartbeatTimer?: NodeJS.Timeout private heartbeatTimer?: NodeJS.Timeout
private heartbeatInterval = 60000 private heartbeatInterval = 60000
private lastHeartbeat = 0 public lastHeartbeat = 0
public constructor(private readonly heartbeatPath: string, private readonly isActive: () => Promise<boolean>) {} public constructor(private readonly heartbeatPath: string, private readonly isActive: () => Promise<boolean>) {}
public alive(): boolean {
const now = Date.now()
return now - this.lastHeartbeat < this.heartbeatInterval
}
/** /**
* Write to the heartbeat file if we haven't already done so within the * Write to the heartbeat file if we haven't already done so within the
* timeout and start or reset a timer that keeps running as long as there is * timeout and start or reset a timer that keeps running as long as there is
* activity. Failures are logged as warnings. * activity. Failures are logged as warnings.
*/ */
public beat(): void { public beat(): void {
const now = Date.now() if (!this.alive()) {
if (now - this.lastHeartbeat >= this.heartbeatInterval) {
logger.trace("heartbeat") logger.trace("heartbeat")
fs.outputFile(this.heartbeatPath, "").catch((error) => { fs.outputFile(this.heartbeatPath, "").catch((error) => {
logger.warn(error.message) logger.warn(error.message)
}) })
this.lastHeartbeat = now this.lastHeartbeat = Date.now()
if (typeof this.heartbeatTimer !== "undefined") { if (typeof this.heartbeatTimer !== "undefined") {
clearTimeout(this.heartbeatTimer) clearTimeout(this.heartbeatTimer)
} }
@ -464,7 +460,7 @@ export class HttpServer {
private listenPromise: Promise<string | null> | undefined private listenPromise: Promise<string | null> | undefined
public readonly protocol: "http" | "https" public readonly protocol: "http" | "https"
private readonly providers = new Map<string, HttpProvider>() private readonly providers = new Map<string, HttpProvider>()
private readonly heart: Heart public readonly heart: Heart
private readonly socketProvider = new SocketProxyProvider() private readonly socketProvider = new SocketProxyProvider()
/** /**
@ -609,8 +605,10 @@ export class HttpServer {
} }
private onRequest = async (request: http.IncomingMessage, response: http.ServerResponse): Promise<void> => { private onRequest = async (request: http.IncomingMessage, response: http.ServerResponse): Promise<void> => {
this.heart.beat()
const route = this.parseUrl(request) const route = this.parseUrl(request)
if (route.providerBase !== "/healthz") {
this.heart.beat()
}
const write = (payload: HttpResponse): void => { const write = (payload: HttpResponse): void => {
response.writeHead(payload.redirect ? HttpCode.Redirect : payload.code || HttpCode.Ok, { response.writeHead(payload.redirect ? HttpCode.Redirect : payload.code || HttpCode.Ok, {
"Content-Type": payload.mime || getMediaMime(payload.filePath), "Content-Type": payload.mime || getMediaMime(payload.filePath),
@ -649,10 +647,7 @@ export class HttpServer {
} }
try { try {
const payload = const payload = (await this.handleRequest(route, request)) || (await route.provider.handleRequest(route, request))
this.maybeRedirect(request, route) ||
(route.provider.authenticated(request) && this.maybeProxy(request)) ||
(await route.provider.handleRequest(route, request))
if (payload.proxy) { if (payload.proxy) {
this.doProxy(route, request, response, payload.proxy) this.doProxy(route, request, response, payload.proxy)
} else { } else {
@ -664,7 +659,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)
} }
@ -687,15 +682,23 @@ export class HttpServer {
} }
/** /**
* Return any necessary redirection before delegating to a provider. * Handle requests that are always in effect no matter what provider is
* registered at the route.
*/ */
private maybeRedirect(request: http.IncomingMessage, route: ProviderRoute): RedirectResponse | undefined { private async handleRequest(route: ProviderRoute, request: http.IncomingMessage): Promise<HttpResponse | undefined> {
// If we're handling TLS ensure all requests are redirected to HTTPS. // If we're handling TLS ensure all requests are redirected to HTTPS.
if (this.options.cert && !(request.connection as tls.TLSSocket).encrypted) { if (this.options.cert && !(request.connection as tls.TLSSocket).encrypted) {
return { redirect: route.fullPath } return { redirect: route.fullPath }
} }
return undefined // Return robots.txt.
if (route.fullPath === "/robots.txt") {
const filePath = path.resolve(__dirname, "../../src/browser/robots.txt")
return { content: await fs.readFile(filePath), filePath }
}
// Handle proxy domains.
return this.maybeProxy(route, request)
} }
/** /**
@ -746,7 +749,7 @@ export class HttpServer {
// can't be transferred so we need an in-between). // can't be transferred so we need an in-between).
const socketProxy = await this.socketProvider.createProxy(socket) const socketProxy = await this.socketProvider.createProxy(socket)
const payload = const payload =
this.maybeProxy(request) || (await route.provider.handleWebSocket(route, request, socketProxy, head)) this.maybeProxy(route, request) || (await route.provider.handleWebSocket(route, request, socketProxy, head))
if (payload && payload.proxy) { if (payload && payload.proxy) {
this.doProxy(route, request, { socket: socketProxy, head }, payload.proxy) this.doProxy(route, request, { socket: socketProxy, head }, payload.proxy)
} }
@ -875,6 +878,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 +888,7 @@ export class HttpServer {
} }
}) })
logger.debug("got cookie doman", field("host", host))
return host ? `Domain=${host}` : undefined return host ? `Domain=${host}` : undefined
} }
@ -894,8 +899,10 @@ export class HttpServer {
* *
* For example if `coder.com` is specified `8080.coder.com` will be proxied * For example if `coder.com` is specified `8080.coder.com` will be proxied
* but `8080.test.coder.com` and `test.8080.coder.com` will not. * but `8080.test.coder.com` and `test.8080.coder.com` will not.
*
* Throw an error if proxying but the user isn't authenticated.
*/ */
public maybeProxy(request: http.IncomingMessage): HttpResponse | undefined { public maybeProxy(route: ProviderRoute, request: http.IncomingMessage): HttpResponse | undefined {
// Split into parts. // Split into parts.
const host = request.headers.host || "" const host = request.headers.host || ""
const idx = host.indexOf(":") const idx = host.indexOf(":")
@ -909,6 +916,9 @@ export class HttpServer {
return undefined return undefined
} }
// Must be authenticated to use the proxy.
route.provider.ensureAuthenticated(request)
return { return {
proxy: { proxy: {
port, port,

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

@ -0,0 +1,60 @@
import { field, logger } from "@coder/logger"
import * as fs from "fs"
import * as path from "path"
import * as util from "util"
import { Args } from "./cli"
import { HttpServer } from "./http"
/* eslint-disable @typescript-eslint/no-var-requires */
export type Activate = (httpServer: HttpServer, args: Args) => void
export interface Plugin {
activate: Activate
}
/**
* Intercept imports so we can inject code-server when the plugin tries to
* import it.
*/
const originalLoad = require("module")._load
// eslint-disable-next-line @typescript-eslint/no-explicit-any
require("module")._load = function (request: string, parent: object, isMain: boolean): any {
return originalLoad.apply(this, [request.replace(/^code-server/, path.resolve(__dirname, "../..")), parent, isMain])
}
const loadPlugin = async (pluginPath: string, httpServer: HttpServer, args: Args): Promise<void> => {
try {
const plugin: Plugin = require(pluginPath)
plugin.activate(httpServer, args)
logger.debug("Loaded plugin", field("name", path.basename(pluginPath)))
} catch (error) {
if (error.code !== "MODULE_NOT_FOUND") {
logger.warn(error.message)
} else {
logger.error(error.message)
}
}
}
const _loadPlugins = async (httpServer: HttpServer, args: Args): Promise<void> => {
const pluginPath = path.resolve(__dirname, "../../plugins")
const files = await util.promisify(fs.readdir)(pluginPath, {
withFileTypes: true,
})
await Promise.all(files.map((file) => loadPlugin(path.join(pluginPath, file.name), httpServer, args)))
}
export const loadPlugins = async (httpServer: HttpServer, args: Args): Promise<void> => {
try {
await _loadPlugins(httpServer, args)
} catch (error) {
if (error.code !== "ENOENT") {
logger.warn(error.message)
}
}
if (process.env.PLUGIN_DIR) {
await loadPlugin(process.env.PLUGIN_DIR, httpServer, args)
}
}

View File

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

View File

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

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 exit: (code?: number) => never public readonly processExit: (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.exit = process.exit this.processExit = 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,6 +71,14 @@ 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
@ -161,28 +169,37 @@ export class WrapperProcess {
} }
}) })
ipcMain().onMessage(async (message) => { ipcMain().onMessage((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.started = undefined this.relaunch()
if (this.process) {
this.process.removeAllListeners()
this.process.kill()
}
try {
await this.start()
} catch (error) {
logger.error(error.message)
ipcMain().exit(typeof error.code === "number" ? error.code : 1)
}
break break
default: default:
logger.error(`Unrecognized message ${message}`) logger.error(`Unrecognized message ${message}`)
break break
} }
}) })
process.on("SIGUSR1", async () => {
logger.info("Received SIGUSR1; hotswapping")
this.relaunch()
})
}
private async relaunch(): Promise<void> {
this.started = undefined
if (this.process) {
this.process.removeAllListeners()
this.process.kill()
}
try {
await this.start()
} catch (error) {
logger.error(error.message)
ipcMain().exit(typeof error.code === "number" ? error.code : 1)
}
} }
public start(): Promise<void> { public start(): Promise<void> {
@ -244,13 +261,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(typeof error.code === "number" ? error.code : 1) ipcMain().exit(error)
}) })
} 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(typeof error.code === "number" ? error.code : 1) ipcMain().exit(error)
}) })
} }
} }

View File

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

View File

@ -8,6 +8,7 @@ import { SettingsProvider, UpdateSettings } from "../src/node/settings"
import { tmpdir } from "../src/node/util" import { tmpdir } from "../src/node/util"
describe("update", () => { describe("update", () => {
return
let version = "1.0.0" let version = "1.0.0"
let spy: string[] = [] let spy: string[] = []
const server = http.createServer((request: http.IncomingMessage, response: http.ServerResponse) => { const server = http.createServer((request: http.IncomingMessage, response: http.ServerResponse) => {

View File

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

View File

@ -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"]
} }

2940
yarn.lock

File diff suppressed because it is too large Load Diff