mirror of
https://github.com/coder/code-server.git
synced 2026-04-13 21:32:52 -05:00
Compare commits
242 Commits
| Author | SHA1 | Date | |
|---|---|---|---|
|
|
9bde62fbd6 | ||
|
|
6fbbb1047f | ||
|
|
e07a591745 | ||
|
|
676c7bf915 | ||
|
|
9ad7d0b7a3 | ||
|
|
07e7c38ea2 | ||
|
|
0b9af6ef67 | ||
|
|
c63dc3a1ea | ||
|
|
860c99e3b8 | ||
|
|
7e1e9d1249 | ||
|
|
62735da694 | ||
|
|
6cc1ee1b00 | ||
|
|
79443c14ff | ||
|
|
a0b7bf2180 | ||
|
|
30f3030530 | ||
|
|
759a78d9d8 | ||
|
|
7093f99a78 | ||
|
|
bca1bcfc03 | ||
|
|
4a3d2e5a94 | ||
|
|
14287df655 | ||
|
|
daf204eeda | ||
|
|
f20f7ac166 | ||
|
|
a7c43a8eb6 | ||
|
|
30d05aeb4b | ||
|
|
07580e1fcb | ||
|
|
e3699cf258 | ||
|
|
2a22676d93 | ||
|
|
36b3183b75 | ||
|
|
ec564091f1 | ||
|
|
ea105a9290 | ||
|
|
e453d3107d | ||
|
|
a4a03c1492 | ||
|
|
d7ba9ae633 | ||
|
|
00383b79b9 | ||
|
|
c6ba12942c | ||
|
|
d7e3112625 | ||
|
|
26c735b434 | ||
|
|
466a04f874 | ||
|
|
e0769dc13a | ||
|
|
fe19391c03 | ||
|
|
021c084e43 | ||
|
|
1902296702 | ||
|
|
bb1bf88439 | ||
|
|
0a8e71c647 | ||
|
|
6bdaada689 | ||
|
|
811cf3364a | ||
|
|
64a6a460c8 | ||
|
|
1e4e72aa5b | ||
|
|
fcfb03382a | ||
|
|
d67bd3f604 | ||
|
|
2d1de749f4 | ||
|
|
c6c293d53a | ||
|
|
daa1c86fe0 | ||
|
|
9002f118c3 | ||
|
|
a5b6d080bd | ||
|
|
9ff37977a8 | ||
|
|
f5489cd3a0 | ||
|
|
c86d7398ab | ||
|
|
9f963c7e66 | ||
|
|
8063c79e44 | ||
|
|
febf4ead96 | ||
|
|
3e28ab85a0 | ||
|
|
85b0804be5 | ||
|
|
ebbcb8d6a7 | ||
|
|
df3089f3ad | ||
|
|
7cc16ceb3a | ||
|
|
bfe731f4f3 | ||
|
|
c4f1c053bf | ||
|
|
4b3c089630 | ||
|
|
1c16814a89 | ||
|
|
c3c24fe4d2 | ||
|
|
6e8248cf0c | ||
|
|
dd996d8f60 | ||
|
|
fae07e14fb | ||
|
|
c308ae0edd | ||
|
|
9035bfa871 | ||
|
|
22c4a7e10f | ||
|
|
607444c695 | ||
|
|
b22f3cb72f | ||
|
|
eacca7d692 | ||
|
|
0aa98279d6 | ||
|
|
55a7e8b56f | ||
|
|
916e24e109 | ||
|
|
c7c62daa67 | ||
|
|
579bb94a6c | ||
|
|
a44b4455f5 | ||
|
|
548a35c0ee | ||
|
|
402f5ebd77 | ||
|
|
c2ac126a50 | ||
|
|
b3811a67e0 | ||
|
|
ddda280df4 | ||
|
|
b415b7524f | ||
|
|
7a982555a8 | ||
|
|
e64b186527 | ||
|
|
11eaf0b470 | ||
|
|
8b5deac92b | ||
|
|
9d87c5328c | ||
|
|
cc5ed1eb57 | ||
|
|
e998dc1e82 | ||
|
|
ffe6a663aa | ||
|
|
938b460685 | ||
|
|
fef619aef8 | ||
|
|
0a2328c1f6 | ||
|
|
e44e574ce1 | ||
|
|
7991e09bbc | ||
|
|
9fb318cf15 | ||
|
|
4a250be79a | ||
|
|
3761f7bd51 | ||
|
|
ceceef1dae | ||
|
|
35a2d71b67 | ||
|
|
617cd38c71 | ||
|
|
75c8fdeed2 | ||
|
|
de41646fc4 | ||
|
|
882a2bfd5a | ||
|
|
b509063e14 | ||
|
|
1a82b2138d | ||
|
|
ada69969ac | ||
|
|
3f508e5e12 | ||
|
|
ce8577b1c3 | ||
|
|
d8d5908d85 | ||
|
|
1558ff6dac | ||
|
|
4b7c2ea322 | ||
|
|
4c4a7413a1 | ||
|
|
ceb2265b14 | ||
|
|
221e95ee89 | ||
|
|
255fa37e1d | ||
|
|
864a9e7bd6 | ||
|
|
a839da34d7 | ||
|
|
3912e9e333 | ||
|
|
eebb8bb314 | ||
|
|
ebbb1187da | ||
|
|
c8f63b61c4 | ||
|
|
c80d093dc4 | ||
|
|
6cc91869d3 | ||
|
|
bf09c294cc | ||
|
|
536ccc0f10 | ||
|
|
312a4d584c | ||
|
|
a730bec6f4 | ||
|
|
ce2eaf2f10 | ||
|
|
5c6cd11836 | ||
|
|
6539dd4dbe | ||
|
|
e8ac0d33f9 | ||
|
|
e237589f2e | ||
|
|
98d8d848a5 | ||
|
|
c8ce380f10 | ||
|
|
c6f054ad6f | ||
|
|
74910ffcdf | ||
|
|
3c90b1e327 | ||
|
|
0dcf469725 | ||
|
|
d8568ebaa9 | ||
|
|
f7790c9719 | ||
|
|
150d37868a | ||
|
|
8590f80c31 | ||
|
|
d6d24966be | ||
|
|
751a5ea3ad | ||
|
|
de568d446b | ||
|
|
7d02f34f71 | ||
|
|
2fad8a2a58 | ||
|
|
a0ff2014c3 | ||
|
|
8d03c22cb0 | ||
|
|
6e27869c09 | ||
|
|
934c8d4eb6 | ||
|
|
9b979ac869 | ||
|
|
3badf6bf7b | ||
|
|
10c2b956ac | ||
|
|
543d64268d | ||
|
|
fd36f8c168 | ||
|
|
c78d164948 | ||
|
|
4dd2c86cca | ||
|
|
42467b3e66 | ||
|
|
361e7103ea | ||
|
|
bac948ea6f | ||
|
|
1c8eede1aa | ||
|
|
486652abaf | ||
|
|
5370f7876d | ||
|
|
eccaf8eb50 | ||
|
|
cbf7c9556c | ||
|
|
b63cf192b5 | ||
|
|
50ed29e0f0 | ||
|
|
ecb9bb2428 | ||
|
|
e86c066438 | ||
|
|
b6e791f7d0 | ||
|
|
c581bca29d | ||
|
|
2fa5037859 | ||
|
|
7c2ca7d03e | ||
|
|
c67d31580f | ||
|
|
58bd7008b4 | ||
|
|
4b6c0a6fc3 | ||
|
|
554b6d6fcf | ||
|
|
8021385ac4 | ||
|
|
5ba650bb6f | ||
|
|
e8f6d30055 | ||
|
|
2819fd51e2 | ||
|
|
638ab7c557 | ||
|
|
0bd808270d | ||
|
|
bc78e16146 | ||
|
|
3764d296c6 | ||
|
|
90eec91f9c | ||
|
|
d3164fc910 | ||
|
|
6c5a9edced | ||
|
|
4727385a01 | ||
|
|
89cfe6876e | ||
|
|
de8e9804ad | ||
|
|
81d25dd048 | ||
|
|
0193516f55 | ||
|
|
19d14d2414 | ||
|
|
739ac468ed | ||
|
|
6c3e4d2a76 | ||
|
|
fb9ebeb7aa | ||
|
|
641b36be6a | ||
|
|
e858a4f4c7 | ||
|
|
a06522f254 | ||
|
|
96af9761b7 | ||
|
|
9ff0e455c3 | ||
|
|
ebef18d626 | ||
|
|
a942531079 | ||
|
|
cc9c9e9db5 | ||
|
|
a7026cc82c | ||
|
|
ed285f97fd | ||
|
|
1b7d4b5a18 | ||
|
|
364f9dd854 | ||
|
|
7e1aebe009 | ||
|
|
609c7ef4ec | ||
|
|
5a6411fa49 | ||
|
|
3da6c561b8 | ||
|
|
bb118eba6e | ||
|
|
96e57f1e6f | ||
|
|
2a9b7a4d5f | ||
|
|
3d9e3b8717 | ||
|
|
264abed82c | ||
|
|
19257a8bc2 | ||
|
|
034266db47 | ||
|
|
69b8096eb3 | ||
|
|
7b982ae782 | ||
|
|
3a37add48d | ||
|
|
7958cc7e29 | ||
|
|
88c76d4794 | ||
|
|
022a2e0860 | ||
|
|
bd2e55dcf3 | ||
|
|
d3773c11f1 | ||
|
|
59694fb72e | ||
|
|
ac2bf56ebc |
@@ -23,6 +23,14 @@ rules:
|
||||
no-dupe-class-members: off
|
||||
"@typescript-eslint/no-use-before-define": off
|
||||
"@typescript-eslint/no-non-null-assertion": off
|
||||
"@typescript-eslint/ban-types": off
|
||||
"@typescript-eslint/no-var-requires": off
|
||||
"@typescript-eslint/explicit-module-boundary-types": off
|
||||
"@typescript-eslint/no-explicit-any": off
|
||||
eqeqeq: error
|
||||
import/order:
|
||||
[error, { alphabetize: { order: "asc" }, groups: [["builtin", "external", "internal"], "parent", "sibling"] }]
|
||||
no-async-promise-executor: off
|
||||
|
||||
settings:
|
||||
# Does not work with CommonJS unfortunately.
|
||||
|
||||
5
.github/ISSUE_TEMPLATE/config.yml
vendored
Normal file
5
.github/ISSUE_TEMPLATE/config.yml
vendored
Normal file
@@ -0,0 +1,5 @@
|
||||
blank_issues_enabled: false
|
||||
contact_links:
|
||||
- name: Question
|
||||
url: https://github.com/cdr/code-server/discussions/new?category_id=22503114
|
||||
about: Ask the community for help
|
||||
7
.github/ISSUE_TEMPLATE/doc.md
vendored
Normal file
7
.github/ISSUE_TEMPLATE/doc.md
vendored
Normal file
@@ -0,0 +1,7 @@
|
||||
---
|
||||
name: Documentation improvement
|
||||
about: Suggest a documentation improvement
|
||||
title: ""
|
||||
labels: "docs"
|
||||
assignees: ""
|
||||
---
|
||||
4
.github/issue_template.md
vendored
4
.github/issue_template.md
vendored
@@ -1,4 +0,0 @@
|
||||
<!--
|
||||
Please file all questions and support requests at https://www.reddit.com/r/codeserver/
|
||||
The issue tracker is only for bugs and features.
|
||||
-->
|
||||
37
.github/lock.yml
vendored
Normal file
37
.github/lock.yml
vendored
Normal file
@@ -0,0 +1,37 @@
|
||||
# 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
|
||||
2
.github/pull_request_template.md
vendored
2
.github/pull_request_template.md
vendored
@@ -1,4 +1,6 @@
|
||||
<!--
|
||||
Please link to the issue this PR solves.
|
||||
If there is no existing issue, please first create one unless the fix is minor.
|
||||
|
||||
Please make sure the base of your PR is the master branch!
|
||||
-->
|
||||
|
||||
10
.github/workflows/ci.yaml
vendored
10
.github/workflows/ci.yaml
vendored
@@ -8,7 +8,7 @@ jobs:
|
||||
steps:
|
||||
- uses: actions/checkout@v1
|
||||
- name: Run ./ci/steps/fmt.sh
|
||||
uses: ./ci/images/debian8
|
||||
uses: ./ci/images/debian10
|
||||
with:
|
||||
args: ./ci/steps/fmt.sh
|
||||
|
||||
@@ -17,7 +17,7 @@ jobs:
|
||||
steps:
|
||||
- uses: actions/checkout@v1
|
||||
- name: Run ./ci/steps/lint.sh
|
||||
uses: ./ci/images/debian8
|
||||
uses: ./ci/images/debian10
|
||||
with:
|
||||
args: ./ci/steps/lint.sh
|
||||
|
||||
@@ -26,7 +26,7 @@ jobs:
|
||||
steps:
|
||||
- uses: actions/checkout@v1
|
||||
- name: Run ./ci/steps/test.sh
|
||||
uses: ./ci/images/debian8
|
||||
uses: ./ci/images/debian10
|
||||
with:
|
||||
args: ./ci/steps/test.sh
|
||||
|
||||
@@ -35,7 +35,7 @@ jobs:
|
||||
steps:
|
||||
- uses: actions/checkout@v1
|
||||
- name: Run ./ci/steps/release.sh
|
||||
uses: ./ci/images/debian8
|
||||
uses: ./ci/images/debian10
|
||||
with:
|
||||
args: ./ci/steps/release.sh
|
||||
- name: Upload npm package artifact
|
||||
@@ -116,7 +116,7 @@ jobs:
|
||||
name: release-packages
|
||||
path: ./release-packages
|
||||
- name: Run ./ci/steps/build-docker-image.sh
|
||||
uses: ./ci/images/debian8
|
||||
uses: ./ci/images/debian10
|
||||
with:
|
||||
args: ./ci/steps/build-docker-image.sh
|
||||
- name: Upload release image
|
||||
|
||||
4
.github/workflows/publish.yaml
vendored
4
.github/workflows/publish.yaml
vendored
@@ -10,7 +10,7 @@ jobs:
|
||||
steps:
|
||||
- uses: actions/checkout@v1
|
||||
- name: Run ./ci/steps/publish-npm.sh
|
||||
uses: ./ci/images/debian8
|
||||
uses: ./ci/images/debian10
|
||||
with:
|
||||
args: ./ci/steps/publish-npm.sh
|
||||
env:
|
||||
@@ -22,7 +22,7 @@ jobs:
|
||||
steps:
|
||||
- uses: actions/checkout@v1
|
||||
- name: Run ./ci/steps/push-docker-manifest.sh
|
||||
uses: ./ci/images/debian8
|
||||
uses: ./ci/images/debian10
|
||||
with:
|
||||
args: ./ci/steps/push-docker-manifest.sh
|
||||
env:
|
||||
|
||||
3
.gitignore
vendored
3
.gitignore
vendored
@@ -10,3 +10,6 @@ release-gcp/
|
||||
release-images/
|
||||
node_modules
|
||||
node-*
|
||||
/plugins
|
||||
/lib/coder-cloud-agent
|
||||
.home
|
||||
|
||||
1
.gitmodules
vendored
1
.gitmodules
vendored
@@ -1,3 +1,4 @@
|
||||
[submodule "lib/vscode"]
|
||||
path = lib/vscode
|
||||
url = https://github.com/microsoft/vscode
|
||||
ignore = dirty
|
||||
|
||||
56
README.md
56
README.md
@@ -1,4 +1,4 @@
|
||||
# code-server
|
||||
# code-server · [](https://github.com/cdr/code-server/discussions) [](https://cdr.co/join-community) [](https://twitter.com/coderhq)
|
||||
|
||||
Run [VS Code](https://github.com/Microsoft/vscode) on any machine anywhere and access it in the browser.
|
||||
|
||||
@@ -6,53 +6,59 @@ Run [VS Code](https://github.com/Microsoft/vscode) on any machine anywhere and a
|
||||
|
||||
## Highlights
|
||||
|
||||
- **Code everywhere**
|
||||
- Code on your Chromebook, tablet, and laptop with a consistent development environment.
|
||||
- Develop on a Linux machine and pick up from any device with a web browser.
|
||||
- **Server-powered**
|
||||
- Take advantage of large cloud servers to speed up tests, compilations, downloads, and more.
|
||||
- Preserve battery life when you're on the go as all intensive tasks runs on your server.
|
||||
- Make use of a spare computer you have lying around and turn it into a full development environment.
|
||||
- Code on any device with a consistent development environment
|
||||
- Use cloud servers to speed up tests, compilations, downloads, and more
|
||||
- Preserve battery life when you're on the go; all intensive tasks run on your server
|
||||
|
||||
## Getting Started
|
||||
|
||||
For a full setup and walkthrough, please see [./doc/guide.md](./doc/guide.md).
|
||||
There are two ways to get started:
|
||||
|
||||
We have a [script](./install.sh) to install code-server for Linux and macOS.
|
||||
1. Using the [install script](./install.sh), which automates most of the process. The script uses the system package manager (if possible)
|
||||
2. Manually installing code-server; see [Installation](./doc/install.md) for instructions applicable to most use cases
|
||||
|
||||
It tries to use the system package manager if possible.
|
||||
|
||||
First run to print out the install process:
|
||||
If you choose to use the install script, you can preview what occurs during the install process:
|
||||
|
||||
```bash
|
||||
curl -fsSL https://code-server.dev/install.sh | sh -s -- --dry-run
|
||||
```
|
||||
|
||||
Now to actually install:
|
||||
To install, run:
|
||||
|
||||
```bash
|
||||
curl -fsSL https://code-server.dev/install.sh | sh
|
||||
```
|
||||
|
||||
The install script will print out how to run and start using code-server.
|
||||
When done, the install script prints out instructions for running and starting code-server.
|
||||
|
||||
If you believe an install script used with `curl | sh` is insecure, please give
|
||||
[this wonderful blogpost](https://sandstorm.io/news/2015-09-24-is-curl-bash-insecure-pgp-verified-install) by
|
||||
[sandstorm.io](https://sandstorm.io) a read.
|
||||
We also have an in-depth [setup and configuration](./doc/guide.md) guide.
|
||||
|
||||
Docs on the install script, manual installation and docker image are at [./doc/install.md](./doc/install.md).
|
||||
### Alpha Program 🐣
|
||||
|
||||
We also highly recommend reading the [FAQ](./doc/FAQ.md) on the [Differences compared to VS Code](./doc/FAQ.md#differences-compared-to-vs-code).
|
||||
We're working on a cloud platform that makes deploying and managing code-server easier. Consider [joining our alpha program](https://codercom.typeform.com/to/U4IKyv0W) if you don't want to worry about
|
||||
|
||||
- TLS
|
||||
- Authentication
|
||||
- Port Forwarding
|
||||
|
||||
## FAQ
|
||||
|
||||
See [./doc/FAQ.md](./doc/FAQ.md).
|
||||
|
||||
## Contributing
|
||||
## Want to help?
|
||||
|
||||
See [./doc/CONTRIBUTING.md](./doc/CONTRIBUTING.md).
|
||||
See [CONTRIBUTING](./doc/CONTRIBUTING.md) for details.
|
||||
|
||||
## Enterprise
|
||||
## Hiring
|
||||
|
||||
Visit [our website](https://coder.com) for more information about our
|
||||
enterprise offerings.
|
||||
We ([@cdr](https://github.com/cdr)) are looking for engineers to help [maintain
|
||||
code-server](https://jobs.lever.co/coder/e40becde-2cbd-4885-9029-e5c7b0a734b8), innovate on open source, and streamline dev workflows.
|
||||
|
||||
Our main office is in Austin, Texas. Remote is ok as long as
|
||||
you're in North America or Europe.
|
||||
|
||||
Please get in [touch](mailto:jobs@coder.com) with your resume/GitHub if interested.
|
||||
|
||||
## For Organizations
|
||||
|
||||
Visit [our website](https://coder.com) for more information about remote development for your organization or enterprise.
|
||||
|
||||
22
ThirdPartyNotices.txt
Normal file
22
ThirdPartyNotices.txt
Normal file
@@ -0,0 +1,22 @@
|
||||
code-server
|
||||
|
||||
THIRD-PARTY SOFTWARE NOTICES AND INFORMATION
|
||||
Do Not Translate or Localize
|
||||
|
||||
1. Microsoft/vscode version 1.47.0 (https://github.com/Microsoft/vscode)
|
||||
|
||||
%% Microsoft/vscode NOTICES AND INFORMATION BEGIN HERE
|
||||
=========================================
|
||||
MIT License
|
||||
|
||||
|
||||
Copyright (c) 2015 - present Microsoft Corporation
|
||||
|
||||
|
||||
Permission is hereby granted, free of charge, to any person obtaining a copy of this software and associated documentation files (the "Software"), to deal in the Software without restriction, including without limitation the rights to use, copy, modify, merge, publish, distribute, sublicense, and/or sell copies of the Software, and to permit persons to whom the Software is furnished to do so, subject to the following conditions:
|
||||
|
||||
|
||||
The above copyright notice and this permission notice shall be included in all copies or substantial portions of the Software.
|
||||
|
||||
|
||||
THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE SOFTWARE.
|
||||
10
ci/README.md
10
ci/README.md
@@ -18,14 +18,17 @@ Make sure you have `$GITHUB_TOKEN` set and [hub](https://github.com/github/hub)
|
||||
1. Update in `package.json`
|
||||
2. Update in [./doc/install.md](../doc/install.md)
|
||||
2. GitHub actions will generate the `npm-package`, `release-packages` and `release-images` artifacts.
|
||||
1. You do not have to wait for these.
|
||||
3. Run `yarn release:github-draft` to create a GitHub draft release from the template with
|
||||
the updated version.
|
||||
1. Summarize the major changes in the release notes and link to the relevant issues.
|
||||
4. Wait for the artifacts in step 2 to build.
|
||||
5. Run `yarn release:github-assets` to download the `release-packages` artifact and then
|
||||
upload them to the draft release.
|
||||
5. Run `yarn release:github-assets` to download the `release-packages` artifact.
|
||||
- It will upload them to the draft release.
|
||||
6. Run some basic sanity tests on one of the released packages.
|
||||
7. Make sure the github release tag is the commit with the artifacts.
|
||||
- Especially make sure the terminal works fine.
|
||||
7. Make sure the github release tag is the commit with the artifacts. This is a bug in
|
||||
`hub` where uploading assets in step 5 will break the tag.
|
||||
8. Publish the release and merge the PR.
|
||||
1. CI will automatically grab the artifacts and then:
|
||||
1. Publish the NPM package from `npm-package`.
|
||||
@@ -35,7 +38,6 @@ Make sure you have `$GITHUB_TOKEN` set and [hub](https://github.com/github/hub)
|
||||
10. Wait for the npm package to be published.
|
||||
11. Update the homebrew package.
|
||||
- Send a pull request to [homebrew-core](https://github.com/Homebrew/homebrew-core) with the URL in the [formula](https://github.com/Homebrew/homebrew-core/blob/master/Formula/code-server.rb) updated.
|
||||
12. Make sure to add a release without the `v` prefix for autoupdate from `3.2.0`.
|
||||
|
||||
## dev
|
||||
|
||||
|
||||
@@ -9,7 +9,8 @@ MINIFY=${MINIFY-true}
|
||||
main() {
|
||||
cd "$(dirname "${0}")/../.."
|
||||
|
||||
tsc --outDir out --tsBuildInfoFile .cache/out.tsbuildinfo
|
||||
tsc
|
||||
|
||||
# If out/node/entry.js does not already have the shebang,
|
||||
# we make sure to add it and make it executable.
|
||||
if ! grep -q -m1 "^#!/usr/bin/env node" out/node/entry.js; then
|
||||
@@ -17,13 +18,20 @@ main() {
|
||||
chmod +x out/node/entry.js
|
||||
fi
|
||||
|
||||
if ! [ -f ./lib/coder-cloud-agent ]; then
|
||||
OS="$(uname | tr '[:upper:]' '[:lower:]')"
|
||||
curl -fsSL "https://storage.googleapis.com/coder-cloud-releases/agent/latest/$OS/cloud-agent" -o ./lib/coder-cloud-agent
|
||||
chmod +x ./lib/coder-cloud-agent
|
||||
fi
|
||||
|
||||
parcel build \
|
||||
--public-url "/static/$(git rev-parse HEAD)/dist" \
|
||||
--public-url "." \
|
||||
--out-dir dist \
|
||||
$([[ $MINIFY ]] || echo --no-minify) \
|
||||
src/browser/pages/app.ts \
|
||||
src/browser/register.ts \
|
||||
src/browser/serviceWorker.ts
|
||||
src/browser/serviceWorker.ts \
|
||||
src/browser/pages/login.ts \
|
||||
src/browser/pages/vscode.ts
|
||||
}
|
||||
|
||||
main "$@"
|
||||
|
||||
@@ -11,15 +11,6 @@ main() {
|
||||
mkdir -p release-packages
|
||||
|
||||
release_archive
|
||||
# Will stop the auto update issues and allow people to upgrade their scripts
|
||||
# for the new release structure.
|
||||
if [[ $ARCH == "amd64" ]]; then
|
||||
if [[ $OS == "linux" ]]; then
|
||||
ARCH=x86_64 release_archive
|
||||
elif [[ $OS == "macos" ]]; then
|
||||
OS=darwin ARCH=x86_64 release_archive
|
||||
fi
|
||||
fi
|
||||
|
||||
if [[ $OS == "linux" ]]; then
|
||||
release_nfpm
|
||||
@@ -30,12 +21,6 @@ release_archive() {
|
||||
local release_name="code-server-$VERSION-$OS-$ARCH"
|
||||
if [[ $OS == "linux" ]]; then
|
||||
tar -czf "release-packages/$release_name.tar.gz" --transform "s/^\.\/release-standalone/$release_name/" ./release-standalone
|
||||
elif [[ $OS == "darwin" && $ARCH == "x86_64" ]]; then
|
||||
# Just exists to make autoupdating from 3.2.0 work again.
|
||||
mv ./release-standalone "./$release_name"
|
||||
zip -r "release-packages/$release_name.zip" "./$release_name"
|
||||
mv "./$release_name" ./release-standalone
|
||||
return
|
||||
else
|
||||
tar -czf "release-packages/$release_name.tar.gz" -s "/^release-standalone/$release_name/" release-standalone
|
||||
fi
|
||||
|
||||
@@ -6,6 +6,10 @@ set -euo pipefail
|
||||
# MINIFY controls whether minified vscode is bundled.
|
||||
MINIFY="${MINIFY-true}"
|
||||
|
||||
# KEEP_MODULES controls whether the script cleans all node_modules requiring a yarn install
|
||||
# to run first.
|
||||
KEEP_MODULES="${KEEP_MODULES-0}"
|
||||
|
||||
main() {
|
||||
cd "$(dirname "${0}")/../.."
|
||||
source ./ci/lib.sh
|
||||
@@ -21,6 +25,12 @@ main() {
|
||||
rsync README.md "$RELEASE_PATH"
|
||||
rsync LICENSE.txt "$RELEASE_PATH"
|
||||
rsync ./lib/vscode/ThirdPartyNotices.txt "$RELEASE_PATH"
|
||||
|
||||
# code-server exports types which can be imported and used by plugins. Those
|
||||
# types import ipc.d.ts but it isn't included in the final vscode build so
|
||||
# we'll copy it ourselves here.
|
||||
mkdir -p "$RELEASE_PATH/lib/vscode/src/vs/server"
|
||||
rsync ./lib/vscode/src/vs/server/ipc.d.ts "$RELEASE_PATH/lib/vscode/src/vs/server"
|
||||
}
|
||||
|
||||
bundle_code_server() {
|
||||
@@ -31,6 +41,7 @@ bundle_code_server() {
|
||||
rsync src/browser/media/ "$RELEASE_PATH/src/browser/media"
|
||||
mkdir -p "$RELEASE_PATH/src/browser/pages"
|
||||
rsync src/browser/pages/*.html "$RELEASE_PATH/src/browser/pages"
|
||||
rsync src/browser/robots.txt "$RELEASE_PATH/src/browser"
|
||||
|
||||
# Adds the commit to package.json
|
||||
jq --slurp '.[0] * .[1]' package.json <(
|
||||
@@ -45,15 +56,25 @@ EOF
|
||||
) > "$RELEASE_PATH/package.json"
|
||||
rsync yarn.lock "$RELEASE_PATH"
|
||||
rsync ci/build/npm-postinstall.sh "$RELEASE_PATH/postinstall.sh"
|
||||
|
||||
if [ "$KEEP_MODULES" = 1 ]; then
|
||||
rsync node_modules/ "$RELEASE_PATH/node_modules"
|
||||
mkdir -p "$RELEASE_PATH/lib"
|
||||
rsync ./lib/coder-cloud-agent "$RELEASE_PATH/lib"
|
||||
fi
|
||||
}
|
||||
|
||||
bundle_vscode() {
|
||||
mkdir -p "$VSCODE_OUT_PATH"
|
||||
rsync "$VSCODE_SRC_PATH/yarn.lock" "$VSCODE_OUT_PATH"
|
||||
rsync "$VSCODE_SRC_PATH/out-vscode${MINIFY+-min}/" "$VSCODE_OUT_PATH/out"
|
||||
rsync "$VSCODE_SRC_PATH/out-vscode${MINIFY:+-min}/" "$VSCODE_OUT_PATH/out"
|
||||
|
||||
rsync "$VSCODE_SRC_PATH/.build/extensions/" "$VSCODE_OUT_PATH/extensions"
|
||||
rm -Rf "$VSCODE_OUT_PATH/extensions/node_modules"
|
||||
if [ "$KEEP_MODULES" = 0 ]; then
|
||||
rm -Rf "$VSCODE_OUT_PATH/extensions/node_modules"
|
||||
else
|
||||
rsync "$VSCODE_SRC_PATH/node_modules/" "$VSCODE_OUT_PATH/node_modules"
|
||||
fi
|
||||
rsync "$VSCODE_SRC_PATH/extensions/package.json" "$VSCODE_OUT_PATH/extensions"
|
||||
rsync "$VSCODE_SRC_PATH/extensions/yarn.lock" "$VSCODE_OUT_PATH/extensions"
|
||||
rsync "$VSCODE_SRC_PATH/extensions/postinstall.js" "$VSCODE_OUT_PATH/extensions"
|
||||
|
||||
@@ -5,16 +5,7 @@ main() {
|
||||
cd "$(dirname "${0}")/../.."
|
||||
source ./ci/lib.sh
|
||||
|
||||
rm -Rf \
|
||||
out \
|
||||
release \
|
||||
release-standalone \
|
||||
release-packages \
|
||||
release-gcp \
|
||||
release-images/ \
|
||||
dist \
|
||||
.tsbuildinfo \
|
||||
.cache/out.tsbuildinfo
|
||||
git clean -Xffd
|
||||
|
||||
pushd lib/vscode
|
||||
git clean -xffd
|
||||
|
||||
12
ci/build/code-server@.service
Normal file
12
ci/build/code-server@.service
Normal file
@@ -0,0 +1,12 @@
|
||||
[Unit]
|
||||
Description=code-server
|
||||
After=network.target
|
||||
|
||||
[Service]
|
||||
Type=exec
|
||||
ExecStart=/usr/bin/code-server
|
||||
Restart=always
|
||||
User=%i
|
||||
|
||||
[Install]
|
||||
WantedBy=default.target
|
||||
@@ -12,5 +12,8 @@ homepage: "https://github.com/cdr/code-server"
|
||||
license: "MIT"
|
||||
files:
|
||||
./ci/build/code-server-nfpm.sh: /usr/bin/code-server
|
||||
./ci/build/code-server.service: /usr/lib/systemd/user/code-server.service
|
||||
./ci/build/code-server@.service: /usr/lib/systemd/system/code-server@.service
|
||||
# Only included for backwards compat with previous releases that shipped
|
||||
# the user service. See #1997
|
||||
./ci/build/code-server-user.service: /usr/lib/systemd/user/code-server.service
|
||||
./release-standalone/**/*: "/usr/lib/code-server/"
|
||||
|
||||
@@ -24,6 +24,13 @@ main() {
|
||||
;;
|
||||
esac
|
||||
|
||||
OS="$(uname | tr '[:upper:]' '[:lower:]')"
|
||||
if curl -fsSL "https://storage.googleapis.com/coder-cloud-releases/agent/latest/$OS/cloud-agent" -o ./lib/coder-cloud-agent; then
|
||||
chmod +x ./lib/coder-cloud-agent
|
||||
else
|
||||
echo "Failed to download cloud agent; --link will not work"
|
||||
fi
|
||||
|
||||
if ! vscode_yarn; then
|
||||
echo "You may not have the required dependencies to build the native modules."
|
||||
echo "Please see https://github.com/cdr/code-server/blob/master/doc/npm.md"
|
||||
@@ -36,6 +43,13 @@ vscode_yarn() {
|
||||
yarn --production --frozen-lockfile
|
||||
cd extensions
|
||||
yarn --production --frozen-lockfile
|
||||
for ext in */; do
|
||||
ext="${ext%/}"
|
||||
echo "extensions/$ext: installing dependencies"
|
||||
cd "$ext"
|
||||
yarn --production --frozen-lockfile
|
||||
cd "$OLDPWD"
|
||||
done
|
||||
}
|
||||
|
||||
main "$@"
|
||||
|
||||
@@ -15,8 +15,7 @@ main() {
|
||||
./release-standalone/bin/code-server --extensions-dir "$EXTENSIONS_DIR" --install-extension ms-python.python
|
||||
local installed_extensions
|
||||
installed_extensions="$(./release-standalone/bin/code-server --extensions-dir "$EXTENSIONS_DIR" --list-extensions 2>&1)"
|
||||
if [[ $installed_extensions != "info Using config file ~/.config/code-server/config.yaml
|
||||
ms-python.python" ]]; then
|
||||
if [[ $installed_extensions != "ms-python.python" ]]; then
|
||||
echo "Unexpected output from listing extensions:"
|
||||
echo "$installed_extensions"
|
||||
exit 1
|
||||
|
||||
@@ -6,7 +6,7 @@ main() {
|
||||
|
||||
cd ./lib/vscode
|
||||
git add -A
|
||||
git diff HEAD > ../../ci/dev/vscode.patch
|
||||
git diff HEAD --full-index > ../../ci/dev/vscode.patch
|
||||
}
|
||||
|
||||
main "$@"
|
||||
|
||||
@@ -1,13 +0,0 @@
|
||||
FROM node:12
|
||||
|
||||
RUN apt-get update && apt-get install -y \
|
||||
curl \
|
||||
iproute2 \
|
||||
vim \
|
||||
iptables \
|
||||
net-tools \
|
||||
libsecret-1-dev \
|
||||
libx11-dev \
|
||||
libxkbfile-dev
|
||||
|
||||
CMD ["/bin/bash"]
|
||||
@@ -1,48 +0,0 @@
|
||||
#!/usr/bin/env bash
|
||||
set -euo pipefail
|
||||
|
||||
# Opens an interactive bash session inside of a docker container
|
||||
# for improved isolation during development.
|
||||
# If the container exists it is restarted if necessary, then reused.
|
||||
|
||||
main() {
|
||||
cd "$(dirname "${0}")/../../.."
|
||||
|
||||
local container_name=code-server-dev
|
||||
|
||||
if docker inspect $container_name &> /dev/null; then
|
||||
echo "-- Starting container"
|
||||
docker start "$container_name" > /dev/null
|
||||
|
||||
enter
|
||||
exit 0
|
||||
fi
|
||||
|
||||
build
|
||||
run
|
||||
enter
|
||||
}
|
||||
|
||||
enter() {
|
||||
echo "--- Entering $container_name"
|
||||
docker exec -it "$container_name" /bin/bash
|
||||
}
|
||||
|
||||
run() {
|
||||
echo "--- Spawning $container_name"
|
||||
docker run \
|
||||
-it \
|
||||
--name $container_name \
|
||||
"-v=$PWD:/code-server" \
|
||||
"-w=/code-server" \
|
||||
"-p=127.0.0.1:8080:8080" \
|
||||
$(if [[ -t 0 ]]; then echo -it; fi) \
|
||||
"$container_name"
|
||||
}
|
||||
|
||||
build() {
|
||||
echo "--- Building $container_name"
|
||||
docker build -t $container_name ./ci/dev/image > /dev/null
|
||||
}
|
||||
|
||||
main "$@"
|
||||
31
ci/dev/image/run.sh
Executable file
31
ci/dev/image/run.sh
Executable file
@@ -0,0 +1,31 @@
|
||||
#!/usr/bin/env bash
|
||||
set -euo pipefail
|
||||
|
||||
main() {
|
||||
cd "$(dirname "$0")/../../.."
|
||||
source ./ci/lib.sh
|
||||
mkdir -p .home
|
||||
|
||||
docker run \
|
||||
-it \
|
||||
--rm \
|
||||
-v "$PWD:/src" \
|
||||
-e HOME="/src/.home" \
|
||||
-e USER="coder" \
|
||||
-e GITHUB_TOKEN \
|
||||
-e KEEP_MODULES \
|
||||
-e MINIFY \
|
||||
-w /src \
|
||||
-p 127.0.0.1:8080:8080 \
|
||||
-u "$(id -u):$(id -g)" \
|
||||
-e CI \
|
||||
"$(docker_build ./ci/images/"${IMAGE-debian10}")" \
|
||||
"$@"
|
||||
}
|
||||
|
||||
docker_build() {
|
||||
docker build "$@" >&2
|
||||
docker build -q "$@"
|
||||
}
|
||||
|
||||
main "$@"
|
||||
@@ -7,10 +7,7 @@ main() {
|
||||
eslint --max-warnings=0 --fix $(git ls-files "*.ts" "*.tsx" "*.js")
|
||||
stylelint $(git ls-files "*.css")
|
||||
tsc --noEmit
|
||||
# See comment in ./ci/image/debian8
|
||||
if [[ ! ${CI-} ]]; then
|
||||
shellcheck -e SC2046,SC2164,SC2154,SC1091,SC1090,SC2002 $(git ls-files "*.sh")
|
||||
fi
|
||||
shellcheck -e SC2046,SC2164,SC2154,SC1091,SC1090,SC2002 $(git ls-files "*.sh")
|
||||
}
|
||||
|
||||
main "$@"
|
||||
|
||||
1491
ci/dev/vscode.patch
1491
ci/dev/vscode.patch
File diff suppressed because it is too large
Load Diff
@@ -37,6 +37,9 @@ class Watcher {
|
||||
|
||||
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 bundler = this.createBundler()
|
||||
|
||||
const cleanup = (code?: number | null): void => {
|
||||
@@ -48,6 +51,12 @@ class Watcher {
|
||||
tsc.removeAllListeners()
|
||||
tsc.kill()
|
||||
|
||||
if (plugin) {
|
||||
Watcher.log("killing plugin")
|
||||
plugin.removeAllListeners()
|
||||
plugin.kill()
|
||||
}
|
||||
|
||||
if (server) {
|
||||
Watcher.log("killing server")
|
||||
server.removeAllListeners()
|
||||
@@ -69,6 +78,12 @@ class Watcher {
|
||||
Watcher.log("tsc terminated unexpectedly")
|
||||
cleanup(code)
|
||||
})
|
||||
if (plugin) {
|
||||
plugin.on("exit", (code) => {
|
||||
Watcher.log("plugin terminated unexpectedly")
|
||||
cleanup(code)
|
||||
})
|
||||
}
|
||||
const bundle = bundler.bundle().catch(() => {
|
||||
Watcher.log("parcel watcher terminated unexpectedly")
|
||||
cleanup(1)
|
||||
@@ -82,6 +97,9 @@ class Watcher {
|
||||
|
||||
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))
|
||||
}
|
||||
|
||||
// From https://github.com/chalk/ansi-regex
|
||||
const pattern = [
|
||||
@@ -140,21 +158,34 @@ class Watcher {
|
||||
bundle.then(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")) {
|
||||
bundle.then(restartServer)
|
||||
}
|
||||
})
|
||||
}
|
||||
}
|
||||
|
||||
private createBundler(out = "dist"): Bundler {
|
||||
return new Bundler(
|
||||
[
|
||||
path.join(this.rootPath, "src/browser/pages/app.ts"),
|
||||
path.join(this.rootPath, "src/browser/register.ts"),
|
||||
path.join(this.rootPath, "src/browser/serviceWorker.ts"),
|
||||
path.join(this.rootPath, "src/browser/pages/login.ts"),
|
||||
path.join(this.rootPath, "src/browser/pages/vscode.ts"),
|
||||
],
|
||||
{
|
||||
outDir: path.join(this.rootPath, out),
|
||||
cacheDir: path.join(this.rootPath, ".cache"),
|
||||
minify: !!process.env.MINIFY,
|
||||
logLevel: 1,
|
||||
publicUrl: "/static/development/dist",
|
||||
publicUrl: ".",
|
||||
},
|
||||
)
|
||||
}
|
||||
|
||||
@@ -1,9 +1,10 @@
|
||||
FROM centos:7
|
||||
|
||||
ARG NODE_VERSION=v12.18.4
|
||||
RUN ARCH="$(uname -m | sed 's/86_64/64/; s/aarch64/arm64/')" && \
|
||||
curl -fsSL "https://nodejs.org/dist/v14.4.0/node-v14.4.0-linux-$ARCH.tar.xz" | tar -C /usr/local -xJ && \
|
||||
mv /usr/local/node-v14.4.0-linux-$ARCH /usr/local/node-v14.4.0
|
||||
ENV PATH=/usr/local/node-v14.4.0/bin:$PATH
|
||||
curl -fsSL "https://nodejs.org/dist/$NODE_VERSION/node-$NODE_VERSION-linux-$ARCH.tar.xz" | tar -C /usr/local -xJ && \
|
||||
mv "/usr/local/node-$NODE_VERSION-linux-$ARCH" "/usr/local/node-$NODE_VERSION"
|
||||
ENV PATH=/usr/local/node-$NODE_VERSION/bin:$PATH
|
||||
RUN npm install -g yarn
|
||||
|
||||
RUN yum groupinstall -y 'Development Tools'
|
||||
@@ -14,11 +15,16 @@ RUN npm config set python python2
|
||||
RUN yum install -y epel-release && yum install -y jq
|
||||
RUN yum install -y rsync
|
||||
|
||||
# Copied from ../debian8/Dockerfile
|
||||
# Install Go dependencies
|
||||
# Copied from ../debian10/Dockerfile
|
||||
# Install Go.
|
||||
RUN ARCH="$(uname -m | sed 's/x86_64/amd64/; s/aarch64/arm64/')" && \
|
||||
curl -fsSL "https://dl.google.com/go/go1.14.3.linux-$ARCH.tar.gz" | tar -C /usr/local -xz
|
||||
ENV PATH=/usr/local/go/bin:/root/go/bin:$PATH
|
||||
ENV GOPATH=/gopath
|
||||
# Ensures running this image as another user works.
|
||||
RUN mkdir -p $GOPATH && chmod -R 777 $GOPATH
|
||||
ENV PATH=/usr/local/go/bin:$GOPATH/bin:$PATH
|
||||
|
||||
# Install Go dependencies
|
||||
ENV GO111MODULE=on
|
||||
RUN go get mvdan.cc/sh/v3/cmd/shfmt
|
||||
RUN go get github.com/goreleaser/nfpm/cmd/nfpm
|
||||
|
||||
@@ -1,4 +1,4 @@
|
||||
FROM debian:8
|
||||
FROM debian:10
|
||||
|
||||
RUN apt-get update
|
||||
|
||||
@@ -6,7 +6,7 @@ RUN apt-get update
|
||||
RUN apt-get install -y curl gnupg
|
||||
|
||||
# Installs node.
|
||||
RUN curl -fsSL https://deb.nodesource.com/setup_14.x | bash - && \
|
||||
RUN curl -fsSL https://deb.nodesource.com/setup_12.x | bash - && \
|
||||
apt-get install -y nodejs
|
||||
|
||||
# Installs yarn.
|
||||
@@ -24,28 +24,23 @@ RUN apt-get install -y build-essential \
|
||||
RUN apt-get install -y gettext-base
|
||||
|
||||
# Misc build dependencies.
|
||||
RUN apt-get install -y git rsync unzip
|
||||
|
||||
# We need latest jq from debian buster for date support.
|
||||
RUN ARCH="$(dpkg --print-architecture)" && \
|
||||
curl -fsSOL http://http.us.debian.org/debian/pool/main/libo/libonig/libonig5_6.9.1-1_$ARCH.deb && \
|
||||
dpkg -i libonig*.deb && \
|
||||
curl -fsSOL http://http.us.debian.org/debian/pool/main/j/jq/libjq1_1.5+dfsg-2+b1_$ARCH.deb && \
|
||||
dpkg -i libjq*.deb && \
|
||||
curl -fsSOL http://http.us.debian.org/debian/pool/main/j/jq/jq_1.5+dfsg-2+b1_$ARCH.deb && \
|
||||
dpkg -i jq*.deb && rm *.deb
|
||||
RUN apt-get install -y git rsync unzip jq
|
||||
|
||||
# Installs shellcheck.
|
||||
# Unfortunately coredumps on debian:8 so disabled for now.
|
||||
#RUN curl -fsSL https://github.com/koalaman/shellcheck/releases/download/v0.7.1/shellcheck-v0.7.1.linux.$(uname -m).tar.xz | \
|
||||
# tar -xJ && \
|
||||
# mv shellcheck*/shellcheck /usr/local/bin && \
|
||||
# rm -R shellcheck*
|
||||
RUN curl -fsSL https://github.com/koalaman/shellcheck/releases/download/v0.7.1/shellcheck-v0.7.1.linux.$(uname -m).tar.xz | \
|
||||
tar -xJ && \
|
||||
mv shellcheck*/shellcheck /usr/local/bin && \
|
||||
rm -R shellcheck*
|
||||
|
||||
# Install Go dependencies
|
||||
# Install Go.
|
||||
RUN ARCH="$(uname -m | sed 's/x86_64/amd64/; s/aarch64/arm64/')" && \
|
||||
curl -fsSL "https://dl.google.com/go/go1.14.3.linux-$ARCH.tar.gz" | tar -C /usr/local -xz
|
||||
ENV PATH=/usr/local/go/bin:/root/go/bin:$PATH
|
||||
ENV GOPATH=/gopath
|
||||
# Ensures running this image as another user works.
|
||||
RUN mkdir -p $GOPATH && chmod -R 777 $GOPATH
|
||||
ENV PATH=/usr/local/go/bin:$GOPATH/bin:$PATH
|
||||
|
||||
# Install Go dependencies
|
||||
ENV GO111MODULE=on
|
||||
RUN go get mvdan.cc/sh/v3/cmd/shfmt
|
||||
RUN go get github.com/goreleaser/nfpm/cmd/nfpm
|
||||
@@ -35,9 +35,14 @@ RUN ARCH="$(dpkg --print-architecture)" && \
|
||||
printf "user: coder\ngroup: coder\n" > /etc/fixuid/config.yml
|
||||
|
||||
COPY release-packages/code-server*.deb /tmp/
|
||||
COPY ci/release-image/entrypoint.sh /usr/bin/entrypoint.sh
|
||||
RUN dpkg -i /tmp/code-server*$(dpkg --print-architecture).deb && rm /tmp/code-server*.deb
|
||||
|
||||
EXPOSE 8080
|
||||
USER coder
|
||||
# This way, if someone sets $DOCKER_USER, docker-exec will still work as
|
||||
# the uid will remain the same. note: only relevant if -u isn't passed to
|
||||
# docker-run.
|
||||
USER 1000
|
||||
ENV USER=coder
|
||||
WORKDIR /home/coder
|
||||
ENTRYPOINT ["dumb-init", "fixuid", "-q", "/usr/bin/code-server", "--bind-addr", "0.0.0.0:8080", "."]
|
||||
ENTRYPOINT ["/usr/bin/entrypoint.sh", "--bind-addr", "0.0.0.0:8080", "."]
|
||||
|
||||
20
ci/release-image/entrypoint.sh
Executable file
20
ci/release-image/entrypoint.sh
Executable file
@@ -0,0 +1,20 @@
|
||||
#!/bin/sh
|
||||
set -eu
|
||||
|
||||
# We do this first to ensure sudo works below when renaming the user.
|
||||
# Otherwise the current container UID may not exist in the passwd database.
|
||||
eval "$(fixuid -q)"
|
||||
|
||||
if [ "${DOCKER_USER-}" ]; then
|
||||
echo "$DOCKER_USER ALL=(ALL) NOPASSWD:ALL" | sudo tee -a /etc/sudoers.d/nopasswd > /dev/null
|
||||
# Unfortunately we cannot change $HOME as we cannot move any bind mounts
|
||||
# nor can we bind mount $HOME into a new home as that requires a privileged container.
|
||||
sudo usermod --login "$DOCKER_USER" coder
|
||||
sudo groupmod -n "$DOCKER_USER" coder
|
||||
|
||||
USER="$DOCKER_USER"
|
||||
|
||||
sudo sed -i "/coder/d" /etc/sudoers.d/nopasswd
|
||||
fi
|
||||
|
||||
dumb-init /usr/bin/code-server "$@"
|
||||
@@ -4,10 +4,11 @@ set -euo pipefail
|
||||
main() {
|
||||
cd "$(dirname "$0")/../.."
|
||||
|
||||
if [[ $OSTYPE == darwin* ]]; then
|
||||
curl -L https://nodejs.org/dist/v14.4.0/node-v14.4.0-darwin-x64.tar.gz | tar -xz
|
||||
PATH="$PATH:node-v14.4.0-darwin-x64/bin"
|
||||
fi
|
||||
NODE_VERSION=v12.18.4
|
||||
NODE_OS="$(uname | tr '[:upper:]' '[:lower:]')"
|
||||
NODE_ARCH="$(uname -m | sed 's/86_64/64/; s/aarch64/arm64/')"
|
||||
curl -L "https://nodejs.org/dist/$NODE_VERSION/node-$NODE_VERSION-$NODE_OS-$NODE_ARCH.tar.gz" | tar -xz
|
||||
PATH="$PWD/node-$NODE_VERSION-$NODE_OS-$NODE_ARCH/bin:$PATH"
|
||||
|
||||
# https://github.com/actions/upload-artifact/issues/38
|
||||
tar -xzf release-npm-package/package.tar.gz
|
||||
|
||||
@@ -2,26 +2,40 @@
|
||||
<!-- DON'T EDIT THIS SECTION, INSTEAD RE-RUN doctoc TO UPDATE -->
|
||||
# Contributing
|
||||
|
||||
- [Pull Requests](#pull-requests)
|
||||
- [Requirements](#requirements)
|
||||
- [Development Workflow](#development-workflow)
|
||||
- [Build](#build)
|
||||
- [Structure](#structure)
|
||||
- [VS Code Patch](#vs-code-patch)
|
||||
- [Currently Known Issues](#currently-known-issues)
|
||||
|
||||
<!-- END doctoc generated TOC please keep comment here to allow auto update -->
|
||||
|
||||
- [Detailed CI and build process docs](../ci)
|
||||
|
||||
## Pull Requests
|
||||
|
||||
Please create a [GitHub Issue](https://github.com/cdr/code-server/issues) for each issue
|
||||
you'd like to address unless the proposed fix is minor.
|
||||
|
||||
In your Pull Requests (PR), link to the issue that the PR solves.
|
||||
|
||||
Please ensure that the base of your PR is the **master** branch. (Note: The default
|
||||
GitHub branch is the latest release branch, though you should point all of your changes to be merged into
|
||||
master).
|
||||
|
||||
## Requirements
|
||||
|
||||
Please refer to [VS Code's prerequisites](https://github.com/Microsoft/vscode/wiki/How-to-Contribute#prerequisites).
|
||||
The prerequisites for contributing to code-server are almost the same as those for
|
||||
[VS Code](https://github.com/Microsoft/vscode/wiki/How-to-Contribute#prerequisites).
|
||||
There are several differences, however. You must:
|
||||
|
||||
Differences:
|
||||
- Use Node.js version 12.x (or greater)
|
||||
- Have [nfpm](https://github.com/goreleaser/nfpm) (which is used to build `.deb` and `.rpm` packages and [jq](https://stedolan.github.io/jq/) (used to build code-server releases) installed
|
||||
|
||||
- We require a minimum of node v12 but later versions should work.
|
||||
- We use [fnpm](https://github.com/goreleaser/nfpm) to build `.deb` and `.rpm` packages.
|
||||
- We use [jq](https://stedolan.github.io/jq/) to build code-server releases.
|
||||
- The [CI container](../ci/images/debian8/Dockerfile) is a useful reference for all our dependencies.
|
||||
The [CI container](../ci/images/debian8/Dockerfile) is a useful reference for all
|
||||
of the dependencies code-server uses.
|
||||
|
||||
## Development Workflow
|
||||
|
||||
@@ -29,100 +43,120 @@ Differences:
|
||||
yarn
|
||||
yarn vscode
|
||||
yarn watch
|
||||
# Visit http://localhost:8080 once the build completed.
|
||||
# Visit http://localhost:8080 once the build is completed.
|
||||
```
|
||||
|
||||
To develop inside of an isolated docker container:
|
||||
To develop inside an isolated Docker container:
|
||||
|
||||
```shell
|
||||
./ci/dev/image/exec.sh
|
||||
|
||||
root@12345:/code-server# yarn
|
||||
root@12345:/code-server# yarn vscode
|
||||
root@12345:/code-server# yarn watch
|
||||
./ci/dev/image/run.sh yarn
|
||||
./ci/dev/image/run.sh yarn vscode
|
||||
./ci/dev/image/run.sh yarn watch
|
||||
```
|
||||
|
||||
Any changes made to the source will be live reloaded.
|
||||
`yarn watch` will live reload changes to the source.
|
||||
|
||||
If changes are made to the patch and you've built previously you must manually
|
||||
reset VS Code then run `yarn vscode:patch`.
|
||||
If you introduce changes to the patch and you've previously built, you
|
||||
must (1) manually reset VS Code and (2) run `yarn vscode:patch`.
|
||||
|
||||
## Build
|
||||
|
||||
You can build using:
|
||||
|
||||
```shell
|
||||
./ci/dev/image/run.sh ./ci/steps/release.sh
|
||||
```
|
||||
|
||||
Run your build with:
|
||||
|
||||
```shell
|
||||
cd release
|
||||
yarn --production
|
||||
# Runs the built JavaScript with Node.
|
||||
node .
|
||||
```
|
||||
|
||||
Build the release packages (make sure that you run `./ci/steps/release.sh` first):
|
||||
|
||||
```shell
|
||||
IMAGE=centos7 ./ci/dev/image/run.sh ./ci/steps/release-packages.sh
|
||||
# The standalone release is in ./release-standalone
|
||||
# .deb, .rpm and the standalone archive are in ./release-packages
|
||||
```
|
||||
|
||||
The `release.sh` script is equal to running:
|
||||
|
||||
```shell
|
||||
yarn
|
||||
yarn vscode
|
||||
yarn build
|
||||
yarn build:vscode
|
||||
yarn release
|
||||
cd release
|
||||
yarn --production
|
||||
# Runs the built JavaScript with Node.
|
||||
node .
|
||||
```
|
||||
|
||||
Now you can build release packages with:
|
||||
And `release-packages.sh` is equal to:
|
||||
|
||||
```
|
||||
```shell
|
||||
yarn release:standalone
|
||||
# The standalone release is in ./release-standalone
|
||||
yarn test:standalone-release
|
||||
yarn package
|
||||
# .deb, .rpm and the standalone archive are in ./release-packages
|
||||
```
|
||||
|
||||
For a faster release build, you can run instead:
|
||||
|
||||
```shell
|
||||
KEEP_MODULES=1 ./ci/steps/release.sh
|
||||
node ./release
|
||||
```
|
||||
|
||||
## Structure
|
||||
|
||||
The `code-server` script serves an HTTP API to login and start a remote VS Code process.
|
||||
The `code-server` script serves an HTTP API for login and starting a remote VS Code process.
|
||||
|
||||
The CLI code is in [./src/node](./src/node) and the HTTP routes are implemented in
|
||||
[./src/node/app](./src/node/app).
|
||||
|
||||
Most of the meaty parts are in our VS Code patch which is described next.
|
||||
Most of the meaty parts are in the VS Code patch, which we described next.
|
||||
|
||||
### VS Code Patch
|
||||
|
||||
Back in v1 of code-server, we had an extensive patch of VS Code that split the codebase
|
||||
into a frontend and server. The frontend consisted of all UI code and the server ran
|
||||
the extensions and exposed an API to the frontend for file access and everything else
|
||||
that the UI needed.
|
||||
In v1 of code-server, we had a patch of VS Code that split the codebase into a front-end
|
||||
and a server. The front-end consisted of all UI code, while the server ran the extensions
|
||||
and exposed an API to the front-end for file access and all UI needs.
|
||||
|
||||
This worked but eventually Microsoft added support to VS Code to run it in the web.
|
||||
They have open sourced the frontend but have kept the server closed source.
|
||||
|
||||
So in interest of piggy backing off their work, v2 and beyond use the VS Code
|
||||
web frontend and fill in the server. This is contained in our
|
||||
Over time, Microsoft added support to VS Code to run it on the web. They have made
|
||||
the front-end open source, but not the server. As such, code-server v2 (and later) uses
|
||||
the VS Code front-end and implements the server. You can find this in
|
||||
[./ci/dev/vscode.patch](../ci/dev/vscode.patch) under the path `src/vs/server`.
|
||||
|
||||
Other notable changes in our patch include:
|
||||
|
||||
- Add our own build file which includes our code and VS Code's web code.
|
||||
- Allow multiple extension directories (both user and built-in).
|
||||
- Modify the loader, websocket, webview, service worker, and asset requests to
|
||||
use the URL of the page as a base (and TLS if necessary for the websocket).
|
||||
- Send client-side telemetry through the server.
|
||||
- Allow modification of the display language.
|
||||
- Make it possible for us to load code on the client.
|
||||
- Make extensions work in the browser.
|
||||
- Make it possible to install extensions of any kind.
|
||||
- Fix getting permanently disconnected when you sleep or hibernate for a while.
|
||||
- Add connection type to web socket query parameters.
|
||||
- Adding our build file, which includes our code and VS Code's web code
|
||||
- Allowing multiple extension directories (both user and built-in)
|
||||
- Modifying the loader, websocket, webview, service worker, and asset requests to
|
||||
use the URL of the page as a base (and TLS, if necessary for the websocket)
|
||||
- Sending client-side telemetry through the server
|
||||
- Allowing modification of the display language
|
||||
- Making it possible for us to load code on the client
|
||||
- Making extensions work in the browser
|
||||
- Making it possible to install extensions of any kind
|
||||
- Fixing issue with getting disconnected when your machine sleeps or hibernates
|
||||
- Adding connection type to web socket query parameters
|
||||
|
||||
Some known issues presently:
|
||||
|
||||
- Creating custom VS Code extensions and debugging them doesn't work.
|
||||
- Extension profiling and tips are currently disabled.
|
||||
|
||||
As the web portion of VS Code matures, we'll be able to shrink and maybe even entirely
|
||||
eliminate our patch. In the meantime, however, upgrading the VS Code version requires
|
||||
ensuring that the patch still applies and has the intended effects.
|
||||
|
||||
To generate a new patch run `yarn vscode:diff`.
|
||||
|
||||
**note**: We have extension docs on the CI and build system at [./ci/README.md](../ci/README.md)
|
||||
|
||||
If functionality doesn't depend on code from VS Code then it should be moved
|
||||
into code-server otherwise it should be in the patch.
|
||||
|
||||
In the future we'd like to run VS Code unit tests against our builds to ensure features
|
||||
As the web portion of VS Code matures, we'll be able to shrink and possibly
|
||||
eliminate our patch. In the meantime, upgrading the VS Code version requires
|
||||
us to ensure that the patch is applied and works as intended. In the future,
|
||||
we'd like to run VS Code unit tests against our builds to ensure that features
|
||||
work as expected.
|
||||
|
||||
To generate a new patch, run `yarn vscode:diff`
|
||||
|
||||
**Note**: We have [extension docs](../ci/README.md) on the CI and build system.
|
||||
|
||||
If the functionality you're working on does NOT depend on code from VS Code, please
|
||||
move it out and into code-server.
|
||||
|
||||
### Currently Known Issues
|
||||
|
||||
- Creating custom VS Code extensions and debugging them doesn't work
|
||||
- Extension profiling and tips are currently disabled
|
||||
|
||||
68
doc/FAQ.md
68
doc/FAQ.md
@@ -6,6 +6,7 @@
|
||||
- [How can I reuse my VS Code configuration?](#how-can-i-reuse-my-vs-code-configuration)
|
||||
- [Differences compared to VS Code?](#differences-compared-to-vs-code)
|
||||
- [How can I request a missing extension?](#how-can-i-request-a-missing-extension)
|
||||
- [How do I configure the marketplace URL?](#how-do-i-configure-the-marketplace-url)
|
||||
- [Where are extensions stored?](#where-are-extensions-stored)
|
||||
- [How is this different from VS Code Codespaces?](#how-is-this-different-from-vs-code-codespaces)
|
||||
- [How should I expose code-server to the internet?](#how-should-i-expose-code-server-to-the-internet)
|
||||
@@ -18,25 +19,25 @@
|
||||
- [How does code-server decide what workspace or folder to open?](#how-does-code-server-decide-what-workspace-or-folder-to-open)
|
||||
- [How do I debug issues with code-server?](#how-do-i-debug-issues-with-code-server)
|
||||
- [Heartbeat File](#heartbeat-file)
|
||||
- [Healthz endpoint](#healthz-endpoint)
|
||||
- [How does the config file work?](#how-does-the-config-file-work)
|
||||
- [Blank screen on iPad?](#blank-screen-on-ipad)
|
||||
- [Isn't an install script piped into sh insecure?](#isnt-an-install-script-piped-into-sh-insecure)
|
||||
- [How do I make my keyboard shortcuts work?](#how-do-i-make-my-keyboard-shortcuts-work)
|
||||
- [Differences compared to Theia?](#differences-compared-to-theia)
|
||||
- [Enterprise](#enterprise)
|
||||
|
||||
<!-- END doctoc generated TOC please keep comment here to allow auto update -->
|
||||
|
||||
## Questions?
|
||||
|
||||
Please file all questions and support requests at https://www.reddit.com/r/codeserver/.
|
||||
|
||||
The issue tracker is **only** for bugs and features.
|
||||
Please file all questions and support requests at https://github.com/cdr/code-server/discussions.
|
||||
|
||||
## How can I reuse my VS Code configuration?
|
||||
|
||||
The very popular [Settings Sync](https://marketplace.visualstudio.com/items?itemName=Shan.code-settings-sync) extension works.
|
||||
|
||||
You can also pass `--data-dir ~/.vscode` to reuse your existing VS Code extensions and configuration.
|
||||
You can also pass `--user-data-dir ~/.vscode` to reuse your existing VS Code extensions and configuration.
|
||||
|
||||
Or copy `~/.vscode` into `~/.local/share/code-server`.
|
||||
|
||||
@@ -78,8 +79,26 @@ point to the .vsix file.
|
||||
|
||||
See below for installing an extension from the cli.
|
||||
|
||||
If you have your own custom marketplace, it is possible to point code-server to it by setting
|
||||
`$SERVICE_URL` and `$ITEM_URL` to point to it.
|
||||
## How do I configure the marketplace URL?
|
||||
|
||||
If you have your own marketplace that implements the VS Code Extension Gallery API, it is possible to
|
||||
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`.
|
||||
|
||||
e.g. to use [open-vsx.org](https://open-vsx.org):
|
||||
|
||||
```bash
|
||||
export SERVICE_URL=https://open-vsx.org/vscode/gallery
|
||||
export ITEM_URL=https://open-vsx.org/vscode/item
|
||||
```
|
||||
|
||||
While you can technically use Microsoft's marketplace with these, please do not do so as it
|
||||
is against their terms of use. See [above](#differences-compared-to-vs-code) and this
|
||||
discussion regarding the use of the Microsoft URLs in forks:
|
||||
|
||||
https://github.com/microsoft/vscode/issues/31168#issue-244533026
|
||||
|
||||
These variables are most valuable to our enterprise customers for whom we have a self hosted marketplace product.
|
||||
|
||||
## Where are extensions stored?
|
||||
|
||||
@@ -103,11 +122,11 @@ code-server --install-extension downloaded-ms-python.python.vsix
|
||||
VS Code Codespaces is a closed source and paid service by Microsoft. It also allows you to access
|
||||
VS Code via the browser.
|
||||
|
||||
However, code-server is free, open source and can be ran on any machine without any limitations.
|
||||
However, code-server is free, open source and can be run on any machine without any limitations.
|
||||
|
||||
While you can self host environments with VS Code Codespaces, you still need to an Azure billing
|
||||
account and you access VS Code via the Codespaces web dashboard instead of directly connecting to
|
||||
your instance.
|
||||
While you can self host environments with VS Code Codespaces, you still need an Azure billing
|
||||
account and you have to access VS Code via the Codespaces web dashboard instead of directly
|
||||
connecting to your instance.
|
||||
|
||||
## How should I expose code-server to the internet?
|
||||
|
||||
@@ -224,6 +243,20 @@ older than X minutes, kill `code-server`.
|
||||
|
||||
[#1636](https://github.com/cdr/code-server/issues/1636) will make the experience here better.
|
||||
|
||||
## Healthz endpoint
|
||||
|
||||
`code-server` exposes an endpoint at `/healthz` which can be used to check
|
||||
whether `code-server` is up without triggering a heartbeat. The response will
|
||||
include a status (`alive` or `expired`) and a timestamp for the last heartbeat
|
||||
(defaults to `0`). This endpoint does not require authentication.
|
||||
|
||||
```json
|
||||
{
|
||||
"status": "alive",
|
||||
"lastHeartbeat": 1599166210566
|
||||
}
|
||||
```
|
||||
|
||||
## How does the config file work?
|
||||
|
||||
When `code-server` starts up, it creates a default config file in `~/.config/code-server/config.yaml` that looks
|
||||
@@ -250,7 +283,7 @@ The default location also respects `$XDG_CONFIG_HOME`.
|
||||
|
||||
Unfortunately at the moment self signed certificates cause a blank screen on iPadOS
|
||||
|
||||
There does seem to a way to get it to work if you create your own CA and create a
|
||||
There does seem to be a way to get it to work if you create your own CA and create a
|
||||
certificate using the CA and then import the CA onto your iPad.
|
||||
|
||||
See [#1566](https://github.com/cdr/code-server/issues/1566#issuecomment-623159434).
|
||||
@@ -272,6 +305,19 @@ This will install a Chrome PWA and now all keybindings will work!
|
||||
|
||||
For other browsers you'll have to remap keybindings unfortunately.
|
||||
|
||||
## Differences compared to Theia?
|
||||
|
||||
[Theia](https://github.com/eclipse-theia/theia) is a browser IDE loosely based on VS Code. It uses the same
|
||||
text editor library named [Monaco](https://github.com/Microsoft/monaco-editor) and the same
|
||||
extension API but everything else is very different. It also uses [open-vsx.org](https://open-vsx.org)
|
||||
for extensions which has an order of magnitude less extensions than our marketplace.
|
||||
See [#1473](https://github.com/cdr/code-server/issues/1473).
|
||||
|
||||
You can't just use your VS Code config in Theia like you can with code-server.
|
||||
|
||||
To summarize, code-server is a patched fork of VS Code to run in the browser whereas
|
||||
Theia takes some parts of VS Code but is an entirely different editor.
|
||||
|
||||
## Enterprise
|
||||
|
||||
Visit [our enterprise page](https://coder.com) for more information about our
|
||||
|
||||
59
doc/guide.md
59
doc/guide.md
@@ -9,6 +9,7 @@
|
||||
- [3. Expose code-server](#3-expose-code-server)
|
||||
- [SSH forwarding](#ssh-forwarding)
|
||||
- [Let's Encrypt](#lets-encrypt)
|
||||
- [NGINX](#nginx)
|
||||
- [Self Signed Certificate](#self-signed-certificate)
|
||||
- [Change the password?](#change-the-password)
|
||||
- [How do I securely access development web services?](#how-do-i-securely-access-development-web-services)
|
||||
@@ -25,6 +26,8 @@ Further docs are at:
|
||||
- [FAQ](./FAQ.md) for common questions.
|
||||
- [CONTRIBUTING](../doc/CONTRIBUTING.md) for development docs
|
||||
|
||||
We highly recommend reading the [FAQ](./FAQ.md) on the [Differences compared to VS Code](./FAQ.md#differences-compared-to-vs-code) before beginning.
|
||||
|
||||
We'll walk you through acquiring a remote machine to run `code-server` on
|
||||
and then exposing `code-server` so you can securely access it.
|
||||
|
||||
@@ -78,7 +81,7 @@ to avoid the slow dashboard.
|
||||
|
||||
## 2. Install code-server
|
||||
|
||||
We have a [script](../install.sh) to install `code-server` for Linux and macOS.
|
||||
We have a [script](../install.sh) to install `code-server` for Linux, macOS and FreeBSD.
|
||||
|
||||
It tries to use the system package manager if possible.
|
||||
|
||||
@@ -128,16 +131,16 @@ sed -i.bak 's/auth: password/auth: none/' ~/.config/code-server/config.yaml
|
||||
Restart `code-server` with (assuming you followed the guide):
|
||||
|
||||
```bash
|
||||
systemctl --user restart code-server
|
||||
sudo systemctl restart code-server@$USER
|
||||
```
|
||||
|
||||
Now forward local port 8080 to `127.0.0.1:8080` on the remote instance.
|
||||
Now forward local port 8080 to `127.0.0.1:8080` on the remote instance by running the following command on your local machine.
|
||||
|
||||
Recommended reading: https://help.ubuntu.com/community/SSH/OpenSSH/PortForwarding.
|
||||
|
||||
```bash
|
||||
# -N disables executing a remote shell
|
||||
ssh -N -L 8080:127.0.0.1:8080 <instance-ip>
|
||||
ssh -N -L 8080:127.0.0.1:8080 [user]@<instance-ip>
|
||||
```
|
||||
|
||||
Now if you access http://127.0.0.1:8080 locally, you should see `code-server`!
|
||||
@@ -191,6 +194,8 @@ mydomain.com
|
||||
reverse_proxy 127.0.0.1:8080
|
||||
```
|
||||
|
||||
Remember to replace `mydomain.com` with your domain name!
|
||||
|
||||
5. Reload caddy with:
|
||||
|
||||
```bash
|
||||
@@ -202,6 +207,48 @@ Visit `https://<your-domain-name>` to access `code-server`. Congratulations!
|
||||
In a future release we plan to integrate Let's Encrypt directly with `code-server` to avoid
|
||||
the dependency on caddy.
|
||||
|
||||
#### NGINX
|
||||
|
||||
If you prefer to use NGINX instead of Caddy then please follow steps 1-2 above and then:
|
||||
|
||||
3. Install `nginx`:
|
||||
|
||||
```bash
|
||||
sudo apt update
|
||||
sudo apt install -y nginx certbot python-certbot-nginx
|
||||
```
|
||||
|
||||
4. Put the following config into `/etc/nginx/sites-available/code-server` with sudo:
|
||||
|
||||
```nginx
|
||||
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;
|
||||
}
|
||||
}
|
||||
```
|
||||
|
||||
Remember to replace `mydomain.com` with your domain name!
|
||||
|
||||
5. Enable the config:
|
||||
|
||||
```bash
|
||||
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
|
||||
```
|
||||
|
||||
Make sure to substitute `me@example.com` with your actual email.
|
||||
|
||||
Visit `https://<your-domain-name>` to access `code-server`. Congratulations!
|
||||
|
||||
### Self Signed Certificate
|
||||
|
||||
**note:** Self signed certificates do not work with iPad and will cause a blank page. You'll
|
||||
@@ -230,7 +277,7 @@ sudo setcap cap_net_bind_service=+ep /usr/lib/code-server/lib/node
|
||||
Assuming you have been following the guide, restart `code-server` with:
|
||||
|
||||
```bash
|
||||
systemctl --user restart code-server
|
||||
sudo systemctl restart code-server@$USER
|
||||
```
|
||||
|
||||
Edit your instance and checkmark the allow HTTPS traffic option.
|
||||
@@ -248,7 +295,7 @@ Edit the `password` field in the `code-server` config file at `~/.config/code-se
|
||||
and then restart `code-server` with:
|
||||
|
||||
```bash
|
||||
systemctl --user restart code-server
|
||||
sudo systemctl restart code-server@$USER
|
||||
```
|
||||
|
||||
### How do I securely access development web services?
|
||||
|
||||
@@ -4,7 +4,7 @@
|
||||
|
||||
- [install.sh](#installsh)
|
||||
- [Flags](#flags)
|
||||
- [Detect Reference](#detect-reference)
|
||||
- [Detection Reference](#detection-reference)
|
||||
- [Debian, Ubuntu](#debian-ubuntu)
|
||||
- [Fedora, CentOS, RHEL, SUSE](#fedora-centos-rhel-suse)
|
||||
- [Arch Linux](#arch-linux)
|
||||
@@ -20,7 +20,7 @@ various distros and operating systems.
|
||||
|
||||
## install.sh
|
||||
|
||||
We have a [script](../install.sh) to install code-server for Linux and macOS.
|
||||
We have a [script](../install.sh) to install code-server for Linux, macOS and FreeBSD.
|
||||
|
||||
It tries to use the system package manager if possible.
|
||||
|
||||
@@ -42,7 +42,7 @@ If you believe an install script used with `curl | sh` is insecure, please give
|
||||
[this wonderful blogpost](https://sandstorm.io/news/2015-09-24-is-curl-bash-insecure-pgp-verified-install) by
|
||||
[sandstorm.io](https://sandstorm.io) a read.
|
||||
|
||||
If you'd still prefer manual installation despite the below [detect reference](#detect-reference) and `--dry-run`
|
||||
If you'd still prefer manual installation despite the below [detection reference](#detection-reference) and `--dry-run`
|
||||
then continue on for docs on manual installation. The [`install.sh`](../install.sh) script runs the _exact_ same
|
||||
commands presented in the rest of this document.
|
||||
|
||||
@@ -56,7 +56,7 @@ commands presented in the rest of this document.
|
||||
- `--version=X.X.X` to install version `X.X.X` instead of latest.
|
||||
- `--help` to see full usage docs.
|
||||
|
||||
### Detect Reference
|
||||
### Detection Reference
|
||||
|
||||
- For Debian, Ubuntu and Raspbian it will install the latest deb package.
|
||||
- For Fedora, CentOS, RHEL and openSUSE it will install the latest rpm package.
|
||||
@@ -70,6 +70,8 @@ commands presented in the rest of this document.
|
||||
- If Homebrew is not installed it will install the latest standalone release into `~/.local`.
|
||||
- Add `~/.local/bin` to your `$PATH` to run code-server.
|
||||
|
||||
- For FreeBSD, it will install the [npm package](#yarn-npm) with `yarn` or `npm`.
|
||||
|
||||
- If ran on an architecture with no releases, it will install the [npm package](#yarn-npm) with `yarn` or `npm`.
|
||||
- We only have releases for amd64 and arm64 presently.
|
||||
- The [npm package](#yarn-npm) builds the native modules on postinstall.
|
||||
@@ -77,18 +79,18 @@ commands presented in the rest of this document.
|
||||
## Debian, Ubuntu
|
||||
|
||||
```bash
|
||||
curl -fOL https://github.com/cdr/code-server/releases/download/v3.4.1/code-server_3.4.1_amd64.deb
|
||||
sudo dpkg -i code-server_3.4.1_amd64.deb
|
||||
systemctl --user enable --now code-server
|
||||
curl -fOL https://github.com/cdr/code-server/releases/download/v3.6.1/code-server_3.6.1_amd64.deb
|
||||
sudo dpkg -i code-server_3.6.1_amd64.deb
|
||||
sudo systemctl enable --now code-server@$USER
|
||||
# Now visit http://127.0.0.1:8080. Your password is in ~/.config/code-server/config.yaml
|
||||
```
|
||||
|
||||
## Fedora, CentOS, RHEL, SUSE
|
||||
|
||||
```bash
|
||||
curl -fOL https://github.com/cdr/code-server/releases/download/v3.4.1/code-server-3.4.1-amd64.rpm
|
||||
sudo rpm -i code-server-3.4.1-amd64.rpm
|
||||
systemctl --user enable --now code-server
|
||||
curl -fOL https://github.com/cdr/code-server/releases/download/v3.6.1/code-server-3.6.1-amd64.rpm
|
||||
sudo rpm -i code-server-3.6.1-amd64.rpm
|
||||
sudo systemctl enable --now code-server@$USER
|
||||
# Now visit http://127.0.0.1:8080. Your password is in ~/.config/code-server/config.yaml
|
||||
```
|
||||
|
||||
@@ -97,7 +99,7 @@ systemctl --user enable --now code-server
|
||||
```bash
|
||||
# Installs code-server from the AUR using yay.
|
||||
yay -S code-server
|
||||
systemctl --user enable --now code-server
|
||||
sudo systemctl enable --now code-server@$USER
|
||||
# Now visit http://127.0.0.1:8080. Your password is in ~/.config/code-server/config.yaml
|
||||
```
|
||||
|
||||
@@ -106,7 +108,7 @@ systemctl --user enable --now code-server
|
||||
git clone https://aur.archlinux.org/code-server.git
|
||||
cd code-server
|
||||
makepkg -si
|
||||
systemctl --user enable --now code-server
|
||||
sudo systemctl enable --now code-server@$USER
|
||||
# Now visit http://127.0.0.1:8080. Your password is in ~/.config/code-server/config.yaml
|
||||
```
|
||||
|
||||
@@ -156,10 +158,10 @@ Here is an example script for installing and using a standalone `code-server` re
|
||||
|
||||
```bash
|
||||
mkdir -p ~/.local/lib ~/.local/bin
|
||||
curl -fL https://github.com/cdr/code-server/releases/download/v3.4.1/code-server-3.4.1-linux-amd64.tar.gz \
|
||||
curl -fL https://github.com/cdr/code-server/releases/download/v3.6.1/code-server-3.6.1-linux-amd64.tar.gz \
|
||||
| tar -C ~/.local/lib -xz
|
||||
mv ~/.local/lib/code-server-3.4.1-linux-amd64 ~/.local/lib/code-server-3.4.1
|
||||
ln -s ~/.local/lib/code-server-3.4.1/bin/code-server ~/.local/bin/code-server
|
||||
mv ~/.local/lib/code-server-3.6.1-linux-amd64 ~/.local/lib/code-server-3.6.1
|
||||
ln -s ~/.local/lib/code-server-3.6.1/bin/code-server ~/.local/bin/code-server
|
||||
PATH="~/.local/bin:$PATH"
|
||||
code-server
|
||||
# Now visit http://127.0.0.1:8080. Your password is in ~/.config/code-server/config.yaml
|
||||
@@ -172,9 +174,16 @@ code-server
|
||||
# It will also mount your current directory into the container as `/home/coder/project`
|
||||
# and forward your UID/GID so that all file system operations occur as your user outside
|
||||
# the container.
|
||||
docker run -it -p 127.0.0.1:8080:8080 \
|
||||
#
|
||||
# Your $HOME/.config is mounted at $HOME/.config within the container to ensure you can
|
||||
# easily access/modify your code-server config in $HOME/.config/code-server/config.json
|
||||
# outside the container.
|
||||
mkdir -p ~/.config
|
||||
docker run -it --name code-server -p 127.0.0.1:8080:8080 \
|
||||
-v "$HOME/.config:/home/coder/.config" \
|
||||
-v "$PWD:/home/coder/project" \
|
||||
-u "$(id -u):$(id -g)" \
|
||||
-e "DOCKER_USER=$USER" \
|
||||
codercom/code-server:latest
|
||||
```
|
||||
|
||||
|
||||
@@ -21,7 +21,9 @@ sudo apt-get install -y \
|
||||
pkg-config \
|
||||
libx11-dev \
|
||||
libxkbfile-dev \
|
||||
libsecret-1-dev
|
||||
libsecret-1-dev \
|
||||
python3
|
||||
npm config set python python3
|
||||
```
|
||||
|
||||
## Fedora, CentOS, RHEL
|
||||
|
||||
98
install.sh
98
install.sh
@@ -14,30 +14,40 @@ usage() {
|
||||
fi
|
||||
|
||||
cath << EOF
|
||||
Installs code-server for Linux and macOS.
|
||||
Installs code-server for Linux, macOS and FreeBSD.
|
||||
It tries to use the system package manager if possible.
|
||||
After successful installation it explains how to start using code-server.
|
||||
|
||||
Pass in user@host to install code-server on user@host over ssh.
|
||||
The remote host must have internet access.
|
||||
${not_curl_usage-}
|
||||
Usage:
|
||||
|
||||
$arg0 [--dry-run] [--version X.X.X] [--method detect] [--prefix ~/.local]
|
||||
$arg0 [--dry-run] [--version X.X.X] [--method detect] \
|
||||
[--prefix ~/.local] [--rsh ssh] [user@host]
|
||||
|
||||
--dry-run
|
||||
Echo the commands for the install process without running them.
|
||||
|
||||
--version X.X.X
|
||||
Install a specific version instead of the latest.
|
||||
|
||||
--method [detect | standalone]
|
||||
Choose the installation method. Defaults to detect.
|
||||
- detect detects the system package manager and tries to use it.
|
||||
Full reference on the process is further below.
|
||||
- standalone installs a standalone release archive into ~/.local
|
||||
Add ~/.local/bin to your \$PATH to use it.
|
||||
|
||||
--prefix <dir>
|
||||
Sets the prefix used by standalone release archives. Defaults to ~/.local
|
||||
The release is unarchived into ~/.local/lib/code-server-X.X.X
|
||||
and the binary symlinked into ~/.local/bin/code-server
|
||||
To install system wide pass ---prefix=/usr/local
|
||||
|
||||
--rsh <bin>
|
||||
Specifies the remote shell for remote installation. Defaults to ssh.
|
||||
|
||||
- For Debian, Ubuntu and Raspbian it will install the latest deb package.
|
||||
- For Fedora, CentOS, RHEL and openSUSE it will install the latest rpm package.
|
||||
- For Arch Linux it will install the AUR package.
|
||||
@@ -48,6 +58,8 @@ Usage:
|
||||
- If Homebrew is not installed it will install the latest standalone release
|
||||
into ~/.local
|
||||
|
||||
- For FreeBSD, it will install the npm package with yarn or npm.
|
||||
|
||||
- If ran on an architecture with no releases, it will install the
|
||||
npm package with yarn or npm.
|
||||
- We only have releases for amd64 and arm64 presently.
|
||||
@@ -82,7 +94,7 @@ echo_systemd_postinstall() {
|
||||
echoh
|
||||
cath << EOF
|
||||
To have systemd start code-server now and restart on boot:
|
||||
systemctl --user enable --now code-server
|
||||
sudo systemctl enable --now code-server@\$USER
|
||||
Or, if you don't want/need a background service you can run:
|
||||
code-server
|
||||
EOF
|
||||
@@ -98,9 +110,19 @@ main() {
|
||||
METHOD \
|
||||
STANDALONE_INSTALL_PREFIX \
|
||||
VERSION \
|
||||
OPTIONAL
|
||||
OPTIONAL \
|
||||
ALL_FLAGS \
|
||||
RSH_ARGS \
|
||||
RSH
|
||||
|
||||
ALL_FLAGS=""
|
||||
while [ "$#" -gt 0 ]; do
|
||||
case "$1" in
|
||||
-*)
|
||||
ALL_FLAGS="${ALL_FLAGS} $1"
|
||||
;;
|
||||
esac
|
||||
|
||||
case "$1" in
|
||||
--dry-run)
|
||||
DRY_RUN=1
|
||||
@@ -126,20 +148,45 @@ main() {
|
||||
--version=*)
|
||||
VERSION="$(parse_arg "$@")"
|
||||
;;
|
||||
--rsh)
|
||||
RSH="$(parse_arg "$@")"
|
||||
shift
|
||||
;;
|
||||
--rsh=*)
|
||||
RSH="$(parse_arg "$@")"
|
||||
;;
|
||||
-h | --h | -help | --help)
|
||||
usage
|
||||
exit 0
|
||||
;;
|
||||
*)
|
||||
--)
|
||||
shift
|
||||
# We remove the -- added above.
|
||||
ALL_FLAGS="${ALL_FLAGS% --}"
|
||||
RSH_ARGS="$*"
|
||||
break
|
||||
;;
|
||||
-*)
|
||||
echoerr "Unknown flag $1"
|
||||
echoerr "Run with --help to see usage."
|
||||
exit 1
|
||||
;;
|
||||
*)
|
||||
RSH_ARGS="$*"
|
||||
break
|
||||
;;
|
||||
esac
|
||||
|
||||
shift
|
||||
done
|
||||
|
||||
if [ "${RSH_ARGS-}" ]; then
|
||||
RSH="${RSH-ssh}"
|
||||
echoh "Installing remotely with $RSH $RSH_ARGS"
|
||||
curl -fsSL https://code-server.dev/install.sh | prefix "$RSH_ARGS" "$RSH" "$RSH_ARGS" sh -s -- "$ALL_FLAGS"
|
||||
return
|
||||
fi
|
||||
|
||||
VERSION="${VERSION-$(echo_latest_version)}"
|
||||
METHOD="${METHOD-detect}"
|
||||
if [ "$METHOD" != detect ] && [ "$METHOD" != standalone ]; then
|
||||
@@ -160,7 +207,7 @@ main() {
|
||||
ARCH="$(arch)"
|
||||
if [ ! "$ARCH" ]; then
|
||||
if [ "$METHOD" = standalone ]; then
|
||||
echoerr "No releases available for the architecture $(uname -m)."
|
||||
echoerr "No precompiled releases for $(uname -m)."
|
||||
echoerr 'Please rerun without the "--method standalone" flag to install from npm.'
|
||||
exit 1
|
||||
fi
|
||||
@@ -169,8 +216,18 @@ main() {
|
||||
return
|
||||
fi
|
||||
|
||||
if [ "$OS" = "freebsd" ]; then
|
||||
if [ "$METHOD" = standalone ]; then
|
||||
echoerr "No precompiled releases available for $OS."
|
||||
echoerr 'Please rerun without the "--method standalone" flag to install from npm.'
|
||||
exit 1
|
||||
fi
|
||||
echoh "No precompiled releases available for $OS."
|
||||
install_npm
|
||||
return
|
||||
fi
|
||||
|
||||
CACHE_DIR="$(echo_cache_dir)"
|
||||
mkdir -p "$CACHE_DIR"
|
||||
|
||||
if [ "$METHOD" = standalone ]; then
|
||||
install_standalone
|
||||
@@ -234,10 +291,11 @@ fetch() {
|
||||
FILE="$2"
|
||||
|
||||
if [ -e "$FILE" ]; then
|
||||
echoh "+ Reusing $CACHE_DIR/${URL##*/}"
|
||||
echoh "+ Reusing $FILE"
|
||||
return
|
||||
fi
|
||||
|
||||
sh_c mkdir -p "$CACHE_DIR"
|
||||
sh_c curl \
|
||||
-#fL \
|
||||
-o "$FILE.incomplete" \
|
||||
@@ -360,6 +418,9 @@ os() {
|
||||
Darwin)
|
||||
echo macos
|
||||
;;
|
||||
FreeBSD)
|
||||
echo freebsd
|
||||
;;
|
||||
esac
|
||||
}
|
||||
|
||||
@@ -371,11 +432,12 @@ os() {
|
||||
# - centos, fedora, rhel, opensuse
|
||||
# - alpine
|
||||
# - arch
|
||||
# - freebsd
|
||||
#
|
||||
# Inspired by https://github.com/docker/docker-install/blob/26ff363bcf3b3f5a00498ac43694bf1c7d9ce16c/install.sh#L111-L120.
|
||||
distro() {
|
||||
if [ "$(uname)" = "Darwin" ]; then
|
||||
echo "macos"
|
||||
if [ "$OS" = "macos" ] || [ "$OS" = "freebsd" ]; then
|
||||
echo "$OS"
|
||||
return
|
||||
fi
|
||||
|
||||
@@ -422,11 +484,14 @@ arch() {
|
||||
x86_64)
|
||||
echo amd64
|
||||
;;
|
||||
amd64) # FreeBSD.
|
||||
echo amd64
|
||||
;;
|
||||
esac
|
||||
}
|
||||
|
||||
command_exists() {
|
||||
command -v "$@" > /dev/null 2>&1
|
||||
command -v "$@" > /dev/null
|
||||
}
|
||||
|
||||
sh_c() {
|
||||
@@ -480,4 +545,15 @@ humanpath() {
|
||||
sed "s# $HOME# ~#g; s#\"$HOME#\"\$HOME#g"
|
||||
}
|
||||
|
||||
# We need to make sure we exit with a non zero exit if the command fails.
|
||||
# /bin/sh does not support -o pipefail unfortunately.
|
||||
prefix() {
|
||||
PREFIX="$1"
|
||||
shift
|
||||
fifo="$(mktemp -d)/fifo"
|
||||
mkfifo "$fifo"
|
||||
sed -e "s#^#$PREFIX: #" "$fifo" &
|
||||
"$@" > "$fifo" 2>&1
|
||||
}
|
||||
|
||||
main "$@"
|
||||
|
||||
Submodule lib/vscode updated: 5763d909d5...93c2f0fbf1
32
package.json
32
package.json
@@ -1,7 +1,7 @@
|
||||
{
|
||||
"name": "code-server",
|
||||
"license": "MIT",
|
||||
"version": "3.4.1",
|
||||
"version": "3.6.2",
|
||||
"description": "Run VS Code on a remote server.",
|
||||
"homepage": "https://github.com/cdr/code-server",
|
||||
"bugs": {
|
||||
@@ -26,37 +26,38 @@
|
||||
"lint": "./ci/dev/lint.sh",
|
||||
"test": "./ci/dev/test.sh",
|
||||
"ci": "./ci/dev/ci.sh",
|
||||
"watch": "NODE_OPTIONS=--max_old_space_size=32384 ts-node ./ci/dev/watch.ts"
|
||||
"watch": "VSCODE_IPC_HOOK_CLI= NODE_OPTIONS=--max_old_space_size=32384 ts-node ./ci/dev/watch.ts"
|
||||
},
|
||||
"main": "out/node/entry.js",
|
||||
"devDependencies": {
|
||||
"@types/fs-extra": "^8.0.1",
|
||||
"@types/http-proxy": "^1.17.4",
|
||||
"@types/js-yaml": "^3.12.3",
|
||||
"@types/mocha": "^5.2.7",
|
||||
"@types/mocha": "^8.0.3",
|
||||
"@types/node": "^12.12.7",
|
||||
"@types/parcel-bundler": "^1.12.1",
|
||||
"@types/pem": "^1.9.5",
|
||||
"@types/safe-compare": "^1.1.0",
|
||||
"@types/semver": "^7.1.0",
|
||||
"@types/tar-fs": "^1.16.2",
|
||||
"@types/tar-stream": "^1.6.1",
|
||||
"@types/ws": "^6.0.4",
|
||||
"@typescript-eslint/eslint-plugin": "^2.0.0",
|
||||
"@typescript-eslint/parser": "^2.0.0",
|
||||
"@types/split2": "^2.1.6",
|
||||
"@types/tar-fs": "^2.0.0",
|
||||
"@types/tar-stream": "^2.1.0",
|
||||
"@types/ws": "^7.2.6",
|
||||
"@typescript-eslint/eslint-plugin": "^3.10.1",
|
||||
"@typescript-eslint/parser": "^3.10.1",
|
||||
"doctoc": "^1.4.0",
|
||||
"eslint": "^6.2.0",
|
||||
"eslint": "^7.7.0",
|
||||
"eslint-config-prettier": "^6.0.0",
|
||||
"eslint-plugin-import": "^2.18.2",
|
||||
"eslint-plugin-prettier": "^3.1.0",
|
||||
"leaked-handles": "^5.2.0",
|
||||
"mocha": "^6.2.0",
|
||||
"mocha": "^8.1.2",
|
||||
"parcel-bundler": "^1.12.4",
|
||||
"prettier": "^2.0.5",
|
||||
"stylelint": "^13.0.0",
|
||||
"stylelint-config-recommended": "^3.0.0",
|
||||
"ts-node": "^8.4.1",
|
||||
"typescript": "3.7.2"
|
||||
"ts-node": "^9.0.0",
|
||||
"typescript": "4.0.2"
|
||||
},
|
||||
"resolutions": {
|
||||
"@types/node": "^12.12.7",
|
||||
@@ -64,16 +65,19 @@
|
||||
"vfile-message": "^2.0.2"
|
||||
},
|
||||
"dependencies": {
|
||||
"@coder/logger": "1.1.11",
|
||||
"@coder/logger": "1.1.16",
|
||||
"env-paths": "^2.2.0",
|
||||
"fs-extra": "^8.1.0",
|
||||
"fs-extra": "^9.0.1",
|
||||
"http-proxy": "^1.18.0",
|
||||
"httpolyglot": "^0.1.2",
|
||||
"js-yaml": "^3.13.1",
|
||||
"limiter": "^1.1.5",
|
||||
"pem": "^1.14.2",
|
||||
"rotating-file-stream": "^2.1.1",
|
||||
"safe-buffer": "^5.1.1",
|
||||
"safe-compare": "^1.1.4",
|
||||
"semver": "^7.1.3",
|
||||
"split2": "^3.2.2",
|
||||
"tar": "^6.0.1",
|
||||
"tar-fs": "^2.0.0",
|
||||
"ws": "^7.2.0",
|
||||
|
||||
@@ -7,32 +7,32 @@
|
||||
"description": "Run editors on a remote server.",
|
||||
"icons": [
|
||||
{
|
||||
"src": "{{BASE}}/static/{{COMMIT}}/src/browser/media/pwa-icon-96.png",
|
||||
"src": "{{CS_STATIC_BASE}}/src/browser/media/pwa-icon-96.png",
|
||||
"type": "image/png",
|
||||
"sizes": "96x96"
|
||||
},
|
||||
{
|
||||
"src": "{{BASE}}/static/{{COMMIT}}/src/browser/media/pwa-icon-128.png",
|
||||
"src": "{{CS_STATIC_BASE}}/src/browser/media/pwa-icon-128.png",
|
||||
"type": "image/png",
|
||||
"sizes": "128x128"
|
||||
},
|
||||
{
|
||||
"src": "{{BASE}}/static/{{COMMIT}}/src/browser/media/pwa-icon-192.png",
|
||||
"src": "{{CS_STATIC_BASE}}/src/browser/media/pwa-icon-192.png",
|
||||
"type": "image/png",
|
||||
"sizes": "192x192"
|
||||
},
|
||||
{
|
||||
"src": "{{BASE}}/static/{{COMMIT}}/src/browser/media/pwa-icon-256.png",
|
||||
"src": "{{CS_STATIC_BASE}}/src/browser/media/pwa-icon-256.png",
|
||||
"type": "image/png",
|
||||
"sizes": "256x256"
|
||||
},
|
||||
{
|
||||
"src": "{{BASE}}/static/{{COMMIT}}/src/browser/media/pwa-icon-384.png",
|
||||
"src": "{{CS_STATIC_BASE}}/src/browser/media/pwa-icon-384.png",
|
||||
"type": "image/png",
|
||||
"sizes": "384x384"
|
||||
},
|
||||
{
|
||||
"src": "{{BASE}}/static/{{COMMIT}}/src/browser/media/pwa-icon-512.png",
|
||||
"src": "{{CS_STATIC_BASE}}/src/browser/media/pwa-icon-512.png",
|
||||
"type": "image/png",
|
||||
"sizes": "512x512"
|
||||
}
|
||||
|
||||
@@ -1,28 +0,0 @@
|
||||
<!DOCTYPE html>
|
||||
<html lang="en">
|
||||
<head>
|
||||
<meta charset="utf-8" />
|
||||
<meta
|
||||
name="viewport"
|
||||
content="width=device-width, initial-scale=1.0, maximum-scale=1.0, minimum-scale=1.0, user-scalable=no"
|
||||
/>
|
||||
<meta
|
||||
http-equiv="Content-Security-Policy"
|
||||
content="style-src 'self' 'unsafe-inline'; manifest-src 'self'; img-src 'self' data:; font-src 'self' data:;"
|
||||
/>
|
||||
<title>code-server</title>
|
||||
<link rel="icon" href="{{BASE}}/static/{{COMMIT}}/src/browser/media/favicon.ico" type="image/x-icon" />
|
||||
<link
|
||||
rel="manifest"
|
||||
href="{{BASE}}/static/{{COMMIT}}/src/browser/media/manifest.json"
|
||||
crossorigin="use-credentials"
|
||||
/>
|
||||
<link rel="apple-touch-icon" href="{{BASE}}/static/{{COMMIT}}/src/browser/media/pwa-icon-384.png" />
|
||||
<link href="{{BASE}}/static/{{COMMIT}}/dist/pages/app.css" rel="stylesheet" />
|
||||
<meta id="coder-options" data-settings="{{OPTIONS}}" />
|
||||
</head>
|
||||
<body>
|
||||
<script data-cfasync="false" src="{{BASE}}/static/{{COMMIT}}/dist/register.js"></script>
|
||||
<script data-cfasync="false" src="{{BASE}}/static/{{COMMIT}}/dist/pages/app.js"></script>
|
||||
</body>
|
||||
</html>
|
||||
@@ -1,37 +0,0 @@
|
||||
import { getOptions, normalize } from "../../common/util"
|
||||
import { ApiEndpoint } from "../../common/http"
|
||||
|
||||
import "./error.css"
|
||||
import "./global.css"
|
||||
import "./home.css"
|
||||
import "./login.css"
|
||||
import "./update.css"
|
||||
|
||||
const options = getOptions()
|
||||
|
||||
const isInput = (el: Element): el is HTMLInputElement => {
|
||||
return !!(el as HTMLInputElement).name
|
||||
}
|
||||
|
||||
document.querySelectorAll("form").forEach((form) => {
|
||||
if (!form.classList.contains("-x11")) {
|
||||
return
|
||||
}
|
||||
form.addEventListener("submit", (event) => {
|
||||
event.preventDefault()
|
||||
const values: { [key: string]: string } = {}
|
||||
Array.from(form.elements).forEach((element) => {
|
||||
if (isInput(element)) {
|
||||
values[element.name] = element.value
|
||||
}
|
||||
})
|
||||
fetch(normalize(`${options.base}/api/${ApiEndpoint.process}`), {
|
||||
method: "POST",
|
||||
body: JSON.stringify(values),
|
||||
})
|
||||
})
|
||||
})
|
||||
|
||||
// TEMP: Until we can get the real ready event.
|
||||
const event = new CustomEvent("ide-ready")
|
||||
window.dispatchEvent(event)
|
||||
@@ -11,28 +11,22 @@
|
||||
content="style-src 'self'; manifest-src 'self'; img-src 'self' data:; font-src 'self' data:;"
|
||||
/>
|
||||
<title>{{ERROR_TITLE}} - code-server</title>
|
||||
<link rel="icon" href="{{BASE}}/static/{{COMMIT}}/src/browser/media/favicon.ico" type="image/x-icon" />
|
||||
<link
|
||||
rel="manifest"
|
||||
href="{{BASE}}/static/{{COMMIT}}/src/browser/media/manifest.json"
|
||||
crossorigin="use-credentials"
|
||||
/>
|
||||
<link rel="apple-touch-icon" href="{{BASE}}/static/{{COMMIT}}/src/browser/media/pwa-icon-384.png" />
|
||||
<link href="{{BASE}}/static/{{COMMIT}}/dist/pages/app.css" rel="stylesheet" />
|
||||
<link rel="icon" href="{{CS_STATIC_BASE}}/src/browser/media/favicon.ico" type="image/x-icon" />
|
||||
<link rel="manifest" href="{{CS_STATIC_BASE}}/src/browser/media/manifest.json" crossorigin="use-credentials" />
|
||||
<link rel="apple-touch-icon" href="{{CS_STATIC_BASE}}/src/browser/media/pwa-icon-384.png" />
|
||||
<link href="{{CS_STATIC_BASE}}/dist/register.css" rel="stylesheet" />
|
||||
<meta id="coder-options" data-settings="{{OPTIONS}}" />
|
||||
</head>
|
||||
<body>
|
||||
<div class="center-container">
|
||||
<div class="error-display">
|
||||
<h2 class="header">{{ERROR_HEADER}}</h2>
|
||||
<div class="body">
|
||||
{{ERROR_BODY}}
|
||||
</div>
|
||||
<div class="body">{{ERROR_BODY}}</div>
|
||||
<div class="links">
|
||||
<a class="link" href="{{BASE}}{{TO}}">go home</a>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
<script data-cfasync="false" src="{{BASE}}/static/{{COMMIT}}/dist/register.js"></script>
|
||||
<script data-cfasync="false" src="{{CS_STATIC_BASE}}/dist/register.js"></script>
|
||||
</body>
|
||||
</html>
|
||||
|
||||
@@ -1,51 +0,0 @@
|
||||
.block-row {
|
||||
display: flex;
|
||||
}
|
||||
|
||||
.block-row > .item {
|
||||
flex: 1;
|
||||
margin: 2px 0;
|
||||
}
|
||||
|
||||
.block-row > button.item {
|
||||
background: none;
|
||||
border: none;
|
||||
cursor: pointer;
|
||||
text-align: left;
|
||||
}
|
||||
|
||||
.block-row > .item > .sub {
|
||||
font-size: 0.95em;
|
||||
}
|
||||
|
||||
.block-row .-link {
|
||||
color: rgb(87, 114, 245);
|
||||
display: block;
|
||||
text-decoration: none;
|
||||
}
|
||||
|
||||
.block-row .-link:hover {
|
||||
text-decoration: underline;
|
||||
}
|
||||
|
||||
.block-row > .item > .icon {
|
||||
height: 1rem;
|
||||
margin-right: 5px;
|
||||
vertical-align: top;
|
||||
width: 1rem;
|
||||
}
|
||||
|
||||
.block-row > .item > .icon.-missing {
|
||||
background-color: rgba(87, 114, 245, 0.2);
|
||||
display: inline-block;
|
||||
text-align: center;
|
||||
}
|
||||
|
||||
.kill-form {
|
||||
display: inline-block;
|
||||
}
|
||||
|
||||
.kill-form > .kill {
|
||||
border-radius: 3px;
|
||||
padding: 2px 5px;
|
||||
}
|
||||
@@ -1,59 +0,0 @@
|
||||
<!DOCTYPE html>
|
||||
<html lang="en">
|
||||
<head>
|
||||
<meta charset="utf-8" />
|
||||
<meta
|
||||
name="viewport"
|
||||
content="width=device-width, initial-scale=1.0, maximum-scale=1.0, minimum-scale=1.0, user-scalable=no"
|
||||
/>
|
||||
<meta
|
||||
http-equiv="Content-Security-Policy"
|
||||
content="style-src 'self' 'unsafe-inline'; manifest-src 'self'; img-src 'self' data:; font-src 'self' data:;"
|
||||
/>
|
||||
<title>code-server</title>
|
||||
<link rel="icon" href="{{BASE}}/static/{{COMMIT}}/src/browser/media/favicon.ico" type="image/x-icon" />
|
||||
<link
|
||||
rel="manifest"
|
||||
href="{{BASE}}/static/{{COMMIT}}/src/browser/media/manifest.json"
|
||||
crossorigin="use-credentials"
|
||||
/>
|
||||
<link rel="apple-touch-icon" href="{{BASE}}/static/{{COMMIT}}/src/browser/media/pwa-icon-384.png" />
|
||||
<link href="{{BASE}}/static/{{COMMIT}}/dist/pages/app.css" rel="stylesheet" />
|
||||
<meta id="coder-options" data-settings="{{OPTIONS}}" />
|
||||
</head>
|
||||
<body>
|
||||
<div class="center-container">
|
||||
<div class="card-box">
|
||||
<div class="header">
|
||||
<h2 class="main">Editors</h2>
|
||||
<div class="sub">Choose an editor to launch below.</div>
|
||||
</div>
|
||||
<div class="content">
|
||||
{{APP_LIST:EDITORS}}
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<div class="card-box">
|
||||
<div class="header">
|
||||
<h2 class="main">Other</h2>
|
||||
<div class="sub">Choose an application to launch below.</div>
|
||||
</div>
|
||||
<div class="content">
|
||||
{{APP_LIST:OTHER}}
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<div class="card-box">
|
||||
<div class="header">
|
||||
<h2 class="main">Version</h2>
|
||||
<div class="sub">Version information and updates.</div>
|
||||
</div>
|
||||
<div class="content">
|
||||
{{UPDATE:NAME}}
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
<script data-cfasync="false" src="{{BASE}}/static/{{COMMIT}}/dist/register.js"></script>
|
||||
<script data-cfasync="false" src="{{BASE}}/static/{{COMMIT}}/dist/pages/app.js"></script>
|
||||
</body>
|
||||
</html>
|
||||
@@ -11,14 +11,10 @@
|
||||
content="style-src 'self'; script-src 'self' 'unsafe-inline'; manifest-src 'self'; img-src 'self' data:; font-src 'self' data:;"
|
||||
/>
|
||||
<title>code-server login</title>
|
||||
<link rel="icon" href="{{BASE}}/static/{{COMMIT}}/src/browser/media/favicon.ico" type="image/x-icon" />
|
||||
<link
|
||||
rel="manifest"
|
||||
href="{{BASE}}/static/{{COMMIT}}/src/browser/media/manifest.json"
|
||||
crossorigin="use-credentials"
|
||||
/>
|
||||
<link rel="apple-touch-icon" href="{{BASE}}/static/{{COMMIT}}/src/browser/media/pwa-icon-384.png" />
|
||||
<link href="{{BASE}}/static/{{COMMIT}}/dist/pages/app.css" rel="stylesheet" />
|
||||
<link rel="icon" href="{{CS_STATIC_BASE}}/src/browser/media/favicon.ico" type="image/x-icon" />
|
||||
<link rel="manifest" href="{{CS_STATIC_BASE}}/src/browser/media/manifest.json" crossorigin="use-credentials" />
|
||||
<link rel="apple-touch-icon" href="{{CS_STATIC_BASE}}/src/browser/media/pwa-icon-384.png" />
|
||||
<link href="{{CS_STATIC_BASE}}/dist/register.css" rel="stylesheet" />
|
||||
<meta id="coder-options" data-settings="{{OPTIONS}}" />
|
||||
</head>
|
||||
<body>
|
||||
@@ -50,11 +46,6 @@
|
||||
</div>
|
||||
</div>
|
||||
</body>
|
||||
<script data-cfasync="false" src="{{BASE}}/static/{{COMMIT}}/dist/register.js"></script>
|
||||
<script>
|
||||
const parts = window.location.pathname.replace(/^\//g, "").split("/")
|
||||
parts[parts.length - 1] = "{{BASE}}"
|
||||
const url = new URL(window.location.origin + "/" + parts.join("/"))
|
||||
document.getElementById("base").value = url.pathname
|
||||
</script>
|
||||
<script data-cfasync="false" src="{{CS_STATIC_BASE}}/dist/register.js"></script>
|
||||
<script data-cfasync="false" src="{{CS_STATIC_BASE}}/dist/pages/login.js"></script>
|
||||
</html>
|
||||
|
||||
7
src/browser/pages/login.ts
Normal file
7
src/browser/pages/login.ts
Normal file
@@ -0,0 +1,7 @@
|
||||
import { getOptions } from "../../common/util"
|
||||
|
||||
const options = getOptions()
|
||||
const el = document.getElementById("base") as HTMLInputElement
|
||||
if (el) {
|
||||
el.value = options.base
|
||||
}
|
||||
@@ -1,43 +0,0 @@
|
||||
<!DOCTYPE html>
|
||||
<html lang="en">
|
||||
<head>
|
||||
<meta charset="utf-8" />
|
||||
<meta
|
||||
name="viewport"
|
||||
content="width=device-width, initial-scale=1.0, maximum-scale=1.0, minimum-scale=1.0, user-scalable=no"
|
||||
/>
|
||||
<meta
|
||||
http-equiv="Content-Security-Policy"
|
||||
content="style-src 'self'; manifest-src 'self'; img-src 'self' data:; font-src 'self' data:;"
|
||||
/>
|
||||
<title>code-server</title>
|
||||
<link rel="icon" href="{{BASE}}/static/{{COMMIT}}/src/browser/media/favicon.ico" type="image/x-icon" />
|
||||
<link
|
||||
rel="manifest"
|
||||
href="{{BASE}}/static/{{COMMIT}}/src/browser/media/manifest.json"
|
||||
crossorigin="use-credentials"
|
||||
/>
|
||||
<link rel="apple-touch-icon" href="{{BASE}}/static/{{COMMIT}}/src/browser/media/pwa-icon-384.png" />
|
||||
<link href="{{BASE}}/static/{{COMMIT}}/dist/pages/app.css" rel="stylesheet" />
|
||||
<meta id="coder-options" data-settings="{{OPTIONS}}" />
|
||||
</head>
|
||||
<body>
|
||||
<div class="center-container">
|
||||
<div class="card-box">
|
||||
<div class="header">
|
||||
<h1 class="main">Update</h1>
|
||||
<div class="sub">Update code-server.</div>
|
||||
</div>
|
||||
<div class="content">
|
||||
<form class="update-form" action="{{BASE}}/update/apply">
|
||||
{{UPDATE_STATUS}} {{ERROR}}
|
||||
<div class="links">
|
||||
<a class="link" href="{{BASE}}{{TO}}">go home</a>
|
||||
</div>
|
||||
</form>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
<script data-cfasync="false" src="{{BASE}}/static/{{COMMIT}}/dist/register.js"></script>
|
||||
</body>
|
||||
</html>
|
||||
@@ -2,12 +2,12 @@
|
||||
<!DOCTYPE html>
|
||||
<html>
|
||||
<head>
|
||||
<meta charset="utf-8" />
|
||||
<script>
|
||||
globalThis.MonacoPerformanceMarks = globalThis.MonacoPerformanceMarks || []
|
||||
globalThis.MonacoPerformanceMarks.push("renderer/started", Date.now())
|
||||
</script>
|
||||
|
||||
<meta
|
||||
http-equiv="Content-Security-Policy"
|
||||
content="font-src 'self' data:; connect-src ws: wss: 'self' https:; default-src ws: wss: 'self'; style-src 'self' 'unsafe-inline'; script-src 'self' 'unsafe-inline' 'unsafe-eval'; manifest-src 'self'; img-src 'self' data: https:;"
|
||||
/>
|
||||
<meta charset="utf-8" />
|
||||
|
||||
<!-- Disable pinch zooming -->
|
||||
<meta
|
||||
@@ -24,21 +24,17 @@
|
||||
<meta id="vscode-remote-nls-configuration" data-settings="{{NLS_CONFIGURATION}}" />
|
||||
|
||||
<!-- Workbench Icon/Manifest/CSS -->
|
||||
<link rel="icon" href="{{BASE}}/static/{{COMMIT}}/src/browser/media/favicon.ico" type="image/x-icon" />
|
||||
<link
|
||||
rel="manifest"
|
||||
href="{{BASE}}/static/{{COMMIT}}/src/browser/media/manifest.json"
|
||||
crossorigin="use-credentials"
|
||||
/>
|
||||
<link rel="icon" href="{{CS_STATIC_BASE}}/src/browser/media/favicon.ico" type="image/x-icon" />
|
||||
<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="{{BASE}}/static/{{COMMIT}}/lib/vscode/out/vs/workbench/workbench.web.api.css">
|
||||
<link data-name="vs/workbench/workbench.web.api" rel="stylesheet" href="{{CS_STATIC_BASE}}/lib/vscode/out/vs/workbench/workbench.web.api.css">
|
||||
END_PROD_ONLY -->
|
||||
<link rel="apple-touch-icon" href="{{BASE}}/static/{{COMMIT}}/src/browser/media/pwa-icon-384.png" />
|
||||
<link rel="apple-touch-icon" href="{{CS_STATIC_BASE}}/src/browser/media/pwa-icon-384.png" />
|
||||
<meta name="apple-mobile-web-app-capable" content="yes" />
|
||||
|
||||
<!-- Prefetch to avoid waterfall -->
|
||||
<!-- PROD_ONLY
|
||||
<link rel="prefetch" href="{{BASE}}/static/{{COMMIT}}/lib/vscode/node_modules/semver-umd/lib/semver-umd.js">
|
||||
<link rel="prefetch" href="{{CS_STATIC_BASE}}/lib/vscode/node_modules/semver-umd/lib/semver-umd.js">
|
||||
END_PROD_ONLY -->
|
||||
|
||||
<meta id="coder-options" data-settings="{{OPTIONS}}" />
|
||||
@@ -47,64 +43,17 @@
|
||||
<body aria-label=""></body>
|
||||
|
||||
<!-- Startup (do not modify order of script tags!) -->
|
||||
<script data-cfasync="false" src="{{CS_STATIC_BASE}}/dist/pages/vscode.js"></script>
|
||||
<script data-cfasync="false" src="{{CS_STATIC_BASE}}/dist/register.js"></script>
|
||||
<script data-cfasync="false" src="{{CS_STATIC_BASE}}/lib/vscode/out/vs/loader.js"></script>
|
||||
<script>
|
||||
const parts = window.location.pathname.replace(/^\//g, "").split("/")
|
||||
parts[parts.length - 1] = "{{BASE}}"
|
||||
const url = new URL(window.location.origin + "/" + parts.join("/"))
|
||||
const staticBase = url.href.replace(/\/+$/, "") + "/static/{{COMMIT}}/lib/vscode"
|
||||
let nlsConfig
|
||||
try {
|
||||
nlsConfig = JSON.parse(document.getElementById("vscode-remote-nls-configuration").getAttribute("data-settings"))
|
||||
if (nlsConfig._resolvedLanguagePackCoreLocation) {
|
||||
const bundles = Object.create(null)
|
||||
nlsConfig.loadBundle = (bundle, language, cb) => {
|
||||
let result = bundles[bundle]
|
||||
if (result) {
|
||||
return cb(undefined, result)
|
||||
}
|
||||
// FIXME: Only works if path separators are /.
|
||||
const path = nlsConfig._resolvedLanguagePackCoreLocation + "/" + bundle.replace(/\//g, "!") + ".nls.json"
|
||||
fetch(`${url.href}/resource/?path=${encodeURIComponent(path)}`)
|
||||
.then((response) => response.json())
|
||||
.then((json) => {
|
||||
bundles[bundle] = json
|
||||
cb(undefined, json)
|
||||
})
|
||||
.catch(cb)
|
||||
}
|
||||
}
|
||||
} catch (error) {
|
||||
/* Probably fine. */
|
||||
}
|
||||
self.require = {
|
||||
baseUrl: `${staticBase}/out`,
|
||||
paths: {
|
||||
"vscode-textmate": `${staticBase}/node_modules/vscode-textmate/release/main`,
|
||||
"vscode-oniguruma": `${staticBase}/node_modules/vscode-oniguruma/release/main`,
|
||||
xterm: `${staticBase}/node_modules/xterm/lib/xterm.js`,
|
||||
"xterm-addon-search": `${staticBase}/node_modules/xterm-addon-search/lib/xterm-addon-search.js`,
|
||||
"xterm-addon-unicode11": `${staticBase}/node_modules/xterm-addon-unicode11/lib/xterm-addon-unicode11.js`,
|
||||
"xterm-addon-web-links": `${staticBase}/node_modules/xterm-addon-web-links/lib/xterm-addon-web-links.js`,
|
||||
"xterm-addon-webgl": `${staticBase}/node_modules/xterm-addon-webgl/lib/xterm-addon-webgl.js`,
|
||||
"semver-umd": `${staticBase}/node_modules/semver-umd/lib/semver-umd.js`,
|
||||
},
|
||||
"vs/nls": nlsConfig,
|
||||
}
|
||||
globalThis.MonacoPerformanceMarks.push("willLoadWorkbenchMain", Date.now())
|
||||
</script>
|
||||
<script data-cfasync="false" src="{{BASE}}/static/{{COMMIT}}/dist/register.js"></script>
|
||||
<script data-cfasync="false" src="{{BASE}}/static/{{COMMIT}}/lib/vscode/out/vs/loader.js"></script>
|
||||
<!-- PROD_ONLY
|
||||
<script data-cfasync="false" src="{{BASE}}/static/{{COMMIT}}/lib/vscode/out/vs/workbench/workbench.web.api.nls.js"></script>
|
||||
<script data-cfasync="false" src="{{BASE}}/static/{{COMMIT}}/lib/vscode/out/vs/workbench/workbench.web.api.js"></script>
|
||||
<script data-cfasync="false" src="{{CS_STATIC_BASE}}/lib/vscode/out/vs/workbench/workbench.web.api.nls.js"></script>
|
||||
<script data-cfasync="false" src="{{CS_STATIC_BASE}}/lib/vscode/out/vs/workbench/workbench.web.api.js"></script>
|
||||
END_PROD_ONLY -->
|
||||
<script>
|
||||
require(["vs/code/browser/workbench/workbench"], function () {})
|
||||
</script>
|
||||
<script>
|
||||
try {
|
||||
document.body.style.background = JSON.parse(localStorage.getItem("colorThemeData")).colorMap["editor.background"]
|
||||
} catch (error) {
|
||||
// Oh well.
|
||||
}
|
||||
</script>
|
||||
</html>
|
||||
|
||||
56
src/browser/pages/vscode.ts
Normal file
56
src/browser/pages/vscode.ts
Normal file
@@ -0,0 +1,56 @@
|
||||
import { getOptions } from "../../common/util"
|
||||
|
||||
const options = getOptions()
|
||||
|
||||
// TODO: Add proper types.
|
||||
/* eslint-disable @typescript-eslint/no-explicit-any */
|
||||
|
||||
let nlsConfig: any
|
||||
try {
|
||||
nlsConfig = JSON.parse(document.getElementById("vscode-remote-nls-configuration")!.getAttribute("data-settings")!)
|
||||
if (nlsConfig._resolvedLanguagePackCoreLocation) {
|
||||
const bundles = Object.create(null)
|
||||
nlsConfig.loadBundle = (bundle: any, _language: any, cb: any): void => {
|
||||
const result = bundles[bundle]
|
||||
if (result) {
|
||||
return cb(undefined, result)
|
||||
}
|
||||
// FIXME: Only works if path separators are /.
|
||||
const path = nlsConfig._resolvedLanguagePackCoreLocation + "/" + bundle.replace(/\//g, "!") + ".nls.json"
|
||||
fetch(`${options.base}/vscode/resource/?path=${encodeURIComponent(path)}`)
|
||||
.then((response) => response.json())
|
||||
.then((json) => {
|
||||
bundles[bundle] = json
|
||||
cb(undefined, json)
|
||||
})
|
||||
.catch(cb)
|
||||
}
|
||||
}
|
||||
} catch (error) {
|
||||
/* Probably fine. */
|
||||
}
|
||||
|
||||
;(self.require as any) = {
|
||||
// Without the full URL VS Code will try to load file://.
|
||||
baseUrl: `${window.location.origin}${options.csStaticBase}/lib/vscode/out`,
|
||||
recordStats: true,
|
||||
paths: {
|
||||
"vscode-textmate": `../node_modules/vscode-textmate/release/main`,
|
||||
"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`,
|
||||
"semver-umd": `../node_modules/semver-umd/lib/semver-umd.js`,
|
||||
"tas-client-umd": `../node_modules/tas-client-umd/lib/tas-client-umd.js`,
|
||||
"iconv-lite-umd": `../node_modules/iconv-lite-umd/lib/iconv-lite-umd.js`,
|
||||
jschardet: `../node_modules/jschardet/dist/jschardet.min.js`,
|
||||
},
|
||||
"vs/nls": nlsConfig,
|
||||
}
|
||||
|
||||
try {
|
||||
document.body.style.background = JSON.parse(localStorage.getItem("colorThemeData")!).colorMap["editor.background"]
|
||||
} catch (error) {
|
||||
// Oh well.
|
||||
}
|
||||
@@ -2,13 +2,17 @@ import { getOptions, normalize } from "../common/util"
|
||||
|
||||
const options = getOptions()
|
||||
|
||||
import "./pages/error.css"
|
||||
import "./pages/global.css"
|
||||
import "./pages/login.css"
|
||||
|
||||
if ("serviceWorker" in navigator) {
|
||||
const path = normalize(`${options.base}/static/${options.commit}/dist/serviceWorker.js`)
|
||||
const path = normalize(`${options.csStaticBase}/dist/serviceWorker.js`)
|
||||
navigator.serviceWorker
|
||||
.register(path, {
|
||||
scope: options.base || "/",
|
||||
scope: (options.base ?? "") + "/",
|
||||
})
|
||||
.then(function () {
|
||||
.then(() => {
|
||||
console.log("[Service Worker] registered")
|
||||
})
|
||||
}
|
||||
|
||||
2
src/browser/robots.txt
Normal file
2
src/browser/robots.txt
Normal file
@@ -0,0 +1,2 @@
|
||||
User-agent: *
|
||||
Disallow: /
|
||||
@@ -8,17 +8,6 @@ self.addEventListener("activate", (event: any) => {
|
||||
event.waitUntil((self as any).clients.claim())
|
||||
})
|
||||
|
||||
self.addEventListener("fetch", (event: any) => {
|
||||
if (!navigator.onLine) {
|
||||
event.respondWith(
|
||||
new Promise((resolve) => {
|
||||
resolve(
|
||||
new Response("OFFLINE", {
|
||||
status: 200,
|
||||
statusText: "OK",
|
||||
}),
|
||||
)
|
||||
}),
|
||||
)
|
||||
}
|
||||
self.addEventListener("fetch", () => {
|
||||
// Without this event handler we won't be recognized as a PWA.
|
||||
})
|
||||
|
||||
@@ -1,60 +0,0 @@
|
||||
export interface Application {
|
||||
readonly categories?: string[]
|
||||
readonly comment?: string
|
||||
readonly directory?: string
|
||||
readonly exec?: string
|
||||
readonly genericName?: string
|
||||
readonly icon?: string
|
||||
readonly installed?: boolean
|
||||
readonly name: string
|
||||
/**
|
||||
* Path if this is a browser app (like VS Code).
|
||||
*/
|
||||
readonly path?: string
|
||||
/**
|
||||
* PID if this is a process.
|
||||
*/
|
||||
readonly pid?: number
|
||||
readonly version?: string
|
||||
}
|
||||
|
||||
export interface ApplicationsResponse {
|
||||
readonly applications: ReadonlyArray<Application>
|
||||
}
|
||||
|
||||
export enum SessionError {
|
||||
FailedToStart = 4000,
|
||||
Starting = 4001,
|
||||
InvalidState = 4002,
|
||||
Unknown = 4003,
|
||||
}
|
||||
|
||||
export interface SessionResponse {
|
||||
/**
|
||||
* Whether the process was spawned or an existing one was returned.
|
||||
*/
|
||||
created: boolean
|
||||
pid: number
|
||||
}
|
||||
|
||||
export interface RecentResponse {
|
||||
readonly paths: string[]
|
||||
readonly workspaces: string[]
|
||||
}
|
||||
|
||||
export interface HealthRequest {
|
||||
readonly event: "health"
|
||||
}
|
||||
|
||||
export type ClientMessage = HealthRequest
|
||||
|
||||
export interface HealthResponse {
|
||||
readonly event: "health"
|
||||
readonly connections: number
|
||||
}
|
||||
|
||||
export type ServerMessage = HealthResponse
|
||||
|
||||
export interface ReadyMessage {
|
||||
protocol: string
|
||||
}
|
||||
@@ -1,19 +1,21 @@
|
||||
import { Callback } from "./types"
|
||||
|
||||
export interface Disposable {
|
||||
dispose(): void
|
||||
}
|
||||
|
||||
export interface Event<T> {
|
||||
(listener: (value: T) => void): Disposable
|
||||
(listener: Callback<T>): Disposable
|
||||
}
|
||||
|
||||
/**
|
||||
* Emitter typecasts for a single event type.
|
||||
*/
|
||||
export class Emitter<T> {
|
||||
private listeners: Array<(value: T) => void> = []
|
||||
private listeners: Array<Callback<T>> = []
|
||||
|
||||
public get event(): Event<T> {
|
||||
return (cb: (value: T) => void): Disposable => {
|
||||
return (cb: Callback<T>): Disposable => {
|
||||
this.listeners.push(cb)
|
||||
|
||||
return {
|
||||
|
||||
@@ -9,16 +9,8 @@ export enum HttpCode {
|
||||
}
|
||||
|
||||
export class HttpError extends Error {
|
||||
public constructor(message: string, public readonly code: number) {
|
||||
public constructor(message: string, public readonly code: number, public readonly details?: object) {
|
||||
super(message)
|
||||
this.name = this.constructor.name
|
||||
}
|
||||
}
|
||||
|
||||
export enum ApiEndpoint {
|
||||
applications = "/applications",
|
||||
process = "/process",
|
||||
recent = "/recent",
|
||||
run = "/run",
|
||||
status = "/status",
|
||||
}
|
||||
|
||||
1
src/common/types.ts
Normal file
1
src/common/types.ts
Normal file
@@ -0,0 +1 @@
|
||||
export type Callback<T, R = void> = (t: T) => R
|
||||
@@ -2,9 +2,8 @@ import { logger, field } from "@coder/logger"
|
||||
|
||||
export interface Options {
|
||||
base: string
|
||||
commit: string
|
||||
csStaticBase: string
|
||||
logLevel: number
|
||||
pid?: number
|
||||
}
|
||||
|
||||
/**
|
||||
@@ -16,7 +15,11 @@ export const split = (str: string, delimiter: string): [string, string] => {
|
||||
return index !== -1 ? [str.substring(0, index).trim(), str.substring(index + 1)] : [str, ""]
|
||||
}
|
||||
|
||||
export const plural = (count: number): string => (count === 1 ? "" : "s")
|
||||
/**
|
||||
* Appends an 's' to the provided string if count is greater than one;
|
||||
* otherwise the string is returned
|
||||
*/
|
||||
export const plural = (count: number, str: string): string => (count === 1 ? str : `${str}s`)
|
||||
|
||||
export const generateUuid = (length = 24): string => {
|
||||
const possible = "0123456789ABCDEFGHIJKLMNOPQRSTUVWXYZabcdefghijklmnopqrstuvwxyz"
|
||||
@@ -33,21 +36,35 @@ export const normalize = (url: string, keepTrailing = false): string => {
|
||||
return url.replace(/\/\/+/g, "/").replace(/\/+$/, keepTrailing ? "/" : "")
|
||||
}
|
||||
|
||||
/**
|
||||
* Remove leading and trailing slashes.
|
||||
*/
|
||||
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 {
|
||||
const el = document.getElementById("coder-options")
|
||||
if (!el) {
|
||||
throw new Error("no options element")
|
||||
}
|
||||
const value = el.getAttribute("data-settings")
|
||||
if (!value) {
|
||||
throw new Error("no options value")
|
||||
}
|
||||
options = JSON.parse(value)
|
||||
options = JSON.parse(document.getElementById("coder-options")!.getAttribute("data-settings")!)
|
||||
} catch (error) {
|
||||
options = {} as T
|
||||
}
|
||||
@@ -61,17 +78,26 @@ export const getOptions = <T extends Options>(): T => {
|
||||
}
|
||||
}
|
||||
|
||||
if (typeof options.logLevel !== "undefined") {
|
||||
logger.level = options.logLevel
|
||||
}
|
||||
if (options.base) {
|
||||
const parts = location.pathname.replace(/^\//g, "").split("/")
|
||||
parts[parts.length - 1] = options.base
|
||||
const url = new URL(location.origin + "/" + parts.join("/"))
|
||||
options.base = normalize(url.pathname, true)
|
||||
}
|
||||
logger.level = options.logLevel
|
||||
|
||||
options.base = resolveBase(options.base)
|
||||
options.csStaticBase = resolveBase(options.csStaticBase)
|
||||
|
||||
logger.debug("got options", field("options", options))
|
||||
|
||||
return options
|
||||
}
|
||||
|
||||
/**
|
||||
* Wrap the value in an array if it's not already an array. If the value is
|
||||
* undefined return an empty array.
|
||||
*/
|
||||
export const arrayify = <T>(value?: T | T[]): T[] => {
|
||||
if (Array.isArray(value)) {
|
||||
return value
|
||||
}
|
||||
if (typeof value === "undefined") {
|
||||
return []
|
||||
}
|
||||
return [value]
|
||||
}
|
||||
|
||||
@@ -1,312 +0,0 @@
|
||||
import { field, logger } from "@coder/logger"
|
||||
import * as cp from "child_process"
|
||||
import * as fs from "fs-extra"
|
||||
import * as http from "http"
|
||||
import * as net from "net"
|
||||
import * as path from "path"
|
||||
import * as url from "url"
|
||||
import * as WebSocket from "ws"
|
||||
import {
|
||||
Application,
|
||||
ApplicationsResponse,
|
||||
ClientMessage,
|
||||
RecentResponse,
|
||||
ServerMessage,
|
||||
SessionError,
|
||||
SessionResponse,
|
||||
} from "../../common/api"
|
||||
import { ApiEndpoint, HttpCode, HttpError } from "../../common/http"
|
||||
import { HttpProvider, HttpProviderOptions, HttpResponse, HttpServer, Route } from "../http"
|
||||
import { findApplications, findWhitelistedApplications, Vscode } from "./bin"
|
||||
import { VscodeHttpProvider } from "./vscode"
|
||||
|
||||
interface VsRecents {
|
||||
[key: string]: (string | { configURIPath: string })[]
|
||||
}
|
||||
|
||||
type VsSettings = [string, string][]
|
||||
|
||||
/**
|
||||
* API HTTP provider.
|
||||
*/
|
||||
export class ApiHttpProvider extends HttpProvider {
|
||||
private readonly ws = new WebSocket.Server({ noServer: true })
|
||||
|
||||
public constructor(
|
||||
options: HttpProviderOptions,
|
||||
private readonly server: HttpServer,
|
||||
private readonly vscode: VscodeHttpProvider,
|
||||
private readonly dataDir?: string,
|
||||
) {
|
||||
super(options)
|
||||
}
|
||||
|
||||
public async handleRequest(route: Route, request: http.IncomingMessage): Promise<HttpResponse> {
|
||||
this.ensureAuthenticated(request)
|
||||
if (!this.isRoot(route)) {
|
||||
throw new HttpError("Not found", HttpCode.NotFound)
|
||||
}
|
||||
|
||||
switch (route.base) {
|
||||
case ApiEndpoint.applications:
|
||||
this.ensureMethod(request)
|
||||
return {
|
||||
mime: "application/json",
|
||||
content: {
|
||||
applications: await this.applications(),
|
||||
},
|
||||
} as HttpResponse<ApplicationsResponse>
|
||||
case ApiEndpoint.process:
|
||||
return this.process(request)
|
||||
case ApiEndpoint.recent:
|
||||
this.ensureMethod(request)
|
||||
return {
|
||||
mime: "application/json",
|
||||
content: await this.recent(),
|
||||
} as HttpResponse<RecentResponse>
|
||||
}
|
||||
|
||||
throw new HttpError("Not found", HttpCode.NotFound)
|
||||
}
|
||||
|
||||
public async handleWebSocket(
|
||||
route: Route,
|
||||
request: http.IncomingMessage,
|
||||
socket: net.Socket,
|
||||
head: Buffer,
|
||||
): Promise<void> {
|
||||
if (!this.authenticated(request)) {
|
||||
throw new Error("not authenticated")
|
||||
}
|
||||
switch (route.base) {
|
||||
case ApiEndpoint.status:
|
||||
return this.handleStatusSocket(request, socket, head)
|
||||
case ApiEndpoint.run:
|
||||
return this.handleRunSocket(route, request, socket, head)
|
||||
}
|
||||
|
||||
throw new HttpError("Not found", HttpCode.NotFound)
|
||||
}
|
||||
|
||||
private async handleStatusSocket(request: http.IncomingMessage, socket: net.Socket, head: Buffer): Promise<void> {
|
||||
const getMessageResponse = async (event: "health"): Promise<ServerMessage> => {
|
||||
switch (event) {
|
||||
case "health":
|
||||
return { event, connections: await this.server.getConnections() }
|
||||
default:
|
||||
throw new Error("unexpected message")
|
||||
}
|
||||
}
|
||||
|
||||
await new Promise<WebSocket>((resolve) => {
|
||||
this.ws.handleUpgrade(request, socket, head, (ws) => {
|
||||
const send = (event: ServerMessage): void => {
|
||||
ws.send(JSON.stringify(event))
|
||||
}
|
||||
ws.on("message", (data) => {
|
||||
logger.trace("got message", field("message", data))
|
||||
try {
|
||||
const message: ClientMessage = JSON.parse(data.toString())
|
||||
getMessageResponse(message.event).then(send)
|
||||
} catch (error) {
|
||||
logger.error(error.message, field("message", data))
|
||||
}
|
||||
})
|
||||
resolve()
|
||||
})
|
||||
})
|
||||
}
|
||||
|
||||
/**
|
||||
* A socket that connects to the process.
|
||||
*/
|
||||
private async handleRunSocket(
|
||||
_route: Route,
|
||||
request: http.IncomingMessage,
|
||||
socket: net.Socket,
|
||||
head: Buffer,
|
||||
): Promise<void> {
|
||||
logger.debug("connecting to process")
|
||||
const ws = await new Promise<WebSocket>((resolve, reject) => {
|
||||
this.ws.handleUpgrade(request, socket, head, (socket) => {
|
||||
socket.binaryType = "arraybuffer"
|
||||
|
||||
socket.on("error", (error) => {
|
||||
socket.close(SessionError.FailedToStart)
|
||||
logger.error("got error while connecting socket", field("error", error))
|
||||
reject(error)
|
||||
})
|
||||
|
||||
resolve(socket as WebSocket)
|
||||
})
|
||||
})
|
||||
|
||||
logger.debug("connected to process")
|
||||
|
||||
// Send ready message.
|
||||
ws.send(
|
||||
Buffer.from(
|
||||
JSON.stringify({
|
||||
protocol: "TODO",
|
||||
}),
|
||||
),
|
||||
)
|
||||
}
|
||||
|
||||
/**
|
||||
* Return whitelisted applications.
|
||||
*/
|
||||
public async applications(): Promise<ReadonlyArray<Application>> {
|
||||
return findWhitelistedApplications()
|
||||
}
|
||||
|
||||
/**
|
||||
* Return installed applications.
|
||||
*/
|
||||
public async installedApplications(): Promise<ReadonlyArray<Application>> {
|
||||
return findApplications()
|
||||
}
|
||||
|
||||
/**
|
||||
* Handle /process endpoint.
|
||||
*/
|
||||
private async process(request: http.IncomingMessage): Promise<HttpResponse> {
|
||||
this.ensureMethod(request, ["DELETE", "POST"])
|
||||
|
||||
const data = await this.getData(request)
|
||||
if (!data) {
|
||||
throw new HttpError("No data was provided", HttpCode.BadRequest)
|
||||
}
|
||||
|
||||
const parsed: Application = JSON.parse(data)
|
||||
|
||||
switch (request.method) {
|
||||
case "DELETE":
|
||||
if (parsed.pid) {
|
||||
await this.killProcess(parsed.pid)
|
||||
} else if (parsed.path) {
|
||||
await this.killProcess(parsed.path)
|
||||
} else {
|
||||
throw new Error("No pid or path was provided")
|
||||
}
|
||||
return {
|
||||
mime: "application/json",
|
||||
code: HttpCode.Ok,
|
||||
}
|
||||
case "POST": {
|
||||
if (!parsed.exec) {
|
||||
throw new Error("No exec was provided")
|
||||
}
|
||||
return {
|
||||
mime: "application/json",
|
||||
content: {
|
||||
created: true,
|
||||
pid: await this.spawnProcess(parsed.exec),
|
||||
},
|
||||
} as HttpResponse<SessionResponse>
|
||||
}
|
||||
}
|
||||
|
||||
throw new HttpError("Not found", HttpCode.NotFound)
|
||||
}
|
||||
|
||||
/**
|
||||
* Kill a process identified by pid or path if a web app.
|
||||
*/
|
||||
public async killProcess(pid: number | string): Promise<void> {
|
||||
if (typeof pid === "string") {
|
||||
switch (pid) {
|
||||
case Vscode.path:
|
||||
await this.vscode.dispose()
|
||||
break
|
||||
default:
|
||||
throw new Error(`Process "${pid}" does not exist`)
|
||||
}
|
||||
} else {
|
||||
process.kill(pid)
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* Spawn a process and return the pid.
|
||||
*/
|
||||
public async spawnProcess(exec: string): Promise<number> {
|
||||
const proc = cp.spawn(exec, {
|
||||
shell: process.env.SHELL || true,
|
||||
env: {
|
||||
...process.env,
|
||||
},
|
||||
})
|
||||
|
||||
proc.on("error", (error) => logger.error("process errored", field("pid", proc.pid), field("error", error)))
|
||||
proc.on("exit", () => logger.debug("process exited", field("pid", proc.pid)))
|
||||
|
||||
logger.debug("started process", field("pid", proc.pid))
|
||||
|
||||
return proc.pid
|
||||
}
|
||||
|
||||
/**
|
||||
* Return VS Code's recent paths.
|
||||
*/
|
||||
public async recent(): Promise<RecentResponse> {
|
||||
try {
|
||||
if (!this.dataDir) {
|
||||
throw new Error("data directory is not set")
|
||||
}
|
||||
|
||||
const state: VsSettings = JSON.parse(await fs.readFile(path.join(this.dataDir, "User/state/global.json"), "utf8"))
|
||||
const setting = Array.isArray(state) && state.find((item) => item[0] === "recently.opened")
|
||||
if (!setting) {
|
||||
return { paths: [], workspaces: [] }
|
||||
}
|
||||
|
||||
const pathPromises: { [key: string]: Promise<string> } = {}
|
||||
const workspacePromises: { [key: string]: Promise<string> } = {}
|
||||
Object.values(JSON.parse(setting[1]) as VsRecents).forEach((recents) => {
|
||||
recents.forEach((recent) => {
|
||||
try {
|
||||
const target = typeof recent === "string" ? pathPromises : workspacePromises
|
||||
const pathname = url.parse(typeof recent === "string" ? recent : recent.configURIPath).pathname
|
||||
if (pathname && !target[pathname]) {
|
||||
target[pathname] = new Promise<string>((resolve) => {
|
||||
fs.stat(pathname)
|
||||
.then(() => resolve(pathname))
|
||||
.catch(() => resolve())
|
||||
})
|
||||
}
|
||||
} catch (error) {
|
||||
logger.debug("invalid path", field("path", recent))
|
||||
}
|
||||
})
|
||||
})
|
||||
|
||||
const [paths, workspaces] = await Promise.all([
|
||||
Promise.all(Object.values(pathPromises)),
|
||||
Promise.all(Object.values(workspacePromises)),
|
||||
])
|
||||
|
||||
return {
|
||||
paths: paths.filter((p) => !!p),
|
||||
workspaces: workspaces.filter((p) => !!p),
|
||||
}
|
||||
} catch (error) {
|
||||
if (error.code !== "ENOENT") {
|
||||
throw error
|
||||
}
|
||||
}
|
||||
|
||||
return { paths: [], workspaces: [] }
|
||||
}
|
||||
|
||||
/**
|
||||
* For these, just return the error message since they'll be requested as
|
||||
* JSON.
|
||||
*/
|
||||
public async getErrorRoot(_route: Route, _title: string, _header: string, error: string): Promise<HttpResponse> {
|
||||
return {
|
||||
mime: "application/json",
|
||||
content: JSON.stringify({ error }),
|
||||
}
|
||||
}
|
||||
}
|
||||
@@ -1,30 +0,0 @@
|
||||
import * as fs from "fs"
|
||||
import * as path from "path"
|
||||
import { Application } from "../../common/api"
|
||||
|
||||
const getVscodeVersion = (): string => {
|
||||
try {
|
||||
return require(path.resolve(__dirname, "../../../lib/vscode/package.json")).version
|
||||
} catch (error) {
|
||||
return "unknown"
|
||||
}
|
||||
}
|
||||
|
||||
export const Vscode: Application = {
|
||||
categories: ["Editor"],
|
||||
icon: fs.readFileSync(path.resolve(__dirname, "../../../lib/vscode/resources/linux/code.png")).toString("base64"),
|
||||
installed: true,
|
||||
name: "VS Code",
|
||||
path: "/",
|
||||
version: getVscodeVersion(),
|
||||
}
|
||||
|
||||
export const findApplications = async (): Promise<ReadonlyArray<Application>> => {
|
||||
const apps: Application[] = [Vscode]
|
||||
|
||||
return apps.sort((a, b): number => a.name.localeCompare(b.name))
|
||||
}
|
||||
|
||||
export const findWhitelistedApplications = async (): Promise<ReadonlyArray<Application>> => {
|
||||
return [Vscode]
|
||||
}
|
||||
@@ -1,147 +0,0 @@
|
||||
import * as http from "http"
|
||||
import * as querystring from "querystring"
|
||||
import { Application } from "../../common/api"
|
||||
import { HttpCode, HttpError } from "../../common/http"
|
||||
import { normalize } from "../../common/util"
|
||||
import { HttpProvider, HttpProviderOptions, HttpResponse, Route } from "../http"
|
||||
import { ApiHttpProvider } from "./api"
|
||||
import { UpdateHttpProvider } from "./update"
|
||||
|
||||
/**
|
||||
* Dashboard HTTP provider.
|
||||
*/
|
||||
export class DashboardHttpProvider extends HttpProvider {
|
||||
public constructor(
|
||||
options: HttpProviderOptions,
|
||||
private readonly api: ApiHttpProvider,
|
||||
private readonly update: UpdateHttpProvider,
|
||||
) {
|
||||
super(options)
|
||||
}
|
||||
|
||||
public async handleRequest(route: Route, request: http.IncomingMessage): Promise<HttpResponse> {
|
||||
if (!this.isRoot(route)) {
|
||||
throw new HttpError("Not found", HttpCode.NotFound)
|
||||
}
|
||||
|
||||
switch (route.base) {
|
||||
case "/spawn": {
|
||||
this.ensureAuthenticated(request)
|
||||
this.ensureMethod(request, "POST")
|
||||
const data = await this.getData(request)
|
||||
const app = data ? querystring.parse(data) : {}
|
||||
if (app.path) {
|
||||
return { redirect: Array.isArray(app.path) ? app.path[0] : app.path }
|
||||
}
|
||||
if (!app.exec) {
|
||||
throw new Error("No exec was provided")
|
||||
}
|
||||
this.api.spawnProcess(Array.isArray(app.exec) ? app.exec[0] : app.exec)
|
||||
return { redirect: this.options.base }
|
||||
}
|
||||
case "/app":
|
||||
case "/": {
|
||||
this.ensureMethod(request)
|
||||
if (!this.authenticated(request)) {
|
||||
return { redirect: "/login", query: { to: this.options.base } }
|
||||
}
|
||||
return route.base === "/" ? this.getRoot(route) : this.getAppRoot(route)
|
||||
}
|
||||
}
|
||||
|
||||
throw new HttpError("Not found", HttpCode.NotFound)
|
||||
}
|
||||
|
||||
public async getRoot(route: Route): Promise<HttpResponse> {
|
||||
const base = this.base(route)
|
||||
const apps = await this.api.installedApplications()
|
||||
const response = await this.getUtf8Resource(this.rootPath, "src/browser/pages/home.html")
|
||||
response.content = response.content
|
||||
.replace(/{{UPDATE:NAME}}/, await this.getUpdate(base))
|
||||
.replace(
|
||||
/{{APP_LIST:EDITORS}}/,
|
||||
this.getAppRows(
|
||||
base,
|
||||
apps.filter((app) => app.categories && app.categories.includes("Editor")),
|
||||
),
|
||||
)
|
||||
.replace(
|
||||
/{{APP_LIST:OTHER}}/,
|
||||
this.getAppRows(
|
||||
base,
|
||||
apps.filter((app) => !app.categories || !app.categories.includes("Editor")),
|
||||
),
|
||||
)
|
||||
return this.replaceTemplates(route, response)
|
||||
}
|
||||
|
||||
public async getAppRoot(route: Route): Promise<HttpResponse> {
|
||||
const response = await this.getUtf8Resource(this.rootPath, "src/browser/pages/app.html")
|
||||
return this.replaceTemplates(route, response)
|
||||
}
|
||||
|
||||
private getAppRows(base: string, apps: ReadonlyArray<Application>): string {
|
||||
return apps.length > 0
|
||||
? apps.map((app) => this.getAppRow(base, app)).join("\n")
|
||||
: `<div class="none">No applications found.</div>`
|
||||
}
|
||||
|
||||
private getAppRow(base: string, app: Application): string {
|
||||
return `<form class="block-row${app.exec ? " -x11" : ""}" method="post" action="${normalize(
|
||||
`${base}${this.options.base}/spawn`,
|
||||
)}">
|
||||
<button class="item -row -link">
|
||||
<input type="hidden" name="path" value="${app.path || ""}">
|
||||
<input type="hidden" name="exec" value="${app.exec || ""}">
|
||||
${
|
||||
app.icon
|
||||
? `<img class="icon" src="data:image/png;base64,${app.icon}"></img>`
|
||||
: `<span class="icon -missing"></span>`
|
||||
}
|
||||
<span class="name">${app.name}</span>
|
||||
</button>
|
||||
</form>`
|
||||
}
|
||||
|
||||
private async getUpdate(base: string): Promise<string> {
|
||||
if (!this.update.enabled) {
|
||||
return `<div class="block-row"><div class="item"><div class="sub">Updates are disabled</div></div></div>`
|
||||
}
|
||||
|
||||
const humanize = (time: number): string => {
|
||||
const d = new Date(time)
|
||||
const pad = (t: number): string => (t < 10 ? "0" : "") + t
|
||||
return (
|
||||
`${d.getFullYear()}-${pad(d.getMonth() + 1)}-${pad(d.getDate())}` +
|
||||
` ${pad(d.getHours())}:${pad(d.getMinutes())}`
|
||||
)
|
||||
}
|
||||
|
||||
const update = await this.update.getUpdate()
|
||||
if (this.update.isLatestVersion(update)) {
|
||||
return `<div class="block-row">
|
||||
<div class="item">
|
||||
Latest: ${update.version}
|
||||
<div class="sub">Up to date</div>
|
||||
</div>
|
||||
<div class="item">
|
||||
${humanize(update.checked)}
|
||||
<a class="sub -link" href="${base}/update/check?to=${this.options.base}">Check now</a>
|
||||
</div>
|
||||
<div class="item" >Current: ${this.update.currentVersion}</div>
|
||||
</div>`
|
||||
}
|
||||
|
||||
return `<div class="block-row">
|
||||
<div class="item">
|
||||
Latest: ${update.version}
|
||||
<div class="sub">Out of date</div>
|
||||
</div>
|
||||
<div class="item">
|
||||
${humanize(update.checked)}
|
||||
<a class="sub -link" href="${base}/update?to=${this.options.base}">Update now</a>
|
||||
</div>
|
||||
<div class="item" >Current: ${this.update.currentVersion}</div>
|
||||
</div>`
|
||||
}
|
||||
}
|
||||
21
src/node/app/health.ts
Normal file
21
src/node/app/health.ts
Normal file
@@ -0,0 +1,21 @@
|
||||
import { HttpProvider, HttpResponse, Heart, HttpProviderOptions } from "../http"
|
||||
|
||||
/**
|
||||
* Check the heartbeat.
|
||||
*/
|
||||
export class HealthHttpProvider extends HttpProvider {
|
||||
public constructor(options: HttpProviderOptions, private readonly heart: Heart) {
|
||||
super(options)
|
||||
}
|
||||
|
||||
public async handleRequest(): Promise<HttpResponse> {
|
||||
return {
|
||||
cache: false,
|
||||
mime: "application/json",
|
||||
content: {
|
||||
status: this.heart.alive() ? "alive" : "expired",
|
||||
lastHeartbeat: this.heart.lastHeartbeat,
|
||||
},
|
||||
}
|
||||
}
|
||||
}
|
||||
@@ -24,7 +24,7 @@ export class ProxyHttpProvider extends HttpProvider {
|
||||
const port = route.base.replace(/^\//, "")
|
||||
return {
|
||||
proxy: {
|
||||
base: `${this.options.base}/${port}`,
|
||||
strip: `${route.providerBase}/${port}`,
|
||||
port,
|
||||
},
|
||||
}
|
||||
@@ -35,7 +35,7 @@ export class ProxyHttpProvider extends HttpProvider {
|
||||
const port = route.base.replace(/^\//, "")
|
||||
return {
|
||||
proxy: {
|
||||
base: `${this.options.base}/${port}`,
|
||||
strip: `${route.providerBase}/${port}`,
|
||||
port,
|
||||
},
|
||||
}
|
||||
|
||||
@@ -8,10 +8,9 @@ import { HttpProvider, HttpResponse, Route } from "../http"
|
||||
import { pathToFsPath } from "../util"
|
||||
|
||||
/**
|
||||
* Static file HTTP provider. Regular static requests (the path is the request
|
||||
* itself) do not require authentication and they only allow access to resources
|
||||
* within the application. Requests for tars (the path is in a query parameter)
|
||||
* do require permissions and can access any directory.
|
||||
* Static file HTTP provider. Static requests do not require authentication if
|
||||
* the resource is in the application's directory except requests to serve a
|
||||
* directory as a tar which always requires authentication.
|
||||
*/
|
||||
export class StaticHttpProvider extends HttpProvider {
|
||||
public async handleRequest(route: Route, request: http.IncomingMessage): Promise<HttpResponse> {
|
||||
@@ -22,7 +21,7 @@ export class StaticHttpProvider extends HttpProvider {
|
||||
return this.getTarredResource(request, pathToFsPath(route.query.tar))
|
||||
}
|
||||
|
||||
const response = await this.getReplacedResource(route)
|
||||
const response = await this.getReplacedResource(request, route)
|
||||
if (!this.isDev) {
|
||||
response.cache = true
|
||||
}
|
||||
@@ -32,17 +31,25 @@ export class StaticHttpProvider extends HttpProvider {
|
||||
/**
|
||||
* Return a resource with variables replaced where necessary.
|
||||
*/
|
||||
protected async getReplacedResource(route: Route): Promise<HttpResponse> {
|
||||
protected async getReplacedResource(request: http.IncomingMessage, route: Route): Promise<HttpResponse> {
|
||||
// The first part is always the commit (for caching purposes).
|
||||
const split = route.requestPath.split("/").slice(1)
|
||||
|
||||
const resourcePath = path.resolve("/", ...split)
|
||||
|
||||
// Make sure it's in code-server or a plugin.
|
||||
const validPaths = [this.rootPath, process.env.PLUGIN_DIR]
|
||||
if (!validPaths.find((p) => p && resourcePath.startsWith(p))) {
|
||||
this.ensureAuthenticated(request)
|
||||
}
|
||||
|
||||
switch (split[split.length - 1]) {
|
||||
case "manifest.json": {
|
||||
const response = await this.getUtf8Resource(this.rootPath, ...split)
|
||||
const response = await this.getUtf8Resource(resourcePath)
|
||||
return this.replaceTemplates(route, response)
|
||||
}
|
||||
}
|
||||
return this.getResource(this.rootPath, ...split)
|
||||
return this.getResource(resourcePath)
|
||||
}
|
||||
|
||||
/**
|
||||
|
||||
@@ -1,21 +1,12 @@
|
||||
import { field, logger } from "@coder/logger"
|
||||
import * as cp from "child_process"
|
||||
import * as fs from "fs-extra"
|
||||
import * as http from "http"
|
||||
import * as https from "https"
|
||||
import * as os from "os"
|
||||
import * as path from "path"
|
||||
import * as semver from "semver"
|
||||
import { Readable, Writable } from "stream"
|
||||
import * as tar from "tar-fs"
|
||||
import * as url from "url"
|
||||
import * as util from "util"
|
||||
import * as zlib from "zlib"
|
||||
import { HttpCode, HttpError } from "../../common/http"
|
||||
import { HttpProvider, HttpProviderOptions, HttpResponse, Route } from "../http"
|
||||
import { settings as globalSettings, SettingsProvider, UpdateSettings } from "../settings"
|
||||
import { tmpdir } from "../util"
|
||||
import { ipcMain } from "../wrapper"
|
||||
|
||||
export interface Update {
|
||||
checked: number
|
||||
@@ -27,7 +18,7 @@ export interface LatestResponse {
|
||||
}
|
||||
|
||||
/**
|
||||
* Update HTTP provider.
|
||||
* HTTP provider for checking updates (does not download/install them).
|
||||
*/
|
||||
export class UpdateHttpProvider extends HttpProvider {
|
||||
private update?: Promise<Update>
|
||||
@@ -41,12 +32,6 @@ export class UpdateHttpProvider extends HttpProvider {
|
||||
* that fulfills `LatestResponse`.
|
||||
*/
|
||||
private readonly latestUrl = "https://api.github.com/repos/cdr/code-server/releases/latest",
|
||||
/**
|
||||
* The URL for downloading a version of code-server. {{VERSION}} and
|
||||
* {{RELEASE_NAME}} will be replaced (for example 2.1.0 and
|
||||
* code-server-2.1.0-linux-x86_64.tar.gz).
|
||||
*/
|
||||
private readonly downloadUrl = "https://github.com/cdr/code-server/releases/download/{{VERSION}}/{{RELEASE_NAME}}",
|
||||
/**
|
||||
* Update information will be stored here. If not provided, the global
|
||||
* settings will be used.
|
||||
@@ -64,66 +49,30 @@ export class UpdateHttpProvider extends HttpProvider {
|
||||
throw new HttpError("Not found", HttpCode.NotFound)
|
||||
}
|
||||
|
||||
switch (route.base) {
|
||||
case "/check":
|
||||
this.getUpdate(true)
|
||||
if (route.query && route.query.to) {
|
||||
return {
|
||||
redirect: Array.isArray(route.query.to) ? route.query.to[0] : route.query.to,
|
||||
query: { to: undefined },
|
||||
}
|
||||
}
|
||||
return this.getRoot(route, request)
|
||||
case "/apply":
|
||||
return this.tryUpdate(route, request)
|
||||
case "/":
|
||||
return this.getRoot(route, request)
|
||||
if (!this.enabled) {
|
||||
throw new Error("update checks are disabled")
|
||||
}
|
||||
|
||||
throw new HttpError("Not found", HttpCode.NotFound)
|
||||
}
|
||||
|
||||
public async getRoot(
|
||||
route: Route,
|
||||
request: http.IncomingMessage,
|
||||
errorOrUpdate?: Update | Error,
|
||||
): Promise<HttpResponse> {
|
||||
if (request.headers["content-type"] === "application/json") {
|
||||
if (!this.enabled) {
|
||||
switch (route.base) {
|
||||
case "/check":
|
||||
case "/": {
|
||||
const update = await this.getUpdate(route.base === "/check")
|
||||
return {
|
||||
content: {
|
||||
isLatest: true,
|
||||
...update,
|
||||
isLatest: this.isLatestVersion(update),
|
||||
},
|
||||
}
|
||||
}
|
||||
const update = await this.getUpdate()
|
||||
return {
|
||||
content: {
|
||||
...update,
|
||||
isLatest: this.isLatestVersion(update),
|
||||
},
|
||||
}
|
||||
}
|
||||
const response = await this.getUtf8Resource(this.rootPath, "src/browser/pages/update.html")
|
||||
response.content = response.content
|
||||
.replace(
|
||||
/{{UPDATE_STATUS}}/,
|
||||
errorOrUpdate && !(errorOrUpdate instanceof Error)
|
||||
? `Updated to ${errorOrUpdate.version}`
|
||||
: await this.getUpdateHtml(),
|
||||
)
|
||||
.replace(/{{ERROR}}/, errorOrUpdate instanceof Error ? `<div class="error">${errorOrUpdate.message}</div>` : "")
|
||||
return this.replaceTemplates(route, response)
|
||||
|
||||
throw new HttpError("Not found", HttpCode.NotFound)
|
||||
}
|
||||
|
||||
/**
|
||||
* Query for and return the latest update.
|
||||
*/
|
||||
public async getUpdate(force?: boolean): Promise<Update> {
|
||||
if (!this.enabled) {
|
||||
throw new Error("updates are not enabled")
|
||||
}
|
||||
|
||||
// Don't run multiple requests at a time.
|
||||
if (!this.update) {
|
||||
this.update = this._getUpdate(force)
|
||||
@@ -171,128 +120,6 @@ export class UpdateHttpProvider extends HttpProvider {
|
||||
}
|
||||
}
|
||||
|
||||
private async getUpdateHtml(): Promise<string> {
|
||||
if (!this.enabled) {
|
||||
return "Updates are disabled"
|
||||
}
|
||||
|
||||
const update = await this.getUpdate()
|
||||
if (this.isLatestVersion(update)) {
|
||||
return "No update available"
|
||||
}
|
||||
|
||||
return `<button type="submit" class="apply -button">Update to ${update.version}</button>`
|
||||
}
|
||||
|
||||
public async tryUpdate(route: Route, request: http.IncomingMessage): Promise<HttpResponse> {
|
||||
try {
|
||||
const update = await this.getUpdate()
|
||||
if (!this.isLatestVersion(update)) {
|
||||
await this.downloadAndApplyUpdate(update)
|
||||
return this.getRoot(route, request, update)
|
||||
}
|
||||
return this.getRoot(route, request)
|
||||
} catch (error) {
|
||||
// For JSON requests propagate the error. Otherwise catch it so we can
|
||||
// show the error inline with the update button instead of an error page.
|
||||
if (request.headers["content-type"] === "application/json") {
|
||||
throw error
|
||||
}
|
||||
return this.getRoot(route, error)
|
||||
}
|
||||
}
|
||||
|
||||
public async downloadAndApplyUpdate(update: Update, targetPath?: string): Promise<void> {
|
||||
const releaseName = await this.getReleaseName(update)
|
||||
const url = this.downloadUrl.replace("{{VERSION}}", update.version).replace("{{RELEASE_NAME}}", releaseName)
|
||||
|
||||
let downloadPath = path.join(tmpdir, "updates", releaseName)
|
||||
fs.mkdirp(path.dirname(downloadPath))
|
||||
|
||||
const response = await this.requestResponse(url)
|
||||
|
||||
try {
|
||||
downloadPath = await this.extractTar(response, downloadPath)
|
||||
logger.debug("Downloaded update", field("path", downloadPath))
|
||||
|
||||
// The archive should have a directory inside at the top level with the
|
||||
// same name as the archive.
|
||||
const directoryPath = path.join(downloadPath, path.basename(downloadPath))
|
||||
await fs.stat(directoryPath)
|
||||
|
||||
if (!targetPath) {
|
||||
// eslint-disable-next-line @typescript-eslint/no-explicit-any
|
||||
targetPath = path.resolve(__dirname, "../../../")
|
||||
}
|
||||
|
||||
// Move the old directory to prevent potential data loss.
|
||||
const backupPath = path.resolve(targetPath, `../${path.basename(targetPath)}.${Date.now().toString()}`)
|
||||
logger.debug("Replacing files", field("target", targetPath), field("backup", backupPath))
|
||||
await fs.move(targetPath, backupPath)
|
||||
|
||||
// Move the new directory.
|
||||
await fs.move(directoryPath, targetPath)
|
||||
|
||||
await fs.remove(downloadPath)
|
||||
|
||||
if (process.send) {
|
||||
ipcMain().relaunch(update.version)
|
||||
}
|
||||
} catch (error) {
|
||||
response.destroy(error)
|
||||
throw error
|
||||
}
|
||||
}
|
||||
|
||||
private async extractTar(response: Readable, downloadPath: string): Promise<string> {
|
||||
downloadPath = downloadPath.replace(/\.tar\.gz$/, "")
|
||||
logger.debug("Extracting tar", field("path", downloadPath))
|
||||
|
||||
response.pause()
|
||||
await fs.remove(downloadPath)
|
||||
|
||||
const decompress = zlib.createGunzip()
|
||||
response.pipe(decompress as Writable)
|
||||
response.on("error", (error) => decompress.destroy(error))
|
||||
response.on("close", () => decompress.end())
|
||||
|
||||
const destination = tar.extract(downloadPath)
|
||||
decompress.pipe(destination)
|
||||
decompress.on("error", (error) => destination.destroy(error))
|
||||
decompress.on("close", () => destination.end())
|
||||
|
||||
await new Promise((resolve, reject) => {
|
||||
destination.on("finish", resolve)
|
||||
destination.on("error", reject)
|
||||
response.resume()
|
||||
})
|
||||
|
||||
return downloadPath
|
||||
}
|
||||
|
||||
/**
|
||||
* Given an update return the name for the packaged archived.
|
||||
*/
|
||||
public async getReleaseName(update: Update): Promise<string> {
|
||||
let target: string = os.platform()
|
||||
if (target === "linux") {
|
||||
const result = await util
|
||||
.promisify(cp.exec)("ldd --version")
|
||||
.catch((error) => ({
|
||||
stderr: error.message,
|
||||
stdout: "",
|
||||
}))
|
||||
if (/musl/.test(result.stderr) || /musl/.test(result.stdout)) {
|
||||
target = "alpine"
|
||||
}
|
||||
}
|
||||
let arch = os.arch()
|
||||
if (arch === "x64") {
|
||||
arch = "x86_64"
|
||||
}
|
||||
return `code-server-${update.version}-${target}-${arch}.tar.gz`
|
||||
}
|
||||
|
||||
private async request(uri: string): Promise<Buffer> {
|
||||
const response = await this.requestResponse(uri)
|
||||
return new Promise((resolve, reject) => {
|
||||
|
||||
@@ -14,7 +14,7 @@ import {
|
||||
WorkbenchOptions,
|
||||
} from "../../../lib/vscode/src/vs/server/ipc"
|
||||
import { HttpCode, HttpError } from "../../common/http"
|
||||
import { generateUuid } from "../../common/util"
|
||||
import { arrayify, generateUuid } from "../../common/util"
|
||||
import { Args } from "../cli"
|
||||
import { HttpProvider, HttpProviderOptions, HttpResponse, Route } from "../http"
|
||||
import { settings } from "../settings"
|
||||
@@ -50,12 +50,15 @@ export class VscodeHttpProvider extends HttpProvider {
|
||||
|
||||
logger.debug("setting up vs code...")
|
||||
return new Promise<WorkbenchOptions>((resolve, reject) => {
|
||||
vscode.once("message", (message: VscodeMessage) => {
|
||||
logger.debug("got message from vs code", field("message", message))
|
||||
return message.type === "options" && message.id === id
|
||||
? resolve(message.options)
|
||||
: reject(new Error("Unexpected response during initialization"))
|
||||
})
|
||||
const onMessage = (message: VscodeMessage) => {
|
||||
// There can be parallel initializations so wait for the right ID.
|
||||
if (message.type === "options" && message.id === id) {
|
||||
logger.trace("got message from vs code", field("message", message))
|
||||
vscode.off("message", onMessage)
|
||||
resolve(message.options)
|
||||
}
|
||||
}
|
||||
vscode.on("message", onMessage)
|
||||
vscode.once("error", reject)
|
||||
vscode.once("exit", (code) => reject(new Error(`VS Code exited unexpectedly with code ${code}`)))
|
||||
this.send({ type: "init", id, options }, vscode)
|
||||
@@ -77,7 +80,7 @@ export class VscodeHttpProvider extends HttpProvider {
|
||||
|
||||
this._vscode = new Promise((resolve, reject) => {
|
||||
vscode.once("message", (message: VscodeMessage) => {
|
||||
logger.debug("got message from vs code", field("message", message))
|
||||
logger.trace("got message from vs code", field("message", message))
|
||||
return message.type === "ready"
|
||||
? resolve(vscode)
|
||||
: reject(new Error("Unexpected response waiting for ready response"))
|
||||
@@ -131,7 +134,7 @@ export class VscodeHttpProvider extends HttpProvider {
|
||||
if (!this.isRoot(route)) {
|
||||
throw new HttpError("Not found", HttpCode.NotFound)
|
||||
} else if (!this.authenticated(request)) {
|
||||
return { redirect: "/login", query: { to: this.options.base } }
|
||||
return { redirect: "/login", query: { to: route.providerBase } }
|
||||
}
|
||||
try {
|
||||
return await this.getRoot(request, route)
|
||||
@@ -183,11 +186,10 @@ export class VscodeHttpProvider extends HttpProvider {
|
||||
}),
|
||||
])
|
||||
|
||||
if (startPath) {
|
||||
settings.write({
|
||||
lastVisited: startPath,
|
||||
})
|
||||
}
|
||||
settings.write({
|
||||
lastVisited: startPath || lastVisited, // If startpath is undefined, then fallback to lastVisited
|
||||
query: route.query,
|
||||
})
|
||||
|
||||
if (!this.isDev) {
|
||||
response.content = response.content.replace(/<!-- PROD_ONLY/g, "").replace(/END_PROD_ONLY -->/g, "")
|
||||
@@ -201,8 +203,6 @@ export class VscodeHttpProvider extends HttpProvider {
|
||||
.replace(`"{{WORKBENCH_WEB_CONFIGURATION}}"`, `'${JSON.stringify(options.workbenchWebConfiguration)}'`)
|
||||
.replace(`"{{NLS_CONFIGURATION}}"`, `'${JSON.stringify(options.nlsConfiguration)}'`)
|
||||
return this.replaceTemplates<Options>(route, response, {
|
||||
base: this.base(route),
|
||||
commit: this.options.commit,
|
||||
disableTelemetry: !!this.args["disable-telemetry"],
|
||||
})
|
||||
}
|
||||
@@ -224,8 +224,7 @@ export class VscodeHttpProvider extends HttpProvider {
|
||||
}
|
||||
for (let i = 0; i < startPaths.length; ++i) {
|
||||
const startPath = startPaths[i]
|
||||
const url =
|
||||
startPath && (typeof startPath.url === "string" ? [startPath.url] : startPath.url || []).find((p) => !!p)
|
||||
const url = arrayify(startPath && startPath.url).find((p) => !!p)
|
||||
if (startPath && url) {
|
||||
return {
|
||||
url,
|
||||
|
||||
173
src/node/cli.ts
173
src/node/cli.ts
@@ -5,7 +5,7 @@ import * as os from "os"
|
||||
import * as path from "path"
|
||||
import { Args as VsArgs } from "../../lib/vscode/src/vs/server/ipc"
|
||||
import { AuthType } from "./http"
|
||||
import { generatePassword, humanPath, paths } from "./util"
|
||||
import { canConnect, generatePassword, humanPath, paths } from "./util"
|
||||
|
||||
export class Optional<T> {
|
||||
public constructor(public readonly value?: T) {}
|
||||
@@ -45,6 +45,10 @@ export interface Args extends VsArgs {
|
||||
readonly "proxy-domain"?: string[]
|
||||
readonly locale?: string
|
||||
readonly _: string[]
|
||||
readonly "reuse-window"?: boolean
|
||||
readonly "new-window"?: boolean
|
||||
|
||||
readonly link?: OptionalString
|
||||
}
|
||||
|
||||
interface Option<T> {
|
||||
@@ -61,6 +65,11 @@ interface Option<T> {
|
||||
* Description of the option. Leave blank to hide the option.
|
||||
*/
|
||||
description?: string
|
||||
|
||||
/**
|
||||
* If marked as beta, the option is not printed unless $CS_BETA is set.
|
||||
*/
|
||||
beta?: boolean
|
||||
}
|
||||
|
||||
type OptionType<T> = T extends boolean
|
||||
@@ -125,14 +134,47 @@ const options: Options<Required<Args>> = {
|
||||
"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." },
|
||||
"install-extension": { type: "string[]", description: "Install or update a VS Code extension by id or vsix." },
|
||||
"install-extension": {
|
||||
type: "string[]",
|
||||
description:
|
||||
"Install or update a VS Code extension by id or vsix. The identifier of an extension is `${publisher}.${name}`.\n" +
|
||||
"To install a specific version provide `@${version}`. For example: 'vscode.csharp@1.2.3'.",
|
||||
},
|
||||
"enable-proposed-api": {
|
||||
type: "string[]",
|
||||
description:
|
||||
"Enable proposed API features for extensions. Can receive one or more extension IDs to enable individually.",
|
||||
},
|
||||
"uninstall-extension": { type: "string[]", description: "Uninstall a VS Code extension by id." },
|
||||
"show-versions": { type: "boolean", description: "Show VS Code extension versions." },
|
||||
"proxy-domain": { type: "string[]", description: "Domain used for proxying ports." },
|
||||
|
||||
"new-window": {
|
||||
type: "boolean",
|
||||
short: "n",
|
||||
description: "Force to open a new window.",
|
||||
},
|
||||
"reuse-window": {
|
||||
type: "boolean",
|
||||
short: "r",
|
||||
description: "Force to open a file or folder in an already opened window.",
|
||||
},
|
||||
|
||||
locale: { type: "string" },
|
||||
log: { type: LogLevel },
|
||||
verbose: { type: "boolean", short: "vvv", description: "Enable verbose logging." },
|
||||
|
||||
link: {
|
||||
type: OptionalString,
|
||||
description: `
|
||||
Securely bind code-server via Coder Cloud with the passed name. You'll get a URL like
|
||||
https://myname.coder-cloud.com at which you can easily access your code-server instance.
|
||||
Authorization is done via GitHub.
|
||||
This is presently beta and requires being accepted for testing.
|
||||
See https://github.com/cdr/code-server/discussions/2137
|
||||
`,
|
||||
beta: true,
|
||||
},
|
||||
}
|
||||
|
||||
export const optionDescriptions = (): string[] => {
|
||||
@@ -144,12 +186,32 @@ export const optionDescriptions = (): string[] => {
|
||||
}),
|
||||
{ short: 0, long: 0 },
|
||||
)
|
||||
return entries.map(
|
||||
([k, v]) =>
|
||||
`${" ".repeat(widths.short - (v.short ? v.short.length : 0))}${v.short ? `-${v.short}` : " "} --${k}${" ".repeat(
|
||||
widths.long - k.length,
|
||||
)} ${v.description}${typeof v.type === "object" ? ` [${Object.values(v.type).join(", ")}]` : ""}`,
|
||||
)
|
||||
return entries
|
||||
.filter(([, v]) => {
|
||||
// If CS_BETA is set, we show beta options but if not, then we do not want
|
||||
// to show beta options.
|
||||
return process.env.CS_BETA || !v.beta
|
||||
})
|
||||
.map(([k, v]) => {
|
||||
const help = `${" ".repeat(widths.short - (v.short ? v.short.length : 0))}${
|
||||
v.short ? `-${v.short}` : " "
|
||||
} --${k} `
|
||||
return (
|
||||
help +
|
||||
v.description
|
||||
?.trim()
|
||||
.split(/\n/)
|
||||
.map((line, i) => {
|
||||
line = line.trim()
|
||||
if (i === 0) {
|
||||
return " ".repeat(widths.long - k.length) + line
|
||||
}
|
||||
return " ".repeat(widths.long + widths.short + 6) + line
|
||||
})
|
||||
.join("\n") +
|
||||
(typeof v.type === "object" ? ` [${Object.values(v.type).join(", ")}]` : "")
|
||||
)
|
||||
})
|
||||
}
|
||||
|
||||
export const parse = (
|
||||
@@ -172,7 +234,7 @@ export const parse = (
|
||||
const arg = argv[i]
|
||||
|
||||
// -- signals the end of option parsing.
|
||||
if (!ended && arg == "--") {
|
||||
if (!ended && arg === "--") {
|
||||
ended = true
|
||||
continue
|
||||
}
|
||||
@@ -220,7 +282,7 @@ export const parse = (
|
||||
throw error(`--${key} requires a value`)
|
||||
}
|
||||
|
||||
if (option.type == OptionalString && value == "false") {
|
||||
if (option.type === OptionalString && value === "false") {
|
||||
continue
|
||||
}
|
||||
|
||||
@@ -265,6 +327,21 @@ export const parse = (
|
||||
|
||||
logger.debug("parsed command line", field("args", args))
|
||||
|
||||
return args
|
||||
}
|
||||
|
||||
export async function setDefaults(args: Args): Promise<Args> {
|
||||
args = { ...args }
|
||||
|
||||
if (!args["user-data-dir"]) {
|
||||
await copyOldMacOSDataDir()
|
||||
args["user-data-dir"] = paths.data
|
||||
}
|
||||
|
||||
if (!args["extensions-dir"]) {
|
||||
args["extensions-dir"] = path.join(args["user-data-dir"], "extensions")
|
||||
}
|
||||
|
||||
// --verbose takes priority over --log and --log takes priority over the
|
||||
// environment variable.
|
||||
if (args.verbose) {
|
||||
@@ -307,21 +384,6 @@ export const parse = (
|
||||
return args
|
||||
}
|
||||
|
||||
export async function setDefaults(args: Args): Promise<Args> {
|
||||
args = { ...args }
|
||||
|
||||
if (!args["user-data-dir"]) {
|
||||
await copyOldMacOSDataDir()
|
||||
args["user-data-dir"] = paths.data
|
||||
}
|
||||
|
||||
if (!args["extensions-dir"]) {
|
||||
args["extensions-dir"] = path.join(args["user-data-dir"], "extensions")
|
||||
}
|
||||
|
||||
return args
|
||||
}
|
||||
|
||||
async function defaultConfigFile(): Promise<string> {
|
||||
return `bind-addr: 127.0.0.1:8080
|
||||
auth: password
|
||||
@@ -348,14 +410,13 @@ export async function readConfigFile(configPath?: string): Promise<Args> {
|
||||
logger.info(`Wrote default config file to ${humanPath(configPath)}`)
|
||||
}
|
||||
|
||||
if (!process.env.CODE_SERVER_PARENT_PID) {
|
||||
logger.info(`Using config file ${humanPath(configPath)}`)
|
||||
}
|
||||
|
||||
const configFile = await fs.readFile(configPath)
|
||||
const config = yaml.safeLoad(configFile.toString(), {
|
||||
filename: configPath,
|
||||
})
|
||||
if (!config || typeof config === "string") {
|
||||
throw new Error(`invalid config: ${config}`)
|
||||
}
|
||||
|
||||
// We convert the config file into a set of flags.
|
||||
// This is a temporary measure until we add a proper CLI library.
|
||||
@@ -376,7 +437,10 @@ export async function readConfigFile(configPath?: string): Promise<Args> {
|
||||
|
||||
function parseBindAddr(bindAddr: string): [string, number] {
|
||||
const u = new URL(`http://${bindAddr}`)
|
||||
return [u.hostname, parseInt(u.port, 10)]
|
||||
// With the http scheme 80 will be dropped so assume it's 80 if missing. This
|
||||
// means --bind-addr <addr> without a port will default to 80 as well and not
|
||||
// the code-server default.
|
||||
return [u.hostname, u.port ? parseInt(u.port, 10) : 80]
|
||||
}
|
||||
|
||||
interface Addr {
|
||||
@@ -428,3 +492,52 @@ async function copyOldMacOSDataDir(): Promise<void> {
|
||||
await fs.copy(oldDataDir, paths.data)
|
||||
}
|
||||
}
|
||||
|
||||
export const shouldRunVsCodeCli = (args: Args): boolean => {
|
||||
return !!args["list-extensions"] || !!args["install-extension"] || !!args["uninstall-extension"]
|
||||
}
|
||||
|
||||
/**
|
||||
* Determine if it looks like the user is trying to open a file or folder in an
|
||||
* existing instance. The arguments here should be the arguments the user
|
||||
* explicitly passed on the command line, not defaults or the configuration.
|
||||
*/
|
||||
export const shouldOpenInExistingInstance = async (args: Args): Promise<string | undefined> => {
|
||||
// Always use the existing instance if we're running from VS Code's terminal.
|
||||
if (process.env.VSCODE_IPC_HOOK_CLI) {
|
||||
return process.env.VSCODE_IPC_HOOK_CLI
|
||||
}
|
||||
|
||||
const readSocketPath = async (): Promise<string | undefined> => {
|
||||
try {
|
||||
return await fs.readFile(path.join(os.tmpdir(), "vscode-ipc"), "utf8")
|
||||
} catch (error) {
|
||||
if (error.code !== "ENOENT") {
|
||||
throw error
|
||||
}
|
||||
}
|
||||
return undefined
|
||||
}
|
||||
|
||||
// If these flags are set then assume the user is trying to open in an
|
||||
// existing instance since these flags have no effect otherwise.
|
||||
const openInFlagCount = ["reuse-window", "new-window"].reduce((prev, cur) => {
|
||||
return args[cur as keyof Args] ? prev + 1 : prev
|
||||
}, 0)
|
||||
if (openInFlagCount > 0) {
|
||||
return readSocketPath()
|
||||
}
|
||||
|
||||
// It's possible the user is trying to spawn another instance of code-server.
|
||||
// Check if any unrelated flags are set (check against one because `_` always
|
||||
// exists), that a file or directory was passed, and that the socket is
|
||||
// active.
|
||||
if (Object.keys(args).length === 1 && args._.length > 0) {
|
||||
const socketPath = await readSocketPath()
|
||||
if (socketPath && (await canConnect(socketPath))) {
|
||||
return socketPath
|
||||
}
|
||||
}
|
||||
|
||||
return undefined
|
||||
}
|
||||
|
||||
43
src/node/coder-cloud.ts
Normal file
43
src/node/coder-cloud.ts
Normal file
@@ -0,0 +1,43 @@
|
||||
import { logger } from "@coder/logger"
|
||||
import { spawn } from "child_process"
|
||||
import path from "path"
|
||||
import split2 from "split2"
|
||||
|
||||
// https://github.com/cdr/coder-cloud
|
||||
const coderCloudAgent = path.resolve(__dirname, "../../lib/coder-cloud-agent")
|
||||
|
||||
function runAgent(...args: string[]): Promise<void> {
|
||||
logger.debug(`running agent with ${args}`)
|
||||
|
||||
const agent = spawn(coderCloudAgent, args, {
|
||||
stdio: ["inherit", "inherit", "pipe"],
|
||||
})
|
||||
|
||||
agent.stderr.pipe(split2()).on("data", (line) => {
|
||||
line = line.replace(/^[0-9-]+ [0-9:]+ [^ ]+\t/, "")
|
||||
logger.info(line)
|
||||
})
|
||||
|
||||
return new Promise((res, rej) => {
|
||||
agent.on("error", rej)
|
||||
|
||||
agent.on("close", (code) => {
|
||||
if (code !== 0) {
|
||||
rej({
|
||||
message: `coder cloud agent exited with ${code}`,
|
||||
})
|
||||
return
|
||||
}
|
||||
res()
|
||||
})
|
||||
})
|
||||
}
|
||||
|
||||
export function coderCloudBind(csAddr: string, serverName = ""): Promise<void> {
|
||||
logger.info("Remember --link is a beta feature and requires being accepted for testing")
|
||||
logger.info("See https://github.com/cdr/code-server/discussions/2137")
|
||||
// addr needs to be in host:port format.
|
||||
// So we trim the protocol.
|
||||
csAddr = csAddr.replace(/^https?:\/\//, "")
|
||||
return runAgent("bind", `--code-server-addr=${csAddr}`, serverName)
|
||||
}
|
||||
@@ -1,25 +1,31 @@
|
||||
import { field, logger } from "@coder/logger"
|
||||
import * as cp from "child_process"
|
||||
import { promises as fs } from "fs"
|
||||
import http from "http"
|
||||
import * as path from "path"
|
||||
import { CliMessage } from "../../lib/vscode/src/vs/server/ipc"
|
||||
import { ApiHttpProvider } from "./app/api"
|
||||
import { DashboardHttpProvider } from "./app/dashboard"
|
||||
import { CliMessage, OpenCommandPipeArgs } from "../../lib/vscode/src/vs/server/ipc"
|
||||
import { plural } from "../common/util"
|
||||
import { HealthHttpProvider } from "./app/health"
|
||||
import { LoginHttpProvider } from "./app/login"
|
||||
import { ProxyHttpProvider } from "./app/proxy"
|
||||
import { StaticHttpProvider } from "./app/static"
|
||||
import { UpdateHttpProvider } from "./app/update"
|
||||
import { VscodeHttpProvider } from "./app/vscode"
|
||||
import { Args, bindAddrFromAllSources, optionDescriptions, parse, readConfigFile, setDefaults } from "./cli"
|
||||
import {
|
||||
Args,
|
||||
bindAddrFromAllSources,
|
||||
optionDescriptions,
|
||||
parse,
|
||||
readConfigFile,
|
||||
setDefaults,
|
||||
shouldOpenInExistingInstance,
|
||||
shouldRunVsCodeCli,
|
||||
} from "./cli"
|
||||
import { coderCloudBind } from "./coder-cloud"
|
||||
import { AuthType, HttpServer, HttpServerOptions } from "./http"
|
||||
import { generateCertificate, hash, open, humanPath } from "./util"
|
||||
import { ipcMain, wrap } from "./wrapper"
|
||||
|
||||
process.on("uncaughtException", (error) => {
|
||||
logger.error(`Uncaught exception: ${error.message}`)
|
||||
if (typeof error.stack !== "undefined") {
|
||||
logger.error(error.stack)
|
||||
}
|
||||
})
|
||||
import { loadPlugins } from "./plugin"
|
||||
import { generateCertificate, hash, humanPath, open } from "./util"
|
||||
import { ipcMain, WrapperProcess } from "./wrapper"
|
||||
|
||||
let pkg: { version?: string; commit?: string } = {}
|
||||
try {
|
||||
@@ -31,7 +37,100 @@ try {
|
||||
const version = pkg.version || "development"
|
||||
const commit = pkg.commit || "development"
|
||||
|
||||
const main = async (args: Args, cliArgs: Args, configArgs: Args): Promise<void> => {
|
||||
export const runVsCodeCli = (args: Args): void => {
|
||||
logger.debug("forking vs code cli...")
|
||||
const vscode = cp.fork(path.resolve(__dirname, "../../lib/vscode/out/vs/server/fork"), [], {
|
||||
env: {
|
||||
...process.env,
|
||||
CODE_SERVER_PARENT_PID: process.pid.toString(),
|
||||
},
|
||||
})
|
||||
vscode.once("message", (message: any) => {
|
||||
logger.debug("got message from VS Code", field("message", message))
|
||||
if (message.type !== "ready") {
|
||||
logger.error("Unexpected response waiting for ready response", field("type", message.type))
|
||||
process.exit(1)
|
||||
}
|
||||
const send: CliMessage = { type: "cli", args }
|
||||
vscode.send(send)
|
||||
})
|
||||
vscode.once("error", (error) => {
|
||||
logger.error("Got error from VS Code", field("error", error))
|
||||
process.exit(1)
|
||||
})
|
||||
vscode.on("exit", (code) => process.exit(code || 0))
|
||||
}
|
||||
|
||||
export const openInExistingInstance = async (args: Args, socketPath: string): Promise<void> => {
|
||||
const pipeArgs: OpenCommandPipeArgs & { fileURIs: string[] } = {
|
||||
type: "open",
|
||||
folderURIs: [],
|
||||
fileURIs: [],
|
||||
forceReuseWindow: args["reuse-window"],
|
||||
forceNewWindow: args["new-window"],
|
||||
}
|
||||
|
||||
const isDir = async (path: string): Promise<boolean> => {
|
||||
try {
|
||||
const st = await fs.stat(path)
|
||||
return st.isDirectory()
|
||||
} catch (error) {
|
||||
return false
|
||||
}
|
||||
}
|
||||
|
||||
for (let i = 0; i < args._.length; i++) {
|
||||
const fp = path.resolve(args._[i])
|
||||
if (await isDir(fp)) {
|
||||
pipeArgs.folderURIs.push(fp)
|
||||
} else {
|
||||
pipeArgs.fileURIs.push(fp)
|
||||
}
|
||||
}
|
||||
|
||||
if (pipeArgs.forceNewWindow && pipeArgs.fileURIs.length > 0) {
|
||||
logger.error("--new-window can only be used with folder paths")
|
||||
process.exit(1)
|
||||
}
|
||||
|
||||
if (pipeArgs.folderURIs.length === 0 && pipeArgs.fileURIs.length === 0) {
|
||||
logger.error("Please specify at least one file or folder")
|
||||
process.exit(1)
|
||||
}
|
||||
|
||||
const vscode = http.request(
|
||||
{
|
||||
path: "/",
|
||||
method: "POST",
|
||||
socketPath,
|
||||
},
|
||||
(response) => {
|
||||
response.on("data", (message) => {
|
||||
logger.debug("got message from VS Code", field("message", message.toString()))
|
||||
})
|
||||
},
|
||||
)
|
||||
vscode.on("error", (error: unknown) => {
|
||||
logger.error("got error from VS Code", field("error", error))
|
||||
})
|
||||
vscode.write(JSON.stringify(pipeArgs))
|
||||
vscode.end()
|
||||
}
|
||||
|
||||
const main = async (args: Args, configArgs: Args): Promise<void> => {
|
||||
if (args.link) {
|
||||
// If we're being exposed to the cloud, we listen on a random address and disable auth.
|
||||
args = {
|
||||
...args,
|
||||
host: "localhost",
|
||||
port: 0,
|
||||
auth: AuthType.None,
|
||||
socket: undefined,
|
||||
cert: undefined,
|
||||
}
|
||||
logger.info("link: disabling auth and listening on random localhost port for cloud agent")
|
||||
}
|
||||
|
||||
if (!args.auth) {
|
||||
args = {
|
||||
...args,
|
||||
@@ -48,7 +147,7 @@ const main = async (args: Args, cliArgs: Args, configArgs: Args): Promise<void>
|
||||
if (args.auth === AuthType.Password && !password) {
|
||||
throw new Error("Please pass in a password via the config file or $PASSWORD")
|
||||
}
|
||||
const [host, port] = bindAddrFromAllSources(cliArgs, configArgs)
|
||||
const [host, port] = bindAddrFromAllSources(args, configArgs)
|
||||
|
||||
// Spawn the main HTTP server.
|
||||
const options: HttpServerOptions = {
|
||||
@@ -73,17 +172,24 @@ const main = async (args: Args, cliArgs: Args, configArgs: Args): Promise<void>
|
||||
}
|
||||
|
||||
const httpServer = new HttpServer(options)
|
||||
const vscode = httpServer.registerHttpProvider("/", VscodeHttpProvider, args)
|
||||
const api = httpServer.registerHttpProvider("/api", ApiHttpProvider, httpServer, vscode, args["user-data-dir"])
|
||||
const update = httpServer.registerHttpProvider("/update", UpdateHttpProvider, false)
|
||||
httpServer.registerHttpProvider(["/", "/vscode"], VscodeHttpProvider, args)
|
||||
httpServer.registerHttpProvider("/update", UpdateHttpProvider, false)
|
||||
httpServer.registerHttpProvider("/proxy", ProxyHttpProvider)
|
||||
httpServer.registerHttpProvider("/login", LoginHttpProvider, args.config!, envPassword)
|
||||
httpServer.registerHttpProvider("/static", StaticHttpProvider)
|
||||
httpServer.registerHttpProvider("/dashboard", DashboardHttpProvider, api, update)
|
||||
httpServer.registerHttpProvider("/healthz", HealthHttpProvider, httpServer.heart)
|
||||
|
||||
ipcMain().onDispose(() => httpServer.dispose())
|
||||
await loadPlugins(httpServer, args)
|
||||
|
||||
ipcMain.onDispose(() => {
|
||||
httpServer.dispose().then((errors) => {
|
||||
errors.forEach((error) => logger.error(error.message))
|
||||
})
|
||||
})
|
||||
|
||||
logger.info(`code-server ${version} ${commit}`)
|
||||
logger.info(`Using config file ${humanPath(args.config)}`)
|
||||
|
||||
const serverAddress = await httpServer.listen()
|
||||
logger.info(`HTTP server listening on ${serverAddress}`)
|
||||
|
||||
@@ -110,34 +216,45 @@ const main = async (args: Args, cliArgs: Args, configArgs: Args): Promise<void>
|
||||
}
|
||||
|
||||
if (httpServer.proxyDomains.size > 0) {
|
||||
logger.info(` - Proxying the following domain${httpServer.proxyDomains.size === 1 ? "" : "s"}:`)
|
||||
logger.info(` - ${plural(httpServer.proxyDomains.size, "Proxying the following domain")}:`)
|
||||
httpServer.proxyDomains.forEach((domain) => logger.info(` - *.${domain}`))
|
||||
}
|
||||
|
||||
if (serverAddress && !options.socket && args.open) {
|
||||
// The web socket doesn't seem to work if browsing with 0.0.0.0.
|
||||
const openAddress = serverAddress.replace(/:\/\/0.0.0.0/, "://localhost")
|
||||
await open(openAddress).catch(console.error)
|
||||
await open(openAddress).catch((error: Error) => {
|
||||
logger.error("Failed to open", field("address", openAddress), field("error", error))
|
||||
})
|
||||
logger.info(`Opened ${openAddress}`)
|
||||
}
|
||||
|
||||
if (args.link) {
|
||||
try {
|
||||
await coderCloudBind(serverAddress!, args.link.value)
|
||||
} catch (err) {
|
||||
logger.error(err.message)
|
||||
ipcMain.exit(1)
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
async function entry(): Promise<void> {
|
||||
const tryParse = async (): Promise<[Args, Args, Args]> => {
|
||||
try {
|
||||
const cliArgs = parse(process.argv.slice(2))
|
||||
const configArgs = await readConfigFile(cliArgs.config)
|
||||
// This prioritizes the flags set in args over the ones in the config file.
|
||||
let args = Object.assign(configArgs, cliArgs)
|
||||
args = await setDefaults(args)
|
||||
return [args, cliArgs, configArgs]
|
||||
} catch (error) {
|
||||
console.error(error.message)
|
||||
process.exit(1)
|
||||
}
|
||||
const cliArgs = parse(process.argv.slice(2))
|
||||
const configArgs = await readConfigFile(cliArgs.config)
|
||||
// This prioritizes the flags set in args over the ones in the config file.
|
||||
let args = Object.assign(configArgs, cliArgs)
|
||||
args = await setDefaults(args)
|
||||
|
||||
// There's no need to check flags like --help or to spawn in an existing
|
||||
// instance for the child process because these would have already happened in
|
||||
// the parent and the child wouldn't have been spawned.
|
||||
if (ipcMain.isChild) {
|
||||
await ipcMain.handshake()
|
||||
ipcMain.preventExit()
|
||||
return main(args, configArgs)
|
||||
}
|
||||
|
||||
const [args, cliArgs, configArgs] = await tryParse()
|
||||
if (args.help) {
|
||||
console.log("code-server", version, commit)
|
||||
console.log("")
|
||||
@@ -147,7 +264,10 @@ async function entry(): Promise<void> {
|
||||
optionDescriptions().forEach((description) => {
|
||||
console.log("", description)
|
||||
})
|
||||
} else if (args.version) {
|
||||
return
|
||||
}
|
||||
|
||||
if (args.version) {
|
||||
if (args.json) {
|
||||
console.log({
|
||||
codeServer: version,
|
||||
@@ -157,32 +277,23 @@ async function entry(): Promise<void> {
|
||||
} else {
|
||||
console.log(version, commit)
|
||||
}
|
||||
process.exit(0)
|
||||
} else if (args["list-extensions"] || args["install-extension"] || args["uninstall-extension"]) {
|
||||
logger.debug("forking vs code cli...")
|
||||
const vscode = cp.fork(path.resolve(__dirname, "../../lib/vscode/out/vs/server/fork"), [], {
|
||||
env: {
|
||||
...process.env,
|
||||
CODE_SERVER_PARENT_PID: process.pid.toString(),
|
||||
},
|
||||
})
|
||||
vscode.once("message", (message) => {
|
||||
logger.debug("Got message from VS Code", field("message", message))
|
||||
if (message.type !== "ready") {
|
||||
logger.error("Unexpected response waiting for ready response")
|
||||
process.exit(1)
|
||||
}
|
||||
const send: CliMessage = { type: "cli", args }
|
||||
vscode.send(send)
|
||||
})
|
||||
vscode.once("error", (error) => {
|
||||
logger.error(error.message)
|
||||
process.exit(1)
|
||||
})
|
||||
vscode.on("exit", (code) => process.exit(code || 0))
|
||||
} else {
|
||||
wrap(() => main(args, cliArgs, configArgs))
|
||||
return
|
||||
}
|
||||
|
||||
if (shouldRunVsCodeCli(args)) {
|
||||
return runVsCodeCli(args)
|
||||
}
|
||||
|
||||
const socketPath = await shouldOpenInExistingInstance(cliArgs)
|
||||
if (socketPath) {
|
||||
return openInExistingInstance(args, socketPath)
|
||||
}
|
||||
|
||||
const wrapper = new WrapperProcess(require("../../package.json").version)
|
||||
return wrapper.start()
|
||||
}
|
||||
|
||||
entry()
|
||||
entry().catch((error) => {
|
||||
logger.error(error.message)
|
||||
ipcMain.exit(error)
|
||||
})
|
||||
|
||||
197
src/node/http.ts
197
src/node/http.ts
@@ -12,7 +12,7 @@ import { Readable } from "stream"
|
||||
import * as tls from "tls"
|
||||
import * as url from "url"
|
||||
import { HttpCode, HttpError } from "../common/http"
|
||||
import { normalize, Options, plural, split } from "../common/util"
|
||||
import { arrayify, normalize, Options, plural, split, trimSlashes } from "../common/util"
|
||||
import { SocketProxyProvider } from "./socket"
|
||||
import { getMediaMime, paths } from "./util"
|
||||
|
||||
@@ -36,9 +36,13 @@ export type Query = { [key: string]: string | string[] | undefined }
|
||||
|
||||
export interface ProxyOptions {
|
||||
/**
|
||||
* A base path to strip from from the request before proxying if necessary.
|
||||
* A path to strip from from the beginning of the request before proxying
|
||||
*/
|
||||
base?: string
|
||||
strip?: string
|
||||
/**
|
||||
* A path to add to the beginning of the request before proxying.
|
||||
*/
|
||||
prepend?: string
|
||||
/**
|
||||
* The port to proxy.
|
||||
*/
|
||||
@@ -79,9 +83,8 @@ export interface HttpResponse<T = string | Buffer | object> {
|
||||
*/
|
||||
mime?: string
|
||||
/**
|
||||
* Redirect to this path. Will rewrite against the base path but NOT the
|
||||
* provider endpoint so you must include it. This allows redirecting outside
|
||||
* of your endpoint.
|
||||
* Redirect to this path. This is constructed against the site base (not the
|
||||
* provider's base).
|
||||
*/
|
||||
redirect?: string
|
||||
/**
|
||||
@@ -133,12 +136,16 @@ export interface HttpServerOptions {
|
||||
|
||||
export interface Route {
|
||||
/**
|
||||
* Base path part (in /test/path it would be "/test").
|
||||
* Provider base path part (for /provider/base/path it would be /provider).
|
||||
*/
|
||||
providerBase: string
|
||||
/**
|
||||
* Base path part (for /provider/base/path it would be /base).
|
||||
*/
|
||||
base: string
|
||||
/**
|
||||
* Remaining part of the route (in /test/path it would be "/path"). It can be
|
||||
* blank.
|
||||
* Remaining part of the route after factoring out the base and provider base
|
||||
* (for /provider/base/path it would be /path). It can be blank.
|
||||
*/
|
||||
requestPath: string
|
||||
/**
|
||||
@@ -161,7 +168,6 @@ interface ProviderRoute extends Route {
|
||||
|
||||
export interface HttpProviderOptions {
|
||||
readonly auth: AuthType
|
||||
readonly base: string
|
||||
readonly commit: string
|
||||
readonly password?: string
|
||||
}
|
||||
@@ -175,7 +181,7 @@ export abstract class HttpProvider {
|
||||
|
||||
public constructor(protected readonly options: HttpProviderOptions) {}
|
||||
|
||||
public dispose(): void {
|
||||
public async dispose(): Promise<void> {
|
||||
// No default behavior.
|
||||
}
|
||||
|
||||
@@ -203,11 +209,11 @@ export abstract class HttpProvider {
|
||||
/**
|
||||
* Get the base relative to the provided route. For each slash we need to go
|
||||
* up a directory. For example:
|
||||
* / => ./
|
||||
* /foo => ./
|
||||
* /foo/ => ./../
|
||||
* /foo/bar => ./../
|
||||
* /foo/bar/ => ./../../
|
||||
* / => .
|
||||
* /foo => .
|
||||
* /foo/ => ./..
|
||||
* /foo/bar => ./..
|
||||
* /foo/bar/ => ./../..
|
||||
*/
|
||||
public base(route: Route): string {
|
||||
const depth = (route.originalPath.match(/\//g) || []).length
|
||||
@@ -229,30 +235,23 @@ export abstract class HttpProvider {
|
||||
/**
|
||||
* Replace common templates strings.
|
||||
*/
|
||||
protected replaceTemplates(route: Route, response: HttpStringFileResponse, sessionId?: string): HttpStringFileResponse
|
||||
protected replaceTemplates<T extends object>(
|
||||
route: Route,
|
||||
response: HttpStringFileResponse,
|
||||
options: T,
|
||||
): HttpStringFileResponse
|
||||
protected replaceTemplates(
|
||||
route: Route,
|
||||
response: HttpStringFileResponse,
|
||||
sessionIdOrOptions?: string | object,
|
||||
extraOptions?: Omit<T, "base" | "csStaticBase" | "logLevel">,
|
||||
): HttpStringFileResponse {
|
||||
if (typeof sessionIdOrOptions === "undefined" || typeof sessionIdOrOptions === "string") {
|
||||
sessionIdOrOptions = {
|
||||
base: this.base(route),
|
||||
commit: this.options.commit,
|
||||
logLevel: logger.level,
|
||||
sessionID: sessionIdOrOptions,
|
||||
} as Options
|
||||
const base = this.base(route)
|
||||
const options: Options = {
|
||||
base,
|
||||
csStaticBase: base + "/static/" + this.options.commit + this.rootPath,
|
||||
logLevel: logger.level,
|
||||
...extraOptions,
|
||||
}
|
||||
response.content = response.content
|
||||
.replace(/{{COMMIT}}/g, this.options.commit)
|
||||
.replace(/{{TO}}/g, Array.isArray(route.query.to) ? route.query.to[0] : route.query.to || "/dashboard")
|
||||
.replace(/{{BASE}}/g, this.base(route))
|
||||
.replace(/"{{OPTIONS}}"/, `'${JSON.stringify(sessionIdOrOptions)}'`)
|
||||
.replace(/{{BASE}}/g, options.base)
|
||||
.replace(/{{CS_STATIC_BASE}}/g, options.csStaticBase)
|
||||
.replace(/"{{OPTIONS}}"/, `'${JSON.stringify(options)}'`)
|
||||
return response
|
||||
}
|
||||
|
||||
@@ -281,7 +280,7 @@ export abstract class HttpProvider {
|
||||
* Helper to error on invalid methods (default GET).
|
||||
*/
|
||||
protected ensureMethod(request: http.IncomingMessage, method?: string | string[]): void {
|
||||
const check = Array.isArray(method) ? method : [method || "GET"]
|
||||
const check = arrayify(method || "GET")
|
||||
if (!request.method || !check.includes(request.method)) {
|
||||
throw new HttpError(`Unsupported method ${request.method}`, HttpCode.BadRequest)
|
||||
}
|
||||
@@ -290,7 +289,7 @@ export abstract class HttpProvider {
|
||||
/**
|
||||
* Helper to error if not authorized.
|
||||
*/
|
||||
protected ensureAuthenticated(request: http.IncomingMessage): void {
|
||||
public ensureAuthenticated(request: http.IncomingMessage): void {
|
||||
if (!this.authenticated(request)) {
|
||||
throw new HttpError("Unauthorized", HttpCode.Unauthorized)
|
||||
}
|
||||
@@ -397,23 +396,26 @@ export abstract class HttpProvider {
|
||||
export class Heart {
|
||||
private heartbeatTimer?: NodeJS.Timeout
|
||||
private heartbeatInterval = 60000
|
||||
private lastHeartbeat = 0
|
||||
public lastHeartbeat = 0
|
||||
|
||||
public constructor(private readonly heartbeatPath: string, private readonly isActive: () => Promise<boolean>) {}
|
||||
|
||||
public alive(): boolean {
|
||||
const now = Date.now()
|
||||
return now - this.lastHeartbeat < this.heartbeatInterval
|
||||
}
|
||||
/**
|
||||
* Write to the heartbeat file if we haven't already done so within the
|
||||
* timeout and start or reset a timer that keeps running as long as there is
|
||||
* activity. Failures are logged as warnings.
|
||||
*/
|
||||
public beat(): void {
|
||||
const now = Date.now()
|
||||
if (now - this.lastHeartbeat >= this.heartbeatInterval) {
|
||||
if (!this.alive()) {
|
||||
logger.trace("heartbeat")
|
||||
fs.outputFile(this.heartbeatPath, "").catch((error) => {
|
||||
logger.warn(error.message)
|
||||
})
|
||||
this.lastHeartbeat = now
|
||||
this.lastHeartbeat = Date.now()
|
||||
if (typeof this.heartbeatTimer !== "undefined") {
|
||||
clearTimeout(this.heartbeatTimer)
|
||||
}
|
||||
@@ -458,7 +460,7 @@ export class HttpServer {
|
||||
private listenPromise: Promise<string | null> | undefined
|
||||
public readonly protocol: "http" | "https"
|
||||
private readonly providers = new Map<string, HttpProvider>()
|
||||
private readonly heart: Heart
|
||||
public readonly heart: Heart
|
||||
private readonly socketProvider = new SocketProxyProvider()
|
||||
|
||||
/**
|
||||
@@ -475,7 +477,7 @@ export class HttpServer {
|
||||
this.proxyDomains = new Set((options.proxyDomains || []).map((d) => d.replace(/^\*\./, "")))
|
||||
this.heart = new Heart(path.join(paths.data, "heartbeat"), async () => {
|
||||
const connections = await this.getConnections()
|
||||
logger.trace(`${connections} active connection${plural(connections)}`)
|
||||
logger.trace(plural(connections, `${connections} active connection`))
|
||||
return connections !== 0
|
||||
})
|
||||
this.protocol = this.options.cert ? "https" : "http"
|
||||
@@ -502,9 +504,15 @@ export class HttpServer {
|
||||
})
|
||||
}
|
||||
|
||||
public dispose(): void {
|
||||
/**
|
||||
* Stop and dispose everything. Return an array of disposal errors.
|
||||
*/
|
||||
public async dispose(): Promise<Error[]> {
|
||||
this.socketProvider.stop()
|
||||
this.providers.forEach((p) => p.dispose())
|
||||
const providers = Array.from(this.providers.values())
|
||||
// Catch so all the errors can be seen rather than just the first one.
|
||||
const responses = await Promise.all<Error | undefined>(providers.map((p) => p.dispose().catch((e) => e)))
|
||||
return responses.filter<Error>((r): r is Error => typeof r !== "undefined")
|
||||
}
|
||||
|
||||
public async getConnections(): Promise<number> {
|
||||
@@ -518,41 +526,51 @@ export class HttpServer {
|
||||
/**
|
||||
* Register a provider for a top-level endpoint.
|
||||
*/
|
||||
public registerHttpProvider<T extends HttpProvider>(endpoint: string, provider: HttpProvider0<T>): T
|
||||
public registerHttpProvider<A1, T extends HttpProvider>(endpoint: string, provider: HttpProvider1<A1, T>, a1: A1): T
|
||||
public registerHttpProvider<T extends HttpProvider>(endpoint: string | string[], provider: HttpProvider0<T>): T
|
||||
public registerHttpProvider<A1, T extends HttpProvider>(
|
||||
endpoint: string | string[],
|
||||
provider: HttpProvider1<A1, T>,
|
||||
a1: A1,
|
||||
): T
|
||||
public registerHttpProvider<A1, A2, T extends HttpProvider>(
|
||||
endpoint: string,
|
||||
endpoint: string | string[],
|
||||
provider: HttpProvider2<A1, A2, T>,
|
||||
a1: A1,
|
||||
a2: A2,
|
||||
): T
|
||||
public registerHttpProvider<A1, A2, A3, T extends HttpProvider>(
|
||||
endpoint: string,
|
||||
endpoint: string | string[],
|
||||
provider: HttpProvider3<A1, A2, A3, T>,
|
||||
a1: A1,
|
||||
a2: A2,
|
||||
a3: A3,
|
||||
): T
|
||||
// eslint-disable-next-line @typescript-eslint/no-explicit-any
|
||||
public registerHttpProvider(endpoint: string, provider: any, ...args: any[]): any {
|
||||
endpoint = endpoint.replace(/^\/+|\/+$/g, "")
|
||||
if (this.providers.has(`/${endpoint}`)) {
|
||||
throw new Error(`${endpoint} is already registered`)
|
||||
}
|
||||
if (/\//.test(endpoint)) {
|
||||
throw new Error(`Only top-level endpoints are supported (got ${endpoint})`)
|
||||
}
|
||||
public registerHttpProvider(endpoint: string | string[], provider: any, ...args: any[]): void {
|
||||
const p = new provider(
|
||||
{
|
||||
auth: this.options.auth || AuthType.None,
|
||||
base: `/${endpoint}`,
|
||||
commit: this.options.commit,
|
||||
password: this.options.password,
|
||||
},
|
||||
...args,
|
||||
)
|
||||
this.providers.set(`/${endpoint}`, p)
|
||||
return p
|
||||
const endpoints = arrayify(endpoint).map(trimSlashes)
|
||||
endpoints.forEach((endpoint) => {
|
||||
if (/\//.test(endpoint)) {
|
||||
throw new Error(`Only top-level endpoints are supported (got ${endpoint})`)
|
||||
}
|
||||
const existingProvider = this.providers.get(`/${endpoint}`)
|
||||
this.providers.set(`/${endpoint}`, p)
|
||||
if (existingProvider) {
|
||||
logger.debug(`Overridding existing /${endpoint} provider`)
|
||||
// If the existing provider isn't registered elsewhere we can dispose.
|
||||
if (!Array.from(this.providers.values()).find((p) => p === existingProvider)) {
|
||||
logger.debug(`Disposing existing /${endpoint} provider`)
|
||||
existingProvider.dispose()
|
||||
}
|
||||
}
|
||||
})
|
||||
}
|
||||
|
||||
/**
|
||||
@@ -560,14 +578,24 @@ export class HttpServer {
|
||||
*/
|
||||
public listen(): Promise<string | null> {
|
||||
if (!this.listenPromise) {
|
||||
this.listenPromise = new Promise((resolve, reject) => {
|
||||
this.listenPromise = new Promise(async (resolve, reject) => {
|
||||
this.server.on("error", reject)
|
||||
this.server.on("upgrade", this.onUpgrade)
|
||||
const onListen = (): void => resolve(this.address())
|
||||
if (this.options.socket) {
|
||||
try {
|
||||
await fs.unlink(this.options.socket)
|
||||
} catch (err) {
|
||||
if (err.code !== "ENOENT") {
|
||||
logger.warn(err.message)
|
||||
}
|
||||
}
|
||||
this.server.listen(this.options.socket, onListen)
|
||||
} else if (this.options.host) {
|
||||
// [] is the correct format when using :: but Node errors with them.
|
||||
this.server.listen(this.options.port, this.options.host.replace(/^\[|\]$/g, ""), onListen)
|
||||
} else {
|
||||
this.server.listen(this.options.port, this.options.host, onListen)
|
||||
this.server.listen(this.options.port, onListen)
|
||||
}
|
||||
})
|
||||
}
|
||||
@@ -587,8 +615,10 @@ export class HttpServer {
|
||||
}
|
||||
|
||||
private onRequest = async (request: http.IncomingMessage, response: http.ServerResponse): Promise<void> => {
|
||||
this.heart.beat()
|
||||
const route = this.parseUrl(request)
|
||||
if (route.providerBase !== "/healthz") {
|
||||
this.heart.beat()
|
||||
}
|
||||
const write = (payload: HttpResponse): void => {
|
||||
response.writeHead(payload.redirect ? HttpCode.Redirect : payload.code || HttpCode.Ok, {
|
||||
"Content-Type": payload.mime || getMediaMime(payload.filePath),
|
||||
@@ -627,10 +657,7 @@ export class HttpServer {
|
||||
}
|
||||
|
||||
try {
|
||||
const payload =
|
||||
this.maybeRedirect(request, route) ||
|
||||
(route.provider.authenticated(request) && this.maybeProxy(request)) ||
|
||||
(await route.provider.handleRequest(route, request))
|
||||
const payload = (await this.handleRequest(route, request)) || (await route.provider.handleRequest(route, request))
|
||||
if (payload.proxy) {
|
||||
this.doProxy(route, request, response, payload.proxy)
|
||||
} else {
|
||||
@@ -642,15 +669,17 @@ export class HttpServer {
|
||||
e = new HttpError("Not found", HttpCode.NotFound)
|
||||
}
|
||||
const code = typeof e.code === "number" ? e.code : HttpCode.ServerError
|
||||
logger.debug("Request error", field("url", request.url), field("code", code))
|
||||
logger.debug("Request error", field("url", request.url), field("code", code), field("error", error))
|
||||
if (code >= HttpCode.ServerError) {
|
||||
logger.error(error.stack)
|
||||
}
|
||||
if (request.headers["content-type"] === "application/json") {
|
||||
write({
|
||||
code,
|
||||
mime: "application/json",
|
||||
content: {
|
||||
error: e.message,
|
||||
...(e.details || {}),
|
||||
},
|
||||
})
|
||||
} else {
|
||||
@@ -663,15 +692,23 @@ export class HttpServer {
|
||||
}
|
||||
|
||||
/**
|
||||
* Return any necessary redirection before delegating to a provider.
|
||||
* Handle requests that are always in effect no matter what provider is
|
||||
* registered at the route.
|
||||
*/
|
||||
private maybeRedirect(request: http.IncomingMessage, route: ProviderRoute): RedirectResponse | undefined {
|
||||
private async handleRequest(route: ProviderRoute, request: http.IncomingMessage): Promise<HttpResponse | undefined> {
|
||||
// If we're handling TLS ensure all requests are redirected to HTTPS.
|
||||
if (this.options.cert && !(request.connection as tls.TLSSocket).encrypted) {
|
||||
return { redirect: route.fullPath }
|
||||
}
|
||||
|
||||
return undefined
|
||||
// Return robots.txt.
|
||||
if (route.fullPath === "/robots.txt") {
|
||||
const filePath = path.resolve(__dirname, "../../src/browser/robots.txt")
|
||||
return { content: await fs.readFile(filePath), filePath }
|
||||
}
|
||||
|
||||
// Handle proxy domains.
|
||||
return this.maybeProxy(route, request)
|
||||
}
|
||||
|
||||
/**
|
||||
@@ -701,6 +738,8 @@ export class HttpServer {
|
||||
}
|
||||
|
||||
private onUpgrade = async (request: http.IncomingMessage, socket: net.Socket, head: Buffer): Promise<void> => {
|
||||
socket.pause()
|
||||
|
||||
try {
|
||||
this.heart.beat()
|
||||
socket.on("error", () => socket.destroy())
|
||||
@@ -722,7 +761,7 @@ export class HttpServer {
|
||||
// can't be transferred so we need an in-between).
|
||||
const socketProxy = await this.socketProvider.createProxy(socket)
|
||||
const payload =
|
||||
this.maybeProxy(request) || (await route.provider.handleWebSocket(route, request, socketProxy, head))
|
||||
this.maybeProxy(route, request) || (await route.provider.handleWebSocket(route, request, socketProxy, head))
|
||||
if (payload && payload.proxy) {
|
||||
this.doProxy(route, request, { socket: socketProxy, head }, payload.proxy)
|
||||
}
|
||||
@@ -759,7 +798,7 @@ export class HttpServer {
|
||||
// that by shifting the next base out of the request path.
|
||||
let provider = this.providers.get(base)
|
||||
if (base !== "/" && provider) {
|
||||
return { ...parse(requestPath), fullPath, query: parsedUrl.query, provider, originalPath }
|
||||
return { ...parse(requestPath), providerBase: base, fullPath, query: parsedUrl.query, provider, originalPath }
|
||||
}
|
||||
|
||||
// Fall back to the top-level provider.
|
||||
@@ -767,7 +806,7 @@ export class HttpServer {
|
||||
if (!provider) {
|
||||
throw new Error(`No provider for ${base}`)
|
||||
}
|
||||
return { base, fullPath, requestPath, query: parsedUrl.query, provider, originalPath }
|
||||
return { base, providerBase: "/", fullPath, requestPath, query: parsedUrl.query, provider, originalPath }
|
||||
}
|
||||
|
||||
/**
|
||||
@@ -806,10 +845,11 @@ export class HttpServer {
|
||||
// sure how best to get this information to the `proxyRes` event handler.
|
||||
// For now I'm sticking it on the request object which is passed through to
|
||||
// the event.
|
||||
;(request as ProxyRequest).base = options.base
|
||||
;(request as ProxyRequest).base = options.strip
|
||||
|
||||
const isHttp = response instanceof http.ServerResponse
|
||||
const path = options.base ? route.fullPath.replace(options.base, "") : route.fullPath
|
||||
const base = options.strip ? route.fullPath.replace(options.strip, "") : route.fullPath
|
||||
const path = normalize("/" + (options.prepend || "") + "/" + base, true)
|
||||
const proxyOptions: proxy.ServerOptions = {
|
||||
changeOrigin: true,
|
||||
ignorePath: true,
|
||||
@@ -850,6 +890,7 @@ export class HttpServer {
|
||||
// isn't setting the host header to match the access domain.
|
||||
host === "localhost"
|
||||
) {
|
||||
logger.debug("no valid cookie doman", field("host", host))
|
||||
return undefined
|
||||
}
|
||||
|
||||
@@ -859,6 +900,7 @@ export class HttpServer {
|
||||
}
|
||||
})
|
||||
|
||||
logger.debug("got cookie doman", field("host", host))
|
||||
return host ? `Domain=${host}` : undefined
|
||||
}
|
||||
|
||||
@@ -869,8 +911,10 @@ export class HttpServer {
|
||||
*
|
||||
* For example if `coder.com` is specified `8080.coder.com` will be proxied
|
||||
* but `8080.test.coder.com` and `test.8080.coder.com` will not.
|
||||
*
|
||||
* Throw an error if proxying but the user isn't authenticated.
|
||||
*/
|
||||
public maybeProxy(request: http.IncomingMessage): HttpResponse | undefined {
|
||||
public maybeProxy(route: ProviderRoute, request: http.IncomingMessage): HttpResponse | undefined {
|
||||
// Split into parts.
|
||||
const host = request.headers.host || ""
|
||||
const idx = host.indexOf(":")
|
||||
@@ -884,6 +928,9 @@ export class HttpServer {
|
||||
return undefined
|
||||
}
|
||||
|
||||
// Must be authenticated to use the proxy.
|
||||
route.provider.ensureAuthenticated(request)
|
||||
|
||||
return {
|
||||
proxy: {
|
||||
port,
|
||||
|
||||
92
src/node/plugin.ts
Normal file
92
src/node/plugin.ts
Normal file
@@ -0,0 +1,92 @@
|
||||
import { field, logger } from "@coder/logger"
|
||||
import * as fs from "fs"
|
||||
import * as path from "path"
|
||||
import * as util from "util"
|
||||
import { Args } from "./cli"
|
||||
import { HttpServer } from "./http"
|
||||
import { paths } from "./util"
|
||||
|
||||
/* eslint-disable @typescript-eslint/no-var-requires */
|
||||
|
||||
export type Activate = (httpServer: HttpServer, args: Args) => void
|
||||
|
||||
/**
|
||||
* Plugins must implement this interface.
|
||||
*/
|
||||
export interface Plugin {
|
||||
activate: Activate
|
||||
}
|
||||
|
||||
/**
|
||||
* Intercept imports so we can inject code-server when the plugin tries to
|
||||
* import it.
|
||||
*/
|
||||
const originalLoad = require("module")._load
|
||||
// eslint-disable-next-line @typescript-eslint/no-explicit-any
|
||||
require("module")._load = function (request: string, parent: object, isMain: boolean): any {
|
||||
return originalLoad.apply(this, [request.replace(/^code-server/, path.resolve(__dirname, "../..")), parent, isMain])
|
||||
}
|
||||
|
||||
/**
|
||||
* Load a plugin and run its activation function.
|
||||
*/
|
||||
const loadPlugin = async (pluginPath: string, httpServer: HttpServer, args: Args): Promise<void> => {
|
||||
try {
|
||||
const plugin: Plugin = require(pluginPath)
|
||||
plugin.activate(httpServer, args)
|
||||
|
||||
const packageJson = require(path.join(pluginPath, "package.json"))
|
||||
logger.debug(
|
||||
"Loaded plugin",
|
||||
field("name", packageJson.name || path.basename(pluginPath)),
|
||||
field("path", pluginPath),
|
||||
field("version", packageJson.version || "n/a"),
|
||||
)
|
||||
} catch (error) {
|
||||
logger.error(error.message)
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* Load all plugins in the specified directory.
|
||||
*/
|
||||
const _loadPlugins = async (pluginDir: string, httpServer: HttpServer, args: Args): Promise<void> => {
|
||||
try {
|
||||
const files = await util.promisify(fs.readdir)(pluginDir, {
|
||||
withFileTypes: true,
|
||||
})
|
||||
await Promise.all(files.map((file) => loadPlugin(path.join(pluginDir, file.name), httpServer, args)))
|
||||
} catch (error) {
|
||||
if (error.code !== "ENOENT") {
|
||||
logger.warn(error.message)
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* Load all plugins from the `plugins` directory, directories specified by
|
||||
* `CS_PLUGIN_PATH` (colon-separated), and individual plugins specified by
|
||||
* `CS_PLUGIN` (also colon-separated).
|
||||
*/
|
||||
export const loadPlugins = async (httpServer: HttpServer, args: Args): Promise<void> => {
|
||||
const pluginPath = process.env.CS_PLUGIN_PATH || `${path.join(paths.data, "plugins")}:/usr/share/code-server/plugins`
|
||||
const plugin = process.env.CS_PLUGIN || ""
|
||||
await Promise.all([
|
||||
// Built-in plugins.
|
||||
_loadPlugins(path.resolve(__dirname, "../../plugins"), httpServer, args),
|
||||
// User-added plugins.
|
||||
...pluginPath
|
||||
.split(":")
|
||||
.filter((p) => !!p)
|
||||
.map((dir) => _loadPlugins(path.resolve(dir), httpServer, args)),
|
||||
// Individual plugins so you don't have to symlink or move them into a
|
||||
// directory specifically for plugins. This lets you load plugins that are
|
||||
// on the same level as other directories that are not plugins (if you tried
|
||||
// to use CS_PLUGIN_PATH code-server would try to load those other
|
||||
// directories as plugins). Intended for development.
|
||||
...plugin
|
||||
.split(":")
|
||||
.filter((p) => !!p)
|
||||
.map((dir) => loadPlugin(path.resolve(dir), httpServer, args)),
|
||||
])
|
||||
}
|
||||
@@ -1,7 +1,8 @@
|
||||
import { logger } from "@coder/logger"
|
||||
import * as fs from "fs-extra"
|
||||
import * as path from "path"
|
||||
import { extend, paths } from "./util"
|
||||
import { logger } from "@coder/logger"
|
||||
import { Route } from "./http"
|
||||
import { paths } from "./util"
|
||||
|
||||
export type Settings = { [key: string]: Settings | string | boolean | number }
|
||||
|
||||
@@ -29,11 +30,13 @@ export class SettingsProvider<T> {
|
||||
|
||||
/**
|
||||
* Write settings combined with current settings. On failure log a warning.
|
||||
* Objects will be merged and everything else will be replaced.
|
||||
* Settings will be merged shallowly.
|
||||
*/
|
||||
public async write(settings: Partial<T>): Promise<void> {
|
||||
try {
|
||||
await fs.writeFile(this.settingsPath, JSON.stringify(extend(await this.read(), settings), null, 2))
|
||||
const oldSettings = await this.read()
|
||||
const nextSettings = { ...oldSettings, ...settings }
|
||||
await fs.writeFile(this.settingsPath, JSON.stringify(nextSettings, null, 2))
|
||||
} catch (error) {
|
||||
logger.warn(error.message)
|
||||
}
|
||||
@@ -55,6 +58,7 @@ export interface CoderSettings extends UpdateSettings {
|
||||
url: string
|
||||
workspace: boolean
|
||||
}
|
||||
query: Route["query"]
|
||||
}
|
||||
|
||||
/**
|
||||
|
||||
@@ -4,7 +4,7 @@ import * as path from "path"
|
||||
import * as tls from "tls"
|
||||
import { Emitter } from "../common/emitter"
|
||||
import { generateUuid } from "../common/util"
|
||||
import { tmpdir } from "./util"
|
||||
import { canConnect, tmpdir } from "./util"
|
||||
|
||||
/**
|
||||
* Provides a way to proxy a TLS socket. Can be used when you need to pass a
|
||||
@@ -89,17 +89,6 @@ export class SocketProxyProvider {
|
||||
}
|
||||
|
||||
public async findFreeSocketPath(basePath: string, maxTries = 100): Promise<string> {
|
||||
const canConnect = (path: string): Promise<boolean> => {
|
||||
return new Promise((resolve) => {
|
||||
const socket = net.connect(path)
|
||||
socket.once("error", () => resolve(false))
|
||||
socket.once("connect", () => {
|
||||
socket.destroy()
|
||||
resolve(true)
|
||||
})
|
||||
})
|
||||
}
|
||||
|
||||
let i = 0
|
||||
let path = basePath
|
||||
while ((await canConnect(path)) && i < maxTries) {
|
||||
|
||||
@@ -1,10 +1,11 @@
|
||||
import * as cp from "child_process"
|
||||
import * as crypto from "crypto"
|
||||
import envPaths from "env-paths"
|
||||
import * as fs from "fs-extra"
|
||||
import * as net from "net"
|
||||
import * as os from "os"
|
||||
import * as path from "path"
|
||||
import * as util from "util"
|
||||
import envPaths from "env-paths"
|
||||
import xdgBasedir from "xdg-basedir"
|
||||
|
||||
export const tmpdir = path.join(os.tmpdir(), "code-server")
|
||||
@@ -199,25 +200,6 @@ export const isObject = <T extends object>(obj: T): obj is T => {
|
||||
return !Array.isArray(obj) && typeof obj === "object" && obj !== null
|
||||
}
|
||||
|
||||
/**
|
||||
* Extend a with b and return a new object. Properties with objects will be
|
||||
* recursively merged while all other properties are just overwritten.
|
||||
*/
|
||||
export function extend<A, B>(a: A, b: B): A & B
|
||||
// eslint-disable-next-line @typescript-eslint/no-explicit-any
|
||||
export function extend(...args: any[]): any {
|
||||
const c = {} as any // eslint-disable-line @typescript-eslint/no-explicit-any
|
||||
for (const obj of args) {
|
||||
if (!isObject(obj)) {
|
||||
continue
|
||||
}
|
||||
for (const key in obj) {
|
||||
c[key] = isObject(obj[key]) ? extend(c[key], obj[key]) : obj[key]
|
||||
}
|
||||
}
|
||||
return c
|
||||
}
|
||||
|
||||
/**
|
||||
* Taken from vs/base/common/charCode.ts. Copied for now instead of importing so
|
||||
* we don't have to set up a `vs` alias to be able to import with types (since
|
||||
@@ -265,3 +247,17 @@ export function pathToFsPath(path: string, keepDriveLetterCasing = false): strin
|
||||
}
|
||||
return value
|
||||
}
|
||||
|
||||
/**
|
||||
* Return a promise that resolves with whether the socket path is active.
|
||||
*/
|
||||
export function canConnect(path: string): Promise<boolean> {
|
||||
return new Promise((resolve) => {
|
||||
const socket = net.connect(path)
|
||||
socket.once("error", () => resolve(false))
|
||||
socket.once("connect", () => {
|
||||
socket.destroy()
|
||||
resolve(true)
|
||||
})
|
||||
})
|
||||
}
|
||||
|
||||
@@ -1,6 +1,9 @@
|
||||
import { logger, field } from "@coder/logger"
|
||||
import { field, logger } from "@coder/logger"
|
||||
import * as cp from "child_process"
|
||||
import * as path from "path"
|
||||
import * as rfs from "rotating-file-stream"
|
||||
import { Emitter } from "../common/emitter"
|
||||
import { paths } from "./util"
|
||||
|
||||
interface HandshakeMessage {
|
||||
type: "handshake"
|
||||
@@ -29,19 +32,13 @@ export class IpcMain {
|
||||
public readonly onMessage = this._onMessage.event
|
||||
private readonly _onDispose = new Emitter<NodeJS.Signals | undefined>()
|
||||
public readonly onDispose = this._onDispose.event
|
||||
public readonly exit: (code?: number) => never
|
||||
public readonly processExit: (code?: number) => never = process.exit
|
||||
|
||||
public constructor(public readonly parentPid?: number) {
|
||||
public constructor(private readonly parentPid?: number) {
|
||||
process.on("SIGINT", () => this._onDispose.emit("SIGINT"))
|
||||
process.on("SIGTERM", () => this._onDispose.emit("SIGTERM"))
|
||||
process.on("exit", () => this._onDispose.emit(undefined))
|
||||
|
||||
// Ensure we control when the process exits.
|
||||
this.exit = process.exit
|
||||
process.exit = function (code?: number) {
|
||||
logger.warn(`process.exit() was prevented: ${code || "unknown code"}.`)
|
||||
} as (code?: number) => never
|
||||
|
||||
this.onDispose((signal) => {
|
||||
// Remove listeners to avoid possibly triggering disposal again.
|
||||
process.removeAllListeners()
|
||||
@@ -68,6 +65,27 @@ export class IpcMain {
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* Ensure we control when the process exits.
|
||||
*/
|
||||
public preventExit(): void {
|
||||
process.exit = function (code?: number) {
|
||||
logger.warn(`process.exit() was prevented: ${code || "unknown code"}.`)
|
||||
} as (code?: number) => never
|
||||
}
|
||||
|
||||
public get isChild(): boolean {
|
||||
return typeof this.parentPid !== "undefined"
|
||||
}
|
||||
|
||||
public exit(error?: number | ProcessError): never {
|
||||
if (error && typeof error !== "number") {
|
||||
this.processExit(typeof error.code === "number" ? error.code : 1)
|
||||
} else {
|
||||
this.processExit(error)
|
||||
}
|
||||
}
|
||||
|
||||
public handshake(child?: cp.ChildProcess): Promise<void> {
|
||||
return new Promise((resolve, reject) => {
|
||||
const target = child || process
|
||||
@@ -116,17 +134,12 @@ export class IpcMain {
|
||||
}
|
||||
}
|
||||
|
||||
let _ipcMain: IpcMain
|
||||
export const ipcMain = (): IpcMain => {
|
||||
if (!_ipcMain) {
|
||||
_ipcMain = new IpcMain(
|
||||
typeof process.env.CODE_SERVER_PARENT_PID !== "undefined"
|
||||
? parseInt(process.env.CODE_SERVER_PARENT_PID)
|
||||
: undefined,
|
||||
)
|
||||
}
|
||||
return _ipcMain
|
||||
}
|
||||
/**
|
||||
* Channel for communication between the child and parent processes.
|
||||
*/
|
||||
export const ipcMain = new IpcMain(
|
||||
typeof process.env.CODE_SERVER_PARENT_PID !== "undefined" ? parseInt(process.env.CODE_SERVER_PARENT_PID) : undefined,
|
||||
)
|
||||
|
||||
export interface WrapperOptions {
|
||||
maxMemory?: number
|
||||
@@ -140,31 +153,27 @@ export interface WrapperOptions {
|
||||
export class WrapperProcess {
|
||||
private process?: cp.ChildProcess
|
||||
private started?: Promise<void>
|
||||
private readonly logStdoutStream: rfs.RotatingFileStream
|
||||
private readonly logStderrStream: rfs.RotatingFileStream
|
||||
|
||||
public constructor(private currentVersion: string, private readonly options?: WrapperOptions) {
|
||||
ipcMain().onDispose(() => {
|
||||
if (this.process) {
|
||||
this.process.removeAllListeners()
|
||||
this.process.kill()
|
||||
}
|
||||
const opts = {
|
||||
size: "10M",
|
||||
maxFiles: 10,
|
||||
}
|
||||
this.logStdoutStream = rfs.createStream(path.join(paths.data, "coder-logs", "code-server-stdout.log"), opts)
|
||||
this.logStderrStream = rfs.createStream(path.join(paths.data, "coder-logs", "code-server-stderr.log"), opts)
|
||||
|
||||
ipcMain.onDispose(() => {
|
||||
this.disposeChild()
|
||||
})
|
||||
|
||||
ipcMain().onMessage(async (message) => {
|
||||
ipcMain.onMessage((message) => {
|
||||
switch (message.type) {
|
||||
case "relaunch":
|
||||
logger.info(`Relaunching: ${this.currentVersion} -> ${message.version}`)
|
||||
this.currentVersion = message.version
|
||||
this.started = undefined
|
||||
if (this.process) {
|
||||
this.process.removeAllListeners()
|
||||
this.process.kill()
|
||||
}
|
||||
try {
|
||||
await this.start()
|
||||
} catch (error) {
|
||||
logger.error(error.message)
|
||||
ipcMain().exit(typeof error.code === "number" ? error.code : 1)
|
||||
}
|
||||
this.relaunch()
|
||||
break
|
||||
default:
|
||||
logger.error(`Unrecognized message ${message}`)
|
||||
@@ -173,25 +182,63 @@ export class WrapperProcess {
|
||||
})
|
||||
}
|
||||
|
||||
private disposeChild(): void {
|
||||
this.started = undefined
|
||||
if (this.process) {
|
||||
this.process.removeAllListeners()
|
||||
this.process.kill()
|
||||
}
|
||||
}
|
||||
|
||||
private async relaunch(): Promise<void> {
|
||||
this.disposeChild()
|
||||
try {
|
||||
await this.start()
|
||||
} catch (error) {
|
||||
logger.error(error.message)
|
||||
ipcMain.exit(typeof error.code === "number" ? error.code : 1)
|
||||
}
|
||||
}
|
||||
|
||||
public start(): Promise<void> {
|
||||
if (!this.started) {
|
||||
this.started = this.spawn().then((child) => {
|
||||
logger.debug(`spawned inner process ${child.pid}`)
|
||||
ipcMain()
|
||||
.handshake(child)
|
||||
.then(() => {
|
||||
child.once("exit", (code) => {
|
||||
logger.debug(`inner process ${child.pid} exited unexpectedly`)
|
||||
ipcMain().exit(code || 0)
|
||||
})
|
||||
})
|
||||
this.process = child
|
||||
// If we have a process then we've already bound this.
|
||||
if (!this.process) {
|
||||
process.on("SIGUSR1", async () => {
|
||||
logger.info("Received SIGUSR1; hotswapping")
|
||||
this.relaunch()
|
||||
})
|
||||
}
|
||||
if (!this.started) {
|
||||
this.started = this._start()
|
||||
}
|
||||
return this.started
|
||||
}
|
||||
|
||||
private async spawn(): Promise<cp.ChildProcess> {
|
||||
private async _start(): Promise<void> {
|
||||
const child = this.spawn()
|
||||
this.process = child
|
||||
|
||||
// Log both to stdout and to the log directory.
|
||||
if (child.stdout) {
|
||||
child.stdout.pipe(this.logStdoutStream)
|
||||
child.stdout.pipe(process.stdout)
|
||||
}
|
||||
if (child.stderr) {
|
||||
child.stderr.pipe(this.logStderrStream)
|
||||
child.stderr.pipe(process.stderr)
|
||||
}
|
||||
|
||||
logger.debug(`spawned inner process ${child.pid}`)
|
||||
|
||||
await ipcMain.handshake(child)
|
||||
|
||||
child.once("exit", (code) => {
|
||||
logger.debug(`inner process ${child.pid} exited unexpectedly`)
|
||||
ipcMain.exit(code || 0)
|
||||
})
|
||||
}
|
||||
|
||||
private spawn(): cp.ChildProcess {
|
||||
// Flags to pass along to the Node binary.
|
||||
let nodeOptions = `${process.env.NODE_OPTIONS || ""} ${(this.options && this.options.nodeOptions) || ""}`
|
||||
if (!/max_old_space_size=(\d+)/g.exec(nodeOptions)) {
|
||||
@@ -205,7 +252,7 @@ export class WrapperProcess {
|
||||
CODE_SERVER_PARENT_PID: process.pid.toString(),
|
||||
NODE_OPTIONS: nodeOptions,
|
||||
},
|
||||
stdio: ["inherit", "inherit", "inherit", "ipc"],
|
||||
stdio: ["ipc"],
|
||||
})
|
||||
}
|
||||
}
|
||||
@@ -213,23 +260,13 @@ export class WrapperProcess {
|
||||
// It's possible that the pipe has closed (for example if you run code-server
|
||||
// --version | head -1). Assume that means we're done.
|
||||
if (!process.stdout.isTTY) {
|
||||
process.stdout.on("error", () => ipcMain().exit())
|
||||
process.stdout.on("error", () => ipcMain.exit())
|
||||
}
|
||||
|
||||
export const wrap = (fn: () => Promise<void>): void => {
|
||||
if (ipcMain().parentPid) {
|
||||
ipcMain()
|
||||
.handshake()
|
||||
.then(() => fn())
|
||||
.catch((error: ProcessError): void => {
|
||||
logger.error(error.message)
|
||||
ipcMain().exit(typeof error.code === "number" ? error.code : 1)
|
||||
})
|
||||
} else {
|
||||
const wrapper = new WrapperProcess(require("../../package.json").version)
|
||||
wrapper.start().catch((error) => {
|
||||
logger.error(error.message)
|
||||
ipcMain().exit(typeof error.code === "number" ? error.code : 1)
|
||||
})
|
||||
// Don't let uncaught exceptions crash the process.
|
||||
process.on("uncaughtException", (error) => {
|
||||
logger.error(`Uncaught exception: ${error.message}`)
|
||||
if (typeof error.stack !== "undefined") {
|
||||
logger.error(error.stack)
|
||||
}
|
||||
}
|
||||
})
|
||||
|
||||
152
test/cli.test.ts
152
test/cli.test.ts
@@ -1,20 +1,31 @@
|
||||
import { logger, Level } from "@coder/logger"
|
||||
import { Level, logger } from "@coder/logger"
|
||||
import * as assert from "assert"
|
||||
import * as fs from "fs-extra"
|
||||
import * as net from "net"
|
||||
import * as os from "os"
|
||||
import * as path from "path"
|
||||
import { parse } from "../src/node/cli"
|
||||
import { Args, parse, setDefaults, shouldOpenInExistingInstance } from "../src/node/cli"
|
||||
import { paths, tmpdir } from "../src/node/util"
|
||||
|
||||
describe("cli", () => {
|
||||
type Mutable<T> = {
|
||||
-readonly [P in keyof T]: T[P]
|
||||
}
|
||||
|
||||
describe("parser", () => {
|
||||
beforeEach(() => {
|
||||
delete process.env.LOG_LEVEL
|
||||
})
|
||||
|
||||
// The parser will always fill these out.
|
||||
// The parser should not set any defaults so the caller can determine what
|
||||
// values the user actually set. These are only set after explicitly calling
|
||||
// `setDefaults`.
|
||||
const defaults = {
|
||||
_: [],
|
||||
"extensions-dir": path.join(paths.data, "extensions"),
|
||||
"user-data-dir": paths.data,
|
||||
}
|
||||
|
||||
it("should set defaults", () => {
|
||||
assert.deepEqual(parse([]), defaults)
|
||||
assert.deepEqual(parse([]), { _: [] })
|
||||
})
|
||||
|
||||
it("should parse all available options", () => {
|
||||
@@ -69,7 +80,7 @@ describe("cli", () => {
|
||||
help: true,
|
||||
host: "0.0.0.0",
|
||||
json: true,
|
||||
log: "trace",
|
||||
log: "error",
|
||||
open: true,
|
||||
port: 8081,
|
||||
socket: path.resolve("mumble"),
|
||||
@@ -83,19 +94,20 @@ describe("cli", () => {
|
||||
|
||||
it("should work with short options", () => {
|
||||
assert.deepEqual(parse(["-vvv", "-v"]), {
|
||||
...defaults,
|
||||
log: "trace",
|
||||
_: [],
|
||||
verbose: true,
|
||||
version: true,
|
||||
})
|
||||
assert.equal(process.env.LOG_LEVEL, "trace")
|
||||
assert.equal(logger.level, Level.Trace)
|
||||
})
|
||||
|
||||
it("should use log level env var", () => {
|
||||
it("should use log level env var", async () => {
|
||||
const args = parse([])
|
||||
assert.deepEqual(args, { _: [] })
|
||||
|
||||
process.env.LOG_LEVEL = "debug"
|
||||
assert.deepEqual(parse([]), {
|
||||
assert.deepEqual(await setDefaults(args), {
|
||||
...defaults,
|
||||
_: [],
|
||||
log: "debug",
|
||||
verbose: false,
|
||||
})
|
||||
@@ -103,8 +115,9 @@ describe("cli", () => {
|
||||
assert.equal(logger.level, Level.Debug)
|
||||
|
||||
process.env.LOG_LEVEL = "trace"
|
||||
assert.deepEqual(parse([]), {
|
||||
assert.deepEqual(await setDefaults(args), {
|
||||
...defaults,
|
||||
_: [],
|
||||
log: "trace",
|
||||
verbose: true,
|
||||
})
|
||||
@@ -113,9 +126,16 @@ describe("cli", () => {
|
||||
})
|
||||
|
||||
it("should prefer --log to env var and --verbose to --log", async () => {
|
||||
let args = parse(["--log", "info"])
|
||||
assert.deepEqual(args, {
|
||||
_: [],
|
||||
log: "info",
|
||||
})
|
||||
|
||||
process.env.LOG_LEVEL = "debug"
|
||||
assert.deepEqual(parse(["--log", "info"]), {
|
||||
assert.deepEqual(await setDefaults(args), {
|
||||
...defaults,
|
||||
_: [],
|
||||
log: "info",
|
||||
verbose: false,
|
||||
})
|
||||
@@ -123,17 +143,26 @@ describe("cli", () => {
|
||||
assert.equal(logger.level, Level.Info)
|
||||
|
||||
process.env.LOG_LEVEL = "trace"
|
||||
assert.deepEqual(parse(["--log", "info"]), {
|
||||
assert.deepEqual(await setDefaults(args), {
|
||||
...defaults,
|
||||
_: [],
|
||||
log: "info",
|
||||
verbose: false,
|
||||
})
|
||||
assert.equal(process.env.LOG_LEVEL, "info")
|
||||
assert.equal(logger.level, Level.Info)
|
||||
|
||||
args = parse(["--log", "info", "--verbose"])
|
||||
assert.deepEqual(args, {
|
||||
_: [],
|
||||
log: "info",
|
||||
verbose: true,
|
||||
})
|
||||
|
||||
process.env.LOG_LEVEL = "warn"
|
||||
assert.deepEqual(parse(["--log", "info", "--verbose"]), {
|
||||
assert.deepEqual(await setDefaults(args), {
|
||||
...defaults,
|
||||
_: [],
|
||||
log: "trace",
|
||||
verbose: true,
|
||||
})
|
||||
@@ -141,9 +170,12 @@ describe("cli", () => {
|
||||
assert.equal(logger.level, Level.Trace)
|
||||
})
|
||||
|
||||
it("should ignore invalid log level env var", () => {
|
||||
it("should ignore invalid log level env var", async () => {
|
||||
process.env.LOG_LEVEL = "bogus"
|
||||
assert.deepEqual(parse([]), defaults)
|
||||
assert.deepEqual(await setDefaults(parse([])), {
|
||||
_: [],
|
||||
...defaults,
|
||||
})
|
||||
})
|
||||
|
||||
it("should error if value isn't provided", () => {
|
||||
@@ -166,7 +198,7 @@ describe("cli", () => {
|
||||
|
||||
it("should not error if the value is optional", () => {
|
||||
assert.deepEqual(parse(["--cert"]), {
|
||||
...defaults,
|
||||
_: [],
|
||||
cert: {
|
||||
value: undefined,
|
||||
},
|
||||
@@ -177,7 +209,7 @@ describe("cli", () => {
|
||||
assert.throws(() => parse(["--socket", "--socket-path-value"]), /--socket requires a value/)
|
||||
// If you actually had a path like this you would do this instead:
|
||||
assert.deepEqual(parse(["--socket", "./--socket-path-value"]), {
|
||||
...defaults,
|
||||
_: [],
|
||||
socket: path.resolve("--socket-path-value"),
|
||||
})
|
||||
assert.throws(() => parse(["--cert", "--socket-path-value"]), /Unknown option --socket-path-value/)
|
||||
@@ -185,7 +217,6 @@ describe("cli", () => {
|
||||
|
||||
it("should allow positional arguments before options", () => {
|
||||
assert.deepEqual(parse(["foo", "test", "--auth", "none"]), {
|
||||
...defaults,
|
||||
_: ["foo", "test"],
|
||||
auth: "none",
|
||||
})
|
||||
@@ -193,12 +224,85 @@ describe("cli", () => {
|
||||
|
||||
it("should support repeatable flags", () => {
|
||||
assert.deepEqual(parse(["--proxy-domain", "*.coder.com"]), {
|
||||
...defaults,
|
||||
_: [],
|
||||
"proxy-domain": ["*.coder.com"],
|
||||
})
|
||||
assert.deepEqual(parse(["--proxy-domain", "*.coder.com", "--proxy-domain", "test.com"]), {
|
||||
...defaults,
|
||||
_: [],
|
||||
"proxy-domain": ["*.coder.com", "test.com"],
|
||||
})
|
||||
})
|
||||
})
|
||||
|
||||
describe("cli", () => {
|
||||
let args: Mutable<Args> = { _: [] }
|
||||
const testDir = path.join(tmpdir, "tests/cli")
|
||||
const vscodeIpcPath = path.join(os.tmpdir(), "vscode-ipc")
|
||||
|
||||
before(async () => {
|
||||
await fs.remove(testDir)
|
||||
await fs.mkdirp(testDir)
|
||||
})
|
||||
|
||||
beforeEach(async () => {
|
||||
delete process.env.VSCODE_IPC_HOOK_CLI
|
||||
args = { _: [] }
|
||||
await fs.remove(vscodeIpcPath)
|
||||
})
|
||||
|
||||
it("should use existing if inside code-server", async () => {
|
||||
process.env.VSCODE_IPC_HOOK_CLI = "test"
|
||||
assert.strictEqual(await shouldOpenInExistingInstance(args), "test")
|
||||
|
||||
args.port = 8081
|
||||
args._.push("./file")
|
||||
assert.strictEqual(await shouldOpenInExistingInstance(args), "test")
|
||||
})
|
||||
|
||||
it("should use existing if --reuse-window is set", async () => {
|
||||
args["reuse-window"] = true
|
||||
assert.strictEqual(await shouldOpenInExistingInstance(args), undefined)
|
||||
|
||||
await fs.writeFile(vscodeIpcPath, "test")
|
||||
assert.strictEqual(await shouldOpenInExistingInstance(args), "test")
|
||||
|
||||
args.port = 8081
|
||||
assert.strictEqual(await shouldOpenInExistingInstance(args), "test")
|
||||
})
|
||||
|
||||
it("should use existing if --new-window is set", async () => {
|
||||
args["new-window"] = true
|
||||
assert.strictEqual(await shouldOpenInExistingInstance(args), undefined)
|
||||
|
||||
await fs.writeFile(vscodeIpcPath, "test")
|
||||
assert.strictEqual(await shouldOpenInExistingInstance(args), "test")
|
||||
|
||||
args.port = 8081
|
||||
assert.strictEqual(await shouldOpenInExistingInstance(args), "test")
|
||||
})
|
||||
|
||||
it("should use existing if no unrelated flags are set, has positional, and socket is active", async () => {
|
||||
assert.strictEqual(await shouldOpenInExistingInstance(args), undefined)
|
||||
|
||||
args._.push("./file")
|
||||
assert.strictEqual(await shouldOpenInExistingInstance(args), undefined)
|
||||
|
||||
const socketPath = path.join(testDir, "socket")
|
||||
await fs.writeFile(vscodeIpcPath, socketPath)
|
||||
assert.strictEqual(await shouldOpenInExistingInstance(args), undefined)
|
||||
|
||||
await new Promise((resolve) => {
|
||||
const server = net.createServer(() => {
|
||||
// Close after getting the first connection.
|
||||
server.close()
|
||||
})
|
||||
server.once("listening", () => resolve(server))
|
||||
server.listen(socketPath)
|
||||
})
|
||||
|
||||
assert.strictEqual(await shouldOpenInExistingInstance(args), socketPath)
|
||||
|
||||
args.port = 8081
|
||||
assert.strictEqual(await shouldOpenInExistingInstance(args), undefined)
|
||||
})
|
||||
})
|
||||
|
||||
@@ -6,8 +6,8 @@ import * as net from "net"
|
||||
import * as path from "path"
|
||||
import * as tls from "tls"
|
||||
import { Emitter } from "../src/common/emitter"
|
||||
import { generateCertificate, tmpdir } from "../src/node/util"
|
||||
import { SocketProxyProvider } from "../src/node/socket"
|
||||
import { generateCertificate, tmpdir } from "../src/node/util"
|
||||
|
||||
describe("SocketProxyProvider", () => {
|
||||
const provider = new SocketProxyProvider()
|
||||
|
||||
@@ -2,40 +2,34 @@ import * as assert from "assert"
|
||||
import * as fs from "fs-extra"
|
||||
import * as http from "http"
|
||||
import * as path from "path"
|
||||
import * as tar from "tar-fs"
|
||||
import * as zlib from "zlib"
|
||||
import { LatestResponse, UpdateHttpProvider } from "../src/node/app/update"
|
||||
import { AuthType } from "../src/node/http"
|
||||
import { SettingsProvider, UpdateSettings } from "../src/node/settings"
|
||||
import { tmpdir } from "../src/node/util"
|
||||
|
||||
describe("update", () => {
|
||||
const archivePath = path.join(tmpdir, "tests/updates/code-server-loose-source")
|
||||
return
|
||||
let version = "1.0.0"
|
||||
let spy: string[] = []
|
||||
const server = http.createServer((request: http.IncomingMessage, response: http.ServerResponse) => {
|
||||
if (!request.url) {
|
||||
throw new Error("no url")
|
||||
}
|
||||
|
||||
spy.push(request.url)
|
||||
response.writeHead(200)
|
||||
|
||||
// Return the latest version.
|
||||
if (request.url === "/latest") {
|
||||
const latest: LatestResponse = {
|
||||
name: version,
|
||||
}
|
||||
response.writeHead(200)
|
||||
return response.end(JSON.stringify(latest))
|
||||
}
|
||||
|
||||
const path = archivePath + (request.url.endsWith(".tar.gz") ? ".tar.gz" : ".zip")
|
||||
|
||||
const stream = fs.createReadStream(path)
|
||||
stream.on("error", (error: NodeJS.ErrnoException) => {
|
||||
response.writeHead(500)
|
||||
response.end(error.message)
|
||||
})
|
||||
response.writeHead(200)
|
||||
stream.on("close", () => response.end())
|
||||
stream.pipe(response)
|
||||
// Anything else is a 404.
|
||||
response.writeHead(404)
|
||||
response.end("not found")
|
||||
})
|
||||
|
||||
const jsonPath = path.join(tmpdir, "tests/updates/update.json")
|
||||
@@ -51,12 +45,10 @@ describe("update", () => {
|
||||
_provider = new UpdateHttpProvider(
|
||||
{
|
||||
auth: AuthType.None,
|
||||
base: "/update",
|
||||
commit: "test",
|
||||
},
|
||||
true,
|
||||
`http://${address.address}:${address.port}/latest`,
|
||||
`http://${address.address}:${address.port}/download/{{VERSION}}/{{RELEASE_NAME}}`,
|
||||
settings,
|
||||
)
|
||||
}
|
||||
@@ -72,32 +64,8 @@ describe("update", () => {
|
||||
host: "localhost",
|
||||
})
|
||||
})
|
||||
|
||||
const p = provider()
|
||||
const archiveName = (await p.getReleaseName({ version: "9999999.99999.9999", checked: 0 })).replace(
|
||||
/.tar.gz$|.zip$/,
|
||||
"",
|
||||
)
|
||||
await fs.remove(path.join(tmpdir, "tests/updates"))
|
||||
await fs.mkdirp(path.join(archivePath, archiveName))
|
||||
|
||||
await Promise.all([
|
||||
fs.writeFile(path.join(archivePath, archiveName, "code-server"), `console.log("UPDATED")`),
|
||||
fs.writeFile(path.join(archivePath, archiveName, "node"), `NODE BINARY`),
|
||||
])
|
||||
|
||||
await new Promise((resolve, reject) => {
|
||||
const write = fs.createWriteStream(archivePath + ".tar.gz")
|
||||
const compress = zlib.createGzip()
|
||||
compress.pipe(write)
|
||||
compress.on("error", (error) => compress.destroy(error))
|
||||
compress.on("close", () => write.end())
|
||||
tar.pack(archivePath).pipe(compress)
|
||||
write.on("close", reject)
|
||||
write.on("finish", () => {
|
||||
resolve()
|
||||
})
|
||||
})
|
||||
await fs.mkdirp(path.join(tmpdir, "tests/updates"))
|
||||
})
|
||||
|
||||
after(() => {
|
||||
@@ -185,53 +153,15 @@ describe("update", () => {
|
||||
assert.equal(p.isLatestVersion(update), true)
|
||||
})
|
||||
|
||||
it("should download and apply an update", async () => {
|
||||
version = "9999999.99999.9999"
|
||||
|
||||
const p = provider()
|
||||
const update = await p.getUpdate(true)
|
||||
|
||||
// Create an existing version.
|
||||
const destination = path.join(tmpdir, "tests/updates/code-server")
|
||||
await fs.mkdirp(destination)
|
||||
const entry = path.join(destination, "code-server")
|
||||
await fs.writeFile(entry, `console.log("OLD")`)
|
||||
assert.equal(`console.log("OLD")`, await fs.readFile(entry, "utf8"))
|
||||
|
||||
// Updating should replace the existing version.
|
||||
await p.downloadAndApplyUpdate(update, destination)
|
||||
assert.equal(`console.log("UPDATED")`, await fs.readFile(entry, "utf8"))
|
||||
|
||||
// There should be a backup.
|
||||
const dir = (await fs.readdir(path.join(tmpdir, "tests/updates"))).filter((dir) => {
|
||||
return dir.startsWith("code-server.")
|
||||
})
|
||||
assert.equal(dir.length, 1)
|
||||
assert.equal(
|
||||
`console.log("OLD")`,
|
||||
await fs.readFile(path.join(tmpdir, "tests/updates", dir[0], "code-server"), "utf8"),
|
||||
)
|
||||
|
||||
const archiveName = await p.getReleaseName(update)
|
||||
assert.deepEqual(spy, ["/latest", `/download/${version}/${archiveName}`])
|
||||
})
|
||||
|
||||
it("should not reject if unable to fetch", async () => {
|
||||
const options = {
|
||||
auth: AuthType.None,
|
||||
base: "/update",
|
||||
commit: "test",
|
||||
}
|
||||
let provider = new UpdateHttpProvider(options, true, "invalid", "invalid", settings)
|
||||
let provider = new UpdateHttpProvider(options, true, "invalid", settings)
|
||||
await assert.doesNotReject(() => provider.getUpdate(true))
|
||||
|
||||
provider = new UpdateHttpProvider(
|
||||
options,
|
||||
true,
|
||||
"http://probably.invalid.dev.localhost/latest",
|
||||
"http://probably.invalid.dev.localhost/download",
|
||||
settings,
|
||||
)
|
||||
provider = new UpdateHttpProvider(options, true, "http://probably.invalid.dev.localhost/latest", settings)
|
||||
await assert.doesNotReject(() => provider.getUpdate(true))
|
||||
})
|
||||
})
|
||||
|
||||
@@ -1,43 +1,7 @@
|
||||
import * as assert from "assert"
|
||||
import { normalize } from "../src/common/util"
|
||||
import { extend } from "../src/node/util"
|
||||
|
||||
describe("util", () => {
|
||||
describe("extend", () => {
|
||||
it("should extend", () => {
|
||||
const a = { foo: { bar: 0, baz: 2 }, garply: 4, waldo: 6 }
|
||||
const b = { foo: { bar: 1, qux: 3 }, garply: "5", fred: 7 }
|
||||
const extended = extend(a, b)
|
||||
assert.deepEqual(extended, {
|
||||
foo: { bar: 1, baz: 2, qux: 3 },
|
||||
garply: "5",
|
||||
waldo: 6,
|
||||
fred: 7,
|
||||
})
|
||||
})
|
||||
|
||||
it("should make deep copies of the original objects", () => {
|
||||
const a = { foo: 0, bar: { frobnozzle: 2 }, mumble: { qux: { thud: 4 } } }
|
||||
const b = { foo: 1, bar: { chad: 3 } }
|
||||
const extended = extend(a, b)
|
||||
assert.notEqual(a.bar, extended.bar)
|
||||
assert.notEqual(b.bar, extended.bar)
|
||||
assert.notEqual(a.mumble, extended.mumble)
|
||||
assert.notEqual(a.mumble.qux, extended.mumble.qux)
|
||||
})
|
||||
|
||||
it("should handle mismatch in type", () => {
|
||||
const a = { foo: { bar: 0, baz: 2, qux: { mumble: 11 } }, garply: 4, waldo: { thud: 10 } }
|
||||
const b = { foo: { bar: [1], baz: { plugh: 8 }, qux: 12 }, garply: { nox: 9 }, waldo: 7 }
|
||||
const extended = extend(a, b)
|
||||
assert.deepEqual(extended, {
|
||||
foo: { bar: [1], baz: { plugh: 8 }, qux: 12 },
|
||||
garply: { nox: 9 },
|
||||
waldo: 7,
|
||||
})
|
||||
})
|
||||
})
|
||||
|
||||
describe("normalize", () => {
|
||||
it("should remove multiple slashes", () => {
|
||||
assert.equal(normalize("//foo//bar//baz///mumble"), "/foo/bar/baz/mumble")
|
||||
|
||||
@@ -8,17 +8,15 @@
|
||||
"noUnusedLocals": true,
|
||||
"forceConsistentCasingInFileNames": true,
|
||||
"outDir": "./out",
|
||||
"allowJs": false,
|
||||
"jsx": "react",
|
||||
"declaration": true,
|
||||
"experimentalDecorators": true,
|
||||
"esModuleInterop": true,
|
||||
"allowSyntheticDefaultImports": true,
|
||||
"sourceMap": true,
|
||||
"tsBuildInfoFile": "./.tsbuildinfo",
|
||||
"tsBuildInfoFile": "./.cache/tsbuildinfo",
|
||||
"incremental": true,
|
||||
"rootDir": "./src",
|
||||
"typeRoots": ["./node_modules/@types", "./typings"]
|
||||
},
|
||||
"include": ["./src/**/*.ts", "./src/**/*.tsx"]
|
||||
"include": ["./src/**/*.ts"]
|
||||
}
|
||||
|
||||
Reference in New Issue
Block a user