mirror of https://github.com/coder/code-server.git
Merge pull request #2 from cdr/master
merge: remote master to local master
This commit is contained in:
commit
b9b9534999
|
@ -19,6 +19,9 @@ extends:
|
||||||
- prettier/@typescript-eslint # Remove conflicts again.
|
- prettier/@typescript-eslint # Remove conflicts again.
|
||||||
|
|
||||||
rules:
|
rules:
|
||||||
|
# Sometimes you need to add args to implement a function signature even
|
||||||
|
# if they are unused.
|
||||||
|
"@typescript-eslint/no-unused-vars": ["error", { "args": "none" }]
|
||||||
# For overloads.
|
# For overloads.
|
||||||
no-dupe-class-members: off
|
no-dupe-class-members: off
|
||||||
"@typescript-eslint/no-use-before-define": off
|
"@typescript-eslint/no-use-before-define": off
|
||||||
|
@ -30,6 +33,9 @@ rules:
|
||||||
eqeqeq: error
|
eqeqeq: error
|
||||||
import/order:
|
import/order:
|
||||||
[error, { alphabetize: { order: "asc" }, groups: [["builtin", "external", "internal"], "parent", "sibling"] }]
|
[error, { alphabetize: { order: "asc" }, groups: [["builtin", "external", "internal"], "parent", "sibling"] }]
|
||||||
|
no-async-promise-executor: off
|
||||||
|
# This isn't a real module, just types, which apparently doesn't resolve.
|
||||||
|
import/no-unresolved: [error, { ignore: ["express-serve-static-core"] }]
|
||||||
|
|
||||||
settings:
|
settings:
|
||||||
# Does not work with CommonJS unfortunately.
|
# Does not work with CommonJS unfortunately.
|
||||||
|
|
|
@ -1 +1,3 @@
|
||||||
* @code-asher @nhooyr
|
* @code-asher @nhooyr
|
||||||
|
|
||||||
|
ci/helm-chart @Matthew-Beckett @alexgorbatchev
|
||||||
|
|
|
@ -2,4 +2,7 @@ blank_issues_enabled: false
|
||||||
contact_links:
|
contact_links:
|
||||||
- name: Question
|
- name: Question
|
||||||
url: https://github.com/cdr/code-server/discussions/new?category_id=22503114
|
url: https://github.com/cdr/code-server/discussions/new?category_id=22503114
|
||||||
about: Ask the community for help
|
about: Ask the community for help on our GitHub Discussions board
|
||||||
|
- name: Chat
|
||||||
|
about: Need immediate help or just want to talk? Hop in our Slack
|
||||||
|
url: https://cdr.co/join-community
|
||||||
|
|
|
@ -8,7 +8,7 @@ jobs:
|
||||||
steps:
|
steps:
|
||||||
- uses: actions/checkout@v1
|
- uses: actions/checkout@v1
|
||||||
- name: Run ./ci/steps/fmt.sh
|
- name: Run ./ci/steps/fmt.sh
|
||||||
uses: ./ci/images/debian8
|
uses: ./ci/images/debian10
|
||||||
with:
|
with:
|
||||||
args: ./ci/steps/fmt.sh
|
args: ./ci/steps/fmt.sh
|
||||||
|
|
||||||
|
@ -17,7 +17,7 @@ jobs:
|
||||||
steps:
|
steps:
|
||||||
- uses: actions/checkout@v1
|
- uses: actions/checkout@v1
|
||||||
- name: Run ./ci/steps/lint.sh
|
- name: Run ./ci/steps/lint.sh
|
||||||
uses: ./ci/images/debian8
|
uses: ./ci/images/debian10
|
||||||
with:
|
with:
|
||||||
args: ./ci/steps/lint.sh
|
args: ./ci/steps/lint.sh
|
||||||
|
|
||||||
|
@ -26,7 +26,7 @@ jobs:
|
||||||
steps:
|
steps:
|
||||||
- uses: actions/checkout@v1
|
- uses: actions/checkout@v1
|
||||||
- name: Run ./ci/steps/test.sh
|
- name: Run ./ci/steps/test.sh
|
||||||
uses: ./ci/images/debian8
|
uses: ./ci/images/debian10
|
||||||
with:
|
with:
|
||||||
args: ./ci/steps/test.sh
|
args: ./ci/steps/test.sh
|
||||||
|
|
||||||
|
@ -35,7 +35,7 @@ jobs:
|
||||||
steps:
|
steps:
|
||||||
- uses: actions/checkout@v1
|
- uses: actions/checkout@v1
|
||||||
- name: Run ./ci/steps/release.sh
|
- name: Run ./ci/steps/release.sh
|
||||||
uses: ./ci/images/debian8
|
uses: ./ci/images/debian10
|
||||||
with:
|
with:
|
||||||
args: ./ci/steps/release.sh
|
args: ./ci/steps/release.sh
|
||||||
- name: Upload npm package artifact
|
- name: Upload npm package artifact
|
||||||
|
@ -116,7 +116,7 @@ jobs:
|
||||||
name: release-packages
|
name: release-packages
|
||||||
path: ./release-packages
|
path: ./release-packages
|
||||||
- name: Run ./ci/steps/build-docker-image.sh
|
- name: Run ./ci/steps/build-docker-image.sh
|
||||||
uses: ./ci/images/debian8
|
uses: ./ci/images/debian10
|
||||||
with:
|
with:
|
||||||
args: ./ci/steps/build-docker-image.sh
|
args: ./ci/steps/build-docker-image.sh
|
||||||
- name: Upload release image
|
- name: Upload release image
|
||||||
|
|
|
@ -10,7 +10,7 @@ jobs:
|
||||||
steps:
|
steps:
|
||||||
- uses: actions/checkout@v1
|
- uses: actions/checkout@v1
|
||||||
- name: Run ./ci/steps/publish-npm.sh
|
- name: Run ./ci/steps/publish-npm.sh
|
||||||
uses: ./ci/images/debian8
|
uses: ./ci/images/debian10
|
||||||
with:
|
with:
|
||||||
args: ./ci/steps/publish-npm.sh
|
args: ./ci/steps/publish-npm.sh
|
||||||
env:
|
env:
|
||||||
|
@ -22,7 +22,7 @@ jobs:
|
||||||
steps:
|
steps:
|
||||||
- uses: actions/checkout@v1
|
- uses: actions/checkout@v1
|
||||||
- name: Run ./ci/steps/push-docker-manifest.sh
|
- name: Run ./ci/steps/push-docker-manifest.sh
|
||||||
uses: ./ci/images/debian8
|
uses: ./ci/images/debian10
|
||||||
with:
|
with:
|
||||||
args: ./ci/steps/push-docker-manifest.sh
|
args: ./ci/steps/push-docker-manifest.sh
|
||||||
env:
|
env:
|
||||||
|
|
|
@ -11,3 +11,5 @@ release-images/
|
||||||
node_modules
|
node_modules
|
||||||
node-*
|
node-*
|
||||||
/plugins
|
/plugins
|
||||||
|
/lib/coder-cloud-agent
|
||||||
|
.home
|
||||||
|
|
|
@ -1,3 +1,4 @@
|
||||||
[submodule "lib/vscode"]
|
[submodule "lib/vscode"]
|
||||||
path = lib/vscode
|
path = lib/vscode
|
||||||
url = https://github.com/microsoft/vscode
|
url = https://github.com/microsoft/vscode
|
||||||
|
ignore = dirty
|
||||||
|
|
56
README.md
56
README.md
|
@ -1,4 +1,4 @@
|
||||||
# code-server
|
# code-server · [!["GitHub Discussions"](https://img.shields.io/badge/%20GitHub-%20Discussions-gray.svg?longCache=true&logo=github&colorB=purple)](https://github.com/cdr/code-server/discussions) [!["Join us on Slack"](https://img.shields.io/badge/join-us%20on%20slack-gray.svg?longCache=true&logo=slack&colorB=brightgreen)](https://cdr.co/join-community) [![Twitter Follow](https://img.shields.io/twitter/follow/CoderHQ?label=%40CoderHQ&style=social)](https://twitter.com/coderhq)
|
||||||
|
|
||||||
Run [VS Code](https://github.com/Microsoft/vscode) on any machine anywhere and access it in the browser.
|
Run [VS Code](https://github.com/Microsoft/vscode) on any machine anywhere and access it in the browser.
|
||||||
|
|
||||||
|
@ -6,62 +6,64 @@ Run [VS Code](https://github.com/Microsoft/vscode) on any machine anywhere and a
|
||||||
|
|
||||||
## Highlights
|
## Highlights
|
||||||
|
|
||||||
- **Code everywhere**
|
- Code on any device with a consistent development environment
|
||||||
- Code on your Chromebook, tablet, and laptop with a consistent development environment.
|
- Use cloud servers to speed up tests, compilations, downloads, and more
|
||||||
- Develop on a Linux machine and pick up from any device with a web browser.
|
- Preserve battery life when you're on the go; all intensive tasks run on your server
|
||||||
- **Server-powered**
|
|
||||||
- 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 run on your server.
|
|
||||||
- Make use of a spare computer you have lying around and turn it into a full development environment.
|
|
||||||
|
|
||||||
## Getting Started
|
## Getting Started
|
||||||
|
|
||||||
For a full setup and walkthrough, please see [./doc/guide.md](./doc/guide.md).
|
There are two ways to get started:
|
||||||
|
|
||||||
### Quick Install
|
1. Using the [install script](./install.sh), which automates most of the process. The script uses the system package manager (if possible)
|
||||||
|
2. Manually installing code-server; see [Installation](./doc/install.md) for instructions applicable to most use cases
|
||||||
|
|
||||||
We have a [script](./install.sh) to install code-server for Linux, macOS and FreeBSD.
|
If you choose to use the install script, you can preview what occurs during the install process:
|
||||||
|
|
||||||
It tries to use the system package manager if possible.
|
|
||||||
|
|
||||||
First run to print out the install process:
|
|
||||||
|
|
||||||
```bash
|
```bash
|
||||||
curl -fsSL https://code-server.dev/install.sh | sh -s -- --dry-run
|
curl -fsSL https://code-server.dev/install.sh | sh -s -- --dry-run
|
||||||
```
|
```
|
||||||
|
|
||||||
Now to actually install:
|
To install, run:
|
||||||
|
|
||||||
```bash
|
```bash
|
||||||
curl -fsSL https://code-server.dev/install.sh | sh
|
curl -fsSL https://code-server.dev/install.sh | sh
|
||||||
```
|
```
|
||||||
|
|
||||||
The install script will print out how to run and start using code-server.
|
When done, the install script prints out instructions for running and starting code-server.
|
||||||
|
|
||||||
### Manual Install
|
We also have an in-depth [setup and configuration](./doc/guide.md) guide.
|
||||||
|
|
||||||
Docs on the install script, manual installation and docker image are at [./doc/install.md](./doc/install.md).
|
### Alpha Program 🐣
|
||||||
|
|
||||||
|
We're working on a cloud platform that makes deploying and managing code-server easier.
|
||||||
|
Consider updating to the latest version and running code-server with our experimental flag `--link` if you don't want to worry about
|
||||||
|
|
||||||
|
- TLS
|
||||||
|
- Authentication
|
||||||
|
- Port Forwarding
|
||||||
|
|
||||||
|
```bash
|
||||||
|
$ code-server --link
|
||||||
|
Proxying code-server to Coder Cloud, you can access your IDE at https://valmar-jon.cdr.co
|
||||||
|
```
|
||||||
|
|
||||||
## FAQ
|
## FAQ
|
||||||
|
|
||||||
See [./doc/FAQ.md](./doc/FAQ.md).
|
See [./doc/FAQ.md](./doc/FAQ.md).
|
||||||
|
|
||||||
## Contributing
|
## Want to help?
|
||||||
|
|
||||||
See [./doc/CONTRIBUTING.md](./doc/CONTRIBUTING.md).
|
See [CONTRIBUTING](./doc/CONTRIBUTING.md) for details.
|
||||||
|
|
||||||
## Hiring
|
## Hiring
|
||||||
|
|
||||||
We ([@cdr](https://github.com/cdr)) are looking for 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](https://jobs.lever.co/coder/e40becde-2cbd-4885-9029-e5c7b0a734b8), 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
|
||||||
you're in North America or Europe.
|
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
|
||||||
|
|
||||||
|
|
|
@ -17,6 +17,8 @@ Make sure you have `$GITHUB_TOKEN` set and [hub](https://github.com/github/hub)
|
||||||
1. Update the version of code-server and make a PR.
|
1. Update the version of code-server and make a PR.
|
||||||
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)
|
||||||
|
3. Update in [./ci/helm-chart/README.md](../ci/helm-chart/README.md)
|
||||||
|
- Remember to update the chart version as well on top of appVersion in `Chart.yaml`.
|
||||||
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.
|
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
|
||||||
|
|
|
@ -18,6 +18,12 @@ main() {
|
||||||
chmod +x out/node/entry.js
|
chmod +x out/node/entry.js
|
||||||
fi
|
fi
|
||||||
|
|
||||||
|
if ! [ -f ./lib/coder-cloud-agent ]; then
|
||||||
|
OS="$(uname | tr '[:upper:]' '[:lower:]')"
|
||||||
|
curl -fsSL "https://storage.googleapis.com/coder-cloud-releases/agent/latest/$OS/cloud-agent" -o ./lib/coder-cloud-agent
|
||||||
|
chmod +x ./lib/coder-cloud-agent
|
||||||
|
fi
|
||||||
|
|
||||||
parcel build \
|
parcel build \
|
||||||
--public-url "." \
|
--public-url "." \
|
||||||
--out-dir dist \
|
--out-dir dist \
|
||||||
|
|
|
@ -11,15 +11,6 @@ main() {
|
||||||
mkdir -p release-packages
|
mkdir -p release-packages
|
||||||
|
|
||||||
release_archive
|
release_archive
|
||||||
# Will stop the auto update issues and allow people to upgrade their scripts
|
|
||||||
# for the new release structure.
|
|
||||||
if [[ $ARCH == "amd64" ]]; then
|
|
||||||
if [[ $OS == "linux" ]]; then
|
|
||||||
ARCH=x86_64 release_archive
|
|
||||||
elif [[ $OS == "macos" ]]; then
|
|
||||||
OS=darwin ARCH=x86_64 release_archive
|
|
||||||
fi
|
|
||||||
fi
|
|
||||||
|
|
||||||
if [[ $OS == "linux" ]]; then
|
if [[ $OS == "linux" ]]; then
|
||||||
release_nfpm
|
release_nfpm
|
||||||
|
@ -30,12 +21,6 @@ release_archive() {
|
||||||
local release_name="code-server-$VERSION-$OS-$ARCH"
|
local release_name="code-server-$VERSION-$OS-$ARCH"
|
||||||
if [[ $OS == "linux" ]]; then
|
if [[ $OS == "linux" ]]; then
|
||||||
tar -czf "release-packages/$release_name.tar.gz" --transform "s/^\.\/release-standalone/$release_name/" ./release-standalone
|
tar -czf "release-packages/$release_name.tar.gz" --transform "s/^\.\/release-standalone/$release_name/" ./release-standalone
|
||||||
elif [[ $OS == "darwin" && $ARCH == "x86_64" ]]; then
|
|
||||||
# Just exists to make autoupdating from 3.2.0 work again.
|
|
||||||
mv ./release-standalone "./$release_name"
|
|
||||||
zip -r "release-packages/$release_name.zip" "./$release_name"
|
|
||||||
mv "./$release_name" ./release-standalone
|
|
||||||
return
|
|
||||||
else
|
else
|
||||||
tar -czf "release-packages/$release_name.tar.gz" -s "/^release-standalone/$release_name/" release-standalone
|
tar -czf "release-packages/$release_name.tar.gz" -s "/^release-standalone/$release_name/" release-standalone
|
||||||
fi
|
fi
|
||||||
|
|
|
@ -6,6 +6,10 @@ set -euo pipefail
|
||||||
# MINIFY controls whether minified vscode is bundled.
|
# MINIFY controls whether minified vscode is bundled.
|
||||||
MINIFY="${MINIFY-true}"
|
MINIFY="${MINIFY-true}"
|
||||||
|
|
||||||
|
# KEEP_MODULES controls whether the script cleans all node_modules requiring a yarn install
|
||||||
|
# to run first.
|
||||||
|
KEEP_MODULES="${KEEP_MODULES-0}"
|
||||||
|
|
||||||
main() {
|
main() {
|
||||||
cd "$(dirname "${0}")/../.."
|
cd "$(dirname "${0}")/../.."
|
||||||
source ./ci/lib.sh
|
source ./ci/lib.sh
|
||||||
|
@ -52,15 +56,25 @@ EOF
|
||||||
) > "$RELEASE_PATH/package.json"
|
) > "$RELEASE_PATH/package.json"
|
||||||
rsync yarn.lock "$RELEASE_PATH"
|
rsync yarn.lock "$RELEASE_PATH"
|
||||||
rsync ci/build/npm-postinstall.sh "$RELEASE_PATH/postinstall.sh"
|
rsync ci/build/npm-postinstall.sh "$RELEASE_PATH/postinstall.sh"
|
||||||
|
|
||||||
|
if [ "$KEEP_MODULES" = 1 ]; then
|
||||||
|
rsync node_modules/ "$RELEASE_PATH/node_modules"
|
||||||
|
mkdir -p "$RELEASE_PATH/lib"
|
||||||
|
rsync ./lib/coder-cloud-agent "$RELEASE_PATH/lib"
|
||||||
|
fi
|
||||||
}
|
}
|
||||||
|
|
||||||
bundle_vscode() {
|
bundle_vscode() {
|
||||||
mkdir -p "$VSCODE_OUT_PATH"
|
mkdir -p "$VSCODE_OUT_PATH"
|
||||||
rsync "$VSCODE_SRC_PATH/yarn.lock" "$VSCODE_OUT_PATH"
|
rsync "$VSCODE_SRC_PATH/yarn.lock" "$VSCODE_OUT_PATH"
|
||||||
rsync "$VSCODE_SRC_PATH/out-vscode${MINIFY+-min}/" "$VSCODE_OUT_PATH/out"
|
rsync "$VSCODE_SRC_PATH/out-vscode${MINIFY:+-min}/" "$VSCODE_OUT_PATH/out"
|
||||||
|
|
||||||
rsync "$VSCODE_SRC_PATH/.build/extensions/" "$VSCODE_OUT_PATH/extensions"
|
rsync "$VSCODE_SRC_PATH/.build/extensions/" "$VSCODE_OUT_PATH/extensions"
|
||||||
|
if [ "$KEEP_MODULES" = 0 ]; then
|
||||||
rm -Rf "$VSCODE_OUT_PATH/extensions/node_modules"
|
rm -Rf "$VSCODE_OUT_PATH/extensions/node_modules"
|
||||||
|
else
|
||||||
|
rsync "$VSCODE_SRC_PATH/node_modules/" "$VSCODE_OUT_PATH/node_modules"
|
||||||
|
fi
|
||||||
rsync "$VSCODE_SRC_PATH/extensions/package.json" "$VSCODE_OUT_PATH/extensions"
|
rsync "$VSCODE_SRC_PATH/extensions/package.json" "$VSCODE_OUT_PATH/extensions"
|
||||||
rsync "$VSCODE_SRC_PATH/extensions/yarn.lock" "$VSCODE_OUT_PATH/extensions"
|
rsync "$VSCODE_SRC_PATH/extensions/yarn.lock" "$VSCODE_OUT_PATH/extensions"
|
||||||
rsync "$VSCODE_SRC_PATH/extensions/postinstall.js" "$VSCODE_OUT_PATH/extensions"
|
rsync "$VSCODE_SRC_PATH/extensions/postinstall.js" "$VSCODE_OUT_PATH/extensions"
|
||||||
|
|
|
@ -5,16 +5,7 @@ main() {
|
||||||
cd "$(dirname "${0}")/../.."
|
cd "$(dirname "${0}")/../.."
|
||||||
source ./ci/lib.sh
|
source ./ci/lib.sh
|
||||||
|
|
||||||
rm -rf \
|
git clean -Xffd
|
||||||
out \
|
|
||||||
release \
|
|
||||||
release-standalone \
|
|
||||||
release-packages \
|
|
||||||
release-gcp \
|
|
||||||
release-images \
|
|
||||||
dist \
|
|
||||||
.cache \
|
|
||||||
node-*
|
|
||||||
|
|
||||||
pushd lib/vscode
|
pushd lib/vscode
|
||||||
git clean -xffd
|
git clean -xffd
|
||||||
|
|
|
@ -24,6 +24,13 @@ main() {
|
||||||
;;
|
;;
|
||||||
esac
|
esac
|
||||||
|
|
||||||
|
OS="$(uname | tr '[:upper:]' '[:lower:]')"
|
||||||
|
if curl -fsSL "https://storage.googleapis.com/coder-cloud-releases/agent/latest/$OS/cloud-agent" -o ./lib/coder-cloud-agent; then
|
||||||
|
chmod +x ./lib/coder-cloud-agent
|
||||||
|
else
|
||||||
|
echo "Failed to download cloud agent; --link will not work"
|
||||||
|
fi
|
||||||
|
|
||||||
if ! vscode_yarn; then
|
if ! vscode_yarn; then
|
||||||
echo "You may not have the required dependencies to build the native modules."
|
echo "You may not have the required dependencies to build the native modules."
|
||||||
echo "Please see https://github.com/cdr/code-server/blob/master/doc/npm.md"
|
echo "Please see https://github.com/cdr/code-server/blob/master/doc/npm.md"
|
||||||
|
@ -36,6 +43,13 @@ vscode_yarn() {
|
||||||
yarn --production --frozen-lockfile
|
yarn --production --frozen-lockfile
|
||||||
cd extensions
|
cd extensions
|
||||||
yarn --production --frozen-lockfile
|
yarn --production --frozen-lockfile
|
||||||
|
for ext in */; do
|
||||||
|
ext="${ext%/}"
|
||||||
|
echo "extensions/$ext: installing dependencies"
|
||||||
|
cd "$ext"
|
||||||
|
yarn --production --frozen-lockfile
|
||||||
|
cd "$OLDPWD"
|
||||||
|
done
|
||||||
}
|
}
|
||||||
|
|
||||||
main "$@"
|
main "$@"
|
||||||
|
|
|
@ -11,7 +11,7 @@ main() {
|
||||||
source ./ci/lib.sh
|
source ./ci/lib.sh
|
||||||
|
|
||||||
download_artifact release-packages ./release-packages
|
download_artifact release-packages ./release-packages
|
||||||
local assets=(./release-packages/code-server*"$VERSION"*{.tar.gz,.zip,.deb,.rpm})
|
local assets=(./release-packages/code-server*"$VERSION"*{.tar.gz,.deb,.rpm})
|
||||||
for i in "${!assets[@]}"; do
|
for i in "${!assets[@]}"; do
|
||||||
assets[$i]="--attach=${assets[$i]}"
|
assets[$i]="--attach=${assets[$i]}"
|
||||||
done
|
done
|
||||||
|
|
|
@ -15,7 +15,17 @@ v$VERSION
|
||||||
|
|
||||||
VS Code v$(vscode_version)
|
VS Code v$(vscode_version)
|
||||||
|
|
||||||
- Summarize changes here with references to issues
|
Upgrading is as easy as installing the new version over the old one. code-server
|
||||||
|
maintains all user data in \`~/.local/share/code-server\` so that it is preserved in between
|
||||||
|
installations.
|
||||||
|
|
||||||
|
## New Features
|
||||||
|
- ⭐ Summarize new features here with references to issues
|
||||||
|
|
||||||
|
## Bug Fixes
|
||||||
|
- ⭐ Summarize bug fixes here with references to issues
|
||||||
|
|
||||||
|
Cheers! 🍻
|
||||||
EOF
|
EOF
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|
|
@ -15,8 +15,8 @@ main() {
|
||||||
./release-standalone/bin/code-server --extensions-dir "$EXTENSIONS_DIR" --install-extension ms-python.python
|
./release-standalone/bin/code-server --extensions-dir "$EXTENSIONS_DIR" --install-extension ms-python.python
|
||||||
local installed_extensions
|
local installed_extensions
|
||||||
installed_extensions="$(./release-standalone/bin/code-server --extensions-dir "$EXTENSIONS_DIR" --list-extensions 2>&1)"
|
installed_extensions="$(./release-standalone/bin/code-server --extensions-dir "$EXTENSIONS_DIR" --list-extensions 2>&1)"
|
||||||
if [[ $installed_extensions != *"info Using config file ~/.config/code-server/config.yaml
|
# We use grep as ms-python.python may have dependency extensions that change.
|
||||||
ms-python.python" ]]; then
|
if ! echo "$installed_extensions" | grep -q "ms-python.python"; then
|
||||||
echo "Unexpected output from listing extensions:"
|
echo "Unexpected output from listing extensions:"
|
||||||
echo "$installed_extensions"
|
echo "$installed_extensions"
|
||||||
exit 1
|
exit 1
|
||||||
|
|
|
@ -19,13 +19,16 @@ main() {
|
||||||
"*.yaml"
|
"*.yaml"
|
||||||
"*.yml"
|
"*.yml"
|
||||||
)
|
)
|
||||||
prettier --write --loglevel=warn $(git ls-files "${prettierExts[@]}")
|
prettier --write --loglevel=warn $(
|
||||||
|
git ls-files "${prettierExts[@]}" | grep -v 'helm-chart'
|
||||||
|
)
|
||||||
|
|
||||||
doctoc --title '# FAQ' doc/FAQ.md > /dev/null
|
doctoc --title '# FAQ' doc/FAQ.md > /dev/null
|
||||||
doctoc --title '# Setup Guide' doc/guide.md > /dev/null
|
doctoc --title '# Setup Guide' doc/guide.md > /dev/null
|
||||||
doctoc --title '# Install' doc/install.md > /dev/null
|
doctoc --title '# Install' doc/install.md > /dev/null
|
||||||
doctoc --title '# npm Install Requirements' doc/npm.md > /dev/null
|
doctoc --title '# npm Install Requirements' doc/npm.md > /dev/null
|
||||||
doctoc --title '# Contributing' doc/CONTRIBUTING.md > /dev/null
|
doctoc --title '# Contributing' doc/CONTRIBUTING.md > /dev/null
|
||||||
|
doctoc --title '# iPad' doc/ipad.md > /dev/null
|
||||||
|
|
||||||
if [[ ${CI-} && $(git ls-files --other --modified --exclude-standard) ]]; then
|
if [[ ${CI-} && $(git ls-files --other --modified --exclude-standard) ]]; then
|
||||||
echo "Files need generation or are formatted incorrectly:"
|
echo "Files need generation or are formatted incorrectly:"
|
||||||
|
|
|
@ -4,16 +4,22 @@ set -euo pipefail
|
||||||
main() {
|
main() {
|
||||||
cd "$(dirname "$0")/../../.."
|
cd "$(dirname "$0")/../../.."
|
||||||
source ./ci/lib.sh
|
source ./ci/lib.sh
|
||||||
|
mkdir -p .home
|
||||||
|
|
||||||
docker run \
|
docker run \
|
||||||
-it \
|
-it \
|
||||||
--rm \
|
--rm \
|
||||||
-v "$PWD:/src" \
|
-v "$PWD:/src" \
|
||||||
|
-e HOME="/src/.home" \
|
||||||
|
-e USER="coder" \
|
||||||
|
-e GITHUB_TOKEN \
|
||||||
|
-e KEEP_MODULES \
|
||||||
|
-e MINIFY \
|
||||||
-w /src \
|
-w /src \
|
||||||
-p 127.0.0.1:8080:8080 \
|
-p 127.0.0.1:8080:8080 \
|
||||||
-u "$(id -u):$(id -g)" \
|
-u "$(id -u):$(id -g)" \
|
||||||
-e CI \
|
-e CI \
|
||||||
"$(docker_build ./ci/images/debian8)" \
|
"$(docker_build ./ci/images/"${IMAGE-debian10}")" \
|
||||||
"$@"
|
"$@"
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|
|
@ -7,9 +7,9 @@ main() {
|
||||||
eslint --max-warnings=0 --fix $(git ls-files "*.ts" "*.tsx" "*.js")
|
eslint --max-warnings=0 --fix $(git ls-files "*.ts" "*.tsx" "*.js")
|
||||||
stylelint $(git ls-files "*.css")
|
stylelint $(git ls-files "*.css")
|
||||||
tsc --noEmit
|
tsc --noEmit
|
||||||
# See comment in ./ci/image/debian8
|
|
||||||
if [[ ! ${CI-} ]]; then
|
|
||||||
shellcheck -e SC2046,SC2164,SC2154,SC1091,SC1090,SC2002 $(git ls-files "*.sh")
|
shellcheck -e SC2046,SC2164,SC2154,SC1091,SC1090,SC2002 $(git ls-files "*.sh")
|
||||||
|
if command -v helm && helm kubeval --help > /dev/null; then
|
||||||
|
helm kubeval ci/helm-chart
|
||||||
fi
|
fi
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|
|
@ -4,7 +4,10 @@ set -euo pipefail
|
||||||
main() {
|
main() {
|
||||||
cd "$(dirname "$0")/../.."
|
cd "$(dirname "$0")/../.."
|
||||||
|
|
||||||
mocha -r ts-node/register ./test/*.test.ts
|
cd test/test-plugin
|
||||||
|
make -s out/index.js
|
||||||
|
cd "$OLDPWD"
|
||||||
|
mocha -r ts-node/register ./test/*.test.ts "$@"
|
||||||
}
|
}
|
||||||
|
|
||||||
main "$@"
|
main "$@"
|
||||||
|
|
1415
ci/dev/vscode.patch
1415
ci/dev/vscode.patch
File diff suppressed because it is too large
Load Diff
|
@ -0,0 +1,23 @@
|
||||||
|
# Patterns to ignore when building packages.
|
||||||
|
# This supports shell glob matching, relative path matching, and
|
||||||
|
# negation (prefixed with !). Only one pattern per line.
|
||||||
|
.DS_Store
|
||||||
|
# Common VCS dirs
|
||||||
|
.git/
|
||||||
|
.gitignore
|
||||||
|
.bzr/
|
||||||
|
.bzrignore
|
||||||
|
.hg/
|
||||||
|
.hgignore
|
||||||
|
.svn/
|
||||||
|
# Common backup files
|
||||||
|
*.swp
|
||||||
|
*.bak
|
||||||
|
*.tmp
|
||||||
|
*.orig
|
||||||
|
*~
|
||||||
|
# Various IDEs
|
||||||
|
.project
|
||||||
|
.idea/
|
||||||
|
*.tmproj
|
||||||
|
.vscode/
|
|
@ -0,0 +1,23 @@
|
||||||
|
apiVersion: v2
|
||||||
|
name: code-server
|
||||||
|
description: A Helm chart for cdr/code-server
|
||||||
|
|
||||||
|
# A chart can be either an 'application' or a 'library' chart.
|
||||||
|
#
|
||||||
|
# Application charts are a collection of templates that can be packaged into versioned archives
|
||||||
|
# to be deployed.
|
||||||
|
#
|
||||||
|
# Library charts provide useful utilities or functions for the chart developer. They're included as
|
||||||
|
# a dependency of application charts to inject those utilities and functions into the rendering
|
||||||
|
# pipeline. Library charts do not define any templates and therefore cannot be deployed.
|
||||||
|
type: application
|
||||||
|
|
||||||
|
# This is the chart version. This version number should be incremented each time you make changes
|
||||||
|
# to the chart and its templates, including the app version.
|
||||||
|
# Versions are expected to follow Semantic Versioning (https://semver.org/)
|
||||||
|
version: 1.0.2
|
||||||
|
|
||||||
|
# This is the version number of the application being deployed. This version number should be
|
||||||
|
# incremented each time you make changes to the application. Versions are not expected to
|
||||||
|
# follow Semantic Versioning. They should reflect the version the application is using.
|
||||||
|
appVersion: 3.7.3
|
|
@ -0,0 +1,117 @@
|
||||||
|
# code-server
|
||||||
|
|
||||||
|
![Version: 1.0.0](https://img.shields.io/badge/Version-1.0.0-informational?style=flat-square) ![Type: application](https://img.shields.io/badge/Type-application-informational?style=flat-square) ![AppVersion: 3.7.3](https://img.shields.io/badge/AppVersion-3.7.3-informational?style=flat-square)
|
||||||
|
|
||||||
|
[code-server](https://github.com/cdr/code-server) code-server is VS Code running
|
||||||
|
on a remote server, accessible through the browser.
|
||||||
|
|
||||||
|
This chart is community maintained by [@Matthew-Beckett](https://github.com/Matthew-Beckett) and [@alexgorbatchev](https://github.com/alexgorbatchev)
|
||||||
|
|
||||||
|
## TL;DR;
|
||||||
|
|
||||||
|
```console
|
||||||
|
$ git clone https://github.com/cdr/code-server
|
||||||
|
$ cd code-server
|
||||||
|
$ helm upgrade --install code-server ci/helm-chart
|
||||||
|
```
|
||||||
|
|
||||||
|
## Introduction
|
||||||
|
|
||||||
|
This chart bootstraps a code-server deployment on a
|
||||||
|
[Kubernetes](http://kubernetes.io) cluster using the [Helm](https://helm.sh)
|
||||||
|
package manager.
|
||||||
|
|
||||||
|
## Prerequisites
|
||||||
|
|
||||||
|
- Kubernetes 1.6+
|
||||||
|
|
||||||
|
## Installing the Chart
|
||||||
|
|
||||||
|
To install the chart with the release name `code-server`:
|
||||||
|
|
||||||
|
```console
|
||||||
|
$ git clone https://github.com/cdr/code-server
|
||||||
|
$ cd code-server
|
||||||
|
$ helm upgrade --install code-server ci/helm-chart
|
||||||
|
```
|
||||||
|
|
||||||
|
The command deploys code-server on the Kubernetes cluster in the default
|
||||||
|
configuration. The [configuration](#configuration) section lists the parameters
|
||||||
|
that can be configured during installation.
|
||||||
|
|
||||||
|
> **Tip**: List all releases using `helm list`
|
||||||
|
|
||||||
|
## Uninstalling the Chart
|
||||||
|
|
||||||
|
To uninstall/delete the `code-server` deployment:
|
||||||
|
|
||||||
|
```console
|
||||||
|
$ helm delete code-server
|
||||||
|
```
|
||||||
|
|
||||||
|
The command removes all the Kubernetes components associated with the chart and
|
||||||
|
deletes the release.
|
||||||
|
|
||||||
|
## Configuration
|
||||||
|
|
||||||
|
The following table lists the configurable parameters of the code-server chart
|
||||||
|
and their default values.
|
||||||
|
|
||||||
|
## Values
|
||||||
|
|
||||||
|
| Key | Type | Default | Description |
|
||||||
|
|-----|------|---------|-------------|
|
||||||
|
| affinity | object | `{}` | |
|
||||||
|
| extraArgs | list | `[]` | |
|
||||||
|
| extraConfigmapMounts | list | `[]` | |
|
||||||
|
| extraContainers | string | `""` | |
|
||||||
|
| extraSecretMounts | list | `[]` | |
|
||||||
|
| extraVars | list | `[]` | |
|
||||||
|
| extraVolumeMounts | list | `[]` | |
|
||||||
|
| fullnameOverride | string | `""` | |
|
||||||
|
| hostnameOverride | string | `""` | |
|
||||||
|
| image.pullPolicy | string | `"Always"` | |
|
||||||
|
| image.repository | string | `"codercom/code-server"` | |
|
||||||
|
| image.tag | string | `"3.7.3"` | |
|
||||||
|
| imagePullSecrets | list | `[]` | |
|
||||||
|
| ingress.enabled | bool | `false` | |
|
||||||
|
| nameOverride | string | `""` | |
|
||||||
|
| nodeSelector | object | `{}` | |
|
||||||
|
| persistence.accessMode | string | `"ReadWriteOnce"` | |
|
||||||
|
| persistence.annotations | object | `{}` | |
|
||||||
|
| persistence.enabled | bool | `true` | |
|
||||||
|
| persistence.size | string | `"1Gi"` | |
|
||||||
|
| podAnnotations | object | `{}` | |
|
||||||
|
| podSecurityContext | object | `{}` | |
|
||||||
|
| replicaCount | int | `1` | |
|
||||||
|
| resources | object | `{}` | |
|
||||||
|
| securityContext.enabled | bool | `true` | |
|
||||||
|
| securityContext.fsGroup | int | `1000` | |
|
||||||
|
| securityContext.runAsUser | int | `1000` | |
|
||||||
|
| service.port | int | `8443` | |
|
||||||
|
| service.type | string | `"ClusterIP"` | |
|
||||||
|
| serviceAccount.create | bool | `true` | |
|
||||||
|
| serviceAccount.name | string | `nil` | |
|
||||||
|
| tolerations | list | `[]` | |
|
||||||
|
| volumePermissions.enabled | bool | `true` | |
|
||||||
|
| volumePermissions.securityContext.runAsUser | int | `0` | |
|
||||||
|
|
||||||
|
Specify each parameter using the `--set key=value[,key=value]` argument to `helm
|
||||||
|
install`. For example,
|
||||||
|
|
||||||
|
```console
|
||||||
|
$ helm upgrade --install code-server \
|
||||||
|
ci/helm-chart \
|
||||||
|
--set persistence.enabled=false
|
||||||
|
```
|
||||||
|
|
||||||
|
The above command sets the the persistence storage to false.
|
||||||
|
|
||||||
|
Alternatively, a YAML file that specifies the values for the above parameters
|
||||||
|
can be provided while installing the chart. For example,
|
||||||
|
|
||||||
|
```console
|
||||||
|
$ helm upgrade --install code-server ci/helm-chart -f values.yaml
|
||||||
|
```
|
||||||
|
|
||||||
|
> **Tip**: You can use the default [values.yaml](values.yaml)
|
|
@ -0,0 +1,25 @@
|
||||||
|
1. Get the application URL by running these commands:
|
||||||
|
{{- if .Values.ingress.enabled }}
|
||||||
|
{{- range $host := .Values.ingress.hosts }}
|
||||||
|
{{- range .paths }}
|
||||||
|
http{{ if $.Values.ingress.tls }}s{{ end }}://{{ $host.host }}{{ . }}
|
||||||
|
{{- end }}
|
||||||
|
{{- end }}
|
||||||
|
{{- else if contains "NodePort" .Values.service.type }}
|
||||||
|
export NODE_PORT=$(kubectl get --namespace {{ .Release.Namespace }} -o jsonpath="{.spec.ports[0].nodePort}" services {{ include "code-server.fullname" . }})
|
||||||
|
export NODE_IP=$(kubectl get nodes --namespace {{ .Release.Namespace }} -o jsonpath="{.items[0].status.addresses[0].address}")
|
||||||
|
echo http://$NODE_IP:$NODE_PORT
|
||||||
|
{{- else if contains "LoadBalancer" .Values.service.type }}
|
||||||
|
NOTE: It may take a few minutes for the LoadBalancer IP to be available.
|
||||||
|
You can watch the status of by running 'kubectl get --namespace {{ .Release.Namespace }} svc -w {{ include "code-server.fullname" . }}'
|
||||||
|
export SERVICE_IP=$(kubectl get svc --namespace {{ .Release.Namespace }} {{ include "code-server.fullname" . }} -o jsonpath='{.status.loadBalancer.ingress[0].ip}')
|
||||||
|
echo http://$SERVICE_IP:{{ .Values.service.port }}
|
||||||
|
{{- else if contains "ClusterIP" .Values.service.type }}
|
||||||
|
export POD_NAME=$(kubectl get pods --namespace {{ .Release.Namespace }} -l "app.kubernetes.io/name={{ include "code-server.name" . }},app.kubernetes.io/instance={{ .Release.Name }}" -o jsonpath="{.items[0].metadata.name}")
|
||||||
|
echo "Visit http://127.0.0.1:8080 to use your application"
|
||||||
|
kubectl port-forward $POD_NAME 8080:80
|
||||||
|
{{- end }}
|
||||||
|
|
||||||
|
Administrator credentials:
|
||||||
|
|
||||||
|
Password: echo $(kubectl get secret --namespace {{ .Release.Namespace }} {{ template "code-server.fullname" . }} -o jsonpath="{.data.password}" | base64 --decode)
|
|
@ -0,0 +1,63 @@
|
||||||
|
{{/* vim: set filetype=mustache: */}}
|
||||||
|
{{/*
|
||||||
|
Expand the name of the chart.
|
||||||
|
*/}}
|
||||||
|
{{- define "code-server.name" -}}
|
||||||
|
{{- default .Chart.Name .Values.nameOverride | trunc 63 | trimSuffix "-" -}}
|
||||||
|
{{- end -}}
|
||||||
|
|
||||||
|
{{/*
|
||||||
|
Create a default fully qualified app name.
|
||||||
|
We truncate at 63 chars because some Kubernetes name fields are limited to this (by the DNS naming spec).
|
||||||
|
If release name contains chart name it will be used as a full name.
|
||||||
|
*/}}
|
||||||
|
{{- define "code-server.fullname" -}}
|
||||||
|
{{- if .Values.fullnameOverride -}}
|
||||||
|
{{- .Values.fullnameOverride | trunc 63 | trimSuffix "-" -}}
|
||||||
|
{{- else -}}
|
||||||
|
{{- $name := default .Chart.Name .Values.nameOverride -}}
|
||||||
|
{{- if contains $name .Release.Name -}}
|
||||||
|
{{- .Release.Name | trunc 63 | trimSuffix "-" -}}
|
||||||
|
{{- else -}}
|
||||||
|
{{- printf "%s-%s" .Release.Name $name | trunc 63 | trimSuffix "-" -}}
|
||||||
|
{{- end -}}
|
||||||
|
{{- end -}}
|
||||||
|
{{- end -}}
|
||||||
|
|
||||||
|
{{/*
|
||||||
|
Create chart name and version as used by the chart label.
|
||||||
|
*/}}
|
||||||
|
{{- define "code-server.chart" -}}
|
||||||
|
{{- printf "%s-%s" .Chart.Name .Chart.Version | replace "+" "_" | trunc 63 | trimSuffix "-" -}}
|
||||||
|
{{- end -}}
|
||||||
|
|
||||||
|
{{/*
|
||||||
|
Common labels
|
||||||
|
*/}}
|
||||||
|
{{- define "code-server.labels" -}}
|
||||||
|
helm.sh/chart: {{ include "code-server.chart" . }}
|
||||||
|
{{ include "code-server.selectorLabels" . }}
|
||||||
|
{{- if .Chart.AppVersion }}
|
||||||
|
app.kubernetes.io/version: {{ .Chart.AppVersion | quote }}
|
||||||
|
{{- end }}
|
||||||
|
app.kubernetes.io/managed-by: {{ .Release.Service }}
|
||||||
|
{{- end }}
|
||||||
|
|
||||||
|
{{/*
|
||||||
|
Selector labels
|
||||||
|
*/}}
|
||||||
|
{{- define "code-server.selectorLabels" -}}
|
||||||
|
app.kubernetes.io/name: {{ include "code-server.name" . }}
|
||||||
|
app.kubernetes.io/instance: {{ .Release.Name }}
|
||||||
|
{{- end }}
|
||||||
|
|
||||||
|
{{/*
|
||||||
|
Create the name of the service account to use
|
||||||
|
*/}}
|
||||||
|
{{- define "code-server.serviceAccountName" -}}
|
||||||
|
{{- if .Values.serviceAccount.create -}}
|
||||||
|
{{ default (include "code-server.fullname" .) .Values.serviceAccount.name }}
|
||||||
|
{{- else -}}
|
||||||
|
{{ default "default" .Values.serviceAccount.name }}
|
||||||
|
{{- end -}}
|
||||||
|
{{- end -}}
|
|
@ -0,0 +1,152 @@
|
||||||
|
apiVersion: apps/v1
|
||||||
|
kind: Deployment
|
||||||
|
metadata:
|
||||||
|
name: {{ include "code-server.fullname" . }}
|
||||||
|
labels:
|
||||||
|
app.kubernetes.io/name: {{ include "code-server.name" . }}
|
||||||
|
helm.sh/chart: {{ include "code-server.chart" . }}
|
||||||
|
app.kubernetes.io/instance: {{ .Release.Name }}
|
||||||
|
app.kubernetes.io/managed-by: {{ .Release.Service }}
|
||||||
|
spec:
|
||||||
|
replicas: 1
|
||||||
|
strategy:
|
||||||
|
type: Recreate
|
||||||
|
selector:
|
||||||
|
matchLabels:
|
||||||
|
app.kubernetes.io/name: {{ include "code-server.name" . }}
|
||||||
|
app.kubernetes.io/instance: {{ .Release.Name }}
|
||||||
|
template:
|
||||||
|
metadata:
|
||||||
|
labels:
|
||||||
|
app.kubernetes.io/name: {{ include "code-server.name" . }}
|
||||||
|
app.kubernetes.io/instance: {{ .Release.Name }}
|
||||||
|
spec:
|
||||||
|
{{- if .Values.hostnameOverride }}
|
||||||
|
hostname: {{ .Values.hostnameOverride }}
|
||||||
|
{{- end }}
|
||||||
|
{{- if .Values.securityContext.enabled }}
|
||||||
|
securityContext:
|
||||||
|
fsGroup: {{ .Values.securityContext.fsGroup }}
|
||||||
|
{{- end }}
|
||||||
|
{{- if and .Values.volumePermissions.enabled .Values.persistence.enabled }}
|
||||||
|
initContainers:
|
||||||
|
- name: init-chmod-data
|
||||||
|
image: busybox:latest
|
||||||
|
imagePullPolicy: IfNotPresent
|
||||||
|
command:
|
||||||
|
- sh
|
||||||
|
- -c
|
||||||
|
- |
|
||||||
|
chown -R {{ .Values.securityContext.runAsUser }}:{{ .Values.securityContext.fsGroup }} /home/coder
|
||||||
|
securityContext:
|
||||||
|
runAsUser: {{ .Values.volumePermissions.securityContext.runAsUser }}
|
||||||
|
volumeMounts:
|
||||||
|
- name: data
|
||||||
|
mountPath: /home/coder
|
||||||
|
{{- end }}
|
||||||
|
containers:
|
||||||
|
{{- if .Values.extraContainers }}
|
||||||
|
{{ toYaml .Values.extraContainers | indent 8}}
|
||||||
|
{{- end }}
|
||||||
|
- name: {{ .Chart.Name }}
|
||||||
|
image: "{{ .Values.image.repository }}:{{ .Values.image.tag }}"
|
||||||
|
imagePullPolicy: {{ .Values.image.pullPolicy }}
|
||||||
|
{{- if .Values.securityContext.enabled }}
|
||||||
|
securityContext:
|
||||||
|
runAsUser: {{ .Values.securityContext.runAsUser }}
|
||||||
|
{{- end }}
|
||||||
|
env:
|
||||||
|
{{- if .Values.extraVars }}
|
||||||
|
{{ toYaml .Values.extraVars | indent 10 }}
|
||||||
|
{{- end }}
|
||||||
|
- name: PASSWORD
|
||||||
|
valueFrom:
|
||||||
|
secretKeyRef:
|
||||||
|
{{- if .Values.existingSecret }}
|
||||||
|
name: {{ .Values.existingSecret }}
|
||||||
|
{{- else }}
|
||||||
|
name: {{ template "code-server.fullname" . }}
|
||||||
|
{{- end }}
|
||||||
|
key: password
|
||||||
|
{{- if .Values.extraArgs }}
|
||||||
|
args:
|
||||||
|
{{ toYaml .Values.extraArgs | indent 10 }}
|
||||||
|
{{- end }}
|
||||||
|
volumeMounts:
|
||||||
|
- name: data
|
||||||
|
mountPath: /home/coder
|
||||||
|
{{- range .Values.extraConfigmapMounts }}
|
||||||
|
- name: {{ .name }}
|
||||||
|
mountPath: {{ .mountPath }}
|
||||||
|
subPath: {{ .subPath | default "" }}
|
||||||
|
readOnly: {{ .readOnly }}
|
||||||
|
{{- end }}
|
||||||
|
{{- range .Values.extraSecretMounts }}
|
||||||
|
- name: {{ .name }}
|
||||||
|
mountPath: {{ .mountPath }}
|
||||||
|
readOnly: {{ .readOnly }}
|
||||||
|
{{- end }}
|
||||||
|
{{- range .Values.extraVolumeMounts }}
|
||||||
|
- name: {{ .name }}
|
||||||
|
mountPath: {{ .mountPath }}
|
||||||
|
subPath: {{ .subPath | default "" }}
|
||||||
|
readOnly: {{ .readOnly }}
|
||||||
|
{{- end }}
|
||||||
|
ports:
|
||||||
|
- name: http
|
||||||
|
containerPort: 8080
|
||||||
|
protocol: TCP
|
||||||
|
livenessProbe:
|
||||||
|
httpGet:
|
||||||
|
path: /
|
||||||
|
port: http
|
||||||
|
readinessProbe:
|
||||||
|
httpGet:
|
||||||
|
path: /
|
||||||
|
port: http
|
||||||
|
resources:
|
||||||
|
{{- toYaml .Values.resources | nindent 12 }}
|
||||||
|
{{- with .Values.nodeSelector }}
|
||||||
|
nodeSelector:
|
||||||
|
{{- toYaml . | nindent 8 }}
|
||||||
|
{{- end }}
|
||||||
|
{{- with .Values.affinity }}
|
||||||
|
affinity:
|
||||||
|
{{- toYaml . | nindent 8 }}
|
||||||
|
{{- end }}
|
||||||
|
{{- with .Values.tolerations }}
|
||||||
|
tolerations:
|
||||||
|
{{- toYaml . | nindent 8 }}
|
||||||
|
{{- end }}
|
||||||
|
serviceAccountName: {{ template "code-server.serviceAccountName" . }}
|
||||||
|
volumes:
|
||||||
|
- name: data
|
||||||
|
{{- if .Values.persistence.enabled }}
|
||||||
|
{{- if not .Values.persistence.hostPath }}
|
||||||
|
persistentVolumeClaim:
|
||||||
|
claimName: {{ .Values.persistence.existingClaim | default (include "code-server.fullname" .) }}
|
||||||
|
{{- else }}
|
||||||
|
hostPath:
|
||||||
|
path: {{ .Values.persistence.hostPath }}
|
||||||
|
type: Directory
|
||||||
|
{{- end -}}
|
||||||
|
{{- else }}
|
||||||
|
emptyDir: {}
|
||||||
|
{{- end -}}
|
||||||
|
{{- range .Values.extraSecretMounts }}
|
||||||
|
- name: {{ .name }}
|
||||||
|
secret:
|
||||||
|
secretName: {{ .secretName }}
|
||||||
|
defaultMode: {{ .defaultMode }}
|
||||||
|
{{- end }}
|
||||||
|
{{- range .Values.extraVolumeMounts }}
|
||||||
|
- name: {{ .name }}
|
||||||
|
{{- if .existingClaim }}
|
||||||
|
persistentVolumeClaim:
|
||||||
|
claimName: {{ .existingClaim }}
|
||||||
|
{{- else }}
|
||||||
|
hostPath:
|
||||||
|
path: {{ .hostPath }}
|
||||||
|
type: Directory
|
||||||
|
{{- end }}
|
||||||
|
{{- end }}
|
|
@ -0,0 +1,41 @@
|
||||||
|
{{- if .Values.ingress.enabled -}}
|
||||||
|
{{- $fullName := include "code-server.fullname" . -}}
|
||||||
|
{{- $svcPort := .Values.service.port -}}
|
||||||
|
{{- if semverCompare ">=1.14-0" .Capabilities.KubeVersion.GitVersion -}}
|
||||||
|
apiVersion: networking.k8s.io/v1beta1
|
||||||
|
{{- else -}}
|
||||||
|
apiVersion: extensions/v1beta1
|
||||||
|
{{- end }}
|
||||||
|
kind: Ingress
|
||||||
|
metadata:
|
||||||
|
name: {{ $fullName }}
|
||||||
|
labels:
|
||||||
|
{{- include "code-server.labels" . | nindent 4 }}
|
||||||
|
{{- with .Values.ingress.annotations }}
|
||||||
|
annotations:
|
||||||
|
{{- toYaml . | nindent 4 }}
|
||||||
|
{{- end }}
|
||||||
|
spec:
|
||||||
|
{{- if .Values.ingress.tls }}
|
||||||
|
tls:
|
||||||
|
{{- range .Values.ingress.tls }}
|
||||||
|
- hosts:
|
||||||
|
{{- range .hosts }}
|
||||||
|
- {{ . | quote }}
|
||||||
|
{{- end }}
|
||||||
|
secretName: {{ .secretName }}
|
||||||
|
{{- end }}
|
||||||
|
{{- end }}
|
||||||
|
rules:
|
||||||
|
{{- range .Values.ingress.hosts }}
|
||||||
|
- host: {{ .host | quote }}
|
||||||
|
http:
|
||||||
|
paths:
|
||||||
|
{{- range .paths }}
|
||||||
|
- path: {{ . }}
|
||||||
|
backend:
|
||||||
|
serviceName: {{ $fullName }}
|
||||||
|
servicePort: {{ $svcPort }}
|
||||||
|
{{- end }}
|
||||||
|
{{- end }}
|
||||||
|
{{- end }}
|
|
@ -0,0 +1,29 @@
|
||||||
|
{{- if and (and .Values.persistence.enabled (not .Values.persistence.existingClaim)) (not .Values.persistence.hostPath) }}
|
||||||
|
kind: PersistentVolumeClaim
|
||||||
|
apiVersion: v1
|
||||||
|
metadata:
|
||||||
|
name: {{ include "code-server.fullname" . }}
|
||||||
|
namespace: {{ .Release.Namespace }}
|
||||||
|
{{- with .Values.persistence.annotations }}
|
||||||
|
annotations:
|
||||||
|
{{ toYaml . | indent 4 }}
|
||||||
|
{{- end }}
|
||||||
|
labels:
|
||||||
|
app.kubernetes.io/name: {{ include "code-server.name" . }}
|
||||||
|
helm.sh/chart: {{ include "code-server.chart" . }}
|
||||||
|
app.kubernetes.io/instance: {{ .Release.Name }}
|
||||||
|
app.kubernetes.io/managed-by: {{ .Release.Service }}
|
||||||
|
spec:
|
||||||
|
accessModes:
|
||||||
|
- {{ .Values.persistence.accessMode | quote }}
|
||||||
|
resources:
|
||||||
|
requests:
|
||||||
|
storage: {{ .Values.persistence.size | quote }}
|
||||||
|
{{- if .Values.persistence.storageClass }}
|
||||||
|
{{- if (eq "-" .Values.persistence.storageClass) }}
|
||||||
|
storageClassName: ""
|
||||||
|
{{- else }}
|
||||||
|
storageClassName: "{{ .Values.persistence.storageClass }}"
|
||||||
|
{{- end }}
|
||||||
|
{{- end }}
|
||||||
|
{{- end }}
|
|
@ -0,0 +1,18 @@
|
||||||
|
apiVersion: v1
|
||||||
|
kind: Secret
|
||||||
|
metadata:
|
||||||
|
name: {{ include "code-server.fullname" . }}
|
||||||
|
annotations:
|
||||||
|
"helm.sh/hook": "pre-install"
|
||||||
|
labels:
|
||||||
|
app.kubernetes.io/name: {{ include "code-server.name" . }}
|
||||||
|
helm.sh/chart: {{ include "code-server.chart" . }}
|
||||||
|
app.kubernetes.io/instance: {{ .Release.Name }}
|
||||||
|
app.kubernetes.io/managed-by: {{ .Release.Service }}
|
||||||
|
type: Opaque
|
||||||
|
data:
|
||||||
|
{{ if .Values.password }}
|
||||||
|
password: "{{ .Values.password | b64enc }}"
|
||||||
|
{{ else }}
|
||||||
|
password: "{{ randAlphaNum 24 | b64enc }}"
|
||||||
|
{{ end }}
|
|
@ -0,0 +1,19 @@
|
||||||
|
apiVersion: v1
|
||||||
|
kind: Service
|
||||||
|
metadata:
|
||||||
|
name: {{ include "code-server.fullname" . }}
|
||||||
|
labels:
|
||||||
|
app.kubernetes.io/name: {{ include "code-server.name" . }}
|
||||||
|
helm.sh/chart: {{ include "code-server.chart" . }}
|
||||||
|
app.kubernetes.io/instance: {{ .Release.Name }}
|
||||||
|
app.kubernetes.io/managed-by: {{ .Release.Service }}
|
||||||
|
spec:
|
||||||
|
type: {{ .Values.service.type }}
|
||||||
|
ports:
|
||||||
|
- port: {{ .Values.service.port }}
|
||||||
|
targetPort: http
|
||||||
|
protocol: TCP
|
||||||
|
name: http
|
||||||
|
selector:
|
||||||
|
app.kubernetes.io/name: {{ include "code-server.name" . }}
|
||||||
|
app.kubernetes.io/instance: {{ .Release.Name }}
|
|
@ -0,0 +1,11 @@
|
||||||
|
{{- if or .Values.serviceAccount.create -}}
|
||||||
|
apiVersion: v1
|
||||||
|
kind: ServiceAccount
|
||||||
|
metadata:
|
||||||
|
labels:
|
||||||
|
app.kubernetes.io/name: {{ include "code-server.name" . }}
|
||||||
|
helm.sh/chart: {{ include "code-server.chart" . }}
|
||||||
|
app.kubernetes.io/instance: {{ .Release.Name }}
|
||||||
|
app.kubernetes.io/managed-by: {{ .Release.Service }}
|
||||||
|
name: {{ template "code-server.serviceAccountName" . }}
|
||||||
|
{{- end -}}
|
|
@ -0,0 +1,18 @@
|
||||||
|
apiVersion: v1
|
||||||
|
kind: Pod
|
||||||
|
metadata:
|
||||||
|
name: "{{ include "code-server.fullname" . }}-test-connection"
|
||||||
|
labels:
|
||||||
|
app.kubernetes.io/name: {{ include "code-server.name" . }}
|
||||||
|
helm.sh/chart: {{ include "code-server.chart" . }}
|
||||||
|
app.kubernetes.io/instance: {{ .Release.Name }}
|
||||||
|
app.kubernetes.io/managed-by: {{ .Release.Service }}
|
||||||
|
annotations:
|
||||||
|
"helm.sh/hook": test-success
|
||||||
|
spec:
|
||||||
|
containers:
|
||||||
|
- name: wget
|
||||||
|
image: busybox
|
||||||
|
command: ['wget']
|
||||||
|
args: ['{{ include "code-server.fullname" . }}:{{ .Values.service.port }}']
|
||||||
|
restartPolicy: Never
|
|
@ -0,0 +1,163 @@
|
||||||
|
# Default values for code-server.
|
||||||
|
# This is a YAML-formatted file.
|
||||||
|
# Declare variables to be passed into your templates.
|
||||||
|
|
||||||
|
replicaCount: 1
|
||||||
|
|
||||||
|
image:
|
||||||
|
repository: codercom/code-server
|
||||||
|
tag: '3.7.3'
|
||||||
|
pullPolicy: Always
|
||||||
|
|
||||||
|
imagePullSecrets: []
|
||||||
|
nameOverride: ""
|
||||||
|
fullnameOverride: ""
|
||||||
|
hostnameOverride: ""
|
||||||
|
|
||||||
|
serviceAccount:
|
||||||
|
# Specifies whether a service account should be created
|
||||||
|
create: true
|
||||||
|
# Annotations to add to the service account
|
||||||
|
annotations: {}
|
||||||
|
# The name of the service account to use.
|
||||||
|
# If not set and create is true, a name is generated using the fullname template
|
||||||
|
name: ""
|
||||||
|
|
||||||
|
podAnnotations: {}
|
||||||
|
|
||||||
|
podSecurityContext: {}
|
||||||
|
# fsGroup: 2000
|
||||||
|
|
||||||
|
securityContext: {}
|
||||||
|
# capabilities:
|
||||||
|
# drop:
|
||||||
|
# - ALL
|
||||||
|
# readOnlyRootFilesystem: true
|
||||||
|
# runAsNonRoot: true
|
||||||
|
# runAsUser: 1000
|
||||||
|
|
||||||
|
service:
|
||||||
|
type: ClusterIP
|
||||||
|
port: 8080
|
||||||
|
|
||||||
|
ingress:
|
||||||
|
enabled: false
|
||||||
|
#annotations:
|
||||||
|
# kubernetes.io/ingress.class: nginx
|
||||||
|
# kubernetes.io/tls-acme: "true"
|
||||||
|
#hosts:
|
||||||
|
# - host: code-server.example.loc
|
||||||
|
# paths:
|
||||||
|
# - /
|
||||||
|
|
||||||
|
#tls:
|
||||||
|
# - secretName: code-server
|
||||||
|
# hosts:
|
||||||
|
# - code-server.example.loc
|
||||||
|
|
||||||
|
# Optional additional arguments
|
||||||
|
extraArgs: []
|
||||||
|
# - --allow-http
|
||||||
|
# - --no-auth
|
||||||
|
|
||||||
|
# Optional additional environment variables
|
||||||
|
extraVars: []
|
||||||
|
# - name: DISABLE_TELEMETRY
|
||||||
|
# value: true
|
||||||
|
|
||||||
|
##
|
||||||
|
## Init containers parameters:
|
||||||
|
## volumePermissions: Change the owner of the persist volume mountpoint to RunAsUser:fsGroup
|
||||||
|
##
|
||||||
|
volumePermissions:
|
||||||
|
enabled: true
|
||||||
|
securityContext:
|
||||||
|
runAsUser: 0
|
||||||
|
|
||||||
|
## Pod Security Context
|
||||||
|
## ref: https://kubernetes.io/docs/tasks/configure-pod-container/security-context/
|
||||||
|
##
|
||||||
|
securityContext:
|
||||||
|
enabled: true
|
||||||
|
fsGroup: 1000
|
||||||
|
runAsUser: 1000
|
||||||
|
|
||||||
|
resources: {}
|
||||||
|
# We usually recommend not to specify default resources and to leave this as a conscious
|
||||||
|
# choice for the user. This also increases chances charts run on environments with little
|
||||||
|
# resources, such as Minikube. If you do want to specify resources, uncomment the following
|
||||||
|
# lines, adjust them as necessary, and remove the curly braces after 'resources:'.
|
||||||
|
# limits:
|
||||||
|
# cpu: 100m
|
||||||
|
# memory: 128Mi
|
||||||
|
# requests:
|
||||||
|
# cpu: 100m
|
||||||
|
# memory: 1000Mi
|
||||||
|
|
||||||
|
nodeSelector: {}
|
||||||
|
|
||||||
|
tolerations: []
|
||||||
|
|
||||||
|
affinity: {}
|
||||||
|
|
||||||
|
## Persist data to a persistent volume
|
||||||
|
persistence:
|
||||||
|
enabled: true
|
||||||
|
## code-server data Persistent Volume Storage Class
|
||||||
|
## If defined, storageClassName: <storageClass>
|
||||||
|
## If set to "-", storageClassName: "", which disables dynamic provisioning
|
||||||
|
## If undefined (the default) or set to null, no storageClassName spec is
|
||||||
|
## set, choosing the default provisioner. (gp2 on AWS, standard on
|
||||||
|
## GKE, AWS & OpenStack)
|
||||||
|
##
|
||||||
|
# storageClass: "-"
|
||||||
|
accessMode: ReadWriteOnce
|
||||||
|
size: 10Gi
|
||||||
|
annotations: {}
|
||||||
|
# existingClaim: ""
|
||||||
|
# hostPath: /data
|
||||||
|
|
||||||
|
serviceAccount:
|
||||||
|
create: true
|
||||||
|
name:
|
||||||
|
|
||||||
|
## Enable an Specify container in extraContainers.
|
||||||
|
## This is meant to allow adding code-server dependencies, like docker-dind.
|
||||||
|
extraContainers: |
|
||||||
|
#- name: docker-dind
|
||||||
|
# image: docker:19.03-dind
|
||||||
|
# imagePullPolicy: IfNotPresent
|
||||||
|
# resources:
|
||||||
|
# requests:
|
||||||
|
# cpu: 250m
|
||||||
|
# memory: 256M
|
||||||
|
# securityContext:
|
||||||
|
# privileged: true
|
||||||
|
# procMount: Default
|
||||||
|
# env:
|
||||||
|
# - name: DOCKER_TLS_CERTDIR
|
||||||
|
# value: ""
|
||||||
|
# - name: DOCKER_DRIVER
|
||||||
|
# value: "overlay2"
|
||||||
|
|
||||||
|
## Additional code-server secret mounts
|
||||||
|
extraSecretMounts: []
|
||||||
|
# - name: secret-files
|
||||||
|
# mountPath: /etc/secrets
|
||||||
|
# secretName: code-server-secret-files
|
||||||
|
# readOnly: true
|
||||||
|
|
||||||
|
## Additional code-server volume mounts
|
||||||
|
extraVolumeMounts: []
|
||||||
|
# - name: extra-volume
|
||||||
|
# mountPath: /mnt/volume
|
||||||
|
# readOnly: true
|
||||||
|
# existingClaim: volume-claim
|
||||||
|
# hostPath: ""
|
||||||
|
|
||||||
|
extraConfigmapMounts: []
|
||||||
|
# - name: certs-configmap
|
||||||
|
# mountPath: /etc/code-server/ssl/
|
||||||
|
# subPath: certificates.crt # (optional)
|
||||||
|
# configMap: certs-configmap
|
||||||
|
# readOnly: true
|
|
@ -1,6 +1,6 @@
|
||||||
FROM centos:7
|
FROM centos:7
|
||||||
|
|
||||||
ARG NODE_VERSION=v12.18.3
|
ARG NODE_VERSION=v12.18.4
|
||||||
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/$NODE_VERSION/node-$NODE_VERSION-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-$NODE_VERSION-linux-$ARCH" "/usr/local/node-$NODE_VERSION"
|
mv "/usr/local/node-$NODE_VERSION-linux-$ARCH" "/usr/local/node-$NODE_VERSION"
|
||||||
|
@ -15,13 +15,18 @@ RUN npm config set python python2
|
||||||
RUN yum install -y epel-release && yum install -y jq
|
RUN yum install -y epel-release && yum install -y jq
|
||||||
RUN yum install -y rsync
|
RUN yum install -y rsync
|
||||||
|
|
||||||
# Copied from ../debian8/Dockerfile
|
# Copied from ../debian10/Dockerfile
|
||||||
# Install Go dependencies
|
# Install Go.
|
||||||
RUN ARCH="$(uname -m | sed 's/x86_64/amd64/; s/aarch64/arm64/')" && \
|
RUN ARCH="$(uname -m | sed 's/x86_64/amd64/; s/aarch64/arm64/')" && \
|
||||||
curl -fsSL "https://dl.google.com/go/go1.14.3.linux-$ARCH.tar.gz" | tar -C /usr/local -xz
|
curl -fsSL "https://dl.google.com/go/go1.14.3.linux-$ARCH.tar.gz" | tar -C /usr/local -xz
|
||||||
ENV PATH=/usr/local/go/bin:/root/go/bin:$PATH
|
ENV GOPATH=/gopath
|
||||||
|
# Ensures running this image as another user works.
|
||||||
|
RUN mkdir -p $GOPATH && chmod -R 777 $GOPATH
|
||||||
|
ENV PATH=/usr/local/go/bin:$GOPATH/bin:$PATH
|
||||||
|
|
||||||
|
# Install Go dependencies
|
||||||
ENV GO111MODULE=on
|
ENV GO111MODULE=on
|
||||||
RUN go get mvdan.cc/sh/v3/cmd/shfmt
|
RUN go get mvdan.cc/sh/v3/cmd/shfmt
|
||||||
RUN go get github.com/goreleaser/nfpm/cmd/nfpm
|
RUN go get github.com/goreleaser/nfpm/cmd/nfpm@v1.9.0
|
||||||
|
|
||||||
RUN curl -fsSL https://get.docker.com | sh
|
RUN curl -fsSL https://get.docker.com | sh
|
||||||
|
|
|
@ -1,4 +1,4 @@
|
||||||
FROM debian:8
|
FROM debian:10
|
||||||
|
|
||||||
RUN apt-get update
|
RUN apt-get update
|
||||||
|
|
||||||
|
@ -24,30 +24,31 @@ RUN apt-get install -y build-essential \
|
||||||
RUN apt-get install -y gettext-base
|
RUN apt-get install -y gettext-base
|
||||||
|
|
||||||
# Misc build dependencies.
|
# Misc build dependencies.
|
||||||
RUN apt-get install -y git rsync unzip
|
RUN apt-get install -y git rsync unzip jq
|
||||||
|
|
||||||
# We need latest jq from debian buster for date support.
|
|
||||||
RUN ARCH="$(dpkg --print-architecture)" && \
|
|
||||||
curl -fsSOL http://http.us.debian.org/debian/pool/main/libo/libonig/libonig5_6.9.1-1_$ARCH.deb && \
|
|
||||||
dpkg -i libonig*.deb && \
|
|
||||||
curl -fsSOL http://http.us.debian.org/debian/pool/main/j/jq/libjq1_1.5+dfsg-2+b1_$ARCH.deb && \
|
|
||||||
dpkg -i libjq*.deb && \
|
|
||||||
curl -fsSOL http://http.us.debian.org/debian/pool/main/j/jq/jq_1.5+dfsg-2+b1_$ARCH.deb && \
|
|
||||||
dpkg -i jq*.deb && rm *.deb
|
|
||||||
|
|
||||||
# Installs shellcheck.
|
# Installs shellcheck.
|
||||||
# Unfortunately coredumps on debian:8 so disabled for now.
|
RUN curl -fsSL https://github.com/koalaman/shellcheck/releases/download/v0.7.1/shellcheck-v0.7.1.linux.$(uname -m).tar.xz | \
|
||||||
#RUN curl -fsSL https://github.com/koalaman/shellcheck/releases/download/v0.7.1/shellcheck-v0.7.1.linux.$(uname -m).tar.xz | \
|
tar -xJ && \
|
||||||
# tar -xJ && \
|
mv shellcheck*/shellcheck /usr/local/bin && \
|
||||||
# mv shellcheck*/shellcheck /usr/local/bin && \
|
rm -R shellcheck*
|
||||||
# rm -R shellcheck*
|
|
||||||
|
|
||||||
# Install Go dependencies
|
# Install Go.
|
||||||
RUN ARCH="$(uname -m | sed 's/x86_64/amd64/; s/aarch64/arm64/')" && \
|
RUN ARCH="$(uname -m | sed 's/x86_64/amd64/; s/aarch64/arm64/')" && \
|
||||||
curl -fsSL "https://dl.google.com/go/go1.14.3.linux-$ARCH.tar.gz" | tar -C /usr/local -xz
|
curl -fsSL "https://dl.google.com/go/go1.14.3.linux-$ARCH.tar.gz" | tar -C /usr/local -xz
|
||||||
ENV PATH=/usr/local/go/bin:/root/go/bin:$PATH
|
ENV GOPATH=/gopath
|
||||||
|
# Ensures running this image as another user works.
|
||||||
|
RUN mkdir -p $GOPATH && chmod -R 777 $GOPATH
|
||||||
|
ENV PATH=/usr/local/go/bin:$GOPATH/bin:$PATH
|
||||||
|
|
||||||
|
# Install Go dependencies
|
||||||
ENV GO111MODULE=on
|
ENV GO111MODULE=on
|
||||||
RUN go get mvdan.cc/sh/v3/cmd/shfmt
|
RUN go get mvdan.cc/sh/v3/cmd/shfmt
|
||||||
RUN go get github.com/goreleaser/nfpm/cmd/nfpm
|
RUN go get github.com/goreleaser/nfpm/cmd/nfpm@v1.9.0
|
||||||
|
|
||||||
|
RUN VERSION="$(curl -fsSL https://storage.googleapis.com/kubernetes-release/release/stable.txt)" && \
|
||||||
|
curl -fsSL "https://storage.googleapis.com/kubernetes-release/release/$VERSION/bin/linux/amd64/kubectl" > /usr/local/bin/kubectl \
|
||||||
|
&& chmod +x /usr/local/bin/kubectl
|
||||||
|
RUN curl -fsSL https://raw.githubusercontent.com/helm/helm/master/scripts/get-helm-3 | bash
|
||||||
|
RUN helm plugin install https://github.com/instrumenta/helm-kubeval
|
||||||
|
|
||||||
RUN curl -fsSL https://get.docker.com | sh
|
RUN curl -fsSL https://get.docker.com | sh
|
|
@ -43,5 +43,6 @@ EXPOSE 8080
|
||||||
# the uid will remain the same. note: only relevant if -u isn't passed to
|
# the uid will remain the same. note: only relevant if -u isn't passed to
|
||||||
# docker-run.
|
# docker-run.
|
||||||
USER 1000
|
USER 1000
|
||||||
|
ENV USER=coder
|
||||||
WORKDIR /home/coder
|
WORKDIR /home/coder
|
||||||
ENTRYPOINT ["/usr/bin/entrypoint.sh", "--bind-addr", "0.0.0.0:8080", "."]
|
ENTRYPOINT ["/usr/bin/entrypoint.sh", "--bind-addr", "0.0.0.0:8080", "."]
|
||||||
|
|
|
@ -1,20 +1,20 @@
|
||||||
#!/bin/sh
|
#!/bin/sh
|
||||||
set -eu
|
set -eu
|
||||||
|
|
||||||
# This isn't set by default.
|
# We do this first to ensure sudo works below when renaming the user.
|
||||||
export USER="$(whoami)"
|
# Otherwise the current container UID may not exist in the passwd database.
|
||||||
|
eval "$(fixuid -q)"
|
||||||
|
|
||||||
if [ "${DOCKER_USER-}" != "$USER" ]; then
|
if [ "${DOCKER_USER-}" ]; then
|
||||||
echo "$DOCKER_USER ALL=(ALL) NOPASSWD:ALL" | sudo tee -a /etc/sudoers.d/nopasswd > /dev/null
|
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
|
# 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.
|
# nor can we bind mount $HOME into a new home as that requires a privileged container.
|
||||||
sudo usermod --login "$DOCKER_USER" coder
|
sudo usermod --login "$DOCKER_USER" coder
|
||||||
sudo groupmod -n "$DOCKER_USER" coder
|
sudo groupmod -n "$DOCKER_USER" coder
|
||||||
|
|
||||||
export USER="$(whoami)"
|
USER="$DOCKER_USER"
|
||||||
|
|
||||||
sudo sed -i "/coder/d" /etc/sudoers.d/nopasswd
|
sudo sed -i "/coder/d" /etc/sudoers.d/nopasswd
|
||||||
sudo sed -i "s/coder/$DOCKER_USER/g" /etc/fixuid/config.yml
|
|
||||||
fi
|
fi
|
||||||
|
|
||||||
dumb-init fixuid -q /usr/bin/code-server "$@"
|
dumb-init /usr/bin/code-server "$@"
|
||||||
|
|
|
@ -4,7 +4,7 @@ set -euo pipefail
|
||||||
main() {
|
main() {
|
||||||
cd "$(dirname "$0")/../.."
|
cd "$(dirname "$0")/../.."
|
||||||
|
|
||||||
NODE_VERSION=v12.18.3
|
NODE_VERSION=v12.18.4
|
||||||
NODE_OS="$(uname | tr '[:upper:]' '[:lower:]')"
|
NODE_OS="$(uname | tr '[:upper:]' '[:lower:]')"
|
||||||
NODE_ARCH="$(uname -m | sed 's/86_64/64/; s/aarch64/arm64/')"
|
NODE_ARCH="$(uname -m | sed 's/86_64/64/; s/aarch64/arm64/')"
|
||||||
curl -L "https://nodejs.org/dist/$NODE_VERSION/node-$NODE_VERSION-$NODE_OS-$NODE_ARCH.tar.gz" | tar -xz
|
curl -L "https://nodejs.org/dist/$NODE_VERSION/node-$NODE_VERSION-$NODE_OS-$NODE_ARCH.tar.gz" | tar -xz
|
||||||
|
|
|
@ -8,6 +8,7 @@
|
||||||
- [Build](#build)
|
- [Build](#build)
|
||||||
- [Structure](#structure)
|
- [Structure](#structure)
|
||||||
- [VS Code Patch](#vs-code-patch)
|
- [VS Code Patch](#vs-code-patch)
|
||||||
|
- [Currently Known Issues](#currently-known-issues)
|
||||||
|
|
||||||
<!-- END doctoc generated TOC please keep comment here to allow auto update -->
|
<!-- END doctoc generated TOC please keep comment here to allow auto update -->
|
||||||
|
|
||||||
|
@ -15,24 +16,26 @@
|
||||||
|
|
||||||
## Pull Requests
|
## Pull Requests
|
||||||
|
|
||||||
Please link to the issue each PR solves.
|
Please create a [GitHub Issue](https://github.com/cdr/code-server/issues) for each issue
|
||||||
If there is no existing issue, please first create one unless the fix is minor.
|
you'd like to address unless the proposed fix is minor.
|
||||||
|
|
||||||
Please make sure the base of your PR is the master branch. We keep the GitHub
|
In your Pull Requests (PR), link to the issue that the PR solves.
|
||||||
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
|
Please ensure that the base of your PR is the **master** branch. (Note: The default
|
||||||
features.
|
GitHub branch is the latest release branch, though you should point all of your changes to be merged into
|
||||||
|
master).
|
||||||
|
|
||||||
## Requirements
|
## Requirements
|
||||||
|
|
||||||
Please refer to [VS Code's prerequisites](https://github.com/Microsoft/vscode/wiki/How-to-Contribute#prerequisites).
|
The prerequisites for contributing to code-server are almost the same as those for
|
||||||
|
[VS Code](https://github.com/Microsoft/vscode/wiki/How-to-Contribute#prerequisites).
|
||||||
|
There are several differences, however. You must:
|
||||||
|
|
||||||
Differences:
|
- Use Node.js version 12.x (or greater)
|
||||||
|
- Have [nfpm](https://github.com/goreleaser/nfpm) (which is used to build `.deb` and `.rpm` packages and [jq](https://stedolan.github.io/jq/) (used to build code-server releases) installed
|
||||||
|
|
||||||
- We require a minimum of node v12 but later versions should work.
|
The [CI container](../ci/images/debian8/Dockerfile) is a useful reference for all
|
||||||
- We use [nfpm](https://github.com/goreleaser/nfpm) to build `.deb` and `.rpm` packages.
|
of the dependencies code-server uses.
|
||||||
- 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.
|
|
||||||
|
|
||||||
## Development Workflow
|
## Development Workflow
|
||||||
|
|
||||||
|
@ -40,10 +43,10 @@ Differences:
|
||||||
yarn
|
yarn
|
||||||
yarn vscode
|
yarn vscode
|
||||||
yarn watch
|
yarn watch
|
||||||
# Visit http://localhost:8080 once the build completed.
|
# Visit http://localhost:8080 once the build is completed.
|
||||||
```
|
```
|
||||||
|
|
||||||
To develop inside of an isolated docker container:
|
To develop inside an isolated Docker container:
|
||||||
|
|
||||||
```shell
|
```shell
|
||||||
./ci/dev/image/run.sh yarn
|
./ci/dev/image/run.sh yarn
|
||||||
|
@ -53,12 +56,12 @@ To develop inside of an isolated docker container:
|
||||||
|
|
||||||
`yarn watch` will live reload changes to the source.
|
`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 you introduce changes to the patch and you've previously built, you
|
||||||
reset VS Code then run `yarn vscode:patch`.
|
must (1) manually reset VS Code and (2) run `yarn vscode:patch`.
|
||||||
|
|
||||||
## Build
|
## Build
|
||||||
|
|
||||||
You can build with:
|
You can build using:
|
||||||
|
|
||||||
```shell
|
```shell
|
||||||
./ci/dev/image/run.sh ./ci/steps/release.sh
|
./ci/dev/image/run.sh ./ci/steps/release.sh
|
||||||
|
@ -66,22 +69,22 @@ You can build with:
|
||||||
|
|
||||||
Run your build with:
|
Run your build with:
|
||||||
|
|
||||||
```
|
```shell
|
||||||
cd release
|
cd release
|
||||||
yarn --production
|
yarn --production
|
||||||
# Runs the built JavaScript with Node.
|
# Runs the built JavaScript with Node.
|
||||||
node .
|
node .
|
||||||
```
|
```
|
||||||
|
|
||||||
Build release packages (make sure you run `./ci/steps/release.sh` first):
|
Build the release packages (make sure that you run `./ci/steps/release.sh` first):
|
||||||
|
|
||||||
```
|
```shell
|
||||||
./ci/dev/image/run.sh ./ci/steps/release-packages.sh
|
IMAGE=centos7 ./ci/dev/image/run.sh ./ci/steps/release-packages.sh
|
||||||
# The standalone release is in ./release-standalone
|
# The standalone release is in ./release-standalone
|
||||||
# .deb, .rpm and the standalone archive are in ./release-packages
|
# .deb, .rpm and the standalone archive are in ./release-packages
|
||||||
```
|
```
|
||||||
|
|
||||||
The `release.sh` script is the equivalent of:
|
The `release.sh` script is equal to running:
|
||||||
|
|
||||||
```shell
|
```shell
|
||||||
yarn
|
yarn
|
||||||
|
@ -91,66 +94,69 @@ yarn build:vscode
|
||||||
yarn release
|
yarn release
|
||||||
```
|
```
|
||||||
|
|
||||||
And `release-packages.sh` is:
|
And `release-packages.sh` is equal to:
|
||||||
|
|
||||||
```
|
```shell
|
||||||
yarn release:standalone
|
yarn release:standalone
|
||||||
yarn test:standalone-release
|
yarn test:standalone-release
|
||||||
yarn package
|
yarn package
|
||||||
```
|
```
|
||||||
|
|
||||||
|
For a faster release build, you can run instead:
|
||||||
|
|
||||||
|
```shell
|
||||||
|
KEEP_MODULES=1 ./ci/steps/release.sh
|
||||||
|
node ./release
|
||||||
|
```
|
||||||
|
|
||||||
## Structure
|
## Structure
|
||||||
|
|
||||||
The `code-server` script serves an HTTP API to login and start a remote VS Code process.
|
The `code-server` script serves an HTTP API for login and starting a remote VS Code process.
|
||||||
|
|
||||||
The CLI code is in [./src/node](./src/node) and the HTTP routes are implemented in
|
The CLI code is in [./src/node](./src/node) and the HTTP routes are implemented in
|
||||||
[./src/node/app](./src/node/app).
|
[./src/node/app](./src/node/app).
|
||||||
|
|
||||||
Most of the meaty parts are in our VS Code patch which is described next.
|
Most of the meaty parts are in the VS Code patch, which we described next.
|
||||||
|
|
||||||
### VS Code Patch
|
### VS Code Patch
|
||||||
|
|
||||||
Back in v1 of code-server, we had an extensive patch of VS Code that split the codebase
|
In v1 of code-server, we had a patch of VS Code that split the codebase into a front-end
|
||||||
into a frontend and server. The frontend consisted of all UI code and the server ran
|
and a server. The front-end consisted of all UI code, while the server ran the extensions
|
||||||
the extensions and exposed an API to the frontend for file access and everything else
|
and exposed an API to the front-end for file access and all UI needs.
|
||||||
that the UI needed.
|
|
||||||
|
|
||||||
This worked but eventually Microsoft added support to VS Code to run it in the web.
|
Over time, Microsoft added support to VS Code to run it on the web. They have made
|
||||||
They have open sourced the frontend but have kept the server closed source.
|
the front-end open source, but not the server. As such, code-server v2 (and later) uses
|
||||||
|
the VS Code front-end and implements the server. You can find this in
|
||||||
So in interest of piggy backing off their work, v2 and beyond use the VS Code
|
|
||||||
web frontend and fill in the server. This is contained in our
|
|
||||||
[./ci/dev/vscode.patch](../ci/dev/vscode.patch) under the path `src/vs/server`.
|
[./ci/dev/vscode.patch](../ci/dev/vscode.patch) under the path `src/vs/server`.
|
||||||
|
|
||||||
Other notable changes in our patch include:
|
Other notable changes in our patch include:
|
||||||
|
|
||||||
- Add our own build file which includes our code and VS Code's web code.
|
- Adding our build file, which includes our code and VS Code's web code
|
||||||
- Allow multiple extension directories (both user and built-in).
|
- Allowing multiple extension directories (both user and built-in)
|
||||||
- Modify the loader, websocket, webview, service worker, and asset requests to
|
- Modifying the loader, websocket, webview, service worker, and asset requests to
|
||||||
use the URL of the page as a base (and TLS if necessary for the websocket).
|
use the URL of the page as a base (and TLS, if necessary for the websocket)
|
||||||
- Send client-side telemetry through the server.
|
- Sending client-side telemetry through the server
|
||||||
- Allow modification of the display language.
|
- Allowing modification of the display language
|
||||||
- Make it possible for us to load code on the client.
|
- Making it possible for us to load code on the client
|
||||||
- Make extensions work in the browser.
|
- Making extensions work in the browser
|
||||||
- Make it possible to install extensions of any kind.
|
- Making it possible to install extensions of any kind
|
||||||
- Fix getting permanently disconnected when you sleep or hibernate for a while.
|
- Fixing issue with getting disconnected when your machine sleeps or hibernates
|
||||||
- Add connection type to web socket query parameters.
|
- Adding connection type to web socket query parameters
|
||||||
|
|
||||||
Some known issues presently:
|
As the web portion of VS Code matures, we'll be able to shrink and possibly
|
||||||
|
eliminate our patch. In the meantime, upgrading the VS Code version requires
|
||||||
- Creating custom VS Code extensions and debugging them doesn't work.
|
us to ensure that the patch is applied and works as intended. In the future,
|
||||||
- Extension profiling and tips are currently disabled.
|
we'd like to run VS Code unit tests against our builds to ensure that features
|
||||||
|
|
||||||
As the web portion of VS Code matures, we'll be able to shrink and maybe even entirely
|
|
||||||
eliminate our patch. In the meantime, however, upgrading the VS Code version requires
|
|
||||||
ensuring that the patch still applies and has the intended effects.
|
|
||||||
|
|
||||||
To generate a new patch run `yarn vscode:diff`.
|
|
||||||
|
|
||||||
**note**: We have extension docs on the CI and build system at [./ci/README.md](../ci/README.md)
|
|
||||||
|
|
||||||
If functionality doesn't depend on code from VS Code then it should be moved
|
|
||||||
into code-server otherwise it should be in the patch.
|
|
||||||
|
|
||||||
In the future we'd like to run VS Code unit tests against our builds to ensure features
|
|
||||||
work as expected.
|
work as expected.
|
||||||
|
|
||||||
|
To generate a new patch, run `yarn vscode:diff`
|
||||||
|
|
||||||
|
**Note**: We have [extension docs](../ci/README.md) on the CI and build system.
|
||||||
|
|
||||||
|
If the functionality you're working on does NOT depend on code from VS Code, please
|
||||||
|
move it out and into code-server.
|
||||||
|
|
||||||
|
### Currently Known Issues
|
||||||
|
|
||||||
|
- Creating custom VS Code extensions and debugging them doesn't work
|
||||||
|
- Extension profiling and tips are currently disabled
|
||||||
|
|
18
doc/FAQ.md
18
doc/FAQ.md
|
@ -3,6 +3,7 @@
|
||||||
# FAQ
|
# FAQ
|
||||||
|
|
||||||
- [Questions?](#questions)
|
- [Questions?](#questions)
|
||||||
|
- [iPad Status?](#ipad-status)
|
||||||
- [How can I reuse my VS Code configuration?](#how-can-i-reuse-my-vs-code-configuration)
|
- [How can I reuse my VS Code configuration?](#how-can-i-reuse-my-vs-code-configuration)
|
||||||
- [Differences compared to VS Code?](#differences-compared-to-vs-code)
|
- [Differences compared to VS Code?](#differences-compared-to-vs-code)
|
||||||
- [How can I request a missing extension?](#how-can-i-request-a-missing-extension)
|
- [How can I request a missing extension?](#how-can-i-request-a-missing-extension)
|
||||||
|
@ -21,7 +22,6 @@
|
||||||
- [Heartbeat File](#heartbeat-file)
|
- [Heartbeat File](#heartbeat-file)
|
||||||
- [Healthz endpoint](#healthz-endpoint)
|
- [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)
|
|
||||||
- [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)
|
||||||
- [How do I make my keyboard shortcuts work?](#how-do-i-make-my-keyboard-shortcuts-work)
|
- [How do I make my keyboard shortcuts work?](#how-do-i-make-my-keyboard-shortcuts-work)
|
||||||
- [Differences compared to Theia?](#differences-compared-to-theia)
|
- [Differences compared to Theia?](#differences-compared-to-theia)
|
||||||
|
@ -33,6 +33,10 @@
|
||||||
|
|
||||||
Please file all questions and support requests at https://github.com/cdr/code-server/discussions.
|
Please file all questions and support requests at https://github.com/cdr/code-server/discussions.
|
||||||
|
|
||||||
|
## iPad Status?
|
||||||
|
|
||||||
|
Please see [./ipad.md](./ipad.md).
|
||||||
|
|
||||||
## How can I reuse my VS Code configuration?
|
## How can I reuse my VS Code configuration?
|
||||||
|
|
||||||
The very popular [Settings Sync](https://marketplace.visualstudio.com/items?itemName=Shan.code-settings-sync) extension works.
|
The very popular [Settings Sync](https://marketplace.visualstudio.com/items?itemName=Shan.code-settings-sync) extension works.
|
||||||
|
@ -144,6 +148,9 @@ For HTTPS, you can use a self signed certificate by passing in just `--cert` or
|
||||||
pass in an existing certificate by providing the path to `--cert` and the path to
|
pass in an existing certificate by providing the path to `--cert` and the path to
|
||||||
the key with `--cert-key`.
|
the key with `--cert-key`.
|
||||||
|
|
||||||
|
The self signed certificate will be generated into
|
||||||
|
`~/.local/share/code-server/self-signed.crt`.
|
||||||
|
|
||||||
If `code-server` has been passed a certificate it will also respond to HTTPS
|
If `code-server` has been passed a certificate it will also respond to HTTPS
|
||||||
requests and will redirect all HTTP requests to HTTPS.
|
requests and will redirect all HTTP requests to HTTPS.
|
||||||
|
|
||||||
|
@ -279,15 +286,6 @@ The `--config` flag or `$CODE_SERVER_CONFIG` can be used to change the config fi
|
||||||
|
|
||||||
The default location also respects `$XDG_CONFIG_HOME`.
|
The default location also respects `$XDG_CONFIG_HOME`.
|
||||||
|
|
||||||
## Blank screen on iPad?
|
|
||||||
|
|
||||||
Unfortunately at the moment self signed certificates cause a blank screen on iPadOS
|
|
||||||
|
|
||||||
There does seem to be a way to get it to work if you create your own CA and create a
|
|
||||||
certificate using the CA and then import the CA onto your iPad.
|
|
||||||
|
|
||||||
See [#1566](https://github.com/cdr/code-server/issues/1566#issuecomment-623159434).
|
|
||||||
|
|
||||||
## Isn't an install script piped into sh insecure?
|
## Isn't an install script piped into sh insecure?
|
||||||
|
|
||||||
Please give
|
Please give
|
||||||
|
|
|
@ -251,8 +251,7 @@ Visit `https://<your-domain-name>` to access `code-server`. Congratulations!
|
||||||
|
|
||||||
### Self Signed Certificate
|
### Self Signed Certificate
|
||||||
|
|
||||||
**note:** Self signed certificates do not work with iPad and will cause a blank page. You'll
|
**note:** Self signed certificates do not work with iPad normally. See [./ipad.md](./ipad.md) for details.
|
||||||
have to use [Let's Encrypt](#lets-encrypt) instead. See the [FAQ](./FAQ.md#blank-screen-on-ipad).
|
|
||||||
|
|
||||||
Recommended reading: https://security.stackexchange.com/a/8112.
|
Recommended reading: https://security.stackexchange.com/a/8112.
|
||||||
|
|
||||||
|
|
|
@ -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 -->
|
||||||
# Install
|
# Install
|
||||||
|
|
||||||
|
- [Upgrading](#upgrading)
|
||||||
- [install.sh](#installsh)
|
- [install.sh](#installsh)
|
||||||
- [Flags](#flags)
|
- [Flags](#flags)
|
||||||
- [Detection Reference](#detection-reference)
|
- [Detection Reference](#detection-reference)
|
||||||
|
@ -12,12 +13,19 @@
|
||||||
- [macOS](#macos)
|
- [macOS](#macos)
|
||||||
- [Standalone Releases](#standalone-releases)
|
- [Standalone Releases](#standalone-releases)
|
||||||
- [Docker](#docker)
|
- [Docker](#docker)
|
||||||
|
- [helm](#helm)
|
||||||
|
|
||||||
<!-- END doctoc generated TOC please keep comment here to allow auto update -->
|
<!-- END doctoc generated TOC please keep comment here to allow auto update -->
|
||||||
|
|
||||||
This document demonstrates how to install `code-server` on
|
This document demonstrates how to install `code-server` on
|
||||||
various distros and operating systems.
|
various distros and operating systems.
|
||||||
|
|
||||||
|
## Upgrading
|
||||||
|
|
||||||
|
When upgrading you can just install the new version over the old one. code-server
|
||||||
|
maintains all user data in `~/.local/share/code-server` so that it is preserved in between
|
||||||
|
installations.
|
||||||
|
|
||||||
## install.sh
|
## install.sh
|
||||||
|
|
||||||
We have a [script](../install.sh) to install code-server for Linux, macOS and FreeBSD.
|
We have a [script](../install.sh) to install code-server for Linux, macOS and FreeBSD.
|
||||||
|
@ -79,8 +87,8 @@ 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.5.0/code-server_3.5.0_amd64.deb
|
curl -fOL https://github.com/cdr/code-server/releases/download/v3.7.3/code-server_3.7.3_amd64.deb
|
||||||
sudo dpkg -i code-server_3.5.0_amd64.deb
|
sudo dpkg -i code-server_3.7.3_amd64.deb
|
||||||
sudo systemctl enable --now code-server@$USER
|
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
|
||||||
```
|
```
|
||||||
|
@ -88,8 +96,8 @@ sudo systemctl enable --now code-server@$USER
|
||||||
## Fedora, CentOS, RHEL, SUSE
|
## Fedora, CentOS, RHEL, SUSE
|
||||||
|
|
||||||
```bash
|
```bash
|
||||||
curl -fOL https://github.com/cdr/code-server/releases/download/v3.5.0/code-server-3.5.0-amd64.rpm
|
curl -fOL https://github.com/cdr/code-server/releases/download/v3.7.3/code-server-3.7.3-amd64.rpm
|
||||||
sudo rpm -i code-server-3.5.0-amd64.rpm
|
sudo rpm -i code-server-3.7.3-amd64.rpm
|
||||||
sudo systemctl enable --now code-server@$USER
|
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 +166,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.5.0/code-server-3.5.0-linux-amd64.tar.gz \
|
curl -fL https://github.com/cdr/code-server/releases/download/v3.7.3/code-server-3.7.3-linux-amd64.tar.gz \
|
||||||
| tar -C ~/.local/lib -xz
|
| tar -C ~/.local/lib -xz
|
||||||
mv ~/.local/lib/code-server-3.5.0-linux-amd64 ~/.local/lib/code-server-3.5.0
|
mv ~/.local/lib/code-server-3.7.3-linux-amd64 ~/.local/lib/code-server-3.7.3
|
||||||
ln -s ~/.local/lib/code-server-3.5.0/bin/code-server ~/.local/bin/code-server
|
ln -s ~/.local/lib/code-server-3.7.3/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
|
||||||
|
@ -192,3 +200,7 @@ Our official image supports `amd64` and `arm64`.
|
||||||
For `arm32` support there is a popular community maintained alternative:
|
For `arm32` support there is a popular community maintained alternative:
|
||||||
|
|
||||||
https://hub.docker.com/r/linuxserver/code-server
|
https://hub.docker.com/r/linuxserver/code-server
|
||||||
|
|
||||||
|
## helm
|
||||||
|
|
||||||
|
See [the chart](../ci/helm-chart).
|
||||||
|
|
|
@ -0,0 +1,53 @@
|
||||||
|
<!-- START doctoc generated TOC please keep comment here to allow auto update -->
|
||||||
|
<!-- DON'T EDIT THIS SECTION, INSTEAD RE-RUN doctoc TO UPDATE -->
|
||||||
|
# iPad
|
||||||
|
|
||||||
|
- [Known Issues](#known-issues)
|
||||||
|
- [How to access code-server with a self signed certificate on iPad?](#how-to-access-code-server-with-a-self-signed-certificate-on-ipad)
|
||||||
|
|
||||||
|
<!-- END doctoc generated TOC please keep comment here to allow auto update -->
|
||||||
|
|
||||||
|
## Known Issues
|
||||||
|
|
||||||
|
- Getting self signed certificates certificates to work is involved, see below.
|
||||||
|
- Keyboard may disappear sometimes [#1313](https://github.com/cdr/code-server/issues/1313), [#979](https://github.com/cdr/code-server/issues/979)
|
||||||
|
- Trackpad scrolling does not work [#1455](https://github.com/cdr/code-server/issues/1455)
|
||||||
|
- See [issues tagged with the iPad label](https://github.com/cdr/code-server/issues?q=is%3Aopen+is%3Aissue+label%3AiPad) for more.
|
||||||
|
|
||||||
|
## How to access code-server with a self signed certificate on iPad?
|
||||||
|
|
||||||
|
Accessing a self signed certificate on iPad isn't as easy as accepting through all
|
||||||
|
the security warnings. Safari will prevent WebSocket connections unless the certificate
|
||||||
|
is installed as a profile on the device.
|
||||||
|
|
||||||
|
The below assumes you are using the self signed certificate that code-server
|
||||||
|
generates for you. If not, that's fine but you'll have to make sure your certificate
|
||||||
|
abides by the following guidelines from Apple: https://support.apple.com/en-us/HT210176
|
||||||
|
|
||||||
|
**note**: Another undocumented requirement we noticed is that the certificate has to have `basicConstraints=CA:true`.
|
||||||
|
|
||||||
|
The following instructions assume you have code-server installed and running
|
||||||
|
with a self signed certificate. If not, please first go through [./guide.md](./guide.md)!
|
||||||
|
|
||||||
|
**warning**: Your iPad must access code-server via a domain name. It could be local
|
||||||
|
DNS like `mymacbookpro.local` but it must be a domain name. Otherwise Safari will
|
||||||
|
refuse to allow WebSockets to connect.
|
||||||
|
|
||||||
|
1. Your certificate **must** have a subject alt name that matches the hostname
|
||||||
|
at which you will access code-server from your iPad. You can pass this to code-server
|
||||||
|
so that it generates the certificate correctly with `--cert-host`.
|
||||||
|
2. Share your self signed certificate with the iPad.
|
||||||
|
- code-server will print the location of the certificate it has generated in the logs.
|
||||||
|
|
||||||
|
```
|
||||||
|
[2020-10-30T08:55:45.139Z] info - Using generated certificate and key for HTTPS: ~/.local/share/code-server/mymbp_local.crt
|
||||||
|
```
|
||||||
|
|
||||||
|
- You can mail it to yourself or if you have a Mac, it's easiest to just Airdrop to the iPad.
|
||||||
|
|
||||||
|
3. When opening the `*.crt` file, you'll be prompted to go into settings to install.
|
||||||
|
4. Go to `Settings -> General -> Profile`, select the profile and then hit `Install`.
|
||||||
|
- It should say the profile is verified.
|
||||||
|
5. Go to `Settings -> About -> Certificate Trust Settings` and enable full trust for
|
||||||
|
the certificate.
|
||||||
|
6. Now you can access code-server! 🍻
|
64
install.sh
64
install.sh
|
@ -17,27 +17,37 @@ usage() {
|
||||||
Installs code-server for Linux, macOS and FreeBSD.
|
Installs code-server for Linux, macOS and FreeBSD.
|
||||||
It tries to use the system package manager if possible.
|
It tries to use the system package manager if possible.
|
||||||
After successful installation it explains how to start using code-server.
|
After successful installation it explains how to start using code-server.
|
||||||
|
|
||||||
|
Pass in user@host to install code-server on user@host over ssh.
|
||||||
|
The remote host must have internet access.
|
||||||
${not_curl_usage-}
|
${not_curl_usage-}
|
||||||
Usage:
|
Usage:
|
||||||
|
|
||||||
$arg0 [--dry-run] [--version X.X.X] [--method detect] [--prefix ~/.local]
|
$arg0 [--dry-run] [--version X.X.X] [--method detect] \
|
||||||
|
[--prefix ~/.local] [--rsh ssh] [user@host]
|
||||||
|
|
||||||
--dry-run
|
--dry-run
|
||||||
Echo the commands for the install process without running them.
|
Echo the commands for the install process without running them.
|
||||||
|
|
||||||
--version X.X.X
|
--version X.X.X
|
||||||
Install a specific version instead of the latest.
|
Install a specific version instead of the latest.
|
||||||
|
|
||||||
--method [detect | standalone]
|
--method [detect | standalone]
|
||||||
Choose the installation method. Defaults to detect.
|
Choose the installation method. Defaults to detect.
|
||||||
- detect detects the system package manager and tries to use it.
|
- detect detects the system package manager and tries to use it.
|
||||||
Full reference on the process is further below.
|
Full reference on the process is further below.
|
||||||
- standalone installs a standalone release archive into ~/.local
|
- standalone installs a standalone release archive into ~/.local
|
||||||
Add ~/.local/bin to your \$PATH to use it.
|
Add ~/.local/bin to your \$PATH to use it.
|
||||||
|
|
||||||
--prefix <dir>
|
--prefix <dir>
|
||||||
Sets the prefix used by standalone release archives. Defaults to ~/.local
|
Sets the prefix used by standalone release archives. Defaults to ~/.local
|
||||||
The release is unarchived into ~/.local/lib/code-server-X.X.X
|
The release is unarchived into ~/.local/lib/code-server-X.X.X
|
||||||
and the binary symlinked into ~/.local/bin/code-server
|
and the binary symlinked into ~/.local/bin/code-server
|
||||||
To install system wide pass ---prefix=/usr/local
|
To install system wide pass ---prefix=/usr/local
|
||||||
|
|
||||||
|
--rsh <bin>
|
||||||
|
Specifies the remote shell for remote installation. Defaults to ssh.
|
||||||
|
|
||||||
- For Debian, Ubuntu and Raspbian it will install the latest deb package.
|
- For Debian, Ubuntu and Raspbian it will install the latest deb package.
|
||||||
- For Fedora, CentOS, RHEL and openSUSE it will install the latest rpm package.
|
- For Fedora, CentOS, RHEL and openSUSE it will install the latest rpm package.
|
||||||
- For Arch Linux it will install the AUR package.
|
- For Arch Linux it will install the AUR package.
|
||||||
|
@ -100,9 +110,19 @@ main() {
|
||||||
METHOD \
|
METHOD \
|
||||||
STANDALONE_INSTALL_PREFIX \
|
STANDALONE_INSTALL_PREFIX \
|
||||||
VERSION \
|
VERSION \
|
||||||
OPTIONAL
|
OPTIONAL \
|
||||||
|
ALL_FLAGS \
|
||||||
|
RSH_ARGS \
|
||||||
|
RSH
|
||||||
|
|
||||||
|
ALL_FLAGS=""
|
||||||
while [ "$#" -gt 0 ]; do
|
while [ "$#" -gt 0 ]; do
|
||||||
|
case "$1" in
|
||||||
|
-*)
|
||||||
|
ALL_FLAGS="${ALL_FLAGS} $1"
|
||||||
|
;;
|
||||||
|
esac
|
||||||
|
|
||||||
case "$1" in
|
case "$1" in
|
||||||
--dry-run)
|
--dry-run)
|
||||||
DRY_RUN=1
|
DRY_RUN=1
|
||||||
|
@ -128,20 +148,45 @@ main() {
|
||||||
--version=*)
|
--version=*)
|
||||||
VERSION="$(parse_arg "$@")"
|
VERSION="$(parse_arg "$@")"
|
||||||
;;
|
;;
|
||||||
|
--rsh)
|
||||||
|
RSH="$(parse_arg "$@")"
|
||||||
|
shift
|
||||||
|
;;
|
||||||
|
--rsh=*)
|
||||||
|
RSH="$(parse_arg "$@")"
|
||||||
|
;;
|
||||||
-h | --h | -help | --help)
|
-h | --h | -help | --help)
|
||||||
usage
|
usage
|
||||||
exit 0
|
exit 0
|
||||||
;;
|
;;
|
||||||
*)
|
--)
|
||||||
|
shift
|
||||||
|
# We remove the -- added above.
|
||||||
|
ALL_FLAGS="${ALL_FLAGS% --}"
|
||||||
|
RSH_ARGS="$*"
|
||||||
|
break
|
||||||
|
;;
|
||||||
|
-*)
|
||||||
echoerr "Unknown flag $1"
|
echoerr "Unknown flag $1"
|
||||||
echoerr "Run with --help to see usage."
|
echoerr "Run with --help to see usage."
|
||||||
exit 1
|
exit 1
|
||||||
;;
|
;;
|
||||||
|
*)
|
||||||
|
RSH_ARGS="$*"
|
||||||
|
break
|
||||||
|
;;
|
||||||
esac
|
esac
|
||||||
|
|
||||||
shift
|
shift
|
||||||
done
|
done
|
||||||
|
|
||||||
|
if [ "${RSH_ARGS-}" ]; then
|
||||||
|
RSH="${RSH-ssh}"
|
||||||
|
echoh "Installing remotely with $RSH $RSH_ARGS"
|
||||||
|
curl -fsSL https://code-server.dev/install.sh | prefix "$RSH_ARGS" "$RSH" "$RSH_ARGS" sh -s -- "$ALL_FLAGS"
|
||||||
|
return
|
||||||
|
fi
|
||||||
|
|
||||||
VERSION="${VERSION-$(echo_latest_version)}"
|
VERSION="${VERSION-$(echo_latest_version)}"
|
||||||
METHOD="${METHOD-detect}"
|
METHOD="${METHOD-detect}"
|
||||||
if [ "$METHOD" != detect ] && [ "$METHOD" != standalone ]; then
|
if [ "$METHOD" != detect ] && [ "$METHOD" != standalone ]; then
|
||||||
|
@ -446,7 +491,7 @@ arch() {
|
||||||
}
|
}
|
||||||
|
|
||||||
command_exists() {
|
command_exists() {
|
||||||
command -v "$@" > /dev/null 2>&1
|
command -v "$@" > /dev/null
|
||||||
}
|
}
|
||||||
|
|
||||||
sh_c() {
|
sh_c() {
|
||||||
|
@ -500,4 +545,15 @@ humanpath() {
|
||||||
sed "s# $HOME# ~#g; s#\"$HOME#\"\$HOME#g"
|
sed "s# $HOME# ~#g; s#\"$HOME#\"\$HOME#g"
|
||||||
}
|
}
|
||||||
|
|
||||||
|
# We need to make sure we exit with a non zero exit if the command fails.
|
||||||
|
# /bin/sh does not support -o pipefail unfortunately.
|
||||||
|
prefix() {
|
||||||
|
PREFIX="$1"
|
||||||
|
shift
|
||||||
|
fifo="$(mktemp -d)/fifo"
|
||||||
|
mkfifo "$fifo"
|
||||||
|
sed -e "s#^#$PREFIX: #" "$fifo" &
|
||||||
|
"$@" > "$fifo" 2>&1
|
||||||
|
}
|
||||||
|
|
||||||
main "$@"
|
main "$@"
|
||||||
|
|
|
@ -1 +1 @@
|
||||||
Subproject commit a0479759d6e9ea56afa657e454193f72aef85bd0
|
Subproject commit e5a624b788d92b8d34d1392e4c4d9789406efe8f
|
17
package.json
17
package.json
|
@ -1,7 +1,7 @@
|
||||||
{
|
{
|
||||||
"name": "code-server",
|
"name": "code-server",
|
||||||
"license": "MIT",
|
"license": "MIT",
|
||||||
"version": "3.5.0",
|
"version": "3.7.3",
|
||||||
"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": {
|
||||||
|
@ -30,6 +30,9 @@
|
||||||
},
|
},
|
||||||
"main": "out/node/entry.js",
|
"main": "out/node/entry.js",
|
||||||
"devDependencies": {
|
"devDependencies": {
|
||||||
|
"@types/body-parser": "^1.19.0",
|
||||||
|
"@types/cookie-parser": "^1.4.2",
|
||||||
|
"@types/express": "^4.17.8",
|
||||||
"@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",
|
||||||
|
@ -39,11 +42,13 @@
|
||||||
"@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/split2": "^2.1.6",
|
||||||
|
"@types/supertest": "^2.0.10",
|
||||||
"@types/tar-fs": "^2.0.0",
|
"@types/tar-fs": "^2.0.0",
|
||||||
"@types/tar-stream": "^2.1.0",
|
"@types/tar-stream": "^2.1.0",
|
||||||
"@types/ws": "^7.2.6",
|
"@types/ws": "^7.2.6",
|
||||||
"@typescript-eslint/eslint-plugin": "^3.10.1",
|
"@typescript-eslint/eslint-plugin": "^4.7.0",
|
||||||
"@typescript-eslint/parser": "^3.10.1",
|
"@typescript-eslint/parser": "^4.7.0",
|
||||||
"doctoc": "^1.4.0",
|
"doctoc": "^1.4.0",
|
||||||
"eslint": "^7.7.0",
|
"eslint": "^7.7.0",
|
||||||
"eslint-config-prettier": "^6.0.0",
|
"eslint-config-prettier": "^6.0.0",
|
||||||
|
@ -55,6 +60,7 @@
|
||||||
"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",
|
||||||
|
"supertest": "^6.0.1",
|
||||||
"ts-node": "^9.0.0",
|
"ts-node": "^9.0.0",
|
||||||
"typescript": "4.0.2"
|
"typescript": "4.0.2"
|
||||||
},
|
},
|
||||||
|
@ -65,17 +71,22 @@
|
||||||
},
|
},
|
||||||
"dependencies": {
|
"dependencies": {
|
||||||
"@coder/logger": "1.1.16",
|
"@coder/logger": "1.1.16",
|
||||||
|
"body-parser": "^1.19.0",
|
||||||
|
"cookie-parser": "^1.4.5",
|
||||||
"env-paths": "^2.2.0",
|
"env-paths": "^2.2.0",
|
||||||
|
"express": "^5.0.0-alpha.8",
|
||||||
"fs-extra": "^9.0.1",
|
"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",
|
||||||
|
"qs": "6.7.0",
|
||||||
"rotating-file-stream": "^2.1.1",
|
"rotating-file-stream": "^2.1.1",
|
||||||
"safe-buffer": "^5.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",
|
||||||
|
"split2": "^3.2.2",
|
||||||
"tar": "^6.0.1",
|
"tar": "^6.0.1",
|
||||||
"tar-fs": "^2.0.0",
|
"tar-fs": "^2.0.0",
|
||||||
"ws": "^7.2.0",
|
"ws": "^7.2.0",
|
||||||
|
|
|
@ -37,3 +37,7 @@ body {
|
||||||
.login-form > .field > .submit {
|
.login-form > .field > .submit {
|
||||||
margin-left: 20px;
|
margin-left: 20px;
|
||||||
}
|
}
|
||||||
|
|
||||||
|
input {
|
||||||
|
-webkit-appearance: none;
|
||||||
|
}
|
||||||
|
|
|
@ -9,11 +9,6 @@
|
||||||
|
|
||||||
<meta charset="utf-8" />
|
<meta charset="utf-8" />
|
||||||
|
|
||||||
<meta
|
|
||||||
http-equiv="Content-Security-Policy"
|
|
||||||
content="font-src 'self' data:; connect-src ws: wss: 'self' https:; default-src ws: wss: 'self'; style-src 'self' 'unsafe-inline'; script-src 'self' 'unsafe-inline' 'unsafe-eval'; manifest-src 'self'; img-src 'self' data: https:;"
|
|
||||||
/>
|
|
||||||
|
|
||||||
<!-- Disable pinch zooming -->
|
<!-- Disable pinch zooming -->
|
||||||
<meta
|
<meta
|
||||||
name="viewport"
|
name="viewport"
|
||||||
|
@ -37,11 +32,6 @@
|
||||||
<link rel="apple-touch-icon" href="{{CS_STATIC_BASE}}/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 -->
|
|
||||||
<!-- PROD_ONLY
|
|
||||||
<link rel="prefetch" href="{{CS_STATIC_BASE}}/lib/vscode/node_modules/semver-umd/lib/semver-umd.js">
|
|
||||||
END_PROD_ONLY -->
|
|
||||||
|
|
||||||
<meta id="coder-options" data-settings="{{OPTIONS}}" />
|
<meta id="coder-options" data-settings="{{OPTIONS}}" />
|
||||||
</head>
|
</head>
|
||||||
|
|
||||||
|
|
|
@ -31,7 +31,8 @@ try {
|
||||||
}
|
}
|
||||||
|
|
||||||
;(self.require as any) = {
|
;(self.require as any) = {
|
||||||
baseUrl: `${options.csStaticBase}/lib/vscode/out`,
|
// Without the full URL VS Code will try to load file://.
|
||||||
|
baseUrl: `${window.location.origin}${options.csStaticBase}/lib/vscode/out`,
|
||||||
recordStats: true,
|
recordStats: true,
|
||||||
paths: {
|
paths: {
|
||||||
"vscode-textmate": `../node_modules/vscode-textmate/release/main`,
|
"vscode-textmate": `../node_modules/vscode-textmate/release/main`,
|
||||||
|
@ -40,7 +41,7 @@ try {
|
||||||
"xterm-addon-search": `../node_modules/xterm-addon-search/lib/xterm-addon-search.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-unicode11": `../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": `../node_modules/xterm-addon-webgl/lib/xterm-addon-webgl.js`,
|
||||||
"semver-umd": `../node_modules/semver-umd/lib/semver-umd.js`,
|
"tas-client-umd": `../node_modules/tas-client-umd/lib/tas-client-umd.js`,
|
||||||
"iconv-lite-umd": `../node_modules/iconv-lite-umd/lib/iconv-lite-umd.js`,
|
"iconv-lite-umd": `../node_modules/iconv-lite-umd/lib/iconv-lite-umd.js`,
|
||||||
jschardet: `../node_modules/jschardet/dist/jschardet.min.js`,
|
jschardet: `../node_modules/jschardet/dist/jschardet.min.js`,
|
||||||
},
|
},
|
||||||
|
|
|
@ -1,4 +1,10 @@
|
||||||
import { Callback } from "./types"
|
import { logger } from "@coder/logger"
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Event emitter callback. Called with the emitted value and a promise that
|
||||||
|
* resolves when all emitters have finished.
|
||||||
|
*/
|
||||||
|
export type Callback<T, R = void | Promise<void>> = (t: T, p: Promise<void>) => R
|
||||||
|
|
||||||
export interface Disposable {
|
export interface Disposable {
|
||||||
dispose(): void
|
dispose(): void
|
||||||
|
@ -32,8 +38,21 @@ export class Emitter<T> {
|
||||||
/**
|
/**
|
||||||
* Emit an event with a value.
|
* Emit an event with a value.
|
||||||
*/
|
*/
|
||||||
public emit(value: T): void {
|
public async emit(value: T): Promise<void> {
|
||||||
this.listeners.forEach((cb) => cb(value))
|
let resolve: () => void
|
||||||
|
const promise = new Promise<void>((r) => (resolve = r))
|
||||||
|
|
||||||
|
await Promise.all(
|
||||||
|
this.listeners.map(async (cb) => {
|
||||||
|
try {
|
||||||
|
await cb(value, promise)
|
||||||
|
} catch (error) {
|
||||||
|
logger.error(error.message)
|
||||||
|
}
|
||||||
|
}),
|
||||||
|
)
|
||||||
|
|
||||||
|
resolve!()
|
||||||
}
|
}
|
||||||
|
|
||||||
public dispose(): void {
|
public dispose(): void {
|
||||||
|
|
|
@ -8,8 +8,12 @@ export enum HttpCode {
|
||||||
ServerError = 500,
|
ServerError = 500,
|
||||||
}
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Represents an error with a message and an HTTP status code. This code will be
|
||||||
|
* used in the HTTP response.
|
||||||
|
*/
|
||||||
export class HttpError extends Error {
|
export class HttpError extends Error {
|
||||||
public constructor(message: string, public readonly code: number, public readonly details?: object) {
|
public constructor(message: string, public readonly status: HttpCode, public readonly details?: object) {
|
||||||
super(message)
|
super(message)
|
||||||
this.name = this.constructor.name
|
this.name = this.constructor.name
|
||||||
}
|
}
|
||||||
|
|
|
@ -1 +0,0 @@
|
||||||
export type Callback<T, R = void> = (t: T) => R
|
|
|
@ -0,0 +1,61 @@
|
||||||
|
import { logger } from "@coder/logger"
|
||||||
|
import express, { Express } from "express"
|
||||||
|
import { promises as fs } from "fs"
|
||||||
|
import http from "http"
|
||||||
|
import * as httpolyglot from "httpolyglot"
|
||||||
|
import { DefaultedArgs } from "./cli"
|
||||||
|
import { handleUpgrade } from "./wsRouter"
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Create an Express app and an HTTP/S server to serve it.
|
||||||
|
*/
|
||||||
|
export const createApp = async (args: DefaultedArgs): Promise<[Express, Express, http.Server]> => {
|
||||||
|
const app = express()
|
||||||
|
|
||||||
|
const server = args.cert
|
||||||
|
? httpolyglot.createServer(
|
||||||
|
{
|
||||||
|
cert: args.cert && (await fs.readFile(args.cert.value)),
|
||||||
|
key: args["cert-key"] && (await fs.readFile(args["cert-key"])),
|
||||||
|
},
|
||||||
|
app,
|
||||||
|
)
|
||||||
|
: http.createServer(app)
|
||||||
|
|
||||||
|
await new Promise<http.Server>(async (resolve, reject) => {
|
||||||
|
server.on("error", reject)
|
||||||
|
if (args.socket) {
|
||||||
|
try {
|
||||||
|
await fs.unlink(args.socket)
|
||||||
|
} catch (error) {
|
||||||
|
if (error.code !== "ENOENT") {
|
||||||
|
logger.error(error.message)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
server.listen(args.socket, resolve)
|
||||||
|
} else {
|
||||||
|
// [] is the correct format when using :: but Node errors with them.
|
||||||
|
server.listen(args.port, args.host.replace(/^\[|\]$/g, ""), resolve)
|
||||||
|
}
|
||||||
|
})
|
||||||
|
|
||||||
|
const wsApp = express()
|
||||||
|
handleUpgrade(wsApp, server)
|
||||||
|
|
||||||
|
return [app, wsApp, server]
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Get the address of a server as a string (protocol *is* included) while
|
||||||
|
* ensuring there is one (will throw if there isn't).
|
||||||
|
*/
|
||||||
|
export const ensureAddress = (server: http.Server): string => {
|
||||||
|
const addr = server.address()
|
||||||
|
if (!addr) {
|
||||||
|
throw new Error("server has no address")
|
||||||
|
}
|
||||||
|
if (typeof addr !== "string") {
|
||||||
|
return `http://${addr.address}:${addr.port}`
|
||||||
|
}
|
||||||
|
return addr
|
||||||
|
}
|
|
@ -1,21 +0,0 @@
|
||||||
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,
|
|
||||||
},
|
|
||||||
}
|
|
||||||
}
|
|
||||||
}
|
|
|
@ -1,144 +0,0 @@
|
||||||
import * as http from "http"
|
|
||||||
import * as limiter from "limiter"
|
|
||||||
import * as querystring from "querystring"
|
|
||||||
import { HttpCode, HttpError } from "../../common/http"
|
|
||||||
import { AuthType, HttpProvider, HttpProviderOptions, HttpResponse, Route } from "../http"
|
|
||||||
import { hash, humanPath } from "../util"
|
|
||||||
|
|
||||||
interface LoginPayload {
|
|
||||||
password?: string
|
|
||||||
/**
|
|
||||||
* Since we must set a cookie with an absolute path, we need to know the full
|
|
||||||
* base path.
|
|
||||||
*/
|
|
||||||
base?: string
|
|
||||||
}
|
|
||||||
|
|
||||||
/**
|
|
||||||
* Login HTTP provider.
|
|
||||||
*/
|
|
||||||
export class LoginHttpProvider extends HttpProvider {
|
|
||||||
public constructor(
|
|
||||||
options: HttpProviderOptions,
|
|
||||||
private readonly configFile: string,
|
|
||||||
private readonly envPassword: boolean,
|
|
||||||
) {
|
|
||||||
super(options)
|
|
||||||
}
|
|
||||||
|
|
||||||
public async handleRequest(route: Route, request: http.IncomingMessage): Promise<HttpResponse> {
|
|
||||||
if (this.options.auth !== AuthType.Password || !this.isRoot(route)) {
|
|
||||||
throw new HttpError("Not found", HttpCode.NotFound)
|
|
||||||
}
|
|
||||||
switch (route.base) {
|
|
||||||
case "/":
|
|
||||||
switch (request.method) {
|
|
||||||
case "POST":
|
|
||||||
this.ensureMethod(request, ["GET", "POST"])
|
|
||||||
return this.tryLogin(route, request)
|
|
||||||
default:
|
|
||||||
this.ensureMethod(request)
|
|
||||||
if (this.authenticated(request)) {
|
|
||||||
return {
|
|
||||||
redirect: (Array.isArray(route.query.to) ? route.query.to[0] : route.query.to) || "/",
|
|
||||||
query: { to: undefined },
|
|
||||||
}
|
|
||||||
}
|
|
||||||
return this.getRoot(route)
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
throw new HttpError("Not found", HttpCode.NotFound)
|
|
||||||
}
|
|
||||||
|
|
||||||
public async getRoot(route: Route, error?: Error): Promise<HttpResponse> {
|
|
||||||
const response = await this.getUtf8Resource(this.rootPath, "src/browser/pages/login.html")
|
|
||||||
response.content = response.content.replace(/{{ERROR}}/, error ? `<div class="error">${error.message}</div>` : "")
|
|
||||||
let passwordMsg = `Check the config file at ${humanPath(this.configFile)} for the password.`
|
|
||||||
if (this.envPassword) {
|
|
||||||
passwordMsg = "Password was set from $PASSWORD."
|
|
||||||
}
|
|
||||||
response.content = response.content.replace(/{{PASSWORD_MSG}}/g, passwordMsg)
|
|
||||||
return this.replaceTemplates(route, response)
|
|
||||||
}
|
|
||||||
|
|
||||||
private readonly limiter = new RateLimiter()
|
|
||||||
|
|
||||||
/**
|
|
||||||
* Try logging in. On failure, show the login page with an error.
|
|
||||||
*/
|
|
||||||
private async tryLogin(route: Route, request: http.IncomingMessage): Promise<HttpResponse> {
|
|
||||||
// Already authenticated via cookies?
|
|
||||||
const providedPassword = this.authenticated(request)
|
|
||||||
if (providedPassword) {
|
|
||||||
return { code: HttpCode.Ok }
|
|
||||||
}
|
|
||||||
|
|
||||||
try {
|
|
||||||
if (!this.limiter.try()) {
|
|
||||||
throw new Error("Login rate limited!")
|
|
||||||
}
|
|
||||||
|
|
||||||
const data = await this.getData(request)
|
|
||||||
const payload = data ? querystring.parse(data) : {}
|
|
||||||
return await this.login(payload, route, request)
|
|
||||||
} catch (error) {
|
|
||||||
return this.getRoot(route, error)
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
/**
|
|
||||||
* Return a cookie if the user is authenticated otherwise throw an error.
|
|
||||||
*/
|
|
||||||
private async login(payload: LoginPayload, route: Route, request: http.IncomingMessage): Promise<HttpResponse> {
|
|
||||||
const password = this.authenticated(request, {
|
|
||||||
key: typeof payload.password === "string" ? [hash(payload.password)] : undefined,
|
|
||||||
})
|
|
||||||
|
|
||||||
if (password) {
|
|
||||||
return {
|
|
||||||
redirect: (Array.isArray(route.query.to) ? route.query.to[0] : route.query.to) || "/",
|
|
||||||
query: { to: undefined },
|
|
||||||
cookie:
|
|
||||||
typeof password === "string"
|
|
||||||
? {
|
|
||||||
key: "key",
|
|
||||||
value: password,
|
|
||||||
path: payload.base,
|
|
||||||
}
|
|
||||||
: undefined,
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
// Only log if it was an actual login attempt.
|
|
||||||
if (payload && payload.password) {
|
|
||||||
console.error(
|
|
||||||
"Failed login attempt",
|
|
||||||
JSON.stringify({
|
|
||||||
xForwardedFor: request.headers["x-forwarded-for"],
|
|
||||||
remoteAddress: request.connection.remoteAddress,
|
|
||||||
userAgent: request.headers["user-agent"],
|
|
||||||
timestamp: Math.floor(new Date().getTime() / 1000),
|
|
||||||
}),
|
|
||||||
)
|
|
||||||
|
|
||||||
throw new Error("Incorrect password")
|
|
||||||
}
|
|
||||||
|
|
||||||
throw new Error("Missing password")
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
// RateLimiter wraps around the limiter library for logins.
|
|
||||||
// It allows 2 logins every minute and 12 logins every hour.
|
|
||||||
class RateLimiter {
|
|
||||||
private readonly minuteLimiter = new limiter.RateLimiter(2, "minute")
|
|
||||||
private readonly hourLimiter = new limiter.RateLimiter(12, "hour")
|
|
||||||
|
|
||||||
public try(): boolean {
|
|
||||||
if (this.minuteLimiter.tryRemoveTokens(1)) {
|
|
||||||
return true
|
|
||||||
}
|
|
||||||
return this.hourLimiter.tryRemoveTokens(1)
|
|
||||||
}
|
|
||||||
}
|
|
|
@ -1,43 +0,0 @@
|
||||||
import * as http from "http"
|
|
||||||
import { HttpCode, HttpError } from "../../common/http"
|
|
||||||
import { HttpProvider, HttpResponse, Route, WsResponse } from "../http"
|
|
||||||
|
|
||||||
/**
|
|
||||||
* Proxy HTTP provider.
|
|
||||||
*/
|
|
||||||
export class ProxyHttpProvider extends HttpProvider {
|
|
||||||
public async handleRequest(route: Route, request: http.IncomingMessage): Promise<HttpResponse> {
|
|
||||||
if (!this.authenticated(request)) {
|
|
||||||
if (this.isRoot(route)) {
|
|
||||||
return { redirect: "/login", query: { to: route.fullPath } }
|
|
||||||
}
|
|
||||||
throw new HttpError("Unauthorized", HttpCode.Unauthorized)
|
|
||||||
}
|
|
||||||
|
|
||||||
// Ensure there is a trailing slash so relative paths work correctly.
|
|
||||||
if (this.isRoot(route) && !route.fullPath.endsWith("/")) {
|
|
||||||
return {
|
|
||||||
redirect: `${route.fullPath}/`,
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
const port = route.base.replace(/^\//, "")
|
|
||||||
return {
|
|
||||||
proxy: {
|
|
||||||
strip: `${route.providerBase}/${port}`,
|
|
||||||
port,
|
|
||||||
},
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
public async handleWebSocket(route: Route, request: http.IncomingMessage): Promise<WsResponse> {
|
|
||||||
this.ensureAuthenticated(request)
|
|
||||||
const port = route.base.replace(/^\//, "")
|
|
||||||
return {
|
|
||||||
proxy: {
|
|
||||||
strip: `${route.providerBase}/${port}`,
|
|
||||||
port,
|
|
||||||
},
|
|
||||||
}
|
|
||||||
}
|
|
||||||
}
|
|
|
@ -1,73 +0,0 @@
|
||||||
import { field, logger } from "@coder/logger"
|
|
||||||
import * as http from "http"
|
|
||||||
import * as path from "path"
|
|
||||||
import { Readable } from "stream"
|
|
||||||
import * as tarFs from "tar-fs"
|
|
||||||
import * as zlib from "zlib"
|
|
||||||
import { HttpProvider, HttpResponse, Route } from "../http"
|
|
||||||
import { pathToFsPath } from "../util"
|
|
||||||
|
|
||||||
/**
|
|
||||||
* Static file HTTP provider. Static requests do not require authentication if
|
|
||||||
* the resource is in the application's directory except requests to serve a
|
|
||||||
* directory as a tar which always requires authentication.
|
|
||||||
*/
|
|
||||||
export class StaticHttpProvider extends HttpProvider {
|
|
||||||
public async handleRequest(route: Route, request: http.IncomingMessage): Promise<HttpResponse> {
|
|
||||||
this.ensureMethod(request)
|
|
||||||
|
|
||||||
if (typeof route.query.tar === "string") {
|
|
||||||
this.ensureAuthenticated(request)
|
|
||||||
return this.getTarredResource(request, pathToFsPath(route.query.tar))
|
|
||||||
}
|
|
||||||
|
|
||||||
const response = await this.getReplacedResource(request, route)
|
|
||||||
if (!this.isDev) {
|
|
||||||
response.cache = true
|
|
||||||
}
|
|
||||||
return response
|
|
||||||
}
|
|
||||||
|
|
||||||
/**
|
|
||||||
* Return a resource with variables replaced where necessary.
|
|
||||||
*/
|
|
||||||
protected async getReplacedResource(request: http.IncomingMessage, route: Route): Promise<HttpResponse> {
|
|
||||||
// The first part is always the commit (for caching purposes).
|
|
||||||
const split = route.requestPath.split("/").slice(1)
|
|
||||||
|
|
||||||
const resourcePath = path.resolve("/", ...split)
|
|
||||||
|
|
||||||
// Make sure it's in code-server or a plugin.
|
|
||||||
const validPaths = [this.rootPath, process.env.PLUGIN_DIR]
|
|
||||||
if (!validPaths.find((p) => p && resourcePath.startsWith(p))) {
|
|
||||||
this.ensureAuthenticated(request)
|
|
||||||
}
|
|
||||||
|
|
||||||
switch (split[split.length - 1]) {
|
|
||||||
case "manifest.json": {
|
|
||||||
const response = await this.getUtf8Resource(resourcePath)
|
|
||||||
return this.replaceTemplates(route, response)
|
|
||||||
}
|
|
||||||
}
|
|
||||||
return this.getResource(resourcePath)
|
|
||||||
}
|
|
||||||
|
|
||||||
/**
|
|
||||||
* Tar up and stream a directory.
|
|
||||||
*/
|
|
||||||
private async getTarredResource(request: http.IncomingMessage, ...parts: string[]): Promise<HttpResponse> {
|
|
||||||
const filePath = path.join(...parts)
|
|
||||||
let stream: Readable = tarFs.pack(filePath)
|
|
||||||
const headers: http.OutgoingHttpHeaders = {}
|
|
||||||
if (request.headers["accept-encoding"] && request.headers["accept-encoding"].includes("gzip")) {
|
|
||||||
logger.debug("gzipping tar", field("filePath", filePath))
|
|
||||||
const compress = zlib.createGzip()
|
|
||||||
stream.pipe(compress)
|
|
||||||
stream.on("error", (error) => compress.destroy(error))
|
|
||||||
stream.on("close", () => compress.end())
|
|
||||||
stream = compress
|
|
||||||
headers["content-encoding"] = "gzip"
|
|
||||||
}
|
|
||||||
return { stream, filePath, mime: "application/x-tar", cache: true, headers }
|
|
||||||
}
|
|
||||||
}
|
|
|
@ -1,237 +0,0 @@
|
||||||
import { field, logger } from "@coder/logger"
|
|
||||||
import * as cp from "child_process"
|
|
||||||
import * as crypto from "crypto"
|
|
||||||
import * as fs from "fs-extra"
|
|
||||||
import * as http from "http"
|
|
||||||
import * as net from "net"
|
|
||||||
import * as path from "path"
|
|
||||||
import {
|
|
||||||
CodeServerMessage,
|
|
||||||
Options,
|
|
||||||
StartPath,
|
|
||||||
VscodeMessage,
|
|
||||||
VscodeOptions,
|
|
||||||
WorkbenchOptions,
|
|
||||||
} from "../../../lib/vscode/src/vs/server/ipc"
|
|
||||||
import { HttpCode, HttpError } from "../../common/http"
|
|
||||||
import { arrayify, generateUuid } from "../../common/util"
|
|
||||||
import { Args } from "../cli"
|
|
||||||
import { HttpProvider, HttpProviderOptions, HttpResponse, Route } from "../http"
|
|
||||||
import { settings } from "../settings"
|
|
||||||
import { pathToFsPath } from "../util"
|
|
||||||
|
|
||||||
export class VscodeHttpProvider extends HttpProvider {
|
|
||||||
private readonly serverRootPath: string
|
|
||||||
private readonly vsRootPath: string
|
|
||||||
private _vscode?: Promise<cp.ChildProcess>
|
|
||||||
|
|
||||||
public constructor(options: HttpProviderOptions, private readonly args: Args) {
|
|
||||||
super(options)
|
|
||||||
this.vsRootPath = path.resolve(this.rootPath, "lib/vscode")
|
|
||||||
this.serverRootPath = path.join(this.vsRootPath, "out/vs/server")
|
|
||||||
}
|
|
||||||
|
|
||||||
public get running(): boolean {
|
|
||||||
return !!this._vscode
|
|
||||||
}
|
|
||||||
|
|
||||||
public async dispose(): Promise<void> {
|
|
||||||
if (this._vscode) {
|
|
||||||
const vscode = await this._vscode
|
|
||||||
vscode.removeAllListeners()
|
|
||||||
this._vscode = undefined
|
|
||||||
vscode.kill()
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
private async initialize(options: VscodeOptions): Promise<WorkbenchOptions> {
|
|
||||||
const id = generateUuid()
|
|
||||||
const vscode = await this.fork()
|
|
||||||
|
|
||||||
logger.debug("setting up vs code...")
|
|
||||||
return new Promise<WorkbenchOptions>((resolve, reject) => {
|
|
||||||
vscode.once("message", (message: VscodeMessage) => {
|
|
||||||
logger.debug("got message from vs code", field("message", message))
|
|
||||||
return message.type === "options" && message.id === id
|
|
||||||
? resolve(message.options)
|
|
||||||
: reject(new Error("Unexpected response during initialization"))
|
|
||||||
})
|
|
||||||
vscode.once("error", reject)
|
|
||||||
vscode.once("exit", (code) => reject(new Error(`VS Code exited unexpectedly with code ${code}`)))
|
|
||||||
this.send({ type: "init", id, options }, vscode)
|
|
||||||
})
|
|
||||||
}
|
|
||||||
|
|
||||||
private fork(): Promise<cp.ChildProcess> {
|
|
||||||
if (!this._vscode) {
|
|
||||||
logger.debug("forking vs code...")
|
|
||||||
const vscode = cp.fork(path.join(this.serverRootPath, "fork"))
|
|
||||||
vscode.on("error", (error) => {
|
|
||||||
logger.error(error.message)
|
|
||||||
this._vscode = undefined
|
|
||||||
})
|
|
||||||
vscode.on("exit", (code) => {
|
|
||||||
logger.error(`VS Code exited unexpectedly with code ${code}`)
|
|
||||||
this._vscode = undefined
|
|
||||||
})
|
|
||||||
|
|
||||||
this._vscode = new Promise((resolve, reject) => {
|
|
||||||
vscode.once("message", (message: VscodeMessage) => {
|
|
||||||
logger.debug("got message from vs code", field("message", message))
|
|
||||||
return message.type === "ready"
|
|
||||||
? resolve(vscode)
|
|
||||||
: reject(new Error("Unexpected response waiting for ready response"))
|
|
||||||
})
|
|
||||||
vscode.once("error", reject)
|
|
||||||
vscode.once("exit", (code) => reject(new Error(`VS Code exited unexpectedly with code ${code}`)))
|
|
||||||
})
|
|
||||||
}
|
|
||||||
|
|
||||||
return this._vscode
|
|
||||||
}
|
|
||||||
|
|
||||||
public async handleWebSocket(route: Route, request: http.IncomingMessage, socket: net.Socket): Promise<void> {
|
|
||||||
if (!this.authenticated(request)) {
|
|
||||||
throw new Error("not authenticated")
|
|
||||||
}
|
|
||||||
|
|
||||||
// VS Code expects a raw socket. It will handle all the web socket frames.
|
|
||||||
// We just need to handle the initial upgrade.
|
|
||||||
// This magic value is specified by the websocket spec.
|
|
||||||
const magic = "258EAFA5-E914-47DA-95CA-C5AB0DC85B11"
|
|
||||||
const reply = crypto
|
|
||||||
.createHash("sha1")
|
|
||||||
.update(request.headers["sec-websocket-key"] + magic)
|
|
||||||
.digest("base64")
|
|
||||||
socket.write(
|
|
||||||
[
|
|
||||||
"HTTP/1.1 101 Switching Protocols",
|
|
||||||
"Upgrade: websocket",
|
|
||||||
"Connection: Upgrade",
|
|
||||||
`Sec-WebSocket-Accept: ${reply}`,
|
|
||||||
].join("\r\n") + "\r\n\r\n",
|
|
||||||
)
|
|
||||||
|
|
||||||
const vscode = await this._vscode
|
|
||||||
this.send({ type: "socket", query: route.query }, vscode, socket)
|
|
||||||
}
|
|
||||||
|
|
||||||
private send(message: CodeServerMessage, vscode?: cp.ChildProcess, socket?: net.Socket): void {
|
|
||||||
if (!vscode || vscode.killed) {
|
|
||||||
throw new Error("vscode is not running")
|
|
||||||
}
|
|
||||||
vscode.send(message, socket)
|
|
||||||
}
|
|
||||||
|
|
||||||
public async handleRequest(route: Route, request: http.IncomingMessage): Promise<HttpResponse> {
|
|
||||||
this.ensureMethod(request)
|
|
||||||
|
|
||||||
switch (route.base) {
|
|
||||||
case "/":
|
|
||||||
if (!this.isRoot(route)) {
|
|
||||||
throw new HttpError("Not found", HttpCode.NotFound)
|
|
||||||
} else if (!this.authenticated(request)) {
|
|
||||||
return { redirect: "/login", query: { to: route.providerBase } }
|
|
||||||
}
|
|
||||||
try {
|
|
||||||
return await this.getRoot(request, route)
|
|
||||||
} catch (error) {
|
|
||||||
const message = `<div>VS Code failed to load.</div> ${
|
|
||||||
this.isDev
|
|
||||||
? `<div>It might not have finished compiling.</div>` +
|
|
||||||
`Check for <code>Finished <span class="success">compilation</span></code> in the output.`
|
|
||||||
: ""
|
|
||||||
} <br><br>${error}`
|
|
||||||
return this.getErrorRoot(route, "VS Code failed to load", "500", message)
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
this.ensureAuthenticated(request)
|
|
||||||
|
|
||||||
switch (route.base) {
|
|
||||||
case "/resource":
|
|
||||||
case "/vscode-remote-resource":
|
|
||||||
if (typeof route.query.path === "string") {
|
|
||||||
return this.getResource(pathToFsPath(route.query.path))
|
|
||||||
}
|
|
||||||
break
|
|
||||||
case "/webview":
|
|
||||||
if (/^\/vscode-resource/.test(route.requestPath)) {
|
|
||||||
return this.getResource(route.requestPath.replace(/^\/vscode-resource(\/file)?/, ""))
|
|
||||||
}
|
|
||||||
return this.getResource(this.vsRootPath, "out/vs/workbench/contrib/webview/browser/pre", route.requestPath)
|
|
||||||
}
|
|
||||||
|
|
||||||
throw new HttpError("Not found", HttpCode.NotFound)
|
|
||||||
}
|
|
||||||
|
|
||||||
private async getRoot(request: http.IncomingMessage, route: Route): Promise<HttpResponse> {
|
|
||||||
const remoteAuthority = request.headers.host as string
|
|
||||||
const { lastVisited } = await settings.read()
|
|
||||||
const startPath = await this.getFirstPath([
|
|
||||||
{ url: route.query.workspace, workspace: true },
|
|
||||||
{ url: route.query.folder, workspace: false },
|
|
||||||
this.args._ && this.args._.length > 0 ? { url: path.resolve(this.args._[this.args._.length - 1]) } : undefined,
|
|
||||||
lastVisited,
|
|
||||||
])
|
|
||||||
const [response, options] = await Promise.all([
|
|
||||||
await this.getUtf8Resource(this.rootPath, "src/browser/pages/vscode.html"),
|
|
||||||
this.initialize({
|
|
||||||
args: this.args,
|
|
||||||
remoteAuthority,
|
|
||||||
startPath,
|
|
||||||
}),
|
|
||||||
])
|
|
||||||
|
|
||||||
settings.write({
|
|
||||||
lastVisited: startPath || lastVisited, // If startpath is undefined, then fallback to lastVisited
|
|
||||||
query: route.query,
|
|
||||||
})
|
|
||||||
|
|
||||||
if (!this.isDev) {
|
|
||||||
response.content = response.content.replace(/<!-- PROD_ONLY/g, "").replace(/END_PROD_ONLY -->/g, "")
|
|
||||||
}
|
|
||||||
|
|
||||||
options.productConfiguration.codeServerVersion = require("../../../package.json").version
|
|
||||||
|
|
||||||
response.content = response.content
|
|
||||||
.replace(`"{{REMOTE_USER_DATA_URI}}"`, `'${JSON.stringify(options.remoteUserDataUri)}'`)
|
|
||||||
.replace(`"{{PRODUCT_CONFIGURATION}}"`, `'${JSON.stringify(options.productConfiguration)}'`)
|
|
||||||
.replace(`"{{WORKBENCH_WEB_CONFIGURATION}}"`, `'${JSON.stringify(options.workbenchWebConfiguration)}'`)
|
|
||||||
.replace(`"{{NLS_CONFIGURATION}}"`, `'${JSON.stringify(options.nlsConfiguration)}'`)
|
|
||||||
return this.replaceTemplates<Options>(route, response, {
|
|
||||||
disableTelemetry: !!this.args["disable-telemetry"],
|
|
||||||
})
|
|
||||||
}
|
|
||||||
|
|
||||||
/**
|
|
||||||
* Choose the first non-empty path.
|
|
||||||
*/
|
|
||||||
private async getFirstPath(
|
|
||||||
startPaths: Array<{ url?: string | string[]; workspace?: boolean } | undefined>,
|
|
||||||
): Promise<StartPath | undefined> {
|
|
||||||
const isFile = async (path: string): Promise<boolean> => {
|
|
||||||
try {
|
|
||||||
const stat = await fs.stat(path)
|
|
||||||
return stat.isFile()
|
|
||||||
} catch (error) {
|
|
||||||
logger.warn(error.message)
|
|
||||||
return false
|
|
||||||
}
|
|
||||||
}
|
|
||||||
for (let i = 0; i < startPaths.length; ++i) {
|
|
||||||
const startPath = startPaths[i]
|
|
||||||
const url = arrayify(startPath && startPath.url).find((p) => !!p)
|
|
||||||
if (startPath && url) {
|
|
||||||
return {
|
|
||||||
url,
|
|
||||||
// The only time `workspace` is undefined is for the command-line
|
|
||||||
// argument, in which case it's a path (not a URL) so we can stat it
|
|
||||||
// without having to parse it.
|
|
||||||
workspace: typeof startPath.workspace !== "undefined" ? startPath.workspace : await isFile(url),
|
|
||||||
}
|
|
||||||
}
|
|
||||||
}
|
|
||||||
return undefined
|
|
||||||
}
|
|
||||||
}
|
|
291
src/node/cli.ts
291
src/node/cli.ts
|
@ -4,8 +4,12 @@ import yaml from "js-yaml"
|
||||||
import * as os from "os"
|
import * as os from "os"
|
||||||
import * as path from "path"
|
import * as path from "path"
|
||||||
import { Args as VsArgs } from "../../lib/vscode/src/vs/server/ipc"
|
import { Args as VsArgs } from "../../lib/vscode/src/vs/server/ipc"
|
||||||
import { AuthType } from "./http"
|
import { canConnect, generateCertificate, generatePassword, humanPath, paths } from "./util"
|
||||||
import { generatePassword, humanPath, paths } from "./util"
|
|
||||||
|
export enum AuthType {
|
||||||
|
Password = "password",
|
||||||
|
None = "none",
|
||||||
|
}
|
||||||
|
|
||||||
export class Optional<T> {
|
export class Optional<T> {
|
||||||
public constructor(public readonly value?: T) {}
|
public constructor(public readonly value?: T) {}
|
||||||
|
@ -22,31 +26,34 @@ export enum LogLevel {
|
||||||
export class OptionalString extends Optional<string> {}
|
export class OptionalString extends Optional<string> {}
|
||||||
|
|
||||||
export interface Args extends VsArgs {
|
export interface Args extends VsArgs {
|
||||||
readonly config?: string
|
config?: string
|
||||||
readonly auth?: AuthType
|
auth?: AuthType
|
||||||
readonly password?: string
|
password?: string
|
||||||
readonly cert?: OptionalString
|
cert?: OptionalString
|
||||||
readonly "cert-key"?: string
|
"cert-host"?: string
|
||||||
readonly "disable-telemetry"?: boolean
|
"cert-key"?: string
|
||||||
readonly help?: boolean
|
"disable-telemetry"?: boolean
|
||||||
readonly host?: string
|
help?: boolean
|
||||||
readonly json?: boolean
|
host?: string
|
||||||
|
json?: boolean
|
||||||
log?: LogLevel
|
log?: LogLevel
|
||||||
readonly open?: boolean
|
open?: boolean
|
||||||
readonly port?: number
|
port?: number
|
||||||
readonly "bind-addr"?: string
|
"bind-addr"?: string
|
||||||
readonly socket?: string
|
socket?: string
|
||||||
readonly version?: boolean
|
version?: boolean
|
||||||
readonly force?: boolean
|
force?: boolean
|
||||||
readonly "list-extensions"?: boolean
|
"list-extensions"?: boolean
|
||||||
readonly "install-extension"?: string[]
|
"install-extension"?: string[]
|
||||||
readonly "show-versions"?: boolean
|
"show-versions"?: boolean
|
||||||
readonly "uninstall-extension"?: string[]
|
"uninstall-extension"?: string[]
|
||||||
readonly "proxy-domain"?: string[]
|
"proxy-domain"?: string[]
|
||||||
readonly locale?: string
|
locale?: string
|
||||||
readonly _: string[]
|
_: string[]
|
||||||
readonly "reuse-window"?: boolean
|
"reuse-window"?: boolean
|
||||||
readonly "new-window"?: boolean
|
"new-window"?: boolean
|
||||||
|
|
||||||
|
link?: OptionalString
|
||||||
}
|
}
|
||||||
|
|
||||||
interface Option<T> {
|
interface Option<T> {
|
||||||
|
@ -63,6 +70,11 @@ interface Option<T> {
|
||||||
* Description of the option. Leave blank to hide the option.
|
* Description of the option. Leave blank to hide the option.
|
||||||
*/
|
*/
|
||||||
description?: string
|
description?: string
|
||||||
|
|
||||||
|
/**
|
||||||
|
* If marked as beta, the option is not printed unless $CS_BETA is set.
|
||||||
|
*/
|
||||||
|
beta?: boolean
|
||||||
}
|
}
|
||||||
|
|
||||||
type OptionType<T> = T extends boolean
|
type OptionType<T> = T extends boolean
|
||||||
|
@ -94,7 +106,11 @@ const options: Options<Required<Args>> = {
|
||||||
cert: {
|
cert: {
|
||||||
type: OptionalString,
|
type: OptionalString,
|
||||||
path: true,
|
path: true,
|
||||||
description: "Path to certificate. Generated if no path is provided.",
|
description: "Path to certificate. A self signed certificate is generated if none is provided.",
|
||||||
|
},
|
||||||
|
"cert-host": {
|
||||||
|
type: "string",
|
||||||
|
description: "Hostname to use when generating a self signed certificate.",
|
||||||
},
|
},
|
||||||
"cert-key": { type: "string", path: true, description: "Path to certificate key when using non-generated cert." },
|
"cert-key": { type: "string", path: true, description: "Path to certificate key when using non-generated cert." },
|
||||||
"disable-telemetry": { type: "boolean", description: "Disable telemetry." },
|
"disable-telemetry": { type: "boolean", description: "Disable telemetry." },
|
||||||
|
@ -130,7 +146,8 @@ const options: Options<Required<Args>> = {
|
||||||
"install-extension": {
|
"install-extension": {
|
||||||
type: "string[]",
|
type: "string[]",
|
||||||
description:
|
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'.",
|
"Install or update a VS Code extension by id or vsix. The identifier of an extension is `${publisher}.${name}`.\n" +
|
||||||
|
"To install a specific version provide `@${version}`. For example: 'vscode.csharp@1.2.3'.",
|
||||||
},
|
},
|
||||||
"enable-proposed-api": {
|
"enable-proposed-api": {
|
||||||
type: "string[]",
|
type: "string[]",
|
||||||
|
@ -144,17 +161,29 @@ const options: Options<Required<Args>> = {
|
||||||
"new-window": {
|
"new-window": {
|
||||||
type: "boolean",
|
type: "boolean",
|
||||||
short: "n",
|
short: "n",
|
||||||
description: "Force to open a new window. (use with open-in)",
|
description: "Force to open a new window.",
|
||||||
},
|
},
|
||||||
"reuse-window": {
|
"reuse-window": {
|
||||||
type: "boolean",
|
type: "boolean",
|
||||||
short: "r",
|
short: "r",
|
||||||
description: "Force to open a file or folder in an already opened window. (use with open-in)",
|
description: "Force to open a file or folder in an already opened window.",
|
||||||
},
|
},
|
||||||
|
|
||||||
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." },
|
||||||
|
|
||||||
|
link: {
|
||||||
|
type: OptionalString,
|
||||||
|
description: `
|
||||||
|
Securely bind code-server via Coder Cloud with the passed name. You'll get a URL like
|
||||||
|
https://myname.coder-cloud.com at which you can easily access your code-server instance.
|
||||||
|
Authorization is done via GitHub.
|
||||||
|
This is presently beta and requires being accepted for testing.
|
||||||
|
See https://github.com/cdr/code-server/discussions/2137
|
||||||
|
`,
|
||||||
|
beta: true,
|
||||||
|
},
|
||||||
}
|
}
|
||||||
|
|
||||||
export const optionDescriptions = (): string[] => {
|
export const optionDescriptions = (): string[] => {
|
||||||
|
@ -166,12 +195,32 @@ export const optionDescriptions = (): string[] => {
|
||||||
}),
|
}),
|
||||||
{ short: 0, long: 0 },
|
{ short: 0, long: 0 },
|
||||||
)
|
)
|
||||||
return entries.map(
|
return entries
|
||||||
([k, v]) =>
|
.filter(([, v]) => {
|
||||||
`${" ".repeat(widths.short - (v.short ? v.short.length : 0))}${v.short ? `-${v.short}` : " "} --${k}${" ".repeat(
|
// If CS_BETA is set, we show beta options but if not, then we do not want
|
||||||
widths.long - k.length,
|
// to show beta options.
|
||||||
)} ${v.description}${typeof v.type === "object" ? ` [${Object.values(v.type).join(", ")}]` : ""}`,
|
return process.env.CS_BETA || !v.beta
|
||||||
|
})
|
||||||
|
.map(([k, v]) => {
|
||||||
|
const help = `${" ".repeat(widths.short - (v.short ? v.short.length : 0))}${
|
||||||
|
v.short ? `-${v.short}` : " "
|
||||||
|
} --${k} `
|
||||||
|
return (
|
||||||
|
help +
|
||||||
|
v.description
|
||||||
|
?.trim()
|
||||||
|
.split(/\n/)
|
||||||
|
.map((line, i) => {
|
||||||
|
line = line.trim()
|
||||||
|
if (i === 0) {
|
||||||
|
return " ".repeat(widths.long - k.length) + line
|
||||||
|
}
|
||||||
|
return " ".repeat(widths.long + widths.short + 6) + line
|
||||||
|
})
|
||||||
|
.join("\n") +
|
||||||
|
(typeof v.type === "object" ? ` [${Object.values(v.type).join(", ")}]` : "")
|
||||||
)
|
)
|
||||||
|
})
|
||||||
}
|
}
|
||||||
|
|
||||||
export const parse = (
|
export const parse = (
|
||||||
|
@ -285,7 +334,46 @@ export const parse = (
|
||||||
args._.push(arg)
|
args._.push(arg)
|
||||||
}
|
}
|
||||||
|
|
||||||
logger.debug("parsed command line", field("args", args))
|
// If a cert was provided a key must also be provided.
|
||||||
|
if (args.cert && args.cert.value && !args["cert-key"]) {
|
||||||
|
throw new Error("--cert-key is missing")
|
||||||
|
}
|
||||||
|
|
||||||
|
logger.debug(() => ["parsed command line", field("args", { ...args, password: undefined })])
|
||||||
|
|
||||||
|
return args
|
||||||
|
}
|
||||||
|
|
||||||
|
export interface DefaultedArgs extends ConfigArgs {
|
||||||
|
auth: AuthType
|
||||||
|
cert?: {
|
||||||
|
value: string
|
||||||
|
}
|
||||||
|
host: string
|
||||||
|
port: number
|
||||||
|
"proxy-domain": string[]
|
||||||
|
verbose: boolean
|
||||||
|
usingEnvPassword: boolean
|
||||||
|
"extensions-dir": string
|
||||||
|
"user-data-dir": string
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Take CLI and config arguments (optional) and return a single set of arguments
|
||||||
|
* with the defaults set. Arguments from the CLI are prioritized over config
|
||||||
|
* arguments.
|
||||||
|
*/
|
||||||
|
export async function setDefaults(cliArgs: Args, configArgs?: ConfigArgs): Promise<DefaultedArgs> {
|
||||||
|
const args = Object.assign({}, configArgs || {}, cliArgs)
|
||||||
|
|
||||||
|
if (!args["user-data-dir"]) {
|
||||||
|
await copyOldMacOSDataDir()
|
||||||
|
args["user-data-dir"] = paths.data
|
||||||
|
}
|
||||||
|
|
||||||
|
if (!args["extensions-dir"]) {
|
||||||
|
args["extensions-dir"] = path.join(args["user-data-dir"], "extensions")
|
||||||
|
}
|
||||||
|
|
||||||
// --verbose takes priority over --log and --log takes priority over the
|
// --verbose takes priority over --log and --log takes priority over the
|
||||||
// environment variable.
|
// environment variable.
|
||||||
|
@ -326,22 +414,49 @@ export const parse = (
|
||||||
break
|
break
|
||||||
}
|
}
|
||||||
|
|
||||||
return args
|
// Default to using a password.
|
||||||
}
|
if (!args.auth) {
|
||||||
|
args.auth = AuthType.Password
|
||||||
export async function setDefaults(args: Args): Promise<Args> {
|
|
||||||
args = { ...args }
|
|
||||||
|
|
||||||
if (!args["user-data-dir"]) {
|
|
||||||
await copyOldMacOSDataDir()
|
|
||||||
args["user-data-dir"] = paths.data
|
|
||||||
}
|
}
|
||||||
|
|
||||||
if (!args["extensions-dir"]) {
|
const addr = bindAddrFromAllSources(configArgs || { _: [] }, cliArgs)
|
||||||
args["extensions-dir"] = path.join(args["user-data-dir"], "extensions")
|
args.host = addr.host
|
||||||
|
args.port = addr.port
|
||||||
|
|
||||||
|
// If we're being exposed to the cloud, we listen on a random address and
|
||||||
|
// disable auth.
|
||||||
|
if (args.link) {
|
||||||
|
args.host = "localhost"
|
||||||
|
args.port = 0
|
||||||
|
args.socket = undefined
|
||||||
|
args.cert = undefined
|
||||||
|
args.auth = AuthType.None
|
||||||
}
|
}
|
||||||
|
|
||||||
return args
|
if (args.cert && !args.cert.value) {
|
||||||
|
const { cert, certKey } = await generateCertificate(args["cert-host"] || "localhost")
|
||||||
|
args.cert = {
|
||||||
|
value: cert,
|
||||||
|
}
|
||||||
|
args["cert-key"] = certKey
|
||||||
|
}
|
||||||
|
|
||||||
|
const usingEnvPassword = !!process.env.PASSWORD
|
||||||
|
if (process.env.PASSWORD) {
|
||||||
|
args.password = process.env.PASSWORD
|
||||||
|
}
|
||||||
|
|
||||||
|
// Ensure it's not readable by child processes.
|
||||||
|
delete process.env.PASSWORD
|
||||||
|
|
||||||
|
// Filter duplicate proxy domains and remove any leading `*.`.
|
||||||
|
const proxyDomains = new Set((args["proxy-domain"] || []).map((d) => d.replace(/^\*\./, "")))
|
||||||
|
args["proxy-domain"] = Array.from(proxyDomains)
|
||||||
|
|
||||||
|
return {
|
||||||
|
...args,
|
||||||
|
usingEnvPassword,
|
||||||
|
} as DefaultedArgs // TODO: Technically no guarantee this is fulfilled.
|
||||||
}
|
}
|
||||||
|
|
||||||
async function defaultConfigFile(): Promise<string> {
|
async function defaultConfigFile(): Promise<string> {
|
||||||
|
@ -352,12 +467,16 @@ cert: false
|
||||||
`
|
`
|
||||||
}
|
}
|
||||||
|
|
||||||
|
interface ConfigArgs extends Args {
|
||||||
|
config: string
|
||||||
|
}
|
||||||
|
|
||||||
/**
|
/**
|
||||||
* Reads the code-server yaml config file and returns it as Args.
|
* Reads the code-server yaml config file and returns it as Args.
|
||||||
*
|
*
|
||||||
* @param configPath Read the config from configPath instead of $CODE_SERVER_CONFIG or the default.
|
* @param configPath Read the config from configPath instead of $CODE_SERVER_CONFIG or the default.
|
||||||
*/
|
*/
|
||||||
export async function readConfigFile(configPath?: string): Promise<Args> {
|
export async function readConfigFile(configPath?: string): Promise<ConfigArgs> {
|
||||||
if (!configPath) {
|
if (!configPath) {
|
||||||
configPath = process.env.CODE_SERVER_CONFIG
|
configPath = process.env.CODE_SERVER_CONFIG
|
||||||
if (!configPath) {
|
if (!configPath) {
|
||||||
|
@ -370,10 +489,6 @@ 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 && !process.env.VSCODE_IPC_HOOK_CLI) {
|
|
||||||
logger.info(`Using config file ${humanPath(configPath)}`)
|
|
||||||
}
|
|
||||||
|
|
||||||
const configFile = await fs.readFile(configPath)
|
const configFile = await fs.readFile(configPath)
|
||||||
const config = yaml.safeLoad(configFile.toString(), {
|
const config = yaml.safeLoad(configFile.toString(), {
|
||||||
filename: configPath,
|
filename: configPath,
|
||||||
|
@ -399,9 +514,15 @@ export async function readConfigFile(configPath?: string): Promise<Args> {
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
function parseBindAddr(bindAddr: string): [string, number] {
|
function parseBindAddr(bindAddr: string): Addr {
|
||||||
const u = new URL(`http://${bindAddr}`)
|
const u = new URL(`http://${bindAddr}`)
|
||||||
return [u.hostname, parseInt(u.port, 10)]
|
return {
|
||||||
|
host: u.hostname,
|
||||||
|
// With the http scheme 80 will be dropped so assume it's 80 if missing.
|
||||||
|
// This means --bind-addr <addr> without a port will default to 80 as well
|
||||||
|
// and not the code-server default.
|
||||||
|
port: u.port ? parseInt(u.port, 10) : 80,
|
||||||
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
interface Addr {
|
interface Addr {
|
||||||
|
@ -412,7 +533,7 @@ interface Addr {
|
||||||
function bindAddrFromArgs(addr: Addr, args: Args): Addr {
|
function bindAddrFromArgs(addr: Addr, args: Args): Addr {
|
||||||
addr = { ...addr }
|
addr = { ...addr }
|
||||||
if (args["bind-addr"]) {
|
if (args["bind-addr"]) {
|
||||||
;[addr.host, addr.port] = parseBindAddr(args["bind-addr"])
|
addr = parseBindAddr(args["bind-addr"])
|
||||||
}
|
}
|
||||||
if (args.host) {
|
if (args.host) {
|
||||||
addr.host = args.host
|
addr.host = args.host
|
||||||
|
@ -427,16 +548,17 @@ function bindAddrFromArgs(addr: Addr, args: Args): Addr {
|
||||||
return addr
|
return addr
|
||||||
}
|
}
|
||||||
|
|
||||||
export function bindAddrFromAllSources(cliArgs: Args, configArgs: Args): [string, number] {
|
function bindAddrFromAllSources(...argsConfig: Args[]): Addr {
|
||||||
let addr: Addr = {
|
let addr: Addr = {
|
||||||
host: "localhost",
|
host: "localhost",
|
||||||
port: 8080,
|
port: 8080,
|
||||||
}
|
}
|
||||||
|
|
||||||
addr = bindAddrFromArgs(addr, configArgs)
|
for (const args of argsConfig) {
|
||||||
addr = bindAddrFromArgs(addr, cliArgs)
|
addr = bindAddrFromArgs(addr, args)
|
||||||
|
}
|
||||||
|
|
||||||
return [addr.host, addr.port]
|
return addr
|
||||||
}
|
}
|
||||||
|
|
||||||
async function copyOldMacOSDataDir(): Promise<void> {
|
async function copyOldMacOSDataDir(): Promise<void> {
|
||||||
|
@ -453,3 +575,52 @@ async function copyOldMacOSDataDir(): Promise<void> {
|
||||||
await fs.copy(oldDataDir, paths.data)
|
await fs.copy(oldDataDir, paths.data)
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
export const shouldRunVsCodeCli = (args: Args): boolean => {
|
||||||
|
return !!args["list-extensions"] || !!args["install-extension"] || !!args["uninstall-extension"]
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Determine if it looks like the user is trying to open a file or folder in an
|
||||||
|
* existing instance. The arguments here should be the arguments the user
|
||||||
|
* explicitly passed on the command line, not defaults or the configuration.
|
||||||
|
*/
|
||||||
|
export const shouldOpenInExistingInstance = async (args: Args): Promise<string | undefined> => {
|
||||||
|
// Always use the existing instance if we're running from VS Code's terminal.
|
||||||
|
if (process.env.VSCODE_IPC_HOOK_CLI) {
|
||||||
|
return process.env.VSCODE_IPC_HOOK_CLI
|
||||||
|
}
|
||||||
|
|
||||||
|
const readSocketPath = async (): Promise<string | undefined> => {
|
||||||
|
try {
|
||||||
|
return await fs.readFile(path.join(os.tmpdir(), "vscode-ipc"), "utf8")
|
||||||
|
} catch (error) {
|
||||||
|
if (error.code !== "ENOENT") {
|
||||||
|
throw error
|
||||||
|
}
|
||||||
|
}
|
||||||
|
return undefined
|
||||||
|
}
|
||||||
|
|
||||||
|
// If these flags are set then assume the user is trying to open in an
|
||||||
|
// existing instance since these flags have no effect otherwise.
|
||||||
|
const openInFlagCount = ["reuse-window", "new-window"].reduce((prev, cur) => {
|
||||||
|
return args[cur as keyof Args] ? prev + 1 : prev
|
||||||
|
}, 0)
|
||||||
|
if (openInFlagCount > 0) {
|
||||||
|
return readSocketPath()
|
||||||
|
}
|
||||||
|
|
||||||
|
// It's possible the user is trying to spawn another instance of code-server.
|
||||||
|
// Check if any unrelated flags are set (check against one because `_` always
|
||||||
|
// exists), that a file or directory was passed, and that the socket is
|
||||||
|
// active.
|
||||||
|
if (Object.keys(args).length === 1 && args._.length > 0) {
|
||||||
|
const socketPath = await readSocketPath()
|
||||||
|
if (socketPath && (await canConnect(socketPath))) {
|
||||||
|
return socketPath
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
return undefined
|
||||||
|
}
|
||||||
|
|
|
@ -0,0 +1,43 @@
|
||||||
|
import { logger } from "@coder/logger"
|
||||||
|
import { spawn } from "child_process"
|
||||||
|
import path from "path"
|
||||||
|
import split2 from "split2"
|
||||||
|
|
||||||
|
// https://github.com/cdr/coder-cloud
|
||||||
|
const coderCloudAgent = path.resolve(__dirname, "../../lib/coder-cloud-agent")
|
||||||
|
|
||||||
|
function runAgent(...args: string[]): Promise<void> {
|
||||||
|
logger.debug(`running agent with ${args}`)
|
||||||
|
|
||||||
|
const agent = spawn(coderCloudAgent, args, {
|
||||||
|
stdio: ["inherit", "inherit", "pipe"],
|
||||||
|
})
|
||||||
|
|
||||||
|
agent.stderr.pipe(split2()).on("data", (line) => {
|
||||||
|
line = line.replace(/^[0-9-]+ [0-9:]+ [^ ]+\t/, "")
|
||||||
|
logger.info(line)
|
||||||
|
})
|
||||||
|
|
||||||
|
return new Promise((res, rej) => {
|
||||||
|
agent.on("error", rej)
|
||||||
|
|
||||||
|
agent.on("close", (code) => {
|
||||||
|
if (code !== 0) {
|
||||||
|
rej({
|
||||||
|
message: `coder cloud agent exited with ${code}`,
|
||||||
|
})
|
||||||
|
return
|
||||||
|
}
|
||||||
|
res()
|
||||||
|
})
|
||||||
|
})
|
||||||
|
}
|
||||||
|
|
||||||
|
export function coderCloudBind(csAddr: string, serverName = ""): Promise<void> {
|
||||||
|
logger.info("Remember --link is a beta feature and requires being accepted for testing")
|
||||||
|
logger.info("See https://github.com/cdr/code-server/discussions/2137")
|
||||||
|
// addr needs to be in host:port format.
|
||||||
|
// So we trim the protocol.
|
||||||
|
csAddr = csAddr.replace(/^https?:\/\//, "")
|
||||||
|
return runAgent("bind", `--code-server-addr=${csAddr}`, serverName)
|
||||||
|
}
|
|
@ -0,0 +1,13 @@
|
||||||
|
import { logger } from "@coder/logger"
|
||||||
|
import * as path from "path"
|
||||||
|
|
||||||
|
let pkg: { version?: string; commit?: string } = {}
|
||||||
|
try {
|
||||||
|
pkg = require("../../package.json")
|
||||||
|
} catch (error) {
|
||||||
|
logger.warn(error.message)
|
||||||
|
}
|
||||||
|
|
||||||
|
export const version = pkg.version || "development"
|
||||||
|
export const commit = pkg.commit || "development"
|
||||||
|
export const rootPath = path.resolve(__dirname, "../..")
|
|
@ -1,151 +1,175 @@
|
||||||
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 http from "http"
|
||||||
import * as path from "path"
|
import * as path from "path"
|
||||||
import { CliMessage, OpenCommandPipeArgs } from "../../lib/vscode/src/vs/server/ipc"
|
import { CliMessage, OpenCommandPipeArgs } from "../../lib/vscode/src/vs/server/ipc"
|
||||||
import { plural } from "../common/util"
|
import { plural } from "../common/util"
|
||||||
import { HealthHttpProvider } from "./app/health"
|
import { createApp, ensureAddress } from "./app"
|
||||||
import { LoginHttpProvider } from "./app/login"
|
import {
|
||||||
import { ProxyHttpProvider } from "./app/proxy"
|
AuthType,
|
||||||
import { StaticHttpProvider } from "./app/static"
|
DefaultedArgs,
|
||||||
import { UpdateHttpProvider } from "./app/update"
|
optionDescriptions,
|
||||||
import { VscodeHttpProvider } from "./app/vscode"
|
parse,
|
||||||
import { Args, bindAddrFromAllSources, optionDescriptions, parse, readConfigFile, setDefaults } from "./cli"
|
readConfigFile,
|
||||||
import { AuthType, HttpServer, HttpServerOptions } from "./http"
|
setDefaults,
|
||||||
import { loadPlugins } from "./plugin"
|
shouldOpenInExistingInstance,
|
||||||
import { generateCertificate, hash, humanPath, open } from "./util"
|
shouldRunVsCodeCli,
|
||||||
import { ipcMain, wrap } from "./wrapper"
|
} from "./cli"
|
||||||
|
import { coderCloudBind } from "./coder-cloud"
|
||||||
|
import { commit, version } from "./constants"
|
||||||
|
import { register } from "./routes"
|
||||||
|
import { humanPath, isFile, open } from "./util"
|
||||||
|
import { isChild, wrapper } from "./wrapper"
|
||||||
|
|
||||||
process.on("uncaughtException", (error) => {
|
export const runVsCodeCli = (args: DefaultedArgs): void => {
|
||||||
logger.error(`Uncaught exception: ${error.message}`)
|
logger.debug("forking vs code cli...")
|
||||||
if (typeof error.stack !== "undefined") {
|
const vscode = cp.fork(path.resolve(__dirname, "../../lib/vscode/out/vs/server/fork"), [], {
|
||||||
logger.error(error.stack)
|
env: {
|
||||||
|
...process.env,
|
||||||
|
CODE_SERVER_PARENT_PID: process.pid.toString(),
|
||||||
|
},
|
||||||
|
})
|
||||||
|
vscode.once("message", (message: any) => {
|
||||||
|
logger.debug("got message from VS Code", field("message", message))
|
||||||
|
if (message.type !== "ready") {
|
||||||
|
logger.error("Unexpected response waiting for ready response", field("type", message.type))
|
||||||
|
process.exit(1)
|
||||||
}
|
}
|
||||||
})
|
const send: CliMessage = { type: "cli", args }
|
||||||
|
vscode.send(send)
|
||||||
let pkg: { version?: string; commit?: string } = {}
|
})
|
||||||
try {
|
vscode.once("error", (error) => {
|
||||||
pkg = require("../../package.json")
|
logger.error("Got error from VS Code", field("error", error))
|
||||||
} catch (error) {
|
process.exit(1)
|
||||||
logger.warn(error.message)
|
})
|
||||||
|
vscode.on("exit", (code) => process.exit(code || 0))
|
||||||
}
|
}
|
||||||
|
|
||||||
const version = pkg.version || "development"
|
export const openInExistingInstance = async (args: DefaultedArgs, socketPath: string): Promise<void> => {
|
||||||
const commit = pkg.commit || "development"
|
const pipeArgs: OpenCommandPipeArgs & { fileURIs: string[] } = {
|
||||||
|
type: "open",
|
||||||
|
folderURIs: [],
|
||||||
|
fileURIs: [],
|
||||||
|
forceReuseWindow: args["reuse-window"],
|
||||||
|
forceNewWindow: args["new-window"],
|
||||||
|
}
|
||||||
|
|
||||||
const main = async (args: Args, cliArgs: Args, configArgs: Args): Promise<void> => {
|
for (let i = 0; i < args._.length; i++) {
|
||||||
if (!args.auth) {
|
const fp = path.resolve(args._[i])
|
||||||
args = {
|
if (await isFile(fp)) {
|
||||||
...args,
|
pipeArgs.fileURIs.push(fp)
|
||||||
auth: AuthType.Password,
|
} else {
|
||||||
|
pipeArgs.folderURIs.push(fp)
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
if (pipeArgs.forceNewWindow && 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.length === 0) {
|
||||||
|
logger.error("Please specify at least one file or folder")
|
||||||
|
process.exit(1)
|
||||||
|
}
|
||||||
|
|
||||||
|
const vscode = http.request(
|
||||||
|
{
|
||||||
|
path: "/",
|
||||||
|
method: "POST",
|
||||||
|
socketPath,
|
||||||
|
},
|
||||||
|
(response) => {
|
||||||
|
response.on("data", (message) => {
|
||||||
|
logger.debug("got message from VS Code", field("message", message.toString()))
|
||||||
|
})
|
||||||
|
},
|
||||||
|
)
|
||||||
|
vscode.on("error", (error: unknown) => {
|
||||||
|
logger.error("got error from VS Code", field("error", error))
|
||||||
|
})
|
||||||
|
vscode.write(JSON.stringify(pipeArgs))
|
||||||
|
vscode.end()
|
||||||
|
}
|
||||||
|
|
||||||
|
const main = async (args: DefaultedArgs): Promise<void> => {
|
||||||
|
logger.info(`code-server ${version} ${commit}`)
|
||||||
|
|
||||||
logger.info(`Using user-data-dir ${humanPath(args["user-data-dir"])}`)
|
logger.info(`Using user-data-dir ${humanPath(args["user-data-dir"])}`)
|
||||||
|
|
||||||
logger.trace(`Using extensions-dir ${humanPath(args["extensions-dir"])}`)
|
logger.trace(`Using extensions-dir ${humanPath(args["extensions-dir"])}`)
|
||||||
|
|
||||||
const envPassword = !!process.env.PASSWORD
|
if (args.auth === AuthType.Password && !args.password) {
|
||||||
const password = args.auth === AuthType.Password && (process.env.PASSWORD || args.password)
|
|
||||||
if (args.auth === AuthType.Password && !password) {
|
|
||||||
throw new Error("Please pass in a password via the config file or $PASSWORD")
|
throw new Error("Please pass in a password via the config file or $PASSWORD")
|
||||||
}
|
}
|
||||||
const [host, port] = bindAddrFromAllSources(cliArgs, configArgs)
|
|
||||||
|
|
||||||
// Spawn the main HTTP server.
|
const [app, wsApp, server] = await createApp(args)
|
||||||
const options: HttpServerOptions = {
|
const serverAddress = ensureAddress(server)
|
||||||
auth: args.auth,
|
await register(app, wsApp, server, args)
|
||||||
commit,
|
|
||||||
host: host,
|
|
||||||
// The hash does not add any actual security but we do it for obfuscation purposes.
|
|
||||||
password: password ? hash(password) : undefined,
|
|
||||||
port: port,
|
|
||||||
proxyDomains: args["proxy-domain"],
|
|
||||||
socket: args.socket,
|
|
||||||
...(args.cert && !args.cert.value
|
|
||||||
? await generateCertificate()
|
|
||||||
: {
|
|
||||||
cert: args.cert && args.cert.value,
|
|
||||||
certKey: args["cert-key"],
|
|
||||||
}),
|
|
||||||
}
|
|
||||||
|
|
||||||
if (options.cert && !options.certKey) {
|
logger.info(`Using config file ${humanPath(args.config)}`)
|
||||||
throw new Error("--cert-key is missing")
|
logger.info(`HTTP server listening on ${serverAddress} ${args.link ? "(randomized by --link)" : ""}`)
|
||||||
}
|
|
||||||
|
|
||||||
const httpServer = new HttpServer(options)
|
|
||||||
httpServer.registerHttpProvider(["/", "/vscode"], VscodeHttpProvider, args)
|
|
||||||
httpServer.registerHttpProvider("/update", UpdateHttpProvider, false)
|
|
||||||
httpServer.registerHttpProvider("/proxy", ProxyHttpProvider)
|
|
||||||
httpServer.registerHttpProvider("/login", LoginHttpProvider, args.config!, envPassword)
|
|
||||||
httpServer.registerHttpProvider("/static", StaticHttpProvider)
|
|
||||||
httpServer.registerHttpProvider("/healthz", HealthHttpProvider, httpServer.heart)
|
|
||||||
|
|
||||||
await loadPlugins(httpServer, args)
|
|
||||||
|
|
||||||
ipcMain().onDispose(() => {
|
|
||||||
httpServer.dispose().then((errors) => {
|
|
||||||
errors.forEach((error) => logger.error(error.message))
|
|
||||||
})
|
|
||||||
})
|
|
||||||
|
|
||||||
logger.info(`code-server ${version} ${commit}`)
|
|
||||||
const serverAddress = await httpServer.listen()
|
|
||||||
logger.info(`HTTP server listening on ${serverAddress}`)
|
|
||||||
|
|
||||||
if (args.auth === AuthType.Password) {
|
if (args.auth === AuthType.Password) {
|
||||||
if (envPassword) {
|
logger.info(" - Authentication is enabled")
|
||||||
|
if (args.usingEnvPassword) {
|
||||||
logger.info(" - Using password from $PASSWORD")
|
logger.info(" - Using password from $PASSWORD")
|
||||||
} else {
|
} else {
|
||||||
logger.info(` - Using password from ${humanPath(args.config)}`)
|
logger.info(` - Using password from ${humanPath(args.config)}`)
|
||||||
}
|
}
|
||||||
logger.info(" - To disable use `--auth none`")
|
|
||||||
} else {
|
} else {
|
||||||
logger.info(" - No authentication")
|
logger.info(` - Authentication is disabled ${args.link ? "(disabled by --link)" : ""}`)
|
||||||
}
|
}
|
||||||
delete process.env.PASSWORD
|
|
||||||
|
|
||||||
if (httpServer.protocol === "https") {
|
if (args.cert) {
|
||||||
logger.info(
|
logger.info(` - Using certificate for HTTPS: ${humanPath(args.cert.value)}`)
|
||||||
args.cert && args.cert.value
|
|
||||||
? ` - Using provided certificate and key for HTTPS`
|
|
||||||
: ` - Using generated certificate and key for HTTPS`,
|
|
||||||
)
|
|
||||||
} else {
|
} else {
|
||||||
logger.info(" - Not serving HTTPS")
|
logger.info(" - Not serving HTTPS")
|
||||||
}
|
}
|
||||||
|
|
||||||
if (httpServer.proxyDomains.size > 0) {
|
if (args["proxy-domain"].length > 0) {
|
||||||
logger.info(` - ${plural(httpServer.proxyDomains.size, "Proxying the following domain")}:`)
|
logger.info(` - ${plural(args["proxy-domain"].length, "Proxying the following domain")}:`)
|
||||||
httpServer.proxyDomains.forEach((domain) => logger.info(` - *.${domain}`))
|
args["proxy-domain"].forEach((domain) => logger.info(` - *.${domain}`))
|
||||||
}
|
}
|
||||||
|
|
||||||
if (serverAddress && !options.socket && args.open) {
|
if (args.link) {
|
||||||
|
try {
|
||||||
|
await coderCloudBind(serverAddress.replace(/^https?:\/\//, ""), args.link.value)
|
||||||
|
logger.info(" - Connected to cloud agent")
|
||||||
|
} catch (err) {
|
||||||
|
logger.error(err.message)
|
||||||
|
wrapper.exit(1)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
if (!args.socket && args.open) {
|
||||||
// The web socket doesn't seem to work if browsing with 0.0.0.0.
|
// The web socket doesn't seem to work if browsing with 0.0.0.0.
|
||||||
const openAddress = serverAddress.replace(/:\/\/0.0.0.0/, "://localhost")
|
const openAddress = serverAddress.replace("://0.0.0.0", "://localhost")
|
||||||
await open(openAddress).catch(console.error)
|
try {
|
||||||
|
await open(openAddress)
|
||||||
logger.info(`Opened ${openAddress}`)
|
logger.info(`Opened ${openAddress}`)
|
||||||
|
} catch (error) {
|
||||||
|
logger.error("Failed to open", field("address", openAddress), field("error", error))
|
||||||
|
}
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
async function entry(): Promise<void> {
|
async function entry(): Promise<void> {
|
||||||
const tryParse = async (): Promise<[Args, Args, Args]> => {
|
// There's no need to check flags like --help or to spawn in an existing
|
||||||
try {
|
// instance for the child process because these would have already happened in
|
||||||
const cliArgs = parse(process.argv.slice(2))
|
// the parent and the child wouldn't have been spawned. We also get the
|
||||||
const configArgs = await readConfigFile(cliArgs.config)
|
// arguments from the parent so we don't have to parse twice and to account
|
||||||
// This prioritizes the flags set in args over the ones in the config file.
|
// for environment manipulation (like how PASSWORD gets removed to avoid
|
||||||
let args = Object.assign(configArgs, cliArgs)
|
// leaking to child processes).
|
||||||
args = await setDefaults(args)
|
if (isChild(wrapper)) {
|
||||||
return [args, cliArgs, configArgs]
|
const args = await wrapper.handshake()
|
||||||
} catch (error) {
|
wrapper.preventExit()
|
||||||
console.error(error.message)
|
return main(args)
|
||||||
process.exit(1)
|
|
||||||
}
|
|
||||||
}
|
}
|
||||||
|
|
||||||
const [args, cliArgs, configArgs] = await tryParse()
|
const cliArgs = parse(process.argv.slice(2))
|
||||||
|
const configArgs = await readConfigFile(cliArgs.config)
|
||||||
|
const args = await setDefaults(cliArgs, configArgs)
|
||||||
|
|
||||||
if (args.help) {
|
if (args.help) {
|
||||||
console.log("code-server", version, commit)
|
console.log("code-server", version, commit)
|
||||||
console.log("")
|
console.log("")
|
||||||
|
@ -155,7 +179,10 @@ async function entry(): Promise<void> {
|
||||||
optionDescriptions().forEach((description) => {
|
optionDescriptions().forEach((description) => {
|
||||||
console.log("", description)
|
console.log("", description)
|
||||||
})
|
})
|
||||||
} else if (args.version) {
|
return
|
||||||
|
}
|
||||||
|
|
||||||
|
if (args.version) {
|
||||||
if (args.json) {
|
if (args.json) {
|
||||||
console.log({
|
console.log({
|
||||||
codeServer: version,
|
codeServer: version,
|
||||||
|
@ -165,83 +192,22 @@ async function entry(): Promise<void> {
|
||||||
} else {
|
} else {
|
||||||
console.log(version, commit)
|
console.log(version, commit)
|
||||||
}
|
}
|
||||||
process.exit(0)
|
return
|
||||||
} else if (args["list-extensions"] || args["install-extension"] || args["uninstall-extension"]) {
|
|
||||||
logger.debug("forking vs code cli...")
|
|
||||||
const vscode = cp.fork(path.resolve(__dirname, "../../lib/vscode/out/vs/server/fork"), [], {
|
|
||||||
env: {
|
|
||||||
...process.env,
|
|
||||||
CODE_SERVER_PARENT_PID: process.pid.toString(),
|
|
||||||
},
|
|
||||||
})
|
|
||||||
vscode.once("message", (message: any) => {
|
|
||||||
logger.debug("Got message from VS Code", field("message", message))
|
|
||||||
if (message.type !== "ready") {
|
|
||||||
logger.error("Unexpected response waiting for ready response")
|
|
||||||
process.exit(1)
|
|
||||||
}
|
}
|
||||||
const send: CliMessage = { type: "cli", args }
|
|
||||||
vscode.send(send)
|
if (shouldRunVsCodeCli(args)) {
|
||||||
})
|
return runVsCodeCli(args)
|
||||||
vscode.once("error", (error) => {
|
|
||||||
logger.error(error.message)
|
|
||||||
process.exit(1)
|
|
||||||
})
|
|
||||||
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 socketPath = await shouldOpenInExistingInstance(cliArgs)
|
||||||
const st = await fs.stat(path)
|
if (socketPath) {
|
||||||
return st.isDirectory()
|
return openInExistingInstance(args, socketPath)
|
||||||
} 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 {
|
|
||||||
wrap(() => main(args, cliArgs, configArgs))
|
|
||||||
}
|
}
|
||||||
|
|
||||||
|
return wrapper.start(args)
|
||||||
}
|
}
|
||||||
|
|
||||||
entry()
|
entry().catch((error) => {
|
||||||
|
logger.error(error.message)
|
||||||
|
wrapper.exit(error)
|
||||||
|
})
|
||||||
|
|
|
@ -0,0 +1,48 @@
|
||||||
|
import { logger } from "@coder/logger"
|
||||||
|
import { promises as fs } from "fs"
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Provides a heartbeat using a local file to indicate activity.
|
||||||
|
*/
|
||||||
|
export class Heart {
|
||||||
|
private heartbeatTimer?: NodeJS.Timeout
|
||||||
|
private heartbeatInterval = 60000
|
||||||
|
public lastHeartbeat = 0
|
||||||
|
|
||||||
|
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
|
||||||
|
* timeout and start or reset a timer that keeps running as long as there is
|
||||||
|
* activity. Failures are logged as warnings.
|
||||||
|
*/
|
||||||
|
public beat(): void {
|
||||||
|
if (this.alive()) {
|
||||||
|
return
|
||||||
|
}
|
||||||
|
|
||||||
|
logger.trace("heartbeat")
|
||||||
|
fs.writeFile(this.heartbeatPath, "").catch((error) => {
|
||||||
|
logger.warn(error.message)
|
||||||
|
})
|
||||||
|
this.lastHeartbeat = Date.now()
|
||||||
|
if (typeof this.heartbeatTimer !== "undefined") {
|
||||||
|
clearTimeout(this.heartbeatTimer)
|
||||||
|
}
|
||||||
|
this.heartbeatTimer = setTimeout(() => {
|
||||||
|
this.isActive()
|
||||||
|
.then((active) => {
|
||||||
|
if (active) {
|
||||||
|
this.beat()
|
||||||
|
}
|
||||||
|
})
|
||||||
|
.catch((error) => {
|
||||||
|
logger.warn(error.message)
|
||||||
|
})
|
||||||
|
}, this.heartbeatInterval)
|
||||||
|
}
|
||||||
|
}
|
964
src/node/http.ts
964
src/node/http.ts
File diff suppressed because it is too large
Load Diff
|
@ -1,60 +1,249 @@
|
||||||
import { field, logger } from "@coder/logger"
|
import { Logger, field } from "@coder/logger"
|
||||||
|
import * as express from "express"
|
||||||
import * as fs from "fs"
|
import * as fs from "fs"
|
||||||
import * as path from "path"
|
import * as path from "path"
|
||||||
import * as util from "util"
|
import * as semver from "semver"
|
||||||
import { Args } from "./cli"
|
import * as pluginapi from "../../typings/pluginapi"
|
||||||
import { HttpServer } from "./http"
|
import { version } from "./constants"
|
||||||
|
import * as util from "./util"
|
||||||
|
const fsp = fs.promises
|
||||||
|
|
||||||
/* eslint-disable @typescript-eslint/no-var-requires */
|
interface Plugin extends pluginapi.Plugin {
|
||||||
|
/**
|
||||||
|
* These fields are populated from the plugin's package.json
|
||||||
|
* and now guaranteed to exist.
|
||||||
|
*/
|
||||||
|
name: string
|
||||||
|
version: string
|
||||||
|
|
||||||
export type Activate = (httpServer: HttpServer, args: Args) => void
|
/**
|
||||||
|
* path to the node module on the disk.
|
||||||
|
*/
|
||||||
|
modulePath: string
|
||||||
|
}
|
||||||
|
|
||||||
export interface Plugin {
|
interface Application extends pluginapi.Application {
|
||||||
activate: Activate
|
/*
|
||||||
|
* Clone of the above without functions.
|
||||||
|
*/
|
||||||
|
plugin: Omit<Plugin, "init" | "router" | "applications">
|
||||||
}
|
}
|
||||||
|
|
||||||
/**
|
/**
|
||||||
* Intercept imports so we can inject code-server when the plugin tries to
|
* PluginAPI implements the plugin API described in typings/pluginapi.d.ts
|
||||||
* import it.
|
* Please see that file for details.
|
||||||
*/
|
*/
|
||||||
const originalLoad = require("module")._load
|
export class PluginAPI {
|
||||||
// eslint-disable-next-line @typescript-eslint/no-explicit-any
|
private readonly plugins = new Map<string, Plugin>()
|
||||||
require("module")._load = function (request: string, parent: object, isMain: boolean): any {
|
private readonly logger: Logger
|
||||||
return originalLoad.apply(this, [request.replace(/^code-server/, path.resolve(__dirname, "../..")), parent, isMain])
|
|
||||||
}
|
|
||||||
|
|
||||||
const loadPlugin = async (pluginPath: string, httpServer: HttpServer, args: Args): Promise<void> => {
|
public constructor(
|
||||||
|
logger: Logger,
|
||||||
|
/**
|
||||||
|
* These correspond to $CS_PLUGIN_PATH and $CS_PLUGIN respectively.
|
||||||
|
*/
|
||||||
|
private readonly csPlugin = "",
|
||||||
|
private readonly csPluginPath = `${path.join(util.paths.data, "plugins")}:/usr/share/code-server/plugins`,
|
||||||
|
) {
|
||||||
|
this.logger = logger.named("pluginapi")
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* applications grabs the full list of applications from
|
||||||
|
* all loaded plugins.
|
||||||
|
*/
|
||||||
|
public async applications(): Promise<Application[]> {
|
||||||
|
const apps = new Array<Application>()
|
||||||
|
for (const [, p] of this.plugins) {
|
||||||
|
if (!p.applications) {
|
||||||
|
continue
|
||||||
|
}
|
||||||
|
const pluginApps = await p.applications()
|
||||||
|
|
||||||
|
// Add plugin key to each app.
|
||||||
|
apps.push(
|
||||||
|
...pluginApps.map((app) => {
|
||||||
|
app = { ...app, path: path.join(p.routerPath, app.path || "") }
|
||||||
|
app = { ...app, iconPath: path.join(app.path || "", app.iconPath) }
|
||||||
|
return {
|
||||||
|
...app,
|
||||||
|
plugin: {
|
||||||
|
name: p.name,
|
||||||
|
version: p.version,
|
||||||
|
modulePath: p.modulePath,
|
||||||
|
|
||||||
|
displayName: p.displayName,
|
||||||
|
description: p.description,
|
||||||
|
routerPath: p.routerPath,
|
||||||
|
homepageURL: p.homepageURL,
|
||||||
|
},
|
||||||
|
}
|
||||||
|
}),
|
||||||
|
)
|
||||||
|
}
|
||||||
|
return apps
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* mount mounts all plugin routers onto r.
|
||||||
|
*/
|
||||||
|
public mount(r: express.Router): void {
|
||||||
|
for (const [, p] of this.plugins) {
|
||||||
|
if (!p.router) {
|
||||||
|
continue
|
||||||
|
}
|
||||||
|
r.use(`${p.routerPath}`, p.router())
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* loadPlugins loads all plugins based on this.csPlugin,
|
||||||
|
* this.csPluginPath and the built in plugins.
|
||||||
|
*/
|
||||||
|
public async loadPlugins(): Promise<void> {
|
||||||
|
for (const dir of this.csPlugin.split(":")) {
|
||||||
|
if (!dir) {
|
||||||
|
continue
|
||||||
|
}
|
||||||
|
await this.loadPlugin(dir)
|
||||||
|
}
|
||||||
|
|
||||||
|
for (const dir of this.csPluginPath.split(":")) {
|
||||||
|
if (!dir) {
|
||||||
|
continue
|
||||||
|
}
|
||||||
|
await this._loadPlugins(dir)
|
||||||
|
}
|
||||||
|
|
||||||
|
// Built-in plugins.
|
||||||
|
await this._loadPlugins(path.join(__dirname, "../../plugins"))
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* _loadPlugins is the counterpart to loadPlugins.
|
||||||
|
*
|
||||||
|
* It differs in that it loads all plugins in a single
|
||||||
|
* directory whereas loadPlugins uses all available directories
|
||||||
|
* as documented.
|
||||||
|
*/
|
||||||
|
private async _loadPlugins(dir: string): Promise<void> {
|
||||||
try {
|
try {
|
||||||
const plugin: Plugin = require(pluginPath)
|
const entries = await fsp.readdir(dir, { withFileTypes: true })
|
||||||
plugin.activate(httpServer, args)
|
for (const ent of entries) {
|
||||||
logger.debug("Loaded plugin", field("name", path.basename(pluginPath)))
|
if (!ent.isDirectory()) {
|
||||||
} catch (error) {
|
continue
|
||||||
if (error.code !== "MODULE_NOT_FOUND") {
|
}
|
||||||
logger.warn(error.message)
|
await this.loadPlugin(path.join(dir, ent.name))
|
||||||
} else {
|
}
|
||||||
logger.error(error.message)
|
} catch (err) {
|
||||||
|
if (err.code !== "ENOENT") {
|
||||||
|
this.logger.warn(`failed to load plugins from ${q(dir)}: ${err.message}`)
|
||||||
|
}
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
}
|
|
||||||
|
|
||||||
const _loadPlugins = async (httpServer: HttpServer, args: Args): Promise<void> => {
|
private async loadPlugin(dir: string): Promise<void> {
|
||||||
const pluginPath = path.resolve(__dirname, "../../plugins")
|
try {
|
||||||
const files = await util.promisify(fs.readdir)(pluginPath, {
|
const str = await fsp.readFile(path.join(dir, "package.json"), {
|
||||||
withFileTypes: true,
|
encoding: "utf8",
|
||||||
})
|
})
|
||||||
await Promise.all(files.map((file) => loadPlugin(path.join(pluginPath, file.name), httpServer, args)))
|
const packageJSON: PackageJSON = JSON.parse(str)
|
||||||
}
|
for (const [, p] of this.plugins) {
|
||||||
|
if (p.name === packageJSON.name) {
|
||||||
export const loadPlugins = async (httpServer: HttpServer, args: Args): Promise<void> => {
|
this.logger.warn(
|
||||||
try {
|
`ignoring duplicate plugin ${q(p.name)} at ${q(dir)}, using previously loaded ${q(p.modulePath)}`,
|
||||||
await _loadPlugins(httpServer, args)
|
)
|
||||||
} catch (error) {
|
return
|
||||||
if (error.code !== "ENOENT") {
|
}
|
||||||
logger.warn(error.message)
|
}
|
||||||
|
const p = this._loadPlugin(dir, packageJSON)
|
||||||
|
this.plugins.set(p.name, p)
|
||||||
|
} catch (err) {
|
||||||
|
if (err.code !== "ENOENT") {
|
||||||
|
this.logger.warn(`failed to load plugin: ${err.stack}`)
|
||||||
|
}
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
if (process.env.PLUGIN_DIR) {
|
/**
|
||||||
await loadPlugin(process.env.PLUGIN_DIR, httpServer, args)
|
* _loadPlugin is the counterpart to loadPlugin and actually
|
||||||
|
* loads the plugin now that we know there is no duplicate
|
||||||
|
* and that the package.json has been read.
|
||||||
|
*/
|
||||||
|
private _loadPlugin(dir: string, packageJSON: PackageJSON): Plugin {
|
||||||
|
dir = path.resolve(dir)
|
||||||
|
|
||||||
|
const logger = this.logger.named(packageJSON.name)
|
||||||
|
logger.debug("loading plugin", field("plugin_dir", dir), field("package_json", packageJSON))
|
||||||
|
|
||||||
|
if (!packageJSON.name) {
|
||||||
|
throw new Error("plugin package.json missing name")
|
||||||
|
}
|
||||||
|
if (!packageJSON.version) {
|
||||||
|
throw new Error("plugin package.json missing version")
|
||||||
|
}
|
||||||
|
if (!packageJSON.engines || !packageJSON.engines["code-server"]) {
|
||||||
|
throw new Error(`plugin package.json missing code-server range like:
|
||||||
|
"engines": {
|
||||||
|
"code-server": "^3.7.0"
|
||||||
|
}
|
||||||
|
`)
|
||||||
|
}
|
||||||
|
if (!semver.satisfies(version, packageJSON.engines["code-server"])) {
|
||||||
|
throw new Error(
|
||||||
|
`plugin range ${q(packageJSON.engines["code-server"])} incompatible` + ` with code-server version ${version}`,
|
||||||
|
)
|
||||||
|
}
|
||||||
|
|
||||||
|
const pluginModule = require(dir)
|
||||||
|
if (!pluginModule.plugin) {
|
||||||
|
throw new Error("plugin module does not export a plugin")
|
||||||
|
}
|
||||||
|
|
||||||
|
const p = {
|
||||||
|
name: packageJSON.name,
|
||||||
|
version: packageJSON.version,
|
||||||
|
modulePath: dir,
|
||||||
|
...pluginModule.plugin,
|
||||||
|
} as Plugin
|
||||||
|
|
||||||
|
if (!p.displayName) {
|
||||||
|
throw new Error("plugin missing displayName")
|
||||||
|
}
|
||||||
|
if (!p.description) {
|
||||||
|
throw new Error("plugin missing description")
|
||||||
|
}
|
||||||
|
if (!p.routerPath) {
|
||||||
|
throw new Error("plugin missing router path")
|
||||||
|
}
|
||||||
|
if (!p.routerPath.startsWith("/") || p.routerPath.length < 2) {
|
||||||
|
throw new Error(`plugin router path ${q(p.routerPath)}: invalid`)
|
||||||
|
}
|
||||||
|
if (!p.homepageURL) {
|
||||||
|
throw new Error("plugin missing homepage")
|
||||||
|
}
|
||||||
|
|
||||||
|
p.init({
|
||||||
|
logger: logger,
|
||||||
|
})
|
||||||
|
|
||||||
|
logger.debug("loaded")
|
||||||
|
|
||||||
|
return p
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
interface PackageJSON {
|
||||||
|
name: string
|
||||||
|
version: string
|
||||||
|
engines: {
|
||||||
|
"code-server": string
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
function q(s: string | undefined): string {
|
||||||
|
if (s === undefined) {
|
||||||
|
s = "undefined"
|
||||||
|
}
|
||||||
|
return JSON.stringify(s)
|
||||||
|
}
|
||||||
|
|
|
@ -0,0 +1,16 @@
|
||||||
|
import proxyServer from "http-proxy"
|
||||||
|
import { HttpCode } from "../common/http"
|
||||||
|
|
||||||
|
export const proxy = proxyServer.createProxyServer({})
|
||||||
|
|
||||||
|
proxy.on("error", (error, _, res) => {
|
||||||
|
res.writeHead(HttpCode.ServerError)
|
||||||
|
res.end(error.message)
|
||||||
|
})
|
||||||
|
|
||||||
|
// Intercept the response to rewrite absolute redirects against the base path.
|
||||||
|
proxy.on("proxyRes", (res, req) => {
|
||||||
|
if (res.headers.location && res.headers.location.startsWith("/") && (req as any).base) {
|
||||||
|
res.headers.location = (req as any).base + res.headers.location
|
||||||
|
}
|
||||||
|
})
|
|
@ -0,0 +1,17 @@
|
||||||
|
import * as express from "express"
|
||||||
|
import { PluginAPI } from "../plugin"
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Implements the /api/applications endpoint
|
||||||
|
*
|
||||||
|
* See typings/pluginapi.d.ts for details.
|
||||||
|
*/
|
||||||
|
export function router(papi: PluginAPI): express.Router {
|
||||||
|
const router = express.Router()
|
||||||
|
|
||||||
|
router.get("/", async (req, res) => {
|
||||||
|
res.json(await papi.applications())
|
||||||
|
})
|
||||||
|
|
||||||
|
return router
|
||||||
|
}
|
|
@ -0,0 +1,89 @@
|
||||||
|
import { Request, Router } from "express"
|
||||||
|
import { HttpCode, HttpError } from "../../common/http"
|
||||||
|
import { normalize } from "../../common/util"
|
||||||
|
import { authenticated, ensureAuthenticated, redirect } from "../http"
|
||||||
|
import { proxy } from "../proxy"
|
||||||
|
import { Router as WsRouter } from "../wsRouter"
|
||||||
|
|
||||||
|
export const router = Router()
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Return the port if the request should be proxied. Anything that ends in a
|
||||||
|
* proxy domain and has a *single* subdomain should be proxied. Anything else
|
||||||
|
* should return `undefined` and will be handled as normal.
|
||||||
|
*
|
||||||
|
* 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.
|
||||||
|
*/
|
||||||
|
const maybeProxy = (req: Request): string | undefined => {
|
||||||
|
// Split into parts.
|
||||||
|
const host = req.headers.host || ""
|
||||||
|
const idx = host.indexOf(":")
|
||||||
|
const domain = idx !== -1 ? host.substring(0, idx) : host
|
||||||
|
const parts = domain.split(".")
|
||||||
|
|
||||||
|
// There must be an exact match.
|
||||||
|
const port = parts.shift()
|
||||||
|
const proxyDomain = parts.join(".")
|
||||||
|
if (!port || !req.args["proxy-domain"].includes(proxyDomain)) {
|
||||||
|
return undefined
|
||||||
|
}
|
||||||
|
|
||||||
|
return port
|
||||||
|
}
|
||||||
|
|
||||||
|
router.all("*", (req, res, next) => {
|
||||||
|
const port = maybeProxy(req)
|
||||||
|
if (!port) {
|
||||||
|
return next()
|
||||||
|
}
|
||||||
|
|
||||||
|
// Must be authenticated to use the proxy.
|
||||||
|
if (!authenticated(req)) {
|
||||||
|
// Let the assets through since they're used on the login page.
|
||||||
|
if (req.path.startsWith("/static/") && req.method === "GET") {
|
||||||
|
return next()
|
||||||
|
}
|
||||||
|
|
||||||
|
// Assume anything that explicitly accepts text/html is a user browsing a
|
||||||
|
// page (as opposed to an xhr request). Don't use `req.accepts()` since
|
||||||
|
// *every* request that I've seen (in Firefox and Chromium at least)
|
||||||
|
// includes `*/*` making it always truthy. Even for css/javascript.
|
||||||
|
if (req.headers.accept && req.headers.accept.includes("text/html")) {
|
||||||
|
// Let the login through.
|
||||||
|
if (/\/login\/?/.test(req.path)) {
|
||||||
|
return next()
|
||||||
|
}
|
||||||
|
// Redirect all other pages to the login.
|
||||||
|
const to = normalize(`${req.baseUrl}${req.path}`)
|
||||||
|
return redirect(req, res, "login", {
|
||||||
|
to: to !== "/" ? to : undefined,
|
||||||
|
})
|
||||||
|
}
|
||||||
|
|
||||||
|
// Everything else gets an unauthorized message.
|
||||||
|
throw new HttpError("Unauthorized", HttpCode.Unauthorized)
|
||||||
|
}
|
||||||
|
|
||||||
|
proxy.web(req, res, {
|
||||||
|
ignorePath: true,
|
||||||
|
target: `http://0.0.0.0:${port}${req.originalUrl}`,
|
||||||
|
})
|
||||||
|
})
|
||||||
|
|
||||||
|
export const wsRouter = WsRouter()
|
||||||
|
|
||||||
|
wsRouter.ws("*", (req, _, next) => {
|
||||||
|
const port = maybeProxy(req)
|
||||||
|
if (!port) {
|
||||||
|
return next()
|
||||||
|
}
|
||||||
|
|
||||||
|
// Must be authenticated to use the proxy.
|
||||||
|
ensureAuthenticated(req)
|
||||||
|
|
||||||
|
proxy.ws(req, req.ws, req.head, {
|
||||||
|
ignorePath: true,
|
||||||
|
target: `http://0.0.0.0:${port}${req.originalUrl}`,
|
||||||
|
})
|
||||||
|
})
|
|
@ -0,0 +1,10 @@
|
||||||
|
import { Router } from "express"
|
||||||
|
|
||||||
|
export const router = Router()
|
||||||
|
|
||||||
|
router.get("/", (req, res) => {
|
||||||
|
res.json({
|
||||||
|
status: req.heart.alive() ? "alive" : "expired",
|
||||||
|
lastHeartbeat: req.heart.lastHeartbeat,
|
||||||
|
})
|
||||||
|
})
|
|
@ -0,0 +1,170 @@
|
||||||
|
import { logger } from "@coder/logger"
|
||||||
|
import bodyParser from "body-parser"
|
||||||
|
import cookieParser from "cookie-parser"
|
||||||
|
import * as express from "express"
|
||||||
|
import { promises as fs } from "fs"
|
||||||
|
import http from "http"
|
||||||
|
import * as path from "path"
|
||||||
|
import * as tls from "tls"
|
||||||
|
import { HttpCode, HttpError } from "../../common/http"
|
||||||
|
import { plural } from "../../common/util"
|
||||||
|
import { AuthType, DefaultedArgs } from "../cli"
|
||||||
|
import { rootPath } from "../constants"
|
||||||
|
import { Heart } from "../heart"
|
||||||
|
import { replaceTemplates } from "../http"
|
||||||
|
import { PluginAPI } from "../plugin"
|
||||||
|
import { getMediaMime, paths } from "../util"
|
||||||
|
import { WebsocketRequest } from "../wsRouter"
|
||||||
|
import * as apps from "./apps"
|
||||||
|
import * as domainProxy from "./domainProxy"
|
||||||
|
import * as health from "./health"
|
||||||
|
import * as login from "./login"
|
||||||
|
import * as proxy from "./pathProxy"
|
||||||
|
// static is a reserved keyword.
|
||||||
|
import * as _static from "./static"
|
||||||
|
import * as update from "./update"
|
||||||
|
import * as vscode from "./vscode"
|
||||||
|
|
||||||
|
declare global {
|
||||||
|
// eslint-disable-next-line @typescript-eslint/no-namespace
|
||||||
|
namespace Express {
|
||||||
|
export interface Request {
|
||||||
|
args: DefaultedArgs
|
||||||
|
heart: Heart
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Register all routes and middleware.
|
||||||
|
*/
|
||||||
|
export const register = async (
|
||||||
|
app: express.Express,
|
||||||
|
wsApp: express.Express,
|
||||||
|
server: http.Server,
|
||||||
|
args: DefaultedArgs,
|
||||||
|
): Promise<void> => {
|
||||||
|
const heart = new Heart(path.join(paths.data, "heartbeat"), async () => {
|
||||||
|
return new Promise((resolve, reject) => {
|
||||||
|
server.getConnections((error, count) => {
|
||||||
|
if (error) {
|
||||||
|
return reject(error)
|
||||||
|
}
|
||||||
|
logger.trace(plural(count, `${count} active connection`))
|
||||||
|
resolve(count > 0)
|
||||||
|
})
|
||||||
|
})
|
||||||
|
})
|
||||||
|
|
||||||
|
app.disable("x-powered-by")
|
||||||
|
wsApp.disable("x-powered-by")
|
||||||
|
|
||||||
|
app.use(cookieParser())
|
||||||
|
wsApp.use(cookieParser())
|
||||||
|
|
||||||
|
app.use(bodyParser.json())
|
||||||
|
app.use(bodyParser.urlencoded({ extended: true }))
|
||||||
|
|
||||||
|
const common: express.RequestHandler = (req, _, next) => {
|
||||||
|
// /healthz|/healthz/ needs to be excluded otherwise health checks will make
|
||||||
|
// it look like code-server is always in use.
|
||||||
|
if (!/^\/healthz\/?$/.test(req.url)) {
|
||||||
|
heart.beat()
|
||||||
|
}
|
||||||
|
|
||||||
|
// Add common variables routes can use.
|
||||||
|
req.args = args
|
||||||
|
req.heart = heart
|
||||||
|
|
||||||
|
next()
|
||||||
|
}
|
||||||
|
|
||||||
|
app.use(common)
|
||||||
|
wsApp.use(common)
|
||||||
|
|
||||||
|
app.use(async (req, res, next) => {
|
||||||
|
// If we're handling TLS ensure all requests are redirected to HTTPS.
|
||||||
|
// TODO: This does *NOT* work if you have a base path since to specify the
|
||||||
|
// protocol we need to specify the whole path.
|
||||||
|
if (args.cert && !(req.connection as tls.TLSSocket).encrypted) {
|
||||||
|
return res.redirect(`https://${req.headers.host}${req.originalUrl}`)
|
||||||
|
}
|
||||||
|
|
||||||
|
// Return robots.txt.
|
||||||
|
if (req.originalUrl === "/robots.txt") {
|
||||||
|
const resourcePath = path.resolve(rootPath, "src/browser/robots.txt")
|
||||||
|
res.set("Content-Type", getMediaMime(resourcePath))
|
||||||
|
return res.send(await fs.readFile(resourcePath))
|
||||||
|
}
|
||||||
|
|
||||||
|
next()
|
||||||
|
})
|
||||||
|
|
||||||
|
app.use("/", domainProxy.router)
|
||||||
|
wsApp.use("/", domainProxy.wsRouter.router)
|
||||||
|
|
||||||
|
app.use("/", vscode.router)
|
||||||
|
wsApp.use("/", vscode.wsRouter.router)
|
||||||
|
app.use("/vscode", vscode.router)
|
||||||
|
wsApp.use("/vscode", vscode.wsRouter.router)
|
||||||
|
|
||||||
|
app.use("/healthz", health.router)
|
||||||
|
|
||||||
|
if (args.auth === AuthType.Password) {
|
||||||
|
app.use("/login", login.router)
|
||||||
|
}
|
||||||
|
|
||||||
|
app.use("/proxy", proxy.router)
|
||||||
|
wsApp.use("/proxy", proxy.wsRouter.router)
|
||||||
|
|
||||||
|
app.use("/static", _static.router)
|
||||||
|
app.use("/update", update.router)
|
||||||
|
|
||||||
|
const papi = new PluginAPI(logger, process.env.CS_PLUGIN, process.env.CS_PLUGIN_PATH)
|
||||||
|
await papi.loadPlugins()
|
||||||
|
papi.mount(app)
|
||||||
|
app.use("/api/applications", apps.router(papi))
|
||||||
|
|
||||||
|
app.use(() => {
|
||||||
|
throw new HttpError("Not Found", HttpCode.NotFound)
|
||||||
|
})
|
||||||
|
|
||||||
|
const errorHandler: express.ErrorRequestHandler = async (err, req, res, next) => {
|
||||||
|
if (err.code === "ENOENT" || err.code === "EISDIR") {
|
||||||
|
err.status = HttpCode.NotFound
|
||||||
|
}
|
||||||
|
|
||||||
|
const status = err.status ?? err.statusCode ?? 500
|
||||||
|
res.status(status)
|
||||||
|
|
||||||
|
// Assume anything that explicitly accepts text/html is a user browsing a
|
||||||
|
// page (as opposed to an xhr request). Don't use `req.accepts()` since
|
||||||
|
// *every* request that I've seen (in Firefox and Chromium at least)
|
||||||
|
// includes `*/*` making it always truthy. Even for css/javascript.
|
||||||
|
if (req.headers.accept && req.headers.accept.includes("text/html")) {
|
||||||
|
const resourcePath = path.resolve(rootPath, "src/browser/pages/error.html")
|
||||||
|
res.set("Content-Type", getMediaMime(resourcePath))
|
||||||
|
const content = await fs.readFile(resourcePath, "utf8")
|
||||||
|
res.send(
|
||||||
|
replaceTemplates(req, content)
|
||||||
|
.replace(/{{ERROR_TITLE}}/g, status)
|
||||||
|
.replace(/{{ERROR_HEADER}}/g, status)
|
||||||
|
.replace(/{{ERROR_BODY}}/g, err.message),
|
||||||
|
)
|
||||||
|
} else {
|
||||||
|
res.json({
|
||||||
|
error: err.message,
|
||||||
|
...(err.details || {}),
|
||||||
|
})
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
app.use(errorHandler)
|
||||||
|
|
||||||
|
const wsErrorHandler: express.ErrorRequestHandler = async (err, req) => {
|
||||||
|
logger.error(`${err.message} ${err.stack}`)
|
||||||
|
;(req as WebsocketRequest).ws.end()
|
||||||
|
}
|
||||||
|
|
||||||
|
wsApp.use(wsErrorHandler)
|
||||||
|
}
|
|
@ -0,0 +1,95 @@
|
||||||
|
import { Router, Request } from "express"
|
||||||
|
import { promises as fs } from "fs"
|
||||||
|
import { RateLimiter as Limiter } from "limiter"
|
||||||
|
import * as path from "path"
|
||||||
|
import safeCompare from "safe-compare"
|
||||||
|
import { rootPath } from "../constants"
|
||||||
|
import { authenticated, getCookieDomain, redirect, replaceTemplates } from "../http"
|
||||||
|
import { hash, humanPath } from "../util"
|
||||||
|
|
||||||
|
enum Cookie {
|
||||||
|
Key = "key",
|
||||||
|
}
|
||||||
|
|
||||||
|
// RateLimiter wraps around the limiter library for logins.
|
||||||
|
// It allows 2 logins every minute and 12 logins every hour.
|
||||||
|
class RateLimiter {
|
||||||
|
private readonly minuteLimiter = new Limiter(2, "minute")
|
||||||
|
private readonly hourLimiter = new Limiter(12, "hour")
|
||||||
|
|
||||||
|
public try(): boolean {
|
||||||
|
if (this.minuteLimiter.tryRemoveTokens(1)) {
|
||||||
|
return true
|
||||||
|
}
|
||||||
|
return this.hourLimiter.tryRemoveTokens(1)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
const getRoot = async (req: Request, error?: Error): Promise<string> => {
|
||||||
|
const content = await fs.readFile(path.join(rootPath, "src/browser/pages/login.html"), "utf8")
|
||||||
|
let passwordMsg = `Check the config file at ${humanPath(req.args.config)} for the password.`
|
||||||
|
if (req.args.usingEnvPassword) {
|
||||||
|
passwordMsg = "Password was set from $PASSWORD."
|
||||||
|
}
|
||||||
|
return replaceTemplates(
|
||||||
|
req,
|
||||||
|
content
|
||||||
|
.replace(/{{PASSWORD_MSG}}/g, passwordMsg)
|
||||||
|
.replace(/{{ERROR}}/, error ? `<div class="error">${error.message}</div>` : ""),
|
||||||
|
)
|
||||||
|
}
|
||||||
|
|
||||||
|
const limiter = new RateLimiter()
|
||||||
|
|
||||||
|
export const router = Router()
|
||||||
|
|
||||||
|
router.use((req, res, next) => {
|
||||||
|
const to = (typeof req.query.to === "string" && req.query.to) || "/"
|
||||||
|
if (authenticated(req)) {
|
||||||
|
return redirect(req, res, to, { to: undefined })
|
||||||
|
}
|
||||||
|
next()
|
||||||
|
})
|
||||||
|
|
||||||
|
router.get("/", async (req, res) => {
|
||||||
|
res.send(await getRoot(req))
|
||||||
|
})
|
||||||
|
|
||||||
|
router.post("/", async (req, res) => {
|
||||||
|
try {
|
||||||
|
if (!limiter.try()) {
|
||||||
|
throw new Error("Login rate limited!")
|
||||||
|
}
|
||||||
|
|
||||||
|
if (!req.body.password) {
|
||||||
|
throw new Error("Missing password")
|
||||||
|
}
|
||||||
|
|
||||||
|
if (req.args.password && safeCompare(req.body.password, req.args.password)) {
|
||||||
|
// The hash does not add any actual security but we do it for
|
||||||
|
// obfuscation purposes (and as a side effect it handles escaping).
|
||||||
|
res.cookie(Cookie.Key, hash(req.body.password), {
|
||||||
|
domain: getCookieDomain(req.headers.host || "", req.args["proxy-domain"]),
|
||||||
|
path: req.body.base || "/",
|
||||||
|
sameSite: "lax",
|
||||||
|
})
|
||||||
|
|
||||||
|
const to = (typeof req.query.to === "string" && req.query.to) || "/"
|
||||||
|
return redirect(req, res, to, { to: undefined })
|
||||||
|
}
|
||||||
|
|
||||||
|
console.error(
|
||||||
|
"Failed login attempt",
|
||||||
|
JSON.stringify({
|
||||||
|
xForwardedFor: req.headers["x-forwarded-for"],
|
||||||
|
remoteAddress: req.connection.remoteAddress,
|
||||||
|
userAgent: req.headers["user-agent"],
|
||||||
|
timestamp: Math.floor(new Date().getTime() / 1000),
|
||||||
|
}),
|
||||||
|
)
|
||||||
|
|
||||||
|
throw new Error("Incorrect password")
|
||||||
|
} catch (error) {
|
||||||
|
res.send(await getRoot(req, error))
|
||||||
|
}
|
||||||
|
})
|
|
@ -0,0 +1,47 @@
|
||||||
|
import { Request, Router } from "express"
|
||||||
|
import qs from "qs"
|
||||||
|
import { HttpCode, HttpError } from "../../common/http"
|
||||||
|
import { normalize } from "../../common/util"
|
||||||
|
import { authenticated, ensureAuthenticated, redirect } from "../http"
|
||||||
|
import { proxy } from "../proxy"
|
||||||
|
import { Router as WsRouter } from "../wsRouter"
|
||||||
|
|
||||||
|
export const router = Router()
|
||||||
|
|
||||||
|
const getProxyTarget = (req: Request, rewrite: boolean): string => {
|
||||||
|
if (rewrite) {
|
||||||
|
const query = qs.stringify(req.query)
|
||||||
|
return `http://0.0.0.0:${req.params.port}/${req.params[0] || ""}${query ? `?${query}` : ""}`
|
||||||
|
}
|
||||||
|
return `http://0.0.0.0:${req.params.port}/${req.originalUrl}`
|
||||||
|
}
|
||||||
|
|
||||||
|
router.all("/(:port)(/*)?", (req, res) => {
|
||||||
|
if (!authenticated(req)) {
|
||||||
|
// If visiting the root (/:port only) redirect to the login page.
|
||||||
|
if (!req.params[0] || req.params[0] === "/") {
|
||||||
|
const to = normalize(`${req.baseUrl}${req.path}`)
|
||||||
|
return redirect(req, res, "login", {
|
||||||
|
to: to !== "/" ? to : undefined,
|
||||||
|
})
|
||||||
|
}
|
||||||
|
throw new HttpError("Unauthorized", HttpCode.Unauthorized)
|
||||||
|
}
|
||||||
|
|
||||||
|
// Absolute redirects need to be based on the subpath when rewriting.
|
||||||
|
;(req as any).base = `${req.baseUrl}/${req.params.port}`
|
||||||
|
|
||||||
|
proxy.web(req, res, {
|
||||||
|
ignorePath: true,
|
||||||
|
target: getProxyTarget(req, true),
|
||||||
|
})
|
||||||
|
})
|
||||||
|
|
||||||
|
export const wsRouter = WsRouter()
|
||||||
|
|
||||||
|
wsRouter.ws("/(:port)(/*)?", ensureAuthenticated, (req) => {
|
||||||
|
proxy.ws(req, req.ws, req.head, {
|
||||||
|
ignorePath: true,
|
||||||
|
target: getProxyTarget(req, true),
|
||||||
|
})
|
||||||
|
})
|
|
@ -0,0 +1,69 @@
|
||||||
|
import { field, logger } from "@coder/logger"
|
||||||
|
import { Router } from "express"
|
||||||
|
import { promises as fs } from "fs"
|
||||||
|
import * as path from "path"
|
||||||
|
import { Readable } from "stream"
|
||||||
|
import * as tarFs from "tar-fs"
|
||||||
|
import * as zlib from "zlib"
|
||||||
|
import { HttpCode, HttpError } from "../../common/http"
|
||||||
|
import { rootPath } from "../constants"
|
||||||
|
import { authenticated, ensureAuthenticated, replaceTemplates } from "../http"
|
||||||
|
import { getMediaMime, pathToFsPath } from "../util"
|
||||||
|
|
||||||
|
export const router = Router()
|
||||||
|
|
||||||
|
// The commit is for caching.
|
||||||
|
router.get("/(:commit)(/*)?", async (req, res) => {
|
||||||
|
// Used by VS Code to load extensions into the web worker.
|
||||||
|
const tar = Array.isArray(req.query.tar) ? req.query.tar[0] : req.query.tar
|
||||||
|
if (typeof tar === "string") {
|
||||||
|
ensureAuthenticated(req)
|
||||||
|
let stream: Readable = tarFs.pack(pathToFsPath(tar))
|
||||||
|
if (req.headers["accept-encoding"] && req.headers["accept-encoding"].includes("gzip")) {
|
||||||
|
logger.debug("gzipping tar", field("path", tar))
|
||||||
|
const compress = zlib.createGzip()
|
||||||
|
stream.pipe(compress)
|
||||||
|
stream.on("error", (error) => compress.destroy(error))
|
||||||
|
stream.on("close", () => compress.end())
|
||||||
|
stream = compress
|
||||||
|
res.header("content-encoding", "gzip")
|
||||||
|
}
|
||||||
|
res.set("Content-Type", "application/x-tar")
|
||||||
|
stream.on("close", () => res.end())
|
||||||
|
return stream.pipe(res)
|
||||||
|
}
|
||||||
|
|
||||||
|
// If not a tar use the remainder of the path to load the resource.
|
||||||
|
if (!req.params[0]) {
|
||||||
|
throw new HttpError("Not Found", HttpCode.NotFound)
|
||||||
|
}
|
||||||
|
|
||||||
|
const resourcePath = path.resolve(req.params[0])
|
||||||
|
|
||||||
|
// Make sure it's in code-server if you aren't authenticated. This lets
|
||||||
|
// unauthenticated users load the login assets.
|
||||||
|
if (!resourcePath.startsWith(rootPath) && !authenticated(req)) {
|
||||||
|
throw new HttpError("Unauthorized", HttpCode.Unauthorized)
|
||||||
|
}
|
||||||
|
|
||||||
|
// Don't cache during development. - can also be used if you want to make a
|
||||||
|
// static request without caching.
|
||||||
|
if (req.params.commit !== "development" && req.params.commit !== "-") {
|
||||||
|
res.header("Cache-Control", "public, max-age=31536000")
|
||||||
|
}
|
||||||
|
|
||||||
|
// Without this the default is to use the directory the script loaded from.
|
||||||
|
if (req.headers["service-worker"]) {
|
||||||
|
res.header("service-worker-allowed", "/")
|
||||||
|
}
|
||||||
|
|
||||||
|
res.set("Content-Type", getMediaMime(resourcePath))
|
||||||
|
|
||||||
|
if (resourcePath.endsWith("manifest.json")) {
|
||||||
|
const content = await fs.readFile(resourcePath, "utf8")
|
||||||
|
return res.send(replaceTemplates(req, content))
|
||||||
|
}
|
||||||
|
|
||||||
|
const content = await fs.readFile(resourcePath)
|
||||||
|
return res.send(content)
|
||||||
|
})
|
|
@ -0,0 +1,18 @@
|
||||||
|
import { Router } from "express"
|
||||||
|
import { version } from "../constants"
|
||||||
|
import { ensureAuthenticated } from "../http"
|
||||||
|
import { UpdateProvider } from "../update"
|
||||||
|
|
||||||
|
export const router = Router()
|
||||||
|
|
||||||
|
const provider = new UpdateProvider()
|
||||||
|
|
||||||
|
router.get("/check", ensureAuthenticated, async (req, res) => {
|
||||||
|
const update = await provider.getUpdate(req.query.force === "true")
|
||||||
|
res.json({
|
||||||
|
checked: update.checked,
|
||||||
|
latest: update.version,
|
||||||
|
current: version,
|
||||||
|
isLatest: provider.isLatestVersion(update),
|
||||||
|
})
|
||||||
|
})
|
|
@ -0,0 +1,105 @@
|
||||||
|
import * as crypto from "crypto"
|
||||||
|
import { Router } from "express"
|
||||||
|
import { promises as fs } from "fs"
|
||||||
|
import * as path from "path"
|
||||||
|
import { commit, rootPath, version } from "../constants"
|
||||||
|
import { authenticated, ensureAuthenticated, redirect, replaceTemplates } from "../http"
|
||||||
|
import { getMediaMime, pathToFsPath } from "../util"
|
||||||
|
import { VscodeProvider } from "../vscode"
|
||||||
|
import { Router as WsRouter } from "../wsRouter"
|
||||||
|
|
||||||
|
export const router = Router()
|
||||||
|
|
||||||
|
const vscode = new VscodeProvider()
|
||||||
|
|
||||||
|
router.get("/", async (req, res) => {
|
||||||
|
if (!authenticated(req)) {
|
||||||
|
return redirect(req, res, "login", {
|
||||||
|
// req.baseUrl can be blank if already at the root.
|
||||||
|
to: req.baseUrl && req.baseUrl !== "/" ? req.baseUrl : undefined,
|
||||||
|
})
|
||||||
|
}
|
||||||
|
|
||||||
|
const [content, options] = await Promise.all([
|
||||||
|
await fs.readFile(path.join(rootPath, "src/browser/pages/vscode.html"), "utf8"),
|
||||||
|
(async () => {
|
||||||
|
try {
|
||||||
|
return await vscode.initialize({ args: req.args, remoteAuthority: req.headers.host || "" }, req.query)
|
||||||
|
} catch (error) {
|
||||||
|
const devMessage = commit === "development" ? "It might not have finished compiling." : ""
|
||||||
|
throw new Error(`VS Code failed to load. ${devMessage} ${error.message}`)
|
||||||
|
}
|
||||||
|
})(),
|
||||||
|
])
|
||||||
|
|
||||||
|
options.productConfiguration.codeServerVersion = version
|
||||||
|
|
||||||
|
res.send(
|
||||||
|
replaceTemplates(
|
||||||
|
req,
|
||||||
|
// Uncomment prod blocks if not in development. TODO: Would this be
|
||||||
|
// better as a build step? Or maintain two HTML files again?
|
||||||
|
commit !== "development" ? content.replace(/<!-- PROD_ONLY/g, "").replace(/END_PROD_ONLY -->/g, "") : content,
|
||||||
|
{
|
||||||
|
disableTelemetry: !!req.args["disable-telemetry"],
|
||||||
|
},
|
||||||
|
)
|
||||||
|
.replace(`"{{REMOTE_USER_DATA_URI}}"`, `'${JSON.stringify(options.remoteUserDataUri)}'`)
|
||||||
|
.replace(`"{{PRODUCT_CONFIGURATION}}"`, `'${JSON.stringify(options.productConfiguration)}'`)
|
||||||
|
.replace(`"{{WORKBENCH_WEB_CONFIGURATION}}"`, `'${JSON.stringify(options.workbenchWebConfiguration)}'`)
|
||||||
|
.replace(`"{{NLS_CONFIGURATION}}"`, `'${JSON.stringify(options.nlsConfiguration)}'`),
|
||||||
|
)
|
||||||
|
})
|
||||||
|
|
||||||
|
/**
|
||||||
|
* TODO: Might currently be unused.
|
||||||
|
*/
|
||||||
|
router.get("/resource(/*)?", ensureAuthenticated, async (req, res) => {
|
||||||
|
if (typeof req.query.path === "string") {
|
||||||
|
res.set("Content-Type", getMediaMime(req.query.path))
|
||||||
|
res.send(await fs.readFile(pathToFsPath(req.query.path)))
|
||||||
|
}
|
||||||
|
})
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Used by VS Code to load files.
|
||||||
|
*/
|
||||||
|
router.get("/vscode-remote-resource(/*)?", ensureAuthenticated, async (req, res) => {
|
||||||
|
if (typeof req.query.path === "string") {
|
||||||
|
res.set("Content-Type", getMediaMime(req.query.path))
|
||||||
|
res.send(await fs.readFile(pathToFsPath(req.query.path)))
|
||||||
|
}
|
||||||
|
})
|
||||||
|
|
||||||
|
/**
|
||||||
|
* VS Code webviews use these paths to load files and to load webview assets
|
||||||
|
* like HTML and JavaScript.
|
||||||
|
*/
|
||||||
|
router.get("/webview/*", ensureAuthenticated, async (req, res) => {
|
||||||
|
res.set("Content-Type", getMediaMime(req.path))
|
||||||
|
if (/^vscode-resource/.test(req.params[0])) {
|
||||||
|
return res.send(await fs.readFile(req.params[0].replace(/^vscode-resource(\/file)?/, "")))
|
||||||
|
}
|
||||||
|
return res.send(
|
||||||
|
await fs.readFile(path.join(vscode.vsRootPath, "out/vs/workbench/contrib/webview/browser/pre", req.params[0])),
|
||||||
|
)
|
||||||
|
})
|
||||||
|
|
||||||
|
export const wsRouter = WsRouter()
|
||||||
|
|
||||||
|
wsRouter.ws("/", ensureAuthenticated, async (req) => {
|
||||||
|
const magic = "258EAFA5-E914-47DA-95CA-C5AB0DC85B11"
|
||||||
|
const reply = crypto
|
||||||
|
.createHash("sha1")
|
||||||
|
.update(req.headers["sec-websocket-key"] + magic)
|
||||||
|
.digest("base64")
|
||||||
|
req.ws.write(
|
||||||
|
[
|
||||||
|
"HTTP/1.1 101 Switching Protocols",
|
||||||
|
"Upgrade: websocket",
|
||||||
|
"Connection: Upgrade",
|
||||||
|
`Sec-WebSocket-Accept: ${reply}`,
|
||||||
|
].join("\r\n") + "\r\n\r\n",
|
||||||
|
)
|
||||||
|
await vscode.sendWebsocket(req.ws, req.query)
|
||||||
|
})
|
|
@ -1,7 +1,7 @@
|
||||||
import { logger } from "@coder/logger"
|
import { logger } from "@coder/logger"
|
||||||
|
import { Query } from "express-serve-static-core"
|
||||||
import * as fs from "fs-extra"
|
import * as fs from "fs-extra"
|
||||||
import * as path from "path"
|
import * as path from "path"
|
||||||
import { Route } from "./http"
|
|
||||||
import { paths } from "./util"
|
import { paths } from "./util"
|
||||||
|
|
||||||
export type Settings = { [key: string]: Settings | string | boolean | number }
|
export type Settings = { [key: string]: Settings | string | boolean | number }
|
||||||
|
@ -58,7 +58,7 @@ export interface CoderSettings extends UpdateSettings {
|
||||||
url: string
|
url: string
|
||||||
workspace: boolean
|
workspace: boolean
|
||||||
}
|
}
|
||||||
query: Route["query"]
|
query: Query
|
||||||
}
|
}
|
||||||
|
|
||||||
/**
|
/**
|
||||||
|
|
|
@ -4,7 +4,7 @@ import * as path from "path"
|
||||||
import * as tls from "tls"
|
import * as tls from "tls"
|
||||||
import { Emitter } from "../common/emitter"
|
import { Emitter } from "../common/emitter"
|
||||||
import { generateUuid } from "../common/util"
|
import { generateUuid } from "../common/util"
|
||||||
import { tmpdir } from "./util"
|
import { canConnect, tmpdir } from "./util"
|
||||||
|
|
||||||
/**
|
/**
|
||||||
* Provides a way to proxy a TLS socket. Can be used when you need to pass a
|
* Provides a way to proxy a TLS socket. Can be used when you need to pass a
|
||||||
|
@ -89,17 +89,6 @@ export class SocketProxyProvider {
|
||||||
}
|
}
|
||||||
|
|
||||||
public async findFreeSocketPath(basePath: string, maxTries = 100): Promise<string> {
|
public async findFreeSocketPath(basePath: string, maxTries = 100): Promise<string> {
|
||||||
const canConnect = (path: string): Promise<boolean> => {
|
|
||||||
return new Promise((resolve) => {
|
|
||||||
const socket = net.connect(path)
|
|
||||||
socket.once("error", () => resolve(false))
|
|
||||||
socket.once("connect", () => {
|
|
||||||
socket.destroy()
|
|
||||||
resolve(true)
|
|
||||||
})
|
|
||||||
})
|
|
||||||
}
|
|
||||||
|
|
||||||
let i = 0
|
let i = 0
|
||||||
let path = basePath
|
let path = basePath
|
||||||
while ((await canConnect(path)) && i < maxTries) {
|
while ((await canConnect(path)) && i < maxTries) {
|
||||||
|
|
|
@ -1,12 +1,10 @@
|
||||||
import { field, logger } from "@coder/logger"
|
import { field, logger } from "@coder/logger"
|
||||||
import * as http from "http"
|
import * as http from "http"
|
||||||
import * as https from "https"
|
import * as https from "https"
|
||||||
import * as path from "path"
|
|
||||||
import * as semver from "semver"
|
import * as semver from "semver"
|
||||||
import * as url from "url"
|
import * as url from "url"
|
||||||
import { HttpCode, HttpError } from "../../common/http"
|
import { version } from "./constants"
|
||||||
import { HttpProvider, HttpProviderOptions, HttpResponse, Route } from "../http"
|
import { settings as globalSettings, SettingsProvider, UpdateSettings } from "./settings"
|
||||||
import { settings as globalSettings, SettingsProvider, UpdateSettings } from "../settings"
|
|
||||||
|
|
||||||
export interface Update {
|
export interface Update {
|
||||||
checked: number
|
checked: number
|
||||||
|
@ -18,15 +16,13 @@ export interface LatestResponse {
|
||||||
}
|
}
|
||||||
|
|
||||||
/**
|
/**
|
||||||
* HTTP provider for checking updates (does not download/install them).
|
* Provide update information.
|
||||||
*/
|
*/
|
||||||
export class UpdateHttpProvider extends HttpProvider {
|
export class UpdateProvider {
|
||||||
private update?: Promise<Update>
|
private update?: Promise<Update>
|
||||||
private updateInterval = 1000 * 60 * 60 * 24 // Milliseconds between update checks.
|
private updateInterval = 1000 * 60 * 60 * 24 // Milliseconds between update checks.
|
||||||
|
|
||||||
public constructor(
|
public constructor(
|
||||||
options: HttpProviderOptions,
|
|
||||||
public readonly enabled: boolean,
|
|
||||||
/**
|
/**
|
||||||
* The URL for getting the latest version of code-server. Should return JSON
|
* The URL for getting the latest version of code-server. Should return JSON
|
||||||
* that fulfills `LatestResponse`.
|
* that fulfills `LatestResponse`.
|
||||||
|
@ -37,37 +33,7 @@ export class UpdateHttpProvider extends HttpProvider {
|
||||||
* settings will be used.
|
* settings will be used.
|
||||||
*/
|
*/
|
||||||
private readonly settings: SettingsProvider<UpdateSettings> = globalSettings,
|
private readonly settings: SettingsProvider<UpdateSettings> = globalSettings,
|
||||||
) {
|
) {}
|
||||||
super(options)
|
|
||||||
}
|
|
||||||
|
|
||||||
public async handleRequest(route: Route, request: http.IncomingMessage): Promise<HttpResponse> {
|
|
||||||
this.ensureAuthenticated(request)
|
|
||||||
this.ensureMethod(request)
|
|
||||||
|
|
||||||
if (!this.isRoot(route)) {
|
|
||||||
throw new HttpError("Not found", HttpCode.NotFound)
|
|
||||||
}
|
|
||||||
|
|
||||||
if (!this.enabled) {
|
|
||||||
throw new Error("update checks are disabled")
|
|
||||||
}
|
|
||||||
|
|
||||||
switch (route.base) {
|
|
||||||
case "/check":
|
|
||||||
case "/": {
|
|
||||||
const update = await this.getUpdate(route.base === "/check")
|
|
||||||
return {
|
|
||||||
content: {
|
|
||||||
...update,
|
|
||||||
isLatest: this.isLatestVersion(update),
|
|
||||||
},
|
|
||||||
}
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
throw new HttpError("Not found", HttpCode.NotFound)
|
|
||||||
}
|
|
||||||
|
|
||||||
/**
|
/**
|
||||||
* Query for and return the latest update.
|
* Query for and return the latest update.
|
||||||
|
@ -89,7 +55,7 @@ export class UpdateHttpProvider extends HttpProvider {
|
||||||
if (!update || update.checked + this.updateInterval < now) {
|
if (!update || update.checked + this.updateInterval < now) {
|
||||||
const buffer = await this.request(this.latestUrl)
|
const buffer = await this.request(this.latestUrl)
|
||||||
const data = JSON.parse(buffer.toString()) as LatestResponse
|
const data = JSON.parse(buffer.toString()) as LatestResponse
|
||||||
update = { checked: now, version: data.name }
|
update = { checked: now, version: data.name.replace(/^v/, "") }
|
||||||
await this.settings.write({ update })
|
await this.settings.write({ update })
|
||||||
}
|
}
|
||||||
logger.debug("got latest version", field("latest", update.version))
|
logger.debug("got latest version", field("latest", update.version))
|
||||||
|
@ -103,18 +69,13 @@ export class UpdateHttpProvider extends HttpProvider {
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
public get currentVersion(): string {
|
|
||||||
return require(path.resolve(__dirname, "../../../package.json")).version
|
|
||||||
}
|
|
||||||
|
|
||||||
/**
|
/**
|
||||||
* Return true if the currently installed version is the latest.
|
* Return true if the currently installed version is the latest.
|
||||||
*/
|
*/
|
||||||
public isLatestVersion(latest: Update): boolean {
|
public isLatestVersion(latest: Update): boolean {
|
||||||
const version = this.currentVersion
|
|
||||||
logger.debug("comparing versions", field("current", version), field("latest", latest.version))
|
logger.debug("comparing versions", field("current", version), field("latest", latest.version))
|
||||||
try {
|
try {
|
||||||
return latest.version === version || semver.lt(latest.version, version)
|
return semver.lte(latest.version, version)
|
||||||
} catch (error) {
|
} catch (error) {
|
||||||
return true
|
return true
|
||||||
}
|
}
|
||||||
|
@ -144,22 +105,20 @@ export class UpdateHttpProvider extends HttpProvider {
|
||||||
logger.debug("Making request", field("uri", uri))
|
logger.debug("Making request", field("uri", uri))
|
||||||
const httpx = uri.startsWith("https") ? https : http
|
const httpx = uri.startsWith("https") ? https : http
|
||||||
const client = httpx.get(uri, { headers: { "User-Agent": "code-server" } }, (response) => {
|
const client = httpx.get(uri, { headers: { "User-Agent": "code-server" } }, (response) => {
|
||||||
if (
|
if (!response.statusCode || response.statusCode < 200 || response.statusCode >= 400) {
|
||||||
response.statusCode &&
|
return reject(new Error(`${uri}: ${response.statusCode || "500"}`))
|
||||||
response.statusCode >= 300 &&
|
}
|
||||||
response.statusCode < 400 &&
|
|
||||||
response.headers.location
|
if (response.statusCode >= 300) {
|
||||||
) {
|
|
||||||
++redirects
|
++redirects
|
||||||
|
response.destroy()
|
||||||
if (redirects > maxRedirects) {
|
if (redirects > maxRedirects) {
|
||||||
return reject(new Error("reached max redirects"))
|
return reject(new Error("reached max redirects"))
|
||||||
}
|
}
|
||||||
response.destroy()
|
if (!response.headers.location) {
|
||||||
return request(url.resolve(uri, response.headers.location))
|
return reject(new Error("received redirect with no location header"))
|
||||||
}
|
}
|
||||||
|
return request(url.resolve(uri, response.headers.location))
|
||||||
if (!response.statusCode || response.statusCode < 200 || response.statusCode >= 400) {
|
|
||||||
return reject(new Error(`${uri}: ${response.statusCode || "500"}`))
|
|
||||||
}
|
}
|
||||||
|
|
||||||
resolve(response)
|
resolve(response)
|
|
@ -2,6 +2,7 @@ import * as cp from "child_process"
|
||||||
import * as crypto from "crypto"
|
import * as crypto from "crypto"
|
||||||
import envPaths from "env-paths"
|
import envPaths from "env-paths"
|
||||||
import * as fs from "fs-extra"
|
import * as fs from "fs-extra"
|
||||||
|
import * as net from "net"
|
||||||
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"
|
||||||
|
@ -53,25 +54,45 @@ export function humanPath(p?: string): string {
|
||||||
return p.replace(os.homedir(), "~")
|
return p.replace(os.homedir(), "~")
|
||||||
}
|
}
|
||||||
|
|
||||||
export const generateCertificate = async (): Promise<{ cert: string; certKey: string }> => {
|
export const generateCertificate = async (hostname: string): Promise<{ cert: string; certKey: string }> => {
|
||||||
const paths = {
|
const certPath = path.join(paths.data, `${hostname.replace(/\./g, "_")}.crt`)
|
||||||
cert: path.join(tmpdir, "self-signed.cert"),
|
const certKeyPath = path.join(paths.data, `${hostname.replace(/\./g, "_")}.key`)
|
||||||
certKey: path.join(tmpdir, "self-signed.key"),
|
|
||||||
}
|
const checks = await Promise.all([fs.pathExists(certPath), fs.pathExists(certKeyPath)])
|
||||||
const checks = await Promise.all([fs.pathExists(paths.cert), fs.pathExists(paths.certKey)])
|
|
||||||
if (!checks[0] || !checks[1]) {
|
if (!checks[0] || !checks[1]) {
|
||||||
// Require on demand so openssl isn't required if you aren't going to
|
// Require on demand so openssl isn't required if you aren't going to
|
||||||
// generate certificates.
|
// generate certificates.
|
||||||
const pem = require("pem") as typeof import("pem")
|
const pem = require("pem") as typeof import("pem")
|
||||||
const certs = await new Promise<import("pem").CertificateCreationResult>((resolve, reject): void => {
|
const certs = await new Promise<import("pem").CertificateCreationResult>((resolve, reject): void => {
|
||||||
pem.createCertificate({ selfSigned: true }, (error, result) => {
|
pem.createCertificate(
|
||||||
|
{
|
||||||
|
selfSigned: true,
|
||||||
|
commonName: hostname,
|
||||||
|
config: `
|
||||||
|
[req]
|
||||||
|
req_extensions = v3_req
|
||||||
|
|
||||||
|
[ v3_req ]
|
||||||
|
basicConstraints = CA:true
|
||||||
|
extendedKeyUsage = serverAuth
|
||||||
|
subjectAltName = @alt_names
|
||||||
|
|
||||||
|
[alt_names]
|
||||||
|
DNS.1 = ${hostname}
|
||||||
|
`,
|
||||||
|
},
|
||||||
|
(error, result) => {
|
||||||
return error ? reject(error) : resolve(result)
|
return error ? reject(error) : resolve(result)
|
||||||
|
},
|
||||||
|
)
|
||||||
})
|
})
|
||||||
})
|
await fs.mkdirp(paths.data)
|
||||||
await fs.mkdirp(tmpdir)
|
await Promise.all([fs.writeFile(certPath, certs.certificate), fs.writeFile(certKeyPath, certs.serviceKey)])
|
||||||
await Promise.all([fs.writeFile(paths.cert, certs.certificate), fs.writeFile(paths.certKey, certs.serviceKey)])
|
}
|
||||||
|
return {
|
||||||
|
cert: certPath,
|
||||||
|
certKey: certKeyPath,
|
||||||
}
|
}
|
||||||
return paths
|
|
||||||
}
|
}
|
||||||
|
|
||||||
export const generatePassword = async (length = 24): Promise<string> => {
|
export const generatePassword = async (length = 24): Promise<string> => {
|
||||||
|
@ -246,3 +267,26 @@ export function pathToFsPath(path: string, keepDriveLetterCasing = false): strin
|
||||||
}
|
}
|
||||||
return value
|
return value
|
||||||
}
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Return a promise that resolves with whether the socket path is active.
|
||||||
|
*/
|
||||||
|
export function canConnect(path: string): Promise<boolean> {
|
||||||
|
return new Promise((resolve) => {
|
||||||
|
const socket = net.connect(path)
|
||||||
|
socket.once("error", () => resolve(false))
|
||||||
|
socket.once("connect", () => {
|
||||||
|
socket.destroy()
|
||||||
|
resolve(true)
|
||||||
|
})
|
||||||
|
})
|
||||||
|
}
|
||||||
|
|
||||||
|
export const isFile = async (path: string): Promise<boolean> => {
|
||||||
|
try {
|
||||||
|
const stat = await fs.stat(path)
|
||||||
|
return stat.isFile()
|
||||||
|
} catch (error) {
|
||||||
|
return false
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
|
@ -0,0 +1,164 @@
|
||||||
|
import { logger } from "@coder/logger"
|
||||||
|
import * as cp from "child_process"
|
||||||
|
import * as net from "net"
|
||||||
|
import * as path from "path"
|
||||||
|
import * as ipc from "../../lib/vscode/src/vs/server/ipc"
|
||||||
|
import { arrayify, generateUuid } from "../common/util"
|
||||||
|
import { rootPath } from "./constants"
|
||||||
|
import { settings } from "./settings"
|
||||||
|
import { SocketProxyProvider } from "./socket"
|
||||||
|
import { isFile } from "./util"
|
||||||
|
import { onMessage, wrapper } from "./wrapper"
|
||||||
|
|
||||||
|
export class VscodeProvider {
|
||||||
|
public readonly serverRootPath: string
|
||||||
|
public readonly vsRootPath: string
|
||||||
|
private _vscode?: Promise<cp.ChildProcess>
|
||||||
|
private readonly socketProvider = new SocketProxyProvider()
|
||||||
|
|
||||||
|
public constructor() {
|
||||||
|
this.vsRootPath = path.resolve(rootPath, "lib/vscode")
|
||||||
|
this.serverRootPath = path.join(this.vsRootPath, "out/vs/server")
|
||||||
|
wrapper.onDispose(() => this.dispose())
|
||||||
|
}
|
||||||
|
|
||||||
|
public async dispose(): Promise<void> {
|
||||||
|
this.socketProvider.stop()
|
||||||
|
if (this._vscode) {
|
||||||
|
const vscode = await this._vscode
|
||||||
|
vscode.removeAllListeners()
|
||||||
|
vscode.kill()
|
||||||
|
this._vscode = undefined
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
public async initialize(
|
||||||
|
options: Omit<ipc.VscodeOptions, "startPath">,
|
||||||
|
query: ipc.Query,
|
||||||
|
): Promise<ipc.WorkbenchOptions> {
|
||||||
|
const { lastVisited } = await settings.read()
|
||||||
|
const startPath = await this.getFirstPath([
|
||||||
|
{ url: query.workspace, workspace: true },
|
||||||
|
{ url: query.folder, workspace: false },
|
||||||
|
options.args._ && options.args._.length > 0
|
||||||
|
? { url: path.resolve(options.args._[options.args._.length - 1]) }
|
||||||
|
: undefined,
|
||||||
|
lastVisited,
|
||||||
|
])
|
||||||
|
|
||||||
|
settings.write({
|
||||||
|
lastVisited: startPath,
|
||||||
|
query,
|
||||||
|
})
|
||||||
|
|
||||||
|
const id = generateUuid()
|
||||||
|
const vscode = await this.fork()
|
||||||
|
|
||||||
|
logger.debug("setting up vs code...")
|
||||||
|
|
||||||
|
this.send(
|
||||||
|
{
|
||||||
|
type: "init",
|
||||||
|
id,
|
||||||
|
options: {
|
||||||
|
...options,
|
||||||
|
startPath,
|
||||||
|
},
|
||||||
|
},
|
||||||
|
vscode,
|
||||||
|
)
|
||||||
|
|
||||||
|
const message = await onMessage<ipc.VscodeMessage, ipc.OptionsMessage>(
|
||||||
|
vscode,
|
||||||
|
(message): message is ipc.OptionsMessage => {
|
||||||
|
// There can be parallel initializations so wait for the right ID.
|
||||||
|
return message.type === "options" && message.id === id
|
||||||
|
},
|
||||||
|
)
|
||||||
|
|
||||||
|
return message.options
|
||||||
|
}
|
||||||
|
|
||||||
|
private fork(): Promise<cp.ChildProcess> {
|
||||||
|
if (this._vscode) {
|
||||||
|
return this._vscode
|
||||||
|
}
|
||||||
|
|
||||||
|
logger.debug("forking vs code...")
|
||||||
|
const vscode = cp.fork(path.join(this.serverRootPath, "fork"))
|
||||||
|
|
||||||
|
const dispose = () => {
|
||||||
|
vscode.removeAllListeners()
|
||||||
|
vscode.kill()
|
||||||
|
this._vscode = undefined
|
||||||
|
}
|
||||||
|
|
||||||
|
vscode.on("error", (error: Error) => {
|
||||||
|
logger.error(error.message)
|
||||||
|
if (error.stack) {
|
||||||
|
logger.debug(error.stack)
|
||||||
|
}
|
||||||
|
dispose()
|
||||||
|
})
|
||||||
|
|
||||||
|
vscode.on("exit", (code) => {
|
||||||
|
logger.error(`VS Code exited unexpectedly with code ${code}`)
|
||||||
|
dispose()
|
||||||
|
})
|
||||||
|
|
||||||
|
this._vscode = onMessage<ipc.VscodeMessage, ipc.ReadyMessage>(vscode, (message): message is ipc.ReadyMessage => {
|
||||||
|
return message.type === "ready"
|
||||||
|
}).then(() => vscode)
|
||||||
|
|
||||||
|
return this._vscode
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* VS Code expects a raw socket. It will handle all the web socket frames.
|
||||||
|
*/
|
||||||
|
public async sendWebsocket(socket: net.Socket, query: ipc.Query): Promise<void> {
|
||||||
|
const vscode = await this._vscode
|
||||||
|
// TLS sockets cannot be transferred to child processes so we need an
|
||||||
|
// in-between. Non-TLS sockets will be returned as-is.
|
||||||
|
const socketProxy = await this.socketProvider.createProxy(socket)
|
||||||
|
this.send({ type: "socket", query }, vscode, socketProxy)
|
||||||
|
}
|
||||||
|
|
||||||
|
private send(message: ipc.CodeServerMessage, vscode?: cp.ChildProcess, socket?: net.Socket): void {
|
||||||
|
if (!vscode || vscode.killed) {
|
||||||
|
throw new Error("vscode is not running")
|
||||||
|
}
|
||||||
|
vscode.send(message, socket)
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Choose the first non-empty path from the provided array.
|
||||||
|
*
|
||||||
|
* Each array item consists of `url` and an optional `workspace` boolean that
|
||||||
|
* indicates whether that url is for a workspace.
|
||||||
|
*
|
||||||
|
* `url` can be a fully qualified URL or just the path portion.
|
||||||
|
*
|
||||||
|
* `url` can also be a query object to make it easier to pass in query
|
||||||
|
* variables directly but anything that isn't a string or string array is not
|
||||||
|
* valid and will be ignored.
|
||||||
|
*/
|
||||||
|
private async getFirstPath(
|
||||||
|
startPaths: Array<{ url?: string | string[] | ipc.Query | ipc.Query[]; workspace?: boolean } | undefined>,
|
||||||
|
): Promise<ipc.StartPath | undefined> {
|
||||||
|
for (let i = 0; i < startPaths.length; ++i) {
|
||||||
|
const startPath = startPaths[i]
|
||||||
|
const url = arrayify(startPath && startPath.url).find((p) => !!p)
|
||||||
|
if (startPath && url && typeof url === "string") {
|
||||||
|
return {
|
||||||
|
url,
|
||||||
|
// The only time `workspace` is undefined is for the command-line
|
||||||
|
// argument, in which case it's a path (not a URL) so we can stat it
|
||||||
|
// without having to parse it.
|
||||||
|
workspace: typeof startPath.workspace !== "undefined" ? startPath.workspace : await isFile(url),
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
return undefined
|
||||||
|
}
|
||||||
|
}
|
|
@ -1,11 +1,70 @@
|
||||||
import { field, logger } from "@coder/logger"
|
import { field, Logger, logger } from "@coder/logger"
|
||||||
import * as cp from "child_process"
|
import * as cp from "child_process"
|
||||||
import * as path from "path"
|
import * as path from "path"
|
||||||
import * as rfs from "rotating-file-stream"
|
import * as rfs from "rotating-file-stream"
|
||||||
import { Emitter } from "../common/emitter"
|
import { Emitter } from "../common/emitter"
|
||||||
|
import { DefaultedArgs } from "./cli"
|
||||||
import { paths } from "./util"
|
import { paths } from "./util"
|
||||||
|
|
||||||
interface HandshakeMessage {
|
const timeoutInterval = 10000 // 10s, matches VS Code's timeouts.
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Listen to a single message from a process. Reject if the process errors,
|
||||||
|
* exits, or times out.
|
||||||
|
*
|
||||||
|
* `fn` is a function that determines whether the message is the one we're
|
||||||
|
* waiting for.
|
||||||
|
*/
|
||||||
|
export function onMessage<M, T extends M>(
|
||||||
|
proc: cp.ChildProcess | NodeJS.Process,
|
||||||
|
fn: (message: M) => message is T,
|
||||||
|
customLogger?: Logger,
|
||||||
|
): Promise<T> {
|
||||||
|
return new Promise((resolve, reject) => {
|
||||||
|
const cleanup = () => {
|
||||||
|
proc.off("error", onError)
|
||||||
|
proc.off("exit", onExit)
|
||||||
|
proc.off("message", onMessage)
|
||||||
|
clearTimeout(timeout)
|
||||||
|
}
|
||||||
|
|
||||||
|
const timeout = setTimeout(() => {
|
||||||
|
cleanup()
|
||||||
|
reject(new Error("timed out"))
|
||||||
|
}, timeoutInterval)
|
||||||
|
|
||||||
|
const onError = (error: Error) => {
|
||||||
|
cleanup()
|
||||||
|
reject(error)
|
||||||
|
}
|
||||||
|
|
||||||
|
const onExit = (code: number) => {
|
||||||
|
cleanup()
|
||||||
|
reject(new Error(`exited unexpectedly with code ${code}`))
|
||||||
|
}
|
||||||
|
|
||||||
|
const onMessage = (message: M) => {
|
||||||
|
;(customLogger || logger).trace("got message", field("message", message))
|
||||||
|
if (fn(message)) {
|
||||||
|
cleanup()
|
||||||
|
resolve(message)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
proc.on("message", onMessage)
|
||||||
|
// NodeJS.Process doesn't have `error` but binding anyway shouldn't break
|
||||||
|
// anything. It does have `exit` but the types aren't working.
|
||||||
|
;(proc as cp.ChildProcess).on("error", onError)
|
||||||
|
;(proc as cp.ChildProcess).on("exit", onExit)
|
||||||
|
})
|
||||||
|
}
|
||||||
|
|
||||||
|
interface ParentHandshakeMessage {
|
||||||
|
type: "handshake"
|
||||||
|
args: DefaultedArgs
|
||||||
|
}
|
||||||
|
|
||||||
|
interface ChildHandshakeMessage {
|
||||||
type: "handshake"
|
type: "handshake"
|
||||||
}
|
}
|
||||||
|
|
||||||
|
@ -14,9 +73,10 @@ interface RelaunchMessage {
|
||||||
version: string
|
version: string
|
||||||
}
|
}
|
||||||
|
|
||||||
export type Message = RelaunchMessage | HandshakeMessage
|
type ChildMessage = RelaunchMessage | ChildHandshakeMessage
|
||||||
|
type ParentMessage = ParentHandshakeMessage
|
||||||
|
|
||||||
export class ProcessError extends Error {
|
class ProcessError extends Error {
|
||||||
public constructor(message: string, public readonly code: number | undefined) {
|
public constructor(message: string, public readonly code: number | undefined) {
|
||||||
super(message)
|
super(message)
|
||||||
this.name = this.constructor.name
|
this.name = this.constructor.name
|
||||||
|
@ -25,52 +85,55 @@ export class ProcessError extends Error {
|
||||||
}
|
}
|
||||||
|
|
||||||
/**
|
/**
|
||||||
* Allows the wrapper and inner processes to communicate.
|
* Wrapper around a process that tries to gracefully exit when a process exits
|
||||||
|
* and provides a way to prevent `process.exit`.
|
||||||
*/
|
*/
|
||||||
export class IpcMain {
|
abstract class Process {
|
||||||
private readonly _onMessage = new Emitter<Message>()
|
/**
|
||||||
public readonly onMessage = this._onMessage.event
|
* Emit this to trigger a graceful exit.
|
||||||
private readonly _onDispose = new Emitter<NodeJS.Signals | undefined>()
|
*/
|
||||||
public readonly onDispose = this._onDispose.event
|
protected readonly _onDispose = new Emitter<NodeJS.Signals | undefined>()
|
||||||
public readonly processExit: (code?: number) => never
|
|
||||||
|
|
||||||
public constructor(public readonly parentPid?: number) {
|
/**
|
||||||
|
* Emitted when the process is about to be disposed.
|
||||||
|
*/
|
||||||
|
public readonly onDispose = this._onDispose.event
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Uniquely named logger for the process.
|
||||||
|
*/
|
||||||
|
public abstract logger: Logger
|
||||||
|
|
||||||
|
public constructor() {
|
||||||
process.on("SIGINT", () => this._onDispose.emit("SIGINT"))
|
process.on("SIGINT", () => this._onDispose.emit("SIGINT"))
|
||||||
process.on("SIGTERM", () => this._onDispose.emit("SIGTERM"))
|
process.on("SIGTERM", () => this._onDispose.emit("SIGTERM"))
|
||||||
process.on("exit", () => this._onDispose.emit(undefined))
|
process.on("exit", () => this._onDispose.emit(undefined))
|
||||||
|
|
||||||
// Ensure we control when the process exits.
|
this.onDispose((signal, wait) => {
|
||||||
this.processExit = process.exit
|
|
||||||
process.exit = function (code?: number) {
|
|
||||||
logger.warn(`process.exit() was prevented: ${code || "unknown code"}.`)
|
|
||||||
} as (code?: number) => never
|
|
||||||
|
|
||||||
this.onDispose((signal) => {
|
|
||||||
// Remove listeners to avoid possibly triggering disposal again.
|
// Remove listeners to avoid possibly triggering disposal again.
|
||||||
process.removeAllListeners()
|
process.removeAllListeners()
|
||||||
|
|
||||||
// Let any other handlers run first then exit.
|
// Try waiting for other handlers to run first then exit.
|
||||||
logger.debug(`${parentPid ? "inner process" : "wrapper"} ${process.pid} disposing`, field("code", signal))
|
this.logger.debug("disposing", field("code", signal))
|
||||||
setTimeout(() => this.exit(0), 0)
|
wait.then(() => this.exit(0))
|
||||||
|
setTimeout(() => this.exit(0), 5000)
|
||||||
})
|
})
|
||||||
|
|
||||||
// Kill the inner process if the parent dies. This is for the case where the
|
|
||||||
// parent process is forcefully terminated and cannot clean up.
|
|
||||||
if (parentPid) {
|
|
||||||
setInterval(() => {
|
|
||||||
try {
|
|
||||||
// process.kill throws an exception if the process doesn't exist.
|
|
||||||
process.kill(parentPid, 0)
|
|
||||||
} catch (_) {
|
|
||||||
// Consider this an error since it should have been able to clean up
|
|
||||||
// the child process unless it was forcefully killed.
|
|
||||||
logger.error(`parent process ${parentPid} died`)
|
|
||||||
this._onDispose.emit(undefined)
|
|
||||||
}
|
}
|
||||||
}, 5000)
|
|
||||||
|
/**
|
||||||
|
* Ensure control over when the process exits.
|
||||||
|
*/
|
||||||
|
public preventExit(): void {
|
||||||
|
;(process.exit as any) = (code?: number) => {
|
||||||
|
this.logger.warn(`process.exit() was prevented: ${code || "unknown code"}.`)
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
private readonly processExit: (code?: number) => never = process.exit
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Will always exit even if normal exit is being prevented.
|
||||||
|
*/
|
||||||
public exit(error?: number | ProcessError): never {
|
public exit(error?: number | ProcessError): never {
|
||||||
if (error && typeof error !== "number") {
|
if (error && typeof error !== "number") {
|
||||||
this.processExit(typeof error.code === "number" ? error.code : 1)
|
this.processExit(typeof error.code === "number" ? error.code : 1)
|
||||||
|
@ -78,48 +141,59 @@ export class IpcMain {
|
||||||
this.processExit(error)
|
this.processExit(error)
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
}
|
||||||
|
|
||||||
public handshake(child?: cp.ChildProcess): Promise<void> {
|
/**
|
||||||
return new Promise((resolve, reject) => {
|
* Child process that will clean up after itself if the parent goes away and can
|
||||||
const target = child || process
|
* perform a handshake with the parent and ask it to relaunch.
|
||||||
const onMessage = (message: Message): void => {
|
*/
|
||||||
logger.debug(
|
class ChildProcess extends Process {
|
||||||
`${child ? "wrapper" : "inner process"} ${process.pid} received message from ${
|
public logger = logger.named(`child:${process.pid}`)
|
||||||
child ? child.pid : this.parentPid
|
|
||||||
}`,
|
public constructor(private readonly parentPid: number) {
|
||||||
field("message", message),
|
super()
|
||||||
)
|
|
||||||
if (message.type === "handshake") {
|
// Kill the inner process if the parent dies. This is for the case where the
|
||||||
target.removeListener("message", onMessage)
|
// parent process is forcefully terminated and cannot clean up.
|
||||||
target.on("message", (msg) => this._onMessage.emit(msg))
|
setInterval(() => {
|
||||||
// The wrapper responds once the inner process starts the handshake.
|
try {
|
||||||
if (child) {
|
// process.kill throws an exception if the process doesn't exist.
|
||||||
if (!target.send) {
|
process.kill(this.parentPid, 0)
|
||||||
throw new Error("child not spawned with IPC")
|
} catch (_) {
|
||||||
|
// Consider this an error since it should have been able to clean up
|
||||||
|
// the child process unless it was forcefully killed.
|
||||||
|
this.logger.error(`parent process ${parentPid} died`)
|
||||||
|
this._onDispose.emit(undefined)
|
||||||
}
|
}
|
||||||
target.send({ type: "handshake" })
|
}, 5000)
|
||||||
}
|
}
|
||||||
resolve()
|
|
||||||
}
|
/**
|
||||||
}
|
* Initiate the handshake and wait for a response from the parent.
|
||||||
target.on("message", onMessage)
|
*/
|
||||||
if (child) {
|
public async handshake(): Promise<DefaultedArgs> {
|
||||||
child.once("error", reject)
|
|
||||||
child.once("exit", (code) => {
|
|
||||||
reject(new ProcessError(`Unexpected exit with code ${code}`, code !== null ? code : undefined))
|
|
||||||
})
|
|
||||||
} else {
|
|
||||||
// The inner process initiates the handshake.
|
|
||||||
this.send({ type: "handshake" })
|
this.send({ type: "handshake" })
|
||||||
}
|
const message = await onMessage<ParentMessage, ParentHandshakeMessage>(
|
||||||
})
|
process,
|
||||||
|
(message): message is ParentHandshakeMessage => {
|
||||||
|
return message.type === "handshake"
|
||||||
|
},
|
||||||
|
this.logger,
|
||||||
|
)
|
||||||
|
return message.args
|
||||||
}
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Notify the parent process that it should relaunch the child.
|
||||||
|
*/
|
||||||
public relaunch(version: string): void {
|
public relaunch(version: string): void {
|
||||||
this.send({ type: "relaunch", version })
|
this.send({ type: "relaunch", version })
|
||||||
}
|
}
|
||||||
|
|
||||||
private send(message: Message): void {
|
/**
|
||||||
|
* Send a message to the parent.
|
||||||
|
*/
|
||||||
|
private send(message: ChildMessage): void {
|
||||||
if (!process.send) {
|
if (!process.send) {
|
||||||
throw new Error("not spawned with IPC")
|
throw new Error("not spawned with IPC")
|
||||||
}
|
}
|
||||||
|
@ -127,34 +201,32 @@ export class IpcMain {
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
let _ipcMain: IpcMain
|
|
||||||
export const ipcMain = (): IpcMain => {
|
|
||||||
if (!_ipcMain) {
|
|
||||||
_ipcMain = new IpcMain(
|
|
||||||
typeof process.env.CODE_SERVER_PARENT_PID !== "undefined"
|
|
||||||
? parseInt(process.env.CODE_SERVER_PARENT_PID)
|
|
||||||
: undefined,
|
|
||||||
)
|
|
||||||
}
|
|
||||||
return _ipcMain
|
|
||||||
}
|
|
||||||
|
|
||||||
export interface WrapperOptions {
|
|
||||||
maxMemory?: number
|
|
||||||
nodeOptions?: string
|
|
||||||
}
|
|
||||||
|
|
||||||
/**
|
/**
|
||||||
* Provides a way to wrap a process for the purpose of updating the running
|
* Parent process wrapper that spawns the child process and performs a handshake
|
||||||
* instance.
|
* with it. Will relaunch the child if it receives a SIGUSR1 or is asked to by
|
||||||
|
* the child. If the child otherwise exits the parent will also exit.
|
||||||
*/
|
*/
|
||||||
export class WrapperProcess {
|
export class ParentProcess extends Process {
|
||||||
private process?: cp.ChildProcess
|
public logger = logger.named(`parent:${process.pid}`)
|
||||||
|
|
||||||
|
private child?: cp.ChildProcess
|
||||||
private started?: Promise<void>
|
private started?: Promise<void>
|
||||||
private readonly logStdoutStream: rfs.RotatingFileStream
|
private readonly logStdoutStream: rfs.RotatingFileStream
|
||||||
private readonly logStderrStream: rfs.RotatingFileStream
|
private readonly logStderrStream: rfs.RotatingFileStream
|
||||||
|
|
||||||
public constructor(private currentVersion: string, private readonly options?: WrapperOptions) {
|
protected readonly _onChildMessage = new Emitter<ChildMessage>()
|
||||||
|
protected readonly onChildMessage = this._onChildMessage.event
|
||||||
|
|
||||||
|
private args?: DefaultedArgs
|
||||||
|
|
||||||
|
public constructor(private currentVersion: string) {
|
||||||
|
super()
|
||||||
|
|
||||||
|
process.on("SIGUSR1", async () => {
|
||||||
|
this.logger.info("Received SIGUSR1; hotswapping")
|
||||||
|
this.relaunch()
|
||||||
|
})
|
||||||
|
|
||||||
const opts = {
|
const opts = {
|
||||||
size: "10M",
|
size: "10M",
|
||||||
maxFiles: 10,
|
maxFiles: 10,
|
||||||
|
@ -162,49 +234,56 @@ export class WrapperProcess {
|
||||||
this.logStdoutStream = rfs.createStream(path.join(paths.data, "coder-logs", "code-server-stdout.log"), opts)
|
this.logStdoutStream = rfs.createStream(path.join(paths.data, "coder-logs", "code-server-stdout.log"), opts)
|
||||||
this.logStderrStream = rfs.createStream(path.join(paths.data, "coder-logs", "code-server-stderr.log"), opts)
|
this.logStderrStream = rfs.createStream(path.join(paths.data, "coder-logs", "code-server-stderr.log"), opts)
|
||||||
|
|
||||||
ipcMain().onDispose(() => {
|
this.onDispose(() => {
|
||||||
if (this.process) {
|
this.disposeChild()
|
||||||
this.process.removeAllListeners()
|
|
||||||
this.process.kill()
|
|
||||||
}
|
|
||||||
})
|
})
|
||||||
|
|
||||||
ipcMain().onMessage((message) => {
|
this.onChildMessage((message) => {
|
||||||
switch (message.type) {
|
switch (message.type) {
|
||||||
case "relaunch":
|
case "relaunch":
|
||||||
logger.info(`Relaunching: ${this.currentVersion} -> ${message.version}`)
|
this.logger.info(`Relaunching: ${this.currentVersion} -> ${message.version}`)
|
||||||
this.currentVersion = message.version
|
this.currentVersion = message.version
|
||||||
this.relaunch()
|
this.relaunch()
|
||||||
break
|
break
|
||||||
default:
|
default:
|
||||||
logger.error(`Unrecognized message ${message}`)
|
this.logger.error(`Unrecognized message ${message}`)
|
||||||
break
|
break
|
||||||
}
|
}
|
||||||
})
|
})
|
||||||
|
}
|
||||||
|
|
||||||
process.on("SIGUSR1", async () => {
|
private disposeChild(): void {
|
||||||
logger.info("Received SIGUSR1; hotswapping")
|
this.started = undefined
|
||||||
this.relaunch()
|
if (this.child) {
|
||||||
})
|
this.child.removeAllListeners()
|
||||||
|
this.child.kill()
|
||||||
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
private async relaunch(): Promise<void> {
|
private async relaunch(): Promise<void> {
|
||||||
this.started = undefined
|
this.disposeChild()
|
||||||
if (this.process) {
|
|
||||||
this.process.removeAllListeners()
|
|
||||||
this.process.kill()
|
|
||||||
}
|
|
||||||
try {
|
try {
|
||||||
await this.start()
|
this.started = this._start()
|
||||||
|
await this.started
|
||||||
} catch (error) {
|
} catch (error) {
|
||||||
logger.error(error.message)
|
this.logger.error(error.message)
|
||||||
ipcMain().exit(typeof error.code === "number" ? error.code : 1)
|
this.exit(typeof error.code === "number" ? error.code : 1)
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
public start(): Promise<void> {
|
public start(args: DefaultedArgs): Promise<void> {
|
||||||
|
// Store for relaunches.
|
||||||
|
this.args = args
|
||||||
if (!this.started) {
|
if (!this.started) {
|
||||||
this.started = this.spawn().then((child) => {
|
this.started = this._start()
|
||||||
|
}
|
||||||
|
return this.started
|
||||||
|
}
|
||||||
|
|
||||||
|
private async _start(): Promise<void> {
|
||||||
|
const child = this.spawn()
|
||||||
|
this.child = child
|
||||||
|
|
||||||
// Log both to stdout and to the log directory.
|
// Log both to stdout and to the log directory.
|
||||||
if (child.stdout) {
|
if (child.stdout) {
|
||||||
child.stdout.pipe(this.logStdoutStream)
|
child.stdout.pipe(this.logStdoutStream)
|
||||||
|
@ -214,60 +293,76 @@ export class WrapperProcess {
|
||||||
child.stderr.pipe(this.logStderrStream)
|
child.stderr.pipe(this.logStderrStream)
|
||||||
child.stderr.pipe(process.stderr)
|
child.stderr.pipe(process.stderr)
|
||||||
}
|
}
|
||||||
logger.debug(`spawned inner process ${child.pid}`)
|
|
||||||
ipcMain()
|
this.logger.debug(`spawned inner process ${child.pid}`)
|
||||||
.handshake(child)
|
|
||||||
.then(() => {
|
await this.handshake(child)
|
||||||
|
|
||||||
child.once("exit", (code) => {
|
child.once("exit", (code) => {
|
||||||
logger.debug(`inner process ${child.pid} exited unexpectedly`)
|
this.logger.debug(`inner process ${child.pid} exited unexpectedly`)
|
||||||
ipcMain().exit(code || 0)
|
this.exit(code || 0)
|
||||||
})
|
})
|
||||||
})
|
|
||||||
this.process = child
|
|
||||||
})
|
|
||||||
}
|
|
||||||
return this.started
|
|
||||||
}
|
|
||||||
|
|
||||||
private async spawn(): Promise<cp.ChildProcess> {
|
|
||||||
// Flags to pass along to the Node binary.
|
|
||||||
let nodeOptions = `${process.env.NODE_OPTIONS || ""} ${(this.options && this.options.nodeOptions) || ""}`
|
|
||||||
if (!/max_old_space_size=(\d+)/g.exec(nodeOptions)) {
|
|
||||||
nodeOptions += ` --max_old_space_size=${(this.options && this.options.maxMemory) || 2048}`
|
|
||||||
}
|
}
|
||||||
|
|
||||||
|
private spawn(): cp.ChildProcess {
|
||||||
// Use spawn (instead of fork) to use the new binary in case it was updated.
|
// Use spawn (instead of fork) to use the new binary in case it was updated.
|
||||||
return cp.spawn(process.argv[0], process.argv.slice(1), {
|
return cp.spawn(process.argv[0], process.argv.slice(1), {
|
||||||
env: {
|
env: {
|
||||||
...process.env,
|
...process.env,
|
||||||
CODE_SERVER_PARENT_PID: process.pid.toString(),
|
CODE_SERVER_PARENT_PID: process.pid.toString(),
|
||||||
NODE_OPTIONS: nodeOptions,
|
NODE_OPTIONS: `--max-old-space-size=2048 ${process.env.NODE_OPTIONS || ""}`,
|
||||||
},
|
},
|
||||||
stdio: ["ipc"],
|
stdio: ["ipc"],
|
||||||
})
|
})
|
||||||
}
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Wait for a handshake from the child then reply.
|
||||||
|
*/
|
||||||
|
private async handshake(child: cp.ChildProcess): Promise<void> {
|
||||||
|
if (!this.args) {
|
||||||
|
throw new Error("started without args")
|
||||||
|
}
|
||||||
|
await onMessage<ChildMessage, ChildHandshakeMessage>(
|
||||||
|
child,
|
||||||
|
(message): message is ChildHandshakeMessage => {
|
||||||
|
return message.type === "handshake"
|
||||||
|
},
|
||||||
|
this.logger,
|
||||||
|
)
|
||||||
|
this.send(child, { type: "handshake", args: this.args })
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Send a message to the child.
|
||||||
|
*/
|
||||||
|
private send(child: cp.ChildProcess, message: ParentMessage): void {
|
||||||
|
child.send(message)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Process wrapper.
|
||||||
|
*/
|
||||||
|
export const wrapper =
|
||||||
|
typeof process.env.CODE_SERVER_PARENT_PID !== "undefined"
|
||||||
|
? new ChildProcess(parseInt(process.env.CODE_SERVER_PARENT_PID))
|
||||||
|
: new ParentProcess(require("../../package.json").version)
|
||||||
|
|
||||||
|
export function isChild(proc: ChildProcess | ParentProcess): proc is ChildProcess {
|
||||||
|
return proc instanceof ChildProcess
|
||||||
}
|
}
|
||||||
|
|
||||||
// It's possible that the pipe has closed (for example if you run code-server
|
// It's possible that the pipe has closed (for example if you run code-server
|
||||||
// --version | head -1). Assume that means we're done.
|
// --version | head -1). Assume that means we're done.
|
||||||
if (!process.stdout.isTTY) {
|
if (!process.stdout.isTTY) {
|
||||||
process.stdout.on("error", () => ipcMain().exit())
|
process.stdout.on("error", () => wrapper.exit())
|
||||||
}
|
}
|
||||||
|
|
||||||
export const wrap = (fn: () => Promise<void>): void => {
|
// Don't let uncaught exceptions crash the process.
|
||||||
if (ipcMain().parentPid) {
|
process.on("uncaughtException", (error) => {
|
||||||
ipcMain()
|
wrapper.logger.error(`Uncaught exception: ${error.message}`)
|
||||||
.handshake()
|
if (typeof error.stack !== "undefined") {
|
||||||
.then(() => fn())
|
wrapper.logger.error(error.stack)
|
||||||
.catch((error: ProcessError): void => {
|
|
||||||
logger.error(error.message)
|
|
||||||
ipcMain().exit(error)
|
|
||||||
})
|
|
||||||
} else {
|
|
||||||
const wrapper = new WrapperProcess(require("../../package.json").version)
|
|
||||||
wrapper.start().catch((error) => {
|
|
||||||
logger.error(error.message)
|
|
||||||
ipcMain().exit(error)
|
|
||||||
})
|
|
||||||
}
|
}
|
||||||
}
|
})
|
||||||
|
|
|
@ -0,0 +1,57 @@
|
||||||
|
import * as express from "express"
|
||||||
|
import * as expressCore from "express-serve-static-core"
|
||||||
|
import * as http from "http"
|
||||||
|
import * as net from "net"
|
||||||
|
|
||||||
|
export const handleUpgrade = (app: express.Express, server: http.Server): void => {
|
||||||
|
server.on("upgrade", (req, socket, head) => {
|
||||||
|
socket.pause()
|
||||||
|
|
||||||
|
req.ws = socket
|
||||||
|
req.head = head
|
||||||
|
req._ws_handled = false
|
||||||
|
|
||||||
|
// Send the request off to be handled by Express.
|
||||||
|
;(app as any).handle(req, new http.ServerResponse(req), () => {
|
||||||
|
if (!req._ws_handled) {
|
||||||
|
socket.end("HTTP/1.1 404 Not Found\r\n\r\n")
|
||||||
|
}
|
||||||
|
})
|
||||||
|
})
|
||||||
|
}
|
||||||
|
|
||||||
|
export interface WebsocketRequest extends express.Request {
|
||||||
|
ws: net.Socket
|
||||||
|
head: Buffer
|
||||||
|
}
|
||||||
|
|
||||||
|
interface InternalWebsocketRequest extends WebsocketRequest {
|
||||||
|
_ws_handled: boolean
|
||||||
|
}
|
||||||
|
|
||||||
|
export type WebSocketHandler = (
|
||||||
|
req: WebsocketRequest,
|
||||||
|
res: express.Response,
|
||||||
|
next: express.NextFunction,
|
||||||
|
) => void | Promise<void>
|
||||||
|
|
||||||
|
export class WebsocketRouter {
|
||||||
|
public readonly router = express.Router()
|
||||||
|
|
||||||
|
public ws(route: expressCore.PathParams, ...handlers: WebSocketHandler[]): void {
|
||||||
|
this.router.get(
|
||||||
|
route,
|
||||||
|
...handlers.map((handler) => {
|
||||||
|
const wrapped: express.Handler = (req, res, next) => {
|
||||||
|
;(req as InternalWebsocketRequest)._ws_handled = true
|
||||||
|
return handler(req as WebsocketRequest, res, next)
|
||||||
|
}
|
||||||
|
return wrapped
|
||||||
|
}),
|
||||||
|
)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
export function Router(): WebsocketRouter {
|
||||||
|
return new WebsocketRouter()
|
||||||
|
}
|
225
test/cli.test.ts
225
test/cli.test.ts
|
@ -1,20 +1,37 @@
|
||||||
import { logger, Level } from "@coder/logger"
|
import { Level, logger } from "@coder/logger"
|
||||||
import * as assert from "assert"
|
import * as assert from "assert"
|
||||||
|
import * as fs from "fs-extra"
|
||||||
|
import * as net from "net"
|
||||||
|
import * as os from "os"
|
||||||
import * as path from "path"
|
import * as path from "path"
|
||||||
import { parse } from "../src/node/cli"
|
import { Args, parse, setDefaults, shouldOpenInExistingInstance } from "../src/node/cli"
|
||||||
|
import { paths, tmpdir } from "../src/node/util"
|
||||||
|
|
||||||
describe("cli", () => {
|
type Mutable<T> = {
|
||||||
|
-readonly [P in keyof T]: T[P]
|
||||||
|
}
|
||||||
|
|
||||||
|
describe("parser", () => {
|
||||||
beforeEach(() => {
|
beforeEach(() => {
|
||||||
delete process.env.LOG_LEVEL
|
delete process.env.LOG_LEVEL
|
||||||
|
delete process.env.PASSWORD
|
||||||
})
|
})
|
||||||
|
|
||||||
// The parser will always fill these out.
|
// The parser should not set any defaults so the caller can determine what
|
||||||
|
// values the user actually set. These are only set after explicitly calling
|
||||||
|
// `setDefaults`.
|
||||||
const defaults = {
|
const defaults = {
|
||||||
_: [],
|
auth: "password",
|
||||||
|
host: "localhost",
|
||||||
|
port: 8080,
|
||||||
|
"proxy-domain": [],
|
||||||
|
usingEnvPassword: false,
|
||||||
|
"extensions-dir": path.join(paths.data, "extensions"),
|
||||||
|
"user-data-dir": paths.data,
|
||||||
}
|
}
|
||||||
|
|
||||||
it("should set defaults", () => {
|
it("should parse nothing", () => {
|
||||||
assert.deepEqual(parse([]), defaults)
|
assert.deepEqual(parse([]), { _: [] })
|
||||||
})
|
})
|
||||||
|
|
||||||
it("should parse all available options", () => {
|
it("should parse all available options", () => {
|
||||||
|
@ -69,7 +86,7 @@ describe("cli", () => {
|
||||||
help: true,
|
help: true,
|
||||||
host: "0.0.0.0",
|
host: "0.0.0.0",
|
||||||
json: true,
|
json: true,
|
||||||
log: "trace",
|
log: "error",
|
||||||
open: true,
|
open: true,
|
||||||
port: 8081,
|
port: 8081,
|
||||||
socket: path.resolve("mumble"),
|
socket: path.resolve("mumble"),
|
||||||
|
@ -83,19 +100,20 @@ describe("cli", () => {
|
||||||
|
|
||||||
it("should work with short options", () => {
|
it("should work with short options", () => {
|
||||||
assert.deepEqual(parse(["-vvv", "-v"]), {
|
assert.deepEqual(parse(["-vvv", "-v"]), {
|
||||||
...defaults,
|
_: [],
|
||||||
log: "trace",
|
|
||||||
verbose: true,
|
verbose: true,
|
||||||
version: true,
|
version: true,
|
||||||
})
|
})
|
||||||
assert.equal(process.env.LOG_LEVEL, "trace")
|
|
||||||
assert.equal(logger.level, Level.Trace)
|
|
||||||
})
|
})
|
||||||
|
|
||||||
it("should use log level env var", () => {
|
it("should use log level env var", async () => {
|
||||||
|
const args = parse([])
|
||||||
|
assert.deepEqual(args, { _: [] })
|
||||||
|
|
||||||
process.env.LOG_LEVEL = "debug"
|
process.env.LOG_LEVEL = "debug"
|
||||||
assert.deepEqual(parse([]), {
|
assert.deepEqual(await setDefaults(args), {
|
||||||
...defaults,
|
...defaults,
|
||||||
|
_: [],
|
||||||
log: "debug",
|
log: "debug",
|
||||||
verbose: false,
|
verbose: false,
|
||||||
})
|
})
|
||||||
|
@ -103,8 +121,9 @@ describe("cli", () => {
|
||||||
assert.equal(logger.level, Level.Debug)
|
assert.equal(logger.level, Level.Debug)
|
||||||
|
|
||||||
process.env.LOG_LEVEL = "trace"
|
process.env.LOG_LEVEL = "trace"
|
||||||
assert.deepEqual(parse([]), {
|
assert.deepEqual(await setDefaults(args), {
|
||||||
...defaults,
|
...defaults,
|
||||||
|
_: [],
|
||||||
log: "trace",
|
log: "trace",
|
||||||
verbose: true,
|
verbose: true,
|
||||||
})
|
})
|
||||||
|
@ -113,9 +132,16 @@ describe("cli", () => {
|
||||||
})
|
})
|
||||||
|
|
||||||
it("should prefer --log to env var and --verbose to --log", async () => {
|
it("should prefer --log to env var and --verbose to --log", async () => {
|
||||||
|
let args = parse(["--log", "info"])
|
||||||
|
assert.deepEqual(args, {
|
||||||
|
_: [],
|
||||||
|
log: "info",
|
||||||
|
})
|
||||||
|
|
||||||
process.env.LOG_LEVEL = "debug"
|
process.env.LOG_LEVEL = "debug"
|
||||||
assert.deepEqual(parse(["--log", "info"]), {
|
assert.deepEqual(await setDefaults(args), {
|
||||||
...defaults,
|
...defaults,
|
||||||
|
_: [],
|
||||||
log: "info",
|
log: "info",
|
||||||
verbose: false,
|
verbose: false,
|
||||||
})
|
})
|
||||||
|
@ -123,17 +149,26 @@ describe("cli", () => {
|
||||||
assert.equal(logger.level, Level.Info)
|
assert.equal(logger.level, Level.Info)
|
||||||
|
|
||||||
process.env.LOG_LEVEL = "trace"
|
process.env.LOG_LEVEL = "trace"
|
||||||
assert.deepEqual(parse(["--log", "info"]), {
|
assert.deepEqual(await setDefaults(args), {
|
||||||
...defaults,
|
...defaults,
|
||||||
|
_: [],
|
||||||
log: "info",
|
log: "info",
|
||||||
verbose: false,
|
verbose: false,
|
||||||
})
|
})
|
||||||
assert.equal(process.env.LOG_LEVEL, "info")
|
assert.equal(process.env.LOG_LEVEL, "info")
|
||||||
assert.equal(logger.level, Level.Info)
|
assert.equal(logger.level, Level.Info)
|
||||||
|
|
||||||
|
args = parse(["--log", "info", "--verbose"])
|
||||||
|
assert.deepEqual(args, {
|
||||||
|
_: [],
|
||||||
|
log: "info",
|
||||||
|
verbose: true,
|
||||||
|
})
|
||||||
|
|
||||||
process.env.LOG_LEVEL = "warn"
|
process.env.LOG_LEVEL = "warn"
|
||||||
assert.deepEqual(parse(["--log", "info", "--verbose"]), {
|
assert.deepEqual(await setDefaults(args), {
|
||||||
...defaults,
|
...defaults,
|
||||||
|
_: [],
|
||||||
log: "trace",
|
log: "trace",
|
||||||
verbose: true,
|
verbose: true,
|
||||||
})
|
})
|
||||||
|
@ -141,9 +176,12 @@ describe("cli", () => {
|
||||||
assert.equal(logger.level, Level.Trace)
|
assert.equal(logger.level, Level.Trace)
|
||||||
})
|
})
|
||||||
|
|
||||||
it("should ignore invalid log level env var", () => {
|
it("should ignore invalid log level env var", async () => {
|
||||||
process.env.LOG_LEVEL = "bogus"
|
process.env.LOG_LEVEL = "bogus"
|
||||||
assert.deepEqual(parse([]), defaults)
|
assert.deepEqual(await setDefaults(parse([])), {
|
||||||
|
_: [],
|
||||||
|
...defaults,
|
||||||
|
})
|
||||||
})
|
})
|
||||||
|
|
||||||
it("should error if value isn't provided", () => {
|
it("should error if value isn't provided", () => {
|
||||||
|
@ -166,7 +204,7 @@ describe("cli", () => {
|
||||||
|
|
||||||
it("should not error if the value is optional", () => {
|
it("should not error if the value is optional", () => {
|
||||||
assert.deepEqual(parse(["--cert"]), {
|
assert.deepEqual(parse(["--cert"]), {
|
||||||
...defaults,
|
_: [],
|
||||||
cert: {
|
cert: {
|
||||||
value: undefined,
|
value: undefined,
|
||||||
},
|
},
|
||||||
|
@ -177,7 +215,7 @@ describe("cli", () => {
|
||||||
assert.throws(() => parse(["--socket", "--socket-path-value"]), /--socket requires a value/)
|
assert.throws(() => parse(["--socket", "--socket-path-value"]), /--socket requires a value/)
|
||||||
// If you actually had a path like this you would do this instead:
|
// If you actually had a path like this you would do this instead:
|
||||||
assert.deepEqual(parse(["--socket", "./--socket-path-value"]), {
|
assert.deepEqual(parse(["--socket", "./--socket-path-value"]), {
|
||||||
...defaults,
|
_: [],
|
||||||
socket: path.resolve("--socket-path-value"),
|
socket: path.resolve("--socket-path-value"),
|
||||||
})
|
})
|
||||||
assert.throws(() => parse(["--cert", "--socket-path-value"]), /Unknown option --socket-path-value/)
|
assert.throws(() => parse(["--cert", "--socket-path-value"]), /Unknown option --socket-path-value/)
|
||||||
|
@ -185,7 +223,6 @@ describe("cli", () => {
|
||||||
|
|
||||||
it("should allow positional arguments before options", () => {
|
it("should allow positional arguments before options", () => {
|
||||||
assert.deepEqual(parse(["foo", "test", "--auth", "none"]), {
|
assert.deepEqual(parse(["foo", "test", "--auth", "none"]), {
|
||||||
...defaults,
|
|
||||||
_: ["foo", "test"],
|
_: ["foo", "test"],
|
||||||
auth: "none",
|
auth: "none",
|
||||||
})
|
})
|
||||||
|
@ -193,12 +230,150 @@ describe("cli", () => {
|
||||||
|
|
||||||
it("should support repeatable flags", () => {
|
it("should support repeatable flags", () => {
|
||||||
assert.deepEqual(parse(["--proxy-domain", "*.coder.com"]), {
|
assert.deepEqual(parse(["--proxy-domain", "*.coder.com"]), {
|
||||||
...defaults,
|
_: [],
|
||||||
"proxy-domain": ["*.coder.com"],
|
"proxy-domain": ["*.coder.com"],
|
||||||
})
|
})
|
||||||
assert.deepEqual(parse(["--proxy-domain", "*.coder.com", "--proxy-domain", "test.com"]), {
|
assert.deepEqual(parse(["--proxy-domain", "*.coder.com", "--proxy-domain", "test.com"]), {
|
||||||
...defaults,
|
_: [],
|
||||||
"proxy-domain": ["*.coder.com", "test.com"],
|
"proxy-domain": ["*.coder.com", "test.com"],
|
||||||
})
|
})
|
||||||
})
|
})
|
||||||
|
|
||||||
|
it("should enforce cert-key with cert value or otherwise generate one", async () => {
|
||||||
|
const args = parse(["--cert"])
|
||||||
|
assert.deepEqual(args, {
|
||||||
|
_: [],
|
||||||
|
cert: {
|
||||||
|
value: undefined,
|
||||||
|
},
|
||||||
|
})
|
||||||
|
assert.throws(() => parse(["--cert", "test"]), /--cert-key is missing/)
|
||||||
|
assert.deepEqual(await setDefaults(args), {
|
||||||
|
_: [],
|
||||||
|
...defaults,
|
||||||
|
cert: {
|
||||||
|
value: path.join(paths.data, "localhost.crt"),
|
||||||
|
},
|
||||||
|
"cert-key": path.join(paths.data, "localhost.key"),
|
||||||
|
})
|
||||||
|
})
|
||||||
|
|
||||||
|
it("should override with --link", async () => {
|
||||||
|
const args = parse("--cert test --cert-key test --socket test --host 0.0.0.0 --port 8888 --link test".split(" "))
|
||||||
|
assert.deepEqual(await setDefaults(args), {
|
||||||
|
_: [],
|
||||||
|
...defaults,
|
||||||
|
auth: "none",
|
||||||
|
host: "localhost",
|
||||||
|
link: {
|
||||||
|
value: "test",
|
||||||
|
},
|
||||||
|
port: 0,
|
||||||
|
cert: undefined,
|
||||||
|
"cert-key": path.resolve("test"),
|
||||||
|
socket: undefined,
|
||||||
|
})
|
||||||
|
})
|
||||||
|
|
||||||
|
it("should use env var password", async () => {
|
||||||
|
process.env.PASSWORD = "test"
|
||||||
|
const args = parse([])
|
||||||
|
assert.deepEqual(args, {
|
||||||
|
_: [],
|
||||||
|
})
|
||||||
|
|
||||||
|
assert.deepEqual(await setDefaults(args), {
|
||||||
|
...defaults,
|
||||||
|
_: [],
|
||||||
|
password: "test",
|
||||||
|
usingEnvPassword: true,
|
||||||
|
})
|
||||||
|
})
|
||||||
|
|
||||||
|
it("should filter proxy domains", async () => {
|
||||||
|
const args = parse(["--proxy-domain", "*.coder.com", "--proxy-domain", "coder.com", "--proxy-domain", "coder.org"])
|
||||||
|
assert.deepEqual(args, {
|
||||||
|
_: [],
|
||||||
|
"proxy-domain": ["*.coder.com", "coder.com", "coder.org"],
|
||||||
|
})
|
||||||
|
|
||||||
|
assert.deepEqual(await setDefaults(args), {
|
||||||
|
...defaults,
|
||||||
|
_: [],
|
||||||
|
"proxy-domain": ["coder.com", "coder.org"],
|
||||||
|
})
|
||||||
|
})
|
||||||
|
})
|
||||||
|
|
||||||
|
describe("cli", () => {
|
||||||
|
let args: Mutable<Args> = { _: [] }
|
||||||
|
const testDir = path.join(tmpdir, "tests/cli")
|
||||||
|
const vscodeIpcPath = path.join(os.tmpdir(), "vscode-ipc")
|
||||||
|
|
||||||
|
before(async () => {
|
||||||
|
await fs.remove(testDir)
|
||||||
|
await fs.mkdirp(testDir)
|
||||||
|
})
|
||||||
|
|
||||||
|
beforeEach(async () => {
|
||||||
|
delete process.env.VSCODE_IPC_HOOK_CLI
|
||||||
|
args = { _: [] }
|
||||||
|
await fs.remove(vscodeIpcPath)
|
||||||
|
})
|
||||||
|
|
||||||
|
it("should use existing if inside code-server", async () => {
|
||||||
|
process.env.VSCODE_IPC_HOOK_CLI = "test"
|
||||||
|
assert.strictEqual(await shouldOpenInExistingInstance(args), "test")
|
||||||
|
|
||||||
|
args.port = 8081
|
||||||
|
args._.push("./file")
|
||||||
|
assert.strictEqual(await shouldOpenInExistingInstance(args), "test")
|
||||||
|
})
|
||||||
|
|
||||||
|
it("should use existing if --reuse-window is set", async () => {
|
||||||
|
args["reuse-window"] = true
|
||||||
|
assert.strictEqual(await shouldOpenInExistingInstance(args), undefined)
|
||||||
|
|
||||||
|
await fs.writeFile(vscodeIpcPath, "test")
|
||||||
|
assert.strictEqual(await shouldOpenInExistingInstance(args), "test")
|
||||||
|
|
||||||
|
args.port = 8081
|
||||||
|
assert.strictEqual(await shouldOpenInExistingInstance(args), "test")
|
||||||
|
})
|
||||||
|
|
||||||
|
it("should use existing if --new-window is set", async () => {
|
||||||
|
args["new-window"] = true
|
||||||
|
assert.strictEqual(await shouldOpenInExistingInstance(args), undefined)
|
||||||
|
|
||||||
|
await fs.writeFile(vscodeIpcPath, "test")
|
||||||
|
assert.strictEqual(await shouldOpenInExistingInstance(args), "test")
|
||||||
|
|
||||||
|
args.port = 8081
|
||||||
|
assert.strictEqual(await shouldOpenInExistingInstance(args), "test")
|
||||||
|
})
|
||||||
|
|
||||||
|
it("should use existing if no unrelated flags are set, has positional, and socket is active", async () => {
|
||||||
|
assert.strictEqual(await shouldOpenInExistingInstance(args), undefined)
|
||||||
|
|
||||||
|
args._.push("./file")
|
||||||
|
assert.strictEqual(await shouldOpenInExistingInstance(args), undefined)
|
||||||
|
|
||||||
|
const socketPath = path.join(testDir, "socket")
|
||||||
|
await fs.writeFile(vscodeIpcPath, socketPath)
|
||||||
|
assert.strictEqual(await shouldOpenInExistingInstance(args), undefined)
|
||||||
|
|
||||||
|
await new Promise((resolve) => {
|
||||||
|
const server = net.createServer(() => {
|
||||||
|
// Close after getting the first connection.
|
||||||
|
server.close()
|
||||||
|
})
|
||||||
|
server.once("listening", () => resolve(server))
|
||||||
|
server.listen(socketPath)
|
||||||
|
})
|
||||||
|
|
||||||
|
assert.strictEqual(await shouldOpenInExistingInstance(args), socketPath)
|
||||||
|
|
||||||
|
args.port = 8081
|
||||||
|
assert.strictEqual(await shouldOpenInExistingInstance(args), undefined)
|
||||||
|
})
|
||||||
})
|
})
|
||||||
|
|
|
@ -0,0 +1,62 @@
|
||||||
|
import { logger } from "@coder/logger"
|
||||||
|
import * as express from "express"
|
||||||
|
import * as fs from "fs"
|
||||||
|
import { describe } from "mocha"
|
||||||
|
import * as path from "path"
|
||||||
|
import * as supertest from "supertest"
|
||||||
|
import { PluginAPI } from "../src/node/plugin"
|
||||||
|
import * as apps from "../src/node/routes/apps"
|
||||||
|
const fsp = fs.promises
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Use $LOG_LEVEL=debug to see debug logs.
|
||||||
|
*/
|
||||||
|
describe("plugin", () => {
|
||||||
|
let papi: PluginAPI
|
||||||
|
let app: express.Application
|
||||||
|
let agent: supertest.SuperAgentTest
|
||||||
|
|
||||||
|
before(async () => {
|
||||||
|
papi = new PluginAPI(logger, path.resolve(__dirname, "test-plugin") + ":meow")
|
||||||
|
await papi.loadPlugins()
|
||||||
|
|
||||||
|
app = express.default()
|
||||||
|
papi.mount(app)
|
||||||
|
|
||||||
|
app.use("/api/applications", apps.router(papi))
|
||||||
|
|
||||||
|
agent = supertest.agent(app)
|
||||||
|
})
|
||||||
|
|
||||||
|
it("/api/applications", async () => {
|
||||||
|
await agent.get("/api/applications").expect(200, [
|
||||||
|
{
|
||||||
|
name: "Test App",
|
||||||
|
version: "4.0.0",
|
||||||
|
|
||||||
|
description: "This app does XYZ.",
|
||||||
|
iconPath: "/test-plugin/test-app/icon.svg",
|
||||||
|
homepageURL: "https://example.com",
|
||||||
|
path: "/test-plugin/test-app",
|
||||||
|
|
||||||
|
plugin: {
|
||||||
|
name: "test-plugin",
|
||||||
|
version: "1.0.0",
|
||||||
|
modulePath: path.join(__dirname, "test-plugin"),
|
||||||
|
|
||||||
|
displayName: "Test Plugin",
|
||||||
|
description: "Plugin used in code-server tests.",
|
||||||
|
routerPath: "/test-plugin",
|
||||||
|
homepageURL: "https://example.com",
|
||||||
|
},
|
||||||
|
},
|
||||||
|
])
|
||||||
|
})
|
||||||
|
|
||||||
|
it("/test-plugin/test-app", async () => {
|
||||||
|
const indexHTML = await fsp.readFile(path.join(__dirname, "test-plugin/public/index.html"), {
|
||||||
|
encoding: "utf8",
|
||||||
|
})
|
||||||
|
await agent.get("/test-plugin/test-app").expect(200, indexHTML)
|
||||||
|
})
|
||||||
|
})
|
|
@ -45,7 +45,7 @@ describe("SocketProxyProvider", () => {
|
||||||
}
|
}
|
||||||
|
|
||||||
before(async () => {
|
before(async () => {
|
||||||
const cert = await generateCertificate()
|
const cert = await generateCertificate("localhost")
|
||||||
const options = {
|
const options = {
|
||||||
cert: fs.readFileSync(cert.cert),
|
cert: fs.readFileSync(cert.cert),
|
||||||
key: fs.readFileSync(cert.certKey),
|
key: fs.readFileSync(cert.certKey),
|
||||||
|
|
|
@ -0,0 +1 @@
|
||||||
|
out
|
|
@ -0,0 +1,6 @@
|
||||||
|
out/index.js: src/index.ts
|
||||||
|
# Typescript always emits, even on errors.
|
||||||
|
yarn build || rm out/index.js
|
||||||
|
|
||||||
|
node_modules: package.json yarn.lock
|
||||||
|
yarn
|
|
@ -0,0 +1,19 @@
|
||||||
|
{
|
||||||
|
"private": true,
|
||||||
|
"name": "test-plugin",
|
||||||
|
"version": "1.0.0",
|
||||||
|
"engines": {
|
||||||
|
"code-server": "^3.7.0"
|
||||||
|
},
|
||||||
|
"main": "out/index.js",
|
||||||
|
"devDependencies": {
|
||||||
|
"@types/express": "^4.17.8",
|
||||||
|
"typescript": "^4.0.5"
|
||||||
|
},
|
||||||
|
"scripts": {
|
||||||
|
"build": "tsc"
|
||||||
|
},
|
||||||
|
"dependencies": {
|
||||||
|
"express": "^4.17.1"
|
||||||
|
}
|
||||||
|
}
|
|
@ -0,0 +1 @@
|
||||||
|
<svg width="121" height="131" xmlns="http://www.w3.org/2000/svg"><defs><linearGradient x1="9.612%" y1="66.482%" x2="89.899%" y2="33.523%" id="a"><stop stop-color="#FCEE39" offset="0%"/><stop stop-color="#F37B3D" offset="100%"/></linearGradient><linearGradient x1="8.601%" y1="15.03%" x2="99.641%" y2="89.058%" id="b"><stop stop-color="#EF5A6B" offset="0%"/><stop stop-color="#F26F4E" offset="57%"/><stop stop-color="#F37B3D" offset="100%"/></linearGradient><linearGradient x1="90.118%" y1="69.931%" x2="17.938%" y2="38.628%" id="c"><stop stop-color="#7C59A4" offset="0%"/><stop stop-color="#AF4C92" offset="38.52%"/><stop stop-color="#DC4183" offset="76.54%"/><stop stop-color="#ED3D7D" offset="95.7%"/></linearGradient><linearGradient x1="91.376%" y1="19.144%" x2="18.895%" y2="70.21%" id="d"><stop stop-color="#EF5A6B" offset="0%"/><stop stop-color="#EE4E72" offset="36.4%"/><stop stop-color="#ED3D7D" offset="100%"/></linearGradient></defs><g fill="none"><path d="M118.623 71.8c.9-.8 1.4-1.9 1.5-3.2.1-2.6-1.8-4.7-4.4-4.9-1.2-.1-2.4.4-3.3 1.1l-83.8 45.9c-1.9.8-3.6 2.2-4.7 4.1-2.9 4.8-1.3 11 3.6 13.9 3.4 2 7.5 1.8 10.7-.2.2-.2.5-.3.7-.5l78-54.8c.4-.3 1.5-1.1 1.7-1.4z" fill="url(#a)" transform="translate(-.023)"/><path d="M118.823 65.1l-63.8-62.6c-1.4-1.5-3.4-2.5-5.7-2.5-4.3 0-7.7 3.5-7.7 7.7 0 2.1.8 3.9 2.1 5.3.4.4.8.7 1.2 1l67.4 57.7c.8.7 1.8 1.2 3 1.3 2.6.1 4.7-1.8 4.9-4.4 0-1.3-.5-2.6-1.4-3.5z" fill="url(#b)" transform="translate(-.023)"/><path d="M57.123 59.5c-.1 0-39.4-31-40.2-31.5l-1.8-.9c-5.8-2.2-12.2.8-14.4 6.6-1.9 5.1.2 10.7 4.6 13.4.7.4 1.3.7 2 .9.4.2 45.4 18.8 45.4 18.8 1.8.8 3.9.3 5.1-1.2 1.5-1.9 1.2-4.6-.7-6.1z" fill="url(#c)" transform="translate(-.023)"/><path d="M49.323 0c-1.7 0-3.3.6-4.6 1.5l-39.8 26.8c-.1.1-.2.1-.2.2h-.1c-1.7 1.2-3.1 3-3.9 5.1-2.2 5.8.8 12.3 6.6 14.4 3.6 1.4 7.5.7 10.4-1.4.7-.5 1.3-1 1.8-1.6l34.6-31.2c1.8-1.4 3-3.6 3-6.1 0-4.2-3.5-7.7-7.8-7.7z" fill="url(#d)" transform="translate(-.023)"/><path fill="#000" d="M34.6 37.4h51v51h-51z"/><path fill="#FFF" d="M39 78.8h19.1V82H39zm-.2-28l1.5-1.4c.4.5.8.8 1.3.8.6 0 .9-.4.9-1.2v-5.3h2.3V49c0 1-.3 1.8-.8 2.3-.5.5-1.3.8-2.3.8-1.5.1-2.3-.5-2.9-1.3zm6.5-7H52v1.9h-4.4V47h4v1.8h-4v1.3h4.5v2h-6.7zm9.7 2h-2.5v-2h7.3v2h-2.5v6.3H55zM39 54h4.3c1 0 1.8.3 2.3.7.3.3.5.8.5 1.4 0 1-.5 1.5-1.3 1.9 1 .3 1.6.9 1.6 2 0 1.4-1.2 2.3-3.1 2.3H39V54zm4.8 2.6c0-.5-.4-.7-1-.7h-1.5v1.5h1.4c.7-.1 1.1-.3 1.1-.8zM43 59h-1.8v1.5H43c.7 0 1.1-.3 1.1-.8s-.4-.7-1.1-.7zm3.8-5h3.9c1.3 0 2.1.3 2.7.9.5.5.7 1.1.7 1.9 0 1.3-.7 2.1-1.7 2.6l2 2.9h-2.6l-1.7-2.5h-1v2.5h-2.3V54zm3.8 4c.8 0 1.2-.4 1.2-1 0-.7-.5-1-1.2-1h-1.5v2h1.5z"/><path d="M56.8 54H59l3.5 8.4H60l-.6-1.5h-3.2l-.6 1.5h-2.4l3.6-8.4zm2 5l-.9-2.3L57 59h1.8zm4-5h2.3v8.3h-2.3zm2.9 0h2.1l3.4 4.4V54h2.3v8.3h-2L68 57.8v4.6h-2.3zm8 7.1l1.3-1.5c.8.7 1.7 1 2.7 1 .6 0 1-.2 1-.6 0-.4-.3-.5-1.4-.8-1.8-.4-3.1-.9-3.1-2.6 0-1.5 1.2-2.7 3.2-2.7 1.4 0 2.5.4 3.4 1.1l-1.2 1.6c-.8-.5-1.6-.8-2.3-.8-.6 0-.8.2-.8.5 0 .4.3.5 1.4.8 1.9.4 3.1 1 3.1 2.6 0 1.7-1.3 2.7-3.4 2.7-1.5.1-2.9-.4-3.9-1.3z" fill="#FFF"/></g></svg>
|
After Width: | Height: | Size: 3.0 KiB |
|
@ -0,0 +1,10 @@
|
||||||
|
<!DOCTYPE html>
|
||||||
|
<html lang="en">
|
||||||
|
<head>
|
||||||
|
<meta charset="UTF-8" />
|
||||||
|
<title>Test Plugin</title>
|
||||||
|
</head>
|
||||||
|
<body>
|
||||||
|
<p>Welcome to the test plugin!</p>
|
||||||
|
</body>
|
||||||
|
</html>
|
|
@ -0,0 +1,39 @@
|
||||||
|
import * as express from "express"
|
||||||
|
import * as fspath from "path"
|
||||||
|
import * as pluginapi from "../../../typings/pluginapi"
|
||||||
|
|
||||||
|
export const plugin: pluginapi.Plugin = {
|
||||||
|
displayName: "Test Plugin",
|
||||||
|
routerPath: "/test-plugin",
|
||||||
|
homepageURL: "https://example.com",
|
||||||
|
description: "Plugin used in code-server tests.",
|
||||||
|
|
||||||
|
init(config) {
|
||||||
|
config.logger.debug("test-plugin loaded!")
|
||||||
|
},
|
||||||
|
|
||||||
|
router() {
|
||||||
|
const r = express.Router()
|
||||||
|
r.get("/test-app", (req, res) => {
|
||||||
|
res.sendFile(fspath.resolve(__dirname, "../public/index.html"))
|
||||||
|
})
|
||||||
|
r.get("/goland/icon.svg", (req, res) => {
|
||||||
|
res.sendFile(fspath.resolve(__dirname, "../public/icon.svg"))
|
||||||
|
})
|
||||||
|
return r
|
||||||
|
},
|
||||||
|
|
||||||
|
applications() {
|
||||||
|
return [
|
||||||
|
{
|
||||||
|
name: "Test App",
|
||||||
|
version: "4.0.0",
|
||||||
|
iconPath: "/icon.svg",
|
||||||
|
path: "/test-app",
|
||||||
|
|
||||||
|
description: "This app does XYZ.",
|
||||||
|
homepageURL: "https://example.com",
|
||||||
|
},
|
||||||
|
]
|
||||||
|
},
|
||||||
|
}
|
|
@ -0,0 +1,69 @@
|
||||||
|
{
|
||||||
|
"compilerOptions": {
|
||||||
|
/* Visit https://aka.ms/tsconfig.json to read more about this file */
|
||||||
|
|
||||||
|
/* Basic Options */
|
||||||
|
// "incremental": true, /* Enable incremental compilation */
|
||||||
|
"target": "es5" /* Specify ECMAScript target version: 'ES3' (default), 'ES5', 'ES2015', 'ES2016', 'ES2017', 'ES2018', 'ES2019', 'ES2020', or 'ESNEXT'. */,
|
||||||
|
"module": "commonjs" /* Specify module code generation: 'none', 'commonjs', 'amd', 'system', 'umd', 'es2015', 'es2020', or 'ESNext'. */,
|
||||||
|
// "lib": [], /* Specify library files to be included in the compilation. */
|
||||||
|
// "allowJs": true, /* Allow javascript files to be compiled. */
|
||||||
|
// "checkJs": true, /* Report errors in .js files. */
|
||||||
|
// "jsx": "preserve", /* Specify JSX code generation: 'preserve', 'react-native', or 'react'. */
|
||||||
|
// "declaration": true, /* Generates corresponding '.d.ts' file. */
|
||||||
|
// "declarationMap": true, /* Generates a sourcemap for each corresponding '.d.ts' file. */
|
||||||
|
// "sourceMap": true, /* Generates corresponding '.map' file. */
|
||||||
|
// "outFile": "./", /* Concatenate and emit output to single file. */
|
||||||
|
"outDir": "./out" /* Redirect output structure to the directory. */,
|
||||||
|
// "rootDir": "./", /* Specify the root directory of input files. Use to control the output directory structure with --outDir. */
|
||||||
|
// "composite": true, /* Enable project compilation */
|
||||||
|
// "tsBuildInfoFile": "./", /* Specify file to store incremental compilation information */
|
||||||
|
// "removeComments": true, /* Do not emit comments to output. */
|
||||||
|
// "noEmit": true, /* Do not emit outputs. */
|
||||||
|
// "importHelpers": true, /* Import emit helpers from 'tslib'. */
|
||||||
|
// "downlevelIteration": true, /* Provide full support for iterables in 'for-of', spread, and destructuring when targeting 'ES5' or 'ES3'. */
|
||||||
|
// "isolatedModules": true, /* Transpile each file as a separate module (similar to 'ts.transpileModule'). */
|
||||||
|
|
||||||
|
/* Strict Type-Checking Options */
|
||||||
|
"strict": true /* Enable all strict type-checking options. */,
|
||||||
|
// "noImplicitAny": true, /* Raise error on expressions and declarations with an implied 'any' type. */
|
||||||
|
// "strictNullChecks": true, /* Enable strict null checks. */
|
||||||
|
// "strictFunctionTypes": true, /* Enable strict checking of function types. */
|
||||||
|
// "strictBindCallApply": true, /* Enable strict 'bind', 'call', and 'apply' methods on functions. */
|
||||||
|
// "strictPropertyInitialization": true, /* Enable strict checking of property initialization in classes. */
|
||||||
|
// "noImplicitThis": true, /* Raise error on 'this' expressions with an implied 'any' type. */
|
||||||
|
// "alwaysStrict": true, /* Parse in strict mode and emit "use strict" for each source file. */
|
||||||
|
|
||||||
|
/* Additional Checks */
|
||||||
|
// "noUnusedLocals": true, /* Report errors on unused locals. */
|
||||||
|
// "noUnusedParameters": true, /* Report errors on unused parameters. */
|
||||||
|
// "noImplicitReturns": true, /* Report error when not all code paths in function return a value. */
|
||||||
|
// "noFallthroughCasesInSwitch": true, /* Report errors for fallthrough cases in switch statement. */
|
||||||
|
|
||||||
|
/* Module Resolution Options */
|
||||||
|
// "moduleResolution": "node", /* Specify module resolution strategy: 'node' (Node.js) or 'classic' (TypeScript pre-1.6). */
|
||||||
|
// "baseUrl": "./", /* Base directory to resolve non-absolute module names. */
|
||||||
|
// "paths": {}, /* A series of entries which re-map imports to lookup locations relative to the 'baseUrl'. */
|
||||||
|
// "rootDirs": [], /* List of root folders whose combined content represents the structure of the project at runtime. */
|
||||||
|
// "typeRoots": [], /* List of folders to include type definitions from. */
|
||||||
|
// "types": [], /* Type declaration files to be included in compilation. */
|
||||||
|
// "allowSyntheticDefaultImports": true, /* Allow default imports from modules with no default export. This does not affect code emit, just typechecking. */
|
||||||
|
"esModuleInterop": true /* Enables emit interoperability between CommonJS and ES Modules via creation of namespace objects for all imports. Implies 'allowSyntheticDefaultImports'. */,
|
||||||
|
// "preserveSymlinks": true, /* Do not resolve the real path of symlinks. */
|
||||||
|
// "allowUmdGlobalAccess": true, /* Allow accessing UMD globals from modules. */
|
||||||
|
|
||||||
|
/* Source Map Options */
|
||||||
|
// "sourceRoot": "", /* Specify the location where debugger should locate TypeScript files instead of source locations. */
|
||||||
|
// "mapRoot": "", /* Specify the location where debugger should locate map files instead of generated locations. */
|
||||||
|
// "inlineSourceMap": true, /* Emit a single file with source maps instead of having a separate file. */
|
||||||
|
// "inlineSources": true, /* Emit the source alongside the sourcemaps within a single file; requires '--inlineSourceMap' or '--sourceMap' to be set. */
|
||||||
|
|
||||||
|
/* Experimental Options */
|
||||||
|
// "experimentalDecorators": true, /* Enables experimental support for ES7 decorators. */
|
||||||
|
// "emitDecoratorMetadata": true, /* Enables experimental support for emitting type metadata for decorators. */
|
||||||
|
|
||||||
|
/* Advanced Options */
|
||||||
|
"skipLibCheck": true /* Skip type checking of declaration files. */,
|
||||||
|
"forceConsistentCasingInFileNames": true /* Disallow inconsistently-cased references to the same file. */
|
||||||
|
}
|
||||||
|
}
|
|
@ -0,0 +1,435 @@
|
||||||
|
# THIS IS AN AUTOGENERATED FILE. DO NOT EDIT THIS FILE DIRECTLY.
|
||||||
|
# yarn lockfile v1
|
||||||
|
|
||||||
|
|
||||||
|
"@types/body-parser@*":
|
||||||
|
version "1.19.0"
|
||||||
|
resolved "https://registry.yarnpkg.com/@types/body-parser/-/body-parser-1.19.0.tgz#0685b3c47eb3006ffed117cdd55164b61f80538f"
|
||||||
|
integrity sha512-W98JrE0j2K78swW4ukqMleo8R7h/pFETjM2DQ90MF6XK2i4LO4W3gQ71Lt4w3bfm2EvVSyWHplECvB5sK22yFQ==
|
||||||
|
dependencies:
|
||||||
|
"@types/connect" "*"
|
||||||
|
"@types/node" "*"
|
||||||
|
|
||||||
|
"@types/connect@*":
|
||||||
|
version "3.4.33"
|
||||||
|
resolved "https://registry.yarnpkg.com/@types/connect/-/connect-3.4.33.tgz#31610c901eca573b8713c3330abc6e6b9f588546"
|
||||||
|
integrity sha512-2+FrkXY4zllzTNfJth7jOqEHC+enpLeGslEhpnTAkg21GkRrWV4SsAtqchtT4YS9/nODBU2/ZfsBY2X4J/dX7A==
|
||||||
|
dependencies:
|
||||||
|
"@types/node" "*"
|
||||||
|
|
||||||
|
"@types/express-serve-static-core@*":
|
||||||
|
version "4.17.13"
|
||||||
|
resolved "https://registry.yarnpkg.com/@types/express-serve-static-core/-/express-serve-static-core-4.17.13.tgz#d9af025e925fc8b089be37423b8d1eac781be084"
|
||||||
|
integrity sha512-RgDi5a4nuzam073lRGKTUIaL3eF2+H7LJvJ8eUnCI0wA6SNjXc44DCmWNiTLs/AZ7QlsFWZiw/gTG3nSQGL0fA==
|
||||||
|
dependencies:
|
||||||
|
"@types/node" "*"
|
||||||
|
"@types/qs" "*"
|
||||||
|
"@types/range-parser" "*"
|
||||||
|
|
||||||
|
"@types/express@^4.17.8":
|
||||||
|
version "4.17.8"
|
||||||
|
resolved "https://registry.yarnpkg.com/@types/express/-/express-4.17.8.tgz#3df4293293317e61c60137d273a2e96cd8d5f27a"
|
||||||
|
integrity sha512-wLhcKh3PMlyA2cNAB9sjM1BntnhPMiM0JOBwPBqttjHev2428MLEB4AYVN+d8s2iyCVZac+o41Pflm/ZH5vLXQ==
|
||||||
|
dependencies:
|
||||||
|
"@types/body-parser" "*"
|
||||||
|
"@types/express-serve-static-core" "*"
|
||||||
|
"@types/qs" "*"
|
||||||
|
"@types/serve-static" "*"
|
||||||
|
|
||||||
|
"@types/mime@*":
|
||||||
|
version "2.0.3"
|
||||||
|
resolved "https://registry.yarnpkg.com/@types/mime/-/mime-2.0.3.tgz#c893b73721db73699943bfc3653b1deb7faa4a3a"
|
||||||
|
integrity sha512-Jus9s4CDbqwocc5pOAnh8ShfrnMcPHuJYzVcSUU7lrh8Ni5HuIqX3oilL86p3dlTrk0LzHRCgA/GQ7uNCw6l2Q==
|
||||||
|
|
||||||
|
"@types/node@*":
|
||||||
|
version "14.14.6"
|
||||||
|
resolved "https://registry.yarnpkg.com/@types/node/-/node-14.14.6.tgz#146d3da57b3c636cc0d1769396ce1cfa8991147f"
|
||||||
|
integrity sha512-6QlRuqsQ/Ox/aJEQWBEJG7A9+u7oSYl3mem/K8IzxXG/kAGbV1YPD9Bg9Zw3vyxC/YP+zONKwy8hGkSt1jxFMw==
|
||||||
|
|
||||||
|
"@types/qs@*":
|
||||||
|
version "6.9.5"
|
||||||
|
resolved "https://registry.yarnpkg.com/@types/qs/-/qs-6.9.5.tgz#434711bdd49eb5ee69d90c1d67c354a9a8ecb18b"
|
||||||
|
integrity sha512-/JHkVHtx/REVG0VVToGRGH2+23hsYLHdyG+GrvoUGlGAd0ErauXDyvHtRI/7H7mzLm+tBCKA7pfcpkQ1lf58iQ==
|
||||||
|
|
||||||
|
"@types/range-parser@*":
|
||||||
|
version "1.2.3"
|
||||||
|
resolved "https://registry.yarnpkg.com/@types/range-parser/-/range-parser-1.2.3.tgz#7ee330ba7caafb98090bece86a5ee44115904c2c"
|
||||||
|
integrity sha512-ewFXqrQHlFsgc09MK5jP5iR7vumV/BYayNC6PgJO2LPe8vrnNFyjQjSppfEngITi0qvfKtzFvgKymGheFM9UOA==
|
||||||
|
|
||||||
|
"@types/serve-static@*":
|
||||||
|
version "1.13.6"
|
||||||
|
resolved "https://registry.yarnpkg.com/@types/serve-static/-/serve-static-1.13.6.tgz#866b1b8dec41c36e28c7be40ac725b88be43c5c1"
|
||||||
|
integrity sha512-nuRJmv7jW7VmCVTn+IgYDkkbbDGyIINOeu/G0d74X3lm6E5KfMeQPJhxIt1ayQeQB3cSxvYs1RA/wipYoFB4EA==
|
||||||
|
dependencies:
|
||||||
|
"@types/mime" "*"
|
||||||
|
"@types/node" "*"
|
||||||
|
|
||||||
|
accepts@~1.3.7:
|
||||||
|
version "1.3.7"
|
||||||
|
resolved "https://registry.yarnpkg.com/accepts/-/accepts-1.3.7.tgz#531bc726517a3b2b41f850021c6cc15eaab507cd"
|
||||||
|
integrity sha512-Il80Qs2WjYlJIBNzNkK6KYqlVMTbZLXgHx2oT0pU/fjRHyEp+PEfEPY0R3WCwAGVOtauxh1hOxNgIf5bv7dQpA==
|
||||||
|
dependencies:
|
||||||
|
mime-types "~2.1.24"
|
||||||
|
negotiator "0.6.2"
|
||||||
|
|
||||||
|
array-flatten@1.1.1:
|
||||||
|
version "1.1.1"
|
||||||
|
resolved "https://registry.yarnpkg.com/array-flatten/-/array-flatten-1.1.1.tgz#9a5f699051b1e7073328f2a008968b64ea2955d2"
|
||||||
|
integrity sha1-ml9pkFGx5wczKPKgCJaLZOopVdI=
|
||||||
|
|
||||||
|
body-parser@1.19.0:
|
||||||
|
version "1.19.0"
|
||||||
|
resolved "https://registry.yarnpkg.com/body-parser/-/body-parser-1.19.0.tgz#96b2709e57c9c4e09a6fd66a8fd979844f69f08a"
|
||||||
|
integrity sha512-dhEPs72UPbDnAQJ9ZKMNTP6ptJaionhP5cBb541nXPlW60Jepo9RV/a4fX4XWW9CuFNK22krhrj1+rgzifNCsw==
|
||||||
|
dependencies:
|
||||||
|
bytes "3.1.0"
|
||||||
|
content-type "~1.0.4"
|
||||||
|
debug "2.6.9"
|
||||||
|
depd "~1.1.2"
|
||||||
|
http-errors "1.7.2"
|
||||||
|
iconv-lite "0.4.24"
|
||||||
|
on-finished "~2.3.0"
|
||||||
|
qs "6.7.0"
|
||||||
|
raw-body "2.4.0"
|
||||||
|
type-is "~1.6.17"
|
||||||
|
|
||||||
|
bytes@3.1.0:
|
||||||
|
version "3.1.0"
|
||||||
|
resolved "https://registry.yarnpkg.com/bytes/-/bytes-3.1.0.tgz#f6cf7933a360e0588fa9fde85651cdc7f805d1f6"
|
||||||
|
integrity sha512-zauLjrfCG+xvoyaqLoV8bLVXXNGC4JqlxFCutSDWA6fJrTo2ZuvLYTqZ7aHBLZSMOopbzwv8f+wZcVzfVTI2Dg==
|
||||||
|
|
||||||
|
content-disposition@0.5.3:
|
||||||
|
version "0.5.3"
|
||||||
|
resolved "https://registry.yarnpkg.com/content-disposition/-/content-disposition-0.5.3.tgz#e130caf7e7279087c5616c2007d0485698984fbd"
|
||||||
|
integrity sha512-ExO0774ikEObIAEV9kDo50o+79VCUdEB6n6lzKgGwupcVeRlhrj3qGAfwq8G6uBJjkqLrhT0qEYFcWng8z1z0g==
|
||||||
|
dependencies:
|
||||||
|
safe-buffer "5.1.2"
|
||||||
|
|
||||||
|
content-type@~1.0.4:
|
||||||
|
version "1.0.4"
|
||||||
|
resolved "https://registry.yarnpkg.com/content-type/-/content-type-1.0.4.tgz#e138cc75e040c727b1966fe5e5f8c9aee256fe3b"
|
||||||
|
integrity sha512-hIP3EEPs8tB9AT1L+NUqtwOAps4mk2Zob89MWXMHjHWg9milF/j4osnnQLXBCBFBk/tvIG/tUc9mOUJiPBhPXA==
|
||||||
|
|
||||||
|
cookie-signature@1.0.6:
|
||||||
|
version "1.0.6"
|
||||||
|
resolved "https://registry.yarnpkg.com/cookie-signature/-/cookie-signature-1.0.6.tgz#e303a882b342cc3ee8ca513a79999734dab3ae2c"
|
||||||
|
integrity sha1-4wOogrNCzD7oylE6eZmXNNqzriw=
|
||||||
|
|
||||||
|
cookie@0.4.0:
|
||||||
|
version "0.4.0"
|
||||||
|
resolved "https://registry.yarnpkg.com/cookie/-/cookie-0.4.0.tgz#beb437e7022b3b6d49019d088665303ebe9c14ba"
|
||||||
|
integrity sha512-+Hp8fLp57wnUSt0tY0tHEXh4voZRDnoIrZPqlo3DPiI4y9lwg/jqx+1Om94/W6ZaPDOUbnjOt/99w66zk+l1Xg==
|
||||||
|
|
||||||
|
debug@2.6.9:
|
||||||
|
version "2.6.9"
|
||||||
|
resolved "https://registry.yarnpkg.com/debug/-/debug-2.6.9.tgz#5d128515df134ff327e90a4c93f4e077a536341f"
|
||||||
|
integrity sha512-bC7ElrdJaJnPbAP+1EotYvqZsb3ecl5wi6Bfi6BJTUcNowp6cvspg0jXznRTKDjm/E7AdgFBVeAPVMNcKGsHMA==
|
||||||
|
dependencies:
|
||||||
|
ms "2.0.0"
|
||||||
|
|
||||||
|
depd@~1.1.2:
|
||||||
|
version "1.1.2"
|
||||||
|
resolved "https://registry.yarnpkg.com/depd/-/depd-1.1.2.tgz#9bcd52e14c097763e749b274c4346ed2e560b5a9"
|
||||||
|
integrity sha1-m81S4UwJd2PnSbJ0xDRu0uVgtak=
|
||||||
|
|
||||||
|
destroy@~1.0.4:
|
||||||
|
version "1.0.4"
|
||||||
|
resolved "https://registry.yarnpkg.com/destroy/-/destroy-1.0.4.tgz#978857442c44749e4206613e37946205826abd80"
|
||||||
|
integrity sha1-l4hXRCxEdJ5CBmE+N5RiBYJqvYA=
|
||||||
|
|
||||||
|
ee-first@1.1.1:
|
||||||
|
version "1.1.1"
|
||||||
|
resolved "https://registry.yarnpkg.com/ee-first/-/ee-first-1.1.1.tgz#590c61156b0ae2f4f0255732a158b266bc56b21d"
|
||||||
|
integrity sha1-WQxhFWsK4vTwJVcyoViyZrxWsh0=
|
||||||
|
|
||||||
|
encodeurl@~1.0.2:
|
||||||
|
version "1.0.2"
|
||||||
|
resolved "https://registry.yarnpkg.com/encodeurl/-/encodeurl-1.0.2.tgz#ad3ff4c86ec2d029322f5a02c3a9a606c95b3f59"
|
||||||
|
integrity sha1-rT/0yG7C0CkyL1oCw6mmBslbP1k=
|
||||||
|
|
||||||
|
escape-html@~1.0.3:
|
||||||
|
version "1.0.3"
|
||||||
|
resolved "https://registry.yarnpkg.com/escape-html/-/escape-html-1.0.3.tgz#0258eae4d3d0c0974de1c169188ef0051d1d1988"
|
||||||
|
integrity sha1-Aljq5NPQwJdN4cFpGI7wBR0dGYg=
|
||||||
|
|
||||||
|
etag@~1.8.1:
|
||||||
|
version "1.8.1"
|
||||||
|
resolved "https://registry.yarnpkg.com/etag/-/etag-1.8.1.tgz#41ae2eeb65efa62268aebfea83ac7d79299b0887"
|
||||||
|
integrity sha1-Qa4u62XvpiJorr/qg6x9eSmbCIc=
|
||||||
|
|
||||||
|
express@^4.17.1:
|
||||||
|
version "4.17.1"
|
||||||
|
resolved "https://registry.yarnpkg.com/express/-/express-4.17.1.tgz#4491fc38605cf51f8629d39c2b5d026f98a4c134"
|
||||||
|
integrity sha512-mHJ9O79RqluphRrcw2X/GTh3k9tVv8YcoyY4Kkh4WDMUYKRZUq0h1o0w2rrrxBqM7VoeUVqgb27xlEMXTnYt4g==
|
||||||
|
dependencies:
|
||||||
|
accepts "~1.3.7"
|
||||||
|
array-flatten "1.1.1"
|
||||||
|
body-parser "1.19.0"
|
||||||
|
content-disposition "0.5.3"
|
||||||
|
content-type "~1.0.4"
|
||||||
|
cookie "0.4.0"
|
||||||
|
cookie-signature "1.0.6"
|
||||||
|
debug "2.6.9"
|
||||||
|
depd "~1.1.2"
|
||||||
|
encodeurl "~1.0.2"
|
||||||
|
escape-html "~1.0.3"
|
||||||
|
etag "~1.8.1"
|
||||||
|
finalhandler "~1.1.2"
|
||||||
|
fresh "0.5.2"
|
||||||
|
merge-descriptors "1.0.1"
|
||||||
|
methods "~1.1.2"
|
||||||
|
on-finished "~2.3.0"
|
||||||
|
parseurl "~1.3.3"
|
||||||
|
path-to-regexp "0.1.7"
|
||||||
|
proxy-addr "~2.0.5"
|
||||||
|
qs "6.7.0"
|
||||||
|
range-parser "~1.2.1"
|
||||||
|
safe-buffer "5.1.2"
|
||||||
|
send "0.17.1"
|
||||||
|
serve-static "1.14.1"
|
||||||
|
setprototypeof "1.1.1"
|
||||||
|
statuses "~1.5.0"
|
||||||
|
type-is "~1.6.18"
|
||||||
|
utils-merge "1.0.1"
|
||||||
|
vary "~1.1.2"
|
||||||
|
|
||||||
|
finalhandler@~1.1.2:
|
||||||
|
version "1.1.2"
|
||||||
|
resolved "https://registry.yarnpkg.com/finalhandler/-/finalhandler-1.1.2.tgz#b7e7d000ffd11938d0fdb053506f6ebabe9f587d"
|
||||||
|
integrity sha512-aAWcW57uxVNrQZqFXjITpW3sIUQmHGG3qSb9mUah9MgMC4NeWhNOlNjXEYq3HjRAvL6arUviZGGJsBg6z0zsWA==
|
||||||
|
dependencies:
|
||||||
|
debug "2.6.9"
|
||||||
|
encodeurl "~1.0.2"
|
||||||
|
escape-html "~1.0.3"
|
||||||
|
on-finished "~2.3.0"
|
||||||
|
parseurl "~1.3.3"
|
||||||
|
statuses "~1.5.0"
|
||||||
|
unpipe "~1.0.0"
|
||||||
|
|
||||||
|
forwarded@~0.1.2:
|
||||||
|
version "0.1.2"
|
||||||
|
resolved "https://registry.yarnpkg.com/forwarded/-/forwarded-0.1.2.tgz#98c23dab1175657b8c0573e8ceccd91b0ff18c84"
|
||||||
|
integrity sha1-mMI9qxF1ZXuMBXPozszZGw/xjIQ=
|
||||||
|
|
||||||
|
fresh@0.5.2:
|
||||||
|
version "0.5.2"
|
||||||
|
resolved "https://registry.yarnpkg.com/fresh/-/fresh-0.5.2.tgz#3d8cadd90d976569fa835ab1f8e4b23a105605a7"
|
||||||
|
integrity sha1-PYyt2Q2XZWn6g1qx+OSyOhBWBac=
|
||||||
|
|
||||||
|
http-errors@1.7.2:
|
||||||
|
version "1.7.2"
|
||||||
|
resolved "https://registry.yarnpkg.com/http-errors/-/http-errors-1.7.2.tgz#4f5029cf13239f31036e5b2e55292bcfbcc85c8f"
|
||||||
|
integrity sha512-uUQBt3H/cSIVfch6i1EuPNy/YsRSOUBXTVfZ+yR7Zjez3qjBz6i9+i4zjNaoqcoFVI4lQJ5plg63TvGfRSDCRg==
|
||||||
|
dependencies:
|
||||||
|
depd "~1.1.2"
|
||||||
|
inherits "2.0.3"
|
||||||
|
setprototypeof "1.1.1"
|
||||||
|
statuses ">= 1.5.0 < 2"
|
||||||
|
toidentifier "1.0.0"
|
||||||
|
|
||||||
|
http-errors@~1.7.2:
|
||||||
|
version "1.7.3"
|
||||||
|
resolved "https://registry.yarnpkg.com/http-errors/-/http-errors-1.7.3.tgz#6c619e4f9c60308c38519498c14fbb10aacebb06"
|
||||||
|
integrity sha512-ZTTX0MWrsQ2ZAhA1cejAwDLycFsd7I7nVtnkT3Ol0aqodaKW+0CTZDQ1uBv5whptCnc8e8HeRRJxRs0kmm/Qfw==
|
||||||
|
dependencies:
|
||||||
|
depd "~1.1.2"
|
||||||
|
inherits "2.0.4"
|
||||||
|
setprototypeof "1.1.1"
|
||||||
|
statuses ">= 1.5.0 < 2"
|
||||||
|
toidentifier "1.0.0"
|
||||||
|
|
||||||
|
iconv-lite@0.4.24:
|
||||||
|
version "0.4.24"
|
||||||
|
resolved "https://registry.yarnpkg.com/iconv-lite/-/iconv-lite-0.4.24.tgz#2022b4b25fbddc21d2f524974a474aafe733908b"
|
||||||
|
integrity sha512-v3MXnZAcvnywkTUEZomIActle7RXXeedOR31wwl7VlyoXO4Qi9arvSenNQWne1TcRwhCL1HwLI21bEqdpj8/rA==
|
||||||
|
dependencies:
|
||||||
|
safer-buffer ">= 2.1.2 < 3"
|
||||||
|
|
||||||
|
inherits@2.0.3:
|
||||||
|
version "2.0.3"
|
||||||
|
resolved "https://registry.yarnpkg.com/inherits/-/inherits-2.0.3.tgz#633c2c83e3da42a502f52466022480f4208261de"
|
||||||
|
integrity sha1-Yzwsg+PaQqUC9SRmAiSA9CCCYd4=
|
||||||
|
|
||||||
|
inherits@2.0.4:
|
||||||
|
version "2.0.4"
|
||||||
|
resolved "https://registry.yarnpkg.com/inherits/-/inherits-2.0.4.tgz#0fa2c64f932917c3433a0ded55363aae37416b7c"
|
||||||
|
integrity sha512-k/vGaX4/Yla3WzyMCvTQOXYeIHvqOKtnqBduzTHpzpQZzAskKMhZ2K+EnBiSM9zGSoIFeMpXKxa4dYeZIQqewQ==
|
||||||
|
|
||||||
|
ipaddr.js@1.9.1:
|
||||||
|
version "1.9.1"
|
||||||
|
resolved "https://registry.yarnpkg.com/ipaddr.js/-/ipaddr.js-1.9.1.tgz#bff38543eeb8984825079ff3a2a8e6cbd46781b3"
|
||||||
|
integrity sha512-0KI/607xoxSToH7GjN1FfSbLoU0+btTicjsQSWQlh/hZykN8KpmMf7uYwPW3R+akZ6R/w18ZlXSHBYXiYUPO3g==
|
||||||
|
|
||||||
|
media-typer@0.3.0:
|
||||||
|
version "0.3.0"
|
||||||
|
resolved "https://registry.yarnpkg.com/media-typer/-/media-typer-0.3.0.tgz#8710d7af0aa626f8fffa1ce00168545263255748"
|
||||||
|
integrity sha1-hxDXrwqmJvj/+hzgAWhUUmMlV0g=
|
||||||
|
|
||||||
|
merge-descriptors@1.0.1:
|
||||||
|
version "1.0.1"
|
||||||
|
resolved "https://registry.yarnpkg.com/merge-descriptors/-/merge-descriptors-1.0.1.tgz#b00aaa556dd8b44568150ec9d1b953f3f90cbb61"
|
||||||
|
integrity sha1-sAqqVW3YtEVoFQ7J0blT8/kMu2E=
|
||||||
|
|
||||||
|
methods@~1.1.2:
|
||||||
|
version "1.1.2"
|
||||||
|
resolved "https://registry.yarnpkg.com/methods/-/methods-1.1.2.tgz#5529a4d67654134edcc5266656835b0f851afcee"
|
||||||
|
integrity sha1-VSmk1nZUE07cxSZmVoNbD4Ua/O4=
|
||||||
|
|
||||||
|
mime-db@1.44.0:
|
||||||
|
version "1.44.0"
|
||||||
|
resolved "https://registry.yarnpkg.com/mime-db/-/mime-db-1.44.0.tgz#fa11c5eb0aca1334b4233cb4d52f10c5a6272f92"
|
||||||
|
integrity sha512-/NOTfLrsPBVeH7YtFPgsVWveuL+4SjjYxaQ1xtM1KMFj7HdxlBlxeyNLzhyJVx7r4rZGJAZ/6lkKCitSc/Nmpg==
|
||||||
|
|
||||||
|
mime-types@~2.1.24:
|
||||||
|
version "2.1.27"
|
||||||
|
resolved "https://registry.yarnpkg.com/mime-types/-/mime-types-2.1.27.tgz#47949f98e279ea53119f5722e0f34e529bec009f"
|
||||||
|
integrity sha512-JIhqnCasI9yD+SsmkquHBxTSEuZdQX5BuQnS2Vc7puQQQ+8yiP5AY5uWhpdv4YL4VM5c6iliiYWPgJ/nJQLp7w==
|
||||||
|
dependencies:
|
||||||
|
mime-db "1.44.0"
|
||||||
|
|
||||||
|
mime@1.6.0:
|
||||||
|
version "1.6.0"
|
||||||
|
resolved "https://registry.yarnpkg.com/mime/-/mime-1.6.0.tgz#32cd9e5c64553bd58d19a568af452acff04981b1"
|
||||||
|
integrity sha512-x0Vn8spI+wuJ1O6S7gnbaQg8Pxh4NNHb7KSINmEWKiPE4RKOplvijn+NkmYmmRgP68mc70j2EbeTFRsrswaQeg==
|
||||||
|
|
||||||
|
ms@2.0.0:
|
||||||
|
version "2.0.0"
|
||||||
|
resolved "https://registry.yarnpkg.com/ms/-/ms-2.0.0.tgz#5608aeadfc00be6c2901df5f9861788de0d597c8"
|
||||||
|
integrity sha1-VgiurfwAvmwpAd9fmGF4jeDVl8g=
|
||||||
|
|
||||||
|
ms@2.1.1:
|
||||||
|
version "2.1.1"
|
||||||
|
resolved "https://registry.yarnpkg.com/ms/-/ms-2.1.1.tgz#30a5864eb3ebb0a66f2ebe6d727af06a09d86e0a"
|
||||||
|
integrity sha512-tgp+dl5cGk28utYktBsrFqA7HKgrhgPsg6Z/EfhWI4gl1Hwq8B/GmY/0oXZ6nF8hDVesS/FpnYaD/kOWhYQvyg==
|
||||||
|
|
||||||
|
negotiator@0.6.2:
|
||||||
|
version "0.6.2"
|
||||||
|
resolved "https://registry.yarnpkg.com/negotiator/-/negotiator-0.6.2.tgz#feacf7ccf525a77ae9634436a64883ffeca346fb"
|
||||||
|
integrity sha512-hZXc7K2e+PgeI1eDBe/10Ard4ekbfrrqG8Ep+8Jmf4JID2bNg7NvCPOZN+kfF574pFQI7mum2AUqDidoKqcTOw==
|
||||||
|
|
||||||
|
on-finished@~2.3.0:
|
||||||
|
version "2.3.0"
|
||||||
|
resolved "https://registry.yarnpkg.com/on-finished/-/on-finished-2.3.0.tgz#20f1336481b083cd75337992a16971aa2d906947"
|
||||||
|
integrity sha1-IPEzZIGwg811M3mSoWlxqi2QaUc=
|
||||||
|
dependencies:
|
||||||
|
ee-first "1.1.1"
|
||||||
|
|
||||||
|
parseurl@~1.3.3:
|
||||||
|
version "1.3.3"
|
||||||
|
resolved "https://registry.yarnpkg.com/parseurl/-/parseurl-1.3.3.tgz#9da19e7bee8d12dff0513ed5b76957793bc2e8d4"
|
||||||
|
integrity sha512-CiyeOxFT/JZyN5m0z9PfXw4SCBJ6Sygz1Dpl0wqjlhDEGGBP1GnsUVEL0p63hoG1fcj3fHynXi9NYO4nWOL+qQ==
|
||||||
|
|
||||||
|
path-to-regexp@0.1.7:
|
||||||
|
version "0.1.7"
|
||||||
|
resolved "https://registry.yarnpkg.com/path-to-regexp/-/path-to-regexp-0.1.7.tgz#df604178005f522f15eb4490e7247a1bfaa67f8c"
|
||||||
|
integrity sha1-32BBeABfUi8V60SQ5yR6G/qmf4w=
|
||||||
|
|
||||||
|
proxy-addr@~2.0.5:
|
||||||
|
version "2.0.6"
|
||||||
|
resolved "https://registry.yarnpkg.com/proxy-addr/-/proxy-addr-2.0.6.tgz#fdc2336505447d3f2f2c638ed272caf614bbb2bf"
|
||||||
|
integrity sha512-dh/frvCBVmSsDYzw6n926jv974gddhkFPfiN8hPOi30Wax25QZyZEGveluCgliBnqmuM+UJmBErbAUFIoDbjOw==
|
||||||
|
dependencies:
|
||||||
|
forwarded "~0.1.2"
|
||||||
|
ipaddr.js "1.9.1"
|
||||||
|
|
||||||
|
qs@6.7.0:
|
||||||
|
version "6.7.0"
|
||||||
|
resolved "https://registry.yarnpkg.com/qs/-/qs-6.7.0.tgz#41dc1a015e3d581f1621776be31afb2876a9b1bc"
|
||||||
|
integrity sha512-VCdBRNFTX1fyE7Nb6FYoURo/SPe62QCaAyzJvUjwRaIsc+NePBEniHlvxFmmX56+HZphIGtV0XeCirBtpDrTyQ==
|
||||||
|
|
||||||
|
range-parser@~1.2.1:
|
||||||
|
version "1.2.1"
|
||||||
|
resolved "https://registry.yarnpkg.com/range-parser/-/range-parser-1.2.1.tgz#3cf37023d199e1c24d1a55b84800c2f3e6468031"
|
||||||
|
integrity sha512-Hrgsx+orqoygnmhFbKaHE6c296J+HTAQXoxEF6gNupROmmGJRoyzfG3ccAveqCBrwr/2yxQ5BVd/GTl5agOwSg==
|
||||||
|
|
||||||
|
raw-body@2.4.0:
|
||||||
|
version "2.4.0"
|
||||||
|
resolved "https://registry.yarnpkg.com/raw-body/-/raw-body-2.4.0.tgz#a1ce6fb9c9bc356ca52e89256ab59059e13d0332"
|
||||||
|
integrity sha512-4Oz8DUIwdvoa5qMJelxipzi/iJIi40O5cGV1wNYp5hvZP8ZN0T+jiNkL0QepXs+EsQ9XJ8ipEDoiH70ySUJP3Q==
|
||||||
|
dependencies:
|
||||||
|
bytes "3.1.0"
|
||||||
|
http-errors "1.7.2"
|
||||||
|
iconv-lite "0.4.24"
|
||||||
|
unpipe "1.0.0"
|
||||||
|
|
||||||
|
safe-buffer@5.1.2:
|
||||||
|
version "5.1.2"
|
||||||
|
resolved "https://registry.yarnpkg.com/safe-buffer/-/safe-buffer-5.1.2.tgz#991ec69d296e0313747d59bdfd2b745c35f8828d"
|
||||||
|
integrity sha512-Gd2UZBJDkXlY7GbJxfsE8/nvKkUEU1G38c1siN6QP6a9PT9MmHB8GnpscSmMJSoF8LOIrt8ud/wPtojys4G6+g==
|
||||||
|
|
||||||
|
"safer-buffer@>= 2.1.2 < 3":
|
||||||
|
version "2.1.2"
|
||||||
|
resolved "https://registry.yarnpkg.com/safer-buffer/-/safer-buffer-2.1.2.tgz#44fa161b0187b9549dd84bb91802f9bd8385cd6a"
|
||||||
|
integrity sha512-YZo3K82SD7Riyi0E1EQPojLz7kpepnSQI9IyPbHHg1XXXevb5dJI7tpyN2ADxGcQbHG7vcyRHk0cbwqcQriUtg==
|
||||||
|
|
||||||
|
send@0.17.1:
|
||||||
|
version "0.17.1"
|
||||||
|
resolved "https://registry.yarnpkg.com/send/-/send-0.17.1.tgz#c1d8b059f7900f7466dd4938bdc44e11ddb376c8"
|
||||||
|
integrity sha512-BsVKsiGcQMFwT8UxypobUKyv7irCNRHk1T0G680vk88yf6LBByGcZJOTJCrTP2xVN6yI+XjPJcNuE3V4fT9sAg==
|
||||||
|
dependencies:
|
||||||
|
debug "2.6.9"
|
||||||
|
depd "~1.1.2"
|
||||||
|
destroy "~1.0.4"
|
||||||
|
encodeurl "~1.0.2"
|
||||||
|
escape-html "~1.0.3"
|
||||||
|
etag "~1.8.1"
|
||||||
|
fresh "0.5.2"
|
||||||
|
http-errors "~1.7.2"
|
||||||
|
mime "1.6.0"
|
||||||
|
ms "2.1.1"
|
||||||
|
on-finished "~2.3.0"
|
||||||
|
range-parser "~1.2.1"
|
||||||
|
statuses "~1.5.0"
|
||||||
|
|
||||||
|
serve-static@1.14.1:
|
||||||
|
version "1.14.1"
|
||||||
|
resolved "https://registry.yarnpkg.com/serve-static/-/serve-static-1.14.1.tgz#666e636dc4f010f7ef29970a88a674320898b2f9"
|
||||||
|
integrity sha512-JMrvUwE54emCYWlTI+hGrGv5I8dEwmco/00EvkzIIsR7MqrHonbD9pO2MOfFnpFntl7ecpZs+3mW+XbQZu9QCg==
|
||||||
|
dependencies:
|
||||||
|
encodeurl "~1.0.2"
|
||||||
|
escape-html "~1.0.3"
|
||||||
|
parseurl "~1.3.3"
|
||||||
|
send "0.17.1"
|
||||||
|
|
||||||
|
setprototypeof@1.1.1:
|
||||||
|
version "1.1.1"
|
||||||
|
resolved "https://registry.yarnpkg.com/setprototypeof/-/setprototypeof-1.1.1.tgz#7e95acb24aa92f5885e0abef5ba131330d4ae683"
|
||||||
|
integrity sha512-JvdAWfbXeIGaZ9cILp38HntZSFSo3mWg6xGcJJsd+d4aRMOqauag1C63dJfDw7OaMYwEbHMOxEZ1lqVRYP2OAw==
|
||||||
|
|
||||||
|
"statuses@>= 1.5.0 < 2", statuses@~1.5.0:
|
||||||
|
version "1.5.0"
|
||||||
|
resolved "https://registry.yarnpkg.com/statuses/-/statuses-1.5.0.tgz#161c7dac177659fd9811f43771fa99381478628c"
|
||||||
|
integrity sha1-Fhx9rBd2Wf2YEfQ3cfqZOBR4Yow=
|
||||||
|
|
||||||
|
toidentifier@1.0.0:
|
||||||
|
version "1.0.0"
|
||||||
|
resolved "https://registry.yarnpkg.com/toidentifier/-/toidentifier-1.0.0.tgz#7e1be3470f1e77948bc43d94a3c8f4d7752ba553"
|
||||||
|
integrity sha512-yaOH/Pk/VEhBWWTlhI+qXxDFXlejDGcQipMlyxda9nthulaxLZUNcUqFxokp0vcYnvteJln5FNQDRrxj3YcbVw==
|
||||||
|
|
||||||
|
type-is@~1.6.17, type-is@~1.6.18:
|
||||||
|
version "1.6.18"
|
||||||
|
resolved "https://registry.yarnpkg.com/type-is/-/type-is-1.6.18.tgz#4e552cd05df09467dcbc4ef739de89f2cf37c131"
|
||||||
|
integrity sha512-TkRKr9sUTxEH8MdfuCSP7VizJyzRNMjj2J2do2Jr3Kym598JVdEksuzPQCnlFPW4ky9Q+iA+ma9BGm06XQBy8g==
|
||||||
|
dependencies:
|
||||||
|
media-typer "0.3.0"
|
||||||
|
mime-types "~2.1.24"
|
||||||
|
|
||||||
|
typescript@^4.0.5:
|
||||||
|
version "4.0.5"
|
||||||
|
resolved "https://registry.yarnpkg.com/typescript/-/typescript-4.0.5.tgz#ae9dddfd1069f1cb5beb3ef3b2170dd7c1332389"
|
||||||
|
integrity sha512-ywmr/VrTVCmNTJ6iV2LwIrfG1P+lv6luD8sUJs+2eI9NLGigaN+nUQc13iHqisq7bra9lnmUSYqbJvegraBOPQ==
|
||||||
|
|
||||||
|
unpipe@1.0.0, unpipe@~1.0.0:
|
||||||
|
version "1.0.0"
|
||||||
|
resolved "https://registry.yarnpkg.com/unpipe/-/unpipe-1.0.0.tgz#b2bf4ee8514aae6165b4817829d21b2ef49904ec"
|
||||||
|
integrity sha1-sr9O6FFKrmFltIF4KdIbLvSZBOw=
|
||||||
|
|
||||||
|
utils-merge@1.0.1:
|
||||||
|
version "1.0.1"
|
||||||
|
resolved "https://registry.yarnpkg.com/utils-merge/-/utils-merge-1.0.1.tgz#9f95710f50a267947b2ccc124741c1028427e713"
|
||||||
|
integrity sha1-n5VxD1CiZ5R7LMwSR0HBAoQn5xM=
|
||||||
|
|
||||||
|
vary@~1.1.2:
|
||||||
|
version "1.1.2"
|
||||||
|
resolved "https://registry.yarnpkg.com/vary/-/vary-1.1.2.tgz#2299f02c6ded30d4a5961b0b9f74524a18f634fc"
|
||||||
|
integrity sha1-IpnwLG3tMNSllhsLn3RSShj2NPw=
|
|
@ -2,13 +2,11 @@ import * as assert from "assert"
|
||||||
import * as fs from "fs-extra"
|
import * as fs from "fs-extra"
|
||||||
import * as http from "http"
|
import * as http from "http"
|
||||||
import * as path from "path"
|
import * as path from "path"
|
||||||
import { LatestResponse, UpdateHttpProvider } from "../src/node/app/update"
|
|
||||||
import { AuthType } from "../src/node/http"
|
|
||||||
import { SettingsProvider, UpdateSettings } from "../src/node/settings"
|
import { SettingsProvider, UpdateSettings } from "../src/node/settings"
|
||||||
|
import { LatestResponse, UpdateProvider } from "../src/node/update"
|
||||||
import { tmpdir } from "../src/node/util"
|
import { tmpdir } from "../src/node/util"
|
||||||
|
|
||||||
describe("update", () => {
|
describe.skip("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) => {
|
||||||
|
@ -35,22 +33,14 @@ describe("update", () => {
|
||||||
const jsonPath = path.join(tmpdir, "tests/updates/update.json")
|
const jsonPath = path.join(tmpdir, "tests/updates/update.json")
|
||||||
const settings = new SettingsProvider<UpdateSettings>(jsonPath)
|
const settings = new SettingsProvider<UpdateSettings>(jsonPath)
|
||||||
|
|
||||||
let _provider: UpdateHttpProvider | undefined
|
let _provider: UpdateProvider | undefined
|
||||||
const provider = (): UpdateHttpProvider => {
|
const provider = (): UpdateProvider => {
|
||||||
if (!_provider) {
|
if (!_provider) {
|
||||||
const address = server.address()
|
const address = server.address()
|
||||||
if (!address || typeof address === "string" || !address.port) {
|
if (!address || typeof address === "string" || !address.port) {
|
||||||
throw new Error("unexpected address")
|
throw new Error("unexpected address")
|
||||||
}
|
}
|
||||||
_provider = new UpdateHttpProvider(
|
_provider = new UpdateProvider(`http://${address.address}:${address.port}/latest`, settings)
|
||||||
{
|
|
||||||
auth: AuthType.None,
|
|
||||||
commit: "test",
|
|
||||||
},
|
|
||||||
true,
|
|
||||||
`http://${address.address}:${address.port}/latest`,
|
|
||||||
settings,
|
|
||||||
)
|
|
||||||
}
|
}
|
||||||
return _provider
|
return _provider
|
||||||
}
|
}
|
||||||
|
@ -154,14 +144,10 @@ describe("update", () => {
|
||||||
})
|
})
|
||||||
|
|
||||||
it("should not reject if unable to fetch", async () => {
|
it("should not reject if unable to fetch", async () => {
|
||||||
const options = {
|
let provider = new UpdateProvider("invalid", settings)
|
||||||
auth: AuthType.None,
|
|
||||||
commit: "test",
|
|
||||||
}
|
|
||||||
let provider = new UpdateHttpProvider(options, true, "invalid", settings)
|
|
||||||
await assert.doesNotReject(() => provider.getUpdate(true))
|
await assert.doesNotReject(() => provider.getUpdate(true))
|
||||||
|
|
||||||
provider = new UpdateHttpProvider(options, true, "http://probably.invalid.dev.localhost/latest", settings)
|
provider = new UpdateProvider("http://probably.invalid.dev.localhost/latest", settings)
|
||||||
await assert.doesNotReject(() => provider.getUpdate(true))
|
await assert.doesNotReject(() => provider.getUpdate(true))
|
||||||
})
|
})
|
||||||
})
|
})
|
||||||
|
|
|
@ -16,7 +16,8 @@
|
||||||
"tsBuildInfoFile": "./.cache/tsbuildinfo",
|
"tsBuildInfoFile": "./.cache/tsbuildinfo",
|
||||||
"incremental": true,
|
"incremental": true,
|
||||||
"rootDir": "./src",
|
"rootDir": "./src",
|
||||||
"typeRoots": ["./node_modules/@types", "./typings"]
|
"typeRoots": ["./node_modules/@types", "./typings"],
|
||||||
|
"downlevelIteration": true
|
||||||
},
|
},
|
||||||
"include": ["./src/**/*.ts"]
|
"include": ["./src/**/*.ts"]
|
||||||
}
|
}
|
||||||
|
|
|
@ -0,0 +1,189 @@
|
||||||
|
/**
|
||||||
|
* This file describes the code-server plugin API for adding new applications.
|
||||||
|
*/
|
||||||
|
import { Logger } from "@coder/logger"
|
||||||
|
import * as express from "express"
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Overlay
|
||||||
|
*
|
||||||
|
* The homepage of code-server will launch into VS Code. However, there will be an overlay
|
||||||
|
* button that when clicked, will show all available applications with their names,
|
||||||
|
* icons and provider plugins. When one clicks on an app's icon, they will be directed
|
||||||
|
* to <code-server-root>/<plugin-path>/<app-path> to access the application.
|
||||||
|
*/
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Plugins
|
||||||
|
*
|
||||||
|
* Plugins are just node modules that contain a top level export "plugin" that implements
|
||||||
|
* the Plugin interface.
|
||||||
|
*
|
||||||
|
* 1. code-server uses $CS_PLUGIN to find plugins.
|
||||||
|
*
|
||||||
|
* e.g. CS_PLUGIN=/tmp/will:/tmp/teffen will cause code-server to load
|
||||||
|
* /tmp/will and /tmp/teffen as plugins.
|
||||||
|
*
|
||||||
|
* 2. code-server uses $CS_PLUGIN_PATH to find plugins. Each subdirectory in
|
||||||
|
* $CS_PLUGIN_PATH with a package.json where the engine is code-server is
|
||||||
|
* a valid plugin.
|
||||||
|
*
|
||||||
|
* e.g. CS_PLUGIN_PATH=/tmp/nhooyr:/tmp/ash will cause code-server to search
|
||||||
|
* /tmp/nhooyr and then /tmp/ash for plugins.
|
||||||
|
*
|
||||||
|
* CS_PLUGIN_PATH defaults to
|
||||||
|
* ~/.local/share/code-server/plugins:/usr/share/code-server/plugins
|
||||||
|
* if unset.
|
||||||
|
*
|
||||||
|
*
|
||||||
|
* 3. Built in plugins are loaded from __dirname/../plugins
|
||||||
|
*
|
||||||
|
* Plugins are required as soon as they are found and then initialized.
|
||||||
|
* See the Plugin interface for details.
|
||||||
|
*
|
||||||
|
* If two plugins are found with the exact same name, then code-server will
|
||||||
|
* use the first one and emit a warning.
|
||||||
|
*
|
||||||
|
*/
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Programmability
|
||||||
|
*
|
||||||
|
* There is also a /api/applications endpoint to allow programmatic access to all
|
||||||
|
* available applications. It could be used to create a custom application dashboard
|
||||||
|
* for example. An important difference with the API is that all application paths
|
||||||
|
* will be absolute (i.e have the plugin path prepended) so that they may be used
|
||||||
|
* directly.
|
||||||
|
*
|
||||||
|
* Example output:
|
||||||
|
*
|
||||||
|
* [
|
||||||
|
* {
|
||||||
|
* "name": "Test App",
|
||||||
|
* "version": "4.0.0",
|
||||||
|
* "iconPath": "/test-plugin/test-app/icon.svg",
|
||||||
|
* "path": "/test-plugin/test-app",
|
||||||
|
* "description": "This app does XYZ.",
|
||||||
|
* "homepageURL": "https://example.com",
|
||||||
|
* "plugin": {
|
||||||
|
* "name": "test-plugin",
|
||||||
|
* "version": "1.0.0",
|
||||||
|
* "modulePath": "/Users/nhooyr/src/cdr/code-server/test/test-plugin",
|
||||||
|
* "displayName": "Test Plugin",
|
||||||
|
* "description": "Plugin used in code-server tests.",
|
||||||
|
* "routerPath": "/test-plugin",
|
||||||
|
* "homepageURL": "https://example.com"
|
||||||
|
* }
|
||||||
|
* }
|
||||||
|
* ]
|
||||||
|
*/
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Your plugin module must have a top level export "plugin" that implements this interface.
|
||||||
|
*
|
||||||
|
* The plugin's router will be mounted at <code-sever-root>/<plugin-path>
|
||||||
|
*/
|
||||||
|
export interface Plugin {
|
||||||
|
/**
|
||||||
|
* name is used as the plugin's unique identifier.
|
||||||
|
* No two plugins may share the same name.
|
||||||
|
*
|
||||||
|
* Fetched from package.json.
|
||||||
|
*/
|
||||||
|
readonly name?: string
|
||||||
|
|
||||||
|
/**
|
||||||
|
* The version for the plugin in the overlay.
|
||||||
|
*
|
||||||
|
* Fetched from package.json.
|
||||||
|
*/
|
||||||
|
readonly version?: string
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Name used in the overlay.
|
||||||
|
*/
|
||||||
|
readonly displayName: string
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Used in overlay.
|
||||||
|
* Should be a full sentence describing the plugin.
|
||||||
|
*/
|
||||||
|
readonly description: string
|
||||||
|
|
||||||
|
/**
|
||||||
|
* The path at which the plugin router is to be registered.
|
||||||
|
*/
|
||||||
|
readonly routerPath: string
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Link to plugin homepage.
|
||||||
|
*/
|
||||||
|
readonly homepageURL: string
|
||||||
|
|
||||||
|
/**
|
||||||
|
* init is called so that the plugin may initialize itself with the config.
|
||||||
|
*/
|
||||||
|
init(config: PluginConfig): void
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Returns the plugin's router.
|
||||||
|
*
|
||||||
|
* Mounted at <code-sever-root>/<plugin-path>
|
||||||
|
*
|
||||||
|
* If not present, the plugin provides no routes.
|
||||||
|
*/
|
||||||
|
router?(): express.Router
|
||||||
|
|
||||||
|
/**
|
||||||
|
* code-server uses this to collect the list of applications that
|
||||||
|
* the plugin can currently provide.
|
||||||
|
* It is called when /api/applications is hit or the overlay needs to
|
||||||
|
* refresh the list of applications
|
||||||
|
*
|
||||||
|
* Ensure this is as fast as possible.
|
||||||
|
*
|
||||||
|
* If not present, the plugin provides no applications.
|
||||||
|
*/
|
||||||
|
applications?(): Application[] | Promise<Application[]>
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* PluginConfig contains the configuration required for initializing
|
||||||
|
* a plugin.
|
||||||
|
*/
|
||||||
|
export interface PluginConfig {
|
||||||
|
/**
|
||||||
|
* All plugin logs should be logged via this logger.
|
||||||
|
*/
|
||||||
|
readonly logger: Logger
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Application represents a user accessible application.
|
||||||
|
*/
|
||||||
|
export interface Application {
|
||||||
|
readonly name: string
|
||||||
|
readonly version: string
|
||||||
|
|
||||||
|
/**
|
||||||
|
* When the user clicks on the icon in the overlay, they will be
|
||||||
|
* redirected to <code-server-root>/<plugin-path>/<app-path>
|
||||||
|
* where the application should be accessible.
|
||||||
|
*
|
||||||
|
* If undefined, then <code-server-root>/<plugin-path> is used.
|
||||||
|
*/
|
||||||
|
readonly path?: string
|
||||||
|
|
||||||
|
readonly description?: string
|
||||||
|
|
||||||
|
/**
|
||||||
|
* The path at which the icon for this application can be accessed.
|
||||||
|
* <code-server-root>/<plugin-path>/<app-path>/<icon-path>
|
||||||
|
*/
|
||||||
|
readonly iconPath: string
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Link to application homepage.
|
||||||
|
*/
|
||||||
|
readonly homepageURL: string
|
||||||
|
}
|
Loading…
Reference in New Issue