mirror of
https://github.com/coder/code-server.git
synced 2026-04-16 12:25:03 -05:00
Compare commits
114 Commits
| Author | SHA1 | Date | |
|---|---|---|---|
|
|
9bde62fbd6 | ||
|
|
6fbbb1047f | ||
|
|
e07a591745 | ||
|
|
676c7bf915 | ||
|
|
9ad7d0b7a3 | ||
|
|
07e7c38ea2 | ||
|
|
0b9af6ef67 | ||
|
|
c63dc3a1ea | ||
|
|
860c99e3b8 | ||
|
|
7e1e9d1249 | ||
|
|
62735da694 | ||
|
|
6cc1ee1b00 | ||
|
|
79443c14ff | ||
|
|
a0b7bf2180 | ||
|
|
30f3030530 | ||
|
|
759a78d9d8 | ||
|
|
7093f99a78 | ||
|
|
bca1bcfc03 | ||
|
|
4a3d2e5a94 | ||
|
|
14287df655 | ||
|
|
daf204eeda | ||
|
|
f20f7ac166 | ||
|
|
a7c43a8eb6 | ||
|
|
30d05aeb4b | ||
|
|
07580e1fcb | ||
|
|
e3699cf258 | ||
|
|
2a22676d93 | ||
|
|
36b3183b75 | ||
|
|
ec564091f1 | ||
|
|
ea105a9290 | ||
|
|
e453d3107d | ||
|
|
a4a03c1492 | ||
|
|
d7ba9ae633 | ||
|
|
00383b79b9 | ||
|
|
c6ba12942c | ||
|
|
d7e3112625 | ||
|
|
26c735b434 | ||
|
|
466a04f874 | ||
|
|
e0769dc13a | ||
|
|
fe19391c03 | ||
|
|
021c084e43 | ||
|
|
1902296702 | ||
|
|
bb1bf88439 | ||
|
|
0a8e71c647 | ||
|
|
6bdaada689 | ||
|
|
811cf3364a | ||
|
|
64a6a460c8 | ||
|
|
1e4e72aa5b | ||
|
|
fcfb03382a | ||
|
|
d67bd3f604 | ||
|
|
2d1de749f4 | ||
|
|
c6c293d53a | ||
|
|
daa1c86fe0 | ||
|
|
9002f118c3 | ||
|
|
a5b6d080bd | ||
|
|
9ff37977a8 | ||
|
|
f5489cd3a0 | ||
|
|
c86d7398ab | ||
|
|
9f963c7e66 | ||
|
|
8063c79e44 | ||
|
|
febf4ead96 | ||
|
|
3e28ab85a0 | ||
|
|
85b0804be5 | ||
|
|
ebbcb8d6a7 | ||
|
|
df3089f3ad | ||
|
|
7cc16ceb3a | ||
|
|
bfe731f4f3 | ||
|
|
c4f1c053bf | ||
|
|
4b3c089630 | ||
|
|
1c16814a89 | ||
|
|
c3c24fe4d2 | ||
|
|
6e8248cf0c | ||
|
|
dd996d8f60 | ||
|
|
fae07e14fb | ||
|
|
c308ae0edd | ||
|
|
9035bfa871 | ||
|
|
22c4a7e10f | ||
|
|
607444c695 | ||
|
|
b22f3cb72f | ||
|
|
eacca7d692 | ||
|
|
0aa98279d6 | ||
|
|
55a7e8b56f | ||
|
|
916e24e109 | ||
|
|
c7c62daa67 | ||
|
|
579bb94a6c | ||
|
|
a44b4455f5 | ||
|
|
548a35c0ee | ||
|
|
402f5ebd77 | ||
|
|
c2ac126a50 | ||
|
|
b3811a67e0 | ||
|
|
ddda280df4 | ||
|
|
b415b7524f | ||
|
|
7a982555a8 | ||
|
|
e64b186527 | ||
|
|
11eaf0b470 | ||
|
|
8b5deac92b | ||
|
|
9d87c5328c | ||
|
|
cc5ed1eb57 | ||
|
|
e998dc1e82 | ||
|
|
ffe6a663aa | ||
|
|
938b460685 | ||
|
|
fef619aef8 | ||
|
|
0a2328c1f6 | ||
|
|
e44e574ce1 | ||
|
|
7991e09bbc | ||
|
|
9fb318cf15 | ||
|
|
4a250be79a | ||
|
|
3761f7bd51 | ||
|
|
ceceef1dae | ||
|
|
35a2d71b67 | ||
|
|
617cd38c71 | ||
|
|
75c8fdeed2 | ||
|
|
de41646fc4 | ||
|
|
882a2bfd5a |
@@ -30,6 +30,7 @@ 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
|
||||||
|
|
||||||
settings:
|
settings:
|
||||||
# Does not work with CommonJS unfortunately.
|
# Does not work with CommonJS unfortunately.
|
||||||
|
|||||||
7
.github/ISSUE_TEMPLATE/doc.md
vendored
Normal file
7
.github/ISSUE_TEMPLATE/doc.md
vendored
Normal file
@@ -0,0 +1,7 @@
|
|||||||
|
---
|
||||||
|
name: Documentation improvement
|
||||||
|
about: Suggest a documentation improvement
|
||||||
|
title: ""
|
||||||
|
labels: "docs"
|
||||||
|
assignees: ""
|
||||||
|
---
|
||||||
10
.github/workflows/ci.yaml
vendored
10
.github/workflows/ci.yaml
vendored
@@ -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
|
||||||
|
|||||||
4
.github/workflows/publish.yaml
vendored
4
.github/workflows/publish.yaml
vendored
@@ -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:
|
||||||
|
|||||||
2
.gitignore
vendored
2
.gitignore
vendored
@@ -11,3 +11,5 @@ release-images/
|
|||||||
node_modules
|
node_modules
|
||||||
node-*
|
node-*
|
||||||
/plugins
|
/plugins
|
||||||
|
/lib/coder-cloud-agent
|
||||||
|
.home
|
||||||
|
|||||||
1
.gitmodules
vendored
1
.gitmodules
vendored
@@ -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
|
||||||
|
|||||||
50
README.md
50
README.md
@@ -1,4 +1,4 @@
|
|||||||
# code-server
|
# code-server · [](https://github.com/cdr/code-server/discussions) [](https://cdr.co/join-community) [](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,58 @@ 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 runs 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 [joining our alpha program](https://codercom.typeform.com/to/U4IKyv0W) if you don't want to worry about
|
||||||
|
|
||||||
|
- TLS
|
||||||
|
- Authentication
|
||||||
|
- Port Forwarding
|
||||||
|
|
||||||
## 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 a engineers to help maintain
|
We ([@cdr](https://github.com/cdr)) are looking for engineers to help [maintain
|
||||||
code-server, innovate on open source and streamline dev workflows.
|
code-server](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
|
||||||
|
|
||||||
|
|||||||
@@ -18,13 +18,15 @@ Make sure you have `$GITHUB_TOKEN` set and [hub](https://github.com/github/hub)
|
|||||||
1. Update in `package.json`
|
1. Update in `package.json`
|
||||||
2. Update in [./doc/install.md](../doc/install.md)
|
2. Update in [./doc/install.md](../doc/install.md)
|
||||||
2. GitHub actions will generate the `npm-package`, `release-packages` and `release-images` artifacts.
|
2. GitHub actions will generate the `npm-package`, `release-packages` and `release-images` artifacts.
|
||||||
|
1. You do not have to wait for these.
|
||||||
3. Run `yarn release:github-draft` to create a GitHub draft release from the template with
|
3. Run `yarn release:github-draft` to create a GitHub draft release from the template with
|
||||||
the updated version.
|
the updated version.
|
||||||
1. Summarize the major changes in the release notes and link to the relevant issues.
|
1. Summarize the major changes in the release notes and link to the relevant issues.
|
||||||
4. Wait for the artifacts in step 2 to build.
|
4. Wait for the artifacts in step 2 to build.
|
||||||
5. Run `yarn release:github-assets` to download the `release-packages` artifact and
|
5. Run `yarn release:github-assets` to download the `release-packages` artifact.
|
||||||
upload them to the draft release.
|
- It will upload them to the draft release.
|
||||||
6. Run some basic sanity tests on one of the released packages.
|
6. Run some basic sanity tests on one of the released packages.
|
||||||
|
- Especially make sure the terminal works fine.
|
||||||
7. Make sure the github release tag is the commit with the artifacts. This is a bug in
|
7. Make sure the github release tag is the commit with the artifacts. This is a bug in
|
||||||
`hub` where uploading assets in step 5 will break the tag.
|
`hub` where uploading assets in step 5 will break the tag.
|
||||||
8. Publish the release and merge the PR.
|
8. Publish the release and merge the PR.
|
||||||
@@ -36,7 +38,6 @@ Make sure you have `$GITHUB_TOKEN` set and [hub](https://github.com/github/hub)
|
|||||||
10. Wait for the npm package to be published.
|
10. Wait for the npm package to be published.
|
||||||
11. Update the homebrew package.
|
11. Update the homebrew package.
|
||||||
- Send a pull request to [homebrew-core](https://github.com/Homebrew/homebrew-core) with the URL in the [formula](https://github.com/Homebrew/homebrew-core/blob/master/Formula/code-server.rb) updated.
|
- Send a pull request to [homebrew-core](https://github.com/Homebrew/homebrew-core) with the URL in the [formula](https://github.com/Homebrew/homebrew-core/blob/master/Formula/code-server.rb) updated.
|
||||||
12. Make sure to add a release without the `v` prefix for autoupdate from `3.2.0`.
|
|
||||||
|
|
||||||
## dev
|
## dev
|
||||||
|
|
||||||
|
|||||||
@@ -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
|
||||||
@@ -37,6 +41,7 @@ bundle_code_server() {
|
|||||||
rsync src/browser/media/ "$RELEASE_PATH/src/browser/media"
|
rsync src/browser/media/ "$RELEASE_PATH/src/browser/media"
|
||||||
mkdir -p "$RELEASE_PATH/src/browser/pages"
|
mkdir -p "$RELEASE_PATH/src/browser/pages"
|
||||||
rsync src/browser/pages/*.html "$RELEASE_PATH/src/browser/pages"
|
rsync src/browser/pages/*.html "$RELEASE_PATH/src/browser/pages"
|
||||||
|
rsync src/browser/robots.txt "$RELEASE_PATH/src/browser"
|
||||||
|
|
||||||
# Adds the commit to package.json
|
# Adds the commit to package.json
|
||||||
jq --slurp '.[0] * .[1]' package.json <(
|
jq --slurp '.[0] * .[1]' package.json <(
|
||||||
@@ -51,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"
|
||||||
rm -Rf "$VSCODE_OUT_PATH/extensions/node_modules"
|
if [ "$KEEP_MODULES" = 0 ]; then
|
||||||
|
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
|
||||||
|
|||||||
@@ -12,7 +12,7 @@ homepage: "https://github.com/cdr/code-server"
|
|||||||
license: "MIT"
|
license: "MIT"
|
||||||
files:
|
files:
|
||||||
./ci/build/code-server-nfpm.sh: /usr/bin/code-server
|
./ci/build/code-server-nfpm.sh: /usr/bin/code-server
|
||||||
./ci/build/code-server.service: /usr/lib/systemd/system/code-server.service
|
./ci/build/code-server@.service: /usr/lib/systemd/system/code-server@.service
|
||||||
# Only included for backwards compat with previous releases that shipped
|
# Only included for backwards compat with previous releases that shipped
|
||||||
# the user service. See #1997
|
# the user service. See #1997
|
||||||
./ci/build/code-server-user.service: /usr/lib/systemd/user/code-server.service
|
./ci/build/code-server-user.service: /usr/lib/systemd/user/code-server.service
|
||||||
|
|||||||
@@ -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,8 +15,7 @@ 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
|
if [[ $installed_extensions != "ms-python.python" ]]; then
|
||||||
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
|
||||||
|
|||||||
@@ -6,7 +6,7 @@ main() {
|
|||||||
|
|
||||||
cd ./lib/vscode
|
cd ./lib/vscode
|
||||||
git add -A
|
git add -A
|
||||||
git diff HEAD > ../../ci/dev/vscode.patch
|
git diff HEAD --full-index > ../../ci/dev/vscode.patch
|
||||||
}
|
}
|
||||||
|
|
||||||
main "$@"
|
main "$@"
|
||||||
|
|||||||
@@ -4,14 +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 \
|
||||||
"$(docker_build ./ci/images/debian8)" \
|
-u "$(id -u):$(id -g)" \
|
||||||
|
-e CI \
|
||||||
|
"$(docker_build ./ci/images/"${IMAGE-debian10}")" \
|
||||||
"$@"
|
"$@"
|
||||||
}
|
}
|
||||||
|
|
||||||
@@ -7,10 +7,7 @@ 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
|
shellcheck -e SC2046,SC2164,SC2154,SC1091,SC1090,SC2002 $(git ls-files "*.sh")
|
||||||
if [[ ! ${CI-} ]]; then
|
|
||||||
shellcheck -e SC2046,SC2164,SC2154,SC1091,SC1090,SC2002 $(git ls-files "*.sh")
|
|
||||||
fi
|
|
||||||
}
|
}
|
||||||
|
|
||||||
main "$@"
|
main "$@"
|
||||||
|
|||||||
File diff suppressed because it is too large
Load Diff
@@ -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,11 +15,16 @@ 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
|
||||||
|
|||||||
@@ -1,4 +1,4 @@
|
|||||||
FROM debian:8
|
FROM debian:10
|
||||||
|
|
||||||
RUN apt-get update
|
RUN apt-get update
|
||||||
|
|
||||||
@@ -24,28 +24,23 @@ 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
|
||||||
@@ -39,6 +39,10 @@ COPY ci/release-image/entrypoint.sh /usr/bin/entrypoint.sh
|
|||||||
RUN dpkg -i /tmp/code-server*$(dpkg --print-architecture).deb && rm /tmp/code-server*.deb
|
RUN dpkg -i /tmp/code-server*$(dpkg --print-architecture).deb && rm /tmp/code-server*.deb
|
||||||
|
|
||||||
EXPOSE 8080
|
EXPOSE 8080
|
||||||
USER coder
|
# This way, if someone sets $DOCKER_USER, docker-exec will still work as
|
||||||
|
# the uid will remain the same. note: only relevant if -u isn't passed to
|
||||||
|
# docker-run.
|
||||||
|
USER 1000
|
||||||
|
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,18 +1,20 @@
|
|||||||
#!/usr/bin/env sh
|
#!/bin/sh
|
||||||
set -eu
|
set -eu
|
||||||
|
|
||||||
|
# We do this first to ensure sudo works below when renaming the user.
|
||||||
|
# Otherwise the current container UID may not exist in the passwd database.
|
||||||
|
eval "$(fixuid -q)"
|
||||||
|
|
||||||
if [ "${DOCKER_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
|
||||||
sudo usermod --login "$DOCKER_USER" \
|
# Unfortunately we cannot change $HOME as we cannot move any bind mounts
|
||||||
--move-home --home "/home/$DOCKER_USER" \
|
# nor can we bind mount $HOME into a new home as that requires a privileged container.
|
||||||
coder
|
sudo usermod --login "$DOCKER_USER" coder
|
||||||
sudo groupmod -n "$DOCKER_USER" coder
|
sudo groupmod -n "$DOCKER_USER" coder
|
||||||
|
|
||||||
|
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
|
|
||||||
export HOME="/home/$DOCKER_USER"
|
|
||||||
fi
|
fi
|
||||||
|
|
||||||
# This isn't set by default.
|
dumb-init /usr/bin/code-server "$@"
|
||||||
export USER="$(whoami)"
|
|
||||||
dumb-init fixuid -q /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,48 +43,48 @@ 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/exec.sh yarn
|
./ci/dev/image/run.sh yarn
|
||||||
./ci/dev/image/exec.sh yarn vscode
|
./ci/dev/image/run.sh yarn vscode
|
||||||
./ci/dev/image/exec.sh yarn watch
|
./ci/dev/image/run.sh yarn watch
|
||||||
```
|
```
|
||||||
|
|
||||||
`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/steps/release.sh
|
./ci/dev/image/run.sh ./ci/steps/release.sh
|
||||||
```
|
```
|
||||||
|
|
||||||
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/exec.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
|
||||||
|
|||||||
15
doc/FAQ.md
15
doc/FAQ.md
@@ -19,6 +19,7 @@
|
|||||||
- [How does code-server decide what workspace or folder to open?](#how-does-code-server-decide-what-workspace-or-folder-to-open)
|
- [How does code-server decide what workspace or folder to open?](#how-does-code-server-decide-what-workspace-or-folder-to-open)
|
||||||
- [How do I debug issues with code-server?](#how-do-i-debug-issues-with-code-server)
|
- [How do I debug issues with code-server?](#how-do-i-debug-issues-with-code-server)
|
||||||
- [Heartbeat File](#heartbeat-file)
|
- [Heartbeat File](#heartbeat-file)
|
||||||
|
- [Healthz endpoint](#healthz-endpoint)
|
||||||
- [How does the config file work?](#how-does-the-config-file-work)
|
- [How does the config file work?](#how-does-the-config-file-work)
|
||||||
- [Blank screen on iPad?](#blank-screen-on-ipad)
|
- [Blank screen on iPad?](#blank-screen-on-ipad)
|
||||||
- [Isn't an install script piped into sh insecure?](#isnt-an-install-script-piped-into-sh-insecure)
|
- [Isn't an install script piped into sh insecure?](#isnt-an-install-script-piped-into-sh-insecure)
|
||||||
@@ -242,6 +243,20 @@ older than X minutes, kill `code-server`.
|
|||||||
|
|
||||||
[#1636](https://github.com/cdr/code-server/issues/1636) will make the experience here better.
|
[#1636](https://github.com/cdr/code-server/issues/1636) will make the experience here better.
|
||||||
|
|
||||||
|
## Healthz endpoint
|
||||||
|
|
||||||
|
`code-server` exposes an endpoint at `/healthz` which can be used to check
|
||||||
|
whether `code-server` is up without triggering a heartbeat. The response will
|
||||||
|
include a status (`alive` or `expired`) and a timestamp for the last heartbeat
|
||||||
|
(defaults to `0`). This endpoint does not require authentication.
|
||||||
|
|
||||||
|
```json
|
||||||
|
{
|
||||||
|
"status": "alive",
|
||||||
|
"lastHeartbeat": 1599166210566
|
||||||
|
}
|
||||||
|
```
|
||||||
|
|
||||||
## How does the config file work?
|
## How does the config file work?
|
||||||
|
|
||||||
When `code-server` starts up, it creates a default config file in `~/.config/code-server/config.yaml` that looks
|
When `code-server` starts up, it creates a default config file in `~/.config/code-server/config.yaml` that looks
|
||||||
|
|||||||
@@ -79,8 +79,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.6.1/code-server_3.6.1_amd64.deb
|
||||||
sudo dpkg -i code-server_3.5.0_amd64.deb
|
sudo dpkg -i code-server_3.6.1_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 +88,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.6.1/code-server-3.6.1-amd64.rpm
|
||||||
sudo rpm -i code-server-3.5.0-amd64.rpm
|
sudo rpm -i code-server-3.6.1-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 +158,10 @@ Here is an example script for installing and using a standalone `code-server` re
|
|||||||
|
|
||||||
```bash
|
```bash
|
||||||
mkdir -p ~/.local/lib ~/.local/bin
|
mkdir -p ~/.local/lib ~/.local/bin
|
||||||
curl -fL https://github.com/cdr/code-server/releases/download/v3.5.0/code-server-3.5.0-linux-amd64.tar.gz \
|
curl -fL https://github.com/cdr/code-server/releases/download/v3.6.1/code-server-3.6.1-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.6.1-linux-amd64 ~/.local/lib/code-server-3.6.1
|
||||||
ln -s ~/.local/lib/code-server-3.5.0/bin/code-server ~/.local/bin/code-server
|
ln -s ~/.local/lib/code-server-3.6.1/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
|
||||||
@@ -179,10 +179,11 @@ code-server
|
|||||||
# easily access/modify your code-server config in $HOME/.config/code-server/config.json
|
# easily access/modify your code-server config in $HOME/.config/code-server/config.json
|
||||||
# outside the container.
|
# outside the container.
|
||||||
mkdir -p ~/.config
|
mkdir -p ~/.config
|
||||||
docker run -it -p 127.0.0.1:8080:8080 \
|
docker run -it --name code-server -p 127.0.0.1:8080:8080 \
|
||||||
-v "$HOME/.config:/home/coder/.config" \
|
-v "$HOME/.config:/home/coder/.config" \
|
||||||
-v "$PWD:/home/coder/project" \
|
-v "$PWD:/home/coder/project" \
|
||||||
-u "$(id -u):$(id -g)" \
|
-u "$(id -u):$(id -g)" \
|
||||||
|
-e "DOCKER_USER=$USER" \
|
||||||
codercom/code-server:latest
|
codercom/code-server:latest
|
||||||
```
|
```
|
||||||
|
|
||||||
|
|||||||
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 "$@"
|
||||||
|
|||||||
Submodule lib/vscode updated: a0479759d6...93c2f0fbf1
@@ -1,7 +1,7 @@
|
|||||||
{
|
{
|
||||||
"name": "code-server",
|
"name": "code-server",
|
||||||
"license": "MIT",
|
"license": "MIT",
|
||||||
"version": "3.5.0",
|
"version": "3.6.2",
|
||||||
"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": {
|
||||||
@@ -39,6 +39,7 @@
|
|||||||
"@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/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",
|
||||||
@@ -76,6 +77,7 @@
|
|||||||
"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",
|
||||||
|
|||||||
@@ -47,5 +47,5 @@
|
|||||||
</div>
|
</div>
|
||||||
</body>
|
</body>
|
||||||
<script data-cfasync="false" src="{{CS_STATIC_BASE}}/dist/register.js"></script>
|
<script data-cfasync="false" src="{{CS_STATIC_BASE}}/dist/register.js"></script>
|
||||||
<script data-cfasync="false" src="{{CS_STATIC_BASE}}/dist/login.js"></script>
|
<script data-cfasync="false" src="{{CS_STATIC_BASE}}/dist/pages/login.js"></script>
|
||||||
</html>
|
</html>
|
||||||
|
|||||||
@@ -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"
|
||||||
|
|||||||
@@ -17,7 +17,7 @@ try {
|
|||||||
}
|
}
|
||||||
// FIXME: Only works if path separators are /.
|
// FIXME: Only works if path separators are /.
|
||||||
const path = nlsConfig._resolvedLanguagePackCoreLocation + "/" + bundle.replace(/\//g, "!") + ".nls.json"
|
const path = nlsConfig._resolvedLanguagePackCoreLocation + "/" + bundle.replace(/\//g, "!") + ".nls.json"
|
||||||
fetch(`{{BASE}}/resource/?path=${encodeURIComponent(path)}`)
|
fetch(`${options.base}/vscode/resource/?path=${encodeURIComponent(path)}`)
|
||||||
.then((response) => response.json())
|
.then((response) => response.json())
|
||||||
.then((json) => {
|
.then((json) => {
|
||||||
bundles[bundle] = json
|
bundles[bundle] = json
|
||||||
@@ -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`,
|
||||||
@@ -41,6 +42,7 @@ try {
|
|||||||
"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`,
|
"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`,
|
||||||
},
|
},
|
||||||
|
|||||||
@@ -10,7 +10,7 @@ if ("serviceWorker" in navigator) {
|
|||||||
const path = normalize(`${options.csStaticBase}/dist/serviceWorker.js`)
|
const path = normalize(`${options.csStaticBase}/dist/serviceWorker.js`)
|
||||||
navigator.serviceWorker
|
navigator.serviceWorker
|
||||||
.register(path, {
|
.register(path, {
|
||||||
scope: options.base || "/",
|
scope: (options.base ?? "") + "/",
|
||||||
})
|
})
|
||||||
.then(() => {
|
.then(() => {
|
||||||
console.log("[Service Worker] registered")
|
console.log("[Service Worker] registered")
|
||||||
|
|||||||
2
src/browser/robots.txt
Normal file
2
src/browser/robots.txt
Normal file
@@ -0,0 +1,2 @@
|
|||||||
|
User-agent: *
|
||||||
|
Disallow: /
|
||||||
21
src/node/app/health.ts
Normal file
21
src/node/app/health.ts
Normal file
@@ -0,0 +1,21 @@
|
|||||||
|
import { HttpProvider, HttpResponse, Heart, HttpProviderOptions } from "../http"
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Check the heartbeat.
|
||||||
|
*/
|
||||||
|
export class HealthHttpProvider extends HttpProvider {
|
||||||
|
public constructor(options: HttpProviderOptions, private readonly heart: Heart) {
|
||||||
|
super(options)
|
||||||
|
}
|
||||||
|
|
||||||
|
public async handleRequest(): Promise<HttpResponse> {
|
||||||
|
return {
|
||||||
|
cache: false,
|
||||||
|
mime: "application/json",
|
||||||
|
content: {
|
||||||
|
status: this.heart.alive() ? "alive" : "expired",
|
||||||
|
lastHeartbeat: this.heart.lastHeartbeat,
|
||||||
|
},
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
@@ -50,12 +50,15 @@ export class VscodeHttpProvider extends HttpProvider {
|
|||||||
|
|
||||||
logger.debug("setting up vs code...")
|
logger.debug("setting up vs code...")
|
||||||
return new Promise<WorkbenchOptions>((resolve, reject) => {
|
return new Promise<WorkbenchOptions>((resolve, reject) => {
|
||||||
vscode.once("message", (message: VscodeMessage) => {
|
const onMessage = (message: VscodeMessage) => {
|
||||||
logger.debug("got message from vs code", field("message", message))
|
// There can be parallel initializations so wait for the right ID.
|
||||||
return message.type === "options" && message.id === id
|
if (message.type === "options" && message.id === id) {
|
||||||
? resolve(message.options)
|
logger.trace("got message from vs code", field("message", message))
|
||||||
: reject(new Error("Unexpected response during initialization"))
|
vscode.off("message", onMessage)
|
||||||
})
|
resolve(message.options)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
vscode.on("message", onMessage)
|
||||||
vscode.once("error", reject)
|
vscode.once("error", reject)
|
||||||
vscode.once("exit", (code) => reject(new Error(`VS Code exited unexpectedly with code ${code}`)))
|
vscode.once("exit", (code) => reject(new Error(`VS Code exited unexpectedly with code ${code}`)))
|
||||||
this.send({ type: "init", id, options }, vscode)
|
this.send({ type: "init", id, options }, vscode)
|
||||||
@@ -77,7 +80,7 @@ export class VscodeHttpProvider extends HttpProvider {
|
|||||||
|
|
||||||
this._vscode = new Promise((resolve, reject) => {
|
this._vscode = new Promise((resolve, reject) => {
|
||||||
vscode.once("message", (message: VscodeMessage) => {
|
vscode.once("message", (message: VscodeMessage) => {
|
||||||
logger.debug("got message from vs code", field("message", message))
|
logger.trace("got message from vs code", field("message", message))
|
||||||
return message.type === "ready"
|
return message.type === "ready"
|
||||||
? resolve(vscode)
|
? resolve(vscode)
|
||||||
: reject(new Error("Unexpected response waiting for ready response"))
|
: reject(new Error("Unexpected response waiting for ready response"))
|
||||||
|
|||||||
148
src/node/cli.ts
148
src/node/cli.ts
@@ -5,7 +5,7 @@ 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 { AuthType } from "./http"
|
||||||
import { generatePassword, humanPath, paths } from "./util"
|
import { canConnect, generatePassword, humanPath, paths } from "./util"
|
||||||
|
|
||||||
export class Optional<T> {
|
export class Optional<T> {
|
||||||
public constructor(public readonly value?: T) {}
|
public constructor(public readonly value?: T) {}
|
||||||
@@ -47,6 +47,8 @@ export interface Args extends VsArgs {
|
|||||||
readonly _: string[]
|
readonly _: string[]
|
||||||
readonly "reuse-window"?: boolean
|
readonly "reuse-window"?: boolean
|
||||||
readonly "new-window"?: boolean
|
readonly "new-window"?: boolean
|
||||||
|
|
||||||
|
readonly link?: OptionalString
|
||||||
}
|
}
|
||||||
|
|
||||||
interface Option<T> {
|
interface Option<T> {
|
||||||
@@ -63,6 +65,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
|
||||||
@@ -130,7 +137,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 +152,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 +186,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 = (
|
||||||
@@ -287,6 +327,21 @@ export const parse = (
|
|||||||
|
|
||||||
logger.debug("parsed command line", field("args", args))
|
logger.debug("parsed command line", field("args", args))
|
||||||
|
|
||||||
|
return args
|
||||||
|
}
|
||||||
|
|
||||||
|
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"]) {
|
||||||
|
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.
|
||||||
if (args.verbose) {
|
if (args.verbose) {
|
||||||
@@ -329,21 +384,6 @@ export const parse = (
|
|||||||
return args
|
return args
|
||||||
}
|
}
|
||||||
|
|
||||||
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"]) {
|
|
||||||
args["extensions-dir"] = path.join(args["user-data-dir"], "extensions")
|
|
||||||
}
|
|
||||||
|
|
||||||
return args
|
|
||||||
}
|
|
||||||
|
|
||||||
async function defaultConfigFile(): Promise<string> {
|
async function defaultConfigFile(): Promise<string> {
|
||||||
return `bind-addr: 127.0.0.1:8080
|
return `bind-addr: 127.0.0.1:8080
|
||||||
auth: password
|
auth: password
|
||||||
@@ -370,10 +410,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,
|
||||||
@@ -401,7 +437,10 @@ export async function readConfigFile(configPath?: string): Promise<Args> {
|
|||||||
|
|
||||||
function parseBindAddr(bindAddr: string): [string, number] {
|
function parseBindAddr(bindAddr: string): [string, number] {
|
||||||
const u = new URL(`http://${bindAddr}`)
|
const u = new URL(`http://${bindAddr}`)
|
||||||
return [u.hostname, parseInt(u.port, 10)]
|
// 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.
|
||||||
|
return [u.hostname, u.port ? parseInt(u.port, 10) : 80]
|
||||||
}
|
}
|
||||||
|
|
||||||
interface Addr {
|
interface Addr {
|
||||||
@@ -453,3 +492,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
|
||||||
|
}
|
||||||
|
|||||||
43
src/node/coder-cloud.ts
Normal file
43
src/node/coder-cloud.ts
Normal file
@@ -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)
|
||||||
|
}
|
||||||
@@ -5,23 +5,27 @@ 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 { LoginHttpProvider } from "./app/login"
|
import { LoginHttpProvider } from "./app/login"
|
||||||
import { ProxyHttpProvider } from "./app/proxy"
|
import { ProxyHttpProvider } from "./app/proxy"
|
||||||
import { StaticHttpProvider } from "./app/static"
|
import { StaticHttpProvider } from "./app/static"
|
||||||
import { UpdateHttpProvider } from "./app/update"
|
import { UpdateHttpProvider } from "./app/update"
|
||||||
import { VscodeHttpProvider } from "./app/vscode"
|
import { VscodeHttpProvider } from "./app/vscode"
|
||||||
import { Args, bindAddrFromAllSources, optionDescriptions, parse, readConfigFile, setDefaults } from "./cli"
|
import {
|
||||||
|
Args,
|
||||||
|
bindAddrFromAllSources,
|
||||||
|
optionDescriptions,
|
||||||
|
parse,
|
||||||
|
readConfigFile,
|
||||||
|
setDefaults,
|
||||||
|
shouldOpenInExistingInstance,
|
||||||
|
shouldRunVsCodeCli,
|
||||||
|
} from "./cli"
|
||||||
|
import { coderCloudBind } from "./coder-cloud"
|
||||||
import { AuthType, HttpServer, HttpServerOptions } from "./http"
|
import { AuthType, HttpServer, HttpServerOptions } from "./http"
|
||||||
import { loadPlugins } from "./plugin"
|
import { loadPlugins } from "./plugin"
|
||||||
import { generateCertificate, hash, humanPath, open } from "./util"
|
import { generateCertificate, hash, humanPath, open } from "./util"
|
||||||
import { ipcMain, wrap } from "./wrapper"
|
import { ipcMain, WrapperProcess } from "./wrapper"
|
||||||
|
|
||||||
process.on("uncaughtException", (error) => {
|
|
||||||
logger.error(`Uncaught exception: ${error.message}`)
|
|
||||||
if (typeof error.stack !== "undefined") {
|
|
||||||
logger.error(error.stack)
|
|
||||||
}
|
|
||||||
})
|
|
||||||
|
|
||||||
let pkg: { version?: string; commit?: string } = {}
|
let pkg: { version?: string; commit?: string } = {}
|
||||||
try {
|
try {
|
||||||
@@ -33,7 +37,100 @@ try {
|
|||||||
const version = pkg.version || "development"
|
const version = pkg.version || "development"
|
||||||
const commit = pkg.commit || "development"
|
const commit = pkg.commit || "development"
|
||||||
|
|
||||||
const main = async (args: Args, cliArgs: Args, configArgs: Args): Promise<void> => {
|
export const runVsCodeCli = (args: Args): void => {
|
||||||
|
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", field("type", message.type))
|
||||||
|
process.exit(1)
|
||||||
|
}
|
||||||
|
const send: CliMessage = { type: "cli", args }
|
||||||
|
vscode.send(send)
|
||||||
|
})
|
||||||
|
vscode.once("error", (error) => {
|
||||||
|
logger.error("Got error from VS Code", field("error", error))
|
||||||
|
process.exit(1)
|
||||||
|
})
|
||||||
|
vscode.on("exit", (code) => process.exit(code || 0))
|
||||||
|
}
|
||||||
|
|
||||||
|
export const openInExistingInstance = async (args: Args, socketPath: string): Promise<void> => {
|
||||||
|
const pipeArgs: OpenCommandPipeArgs & { fileURIs: string[] } = {
|
||||||
|
type: "open",
|
||||||
|
folderURIs: [],
|
||||||
|
fileURIs: [],
|
||||||
|
forceReuseWindow: args["reuse-window"],
|
||||||
|
forceNewWindow: args["new-window"],
|
||||||
|
}
|
||||||
|
|
||||||
|
const isDir = async (path: string): Promise<boolean> => {
|
||||||
|
try {
|
||||||
|
const st = await fs.stat(path)
|
||||||
|
return st.isDirectory()
|
||||||
|
} catch (error) {
|
||||||
|
return false
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
for (let i = 0; i < args._.length; i++) {
|
||||||
|
const fp = path.resolve(args._[i])
|
||||||
|
if (await isDir(fp)) {
|
||||||
|
pipeArgs.folderURIs.push(fp)
|
||||||
|
} else {
|
||||||
|
pipeArgs.fileURIs.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: Args, configArgs: Args): Promise<void> => {
|
||||||
|
if (args.link) {
|
||||||
|
// If we're being exposed to the cloud, we listen on a random address and disable auth.
|
||||||
|
args = {
|
||||||
|
...args,
|
||||||
|
host: "localhost",
|
||||||
|
port: 0,
|
||||||
|
auth: AuthType.None,
|
||||||
|
socket: undefined,
|
||||||
|
cert: undefined,
|
||||||
|
}
|
||||||
|
logger.info("link: disabling auth and listening on random localhost port for cloud agent")
|
||||||
|
}
|
||||||
|
|
||||||
if (!args.auth) {
|
if (!args.auth) {
|
||||||
args = {
|
args = {
|
||||||
...args,
|
...args,
|
||||||
@@ -50,7 +147,7 @@ const main = async (args: Args, cliArgs: Args, configArgs: Args): Promise<void>
|
|||||||
if (args.auth === AuthType.Password && !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)
|
const [host, port] = bindAddrFromAllSources(args, configArgs)
|
||||||
|
|
||||||
// Spawn the main HTTP server.
|
// Spawn the main HTTP server.
|
||||||
const options: HttpServerOptions = {
|
const options: HttpServerOptions = {
|
||||||
@@ -80,16 +177,19 @@ const main = async (args: Args, cliArgs: Args, configArgs: Args): Promise<void>
|
|||||||
httpServer.registerHttpProvider("/proxy", ProxyHttpProvider)
|
httpServer.registerHttpProvider("/proxy", ProxyHttpProvider)
|
||||||
httpServer.registerHttpProvider("/login", LoginHttpProvider, args.config!, envPassword)
|
httpServer.registerHttpProvider("/login", LoginHttpProvider, args.config!, envPassword)
|
||||||
httpServer.registerHttpProvider("/static", StaticHttpProvider)
|
httpServer.registerHttpProvider("/static", StaticHttpProvider)
|
||||||
|
httpServer.registerHttpProvider("/healthz", HealthHttpProvider, httpServer.heart)
|
||||||
|
|
||||||
await loadPlugins(httpServer, args)
|
await loadPlugins(httpServer, args)
|
||||||
|
|
||||||
ipcMain().onDispose(() => {
|
ipcMain.onDispose(() => {
|
||||||
httpServer.dispose().then((errors) => {
|
httpServer.dispose().then((errors) => {
|
||||||
errors.forEach((error) => logger.error(error.message))
|
errors.forEach((error) => logger.error(error.message))
|
||||||
})
|
})
|
||||||
})
|
})
|
||||||
|
|
||||||
logger.info(`code-server ${version} ${commit}`)
|
logger.info(`code-server ${version} ${commit}`)
|
||||||
|
logger.info(`Using config file ${humanPath(args.config)}`)
|
||||||
|
|
||||||
const serverAddress = await httpServer.listen()
|
const serverAddress = await httpServer.listen()
|
||||||
logger.info(`HTTP server listening on ${serverAddress}`)
|
logger.info(`HTTP server listening on ${serverAddress}`)
|
||||||
|
|
||||||
@@ -123,27 +223,38 @@ const main = async (args: Args, cliArgs: Args, configArgs: Args): Promise<void>
|
|||||||
if (serverAddress && !options.socket && args.open) {
|
if (serverAddress && !options.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)
|
await open(openAddress).catch((error: Error) => {
|
||||||
|
logger.error("Failed to open", field("address", openAddress), field("error", error))
|
||||||
|
})
|
||||||
logger.info(`Opened ${openAddress}`)
|
logger.info(`Opened ${openAddress}`)
|
||||||
}
|
}
|
||||||
|
|
||||||
|
if (args.link) {
|
||||||
|
try {
|
||||||
|
await coderCloudBind(serverAddress!, args.link.value)
|
||||||
|
} catch (err) {
|
||||||
|
logger.error(err.message)
|
||||||
|
ipcMain.exit(1)
|
||||||
|
}
|
||||||
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
async function entry(): Promise<void> {
|
async function entry(): Promise<void> {
|
||||||
const tryParse = async (): Promise<[Args, Args, Args]> => {
|
const cliArgs = parse(process.argv.slice(2))
|
||||||
try {
|
const configArgs = await readConfigFile(cliArgs.config)
|
||||||
const cliArgs = parse(process.argv.slice(2))
|
// This prioritizes the flags set in args over the ones in the config file.
|
||||||
const configArgs = await readConfigFile(cliArgs.config)
|
let args = Object.assign(configArgs, cliArgs)
|
||||||
// This prioritizes the flags set in args over the ones in the config file.
|
args = await setDefaults(args)
|
||||||
let args = Object.assign(configArgs, cliArgs)
|
|
||||||
args = await setDefaults(args)
|
// There's no need to check flags like --help or to spawn in an existing
|
||||||
return [args, cliArgs, configArgs]
|
// instance for the child process because these would have already happened in
|
||||||
} catch (error) {
|
// the parent and the child wouldn't have been spawned.
|
||||||
console.error(error.message)
|
if (ipcMain.isChild) {
|
||||||
process.exit(1)
|
await ipcMain.handshake()
|
||||||
}
|
ipcMain.preventExit()
|
||||||
|
return main(args, configArgs)
|
||||||
}
|
}
|
||||||
|
|
||||||
const [args, cliArgs, configArgs] = await tryParse()
|
|
||||||
if (args.help) {
|
if (args.help) {
|
||||||
console.log("code-server", version, commit)
|
console.log("code-server", version, commit)
|
||||||
console.log("")
|
console.log("")
|
||||||
@@ -153,7 +264,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,
|
||||||
@@ -163,83 +277,23 @@ async function entry(): Promise<void> {
|
|||||||
} else {
|
} else {
|
||||||
console.log(version, commit)
|
console.log(version, commit)
|
||||||
}
|
}
|
||||||
process.exit(0)
|
return
|
||||||
} else if (process.env.VSCODE_IPC_HOOK_CLI) {
|
|
||||||
const pipeArgs: OpenCommandPipeArgs = {
|
|
||||||
type: "open",
|
|
||||||
folderURIs: [],
|
|
||||||
forceReuseWindow: args["reuse-window"],
|
|
||||||
forceNewWindow: args["new-window"],
|
|
||||||
}
|
|
||||||
const isDir = async (path: string): Promise<boolean> => {
|
|
||||||
try {
|
|
||||||
const st = await fs.stat(path)
|
|
||||||
return st.isDirectory()
|
|
||||||
} catch (error) {
|
|
||||||
return false
|
|
||||||
}
|
|
||||||
}
|
|
||||||
for (let i = 0; i < args._.length; i++) {
|
|
||||||
const fp = path.resolve(args._[i])
|
|
||||||
if (await isDir(fp)) {
|
|
||||||
pipeArgs.folderURIs.push(fp)
|
|
||||||
} else {
|
|
||||||
if (!pipeArgs.fileURIs) {
|
|
||||||
pipeArgs.fileURIs = []
|
|
||||||
}
|
|
||||||
pipeArgs.fileURIs.push(fp)
|
|
||||||
}
|
|
||||||
}
|
|
||||||
if (pipeArgs.forceNewWindow && pipeArgs.fileURIs && pipeArgs.fileURIs.length > 0) {
|
|
||||||
logger.error("new-window can only be used with folder paths")
|
|
||||||
process.exit(1)
|
|
||||||
}
|
|
||||||
if (pipeArgs.folderURIs.length === 0 && (!pipeArgs.fileURIs || pipeArgs.fileURIs.length === 0)) {
|
|
||||||
logger.error("Please specify at least one file or folder argument")
|
|
||||||
process.exit(1)
|
|
||||||
}
|
|
||||||
const vscode = http.request(
|
|
||||||
{
|
|
||||||
path: "/",
|
|
||||||
method: "POST",
|
|
||||||
socketPath: process.env["VSCODE_IPC_HOOK_CLI"],
|
|
||||||
},
|
|
||||||
(res) => {
|
|
||||||
res.on("data", (message) => {
|
|
||||||
logger.debug("Got message from VS Code", field("message", message.toString()))
|
|
||||||
})
|
|
||||||
},
|
|
||||||
)
|
|
||||||
vscode.on("error", (err) => {
|
|
||||||
logger.debug("Got error from VS Code", field("error", err))
|
|
||||||
})
|
|
||||||
vscode.write(JSON.stringify(pipeArgs))
|
|
||||||
vscode.end()
|
|
||||||
} else 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)
|
|
||||||
})
|
|
||||||
vscode.once("error", (error) => {
|
|
||||||
logger.error(error.message)
|
|
||||||
process.exit(1)
|
|
||||||
})
|
|
||||||
vscode.on("exit", (code) => process.exit(code || 0))
|
|
||||||
} else {
|
|
||||||
wrap(() => main(args, cliArgs, configArgs))
|
|
||||||
}
|
}
|
||||||
|
|
||||||
|
if (shouldRunVsCodeCli(args)) {
|
||||||
|
return runVsCodeCli(args)
|
||||||
|
}
|
||||||
|
|
||||||
|
const socketPath = await shouldOpenInExistingInstance(cliArgs)
|
||||||
|
if (socketPath) {
|
||||||
|
return openInExistingInstance(args, socketPath)
|
||||||
|
}
|
||||||
|
|
||||||
|
const wrapper = new WrapperProcess(require("../../package.json").version)
|
||||||
|
return wrapper.start()
|
||||||
}
|
}
|
||||||
|
|
||||||
entry()
|
entry().catch((error) => {
|
||||||
|
logger.error(error.message)
|
||||||
|
ipcMain.exit(error)
|
||||||
|
})
|
||||||
|
|||||||
@@ -289,7 +289,7 @@ export abstract class HttpProvider {
|
|||||||
/**
|
/**
|
||||||
* Helper to error if not authorized.
|
* Helper to error if not authorized.
|
||||||
*/
|
*/
|
||||||
protected ensureAuthenticated(request: http.IncomingMessage): void {
|
public ensureAuthenticated(request: http.IncomingMessage): void {
|
||||||
if (!this.authenticated(request)) {
|
if (!this.authenticated(request)) {
|
||||||
throw new HttpError("Unauthorized", HttpCode.Unauthorized)
|
throw new HttpError("Unauthorized", HttpCode.Unauthorized)
|
||||||
}
|
}
|
||||||
@@ -396,23 +396,26 @@ export abstract class HttpProvider {
|
|||||||
export class Heart {
|
export class Heart {
|
||||||
private heartbeatTimer?: NodeJS.Timeout
|
private heartbeatTimer?: NodeJS.Timeout
|
||||||
private heartbeatInterval = 60000
|
private heartbeatInterval = 60000
|
||||||
private lastHeartbeat = 0
|
public lastHeartbeat = 0
|
||||||
|
|
||||||
public constructor(private readonly heartbeatPath: string, private readonly isActive: () => Promise<boolean>) {}
|
public constructor(private readonly heartbeatPath: string, private readonly isActive: () => Promise<boolean>) {}
|
||||||
|
|
||||||
|
public alive(): boolean {
|
||||||
|
const now = Date.now()
|
||||||
|
return now - this.lastHeartbeat < this.heartbeatInterval
|
||||||
|
}
|
||||||
/**
|
/**
|
||||||
* Write to the heartbeat file if we haven't already done so within the
|
* Write to the heartbeat file if we haven't already done so within the
|
||||||
* timeout and start or reset a timer that keeps running as long as there is
|
* timeout and start or reset a timer that keeps running as long as there is
|
||||||
* activity. Failures are logged as warnings.
|
* activity. Failures are logged as warnings.
|
||||||
*/
|
*/
|
||||||
public beat(): void {
|
public beat(): void {
|
||||||
const now = Date.now()
|
if (!this.alive()) {
|
||||||
if (now - this.lastHeartbeat >= this.heartbeatInterval) {
|
|
||||||
logger.trace("heartbeat")
|
logger.trace("heartbeat")
|
||||||
fs.outputFile(this.heartbeatPath, "").catch((error) => {
|
fs.outputFile(this.heartbeatPath, "").catch((error) => {
|
||||||
logger.warn(error.message)
|
logger.warn(error.message)
|
||||||
})
|
})
|
||||||
this.lastHeartbeat = now
|
this.lastHeartbeat = Date.now()
|
||||||
if (typeof this.heartbeatTimer !== "undefined") {
|
if (typeof this.heartbeatTimer !== "undefined") {
|
||||||
clearTimeout(this.heartbeatTimer)
|
clearTimeout(this.heartbeatTimer)
|
||||||
}
|
}
|
||||||
@@ -457,7 +460,7 @@ export class HttpServer {
|
|||||||
private listenPromise: Promise<string | null> | undefined
|
private listenPromise: Promise<string | null> | undefined
|
||||||
public readonly protocol: "http" | "https"
|
public readonly protocol: "http" | "https"
|
||||||
private readonly providers = new Map<string, HttpProvider>()
|
private readonly providers = new Map<string, HttpProvider>()
|
||||||
private readonly heart: Heart
|
public readonly heart: Heart
|
||||||
private readonly socketProvider = new SocketProxyProvider()
|
private readonly socketProvider = new SocketProxyProvider()
|
||||||
|
|
||||||
/**
|
/**
|
||||||
@@ -575,14 +578,24 @@ export class HttpServer {
|
|||||||
*/
|
*/
|
||||||
public listen(): Promise<string | null> {
|
public listen(): Promise<string | null> {
|
||||||
if (!this.listenPromise) {
|
if (!this.listenPromise) {
|
||||||
this.listenPromise = new Promise((resolve, reject) => {
|
this.listenPromise = new Promise(async (resolve, reject) => {
|
||||||
this.server.on("error", reject)
|
this.server.on("error", reject)
|
||||||
this.server.on("upgrade", this.onUpgrade)
|
this.server.on("upgrade", this.onUpgrade)
|
||||||
const onListen = (): void => resolve(this.address())
|
const onListen = (): void => resolve(this.address())
|
||||||
if (this.options.socket) {
|
if (this.options.socket) {
|
||||||
|
try {
|
||||||
|
await fs.unlink(this.options.socket)
|
||||||
|
} catch (err) {
|
||||||
|
if (err.code !== "ENOENT") {
|
||||||
|
logger.warn(err.message)
|
||||||
|
}
|
||||||
|
}
|
||||||
this.server.listen(this.options.socket, onListen)
|
this.server.listen(this.options.socket, onListen)
|
||||||
|
} else if (this.options.host) {
|
||||||
|
// [] is the correct format when using :: but Node errors with them.
|
||||||
|
this.server.listen(this.options.port, this.options.host.replace(/^\[|\]$/g, ""), onListen)
|
||||||
} else {
|
} else {
|
||||||
this.server.listen(this.options.port, this.options.host, onListen)
|
this.server.listen(this.options.port, onListen)
|
||||||
}
|
}
|
||||||
})
|
})
|
||||||
}
|
}
|
||||||
@@ -602,8 +615,10 @@ export class HttpServer {
|
|||||||
}
|
}
|
||||||
|
|
||||||
private onRequest = async (request: http.IncomingMessage, response: http.ServerResponse): Promise<void> => {
|
private onRequest = async (request: http.IncomingMessage, response: http.ServerResponse): Promise<void> => {
|
||||||
this.heart.beat()
|
|
||||||
const route = this.parseUrl(request)
|
const route = this.parseUrl(request)
|
||||||
|
if (route.providerBase !== "/healthz") {
|
||||||
|
this.heart.beat()
|
||||||
|
}
|
||||||
const write = (payload: HttpResponse): void => {
|
const write = (payload: HttpResponse): void => {
|
||||||
response.writeHead(payload.redirect ? HttpCode.Redirect : payload.code || HttpCode.Ok, {
|
response.writeHead(payload.redirect ? HttpCode.Redirect : payload.code || HttpCode.Ok, {
|
||||||
"Content-Type": payload.mime || getMediaMime(payload.filePath),
|
"Content-Type": payload.mime || getMediaMime(payload.filePath),
|
||||||
@@ -642,10 +657,7 @@ export class HttpServer {
|
|||||||
}
|
}
|
||||||
|
|
||||||
try {
|
try {
|
||||||
const payload =
|
const payload = (await this.handleRequest(route, request)) || (await route.provider.handleRequest(route, request))
|
||||||
this.maybeRedirect(request, route) ||
|
|
||||||
(route.provider.authenticated(request) && this.maybeProxy(request)) ||
|
|
||||||
(await route.provider.handleRequest(route, request))
|
|
||||||
if (payload.proxy) {
|
if (payload.proxy) {
|
||||||
this.doProxy(route, request, response, payload.proxy)
|
this.doProxy(route, request, response, payload.proxy)
|
||||||
} else {
|
} else {
|
||||||
@@ -680,15 +692,23 @@ export class HttpServer {
|
|||||||
}
|
}
|
||||||
|
|
||||||
/**
|
/**
|
||||||
* Return any necessary redirection before delegating to a provider.
|
* Handle requests that are always in effect no matter what provider is
|
||||||
|
* registered at the route.
|
||||||
*/
|
*/
|
||||||
private maybeRedirect(request: http.IncomingMessage, route: ProviderRoute): RedirectResponse | undefined {
|
private async handleRequest(route: ProviderRoute, request: http.IncomingMessage): Promise<HttpResponse | undefined> {
|
||||||
// If we're handling TLS ensure all requests are redirected to HTTPS.
|
// If we're handling TLS ensure all requests are redirected to HTTPS.
|
||||||
if (this.options.cert && !(request.connection as tls.TLSSocket).encrypted) {
|
if (this.options.cert && !(request.connection as tls.TLSSocket).encrypted) {
|
||||||
return { redirect: route.fullPath }
|
return { redirect: route.fullPath }
|
||||||
}
|
}
|
||||||
|
|
||||||
return undefined
|
// Return robots.txt.
|
||||||
|
if (route.fullPath === "/robots.txt") {
|
||||||
|
const filePath = path.resolve(__dirname, "../../src/browser/robots.txt")
|
||||||
|
return { content: await fs.readFile(filePath), filePath }
|
||||||
|
}
|
||||||
|
|
||||||
|
// Handle proxy domains.
|
||||||
|
return this.maybeProxy(route, request)
|
||||||
}
|
}
|
||||||
|
|
||||||
/**
|
/**
|
||||||
@@ -718,6 +738,8 @@ export class HttpServer {
|
|||||||
}
|
}
|
||||||
|
|
||||||
private onUpgrade = async (request: http.IncomingMessage, socket: net.Socket, head: Buffer): Promise<void> => {
|
private onUpgrade = async (request: http.IncomingMessage, socket: net.Socket, head: Buffer): Promise<void> => {
|
||||||
|
socket.pause()
|
||||||
|
|
||||||
try {
|
try {
|
||||||
this.heart.beat()
|
this.heart.beat()
|
||||||
socket.on("error", () => socket.destroy())
|
socket.on("error", () => socket.destroy())
|
||||||
@@ -739,7 +761,7 @@ export class HttpServer {
|
|||||||
// can't be transferred so we need an in-between).
|
// can't be transferred so we need an in-between).
|
||||||
const socketProxy = await this.socketProvider.createProxy(socket)
|
const socketProxy = await this.socketProvider.createProxy(socket)
|
||||||
const payload =
|
const payload =
|
||||||
this.maybeProxy(request) || (await route.provider.handleWebSocket(route, request, socketProxy, head))
|
this.maybeProxy(route, request) || (await route.provider.handleWebSocket(route, request, socketProxy, head))
|
||||||
if (payload && payload.proxy) {
|
if (payload && payload.proxy) {
|
||||||
this.doProxy(route, request, { socket: socketProxy, head }, payload.proxy)
|
this.doProxy(route, request, { socket: socketProxy, head }, payload.proxy)
|
||||||
}
|
}
|
||||||
@@ -889,8 +911,10 @@ export class HttpServer {
|
|||||||
*
|
*
|
||||||
* For example if `coder.com` is specified `8080.coder.com` will be proxied
|
* For example if `coder.com` is specified `8080.coder.com` will be proxied
|
||||||
* but `8080.test.coder.com` and `test.8080.coder.com` will not.
|
* but `8080.test.coder.com` and `test.8080.coder.com` will not.
|
||||||
|
*
|
||||||
|
* Throw an error if proxying but the user isn't authenticated.
|
||||||
*/
|
*/
|
||||||
public maybeProxy(request: http.IncomingMessage): HttpResponse | undefined {
|
public maybeProxy(route: ProviderRoute, request: http.IncomingMessage): HttpResponse | undefined {
|
||||||
// Split into parts.
|
// Split into parts.
|
||||||
const host = request.headers.host || ""
|
const host = request.headers.host || ""
|
||||||
const idx = host.indexOf(":")
|
const idx = host.indexOf(":")
|
||||||
@@ -904,6 +928,9 @@ export class HttpServer {
|
|||||||
return undefined
|
return undefined
|
||||||
}
|
}
|
||||||
|
|
||||||
|
// Must be authenticated to use the proxy.
|
||||||
|
route.provider.ensureAuthenticated(request)
|
||||||
|
|
||||||
return {
|
return {
|
||||||
proxy: {
|
proxy: {
|
||||||
port,
|
port,
|
||||||
|
|||||||
@@ -4,11 +4,15 @@ import * as path from "path"
|
|||||||
import * as util from "util"
|
import * as util from "util"
|
||||||
import { Args } from "./cli"
|
import { Args } from "./cli"
|
||||||
import { HttpServer } from "./http"
|
import { HttpServer } from "./http"
|
||||||
|
import { paths } from "./util"
|
||||||
|
|
||||||
/* eslint-disable @typescript-eslint/no-var-requires */
|
/* eslint-disable @typescript-eslint/no-var-requires */
|
||||||
|
|
||||||
export type Activate = (httpServer: HttpServer, args: Args) => void
|
export type Activate = (httpServer: HttpServer, args: Args) => void
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Plugins must implement this interface.
|
||||||
|
*/
|
||||||
export interface Plugin {
|
export interface Plugin {
|
||||||
activate: Activate
|
activate: Activate
|
||||||
}
|
}
|
||||||
@@ -23,38 +27,66 @@ require("module")._load = function (request: string, parent: object, isMain: boo
|
|||||||
return originalLoad.apply(this, [request.replace(/^code-server/, path.resolve(__dirname, "../..")), parent, isMain])
|
return originalLoad.apply(this, [request.replace(/^code-server/, path.resolve(__dirname, "../..")), parent, isMain])
|
||||||
}
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Load a plugin and run its activation function.
|
||||||
|
*/
|
||||||
const loadPlugin = async (pluginPath: string, httpServer: HttpServer, args: Args): Promise<void> => {
|
const loadPlugin = async (pluginPath: string, httpServer: HttpServer, args: Args): Promise<void> => {
|
||||||
try {
|
try {
|
||||||
const plugin: Plugin = require(pluginPath)
|
const plugin: Plugin = require(pluginPath)
|
||||||
plugin.activate(httpServer, args)
|
plugin.activate(httpServer, args)
|
||||||
logger.debug("Loaded plugin", field("name", path.basename(pluginPath)))
|
|
||||||
|
const packageJson = require(path.join(pluginPath, "package.json"))
|
||||||
|
logger.debug(
|
||||||
|
"Loaded plugin",
|
||||||
|
field("name", packageJson.name || path.basename(pluginPath)),
|
||||||
|
field("path", pluginPath),
|
||||||
|
field("version", packageJson.version || "n/a"),
|
||||||
|
)
|
||||||
} catch (error) {
|
} catch (error) {
|
||||||
if (error.code !== "MODULE_NOT_FOUND") {
|
logger.error(error.message)
|
||||||
logger.warn(error.message)
|
|
||||||
} else {
|
|
||||||
logger.error(error.message)
|
|
||||||
}
|
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
const _loadPlugins = async (httpServer: HttpServer, args: Args): Promise<void> => {
|
/**
|
||||||
const pluginPath = path.resolve(__dirname, "../../plugins")
|
* Load all plugins in the specified directory.
|
||||||
const files = await util.promisify(fs.readdir)(pluginPath, {
|
*/
|
||||||
withFileTypes: true,
|
const _loadPlugins = async (pluginDir: string, httpServer: HttpServer, args: Args): Promise<void> => {
|
||||||
})
|
|
||||||
await Promise.all(files.map((file) => loadPlugin(path.join(pluginPath, file.name), httpServer, args)))
|
|
||||||
}
|
|
||||||
|
|
||||||
export const loadPlugins = async (httpServer: HttpServer, args: Args): Promise<void> => {
|
|
||||||
try {
|
try {
|
||||||
await _loadPlugins(httpServer, args)
|
const files = await util.promisify(fs.readdir)(pluginDir, {
|
||||||
|
withFileTypes: true,
|
||||||
|
})
|
||||||
|
await Promise.all(files.map((file) => loadPlugin(path.join(pluginDir, file.name), httpServer, args)))
|
||||||
} catch (error) {
|
} catch (error) {
|
||||||
if (error.code !== "ENOENT") {
|
if (error.code !== "ENOENT") {
|
||||||
logger.warn(error.message)
|
logger.warn(error.message)
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
}
|
||||||
if (process.env.PLUGIN_DIR) {
|
|
||||||
await loadPlugin(process.env.PLUGIN_DIR, httpServer, args)
|
/**
|
||||||
}
|
* Load all plugins from the `plugins` directory, directories specified by
|
||||||
|
* `CS_PLUGIN_PATH` (colon-separated), and individual plugins specified by
|
||||||
|
* `CS_PLUGIN` (also colon-separated).
|
||||||
|
*/
|
||||||
|
export const loadPlugins = async (httpServer: HttpServer, args: Args): Promise<void> => {
|
||||||
|
const pluginPath = process.env.CS_PLUGIN_PATH || `${path.join(paths.data, "plugins")}:/usr/share/code-server/plugins`
|
||||||
|
const plugin = process.env.CS_PLUGIN || ""
|
||||||
|
await Promise.all([
|
||||||
|
// Built-in plugins.
|
||||||
|
_loadPlugins(path.resolve(__dirname, "../../plugins"), httpServer, args),
|
||||||
|
// User-added plugins.
|
||||||
|
...pluginPath
|
||||||
|
.split(":")
|
||||||
|
.filter((p) => !!p)
|
||||||
|
.map((dir) => _loadPlugins(path.resolve(dir), httpServer, args)),
|
||||||
|
// Individual plugins so you don't have to symlink or move them into a
|
||||||
|
// directory specifically for plugins. This lets you load plugins that are
|
||||||
|
// on the same level as other directories that are not plugins (if you tried
|
||||||
|
// to use CS_PLUGIN_PATH code-server would try to load those other
|
||||||
|
// directories as plugins). Intended for development.
|
||||||
|
...plugin
|
||||||
|
.split(":")
|
||||||
|
.filter((p) => !!p)
|
||||||
|
.map((dir) => loadPlugin(path.resolve(dir), httpServer, args)),
|
||||||
|
])
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -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) {
|
||||||
|
|||||||
@@ -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"
|
||||||
@@ -246,3 +247,17 @@ 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)
|
||||||
|
})
|
||||||
|
})
|
||||||
|
}
|
||||||
|
|||||||
@@ -32,19 +32,13 @@ export class IpcMain {
|
|||||||
public readonly onMessage = this._onMessage.event
|
public readonly onMessage = this._onMessage.event
|
||||||
private readonly _onDispose = new Emitter<NodeJS.Signals | undefined>()
|
private readonly _onDispose = new Emitter<NodeJS.Signals | undefined>()
|
||||||
public readonly onDispose = this._onDispose.event
|
public readonly onDispose = this._onDispose.event
|
||||||
public readonly processExit: (code?: number) => never
|
public readonly processExit: (code?: number) => never = process.exit
|
||||||
|
|
||||||
public constructor(public readonly parentPid?: number) {
|
public constructor(private readonly parentPid?: number) {
|
||||||
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.processExit = process.exit
|
|
||||||
process.exit = function (code?: number) {
|
|
||||||
logger.warn(`process.exit() was prevented: ${code || "unknown code"}.`)
|
|
||||||
} as (code?: number) => never
|
|
||||||
|
|
||||||
this.onDispose((signal) => {
|
this.onDispose((signal) => {
|
||||||
// Remove listeners to avoid possibly triggering disposal again.
|
// Remove listeners to avoid possibly triggering disposal again.
|
||||||
process.removeAllListeners()
|
process.removeAllListeners()
|
||||||
@@ -71,6 +65,19 @@ export class IpcMain {
|
|||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Ensure we control when the process exits.
|
||||||
|
*/
|
||||||
|
public preventExit(): void {
|
||||||
|
process.exit = function (code?: number) {
|
||||||
|
logger.warn(`process.exit() was prevented: ${code || "unknown code"}.`)
|
||||||
|
} as (code?: number) => never
|
||||||
|
}
|
||||||
|
|
||||||
|
public get isChild(): boolean {
|
||||||
|
return typeof this.parentPid !== "undefined"
|
||||||
|
}
|
||||||
|
|
||||||
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)
|
||||||
@@ -127,17 +134,12 @@ export class IpcMain {
|
|||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
let _ipcMain: IpcMain
|
/**
|
||||||
export const ipcMain = (): IpcMain => {
|
* Channel for communication between the child and parent processes.
|
||||||
if (!_ipcMain) {
|
*/
|
||||||
_ipcMain = new IpcMain(
|
export const ipcMain = new IpcMain(
|
||||||
typeof process.env.CODE_SERVER_PARENT_PID !== "undefined"
|
typeof process.env.CODE_SERVER_PARENT_PID !== "undefined" ? parseInt(process.env.CODE_SERVER_PARENT_PID) : undefined,
|
||||||
? parseInt(process.env.CODE_SERVER_PARENT_PID)
|
)
|
||||||
: undefined,
|
|
||||||
)
|
|
||||||
}
|
|
||||||
return _ipcMain
|
|
||||||
}
|
|
||||||
|
|
||||||
export interface WrapperOptions {
|
export interface WrapperOptions {
|
||||||
maxMemory?: number
|
maxMemory?: number
|
||||||
@@ -162,14 +164,11 @@ 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(() => {
|
ipcMain.onDispose(() => {
|
||||||
if (this.process) {
|
this.disposeChild()
|
||||||
this.process.removeAllListeners()
|
|
||||||
this.process.kill()
|
|
||||||
}
|
|
||||||
})
|
})
|
||||||
|
|
||||||
ipcMain().onMessage((message) => {
|
ipcMain.onMessage((message) => {
|
||||||
switch (message.type) {
|
switch (message.type) {
|
||||||
case "relaunch":
|
case "relaunch":
|
||||||
logger.info(`Relaunching: ${this.currentVersion} -> ${message.version}`)
|
logger.info(`Relaunching: ${this.currentVersion} -> ${message.version}`)
|
||||||
@@ -181,55 +180,65 @@ export class WrapperProcess {
|
|||||||
break
|
break
|
||||||
}
|
}
|
||||||
})
|
})
|
||||||
|
|
||||||
process.on("SIGUSR1", async () => {
|
|
||||||
logger.info("Received SIGUSR1; hotswapping")
|
|
||||||
this.relaunch()
|
|
||||||
})
|
|
||||||
}
|
}
|
||||||
|
|
||||||
private async relaunch(): Promise<void> {
|
private disposeChild(): void {
|
||||||
this.started = undefined
|
this.started = undefined
|
||||||
if (this.process) {
|
if (this.process) {
|
||||||
this.process.removeAllListeners()
|
this.process.removeAllListeners()
|
||||||
this.process.kill()
|
this.process.kill()
|
||||||
}
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
private async relaunch(): Promise<void> {
|
||||||
|
this.disposeChild()
|
||||||
try {
|
try {
|
||||||
await this.start()
|
await this.start()
|
||||||
} catch (error) {
|
} catch (error) {
|
||||||
logger.error(error.message)
|
logger.error(error.message)
|
||||||
ipcMain().exit(typeof error.code === "number" ? error.code : 1)
|
ipcMain.exit(typeof error.code === "number" ? error.code : 1)
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
public start(): Promise<void> {
|
public start(): Promise<void> {
|
||||||
if (!this.started) {
|
// If we have a process then we've already bound this.
|
||||||
this.started = this.spawn().then((child) => {
|
if (!this.process) {
|
||||||
// Log both to stdout and to the log directory.
|
process.on("SIGUSR1", async () => {
|
||||||
if (child.stdout) {
|
logger.info("Received SIGUSR1; hotswapping")
|
||||||
child.stdout.pipe(this.logStdoutStream)
|
this.relaunch()
|
||||||
child.stdout.pipe(process.stdout)
|
|
||||||
}
|
|
||||||
if (child.stderr) {
|
|
||||||
child.stderr.pipe(this.logStderrStream)
|
|
||||||
child.stderr.pipe(process.stderr)
|
|
||||||
}
|
|
||||||
logger.debug(`spawned inner process ${child.pid}`)
|
|
||||||
ipcMain()
|
|
||||||
.handshake(child)
|
|
||||||
.then(() => {
|
|
||||||
child.once("exit", (code) => {
|
|
||||||
logger.debug(`inner process ${child.pid} exited unexpectedly`)
|
|
||||||
ipcMain().exit(code || 0)
|
|
||||||
})
|
|
||||||
})
|
|
||||||
this.process = child
|
|
||||||
})
|
})
|
||||||
}
|
}
|
||||||
|
if (!this.started) {
|
||||||
|
this.started = this._start()
|
||||||
|
}
|
||||||
return this.started
|
return this.started
|
||||||
}
|
}
|
||||||
|
|
||||||
private async spawn(): Promise<cp.ChildProcess> {
|
private async _start(): Promise<void> {
|
||||||
|
const child = this.spawn()
|
||||||
|
this.process = child
|
||||||
|
|
||||||
|
// Log both to stdout and to the log directory.
|
||||||
|
if (child.stdout) {
|
||||||
|
child.stdout.pipe(this.logStdoutStream)
|
||||||
|
child.stdout.pipe(process.stdout)
|
||||||
|
}
|
||||||
|
if (child.stderr) {
|
||||||
|
child.stderr.pipe(this.logStderrStream)
|
||||||
|
child.stderr.pipe(process.stderr)
|
||||||
|
}
|
||||||
|
|
||||||
|
logger.debug(`spawned inner process ${child.pid}`)
|
||||||
|
|
||||||
|
await ipcMain.handshake(child)
|
||||||
|
|
||||||
|
child.once("exit", (code) => {
|
||||||
|
logger.debug(`inner process ${child.pid} exited unexpectedly`)
|
||||||
|
ipcMain.exit(code || 0)
|
||||||
|
})
|
||||||
|
}
|
||||||
|
|
||||||
|
private spawn(): cp.ChildProcess {
|
||||||
// Flags to pass along to the Node binary.
|
// Flags to pass along to the Node binary.
|
||||||
let nodeOptions = `${process.env.NODE_OPTIONS || ""} ${(this.options && this.options.nodeOptions) || ""}`
|
let nodeOptions = `${process.env.NODE_OPTIONS || ""} ${(this.options && this.options.nodeOptions) || ""}`
|
||||||
if (!/max_old_space_size=(\d+)/g.exec(nodeOptions)) {
|
if (!/max_old_space_size=(\d+)/g.exec(nodeOptions)) {
|
||||||
@@ -251,23 +260,13 @@ export class WrapperProcess {
|
|||||||
// 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", () => ipcMain.exit())
|
||||||
}
|
}
|
||||||
|
|
||||||
export const wrap = (fn: () => Promise<void>): void => {
|
// Don't let uncaught exceptions crash the process.
|
||||||
if (ipcMain().parentPid) {
|
process.on("uncaughtException", (error) => {
|
||||||
ipcMain()
|
logger.error(`Uncaught exception: ${error.message}`)
|
||||||
.handshake()
|
if (typeof error.stack !== "undefined") {
|
||||||
.then(() => fn())
|
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)
|
|
||||||
})
|
|
||||||
}
|
}
|
||||||
}
|
})
|
||||||
|
|||||||
152
test/cli.test.ts
152
test/cli.test.ts
@@ -1,20 +1,31 @@
|
|||||||
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
|
||||||
})
|
})
|
||||||
|
|
||||||
// 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 = {
|
||||||
_: [],
|
"extensions-dir": path.join(paths.data, "extensions"),
|
||||||
|
"user-data-dir": paths.data,
|
||||||
}
|
}
|
||||||
|
|
||||||
it("should set defaults", () => {
|
it("should set defaults", () => {
|
||||||
assert.deepEqual(parse([]), defaults)
|
assert.deepEqual(parse([]), { _: [] })
|
||||||
})
|
})
|
||||||
|
|
||||||
it("should parse all available options", () => {
|
it("should parse all available options", () => {
|
||||||
@@ -69,7 +80,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 +94,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 +115,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 +126,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 +143,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 +170,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 +198,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 +209,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 +217,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 +224,85 @@ 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"],
|
||||||
})
|
})
|
||||||
})
|
})
|
||||||
})
|
})
|
||||||
|
|
||||||
|
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)
|
||||||
|
})
|
||||||
|
})
|
||||||
|
|||||||
@@ -8,6 +8,7 @@ import { SettingsProvider, UpdateSettings } from "../src/node/settings"
|
|||||||
import { tmpdir } from "../src/node/util"
|
import { tmpdir } from "../src/node/util"
|
||||||
|
|
||||||
describe("update", () => {
|
describe("update", () => {
|
||||||
|
return
|
||||||
let version = "1.0.0"
|
let version = "1.0.0"
|
||||||
let spy: string[] = []
|
let spy: string[] = []
|
||||||
const server = http.createServer((request: http.IncomingMessage, response: http.ServerResponse) => {
|
const server = http.createServer((request: http.IncomingMessage, response: http.ServerResponse) => {
|
||||||
|
|||||||
16
yarn.lock
16
yarn.lock
@@ -1107,6 +1107,13 @@
|
|||||||
resolved "https://registry.yarnpkg.com/@types/semver/-/semver-7.3.3.tgz#3ad6ed949e7487e7bda6f886b4a2434a2c3d7b1a"
|
resolved "https://registry.yarnpkg.com/@types/semver/-/semver-7.3.3.tgz#3ad6ed949e7487e7bda6f886b4a2434a2c3d7b1a"
|
||||||
integrity sha512-jQxClWFzv9IXdLdhSaTf16XI3NYe6zrEbckSpb5xhKfPbWgIyAY0AFyWWWfaiDcBuj3UHmMkCIwSRqpKMTZL2Q==
|
integrity sha512-jQxClWFzv9IXdLdhSaTf16XI3NYe6zrEbckSpb5xhKfPbWgIyAY0AFyWWWfaiDcBuj3UHmMkCIwSRqpKMTZL2Q==
|
||||||
|
|
||||||
|
"@types/split2@^2.1.6":
|
||||||
|
version "2.1.6"
|
||||||
|
resolved "https://registry.yarnpkg.com/@types/split2/-/split2-2.1.6.tgz#b095c9e064853824b22c67993d99b066777402b1"
|
||||||
|
integrity sha512-ddaFSOMuy2Rp97l6q/LEteQygvTQJuEZ+SRhxFKR0uXGsdbFDqX/QF2xoGcOqLQ8XV91v01SnAv2vpgihNgW/Q==
|
||||||
|
dependencies:
|
||||||
|
"@types/node" "*"
|
||||||
|
|
||||||
"@types/tar-fs@^2.0.0":
|
"@types/tar-fs@^2.0.0":
|
||||||
version "2.0.0"
|
version "2.0.0"
|
||||||
resolved "https://registry.yarnpkg.com/@types/tar-fs/-/tar-fs-2.0.0.tgz#db94cb4ea1cccecafe3d1a53812807efb4bbdbc1"
|
resolved "https://registry.yarnpkg.com/@types/tar-fs/-/tar-fs-2.0.0.tgz#db94cb4ea1cccecafe3d1a53812807efb4bbdbc1"
|
||||||
@@ -5996,7 +6003,7 @@ readable-stream@^2.0.2, readable-stream@^2.2.2, readable-stream@^2.3.3, readable
|
|||||||
string_decoder "~1.1.1"
|
string_decoder "~1.1.1"
|
||||||
util-deprecate "~1.0.1"
|
util-deprecate "~1.0.1"
|
||||||
|
|
||||||
readable-stream@^3.1.1, readable-stream@^3.4.0, readable-stream@^3.6.0:
|
readable-stream@^3.0.0, readable-stream@^3.1.1, readable-stream@^3.4.0, readable-stream@^3.6.0:
|
||||||
version "3.6.0"
|
version "3.6.0"
|
||||||
resolved "https://registry.yarnpkg.com/readable-stream/-/readable-stream-3.6.0.tgz#337bbda3adc0706bd3e024426a286d4b4b2c9198"
|
resolved "https://registry.yarnpkg.com/readable-stream/-/readable-stream-3.6.0.tgz#337bbda3adc0706bd3e024426a286d4b4b2c9198"
|
||||||
integrity sha512-BViHy7LKeTz4oNnkcLJ+lVSL6vpiFeX6/d3oSH8zCW7UxP2onchk+vTGB143xuFjHS3deTgkKoXXymXqymiIdA==
|
integrity sha512-BViHy7LKeTz4oNnkcLJ+lVSL6vpiFeX6/d3oSH8zCW7UxP2onchk+vTGB143xuFjHS3deTgkKoXXymXqymiIdA==
|
||||||
@@ -6621,6 +6628,13 @@ split-string@^3.0.1, split-string@^3.0.2:
|
|||||||
dependencies:
|
dependencies:
|
||||||
extend-shallow "^3.0.0"
|
extend-shallow "^3.0.0"
|
||||||
|
|
||||||
|
split2@^3.2.2:
|
||||||
|
version "3.2.2"
|
||||||
|
resolved "https://registry.yarnpkg.com/split2/-/split2-3.2.2.tgz#bf2cf2a37d838312c249c89206fd7a17dd12365f"
|
||||||
|
integrity sha512-9NThjpgZnifTkJpzTZ7Eue85S49QwpNhZTq6GRJwObb6jnLFNGB7Qm73V5HewTROPyxD0C29xqmaI68bQtV+hg==
|
||||||
|
dependencies:
|
||||||
|
readable-stream "^3.0.0"
|
||||||
|
|
||||||
sprintf-js@~1.0.2:
|
sprintf-js@~1.0.2:
|
||||||
version "1.0.3"
|
version "1.0.3"
|
||||||
resolved "https://registry.yarnpkg.com/sprintf-js/-/sprintf-js-1.0.3.tgz#04e6926f662895354f3dd015203633b857297e2c"
|
resolved "https://registry.yarnpkg.com/sprintf-js/-/sprintf-js-1.0.3.tgz#04e6926f662895354f3dd015203633b857297e2c"
|
||||||
|
|||||||
Reference in New Issue
Block a user