diff --git a/.eslintrc.json b/.eslintrc.json index a3287e03b6721..d34d16b3c8440 100644 --- a/.eslintrc.json +++ b/.eslintrc.json @@ -790,6 +790,18 @@ "**/vs/workbench/workbench.web.api" ] }, + { + "target": "**/vs/server/browser/**", + "restrictions": [ + "vs/nls", + "vs/css!./**/*", + "**/vs/base/**/{common,browser}/**", + "**/vs/base/parts/**/{common,browser}/**", + "**/vs/platform/**/{common,browser}/**", + "**/vs/code/**/{common,browser}/**", + "**/vs/workbench/workbench.web.api" + ] + }, { "target": "**/vs/code/node/**", "restrictions": [ @@ -825,7 +837,7 @@ ] }, { - "target": "**/vs/server/**", + "target": "**/vs/server/node/**", "restrictions": [ "vs/nls", "**/vs/base/**/{common,node}/**", @@ -982,16 +994,6 @@ "xterm*" ] } - ], - "header/header": [ - 2, - "block", - [ - "---------------------------------------------------------------------------------------------", - " * Copyright (c) Microsoft Corporation. All rights reserved.", - " * Licensed under the MIT License. See License.txt in the project root for license information.", - " *--------------------------------------------------------------------------------------------" - ] ] }, "overrides": [ diff --git a/.gitignore b/.gitignore index 95f843584c0e4..3672121444947 100644 --- a/.gitignore +++ b/.gitignore @@ -1,4 +1,4 @@ -.DS_Store +*.DS_Store .cache npm-debug.log Thumbs.db @@ -7,8 +7,6 @@ node_modules/ extensions/**/dist/ /out*/ /extensions/**/out/ -src/vs/server -resources/server build/node_modules coverage/ test_data/ diff --git a/.gitpod.Dockerfile b/.gitpod.Dockerfile new file mode 100644 index 0000000000000..c417f1e5b1962 --- /dev/null +++ b/.gitpod.Dockerfile @@ -0,0 +1,21 @@ +FROM gitpod/workspace-full:latest + +USER gitpod + +# We use latest major version of Node.js distributed VS Code. (see about dialog in your local VS Code) +RUN bash -c ". .nvm/nvm.sh \ + && nvm install 14 \ + && nvm use 14 \ + && nvm alias default 14" + +RUN echo "nvm use default &>/dev/null" >> ~/.bashrc.d/51-nvm-fix + +# Install dependencies +RUN sudo apt-get update \ + && sudo apt-get install -y --no-install-recommends \ + xvfb x11vnc fluxbox dbus-x11 x11-utils x11-xserver-utils xdg-utils \ + fbautostart xterm eterm gnome-terminal gnome-keyring seahorse nautilus \ + libx11-dev libxkbfile-dev libsecret-1-dev libnotify4 libnss3 libxss1 \ + libasound2 libgbm1 xfonts-base xfonts-terminus fonts-noto fonts-wqy-microhei \ + fonts-droid-fallback vim-tiny nano libgconf2-dev libgtk-3-dev twm \ + && sudo apt-get clean && sudo rm -rf /var/cache/apt/* && sudo rm -rf /var/lib/apt/lists/* && sudo rm -rf /tmp/* diff --git a/.gitpod.yml b/.gitpod.yml new file mode 100644 index 0000000000000..d466fcf9a720a --- /dev/null +++ b/.gitpod.yml @@ -0,0 +1,28 @@ +image: + file: .gitpod.Dockerfile +ports: + - port: 3000 + onOpen: open-browser +tasks: + - init: | + yarn + yarn server:init + command: | + gp sync-done init + export NODE_ENV=development + export VSCODE_DEV=1 + yarn gulp watch-init + name: watch app + - command: | + export NODE_ENV=development + export VSCODE_DEV=1 + gp sync-await init + node out/server.js + name: run app + openMode: split-right +github: + prebuilds: + pullRequestsFromForks: true +vscode: + extensions: + - dbaeumer.vscode-eslint diff --git a/.leewayignore b/.leewayignore new file mode 100644 index 0000000000000..83e4d532948d0 --- /dev/null +++ b/.leewayignore @@ -0,0 +1,4 @@ +/node_modules/ +/out/ +/.build/ +/.git/ \ No newline at end of file diff --git a/.vscode/launch.json b/.vscode/launch.json index 7524c58bdc4a8..88952b0d53548 100644 --- a/.vscode/launch.json +++ b/.vscode/launch.json @@ -1,6 +1,21 @@ { "version": "0.1.0", "configurations": [ + { + "type": "node", + "request": "launch", + "name": "Code Server", + "args": [ + "${workspaceFolder}/out/server.js" + ], + "outFiles": [ + "${workspaceFolder}/out/**/*.js" + ], + "env": { + "NODE_ENV": "development", + "VSCODE_DEV": "1" + } + }, { "type": "node", "request": "launch", diff --git a/README.md b/README.md index 1883fe4b76bf7..4c1169f2727f6 100644 --- a/README.md +++ b/README.md @@ -1,75 +1,72 @@ -# Visual Studio Code - Open Source ("Code - OSS") -[![Feature Requests](https://img.shields.io/github/issues/microsoft/vscode/feature-request.svg)](https://github.com/microsoft/vscode/issues?q=is%3Aopen+is%3Aissue+label%3Afeature-request+sort%3Areactions-%2B1-desc) -[![Bugs](https://img.shields.io/github/issues/microsoft/vscode/bug.svg)](https://github.com/microsoft/vscode/issues?utf8=✓&q=is%3Aissue+is%3Aopen+label%3Abug) -[![Gitter](https://img.shields.io/badge/chat-on%20gitter-yellow.svg)](https://gitter.im/Microsoft/vscode) +# OpenVSCode Server -## The Repository +[![Gitpod ready-to-code](https://img.shields.io/badge/Gitpod-ready--to--code-908a85?logo=gitpod)](https://gitpod.io/from-referrer) +[![GitHub](https://img.shields.io/github/license/gitpod-io/openvscode-server)](https://github.com/gitpod-io/openvscode-server/blob/main/LICENSE.txt) +[![Discord](https://img.shields.io/discord/816244985187008514)](https://www.gitpod.io/chat) -This repository ("`Code - OSS`") is where we (Microsoft) develop the [Visual Studio Code](https://code.visualstudio.com) product together with the community. Not only do we work on code and issues here, we also publish our [roadmap](https://github.com/microsoft/vscode/wiki/Roadmap), [monthly iteration plans](https://github.com/microsoft/vscode/wiki/Iteration-Plans), and our [endgame plans](https://github.com/microsoft/vscode/wiki/Running-the-Endgame). This source code is available to everyone under the standard [MIT license](https://github.com/microsoft/vscode/blob/main/LICENSE.txt). +## What is this? -## Visual Studio Code +This project provides a version of VS Code that runs a server on a remote machine and allows access through a modern web browser. It's based on the very same architecture used by [Gitpod](https://www.gitpod.io) or [GitHub Codespaces](https://github.com) at scale. -

- VS Code in action -

+Screenshot 2021-09-02 at 08 39 26 -[Visual Studio Code](https://code.visualstudio.com) is a distribution of the `Code - OSS` repository with Microsoft specific customizations released under a traditional [Microsoft product license](https://code.visualstudio.com/License/). +## Why? -[Visual Studio Code](https://code.visualstudio.com) combines the simplicity of a code editor with what developers need for their core edit-build-debug cycle. It provides comprehensive code editing, navigation, and understanding support along with lightweight debugging, a rich extensibility model, and lightweight integration with existing tools. +VS Code has traditionally been a desktop IDE built with web technologies. A few years back, people started patching it in order to run it in a remote context and to make it accessible through web browsers. These efforts have been complex and error prone, because many changes had to be made across the large code base of VS Code. -Visual Studio Code is updated monthly with new features and bug fixes. You can download it for Windows, macOS, and Linux on [Visual Studio Code's website](https://code.visualstudio.com/Download). To get the latest releases every day, install the [Insiders build](https://code.visualstudio.com/insiders). +Luckily, in 2019 the VS Code team started to refactor its architecture to support a browser-based working mode. While this architecture has been adopted by Gitpod and GitHub, the important bits have not been open-sourced, until now. As a result, many people in the community still use the old, hard to maintain and error-prone approach. -## Contributing +At Gitpod, we've been asked a lot about how we do it. So we thought we might as well share the minimal set of changes needed so people can rely on the latest version of VS Code, have a straightforward upgrade path and low maintenance effort. + +## Getting started -There are many ways in which you can participate in this project, for example: +### Docker -* [Submit bugs and feature requests](https://github.com/microsoft/vscode/issues), and help us verify as they are checked in -* Review [source code changes](https://github.com/microsoft/vscode/pulls) -* Review the [documentation](https://github.com/microsoft/vscode-docs) and make pull requests for anything from typos to additional and new content +- Start the server: + ```bash + docker run -it --init -p 3000:3000 -v "$(pwd):/home/workspace:cached" gitpod/openvscode-server + ``` +- Visit [localhost:3000](http://localhost:3000). -If you are interested in fixing issues and contributing directly to the code base, -please see the document [How to Contribute](https://github.com/microsoft/vscode/wiki/How-to-Contribute), which covers the following: +_Note_: Feel free to use the `nightly` tag to test the latest version, i.e. `gitpod/openvscode-server:nightly`. -* [How to build and run from source](https://github.com/microsoft/vscode/wiki/How-to-Contribute) -* [The development workflow, including debugging and running tests](https://github.com/microsoft/vscode/wiki/How-to-Contribute#debugging) -* [Coding guidelines](https://github.com/microsoft/vscode/wiki/Coding-Guidelines) -* [Submitting pull requests](https://github.com/microsoft/vscode/wiki/How-to-Contribute#pull-requests) -* [Finding an issue to work on](https://github.com/microsoft/vscode/wiki/How-to-Contribute#where-to-contribute) -* [Contributing to translations](https://aka.ms/vscodeloc) +### Linux -## Feedback +- [Download the latest release](https://github.com/gitpod-io/openvscode-server/releases/latest) +- Untar and run the server: + ```bash + tar -xzf openvscode-server-v${OPENVSCODE_SERVER_VERSION}.tar.gz + cd openvscode-server-v${OPENVSCODE_SERVER_VERSION} + ./server.sh + ``` +- Visit [localhost:3000](http://localhost:3000). -* Ask a question on [Stack Overflow](https://stackoverflow.com/questions/tagged/vscode) -* [Request a new feature](CONTRIBUTING.md) -* Upvote [popular feature requests](https://github.com/microsoft/vscode/issues?q=is%3Aopen+is%3Aissue+label%3Afeature-request+sort%3Areactions-%2B1-desc) -* [File an issue](https://github.com/microsoft/vscode/issues) -* Follow [@code](https://twitter.com/code) and let us know what you think! +_Note_: You can use [pre-releases](https://github.com/gitpod-io/openvscode-server/releases) to test nightly changes. -See our [wiki](https://github.com/microsoft/vscode/wiki/Feedback-Channels) for a description of each of these channels and information on some other available community-driven channels. +### Deployment guides -## Related Projects +Please refer to [Guides](./docs/guides/) to learn how to deploy OpenVSCode Server to your cloud provider of choice. -Many of the core components and extensions to VS Code live in their own repositories on GitHub. For example, the [node debug adapter](https://github.com/microsoft/vscode-node-debug) and the [mono debug adapter](https://github.com/microsoft/vscode-mono-debug) have their own repositories. For a complete list, please visit the [Related Projects](https://github.com/microsoft/vscode/wiki/Related-Projects) page on our [wiki](https://github.com/microsoft/vscode/wiki). +## The scope of this project -## Bundled Extensions +This project only adds minimal bits required to run VS Code in a server scenario. We have no intention of changing VS Code in any way or to add additional features to VS Code itself. Please report feature requests, bug fixes, etc. in the upstream repository. -VS Code includes a set of built-in extensions located in the [extensions](extensions) folder, including grammars and snippets for many languages. Extensions that provide rich language support (code completion, Go to Definition) for a language have the suffix `language-features`. For example, the `json` extension provides coloring for `JSON` and the `json-language-features` provides rich language support for `JSON`. +> **For any feature requests, bug reports, or contributions that are not specific to running VS Code in a server context, please go to [Visual Studio Code - Open Source "OSS"](https://github.com/microsoft/vscode)** -## Development Container +## Supporters -This repository includes a Visual Studio Code Remote - Containers / GitHub Codespaces development container. +The project is supported by companies such as [GitLab](https://gitlab.com/), [VMware](https://www.vmware.com/), [Uber](https://www.uber.com/), [SAP](https://www.sap.com/), [Sourcegraph](https://sourcegraph.com/), [RStudio](https://www.rstudio.com/), [SUSE](https://rancher.com/), [Tabnine](https://www.tabnine.com/), [Render](https://render.com/) and [TypeFox](https://www.typefox.io/). -- For [Remote - Containers](https://aka.ms/vscode-remote/download/containers), use the **Remote-Containers: Clone Repository in Container Volume...** command which creates a Docker volume for better disk I/O on macOS and Windows. -- For Codespaces, install the [Github Codespaces](https://marketplace.visualstudio.com/items?itemName=GitHub.codespaces) extension in VS Code, and use the **Codespaces: Create New Codespace** command. +## Contributing -Docker / the Codespace should have at least **4 Cores and 6 GB of RAM (8 GB recommended)** to run full build. See the [development container README](.devcontainer/README.md) for more information. +Thanks for your interest in contributing to the project 🙏. You can start a development environment with the following button: -## Code of Conduct +[![Open in Gitpod](https://gitpod.io/button/open-in-gitpod.svg)](https://gitpod.io/from-referrer) -This project has adopted the [Microsoft Open Source Code of Conduct](https://opensource.microsoft.com/codeofconduct/). For more information see the [Code of Conduct FAQ](https://opensource.microsoft.com/codeofconduct/faq/) or contact [opencode@microsoft.com](mailto:opencode@microsoft.com) with any additional questions or comments. +To learn about the code structure and other topics related to contributing, please refer to the [development docs](./docs/development.md). -## License +## Community & Feedback -Copyright (c) Microsoft Corporation. All rights reserved. +To learn what others are up to and to provide feedback, please head over to the [Discussions](https://github.com/gitpod-io/openvscode-server/discussions). -Licensed under the [MIT](LICENSE.txt) license. +You can also follow us on Twitter [@gitpod](https://twitter.com/gitpod) or come [chat with us](https://www.gitpod.io/chat). diff --git a/SECURITY.md b/SECURITY.md deleted file mode 100644 index a050f362c1528..0000000000000 --- a/SECURITY.md +++ /dev/null @@ -1,41 +0,0 @@ - - -## Security - -Microsoft takes the security of our software products and services seriously, which includes all source code repositories managed through our GitHub organizations, which include [Microsoft](https://github.com/Microsoft), [Azure](https://github.com/Azure), [DotNet](https://github.com/dotnet), [AspNet](https://github.com/aspnet), [Xamarin](https://github.com/xamarin), and [our GitHub organizations](https://opensource.microsoft.com/). - -If you believe you have found a security vulnerability in any Microsoft-owned repository that meets [Microsoft's definition of a security vulnerability](https://docs.microsoft.com/en-us/previous-versions/tn-archive/cc751383(v=technet.10)), please report it to us as described below. - -## Reporting Security Issues - -**Please do not report security vulnerabilities through public GitHub issues.** - -Instead, please report them to the Microsoft Security Response Center (MSRC) at [https://msrc.microsoft.com/create-report](https://msrc.microsoft.com/create-report). - -If you prefer to submit without logging in, send email to [secure@microsoft.com](mailto:secure@microsoft.com). If possible, encrypt your message with our PGP key; please download it from the [Microsoft Security Response Center PGP Key page](https://www.microsoft.com/en-us/msrc/pgp-key-msrc). - -You should receive a response within 24 hours. If for some reason you do not, please follow up via email to ensure we received your original message. Additional information can be found at [microsoft.com/msrc](https://www.microsoft.com/msrc). - -Please include the requested information listed below (as much as you can provide) to help us better understand the nature and scope of the possible issue: - - * Type of issue (e.g. buffer overflow, SQL injection, cross-site scripting, etc.) - * Full paths of source file(s) related to the manifestation of the issue - * The location of the affected source code (tag/branch/commit or direct URL) - * Any special configuration required to reproduce the issue - * Step-by-step instructions to reproduce the issue - * Proof-of-concept or exploit code (if possible) - * Impact of the issue, including how an attacker might exploit the issue - -This information will help us triage your report more quickly. - -If you are reporting for a bug bounty, more complete reports can contribute to a higher bounty award. Please visit our [Microsoft Bug Bounty Program](https://microsoft.com/msrc/bounty) page for more details about our active programs. - -## Preferred Languages - -We prefer all communications to be in English. - -## Policy - -Microsoft follows the principle of [Coordinated Vulnerability Disclosure](https://www.microsoft.com/en-us/msrc/cvd). - - diff --git a/build/azure-pipelines/common/sign.js b/build/azure-pipelines/common/sign.js index 94ddf5fe2463d..96382c1c329c2 100644 --- a/build/azure-pipelines/common/sign.js +++ b/build/azure-pipelines/common/sign.js @@ -69,9 +69,17 @@ function main([esrpCliPath, type, cert, username, password, folderPath, pattern] '-r', 'true', '-e', keyFile, ]; - cp.spawnSync('dotnet', args, { stdio: 'inherit' }); + try { + cp.execFileSync('dotnet', args, { stdio: 'inherit' }); + } + catch (err) { + console.error('ESRP failed'); + console.error(err); + process.exit(1); + } } exports.main = main; if (require.main === module) { main(process.argv.slice(2)); + process.exit(0); } diff --git a/build/gulpfile.extensions.js b/build/gulpfile.extensions.js index b2379127dd215..e31f8ea0e24f4 100644 --- a/build/gulpfile.extensions.js +++ b/build/gulpfile.extensions.js @@ -222,7 +222,9 @@ const compileExtensionsBuildTask = task.define('compile-extensions-build', task. )); gulp.task(compileExtensionsBuildTask); -gulp.task(task.define('extensions-ci', task.series(compileExtensionsBuildTask, compileExtensionMediaBuildTask))); +const compileExtensionsCi = task.series(compileExtensionsBuildTask, compileExtensionMediaBuildTask); +gulp.task(task.define('extensions-ci', compileExtensionsCi)); +exports.compileExtensionsCi = compileExtensionsCi; exports.compileExtensionsBuildTask = compileExtensionsBuildTask; diff --git a/build/gulpfile.server.js b/build/gulpfile.server.js new file mode 100644 index 0000000000000..9a0c76a9e307c --- /dev/null +++ b/build/gulpfile.server.js @@ -0,0 +1,351 @@ +/*-------------------------------------------------------- +* Copyright (C) Gitpod. All rights reserved. +*--------------------------------------------------------*/ + +// @ts-check +'use strict'; + +const cp = require('child_process'); +const gulp = require('gulp'); +const path = require('path'); +const es = require('event-stream'); +const util = require('./lib/util'); +const task = require('./lib/task'); +const common = require('./lib/optimize'); +const product = require('../product.json'); +const rename = require('gulp-rename'); +const replace = require('gulp-replace'); +const filter = require('gulp-filter'); +const _ = require('underscore'); +const { getProductionDependencies } = require('./lib/dependencies'); +const vfs = require('vinyl-fs'); +const packageJson = require('../package.json'); + +const { compileBuildTask } = require('./gulpfile.compile'); +gulp.task(task.define('compile-server', compileBuildTask)); +const { compileExtensionsCi } = require('./gulpfile.extensions'); + +gulp.task(task.define('watch-init', require('./lib/compilation').watchTask('out', false))); + +const root = path.dirname(__dirname); +const commit = util.getVersion(root); +const date = new Date().toISOString(); + +/** + * @param {{ + * qualifier: string + * header?: string + * }} options + */ +function defineTasks(options) { + const qualifier = options.qualifier; + const webResources = [ + // Workbench + 'out-build/vs/{base,platform,editor,workbench}/**/*.{svg,png,jpg}', + `out-build/vs/${qualifier}/browser/workbench/*.html`, + 'out-build/vs/base/browser/ui/codicons/codicon/**', + 'out-build/vs/**/markdown.css', + + // Webview + 'out-build/vs/workbench/contrib/webview/browser/pre/**', + + // Extension Worker + 'out-build/vs/workbench/services/extensions/worker/*.html', + + // Excludes + '!out-build/vs/**/{node,electron-browser,electron-sandbox,electron-main}/**', + '!out-build/vs/editor/standalone/**', + '!out-build/vs/workbench/**/*-tb.png', + '!**/test/**' + ]; + + const serverResources = [ + // Server + `out-build/${qualifier}-cli.js`, + `out-build/${qualifier}.js`, + 'out-build/bootstrap.js', + 'out-build/bootstrap-fork.js', + 'out-build/bootstrap-node.js', + 'out-build/bootstrap-amd.js', + 'out-build/paths.js', + 'out-build/serverUriTransformer.js', + 'out-build/vs/base/common/performance.js', + + // Excludes + '!out-build/vs/**/{node,browser,electron-browser,electron-sandbox,electron-main}/**', + '!out-build/vs/editor/standalone/**', + '!**/test/**' + ]; + + const buildfile = require('../src/buildfile'); + + const webEntryPoints = _.flatten([ + buildfile.entrypoint('vs/workbench/workbench.web.api'), + buildfile.base, + buildfile.workerExtensionHost, + buildfile.workerNotebook, + buildfile.workerLanguageDetection, + buildfile.keyboardMaps, + buildfile.workbenchWeb + ]).map(p => { + if (p.name === 'vs/code/browser/workbench/workbench') { + return { + ...p, + name: `vs/${qualifier}/browser/workbench/workbench` + }; + } + return p; + }); + + const serverEntryPoints = _.flatten([ + buildfile.entrypoint(`vs/${qualifier}/node/cli`), + buildfile.entrypoint(`vs/${qualifier}/node/server`), + buildfile.entrypoint('vs/workbench/services/extensions/node/extensionHostProcess'), + buildfile.entrypoint('vs/platform/files/node/watcher/unix/watcherApp'), + buildfile.entrypoint('vs/platform/files/node/watcher/nsfw/watcherApp'), + buildfile.entrypoint('vs/platform/terminal/node/ptyHostMain') + ]); + + const outWeb = `out-${qualifier}-web`; + + const optimizeWebTask = task.define(`optimize-${qualifier}-web`, task.series( + util.rimraf(outWeb), + common.optimizeTask({ + src: 'out-build', + entryPoints: _.flatten(webEntryPoints), + resources: webResources, + loaderConfig: common.loaderConfig(), + out: outWeb, + bundleInfo: undefined, + header: options?.header + }) + )); + gulp.task(optimizeWebTask); + + const outServer = `out-${qualifier}-server`; + + const optimizeServerTask = task.define(`optimize-${qualifier}-server`, task.series( + util.rimraf(outServer), + common.optimizeTask({ + src: 'out-build', + entryPoints: _.flatten(serverEntryPoints), + resources: serverResources, + loaderConfig: common.loaderConfig(), + out: outServer, + bundleInfo: undefined, + header: options?.header + }) + )); + gulp.task(optimizeServerTask); + + const optimizeWebServerTask = task.define(`optimize-${qualifier}`, task.parallel(optimizeWebTask, optimizeServerTask)); + gulp.task(optimizeWebServerTask); + + const outWebMin = outWeb + '-min'; + + const minifyWebTask = task.define(`minify-${qualifier}-web`, task.series( + optimizeWebTask, + util.rimraf(outWebMin), + common.minifyTask(outWeb) + )); + gulp.task(minifyWebTask); + + const outServerMin = outServer + '-min'; + + const minifyServerTask = task.define(`minify-${qualifier}-server`, task.series( + optimizeServerTask, + util.rimraf(outServerMin), + common.minifyTask(outServer, '/out') + )); + gulp.task(minifyWebTask); + + const minifyWebServerTask = task.define(`minify-${qualifier}`, task.parallel(minifyWebTask, minifyServerTask)); + gulp.task(minifyWebServerTask); + + /** + * @param {string} sourceFolderName + * @param {string} destinationFolderName + */ + function packageWebTask(sourceFolderName, destinationFolderName) { + const destination = path.join(path.dirname(root), destinationFolderName); + + return () => { + const json = require('gulp-json-editor'); + + const src = gulp.src(sourceFolderName + '/**', { base: '.' }) + .pipe(rename(function (path) { path.dirname = path.dirname.replace(new RegExp('^' + sourceFolderName), 'out'); })); + + const sources = es.merge(src); + + let version = packageJson.version; + const quality = product.quality; + + if (quality && quality !== 'stable') { + version += '-' + quality; + } + + const productJsonStream = gulp.src(['product.json'], { base: '.' }) + .pipe(json({ commit, date })); + + const base = 'remote/web'; + + const dependenciesSrc = _.flatten(getProductionDependencies(path.join(root, base)) + .map(d => path.relative(root, d.path)) + .map(d => [`${d}/**`, `!${d}/**/{test,tests}/**`, `!${d}/.bin/**`])); + + const runtimeDependencies = gulp.src(dependenciesSrc, { base, dot: true }) + .pipe(filter(['**', '!**/package-lock.json', '!**/yarn.lock'])) + .pipe(util.cleanNodeModules(path.join(__dirname, '.webignore'))); + + const name = product.applicationName; + const packageJsonStream = gulp.src([base + '/package.json'], { base }) + .pipe(json({ name, version })); + + const indexStream = gulp.src([`out-build/vs/${qualifier}/browser/workbench/workbench.html`], { base: `out-build/vs/${qualifier}/browser/workbench` }) + .pipe(rename('index.html')) + .pipe(replace('static/', '')); + + const manifest = gulp.src(`resources/${qualifier}/manifest.json`, { base: 'resources/' + qualifier }) + .pipe(json({ + name: product.nameLong, + 'short_name': product.nameShort + })); + + let all = es.merge( + packageJsonStream, + productJsonStream, + // license, + sources, + runtimeDependencies, + indexStream, + // favicon, + manifest, + // pwaicons + ); + + let result = all + .pipe(util.skipDirectories()) + .pipe(util.fixWin32DirectoryPermissions()); + + return result.pipe(vfs.dest(destination)); + }; + } + + /** + * @param {string} sourceFolderName + * @param {string} destinationFolderName + */ + function packageServerTask(sourceFolderName, destinationFolderName) { + const destination = path.join(path.dirname(root), destinationFolderName); + + return () => { + const json = require('gulp-json-editor'); + + const src = gulp.src(sourceFolderName + '/**', { base: '.' }) + .pipe(rename(function (path) { path.dirname = path.dirname.replace(new RegExp('^' + sourceFolderName), 'out'); })); + + const extensions = gulp.src('.build/extensions/**', { base: '.build', dot: true }); + + const sources = es.merge(src, extensions); + + let version = packageJson.version; + const quality = product.quality; + + if (quality && quality !== 'stable') { + version += '-' + quality; + } + + const productJsonStream = gulp.src(['product.json'], { base: '.' }) + .pipe(json({ commit, date })); + + const base = 'remote'; + const dependenciesSrc = _.flatten(getProductionDependencies(path.join(root, base)) + .map(d => path.relative(root, d.path)) + .map(d => [`${d}/**`, `!${d}/**/{test,tests}/**`, `!${d}/.bin/**`])); + + const runtimeDependencies = gulp.src(dependenciesSrc, { base, dot: true }) + .pipe(filter(['**', '!**/package-lock.json', '!**/yarn.lock'])) + .pipe(util.cleanNodeModules(path.join(__dirname, '.moduleignore'))); + + const name = product.applicationName; + const packageJsonStream = gulp.src([base + '/package.json'], { base }) + .pipe(json({ name, version })); + + cp.execSync('yarn gulp node'); + const nodePath = cp.execSync('node ' + path.join(root, 'build/lib/node'), { encoding: 'utf-8' }); + const nodeStream = gulp.src([nodePath], { base: path.dirname(nodePath) }); + + const resourcesBase = path.join(root, 'resources/' + qualifier); + const binStream = gulp.src([ + path.join(resourcesBase, 'bin/code.' + (process.platform === 'win32' ? 'cmd' : 'sh')), + path.join(resourcesBase, 'server.' + (process.platform === 'win32' ? 'cmd' : 'sh')) + ], { base: resourcesBase }) + .pipe(util.setExecutableBit(['**/*.sh'])) + .pipe(rename(path => { + if (path.basename === 'code' && path.extname === '.sh') { + path.extname = ''; + } + })); + + let all = es.merge( + packageJsonStream, + productJsonStream, + // license, + sources, + runtimeDependencies, + nodeStream, + binStream + ); + + let result = all + .pipe(util.skipDirectories()) + .pipe(util.fixWin32DirectoryPermissions()); + + return result.pipe(vfs.dest(destination)); + }; + } + + const dashed = (str) => (str ? `-${str}` : ``); + + ['', 'min'].forEach(minified => { + const destination = qualifier + '-pkg'; + const destinationWeb = qualifier + '-pkg-web'; + const destinationServer = qualifier + '-pkg-server'; + + const webRoot = path.join(path.dirname(root), destinationWeb); + const packageWeb = task.define(`package-${qualifier}-web${dashed(minified)}`, task.series( + util.rimraf(webRoot), + packageWebTask(outWeb + dashed(minified), destinationWeb) + )); + gulp.task(packageWeb); + + const serverRoot = path.join(path.dirname(root), destinationServer); + const packageServer = task.define(`package-${qualifier}-server${dashed(minified)}`, task.series( + util.rimraf(serverRoot), + packageServerTask(outServer + dashed(minified), destinationServer) + )); + gulp.task(packageServer); + + const packageRoot = path.join(path.dirname(root), destination); + const packageWebServer = task.define(`package-${qualifier}${dashed(minified)}`, task.series( + task.parallel(packageWeb, packageServer), + util.rimraf(packageRoot), + () => gulp.src(path.join(webRoot, '**'), { base: webRoot }).pipe(gulp.dest(packageRoot)), + () => gulp.src(path.join(serverRoot, '**'), { base: serverRoot }).pipe(gulp.dest(packageRoot)) + )); + gulp.task(packageWebServer); + + const webServerTask = task.define(`${qualifier}${dashed(minified)}`, task.series( + compileBuildTask, + compileExtensionsCi, + minified ? minifyWebServerTask : optimizeWebServerTask, + packageWebServer + )); + gulp.task(webServerTask); + }); +} + +defineTasks({ + qualifier: 'server' +}); +exports.defineTasks = defineTasks; diff --git a/build/hygiene.js b/build/hygiene.js index 9d6756dbc86d7..be53e68d7736a 100644 --- a/build/hygiene.js +++ b/build/hygiene.js @@ -28,10 +28,10 @@ function hygiene(some, linting = true) { const productJson = es.through(function (file) { const product = JSON.parse(file.contents.toString('utf8')); - if (product.extensionsGallery) { - console.error(`product.json: Contains 'extensionsGallery'`); + /* if (product.extensionsGallery) { + console.error('product.json: Contains "extensionsGallery"'); errorCount++; - } + } */ this.emit('data', file); }); @@ -59,13 +59,15 @@ function hygiene(some, linting = true) { }); const copyrights = es.through(function (file) { - const lines = file.__lines; - - for (let i = 0; i < copyrightHeaderLines.length; i++) { - if (lines[i] !== copyrightHeaderLines[i]) { - console.error(file.relative + ': Missing or bad copyright statement'); - errorCount++; - break; + if (file.relative.indexOf('vs/server') === -1 && file.relative.indexOf('gulpfile.server') === -1) { + const lines = file.__lines; + + for (let i = 0; i < copyrightHeaderLines.length; i++) { + if (lines[i] !== copyrightHeaderLines[i]) { + console.error(file.relative + ': Missing or bad copyright statement'); + errorCount++; + break; + } } } diff --git a/build/lib/compilation.js b/build/lib/compilation.js index c6e9196abceed..a56c43ff01b3c 100644 --- a/build/lib/compilation.js +++ b/build/lib/compilation.js @@ -16,6 +16,7 @@ const util = require("./util"); const fancyLog = require("fancy-log"); const ansiColors = require("ansi-colors"); const os = require("os"); +const replace = require('gulp-replace'); const watch = require('./watch'); const reporter = (0, reporter_1.createReporter)(); function getTypeScriptCompilerOptions(src) { @@ -35,6 +36,22 @@ function getTypeScriptCompilerOptions(src) { function createCompile(src, build, emitError) { const tsb = require('gulp-tsb'); const sourcemaps = require('gulp-sourcemaps'); + const rootDir = path.dirname(path.dirname(__dirname)); + const product = JSON.parse(fs.readFileSync(path.join(rootDir, 'product.json'), 'utf-8')); + // Running out of sources + if (!build) { + Object.assign(product, { + nameShort: `${product.nameShort} Dev`, + nameLong: `${product.nameLong} Dev`, + dataFolderName: `${product.dataFolderName}-dev` + }); + } + Object.assign(product, { + commit: util.getVersion(rootDir), + date: new Date().toISOString(), + version: JSON.parse(fs.readFileSync(path.join(rootDir, 'package.json'), 'utf-8')).version + }); + const productJson = JSON.stringify(product, undefined, 4); const projectPath = path.join(__dirname, '../../', src, 'tsconfig.json'); const overrideOptions = Object.assign(Object.assign({}, getTypeScriptCompilerOptions(src)), { inlineSources: Boolean(build) }); if (!build) { @@ -48,6 +65,7 @@ function createCompile(src, build, emitError) { const noDeclarationsFilter = util.filter(data => !(/\.d\.ts$/.test(data.path))); const input = es.through(); const output = input + .pipe(replace(/{\s*\/\*BUILD->INSERT_PRODUCT_CONFIGURATION\*\/\s*}/, productJson, { skipBinary: true })) .pipe(utf8Filter) .pipe(bom()) // this is required to preserve BOM in test files that loose it otherwise .pipe(utf8Filter.restore) @@ -59,7 +77,7 @@ function createCompile(src, build, emitError) { .pipe(noDeclarationsFilter.restore) .pipe(sourcemaps.write('.', { addComment: false, - includeContent: !!build, + includeContent: true, sourceRoot: overrideOptions.sourceRoot })) .pipe(tsFilter.restore) diff --git a/build/lib/compilation.ts b/build/lib/compilation.ts index 3db8cf0f9178f..ad803ac86f7d8 100644 --- a/build/lib/compilation.ts +++ b/build/lib/compilation.ts @@ -17,6 +17,7 @@ import * as fancyLog from 'fancy-log'; import * as ansiColors from 'ansi-colors'; import * as os from 'os'; import ts = require('typescript'); +const replace = require('gulp-replace'); const watch = require('./watch'); @@ -41,6 +42,22 @@ function createCompile(src: string, build: boolean, emitError?: boolean) { const tsb = require('gulp-tsb') as typeof import('gulp-tsb'); const sourcemaps = require('gulp-sourcemaps') as typeof import('gulp-sourcemaps'); + const rootDir = path.dirname(path.dirname(__dirname)); + const product = JSON.parse(fs.readFileSync(path.join(rootDir, 'product.json'), 'utf-8')); + // Running out of sources + if (!build) { + Object.assign(product, { + nameShort: `${product.nameShort} Dev`, + nameLong: `${product.nameLong} Dev`, + dataFolderName: `${product.dataFolderName}-dev` + }); + } + Object.assign(product, { + commit: util.getVersion(rootDir), + date: new Date().toISOString(), + version: JSON.parse(fs.readFileSync(path.join(rootDir, 'package.json'), 'utf-8')).version + }); + const productJson = JSON.stringify(product, undefined, 4); const projectPath = path.join(__dirname, '../../', src, 'tsconfig.json'); const overrideOptions = { ...getTypeScriptCompilerOptions(src), inlineSources: Boolean(build) }; @@ -59,6 +76,7 @@ function createCompile(src: string, build: boolean, emitError?: boolean) { const input = es.through(); const output = input + .pipe(replace(/{\s*\/\*BUILD->INSERT_PRODUCT_CONFIGURATION\*\/\s*}/, productJson, { skipBinary: true })) .pipe(utf8Filter) .pipe(bom()) // this is required to preserve BOM in test files that loose it otherwise .pipe(utf8Filter.restore) @@ -70,7 +88,7 @@ function createCompile(src: string, build: boolean, emitError?: boolean) { .pipe(noDeclarationsFilter.restore) .pipe(sourcemaps.write('.', { addComment: false, - includeContent: !!build, + includeContent: true, sourceRoot: overrideOptions.sourceRoot })) .pipe(tsFilter.restore) diff --git a/build/npm/postinstall.js b/build/npm/postinstall.js index 2433da29e8637..17dd7447f892f 100644 --- a/build/npm/postinstall.js +++ b/build/npm/postinstall.js @@ -90,5 +90,9 @@ runtime "${runtime}"`; yarnInstall(watchPath); } -cp.execSync('git config pull.rebase merges'); -cp.execSync('git config blame.ignoreRevsFile .git-blame-ignore'); +try { + cp.execSync('git config pull.rebase merges'); + cp.execSync('git config blame.ignoreRevsFile .git-blame-ignore'); +} catch (e) { + console.error(e) +} diff --git a/docs/development.md b/docs/development.md new file mode 100644 index 0000000000000..23ba4f61bef31 --- /dev/null +++ b/docs/development.md @@ -0,0 +1,54 @@ +# Developing OpenVSCode Server + +This guide implies that you have a good understanding of [source code organization](https://github.com/microsoft/vscode/wiki/Source-Code-Organization) and [development flow](https://github.com/microsoft/vscode/wiki/How-to-Contribute) of Code-OSS. + +## Source Code Organization + +We add [server](../src/vs/server) layer glueing everything required to run the server including the [web workbench](../src/vs/server/browser/workbench/workbench.ts), the [remote server](../src/vs/server/node/server.ts) and the [remote CLI](../src/vs/server/node/cli.ts). + +The server consist of 2 applications: +- The web workbench is an entry point to a browser application configuring various services +like how to establish the connection with the backend, resolve remote resources, load webviews, and so on. +- The server is running on a remote machine that serves the web workbench and static resources for webviews, extensions, and so on, as well as provides access to the file system, terminals, extensions, and so on. + +The workbench and the server are communicating via RPC calls over web socket connections. There are 2 kinds of connections that we support right now: +- the management connection provides access to the server RPC channels, like filesystem and terminals; +- the extension connection creates the remote extension host process per a browser window to run extensions. + +For each window, the server installs the CLI socket server and injects a special env var pointing to the socket file into each terminal. It allows the remote CLI to send commands to a proper window, for instance, to open a file. + +Note that the workbench can be also bundled independently to serve from some CDN services. The server can run in headless mode if sources of the web workbench are missing. +## Building + +### Starting from sources + +- [Start a Gitpod workspace](https://gitpod.io/#https://github.com/gitpod-io/openvscode-server) +- Dev version of the server should be already up and running. Notice that the dev version is slower to load since it is not bundled (around 2000 files). + +### Bundling + +Run `yarn gulp server-min` to create production-ready distributable from sources. After the build is finished, you will be able to find following folders next to the project directory: +- `server-pkg-web` contains the web workbench static resources, +- `server-pkg-server` contains the headless remote server with the remote CLI, +- `serfver-pkg` contains everything together to be distributed standalone. + +You can find gulp bundling tasks [here](../build/gulpfile.server.js). + +### Updating VS Code + +- Update your local VS Code, open the About dialog and remember the release commit and Node.js version. +- Fetch latest upstream changes and rebase the branch based on the local VS Code's commit. Drop all commits before `code web server initial commit`. +- Check that [.gitpod.Dockerfile](./.gitpod.Dockerfile) and [remote/.yarnrc](./remote/.yarnrc) has latest major Node.js version of local VS Code's Node.js version. +- Recompile everything: `git clean -dfx && yarn && yarn server:init` +- Run smoke tests: `yarn server:smoketest`. +- Start the dev server and play: + - filesystem (open some project) + - extension host process: check language smartness + - extension management (installing/uninstalling) + - install VIM extension to test web extensions + - terminals + - code cli should open files and manage extensions: `alias code='export VSCODE_DEV=1 && node out/server-cli.js'` +- Check server/browser logs for any warnings/errors about missing capabilities and fix them. +- Build the production server with all changes: `yarn gulp server-min`. +- Run it and play as with the dev server: `/workspace/server-pkg/server.sh` +- Open a PR with your changes and ask for help if needed. It should be agaist `gitpod-io/openvscode-server` repo and `main` branch! diff --git a/docs/guides/README.md b/docs/guides/README.md new file mode 100644 index 0000000000000..c561a2e7cf95f --- /dev/null +++ b/docs/guides/README.md @@ -0,0 +1,16 @@ +# OpenVSCode Server Guides + +In this directory, you find a non-exhaustive collection of deployment guides for OpenVSCode Server. + +Each directory contains a `README.md` file as the main guide. Additional, supporting files may be available in any given directory where necessary. + +## Add a new guide + +We welcome community contributions 🙏. Please open an issue and/or pull request if you would like to add a new deployment guide. + +To add a new guide: +1. Copy the `_template` directory and name it based on the deployment platform you write a guide for +1. Update the `/README.md` file by completing the existing sections +1. Open a pull request + +For guidance & inspiration, please refer to existing guides. diff --git a/docs/guides/_template/README.md b/docs/guides/_template/README.md new file mode 100644 index 0000000000000..396ae66b3d2e8 --- /dev/null +++ b/docs/guides/_template/README.md @@ -0,0 +1,11 @@ +# Deploy OpenVSCode Server to [NEW-DEPLOYMENT-PLATFORM] + +## Prerequisites + +## Setup + +## Start the server + +## Access OpenVSCode Server + +## Teardown diff --git a/docs/guides/aws-ec2/README.md b/docs/guides/aws-ec2/README.md new file mode 100644 index 0000000000000..5344c73fee2dd --- /dev/null +++ b/docs/guides/aws-ec2/README.md @@ -0,0 +1,65 @@ +# Deploy OpenVSCode Server to AWS EC2 + +## Prerequisites + +To complete this guide, you need: +* an [AWS](https://aws.amazon.com/) account + +## Setup + +### Create a VM + +1. Navigate to https://console.aws.amazon.com/ec2 +1. Launch a Ubuntu 20.04 instance with the default settings + * **Caution**: Please follow security best practices when setting up your VM + +### Download & extract OpenVSCode Server + +**Caution**: Make sure you successfully connected to the VM before you execute the following commands. + +First, let's define the release version we want to download. You can find the latest version on the [Releases](https://github.com/gitpod-io/openvscode-server/releases) page. + +```bash +export SERVER_VERSION=1.60.0 # Replace with the latest version +``` + +With that in place, let's download & extract OpenVSCode server: + +```bash +wget https://github.com/gitpod-io/openvscode-server/releases/download/openvscode-server-v$SERVER_VERSION/openvscode-server-v$SERVER_VERSION-linux-x64.tar.gz -O code-server.tar.gz +tar -xzf code-server.tar.gz +rm code-server.tar.gz +``` + +### Create an inbound rule for port 3000 + +To access OpenVSCode Server on port 3000 later, we have to create an inbound rule: +1. Open the instance summary page +1. Select the "Security" tab +1. In the "Security groups" section, click on the link to open the security group page +1. In the "Inbound rules" table, click the "Edit inbound rules" button on the right side +1. Click "Add rule" and populate the following fields (use default values for everything else): + * Type: Custom TCP + * Port range: 3000 + * Source: Anywhere-IPv4 +1. Click "Save rules" + +## Start the server + +While you are still connected to the VM, execute the following commands to start OpenVSCode Server: + +```bash +cd openvscode-server-v$SERVER_VERSION-linux-x64 +./server.sh +``` + +## Access OpenVSCode Server + +1. Navigate to your VM's instance summary page +1. Copy the "Public IPv4 address" +1. Paste the IP address in a new browser tab and add `:3000`, i.e. `http://18.118.194.234:3000` + +## Teardown + +1. Navigate to your VM's instance summary page +1. Click "Instance state" and select "Terminate instance" diff --git a/docs/guides/azure-vm/README.md b/docs/guides/azure-vm/README.md new file mode 100644 index 0000000000000..076307b0ab4a9 --- /dev/null +++ b/docs/guides/azure-vm/README.md @@ -0,0 +1,63 @@ +# Deploy OpenVSCode Server to Azure VM + +## Prerequisites + +To complete this guide, you need: +* an [Azure](https://azure.microsoft.com/en-us/) subscription + +## Setup + +### Create a VM + +1. Navigate to https://portal.azure.com/#create/Microsoft.VirtualMachine-ARM +1. Launch a Ubuntu 20.04 instance with the default settings + * **Caution**: Please follow security best practices when setting up your VM + +### Download & extract OpenVSCode Server + +**Caution**: Make sure you successfully connected to the VM before you execute the following commands. + +First, let's define the release version we want to download. You can find the latest version on the [Releases](https://github.com/gitpod-io/openvscode-server/releases) page. + +```bash +export SERVER_VERSION=1.60.0 # Replace with the latest version +``` + +With that in place, let's download & extract OpenVSCode server: + +```bash +wget https://github.com/gitpod-io/openvscode-server/releases/download/openvscode-server-v$SERVER_VERSION/openvscode-server-v$SERVER_VERSION-linux-x64.tar.gz -O code-server.tar.gz +tar -xzf code-server.tar.gz +rm code-server.tar.gz +``` +### Create an inbound rule for port 3000 + +To access OpenVSCode Server on port 3000 later, we have to create an inbound rule: +1. Open the instance dashboard +1. In the left menu pane, under Settings, select Networking. +1. In the bottom section, for the NSG rules for the network interface, select Add inbound port rule. +1. Populate the following fields (use default values for everything else): + * Destination port ranges: 3000 + * Protocol: TCP +1. Click "Add" + + +## Start the server + +While you are still connected to the VM, execute the following commands to start OpenVSCode Server: + +```bash +cd openvscode-server-v$SERVER_VERSION-linux-x64 +./server.sh +``` + +## Access OpenVSCode Server + +1. Navigate to the Overview pane for the VM +1. Copy the "Public IP address" +1. Paste the IP address in a new browser tab and add `:3000`, i.e. `http://52.251.44.195:3000` + +## Teardown + +1. Navigate to the Overview pane for the VM. You can find the VM under All Resources. +1. Click on "Stop" diff --git a/docs/guides/digital-ocean/README.md b/docs/guides/digital-ocean/README.md new file mode 100644 index 0000000000000..e2ce46a8e0b82 --- /dev/null +++ b/docs/guides/digital-ocean/README.md @@ -0,0 +1,87 @@ +# Deploy OpenVSCode Server to Digital Ocean + +## Prerequisites + +To complete this guide, you need: +* a [Digital Ocean](https://www.digitalocean.com/) account + +## Setup + +### Create the Droplet + +First, you need to create a Virtual Machine to host your server. If you don't have one already, you can start with [our template](https://cloud.digitalocean.com/droplets/new?use_case=droplet&i=59c3b0&fleetUuid=a8fdcc26-2bf0-449d-8113-e458327192fe&distro=ubuntu&distroImage=ubuntu-20-04-x64&size=s-1vcpu-1gb-amd®ion=fra1&options=ipv6), then change a couple of settings as explained below. + +- You either need to set a password or add an SSH key. For demonstration purposes, it's easier to use a password. **Caution**: This is for demo purposes, please follow security best practices for a production environment. +- We need to do is to check the checkbox User data and add the following script to the text field below: **TODO: What script, cc @filiptronicek** + +### Prepare the Droplet + +- First things first, you need to turn on the Droplet by selecting it in the dashboard and toggling the switch on the top right of the page. +- Then, you need to copy the Droplet's IP address, available on the same page in the top bar. If you are unsure whether to copy the **ipv4** or **ipv6** address, select **ipv4**. +- Now you can connect to your droplet via SSH, which you can do straight from your terminal by executing the following commands (you will need to replace `DROPLET_IP` with the actual address you copied in the previous step): + ``` + ssh root@DROPLET_IP + ``` +- When prompted, enter the password you chose during the configuration. + +### Download & extract OpenVSCode Server + +**Caution**: Make sure you successfully connected to the Droplet before you execute the following commands. + +First, let's define the release version we want to download. You can find the latest version on the [Releases](https://github.com/gitpod-io/openvscode-server/releases) page. + +```bash +export SERVER_VERSION=1.60.0 # Replace with the latest version +``` + +With that in place, let's download & extract OpenVSCode server: + +```bash +wget https://github.com/gitpod-io/openvscode-server/releases/download/openvscode-server-v$SERVER_VERSION/openvscode-server-v$SERVER_VERSION-linux-x64.tar.gz -O code-server.tar.gz +tar -xzf code-server.tar.gz +rm code-server.tar.gz +``` + +## Start the server + +While you are still connected to the Droplet, execute the following commands to start OpenVSCode Server: + +```bash +cd openvscode-server-v$SERVER_VERSION-linux-x64 +./server.sh +``` + +> Gotcha: If you close the SSH session, the server will stop as well. To avoid this, you can run the server script in the background with the command shown below. If you want to do things like kill the process or bring it back to the foreground, refer to [Run a Linux Command in the Background](https://linuxize.com/post/how-to-run-linux-commands-in-background/#run-a-linux-command-in-the-background) or use a multiplexer such as [tmux](https://en.wikipedia.org/wiki/Tmux) [[tmux - a very simple beginner's guide](https://www.ocf.berkeley.edu/~ckuehl/tmux/)]. +``` +./server.sh >/dev/null 2>&1 & +``` + +You're all set! + +## Access OpenVSCode Server + +You can now access your IDE at `http://:3000`. + +## Teardown + +Delete the Droplet through the Digital Ocean web interface. + +## Further steps + +### Running OpenVSCode Server on startup + +If you want to run the server on boot, you can add this to your Crontab file (`crontab -e`): + +``` +@reboot /root/openvscode-server-v-linux-x64/server.sh +``` + +Make sure you replace `REPLACE_WITH_LATEST_VERSION` with the version you used earlier. + +### Add a custom domain + +You can follow the official [DNS Quickstart](https://docs.digitalocean.com/products/networking/dns/quickstart/) guide for setting up a custom domain with your Droplet. + +### Secure the Droplet + +There is an awesome video by Mason Egger called [Securing Your Droplet](https://youtu.be/L8e_eAm4fFM), which explains some key steps for hardening the security of the Droplet. diff --git a/docs/guides/gcp-gce/README.md b/docs/guides/gcp-gce/README.md new file mode 100644 index 0000000000000..1b3ad3da7ef3c --- /dev/null +++ b/docs/guides/gcp-gce/README.md @@ -0,0 +1,13 @@ +# Deploy OpenVSCode Server to Google Cloud Platform - Compute Engine + +## Prerequisites + +To complete this guide, you need: +* a [Google Cloud Platform](https://cloud.google.com/) account +* a project where you can create a virtual machine + +## Start the interactive tutorial + +This guide is available as an interactive Cloud Shell tutorial. To get started, please click the following button: + +[![Open in Cloud Shell](https://gstatic.com/cloudssh/images/open-btn.png)](https://ssh.cloud.google.com/cloudshell/open?cloudshell_git_repo=https://github.com/gitpod-io/openvscode-server&cloudshell_tutorial=docs/guides/gcp-gce/cloud-shell-tutorial.md) diff --git a/docs/guides/gcp-gce/cloud-shell-tutorial.md b/docs/guides/gcp-gce/cloud-shell-tutorial.md new file mode 100644 index 0000000000000..e48f538bb814f --- /dev/null +++ b/docs/guides/gcp-gce/cloud-shell-tutorial.md @@ -0,0 +1,88 @@ +# How to set up OpenVSCode Server on GCE + +## Welcome 👋! + +In this tutorial, you are going to set up [OpenVSCode Server](https://github.com/gitpod-io/openvscode-server) on GCE. + +**Time to complete**: Less than 10 minutes + +Click the **Start** button to move to the next step. + +## Enable required APIs + +Click the button below to enable the APIs required to complete this tutorial. + + + +## Create a VM + +Let's first create a virtual machine to host our server: + +```bash +gcloud beta compute instances create openvscode-server --machine-type=e2-micro --image=ubuntu-2004-focal-v20210908 --image-project=ubuntu-os-cloud --boot-disk-size=10GB --boot-disk-type=pd-balanced --boot-disk-device-name=openvscode-server --tags=http-openvscode-server +``` + +**Tip**: Click the copy button on the side of the code box and paste the command in the Cloud Shell terminal to run it. + +### Allow HTTP & HTTPS + +To access OpenVSCode Server, we have to allow HTTP traffic on port 3000 to the server: + +```bash +gcloud compute firewall-rules create openvscode-server-allow-http-3000 --direction=INGRESS --priority=1000 --network=default --action=ALLOW --rules=tcp:3000 --source-ranges=0.0.0.0/0 --target-tags=http-openvscode-server +``` + +Next, you will ssh into the newly created VM to install OpenVSCode Server. + +## Install OpenVSCode Server + +### Connect to the VM + +```bash +gcloud beta compute ssh "openvscode-server" --project "dcs-openvscode-server" +``` + +### Download OpenVSCode Server + +**Caution**: Make sure you successfully connected to the `openvscode-server` VM before you execute the following commands. Your prompt should read: `your-name@openvscode-server:~$` + +First, let's define the release version we want to download. You can find the latest version on the [Releases](https://github.com/gitpod-io/openvscode-server/releases) page. + +```bash +export SERVER_VERSION=1.60.0 # Replace with the latest version +``` + +With that in place, let's download & extract OpenVSCode server: + +```bash +wget https://github.com/gitpod-io/openvscode-server/releases/download/openvscode-server-v$SERVER_VERSION/openvscode-server-v$SERVER_VERSION-linux-x64.tar.gz -O code-server.tar.gz +tar -xzf code-server.tar.gz +rm code-server.tar.gz +``` + +### Execute the startup script + +While you are still connected to the VM, execute the following command to start OpenVSCode Server: + +```bash +cd openvscode-server-v$SERVER_VERSION-linux-x64 +./server.sh +``` + +**Note**: If you cancel the script, the OpenVSCode Server will stop. + +Next up, you are going to access the shiny new OpenVSCode Server in your browser. + +## Access OpenVSCode Server in your browser + +Congratulations 🎉! Use the following command to access OpenVSCode Server in a new browser tab. + +With the server still running, open a new Cloud Shell tab and execute the following command: + +```bash +export SERVER_IP=$(gcloud compute instances describe openvscode-server \ + --format='get(networkInterfaces[0].accessConfigs[0].natIP)') +echo "http://$SERVER_IP:3000" +``` + +Click the URL displayed in the terminal to see OpenVSCode Server up and running. diff --git a/docs/guides/kubernetes/.gitignore b/docs/guides/kubernetes/.gitignore new file mode 100644 index 0000000000000..55059ed30d99c --- /dev/null +++ b/docs/guides/kubernetes/.gitignore @@ -0,0 +1,2 @@ +config-env + diff --git a/docs/guides/kubernetes/README.md b/docs/guides/kubernetes/README.md new file mode 100644 index 0000000000000..f16cf1cd0aa36 --- /dev/null +++ b/docs/guides/kubernetes/README.md @@ -0,0 +1,118 @@ +# Deploy OpenVSCode Server to Kubernetes + +## Prerequisites + +To complete this guide, you need: +- a Kubernetes 1.19+ cluster +- the [kubectl CLI](https://kubernetes.io/docs/reference/kubectl/overview/) +- the [ytt CLI](https://carvel.dev/ytt/) (YAML templating tool from the [Carvel tools](https://carvel.dev/)) + +## Setup + +The directories `config` and `config-ext` contain Kubernetes manifest files, +using ytt for templating. + +Check out file `config/values.yml` to see overridable parameters: + +```yaml +#@data/values +--- +#! Set application name. +APP: openvscode-server + +#! Set target namespace. +NAMESPACE: openvscode + +#! Set the public-facing domain used for accessing the application. +DOMAIN: openvscode.example.com + +#! Set storage class to use for persistent data. +STORAGE_CLASS: "" + +#! Set storage size for persistent data. +STORAGE_SIZE: 2Gi + +#! Set storage mode for persistent data. +STORAGE_ACCESS_MODE: ReadWriteOnce + +#! Set cert-manager cluster issuer to use when ingress TLS is enabled. +CERT_MANAGER_CLUSTER_ISSUER: letsencrypt-prod +``` + +In order to override any of these parameters, create a new file. +For example, let's create file `config-env/my-values.yml`: + +```yaml +#@data/values +--- +#! Set a custom value for parameter domain. +DOMAIN: vscode.company.com +``` + +In `config-ext` you'll find configuration overlays, providing extensions +for the base configuration. + +For example, let's say you want to expose OpenVSCode Server using an +`Ingress` route (`ClusterIP` is used by default, preventing any external access), +you may want to add the overlay `config-ext/ingress.yml`. + +In case you're using [Contour](https://projectcontour.io/) +as an `Ingress` controller, you need to enable WebSocket support by adding the overlay +`config-ext/ingress-contour.yml`. + +In order to enable TLS configuration at the `Ingress` level, just add the +overlay `config-ext/ingress-tls.yml`: please note that you need +[cert-manager](https://cert-manager.io/) up and running in order to +generate TLS certificates. + +Now that you have your customized parameters ready, let's use ytt to generate +the target Kubernetes manifest files: + +```shell +ytt -f config -f config-ext/ingress.yml -f config-ext/ingress-contour.yml -f config-env/my-values.yml +``` + +## Start the server + +You're almost done! + +Deploy OpenVSCode Server to your Kubernetes cluster using kubectl and ytt: + +```shell +ytt -f config -f config-ext/ingress.yml -f config-ext/ingress-contour.yml -f config-env/my-values.yml | kubectl apply -f- +``` + +OpenVSCode Server is running in the namespace `openvscode` by default: + +```shell +kubectl -n openvscode get po +NAME READY STATUS RESTARTS AGE +server-56c675fb56-c8jz6 1/1 Running 0 53s +``` + +## Access OpenVSCode Server + +The default configuration does not expose OpenVSCode Server: you need to +open a connection to the Kubernetes service from your workstation: + +```shell +kubectl -n openvscode port-forward svc/server 3000:3000 +``` + +Now open your browser at localhost:3000 and start using OpenVSCode Server. + +In case you enabled overlays for `Ingress` support, just open your browser +with the domain you've set in the configuration parameters (see `DOMAIN`): + +```shell +kubectl -n openvscode get ingress +``` + +## Teardown + +Remove OpenVSCode Server from your cluster by deleting its namespace +(default is `openvscode`): + +```shell +kubectl delete ns openvscode +``` diff --git a/docs/guides/kubernetes/config-ext/ingress-contour.yml b/docs/guides/kubernetes/config-ext/ingress-contour.yml new file mode 100644 index 0000000000000..594ddf317a5f9 --- /dev/null +++ b/docs/guides/kubernetes/config-ext/ingress-contour.yml @@ -0,0 +1,10 @@ +#@ load("@ytt:overlay", "overlay") +#@ load("@ytt:data", "data") + +#@overlay/match by=overlay.subset({"kind": "Ingress", "metadata":{"name":"server"}}),expects=1 +--- +metadata: + #@overlay/match missing_ok=True + annotations: + #@overlay/match missing_ok=True + projectcontour.io/websocket-routes: / diff --git a/docs/guides/kubernetes/config-ext/ingress-tls.yml b/docs/guides/kubernetes/config-ext/ingress-tls.yml new file mode 100644 index 0000000000000..915fee5c34fa3 --- /dev/null +++ b/docs/guides/kubernetes/config-ext/ingress-tls.yml @@ -0,0 +1,17 @@ +#@ load("@ytt:overlay", "overlay") +#@ load("@ytt:data", "data") + +#@overlay/match by=overlay.subset({"kind": "Ingress", "metadata":{"name":"server"}}),expects=1 +--- +metadata: + #@overlay/match missing_ok=True + annotations: + kubernetes.io/tls-acme: "true" + ingress.kubernetes.io/force-ssl-redirect: "true" + cert-manager.io/cluster-issuer: #@ data.values.CERT_MANAGER_CLUSTER_ISSUER +spec: + #@overlay/match missing_ok=True + tls: + - hosts: + - #@ data.values.DOMAIN + secretName: #@ "{}-tls".format(data.values.DOMAIN) diff --git a/docs/guides/kubernetes/config-ext/ingress.yml b/docs/guides/kubernetes/config-ext/ingress.yml new file mode 100644 index 0000000000000..369c8d7e496f9 --- /dev/null +++ b/docs/guides/kubernetes/config-ext/ingress.yml @@ -0,0 +1,19 @@ +#@ load("@ytt:data", "data") +--- +apiVersion: networking.k8s.io/v1 +kind: Ingress +metadata: + name: server + namespace: #@ data.values.NAMESPACE +spec: + rules: + - host: #@ data.values.DOMAIN + http: + paths: + - pathType: ImplementationSpecific + path: / + backend: + service: + name: server + port: + number: 3000 diff --git a/docs/guides/kubernetes/config-ext/load-balancer.yml b/docs/guides/kubernetes/config-ext/load-balancer.yml new file mode 100644 index 0000000000000..df0b66f2c4e4a --- /dev/null +++ b/docs/guides/kubernetes/config-ext/load-balancer.yml @@ -0,0 +1,5 @@ +#@ load("@ytt:data", "data") +#@overlay/match by=overlay.subset({"kind": "Service", "metadata":{"name":"server"}}),expects=1 +--- +spec: + type: LoadBalancer diff --git a/docs/guides/kubernetes/config/0-namespace.yml b/docs/guides/kubernetes/config/0-namespace.yml new file mode 100644 index 0000000000000..47a00de1c7c24 --- /dev/null +++ b/docs/guides/kubernetes/config/0-namespace.yml @@ -0,0 +1,6 @@ +#@ load("@ytt:data", "data") +--- +apiVersion: v1 +kind: Namespace +metadata: + name: #@ data.values.NAMESPACE diff --git a/docs/guides/kubernetes/config/deployment.yml b/docs/guides/kubernetes/config/deployment.yml new file mode 100644 index 0000000000000..0843bc659c860 --- /dev/null +++ b/docs/guides/kubernetes/config/deployment.yml @@ -0,0 +1,52 @@ +#@ load("@ytt:data", "data") +#@ load("helpers.star", "labels_for_component") +--- +apiVersion: apps/v1 +kind: Deployment +metadata: + name: server + namespace: #@ data.values.NAMESPACE + labels: #@ labels_for_component("openvscode-server") +spec: + replicas: 1 + selector: + matchLabels: #@ labels_for_component("openvscode-server") + template: + metadata: + labels: #@ labels_for_component("openvscode-server") + spec: + securityContext: + fsGroup: 1000 + initContainers: + - name: init-data + image: busybox:1.34.0 + command: [ "mkdir", "-p", "/home/workspace/.opencode-server" ] + volumeMounts: + - mountPath: /home/workspace + name: data + containers: + - name: server + image: gitpod/openvscode-server + resources: + requests: + memory: 256M + limits: + memory: 256M + ports: + - name: http + containerPort: 3000 + livenessProbe: + httpGet: + port: http + path: / + readinessProbe: + httpGet: + port: http + path: / + volumeMounts: + - mountPath: /home/workspace + name: data + volumes: + - name: data + persistentVolumeClaim: + claimName: data-pv-claim diff --git a/docs/guides/kubernetes/config/helpers.star b/docs/guides/kubernetes/config/helpers.star new file mode 100644 index 0000000000000..167822073b741 --- /dev/null +++ b/docs/guides/kubernetes/config/helpers.star @@ -0,0 +1,8 @@ +load("@ytt:data", "data") + +def labels_for_component(comp): + return { + "app.kubernetes.io/name": comp, + "app.kubernetes.io/part-of": data.values.APP, + } +end diff --git a/docs/guides/kubernetes/config/pvc.yml b/docs/guides/kubernetes/config/pvc.yml new file mode 100644 index 0000000000000..677578207f332 --- /dev/null +++ b/docs/guides/kubernetes/config/pvc.yml @@ -0,0 +1,15 @@ +#@ load("@ytt:data", "data") +--- +apiVersion: v1 +kind: PersistentVolumeClaim +metadata: + name: data-pv-claim + namespace: #@ data.values.NAMESPACE +spec: + #@ if/end data.values.STORAGE_CLASS != "": + storageClassName: #@ data.values.STORAGE_CLASS + accessModes: + - #@ data.values.STORAGE_ACCESS_MODE + resources: + requests: + storage: #@ data.values.STORAGE_SIZE diff --git a/docs/guides/kubernetes/config/service.yml b/docs/guides/kubernetes/config/service.yml new file mode 100644 index 0000000000000..cab071d477a53 --- /dev/null +++ b/docs/guides/kubernetes/config/service.yml @@ -0,0 +1,14 @@ +#@ load("@ytt:data", "data") +#@ load("helpers.star", "labels_for_component") +--- +apiVersion: v1 +kind: Service +metadata: + name: server + namespace: #@ data.values.NAMESPACE +spec: + ports: + - port: 3000 + protocol: TCP + targetPort: http + selector: #@ labels_for_component("openvscode-server") diff --git a/docs/guides/kubernetes/config/values.yml b/docs/guides/kubernetes/config/values.yml new file mode 100644 index 0000000000000..737a5b85b8f60 --- /dev/null +++ b/docs/guides/kubernetes/config/values.yml @@ -0,0 +1,22 @@ +#@data/values +--- +#! Set application name. +APP: openvscode-server + +#! Set target namespace. +NAMESPACE: openvscode + +#! Set the public-facing domain used for accessing the application. +DOMAIN: openvscode.example.com + +#! Set storage class to use for persistent data. +STORAGE_CLASS: "" + +#! Set storage size for persistent data. +STORAGE_SIZE: 2Gi + +#! Set storage mode for persistent data. +STORAGE_ACCESS_MODE: ReadWriteOnce + +#! Set cert-manager cluster issuer to use when ingress TLS is enabled. +CERT_MANAGER_CLUSTER_ISSUER: letsencrypt-prod diff --git a/docs/guides/railway/README.md b/docs/guides/railway/README.md new file mode 100644 index 0000000000000..a17c459fe7566 --- /dev/null +++ b/docs/guides/railway/README.md @@ -0,0 +1,16 @@ +# Deploy OpenVSCode Server to Railway + +## Prerequisites + +To complete this guide, you need: +* a [Railway](https://railway.app/) account + +## Deploy & access OpenVSCode Server + +To deploy to Railway, click the following button and follow the instructions: + +[![Deploy on Railway](https://railway.app/button.svg)](https://railway.app/new/template?template=https%3A%2F%2Fgithub.com%2Fgitpod-io%2Fopenvscode-releases&envs=RELEASE_TAG%2CPORT&RELEASE_TAGDefault=openvscode-server-v1.60.0&PORTDefault=3000) + +## Teardown + +Delete the project in the Railway dashboard. diff --git a/docs/guides/render/README.md b/docs/guides/render/README.md new file mode 100644 index 0000000000000..1067bdb90c877 --- /dev/null +++ b/docs/guides/render/README.md @@ -0,0 +1,95 @@ +# Deploy OpenVSCode Server to Render + +## Prerequisites + +To complete this guide, you need: +* a [Render](https://render.com/) account + +## Setup + +To deploy to Render, click the following button and follow the instructions: + +[![Deploy to Render](https://render.com/images/deploy-to-render-button.svg)](https://render.com/deploy?repo=https://github.com/render-examples/gitpod-openvscode-server) + +After that, create a name for the service group (for example `OpenVSCode Server`) and click Apply. + +## Start the server + +Render starts the server automatically. + +## Access OpenVSCode Server + +When the deployment is complete, you will see your server listed in the Services section of the Dashboard. Click the dashboard entry to see your server URL to access OpenVSCode Server. + +![image showing where the URL can be found](https://user-images.githubusercontent.com/36797588/134728867-54de3d3f-31e5-4c08-a239-f6d2babeec7b.png) + +## Teardown + +Delete the service in your dashboard. + + +--- + + +# Deploy Secure OpenVSCode Server to Render with OAuth + +## Prerequisites + +To complete this guide, you need: +* a [Render](https://render.com/) account +* an account with the [OAuth Provider](https://oauth2-proxy.github.io/oauth2-proxy/docs/configuration/oauth_provider) of your choice. + +## Set up provider account + +Consult the [OAuth2-Proxy Provider Configuration Documentation](https://oauth2-proxy.github.io/oauth2-proxy/docs/configuration/oauth_provider/), and select at least one provider to use for authenticating users of Open VSCode. Create an OAuth application with your provider of choice. For the Homepage/Base URI, enter a placeholder like `https://openvscode-secure-server.onrender.com`, and for the Callback/Redirect URI, enter a placeholder like `https://openvscode-secure-server.onrender.com/oauth2/callback`. You will update the OAuth2 app with your URIs once your OAuth2-Proxy Server deployment is complete. Save the Client Secret and ID in a secure place like a password manager for later reference. + + +## Set up Open VSCode Server + +To deploy Open VSCode to Render as a private service, click the following button and follow the instructions: + +[![Deploy to Render](https://render.com/images/deploy-to-render-button.svg)](https://render.com/deploy?repo=https://github.com/render-examples/openvscode-private-server) + +After that, create a name for the service group (for example `Private OpenVSCode Server`) and click Apply. + +## Start the server + +Render starts the server automatically. Copy the service address to the clipboard: +![Image showing where the service address can be found](https://user-images.githubusercontent.com/36797588/135016293-fb9b351b-f764-4c22-a1a3-7bfdec386f50.jpeg) + + +## Set up OAuth2-Proxy server + +Fork the [OAuth2-Proxy Render Example Repository](https://github.com/dnilasor/oauth2-proxy). In the Render Dashboard, select YAML from the side navigation and click the New From YAML button: +![Image showing where to initialize a new service from YAML](https://user-images.githubusercontent.com/36797588/135017966-06eb2d3a-1255-42df-800d-38413b8180d8.jpeg) + +After that, use your connected GitHub account or the full URL of your public OAuth-Proxy fork to create a deployment based on the fork. + +## Configure OAuth server + +Create a name for the service group (for example, `Secure Access To Open VSCode`). Next, enter the environment variable values to configure OAuth. + +- For `OAUTH2_PROXY_UPSTREAMS` enter the Service Address for Private Open VSCode Server appended by http:// +- For `OAUTH2_PROXY_CLIENT_ID` enter the Client ID from your OAuth App +- For `OAUTH2_PROXY_CLIENT_SECRET` enter the Client Secret from your OAuth App or password manager +- For `OAUTH2_PROXY_PROVIDER` enter the name of your OAuth provider + +![Image showing YAML service creation and input of sync: false values](https://user-images.githubusercontent.com/36797588/135025049-fd399efb-3c17-4a12-9539-0d12e4306eeb.jpeg) + +## Start the server + +Render starts the server automatically. + +## Access OpenVSCode Server + +When the deployment is complete, you will see your OAuth server listed in the Services section of the Dashboard. Click the dashboard entry to see your server URL to access OpenVSCode Server. You will be prompted to authenticate and then redirected to the private Open VSCode service. + +## Teardown + +Delete the service in your dashboard. + + + + + + diff --git a/docs/sourcedive.snb.md b/docs/sourcedive.snb.md new file mode 100644 index 0000000000000..66d75dda3bfe3 --- /dev/null +++ b/docs/sourcedive.snb.md @@ -0,0 +1,169 @@ +# Interactive introduction to the codebase + +This is a technical deep dive into how OpenVSCode Server turns VS Code into a web IDE, with interactive code search queries and snippets. This is [best viewed on Sourcegraph](https://sourcegraph.com/github.com/sourcegraph/openvscode-server/-/blob/doc/sourcedive.snb.md). + +The code snippets in this file correspond to search queries and can be displayed by clicking the blue "Run search" button to the right of each query. For example, here is a snippet that shows off an instance of dependency injection within VS Code: + +```sourcegraph +patterntype:structural repo:^github\.com/gitpod-io/openvscode-server$@5c8a1f file:^src/vs/code/browser/workbench/workbench\.ts create(document.body, {:[1]}) +``` + +## Architectural overview + +OpenVSCode Server is a fork of VS Code that extends the editor to be runnable in the browser, speaking to a web server that provides a remote dev environment. + +Upstream VS Code consists of [layers](https://github.com/microsoft/vscode/wiki/Source-Code-Organization): + +* `base`: Provides general utilities and user interface building blocks. +* `platform`: Defines service injection support and the base services for VS Code. +* `editor`: The "Monaco" editor is available as a separate downloadable component. +* `workbench`: Hosts the "Monaco" editor and provides the framework for "viewlets" like the Explorer, Status Bar, or Menu Bar, leveraging Electron to implement the VS Code desktop application. +* `code`: The entry point to the desktop app that stitches everything together, this includes the Electron main file and the CLI for example. + +OpenVSCode Server adds an additional [`server` layer](https://github.com/gitpod-io/openvscode-server/tree/main/src/vs/server). The client side remains largely unchanged, save for the injection of RPC-based handlers for things like filesystem and terminal interactions, in place of local handlers. The `server` layer has 3 main components: + +* Web-based workbench +* Remote server +* Remote CLI + +The web-based workbench lives on the client side and is the place where the RPC-based dependencies are injected. VS Code's codebase is modular and makes heavy use of dependency injection, which makes it easier to substitute different implementations. The entrypoint into the web-based workbench is in [workbench.ts](https://sourcegraph.com/github.com/gitpod-io/openvscode-server/-/blob/src/vs/code/browser/workbench/workbench.ts). In that file, the `create` function creates the workbench using dependency injection: + +```sourcegraph +patterntype:structural repo:/gitpod-io/openvscode-server$@5c8a1f fork:yes file:server/browser/workbench/workbench.ts create(document.body, :[2]) +``` + +The workbench talks to the remote server via RPC. There are 2 RPC channels: + +* The management connection handles filesystem and terminal requests +* The extension connection creates the remote extension host process and handles extension-related requests + +Let's take a look at how those connections are set up on the server side. The main entrypoint into the server lives in [`server.main.ts`](https://sourcegraph.com/github.com/gitpod-io/openvscode-server@5c8a1f/-/blob/src/vs/server/node/server.main.ts?L11): + +```sourcegraph +repo:^github\.com/gitpod-io/openvscode-server$@5c8a1f file:^src/vs/server/node/server\.main\.ts export async function main +``` + +Of particular note in the `main` function is `channelServer`, which registers different service channels for handling different types of requests received from the client, such as logging, debugging, and filesystem requests. + + +```sourcegraph +repo:^github\.com/gitpod-io/openvscode-server$@5c8a1f file:^src/vs/server/node/server\.main\.ts const channelServer = +``` + +These channels then relay the requests to the appropriate service implementations. Lower in the `main` function, a `ServiceCollection` instance is used as a dependency injection container that holds all the concrete implementations of the various service types: + +```sourcegraph +repo:^github\.com/gitpod-io/openvscode-server$@8b3e975 file:^src/vs/server/node/server\.main\.ts Const services = new ServiceCollection() +``` + +Then the whole bundle is wrapped by an HTTP server, which is the outermost container that handles requests from the client: + +```sourcegraph +repo:^github\.com/gitpod-io/openvscode-server$@8b3e975 file:^src/vs/server/node/server\.main\.ts const server = http.createServer +``` + +Some of the handler endpoints correspond to static resources: + +```sourcegraph +repo:^github\.com/gitpod-io/openvscode-server$@8b3e975 file:^src/vs/server/node/server\.main\.ts if (pathname === '/vscode-remote-resource') +``` + +Then there are the endpoints that upgrade a HTTP request to a WebSocket connection: + +```sourcegraph +repo:^github\.com/gitpod-io/openvscode-server$@8b3e975 file:^src/vs/server/node/server\.main\.ts server.on('upgrade', +``` + +The aforementioned management connection and extension connection use these WebSocket connections. The management connection connects `channelServer` with your editor window, including requests for handling terminal and fileystem requests. + +On the extension side, there's a special protocol over WebSocket that initiates a handshake to set up the remote extension host process: + +```sourcegraph +repo:^github\.com/gitpod-io/openvscode-server$@8b3e975 file:^src/vs/server/node/server\.main\.ts const controlListener = protocol.onControlMessage +``` + +The extension connection will fork the extension host process and connect the user's editor window. Among other things, keystrokes are sent down this connection, as they may be relevant to extensions in use. + + +## Startup + +Now, let's walk through what happens at startup. + +On server startup, first we create the channel server and register channels to handle RPC calls and events from the web workbench: + +```sourcegraph +repo:^github\.com/gitpod-io/openvscode-server$@8b3e975 const channelServer = new IPCServer +``` + + +Then we create the service collection (effectively the dependency injection container for service implementations, as described earlier) with all services required for the RPC channels: + +```sourcegraph +repo:^github\.com/gitpod-io/openvscode-server$@8b3e975 file:^src/vs/server/node/server\.main\.ts services.set(IRawURITransformerFactory, rawURITransformerFactory) +``` + +Then we instantiate these services and start the HTTP server: + +```sourcegraph +repo:^github\.com/gitpod-io/openvscode-server$@8b3e975 file:^src/vs/server/node/server\.main\.ts const clients = new Map() +``` + +When a user tries to access the server, the web workbench is served by HTTP listener: + +```sourcegraph +repo:^github\.com/gitpod-io/openvscode-server$@8b3e975 file:^src/vs/server/node/server\.main\.ts return handleRoot(req, res, devMode ? options.mainDev || +``` + +The web workbench first loads 3rd party dependencies like xterm: + +```sourcegraph +repo:^github\.com/gitpod-io/openvscode-server$@8b3e975 xterm': `${window.location.origin} file:workbench-dev.html +``` + +...and then itself: + +```sourcegraph +repo:^github\.com/gitpod-io/openvscode-server$@8b3e975 require(['vs/server/browser/workbench/workbench'], +``` + +The web workbench uses dependency injection to configure how to establish WebSockets, load static resources, load webviews, and so on: + +```sourcegraph +repo:^github\.com/gitpod-io/openvscode-server$@8b3e975 create(document.body, { file:src/vs/server/ +``` + +When the web workbench is created, it opens WebSocket connections to the server: + +```sourcegraph +repo:^github\.com/gitpod-io/openvscode-server$@8b3e975 if (req.headers['upgrade'] !== 'websocket' || !req.url) +``` + +There is one connection to the RPC channel server to notify about a new client: + +```sourcegraph +repo:^github\.com/gitpod-io/openvscode-server$@8b3e975 onDidClientConnectEmitter.fire({ protocol, onDidClientDisconnect: onDidClientDisconnectEmitter.event }) +``` + +...and another for the extension host process which is running remote extensions: + +```sourcegraph +repo:^github\.com/gitpod-io/openvscode-server$@8b3e975 const extensionHost = cp.fork(FileAccess.asFileUri +``` + +When a user creates a new terminal the management connection is used to call the remote terminal channel: + +```sourcegraph +repo:^github\.com/gitpod-io/openvscode-server$@8b3e975 return this.createProcess(context.remoteAuthority, args); +``` + +...which delegates to pseudo terminal service: + +```sourcegraph +repo:^github\.com/gitpod-io/openvscode-server$@8b3e975 const persistentTerminalId = await this.ptyService.createProcess +``` + +The terminal service then enacts the corresponding action and relays the response back through the request chain covered above. + +## Diving in + +This hopefully gives you a good overview of how OpenVSCode Server turns VS Code into a web-based IDE. The code is completely open-source and released by Gitpod. You can try out [Gitpod](https://www.gitpod.io/) as a service or dive into more of the [source code on Sourcegraph](https://sourcegraph.com/github.com/gitpod-io/openvscode-server). diff --git a/extensions/vscode-api-tests/src/singlefolder-tests/debug.test.ts b/extensions/vscode-api-tests/src/singlefolder-tests/debug.test.ts index 189bb8e3747c4..f034f27faff9b 100644 --- a/extensions/vscode-api-tests/src/singlefolder-tests/debug.test.ts +++ b/extensions/vscode-api-tests/src/singlefolder-tests/debug.test.ts @@ -105,13 +105,14 @@ suite('vscode API - debug', function () { await fourthVariablesRetrieved; assert.strictEqual(stoppedEvents, 4); - const fifthVariablesRetrieved = new Promise(resolve => variablesReceived = resolve); - await commands.executeCommand('workbench.action.debug.stepOut'); - await fifthVariablesRetrieved; - assert.strictEqual(stoppedEvents, 5); + // const fifthVariablesRetrieved = new Promise(resolve => variablesReceived = resolve); + // await commands.executeCommand('workbench.action.debug.stepOut'); + // await fifthVariablesRetrieved; + // assert.strictEqual(stoppedEvents, 5); let sessionTerminated: () => void; - toDispose.push(debug.onDidTerminateDebugSession(() => { + toDispose.push(debug.onDidTerminateDebugSession(async () => { + await new Promise(c => setTimeout(c, 500)); sessionTerminated(); })); const sessionTerminatedPromise = new Promise(resolve => sessionTerminated = resolve); diff --git a/extensions/vscode-api-tests/src/singlefolder-tests/terminal.test.ts b/extensions/vscode-api-tests/src/singlefolder-tests/terminal.test.ts index cc8fbb858346e..aa79413f3982b 100644 --- a/extensions/vscode-api-tests/src/singlefolder-tests/terminal.test.ts +++ b/extensions/vscode-api-tests/src/singlefolder-tests/terminal.test.ts @@ -4,12 +4,12 @@ *--------------------------------------------------------------------------------------------*/ import { deepEqual, deepStrictEqual, doesNotThrow, equal, strictEqual, throws } from 'assert'; -import { ConfigurationTarget, Disposable, env, EnvironmentVariableMutator, EnvironmentVariableMutatorType, EventEmitter, ExtensionContext, extensions, ExtensionTerminalOptions, Pseudoterminal, Terminal, TerminalDimensions, TerminalOptions, TerminalState, UIKind, window, workspace } from 'vscode'; +import { ConfigurationTarget, Disposable, /* env, */ EnvironmentVariableMutator, EnvironmentVariableMutatorType, EventEmitter, ExtensionContext, extensions, ExtensionTerminalOptions, Pseudoterminal, Terminal, TerminalDimensions, TerminalOptions, TerminalState, /* UIKind, */ window, workspace } from 'vscode'; import { assertNoRpc } from '../utils'; // Disable terminal tests: // - Web https://github.com/microsoft/vscode/issues/92826 -(env.uiKind === UIKind.Web ? suite.skip : suite)('vscode API - terminal', () => { +suite('vscode API - terminal', () => { let extensionContext: ExtensionContext; suiteSetup(async () => { diff --git a/package.json b/package.json index 1a5b1c7b54758..94e34a16a7e8e 100644 --- a/package.json +++ b/package.json @@ -55,7 +55,9 @@ "minify-vscode-reh-web": "node --max_old_space_size=4095 ./node_modules/gulp/bin/gulp.js minify-vscode-reh-web", "hygiene": "node --max_old_space_size=4095 ./node_modules/gulp/bin/gulp.js hygiene", "core-ci": "node --max_old_space_size=4095 ./node_modules/gulp/bin/gulp.js core-ci", - "extensions-ci": "node --max_old_space_size=4095 ./node_modules/gulp/bin/gulp.js extensions-ci" + "extensions-ci": "node --max_old_space_size=4095 ./node_modules/gulp/bin/gulp.js extensions-ci", + "server:init": "yarn --cwd ./build compile && yarn compile && yarn download-builtin-extensions", + "server:smoketest": "yarn --cwd ./test/smoke compile && yarn smoketest-no-compile --web --verbose --headless --electronArgs=\"--disable-dev-shm-usage --use-gl=swiftshader\"" }, "dependencies": { "@microsoft/applicationinsights-web": "^2.6.4", diff --git a/product.json b/product.json index eaaf8983bac17..4036cae6ad764 100644 --- a/product.json +++ b/product.json @@ -1,36 +1,526 @@ { - "nameShort": "Code - OSS", - "nameLong": "Code - OSS", - "applicationName": "code-oss", - "dataFolderName": ".vscode-oss", - "win32MutexName": "vscodeoss", + "nameShort": "OpenVSCode Server", + "nameLong": "OpenVSCode Server", + "applicationName": "opencode-server", + "dataFolderName": ".opencode-server", + "win32MutexName": "opencodeserver", "licenseName": "MIT", - "licenseUrl": "https://github.com/microsoft/vscode/blob/main/LICENSE.txt", - "win32DirName": "Microsoft Code OSS", - "win32NameVersion": "Microsoft Code OSS", - "win32RegValueName": "CodeOSS", + "licenseUrl": "https://github.com/gitpod-io/vscode/blob/main/LICENSE.txt", + "win32DirName": "OpenCode Server", + "win32NameVersion": "OpenCode Server", + "win32RegValueName": "OpenCodeServer", "win32AppId": "{{E34003BB-9E10-4501-8C11-BE3FAA83F23F}", "win32x64AppId": "{{D77B7E06-80BA-4137-BCF4-654B95CCEBC5}", "win32arm64AppId": "{{D1ACE434-89C5-48D1-88D3-E2991DF85475}", "win32UserAppId": "{{C6065F05-9603-4FC4-8101-B9781A25D88E}", "win32x64UserAppId": "{{CC6B787D-37A0-49E8-AE24-8559A032BE0C}", "win32arm64UserAppId": "{{3AEBF0C8-F733-4AD4-BADE-FDB816D53D7B}", - "win32AppUserModelId": "Microsoft.CodeOSS", - "win32ShellNameShort": "C&ode - OSS", - "darwinBundleIdentifier": "com.visualstudio.code.oss", - "linuxIconName": "com.visualstudio.code.oss", + "win32AppUserModelId": "OpenCode", + "win32ShellNameShort": "OpenC&ode", + "darwinBundleIdentifier": "opencode.server", + "linuxIconName": "opencode.server", "licenseFileName": "LICENSE.txt", - "reportIssueUrl": "https://github.com/microsoft/vscode/issues/new", - "urlProtocol": "code-oss", - "webviewContentExternalBaseUrlTemplate": "https://{{uuid}}.vscode-webview.net/{{quality}}/{{commit}}/out/vs/workbench/contrib/webview/browser/pre/", + "reportIssueUrl": "https://github.com/gitpod-io/gitpod/issues/new", + "urlProtocol": "opencode-server", "extensionAllowedProposedApi": [ + "GitHub.vscode-pull-request-github-insiders", + "GitHub.vscode-pull-request-github", + "ms-python.python", + "ms-toolsai.jupyter", + "ms-vscode.js-debug-nightly", + "ms-vscode.js-debug", + "ms-vscode.lsif-browser", "ms-vscode.vscode-js-profile-flame", "ms-vscode.vscode-js-profile-table", - "ms-vscode.remotehub", - "ms-vscode.remotehub-insiders", - "GitHub.remotehub", - "GitHub.remotehub-insiders" + "ms-vscode.vscode-selfhost-test-provider", + "dbaeumer.vscode-eslint" ], + "extensionTips": { + "msjsdiag.debugger-for-chrome": "{**/*.ts,**/*.tsx,**/*.js,**/*.jsx,**/*.es6,**/.babelrc}", + "firefox-devtools.vscode-firefox-debug": "{**/*.ts,**/*.tsx,**/*.js,**/*.jsx,**/*.es6,**/.babelrc}", + "golang.Go": "**/*.go", + "ms-vscode.PowerShell": "{**/*.ps1,**/*.psd1,**/*.psm1,**/*.ps.config,**/*.ps1.config}", + "austin.code-gnu-global": "{**/*.c,**/*.cpp,**/*.h}", + "Ionide.Ionide-fsharp": "{**/*.fsx,**/*.fsi,**/*.fs,**/*.ml,**/*.mli}", + "dbaeumer.vscode-eslint": "{**/*.js,**/*.jsx,**/*.es6,**/.eslintrc.*,**/.eslintrc,**/.babelrc,**/jsconfig.json}", + "dbaeumer.jshint": "{**/*.js,**/*.jsx,**/*.es6,**/.babelrc,**/jsconfig.json,**/.jshintrc,**/.jshintignore}", + "ms-vscode.vscode-typescript-tslint-plugin": "{**/tslint.json}", + "bmewburn.vscode-intelephense-client": "{**/*.php,**/php.ini}", + "felixfbecker.php-intellisense": "{**/*.php,**/php.ini}", + "felixfbecker.php-debug": "{**/*.php,**/php.ini}", + "ikappas.phpcs": "{**/*.php,**/php.ini}", + "rust-lang.rust": "{**/*.rs,**/*.rslib}", + "ms-vscode.cpptools-extension-pack": "{**/*.c,**/*.cpp,**/*.cc,**/.cxx,**/*.hh,**/*.hpp,**/*.hxx,**/*.h}", + "DavidAnson.vscode-markdownlint": "{**/*.md}", + "ms-azuretools.vscode-docker": "{**/dockerfile,**/Dockerfile,**/docker-compose.yml,**/docker-compose.*.yml,**/*.cs,**/project.json,**/global.json,**/*.csproj,**/*.cshtml,**/*.sln,**/appsettings.json,**/*.py,**/*.ipynb,**/*.js,**/*.ts,**/package.json}", + "EditorConfig.EditorConfig": "{**/.editorconfig}", + "HookyQR.beautify": "{**/.jsbeautifyrc,**/*.js,**/*.jsx,**/*.es6,**/.eslintrc.*,**/.babelrc,**/jsconfig.json}", + "donjayamanne.githistory": "{**/.gitignore,**/.git}", + "felipecaputo.git-project-manager": "{**/.gitignore,**/.git}", + "eamodio.gitlens": "{**/.gitignore,**/.git}", + "rebornix.Ruby": "{**/*.rb,**/*.erb,**/*.reek,**/.fasterer.yml,**/ruby-lint.yml,**/.rubocop.yml}", + "DotJoshJohnson.xml": "{**/*.xml}", + "shinnn.stylelint": "{**/.stylelintrc,**/stylelint.config.js}", + "eg2.vscode-npm-script": "{**/package.json}", + "ms-mssql.mssql": "{**/*.sql}", + "bajdzis.vscode-database": "{**/*.sql}", + "mtxr.sqltools": "{**/*.sql}", + "usqlextpublisher.usql-vscode-ext": "{**/*.usql}", + "ms-vscode.sublime-keybindings": "{**/.sublime-project,**/.sublime-workspace}", + "k--kato.intellij-idea-keybindings": "{**/.idea}", + "christian-kohler.npm-intellisense": "{**/package.json}", + "octref.vetur": "{**/*.vue}", + "ms-python.python": "{**/*.py,**/*.ipynb}", + "ms-toolsai.jupyter": "{**/*.ipynb}", + "cake-build.cake-vscode": "{**/build.cake}", + "Angular.ng-template": "{**/.angular-cli.json,**/angular.json,**/*.ng.html,**/*.ng,**/*.ngml}", + "vscjava.vscode-maven": "**/pom.xml", + "ms-azuretools.vscode-azureterraform": "**/*.tf", + "HashiCorp.terraform": "**/*.tf", + "vsciot-vscode.vscode-arduino": "**/*.ino", + "ms-kubernetes-tools.vscode-kubernetes-tools": "{**/Chart.yaml}", + "GoogleCloudTools.cloudcode": "{**/skaffold.yaml}", + "Oracle.oracledevtools": "{**/*.sql}" + }, + "extensionImportantTips": { + "ms-python.python": { + "name": "Python", + "languages": [ + "python" + ], + "pattern": "{**/*.py,**/*.ipynb}" + }, + "ms-toolsai.jupyter": { + "name": "Jupyter", + "pattern": "{**/*.ipynb}" + }, + "golang.Go": { + "name": "Go", + "languages": [ + "go" + ], + "pattern": "**/*.go" + }, + "vscjava.vscode-java-pack": { + "name": "Java", + "languages": [ + "java" + ], + "pattern": "{**/*.java}", + "isExtensionPack": true + }, + "ms-vscode.PowerShell": { + "name": "PowerShell", + "languages": [ + "powershell" + ], + "pattern": "{**/*.ps1,**/*.psd1,**/*.psm1}" + }, + "ms-vscode.cpptools": { + "name": "C/C++", + "languages": [ + "c", + "cpp" + ], + "pattern": "{**/*.c,**/*.cpp,**/*.cc,**/.cxx,**/*.hh,**/*.hpp,**/*.hxx,**/*.h}" + }, + "ms-azuretools.vscode-docker": { + "name": "Docker", + "languages": [ + "dockerfile" + ], + "pattern": "{**/dockerfile,**/Dockerfile,**/docker-compose.yml,**/docker-compose.*.yml}" + }, + "octref.vetur": { + "name": "Vetur", + "languages": [ + "vue" + ], + "pattern": "{**/*.vue}" + }, + "ms-vscode.cmake-tools": { + "name": "CMake Tools", + "pattern": "{**/CMakeLists.txt}" + }, + "msazurermtools.azurerm-vscode-tools": { + "name": "Azure Resource Manager", + "pattern": "{**/azuredeploy.json}" + }, + "ms-azuretools.vscode-bicep": { + "name": "Bicep", + "pattern": "{**/*.bicep}" + }, + "svelte.svelte-vscode": { + "name": "Svelte", + "pattern": "{**/*.svelte}" + } + }, + "keymapExtensionTips": [ + "vscodevim.vim", + "ms-vscode.sublime-keybindings", + "ms-vscode.atom-keybindings", + "ms-vscode.brackets-keybindings", + "ms-vscode.vs-keybindings", + "ms-vscode.notepadplusplus-keybindings", + "k--kato.intellij-idea-keybindings", + "hiro-sun.vscode-emacs", + "alphabotsec.vscode-eclipse-keybindings", + "alefragnani.delphi-keybindings" + ], + "languageExtensionTips": [ + "ms-python.python", + "ms-toolsai.jupyter", + "vscjava.vscode-java-pack", + "ecmel.vscode-html-css", + "octref.vetur", + "bmewburn.vscode-intelephense-client", + "dsznajder.es7-react-js-snippets", + "golang.go", + "ms-vscode.powershell", + "dart-code.dart-code", + "rust-lang.rust", + "rebornix.ruby" + ], + "configBasedExtensionTips": { + "git": { + "configPath": ".git/config", + "configName": "Git", + "recommendations": { + "github.vscode-pull-request-github": { + "name": "GitHub Pull Request", + "remotes": [ + "github.com" + ] + }, + "eamodio.gitlens": { + "name": "GitLens" + } + } + } + }, + "extensionKeywords": { + "md": [ + "Markdown" + ], + "js": [ + "JavaScript" + ], + "jsx": [ + "JavaScript" + ], + "es6": [ + "JavaScript" + ], + "html": [ + "Html" + ], + "ts": [ + "TypeScript" + ], + "tsx": [ + "TypeScript" + ], + "css": [ + "CSS" + ], + "scss": [ + "SASS" + ], + "txt": [ + "Text" + ], + "php": [ + "PHP" + ], + "php3": [ + "PHP" + ], + "php4": [ + "PHP" + ], + "ph3": [ + "PHP" + ], + "ph4": [ + "PHP" + ], + "xml": [ + "XML" + ], + "py": [ + "Python" + ], + "pyc": [ + "Python" + ], + "pyd": [ + "Python" + ], + "pyo": [ + "Python" + ], + "pyw": [ + "Python" + ], + "pyz": [ + "Python" + ], + "java": [ + "Java" + ], + "class": [ + "Java" + ], + "jar": [ + "Java" + ], + "c": [ + "c", + "objective c", + "objective-c" + ], + "m": [ + "objective c", + "objective-c" + ], + "mm": [ + "objective c", + "objective-c" + ], + "cpp": [ + "cpp", + "c plus plus", + "c", + "c++" + ], + "cc": [ + "cpp", + "c plus plus", + "c", + "c++" + ], + "cxx": [ + "cpp", + "c plus plus", + "c++" + ], + "hh": [ + "cpp", + "c plus plus", + "c++" + ], + "hpp": [ + "cpp", + "c++" + ], + "h": [ + "cpp", + "c plus plus", + "c++", + "c", + "objective c", + "objective-c" + ], + "sql": [ + "sql" + ], + "sh": [ + "bash" + ], + "bash": [ + "bash" + ], + "zsh": [ + "bash", + "zshell" + ], + "cs": [ + "c#", + "csharp" + ], + "csproj": [ + "c#", + "csharp" + ], + "sln": [ + "c#", + "csharp" + ], + "go": [ + "go" + ], + "sty": [ + "latex" + ], + "tex": [ + "latex" + ], + "ps": [ + "powershell" + ], + "ps1": [ + "powershell" + ], + "rs": [ + "rust" + ], + "rslib": [ + "rust" + ], + "hs": [ + "haskell" + ], + "lhs": [ + "haskell" + ], + "scm": [ + "scheme" + ], + "ss": [ + "scheme" + ], + "clj": [ + "clojure" + ], + "cljs": [ + "clojure" + ], + "cljc": [ + "clojure" + ], + "edn": [ + "clojure" + ], + "erl": [ + "erlang" + ], + "hrl": [ + "erlang" + ], + "scala": [ + "scala" + ], + "sc": [ + "scala" + ], + "pl": [ + "perl" + ], + "pm": [ + "perl" + ], + "t": [ + "perl" + ], + "pod": [ + "perl" + ], + "groovy": [ + "groovy" + ], + "swift": [ + "swift" + ], + "rb": [ + "ruby" + ], + "rbw": [ + "ruby" + ], + "jl": [ + "julia" + ], + "f": [ + "fortran" + ], + "for": [ + "fortran" + ], + "f90": [ + "fortran" + ], + "f95": [ + "fortran" + ], + "coffee": [ + "CoffeeScript" + ], + "litcoffee": [ + "CoffeeScript" + ], + "yaml": [ + "yaml" + ], + "yml": [ + "yaml" + ], + "dart": [ + "dart" + ], + "json": [ + "json" + ] + }, + "extensionAllowedBadgeProviders": [ + "api.bintray.com", + "api.travis-ci.com", + "api.travis-ci.org", + "app.fossa.io", + "badge.buildkite.com", + "badge.fury.io", + "badge.waffle.io", + "badgen.net", + "badges.frapsoft.com", + "badges.gitter.im", + "badges.greenkeeper.io", + "cdn.travis-ci.com", + "cdn.travis-ci.org", + "ci.appveyor.com", + "circleci.com", + "cla.opensource.microsoft.com", + "codacy.com", + "codeclimate.com", + "codecov.io", + "coveralls.io", + "david-dm.org", + "deepscan.io", + "dev.azure.com", + "docs.rs", + "flat.badgen.net", + "gemnasium.com", + "githost.io", + "gitlab.com", + "godoc.org", + "goreportcard.com", + "img.shields.io", + "isitmaintained.com", + "marketplace.visualstudio.com", + "nodesecurity.io", + "opencollective.com", + "snyk.io", + "travis-ci.com", + "travis-ci.org", + "visualstudio.com", + "vsmarketplacebadge.apphb.com", + "www.bithound.io", + "www.versioneye.com" + ], + "extensionAllowedBadgeProvidersRegex": [ + "^https:\\/\\/github\\.com\\/[^/]+\\/[^/]+\\/workflows\\/.*badge\\.svg" + ], + "extensionKind": { + "Shan.code-settings-sync": ["ui"], + "shalldie.background": ["ui"], + "CoenraadS.bracket-pair-colorizer": ["ui", "workspace"], + "CoenraadS.bracket-pair-colorizer-2": ["ui"], + "wayou.vscode-todo-highlight": ["ui", "workspace"], + "aaron-bond.better-comments": ["ui", "workspace"], + "vscodevim.vim": ["ui"], + "tuttieee.emacs-mcx": ["ui"] + }, + "extensionPointExtensionKind": { + "typescriptServerPlugins": ["workspace"] + }, "builtInExtensions": [ { "name": "ms-vscode.references-view", @@ -92,5 +582,15 @@ "publisherDisplayName": "Microsoft" } } + ], + "extensionsGallery": { + "serviceUrl": "https://open-vsx.org/vscode/gallery", + "itemUrl": "https://open-vsx.org/vscode/item", + "resourceUrlTemplate": "https://open-vsx.org/vscode/asset/{publisher}/{name}/{version}/Microsoft.VisualStudio.Code.WebResources/{path}", + "controlUrl": "", + "recommendationsUrl": "" + }, + "linkProtectionTrustedDomains": [ + "https://open-vsx.org" ] } diff --git a/remote/.yarnrc b/remote/.yarnrc index bce4202aea7c9..f29826e0b76d4 100644 --- a/remote/.yarnrc +++ b/remote/.yarnrc @@ -1,3 +1,3 @@ disturl "http://nodejs.org/dist" -target "14.16.0" +target "14.17.6" runtime "node" diff --git a/resources/server/bin/code.cmd b/resources/server/bin/code.cmd new file mode 100644 index 0000000000000..fab09533b42ea --- /dev/null +++ b/resources/server/bin/code.cmd @@ -0,0 +1,5 @@ +@echo off +setlocal +set VSCODE_DEV= +"%~dp0\..\node.exe" "%~dp0\..\out\server-cli.js" %* +endlocal diff --git a/resources/server/bin/code.sh b/resources/server/bin/code.sh new file mode 100644 index 0000000000000..ded4731f6d4b0 --- /dev/null +++ b/resources/server/bin/code.sh @@ -0,0 +1,10 @@ +#!/usr/bin/env bash + +if [[ "$OSTYPE" == "darwin"* ]]; then + realpath() { [[ $1 = /* ]] && echo "$1" || echo "$PWD/${1#./}"; } + ROOT=$(dirname "$(realpath "$0")") +else + ROOT=$(dirname "$(readlink -f $0)") +fi + +exec $ROOT/../node $ROOT/../out/server-cli.js "$@" diff --git a/resources/server/manifest.json b/resources/server/manifest.json new file mode 100644 index 0000000000000..80f7b447d0615 --- /dev/null +++ b/resources/server/manifest.json @@ -0,0 +1,5 @@ +{ + "start_url": "/", + "lang": "en-US", + "display": "standalone" +} diff --git a/resources/server/server.cmd b/resources/server/server.cmd new file mode 100644 index 0000000000000..be5203f93c70e --- /dev/null +++ b/resources/server/server.cmd @@ -0,0 +1,6 @@ +@echo off +setlocal +set VSCODE_DEV= +set PATH="%~dp0\bin";%PATH% +"%~dp0\node.exe" "%~dp0\out\server.js" %* +endlocal diff --git a/resources/server/server.sh b/resources/server/server.sh new file mode 100644 index 0000000000000..2574baa98f30e --- /dev/null +++ b/resources/server/server.sh @@ -0,0 +1,11 @@ +#!/usr/bin/env bash + +if [[ "$OSTYPE" == "darwin"* ]]; then + realpath() { [[ $1 = /* ]] && echo "$1" || echo "$PWD/${1#./}"; } + ROOT=$(dirname "$(realpath "$0")") +else + ROOT=$(dirname "$(readlink -f $0)") +fi + +PATH=$ROOT/bin:$PATH +exec $ROOT/node $ROOT/out/server.js "$@" diff --git a/resources/server/test/test-web-integration.sh b/resources/server/test/test-web-integration.sh new file mode 100755 index 0000000000000..09ddddffc1c7a --- /dev/null +++ b/resources/server/test/test-web-integration.sh @@ -0,0 +1,31 @@ +#!/usr/bin/env bash +set -e + +if [[ "$OSTYPE" == "darwin"* ]]; then + realpath() { [[ $1 = /* ]] && echo "$1" || echo "$PWD/${1#./}"; } + ROOT=$(dirname $(dirname $(dirname $(dirname $(realpath "$0"))))) +else + ROOT=$(dirname $(dirname $(dirname $(dirname $(readlink -f $0))))) +fi + +cd $ROOT + +# Tests in the extension host +TEST_SCRIPT="$ROOT/test/integration/browser/out/index.js" + +/usr/bin/env node "$TEST_SCRIPT" --workspacePath=$ROOT/extensions/vscode-api-tests/testWorkspace --enable-proposed-api=vscode.vscode-api-tests --extensionDevelopmentPath=$ROOT/extensions/vscode-api-tests --extensionTestsPath=$ROOT/extensions/vscode-api-tests/out/singlefolder-tests "$@" + +/usr/bin/env node "$TEST_SCRIPT" --workspacePath=$ROOT/extensions/vscode-api-tests/testworkspace.code-workspace --enable-proposed-api=vscode.vscode-api-tests --extensionDevelopmentPath=$ROOT/extensions/vscode-api-tests --extensionTestsPath=$ROOT/extensions/vscode-api-tests/out/workspace-tests "$@" + +# This seems it's electron only? +# /usr/bin/env node "$TEST_SCRIPT" --workspacePath=$ROOT/extensions/vscode-colorize-tests/test --extensionDevelopmentPath=$ROOT/extensions/vscode-colorize-tests --extensionTestsPath=$ROOT/extensions/vscode-colorize-tests/out "$@" + +/usr/bin/env node "$TEST_SCRIPT" --workspacePath=$ROOT/extensions/typescript-language-features/test-workspace --extensionDevelopmentPath=$ROOT/extensions/typescript-language-features --extensionTestsPath=$ROOT/extensions/typescript-language-features/out/test/unit "$@" + +/usr/bin/env node "$TEST_SCRIPT" --workspacePath=$ROOT/extensions/markdown-language-features/test-workspace --extensionDevelopmentPath=$ROOT/extensions/markdown-language-features --extensionTestsPath=$ROOT/extensions/markdown-language-features/out/test "$@" + +/usr/bin/env node "$TEST_SCRIPT" --workspacePath=$ROOT/extensions/emmet/test-workspace --extensionDevelopmentPath=$ROOT/extensions/emmet --extensionTestsPath=$ROOT/extensions/emmet/out/test "$@" + +/usr/bin/env node "$TEST_SCRIPT" --workspacePath=$(mktemp -d 2>/dev/null) --enable-proposed-api=vscode.git --extensionDevelopmentPath=$ROOT/extensions/git --extensionTestsPath=$ROOT/extensions/git/out/test "$@" + +/usr/bin/env node "$TEST_SCRIPT" --workspacePath=$(mktemp -d 2>/dev/null) --extensionDevelopmentPath=$ROOT/extensions/ipynb --extensionTestsPath=$ROOT/extensions/ipynb/out/test "$@" diff --git a/resources/server/web.sh b/resources/server/web.sh new file mode 100755 index 0000000000000..7bca95eb23572 --- /dev/null +++ b/resources/server/web.sh @@ -0,0 +1,14 @@ +#!/usr/bin/env bash + +if [[ "$OSTYPE" == "darwin"* ]]; then + realpath() { [[ $1 = /* ]] && echo "$1" || echo "$PWD/${1#./}"; } + ROOT=$(dirname $(dirname $(dirname $(realpath "$0")))) +else + ROOT=$(dirname $(dirname $(dirname $(readlink -f $0)))) +fi + +export NODE_ENV=development +export VSCODE_DEV=1 + +SERVER_SCRIPT="$ROOT/out/server.js" +exec /usr/bin/env node "$SERVER_SCRIPT" "$@" diff --git a/scripts/setup-google-adc.sh b/scripts/setup-google-adc.sh new file mode 100755 index 0000000000000..30e087bf00f54 --- /dev/null +++ b/scripts/setup-google-adc.sh @@ -0,0 +1,22 @@ +#!/bin/bash +# #### Instructions to fill the GCP_ADC_FILE env var +# 1. `gcloud auth login ` and authenticate +# 2. `gcloud auth application-default login` and authenticate +# 3. `cat ~/.config/gcloud/application_default_credentials.json` and copy the output +# 4. Go to https://gitpod.io/settings/ and create: +# - name: GCP_ADC_FILE +# - value: paste-the-output +# - repo: gitpod-io/vscode + +GCLOUD_ADC_PATH="/home/gitpod/.config/gcloud/application_default_credentials.json" + +if [ ! -f "$GCLOUD_ADC_PATH" ]; then + if [ -z "$GCP_ADC_FILE" ]; then + echo "GCP_ADC_FILE not set, doing nothing." + return; + fi + echo "$GCP_ADC_FILE" > "$GCLOUD_ADC_PATH" + echo "Set GOOGLE_APPLICATION_CREDENTIALS value based on contents from GCP_ADC_FILE" +fi +export GOOGLE_APPLICATION_CREDENTIALS="$GCLOUD_ADC_PATH" + diff --git a/scripts/sync-with-upstream.sh b/scripts/sync-with-upstream.sh new file mode 100755 index 0000000000000..373d31e24e6c1 --- /dev/null +++ b/scripts/sync-with-upstream.sh @@ -0,0 +1,89 @@ +#!/usr/bin/env bash + +echo "Syncing openvscode-server with upstream" + +upstream_url="https://github.com/microsoft/vscode.git" +upstream_branch=${1:-"upstream/main"} +local_branch=${2:-"main"} +base_commit_msg=${3:-"code web server initial commit"} +only_sync=${4:-"false"} + +exit_script() { + reason=$1 + echo "Update script ended unsucessfully" + echo "Reason: $reason" + exit 1 +} + +if [[ "$OSTYPE" == "darwin"* ]]; then + realpath() { [[ $1 = /* ]] && echo "$1" || echo "$PWD/${1#./}"; } + ROOT=$(dirname $(dirname $(realpath "$0"))) +else + ROOT=$(dirname $(dirname $(readlink -f $0))) + # --disable-dev-shm-usage --use-gl=swiftshader: when run on docker containers where size of /dev/shm + # partition < 64MB which causes OOM failure for chromium compositor that uses the partition for shared memory + LINUX_EXTRA_ARGS="--disable-dev-shm-usage --use-gl=swiftshader" +fi + +# Checks is there's an upstream remote repository and if not +# set it to $upstream_url +check_upstream() { + git remote -v | grep --quiet upstream + if [[ $? -ne 0 ]]; then + echo "Upstream repository not configured" + echo "Setting upstream URL to ${upstream_url}" + git remote add upstream $upstream_url + fi +} + +# Gets the base commit +get_base_commit() { + local base_commit=$(git log --pretty="%H" --max-count=1 --grep "$base_commit_msg") + if [[ -z $base_commit ]]; then + exit_script "Could not find base commit" + fi + echo $base_commit +} + +# Fetch updates from upstream and rebase +sync() { + echo "Shallow fetching upstream..." + git fetch upstream + git checkout $local_branch + echo "Rebasing $local_branch branch onto $upstream_branch from upstream" + git rebase --onto=$upstream_branch $(get_base_commit)~ $local_branch + if [[ $? -ne 0 ]]; then + echo "There are merge conflicts doing the rebase." + echo "Please resolve them or abort the rebase." + exit_script "Could not rebase succesfully" + fi + echo "$local_branch sucessfully updated" +} + +cd $ROOT + +# Sync +check_upstream +sync + +if [[ "$only_sync" == "true" ]]; then + exit 0 +fi + +# Clean and build +# git clean -dfx +yarn && yarn server:init +if [[ $? -ne 0 ]]; then + exit_script "There are some errors during compilation" +fi + +# Configuration +export NODE_ENV=development +export VSCODE_DEV=1 +export VSCODE_CLI=1 + +# Run smoke tests +yarn smoketest --web --headless --verbose --electronArgs=$LINUX_EXTRA_ARGS +if [[ $? -ne 0 ]]; then + exit_script "Some smoke test are failing" +fi diff --git a/src/server-cli.js b/src/server-cli.js new file mode 100644 index 0000000000000..a02cc76853865 --- /dev/null +++ b/src/server-cli.js @@ -0,0 +1,8 @@ +/*--------------------------------------------------------------------------------------------- + * Copyright (c) Gitpod. All rights reserved. + *--------------------------------------------------------------------------------------------*/ + +//@ts-check +'use strict'; + +require('./bootstrap-amd').load('vs/server/node/cli'); diff --git a/src/server.js b/src/server.js new file mode 100644 index 0000000000000..5ee2930271951 --- /dev/null +++ b/src/server.js @@ -0,0 +1,12 @@ +/*--------------------------------------------------------------------------------------------- + * Copyright (c) Gitpod. All rights reserved. + *--------------------------------------------------------------------------------------------*/ + +//@ts-check +'use strict'; + +const path = require('path'); +process.env.VSCODE_INJECT_NODE_MODULE_LOOKUP_PATH = path.join(__dirname, '../remote/node_modules'); +require('./bootstrap-node').injectNodeModuleLookupPath(process.env.VSCODE_INJECT_NODE_MODULE_LOOKUP_PATH); +require('./bootstrap-amd').load('vs/server/node/server'); + diff --git a/src/serverUriTransformer.js b/src/serverUriTransformer.js new file mode 100644 index 0000000000000..f0e12e2287f0f --- /dev/null +++ b/src/serverUriTransformer.js @@ -0,0 +1,60 @@ +/*--------------------------------------------------------------------------------------------- + * Copyright (c) Gitpod. All rights reserved. + *--------------------------------------------------------------------------------------------*/ + +//@ts-check +'use strict'; + +/** @type {(remoteAuthority: string) => import('./vs/base/common/uriIpc').IRawURITransformer} */ +module.exports = (remoteAuthority) => { + return { + /** @param {import('./vs/base/common/uri').UriComponents} uri */ + transformIncoming: uri => { + if (uri.scheme === 'vscode-remote') { + if (uri.path.startsWith('/vscode-resource')) { + // webview resources + return { + scheme: 'file', + path: JSON.parse(uri.query).requestResourcePath + }; + } + return { + scheme: 'file', + path: uri.path + }; + } + if (uri.scheme === 'file') { + return { + scheme: 'vscode-local', + path: uri.path + }; + } + return uri; + }, + transformOutgoing: uri => { + if (uri.scheme === 'file') { + return { + scheme: 'vscode-remote', + authority: remoteAuthority, + path: uri.path + }; + } + if (uri.scheme === 'vscode-local') { + return { + scheme: 'file', + path: uri.path + }; + } + return uri; + }, + transformOutgoingScheme: scheme => { + if (scheme === 'file') { + return 'vscode-remote'; + } + if (scheme === 'vscode-local') { + return 'file'; + } + return scheme; + } + }; +}; diff --git a/src/vs/platform/driver/browser/baseDriver.ts b/src/vs/platform/driver/browser/baseDriver.ts index 482cabc24ce97..ce37163e4ad2b 100644 --- a/src/vs/platform/driver/browser/baseDriver.ts +++ b/src/vs/platform/driver/browser/baseDriver.ts @@ -140,8 +140,8 @@ export abstract class BaseWindowDriver implements IWindowDriver { const lines: string[] = []; - for (let i = 0; i < xterm.buffer.length; i++) { - lines.push(xterm.buffer.getLine(i)!.translateToString(true)); + for (let i = 0; i < xterm.buffer.active.length; i++) { + lines.push(xterm.buffer.active.getLine(i)!.translateToString(true)); } return lines; @@ -160,7 +160,7 @@ export abstract class BaseWindowDriver implements IWindowDriver { throw new Error(`Xterm not found: ${selector}`); } - xterm._core._coreService.triggerDataEvent(text); + xterm._core.coreService.triggerDataEvent(text); } getLocaleInfo(): Promise { diff --git a/src/vs/server/browser/workbench/workbench-dev.html b/src/vs/server/browser/workbench/workbench-dev.html new file mode 100644 index 0000000000000..b959ba6cf0ac4 --- /dev/null +++ b/src/vs/server/browser/workbench/workbench-dev.html @@ -0,0 +1,63 @@ + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + diff --git a/src/vs/server/browser/workbench/workbench.html b/src/vs/server/browser/workbench/workbench.html new file mode 100644 index 0000000000000..166fc2c357b16 --- /dev/null +++ b/src/vs/server/browser/workbench/workbench.html @@ -0,0 +1,62 @@ + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + diff --git a/src/vs/server/browser/workbench/workbench.ts b/src/vs/server/browser/workbench/workbench.ts new file mode 100644 index 0000000000000..f74f64e1b2ac7 --- /dev/null +++ b/src/vs/server/browser/workbench/workbench.ts @@ -0,0 +1,499 @@ +/*--------------------------------------------------------------------------------------------- + * Copyright (c) Gitpod. All rights reserved. + *--------------------------------------------------------------------------------------------*/ + +import { isStandalone } from 'vs/base/browser/browser'; +import { streamToBuffer } from 'vs/base/common/buffer'; +import { CancellationToken } from 'vs/base/common/cancellation'; +import { Emitter, Event } from 'vs/base/common/event'; +import { Schemas } from 'vs/base/common/network'; +import { isEqual } from 'vs/base/common/resources'; +import { URI, UriComponents } from 'vs/base/common/uri'; +import { generateUuid } from 'vs/base/common/uuid'; +import { request } from 'vs/base/parts/request/browser/request'; +import { localize } from 'vs/nls'; +import { parseLogLevel } from 'vs/platform/log/common/log'; +import { defaultWebSocketFactory } from 'vs/platform/remote/browser/browserSocketFactory'; +import { isFolderToOpen, isWorkspaceToOpen } from 'vs/platform/windows/common/windows'; +import { create, Disposable, ICredentialsProvider, IHomeIndicator, IProductQualityChangeHandler, IURLCallbackProvider, IWindowIndicator, IWorkbenchConstructionOptions, IWorkspace, IWorkspaceProvider } from 'vs/workbench/workbench.web.api'; + +function doCreateUri(path: string, queryValues: Map): URI { + let query: string | undefined = undefined; + + if (queryValues) { + let index = 0; + queryValues.forEach((value, key) => { + if (!query) { + query = ''; + } + + const prefix = (index++ === 0) ? '' : '&'; + query += `${prefix}${key}=${encodeURIComponent(value)}`; + }); + } + + return URI.parse(window.location.href).with({ path, query }); +} + +interface ICredential { + service: string; + account: string; + password: string; +} + +class LocalStorageCredentialsProvider implements ICredentialsProvider { + + static readonly CREDENTIALS_OPENED_KEY = 'credentials.provider'; + + private readonly authService: string | undefined; + + private _credentials: ICredential[] | undefined; + private get credentials(): ICredential[] { + if (!this._credentials) { + try { + const serializedCredentials = window.localStorage.getItem(LocalStorageCredentialsProvider.CREDENTIALS_OPENED_KEY); + if (serializedCredentials) { + this._credentials = JSON.parse(serializedCredentials); + } + } catch (error) { + // ignore + } + + if (!Array.isArray(this._credentials)) { + this._credentials = []; + } + } + + return this._credentials; + } + + private save(): void { + window.localStorage.setItem(LocalStorageCredentialsProvider.CREDENTIALS_OPENED_KEY, JSON.stringify(this.credentials)); + } + + async getPassword(service: string, account: string): Promise { + return this.doGetPassword(service, account); + } + + private async doGetPassword(service: string, account?: string): Promise { + for (const credential of this.credentials) { + if (credential.service === service) { + if (typeof account !== 'string' || account === credential.account) { + return credential.password; + } + } + } + + return null; + } + + async setPassword(service: string, account: string, password: string): Promise { + this.doDeletePassword(service, account); + + this.credentials.push({ service, account, password }); + + this.save(); + + try { + if (password && service === this.authService) { + const value = JSON.parse(password); + if (Array.isArray(value) && value.length === 0) { + await this.logout(service); + } + } + } catch (error) { + console.log(error); + } + } + + async deletePassword(service: string, account: string): Promise { + const result = await this.doDeletePassword(service, account); + + if (result && service === this.authService) { + try { + await this.logout(service); + } catch (error) { + console.log(error); + } + } + + return result; + } + + private async doDeletePassword(service: string, account: string): Promise { + let found = false; + + this._credentials = this.credentials.filter(credential => { + if (credential.service === service && credential.account === account) { + found = true; + + return false; + } + + return true; + }); + + if (found) { + this.save(); + } + + return found; + } + + async findPassword(service: string): Promise { + return this.doGetPassword(service); + } + + async findCredentials(service: string): Promise> { + return this.credentials + .filter(credential => credential.service === service) + .map(({ account, password }) => ({ account, password })); + } + + private async logout(service: string): Promise { + const queryValues: Map = new Map(); + queryValues.set('logout', String(true)); + queryValues.set('service', service); + + await request({ + url: doCreateUri('/auth/logout', queryValues).toString(true) + }, CancellationToken.None); + } +} + +class PollingURLCallbackProvider extends Disposable implements IURLCallbackProvider { + + static readonly FETCH_INTERVAL = 500; // fetch every 500ms + static readonly FETCH_TIMEOUT = 5 * 60 * 1000; // ...but stop after 5min + + static readonly QUERY_KEYS = { + REQUEST_ID: 'vscode-requestId', + SCHEME: 'vscode-scheme', + AUTHORITY: 'vscode-authority', + PATH: 'vscode-path', + QUERY: 'vscode-query', + FRAGMENT: 'vscode-fragment' + }; + + private readonly _onCallback = this._register(new Emitter()); + readonly onCallback = this._onCallback.event; + + create(options?: Partial): URI { + const queryValues: Map = new Map(); + + const requestId = generateUuid(); + queryValues.set(PollingURLCallbackProvider.QUERY_KEYS.REQUEST_ID, requestId); + + const { scheme, authority, path, query, fragment } = options ? options : { scheme: undefined, authority: undefined, path: undefined, query: undefined, fragment: undefined }; + + if (scheme) { + queryValues.set(PollingURLCallbackProvider.QUERY_KEYS.SCHEME, scheme); + } + + if (authority) { + queryValues.set(PollingURLCallbackProvider.QUERY_KEYS.AUTHORITY, authority); + } + + if (path) { + queryValues.set(PollingURLCallbackProvider.QUERY_KEYS.PATH, path); + } + + if (query) { + queryValues.set(PollingURLCallbackProvider.QUERY_KEYS.QUERY, query); + } + + if (fragment) { + queryValues.set(PollingURLCallbackProvider.QUERY_KEYS.FRAGMENT, fragment); + } + + // Start to poll on the callback being fired + this.periodicFetchCallback(requestId, Date.now()); + + return doCreateUri('/callback', queryValues); + } + + private async periodicFetchCallback(requestId: string, startTime: number): Promise { + + // Ask server for callback results + const queryValues: Map = new Map(); + queryValues.set(PollingURLCallbackProvider.QUERY_KEYS.REQUEST_ID, requestId); + + const result = await request({ + url: doCreateUri('/fetch-callback', queryValues).toString(true) + }, CancellationToken.None); + + // Check for callback results + const content = await streamToBuffer(result.stream); + if (content.byteLength > 0) { + try { + this._onCallback.fire(URI.revive(JSON.parse(content.toString()))); + } catch (error) { + console.error(error); + } + + return; // done + } + + // Continue fetching unless we hit the timeout + if (Date.now() - startTime < PollingURLCallbackProvider.FETCH_TIMEOUT) { + setTimeout(() => this.periodicFetchCallback(requestId, startTime), PollingURLCallbackProvider.FETCH_INTERVAL); + } + } +} + +class WorkspaceProvider implements IWorkspaceProvider { + + static QUERY_PARAM_EMPTY_WINDOW = 'ew'; + static QUERY_PARAM_FOLDER = 'folder'; + static QUERY_PARAM_WORKSPACE = 'workspace'; + + static QUERY_PARAM_PAYLOAD = 'payload'; + + readonly trusted = true; + + constructor( + readonly workspace: IWorkspace, + readonly payload: object + ) { } + + async open(workspace: IWorkspace, options?: { reuse?: boolean, payload?: object }): Promise { + if (options?.reuse && !options.payload && this.isSame(this.workspace, workspace)) { + return true; // return early if workspace and environment is not changing and we are reusing window + } + + const targetHref = this.createTargetUrl(workspace, options); + if (targetHref) { + if (options?.reuse) { + window.location.href = targetHref; + return true; + } else { + let result; + if (isStandalone) { + result = window.open(targetHref, '_blank', 'toolbar=no'); // ensures to open another 'standalone' window! + } else { + result = window.open(targetHref); + } + + return !!result; + } + } + return false; + } + + private createTargetUrl(workspace: IWorkspace, options?: { reuse?: boolean, payload?: object }): string | undefined { + + // Empty + let targetHref: string | undefined = undefined; + if (!workspace) { + targetHref = `${document.location.origin}${document.location.pathname}?${WorkspaceProvider.QUERY_PARAM_EMPTY_WINDOW}=true`; + } + + // Folder + else if (isFolderToOpen(workspace)) { + targetHref = `${document.location.origin}${document.location.pathname}?${WorkspaceProvider.QUERY_PARAM_FOLDER}=${encodeURIComponent(workspace.folderUri.toString())}`; + } + + // Workspace + else if (isWorkspaceToOpen(workspace)) { + targetHref = `${document.location.origin}${document.location.pathname}?${WorkspaceProvider.QUERY_PARAM_WORKSPACE}=${encodeURIComponent(workspace.workspaceUri.toString())}`; + } + + // Append payload if any + if (options?.payload) { + targetHref += `&${WorkspaceProvider.QUERY_PARAM_PAYLOAD}=${encodeURIComponent(JSON.stringify(options.payload))}`; + } + + return targetHref; + } + + private isSame(workspaceA: IWorkspace, workspaceB: IWorkspace): boolean { + if (!workspaceA || !workspaceB) { + return workspaceA === workspaceB; // both empty + } + + if (isFolderToOpen(workspaceA) && isFolderToOpen(workspaceB)) { + return isEqual(workspaceA.folderUri, workspaceB.folderUri); // same workspace + } + + if (isWorkspaceToOpen(workspaceA) && isWorkspaceToOpen(workspaceB)) { + return isEqual(workspaceA.workspaceUri, workspaceB.workspaceUri); // same workspace + } + + return false; + } + + hasRemote(): boolean { + if (this.workspace) { + if (isFolderToOpen(this.workspace)) { + return this.workspace.folderUri.scheme === Schemas.vscodeRemote; + } + + if (isWorkspaceToOpen(this.workspace)) { + return this.workspace.workspaceUri.scheme === Schemas.vscodeRemote; + } + } + + return true; + } +} + +class WindowIndicator implements IWindowIndicator { + + readonly onDidChange = Event.None; + + readonly label: string; + readonly tooltip: string; + readonly command: string | undefined; + + constructor(workspace: IWorkspace) { + let repositoryOwner: string | undefined = undefined; + let repositoryName: string | undefined = undefined; + + if (workspace) { + let uri: URI | undefined = undefined; + if (isFolderToOpen(workspace)) { + uri = workspace.folderUri; + } else if (isWorkspaceToOpen(workspace)) { + uri = workspace.workspaceUri; + } + + if (uri?.scheme === 'github' || uri?.scheme === 'codespace') { + [repositoryOwner, repositoryName] = uri.authority.split('+'); + } + } + + // Repo + if (repositoryName && repositoryOwner) { + this.label = localize('playgroundLabelRepository', "$(remote) VS Code Web Playground: {0}/{1}", repositoryOwner, repositoryName); + this.tooltip = localize('playgroundRepositoryTooltip', "VS Code Web Playground: {0}/{1}", repositoryOwner, repositoryName); + } + + // No Repo + else { + this.label = localize('playgroundLabel', "$(remote) VS Code Web Playground"); + this.tooltip = localize('playgroundTooltip', "VS Code Web Playground"); + } + } +} + + +(function () { + // Find config by checking for DOM + const configElement = document.getElementById('vscode-workbench-web-configuration'); + const configElementAttribute = configElement ? configElement.getAttribute('data-settings') : undefined; + const config: IWorkbenchConstructionOptions = configElementAttribute ? JSON.parse(configElementAttribute) : {}; + + // Find workspace to open and payload + let workspace: IWorkspace; + let payload = Object.create(null); + let logLevel: string | undefined = undefined; + + const query = new URL(document.location.href).searchParams; + query.forEach((value, key) => { + switch (key) { + + // Folder + case WorkspaceProvider.QUERY_PARAM_FOLDER: + workspace = { folderUri: URI.parse(value) }; + break; + + // Workspace + case WorkspaceProvider.QUERY_PARAM_WORKSPACE: + workspace = { workspaceUri: URI.parse(value) }; + break; + + // Empty + case WorkspaceProvider.QUERY_PARAM_EMPTY_WINDOW: + workspace = undefined; + break; + + // Payload + case WorkspaceProvider.QUERY_PARAM_PAYLOAD: + try { + payload = JSON.parse(value); + } catch (error) { + console.error(error); // possible invalid JSON + } + break; + + // Log level + case 'logLevel': + logLevel = value; + break; + } + }); + + // Workspace Provider + const workspaceProvider = new WorkspaceProvider(workspace, payload); + + // Home Indicator + const homeIndicator: IHomeIndicator = { + href: 'https://github.com/microsoft/vscode', + icon: 'code', + title: localize('home', "Home") + }; + + // Window indicator (unless connected to a remote) + let windowIndicator: WindowIndicator | undefined = undefined; + if (!workspaceProvider.hasRemote()) { + windowIndicator = new WindowIndicator(workspace); + } + + // Product Quality Change Handler + const productQualityChangeHandler: IProductQualityChangeHandler = (quality) => { + let queryString = `quality=${quality}`; + + // Save all other query params we might have + const query = new URL(document.location.href).searchParams; + query.forEach((value, key) => { + if (key !== 'quality') { + queryString += `&${key}=${value}`; + } + }); + + window.location.href = `${window.location.origin}?${queryString}`; + }; + + // Finally create workbench + const remoteAuthority = window.location.host; + // TODO(ak) secure by using external endpoint + const webviewEndpoint = new URL(window.location.href); + webviewEndpoint.pathname = '/out/vs/workbench/contrib/webview/browser/pre/'; + webviewEndpoint.search = ''; + + // TODO(ak) secure by using external endpoint + const webWorkerExtensionEndpoint = new URL(window.location.href); + webWorkerExtensionEndpoint.pathname = `/out/vs/workbench/services/extensions/worker/${window.location.protocol === 'https:' ? 'https' : 'http'}WebWorkerExtensionHostIframe.html`; + webWorkerExtensionEndpoint.search = ''; + + create(document.body, { + webviewEndpoint: webviewEndpoint.href, + webWorkerExtensionHostIframeSrc: webWorkerExtensionEndpoint.href, + remoteAuthority, + webSocketFactory: { + create: url => { + const codeServerUrl = new URL(url); + codeServerUrl.protocol = location.protocol === 'https:' ? 'wss:' : 'ws:'; + return defaultWebSocketFactory.create(codeServerUrl.toString()); + } + }, + resourceUriProvider: uri => { + return URI.from({ + scheme: location.protocol === 'https:' ? 'https' : 'http', + authority: remoteAuthority, + path: `/vscode-remote-resource`, + query: `path=${encodeURIComponent(uri.path)}` + }); + }, + developmentOptions: { + logLevel: logLevel ? parseLogLevel(logLevel) : undefined, + ...config.developmentOptions + }, + homeIndicator, + windowIndicator, + productQualityChangeHandler, + workspaceProvider, + urlCallbackProvider: new PollingURLCallbackProvider(), + credentialsProvider: new LocalStorageCredentialsProvider() + }); +})(); diff --git a/src/vs/server/node/cli.main.ts b/src/vs/server/node/cli.main.ts new file mode 100644 index 0000000000000..f270dfc96cb74 --- /dev/null +++ b/src/vs/server/node/cli.main.ts @@ -0,0 +1,214 @@ +/*--------------------------------------------------------------------------------------------- + * Copyright (c) Gitpod. All rights reserved. + *--------------------------------------------------------------------------------------------*/ + +import * as assert from 'assert'; +import * as fs from 'fs'; +import type * as http from 'http'; +import * as path from 'path'; +import { URI } from 'vs/base/common/uri'; +import { whenDeleted } from 'vs/base/node/pfs'; +import { localize } from 'vs/nls'; +import { NativeParsedArgs } from 'vs/platform/environment/common/argv'; +import { buildHelpMessage, buildVersionMessage, ErrorReporter, OptionDescriptions, OPTIONS as ALL_OPTIONS, parseArgs } from 'vs/platform/environment/node/argv'; +import { createWaitMarkerFile } from 'vs/platform/environment/node/wait'; +import product from 'vs/platform/product/common/product'; +import type { PipeCommand } from 'vs/workbench/api/node/extHostCLIServer'; + +const OPTIONS_KEYS: (keyof typeof ALL_OPTIONS)[] = [ + 'help', + + 'diff', + 'add', + 'goto', + 'new-window', + 'reuse-window', + 'folder-uri', + 'file-uri', + 'wait', + + 'list-extensions', + 'show-versions', + 'category', + 'install-extension', + 'uninstall-extension', + 'force', + + 'version', + 'status', + 'verbose' +]; +export interface ServerNativeParsedArgs extends NativeParsedArgs { + 'openExternal'?: string[] +} + +export interface ServerCliOptions { + createRequestOptions(): http.RequestOptions + parseArgs?(args: string[], errorReporter?: ErrorReporter): T + handleArgs?(args: T): Promise +} + +async function doMain(processArgv: string[], options: ServerCliOptions): Promise { + + let args: T; + + try { + const errorReporter: ErrorReporter = { + onUnknownOption: (id) => { + console.warn(localize('unknownOption', "Warning: '{0}' is not in the list of known options.", id)); + }, + onMultipleValues: (id, val) => { + console.warn(localize('multipleValues', "Option '{0}' is defined more than once. Using value '{1}.'", id, val)); + } + }; + + args = options.parseArgs ? options.parseArgs(processArgv.slice(2), errorReporter) : parseArgs(processArgv.slice(2), OPTIONS, errorReporter) as T; + if (args.goto) { + args._.forEach(arg => assert(/^(\w:)?[^:]+(:\d*){0,2}$/.test(arg), localize('gotoValidation', "Arguments in `--goto` mode should be in the format of `FILE(:LINE(:CHARACTER))`."))); + } + } catch (err) { + console.error(err.message); + return; + } + + // Help + if (args.help) { + const executable = `${product.applicationName}`; + console.log(buildHelpMessage(product.nameLong, executable, product.version, OPTIONS)); + } + + // Version Info + else if (args.version) { + console.log(buildVersionMessage(product.version, product.commit)); + } + + // Status + else if (args.status) { + console.log(await sendCommand(options.createRequestOptions(), { + type: 'status' + })); + } + + // open external URIs + else if (args['openExternal']) { + await sendCommand(options.createRequestOptions(), { + type: 'openExternal', + uris: args['openExternal'] + }); + } + + else if (options.handleArgs && await options.handleArgs(args)) { + return; + } + + // Extensionst Management + else if (args['list-extensions'] || args['install-extension'] || args['uninstall-extension']) { + console.log(await sendCommand(options.createRequestOptions(), { + type: 'extensionManagement', + list: args['list-extensions'] ? { + category: args.category, + showVersions: args['show-versions'] + } : undefined, + install: args['install-extension'], + uninstall: args['uninstall-extension'], + force: args['force'] + })); + } + + // Just Code + else { + const waitMarkerFilePath = args.wait ? createWaitMarkerFile(args.verbose) : undefined; + const fileURIs: string[] = [...args['file-uri'] || []]; + const folderURIs: string[] = [...args['folder-uri'] || []]; + const pendingFiles: Promise[] = []; + for (const arg of args._) { + if (arg === '-') { + // don't support reading from stdin yet + continue; + } + const filePath = path.resolve(process.cwd(), arg); + pendingFiles.push(fs.promises.stat(filePath).then(stat => { + const uris = stat.isFile() ? fileURIs : folderURIs; + uris.push(URI.parse(filePath).toString()); + }, e => { + if (e.code === 'ENOENT') { + // open a new file + fileURIs.push(URI.parse(filePath).toString()); + } else { + console.log(`failed to resolve '${filePath}' path:`, e); + } + })); + } + await Promise.all(pendingFiles); + await sendCommand(options.createRequestOptions(), { + type: 'open', + fileURIs, + folderURIs, + forceNewWindow: args['new-window'], + diffMode: args.diff, + addMode: args.add, + gotoLineMode: args.goto, + forceReuseWindow: args['reuse-window'], + waitMarkerFilePath + }); + if (waitMarkerFilePath) { + // Complete when wait marker file is deleted + await whenDeleted(waitMarkerFilePath); + } + } +} + +export async function sendCommand(options: http.RequestOptions, command: PipeCommand): Promise { + const http = await import('http'); + while (true) { + try { + return await new Promise((resolve, reject) => { + const req = http.request(options, res => { + const chunks: string[] = []; + res.setEncoding('utf8'); + res.on('data', d => chunks.push(d)); + res.on('end', () => { + const result = chunks.join(''); + if (res.statusCode !== 200) { + reject(new Error(`Bad status code: ${res.statusCode}: ${result}`)); + } else { + resolve(result); + } + }); + }); + req.on('error', err => reject(err)); + req.write(JSON.stringify(command)); + req.end(); + }); + } catch (e) { + // Code Server is not running yet, let's try again + if (e.code !== 'ECONNREFUSED') { + throw e; + } + await new Promise(resolve => setTimeout(resolve, 500)); + } + } +} + +export const OPTIONS: OptionDescriptions = { + _: ALL_OPTIONS['_'], + 'openExternal': { + type: 'string[]' + } +}; +for (const key of OPTIONS_KEYS) { + Object.assign(OPTIONS, { [key]: ALL_OPTIONS[key] }); +} + +function eventuallyExit(code: number): void { + setTimeout(() => process.exit(code), 0); +} + +export function main(processArgv: string[], options: ServerCliOptions): void { + doMain(processArgv, options) + .then(() => eventuallyExit(0)) + .then(null, err => { + console.error(err.message || err.stack || err); + eventuallyExit(1); + }); +} diff --git a/src/vs/server/node/cli.ts b/src/vs/server/node/cli.ts new file mode 100644 index 0000000000000..371fb1e05bdbd --- /dev/null +++ b/src/vs/server/node/cli.ts @@ -0,0 +1,21 @@ +/*--------------------------------------------------------------------------------------------- + * Copyright (c) Gitpod. All rights reserved. + *--------------------------------------------------------------------------------------------*/ + +import { main } from 'vs/server/node/cli.main'; + +main(process.argv, { + createRequestOptions: () => { + const ipcHandlePath = process.env['VSCODE_IPC_HOOK_CLI']; + + if (!ipcHandlePath) { + throw new Error('Missing VSCODE_IPC_HOOK_CLI'); + } + return { + socketPath: ipcHandlePath, + method: 'POST' + }; + } +}); + + diff --git a/src/vs/server/node/remote-terminal.ts b/src/vs/server/node/remote-terminal.ts new file mode 100644 index 0000000000000..a81b82beb2f79 --- /dev/null +++ b/src/vs/server/node/remote-terminal.ts @@ -0,0 +1,335 @@ +/*--------------------------------------------------------------------------------------------- + * Copyright (c) Gitpod. All rights reserved. + *--------------------------------------------------------------------------------------------*/ + +import * as os from 'os'; +import * as path from 'path'; +import { CancellationToken } from 'vs/base/common/cancellation'; +import { URI } from 'vs/base/common/uri'; +import { Emitter, Event } from 'vs/base/common/event'; +import { IConfigurationService } from 'vs/platform/configuration/common/configuration'; +import { ILogService } from 'vs/platform/log/common/log'; +import product from 'vs/platform/product/common/product'; +import { RemoteAgentConnectionContext } from 'vs/platform/remote/common/remoteAgentEnvironment'; +import { ITelemetryService } from 'vs/platform/telemetry/common/telemetry'; +import { IPtyService, IReconnectConstants, IShellLaunchConfig, LocalReconnectConstants, TerminalSettingId } from 'vs/platform/terminal/common/terminal'; +import { PtyHostService } from 'vs/platform/terminal/node/ptyHostService'; +import { ICreateTerminalProcessArguments, ICreateTerminalProcessResult, REMOTE_TERMINAL_CHANNEL_NAME } from 'vs/workbench/contrib/terminal/common/remoteTerminalChannel'; +import * as platform from 'vs/base/common/platform'; +import { IWorkspaceFolderData } from 'vs/platform/terminal/common/terminalProcess'; +import { IWorkspaceFolder } from 'vs/platform/workspace/common/workspace'; +import { createTerminalEnvironment, createVariableResolver, getCwd, getDefaultShell, getDefaultShellArgs } from 'vs/workbench/contrib/terminal/common/terminalEnvironment'; +import { deserializeEnvironmentVariableCollection } from 'vs/workbench/contrib/terminal/common/environmentVariableShared'; +import { MergedEnvironmentVariableCollection } from 'vs/workbench/contrib/terminal/common/environmentVariableCollection'; +import { IEnvironmentVariableCollection } from 'vs/workbench/contrib/terminal/common/environmentVariable'; +import { getSystemShellSync } from 'vs/base/node/shell'; +import { ServicesAccessor } from 'vs/platform/instantiation/common/instantiation'; +import { IPCServer, IServerChannel } from 'vs/base/parts/ipc/common/ipc'; +import { AbstractVariableResolverService } from 'vs/workbench/services/configurationResolver/common/variableResolver'; +import { TernarySearchTree } from 'vs/base/common/map'; +import { CLIServerBase } from 'vs/workbench/api/node/extHostCLIServer'; +import { createRandomIPCHandle } from 'vs/base/parts/ipc/node/ipc.net'; +import { IRawURITransformerFactory } from 'vs/server/node/server.main'; +import { IURITransformer, transformIncomingURIs, URITransformer } from 'vs/base/common/uriIpc'; +import { cloneAndChange } from 'vs/base/common/objects'; +import { INativeEnvironmentService } from 'vs/platform/environment/common/environment'; + +export function registerRemoteTerminal(services: ServicesAccessor, channelServer: IPCServer) { + const configurationService = services.get(IConfigurationService); + const logService = services.get(ILogService); + const environmentService = services.get(INativeEnvironmentService); + const telemetryService = services.get(ITelemetryService); + const rawURITransformerFactory = services.get(IRawURITransformerFactory); + + const reconnectConstants: IReconnectConstants = { + graceTime: LocalReconnectConstants.GraceTime, + shortGraceTime: LocalReconnectConstants.ShortGraceTime, + scrollback: configurationService.getValue(TerminalSettingId.PersistentSessionScrollback) ?? 100 + }; + const ptyHostService = new PtyHostService(reconnectConstants, configurationService, environmentService, logService, telemetryService); + channelServer.registerChannel(REMOTE_TERMINAL_CHANNEL_NAME, new RemoteTerminalChannelServer(rawURITransformerFactory, logService, ptyHostService)); +} + +function toWorkspaceFolder(data: IWorkspaceFolderData): IWorkspaceFolder { + return { + uri: URI.revive(data.uri), + name: data.name, + index: data.index, + toResource: () => { + throw new Error('Not implemented'); + } + }; +} + +export class RemoteTerminalChannelServer implements IServerChannel { + + private _lastRequestId = 0; + private _pendingRequests = new Map void, reject: (error: any) => void, uriTransformer: IURITransformer }>(); + + private readonly _onExecuteCommand = new Emitter<{ reqId: number, commandId: string, commandArgs: any[] }>(); + readonly onExecuteCommand = this._onExecuteCommand.event; + + constructor( + private readonly rawURITransformerFactory: IRawURITransformerFactory, + private readonly logService: ILogService, + private readonly ptyService: IPtyService, + ) { + } + + public async call(context: RemoteAgentConnectionContext, command: string, args: any, cancellationToken?: CancellationToken | undefined): Promise { + if (command === '$createProcess') { + return this.createProcess(context.remoteAuthority, args); + } + + if (command === '$sendCommandResult') { + return this.sendCommandResult(args[0], args[1], args[2]); + } + + // Generic method handling for all other commands + const serviceRecord = this.ptyService as unknown as Record Promise>; + const serviceFunc = serviceRecord[command.substring(1)]; + if (!serviceFunc) { + this.logService.error('Unknown command: ' + command); + return; + } + + if (Array.isArray(args)) { + return serviceFunc.call(this.ptyService, ...args); + } else { + return serviceFunc.call(this.ptyService, args); + } + } + + public listen(context: RemoteAgentConnectionContext, event: string, args: any): Event { + if (event === '$onExecuteCommand') { + return this._onExecuteCommand.event; + } + + const serviceRecord = this.ptyService as unknown as Record>; + const result = serviceRecord[event.substring(1, event.endsWith('Event') ? event.length - 'Event'.length : undefined)]; + if (!result) { + this.logService.error('Unknown event: ' + event); + return Event.None; + } + return result; + } + + private executeCommand(uriTransformer: IURITransformer, id: string, args: any[]): Promise { + let resolve: (data: any) => void, reject: (error: any) => void; + const promise = new Promise((c, e) => { resolve = c; reject = e; }); + + const reqId = ++this._lastRequestId; + this._pendingRequests.set(reqId, { resolve: resolve!, reject: reject!, uriTransformer }); + + const commandArgs = cloneAndChange(args, value => { + if (value instanceof URI) { + return uriTransformer.transformOutgoingURI(value); + } + return; + }); + this._onExecuteCommand.fire({ reqId, commandId: id, commandArgs }); + + return promise; + } + + private async sendCommandResult(reqId: number, isError: boolean, payload: any): Promise { + const reqData = this._pendingRequests.get(reqId); + if (!reqData) { + return; + } + + this._pendingRequests.delete(reqId); + + const result = transformIncomingURIs(payload, reqData.uriTransformer); + if (isError) { + reqData.reject(result); + } else { + reqData.resolve(result); + } + } + + private async createProcess(remoteAuthority: string, args: ICreateTerminalProcessArguments): Promise { + const uriTransformer = new URITransformer(this.rawURITransformerFactory(remoteAuthority)); + + const shellLaunchConfigDto = args.shellLaunchConfig; + // See $spawnExtHostProcess in src/vs/workbench/api/node/extHostTerminalService.ts for a reference implementation + const shellLaunchConfig: IShellLaunchConfig = { + name: shellLaunchConfigDto.name, + executable: shellLaunchConfigDto.executable, + args: shellLaunchConfigDto.args, + cwd: typeof shellLaunchConfigDto.cwd === 'string' ? shellLaunchConfigDto.cwd : URI.revive(shellLaunchConfigDto.cwd), + env: shellLaunchConfigDto.env + }; + + let lastActiveWorkspace: IWorkspaceFolder | undefined; + if (args.activeWorkspaceFolder) { + lastActiveWorkspace = toWorkspaceFolder(args.activeWorkspaceFolder); + } + + const processEnv = { ...process.env, ...args.resolverEnv } as platform.IProcessEnvironment; + const configurationResolverService = new RemoteTerminalVariableResolverService( + args.workspaceFolders.map(toWorkspaceFolder), + args.resolvedVariables, + args.activeFileResource ? URI.revive(args.activeFileResource) : undefined, + processEnv + ); + const variableResolver = createVariableResolver(lastActiveWorkspace, processEnv, configurationResolverService); + + // Merge in shell and args from settings + if (!shellLaunchConfig.executable) { + shellLaunchConfig.executable = getDefaultShell( + key => args.configuration[key], + getSystemShellSync(platform.OS, process.env as platform.IProcessEnvironment), + process.env.hasOwnProperty('PROCESSOR_ARCHITEW6432'), + process.env.windir, + variableResolver, + this.logService, + false + ); + shellLaunchConfig.args = getDefaultShellArgs( + key => args.configuration[key], + false, + variableResolver, + this.logService + ); + } else if (variableResolver) { + shellLaunchConfig.executable = variableResolver(shellLaunchConfig.executable); + if (shellLaunchConfig.args) { + if (Array.isArray(shellLaunchConfig.args)) { + const resolvedArgs: string[] = []; + for (const arg of shellLaunchConfig.args) { + resolvedArgs.push(variableResolver(arg)); + } + shellLaunchConfig.args = resolvedArgs; + } else { + shellLaunchConfig.args = variableResolver(shellLaunchConfig.args); + } + } + } + + // Get the initial cwd + const initialCwd = getCwd( + shellLaunchConfig, + os.homedir(), + variableResolver, + lastActiveWorkspace?.uri, + args.configuration['terminal.integrated.cwd'], + this.logService + ); + shellLaunchConfig.cwd = initialCwd; + + const env = createTerminalEnvironment( + shellLaunchConfig, + args.configuration['terminal.integrated.env.linux'], + variableResolver, + product.version, + args.configuration['terminal.integrated.detectLocale'] || 'auto', + processEnv + ); + + // Apply extension environment variable collections to the environment + if (!shellLaunchConfig.strictEnv) { + const collection = new Map(); + for (const [name, serialized] of args.envVariableCollections) { + collection.set(name, { + map: deserializeEnvironmentVariableCollection(serialized) + }); + } + const mergedCollection = new MergedEnvironmentVariableCollection(collection); + mergedCollection.applyToProcessEnvironment(env, variableResolver); + } + + const ipcHandle = createRandomIPCHandle(); + env['VSCODE_IPC_HOOK_CLI'] = ipcHandle; + const cliServer = new CLIServerBase( + { + executeCommand: (id, ...args) => this.executeCommand(uriTransformer, id, args) + }, + this.logService, + ipcHandle + ); + + const persistentTerminalId = await this.ptyService.createProcess( + shellLaunchConfig, + initialCwd, + args.cols, + args.rows, + args.unicodeVersion, + env, + processEnv, + false, + args.shouldPersistTerminal, + args.workspaceId, + args.workspaceName + ); + this.ptyService.onProcessExit(e => { + if (e.id === persistentTerminalId) { + cliServer.dispose(); + } + }); + + return { + persistentTerminalId, + resolvedShellLaunchConfig: shellLaunchConfig + }; + } +} + +/** + * See ExtHostVariableResolverService in src/vs/workbench/api/common/extHostDebugService.ts for a reference implementation. + */ +class RemoteTerminalVariableResolverService extends AbstractVariableResolverService { + + private readonly structure = TernarySearchTree.forUris(() => false); + + constructor(folders: IWorkspaceFolder[], resolvedVariables: { [name: string]: string }, activeFileResource: URI | undefined, env: platform.IProcessEnvironment) { + super({ + getFolderUri: (folderName: string): URI | undefined => { + const found = folders.filter(f => f.name === folderName); + if (found && found.length > 0) { + return found[0].uri; + } + return undefined; + }, + getWorkspaceFolderCount: (): number => { + return folders.length; + }, + getConfigurationValue: (folderUri: URI | undefined, section: string): string | undefined => { + return resolvedVariables['config:' + section]; + }, + getAppRoot: (): string | undefined => { + return env['VSCODE_CWD'] || process.cwd(); + }, + getExecPath: (): string | undefined => { + return env['VSCODE_EXEC_PATH']; + }, + getFilePath: (): string | undefined => { + if (activeFileResource) { + return path.normalize(activeFileResource.fsPath); + } + return undefined; + }, + getWorkspaceFolderPathForFile: (): string | undefined => { + if (activeFileResource) { + const ws = this.structure.findSubstr(activeFileResource); + if (ws) { + return path.normalize(ws.uri.fsPath); + } + } + return undefined; + }, + getSelectedText: (): string | undefined => { + return resolvedVariables.selectedText; + }, + getLineNumber: (): string | undefined => { + return resolvedVariables.lineNumber; + } + }, undefined, Promise.resolve(env)); + + // Set up the workspace folder data structure + folders.forEach(folder => { + this.structure.set(folder.uri, folder); + }); + } + +} diff --git a/src/vs/server/node/server.main.ts b/src/vs/server/node/server.main.ts new file mode 100644 index 0000000000000..7ad12adcd8333 --- /dev/null +++ b/src/vs/server/node/server.main.ts @@ -0,0 +1,1025 @@ +/*--------------------------------------------------------------------------------------------- + * Copyright (c) Gitpod. All rights reserved. + *--------------------------------------------------------------------------------------------*/ + +import * as cp from 'child_process'; +import * as crypto from 'crypto'; +import * as fs from 'fs'; +import * as http from 'http'; +import * as net from 'net'; +import * as os from 'os'; +import * as path from 'path'; +import * as url from 'url'; +import { RunOnceScheduler } from 'vs/base/common/async'; +import { VSBuffer } from 'vs/base/common/buffer'; +import { CancellationToken, CancellationTokenSource } from 'vs/base/common/cancellation'; +import { IRemoteConsoleLog } from 'vs/base/common/console'; +import { isPromiseCanceledError, onUnexpectedError, setUnexpectedErrorHandler } from 'vs/base/common/errors'; +import { Emitter, Event } from 'vs/base/common/event'; +import { IDisposable } from 'vs/base/common/lifecycle'; +import { FileAccess, Schemas } from 'vs/base/common/network'; +import { dirname, join } from 'vs/base/common/path'; +import * as platform from 'vs/base/common/platform'; +import Severity from 'vs/base/common/severity'; +import { ReadableStreamEventPayload } from 'vs/base/common/stream'; +import { URI } from 'vs/base/common/uri'; +import { IRawURITransformer, transformIncomingURIs, transformOutgoingURIs, URITransformer } from 'vs/base/common/uriIpc'; +import { generateUuid } from 'vs/base/common/uuid'; +import { ClientConnectionEvent, IPCServer, IServerChannel } from 'vs/base/parts/ipc/common/ipc'; +import { PersistentProtocol, ProtocolConstants } from 'vs/base/parts/ipc/common/ipc.net'; +import { NodeSocket, WebSocketNodeSocket } from 'vs/base/parts/ipc/node/ipc.net'; +import { IConfigurationService } from 'vs/platform/configuration/common/configuration'; +import { ConfigurationService } from 'vs/platform/configuration/common/configurationService'; +import { ExtensionHostDebugBroadcastChannel } from 'vs/platform/debug/common/extensionHostDebugIpc'; +import { IDownloadService } from 'vs/platform/download/common/download'; +import { DownloadService } from 'vs/platform/download/common/downloadService'; +import { NativeParsedArgs } from 'vs/platform/environment/common/argv'; +import { IEnvironmentService, INativeEnvironmentService } from 'vs/platform/environment/common/environment'; +import { OptionDescriptions, OPTIONS, parseArgs } from 'vs/platform/environment/node/argv'; +import { NativeEnvironmentService } from 'vs/platform/environment/node/environmentService'; +import { ExtensionGalleryServiceWithNoStorageService } from 'vs/platform/extensionManagement/common/extensionGalleryService'; +import { IExtensionGalleryService, IExtensionManagementService } from 'vs/platform/extensionManagement/common/extensionManagement'; +import { ExtensionManagementChannel } from 'vs/platform/extensionManagement/common/extensionManagementIpc'; +import { ExtensionManagementService } from 'vs/platform/extensionManagement/node/extensionManagementService'; +import { ExtensionIdentifier, IExtensionDescription } from 'vs/platform/extensions/common/extensions'; +import { IFileService } from 'vs/platform/files/common/files'; +import { FileService } from 'vs/platform/files/common/fileService'; +import { DiskFileSystemProvider } from 'vs/platform/files/node/diskFileSystemProvider'; +import { SyncDescriptor } from 'vs/platform/instantiation/common/descriptors'; +import { createDecorator, ServicesAccessor } from 'vs/platform/instantiation/common/instantiation'; +import { InstantiationService } from 'vs/platform/instantiation/common/instantiationService'; +import { ServiceCollection } from 'vs/platform/instantiation/common/serviceCollection'; +import { BufferLogService } from 'vs/platform/log/common/bufferLog'; +import { ConsoleMainLogger, getLogLevel, ILogService, MultiplexLogService } from 'vs/platform/log/common/log'; +import { LogLevelChannel } from 'vs/platform/log/common/logIpc'; +import { SpdLogLogger } from 'vs/platform/log/node/spdlogLog'; +import product from 'vs/platform/product/common/product'; +import { IProductService } from 'vs/platform/product/common/productService'; +import { ConnectionType, ErrorMessage, HandshakeMessage, IRemoteExtensionHostStartParams, OKMessage, SignRequest } from 'vs/platform/remote/common/remoteAgentConnection'; +import { RemoteAgentConnectionContext } from 'vs/platform/remote/common/remoteAgentEnvironment'; +import { IRequestService } from 'vs/platform/request/common/request'; +import { RequestChannel } from 'vs/platform/request/common/requestIpc'; +import { RequestService } from 'vs/platform/request/node/requestService'; +import { ITelemetryService } from 'vs/platform/telemetry/common/telemetry'; +import { NullTelemetryService } from 'vs/platform/telemetry/common/telemetryUtils'; +import { IFileChangeDto } from 'vs/workbench/api/common/extHost.protocol'; +import { IExtHostReadyMessage, IExtHostSocketMessage } from 'vs/workbench/services/extensions/common/extensionHostProtocol'; +import { Logger } from 'vs/workbench/services/extensions/common/extensionPoints'; +import { ExtensionScanner, ExtensionScannerInput, IExtensionReference } from 'vs/workbench/services/extensions/node/extensionPoints'; +import { IGetEnvironmentDataArguments, IRemoteAgentEnvironmentDTO, IScanExtensionsArguments, IScanSingleExtensionArguments } from 'vs/workbench/services/remote/common/remoteAgentEnvironmentChannel'; +import { REMOTE_FILE_SYSTEM_CHANNEL_NAME } from 'vs/workbench/services/remote/common/remoteAgentFileSystemChannel'; +import { RemoteExtensionLogFileName } from 'vs/workbench/services/remote/common/remoteAgentService'; + +export type IRawURITransformerFactory = (remoteAuthority: string) => IRawURITransformer; +export const IRawURITransformerFactory = createDecorator('rawURITransformerFactory'); + +const APP_ROOT = dirname(FileAccess.asFileUri('', require).fsPath); +const uriTransformerPath = path.join(APP_ROOT, 'out/serverUriTransformer'); +const rawURITransformerFactory: IRawURITransformerFactory = require.__$__nodeRequire(uriTransformerPath); + +const WEB_MAIN = path.join(APP_ROOT, 'out', 'vs', 'server', 'browser', 'workbench', 'workbench.html'); +const WEB_MAIN_DEV = path.join(APP_ROOT, 'out', 'vs', 'server', 'browser', 'workbench', 'workbench-dev.html'); + +function registerErrorHandler(logService: ILogService): void { + setUnexpectedErrorHandler(e => logService.error(e)); + // Print a console message when rejection isn't handled within N seconds. For details: + // see https://nodejs.org/api/process.html#process_event_unhandledrejection + // and https://nodejs.org/api/process.html#process_event_rejectionhandled + const unhandledPromises: Promise[] = []; + process.on('unhandledRejection', (reason: any, promise: Promise) => { + unhandledPromises.push(promise); + setTimeout(() => { + const idx = unhandledPromises.indexOf(promise); + if (idx >= 0) { + promise.catch(e => { + unhandledPromises.splice(idx, 1); + if (!isPromiseCanceledError(e)) { + logService.warn(`rejected promise not handled within 1 second: ${e}`); + if (e && e.stack) { + logService.warn(`stack trace: ${e.stack}`); + } + onUnexpectedError(reason); + } + }); + } + }, 1000); + }); + + process.on('rejectionHandled', (promise: Promise) => { + const idx = unhandledPromises.indexOf(promise); + if (idx >= 0) { + unhandledPromises.splice(idx, 1); + } + }); + + // Print a console message when an exception isn't handled. + process.on('uncaughtException', function (err: Error) { + onUnexpectedError(err); + }); +} + +interface ManagementProtocol { + protocol: PersistentProtocol + graceTimeReconnection: RunOnceScheduler + shortGraceTimeReconnection: RunOnceScheduler +} + +interface Client { + management?: ManagementProtocol + extensionHost?: cp.ChildProcess +} + +function safeDisposeProtocolAndSocket(protocol: PersistentProtocol): void { + try { + protocol.acceptDisconnect(); + const socket = protocol.getSocket(); + protocol.dispose(); + socket.dispose(); + } catch (err) { + onUnexpectedError(err); + } +} + +// TODO is it enough? +const textMimeType = new Map([ + ['.html', 'text/html'], + ['.js', 'text/javascript'], + ['.json', 'application/json'], + ['.css', 'text/css'], + ['.svg', 'image/svg+xml'] +]); + +// TODO is it enough? +const mapExtToMediaMimes = new Map([ + ['.bmp', 'image/bmp'], + ['.gif', 'image/gif'], + ['.ico', 'image/x-icon'], + ['.jpe', 'image/jpg'], + ['.jpeg', 'image/jpg'], + ['.jpg', 'image/jpg'], + ['.png', 'image/png'], + ['.tga', 'image/x-tga'], + ['.tif', 'image/tiff'], + ['.tiff', 'image/tiff'], + ['.woff', 'application/font-woff'] +]); + +function getMediaMime(forPath: string): string | undefined { + const ext = path.extname(forPath); + return mapExtToMediaMimes.get(ext.toLowerCase()); +} + +function serveError(req: http.IncomingMessage, res: http.ServerResponse, errorCode: number, errorMessage: string): void { + res.writeHead(errorCode, { 'Content-Type': 'text/plain' }); + res.end(errorMessage); +} + +function getFirstQueryValue(parsedUrl: url.UrlWithParsedQuery, key: string): string | undefined { + const result = parsedUrl.query[key]; + return Array.isArray(result) ? result[0] : result; +} + +function getFirstQueryValues(parsedUrl: url.UrlWithParsedQuery, ignoreKeys?: string[]): Map { + const queryValues: Map = new Map(); + + for (const key in parsedUrl.query) { + if (ignoreKeys && ignoreKeys.indexOf(key) >= 0) { + continue; + } + + const value = getFirstQueryValue(parsedUrl, key); + if (typeof value === 'string') { + queryValues.set(key, value); + } + } + + return queryValues; +} + +async function serveFile(logService: ILogService, req: http.IncomingMessage, res: http.ServerResponse, filePath: string, responseHeaders: http.OutgoingHttpHeaders = {}) { + try { + + // Sanity checks + filePath = path.normalize(filePath); // ensure no "." and ".." + + const stat = await fs.promises.stat(filePath); + + // Check if file modified since + const etag = `W/"${[stat.ino, stat.size, stat.mtime.getTime()].join('-')}"`; // weak validator (https://developer.mozilla.org/en-US/docs/Web/HTTP/Headers/ETag) + if (req.headers['if-none-match'] === etag) { + res.writeHead(304); + return res.end(); + } + + // Headers + responseHeaders['Content-Type'] = textMimeType.get(path.extname(filePath)) || getMediaMime(filePath) || 'text/plain'; + responseHeaders['Etag'] = etag; + + res.writeHead(200, responseHeaders); + + // Data + fs.createReadStream(filePath).pipe(res); + } catch (error) { + logService.error(error); + res.writeHead(404, { 'Content-Type': 'text/plain' }); + return res.end('Not found'); + } +} + +async function handleRoot(req: http.IncomingMessage, resp: http.ServerResponse, entryPointPath: string, environmentService: INativeEnvironmentService) { + if (!req.headers.host) { + return serveError(req, resp, 400, 'Bad request.'); + } + + const workbenchConfig = { + developmentOptions: { + enableSmokeTestDriver: environmentService.driverHandle === 'web' ? true : undefined + } + }; + + const escapeQuote = (str: string) => str.replace(/"/g, '"'); + const entryPointContent = (await fs.promises.readFile(entryPointPath)) + .toString() + .replace('{{WORKBENCH_WEB_CONFIGURATION}}', escapeQuote(JSON.stringify(workbenchConfig))); + + resp.writeHead(200, { + 'Content-Type': 'text/html' + }); + return resp.end(entryPointContent); +} + +const mapCallbackUriToRequestId = new Map(); +async function handleCallback(logService: ILogService, req: http.IncomingMessage, res: http.ServerResponse, parsedUrl: url.UrlWithParsedQuery) { + const wellKnownKeys = ['vscode-requestId', 'vscode-scheme', 'vscode-authority', 'vscode-path', 'vscode-query', 'vscode-fragment']; + const [requestId, vscodeScheme, vscodeAuthority, vscodePath, vscodeQuery, vscodeFragment] = wellKnownKeys.map(key => { + const value = getFirstQueryValue(parsedUrl, key); + if (value) { + return decodeURIComponent(value); + } + + return value; + }); + + if (!requestId) { + res.writeHead(400, { 'Content-Type': 'text/plain' }); + return res.end(`Bad request.`); + } + + // merge over additional query values that we got + let query = vscodeQuery; + let index = 0; + getFirstQueryValues(parsedUrl, wellKnownKeys).forEach((value, key) => { + if (!query) { + query = ''; + } + + const prefix = (index++ === 0) ? '' : '&'; + query += `${prefix}${key}=${value}`; + }); + + // add to map of known callbacks + mapCallbackUriToRequestId.set(requestId, JSON.stringify({ scheme: vscodeScheme || product.urlProtocol, authority: vscodeAuthority, path: vscodePath, query, fragment: vscodeFragment })); + return serveFile(logService, req, res, FileAccess.asFileUri('vs/code/browser/workbench/callback.html', require).fsPath, { 'Content-Type': 'text/html' }); +} + +async function handleFetchCallback(req: http.IncomingMessage, res: http.ServerResponse, parsedUrl: url.UrlWithParsedQuery) { + const requestId = getFirstQueryValue(parsedUrl, 'vscode-requestId'); + if (!requestId) { + res.writeHead(400, { 'Content-Type': 'text/plain' }); + return res.end(`Bad request.`); + } + + const knownCallbackUri = mapCallbackUriToRequestId.get(requestId); + if (knownCallbackUri) { + mapCallbackUriToRequestId.delete(requestId); + } + + res.writeHead(200, { 'Content-Type': 'text/json' }); + return res.end(knownCallbackUri); +} + + +interface ServerParsedArgs extends NativeParsedArgs { + port?: string + host?: string +} +const SERVER_OPTIONS: OptionDescriptions = { + ...OPTIONS, + port: { type: 'string' }, + host: { type: 'string' } +}; + +export interface IStartServerResult { + installingInitialExtensions?: Promise +} + +export interface IServerOptions { + port?: number; + main?: string + mainDev?: string + skipExtensions?: Set + configure?(services: ServiceCollection, channelServer: IPCServer): void + start?(accessor: ServicesAccessor, channelServer: IPCServer): IStartServerResult | void + + configureExtensionHostForkOptions?(opts: cp.ForkOptions, accessor: ServicesAccessor, channelServer: IPCServer): void; + configureExtensionHostProcess?(extensionHost: cp.ChildProcess, accessor: ServicesAccessor, channelServer: IPCServer): IDisposable; + + handleRequest?(pathname: string | null, req: http.IncomingMessage, res: http.ServerResponse, accessor: ServicesAccessor, channelServer: IPCServer): Promise; +} + +export async function main(options: IServerOptions): Promise { + const connectionToken = generateUuid(); + + const parsedArgs = parseArgs(process.argv, SERVER_OPTIONS); + + // VSCODE_AGENT_FOLDER used by smoke and integration tests. + parsedArgs['user-data-dir'] = process.env.VSCODE_AGENT_FOLDER || path.join(os.homedir(), product.dataFolderName); + + const productService = { _serviceBrand: undefined, ...product }; + const environmentService = new NativeEnvironmentService(parsedArgs, productService); + + const devMode = !environmentService.isBuilt; + + // see src/vs/code/electron-main/main.ts#142 + const bufferLogService = new BufferLogService(); + const logService = new MultiplexLogService([new ConsoleMainLogger(getLogLevel(environmentService)), bufferLogService]); + registerErrorHandler(logService); + + // see src/vs/code/electron-main/main.ts#204 + await Promise.all([ + environmentService.extensionsPath, + environmentService.logsPath, + environmentService.globalStorageHome.fsPath, + environmentService.workspaceStorageHome.fsPath + ].map(path => path ? fs.promises.mkdir(path, { recursive: true }) : undefined)); + + const onDidClientConnectEmitter = new Emitter(); + const channelServer = new IPCServer(onDidClientConnectEmitter.event); + channelServer.registerChannel('logger', new LogLevelChannel(logService)); + channelServer.registerChannel(ExtensionHostDebugBroadcastChannel.ChannelName, new ExtensionHostDebugBroadcastChannel()); + + const fileService = new FileService(logService); + const diskFileSystemProvider = new DiskFileSystemProvider(logService); + fileService.registerProvider(Schemas.file, diskFileSystemProvider); + const rootPath = FileAccess.asFileUri('', require).fsPath; + + const extraDevSystemExtensionsRoot = path.normalize(path.join(rootPath, '..', '.build', 'builtInExtensions')); + const logger = new Logger((severity, source, message) => { + const msg = devMode && source ? `[${source}]: ${message}` : message; + if (severity === Severity.Error) { + logService.error(msg); + } else if (severity === Severity.Warning) { + logService.warn(msg); + } else { + logService.info(msg); + } + }); + // see used APIs in vs/workbench/services/remote/common/remoteAgentEnvironmentChannel.ts + class RemoteExtensionsEnvironment implements IServerChannel { + protected extensionHostLogFileSeq = 1; + async call(ctx: RemoteAgentConnectionContext, command: string, arg?: any, cancellationToken?: CancellationToken | undefined): Promise { + if (command === 'getEnvironmentData') { + const args: IGetEnvironmentDataArguments = arg; + const uriTranformer = new URITransformer(rawURITransformerFactory(args.remoteAuthority)); + return transformOutgoingURIs({ + pid: process.pid, + connectionToken, + appRoot: URI.file(environmentService.appRoot), + settingsPath: environmentService.machineSettingsResource, + logsPath: URI.file(environmentService.logsPath), + extensionsPath: URI.file(environmentService.extensionsPath), + extensionHostLogsPath: URI.file(path.join(environmentService.logsPath, `extension_host_${this.extensionHostLogFileSeq++}`)), + globalStorageHome: environmentService.globalStorageHome, + workspaceStorageHome: environmentService.workspaceStorageHome, + userHome: environmentService.userHome, + os: platform.OS, + arch: process.arch, + marks: [], + useHostProxy: false + } as IRemoteAgentEnvironmentDTO, uriTranformer); + } + if (command === 'scanSingleExtension') { + let args: IScanSingleExtensionArguments = arg; + const uriTranformer = new URITransformer(rawURITransformerFactory(args.remoteAuthority)); + args = transformIncomingURIs(args, uriTranformer); + // see scanSingleExtension in src/vs/workbench/services/extensions/electron-browser/cachedExtensionScanner.ts + // TODO: read built nls file + const translations = {}; + const input = new ExtensionScannerInput(product.version, product.date, product.commit, args.language, devMode, URI.revive(args.extensionLocation).fsPath, args.isBuiltin, false, translations); + const extension = await ExtensionScanner.scanSingleExtension(input, logService); + if (!extension) { + return undefined; + } + return transformOutgoingURIs(extension, uriTranformer); + } + if (command === 'scanExtensions') { + let args: IScanExtensionsArguments = arg; + const uriTranformer = new URITransformer(rawURITransformerFactory(args.remoteAuthority)); + args = transformIncomingURIs(args, uriTranformer); + // see _scanInstalledExtensions in src/vs/workbench/services/extensions/electron-browser/cachedExtensionScanner.ts + // TODO: read built nls file + const translations = {}; + let pendingSystem = ExtensionScanner.scanExtensions(new ExtensionScannerInput(product.version, product.date, product.commit, args.language, devMode, environmentService.builtinExtensionsPath, true, false, translations), logger); + const builtInExtensions = product.builtInExtensions; + if (devMode && builtInExtensions && builtInExtensions.length) { + pendingSystem = ExtensionScanner.mergeBuiltinExtensions(pendingSystem, ExtensionScanner.scanExtensions(new ExtensionScannerInput(product.version, product.date, product.commit, args.language, devMode, extraDevSystemExtensionsRoot, true, false, translations), logger, { + resolveExtensions: () => { + const result: IExtensionReference[] = []; + for (const extension of builtInExtensions) { + result.push({ name: extension.name, path: path.join(extraDevSystemExtensionsRoot, extension.name) }); + } + return Promise.resolve(result); + } + })); + } + const pendingUser = extensionsInstalled.then(() => ExtensionScanner.scanExtensions(new ExtensionScannerInput(product.version, product.date, product.commit, args.language, devMode, environmentService.extensionsPath, false, false, translations), logger)); + let pendingDev: Promise[] = []; + if (args.extensionDevelopmentPath) { + pendingDev = args.extensionDevelopmentPath.map(devPath => ExtensionScanner.scanOneOrMultipleExtensions(new ExtensionScannerInput(product.version, product.date, product.commit, args.language, devMode, URI.revive(devPath).fsPath, false, true, translations), logger)); + } + const result: IExtensionDescription[] = []; + const skipExtensions = new Set([...args.skipExtensions.map(ExtensionIdentifier.toKey), ...(options?.skipExtensions || [])]); + for (const extensions of await Promise.all([...pendingDev, pendingUser, pendingSystem])) { + for (let i = extensions.length - 1; i >= 0; i--) { + const extension = extensions[i]; + const key = ExtensionIdentifier.toKey(extension.identifier); + if (skipExtensions.has(key)) { + continue; + } + skipExtensions.add(key); + result.unshift(transformOutgoingURIs(extension, uriTranformer)); + } + } + return result; + } + logService.error('Unknown command: RemoteExtensionsEnvironment.' + command); + throw new Error('Unknown command: RemoteExtensionsEnvironment.' + command); + } + listen(ctx: RemoteAgentConnectionContext, event: string, arg?: any): Event { + logService.error('Unknown event: RemoteExtensionsEnvironment.' + event); + throw new Error('Unknown event: RemoteExtensionsEnvironment.' + event); + } + } + channelServer.registerChannel('remoteextensionsenvironment', new RemoteExtensionsEnvironment()); + + // see used APIs in src/vs/workbench/services/remote/common/remoteAgentFileSystemChannel.ts + class RemoteFileSystem implements IServerChannel { + protected readonly watchers = new Map + }>(); + protected readonly watchHandles = new Map(); + async call(ctx: RemoteAgentConnectionContext, command: string, arg?: any, cancellationToken?: CancellationToken | undefined): Promise { + if (command === 'stat') { + const uriTranformer = new URITransformer(rawURITransformerFactory(ctx.remoteAuthority)); + return diskFileSystemProvider.stat(URI.revive(uriTranformer.transformIncoming(arg[0]))); + } + if (command === 'open') { + const uriTranformer = new URITransformer(rawURITransformerFactory(ctx.remoteAuthority)); + return diskFileSystemProvider.open(URI.revive(uriTranformer.transformIncoming(arg[0])), arg[1]); + } + if (command === 'close') { + return diskFileSystemProvider.close(arg[0]); + } + if (command === 'read') { + const length = arg[2]; + const data = VSBuffer.alloc(length); + const read = await diskFileSystemProvider.read(arg[0], arg[1], data.buffer, 0, length); + return [read, data.slice(0, read)]; + } + if (command === 'readFile') { + const uriTranformer = new URITransformer(rawURITransformerFactory(ctx.remoteAuthority)); + const data = await diskFileSystemProvider.readFile(URI.revive(uriTranformer.transformIncoming(arg[0]))); + return VSBuffer.wrap(data); + } + if (command === 'write') { + const data = arg[2] as VSBuffer; + await diskFileSystemProvider.write(arg[0], arg[1], data.buffer, arg[3], arg[4]); + return; + } + if (command === 'writeFile') { + const uriTranformer = new URITransformer(rawURITransformerFactory(ctx.remoteAuthority)); + const data = arg[1] as VSBuffer; + await diskFileSystemProvider.writeFile(URI.revive(uriTranformer.transformIncoming(arg[0])), data.buffer, arg[2]); + return; + } + if (command === 'delete') { + const uriTranformer = new URITransformer(rawURITransformerFactory(ctx.remoteAuthority)); + await diskFileSystemProvider.delete(URI.revive(uriTranformer.transformIncoming(arg[0])), arg[1]); + return; + } + if (command === 'mkdir') { + const uriTranformer = new URITransformer(rawURITransformerFactory(ctx.remoteAuthority)); + await diskFileSystemProvider.mkdir(URI.revive(uriTranformer.transformIncoming(arg[0]))); + return; + } + if (command === 'readdir') { + const uriTranformer = new URITransformer(rawURITransformerFactory(ctx.remoteAuthority)); + return diskFileSystemProvider.readdir(URI.revive(uriTranformer.transformIncoming(arg[0]))); + } + if (command === 'rename') { + const uriTranformer = new URITransformer(rawURITransformerFactory(ctx.remoteAuthority)); + return diskFileSystemProvider.rename( + URI.revive(uriTranformer.transformIncoming(arg[0])), + URI.revive(uriTranformer.transformIncoming(arg[1])), + arg[2] + ); + } + if (command === 'copy') { + const uriTranformer = new URITransformer(rawURITransformerFactory(ctx.remoteAuthority)); + return diskFileSystemProvider.copy( + URI.revive(uriTranformer.transformIncoming(arg[0])), + URI.revive(uriTranformer.transformIncoming(arg[1])), + arg[2] + ); + } + if (command === 'watch') { + const watcher = this.watchers.get(arg[0])?.watcher; + if (watcher) { + const uriTranformer = new URITransformer(rawURITransformerFactory(ctx.remoteAuthority)); + const unwatch = watcher.watch(URI.revive(uriTranformer.transformIncoming(arg[2])), arg[3]); + this.watchHandles.set( + arg[0] + ':' + arg[1], + unwatch + ); + } else { + logService.error(`'filechange' event should be called before 'watch' first request`); + } + return; + } + if (command === 'unwatch') { + this.watchHandles.get(arg[0] + ':' + arg[1])?.dispose(); + this.watchHandles.delete(arg[0] + ':' + arg[1]); + return; + } + logService.error('Unknown command: RemoteFileSystem.' + command); + throw new Error('Unknown command: RemoteFileSystem.' + command); + } + protected obtainFileChangeEmitter(ctx: RemoteAgentConnectionContext, session: string): Emitter { + let existing = this.watchers.get(session); + if (existing) { + return existing.emitter; + } + const watcher = new DiskFileSystemProvider(logService); + const emitter = new Emitter({ + onLastListenerRemove: () => { + this.watchers.delete(session); + emitter.dispose(); + watcher.dispose(); + logService.info(`[session:${session}] closed watching fs`); + } + }); + logService.info(`[session:${session}] started watching fs`); + this.watchers.set(session, { watcher, emitter }); + + const uriTranformer = new URITransformer(rawURITransformerFactory(ctx.remoteAuthority)); + watcher.onDidChangeFile(changes => emitter.fire( + changes.map(change => ({ + resource: uriTranformer.transformOutgoingURI(change.resource), + type: change.type + } as IFileChangeDto)) + )); + watcher.onDidErrorOccur(error => emitter.fire(error)); + return emitter; + } + listen(ctx: RemoteAgentConnectionContext, event: string, arg?: any): Event { + if (event === 'filechange') { + return this.obtainFileChangeEmitter(ctx, arg[0]).event; + } + if (event === 'readFileStream') { + const uriTranformer = new URITransformer(rawURITransformerFactory(ctx.remoteAuthority)); + const resource = URI.revive(transformIncomingURIs(arg[0], uriTranformer)); + const emitter = new Emitter>({ + onLastListenerRemove: () => { + cancellationTokenSource.cancel(); + } + }); + const cancellationTokenSource = new CancellationTokenSource(); + const stream = diskFileSystemProvider.readFileStream(resource, arg[1], cancellationTokenSource.token); + stream.on('data', data => emitter.fire(VSBuffer.wrap(data))); + stream.on('error', error => emitter.fire(error)); + stream.on('end', () => { + emitter.fire('end'); + emitter.dispose(); + cancellationTokenSource.dispose(); + }); + return emitter.event; + } + logService.error('Unknown event: RemoteFileSystem.' + event); + throw new Error('Unknown event: RemoteFileSystem.' + event); + } + } + channelServer.registerChannel(REMOTE_FILE_SYSTEM_CHANNEL_NAME, new RemoteFileSystem()); + + // Init services + const services = new ServiceCollection(); + services.set(IRawURITransformerFactory, rawURITransformerFactory); + + services.set(IEnvironmentService, environmentService); + services.set(INativeEnvironmentService, environmentService); + services.set(ILogService, logService); + services.set(ITelemetryService, NullTelemetryService); + + services.set(IFileService, fileService); + + services.set(IConfigurationService, new SyncDescriptor(ConfigurationService, [environmentService.settingsResource, fileService])); + services.set(IProductService, productService); + services.set(IRequestService, new SyncDescriptor(RequestService)); + services.set(IDownloadService, new SyncDescriptor(DownloadService)); + + services.set(IExtensionGalleryService, new SyncDescriptor(ExtensionGalleryServiceWithNoStorageService)); + services.set(IExtensionManagementService, new SyncDescriptor(ExtensionManagementService)); + + services.set(IRequestService, new SyncDescriptor(RequestService)); + + if (options.configure) { + options.configure(services, channelServer); + } + + let resolveExtensionsInstalled: (value?: unknown) => void; + const extensionsInstalled = new Promise(resolve => resolveExtensionsInstalled = resolve); + + // Startup + const instantiationService = new InstantiationService(services); + instantiationService.invokeFunction(accessor => { + let startResult = undefined; + if (options.start) { + startResult = options.start(accessor, channelServer); + } + if (startResult && startResult.installingInitialExtensions) { + startResult.installingInitialExtensions.then(resolveExtensionsInstalled); + } else { + resolveExtensionsInstalled(); + } + + const extensionManagementService = accessor.get(IExtensionManagementService); + channelServer.registerChannel('extensions', new ExtensionManagementChannel(extensionManagementService, requestContext => new URITransformer(rawURITransformerFactory(requestContext)))); + (extensionManagementService as ExtensionManagementService).removeDeprecatedExtensions(); + + const requestService = accessor.get(IRequestService); + channelServer.registerChannel('request', new RequestChannel(requestService)); + + // Delay creation of spdlog for perf reasons (https://github.com/microsoft/vscode/issues/72906) + bufferLogService.logger = new SpdLogLogger('main', join(environmentService.logsPath, `${RemoteExtensionLogFileName}.log`), true, bufferLogService.getLevel()); + + const clients = new Map(); + + const server = http.createServer(async (req, res) => { + if (!req.url) { + return serveError(req, res, 400, 'Bad Request.'); + } + try { + const parsedUrl = url.parse(req.url, true); + const pathname = parsedUrl.pathname; + + if (options.handleRequest && await instantiationService.invokeFunction(accessor => options.handleRequest!(pathname, req, res, accessor, channelServer))) { + return; + } + + //#region headless + if (pathname === '/vscode-remote-resource') { + const filePath = parsedUrl.query['path']; + const fsPath = typeof filePath === 'string' && URI.from({ scheme: 'file', path: filePath }).fsPath; + if (!fsPath) { + return serveError(req, res, 400, 'Bad Request.'); + } + return serveFile(logService, req, res, fsPath); + } + //#region headless end + + //#region static + if (pathname === '/') { + return handleRoot(req, res, devMode ? options.mainDev || WEB_MAIN_DEV : options.main || WEB_MAIN, environmentService); + } + + if (pathname === '/callback') { + return handleCallback(logService, req, res, parsedUrl); + } + + if (pathname === '/fetch-callback') { + return handleFetchCallback(req, res, parsedUrl); + } + + if (pathname === '/manifest.json') { + res.writeHead(200, { 'Content-Type': 'application/json' }); + return res.end(JSON.stringify({ + 'name': product.nameLong, + 'short_name': product.nameShort, + 'start_url': '/', + 'lang': 'en-US', + 'display': 'standalone' + })); + } + if (pathname) { + let relativeFilePath; + if (/^\/static\//.test(pathname)) { + relativeFilePath = path.normalize(decodeURIComponent(pathname.substr('/static/'.length))); + } else { + relativeFilePath = path.normalize(decodeURIComponent(pathname)); + } + return serveFile(logService, req, res, path.join(APP_ROOT, relativeFilePath)); + } + //#region static end + + logService.error(`${req.method} ${req.url} not found`); + return serveError(req, res, 404, 'Not found.'); + } catch (error) { + logService.error(error); + + return serveError(req, res, 500, 'Internal Server Error.'); + } + }); + server.on('error', e => logService.error(e)); + server.on('upgrade', (req: http.IncomingMessage, socket: net.Socket) => { + if (req.headers['upgrade'] !== 'websocket' || !req.url) { + logService.error(`failed to upgrade for header "${req.headers['upgrade']}" and url: "${req.url}".`); + socket.end('HTTP/1.1 400 Bad Request'); + return; + } + const { query } = url.parse(req.url, true); + // /?reconnectionToken=c0e3a8af-6838-44fb-851b-675401030831&reconnection=false&skipWebSocketFrames=false + const reconnection = 'reconnection' in query && query['reconnection'] === 'true'; + let token: string | undefined; + if ('reconnectionToken' in query && typeof query['reconnectionToken'] === 'string') { + token = query['reconnectionToken']; + } + // TODO skipWebSocketFrames (support of VS Code desktop?) + if (!token) { + logService.error(`missing token for "${req.url}".`); + socket.end('HTTP/1.1 400 Bad Request'); + return; + } + logService.info(`[${token}] Socket upgraded for "${req.url}".`); + socket.on('error', e => { + logService.error(`[${token}] Socket failed for "${req.url}".`, e); + }); + + const acceptKey = req.headers['sec-websocket-key']; + const hash = crypto.createHash('sha1').update(acceptKey + '258EAFA5-E914-47DA-95CA-C5AB0DC85B11').digest('base64'); + const responseHeaders = ['HTTP/1.1 101 Web Socket Protocol Handshake', 'Upgrade: WebSocket', 'Connection: Upgrade', `Sec-WebSocket-Accept: ${hash}`]; + + let permessageDeflate = false; + if (String(req.headers['sec-websocket-extensions']).indexOf('permessage-deflate') !== -1) { + permessageDeflate = true; + responseHeaders.push('Sec-WebSocket-Extensions: permessage-deflate; server_max_window_bits=15'); + } + + socket.write(responseHeaders.join('\r\n') + '\r\n\r\n'); + + const client = clients.get(token) || {}; + clients.set(token, client); + + const webSocket = new WebSocketNodeSocket(new NodeSocket(socket), permessageDeflate, null, permessageDeflate); + const protocol = new PersistentProtocol(webSocket); + const controlListener = protocol.onControlMessage(async raw => { + const msg = JSON.parse(raw.toString()); + if (msg.type === 'error') { + logService.error(`[${token}] error control message:`, msg.reason); + safeDisposeProtocolAndSocket(protocol); + } else if (msg.type === 'auth') { + protocol.sendControl(VSBuffer.fromString(JSON.stringify({ + type: 'sign', + data: productService.nameShort + ' Server' + } as SignRequest))); + } else if (msg.type === 'connectionType') { + controlListener.dispose(); + // TODO version matching msg.commit + // TODO auth check msg.signedData + for (const [token, client] of clients) { + if (client.management) { + if (client.management.graceTimeReconnection.isScheduled() && !client.management.shortGraceTimeReconnection.isScheduled()) { + logService.info(`[${token}] Another connection is established, closing this connection after ${ProtocolConstants.ReconnectionShortGraceTime}ms reconnection timeout.`); + client.management.shortGraceTimeReconnection.schedule(); + } + } + if (client.extensionHost) { + client.extensionHost.send({ + type: 'VSCODE_EXTHOST_IPC_REDUCE_GRACE_TIME' + }); + } + } + if (msg.desiredConnectionType === ConnectionType.Management) { + if (!reconnection) { + if (client.management) { + logService.error(`[${token}] Falied to connect: management connection is already running.`); + protocol.sendControl(VSBuffer.fromString(JSON.stringify({ type: 'error', reason: 'Management connection is already running.' } as ErrorMessage))); + safeDisposeProtocolAndSocket(protocol); + return; + } + + const onDidClientDisconnectEmitter = new Emitter(); + let disposed = false; + function dispose(): void { + if (disposed) { + return; + } + disposed = true; + graceTimeReconnection.dispose(); + shortGraceTimeReconnection.dispose(); + client.management = undefined; + protocol.sendDisconnect(); + const socket = protocol.getSocket(); + protocol.dispose(); + socket.end(); + onDidClientDisconnectEmitter.fire(undefined); + onDidClientDisconnectEmitter.dispose(); + logService.info(`[${token}] Management connection is disposed.`); + } + + protocol.sendControl(VSBuffer.fromString(JSON.stringify({ type: 'ok' } as OKMessage))); + const graceTimeReconnection = new RunOnceScheduler(() => { + logService.info(`[${token}] Management connection expired after ${ProtocolConstants.ReconnectionGraceTime}ms (grace).`); + dispose(); + }, ProtocolConstants.ReconnectionGraceTime); + const shortGraceTimeReconnection = new RunOnceScheduler(() => { + logService.info(`[${token}] Management connection expired after ${ProtocolConstants.ReconnectionShortGraceTime}ms (short grace).`); + dispose(); + }, ProtocolConstants.ReconnectionShortGraceTime); + client.management = { protocol, graceTimeReconnection, shortGraceTimeReconnection }; + protocol.onDidDispose(() => dispose()); + protocol.onSocketClose(() => { + logService.info(`[${token}] Management connection socket is closed, waiting to reconnect within ${ProtocolConstants.ReconnectionGraceTime}ms.`); + graceTimeReconnection.schedule(); + }); + onDidClientConnectEmitter.fire({ protocol, onDidClientDisconnect: onDidClientDisconnectEmitter.event }); + logService.info(`[${token}] Management connection is connected.`); + } else { + if (!client.management) { + logService.error(`[${token}] Failed to reconnect: management connection is not running.`); + protocol.sendControl(VSBuffer.fromString(JSON.stringify({ type: 'error', reason: 'Management connection is not running.' } as ErrorMessage))); + safeDisposeProtocolAndSocket(protocol); + return; + } + + protocol.sendControl(VSBuffer.fromString(JSON.stringify({ type: 'ok' } as OKMessage))); + client.management.graceTimeReconnection.cancel(); + client.management.shortGraceTimeReconnection.cancel(); + client.management.protocol.beginAcceptReconnection(protocol.getSocket(), protocol.readEntireBuffer()); + client.management.protocol.endAcceptReconnection(); + protocol.dispose(); + logService.info(`[${token}] Management connection is reconnected.`); + } + } else if (msg.desiredConnectionType === ConnectionType.ExtensionHost) { + const params: IRemoteExtensionHostStartParams = { + language: 'en', + ...msg.args + // TODO what if params.port is 0? + }; + + if (!reconnection) { + if (client.extensionHost) { + logService.error(`[${token}] Falied to connect: extension host is already running.`); + protocol.sendControl(VSBuffer.fromString(JSON.stringify({ type: 'error', reason: 'Extension host is already running.' } as ErrorMessage))); + safeDisposeProtocolAndSocket(protocol); + return; + } + + protocol.sendControl(VSBuffer.fromString(JSON.stringify({ debugPort: params.port } /* Omit */))); + const initialDataChunk = Buffer.from(protocol.readEntireBuffer().buffer).toString('base64'); + protocol.dispose(); + socket.pause(); + await webSocket.drain(); + + try { + // see src/vs/workbench/services/extensions/electron-browser/localProcessExtensionHost.ts + const opts: cp.ForkOptions = { + env: { + ...process.env, + VSCODE_AMD_ENTRYPOINT: 'vs/workbench/services/extensions/node/extensionHostProcess', + VSCODE_PIPE_LOGGING: 'true', + VSCODE_VERBOSE_LOGGING: 'true', + VSCODE_LOG_NATIVE: 'false', + VSCODE_EXTHOST_WILL_SEND_SOCKET: 'true', + VSCODE_HANDLES_UNCAUGHT_ERRORS: 'true', + VSCODE_LOG_STACK: 'true', + VSCODE_LOG_LEVEL: environmentService.verbose ? 'trace' : environmentService.logLevel + }, + // see https://github.com/akosyakov/gitpod-code/blob/33b49a273f1f6d44f303426b52eaf89f0f5cc596/src/vs/base/parts/ipc/node/ipc.cp.ts#L72-L78 + execArgv: [], + silent: true + }; + if (typeof params.port === 'number') { + if (params.port !== 0) { + opts.execArgv = [ + '--nolazy', + (params.break ? '--inspect-brk=' : '--inspect=') + params.port + ]; + } else { + // TODO we should return a dynamically allocated port to the client, + // it is better to avoid it? + opts.execArgv = ['--inspect-port=0']; + } + } + if (options.configureExtensionHostForkOptions) { + instantiationService.invokeFunction(accessor => options.configureExtensionHostForkOptions!(opts, accessor, channelServer)); + } + const extensionHost = cp.fork(FileAccess.asFileUri('bootstrap-fork', require).fsPath, ['--type=extensionHost', '--uriTransformerPath=' + uriTransformerPath], opts); + extensionHost.stdout!.setEncoding('utf8'); + extensionHost.stderr!.setEncoding('utf8'); + Event.fromNodeEventEmitter(extensionHost.stdout!, 'data')(msg => logService.info(`[${token}][extension host][${extensionHost.pid}][stdout] ${msg}`)); + Event.fromNodeEventEmitter(extensionHost.stderr!, 'data')(msg => logService.info(`[${token}][extension host][${extensionHost.pid}][stderr] ${msg}`)); + extensionHost.on('message', msg => { + if (msg && (msg).type === '__$console') { + logService.info(`[${token}][extension host][${extensionHost.pid}][__$console] ${(msg).arguments}`); + } + }); + + let disposed = false; + let toDispose: IDisposable = { dispose: () => { } }; + function dispose(): void { + if (disposed) { + return; + } + disposed = true; + toDispose.dispose(); + socket.end(); + extensionHost.kill(); + client.extensionHost = undefined; + logService.info(`[${token}] Extension host is disconnected.`); + } + + extensionHost.on('error', err => { + dispose(); + logService.error(`[${token}] Extension host failed with: `, err); + }); + extensionHost.on('exit', (code: number, signal: string) => { + dispose(); + if (code !== 0 && signal !== 'SIGTERM') { + logService.error(`[${token}] Extension host exited with code: ${code} and signal: ${signal}.`); + } + }); + + const readyListener = (msg: any) => { + if (msg && (msg).type === 'VSCODE_EXTHOST_IPC_READY') { + extensionHost.removeListener('message', readyListener); + const inflateBytes = Buffer.from(webSocket.recordedInflateBytes.buffer).toString('base64'); + extensionHost.send({ + type: 'VSCODE_EXTHOST_IPC_SOCKET', + initialDataChunk, + skipWebSocketFrames: false, // TODO skipWebSocketFrames - i.e. when we connect from Node (VS Code?) + permessageDeflate, + inflateBytes + } as IExtHostSocketMessage, socket); + logService.info(`[${token}] Extension host is connected.`); + } + }; + extensionHost.on('message', readyListener); + + if (options.configureExtensionHostProcess) { + toDispose = instantiationService.invokeFunction(accessor => options.configureExtensionHostProcess!(extensionHost, accessor, channelServer)); + } + client.extensionHost = extensionHost; + logService.info(`[${token}] Extension host is started.`); + } catch (e) { + logService.error(`[${token}] Failed to start the extension host process: `, e); + } + } else { + if (!client.extensionHost) { + logService.error(`[${token}] Failed to reconnect: extension host is not running.`); + protocol.sendControl(VSBuffer.fromString(JSON.stringify({ type: 'error', reason: 'Extension host is not running.' } as ErrorMessage))); + safeDisposeProtocolAndSocket(protocol); + return; + } + + protocol.sendControl(VSBuffer.fromString(JSON.stringify({ debugPort: params.port } /* Omit */))); + const initialDataChunk = Buffer.from(protocol.readEntireBuffer().buffer).toString('base64'); + protocol.dispose(); + socket.pause(); + await webSocket.drain(); + + const inflateBytes = Buffer.from(webSocket.recordedInflateBytes.buffer).toString('base64'); + client.extensionHost.send({ + type: 'VSCODE_EXTHOST_IPC_SOCKET', + initialDataChunk, + skipWebSocketFrames: false, // TODO skipWebSocketFrames - i.e. when we connect from Node (VS Code?) + permessageDeflate, + inflateBytes + } as IExtHostSocketMessage, socket); + logService.info(`[${token}] Extension host is reconnected.`); + } + } else { + logService.error(`[${token}] Unexpected connection type:`, msg.desiredConnectionType); + safeDisposeProtocolAndSocket(protocol); + } + } else { + logService.error(`[${token}] Unexpected control message:`, msg.type); + safeDisposeProtocolAndSocket(protocol); + } + }); + }); + + let port = 3000; + if (parsedArgs.port) { + port = Number(parsedArgs.port); + } else if (typeof options.port === 'number') { + port = options.port; + } + const host = parsedArgs.host || '0.0.0.0'; + server.listen(port, host, () => { + const addressInfo = server.address() as net.AddressInfo; + const address = addressInfo.address === '0.0.0.0' || addressInfo.address === '127.0.0.1' ? 'localhost' : addressInfo.address; + const port = addressInfo.port === 80 ? '' : String(addressInfo.port); + logService.info(`Web UI available at http://${address}:${port}`); + }); + }); +} diff --git a/src/vs/server/node/server.ts b/src/vs/server/node/server.ts new file mode 100644 index 0000000000000..d90dff3778e00 --- /dev/null +++ b/src/vs/server/node/server.ts @@ -0,0 +1,12 @@ +/*--------------------------------------------------------------------------------------------- + * Copyright (c) Gitpod. All rights reserved. + *--------------------------------------------------------------------------------------------*/ + +import { registerRemoteTerminal } from 'vs/server/node/remote-terminal'; +import { main } from 'vs/server/node/server.main'; + +main({ + start: (services, channelServer) => { + registerRemoteTerminal(services, channelServer); + } +}); diff --git a/test/automation/src/extensions.ts b/test/automation/src/extensions.ts index 0288d79374fbf..e4ec51127e4f5 100644 --- a/test/automation/src/extensions.ts +++ b/test/automation/src/extensions.ts @@ -27,6 +27,12 @@ export class Extensions extends Viewlet { async searchForExtension(id: string): Promise { await this.code.waitAndClick(SEARCH_BOX); await this.code.waitForActiveElement(SEARCH_BOX); + if (process.platform === 'darwin') { + await this.code.dispatchKeybinding('cmd+a'); + } else { + await this.code.dispatchKeybinding('ctrl+a'); + } + await this.code.dispatchKeybinding('delete'); await this.code.waitForTypeInEditor(SEARCH_BOX, `@id:${id}`); await this.code.waitForTextContent(`div.part.sidebar div.composite.title h2`, 'Extensions: Marketplace'); await this.code.waitForElement(`div.extensions-viewlet[id="workbench.view.extensions"] .monaco-list-row[data-extension-id="${id}"]`); diff --git a/test/automation/src/playwrightDriver.ts b/test/automation/src/playwrightDriver.ts index 7693625ecbdf5..ed44a306e60e7 100644 --- a/test/automation/src/playwrightDriver.ts +++ b/test/automation/src/playwrightDriver.ts @@ -29,7 +29,8 @@ const vscodeToPlaywrightKey: { [key: string]: string } = { down: 'ArrowDown', left: 'ArrowLeft', home: 'Home', - esc: 'Escape' + esc: 'Escape', + delete: 'Delete' }; let traceCounter = 1; @@ -40,7 +41,7 @@ function buildDriver(browser: playwright.Browser, context: playwright.BrowserCon getWindowIds: () => { return Promise.resolve([1]); }, - capturePage: () => Promise.resolve(''), + capturePage: () => page.screenshot().then(buffer => buffer.toString('base64')), reloadWindow: (windowId) => Promise.resolve(), exitApplication: async () => { try { @@ -81,10 +82,7 @@ function buildDriver(browser: playwright.Browser, context: playwright.BrowserCon await page.mouse.click(x + (xoffset ? xoffset : 0), y + (yoffset ? yoffset : 0)); }, doubleClick: async (windowId, selector) => { - await driver.click(windowId, selector, 0, 0); - await timeout(60); - await driver.click(windowId, selector, 0, 0); - await timeout(100); + await page.dblclick(selector, { delay: 70 }); }, setValue: async (windowId, selector, text) => page.evaluate(`window.driver.setValue('${selector}', '${text}')`).then(undefined), getTitle: (windowId) => page.evaluate(`window.driver.getTitle()`), @@ -206,7 +204,8 @@ export function connect(options: Options = {}): Promise<{ client: IDisposable, d } }); const payloadParam = `[["enableProposedApi",""],["skipWelcome","true"]]`; - await page.goto(`${endpoint}&folder=vscode-remote://localhost:9888${URI.file(workspacePath!).path}&payload=${payloadParam}`); + const match = /http:\/\/(.*)/.exec(endpoint!); + await page.goto(`${endpoint}/?folder=vscode-remote://${match![1]}${URI.file(workspacePath!).path}&payload=${payloadParam}`); const result = { client: { dispose: () => { diff --git a/test/automation/src/terminal.ts b/test/automation/src/terminal.ts index 207f8c90b16e9..bd2e78d89742b 100644 --- a/test/automation/src/terminal.ts +++ b/test/automation/src/terminal.ts @@ -6,7 +6,7 @@ import { Code } from './code'; import { QuickAccess } from './quickaccess'; -const PANEL_SELECTOR = 'div[id="workbench.panel.terminal"]'; +const PANEL_SELECTOR = 'div[id="workbench.parts.panel"]'; const XTERM_SELECTOR = `${PANEL_SELECTOR} .terminal-wrapper`; const XTERM_TEXTAREA = `${XTERM_SELECTOR} textarea.xterm-helper-textarea`; diff --git a/test/integration/browser/src/index.ts b/test/integration/browser/src/index.ts index 31aa5523f7d9c..371799f7a5d7d 100644 --- a/test/integration/browser/src/index.ts +++ b/test/integration/browser/src/index.ts @@ -56,9 +56,9 @@ async function runTestsInBrowser(browserType: BrowserType, endpoint: url.UrlWith const payloadParam = `[["extensionDevelopmentPath","${testExtensionUri}"],["extensionTestsPath","${testFilesUri}"],["enableProposedApi",""],["webviewExternalEndpointCommit","5f19eee5dc9588ca96192f89587b5878b7d7180d"],["skipWelcome","true"]]`; if (path.extname(testWorkspaceUri) === '.code-workspace') { - await page.goto(`${endpoint.href}&workspace=${testWorkspaceUri}&payload=${payloadParam}`); + await page.goto(`${endpoint.href}?workspace=${testWorkspaceUri}&payload=${payloadParam}`); } else { - await page.goto(`${endpoint.href}&folder=${testWorkspaceUri}&payload=${payloadParam}`); + await page.goto(`${endpoint.href}?folder=${testWorkspaceUri}&payload=${payloadParam}`); } await page.exposeFunction('codeAutomationLog', (type: string, args: any[]) => { diff --git a/test/smoke/src/areas/explorer/explorer.test.ts b/test/smoke/src/areas/explorer/explorer.test.ts new file mode 100644 index 0000000000000..56536c531f73c --- /dev/null +++ b/test/smoke/src/areas/explorer/explorer.test.ts @@ -0,0 +1,24 @@ +/*--------------------------------------------------------------------------------------------- + * Copyright (c) Gitpod. All rights reserved. + *--------------------------------------------------------------------------------------------*/ + +import minimist = require('minimist'); +import { Application } from '../../../../automation'; +import { afterSuite, beforeSuite } from '../../utils'; + +export function setup(opts: minimist.ParsedArgs) { + describe('Explorer', () => { + beforeSuite(opts); + + afterSuite(opts); + + it.skip('shows explorer and opens a file', async function () { + const app = this.app as Application; + await app.workbench.explorer.openExplorerView(); + + await new Promise(c => setTimeout(c, 500)); + + await app.workbench.explorer.openFile('app.js'); + }); + }); +} diff --git a/test/smoke/src/areas/extensions/extensions.test.ts b/test/smoke/src/areas/extensions/extensions.test.ts index 5ee13542b00a0..fd6c5ea948bfa 100644 --- a/test/smoke/src/areas/extensions/extensions.test.ts +++ b/test/smoke/src/areas/extensions/extensions.test.ts @@ -4,7 +4,7 @@ *--------------------------------------------------------------------------------------------*/ import minimist = require('minimist'); -import { Application, Quality } from '../../../../automation'; +import { Application } from '../../../../automation'; import { afterSuite, beforeSuite } from '../../utils'; export function setup(opts: minimist.ParsedArgs) { @@ -15,9 +15,9 @@ export function setup(opts: minimist.ParsedArgs) { it(`install and enable vscode-smoketest-check extension`, async function () { const app = this.app as Application; - if (app.quality === Quality.Dev) { - this.skip(); - } + // if (app.quality === Quality.Dev) { + // this.skip(); + // } await app.workbench.extensions.openExtensionsViewlet(); @@ -30,5 +30,22 @@ export function setup(opts: minimist.ParsedArgs) { await app.workbench.quickaccess.runCommand('Smoke Test Check'); }); + it(`install and enable smoketest-check-web extension in web worker`, async function () { + const app = this.app as Application; + + // if (app.quality === Quality.Dev) { + // this.skip(); + // } + + await app.workbench.extensions.openExtensionsViewlet(); + + await app.workbench.extensions.installExtension('jeanp413.smoketest-check-web', true); + + // Close extension editor because keybindings dispatch is not working when web views are opened and focused + // https://github.com/microsoft/vscode/issues/110276 + await app.workbench.extensions.closeExtension('smoketest-check-web'); + + await app.workbench.quickaccess.runCommand('Smoke Test Check Web'); + }); }); } diff --git a/test/smoke/src/areas/statusbar/statusbar.test.ts b/test/smoke/src/areas/statusbar/statusbar.test.ts index 954cefedcc300..66cfb36e3c955 100644 --- a/test/smoke/src/areas/statusbar/statusbar.test.ts +++ b/test/smoke/src/areas/statusbar/statusbar.test.ts @@ -16,9 +16,9 @@ export function setup(opts: minimist.ParsedArgs) { const app = this.app as Application; await app.workbench.statusbar.waitForStatusbarElement(StatusBarElement.BRANCH_STATUS); - if (app.quality !== Quality.Dev) { - await app.workbench.statusbar.waitForStatusbarElement(StatusBarElement.FEEDBACK_ICON); - } + // if (app.quality !== Quality.Dev) { + // await app.workbench.statusbar.waitForStatusbarElement(StatusBarElement.FEEDBACK_ICON); + // } await app.workbench.statusbar.waitForStatusbarElement(StatusBarElement.SYNC_STATUS); await app.workbench.statusbar.waitForStatusbarElement(StatusBarElement.PROBLEMS_STATUS); @@ -89,7 +89,7 @@ export function setup(opts: minimist.ParsedArgs) { await app.workbench.statusbar.waitForEOL('CRLF'); }); - it(`verifies that 'Tweet us feedback' pop-up appears when clicking on 'Feedback' icon`, async function () { + it.skip(`verifies that 'Tweet us feedback' pop-up appears when clicking on 'Feedback' icon`, async function () { const app = this.app as Application; if (app.quality === Quality.Dev) { diff --git a/test/smoke/src/areas/terminal/terminal.test.ts b/test/smoke/src/areas/terminal/terminal.test.ts new file mode 100644 index 0000000000000..413071f9b6056 --- /dev/null +++ b/test/smoke/src/areas/terminal/terminal.test.ts @@ -0,0 +1,45 @@ +/*--------------------------------------------------------------------------------------------- + * Copyright (c) Gitpod. All rights reserved. + *--------------------------------------------------------------------------------------------*/ + +import minimist = require('minimist'); +import * as path from 'path'; +import { Application, Quality } from '../../../../automation'; +import { afterSuite, beforeSuite } from '../../utils'; + +export function setup(opts: minimist.ParsedArgs) { + describe('Terminal', () => { + beforeSuite(opts); + + afterSuite(opts); + + it('shows terminal and runs command', async function () { + const app = this.app as Application; + + // Canvas may cause problems when running in a container + await app.workbench.settingsEditor.addUserSetting('terminal.integrated.gpuAcceleration', '"off"'); + + await app.workbench.terminal.showTerminal(); + await app.workbench.terminal.runCommand('ls'); + await app.workbench.terminal.waitForTerminalText(lines => lines.some(l => l.includes('app.js'))); + }); + + it('shows terminal and runs cli command', async function () { + const app = this.app as Application; + + if (app.quality !== Quality.Dev) { + this.skip(); + } + + const rootPath = process.env['VSCODE_REPOSITORY']; + if (!rootPath) { + throw new Error('VSCODE_REPOSITORY env variable not found'); + } + + const cliPath = path.join(rootPath, 'out', 'server-cli.js'); + + await app.workbench.terminal.runCommand(`node ${cliPath} app.js`); + await app.workbench.editors.waitForActiveTab('app.js'); + }); + }); +} diff --git a/test/smoke/src/main.ts b/test/smoke/src/main.ts index b05d6742d0159..cce2c4ed1d13f 100644 --- a/test/smoke/src/main.ts +++ b/test/smoke/src/main.ts @@ -27,6 +27,8 @@ import { setup as setupDataExtensionTests } from './areas/extensions/extensions. import { setup as setupDataMultirootTests } from './areas/multiroot/multiroot.test'; import { setup as setupDataLocalizationTests } from './areas/workbench/localization.test'; import { setup as setupLaunchTests } from './areas/workbench/launch.test'; +import { setup as setupDataExplorerTests } from './areas/explorer/explorer.test'; +import { setup as setupDataTerminalTests } from './areas/terminal/terminal.test'; const testDataPath = path.join(os.tmpdir(), 'vscsmoke'); if (fs.existsSync(testDataPath)) { @@ -343,12 +345,14 @@ if (!opts.web && opts['build'] && !opts['remote']) { describe(`VSCode Smoke Tests (${opts.web ? 'Web' : 'Electron'})`, () => { if (!opts.web) { setupDataLossTests(opts); } if (!opts.web) { setupDataPreferencesTests(opts); } + setupDataExplorerTests(opts); setupDataSearchTests(opts); setupDataNotebookTests(opts); setupDataLanguagesTests(opts); setupDataEditorTests(opts); setupDataStatusbarTests(opts); setupDataExtensionTests(opts); + setupDataTerminalTests(opts); if (!opts.web) { setupDataMultirootTests(opts); } if (!opts.web) { setupDataLocalizationTests(opts); } if (!opts.web) { setupLaunchTests(); }