mirror of
https://github.com/coder/code-server.git
synced 2026-04-15 09:53:55 -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
|
||||
import/order:
|
||||
[error, { alphabetize: { order: "asc" }, groups: [["builtin", "external", "internal"], "parent", "sibling"] }]
|
||||
no-async-promise-executor: off
|
||||
|
||||
settings:
|
||||
# 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:
|
||||
- uses: actions/checkout@v1
|
||||
- name: Run ./ci/steps/fmt.sh
|
||||
uses: ./ci/images/debian8
|
||||
uses: ./ci/images/debian10
|
||||
with:
|
||||
args: ./ci/steps/fmt.sh
|
||||
|
||||
@@ -17,7 +17,7 @@ jobs:
|
||||
steps:
|
||||
- uses: actions/checkout@v1
|
||||
- name: Run ./ci/steps/lint.sh
|
||||
uses: ./ci/images/debian8
|
||||
uses: ./ci/images/debian10
|
||||
with:
|
||||
args: ./ci/steps/lint.sh
|
||||
|
||||
@@ -26,7 +26,7 @@ jobs:
|
||||
steps:
|
||||
- uses: actions/checkout@v1
|
||||
- name: Run ./ci/steps/test.sh
|
||||
uses: ./ci/images/debian8
|
||||
uses: ./ci/images/debian10
|
||||
with:
|
||||
args: ./ci/steps/test.sh
|
||||
|
||||
@@ -35,7 +35,7 @@ jobs:
|
||||
steps:
|
||||
- uses: actions/checkout@v1
|
||||
- name: Run ./ci/steps/release.sh
|
||||
uses: ./ci/images/debian8
|
||||
uses: ./ci/images/debian10
|
||||
with:
|
||||
args: ./ci/steps/release.sh
|
||||
- name: Upload npm package artifact
|
||||
@@ -116,7 +116,7 @@ jobs:
|
||||
name: release-packages
|
||||
path: ./release-packages
|
||||
- name: Run ./ci/steps/build-docker-image.sh
|
||||
uses: ./ci/images/debian8
|
||||
uses: ./ci/images/debian10
|
||||
with:
|
||||
args: ./ci/steps/build-docker-image.sh
|
||||
- name: Upload release image
|
||||
|
||||
4
.github/workflows/publish.yaml
vendored
4
.github/workflows/publish.yaml
vendored
@@ -10,7 +10,7 @@ jobs:
|
||||
steps:
|
||||
- uses: actions/checkout@v1
|
||||
- name: Run ./ci/steps/publish-npm.sh
|
||||
uses: ./ci/images/debian8
|
||||
uses: ./ci/images/debian10
|
||||
with:
|
||||
args: ./ci/steps/publish-npm.sh
|
||||
env:
|
||||
@@ -22,7 +22,7 @@ jobs:
|
||||
steps:
|
||||
- uses: actions/checkout@v1
|
||||
- name: Run ./ci/steps/push-docker-manifest.sh
|
||||
uses: ./ci/images/debian8
|
||||
uses: ./ci/images/debian10
|
||||
with:
|
||||
args: ./ci/steps/push-docker-manifest.sh
|
||||
env:
|
||||
|
||||
2
.gitignore
vendored
2
.gitignore
vendored
@@ -11,3 +11,5 @@ release-images/
|
||||
node_modules
|
||||
node-*
|
||||
/plugins
|
||||
/lib/coder-cloud-agent
|
||||
.home
|
||||
|
||||
1
.gitmodules
vendored
1
.gitmodules
vendored
@@ -1,3 +1,4 @@
|
||||
[submodule "lib/vscode"]
|
||||
path = lib/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.
|
||||
|
||||
@@ -6,62 +6,58 @@ Run [VS Code](https://github.com/Microsoft/vscode) on any machine anywhere and a
|
||||
|
||||
## Highlights
|
||||
|
||||
- **Code everywhere**
|
||||
- Code on your Chromebook, tablet, and laptop with a consistent development environment.
|
||||
- Develop on a Linux machine and pick up from any device with a web browser.
|
||||
- **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.
|
||||
- Code on any device with a consistent development environment
|
||||
- Use cloud servers to speed up tests, compilations, downloads, and more
|
||||
- Preserve battery life when you're on the go; all intensive tasks run on your server
|
||||
|
||||
## 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.
|
||||
|
||||
It tries to use the system package manager if possible.
|
||||
|
||||
First run to print out the install process:
|
||||
If you choose to use the install script, you can preview what occurs during the install process:
|
||||
|
||||
```bash
|
||||
curl -fsSL https://code-server.dev/install.sh | sh -s -- --dry-run
|
||||
```
|
||||
|
||||
Now to actually install:
|
||||
To install, run:
|
||||
|
||||
```bash
|
||||
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
|
||||
|
||||
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
|
||||
|
||||
We ([@cdr](https://github.com/cdr)) are looking for a engineers to help maintain
|
||||
code-server, innovate on open source and streamline dev workflows.
|
||||
We ([@cdr](https://github.com/cdr)) are looking for engineers to help [maintain
|
||||
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
|
||||
you're in North America or Europe.
|
||||
|
||||
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).
|
||||
Please get in [touch](mailto:jobs@coder.com) with your resume/GitHub if interested.
|
||||
|
||||
## 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`
|
||||
2. Update in [./doc/install.md](../doc/install.md)
|
||||
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
|
||||
the updated version.
|
||||
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.
|
||||
5. Run `yarn release:github-assets` to download the `release-packages` artifact and
|
||||
upload them to the draft release.
|
||||
5. Run `yarn release:github-assets` to download the `release-packages` artifact.
|
||||
- It will upload them to the draft release.
|
||||
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
|
||||
`hub` where uploading assets in step 5 will break the tag.
|
||||
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.
|
||||
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.
|
||||
12. Make sure to add a release without the `v` prefix for autoupdate from `3.2.0`.
|
||||
|
||||
## dev
|
||||
|
||||
|
||||
@@ -18,6 +18,12 @@ main() {
|
||||
chmod +x out/node/entry.js
|
||||
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 \
|
||||
--public-url "." \
|
||||
--out-dir dist \
|
||||
|
||||
@@ -11,15 +11,6 @@ main() {
|
||||
mkdir -p release-packages
|
||||
|
||||
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
|
||||
release_nfpm
|
||||
@@ -30,12 +21,6 @@ release_archive() {
|
||||
local release_name="code-server-$VERSION-$OS-$ARCH"
|
||||
if [[ $OS == "linux" ]]; then
|
||||
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
|
||||
tar -czf "release-packages/$release_name.tar.gz" -s "/^release-standalone/$release_name/" release-standalone
|
||||
fi
|
||||
|
||||
@@ -6,6 +6,10 @@ set -euo pipefail
|
||||
# MINIFY controls whether minified vscode is bundled.
|
||||
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() {
|
||||
cd "$(dirname "${0}")/../.."
|
||||
source ./ci/lib.sh
|
||||
@@ -37,6 +41,7 @@ bundle_code_server() {
|
||||
rsync src/browser/media/ "$RELEASE_PATH/src/browser/media"
|
||||
mkdir -p "$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
|
||||
jq --slurp '.[0] * .[1]' package.json <(
|
||||
@@ -51,15 +56,25 @@ EOF
|
||||
) > "$RELEASE_PATH/package.json"
|
||||
rsync yarn.lock "$RELEASE_PATH"
|
||||
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() {
|
||||
mkdir -p "$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"
|
||||
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/yarn.lock" "$VSCODE_OUT_PATH/extensions"
|
||||
rsync "$VSCODE_SRC_PATH/extensions/postinstall.js" "$VSCODE_OUT_PATH/extensions"
|
||||
|
||||
@@ -5,16 +5,7 @@ main() {
|
||||
cd "$(dirname "${0}")/../.."
|
||||
source ./ci/lib.sh
|
||||
|
||||
rm -rf \
|
||||
out \
|
||||
release \
|
||||
release-standalone \
|
||||
release-packages \
|
||||
release-gcp \
|
||||
release-images \
|
||||
dist \
|
||||
.cache \
|
||||
node-*
|
||||
git clean -Xffd
|
||||
|
||||
pushd lib/vscode
|
||||
git clean -xffd
|
||||
|
||||
@@ -12,7 +12,7 @@ homepage: "https://github.com/cdr/code-server"
|
||||
license: "MIT"
|
||||
files:
|
||||
./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
|
||||
# the user service. See #1997
|
||||
./ci/build/code-server-user.service: /usr/lib/systemd/user/code-server.service
|
||||
|
||||
@@ -24,6 +24,13 @@ main() {
|
||||
;;
|
||||
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
|
||||
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"
|
||||
@@ -36,6 +43,13 @@ vscode_yarn() {
|
||||
yarn --production --frozen-lockfile
|
||||
cd extensions
|
||||
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 "$@"
|
||||
|
||||
@@ -11,7 +11,7 @@ main() {
|
||||
source ./ci/lib.sh
|
||||
|
||||
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
|
||||
assets[$i]="--attach=${assets[$i]}"
|
||||
done
|
||||
|
||||
@@ -15,8 +15,7 @@ main() {
|
||||
./release-standalone/bin/code-server --extensions-dir "$EXTENSIONS_DIR" --install-extension ms-python.python
|
||||
local installed_extensions
|
||||
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
|
||||
ms-python.python" ]]; then
|
||||
if [[ $installed_extensions != "ms-python.python" ]]; then
|
||||
echo "Unexpected output from listing extensions:"
|
||||
echo "$installed_extensions"
|
||||
exit 1
|
||||
|
||||
@@ -6,7 +6,7 @@ main() {
|
||||
|
||||
cd ./lib/vscode
|
||||
git add -A
|
||||
git diff HEAD > ../../ci/dev/vscode.patch
|
||||
git diff HEAD --full-index > ../../ci/dev/vscode.patch
|
||||
}
|
||||
|
||||
main "$@"
|
||||
|
||||
@@ -4,14 +4,22 @@ set -euo pipefail
|
||||
main() {
|
||||
cd "$(dirname "$0")/../../.."
|
||||
source ./ci/lib.sh
|
||||
mkdir -p .home
|
||||
|
||||
docker run \
|
||||
-it \
|
||||
--rm \
|
||||
-v "$PWD:/src" \
|
||||
-e HOME="/src/.home" \
|
||||
-e USER="coder" \
|
||||
-e GITHUB_TOKEN \
|
||||
-e KEEP_MODULES \
|
||||
-e MINIFY \
|
||||
-w /src \
|
||||
-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")
|
||||
stylelint $(git ls-files "*.css")
|
||||
tsc --noEmit
|
||||
# See comment in ./ci/image/debian8
|
||||
if [[ ! ${CI-} ]]; then
|
||||
shellcheck -e SC2046,SC2164,SC2154,SC1091,SC1090,SC2002 $(git ls-files "*.sh")
|
||||
fi
|
||||
shellcheck -e SC2046,SC2164,SC2154,SC1091,SC1090,SC2002 $(git ls-files "*.sh")
|
||||
}
|
||||
|
||||
main "$@"
|
||||
|
||||
File diff suppressed because it is too large
Load Diff
@@ -1,6 +1,6 @@
|
||||
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/')" && \
|
||||
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"
|
||||
@@ -15,11 +15,16 @@ RUN npm config set python python2
|
||||
RUN yum install -y epel-release && yum install -y jq
|
||||
RUN yum install -y rsync
|
||||
|
||||
# Copied from ../debian8/Dockerfile
|
||||
# Install Go dependencies
|
||||
# Copied from ../debian10/Dockerfile
|
||||
# Install Go.
|
||||
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
|
||||
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
|
||||
RUN go get mvdan.cc/sh/v3/cmd/shfmt
|
||||
RUN go get github.com/goreleaser/nfpm/cmd/nfpm
|
||||
|
||||
@@ -1,4 +1,4 @@
|
||||
FROM debian:8
|
||||
FROM debian:10
|
||||
|
||||
RUN apt-get update
|
||||
|
||||
@@ -24,28 +24,23 @@ RUN apt-get install -y build-essential \
|
||||
RUN apt-get install -y gettext-base
|
||||
|
||||
# Misc build dependencies.
|
||||
RUN apt-get install -y git rsync unzip
|
||||
|
||||
# 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
|
||||
RUN apt-get install -y git rsync unzip jq
|
||||
|
||||
# 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 | \
|
||||
# tar -xJ && \
|
||||
# mv shellcheck*/shellcheck /usr/local/bin && \
|
||||
# rm -R shellcheck*
|
||||
RUN curl -fsSL https://github.com/koalaman/shellcheck/releases/download/v0.7.1/shellcheck-v0.7.1.linux.$(uname -m).tar.xz | \
|
||||
tar -xJ && \
|
||||
mv shellcheck*/shellcheck /usr/local/bin && \
|
||||
rm -R shellcheck*
|
||||
|
||||
# Install Go dependencies
|
||||
# Install Go.
|
||||
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
|
||||
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
|
||||
RUN go get mvdan.cc/sh/v3/cmd/shfmt
|
||||
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
|
||||
|
||||
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
|
||||
ENTRYPOINT ["/usr/bin/entrypoint.sh", "--bind-addr", "0.0.0.0:8080", "."]
|
||||
|
||||
@@ -1,18 +1,20 @@
|
||||
#!/usr/bin/env sh
|
||||
#!/bin/sh
|
||||
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
|
||||
echo "$DOCKER_USER ALL=(ALL) NOPASSWD:ALL" | sudo tee -a /etc/sudoers.d/nopasswd > /dev/null
|
||||
sudo usermod --login "$DOCKER_USER" \
|
||||
--move-home --home "/home/$DOCKER_USER" \
|
||||
coder
|
||||
# Unfortunately we cannot change $HOME as we cannot move any bind mounts
|
||||
# nor can we bind mount $HOME into a new home as that requires a privileged container.
|
||||
sudo usermod --login "$DOCKER_USER" coder
|
||||
sudo groupmod -n "$DOCKER_USER" coder
|
||||
|
||||
USER="$DOCKER_USER"
|
||||
|
||||
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
|
||||
|
||||
# This isn't set by default.
|
||||
export USER="$(whoami)"
|
||||
dumb-init fixuid -q /usr/bin/code-server "$@"
|
||||
dumb-init /usr/bin/code-server "$@"
|
||||
|
||||
@@ -4,7 +4,7 @@ set -euo pipefail
|
||||
main() {
|
||||
cd "$(dirname "$0")/../.."
|
||||
|
||||
NODE_VERSION=v12.18.3
|
||||
NODE_VERSION=v12.18.4
|
||||
NODE_OS="$(uname | tr '[:upper:]' '[:lower:]')"
|
||||
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
|
||||
|
||||
@@ -8,6 +8,7 @@
|
||||
- [Build](#build)
|
||||
- [Structure](#structure)
|
||||
- [VS Code Patch](#vs-code-patch)
|
||||
- [Currently Known Issues](#currently-known-issues)
|
||||
|
||||
<!-- END doctoc generated TOC please keep comment here to allow auto update -->
|
||||
|
||||
@@ -15,24 +16,26 @@
|
||||
|
||||
## Pull Requests
|
||||
|
||||
Please link to the issue each PR solves.
|
||||
If there is no existing issue, please first create one unless the fix is minor.
|
||||
Please create a [GitHub Issue](https://github.com/cdr/code-server/issues) for each issue
|
||||
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
|
||||
default branch the latest release branch to avoid confusion as the
|
||||
documentation is on GitHub and we don't want users to see docs on unreleased
|
||||
features.
|
||||
In your Pull Requests (PR), link to the issue that the PR solves.
|
||||
|
||||
Please ensure that the base of your PR is the **master** branch. (Note: The default
|
||||
GitHub branch is the latest release branch, though you should point all of your changes to be merged into
|
||||
master).
|
||||
|
||||
## 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.
|
||||
- We use [nfpm](https://github.com/goreleaser/nfpm) to build `.deb` and `.rpm` packages.
|
||||
- We use [jq](https://stedolan.github.io/jq/) to build code-server releases.
|
||||
- The [CI container](../ci/images/debian8/Dockerfile) is a useful reference for all our dependencies.
|
||||
The [CI container](../ci/images/debian8/Dockerfile) is a useful reference for all
|
||||
of the dependencies code-server uses.
|
||||
|
||||
## Development Workflow
|
||||
|
||||
@@ -40,48 +43,48 @@ Differences:
|
||||
yarn
|
||||
yarn vscode
|
||||
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
|
||||
./ci/dev/image/exec.sh yarn
|
||||
./ci/dev/image/exec.sh yarn vscode
|
||||
./ci/dev/image/exec.sh yarn watch
|
||||
./ci/dev/image/run.sh yarn
|
||||
./ci/dev/image/run.sh yarn vscode
|
||||
./ci/dev/image/run.sh yarn watch
|
||||
```
|
||||
|
||||
`yarn watch` will live reload changes to the source.
|
||||
|
||||
If changes are made to the patch and you've built previously you must manually
|
||||
reset VS Code then run `yarn vscode:patch`.
|
||||
If you introduce changes to the patch and you've previously built, you
|
||||
must (1) manually reset VS Code and (2) run `yarn vscode:patch`.
|
||||
|
||||
## Build
|
||||
|
||||
You can build with:
|
||||
You can build using:
|
||||
|
||||
```shell
|
||||
./ci/steps/release.sh
|
||||
./ci/dev/image/run.sh ./ci/steps/release.sh
|
||||
```
|
||||
|
||||
Run your build with:
|
||||
|
||||
```
|
||||
```shell
|
||||
cd release
|
||||
yarn --production
|
||||
# Runs the built JavaScript with 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):
|
||||
|
||||
```
|
||||
./ci/dev/image/exec.sh ./ci/steps/release-packages.sh
|
||||
```shell
|
||||
IMAGE=centos7 ./ci/dev/image/run.sh ./ci/steps/release-packages.sh
|
||||
# The standalone release is in ./release-standalone
|
||||
# .deb, .rpm and the standalone archive are in ./release-packages
|
||||
```
|
||||
|
||||
The `release.sh` script is the equivalent of:
|
||||
The `release.sh` script is equal to running:
|
||||
|
||||
```shell
|
||||
yarn
|
||||
@@ -91,66 +94,69 @@ yarn build:vscode
|
||||
yarn release
|
||||
```
|
||||
|
||||
And `release-packages.sh` is:
|
||||
And `release-packages.sh` is equal to:
|
||||
|
||||
```
|
||||
```shell
|
||||
yarn release:standalone
|
||||
yarn test:standalone-release
|
||||
yarn package
|
||||
```
|
||||
|
||||
For a faster release build, you can run instead:
|
||||
|
||||
```shell
|
||||
KEEP_MODULES=1 ./ci/steps/release.sh
|
||||
node ./release
|
||||
```
|
||||
|
||||
## 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
|
||||
[./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
|
||||
|
||||
Back in v1 of code-server, we had an extensive patch of VS Code that split the codebase
|
||||
into a frontend and server. The frontend consisted of all UI code and the server ran
|
||||
the extensions and exposed an API to the frontend for file access and everything else
|
||||
that the UI needed.
|
||||
In v1 of code-server, we had a patch of VS Code that split the codebase into a front-end
|
||||
and a server. The front-end consisted of all UI code, while the server ran the extensions
|
||||
and exposed an API to the front-end for file access and all UI needs.
|
||||
|
||||
This worked but eventually Microsoft added support to VS Code to run it in the web.
|
||||
They have open sourced the frontend but have kept the server closed source.
|
||||
|
||||
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
|
||||
Over time, Microsoft added support to VS Code to run it on the web. They have made
|
||||
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
|
||||
[./ci/dev/vscode.patch](../ci/dev/vscode.patch) under the path `src/vs/server`.
|
||||
|
||||
Other notable changes in our patch include:
|
||||
|
||||
- Add our own build file which includes our code and VS Code's web code.
|
||||
- Allow multiple extension directories (both user and built-in).
|
||||
- Modify 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).
|
||||
- Send client-side telemetry through the server.
|
||||
- Allow modification of the display language.
|
||||
- Make it possible for us to load code on the client.
|
||||
- Make extensions work in the browser.
|
||||
- Make it possible to install extensions of any kind.
|
||||
- Fix getting permanently disconnected when you sleep or hibernate for a while.
|
||||
- Add connection type to web socket query parameters.
|
||||
- Adding our build file, which includes our code and VS Code's web code
|
||||
- Allowing multiple extension directories (both user and built-in)
|
||||
- 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)
|
||||
- Sending client-side telemetry through the server
|
||||
- Allowing modification of the display language
|
||||
- Making it possible for us to load code on the client
|
||||
- Making extensions work in the browser
|
||||
- Making it possible to install extensions of any kind
|
||||
- Fixing issue with getting disconnected when your machine sleeps or hibernates
|
||||
- Adding connection type to web socket query parameters
|
||||
|
||||
Some known issues presently:
|
||||
|
||||
- Creating custom VS Code extensions and debugging them doesn't work.
|
||||
- Extension profiling and tips are currently disabled.
|
||||
|
||||
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
|
||||
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
|
||||
us to ensure that the patch is applied and works as intended. In the future,
|
||||
we'd like to run VS Code unit tests against our builds to ensure that features
|
||||
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 do I debug issues with code-server?](#how-do-i-debug-issues-with-code-server)
|
||||
- [Heartbeat File](#heartbeat-file)
|
||||
- [Healthz endpoint](#healthz-endpoint)
|
||||
- [How does the config file work?](#how-does-the-config-file-work)
|
||||
- [Blank screen on iPad?](#blank-screen-on-ipad)
|
||||
- [Isn't an install script piped into sh insecure?](#isnt-an-install-script-piped-into-sh-insecure)
|
||||
@@ -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.
|
||||
|
||||
## 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?
|
||||
|
||||
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
|
||||
|
||||
```bash
|
||||
curl -fOL https://github.com/cdr/code-server/releases/download/v3.5.0/code-server_3.5.0_amd64.deb
|
||||
sudo dpkg -i code-server_3.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.6.1_amd64.deb
|
||||
sudo systemctl enable --now code-server@$USER
|
||||
# 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
|
||||
|
||||
```bash
|
||||
curl -fOL https://github.com/cdr/code-server/releases/download/v3.5.0/code-server-3.5.0-amd64.rpm
|
||||
sudo rpm -i code-server-3.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.6.1-amd64.rpm
|
||||
sudo systemctl enable --now code-server@$USER
|
||||
# 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
|
||||
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
|
||||
mv ~/.local/lib/code-server-3.5.0-linux-amd64 ~/.local/lib/code-server-3.5.0
|
||||
ln -s ~/.local/lib/code-server-3.5.0/bin/code-server ~/.local/bin/code-server
|
||||
mv ~/.local/lib/code-server-3.6.1-linux-amd64 ~/.local/lib/code-server-3.6.1
|
||||
ln -s ~/.local/lib/code-server-3.6.1/bin/code-server ~/.local/bin/code-server
|
||||
PATH="~/.local/bin:$PATH"
|
||||
code-server
|
||||
# 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
|
||||
# outside the container.
|
||||
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 "$PWD:/home/coder/project" \
|
||||
-u "$(id -u):$(id -g)" \
|
||||
-e "DOCKER_USER=$USER" \
|
||||
codercom/code-server:latest
|
||||
```
|
||||
|
||||
|
||||
64
install.sh
64
install.sh
@@ -17,27 +17,37 @@ usage() {
|
||||
Installs code-server for Linux, macOS and FreeBSD.
|
||||
It tries to use the system package manager if possible.
|
||||
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-}
|
||||
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
|
||||
Echo the commands for the install process without running them.
|
||||
|
||||
--version X.X.X
|
||||
Install a specific version instead of the latest.
|
||||
|
||||
--method [detect | standalone]
|
||||
Choose the installation method. Defaults to detect.
|
||||
- detect detects the system package manager and tries to use it.
|
||||
Full reference on the process is further below.
|
||||
- standalone installs a standalone release archive into ~/.local
|
||||
Add ~/.local/bin to your \$PATH to use it.
|
||||
|
||||
--prefix <dir>
|
||||
Sets the prefix used by standalone release archives. Defaults to ~/.local
|
||||
The release is unarchived into ~/.local/lib/code-server-X.X.X
|
||||
and the binary symlinked into ~/.local/bin/code-server
|
||||
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 Fedora, CentOS, RHEL and openSUSE it will install the latest rpm package.
|
||||
- For Arch Linux it will install the AUR package.
|
||||
@@ -100,9 +110,19 @@ main() {
|
||||
METHOD \
|
||||
STANDALONE_INSTALL_PREFIX \
|
||||
VERSION \
|
||||
OPTIONAL
|
||||
OPTIONAL \
|
||||
ALL_FLAGS \
|
||||
RSH_ARGS \
|
||||
RSH
|
||||
|
||||
ALL_FLAGS=""
|
||||
while [ "$#" -gt 0 ]; do
|
||||
case "$1" in
|
||||
-*)
|
||||
ALL_FLAGS="${ALL_FLAGS} $1"
|
||||
;;
|
||||
esac
|
||||
|
||||
case "$1" in
|
||||
--dry-run)
|
||||
DRY_RUN=1
|
||||
@@ -128,20 +148,45 @@ main() {
|
||||
--version=*)
|
||||
VERSION="$(parse_arg "$@")"
|
||||
;;
|
||||
--rsh)
|
||||
RSH="$(parse_arg "$@")"
|
||||
shift
|
||||
;;
|
||||
--rsh=*)
|
||||
RSH="$(parse_arg "$@")"
|
||||
;;
|
||||
-h | --h | -help | --help)
|
||||
usage
|
||||
exit 0
|
||||
;;
|
||||
*)
|
||||
--)
|
||||
shift
|
||||
# We remove the -- added above.
|
||||
ALL_FLAGS="${ALL_FLAGS% --}"
|
||||
RSH_ARGS="$*"
|
||||
break
|
||||
;;
|
||||
-*)
|
||||
echoerr "Unknown flag $1"
|
||||
echoerr "Run with --help to see usage."
|
||||
exit 1
|
||||
;;
|
||||
*)
|
||||
RSH_ARGS="$*"
|
||||
break
|
||||
;;
|
||||
esac
|
||||
|
||||
shift
|
||||
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)}"
|
||||
METHOD="${METHOD-detect}"
|
||||
if [ "$METHOD" != detect ] && [ "$METHOD" != standalone ]; then
|
||||
@@ -446,7 +491,7 @@ arch() {
|
||||
}
|
||||
|
||||
command_exists() {
|
||||
command -v "$@" > /dev/null 2>&1
|
||||
command -v "$@" > /dev/null
|
||||
}
|
||||
|
||||
sh_c() {
|
||||
@@ -500,4 +545,15 @@ humanpath() {
|
||||
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 "$@"
|
||||
|
||||
Submodule lib/vscode updated: a0479759d6...93c2f0fbf1
@@ -1,7 +1,7 @@
|
||||
{
|
||||
"name": "code-server",
|
||||
"license": "MIT",
|
||||
"version": "3.5.0",
|
||||
"version": "3.6.2",
|
||||
"description": "Run VS Code on a remote server.",
|
||||
"homepage": "https://github.com/cdr/code-server",
|
||||
"bugs": {
|
||||
@@ -39,6 +39,7 @@
|
||||
"@types/pem": "^1.9.5",
|
||||
"@types/safe-compare": "^1.1.0",
|
||||
"@types/semver": "^7.1.0",
|
||||
"@types/split2": "^2.1.6",
|
||||
"@types/tar-fs": "^2.0.0",
|
||||
"@types/tar-stream": "^2.1.0",
|
||||
"@types/ws": "^7.2.6",
|
||||
@@ -76,6 +77,7 @@
|
||||
"safe-buffer": "^5.1.1",
|
||||
"safe-compare": "^1.1.4",
|
||||
"semver": "^7.1.3",
|
||||
"split2": "^3.2.2",
|
||||
"tar": "^6.0.1",
|
||||
"tar-fs": "^2.0.0",
|
||||
"ws": "^7.2.0",
|
||||
|
||||
@@ -47,5 +47,5 @@
|
||||
</div>
|
||||
</body>
|
||||
<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>
|
||||
|
||||
@@ -9,11 +9,6 @@
|
||||
|
||||
<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 -->
|
||||
<meta
|
||||
name="viewport"
|
||||
|
||||
@@ -17,7 +17,7 @@ try {
|
||||
}
|
||||
// FIXME: Only works if path separators are /.
|
||||
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((json) => {
|
||||
bundles[bundle] = json
|
||||
@@ -31,7 +31,8 @@ try {
|
||||
}
|
||||
|
||||
;(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,
|
||||
paths: {
|
||||
"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-webgl": `../node_modules/xterm-addon-webgl/lib/xterm-addon-webgl.js`,
|
||||
"semver-umd": `../node_modules/semver-umd/lib/semver-umd.js`,
|
||||
"tas-client-umd": `../node_modules/tas-client-umd/lib/tas-client-umd.js`,
|
||||
"iconv-lite-umd": `../node_modules/iconv-lite-umd/lib/iconv-lite-umd.js`,
|
||||
jschardet: `../node_modules/jschardet/dist/jschardet.min.js`,
|
||||
},
|
||||
|
||||
@@ -10,7 +10,7 @@ if ("serviceWorker" in navigator) {
|
||||
const path = normalize(`${options.csStaticBase}/dist/serviceWorker.js`)
|
||||
navigator.serviceWorker
|
||||
.register(path, {
|
||||
scope: options.base || "/",
|
||||
scope: (options.base ?? "") + "/",
|
||||
})
|
||||
.then(() => {
|
||||
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...")
|
||||
return new Promise<WorkbenchOptions>((resolve, reject) => {
|
||||
vscode.once("message", (message: VscodeMessage) => {
|
||||
logger.debug("got message from vs code", field("message", message))
|
||||
return message.type === "options" && message.id === id
|
||||
? resolve(message.options)
|
||||
: reject(new Error("Unexpected response during initialization"))
|
||||
})
|
||||
const onMessage = (message: VscodeMessage) => {
|
||||
// There can be parallel initializations so wait for the right ID.
|
||||
if (message.type === "options" && message.id === id) {
|
||||
logger.trace("got message from vs code", field("message", message))
|
||||
vscode.off("message", onMessage)
|
||||
resolve(message.options)
|
||||
}
|
||||
}
|
||||
vscode.on("message", onMessage)
|
||||
vscode.once("error", reject)
|
||||
vscode.once("exit", (code) => reject(new Error(`VS Code exited unexpectedly with code ${code}`)))
|
||||
this.send({ type: "init", id, options }, vscode)
|
||||
@@ -77,7 +80,7 @@ export class VscodeHttpProvider extends HttpProvider {
|
||||
|
||||
this._vscode = new Promise((resolve, reject) => {
|
||||
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"
|
||||
? resolve(vscode)
|
||||
: 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 { Args as VsArgs } from "../../lib/vscode/src/vs/server/ipc"
|
||||
import { AuthType } from "./http"
|
||||
import { generatePassword, humanPath, paths } from "./util"
|
||||
import { canConnect, generatePassword, humanPath, paths } from "./util"
|
||||
|
||||
export class Optional<T> {
|
||||
public constructor(public readonly value?: T) {}
|
||||
@@ -47,6 +47,8 @@ export interface Args extends VsArgs {
|
||||
readonly _: string[]
|
||||
readonly "reuse-window"?: boolean
|
||||
readonly "new-window"?: boolean
|
||||
|
||||
readonly link?: OptionalString
|
||||
}
|
||||
|
||||
interface Option<T> {
|
||||
@@ -63,6 +65,11 @@ interface Option<T> {
|
||||
* Description of the option. Leave blank to hide the option.
|
||||
*/
|
||||
description?: string
|
||||
|
||||
/**
|
||||
* If marked as beta, the option is not printed unless $CS_BETA is set.
|
||||
*/
|
||||
beta?: boolean
|
||||
}
|
||||
|
||||
type OptionType<T> = T extends boolean
|
||||
@@ -130,7 +137,8 @@ const options: Options<Required<Args>> = {
|
||||
"install-extension": {
|
||||
type: "string[]",
|
||||
description:
|
||||
"Install or update a VS Code extension by id or vsix. The identifier of an extension is `${publisher}.${name}`. To install a specific version provide `@${version}`. For example: 'vscode.csharp@1.2.3'.",
|
||||
"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": {
|
||||
type: "string[]",
|
||||
@@ -144,17 +152,29 @@ const options: Options<Required<Args>> = {
|
||||
"new-window": {
|
||||
type: "boolean",
|
||||
short: "n",
|
||||
description: "Force to open a new window. (use with open-in)",
|
||||
description: "Force to open a new window.",
|
||||
},
|
||||
"reuse-window": {
|
||||
type: "boolean",
|
||||
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" },
|
||||
log: { type: LogLevel },
|
||||
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[] => {
|
||||
@@ -166,12 +186,32 @@ export const optionDescriptions = (): string[] => {
|
||||
}),
|
||||
{ short: 0, long: 0 },
|
||||
)
|
||||
return entries.map(
|
||||
([k, v]) =>
|
||||
`${" ".repeat(widths.short - (v.short ? v.short.length : 0))}${v.short ? `-${v.short}` : " "} --${k}${" ".repeat(
|
||||
widths.long - k.length,
|
||||
)} ${v.description}${typeof v.type === "object" ? ` [${Object.values(v.type).join(", ")}]` : ""}`,
|
||||
)
|
||||
return entries
|
||||
.filter(([, v]) => {
|
||||
// If CS_BETA is set, we show beta options but if not, then we do not want
|
||||
// to show beta options.
|
||||
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 = (
|
||||
@@ -287,6 +327,21 @@ export const parse = (
|
||||
|
||||
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
|
||||
// environment variable.
|
||||
if (args.verbose) {
|
||||
@@ -329,21 +384,6 @@ export const parse = (
|
||||
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> {
|
||||
return `bind-addr: 127.0.0.1:8080
|
||||
auth: password
|
||||
@@ -370,10 +410,6 @@ export async function readConfigFile(configPath?: string): Promise<Args> {
|
||||
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 config = yaml.safeLoad(configFile.toString(), {
|
||||
filename: configPath,
|
||||
@@ -401,7 +437,10 @@ export async function readConfigFile(configPath?: string): Promise<Args> {
|
||||
|
||||
function parseBindAddr(bindAddr: string): [string, number] {
|
||||
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 {
|
||||
@@ -453,3 +492,52 @@ async function copyOldMacOSDataDir(): Promise<void> {
|
||||
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 { CliMessage, OpenCommandPipeArgs } from "../../lib/vscode/src/vs/server/ipc"
|
||||
import { plural } from "../common/util"
|
||||
import { HealthHttpProvider } from "./app/health"
|
||||
import { LoginHttpProvider } from "./app/login"
|
||||
import { ProxyHttpProvider } from "./app/proxy"
|
||||
import { StaticHttpProvider } from "./app/static"
|
||||
import { UpdateHttpProvider } from "./app/update"
|
||||
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 { loadPlugins } from "./plugin"
|
||||
import { generateCertificate, hash, humanPath, open } from "./util"
|
||||
import { ipcMain, wrap } from "./wrapper"
|
||||
|
||||
process.on("uncaughtException", (error) => {
|
||||
logger.error(`Uncaught exception: ${error.message}`)
|
||||
if (typeof error.stack !== "undefined") {
|
||||
logger.error(error.stack)
|
||||
}
|
||||
})
|
||||
import { ipcMain, WrapperProcess } from "./wrapper"
|
||||
|
||||
let pkg: { version?: string; commit?: string } = {}
|
||||
try {
|
||||
@@ -33,7 +37,100 @@ try {
|
||||
const version = pkg.version || "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) {
|
||||
args = {
|
||||
...args,
|
||||
@@ -50,7 +147,7 @@ const main = async (args: Args, cliArgs: Args, configArgs: Args): Promise<void>
|
||||
if (args.auth === AuthType.Password && !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.
|
||||
const options: HttpServerOptions = {
|
||||
@@ -80,16 +177,19 @@ const main = async (args: Args, cliArgs: Args, configArgs: Args): Promise<void>
|
||||
httpServer.registerHttpProvider("/proxy", ProxyHttpProvider)
|
||||
httpServer.registerHttpProvider("/login", LoginHttpProvider, args.config!, envPassword)
|
||||
httpServer.registerHttpProvider("/static", StaticHttpProvider)
|
||||
httpServer.registerHttpProvider("/healthz", HealthHttpProvider, httpServer.heart)
|
||||
|
||||
await loadPlugins(httpServer, args)
|
||||
|
||||
ipcMain().onDispose(() => {
|
||||
ipcMain.onDispose(() => {
|
||||
httpServer.dispose().then((errors) => {
|
||||
errors.forEach((error) => logger.error(error.message))
|
||||
})
|
||||
})
|
||||
|
||||
logger.info(`code-server ${version} ${commit}`)
|
||||
logger.info(`Using config file ${humanPath(args.config)}`)
|
||||
|
||||
const serverAddress = await httpServer.listen()
|
||||
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) {
|
||||
// The web socket doesn't seem to work if browsing with 0.0.0.0.
|
||||
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}`)
|
||||
}
|
||||
|
||||
if (args.link) {
|
||||
try {
|
||||
await coderCloudBind(serverAddress!, args.link.value)
|
||||
} catch (err) {
|
||||
logger.error(err.message)
|
||||
ipcMain.exit(1)
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
async function entry(): Promise<void> {
|
||||
const tryParse = async (): Promise<[Args, Args, Args]> => {
|
||||
try {
|
||||
const cliArgs = parse(process.argv.slice(2))
|
||||
const configArgs = await readConfigFile(cliArgs.config)
|
||||
// This prioritizes the flags set in args over the ones in the config file.
|
||||
let args = Object.assign(configArgs, cliArgs)
|
||||
args = await setDefaults(args)
|
||||
return [args, cliArgs, configArgs]
|
||||
} catch (error) {
|
||||
console.error(error.message)
|
||||
process.exit(1)
|
||||
}
|
||||
const cliArgs = parse(process.argv.slice(2))
|
||||
const configArgs = await readConfigFile(cliArgs.config)
|
||||
// This prioritizes the flags set in args over the ones in the config file.
|
||||
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
|
||||
// instance for the child process because these would have already happened in
|
||||
// the parent and the child wouldn't have been spawned.
|
||||
if (ipcMain.isChild) {
|
||||
await ipcMain.handshake()
|
||||
ipcMain.preventExit()
|
||||
return main(args, configArgs)
|
||||
}
|
||||
|
||||
const [args, cliArgs, configArgs] = await tryParse()
|
||||
if (args.help) {
|
||||
console.log("code-server", version, commit)
|
||||
console.log("")
|
||||
@@ -153,7 +264,10 @@ async function entry(): Promise<void> {
|
||||
optionDescriptions().forEach((description) => {
|
||||
console.log("", description)
|
||||
})
|
||||
} else if (args.version) {
|
||||
return
|
||||
}
|
||||
|
||||
if (args.version) {
|
||||
if (args.json) {
|
||||
console.log({
|
||||
codeServer: version,
|
||||
@@ -163,83 +277,23 @@ async function entry(): Promise<void> {
|
||||
} else {
|
||||
console.log(version, commit)
|
||||
}
|
||||
process.exit(0)
|
||||
} else if (process.env.VSCODE_IPC_HOOK_CLI) {
|
||||
const pipeArgs: OpenCommandPipeArgs = {
|
||||
type: "open",
|
||||
folderURIs: [],
|
||||
forceReuseWindow: args["reuse-window"],
|
||||
forceNewWindow: args["new-window"],
|
||||
}
|
||||
const isDir = async (path: string): Promise<boolean> => {
|
||||
try {
|
||||
const st = await fs.stat(path)
|
||||
return st.isDirectory()
|
||||
} catch (error) {
|
||||
return false
|
||||
}
|
||||
}
|
||||
for (let i = 0; i < args._.length; i++) {
|
||||
const fp = path.resolve(args._[i])
|
||||
if (await isDir(fp)) {
|
||||
pipeArgs.folderURIs.push(fp)
|
||||
} else {
|
||||
if (!pipeArgs.fileURIs) {
|
||||
pipeArgs.fileURIs = []
|
||||
}
|
||||
pipeArgs.fileURIs.push(fp)
|
||||
}
|
||||
}
|
||||
if (pipeArgs.forceNewWindow && pipeArgs.fileURIs && pipeArgs.fileURIs.length > 0) {
|
||||
logger.error("new-window can only be used with folder paths")
|
||||
process.exit(1)
|
||||
}
|
||||
if (pipeArgs.folderURIs.length === 0 && (!pipeArgs.fileURIs || pipeArgs.fileURIs.length === 0)) {
|
||||
logger.error("Please specify at least one file or folder argument")
|
||||
process.exit(1)
|
||||
}
|
||||
const vscode = http.request(
|
||||
{
|
||||
path: "/",
|
||||
method: "POST",
|
||||
socketPath: process.env["VSCODE_IPC_HOOK_CLI"],
|
||||
},
|
||||
(res) => {
|
||||
res.on("data", (message) => {
|
||||
logger.debug("Got message from VS Code", field("message", message.toString()))
|
||||
})
|
||||
},
|
||||
)
|
||||
vscode.on("error", (err) => {
|
||||
logger.debug("Got error from VS Code", field("error", err))
|
||||
})
|
||||
vscode.write(JSON.stringify(pipeArgs))
|
||||
vscode.end()
|
||||
} else 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))
|
||||
return
|
||||
}
|
||||
|
||||
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.
|
||||
*/
|
||||
protected ensureAuthenticated(request: http.IncomingMessage): void {
|
||||
public ensureAuthenticated(request: http.IncomingMessage): void {
|
||||
if (!this.authenticated(request)) {
|
||||
throw new HttpError("Unauthorized", HttpCode.Unauthorized)
|
||||
}
|
||||
@@ -396,23 +396,26 @@ export abstract class HttpProvider {
|
||||
export class Heart {
|
||||
private heartbeatTimer?: NodeJS.Timeout
|
||||
private heartbeatInterval = 60000
|
||||
private lastHeartbeat = 0
|
||||
public lastHeartbeat = 0
|
||||
|
||||
public constructor(private readonly heartbeatPath: string, private readonly isActive: () => Promise<boolean>) {}
|
||||
|
||||
public alive(): boolean {
|
||||
const now = Date.now()
|
||||
return now - this.lastHeartbeat < this.heartbeatInterval
|
||||
}
|
||||
/**
|
||||
* Write to the heartbeat file if we haven't already done so within the
|
||||
* timeout and start or reset a timer that keeps running as long as there is
|
||||
* activity. Failures are logged as warnings.
|
||||
*/
|
||||
public beat(): void {
|
||||
const now = Date.now()
|
||||
if (now - this.lastHeartbeat >= this.heartbeatInterval) {
|
||||
if (!this.alive()) {
|
||||
logger.trace("heartbeat")
|
||||
fs.outputFile(this.heartbeatPath, "").catch((error) => {
|
||||
logger.warn(error.message)
|
||||
})
|
||||
this.lastHeartbeat = now
|
||||
this.lastHeartbeat = Date.now()
|
||||
if (typeof this.heartbeatTimer !== "undefined") {
|
||||
clearTimeout(this.heartbeatTimer)
|
||||
}
|
||||
@@ -457,7 +460,7 @@ export class HttpServer {
|
||||
private listenPromise: Promise<string | null> | undefined
|
||||
public readonly protocol: "http" | "https"
|
||||
private readonly providers = new Map<string, HttpProvider>()
|
||||
private readonly heart: Heart
|
||||
public readonly heart: Heart
|
||||
private readonly socketProvider = new SocketProxyProvider()
|
||||
|
||||
/**
|
||||
@@ -575,14 +578,24 @@ export class HttpServer {
|
||||
*/
|
||||
public listen(): Promise<string | null> {
|
||||
if (!this.listenPromise) {
|
||||
this.listenPromise = new Promise((resolve, reject) => {
|
||||
this.listenPromise = new Promise(async (resolve, reject) => {
|
||||
this.server.on("error", reject)
|
||||
this.server.on("upgrade", this.onUpgrade)
|
||||
const onListen = (): void => resolve(this.address())
|
||||
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)
|
||||
} 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 {
|
||||
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> => {
|
||||
this.heart.beat()
|
||||
const route = this.parseUrl(request)
|
||||
if (route.providerBase !== "/healthz") {
|
||||
this.heart.beat()
|
||||
}
|
||||
const write = (payload: HttpResponse): void => {
|
||||
response.writeHead(payload.redirect ? HttpCode.Redirect : payload.code || HttpCode.Ok, {
|
||||
"Content-Type": payload.mime || getMediaMime(payload.filePath),
|
||||
@@ -642,10 +657,7 @@ export class HttpServer {
|
||||
}
|
||||
|
||||
try {
|
||||
const payload =
|
||||
this.maybeRedirect(request, route) ||
|
||||
(route.provider.authenticated(request) && this.maybeProxy(request)) ||
|
||||
(await route.provider.handleRequest(route, request))
|
||||
const payload = (await this.handleRequest(route, request)) || (await route.provider.handleRequest(route, request))
|
||||
if (payload.proxy) {
|
||||
this.doProxy(route, request, response, payload.proxy)
|
||||
} 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 (this.options.cert && !(request.connection as tls.TLSSocket).encrypted) {
|
||||
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> => {
|
||||
socket.pause()
|
||||
|
||||
try {
|
||||
this.heart.beat()
|
||||
socket.on("error", () => socket.destroy())
|
||||
@@ -739,7 +761,7 @@ export class HttpServer {
|
||||
// can't be transferred so we need an in-between).
|
||||
const socketProxy = await this.socketProvider.createProxy(socket)
|
||||
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) {
|
||||
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
|
||||
* 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.
|
||||
const host = request.headers.host || ""
|
||||
const idx = host.indexOf(":")
|
||||
@@ -904,6 +928,9 @@ export class HttpServer {
|
||||
return undefined
|
||||
}
|
||||
|
||||
// Must be authenticated to use the proxy.
|
||||
route.provider.ensureAuthenticated(request)
|
||||
|
||||
return {
|
||||
proxy: {
|
||||
port,
|
||||
|
||||
@@ -4,11 +4,15 @@ import * as path from "path"
|
||||
import * as util from "util"
|
||||
import { Args } from "./cli"
|
||||
import { HttpServer } from "./http"
|
||||
import { paths } from "./util"
|
||||
|
||||
/* eslint-disable @typescript-eslint/no-var-requires */
|
||||
|
||||
export type Activate = (httpServer: HttpServer, args: Args) => void
|
||||
|
||||
/**
|
||||
* Plugins must implement this interface.
|
||||
*/
|
||||
export interface Plugin {
|
||||
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])
|
||||
}
|
||||
|
||||
/**
|
||||
* Load a plugin and run its activation function.
|
||||
*/
|
||||
const loadPlugin = async (pluginPath: string, httpServer: HttpServer, args: Args): Promise<void> => {
|
||||
try {
|
||||
const plugin: Plugin = require(pluginPath)
|
||||
plugin.activate(httpServer, args)
|
||||
logger.debug("Loaded plugin", field("name", path.basename(pluginPath)))
|
||||
|
||||
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) {
|
||||
if (error.code !== "MODULE_NOT_FOUND") {
|
||||
logger.warn(error.message)
|
||||
} else {
|
||||
logger.error(error.message)
|
||||
}
|
||||
logger.error(error.message)
|
||||
}
|
||||
}
|
||||
|
||||
const _loadPlugins = async (httpServer: HttpServer, args: Args): Promise<void> => {
|
||||
const pluginPath = path.resolve(__dirname, "../../plugins")
|
||||
const files = await util.promisify(fs.readdir)(pluginPath, {
|
||||
withFileTypes: true,
|
||||
})
|
||||
await Promise.all(files.map((file) => loadPlugin(path.join(pluginPath, file.name), httpServer, args)))
|
||||
}
|
||||
|
||||
export const loadPlugins = async (httpServer: HttpServer, args: Args): Promise<void> => {
|
||||
/**
|
||||
* Load all plugins in the specified directory.
|
||||
*/
|
||||
const _loadPlugins = async (pluginDir: string, httpServer: HttpServer, args: Args): Promise<void> => {
|
||||
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) {
|
||||
if (error.code !== "ENOENT") {
|
||||
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 { Emitter } from "../common/emitter"
|
||||
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
|
||||
@@ -89,17 +89,6 @@ export class SocketProxyProvider {
|
||||
}
|
||||
|
||||
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 path = basePath
|
||||
while ((await canConnect(path)) && i < maxTries) {
|
||||
|
||||
@@ -2,6 +2,7 @@ import * as cp from "child_process"
|
||||
import * as crypto from "crypto"
|
||||
import envPaths from "env-paths"
|
||||
import * as fs from "fs-extra"
|
||||
import * as net from "net"
|
||||
import * as os from "os"
|
||||
import * as path from "path"
|
||||
import * as util from "util"
|
||||
@@ -246,3 +247,17 @@ export function pathToFsPath(path: string, keepDriveLetterCasing = false): strin
|
||||
}
|
||||
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
|
||||
private readonly _onDispose = new Emitter<NodeJS.Signals | undefined>()
|
||||
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("SIGTERM", () => this._onDispose.emit("SIGTERM"))
|
||||
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) => {
|
||||
// Remove listeners to avoid possibly triggering disposal again.
|
||||
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 {
|
||||
if (error && typeof error !== "number") {
|
||||
this.processExit(typeof error.code === "number" ? error.code : 1)
|
||||
@@ -127,17 +134,12 @@ export class IpcMain {
|
||||
}
|
||||
}
|
||||
|
||||
let _ipcMain: IpcMain
|
||||
export const ipcMain = (): IpcMain => {
|
||||
if (!_ipcMain) {
|
||||
_ipcMain = new IpcMain(
|
||||
typeof process.env.CODE_SERVER_PARENT_PID !== "undefined"
|
||||
? parseInt(process.env.CODE_SERVER_PARENT_PID)
|
||||
: undefined,
|
||||
)
|
||||
}
|
||||
return _ipcMain
|
||||
}
|
||||
/**
|
||||
* Channel for communication between the child and parent processes.
|
||||
*/
|
||||
export const ipcMain = new IpcMain(
|
||||
typeof process.env.CODE_SERVER_PARENT_PID !== "undefined" ? parseInt(process.env.CODE_SERVER_PARENT_PID) : undefined,
|
||||
)
|
||||
|
||||
export interface WrapperOptions {
|
||||
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.logStderrStream = rfs.createStream(path.join(paths.data, "coder-logs", "code-server-stderr.log"), opts)
|
||||
|
||||
ipcMain().onDispose(() => {
|
||||
if (this.process) {
|
||||
this.process.removeAllListeners()
|
||||
this.process.kill()
|
||||
}
|
||||
ipcMain.onDispose(() => {
|
||||
this.disposeChild()
|
||||
})
|
||||
|
||||
ipcMain().onMessage((message) => {
|
||||
ipcMain.onMessage((message) => {
|
||||
switch (message.type) {
|
||||
case "relaunch":
|
||||
logger.info(`Relaunching: ${this.currentVersion} -> ${message.version}`)
|
||||
@@ -181,55 +180,65 @@ export class WrapperProcess {
|
||||
break
|
||||
}
|
||||
})
|
||||
|
||||
process.on("SIGUSR1", async () => {
|
||||
logger.info("Received SIGUSR1; hotswapping")
|
||||
this.relaunch()
|
||||
})
|
||||
}
|
||||
|
||||
private async relaunch(): Promise<void> {
|
||||
private disposeChild(): void {
|
||||
this.started = undefined
|
||||
if (this.process) {
|
||||
this.process.removeAllListeners()
|
||||
this.process.kill()
|
||||
}
|
||||
}
|
||||
|
||||
private async relaunch(): Promise<void> {
|
||||
this.disposeChild()
|
||||
try {
|
||||
await this.start()
|
||||
} catch (error) {
|
||||
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> {
|
||||
if (!this.started) {
|
||||
this.started = this.spawn().then((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}`)
|
||||
ipcMain()
|
||||
.handshake(child)
|
||||
.then(() => {
|
||||
child.once("exit", (code) => {
|
||||
logger.debug(`inner process ${child.pid} exited unexpectedly`)
|
||||
ipcMain().exit(code || 0)
|
||||
})
|
||||
})
|
||||
this.process = child
|
||||
// If we have a process then we've already bound this.
|
||||
if (!this.process) {
|
||||
process.on("SIGUSR1", async () => {
|
||||
logger.info("Received SIGUSR1; hotswapping")
|
||||
this.relaunch()
|
||||
})
|
||||
}
|
||||
if (!this.started) {
|
||||
this.started = this._start()
|
||||
}
|
||||
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.
|
||||
let nodeOptions = `${process.env.NODE_OPTIONS || ""} ${(this.options && this.options.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
|
||||
// --version | head -1). Assume that means we're done.
|
||||
if (!process.stdout.isTTY) {
|
||||
process.stdout.on("error", () => ipcMain().exit())
|
||||
process.stdout.on("error", () => ipcMain.exit())
|
||||
}
|
||||
|
||||
export const wrap = (fn: () => Promise<void>): void => {
|
||||
if (ipcMain().parentPid) {
|
||||
ipcMain()
|
||||
.handshake()
|
||||
.then(() => fn())
|
||||
.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)
|
||||
})
|
||||
// Don't let uncaught exceptions crash the process.
|
||||
process.on("uncaughtException", (error) => {
|
||||
logger.error(`Uncaught exception: ${error.message}`)
|
||||
if (typeof error.stack !== "undefined") {
|
||||
logger.error(error.stack)
|
||||
}
|
||||
}
|
||||
})
|
||||
|
||||
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 fs from "fs-extra"
|
||||
import * as net from "net"
|
||||
import * as os from "os"
|
||||
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(() => {
|
||||
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 = {
|
||||
_: [],
|
||||
"extensions-dir": path.join(paths.data, "extensions"),
|
||||
"user-data-dir": paths.data,
|
||||
}
|
||||
|
||||
it("should set defaults", () => {
|
||||
assert.deepEqual(parse([]), defaults)
|
||||
assert.deepEqual(parse([]), { _: [] })
|
||||
})
|
||||
|
||||
it("should parse all available options", () => {
|
||||
@@ -69,7 +80,7 @@ describe("cli", () => {
|
||||
help: true,
|
||||
host: "0.0.0.0",
|
||||
json: true,
|
||||
log: "trace",
|
||||
log: "error",
|
||||
open: true,
|
||||
port: 8081,
|
||||
socket: path.resolve("mumble"),
|
||||
@@ -83,19 +94,20 @@ describe("cli", () => {
|
||||
|
||||
it("should work with short options", () => {
|
||||
assert.deepEqual(parse(["-vvv", "-v"]), {
|
||||
...defaults,
|
||||
log: "trace",
|
||||
_: [],
|
||||
verbose: 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"
|
||||
assert.deepEqual(parse([]), {
|
||||
assert.deepEqual(await setDefaults(args), {
|
||||
...defaults,
|
||||
_: [],
|
||||
log: "debug",
|
||||
verbose: false,
|
||||
})
|
||||
@@ -103,8 +115,9 @@ describe("cli", () => {
|
||||
assert.equal(logger.level, Level.Debug)
|
||||
|
||||
process.env.LOG_LEVEL = "trace"
|
||||
assert.deepEqual(parse([]), {
|
||||
assert.deepEqual(await setDefaults(args), {
|
||||
...defaults,
|
||||
_: [],
|
||||
log: "trace",
|
||||
verbose: true,
|
||||
})
|
||||
@@ -113,9 +126,16 @@ describe("cli", () => {
|
||||
})
|
||||
|
||||
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"
|
||||
assert.deepEqual(parse(["--log", "info"]), {
|
||||
assert.deepEqual(await setDefaults(args), {
|
||||
...defaults,
|
||||
_: [],
|
||||
log: "info",
|
||||
verbose: false,
|
||||
})
|
||||
@@ -123,17 +143,26 @@ describe("cli", () => {
|
||||
assert.equal(logger.level, Level.Info)
|
||||
|
||||
process.env.LOG_LEVEL = "trace"
|
||||
assert.deepEqual(parse(["--log", "info"]), {
|
||||
assert.deepEqual(await setDefaults(args), {
|
||||
...defaults,
|
||||
_: [],
|
||||
log: "info",
|
||||
verbose: false,
|
||||
})
|
||||
assert.equal(process.env.LOG_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"
|
||||
assert.deepEqual(parse(["--log", "info", "--verbose"]), {
|
||||
assert.deepEqual(await setDefaults(args), {
|
||||
...defaults,
|
||||
_: [],
|
||||
log: "trace",
|
||||
verbose: true,
|
||||
})
|
||||
@@ -141,9 +170,12 @@ describe("cli", () => {
|
||||
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"
|
||||
assert.deepEqual(parse([]), defaults)
|
||||
assert.deepEqual(await setDefaults(parse([])), {
|
||||
_: [],
|
||||
...defaults,
|
||||
})
|
||||
})
|
||||
|
||||
it("should error if value isn't provided", () => {
|
||||
@@ -166,7 +198,7 @@ describe("cli", () => {
|
||||
|
||||
it("should not error if the value is optional", () => {
|
||||
assert.deepEqual(parse(["--cert"]), {
|
||||
...defaults,
|
||||
_: [],
|
||||
cert: {
|
||||
value: undefined,
|
||||
},
|
||||
@@ -177,7 +209,7 @@ describe("cli", () => {
|
||||
assert.throws(() => parse(["--socket", "--socket-path-value"]), /--socket requires a value/)
|
||||
// If you actually had a path like this you would do this instead:
|
||||
assert.deepEqual(parse(["--socket", "./--socket-path-value"]), {
|
||||
...defaults,
|
||||
_: [],
|
||||
socket: path.resolve("--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", () => {
|
||||
assert.deepEqual(parse(["foo", "test", "--auth", "none"]), {
|
||||
...defaults,
|
||||
_: ["foo", "test"],
|
||||
auth: "none",
|
||||
})
|
||||
@@ -193,12 +224,85 @@ describe("cli", () => {
|
||||
|
||||
it("should support repeatable flags", () => {
|
||||
assert.deepEqual(parse(["--proxy-domain", "*.coder.com"]), {
|
||||
...defaults,
|
||||
_: [],
|
||||
"proxy-domain": ["*.coder.com"],
|
||||
})
|
||||
assert.deepEqual(parse(["--proxy-domain", "*.coder.com", "--proxy-domain", "test.com"]), {
|
||||
...defaults,
|
||||
_: [],
|
||||
"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"
|
||||
|
||||
describe("update", () => {
|
||||
return
|
||||
let version = "1.0.0"
|
||||
let spy: string[] = []
|
||||
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"
|
||||
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":
|
||||
version "2.0.0"
|
||||
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"
|
||||
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"
|
||||
resolved "https://registry.yarnpkg.com/readable-stream/-/readable-stream-3.6.0.tgz#337bbda3adc0706bd3e024426a286d4b4b2c9198"
|
||||
integrity sha512-BViHy7LKeTz4oNnkcLJ+lVSL6vpiFeX6/d3oSH8zCW7UxP2onchk+vTGB143xuFjHS3deTgkKoXXymXqymiIdA==
|
||||
@@ -6621,6 +6628,13 @@ split-string@^3.0.1, split-string@^3.0.2:
|
||||
dependencies:
|
||||
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:
|
||||
version "1.0.3"
|
||||
resolved "https://registry.yarnpkg.com/sprintf-js/-/sprintf-js-1.0.3.tgz#04e6926f662895354f3dd015203633b857297e2c"
|
||||
|
||||
Reference in New Issue
Block a user