Compare commits
71 Commits
| Author | SHA1 | Date | |
|---|---|---|---|
|
|
11f53784c5 | ||
|
|
7e1bb8fc96 | ||
|
|
ebe4d7ef29 | ||
|
|
f71d98f95c | ||
|
|
7fe475c1ef | ||
|
|
261af28f70 | ||
|
|
0713fa900b | ||
|
|
cc18175ce3 | ||
|
|
27f0f195a8 | ||
|
|
7282ebf436 | ||
|
|
c35d558352 | ||
|
|
8cb4e2c226 | ||
|
|
e5067ba2a9 | ||
|
|
fa0853dca6 | ||
|
|
a898dd34b9 | ||
|
|
4eb4375119 | ||
|
|
290c533c8e | ||
|
|
67e2a99df2 | ||
|
|
0ad7d93ea6 | ||
|
|
4cb8a32f4c | ||
|
|
833314aae8 | ||
|
|
5247878d93 | ||
|
|
ae65c83cbd | ||
|
|
eca4448877 | ||
|
|
93fb76e4a7 | ||
|
|
a1537d7138 | ||
|
|
def81245a4 | ||
|
|
37c80c9bbd | ||
|
|
be37821ab9 | ||
|
|
f74f1721e6 | ||
|
|
fb63c0cd22 | ||
|
|
bb26d2edd3 | ||
|
|
303fe2bc4e | ||
|
|
5a38ab95fe | ||
|
|
19710ab144 | ||
|
|
a018e30d6f | ||
|
|
fb835838db | ||
|
|
3d7fbec40f | ||
|
|
96170de191 | ||
|
|
2e2d03371f | ||
|
|
a0db6723c1 | ||
|
|
23ead21b1d | ||
|
|
42390da097 | ||
|
|
d0f6cbb02d | ||
|
|
fa59156a2a | ||
|
|
8ffe599796 | ||
|
|
a6f8840009 | ||
|
|
1feb30a7ff | ||
|
|
182aca6490 | ||
|
|
8311cf5657 | ||
|
|
4de2511162 | ||
|
|
3f7b91e2e2 | ||
|
|
431137da45 | ||
|
|
4d276b88c0 | ||
|
|
e28c9ab287 | ||
|
|
b540737b10 | ||
|
|
4380356e0c | ||
|
|
72caafe8b0 | ||
|
|
08b9e9ad1f | ||
|
|
2dc7863ec3 | ||
|
|
30100caf0c | ||
|
|
f79bb210ec | ||
|
|
182791319a | ||
|
|
624cd9d44f | ||
|
|
95ef6dbf2f | ||
|
|
016daf2fdd | ||
|
|
247c4ec776 | ||
|
|
d55e06936b | ||
|
|
2a3608df53 | ||
|
|
c6062c3d0a | ||
|
|
9ff535eddc |
@@ -18,6 +18,7 @@ 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)
|
||||
3. Update in [./ci/helm-chart/README.md](../ci/helm-chart/README.md)
|
||||
- Remember to update the chart version as well on top of appVersion in `Chart.yaml`.
|
||||
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
|
||||
@@ -66,6 +67,10 @@ This directory contains scripts used for the development of code-server.
|
||||
- [./ci/dev/watch.ts](./dev/watch.ts) (`yarn watch`)
|
||||
- Starts a process to build and launch code-server and restart on any code changes.
|
||||
- Example usage in [./doc/CONTRIBUTING.md](../doc/CONTRIBUTING.md).
|
||||
- [./ci/dev/gen_icons.sh](./ci/dev/gen_icons.sh) (`yarn icons`)
|
||||
- Generates the various icons from a single `.svg` favicon in
|
||||
`src/browser/media/favicon.svg`.
|
||||
- Requires [imagemagick](https://imagemagick.org/index.php)
|
||||
|
||||
## build
|
||||
|
||||
|
||||
@@ -15,7 +15,11 @@ v$VERSION
|
||||
|
||||
VS Code v$(vscode_version)
|
||||
|
||||
# New Features
|
||||
Upgrading is as easy as installing the new version over the old one. code-server
|
||||
maintains all user data in \`~/.local/share/code-server\` so that it is preserved in between
|
||||
installations.
|
||||
|
||||
## New Features
|
||||
- ⭐ Summarize new features here with references to issues
|
||||
|
||||
## Bug Fixes
|
||||
|
||||
21
ci/dev/gen_icons.sh
Executable file
@@ -0,0 +1,21 @@
|
||||
#!/bin/sh
|
||||
set -eu
|
||||
|
||||
main() {
|
||||
cd src/browser/media
|
||||
|
||||
# We need .ico for backwards compatibility.
|
||||
# The other two are the only icon sizes required by Chrome and
|
||||
# we use them for stuff like apple-touch-icon as well.
|
||||
# https://web.dev/add-manifest/
|
||||
#
|
||||
# This should be enough and we can always add more if there are problems.
|
||||
|
||||
# -background defaults to white but we want it transparent.
|
||||
# https://imagemagick.org/script/command-line-options.php#background
|
||||
convert -background transparent -resize 256x256 favicon.svg favicon.ico
|
||||
convert -background transparent -resize 192x192 favicon.svg pwa-icon-192.png
|
||||
convert -background transparent -resize 512x512 favicon.svg pwa-icon-512.png
|
||||
}
|
||||
|
||||
main "$@"
|
||||
@@ -11,6 +11,11 @@ main() {
|
||||
if command -v helm && helm kubeval --help > /dev/null; then
|
||||
helm kubeval ci/helm-chart
|
||||
fi
|
||||
|
||||
cd lib/vscode
|
||||
# Run this periodically in vanilla VS code to make sure we don't add any more warnings.
|
||||
yarn eslint --max-warnings=3
|
||||
cd "$OLDPWD"
|
||||
}
|
||||
|
||||
main "$@"
|
||||
|
||||
1178
ci/dev/vscode.patch
@@ -15,9 +15,9 @@ type: application
|
||||
# This is the chart version. This version number should be incremented each time you make changes
|
||||
# to the chart and its templates, including the app version.
|
||||
# Versions are expected to follow Semantic Versioning (https://semver.org/)
|
||||
version: 1.0.0
|
||||
version: 1.0.3
|
||||
|
||||
# This is the version number of the application being deployed. This version number should be
|
||||
# incremented each time you make changes to the application. Versions are not expected to
|
||||
# follow Semantic Versioning. They should reflect the version the application is using.
|
||||
appVersion: 3.7.1
|
||||
appVersion: 3.7.4
|
||||
|
||||
@@ -1,6 +1,6 @@
|
||||
# code-server
|
||||
|
||||
  
|
||||
  
|
||||
|
||||
[code-server](https://github.com/cdr/code-server) code-server is VS Code running
|
||||
on a remote server, accessible through the browser.
|
||||
@@ -72,7 +72,7 @@ and their default values.
|
||||
| hostnameOverride | string | `""` | |
|
||||
| image.pullPolicy | string | `"Always"` | |
|
||||
| image.repository | string | `"codercom/code-server"` | |
|
||||
| image.tag | string | `"3.7.1"` | |
|
||||
| image.tag | string | `"3.7.4"` | |
|
||||
| imagePullSecrets | list | `[]` | |
|
||||
| ingress.enabled | bool | `false` | |
|
||||
| nameOverride | string | `""` | |
|
||||
|
||||
@@ -6,7 +6,7 @@ replicaCount: 1
|
||||
|
||||
image:
|
||||
repository: codercom/code-server
|
||||
tag: '3.7.1'
|
||||
tag: '3.7.4'
|
||||
pullPolicy: Always
|
||||
|
||||
imagePullSecrets: []
|
||||
|
||||
@@ -7,7 +7,7 @@ main() {
|
||||
yarn --frozen-lockfile
|
||||
|
||||
git submodule update --init
|
||||
# We do not `yarn vscode` to make test.sh faster.
|
||||
# We do not `yarn vscode` to make fmt.sh faster.
|
||||
# If the patch fails to apply, then it's likely already applied
|
||||
yarn vscode:patch &> /dev/null || true
|
||||
|
||||
|
||||
@@ -7,9 +7,8 @@ main() {
|
||||
yarn --frozen-lockfile
|
||||
|
||||
git submodule update --init
|
||||
# We do not `yarn vscode` to make test.sh faster.
|
||||
# If the patch fails to apply, then it's likely already applied
|
||||
yarn vscode:patch &> /dev/null || true
|
||||
# We need to fetch VS Code's deps for lint dependencies.
|
||||
yarn vscode
|
||||
|
||||
yarn lint
|
||||
}
|
||||
|
||||
@@ -2,6 +2,7 @@
|
||||
<!-- DON'T EDIT THIS SECTION, INSTEAD RE-RUN doctoc TO UPDATE -->
|
||||
# Install
|
||||
|
||||
- [Upgrading](#upgrading)
|
||||
- [install.sh](#installsh)
|
||||
- [Flags](#flags)
|
||||
- [Detection Reference](#detection-reference)
|
||||
@@ -19,6 +20,12 @@
|
||||
This document demonstrates how to install `code-server` on
|
||||
various distros and operating systems.
|
||||
|
||||
## Upgrading
|
||||
|
||||
When upgrading you can just install the new version over the old one. code-server
|
||||
maintains all user data in `~/.local/share/code-server` so that it is preserved in between
|
||||
installations.
|
||||
|
||||
## install.sh
|
||||
|
||||
We have a [script](../install.sh) to install code-server for Linux, macOS and FreeBSD.
|
||||
@@ -80,8 +87,8 @@ commands presented in the rest of this document.
|
||||
## Debian, Ubuntu
|
||||
|
||||
```bash
|
||||
curl -fOL https://github.com/cdr/code-server/releases/download/v3.7.1/code-server_3.7.1_amd64.deb
|
||||
sudo dpkg -i code-server_3.7.1_amd64.deb
|
||||
curl -fOL https://github.com/cdr/code-server/releases/download/v3.7.4/code-server_3.7.4_amd64.deb
|
||||
sudo dpkg -i code-server_3.7.4_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
|
||||
```
|
||||
@@ -89,8 +96,8 @@ sudo systemctl enable --now code-server@$USER
|
||||
## Fedora, CentOS, RHEL, SUSE
|
||||
|
||||
```bash
|
||||
curl -fOL https://github.com/cdr/code-server/releases/download/v3.7.1/code-server-3.7.1-amd64.rpm
|
||||
sudo rpm -i code-server-3.7.1-amd64.rpm
|
||||
curl -fOL https://github.com/cdr/code-server/releases/download/v3.7.4/code-server-3.7.4-amd64.rpm
|
||||
sudo rpm -i code-server-3.7.4-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
|
||||
```
|
||||
@@ -159,10 +166,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.7.1/code-server-3.7.1-linux-amd64.tar.gz \
|
||||
curl -fL https://github.com/cdr/code-server/releases/download/v3.7.4/code-server-3.7.4-linux-amd64.tar.gz \
|
||||
| tar -C ~/.local/lib -xz
|
||||
mv ~/.local/lib/code-server-3.7.1-linux-amd64 ~/.local/lib/code-server-3.7.1
|
||||
ln -s ~/.local/lib/code-server-3.7.1/bin/code-server ~/.local/bin/code-server
|
||||
mv ~/.local/lib/code-server-3.7.4-linux-amd64 ~/.local/lib/code-server-3.7.4
|
||||
ln -s ~/.local/lib/code-server-3.7.4/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
|
||||
|
||||
@@ -1,7 +1,7 @@
|
||||
{
|
||||
"name": "code-server",
|
||||
"license": "MIT",
|
||||
"version": "3.7.1",
|
||||
"version": "3.7.4",
|
||||
"description": "Run VS Code on a remote server.",
|
||||
"homepage": "https://github.com/cdr/code-server",
|
||||
"bugs": {
|
||||
@@ -26,7 +26,8 @@
|
||||
"lint": "./ci/dev/lint.sh",
|
||||
"test": "./ci/dev/test.sh",
|
||||
"ci": "./ci/dev/ci.sh",
|
||||
"watch": "VSCODE_IPC_HOOK_CLI= 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",
|
||||
"icons": "./ci/dev/gen_icons.sh"
|
||||
},
|
||||
"main": "out/node/entry.js",
|
||||
"devDependencies": {
|
||||
|
||||
|
Before Width: | Height: | Size: 1.1 KiB After Width: | Height: | Size: 12 KiB |
1
src/browser/media/favicon.svg
Normal file
@@ -0,0 +1 @@
|
||||
<?xml version="1.0" encoding="UTF-8" standalone="no"?><!DOCTYPE svg PUBLIC "-//W3C//DTD SVG 1.1//EN" "http://www.w3.org/Graphics/SVG/1.1/DTD/svg11.dtd"><svg width="100%" height="100%" viewBox="0 0 2250 2250" version="1.1" xmlns="http://www.w3.org/2000/svg" xmlns:xlink="http://www.w3.org/1999/xlink" xml:space="preserve" xmlns:serif="http://www.serif.com/" style="fill-rule:evenodd;clip-rule:evenodd;stroke-linejoin:round;stroke-miterlimit:2;"><path d="M2029.18,672.912c-0,-249.515 -202.574,-452.089 -452.089,-452.089l-904.176,0c-249.515,0 -452.089,202.574 -452.089,452.089l0,904.176c0,249.515 202.574,452.089 452.089,452.089l904.176,-0c249.515,-0 452.089,-202.574 452.089,-452.089l-0,-904.176Z" style="fill:#fff;"/><path d="M1748.89,1058.72c-28.26,-0 -47.092,-16.57 -47.092,-50.58l0,-195.345c0,-124.707 -51.376,-193.601 -184.095,-193.601l-61.651,0l0,131.683l18.839,-0c52.23,-0 77.061,28.779 77.061,80.23l0,172.672c0,74.998 22.262,105.521 71.07,121.218c-48.808,14.827 -71.07,46.22 -71.07,121.218l0,128.197c0,35.753 0,70.636 -9.418,106.39c-9.418,33.14 -24.831,64.534 -46.237,91.567c-11.987,15.701 -25.688,28.78 -41.098,40.991l-0,17.44l61.647,-0c132.72,-0 184.097,-68.895 184.097,-193.601l-0,-195.345c-0,-34.883 17.975,-50.58 47.091,-50.58l35.108,0l-0,-131.684l-34.252,0l0,-0.87Z" style="fill-rule:nonzero;"/><path d="M1329.33,818.057l-190.087,-0c-4.282,-0 -7.705,-3.489 -7.705,-7.849l0,-14.824c0,-4.362 3.423,-7.849 7.705,-7.849l190.943,-0c4.28,-0 7.705,3.487 7.705,7.849l0,14.824c0,4.36 -4.282,7.849 -8.561,7.849Z" style="fill-rule:nonzero;"/><path d="M1361.87,1006.42l-138.711,-0c-4.282,-0 -7.708,-3.491 -7.708,-7.851l-0,-14.824c-0,-4.359 3.426,-7.849 7.708,-7.849l138.711,0c4.283,0 7.705,3.49 7.705,7.849l0,14.824c0,3.49 -3.422,7.851 -7.705,7.851Z" style="fill-rule:nonzero;"/><path d="M1416.67,912.236l-277.423,0c-4.282,0 -7.705,-3.487 -7.705,-7.848l0,-14.826c0,-4.36 3.423,-7.848 7.705,-7.848l276.567,0c4.282,0 7.707,3.488 7.707,7.848l0,14.826c0,3.488 -2.569,7.848 -6.851,7.848Z" style="fill-rule:nonzero;"/><path d="M919.188,860.762c18.837,0 37.676,1.745 55.657,6.105l-0,-35.757c-0,-50.58 25.687,-80.23 77.063,-80.23l18.837,-0l-0,-131.683l-61.651,0c-132.72,0 -184.093,68.894 -184.093,193.601l0,64.532c29.967,-10.463 61.651,-16.568 94.187,-16.568Z" style="fill-rule:nonzero;"/><path d="M1474.9,1335.15c-13.701,-110.754 -97.614,-203.194 -205.501,-224.124c-29.967,-6.103 -59.938,-6.978 -89.049,-1.744c-0.856,-0 -0.856,-0.873 -1.712,-0.873c-47.094,-100.288 -148.13,-166.566 -257.731,-166.566c-109.6,-0 -209.78,64.535 -257.731,164.823c-0.856,-0 -0.856,0.872 -1.712,0.872c-30.824,-3.49 -61.65,-1.747 -92.475,6.104c-106.174,26.16 -186.662,116.857 -201.218,226.738c-1.712,11.337 -2.569,22.673 -2.569,33.141c0,33.136 22.263,63.659 54.8,68.02c40.244,6.106 75.35,-25.29 74.494,-65.404c-0,-6.106 -0,-13.084 0.856,-19.187c6.85,-55.814 48.806,-102.904 103.605,-115.987c17.126,-4.361 34.251,-5.231 50.519,-2.614c52.232,6.977 103.606,-20.059 125.869,-67.149c16.27,-34.884 41.957,-65.409 76.207,-81.978c37.672,-18.314 80.487,-20.927 119.876,-6.974c41.097,14.824 71.921,46.218 90.76,85.461c19.693,38.374 29.111,65.407 71.069,70.64c17.124,2.614 65.074,1.743 83.056,0.871c35.106,-0 70.213,12.209 95.044,37.499c16.266,17.441 28.254,39.244 33.393,63.661c7.705,39.244 -1.713,78.486 -24.832,108.137c-16.27,20.93 -38.532,36.626 -63.363,43.604c-11.987,3.49 -23.975,4.36 -35.962,4.36l-189.232,0c-37.672,0 -67.642,-30.52 -67.642,-68.894l-0,-255.519c-0,-10.462 -8.561,-19.182 -18.837,-19.182l-26.544,-0c-52.233,0.87 -94.187,60.173 -94.187,122.96l-0,229.358c-0,68.021 53.942,122.961 120.731,122.961c0,0 297.119,-0.874 301.399,-0.874c68.499,-6.976 131.863,-42.73 174.673,-97.671c42.814,-53.196 62.507,-122.963 53.946,-194.47Z" style="fill-rule:nonzero;"/></svg>
|
||||
|
After Width: | Height: | Size: 3.6 KiB |
@@ -6,31 +6,11 @@
|
||||
"background-color": "#fff",
|
||||
"description": "Run editors on a remote server.",
|
||||
"icons": [
|
||||
{
|
||||
"src": "{{CS_STATIC_BASE}}/src/browser/media/pwa-icon-96.png",
|
||||
"type": "image/png",
|
||||
"sizes": "96x96"
|
||||
},
|
||||
{
|
||||
"src": "{{CS_STATIC_BASE}}/src/browser/media/pwa-icon-128.png",
|
||||
"type": "image/png",
|
||||
"sizes": "128x128"
|
||||
},
|
||||
{
|
||||
"src": "{{CS_STATIC_BASE}}/src/browser/media/pwa-icon-192.png",
|
||||
"type": "image/png",
|
||||
"sizes": "192x192"
|
||||
},
|
||||
{
|
||||
"src": "{{CS_STATIC_BASE}}/src/browser/media/pwa-icon-256.png",
|
||||
"type": "image/png",
|
||||
"sizes": "256x256"
|
||||
},
|
||||
{
|
||||
"src": "{{CS_STATIC_BASE}}/src/browser/media/pwa-icon-384.png",
|
||||
"type": "image/png",
|
||||
"sizes": "384x384"
|
||||
},
|
||||
{
|
||||
"src": "{{CS_STATIC_BASE}}/src/browser/media/pwa-icon-512.png",
|
||||
"type": "image/png",
|
||||
|
||||
|
Before Width: | Height: | Size: 12 KiB |
|
Before Width: | Height: | Size: 22 KiB After Width: | Height: | Size: 9.3 KiB |
|
Before Width: | Height: | Size: 32 KiB |
|
Before Width: | Height: | Size: 58 KiB |
|
Before Width: | Height: | Size: 86 KiB After Width: | Height: | Size: 25 KiB |
|
Before Width: | Height: | Size: 8.2 KiB |
@@ -11,9 +11,11 @@
|
||||
content="style-src 'self'; manifest-src 'self'; img-src 'self' data:; font-src 'self' data:;"
|
||||
/>
|
||||
<title>{{ERROR_TITLE}} - code-server</title>
|
||||
<link rel="icon" href="{{CS_STATIC_BASE}}/src/browser/media/favicon.ico" type="image/x-icon" />
|
||||
<link rel="icon" href="{{CS_STATIC_BASE}}/src/browser/media/favicon.svg" />
|
||||
<link rel="alternate icon" href="{{CS_STATIC_BASE}}/src/browser/media/favicon.ico" />
|
||||
<link rel="manifest" href="{{CS_STATIC_BASE}}/src/browser/media/manifest.json" crossorigin="use-credentials" />
|
||||
<link rel="apple-touch-icon" href="{{CS_STATIC_BASE}}/src/browser/media/pwa-icon-384.png" />
|
||||
<link rel="apple-touch-icon" sizes="192x192" href="{{CS_STATIC_BASE}}/src/browser/media/pwa-icon-192.png" />
|
||||
<link rel="apple-touch-icon" sizes="512x512" href="{{CS_STATIC_BASE}}/src/browser/media/pwa-icon-512.png" />
|
||||
<link href="{{CS_STATIC_BASE}}/dist/register.css" rel="stylesheet" />
|
||||
<meta id="coder-options" data-settings="{{OPTIONS}}" />
|
||||
</head>
|
||||
|
||||
@@ -37,3 +37,7 @@ body {
|
||||
.login-form > .field > .submit {
|
||||
margin-left: 20px;
|
||||
}
|
||||
|
||||
input {
|
||||
-webkit-appearance: none;
|
||||
}
|
||||
|
||||
@@ -11,9 +11,11 @@
|
||||
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="{{CS_STATIC_BASE}}/src/browser/media/favicon.ico" type="image/x-icon" />
|
||||
<link rel="icon" href="{{CS_STATIC_BASE}}/src/browser/media/favicon.svg" />
|
||||
<link rel="alternate icon" href="{{CS_STATIC_BASE}}/src/browser/media/favicon.ico" />
|
||||
<link rel="manifest" href="{{CS_STATIC_BASE}}/src/browser/media/manifest.json" crossorigin="use-credentials" />
|
||||
<link rel="apple-touch-icon" href="{{CS_STATIC_BASE}}/src/browser/media/pwa-icon-384.png" />
|
||||
<link rel="apple-touch-icon" sizes="192x192" href="{{CS_STATIC_BASE}}/src/browser/media/pwa-icon-192.png" />
|
||||
<link rel="apple-touch-icon" sizes="512x512" href="{{CS_STATIC_BASE}}/src/browser/media/pwa-icon-512.png" />
|
||||
<link href="{{CS_STATIC_BASE}}/dist/register.css" rel="stylesheet" />
|
||||
<meta id="coder-options" data-settings="{{OPTIONS}}" />
|
||||
</head>
|
||||
|
||||
@@ -24,19 +24,16 @@
|
||||
<meta id="vscode-remote-nls-configuration" data-settings="{{NLS_CONFIGURATION}}" />
|
||||
|
||||
<!-- Workbench Icon/Manifest/CSS -->
|
||||
<link rel="icon" href="{{CS_STATIC_BASE}}/src/browser/media/favicon.ico" type="image/x-icon" />
|
||||
<link rel="icon" href="{{CS_STATIC_BASE}}/src/browser/media/favicon.svg" />
|
||||
<link rel="alternate icon" href="{{CS_STATIC_BASE}}/src/browser/media/favicon.ico" />
|
||||
<link rel="manifest" href="{{CS_STATIC_BASE}}/src/browser/media/manifest.json" crossorigin="use-credentials" />
|
||||
<!-- PROD_ONLY
|
||||
<link data-name="vs/workbench/workbench.web.api" rel="stylesheet" href="{{CS_STATIC_BASE}}/lib/vscode/out/vs/workbench/workbench.web.api.css">
|
||||
END_PROD_ONLY -->
|
||||
<link rel="apple-touch-icon" href="{{CS_STATIC_BASE}}/src/browser/media/pwa-icon-384.png" />
|
||||
<link rel="apple-touch-icon" sizes="192x192" href="{{CS_STATIC_BASE}}/src/browser/media/pwa-icon-192.png" />
|
||||
<link rel="apple-touch-icon" sizes="512x512" href="{{CS_STATIC_BASE}}/src/browser/media/pwa-icon-512.png" />
|
||||
<meta name="apple-mobile-web-app-capable" content="yes" />
|
||||
|
||||
<!-- Prefetch to avoid waterfall -->
|
||||
<!-- PROD_ONLY
|
||||
<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}}" />
|
||||
</head>
|
||||
|
||||
|
||||
@@ -41,7 +41,6 @@ try {
|
||||
"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`,
|
||||
|
||||
@@ -33,6 +33,7 @@ export interface Args extends VsArgs {
|
||||
"cert-host"?: string
|
||||
"cert-key"?: string
|
||||
"disable-telemetry"?: boolean
|
||||
"disable-update-check"?: boolean
|
||||
help?: boolean
|
||||
host?: string
|
||||
json?: boolean
|
||||
@@ -114,6 +115,12 @@ const options: Options<Required<Args>> = {
|
||||
},
|
||||
"cert-key": { type: "string", path: true, description: "Path to certificate key when using non-generated cert." },
|
||||
"disable-telemetry": { type: "boolean", description: "Disable telemetry." },
|
||||
"disable-update-check": {
|
||||
type: "boolean",
|
||||
description:
|
||||
"Disable update check. Without this flag, code-server checks every 6 hours against the latest github release and \n" +
|
||||
"then notifies you once every week that a new release is available.",
|
||||
},
|
||||
help: { type: "boolean", short: "h", description: "Show this output." },
|
||||
json: { type: "boolean" },
|
||||
open: { type: "boolean", description: "Open in browser on startup. Does not work remotely." },
|
||||
|
||||
@@ -19,7 +19,7 @@ import { coderCloudBind } from "./coder-cloud"
|
||||
import { commit, version } from "./constants"
|
||||
import { register } from "./routes"
|
||||
import { humanPath, isFile, open } from "./util"
|
||||
import { ipcMain, WrapperProcess } from "./wrapper"
|
||||
import { isChild, wrapper } from "./wrapper"
|
||||
|
||||
export const runVsCodeCli = (args: DefaultedArgs): void => {
|
||||
logger.debug("forking vs code cli...")
|
||||
@@ -121,7 +121,7 @@ const main = async (args: DefaultedArgs): Promise<void> => {
|
||||
}
|
||||
|
||||
if (args.cert) {
|
||||
logger.info(" - Using certificate for HTTPS: ${humanPath(args.cert.value)}")
|
||||
logger.info(` - Using certificate for HTTPS: ${humanPath(args.cert.value)}`)
|
||||
} else {
|
||||
logger.info(" - Not serving HTTPS")
|
||||
}
|
||||
@@ -137,7 +137,7 @@ const main = async (args: DefaultedArgs): Promise<void> => {
|
||||
logger.info(" - Connected to cloud agent")
|
||||
} catch (err) {
|
||||
logger.error(err.message)
|
||||
ipcMain.exit(1)
|
||||
wrapper.exit(1)
|
||||
}
|
||||
}
|
||||
|
||||
@@ -154,19 +154,22 @@ const main = async (args: DefaultedArgs): Promise<void> => {
|
||||
}
|
||||
|
||||
async function entry(): Promise<void> {
|
||||
// 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. We also get the
|
||||
// arguments from the parent so we don't have to parse twice and to account
|
||||
// for environment manipulation (like how PASSWORD gets removed to avoid
|
||||
// leaking to child processes).
|
||||
if (isChild(wrapper)) {
|
||||
const args = await wrapper.handshake()
|
||||
wrapper.preventExit()
|
||||
return main(args)
|
||||
}
|
||||
|
||||
const cliArgs = parse(process.argv.slice(2))
|
||||
const configArgs = await readConfigFile(cliArgs.config)
|
||||
const args = await setDefaults(cliArgs, configArgs)
|
||||
|
||||
// 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)
|
||||
}
|
||||
|
||||
if (args.help) {
|
||||
console.log("code-server", version, commit)
|
||||
console.log("")
|
||||
@@ -201,11 +204,10 @@ async function entry(): Promise<void> {
|
||||
return openInExistingInstance(args, socketPath)
|
||||
}
|
||||
|
||||
const wrapper = new WrapperProcess(require("../../package.json").version)
|
||||
return wrapper.start()
|
||||
return wrapper.start(args)
|
||||
}
|
||||
|
||||
entry().catch((error) => {
|
||||
logger.error(error.message)
|
||||
ipcMain.exit(error)
|
||||
wrapper.exit(error)
|
||||
})
|
||||
|
||||
@@ -66,7 +66,11 @@ export const register = async (
|
||||
app.use(bodyParser.urlencoded({ extended: true }))
|
||||
|
||||
const common: express.RequestHandler = (req, _, next) => {
|
||||
heart.beat()
|
||||
// /healthz|/healthz/ needs to be excluded otherwise health checks will make
|
||||
// it look like code-server is always in use.
|
||||
if (!/^\/healthz\/?$/.test(req.url)) {
|
||||
heart.beat()
|
||||
}
|
||||
|
||||
// Add common variables routes can use.
|
||||
req.args = args
|
||||
|
||||
@@ -7,13 +7,33 @@ import * as tarFs from "tar-fs"
|
||||
import * as zlib from "zlib"
|
||||
import { HttpCode, HttpError } from "../../common/http"
|
||||
import { rootPath } from "../constants"
|
||||
import { authenticated, replaceTemplates } from "../http"
|
||||
import { authenticated, ensureAuthenticated, replaceTemplates } from "../http"
|
||||
import { getMediaMime, pathToFsPath } from "../util"
|
||||
|
||||
export const router = Router()
|
||||
|
||||
// The commit is for caching.
|
||||
router.get("/(:commit)(/*)?", async (req, res) => {
|
||||
// Used by VS Code to load extensions into the web worker.
|
||||
const tar = Array.isArray(req.query.tar) ? req.query.tar[0] : req.query.tar
|
||||
if (typeof tar === "string") {
|
||||
ensureAuthenticated(req)
|
||||
let stream: Readable = tarFs.pack(pathToFsPath(tar))
|
||||
if (req.headers["accept-encoding"] && req.headers["accept-encoding"].includes("gzip")) {
|
||||
logger.debug("gzipping tar", field("path", tar))
|
||||
const compress = zlib.createGzip()
|
||||
stream.pipe(compress)
|
||||
stream.on("error", (error) => compress.destroy(error))
|
||||
stream.on("close", () => compress.end())
|
||||
stream = compress
|
||||
res.header("content-encoding", "gzip")
|
||||
}
|
||||
res.set("Content-Type", "application/x-tar")
|
||||
stream.on("close", () => res.end())
|
||||
return stream.pipe(res)
|
||||
}
|
||||
|
||||
// If not a tar use the remainder of the path to load the resource.
|
||||
if (!req.params[0]) {
|
||||
throw new HttpError("Not Found", HttpCode.NotFound)
|
||||
}
|
||||
@@ -32,24 +52,9 @@ router.get("/(:commit)(/*)?", async (req, res) => {
|
||||
res.header("Cache-Control", "public, max-age=31536000")
|
||||
}
|
||||
|
||||
/**
|
||||
* Used by VS Code to load extensions into the web worker.
|
||||
*/
|
||||
const tar = Array.isArray(req.query.tar) ? req.query.tar[0] : req.query.tar
|
||||
if (typeof tar === "string") {
|
||||
let stream: Readable = tarFs.pack(pathToFsPath(tar))
|
||||
if (req.headers["accept-encoding"] && req.headers["accept-encoding"].includes("gzip")) {
|
||||
logger.debug("gzipping tar", field("path", resourcePath))
|
||||
const compress = zlib.createGzip()
|
||||
stream.pipe(compress)
|
||||
stream.on("error", (error) => compress.destroy(error))
|
||||
stream.on("close", () => compress.end())
|
||||
stream = compress
|
||||
res.header("content-encoding", "gzip")
|
||||
}
|
||||
res.set("Content-Type", "application/x-tar")
|
||||
stream.on("close", () => res.end())
|
||||
return stream.pipe(res)
|
||||
// Without this the default is to use the directory the script loaded from.
|
||||
if (req.headers["service-worker"]) {
|
||||
res.header("service-worker-allowed", "/")
|
||||
}
|
||||
|
||||
res.set("Content-Type", getMediaMime(resourcePath))
|
||||
|
||||
@@ -7,7 +7,7 @@ export const router = Router()
|
||||
|
||||
const provider = new UpdateProvider()
|
||||
|
||||
router.get("/", ensureAuthenticated, async (req, res) => {
|
||||
router.get("/check", ensureAuthenticated, async (req, res) => {
|
||||
const update = await provider.getUpdate(req.query.force === "true")
|
||||
res.json({
|
||||
checked: update.checked,
|
||||
|
||||
@@ -42,6 +42,7 @@ router.get("/", async (req, res) => {
|
||||
commit !== "development" ? content.replace(/<!-- PROD_ONLY/g, "").replace(/END_PROD_ONLY -->/g, "") : content,
|
||||
{
|
||||
disableTelemetry: !!req.args["disable-telemetry"],
|
||||
disableUpdateCheck: !!req.args["disable-update-check"],
|
||||
},
|
||||
)
|
||||
.replace(`"{{REMOTE_USER_DATA_URI}}"`, `'${JSON.stringify(options.remoteUserDataUri)}'`)
|
||||
|
||||
@@ -75,7 +75,7 @@ export class UpdateProvider {
|
||||
public isLatestVersion(latest: Update): boolean {
|
||||
logger.debug("comparing versions", field("current", version), field("latest", latest.version))
|
||||
try {
|
||||
return latest.version === version || semver.lt(latest.version, version)
|
||||
return semver.lte(latest.version, version)
|
||||
} catch (error) {
|
||||
return true
|
||||
}
|
||||
|
||||
@@ -1,4 +1,4 @@
|
||||
import { field, logger } from "@coder/logger"
|
||||
import { logger } from "@coder/logger"
|
||||
import * as cp from "child_process"
|
||||
import * as net from "net"
|
||||
import * as path from "path"
|
||||
@@ -8,19 +8,18 @@ import { rootPath } from "./constants"
|
||||
import { settings } from "./settings"
|
||||
import { SocketProxyProvider } from "./socket"
|
||||
import { isFile } from "./util"
|
||||
import { ipcMain } from "./wrapper"
|
||||
import { onMessage, wrapper } from "./wrapper"
|
||||
|
||||
export class VscodeProvider {
|
||||
public readonly serverRootPath: string
|
||||
public readonly vsRootPath: string
|
||||
private _vscode?: Promise<cp.ChildProcess>
|
||||
private timeoutInterval = 10000 // 10s, matches VS Code's timeouts.
|
||||
private readonly socketProvider = new SocketProxyProvider()
|
||||
|
||||
public constructor() {
|
||||
this.vsRootPath = path.resolve(rootPath, "lib/vscode")
|
||||
this.serverRootPath = path.join(this.vsRootPath, "out/vs/server")
|
||||
ipcMain.onDispose(() => this.dispose())
|
||||
wrapper.onDispose(() => this.dispose())
|
||||
}
|
||||
|
||||
public async dispose(): Promise<void> {
|
||||
@@ -69,10 +68,13 @@ export class VscodeProvider {
|
||||
vscode,
|
||||
)
|
||||
|
||||
const message = await this.onMessage(vscode, (message): message is ipc.OptionsMessage => {
|
||||
// There can be parallel initializations so wait for the right ID.
|
||||
return message.type === "options" && message.id === id
|
||||
})
|
||||
const message = await onMessage<ipc.VscodeMessage, ipc.OptionsMessage>(
|
||||
vscode,
|
||||
(message): message is ipc.OptionsMessage => {
|
||||
// There can be parallel initializations so wait for the right ID.
|
||||
return message.type === "options" && message.id === id
|
||||
},
|
||||
)
|
||||
|
||||
return message.options
|
||||
}
|
||||
@@ -104,61 +106,13 @@ export class VscodeProvider {
|
||||
dispose()
|
||||
})
|
||||
|
||||
this._vscode = this.onMessage(vscode, (message): message is ipc.ReadyMessage => {
|
||||
this._vscode = onMessage<ipc.VscodeMessage, ipc.ReadyMessage>(vscode, (message): message is ipc.ReadyMessage => {
|
||||
return message.type === "ready"
|
||||
}).then(() => vscode)
|
||||
|
||||
return this._vscode
|
||||
}
|
||||
|
||||
/**
|
||||
* Listen to a single message from a process. Reject if the process errors,
|
||||
* exits, or times out.
|
||||
*
|
||||
* `fn` is a function that determines whether the message is the one we're
|
||||
* waiting for.
|
||||
*/
|
||||
private onMessage<T extends ipc.VscodeMessage>(
|
||||
proc: cp.ChildProcess,
|
||||
fn: (message: ipc.VscodeMessage) => message is T,
|
||||
): Promise<T> {
|
||||
return new Promise((resolve, reject) => {
|
||||
const cleanup = () => {
|
||||
proc.off("error", onError)
|
||||
proc.off("exit", onExit)
|
||||
proc.off("message", onMessage)
|
||||
clearTimeout(timeout)
|
||||
}
|
||||
|
||||
const timeout = setTimeout(() => {
|
||||
cleanup()
|
||||
reject(new Error("timed out"))
|
||||
}, this.timeoutInterval)
|
||||
|
||||
const onError = (error: Error) => {
|
||||
cleanup()
|
||||
reject(error)
|
||||
}
|
||||
|
||||
const onExit = (code: number | null) => {
|
||||
cleanup()
|
||||
reject(new Error(`VS Code exited unexpectedly with code ${code}`))
|
||||
}
|
||||
|
||||
const onMessage = (message: ipc.VscodeMessage) => {
|
||||
logger.trace("got message from vscode", field("message", message))
|
||||
if (fn(message)) {
|
||||
cleanup()
|
||||
resolve(message)
|
||||
}
|
||||
}
|
||||
|
||||
proc.on("message", onMessage)
|
||||
proc.on("error", onError)
|
||||
proc.on("exit", onExit)
|
||||
})
|
||||
}
|
||||
|
||||
/**
|
||||
* VS Code expects a raw socket. It will handle all the web socket frames.
|
||||
*/
|
||||
|
||||
@@ -1,11 +1,70 @@
|
||||
import { field, logger } from "@coder/logger"
|
||||
import { field, Logger, 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 { DefaultedArgs } from "./cli"
|
||||
import { paths } from "./util"
|
||||
|
||||
interface HandshakeMessage {
|
||||
const timeoutInterval = 10000 // 10s, matches VS Code's timeouts.
|
||||
|
||||
/**
|
||||
* Listen to a single message from a process. Reject if the process errors,
|
||||
* exits, or times out.
|
||||
*
|
||||
* `fn` is a function that determines whether the message is the one we're
|
||||
* waiting for.
|
||||
*/
|
||||
export function onMessage<M, T extends M>(
|
||||
proc: cp.ChildProcess | NodeJS.Process,
|
||||
fn: (message: M) => message is T,
|
||||
customLogger?: Logger,
|
||||
): Promise<T> {
|
||||
return new Promise((resolve, reject) => {
|
||||
const cleanup = () => {
|
||||
proc.off("error", onError)
|
||||
proc.off("exit", onExit)
|
||||
proc.off("message", onMessage)
|
||||
clearTimeout(timeout)
|
||||
}
|
||||
|
||||
const timeout = setTimeout(() => {
|
||||
cleanup()
|
||||
reject(new Error("timed out"))
|
||||
}, timeoutInterval)
|
||||
|
||||
const onError = (error: Error) => {
|
||||
cleanup()
|
||||
reject(error)
|
||||
}
|
||||
|
||||
const onExit = (code: number) => {
|
||||
cleanup()
|
||||
reject(new Error(`exited unexpectedly with code ${code}`))
|
||||
}
|
||||
|
||||
const onMessage = (message: M) => {
|
||||
;(customLogger || logger).trace("got message", field("message", message))
|
||||
if (fn(message)) {
|
||||
cleanup()
|
||||
resolve(message)
|
||||
}
|
||||
}
|
||||
|
||||
proc.on("message", onMessage)
|
||||
// NodeJS.Process doesn't have `error` but binding anyway shouldn't break
|
||||
// anything. It does have `exit` but the types aren't working.
|
||||
;(proc as cp.ChildProcess).on("error", onError)
|
||||
;(proc as cp.ChildProcess).on("exit", onExit)
|
||||
})
|
||||
}
|
||||
|
||||
interface ParentHandshakeMessage {
|
||||
type: "handshake"
|
||||
args: DefaultedArgs
|
||||
}
|
||||
|
||||
interface ChildHandshakeMessage {
|
||||
type: "handshake"
|
||||
}
|
||||
|
||||
@@ -14,9 +73,10 @@ interface RelaunchMessage {
|
||||
version: string
|
||||
}
|
||||
|
||||
export type Message = RelaunchMessage | HandshakeMessage
|
||||
type ChildMessage = RelaunchMessage | ChildHandshakeMessage
|
||||
type ParentMessage = ParentHandshakeMessage
|
||||
|
||||
export class ProcessError extends Error {
|
||||
class ProcessError extends Error {
|
||||
public constructor(message: string, public readonly code: number | undefined) {
|
||||
super(message)
|
||||
this.name = this.constructor.name
|
||||
@@ -25,16 +85,26 @@ export class ProcessError extends Error {
|
||||
}
|
||||
|
||||
/**
|
||||
* Allows the wrapper and inner processes to communicate.
|
||||
* Wrapper around a process that tries to gracefully exit when a process exits
|
||||
* and provides a way to prevent `process.exit`.
|
||||
*/
|
||||
export class IpcMain {
|
||||
private readonly _onMessage = new Emitter<Message>()
|
||||
public readonly onMessage = this._onMessage.event
|
||||
private readonly _onDispose = new Emitter<NodeJS.Signals | undefined>()
|
||||
public readonly onDispose = this._onDispose.event
|
||||
public readonly processExit: (code?: number) => never = process.exit
|
||||
abstract class Process {
|
||||
/**
|
||||
* Emit this to trigger a graceful exit.
|
||||
*/
|
||||
protected readonly _onDispose = new Emitter<NodeJS.Signals | undefined>()
|
||||
|
||||
public constructor(private readonly parentPid?: number) {
|
||||
/**
|
||||
* Emitted when the process is about to be disposed.
|
||||
*/
|
||||
public readonly onDispose = this._onDispose.event
|
||||
|
||||
/**
|
||||
* Uniquely named logger for the process.
|
||||
*/
|
||||
public abstract logger: Logger
|
||||
|
||||
public constructor() {
|
||||
process.on("SIGINT", () => this._onDispose.emit("SIGINT"))
|
||||
process.on("SIGTERM", () => this._onDispose.emit("SIGTERM"))
|
||||
process.on("exit", () => this._onDispose.emit(undefined))
|
||||
@@ -43,42 +113,27 @@ export class IpcMain {
|
||||
// Remove listeners to avoid possibly triggering disposal again.
|
||||
process.removeAllListeners()
|
||||
|
||||
// Try waiting for other handlers run first then exit.
|
||||
logger.debug(`${parentPid ? "inner process" : "wrapper"} ${process.pid} disposing`, field("code", signal))
|
||||
// Try waiting for other handlers to run first then exit.
|
||||
this.logger.debug("disposing", field("code", signal))
|
||||
wait.then(() => this.exit(0))
|
||||
setTimeout(() => this.exit(0), 5000)
|
||||
})
|
||||
|
||||
// Kill the inner process if the parent dies. This is for the case where the
|
||||
// parent process is forcefully terminated and cannot clean up.
|
||||
if (parentPid) {
|
||||
setInterval(() => {
|
||||
try {
|
||||
// process.kill throws an exception if the process doesn't exist.
|
||||
process.kill(parentPid, 0)
|
||||
} catch (_) {
|
||||
// Consider this an error since it should have been able to clean up
|
||||
// the child process unless it was forcefully killed.
|
||||
logger.error(`parent process ${parentPid} died`)
|
||||
this._onDispose.emit(undefined)
|
||||
}
|
||||
}, 5000)
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* Ensure we control when the process exits.
|
||||
* Ensure control over 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
|
||||
;(process.exit as any) = (code?: number) => {
|
||||
this.logger.warn(`process.exit() was prevented: ${code || "unknown code"}.`)
|
||||
}
|
||||
}
|
||||
|
||||
public get isChild(): boolean {
|
||||
return typeof this.parentPid !== "undefined"
|
||||
}
|
||||
private readonly processExit: (code?: number) => never = process.exit
|
||||
|
||||
/**
|
||||
* Will always exit even if normal exit is being prevented.
|
||||
*/
|
||||
public exit(error?: number | ProcessError): never {
|
||||
if (error && typeof error !== "number") {
|
||||
this.processExit(typeof error.code === "number" ? error.code : 1)
|
||||
@@ -86,48 +141,59 @@ export class IpcMain {
|
||||
this.processExit(error)
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
public handshake(child?: cp.ChildProcess): Promise<void> {
|
||||
return new Promise((resolve, reject) => {
|
||||
const target = child || process
|
||||
const onMessage = (message: Message): void => {
|
||||
logger.debug(
|
||||
`${child ? "wrapper" : "inner process"} ${process.pid} received message from ${
|
||||
child ? child.pid : this.parentPid
|
||||
}`,
|
||||
field("message", message),
|
||||
)
|
||||
if (message.type === "handshake") {
|
||||
target.removeListener("message", onMessage)
|
||||
target.on("message", (msg) => this._onMessage.emit(msg))
|
||||
// The wrapper responds once the inner process starts the handshake.
|
||||
if (child) {
|
||||
if (!target.send) {
|
||||
throw new Error("child not spawned with IPC")
|
||||
}
|
||||
target.send({ type: "handshake" })
|
||||
}
|
||||
resolve()
|
||||
}
|
||||
/**
|
||||
* Child process that will clean up after itself if the parent goes away and can
|
||||
* perform a handshake with the parent and ask it to relaunch.
|
||||
*/
|
||||
class ChildProcess extends Process {
|
||||
public logger = logger.named(`child:${process.pid}`)
|
||||
|
||||
public constructor(private readonly parentPid: number) {
|
||||
super()
|
||||
|
||||
// Kill the inner process if the parent dies. This is for the case where the
|
||||
// parent process is forcefully terminated and cannot clean up.
|
||||
setInterval(() => {
|
||||
try {
|
||||
// process.kill throws an exception if the process doesn't exist.
|
||||
process.kill(this.parentPid, 0)
|
||||
} catch (_) {
|
||||
// Consider this an error since it should have been able to clean up
|
||||
// the child process unless it was forcefully killed.
|
||||
this.logger.error(`parent process ${parentPid} died`)
|
||||
this._onDispose.emit(undefined)
|
||||
}
|
||||
target.on("message", onMessage)
|
||||
if (child) {
|
||||
child.once("error", reject)
|
||||
child.once("exit", (code) => {
|
||||
reject(new ProcessError(`Unexpected exit with code ${code}`, code !== null ? code : undefined))
|
||||
})
|
||||
} else {
|
||||
// The inner process initiates the handshake.
|
||||
this.send({ type: "handshake" })
|
||||
}
|
||||
})
|
||||
}, 5000)
|
||||
}
|
||||
|
||||
/**
|
||||
* Initiate the handshake and wait for a response from the parent.
|
||||
*/
|
||||
public async handshake(): Promise<DefaultedArgs> {
|
||||
this.send({ type: "handshake" })
|
||||
const message = await onMessage<ParentMessage, ParentHandshakeMessage>(
|
||||
process,
|
||||
(message): message is ParentHandshakeMessage => {
|
||||
return message.type === "handshake"
|
||||
},
|
||||
this.logger,
|
||||
)
|
||||
return message.args
|
||||
}
|
||||
|
||||
/**
|
||||
* Notify the parent process that it should relaunch the child.
|
||||
*/
|
||||
public relaunch(version: string): void {
|
||||
this.send({ type: "relaunch", version })
|
||||
}
|
||||
|
||||
private send(message: Message): void {
|
||||
/**
|
||||
* Send a message to the parent.
|
||||
*/
|
||||
private send(message: ChildMessage): void {
|
||||
if (!process.send) {
|
||||
throw new Error("not spawned with IPC")
|
||||
}
|
||||
@@ -136,28 +202,31 @@ export class IpcMain {
|
||||
}
|
||||
|
||||
/**
|
||||
* Channel for communication between the child and parent processes.
|
||||
* Parent process wrapper that spawns the child process and performs a handshake
|
||||
* with it. Will relaunch the child if it receives a SIGUSR1 or is asked to by
|
||||
* the child. If the child otherwise exits the parent will also exit.
|
||||
*/
|
||||
export const ipcMain = new IpcMain(
|
||||
typeof process.env.CODE_SERVER_PARENT_PID !== "undefined" ? parseInt(process.env.CODE_SERVER_PARENT_PID) : undefined,
|
||||
)
|
||||
export class ParentProcess extends Process {
|
||||
public logger = logger.named(`parent:${process.pid}`)
|
||||
|
||||
export interface WrapperOptions {
|
||||
maxMemory?: number
|
||||
nodeOptions?: string
|
||||
}
|
||||
|
||||
/**
|
||||
* Provides a way to wrap a process for the purpose of updating the running
|
||||
* instance.
|
||||
*/
|
||||
export class WrapperProcess {
|
||||
private process?: cp.ChildProcess
|
||||
private child?: 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) {
|
||||
protected readonly _onChildMessage = new Emitter<ChildMessage>()
|
||||
protected readonly onChildMessage = this._onChildMessage.event
|
||||
|
||||
private args?: DefaultedArgs
|
||||
|
||||
public constructor(private currentVersion: string) {
|
||||
super()
|
||||
|
||||
process.on("SIGUSR1", async () => {
|
||||
this.logger.info("Received SIGUSR1; hotswapping")
|
||||
this.relaunch()
|
||||
})
|
||||
|
||||
const opts = {
|
||||
size: "10M",
|
||||
maxFiles: 10,
|
||||
@@ -165,19 +234,19 @@ export class WrapperProcess {
|
||||
this.logStdoutStream = rfs.createStream(path.join(paths.data, "coder-logs", "code-server-stdout.log"), opts)
|
||||
this.logStderrStream = rfs.createStream(path.join(paths.data, "coder-logs", "code-server-stderr.log"), opts)
|
||||
|
||||
ipcMain.onDispose(() => {
|
||||
this.onDispose(() => {
|
||||
this.disposeChild()
|
||||
})
|
||||
|
||||
ipcMain.onMessage((message) => {
|
||||
this.onChildMessage((message) => {
|
||||
switch (message.type) {
|
||||
case "relaunch":
|
||||
logger.info(`Relaunching: ${this.currentVersion} -> ${message.version}`)
|
||||
this.logger.info(`Relaunching: ${this.currentVersion} -> ${message.version}`)
|
||||
this.currentVersion = message.version
|
||||
this.relaunch()
|
||||
break
|
||||
default:
|
||||
logger.error(`Unrecognized message ${message}`)
|
||||
this.logger.error(`Unrecognized message ${message}`)
|
||||
break
|
||||
}
|
||||
})
|
||||
@@ -185,30 +254,26 @@ export class WrapperProcess {
|
||||
|
||||
private disposeChild(): void {
|
||||
this.started = undefined
|
||||
if (this.process) {
|
||||
this.process.removeAllListeners()
|
||||
this.process.kill()
|
||||
if (this.child) {
|
||||
this.child.removeAllListeners()
|
||||
this.child.kill()
|
||||
}
|
||||
}
|
||||
|
||||
private async relaunch(): Promise<void> {
|
||||
this.disposeChild()
|
||||
try {
|
||||
await this.start()
|
||||
this.started = this._start()
|
||||
await this.started
|
||||
} catch (error) {
|
||||
logger.error(error.message)
|
||||
ipcMain.exit(typeof error.code === "number" ? error.code : 1)
|
||||
this.logger.error(error.message)
|
||||
this.exit(typeof error.code === "number" ? error.code : 1)
|
||||
}
|
||||
}
|
||||
|
||||
public start(): Promise<void> {
|
||||
// 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()
|
||||
})
|
||||
}
|
||||
public start(args: DefaultedArgs): Promise<void> {
|
||||
// Store for relaunches.
|
||||
this.args = args
|
||||
if (!this.started) {
|
||||
this.started = this._start()
|
||||
}
|
||||
@@ -217,7 +282,7 @@ export class WrapperProcess {
|
||||
|
||||
private async _start(): Promise<void> {
|
||||
const child = this.spawn()
|
||||
this.process = child
|
||||
this.child = child
|
||||
|
||||
// Log both to stdout and to the log directory.
|
||||
if (child.stdout) {
|
||||
@@ -229,45 +294,75 @@ export class WrapperProcess {
|
||||
child.stderr.pipe(process.stderr)
|
||||
}
|
||||
|
||||
logger.debug(`spawned inner process ${child.pid}`)
|
||||
this.logger.debug(`spawned inner process ${child.pid}`)
|
||||
|
||||
await ipcMain.handshake(child)
|
||||
await this.handshake(child)
|
||||
|
||||
child.once("exit", (code) => {
|
||||
logger.debug(`inner process ${child.pid} exited unexpectedly`)
|
||||
ipcMain.exit(code || 0)
|
||||
this.logger.debug(`inner process ${child.pid} exited unexpectedly`)
|
||||
this.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)) {
|
||||
nodeOptions += ` --max_old_space_size=${(this.options && this.options.maxMemory) || 2048}`
|
||||
}
|
||||
|
||||
// Use spawn (instead of fork) to use the new binary in case it was updated.
|
||||
return cp.spawn(process.argv[0], process.argv.slice(1), {
|
||||
env: {
|
||||
...process.env,
|
||||
CODE_SERVER_PARENT_PID: process.pid.toString(),
|
||||
NODE_OPTIONS: nodeOptions,
|
||||
NODE_OPTIONS: `--max-old-space-size=2048 ${process.env.NODE_OPTIONS || ""}`,
|
||||
},
|
||||
stdio: ["ipc"],
|
||||
})
|
||||
}
|
||||
|
||||
/**
|
||||
* Wait for a handshake from the child then reply.
|
||||
*/
|
||||
private async handshake(child: cp.ChildProcess): Promise<void> {
|
||||
if (!this.args) {
|
||||
throw new Error("started without args")
|
||||
}
|
||||
await onMessage<ChildMessage, ChildHandshakeMessage>(
|
||||
child,
|
||||
(message): message is ChildHandshakeMessage => {
|
||||
return message.type === "handshake"
|
||||
},
|
||||
this.logger,
|
||||
)
|
||||
this.send(child, { type: "handshake", args: this.args })
|
||||
}
|
||||
|
||||
/**
|
||||
* Send a message to the child.
|
||||
*/
|
||||
private send(child: cp.ChildProcess, message: ParentMessage): void {
|
||||
child.send(message)
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* Process wrapper.
|
||||
*/
|
||||
export const wrapper =
|
||||
typeof process.env.CODE_SERVER_PARENT_PID !== "undefined"
|
||||
? new ChildProcess(parseInt(process.env.CODE_SERVER_PARENT_PID))
|
||||
: new ParentProcess(require("../../package.json").version)
|
||||
|
||||
export function isChild(proc: ChildProcess | ParentProcess): proc is ChildProcess {
|
||||
return proc instanceof ChildProcess
|
||||
}
|
||||
|
||||
// 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", () => wrapper.exit())
|
||||
}
|
||||
|
||||
// Don't let uncaught exceptions crash the process.
|
||||
process.on("uncaughtException", (error) => {
|
||||
logger.error(`Uncaught exception: ${error.message}`)
|
||||
wrapper.logger.error(`Uncaught exception: ${error.message}`)
|
||||
if (typeof error.stack !== "undefined") {
|
||||
logger.error(error.stack)
|
||||
wrapper.logger.error(error.stack)
|
||||
}
|
||||
})
|
||||
|
||||