mirror of
https://github.com/coder/code-server.git
synced 2026-04-16 03:48:16 -05:00
Compare commits
138 Commits
| Author | SHA1 | Date | |
|---|---|---|---|
|
|
c55f0e8b0b | ||
|
|
0501a19603 | ||
|
|
9e583fa562 | ||
|
|
3b91cffae5 | ||
|
|
4b4ec37880 | ||
|
|
9d9f3a41ab | ||
|
|
c3eb9b800e | ||
|
|
6c9c84090e | ||
|
|
d15731a570 | ||
|
|
80f30fc42c | ||
|
|
62b3a6fd9f | ||
|
|
6a2740f57e | ||
|
|
3d4660491a | ||
|
|
65d7420ee7 | ||
|
|
0bc96989c2 | ||
|
|
bd09533f40 | ||
|
|
db35c47315 | ||
|
|
3157a40044 | ||
|
|
5fe16be62d | ||
|
|
dd29a823c3 | ||
|
|
cd26f84bc6 | ||
|
|
ccb9d948c0 | ||
|
|
0a072f7532 | ||
|
|
16a5f2e171 | ||
|
|
e705948ef3 | ||
|
|
6606040835 | ||
|
|
cdf0deb534 | ||
|
|
d052cc246d | ||
|
|
e4a797dc9e | ||
|
|
1b60ef418c | ||
|
|
31d5823d10 | ||
|
|
605c3c6367 | ||
|
|
94b2774f8c | ||
|
|
4399c7df64 | ||
|
|
03651e5e0b | ||
|
|
219cad1783 | ||
|
|
dcc9cf3dd2 | ||
|
|
13404df267 | ||
|
|
13e91dedc1 | ||
|
|
0cbf1ca58c | ||
|
|
9c861570cd | ||
|
|
743bab09cd | ||
|
|
976891a2e2 | ||
|
|
946e4e8843 | ||
|
|
49c9c191b9 | ||
|
|
433a3d91dd | ||
|
|
0100d4c72a | ||
|
|
aa9755b8fb | ||
|
|
4abc525801 | ||
|
|
705e821741 | ||
|
|
0e97a94acf | ||
|
|
3520ad2286 | ||
|
|
ad17c7dc8e | ||
|
|
271bc06754 | ||
|
|
dfc505b977 | ||
|
|
ee1de79af6 | ||
|
|
fa6f47965a | ||
|
|
1af83a3c56 | ||
|
|
8745f1aa63 | ||
|
|
bb1799ba04 | ||
|
|
70fbfc7ada | ||
|
|
59f667ec14 | ||
|
|
816152ddbd | ||
|
|
45319ec648 | ||
|
|
e588f8b0b0 | ||
|
|
7c26a82a17 | ||
|
|
75e44fdc84 | ||
|
|
8a4ed5a2e0 | ||
|
|
a8d873f9f8 | ||
|
|
7183b5a43e | ||
|
|
8136769b66 | ||
|
|
672038c6af | ||
|
|
2d7aeb5bf9 | ||
|
|
f342c327f3 | ||
|
|
4619786019 | ||
|
|
1dfcd0448e | ||
|
|
48ce49eec8 | ||
|
|
d8c344beda | ||
|
|
beebf53adc | ||
|
|
a14fa862da | ||
|
|
fc0d127470 | ||
|
|
6eda7ae81f | ||
|
|
3f75aa2db1 | ||
|
|
328b864534 | ||
|
|
876d0275b3 | ||
|
|
b9989ca2db | ||
|
|
4e310b4985 | ||
|
|
d54b4accac | ||
|
|
5e7b419820 | ||
|
|
1b80244af7 | ||
|
|
999960eef5 | ||
|
|
6c95f72d2b | ||
|
|
8ef950af4c | ||
|
|
e5f03e0b06 | ||
|
|
77c1150b8d | ||
|
|
fb2625dbe8 | ||
|
|
82d93186f8 | ||
|
|
8f72481712 | ||
|
|
edb3b41800 | ||
|
|
97a292235f | ||
|
|
4c6e43d8d6 | ||
|
|
8bae81dd83 | ||
|
|
be90b5de35 | ||
|
|
1f819fa534 | ||
|
|
5a36627aae | ||
|
|
c11d51ed7a | ||
|
|
900c34c613 | ||
|
|
c91033c611 | ||
|
|
2c6a47d42b | ||
|
|
05d66c1358 | ||
|
|
bf10341e3f | ||
|
|
83eade6897 | ||
|
|
f3ef414fd2 | ||
|
|
014faf5b1c | ||
|
|
1f95a23d1d | ||
|
|
ed0926819d | ||
|
|
ee04915d12 | ||
|
|
e58b072f24 | ||
|
|
b27e1d8dd4 | ||
|
|
8156fc040a | ||
|
|
80f3884e0f | ||
|
|
4229e95a7d | ||
|
|
e0a1220126 | ||
|
|
e8063c7efd | ||
|
|
1440b263f4 | ||
|
|
f84757507b | ||
|
|
76f4054242 | ||
|
|
a673cf2833 | ||
|
|
30ade712bf | ||
|
|
92d0d28dd7 | ||
|
|
a3cea88f51 | ||
|
|
4f3c8a556e | ||
|
|
6a692487c8 | ||
|
|
0609a1b2bd | ||
|
|
3c61d96d4f | ||
|
|
7925f88776 | ||
|
|
ddd09454df | ||
|
|
b37ff28a0a |
@@ -3,4 +3,9 @@ root = true
|
||||
[*]
|
||||
indent_style = space
|
||||
trim_trailing_whitespace = true
|
||||
|
||||
# The indent size used in the `package.json` file cannot be changed
|
||||
# https://github.com/npm/npm/pull/3180#issuecomment-16336516
|
||||
[{*.yml,*.yaml,package.json}]
|
||||
indent_style = space
|
||||
indent_size = 2
|
||||
|
||||
@@ -36,11 +36,8 @@ rules:
|
||||
import/order:
|
||||
[error, { alphabetize: { order: "asc" }, groups: [["builtin", "external", "internal"], "parent", "sibling"] }]
|
||||
no-async-promise-executor: off
|
||||
# This isn't a real module, just types, which apparently doesn't resolve.
|
||||
import/no-unresolved: [error, { ignore: ["express-serve-static-core"] }]
|
||||
|
||||
settings:
|
||||
# Does not work with CommonJS unfortunately.
|
||||
import/ignore:
|
||||
- env-paths
|
||||
- xdg-basedir
|
||||
import/resolver:
|
||||
typescript:
|
||||
alwaysTryTypes: true
|
||||
|
||||
2
.github/CODEOWNERS
vendored
2
.github/CODEOWNERS
vendored
@@ -1,3 +1,3 @@
|
||||
* @cdr/code-server-reviewers
|
||||
|
||||
ci/helm-chart @Matthew-Beckett @alexgorbatchev
|
||||
ci/helm-chart/ @Matthew-Beckett @alexgorbatchev
|
||||
|
||||
18
.github/ISSUE_TEMPLATE/extension-request.md
vendored
18
.github/ISSUE_TEMPLATE/extension-request.md
vendored
@@ -1,18 +0,0 @@
|
||||
---
|
||||
name: Extension request
|
||||
about: Request an extension missing from the code-server marketplace
|
||||
title: ""
|
||||
labels: extension-request
|
||||
assignees: ""
|
||||
---
|
||||
|
||||
<!--
|
||||
Details on the code-server extension marketplace are at
|
||||
|
||||
https://github.com/cdr/code-server/blob/master/docs/FAQ.md#whats-the-deal-with-extensions
|
||||
|
||||
Please fill in the issue template!
|
||||
-->
|
||||
|
||||
- [ ] Extension name:
|
||||
- [ ] Extension GitHub or homepage:
|
||||
8
.github/codecov.yml
vendored
8
.github/codecov.yml
vendored
@@ -8,6 +8,14 @@ coverage:
|
||||
range: "40...70"
|
||||
status:
|
||||
patch: off
|
||||
notify:
|
||||
slack:
|
||||
default:
|
||||
url: secret:v1::tXC7VwEIKYjNU8HRgRv2GdKOSCt5UzpykKZb+o1eCDqBgb2PEqwE3A26QUPYMLo4BO2qtrJhFIvwhUvlPwyzDCNGoNiuZfXr0UeZZ0y1TcZu672R/NBNMwEPO/e1Ye0pHxjzKHnuH7HqbjFucox/RBQLtiL3J56SWGE3JtbkC6o=
|
||||
threshold: 1%
|
||||
only_pulls: false
|
||||
branches:
|
||||
- "main"
|
||||
|
||||
parsers:
|
||||
gcov:
|
||||
|
||||
37
.github/lock.yml
vendored
37
.github/lock.yml
vendored
@@ -1,37 +0,0 @@
|
||||
# Configuration for Lock Threads - https://github.com/dessant/lock-threads-app
|
||||
|
||||
# Number of days of inactivity before a closed issue or pull request is locked
|
||||
daysUntilLock: 90
|
||||
|
||||
# Skip issues and pull requests created before a given timestamp. Timestamp must
|
||||
# follow ISO 8601 (`YYYY-MM-DD`). Set to `false` to disable
|
||||
skipCreatedBefore: false
|
||||
|
||||
# Issues and pull requests with these labels will be ignored. Set to `[]` to disable
|
||||
exemptLabels: []
|
||||
|
||||
# Label to add before locking, such as `outdated`. Set to `false` to disable
|
||||
lockLabel: false
|
||||
|
||||
# Comment to post before locking. Set to `false` to disable
|
||||
lockComment: >
|
||||
This thread has been automatically locked since there has not been
|
||||
any recent activity after it was closed. Please open a new issue for
|
||||
related bugs.
|
||||
|
||||
# Assign `resolved` as the reason for locking. Set to `false` to disable
|
||||
setLockReason: true
|
||||
# Limit to only `issues` or `pulls`
|
||||
# only: issues
|
||||
|
||||
# Optionally, specify configuration settings just for `issues` or `pulls`
|
||||
# issues:
|
||||
# exemptLabels:
|
||||
# - help-wanted
|
||||
# lockLabel: outdated
|
||||
|
||||
# pulls:
|
||||
# daysUntilLock: 30
|
||||
|
||||
# Repository to extend settings from
|
||||
# _extends: repo
|
||||
12
.github/ranger.yml
vendored
12
.github/ranger.yml
vendored
@@ -19,18 +19,6 @@ labels:
|
||||
action: comment
|
||||
delay: 5s
|
||||
message: "Thanks for making your first contribution! :slightly_smiling_face:"
|
||||
extension-request:
|
||||
action: close
|
||||
delay: 5s
|
||||
comment: >
|
||||
Thanks for opening an extension request!
|
||||
We are currently in the process of switching extension
|
||||
marketplaces and transitioning over to [Open VSX](https://open-vsx.org/).
|
||||
Once https://github.com/eclipse/openvsx/issues/249 is implemented, we
|
||||
can fully make this transition. Therefore, we are no longer accepting
|
||||
new requests for extension requests. We suggest installing the VSIX
|
||||
file and then installing into code-server as a temporary workaround.
|
||||
See [docs](https://github.com/cdr/code-server/blob/main/docs/FAQ.md#installing-vsix-extensions-via-the-command-line) for more info.
|
||||
"upstream:vscode":
|
||||
action: close
|
||||
delay: 5s
|
||||
|
||||
76
.github/workflows/ci.yaml
vendored
76
.github/workflows/ci.yaml
vendored
@@ -19,8 +19,6 @@ jobs:
|
||||
name: Pre-build checks
|
||||
runs-on: ubuntu-latest
|
||||
timeout-minutes: 15
|
||||
env:
|
||||
CODECOV_TOKEN: ${{ secrets.CODECOV_TOKEN }}
|
||||
steps:
|
||||
- name: Checkout repo
|
||||
uses: actions/checkout@v2
|
||||
@@ -33,17 +31,21 @@ jobs:
|
||||
- name: Install helm
|
||||
uses: azure/setup-helm@v1.1
|
||||
|
||||
- name: Fetch dependencies from cache
|
||||
id: cache-yarn
|
||||
uses: actions/cache@v2
|
||||
with:
|
||||
path: "**/node_modules"
|
||||
key: yarn-build-${{ hashFiles('**/yarn.lock') }}
|
||||
restore-keys: |
|
||||
yarn-build-
|
||||
# NOTE@jsjoeio
|
||||
# disabling this until we can audit the build process
|
||||
# and the usefulness of this step
|
||||
# See: https://github.com/cdr/code-server/issues/4287
|
||||
# - name: Fetch dependencies from cache
|
||||
# id: cache-yarn
|
||||
# uses: actions/cache@v2
|
||||
# with:
|
||||
# path: "**/node_modules"
|
||||
# key: yarn-build-${{ hashFiles('**/yarn.lock') }}
|
||||
# restore-keys: |
|
||||
# yarn-build-
|
||||
|
||||
- name: Install dependencies
|
||||
if: steps.cache-yarn.outputs.cache-hit != 'true'
|
||||
# if: steps.cache-yarn.outputs.cache-hit != 'true'
|
||||
run: yarn --frozen-lockfile
|
||||
|
||||
- name: Run yarn fmt
|
||||
@@ -54,19 +56,11 @@ jobs:
|
||||
run: yarn lint
|
||||
if: success()
|
||||
|
||||
- name: Run code-server unit tests
|
||||
run: yarn test:unit
|
||||
if: success()
|
||||
|
||||
- name: Upload coverage report to Codecov
|
||||
run: yarn coverage
|
||||
if: success()
|
||||
|
||||
audit-ci:
|
||||
name: Run audit-ci
|
||||
needs: prebuild
|
||||
runs-on: ubuntu-latest
|
||||
timeout-minutes: 5
|
||||
timeout-minutes: 15
|
||||
steps:
|
||||
- name: Checkout repo
|
||||
uses: actions/checkout@v2
|
||||
@@ -98,6 +92,8 @@ jobs:
|
||||
needs: prebuild
|
||||
runs-on: ubuntu-latest
|
||||
timeout-minutes: 30
|
||||
env:
|
||||
CODECOV_TOKEN: ${{ secrets.CODECOV_TOKEN }}
|
||||
steps:
|
||||
- uses: actions/checkout@v2
|
||||
with:
|
||||
@@ -146,14 +142,25 @@ jobs:
|
||||
path: |
|
||||
vendor/modules/code-oss-dev/.build
|
||||
vendor/modules/code-oss-dev/out-build
|
||||
vendor/modules/code-oss-dev/out-vscode
|
||||
vendor/modules/code-oss-dev/out-vscode-min
|
||||
key: vscode-build-${{ steps.vscode-rev.outputs.rev }}
|
||||
vendor/modules/code-oss-dev/out-vscode-reh-web
|
||||
vendor/modules/code-oss-dev/out-vscode-reh-web-min
|
||||
key: vscode-reh-build-${{ steps.vscode-rev.outputs.rev }}
|
||||
|
||||
- name: Build vscode
|
||||
if: steps.cache-vscode.outputs.cache-hit != 'true'
|
||||
run: yarn build:vscode
|
||||
|
||||
# Our code imports code from VS Code's `out` directory meaning VS Code
|
||||
# must be built before running these tests.
|
||||
# TODO: Move to its own step?
|
||||
- name: Run code-server unit tests
|
||||
run: yarn test:unit
|
||||
if: success()
|
||||
|
||||
- name: Upload coverage report to Codecov
|
||||
run: yarn coverage
|
||||
if: success()
|
||||
|
||||
# The release package does not contain any native modules
|
||||
# and is neutral to architecture/os/libc version.
|
||||
- name: Create release package
|
||||
@@ -244,10 +251,14 @@ jobs:
|
||||
# so we just build with "native"/x86_64 node, then download arm64/armv7l node
|
||||
# and then put it in our release. We can't smoke test the cross build this way,
|
||||
# but this means we don't need to maintain a self-hosted runner!
|
||||
|
||||
# NOTE@jsjoeio:
|
||||
# We used to use 16.04 until GitHub deprecated it on September 20, 2021
|
||||
# See here: https://github.com/actions/virtual-environments/pull/3862/files
|
||||
package-linux-cross:
|
||||
name: Linux cross-compile builds
|
||||
needs: build
|
||||
runs-on: ubuntu-16.04
|
||||
runs-on: ubuntu-18.04
|
||||
timeout-minutes: 15
|
||||
strategy:
|
||||
matrix:
|
||||
@@ -279,7 +290,7 @@ jobs:
|
||||
echo "$HOME/.local/bin" >> $GITHUB_PATH
|
||||
|
||||
- name: Install cross-compiler
|
||||
run: sudo apt install $PACKAGE
|
||||
run: sudo apt update && sudo apt install $PACKAGE
|
||||
env:
|
||||
PACKAGE: ${{ format('g++-{0}', matrix.prefix) }}
|
||||
|
||||
@@ -367,9 +378,6 @@ jobs:
|
||||
with:
|
||||
node-version: "14"
|
||||
|
||||
- name: Install playwright OS dependencies
|
||||
run: npx playwright install-deps
|
||||
|
||||
- name: Fetch dependencies from cache
|
||||
id: cache-yarn
|
||||
uses: actions/cache@v2
|
||||
@@ -395,14 +403,10 @@ jobs:
|
||||
if: steps.cache-yarn.outputs.cache-hit != 'true'
|
||||
run: yarn --frozen-lockfile
|
||||
|
||||
# HACK: this shouldn't need to exist, but put it here anyway
|
||||
# in an attempt to solve Playwright cache failures.
|
||||
- name: Reinstall playwright
|
||||
if: steps.cache-yarn.outputs.cache-hit == 'true'
|
||||
- name: Install Playwright OS dependencies
|
||||
run: |
|
||||
cd test/
|
||||
rm -r node_modules/playwright
|
||||
yarn install --check-files
|
||||
./test/node_modules/.bin/playwright install-deps
|
||||
./test/node_modules/.bin/playwright install
|
||||
|
||||
- name: Run end-to-end tests
|
||||
run: yarn test:e2e
|
||||
@@ -424,7 +428,7 @@ jobs:
|
||||
uses: actions/checkout@v2
|
||||
- name: Run Trivy vulnerability scanner in repo mode
|
||||
#Commit SHA for v0.0.17
|
||||
uses: aquasecurity/trivy-action@8eccb5539730451af599c84f444c6d6cf0fc2bb0
|
||||
uses: aquasecurity/trivy-action@0769bbf0d2a77b3c15b3b57fbcdd2edd25a1c3f0
|
||||
with:
|
||||
scan-type: "fs"
|
||||
scan-ref: "."
|
||||
|
||||
2
.github/workflows/docker.yaml
vendored
2
.github/workflows/docker.yaml
vendored
@@ -6,7 +6,7 @@ on:
|
||||
workflow_dispatch:
|
||||
|
||||
release:
|
||||
types: [published]
|
||||
types: [released]
|
||||
|
||||
jobs:
|
||||
docker-images:
|
||||
|
||||
2
.github/workflows/docs-preview.yaml
vendored
2
.github/workflows/docs-preview.yaml
vendored
@@ -37,7 +37,7 @@ jobs:
|
||||
- name: Install Node.js
|
||||
uses: actions/setup-node@v2
|
||||
with:
|
||||
node-version: 12.x
|
||||
node-version: 14
|
||||
|
||||
- name: Cache Node Modules
|
||||
uses: actions/cache@v2
|
||||
|
||||
4
.github/workflows/installer.yml
vendored
4
.github/workflows/installer.yml
vendored
@@ -5,10 +5,12 @@ on:
|
||||
branches:
|
||||
- main
|
||||
paths:
|
||||
- "installer.sh"
|
||||
- "install.sh"
|
||||
pull_request:
|
||||
branches:
|
||||
- main
|
||||
paths:
|
||||
- "install.sh"
|
||||
|
||||
jobs:
|
||||
ubuntu:
|
||||
|
||||
2
.github/workflows/npm-brew.yaml
vendored
2
.github/workflows/npm-brew.yaml
vendored
@@ -6,7 +6,7 @@ on:
|
||||
workflow_dispatch:
|
||||
|
||||
release:
|
||||
types: [published]
|
||||
types: [released]
|
||||
|
||||
jobs:
|
||||
# NOTE: this job requires curl, jq and yarn
|
||||
|
||||
6
.github/workflows/scripts.yml
vendored
6
.github/workflows/scripts.yml
vendored
@@ -5,10 +5,14 @@ on:
|
||||
branches:
|
||||
- main
|
||||
paths:
|
||||
- "installer.sh"
|
||||
- "**.sh"
|
||||
- "**.bats"
|
||||
pull_request:
|
||||
branches:
|
||||
- main
|
||||
paths:
|
||||
- "**.sh"
|
||||
- "**.bats"
|
||||
|
||||
jobs:
|
||||
test:
|
||||
|
||||
@@ -2,3 +2,16 @@ printWidth: 120
|
||||
semi: false
|
||||
trailingComma: all
|
||||
arrowParens: always
|
||||
singleQuote: false
|
||||
useTabs: false
|
||||
|
||||
overrides:
|
||||
# Attempt to keep VScode's existing code style intact.
|
||||
- files: "vendor/modules/code-oss-dev/**/*.ts"
|
||||
options:
|
||||
# No limit defined upstream.
|
||||
printWidth: 10000
|
||||
semi: true
|
||||
singleQuote: true
|
||||
useTabs: true
|
||||
arrowParens: avoid
|
||||
|
||||
200
CHANGELOG.md
200
CHANGELOG.md
@@ -1,175 +1,131 @@
|
||||
# Changelog
|
||||
|
||||
<!--
|
||||
All notable changes to this project will be documented in this file.
|
||||
|
||||
This should be updated on every PR.
|
||||
The format is based on [Keep a Changelog](https://keepachangelog.com/en/1.0.0/),
|
||||
and this project adheres to [Semantic Versioning](https://semver.org/spec/v2.0.0.html).
|
||||
|
||||
We copy from here into the release notes.
|
||||
<!-- Example:
|
||||
|
||||
-->
|
||||
## [9.99.999] - 9090-09-09
|
||||
|
||||
<!--
|
||||
Add next version above previous version but below this line using the template
|
||||
VS Code v99.99.999
|
||||
|
||||
## Next Version
|
||||
|
||||
VS Code v0.00.0
|
||||
|
||||
### New Features
|
||||
|
||||
- item
|
||||
|
||||
### Bug Fixes
|
||||
|
||||
- fix(socket): did this thing #321 @githubuser
|
||||
|
||||
### Documentation
|
||||
|
||||
- item
|
||||
|
||||
### Development
|
||||
|
||||
- item
|
||||
### Changed
|
||||
### Added
|
||||
### Deprecated
|
||||
### Removed
|
||||
### Fixed
|
||||
### Security
|
||||
|
||||
-->
|
||||
|
||||
## Next Version
|
||||
## [Unreleased](https://github.com/cdr/code-server/releases)
|
||||
|
||||
VS Code v0.00.0
|
||||
VS Code v1.63.0
|
||||
|
||||
### New Features
|
||||
code-server has been rebased on upstream's newly open-sourced server
|
||||
implementation (#4414).
|
||||
|
||||
- item
|
||||
### Changed
|
||||
|
||||
### Bug Fixes
|
||||
- Web socket compression has been made the default (when supported). This means
|
||||
the `--enable` flag will no longer take `permessage-deflate` as an option.
|
||||
- Extra extension directories have been removed. The `--extra-extensions-dir`
|
||||
and `--extra-builtin-extensions-dir` will no longer be accepted.
|
||||
- The `--install-source` and `--locale` flags have been removed.
|
||||
- The static endpoint can no longer reach outside code-server. However the
|
||||
vscode-remote-resource endpoint still can.
|
||||
- OpenVSX has been made the default marketplace. However this means web
|
||||
extensions like Vim may be broken.
|
||||
|
||||
- Fix logout when using a base path (#3608)
|
||||
### Deprecated
|
||||
|
||||
### Documentation
|
||||
- `--link` is now deprecated (#4562).
|
||||
|
||||
- docs: add Pomerium #3424 @desimone
|
||||
- docs: fix confusing sentence in pull requests section #3460 @shiv-tyagi
|
||||
- docs: remove toc from changelog @oxy @jsjoeio
|
||||
- docs(MAINTAINING): add information about CHANGELOG #3467 @jsjoeio
|
||||
- docs: move release process to MAINTAINING.md #3441 @oxy @Prashant168
|
||||
- docs: format 'Caddy' from guide.md @PisecesPeng
|
||||
### Security
|
||||
|
||||
### Development
|
||||
- We fixed a XSS vulnerability by escaping HTML from messages in the error page (#4430).
|
||||
|
||||
- chore: cross-compile docker images with buildx #3166 @oxy
|
||||
- chore: update node to v14 #3458 @oxy
|
||||
- chore: update .gitignore #3557 @cuining
|
||||
- fix: use sufficient computational effort for password hash #3422 @jsjoeio
|
||||
- docs(CONTRIBUTING): add section on testing #3629 @jsjoeio
|
||||
## [3.12.0](https://github.com/cdr/code-server/releases/tag/v3.12.0) - 2021-09-15
|
||||
|
||||
### Development
|
||||
VS Code v1.60.0
|
||||
|
||||
- fix(publish): update cdrci fork in brew-bump.sh #3468 @jsjoeio
|
||||
- chore(dev): migrate away from parcel #3578 @jsjoeio
|
||||
### Changed
|
||||
|
||||
## 3.10.2
|
||||
- Upgrade VS Code to 1.60.0.
|
||||
|
||||
### Fixed
|
||||
|
||||
- Fix logout when using a base path (#3608).
|
||||
|
||||
## [3.11.1](https://github.com/cdr/code-server/releases/tag/v3.11.1) - 2021-08-06
|
||||
|
||||
Undocumented (see releases page).
|
||||
|
||||
## [3.11.0](https://github.com/cdr/code-server/releases/tag/v3.11.0) - 2021-06-14
|
||||
|
||||
Undocumented (see releases page).
|
||||
|
||||
## [3.10.2](https://github.com/cdr/code-server/releases/tag/v3.10.2) - 2021-05-21
|
||||
|
||||
VS Code v1.56.1
|
||||
|
||||
### New Features
|
||||
### Added
|
||||
|
||||
- feat: support `extraInitContainers` in helm chart values #3393 @strowk
|
||||
- feat: change `extraContainers` to support templating in helm chart #3393 @strowk
|
||||
- Support `extraInitContainers` in helm chart values (#3393).
|
||||
|
||||
### Bug Fixes
|
||||
### Changed
|
||||
|
||||
- fix: use correct command to Open Folder on Welcome page #3437 @jsjoeio
|
||||
- Change `extraContainers` to support templating in helm chart (#3393).
|
||||
|
||||
### Development
|
||||
### Fixed
|
||||
|
||||
- fix(ci): update brew-bump.sh to update remote first #3438 @jsjoeio
|
||||
- Fix "Open Folder" on welcome page (#3437).
|
||||
|
||||
## 3.10.1
|
||||
## [3.10.1](https://github.com/cdr/code-server/releases/tag/v3.10.1) - 2021-05-17
|
||||
|
||||
VS Code v1.56.1
|
||||
|
||||
### Bug Fixes
|
||||
### Fixed
|
||||
|
||||
- fix: Check the logged user instead of $USER #3330 @videlanicolas
|
||||
- fix: Fix broken node_modules.asar symlink in npm package #3355 @code-asher
|
||||
- fix: Update cloud agent to fix version issue #3342 @oxy
|
||||
- Check the logged user instead of $USER (#3330).
|
||||
- Fix broken node_modules.asar symlink in npm package (#3355).
|
||||
- Update cloud agent to fix version issue (#3342).
|
||||
|
||||
### Documentation
|
||||
### Changed
|
||||
|
||||
- docs(install): add raspberry pi section #3376 @jsjoeio
|
||||
- docs(maintaining): add pull requests section #3378 @jsjoeio
|
||||
- docs(maintaining): add merge strategies section #3379 @jsjoeio
|
||||
- refactor: move default PR template #3375 @jsjoeio
|
||||
- docs(contributing): add commits section #3377 @jsjoeio
|
||||
- Use xdgBasedir.runtime instead of tmp (#3304).
|
||||
|
||||
### Development
|
||||
|
||||
- chore: ignore updates to microsoft/playwright-github-action
|
||||
- fix(socket): use xdgBasedir.runtime instead of tmp #3304 @jsjoeio
|
||||
- fix(ci): re-enable trivy-scan-repo #3368 @jsjoeio
|
||||
|
||||
## 3.10.0
|
||||
## [3.10.0](https://github.com/cdr/code-server/releases/tag/v3.10.0) - 2021-05-10
|
||||
|
||||
VS Code v1.56.0
|
||||
|
||||
### New Features
|
||||
### Changed
|
||||
|
||||
- feat: minor connections refactor #3178 @code-asher
|
||||
- feat(security): add code-scanning with CodeQL #3229 @jsjoeio
|
||||
- feat(ci): add trivy job for security #3261 @jsjoeio
|
||||
- feat(vscode): update to version 1.56.0 #3269 @oxy
|
||||
- feat: use ptyHostService #3308 @code-asher
|
||||
- Update to VS Code 1.56.0 (#3269).
|
||||
- Minor connections refactor (#3178). Improves connection stability.
|
||||
- Use ptyHostService (#3308). This brings us closer to upstream VS Code.
|
||||
|
||||
### Bug Fixes
|
||||
### Added
|
||||
|
||||
- fix(socket): did this thing #321 @githubuser
|
||||
- fix(login): rate limiter shouldn't count successful logins #3141 @jsjoeio
|
||||
- chore(lib/vscode): update netmask #3187 @oxy
|
||||
- chore(deps): update dependencies with CVEs #3223 @oxy
|
||||
- fix: refactor logout #3277 @code-asher
|
||||
- fix: add flag for toggling permessage-deflate #3286 @code-asher
|
||||
- fix: make sure directories exist #3309 @code-asher
|
||||
- Add flag for toggling permessage-deflate (#3286). The default is off so
|
||||
compression will no longer be used by default. Use the --enable flag to
|
||||
toggle it back on.
|
||||
|
||||
### Documentation
|
||||
### Fixed
|
||||
|
||||
- docs(FAQ): add mention of sysbox #3087 @bpmct
|
||||
- docs: add security policy #3148 @jsjoeio
|
||||
- docs(guide.md): add `caddy` example for serving from sub-path #3217 @catthehacker
|
||||
- docs: revamp debugging section #3224 @code-asher
|
||||
- docs(readme): refactor to use codecov shield #3227 @jsjoeio
|
||||
- docs(maintaining): use milestones over boards #3228 @jsjoeio
|
||||
- docs(faq): add entry for accessing OSX folders #3247 @bpmct
|
||||
- docs(termux): add workaround for Android backspace issue #3251 @jsjoeio
|
||||
- docs(maintaining): add triage to workflow #3284 @jsjoeio
|
||||
- docs(security): add section for tools #3287 @jsjoeio
|
||||
- docs(maintaining): add versioning #3288 @jsjoeio
|
||||
- docs: add changelog #3337 @jsjoeio
|
||||
- Make rate limiter not count against successful logins (#3141).
|
||||
- Refactor logout (#3277). This fixes logging out in some scenarios.
|
||||
- Make sure directories exist (#3309). This fixes some errors on startup.
|
||||
|
||||
### Development
|
||||
### Security
|
||||
|
||||
- fix(update-vscode): add check/docs for git-subtree #3129 @oxy
|
||||
- refactor(testing): migrate to playwright-test from jest-playwright #3133 @jsjoeio
|
||||
- refactor(ci): remove unmaintained CI images and update release workflow #3147 @oxy
|
||||
- chore(ci): migrate from hub to gh #3168 @oxy
|
||||
- feat(testing): add e2e tests for code-server and terminal #3169 @jsjoeio
|
||||
- chore(ranger): fix syntax for extension-request #3172 @oxy
|
||||
- feat(testing): add codecov to generate test coverage reports #3194 @jsjoeio
|
||||
- feat(testing): add tests for registerServiceWorker #3200 @jsjoeio
|
||||
- refactor(testing): fix flaky terminal test #3230 @jsjoeio
|
||||
- chore: ignore 15.x @types/node updates #3244 @jsjoeio
|
||||
- chore(build): compile vscode+extensions in parallel #3250 @oxy
|
||||
- fix(deps): remove eslint-plugin-jest-playwright #3260 @jsjoeio
|
||||
- fix(testing): reduce flakiness of terminal.test.ts and use 1 worker for e2e tests #3263 @jsjoeio
|
||||
- feat(testing): add isConnected check #3271 @jsjoeio
|
||||
- feat(testing): add test for src/node/constants.ts #3290 @jsjoeio
|
||||
- feat: test static route #3297 @code-asher
|
||||
- refactor(ci): split audit from prebuild #3298 @oxy
|
||||
- chore(lib/vscode): cleanup/update build deps #3314 @oxy
|
||||
- fix(build): download correct cloud-agent for arch #3331 @oxy
|
||||
- fix: xmldom and underscore #3332 @oxy
|
||||
- Update dependencies with CVEs (#3223).
|
||||
|
||||
## Previous versions
|
||||
|
||||
This was added with `3.10.0`, which means any previous versions are not documented in the changelog.
|
||||
This was added with `3.10.0`, which means any previous versions are not
|
||||
documented in the changelog.
|
||||
|
||||
To see those, please visit the [Releases page](https://github.com/cdr/code-server/releases).
|
||||
|
||||
@@ -1,8 +0,0 @@
|
||||
{
|
||||
"rpm": {
|
||||
"armv7l": "armhfp"
|
||||
},
|
||||
"deb": {
|
||||
"armv7l": "armhf"
|
||||
}
|
||||
}
|
||||
@@ -15,23 +15,21 @@ main() {
|
||||
chmod +x out/node/entry.js
|
||||
fi
|
||||
|
||||
# for arch; we do not use OS from lib.sh and get our own.
|
||||
# lib.sh normalizes macos to darwin - but cloud-agent's binaries do not
|
||||
source ./ci/lib.sh
|
||||
OS="$(uname | tr '[:upper:]' '[:lower:]')"
|
||||
|
||||
mkdir -p ./lib
|
||||
|
||||
if ! [ -f ./lib/coder-cloud-agent ]; then
|
||||
echo "Downloading the cloud agent..."
|
||||
|
||||
# for arch; we do not use OS from lib.sh and get our own.
|
||||
# lib.sh normalizes macos to darwin - but cloud-agent's binaries do not
|
||||
source ./ci/lib.sh
|
||||
OS="$(uname | tr '[:upper:]' '[:lower:]')"
|
||||
|
||||
set +e
|
||||
curl -fsSL "https://github.com/cdr/cloud-agent/releases/latest/download/cloud-agent-$OS-$ARCH" -o ./lib/coder-cloud-agent
|
||||
chmod +x ./lib/coder-cloud-agent
|
||||
set -e
|
||||
fi
|
||||
|
||||
yarn browserify out/browser/register.js -o out/browser/register.browserified.js
|
||||
yarn browserify out/browser/pages/login.js -o out/browser/pages/login.browserified.js
|
||||
yarn browserify out/browser/pages/vscode.js -o out/browser/pages/vscode.browserified.js
|
||||
}
|
||||
|
||||
main "$@"
|
||||
|
||||
28
ci/build/build-lib.sh
Executable file
28
ci/build/build-lib.sh
Executable file
@@ -0,0 +1,28 @@
|
||||
#!/usr/bin/env bash
|
||||
|
||||
# This is a library which contains functions used inside ci/build
|
||||
#
|
||||
# We separated it into it's own file so that we could easily unit test
|
||||
# these functions and helpers.
|
||||
|
||||
# On some CPU architectures (notably node/uname "armv7l", default on Raspberry Pis),
|
||||
# different package managers have different labels for the same CPU (deb=armhf, rpm=armhfp).
|
||||
# This function returns the overriden arch on platforms
|
||||
# with alternate labels, or the same arch otherwise.
|
||||
get_nfpm_arch() {
|
||||
local PKG_FORMAT="${1:-}"
|
||||
local ARCH="${2:-}"
|
||||
|
||||
case "$ARCH" in
|
||||
armv7l)
|
||||
if [ "$PKG_FORMAT" = "deb" ]; then
|
||||
echo armhf
|
||||
elif [ "$PKG_FORMAT" = "rpm" ]; then
|
||||
echo armhfp
|
||||
fi
|
||||
;;
|
||||
*)
|
||||
echo "$ARCH"
|
||||
;;
|
||||
esac
|
||||
}
|
||||
@@ -7,6 +7,7 @@ set -euo pipefail
|
||||
main() {
|
||||
cd "$(dirname "${0}")/../.."
|
||||
source ./ci/lib.sh
|
||||
source ./ci/build/build-lib.sh
|
||||
|
||||
# Allow us to override architecture
|
||||
# we use this for our Linux ARM64 cross compile builds
|
||||
@@ -43,30 +44,24 @@ release_gcp() {
|
||||
cp "./release-packages/$release_name.tar.gz" "./release-gcp/latest/$OS-$ARCH.tar.gz"
|
||||
}
|
||||
|
||||
# On some CPU architectures (notably node/uname "armv7l", default on Raspberry Pis),
|
||||
# different package managers have different labels for the same CPU (deb=armhf, rpm=armhfp).
|
||||
# This function parses arch-override.json and returns the overriden arch on platforms
|
||||
# with alternate labels, or the same arch otherwise.
|
||||
get_nfpm_arch() {
|
||||
if jq -re ".${PKG_FORMAT}.${ARCH}" ./ci/build/arch-override.json > /dev/null; then
|
||||
jq -re ".${PKG_FORMAT}.${ARCH}" ./ci/build/arch-override.json
|
||||
else
|
||||
echo "$ARCH"
|
||||
fi
|
||||
}
|
||||
|
||||
# Generates deb and rpm packages.
|
||||
release_nfpm() {
|
||||
local nfpm_config
|
||||
|
||||
export NFPM_ARCH
|
||||
|
||||
PKG_FORMAT="deb"
|
||||
NFPM_ARCH="$(get_nfpm_arch)"
|
||||
NFPM_ARCH="$(get_nfpm_arch $PKG_FORMAT "$ARCH")"
|
||||
nfpm_config="$(envsubst < ./ci/build/nfpm.yaml)"
|
||||
echo "Building deb"
|
||||
echo "$nfpm_config" | head --lines=4
|
||||
nfpm pkg -f <(echo "$nfpm_config") --target "release-packages/code-server_${VERSION}_${NFPM_ARCH}.deb"
|
||||
|
||||
PKG_FORMAT="rpm"
|
||||
NFPM_ARCH="$(get_nfpm_arch)"
|
||||
NFPM_ARCH="$(get_nfpm_arch $PKG_FORMAT "$ARCH")"
|
||||
nfpm_config="$(envsubst < ./ci/build/nfpm.yaml)"
|
||||
echo "Building rpm"
|
||||
echo "$nfpm_config" | head --lines=4
|
||||
nfpm pkg -f <(echo "$nfpm_config") --target "release-packages/code-server-$VERSION-$NFPM_ARCH.rpm"
|
||||
}
|
||||
|
||||
|
||||
@@ -49,7 +49,7 @@ bundle_code_server() {
|
||||
{
|
||||
"commit": "$(git rev-parse HEAD)",
|
||||
"scripts": {
|
||||
"postinstall": "bash ./postinstall.sh"
|
||||
"postinstall": "sh ./postinstall.sh"
|
||||
}
|
||||
}
|
||||
EOF
|
||||
@@ -67,7 +67,7 @@ EOF
|
||||
bundle_vscode() {
|
||||
mkdir -p "$VSCODE_OUT_PATH"
|
||||
rsync "$VSCODE_SRC_PATH/yarn.lock" "$VSCODE_OUT_PATH"
|
||||
rsync "$VSCODE_SRC_PATH/out-vscode${MINIFY:+-min}/" "$VSCODE_OUT_PATH/out"
|
||||
rsync "$VSCODE_SRC_PATH/out-vscode-reh-web${MINIFY:+-min}/" "$VSCODE_OUT_PATH/out"
|
||||
|
||||
rsync "$VSCODE_SRC_PATH/.build/extensions/" "$VSCODE_OUT_PATH/extensions"
|
||||
if [ "$KEEP_MODULES" = 0 ]; then
|
||||
@@ -79,9 +79,8 @@ bundle_vscode() {
|
||||
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,web}
|
||||
rsync "$VSCODE_SRC_PATH/resources/linux/code.png" "$VSCODE_OUT_PATH/resources/linux/code.png"
|
||||
rsync "$VSCODE_SRC_PATH/resources/web/callback.html" "$VSCODE_OUT_PATH/resources/web/callback.html"
|
||||
mkdir -p "$VSCODE_OUT_PATH/resources/"
|
||||
rsync "$VSCODE_SRC_PATH/resources/" "$VSCODE_OUT_PATH/resources/"
|
||||
|
||||
# Add the commit and date and enable telemetry. This just makes telemetry
|
||||
# available; telemetry can still be disabled by flag or setting.
|
||||
@@ -89,7 +88,8 @@ bundle_vscode() {
|
||||
cat << EOF
|
||||
{
|
||||
"enableTelemetry": true,
|
||||
"commit": "$(git rev-parse HEAD)",
|
||||
"commit": "$(cd "$VSCODE_SRC_PATH" && git rev-parse HEAD)",
|
||||
"quality": "stable",
|
||||
"date": $(jq -n 'now | todate')
|
||||
}
|
||||
EOF
|
||||
|
||||
@@ -11,11 +11,8 @@ main() {
|
||||
|
||||
cd vendor/modules/code-oss-dev
|
||||
|
||||
yarn gulp compile-build compile-extensions-build compile-extension-media
|
||||
yarn gulp optimize --gulpfile ./coder.js
|
||||
if [[ $MINIFY ]]; then
|
||||
yarn gulp minify --gulpfile ./coder.js
|
||||
fi
|
||||
# Any platform works since we have our own packaging step (for now).
|
||||
yarn gulp "vscode-reh-web-linux-x64${MINIFY:+-min}"
|
||||
}
|
||||
|
||||
main "$@"
|
||||
|
||||
@@ -57,6 +57,9 @@ main() {
|
||||
esac
|
||||
|
||||
OS="$(uname | tr '[:upper:]' '[:lower:]')"
|
||||
|
||||
mkdir -p ./lib
|
||||
|
||||
if curl -fsSL "https://github.com/cdr/cloud-agent/releases/latest/download/cloud-agent-$OS-$ARCH" -o ./lib/coder-cloud-agent; then
|
||||
chmod +x ./lib/coder-cloud-agent
|
||||
else
|
||||
|
||||
@@ -4,24 +4,32 @@ set -euo pipefail
|
||||
main() {
|
||||
cd "$(dirname "$0")/../.."
|
||||
|
||||
echo 'Installing code-server test dependencies...'
|
||||
echo "Installing code-server test dependencies..."
|
||||
|
||||
local args=(install)
|
||||
if [[ ${CI-} ]]; then
|
||||
args+=(--frozen-lockfile)
|
||||
fi
|
||||
|
||||
cd test
|
||||
yarn install
|
||||
yarn "${args[@]}"
|
||||
cd ..
|
||||
|
||||
cd vendor
|
||||
echo 'Installing vendor dependencies...'
|
||||
echo "Installing vendor dependencies..."
|
||||
|
||||
# * We install in 'modules' instead of 'node_modules' because VS Code's extensions
|
||||
# use a webpack config which cannot differentiate between its own node_modules
|
||||
# and itself being in a directory with the same name.
|
||||
#
|
||||
# * We ignore scripts because NPM/Yarn's default behavior is to assume that
|
||||
# We install in 'modules' instead of 'node_modules' because VS Code's
|
||||
# extensions use a webpack config which cannot differentiate between its own
|
||||
# node_modules and itself being in a directory with the same name.
|
||||
args+=(--modules-folder modules)
|
||||
|
||||
# We ignore scripts because NPM/Yarn's default behavior is to assume that
|
||||
# devDependencies are not needed, and that even git repo based packages are
|
||||
# assumed to be compiled. Because the default behavior for VS Code's `postinstall`
|
||||
# assumes we're also compiled, this needs to be ignored.
|
||||
yarn install --modules-folder modules --ignore-scripts --frozen-lockfile
|
||||
# assumed to be compiled. Because the default behavior for VS Code's
|
||||
# `postinstall` assumes we're also compiled, this needs to be ignored.
|
||||
args+=(--ignore-scripts)
|
||||
|
||||
yarn "${args[@]}"
|
||||
|
||||
# Finally, run the vendor `postinstall`
|
||||
yarn run postinstall
|
||||
|
||||
@@ -1,6 +1,13 @@
|
||||
#!/usr/bin/env bash
|
||||
set -euo pipefail
|
||||
|
||||
help() {
|
||||
echo >&2 " You can build with 'yarn watch' or you can build a release"
|
||||
echo >&2 " For example: 'yarn build && yarn build:vscode && KEEP_MODULES=1 yarn release'"
|
||||
echo >&2 " Then 'CODE_SERVER_TEST_ENTRY=./release yarn test:e2e'"
|
||||
echo >&2 " You can manually run that release with 'node ./release'"
|
||||
}
|
||||
|
||||
main() {
|
||||
cd "$(dirname "$0")/../.."
|
||||
|
||||
@@ -21,13 +28,13 @@ main() {
|
||||
# wrong (native modules version issues, incomplete build, etc).
|
||||
if [[ ! -d $dir/out ]]; then
|
||||
echo >&2 "No code-server build detected"
|
||||
echo >&2 "You can build it with 'yarn build' or 'yarn watch'"
|
||||
help
|
||||
exit 1
|
||||
fi
|
||||
|
||||
if [[ ! -d $dir/vendor/modules/code-oss-dev/out ]]; then
|
||||
echo >&2 "No VS Code build detected"
|
||||
echo >&2 "You can build it with 'yarn build:vscode' or 'yarn watch'"
|
||||
help
|
||||
exit 1
|
||||
fi
|
||||
|
||||
|
||||
@@ -3,12 +3,26 @@ set -euo pipefail
|
||||
|
||||
main() {
|
||||
cd "$(dirname "$0")/../.."
|
||||
cd test/unit/node/test-plugin
|
||||
|
||||
source ./ci/lib.sh
|
||||
|
||||
pushd test/unit/node/test-plugin
|
||||
make -s out/index.js
|
||||
popd
|
||||
|
||||
# Our code imports from `out` in order to work during development but if you
|
||||
# have only built for production you will have not have this directory. In
|
||||
# that case symlink `out` to a production build directory.
|
||||
local vscode="vendor/modules/code-oss-dev"
|
||||
local link="$vscode/out"
|
||||
local target="out-build"
|
||||
if [[ ! -e $link ]] && [[ -d $vscode/$target ]]; then
|
||||
ln -s "$target" "$link"
|
||||
fi
|
||||
|
||||
# We must keep jest in a sub-directory. See ../../test/package.json for more
|
||||
# information. We must also run it from the root otherwise coverage will not
|
||||
# include our source files.
|
||||
cd "$OLDPWD"
|
||||
CS_DISABLE_PLUGINS=true ./test/node_modules/.bin/jest "$@"
|
||||
}
|
||||
|
||||
|
||||
291
ci/dev/watch.ts
291
ci/dev/watch.ts
@@ -1,157 +1,156 @@
|
||||
import browserify from "browserify"
|
||||
import * as cp from "child_process"
|
||||
import * as fs from "fs"
|
||||
import { spawn, fork, ChildProcess } from "child_process"
|
||||
import { promises as fs } from "fs"
|
||||
import * as path from "path"
|
||||
import { onLine } from "../../src/node/util"
|
||||
import { CompilationStats, onLine, OnLineCallback } from "../../src/node/util"
|
||||
|
||||
interface DevelopmentCompilers {
|
||||
[key: string]: ChildProcess | undefined
|
||||
vscode: ChildProcess
|
||||
vscodeWebExtensions: ChildProcess
|
||||
codeServer: ChildProcess
|
||||
plugins: ChildProcess | undefined
|
||||
}
|
||||
|
||||
class Watcher {
|
||||
private rootPath = path.resolve(process.cwd())
|
||||
private readonly paths = {
|
||||
/** Path to uncompiled VS Code source. */
|
||||
vscodeDir: path.join(this.rootPath, "vendor", "modules", "code-oss-dev"),
|
||||
compilationStatsFile: path.join(this.rootPath, "out", "watcher.json"),
|
||||
pluginDir: process.env.PLUGIN_DIR,
|
||||
}
|
||||
|
||||
//#region Web Server
|
||||
|
||||
/** Development web server. */
|
||||
private webServer: ChildProcess | undefined
|
||||
|
||||
private reloadWebServer = (): void => {
|
||||
if (this.webServer) {
|
||||
this.webServer.kill()
|
||||
}
|
||||
|
||||
// Pass CLI args, save for `node` and the initial script name.
|
||||
const args = process.argv.slice(2)
|
||||
this.webServer = fork(path.join(this.rootPath, "out/node/entry.js"), args)
|
||||
const { pid } = this.webServer
|
||||
|
||||
this.webServer.on("exit", () => console.log("[Code Server]", `Web process ${pid} exited`))
|
||||
|
||||
console.log("\n[Code Server]", `Spawned web server process ${pid}`)
|
||||
}
|
||||
|
||||
//#endregion
|
||||
|
||||
//#region Compilers
|
||||
|
||||
private readonly compilers: DevelopmentCompilers = {
|
||||
codeServer: spawn("tsc", ["--watch", "--pretty", "--preserveWatchOutput"], { cwd: this.rootPath }),
|
||||
vscode: spawn("yarn", ["watch"], { cwd: this.paths.vscodeDir }),
|
||||
vscodeWebExtensions: spawn("yarn", ["watch-web"], { cwd: this.paths.vscodeDir }),
|
||||
plugins: this.paths.pluginDir ? spawn("yarn", ["build", "--watch"], { cwd: this.paths.pluginDir }) : undefined,
|
||||
}
|
||||
|
||||
public async initialize(): Promise<void> {
|
||||
for (const event of ["SIGINT", "SIGTERM"]) {
|
||||
process.on(event, () => this.dispose(0))
|
||||
}
|
||||
|
||||
for (const [processName, devProcess] of Object.entries(this.compilers)) {
|
||||
if (!devProcess) continue
|
||||
|
||||
devProcess.on("exit", (code) => {
|
||||
console.log(`[${processName}]`, "Terminated unexpectedly")
|
||||
this.dispose(code)
|
||||
})
|
||||
|
||||
if (devProcess.stderr) {
|
||||
devProcess.stderr.on("data", (d: string | Uint8Array) => process.stderr.write(d))
|
||||
}
|
||||
}
|
||||
|
||||
onLine(this.compilers.vscode, this.parseVSCodeLine)
|
||||
onLine(this.compilers.codeServer, this.parseCodeServerLine)
|
||||
|
||||
if (this.compilers.plugins) {
|
||||
onLine(this.compilers.plugins, this.parsePluginLine)
|
||||
}
|
||||
}
|
||||
|
||||
//#endregion
|
||||
|
||||
//#region Line Parsers
|
||||
|
||||
private parseVSCodeLine: OnLineCallback = (strippedLine, originalLine) => {
|
||||
if (!strippedLine.length) return
|
||||
|
||||
console.log("[VS Code]", originalLine)
|
||||
|
||||
if (strippedLine.includes("Finished compilation with")) {
|
||||
console.log("[VS Code] ✨ Finished compiling! ✨", "(Refresh your web browser ♻️)")
|
||||
this.emitCompilationStats()
|
||||
this.reloadWebServer()
|
||||
}
|
||||
}
|
||||
|
||||
private parseCodeServerLine: OnLineCallback = (strippedLine, originalLine) => {
|
||||
if (!strippedLine.length) return
|
||||
|
||||
console.log("[Compiler][Code Server]", originalLine)
|
||||
|
||||
if (strippedLine.includes("Watching for file changes")) {
|
||||
console.log("[Compiler][Code Server]", "Finished compiling!", "(Refresh your web browser ♻️)")
|
||||
this.reloadWebServer()
|
||||
}
|
||||
}
|
||||
|
||||
private parsePluginLine: OnLineCallback = (strippedLine, originalLine) => {
|
||||
if (!strippedLine.length) return
|
||||
|
||||
console.log("[Compiler][Plugin]", originalLine)
|
||||
|
||||
if (strippedLine.includes("Watching for file changes...")) {
|
||||
this.reloadWebServer()
|
||||
}
|
||||
}
|
||||
|
||||
//#endregion
|
||||
|
||||
//#region Utilities
|
||||
|
||||
/**
|
||||
* Emits a file containing compilation data.
|
||||
* This is especially useful when Express needs to determine if VS Code is still compiling.
|
||||
*/
|
||||
private emitCompilationStats(): Promise<void> {
|
||||
const stats: CompilationStats = {
|
||||
lastCompiledAt: new Date(),
|
||||
}
|
||||
|
||||
console.log("Writing watcher stats...")
|
||||
return fs.writeFile(this.paths.compilationStatsFile, JSON.stringify(stats, null, 2))
|
||||
}
|
||||
|
||||
private dispose(code: number | null): void {
|
||||
for (const [processName, devProcess] of Object.entries(this.compilers)) {
|
||||
console.log(`[${processName}]`, "Killing...\n")
|
||||
devProcess?.removeAllListeners()
|
||||
devProcess?.kill()
|
||||
}
|
||||
process.exit(typeof code === "number" ? code : 0)
|
||||
}
|
||||
|
||||
//#endregion
|
||||
}
|
||||
|
||||
async function main(): Promise<void> {
|
||||
try {
|
||||
const watcher = new Watcher()
|
||||
await watcher.watch()
|
||||
} catch (error) {
|
||||
await watcher.initialize()
|
||||
} catch (error: any) {
|
||||
console.error(error.message)
|
||||
process.exit(1)
|
||||
}
|
||||
}
|
||||
|
||||
class Watcher {
|
||||
private readonly rootPath = path.resolve(__dirname, "../..")
|
||||
private readonly vscodeSourcePath = path.join(this.rootPath, "vendor/modules/code-oss-dev")
|
||||
|
||||
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 plugin = process.env.PLUGIN_DIR
|
||||
? cp.spawn("yarn", ["build", "--watch"], { cwd: process.env.PLUGIN_DIR })
|
||||
: undefined
|
||||
|
||||
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 (plugin) {
|
||||
Watcher.log("killing plugin")
|
||||
plugin.removeAllListeners()
|
||||
plugin.kill()
|
||||
}
|
||||
|
||||
if (server) {
|
||||
Watcher.log("killing server")
|
||||
server.removeAllListeners()
|
||||
server.kill()
|
||||
}
|
||||
|
||||
Watcher.log("killing watch")
|
||||
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)
|
||||
})
|
||||
if (plugin) {
|
||||
plugin.on("exit", (code) => {
|
||||
Watcher.log("plugin terminated unexpectedly")
|
||||
cleanup(code)
|
||||
})
|
||||
}
|
||||
|
||||
vscode.stderr.on("data", (d) => process.stderr.write(d))
|
||||
tsc.stderr.on("data", (d) => process.stderr.write(d))
|
||||
if (plugin) {
|
||||
plugin.stderr.on("data", (d) => process.stderr.write(d))
|
||||
}
|
||||
|
||||
const browserFiles = [
|
||||
path.join(this.rootPath, "out/browser/register.js"),
|
||||
path.join(this.rootPath, "out/browser/pages/login.js"),
|
||||
path.join(this.rootPath, "out/browser/pages/vscode.js"),
|
||||
]
|
||||
|
||||
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) {
|
||||
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")) {
|
||||
bundleBrowserCode(browserFiles)
|
||||
restartServer()
|
||||
}
|
||||
})
|
||||
|
||||
if (plugin) {
|
||||
onLine(plugin, (line, original) => {
|
||||
// tsc outputs blank lines; skip them.
|
||||
if (line !== "") {
|
||||
console.log("[plugin]", original)
|
||||
}
|
||||
if (line.includes("Watching for file changes")) {
|
||||
restartServer()
|
||||
}
|
||||
})
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
function bundleBrowserCode(inputFiles: string[]) {
|
||||
console.log(`[browser] bundling...`)
|
||||
inputFiles.forEach(async (path: string) => {
|
||||
const outputPath = path.replace(".js", ".browserified.js")
|
||||
browserify()
|
||||
.add(path)
|
||||
.bundle()
|
||||
.on("error", function (error: Error) {
|
||||
console.error(error.toString())
|
||||
})
|
||||
.pipe(fs.createWriteStream(outputPath))
|
||||
})
|
||||
console.log(`[browser] done bundling`)
|
||||
}
|
||||
|
||||
main()
|
||||
|
||||
@@ -15,9 +15,9 @@ type: application
|
||||
# This is the chart version. This version number should be incremented each time you make changes
|
||||
# to the chart and its templates, including the app version.
|
||||
# Versions are expected to follow Semantic Versioning (https://semver.org/)
|
||||
version: 1.0.3
|
||||
version: 1.0.5
|
||||
|
||||
# This is the version number of the application being deployed. This version number should be
|
||||
# incremented each time you make changes to the application. Versions are not expected to
|
||||
# follow Semantic Versioning. They should reflect the version the application is using.
|
||||
appVersion: 3.12.0
|
||||
appVersion: 4.0.0
|
||||
|
||||
@@ -15,9 +15,8 @@
|
||||
export SERVICE_IP=$(kubectl get svc --namespace {{ .Release.Namespace }} {{ include "code-server.fullname" . }} -o jsonpath='{.status.loadBalancer.ingress[0].ip}')
|
||||
echo http://$SERVICE_IP:{{ .Values.service.port }}
|
||||
{{- else if contains "ClusterIP" .Values.service.type }}
|
||||
export POD_NAME=$(kubectl get pods --namespace {{ .Release.Namespace }} -l "app.kubernetes.io/name={{ include "code-server.name" . }},app.kubernetes.io/instance={{ .Release.Name }}" -o jsonpath="{.items[0].metadata.name}")
|
||||
echo "Visit http://127.0.0.1:8080 to use your application"
|
||||
kubectl port-forward $POD_NAME 8080:80
|
||||
kubectl port-forward --namespace {{ .Release.Namespace }} service/{{ include "code-server.fullname" . }} 8080:http
|
||||
{{- end }}
|
||||
|
||||
Administrator credentials:
|
||||
|
||||
@@ -142,6 +142,12 @@ spec:
|
||||
secretName: {{ .secretName }}
|
||||
defaultMode: {{ .defaultMode }}
|
||||
{{- end }}
|
||||
{{- range .Values.extraConfigmapMounts }}
|
||||
- name: {{ .name }}
|
||||
configMap:
|
||||
name: {{ .configMap }}
|
||||
defaultMode: {{ .defaultMode }}
|
||||
{{- end }}
|
||||
{{- range .Values.extraVolumeMounts }}
|
||||
- name: {{ .name }}
|
||||
{{- if .existingClaim }}
|
||||
|
||||
@@ -1,7 +1,9 @@
|
||||
{{- if .Values.ingress.enabled -}}
|
||||
{{- $fullName := include "code-server.fullname" . -}}
|
||||
{{- $svcPort := .Values.service.port -}}
|
||||
{{- if semverCompare ">=1.14-0" .Capabilities.KubeVersion.GitVersion -}}
|
||||
{{- if semverCompare ">=1.19-0" $.Capabilities.KubeVersion.GitVersion -}}
|
||||
apiVersion: networking.k8s.io/v1
|
||||
{{- else if semverCompare ">=1.14-0" .Capabilities.KubeVersion.GitVersion -}}
|
||||
apiVersion: networking.k8s.io/v1beta1
|
||||
{{- else -}}
|
||||
apiVersion: extensions/v1beta1
|
||||
@@ -27,6 +29,22 @@ spec:
|
||||
{{- end }}
|
||||
{{- end }}
|
||||
rules:
|
||||
{{- if semverCompare ">=1.19-0" $.Capabilities.KubeVersion.GitVersion -}}
|
||||
{{- range .Values.ingress.hosts }}
|
||||
- host: {{ .host | quote }}
|
||||
http:
|
||||
paths:
|
||||
{{- range .paths }}
|
||||
- path: {{ . }}
|
||||
pathType: Prefix
|
||||
backend:
|
||||
service:
|
||||
name: {{ $fullName }}
|
||||
port:
|
||||
number: {{ $svcPort }}
|
||||
{{- end }}
|
||||
{{- end }}
|
||||
{{- else -}}
|
||||
{{- range .Values.ingress.hosts }}
|
||||
- host: {{ .host | quote }}
|
||||
http:
|
||||
@@ -39,3 +57,4 @@ spec:
|
||||
{{- end }}
|
||||
{{- end }}
|
||||
{{- end }}
|
||||
{{- end }}
|
||||
@@ -6,7 +6,7 @@ replicaCount: 1
|
||||
|
||||
image:
|
||||
repository: codercom/code-server
|
||||
tag: '3.12.0'
|
||||
tag: '4.0.0'
|
||||
pullPolicy: Always
|
||||
|
||||
imagePullSecrets: []
|
||||
@@ -28,14 +28,6 @@ podAnnotations: {}
|
||||
podSecurityContext: {}
|
||||
# fsGroup: 2000
|
||||
|
||||
securityContext: {}
|
||||
# capabilities:
|
||||
# drop:
|
||||
# - ALL
|
||||
# readOnlyRootFilesystem: true
|
||||
# runAsNonRoot: true
|
||||
# runAsUser: 1000
|
||||
|
||||
service:
|
||||
type: ClusterIP
|
||||
port: 8080
|
||||
@@ -127,10 +119,6 @@ persistence:
|
||||
# existingClaim: ""
|
||||
# hostPath: /data
|
||||
|
||||
serviceAccount:
|
||||
create: true
|
||||
name:
|
||||
|
||||
## Enable an Specify container in extraContainers.
|
||||
## This is meant to allow adding code-server dependencies, like docker-dind.
|
||||
extraContainers: |
|
||||
|
||||
@@ -5,6 +5,21 @@ main() {
|
||||
cd "$(dirname "$0")/../.."
|
||||
# Only sourcing this so we get access to $VERSION
|
||||
source ./ci/lib.sh
|
||||
source ./ci/steps/steps-lib.sh
|
||||
|
||||
echo "Checking environment variables"
|
||||
|
||||
# We need VERSION to bump the brew formula
|
||||
if is_env_var_set "VERSION"; then
|
||||
echo "VERSION is not set"
|
||||
exit 1
|
||||
fi
|
||||
|
||||
# We need HOMEBREW_GITHUB_API_TOKEN to push up commits
|
||||
if is_env_var_set "HOMEBREW_GITHUB_API_TOKEN"; then
|
||||
echo "HOMEBREW_GITHUB_API_TOKEN is not set"
|
||||
exit 1
|
||||
fi
|
||||
|
||||
# NOTE: we need to make sure cdrci/homebrew-core
|
||||
# is up-to-date
|
||||
@@ -13,27 +28,65 @@ main() {
|
||||
echo "Cloning cdrci/homebrew-core"
|
||||
git clone https://github.com/cdrci/homebrew-core.git
|
||||
|
||||
echo "Changing into homebrew-core directory"
|
||||
cd homebrew-core && pwd
|
||||
# Make sure the git clone step is successful
|
||||
if directory_exists "homebrew-core"; then
|
||||
echo "git clone failed. Cannot find homebrew-core directory."
|
||||
ls -la
|
||||
exit 1
|
||||
fi
|
||||
|
||||
echo "Adding Homebrew/homebrew-core as $(upstream)"
|
||||
echo "Changing into homebrew-core directory"
|
||||
pushd homebrew-core && pwd
|
||||
|
||||
echo "Adding Homebrew/homebrew-core"
|
||||
git remote add upstream https://github.com/Homebrew/homebrew-core.git
|
||||
|
||||
# Make sure the git remote step is successful
|
||||
if ! git config remote.upstream.url > /dev/null; then
|
||||
echo "git remote add upstream failed."
|
||||
echo "Could not find upstream in list of remotes."
|
||||
git remote -v
|
||||
exit 1
|
||||
fi
|
||||
|
||||
# TODO@jsjoeio - can I somehow check that this succeeded?
|
||||
echo "Fetching upstream Homebrew/hombrew-core commits"
|
||||
git fetch upstream
|
||||
|
||||
# TODO@jsjoeio - can I somehow check that this succeeded?
|
||||
echo "Merging in latest Homebrew/homebrew-core changes"
|
||||
git merge upstream/master
|
||||
|
||||
echo "Pushing changes to cdrci/homebrew-core fork on GitHub"
|
||||
# Source: https://serverfault.com/a/912788
|
||||
# shellcheck disable=SC2016,SC2028
|
||||
echo '#!/bin/sh\nexec echo "$HOMEBREW_GITHUB_API_TOKEN"' > "$HOME"/.git-askpass.sh
|
||||
# Ensure it's executable since we just created it
|
||||
chmod +x "$HOME/.git-askpass.sh"
|
||||
|
||||
# GIT_ASKPASS lets us use the password when pushing without revealing it in the process list
|
||||
# See: https://serverfault.com/a/912788
|
||||
GIT_ASKPASS="$HOME/.git-askpass.sh" git push https://cdr-oss@github.com/cdr-oss/homebrew-core.git --all
|
||||
PATH_TO_GIT_ASKPASS="$HOME/git-askpass.sh"
|
||||
# Source: https://serverfault.com/a/912788
|
||||
# shellcheck disable=SC2016,SC2028
|
||||
echo 'echo $HOMEBREW_GITHUB_API_TOKEN' > "$PATH_TO_ASKPASS"
|
||||
|
||||
# Make sure the git-askpass.sh file creation is successful
|
||||
if file_exists "$PATH_TO_GIT_ASKPASS"; then
|
||||
echo "git-askpass.sh not found in $HOME."
|
||||
ls -la "$HOME"
|
||||
exit 1
|
||||
fi
|
||||
|
||||
# Ensure it's executable since we just created it
|
||||
chmod +x "$PATH_TO_GIT_ASKPASS"
|
||||
|
||||
# Make sure the git-askpass.sh file is executable
|
||||
if is_executable "$PATH_TO_GIT_ASKPASS"; then
|
||||
echo "$PATH_TO_GIT_ASKPASS is not executable."
|
||||
ls -la "$PATH_TO_GIT_ASKPASS"
|
||||
exit 1
|
||||
fi
|
||||
|
||||
# Export the variables so git sees them
|
||||
export HOMEBREW_GITHUB_API_TOKEN="$HOMEBREW_GITHUB_API_TOKEN"
|
||||
export GIT_ASKPASS="$PATH_TO_ASKPASS"
|
||||
git push https://cdr-oss@github.com/cdr-oss/homebrew-core.git --all
|
||||
|
||||
# Find the docs for bump-formula-pr here
|
||||
# https://github.com/Homebrew/brew/blob/master/Library/Homebrew/dev-cmd/bump-formula-pr.rb#L18
|
||||
@@ -48,8 +101,14 @@ main() {
|
||||
fi
|
||||
|
||||
# Clean up and remove homebrew-core
|
||||
cd ..
|
||||
popd
|
||||
rm -rf homebrew-core
|
||||
|
||||
# Make sure homebrew-core is removed
|
||||
if directory_exists "homebrew-core"; then
|
||||
echo "rm -rf homebrew-core failed."
|
||||
ls -la
|
||||
fi
|
||||
}
|
||||
|
||||
main "$@"
|
||||
|
||||
47
ci/steps/steps-lib.sh
Executable file
47
ci/steps/steps-lib.sh
Executable file
@@ -0,0 +1,47 @@
|
||||
#!/usr/bin/env bash
|
||||
|
||||
# This is a library which contains functions used inside ci/steps
|
||||
#
|
||||
# We separated it into it's own file so that we could easily unit test
|
||||
# these functions and helpers
|
||||
|
||||
# Checks whether and environment variable is set.
|
||||
# Source: https://stackoverflow.com/a/62210688/3015595
|
||||
is_env_var_set() {
|
||||
local name="${1:-}"
|
||||
if test -n "${!name:-}"; then
|
||||
return 0
|
||||
else
|
||||
return 1
|
||||
fi
|
||||
}
|
||||
|
||||
# Checks whether a directory exists.
|
||||
directory_exists() {
|
||||
local dir="${1:-}"
|
||||
if [[ -d "${dir:-}" ]]; then
|
||||
return 0
|
||||
else
|
||||
return 1
|
||||
fi
|
||||
}
|
||||
|
||||
# Checks whether a file exists.
|
||||
file_exists() {
|
||||
local file="${1:-}"
|
||||
if test -f "${file:-}"; then
|
||||
return 0
|
||||
else
|
||||
return 1
|
||||
fi
|
||||
}
|
||||
|
||||
# Checks whether a file is executable.
|
||||
is_executable() {
|
||||
local file="${1:-}"
|
||||
if [ -f "${file}" ] && [ -r "${file}" ] && [ -x "${file}" ]; then
|
||||
return 0
|
||||
else
|
||||
return 1
|
||||
fi
|
||||
}
|
||||
@@ -3,13 +3,16 @@
|
||||
# Contributing
|
||||
|
||||
- [Requirements](#requirements)
|
||||
- [Linux-specific requirements](#linux-specific-requirements)
|
||||
- [Creating pull requests](#creating-pull-requests)
|
||||
- [Commits and commit history](#commits-and-commit-history)
|
||||
- [Development workflow](#development-workflow)
|
||||
- [Updates to VS Code](#updates-to-vs-code)
|
||||
- [Build](#build)
|
||||
- [Test](#test)
|
||||
- [Help](#help)
|
||||
- [Test](#test)
|
||||
- [Unit tests](#unit-tests)
|
||||
- [Script tests](#script-tests)
|
||||
- [Integration tests](#integration-tests)
|
||||
- [End-to-end tests](#end-to-end-tests)
|
||||
- [Structure](#structure)
|
||||
@@ -32,7 +35,7 @@ Here is what is needed:
|
||||
- [`git-lfs`](https://git-lfs.github.com)
|
||||
- [`yarn`](https://classic.yarnpkg.com/en/)
|
||||
- Used to install JS packages and run scripts
|
||||
- [`nfpm`](https://classic.yarnpkg.com/en/)
|
||||
- [`nfpm`](https://nfpm.goreleaser.com/)
|
||||
- Used to build `.deb` and `.rpm` packages
|
||||
- [`jq`](https://stedolan.github.io/jq/)
|
||||
- Used to build code-server releases
|
||||
@@ -41,13 +44,21 @@ Here is what is needed:
|
||||
signature
|
||||
verification](https://docs.github.com/en/github/authenticating-to-github/managing-commit-signature-verification)
|
||||
or follow [this tutorial](https://joeprevite.com/verify-commits-on-github)
|
||||
- `build-essential` (Linux only - used by VS Code)
|
||||
- Get this by running `apt-get install -y build-essential`
|
||||
- `rsync` and `unzip`
|
||||
- Used for code-server releases
|
||||
- `bats`
|
||||
- Used to run script unit tests
|
||||
|
||||
### Linux-specific requirements
|
||||
|
||||
If you're developing code-server on Linux, make sure you have installed or install the following dependencies:
|
||||
|
||||
```shell
|
||||
sudo apt-get install build-essential g++ libx11-dev libxkbfile-dev libsecret-1-dev python-is-python3
|
||||
```
|
||||
|
||||
These are required by VS Code. See [their Wiki](https://github.com/microsoft/vscode/wiki/How-to-Contribute#prerequisites) for more information.
|
||||
|
||||
## Creating pull requests
|
||||
|
||||
Please create a [GitHub Issue](https://github.com/cdr/code-server/issues) that
|
||||
@@ -67,31 +78,37 @@ we'll guide you.
|
||||
|
||||
## Development workflow
|
||||
|
||||
```shell
|
||||
yarn
|
||||
yarn watch
|
||||
# Visit http://localhost:8080 once the build is completed.
|
||||
```
|
||||
The current development workflow is a bit tricky because we have this repo and we use our `cdr/vscode` fork inside it with [`yarn link`](https://classic.yarnpkg.com/lang/en/docs/cli/link/).
|
||||
|
||||
`yarn watch` will live reload changes to the source.
|
||||
Here are these steps you should follow to get your dev environment setup:
|
||||
|
||||
1. `git clone https://github.com/cdr/code-server.git` - Clone `code-server`
|
||||
2. `git clone https://github.com/cdr/vscode.git` - Clone `vscode`
|
||||
3. `cd vscode && yarn install` - install the dependencies in the `vscode` repo
|
||||
4. `cd code-server && yarn install` - install the dependencies in the `code-server` repo
|
||||
5. `cd vscode && yarn link` - use `yarn` to create a symlink to the `vscode` repo (`code-oss-dev` package)
|
||||
6. `cd code-server && yarn link code-oss-dev --modules-folder vendor/modules` - links your local `vscode` repo (`code-oss-dev` package) inside your local version of code-server
|
||||
7. `cd code-server && yarn watch` - this will spin up code-server on localhost:8080 which you can start developing. It will live reload changes to the source.
|
||||
|
||||
### Updates to VS Code
|
||||
|
||||
If changes are made and merged into `main` in the [`cdr/vscode`](https://github.com/cdr/vscode) repo, then you'll need to update the version in the `code-server` repo by following these steps:
|
||||
|
||||
1. Update the package tag listed in `vendor/package.json`:
|
||||
|
||||
```json
|
||||
{
|
||||
"devDependencies": {
|
||||
"vscode": "cdr/vscode#X.XX.X-code-server"
|
||||
"vscode": "cdr/vscode#<latest-commit-sha>"
|
||||
}
|
||||
}
|
||||
```
|
||||
|
||||
2. From the code-server **project root**, run `yarn install`.
|
||||
Then, test code-server locally to make sure everything works.
|
||||
1. Check the Node.js version that's used by Electron (which is shipped with VS
|
||||
3. Check the Node.js version that's used by Electron (which is shipped with VS
|
||||
Code. If necessary, update your version of Node.js to match.
|
||||
1. Open a PR
|
||||
4. Open a PR
|
||||
|
||||
> Watch for updates to
|
||||
> `vendor/modules/code-oss-dev/src/vs/code/browser/workbench/workbench.html`. You may need to
|
||||
@@ -129,13 +146,18 @@ yarn package
|
||||
> If you need your builds to support older distros, run the build commands
|
||||
> inside a Docker container with all the build requirements installed.
|
||||
|
||||
### Test
|
||||
### Help
|
||||
|
||||
There are three kinds of tests in code-server:
|
||||
If you get stuck or need help, you can always start a new GitHub Discussion [here](https://github.com/cdr/code-server/discussions). One of the maintainers will respond and help you out.
|
||||
|
||||
## Test
|
||||
|
||||
There are four kinds of tests in code-server:
|
||||
|
||||
1. Unit tests
|
||||
2. Integration tests
|
||||
3. End-to-end tests
|
||||
2. Script tests
|
||||
3. Integration tests
|
||||
4. End-to-end tests
|
||||
|
||||
### Unit tests
|
||||
|
||||
@@ -146,6 +168,14 @@ These live under [test/unit](../test/unit).
|
||||
|
||||
We use unit tests for functions and things that can be tested in isolation. The file structure is modeled closely after `/src` so it's easy for people to know where test files should live.
|
||||
|
||||
### Script tests
|
||||
|
||||
Our script tests are written in bash and run using [bats](https://github.com/bats-core/bats-core).
|
||||
|
||||
These tests live under `test/scripts`.
|
||||
|
||||
We use these to test anything related to our scripts (most of which live under `ci`).
|
||||
|
||||
### Integration tests
|
||||
|
||||
These are a work in progress. We build code-server and run a script called
|
||||
|
||||
63
docs/FAQ.md
63
docs/FAQ.md
@@ -26,7 +26,8 @@
|
||||
- [Can I use Docker in a code-server container?](#can-i-use-docker-in-a-code-server-container)
|
||||
- [How do I disable telemetry?](#how-do-i-disable-telemetry)
|
||||
- [What's the difference between code-server and Theia?](#whats-the-difference-between-code-server-and-theia)
|
||||
- [What's the difference between code-server and VS Code Codespaces?](#whats-the-difference-between-code-server-and-vs-code-codespaces)
|
||||
- [What's the difference between code-server and OpenVSCode-Server?](#whats-the-difference-between-code-server-and-openvscode-server)
|
||||
- [What's the difference between code-server and GitHub Codespaces?](#whats-the-difference-between-code-server-and-github-codespaces)
|
||||
- [Does code-server have any security login validation?](#does-code-server-have-any-security-login-validation)
|
||||
- [Are there community projects involving code-server?](#are-there-community-projects-involving-code-server)
|
||||
- [How do I change the port?](#how-do-i-change-the-port)
|
||||
@@ -100,8 +101,11 @@ Service](https://cdn.vsassets.io/v/M146_20190123.39/_content/Microsoft-Visual-St
|
||||
> Visual Studio Products and Services.
|
||||
|
||||
Because of this, we can't offer any extensions on Microsoft's marketplace.
|
||||
Instead, we've created a marketplace offering open-source extensions. The
|
||||
marketplace works by scraping GitHub for VS Code extensions and building them.
|
||||
Instead, we use the [Open-VSX extension gallery](https://open-vsx.org), which is also used by various other forks.
|
||||
It isn't perfect, but its getting better by the day with more and more extensions.
|
||||
|
||||
We also offer our own marketplace for open source extensions, but plan to
|
||||
deprecate it at a future date and completely migrate to Open-VSX.
|
||||
|
||||
These are the closed-source extensions that are presently unavailable:
|
||||
|
||||
@@ -117,15 +121,8 @@ For more about the closed source portions of VS Code, see [vscodium/vscodium](ht
|
||||
|
||||
## How can I request an extension that's missing from the marketplace?
|
||||
|
||||
We are in the process of transitioning to [Open VSX](https://open-vsx.org/).
|
||||
Once we've [implemented Open
|
||||
VSX](https://github.com/eclipse/openvsx/issues/249), we can finalize this
|
||||
transition. As such, we are not currently accepting new extension requests.
|
||||
|
||||
In the meantime, we suggest:
|
||||
|
||||
- [Switching to Open VSX](#how-do-i-configure-the-marketplace-url) now
|
||||
- Downloading and [installing the extension manually](#installing-an-extension-manually)
|
||||
To add an extension to Open-VSX, please see [open-vsx/publish-extensions](https://github.com/open-vsx/publish-extensions).
|
||||
We no longer plan to add new extensions to our legacy extension gallery.
|
||||
|
||||
## How do I install an extension?
|
||||
|
||||
@@ -158,20 +155,19 @@ You can also download extensions using the command line. For instance,
|
||||
downloading from OpenVSX can be done like this:
|
||||
|
||||
```shell
|
||||
SERVICE_URL=https://open-vsx.org/vscode/gallery ITEM_URL=https://open-vsx.org/vscode/item code-server --install-extension <extension id>
|
||||
code-server --install-extension <extension id>
|
||||
```
|
||||
|
||||
## How do I use my own extensions marketplace?
|
||||
|
||||
If you own a marketplace that implements the VS Code Extension Gallery API, you
|
||||
can point code-server to it by setting `$SERVICE_URL` and `$ITEM_URL`. These correspond directly
|
||||
to `serviceUrl` and `itemUrl` in VS Code's `product.json`.
|
||||
can point code-server to it by setting `$EXTENSIONS_GALLERY`.
|
||||
This corresponds directly with the `extensionsGallery` entry in in VS Code's `product.json`.
|
||||
|
||||
For example, to use [open-vsx.org](https://open-vsx.org), run:
|
||||
For example, to use the legacy Coder extensions marketplace:
|
||||
|
||||
```bash
|
||||
export SERVICE_URL=https://open-vsx.org/vscode/gallery
|
||||
export ITEM_URL=https://open-vsx.org/vscode/item
|
||||
export EXTENSIONS_GALLERY='{"serviceUrl": "https://extensions.coder.com/api"}'
|
||||
```
|
||||
|
||||
Though you can technically use Microsoft's marketplace in this manner, we
|
||||
@@ -377,18 +373,31 @@ for extensions.
|
||||
|
||||
Theia doesn't allow you to reuse your existing VS Code config.
|
||||
|
||||
## What's the difference between code-server and VS Code Codespaces?
|
||||
## What's the difference between code-server and OpenVSCode-Server?
|
||||
|
||||
Both code-server and VS Code Codespaces allow you to access VS Code via a
|
||||
browser.
|
||||
code-server and OpenVSCode-Server both allow you to access VS Code via a
|
||||
browser. The two projects also use their own [forks of VS Code](https://github.com/cdr/vscode) to
|
||||
leverage modern VS Code APIs and stay up to date with the upsteam version.
|
||||
|
||||
VS Code Codespaces, however, is a closed-source, paid service offered by
|
||||
Microsoft. While you can self-host environments with VS Code Codespaces, you
|
||||
still need an Azure billing account, and you must access VS Code via the
|
||||
Codespaces web dashboard instead of connecting directly to it.
|
||||
However, OpenVSCode-Server is scoped at only making VS Code available in the web browser.
|
||||
code-server includes some other features:
|
||||
|
||||
On the other hand, code-server is free, open-source, and can be run on any
|
||||
machine with few limitations.
|
||||
- password auth
|
||||
- proxy web ports
|
||||
- certificate support
|
||||
- plugin API
|
||||
- settings sync (coming soon)
|
||||
|
||||
For more details, see [this discussion post](https://github.com/cdr/code-server/discussions/4267#discussioncomment-1411583).
|
||||
|
||||
## What's the difference between code-server and GitHub Codespaces?
|
||||
|
||||
Both code-server and GitHub Codespaces allow you to access VS Code via a
|
||||
browser. GitHub Codespaces, however, is a closed-source, paid service offered by
|
||||
GitHub and Microsoft.
|
||||
|
||||
On the other hand, code-server is self-hosted, free, open-source, and
|
||||
can be run on any machine with few limitations.
|
||||
|
||||
## Does code-server have any security login validation?
|
||||
|
||||
|
||||
@@ -2,6 +2,9 @@
|
||||
<!-- DON'T EDIT THIS SECTION, INSTEAD RE-RUN doctoc TO UPDATE -->
|
||||
# Maintaining
|
||||
|
||||
- [Team](#team)
|
||||
- [Onboarding](#onboarding)
|
||||
- [Offboarding](#offboarding)
|
||||
- [Workflow](#workflow)
|
||||
- [Milestones](#milestones)
|
||||
- [Triage](#triage)
|
||||
@@ -12,19 +15,45 @@
|
||||
- [Changelog](#changelog)
|
||||
- [Releases](#releases)
|
||||
- [Publishing a release](#publishing-a-release)
|
||||
- [AUR](#aur)
|
||||
- [Docker](#docker)
|
||||
- [Homebrew](#homebrew)
|
||||
- [npm](#npm)
|
||||
- [Syncing with Upstream VS Code](#syncing-with-upstream-vs-code)
|
||||
- [Testing](#testing)
|
||||
- [Documentation](#documentation)
|
||||
- [Troubleshooting](#troubleshooting)
|
||||
|
||||
<!-- END doctoc generated TOC please keep comment here to allow auto update -->
|
||||
|
||||
This document is meant to serve current and future maintainers of code-server,
|
||||
as well as share our workflow for maintaining the project.
|
||||
|
||||
## Team
|
||||
|
||||
Current maintainers:
|
||||
|
||||
- @code-asher
|
||||
- @oxy
|
||||
- @TeffenEllis
|
||||
- @jsjoeio
|
||||
|
||||
This document is meant to serve current and future maintainers of code-server,
|
||||
as well as share our workflow for maintaining the project.
|
||||
Occassionally, other Coder employees may step in time to time to assist with code-server.
|
||||
|
||||
### Onboarding
|
||||
|
||||
To onboard a new maintainer to the project, please make sure to do the following:
|
||||
|
||||
- [ ] Add to [cdr/code-server-reviewers](https://github.com/orgs/cdr/teams/code-server-reviewers)
|
||||
- [ ] Add as Admin under [Repository Settings > Access](https://github.com/cdr/code-server/settings/access)
|
||||
- [ ] Add to [npm Coder org](https://www.npmjs.com/org/coder)
|
||||
- [ ] Add as [AUR maintainer](https://aur.archlinux.org/packages/code-server/) (talk to Colin)
|
||||
- [ ] Introduce to community via Discussion (see [example](https://github.com/cdr/code-server/discussions/3955))
|
||||
|
||||
### Offboarding
|
||||
|
||||
Very similar to Onboarding but Remove maintainer from all teams and revoke access. Please also do the following:
|
||||
|
||||
- [ ] Write farewell post via Discussion (see [example](https://github.com/cdr/code-server/discussions/3933))
|
||||
|
||||
## Workflow
|
||||
|
||||
@@ -98,8 +127,7 @@ the issue.
|
||||
|
||||
### Merge strategies
|
||||
|
||||
For most things, we recommend the **squash and merge** strategy. If you're
|
||||
updating `lib/vscode`, we suggest using the **rebase and merge** strategy. There
|
||||
For most things, we recommend the **squash and merge** strategy. There
|
||||
may be times where **creating a merge commit** makes sense as well. Use your
|
||||
best judgment. If you're unsure, you can always discuss in the PR with the team.
|
||||
|
||||
@@ -137,6 +165,7 @@ If you're the current release manager, follow these steps:
|
||||
|
||||
### Publishing a release
|
||||
|
||||
1. Create a release branch called `v0.0.0` but replace with new version
|
||||
1. Run `yarn release:prep` and type in the new version (e.g., `3.8.1`)
|
||||
1. GitHub Actions will generate the `npm-package`, `release-packages` and
|
||||
`release-images` artifacts. You do not have to wait for this step to complete
|
||||
@@ -159,6 +188,59 @@ If you're the current release manager, follow these steps:
|
||||
[cdr/code-server-aur](https://github.com/cdr/code-server-aur).
|
||||
1. Wait for the npm package to be published.
|
||||
|
||||
#### AUR
|
||||
|
||||
We publish to AUR as a package [here](https://aur.archlinux.org/packages/code-server/). This process is manual and can be done by following the steps in [this repo](https://github.com/cdr/code-server-aur).
|
||||
|
||||
#### Docker
|
||||
|
||||
We publish code-server as a Docker image [here](https://registry.hub.docker.com/r/codercom/code-server), tagging it both with the version and latest.
|
||||
|
||||
This is currently automated with the release process.
|
||||
|
||||
#### Homebrew
|
||||
|
||||
We publish code-server on Homebrew [here](https://github.com/Homebrew/homebrew-core/blob/master/Formula/code-server.rb).
|
||||
|
||||
This is currently automated with the release process (but may fail occassionally). If it does, run this locally:
|
||||
|
||||
```shell
|
||||
# Replace VERSION with version
|
||||
brew bump-formula-pr --version="${VERSION}" code-server --no-browse --no-audit
|
||||
```
|
||||
|
||||
#### npm
|
||||
|
||||
We publish code-server as a npm package [here](https://www.npmjs.com/package/code-server/v/latest).
|
||||
|
||||
This is currently automated with the release process.
|
||||
|
||||
## Syncing with Upstream VS Code
|
||||
|
||||
The VS Code portion of code-server lives under [`cdr/vscode`](https://github.com/cdr/vscode). To update VS Code for code-server, follow these steps:
|
||||
|
||||
1. `git checkout -b vscode-update` - Create a new branch locally based off `main`
|
||||
2. `git fetch upstream` - Fetch upstream (VS Code)'s latest `main` branch
|
||||
3. `git merge upstream/main` - Merge it locally
|
||||
1. If there are merge conflicts, fix them locally
|
||||
4. Open a PR merging your branch (`vscode-update`) into `main` and add the code-server review team
|
||||
|
||||
Ideally, our fork stays as close to upstream as possible. See the differences between our fork and upstream [here](https://github.com/microsoft/vscode/compare/main...cdr:main).
|
||||
|
||||
## Testing
|
||||
|
||||
Our testing structure is laid out under our [Contributing docs](https://coder.com/docs/code-server/latest/CONTRIBUTING#test).
|
||||
|
||||
We hope to eventually hit 100% test converage with our unit tests, and maybe one day our scripts (coverage not tracked currently).
|
||||
|
||||
If you're ever looking to add more tests, here are a few ways to get started:
|
||||
|
||||
- run `yarn test:unit` and look at the coverage chart. You'll see all the uncovered lines. This is a good place to start.
|
||||
- look at `test/scripts` to see which scripts are tested. We can always use more tests there.
|
||||
- look at `test/e2e`. We can always use more end-to-end tests.
|
||||
|
||||
Otherwise, talk to a current maintainer and ask which part of the codebase is lacking most when it comes to tests.
|
||||
|
||||
## Documentation
|
||||
|
||||
### Troubleshooting
|
||||
|
||||
@@ -1,6 +1,6 @@
|
||||
# code-server
|
||||
|
||||
[](https://github.com/cdr/code-server/discussions) [](https://cdr.co/join-community) [](https://twitter.com/coderhq) [](https://codecov.io/gh/cdr/code-server) [](https://github.com/cdr/code-server/tree/v3.12.0/docs)
|
||||
[](https://github.com/cdr/code-server/discussions) [](https://cdr.co/join-community) [](https://twitter.com/coderhq) [](https://codecov.io/gh/cdr/code-server) [](https://github.com/cdr/code-server/tree/v4.0.0/docs)
|
||||
|
||||
Run [VS Code](https://github.com/Microsoft/vscode) on any machine anywhere and
|
||||
access it in the browser.
|
||||
@@ -14,6 +14,9 @@ access it in the browser.
|
||||
- Preserve battery life when you're on the go; all intensive tasks run on your
|
||||
server
|
||||
|
||||
| 🔔 code-server is a free browser-based IDE while [Coder](https://coder.com/), is our enterprise developer workspace platform. For more information, visit [Coder.com](https://coder.com/docs/comparison)
|
||||
| ---
|
||||
|
||||
## Requirements
|
||||
|
||||
See [requirements](requirements.md) for minimum specs, as well as instructions
|
||||
@@ -53,10 +56,6 @@ code-server.
|
||||
We also have an in-depth [setup and
|
||||
configuration](https://coder.com/docs/code-server/latest/guide) guide.
|
||||
|
||||
## TLS and authentication (beta)
|
||||
|
||||
To add TLS and authentication out of the box, use [code-server --link](https://coder.com/docs/code-server/latest/link).
|
||||
|
||||
## Questions?
|
||||
|
||||
See answers to [frequently asked
|
||||
|
||||
@@ -30,15 +30,19 @@ SERVICE_URL=https://open-vsx.org/vscode/gallery \
|
||||
|
||||
As `code-server` is based on VS Code, you can follow the steps described on Duckly's [Pair programming with VS Code](https://duckly.com/tools/vscode) page and skip the installation step.
|
||||
|
||||
## Code sharing with CodeTogether
|
||||
## Code sharing using CodeTogether
|
||||
|
||||
[CodeTogether](https://www.codetogether.com/) is another service with cross-platform live collaborative features:
|
||||
[CodeTogether](https://www.codetogether.com/) is a real-time cross-IDE replacement for Microsoft Live Share providing:
|
||||
|
||||
- Sharing ports,
|
||||
- Sharing, read/write terminals,
|
||||
- Joining via web browser or another IDE.
|
||||
|
||||
However, some of these are [paid options](https://www.codetogether.com/pricing/).
|
||||
- Cross-IDE support - between VS Code, Eclipse, IntelliJ and IDEs based on them (browser or desktop)
|
||||
- Real-time editing - shared or individual cursors for pairing, mobbing, swarming, or whatever
|
||||
- P2P encrypted - servers can't decrypt the traffic ([Security Details](https://codetogether.com/download/security/))
|
||||
- SaaS or [On-premises](https://codetogether.com/on-premises/) options
|
||||
- Shared servers, terminals, and consoles
|
||||
- Unit Testing - with support for Red, Green, Refactor TDD
|
||||
- Joining via a web browser or your preferred IDE
|
||||
- Free unlimited 1 hour sessions with 4 participants
|
||||
- Multiple plans including [free or paid options](https://www.codetogether.com/pricing/)
|
||||
|
||||
### Installing the CodeTogether extension
|
||||
|
||||
@@ -56,6 +60,6 @@ However, some of these are [paid options](https://www.codetogether.com/pricing/)
|
||||
code-server --enable-proposed-api genuitecllc.codetogether
|
||||
```
|
||||
|
||||
Another option would be to add a value in code-server's [config file](https://coder.com/docs/code-server/v3.12.0/FAQ#how-does-the-config-file-work).
|
||||
Another option would be to add a value in code-server's [config file](https://coder.com/docs/code-server/v4.0.0/FAQ#how-does-the-config-file-work).
|
||||
|
||||
3. Refresh code-server and navigate to the CodeTogether icon in the sidebar to host or join a coding session.
|
||||
|
||||
401
docs/guide.md
401
docs/guide.md
@@ -1,396 +1,5 @@
|
||||
<!-- 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
|
||||
|
||||
- [Expose code-server](#expose-code-server)
|
||||
- [Port forwarding via SSH](#port-forwarding-via-ssh)
|
||||
- [Using Let's Encrypt with Caddy](#using-lets-encrypt-with-caddy)
|
||||
- [Using Let's Encrypt with NGINX](#using-lets-encrypt-with-nginx)
|
||||
- [Using a self-signed certificate](#using-a-self-signed-certificate)
|
||||
- [External authentication](#external-authentication)
|
||||
- [HTTPS and self-signed certificates](#https-and-self-signed-certificates)
|
||||
- [Accessing web services](#accessing-web-services)
|
||||
- [Using a subdomain](#using-a-subdomain)
|
||||
- [Using a subpath](#using-a-subpath)
|
||||
- [Stripping `/proxy/<port>` from the request path](#stripping-proxyport-from-the-request-path)
|
||||
- [Proxying to create a React app](#proxying-to-create-a-react-app)
|
||||
- [Proxying to a Vue app](#proxying-to-a-vue-app)
|
||||
- [Setup Guide](#setup-guide)
|
||||
- [Expose code-server](#expose-code-server-1)
|
||||
- [Port forwarding via SSH](#port-forwarding-via-ssh-1)
|
||||
- [Using Let's Encrypt with Caddy](#using-lets-encrypt-with-caddy-1)
|
||||
- [Using Let's Encrypt with NGINX](#using-lets-encrypt-with-nginx-1)
|
||||
- [Using a self-signed certificate](#using-a-self-signed-certificate-1)
|
||||
- [External authentication](#external-authentication-1)
|
||||
- [HTTPS and self-signed certificates](#https-and-self-signed-certificates-1)
|
||||
- [Accessing web services](#accessing-web-services-1)
|
||||
- [Using a subdomain](#using-a-subdomain-1)
|
||||
- [Using a subpath](#using-a-subpath-1)
|
||||
- [Stripping `/proxy/<port>` from the request path](#stripping-proxyport-from-the-request-path-1)
|
||||
- [Proxying to create a React app](#proxying-to-create-a-react-app-1)
|
||||
- [Proxying to a Vue app](#proxying-to-a-vue-app-1)
|
||||
- [SSH into code-server on VS Code](#ssh-into-code-server-on-vs-code)
|
||||
- [Option 1: cloudflared tunnel](#option-1-cloudflared-tunnel)
|
||||
- [Option 2: ngrok tunnel](#option-2-ngrok-tunnel)
|
||||
|
||||
<!-- END doctoc generated TOC please keep comment here to allow auto update -->
|
||||
|
||||
This article will walk you through exposing code-server securely once you've
|
||||
completed the [installation process](install.md).
|
||||
|
||||
## Expose code-server
|
||||
|
||||
**Never** expose code-server directly to the internet without some form of
|
||||
authentication and encryption, otherwise someone can take over your machine via
|
||||
the terminal.
|
||||
|
||||
By default, code-server uses password authentication. As such, you must copy the
|
||||
password from code-server's config file to log in. To avoid exposing itself
|
||||
unnecessarily, code-server listens on `localhost`; this practice is fine for
|
||||
testing, but it doesn't work if you want to access code-server from a different
|
||||
machine.
|
||||
|
||||
> **Rate limits:** code-server rate limits password authentication attempts to
|
||||
> two per minute plus an additional twelve per hour.
|
||||
|
||||
There are several approaches to operating and exposing code-server securely:
|
||||
|
||||
- Port forwarding via SSH
|
||||
- Using Let's Encrypt with Caddy
|
||||
- Using Let's Encrypt with NGINX
|
||||
- Using a self-signed certificate
|
||||
|
||||
### Port forwarding via SSH
|
||||
|
||||
We highly recommend using [port forwarding via
|
||||
SSH](https://help.ubuntu.com/community/SSH/OpenSSH/PortForwarding) to access
|
||||
code-server. If you have an SSH server on your remote machine, this approach
|
||||
doesn't required additional setup.
|
||||
|
||||
The downside to SSH forwarding, however, is that you can't access code-server
|
||||
when using machines without SSH clients (such as iPads). If this applies to you,
|
||||
we recommend using another method, such as [Let's Encrypt](#let-encrypt) instead.
|
||||
|
||||
> To work properly, your environment should have WebSockets enabled, which
|
||||
> code-server uses to communicate between the browser and server.
|
||||
|
||||
1. SSH into your instance and edit the code-server config file to disable
|
||||
password authentication:
|
||||
|
||||
```console
|
||||
# Replaces "auth: password" with "auth: none" in the code-server config.
|
||||
sed -i.bak 's/auth: password/auth: none/' ~/.config/code-server/config.yaml
|
||||
```
|
||||
|
||||
2. Restart code-server:
|
||||
|
||||
```console
|
||||
sudo systemctl restart code-server@$USER
|
||||
```
|
||||
|
||||
3. Forward local port `8080` to `127.0.0.1:8080` on the remote instance by running the following command on your local machine:
|
||||
|
||||
```console
|
||||
# -N disables executing a remote shell
|
||||
ssh -N -L 8080:127.0.0.1:8080 [user]@<instance-ip>
|
||||
```
|
||||
|
||||
4. At this point, you can access code-server by pointing your web browser to `http://127.0.0.1:8080`.
|
||||
|
||||
5. If you'd like to make the port forwarding via SSH persistent, we recommend
|
||||
using [mutagen](https://mutagen.io/documentation/introduction/installation)
|
||||
to do so. Once you've installed mutagen, you can port forward as follows:
|
||||
|
||||
```console
|
||||
# This is the same as the above SSH command, but it runs in the background
|
||||
# continuously. Be sure to 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
|
||||
```
|
||||
|
||||
6. Optional, but highly recommended: add the following to `~/.ssh/config` so
|
||||
that you can detect bricked SSH connections:
|
||||
|
||||
```bash
|
||||
Host *
|
||||
ServerAliveInterval 5
|
||||
ExitOnForwardFailure yes
|
||||
```
|
||||
|
||||
> You can [forward your
|
||||
> SSH](https://developer.github.com/v3/guides/using-ssh-agent-forwarding/) and
|
||||
> [GPG agent](https://wiki.gnupg.org/AgentForwarding) to the instance to
|
||||
> securely access GitHub and sign commits without having to copy your keys.
|
||||
|
||||
### Using Let's Encrypt with Caddy
|
||||
|
||||
Using [Let's Encrypt](https://letsencrypt.org) is an option if you want to
|
||||
access code-server on an iPad or do not want to use SSH port forwarding.
|
||||
|
||||
1. This option requires that the remote machine be exposed to the internet. Make sure that your instance allows HTTP/HTTP traffic.
|
||||
|
||||
1. You'll need a domain name (if you don't have one, you can purchase one from
|
||||
[Google Domains](https://domains.google.com) or the domain service of your
|
||||
choice)). Once you have a domain name, add an A record to your domain that contains your
|
||||
instance's IP address.
|
||||
|
||||
1. Install [Caddy](https://caddyserver.com/docs/download#debian-ubuntu-raspbian):
|
||||
|
||||
```console
|
||||
sudo apt install -y debian-keyring debian-archive-keyring apt-transport-https
|
||||
curl -1sLf 'https://dl.cloudsmith.io/public/caddy/stable/cfg/gpg/gpg.155B6D79CA56EA34.key' | sudo apt-key add -
|
||||
curl -1sLf 'https://dl.cloudsmith.io/public/caddy/stable/cfg/setup/config.deb.txt?distro=debian&version=any-version' | sudo tee -a /etc/apt/sources.list.d/caddy-stable.list
|
||||
sudo apt update
|
||||
sudo apt install caddy
|
||||
```
|
||||
|
||||
1. Replace `/etc/caddy/Caddyfile` using `sudo` so that the file looks like this:
|
||||
|
||||
```text
|
||||
mydomain.com
|
||||
|
||||
reverse_proxy 127.0.0.1:8080
|
||||
```
|
||||
|
||||
If you want to serve code-server from a sub-path, you can do so as follows:
|
||||
|
||||
```text
|
||||
mydomain.com/code/* {
|
||||
uri strip_prefix /code
|
||||
reverse_proxy 127.0.0.1:8080
|
||||
}
|
||||
```
|
||||
|
||||
Remember to replace `mydomain.com` with your domain name!
|
||||
|
||||
1. Reload Caddy:
|
||||
|
||||
```console
|
||||
sudo systemctl reload caddy
|
||||
```
|
||||
|
||||
At this point, you should be able to access code-server via
|
||||
`https://mydomain.com`.
|
||||
|
||||
### Using Let's Encrypt with NGINX
|
||||
|
||||
1. This option requires that the remote machine be exposed to the internet. Make sure that your instance allows HTTP/HTTP traffic.
|
||||
|
||||
1. You'll need a domain name (if you don't have one, you can purchase one from
|
||||
[Google Domains](https://domains.google.com) or the domain service of your
|
||||
choice)). Once you have a domain name, add an A record to your domain that contains your
|
||||
instance's IP address.
|
||||
|
||||
1. Install NGINX:
|
||||
|
||||
```bash
|
||||
sudo apt update
|
||||
sudo apt install -y nginx certbot python3-certbot-nginx
|
||||
```
|
||||
|
||||
1. Update `/etc/nginx/sites-available/code-server` using sudo with the following
|
||||
configuration:
|
||||
|
||||
```text
|
||||
server {
|
||||
listen 80;
|
||||
listen [::]:80;
|
||||
server_name mydomain.com;
|
||||
|
||||
location / {
|
||||
proxy_pass http://localhost:8080/;
|
||||
proxy_set_header Host $host;
|
||||
proxy_set_header Upgrade $http_upgrade;
|
||||
proxy_set_header Connection upgrade;
|
||||
proxy_set_header Accept-Encoding gzip;
|
||||
}
|
||||
}
|
||||
```
|
||||
|
||||
Be sure to replace `mydomain.com` with your domain name!
|
||||
|
||||
1. Enable the config:
|
||||
|
||||
```console
|
||||
sudo ln -s ../sites-available/code-server /etc/nginx/sites-enabled/code-server
|
||||
sudo certbot --non-interactive --redirect --agree-tos --nginx -d mydomain.com -m me@example.com
|
||||
```
|
||||
|
||||
Be sure to replace `me@example.com` with your actual email.
|
||||
|
||||
At this point, you should be able to access code-server via
|
||||
`https://mydomain.com`.
|
||||
|
||||
### Using a self-signed certificate
|
||||
|
||||
> Self signed certificates do not work with iPad; see [./ipad.md](./ipad.md) for
|
||||
> more information.
|
||||
|
||||
Before proceeding, we recommend familiarizing yourself with the [risks of
|
||||
self-signing a certificate for
|
||||
SSL](https://security.stackexchange.com/questions/8110).
|
||||
|
||||
We recommend self-signed certificates as a last resort, since self-signed
|
||||
certificates do not work with iPads and may cause unexpected issues with
|
||||
code-server. You should only proceed with this option if:
|
||||
|
||||
- You do not want to buy a domain or you cannot expose the remote machine to
|
||||
the internet
|
||||
- You do not want to use port forwarding via SSH
|
||||
|
||||
To use a self-signed certificate:
|
||||
|
||||
1. This option requires that the remote machine be exposed to the internet. Make
|
||||
sure that your instance allows HTTP/HTTP traffic.
|
||||
|
||||
1. SSH into your instance and edit your code-server config file to use a
|
||||
randomly generated self-signed certificate:
|
||||
|
||||
```console
|
||||
# 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
|
||||
```
|
||||
|
||||
1. Restart code-server:
|
||||
|
||||
```console
|
||||
sudo systemctl restart code-server@$USER
|
||||
```
|
||||
|
||||
At this point, you should be able to access code-server via
|
||||
`https://<your-instance-ip>`.
|
||||
|
||||
If you'd like to avoid the warnings displayed by code-server when using a
|
||||
self-signed certificate, you can use [mkcert](https://mkcert.dev) to create a
|
||||
self-signed certificate that's trusted by your operating system, then pass the
|
||||
certificate to code-server via the `cert` and `cert-key` config fields.
|
||||
|
||||
## External authentication
|
||||
|
||||
If you want to use external authentication mechanism (e.g., Sign in with
|
||||
Google), you can do this with a reverse proxy such as:
|
||||
|
||||
- [Pomerium](https://www.pomerium.io/guides/code-server.html)
|
||||
- [oauth2_proxy](https://github.com/pusher/oauth2_proxy)
|
||||
- [Cloudflare Access](https://teams.cloudflare.com/access)
|
||||
|
||||
## HTTPS and self-signed certificates
|
||||
|
||||
For HTTPS, you can use a self-signed certificate by:
|
||||
|
||||
- Passing in `--cert`
|
||||
- Passing in an existing certificate by providing the path to `--cert` and the
|
||||
path to the key with `--cert-key`
|
||||
|
||||
The self signed certificate will be generated to
|
||||
`~/.local/share/code-server/self-signed.crt`.
|
||||
|
||||
If you pass a certificate to code-server, it will respond to HTTPS requests and
|
||||
redirect all HTTP requests to HTTPS.
|
||||
|
||||
> You can use [Let's Encrypt](https://letsencrypt.org/) to get a TLS certificate
|
||||
> for free.
|
||||
|
||||
Note: if you set `proxy_set_header Host $host;` in your reverse proxy config, it will change the address displayed in the green section of code-server in the bottom left to show the correct address.
|
||||
|
||||
## Accessing web services
|
||||
|
||||
If you're working on web services and want to access it locally, code-server
|
||||
can proxy to any port using either a subdomain or a subpath, allowing you to
|
||||
securely access these services using code-server's built-in authentication.
|
||||
|
||||
### Using a subdomain
|
||||
|
||||
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.
|
||||
|
||||
To set your domain, start code-server with the `--proxy-domain` flag:
|
||||
|
||||
```console
|
||||
code-server --proxy-domain <domain>
|
||||
```
|
||||
|
||||
Now you can browse to `<port>.<domain>`. Note that this uses the host header, so
|
||||
ensure your reverse proxy (if you're using one) forwards that information.
|
||||
|
||||
### Using a subpath
|
||||
|
||||
Simply browse to `/proxy/<port>/`.
|
||||
|
||||
### Stripping `/proxy/<port>` from the request path
|
||||
|
||||
You may notice that the code-server proxy strips `/proxy/<port>` from the
|
||||
request path.
|
||||
|
||||
HTTP servers should use relative URLs to avoid the need to be coupled to the
|
||||
absolute path at which they are served. This means you must [use trailing
|
||||
slashes on all paths with
|
||||
subpaths](https://blog.cdivilly.com/2019/02/28/uri-trailing-slashes).
|
||||
|
||||
This reasoning is why the default behavior is to strip `/proxy/<port>` from the
|
||||
base path. If your application uses relative URLs and does not assume the
|
||||
absolute path at which it is being served, it will just work no matter what port
|
||||
you decide to serve it off or if you put it in behind code-server or any other
|
||||
proxy.
|
||||
|
||||
However, some prefer the cleaner aesthetic of no trailing slashes. Omitting the
|
||||
trailing slashes couples you to the base path, since you cannot use relative
|
||||
redirects correctly anymore. If you're okay with this tradeoff, use `/absproxy`
|
||||
instead and the path will be passed as is (e.g., `/absproxy/3000/my-app-path`).
|
||||
|
||||
### Proxying to create a React app
|
||||
|
||||
You must use `/absproxy/<port>` with `create-react-app` (see
|
||||
[#2565](https://github.com/cdr/code-server/issues/2565) and
|
||||
[#2222](https://github.com/cdr/code-server/issues/2222) for more information).
|
||||
You will need to inform `create-react-app` of the path at which you are serving
|
||||
via `$PUBLIC_URL` and webpack via `$WDS_SOCKET_PATH`:
|
||||
|
||||
```sh
|
||||
PUBLIC_URL=/absproxy/3000 \
|
||||
WDS_SOCKET_PATH=$PUBLIC_URL/sockjs-node \
|
||||
BROWSER=none yarn start
|
||||
```
|
||||
|
||||
You should then be able to visit `https://my-code-server-address.io/absproxy/3000` to see your app exposed through
|
||||
code-server!
|
||||
|
||||
> We highly recommend using the subdomain approach instead to avoid this class of issue.
|
||||
|
||||
### Proxying to a Vue app
|
||||
|
||||
Similar to the situation with React apps, you have to make a few modifications to proxy a Vue app.
|
||||
|
||||
1. add `vue.config.js`
|
||||
2. update the values to match this (you can use any free port):
|
||||
|
||||
```js
|
||||
module.exports = {
|
||||
devServer: {
|
||||
port: 3454,
|
||||
sockPath: "sockjs-node",
|
||||
},
|
||||
publicPath: "/absproxy/3454",
|
||||
}
|
||||
```
|
||||
|
||||
3. access app at `<code-server-root>/absproxy/3454` e.g. `http://localhost:8080/absproxy/3454`
|
||||
|
||||
Read more about `publicPath` in the [Vue.js docs](https://cli.vuejs.org/config/#publicpath)
|
||||
|
||||
<!-- 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
|
||||
|
||||
- [Expose code-server](#expose-code-server)
|
||||
@@ -398,6 +7,7 @@ Read more about `publicPath` in the [Vue.js docs](https://cli.vuejs.org/config/#
|
||||
- [Using Let's Encrypt with Caddy](#using-lets-encrypt-with-caddy)
|
||||
- [Using Let's Encrypt with NGINX](#using-lets-encrypt-with-nginx)
|
||||
- [Using a self-signed certificate](#using-a-self-signed-certificate)
|
||||
- [TLS 1.3 and Safari](#tls-13-and-safari)
|
||||
- [External authentication](#external-authentication)
|
||||
- [HTTPS and self-signed certificates](#https-and-self-signed-certificates)
|
||||
- [Accessing web services](#accessing-web-services)
|
||||
@@ -536,7 +146,7 @@ sudo apt install caddy
|
||||
mydomain.com/code/* {
|
||||
uri strip_prefix /code
|
||||
reverse_proxy 127.0.0.1:8080
|
||||
}
|
||||
}
|
||||
```
|
||||
|
||||
Remember to replace `mydomain.com` with your domain name!
|
||||
@@ -647,6 +257,13 @@ self-signed certificate, you can use [mkcert](https://mkcert.dev) to create a
|
||||
self-signed certificate that's trusted by your operating system, then pass the
|
||||
certificate to code-server via the `cert` and `cert-key` config fields.
|
||||
|
||||
### TLS 1.3 and Safari
|
||||
|
||||
If you will be using Safari and your configuration does not allow anything less
|
||||
than TLS 1.3 you will need to add support for TLS 1.2 since Safari does not
|
||||
support TLS 1.3 for web sockets at the time of writing. If this is the case you
|
||||
should see OSSStatus: 9836 in the browser console.
|
||||
|
||||
## External authentication
|
||||
|
||||
If you want to use external authentication mechanism (e.g., Sign in with
|
||||
|
||||
@@ -1,6 +1,6 @@
|
||||
# code-server Helm Chart
|
||||
|
||||
[](https://img.shields.io/badge/Version-1.0.0-informational?style=flat-square) [](https://img.shields.io/badge/Type-application-informational?style=flat-square) [](https://img.shields.io/badge/AppVersion-3.12.0-informational?style=flat-square)
|
||||
[](https://img.shields.io/badge/Version-1.0.0-informational?style=flat-square) [](https://img.shields.io/badge/Type-application-informational?style=flat-square) [](https://img.shields.io/badge/AppVersion-4.0.0-informational?style=flat-square)
|
||||
|
||||
[code-server](https://github.com/cdr/code-server) code-server is VS Code running
|
||||
on a remote server, accessible through the browser.
|
||||
@@ -73,7 +73,7 @@ and their default values.
|
||||
| hostnameOverride | string | `""` |
|
||||
| image.pullPolicy | string | `"Always"` |
|
||||
| image.repository | string | `"codercom/code-server"` |
|
||||
| image.tag | string | `"3.12.0"` |
|
||||
| image.tag | string | `"4.0.0"` |
|
||||
| imagePullSecrets | list | `[]` |
|
||||
| ingress.enabled | bool | `false` |
|
||||
| nameOverride | string | `""` |
|
||||
|
||||
110
docs/ipad.md
110
docs/ipad.md
@@ -3,14 +3,14 @@
|
||||
# iPad
|
||||
|
||||
- [Using the code-server progressive web app (PWA)](#using-the-code-server-progressive-web-app-pwa)
|
||||
- [Access code-server with a self-signed certificate on an iPad](#access-code-server-with-a-self-signed-certificate-on-an-ipad)
|
||||
- [Certificate requirements](#certificate-requirements)
|
||||
- [Sharing a self-signed certificate with an iPad](#sharing-a-self-signed-certificate-with-an-ipad)
|
||||
- [Access code-server using Servediter](#access-code-server-using-servediter)
|
||||
- [Raspberry Pi USB-C network](#raspberry-pi-usb-c-network)
|
||||
- [Recommendations](#recommendations)
|
||||
- [Known issues](#known-issues)
|
||||
- [Workaround for issue with `ctrl+c` not stopping a running process in the terminal](#workaround-for-issue-with-ctrlc-not-stopping-a-running-process-in-the-terminal)
|
||||
- [Access code-server with a self-signed certificate on an iPad](#access-code-server-with-a-self-signed-certificate-on-an-ipad)
|
||||
- [Certificate requirements](#certificate-requirements)
|
||||
- [Sharing a self-signed certificate with an iPad](#sharing-a-self-signed-certificate-with-an-ipad)
|
||||
|
||||
<!-- END doctoc generated TOC please keep comment here to allow auto update -->
|
||||
|
||||
@@ -45,51 +45,6 @@ can add this to `keybindings.json`:
|
||||
|
||||
4. Test the command by using `cmd+w` to close an active file.
|
||||
|
||||
## Access code-server with a self-signed certificate on an iPad
|
||||
|
||||
If you've installed code-server and are [running it with a self-signed
|
||||
certificate](./guide.md#using-a-self-signed-certificate), you may see multiple
|
||||
security warnings from Safari. To fix this, you'll need to install the
|
||||
self-signed certificate generated by code-server as a profile on your device (you'll also need to do this to
|
||||
enable WebSocket connections).
|
||||
|
||||
### Certificate requirements
|
||||
|
||||
- We're assuming that you're using the self-signed certificate code-server
|
||||
generates for you (if not, make sure that your certificate [abides by the
|
||||
guidelines issued by Apple](https://support.apple.com/en-us/HT210176)).
|
||||
- We've noticed that the certificate has to include `basicConstraints=CA:true`.
|
||||
- Your certificate must have a subject alt name that matches the hostname you'll
|
||||
use to access code-server from the iPad. You can pass this name to code-server
|
||||
so that it generates the certificate correctly using `--cert-host`.
|
||||
|
||||
### Sharing a self-signed certificate with an iPad
|
||||
|
||||
To share a self-signed certificate with an iPad:
|
||||
|
||||
1. Get the location of the certificate code-server generated; code-server prints
|
||||
the certificate's location in its logs:
|
||||
|
||||
```console
|
||||
[2020-10-30T08:55:45.139Z] info - Using generated certificate and key for HTTPS: ~/.local/share/code-server/mymbp_local.crt
|
||||
```
|
||||
|
||||
2. Send the certificate to the iPad, either by emailing it to yourself or using
|
||||
Apple's Airdrop feature.
|
||||
|
||||
3. Open the `*.crt` file so that you're prompted to go into Settings to install.
|
||||
|
||||
4. Go to **Settings** > **General** > **Profile**, and select the profile. Tap **Install**.
|
||||
|
||||
5. Go to **Settings** > **About** > **Certificate Trust Settings** and [enable
|
||||
full trust for your certificate](https://support.apple.com/en-us/HT204477).
|
||||
|
||||
You should be able to access code-server without all of Safari's warnings now.
|
||||
|
||||
**warning**: Your iPad must access code-server via a domain name. It could be local
|
||||
DNS like `mymacbookpro.local`, but it must be a domain name. Otherwise, Safari will
|
||||
not allow WebSockets connections.
|
||||
|
||||
## Access code-server using Servediter
|
||||
|
||||
If you are unable to get the self-signed certificate working, or you do not have a domain
|
||||
@@ -149,7 +104,6 @@ and tricks helpful:
|
||||
process](#access-code-server-with-a-self-signed-certificate-on-an-ipad)
|
||||
- Keyboard issues:
|
||||
- The keyboard disappear sometimes
|
||||
[#1313](https://github.com/cdr/code-server/issues/1313),
|
||||
[#979](https://github.com/cdr/code-server/issues/979)
|
||||
- Some expectations regarding shortcuts may not be met:
|
||||
- `cmd + n` opens new browser window instead of new file, and it's difficult
|
||||
@@ -157,15 +111,12 @@ and tricks helpful:
|
||||
- In general, expect to edit your keyboard shortcuts
|
||||
- There's no escape key by default on the Magic Keyboard, so most users set
|
||||
the globe key to be an escape key
|
||||
- Trackpad scrolling does not work
|
||||
- Trackpad scrolling does not work on iPadOS < 14.5
|
||||
([#1455](https://github.com/cdr/code-server/issues/1455))
|
||||
- Bug tracking of a WebKit fix
|
||||
[here](https://bugs.webkit.org/show_bug.cgi?id=210071#c13)
|
||||
- Tracking of [WebKit patch](https://trac.webkit.org/changeset/270712/webkit)
|
||||
- Alternatives:
|
||||
- Install line-jump extension and use keyboard to navigate by jumping large
|
||||
amount of lines
|
||||
- Use touch scrolling
|
||||
- [WebKit fix](https://bugs.webkit.org/show_bug.cgi?id=210071#c13)
|
||||
- Keyboard may lose focus in Safari / split view [#4182](https://github.com/cdr/code-server/issues/4182)
|
||||
- Terminal text does not appear by default [#3824](https://github.com/cdr/code-server/issues/3824)
|
||||
- Copy & paste in terminal does not work well with keyboard shortcuts [#3491](https://github.com/cdr/code-server/issues/3491)
|
||||
- `ctrl+c` does not stop a long-running process in the browser
|
||||
- Tracking upstream issue here:
|
||||
[#114009](https://github.com/microsoft/vscode/issues/114009)
|
||||
@@ -199,3 +150,48 @@ In the meantime, you can manually define a shortcut as a workaround:
|
||||
```
|
||||
|
||||
_Source: [StackOverflow](https://stackoverflow.com/a/52735954/3015595)_
|
||||
|
||||
## Access code-server with a self-signed certificate on an iPad
|
||||
|
||||
If you've installed code-server and are [running it with a self-signed
|
||||
certificate](./guide.md#using-a-self-signed-certificate), you may see multiple
|
||||
security warnings from Safari. To fix this, you'll need to install the
|
||||
self-signed certificate generated by code-server as a profile on your device (you'll also need to do this to
|
||||
enable WebSocket connections).
|
||||
|
||||
### Certificate requirements
|
||||
|
||||
- We're assuming that you're using the self-signed certificate code-server
|
||||
generates for you (if not, make sure that your certificate [abides by the
|
||||
guidelines issued by Apple](https://support.apple.com/en-us/HT210176)).
|
||||
- We've noticed that the certificate has to include `basicConstraints=CA:true`.
|
||||
- Your certificate must have a subject alt name that matches the hostname you'll
|
||||
use to access code-server from the iPad. You can pass this name to code-server
|
||||
so that it generates the certificate correctly using `--cert-host`.
|
||||
|
||||
### Sharing a self-signed certificate with an iPad
|
||||
|
||||
To share a self-signed certificate with an iPad:
|
||||
|
||||
1. Get the location of the certificate code-server generated; code-server prints
|
||||
the certificate's location in its logs:
|
||||
|
||||
```console
|
||||
[2020-10-30T08:55:45.139Z] info - Using generated certificate and key for HTTPS: ~/.local/share/code-server/mymbp_local.crt
|
||||
```
|
||||
|
||||
2. Send the certificate to the iPad, either by emailing it to yourself or using
|
||||
Apple's Airdrop feature.
|
||||
|
||||
3. Open the `*.crt` file so that you're prompted to go into Settings to install.
|
||||
|
||||
4. Go to **Settings** > **General** > **Profile**, and select the profile. Tap **Install**.
|
||||
|
||||
5. Go to **Settings** > **About** > **Certificate Trust Settings** and [enable
|
||||
full trust for your certificate](https://support.apple.com/en-us/HT204477).
|
||||
|
||||
You should be able to access code-server without all of Safari's warnings now.
|
||||
|
||||
**warning**: Your iPad must access code-server via a domain name. It could be local
|
||||
DNS like `mymacbookpro.local`, but it must be a domain name. Otherwise, Safari will
|
||||
not allow WebSockets connections.
|
||||
|
||||
@@ -1,6 +1,8 @@
|
||||
# code-server --link
|
||||
|
||||
Run code-server with the beta flag `--link` and you'll get TLS, authentication, and a dedicated URL
|
||||
> Note: This feature is no longer recommended due to instability. Stay tuned for a revised version.
|
||||
|
||||
Run code-server with the flag `--link` and you'll get TLS, authentication, and a dedicated URL
|
||||
for accessing your IDE out of the box.
|
||||
|
||||
```console
|
||||
|
||||
@@ -1,5 +1,5 @@
|
||||
{
|
||||
"versions": ["v3.12.0"],
|
||||
"versions": ["v4.0.0"],
|
||||
"routes": [
|
||||
{
|
||||
"title": "Home",
|
||||
|
||||
153
docs/termux.md
153
docs/termux.md
@@ -5,67 +5,144 @@
|
||||
- [Install](#install)
|
||||
- [Upgrade](#upgrade)
|
||||
- [Known Issues](#known-issues)
|
||||
- [Search doesn't work](#search-doesnt-work)
|
||||
- [Backspace doesn't work](#backspace-doesnt-work)
|
||||
- [Git won't work in `/sdcard`](#git-wont-work-in-sdcard)
|
||||
- [Extra](#extra)
|
||||
- [Create a new user](#create-a-new-user)
|
||||
- [Install Go](#install-go)
|
||||
- [Install Python](#install-python)
|
||||
|
||||
<!-- END doctoc generated TOC please keep comment here to allow auto update -->
|
||||
|
||||
Termux is a terminal application and Linux environment that you can also use to
|
||||
run code-server from your Android phone.
|
||||
|
||||
## Install
|
||||
|
||||
1. Install Termux from [F-Droid](https://f-droid.org/en/packages/com.termux/).
|
||||
1. Make sure it's up-to-date: `apt update && apt upgrade`
|
||||
1. Install required packages: `apt install build-essential python git nodejs yarn`
|
||||
1. Install code-server: `yarn global add code-server`
|
||||
1. Run code-server: `code-server` and navigate to localhost:8080 in your browser
|
||||
1. Get [Termux](https://f-droid.org/en/packages/com.termux/) from **F-Droid**.
|
||||
2. Install Debian by running the following.
|
||||
- Run `termux-setup-storage` to allow storage access, or else code-server won't be able to read from `/sdcard`.\
|
||||
If you used the Andronix command then you may have to edit the `start-debian.sh` script to mount `/sdcard` just as simple as uncommenting the `command+=" -b /sdcard"` line.
|
||||
> The following command was extracted from [Andronix](https://andronix.app/) you can also use [proot-distro](https://github.com/termux/proot-distro).
|
||||
> After Debian is installed the `~ $` will change to `root@localhost`.
|
||||
|
||||
```bash
|
||||
pkg update -y && pkg install wget curl proot tar -y && wget https://raw.githubusercontent.com/AndronixApp/AndronixOrigin/master/Installer/Debian/debian.sh -O debian.sh && chmod +x debian.sh && bash debian.sh
|
||||
```
|
||||
|
||||
3. Run the following commands to setup Debian.
|
||||
|
||||
```bash
|
||||
apt update
|
||||
apt upgrade -y
|
||||
apt-get install nano vim sudo curl wget git -y
|
||||
```
|
||||
|
||||
4. Install [NVM](https://github.com/nvm-sh/nvm) by following the install guide in the README, just a curl/wget command.
|
||||
5. Set up NVM for multi-user. After installing NVM it automatically adds the necessary commands for it to work, but it will only work if you are logged in as root;
|
||||
|
||||
- Copy the lines NVM asks you to run after running the install script.
|
||||
- Run `nano /root/.bashrc` and comment out those lines by adding a `#` at the start.
|
||||
- Run `nano /etc/profile` and paste those lines at the end and make sure to replace `$HOME` with `/root`
|
||||
- Now run `exit` and start Debain again.
|
||||
|
||||
6. After following the instructions and setting up NVM you can now install the [required node version](https://coder.com/docs/code-server/latest/npm#nodejs-version) using `nvm install version_here`.
|
||||
7. To install `code-server` run the following.
|
||||
> To check the install process (Will not actually install code-server)
|
||||
> If it all looks good, you can install code-server by running the second command
|
||||
|
||||
```bash
|
||||
curl -fsSL https://code-server.dev/install.sh | sh -s -- --dry-run
|
||||
```
|
||||
|
||||
```bash
|
||||
curl -fsSL https://code-server.dev/install.sh | sh
|
||||
```
|
||||
|
||||
8. You can now start code server by simply running `code-server`.
|
||||
|
||||
> Consider using a new user instead of root, read [here](https://www.howtogeek.com/124950/htg-explains-why-you-shouldnt-log-into-your-linux-system-as-root/) why using root is not recommended.\
|
||||
> Learn how to add a user [here](#create-a-new-user).
|
||||
|
||||
## Upgrade
|
||||
|
||||
To upgrade run: `yarn global upgrade code-server --latest`
|
||||
1. Remove all previous installs `rm -rf ~/.local/lib/code-server-*`
|
||||
2. Run the install script again `curl -fsSL https://code-server.dev/install.sh | sh`
|
||||
|
||||
## Known Issues
|
||||
|
||||
The following details known issues and suggested workarounds for using
|
||||
code-server with Termux.
|
||||
### Git won't work in `/sdcard`
|
||||
|
||||
### Search doesn't work
|
||||
Issue : Using git in the `/sdcard` directory will fail during cloning/commit/staging/etc...\
|
||||
Fix : None\
|
||||
Potential Workaround :
|
||||
|
||||
There is a known issue with search not working on Android because it's missing
|
||||
`bin/rg` ([context](https://github.com/cdr/code-server/issues/1730#issuecomment-721515979)). To fix this:
|
||||
1. Create a soft-link from the debian-fs to your folder in `/sdcard`
|
||||
2. Use git from termux (preferred)
|
||||
|
||||
1. Install `ripgrep` with `pkg`
|
||||
## Extra
|
||||
|
||||
```sh
|
||||
pkg install ripgrep
|
||||
```
|
||||
### Create a new user
|
||||
|
||||
1. Make a soft link using `ln -s`
|
||||
To create a new user follow these simple steps -
|
||||
|
||||
```sh
|
||||
# run this command inside the code-server directory
|
||||
ln -s $PREFIX/bin/rg ./vendor/modules/code-oss-dev/vscode-ripgrep/bin/rg
|
||||
```
|
||||
1. Create a new user by running `useradd username -m`.
|
||||
2. Change the password by running `passwd username`.
|
||||
3. Give your new user sudo access by runnning `visudo`, scroll down to `User privilege specification` and add the following line after root `username ALL=(ALL:ALL) ALL`.
|
||||
4. Now edit the `/etc/passwd` file with your commadline editor of choice and at the end of the line that specifies your user change `/bin/sh` to `/bin/bash`.
|
||||
5. Now switch users, by running `su - username`
|
||||
|
||||
### Backspace doesn't work
|
||||
- Remember the `-` betweeen `su` and username is required to execute `/etc/profile`,\
|
||||
since `/etc/profile` may have some necessary things to be executed you should always add a `-`.
|
||||
|
||||
When using Android's on-screen keyboard, the backspace key doesn't work
|
||||
properly. This is a known upstream issue:
|
||||
### Install Go
|
||||
|
||||
- [Issues with backspace in Codespaces on Android (Surface Duo)](https://github.com/microsoft/vscode/issues/107602)
|
||||
- [Support mobile platforms](https://github.com/xtermjs/xterm.js/issues/1101)
|
||||
> From https://golang.org/doc/install
|
||||
|
||||
There are two workarounds.
|
||||
1. Go to https://golang.org/dl/ and copy the download link for `linux arm` and run the following.
|
||||
|
||||
**Option 1:** Modify keyboard dispatch settings
|
||||
```bash
|
||||
wget download_link
|
||||
```
|
||||
|
||||
1. Open the Command Palette
|
||||
2. Search for **Preferences: Open Settings (JSON)**
|
||||
3. Add `"keyboard.dispatch": "keyCode"`
|
||||
2. Extract the downloaded archive. (This step will erase all previous GO installs, make sure to create a backup if you have previously installed GO)
|
||||
|
||||
The backspace button should work at this point.
|
||||
```bash
|
||||
rm -rf /usr/local/go && tar -C /usr/local -xzf archive_name
|
||||
```
|
||||
|
||||
_Thanks to @Nefomemes for the [suggestion](https://github.com/cdr/code-server/issues/1141#issuecomment-789463707)!_
|
||||
3. Run `nano /etc/profile` and add the following line `export PATH=$PATH:/usr/local/go/bin`.
|
||||
4. Now run `exit` (depending on if you have switched users or not, you may have to run `exit` multiple times to get to normal termux shell) and start Debian again.
|
||||
5. Check if your install was successful by running `go version`
|
||||
|
||||
**Option 2:** Use a Bluetooth keyboard.
|
||||
### Install Python
|
||||
|
||||
> Run these commands as root
|
||||
|
||||
1. Run the following command to install required packages to build python.
|
||||
|
||||
```bash
|
||||
sudo apt-get update
|
||||
sudo apt-get install make build-essential libssl-dev zlib1g-dev \
|
||||
libbz2-dev libreadline-dev libsqlite3-dev wget curl llvm \
|
||||
libncursesw5-dev xz-utils tk-dev libxml2-dev libxmlsec1-dev libffi-dev liblzma-dev
|
||||
```
|
||||
|
||||
2. Install [pyenv](https://github.com/pyenv/pyenv/) from [pyenv-installer](https://github.com/pyenv/pyenv-installer) by running.
|
||||
|
||||
```bash
|
||||
curl -L https://github.com/pyenv/pyenv-installer/raw/master/bin/pyenv-installer | bash
|
||||
```
|
||||
|
||||
3. Run `nano /etc/profile` and add the following
|
||||
|
||||
```bash
|
||||
export PYENV_ROOT="/root/.pyenv"
|
||||
export PATH="/root/.pyenv/bin:$PATH"
|
||||
eval "$(pyenv init --path)"
|
||||
eval "$(pyenv virtualenv-init -)"
|
||||
```
|
||||
|
||||
4. Exit start Debian again.
|
||||
5. Run `pyenv versions` to list all installable versions.
|
||||
6. Run `pyenv install version` to install the desired python version.
|
||||
> The build process may take some time (an hour or 2 depending on your device).
|
||||
7. Run `touch /root/.pyenv/version && echo "your_version_here" > /root/.pyenv/version`
|
||||
8. (You may have to start Debian again) Run `python3 -V` to verify if PATH works or not.
|
||||
> If `python3` doesn't work but pyenv says that the install was successful in step 6 then try running `$PYENV_ROOT/versions/your_version/bin/python3`.
|
||||
|
||||
42
package.json
42
package.json
@@ -1,7 +1,7 @@
|
||||
{
|
||||
"name": "code-server",
|
||||
"license": "MIT",
|
||||
"version": "3.12.0",
|
||||
"version": "4.0.0",
|
||||
"description": "Run VS Code on a remote server.",
|
||||
"homepage": "https://github.com/cdr/code-server",
|
||||
"bugs": {
|
||||
@@ -19,7 +19,7 @@
|
||||
"release:prep": "./ci/build/release-prep.sh",
|
||||
"test:e2e": "./ci/dev/test-e2e.sh",
|
||||
"test:standalone-release": "./ci/build/test-standalone-release.sh",
|
||||
"test:unit": "./ci/dev/test-unit.sh",
|
||||
"test:unit": "./ci/dev/test-unit.sh --forceExit --detectOpenHandles",
|
||||
"test:scripts": "./ci/dev/test-scripts.sh",
|
||||
"package": "./ci/build/build-packages.sh",
|
||||
"postinstall": "./ci/dev/postinstall.sh",
|
||||
@@ -28,15 +28,13 @@
|
||||
"lint": "./ci/dev/lint.sh",
|
||||
"test": "echo 'Run yarn test:unit or yarn test:e2e' && exit 1",
|
||||
"ci": "./ci/dev/ci.sh",
|
||||
"watch": "VSCODE_IPC_HOOK_CLI= NODE_OPTIONS='--max_old_space_size=32384 --trace-warnings' ts-node ./ci/dev/watch.ts",
|
||||
"watch": "VSCODE_DEV=1 VSCODE_IPC_HOOK_CLI= NODE_OPTIONS='--max_old_space_size=32384 --trace-warnings' ts-node ./ci/dev/watch.ts",
|
||||
"icons": "./ci/dev/gen_icons.sh",
|
||||
"coverage": "codecov"
|
||||
},
|
||||
"main": "out/node/entry.js",
|
||||
"devDependencies": {
|
||||
"@schemastore/package": "^0.0.6",
|
||||
"@types/body-parser": "^1.19.0",
|
||||
"@types/browserify": "^12.0.36",
|
||||
"@types/compression": "^1.7.0",
|
||||
"@types/cookie-parser": "^1.4.2",
|
||||
"@types/express": "^4.17.8",
|
||||
@@ -48,29 +46,28 @@
|
||||
"@types/safe-compare": "^1.1.0",
|
||||
"@types/semver": "^7.1.0",
|
||||
"@types/split2": "^3.2.0",
|
||||
"@types/tar-fs": "^2.0.0",
|
||||
"@types/tar-stream": "^2.1.0",
|
||||
"@types/ws": "^7.2.6",
|
||||
"@typescript-eslint/eslint-plugin": "^4.7.0",
|
||||
"@typescript-eslint/parser": "^4.7.0",
|
||||
"audit-ci": "^4.0.0",
|
||||
"browserify": "^17.0.0",
|
||||
"@types/trusted-types": "^2.0.2",
|
||||
"@types/ws": "^8.0.0",
|
||||
"@typescript-eslint/eslint-plugin": "^5.0.0",
|
||||
"@typescript-eslint/parser": "^5.0.0",
|
||||
"audit-ci": "^5.0.0",
|
||||
"codecov": "^3.8.3",
|
||||
"doctoc": "^2.0.0",
|
||||
"eslint": "^7.7.0",
|
||||
"eslint-config-prettier": "^8.1.0",
|
||||
"eslint-import-resolver-alias": "^1.1.2",
|
||||
"eslint-import-resolver-typescript": "^2.5.0",
|
||||
"eslint-plugin-import": "^2.18.2",
|
||||
"eslint-plugin-prettier": "^4.0.0",
|
||||
"prettier": "^2.2.1",
|
||||
"prettier-plugin-sh": "^0.7.1",
|
||||
"prettier-plugin-sh": "^0.8.0",
|
||||
"shellcheck": "^1.0.0",
|
||||
"stylelint": "^13.0.0",
|
||||
"stylelint-config-recommended": "^5.0.0",
|
||||
"ts-node": "^10.0.0",
|
||||
"typescript": "^4.1.3"
|
||||
"typescript": "^4.4.0-dev.20210528"
|
||||
},
|
||||
"resolutions": {
|
||||
"ansi-regex": "^5.0.1",
|
||||
"normalize-package-data": "^3.0.0",
|
||||
"doctoc/underscore": "^1.13.1",
|
||||
"doctoc/**/trim": "^1.0.0",
|
||||
@@ -78,13 +75,13 @@
|
||||
"browserslist": "^4.16.5",
|
||||
"safe-buffer": "^5.1.1",
|
||||
"vfile-message": "^2.0.2",
|
||||
"argon2/@mapbox/node-pre-gyp/tar": "^6.1.9",
|
||||
"path-parse": "^1.0.7"
|
||||
"tar": "^6.1.9",
|
||||
"path-parse": "^1.0.7",
|
||||
"vm2": "^3.9.4"
|
||||
},
|
||||
"dependencies": {
|
||||
"@coder/logger": "1.1.16",
|
||||
"argon2": "^0.28.0",
|
||||
"body-parser": "^1.19.0",
|
||||
"compression": "^1.7.4",
|
||||
"cookie-parser": "^1.4.5",
|
||||
"env-paths": "^2.2.0",
|
||||
@@ -96,13 +93,12 @@
|
||||
"pem": "^1.14.2",
|
||||
"proxy-agent": "^5.0.0",
|
||||
"proxy-from-env": "^1.1.0",
|
||||
"qs": "6.10.1",
|
||||
"rotating-file-stream": "^2.1.1",
|
||||
"qs": "6.10.2",
|
||||
"rotating-file-stream": "^3.0.0",
|
||||
"safe-buffer": "^5.1.1",
|
||||
"safe-compare": "^1.1.4",
|
||||
"semver": "^7.1.3",
|
||||
"split2": "^3.2.2",
|
||||
"tar-fs": "^2.0.0",
|
||||
"split2": "^4.0.0",
|
||||
"ws": "^8.0.0",
|
||||
"xdg-basedir": "^4.0.0",
|
||||
"yarn": "^1.22.4"
|
||||
@@ -119,7 +115,7 @@
|
||||
"browser-ide"
|
||||
],
|
||||
"engines": {
|
||||
"node": "= 14"
|
||||
"node": ">= 14"
|
||||
},
|
||||
"jest": {
|
||||
"transform": {
|
||||
|
||||
@@ -1,20 +0,0 @@
|
||||
{
|
||||
"name": "code-server",
|
||||
"short_name": "code-server",
|
||||
"start_url": "{{BASE}}",
|
||||
"display": "fullscreen",
|
||||
"background-color": "#fff",
|
||||
"description": "Run editors on a remote server.",
|
||||
"icons": [
|
||||
{
|
||||
"src": "{{CS_STATIC_BASE}}/src/browser/media/pwa-icon-192.png",
|
||||
"type": "image/png",
|
||||
"sizes": "192x192"
|
||||
},
|
||||
{
|
||||
"src": "{{CS_STATIC_BASE}}/src/browser/media/pwa-icon-512.png",
|
||||
"type": "image/png",
|
||||
"sizes": "512x512"
|
||||
}
|
||||
]
|
||||
}
|
||||
@@ -10,10 +10,11 @@
|
||||
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="{{CS_STATIC_BASE}}/src/browser/media/favicon-dark-support.svg" />
|
||||
<link rel="alternate icon" href="{{CS_STATIC_BASE}}/src/browser/media/favicon.ico" />
|
||||
<link rel="manifest" href="{{CS_STATIC_BASE}}/src/browser/media/manifest.json" crossorigin="use-credentials" />
|
||||
<link rel="manifest" href="/manifest.json" crossorigin="use-credentials" />
|
||||
<link rel="apple-touch-icon" sizes="192x192" href="{{CS_STATIC_BASE}}/src/browser/media/pwa-icon-192.png" />
|
||||
<link rel="apple-touch-icon" sizes="512x512" href="{{CS_STATIC_BASE}}/src/browser/media/pwa-icon-512.png" />
|
||||
<link href="{{CS_STATIC_BASE}}/src/browser/pages/global.css" rel="stylesheet" />
|
||||
@@ -30,6 +31,5 @@
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
<script data-cfasync="false" src="{{CS_STATIC_BASE}}/out/browser/register.browserified.js"></script>
|
||||
</body>
|
||||
</html>
|
||||
|
||||
@@ -13,7 +13,7 @@
|
||||
<title>code-server login</title>
|
||||
<link rel="icon" href="{{CS_STATIC_BASE}}/src/browser/media/favicon-dark-support.svg" />
|
||||
<link rel="alternate icon" href="{{CS_STATIC_BASE}}/src/browser/media/favicon.ico" />
|
||||
<link rel="manifest" href="{{CS_STATIC_BASE}}/src/browser/media/manifest.json" crossorigin="use-credentials" />
|
||||
<link rel="manifest" href="{{BASE}}/manifest.json" crossorigin="use-credentials" />
|
||||
<link rel="apple-touch-icon" sizes="192x192" href="{{CS_STATIC_BASE}}/src/browser/media/pwa-icon-192.png" />
|
||||
<link rel="apple-touch-icon" sizes="512x512" href="{{CS_STATIC_BASE}}/src/browser/media/pwa-icon-512.png" />
|
||||
<link href="{{CS_STATIC_BASE}}/src/browser/pages/global.css" rel="stylesheet" />
|
||||
@@ -30,7 +30,8 @@
|
||||
<div class="content">
|
||||
<form class="login-form" method="post">
|
||||
<input class="user" type="text" autocomplete="username" />
|
||||
<input id="base" type="hidden" name="base" value="/" />
|
||||
<input id="base" type="hidden" name="base" value="{{BASE}}" />
|
||||
<input id="href" type="hidden" name="href" value="" />
|
||||
<div class="field">
|
||||
<input
|
||||
required
|
||||
@@ -48,6 +49,13 @@
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
<script>
|
||||
// Inform the backend about the path since the proxy might have rewritten
|
||||
// it out of the headers and cookies must be set with absolute paths.
|
||||
const el = document.getElementById("href")
|
||||
if (el) {
|
||||
el.value = location.href
|
||||
}
|
||||
</script>
|
||||
</body>
|
||||
<script data-cfasync="false" src="{{CS_STATIC_BASE}}/out/browser/pages/login.browserified.js"></script>
|
||||
</html>
|
||||
|
||||
@@ -1,8 +0,0 @@
|
||||
import { getOptions } from "../../common/util"
|
||||
import "../register"
|
||||
|
||||
const options = getOptions()
|
||||
const el = document.getElementById("base") as HTMLInputElement
|
||||
if (el) {
|
||||
el.value = options.base
|
||||
}
|
||||
@@ -1,54 +0,0 @@
|
||||
<!-- Copyright (C) Microsoft Corporation. All rights reserved. -->
|
||||
<!DOCTYPE html>
|
||||
<html>
|
||||
<head>
|
||||
<script>
|
||||
performance.mark("code/didStartRenderer")
|
||||
</script>
|
||||
|
||||
<meta charset="utf-8" />
|
||||
|
||||
<!-- Disable pinch zooming -->
|
||||
<meta
|
||||
name="viewport"
|
||||
content="width=device-width, initial-scale=1.0, maximum-scale=1.0, minimum-scale=1.0, user-scalable=no"
|
||||
/>
|
||||
|
||||
<!-- Workbench Configuration -->
|
||||
<meta id="vscode-workbench-web-configuration" data-settings="{{WORKBENCH_WEB_CONFIGURATION}}" />
|
||||
|
||||
<!-- Workarounds/Hacks (remote user data uri) -->
|
||||
<meta id="vscode-remote-user-data-uri" data-settings="{{REMOTE_USER_DATA_URI}}" />
|
||||
<meta id="vscode-remote-product-configuration" data-settings="{{PRODUCT_CONFIGURATION}}" />
|
||||
<meta id="vscode-remote-nls-configuration" data-settings="{{NLS_CONFIGURATION}}" />
|
||||
|
||||
<!-- Workbench Icon/Manifest/CSS -->
|
||||
<link rel="icon" href="{{CS_STATIC_BASE}}/src/browser/media/favicon-dark-support.svg" />
|
||||
<link rel="alternate icon" href="{{CS_STATIC_BASE}}/src/browser/media/favicon.ico" />
|
||||
<link rel="manifest" href="{{CS_STATIC_BASE}}/src/browser/media/manifest.json" crossorigin="use-credentials" />
|
||||
<!-- PROD_ONLY
|
||||
<link data-name="vs/workbench/workbench.web.api" rel="stylesheet" href="{{CS_STATIC_BASE}}/vendor/modules/code-oss-dev/out/vs/workbench/workbench.web.api.css">
|
||||
END_PROD_ONLY -->
|
||||
<link rel="apple-touch-icon" sizes="192x192" href="{{CS_STATIC_BASE}}/src/browser/media/pwa-icon-192.png" />
|
||||
<link rel="apple-touch-icon" sizes="512x512" href="{{CS_STATIC_BASE}}/src/browser/media/pwa-icon-512.png" />
|
||||
<meta name="apple-mobile-web-app-capable" content="yes" />
|
||||
|
||||
<meta id="coder-options" data-settings="{{OPTIONS}}" />
|
||||
</head>
|
||||
|
||||
<body aria-label=""></body>
|
||||
|
||||
<!-- Startup (do not modify order of script tags!) -->
|
||||
<script data-cfasync="false" src="{{CS_STATIC_BASE}}/out/browser/pages/vscode.browserified.js"></script>
|
||||
<script data-cfasync="false" src="{{CS_STATIC_BASE}}/vendor/modules/code-oss-dev/out/vs/loader.js"></script>
|
||||
<script>
|
||||
performance.mark("code/willLoadWorkbenchMain")
|
||||
</script>
|
||||
<!-- PROD_ONLY
|
||||
<script data-cfasync="false" src="{{CS_STATIC_BASE}}/vendor/modules/code-oss-dev/out/vs/workbench/workbench.web.api.nls.js"></script>
|
||||
<script data-cfasync="false" src="{{CS_STATIC_BASE}}/vendor/modules/code-oss-dev/out/vs/workbench/workbench.web.api.js"></script>
|
||||
END_PROD_ONLY -->
|
||||
<script>
|
||||
require(["vs/code/browser/workbench/workbench"], function () {})
|
||||
</script>
|
||||
</html>
|
||||
@@ -1,253 +0,0 @@
|
||||
import { getOptions, Options } from "../../common/util"
|
||||
import "../register"
|
||||
|
||||
// TODO@jsjoeio: Add proper types.
|
||||
type FixMeLater = any
|
||||
|
||||
// NOTE@jsjoeio
|
||||
// This lives here ../../../lib/vscode/src/vs/base/common/platform.ts#L106
|
||||
export const nlsConfigElementId = "vscode-remote-nls-configuration"
|
||||
|
||||
type NlsConfiguration = {
|
||||
locale: string
|
||||
availableLanguages: { [key: string]: string } | {}
|
||||
_languagePackId?: string
|
||||
_translationsConfigFile?: string
|
||||
_cacheRoot?: string
|
||||
_resolvedLanguagePackCoreLocation?: string
|
||||
_corruptedFile?: string
|
||||
_languagePackSupport?: boolean
|
||||
loadBundle?: FixMeLater
|
||||
}
|
||||
|
||||
/**
|
||||
* Helper function to create the path to the bundle
|
||||
* for getNlsConfiguration.
|
||||
*/
|
||||
export function createBundlePath(_resolvedLanguagePackCoreLocation: string | undefined, bundle: string) {
|
||||
// NOTE@jsjoeio - this comment was here before me
|
||||
// Refers to operating systems that use a different path separator.
|
||||
// Probably just Windows but we're not sure if "/" breaks on Windows
|
||||
// so we'll leave it alone for now.
|
||||
// FIXME: Only works if path separators are /.
|
||||
return (_resolvedLanguagePackCoreLocation || "") + "/" + bundle.replace(/\//g, "!") + ".nls.json"
|
||||
}
|
||||
|
||||
/**
|
||||
* A helper function to get the NLS Configuration settings.
|
||||
*
|
||||
* This is used by VSCode for localizations (i.e. changing
|
||||
* the display language).
|
||||
*
|
||||
* Make sure to wrap this in a try/catch block when you call it.
|
||||
**/
|
||||
export function getNlsConfiguration(_document: Document, base: string) {
|
||||
const errorMsgPrefix = "[vscode]"
|
||||
const nlsConfigElement = _document?.getElementById(nlsConfigElementId)
|
||||
const dataSettings = nlsConfigElement?.getAttribute("data-settings")
|
||||
|
||||
if (!nlsConfigElement) {
|
||||
throw new Error(
|
||||
`${errorMsgPrefix} Could not parse NLS configuration. Could not find nlsConfigElement with id: ${nlsConfigElementId}`,
|
||||
)
|
||||
}
|
||||
|
||||
if (!dataSettings) {
|
||||
throw new Error(
|
||||
`${errorMsgPrefix} Could not parse NLS configuration. Found nlsConfigElement but missing data-settings attribute.`,
|
||||
)
|
||||
}
|
||||
|
||||
const nlsConfig = JSON.parse(dataSettings) as NlsConfiguration
|
||||
|
||||
if (nlsConfig._resolvedLanguagePackCoreLocation) {
|
||||
// NOTE@jsjoeio
|
||||
// Not sure why we use Object.create(null) instead of {}
|
||||
// They are not the same
|
||||
// See: https://stackoverflow.com/a/15518712/3015595
|
||||
// We copied this from ../../../lib/vscode/src/bootstrap.js#L143
|
||||
const bundles: {
|
||||
[key: string]: string
|
||||
} = Object.create(null)
|
||||
|
||||
type LoadBundleCallback = (_: undefined, result?: string) => void
|
||||
|
||||
nlsConfig.loadBundle = async (bundle: string, _language: string, cb: LoadBundleCallback): Promise<void> => {
|
||||
const result = bundles[bundle]
|
||||
|
||||
if (result) {
|
||||
return cb(undefined, result)
|
||||
}
|
||||
|
||||
try {
|
||||
const path = createBundlePath(nlsConfig._resolvedLanguagePackCoreLocation, bundle)
|
||||
const response = await fetch(`${base}/vscode/resource/?path=${encodeURIComponent(path)}`)
|
||||
const json = await response.json()
|
||||
bundles[bundle] = json
|
||||
return cb(undefined, json)
|
||||
} catch (error) {
|
||||
return cb(error)
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
return nlsConfig
|
||||
}
|
||||
|
||||
type GetLoaderParams = {
|
||||
nlsConfig: NlsConfiguration
|
||||
options: Options
|
||||
_window: Window
|
||||
}
|
||||
|
||||
/**
|
||||
* Link to types in the loader source repo
|
||||
* https://github.com/microsoft/vscode-loader/blob/main/src/loader.d.ts#L280
|
||||
*/
|
||||
type Loader = {
|
||||
baseUrl: string
|
||||
recordStats: boolean
|
||||
// TODO@jsjoeio: There don't appear to be any types for trustedTypes yet.
|
||||
trustedTypesPolicy: FixMeLater
|
||||
paths: {
|
||||
[key: string]: string
|
||||
}
|
||||
"vs/nls": NlsConfiguration
|
||||
}
|
||||
|
||||
/**
|
||||
* A helper function which creates a script url if the value
|
||||
* is valid.
|
||||
*
|
||||
* Extracted into a function to make it easier to test
|
||||
*/
|
||||
export function _createScriptURL(value: string, origin: string): string {
|
||||
if (value.startsWith(origin)) {
|
||||
return value
|
||||
}
|
||||
throw new Error(`Invalid script url: ${value}`)
|
||||
}
|
||||
|
||||
/**
|
||||
* A helper function to get the require loader
|
||||
*
|
||||
* This used by VSCode/code-server
|
||||
* to load files.
|
||||
*
|
||||
* We extracted the logic into a function so that
|
||||
* it's easier to test.
|
||||
**/
|
||||
export function getConfigurationForLoader({ nlsConfig, options, _window }: GetLoaderParams) {
|
||||
const loader: Loader = {
|
||||
// Without the full URL VS Code will try to load file://.
|
||||
baseUrl: `${window.location.origin}${options.csStaticBase}/vendor/modules/code-oss-dev/out`,
|
||||
recordStats: true,
|
||||
trustedTypesPolicy: (_window as FixMeLater).trustedTypes?.createPolicy("amdLoader", {
|
||||
createScriptURL(value: string): string {
|
||||
return _createScriptURL(value, window.location.origin)
|
||||
},
|
||||
}),
|
||||
paths: {
|
||||
"vscode-textmate": `../node_modules/vscode-textmate/release/main`,
|
||||
"vscode-oniguruma": `../node_modules/vscode-oniguruma/release/main`,
|
||||
xterm: `../node_modules/xterm/lib/xterm.js`,
|
||||
"xterm-addon-search": `../node_modules/xterm-addon-search/lib/xterm-addon-search.js`,
|
||||
"xterm-addon-unicode11": `../node_modules/xterm-addon-unicode11/lib/xterm-addon-unicode11.js`,
|
||||
"xterm-addon-webgl": `../node_modules/xterm-addon-webgl/lib/xterm-addon-webgl.js`,
|
||||
"tas-client-umd": `../node_modules/tas-client-umd/lib/tas-client-umd.js`,
|
||||
"iconv-lite-umd": `../node_modules/iconv-lite-umd/lib/iconv-lite-umd.js`,
|
||||
jschardet: `../node_modules/jschardet/dist/jschardet.min.js`,
|
||||
},
|
||||
"vs/nls": nlsConfig,
|
||||
}
|
||||
|
||||
return loader
|
||||
}
|
||||
|
||||
/**
|
||||
* Sets the body background color to match the theme.
|
||||
*/
|
||||
export function setBodyBackgroundToThemeBackgroundColor(_document: Document, _localStorage: Storage) {
|
||||
const errorMsgPrefix = "[vscode]"
|
||||
const colorThemeData = _localStorage.getItem("colorThemeData")
|
||||
|
||||
if (!colorThemeData) {
|
||||
throw new Error(
|
||||
`${errorMsgPrefix} Could not set body background to theme background color. Could not find colorThemeData in localStorage.`,
|
||||
)
|
||||
}
|
||||
|
||||
let _colorThemeData
|
||||
try {
|
||||
// We wrap this JSON.parse logic in a try/catch
|
||||
// because it can throw if the JSON is invalid.
|
||||
// and instead of throwing a random error
|
||||
// we can throw our own error, which will be more helpful
|
||||
// to the end user.
|
||||
_colorThemeData = JSON.parse(colorThemeData)
|
||||
} catch {
|
||||
throw new Error(
|
||||
`${errorMsgPrefix} Could not set body background to theme background color. Could not parse colorThemeData from localStorage.`,
|
||||
)
|
||||
}
|
||||
|
||||
const hasColorMapProperty = Object.prototype.hasOwnProperty.call(_colorThemeData, "colorMap")
|
||||
if (!hasColorMapProperty) {
|
||||
throw new Error(
|
||||
`${errorMsgPrefix} Could not set body background to theme background color. colorThemeData is missing colorMap.`,
|
||||
)
|
||||
}
|
||||
|
||||
const editorBgColor = _colorThemeData.colorMap["editor.background"]
|
||||
|
||||
if (!editorBgColor) {
|
||||
throw new Error(
|
||||
`${errorMsgPrefix} Could not set body background to theme background color. colorThemeData.colorMap["editor.background"] is undefined.`,
|
||||
)
|
||||
}
|
||||
|
||||
_document.body.style.background = editorBgColor
|
||||
|
||||
return null
|
||||
}
|
||||
|
||||
/**
|
||||
* A helper function to encapsulate all the
|
||||
* logic used in this file.
|
||||
*
|
||||
* We purposely include all of this in a single function
|
||||
* so that it's easier to test.
|
||||
*/
|
||||
export function main(_document: Document | undefined, _window: Window | undefined, _localStorage: Storage | undefined) {
|
||||
if (!_document) {
|
||||
throw new Error(`document is undefined.`)
|
||||
}
|
||||
|
||||
if (!_window) {
|
||||
throw new Error(`window is undefined.`)
|
||||
}
|
||||
|
||||
if (!_localStorage) {
|
||||
throw new Error(`localStorage is undefined.`)
|
||||
}
|
||||
|
||||
const options = getOptions()
|
||||
const nlsConfig = getNlsConfiguration(_document, options.base)
|
||||
|
||||
const loader = getConfigurationForLoader({
|
||||
nlsConfig,
|
||||
options,
|
||||
_window,
|
||||
})
|
||||
|
||||
;(self.require as unknown as Loader) = loader
|
||||
|
||||
setBodyBackgroundToThemeBackgroundColor(_document, _localStorage)
|
||||
}
|
||||
|
||||
try {
|
||||
main(document, window, localStorage)
|
||||
} catch (error) {
|
||||
console.error("[vscode] failed to initialize VS Code")
|
||||
console.error(error)
|
||||
}
|
||||
@@ -1,23 +0,0 @@
|
||||
import { logger } from "@coder/logger"
|
||||
import { getOptions, normalize, logError } from "../common/util"
|
||||
|
||||
export async function registerServiceWorker(): Promise<void> {
|
||||
const options = getOptions()
|
||||
logger.level = options.logLevel
|
||||
|
||||
const path = normalize(`${options.csStaticBase}/out/browser/serviceWorker.js`)
|
||||
try {
|
||||
await navigator.serviceWorker.register(path, {
|
||||
scope: options.base + "/",
|
||||
})
|
||||
logger.info(`[Service Worker] registered`)
|
||||
} catch (error) {
|
||||
logError(logger, `[Service Worker] registration`, error)
|
||||
}
|
||||
}
|
||||
|
||||
if (typeof navigator !== "undefined" && "serviceWorker" in navigator) {
|
||||
registerServiceWorker()
|
||||
} else {
|
||||
logger.error(`[Service Worker] navigator is undefined`)
|
||||
}
|
||||
@@ -1,14 +0,0 @@
|
||||
/* eslint-disable @typescript-eslint/no-explicit-any */
|
||||
|
||||
self.addEventListener("install", () => {
|
||||
console.log("[Service Worker] installed")
|
||||
})
|
||||
|
||||
self.addEventListener("activate", (event: any) => {
|
||||
event.waitUntil((self as any).clients.claim())
|
||||
console.log("[Service Worker] activated")
|
||||
})
|
||||
|
||||
self.addEventListener("fetch", () => {
|
||||
// Without this event handler we won't be recognized as a PWA.
|
||||
})
|
||||
@@ -7,7 +7,7 @@ import { logger } from "@coder/logger"
|
||||
export type Callback<T, R = void | Promise<void>> = (t: T, p: Promise<void>) => R
|
||||
|
||||
export interface Disposable {
|
||||
dispose(): void
|
||||
dispose(): void | Promise<void>
|
||||
}
|
||||
|
||||
export interface Event<T> {
|
||||
@@ -46,7 +46,7 @@ export class Emitter<T> {
|
||||
this.listeners.map(async (cb) => {
|
||||
try {
|
||||
await cb(value, promise)
|
||||
} catch (error) {
|
||||
} catch (error: any) {
|
||||
logger.error(error.message)
|
||||
}
|
||||
}),
|
||||
|
||||
@@ -13,8 +13,12 @@ export enum HttpCode {
|
||||
* used in the HTTP response.
|
||||
*/
|
||||
export class HttpError extends Error {
|
||||
public constructor(message: string, public readonly status: HttpCode, public readonly details?: object) {
|
||||
public constructor(message: string, public readonly statusCode: HttpCode, public readonly details?: object) {
|
||||
super(message)
|
||||
this.name = this.constructor.name
|
||||
}
|
||||
}
|
||||
|
||||
export enum CookieKeys {
|
||||
Session = "code-server-session",
|
||||
}
|
||||
|
||||
@@ -1,19 +1,3 @@
|
||||
/*
|
||||
* This file exists in two locations:
|
||||
* - src/common/util.ts
|
||||
* - lib/vscode/src/vs/server/common/util.ts
|
||||
* The second is a symlink to the first.
|
||||
*/
|
||||
|
||||
/**
|
||||
* Base options included on every page.
|
||||
*/
|
||||
export interface Options {
|
||||
base: string
|
||||
csStaticBase: string
|
||||
logLevel: number
|
||||
}
|
||||
|
||||
/**
|
||||
* Split a string up to the delimiter. If the delimiter doesn't exist the first
|
||||
* item will have all the text and the second item will be an empty string.
|
||||
@@ -39,6 +23,12 @@ export const generateUuid = (length = 24): string => {
|
||||
|
||||
/**
|
||||
* Remove extra slashes in a URL.
|
||||
*
|
||||
* This is meant to fill the job of `path.join` so you can concatenate paths and
|
||||
* then normalize out any extra slashes.
|
||||
*
|
||||
* If you are using `path.join` you do not need this but note that `path` is for
|
||||
* file system paths, not URLs.
|
||||
*/
|
||||
export const normalize = (url: string, keepTrailing = false): string => {
|
||||
return url.replace(/\/\/+/g, "/").replace(/\/+$/, keepTrailing ? "/" : "")
|
||||
@@ -51,50 +41,6 @@ export const trimSlashes = (url: string): string => {
|
||||
return url.replace(/^\/+|\/+$/g, "")
|
||||
}
|
||||
|
||||
/**
|
||||
* Resolve a relative base against the window location. This is used for
|
||||
* anything that doesn't work with a relative path.
|
||||
*/
|
||||
export const resolveBase = (base?: string): string => {
|
||||
// After resolving the base will either start with / or be an empty string.
|
||||
if (!base || base.startsWith("/")) {
|
||||
return base ?? ""
|
||||
}
|
||||
const parts = location.pathname.split("/")
|
||||
parts[parts.length - 1] = base
|
||||
const url = new URL(location.origin + "/" + parts.join("/"))
|
||||
return normalize(url.pathname)
|
||||
}
|
||||
|
||||
/**
|
||||
* Get options embedded in the HTML or query params.
|
||||
*/
|
||||
export const getOptions = <T extends Options>(): T => {
|
||||
let options: T
|
||||
try {
|
||||
options = JSON.parse(document.getElementById("coder-options")!.getAttribute("data-settings")!)
|
||||
} catch (error) {
|
||||
options = {} as T
|
||||
}
|
||||
|
||||
// You can also pass options in stringified form to the options query
|
||||
// variable. Options provided here will override the ones in the options
|
||||
// element.
|
||||
const params = new URLSearchParams(location.search)
|
||||
const queryOpts = params.get("options")
|
||||
if (queryOpts) {
|
||||
options = {
|
||||
...options,
|
||||
...JSON.parse(queryOpts),
|
||||
}
|
||||
}
|
||||
|
||||
options.base = resolveBase(options.base)
|
||||
options.csStaticBase = resolveBase(options.csStaticBase)
|
||||
|
||||
return options
|
||||
}
|
||||
|
||||
/**
|
||||
* Wrap the value in an array if it's not already an array. If the value is
|
||||
* undefined return an empty array.
|
||||
@@ -109,19 +55,8 @@ export const arrayify = <T>(value?: T | T[]): T[] => {
|
||||
return [value]
|
||||
}
|
||||
|
||||
/**
|
||||
* Get the first string. If there's no string return undefined.
|
||||
*/
|
||||
export const getFirstString = (value: string | string[] | object | undefined): string | undefined => {
|
||||
if (Array.isArray(value)) {
|
||||
return value[0]
|
||||
}
|
||||
|
||||
return typeof value === "string" ? value : undefined
|
||||
}
|
||||
|
||||
// TODO: Might make sense to add Error handling to the logger itself.
|
||||
export function logError(logger: { error: (msg: string) => void }, prefix: string, err: Error | string): void {
|
||||
export function logError(logger: { error: (msg: string) => void }, prefix: string, err: unknown): void {
|
||||
if (err instanceof Error) {
|
||||
logger.error(`${prefix}: ${err.message} ${err.stack}`)
|
||||
} else {
|
||||
|
||||
138
src/node/app.ts
138
src/node/app.ts
@@ -4,17 +4,57 @@ import express, { Express } from "express"
|
||||
import { promises as fs } from "fs"
|
||||
import http from "http"
|
||||
import * as httpolyglot from "httpolyglot"
|
||||
import { Disposable } from "../common/emitter"
|
||||
import * as util from "../common/util"
|
||||
import { DefaultedArgs } from "./cli"
|
||||
import { disposer } from "./http"
|
||||
import { isNodeJSErrnoException } from "./util"
|
||||
import { handleUpgrade } from "./wsRouter"
|
||||
|
||||
type ListenOptions = Pick<DefaultedArgs, "socket" | "port" | "host">
|
||||
|
||||
export interface App extends Disposable {
|
||||
/** Handles regular HTTP requests. */
|
||||
router: Express
|
||||
/** Handles websocket requests. */
|
||||
wsRouter: Express
|
||||
/** The underlying HTTP server. */
|
||||
server: http.Server
|
||||
}
|
||||
|
||||
const listen = (server: http.Server, { host, port, socket }: ListenOptions) => {
|
||||
return new Promise<void>(async (resolve, reject) => {
|
||||
server.on("error", reject)
|
||||
|
||||
const onListen = () => {
|
||||
// Promise resolved earlier so this is an unrelated error.
|
||||
server.off("error", reject)
|
||||
server.on("error", (err) => util.logError(logger, "http server error", err))
|
||||
|
||||
resolve()
|
||||
}
|
||||
|
||||
if (socket) {
|
||||
try {
|
||||
await fs.unlink(socket)
|
||||
} catch (error: any) {
|
||||
handleArgsSocketCatchError(error)
|
||||
}
|
||||
|
||||
server.listen(socket, onListen)
|
||||
} else {
|
||||
// [] is the correct format when using :: but Node errors with them.
|
||||
server.listen(port, host.replace(/^\[|\]$/g, ""), onListen)
|
||||
}
|
||||
})
|
||||
}
|
||||
|
||||
/**
|
||||
* Create an Express app and an HTTP/S server to serve it.
|
||||
*/
|
||||
export const createApp = async (args: DefaultedArgs): Promise<[Express, Express, http.Server]> => {
|
||||
const app = express()
|
||||
|
||||
app.use(compression())
|
||||
export const createApp = async (args: DefaultedArgs): Promise<App> => {
|
||||
const router = express()
|
||||
router.use(compression())
|
||||
|
||||
const server = args.cert
|
||||
? httpolyglot.createServer(
|
||||
@@ -22,57 +62,73 @@ export const createApp = async (args: DefaultedArgs): Promise<[Express, Express,
|
||||
cert: args.cert && (await fs.readFile(args.cert.value)),
|
||||
key: args["cert-key"] && (await fs.readFile(args["cert-key"])),
|
||||
},
|
||||
app,
|
||||
router,
|
||||
)
|
||||
: http.createServer(app)
|
||||
: http.createServer(router)
|
||||
|
||||
let resolved = false
|
||||
await new Promise<void>(async (resolve2, reject) => {
|
||||
const resolve = () => {
|
||||
resolved = true
|
||||
resolve2()
|
||||
}
|
||||
server.on("error", (err) => {
|
||||
if (!resolved) {
|
||||
reject(err)
|
||||
} else {
|
||||
// Promise resolved earlier so this is an unrelated error.
|
||||
util.logError(logger, "http server error", err)
|
||||
}
|
||||
})
|
||||
const dispose = disposer(server)
|
||||
|
||||
if (args.socket) {
|
||||
try {
|
||||
await fs.unlink(args.socket)
|
||||
} catch (error) {
|
||||
if (error.code !== "ENOENT") {
|
||||
logger.error(error.message)
|
||||
}
|
||||
}
|
||||
server.listen(args.socket, resolve)
|
||||
} else {
|
||||
// [] is the correct format when using :: but Node errors with them.
|
||||
server.listen(args.port, args.host.replace(/^\[|\]$/g, ""), resolve)
|
||||
}
|
||||
})
|
||||
await listen(server, args)
|
||||
|
||||
const wsApp = express()
|
||||
handleUpgrade(wsApp, server)
|
||||
const wsRouter = express()
|
||||
handleUpgrade(wsRouter, server)
|
||||
|
||||
return [app, wsApp, server]
|
||||
return { router, wsRouter, server, dispose }
|
||||
}
|
||||
|
||||
/**
|
||||
* Get the address of a server as a string (protocol *is* included) while
|
||||
* ensuring there is one (will throw if there isn't).
|
||||
*
|
||||
* The address might be a URL or it might be a pipe or socket path.
|
||||
*/
|
||||
export const ensureAddress = (server: http.Server): string => {
|
||||
export const ensureAddress = (server: http.Server, protocol: string): URL | string => {
|
||||
const addr = server.address()
|
||||
|
||||
if (!addr) {
|
||||
throw new Error("server has no address") // NOTE@jsjoeio test this line
|
||||
throw new Error("Server has no address")
|
||||
}
|
||||
|
||||
if (typeof addr !== "string") {
|
||||
return `http://${addr.address}:${addr.port}`
|
||||
return new URL(`${protocol}://${addr.address}:${addr.port}`)
|
||||
}
|
||||
|
||||
// If this is a string then it is a pipe or Unix socket.
|
||||
return addr
|
||||
}
|
||||
|
||||
/**
|
||||
* Handles error events from the server.
|
||||
*
|
||||
* If the outlying Promise didn't resolve
|
||||
* then we reject with the error.
|
||||
*
|
||||
* Otherwise, we log the error.
|
||||
*
|
||||
* We extracted into a function so that we could
|
||||
* test this logic more easily.
|
||||
*/
|
||||
export const handleServerError = (resolved: boolean, err: Error, reject: (err: Error) => void) => {
|
||||
// Promise didn't resolve earlier so this means it's an error
|
||||
// that occurs before the server can successfully listen.
|
||||
// Possibly triggered by listening on an invalid port or socket.
|
||||
if (!resolved) {
|
||||
reject(err)
|
||||
} else {
|
||||
// Promise resolved earlier so this is an unrelated error.
|
||||
util.logError(logger, "http server error", err)
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* Handles the error that occurs in the catch block
|
||||
* after we try fs.unlink(args.socket).
|
||||
*
|
||||
* We extracted into a function so that we could
|
||||
* test this logic more easily.
|
||||
*/
|
||||
export const handleArgsSocketCatchError = (error: any) => {
|
||||
if (!isNodeJSErrnoException(error) || error.code !== "ENOENT") {
|
||||
logger.error(error.message ? error.message : error)
|
||||
}
|
||||
return addr // NOTE@jsjoeio test this line
|
||||
}
|
||||
|
||||
226
src/node/cli.ts
226
src/node/cli.ts
@@ -3,12 +3,21 @@ import { promises as fs } from "fs"
|
||||
import yaml from "js-yaml"
|
||||
import * as os from "os"
|
||||
import * as path from "path"
|
||||
import { Args as VsArgs } from "../../typings/ipc"
|
||||
import { canConnect, generateCertificate, generatePassword, humanPath, paths } from "./util"
|
||||
import {
|
||||
canConnect,
|
||||
generateCertificate,
|
||||
generatePassword,
|
||||
humanPath,
|
||||
paths,
|
||||
isNodeJSErrnoException,
|
||||
isFile,
|
||||
} from "./util"
|
||||
|
||||
const DEFAULT_SOCKET_PATH = path.join(os.tmpdir(), "vscode-ipc")
|
||||
|
||||
export enum Feature {
|
||||
/** Web socket compression. */
|
||||
PermessageDeflate = "permessage-deflate",
|
||||
// No current experimental features!
|
||||
Placeholder = "placeholder",
|
||||
}
|
||||
|
||||
export enum AuthType {
|
||||
@@ -30,7 +39,13 @@ export enum LogLevel {
|
||||
|
||||
export class OptionalString extends Optional<string> {}
|
||||
|
||||
export interface Args extends VsArgs {
|
||||
/**
|
||||
* Arguments that the user explicitly provided on the command line. All
|
||||
* arguments must be optional.
|
||||
*
|
||||
* For arguments with defaults see DefaultedArgs.
|
||||
*/
|
||||
export interface UserProvidedArgs {
|
||||
config?: string
|
||||
auth?: AuthType
|
||||
password?: string
|
||||
@@ -38,30 +53,39 @@ export interface Args extends VsArgs {
|
||||
cert?: OptionalString
|
||||
"cert-host"?: string
|
||||
"cert-key"?: string
|
||||
"disable-telemetry"?: boolean
|
||||
"disable-update-check"?: boolean
|
||||
enable?: string[]
|
||||
help?: boolean
|
||||
host?: string
|
||||
port?: number
|
||||
json?: boolean
|
||||
log?: LogLevel
|
||||
open?: boolean
|
||||
port?: number
|
||||
"bind-addr"?: string
|
||||
socket?: string
|
||||
version?: boolean
|
||||
force?: boolean
|
||||
"list-extensions"?: boolean
|
||||
"install-extension"?: string[]
|
||||
"show-versions"?: boolean
|
||||
"uninstall-extension"?: string[]
|
||||
"proxy-domain"?: string[]
|
||||
locale?: string
|
||||
_: string[]
|
||||
"reuse-window"?: boolean
|
||||
"new-window"?: boolean
|
||||
|
||||
"ignore-last-opened"?: boolean
|
||||
link?: OptionalString
|
||||
verbose?: boolean
|
||||
/* Positional arguments. */
|
||||
_?: string[]
|
||||
|
||||
// VS Code flags.
|
||||
"disable-telemetry"?: boolean
|
||||
force?: boolean
|
||||
"user-data-dir"?: string
|
||||
"enable-proposed-api"?: string[]
|
||||
"extensions-dir"?: string
|
||||
"builtin-extensions-dir"?: string
|
||||
"install-extension"?: string[]
|
||||
"uninstall-extension"?: string[]
|
||||
"list-extensions"?: boolean
|
||||
"locate-extension"?: string[]
|
||||
"show-versions"?: boolean
|
||||
category?: string
|
||||
}
|
||||
|
||||
interface Option<T> {
|
||||
@@ -80,9 +104,9 @@ interface Option<T> {
|
||||
description?: string
|
||||
|
||||
/**
|
||||
* If marked as beta, the option is marked as beta in help.
|
||||
* If marked as deprecated, the option is marked as deprecated in help.
|
||||
*/
|
||||
beta?: boolean
|
||||
deprecated?: boolean
|
||||
}
|
||||
|
||||
type OptionType<T> = T extends boolean
|
||||
@@ -105,7 +129,7 @@ type Options<T> = {
|
||||
[P in keyof T]: Option<OptionType<T[P]>>
|
||||
}
|
||||
|
||||
const options: Options<Required<Args>> = {
|
||||
const options: Options<Required<UserProvidedArgs>> = {
|
||||
auth: { type: AuthType, description: "The type of authentication to use." },
|
||||
password: {
|
||||
type: "string",
|
||||
@@ -162,10 +186,10 @@ const options: Options<Required<Args>> = {
|
||||
"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 },
|
||||
"extra-extensions-dir": { type: "string[]", path: true },
|
||||
"extra-builtin-extensions-dir": { type: "string[]", path: true },
|
||||
"list-extensions": { type: "boolean", description: "List installed VS Code extensions." },
|
||||
force: { type: "boolean", description: "Avoid prompts when installing VS Code extensions." },
|
||||
"locate-extension": { type: "string[]" },
|
||||
category: { type: "string" },
|
||||
"install-extension": {
|
||||
type: "string[]",
|
||||
description:
|
||||
@@ -196,7 +220,6 @@ const options: Options<Required<Args>> = {
|
||||
description: "Force to open a file or folder in an already opened window.",
|
||||
},
|
||||
|
||||
locale: { type: "string" },
|
||||
log: { type: LogLevel },
|
||||
verbose: { type: "boolean", short: "vvv", description: "Enable verbose logging." },
|
||||
|
||||
@@ -207,7 +230,7 @@ const options: Options<Required<Args>> = {
|
||||
https://hostname-username.cdr.co at which you can easily access your code-server instance.
|
||||
Authorization is done via GitHub.
|
||||
`,
|
||||
beta: true,
|
||||
deprecated: true,
|
||||
},
|
||||
}
|
||||
|
||||
@@ -230,7 +253,7 @@ export const optionDescriptions = (): string[] => {
|
||||
.map((line, i) => {
|
||||
line = line.trim()
|
||||
if (i === 0) {
|
||||
return " ".repeat(widths.long - k.length) + (v.beta ? "(beta) " : "") + line
|
||||
return " ".repeat(widths.long - k.length) + (v.deprecated ? "(deprecated) " : "") + line
|
||||
}
|
||||
return " ".repeat(widths.long + widths.short + 6) + line
|
||||
})
|
||||
@@ -253,12 +276,16 @@ export function splitOnFirstEquals(str: string): string[] {
|
||||
return split
|
||||
}
|
||||
|
||||
/**
|
||||
* Parse arguments into UserProvidedArgs. This should not go beyond checking
|
||||
* that arguments are valid types and have values when required.
|
||||
*/
|
||||
export const parse = (
|
||||
argv: string[],
|
||||
opts?: {
|
||||
configFile?: string
|
||||
},
|
||||
): Args => {
|
||||
): UserProvidedArgs => {
|
||||
const error = (msg: string): Error => {
|
||||
if (opts?.configFile) {
|
||||
msg = `error reading ${opts.configFile}: ${msg}`
|
||||
@@ -267,7 +294,7 @@ export const parse = (
|
||||
return new Error(msg)
|
||||
}
|
||||
|
||||
const args: Args = { _: [] }
|
||||
const args: UserProvidedArgs = {}
|
||||
let ended = false
|
||||
|
||||
for (let i = 0; i < argv.length; ++i) {
|
||||
@@ -281,17 +308,17 @@ export const parse = (
|
||||
|
||||
// Options start with a dash and require a value if non-boolean.
|
||||
if (!ended && arg.startsWith("-")) {
|
||||
let key: keyof Args | undefined
|
||||
let key: keyof UserProvidedArgs | undefined
|
||||
let value: string | undefined
|
||||
if (arg.startsWith("--")) {
|
||||
const split = splitOnFirstEquals(arg.replace(/^--/, ""))
|
||||
key = split[0] as keyof Args
|
||||
key = split[0] as keyof UserProvidedArgs
|
||||
value = split[1]
|
||||
} else {
|
||||
const short = arg.replace(/^-/, "")
|
||||
const pair = Object.entries(options).find(([, v]) => v.short === short)
|
||||
if (pair) {
|
||||
key = pair[0] as keyof Args
|
||||
key = pair[0] as keyof UserProvidedArgs
|
||||
}
|
||||
}
|
||||
|
||||
@@ -366,6 +393,10 @@ export const parse = (
|
||||
}
|
||||
|
||||
// Everything else goes into _.
|
||||
if (typeof args._ === "undefined") {
|
||||
args._ = []
|
||||
}
|
||||
|
||||
args._.push(arg)
|
||||
}
|
||||
|
||||
@@ -374,11 +405,19 @@ export const parse = (
|
||||
throw new Error("--cert-key is missing")
|
||||
}
|
||||
|
||||
logger.debug(() => ["parsed command line", field("args", { ...args, password: undefined })])
|
||||
logger.debug(() => [
|
||||
`parsed ${opts?.configFile ? "config" : "command line"}`,
|
||||
field("args", { ...args, password: undefined }),
|
||||
])
|
||||
|
||||
return args
|
||||
}
|
||||
|
||||
/**
|
||||
* User-provided arguments with defaults. The distinction between user-provided
|
||||
* args and defaulted args exists so we can tell the difference between end
|
||||
* values and what the user actually provided on the command line.
|
||||
*/
|
||||
export interface DefaultedArgs extends ConfigArgs {
|
||||
auth: AuthType
|
||||
cert?: {
|
||||
@@ -392,6 +431,8 @@ export interface DefaultedArgs extends ConfigArgs {
|
||||
usingEnvHashedPassword: boolean
|
||||
"extensions-dir": string
|
||||
"user-data-dir": string
|
||||
/* Positional arguments. */
|
||||
_: []
|
||||
}
|
||||
|
||||
/**
|
||||
@@ -399,7 +440,7 @@ export interface DefaultedArgs extends ConfigArgs {
|
||||
* with the defaults set. Arguments from the CLI are prioritized over config
|
||||
* arguments.
|
||||
*/
|
||||
export async function setDefaults(cliArgs: Args, configArgs?: ConfigArgs): Promise<DefaultedArgs> {
|
||||
export async function setDefaults(cliArgs: UserProvidedArgs, configArgs?: ConfigArgs): Promise<DefaultedArgs> {
|
||||
const args = Object.assign({}, configArgs || {}, cliArgs)
|
||||
|
||||
if (!args["user-data-dir"]) {
|
||||
@@ -454,7 +495,7 @@ export async function setDefaults(cliArgs: Args, configArgs?: ConfigArgs): Promi
|
||||
args.auth = AuthType.Password
|
||||
}
|
||||
|
||||
const addr = bindAddrFromAllSources(configArgs || { _: [] }, cliArgs)
|
||||
const addr = bindAddrFromAllSources(configArgs || {}, cliArgs)
|
||||
args.host = addr.host
|
||||
args.port = addr.port
|
||||
|
||||
@@ -495,6 +536,10 @@ export async function setDefaults(cliArgs: Args, configArgs?: ConfigArgs): Promi
|
||||
const proxyDomains = new Set((args["proxy-domain"] || []).map((d) => d.replace(/^\*\./, "")))
|
||||
args["proxy-domain"] = Array.from(proxyDomains)
|
||||
|
||||
if (typeof args._ === "undefined") {
|
||||
args._ = []
|
||||
}
|
||||
|
||||
return {
|
||||
...args,
|
||||
usingEnvPassword,
|
||||
@@ -502,15 +547,26 @@ export async function setDefaults(cliArgs: Args, configArgs?: ConfigArgs): Promi
|
||||
} as DefaultedArgs // TODO: Technically no guarantee this is fulfilled.
|
||||
}
|
||||
|
||||
async function defaultConfigFile(): Promise<string> {
|
||||
/**
|
||||
* Helper function to return the default config file.
|
||||
*
|
||||
* @param {string} password - Password passed in (usually from generatePassword())
|
||||
* @returns The default config file:
|
||||
*
|
||||
* - bind-addr: 127.0.0.1:8080
|
||||
* - auth: password
|
||||
* - password: <password>
|
||||
* - cert: false
|
||||
*/
|
||||
export function defaultConfigFile(password: string): string {
|
||||
return `bind-addr: 127.0.0.1:8080
|
||||
auth: password
|
||||
password: ${await generatePassword()}
|
||||
password: ${password}
|
||||
cert: false
|
||||
`
|
||||
}
|
||||
|
||||
interface ConfigArgs extends Args {
|
||||
interface ConfigArgs extends UserProvidedArgs {
|
||||
config: string
|
||||
}
|
||||
|
||||
@@ -530,11 +586,12 @@ export async function readConfigFile(configPath?: string): Promise<ConfigArgs> {
|
||||
await fs.mkdir(path.dirname(configPath), { recursive: true })
|
||||
|
||||
try {
|
||||
await fs.writeFile(configPath, await defaultConfigFile(), {
|
||||
const generatedPassword = await generatePassword()
|
||||
await fs.writeFile(configPath, defaultConfigFile(generatedPassword), {
|
||||
flag: "wx", // wx means to fail if the path exists.
|
||||
})
|
||||
logger.info(`Wrote default config file to ${humanPath(configPath)}`)
|
||||
} catch (error) {
|
||||
logger.info(`Wrote default config file to ${humanPath(os.homedir(), configPath)}`)
|
||||
} catch (error: any) {
|
||||
// EEXIST is fine; we don't want to overwrite existing configurations.
|
||||
if (error.code !== "EEXIST") {
|
||||
throw error
|
||||
@@ -551,7 +608,7 @@ export async function readConfigFile(configPath?: string): Promise<ConfigArgs> {
|
||||
*/
|
||||
export function parseConfigFile(configFile: string, configPath: string): ConfigArgs {
|
||||
if (!configFile) {
|
||||
return { _: [], config: configPath }
|
||||
return { config: configPath }
|
||||
}
|
||||
|
||||
const config = yaml.load(configFile, {
|
||||
@@ -594,7 +651,11 @@ interface Addr {
|
||||
port: number
|
||||
}
|
||||
|
||||
function bindAddrFromArgs(addr: Addr, args: Args): Addr {
|
||||
/**
|
||||
* This function creates the bind address
|
||||
* using the CLI args.
|
||||
*/
|
||||
export function bindAddrFromArgs(addr: Addr, args: UserProvidedArgs): Addr {
|
||||
addr = { ...addr }
|
||||
if (args["bind-addr"]) {
|
||||
addr = parseBindAddr(args["bind-addr"])
|
||||
@@ -612,7 +673,7 @@ function bindAddrFromArgs(addr: Addr, args: Args): Addr {
|
||||
return addr
|
||||
}
|
||||
|
||||
function bindAddrFromAllSources(...argsConfig: Args[]): Addr {
|
||||
function bindAddrFromAllSources(...argsConfig: UserProvidedArgs[]): Addr {
|
||||
let addr: Addr = {
|
||||
host: "localhost",
|
||||
port: 8080,
|
||||
@@ -625,51 +686,92 @@ function bindAddrFromAllSources(...argsConfig: Args[]): Addr {
|
||||
return addr
|
||||
}
|
||||
|
||||
export const shouldRunVsCodeCli = (args: Args): boolean => {
|
||||
return !!args["list-extensions"] || !!args["install-extension"] || !!args["uninstall-extension"]
|
||||
/**
|
||||
* Reads the socketPath based on path passed in.
|
||||
*
|
||||
* The one usually passed in is the DEFAULT_SOCKET_PATH.
|
||||
*
|
||||
* If it can't read the path, it throws an error and returns undefined.
|
||||
*/
|
||||
export async function readSocketPath(path: string): Promise<string | undefined> {
|
||||
try {
|
||||
return await fs.readFile(path, "utf8")
|
||||
} catch (error) {
|
||||
// If it doesn't exist, we don't care.
|
||||
// But if it fails for some reason, we should throw.
|
||||
// We want to surface that to the user.
|
||||
if (!isNodeJSErrnoException(error) || error.code !== "ENOENT") {
|
||||
throw error
|
||||
}
|
||||
}
|
||||
return undefined
|
||||
}
|
||||
|
||||
/**
|
||||
* Determine if it looks like the user is trying to open a file or folder in an
|
||||
* existing instance. The arguments here should be the arguments the user
|
||||
* explicitly passed on the command line, not defaults or the configuration.
|
||||
* explicitly passed on the command line, *NOT DEFAULTS* or the configuration.
|
||||
*/
|
||||
export const shouldOpenInExistingInstance = async (args: Args): Promise<string | undefined> => {
|
||||
export const shouldOpenInExistingInstance = async (args: UserProvidedArgs): Promise<string | undefined> => {
|
||||
// Always use the existing instance if we're running from VS Code's terminal.
|
||||
if (process.env.VSCODE_IPC_HOOK_CLI) {
|
||||
logger.debug("Found VSCODE_IPC_HOOK_CLI")
|
||||
return process.env.VSCODE_IPC_HOOK_CLI
|
||||
}
|
||||
|
||||
const readSocketPath = async (): Promise<string | undefined> => {
|
||||
try {
|
||||
return await fs.readFile(path.join(os.tmpdir(), "vscode-ipc"), "utf8")
|
||||
} catch (error) {
|
||||
if (error.code !== "ENOENT") {
|
||||
throw error
|
||||
}
|
||||
}
|
||||
return undefined
|
||||
}
|
||||
|
||||
// If these flags are set then assume the user is trying to open in an
|
||||
// existing instance since these flags have no effect otherwise.
|
||||
const openInFlagCount = ["reuse-window", "new-window"].reduce((prev, cur) => {
|
||||
return args[cur as keyof Args] ? prev + 1 : prev
|
||||
return args[cur as keyof UserProvidedArgs] ? prev + 1 : prev
|
||||
}, 0)
|
||||
if (openInFlagCount > 0) {
|
||||
return readSocketPath()
|
||||
logger.debug("Found --reuse-window or --new-window")
|
||||
return readSocketPath(DEFAULT_SOCKET_PATH)
|
||||
}
|
||||
|
||||
// It's possible the user is trying to spawn another instance of code-server.
|
||||
// Check if any unrelated flags are set (check against one because `_` always
|
||||
// exists), that a file or directory was passed, and that the socket is
|
||||
// active.
|
||||
if (Object.keys(args).length === 1 && args._.length > 0) {
|
||||
const socketPath = await readSocketPath()
|
||||
// 1. Check if any unrelated flags are set (this should only run when
|
||||
// code-server is invoked exactly like this: `code-server my-file`).
|
||||
// 2. That a file or directory was passed.
|
||||
// 3. That the socket is active.
|
||||
if (Object.keys(args).length === 1 && typeof args._ !== "undefined" && args._.length > 0) {
|
||||
const socketPath = await readSocketPath(DEFAULT_SOCKET_PATH)
|
||||
if (socketPath && (await canConnect(socketPath))) {
|
||||
logger.debug("Found existing code-server socket")
|
||||
return socketPath
|
||||
}
|
||||
}
|
||||
|
||||
return undefined
|
||||
}
|
||||
|
||||
/**
|
||||
* Convert our arguments to VS Code server arguments.
|
||||
*/
|
||||
export const toVsCodeArgs = async (args: DefaultedArgs): Promise<CodeServerLib.ServerParsedArgs> => {
|
||||
let workspace = ""
|
||||
let folder = ""
|
||||
if (args._.length) {
|
||||
const lastEntry = path.resolve(args._[args._.length - 1])
|
||||
const entryIsFile = await isFile(lastEntry)
|
||||
if (entryIsFile && path.extname(lastEntry) === ".code-workspace") {
|
||||
workspace = lastEntry
|
||||
} else if (!entryIsFile) {
|
||||
folder = lastEntry
|
||||
}
|
||||
// Otherwise it is a regular file. Spawning VS Code with a file is not yet
|
||||
// supported but it can be done separately after code-server spawns.
|
||||
}
|
||||
|
||||
return {
|
||||
"connection-token": "0000",
|
||||
...args,
|
||||
workspace,
|
||||
folder,
|
||||
"accept-server-license-terms": true,
|
||||
/** Type casting. */
|
||||
help: !!args.help,
|
||||
version: !!args.version,
|
||||
port: args.port?.toString(),
|
||||
}
|
||||
}
|
||||
|
||||
@@ -33,9 +33,11 @@ function runAgent(...args: string[]): Promise<void> {
|
||||
})
|
||||
}
|
||||
|
||||
export function coderCloudBind(csAddr: string, serverName = ""): Promise<void> {
|
||||
// addr needs to be in host:port format.
|
||||
// So we trim the protocol.
|
||||
csAddr = csAddr.replace(/^https?:\/\//, "")
|
||||
return runAgent("bind", `--code-server-addr=${csAddr}`, serverName)
|
||||
export function coderCloudBind(address: URL | string, serverName = ""): Promise<void> {
|
||||
if (typeof address === "string") {
|
||||
throw new Error("Cannot link socket paths")
|
||||
}
|
||||
|
||||
// Address needs to be in hostname:port format without the protocol.
|
||||
return runAgent("bind", `--code-server-addr=${address.host}`, serverName)
|
||||
}
|
||||
|
||||
@@ -1,13 +1,15 @@
|
||||
import { logger } from "@coder/logger"
|
||||
import { JSONSchemaForNPMPackageJsonFiles } from "@schemastore/package"
|
||||
import type { JSONSchemaForNPMPackageJsonFiles } from "@schemastore/package"
|
||||
import * as os from "os"
|
||||
import * as path from "path"
|
||||
|
||||
export const WORKBENCH_WEB_CONFIG_ID = "vscode-workbench-web-configuration"
|
||||
|
||||
export function getPackageJson(relativePath: string): JSONSchemaForNPMPackageJsonFiles {
|
||||
let pkg = {}
|
||||
try {
|
||||
pkg = require(relativePath)
|
||||
} catch (error) {
|
||||
} catch (error: any) {
|
||||
logger.warn(error.message)
|
||||
}
|
||||
|
||||
@@ -16,8 +18,10 @@ export function getPackageJson(relativePath: string): JSONSchemaForNPMPackageJso
|
||||
|
||||
const pkg = getPackageJson("../../package.json")
|
||||
|
||||
export const pkgName = pkg.name || "code-server"
|
||||
export const version = pkg.version || "development"
|
||||
export const commit = pkg.commit || "development"
|
||||
export const rootPath = path.resolve(__dirname, "../..")
|
||||
export const vsRootPath = path.join(rootPath, "vendor/modules/code-oss-dev")
|
||||
export const tmpdir = path.join(os.tmpdir(), "code-server")
|
||||
export const isDevMode = commit === "development"
|
||||
|
||||
@@ -1,19 +1,12 @@
|
||||
import { logger } from "@coder/logger"
|
||||
import {
|
||||
optionDescriptions,
|
||||
parse,
|
||||
readConfigFile,
|
||||
setDefaults,
|
||||
shouldOpenInExistingInstance,
|
||||
shouldRunVsCodeCli,
|
||||
} from "./cli"
|
||||
import { optionDescriptions, parse, readConfigFile, setDefaults, shouldOpenInExistingInstance } from "./cli"
|
||||
import { commit, version } from "./constants"
|
||||
import { openInExistingInstance, runCodeServer, runVsCodeCli } from "./main"
|
||||
import * as proxyAgent from "./proxy_agent"
|
||||
import { openInExistingInstance, runCodeServer, runVsCodeCli, shouldSpawnCliProcess } from "./main"
|
||||
import { monkeyPatchProxyProtocols } from "./proxy_agent"
|
||||
import { isChild, wrapper } from "./wrapper"
|
||||
|
||||
async function entry(): Promise<void> {
|
||||
proxyAgent.monkeyPatch(false)
|
||||
monkeyPatchProxyProtocols()
|
||||
|
||||
// There's no need to check flags like --help or to spawn in an existing
|
||||
// instance for the child process because these would have already happened in
|
||||
@@ -24,7 +17,8 @@ async function entry(): Promise<void> {
|
||||
if (isChild(wrapper)) {
|
||||
const args = await wrapper.handshake()
|
||||
wrapper.preventExit()
|
||||
await runCodeServer(args)
|
||||
const server = await runCodeServer(args)
|
||||
wrapper.onDispose(() => server.dispose())
|
||||
return
|
||||
}
|
||||
|
||||
@@ -36,6 +30,8 @@ async function entry(): Promise<void> {
|
||||
console.log("code-server", version, commit)
|
||||
console.log("")
|
||||
console.log(`Usage: code-server [options] [path]`)
|
||||
console.log(` - Opening a directory: code-server ./path/to/your/project`)
|
||||
console.log(` - Opening a saved workspace: code-server ./path/to/your/project.code-workspace`)
|
||||
console.log("")
|
||||
console.log("Options")
|
||||
optionDescriptions().forEach((description) => {
|
||||
@@ -46,23 +42,27 @@ async function entry(): Promise<void> {
|
||||
|
||||
if (args.version) {
|
||||
if (args.json) {
|
||||
console.log({
|
||||
codeServer: version,
|
||||
commit,
|
||||
vscode: require("../../vendor/modules/code-oss-dev/package.json").version,
|
||||
})
|
||||
console.log(
|
||||
JSON.stringify({
|
||||
codeServer: version,
|
||||
commit,
|
||||
vscode: require("../../vendor/modules/code-oss-dev/package.json").version,
|
||||
}),
|
||||
)
|
||||
} else {
|
||||
console.log(version, commit)
|
||||
}
|
||||
return
|
||||
}
|
||||
|
||||
if (shouldRunVsCodeCli(args)) {
|
||||
if (shouldSpawnCliProcess(args)) {
|
||||
logger.debug("Found VS Code arguments; spawning VS Code CLI")
|
||||
return runVsCodeCli(args)
|
||||
}
|
||||
|
||||
const socketPath = await shouldOpenInExistingInstance(cliArgs)
|
||||
if (socketPath) {
|
||||
logger.debug("Trying to open in existing instance")
|
||||
return openInExistingInstance(args, socketPath)
|
||||
}
|
||||
|
||||
|
||||
150
src/node/http.ts
150
src/node/http.ts
@@ -1,13 +1,27 @@
|
||||
import { field, logger } from "@coder/logger"
|
||||
import * as express from "express"
|
||||
import * as expressCore from "express-serve-static-core"
|
||||
import qs from "qs"
|
||||
import { HttpCode, HttpError } from "../common/http"
|
||||
import { normalize, Options } from "../common/util"
|
||||
import * as http from "http"
|
||||
import * as net from "net"
|
||||
import * as qs from "qs"
|
||||
import { Disposable } from "../common/emitter"
|
||||
import { CookieKeys, HttpCode, HttpError } from "../common/http"
|
||||
import { normalize } from "../common/util"
|
||||
import { AuthType, DefaultedArgs } from "./cli"
|
||||
import { commit, rootPath } from "./constants"
|
||||
import { version as codeServerVersion } from "./constants"
|
||||
import { Heart } from "./heart"
|
||||
import { getPasswordMethod, IsCookieValidArgs, isCookieValid, sanitizeString, escapeHtml } from "./util"
|
||||
import { getPasswordMethod, IsCookieValidArgs, isCookieValid, sanitizeString, escapeHtml, escapeJSON } from "./util"
|
||||
|
||||
/**
|
||||
* Base options included on every page.
|
||||
*/
|
||||
export interface ClientConfiguration {
|
||||
codeServerVersion: string
|
||||
/** Relative path from this page to the root. No trailing slash. */
|
||||
base: string
|
||||
/** Relative path from this page to the static root. No trailing slash. */
|
||||
csStaticBase: string
|
||||
}
|
||||
|
||||
declare global {
|
||||
// eslint-disable-next-line @typescript-eslint/no-namespace
|
||||
@@ -19,6 +33,16 @@ declare global {
|
||||
}
|
||||
}
|
||||
|
||||
export const createClientConfiguration = (req: express.Request): ClientConfiguration => {
|
||||
const base = relativeRoot(req.originalUrl)
|
||||
|
||||
return {
|
||||
base,
|
||||
csStaticBase: base + "/_static",
|
||||
codeServerVersion,
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* Replace common variable strings in HTML templates.
|
||||
*/
|
||||
@@ -27,18 +51,16 @@ export const replaceTemplates = <T extends object>(
|
||||
content: string,
|
||||
extraOpts?: Omit<T, "base" | "csStaticBase" | "logLevel">,
|
||||
): string => {
|
||||
const base = relativeRoot(req)
|
||||
const options: Options = {
|
||||
base,
|
||||
csStaticBase: base + "/static/" + commit + rootPath,
|
||||
logLevel: logger.level,
|
||||
const serverOptions: ClientConfiguration = {
|
||||
...createClientConfiguration(req),
|
||||
...extraOpts,
|
||||
}
|
||||
|
||||
return content
|
||||
.replace(/{{TO}}/g, (typeof req.query.to === "string" && escapeHtml(req.query.to)) || "/")
|
||||
.replace(/{{BASE}}/g, options.base)
|
||||
.replace(/{{CS_STATIC_BASE}}/g, options.csStaticBase)
|
||||
.replace(/"{{OPTIONS}}"/, `'${JSON.stringify(options)}'`)
|
||||
.replace(/{{BASE}}/g, serverOptions.base)
|
||||
.replace(/{{CS_STATIC_BASE}}/g, serverOptions.csStaticBase)
|
||||
.replace("{{OPTIONS}}", () => escapeJSON(serverOptions))
|
||||
}
|
||||
|
||||
/**
|
||||
@@ -72,7 +94,7 @@ export const authenticated = async (req: express.Request): Promise<boolean> => {
|
||||
const passwordMethod = getPasswordMethod(hashedPasswordFromArgs)
|
||||
const isCookieValidArgs: IsCookieValidArgs = {
|
||||
passwordMethod,
|
||||
cookieKey: sanitizeString(req.cookies.key),
|
||||
cookieKey: sanitizeString(req.cookies[CookieKeys.Session]),
|
||||
passwordFromArgs: req.args.password || "",
|
||||
hashedPasswordFromArgs: req.args["hashed-password"],
|
||||
}
|
||||
@@ -87,15 +109,28 @@ export const authenticated = async (req: express.Request): Promise<boolean> => {
|
||||
|
||||
/**
|
||||
* Get the relative path that will get us to the root of the page. For each
|
||||
* slash we need to go up a directory. For example:
|
||||
* slash we need to go up a directory. Will not have a trailing slash.
|
||||
*
|
||||
* For example:
|
||||
*
|
||||
* / => .
|
||||
* /foo => .
|
||||
* /foo/ => ./..
|
||||
* /foo/bar => ./..
|
||||
* /foo/bar/ => ./../..
|
||||
*
|
||||
* All paths must be relative in order to work behind a reverse proxy since we
|
||||
* we do not know the base path. Anything that needs to be absolute (for
|
||||
* example cookies) must get the base path from the frontend.
|
||||
*
|
||||
* All relative paths must be prefixed with the relative root to ensure they
|
||||
* work no matter the depth at which they happen to appear.
|
||||
*
|
||||
* For Express `req.originalUrl` should be used as they remove the base from the
|
||||
* standard `url` property making it impossible to get the true depth.
|
||||
*/
|
||||
export const relativeRoot = (req: express.Request): string => {
|
||||
const depth = (req.originalUrl.split("?", 1)[0].match(/\//g) || []).length
|
||||
export const relativeRoot = (originalUrl: string): string => {
|
||||
const depth = (originalUrl.split("?", 1)[0].match(/\//g) || []).length
|
||||
return normalize("./" + (depth > 1 ? "../".repeat(depth - 1) : ""))
|
||||
}
|
||||
|
||||
@@ -117,7 +152,7 @@ export const redirect = (
|
||||
}
|
||||
})
|
||||
|
||||
const relativePath = normalize(`${relativeRoot(req)}/${to}`, true)
|
||||
const relativePath = normalize(`${relativeRoot(req.originalUrl)}/${to}`, true)
|
||||
const queryString = qs.stringify(query)
|
||||
const redirectPath = `${relativePath}${queryString ? `?${queryString}` : ""}`
|
||||
logger.debug(`redirecting from ${req.originalUrl} to ${redirectPath}`)
|
||||
@@ -170,3 +205,82 @@ export const getCookieDomain = (host: string, proxyDomains: string[]): string |
|
||||
logger.debug("got cookie doman", field("host", host))
|
||||
return host || undefined
|
||||
}
|
||||
|
||||
/**
|
||||
* Return a function capable of fully disposing an HTTP server.
|
||||
*/
|
||||
export function disposer(server: http.Server): Disposable["dispose"] {
|
||||
const sockets = new Set<net.Socket>()
|
||||
let cleanupTimeout: undefined | NodeJS.Timeout
|
||||
|
||||
server.on("connection", (socket) => {
|
||||
sockets.add(socket)
|
||||
|
||||
socket.on("close", () => {
|
||||
sockets.delete(socket)
|
||||
|
||||
if (cleanupTimeout && sockets.size === 0) {
|
||||
clearTimeout(cleanupTimeout)
|
||||
cleanupTimeout = undefined
|
||||
}
|
||||
})
|
||||
})
|
||||
|
||||
return () => {
|
||||
return new Promise<void>((resolve, reject) => {
|
||||
// The whole reason we need this disposer is because close will not
|
||||
// actually close anything; it only prevents future connections then waits
|
||||
// until everything is closed.
|
||||
server.close((err) => {
|
||||
if (err) {
|
||||
return reject(err)
|
||||
}
|
||||
|
||||
resolve()
|
||||
})
|
||||
|
||||
// If there are sockets remaining we might need to force close them or
|
||||
// this promise might never resolve.
|
||||
if (sockets.size > 0) {
|
||||
// Give sockets a chance to close up shop.
|
||||
cleanupTimeout = setTimeout(() => {
|
||||
cleanupTimeout = undefined
|
||||
|
||||
for (const socket of sockets.values()) {
|
||||
console.warn("a socket was left hanging")
|
||||
socket.destroy()
|
||||
}
|
||||
}, 1000)
|
||||
}
|
||||
})
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* Get the options for setting a cookie. The options must be identical for
|
||||
* setting and unsetting cookies otherwise they are considered separate.
|
||||
*/
|
||||
export const getCookieOptions = (req: express.Request): express.CookieOptions => {
|
||||
// Normally we set paths relatively. However browsers do not appear to allow
|
||||
// cookies to be set relatively which means we need an absolute path. We
|
||||
// cannot be guaranteed we know the path since a reverse proxy might have
|
||||
// rewritten it. That means we need to get the path from the frontend.
|
||||
|
||||
// The reason we need to set the path (as opposed to defaulting to /) is to
|
||||
// avoid code-server instances on different sub-paths clobbering each other or
|
||||
// from accessing each other's tokens (and to prevent other services from
|
||||
// accessing code-server's tokens).
|
||||
|
||||
// When logging in or out the request must include the href (the full current
|
||||
// URL of that page) and the relative path to the root as given to it by the
|
||||
// backend. Using these two we can determine the true absolute root.
|
||||
const url = new URL(
|
||||
req.query.base || req.body.base || "/",
|
||||
req.query.href || req.body.href || "http://" + (req.headers.host || "localhost"),
|
||||
)
|
||||
return {
|
||||
domain: getCookieDomain(url.host, req.args["proxy-domain"]),
|
||||
path: normalize(url.pathname) || "/",
|
||||
sameSite: "lax",
|
||||
}
|
||||
}
|
||||
|
||||
121
src/node/main.ts
121
src/node/main.ts
@@ -1,68 +1,73 @@
|
||||
import { field, logger } from "@coder/logger"
|
||||
import * as cp from "child_process"
|
||||
import http from "http"
|
||||
import * as path from "path"
|
||||
import { CliMessage, OpenCommandPipeArgs } from "../../typings/ipc"
|
||||
import * as os from "os"
|
||||
import path from "path"
|
||||
import { Disposable } from "../common/emitter"
|
||||
import { plural } from "../common/util"
|
||||
import { createApp, ensureAddress } from "./app"
|
||||
import { AuthType, DefaultedArgs, Feature } from "./cli"
|
||||
import { AuthType, DefaultedArgs, Feature, toVsCodeArgs, UserProvidedArgs } from "./cli"
|
||||
import { coderCloudBind } from "./coder_cloud"
|
||||
import { commit, version } from "./constants"
|
||||
import { register } from "./routes"
|
||||
import { humanPath, isFile, open } from "./util"
|
||||
import { humanPath, isFile, loadAMDModule, open } from "./util"
|
||||
|
||||
export const runVsCodeCli = (args: DefaultedArgs): void => {
|
||||
logger.debug("forking vs code cli...")
|
||||
const vscode = cp.fork(path.resolve(__dirname, "../../vendor/modules/code-oss-dev/out/vs/server/fork"), [], {
|
||||
env: {
|
||||
...process.env,
|
||||
CODE_SERVER_PARENT_PID: process.pid.toString(),
|
||||
},
|
||||
})
|
||||
vscode.once("message", (message: any) => {
|
||||
logger.debug("got message from VS Code", field("message", message))
|
||||
if (message.type !== "ready") {
|
||||
logger.error("Unexpected response waiting for ready response", field("type", message.type))
|
||||
process.exit(1)
|
||||
}
|
||||
const send: CliMessage = { type: "cli", args }
|
||||
vscode.send(send)
|
||||
})
|
||||
vscode.once("error", (error) => {
|
||||
logger.error("Got error from VS Code", field("error", error))
|
||||
process.exit(1)
|
||||
})
|
||||
vscode.on("exit", (code) => process.exit(code || 0))
|
||||
/**
|
||||
* Return true if the user passed an extension-related VS Code flag.
|
||||
*/
|
||||
export const shouldSpawnCliProcess = (args: UserProvidedArgs): boolean => {
|
||||
return (
|
||||
!!args["list-extensions"] ||
|
||||
!!args["install-extension"] ||
|
||||
!!args["uninstall-extension"] ||
|
||||
!!args["locate-extension"]
|
||||
)
|
||||
}
|
||||
|
||||
/**
|
||||
* This is useful when an CLI arg should be passed to VS Code directly,
|
||||
* such as when managing extensions.
|
||||
* @deprecated This should be removed when code-server merges with lib/vscode.
|
||||
*/
|
||||
export const runVsCodeCli = async (args: DefaultedArgs): Promise<void> => {
|
||||
logger.debug("Running VS Code CLI")
|
||||
|
||||
// See ../../vendor/modules/code-oss-dev/src/vs/server/main.js.
|
||||
const spawnCli = await loadAMDModule<CodeServerLib.SpawnCli>("vs/server/remoteExtensionHostAgent", "spawnCli")
|
||||
|
||||
try {
|
||||
await spawnCli(await toVsCodeArgs(args))
|
||||
} catch (error: any) {
|
||||
logger.error("Got error from VS Code", error)
|
||||
}
|
||||
|
||||
process.exit(0)
|
||||
}
|
||||
|
||||
export const openInExistingInstance = async (args: DefaultedArgs, socketPath: string): Promise<void> => {
|
||||
const pipeArgs: OpenCommandPipeArgs & { fileURIs: string[] } = {
|
||||
const pipeArgs: CodeServerLib.OpenCommandPipeArgs & { fileURIs: string[] } = {
|
||||
type: "open",
|
||||
folderURIs: [],
|
||||
fileURIs: [],
|
||||
forceReuseWindow: args["reuse-window"],
|
||||
forceNewWindow: args["new-window"],
|
||||
}
|
||||
|
||||
for (let i = 0; i < args._.length; i++) {
|
||||
const fp = path.resolve(args._[i])
|
||||
const paths = args._ || []
|
||||
for (let i = 0; i < paths.length; i++) {
|
||||
const fp = path.resolve(paths[i])
|
||||
if (await isFile(fp)) {
|
||||
pipeArgs.fileURIs.push(fp)
|
||||
} else {
|
||||
pipeArgs.folderURIs.push(fp)
|
||||
}
|
||||
}
|
||||
|
||||
if (pipeArgs.forceNewWindow && pipeArgs.fileURIs.length > 0) {
|
||||
logger.error("--new-window can only be used with folder paths")
|
||||
process.exit(1)
|
||||
}
|
||||
|
||||
if (pipeArgs.folderURIs.length === 0 && pipeArgs.fileURIs.length === 0) {
|
||||
logger.error("Please specify at least one file or folder")
|
||||
process.exit(1)
|
||||
}
|
||||
|
||||
const vscode = http.request(
|
||||
{
|
||||
path: "/",
|
||||
@@ -82,11 +87,13 @@ export const openInExistingInstance = async (args: DefaultedArgs, socketPath: st
|
||||
vscode.end()
|
||||
}
|
||||
|
||||
export const runCodeServer = async (args: DefaultedArgs): Promise<http.Server> => {
|
||||
export const runCodeServer = async (
|
||||
args: DefaultedArgs,
|
||||
): Promise<{ dispose: Disposable["dispose"]; server: http.Server }> => {
|
||||
logger.info(`code-server ${version} ${commit}`)
|
||||
|
||||
logger.info(`Using user-data-dir ${humanPath(args["user-data-dir"])}`)
|
||||
logger.trace(`Using extensions-dir ${humanPath(args["extensions-dir"])}`)
|
||||
logger.info(`Using user-data-dir ${humanPath(os.homedir(), args["user-data-dir"])}`)
|
||||
logger.trace(`Using extensions-dir ${humanPath(os.homedir(), args["extensions-dir"])}`)
|
||||
|
||||
if (args.auth === AuthType.Password && !args.password && !args["hashed-password"]) {
|
||||
throw new Error(
|
||||
@@ -94,12 +101,18 @@ export const runCodeServer = async (args: DefaultedArgs): Promise<http.Server> =
|
||||
)
|
||||
}
|
||||
|
||||
const [app, wsApp, server] = await createApp(args)
|
||||
const serverAddress = ensureAddress(server)
|
||||
await register(app, wsApp, server, args)
|
||||
const app = await createApp(args)
|
||||
const protocol = args.cert ? "https" : "http"
|
||||
const serverAddress = ensureAddress(app.server, protocol)
|
||||
const disposeRoutes = await register(app, args)
|
||||
|
||||
logger.info(`Using config file ${humanPath(os.homedir(), args.config)}`)
|
||||
logger.info(
|
||||
`${protocol.toUpperCase()} server listening on ${serverAddress.toString()} ${
|
||||
args.link ? "(randomized by --link)" : ""
|
||||
}`,
|
||||
)
|
||||
|
||||
logger.info(`Using config file ${humanPath(args.config)}`)
|
||||
logger.info(`HTTP server listening on ${serverAddress} ${args.link ? "(randomized by --link)" : ""}`)
|
||||
if (args.auth === AuthType.Password) {
|
||||
logger.info(" - Authentication is enabled")
|
||||
if (args.usingEnvPassword) {
|
||||
@@ -107,14 +120,14 @@ export const runCodeServer = async (args: DefaultedArgs): Promise<http.Server> =
|
||||
} else if (args.usingEnvHashedPassword) {
|
||||
logger.info(" - Using password from $HASHED_PASSWORD")
|
||||
} else {
|
||||
logger.info(` - Using password from ${humanPath(args.config)}`)
|
||||
logger.info(` - Using password from ${humanPath(os.homedir(), args.config)}`)
|
||||
}
|
||||
} else {
|
||||
logger.info(` - Authentication is disabled ${args.link ? "(disabled by --link)" : ""}`)
|
||||
}
|
||||
|
||||
if (args.cert) {
|
||||
logger.info(` - Using certificate for HTTPS: ${humanPath(args.cert.value)}`)
|
||||
logger.info(` - Using certificate for HTTPS: ${humanPath(os.homedir(), args.cert.value)}`)
|
||||
} else {
|
||||
logger.info(` - Not serving HTTPS ${args.link ? "(disabled by --link)" : ""}`)
|
||||
}
|
||||
@@ -125,7 +138,7 @@ export const runCodeServer = async (args: DefaultedArgs): Promise<http.Server> =
|
||||
}
|
||||
|
||||
if (args.link) {
|
||||
await coderCloudBind(serverAddress.replace(/^https?:\/\//, ""), args.link.value)
|
||||
await coderCloudBind(serverAddress, args.link.value)
|
||||
logger.info(" - Connected to cloud agent")
|
||||
}
|
||||
|
||||
@@ -144,16 +157,20 @@ export const runCodeServer = async (args: DefaultedArgs): Promise<http.Server> =
|
||||
)
|
||||
}
|
||||
|
||||
if (!args.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")
|
||||
if (args.open) {
|
||||
try {
|
||||
await open(openAddress)
|
||||
logger.info(`Opened ${openAddress}`)
|
||||
await open(serverAddress)
|
||||
logger.info(`Opened ${serverAddress}`)
|
||||
} catch (error) {
|
||||
logger.error("Failed to open", field("address", openAddress), field("error", error))
|
||||
logger.error("Failed to open", field("address", serverAddress.toString()), field("error", error))
|
||||
}
|
||||
}
|
||||
|
||||
return server
|
||||
return {
|
||||
server: app.server,
|
||||
dispose: async () => {
|
||||
disposeRoutes()
|
||||
await app.dispose()
|
||||
},
|
||||
}
|
||||
}
|
||||
|
||||
@@ -172,9 +172,9 @@ export class PluginAPI {
|
||||
}
|
||||
await this.loadPlugin(path.join(dir, ent.name))
|
||||
}
|
||||
} catch (err) {
|
||||
if (err.code !== "ENOENT") {
|
||||
this.logger.warn(`failed to load plugins from ${q(dir)}: ${err.message}`)
|
||||
} catch (error: any) {
|
||||
if (error.code !== "ENOENT") {
|
||||
this.logger.warn(`failed to load plugins from ${q(dir)}: ${error.message}`)
|
||||
}
|
||||
}
|
||||
}
|
||||
@@ -195,9 +195,9 @@ export class PluginAPI {
|
||||
}
|
||||
const p = this._loadPlugin(dir, packageJSON)
|
||||
this.plugins.set(p.name, p)
|
||||
} catch (err) {
|
||||
if (err.code !== "ENOENT") {
|
||||
this.logger.warn(`failed to load plugin: ${err.stack}`)
|
||||
} catch (error: any) {
|
||||
if (error.code !== "ENOENT") {
|
||||
this.logger.warn(`failed to load plugin: ${error.stack}`)
|
||||
}
|
||||
}
|
||||
}
|
||||
@@ -278,7 +278,7 @@ export class PluginAPI {
|
||||
}
|
||||
try {
|
||||
await p.deinit()
|
||||
} catch (error) {
|
||||
} catch (error: any) {
|
||||
this.logger.error("plugin failed to deinit", field("name", p.name), field("error", error.message))
|
||||
}
|
||||
}),
|
||||
|
||||
@@ -1,7 +1,10 @@
|
||||
import { logger } from "@coder/logger"
|
||||
import * as http from "http"
|
||||
import * as proxyAgent from "proxy-agent"
|
||||
import * as proxyFromEnv from "proxy-from-env"
|
||||
/*---------------------------------------------------------------------------------------------
|
||||
* Copyright (c) Coder Technologies. All rights reserved.
|
||||
* Licensed under the MIT License. See License.txt in the project root for license information.
|
||||
*--------------------------------------------------------------------------------------------*/
|
||||
|
||||
import ProxyAgent from "proxy-agent"
|
||||
import { getProxyForUrl } from "proxy-from-env"
|
||||
|
||||
/**
|
||||
* This file has nothing to do with the code-server proxy.
|
||||
@@ -11,10 +14,6 @@ import * as proxyFromEnv from "proxy-from-env"
|
||||
* - https://www.npmjs.com/package/proxy-agent
|
||||
* - https://www.npmjs.com/package/proxy-from-env
|
||||
*
|
||||
* This file exists in two locations:
|
||||
* - src/node/proxy_agent.ts
|
||||
* - lib/vscode/src/vs/base/node/proxy_agent.ts
|
||||
* The second is a symlink to the first.
|
||||
*/
|
||||
|
||||
/**
|
||||
@@ -31,50 +30,41 @@ import * as proxyFromEnv from "proxy-from-env"
|
||||
* Even if they do, it's probably the same proxy so we should be fine! And those knobs
|
||||
* are deprecated anyway.
|
||||
*/
|
||||
export function monkeyPatch(inVSCode: boolean): void {
|
||||
if (shouldEnableProxy()) {
|
||||
const http = require("http")
|
||||
const https = require("https")
|
||||
|
||||
// If we do not pass in a proxy URL, proxy-agent will get the URL from the environment.
|
||||
// See https://www.npmjs.com/package/proxy-from-env.
|
||||
// Also see shouldEnableProxy.
|
||||
const pa = newProxyAgent(inVSCode)
|
||||
http.globalAgent = pa
|
||||
https.globalAgent = pa
|
||||
export function monkeyPatchProxyProtocols(): void {
|
||||
if (!shouldEnableProxy()) {
|
||||
return
|
||||
}
|
||||
|
||||
const http = require("http")
|
||||
const https = require("https")
|
||||
|
||||
// If we do not pass in a proxy URL, proxy-agent will get the URL from the environment.
|
||||
// See https://www.npmjs.com/package/proxy-from-env.
|
||||
// Also see shouldEnableProxy.
|
||||
const pa = new ProxyAgent()
|
||||
http.globalAgent = pa
|
||||
https.globalAgent = pa
|
||||
}
|
||||
|
||||
function newProxyAgent(inVSCode: boolean): http.Agent {
|
||||
// The reasoning for this split is that VS Code's build process does not have
|
||||
// esModuleInterop enabled but the code-server one does. As a result depending on where
|
||||
// we execute, we either have a default attribute or we don't.
|
||||
//
|
||||
// I can't enable esModuleInterop in VS Code's build process as it breaks and spits out
|
||||
// a huge number of errors. And we can't use require as otherwise the modules won't be
|
||||
// included in the final product.
|
||||
if (inVSCode) {
|
||||
return new (proxyAgent as any)()
|
||||
} else {
|
||||
return new (proxyAgent as any).default()
|
||||
}
|
||||
}
|
||||
const sampleUrls = [new URL("http://example.com"), new URL("https://example.com")]
|
||||
|
||||
// If they have $NO_PROXY set to example.com then this check won't work!
|
||||
// But that's drastically unlikely.
|
||||
export function shouldEnableProxy(): boolean {
|
||||
const testedProxyEndpoints = sampleUrls.map((url) => {
|
||||
return {
|
||||
url,
|
||||
proxyUrl: getProxyForUrl(url.toString()),
|
||||
}
|
||||
})
|
||||
|
||||
let shouldEnable = false
|
||||
|
||||
const httpProxy = proxyFromEnv.getProxyForUrl(`http://example.com`)
|
||||
if (httpProxy) {
|
||||
shouldEnable = true
|
||||
logger.debug(`using $HTTP_PROXY ${httpProxy}`)
|
||||
}
|
||||
|
||||
const httpsProxy = proxyFromEnv.getProxyForUrl(`https://example.com`)
|
||||
if (httpsProxy) {
|
||||
shouldEnable = true
|
||||
logger.debug(`using $HTTPS_PROXY ${httpsProxy}`)
|
||||
for (const { url, proxyUrl } of testedProxyEndpoints) {
|
||||
if (proxyUrl) {
|
||||
console.debug(`${url.protocol} -- Using "${proxyUrl}"`)
|
||||
shouldEnable = true
|
||||
}
|
||||
}
|
||||
|
||||
return shouldEnable
|
||||
|
||||
67
src/node/routes/errors.ts
Normal file
67
src/node/routes/errors.ts
Normal file
@@ -0,0 +1,67 @@
|
||||
import { logger } from "@coder/logger"
|
||||
import express from "express"
|
||||
import { promises as fs } from "fs"
|
||||
import path from "path"
|
||||
import { WebsocketRequest } from "../../../typings/pluginapi"
|
||||
import { HttpCode } from "../../common/http"
|
||||
import { rootPath } from "../constants"
|
||||
import { replaceTemplates } from "../http"
|
||||
import { escapeHtml, getMediaMime } from "../util"
|
||||
|
||||
interface ErrorWithStatusCode {
|
||||
statusCode: number
|
||||
}
|
||||
|
||||
interface ErrorWithCode {
|
||||
code: string
|
||||
}
|
||||
|
||||
/** Error is network related. */
|
||||
export const errorHasStatusCode = (error: any): error is ErrorWithStatusCode => {
|
||||
return error && "statusCode" in error
|
||||
}
|
||||
|
||||
/** Error originates from file system. */
|
||||
export const errorHasCode = (error: any): error is ErrorWithCode => {
|
||||
return error && "code" in error
|
||||
}
|
||||
|
||||
const notFoundCodes = [404, "ENOENT", "EISDIR"]
|
||||
|
||||
export const errorHandler: express.ErrorRequestHandler = async (err, req, res, next) => {
|
||||
let statusCode = 500
|
||||
|
||||
if (errorHasStatusCode(err)) {
|
||||
statusCode = err.statusCode
|
||||
} else if (errorHasCode(err) && notFoundCodes.includes(err.code)) {
|
||||
statusCode = HttpCode.NotFound
|
||||
}
|
||||
|
||||
res.status(statusCode)
|
||||
|
||||
// Assume anything that explicitly accepts text/html is a user browsing a
|
||||
// page (as opposed to an xhr request). Don't use `req.accepts()` since
|
||||
// *every* request that I've seen (in Firefox and Chromium at least)
|
||||
// includes `*/*` making it always truthy. Even for css/javascript.
|
||||
if (req.headers.accept && req.headers.accept.includes("text/html")) {
|
||||
const resourcePath = path.resolve(rootPath, "src/browser/pages/error.html")
|
||||
res.set("Content-Type", getMediaMime(resourcePath))
|
||||
const content = await fs.readFile(resourcePath, "utf8")
|
||||
res.send(
|
||||
replaceTemplates(req, content)
|
||||
.replace(/{{ERROR_TITLE}}/g, statusCode.toString())
|
||||
.replace(/{{ERROR_HEADER}}/g, statusCode.toString())
|
||||
.replace(/{{ERROR_BODY}}/g, escapeHtml(err.message)),
|
||||
)
|
||||
} else {
|
||||
res.json({
|
||||
error: err.message,
|
||||
...(err.details || {}),
|
||||
})
|
||||
}
|
||||
}
|
||||
|
||||
export const wsErrorHandler: express.ErrorRequestHandler = async (err, req, res, next) => {
|
||||
logger.error(`${err.message} ${err.stack}`)
|
||||
;(req as WebsocketRequest).ws.end()
|
||||
}
|
||||
@@ -1,44 +1,37 @@
|
||||
import { logger } from "@coder/logger"
|
||||
import bodyParser from "body-parser"
|
||||
import cookieParser from "cookie-parser"
|
||||
import * as express from "express"
|
||||
import { promises as fs } from "fs"
|
||||
import http from "http"
|
||||
import * as path from "path"
|
||||
import * as tls from "tls"
|
||||
import * as pluginapi from "../../../typings/pluginapi"
|
||||
import { Disposable } from "../../common/emitter"
|
||||
import { HttpCode, HttpError } from "../../common/http"
|
||||
import { plural } from "../../common/util"
|
||||
import { App } from "../app"
|
||||
import { AuthType, DefaultedArgs } from "../cli"
|
||||
import { rootPath } from "../constants"
|
||||
import { commit, rootPath } from "../constants"
|
||||
import { Heart } from "../heart"
|
||||
import { ensureAuthenticated, redirect, replaceTemplates } from "../http"
|
||||
import { ensureAuthenticated, redirect } from "../http"
|
||||
import { PluginAPI } from "../plugin"
|
||||
import { getMediaMime, paths } from "../util"
|
||||
import { wrapper } from "../wrapper"
|
||||
import * as apps from "./apps"
|
||||
import * as domainProxy from "./domainProxy"
|
||||
import { errorHandler, wsErrorHandler } from "./errors"
|
||||
import * as health from "./health"
|
||||
import * as login from "./login"
|
||||
import * as logout from "./logout"
|
||||
import * as pathProxy from "./pathProxy"
|
||||
// static is a reserved keyword.
|
||||
import * as _static from "./static"
|
||||
import * as update from "./update"
|
||||
import * as vscode from "./vscode"
|
||||
import { CodeServerRouteWrapper } from "./vscode"
|
||||
|
||||
/**
|
||||
* Register all routes and middleware.
|
||||
*/
|
||||
export const register = async (
|
||||
app: express.Express,
|
||||
wsApp: express.Express,
|
||||
server: http.Server,
|
||||
args: DefaultedArgs,
|
||||
): Promise<void> => {
|
||||
export const register = async (app: App, args: DefaultedArgs): Promise<Disposable["dispose"]> => {
|
||||
const heart = new Heart(path.join(paths.data, "heartbeat"), async () => {
|
||||
return new Promise((resolve, reject) => {
|
||||
server.getConnections((error, count) => {
|
||||
app.server.getConnections((error, count) => {
|
||||
if (error) {
|
||||
return reject(error)
|
||||
}
|
||||
@@ -47,15 +40,12 @@ export const register = async (
|
||||
})
|
||||
})
|
||||
})
|
||||
server.on("close", () => {
|
||||
heart.dispose()
|
||||
})
|
||||
|
||||
app.disable("x-powered-by")
|
||||
wsApp.disable("x-powered-by")
|
||||
app.router.disable("x-powered-by")
|
||||
app.wsRouter.disable("x-powered-by")
|
||||
|
||||
app.use(cookieParser())
|
||||
wsApp.use(cookieParser())
|
||||
app.router.use(cookieParser())
|
||||
app.wsRouter.use(cookieParser())
|
||||
|
||||
const common: express.RequestHandler = (req, _, next) => {
|
||||
// /healthz|/healthz/ needs to be excluded otherwise health checks will make
|
||||
@@ -71,10 +61,10 @@ export const register = async (
|
||||
next()
|
||||
}
|
||||
|
||||
app.use(common)
|
||||
wsApp.use(common)
|
||||
app.router.use(common)
|
||||
app.wsRouter.use(common)
|
||||
|
||||
app.use(async (req, res, next) => {
|
||||
app.router.use(async (req, res, next) => {
|
||||
// If we're handling TLS ensure all requests are redirected to HTTPS.
|
||||
// TODO: This does *NOT* work if you have a base path since to specify the
|
||||
// protocol we need to specify the whole path.
|
||||
@@ -92,100 +82,80 @@ export const register = async (
|
||||
next()
|
||||
})
|
||||
|
||||
app.use("/", domainProxy.router)
|
||||
wsApp.use("/", domainProxy.wsRouter.router)
|
||||
app.router.use("/", domainProxy.router)
|
||||
app.wsRouter.use("/", domainProxy.wsRouter.router)
|
||||
|
||||
app.all("/proxy/(:port)(/*)?", (req, res) => {
|
||||
app.router.all("/proxy/(:port)(/*)?", (req, res) => {
|
||||
pathProxy.proxy(req, res)
|
||||
})
|
||||
wsApp.get("/proxy/(:port)(/*)?", async (req) => {
|
||||
app.wsRouter.get("/proxy/(:port)(/*)?", async (req) => {
|
||||
await pathProxy.wsProxy(req as pluginapi.WebsocketRequest)
|
||||
})
|
||||
// These two routes pass through the path directly.
|
||||
// So the proxied app must be aware it is running
|
||||
// under /absproxy/<someport>/
|
||||
app.all("/absproxy/(:port)(/*)?", (req, res) => {
|
||||
app.router.all("/absproxy/(:port)(/*)?", (req, res) => {
|
||||
pathProxy.proxy(req, res, {
|
||||
passthroughPath: true,
|
||||
})
|
||||
})
|
||||
wsApp.get("/absproxy/(:port)(/*)?", async (req) => {
|
||||
app.wsRouter.get("/absproxy/(:port)(/*)?", async (req) => {
|
||||
await pathProxy.wsProxy(req as pluginapi.WebsocketRequest, {
|
||||
passthroughPath: true,
|
||||
})
|
||||
})
|
||||
|
||||
let pluginApi: PluginAPI
|
||||
if (!process.env.CS_DISABLE_PLUGINS) {
|
||||
const workingDir = args._ && args._.length > 0 ? path.resolve(args._[args._.length - 1]) : undefined
|
||||
const pluginApi = new PluginAPI(logger, process.env.CS_PLUGIN, process.env.CS_PLUGIN_PATH, workingDir)
|
||||
pluginApi = new PluginAPI(logger, process.env.CS_PLUGIN, process.env.CS_PLUGIN_PATH, workingDir)
|
||||
await pluginApi.loadPlugins()
|
||||
pluginApi.mount(app, wsApp)
|
||||
app.use("/api/applications", ensureAuthenticated, apps.router(pluginApi))
|
||||
wrapper.onDispose(() => pluginApi.dispose())
|
||||
pluginApi.mount(app.router, app.wsRouter)
|
||||
app.router.use("/api/applications", ensureAuthenticated, apps.router(pluginApi))
|
||||
}
|
||||
|
||||
app.use(bodyParser.json())
|
||||
app.use(bodyParser.urlencoded({ extended: true }))
|
||||
app.router.use(express.json())
|
||||
app.router.use(express.urlencoded({ extended: true }))
|
||||
|
||||
app.use("/", vscode.router)
|
||||
wsApp.use("/", vscode.wsRouter.router)
|
||||
app.use("/vscode", vscode.router)
|
||||
wsApp.use("/vscode", vscode.wsRouter.router)
|
||||
app.router.use(
|
||||
"/_static",
|
||||
express.static(rootPath, {
|
||||
cacheControl: commit !== "development",
|
||||
fallthrough: false,
|
||||
}),
|
||||
)
|
||||
|
||||
app.use("/healthz", health.router)
|
||||
wsApp.use("/healthz", health.wsRouter.router)
|
||||
app.router.use("/healthz", health.router)
|
||||
app.wsRouter.use("/healthz", health.wsRouter.router)
|
||||
|
||||
if (args.auth === AuthType.Password) {
|
||||
app.use("/login", login.router)
|
||||
app.use("/logout", logout.router)
|
||||
app.router.use("/login", login.router)
|
||||
app.router.use("/logout", logout.router)
|
||||
} else {
|
||||
app.all("/login", (req, res) => redirect(req, res, "/", {}))
|
||||
app.all("/logout", (req, res) => redirect(req, res, "/", {}))
|
||||
app.router.all("/login", (req, res) => redirect(req, res, "/", {}))
|
||||
app.router.all("/logout", (req, res) => redirect(req, res, "/", {}))
|
||||
}
|
||||
|
||||
app.use("/static", _static.router)
|
||||
app.use("/update", update.router)
|
||||
app.router.use("/update", update.router)
|
||||
|
||||
app.use(() => {
|
||||
const vsServerRouteHandler = new CodeServerRouteWrapper()
|
||||
|
||||
// Note that the root route is replaced in Coder Enterprise by the plugin API.
|
||||
for (const routePrefix of ["/vscode", "/"]) {
|
||||
app.router.use(routePrefix, vsServerRouteHandler.router)
|
||||
app.wsRouter.use(routePrefix, vsServerRouteHandler.wsRouter)
|
||||
}
|
||||
|
||||
app.router.use(() => {
|
||||
throw new HttpError("Not Found", HttpCode.NotFound)
|
||||
})
|
||||
|
||||
const errorHandler: express.ErrorRequestHandler = async (err, req, res, next) => {
|
||||
if (err.code === "ENOENT" || err.code === "EISDIR") {
|
||||
err.status = HttpCode.NotFound
|
||||
}
|
||||
app.router.use(errorHandler)
|
||||
app.wsRouter.use(wsErrorHandler)
|
||||
|
||||
const status = err.status ?? err.statusCode ?? 500
|
||||
res.status(status)
|
||||
|
||||
// Assume anything that explicitly accepts text/html is a user browsing a
|
||||
// page (as opposed to an xhr request). Don't use `req.accepts()` since
|
||||
// *every* request that I've seen (in Firefox and Chromium at least)
|
||||
// includes `*/*` making it always truthy. Even for css/javascript.
|
||||
if (req.headers.accept && req.headers.accept.includes("text/html")) {
|
||||
const resourcePath = path.resolve(rootPath, "src/browser/pages/error.html")
|
||||
res.set("Content-Type", getMediaMime(resourcePath))
|
||||
const content = await fs.readFile(resourcePath, "utf8")
|
||||
res.send(
|
||||
replaceTemplates(req, content)
|
||||
.replace(/{{ERROR_TITLE}}/g, status)
|
||||
.replace(/{{ERROR_HEADER}}/g, status)
|
||||
.replace(/{{ERROR_BODY}}/g, err.message),
|
||||
)
|
||||
} else {
|
||||
res.json({
|
||||
error: err.message,
|
||||
...(err.details || {}),
|
||||
})
|
||||
}
|
||||
return () => {
|
||||
heart.dispose()
|
||||
pluginApi?.dispose()
|
||||
vsServerRouteHandler.dispose()
|
||||
}
|
||||
|
||||
app.use(errorHandler)
|
||||
|
||||
const wsErrorHandler: express.ErrorRequestHandler = async (err, req, res, next) => {
|
||||
logger.error(`${err.message} ${err.stack}`)
|
||||
;(req as pluginapi.WebsocketRequest).ws.end()
|
||||
}
|
||||
|
||||
wsApp.use(wsErrorHandler)
|
||||
}
|
||||
|
||||
@@ -1,15 +1,13 @@
|
||||
import { Router, Request } from "express"
|
||||
import { promises as fs } from "fs"
|
||||
import { RateLimiter as Limiter } from "limiter"
|
||||
import * as os from "os"
|
||||
import * as path from "path"
|
||||
import { CookieKeys } from "../../common/http"
|
||||
import { rootPath } from "../constants"
|
||||
import { authenticated, getCookieDomain, redirect, replaceTemplates } from "../http"
|
||||
import { authenticated, getCookieOptions, redirect, replaceTemplates } from "../http"
|
||||
import { getPasswordMethod, handlePasswordValidation, humanPath, sanitizeString, escapeHtml } from "../util"
|
||||
|
||||
export enum Cookie {
|
||||
Key = "key",
|
||||
}
|
||||
|
||||
// RateLimiter wraps around the limiter library for logins.
|
||||
// It allows 2 logins every minute plus 12 logins every hour.
|
||||
export class RateLimiter {
|
||||
@@ -30,7 +28,7 @@ export class RateLimiter {
|
||||
|
||||
const getRoot = async (req: Request, error?: Error): Promise<string> => {
|
||||
const content = await fs.readFile(path.join(rootPath, "src/browser/pages/login.html"), "utf8")
|
||||
let passwordMsg = `Check the config file at ${humanPath(req.args.config)} for the password.`
|
||||
let passwordMsg = `Check the config file at ${humanPath(os.homedir(), req.args.config)} for the password.`
|
||||
if (req.args.usingEnvPassword) {
|
||||
passwordMsg = "Password was set from $PASSWORD."
|
||||
} else if (req.args.usingEnvHashedPassword) {
|
||||
@@ -61,7 +59,7 @@ router.get("/", async (req, res) => {
|
||||
res.send(await getRoot(req))
|
||||
})
|
||||
|
||||
router.post("/", async (req, res) => {
|
||||
router.post<{}, string, { password: string; base?: string }, { to?: string }>("/", async (req, res) => {
|
||||
const password = sanitizeString(req.body.password)
|
||||
const hashedPasswordFromArgs = req.args["hashed-password"]
|
||||
|
||||
@@ -86,11 +84,7 @@ router.post("/", async (req, res) => {
|
||||
if (isPasswordValid) {
|
||||
// The hash does not add any actual security but we do it for
|
||||
// obfuscation purposes (and as a side effect it handles escaping).
|
||||
res.cookie(Cookie.Key, hashedPassword, {
|
||||
domain: getCookieDomain(req.headers.host || "", req.args["proxy-domain"]),
|
||||
path: req.body.base || "/",
|
||||
sameSite: "lax",
|
||||
})
|
||||
res.cookie(CookieKeys.Session, hashedPassword, getCookieOptions(req))
|
||||
|
||||
const to = (typeof req.query.to === "string" && req.query.to) || "/"
|
||||
return redirect(req, res, to, { to: undefined })
|
||||
@@ -111,7 +105,7 @@ router.post("/", async (req, res) => {
|
||||
)
|
||||
|
||||
throw new Error("Incorrect password")
|
||||
} catch (error) {
|
||||
} catch (error: any) {
|
||||
const renderedHtml = await getRoot(req, error)
|
||||
res.send(renderedHtml)
|
||||
}
|
||||
|
||||
@@ -1,17 +1,14 @@
|
||||
import { Router } from "express"
|
||||
import { getCookieDomain, redirect } from "../http"
|
||||
import { Cookie } from "./login"
|
||||
import { CookieKeys } from "../../common/http"
|
||||
import { getCookieOptions, redirect } from "../http"
|
||||
import { sanitizeString } from "../util"
|
||||
|
||||
export const router = Router()
|
||||
|
||||
router.get("/", async (req, res) => {
|
||||
router.get<{}, undefined, undefined, { base?: string; to?: string }>("/", async (req, res) => {
|
||||
// Must use the *identical* properties used to set the cookie.
|
||||
res.clearCookie(Cookie.Key, {
|
||||
domain: getCookieDomain(req.headers.host || "", req.args["proxy-domain"]),
|
||||
path: req.query.base || "/",
|
||||
sameSite: "lax",
|
||||
})
|
||||
res.clearCookie(CookieKeys.Session, getCookieOptions(req))
|
||||
|
||||
const to = (typeof req.query.to === "string" && req.query.to) || "/"
|
||||
return redirect(req, res, to, { to: undefined, base: undefined })
|
||||
const to = sanitizeString(req.query.to) || "/"
|
||||
return redirect(req, res, to, { to: undefined, base: undefined, href: undefined })
|
||||
})
|
||||
|
||||
@@ -1,6 +1,6 @@
|
||||
import { Request, Response } from "express"
|
||||
import * as path from "path"
|
||||
import qs from "qs"
|
||||
import * as qs from "qs"
|
||||
import * as pluginapi from "../../../typings/pluginapi"
|
||||
import { HttpCode, HttpError } from "../../common/http"
|
||||
import { normalize } from "../../common/util"
|
||||
|
||||
@@ -1,71 +0,0 @@
|
||||
import { field, logger } from "@coder/logger"
|
||||
import { Router } from "express"
|
||||
import { promises as fs } from "fs"
|
||||
import * as path from "path"
|
||||
import { Readable } from "stream"
|
||||
import * as tarFs from "tar-fs"
|
||||
import * as zlib from "zlib"
|
||||
import { HttpCode, HttpError } from "../../common/http"
|
||||
import { getFirstString } from "../../common/util"
|
||||
import { rootPath } from "../constants"
|
||||
import { authenticated, ensureAuthenticated, replaceTemplates } from "../http"
|
||||
import { getMediaMime, pathToFsPath } from "../util"
|
||||
|
||||
export const router = Router()
|
||||
|
||||
// The commit is for caching.
|
||||
router.get("/(:commit)(/*)?", async (req, res) => {
|
||||
// Used by VS Code to load extensions into the web worker.
|
||||
const tar = getFirstString(req.query.tar)
|
||||
if (tar) {
|
||||
await ensureAuthenticated(req)
|
||||
let stream: Readable = tarFs.pack(pathToFsPath(tar))
|
||||
if (req.headers["accept-encoding"] && req.headers["accept-encoding"].includes("gzip")) {
|
||||
logger.debug("gzipping tar", field("path", tar))
|
||||
const compress = zlib.createGzip()
|
||||
stream.pipe(compress)
|
||||
stream.on("error", (error) => compress.destroy(error))
|
||||
stream.on("close", () => compress.end())
|
||||
stream = compress
|
||||
res.header("content-encoding", "gzip")
|
||||
}
|
||||
res.set("Content-Type", "application/x-tar")
|
||||
stream.on("close", () => res.end())
|
||||
return stream.pipe(res)
|
||||
}
|
||||
|
||||
// If not a tar use the remainder of the path to load the resource.
|
||||
if (!req.params[0]) {
|
||||
throw new HttpError("Not Found", HttpCode.NotFound)
|
||||
}
|
||||
|
||||
const resourcePath = path.resolve(req.params[0])
|
||||
|
||||
// Make sure it's in code-server if you aren't authenticated. This lets
|
||||
// unauthenticated users load the login assets.
|
||||
const isAuthenticated = await authenticated(req)
|
||||
if (!resourcePath.startsWith(rootPath) && !isAuthenticated) {
|
||||
throw new HttpError("Unauthorized", HttpCode.Unauthorized)
|
||||
}
|
||||
|
||||
// Don't cache during development. - can also be used if you want to make a
|
||||
// static request without caching.
|
||||
if (req.params.commit !== "development" && req.params.commit !== "-") {
|
||||
res.header("Cache-Control", "public, max-age=31536000")
|
||||
}
|
||||
|
||||
// Without this the default is to use the directory the script loaded from.
|
||||
if (req.headers["service-worker"]) {
|
||||
res.header("service-worker-allowed", "/")
|
||||
}
|
||||
|
||||
res.set("Content-Type", getMediaMime(resourcePath))
|
||||
|
||||
if (resourcePath.endsWith("manifest.json")) {
|
||||
const content = await fs.readFile(resourcePath, "utf8")
|
||||
return res.send(replaceTemplates(req, content))
|
||||
}
|
||||
|
||||
const content = await fs.readFile(resourcePath)
|
||||
return res.send(content)
|
||||
})
|
||||
@@ -1,232 +1,109 @@
|
||||
import * as crypto from "crypto"
|
||||
import { Request, Router } from "express"
|
||||
import { promises as fs } from "fs"
|
||||
import * as path from "path"
|
||||
import qs from "qs"
|
||||
import * as ipc from "../../../typings/ipc"
|
||||
import { Emitter } from "../../common/emitter"
|
||||
import { HttpCode, HttpError } from "../../common/http"
|
||||
import { getFirstString } from "../../common/util"
|
||||
import { Feature } from "../cli"
|
||||
import { isDevMode, rootPath, version } from "../constants"
|
||||
import { authenticated, ensureAuthenticated, redirect, replaceTemplates } from "../http"
|
||||
import { getMediaMime, pathToFsPath } from "../util"
|
||||
import { VscodeProvider } from "../vscode"
|
||||
import { logger } from "@coder/logger"
|
||||
import * as express from "express"
|
||||
import { WebsocketRequest } from "../../../typings/pluginapi"
|
||||
import { logError } from "../../common/util"
|
||||
import { isDevMode } from "../constants"
|
||||
import { toVsCodeArgs } from "../cli"
|
||||
import { ensureAuthenticated, authenticated, redirect } from "../http"
|
||||
import { loadAMDModule, readCompilationStats } from "../util"
|
||||
import { Router as WsRouter } from "../wsRouter"
|
||||
import { errorHandler } from "./errors"
|
||||
|
||||
export const router = Router()
|
||||
export class CodeServerRouteWrapper {
|
||||
/** Assigned in `ensureCodeServerLoaded` */
|
||||
private _codeServerMain!: CodeServerLib.IServerAPI
|
||||
private _wsRouterWrapper = WsRouter()
|
||||
public router = express.Router()
|
||||
|
||||
const vscode = new VscodeProvider()
|
||||
|
||||
router.get("/", async (req, res) => {
|
||||
const isAuthenticated = await authenticated(req)
|
||||
if (!isAuthenticated) {
|
||||
return redirect(req, res, "login", {
|
||||
// req.baseUrl can be blank if already at the root.
|
||||
to: req.baseUrl && req.baseUrl !== "/" ? req.baseUrl : undefined,
|
||||
})
|
||||
public get wsRouter() {
|
||||
return this._wsRouterWrapper.router
|
||||
}
|
||||
|
||||
const [content, options] = await Promise.all([
|
||||
await fs.readFile(path.join(rootPath, "src/browser/pages/vscode.html"), "utf8"),
|
||||
(async () => {
|
||||
try {
|
||||
return await vscode.initialize({ args: req.args, remoteAuthority: req.headers.host || "" }, req.query)
|
||||
} catch (error) {
|
||||
const devMessage = isDevMode ? "It might not have finished compiling." : ""
|
||||
throw new Error(`VS Code failed to load. ${devMessage} ${error.message}`)
|
||||
//#region Route Handlers
|
||||
|
||||
private $root: express.Handler = async (req, res, next) => {
|
||||
const isAuthenticated = await authenticated(req)
|
||||
|
||||
if (!isAuthenticated) {
|
||||
return redirect(req, res, "login", {
|
||||
// req.baseUrl can be blank if already at the root.
|
||||
to: req.baseUrl && req.baseUrl !== "/" ? req.baseUrl : undefined,
|
||||
})
|
||||
}
|
||||
|
||||
next()
|
||||
}
|
||||
|
||||
private $proxyRequest: express.Handler = async (req, res, next) => {
|
||||
// We allow certain errors to propagate so that other routers may handle requests
|
||||
// outside VS Code
|
||||
const requestErrorHandler = (error: any) => {
|
||||
if (error instanceof Error && ["EntryNotFound", "FileNotFound", "HttpError"].includes(error.message)) {
|
||||
next()
|
||||
}
|
||||
})(),
|
||||
])
|
||||
errorHandler(error, req, res, next)
|
||||
}
|
||||
|
||||
options.productConfiguration.codeServerVersion = version
|
||||
req.once("error", requestErrorHandler)
|
||||
|
||||
res.send(
|
||||
replaceTemplates<ipc.Options>(
|
||||
req,
|
||||
// Uncomment prod blocks if not in development. TODO: Would this be
|
||||
// better as a build step? Or maintain two HTML files again?
|
||||
!isDevMode ? content.replace(/<!-- PROD_ONLY/g, "").replace(/END_PROD_ONLY -->/g, "") : content,
|
||||
{
|
||||
authed: req.args.auth !== "none",
|
||||
disableUpdateCheck: !!req.args["disable-update-check"],
|
||||
},
|
||||
this._codeServerMain.handleRequest(req, res)
|
||||
}
|
||||
|
||||
private $proxyWebsocket = async (req: WebsocketRequest) => {
|
||||
this._codeServerMain.handleUpgrade(req, req.socket)
|
||||
|
||||
req.socket.resume()
|
||||
}
|
||||
|
||||
//#endregion
|
||||
|
||||
/**
|
||||
* Fetches a code server instance asynchronously to avoid an initial memory overhead.
|
||||
*/
|
||||
private ensureCodeServerLoaded: express.Handler = async (req, _res, next) => {
|
||||
if (this._codeServerMain) {
|
||||
// Already loaded...
|
||||
return next()
|
||||
}
|
||||
|
||||
if (isDevMode) {
|
||||
// Is the development mode file watcher still busy?
|
||||
const compileStats = await readCompilationStats()
|
||||
|
||||
if (!compileStats || !compileStats.lastCompiledAt) {
|
||||
return next(new Error("VS Code may still be compiling..."))
|
||||
}
|
||||
}
|
||||
|
||||
// Create the server...
|
||||
|
||||
const { args } = req
|
||||
|
||||
/**
|
||||
* @file ../../../vendor/modules/code-oss-dev/src/vs/server/main.js
|
||||
*/
|
||||
const createVSServer = await loadAMDModule<CodeServerLib.CreateServer>(
|
||||
"vs/server/remoteExtensionHostAgent",
|
||||
"createServer",
|
||||
)
|
||||
.replace(`"{{REMOTE_USER_DATA_URI}}"`, `'${JSON.stringify(options.remoteUserDataUri)}'`)
|
||||
.replace(`"{{PRODUCT_CONFIGURATION}}"`, `'${JSON.stringify(options.productConfiguration)}'`)
|
||||
.replace(`"{{WORKBENCH_WEB_CONFIGURATION}}"`, `'${JSON.stringify(options.workbenchWebConfiguration)}'`)
|
||||
.replace(`"{{NLS_CONFIGURATION}}"`, `'${JSON.stringify(options.nlsConfiguration)}'`),
|
||||
)
|
||||
})
|
||||
|
||||
/**
|
||||
* TODO: Might currently be unused.
|
||||
*/
|
||||
router.get("/resource(/*)?", ensureAuthenticated, async (req, res) => {
|
||||
const path = getFirstString(req.query.path)
|
||||
if (path) {
|
||||
res.set("Content-Type", getMediaMime(path))
|
||||
res.send(await fs.readFile(pathToFsPath(path)))
|
||||
}
|
||||
})
|
||||
|
||||
/**
|
||||
* Used by VS Code to load files.
|
||||
*/
|
||||
router.get("/vscode-remote-resource(/*)?", ensureAuthenticated, async (req, res) => {
|
||||
const path = getFirstString(req.query.path)
|
||||
if (path) {
|
||||
res.set("Content-Type", getMediaMime(path))
|
||||
res.send(await fs.readFile(pathToFsPath(path)))
|
||||
}
|
||||
})
|
||||
|
||||
/**
|
||||
* VS Code webviews use these paths to load files and to load webview assets
|
||||
* like HTML and JavaScript.
|
||||
*/
|
||||
router.get("/webview/*", ensureAuthenticated, async (req, res) => {
|
||||
res.set("Content-Type", getMediaMime(req.path))
|
||||
if (/^vscode-resource/.test(req.params[0])) {
|
||||
return res.send(await fs.readFile(req.params[0].replace(/^vscode-resource(\/file)?/, "")))
|
||||
}
|
||||
return res.send(
|
||||
await fs.readFile(path.join(vscode.vsRootPath, "out/vs/workbench/contrib/webview/browser/pre", req.params[0])),
|
||||
)
|
||||
})
|
||||
|
||||
interface Callback {
|
||||
uri: {
|
||||
scheme: string
|
||||
authority?: string
|
||||
path?: string
|
||||
query?: string
|
||||
fragment?: string
|
||||
}
|
||||
timeout: NodeJS.Timeout
|
||||
}
|
||||
|
||||
const callbacks = new Map<string, Callback>()
|
||||
const callbackEmitter = new Emitter<{ id: string; callback: Callback }>()
|
||||
|
||||
/**
|
||||
* Get vscode-requestId from the query and throw if it's missing or invalid.
|
||||
*/
|
||||
const getRequestId = (req: Request): string => {
|
||||
if (!req.query["vscode-requestId"]) {
|
||||
throw new HttpError("vscode-requestId is missing", HttpCode.BadRequest)
|
||||
}
|
||||
|
||||
if (typeof req.query["vscode-requestId"] !== "string") {
|
||||
throw new HttpError("vscode-requestId is not a string", HttpCode.BadRequest)
|
||||
}
|
||||
|
||||
return req.query["vscode-requestId"]
|
||||
}
|
||||
|
||||
// Matches VS Code's fetch timeout.
|
||||
const fetchTimeout = 5 * 60 * 1000
|
||||
|
||||
// The callback endpoints are used during authentication. A URI is stored on
|
||||
// /callback and then fetched later on /fetch-callback.
|
||||
// See ../../../lib/vscode/resources/web/code-web.js
|
||||
router.get("/callback", ensureAuthenticated, async (req, res) => {
|
||||
const uriKeys = [
|
||||
"vscode-requestId",
|
||||
"vscode-scheme",
|
||||
"vscode-authority",
|
||||
"vscode-path",
|
||||
"vscode-query",
|
||||
"vscode-fragment",
|
||||
]
|
||||
|
||||
const id = getRequestId(req)
|
||||
|
||||
// Move any query variables that aren't URI keys into the URI's query
|
||||
// (importantly, this will include the code for oauth).
|
||||
const query: qs.ParsedQs = {}
|
||||
for (const key in req.query) {
|
||||
if (!uriKeys.includes(key)) {
|
||||
query[key] = req.query[key]
|
||||
try {
|
||||
this._codeServerMain = await createVSServer(null, await toVsCodeArgs(args))
|
||||
} catch (createServerError) {
|
||||
logError(logger, "CodeServerRouteWrapper", createServerError)
|
||||
return next(createServerError)
|
||||
}
|
||||
|
||||
return next()
|
||||
}
|
||||
|
||||
const callback = {
|
||||
uri: {
|
||||
scheme: getFirstString(req.query["vscode-scheme"]) || "code-oss",
|
||||
authority: getFirstString(req.query["vscode-authority"]),
|
||||
path: getFirstString(req.query["vscode-path"]),
|
||||
query: (getFirstString(req.query.query) || "") + "&" + qs.stringify(query),
|
||||
fragment: getFirstString(req.query["vscode-fragment"]),
|
||||
},
|
||||
// Make sure the map doesn't leak if nothing fetches this URI.
|
||||
timeout: setTimeout(() => callbacks.delete(id), fetchTimeout),
|
||||
constructor() {
|
||||
this.router.get("/", this.ensureCodeServerLoaded, this.$root)
|
||||
this.router.all("*", ensureAuthenticated, this.ensureCodeServerLoaded, this.$proxyRequest)
|
||||
this._wsRouterWrapper.ws("/", ensureAuthenticated, this.ensureCodeServerLoaded, this.$proxyWebsocket)
|
||||
}
|
||||
|
||||
callbacks.set(id, callback)
|
||||
callbackEmitter.emit({ id, callback })
|
||||
|
||||
res.sendFile(path.join(rootPath, "vendor/modules/code-oss-dev/resources/web/callback.html"))
|
||||
})
|
||||
|
||||
router.get("/fetch-callback", ensureAuthenticated, async (req, res) => {
|
||||
const id = getRequestId(req)
|
||||
|
||||
const send = (callback: Callback) => {
|
||||
clearTimeout(callback.timeout)
|
||||
callbacks.delete(id)
|
||||
res.json(callback.uri)
|
||||
dispose() {
|
||||
this._codeServerMain?.dispose()
|
||||
}
|
||||
|
||||
const callback = callbacks.get(id)
|
||||
if (callback) {
|
||||
return send(callback)
|
||||
}
|
||||
|
||||
// VS Code will try again if the route returns no content but it seems more
|
||||
// efficient to just wait on this request for as long as possible?
|
||||
const handler = callbackEmitter.event(({ id: emitId, callback }) => {
|
||||
if (id === emitId) {
|
||||
handler.dispose()
|
||||
send(callback)
|
||||
}
|
||||
})
|
||||
|
||||
// If the client closes the connection.
|
||||
req.on("close", () => handler.dispose())
|
||||
})
|
||||
|
||||
export const wsRouter = WsRouter()
|
||||
|
||||
wsRouter.ws("/", ensureAuthenticated, async (req) => {
|
||||
const magic = "258EAFA5-E914-47DA-95CA-C5AB0DC85B11"
|
||||
const reply = crypto
|
||||
.createHash("sha1")
|
||||
.update(req.headers["sec-websocket-key"] + magic)
|
||||
.digest("base64")
|
||||
|
||||
const responseHeaders = [
|
||||
"HTTP/1.1 101 Switching Protocols",
|
||||
"Upgrade: websocket",
|
||||
"Connection: Upgrade",
|
||||
`Sec-WebSocket-Accept: ${reply}`,
|
||||
]
|
||||
|
||||
// See if the browser reports it supports web socket compression.
|
||||
// TODO: Parse this header properly.
|
||||
const extensions = req.headers["sec-websocket-extensions"]
|
||||
const isCompressionSupported = extensions ? extensions.includes("permessage-deflate") : false
|
||||
|
||||
// TODO: For now we only use compression if the user enables it.
|
||||
const isCompressionEnabled = !!req.args.enable?.includes(Feature.PermessageDeflate)
|
||||
|
||||
const useCompression = isCompressionEnabled && isCompressionSupported
|
||||
if (useCompression) {
|
||||
// This response header tells the browser the server supports compression.
|
||||
responseHeaders.push("Sec-WebSocket-Extensions: permessage-deflate; server_max_window_bits=15")
|
||||
}
|
||||
|
||||
req.ws.write(responseHeaders.join("\r\n") + "\r\n\r\n")
|
||||
|
||||
await vscode.sendWebsocket(req.ws, req.query, useCompression)
|
||||
})
|
||||
}
|
||||
|
||||
@@ -20,7 +20,7 @@ export class SettingsProvider<T> {
|
||||
try {
|
||||
const raw = (await fs.readFile(this.settingsPath, "utf8")).trim()
|
||||
return raw ? JSON.parse(raw) : {}
|
||||
} catch (error) {
|
||||
} catch (error: any) {
|
||||
if (error.code !== "ENOENT") {
|
||||
logger.warn(error.message)
|
||||
}
|
||||
@@ -37,7 +37,7 @@ export class SettingsProvider<T> {
|
||||
const oldSettings = await this.read()
|
||||
const nextSettings = { ...oldSettings, ...settings }
|
||||
await fs.writeFile(this.settingsPath, JSON.stringify(nextSettings, null, 2))
|
||||
} catch (error) {
|
||||
} catch (error: any) {
|
||||
logger.warn(error.message)
|
||||
}
|
||||
}
|
||||
|
||||
@@ -60,7 +60,7 @@ export class UpdateProvider {
|
||||
}
|
||||
logger.debug("got latest version", field("latest", update.version))
|
||||
return update
|
||||
} catch (error) {
|
||||
} catch (error: any) {
|
||||
logger.error("Failed to get latest version", field("error", error.message))
|
||||
return {
|
||||
checked: now,
|
||||
|
||||
@@ -1,66 +0,0 @@
|
||||
// In a bit of a hack, this file is stored in two places
|
||||
// - src/node/uri_transformer.ts
|
||||
// - lib/vscode/src/vs/server/uriTransformer.ts
|
||||
|
||||
// The reason for this is that we need a CommonJS-compiled
|
||||
// version of this file to supply as a command line argument
|
||||
// to extensionHostProcessSetup.ts; but we also need to include
|
||||
// it ourselves cleanly in `lib/vscode/src/vs/server`.
|
||||
|
||||
// @oxy: Could not figure out how to compile as a CommonJS module
|
||||
// in the same tree as VSCode, which is why I came up with the solution
|
||||
// of storing it in two places.
|
||||
|
||||
// NOTE: copied over from lib/vscode/src/vs/common/uriIpc.ts
|
||||
// remember to update this for proper type checks!
|
||||
|
||||
interface UriParts {
|
||||
scheme: string
|
||||
authority?: string
|
||||
path?: string
|
||||
}
|
||||
|
||||
interface IRawURITransformer {
|
||||
transformIncoming(uri: UriParts): UriParts
|
||||
transformOutgoing(uri: UriParts): UriParts
|
||||
transformOutgoingScheme(scheme: string): string
|
||||
}
|
||||
|
||||
// Using `export =` is deliberate.
|
||||
// See lib/vscode/src/vs/workbench/services/extensions/node/extensionHostProcessSetup.ts;
|
||||
// they include the file directly with a node require and expect a function as `module.exports`.
|
||||
// `export =` in TypeScript is equivalent to `module.exports =` in vanilla JS.
|
||||
export = function rawURITransformerFactory(authority: string) {
|
||||
return new RawURITransformer(authority)
|
||||
}
|
||||
|
||||
class RawURITransformer implements IRawURITransformer {
|
||||
constructor(private readonly authority: string) {}
|
||||
|
||||
transformIncoming(uri: UriParts): UriParts {
|
||||
switch (uri.scheme) {
|
||||
case "vscode-remote":
|
||||
return { scheme: "file", path: uri.path }
|
||||
default:
|
||||
return uri
|
||||
}
|
||||
}
|
||||
|
||||
transformOutgoing(uri: UriParts): UriParts {
|
||||
switch (uri.scheme) {
|
||||
case "file":
|
||||
return { scheme: "vscode-remote", authority: this.authority, path: uri.path }
|
||||
default:
|
||||
return uri
|
||||
}
|
||||
}
|
||||
|
||||
transformOutgoingScheme(scheme: string): string {
|
||||
switch (scheme) {
|
||||
case "file":
|
||||
return "vscode-remote"
|
||||
default:
|
||||
return scheme
|
||||
}
|
||||
}
|
||||
}
|
||||
160
src/node/util.ts
160
src/node/util.ts
@@ -3,14 +3,15 @@ import * as argon2 from "argon2"
|
||||
import * as cp from "child_process"
|
||||
import * as crypto from "crypto"
|
||||
import envPaths from "env-paths"
|
||||
import { promises as fs } from "fs"
|
||||
import { promises as fs, Stats } from "fs"
|
||||
import * as net from "net"
|
||||
import * as os from "os"
|
||||
import * as path from "path"
|
||||
import safeCompare from "safe-compare"
|
||||
import * as util from "util"
|
||||
import xdgBasedir from "xdg-basedir"
|
||||
import { getFirstString } from "../common/util"
|
||||
import { logError } from "../common/util"
|
||||
import { isDevMode, rootPath, vsRootPath } from "./constants"
|
||||
|
||||
export interface Paths {
|
||||
data: string
|
||||
@@ -25,10 +26,11 @@ const pattern = [
|
||||
].join("|")
|
||||
const re = new RegExp(pattern, "g")
|
||||
|
||||
export type OnLineCallback = (strippedLine: string, originalLine: string) => void
|
||||
/**
|
||||
* Split stdout on newlines and strip ANSI codes.
|
||||
*/
|
||||
export const onLine = (proc: cp.ChildProcess, callback: (strippedLine: string, originalLine: string) => void): void => {
|
||||
export const onLine = (proc: cp.ChildProcess, callback: OnLineCallback): void => {
|
||||
let buffer = ""
|
||||
if (!proc.stdout) {
|
||||
throw new Error("no stdout")
|
||||
@@ -88,16 +90,17 @@ export function getEnvPaths(): Paths {
|
||||
}
|
||||
|
||||
/**
|
||||
* humanPath replaces the home directory in p with ~.
|
||||
* humanPath replaces the home directory in path with ~.
|
||||
* Makes it more readable.
|
||||
*
|
||||
* @param p
|
||||
* @param homedir - the home directory(i.e. `os.homedir()`)
|
||||
* @param path - a file path
|
||||
*/
|
||||
export function humanPath(p?: string): string {
|
||||
if (!p) {
|
||||
export function humanPath(homedir: string, path?: string): string {
|
||||
if (!path) {
|
||||
return ""
|
||||
}
|
||||
return p.replace(os.homedir(), "~")
|
||||
return path.replace(homedir, "~")
|
||||
}
|
||||
|
||||
export const generateCertificate = async (hostname: string): Promise<{ cert: string; certKey: string }> => {
|
||||
@@ -157,7 +160,7 @@ export const generatePassword = async (length = 24): Promise<string> => {
|
||||
export const hash = async (password: string): Promise<string> => {
|
||||
try {
|
||||
return await argon2.hash(password)
|
||||
} catch (error) {
|
||||
} catch (error: any) {
|
||||
logger.error(error)
|
||||
return ""
|
||||
}
|
||||
@@ -172,7 +175,7 @@ export const isHashMatch = async (password: string, hash: string) => {
|
||||
}
|
||||
try {
|
||||
return await argon2.verify(hash, password)
|
||||
} catch (error) {
|
||||
} catch (error: any) {
|
||||
throw new Error(error)
|
||||
}
|
||||
}
|
||||
@@ -318,10 +321,10 @@ export async function isCookieValid({
|
||||
* - greater than 0 characters
|
||||
* - trims whitespace
|
||||
*/
|
||||
export function sanitizeString(str: string): string {
|
||||
export function sanitizeString(str: unknown): string {
|
||||
// Very basic sanitization of string
|
||||
// Credit: https://stackoverflow.com/a/46719000/3015595
|
||||
return typeof str === "string" && str.trim().length > 0 ? str.trim() : ""
|
||||
return typeof str === "string" ? str.trim() : ""
|
||||
}
|
||||
|
||||
const mimeTypes: { [key: string]: string } = {
|
||||
@@ -393,9 +396,17 @@ export const isWsl = async (): Promise<boolean> => {
|
||||
}
|
||||
|
||||
/**
|
||||
* Try opening a URL using whatever the system has set for opening URLs.
|
||||
* Try opening an address using whatever the system has set for opening URLs.
|
||||
*/
|
||||
export const open = async (url: string): Promise<void> => {
|
||||
export const open = async (address: URL | string): Promise<void> => {
|
||||
if (typeof address === "string") {
|
||||
throw new Error("Cannot open socket paths")
|
||||
}
|
||||
// Web sockets do not seem to work if browsing with 0.0.0.0.
|
||||
const url = new URL(address)
|
||||
if (url.hostname === "0.0.0.0") {
|
||||
url.hostname = "localhost"
|
||||
}
|
||||
const args = [] as string[]
|
||||
const options = {} as cp.SpawnOptions
|
||||
const platform = (await isWsl()) ? "wsl" : process.platform
|
||||
@@ -403,9 +414,9 @@ export const open = async (url: string): Promise<void> => {
|
||||
if (platform === "win32" || platform === "wsl") {
|
||||
command = platform === "wsl" ? "cmd.exe" : "cmd"
|
||||
args.push("/c", "start", '""', "/b")
|
||||
url = url.replace(/&/g, "^&")
|
||||
url.search = url.search.replace(/&/g, "^&")
|
||||
}
|
||||
const proc = cp.spawn(command, [...args, url], options)
|
||||
const proc = cp.spawn(command, [...args, url.toString()], options)
|
||||
await new Promise<void>((resolve, reject) => {
|
||||
proc.on("error", reject)
|
||||
proc.on("close", (code) => {
|
||||
@@ -439,55 +450,6 @@ export const isObject = <T extends object>(obj: T): obj is T => {
|
||||
return !Array.isArray(obj) && typeof obj === "object" && obj !== null
|
||||
}
|
||||
|
||||
/**
|
||||
* 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`).
|
||||
*/
|
||||
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: getFirstString(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
|
||||
}
|
||||
|
||||
/**
|
||||
* Return a promise that resolves with whether the socket path is active.
|
||||
*/
|
||||
@@ -524,3 +486,71 @@ export function escapeHtml(unsafe: string): string {
|
||||
.replace(/"/g, """)
|
||||
.replace(/'/g, "'")
|
||||
}
|
||||
|
||||
/**
|
||||
* A helper function which returns a boolean indicating whether
|
||||
* the given error is a NodeJS.ErrnoException by checking if
|
||||
* it has a .code property.
|
||||
*/
|
||||
export function isNodeJSErrnoException(error: unknown): error is NodeJS.ErrnoException {
|
||||
return error !== undefined && (error as NodeJS.ErrnoException).code !== undefined
|
||||
}
|
||||
|
||||
// TODO: Replace with proper templating system.
|
||||
export const escapeJSON = (value: cp.Serializable) => JSON.stringify(value).replace(/"/g, """)
|
||||
|
||||
type AMDModule<T> = { [exportName: string]: T }
|
||||
|
||||
/**
|
||||
* Loads AMD module, typically from a compiled VSCode bundle.
|
||||
*
|
||||
* @deprecated This should be gradually phased out as code-server migrates to lib/vscode
|
||||
* @param amdPath Path to module relative to lib/vscode
|
||||
* @param exportName Given name of export in the file
|
||||
*/
|
||||
export const loadAMDModule = async <T>(amdPath: string, exportName: string): Promise<T> => {
|
||||
// Set default remote native node modules path, if unset
|
||||
process.env["VSCODE_INJECT_NODE_MODULE_LOOKUP_PATH"] =
|
||||
process.env["VSCODE_INJECT_NODE_MODULE_LOOKUP_PATH"] || path.join(vsRootPath, "remote", "node_modules")
|
||||
|
||||
require(path.join(vsRootPath, "out/bootstrap-node")).injectNodeModuleLookupPath(
|
||||
process.env["VSCODE_INJECT_NODE_MODULE_LOOKUP_PATH"],
|
||||
)
|
||||
|
||||
const module = await new Promise<AMDModule<T>>((resolve, reject) => {
|
||||
require(path.join(vsRootPath, "out/bootstrap-amd")).load(amdPath, resolve, reject)
|
||||
})
|
||||
|
||||
return module[exportName] as T
|
||||
}
|
||||
|
||||
export interface CompilationStats {
|
||||
lastCompiledAt: Date
|
||||
}
|
||||
|
||||
export const readCompilationStats = async (): Promise<null | CompilationStats> => {
|
||||
if (!isDevMode) {
|
||||
throw new Error("Compilation stats are only present in development")
|
||||
}
|
||||
|
||||
const filePath = path.join(rootPath, "out/watcher.json")
|
||||
let stat: Stats
|
||||
try {
|
||||
stat = await fs.stat(filePath)
|
||||
} catch (error) {
|
||||
return null
|
||||
}
|
||||
|
||||
if (!stat.isFile()) {
|
||||
return null
|
||||
}
|
||||
|
||||
try {
|
||||
const file = await fs.readFile(filePath)
|
||||
return JSON.parse(file.toString("utf-8"))
|
||||
} catch (error) {
|
||||
logError(logger, "VS Code", error)
|
||||
}
|
||||
|
||||
return null
|
||||
}
|
||||
|
||||
@@ -1,168 +0,0 @@
|
||||
import { logger } from "@coder/logger"
|
||||
import * as cp from "child_process"
|
||||
import * as net from "net"
|
||||
import * as path from "path"
|
||||
import * as ipc from "../../typings/ipc"
|
||||
import { arrayify, generateUuid } from "../common/util"
|
||||
import { rootPath } from "./constants"
|
||||
import { settings } from "./settings"
|
||||
import { SocketProxyProvider } from "./socket"
|
||||
import { isFile } from "./util"
|
||||
import { onMessage, wrapper } from "./wrapper"
|
||||
|
||||
export class VscodeProvider {
|
||||
public readonly serverRootPath: string
|
||||
public readonly vsRootPath: string
|
||||
private _vscode?: Promise<cp.ChildProcess>
|
||||
private readonly socketProvider = new SocketProxyProvider()
|
||||
|
||||
public constructor() {
|
||||
this.vsRootPath = path.resolve(rootPath, "vendor/modules/code-oss-dev")
|
||||
this.serverRootPath = path.join(this.vsRootPath, "out/vs/server")
|
||||
wrapper.onDispose(() => this.dispose())
|
||||
}
|
||||
|
||||
public async dispose(): Promise<void> {
|
||||
this.socketProvider.stop()
|
||||
if (this._vscode) {
|
||||
const vscode = await this._vscode
|
||||
vscode.removeAllListeners()
|
||||
vscode.kill()
|
||||
this._vscode = undefined
|
||||
}
|
||||
}
|
||||
|
||||
public async initialize(
|
||||
options: Omit<ipc.VscodeOptions, "startPath">,
|
||||
query: ipc.Query,
|
||||
): Promise<ipc.WorkbenchOptions> {
|
||||
const { lastVisited } = await settings.read()
|
||||
let startPath = await this.getFirstPath([
|
||||
{ url: query.workspace, workspace: true },
|
||||
{ url: query.folder, workspace: false },
|
||||
options.args._ && options.args._.length > 0
|
||||
? { url: path.resolve(options.args._[options.args._.length - 1]) }
|
||||
: undefined,
|
||||
!options.args["ignore-last-opened"] ? lastVisited : undefined,
|
||||
])
|
||||
|
||||
if (query.ew) {
|
||||
startPath = undefined
|
||||
}
|
||||
|
||||
settings.write({
|
||||
lastVisited: startPath,
|
||||
query,
|
||||
})
|
||||
|
||||
const id = generateUuid()
|
||||
const vscode = await this.fork()
|
||||
|
||||
logger.debug("setting up vs code...")
|
||||
|
||||
this.send(
|
||||
{
|
||||
type: "init",
|
||||
id,
|
||||
options: {
|
||||
...options,
|
||||
startPath,
|
||||
},
|
||||
},
|
||||
vscode,
|
||||
)
|
||||
|
||||
const message = await onMessage<ipc.VscodeMessage, ipc.OptionsMessage>(
|
||||
vscode,
|
||||
(message): message is ipc.OptionsMessage => {
|
||||
// There can be parallel initializations so wait for the right ID.
|
||||
return message.type === "options" && message.id === id
|
||||
},
|
||||
)
|
||||
|
||||
return message.options
|
||||
}
|
||||
|
||||
private fork(): Promise<cp.ChildProcess> {
|
||||
if (this._vscode) {
|
||||
return this._vscode
|
||||
}
|
||||
|
||||
logger.debug("forking vs code...")
|
||||
const vscode = cp.fork(path.join(this.serverRootPath, "fork"))
|
||||
|
||||
const dispose = () => {
|
||||
vscode.removeAllListeners()
|
||||
vscode.kill()
|
||||
this._vscode = undefined
|
||||
}
|
||||
|
||||
vscode.on("error", (error: Error) => {
|
||||
logger.error(error.message)
|
||||
if (error.stack) {
|
||||
logger.debug(error.stack)
|
||||
}
|
||||
dispose()
|
||||
})
|
||||
|
||||
vscode.on("exit", (code) => {
|
||||
logger.error(`VS Code exited unexpectedly with code ${code}`)
|
||||
dispose()
|
||||
})
|
||||
|
||||
this._vscode = onMessage<ipc.VscodeMessage, ipc.ReadyMessage>(vscode, (message): message is ipc.ReadyMessage => {
|
||||
return message.type === "ready"
|
||||
}).then(() => vscode)
|
||||
|
||||
return this._vscode
|
||||
}
|
||||
|
||||
/**
|
||||
* VS Code expects a raw socket. It will handle all the web socket frames.
|
||||
*/
|
||||
public async sendWebsocket(socket: net.Socket, query: ipc.Query, permessageDeflate: boolean): Promise<void> {
|
||||
const vscode = await this._vscode
|
||||
// TLS sockets cannot be transferred to child processes so we need an
|
||||
// in-between. Non-TLS sockets will be returned as-is.
|
||||
const socketProxy = await this.socketProvider.createProxy(socket)
|
||||
this.send({ type: "socket", query, permessageDeflate }, vscode, socketProxy)
|
||||
}
|
||||
|
||||
private send(message: ipc.CodeServerMessage, vscode?: cp.ChildProcess, socket?: net.Socket): void {
|
||||
if (!vscode || vscode.killed) {
|
||||
throw new Error("vscode is not running")
|
||||
}
|
||||
vscode.send(message, socket)
|
||||
}
|
||||
|
||||
/**
|
||||
* Choose the first non-empty path from the provided array.
|
||||
*
|
||||
* Each array item consists of `url` and an optional `workspace` boolean that
|
||||
* indicates whether that url is for a workspace.
|
||||
*
|
||||
* `url` can be a fully qualified URL or just the path portion.
|
||||
*
|
||||
* `url` can also be a query object to make it easier to pass in query
|
||||
* variables directly but anything that isn't a string or string array is not
|
||||
* valid and will be ignored.
|
||||
*/
|
||||
private async getFirstPath(
|
||||
startPaths: Array<{ url?: string | string[] | ipc.Query | ipc.Query[]; workspace?: boolean } | undefined>,
|
||||
): Promise<ipc.StartPath | undefined> {
|
||||
for (let i = 0; i < startPaths.length; ++i) {
|
||||
const startPath = startPaths[i]
|
||||
const url = arrayify(startPath && startPath.url).find((p) => !!p)
|
||||
if (startPath && url && typeof url === "string") {
|
||||
return {
|
||||
url,
|
||||
// The only time `workspace` is undefined is for the command-line
|
||||
// argument, in which case it's a path (not a URL) so we can stat it
|
||||
// without having to parse it.
|
||||
workspace: typeof startPath.workspace !== "undefined" ? startPath.workspace : await isFile(url),
|
||||
}
|
||||
}
|
||||
}
|
||||
return undefined
|
||||
}
|
||||
}
|
||||
@@ -267,7 +267,7 @@ export class ParentProcess extends Process {
|
||||
try {
|
||||
this.started = this._start()
|
||||
await this.started
|
||||
} catch (error) {
|
||||
} catch (error: any) {
|
||||
this.logger.error(error.message)
|
||||
this.exit(typeof error.code === "number" ? error.code : 1)
|
||||
}
|
||||
@@ -314,7 +314,7 @@ export class ParentProcess extends Process {
|
||||
CODE_SERVER_PARENT_PID: process.pid.toString(),
|
||||
NODE_OPTIONS: `--max-old-space-size=2048 ${process.env.NODE_OPTIONS || ""}`,
|
||||
},
|
||||
stdio: ["inherit", "inherit", "inherit", "ipc"],
|
||||
stdio: ["pipe", "pipe", "pipe", "ipc"],
|
||||
})
|
||||
}
|
||||
|
||||
|
||||
@@ -50,4 +50,5 @@ export function Router(): WebsocketRouter {
|
||||
return new WebsocketRouter()
|
||||
}
|
||||
|
||||
// eslint-disable-next-line import/no-named-as-default-member -- the typings are not updated correctly
|
||||
export const wss = new Websocket.Server({ noServer: true })
|
||||
|
||||
@@ -1,15 +0,0 @@
|
||||
import { describe, test, expect } from "./baseFixture"
|
||||
|
||||
// This is a "gut-check" test to make sure playwright is working as expected
|
||||
describe("browser", true, () => {
|
||||
test("browser should display correct userAgent", async ({ codeServerPage, browserName }) => {
|
||||
const displayNames = {
|
||||
chromium: "Chrome",
|
||||
firefox: "Firefox",
|
||||
webkit: "Safari",
|
||||
}
|
||||
const userAgent = await codeServerPage.page.evaluate(() => navigator.userAgent)
|
||||
|
||||
expect(userAgent).toContain(displayNames[browserName])
|
||||
})
|
||||
})
|
||||
@@ -1,11 +1,12 @@
|
||||
import { describe, test, expect } from "./baseFixture"
|
||||
// NOTE@jsjoeio commenting out until we can figure out what's wrong
|
||||
// import { describe, test, expect } from "./baseFixture"
|
||||
|
||||
describe("logout", true, () => {
|
||||
test("should be able logout", async ({ codeServerPage }) => {
|
||||
// Recommended by Playwright for async navigation
|
||||
// https://github.com/microsoft/playwright/issues/1987#issuecomment-620182151
|
||||
await Promise.all([codeServerPage.page.waitForNavigation(), codeServerPage.navigateMenus(["Log Out"])])
|
||||
const currentUrl = codeServerPage.page.url()
|
||||
expect(currentUrl).toBe(`${await codeServerPage.address()}/login`)
|
||||
})
|
||||
})
|
||||
// describe("logout", true, () => {
|
||||
// test("should be able logout", async ({ codeServerPage }) => {
|
||||
// // Recommended by Playwright for async navigation
|
||||
// // https://github.com/microsoft/playwright/issues/1987#issuecomment-620182151
|
||||
// await Promise.all([codeServerPage.page.waitForNavigation(), codeServerPage.navigateMenus(["Log Out"])])
|
||||
// const currentUrl = codeServerPage.page.url()
|
||||
// expect(currentUrl).toBe(`${await codeServerPage.address()}/login`)
|
||||
// })
|
||||
// })
|
||||
|
||||
@@ -1,11 +1,12 @@
|
||||
import { Logger, logger } from "@coder/logger"
|
||||
import { field, Logger, logger } from "@coder/logger"
|
||||
import * as cp from "child_process"
|
||||
import { promises as fs } from "fs"
|
||||
import * as path from "path"
|
||||
import { Page } from "playwright"
|
||||
import { logError } from "../../../src/common/util"
|
||||
import { onLine } from "../../../src/node/util"
|
||||
import { PASSWORD, workspaceDir } from "../../utils/constants"
|
||||
import { tmpdir } from "../../utils/helpers"
|
||||
import { idleTimer, tmpdir } from "../../utils/helpers"
|
||||
|
||||
interface CodeServerProcess {
|
||||
process: cp.ChildProcess
|
||||
@@ -51,9 +52,9 @@ export class CodeServer {
|
||||
*/
|
||||
private async createWorkspace(): Promise<string> {
|
||||
const dir = await tmpdir(workspaceDir)
|
||||
await fs.mkdir(path.join(dir, ".vscode"))
|
||||
await fs.mkdir(path.join(dir, "User"))
|
||||
await fs.writeFile(
|
||||
path.join(dir, ".vscode/settings.json"),
|
||||
path.join(dir, "User/settings.json"),
|
||||
JSON.stringify({
|
||||
"workbench.startupEditor": "none",
|
||||
}),
|
||||
@@ -99,34 +100,44 @@ export class CodeServer {
|
||||
},
|
||||
)
|
||||
|
||||
const timer = idleTimer("Failed to extract address; did the format change?", reject)
|
||||
|
||||
proc.on("error", (error) => {
|
||||
this.logger.error(error.message)
|
||||
timer.dispose()
|
||||
reject(error)
|
||||
})
|
||||
|
||||
proc.on("close", () => {
|
||||
proc.on("close", (code) => {
|
||||
const error = new Error("closed unexpectedly")
|
||||
if (!this.closed) {
|
||||
this.logger.error(error.message)
|
||||
this.logger.error(error.message, field("code", code))
|
||||
}
|
||||
timer.dispose()
|
||||
reject(error)
|
||||
})
|
||||
|
||||
let resolved = false
|
||||
proc.stdout.setEncoding("utf8")
|
||||
onLine(proc, (line) => {
|
||||
// As long as we are actively getting input reset the timer. If we stop
|
||||
// getting input and still have not found the address the timer will
|
||||
// reject.
|
||||
timer.reset()
|
||||
|
||||
// Log the line without the timestamp.
|
||||
this.logger.trace(line.replace(/\[.+\]/, ""))
|
||||
if (resolved) {
|
||||
return
|
||||
}
|
||||
const match = line.trim().match(/HTTP server listening on (https?:\/\/[.:\d]+)$/)
|
||||
const match = line.trim().match(/HTTPS? server listening on (https?:\/\/[.:\d]+)\/?$/)
|
||||
if (match) {
|
||||
// Cookies don't seem to work on IP address so swap to localhost.
|
||||
// TODO: Investigate whether this is a bug with code-server.
|
||||
const address = match[1].replace("127.0.0.1", "localhost")
|
||||
this.logger.debug(`spawned on ${address}`)
|
||||
resolved = true
|
||||
timer.dispose()
|
||||
resolve({ process: proc, address })
|
||||
}
|
||||
})
|
||||
@@ -156,7 +167,14 @@ export class CodeServer {
|
||||
export class CodeServerPage {
|
||||
private readonly editorSelector = "div.monaco-workbench"
|
||||
|
||||
constructor(private readonly codeServer: CodeServer, public readonly page: Page) {}
|
||||
constructor(private readonly codeServer: CodeServer, public readonly page: Page) {
|
||||
this.page.on("console", (message) => {
|
||||
this.codeServer.logger.debug(message)
|
||||
})
|
||||
this.page.on("pageerror", (error) => {
|
||||
logError(this.codeServer.logger, "page", error)
|
||||
})
|
||||
}
|
||||
|
||||
address() {
|
||||
return this.codeServer.address()
|
||||
|
||||
@@ -2,23 +2,28 @@
|
||||
"license": "MIT",
|
||||
"#": "We must put jest in a sub-directory otherwise VS Code somehow picks up the types and generates conflicts with mocha.",
|
||||
"devDependencies": {
|
||||
"@playwright/test": "^1.12.1",
|
||||
"@types/jest": "^26.0.20",
|
||||
"@types/jsdom": "^16.2.6",
|
||||
"@playwright/test": "^1.16.3",
|
||||
"@types/jest": "^27.0.2",
|
||||
"@types/jsdom": "^16.2.13",
|
||||
"@types/node-fetch": "^2.5.8",
|
||||
"@types/supertest": "^2.0.10",
|
||||
"@types/supertest": "^2.0.11",
|
||||
"@types/wtfnode": "^0.7.0",
|
||||
"argon2": "^0.28.0",
|
||||
"jest": "^26.6.3",
|
||||
"jest": "^27.3.1",
|
||||
"jest-fetch-mock": "^3.0.3",
|
||||
"jsdom": "^16.4.0",
|
||||
"node-fetch": "^2.6.1",
|
||||
"playwright": "^1.12.1",
|
||||
"supertest": "^6.1.1",
|
||||
"ts-jest": "^26.4.4",
|
||||
"wtfnode": "^0.9.0"
|
||||
"playwright": "^1.16.3",
|
||||
"supertest": "^6.1.6",
|
||||
"ts-jest": "^27.0.7",
|
||||
"wtfnode": "^0.9.1"
|
||||
},
|
||||
"resolutions": {
|
||||
"argon2/@mapbox/node-pre-gyp/tar": "^6.1.9"
|
||||
"ansi-regex": "^5.0.1",
|
||||
"argon2/@mapbox/node-pre-gyp/tar": "^6.1.9",
|
||||
"set-value": "^4.0.1",
|
||||
"tmpl": "^1.0.5",
|
||||
"path-parse": "^1.0.7",
|
||||
"json-schema": "^0.4.0"
|
||||
}
|
||||
}
|
||||
|
||||
@@ -2,7 +2,12 @@ import { PlaywrightTestConfig } from "@playwright/test"
|
||||
|
||||
import path from "path"
|
||||
|
||||
// Run tests in three browsers.
|
||||
// The default configuration runs all tests in three browsers with workers equal
|
||||
// to half the available threads. See 'yarn test:e2e --help' to customize from
|
||||
// the command line. For example:
|
||||
// yarn test:e2e --workers 1 # Run with one worker
|
||||
// yarn test:e2e --project Chromium # Only run on Chromium
|
||||
// yarn test:e2e --grep login # Run tests matching "login"
|
||||
const config: PlaywrightTestConfig = {
|
||||
testDir: path.join(__dirname, "e2e"), // Search for tests in this directory.
|
||||
timeout: 60000, // Each test is given 60 seconds.
|
||||
|
||||
21
test/scripts/build-lib.bats
Normal file
21
test/scripts/build-lib.bats
Normal file
@@ -0,0 +1,21 @@
|
||||
#!/usr/bin/env bats
|
||||
|
||||
SCRIPT_NAME="build-lib.sh"
|
||||
SCRIPT="$BATS_TEST_DIRNAME/../../ci/build/$SCRIPT_NAME"
|
||||
|
||||
source "$SCRIPT"
|
||||
|
||||
@test "get_nfpm_arch should return armhfp for rpm on armv7l" {
|
||||
run get_nfpm_arch rpm armv7l
|
||||
[ "$output" = "armhfp" ]
|
||||
}
|
||||
|
||||
@test "get_nfpm_arch should return armhf for deb on armv7l" {
|
||||
run get_nfpm_arch deb armv7l
|
||||
[ "$output" = "armhf" ]
|
||||
}
|
||||
|
||||
@test "get_nfpm_arch should return arch if no arch override exists " {
|
||||
run get_nfpm_arch deb i386
|
||||
[ "$output" = "i386" ]
|
||||
}
|
||||
46
test/scripts/steps-lib.bats
Normal file
46
test/scripts/steps-lib.bats
Normal file
@@ -0,0 +1,46 @@
|
||||
#!/usr/bin/env bats
|
||||
|
||||
SCRIPT_NAME="steps-lib.sh"
|
||||
SCRIPT="$BATS_TEST_DIRNAME/../../ci/steps/$SCRIPT_NAME"
|
||||
|
||||
source "$SCRIPT"
|
||||
|
||||
@test "is_env_var_set should return 1 if env var is not set" {
|
||||
run is_env_var_set "ASDF_TEST_SET"
|
||||
[ "$status" = 1 ]
|
||||
}
|
||||
|
||||
@test "is_env_var_set should return 0 if env var is set" {
|
||||
ASDF_TEST_SET="test" run is_env_var_set "ASDF_TEST_SET"
|
||||
[ "$status" = 0 ]
|
||||
}
|
||||
|
||||
@test "directory_exists should 1 if directory doesn't exist" {
|
||||
run directory_exists "/tmp/asdfasdfasdf"
|
||||
[ "$status" = 1 ]
|
||||
}
|
||||
|
||||
@test "directory_exists should 0 if directory exists" {
|
||||
run directory_exists "$(pwd)"
|
||||
[ "$status" = 0 ]
|
||||
}
|
||||
|
||||
@test "file_exists should 1 if file doesn't exist" {
|
||||
run file_exists "hello-asfd.sh"
|
||||
[ "$status" = 1 ]
|
||||
}
|
||||
|
||||
@test "file_exists should 0 if file exists" {
|
||||
run file_exists "$SCRIPT"
|
||||
[ "$status" = 0 ]
|
||||
}
|
||||
|
||||
@test "is_executable should 1 if file isn't executable" {
|
||||
run is_executable "hello-asfd.sh"
|
||||
[ "$status" = 1 ]
|
||||
}
|
||||
|
||||
@test "is_executable should 0 if file is executable" {
|
||||
run is_executable "$SCRIPT"
|
||||
[ "$status" = 0 ]
|
||||
}
|
||||
@@ -1,90 +0,0 @@
|
||||
import { JSDOM } from "jsdom"
|
||||
import { LocationLike } from "../../common/util.test"
|
||||
|
||||
describe("login", () => {
|
||||
describe("there is an element with id 'base'", () => {
|
||||
beforeEach(() => {
|
||||
const dom = new JSDOM()
|
||||
global.document = dom.window.document
|
||||
|
||||
const location: LocationLike = {
|
||||
pathname: "/healthz",
|
||||
origin: "http://localhost:8080",
|
||||
}
|
||||
|
||||
global.location = location as Location
|
||||
})
|
||||
afterEach(() => {
|
||||
// Reset the global.document
|
||||
global.document = undefined as any as Document
|
||||
global.location = undefined as any as Location
|
||||
})
|
||||
it("should set the value to options.base", () => {
|
||||
// Mock getElementById
|
||||
const spy = jest.spyOn(document, "getElementById")
|
||||
// Create a fake element and set the attribute
|
||||
const mockElement = document.createElement("input")
|
||||
mockElement.setAttribute("id", "base")
|
||||
const expected = {
|
||||
base: "./hello-world",
|
||||
csStaticBase: "./static/development/Users/jp/Dev/code-server",
|
||||
logLevel: 2,
|
||||
disableTelemetry: false,
|
||||
disableUpdateCheck: false,
|
||||
}
|
||||
mockElement.setAttribute("data-settings", JSON.stringify(expected))
|
||||
document.body.appendChild(mockElement)
|
||||
spy.mockImplementation(() => mockElement)
|
||||
// Load file
|
||||
require("../../../../src/browser/pages/login")
|
||||
|
||||
const el: HTMLInputElement | null = document.querySelector("input#base")
|
||||
expect(el?.value).toBe("/hello-world")
|
||||
})
|
||||
})
|
||||
describe("there is not an element with id 'base'", () => {
|
||||
let spy: jest.SpyInstance
|
||||
|
||||
beforeAll(() => {
|
||||
// This is important because we're manually requiring the file
|
||||
// If you don't call this before all tests
|
||||
// the module registry from other tests may cause side effects.
|
||||
jest.resetModuleRegistry()
|
||||
})
|
||||
|
||||
beforeEach(() => {
|
||||
const dom = new JSDOM()
|
||||
global.document = dom.window.document
|
||||
spy = jest.spyOn(document, "getElementById")
|
||||
|
||||
const location: LocationLike = {
|
||||
pathname: "/healthz",
|
||||
origin: "http://localhost:8080",
|
||||
}
|
||||
|
||||
global.location = location as Location
|
||||
})
|
||||
|
||||
afterEach(() => {
|
||||
spy.mockClear()
|
||||
jest.resetModules()
|
||||
// Reset the global.document
|
||||
global.document = undefined as any as Document
|
||||
global.location = undefined as any as Location
|
||||
})
|
||||
|
||||
afterAll(() => {
|
||||
jest.restoreAllMocks()
|
||||
})
|
||||
|
||||
it("should do nothing", () => {
|
||||
spy.mockImplementation(() => null)
|
||||
// Load file
|
||||
require("../../../../src/browser/pages/login")
|
||||
|
||||
// It's called once by getOptions in the top of the file
|
||||
// and then another to get the base element
|
||||
expect(spy).toHaveBeenCalledTimes(2)
|
||||
})
|
||||
})
|
||||
})
|
||||
@@ -1,400 +0,0 @@
|
||||
/**
|
||||
* @jest-environment jsdom
|
||||
*/
|
||||
import fetchMock from "jest-fetch-mock"
|
||||
import { JSDOM } from "jsdom"
|
||||
import {
|
||||
getNlsConfiguration,
|
||||
nlsConfigElementId,
|
||||
getConfigurationForLoader,
|
||||
setBodyBackgroundToThemeBackgroundColor,
|
||||
_createScriptURL,
|
||||
main,
|
||||
createBundlePath,
|
||||
} from "../../../../src/browser/pages/vscode"
|
||||
|
||||
describe("vscode", () => {
|
||||
describe("getNlsConfiguration", () => {
|
||||
let _document: Document
|
||||
|
||||
beforeEach(() => {
|
||||
// We use underscores to not confuse with global values
|
||||
const { window: _window } = new JSDOM()
|
||||
_document = _window.document
|
||||
fetchMock.enableMocks()
|
||||
})
|
||||
|
||||
afterEach(() => {
|
||||
fetchMock.resetMocks()
|
||||
})
|
||||
|
||||
it("should throw an error if no nlsConfigElement", () => {
|
||||
const errorMsgPrefix = "[vscode]"
|
||||
const errorMessage = `${errorMsgPrefix} Could not parse NLS configuration. Could not find nlsConfigElement with id: ${nlsConfigElementId}`
|
||||
|
||||
expect(() => {
|
||||
getNlsConfiguration(_document, "")
|
||||
}).toThrowError(errorMessage)
|
||||
})
|
||||
it("should throw an error if no nlsConfig", () => {
|
||||
const mockElement = _document.createElement("div")
|
||||
mockElement.setAttribute("id", nlsConfigElementId)
|
||||
_document.body.appendChild(mockElement)
|
||||
|
||||
const errorMsgPrefix = "[vscode]"
|
||||
const errorMessage = `${errorMsgPrefix} Could not parse NLS configuration. Found nlsConfigElement but missing data-settings attribute.`
|
||||
|
||||
expect(() => {
|
||||
getNlsConfiguration(_document, "")
|
||||
}).toThrowError(errorMessage)
|
||||
|
||||
_document.body.removeChild(mockElement)
|
||||
})
|
||||
it("should return the correct configuration", () => {
|
||||
const mockElement = _document.createElement("div")
|
||||
const dataSettings = {
|
||||
first: "Jane",
|
||||
last: "Doe",
|
||||
}
|
||||
|
||||
mockElement.setAttribute("id", nlsConfigElementId)
|
||||
mockElement.setAttribute("data-settings", JSON.stringify(dataSettings))
|
||||
_document.body.appendChild(mockElement)
|
||||
const actual = getNlsConfiguration(_document, "")
|
||||
|
||||
expect(actual).toStrictEqual(dataSettings)
|
||||
|
||||
_document.body.removeChild(mockElement)
|
||||
})
|
||||
it("should return and have a loadBundle property if _resolvedLangaugePackCoreLocation", async () => {
|
||||
const mockElement = _document.createElement("div")
|
||||
const dataSettings = {
|
||||
locale: "en",
|
||||
availableLanguages: ["en", "de"],
|
||||
_resolvedLanguagePackCoreLocation: "./",
|
||||
}
|
||||
|
||||
mockElement.setAttribute("id", nlsConfigElementId)
|
||||
mockElement.setAttribute("data-settings", JSON.stringify(dataSettings))
|
||||
_document.body.appendChild(mockElement)
|
||||
const nlsConfig = getNlsConfiguration(_document, "")
|
||||
|
||||
expect(nlsConfig._resolvedLanguagePackCoreLocation).not.toBe(undefined)
|
||||
expect(nlsConfig.loadBundle).not.toBe(undefined)
|
||||
|
||||
const mockCallbackFn = jest.fn((_, bundle) => {
|
||||
return bundle
|
||||
})
|
||||
|
||||
fetchMock.mockOnce(JSON.stringify({ key: "hello world" }))
|
||||
// Ensure that load bundle works as expected
|
||||
// by mocking the fetch response and checking that the callback
|
||||
// had the expected value
|
||||
await nlsConfig.loadBundle("hello", "en", mockCallbackFn)
|
||||
expect(mockCallbackFn).toHaveBeenCalledTimes(1)
|
||||
expect(mockCallbackFn).toHaveBeenCalledWith(undefined, { key: "hello world" })
|
||||
|
||||
// Call it again to ensure it loads from the cache
|
||||
// it should return the same value
|
||||
await nlsConfig.loadBundle("hello", "en", mockCallbackFn)
|
||||
expect(mockCallbackFn).toHaveBeenCalledTimes(2)
|
||||
expect(mockCallbackFn).toHaveBeenCalledWith(undefined, { key: "hello world" })
|
||||
|
||||
fetchMock.mockReject(new Error("fake error message"))
|
||||
const mockCallbackFn2 = jest.fn((error) => error)
|
||||
// Call it for a different bundle and mock a failed fetch call
|
||||
// to ensure we get the expected error
|
||||
const error = await nlsConfig.loadBundle("goodbye", "es", mockCallbackFn2)
|
||||
expect(error.message).toEqual("fake error message")
|
||||
|
||||
// Clean up
|
||||
_document.body.removeChild(mockElement)
|
||||
})
|
||||
})
|
||||
describe("createBundlePath", () => {
|
||||
it("should return the correct path", () => {
|
||||
const _resolvedLangaugePackCoreLocation = "./languages"
|
||||
const bundle = "/bundle.js"
|
||||
const expected = "./languages/!bundle.js.nls.json"
|
||||
const actual = createBundlePath(_resolvedLangaugePackCoreLocation, bundle)
|
||||
expect(actual).toBe(expected)
|
||||
})
|
||||
it("should return the correct path (even if _resolvedLangaugePackCoreLocation is undefined)", () => {
|
||||
const _resolvedLangaugePackCoreLocation = undefined
|
||||
const bundle = "/bundle.js"
|
||||
const expected = "/!bundle.js.nls.json"
|
||||
const actual = createBundlePath(_resolvedLangaugePackCoreLocation, bundle)
|
||||
expect(actual).toBe(expected)
|
||||
})
|
||||
})
|
||||
describe("setBodyBackgroundToThemeBackgroundColor", () => {
|
||||
let _document: Document
|
||||
let _localStorage: Storage
|
||||
|
||||
beforeEach(() => {
|
||||
// We need to set the url in the JSDOM constructor
|
||||
// to prevent this error "SecurityError: localStorage is not available for opaque origins"
|
||||
// See: https://github.com/jsdom/jsdom/issues/2304#issuecomment-622314949
|
||||
const { window: _window } = new JSDOM("", { url: "http://localhost" })
|
||||
_document = _window.document
|
||||
_localStorage = _window.localStorage
|
||||
})
|
||||
it("should return null", () => {
|
||||
const test = {
|
||||
colorMap: {
|
||||
[`editor.background`]: "#ff3270",
|
||||
},
|
||||
}
|
||||
_localStorage.setItem("colorThemeData", JSON.stringify(test))
|
||||
|
||||
expect(setBodyBackgroundToThemeBackgroundColor(_document, _localStorage)).toBeNull()
|
||||
|
||||
_localStorage.removeItem("colorThemeData")
|
||||
})
|
||||
it("should throw an error if it can't find colorThemeData in localStorage", () => {
|
||||
const errorMsgPrefix = "[vscode]"
|
||||
const errorMessage = `${errorMsgPrefix} Could not set body background to theme background color. Could not find colorThemeData in localStorage.`
|
||||
|
||||
expect(() => {
|
||||
setBodyBackgroundToThemeBackgroundColor(_document, _localStorage)
|
||||
}).toThrowError(errorMessage)
|
||||
})
|
||||
it("should throw an error if there is an error parsing colorThemeData from localStorage", () => {
|
||||
const errorMsgPrefix = "[vscode]"
|
||||
const errorMessage = `${errorMsgPrefix} Could not set body background to theme background color. Could not parse colorThemeData from localStorage.`
|
||||
|
||||
_localStorage.setItem(
|
||||
"colorThemeData",
|
||||
'{"id":"vs-dark max-SS-Cyberpunk-themes-cyberpunk-umbra-color-theme-json","label":"Activate UMBRA protocol","settingsId":"Activate "errorForeground":"#ff3270","foreground":"#ffffff","sideBarTitle.foreground":"#bbbbbb"},"watch\\":::false}',
|
||||
)
|
||||
|
||||
expect(() => {
|
||||
setBodyBackgroundToThemeBackgroundColor(_document, _localStorage)
|
||||
}).toThrowError(errorMessage)
|
||||
|
||||
localStorage.removeItem("colorThemeData")
|
||||
})
|
||||
it("should throw an error if there is no colorMap property", () => {
|
||||
const errorMsgPrefix = "[vscode]"
|
||||
const errorMessage = `${errorMsgPrefix} Could not set body background to theme background color. colorThemeData is missing colorMap.`
|
||||
|
||||
const test = {
|
||||
id: "hey-joe",
|
||||
}
|
||||
_localStorage.setItem("colorThemeData", JSON.stringify(test))
|
||||
|
||||
expect(() => {
|
||||
setBodyBackgroundToThemeBackgroundColor(_document, _localStorage)
|
||||
}).toThrowError(errorMessage)
|
||||
|
||||
_localStorage.removeItem("colorThemeData")
|
||||
})
|
||||
it("should throw an error if there is no editor.background color", () => {
|
||||
const errorMsgPrefix = "[vscode]"
|
||||
const errorMessage = `${errorMsgPrefix} Could not set body background to theme background color. colorThemeData.colorMap["editor.background"] is undefined.`
|
||||
|
||||
const test = {
|
||||
id: "hey-joe",
|
||||
colorMap: {
|
||||
editor: "#fff",
|
||||
},
|
||||
}
|
||||
_localStorage.setItem("colorThemeData", JSON.stringify(test))
|
||||
|
||||
expect(() => {
|
||||
setBodyBackgroundToThemeBackgroundColor(_document, _localStorage)
|
||||
}).toThrowError(errorMessage)
|
||||
|
||||
_localStorage.removeItem("colorThemeData")
|
||||
})
|
||||
it("should set the body background to the editor background color", () => {
|
||||
const test = {
|
||||
colorMap: {
|
||||
[`editor.background`]: "#ff3270",
|
||||
},
|
||||
}
|
||||
_localStorage.setItem("colorThemeData", JSON.stringify(test))
|
||||
|
||||
setBodyBackgroundToThemeBackgroundColor(_document, _localStorage)
|
||||
|
||||
// When the body.style.backgroundColor is set using hex
|
||||
// it is converted to rgb
|
||||
// which is why we use that in the assertion
|
||||
expect(_document.body.style.backgroundColor).toBe("rgb(255, 50, 112)")
|
||||
|
||||
_localStorage.removeItem("colorThemeData")
|
||||
})
|
||||
})
|
||||
describe("getConfigurationForLoader", () => {
|
||||
let _window: Window
|
||||
|
||||
beforeEach(() => {
|
||||
const { window: __window } = new JSDOM()
|
||||
// @ts-expect-error the Window from JSDOM is not exactly the same as Window
|
||||
// so we expect an error here
|
||||
_window = __window
|
||||
})
|
||||
it("should return a loader object (with undefined trustedTypesPolicy)", () => {
|
||||
const options = {
|
||||
base: ".",
|
||||
csStaticBase: "/",
|
||||
logLevel: 1,
|
||||
}
|
||||
const nlsConfig = {
|
||||
first: "Jane",
|
||||
last: "Doe",
|
||||
locale: "en",
|
||||
availableLanguages: {},
|
||||
}
|
||||
const loader = getConfigurationForLoader({
|
||||
options,
|
||||
_window,
|
||||
nlsConfig: nlsConfig,
|
||||
})
|
||||
|
||||
expect(loader).toStrictEqual({
|
||||
baseUrl: "http://localhost//vendor/modules/code-oss-dev/out",
|
||||
paths: {
|
||||
"iconv-lite-umd": "../node_modules/iconv-lite-umd/lib/iconv-lite-umd.js",
|
||||
jschardet: "../node_modules/jschardet/dist/jschardet.min.js",
|
||||
"tas-client-umd": "../node_modules/tas-client-umd/lib/tas-client-umd.js",
|
||||
"vscode-oniguruma": "../node_modules/vscode-oniguruma/release/main",
|
||||
"vscode-textmate": "../node_modules/vscode-textmate/release/main",
|
||||
xterm: "../node_modules/xterm/lib/xterm.js",
|
||||
"xterm-addon-search": "../node_modules/xterm-addon-search/lib/xterm-addon-search.js",
|
||||
"xterm-addon-unicode11": "../node_modules/xterm-addon-unicode11/lib/xterm-addon-unicode11.js",
|
||||
"xterm-addon-webgl": "../node_modules/xterm-addon-webgl/lib/xterm-addon-webgl.js",
|
||||
},
|
||||
recordStats: true,
|
||||
|
||||
trustedTypesPolicy: undefined,
|
||||
"vs/nls": {
|
||||
availableLanguages: {},
|
||||
first: "Jane",
|
||||
last: "Doe",
|
||||
locale: "en",
|
||||
},
|
||||
})
|
||||
})
|
||||
it("should return a loader object with trustedTypesPolicy", () => {
|
||||
interface PolicyOptions {
|
||||
createScriptUrl: (url: string) => string
|
||||
}
|
||||
|
||||
function mockCreatePolicy(policyName: string, options: PolicyOptions) {
|
||||
return {
|
||||
name: policyName,
|
||||
...options,
|
||||
}
|
||||
}
|
||||
|
||||
const mockFn = jest.fn(mockCreatePolicy)
|
||||
|
||||
// @ts-expect-error we are adding a custom property to window
|
||||
_window.trustedTypes = {
|
||||
createPolicy: mockFn,
|
||||
}
|
||||
|
||||
const options = {
|
||||
base: "/",
|
||||
csStaticBase: "/",
|
||||
logLevel: 1,
|
||||
}
|
||||
const nlsConfig = {
|
||||
first: "Jane",
|
||||
last: "Doe",
|
||||
locale: "en",
|
||||
availableLanguages: {},
|
||||
}
|
||||
const loader = getConfigurationForLoader({
|
||||
options,
|
||||
_window,
|
||||
nlsConfig: nlsConfig,
|
||||
})
|
||||
|
||||
expect(loader.trustedTypesPolicy).not.toBe(undefined)
|
||||
expect(loader.trustedTypesPolicy.name).toBe("amdLoader")
|
||||
|
||||
// Check that we can actually create a script URL
|
||||
// using the createScriptURL on the loader object
|
||||
const scriptUrl = loader.trustedTypesPolicy.createScriptURL("http://localhost/foo.js")
|
||||
expect(scriptUrl).toBe("http://localhost/foo.js")
|
||||
})
|
||||
})
|
||||
describe("_createScriptURL", () => {
|
||||
it("should return the correct url", () => {
|
||||
const url = _createScriptURL("localhost/foo/bar.js", "localhost")
|
||||
|
||||
expect(url).toBe("localhost/foo/bar.js")
|
||||
})
|
||||
it("should throw if the value doesn't start with the origin", () => {
|
||||
expect(() => {
|
||||
_createScriptURL("localhost/foo/bar.js", "coder.com")
|
||||
}).toThrow("Invalid script url: localhost/foo/bar.js")
|
||||
})
|
||||
})
|
||||
describe("main", () => {
|
||||
let _window: Window
|
||||
let _document: Document
|
||||
let _localStorage: Storage
|
||||
|
||||
beforeEach(() => {
|
||||
// We need to set the url in the JSDOM constructor
|
||||
// to prevent this error "SecurityError: localStorage is not available for opaque origins"
|
||||
// See: https://github.com/jsdom/jsdom/issues/2304#issuecomment-62231494
|
||||
const { window: __window } = new JSDOM("", { url: "http://localhost" })
|
||||
// @ts-expect-error the Window from JSDOM is not exactly the same as Window
|
||||
// so we expect an error here
|
||||
_window = __window
|
||||
_document = __window.document
|
||||
_localStorage = __window.localStorage
|
||||
|
||||
const mockElement = _document.createElement("div")
|
||||
const dataSettings = {
|
||||
first: "Jane",
|
||||
last: "Doe",
|
||||
}
|
||||
|
||||
mockElement.setAttribute("id", nlsConfigElementId)
|
||||
mockElement.setAttribute("data-settings", JSON.stringify(dataSettings))
|
||||
_document.body.appendChild(mockElement)
|
||||
|
||||
const test = {
|
||||
colorMap: {
|
||||
[`editor.background`]: "#ff3270",
|
||||
},
|
||||
}
|
||||
_localStorage.setItem("colorThemeData", JSON.stringify(test))
|
||||
})
|
||||
afterEach(() => {
|
||||
_localStorage.removeItem("colorThemeData")
|
||||
})
|
||||
it("should throw if document is missing", () => {
|
||||
expect(() => {
|
||||
main(undefined, _window, _localStorage)
|
||||
}).toThrow("document is undefined.")
|
||||
})
|
||||
it("should throw if window is missing", () => {
|
||||
expect(() => {
|
||||
main(_document, undefined, _localStorage)
|
||||
}).toThrow("window is undefined.")
|
||||
})
|
||||
it("should throw if localStorage is missing", () => {
|
||||
expect(() => {
|
||||
main(_document, _window, undefined)
|
||||
}).toThrow("localStorage is undefined.")
|
||||
})
|
||||
it("should add loader to self.require", () => {
|
||||
main(_document, _window, _localStorage)
|
||||
|
||||
expect(Object.prototype.hasOwnProperty.call(self, "require")).toBe(true)
|
||||
})
|
||||
it("should not throw in browser context", () => {
|
||||
// Assuming we call it in a normal browser context
|
||||
// where everything is defined
|
||||
expect(() => {
|
||||
main(_document, _window, _localStorage)
|
||||
}).not.toThrow()
|
||||
})
|
||||
})
|
||||
})
|
||||
@@ -1,183 +0,0 @@
|
||||
import { JSDOM } from "jsdom"
|
||||
import { registerServiceWorker } from "../../../src/browser/register"
|
||||
import { createLoggerMock } from "../../utils/helpers"
|
||||
import { LocationLike } from "../common/util.test"
|
||||
|
||||
describe("register", () => {
|
||||
describe("when navigator and serviceWorker are defined", () => {
|
||||
const mockRegisterFn = jest.fn()
|
||||
|
||||
beforeAll(() => {
|
||||
const { window } = new JSDOM()
|
||||
global.window = window as unknown as Window & typeof globalThis
|
||||
global.document = window.document
|
||||
global.navigator = window.navigator
|
||||
global.location = window.location
|
||||
|
||||
Object.defineProperty(global.navigator, "serviceWorker", {
|
||||
value: {
|
||||
register: mockRegisterFn,
|
||||
},
|
||||
})
|
||||
})
|
||||
|
||||
const loggerModule = createLoggerMock()
|
||||
beforeEach(() => {
|
||||
jest.clearAllMocks()
|
||||
jest.mock("@coder/logger", () => loggerModule)
|
||||
})
|
||||
|
||||
afterEach(() => {
|
||||
jest.resetModules()
|
||||
})
|
||||
|
||||
afterAll(() => {
|
||||
jest.restoreAllMocks()
|
||||
|
||||
// We don't want these to stay around because it can affect other tests
|
||||
global.window = undefined as unknown as Window & typeof globalThis
|
||||
global.document = undefined as unknown as Document & typeof globalThis
|
||||
global.navigator = undefined as unknown as Navigator & typeof globalThis
|
||||
global.location = undefined as unknown as Location & typeof globalThis
|
||||
})
|
||||
|
||||
it("test should have access to browser globals from beforeAll", () => {
|
||||
expect(typeof global.window).not.toBeFalsy()
|
||||
expect(typeof global.document).not.toBeFalsy()
|
||||
expect(typeof global.navigator).not.toBeFalsy()
|
||||
expect(typeof global.location).not.toBeFalsy()
|
||||
})
|
||||
|
||||
it("should register a ServiceWorker", () => {
|
||||
// Load service worker like you would in the browser
|
||||
require("../../../src/browser/register")
|
||||
expect(mockRegisterFn).toHaveBeenCalled()
|
||||
expect(mockRegisterFn).toHaveBeenCalledTimes(1)
|
||||
})
|
||||
|
||||
it("should log an error if something doesn't work", () => {
|
||||
const message = "Can't find browser"
|
||||
const error = new Error(message)
|
||||
|
||||
mockRegisterFn.mockImplementation(() => {
|
||||
throw error
|
||||
})
|
||||
|
||||
// Load service worker like you would in the browser
|
||||
require("../../../src/browser/register")
|
||||
|
||||
expect(mockRegisterFn).toHaveBeenCalled()
|
||||
expect(loggerModule.logger.error).toHaveBeenCalled()
|
||||
expect(loggerModule.logger.error).toHaveBeenCalledTimes(1)
|
||||
expect(loggerModule.logger.error).toHaveBeenCalledWith(
|
||||
`[Service Worker] registration: ${error.message} ${error.stack}`,
|
||||
)
|
||||
})
|
||||
})
|
||||
|
||||
describe("when navigator and serviceWorker are NOT defined", () => {
|
||||
const loggerModule = createLoggerMock()
|
||||
beforeEach(() => {
|
||||
jest.clearAllMocks()
|
||||
jest.mock("@coder/logger", () => loggerModule)
|
||||
})
|
||||
|
||||
afterAll(() => {
|
||||
jest.restoreAllMocks()
|
||||
})
|
||||
|
||||
it("should log an error", () => {
|
||||
// Load service worker like you would in the browser
|
||||
require("../../../src/browser/register")
|
||||
expect(loggerModule.logger.error).toHaveBeenCalled()
|
||||
expect(loggerModule.logger.error).toHaveBeenCalledTimes(1)
|
||||
expect(loggerModule.logger.error).toHaveBeenCalledWith("[Service Worker] navigator is undefined")
|
||||
})
|
||||
})
|
||||
|
||||
describe("registerServiceWorker", () => {
|
||||
let serviceWorkerPath: string
|
||||
let serviceWorkerScope: string
|
||||
const mockFn = jest.fn((path: string, options: { scope: string }) => {
|
||||
serviceWorkerPath = path
|
||||
serviceWorkerScope = options.scope
|
||||
return undefined
|
||||
})
|
||||
|
||||
beforeAll(() => {
|
||||
const location: LocationLike = {
|
||||
pathname: "",
|
||||
origin: "http://localhost:8080",
|
||||
}
|
||||
const { window } = new JSDOM()
|
||||
global.window = window as unknown as Window & typeof globalThis
|
||||
global.document = window.document
|
||||
global.navigator = window.navigator
|
||||
global.location = location as Location
|
||||
|
||||
Object.defineProperty(global.navigator, "serviceWorker", {
|
||||
value: {
|
||||
register: mockFn,
|
||||
},
|
||||
})
|
||||
})
|
||||
|
||||
afterEach(() => {
|
||||
mockFn.mockClear()
|
||||
jest.resetModules()
|
||||
})
|
||||
|
||||
afterAll(() => {
|
||||
jest.restoreAllMocks()
|
||||
|
||||
// We don't want these to stay around because it can affect other tests
|
||||
global.window = undefined as unknown as Window & typeof globalThis
|
||||
global.document = undefined as unknown as Document & typeof globalThis
|
||||
global.navigator = undefined as unknown as Navigator & typeof globalThis
|
||||
global.location = undefined as unknown as Location & typeof globalThis
|
||||
})
|
||||
it("should register when options.base is undefined", async () => {
|
||||
// Mock getElementById
|
||||
const csStaticBasePath = "/static/development/Users/jp/Dev/code-server"
|
||||
const spy = jest.spyOn(document, "getElementById")
|
||||
// Create a fake element and set the attribute
|
||||
const mockElement = document.createElement("div")
|
||||
mockElement.id = "coder-options"
|
||||
mockElement.setAttribute(
|
||||
"data-settings",
|
||||
`{"csStaticBase":"${csStaticBasePath}","logLevel":2,"disableUpdateCheck":false}`,
|
||||
)
|
||||
// Return mockElement from the spy
|
||||
// this way, when we call "getElementById"
|
||||
// it returns the element
|
||||
spy.mockImplementation(() => mockElement)
|
||||
|
||||
await registerServiceWorker()
|
||||
|
||||
expect(mockFn).toBeCalled()
|
||||
expect(serviceWorkerPath).toMatch(`${csStaticBasePath}/out/browser/serviceWorker.js`)
|
||||
expect(serviceWorkerScope).toMatch("/")
|
||||
})
|
||||
it("should register when options.base is defined", async () => {
|
||||
const csStaticBasePath = "/static/development/Users/jp/Dev/code-server"
|
||||
const spy = jest.spyOn(document, "getElementById")
|
||||
// Create a fake element and set the attribute
|
||||
const mockElement = document.createElement("div")
|
||||
mockElement.id = "coder-options"
|
||||
mockElement.setAttribute(
|
||||
"data-settings",
|
||||
`{"base":"proxy/","csStaticBase":"${csStaticBasePath}","logLevel":2,"disableUpdateCheck":false}`,
|
||||
)
|
||||
// Return mockElement from the spy
|
||||
// this way, when we call "getElementById"
|
||||
// it returns the element
|
||||
spy.mockImplementation(() => mockElement)
|
||||
|
||||
await registerServiceWorker()
|
||||
|
||||
expect(mockFn).toBeCalled()
|
||||
expect(serviceWorkerPath).toMatch(`/out/browser/serviceWorker.js`)
|
||||
expect(serviceWorkerScope).toMatch("/")
|
||||
})
|
||||
})
|
||||
})
|
||||
@@ -1,92 +0,0 @@
|
||||
interface MockEvent {
|
||||
claim: jest.Mock<any, any>
|
||||
waitUntil?: jest.Mock<any, any>
|
||||
}
|
||||
|
||||
interface Listener {
|
||||
event: string
|
||||
cb: (event?: MockEvent) => void
|
||||
}
|
||||
|
||||
describe("serviceWorker", () => {
|
||||
let listeners: Listener[] = []
|
||||
let spy: jest.SpyInstance
|
||||
let claimSpy: jest.Mock<any, any>
|
||||
let waitUntilSpy: jest.Mock<any, any>
|
||||
|
||||
function emit(event: string) {
|
||||
listeners
|
||||
.filter((listener) => listener.event === event)
|
||||
.forEach((listener) => {
|
||||
switch (event) {
|
||||
case "activate":
|
||||
listener.cb({
|
||||
claim: jest.fn(),
|
||||
waitUntil: jest.fn(() => waitUntilSpy()),
|
||||
})
|
||||
break
|
||||
default:
|
||||
listener.cb()
|
||||
}
|
||||
})
|
||||
}
|
||||
|
||||
beforeEach(() => {
|
||||
claimSpy = jest.fn()
|
||||
spy = jest.spyOn(console, "log")
|
||||
waitUntilSpy = jest.fn()
|
||||
|
||||
Object.assign(global, {
|
||||
self: global,
|
||||
addEventListener: (event: string, cb: () => void) => {
|
||||
listeners.push({ event, cb })
|
||||
},
|
||||
clients: {
|
||||
claim: claimSpy.mockResolvedValue("claimed"),
|
||||
},
|
||||
})
|
||||
})
|
||||
|
||||
afterEach(() => {
|
||||
jest.restoreAllMocks()
|
||||
jest.resetModules()
|
||||
spy.mockClear()
|
||||
claimSpy.mockClear()
|
||||
|
||||
// Clear all the listeners
|
||||
listeners = []
|
||||
})
|
||||
|
||||
it("should add 3 listeners: install, activate and fetch", () => {
|
||||
require("../../../src/browser/serviceWorker.ts")
|
||||
const listenerEventNames = listeners.map((listener) => listener.event)
|
||||
|
||||
expect(listeners).toHaveLength(3)
|
||||
expect(listenerEventNames).toContain("install")
|
||||
expect(listenerEventNames).toContain("activate")
|
||||
expect(listenerEventNames).toContain("fetch")
|
||||
})
|
||||
|
||||
it("should call the proper callbacks for 'install'", async () => {
|
||||
require("../../../src/browser/serviceWorker.ts")
|
||||
emit("install")
|
||||
expect(spy).toHaveBeenCalledWith("[Service Worker] installed")
|
||||
expect(spy).toHaveBeenCalledTimes(1)
|
||||
})
|
||||
|
||||
it("should do nothing when 'fetch' is called", async () => {
|
||||
require("../../../src/browser/serviceWorker.ts")
|
||||
emit("fetch")
|
||||
expect(spy).not.toHaveBeenCalled()
|
||||
})
|
||||
|
||||
it("should call the proper callbacks for 'activate'", async () => {
|
||||
require("../../../src/browser/serviceWorker.ts")
|
||||
emit("activate")
|
||||
|
||||
// Activate serviceWorker
|
||||
expect(spy).toHaveBeenCalledWith("[Service Worker] activated")
|
||||
expect(waitUntilSpy).toHaveBeenCalled()
|
||||
expect(claimSpy).toHaveBeenCalled()
|
||||
})
|
||||
})
|
||||
@@ -19,7 +19,7 @@ describe("http", () => {
|
||||
const httpError = new HttpError(message, HttpCode.BadRequest)
|
||||
|
||||
expect(httpError.message).toBe(message)
|
||||
expect(httpError.status).toBe(400)
|
||||
expect(httpError.statusCode).toBe(400)
|
||||
expect(httpError.details).toBeUndefined()
|
||||
})
|
||||
it("should have details if provided", () => {
|
||||
|
||||
@@ -74,107 +74,6 @@ describe("util", () => {
|
||||
})
|
||||
})
|
||||
|
||||
describe("resolveBase", () => {
|
||||
beforeEach(() => {
|
||||
const location: LocationLike = {
|
||||
pathname: "/healthz",
|
||||
origin: "http://localhost:8080",
|
||||
}
|
||||
|
||||
// Because resolveBase is not a pure function
|
||||
// and relies on the global location to be set
|
||||
// we set it before all the tests
|
||||
// and tell TS that our location should be looked at
|
||||
// as Location (even though it's missing some properties)
|
||||
global.location = location as Location
|
||||
})
|
||||
|
||||
it("should resolve a base", () => {
|
||||
expect(util.resolveBase("localhost:8080")).toBe("/localhost:8080")
|
||||
})
|
||||
|
||||
it("should resolve a base with a forward slash at the beginning", () => {
|
||||
expect(util.resolveBase("/localhost:8080")).toBe("/localhost:8080")
|
||||
})
|
||||
|
||||
it("should resolve a base with query params", () => {
|
||||
expect(util.resolveBase("localhost:8080?folder=hello-world")).toBe("/localhost:8080")
|
||||
})
|
||||
|
||||
it("should resolve a base with a path", () => {
|
||||
expect(util.resolveBase("localhost:8080/hello/world")).toBe("/localhost:8080/hello/world")
|
||||
})
|
||||
|
||||
it("should resolve a base to an empty string when not provided", () => {
|
||||
expect(util.resolveBase()).toBe("")
|
||||
})
|
||||
})
|
||||
|
||||
describe("getOptions", () => {
|
||||
beforeEach(() => {
|
||||
const location: LocationLike = {
|
||||
pathname: "/healthz",
|
||||
origin: "http://localhost:8080",
|
||||
// search: "?environmentId=600e0187-0909d8a00cb0a394720d4dce",
|
||||
}
|
||||
|
||||
// Because resolveBase is not a pure function
|
||||
// and relies on the global location to be set
|
||||
// we set it before all the tests
|
||||
// and tell TS that our location should be looked at
|
||||
// as Location (even though it's missing some properties)
|
||||
global.location = location as Location
|
||||
})
|
||||
|
||||
afterEach(() => {
|
||||
jest.restoreAllMocks()
|
||||
})
|
||||
|
||||
it("should return options with base and cssStaticBase even if it doesn't exist", () => {
|
||||
expect(util.getOptions()).toStrictEqual({
|
||||
base: "",
|
||||
csStaticBase: "",
|
||||
})
|
||||
})
|
||||
|
||||
it("should return options when they do exist", () => {
|
||||
// Mock getElementById
|
||||
const spy = jest.spyOn(document, "getElementById")
|
||||
// Create a fake element and set the attribute
|
||||
const mockElement = document.createElement("div")
|
||||
mockElement.setAttribute(
|
||||
"data-settings",
|
||||
'{"base":".","csStaticBase":"./static/development/Users/jp/Dev/code-server","logLevel":2,"disableUpdateCheck":false}',
|
||||
)
|
||||
// Return mockElement from the spy
|
||||
// this way, when we call "getElementById"
|
||||
// it returns the element
|
||||
spy.mockImplementation(() => mockElement)
|
||||
|
||||
expect(util.getOptions()).toStrictEqual({
|
||||
base: "",
|
||||
csStaticBase: "/static/development/Users/jp/Dev/code-server",
|
||||
disableUpdateCheck: false,
|
||||
logLevel: 2,
|
||||
})
|
||||
})
|
||||
|
||||
it("should include queryOpts", () => {
|
||||
// Trying to understand how the implementation works
|
||||
// 1. It grabs the search params from location.search (i.e. ?)
|
||||
// 2. it then grabs the "options" param if it exists
|
||||
// 3. then it creates a new options object
|
||||
// spreads the original options
|
||||
// then parses the queryOpts
|
||||
location.search = '?options={"logLevel":2}'
|
||||
expect(util.getOptions()).toStrictEqual({
|
||||
base: "",
|
||||
csStaticBase: "",
|
||||
logLevel: 2,
|
||||
})
|
||||
})
|
||||
})
|
||||
|
||||
describe("arrayify", () => {
|
||||
it("should return value it's already an array", () => {
|
||||
expect(util.arrayify(["hello", "world"])).toStrictEqual(["hello", "world"])
|
||||
@@ -194,20 +93,6 @@ describe("util", () => {
|
||||
})
|
||||
})
|
||||
|
||||
describe("getFirstString", () => {
|
||||
it("should return the string if passed a string", () => {
|
||||
expect(util.getFirstString("Hello world!")).toBe("Hello world!")
|
||||
})
|
||||
|
||||
it("should get the first string from an array", () => {
|
||||
expect(util.getFirstString(["Hello", "World"])).toBe("Hello")
|
||||
})
|
||||
|
||||
it("should return undefined if the value isn't an array or a string", () => {
|
||||
expect(util.getFirstString({ name: "Coder" })).toBe(undefined)
|
||||
})
|
||||
})
|
||||
|
||||
describe("logError", () => {
|
||||
afterEach(() => {
|
||||
jest.clearAllMocks()
|
||||
|
||||
3
test/unit/node/__snapshots__/app.test.ts.snap
Normal file
3
test/unit/node/__snapshots__/app.test.ts.snap
Normal file
@@ -0,0 +1,3 @@
|
||||
// Jest Snapshot v1, https://goo.gl/fbAQLP
|
||||
|
||||
exports[`handleServerError should log an error if resolved is true 1`] = `"Cannot read property 'handle' of undefined"`;
|
||||
@@ -1,6 +1,134 @@
|
||||
import { logger } from "@coder/logger"
|
||||
import { promises, rmdirSync } from "fs"
|
||||
import * as http from "http"
|
||||
import { ensureAddress } from "../../../src/node/app"
|
||||
import { getAvailablePort } from "../../utils/helpers"
|
||||
import * as https from "https"
|
||||
import * as path from "path"
|
||||
import { createApp, ensureAddress, handleArgsSocketCatchError, handleServerError } from "../../../src/node/app"
|
||||
import { OptionalString, setDefaults } from "../../../src/node/cli"
|
||||
import { generateCertificate } from "../../../src/node/util"
|
||||
import { getAvailablePort, tmpdir } from "../../utils/helpers"
|
||||
|
||||
describe("createApp", () => {
|
||||
let spy: jest.SpyInstance
|
||||
let unlinkSpy: jest.SpyInstance
|
||||
let port: number
|
||||
let tmpDirPath: string
|
||||
let tmpFilePath: string
|
||||
|
||||
beforeAll(async () => {
|
||||
tmpDirPath = await tmpdir("unlink-socket")
|
||||
tmpFilePath = path.join(tmpDirPath, "unlink-socket-file")
|
||||
})
|
||||
|
||||
beforeEach(async () => {
|
||||
spy = jest.spyOn(logger, "error")
|
||||
// NOTE:@jsjoeio
|
||||
// Be mindful when spying.
|
||||
// You can't spy on fs functions if you do import * as fs
|
||||
// You have to import individually, like we do here with promises
|
||||
// then you can spy on those modules methods, like unlink.
|
||||
// See: https://github.com/aelbore/esbuild-jest/issues/26#issuecomment-893763840
|
||||
unlinkSpy = jest.spyOn(promises, "unlink")
|
||||
port = await getAvailablePort()
|
||||
})
|
||||
|
||||
afterEach(() => {
|
||||
jest.clearAllMocks()
|
||||
})
|
||||
|
||||
afterAll(() => {
|
||||
jest.restoreAllMocks()
|
||||
// Ensure directory was removed
|
||||
rmdirSync(tmpDirPath, { recursive: true })
|
||||
})
|
||||
|
||||
it("should return an Express app, a WebSockets Express app and an http server", async () => {
|
||||
const defaultArgs = await setDefaults({
|
||||
port,
|
||||
})
|
||||
const app = await createApp(defaultArgs)
|
||||
|
||||
// This doesn't check much, but it's a good sanity check
|
||||
// to ensure we actually get back values from createApp
|
||||
expect(app.router).not.toBeNull()
|
||||
expect(app.wsRouter).not.toBeNull()
|
||||
expect(app.server).toBeInstanceOf(http.Server)
|
||||
|
||||
// Cleanup
|
||||
app.dispose()
|
||||
})
|
||||
|
||||
it("should handle error events on the server", async () => {
|
||||
const defaultArgs = await setDefaults({
|
||||
port,
|
||||
})
|
||||
|
||||
const app = await createApp(defaultArgs)
|
||||
|
||||
const testError = new Error("Test error")
|
||||
// We can easily test how the server handles errors
|
||||
// By emitting an error event
|
||||
// Ref: https://stackoverflow.com/a/33872506/3015595
|
||||
app.server.emit("error", testError)
|
||||
expect(spy).toHaveBeenCalledTimes(1)
|
||||
expect(spy).toHaveBeenCalledWith(`http server error: ${testError.message} ${testError.stack}`)
|
||||
|
||||
// Cleanup
|
||||
app.dispose()
|
||||
})
|
||||
|
||||
it("should reject errors that happen before the server can listen", async () => {
|
||||
// We listen on an invalid port
|
||||
// causing the app to reject the Promise called at startup
|
||||
const port = 2
|
||||
const defaultArgs = await setDefaults({
|
||||
port,
|
||||
})
|
||||
|
||||
async function masterBall() {
|
||||
const app = await createApp(defaultArgs)
|
||||
|
||||
const testError = new Error("Test error")
|
||||
|
||||
app.server.emit("error", testError)
|
||||
|
||||
// Cleanup
|
||||
app.dispose()
|
||||
}
|
||||
|
||||
expect(() => masterBall()).rejects.toThrow(`listen EACCES: permission denied 127.0.0.1:${port}`)
|
||||
})
|
||||
|
||||
it("should unlink a socket before listening on the socket", async () => {
|
||||
await promises.writeFile(tmpFilePath, "")
|
||||
const defaultArgs = await setDefaults({
|
||||
socket: tmpFilePath,
|
||||
})
|
||||
|
||||
const app = await createApp(defaultArgs)
|
||||
|
||||
expect(unlinkSpy).toHaveBeenCalledTimes(1)
|
||||
app.dispose()
|
||||
})
|
||||
|
||||
it("should create an https server if args.cert exists", async () => {
|
||||
const testCertificate = await generateCertificate("localhost")
|
||||
const cert = new OptionalString(testCertificate.cert)
|
||||
const defaultArgs = await setDefaults({
|
||||
port,
|
||||
cert,
|
||||
["cert-key"]: testCertificate.certKey,
|
||||
})
|
||||
const app = await createApp(defaultArgs)
|
||||
|
||||
// This doesn't check much, but it's a good sanity check
|
||||
// to ensure we actually get an https.Server
|
||||
expect(app.server).toBeInstanceOf(https.Server)
|
||||
|
||||
// Cleanup
|
||||
app.dispose()
|
||||
})
|
||||
})
|
||||
|
||||
describe("ensureAddress", () => {
|
||||
let mockServer: http.Server
|
||||
@@ -14,17 +142,115 @@ describe("ensureAddress", () => {
|
||||
})
|
||||
|
||||
it("should throw and error if no address", () => {
|
||||
expect(() => ensureAddress(mockServer)).toThrow("server has no address")
|
||||
})
|
||||
it("should return the address if it exists and not a string", async () => {
|
||||
const port = await getAvailablePort()
|
||||
mockServer.listen(port)
|
||||
const address = ensureAddress(mockServer)
|
||||
expect(address).toBe(`http://:::${port}`)
|
||||
expect(() => ensureAddress(mockServer, "http")).toThrow("Server has no address")
|
||||
})
|
||||
it("should return the address if it exists", async () => {
|
||||
mockServer.address = () => "http://localhost:8080"
|
||||
const address = ensureAddress(mockServer)
|
||||
expect(address).toBe(`http://localhost:8080`)
|
||||
mockServer.address = () => "http://localhost:8080/"
|
||||
const address = ensureAddress(mockServer, "http")
|
||||
expect(address.toString()).toBe(`http://localhost:8080/`)
|
||||
})
|
||||
})
|
||||
|
||||
describe("handleServerError", () => {
|
||||
let spy: jest.SpyInstance
|
||||
|
||||
beforeEach(() => {
|
||||
spy = jest.spyOn(logger, "error")
|
||||
})
|
||||
|
||||
afterEach(() => {
|
||||
jest.clearAllMocks()
|
||||
})
|
||||
|
||||
afterAll(() => {
|
||||
jest.restoreAllMocks()
|
||||
})
|
||||
|
||||
it("should call reject if resolved is false", async () => {
|
||||
const resolved = false
|
||||
const reject = jest.fn((err: Error) => undefined)
|
||||
const error = new Error("handleServerError Error")
|
||||
|
||||
handleServerError(resolved, error, reject)
|
||||
|
||||
expect(reject).toHaveBeenCalledTimes(1)
|
||||
expect(reject).toHaveBeenCalledWith(error)
|
||||
})
|
||||
|
||||
it("should log an error if resolved is true", async () => {
|
||||
const resolved = true
|
||||
const reject = jest.fn((err: Error) => undefined)
|
||||
const error = new Error("handleServerError Error")
|
||||
|
||||
handleServerError(resolved, error, reject)
|
||||
|
||||
expect(spy).toHaveBeenCalledTimes(1)
|
||||
expect(spy).toThrowErrorMatchingSnapshot()
|
||||
})
|
||||
})
|
||||
|
||||
describe("handleArgsSocketCatchError", () => {
|
||||
let spy: jest.SpyInstance
|
||||
|
||||
beforeEach(() => {
|
||||
spy = jest.spyOn(logger, "error")
|
||||
})
|
||||
|
||||
afterEach(() => {
|
||||
jest.clearAllMocks()
|
||||
})
|
||||
|
||||
afterAll(() => {
|
||||
jest.restoreAllMocks()
|
||||
})
|
||||
|
||||
it("should log an error if its not an NodeJS.ErrnoException", () => {
|
||||
const error = new Error()
|
||||
|
||||
handleArgsSocketCatchError(error)
|
||||
|
||||
expect(spy).toHaveBeenCalledTimes(1)
|
||||
expect(spy).toHaveBeenCalledWith(error)
|
||||
})
|
||||
|
||||
it("should log an error if its not an NodeJS.ErrnoException (and the error has a message)", () => {
|
||||
const errorMessage = "handleArgsSocketCatchError Error"
|
||||
const error = new Error(errorMessage)
|
||||
|
||||
handleArgsSocketCatchError(error)
|
||||
|
||||
expect(spy).toHaveBeenCalledTimes(1)
|
||||
expect(spy).toHaveBeenCalledWith(errorMessage)
|
||||
})
|
||||
|
||||
it("should not log an error if its a iNodeJS.ErrnoException", () => {
|
||||
const error: NodeJS.ErrnoException = new Error()
|
||||
error.code = "ENOENT"
|
||||
|
||||
handleArgsSocketCatchError(error)
|
||||
|
||||
expect(spy).toHaveBeenCalledTimes(0)
|
||||
})
|
||||
|
||||
it("should log an error if the code is not ENOENT (and the error has a message)", () => {
|
||||
const errorMessage = "no access"
|
||||
const error: NodeJS.ErrnoException = new Error()
|
||||
error.code = "EACCESS"
|
||||
error.message = errorMessage
|
||||
|
||||
handleArgsSocketCatchError(error)
|
||||
|
||||
expect(spy).toHaveBeenCalledTimes(1)
|
||||
expect(spy).toHaveBeenCalledWith(errorMessage)
|
||||
})
|
||||
|
||||
it("should log an error if the code is not ENOENT", () => {
|
||||
const error: NodeJS.ErrnoException = new Error()
|
||||
error.code = "EACCESS"
|
||||
|
||||
handleArgsSocketCatchError(error)
|
||||
|
||||
expect(spy).toHaveBeenCalledTimes(1)
|
||||
expect(spy).toHaveBeenCalledWith(error)
|
||||
})
|
||||
})
|
||||
|
||||
@@ -3,12 +3,34 @@ import { promises as fs } from "fs"
|
||||
import * as net from "net"
|
||||
import * as os from "os"
|
||||
import * as path from "path"
|
||||
import { Args, parse, setDefaults, shouldOpenInExistingInstance, splitOnFirstEquals } from "../../../src/node/cli"
|
||||
import { tmpdir } from "../../../src/node/constants"
|
||||
import { paths } from "../../../src/node/util"
|
||||
import {
|
||||
UserProvidedArgs,
|
||||
bindAddrFromArgs,
|
||||
defaultConfigFile,
|
||||
parse,
|
||||
readSocketPath,
|
||||
setDefaults,
|
||||
shouldOpenInExistingInstance,
|
||||
splitOnFirstEquals,
|
||||
toVsCodeArgs,
|
||||
} from "../../../src/node/cli"
|
||||
import { shouldSpawnCliProcess } from "../../../src/node/main"
|
||||
import { generatePassword, paths } from "../../../src/node/util"
|
||||
import { clean, useEnv, tmpdir } from "../../utils/helpers"
|
||||
|
||||
type Mutable<T> = {
|
||||
-readonly [P in keyof T]: T[P]
|
||||
// The parser should not set any defaults so the caller can determine what
|
||||
// values the user actually set. These are only set after explicitly calling
|
||||
// `setDefaults`.
|
||||
const defaults = {
|
||||
auth: "password",
|
||||
host: "localhost",
|
||||
port: 8080,
|
||||
"proxy-domain": [],
|
||||
usingEnvPassword: false,
|
||||
usingEnvHashedPassword: false,
|
||||
"extensions-dir": path.join(paths.data, "extensions"),
|
||||
"user-data-dir": paths.data,
|
||||
_: [],
|
||||
}
|
||||
|
||||
describe("parser", () => {
|
||||
@@ -18,78 +40,66 @@ describe("parser", () => {
|
||||
console.log = jest.fn()
|
||||
})
|
||||
|
||||
// The parser should not set any defaults so the caller can determine what
|
||||
// values the user actually set. These are only set after explicitly calling
|
||||
// `setDefaults`.
|
||||
const defaults = {
|
||||
auth: "password",
|
||||
host: "localhost",
|
||||
port: 8080,
|
||||
"proxy-domain": [],
|
||||
usingEnvPassword: false,
|
||||
usingEnvHashedPassword: false,
|
||||
"extensions-dir": path.join(paths.data, "extensions"),
|
||||
"user-data-dir": paths.data,
|
||||
}
|
||||
|
||||
it("should parse nothing", () => {
|
||||
expect(parse([])).toStrictEqual({ _: [] })
|
||||
it("should parse nothing", async () => {
|
||||
expect(parse([])).toStrictEqual({})
|
||||
})
|
||||
|
||||
it("should parse all available options", () => {
|
||||
it("should parse all available options", async () => {
|
||||
expect(
|
||||
parse([
|
||||
"--enable",
|
||||
"feature1",
|
||||
"--enable",
|
||||
"feature2",
|
||||
"--bind-addr=192.169.0.1:8080",
|
||||
"--auth",
|
||||
"none",
|
||||
"--extensions-dir",
|
||||
"foo",
|
||||
"--builtin-extensions-dir",
|
||||
"foobar",
|
||||
"--extra-extensions-dir",
|
||||
"nozzle",
|
||||
"1",
|
||||
"--extra-builtin-extensions-dir",
|
||||
"bazzle",
|
||||
"--verbose",
|
||||
"2",
|
||||
"--log",
|
||||
"error",
|
||||
"--help",
|
||||
"--open",
|
||||
"--socket=mumble",
|
||||
"3",
|
||||
"--user-data-dir",
|
||||
"bar",
|
||||
"--cert=baz",
|
||||
"--cert-key",
|
||||
"qux",
|
||||
"--version",
|
||||
"--json",
|
||||
"--port=8081",
|
||||
"--host",
|
||||
"0.0.0.0",
|
||||
"4",
|
||||
"--",
|
||||
"-5",
|
||||
"--6",
|
||||
]),
|
||||
parse(
|
||||
[
|
||||
["--enable", "feature1"],
|
||||
["--enable", "feature2"],
|
||||
|
||||
"--bind-addr=192.169.0.1:8080",
|
||||
|
||||
["--auth", "none"],
|
||||
|
||||
["--extensions-dir", "path/to/ext/dir"],
|
||||
|
||||
["--builtin-extensions-dir", "path/to/builtin/ext/dir"],
|
||||
|
||||
"1",
|
||||
"--verbose",
|
||||
"2",
|
||||
|
||||
["--log", "error"],
|
||||
|
||||
"--help",
|
||||
|
||||
"--open",
|
||||
|
||||
"--socket=mumble",
|
||||
|
||||
"3",
|
||||
|
||||
["--user-data-dir", "path/to/user/dir"],
|
||||
|
||||
["--cert=path/to/cert", "--cert-key", "path/to/cert/key"],
|
||||
|
||||
"--version",
|
||||
|
||||
"--json",
|
||||
|
||||
"--port=8081",
|
||||
|
||||
["--host", "0.0.0.0"],
|
||||
"4",
|
||||
"--",
|
||||
"--5",
|
||||
].flat(),
|
||||
),
|
||||
).toEqual({
|
||||
_: ["1", "2", "3", "4", "-5", "--6"],
|
||||
_: ["1", "2", "3", "4", "--5"],
|
||||
auth: "none",
|
||||
"builtin-extensions-dir": path.resolve("foobar"),
|
||||
"cert-key": path.resolve("qux"),
|
||||
"builtin-extensions-dir": path.resolve("path/to/builtin/ext/dir"),
|
||||
"extensions-dir": path.resolve("path/to/ext/dir"),
|
||||
"user-data-dir": path.resolve("path/to/user/dir"),
|
||||
"cert-key": path.resolve("path/to/cert/key"),
|
||||
cert: {
|
||||
value: path.resolve("baz"),
|
||||
value: path.resolve("path/to/cert"),
|
||||
},
|
||||
enable: ["feature1", "feature2"],
|
||||
"extensions-dir": path.resolve("foo"),
|
||||
"extra-builtin-extensions-dir": [path.resolve("bazzle")],
|
||||
"extra-extensions-dir": [path.resolve("nozzle")],
|
||||
help: true,
|
||||
host: "0.0.0.0",
|
||||
json: true,
|
||||
@@ -97,16 +107,14 @@ describe("parser", () => {
|
||||
open: true,
|
||||
port: 8081,
|
||||
socket: path.resolve("mumble"),
|
||||
"user-data-dir": path.resolve("bar"),
|
||||
verbose: true,
|
||||
version: true,
|
||||
"bind-addr": "192.169.0.1:8080",
|
||||
})
|
||||
})
|
||||
|
||||
it("should work with short options", () => {
|
||||
it("should work with short options", async () => {
|
||||
expect(parse(["-vvv", "-v"])).toEqual({
|
||||
_: [],
|
||||
verbose: true,
|
||||
version: true,
|
||||
})
|
||||
@@ -114,13 +122,12 @@ describe("parser", () => {
|
||||
|
||||
it("should use log level env var", async () => {
|
||||
const args = parse([])
|
||||
expect(args).toEqual({ _: [] })
|
||||
expect(args).toEqual({})
|
||||
|
||||
process.env.LOG_LEVEL = "debug"
|
||||
const defaults = await setDefaults(args)
|
||||
expect(defaults).toStrictEqual({
|
||||
...defaults,
|
||||
_: [],
|
||||
log: "debug",
|
||||
verbose: false,
|
||||
})
|
||||
@@ -131,7 +138,6 @@ describe("parser", () => {
|
||||
const updated = await setDefaults(args)
|
||||
expect(updated).toStrictEqual({
|
||||
...updated,
|
||||
_: [],
|
||||
log: "trace",
|
||||
verbose: true,
|
||||
})
|
||||
@@ -142,7 +148,6 @@ describe("parser", () => {
|
||||
it("should prefer --log to env var and --verbose to --log", async () => {
|
||||
let args = parse(["--log", "info"])
|
||||
expect(args).toEqual({
|
||||
_: [],
|
||||
log: "info",
|
||||
})
|
||||
|
||||
@@ -150,7 +155,6 @@ describe("parser", () => {
|
||||
const defaults = await setDefaults(args)
|
||||
expect(defaults).toEqual({
|
||||
...defaults,
|
||||
_: [],
|
||||
log: "info",
|
||||
verbose: false,
|
||||
})
|
||||
@@ -161,7 +165,6 @@ describe("parser", () => {
|
||||
const updated = await setDefaults(args)
|
||||
expect(updated).toEqual({
|
||||
...defaults,
|
||||
_: [],
|
||||
log: "info",
|
||||
verbose: false,
|
||||
})
|
||||
@@ -170,7 +173,6 @@ describe("parser", () => {
|
||||
|
||||
args = parse(["--log", "info", "--verbose"])
|
||||
expect(args).toEqual({
|
||||
_: [],
|
||||
log: "info",
|
||||
verbose: true,
|
||||
})
|
||||
@@ -179,7 +181,6 @@ describe("parser", () => {
|
||||
const updatedAgain = await setDefaults(args)
|
||||
expect(updatedAgain).toEqual({
|
||||
...defaults,
|
||||
_: [],
|
||||
log: "trace",
|
||||
verbose: true,
|
||||
})
|
||||
@@ -192,7 +193,6 @@ describe("parser", () => {
|
||||
const defaults = await setDefaults(parse([]))
|
||||
expect(defaults).toEqual({
|
||||
...defaults,
|
||||
_: [],
|
||||
})
|
||||
})
|
||||
|
||||
@@ -214,9 +214,8 @@ describe("parser", () => {
|
||||
expect(() => parse(["--foo"])).toThrowError(/Unknown option --foo/)
|
||||
})
|
||||
|
||||
it("should not error if the value is optional", () => {
|
||||
it("should not error if the value is optional", async () => {
|
||||
expect(parse(["--cert"])).toEqual({
|
||||
_: [],
|
||||
cert: {
|
||||
value: undefined,
|
||||
},
|
||||
@@ -227,26 +226,23 @@ describe("parser", () => {
|
||||
expect(() => parse(["--socket", "--socket-path-value"])).toThrowError(/--socket requires a value/)
|
||||
// If you actually had a path like this you would do this instead:
|
||||
expect(parse(["--socket", "./--socket-path-value"])).toEqual({
|
||||
_: [],
|
||||
socket: path.resolve("--socket-path-value"),
|
||||
})
|
||||
expect(() => parse(["--cert", "--socket-path-value"])).toThrowError(/Unknown option --socket-path-value/)
|
||||
})
|
||||
|
||||
it("should allow positional arguments before options", () => {
|
||||
expect(parse(["foo", "test", "--auth", "none"])).toEqual({
|
||||
_: ["foo", "test"],
|
||||
it("should allow positional arguments before options", async () => {
|
||||
expect(parse(["test", "--auth", "none"])).toEqual({
|
||||
_: ["test"],
|
||||
auth: "none",
|
||||
})
|
||||
})
|
||||
|
||||
it("should support repeatable flags", () => {
|
||||
it("should support repeatable flags", async () => {
|
||||
expect(parse(["--proxy-domain", "*.coder.com"])).toEqual({
|
||||
_: [],
|
||||
"proxy-domain": ["*.coder.com"],
|
||||
})
|
||||
expect(parse(["--proxy-domain", "*.coder.com", "--proxy-domain", "test.com"])).toEqual({
|
||||
_: [],
|
||||
"proxy-domain": ["*.coder.com", "test.com"],
|
||||
})
|
||||
})
|
||||
@@ -254,7 +250,6 @@ describe("parser", () => {
|
||||
it("should enforce cert-key with cert value or otherwise generate one", async () => {
|
||||
const args = parse(["--cert"])
|
||||
expect(args).toEqual({
|
||||
_: [],
|
||||
cert: {
|
||||
value: undefined,
|
||||
},
|
||||
@@ -262,7 +257,6 @@ describe("parser", () => {
|
||||
expect(() => parse(["--cert", "test"])).toThrowError(/--cert-key is missing/)
|
||||
const defaultArgs = await setDefaults(args)
|
||||
expect(defaultArgs).toEqual({
|
||||
_: [],
|
||||
...defaults,
|
||||
cert: {
|
||||
value: path.join(paths.data, "localhost.crt"),
|
||||
@@ -275,7 +269,6 @@ describe("parser", () => {
|
||||
const args = parse("--cert test --cert-key test --socket test --host 0.0.0.0 --port 8888 --link test".split(" "))
|
||||
const defaultArgs = await setDefaults(args)
|
||||
expect(defaultArgs).toEqual({
|
||||
_: [],
|
||||
...defaults,
|
||||
auth: "none",
|
||||
host: "localhost",
|
||||
@@ -292,14 +285,11 @@ describe("parser", () => {
|
||||
it("should use env var password", async () => {
|
||||
process.env.PASSWORD = "test"
|
||||
const args = parse([])
|
||||
expect(args).toEqual({
|
||||
_: [],
|
||||
})
|
||||
expect(args).toEqual({})
|
||||
|
||||
const defaultArgs = await setDefaults(args)
|
||||
expect(defaultArgs).toEqual({
|
||||
...defaults,
|
||||
_: [],
|
||||
password: "test",
|
||||
usingEnvPassword: true,
|
||||
})
|
||||
@@ -309,44 +299,49 @@ describe("parser", () => {
|
||||
process.env.HASHED_PASSWORD =
|
||||
"$argon2i$v=19$m=4096,t=3,p=1$0qR/o+0t00hsbJFQCKSfdQ$oFcM4rL6o+B7oxpuA4qlXubypbBPsf+8L531U7P9HYY" // test
|
||||
const args = parse([])
|
||||
expect(args).toEqual({
|
||||
_: [],
|
||||
})
|
||||
expect(args).toEqual({})
|
||||
|
||||
const defaultArgs = await setDefaults(args)
|
||||
expect(defaultArgs).toEqual({
|
||||
...defaults,
|
||||
_: [],
|
||||
"hashed-password":
|
||||
"$argon2i$v=19$m=4096,t=3,p=1$0qR/o+0t00hsbJFQCKSfdQ$oFcM4rL6o+B7oxpuA4qlXubypbBPsf+8L531U7P9HYY",
|
||||
usingEnvHashedPassword: true,
|
||||
})
|
||||
})
|
||||
|
||||
it("should error if password passed in", () => {
|
||||
expect(() => parse(["--password", "supersecret123"])).toThrowError(
|
||||
"--password can only be set in the config file or passed in via $PASSWORD",
|
||||
)
|
||||
})
|
||||
|
||||
it("should error if hashed-password passed in", () => {
|
||||
expect(() => parse(["--hashed-password", "fdas423fs8a"])).toThrowError(
|
||||
"--hashed-password can only be set in the config file or passed in via $HASHED_PASSWORD",
|
||||
)
|
||||
})
|
||||
|
||||
it("should filter proxy domains", async () => {
|
||||
const args = parse(["--proxy-domain", "*.coder.com", "--proxy-domain", "coder.com", "--proxy-domain", "coder.org"])
|
||||
expect(args).toEqual({
|
||||
_: [],
|
||||
"proxy-domain": ["*.coder.com", "coder.com", "coder.org"],
|
||||
})
|
||||
|
||||
const defaultArgs = await setDefaults(args)
|
||||
expect(defaultArgs).toEqual({
|
||||
...defaults,
|
||||
_: [],
|
||||
"proxy-domain": ["coder.com", "coder.org"],
|
||||
})
|
||||
})
|
||||
it("should allow '=,$/' in strings", async () => {
|
||||
const args = parse([
|
||||
"--enable-proposed-api",
|
||||
"--disable-update-check",
|
||||
"$argon2i$v=19$m=4096,t=3,p=1$0qr/o+0t00hsbjfqcksfdq$ofcm4rl6o+b7oxpua4qlxubypbbpsf+8l531u7p9hyy",
|
||||
])
|
||||
expect(args).toEqual({
|
||||
_: [],
|
||||
"enable-proposed-api": [
|
||||
"$argon2i$v=19$m=4096,t=3,p=1$0qr/o+0t00hsbjfqcksfdq$ofcm4rl6o+b7oxpua4qlxubypbbpsf+8l531u7p9hyy",
|
||||
],
|
||||
"disable-update-check": true,
|
||||
_: ["$argon2i$v=19$m=4096,t=3,p=1$0qr/o+0t00hsbjfqcksfdq$ofcm4rl6o+b7oxpua4qlxubypbbpsf+8l531u7p9hyy"],
|
||||
})
|
||||
})
|
||||
it("should parse options with double-dash and multiple equal signs ", async () => {
|
||||
@@ -359,7 +354,6 @@ describe("parser", () => {
|
||||
},
|
||||
)
|
||||
expect(args).toEqual({
|
||||
_: [],
|
||||
"hashed-password":
|
||||
"$argon2i$v=19$m=4096,t=3,p=1$0qr/o+0t00hsbjfqcksfdq$ofcm4rl6o+b7oxpua4qlxubypbbpsf+8l531u7p9hyy",
|
||||
})
|
||||
@@ -367,31 +361,32 @@ describe("parser", () => {
|
||||
})
|
||||
|
||||
describe("cli", () => {
|
||||
let args: Mutable<Args> = { _: [] }
|
||||
const testDir = path.join(tmpdir, "tests/cli")
|
||||
let testDir: string
|
||||
const vscodeIpcPath = path.join(os.tmpdir(), "vscode-ipc")
|
||||
|
||||
beforeAll(async () => {
|
||||
testDir = await tmpdir("cli")
|
||||
await fs.rmdir(testDir, { recursive: true })
|
||||
await fs.mkdir(testDir, { recursive: true })
|
||||
})
|
||||
|
||||
beforeEach(async () => {
|
||||
delete process.env.VSCODE_IPC_HOOK_CLI
|
||||
args = { _: [] }
|
||||
await fs.rmdir(vscodeIpcPath, { recursive: true })
|
||||
})
|
||||
|
||||
it("should use existing if inside code-server", async () => {
|
||||
process.env.VSCODE_IPC_HOOK_CLI = "test"
|
||||
const args: UserProvidedArgs = {}
|
||||
expect(await shouldOpenInExistingInstance(args)).toStrictEqual("test")
|
||||
|
||||
args.port = 8081
|
||||
args._.push("./file")
|
||||
args._ = ["./file"]
|
||||
expect(await shouldOpenInExistingInstance(args)).toStrictEqual("test")
|
||||
})
|
||||
|
||||
it("should use existing if --reuse-window is set", async () => {
|
||||
const args: UserProvidedArgs = {}
|
||||
args["reuse-window"] = true
|
||||
await expect(shouldOpenInExistingInstance(args)).resolves.toStrictEqual(undefined)
|
||||
|
||||
@@ -403,6 +398,7 @@ describe("cli", () => {
|
||||
})
|
||||
|
||||
it("should use existing if --new-window is set", async () => {
|
||||
const args: UserProvidedArgs = {}
|
||||
args["new-window"] = true
|
||||
expect(await shouldOpenInExistingInstance(args)).toStrictEqual(undefined)
|
||||
|
||||
@@ -414,9 +410,10 @@ describe("cli", () => {
|
||||
})
|
||||
|
||||
it("should use existing if no unrelated flags are set, has positional, and socket is active", async () => {
|
||||
const args: UserProvidedArgs = {}
|
||||
expect(await shouldOpenInExistingInstance(args)).toStrictEqual(undefined)
|
||||
|
||||
args._.push("./file")
|
||||
args._ = ["./file"]
|
||||
expect(await shouldOpenInExistingInstance(args)).toStrictEqual(undefined)
|
||||
|
||||
const socketPath = path.join(testDir, "socket")
|
||||
@@ -463,3 +460,265 @@ describe("splitOnFirstEquals", () => {
|
||||
expect(actual).toEqual(expect.arrayContaining(expected))
|
||||
})
|
||||
})
|
||||
|
||||
describe("shouldSpawnCliProcess", () => {
|
||||
it("should return false if no 'extension' related args passed in", async () => {
|
||||
const args = {}
|
||||
const actual = await shouldSpawnCliProcess(args)
|
||||
const expected = false
|
||||
|
||||
expect(actual).toBe(expected)
|
||||
})
|
||||
|
||||
it("should return true if 'list-extensions' passed in", async () => {
|
||||
const args = {
|
||||
["list-extensions"]: true,
|
||||
}
|
||||
const actual = await shouldSpawnCliProcess(args)
|
||||
const expected = true
|
||||
|
||||
expect(actual).toBe(expected)
|
||||
})
|
||||
|
||||
it("should return true if 'install-extension' passed in", async () => {
|
||||
const args = {
|
||||
["install-extension"]: ["hello.world"],
|
||||
}
|
||||
const actual = await shouldSpawnCliProcess(args)
|
||||
const expected = true
|
||||
|
||||
expect(actual).toBe(expected)
|
||||
})
|
||||
|
||||
it("should return true if 'uninstall-extension' passed in", async () => {
|
||||
const args: UserProvidedArgs = {
|
||||
["uninstall-extension"]: ["hello.world"],
|
||||
}
|
||||
const actual = await shouldSpawnCliProcess(args)
|
||||
const expected = true
|
||||
|
||||
expect(actual).toBe(expected)
|
||||
})
|
||||
})
|
||||
|
||||
describe("bindAddrFromArgs", () => {
|
||||
it("should return the bind address", () => {
|
||||
const args: UserProvidedArgs = {}
|
||||
|
||||
const addr = {
|
||||
host: "localhost",
|
||||
port: 8080,
|
||||
}
|
||||
|
||||
const actual = bindAddrFromArgs(addr, args)
|
||||
const expected = addr
|
||||
|
||||
expect(actual).toStrictEqual(expected)
|
||||
})
|
||||
|
||||
it("should use the bind-address if set in args", () => {
|
||||
const args: UserProvidedArgs = {
|
||||
["bind-addr"]: "localhost:3000",
|
||||
}
|
||||
|
||||
const addr = {
|
||||
host: "localhost",
|
||||
port: 8080,
|
||||
}
|
||||
|
||||
const actual = bindAddrFromArgs(addr, args)
|
||||
const expected = {
|
||||
host: "localhost",
|
||||
port: 3000,
|
||||
}
|
||||
|
||||
expect(actual).toStrictEqual(expected)
|
||||
})
|
||||
|
||||
it("should use the host if set in args", () => {
|
||||
const args: UserProvidedArgs = {
|
||||
["host"]: "coder",
|
||||
}
|
||||
|
||||
const addr = {
|
||||
host: "localhost",
|
||||
port: 8080,
|
||||
}
|
||||
|
||||
const actual = bindAddrFromArgs(addr, args)
|
||||
const expected = {
|
||||
host: "coder",
|
||||
port: 8080,
|
||||
}
|
||||
|
||||
expect(actual).toStrictEqual(expected)
|
||||
})
|
||||
|
||||
it("should use process.env.PORT if set", () => {
|
||||
const [setValue, resetValue] = useEnv("PORT")
|
||||
setValue("8000")
|
||||
|
||||
const args: UserProvidedArgs = {}
|
||||
|
||||
const addr = {
|
||||
host: "localhost",
|
||||
port: 8080,
|
||||
}
|
||||
|
||||
const actual = bindAddrFromArgs(addr, args)
|
||||
const expected = {
|
||||
host: "localhost",
|
||||
port: 8000,
|
||||
}
|
||||
|
||||
expect(actual).toStrictEqual(expected)
|
||||
resetValue()
|
||||
})
|
||||
|
||||
it("should set port if in args", () => {
|
||||
const args: UserProvidedArgs = {
|
||||
port: 3000,
|
||||
}
|
||||
|
||||
const addr = {
|
||||
host: "localhost",
|
||||
port: 8080,
|
||||
}
|
||||
|
||||
const actual = bindAddrFromArgs(addr, args)
|
||||
const expected = {
|
||||
host: "localhost",
|
||||
port: 3000,
|
||||
}
|
||||
|
||||
expect(actual).toStrictEqual(expected)
|
||||
})
|
||||
|
||||
it("should use the args.port over process.env.PORT if both set", () => {
|
||||
const [setValue, resetValue] = useEnv("PORT")
|
||||
setValue("8000")
|
||||
|
||||
const args: UserProvidedArgs = {
|
||||
port: 3000,
|
||||
}
|
||||
|
||||
const addr = {
|
||||
host: "localhost",
|
||||
port: 8080,
|
||||
}
|
||||
|
||||
const actual = bindAddrFromArgs(addr, args)
|
||||
const expected = {
|
||||
host: "localhost",
|
||||
port: 3000,
|
||||
}
|
||||
|
||||
expect(actual).toStrictEqual(expected)
|
||||
resetValue()
|
||||
})
|
||||
})
|
||||
|
||||
describe("defaultConfigFile", () => {
|
||||
it("should return the default config file as a string", async () => {
|
||||
const password = await generatePassword()
|
||||
const actual = defaultConfigFile(password)
|
||||
|
||||
expect(actual).toMatch(`bind-addr: 127.0.0.1:8080
|
||||
auth: password
|
||||
password: ${password}
|
||||
cert: false`)
|
||||
})
|
||||
})
|
||||
|
||||
describe("readSocketPath", () => {
|
||||
const fileContents = "readSocketPath file contents"
|
||||
let tmpDirPath: string
|
||||
let tmpFilePath: string
|
||||
|
||||
beforeEach(async () => {
|
||||
tmpDirPath = await tmpdir("readSocketPath")
|
||||
tmpFilePath = path.join(tmpDirPath, "readSocketPath.txt")
|
||||
await fs.writeFile(tmpFilePath, fileContents)
|
||||
})
|
||||
|
||||
afterEach(async () => {
|
||||
await fs.rmdir(tmpDirPath, { recursive: true })
|
||||
})
|
||||
|
||||
it("should throw an error if it can't read the file", async () => {
|
||||
// TODO@jsjoeio - implement
|
||||
// Test it on a directory.... ESDIR
|
||||
// TODO@jsjoeio - implement
|
||||
expect(() => readSocketPath(tmpDirPath)).rejects.toThrow("EISDIR")
|
||||
})
|
||||
it("should return undefined if it can't read the file", async () => {
|
||||
// TODO@jsjoeio - implement
|
||||
const socketPath = await readSocketPath(path.join(tmpDirPath, "not-a-file"))
|
||||
expect(socketPath).toBeUndefined()
|
||||
})
|
||||
it("should return the file contents", async () => {
|
||||
const contents = await readSocketPath(tmpFilePath)
|
||||
expect(contents).toBe(fileContents)
|
||||
})
|
||||
it("should return the same file contents for two different calls", async () => {
|
||||
const contents1 = await readSocketPath(tmpFilePath)
|
||||
const contents2 = await readSocketPath(tmpFilePath)
|
||||
expect(contents2).toBe(contents1)
|
||||
})
|
||||
})
|
||||
|
||||
describe("toVsCodeArgs", () => {
|
||||
const vscodeDefaults = {
|
||||
...defaults,
|
||||
"connection-token": "0000",
|
||||
"accept-server-license-terms": true,
|
||||
help: false,
|
||||
port: "8080",
|
||||
version: false,
|
||||
}
|
||||
|
||||
beforeAll(async () => {
|
||||
// Clean up temporary directories from the previous run.
|
||||
await clean("vscode-args")
|
||||
})
|
||||
|
||||
it("should convert empty args", async () => {
|
||||
expect(await toVsCodeArgs(await setDefaults(parse([])))).toStrictEqual({
|
||||
...vscodeDefaults,
|
||||
folder: "",
|
||||
workspace: "",
|
||||
})
|
||||
})
|
||||
|
||||
it("should convert with workspace", async () => {
|
||||
const workspace = path.join(await tmpdir("vscode-args"), "test.code-workspace")
|
||||
await fs.writeFile(workspace, "foobar")
|
||||
expect(await toVsCodeArgs(await setDefaults(parse([workspace])))).toStrictEqual({
|
||||
...vscodeDefaults,
|
||||
workspace,
|
||||
folder: "",
|
||||
_: [workspace],
|
||||
})
|
||||
})
|
||||
|
||||
it("should convert with folder", async () => {
|
||||
const folder = await tmpdir("vscode-args")
|
||||
expect(await toVsCodeArgs(await setDefaults(parse([folder])))).toStrictEqual({
|
||||
...vscodeDefaults,
|
||||
folder,
|
||||
workspace: "",
|
||||
_: [folder],
|
||||
})
|
||||
})
|
||||
|
||||
it("should ignore regular file", async () => {
|
||||
const file = path.join(await tmpdir("vscode-args"), "file")
|
||||
await fs.writeFile(file, "foobar")
|
||||
expect(await toVsCodeArgs(await setDefaults(parse([file])))).toStrictEqual({
|
||||
...vscodeDefaults,
|
||||
folder: "",
|
||||
workspace: "",
|
||||
_: [file],
|
||||
})
|
||||
})
|
||||
})
|
||||
|
||||
11
test/unit/node/http.test.ts
Normal file
11
test/unit/node/http.test.ts
Normal file
@@ -0,0 +1,11 @@
|
||||
import { relativeRoot } from "../../../src/node/http"
|
||||
|
||||
describe("http", () => {
|
||||
it("should construct a relative path to the root", () => {
|
||||
expect(relativeRoot("/")).toStrictEqual(".")
|
||||
expect(relativeRoot("/foo")).toStrictEqual(".")
|
||||
expect(relativeRoot("/foo/")).toStrictEqual("./..")
|
||||
expect(relativeRoot("/foo/bar ")).toStrictEqual("./..")
|
||||
expect(relativeRoot("/foo/bar/")).toStrictEqual("./../..")
|
||||
})
|
||||
})
|
||||
@@ -58,7 +58,7 @@ describe("plugin", () => {
|
||||
})
|
||||
|
||||
afterAll(async () => {
|
||||
await s.close()
|
||||
await s.dispose()
|
||||
})
|
||||
|
||||
it("/api/applications", async () => {
|
||||
|
||||
@@ -1,7 +1,7 @@
|
||||
import bodyParser from "body-parser"
|
||||
import * as bodyParser from "body-parser"
|
||||
import * as express from "express"
|
||||
import * as http from "http"
|
||||
import * as nodeFetch from "node-fetch"
|
||||
import nodeFetch from "node-fetch"
|
||||
import { HttpCode } from "../../../src/common/http"
|
||||
import { proxy } from "../../../src/node/proxy"
|
||||
import { getAvailablePort } from "../../utils/helpers"
|
||||
@@ -24,7 +24,7 @@ describe("proxy", () => {
|
||||
})
|
||||
|
||||
afterAll(async () => {
|
||||
await nhooyrDevServer.close()
|
||||
await nhooyrDevServer.dispose()
|
||||
})
|
||||
|
||||
beforeEach(() => {
|
||||
@@ -33,7 +33,7 @@ describe("proxy", () => {
|
||||
|
||||
afterEach(async () => {
|
||||
if (codeServer) {
|
||||
await codeServer.close()
|
||||
await codeServer.dispose()
|
||||
codeServer = undefined
|
||||
}
|
||||
})
|
||||
@@ -202,13 +202,13 @@ describe("proxy (standalone)", () => {
|
||||
it("should return a 500 when proxy target errors ", async () => {
|
||||
// Close the proxy target so that proxy errors
|
||||
await proxyTarget.close()
|
||||
const errorResp = await nodeFetch.default(`${URL}/error`)
|
||||
const errorResp = await nodeFetch(`${URL}/error`)
|
||||
expect(errorResp.status).toBe(HttpCode.ServerError)
|
||||
expect(errorResp.statusText).toBe("Internal Server Error")
|
||||
})
|
||||
|
||||
it("should proxy correctly", async () => {
|
||||
const resp = await nodeFetch.default(`${URL}/route`)
|
||||
const resp = await nodeFetch(`${URL}/route`)
|
||||
expect(resp.status).toBe(200)
|
||||
expect(resp.statusText).toBe("OK")
|
||||
})
|
||||
|
||||
35
test/unit/node/routes/errors.test.ts
Normal file
35
test/unit/node/routes/errors.test.ts
Normal file
@@ -0,0 +1,35 @@
|
||||
import express from "express"
|
||||
import { errorHandler } from "../../../../src/node/routes/errors"
|
||||
|
||||
describe("error page is rendered for text/html requests", () => {
|
||||
it("escapes any html in the error messages", async () => {
|
||||
const next = jest.fn()
|
||||
const err = {
|
||||
code: "ENOENT",
|
||||
statusCode: 404,
|
||||
message: ";>hello<script>alert(1)</script>",
|
||||
}
|
||||
const req = createRequest()
|
||||
const res = {
|
||||
status: jest.fn().mockReturnValue(this),
|
||||
send: jest.fn().mockReturnValue(this),
|
||||
set: jest.fn().mockReturnValue(this),
|
||||
} as unknown as express.Response
|
||||
|
||||
await errorHandler(err, req, res, next)
|
||||
expect(res.status).toHaveBeenCalledWith(404)
|
||||
expect(res.send).toHaveBeenCalledWith(expect.not.stringContaining("<script>"))
|
||||
})
|
||||
})
|
||||
|
||||
function createRequest(): express.Request {
|
||||
return {
|
||||
headers: {
|
||||
accept: ["text/html"],
|
||||
},
|
||||
originalUrl: "http://example.com/test",
|
||||
query: {
|
||||
to: "test",
|
||||
},
|
||||
} as unknown as express.Request
|
||||
}
|
||||
@@ -6,7 +6,7 @@ describe("health", () => {
|
||||
|
||||
afterEach(async () => {
|
||||
if (codeServer) {
|
||||
await codeServer.close()
|
||||
await codeServer.dispose()
|
||||
codeServer = undefined
|
||||
}
|
||||
})
|
||||
|
||||
Some files were not shown because too many files have changed in this diff Show More
Reference in New Issue
Block a user