Compare commits
161 Commits
| Author | SHA1 | Date | |
|---|---|---|---|
|
|
6f1309795e | ||
|
|
5f94d5a687 | ||
|
|
d6b1d0c7ff | ||
|
|
ce9d14d55e | ||
|
|
eccee53142 | ||
|
|
f7f11ad6c2 | ||
|
|
0c2381f4ff | ||
|
|
e4ddffd0e2 | ||
|
|
f5ac262a2f | ||
|
|
04cd74cad0 | ||
|
|
7b1edd5ad4 | ||
|
|
47d50c9163 | ||
|
|
8a3466e86f | ||
|
|
59f0128d27 | ||
|
|
761c2035c7 | ||
|
|
094ca2ad97 | ||
|
|
c33a4651c9 | ||
|
|
783dfe0a14 | ||
|
|
b9f43c3542 | ||
|
|
65325eef89 | ||
|
|
2421cab479 | ||
|
|
ec1c74c146 | ||
|
|
e955da14fa | ||
|
|
52eeccaba1 | ||
|
|
3a1e3bc596 | ||
|
|
e0dbd8f74a | ||
|
|
6a25b3bfa0 | ||
|
|
aee2599904 | ||
|
|
d56381666a | ||
|
|
611cde7202 | ||
|
|
181bad9563 | ||
|
|
73b2ff0945 | ||
|
|
89c5a4dfea | ||
|
|
d4b3d21dce | ||
|
|
40778b15ca | ||
|
|
d7234029e6 | ||
|
|
10b06cae10 | ||
|
|
0bd2602774 | ||
|
|
c69346a9a7 | ||
|
|
5651201643 | ||
|
|
f475767c2b | ||
|
|
a0a77e379e | ||
|
|
f4a78587b0 | ||
|
|
b3ae4d67d3 | ||
|
|
d30f3dbdf7 | ||
|
|
1739b21600 | ||
|
|
a346c6d565 | ||
|
|
502c262c82 | ||
|
|
4aae5eaeca | ||
|
|
41d625abb6 | ||
|
|
8626bed4ef | ||
|
|
dc632ac176 | ||
|
|
c0d6eb4664 | ||
|
|
524b0205e9 | ||
|
|
1e432b25ea | ||
|
|
d6ea9d78f6 | ||
|
|
28edf4af2e | ||
|
|
d288131a33 | ||
|
|
e02d94ad2f | ||
|
|
4f67f4e096 | ||
|
|
00d164b67f | ||
|
|
c5179c2a06 | ||
|
|
95ac0ddfb7 | ||
|
|
2e31e8f0c9 | ||
|
|
169f8c67fe | ||
|
|
b706e85efb | ||
|
|
7c7f62d3f3 | ||
|
|
231e31656a | ||
|
|
4590c3a3db | ||
|
|
e9fe4c0466 | ||
|
|
6282cd7e7b | ||
|
|
bc453b5f0d | ||
|
|
0ec1c69c06 | ||
|
|
193a45113c | ||
|
|
3e7582880e | ||
|
|
c63f1ea62a | ||
|
|
1a375a44e0 | ||
|
|
be032cf735 | ||
|
|
4875f6aa87 | ||
|
|
0a2f06b296 | ||
|
|
6a2662eeee | ||
|
|
a64f80d2d4 | ||
|
|
1898dea314 | ||
|
|
81411b2af9 | ||
|
|
c4b620d69e | ||
|
|
c04befac68 | ||
|
|
fd36a99a4c | ||
|
|
4b09746c37 | ||
|
|
870cf4f3fe | ||
|
|
1ff35f177d | ||
|
|
f3edb1cc5f | ||
|
|
86dc38e69f | ||
|
|
a2b69c8f3f | ||
|
|
4cfd7c50ad | ||
|
|
a96606e589 | ||
|
|
30aefe19b5 | ||
|
|
37184f456c | ||
|
|
05456024c4 | ||
|
|
5accf3fe5f | ||
|
|
2dd27b4cb8 | ||
|
|
af28885ea6 | ||
|
|
f21ba53609 | ||
|
|
181e0ea6c8 | ||
|
|
6074ca275b | ||
|
|
d0d5461a67 | ||
|
|
8608ae2f08 | ||
|
|
401f08db63 | ||
|
|
caa299b60d | ||
|
|
dcde596002 | ||
|
|
ee14db20f1 | ||
|
|
27ba64c7e4 | ||
|
|
c7753f2cf9 | ||
|
|
974d4cb8fc | ||
|
|
29b6115c77 | ||
|
|
28e91ba70c | ||
|
|
5aded14b87 | ||
|
|
a288351ad4 | ||
|
|
3b39482420 | ||
|
|
a5c35af81b | ||
|
|
b78bdaf46e | ||
|
|
aefef5b0e8 | ||
|
|
ca998240a0 | ||
|
|
d2a31477c7 | ||
|
|
9c6581273e | ||
|
|
d1445a8135 | ||
|
|
5fc00acc39 | ||
|
|
363cdd02df | ||
|
|
a5d1d3b90e | ||
|
|
aaa6c279a1 | ||
|
|
498becd11f | ||
|
|
411c61fb02 | ||
|
|
74a0bacdcf | ||
|
|
e7e7b0ffb7 | ||
|
|
fd339a7433 | ||
|
|
561b6343c8 | ||
|
|
e68d72c4d6 | ||
|
|
737a8f5965 | ||
|
|
c0dd29c591 | ||
|
|
8aa5675ba2 | ||
|
|
2086648c87 | ||
|
|
3a98d856a5 | ||
|
|
90fd1f7dd1 | ||
|
|
77ad73d579 | ||
|
|
13534fa0c0 | ||
|
|
37299abcc9 | ||
|
|
e480f6527e | ||
|
|
26584f2060 | ||
|
|
a4c0fd1fdc | ||
|
|
6c104c016e | ||
|
|
599670136d | ||
|
|
ce637d318d | ||
|
|
d8654b5a19 | ||
|
|
12c3ccd6c7 | ||
|
|
7954656610 | ||
|
|
87ebf03eb7 | ||
|
|
df1c34e291 | ||
|
|
4a65b58772 | ||
|
|
11fdb8854b | ||
|
|
0a92bb1607 | ||
|
|
5bac2cbdb8 | ||
|
|
511c3e95b2 |
@@ -1,2 +1,3 @@
|
||||
**
|
||||
!release
|
||||
!release-packages
|
||||
!ci
|
||||
|
||||
@@ -21,3 +21,11 @@ extends:
|
||||
rules:
|
||||
# For overloads.
|
||||
no-dupe-class-members: off
|
||||
"@typescript-eslint/no-use-before-define": off
|
||||
"@typescript-eslint/no-non-null-assertion": off
|
||||
|
||||
settings:
|
||||
# Does not work with CommonJS unfortunately.
|
||||
import/ignore:
|
||||
- env-paths
|
||||
- xdg-basedir
|
||||
|
||||
12
.github/issue_template.md
vendored
12
.github/issue_template.md
vendored
@@ -2,5 +2,17 @@
|
||||
Please file all questions and support requests at https://www.reddit.com/r/codeserver/
|
||||
The issue tracker is only for bugs.
|
||||
|
||||
Please see https://github.com/cdr/code-server/blob/master/doc/FAQ.md#how-do-i-debug-issues-with-code-server
|
||||
and include any logging information relevant to the issue.
|
||||
|
||||
Please search for existing issues before filing.
|
||||
|
||||
Please ensure you cannot reproduce on VS Code before filing.
|
||||
|
||||
Please fill in the issue template or we will close your issue!
|
||||
-->
|
||||
|
||||
- Web Browser:
|
||||
- Local OS:
|
||||
- Remote OS:
|
||||
- `code-server --version`:
|
||||
|
||||
148
.github/workflows/ci.yaml
vendored
Normal file
148
.github/workflows/ci.yaml
vendored
Normal file
@@ -0,0 +1,148 @@
|
||||
name: ci
|
||||
|
||||
on: [push, pull_request]
|
||||
|
||||
jobs:
|
||||
fmt:
|
||||
runs-on: ubuntu-latest
|
||||
steps:
|
||||
- uses: actions/checkout@v1
|
||||
- name: Run ./ci/steps/fmt.sh
|
||||
uses: ./ci/container
|
||||
with:
|
||||
args: ./ci/steps/fmt.sh
|
||||
|
||||
lint:
|
||||
runs-on: ubuntu-latest
|
||||
steps:
|
||||
- uses: actions/checkout@v1
|
||||
- name: Run ./ci/steps/lint.sh
|
||||
uses: ./ci/container
|
||||
with:
|
||||
args: ./ci/steps/lint.sh
|
||||
|
||||
test:
|
||||
runs-on: ubuntu-latest
|
||||
steps:
|
||||
- uses: actions/checkout@v1
|
||||
- name: Run ./ci/steps/test.sh
|
||||
uses: ./ci/container
|
||||
with:
|
||||
args: ./ci/steps/test.sh
|
||||
|
||||
release:
|
||||
runs-on: ubuntu-latest
|
||||
steps:
|
||||
- uses: actions/checkout@v1
|
||||
- name: Run ./ci/steps/release.sh
|
||||
uses: ./ci/container
|
||||
with:
|
||||
args: ./ci/steps/release.sh
|
||||
- name: Upload npm package artifact
|
||||
uses: actions/upload-artifact@v2
|
||||
with:
|
||||
name: npm-package
|
||||
path: ./release-npm-package
|
||||
|
||||
linux-amd64:
|
||||
needs: release
|
||||
runs-on: ubuntu-latest
|
||||
steps:
|
||||
- uses: actions/checkout@v1
|
||||
- name: Download npm package
|
||||
uses: actions/download-artifact@v2
|
||||
with:
|
||||
name: npm-package
|
||||
path: ./release-npm-package
|
||||
- name: Run ./ci/steps/release-static.sh
|
||||
uses: ./ci/container
|
||||
with:
|
||||
args: ./ci/steps/release-static.sh
|
||||
- name: Upload release artifacts
|
||||
uses: actions/upload-artifact@v2
|
||||
with:
|
||||
name: release-packages
|
||||
path: ./release-packages
|
||||
|
||||
linux-arm64:
|
||||
needs: release
|
||||
runs-on: ubuntu-arm64-latest
|
||||
steps:
|
||||
- uses: actions/checkout@v1
|
||||
- name: Download npm package
|
||||
uses: actions/download-artifact@v2
|
||||
with:
|
||||
name: npm-package
|
||||
path: ./release-npm-package
|
||||
- name: Run ./ci/steps/release-static.sh
|
||||
uses: ./ci/container/arm64
|
||||
with:
|
||||
args: ./ci/steps/release-static.sh
|
||||
- name: Upload release artifacts
|
||||
uses: actions/upload-artifact@v2
|
||||
with:
|
||||
name: release-packages
|
||||
path: ./release-packages
|
||||
|
||||
macos-amd64:
|
||||
needs: release
|
||||
runs-on: macos-latest
|
||||
steps:
|
||||
- uses: actions/checkout@v1
|
||||
- name: Download npm package
|
||||
uses: actions/download-artifact@v2
|
||||
with:
|
||||
name: npm-package
|
||||
path: ./release-npm-package
|
||||
- run: brew unlink node@12
|
||||
- run: brew install node
|
||||
- run: ./ci/steps/release-static.sh
|
||||
env:
|
||||
# Otherwise we get rate limited when fetching the ripgrep binary.
|
||||
# For whatever reason only MacOS needs it.
|
||||
GITHUB_TOKEN: ${{ secrets.GITHUB_TOKEN }}
|
||||
- name: Upload release artifacts
|
||||
uses: actions/upload-artifact@v2
|
||||
with:
|
||||
name: release-packages
|
||||
path: ./release-packages
|
||||
|
||||
docker-amd64:
|
||||
runs-on: ubuntu-latest
|
||||
needs: linux-amd64
|
||||
steps:
|
||||
- uses: actions/checkout@v1
|
||||
- name: Download release package
|
||||
uses: actions/download-artifact@v2
|
||||
with:
|
||||
name: release-packages
|
||||
path: ./release-packages
|
||||
- name: Run ./ci/steps/build-docker-image.sh
|
||||
uses: ./ci/container
|
||||
with:
|
||||
args: ./ci/steps/build-docker-image.sh
|
||||
- name: Upload release image
|
||||
uses: actions/upload-artifact@v2
|
||||
with:
|
||||
name: release-images
|
||||
path: ./release-images
|
||||
|
||||
docker-arm64:
|
||||
runs-on: ubuntu-arm64-latest
|
||||
needs: linux-arm64
|
||||
steps:
|
||||
- uses: actions/checkout@v1
|
||||
- name: Download release package
|
||||
uses: actions/download-artifact@v2
|
||||
with:
|
||||
name: release-packages
|
||||
path: ./release-packages
|
||||
- name: Run ./ci/steps/build-docker-image.sh
|
||||
uses: ./ci/container/arm64
|
||||
with:
|
||||
args: ./ci/steps/build-docker-image.sh
|
||||
- name: Upload release image
|
||||
uses: actions/upload-artifact@v2
|
||||
with:
|
||||
name: release-images
|
||||
path: ./release-images
|
||||
31
.github/workflows/publish.yaml
vendored
Normal file
31
.github/workflows/publish.yaml
vendored
Normal file
@@ -0,0 +1,31 @@
|
||||
name: publish
|
||||
|
||||
on:
|
||||
release:
|
||||
types: [published]
|
||||
|
||||
jobs:
|
||||
npm:
|
||||
runs-on: ubuntu-latest
|
||||
steps:
|
||||
- uses: actions/checkout@v1
|
||||
- name: Run ./ci/steps/publish-npm.sh
|
||||
uses: ./ci/container
|
||||
with:
|
||||
args: ./ci/steps/publish-npm.sh
|
||||
env:
|
||||
GITHUB_TOKEN: ${{ secrets.GITHUB_TOKEN }}
|
||||
NPM_TOKEN: ${{ secrets.NPM_TOKEN }}
|
||||
|
||||
docker:
|
||||
runs-on: ubuntu-latest
|
||||
steps:
|
||||
- uses: actions/checkout@v1
|
||||
- name: Run ./ci/steps/push-docker-manifest.sh
|
||||
uses: ./ci/container
|
||||
with:
|
||||
args: ./ci/steps/push-docker-manifest.sh
|
||||
env:
|
||||
GITHUB_TOKEN: ${{ secrets.GITHUB_TOKEN }}
|
||||
DOCKER_USERNAME: ${{ secrets.DOCKER_USERNAME }}
|
||||
DOCKER_PASSWORD: ${{ secrets.DOCKER_PASSWORD }}
|
||||
11
.gitignore
vendored
11
.gitignore
vendored
@@ -1,8 +1,11 @@
|
||||
*.tsbuildinfo
|
||||
.tsbuildinfo
|
||||
.cache
|
||||
build
|
||||
dist*
|
||||
out*
|
||||
release*
|
||||
release/
|
||||
release-npm-package/
|
||||
release-static/
|
||||
release-packages/
|
||||
release-gcp/
|
||||
release-images/
|
||||
node_modules
|
||||
binaries
|
||||
|
||||
55
.travis.yml
55
.travis.yml
@@ -1,55 +0,0 @@
|
||||
language: minimal
|
||||
|
||||
jobs:
|
||||
include:
|
||||
- name: Test
|
||||
if: tag IS blank
|
||||
script: ./ci/image/run.sh "yarn && GITHUB_TOKEN=3229b0eec0a1622d6d1d1e00fca5b626070f5a10 yarn vscode && ./ci/ci.sh"
|
||||
deploy: null
|
||||
- name: Linux Release
|
||||
if: tag IS present
|
||||
script:
|
||||
- travis_wait 60 ./ci/image/run.sh "yarn && yarn vscode && ci/release.sh"
|
||||
- ./ci/release-image/push.sh
|
||||
- name: Linux ARM64 Release
|
||||
if: tag IS present
|
||||
script:
|
||||
- ./ci/image/run.sh "yarn && yarn vscode && ci/release.sh"
|
||||
- ./ci/release-image/push.sh
|
||||
arch: arm64
|
||||
- name: MacOS Release
|
||||
if: tag IS present
|
||||
os: osx
|
||||
language: node_js
|
||||
node_js: 12
|
||||
script: yarn && yarn vscode && travis_wait 60 ci/release.sh
|
||||
|
||||
before_deploy:
|
||||
- echo "$JSON_KEY" | base64 --decode > ./ci/key.json
|
||||
|
||||
deploy:
|
||||
- provider: releases
|
||||
edge: true
|
||||
draft: true
|
||||
tag_name: $TRAVIS_TAG
|
||||
target_commitish: $TRAVIS_COMMIT
|
||||
name: $TRAVIS_TAG
|
||||
file:
|
||||
- release/*.tar.gz
|
||||
- release/*.zip
|
||||
on:
|
||||
tags: true
|
||||
- provider: gcs
|
||||
edge: true
|
||||
bucket: "codesrv-ci.cdr.sh"
|
||||
upload_dir: "releases"
|
||||
key_file: ./ci/key.json
|
||||
local_dir: release-upload
|
||||
on:
|
||||
tags: true
|
||||
|
||||
cache:
|
||||
timeout: 600
|
||||
yarn: true
|
||||
directories:
|
||||
- lib/vscode/.build/extensions
|
||||
108
README.md
108
README.md
@@ -1,48 +1,96 @@
|
||||
# code-server
|
||||
|
||||
`code-server` is [VS Code](https://github.com/Microsoft/vscode) running on a
|
||||
remote server, accessible through the browser.
|
||||
Run [VS Code](https://github.com/Microsoft/vscode) on any machine anywhere and access it in the browser.
|
||||
|
||||
Try it out:
|
||||
|
||||
```bash
|
||||
docker run -it -p 127.0.0.1:8080:8080 -v "$PWD:/home/coder/project" codercom/code-server
|
||||
```
|
||||
|
||||
- **Code anywhere:** Code on your Chromebook, tablet, and laptop with a
|
||||
- **Code everywhere:** Code on your Chromebook, tablet, and laptop with a
|
||||
consistent dev 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
|
||||
since all intensive computation runs on your server.
|
||||
- **Server-powered:** Take advantage of large cloud servers to speed up tests, compilations, downloads, and more.
|
||||
Preserve battery life when you're on the go since all intensive tasks runs on your server.
|
||||
Make use of a spare computer you have lying around and turn it into a full development environment.
|
||||
|
||||

|
||||

|
||||
|
||||
## Getting Started
|
||||
|
||||
### Requirements
|
||||
For a full setup and walkthrough, please see [./doc/guide.md](./doc/guide.md).
|
||||
|
||||
- 64-bit host.
|
||||
- At least 1GB of RAM.
|
||||
- 2 cores or more are recommended (1 core works but not optimally).
|
||||
- Secure connection over HTTPS or localhost (required for service workers and
|
||||
clipboard support).
|
||||
- For Linux: GLIBC 2.17 or later and GLIBCXX 3.4.15 or later.
|
||||
### Debian, Ubuntu
|
||||
|
||||
### Run over SSH
|
||||
```bash
|
||||
curl -sSOL https://github.com/cdr/code-server/releases/download/v3.3.1/code-server_3.3.1_amd64.deb
|
||||
sudo dpkg -i code-server_3.3.1_amd64.deb
|
||||
systemctl --user enable --now code-server
|
||||
# Now visit http://127.0.0.1:8080. Your password is in ~/.config/code-server/config.yaml
|
||||
```
|
||||
|
||||
Use [sshcode](https://github.com/codercom/sshcode) for a simple setup.
|
||||
### Fedora, Red Hat, SUSE
|
||||
|
||||
### Digital Ocean
|
||||
```bash
|
||||
curl -sSOL https://github.com/cdr/code-server/releases/download/v3.3.1/code-server-3.3.1-amd64.rpm
|
||||
sudo yum install -y code-server-3.3.1-amd64.rpm
|
||||
systemctl --user enable --now code-server
|
||||
# Now visit http://127.0.0.1:8080. Your password is in ~/.config/code-server/config.yaml
|
||||
```
|
||||
|
||||
[](https://marketplace.digitalocean.com/apps/code-server)
|
||||
### npm
|
||||
|
||||
### Releases
|
||||
We recommend installing from `npm` if we don't have a precompiled release for your machine's
|
||||
platform or architecture or your glibc < v2.19.
|
||||
|
||||
1. [Download a release](https://github.com/cdr/code-server/releases). (Linux and
|
||||
OS X supported. Windows support planned.)
|
||||
2. Unpack the downloaded release then run the included `code-server` script.
|
||||
3. In your browser navigate to `localhost:8080`.
|
||||
**note:** Installing via `npm` builds native modules on install and so requires C dependencies.
|
||||
See [./doc/npm.md](./doc/npm.md) for installing these dependencies.
|
||||
|
||||
You will need at least node v12 installed. See [#1633](https://github.com/cdr/code-server/issues/1633).
|
||||
|
||||
```bash
|
||||
npm install -g code-server
|
||||
code-server
|
||||
# Now visit http://127.0.0.1:8080. Your password is in ~/.config/code-server/config.yaml
|
||||
```
|
||||
|
||||
### macOS
|
||||
|
||||
```bash
|
||||
brew install code-server
|
||||
brew services start code-server
|
||||
# Now visit http://127.0.0.1:8080. Your password is in ~/.config/code-server/config.yaml
|
||||
```
|
||||
|
||||
### Docker
|
||||
|
||||
```bash
|
||||
# This will start a code-server container and expose it at http://127.0.0.1:8080.
|
||||
# It will also mount your current directory into the container as `/home/coder/project`
|
||||
# and forward your UID/GID so that all file system operations occur as your user outside
|
||||
# the container.
|
||||
docker run -it -p 127.0.0.1:8080:8080 \
|
||||
-v "$PWD:/home/coder/project" \
|
||||
-u "$(id -u):$(id -g)" \
|
||||
codercom/code-server:latest
|
||||
```
|
||||
|
||||
### Static Releases
|
||||
|
||||
We publish self contained `.tar.gz` archives for every release on [github](https://github.com/cdr/code-server/releases).
|
||||
They bundle the node binary and node_modules.
|
||||
|
||||
1. Download the latest release archive for your system from [github](https://github.com/cdr/code-server/releases).
|
||||
2. Unpack the release.
|
||||
3. You can run code-server by executing `./bin/code-server`.
|
||||
|
||||
Add the code-server `bin` directory to your `$PATH` to easily execute `code-server` without the full path every time.
|
||||
|
||||
Here is an example script for installing and using a static `code-server` release on Linux:
|
||||
|
||||
```bash
|
||||
curl -sSL https://github.com/cdr/code-server/releases/download/v3.3.1/code-server-3.3.1-linux-amd64.tar.gz \
|
||||
| sudo tar -C /usr/local -xz
|
||||
sudo mv /usr/local/code-server-3.3.1-linux-amd64 /usr/local/code-server-3.3.1
|
||||
PATH="/usr/local/code-server-3.3.1/bin:$PATH"
|
||||
code-server
|
||||
# Now visit http://127.0.0.1:8080. Your password is in ~/.config/code-server/config.yaml
|
||||
```
|
||||
|
||||
## FAQ
|
||||
|
||||
@@ -54,5 +102,5 @@ See [./doc/CONTRIBUTING.md](./doc/CONTRIBUTING.md).
|
||||
|
||||
## Enterprise
|
||||
|
||||
Visit [our enterprise page](https://coder.com) for more information about our
|
||||
Visit [our website](https://coder.com) for more information about our
|
||||
enterprise offerings.
|
||||
|
||||
137
ci/README.md
Normal file
137
ci/README.md
Normal file
@@ -0,0 +1,137 @@
|
||||
# ci
|
||||
|
||||
This directory contains scripts used for code-server's continuous integration infrastructure.
|
||||
|
||||
Some of these scripts contain more detailed documentation and options
|
||||
in header comments.
|
||||
|
||||
Any file or directory in this subdirectory should be documented here.
|
||||
|
||||
- [./ci/lib.sh](./lib.sh)
|
||||
- Contains code duplicated across these scripts.
|
||||
|
||||
## Publishing a release
|
||||
|
||||
Make sure you have `$GITHUB_TOKEN` set and [hub](https://github.com/github/hub) installed.
|
||||
|
||||
1. Update the version of code-server in `package.json` and README.md/guide.md install examples and make a PR.
|
||||
2. GitHub actions will generate the `npm-package`, `release-packages` and `release-images` artifacts.
|
||||
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 then
|
||||
upload them to the draft release.
|
||||
6. Run some basic sanity tests on one of the released packages.
|
||||
7. Make sure the github release tag is the commit with the artifacts.
|
||||
8. Publish the release and merge the PR.
|
||||
1. CI will automatically grab the artifacts and then:
|
||||
1. Publish the NPM package from `npm-package`.
|
||||
2. Publish the Docker Hub image from `release-images`.
|
||||
9. Update the homebrew and AUR packages.
|
||||
|
||||
## dev
|
||||
|
||||
This directory contains scripts used for the development of code-server.
|
||||
|
||||
- [./ci/dev/container](./dev/container)
|
||||
- See [./doc/CONTRIBUTING.md](../doc/CONTRIBUTING.md) for docs on the development container.
|
||||
- [./ci/dev/fmt.sh](./dev/fmt.sh) (`yarn fmt`)
|
||||
- Runs formatters.
|
||||
- [./ci/dev/lint.sh](./dev/lint.sh) (`yarn lint`)
|
||||
- Runs linters.
|
||||
- [./ci/dev/test.sh](./dev/test.sh) (`yarn test`)
|
||||
- Runs tests.
|
||||
- [./ci/dev/ci.sh](./dev/ci.sh) (`yarn ci`)
|
||||
- Runs `yarn fmt`, `yarn lint` and `yarn test`.
|
||||
- [./ci/dev/vscode.sh](./dev/vscode.sh) (`yarn vscode`)
|
||||
- Ensures [./lib/vscode](../lib/vscode) is cloned, patched and dependencies are installed.
|
||||
- [./ci/dev/patch-vscode.sh](./dev/patch-vscode.sh) (`yarn vscode:patch`)
|
||||
- Applies [./ci/dev/vscode.patch](./dev/vscode.patch) to [./lib/vscode](../lib/vscode).
|
||||
- [./ci/dev/diff-vscode.sh](./dev/diff-vscode.sh) (`yarn vscode:diff`)
|
||||
- Diffs [./lib/vscode](../lib/vscode) into [./ci/dev/vscode.patch](./dev/vscode.patch).
|
||||
- [./ci/dev/vscode.patch](./dev/vscode.patch)
|
||||
- Our patch of VS Code, see [./doc/CONTRIBUTING.md](../doc/CONTRIBUTING.md#vs-code-patch).
|
||||
- Generate it with `yarn vscode:diff` and apply with `yarn vscode:patch`.
|
||||
- [./ci/dev/watch.ts](./dev/watch.ts) (`yarn watch`)
|
||||
- Starts a process to build and launch code-server and restart on any code changes.
|
||||
- Example usage in [./doc/CONTRIBUTING.md](../doc/CONTRIBUTING.md).
|
||||
|
||||
## build
|
||||
|
||||
This directory contains the scripts used to build and release code-server.
|
||||
You can disable minification by setting `MINIFY=`.
|
||||
|
||||
- [./ci/build/build-code-server.sh](./build/build-code-server.sh) (`yarn build`)
|
||||
- Builds code-server into `./out` and bundles the frontend into `./dist`.
|
||||
- [./ci/build/build-vscode.sh](./build/build-vscode.sh) (`yarn build:vscode`)
|
||||
- Builds vscode into `./lib/vscode/out-vscode`.
|
||||
- [./ci/build/build-release.sh](./build/build-release.sh) (`yarn release`)
|
||||
- Bundles the output of the above two scripts into a single node module at `./release`.
|
||||
- [./ci/build/build-static-release.sh](./build/build-static-release.sh) (`yarn release:static`)
|
||||
- Requires a node module already built into `./release` with the above script.
|
||||
- Will build a static release with node and node_modules bundled into `./release-static`.
|
||||
- [./ci/build/clean.sh](./build/clean.sh) (`yarn clean`)
|
||||
- Removes all build artifacts.
|
||||
- Will also `git reset --hard lib/vscode`.
|
||||
- Useful to do a clean build.
|
||||
- [./ci/build/code-server.sh](./build/code-server.sh)
|
||||
- Copied into static releases to run code-server with the bundled node binary.
|
||||
- [./ci/build/test-static-release.sh](./build/test-static-release.sh) (`yarn test:static-release`)
|
||||
- Ensures code-server in the `./release-static` directory works by installing an extension.
|
||||
- [./ci/build/build-packages.sh](./build/build-packages.sh) (`yarn package`)
|
||||
- Packages `./release-static` into a `.tar.gz` archive in `./release-packages`.
|
||||
- If on linux, [nfpm](https://github.com/goreleaser/nfpm) is used to generate `.deb` and `.rpm`.
|
||||
- [./ci/build/nfpm.yaml](./build/nfpm.yaml)
|
||||
- Used to configure [nfpm](https://github.com/goreleaser/nfpm) to generate `.deb` and `.rpm`.
|
||||
- [./ci/build/code-server-nfpm.sh](./build/code-server-nfpm.sh)
|
||||
- Entrypoint script for code-server for `.deb` and .rpm`.
|
||||
- [./ci/build/code-server.service](./build/code-server.service)
|
||||
- systemd user service packaged into the `.deb` and `.rpm`.
|
||||
- [./ci/build/release-github-draft.sh](./build/release-github-draft.sh) (`yarn release:github-draft`)
|
||||
- Uses [hub](https://github.com/github/hub) to create a draft release with a template description.
|
||||
- [./ci/build/release-github-assets.sh](./build/release-github-assets.sh) (`yarn release:github-assets`)
|
||||
- Downloads the release-package artifacts for the current commit from CI.
|
||||
- Uses [hub](https://github.com/github/hub) to upload the artifacts to the release
|
||||
specified in `package.json`.
|
||||
- [./ci/build/npm-postinstall.sh](./build/npm-postinstall.sh)
|
||||
- Post install script for the npm package.
|
||||
- Bundled by`yarn release`.
|
||||
|
||||
## release-container
|
||||
|
||||
This directory contains the release docker container.
|
||||
|
||||
- [./release-container/build.sh](./release-container/build.sh)
|
||||
- Builds the release container with the tag `codercom/code-server-$ARCH:$VERSION`.
|
||||
- Assumes debian releases are ready in `./release-packages`.
|
||||
|
||||
## container
|
||||
|
||||
This directory contains the container for CI.
|
||||
|
||||
## steps
|
||||
|
||||
This directory contains the scripts used in CI.
|
||||
Helps avoid clobbering the CI configuration.
|
||||
|
||||
- [./steps/fmt.sh](./steps/fmt.sh)
|
||||
- Runs `yarn fmt` after ensuring VS Code is patched.
|
||||
- [./steps/lint.sh](./steps/lint.sh)
|
||||
- Runs `yarn lint` after ensuring VS Code is patched.
|
||||
- [./steps/test.sh](./steps/test.sh)
|
||||
- Runs `yarn test` after ensuring VS Code is patched.
|
||||
- [./steps/release.sh](./steps/release.sh)
|
||||
- Runs the release process.
|
||||
- Generates the npm package at `./release`.
|
||||
- [./steps/release-static.sh](./steps/release-static.sh)
|
||||
- Takes the output of the previous script and generates a static release and
|
||||
release packages into `release-packages`.
|
||||
- [./steps/publish-npm.sh](./steps/publish-npm.sh)
|
||||
- Grabs the `npm-package` release artifact for the current commit and publishes it on npm.
|
||||
- [./steps/build-docker-image.sh](./steps/build-docker-image.sh)
|
||||
- Builds the docker image and then saves it into `./release-images/code-server-$ARCH-$VERSION.tar`.
|
||||
- [./steps/push-docker-manifest.sh](./steps/push-docker-manifest.sh)
|
||||
- Loads all images in `./release-images` and then builds and pushes a multi architecture
|
||||
docker manifest for the amd64 and arm64 images to `codercom/code-server:$VERSION` and
|
||||
`codercom/code-server:latest`.
|
||||
372
ci/build.ts
372
ci/build.ts
@@ -1,372 +0,0 @@
|
||||
import * as cp from "child_process"
|
||||
import * as fs from "fs-extra"
|
||||
import Bundler from "parcel-bundler"
|
||||
import * as path from "path"
|
||||
import * as util from "util"
|
||||
|
||||
enum Task {
|
||||
Build = "build",
|
||||
Watch = "watch",
|
||||
}
|
||||
|
||||
class Builder {
|
||||
private readonly rootPath = path.resolve(__dirname, "..")
|
||||
private readonly vscodeSourcePath = path.join(this.rootPath, "lib/vscode")
|
||||
private readonly buildPath = path.join(this.rootPath, "build")
|
||||
private readonly codeServerVersion: string
|
||||
private currentTask?: Task
|
||||
|
||||
public constructor() {
|
||||
this.ensureArgument("rootPath", this.rootPath)
|
||||
this.codeServerVersion = this.ensureArgument(
|
||||
"codeServerVersion",
|
||||
process.env.VERSION || require(path.join(this.rootPath, "package.json")).version,
|
||||
)
|
||||
}
|
||||
|
||||
public run(task: Task | undefined): void {
|
||||
this.currentTask = task
|
||||
this.doRun(task).catch((error) => {
|
||||
console.error(error.message)
|
||||
process.exit(1)
|
||||
})
|
||||
}
|
||||
|
||||
private async task<T>(message: string, fn: () => Promise<T>): Promise<T> {
|
||||
const time = Date.now()
|
||||
this.log(`${message}...`, !process.env.CI)
|
||||
try {
|
||||
const t = await fn()
|
||||
process.stdout.write(`took ${Date.now() - time}ms\n`)
|
||||
return t
|
||||
} catch (error) {
|
||||
process.stdout.write("failed\n")
|
||||
throw error
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* Writes to stdout with an optional newline.
|
||||
*/
|
||||
private log(message: string, skipNewline = false): void {
|
||||
process.stdout.write(`[${this.currentTask || "default"}] ${message}`)
|
||||
if (!skipNewline) {
|
||||
process.stdout.write("\n")
|
||||
}
|
||||
}
|
||||
|
||||
private async doRun(task: Task | undefined): Promise<void> {
|
||||
if (!task) {
|
||||
throw new Error("No task provided")
|
||||
}
|
||||
|
||||
switch (task) {
|
||||
case Task.Watch:
|
||||
return this.watch()
|
||||
case Task.Build:
|
||||
return this.build()
|
||||
default:
|
||||
throw new Error(`No task matching "${task}"`)
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* Make sure the argument is set. Display the value if it is.
|
||||
*/
|
||||
private ensureArgument(name: string, arg?: string): string {
|
||||
if (!arg) {
|
||||
throw new Error(`${name} is missing`)
|
||||
}
|
||||
this.log(`${name} is "${arg}"`)
|
||||
return arg
|
||||
}
|
||||
|
||||
/**
|
||||
* Build VS Code and code-server.
|
||||
*/
|
||||
private async build(): Promise<void> {
|
||||
process.env.NODE_OPTIONS = "--max-old-space-size=32384 " + (process.env.NODE_OPTIONS || "")
|
||||
process.env.NODE_ENV = "production"
|
||||
|
||||
await this.task("cleaning up old build", async () => {
|
||||
if (!process.env.SKIP_VSCODE) {
|
||||
return fs.remove(this.buildPath)
|
||||
}
|
||||
// If skipping VS Code, keep the existing build if any.
|
||||
try {
|
||||
const files = await fs.readdir(this.buildPath)
|
||||
return Promise.all(files.filter((f) => f !== "lib").map((f) => fs.remove(path.join(this.buildPath, f))))
|
||||
} catch (error) {
|
||||
if (error.code !== "ENOENT") {
|
||||
throw error
|
||||
}
|
||||
}
|
||||
})
|
||||
|
||||
const commit = require(path.join(this.vscodeSourcePath, "build/lib/util")).getVersion(this.rootPath) as string
|
||||
if (!process.env.SKIP_VSCODE) {
|
||||
await this.buildVscode(commit)
|
||||
} else {
|
||||
this.log("skipping vs code build")
|
||||
}
|
||||
await this.buildCodeServer(commit)
|
||||
|
||||
this.log(`final build: ${this.buildPath}`)
|
||||
}
|
||||
|
||||
private async buildCodeServer(commit: string): Promise<void> {
|
||||
await this.task("building code-server", async () => {
|
||||
return util.promisify(cp.exec)("tsc --outDir ./out-build --tsBuildInfoFile ./.prod.tsbuildinfo", {
|
||||
cwd: this.rootPath,
|
||||
})
|
||||
})
|
||||
|
||||
await this.task("bundling code-server", async () => {
|
||||
return this.createBundler("dist-build", commit).bundle()
|
||||
})
|
||||
|
||||
await this.task("copying code-server into build directory", async () => {
|
||||
await fs.mkdirp(this.buildPath)
|
||||
await Promise.all([
|
||||
fs.copy(path.join(this.rootPath, "out-build"), path.join(this.buildPath, "out")),
|
||||
fs.copy(path.join(this.rootPath, "dist-build"), path.join(this.buildPath, "dist")),
|
||||
// For source maps and images.
|
||||
fs.copy(path.join(this.rootPath, "src"), path.join(this.buildPath, "src")),
|
||||
])
|
||||
})
|
||||
|
||||
await this.copyDependencies("code-server", this.rootPath, this.buildPath, false, {
|
||||
commit,
|
||||
version: this.codeServerVersion,
|
||||
})
|
||||
}
|
||||
|
||||
private async buildVscode(commit: string): Promise<void> {
|
||||
await this.task("building vs code", () => {
|
||||
return util.promisify(cp.exec)("yarn gulp compile-build", { cwd: this.vscodeSourcePath })
|
||||
})
|
||||
|
||||
await this.task("building builtin extensions", async () => {
|
||||
const exists = await fs.pathExists(path.join(this.vscodeSourcePath, ".build/extensions"))
|
||||
if (exists && !process.env.CI) {
|
||||
process.stdout.write("already built, skipping...")
|
||||
} else {
|
||||
await util.promisify(cp.exec)("yarn gulp compile-extensions-build", { cwd: this.vscodeSourcePath })
|
||||
}
|
||||
})
|
||||
|
||||
await this.task("optimizing vs code", async () => {
|
||||
return util.promisify(cp.exec)("yarn gulp optimize --gulpfile ./coder.js", { cwd: this.vscodeSourcePath })
|
||||
})
|
||||
|
||||
if (process.env.MINIFY) {
|
||||
await this.task("minifying vs code", () => {
|
||||
return util.promisify(cp.exec)("yarn gulp minify --gulpfile ./coder.js", { cwd: this.vscodeSourcePath })
|
||||
})
|
||||
}
|
||||
|
||||
const vscodeBuildPath = path.join(this.buildPath, "lib/vscode")
|
||||
await this.task("copying vs code into build directory", async () => {
|
||||
await fs.mkdirp(path.join(vscodeBuildPath, "resources/linux"))
|
||||
await Promise.all([
|
||||
fs.move(
|
||||
path.join(this.vscodeSourcePath, `out-vscode${process.env.MINIFY ? "-min" : ""}`),
|
||||
path.join(vscodeBuildPath, "out"),
|
||||
),
|
||||
fs.copy(path.join(this.vscodeSourcePath, ".build/extensions"), path.join(vscodeBuildPath, "extensions")),
|
||||
fs.copy(
|
||||
path.join(this.vscodeSourcePath, "resources/linux/code.png"),
|
||||
path.join(vscodeBuildPath, "resources/linux/code.png"),
|
||||
),
|
||||
])
|
||||
})
|
||||
|
||||
await this.copyDependencies("vs code", this.vscodeSourcePath, vscodeBuildPath, true, {
|
||||
commit,
|
||||
date: new Date().toISOString(),
|
||||
})
|
||||
}
|
||||
|
||||
private async copyDependencies(
|
||||
name: string,
|
||||
sourcePath: string,
|
||||
buildPath: string,
|
||||
ignoreScripts: boolean,
|
||||
merge: object,
|
||||
): Promise<void> {
|
||||
await this.task(`copying ${name} dependencies`, async () => {
|
||||
return Promise.all(
|
||||
["node_modules", "package.json", "yarn.lock"].map((fileName) => {
|
||||
return fs.copy(path.join(sourcePath, fileName), path.join(buildPath, fileName))
|
||||
}),
|
||||
)
|
||||
})
|
||||
|
||||
const fileName = name === "code-server" ? "package" : "product"
|
||||
await this.task(`writing final ${name} ${fileName}.json`, async () => {
|
||||
const json = JSON.parse(await fs.readFile(path.join(sourcePath, `${fileName}.json`), "utf8"))
|
||||
return fs.writeFile(
|
||||
path.join(buildPath, `${fileName}.json`),
|
||||
JSON.stringify(
|
||||
{
|
||||
...json,
|
||||
...merge,
|
||||
},
|
||||
null,
|
||||
2,
|
||||
),
|
||||
)
|
||||
})
|
||||
|
||||
if (process.env.MINIFY) {
|
||||
await this.task(`restricting ${name} to production dependencies`, async () => {
|
||||
await util.promisify(cp.exec)(`yarn --production ${ignoreScripts ? "--ignore-scripts" : ""}`, {
|
||||
cwd: buildPath,
|
||||
})
|
||||
})
|
||||
}
|
||||
}
|
||||
|
||||
private async watch(): Promise<void> {
|
||||
let server: cp.ChildProcess | undefined
|
||||
const restartServer = (): void => {
|
||||
if (server) {
|
||||
server.kill()
|
||||
}
|
||||
const s = cp.fork(path.join(this.rootPath, "out/node/entry.js"), process.argv.slice(3))
|
||||
console.log(`[server] spawned process ${s.pid}`)
|
||||
s.on("exit", () => console.log(`[server] process ${s.pid} exited`))
|
||||
server = s
|
||||
}
|
||||
|
||||
const vscode = cp.spawn("yarn", ["watch"], { cwd: this.vscodeSourcePath })
|
||||
const tsc = cp.spawn("tsc", ["--watch", "--pretty", "--preserveWatchOutput"], { cwd: this.rootPath })
|
||||
const bundler = this.createBundler()
|
||||
|
||||
const cleanup = (code?: number | null): void => {
|
||||
this.log("killing vs code watcher")
|
||||
vscode.removeAllListeners()
|
||||
vscode.kill()
|
||||
|
||||
this.log("killing tsc")
|
||||
tsc.removeAllListeners()
|
||||
tsc.kill()
|
||||
|
||||
if (server) {
|
||||
this.log("killing server")
|
||||
server.removeAllListeners()
|
||||
server.kill()
|
||||
}
|
||||
|
||||
this.log("killing bundler")
|
||||
process.exit(code || 0)
|
||||
}
|
||||
|
||||
process.on("SIGINT", () => cleanup())
|
||||
process.on("SIGTERM", () => cleanup())
|
||||
|
||||
vscode.on("exit", (code) => {
|
||||
this.log("vs code watcher terminated unexpectedly")
|
||||
cleanup(code)
|
||||
})
|
||||
tsc.on("exit", (code) => {
|
||||
this.log("tsc terminated unexpectedly")
|
||||
cleanup(code)
|
||||
})
|
||||
const bundle = bundler.bundle().catch(() => {
|
||||
this.log("parcel watcher terminated unexpectedly")
|
||||
cleanup(1)
|
||||
})
|
||||
bundler.on("buildEnd", () => {
|
||||
console.log("[parcel] bundled")
|
||||
})
|
||||
bundler.on("buildError", (error) => {
|
||||
console.error("[parcel]", error)
|
||||
})
|
||||
|
||||
vscode.stderr.on("data", (d) => process.stderr.write(d))
|
||||
tsc.stderr.on("data", (d) => process.stderr.write(d))
|
||||
|
||||
// From https://github.com/chalk/ansi-regex
|
||||
const pattern = [
|
||||
"[\\u001B\\u009B][[\\]()#;?]*(?:(?:(?:[a-zA-Z\\d]*(?:;[-a-zA-Z\\d\\/#&.:=?%@~_]*)*)?\\u0007)",
|
||||
"(?:(?:\\d{1,4}(?:;\\d{0,4})*)?[\\dA-PR-TZcf-ntqry=><~]))",
|
||||
].join("|")
|
||||
const re = new RegExp(pattern, "g")
|
||||
|
||||
/**
|
||||
* Split stdout on newlines and strip ANSI codes.
|
||||
*/
|
||||
const onLine = (proc: cp.ChildProcess, callback: (strippedLine: string, originalLine: string) => void): void => {
|
||||
let buffer = ""
|
||||
if (!proc.stdout) {
|
||||
throw new Error("no stdout")
|
||||
}
|
||||
proc.stdout.setEncoding("utf8")
|
||||
proc.stdout.on("data", (d) => {
|
||||
const data = buffer + d
|
||||
const split = data.split("\n")
|
||||
const last = split.length - 1
|
||||
|
||||
for (let i = 0; i < last; ++i) {
|
||||
callback(split[i].replace(re, ""), split[i])
|
||||
}
|
||||
|
||||
// The last item will either be an empty string (the data ended with a
|
||||
// newline) or a partial line (did not end with a newline) and we must
|
||||
// wait to parse it until we get a full line.
|
||||
buffer = split[last]
|
||||
})
|
||||
}
|
||||
|
||||
let startingVscode = false
|
||||
let startedVscode = false
|
||||
onLine(vscode, (line, original) => {
|
||||
console.log("[vscode]", original)
|
||||
// Wait for watch-client since "Finished compilation" will appear multiple
|
||||
// times before the client starts building.
|
||||
if (!startingVscode && line.includes("Starting watch-client")) {
|
||||
startingVscode = true
|
||||
} else if (startingVscode && line.includes("Finished compilation")) {
|
||||
if (startedVscode) {
|
||||
bundle.then(restartServer)
|
||||
}
|
||||
startedVscode = true
|
||||
}
|
||||
})
|
||||
|
||||
onLine(tsc, (line, original) => {
|
||||
// tsc outputs blank lines; skip them.
|
||||
if (line !== "") {
|
||||
console.log("[tsc]", original)
|
||||
}
|
||||
if (line.includes("Watching for file changes")) {
|
||||
bundle.then(restartServer)
|
||||
}
|
||||
})
|
||||
}
|
||||
|
||||
private createBundler(out = "dist", commit?: string): Bundler {
|
||||
return new Bundler(
|
||||
[
|
||||
path.join(this.rootPath, "src/browser/pages/app.ts"),
|
||||
path.join(this.rootPath, "src/browser/register.ts"),
|
||||
path.join(this.rootPath, "src/browser/serviceWorker.ts"),
|
||||
],
|
||||
{
|
||||
cache: true,
|
||||
cacheDir: path.join(this.rootPath, ".cache"),
|
||||
detailedReport: true,
|
||||
minify: !!process.env.MINIFY,
|
||||
hmr: false,
|
||||
logLevel: 1,
|
||||
outDir: path.join(this.rootPath, out),
|
||||
publicUrl: `/static/${commit || "development"}/dist`,
|
||||
target: "browser",
|
||||
},
|
||||
)
|
||||
}
|
||||
}
|
||||
|
||||
const builder = new Builder()
|
||||
builder.run(process.argv[2] as Task)
|
||||
29
ci/build/build-code-server.sh
Executable file
29
ci/build/build-code-server.sh
Executable file
@@ -0,0 +1,29 @@
|
||||
#!/usr/bin/env bash
|
||||
set -euo pipefail
|
||||
|
||||
# Builds code-server into out and the frontend into dist.
|
||||
|
||||
# MINIFY controls whether parcel minifies dist.
|
||||
MINIFY=${MINIFY-true}
|
||||
|
||||
main() {
|
||||
cd "$(dirname "${0}")/../.."
|
||||
|
||||
npx tsc --outDir out --tsBuildInfoFile .cache/out.tsbuildinfo
|
||||
# If out/node/entry.js does not already have the shebang,
|
||||
# we make sure to add it and make it executable.
|
||||
if ! grep -q -m1 "^#!/usr/bin/env node" out/node/entry.js; then
|
||||
sed -i.bak "1s;^;#!/usr/bin/env node\n;" out/node/entry.js && rm out/node/entry.js.bak
|
||||
chmod +x out/node/entry.js
|
||||
fi
|
||||
|
||||
npx parcel build \
|
||||
--public-url "/static/$(git rev-parse HEAD)/dist" \
|
||||
--out-dir dist \
|
||||
$([[ $MINIFY ]] || echo --no-minify) \
|
||||
src/browser/pages/app.ts \
|
||||
src/browser/register.ts \
|
||||
src/browser/serviceWorker.ts
|
||||
}
|
||||
|
||||
main "$@"
|
||||
46
ci/build/build-packages.sh
Executable file
46
ci/build/build-packages.sh
Executable file
@@ -0,0 +1,46 @@
|
||||
#!/usr/bin/env bash
|
||||
set -euo pipefail
|
||||
|
||||
# Packages code-server for the current OS and architecture into ./release-packages.
|
||||
# This script assumes that a static release is built already into ./release-static.
|
||||
|
||||
main() {
|
||||
cd "$(dirname "${0}")/../.."
|
||||
source ./ci/lib.sh
|
||||
|
||||
local release_name="code-server-$VERSION-$OS-$ARCH"
|
||||
mkdir -p release-packages
|
||||
|
||||
if [[ $OS == "linux" ]]; then
|
||||
tar -czf "release-packages/$release_name.tar.gz" --transform "s/^\.\/release-static/$release_name/" ./release-static
|
||||
else
|
||||
tar -czf "release-packages/$release_name.tar.gz" -s "/^release-static/$release_name/" release-static
|
||||
fi
|
||||
|
||||
echo "done (release-packages/$release_name)"
|
||||
|
||||
release_gcp
|
||||
|
||||
if [[ $OSTYPE == linux* ]]; then
|
||||
release_nfpm
|
||||
fi
|
||||
}
|
||||
|
||||
release_gcp() {
|
||||
mkdir -p "release-gcp/$VERSION"
|
||||
cp "release-packages/$release_name.tar.gz" "./release-gcp/$VERSION/$OS-$ARCH.tar.gz"
|
||||
mkdir -p "release-gcp/latest"
|
||||
cp "./release-packages/$release_name.tar.gz" "./release-gcp/latest/$OS-$ARCH.tar.gz"
|
||||
}
|
||||
|
||||
# Generates deb and rpm packages.
|
||||
release_nfpm() {
|
||||
local nfpm_config
|
||||
nfpm_config=$(envsubst < ./ci/build/nfpm.yaml)
|
||||
|
||||
# The underscores are convention for .deb.
|
||||
nfpm pkg -f <(echo "$nfpm_config") --target "release-packages/code-server_${VERSION}_${ARCH}.deb"
|
||||
nfpm pkg -f <(echo "$nfpm_config") --target "release-packages/code-server-$VERSION-$ARCH.rpm"
|
||||
}
|
||||
|
||||
main "$@"
|
||||
80
ci/build/build-release.sh
Executable file
80
ci/build/build-release.sh
Executable file
@@ -0,0 +1,80 @@
|
||||
#!/usr/bin/env bash
|
||||
set -euo pipefail
|
||||
|
||||
# This script requires vscode to be built with matching MINIFY.
|
||||
|
||||
# MINIFY controls whether minified vscode is bundled.
|
||||
MINIFY="${MINIFY-true}"
|
||||
|
||||
main() {
|
||||
cd "$(dirname "${0}")/../.."
|
||||
source ./ci/lib.sh
|
||||
|
||||
VSCODE_SRC_PATH="lib/vscode"
|
||||
VSCODE_OUT_PATH="$RELEASE_PATH/lib/vscode"
|
||||
|
||||
mkdir -p "$RELEASE_PATH"
|
||||
|
||||
bundle_code_server
|
||||
bundle_vscode
|
||||
|
||||
rsync README.md "$RELEASE_PATH"
|
||||
rsync LICENSE.txt "$RELEASE_PATH"
|
||||
rsync ./lib/vscode/ThirdPartyNotices.txt "$RELEASE_PATH"
|
||||
}
|
||||
|
||||
bundle_code_server() {
|
||||
rsync out dist "$RELEASE_PATH"
|
||||
|
||||
# For source maps and images.
|
||||
mkdir -p "$RELEASE_PATH/src/browser"
|
||||
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"
|
||||
|
||||
# Adds the commit to package.json
|
||||
jq --slurp '.[0] * .[1]' package.json <(
|
||||
cat << EOF
|
||||
{
|
||||
"commit": "$(git rev-parse HEAD)",
|
||||
"scripts": {
|
||||
"postinstall": "./postinstall.sh"
|
||||
}
|
||||
}
|
||||
EOF
|
||||
) > "$RELEASE_PATH/package.json"
|
||||
rsync yarn.lock "$RELEASE_PATH"
|
||||
rsync ci/build/npm-postinstall.sh "$RELEASE_PATH/postinstall.sh"
|
||||
}
|
||||
|
||||
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/.build/extensions/" "$VSCODE_OUT_PATH/extensions"
|
||||
rm -Rf "$VSCODE_OUT_PATH/extensions/node_modules"
|
||||
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"
|
||||
|
||||
mkdir -p "$VSCODE_OUT_PATH/resources/linux"
|
||||
rsync "$VSCODE_SRC_PATH/resources/linux/code.png" "$VSCODE_OUT_PATH/resources/linux/code.png"
|
||||
|
||||
# Adds the commit and date to product.json
|
||||
jq --slurp '.[0] * .[1]' "$VSCODE_SRC_PATH/product.json" <(
|
||||
cat << EOF
|
||||
{
|
||||
"commit": "$(git rev-parse HEAD)",
|
||||
"date": $(jq -n 'now | todate')
|
||||
}
|
||||
EOF
|
||||
) > "$VSCODE_OUT_PATH/product.json"
|
||||
|
||||
# We remove the scripts field so that later on we can run
|
||||
# yarn to fetch node_modules if necessary without build scripts running.
|
||||
# We cannot use --no-scripts because we still want dependant package scripts to run.
|
||||
jq 'del(.scripts)' < "$VSCODE_SRC_PATH/package.json" > "$VSCODE_OUT_PATH/package.json"
|
||||
}
|
||||
|
||||
main "$@"
|
||||
25
ci/build/build-static-release.sh
Executable file
25
ci/build/build-static-release.sh
Executable file
@@ -0,0 +1,25 @@
|
||||
#!/usr/bin/env bash
|
||||
set -euo pipefail
|
||||
|
||||
main() {
|
||||
cd "$(dirname "${0}")/../.."
|
||||
source ./ci/lib.sh
|
||||
|
||||
rsync "$RELEASE_PATH/" "$RELEASE_PATH-static"
|
||||
RELEASE_PATH+=-static
|
||||
|
||||
# We cannot find the path to node from $PATH because yarn shims a script to ensure
|
||||
# we use the same version it's using so we instead run a script with yarn that
|
||||
# will print the path to node.
|
||||
local node_path
|
||||
node_path="$(yarn -s node <<< 'console.info(process.execPath)')"
|
||||
|
||||
mkdir -p "$RELEASE_PATH/bin"
|
||||
rsync ./ci/build/code-server.sh "$RELEASE_PATH/bin/code-server"
|
||||
rsync "$node_path" "$RELEASE_PATH/lib/node"
|
||||
|
||||
cd "$RELEASE_PATH"
|
||||
yarn --production --frozen-lockfile
|
||||
}
|
||||
|
||||
main "$@"
|
||||
21
ci/build/build-vscode.sh
Executable file
21
ci/build/build-vscode.sh
Executable file
@@ -0,0 +1,21 @@
|
||||
#!/usr/bin/env bash
|
||||
set -euo pipefail
|
||||
|
||||
# Builds vscode into lib/vscode/out-vscode.
|
||||
|
||||
# MINIFY controls whether a minified version of vscode is built.
|
||||
MINIFY=${MINIFY-true}
|
||||
|
||||
main() {
|
||||
cd "$(dirname "${0}")/../.."
|
||||
cd lib/vscode
|
||||
|
||||
yarn gulp compile-build
|
||||
yarn gulp compile-extensions-build
|
||||
yarn gulp optimize --gulpfile ./coder.js
|
||||
if [[ $MINIFY ]]; then
|
||||
yarn gulp minify --gulpfile ./coder.js
|
||||
fi
|
||||
}
|
||||
|
||||
main "$@"
|
||||
25
ci/build/clean.sh
Executable file
25
ci/build/clean.sh
Executable file
@@ -0,0 +1,25 @@
|
||||
#!/usr/bin/env bash
|
||||
set -euo pipefail
|
||||
|
||||
main() {
|
||||
cd "$(dirname "${0}")/../.."
|
||||
source ./ci/lib.sh
|
||||
|
||||
rm -Rf \
|
||||
out \
|
||||
release \
|
||||
release-static \
|
||||
release-packages \
|
||||
release-gcp \
|
||||
release-images/ \
|
||||
dist \
|
||||
.tsbuildinfo \
|
||||
.cache/out.tsbuildinfo
|
||||
|
||||
pushd lib/vscode
|
||||
git clean -xffd
|
||||
git reset --hard
|
||||
popd
|
||||
}
|
||||
|
||||
main "$@"
|
||||
3
ci/build/code-server-nfpm.sh
Executable file
3
ci/build/code-server-nfpm.sh
Executable file
@@ -0,0 +1,3 @@
|
||||
#!/usr/bin/env sh
|
||||
|
||||
exec /usr/lib/code-server/bin/code-server "$@"
|
||||
11
ci/build/code-server.service
Normal file
11
ci/build/code-server.service
Normal file
@@ -0,0 +1,11 @@
|
||||
[Unit]
|
||||
Description=code-server
|
||||
After=network.target
|
||||
|
||||
[Service]
|
||||
Type=exec
|
||||
ExecStart=/usr/bin/code-server
|
||||
Restart=always
|
||||
|
||||
[Install]
|
||||
WantedBy=default.target
|
||||
20
ci/build/code-server.sh
Executable file
20
ci/build/code-server.sh
Executable file
@@ -0,0 +1,20 @@
|
||||
#!/usr/bin/env sh
|
||||
|
||||
# This script is intended to be bundled into the static releases.
|
||||
# Runs code-server with the bundled Node binary.
|
||||
|
||||
# More complicated than readlink -f or realpath to support macOS.
|
||||
# See https://github.com/cdr/code-server/issues/1537
|
||||
bin_dir() {
|
||||
# We read the symlink, which may be relative from $0.
|
||||
dst="$(readlink "$0")"
|
||||
# We cd into the $0 directory.
|
||||
cd "$(dirname "$0")" || exit 1
|
||||
# Now we can cd into the dst directory.
|
||||
cd "$(dirname "$dst")" || exit 1
|
||||
# Finally we use pwd -P to print the absolute path of the directory of $dst.
|
||||
pwd -P || exit 1
|
||||
}
|
||||
|
||||
BIN_DIR=$(bin_dir)
|
||||
exec "$BIN_DIR/../lib/node" "$BIN_DIR/.." "$@"
|
||||
17
ci/build/nfpm.yaml
Normal file
17
ci/build/nfpm.yaml
Normal file
@@ -0,0 +1,17 @@
|
||||
name: "code-server"
|
||||
arch: "${ARCH}"
|
||||
platform: "linux"
|
||||
version: "v${VERSION}"
|
||||
section: "devel"
|
||||
priority: "optional"
|
||||
maintainer: "Anmol Sethi <hi@nhooyr.io>"
|
||||
description: |
|
||||
Run VS Code in the browser.
|
||||
vendor: "Coder"
|
||||
homepage: "https://github.com/cdr/code-server"
|
||||
license: "MIT"
|
||||
bindir: "/usr/bin"
|
||||
files:
|
||||
./ci/build/code-server-nfpm.sh: /usr/bin/code-server
|
||||
./ci/build/code-server.service: /usr/lib/systemd/user/code-server.service
|
||||
./release-static/**/*: "/usr/lib/code-server/"
|
||||
41
ci/build/npm-postinstall.sh
Executable file
41
ci/build/npm-postinstall.sh
Executable file
@@ -0,0 +1,41 @@
|
||||
#!/usr/bin/env sh
|
||||
set -eu
|
||||
|
||||
main() {
|
||||
# Grabs the major version of node from $npm_config_user_agent which looks like
|
||||
# yarn/1.21.1 npm/? node/v14.2.0 darwin x64
|
||||
major_node_version=$(echo "$npm_config_user_agent" | sed -n 's/.*node\/v\([^.]*\).*/\1/p')
|
||||
if [ "$major_node_version" -lt 12 ]; then
|
||||
echo "code-server currently requires at least node v12"
|
||||
echo "We have detected that you are on node v$major_node_version"
|
||||
echo "See https://github.com/cdr/code-server/issues/1633"
|
||||
exit 1
|
||||
fi
|
||||
|
||||
case "${npm_config_user_agent-}" in npm*)
|
||||
# We are running under npm.
|
||||
if [ "${npm_config_unsafe_perm-}" != "true" ]; then
|
||||
echo "Please pass --unsafe-perm to npm to install code-server"
|
||||
echo "Otherwise the postinstall script does not have permissions to run"
|
||||
echo "See https://docs.npmjs.com/misc/config#unsafe-perm"
|
||||
echo "See https://stackoverflow.com/questions/49084929/npm-sudo-global-installation-unsafe-perm"
|
||||
exit 1
|
||||
fi
|
||||
;;
|
||||
esac
|
||||
|
||||
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"
|
||||
exit 1
|
||||
fi
|
||||
}
|
||||
|
||||
vscode_yarn() {
|
||||
cd lib/vscode
|
||||
yarn --production --frozen-lockfile
|
||||
cd extensions
|
||||
yarn --production --frozen-lockfile
|
||||
}
|
||||
|
||||
main "$@"
|
||||
21
ci/build/release-github-assets.sh
Executable file
21
ci/build/release-github-assets.sh
Executable file
@@ -0,0 +1,21 @@
|
||||
#!/usr/bin/env bash
|
||||
set -euo pipefail
|
||||
|
||||
# Downloads the release artifacts from CI for the current
|
||||
# commit and then uploads them to the release with the version
|
||||
# in package.json.
|
||||
# You will need $GITHUB_TOKEN set.
|
||||
|
||||
main() {
|
||||
cd "$(dirname "$0")/../.."
|
||||
source ./ci/lib.sh
|
||||
|
||||
download_artifact release-packages ./release-packages
|
||||
local assets=(./release-packages/*)
|
||||
for i in "${!assets[@]}"; do
|
||||
assets[$i]="--attach=${assets[$i]}"
|
||||
done
|
||||
EDITOR=true hub release edit --draft "${assets[@]}" "v$VERSION"
|
||||
}
|
||||
|
||||
main "$@"
|
||||
22
ci/build/release-github-draft.sh
Executable file
22
ci/build/release-github-draft.sh
Executable file
@@ -0,0 +1,22 @@
|
||||
#!/usr/bin/env bash
|
||||
set -euo pipefail
|
||||
|
||||
# Creates a draft release with the template for the version in package.json
|
||||
|
||||
main() {
|
||||
cd "$(dirname "$0")/../.."
|
||||
source ./ci/lib.sh
|
||||
|
||||
hub release create \
|
||||
--file - \
|
||||
-t "$(git rev-parse HEAD)" \
|
||||
--draft "${assets[@]}" "v$VERSION" << EOF
|
||||
v$VERSION
|
||||
|
||||
VS Code v$(vscode_version)
|
||||
|
||||
- Summarize changes here with references to issues
|
||||
EOF
|
||||
}
|
||||
|
||||
main "$@"
|
||||
27
ci/build/test-static-release.sh
Executable file
27
ci/build/test-static-release.sh
Executable file
@@ -0,0 +1,27 @@
|
||||
#!/usr/bin/env bash
|
||||
set -euo pipefail
|
||||
|
||||
# Makes sure the release works.
|
||||
# This is to make sure we don't have Node version errors or any other
|
||||
# compilation-related errors.
|
||||
main() {
|
||||
cd "$(dirname "${0}")/../.."
|
||||
|
||||
local EXTENSIONS_DIR
|
||||
EXTENSIONS_DIR="$(mktemp -d)"
|
||||
|
||||
echo "Testing static release"
|
||||
|
||||
./release-static/bin/code-server --extensions-dir "$EXTENSIONS_DIR" --install-extension ms-python.python
|
||||
local installed_extensions
|
||||
installed_extensions="$(./release-static/bin/code-server --extensions-dir "$EXTENSIONS_DIR" --list-extensions 2>&1)"
|
||||
if [[ $installed_extensions != "ms-python.python" ]]; then
|
||||
echo "Unexpected output from listing extensions:"
|
||||
echo "$installed_extensions"
|
||||
exit 1
|
||||
fi
|
||||
|
||||
echo "Static release works correctly"
|
||||
}
|
||||
|
||||
main "$@"
|
||||
11
ci/clean.sh
11
ci/clean.sh
@@ -1,11 +0,0 @@
|
||||
#!/usr/bin/env bash
|
||||
|
||||
set -euo pipefail
|
||||
|
||||
main() {
|
||||
git clean -Xffd
|
||||
git submodule foreach --recursive git clean -xffd
|
||||
git submodule foreach --recursive git reset --hard
|
||||
}
|
||||
|
||||
main "$@"
|
||||
@@ -1,6 +0,0 @@
|
||||
#!/usr/bin/env sh
|
||||
# code-server.sh -- Run code-server with the bundled Node binary.
|
||||
|
||||
dir="$(dirname "$(readlink -f "$0" || realpath "$0")")"
|
||||
|
||||
exec "$dir/node" "$dir/out/node/entry.js" "$@"
|
||||
53
ci/container/Dockerfile
Normal file
53
ci/container/Dockerfile
Normal file
@@ -0,0 +1,53 @@
|
||||
FROM debian:8
|
||||
|
||||
RUN apt-get update
|
||||
|
||||
# Needed for debian repositories added below.
|
||||
RUN apt-get install -y curl gnupg
|
||||
|
||||
# Installs node.
|
||||
RUN curl -sSL https://deb.nodesource.com/setup_14.x | bash - && \
|
||||
apt-get install -y nodejs
|
||||
|
||||
# Installs yarn.
|
||||
RUN curl -sSL https://dl.yarnpkg.com/debian/pubkey.gpg | apt-key add - && \
|
||||
echo "deb https://dl.yarnpkg.com/debian/ stable main" | tee /etc/apt/sources.list.d/yarn.list && \
|
||||
apt-get update && apt-get install -y yarn
|
||||
|
||||
# Installs VS Code build deps.
|
||||
RUN apt-get install -y build-essential \
|
||||
libsecret-1-dev \
|
||||
libx11-dev \
|
||||
libxkbfile-dev
|
||||
|
||||
# Installs envsubst.
|
||||
RUN apt-get install -y gettext-base
|
||||
|
||||
# Misc build dependencies.
|
||||
RUN apt-get install -y git rsync
|
||||
|
||||
# We need latest jq from debian buster for date support.
|
||||
RUN ARCH="$(dpkg --print-architecture)" && \
|
||||
curl -sSOL http://http.us.debian.org/debian/pool/main/libo/libonig/libonig5_6.9.1-1_$ARCH.deb && \
|
||||
dpkg -i libonig*.deb && \
|
||||
curl -sSOL http://http.us.debian.org/debian/pool/main/j/jq/libjq1_1.5+dfsg-2+b1_$ARCH.deb && \
|
||||
dpkg -i libjq*.deb && \
|
||||
curl -sSOL http://http.us.debian.org/debian/pool/main/j/jq/jq_1.5+dfsg-2+b1_$ARCH.deb && \
|
||||
dpkg -i jq*.deb && rm *.deb
|
||||
|
||||
# Installs shellcheck.
|
||||
# Unfortunately coredumps on debian:8 so disabled for now.
|
||||
#RUN curl -sSL 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
|
||||
RUN ARCH="$(dpkg --print-architecture)" && \
|
||||
curl -sSL "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 GO111MODULE=on
|
||||
RUN go get mvdan.cc/sh/v3/cmd/shfmt
|
||||
RUN go get github.com/goreleaser/nfpm/cmd/nfpm
|
||||
|
||||
RUN curl -fsSL https://get.docker.com | sh
|
||||
53
ci/container/arm64/Dockerfile
Normal file
53
ci/container/arm64/Dockerfile
Normal file
@@ -0,0 +1,53 @@
|
||||
FROM debian:9
|
||||
|
||||
RUN apt-get update
|
||||
|
||||
# Needed for debian repositories added below.
|
||||
RUN apt-get install -y curl gnupg
|
||||
|
||||
# Installs node.
|
||||
RUN curl -sSL https://deb.nodesource.com/setup_14.x | bash - && \
|
||||
apt-get install -y nodejs
|
||||
|
||||
# Installs yarn.
|
||||
RUN curl -sSL https://dl.yarnpkg.com/debian/pubkey.gpg | apt-key add - && \
|
||||
echo "deb https://dl.yarnpkg.com/debian/ stable main" | tee /etc/apt/sources.list.d/yarn.list && \
|
||||
apt-get update && apt-get install -y yarn
|
||||
|
||||
# Installs VS Code build deps.
|
||||
RUN apt-get install -y build-essential \
|
||||
libsecret-1-dev \
|
||||
libx11-dev \
|
||||
libxkbfile-dev
|
||||
|
||||
# Installs envsubst.
|
||||
RUN apt-get install -y gettext-base
|
||||
|
||||
# Misc build dependencies.
|
||||
RUN apt-get install -y git rsync
|
||||
|
||||
# We need latest jq from debian buster for date support.
|
||||
RUN ARCH="$(dpkg --print-architecture)" && \
|
||||
curl -sSOL http://http.us.debian.org/debian/pool/main/libo/libonig/libonig5_6.9.1-1_$ARCH.deb && \
|
||||
dpkg -i libonig*.deb && \
|
||||
curl -sSOL http://http.us.debian.org/debian/pool/main/j/jq/libjq1_1.5+dfsg-2+b1_$ARCH.deb && \
|
||||
dpkg -i libjq*.deb && \
|
||||
curl -sSOL http://http.us.debian.org/debian/pool/main/j/jq/jq_1.5+dfsg-2+b1_$ARCH.deb && \
|
||||
dpkg -i jq*.deb && rm *.deb
|
||||
|
||||
# Installs shellcheck.
|
||||
# Unfortunately coredumps on debian:8 so disabled for now.
|
||||
#RUN curl -sSL 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
|
||||
RUN ARCH="$(dpkg --print-architecture)" && \
|
||||
curl -sSL "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 GO111MODULE=on
|
||||
RUN go get mvdan.cc/sh/v3/cmd/shfmt
|
||||
RUN go get github.com/goreleaser/nfpm/cmd/nfpm
|
||||
|
||||
RUN curl -fsSL https://get.docker.com | sh
|
||||
6
ci/container/arm64/README.md
Normal file
6
ci/container/arm64/README.md
Normal file
@@ -0,0 +1,6 @@
|
||||
# arm64
|
||||
|
||||
Unfortunately there is no arm64 build of `debian:8` so
|
||||
we need to use `debian:9` instead.
|
||||
|
||||
This is just an exact copy of [../Dockerfile](../Dockerfile) with the base image change.
|
||||
@@ -2,7 +2,7 @@
|
||||
set -euo pipefail
|
||||
|
||||
main() {
|
||||
cd "$(dirname "$0")/.."
|
||||
cd "$(dirname "$0")/../.."
|
||||
|
||||
yarn fmt
|
||||
yarn lint
|
||||
13
ci/dev/container/Dockerfile
Normal file
13
ci/dev/container/Dockerfile
Normal file
@@ -0,0 +1,13 @@
|
||||
FROM node:12
|
||||
|
||||
RUN apt-get update && apt-get install -y \
|
||||
curl \
|
||||
iproute2 \
|
||||
vim \
|
||||
iptables \
|
||||
net-tools \
|
||||
libsecret-1-dev \
|
||||
libx11-dev \
|
||||
libxkbfile-dev
|
||||
|
||||
CMD ["/bin/bash"]
|
||||
48
ci/dev/container/exec.sh
Executable file
48
ci/dev/container/exec.sh
Executable file
@@ -0,0 +1,48 @@
|
||||
#!/usr/bin/env bash
|
||||
set -euo pipefail
|
||||
|
||||
# Opens an interactive bash session inside of a docker container
|
||||
# for improved isolation during development.
|
||||
# If the container exists it is restarted if necessary, then reused.
|
||||
|
||||
main() {
|
||||
cd "$(dirname "${0}")/../../.."
|
||||
|
||||
local container_name=code-server-dev
|
||||
|
||||
if docker inspect $container_name &> /dev/null; then
|
||||
echo "-- Starting container"
|
||||
docker start "$container_name" > /dev/null
|
||||
|
||||
enter
|
||||
exit 0
|
||||
fi
|
||||
|
||||
build
|
||||
run
|
||||
enter
|
||||
}
|
||||
|
||||
enter() {
|
||||
echo "--- Entering $container_name"
|
||||
docker exec -it "$container_name" /bin/bash
|
||||
}
|
||||
|
||||
run() {
|
||||
echo "--- Spawning $container_name"
|
||||
docker run \
|
||||
-it \
|
||||
--name $container_name \
|
||||
"-v=$PWD:/code-server" \
|
||||
"-w=/code-server" \
|
||||
"-p=127.0.0.1:8080:8080" \
|
||||
$(if [[ -t 0 ]]; then echo -it; fi) \
|
||||
"$container_name"
|
||||
}
|
||||
|
||||
build() {
|
||||
echo "--- Building $container_name"
|
||||
docker build -t $container_name ./ci/dev/container > /dev/null
|
||||
}
|
||||
|
||||
main "$@"
|
||||
12
ci/dev/diff-vscode.sh
Executable file
12
ci/dev/diff-vscode.sh
Executable file
@@ -0,0 +1,12 @@
|
||||
#!/usr/bin/env bash
|
||||
set -euo pipefail
|
||||
|
||||
main() {
|
||||
cd "$(dirname "$0")/../.."
|
||||
|
||||
cd ./lib/vscode
|
||||
git add -A
|
||||
git diff HEAD > ../../ci/dev/vscode.patch
|
||||
}
|
||||
|
||||
main "$@"
|
||||
@@ -1,8 +1,9 @@
|
||||
#!/usr/bin/env bash
|
||||
|
||||
set -euo pipefail
|
||||
|
||||
main() {
|
||||
cd "$(dirname "$0")/../.."
|
||||
|
||||
shfmt -i 2 -w -s -sr $(git ls-files "*.sh")
|
||||
|
||||
local prettierExts
|
||||
@@ -20,6 +21,9 @@ main() {
|
||||
)
|
||||
prettier --write --loglevel=warn $(git ls-files "${prettierExts[@]}")
|
||||
|
||||
doctoc --title '# FAQ' doc/FAQ.md > /dev/null
|
||||
doctoc --title '# Setup Guide' doc/guide.md > /dev/null
|
||||
|
||||
if [[ ${CI-} && $(git ls-files --other --modified --exclude-standard) ]]; then
|
||||
echo "Files need generation or are formatted incorrectly:"
|
||||
git -c color.ui=always status | grep --color=no '\[31m'
|
||||
@@ -1,11 +1,16 @@
|
||||
#!/usr/bin/env bash
|
||||
|
||||
set -euo pipefail
|
||||
|
||||
main() {
|
||||
cd "$(dirname "$0")/../.."
|
||||
|
||||
eslint --max-warnings=0 --fix $(git ls-files "*.ts" "*.tsx" "*.js")
|
||||
stylelint $(git ls-files "*.css")
|
||||
tsc --noEmit
|
||||
# See comment in ./ci/container/Dockerfile
|
||||
if [[ ! ${CI-} ]]; then
|
||||
shellcheck -e SC2046,SC2164,SC2154 $(git ls-files "*.sh")
|
||||
fi
|
||||
}
|
||||
|
||||
main "$@"
|
||||
11
ci/dev/patch-vscode.sh
Executable file
11
ci/dev/patch-vscode.sh
Executable file
@@ -0,0 +1,11 @@
|
||||
#!/usr/bin/env bash
|
||||
set -euo pipefail
|
||||
|
||||
main() {
|
||||
cd "$(dirname "$0")/../.."
|
||||
|
||||
cd ./lib/vscode
|
||||
git apply ../../ci/dev/vscode.patch
|
||||
}
|
||||
|
||||
main "$@"
|
||||
10
ci/dev/test.sh
Executable file
10
ci/dev/test.sh
Executable file
@@ -0,0 +1,10 @@
|
||||
#!/usr/bin/env bash
|
||||
set -euo pipefail
|
||||
|
||||
main() {
|
||||
cd "$(dirname "$0")/../.."
|
||||
|
||||
mocha -r ts-node/register ./test/*.test.ts
|
||||
}
|
||||
|
||||
main "$@"
|
||||
File diff suppressed because it is too large
Load Diff
@@ -5,7 +5,7 @@ set -euo pipefail
|
||||
# 2. Patches it.
|
||||
# 3. Installs it.
|
||||
main() {
|
||||
cd "$(dirname "$0")/.."
|
||||
cd "$(dirname "$0")/../.."
|
||||
|
||||
git submodule update --init
|
||||
|
||||
@@ -15,10 +15,7 @@ main() {
|
||||
(
|
||||
cd lib/vscode
|
||||
# Install VS Code dependencies.
|
||||
yarn
|
||||
|
||||
# NODE_MODULE_VERSION mismatch errors without this.
|
||||
npm rebuild
|
||||
yarn ${CI+--frozen-lockfile}
|
||||
)
|
||||
}
|
||||
|
||||
163
ci/dev/watch.ts
Normal file
163
ci/dev/watch.ts
Normal file
@@ -0,0 +1,163 @@
|
||||
import * as cp from "child_process"
|
||||
import Bundler from "parcel-bundler"
|
||||
import * as path from "path"
|
||||
|
||||
async function main(): Promise<void> {
|
||||
try {
|
||||
const watcher = new Watcher()
|
||||
await watcher.watch()
|
||||
} catch (error) {
|
||||
console.error(error.message)
|
||||
process.exit(1)
|
||||
}
|
||||
}
|
||||
|
||||
class Watcher {
|
||||
private readonly rootPath = path.resolve(__dirname, "../..")
|
||||
private readonly vscodeSourcePath = path.join(this.rootPath, "lib/vscode")
|
||||
|
||||
private static log(message: string, skipNewline = false): void {
|
||||
process.stdout.write(message)
|
||||
if (!skipNewline) {
|
||||
process.stdout.write("\n")
|
||||
}
|
||||
}
|
||||
|
||||
public async watch(): Promise<void> {
|
||||
let server: cp.ChildProcess | undefined
|
||||
const restartServer = (): void => {
|
||||
if (server) {
|
||||
server.kill()
|
||||
}
|
||||
const s = cp.fork(path.join(this.rootPath, "out/node/entry.js"), process.argv.slice(2))
|
||||
console.log(`[server] spawned process ${s.pid}`)
|
||||
s.on("exit", () => console.log(`[server] process ${s.pid} exited`))
|
||||
server = s
|
||||
}
|
||||
|
||||
const vscode = cp.spawn("yarn", ["watch"], { cwd: this.vscodeSourcePath })
|
||||
const tsc = cp.spawn("tsc", ["--watch", "--pretty", "--preserveWatchOutput"], { cwd: this.rootPath })
|
||||
const bundler = this.createBundler()
|
||||
|
||||
const cleanup = (code?: number | null): void => {
|
||||
Watcher.log("killing vs code watcher")
|
||||
vscode.removeAllListeners()
|
||||
vscode.kill()
|
||||
|
||||
Watcher.log("killing tsc")
|
||||
tsc.removeAllListeners()
|
||||
tsc.kill()
|
||||
|
||||
if (server) {
|
||||
Watcher.log("killing server")
|
||||
server.removeAllListeners()
|
||||
server.kill()
|
||||
}
|
||||
|
||||
Watcher.log("killing bundler")
|
||||
process.exit(code || 0)
|
||||
}
|
||||
|
||||
process.on("SIGINT", () => cleanup())
|
||||
process.on("SIGTERM", () => cleanup())
|
||||
|
||||
vscode.on("exit", (code) => {
|
||||
Watcher.log("vs code watcher terminated unexpectedly")
|
||||
cleanup(code)
|
||||
})
|
||||
tsc.on("exit", (code) => {
|
||||
Watcher.log("tsc terminated unexpectedly")
|
||||
cleanup(code)
|
||||
})
|
||||
const bundle = bundler.bundle().catch(() => {
|
||||
Watcher.log("parcel watcher terminated unexpectedly")
|
||||
cleanup(1)
|
||||
})
|
||||
bundler.on("buildEnd", () => {
|
||||
console.log("[parcel] bundled")
|
||||
})
|
||||
bundler.on("buildError", (error) => {
|
||||
console.error("[parcel]", error)
|
||||
})
|
||||
|
||||
vscode.stderr.on("data", (d) => process.stderr.write(d))
|
||||
tsc.stderr.on("data", (d) => process.stderr.write(d))
|
||||
|
||||
// From https://github.com/chalk/ansi-regex
|
||||
const pattern = [
|
||||
"[\\u001B\\u009B][[\\]()#;?]*(?:(?:(?:[a-zA-Z\\d]*(?:;[-a-zA-Z\\d\\/#&.:=?%@~_]*)*)?\\u0007)",
|
||||
"(?:(?:\\d{1,4}(?:;\\d{0,4})*)?[\\dA-PR-TZcf-ntqry=><~]))",
|
||||
].join("|")
|
||||
const re = new RegExp(pattern, "g")
|
||||
|
||||
/**
|
||||
* Split stdout on newlines and strip ANSI codes.
|
||||
*/
|
||||
const onLine = (proc: cp.ChildProcess, callback: (strippedLine: string, originalLine: string) => void): void => {
|
||||
let buffer = ""
|
||||
if (!proc.stdout) {
|
||||
throw new Error("no stdout")
|
||||
}
|
||||
proc.stdout.setEncoding("utf8")
|
||||
proc.stdout.on("data", (d) => {
|
||||
const data = buffer + d
|
||||
const split = data.split("\n")
|
||||
const last = split.length - 1
|
||||
|
||||
for (let i = 0; i < last; ++i) {
|
||||
callback(split[i].replace(re, ""), split[i])
|
||||
}
|
||||
|
||||
// The last item will either be an empty string (the data ended with a
|
||||
// newline) or a partial line (did not end with a newline) and we must
|
||||
// wait to parse it until we get a full line.
|
||||
buffer = split[last]
|
||||
})
|
||||
}
|
||||
|
||||
let startingVscode = false
|
||||
let startedVscode = false
|
||||
onLine(vscode, (line, original) => {
|
||||
console.log("[vscode]", original)
|
||||
// Wait for watch-client since "Finished compilation" will appear multiple
|
||||
// times before the client starts building.
|
||||
if (!startingVscode && line.includes("Starting watch-client")) {
|
||||
startingVscode = true
|
||||
} else if (startingVscode && line.includes("Finished compilation")) {
|
||||
if (startedVscode) {
|
||||
bundle.then(restartServer)
|
||||
}
|
||||
startedVscode = true
|
||||
}
|
||||
})
|
||||
|
||||
onLine(tsc, (line, original) => {
|
||||
// tsc outputs blank lines; skip them.
|
||||
if (line !== "") {
|
||||
console.log("[tsc]", original)
|
||||
}
|
||||
if (line.includes("Watching for file changes")) {
|
||||
bundle.then(restartServer)
|
||||
}
|
||||
})
|
||||
}
|
||||
|
||||
private createBundler(out = "dist"): Bundler {
|
||||
return new Bundler(
|
||||
[
|
||||
path.join(this.rootPath, "src/browser/pages/app.ts"),
|
||||
path.join(this.rootPath, "src/browser/register.ts"),
|
||||
path.join(this.rootPath, "src/browser/serviceWorker.ts"),
|
||||
],
|
||||
{
|
||||
outDir: path.join(this.rootPath, out),
|
||||
cacheDir: path.join(this.rootPath, ".cache"),
|
||||
minify: !!process.env.MINIFY,
|
||||
logLevel: 1,
|
||||
publicUrl: "/static/development/dist",
|
||||
},
|
||||
)
|
||||
}
|
||||
}
|
||||
|
||||
main()
|
||||
@@ -1,23 +0,0 @@
|
||||
FROM centos:7
|
||||
|
||||
RUN yum update -y && yum install -y \
|
||||
devtoolset-6 \
|
||||
gcc-c++ \
|
||||
xz \
|
||||
ccache \
|
||||
git \
|
||||
wget \
|
||||
openssl \
|
||||
libxkbfile-devel \
|
||||
libsecret-devel \
|
||||
libx11-devel
|
||||
|
||||
RUN mkdir /usr/share/node && cd /usr/share/node \
|
||||
&& curl "https://nodejs.org/dist/v12.14.0/node-v12.14.0-linux-$(uname -m | sed 's/86_//; s/aarch/arm/').tar.xz" | tar xJ --strip-components=1 --
|
||||
ENV PATH "$PATH:/usr/share/node/bin"
|
||||
RUN npm install -g yarn
|
||||
|
||||
RUN curl -L "https://github.com/mvdan/sh/releases/download/v3.0.1/shfmt_v3.0.1_linux_$(uname -m | sed 's/x86_/amd/; s/aarch64/arm/')" > /usr/local/bin/shfmt \
|
||||
&& chmod +x /usr/local/bin/shfmt
|
||||
|
||||
ENTRYPOINT ["/bin/bash", "-c"]
|
||||
@@ -1,13 +0,0 @@
|
||||
#!/usr/bin/env bash
|
||||
|
||||
set -euo pipefail
|
||||
|
||||
main() {
|
||||
cd "$(dirname "$0")/../.."
|
||||
|
||||
docker build ci/image
|
||||
imageTag="$(docker build -q ci/image)"
|
||||
docker run -t --rm -e CI -e GITHUB_TOKEN -e TRAVIS_TAG -v "$(yarn cache dir):/usr/local/share/.cache/yarn/v6" -v "$PWD:/repo" -w /repo "$imageTag" "$*"
|
||||
}
|
||||
|
||||
main "$@"
|
||||
99
ci/lib.sh
Normal file → Executable file
99
ci/lib.sh
Normal file → Executable file
@@ -1,10 +1,95 @@
|
||||
#!/usr/bin/env bash
|
||||
set -euo pipefail
|
||||
|
||||
set_version() {
|
||||
local code_server_version=${VERSION:-${TRAVIS_TAG:-}}
|
||||
if [[ -z $code_server_version ]]; then
|
||||
code_server_version=$(grep version ./package.json | head -1 | awk -F: '{ print $2 }' | sed 's/[",]//g' | tr -d '[:space:]')
|
||||
fi
|
||||
export VERSION=$code_server_version
|
||||
pushd() {
|
||||
builtin pushd "$@" > /dev/null
|
||||
}
|
||||
|
||||
popd() {
|
||||
builtin popd > /dev/null
|
||||
}
|
||||
|
||||
pkg_json_version() {
|
||||
jq -r .version package.json
|
||||
}
|
||||
|
||||
vscode_version() {
|
||||
jq -r .version lib/vscode/package.json
|
||||
}
|
||||
|
||||
os() {
|
||||
local os
|
||||
os=$(uname | tr '[:upper:]' '[:lower:]')
|
||||
if [[ $os == "linux" ]]; then
|
||||
# Alpine's ldd doesn't have a version flag but if you use an invalid flag
|
||||
# (like --version) it outputs the version to stderr and exits with 1.
|
||||
local ldd_output
|
||||
ldd_output=$(ldd --version 2>&1 || true)
|
||||
if echo "$ldd_output" | grep -iq musl; then
|
||||
os="alpine"
|
||||
fi
|
||||
elif [[ $os == "darwin" ]]; then
|
||||
os="macos"
|
||||
fi
|
||||
echo "$os"
|
||||
}
|
||||
|
||||
arch() {
|
||||
case "$(uname -m)" in
|
||||
aarch64)
|
||||
echo arm64
|
||||
;;
|
||||
x86_64)
|
||||
echo amd64
|
||||
;;
|
||||
*)
|
||||
echo "unknown architecture $(uname -a)"
|
||||
exit 1
|
||||
;;
|
||||
esac
|
||||
}
|
||||
|
||||
curl() {
|
||||
command curl -H "Authorization: token $GITHUB_TOKEN" "$@"
|
||||
}
|
||||
|
||||
# Grabs the most recent ci.yaml github workflow run that was successful and triggered from the same commit being pushd.
|
||||
# This will contain the artifacts we want.
|
||||
# https://developer.github.com/v3/actions/workflow-runs/#list-workflow-runs
|
||||
get_artifacts_url() {
|
||||
curl -sSL 'https://api.github.com/repos/cdr/code-server/actions/workflows/ci.yaml/runs?status=success&event=push' | jq -r ".workflow_runs[] | select(.head_sha == \"$(git rev-parse HEAD)\") | .artifacts_url" | head -n 1
|
||||
}
|
||||
|
||||
# Grabs the artifact's download url.
|
||||
# https://developer.github.com/v3/actions/artifacts/#list-workflow-run-artifacts
|
||||
get_artifact_url() {
|
||||
local artifact_name="$1"
|
||||
curl -sSL "$(get_artifacts_url)" | jq -r ".artifacts[] | select(.name == \"$artifact_name\") | .archive_download_url" | head -n 1
|
||||
}
|
||||
|
||||
# Uses the above two functions to download a artifact into a directory.
|
||||
download_artifact() {
|
||||
local artifact_name="$1"
|
||||
local dst="$2"
|
||||
|
||||
local tmp_file
|
||||
tmp_file="$(mktemp)"
|
||||
|
||||
curl -sSL "$(get_artifact_url "$artifact_name")" > "$tmp_file"
|
||||
unzip -q -o "$tmp_file" -d "$dst"
|
||||
rm "$tmp_file"
|
||||
}
|
||||
|
||||
rsync() {
|
||||
command rsync -a --del "$@"
|
||||
}
|
||||
|
||||
VERSION="$(pkg_json_version)"
|
||||
export VERSION
|
||||
ARCH="$(arch)"
|
||||
export ARCH
|
||||
OS=$(os)
|
||||
export OS
|
||||
|
||||
# RELEASE_PATH is the destination directory for the release from the root.
|
||||
# Defaults to release
|
||||
RELEASE_PATH="${RELEASE_PATH-release}"
|
||||
|
||||
@@ -13,6 +13,7 @@ RUN apt-get update \
|
||||
ssh \
|
||||
sudo \
|
||||
vim \
|
||||
lsb-release \
|
||||
&& rm -rf /var/lib/apt/lists/*
|
||||
|
||||
# https://wiki.debian.org/Locale#Manually
|
||||
@@ -26,18 +27,17 @@ ENV SHELL=/bin/bash
|
||||
RUN adduser --gecos '' --disabled-password coder && \
|
||||
echo "coder ALL=(ALL) NOPASSWD:ALL" >> /etc/sudoers.d/nopasswd
|
||||
|
||||
RUN curl -SsL https://github.com/boxboat/fixuid/releases/download/v0.4/fixuid-0.4-linux-amd64.tar.gz | tar -C /usr/local/bin -xzf - && \
|
||||
RUN ARCH="$(dpkg --print-architecture)" && \
|
||||
curl -sSL "https://github.com/boxboat/fixuid/releases/download/v0.4.1/fixuid-0.4.1-linux-$ARCH.tar.gz" | tar -C /usr/local/bin -xzf - && \
|
||||
chown root:root /usr/local/bin/fixuid && \
|
||||
chmod 4755 /usr/local/bin/fixuid && \
|
||||
mkdir -p /etc/fixuid && \
|
||||
printf "user: coder\ngroup: coder\n" > /etc/fixuid/config.yml
|
||||
|
||||
COPY release/code-server*.tar.gz /tmp/
|
||||
RUN cd /tmp && tar -xzf code-server*.tar.gz && rm code-server*.tar.gz && \
|
||||
mv code-server* /usr/local/lib/code-server && \
|
||||
ln -s /usr/local/lib/code-server/code-server /usr/local/bin/code-server
|
||||
COPY release-packages/code-server*.deb /tmp/
|
||||
RUN dpkg -i /tmp/code-server*$(dpkg --print-architecture).deb && rm /tmp/code-server*.deb
|
||||
|
||||
EXPOSE 8080
|
||||
USER coder
|
||||
WORKDIR /home/coder
|
||||
ENTRYPOINT ["dumb-init", "fixuid", "-q", "/usr/local/bin/code-server", "--host", "0.0.0.0", "."]
|
||||
ENTRYPOINT ["dumb-init", "fixuid", "-q", "/usr/bin/code-server", "--bind-addr", "0.0.0.0:8080", "."]
|
||||
11
ci/release-container/build.sh
Executable file
11
ci/release-container/build.sh
Executable file
@@ -0,0 +1,11 @@
|
||||
#!/usr/bin/env bash
|
||||
set -euo pipefail
|
||||
|
||||
main() {
|
||||
cd "$(dirname "$0")/../.."
|
||||
source ./ci/lib.sh
|
||||
|
||||
docker build -t "codercom/code-server-$ARCH:$VERSION" -f ./ci/release-container/Dockerfile .
|
||||
}
|
||||
|
||||
main "$@"
|
||||
@@ -1,22 +0,0 @@
|
||||
#!/usr/bin/env bash
|
||||
|
||||
set -euo pipefail
|
||||
|
||||
main() {
|
||||
cd "$(dirname "$0")/../.."
|
||||
source ./ci/lib.sh
|
||||
set_version
|
||||
|
||||
if [[ ${CI:-} ]]; then
|
||||
echo "$DOCKER_PASSWORD" | docker login -u "$DOCKER_USERNAME" --password-stdin
|
||||
fi
|
||||
|
||||
imageTag="codercom/code-server:$VERSION"
|
||||
if [[ ${TRAVIS_CPU_ARCH:-} == "arm64" ]]; then
|
||||
imageTag+="-arm64"
|
||||
fi
|
||||
docker build -t "$imageTag" -f ./ci/release-image/Dockerfile .
|
||||
docker push codercom/code-server
|
||||
}
|
||||
|
||||
main "$@"
|
||||
@@ -1,75 +0,0 @@
|
||||
#!/usr/bin/env bash
|
||||
# ci.bash -- Build code-server in the CI.
|
||||
|
||||
set -euo pipefail
|
||||
|
||||
function package() {
|
||||
local target
|
||||
target=$(uname | tr '[:upper:]' '[:lower:]')
|
||||
if [[ $target == "linux" ]]; then
|
||||
# Alpine's ldd doesn't have a version flag but if you use an invalid flag
|
||||
# (like --version) it outputs the version to stderr and exits with 1.
|
||||
local ldd_output
|
||||
ldd_output=$(ldd --version 2>&1 || true)
|
||||
if echo "$ldd_output" | grep -iq musl; then
|
||||
target="alpine"
|
||||
fi
|
||||
fi
|
||||
|
||||
local arch
|
||||
arch="$(uname -m)"
|
||||
|
||||
echo -n "Creating release..."
|
||||
|
||||
cp "$(command -v node)" ./build
|
||||
cp README.md ./build
|
||||
cp LICENSE.txt ./build
|
||||
cp ./lib/vscode/ThirdPartyNotices.txt ./build
|
||||
cp ./ci/code-server.sh ./build/code-server
|
||||
|
||||
local archive_name="code-server-$VERSION-$target-$arch"
|
||||
mkdir -p ./release
|
||||
|
||||
local ext
|
||||
if [[ $target == "linux" ]]; then
|
||||
ext=".tar.gz"
|
||||
tar -czf "release/$archive_name$ext" --transform "s/^\.\/build/$archive_name/" ./build
|
||||
else
|
||||
mv ./build "./$archive_name"
|
||||
ext=".zip"
|
||||
zip -r "release/$archive_name$ext" "./$archive_name"
|
||||
mv "./$archive_name" ./build
|
||||
fi
|
||||
|
||||
echo "done (release/$archive_name)"
|
||||
|
||||
mkdir -p "./release-upload/$VERSION"
|
||||
cp "./release/$archive_name$ext" "./release-upload/$VERSION/$target-$arch$ext"
|
||||
mkdir -p "./release-upload/latest"
|
||||
cp "./release/$archive_name$ext" "./release-upload/latest/$target-$arch$ext"
|
||||
}
|
||||
|
||||
# This script assumes that yarn has already ran.
|
||||
function build() {
|
||||
# Always minify and package on CI.
|
||||
if [[ ${CI:-} ]]; then
|
||||
export MINIFY="true"
|
||||
fi
|
||||
|
||||
yarn build
|
||||
}
|
||||
|
||||
function main() {
|
||||
cd "$(dirname "${0}")/.."
|
||||
source ./ci/lib.sh
|
||||
|
||||
set_version
|
||||
|
||||
build
|
||||
|
||||
if [[ ${CI:-} ]]; then
|
||||
package
|
||||
fi
|
||||
}
|
||||
|
||||
main "$@"
|
||||
14
ci/steps/build-docker-image.sh
Executable file
14
ci/steps/build-docker-image.sh
Executable file
@@ -0,0 +1,14 @@
|
||||
#!/usr/bin/env bash
|
||||
set -euo pipefail
|
||||
|
||||
main() {
|
||||
cd "$(dirname "$0")/../.."
|
||||
source ./ci/lib.sh
|
||||
|
||||
./ci/release-container/build.sh
|
||||
|
||||
mkdir -p release-images
|
||||
docker save "codercom/code-server-$ARCH:$VERSION" > "release-images/code-server-$ARCH-$VERSION.tar"
|
||||
}
|
||||
|
||||
main "$@"
|
||||
17
ci/steps/fmt.sh
Executable file
17
ci/steps/fmt.sh
Executable file
@@ -0,0 +1,17 @@
|
||||
#!/usr/bin/env bash
|
||||
set -euo pipefail
|
||||
|
||||
main() {
|
||||
cd "$(dirname "$0")/../.."
|
||||
|
||||
yarn --frozen-lockfile
|
||||
|
||||
git submodule update --init
|
||||
# We do not `yarn vscode` to make test.sh faster.
|
||||
# If the patch fails to apply, then it's likely already applied
|
||||
yarn vscode:patch &> /dev/null || true
|
||||
|
||||
yarn fmt
|
||||
}
|
||||
|
||||
main "$@"
|
||||
17
ci/steps/lint.sh
Executable file
17
ci/steps/lint.sh
Executable file
@@ -0,0 +1,17 @@
|
||||
#!/usr/bin/env bash
|
||||
set -euo pipefail
|
||||
|
||||
main() {
|
||||
cd "$(dirname "$0")/../.."
|
||||
|
||||
yarn --frozen-lockfile
|
||||
|
||||
git submodule update --init
|
||||
# We do not `yarn vscode` to make test.sh faster.
|
||||
# If the patch fails to apply, then it's likely already applied
|
||||
yarn vscode:patch &> /dev/null || true
|
||||
|
||||
yarn lint
|
||||
}
|
||||
|
||||
main "$@"
|
||||
18
ci/steps/publish-npm.sh
Executable file
18
ci/steps/publish-npm.sh
Executable file
@@ -0,0 +1,18 @@
|
||||
#!/usr/bin/env bash
|
||||
set -euo pipefail
|
||||
|
||||
main() {
|
||||
cd "$(dirname "$0")/../.."
|
||||
source ./ci/lib.sh
|
||||
|
||||
if [[ ${CI-} ]]; then
|
||||
echo "//registry.npmjs.org/:_authToken=${NPM_TOKEN}" > ~/.npmrc
|
||||
fi
|
||||
|
||||
download_artifact npm-package ./release-npm-package
|
||||
# https://github.com/actions/upload-artifact/issues/38
|
||||
tar -xzf release-npm-package/package.tar.gz
|
||||
yarn publish --non-interactive release
|
||||
}
|
||||
|
||||
main "$@"
|
||||
37
ci/steps/push-docker-manifest.sh
Executable file
37
ci/steps/push-docker-manifest.sh
Executable file
@@ -0,0 +1,37 @@
|
||||
#!/usr/bin/env bash
|
||||
set -euo pipefail
|
||||
|
||||
main() {
|
||||
cd "$(dirname "$0")/../.."
|
||||
source ./ci/lib.sh
|
||||
|
||||
download_artifact release-images ./release-images
|
||||
if [[ ${CI-} ]]; then
|
||||
echo "$DOCKER_PASSWORD" | docker login -u "$DOCKER_USERNAME" --password-stdin
|
||||
fi
|
||||
|
||||
for img in ./release-images/*; do
|
||||
docker load -i "$img"
|
||||
done
|
||||
|
||||
# We have to ensure the amd64 and arm64 images exist on the remote registry
|
||||
# in order to build the manifest.
|
||||
# We don't put the arch in the tag to avoid polluting the main repository.
|
||||
# These other repositories are private so they don't pollute our organization namespace.
|
||||
docker push "codercom/code-server-amd64:$VERSION"
|
||||
docker push "codercom/code-server-arm64:$VERSION"
|
||||
|
||||
export DOCKER_CLI_EXPERIMENTAL=enabled
|
||||
|
||||
docker manifest create "codercom/code-server:$VERSION" \
|
||||
"codercom/code-server-amd64:$VERSION" \
|
||||
"codercom/code-server-arm64:$VERSION"
|
||||
docker manifest push --purge "codercom/code-server:$VERSION"
|
||||
|
||||
docker manifest create "codercom/code-server:latest" \
|
||||
"codercom/code-server-amd64:$VERSION" \
|
||||
"codercom/code-server-arm64:$VERSION"
|
||||
docker manifest push --purge "codercom/code-server:latest"
|
||||
}
|
||||
|
||||
main "$@"
|
||||
15
ci/steps/release-static.sh
Executable file
15
ci/steps/release-static.sh
Executable file
@@ -0,0 +1,15 @@
|
||||
#!/usr/bin/env bash
|
||||
set -euo pipefail
|
||||
|
||||
main() {
|
||||
cd "$(dirname "$0")/../.."
|
||||
|
||||
# https://github.com/actions/upload-artifact/issues/38
|
||||
tar -xzf release-npm-package/package.tar.gz
|
||||
|
||||
yarn release:static
|
||||
yarn test:static-release
|
||||
yarn package
|
||||
}
|
||||
|
||||
main "$@"
|
||||
18
ci/steps/release.sh
Executable file
18
ci/steps/release.sh
Executable file
@@ -0,0 +1,18 @@
|
||||
#!/usr/bin/env bash
|
||||
set -euo pipefail
|
||||
|
||||
main() {
|
||||
cd "$(dirname "$0")/../.."
|
||||
|
||||
yarn --frozen-lockfile
|
||||
yarn vscode
|
||||
yarn build
|
||||
yarn build:vscode
|
||||
yarn release
|
||||
|
||||
# https://github.com/actions/upload-artifact/issues/38
|
||||
mkdir -p release-npm-package
|
||||
tar -czf release-npm-package/package.tar.gz release
|
||||
}
|
||||
|
||||
main "$@"
|
||||
17
ci/steps/test.sh
Executable file
17
ci/steps/test.sh
Executable file
@@ -0,0 +1,17 @@
|
||||
#!/usr/bin/env bash
|
||||
set -euo pipefail
|
||||
|
||||
main() {
|
||||
cd "$(dirname "$0")/../.."
|
||||
|
||||
yarn --frozen-lockfile
|
||||
|
||||
git submodule update --init
|
||||
# We do not `yarn vscode` to make test.sh faster.
|
||||
# If the patch fails to apply, then it's likely already applied
|
||||
yarn vscode:patch &> /dev/null || true
|
||||
|
||||
yarn test
|
||||
}
|
||||
|
||||
main "$@"
|
||||
@@ -1,4 +0,0 @@
|
||||
{
|
||||
"extends": "../tsconfig.json",
|
||||
"include": ["./**/*.ts"]
|
||||
}
|
||||
@@ -1,13 +1,34 @@
|
||||
# Contributing
|
||||
|
||||
## Development Workflow
|
||||
- [Detailed CI and build process docs](../ci)
|
||||
|
||||
- [VS Code prerequisites](https://github.com/Microsoft/vscode/wiki/How-to-Contribute#prerequisites)
|
||||
## Requirements
|
||||
|
||||
Please refer to [VS Code's prerequisites](https://github.com/Microsoft/vscode/wiki/How-to-Contribute#prerequisites).
|
||||
|
||||
Differences:
|
||||
|
||||
- We require a minimum of node v12 but later versions should work.
|
||||
- We use [fnpm](https://github.com/goreleaser/nfpm) to build `.deb` and `.rpm` packages.
|
||||
- The [CI container](../ci/container/Dockerfile) is a useful reference for all our dependencies.
|
||||
|
||||
## Development Workflow
|
||||
|
||||
```shell
|
||||
yarn
|
||||
yarn vscode
|
||||
yarn watch # Visit http://localhost:8080 once completed.
|
||||
yarn watch
|
||||
# Visit http://localhost:8080 once the build completed.
|
||||
```
|
||||
|
||||
To develop inside of an isolated docker container:
|
||||
|
||||
```shell
|
||||
./ci/dev/container/exec.sh
|
||||
|
||||
root@12345:/code-server# yarn
|
||||
root@12345:/code-server# yarn vscode
|
||||
root@12345:/code-server# yarn watch
|
||||
```
|
||||
|
||||
Any changes made to the source will be live reloaded.
|
||||
@@ -15,16 +36,82 @@ Any changes made to the source will be live reloaded.
|
||||
If changes are made to the patch and you've built previously you must manually
|
||||
reset VS Code then run `yarn vscode:patch`.
|
||||
|
||||
Some docs are available at [../src/node/app](../src/node/app) on how code-server
|
||||
works internally.
|
||||
|
||||
## Build
|
||||
|
||||
- [VS Code prerequisites](https://github.com/Microsoft/vscode/wiki/How-to-Contribute#prerequisites)
|
||||
|
||||
```shell
|
||||
yarn
|
||||
yarn vscode
|
||||
yarn build
|
||||
node ./build/out/node/entry.js # Run the built JavaScript with Node.
|
||||
yarn build:vscode
|
||||
yarn release
|
||||
cd release
|
||||
yarn --production
|
||||
# Runs the built JavaScript with Node.
|
||||
node .
|
||||
```
|
||||
|
||||
Now you can make it static and build packages with:
|
||||
|
||||
```
|
||||
yarn release:static
|
||||
yarn test:static-release
|
||||
yarn package
|
||||
# The static release is in ./release-static
|
||||
# .deb, .rpm and the static archive are in ./release-packages
|
||||
```
|
||||
|
||||
## Structure
|
||||
|
||||
The `code-server` script serves an HTTP API to login and start 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.
|
||||
|
||||
### 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.
|
||||
|
||||
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
|
||||
[./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.
|
||||
|
||||
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
|
||||
work as expected.
|
||||
|
||||
197
doc/FAQ.md
197
doc/FAQ.md
@@ -1,9 +1,32 @@
|
||||
<!-- START doctoc generated TOC please keep comment here to allow auto update -->
|
||||
<!-- DON'T EDIT THIS SECTION, INSTEAD RE-RUN doctoc TO UPDATE -->
|
||||
# FAQ
|
||||
|
||||
- [Questions?](#questions)
|
||||
- [What's the deal with extensions?](#whats-the-deal-with-extensions)
|
||||
- [Where are extensions stored?](#where-are-extensions-stored)
|
||||
- [How is this different from VS Code Codespaces?](#how-is-this-different-from-vs-code-codespaces)
|
||||
- [How should I expose code-server to the internet?](#how-should-i-expose-code-server-to-the-internet)
|
||||
- [How do I securely access web services?](#how-do-i-securely-access-web-services)
|
||||
- [Sub-domains](#sub-domains)
|
||||
- [Sub-paths](#sub-paths)
|
||||
- [Multi-tenancy](#multi-tenancy)
|
||||
- [Docker in code-server docker container?](#docker-in-code-server-docker-container)
|
||||
- [Collaboration](#collaboration)
|
||||
- [How can I disable telemetry?](#how-can-i-disable-telemetry)
|
||||
- [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)
|
||||
- [How does the config file work?](#how-does-the-config-file-work)
|
||||
- [Enterprise](#enterprise)
|
||||
|
||||
<!-- END doctoc generated TOC please keep comment here to allow auto update -->
|
||||
|
||||
## Questions?
|
||||
|
||||
Please file all questions and support requests at https://www.reddit.com/r/codeserver/
|
||||
The issue tracker is only for bugs.
|
||||
Please file all questions and support requests at https://www.reddit.com/r/codeserver/.
|
||||
|
||||
The issue tracker is **only** for bugs.
|
||||
|
||||
## What's the deal with extensions?
|
||||
|
||||
@@ -19,44 +42,63 @@ As a result, Coder has created its own marketplace for open source extensions. I
|
||||
GitHub for VS Code extensions and building them. It's not perfect but getting better by the day with
|
||||
more and more extensions.
|
||||
|
||||
Issue [https://github.com/cdr/code-server/issues/1299](#1299) is a big one in making the experience here
|
||||
Issue [#1299](https://github.com/cdr/code-server/issues/1299) is a big one in making the experience here
|
||||
better by allowing the community to submit extensions and repos to avoid waiting until the scraper finds
|
||||
an extension.
|
||||
|
||||
If an extension does not work, try to grab its VSIX from its Github releases or build it yourself and
|
||||
copy it to the extensions folder.
|
||||
If an extension is not available or does not work, you can grab its VSIX from its Github releases or
|
||||
build it yourself. Then run the `Extensions: Install from VSIX` command in the Command Palette and
|
||||
point to the .vsix file.
|
||||
|
||||
## How is this different from VS Code Online?
|
||||
See below for installing an extension from the cli.
|
||||
|
||||
VS Code Online is a closed source managed service by Microsoft and only runs on Azure.
|
||||
Feel free to file an issue to add a missing extension to the marketplace.
|
||||
|
||||
code-server is open source and can be freely run on any machine.
|
||||
If you have your own custom marketplace, it is possible to point code-server to it by setting
|
||||
`$SERVICE_URL` and `$ITEM_URL` to point to it.
|
||||
|
||||
## Where are extensions stored?
|
||||
|
||||
Defaults to `~/.local/share/code-server/extensions`.
|
||||
|
||||
If the `XDG_DATA_HOME` environment variable is set the data directory will be
|
||||
`$XDG_DATA_HOME/code-server/extensions`. In general we try to follow the XDG directory spec.
|
||||
|
||||
You can install an extension on the CLI with:
|
||||
|
||||
```bash
|
||||
# From the Coder extension marketplace
|
||||
code-server --install-extension ms-python.python
|
||||
|
||||
# From a downloaded VSIX on the file system
|
||||
code-server --install-extension downloaded-ms-python.python.vsix
|
||||
```
|
||||
|
||||
## How is this different from VS Code Codespaces?
|
||||
|
||||
VS Code Codespaces is a closed source and paid service by Microsoft. It also allows you to access
|
||||
VS Code via the browser.
|
||||
|
||||
However, code-server is free, open source and can be ran on any machine without any limitations.
|
||||
|
||||
While you can self host environments with VS Code Codespaces, you still need to an Azure billing
|
||||
account and you access VS Code via the Codespaces web dashboard instead of directly connecting to
|
||||
your instance.
|
||||
|
||||
## How should I expose code-server to the internet?
|
||||
|
||||
By far the most secure method of using code-server is via
|
||||
[sshcode](https://github.com/codercom/sshcode) as it runs code-server and then forwards
|
||||
its port over SSH and requires no setup on your part other than having a working SSH server.
|
||||
Please follow [./guide.md](./guide.md) for our recommendations on setting up and using code-server.
|
||||
|
||||
You can also forward your SSH key and GPG agent to the remote machine to securely access GitHub
|
||||
and securely sign commits without duplicating your keys onto the the remote machine.
|
||||
code-server only supports password authentication natively.
|
||||
|
||||
1. https://developer.github.com/v3/guides/using-ssh-agent-forwarding/
|
||||
1. https://wiki.gnupg.org/AgentForwarding
|
||||
**note**: code-server will rate limit password authentication attempts at 2 a minute and 12 an hour.
|
||||
|
||||
If you cannot use sshcode, then you will need to ensure there is some sort of authorization in
|
||||
front of code-server and that you are using HTTPS to secure all connections.
|
||||
If you want to use external authentication (i.e sign in with Google) you should handle this
|
||||
with a reverse proxy using something like [oauth2_proxy](https://github.com/pusher/oauth2_proxy).
|
||||
|
||||
By default when listening externally, code-server enables password authentication using a
|
||||
randomly generated password so you can use that. You can set the `PASSWORD` environment variable
|
||||
to use your own instead. If you want to handle authentication yourself, use `--auth none`
|
||||
to disable password authentication.
|
||||
|
||||
If you want to use external authentication you should handle this with a reverse
|
||||
proxy using something like [oauth2_proxy](https://github.com/pusher/oauth2_proxy).
|
||||
|
||||
For HTTPS, you can use a self signed certificate by passing in just `--cert` or pass in an existing
|
||||
certificate by providing the path to `--cert` and the path to its key with `--cert-key`.
|
||||
For HTTPS, you can use a self signed certificate by passing in just `--cert` or
|
||||
pass in an existing certificate by providing the path to `--cert` and the path to
|
||||
its key with `--cert-key`.
|
||||
|
||||
If `code-server` has been passed a certificate it will also respond to HTTPS
|
||||
requests and will redirect all HTTP requests to HTTPS. Otherwise it will respond
|
||||
@@ -65,16 +107,38 @@ only to HTTP requests.
|
||||
You can use [Let's Encrypt](https://letsencrypt.org/) to get an SSL certificate
|
||||
for free.
|
||||
|
||||
## x86 releases?
|
||||
Again, Please follow [./guide.md](./guide.md) for our recommendations on setting up and using code-server.
|
||||
|
||||
node has dropped support for x86 and so we decided to as well. See
|
||||
[nodejs/build/issues/885](https://github.com/nodejs/build/issues/885).
|
||||
## How do I securely access web services?
|
||||
|
||||
## Alpine builds?
|
||||
code-server is capable of proxying to any port using either a subdomain or a
|
||||
subpath which means you can securely access these services using code-server's
|
||||
built-in authentication.
|
||||
|
||||
Just install `libc-dev` and code-server should work.
|
||||
### Sub-domains
|
||||
|
||||
## Multi Tenancy
|
||||
You will need a DNS entry that points to your server for each port you want to
|
||||
access. You can either set up a wildcard DNS entry for `*.<domain>` if your domain
|
||||
name registrar supports it or you can create one for every port you want to
|
||||
access (`3000.<domain>`, `8080.<domain>`, etc).
|
||||
|
||||
You should also set up TLS certificates for these subdomains, either using a
|
||||
wildcard certificate for `*.<domain>` or individual certificates for each port.
|
||||
|
||||
Start code-server with the `--proxy-domain` flag set to your domain.
|
||||
|
||||
```
|
||||
code-server --proxy-domain <domain>
|
||||
```
|
||||
|
||||
Now you can browse to `<port>.<domain>`. Note that this uses the host header so
|
||||
ensure your reverse proxy forwards that information if you are using one.
|
||||
|
||||
### Sub-paths
|
||||
|
||||
Just browse to `/proxy/<port>/`.
|
||||
|
||||
## Multi-tenancy
|
||||
|
||||
If you want to run multiple code-server's on shared infrastructure, we recommend using virtual
|
||||
machines with a VM per user. This will easily allow users to run a docker daemon. If you want
|
||||
@@ -96,14 +160,77 @@ to make volume mounts in any other directory work.
|
||||
|
||||
## Collaboration
|
||||
|
||||
At the moment we have no plans for multi user collaboration on code-server but we understand there is strong
|
||||
demand and will work on it when the time is right.
|
||||
We understand the high demand but the team is swamped right now.
|
||||
|
||||
You can follow progress at [#33](https://github.com/cdr/code-server/issues/33).
|
||||
|
||||
## How can I disable telemetry?
|
||||
|
||||
Use the `--disable-telemetry` flag to completely disable telemetry. We use the
|
||||
data collected only to improve code-server.
|
||||
|
||||
## How does code-server decide what workspace or folder to open?
|
||||
|
||||
code-server tries the following in order:
|
||||
|
||||
1. The `workspace` query parameter.
|
||||
2. The `folder` query parameter.
|
||||
3. The workspace or directory passed on the command line.
|
||||
4. The last opened workspace or directory.
|
||||
|
||||
## How do I debug issues with code-server?
|
||||
|
||||
First run code-server with at least `debug` logging (or `trace` to be really
|
||||
thorough) by setting the `--log` flag or the `LOG_LEVEL` environment variable.
|
||||
`-vvv` and `--verbose` are aliases for `--log trace`.
|
||||
|
||||
```
|
||||
code-server --log debug
|
||||
```
|
||||
|
||||
Once this is done, replicate the issue you're having then collect logging
|
||||
information from the following places:
|
||||
|
||||
1. stdout
|
||||
2. The most recently created directory in the `~/.local/share/code-server/logs` directory.
|
||||
3. The browser console and network tabs.
|
||||
|
||||
Additionally, collecting core dumps (you may need to enable them first) if
|
||||
code-server crashes can be helpful.
|
||||
|
||||
## Heartbeat File
|
||||
|
||||
`code-server` touches `~/.local/share/code-server/heartbeat` once a minute as long
|
||||
as there is an active browser connection.
|
||||
|
||||
If you want to shutdown `code-server` if there hasn't been an active connection in X minutes
|
||||
you can do so by continuously checking the last modified time on the heartbeat file and if it is
|
||||
older than X minutes, you should kill `code-server`.
|
||||
|
||||
[#1636](https://github.com/cdr/code-server/issues/1636) will make the experience here better.
|
||||
|
||||
## 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
|
||||
like this:
|
||||
|
||||
```yaml
|
||||
bind-addr: 127.0.0.1:8080
|
||||
auth: password
|
||||
password: mewkmdasosafuio3422 # This is randomly generated for each config.yaml
|
||||
cert: false
|
||||
```
|
||||
|
||||
Each key in the file maps directly to a `code-server` flag. Run `code-server --help` to see
|
||||
a listing of all the flags.
|
||||
|
||||
The default config here says to listen on the loopback IP port 8080, enable password authorization
|
||||
and no TLS. Any flags passed to `code-server` will take priority over the config file.
|
||||
|
||||
The `--config` flag or `$CODE_SERVER_CONFIG` can be used to change the config file's location.
|
||||
|
||||
The default location also respects `$XDG_CONFIG_HOME`.
|
||||
|
||||
## Enterprise
|
||||
|
||||
Visit [our enterprise page](https://coder.com) for more information about our
|
||||
|
||||
@@ -1,24 +0,0 @@
|
||||
<?xml version="1.0" encoding="UTF-8"?>
|
||||
<svg width="200px" height="40px" viewBox="0 0 200 40" version="1.1" xmlns="http://www.w3.org/2000/svg" xmlns:xlink="http://www.w3.org/1999/xlink">
|
||||
<!-- Generator: Sketch 52.5 (67469) - http://www.bohemiancoding.com/sketch -->
|
||||
<title>do-btn-blue-ghost</title>
|
||||
<desc>Created with Sketch.</desc>
|
||||
<g id="Page-1" stroke="none" stroke-width="1" fill="none" fill-rule="evenodd">
|
||||
<g id="Partner-welcome-kit-Copy-3" transform="translate(-651.000000, -828.000000)">
|
||||
<g id="do-btn-blue-ghost" transform="translate(651.000000, 828.000000)">
|
||||
<rect id="Rectangle-Copy-4" stroke="#0069FF" x="0.5" y="0.5" width="199" height="39" rx="6"></rect>
|
||||
<path d="M6,0 L47,0 L47,40 L6,40 C2.6862915,40 4.05812251e-16,37.3137085 0,34 L-8.8817842e-16,6 C-1.29399067e-15,2.6862915 2.6862915,6.08718376e-16 6,0 Z" id="Rectangle-Copy-5" fill="#0069FF"></path>
|
||||
<g id="DO_Logo_horizontal_blue-Copy-3" transform="translate(13.000000, 10.000000)" fill="#FFFFFF">
|
||||
<path d="M10.0098493,20 L10.0098493,16.1262429 C14.12457,16.1262429 17.2897398,12.0548452 15.7269372,7.74627862 C15.1334679,6.14538921 13.8674,4.86072487 12.2650328,4.28756693 C7.952489,2.72620566 3.87733294,5.88845634 3.87733294,9.99938223 C3.87733294,9.99938223 3.87733294,9.99938223 3.87733294,9.99938223 L0,9.99938223 C0,3.45747613 6.3303395,-1.64165309 13.1948014,0.492866119 C16.2017127,1.42177726 18.57559,3.81322933 19.5053586,6.79760341 C21.6418482,13.6754986 16.5577943,20 10.0098493,20 Z" id="XMLID_49_"></path>
|
||||
<polygon id="XMLID_47_" points="9.52380952 16.1904762 5.71428571 16.1904762 5.71428571 12.3809524 5.71428571 12.3809524 9.52380952 12.3809524 9.52380952 12.3809524"></polygon>
|
||||
<polygon id="XMLID_46_" points="6.66666667 19.047619 3.80952381 19.047619 3.80952381 19.047619 3.80952381 16.1904762 6.66666667 16.1904762"></polygon>
|
||||
<polygon id="XMLID_45_" points="3.80952381 16.1904762 0.952380952 16.1904762 0.952380952 16.1904762 0.952380952 13.3333333 0.952380952 13.3333333 3.80952381 13.3333333 3.80952381 13.3333333"></polygon>
|
||||
</g>
|
||||
<!-- Modified to add GitHub font-family after DigitalOcean's font-family, otherwise it looks bad on GitHub -->
|
||||
<text id="Create-a-Droplet-Copy-3" font-family="Sailec-Medium, Sailec, -apple-system, BlinkMacSystemFont, Segoe UI, Helvetica, Arial, sans-serif, Apple Color Emoji, Segoe UI Emoji, Segoe UI Symbol" font-size="16" font-weight="400" fill="#0069FF">
|
||||
<tspan x="58" y="26">Create a Droplet</tspan>
|
||||
</text>
|
||||
</g>
|
||||
</g>
|
||||
</g>
|
||||
</svg>
|
||||
|
Before Width: | Height: | Size: 2.7 KiB |
249
doc/guide.md
Normal file
249
doc/guide.md
Normal file
@@ -0,0 +1,249 @@
|
||||
<!-- START doctoc generated TOC please keep comment here to allow auto update -->
|
||||
<!-- DON'T EDIT THIS SECTION, INSTEAD RE-RUN doctoc TO UPDATE -->
|
||||
# Setup Guide
|
||||
|
||||
- [1. Acquire a remote machine](#1-acquire-a-remote-machine)
|
||||
- [Requirements](#requirements)
|
||||
- [Google Cloud](#google-cloud)
|
||||
- [2. Install code-server](#2-install-code-server)
|
||||
- [3. Expose code-server](#3-expose-code-server)
|
||||
- [SSH forwarding](#ssh-forwarding)
|
||||
- [Let's Encrypt](#lets-encrypt)
|
||||
- [Self Signed Certificate](#self-signed-certificate)
|
||||
- [Change the password?](#change-the-password)
|
||||
- [How do I securely access development web services?](#how-do-i-securely-access-development-web-services)
|
||||
|
||||
<!-- END doctoc generated TOC please keep comment here to allow auto update -->
|
||||
|
||||
This guide demonstrates how to setup and use code-server.
|
||||
To reiterate, code-server lets you run VS Code on a remote server and then access it via a browser.
|
||||
|
||||
Further docs are at:
|
||||
|
||||
- [README.md](../README.md) for a general overview
|
||||
- [FAQ.md](./FAQ.md) for common questions.
|
||||
- [CONTRIBUTING.md](../doc/CONTRIBUTING.md) for development docs
|
||||
|
||||
We'll walk you through acquiring a remote machine to run code-server on and then exposing `code-server` so you can
|
||||
securely access it.
|
||||
|
||||
## 1. Acquire a remote machine
|
||||
|
||||
First, you need a machine to run code-server on. You can use a physical
|
||||
machine you have lying around or use a VM on GCP/AWS.
|
||||
|
||||
### Requirements
|
||||
|
||||
For a good experience, we recommend at least:
|
||||
|
||||
- 1 GB of RAM
|
||||
- 2 cores
|
||||
|
||||
You can use whatever linux distribution floats your boat but in this guide we assume Debian on Google Cloud.
|
||||
|
||||
### Google Cloud
|
||||
|
||||
For demonstration purposes, this guide assumes you're using a VM on GCP but you should be
|
||||
able to easily use any machine or VM provider.
|
||||
|
||||
You can sign up at https://console.cloud.google.com/getting-started. You'll get a 12 month \$300
|
||||
free trial.
|
||||
|
||||
Once you've signed up and created a GCP project, create a new Compute Engine VM Instance.
|
||||
|
||||
1. Navigate to `Compute Engine -> VM Instances` on the sidebar.
|
||||
2. Now click `Create Instance` to create a new instance.
|
||||
3. Name it whatever you want.
|
||||
4. Choose the region closest to you based on [gcping.com](http://www.gcping.com).
|
||||
5. Any zone is fine.
|
||||
6. We'd recommend a `E2` series instance from the General-purpose family.
|
||||
- Change the type to custom and set at least 2 cores and 2 GB of ram.
|
||||
- Add more vCPUs and memory as you prefer, you can edit after creating the instance as well.
|
||||
- https://cloud.google.com/compute/docs/machine-types#general_purpose
|
||||
7. We highly recommend switching the persistent disk to a SSD of at least 32 GB.
|
||||
- Click `Change` under `Boot Disk` and change the type to `SSD Persistent Disk` and the size
|
||||
to `32`.
|
||||
- You can always grow your disk later.
|
||||
- The default OS of Debian 10 is fine.
|
||||
8. Navigate to `Networking -> Network interfaces` and edit the existing interface
|
||||
to use a static external IP.
|
||||
- Click done to save network interface changes.
|
||||
9. If you do not have a [project wide SSH key](https://cloud.google.com/compute/docs/instances/adding-removing-ssh-keys#project-wide), navigate to `Security - > SSH Keys` and add your public key there.
|
||||
10. Click create!
|
||||
|
||||
Remember, you can shutdown your server when not in use to lower costs.
|
||||
We highly recommend learning to use the [`gcloud`](https://cloud.google.com/sdk/gcloud) cli
|
||||
to avoid the slow dashboard.
|
||||
|
||||
## 2. Install code-server
|
||||
|
||||
SSH into your instance and run the appropriate commands documented in [README.md](../README.md).
|
||||
|
||||
Assuming Debian:
|
||||
|
||||
```bash
|
||||
curl -sSOL https://github.com/cdr/code-server/releases/download/v3.3.1/code-server_3.3.1_amd64.deb
|
||||
sudo dpkg -i code-server_3.3.1_amd64.deb
|
||||
systemctl --user enable --now code-server
|
||||
# Now code-server is running at http://127.0.0.1:8080
|
||||
# Your password is in ~/.config/code-server/config.yaml
|
||||
```
|
||||
|
||||
## 3. Expose code-server
|
||||
|
||||
**Never**, **ever** expose `code-server` directly to the internet without some form of authentication
|
||||
and encryption as someone can completely takeover your machine with the terminal.
|
||||
|
||||
There are several approaches to securely operating and exposing code-server.
|
||||
|
||||
By default, code-server will enable password authentication which will
|
||||
require you to copy the password from the code-server config file to login. You
|
||||
can also set a custom password with `$PASSWORD`.
|
||||
|
||||
**tip**: You can list the full set of code-server options with `code-server --help`
|
||||
|
||||
### SSH forwarding
|
||||
|
||||
We highly recommend this approach for not requiring any additional setup, you just need an
|
||||
SSH server on your remote machine. The downside is you won't be able to access `code-server`
|
||||
without an SSH client like an iPad. If that's important to you, skip to [Let's Encrypt](#lets-encrypt).
|
||||
|
||||
Recommended reading: https://help.ubuntu.com/community/SSH/OpenSSH/PortForwarding.
|
||||
|
||||
First, ssh into your instance and edit your code-server config file to disable password authentication.
|
||||
|
||||
```bash
|
||||
# Replaces "auth: password" with "auth: none" in the code-server config.
|
||||
sed -i.bak 's/auth: password/auth: none/' ~/.config/code-server/config.yaml
|
||||
```
|
||||
|
||||
Restart code-server with (assuming you followed the guide):
|
||||
|
||||
```bash
|
||||
systemctl --user restart code-server
|
||||
```
|
||||
|
||||
Now forward local port 8080 to `127.0.0.1:8080` on the remote instance.
|
||||
|
||||
```bash
|
||||
# -N disables executing a remote shell
|
||||
ssh -N -L 8080:127.0.0.1:8080 <instance-ip>
|
||||
```
|
||||
|
||||
Now if you access http://127.0.0.1:8080 locally, you should see code-server!
|
||||
|
||||
If you want to make the SSH port forwarding persistent we recommend using
|
||||
[mutagen](https://mutagen.io/documentation/introduction/installation).
|
||||
|
||||
```
|
||||
# Same as the above SSH command but runs in the background continously.
|
||||
# Add `mutagen daemon start` to your ~/.bashrc to start the mutagen daemon when you open a shell.
|
||||
mutagen forward create --name=code-server tcp:127.0.0.1:8080 <instance-ip>:tcp:127.0.0.1:8080
|
||||
```
|
||||
|
||||
We also recommend adding the following lines to your `~/.ssh/config` to quickly detect bricked SSH connections:
|
||||
|
||||
```bash
|
||||
Host *
|
||||
ServerAliveInterval 5
|
||||
ExitOnForwardFailure yes
|
||||
```
|
||||
|
||||
You can also forward your SSH key and GPG agent to the instance to securely access GitHub
|
||||
and sign commits without copying your keys onto the instance.
|
||||
|
||||
1. https://developer.github.com/v3/guides/using-ssh-agent-forwarding/
|
||||
2. https://wiki.gnupg.org/AgentForwarding
|
||||
|
||||
### Let's Encrypt
|
||||
|
||||
[Let's Encrypt](https://letsencrypt.org) is a great option if you want to access code-server on an iPad
|
||||
or do not want to use SSH forwarding. This does require that the remote machine is exposed to the internet.
|
||||
|
||||
Assuming you have been following the guide, edit your instance and checkmark the allow HTTP/HTTPS traffic options.
|
||||
|
||||
1. You'll need to buy a domain name. We recommend [Google Domains](https://domains.google.com).
|
||||
2. Add an A record to your domain with your instance's IP.
|
||||
3. Install caddy https://caddyserver.com/docs/download#debian-ubuntu-raspbian.
|
||||
|
||||
```bash
|
||||
echo "deb [trusted=yes] https://apt.fury.io/caddy/ /" \
|
||||
| sudo tee -a /etc/apt/sources.list.d/caddy-fury.list
|
||||
sudo apt update
|
||||
sudo apt install caddy
|
||||
```
|
||||
|
||||
4. Replace `/etc/caddy/Caddyfile` with sudo to look like this:
|
||||
|
||||
```
|
||||
mydomain.com
|
||||
|
||||
reverse_proxy 127.0.0.1:8080
|
||||
```
|
||||
|
||||
5. Reload caddy with:
|
||||
|
||||
```bash
|
||||
sudo systemctl reload caddy
|
||||
```
|
||||
|
||||
Visit `https://<your-domain-name>` to access code-server. Congratulations!
|
||||
|
||||
In a future release we plan to integrate Let's Encrypt directly with code-server to avoid
|
||||
the dependency on caddy.
|
||||
|
||||
### Self Signed Certificate
|
||||
|
||||
**note:** Self signed certificates do not work with iPad and will cause a blank page. You'll
|
||||
have to use [Let's Encrypt](#lets-encrypt) instead.
|
||||
|
||||
Recommended reading: https://security.stackexchange.com/a/8112.
|
||||
|
||||
We recommend this as a last resort as self signed certificates do not work with iPads and can
|
||||
cause other bizarre issues. Not to mention all the warnings when you access code-server.
|
||||
Only use this if:
|
||||
|
||||
1. You do not want to buy a domain.
|
||||
2. You cannot expose the remote machine to the internet.
|
||||
3. You do not want to use SSH forwarding.
|
||||
|
||||
ssh into your instance and edit your code-server config file to use a randomly generated self signed certificate:
|
||||
|
||||
```bash
|
||||
# Replaces "cert: false" with "cert: true" in the code-server config.
|
||||
sed -i.bak 's/cert: false/cert: true/' ~/.config/code-server/config.yaml
|
||||
# Replaces "bind-addr: 127.0.0.1:8080" with "bind-addr: 0.0.0.0:443" in the code-server config.
|
||||
sed -i.bak 's/bind-addr: 127.0.0.1:8080/bind-addr: 0.0.0.0:443/' ~/.config/code-server/config.yaml
|
||||
# Allows code-server to listen on port 443.
|
||||
sudo setcap cap_net_bind_service=+ep /usr/lib/code-server/lib/node
|
||||
```
|
||||
|
||||
Assuming you have been following the guide, restart code-server with:
|
||||
|
||||
```bash
|
||||
systemctl --user restart code-server
|
||||
```
|
||||
|
||||
Edit your instance and checkmark the allow HTTPS traffic option.
|
||||
|
||||
Visit `https://<your-instance-ip>` to access code-server.
|
||||
You'll get a warning when accessing but if you click through you should be good.
|
||||
|
||||
To avoid the warnings, you can use [mkcert](https://mkcert.dev) to create a self signed certificate
|
||||
trusted by your OS and then pass it into code-server via the `cert` and `cert-key` config
|
||||
fields.
|
||||
|
||||
### Change the password?
|
||||
|
||||
Edit the code-server config file at `~/.config/code-server/config.yaml` and then restart
|
||||
code-server with:
|
||||
|
||||
```bash
|
||||
systemctl --user restart code-server
|
||||
```
|
||||
|
||||
### How do I securely access development web services?
|
||||
|
||||
If you're working on a web service and want to access it locally, code-server can proxy it for you.
|
||||
|
||||
See [FAQ.md](https://github.com/cdr/code-server/blob/master/doc/FAQ.md#how-do-i-securely-access-web-services).
|
||||
34
doc/npm.md
Normal file
34
doc/npm.md
Normal file
@@ -0,0 +1,34 @@
|
||||
# npm Install Requirements
|
||||
|
||||
If you're installing the npm module you'll need certain dependencies to build
|
||||
the native modules used by VS Code.
|
||||
|
||||
You also need at least node v12 installed. See [#1633](https://github.com/cdr/code-server/issues/1633).
|
||||
|
||||
## Ubuntu, Debian
|
||||
|
||||
```bash
|
||||
sudo apt-get install -y \
|
||||
build-essential \
|
||||
pkg-config \
|
||||
libx11-dev \
|
||||
libxkbfile-dev \
|
||||
libsecret-1-dev
|
||||
```
|
||||
|
||||
## Fedora, Red Hat, SUSE
|
||||
|
||||
```bash
|
||||
sudo yum groupinstall -y 'Development Tools'
|
||||
sudo yum config-manager --set-enabled PowerTools
|
||||
sudo yum install -y python2 libsecret-devel libX11-devel libxkbfile-devel
|
||||
npm config set python python2
|
||||
```
|
||||
|
||||
## macOS
|
||||
|
||||
Install [Xcode](https://developer.apple.com/xcode/downloads/) and run:
|
||||
|
||||
```bash
|
||||
xcode-select --install
|
||||
```
|
||||
Submodule lib/vscode updated: 78a4c91400...5763d909d5
70
package.json
70
package.json
@@ -1,22 +1,39 @@
|
||||
{
|
||||
"name": "code-server",
|
||||
"license": "MIT",
|
||||
"version": "3.0.1",
|
||||
"scripts": {
|
||||
"clean": "ci/clean.sh",
|
||||
"vscode": "ci/vscode.sh",
|
||||
"vscode:patch": "cd ./lib/vscode && git apply ../../ci/vscode.patch",
|
||||
"vscode:diff": "cd ./lib/vscode && git diff HEAD > ../../ci/vscode.patch",
|
||||
"test": "mocha -r ts-node/register ./test/*.test.ts",
|
||||
"lint": "ci/lint.sh",
|
||||
"fmt": "ci/fmt.sh",
|
||||
"runner": "cd ./ci && NODE_OPTIONS=--max_old_space_size=32384 ts-node ./build.ts",
|
||||
"build": "yarn runner build",
|
||||
"watch": "yarn runner watch"
|
||||
"version": "3.3.1",
|
||||
"description": "Run VS Code on a remote server.",
|
||||
"homepage": "https://github.com/cdr/code-server",
|
||||
"bugs": {
|
||||
"url": "https://github.com/cdr/code-server/issues"
|
||||
},
|
||||
"repository": "https://github.com/cdr/code-server",
|
||||
"scripts": {
|
||||
"clean": "./ci/build/clean.sh",
|
||||
"vscode": "./ci/dev/vscode.sh",
|
||||
"vscode:patch": "./ci/dev/patch-vscode.sh",
|
||||
"vscode:diff": "./ci/dev/diff-vscode.sh",
|
||||
"build": "./ci/build/build-code-server.sh",
|
||||
"build:vscode": "./ci/build/build-vscode.sh",
|
||||
"release": "./ci/build/build-release.sh",
|
||||
"release:static": "./ci/build/build-static-release.sh",
|
||||
"release:github-draft": "./ci/build/release-github-draft.sh",
|
||||
"release:github-assets": "./ci/build/release-github-assets.sh",
|
||||
"test:static-release": "./ci/build/test-static-release.sh",
|
||||
"package": "./ci/build/build-packages.sh",
|
||||
"_____": "",
|
||||
"fmt": "./ci/dev/fmt.sh",
|
||||
"lint": "./ci/dev/lint.sh",
|
||||
"test": "./ci/dev/test.sh",
|
||||
"ci": "./ci/dev/ci.sh",
|
||||
"watch": "NODE_OPTIONS=--max_old_space_size=32384 ts-node ./ci/dev/watch.ts"
|
||||
},
|
||||
"main": "out/node/entry.js",
|
||||
"devDependencies": {
|
||||
"@types/adm-zip": "^0.4.32",
|
||||
"@types/fs-extra": "^8.0.1",
|
||||
"@types/http-proxy": "^1.17.4",
|
||||
"@types/js-yaml": "^3.12.3",
|
||||
"@types/mocha": "^5.2.7",
|
||||
"@types/node": "^12.12.7",
|
||||
"@types/parcel-bundler": "^1.12.1",
|
||||
@@ -24,12 +41,11 @@
|
||||
"@types/safe-compare": "^1.1.0",
|
||||
"@types/semver": "^7.1.0",
|
||||
"@types/tar-fs": "^1.16.2",
|
||||
"@types/ssh2": "0.5.39",
|
||||
"@types/ssh2-streams": "^0.1.6",
|
||||
"@types/tar-stream": "^1.6.1",
|
||||
"@types/ws": "^6.0.4",
|
||||
"@typescript-eslint/eslint-plugin": "^2.0.0",
|
||||
"@typescript-eslint/parser": "^2.0.0",
|
||||
"doctoc": "^1.4.0",
|
||||
"eslint": "^6.2.0",
|
||||
"eslint-config-prettier": "^6.0.0",
|
||||
"eslint-plugin-import": "^2.18.2",
|
||||
@@ -37,7 +53,7 @@
|
||||
"leaked-handles": "^5.2.0",
|
||||
"mocha": "^6.2.0",
|
||||
"parcel-bundler": "^1.12.4",
|
||||
"prettier": "^1.18.2",
|
||||
"prettier": "^2.0.5",
|
||||
"stylelint": "^13.0.0",
|
||||
"stylelint-config-recommended": "^3.0.0",
|
||||
"ts-node": "^8.4.1",
|
||||
@@ -51,15 +67,33 @@
|
||||
"dependencies": {
|
||||
"@coder/logger": "1.1.11",
|
||||
"adm-zip": "^0.4.14",
|
||||
"env-paths": "^2.2.0",
|
||||
"fs-extra": "^8.1.0",
|
||||
"http-proxy": "^1.18.0",
|
||||
"httpolyglot": "^0.1.2",
|
||||
"node-pty": "^0.9.0",
|
||||
"js-yaml": "^3.13.1",
|
||||
"limiter": "^1.1.5",
|
||||
"pem": "^1.14.2",
|
||||
"safe-compare": "^1.1.4",
|
||||
"semver": "^7.1.3",
|
||||
"tar": "^6.0.1",
|
||||
"ssh2": "^0.8.7",
|
||||
"tar-fs": "^2.0.0",
|
||||
"ws": "^7.2.0"
|
||||
"ws": "^7.2.0",
|
||||
"xdg-basedir": "^4.0.0",
|
||||
"yarn": "^1.22.4"
|
||||
},
|
||||
"bin": {
|
||||
"code-server": "out/node/entry.js"
|
||||
},
|
||||
"keywords": [
|
||||
"vscode",
|
||||
"development",
|
||||
"ide",
|
||||
"coder",
|
||||
"vscode-remote",
|
||||
"browser-ide"
|
||||
],
|
||||
"engines": {
|
||||
"node": ">= 12"
|
||||
}
|
||||
}
|
||||
|
||||
@@ -8,7 +8,7 @@
|
||||
/>
|
||||
<meta
|
||||
http-equiv="Content-Security-Policy"
|
||||
content="style-src 'self' 'unsafe-inline'; manifest-src 'self'; img-src 'self' data:;"
|
||||
content="style-src 'self' 'unsafe-inline'; manifest-src 'self'; img-src 'self' data:; font-src 'self' data:;"
|
||||
/>
|
||||
<title>code-server</title>
|
||||
<link rel="icon" href="{{BASE}}/static/{{COMMIT}}/src/browser/media/favicon.ico" type="image/x-icon" />
|
||||
|
||||
@@ -6,7 +6,10 @@
|
||||
name="viewport"
|
||||
content="width=device-width, initial-scale=1.0, maximum-scale=1.0, minimum-scale=1.0, user-scalable=no"
|
||||
/>
|
||||
<meta http-equiv="Content-Security-Policy" content="style-src 'self'; manifest-src 'self'; img-src 'self' data:;" />
|
||||
<meta
|
||||
http-equiv="Content-Security-Policy"
|
||||
content="style-src 'self'; manifest-src 'self'; img-src 'self' data:; font-src 'self' data:;"
|
||||
/>
|
||||
<title>{{ERROR_TITLE}} - code-server</title>
|
||||
<link rel="icon" href="{{BASE}}/static/{{COMMIT}}/src/browser/media/favicon.ico" type="image/x-icon" />
|
||||
<link
|
||||
|
||||
@@ -8,7 +8,7 @@
|
||||
/>
|
||||
<meta
|
||||
http-equiv="Content-Security-Policy"
|
||||
content="style-src 'self' 'unsafe-inline'; manifest-src 'self'; img-src 'self' data:;"
|
||||
content="style-src 'self' 'unsafe-inline'; manifest-src 'self'; img-src 'self' data:; font-src 'self' data:;"
|
||||
/>
|
||||
<title>code-server</title>
|
||||
<link rel="icon" href="{{BASE}}/static/{{COMMIT}}/src/browser/media/favicon.ico" type="image/x-icon" />
|
||||
@@ -17,7 +17,7 @@
|
||||
href="{{BASE}}/static/{{COMMIT}}/src/browser/media/manifest.json"
|
||||
crossorigin="use-credentials"
|
||||
/>
|
||||
<link rel="apple-touch-icon" href="{{BASE}}/static/{{COMMIT}}/src/browser/media/pwa-icon-384.pnggg" />
|
||||
<link rel="apple-touch-icon" href="{{BASE}}/static/{{COMMIT}}/src/browser/media/pwa-icon-384.png" />
|
||||
<link href="{{BASE}}/static/{{COMMIT}}/dist/pages/app.css" rel="stylesheet" />
|
||||
<meta id="coder-options" data-settings="{{OPTIONS}}" />
|
||||
</head>
|
||||
|
||||
@@ -8,7 +8,7 @@
|
||||
/>
|
||||
<meta
|
||||
http-equiv="Content-Security-Policy"
|
||||
content="style-src 'self'; script-src 'self' 'unsafe-inline'; manifest-src 'self'; img-src 'self' data:;"
|
||||
content="style-src 'self'; script-src 'self' 'unsafe-inline'; manifest-src 'self'; img-src 'self' data:; font-src 'self' data:;"
|
||||
/>
|
||||
<title>code-server login</title>
|
||||
<link rel="icon" href="{{BASE}}/static/{{COMMIT}}/src/browser/media/favicon.ico" type="image/x-icon" />
|
||||
@@ -26,7 +26,7 @@
|
||||
<div class="card-box">
|
||||
<div class="header">
|
||||
<h1 class="main">Welcome to code-server</h1>
|
||||
<div class="sub">Please log in below. Check code-server's logs for the generated password.</div>
|
||||
<div class="sub">Please log in below. {{PASSWORD_MSG}}</div>
|
||||
</div>
|
||||
<div class="content">
|
||||
<form class="login-form" method="post">
|
||||
|
||||
@@ -6,7 +6,10 @@
|
||||
name="viewport"
|
||||
content="width=device-width, initial-scale=1.0, maximum-scale=1.0, minimum-scale=1.0, user-scalable=no"
|
||||
/>
|
||||
<meta http-equiv="Content-Security-Policy" content="style-src 'self'; manifest-src 'self'; img-src 'self' data:;" />
|
||||
<meta
|
||||
http-equiv="Content-Security-Policy"
|
||||
content="style-src 'self'; manifest-src 'self'; img-src 'self' data:; font-src 'self' data:;"
|
||||
/>
|
||||
<title>code-server</title>
|
||||
<link rel="icon" href="{{BASE}}/static/{{COMMIT}}/src/browser/media/favicon.ico" type="image/x-icon" />
|
||||
<link
|
||||
|
||||
@@ -6,7 +6,7 @@
|
||||
|
||||
<meta
|
||||
http-equiv="Content-Security-Policy"
|
||||
content="font-src 'self'; 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:;"
|
||||
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 -->
|
||||
@@ -80,7 +80,7 @@
|
||||
baseUrl: `${staticBase}/out`,
|
||||
paths: {
|
||||
"vscode-textmate": `${staticBase}/node_modules/vscode-textmate/release/main`,
|
||||
"onigasm-umd": `${staticBase}/node_modules/onigasm-umd/release/main`,
|
||||
"vscode-oniguruma": `${staticBase}/node_modules/vscode-oniguruma/release/main`,
|
||||
xterm: `${staticBase}/node_modules/xterm/lib/xterm.js`,
|
||||
"xterm-addon-search": `${staticBase}/node_modules/xterm-addon-search/lib/xterm-addon-search.js`,
|
||||
"xterm-addon-unicode11": `${staticBase}/node_modules/xterm-addon-unicode11/lib/xterm-addon-unicode11.js`,
|
||||
@@ -98,6 +98,13 @@
|
||||
<script data-cfasync="false" src="{{BASE}}/static/{{COMMIT}}/lib/vscode/out/vs/workbench/workbench.web.api.js"></script>
|
||||
END_PROD_ONLY -->
|
||||
<script>
|
||||
require(["vs/code/browser/workbench/workbench"], function() {})
|
||||
require(["vs/code/browser/workbench/workbench"], function () {})
|
||||
</script>
|
||||
<script>
|
||||
try {
|
||||
document.body.style.background = JSON.parse(localStorage.getItem("colorThemeData")).colorMap["editor.background"]
|
||||
} catch (error) {
|
||||
// Oh well.
|
||||
}
|
||||
</script>
|
||||
</html>
|
||||
|
||||
@@ -8,7 +8,7 @@ if ("serviceWorker" in navigator) {
|
||||
.register(path, {
|
||||
scope: options.base || "/",
|
||||
})
|
||||
.then(function() {
|
||||
.then(function () {
|
||||
console.log("[Service Worker] registered")
|
||||
})
|
||||
}
|
||||
|
||||
@@ -1,204 +0,0 @@
|
||||
import { field, logger, Logger } from "@coder/logger"
|
||||
import { Emitter } from "../common/emitter"
|
||||
import { generateUuid } from "../common/util"
|
||||
|
||||
const decoder = new TextDecoder("utf8")
|
||||
export const decode = (buffer: string | ArrayBuffer): string => {
|
||||
return typeof buffer !== "string" ? decoder.decode(buffer) : buffer
|
||||
}
|
||||
|
||||
/**
|
||||
* A web socket that reconnects itself when it closes. Sending messages while
|
||||
* disconnected will throw an error.
|
||||
*/
|
||||
export class ReconnectingSocket {
|
||||
protected readonly _onMessage = new Emitter<string | ArrayBuffer>()
|
||||
public readonly onMessage = this._onMessage.event
|
||||
protected readonly _onDisconnect = new Emitter<number | undefined>()
|
||||
public readonly onDisconnect = this._onDisconnect.event
|
||||
protected readonly _onClose = new Emitter<number | undefined>()
|
||||
public readonly onClose = this._onClose.event
|
||||
protected readonly _onConnect = new Emitter<void>()
|
||||
public readonly onConnect = this._onConnect.event
|
||||
|
||||
// This helps distinguish messages between sockets.
|
||||
private readonly logger: Logger
|
||||
|
||||
private socket?: WebSocket
|
||||
private connecting?: Promise<void>
|
||||
private closed = false
|
||||
private readonly openTimeout = 10000
|
||||
|
||||
// Every time the socket fails to connect, the retry will be increasingly
|
||||
// delayed up to a maximum.
|
||||
private readonly retryBaseDelay = 1000
|
||||
private readonly retryMaxDelay = 10000
|
||||
private retryDelay?: number
|
||||
private readonly retryDelayFactor = 1.5
|
||||
|
||||
// The socket must be connected for this amount of time before resetting the
|
||||
// retry delay. This prevents rapid retries when the socket does connect but
|
||||
// is closed shortly after.
|
||||
private resetRetryTimeout?: NodeJS.Timeout
|
||||
private readonly resetRetryDelay = 10000
|
||||
|
||||
private _binaryType: typeof WebSocket.prototype.binaryType = "arraybuffer"
|
||||
|
||||
public constructor(private path: string, public readonly id: string = generateUuid(4)) {
|
||||
// On Firefox the socket seems to somehow persist a page reload so the close
|
||||
// event runs and we see "attempting to reconnect".
|
||||
if (typeof window !== "undefined") {
|
||||
window.addEventListener("beforeunload", () => this.close())
|
||||
}
|
||||
this.logger = logger.named(this.id)
|
||||
}
|
||||
|
||||
public set binaryType(b: typeof WebSocket.prototype.binaryType) {
|
||||
this._binaryType = b
|
||||
if (this.socket) {
|
||||
this.socket.binaryType = b
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* Permanently close the connection. Will not attempt to reconnect. Will
|
||||
* remove event listeners.
|
||||
*/
|
||||
public close(code?: number): void {
|
||||
if (this.closed) {
|
||||
return
|
||||
}
|
||||
|
||||
if (code) {
|
||||
this.logger.info(`closing with code ${code}`)
|
||||
}
|
||||
|
||||
if (this.resetRetryTimeout) {
|
||||
clearTimeout(this.resetRetryTimeout)
|
||||
}
|
||||
|
||||
this.closed = true
|
||||
|
||||
if (this.socket) {
|
||||
this.socket.close()
|
||||
} else {
|
||||
this._onClose.emit(code)
|
||||
}
|
||||
}
|
||||
|
||||
public dispose(): void {
|
||||
this._onMessage.dispose()
|
||||
this._onDisconnect.dispose()
|
||||
this._onClose.dispose()
|
||||
this._onConnect.dispose()
|
||||
this.logger.debug("disposed handlers")
|
||||
}
|
||||
|
||||
/**
|
||||
* Send a message on the socket. Logs an error if currently disconnected.
|
||||
*/
|
||||
public send(message: string | ArrayBuffer): void {
|
||||
this.logger.trace(() => ["sending message", field("message", decode(message))])
|
||||
if (!this.socket) {
|
||||
return logger.error("tried to send message on closed socket")
|
||||
}
|
||||
this.socket.send(message)
|
||||
}
|
||||
|
||||
/**
|
||||
* Connect to the socket. Can also be called to wait until the connection is
|
||||
* established in the case of disconnections. Multiple calls will be handled
|
||||
* correctly.
|
||||
*/
|
||||
public async connect(): Promise<void> {
|
||||
if (!this.connecting) {
|
||||
this.connecting = new Promise((resolve, reject) => {
|
||||
const tryConnect = (): void => {
|
||||
if (this.closed) {
|
||||
return reject(new Error("disconnected")) // Don't keep trying if we've closed permanently.
|
||||
}
|
||||
if (typeof this.retryDelay === "undefined") {
|
||||
this.retryDelay = 0
|
||||
} else {
|
||||
this.retryDelay = this.retryDelay * this.retryDelayFactor || this.retryBaseDelay
|
||||
if (this.retryDelay > this.retryMaxDelay) {
|
||||
this.retryDelay = this.retryMaxDelay
|
||||
}
|
||||
}
|
||||
this._connect()
|
||||
.then((socket) => {
|
||||
this.logger.info("connected")
|
||||
this.socket = socket
|
||||
this.socket.binaryType = this._binaryType
|
||||
if (this.resetRetryTimeout) {
|
||||
clearTimeout(this.resetRetryTimeout)
|
||||
}
|
||||
this.resetRetryTimeout = setTimeout(() => (this.retryDelay = undefined), this.resetRetryDelay)
|
||||
this.connecting = undefined
|
||||
this._onConnect.emit()
|
||||
resolve()
|
||||
})
|
||||
.catch((error) => {
|
||||
this.logger.error(`failed to connect: ${error.message}`)
|
||||
tryConnect()
|
||||
})
|
||||
}
|
||||
tryConnect()
|
||||
})
|
||||
}
|
||||
return this.connecting
|
||||
}
|
||||
|
||||
private async _connect(): Promise<WebSocket> {
|
||||
const socket = await new Promise<WebSocket>((resolve, _reject) => {
|
||||
if (this.retryDelay) {
|
||||
this.logger.info(`retrying in ${this.retryDelay}ms...`)
|
||||
}
|
||||
setTimeout(() => {
|
||||
this.logger.info("connecting...", field("path", this.path))
|
||||
const socket = new WebSocket(this.path)
|
||||
|
||||
const reject = (): void => {
|
||||
_reject(new Error("socket closed"))
|
||||
}
|
||||
|
||||
const timeout = setTimeout(() => {
|
||||
// eslint-disable-next-line @typescript-eslint/no-use-before-define
|
||||
socket.removeEventListener("open", open)
|
||||
socket.removeEventListener("close", reject)
|
||||
_reject(new Error("timeout"))
|
||||
}, this.openTimeout)
|
||||
|
||||
const open = (): void => {
|
||||
clearTimeout(timeout)
|
||||
socket.removeEventListener("close", reject)
|
||||
resolve(socket)
|
||||
}
|
||||
|
||||
socket.addEventListener("open", open)
|
||||
socket.addEventListener("close", reject)
|
||||
}, this.retryDelay)
|
||||
})
|
||||
|
||||
socket.addEventListener("message", (event) => {
|
||||
this.logger.trace(() => ["got message", field("message", decode(event.data))])
|
||||
this._onMessage.emit(event.data)
|
||||
})
|
||||
socket.addEventListener("close", (event) => {
|
||||
this.socket = undefined
|
||||
if (!this.closed) {
|
||||
this._onDisconnect.emit(event.code)
|
||||
// It might be closed in the event handler.
|
||||
if (!this.closed) {
|
||||
this.logger.info("connection closed; attempting to reconnect")
|
||||
this.connect()
|
||||
}
|
||||
} else {
|
||||
this._onClose.emit(event.code)
|
||||
this.logger.info("connection closed permanently")
|
||||
}
|
||||
})
|
||||
|
||||
return socket
|
||||
}
|
||||
}
|
||||
@@ -1,60 +0,0 @@
|
||||
Implementation of [VS Code](https://code.visualstudio.com/) remote/web for use
|
||||
in `code-server`.
|
||||
|
||||
## Docker
|
||||
|
||||
To debug Golang in VS Code using the
|
||||
[ms-vscode-go extension](https://marketplace.visualstudio.com/items?itemName=ms-vscode.Go),
|
||||
you need to add `--security-opt seccomp=unconfined` to your `docker run`
|
||||
arguments when launching code-server with Docker. See
|
||||
[#725](https://github.com/cdr/code-server/issues/725) for details.
|
||||
|
||||
## Known Issues
|
||||
|
||||
- Creating custom VS Code extensions and debugging them doesn't work.
|
||||
- Extension profiling and tips are currently disabled.
|
||||
|
||||
## Extensions
|
||||
|
||||
`code-server` does not provide access to the official
|
||||
[Visual Studio Marketplace](https://marketplace.visualstudio.com/vscode). Instead,
|
||||
Coder has created a custom extension marketplace that we manage for open-source
|
||||
extensions. If you want to use an extension with code-server that we do not have
|
||||
in our marketplace please look for a release in the extension’s repository,
|
||||
contact us to see if we have one in the works or, if you build an extension
|
||||
locally from open source, you can copy it to the `extensions` folder. If you
|
||||
build one locally from open-source please contribute it to the project and let
|
||||
us know so we can give you props! If you have your own custom marketplace, it is
|
||||
possible to point code-server to it by setting the `SERVICE_URL` and `ITEM_URL`
|
||||
environment variables.
|
||||
|
||||
## Development: upgrading VS Code
|
||||
|
||||
We patch VS Code to provide and fix some functionality. 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.
|
||||
|
||||
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.
|
||||
|
||||
To generate a new patch, **stage all the changes** you want to be included in
|
||||
the patch in the VS Code source, then run `yarn patch:generate` in this
|
||||
directory.
|
||||
|
||||
Our changes include:
|
||||
|
||||
- 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.
|
||||
- Make changing the display language work.
|
||||
- Make it possible for us to load code on the client.
|
||||
- Make extensions work in the browser.
|
||||
- Fix getting permanently disconnected when you sleep or hibernate for a while.
|
||||
- Make it possible to automatically update the binary.
|
||||
- Add connection type to web socket query parameters.
|
||||
|
||||
## Future
|
||||
|
||||
- Run VS Code unit tests against our builds to ensure features work as expected.
|
||||
@@ -43,7 +43,7 @@ export class ApiHttpProvider extends HttpProvider {
|
||||
|
||||
public async handleRequest(route: Route, request: http.IncomingMessage): Promise<HttpResponse> {
|
||||
this.ensureAuthenticated(request)
|
||||
if (route.requestPath !== "/index.html") {
|
||||
if (!this.isRoot(route)) {
|
||||
throw new HttpError("Not found", HttpCode.NotFound)
|
||||
}
|
||||
|
||||
|
||||
@@ -20,7 +20,7 @@ export class DashboardHttpProvider extends HttpProvider {
|
||||
}
|
||||
|
||||
public async handleRequest(route: Route, request: http.IncomingMessage): Promise<HttpResponse> {
|
||||
if (route.requestPath !== "/index.html") {
|
||||
if (!this.isRoot(route)) {
|
||||
throw new HttpError("Not found", HttpCode.NotFound)
|
||||
}
|
||||
|
||||
|
||||
@@ -1,8 +1,9 @@
|
||||
import * as http from "http"
|
||||
import * as limiter from "limiter"
|
||||
import * as querystring from "querystring"
|
||||
import { HttpCode, HttpError } from "../../common/http"
|
||||
import { AuthType, HttpProvider, HttpResponse, Route } from "../http"
|
||||
import { hash } from "../util"
|
||||
import { AuthType, HttpProvider, HttpProviderOptions, HttpResponse, Route } from "../http"
|
||||
import { hash, humanPath } from "../util"
|
||||
|
||||
interface LoginPayload {
|
||||
password?: string
|
||||
@@ -17,8 +18,16 @@ interface LoginPayload {
|
||||
* Login HTTP provider.
|
||||
*/
|
||||
export class LoginHttpProvider extends HttpProvider {
|
||||
public constructor(
|
||||
options: HttpProviderOptions,
|
||||
private readonly configFile: string,
|
||||
private readonly envPassword: boolean,
|
||||
) {
|
||||
super(options)
|
||||
}
|
||||
|
||||
public async handleRequest(route: Route, request: http.IncomingMessage): Promise<HttpResponse> {
|
||||
if (this.options.auth !== AuthType.Password || route.requestPath !== "/index.html") {
|
||||
if (this.options.auth !== AuthType.Password || !this.isRoot(route)) {
|
||||
throw new HttpError("Not found", HttpCode.NotFound)
|
||||
}
|
||||
switch (route.base) {
|
||||
@@ -45,9 +54,16 @@ export class LoginHttpProvider extends HttpProvider {
|
||||
public async getRoot(route: Route, error?: Error): Promise<HttpResponse> {
|
||||
const response = await this.getUtf8Resource(this.rootPath, "src/browser/pages/login.html")
|
||||
response.content = response.content.replace(/{{ERROR}}/, error ? `<div class="error">${error.message}</div>` : "")
|
||||
let passwordMsg = `Check the config file at ${humanPath(this.configFile)} for the password.`
|
||||
if (this.envPassword) {
|
||||
passwordMsg = "Password was set from $PASSWORD."
|
||||
}
|
||||
response.content = response.content.replace(/{{PASSWORD_MSG}}/g, passwordMsg)
|
||||
return this.replaceTemplates(route, response)
|
||||
}
|
||||
|
||||
private readonly limiter = new RateLimiter()
|
||||
|
||||
/**
|
||||
* Try logging in. On failure, show the login page with an error.
|
||||
*/
|
||||
@@ -59,6 +75,10 @@ export class LoginHttpProvider extends HttpProvider {
|
||||
}
|
||||
|
||||
try {
|
||||
if (!this.limiter.try()) {
|
||||
throw new Error("Login rate limited!")
|
||||
}
|
||||
|
||||
const data = await this.getData(request)
|
||||
const payload = data ? querystring.parse(data) : {}
|
||||
return await this.login(payload, route, request)
|
||||
@@ -108,3 +128,17 @@ export class LoginHttpProvider extends HttpProvider {
|
||||
throw new Error("Missing password")
|
||||
}
|
||||
}
|
||||
|
||||
// RateLimiter wraps around the limiter library for logins.
|
||||
// It allows 2 logins every minute and 12 logins every hour.
|
||||
class RateLimiter {
|
||||
private readonly minuteLimiter = new limiter.RateLimiter(2, "minute")
|
||||
private readonly hourLimiter = new limiter.RateLimiter(12, "hour")
|
||||
|
||||
public try(): boolean {
|
||||
if (this.minuteLimiter.tryRemoveTokens(1)) {
|
||||
return true
|
||||
}
|
||||
return this.hourLimiter.tryRemoveTokens(1)
|
||||
}
|
||||
}
|
||||
|
||||
43
src/node/app/proxy.ts
Normal file
43
src/node/app/proxy.ts
Normal file
@@ -0,0 +1,43 @@
|
||||
import * as http from "http"
|
||||
import { HttpCode, HttpError } from "../../common/http"
|
||||
import { HttpProvider, HttpResponse, Route, WsResponse } from "../http"
|
||||
|
||||
/**
|
||||
* Proxy HTTP provider.
|
||||
*/
|
||||
export class ProxyHttpProvider extends HttpProvider {
|
||||
public async handleRequest(route: Route, request: http.IncomingMessage): Promise<HttpResponse> {
|
||||
if (!this.authenticated(request)) {
|
||||
if (this.isRoot(route)) {
|
||||
return { redirect: "/login", query: { to: route.fullPath } }
|
||||
}
|
||||
throw new HttpError("Unauthorized", HttpCode.Unauthorized)
|
||||
}
|
||||
|
||||
// Ensure there is a trailing slash so relative paths work correctly.
|
||||
if (this.isRoot(route) && !route.fullPath.endsWith("/")) {
|
||||
return {
|
||||
redirect: `${route.fullPath}/`,
|
||||
}
|
||||
}
|
||||
|
||||
const port = route.base.replace(/^\//, "")
|
||||
return {
|
||||
proxy: {
|
||||
base: `${this.options.base}/${port}`,
|
||||
port,
|
||||
},
|
||||
}
|
||||
}
|
||||
|
||||
public async handleWebSocket(route: Route, request: http.IncomingMessage): Promise<WsResponse> {
|
||||
this.ensureAuthenticated(request)
|
||||
const port = route.base.replace(/^\//, "")
|
||||
return {
|
||||
proxy: {
|
||||
base: `${this.options.base}/${port}`,
|
||||
port,
|
||||
},
|
||||
}
|
||||
}
|
||||
}
|
||||
@@ -5,6 +5,7 @@ import { Readable } from "stream"
|
||||
import * as tarFs from "tar-fs"
|
||||
import * as zlib from "zlib"
|
||||
import { HttpProvider, HttpResponse, Route } from "../http"
|
||||
import { pathToFsPath } from "../util"
|
||||
|
||||
/**
|
||||
* Static file HTTP provider. Regular static requests (the path is the request
|
||||
@@ -18,7 +19,7 @@ export class StaticHttpProvider extends HttpProvider {
|
||||
|
||||
if (typeof route.query.tar === "string") {
|
||||
this.ensureAuthenticated(request)
|
||||
return this.getTarredResource(request, route.query.tar)
|
||||
return this.getTarredResource(request, pathToFsPath(route.query.tar))
|
||||
}
|
||||
|
||||
const response = await this.getReplacedResource(route)
|
||||
|
||||
@@ -61,7 +61,7 @@ export class UpdateHttpProvider extends HttpProvider {
|
||||
this.ensureAuthenticated(request)
|
||||
this.ensureMethod(request)
|
||||
|
||||
if (route.requestPath !== "/index.html") {
|
||||
if (!this.isRoot(route)) {
|
||||
throw new HttpError("Not found", HttpCode.NotFound)
|
||||
}
|
||||
|
||||
@@ -87,8 +87,7 @@ export class UpdateHttpProvider extends HttpProvider {
|
||||
public async getRoot(
|
||||
route: Route,
|
||||
request: http.IncomingMessage,
|
||||
appliedUpdate?: string,
|
||||
error?: Error,
|
||||
errorOrUpdate?: Update | Error,
|
||||
): Promise<HttpResponse> {
|
||||
if (request.headers["content-type"] === "application/json") {
|
||||
if (!this.enabled) {
|
||||
@@ -108,8 +107,13 @@ export class UpdateHttpProvider extends HttpProvider {
|
||||
}
|
||||
const response = await this.getUtf8Resource(this.rootPath, "src/browser/pages/update.html")
|
||||
response.content = response.content
|
||||
.replace(/{{UPDATE_STATUS}}/, appliedUpdate ? `Updated to ${appliedUpdate}` : await this.getUpdateHtml())
|
||||
.replace(/{{ERROR}}/, error ? `<div class="error">${error.message}</div>` : "")
|
||||
.replace(
|
||||
/{{UPDATE_STATUS}}/,
|
||||
errorOrUpdate && !(errorOrUpdate instanceof Error)
|
||||
? `Updated to ${errorOrUpdate.version}`
|
||||
: await this.getUpdateHtml(),
|
||||
)
|
||||
.replace(/{{ERROR}}/, errorOrUpdate instanceof Error ? `<div class="error">${errorOrUpdate.message}</div>` : "")
|
||||
return this.replaceTemplates(route, response)
|
||||
}
|
||||
|
||||
@@ -186,11 +190,16 @@ export class UpdateHttpProvider extends HttpProvider {
|
||||
const update = await this.getUpdate()
|
||||
if (!this.isLatestVersion(update)) {
|
||||
await this.downloadAndApplyUpdate(update)
|
||||
return this.getRoot(route, request, update.version)
|
||||
return this.getRoot(route, request, update)
|
||||
}
|
||||
return this.getRoot(route, request)
|
||||
} catch (error) {
|
||||
return this.getRoot(route, request, undefined, error)
|
||||
// For JSON requests propagate the error. Otherwise catch it so we can
|
||||
// show the error inline with the update button instead of an error page.
|
||||
if (request.headers["content-type"] === "application/json") {
|
||||
throw error
|
||||
}
|
||||
return this.getRoot(route, error)
|
||||
}
|
||||
}
|
||||
|
||||
@@ -221,8 +230,13 @@ export class UpdateHttpProvider extends HttpProvider {
|
||||
targetPath = path.resolve(__dirname, "../../../")
|
||||
}
|
||||
|
||||
logger.debug("Replacing files", field("target", targetPath))
|
||||
await fs.move(directoryPath, targetPath, { overwrite: true })
|
||||
// Move the old directory to prevent potential data loss.
|
||||
const backupPath = path.resolve(targetPath, `../${path.basename(targetPath)}.${Date.now().toString()}`)
|
||||
logger.debug("Replacing files", field("target", targetPath), field("backup", backupPath))
|
||||
await fs.move(targetPath, backupPath)
|
||||
|
||||
// Move the new directory.
|
||||
await fs.move(directoryPath, targetPath)
|
||||
|
||||
await fs.remove(downloadPath)
|
||||
|
||||
@@ -357,7 +371,7 @@ export class UpdateHttpProvider extends HttpProvider {
|
||||
}
|
||||
|
||||
if (!response.statusCode || response.statusCode < 200 || response.statusCode >= 400) {
|
||||
return reject(new Error(`${response.statusCode || "500"}`))
|
||||
return reject(new Error(`${uri}: ${response.statusCode || "500"}`))
|
||||
}
|
||||
|
||||
resolve(response)
|
||||
|
||||
@@ -5,7 +5,6 @@ import * as fs from "fs-extra"
|
||||
import * as http from "http"
|
||||
import * as net from "net"
|
||||
import * as path from "path"
|
||||
import * as url from "url"
|
||||
import {
|
||||
CodeServerMessage,
|
||||
Options,
|
||||
@@ -19,6 +18,7 @@ import { generateUuid } from "../../common/util"
|
||||
import { Args } from "../cli"
|
||||
import { HttpProvider, HttpProviderOptions, HttpResponse, Route } from "../http"
|
||||
import { settings } from "../settings"
|
||||
import { pathToFsPath } from "../util"
|
||||
|
||||
export class VscodeHttpProvider extends HttpProvider {
|
||||
private readonly serverRootPath: string
|
||||
@@ -128,7 +128,7 @@ export class VscodeHttpProvider extends HttpProvider {
|
||||
|
||||
switch (route.base) {
|
||||
case "/":
|
||||
if (route.requestPath !== "/index.html") {
|
||||
if (!this.isRoot(route)) {
|
||||
throw new HttpError("Not found", HttpCode.NotFound)
|
||||
} else if (!this.authenticated(request)) {
|
||||
return { redirect: "/login", query: { to: this.options.base } }
|
||||
@@ -152,7 +152,7 @@ export class VscodeHttpProvider extends HttpProvider {
|
||||
case "/resource":
|
||||
case "/vscode-remote-resource":
|
||||
if (typeof route.query.path === "string") {
|
||||
return this.getResource(route.query.path)
|
||||
return this.getResource(pathToFsPath(route.query.path))
|
||||
}
|
||||
break
|
||||
case "/webview":
|
||||
@@ -168,15 +168,12 @@ export class VscodeHttpProvider extends HttpProvider {
|
||||
private async getRoot(request: http.IncomingMessage, route: Route): Promise<HttpResponse> {
|
||||
const remoteAuthority = request.headers.host as string
|
||||
const { lastVisited } = await settings.read()
|
||||
const startPath = await this.getFirstValidPath(
|
||||
[
|
||||
{ url: route.query.workspace, workspace: true },
|
||||
{ url: route.query.folder, workspace: false },
|
||||
this.args._ && this.args._.length > 0 ? { url: path.resolve(this.args._[this.args._.length - 1]) } : undefined,
|
||||
lastVisited,
|
||||
],
|
||||
remoteAuthority,
|
||||
)
|
||||
const startPath = await this.getFirstPath([
|
||||
{ url: route.query.workspace, workspace: true },
|
||||
{ url: route.query.folder, workspace: false },
|
||||
this.args._ && this.args._.length > 0 ? { url: path.resolve(this.args._[this.args._.length - 1]) } : undefined,
|
||||
lastVisited,
|
||||
])
|
||||
const [response, options] = await Promise.all([
|
||||
await this.getUtf8Resource(this.rootPath, "src/browser/pages/vscode.html"),
|
||||
this.initialize({
|
||||
@@ -196,6 +193,8 @@ export class VscodeHttpProvider extends HttpProvider {
|
||||
response.content = response.content.replace(/<!-- PROD_ONLY/g, "").replace(/END_PROD_ONLY -->/g, "")
|
||||
}
|
||||
|
||||
options.productConfiguration.codeServerVersion = require("../../../package.json").version
|
||||
|
||||
response.content = response.content
|
||||
.replace(`"{{REMOTE_USER_DATA_URI}}"`, `'${JSON.stringify(options.remoteUserDataUri)}'`)
|
||||
.replace(`"{{PRODUCT_CONFIGURATION}}"`, `'${JSON.stringify(options.productConfiguration)}'`)
|
||||
@@ -209,41 +208,31 @@ export class VscodeHttpProvider extends HttpProvider {
|
||||
}
|
||||
|
||||
/**
|
||||
* Choose the first valid path. If `workspace` is undefined then either a
|
||||
* workspace or a directory are acceptable. Otherwise it must be a file if a
|
||||
* workspace or a directory otherwise.
|
||||
* Choose the first non-empty path.
|
||||
*/
|
||||
private async getFirstValidPath(
|
||||
private async getFirstPath(
|
||||
startPaths: Array<{ url?: string | string[]; workspace?: boolean } | undefined>,
|
||||
remoteAuthority: string,
|
||||
): Promise<StartPath | undefined> {
|
||||
const isFile = async (path: string): Promise<boolean> => {
|
||||
try {
|
||||
const stat = await fs.stat(path)
|
||||
return stat.isFile()
|
||||
} catch (error) {
|
||||
logger.warn(error.message)
|
||||
return false
|
||||
}
|
||||
}
|
||||
for (let i = 0; i < startPaths.length; ++i) {
|
||||
const startPath = startPaths[i]
|
||||
if (!startPath) {
|
||||
continue
|
||||
}
|
||||
const paths = typeof startPath.url === "string" ? [startPath.url] : startPath.url || []
|
||||
for (let j = 0; j < paths.length; ++j) {
|
||||
const uri = url.parse(paths[j])
|
||||
try {
|
||||
if (!uri.pathname) {
|
||||
throw new Error(`${paths[j]} is not a valid URL`)
|
||||
}
|
||||
const stat = await fs.stat(uri.pathname)
|
||||
if (typeof startPath.workspace === "undefined" || startPath.workspace !== stat.isDirectory()) {
|
||||
return {
|
||||
url: url.format({
|
||||
protocol: uri.protocol || "vscode-remote",
|
||||
hostname: remoteAuthority.split(":")[0],
|
||||
port: remoteAuthority.split(":")[1],
|
||||
pathname: uri.pathname,
|
||||
slashes: true,
|
||||
}),
|
||||
workspace: !stat.isDirectory(),
|
||||
}
|
||||
}
|
||||
} catch (error) {
|
||||
logger.warn(error.message)
|
||||
const url =
|
||||
startPath && (typeof startPath.url === "string" ? [startPath.url] : startPath.url || []).find((p) => !!p)
|
||||
if (startPath && url) {
|
||||
return {
|
||||
url,
|
||||
// The only time `workspace` is undefined is for the command-line
|
||||
// argument, in which case it's a path (not a URL) so we can stat it
|
||||
// without having to parse it.
|
||||
workspace: typeof startPath.workspace !== "undefined" ? startPath.workspace : await isFile(url),
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
204
src/node/cli.ts
204
src/node/cli.ts
@@ -1,8 +1,11 @@
|
||||
import { field, Level, logger } from "@coder/logger"
|
||||
import * as fs from "fs-extra"
|
||||
import yaml from "js-yaml"
|
||||
import * as os from "os"
|
||||
import * as path from "path"
|
||||
import { field, logger, Level } from "@coder/logger"
|
||||
import { Args as VsArgs } from "../../lib/vscode/src/vs/server/ipc"
|
||||
import { AuthType } from "./http"
|
||||
import { xdgLocalDir } from "./util"
|
||||
import { generatePassword, humanPath, paths } from "./util"
|
||||
|
||||
export class Optional<T> {
|
||||
public constructor(public readonly value?: T) {}
|
||||
@@ -19,10 +22,11 @@ export enum LogLevel {
|
||||
export class OptionalString extends Optional<string> {}
|
||||
|
||||
export interface Args extends VsArgs {
|
||||
readonly config?: string
|
||||
readonly auth?: AuthType
|
||||
readonly password?: string
|
||||
readonly cert?: OptionalString
|
||||
readonly "cert-key"?: string
|
||||
readonly "disable-updates"?: boolean
|
||||
readonly "disable-telemetry"?: boolean
|
||||
readonly help?: boolean
|
||||
readonly host?: string
|
||||
@@ -30,15 +34,15 @@ export interface Args extends VsArgs {
|
||||
log?: LogLevel
|
||||
readonly open?: boolean
|
||||
readonly port?: number
|
||||
readonly "bind-addr"?: string
|
||||
readonly socket?: string
|
||||
readonly "ssh-host-key"?: string
|
||||
readonly "disable-ssh"?: boolean
|
||||
readonly version?: boolean
|
||||
readonly force?: boolean
|
||||
readonly "list-extensions"?: boolean
|
||||
readonly "install-extension"?: string[]
|
||||
readonly "show-versions"?: boolean
|
||||
readonly "uninstall-extension"?: string[]
|
||||
readonly "proxy-domain"?: string[]
|
||||
readonly locale?: string
|
||||
readonly _: string[]
|
||||
}
|
||||
@@ -81,26 +85,39 @@ type Options<T> = {
|
||||
|
||||
const options: Options<Required<Args>> = {
|
||||
auth: { type: AuthType, description: "The type of authentication to use." },
|
||||
password: {
|
||||
type: "string",
|
||||
description: "The password for password authentication (can only be passed in via $PASSWORD or the config file).",
|
||||
},
|
||||
cert: {
|
||||
type: OptionalString,
|
||||
path: true,
|
||||
description: "Path to certificate. Generated if no path is provided.",
|
||||
},
|
||||
"cert-key": { type: "string", path: true, description: "Path to certificate key when using non-generated cert." },
|
||||
"disable-updates": { type: "boolean", description: "Disable automatic updates." },
|
||||
"disable-telemetry": { type: "boolean", description: "Disable telemetry." },
|
||||
host: { type: "string", description: "Host for the HTTP server." },
|
||||
help: { type: "boolean", short: "h", description: "Show this output." },
|
||||
json: { type: "boolean" },
|
||||
open: { type: "boolean", description: "Open in browser on startup. Does not work remotely." },
|
||||
port: { type: "number", description: "Port for the HTTP server." },
|
||||
socket: { type: "string", path: true, description: "Path to a socket (host and port will be ignored)." },
|
||||
|
||||
"bind-addr": {
|
||||
type: "string",
|
||||
description: "Address to bind to in host:port. You can also use $PORT to override the port.",
|
||||
},
|
||||
|
||||
config: {
|
||||
type: "string",
|
||||
description: "Path to yaml config file. Every flag maps directly to a key in the config file.",
|
||||
},
|
||||
|
||||
// These two have been deprecated by bindAddr.
|
||||
host: { type: "string", description: "" },
|
||||
port: { type: "number", description: "" },
|
||||
|
||||
socket: { type: "string", path: true, description: "Path to a socket (bind-addr will be ignored)." },
|
||||
version: { type: "boolean", short: "v", description: "Display version information." },
|
||||
_: { type: "string[]" },
|
||||
|
||||
"disable-ssh": { type: "boolean" },
|
||||
"ssh-host-key": { type: "string", path: true },
|
||||
|
||||
"user-data-dir": { type: "string", path: true, description: "Path to the user data directory." },
|
||||
"extensions-dir": { type: "string", path: true, description: "Path to the extensions directory." },
|
||||
"builtin-extensions-dir": { type: "string", path: true },
|
||||
@@ -111,6 +128,7 @@ const options: Options<Required<Args>> = {
|
||||
"install-extension": { type: "string[]", description: "Install or update a VS Code extension by id or vsix." },
|
||||
"uninstall-extension": { type: "string[]", description: "Uninstall a VS Code extension by id." },
|
||||
"show-versions": { type: "boolean", description: "Show VS Code extension versions." },
|
||||
"proxy-domain": { type: "string[]", description: "Domain used for proxying ports." },
|
||||
|
||||
locale: { type: "string" },
|
||||
log: { type: LogLevel },
|
||||
@@ -134,7 +152,19 @@ export const optionDescriptions = (): string[] => {
|
||||
)
|
||||
}
|
||||
|
||||
export const parse = (argv: string[]): Args => {
|
||||
export const parse = async (
|
||||
argv: string[],
|
||||
opts?: {
|
||||
configFile: string
|
||||
},
|
||||
): Promise<Args> => {
|
||||
const error = (msg: string): Error => {
|
||||
if (opts?.configFile) {
|
||||
msg = `error reading ${opts.configFile}: ${msg}`
|
||||
}
|
||||
return new Error(msg)
|
||||
}
|
||||
|
||||
const args: Args = { _: [] }
|
||||
let ended = false
|
||||
|
||||
@@ -164,7 +194,11 @@ export const parse = (argv: string[]): Args => {
|
||||
}
|
||||
|
||||
if (!key || !options[key]) {
|
||||
throw new Error(`Unknown option ${arg}`)
|
||||
throw error(`Unknown option ${arg}`)
|
||||
}
|
||||
|
||||
if (key === "password" && !opts?.configFile) {
|
||||
throw new Error("--password can only be set in the config file or passed in via $PASSWORD")
|
||||
}
|
||||
|
||||
const option = options[key]
|
||||
@@ -183,7 +217,11 @@ export const parse = (argv: string[]): Args => {
|
||||
;(args[key] as OptionalString) = new OptionalString(value)
|
||||
continue
|
||||
} else if (!value) {
|
||||
throw new Error(`--${key} requires a value`)
|
||||
throw error(`--${key} requires a value`)
|
||||
}
|
||||
|
||||
if (option.type == OptionalString && value == "false") {
|
||||
continue
|
||||
}
|
||||
|
||||
if (option.path) {
|
||||
@@ -203,15 +241,15 @@ export const parse = (argv: string[]): Args => {
|
||||
case "number":
|
||||
;(args[key] as number) = parseInt(value, 10)
|
||||
if (isNaN(args[key] as number)) {
|
||||
throw new Error(`--${key} must be a number`)
|
||||
throw error(`--${key} must be a number`)
|
||||
}
|
||||
break
|
||||
case OptionalString:
|
||||
;(args[key] as OptionalString) = new OptionalString(value)
|
||||
break
|
||||
default: {
|
||||
if (!Object.values(option.type).find((v) => v === value)) {
|
||||
throw new Error(`--${key} valid values: [${Object.values(option.type).join(", ")}]`)
|
||||
if (!Object.values(option.type).includes(value)) {
|
||||
throw error(`--${key} valid values: [${Object.values(option.type).join(", ")}]`)
|
||||
}
|
||||
;(args[key] as string) = value
|
||||
break
|
||||
@@ -227,20 +265,26 @@ export const parse = (argv: string[]): Args => {
|
||||
|
||||
logger.debug("parsed command line", field("args", args))
|
||||
|
||||
// Ensure the environment variable and the flag are synced up. The flag takes
|
||||
// priority over the environment variable.
|
||||
if (args.log === LogLevel.Trace || process.env.LOG_LEVEL === LogLevel.Trace || args.verbose) {
|
||||
args.log = process.env.LOG_LEVEL = LogLevel.Trace
|
||||
args.verbose = true
|
||||
} else if (!args.log && process.env.LOG_LEVEL) {
|
||||
// --verbose takes priority over --log and --log takes priority over the
|
||||
// environment variable.
|
||||
if (args.verbose) {
|
||||
args.log = LogLevel.Trace
|
||||
} else if (
|
||||
!args.log &&
|
||||
process.env.LOG_LEVEL &&
|
||||
Object.values(LogLevel).includes(process.env.LOG_LEVEL as LogLevel)
|
||||
) {
|
||||
args.log = process.env.LOG_LEVEL as LogLevel
|
||||
} else if (args.log) {
|
||||
process.env.LOG_LEVEL = args.log
|
||||
}
|
||||
|
||||
// Sync --log, --verbose, the environment variable, and logger level.
|
||||
if (args.log) {
|
||||
process.env.LOG_LEVEL = args.log
|
||||
}
|
||||
switch (args.log) {
|
||||
case LogLevel.Trace:
|
||||
logger.level = Level.Trace
|
||||
args.verbose = true
|
||||
break
|
||||
case LogLevel.Debug:
|
||||
logger.level = Level.Debug
|
||||
@@ -257,7 +301,8 @@ export const parse = (argv: string[]): Args => {
|
||||
}
|
||||
|
||||
if (!args["user-data-dir"]) {
|
||||
args["user-data-dir"] = xdgLocalDir
|
||||
await copyOldMacOSDataDir()
|
||||
args["user-data-dir"] = paths.data
|
||||
}
|
||||
|
||||
if (!args["extensions-dir"]) {
|
||||
@@ -266,3 +311,108 @@ export const parse = (argv: string[]): Args => {
|
||||
|
||||
return args
|
||||
}
|
||||
|
||||
async function defaultConfigFile(): Promise<string> {
|
||||
return `bind-addr: 127.0.0.1:8080
|
||||
auth: password
|
||||
password: ${await generatePassword()}
|
||||
cert: false
|
||||
`
|
||||
}
|
||||
|
||||
/**
|
||||
* Reads the code-server yaml config file and returns it as Args.
|
||||
*
|
||||
* @param configPath Read the config from configPath instead of $CODE_SERVER_CONFIG or the default.
|
||||
*/
|
||||
export async function readConfigFile(configPath?: string): Promise<Args> {
|
||||
if (!configPath) {
|
||||
configPath = process.env.CODE_SERVER_CONFIG
|
||||
if (!configPath) {
|
||||
configPath = path.join(paths.config, "config.yaml")
|
||||
}
|
||||
}
|
||||
|
||||
if (!(await fs.pathExists(configPath))) {
|
||||
await fs.outputFile(configPath, await defaultConfigFile())
|
||||
logger.info(`Wrote default config file to ${humanPath(configPath)}`)
|
||||
}
|
||||
|
||||
logger.info(`Using config file ${humanPath(configPath)}`)
|
||||
|
||||
const configFile = await fs.readFile(configPath)
|
||||
const config = yaml.safeLoad(configFile.toString(), {
|
||||
filename: configPath,
|
||||
})
|
||||
|
||||
// We convert the config file into a set of flags.
|
||||
// This is a temporary measure until we add a proper CLI library.
|
||||
const configFileArgv = Object.entries(config).map(([optName, opt]) => {
|
||||
if (opt === true) {
|
||||
return `--${optName}`
|
||||
}
|
||||
return `--${optName}=${opt}`
|
||||
})
|
||||
const args = await parse(configFileArgv, {
|
||||
configFile: configPath,
|
||||
})
|
||||
return {
|
||||
...args,
|
||||
config: configPath,
|
||||
}
|
||||
}
|
||||
|
||||
function parseBindAddr(bindAddr: string): [string, number] {
|
||||
const u = new URL(`http://${bindAddr}`)
|
||||
return [u.hostname, parseInt(u.port, 10)]
|
||||
}
|
||||
|
||||
interface Addr {
|
||||
host: string
|
||||
port: number
|
||||
}
|
||||
|
||||
function bindAddrFromArgs(addr: Addr, args: Args): Addr {
|
||||
addr = { ...addr }
|
||||
if (args["bind-addr"]) {
|
||||
;[addr.host, addr.port] = parseBindAddr(args["bind-addr"])
|
||||
}
|
||||
if (args.host) {
|
||||
addr.host = args.host
|
||||
}
|
||||
|
||||
if (process.env.PORT) {
|
||||
addr.port = parseInt(process.env.PORT, 10)
|
||||
}
|
||||
if (args.port !== undefined) {
|
||||
addr.port = args.port
|
||||
}
|
||||
return addr
|
||||
}
|
||||
|
||||
export function bindAddrFromAllSources(cliArgs: Args, configArgs: Args): [string, number] {
|
||||
let addr: Addr = {
|
||||
host: "localhost",
|
||||
port: 8080,
|
||||
}
|
||||
|
||||
addr = bindAddrFromArgs(addr, configArgs)
|
||||
addr = bindAddrFromArgs(addr, cliArgs)
|
||||
|
||||
return [addr.host, addr.port]
|
||||
}
|
||||
|
||||
async function copyOldMacOSDataDir(): Promise<void> {
|
||||
if (os.platform() !== "darwin") {
|
||||
return
|
||||
}
|
||||
if (await fs.pathExists(paths.data)) {
|
||||
return
|
||||
}
|
||||
|
||||
// If the old data directory exists, we copy it in.
|
||||
const oldDataDir = path.join(os.homedir(), "Library/Application Support", "code-server")
|
||||
if (await fs.pathExists(oldDataDir)) {
|
||||
await fs.copy(oldDataDir, paths.data)
|
||||
}
|
||||
}
|
||||
|
||||
@@ -5,99 +5,107 @@ import { CliMessage } from "../../lib/vscode/src/vs/server/ipc"
|
||||
import { ApiHttpProvider } from "./app/api"
|
||||
import { DashboardHttpProvider } from "./app/dashboard"
|
||||
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, optionDescriptions, parse } from "./cli"
|
||||
import { AuthType, HttpServer } from "./http"
|
||||
import { SshProvider } from "./ssh/server"
|
||||
import { generateCertificate, generatePassword, generateSshHostKey, hash, open } from "./util"
|
||||
import { Args, bindAddrFromAllSources, optionDescriptions, parse, readConfigFile } from "./cli"
|
||||
import { AuthType, HttpServer, HttpServerOptions } from "./http"
|
||||
import { generateCertificate, hash, open, humanPath } from "./util"
|
||||
import { ipcMain, wrap } from "./wrapper"
|
||||
|
||||
const main = async (args: Args): Promise<void> => {
|
||||
const auth = args.auth || AuthType.Password
|
||||
const originalPassword = auth === AuthType.Password && (process.env.PASSWORD || (await generatePassword()))
|
||||
|
||||
let commit: string | undefined
|
||||
try {
|
||||
commit = require("../../package.json").commit
|
||||
} catch (error) {
|
||||
logger.warn(error.message)
|
||||
process.on("uncaughtException", (error) => {
|
||||
logger.error(`Uncaught exception: ${error.message}`)
|
||||
if (typeof error.stack !== "undefined") {
|
||||
logger.error(error.stack)
|
||||
}
|
||||
})
|
||||
|
||||
let pkg: { version?: string; commit?: string } = {}
|
||||
try {
|
||||
pkg = require("../../package.json")
|
||||
} catch (error) {
|
||||
logger.warn(error.message)
|
||||
}
|
||||
|
||||
const version = pkg.version || "development"
|
||||
const commit = pkg.commit || "development"
|
||||
|
||||
const main = async (cliArgs: Args): Promise<void> => {
|
||||
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)
|
||||
|
||||
if (!args.auth) {
|
||||
args = {
|
||||
...args,
|
||||
auth: AuthType.Password,
|
||||
}
|
||||
}
|
||||
|
||||
logger.info(`Using user-data-dir ${humanPath(args["user-data-dir"])}`)
|
||||
|
||||
logger.trace(`Using extensions-dir ${humanPath(args["extensions-dir"])}`)
|
||||
|
||||
const envPassword = !!process.env.PASSWORD
|
||||
const password = args.auth === AuthType.Password && (process.env.PASSWORD || args.password)
|
||||
if (args.auth === AuthType.Password && !password) {
|
||||
throw new Error("Please pass in a password via the config file or $PASSWORD")
|
||||
}
|
||||
const [host, port] = bindAddrFromAllSources(cliArgs, configArgs)
|
||||
|
||||
// Spawn the main HTTP server.
|
||||
const options = {
|
||||
auth,
|
||||
cert: args.cert ? args.cert.value : undefined,
|
||||
certKey: args["cert-key"],
|
||||
sshHostKey: args["ssh-host-key"],
|
||||
commit: commit || "development",
|
||||
host: args.host || (args.auth === AuthType.Password && typeof args.cert !== "undefined" ? "0.0.0.0" : "localhost"),
|
||||
password: originalPassword ? hash(originalPassword) : undefined,
|
||||
port: typeof args.port !== "undefined" ? args.port : process.env.PORT ? parseInt(process.env.PORT, 10) : 8080,
|
||||
const options: HttpServerOptions = {
|
||||
auth: args.auth,
|
||||
commit,
|
||||
host: host,
|
||||
// The hash does not add any actual security but we do it for obfuscation purposes.
|
||||
password: password ? hash(password) : undefined,
|
||||
port: port,
|
||||
proxyDomains: args["proxy-domain"],
|
||||
socket: args.socket,
|
||||
...(args.cert && !args.cert.value
|
||||
? await generateCertificate()
|
||||
: {
|
||||
cert: args.cert && args.cert.value,
|
||||
certKey: args["cert-key"],
|
||||
}),
|
||||
}
|
||||
|
||||
if (!options.cert && args.cert) {
|
||||
const { cert, certKey } = await generateCertificate()
|
||||
options.cert = cert
|
||||
options.certKey = certKey
|
||||
} else if (args.cert && !args["cert-key"]) {
|
||||
if (options.cert && !options.certKey) {
|
||||
throw new Error("--cert-key is missing")
|
||||
}
|
||||
|
||||
if (!args["disable-ssh"]) {
|
||||
if (!options.sshHostKey && typeof options.sshHostKey !== "undefined") {
|
||||
throw new Error("--ssh-host-key cannot be blank")
|
||||
} else if (!options.sshHostKey) {
|
||||
try {
|
||||
options.sshHostKey = await generateSshHostKey()
|
||||
} catch (error) {
|
||||
logger.error("Unable to start SSH server", field("error", error.message))
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
const httpServer = new HttpServer(options)
|
||||
const vscode = httpServer.registerHttpProvider("/", VscodeHttpProvider, args)
|
||||
const api = httpServer.registerHttpProvider("/api", ApiHttpProvider, httpServer, vscode, args["user-data-dir"])
|
||||
const update = httpServer.registerHttpProvider("/update", UpdateHttpProvider, !args["disable-updates"])
|
||||
httpServer.registerHttpProvider("/login", LoginHttpProvider)
|
||||
const update = httpServer.registerHttpProvider("/update", UpdateHttpProvider, false)
|
||||
httpServer.registerHttpProvider("/proxy", ProxyHttpProvider)
|
||||
httpServer.registerHttpProvider("/login", LoginHttpProvider, args.config!, envPassword)
|
||||
httpServer.registerHttpProvider("/static", StaticHttpProvider)
|
||||
httpServer.registerHttpProvider("/dashboard", DashboardHttpProvider, api, update)
|
||||
|
||||
ipcMain().onDispose(() => httpServer.dispose())
|
||||
|
||||
logger.info(`code-server ${require("../../package.json").version}`)
|
||||
|
||||
let sshPort = ""
|
||||
if (!args["disable-ssh"] && options.sshHostKey) {
|
||||
const sshProvider = httpServer.registerHttpProvider("/ssh", SshProvider, options.sshHostKey as string)
|
||||
try {
|
||||
sshPort = await sshProvider.listen()
|
||||
} catch (error) {
|
||||
logger.warn(`SSH server: ${error.message}`)
|
||||
}
|
||||
}
|
||||
|
||||
logger.info(`code-server ${version} ${commit}`)
|
||||
const serverAddress = await httpServer.listen()
|
||||
logger.info(`Server listening on ${serverAddress}`)
|
||||
logger.info(`HTTP server listening on ${serverAddress}`)
|
||||
|
||||
if (auth === AuthType.Password && !process.env.PASSWORD) {
|
||||
logger.info(` - Password is ${originalPassword}`)
|
||||
logger.info(" - To use your own password, set the PASSWORD environment variable")
|
||||
if (!args.auth) {
|
||||
logger.info(" - To disable use `--auth none`")
|
||||
if (args.auth === AuthType.Password) {
|
||||
if (envPassword) {
|
||||
logger.info(" - Using password from $PASSWORD")
|
||||
} else {
|
||||
logger.info(` - Using password from ${humanPath(args.config)}`)
|
||||
}
|
||||
} else if (auth === AuthType.Password) {
|
||||
logger.info(" - Using custom password for authentication")
|
||||
logger.info(" - To disable use `--auth none`")
|
||||
} else {
|
||||
logger.info(" - No authentication")
|
||||
}
|
||||
delete process.env.PASSWORD
|
||||
|
||||
if (httpServer.protocol === "https") {
|
||||
logger.info(
|
||||
typeof args.cert === "string"
|
||||
args.cert && args.cert.value
|
||||
? ` - Using provided certificate and key for HTTPS`
|
||||
: ` - Using generated certificate and key for HTTPS`,
|
||||
)
|
||||
@@ -105,74 +113,75 @@ const main = async (args: Args): Promise<void> => {
|
||||
logger.info(" - Not serving HTTPS")
|
||||
}
|
||||
|
||||
logger.info(` - Automatic updates are ${update.enabled ? "enabled" : "disabled"}`)
|
||||
|
||||
if (sshPort) {
|
||||
logger.info(` - SSH Server - Listening :${sshPort}`)
|
||||
} else {
|
||||
logger.info(" - SSH Server - Disabled")
|
||||
if (httpServer.proxyDomains.size > 0) {
|
||||
logger.info(` - Proxying the following domain${httpServer.proxyDomains.size === 1 ? "" : "s"}:`)
|
||||
httpServer.proxyDomains.forEach((domain) => logger.info(` - *.${domain}`))
|
||||
}
|
||||
|
||||
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)
|
||||
logger.info(` - Opened ${openAddress}`)
|
||||
logger.info(`Opened ${openAddress}`)
|
||||
}
|
||||
}
|
||||
|
||||
const tryParse = (): Args => {
|
||||
try {
|
||||
return parse(process.argv.slice(2))
|
||||
} catch (error) {
|
||||
console.error(error.message)
|
||||
process.exit(1)
|
||||
}
|
||||
}
|
||||
|
||||
const args = tryParse()
|
||||
if (args.help) {
|
||||
console.log("code-server", require("../../package.json").version)
|
||||
console.log("")
|
||||
console.log(`Usage: code-server [options] [path]`)
|
||||
console.log("")
|
||||
console.log("Options")
|
||||
optionDescriptions().forEach((description) => {
|
||||
console.log("", description)
|
||||
})
|
||||
} else if (args.version) {
|
||||
const version = require("../../package.json").version
|
||||
if (args.json) {
|
||||
console.log({
|
||||
codeServer: version,
|
||||
vscode: require("../../lib/vscode/package.json").version,
|
||||
})
|
||||
} else {
|
||||
console.log(version)
|
||||
}
|
||||
process.exit(0)
|
||||
} 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) => {
|
||||
logger.debug("Got message from VS Code", field("message", message))
|
||||
if (message.type !== "ready") {
|
||||
logger.error("Unexpected response waiting for ready response")
|
||||
async function entry(): Promise<void> {
|
||||
const tryParse = async (): Promise<Args> => {
|
||||
try {
|
||||
return await parse(process.argv.slice(2))
|
||||
} catch (error) {
|
||||
console.error(error.message)
|
||||
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))
|
||||
}
|
||||
|
||||
const args = await tryParse()
|
||||
if (args.help) {
|
||||
console.log("code-server", version, commit)
|
||||
console.log("")
|
||||
console.log(`Usage: code-server [options] [path]`)
|
||||
console.log("")
|
||||
console.log("Options")
|
||||
optionDescriptions().forEach((description) => {
|
||||
console.log("", description)
|
||||
})
|
||||
} else if (args.version) {
|
||||
if (args.json) {
|
||||
console.log({
|
||||
codeServer: version,
|
||||
commit,
|
||||
vscode: require("../../lib/vscode/package.json").version,
|
||||
})
|
||||
} else {
|
||||
console.log(version, commit)
|
||||
}
|
||||
process.exit(0)
|
||||
} 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) => {
|
||||
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))
|
||||
}
|
||||
}
|
||||
|
||||
entry()
|
||||
|
||||
260
src/node/http.ts
260
src/node/http.ts
@@ -1,6 +1,7 @@
|
||||
import { field, logger } from "@coder/logger"
|
||||
import * as fs from "fs-extra"
|
||||
import * as http from "http"
|
||||
import proxy from "http-proxy"
|
||||
import * as httpolyglot from "httpolyglot"
|
||||
import * as https from "https"
|
||||
import * as net from "net"
|
||||
@@ -13,11 +14,15 @@ import * as url from "url"
|
||||
import { HttpCode, HttpError } from "../common/http"
|
||||
import { normalize, Options, plural, split } from "../common/util"
|
||||
import { SocketProxyProvider } from "./socket"
|
||||
import { getMediaMime, xdgLocalDir } from "./util"
|
||||
import { getMediaMime, paths } from "./util"
|
||||
|
||||
export type Cookies = { [key: string]: string[] | undefined }
|
||||
export type PostData = { [key: string]: string | string[] | undefined }
|
||||
|
||||
interface ProxyRequest extends http.IncomingMessage {
|
||||
base?: string
|
||||
}
|
||||
|
||||
interface AuthPayload extends Cookies {
|
||||
key?: string[]
|
||||
}
|
||||
@@ -29,6 +34,17 @@ export enum AuthType {
|
||||
|
||||
export type Query = { [key: string]: string | string[] | undefined }
|
||||
|
||||
export interface ProxyOptions {
|
||||
/**
|
||||
* A base path to strip from from the request before proxying if necessary.
|
||||
*/
|
||||
base?: string
|
||||
/**
|
||||
* The port to proxy.
|
||||
*/
|
||||
port: string
|
||||
}
|
||||
|
||||
export interface HttpResponse<T = string | Buffer | object> {
|
||||
/*
|
||||
* Whether to set cache-control headers for this response.
|
||||
@@ -77,6 +93,17 @@ export interface HttpResponse<T = string | Buffer | object> {
|
||||
* `undefined` to remove a query variable.
|
||||
*/
|
||||
query?: Query
|
||||
/**
|
||||
* Indicates the request should be proxied.
|
||||
*/
|
||||
proxy?: ProxyOptions
|
||||
}
|
||||
|
||||
export interface WsResponse {
|
||||
/**
|
||||
* Indicates the web socket should be proxied.
|
||||
*/
|
||||
proxy?: ProxyOptions
|
||||
}
|
||||
|
||||
/**
|
||||
@@ -100,14 +127,31 @@ export interface HttpServerOptions {
|
||||
readonly host?: string
|
||||
readonly password?: string
|
||||
readonly port?: number
|
||||
readonly proxyDomains?: string[]
|
||||
readonly socket?: string
|
||||
}
|
||||
|
||||
export interface Route {
|
||||
/**
|
||||
* Base path part (in /test/path it would be "/test").
|
||||
*/
|
||||
base: string
|
||||
/**
|
||||
* Remaining part of the route (in /test/path it would be "/path"). It can be
|
||||
* blank.
|
||||
*/
|
||||
requestPath: string
|
||||
/**
|
||||
* Query variables included in the request.
|
||||
*/
|
||||
query: querystring.ParsedUrlQuery
|
||||
/**
|
||||
* Normalized version of `originalPath`.
|
||||
*/
|
||||
fullPath: string
|
||||
/**
|
||||
* Original path of the request without any modifications.
|
||||
*/
|
||||
originalPath: string
|
||||
}
|
||||
|
||||
@@ -136,7 +180,9 @@ export abstract class HttpProvider {
|
||||
}
|
||||
|
||||
/**
|
||||
* Handle web sockets on the registered endpoint.
|
||||
* Handle web sockets on the registered endpoint. Normally the provider
|
||||
* handles the request itself but it can return a response when necessary. The
|
||||
* default is to throw a 404.
|
||||
*/
|
||||
public handleWebSocket(
|
||||
/* eslint-disable @typescript-eslint/no-unused-vars */
|
||||
@@ -145,7 +191,7 @@ export abstract class HttpProvider {
|
||||
_socket: net.Socket,
|
||||
_head: Buffer,
|
||||
/* eslint-enable @typescript-eslint/no-unused-vars */
|
||||
): Promise<void> {
|
||||
): Promise<WsResponse | void> {
|
||||
throw new HttpError("Not found", HttpCode.NotFound)
|
||||
}
|
||||
|
||||
@@ -264,7 +310,7 @@ export abstract class HttpProvider {
|
||||
* Return the provided password value if the payload contains the right
|
||||
* password otherwise return false. If no payload is specified use cookies.
|
||||
*/
|
||||
protected authenticated(request: http.IncomingMessage, payload?: AuthPayload): string | boolean {
|
||||
public authenticated(request: http.IncomingMessage, payload?: AuthPayload): string | boolean {
|
||||
switch (this.options.auth) {
|
||||
case AuthType.None:
|
||||
return true
|
||||
@@ -335,6 +381,14 @@ export abstract class HttpProvider {
|
||||
}
|
||||
return cookies as T
|
||||
}
|
||||
|
||||
/**
|
||||
* Return true if the route is for the root page. For example /base, /base/,
|
||||
* or /base/index.html but not /base/path or /base/file.js.
|
||||
*/
|
||||
protected isRoot(route: Route): boolean {
|
||||
return !route.requestPath || route.requestPath === "/index.html"
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
@@ -407,8 +461,19 @@ export class HttpServer {
|
||||
private readonly heart: Heart
|
||||
private readonly socketProvider = new SocketProxyProvider()
|
||||
|
||||
/**
|
||||
* Proxy domains are stored here without the leading `*.`
|
||||
*/
|
||||
public readonly proxyDomains: Set<string>
|
||||
|
||||
/**
|
||||
* Provides the actual proxying functionality.
|
||||
*/
|
||||
private readonly proxy = proxy.createProxyServer({})
|
||||
|
||||
public constructor(private readonly options: HttpServerOptions) {
|
||||
this.heart = new Heart(path.join(xdgLocalDir, "heartbeat"), async () => {
|
||||
this.proxyDomains = new Set((options.proxyDomains || []).map((d) => d.replace(/^\*\./, "")))
|
||||
this.heart = new Heart(path.join(paths.data, "heartbeat"), async () => {
|
||||
const connections = await this.getConnections()
|
||||
logger.trace(`${connections} active connection${plural(connections)}`)
|
||||
return connections !== 0
|
||||
@@ -425,6 +490,16 @@ export class HttpServer {
|
||||
} else {
|
||||
this.server = http.createServer(this.onRequest)
|
||||
}
|
||||
this.proxy.on("error", (error, _request, response) => {
|
||||
response.writeHead(HttpCode.ServerError)
|
||||
response.end(error.message)
|
||||
})
|
||||
// Intercept the response to rewrite absolute redirects against the base path.
|
||||
this.proxy.on("proxyRes", (response, request: ProxyRequest) => {
|
||||
if (response.headers.location && response.headers.location.startsWith("/") && request.base) {
|
||||
response.headers.location = request.base + response.headers.location
|
||||
}
|
||||
})
|
||||
}
|
||||
|
||||
public dispose(): void {
|
||||
@@ -525,9 +600,12 @@ export class HttpServer {
|
||||
"Set-Cookie": [
|
||||
`${payload.cookie.key}=${payload.cookie.value}`,
|
||||
`Path=${normalize(payload.cookie.path || "/", true)}`,
|
||||
this.getCookieDomain(request.headers.host || ""),
|
||||
// "HttpOnly",
|
||||
"SameSite=strict",
|
||||
].join(";"),
|
||||
"SameSite=lax",
|
||||
]
|
||||
.filter((l) => !!l)
|
||||
.join(";"),
|
||||
}
|
||||
: {}),
|
||||
...payload.headers,
|
||||
@@ -547,25 +625,40 @@ export class HttpServer {
|
||||
response.end()
|
||||
}
|
||||
}
|
||||
|
||||
try {
|
||||
const payload = this.maybeRedirect(request, route) || (await route.provider.handleRequest(route, request))
|
||||
if (!payload) {
|
||||
throw new HttpError("Not found", HttpCode.NotFound)
|
||||
const payload =
|
||||
this.maybeRedirect(request, route) ||
|
||||
(route.provider.authenticated(request) && this.maybeProxy(request)) ||
|
||||
(await route.provider.handleRequest(route, request))
|
||||
if (payload.proxy) {
|
||||
this.doProxy(route, request, response, payload.proxy)
|
||||
} else {
|
||||
write(payload)
|
||||
}
|
||||
write(payload)
|
||||
} catch (error) {
|
||||
let e = error
|
||||
if (error.code === "ENOENT" || error.code === "EISDIR") {
|
||||
e = new HttpError("Not found", HttpCode.NotFound)
|
||||
}
|
||||
logger.debug("Request error", field("url", request.url))
|
||||
logger.debug(error.stack)
|
||||
const code = typeof e.code === "number" ? e.code : HttpCode.ServerError
|
||||
const payload = await route.provider.getErrorRoot(route, code, code, e.message)
|
||||
write({
|
||||
code,
|
||||
...payload,
|
||||
})
|
||||
logger.debug("Request error", field("url", request.url), field("code", code))
|
||||
if (code >= HttpCode.ServerError) {
|
||||
logger.error(error.stack)
|
||||
}
|
||||
if (request.headers["content-type"] === "application/json") {
|
||||
write({
|
||||
code,
|
||||
content: {
|
||||
error: e.message,
|
||||
},
|
||||
})
|
||||
} else {
|
||||
write({
|
||||
code,
|
||||
...(await route.provider.getErrorRoot(route, code, code, e.message)),
|
||||
})
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
@@ -625,7 +718,14 @@ export class HttpServer {
|
||||
throw new HttpError("Not found", HttpCode.NotFound)
|
||||
}
|
||||
|
||||
await route.provider.handleWebSocket(route, request, await this.socketProvider.createProxy(socket), head)
|
||||
// The socket proxy is so we can pass them to child processes (TLS sockets
|
||||
// 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))
|
||||
if (payload && payload.proxy) {
|
||||
this.doProxy(route, request, { socket: socketProxy, head }, payload.proxy)
|
||||
}
|
||||
} catch (error) {
|
||||
socket.destroy(error)
|
||||
logger.warn(`discarding socket connection: ${error.message}`)
|
||||
@@ -647,7 +747,6 @@ export class HttpServer {
|
||||
// Happens if it's a plain `domain.com`.
|
||||
base = "/"
|
||||
}
|
||||
requestPath = requestPath || "/index.html"
|
||||
return { base, requestPath }
|
||||
}
|
||||
|
||||
@@ -670,4 +769,125 @@ export class HttpServer {
|
||||
}
|
||||
return { base, fullPath, requestPath, query: parsedUrl.query, provider, originalPath }
|
||||
}
|
||||
|
||||
/**
|
||||
* Proxy a request to the target.
|
||||
*/
|
||||
private doProxy(
|
||||
route: Route,
|
||||
request: http.IncomingMessage,
|
||||
response: http.ServerResponse,
|
||||
options: ProxyOptions,
|
||||
): void
|
||||
/**
|
||||
* Proxy a web socket to the target.
|
||||
*/
|
||||
private doProxy(
|
||||
route: Route,
|
||||
request: http.IncomingMessage,
|
||||
response: { socket: net.Socket; head: Buffer },
|
||||
options: ProxyOptions,
|
||||
): void
|
||||
/**
|
||||
* Proxy a request or web socket to the target.
|
||||
*/
|
||||
private doProxy(
|
||||
route: Route,
|
||||
request: http.IncomingMessage,
|
||||
response: http.ServerResponse | { socket: net.Socket; head: Buffer },
|
||||
options: ProxyOptions,
|
||||
): void {
|
||||
const port = parseInt(options.port, 10)
|
||||
if (isNaN(port)) {
|
||||
throw new HttpError(`"${options.port}" is not a valid number`, HttpCode.BadRequest)
|
||||
}
|
||||
|
||||
// REVIEW: Absolute redirects need to be based on the subpath but I'm not
|
||||
// sure how best to get this information to the `proxyRes` event handler.
|
||||
// For now I'm sticking it on the request object which is passed through to
|
||||
// the event.
|
||||
;(request as ProxyRequest).base = options.base
|
||||
|
||||
const isHttp = response instanceof http.ServerResponse
|
||||
const path = options.base ? route.fullPath.replace(options.base, "") : route.fullPath
|
||||
const proxyOptions: proxy.ServerOptions = {
|
||||
changeOrigin: true,
|
||||
ignorePath: true,
|
||||
target: `${isHttp ? "http" : "ws"}://127.0.0.1:${port}${path}${
|
||||
Object.keys(route.query).length > 0 ? `?${querystring.stringify(route.query)}` : ""
|
||||
}`,
|
||||
ws: !isHttp,
|
||||
}
|
||||
|
||||
if (response instanceof http.ServerResponse) {
|
||||
this.proxy.web(request, response, proxyOptions)
|
||||
} else {
|
||||
this.proxy.ws(request, response.socket, response.head, proxyOptions)
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* Get the value that should be used for setting a cookie domain. This will
|
||||
* allow the user to authenticate only once. This will use the highest level
|
||||
* domain (e.g. `coder.com` over `test.coder.com` if both are specified).
|
||||
*/
|
||||
private getCookieDomain(host: string): string | undefined {
|
||||
const idx = host.lastIndexOf(":")
|
||||
host = idx !== -1 ? host.substring(0, idx) : host
|
||||
if (
|
||||
// Might be blank/missing, so there's nothing more to do.
|
||||
!host ||
|
||||
// IP addresses can't have subdomains so there's no value in setting the
|
||||
// domain for them. Assume anything with a : is ipv6 (valid domain name
|
||||
// characters are alphanumeric or dashes).
|
||||
host.includes(":") ||
|
||||
// Assume anything entirely numbers and dots is ipv4 (currently tlds
|
||||
// cannot be entirely numbers).
|
||||
!/[^0-9.]/.test(host) ||
|
||||
// localhost subdomains don't seem to work at all (browser bug?).
|
||||
host.endsWith(".localhost") ||
|
||||
// It might be localhost (or an IP, see above) if it's a proxy and it
|
||||
// isn't setting the host header to match the access domain.
|
||||
host === "localhost"
|
||||
) {
|
||||
return undefined
|
||||
}
|
||||
|
||||
this.proxyDomains.forEach((domain) => {
|
||||
if (host.endsWith(domain) && domain.length < host.length) {
|
||||
host = domain
|
||||
}
|
||||
})
|
||||
|
||||
return host ? `Domain=${host}` : undefined
|
||||
}
|
||||
|
||||
/**
|
||||
* Return a response if the request should be proxied. Anything that ends in a
|
||||
* proxy domain and has a *single* subdomain should be proxied. Anything else
|
||||
* should return `undefined` and will be handled as normal.
|
||||
*
|
||||
* For example if `coder.com` is specified `8080.coder.com` will be proxied
|
||||
* but `8080.test.coder.com` and `test.8080.coder.com` will not.
|
||||
*/
|
||||
public maybeProxy(request: http.IncomingMessage): HttpResponse | undefined {
|
||||
// Split into parts.
|
||||
const host = request.headers.host || ""
|
||||
const idx = host.indexOf(":")
|
||||
const domain = idx !== -1 ? host.substring(0, idx) : host
|
||||
const parts = domain.split(".")
|
||||
|
||||
// There must be an exact match.
|
||||
const port = parts.shift()
|
||||
const proxyDomain = parts.join(".")
|
||||
if (!port || !this.proxyDomains.has(proxyDomain)) {
|
||||
return undefined
|
||||
}
|
||||
|
||||
return {
|
||||
proxy: {
|
||||
port,
|
||||
},
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
@@ -1,6 +1,6 @@
|
||||
import * as fs from "fs-extra"
|
||||
import * as path from "path"
|
||||
import { extend, xdgLocalDir } from "./util"
|
||||
import { extend, paths } from "./util"
|
||||
import { logger } from "@coder/logger"
|
||||
|
||||
export type Settings = { [key: string]: Settings | string | boolean | number }
|
||||
@@ -60,4 +60,4 @@ export interface CoderSettings extends UpdateSettings {
|
||||
/**
|
||||
* Global code-server settings file.
|
||||
*/
|
||||
export const settings = new SettingsProvider<CoderSettings>(path.join(xdgLocalDir, "coder.json"))
|
||||
export const settings = new SettingsProvider<CoderSettings>(path.join(paths.data, "coder.json"))
|
||||
|
||||
@@ -1,110 +0,0 @@
|
||||
import * as http from "http"
|
||||
import * as net from "net"
|
||||
import * as ssh from "ssh2"
|
||||
import * as ws from "ws"
|
||||
import * as fs from "fs"
|
||||
import { logger } from "@coder/logger"
|
||||
import safeCompare from "safe-compare"
|
||||
import { HttpProvider, HttpResponse, HttpProviderOptions, Route } from "../http"
|
||||
import { HttpCode } from "../../common/http"
|
||||
import { forwardSshPort, fillSshSession } from "./ssh"
|
||||
import { hash } from "../util"
|
||||
|
||||
export class SshProvider extends HttpProvider {
|
||||
private readonly wss = new ws.Server({ noServer: true })
|
||||
private sshServer: ssh.Server
|
||||
|
||||
public constructor(options: HttpProviderOptions, hostKeyPath: string) {
|
||||
super(options)
|
||||
const hostKey = fs.readFileSync(hostKeyPath)
|
||||
this.sshServer = new ssh.Server({ hostKeys: [hostKey] }, this.handleSsh)
|
||||
|
||||
this.sshServer.on("error", (err) => {
|
||||
logger.trace(`SSH server error: ${err.stack}`)
|
||||
})
|
||||
}
|
||||
|
||||
public async listen(): Promise<string> {
|
||||
return new Promise((resolve, reject) => {
|
||||
this.sshServer.once("error", reject)
|
||||
this.sshServer.listen(() => {
|
||||
resolve(this.sshServer.address().port.toString())
|
||||
})
|
||||
})
|
||||
}
|
||||
|
||||
public async handleRequest(): Promise<HttpResponse> {
|
||||
// SSH has no HTTP endpoints
|
||||
return { code: HttpCode.NotFound }
|
||||
}
|
||||
|
||||
public handleWebSocket(
|
||||
_route: Route,
|
||||
request: http.IncomingMessage,
|
||||
socket: net.Socket,
|
||||
head: Buffer,
|
||||
): Promise<void> {
|
||||
// Create a fake websocket to the sshServer
|
||||
const sshSocket = net.connect(this.sshServer.address().port, "localhost")
|
||||
|
||||
return new Promise((resolve) => {
|
||||
this.wss.handleUpgrade(request, socket, head, (ws) => {
|
||||
// Send SSH data to WS as compressed binary
|
||||
sshSocket.on("data", (data) => {
|
||||
ws.send(data, {
|
||||
binary: true,
|
||||
compress: true,
|
||||
fin: true,
|
||||
})
|
||||
})
|
||||
|
||||
// Send WS data to SSH as buffer
|
||||
ws.on("message", (msg) => {
|
||||
// Buffer.from is cool with all types, but casting as string keeps typing simple
|
||||
sshSocket.write(Buffer.from(msg as string))
|
||||
})
|
||||
|
||||
ws.on("error", (err) => {
|
||||
logger.error(`SSH websocket error: ${err.stack}`)
|
||||
})
|
||||
|
||||
resolve()
|
||||
})
|
||||
})
|
||||
}
|
||||
|
||||
/**
|
||||
* Determine how to handle incoming SSH connections.
|
||||
*/
|
||||
private handleSsh = (client: ssh.Connection, info: ssh.ClientInfo): void => {
|
||||
logger.debug(`Incoming SSH connection from ${info.ip}`)
|
||||
client.on("authentication", (ctx) => {
|
||||
// Allow any auth to go through if we have no password
|
||||
if (!this.options.password) {
|
||||
return ctx.accept()
|
||||
}
|
||||
|
||||
// Otherwise require the same password as code-server
|
||||
if (ctx.method === "password") {
|
||||
if (
|
||||
safeCompare(this.options.password, hash(ctx.password)) ||
|
||||
safeCompare(this.options.password, ctx.password)
|
||||
) {
|
||||
return ctx.accept()
|
||||
}
|
||||
}
|
||||
|
||||
// Reject, letting them know that password is the only method we allow
|
||||
ctx.reject(["password"])
|
||||
})
|
||||
client.on("tcpip", forwardSshPort)
|
||||
client.on("session", fillSshSession)
|
||||
client.on("error", (err) => {
|
||||
// Don't bother logging Keepalive errors, they probably just disconnected
|
||||
if (err.message === "Keepalive timeout") {
|
||||
return logger.debug("SSH client keepalive timeout")
|
||||
}
|
||||
logger.error(`SSH client error: ${err.stack}`)
|
||||
})
|
||||
}
|
||||
}
|
||||
@@ -1,201 +0,0 @@
|
||||
/**
|
||||
* Provides utilities for handling SSH connections
|
||||
*/
|
||||
import * as fs from "fs"
|
||||
import * as path from "path"
|
||||
import * as ssh from "ssh2"
|
||||
import { FileEntry, SFTPStream } from "ssh2-streams"
|
||||
|
||||
/**
|
||||
* Fills out all the functionality of SFTP using fs.
|
||||
*/
|
||||
export function fillSftpStream(accept: () => SFTPStream): void {
|
||||
const sftp = accept()
|
||||
|
||||
let oid = 0
|
||||
const fds: { [key: number]: boolean } = {}
|
||||
const ods: {
|
||||
[key: number]: {
|
||||
path: string
|
||||
read: boolean
|
||||
}
|
||||
} = {}
|
||||
|
||||
const sftpStatus = (reqID: number, err?: NodeJS.ErrnoException | null): boolean => {
|
||||
let code = ssh.SFTP_STATUS_CODE.OK
|
||||
if (err) {
|
||||
if (err.code === "EACCES") {
|
||||
code = ssh.SFTP_STATUS_CODE.PERMISSION_DENIED
|
||||
} else if (err.code === "ENOENT") {
|
||||
code = ssh.SFTP_STATUS_CODE.NO_SUCH_FILE
|
||||
} else {
|
||||
code = ssh.SFTP_STATUS_CODE.FAILURE
|
||||
}
|
||||
}
|
||||
return sftp.status(reqID, code)
|
||||
}
|
||||
|
||||
sftp.on("OPEN", (reqID, filename) => {
|
||||
fs.open(filename, "w", (err, fd) => {
|
||||
if (err) {
|
||||
return sftpStatus(reqID, err)
|
||||
}
|
||||
fds[fd] = true
|
||||
const buf = Buffer.alloc(4)
|
||||
buf.writeUInt32BE(fd, 0)
|
||||
return sftp.handle(reqID, buf)
|
||||
})
|
||||
})
|
||||
|
||||
sftp.on("OPENDIR", (reqID, path) => {
|
||||
const buf = Buffer.alloc(4)
|
||||
const id = oid++
|
||||
buf.writeUInt32BE(id, 0)
|
||||
ods[id] = {
|
||||
path,
|
||||
read: false,
|
||||
}
|
||||
sftp.handle(reqID, buf)
|
||||
})
|
||||
|
||||
sftp.on("READDIR", (reqID, handle) => {
|
||||
const od = handle.readUInt32BE(0)
|
||||
if (!ods[od]) {
|
||||
return sftp.status(reqID, ssh.SFTP_STATUS_CODE.NO_SUCH_FILE)
|
||||
}
|
||||
if (ods[od].read) {
|
||||
sftp.status(reqID, ssh.SFTP_STATUS_CODE.EOF)
|
||||
return
|
||||
}
|
||||
return fs.readdir(ods[od].path, (err, files) => {
|
||||
if (err) {
|
||||
return sftpStatus(reqID, err)
|
||||
}
|
||||
return Promise.all(
|
||||
files.map((f) => {
|
||||
return new Promise<FileEntry>((resolve, reject) => {
|
||||
const fullPath = path.join(ods[od].path, f)
|
||||
fs.stat(fullPath, (err, stats) => {
|
||||
if (err) {
|
||||
return reject(err)
|
||||
}
|
||||
|
||||
resolve({
|
||||
filename: f,
|
||||
longname: fullPath,
|
||||
attrs: {
|
||||
atime: stats.atimeMs,
|
||||
gid: stats.gid,
|
||||
mode: stats.mode,
|
||||
size: stats.size,
|
||||
mtime: stats.mtimeMs,
|
||||
uid: stats.uid,
|
||||
},
|
||||
})
|
||||
})
|
||||
})
|
||||
}),
|
||||
)
|
||||
.then((files) => {
|
||||
sftp.name(reqID, files)
|
||||
ods[od].read = true
|
||||
})
|
||||
.catch(() => {
|
||||
sftp.status(reqID, ssh.SFTP_STATUS_CODE.FAILURE)
|
||||
})
|
||||
})
|
||||
})
|
||||
|
||||
sftp.on("WRITE", (reqID, handle, offset, data) => {
|
||||
const fd = handle.readUInt32BE(0)
|
||||
if (!fds[fd]) {
|
||||
return sftp.status(reqID, ssh.SFTP_STATUS_CODE.NO_SUCH_FILE)
|
||||
}
|
||||
return fs.write(fd, data, offset, (err) => sftpStatus(reqID, err))
|
||||
})
|
||||
|
||||
sftp.on("CLOSE", (reqID, handle) => {
|
||||
const fd = handle.readUInt32BE(0)
|
||||
if (!fds[fd]) {
|
||||
if (ods[fd]) {
|
||||
delete ods[fd]
|
||||
return sftp.status(reqID, ssh.SFTP_STATUS_CODE.OK)
|
||||
}
|
||||
return sftp.status(reqID, ssh.SFTP_STATUS_CODE.NO_SUCH_FILE)
|
||||
}
|
||||
return fs.close(fd, (err) => sftpStatus(reqID, err))
|
||||
})
|
||||
|
||||
sftp.on("STAT", (reqID, path) => {
|
||||
fs.stat(path, (err, stats) => {
|
||||
if (err) {
|
||||
return sftpStatus(reqID, err)
|
||||
}
|
||||
return sftp.attrs(reqID, {
|
||||
atime: stats.atime.getTime(),
|
||||
gid: stats.gid,
|
||||
mode: stats.mode,
|
||||
mtime: stats.mtime.getTime(),
|
||||
size: stats.size,
|
||||
uid: stats.uid,
|
||||
})
|
||||
})
|
||||
})
|
||||
|
||||
sftp.on("MKDIR", (reqID, path) => {
|
||||
fs.mkdir(path, (err) => sftpStatus(reqID, err))
|
||||
})
|
||||
|
||||
sftp.on("LSTAT", (reqID, path) => {
|
||||
fs.lstat(path, (err, stats) => {
|
||||
if (err) {
|
||||
return sftpStatus(reqID, err)
|
||||
}
|
||||
return sftp.attrs(reqID, {
|
||||
atime: stats.atimeMs,
|
||||
gid: stats.gid,
|
||||
mode: stats.mode,
|
||||
mtime: stats.mtimeMs,
|
||||
size: stats.size,
|
||||
uid: stats.uid,
|
||||
})
|
||||
})
|
||||
})
|
||||
|
||||
sftp.on("REMOVE", (reqID, path) => {
|
||||
fs.unlink(path, (err) => sftpStatus(reqID, err))
|
||||
})
|
||||
|
||||
sftp.on("RMDIR", (reqID, path) => {
|
||||
fs.rmdir(path, (err) => sftpStatus(reqID, err))
|
||||
})
|
||||
|
||||
sftp.on("REALPATH", (reqID, path) => {
|
||||
fs.realpath(path, (pathErr, resolved) => {
|
||||
if (pathErr) {
|
||||
return sftpStatus(reqID, pathErr)
|
||||
}
|
||||
fs.stat(path, (statErr, stat) => {
|
||||
if (statErr) {
|
||||
return sftpStatus(reqID, statErr)
|
||||
}
|
||||
sftp.name(reqID, [
|
||||
{
|
||||
filename: resolved,
|
||||
longname: resolved,
|
||||
attrs: {
|
||||
mode: stat.mode,
|
||||
uid: stat.uid,
|
||||
gid: stat.gid,
|
||||
size: stat.size,
|
||||
atime: stat.atime.getTime(),
|
||||
mtime: stat.mtime.getTime(),
|
||||
},
|
||||
},
|
||||
])
|
||||
return
|
||||
})
|
||||
return
|
||||
})
|
||||
})
|
||||
}
|
||||
@@ -1,122 +0,0 @@
|
||||
/**
|
||||
* Provides utilities for handling SSH connections
|
||||
*/
|
||||
import * as net from "net"
|
||||
import * as cp from "child_process"
|
||||
import * as ssh from "ssh2"
|
||||
import * as nodePty from "node-pty"
|
||||
import { fillSftpStream } from "./sftp"
|
||||
|
||||
/**
|
||||
* Fills out all of the functionality of SSH using node equivalents.
|
||||
*/
|
||||
export function fillSshSession(accept: () => ssh.Session): void {
|
||||
let pty: nodePty.IPty | undefined
|
||||
let activeProcess: cp.ChildProcess
|
||||
let ptyInfo: ssh.PseudoTtyInfo | undefined
|
||||
const env: { [key: string]: string } = {}
|
||||
|
||||
const session = accept()
|
||||
|
||||
// Run a command, stream back the data
|
||||
const cmd = (command: string, channel: ssh.ServerChannel): void => {
|
||||
if (ptyInfo) {
|
||||
// Remove undefined and project env vars
|
||||
// keysToRemove taken from sanitizeProcessEnvironment
|
||||
const keysToRemove = [/^ELECTRON_.+$/, /^GOOGLE_API_KEY$/, /^VSCODE_.+$/, /^SNAP(|_.*)$/]
|
||||
const env = Object.keys(process.env).reduce((prev, k) => {
|
||||
if (process.env[k] === undefined) {
|
||||
return prev
|
||||
}
|
||||
const val = process.env[k] as string
|
||||
if (keysToRemove.find((rx) => val.search(rx))) {
|
||||
return prev
|
||||
}
|
||||
prev[k] = val
|
||||
return prev
|
||||
}, {} as { [key: string]: string })
|
||||
|
||||
pty = nodePty.spawn(command, [], {
|
||||
cols: ptyInfo.cols,
|
||||
rows: ptyInfo.rows,
|
||||
env,
|
||||
})
|
||||
pty.onData((d) => channel.write(d))
|
||||
pty.on("exit", (exitCode) => {
|
||||
channel.exit(exitCode)
|
||||
channel.close()
|
||||
})
|
||||
channel.on("data", (d: string) => pty && pty.write(d))
|
||||
return
|
||||
}
|
||||
|
||||
const proc = cp.spawn(command, { shell: true })
|
||||
proc.stdout.on("data", (d) => channel.stdout.write(d))
|
||||
proc.stderr.on("data", (d) => channel.stderr.write(d))
|
||||
proc.on("exit", (exitCode) => {
|
||||
channel.exit(exitCode || 0)
|
||||
channel.close()
|
||||
})
|
||||
channel.stdin.on("data", (d: unknown) => proc.stdin.write(d))
|
||||
channel.stdin.on("close", () => proc.stdin.end())
|
||||
}
|
||||
|
||||
session.on("pty", (accept, _, info) => {
|
||||
ptyInfo = info
|
||||
accept && accept()
|
||||
})
|
||||
|
||||
session.on("shell", (accept) => {
|
||||
cmd(process.env.SHELL || "/usr/bin/env bash", accept())
|
||||
})
|
||||
|
||||
session.on("exec", (accept, _, info) => {
|
||||
cmd(info.command, accept())
|
||||
})
|
||||
|
||||
session.on("sftp", fillSftpStream)
|
||||
|
||||
session.on("signal", (accept, _, info) => {
|
||||
accept && accept()
|
||||
process.kill((pty || activeProcess).pid, info.name)
|
||||
})
|
||||
|
||||
session.on("env", (accept, _reject, info) => {
|
||||
accept && accept()
|
||||
env[info.key] = info.value
|
||||
})
|
||||
|
||||
session.on("auth-agent", (accept) => {
|
||||
accept()
|
||||
})
|
||||
|
||||
session.on("window-change", (accept, reject, info) => {
|
||||
if (pty) {
|
||||
pty.resize(info.cols, info.rows)
|
||||
accept && accept()
|
||||
} else {
|
||||
reject()
|
||||
}
|
||||
})
|
||||
}
|
||||
|
||||
/**
|
||||
* Pipes a requested port over SSH
|
||||
*/
|
||||
export function forwardSshPort(
|
||||
accept: () => ssh.ServerChannel,
|
||||
reject: () => boolean,
|
||||
info: ssh.TcpipRequestInfo,
|
||||
): void {
|
||||
const fwdSocket = net.createConnection(info.destPort, info.destIP)
|
||||
fwdSocket.on("error", () => reject())
|
||||
fwdSocket.on("connect", () => {
|
||||
const channel = accept()
|
||||
channel.pipe(fwdSocket)
|
||||
channel.on("close", () => fwdSocket.end())
|
||||
fwdSocket.pipe(channel)
|
||||
fwdSocket.on("close", () => channel.close())
|
||||
fwdSocket.on("error", () => channel.end())
|
||||
fwdSocket.on("end", () => channel.end())
|
||||
})
|
||||
}
|
||||
121
src/node/util.ts
121
src/node/util.ts
@@ -4,24 +4,54 @@ import * as fs from "fs-extra"
|
||||
import * as os from "os"
|
||||
import * as path from "path"
|
||||
import * as util from "util"
|
||||
import envPaths from "env-paths"
|
||||
import xdgBasedir from "xdg-basedir"
|
||||
|
||||
export const tmpdir = path.join(os.tmpdir(), "code-server")
|
||||
|
||||
const getXdgDataDir = (): string => {
|
||||
switch (process.platform) {
|
||||
case "win32":
|
||||
return path.join(process.env.XDG_DATA_HOME || path.join(os.homedir(), "AppData/Local"), "code-server/Data")
|
||||
case "darwin":
|
||||
return path.join(
|
||||
process.env.XDG_DATA_HOME || path.join(os.homedir(), "Library/Application Support"),
|
||||
"code-server",
|
||||
)
|
||||
default:
|
||||
return path.join(process.env.XDG_DATA_HOME || path.join(os.homedir(), ".local/share"), "code-server")
|
||||
}
|
||||
interface Paths {
|
||||
data: string
|
||||
config: string
|
||||
}
|
||||
|
||||
export const xdgLocalDir = getXdgDataDir()
|
||||
export const paths = getEnvPaths()
|
||||
|
||||
/**
|
||||
* Gets the config and data paths for the current platform/configuration.
|
||||
* On MacOS this function gets the standard XDG directories instead of using the native macOS
|
||||
* ones. Most CLIs do this as in practice only GUI apps use the standard macOS directories.
|
||||
*/
|
||||
function getEnvPaths(): Paths {
|
||||
let paths: Paths
|
||||
if (process.platform === "win32") {
|
||||
paths = envPaths("code-server", {
|
||||
suffix: "",
|
||||
})
|
||||
} else {
|
||||
if (xdgBasedir.data === undefined || xdgBasedir.config === undefined) {
|
||||
throw new Error("No home folder?")
|
||||
}
|
||||
paths = {
|
||||
data: path.join(xdgBasedir.data, "code-server"),
|
||||
config: path.join(xdgBasedir.config, "code-server"),
|
||||
}
|
||||
}
|
||||
|
||||
return paths
|
||||
}
|
||||
|
||||
/**
|
||||
* humanPath replaces the home directory in p with ~.
|
||||
* Makes it more readable.
|
||||
*
|
||||
* @param p
|
||||
*/
|
||||
export function humanPath(p?: string): string {
|
||||
if (!p) {
|
||||
return ""
|
||||
}
|
||||
return p.replace(os.homedir(), "~")
|
||||
}
|
||||
|
||||
export const generateCertificate = async (): Promise<{ cert: string; certKey: string }> => {
|
||||
const paths = {
|
||||
@@ -44,12 +74,6 @@ export const generateCertificate = async (): Promise<{ cert: string; certKey: st
|
||||
return paths
|
||||
}
|
||||
|
||||
export const generateSshHostKey = async (): Promise<string> => {
|
||||
// Just reuse the SSL cert as the SSH host key
|
||||
const { certKey } = await generateCertificate()
|
||||
return certKey
|
||||
}
|
||||
|
||||
export const generatePassword = async (length = 24): Promise<string> => {
|
||||
const buffer = Buffer.alloc(Math.ceil(length / 2))
|
||||
await util.promisify(crypto.randomFill)(buffer)
|
||||
@@ -57,10 +81,7 @@ export const generatePassword = async (length = 24): Promise<string> => {
|
||||
}
|
||||
|
||||
export const hash = (str: string): string => {
|
||||
return crypto
|
||||
.createHash("sha256")
|
||||
.update(str)
|
||||
.digest("hex")
|
||||
return crypto.createHash("sha256").update(str).digest("hex")
|
||||
}
|
||||
|
||||
const mimeTypes: { [key: string]: string } = {
|
||||
@@ -126,11 +147,7 @@ export const getMediaMime = (filePath?: string): string => {
|
||||
|
||||
export const isWsl = async (): Promise<boolean> => {
|
||||
return (
|
||||
(process.platform === "linux" &&
|
||||
os
|
||||
.release()
|
||||
.toLowerCase()
|
||||
.indexOf("microsoft") !== -1) ||
|
||||
(process.platform === "linux" && os.release().toLowerCase().indexOf("microsoft") !== -1) ||
|
||||
(await fs.readFile("/proc/version", "utf8")).toLowerCase().indexOf("microsoft") !== -1
|
||||
)
|
||||
}
|
||||
@@ -200,3 +217,51 @@ export function extend(...args: any[]): any {
|
||||
}
|
||||
return c
|
||||
}
|
||||
|
||||
/**
|
||||
* Taken from vs/base/common/charCode.ts. Copied for now instead of importing so
|
||||
* we don't have to set up a `vs` alias to be able to import with types (since
|
||||
* the alternative is to directly import from `out`).
|
||||
*/
|
||||
const enum CharCode {
|
||||
Slash = 47,
|
||||
A = 65,
|
||||
Z = 90,
|
||||
a = 97,
|
||||
z = 122,
|
||||
Colon = 58,
|
||||
}
|
||||
|
||||
/**
|
||||
* Compute `fsPath` for the given uri.
|
||||
* Taken from vs/base/common/uri.ts. It's not imported to avoid also importing
|
||||
* everything that file imports.
|
||||
*/
|
||||
export function pathToFsPath(path: string, keepDriveLetterCasing = false): string {
|
||||
const isWindows = process.platform === "win32"
|
||||
const uri = { authority: undefined, path, scheme: "file" }
|
||||
let value: string
|
||||
if (uri.authority && uri.path.length > 1 && uri.scheme === "file") {
|
||||
// unc path: file://shares/c$/far/boo
|
||||
value = `//${uri.authority}${uri.path}`
|
||||
} else if (
|
||||
uri.path.charCodeAt(0) === CharCode.Slash &&
|
||||
((uri.path.charCodeAt(1) >= CharCode.A && uri.path.charCodeAt(1) <= CharCode.Z) ||
|
||||
(uri.path.charCodeAt(1) >= CharCode.a && uri.path.charCodeAt(1) <= CharCode.z)) &&
|
||||
uri.path.charCodeAt(2) === CharCode.Colon
|
||||
) {
|
||||
if (!keepDriveLetterCasing) {
|
||||
// windows drive letter: file:///c:/far/boo
|
||||
value = uri.path[1].toLowerCase() + uri.path.substr(2)
|
||||
} else {
|
||||
value = uri.path.substr(1)
|
||||
}
|
||||
} else {
|
||||
// other path
|
||||
value = uri.path
|
||||
}
|
||||
if (isWindows) {
|
||||
value = value.replace(/\//g, "\\")
|
||||
}
|
||||
return value
|
||||
}
|
||||
|
||||
@@ -38,7 +38,7 @@ export class IpcMain {
|
||||
|
||||
// Ensure we control when the process exits.
|
||||
this.exit = process.exit
|
||||
process.exit = function(code?: number) {
|
||||
process.exit = function (code?: number) {
|
||||
logger.warn(`process.exit() was prevented: ${code || "unknown code"}.`)
|
||||
} as (code?: number) => never
|
||||
|
||||
|
||||
149
test/cli.test.ts
149
test/cli.test.ts
@@ -1,24 +1,29 @@
|
||||
import { logger, Level } from "@coder/logger"
|
||||
import * as assert from "assert"
|
||||
import * as path from "path"
|
||||
import { parse } from "../src/node/cli"
|
||||
import { xdgLocalDir } from "../src/node/util"
|
||||
import { paths } from "../src/node/util"
|
||||
|
||||
describe("cli", () => {
|
||||
beforeEach(() => {
|
||||
delete process.env.LOG_LEVEL
|
||||
})
|
||||
|
||||
it("should set defaults", () => {
|
||||
assert.deepEqual(parse([]), {
|
||||
_: [],
|
||||
"extensions-dir": path.join(xdgLocalDir, "extensions"),
|
||||
"user-data-dir": xdgLocalDir,
|
||||
})
|
||||
// The parser will always fill these out.
|
||||
const defaults = {
|
||||
_: [],
|
||||
"extensions-dir": path.join(paths.data, "extensions"),
|
||||
"user-data-dir": paths.data,
|
||||
}
|
||||
|
||||
it("should set defaults", async () => {
|
||||
assert.deepEqual(await await parse([]), defaults)
|
||||
})
|
||||
|
||||
it("should parse all available options", () => {
|
||||
it("should parse all available options", async () => {
|
||||
assert.deepEqual(
|
||||
parse([
|
||||
await await parse([
|
||||
"--bind-addr=192.169.0.1:8080",
|
||||
"--auth",
|
||||
"none",
|
||||
"--extensions-dir",
|
||||
@@ -74,90 +79,132 @@ describe("cli", () => {
|
||||
"user-data-dir": path.resolve("bar"),
|
||||
verbose: true,
|
||||
version: true,
|
||||
"bind-addr": "192.169.0.1:8080",
|
||||
},
|
||||
)
|
||||
})
|
||||
|
||||
it("should work with short options", () => {
|
||||
assert.deepEqual(parse(["-vvv", "-v"]), {
|
||||
_: [],
|
||||
"extensions-dir": path.join(xdgLocalDir, "extensions"),
|
||||
"user-data-dir": xdgLocalDir,
|
||||
it("should work with short options", async () => {
|
||||
assert.deepEqual(await 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 () => {
|
||||
process.env.LOG_LEVEL = "debug"
|
||||
assert.deepEqual(parse([]), {
|
||||
_: [],
|
||||
"extensions-dir": path.join(xdgLocalDir, "extensions"),
|
||||
"user-data-dir": xdgLocalDir,
|
||||
assert.deepEqual(await parse([]), {
|
||||
...defaults,
|
||||
log: "debug",
|
||||
})
|
||||
assert.equal(process.env.LOG_LEVEL, "debug")
|
||||
assert.equal(logger.level, Level.Debug)
|
||||
|
||||
process.env.LOG_LEVEL = "trace"
|
||||
assert.deepEqual(await parse([]), {
|
||||
...defaults,
|
||||
log: "trace",
|
||||
verbose: true,
|
||||
})
|
||||
assert.equal(process.env.LOG_LEVEL, "trace")
|
||||
assert.equal(logger.level, Level.Trace)
|
||||
})
|
||||
|
||||
it("should prefer --log to env var", () => {
|
||||
it("should prefer --log to env var and --verbose to --log", async () => {
|
||||
process.env.LOG_LEVEL = "debug"
|
||||
assert.deepEqual(parse(["--log", "info"]), {
|
||||
_: [],
|
||||
"extensions-dir": path.join(xdgLocalDir, "extensions"),
|
||||
"user-data-dir": xdgLocalDir,
|
||||
assert.deepEqual(await parse(["--log", "info"]), {
|
||||
...defaults,
|
||||
log: "info",
|
||||
})
|
||||
assert.equal(process.env.LOG_LEVEL, "info")
|
||||
assert.equal(logger.level, Level.Info)
|
||||
|
||||
process.env.LOG_LEVEL = "trace"
|
||||
assert.deepEqual(await parse(["--log", "info"]), {
|
||||
...defaults,
|
||||
log: "info",
|
||||
})
|
||||
assert.equal(process.env.LOG_LEVEL, "info")
|
||||
assert.equal(logger.level, Level.Info)
|
||||
|
||||
process.env.LOG_LEVEL = "warn"
|
||||
assert.deepEqual(await parse(["--log", "info", "--verbose"]), {
|
||||
...defaults,
|
||||
log: "trace",
|
||||
verbose: true,
|
||||
})
|
||||
assert.equal(process.env.LOG_LEVEL, "trace")
|
||||
assert.equal(logger.level, Level.Trace)
|
||||
})
|
||||
|
||||
it("should error if value isn't provided", () => {
|
||||
assert.throws(() => parse(["--auth"]), /--auth requires a value/)
|
||||
assert.throws(() => parse(["--auth=", "--log=debug"]), /--auth requires a value/)
|
||||
assert.throws(() => parse(["--auth", "--log"]), /--auth requires a value/)
|
||||
assert.throws(() => parse(["--auth", "--invalid"]), /--auth requires a value/)
|
||||
it("should ignore invalid log level env var", async () => {
|
||||
process.env.LOG_LEVEL = "bogus"
|
||||
assert.deepEqual(await parse([]), defaults)
|
||||
})
|
||||
|
||||
it("should error if value is invalid", () => {
|
||||
assert.throws(() => parse(["--port", "foo"]), /--port must be a number/)
|
||||
assert.throws(() => parse(["--auth", "invalid"]), /--auth valid values: \[password, none\]/)
|
||||
assert.throws(() => parse(["--log", "invalid"]), /--log valid values: \[trace, debug, info, warn, error\]/)
|
||||
it("should error if value isn't provided", async () => {
|
||||
await assert.rejects(async () => await parse(["--auth"]), /--auth requires a value/)
|
||||
await assert.rejects(async () => await parse(["--auth=", "--log=debug"]), /--auth requires a value/)
|
||||
await assert.rejects(async () => await parse(["--auth", "--log"]), /--auth requires a value/)
|
||||
await assert.rejects(async () => await parse(["--auth", "--invalid"]), /--auth requires a value/)
|
||||
await assert.rejects(async () => await parse(["--bind-addr"]), /--bind-addr requires a value/)
|
||||
})
|
||||
|
||||
it("should error if the option doesn't exist", () => {
|
||||
assert.throws(() => parse(["--foo"]), /Unknown option --foo/)
|
||||
it("should error if value is invalid", async () => {
|
||||
await assert.rejects(async () => await parse(["--port", "foo"]), /--port must be a number/)
|
||||
await assert.rejects(async () => await parse(["--auth", "invalid"]), /--auth valid values: \[password, none\]/)
|
||||
await assert.rejects(
|
||||
async () => await parse(["--log", "invalid"]),
|
||||
/--log valid values: \[trace, debug, info, warn, error\]/,
|
||||
)
|
||||
})
|
||||
|
||||
it("should not error if the value is optional", () => {
|
||||
assert.deepEqual(parse(["--cert"]), {
|
||||
_: [],
|
||||
"extensions-dir": path.join(xdgLocalDir, "extensions"),
|
||||
"user-data-dir": xdgLocalDir,
|
||||
it("should error if the option doesn't exist", async () => {
|
||||
await assert.rejects(async () => await parse(["--foo"]), /Unknown option --foo/)
|
||||
})
|
||||
|
||||
it("should not error if the value is optional", async () => {
|
||||
assert.deepEqual(await parse(["--cert"]), {
|
||||
...defaults,
|
||||
cert: {
|
||||
value: undefined,
|
||||
},
|
||||
})
|
||||
})
|
||||
|
||||
it("should not allow option-like values", () => {
|
||||
assert.throws(() => parse(["--socket", "--socket-path-value"]), /--socket requires a value/)
|
||||
it("should not allow option-like values", async () => {
|
||||
await assert.rejects(async () => await 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"]), {
|
||||
_: [],
|
||||
"extensions-dir": path.join(xdgLocalDir, "extensions"),
|
||||
"user-data-dir": xdgLocalDir,
|
||||
assert.deepEqual(await parse(["--socket", "./--socket-path-value"]), {
|
||||
...defaults,
|
||||
socket: path.resolve("--socket-path-value"),
|
||||
})
|
||||
assert.throws(() => parse(["--cert", "--socket-path-value"]), /Unknown option --socket-path-value/)
|
||||
await assert.rejects(
|
||||
async () => await parse(["--cert", "--socket-path-value"]),
|
||||
/Unknown option --socket-path-value/,
|
||||
)
|
||||
})
|
||||
|
||||
it("should allow positional arguments before options", () => {
|
||||
assert.deepEqual(parse(["foo", "test", "--auth", "none"]), {
|
||||
it("should allow positional arguments before options", async () => {
|
||||
assert.deepEqual(await parse(["foo", "test", "--auth", "none"]), {
|
||||
...defaults,
|
||||
_: ["foo", "test"],
|
||||
"extensions-dir": path.join(xdgLocalDir, "extensions"),
|
||||
"user-data-dir": xdgLocalDir,
|
||||
auth: "none",
|
||||
})
|
||||
})
|
||||
|
||||
it("should support repeatable flags", async () => {
|
||||
assert.deepEqual(await parse(["--proxy-domain", "*.coder.com"]), {
|
||||
...defaults,
|
||||
"proxy-domain": ["*.coder.com"],
|
||||
})
|
||||
assert.deepEqual(await parse(["--proxy-domain", "*.coder.com", "--proxy-domain", "test.com"]), {
|
||||
...defaults,
|
||||
"proxy-domain": ["*.coder.com", "test.com"],
|
||||
})
|
||||
})
|
||||
})
|
||||
|
||||
@@ -214,13 +214,18 @@ describe("update", () => {
|
||||
await p.downloadAndApplyUpdate(update, destination)
|
||||
assert.equal(`console.log("UPDATED")`, await fs.readFile(entry, "utf8"))
|
||||
|
||||
// Should still work if there is no existing version somehow.
|
||||
await fs.remove(destination)
|
||||
await p.downloadAndApplyUpdate(update, destination)
|
||||
assert.equal(`console.log("UPDATED")`, await fs.readFile(entry, "utf8"))
|
||||
// There should be a backup.
|
||||
const dir = (await fs.readdir(path.join(tmpdir, "tests/updates"))).filter((dir) => {
|
||||
return dir.startsWith("code-server.")
|
||||
})
|
||||
assert.equal(dir.length, 1)
|
||||
assert.equal(
|
||||
`console.log("OLD")`,
|
||||
await fs.readFile(path.join(tmpdir, "tests/updates", dir[0], "code-server"), "utf8"),
|
||||
)
|
||||
|
||||
const archiveName = await p.getReleaseName(update)
|
||||
assert.deepEqual(spy, ["/latest", `/download/${version}/${archiveName}`, `/download/${version}/${archiveName}`])
|
||||
assert.deepEqual(spy, ["/latest", `/download/${version}/${archiveName}`])
|
||||
})
|
||||
|
||||
it("should not reject if unable to fetch", async () => {
|
||||
|
||||
Reference in New Issue
Block a user