diff --git a/.eslintrc.yaml b/.eslintrc.yaml index 306dd2c22..92657d629 100644 --- a/.eslintrc.yaml +++ b/.eslintrc.yaml @@ -30,6 +30,7 @@ rules: eqeqeq: error import/order: [error, { alphabetize: { order: "asc" }, groups: [["builtin", "external", "internal"], "parent", "sibling"] }] + no-async-promise-executor: off settings: # Does not work with CommonJS unfortunately. diff --git a/.github/ISSUE_TEMPLATE/doc.md b/.github/ISSUE_TEMPLATE/doc.md new file mode 100644 index 000000000..ba63b11bd --- /dev/null +++ b/.github/ISSUE_TEMPLATE/doc.md @@ -0,0 +1,7 @@ +--- +name: Documentation improvement +about: Suggest a documentation improvement +title: "" +labels: "docs" +assignees: "" +--- diff --git a/.github/workflows/ci.yaml b/.github/workflows/ci.yaml index dcf917841..a265c98ef 100644 --- a/.github/workflows/ci.yaml +++ b/.github/workflows/ci.yaml @@ -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 diff --git a/.github/workflows/publish.yaml b/.github/workflows/publish.yaml index c2fe429b9..74540651f 100644 --- a/.github/workflows/publish.yaml +++ b/.github/workflows/publish.yaml @@ -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: diff --git a/.gitignore b/.gitignore index 616f9b01b..4929c46fb 100644 --- a/.gitignore +++ b/.gitignore @@ -11,3 +11,5 @@ release-images/ node_modules node-* /plugins +/lib/coder-cloud-agent +.home diff --git a/.gitmodules b/.gitmodules index 9854a1b1d..f2cdafc7a 100644 --- a/.gitmodules +++ b/.gitmodules @@ -1,3 +1,4 @@ [submodule "lib/vscode"] path = lib/vscode url = https://github.com/microsoft/vscode + ignore = dirty diff --git a/README.md b/README.md index 3c94712fa..f126580b8 100644 --- a/README.md +++ b/README.md @@ -1,4 +1,4 @@ -# code-server +# code-server · [!["GitHub Discussions"](https://img.shields.io/badge/%20GitHub-%20Discussions-gray.svg?longCache=true&logo=github&colorB=purple)](https://github.com/cdr/code-server/discussions) [!["Join us on Slack"](https://img.shields.io/badge/join-us%20on%20slack-gray.svg?longCache=true&logo=slack&colorB=brightgreen)](https://cdr.co/join-community) [![Twitter Follow](https://img.shields.io/twitter/follow/CoderHQ?label=%40CoderHQ&style=social)](https://twitter.com/coderhq) Run [VS Code](https://github.com/Microsoft/vscode) on any machine anywhere and access it in the browser. @@ -11,7 +11,7 @@ Run [VS Code](https://github.com/Microsoft/vscode) on any machine anywhere and a - 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. + - Preserve battery life when you're on the go as all intensive tasks run on your server. - Make use of a spare computer you have lying around and turn it into a full development environment. ## Getting Started @@ -52,7 +52,7 @@ See [./doc/CONTRIBUTING.md](./doc/CONTRIBUTING.md). ## Hiring -We ([@cdr](https://github.com/cdr)) are looking for a engineers to help maintain +We ([@cdr](https://github.com/cdr)) are looking for engineers to help maintain code-server, innovate on open source and streamline dev workflows. Our main office is in Austin, Texas. Remote is ok as long as diff --git a/ci/build/build-code-server.sh b/ci/build/build-code-server.sh index df0852804..0aff035af 100755 --- a/ci/build/build-code-server.sh +++ b/ci/build/build-code-server.sh @@ -18,6 +18,12 @@ main() { chmod +x out/node/entry.js fi + if ! [ -f ./lib/coder-cloud-agent ]; then + OS="$(uname | tr '[:upper:]' '[:lower:]')" + curl -fsSL "https://storage.googleapis.com/coder-cloud-releases/agent/latest/$OS/cloud-agent" -o ./lib/coder-cloud-agent + chmod +x ./lib/coder-cloud-agent + fi + parcel build \ --public-url "." \ --out-dir dist \ diff --git a/ci/build/build-packages.sh b/ci/build/build-packages.sh index 058a54781..a5ef794e5 100755 --- a/ci/build/build-packages.sh +++ b/ci/build/build-packages.sh @@ -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 diff --git a/ci/build/build-release.sh b/ci/build/build-release.sh index 1cb00ad01..95579eb82 100755 --- a/ci/build/build-release.sh +++ b/ci/build/build-release.sh @@ -6,6 +6,10 @@ set -euo pipefail # MINIFY controls whether minified vscode is bundled. MINIFY="${MINIFY-true}" +# KEEP_MODULES controls whether the script cleans all node_modules requiring a yarn install +# to run first. +KEEP_MODULES="${KEEP_MODULES-0}" + main() { cd "$(dirname "${0}")/../.." source ./ci/lib.sh @@ -37,6 +41,7 @@ bundle_code_server() { rsync src/browser/media/ "$RELEASE_PATH/src/browser/media" mkdir -p "$RELEASE_PATH/src/browser/pages" rsync src/browser/pages/*.html "$RELEASE_PATH/src/browser/pages" + rsync src/browser/robots.txt "$RELEASE_PATH/src/browser" # Adds the commit to package.json jq --slurp '.[0] * .[1]' package.json <( @@ -51,6 +56,12 @@ 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() { @@ -59,7 +70,11 @@ bundle_vscode() { 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" diff --git a/ci/build/clean.sh b/ci/build/clean.sh index 0e0425a4b..b80632278 100755 --- a/ci/build/clean.sh +++ b/ci/build/clean.sh @@ -5,16 +5,7 @@ main() { cd "$(dirname "${0}")/../.." source ./ci/lib.sh - rm -rf \ - out \ - release \ - release-standalone \ - release-packages \ - release-gcp \ - release-images \ - dist \ - .cache \ - node-* + git clean -Xffd pushd lib/vscode git clean -xffd diff --git a/ci/build/npm-postinstall.sh b/ci/build/npm-postinstall.sh index 127d6408a..bd7922d5c 100755 --- a/ci/build/npm-postinstall.sh +++ b/ci/build/npm-postinstall.sh @@ -24,6 +24,10 @@ main() { ;; esac + 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 + 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 +40,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 "$@" diff --git a/ci/build/release-github-assets.sh b/ci/build/release-github-assets.sh index f2d9ff8c3..7fba67703 100755 --- a/ci/build/release-github-assets.sh +++ b/ci/build/release-github-assets.sh @@ -11,7 +11,7 @@ main() { source ./ci/lib.sh download_artifact release-packages ./release-packages - local assets=(./release-packages/code-server*"$VERSION"*{.tar.gz,.zip,.deb,.rpm}) + local assets=(./release-packages/code-server*"$VERSION"*{.tar.gz,.deb,.rpm}) for i in "${!assets[@]}"; do assets[$i]="--attach=${assets[$i]}" done diff --git a/ci/build/test-standalone-release.sh b/ci/build/test-standalone-release.sh index 92b58e8c0..0344ea39f 100755 --- a/ci/build/test-standalone-release.sh +++ b/ci/build/test-standalone-release.sh @@ -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 diff --git a/ci/dev/diff-vscode.sh b/ci/dev/diff-vscode.sh index 98c955dff..38f7cb563 100755 --- a/ci/dev/diff-vscode.sh +++ b/ci/dev/diff-vscode.sh @@ -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 "$@" diff --git a/ci/dev/image/run.sh b/ci/dev/image/run.sh index 70ab67e1d..3d5e15dd6 100755 --- a/ci/dev/image/run.sh +++ b/ci/dev/image/run.sh @@ -4,16 +4,22 @@ set -euo pipefail main() { cd "$(dirname "$0")/../../.." source ./ci/lib.sh + mkdir -p .home docker run \ -it \ --rm \ -v "$PWD:/src" \ + -e HOME="/src/.home" \ + -e USER="coder" \ + -e GITHUB_TOKEN \ + -e KEEP_MODULES \ + -e MINIFY \ -w /src \ -p 127.0.0.1:8080:8080 \ -u "$(id -u):$(id -g)" \ -e CI \ - "$(docker_build ./ci/images/debian8)" \ + "$(docker_build ./ci/images/"${IMAGE-debian10}")" \ "$@" } diff --git a/ci/dev/lint.sh b/ci/dev/lint.sh index 219c3793b..5f7c549bc 100755 --- a/ci/dev/lint.sh +++ b/ci/dev/lint.sh @@ -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 "$@" diff --git a/ci/dev/vscode.patch b/ci/dev/vscode.patch index 14bdce6b1..f15c7d7a7 100644 --- a/ci/dev/vscode.patch +++ b/ci/dev/vscode.patch @@ -1,5 +1,5 @@ diff --git a/.gitignore b/.gitignore -index 0fe46b6eadc..e545e004cef 100644 +index 0fe46b6eadc4ccc819fbf342ee1071bb657792b3..e545e004cef31fa5f40ba8df6a2317ea5b69ddb5 100644 --- a/.gitignore +++ b/.gitignore @@ -25,7 +25,6 @@ out-vscode-reh-web-pkg/ @@ -12,15 +12,15 @@ index 0fe46b6eadc..e545e004cef 100644 coverage/ diff --git a/.yarnrc b/.yarnrc deleted file mode 100644 -index 135e10442a7..00000000000 +index 3c6eccfb102f2084d16395d70d65f05a91b6d47b..0000000000000000000000000000000000000000 --- a/.yarnrc +++ /dev/null @@ -1,3 +0,0 @@ -disturl "https://atom.io/download/electron" --target "7.3.2" +-target "9.2.1" -runtime "electron" diff --git a/build/gulpfile.reh.js b/build/gulpfile.reh.js -index f2ea1bd3701..3f660f99819 100644 +index f2ea1bd37010b1eb8a43ce9beaae4a88810f6e2d..3f660f9981921ec465d2b8809a1a5ea5663f4c1f 100644 --- a/build/gulpfile.reh.js +++ b/build/gulpfile.reh.js @@ -52,6 +52,7 @@ gulp.task('vscode-reh-web-linux-x64-min', noop); @@ -31,8 +31,34 @@ index f2ea1bd3701..3f660f99819 100644 const yarnrc = fs.readFileSync(path.join(REPO_ROOT, 'remote', '.yarnrc'), 'utf8'); const target = /^target "(.*)"$/m.exec(yarnrc)[1]; return target; +diff --git a/build/lib/extensions.js b/build/lib/extensions.js +index 9cc40c4e1befd38886dc5880581d6f462a38dd3a..34e1fc89a8ac1c273a5cb41f19a088a8ec759d24 100644 +--- a/build/lib/extensions.js ++++ b/build/lib/extensions.js +@@ -66,7 +66,7 @@ function fromLocal(extensionPath, forWeb) { + if (isWebPacked) { + input = updateExtensionPackageJSON(input, (data) => { + delete data.scripts; +- delete data.dependencies; ++ // https://github.com/cdr/code-server/pull/2041#issuecomment-685910322 + delete data.devDependencies; + if (data.main) { + data.main = data.main.replace('/out/', /dist/); +diff --git a/build/lib/extensions.ts b/build/lib/extensions.ts +index 7e529f17cb84d28d84de4ff64fa9fb8fc48135a9..462d699dc485369c74a4d9fdfefa48ba6124ac3a 100644 +--- a/build/lib/extensions.ts ++++ b/build/lib/extensions.ts +@@ -70,7 +70,7 @@ function fromLocal(extensionPath: string, forWeb: boolean): Stream { + if (isWebPacked) { + input = updateExtensionPackageJSON(input, (data: any) => { + delete data.scripts; +- delete data.dependencies; ++ // https://github.com/cdr/code-server/pull/2041#issuecomment-685910322 + delete data.devDependencies; + if (data.main) { + data.main = data.main.replace('/out/', /dist/); diff --git a/build/lib/node.js b/build/lib/node.js -index 403ae3d9657..738ee8cee0e 100644 +index 403ae3d9657f823019542e739fc39292db20e4fe..738ee8cee0e79aa239af10e1abefc9e836b8ce33 100644 --- a/build/lib/node.js +++ b/build/lib/node.js @@ -5,11 +5,8 @@ @@ -49,7 +75,7 @@ index 403ae3d9657..738ee8cee0e 100644 const nodePath = path.join(root, '.build', 'node', `v${version}`, `${process.platform}-${process.arch}`, node); console.log(nodePath); diff --git a/build/lib/node.ts b/build/lib/node.ts -index 64397034461..c53dccf4dc0 100644 +index 64397034461b1661f82007c141cbf4c039a3b722..c53dccf4dc0a99122ed96cf10c2eb632bb25059e 100644 --- a/build/lib/node.ts +++ b/build/lib/node.ts @@ -4,13 +4,10 @@ @@ -70,7 +96,7 @@ index 64397034461..c53dccf4dc0 100644 \ No newline at end of file +console.log(nodePath); diff --git a/build/lib/util.js b/build/lib/util.js -index e552a036f89..169e8614b9f 100644 +index e552a036f89bd581644459fd5c27fe4ae1379f62..169e8614b9f6a2bd68446144ab7e1ce5c6d49b64 100644 --- a/build/lib/util.js +++ b/build/lib/util.js @@ -257,6 +257,7 @@ function streamToPromise(stream) { @@ -82,7 +108,7 @@ index e552a036f89..169e8614b9f 100644 const target = /^target "(.*)"$/m.exec(yarnrc)[1]; return target; diff --git a/build/lib/util.ts b/build/lib/util.ts -index 035c7e95ea3..4ff8dcfe6b2 100644 +index 035c7e95ea3006bb3dabd68bbf54db80de4aaaf2..4ff8dcfe6b21a0ec8064ebc7bb05506b8f1faa91 100644 --- a/build/lib/util.ts +++ b/build/lib/util.ts @@ -322,6 +322,7 @@ export function streamToPromise(stream: NodeJS.ReadWriteStream): Promise { @@ -94,7 +120,7 @@ index 035c7e95ea3..4ff8dcfe6b2 100644 const target = /^target "(.*)"$/m.exec(yarnrc)![1]; return target; diff --git a/build/npm/postinstall.js b/build/npm/postinstall.js -index 8f8b0019a77..ea054c725be 100644 +index 8f8b0019a7792a993fbd6bf95b013b596aa2935a..ea054c725bea2eec342e12b07314241aa18a4951 100644 --- a/build/npm/postinstall.js +++ b/build/npm/postinstall.js @@ -33,10 +33,11 @@ function yarnInstall(location, opts) { @@ -127,7 +153,7 @@ index 8f8b0019a77..ea054c725be 100644 cp.execSync('git config pull.rebase true'); diff --git a/build/npm/preinstall.js b/build/npm/preinstall.js -index cb88d37adef..6b3253af0a3 100644 +index cb88d37adefd4882f61a2711fdd7f72b89e1a6e3..6b3253af0a3a0aa4d75456379ef1c00f4cb98d13 100644 --- a/build/npm/preinstall.js +++ b/build/npm/preinstall.js @@ -8,8 +8,9 @@ let err = false; @@ -144,10 +170,10 @@ index cb88d37adef..6b3253af0a3 100644 const cp = require('child_process'); diff --git a/coder.js b/coder.js new file mode 100644 -index 00000000000..9cb693af63b +index 0000000000000000000000000000000000000000..df5b42cba463b6c0043aebbc835f852f1284aa36 --- /dev/null +++ b/coder.js -@@ -0,0 +1,63 @@ +@@ -0,0 +1,64 @@ +// This must be ran from VS Code's root. +const gulp = require("gulp"); +const path = require("path"); @@ -163,6 +189,7 @@ index 00000000000..9cb693af63b + buildfile.base, + buildfile.workbenchWeb, + buildfile.workerExtensionHost, ++ buildfile.workerNotebook, + buildfile.keyboardMaps, + buildfile.entrypoint("vs/platform/files/node/watcher/unix/watcherApp", ["vs/css", "vs/nls"]), + buildfile.entrypoint("vs/platform/files/node/watcher/nsfw/watcherApp", ["vs/css", "vs/nls"]), @@ -212,7 +239,7 @@ index 00000000000..9cb693af63b + common.minifyTask("out-vscode") +)); diff --git a/extensions/postinstall.js b/extensions/postinstall.js -index da4fa3e9d04..50f3e1144f8 100644 +index da4fa3e9d0443d679dfbab1000b434af2ae01afd..50f3e1144f8057883dea8b91ec2f7073458dbd94 100644 --- a/extensions/postinstall.js +++ b/extensions/postinstall.js @@ -24,6 +24,9 @@ function processRoot() { @@ -226,10 +253,10 @@ index da4fa3e9d04..50f3e1144f8 100644 function processLib() { diff --git a/package.json b/package.json -index 226f51a1ec5..5c4e5af5f69 100644 +index 9b5ee0f876303283eb766fd2bb3ed818c50b1d3e..30ef9fa81b1cd844138388d794d4d6d9db5c7fba 100644 --- a/package.json +++ b/package.json -@@ -45,7 +45,11 @@ +@@ -46,7 +46,11 @@ "watch-web": "gulp watch-web --max_old_space_size=4095", "eslint": "eslint -c .eslintrc.json --rulesdir ./build/lib/eslint --ext .ts --ext .js ./src/vs ./extensions" }, @@ -241,15 +268,15 @@ index 226f51a1ec5..5c4e5af5f69 100644 "applicationinsights": "1.0.8", "chokidar": "3.2.3", "graceful-fs": "4.2.3", -@@ -59,6 +63,7 @@ - "native-keymap": "2.1.2", +@@ -60,6 +64,7 @@ + "native-keymap": "2.2.0", "native-watchdog": "1.3.0", "node-pty": "0.10.0-beta8", + "rimraf": "^2.2.8", "semver-umd": "^5.5.7", "spdlog": "^0.11.1", "sudo-prompt": "9.1.1", -@@ -159,7 +164,6 @@ +@@ -160,7 +165,6 @@ "pump": "^1.0.1", "queue": "3.0.6", "rcedit": "^1.1.0", @@ -257,7 +284,7 @@ index 226f51a1ec5..5c4e5af5f69 100644 "sinon": "^1.17.2", "source-map": "^0.4.4", "style-loader": "^1.0.0", -@@ -190,5 +194,8 @@ +@@ -192,5 +196,8 @@ "windows-foreground-love": "0.2.0", "windows-mutex": "0.3.0", "windows-process-tree": "0.2.4" @@ -267,7 +294,7 @@ index 226f51a1ec5..5c4e5af5f69 100644 } } diff --git a/product.json b/product.json -index 2b884d18f30..518b935b837 100644 +index b9349015e3475bff07104ca2fa859954a37f962a..4c32260abc42efe17ee7d717e4dcebf182044e8c 100644 --- a/product.json +++ b/product.json @@ -20,7 +20,7 @@ @@ -281,18 +308,18 @@ index 2b884d18f30..518b935b837 100644 "ms-vscode.vscode-js-profile-flame", diff --git a/remote/.yarnrc b/remote/.yarnrc deleted file mode 100644 -index 1e16cde724c..00000000000 +index c1a32ce532afa501fb19bdbcf6bcb0ec151ecd99..0000000000000000000000000000000000000000 --- a/remote/.yarnrc +++ /dev/null @@ -1,3 +0,0 @@ -disturl "http://nodejs.org/dist" --target "12.4.0" +-target "12.14.1" -runtime "node" diff --git a/src/vs/base/common/network.ts b/src/vs/base/common/network.ts -index 1286c5117a4..e60dd11d039 100644 +index 4b6aebc16466dff58a9dfab4a680d230fa1f71a5..dd72e179ec0fa9a0b3e16e497225cb6da6218af3 100644 --- a/src/vs/base/common/network.ts +++ b/src/vs/base/common/network.ts -@@ -111,16 +111,17 @@ class RemoteAuthoritiesImpl { +@@ -113,16 +113,17 @@ class RemoteAuthoritiesImpl { if (host && host.indexOf(':') !== -1) { host = `[${host}]`; } @@ -314,7 +341,7 @@ index 1286c5117a4..e60dd11d039 100644 }); } diff --git a/src/vs/base/common/platform.ts b/src/vs/base/common/platform.ts -index 0bbc5d6ef91..61f139b9c55 100644 +index 0bbc5d6ef911b1e98d26ad796873a9b6b7fb04ec..61f139b9c557b9c46e5a9640ab0e37a6fb7692ee 100644 --- a/src/vs/base/common/platform.ts +++ b/src/vs/base/common/platform.ts @@ -59,6 +59,17 @@ if (typeof navigator === 'object' && !isElectronRenderer) { @@ -336,7 +363,7 @@ index 0bbc5d6ef91..61f139b9c55 100644 _isWindows = (process.platform === 'win32'); _isMacintosh = (process.platform === 'darwin'); diff --git a/src/vs/base/common/processes.ts b/src/vs/base/common/processes.ts -index c52f7b3774f..08a87fa970f 100644 +index c52f7b3774f399d3fa161682316b20d807072806..08a87fa970f159f84691c5068cf5e38f0926015c 100644 --- a/src/vs/base/common/processes.ts +++ b/src/vs/base/common/processes.ts @@ -110,7 +110,8 @@ export function sanitizeProcessEnvironment(env: IProcessEnvironment, ...preserve @@ -350,7 +377,7 @@ index c52f7b3774f..08a87fa970f 100644 const envKeys = Object.keys(env); envKeys diff --git a/src/vs/base/common/uriIpc.ts b/src/vs/base/common/uriIpc.ts -index ef2291d49b1..29b2f9dfc2b 100644 +index ef2291d49b13c9c995afc90eab9c92afabc2b3b4..29b2f9dfc2b7fa998ac1188db06dee95419fcd5b 100644 --- a/src/vs/base/common/uriIpc.ts +++ b/src/vs/base/common/uriIpc.ts @@ -5,6 +5,7 @@ @@ -416,7 +443,7 @@ index ef2291d49b1..29b2f9dfc2b 100644 \ No newline at end of file +} diff --git a/src/vs/base/node/languagePacks.js b/src/vs/base/node/languagePacks.js -index 2c64061da7b..c0ef8faedd4 100644 +index 2c64061da7b01aef0bfe3cec851da232ca9461c8..c0ef8faedd406c38bf9c55bbbdbbb060046492d9 100644 --- a/src/vs/base/node/languagePacks.js +++ b/src/vs/base/node/languagePacks.js @@ -128,7 +128,10 @@ function factory(nodeRequire, path, fs, perf) { @@ -432,19 +459,18 @@ index 2c64061da7b..c0ef8faedd4 100644 // Do nothing. If we can't read the file we have no // language pack config. diff --git a/src/vs/code/browser/workbench/workbench.ts b/src/vs/code/browser/workbench/workbench.ts -index c629f7fffa1..c266e1fb06f 100644 +index ad5272b22320a361cec0eed40d57629b06147c01..c9280b14472507ebb9a277f554485f08b090cb69 100644 --- a/src/vs/code/browser/workbench/workbench.ts +++ b/src/vs/code/browser/workbench/workbench.ts -@@ -13,6 +13,8 @@ import { isFolderToOpen, isWorkspaceToOpen } from 'vs/platform/windows/common/wi - import { isEqual } from 'vs/base/common/resources'; +@@ -16,6 +16,7 @@ import { isEqual } from 'vs/base/common/resources'; import { isStandalone } from 'vs/base/browser/browser'; import { localize } from 'vs/nls'; -+import { Schemas } from 'vs/base/common/network'; + import { Schemas } from 'vs/base/common/network'; +import { encodePath } from 'vs/server/node/util'; interface ICredential { service: string; -@@ -243,12 +245,18 @@ class WorkspaceProvider implements IWorkspaceProvider { +@@ -253,12 +254,18 @@ class WorkspaceProvider implements IWorkspaceProvider { // Folder else if (isFolderToOpen(workspace)) { @@ -465,7 +491,7 @@ index c629f7fffa1..c266e1fb06f 100644 } // Append payload if any -@@ -285,7 +293,22 @@ class WorkspaceProvider implements IWorkspaceProvider { +@@ -348,7 +355,22 @@ class WindowIndicator implements IWindowIndicator { throw new Error('Missing web configuration element'); } @@ -489,7 +515,7 @@ index c629f7fffa1..c266e1fb06f 100644 // Revive static extension locations if (Array.isArray(config.staticExtensions)) { -@@ -297,40 +320,7 @@ class WorkspaceProvider implements IWorkspaceProvider { +@@ -360,40 +382,7 @@ class WindowIndicator implements IWindowIndicator { // Find workspace to open and payload let foundWorkspace = false; let workspace: IWorkspace; @@ -532,7 +558,7 @@ index c629f7fffa1..c266e1fb06f 100644 // If no workspace is provided through the URL, check for config attribute from server if (!foundWorkspace) { diff --git a/src/vs/platform/environment/node/argv.ts b/src/vs/platform/environment/node/argv.ts -index 2379b626c81..28f8971cf39 100644 +index 92dd2bcf87dba5e5f07f2707a91b1a364ab1b05f..047522bd1a2c1edfda05c3739838fecbd70db6c5 100644 --- a/src/vs/platform/environment/node/argv.ts +++ b/src/vs/platform/environment/node/argv.ts @@ -8,6 +8,8 @@ import { localize } from 'vs/nls'; @@ -544,7 +570,7 @@ index 2379b626c81..28f8971cf39 100644 _: string[]; 'folder-uri'?: string[]; // undefined or array of 1 or more 'file-uri'?: string[]; // undefined or array of 1 or more -@@ -141,6 +143,8 @@ export const OPTIONS: OptionDescriptions> = { +@@ -142,6 +144,8 @@ export const OPTIONS: OptionDescriptions> = { 'extensions-dir': { type: 'string', deprecates: 'extensionHomePath', cat: 'e', args: 'dir', description: localize('extensionHomePath', "Set the root path for extensions.") }, 'extensions-download-dir': { type: 'string' }, 'builtin-extensions-dir': { type: 'string' }, @@ -553,13 +579,13 @@ index 2379b626c81..28f8971cf39 100644 'list-extensions': { type: 'boolean', cat: 'e', description: localize('listExtensions', "List the installed extensions.") }, 'show-versions': { type: 'boolean', cat: 'e', description: localize('showVersions', "Show versions of installed extensions, when using --list-extension.") }, 'category': { type: 'string', cat: 'e', description: localize('category', "Filters installed extensions by provided category, when using --list-extension.") }, -@@ -403,4 +407,3 @@ export function buildHelpMessage(productName: string, executableName: string, ve +@@ -405,4 +409,3 @@ export function buildHelpMessage(productName: string, executableName: string, ve export function buildVersionMessage(version: string | undefined, commit: string | undefined): string { return `${version || localize('unknownVersion', "Unknown version")}\n${commit || localize('unknownCommit', "Unknown commit")}\n${process.arch}`; } - diff --git a/src/vs/platform/environment/node/environmentService.ts b/src/vs/platform/environment/node/environmentService.ts -index 5c0dc4ad4ae..38b8c7573a8 100644 +index 45d5ec2cc02707d91f19a66d408ae46a1201a9e8..4ed498c63ceb55d15bd104a92b701ead3dfa81f2 100644 --- a/src/vs/platform/environment/node/environmentService.ts +++ b/src/vs/platform/environment/node/environmentService.ts @@ -38,6 +38,8 @@ export interface INativeEnvironmentService extends IEnvironmentService { @@ -586,7 +612,7 @@ index 5c0dc4ad4ae..38b8c7573a8 100644 get extensionDevelopmentLocationURI(): URI[] | undefined { const s = this._args.extensionDevelopmentPath; diff --git a/src/vs/platform/extensionManagement/node/extensionsScanner.ts b/src/vs/platform/extensionManagement/node/extensionsScanner.ts -index 575b2aafc38..873181f9678 100644 +index 575b2aafc3802cd6f5f943930e30de9f2c2690de..873181f967856759e3dc001e5bbe06e2f9eb676a 100644 --- a/src/vs/platform/extensionManagement/node/extensionsScanner.ts +++ b/src/vs/platform/extensionManagement/node/extensionsScanner.ts @@ -85,7 +85,7 @@ export class ExtensionsScanner extends Disposable { @@ -633,7 +659,7 @@ index 575b2aafc38..873181f9678 100644 + } } diff --git a/src/vs/platform/product/common/product.ts b/src/vs/platform/product/common/product.ts -index 3370a608b4b..37b3592d39d 100644 +index bb33203d1727b1c076efac9113afc3b2580cdbd9..c53cea338cdaa0f0ac15542c129e1572b3f13b80 100644 --- a/src/vs/platform/product/common/product.ts +++ b/src/vs/platform/product/common/product.ts @@ -30,6 +30,12 @@ if (isWeb) { @@ -650,7 +676,7 @@ index 3370a608b4b..37b3592d39d 100644 // Node: AMD loader diff --git a/src/vs/platform/product/common/productService.ts b/src/vs/platform/product/common/productService.ts -index 040c869d94c..bf16defcf7b 100644 +index d1cb00a6d63621a4873a6a5e815220d084ceac2a..1a69d6f63a7406d364aa3e2b32fb75309f212e98 100644 --- a/src/vs/platform/product/common/productService.ts +++ b/src/vs/platform/product/common/productService.ts @@ -30,6 +30,8 @@ export type ConfigurationSyncStore = { @@ -663,7 +689,7 @@ index 040c869d94c..bf16defcf7b 100644 readonly date?: string; readonly quality?: string; diff --git a/src/vs/platform/remote/browser/browserSocketFactory.ts b/src/vs/platform/remote/browser/browserSocketFactory.ts -index 3715cbb8e6e..c65de8ad37e 100644 +index 3715cbb8e6ee41c3d9b5090918d243b723ae2d00..c65de8ad37e727d66da97a8f8b170cbcef87181b 100644 --- a/src/vs/platform/remote/browser/browserSocketFactory.ts +++ b/src/vs/platform/remote/browser/browserSocketFactory.ts @@ -208,7 +208,8 @@ export class BrowserSocketFactory implements ISocketFactory { @@ -684,10 +710,10 @@ index 3715cbb8e6e..c65de8ad37e 100644 - - diff --git a/src/vs/platform/remote/common/remoteAgentConnection.ts b/src/vs/platform/remote/common/remoteAgentConnection.ts -index 2185bb5228c..35463ca6520 100644 +index 18d3d04fd20335975293e37b3b641120dd92da20..4e49f9d63623da6c84624144765f76ec127ea526 100644 --- a/src/vs/platform/remote/common/remoteAgentConnection.ts +++ b/src/vs/platform/remote/common/remoteAgentConnection.ts -@@ -89,7 +89,7 @@ async function connectToRemoteExtensionHostAgent(options: ISimpleConnectionOptio +@@ -92,7 +92,7 @@ async function connectToRemoteExtensionHostAgent(options: ISimpleConnectionOptio options.socketFactory.connect( options.host, options.port, @@ -696,9 +722,52 @@ index 2185bb5228c..35463ca6520 100644 (err: any, socket: ISocket | undefined) => { if (err || !socket) { options.logService.error(`${logPrefix} socketFactory.connect() failed. Error:`); +diff --git a/src/vs/platform/storage/browser/storageService.ts b/src/vs/platform/storage/browser/storageService.ts +index ab3fd347b69f8a3d9b96e706cd87c911b8ffed6b..9d351037b577f9f1edfd18ae9b3c48a211f4467f 100644 +--- a/src/vs/platform/storage/browser/storageService.ts ++++ b/src/vs/platform/storage/browser/storageService.ts +@@ -122,8 +122,8 @@ export class BrowserStorageService extends Disposable implements IStorageService + return this.getStorage(scope).getNumber(key, fallbackValue); + } + +- store(key: string, value: string | boolean | number | undefined | null, scope: StorageScope): void { +- this.getStorage(scope).set(key, value); ++ store(key: string, value: string | boolean | number | undefined | null, scope: StorageScope): Promise { ++ return this.getStorage(scope).set(key, value); + } + + remove(key: string, scope: StorageScope): void { +diff --git a/src/vs/platform/storage/common/storage.ts b/src/vs/platform/storage/common/storage.ts +index 6611f1dae42055f69a55c1c154d9475f11cd4d0a..d598d4909d5ff6d1614e4a038b1865e1f9a4e963 100644 +--- a/src/vs/platform/storage/common/storage.ts ++++ b/src/vs/platform/storage/common/storage.ts +@@ -85,7 +85,7 @@ export interface IStorageService { + * The scope argument allows to define the scope of the storage + * operation to either the current workspace only or all workspaces. + */ +- store(key: string, value: string | boolean | number | undefined | null, scope: StorageScope): void; ++ store(key: string, value: string | boolean | number | undefined | null, scope: StorageScope): Promise | void; + + /** + * Delete an element stored under the provided key from storage. +diff --git a/src/vs/platform/storage/node/storageService.ts b/src/vs/platform/storage/node/storageService.ts +index ac657056aa68549f0053cfb1ec68835ba4ce20f9..143f9b5681eb867c5e5c5437946ab785eb34e4b4 100644 +--- a/src/vs/platform/storage/node/storageService.ts ++++ b/src/vs/platform/storage/node/storageService.ts +@@ -202,8 +202,8 @@ export class NativeStorageService extends Disposable implements IStorageService + return this.getStorage(scope).getNumber(key, fallbackValue); + } + +- store(key: string, value: string | boolean | number | undefined | null, scope: StorageScope): void { +- this.getStorage(scope).set(key, value); ++ store(key: string, value: string | boolean | number | undefined | null, scope: StorageScope): Promise { ++ return this.getStorage(scope).set(key, value); + } + + remove(key: string, scope: StorageScope): void { diff --git a/src/vs/server/browser/client.ts b/src/vs/server/browser/client.ts new file mode 100644 -index 00000000000..3c0703b7174 +index 0000000000000000000000000000000000000000..3c0703b7174ad792a4b42841e96ee93765d71601 --- /dev/null +++ b/src/vs/server/browser/client.ts @@ -0,0 +1,189 @@ @@ -893,7 +962,7 @@ index 00000000000..3c0703b7174 +}; diff --git a/src/vs/server/browser/extHostNodeProxy.ts b/src/vs/server/browser/extHostNodeProxy.ts new file mode 100644 -index 00000000000..ed7c078077b +index 0000000000000000000000000000000000000000..ed7c078077b0c375758529959b280e091436113a --- /dev/null +++ b/src/vs/server/browser/extHostNodeProxy.ts @@ -0,0 +1,46 @@ @@ -945,7 +1014,7 @@ index 00000000000..ed7c078077b +export const IExtHostNodeProxy = createDecorator('IExtHostNodeProxy'); diff --git a/src/vs/server/browser/mainThreadNodeProxy.ts b/src/vs/server/browser/mainThreadNodeProxy.ts new file mode 100644 -index 00000000000..0d2e93edae2 +index 0000000000000000000000000000000000000000..0d2e93edae2baf34d27b7b52be0bf4960f244531 --- /dev/null +++ b/src/vs/server/browser/mainThreadNodeProxy.ts @@ -0,0 +1,37 @@ @@ -988,7 +1057,7 @@ index 00000000000..0d2e93edae2 +} diff --git a/src/vs/server/browser/worker.ts b/src/vs/server/browser/worker.ts new file mode 100644 -index 00000000000..5ae44cdc856 +index 0000000000000000000000000000000000000000..5ae44cdc856bf81326a4c516b8be9afb2c746a67 --- /dev/null +++ b/src/vs/server/browser/worker.ts @@ -0,0 +1,56 @@ @@ -1050,7 +1119,7 @@ index 00000000000..5ae44cdc856 +}; diff --git a/src/vs/server/common/nodeProxy.ts b/src/vs/server/common/nodeProxy.ts new file mode 100644 -index 00000000000..14b9de879ce +index 0000000000000000000000000000000000000000..14b9de879ceab4c1976770fa7810d276c5aa3e36 --- /dev/null +++ b/src/vs/server/common/nodeProxy.ts @@ -0,0 +1,47 @@ @@ -1103,7 +1172,7 @@ index 00000000000..14b9de879ce +} diff --git a/src/vs/server/common/telemetry.ts b/src/vs/server/common/telemetry.ts new file mode 100644 -index 00000000000..4ea6d95d36a +index 0000000000000000000000000000000000000000..4ea6d95d36aaac07dbd4d0e16ab3c1bba255f683 --- /dev/null +++ b/src/vs/server/common/telemetry.ts @@ -0,0 +1,65 @@ @@ -1174,7 +1243,7 @@ index 00000000000..4ea6d95d36a +} diff --git a/src/vs/server/entry.ts b/src/vs/server/entry.ts new file mode 100644 -index 00000000000..ab020fbb4e4 +index 0000000000000000000000000000000000000000..ab020fbb4e4ab3748cc807765ff9c362389faafa --- /dev/null +++ b/src/vs/server/entry.ts @@ -0,0 +1,78 @@ @@ -1258,7 +1327,7 @@ index 00000000000..ab020fbb4e4 +} diff --git a/src/vs/server/fork.js b/src/vs/server/fork.js new file mode 100644 -index 00000000000..56331ff1fc3 +index 0000000000000000000000000000000000000000..56331ff1fc32bbd82e769aaecb551e427f798ec3 --- /dev/null +++ b/src/vs/server/fork.js @@ -0,0 +1,3 @@ @@ -1267,7 +1336,7 @@ index 00000000000..56331ff1fc3 +require('../../bootstrap-amd').load('vs/server/entry'); diff --git a/src/vs/server/ipc.d.ts b/src/vs/server/ipc.d.ts new file mode 100644 -index 00000000000..33b28cf2d53 +index 0000000000000000000000000000000000000000..33b28cf2d53746ee9c50c056ac2e087dcee0a4e2 --- /dev/null +++ b/src/vs/server/ipc.d.ts @@ -0,0 +1,131 @@ @@ -1404,7 +1473,7 @@ index 00000000000..33b28cf2d53 +} diff --git a/src/vs/server/node/channel.ts b/src/vs/server/node/channel.ts new file mode 100644 -index 00000000000..e10cc9c218b +index 0000000000000000000000000000000000000000..e10cc9c218b27d859a523be3db5b8a30ef90d953 --- /dev/null +++ b/src/vs/server/node/channel.ts @@ -0,0 +1,360 @@ @@ -1770,7 +1839,7 @@ index 00000000000..e10cc9c218b +} diff --git a/src/vs/server/node/connection.ts b/src/vs/server/node/connection.ts new file mode 100644 -index 00000000000..36e80fb6966 +index 0000000000000000000000000000000000000000..36e80fb6966ae2cb53c98f3d31e2193d00c509c3 --- /dev/null +++ b/src/vs/server/node/connection.ts @@ -0,0 +1,157 @@ @@ -1933,7 +2002,7 @@ index 00000000000..36e80fb6966 +} diff --git a/src/vs/server/node/insights.ts b/src/vs/server/node/insights.ts new file mode 100644 -index 00000000000..a0ece345f28 +index 0000000000000000000000000000000000000000..a0ece345f28f06afb2af12fe4901ad228b2475a4 --- /dev/null +++ b/src/vs/server/node/insights.ts @@ -0,0 +1,124 @@ @@ -2063,7 +2132,7 @@ index 00000000000..a0ece345f28 +} diff --git a/src/vs/server/node/ipc.ts b/src/vs/server/node/ipc.ts new file mode 100644 -index 00000000000..5e560eb46e6 +index 0000000000000000000000000000000000000000..5e560eb46e6a0a18c91e440c655ac0d44b09b6dd --- /dev/null +++ b/src/vs/server/node/ipc.ts @@ -0,0 +1,61 @@ @@ -2130,7 +2199,7 @@ index 00000000000..5e560eb46e6 +export const ipcMain = new IpcMain(); diff --git a/src/vs/server/node/logger.ts b/src/vs/server/node/logger.ts new file mode 100644 -index 00000000000..2a39c524aaa +index 0000000000000000000000000000000000000000..2a39c524aaa1b4031e04a631842f30b6fec3d98a --- /dev/null +++ b/src/vs/server/node/logger.ts @@ -0,0 +1,2 @@ @@ -2138,7 +2207,7 @@ index 00000000000..2a39c524aaa +export const logger = baseLogger.named('vscode'); diff --git a/src/vs/server/node/marketplace.ts b/src/vs/server/node/marketplace.ts new file mode 100644 -index 00000000000..8956fc40d48 +index 0000000000000000000000000000000000000000..8956fc40d48448b9932036c4c286464881807338 --- /dev/null +++ b/src/vs/server/node/marketplace.ts @@ -0,0 +1,174 @@ @@ -2318,7 +2387,7 @@ index 00000000000..8956fc40d48 +}; diff --git a/src/vs/server/node/nls.ts b/src/vs/server/node/nls.ts new file mode 100644 -index 00000000000..3d428a57d31 +index 0000000000000000000000000000000000000000..3d428a57d31f29c40f9c3ce45f715b443badf4e9 --- /dev/null +++ b/src/vs/server/node/nls.ts @@ -0,0 +1,88 @@ @@ -2412,7 +2481,7 @@ index 00000000000..3d428a57d31 +}; diff --git a/src/vs/server/node/protocol.ts b/src/vs/server/node/protocol.ts new file mode 100644 -index 00000000000..3c74512192a +index 0000000000000000000000000000000000000000..3c74512192aec6220216bc8563b3127b9cfd5fbf --- /dev/null +++ b/src/vs/server/node/protocol.ts @@ -0,0 +1,73 @@ @@ -2491,7 +2560,7 @@ index 00000000000..3c74512192a +} diff --git a/src/vs/server/node/server.ts b/src/vs/server/node/server.ts new file mode 100644 -index 00000000000..4b88fedb2f0 +index 0000000000000000000000000000000000000000..4b88fedb2f05d300fb50978e63721d4d04b7fb5f --- /dev/null +++ b/src/vs/server/node/server.ts @@ -0,0 +1,285 @@ @@ -2782,7 +2851,7 @@ index 00000000000..4b88fedb2f0 +} diff --git a/src/vs/server/node/util.ts b/src/vs/server/node/util.ts new file mode 100644 -index 00000000000..fa47e993b46 +index 0000000000000000000000000000000000000000..fa47e993b46802f1a26457649e9e8bc467a73bf2 --- /dev/null +++ b/src/vs/server/node/util.ts @@ -0,0 +1,13 @@ @@ -2800,7 +2869,7 @@ index 00000000000..fa47e993b46 + return path.split("/").map((p) => encodeURIComponent(p)).join("/"); +}; diff --git a/src/vs/workbench/api/browser/extensionHost.contribution.ts b/src/vs/workbench/api/browser/extensionHost.contribution.ts -index 3d77009b908..11deb1b99ac 100644 +index bfabf0008910c87146df53a2e10fe63bae517a86..32b3b1cf84c8d280fd7f03d541b867691d51c2fb 100644 --- a/src/vs/workbench/api/browser/extensionHost.contribution.ts +++ b/src/vs/workbench/api/browser/extensionHost.contribution.ts @@ -60,6 +60,7 @@ import './mainThreadComments'; @@ -2811,8 +2880,26 @@ index 3d77009b908..11deb1b99ac 100644 import './mainThreadTunnelService'; import './mainThreadAuthentication'; import './mainThreadTimeline'; +diff --git a/src/vs/workbench/api/browser/mainThreadStorage.ts b/src/vs/workbench/api/browser/mainThreadStorage.ts +index 7bc3904963bed2925f3640b6bd929347159dd3cf..c6db2368ae9eaca61889efcf3c49763c01ff7459 100644 +--- a/src/vs/workbench/api/browser/mainThreadStorage.ts ++++ b/src/vs/workbench/api/browser/mainThreadStorage.ts +@@ -58,11 +58,11 @@ export class MainThreadStorage implements MainThreadStorageShape { + return JSON.parse(jsonValue); + } + +- $setValue(shared: boolean, key: string, value: object): Promise { ++ async $setValue(shared: boolean, key: string, value: object): Promise { + let jsonValue: string; + try { + jsonValue = JSON.stringify(value); +- this._storageService.store(key, jsonValue, shared ? StorageScope.GLOBAL : StorageScope.WORKSPACE); ++ await this._storageService.store(key, jsonValue, shared ? StorageScope.GLOBAL : StorageScope.WORKSPACE); + } catch (err) { + return Promise.reject(err); + } diff --git a/src/vs/workbench/api/common/extHost.api.impl.ts b/src/vs/workbench/api/common/extHost.api.impl.ts -index 97793666ad8..13cd137db1e 100644 +index 3595cd3e38136222044a13050b15105bbe539068..989caefff7c4b8203c03cec8fa451f5e70ea8964 100644 --- a/src/vs/workbench/api/common/extHost.api.impl.ts +++ b/src/vs/workbench/api/common/extHost.api.impl.ts @@ -68,6 +68,7 @@ import { IURITransformerService } from 'vs/workbench/api/common/extHostUriTransf @@ -2823,7 +2910,7 @@ index 97793666ad8..13cd137db1e 100644 import { ExtHostTheming } from 'vs/workbench/api/common/extHostTheming'; import { IExtHostTunnelService } from 'vs/workbench/api/common/extHostTunnelService'; import { IExtHostApiDeprecationService } from 'vs/workbench/api/common/extHostApiDeprecationService'; -@@ -97,6 +98,7 @@ export function createApiFactoryAndRegisterActors(accessor: ServicesAccessor): I +@@ -100,6 +101,7 @@ export function createApiFactoryAndRegisterActors(accessor: ServicesAccessor): I const extHostStorage = accessor.get(IExtHostStorage); const extensionStoragePaths = accessor.get(IExtensionStoragePaths); const extHostLogService = accessor.get(ILogService); @@ -2831,7 +2918,7 @@ index 97793666ad8..13cd137db1e 100644 const extHostTunnelService = accessor.get(IExtHostTunnelService); const extHostApiDeprecation = accessor.get(IExtHostApiDeprecationService); const extHostWindow = accessor.get(IExtHostWindow); -@@ -107,6 +109,7 @@ export function createApiFactoryAndRegisterActors(accessor: ServicesAccessor): I +@@ -110,6 +112,7 @@ export function createApiFactoryAndRegisterActors(accessor: ServicesAccessor): I rpcProtocol.set(ExtHostContext.ExtHostConfiguration, extHostConfiguration); rpcProtocol.set(ExtHostContext.ExtHostExtensionService, extensionService); rpcProtocol.set(ExtHostContext.ExtHostStorage, extHostStorage); @@ -2840,10 +2927,10 @@ index 97793666ad8..13cd137db1e 100644 rpcProtocol.set(ExtHostContext.ExtHostWindow, extHostWindow); diff --git a/src/vs/workbench/api/common/extHost.protocol.ts b/src/vs/workbench/api/common/extHost.protocol.ts -index eb5d8ea8455..da9eb521ca4 100644 +index 4b7946662950f18179a5b6e3552abd39e68ca80e..ca1352d311a94b42e18d0d9e4859b18ec2bb271d 100644 --- a/src/vs/workbench/api/common/extHost.protocol.ts +++ b/src/vs/workbench/api/common/extHost.protocol.ts -@@ -769,6 +769,16 @@ export interface MainThreadLabelServiceShape extends IDisposable { +@@ -795,6 +795,16 @@ export interface MainThreadLabelServiceShape extends IDisposable { $unregisterResourceLabelFormatter(handle: number): void; } @@ -2860,7 +2947,7 @@ index eb5d8ea8455..da9eb521ca4 100644 export interface MainThreadSearchShape extends IDisposable { $registerFileSearchProvider(handle: number, scheme: string): void; $registerTextSearchProvider(handle: number, scheme: string): void; -@@ -1707,6 +1717,7 @@ export const MainContext = { +@@ -1765,6 +1775,7 @@ export const MainContext = { MainThreadWindow: createMainId('MainThreadWindow'), MainThreadLabelService: createMainId('MainThreadLabelService'), MainThreadNotebook: createMainId('MainThreadNotebook'), @@ -2868,7 +2955,7 @@ index eb5d8ea8455..da9eb521ca4 100644 MainThreadTheming: createMainId('MainThreadTheming'), MainThreadTunnelService: createMainId('MainThreadTunnelService'), MainThreadTimeline: createMainId('MainThreadTimeline') -@@ -1745,6 +1756,7 @@ export const ExtHostContext = { +@@ -1806,6 +1817,7 @@ export const ExtHostContext = { ExtHostOutputService: createMainId('ExtHostOutputService'), ExtHosLabelService: createMainId('ExtHostLabelService'), ExtHostNotebook: createMainId('ExtHostNotebook'), @@ -2877,10 +2964,10 @@ index eb5d8ea8455..da9eb521ca4 100644 ExtHostTunnelService: createMainId('ExtHostTunnelService'), ExtHostAuthentication: createMainId('ExtHostAuthentication'), diff --git a/src/vs/workbench/api/common/extHostExtensionService.ts b/src/vs/workbench/api/common/extHostExtensionService.ts -index 34639e18b6f..9c22fe6f090 100644 +index 0bb5188614bcbf98b85c9208edc2b173f70b1670..38ff3e2e05645be8df619ed2b47fa2984b918741 100644 --- a/src/vs/workbench/api/common/extHostExtensionService.ts +++ b/src/vs/workbench/api/common/extHostExtensionService.ts -@@ -32,6 +32,7 @@ import { IExtHostInitDataService } from 'vs/workbench/api/common/extHostInitData +@@ -31,6 +31,7 @@ import { IExtHostInitDataService } from 'vs/workbench/api/common/extHostInitData import { IExtensionStoragePaths } from 'vs/workbench/api/common/extHostStoragePaths'; import { IExtHostRpcService } from 'vs/workbench/api/common/extHostRpcService'; import { ServiceCollection } from 'vs/platform/instantiation/common/serviceCollection'; @@ -2921,7 +3008,7 @@ index 34639e18b6f..9c22fe6f090 100644 this._loadExtensionContext(extensionDescription) ]).then(values => { return AbstractExtHostExtensionService._callActivate(this._logService, extensionDescription.identifier, values[0], values[1], activationTimesBuilder); -@@ -754,7 +758,7 @@ export abstract class AbstractExtHostExtensionService extends Disposable impleme +@@ -746,7 +750,7 @@ export abstract class AbstractExtHostExtensionService extends Disposable impleme protected abstract _beforeAlmostReadyToRunExtensions(): Promise; protected abstract _getEntryPoint(extensionDescription: IExtensionDescription): string | undefined; @@ -2931,7 +3018,7 @@ index 34639e18b6f..9c22fe6f090 100644 } diff --git a/src/vs/workbench/api/node/extHost.node.services.ts b/src/vs/workbench/api/node/extHost.node.services.ts -index b3c89e51cfc..e21abe4e13b 100644 +index b3c89e51cfc25a53293a352a2a8ad50d5f26d595..e21abe4e13bc25a5b72f556bbfb61085842faeb7 100644 --- a/src/vs/workbench/api/node/extHost.node.services.ts +++ b/src/vs/workbench/api/node/extHost.node.services.ts @@ -3,6 +3,8 @@ @@ -2948,8 +3035,33 @@ index b3c89e51cfc..e21abe4e13b 100644 registerSingleton(IExtHostTerminalService, ExtHostTerminalService); registerSingleton(IExtHostTunnelService, ExtHostTunnelService); +registerSingleton(IExtHostNodeProxy, class extends NotImplementedProxy(String(IExtHostNodeProxy)) { whenReady = Promise.resolve(); }); +diff --git a/src/vs/workbench/api/node/extHostCLIServer.ts b/src/vs/workbench/api/node/extHostCLIServer.ts +index 7cae126cc0f804273850933468690e0f9f10a5b8..08c2aa5cdae3f3d06bb08b7055dc7e7def260132 100644 +--- a/src/vs/workbench/api/node/extHostCLIServer.ts ++++ b/src/vs/workbench/api/node/extHostCLIServer.ts +@@ -11,6 +11,8 @@ import { IWindowOpenable, IOpenWindowOptions } from 'vs/platform/windows/common/ + import { URI } from 'vs/base/common/uri'; + import { hasWorkspaceFileExtension } from 'vs/platform/workspaces/common/workspaces'; + import { ILogService } from 'vs/platform/log/common/log'; ++import { join } from 'vs/base/common/path'; ++import { tmpdir } from 'os'; + + export interface OpenCommandPipeArgs { + type: 'open'; +@@ -54,6 +56,11 @@ export class CLIServer { + private async setup(): Promise { + this._ipcHandlePath = generateRandomPipeName(); + ++ // NOTE@coder: Write this out so we can get the most recent path. ++ fs.promises.writeFile(join(tmpdir(), "vscode-ipc"), this._ipcHandlePath).catch((error) => { ++ this.logService.error(error); ++ }); ++ + try { + this._server.listen(this.ipcHandlePath); + this._server.on('error', err => this.logService.error(err)); diff --git a/src/vs/workbench/api/worker/extHost.worker.services.ts b/src/vs/workbench/api/worker/extHost.worker.services.ts -index 3843fdec386..8aac4df5278 100644 +index 3843fdec386edc09a1d361b63de892a04e0070ed..8aac4df527857e964798362a69f5591bef07c165 100644 --- a/src/vs/workbench/api/worker/extHost.worker.services.ts +++ b/src/vs/workbench/api/worker/extHost.worker.services.ts @@ -8,6 +8,7 @@ import { ILogService } from 'vs/platform/log/common/log'; @@ -2966,18 +3078,18 @@ index 3843fdec386..8aac4df5278 100644 registerSingleton(ILogService, ExtHostLogService); +registerSingleton(IExtHostNodeProxy, ExtHostNodeProxy); diff --git a/src/vs/workbench/api/worker/extHostExtensionService.ts b/src/vs/workbench/api/worker/extHostExtensionService.ts -index c71ab1c7da4..572b07ff251 100644 +index a6a149083719d7479268e24eb5339f6cbf93e655..360888dc7dff9437f6c85f7a2043ad9e7c4daf21 100644 --- a/src/vs/workbench/api/worker/extHostExtensionService.ts +++ b/src/vs/workbench/api/worker/extHostExtensionService.ts -@@ -9,6 +9,7 @@ import { AbstractExtHostExtensionService } from 'vs/workbench/api/common/extHost - import { URI } from 'vs/base/common/uri'; +@@ -10,6 +10,7 @@ import { URI } from 'vs/base/common/uri'; import { RequireInterceptor } from 'vs/workbench/api/common/extHostRequireInterceptor'; import { IExtensionDescription } from 'vs/platform/extensions/common/extensions'; + import { ExtensionRuntime } from 'vs/workbench/api/common/extHostTypes'; +import { loadCommonJSModule } from 'vs/server/browser/worker'; class WorkerRequireInterceptor extends RequireInterceptor { -@@ -42,10 +43,15 @@ export class ExtHostExtensionService extends AbstractExtHostExtensionService { +@@ -44,10 +45,15 @@ export class ExtHostExtensionService extends AbstractExtHostExtensionService { } protected _getEntryPoint(extensionDescription: IExtensionDescription): string | undefined { @@ -2996,7 +3108,7 @@ index c71ab1c7da4..572b07ff251 100644 module = module.with({ path: ensureSuffix(module.path, '.js') }); const response = await fetch(module.toString(true)); diff --git a/src/vs/workbench/browser/parts/activitybar/media/activitybarpart.css b/src/vs/workbench/browser/parts/activitybar/media/activitybarpart.css -index ced2d815834..dfcae73e8a0 100644 +index ced2d815834e40a1543e80516472799075980733..dfcae73e8a042307600c67f163aa00ba9e0762f4 100644 --- a/src/vs/workbench/browser/parts/activitybar/media/activitybarpart.css +++ b/src/vs/workbench/browser/parts/activitybar/media/activitybarpart.css @@ -55,6 +55,10 @@ @@ -3011,7 +3123,7 @@ index ced2d815834..dfcae73e8a0 100644 .monaco-workbench .activitybar > .content > .home-bar > .home-bar-icon-badge { diff --git a/src/vs/workbench/browser/web.main.ts b/src/vs/workbench/browser/web.main.ts -index 0462617196b..11434d27af9 100644 +index 511d7376a2bfebde59b4c67fed54c39e9dd534c9..c7c45f8e4e4ffe56a8782f58af75c6a7835142cf 100644 --- a/src/vs/workbench/browser/web.main.ts +++ b/src/vs/workbench/browser/web.main.ts @@ -45,6 +45,7 @@ import { FileLogService } from 'vs/platform/log/common/fileLogService'; @@ -3022,7 +3134,7 @@ index 0462617196b..11434d27af9 100644 import { coalesce } from 'vs/base/common/arrays'; import { InMemoryFileSystemProvider } from 'vs/platform/files/common/inMemoryFilesystemProvider'; import { WebResourceIdentityService, IResourceIdentityService } from 'vs/platform/resource/common/resourceIdentityService'; -@@ -84,6 +85,8 @@ class BrowserMain extends Disposable { +@@ -87,6 +88,8 @@ class BrowserMain extends Disposable { // Startup const instantiationService = workbench.startup(); @@ -3032,7 +3144,7 @@ index 0462617196b..11434d27af9 100644 return instantiationService.invokeFunction(accessor => { const commandService = accessor.get(ICommandService); diff --git a/src/vs/workbench/common/resources.ts b/src/vs/workbench/common/resources.ts -index 18ea0bfedb4..d59a17c17f4 100644 +index 18ea0bfedb4492327429a38237b05915b29f6dd0..d59a17c17f4fffa23d786ce36b4ff624d5688a58 100644 --- a/src/vs/workbench/common/resources.ts +++ b/src/vs/workbench/common/resources.ts @@ -15,6 +15,7 @@ import { ParsedExpression, IExpression, parse } from 'vs/base/common/glob'; @@ -3054,7 +3166,7 @@ index 18ea0bfedb4..d59a17c17f4 100644 this._langIdKey.set(value ? this._modeService.getModeIdByFilepathOrFirstLine(value) : null); this._extensionKey.set(value ? extname(value) : null); diff --git a/src/vs/workbench/contrib/scm/browser/media/scm.css b/src/vs/workbench/contrib/scm/browser/media/scm.css -index 9947f240bf2..bdba0a2fc64 100644 +index b1838de8f21c60141d01cc424a5e000a32f1c828..0a480032e0cc8d5219cd240f8807aa317718659d 100644 --- a/src/vs/workbench/contrib/scm/browser/media/scm.css +++ b/src/vs/workbench/contrib/scm/browser/media/scm.css @@ -138,9 +138,11 @@ @@ -3073,7 +3185,7 @@ index 9947f240bf2..bdba0a2fc64 100644 .scm-view .monaco-list .monaco-list-row .resource-group > .actions, .scm-view .monaco-list .monaco-list-row .resource > .name > .monaco-icon-label > .actions { diff --git a/src/vs/workbench/services/dialogs/browser/dialogService.ts b/src/vs/workbench/services/dialogs/browser/dialogService.ts -index 6e3182a696d..7df85da165a 100644 +index 1360c248eb7ff937c92d08bbf30d2b76ea606dc0..adccf8b88d62381c3ec484df40c6d63142ec9ef5 100644 --- a/src/vs/workbench/services/dialogs/browser/dialogService.ts +++ b/src/vs/workbench/services/dialogs/browser/dialogService.ts @@ -124,11 +124,12 @@ export class DialogService implements IDialogService { @@ -3092,10 +3204,10 @@ index 6e3182a696d..7df85da165a 100644 }; diff --git a/src/vs/workbench/services/environment/browser/environmentService.ts b/src/vs/workbench/services/environment/browser/environmentService.ts -index ba2701ec54d..4d4aaa6958b 100644 +index 819607be0c13fed28eb7fbe6d4a62c0b860b1aa9..b046943311b713a579cc3a94983ea1b7fca7b9b1 100644 --- a/src/vs/workbench/services/environment/browser/environmentService.ts +++ b/src/vs/workbench/services/environment/browser/environmentService.ts -@@ -121,8 +121,18 @@ export class BrowserWorkbenchEnvironmentService implements IWorkbenchEnvironment +@@ -116,8 +116,18 @@ export class BrowserWorkbenchEnvironmentService implements IWorkbenchEnvironment @memoize get logFile(): URI { return joinPath(this.options.logsPath, 'window.log'); } @@ -3115,7 +3227,7 @@ index ba2701ec54d..4d4aaa6958b 100644 @memoize get settingsResource(): URI { return joinPath(this.userRoamingDataHome, 'settings.json'); } -@@ -284,7 +294,12 @@ export class BrowserWorkbenchEnvironmentService implements IWorkbenchEnvironment +@@ -279,7 +289,12 @@ export class BrowserWorkbenchEnvironmentService implements IWorkbenchEnvironment extensionHostDebugEnvironment.params.port = parseInt(value); break; case 'enableProposedApi': @@ -3130,10 +3242,10 @@ index ba2701ec54d..4d4aaa6958b 100644 } } diff --git a/src/vs/workbench/services/extensionManagement/common/extensionEnablementService.ts b/src/vs/workbench/services/extensionManagement/common/extensionEnablementService.ts -index c28b1477400..6090200d9c3 100644 +index 32f3dc52c1ff645df6471a03542d6ec3eb73a277..c2f4497d2eba13a771b2665ad58f12ecdfa7606a 100644 --- a/src/vs/workbench/services/extensionManagement/common/extensionEnablementService.ts +++ b/src/vs/workbench/services/extensionManagement/common/extensionEnablementService.ts -@@ -163,7 +163,7 @@ export class ExtensionEnablementService extends Disposable implements IWorkbench +@@ -205,7 +205,7 @@ export class ExtensionEnablementService extends Disposable implements IWorkbench } } } @@ -3143,10 +3255,23 @@ index c28b1477400..6090200d9c3 100644 return false; } diff --git a/src/vs/workbench/services/extensionManagement/common/extensionManagementService.ts b/src/vs/workbench/services/extensionManagement/common/extensionManagementService.ts -index 33eb56db3c2..e5167794c3f 100644 +index a982b3ecc58c5a2f3a92be7b8cca3a1cacbb7d47..97f9bfcf0e679be683b1b09cd569149e7962f5ad 100644 --- a/src/vs/workbench/services/extensionManagement/common/extensionManagementService.ts +++ b/src/vs/workbench/services/extensionManagement/common/extensionManagementService.ts -@@ -236,6 +236,11 @@ export class ExtensionManagementService extends Disposable implements IExtension +@@ -211,8 +211,11 @@ export class ExtensionManagementService extends Disposable implements IExtension + } + + // Install Language pack on all servers ++ // NOTE@coder: It does not appear language packs can be installed on the web ++ // extension management server at this time. Filter out the web to fix this. + if (isLanguagePackExtension(manifest)) { +- return Promise.all(this.servers.map(server => server.extensionManagementService.installFromGallery(gallery))).then(([local]) => local); ++ const servers = this.servers.filter(s => s !== this.extensionManagementServerService.webExtensionManagementServer); ++ return Promise.all(servers.map(server => server.extensionManagementService.installFromGallery(gallery))).then(([local]) => local); + } + + // 1. Install on preferred location +@@ -245,6 +248,11 @@ export class ExtensionManagementService extends Disposable implements IExtension return this.extensionManagementServerService.webExtensionManagementServer.extensionManagementService.installFromGallery(gallery); } @@ -3159,10 +3284,10 @@ index 33eb56db3c2..e5167794c3f 100644 const error = new Error(localize('cannot be installed', "Cannot install '{0}' because this extension has defined that it cannot run on the remote server.", gallery.displayName || gallery.name)); error.name = INSTALL_ERROR_NOT_SUPPORTED; diff --git a/src/vs/workbench/services/extensions/browser/extensionService.ts b/src/vs/workbench/services/extensions/browser/extensionService.ts -index d0710e77fa2..ceb27174aee 100644 +index 9e979d28691d0b0b26fde5e46b606731e31f3da5..dd31879c7dd899c73c4a1371996912f4513bfd0d 100644 --- a/src/vs/workbench/services/extensions/browser/extensionService.ts +++ b/src/vs/workbench/services/extensions/browser/extensionService.ts -@@ -116,8 +116,10 @@ export class ExtensionService extends AbstractExtensionService implements IExten +@@ -125,8 +125,10 @@ export class ExtensionService extends AbstractExtensionService implements IExten this._remoteAgentService.getEnvironment(), this._remoteAgentService.scanExtensions() ]); @@ -3175,7 +3300,7 @@ index d0710e77fa2..ceb27174aee 100644 const remoteAgentConnection = this._remoteAgentService.getConnection(); this._runningLocation = _determineRunningLocation(this._productService, this._configService, localExtensions, remoteExtensions, Boolean(remoteEnv && remoteAgentConnection)); diff --git a/src/vs/workbench/services/extensions/common/extensionsUtil.ts b/src/vs/workbench/services/extensions/common/extensionsUtil.ts -index 65e532ee58d..0b6282fde7a 100644 +index 65e532ee58dfc06ed944846d01b885cb8f260ebc..0b6282fde7ad03c7ea9872a777cbf487253abed1 100644 --- a/src/vs/workbench/services/extensions/common/extensionsUtil.ts +++ b/src/vs/workbench/services/extensions/common/extensionsUtil.ts @@ -37,7 +37,8 @@ export function canExecuteOnWorkspace(manifest: IExtensionManifest, productServi @@ -3189,7 +3314,7 @@ index 65e532ee58d..0b6282fde7a 100644 export function getExtensionKind(manifest: IExtensionManifest, productService: IProductService, configurationService: IConfigurationService): ExtensionKind[] { diff --git a/src/vs/workbench/services/extensions/node/extensionHostProcessSetup.ts b/src/vs/workbench/services/extensions/node/extensionHostProcessSetup.ts -index 49542eda74c..de0e2da0a4c 100644 +index 49542eda74c65e485272cd37d586911886aa3ad7..de0e2da0a4c2dca91dc7e0e48c28a8a75ca3e7d4 100644 --- a/src/vs/workbench/services/extensions/node/extensionHostProcessSetup.ts +++ b/src/vs/workbench/services/extensions/node/extensionHostProcessSetup.ts @@ -16,7 +16,7 @@ import { IInitData } from 'vs/workbench/api/common/extHost.protocol'; @@ -3246,7 +3371,7 @@ index 49542eda74c..de0e2da0a4c 100644 console.error(e); } diff --git a/src/vs/workbench/services/extensions/worker/extensionHostWorkerMain.ts b/src/vs/workbench/services/extensions/worker/extensionHostWorkerMain.ts -index 79455414c06..a407593b4dc 100644 +index 79455414c06b95612a0dce2cad01f2bb2f40ef49..a407593b4dc6053309ed560898918cf67470e836 100644 --- a/src/vs/workbench/services/extensions/worker/extensionHostWorkerMain.ts +++ b/src/vs/workbench/services/extensions/worker/extensionHostWorkerMain.ts @@ -14,7 +14,11 @@ @@ -3263,7 +3388,7 @@ index 79455414c06..a407593b4dc 100644 require(['vs/workbench/services/extensions/worker/extensionHostWorker'], () => { }, err => console.error(err)); diff --git a/src/vs/workbench/services/localizations/electron-browser/localizationsService.ts b/src/vs/workbench/services/localizations/electron-browser/localizationsService.ts -index 44999bd842e..601b1c54088 100644 +index 44999bd842eae12b752b2e7e8c4904272b111dc1..601b1c5408835c743fe07e34da4d4534873bf832 100644 --- a/src/vs/workbench/services/localizations/electron-browser/localizationsService.ts +++ b/src/vs/workbench/services/localizations/electron-browser/localizationsService.ts @@ -5,17 +5,17 @@ @@ -3288,7 +3413,7 @@ index 44999bd842e..601b1c54088 100644 } diff --git a/src/vs/workbench/workbench.web.main.ts b/src/vs/workbench/workbench.web.main.ts -index 0669178db4c..28fafeb2de2 100644 +index f02bbbf874b5b18ac8d077ad56a8a4a57e77a4a6..86271940724aaf28e4eda93e59920820a7d93987 100644 --- a/src/vs/workbench/workbench.web.main.ts +++ b/src/vs/workbench/workbench.web.main.ts @@ -35,7 +35,8 @@ import 'vs/workbench/services/textfile/browser/browserTextFileService'; @@ -3302,7 +3427,7 @@ index 0669178db4c..28fafeb2de2 100644 import 'vs/workbench/services/credentials/browser/credentialsService'; import 'vs/workbench/services/url/browser/urlService'; diff --git a/yarn.lock b/yarn.lock -index b2fbf543af3..f10dddd6594 100644 +index 140ed883c1a92ebcd7a284b98ca71261fa9cb631..b363d7de5000fd370bb4221f48e193382648a185 100644 --- a/yarn.lock +++ b/yarn.lock @@ -140,6 +140,23 @@ @@ -3329,7 +3454,7 @@ index b2fbf543af3..f10dddd6594 100644 "@electron/get@^1.0.1": version "1.7.2" resolved "https://registry.yarnpkg.com/@electron/get/-/get-1.7.2.tgz#286436a9fb56ff1a1fcdf0e80131fd65f4d1e0fd" -@@ -5421,6 +5438,13 @@ jsprim@^1.2.2: +@@ -5375,6 +5392,13 @@ jsprim@^1.2.2: json-schema "0.2.3" verror "1.10.0" @@ -3343,7 +3468,7 @@ index b2fbf543af3..f10dddd6594 100644 just-debounce@^1.0.0: version "1.0.0" resolved "https://registry.yarnpkg.com/just-debounce/-/just-debounce-1.0.0.tgz#87fccfaeffc0b68cd19d55f6722943f929ea35ea" -@@ -6008,26 +6032,11 @@ minimatch@0.3: +@@ -5955,26 +5979,11 @@ minimatch@0.3: dependencies: brace-expansion "^1.1.7" @@ -3371,7 +3496,7 @@ index b2fbf543af3..f10dddd6594 100644 minipass@^2.2.1, minipass@^2.3.3: version "2.3.3" resolved "https://registry.yarnpkg.com/minipass/-/minipass-2.3.3.tgz#a7dcc8b7b833f5d368759cce544dccb55f50f233" -@@ -6797,6 +6806,11 @@ p-try@^2.0.0: +@@ -6716,6 +6725,11 @@ p-try@^2.0.0: resolved "https://registry.yarnpkg.com/p-try/-/p-try-2.0.0.tgz#85080bb87c64688fa47996fe8f7dfbe8211760b1" integrity sha512-hMp0onDKIajHfIkdRk3P4CdCmErkYAxxDtP3Wx/4nZ3aGlau2VKh3mZpcuFkH27WQkL/3WBCPOktzA9ZOAnMQQ== diff --git a/ci/images/centos7/Dockerfile b/ci/images/centos7/Dockerfile index 92c212024..acc6bbf6b 100644 --- a/ci/images/centos7/Dockerfile +++ b/ci/images/centos7/Dockerfile @@ -1,6 +1,6 @@ FROM centos:7 -ARG NODE_VERSION=v12.18.3 +ARG NODE_VERSION=v12.18.4 RUN ARCH="$(uname -m | sed 's/86_64/64/; s/aarch64/arm64/')" && \ curl -fsSL "https://nodejs.org/dist/$NODE_VERSION/node-$NODE_VERSION-linux-$ARCH.tar.xz" | tar -C /usr/local -xJ && \ mv "/usr/local/node-$NODE_VERSION-linux-$ARCH" "/usr/local/node-$NODE_VERSION" @@ -15,11 +15,16 @@ RUN npm config set python python2 RUN yum install -y epel-release && yum install -y jq RUN yum install -y rsync -# Copied from ../debian8/Dockerfile -# Install Go dependencies +# Copied from ../debian10/Dockerfile +# Install Go. RUN ARCH="$(uname -m | sed 's/x86_64/amd64/; s/aarch64/arm64/')" && \ curl -fsSL "https://dl.google.com/go/go1.14.3.linux-$ARCH.tar.gz" | tar -C /usr/local -xz -ENV PATH=/usr/local/go/bin:/root/go/bin:$PATH +ENV GOPATH=/gopath +# Ensures running this image as another user works. +RUN mkdir -p $GOPATH && chmod -R 777 $GOPATH +ENV PATH=/usr/local/go/bin:$GOPATH/bin:$PATH + +# Install Go dependencies ENV GO111MODULE=on RUN go get mvdan.cc/sh/v3/cmd/shfmt RUN go get github.com/goreleaser/nfpm/cmd/nfpm diff --git a/ci/images/debian8/Dockerfile b/ci/images/debian10/Dockerfile similarity index 55% rename from ci/images/debian8/Dockerfile rename to ci/images/debian10/Dockerfile index 4c62a398b..108348b65 100644 --- a/ci/images/debian8/Dockerfile +++ b/ci/images/debian10/Dockerfile @@ -1,4 +1,4 @@ -FROM debian:8 +FROM debian:10 RUN apt-get update @@ -24,28 +24,23 @@ RUN apt-get install -y build-essential \ RUN apt-get install -y gettext-base # Misc build dependencies. -RUN apt-get install -y git rsync unzip - -# We need latest jq from debian buster for date support. -RUN ARCH="$(dpkg --print-architecture)" && \ - curl -fsSOL http://http.us.debian.org/debian/pool/main/libo/libonig/libonig5_6.9.1-1_$ARCH.deb && \ - dpkg -i libonig*.deb && \ - curl -fsSOL http://http.us.debian.org/debian/pool/main/j/jq/libjq1_1.5+dfsg-2+b1_$ARCH.deb && \ - dpkg -i libjq*.deb && \ - curl -fsSOL http://http.us.debian.org/debian/pool/main/j/jq/jq_1.5+dfsg-2+b1_$ARCH.deb && \ - dpkg -i jq*.deb && rm *.deb +RUN apt-get install -y git rsync unzip jq # Installs shellcheck. -# Unfortunately coredumps on debian:8 so disabled for now. -#RUN curl -fsSL https://github.com/koalaman/shellcheck/releases/download/v0.7.1/shellcheck-v0.7.1.linux.$(uname -m).tar.xz | \ -# tar -xJ && \ -# mv shellcheck*/shellcheck /usr/local/bin && \ -# rm -R shellcheck* +RUN curl -fsSL https://github.com/koalaman/shellcheck/releases/download/v0.7.1/shellcheck-v0.7.1.linux.$(uname -m).tar.xz | \ + tar -xJ && \ + mv shellcheck*/shellcheck /usr/local/bin && \ + rm -R shellcheck* -# Install Go dependencies +# Install Go. RUN ARCH="$(uname -m | sed 's/x86_64/amd64/; s/aarch64/arm64/')" && \ curl -fsSL "https://dl.google.com/go/go1.14.3.linux-$ARCH.tar.gz" | tar -C /usr/local -xz -ENV PATH=/usr/local/go/bin:/root/go/bin:$PATH +ENV GOPATH=/gopath +# Ensures running this image as another user works. +RUN mkdir -p $GOPATH && chmod -R 777 $GOPATH +ENV PATH=/usr/local/go/bin:$GOPATH/bin:$PATH + +# Install Go dependencies ENV GO111MODULE=on RUN go get mvdan.cc/sh/v3/cmd/shfmt RUN go get github.com/goreleaser/nfpm/cmd/nfpm diff --git a/ci/release-image/Dockerfile b/ci/release-image/Dockerfile index 4dcd2bfb4..5c31ecbe7 100644 --- a/ci/release-image/Dockerfile +++ b/ci/release-image/Dockerfile @@ -39,6 +39,9 @@ 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 WORKDIR /home/coder ENTRYPOINT ["/usr/bin/entrypoint.sh", "--bind-addr", "0.0.0.0:8080", "."] diff --git a/ci/release-image/entrypoint.sh b/ci/release-image/entrypoint.sh index 6e7525ce6..b4343e7ed 100755 --- a/ci/release-image/entrypoint.sh +++ b/ci/release-image/entrypoint.sh @@ -1,18 +1,21 @@ -#!/usr/bin/env sh +#!/bin/sh set -eu -if [ "${DOCKER_USER-}" ]; then +# This isn't set by default. +USER="$(whoami)" +export USER + +if [ "${DOCKER_USER-}" ] && [ "$DOCKER_USER" != "$USER" ]; then echo "$DOCKER_USER ALL=(ALL) NOPASSWD:ALL" | sudo tee -a /etc/sudoers.d/nopasswd > /dev/null - sudo usermod --login "$DOCKER_USER" \ - --move-home --home "/home/$DOCKER_USER" \ - coder + # Unfortunately we cannot change $HOME as we cannot move any bind mounts + # nor can we bind mount $HOME into a new home as that requires a privileged container. + sudo usermod --login "$DOCKER_USER" coder sudo groupmod -n "$DOCKER_USER" coder + USER="$DOCKER_USER" + sudo sed -i "/coder/d" /etc/sudoers.d/nopasswd sudo sed -i "s/coder/$DOCKER_USER/g" /etc/fixuid/config.yml - export HOME="/home/$DOCKER_USER" fi -# This isn't set by default. -export USER="$(whoami)" dumb-init fixuid -q /usr/bin/code-server "$@" diff --git a/ci/steps/release-packages.sh b/ci/steps/release-packages.sh index cc6cd2a06..ba8d61d5c 100755 --- a/ci/steps/release-packages.sh +++ b/ci/steps/release-packages.sh @@ -4,7 +4,7 @@ set -euo pipefail main() { cd "$(dirname "$0")/../.." - NODE_VERSION=v12.18.3 + NODE_VERSION=v12.18.4 NODE_OS="$(uname | tr '[:upper:]' '[:lower:]')" NODE_ARCH="$(uname -m | sed 's/86_64/64/; s/aarch64/arm64/')" curl -L "https://nodejs.org/dist/$NODE_VERSION/node-$NODE_VERSION-$NODE_OS-$NODE_ARCH.tar.gz" | tar -xz diff --git a/doc/CONTRIBUTING.md b/doc/CONTRIBUTING.md index 80348848d..62c20f915 100644 --- a/doc/CONTRIBUTING.md +++ b/doc/CONTRIBUTING.md @@ -32,7 +32,7 @@ Differences: - We require a minimum of node v12 but later versions should work. - We use [nfpm](https://github.com/goreleaser/nfpm) to build `.deb` and `.rpm` packages. - We use [jq](https://stedolan.github.io/jq/) to build code-server releases. -- The [CI container](../ci/images/debian8/Dockerfile) is a useful reference for all our dependencies. +- The [CI container](../ci/images/debian10/Dockerfile) is a useful reference for all our dependencies. ## Development Workflow @@ -76,7 +76,7 @@ node . Build release packages (make sure you run `./ci/steps/release.sh` first): ``` -./ci/dev/image/run.sh ./ci/steps/release-packages.sh +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 ``` @@ -99,6 +99,13 @@ yarn test:standalone-release yarn package ``` +For a faster release build you can also run: + +``` +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. diff --git a/doc/FAQ.md b/doc/FAQ.md index 129498e4f..370dd6660 100644 --- a/doc/FAQ.md +++ b/doc/FAQ.md @@ -19,6 +19,7 @@ - [How does code-server decide what workspace or folder to open?](#how-does-code-server-decide-what-workspace-or-folder-to-open) - [How do I debug issues with code-server?](#how-do-i-debug-issues-with-code-server) - [Heartbeat File](#heartbeat-file) +- [Healthz endpoint](#healthz-endpoint) - [How does the config file work?](#how-does-the-config-file-work) - [Blank screen on iPad?](#blank-screen-on-ipad) - [Isn't an install script piped into sh insecure?](#isnt-an-install-script-piped-into-sh-insecure) @@ -242,6 +243,20 @@ older than X minutes, kill `code-server`. [#1636](https://github.com/cdr/code-server/issues/1636) will make the experience here better. +## Healthz endpoint + +`code-server` exposes an endpoint at `/healthz` which can be used to check +whether `code-server` is up without triggering a heartbeat. The response will +include a status (`alive` or `expired`) and a timestamp for the last heartbeat +(defaults to `0`). This endpoint does not require authentication. + +```json +{ + "status": "alive", + "lastHeartbeat": 1599166210566 +} +``` + ## How does the config file work? When `code-server` starts up, it creates a default config file in `~/.config/code-server/config.yaml` that looks diff --git a/doc/install.md b/doc/install.md index 5938cff07..b53a60675 100644 --- a/doc/install.md +++ b/doc/install.md @@ -79,8 +79,8 @@ commands presented in the rest of this document. ## Debian, Ubuntu ```bash -curl -fOL https://github.com/cdr/code-server/releases/download/v3.5.0/code-server_3.5.0_amd64.deb -sudo dpkg -i code-server_3.5.0_amd64.deb +curl -fOL https://github.com/cdr/code-server/releases/download/v3.6.0/code-server_3.6.0_amd64.deb +sudo dpkg -i code-server_3.6.0_amd64.deb sudo systemctl enable --now code-server@$USER # Now visit http://127.0.0.1:8080. Your password is in ~/.config/code-server/config.yaml ``` @@ -88,8 +88,8 @@ sudo systemctl enable --now code-server@$USER ## Fedora, CentOS, RHEL, SUSE ```bash -curl -fOL https://github.com/cdr/code-server/releases/download/v3.5.0/code-server-3.5.0-amd64.rpm -sudo rpm -i code-server-3.5.0-amd64.rpm +curl -fOL https://github.com/cdr/code-server/releases/download/v3.6.0/code-server-3.6.0-amd64.rpm +sudo rpm -i code-server-3.6.0-amd64.rpm sudo systemctl enable --now code-server@$USER # Now visit http://127.0.0.1:8080. Your password is in ~/.config/code-server/config.yaml ``` @@ -158,10 +158,10 @@ Here is an example script for installing and using a standalone `code-server` re ```bash mkdir -p ~/.local/lib ~/.local/bin -curl -fL https://github.com/cdr/code-server/releases/download/v3.5.0/code-server-3.5.0-linux-amd64.tar.gz \ +curl -fL https://github.com/cdr/code-server/releases/download/v3.6.0/code-server-3.6.0-linux-amd64.tar.gz \ | tar -C ~/.local/lib -xz -mv ~/.local/lib/code-server-3.5.0-linux-amd64 ~/.local/lib/code-server-3.5.0 -ln -s ~/.local/lib/code-server-3.5.0/bin/code-server ~/.local/bin/code-server +mv ~/.local/lib/code-server-3.6.0-linux-amd64 ~/.local/lib/code-server-3.6.0 +ln -s ~/.local/lib/code-server-3.6.0/bin/code-server ~/.local/bin/code-server PATH="~/.local/bin:$PATH" code-server # Now visit http://127.0.0.1:8080. Your password is in ~/.config/code-server/config.yaml @@ -179,10 +179,11 @@ code-server # easily access/modify your code-server config in $HOME/.config/code-server/config.json # outside the container. mkdir -p ~/.config -docker run -it -p 127.0.0.1:8080:8080 \ +docker run -it --name code-server -p 127.0.0.1:8080:8080 \ -v "$HOME/.config:/home/coder/.config" \ -v "$PWD:/home/coder/project" \ -u "$(id -u):$(id -g)" \ + -e "DOCKER_USER=$USER" \ codercom/code-server:latest ``` diff --git a/install.sh b/install.sh index 0b768def3..08e3a51e8 100755 --- a/install.sh +++ b/install.sh @@ -17,21 +17,28 @@ usage() { Installs code-server for Linux, macOS and FreeBSD. It tries to use the system package manager if possible. After successful installation it explains how to start using code-server. + +Pass in user@host to install code-server on user@host over ssh. +The remote host must have internet access. ${not_curl_usage-} Usage: - $arg0 [--dry-run] [--version X.X.X] [--method detect] [--prefix ~/.local] + $arg0 [--dry-run] [--version X.X.X] [--method detect] \ + [--prefix ~/.local] [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 Sets the prefix used by standalone release archives. Defaults to ~/.local The release is unarchived into ~/.local/lib/code-server-X.X.X @@ -100,9 +107,18 @@ main() { METHOD \ STANDALONE_INSTALL_PREFIX \ VERSION \ - OPTIONAL + OPTIONAL \ + ALL_FLAGS \ + SSH_ARGS + ALL_FLAGS="" while [ "$#" -gt 0 ]; do + case "$1" in + -*) + ALL_FLAGS="${ALL_FLAGS} $1" + ;; + esac + case "$1" in --dry-run) DRY_RUN=1 @@ -132,16 +148,33 @@ main() { usage exit 0 ;; - *) + --) + shift + # We remove the -- added above. + ALL_FLAGS="${ALL_FLAGS% --}" + SSH_ARGS="$*" + break + ;; + -*) echoerr "Unknown flag $1" echoerr "Run with --help to see usage." exit 1 ;; + *) + SSH_ARGS="$*" + break + ;; esac shift done + if [ "${SSH_ARGS-}" ]; then + echoh "Installing remotely with ssh $SSH_ARGS" + curl -fsSL https://code-server.dev/install.sh | prefix "$SSH_ARGS" ssh "$SSH_ARGS" sh -s -- "$ALL_FLAGS" + return + fi + VERSION="${VERSION-$(echo_latest_version)}" METHOD="${METHOD-detect}" if [ "$METHOD" != detect ] && [ "$METHOD" != standalone ]; then @@ -446,7 +479,7 @@ arch() { } command_exists() { - command -v "$@" > /dev/null 2>&1 + command -v "$@" > /dev/null } sh_c() { @@ -500,4 +533,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 "$@" diff --git a/lib/vscode b/lib/vscode index a0479759d..2af051012 160000 --- a/lib/vscode +++ b/lib/vscode @@ -1 +1 @@ -Subproject commit a0479759d6e9ea56afa657e454193f72aef85bd0 +Subproject commit 2af051012b66169dde0c4dfae3f5ef48f787ff69 diff --git a/package.json b/package.json index 4d75331e9..cc3edd30a 100644 --- a/package.json +++ b/package.json @@ -1,7 +1,7 @@ { "name": "code-server", "license": "MIT", - "version": "3.5.0", + "version": "3.6.0", "description": "Run VS Code on a remote server.", "homepage": "https://github.com/cdr/code-server", "bugs": { @@ -39,6 +39,7 @@ "@types/pem": "^1.9.5", "@types/safe-compare": "^1.1.0", "@types/semver": "^7.1.0", + "@types/split2": "^2.1.6", "@types/tar-fs": "^2.0.0", "@types/tar-stream": "^2.1.0", "@types/ws": "^7.2.6", @@ -76,6 +77,7 @@ "safe-buffer": "^5.1.1", "safe-compare": "^1.1.4", "semver": "^7.1.3", + "split2": "^3.2.2", "tar": "^6.0.1", "tar-fs": "^2.0.0", "ws": "^7.2.0", diff --git a/src/browser/pages/login.html b/src/browser/pages/login.html index f2b262991..fc772f392 100644 --- a/src/browser/pages/login.html +++ b/src/browser/pages/login.html @@ -47,5 +47,5 @@ - + diff --git a/src/browser/pages/vscode.ts b/src/browser/pages/vscode.ts index feb38a289..3ca1db83e 100644 --- a/src/browser/pages/vscode.ts +++ b/src/browser/pages/vscode.ts @@ -17,7 +17,7 @@ try { } // FIXME: Only works if path separators are /. const path = nlsConfig._resolvedLanguagePackCoreLocation + "/" + bundle.replace(/\//g, "!") + ".nls.json" - fetch(`{{BASE}}/resource/?path=${encodeURIComponent(path)}`) + fetch(`${options.base}/vscode/resource/?path=${encodeURIComponent(path)}`) .then((response) => response.json()) .then((json) => { bundles[bundle] = json diff --git a/src/browser/register.ts b/src/browser/register.ts index 60f054bc3..4f8345808 100644 --- a/src/browser/register.ts +++ b/src/browser/register.ts @@ -10,7 +10,7 @@ if ("serviceWorker" in navigator) { const path = normalize(`${options.csStaticBase}/dist/serviceWorker.js`) navigator.serviceWorker .register(path, { - scope: options.base || "/", + scope: (options.base ?? "") + "/", }) .then(() => { console.log("[Service Worker] registered") diff --git a/src/browser/robots.txt b/src/browser/robots.txt new file mode 100644 index 000000000..1f53798bb --- /dev/null +++ b/src/browser/robots.txt @@ -0,0 +1,2 @@ +User-agent: * +Disallow: / diff --git a/src/node/app/health.ts b/src/node/app/health.ts index 6a3aae94c..48d6897cf 100644 --- a/src/node/app/health.ts +++ b/src/node/app/health.ts @@ -1,6 +1,4 @@ -import * as http from "http" -import { HttpCode, HttpError } from "../../common/http" -import { HttpProvider, HttpResponse, Route, Heart, HttpProviderOptions } from "../http" +import { HttpProvider, HttpResponse, Heart, HttpProviderOptions } from "../http" /** * Check the heartbeat. @@ -10,15 +8,8 @@ export class HealthHttpProvider extends HttpProvider { super(options) } - public async handleRequest(route: Route, request: http.IncomingMessage): Promise { - if (!this.authenticated(request)) { - if (this.isRoot(route)) { - return { redirect: "/login", query: { to: route.fullPath } } - } - throw new HttpError("Unauthorized", HttpCode.Unauthorized) - } - - const result = { + public async handleRequest(): Promise { + return { cache: false, mime: "application/json", content: { @@ -26,7 +17,5 @@ export class HealthHttpProvider extends HttpProvider { lastHeartbeat: this.heart.lastHeartbeat, }, } - - return result } } diff --git a/src/node/cli.ts b/src/node/cli.ts index e5d069551..1403d8920 100644 --- a/src/node/cli.ts +++ b/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 { public constructor(public readonly value?: T) {} @@ -47,6 +47,8 @@ export interface Args extends VsArgs { readonly _: string[] readonly "reuse-window"?: boolean readonly "new-window"?: boolean + + readonly link?: OptionalString } interface Option { @@ -63,6 +65,11 @@ interface Option { * 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 extends boolean @@ -130,7 +137,8 @@ const options: Options> = { "install-extension": { type: "string[]", description: - "Install or update a VS Code extension by id or vsix. The identifier of an extension is `${publisher}.${name}`. To install a specific version provide `@${version}`. For example: 'vscode.csharp@1.2.3'.", + "Install or update a VS Code extension by id or vsix. The identifier of an extension is `${publisher}.${name}`.\n" + + "To install a specific version provide `@${version}`. For example: 'vscode.csharp@1.2.3'.", }, "enable-proposed-api": { type: "string[]", @@ -144,17 +152,29 @@ const options: Options> = { "new-window": { type: "boolean", short: "n", - description: "Force to open a new window. (use with open-in)", + description: "Force to open a new window.", }, "reuse-window": { type: "boolean", short: "r", - description: "Force to open a file or folder in an already opened window. (use with open-in)", + description: "Force to open a file or folder in an already opened window.", }, locale: { type: "string" }, log: { type: LogLevel }, verbose: { type: "boolean", short: "vvv", description: "Enable verbose logging." }, + + link: { + type: OptionalString, + description: ` + Securely bind code-server via Coder Cloud with the passed name. You'll get a URL like + https://myname.coder-cloud.com at which you can easily access your code-server instance. + Authorization is done via GitHub. + This is presently beta and requires being accepted for testing. + See https://github.com/cdr/code-server/discussions/2137 + `, + beta: true, + }, } export const optionDescriptions = (): string[] => { @@ -166,12 +186,32 @@ export const optionDescriptions = (): string[] => { }), { short: 0, long: 0 }, ) - return entries.map( - ([k, v]) => - `${" ".repeat(widths.short - (v.short ? v.short.length : 0))}${v.short ? `-${v.short}` : " "} --${k}${" ".repeat( - widths.long - k.length, - )} ${v.description}${typeof v.type === "object" ? ` [${Object.values(v.type).join(", ")}]` : ""}`, - ) + return entries + .filter(([, v]) => { + // If CS_BETA is set, we show beta options but if not, then we do not want + // to show beta options. + return process.env.CS_BETA || !v.beta + }) + .map(([k, v]) => { + const help = `${" ".repeat(widths.short - (v.short ? v.short.length : 0))}${ + v.short ? `-${v.short}` : " " + } --${k} ` + return ( + help + + v.description + ?.trim() + .split(/\n/) + .map((line, i) => { + line = line.trim() + if (i === 0) { + return " ".repeat(widths.long - k.length) + line + } + return " ".repeat(widths.long + widths.short + 6) + line + }) + .join("\n") + + (typeof v.type === "object" ? ` [${Object.values(v.type).join(", ")}]` : "") + ) + }) } export const parse = ( @@ -287,6 +327,21 @@ export const parse = ( logger.debug("parsed command line", field("args", args)) + return args +} + +export async function setDefaults(args: Args): Promise { + args = { ...args } + + if (!args["user-data-dir"]) { + await copyOldMacOSDataDir() + args["user-data-dir"] = paths.data + } + + if (!args["extensions-dir"]) { + args["extensions-dir"] = path.join(args["user-data-dir"], "extensions") + } + // --verbose takes priority over --log and --log takes priority over the // environment variable. if (args.verbose) { @@ -329,21 +384,6 @@ export const parse = ( return args } -export async function setDefaults(args: Args): Promise { - args = { ...args } - - 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 { return `bind-addr: 127.0.0.1:8080 auth: password @@ -370,10 +410,6 @@ export async function readConfigFile(configPath?: string): Promise { logger.info(`Wrote default config file to ${humanPath(configPath)}`) } - if (!process.env.CODE_SERVER_PARENT_PID && !process.env.VSCODE_IPC_HOOK_CLI) { - logger.info(`Using config file ${humanPath(configPath)}`) - } - const configFile = await fs.readFile(configPath) const config = yaml.safeLoad(configFile.toString(), { filename: configPath, @@ -401,7 +437,10 @@ export async function readConfigFile(configPath?: string): Promise { 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 without a port will default to 80 as well and not + // the code-server default. + return [u.hostname, u.port ? parseInt(u.port, 10) : 80] } interface Addr { @@ -453,3 +492,52 @@ async function copyOldMacOSDataDir(): Promise { 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 => { + // 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 => { + 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 +} diff --git a/src/node/coder-cloud.ts b/src/node/coder-cloud.ts new file mode 100644 index 000000000..1241bc90b --- /dev/null +++ b/src/node/coder-cloud.ts @@ -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 { + 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 { + 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) +} diff --git a/src/node/entry.ts b/src/node/entry.ts index 692559663..96db046e2 100644 --- a/src/node/entry.ts +++ b/src/node/entry.ts @@ -11,18 +11,21 @@ import { ProxyHttpProvider } from "./app/proxy" import { StaticHttpProvider } from "./app/static" import { UpdateHttpProvider } from "./app/update" import { VscodeHttpProvider } from "./app/vscode" -import { Args, bindAddrFromAllSources, optionDescriptions, parse, readConfigFile, setDefaults } from "./cli" +import { + Args, + bindAddrFromAllSources, + optionDescriptions, + parse, + readConfigFile, + setDefaults, + shouldOpenInExistingInstance, + shouldRunVsCodeCli, +} from "./cli" +import { coderCloudBind } from "./coder-cloud" import { AuthType, HttpServer, HttpServerOptions } from "./http" import { loadPlugins } from "./plugin" import { generateCertificate, hash, humanPath, open } from "./util" -import { ipcMain, wrap } from "./wrapper" - -process.on("uncaughtException", (error) => { - logger.error(`Uncaught exception: ${error.message}`) - if (typeof error.stack !== "undefined") { - logger.error(error.stack) - } -}) +import { ipcMain, WrapperProcess } from "./wrapper" let pkg: { version?: string; commit?: string } = {} try { @@ -34,7 +37,100 @@ try { const version = pkg.version || "development" const commit = pkg.commit || "development" -const main = async (args: Args, cliArgs: Args, configArgs: Args): Promise => { +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 => { + const pipeArgs: OpenCommandPipeArgs & { fileURIs: string[] } = { + type: "open", + folderURIs: [], + fileURIs: [], + forceReuseWindow: args["reuse-window"], + forceNewWindow: args["new-window"], + } + + const isDir = async (path: string): Promise => { + 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 => { + 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, @@ -51,7 +147,7 @@ const main = async (args: Args, cliArgs: Args, configArgs: Args): Promise 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 = { @@ -85,13 +181,15 @@ const main = async (args: Args, cliArgs: Args, configArgs: Args): Promise await loadPlugins(httpServer, args) - ipcMain().onDispose(() => { + ipcMain.onDispose(() => { httpServer.dispose().then((errors) => { errors.forEach((error) => logger.error(error.message)) }) }) logger.info(`code-server ${version} ${commit}`) + logger.info(`Using config file ${humanPath(args.config)}`) + const serverAddress = await httpServer.listen() logger.info(`HTTP server listening on ${serverAddress}`) @@ -125,27 +223,38 @@ const main = async (args: Args, cliArgs: Args, configArgs: Args): Promise 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 { - 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("") @@ -155,7 +264,10 @@ async function entry(): Promise { optionDescriptions().forEach((description) => { console.log("", description) }) - } else if (args.version) { + return + } + + if (args.version) { if (args.json) { console.log({ codeServer: version, @@ -165,83 +277,23 @@ async function entry(): Promise { } else { console.log(version, commit) } - process.exit(0) - } else if (process.env.VSCODE_IPC_HOOK_CLI) { - const pipeArgs: OpenCommandPipeArgs = { - type: "open", - folderURIs: [], - forceReuseWindow: args["reuse-window"], - forceNewWindow: args["new-window"], - } - const isDir = async (path: string): Promise => { - try { - const st = await fs.stat(path) - return st.isDirectory() - } catch (error) { - return false - } - } - for (let i = 0; i < args._.length; i++) { - const fp = path.resolve(args._[i]) - if (await isDir(fp)) { - pipeArgs.folderURIs.push(fp) - } else { - if (!pipeArgs.fileURIs) { - pipeArgs.fileURIs = [] - } - pipeArgs.fileURIs.push(fp) - } - } - if (pipeArgs.forceNewWindow && pipeArgs.fileURIs && pipeArgs.fileURIs.length > 0) { - logger.error("new-window can only be used with folder paths") - process.exit(1) - } - if (pipeArgs.folderURIs.length === 0 && (!pipeArgs.fileURIs || pipeArgs.fileURIs.length === 0)) { - logger.error("Please specify at least one file or folder argument") - process.exit(1) - } - const vscode = http.request( - { - path: "/", - method: "POST", - socketPath: process.env["VSCODE_IPC_HOOK_CLI"], - }, - (res) => { - res.on("data", (message) => { - logger.debug("Got message from VS Code", field("message", message.toString())) - }) - }, - ) - vscode.on("error", (err) => { - logger.debug("Got error from VS Code", field("error", err)) - }) - vscode.write(JSON.stringify(pipeArgs)) - vscode.end() - } else if (args["list-extensions"] || args["install-extension"] || args["uninstall-extension"]) { - logger.debug("forking vs code cli...") - const vscode = cp.fork(path.resolve(__dirname, "../../lib/vscode/out/vs/server/fork"), [], { - env: { - ...process.env, - CODE_SERVER_PARENT_PID: process.pid.toString(), - }, - }) - vscode.once("message", (message: any) => { - logger.debug("Got message from VS Code", field("message", message)) - if (message.type !== "ready") { - logger.error("Unexpected response waiting for ready response") - process.exit(1) - } - const send: CliMessage = { type: "cli", args } - vscode.send(send) - }) - vscode.once("error", (error) => { - logger.error(error.message) - process.exit(1) - }) - vscode.on("exit", (code) => process.exit(code || 0)) - } else { - wrap(() => main(args, cliArgs, configArgs)) + return } + + if (shouldRunVsCodeCli(args)) { + return runVsCodeCli(args) + } + + const socketPath = await shouldOpenInExistingInstance(cliArgs) + if (socketPath) { + return openInExistingInstance(args, socketPath) + } + + const wrapper = new WrapperProcess(require("../../package.json").version) + return wrapper.start() } -entry() +entry().catch((error) => { + logger.error(error.message) + ipcMain.exit(error) +}) diff --git a/src/node/http.ts b/src/node/http.ts index 37bbcfbdd..c616c8837 100644 --- a/src/node/http.ts +++ b/src/node/http.ts @@ -289,7 +289,7 @@ export abstract class HttpProvider { /** * Helper to error if not authorized. */ - protected ensureAuthenticated(request: http.IncomingMessage): void { + public ensureAuthenticated(request: http.IncomingMessage): void { if (!this.authenticated(request)) { throw new HttpError("Unauthorized", HttpCode.Unauthorized) } @@ -578,14 +578,24 @@ export class HttpServer { */ public listen(): Promise { 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) } }) } @@ -647,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 { @@ -685,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 { // 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) } /** @@ -744,7 +759,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) } @@ -894,8 +909,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(":") @@ -909,6 +926,9 @@ export class HttpServer { return undefined } + // Must be authenticated to use the proxy. + route.provider.ensureAuthenticated(request) + return { proxy: { port, diff --git a/src/node/plugin.ts b/src/node/plugin.ts index 54f7f2b76..7469f317d 100644 --- a/src/node/plugin.ts +++ b/src/node/plugin.ts @@ -4,11 +4,15 @@ import * as path from "path" import * as util from "util" import { Args } from "./cli" import { HttpServer } from "./http" +import { paths } from "./util" /* eslint-disable @typescript-eslint/no-var-requires */ export type Activate = (httpServer: HttpServer, args: Args) => void +/** + * Plugins must implement this interface. + */ export interface Plugin { activate: Activate } @@ -23,38 +27,66 @@ require("module")._load = function (request: string, parent: object, isMain: boo return originalLoad.apply(this, [request.replace(/^code-server/, path.resolve(__dirname, "../..")), parent, isMain]) } +/** + * Load a plugin and run its activation function. + */ const loadPlugin = async (pluginPath: string, httpServer: HttpServer, args: Args): Promise => { try { const plugin: Plugin = require(pluginPath) plugin.activate(httpServer, args) - logger.debug("Loaded plugin", field("name", path.basename(pluginPath))) + + const packageJson = require(path.join(pluginPath, "package.json")) + logger.debug( + "Loaded plugin", + field("name", packageJson.name || path.basename(pluginPath)), + field("path", pluginPath), + field("version", packageJson.version || "n/a"), + ) } catch (error) { - if (error.code !== "MODULE_NOT_FOUND") { - logger.warn(error.message) - } else { - logger.error(error.message) - } + logger.error(error.message) } } -const _loadPlugins = async (httpServer: HttpServer, args: Args): Promise => { - const pluginPath = path.resolve(__dirname, "../../plugins") - const files = await util.promisify(fs.readdir)(pluginPath, { - withFileTypes: true, - }) - await Promise.all(files.map((file) => loadPlugin(path.join(pluginPath, file.name), httpServer, args))) -} - -export const loadPlugins = async (httpServer: HttpServer, args: Args): Promise => { +/** + * Load all plugins in the specified directory. + */ +const _loadPlugins = async (pluginDir: string, httpServer: HttpServer, args: Args): Promise => { try { - await _loadPlugins(httpServer, args) + const files = await util.promisify(fs.readdir)(pluginDir, { + withFileTypes: true, + }) + await Promise.all(files.map((file) => loadPlugin(path.join(pluginDir, file.name), httpServer, args))) } catch (error) { if (error.code !== "ENOENT") { logger.warn(error.message) } } - - if (process.env.PLUGIN_DIR) { - await loadPlugin(process.env.PLUGIN_DIR, httpServer, args) - } +} + +/** + * Load all plugins from the `plugins` directory, directories specified by + * `CS_PLUGIN_PATH` (colon-separated), and individual plugins specified by + * `CS_PLUGIN` (also colon-separated). + */ +export const loadPlugins = async (httpServer: HttpServer, args: Args): Promise => { + 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)), + ]) } diff --git a/src/node/socket.ts b/src/node/socket.ts index e5fe66778..ada024831 100644 --- a/src/node/socket.ts +++ b/src/node/socket.ts @@ -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 { - const canConnect = (path: string): Promise => { - 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) { diff --git a/src/node/util.ts b/src/node/util.ts index c0f37f74b..75122fe76 100644 --- a/src/node/util.ts +++ b/src/node/util.ts @@ -2,6 +2,7 @@ import * as cp from "child_process" import * as crypto from "crypto" import envPaths from "env-paths" import * as fs from "fs-extra" +import * as net from "net" import * as os from "os" import * as path from "path" import * as util from "util" @@ -246,3 +247,17 @@ export function pathToFsPath(path: string, keepDriveLetterCasing = false): strin } return value } + +/** + * Return a promise that resolves with whether the socket path is active. + */ +export function canConnect(path: string): Promise { + return new Promise((resolve) => { + const socket = net.connect(path) + socket.once("error", () => resolve(false)) + socket.once("connect", () => { + socket.destroy() + resolve(true) + }) + }) +} diff --git a/src/node/wrapper.ts b/src/node/wrapper.ts index ba459efd1..cce841901 100644 --- a/src/node/wrapper.ts +++ b/src/node/wrapper.ts @@ -32,19 +32,13 @@ export class IpcMain { public readonly onMessage = this._onMessage.event private readonly _onDispose = new Emitter() public readonly onDispose = this._onDispose.event - public readonly processExit: (code?: number) => never + public readonly processExit: (code?: number) => never = process.exit - public constructor(public readonly parentPid?: number) { + public constructor(private readonly parentPid?: number) { process.on("SIGINT", () => this._onDispose.emit("SIGINT")) process.on("SIGTERM", () => this._onDispose.emit("SIGTERM")) process.on("exit", () => this._onDispose.emit(undefined)) - // Ensure we control when the process exits. - this.processExit = process.exit - process.exit = function (code?: number) { - logger.warn(`process.exit() was prevented: ${code || "unknown code"}.`) - } as (code?: number) => never - this.onDispose((signal) => { // Remove listeners to avoid possibly triggering disposal again. process.removeAllListeners() @@ -71,6 +65,19 @@ export class IpcMain { } } + /** + * Ensure we control when the process exits. + */ + public preventExit(): void { + process.exit = function (code?: number) { + logger.warn(`process.exit() was prevented: ${code || "unknown code"}.`) + } as (code?: number) => never + } + + public get isChild(): boolean { + return typeof this.parentPid !== "undefined" + } + public exit(error?: number | ProcessError): never { if (error && typeof error !== "number") { this.processExit(typeof error.code === "number" ? error.code : 1) @@ -127,17 +134,12 @@ export class IpcMain { } } -let _ipcMain: IpcMain -export const ipcMain = (): IpcMain => { - if (!_ipcMain) { - _ipcMain = new IpcMain( - typeof process.env.CODE_SERVER_PARENT_PID !== "undefined" - ? parseInt(process.env.CODE_SERVER_PARENT_PID) - : undefined, - ) - } - return _ipcMain -} +/** + * Channel for communication between the child and parent processes. + */ +export const ipcMain = new IpcMain( + typeof process.env.CODE_SERVER_PARENT_PID !== "undefined" ? parseInt(process.env.CODE_SERVER_PARENT_PID) : undefined, +) export interface WrapperOptions { maxMemory?: number @@ -162,14 +164,11 @@ export class WrapperProcess { this.logStdoutStream = rfs.createStream(path.join(paths.data, "coder-logs", "code-server-stdout.log"), opts) this.logStderrStream = rfs.createStream(path.join(paths.data, "coder-logs", "code-server-stderr.log"), opts) - ipcMain().onDispose(() => { - if (this.process) { - this.process.removeAllListeners() - this.process.kill() - } + ipcMain.onDispose(() => { + this.disposeChild() }) - ipcMain().onMessage((message) => { + ipcMain.onMessage((message) => { switch (message.type) { case "relaunch": logger.info(`Relaunching: ${this.currentVersion} -> ${message.version}`) @@ -181,55 +180,65 @@ export class WrapperProcess { break } }) - - process.on("SIGUSR1", async () => { - logger.info("Received SIGUSR1; hotswapping") - this.relaunch() - }) } - private async relaunch(): Promise { + private disposeChild(): void { this.started = undefined if (this.process) { this.process.removeAllListeners() this.process.kill() } + } + + private async relaunch(): Promise { + this.disposeChild() try { await this.start() } catch (error) { logger.error(error.message) - ipcMain().exit(typeof error.code === "number" ? error.code : 1) + ipcMain.exit(typeof error.code === "number" ? error.code : 1) } } public start(): Promise { - if (!this.started) { - this.started = this.spawn().then((child) => { - // Log both to stdout and to the log directory. - if (child.stdout) { - child.stdout.pipe(this.logStdoutStream) - child.stdout.pipe(process.stdout) - } - if (child.stderr) { - child.stderr.pipe(this.logStderrStream) - child.stderr.pipe(process.stderr) - } - logger.debug(`spawned inner process ${child.pid}`) - ipcMain() - .handshake(child) - .then(() => { - child.once("exit", (code) => { - logger.debug(`inner process ${child.pid} exited unexpectedly`) - ipcMain().exit(code || 0) - }) - }) - this.process = child + // If we have a process then we've already bound this. + if (!this.process) { + process.on("SIGUSR1", async () => { + logger.info("Received SIGUSR1; hotswapping") + this.relaunch() }) } + if (!this.started) { + this.started = this._start() + } return this.started } - private async spawn(): Promise { + private async _start(): Promise { + const child = this.spawn() + this.process = child + + // Log both to stdout and to the log directory. + if (child.stdout) { + child.stdout.pipe(this.logStdoutStream) + child.stdout.pipe(process.stdout) + } + if (child.stderr) { + child.stderr.pipe(this.logStderrStream) + child.stderr.pipe(process.stderr) + } + + logger.debug(`spawned inner process ${child.pid}`) + + await ipcMain.handshake(child) + + child.once("exit", (code) => { + logger.debug(`inner process ${child.pid} exited unexpectedly`) + ipcMain.exit(code || 0) + }) + } + + private spawn(): cp.ChildProcess { // Flags to pass along to the Node binary. let nodeOptions = `${process.env.NODE_OPTIONS || ""} ${(this.options && this.options.nodeOptions) || ""}` if (!/max_old_space_size=(\d+)/g.exec(nodeOptions)) { @@ -251,23 +260,13 @@ export class WrapperProcess { // It's possible that the pipe has closed (for example if you run code-server // --version | head -1). Assume that means we're done. if (!process.stdout.isTTY) { - process.stdout.on("error", () => ipcMain().exit()) + process.stdout.on("error", () => ipcMain.exit()) } -export const wrap = (fn: () => Promise): void => { - if (ipcMain().parentPid) { - ipcMain() - .handshake() - .then(() => fn()) - .catch((error: ProcessError): void => { - logger.error(error.message) - ipcMain().exit(error) - }) - } else { - const wrapper = new WrapperProcess(require("../../package.json").version) - wrapper.start().catch((error) => { - logger.error(error.message) - ipcMain().exit(error) - }) +// Don't let uncaught exceptions crash the process. +process.on("uncaughtException", (error) => { + logger.error(`Uncaught exception: ${error.message}`) + if (typeof error.stack !== "undefined") { + logger.error(error.stack) } -} +}) diff --git a/test/cli.test.ts b/test/cli.test.ts index f4f6c8849..ae5256142 100644 --- a/test/cli.test.ts +++ b/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 = { + -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 = { _: [] } + 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) + }) +}) diff --git a/test/update.test.ts b/test/update.test.ts index 15719bfac..7e4b80f21 100644 --- a/test/update.test.ts +++ b/test/update.test.ts @@ -8,6 +8,7 @@ import { SettingsProvider, UpdateSettings } from "../src/node/settings" import { tmpdir } from "../src/node/util" describe("update", () => { + return let version = "1.0.0" let spy: string[] = [] const server = http.createServer((request: http.IncomingMessage, response: http.ServerResponse) => { diff --git a/yarn.lock b/yarn.lock index 68221a85d..6f388626b 100644 --- a/yarn.lock +++ b/yarn.lock @@ -1107,6 +1107,13 @@ resolved "https://registry.yarnpkg.com/@types/semver/-/semver-7.3.3.tgz#3ad6ed949e7487e7bda6f886b4a2434a2c3d7b1a" integrity sha512-jQxClWFzv9IXdLdhSaTf16XI3NYe6zrEbckSpb5xhKfPbWgIyAY0AFyWWWfaiDcBuj3UHmMkCIwSRqpKMTZL2Q== +"@types/split2@^2.1.6": + version "2.1.6" + resolved "https://registry.yarnpkg.com/@types/split2/-/split2-2.1.6.tgz#b095c9e064853824b22c67993d99b066777402b1" + integrity sha512-ddaFSOMuy2Rp97l6q/LEteQygvTQJuEZ+SRhxFKR0uXGsdbFDqX/QF2xoGcOqLQ8XV91v01SnAv2vpgihNgW/Q== + dependencies: + "@types/node" "*" + "@types/tar-fs@^2.0.0": version "2.0.0" resolved "https://registry.yarnpkg.com/@types/tar-fs/-/tar-fs-2.0.0.tgz#db94cb4ea1cccecafe3d1a53812807efb4bbdbc1" @@ -5996,7 +6003,7 @@ readable-stream@^2.0.2, readable-stream@^2.2.2, readable-stream@^2.3.3, readable string_decoder "~1.1.1" util-deprecate "~1.0.1" -readable-stream@^3.1.1, readable-stream@^3.4.0, readable-stream@^3.6.0: +readable-stream@^3.0.0, readable-stream@^3.1.1, readable-stream@^3.4.0, readable-stream@^3.6.0: version "3.6.0" resolved "https://registry.yarnpkg.com/readable-stream/-/readable-stream-3.6.0.tgz#337bbda3adc0706bd3e024426a286d4b4b2c9198" integrity sha512-BViHy7LKeTz4oNnkcLJ+lVSL6vpiFeX6/d3oSH8zCW7UxP2onchk+vTGB143xuFjHS3deTgkKoXXymXqymiIdA== @@ -6621,6 +6628,13 @@ split-string@^3.0.1, split-string@^3.0.2: dependencies: extend-shallow "^3.0.0" +split2@^3.2.2: + version "3.2.2" + resolved "https://registry.yarnpkg.com/split2/-/split2-3.2.2.tgz#bf2cf2a37d838312c249c89206fd7a17dd12365f" + integrity sha512-9NThjpgZnifTkJpzTZ7Eue85S49QwpNhZTq6GRJwObb6jnLFNGB7Qm73V5HewTROPyxD0C29xqmaI68bQtV+hg== + dependencies: + readable-stream "^3.0.0" + sprintf-js@~1.0.2: version "1.0.3" resolved "https://registry.yarnpkg.com/sprintf-js/-/sprintf-js-1.0.3.tgz#04e6926f662895354f3dd015203633b857297e2c"