diff --git a/.eslintrc.json b/.eslintrc.json index 3b8d12542e55d..6bc43e6b40f9a 100644 --- a/.eslintrc.json +++ b/.eslintrc.json @@ -632,6 +632,18 @@ } ] } + }, + { + "files": [ + "build/gulpfile.gitpod.js", + "src/vs/gitpod/*", + "extensions/gitpod*/**" + ], + "rules": { + "header/header": [ + "off" + ] + } } ] } diff --git a/.gitpod.Dockerfile b/.gitpod.Dockerfile new file mode 100644 index 0000000000000..e98ad04da9ad4 --- /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 16 \ + && nvm use 16 \ + && nvm alias default 16" + +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..bea6d7c68b5a0 --- /dev/null +++ b/.gitpod.yml @@ -0,0 +1,36 @@ +image: + file: .gitpod.Dockerfile +ports: + - port: 9888 + 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 + cd ./extensions + yarn watch + name: watch extension + - command: | + export NODE_ENV=development + export VSCODE_DEV=1 + gp sync-await init + ./scripts/code-server.sh --without-connection-token + name: run app + openMode: split-right +github: + prebuilds: + pullRequestsFromForks: true +vscode: + extensions: + - dbaeumer.vscode-eslint + - svelte.svelte-vscode diff --git a/.vscode/launch.json b/.vscode/launch.json index 3bf8c67198fa4..0f2791012a252 100644 --- a/.vscode/launch.json +++ b/.vscode/launch.json @@ -1,6 +1,36 @@ { "version": "0.1.0", "configurations": [ + { + "name": "Launch gitpod-web", + "type": "extensionHost", + "request": "launch", + "runtimeExecutable": "${execPath}", + "args": [ + "${workspaceFolder}", + "--extensionDevelopmentPath=${workspaceRoot}/extensions/gitpod-web", + "--log=debug" + ], + "outFiles": [ + "${workspaceRoot}/extensions/gitpod-shared/out/**/*.js", + "${workspaceRoot}/extensions/gitpod-web/out/**/*.js", + ] + }, + { + "name": "Launch gitpod-remote", + "type": "extensionHost", + "request": "launch", + "runtimeExecutable": "${execPath}", + "args": [ + "${workspaceFolder}", + "--extensionDevelopmentPath=${workspaceRoot}/extensions/gitpod-remote", + "--log=debug" + ], + "outFiles": [ + "${workspaceRoot}/extensions/gitpod-shared/out/**/*.js", + "${workspaceRoot}/extensions/gitpod-remote/out/**/*.js", + ] + }, { "type": "node", "request": "launch", @@ -272,7 +302,14 @@ "presentation": { "group": "0_vscode", "order": 2 - } + }, + "env": { + "VSCODE_DEV": "1", + "NODE_ENV": "development" + }, + "args": [ + "--without-connection-token" + ] }, { "type": "node", diff --git a/BUILD.yaml b/BUILD.yaml new file mode 100644 index 0000000000000..e1e418e348c0a --- /dev/null +++ b/BUILD.yaml @@ -0,0 +1,17 @@ +packages: + - name: install + type: generic + srcs: + - "**" + config: + commands: + - ["yarn"] + - name: init + type: generic + deps: + - ":install" + config: + commands: + - ["yarn", "--cwd", "./install/build", "compile"] + - ["yarn", "--cwd", "./install", "compile"] + - ["yarn", "--cwd", "./install", "download-builtin-extensions"] diff --git a/build/.moduleignore b/build/.moduleignore index 1a1461b23301e..52d0a7989b2db 100644 --- a/build/.moduleignore +++ b/build/.moduleignore @@ -168,4 +168,9 @@ xterm-addon-*/fixtures/** xterm-addon-*/out/** xterm-addon-*/out-test/** - +# TODO: Remove this once gitpod-protocol package doesn't ship source code +@gitpod/gitpod-protocol/src/** +@gitpod/gitpod-protocol/data/** +@gitpod/gitpod-protocol/node_modules/** +@gitpod/gitpod-protocol/lib/*.d.ts +@gitpod/gitpod-protocol/lib/*.d.ts.map diff --git a/build/.webignore b/build/.webignore index afb0cee5808e1..7f44f1a7931e5 100644 --- a/build/.webignore +++ b/build/.webignore @@ -46,6 +46,15 @@ xterm-addon-webgl/out/** !@microsoft/applicationinsights-core-js/browser/applicationinsights-core-js.min.js !@microsoft/applicationinsights-shims/dist/umd/applicationinsights-shims.min.js +@improbable-eng/** +!@improbable-eng/grpc-web/dist/grpc-web-client.umd.js +@gitpod/** +!@gitpod/local-app-api-grpcweb/lib/localapp.js +!@gitpod/ide-metrics-api-grpcweb/lib/index.js +browser-headers/** +google-protobuf/** +@zip.js/** +!@zip.js/zip.js/dist/zip-no-worker-deflate.min.js diff --git a/build/gulpfile.gitpod.js b/build/gulpfile.gitpod.js new file mode 100644 index 0000000000000..c1f1abd0fc014 --- /dev/null +++ b/build/gulpfile.gitpod.js @@ -0,0 +1,80 @@ +/*!-------------------------------------------------------- +* Copyright (C) Gitpod. All rights reserved. +*--------------------------------------------------------*/ + +'use strict'; + +const promisify = require('util').promisify; +const cp = require('child_process'); +const argv = require('yargs').argv; +const vsce = require('vsce'); +const gulp = require('gulp'); +const path = require('path'); +const es = require('event-stream'); +const util = require('./lib/util'); +const task = require('./lib/task'); +const rename = require('gulp-rename'); +const ext = require('./lib/extensions'); + +gulp.task(task.define('watch-init', require('./lib/compilation').watchTask('out', false))); + +const extensionsPath = path.join(path.dirname(__dirname), 'extensions'); +const marketplaceExtensions = ['gitpod-remote']; +const outMarketplaceExtensions = 'out-gitpod-marketplace'; +const cleanMarketplaceExtensions = task.define('clean-gitpod-marketplace-extensions', util.rimraf(outMarketplaceExtensions)); +const bumpMarketplaceExtensions = task.define('bump-marketplace-extensions', () => { + if ('new-version' in argv && argv['new-version']) { + const newVersion = argv['new-version']; + console.log(newVersion); + return Promise.allSettled(marketplaceExtensions.map(async extensionName => { + const { stderr } = await promisify(cp.exec)(`yarn version --new-version ${newVersion} --cwd ${path.join(extensionsPath, extensionName)} --no-git-tag-version`, { encoding: 'utf8' }); + if (stderr) { + throw new Error('failed to bump up version: ' + stderr); + } + })); + } +}); + +const bundlePortsWebview = task.define('bundle-remote-ports-webview', async () => { + await promisify(cp.exec)(`yarn --cwd ${path.join(extensionsPath, 'gitpod-remote')} run build:webview`, { encoding: 'utf8' }); + gulp.src([`${path.join(extensionsPath, 'gitpod-remote')}/public/**/*`]).pipe(gulp.dest(path.join(outMarketplaceExtensions, 'gitpod-remote/public/'))); +}); +gulp.task(bundlePortsWebview); +for (const extensionName of marketplaceExtensions) { + const cleanExtension = task.define('gitpod:clean-extension:' + extensionName, util.rimraf(path.join(outMarketplaceExtensions, extensionName))); + const bumpExtension = task.define('gitpod:bump-extension:' + extensionName, async () => { + if ('new-version' in argv && argv['new-version']) { + const newVersion = argv['new-version']; + const { stderr } = await promisify(cp.exec)(`yarn version --new-version ${newVersion} --cwd ${path.join(extensionsPath, extensionName)} --no-git-tag-version`, { encoding: 'utf8' }); + if (stderr) { + throw new Error('failed to bump up version: ' + stderr); + } + } + }); + const bundleExtension = task.define('gitpod:bundle-extension:' + extensionName, task.series( + cleanExtension, + bumpExtension, + () => + ext.minifyExtensionResources( + ext.fromLocal(path.join(extensionsPath, extensionName), false) + .pipe(rename(p => p.dirname = `${extensionName}/${p.dirname}`)) + ).pipe(gulp.dest(outMarketplaceExtensions)) + )); + gulp.task(bundleExtension); + const publishExtension = task.define('gitpod:publish-extension:' + extensionName, task.series( + bundleExtension, + bundlePortsWebview, + () => vsce.publish({ + cwd: path.join(outMarketplaceExtensions, extensionName) + }) + )); + gulp.task(publishExtension); + const packageExtension = task.define('gitpod:package-extension:' + extensionName, task.series( + bundleExtension, + bundlePortsWebview, + () => vsce.createVSIX({ + cwd: path.join(outMarketplaceExtensions, extensionName) + }) + )); + gulp.task(packageExtension); +} diff --git a/build/gulpfile.reh.js b/build/gulpfile.reh.js index 7475e04d6e502..b417ab263d4bb 100644 --- a/build/gulpfile.reh.js +++ b/build/gulpfile.reh.js @@ -364,7 +364,12 @@ function tweakProductForServerWeb(product) { loaderConfig: optimize.loaderConfig(), inlineAmdImages: true, bundleInfo: undefined, - fileContentMapper: createVSCodeWebFileContentMapper('.build/extensions', type === 'reh-web' ? tweakProductForServerWeb(product) : product) + fileContentMapper: createVSCodeWebFileContentMapper('.build/extensions', type === 'reh-web' ? tweakProductForServerWeb(product) : product), + header: [ + '/*!-----------------------------------------', + ' * Copyright (c) Gitpod. All rights reserved.', + ' *-----------------------------------------*/' + ].join('\n') }, commonJS: { src: 'out-build', @@ -408,12 +413,61 @@ function tweakProductForServerWeb(product) { )); gulp.task(serverTaskCI); + /** + * This dummy extension is a mock the for built-in extension called `github-authentication`. + * In Gitpod we don't use the built-in extension (it's implemented inside gitpod-web extension) + * but if this one is missing, it breaks the GitHub Authentication for extensions that depend + * explicitly on `github-authentication` like `github.vscode-pull-request-github` + */ + const createDummyGitHubAuthExtensionTask = task.define('createDummyGitHubAuthExtensionTask', (done) => { + const dir = path.join(BUILD_ROOT, destinationFolderName, 'extensions', 'github-authentication'); + + if (!fs.existsSync(dir)) { + fs.mkdirSync(dir, { recursive: true }); + } + + const packageJsonContent = { + name: 'github-authentication', + displayName: 'GitHub Authentication', + description: 'Gitpod Override', // TODO: change + publisher: 'vscode', + license: 'MIT', + version: '0.0.2', + engines: { + vscode: '^1.41.0', + }, + categories: ['Other'], + api: 'none', + extensionKind: ['ui', 'workspace'], + activationEvents: [ + 'onAuthenticationRequest:github', + ], + capabilities: { + virtualWorkspaces: true, + untrustedWorkspaces: { + supported: true, + }, + }, + main: './extension.js', + }; + + const extensionJsContent = `module.exports = function activate() {}`; + + fs.writeFileSync(path.join(dir, 'package.json'), JSON.stringify(packageJsonContent, null, 2)); + fs.writeFileSync(path.join(dir, 'extension.js'), extensionJsContent); + + done(); + }); + + gulp.task(createDummyGitHubAuthExtensionTask); + const serverTask = task.define(`vscode-${type}${dashed(platform)}${dashed(arch)}${dashed(minified)}`, task.series( compileBuildTask, compileExtensionsBuildTask, compileExtensionMediaBuildTask, minified ? minifyTask : optimizeTask, - serverTaskCI + serverTaskCI, + createDummyGitHubAuthExtensionTask )); gulp.task(serverTask); }); diff --git a/build/gulpfile.vscode.web.js b/build/gulpfile.vscode.web.js index 6de4d03d414ae..29c1aa191f020 100644 --- a/build/gulpfile.vscode.web.js +++ b/build/gulpfile.vscode.web.js @@ -34,7 +34,7 @@ const version = (quality && quality !== 'stable') ? `${packageJson.version}-${qu const vscodeWebResourceIncludes = [ // Workbench 'out-build/vs/{base,platform,editor,workbench}/**/*.{svg,png,jpg,mp3}', - 'out-build/vs/code/browser/workbench/*.html', + 'out-build/vs/gitpod/browser/workbench/*.html', 'out-build/vs/base/browser/ui/codicons/codicon/**/*.ttf', 'out-build/vs/**/markdown.css', @@ -176,7 +176,7 @@ const optimizeVSCodeWebTask = task.define('optimize-vscode-web', task.series( const minifyVSCodeWebTask = task.define('minify-vscode-web', task.series( optimizeVSCodeWebTask, util.rimraf('out-vscode-web-min'), - optimize.minifyTask('out-vscode-web', `https://ticino.blob.core.windows.net/sourcemaps/${commit}/core`) + optimize.minifyTask('out-vscode-web') )); gulp.task(minifyVSCodeWebTask); @@ -192,7 +192,7 @@ function packageTask(sourceFolderName, destinationFolderName) { const extensions = gulp.src('.build/web/extensions/**', { base: '.build/web', dot: true }); const sources = es.merge(src, extensions) - .pipe(filter(['**', '!**/*.js.map'], { dot: true })); + .pipe(filter(['**'], { dot: true })); const name = product.nameShort; const packageJsonStream = gulp.src(['remote/web/package.json'], { base: 'remote/web' }) diff --git a/build/hygiene.js b/build/hygiene.js index 67f074c4ac094..f9234bc061f1a 100644 --- a/build/hygiene.js +++ b/build/hygiene.js @@ -87,13 +87,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('gitpod') === -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 8a5e93cbb4c26..bb080620ace3c 100644 --- a/build/lib/compilation.js +++ b/build/lib/compilation.js @@ -19,6 +19,9 @@ const os = require("os"); const File = require("vinyl"); const task = require("./task"); const watch = require('./watch'); +const packageJson = require('../../package.json'); +const productJson = require('../../product.json'); +const replace = require('gulp-replace'); // --- gulp-tsb: compile and transpile -------------------------------- const reporter = (0, reporter_1.createReporter)(); function getTypeScriptCompilerOptions(src) { @@ -53,8 +56,19 @@ function createCompile(src, build, emitError, transpileOnly) { const utf8Filter = util.filter(data => /(\/|\\)test(\/|\\).*utf8/.test(data.path)); const tsFilter = util.filter(data => /\.ts$/.test(data.path)); const noDeclarationsFilter = util.filter(data => !(/\.d\.ts$/.test(data.path))); + const productJsFilter = util.filter(data => !build && data.path.endsWith('vs/platform/product/common/product.ts')); + const productConfiguration = JSON.stringify({ + ...productJson, + version: `${packageJson.version}-dev`, + nameShort: `${productJson.nameShort} Dev`, + nameLong: `${productJson.nameLong} Dev`, + dataFolderName: `${productJson.dataFolderName}-dev` + }); const input = es.through(); const output = input + .pipe(productJsFilter) + .pipe(replace(/{\s*\/\*BUILD->INSERT_PRODUCT_CONFIGURATION\*\/\s*}/, productConfiguration, { skipBinary: true })) + .pipe(productJsFilter.restore) .pipe(utf8Filter) .pipe(bom()) // this is required to preserve BOM in test files that loose it otherwise .pipe(utf8Filter.restore) diff --git a/build/lib/compilation.ts b/build/lib/compilation.ts index b9769822d73f1..82e9c1a8ab368 100644 --- a/build/lib/compilation.ts +++ b/build/lib/compilation.ts @@ -18,6 +18,9 @@ import ts = require('typescript'); import * as File from 'vinyl'; import * as task from './task'; const watch = require('./watch'); +const packageJson = require('../../package.json'); +const productJson = require('../../product.json'); +const replace = require('gulp-replace'); // --- gulp-tsb: compile and transpile -------------------------------- @@ -63,8 +66,20 @@ function createCompile(src: string, build: boolean, emitError: boolean, transpil const tsFilter = util.filter(data => /\.ts$/.test(data.path)); const noDeclarationsFilter = util.filter(data => !(/\.d\.ts$/.test(data.path))); + const productJsFilter = util.filter(data => !build && data.path.endsWith('vs/platform/product/common/product.ts')); + const productConfiguration = JSON.stringify({ + ...productJson, + version: `${packageJson.version}-dev`, + nameShort: `${productJson.nameShort} Dev`, + nameLong: `${productJson.nameLong} Dev`, + dataFolderName: `${productJson.dataFolderName}-dev` + }); + const input = es.through(); const output = input + .pipe(productJsFilter) + .pipe(replace(/{\s*\/\*BUILD->INSERT_PRODUCT_CONFIGURATION\*\/\s*}/, productConfiguration, { skipBinary: true })) + .pipe(productJsFilter.restore) .pipe(utf8Filter) .pipe(bom()) // this is required to preserve BOM in test files that loose it otherwise .pipe(utf8Filter.restore) diff --git a/build/lib/extensions.js b/build/lib/extensions.js index 8e7f238bb3c70..46bf0e67ea3e7 100644 --- a/build/lib/extensions.js +++ b/build/lib/extensions.js @@ -4,7 +4,7 @@ * Licensed under the MIT License. See License.txt in the project root for license information. *--------------------------------------------------------------------------------------------*/ Object.defineProperty(exports, "__esModule", { value: true }); -exports.buildExtensionMedia = exports.webpackExtensions = exports.translatePackageJSON = exports.scanBuiltinExtensions = exports.packageMarketplaceExtensionsStream = exports.packageLocalExtensionsStream = exports.fromGithub = exports.fromMarketplace = void 0; +exports.buildExtensionMedia = exports.webpackExtensions = exports.translatePackageJSON = exports.scanBuiltinExtensions = exports.packageMarketplaceExtensionsStream = exports.packageLocalExtensionsStream = exports.fromGithub = exports.fromMarketplace = exports.fromLocal = exports.minifyExtensionResources = void 0; const es = require("event-stream"); const fs = require("fs"); const cp = require("child_process"); @@ -46,6 +46,7 @@ function minifyExtensionResources(input) { })) .pipe(jsonFilter.restore); } +exports.minifyExtensionResources = minifyExtensionResources; function updateExtensionPackageJSON(input, update) { const packageJsonFilter = filter('extensions/*/package.json', { restore: true }); return input @@ -77,6 +78,7 @@ function fromLocal(extensionPath, forWeb) { } return input; } +exports.fromLocal = fromLocal; function fromLocalWebpack(extensionPath, webpackConfigFileName) { const result = es.through(); const packagedDependencies = []; @@ -246,6 +248,10 @@ const excludedExtensions = [ 'vscode-test-resolver', 'ms-vscode.node-debug', 'ms-vscode.node-debug2', + 'github-authentication', + 'gitpod-shared', + 'gitpod-remote', + 'gitpod', ]; const marketplaceWebExtensionsExclude = new Set([ 'ms-vscode.node-debug', diff --git a/build/lib/extensions.ts b/build/lib/extensions.ts index 6cad21018d065..a5c8a1ed2fc89 100644 --- a/build/lib/extensions.ts +++ b/build/lib/extensions.ts @@ -32,7 +32,7 @@ const root = path.dirname(path.dirname(__dirname)); const commit = getVersion(root); const sourceMappingURLBase = `https://ticino.blob.core.windows.net/sourcemaps/${commit}`; -function minifyExtensionResources(input: Stream): Stream { +export function minifyExtensionResources(input: Stream): Stream { const jsonFilter = filter(['**/*.json', '**/*.code-snippets'], { restore: true }); return input .pipe(jsonFilter) @@ -62,7 +62,7 @@ function updateExtensionPackageJSON(input: Stream, update: (data: any) => any): .pipe(packageJsonFilter.restore); } -function fromLocal(extensionPath: string, forWeb: boolean): Stream { +export function fromLocal(extensionPath: string, forWeb: boolean): Stream { const webpackConfigFileName = forWeb ? 'extension-browser.webpack.config.js' : 'extension.webpack.config.js'; const isWebPacked = fs.existsSync(path.join(extensionPath, webpackConfigFileName)); @@ -287,6 +287,10 @@ const excludedExtensions = [ 'vscode-test-resolver', 'ms-vscode.node-debug', 'ms-vscode.node-debug2', + 'github-authentication', + 'gitpod-shared', + 'gitpod-remote', + 'gitpod', ]; const marketplaceWebExtensionsExclude = new Set([ diff --git a/doc/DEV.md b/doc/DEV.md new file mode 100644 index 0000000000000..575b31e3d814a --- /dev/null +++ b/doc/DEV.md @@ -0,0 +1,31 @@ +## Observability + +### Metrics defintion +- Declare new metrics or update existing in https://github.com/gitpod-io/gitpod/blob/ad355c4d9abd858a44daf15f9bd6747976142911/install/installer/pkg/components/ide-metrics/configmap.go +- Create a new branch and push, wait for https://werft.gitpod-dev.com/ to create a preview env. + +### Collecting metrics +- Convert VS Code telemetry to metrics in https://github.com/gitpod-io/openvscode-server/blob/63796b8c6eca9bcaf36b90ae1e96dae32638bab6/src/vs/gitpod/common/insightsHelper.ts#L35. + +### Testing from sources +- Add to product.json (don't commit!): +```jsonc +"gitpodPreview": { + "host": "", + // optionally to log to stdout or browser console + "log": { + "metrics": true, + "analytics": false, + } +} +``` +- Restart VS Code Server and open VS Code preview page to trigger telemetry events. +- In dev workspace for gitpod-io/gitpod run `./dev/preview/portforward-monitoring-satellite.sh -c harvester` +- Navigate to a printed Grafana link, open Explorer view, select prometheus as a data source and query for metrics. + +### Integration testing + +- Commit changes in this repo. +- Update codeCommit in WORKSPACE.yaml in gitpod-io/gitpod and push. +- Wait for https://werft.gitpod-dev.com/ to update preview envs. +- Test the complete integration. diff --git a/extensions/.gitignore b/extensions/.gitignore new file mode 100644 index 0000000000000..cc1b7f164211e --- /dev/null +++ b/extensions/.gitignore @@ -0,0 +1 @@ +tsconfig.tsbuildinfo diff --git a/extensions/github/package.json b/extensions/github/package.json index bdd04a2e4c867..6bcfae440c38f 100644 --- a/extensions/github/package.json +++ b/extensions/github/package.json @@ -50,10 +50,6 @@ } ], "continueEditSession": [ - { - "command": "github.openOnVscodeDev", - "when": "github.hasGitHubRepo" - } ], "menus": { "commandPalette": [ diff --git a/extensions/gitpod-remote/.gitignore b/extensions/gitpod-remote/.gitignore new file mode 100644 index 0000000000000..364fdec1aa19d --- /dev/null +++ b/extensions/gitpod-remote/.gitignore @@ -0,0 +1 @@ +public/ diff --git a/extensions/gitpod-remote/.vscodeignore b/extensions/gitpod-remote/.vscodeignore new file mode 100644 index 0000000000000..f65bc15d8391a --- /dev/null +++ b/extensions/gitpod-remote/.vscodeignore @@ -0,0 +1,9 @@ +.gitignore +src/** +!src/common/config.json +out/** +build/** +extension.webpack.config.js +extension-browser.webpack.config.js +tsconfig.json +yarn.lock diff --git a/extensions/gitpod-remote/README.md b/extensions/gitpod-remote/README.md new file mode 100644 index 0000000000000..c5480b8b2421a --- /dev/null +++ b/extensions/gitpod-remote/README.md @@ -0,0 +1,29 @@ +# Gitpod Remote + +Gitpod is an open-source Kubernetes application for automated and ready-to-code development environments that blends in your existing workflow. It enables you to describe your dev environment as code and start instant and fresh development environments for each new task directly from your browser. + +Tightly integrated with GitLab, GitHub, and Bitbucket, Gitpod automatically and continuously prebuilds dev environments for all your branches. As a result, team members can instantly start coding with fresh, ephemeral and fully-compiled dev environments - no matter if you are building a new feature, want to fix a bug or do a code review. + +![image](https://user-images.githubusercontent.com/120486/116072013-40a8aa00-a697-11eb-846b-89e6f5e1a82e.png) + +## Getting Started + +[Start a new Gitpod workspace](https://www.gitpod.io/docs/getting-started) and use F1 -> `Open in VS Code` command to connect to it from VS Code Desktop. + +## Documentation + +All documentation can be found on https://www.gitpod.io/docs. +For example, see [Introduction](https://www.gitpod.io/docs) and [Getting Started](https://www.gitpod.io/docs/getting-started) sections. πŸ“š + +## Questions + +For questions and support please use the [community forum](http://community.gitpod.io) or the [community discord server](https://www.gitpod.io/chat). +Join the conversation, and connect with other community members. πŸ’¬ + +You can also follow [`@gitpod`](https://twitter.com/gitpod) for announcements and updates from our team. + +## Issues + +The issue tracker is used for tracking **bug reports** and **feature requests** for the Gitpod open source project as well as planning current and future development efforts. πŸ—ΊοΈ + +You can upvote [popular feature requests](https://github.com/gitpod-io/gitpod/issues?q=is%3Aissue+is%3Aopen+sort%3Areactions-%2B1-desc) or [create a new one](https://github.com/gitpod-io/gitpod/issues/new?template=feature_request.md). diff --git a/extensions/gitpod-remote/extension.webpack.config.js b/extensions/gitpod-remote/extension.webpack.config.js new file mode 100644 index 0000000000000..08296ca0684c1 --- /dev/null +++ b/extensions/gitpod-remote/extension.webpack.config.js @@ -0,0 +1,21 @@ +/*--------------------------------------------------------------------------------------------- + * Copyright (c) Microsoft Corporation. All rights reserved. + * Licensed under the MIT License. See License.txt in the project root for license information. + *--------------------------------------------------------------------------------------------*/ + +//@ts-check + +'use strict'; + +const withDefaults = require('../shared.webpack.config'); + +module.exports = withDefaults({ + context: __dirname, + entry: { + extension: './src/extension.ts', + cli: './src/cli.ts', + }, + externals: { + 'keytar': 'commonjs keytar' + } +}); diff --git a/extensions/gitpod-remote/package.json b/extensions/gitpod-remote/package.json new file mode 100644 index 0000000000000..ea8e47ba82ddc --- /dev/null +++ b/extensions/gitpod-remote/package.json @@ -0,0 +1,314 @@ +{ + "name": "gitpod-remote-ssh", + "displayName": "%displayName%", + "description": "%description%", + "publisher": "gitpod", + "version": "0.0.38", + "license": "MIT", + "preview": true, + "icon": "resources/gitpod.png", + "repository": { + "type": "git", + "url": "https://github.com/gitpod-io/openvscode-server.git" + }, + "bugs": { + "url": "https://github.com/gitpod-io/gitpod/issues" + }, + "engines": { + "vscode": "^1.58.2" + }, + "categories": [ + "Other" + ], + "keywords": [ + "remote development", + "remote" + ], + "extensionKind": [ + "workspace" + ], + "capabilities": { + "untrustedWorkspaces": { + "supported": true + } + }, + "activationEvents": [ + "*" + ], + "main": "./out/extension.js", + "contributes": { + "commands": [ + { + "command": "gitpod.stop.ws", + "title": "%stopWorkspace%", + "enablement": "gitpod.inWorkspace == true && gitpod.workspaceOwned == true" + }, + { + "command": "gitpod.open.settings", + "title": "%openSettings%", + "enablement": "gitpod.inWorkspace == true" + }, + { + "command": "gitpod.open.accessControl", + "title": "%openAccessControl%", + "enablement": "gitpod.inWorkspace == true" + }, + { + "command": "gitpod.open.context", + "title": "%openContext%", + "enablement": "gitpod.inWorkspace == true" + }, + { + "command": "gitpod.open.dashboard", + "title": "%openDashboard%", + "enablement": "gitpod.inWorkspace == true" + }, + { + "command": "gitpod.open.documentation", + "title": "%openDocumentation%", + "enablement": "gitpod.inWorkspace == true" + }, + { + "command": "gitpod.open.twitter", + "title": "%openTwitter%", + "enablement": "gitpod.inWorkspace == true" + }, + { + "command": "gitpod.open.discord", + "title": "%openDiscord%", + "enablement": "gitpod.inWorkspace == true" + }, + { + "command": "gitpod.reportIssue", + "title": "%reportIssue%", + "enablement": "gitpod.inWorkspace == true" + }, + { + "command": "gitpod.upgradeSubscription", + "title": "%upgradeSubscription%", + "enablement": "gitpod.inWorkspace == true && gitpod.workspaceOwned == true" + }, + { + "command": "gitpod.ExtendTimeout", + "title": "%extendTimeout%", + "enablement": "gitpod.inWorkspace == true && gitpod.workspaceOwned == true" + }, + { + "command": "gitpod.takeSnapshot", + "title": "%takeSnapshot%", + "enablement": "gitpod.inWorkspace == true && gitpod.workspaceOwned == true" + }, + { + "command": "gitpod.shareWorkspace", + "title": "%shareWorkspace%", + "enablement": "gitpod.inWorkspace == true && gitpod.workspaceOwned == true && gitpod.workspaceShared == false" + }, + { + "command": "gitpod.stopSharingWorkspace", + "title": "%stopSharingWorkspace%", + "enablement": "gitpod.inWorkspace == true && gitpod.workspaceOwned == true && gitpod.workspaceShared == true" + }, + { + "command": "gitpod.openInBrowser", + "title": "%openInBrowser%", + "enablement": "gitpod.inWorkspace == true && gitpod.UIKind == 'desktop'" + }, + { + "command": "gitpod.ports.openBrowser", + "title": "%openBrowser%", + "icon": "$(globe)" + }, + { + "command": "gitpod.ports.retryAutoExpose", + "title": "%retryAutoExpose%", + "icon": "$(refresh)" + }, + { + "command": "gitpod.ports.preview", + "title": "%openPreview%", + "icon": "$(open-preview)" + }, + { + "command": "gitpod.ports.makePrivate", + "title": "%makePrivate%", + "icon": "$(unlock)" + }, + { + "command": "gitpod.ports.makePublic", + "title": "%makePublic%", + "icon": "$(lock)" + }, + { + "command": "gitpod.ports.tunnelNetwork", + "title": "%tunnelNetwork%", + "icon": "$(eye)" + }, + { + "command": "gitpod.ports.tunnelHost", + "title": "%tunnelHost%", + "icon": "$(eye-closed)" + } + ], + "commandPalette": [ + { + "command": "gitpod.ports.preview", + "when": "false" + }, + { + "command": "gitpod.ports.openBrowser", + "when": "false" + }, + { + "command": "gitpod.ports.retryAutoExpose", + "when": "false" + }, + { + "command": "gitpod.ports.makePublic", + "when": "false" + }, + { + "command": "gitpod.ports.makePrivate", + "when": "false" + }, + { + "command": "gitpod.ports.tunnelNetwork", + "when": "false" + }, + { + "command": "gitpod.ports.tunnelHost", + "when": "false" + } + ], + "menus": { + "statusBar/remoteIndicator": [ + { + "command": "gitpod.stop.ws", + "group": "remote_00_gitpod_navigation@10", + "when": "gitpod.inWorkspace == true && gitpod.workspaceOwned == true" + }, + { + "command": "gitpod.open.settings", + "group": "remote_00_gitpod_navigation@20", + "when": "gitpod.inWorkspace == true" + }, + { + "command": "gitpod.open.accessControl", + "group": "remote_00_gitpod_navigation@30", + "when": "gitpod.inWorkspace == true" + }, + { + "command": "gitpod.open.context", + "group": "remote_00_gitpod_navigation@40", + "when": "gitpod.inWorkspace == true" + }, + { + "command": "gitpod.open.dashboard", + "group": "remote_00_gitpod_navigation@50", + "when": "gitpod.inWorkspace == true" + }, + { + "command": "gitpod.open.documentation", + "group": "remote_00_gitpod_navigation@60", + "when": "gitpod.inWorkspace == true" + }, + { + "command": "gitpod.open.twitter", + "group": "remote_00_gitpod_navigation@70", + "when": "gitpod.inWorkspace == true" + }, + { + "command": "gitpod.open.discord", + "group": "remote_00_gitpod_navigation@80", + "when": "gitpod.inWorkspace == true" + }, + { + "command": "gitpod.reportIssue", + "group": "remote_00_gitpod_navigation@90", + "when": "gitpod.inWorkspace == true" + }, + { + "command": "gitpod.upgradeSubscription", + "group": "remote_00_gitpod_navigation@100", + "when": "gitpod.inWorkspace == true && gitpod.workspaceOwned == true" + }, + { + "command": "gitpod.ExtendTimeout", + "group": "remote_00_gitpod_navigation@110", + "when": "gitpod.inWorkspace == true && gitpod.workspaceOwned == true" + }, + { + "command": "gitpod.takeSnapshot", + "group": "remote_00_gitpod_navigation@120", + "when": "gitpod.inWorkspace == true && gitpod.workspaceOwned == true" + }, + { + "command": "gitpod.shareWorkspace", + "group": "remote_00_gitpod_navigation@130", + "when": "gitpod.inWorkspace == true && gitpod.workspaceOwned == true && gitpod.workspaceShared == false" + }, + { + "command": "gitpod.stopSharingWorkspace", + "group": "remote_00_gitpod_navigation@130", + "when": "gitpod.inWorkspace == true && gitpod.workspaceOwned == true && gitpod.workspaceShared == true" + }, + { + "command": "gitpod.openInBrowser", + "group": "remote_00_gitpod_navigation@1000", + "when": "gitpod.inWorkspace == true && gitpod.UIKind == 'desktop'" + } + ] + }, + "views": { + "portsView": [ + { + "id": "gitpod.portsView", + "name": "Exposed Ports", + "type": "webview", + "icon": "$(plug)" + } + ] + }, + "viewsContainers": { + "panel": [ + { + "id": "portsView", + "title": "Exposed Ports", + "icon": "$(plug)" + } + ] + } + }, + "scripts": { + "vscode:prepublish": "yarn build:webview; yarn compile", + "compile": "tsc -b", + "watch": "tsc -b -w", + "build:webview": "rollup -c", + "watch:webview": "rollup -c -w", + "start:webview": "sirv public --no-clear" + }, + "devDependencies": { + "@types/node": "16.x", + "@tsconfig/svelte": "^2.0.0", + "@types/vscode-webview": "^1.57.0", + "svelte": "^3.0.0", + "svelte-check": "^2.0.0", + "rollup": "^2.3.4", + "rollup-plugin-copy": "^3.4.0", + "rollup-plugin-css-only": "^3.1.0", + "rollup-plugin-livereload": "^2.0.0", + "rollup-plugin-svelte": "^7.0.0", + "rollup-plugin-terser": "^7.0.0", + "@rollup/plugin-commonjs": "^17.0.0", + "@rollup/plugin-json": "^4.1.0", + "@rollup/plugin-node-resolve": "^11.0.0", + "@rollup/plugin-typescript": "^8.0.0", + "@rollup/plugin-alias": "^3.1.9", + "svelte-preprocess": "^4.0.0", + "sirv-cli": "^2.0.0" + }, + "dependencies": { + "gitpod-shared": "0.0.1", + "jsonc-parser": "^3.2.0", + "vscode-nls": "^5.0.0" + } +} diff --git a/extensions/gitpod-remote/package.nls.json b/extensions/gitpod-remote/package.nls.json new file mode 100644 index 0000000000000..a27f9cc21e486 --- /dev/null +++ b/extensions/gitpod-remote/package.nls.json @@ -0,0 +1,28 @@ +{ + "displayName": "Gitpod Remote", + "description": "Gitpod Remote Support", + "openDashboard": "Gitpod: Open Dashboard", + "openAccessControl": "Gitpod: Open Access Control", + "openSettings": "Gitpod: Open Settings", + "openContext": "Gitpod: Open Context", + "openDocumentation": "Gitpod: Documentation", + "openDiscord": "Gitpod: Open Community Chat", + "openTwitter": "Gitpod: Follow us on Twitter", + "reportIssue": "Gitpod: Report Issue", + "stopWorkspace": "Gitpod: Stop Workspace", + "upgradeSubscription": "Gitpod: Upgrade Subscription", + "extendTimeout": "Gitpod: Extend Workspace Timeout", + "takeSnapshot": "Gitpod: Share Workspace Snapshot", + "shareWorkspace": "Gitpod: Share Running Workspace", + "stopSharingWorkspace": "Gitpod: Stop Sharing Running Workspace", + "openInStable": "Gitpod: Open in VS Code", + "openInInsiders": "Gitpod: Open in VS Code Insiders", + "openInBrowser": "Gitpod: Open in Browser", + "makePrivate": "Make Private", + "makePublic": "Make Public", + "copyWebLink": "Copy web link", + "tunnelNetwork": "Tunnel on all interfaces", + "tunnelHost": "Tunnel on localhost", + "retryAutoExpose": "Retry to expose", + "openWebLinkInBrowser": "Open web link in Browser" +} diff --git a/extensions/gitpod-remote/resources/gitpod.png b/extensions/gitpod-remote/resources/gitpod.png new file mode 100644 index 0000000000000..d8bdece34e139 Binary files /dev/null and b/extensions/gitpod-remote/resources/gitpod.png differ diff --git a/extensions/gitpod-remote/rollup.config.js b/extensions/gitpod-remote/rollup.config.js new file mode 100644 index 0000000000000..6747775cb43bd --- /dev/null +++ b/extensions/gitpod-remote/rollup.config.js @@ -0,0 +1,106 @@ +import svelte from 'rollup-plugin-svelte'; +import commonjs from '@rollup/plugin-commonjs'; +import resolve from '@rollup/plugin-node-resolve'; +import livereload from 'rollup-plugin-livereload'; +import { terser } from 'rollup-plugin-terser'; +import sveltePreprocess from 'svelte-preprocess'; +import typescript from '@rollup/plugin-typescript'; +import alias from '@rollup/plugin-alias'; +import css from 'rollup-plugin-css-only'; +import path from 'path'; +import copy from 'rollup-plugin-copy'; +import json from '@rollup/plugin-json'; + +const production = !process.env.ROLLUP_WATCH; + +function serve() { + let server; + + function toExit() { + if (server) { server.kill(0); } + } + + return { + writeBundle() { + if (server) { return; } + server = require('child_process').spawn('npm', ['run', 'start', '--', '--dev'], { + stdio: ['ignore', 'inherit', 'inherit'], + shell: true + }); + + process.on('SIGTERM', toExit); + process.on('exit', toExit); + } + }; +} + +export default { + input: path.join(__dirname, '../gitpod-shared/portsview/src/main.ts'), + output: [ + { + sourcemap: !production, + format: 'es', + file: path.join(__dirname, './public/portsview.js'), + }, + ], + plugins: [ + alias({ + entries: [ + { find: 'package.nls.json', replacement: path.join(__dirname, 'package.nls.json') }, + ] + }), + svelte({ + preprocess: sveltePreprocess({ + typescript: { + tsconfigFile: '../gitpod-shared/portsview/tsconfig.json' + }, + sourceMap: !production + }), + compilerOptions: { + // enable run-time checks when not in production + dev: !production + } + }), + copy({ + targets: [ + { src: 'node_modules/@vscode/codicons/dist/codicon.css', dest: 'public' }, + { src: 'node_modules/@vscode/codicons/dist/codicon.ttf', dest: 'public' } + ], + }), + json({ compact: true }), + // we'll extract any component CSS out into + // a separate file - better for performance + css({ output: 'portsview.css' }), + + // If you have external dependencies installed from + // npm, you'll most likely need these plugins. In + // some cases you'll need additional configuration - + // consult the documentation for details: + // https://github.com/rollup/plugins/tree/master/packages/commonjs + resolve({ + browser: true, + dedupe: ['svelte'] + }), + commonjs(), + typescript({ + sourceMap: !production, + inlineSources: !production, + tsconfig: '../gitpod-shared/portsview/tsconfig.json' + }), + + // In dev mode, call `npm run start` once + // the bundle has been generated + // !production && serve(), + + // Watch the `public` directory and refresh the + // browser on changes when not in production + !production && livereload('public'), + + // If we're building for production (npm run build + // instead of npm run dev), minify + production && terser() + ], + watch: { + clearScreen: false + } +}; diff --git a/extensions/gitpod-remote/src/cli.ts b/extensions/gitpod-remote/src/cli.ts new file mode 100644 index 0000000000000..cc724b846d39c --- /dev/null +++ b/extensions/gitpod-remote/src/cli.ts @@ -0,0 +1,48 @@ +/*--------------------------------------------------------------------------------------------- + * Copyright (c) Gitpod. All rights reserved. + *--------------------------------------------------------------------------------------------*/ + +import * as http from 'http'; + +function fatal(err: any): void { + console.error(err); + process.exit(1); +} + +async function main(argv: string[]): Promise { + if (!process.env['GITPOD_REMOTE_CLI_IPC']) { + return fatal('Missing pipe'); + } + if (argv[2] === '--preview' && argv[3]) { + try { + await new Promise((resolve, reject) => { + const req = http.request({ + socketPath: process.env['GITPOD_REMOTE_CLI_IPC'], + method: 'POST', + }, 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(undefined); + } + }); + }); + req.on('error', err => reject(err)); + req.write(JSON.stringify({ + type: 'preview', + url: argv[3] + })); + req.end(); + }); + } catch (e) { + fatal(e); + } + } +} + +main(process.argv); diff --git a/extensions/gitpod-remote/src/extension.ts b/extensions/gitpod-remote/src/extension.ts new file mode 100644 index 0000000000000..2a359fa803bcb --- /dev/null +++ b/extensions/gitpod-remote/src/extension.ts @@ -0,0 +1,204 @@ +/*--------------------------------------------------------------------------------------------- + * Copyright (c) Gitpod. All rights reserved. + *--------------------------------------------------------------------------------------------*/ + +/// + +import { GitpodExtensionContext, registerTasks, setupGitpodContext, registerIpcHookCli } from 'gitpod-shared'; +import * as path from 'path'; +import * as vscode from 'vscode'; +import { configureMachineSettings } from './machineSettings'; +import { observePortsStatus, registerPortCommands, tunnelPorts } from './ports'; +import { GitpodPortViewProvider } from './portViewProvider'; +import { initializeRemoteExtensions, installInitialExtensions, ISyncExtension } from './remoteExtensionInit'; + +let gitpodContext: GitpodExtensionContext | undefined; +export async function activate(context: vscode.ExtensionContext) { + gitpodContext = await setupGitpodContext(context); + if (!gitpodContext) { + return; + } + + registerTasks(gitpodContext); + installInitialExtensions(gitpodContext); + + registerPortCommands(gitpodContext); + const portViewProvider = new GitpodPortViewProvider(gitpodContext); + context.subscriptions.push(vscode.window.registerWebviewViewProvider(GitpodPortViewProvider.viewType, portViewProvider, { webviewOptions: { retainContextWhenHidden: true } })); + + const [onPortUpdate, disposePortObserve] = observePortsStatus(gitpodContext!); + context.subscriptions.push(disposePortObserve); + let initial = true; + context.subscriptions.push(onPortUpdate.event(portList => { + const promise = configureMachineSettings(gitpodContext!, portList); + if (initial) { + initial = false; + promise.then(() => tunnelPorts(portList)); + } + portViewProvider.updatePortsStatus(portList); + })); + + // We are moving the heartbeat to gitpod-desktop extension, + // so we register a command to cancel the heartbeat on the gitpod-remote extension + // and then gitpod-desktop will take care of it. + const toDispose = registerHearbeat(gitpodContext); + context.subscriptions.push(toDispose); + context.subscriptions.push(vscode.commands.registerCommand('__gitpod.cancelGitpodRemoteHeartbeat', () => { + toDispose.dispose(); + gitpodContext?.logger.info('__gitpod.cancelGitpodRemoteHeartbeat command executed'); + return true; + })); + + registerCLI(gitpodContext); + // configure task terminals if Gitpod Code Server is running + if (process.env.GITPOD_THEIA_PORT) { + registerIpcHookCli(gitpodContext); + } + + // For port tunneling we rely on Remote SSH capabilities + // and gitpod.gitpod to disable auto tunneling from the current local machine. + vscode.commands.executeCommand('gitpod.api.autoTunnel', gitpodContext.info.getGitpodHost(), gitpodContext.info.getInstanceId(), false); + + // For collecting logs, will be called by gitpod-desktop extension; + context.subscriptions.push(vscode.commands.registerCommand('__gitpod.getGitpodRemoteLogsUri', () => { + return context.logUri; + })); + + // Initialize remote extensions + context.subscriptions.push(vscode.commands.registerCommand('__gitpod.initializeRemoteExtensions', (extensions: ISyncExtension[]) => initializeRemoteExtensions(extensions, gitpodContext!))); + + // TODO + // - auth? + // - .gitpod.yml validations + // - add to .gitpod.yml command + // - cli integration + // - git credential helper + await gitpodContext.active; +} + +export function deactivate() { + if (!gitpodContext) { + return; + } + return gitpodContext.dispose(); +} + +/** + * configure CLI in regular terminals + */ +export function registerCLI(context: GitpodExtensionContext): void { + context.environmentVariableCollection.replace('EDITOR', 'code'); + context.environmentVariableCollection.replace('VISUAL', 'code'); + context.environmentVariableCollection.replace('GP_OPEN_EDITOR', 'code'); + context.environmentVariableCollection.replace('GIT_EDITOR', 'code --wait'); + context.environmentVariableCollection.replace('GP_PREVIEW_BROWSER', `${process.execPath} ${path.join(__dirname, 'cli.js')} --preview`); + context.environmentVariableCollection.replace('GP_EXTERNAL_BROWSER', 'code --openExternal'); + + const ipcHookCli = context.ipcHookCli; + if (!ipcHookCli) { + return; + } + context.environmentVariableCollection.replace('GITPOD_REMOTE_CLI_IPC', ipcHookCli); +} + +export function registerHearbeat(context: GitpodExtensionContext): vscode.Disposable { + const disposables: vscode.Disposable[] = []; + + let lastActivity = 0; + const updateLastActivitiy = () => { + lastActivity = new Date().getTime(); + }; + const sendHeartBeat = async (wasClosed?: true) => { + const suffix = wasClosed ? 'was closed heartbeat' : 'heartbeat'; + if (wasClosed) { + context.logger.trace('sending ' + suffix); + } + try { + await context.gitpod.server.sendHeartBeat({ instanceId: context.info.getInstanceId(), wasClosed }); + if (wasClosed) { + context.fireAnalyticsEvent({ eventName: 'ide_close_signal', properties: { clientKind: 'vscode' } }); + } + } catch (err) { + context.logger.error(`failed to send ${suffix}:`, err); + console.error(`failed to send ${suffix}`, err); + } + }; + sendHeartBeat(); + if (!context.devMode) { + const sendCloseHeartbeat = () => sendHeartBeat(true); + context.pendingWillCloseSocket.push(sendCloseHeartbeat); + disposables.push({ + dispose() { + const idx = context.pendingWillCloseSocket.indexOf(sendCloseHeartbeat); + if (idx >= 0) { + context.pendingWillCloseSocket.splice(idx, 1); + } + } + }); + } + + const activityInterval = 10000; + const heartBeatHandle = setInterval(() => { + if (lastActivity + activityInterval < new Date().getTime()) { + // no activity, no heartbeat + return; + } + sendHeartBeat(); + }, activityInterval); + + disposables.push( + { + dispose() { + clearInterval(heartBeatHandle); + } + }, + vscode.window.onDidChangeActiveTextEditor(updateLastActivitiy), + vscode.window.onDidChangeVisibleTextEditors(updateLastActivitiy), + vscode.window.onDidChangeTextEditorSelection(updateLastActivitiy), + vscode.window.onDidChangeTextEditorVisibleRanges(updateLastActivitiy), + vscode.window.onDidChangeTextEditorOptions(updateLastActivitiy), + vscode.window.onDidChangeTextEditorViewColumn(updateLastActivitiy), + vscode.window.onDidChangeActiveTerminal(updateLastActivitiy), + vscode.window.onDidOpenTerminal(updateLastActivitiy), + vscode.window.onDidCloseTerminal(updateLastActivitiy), + vscode.window.onDidChangeTerminalState(updateLastActivitiy), + vscode.window.onDidChangeWindowState(updateLastActivitiy), + vscode.window.onDidChangeActiveColorTheme(updateLastActivitiy), + vscode.authentication.onDidChangeSessions(updateLastActivitiy), + vscode.debug.onDidChangeActiveDebugSession(updateLastActivitiy), + vscode.debug.onDidStartDebugSession(updateLastActivitiy), + vscode.debug.onDidReceiveDebugSessionCustomEvent(updateLastActivitiy), + vscode.debug.onDidTerminateDebugSession(updateLastActivitiy), + vscode.debug.onDidChangeBreakpoints(updateLastActivitiy), + vscode.extensions.onDidChange(updateLastActivitiy), + vscode.languages.onDidChangeDiagnostics(updateLastActivitiy), + vscode.tasks.onDidStartTask(updateLastActivitiy), + vscode.tasks.onDidStartTaskProcess(updateLastActivitiy), + vscode.tasks.onDidEndTask(updateLastActivitiy), + vscode.tasks.onDidEndTaskProcess(updateLastActivitiy), + vscode.workspace.onDidChangeWorkspaceFolders(updateLastActivitiy), + vscode.workspace.onDidOpenTextDocument(updateLastActivitiy), + vscode.workspace.onDidCloseTextDocument(updateLastActivitiy), + vscode.workspace.onDidChangeTextDocument(updateLastActivitiy), + vscode.workspace.onDidSaveTextDocument(updateLastActivitiy), + vscode.workspace.onDidChangeNotebookDocument(updateLastActivitiy), + vscode.workspace.onDidSaveNotebookDocument(updateLastActivitiy), + vscode.workspace.onDidOpenNotebookDocument(updateLastActivitiy), + vscode.workspace.onDidCloseNotebookDocument(updateLastActivitiy), + vscode.workspace.onWillCreateFiles(updateLastActivitiy), + vscode.workspace.onDidCreateFiles(updateLastActivitiy), + vscode.workspace.onWillDeleteFiles(updateLastActivitiy), + vscode.workspace.onDidDeleteFiles(updateLastActivitiy), + vscode.workspace.onWillRenameFiles(updateLastActivitiy), + vscode.workspace.onDidRenameFiles(updateLastActivitiy), + vscode.workspace.onDidChangeConfiguration(updateLastActivitiy), + vscode.languages.registerHoverProvider('*', { + provideHover: () => { + updateLastActivitiy(); + return null; + } + }) + ); + + return vscode.Disposable.from(...disposables); +} diff --git a/extensions/gitpod-remote/src/machineSettings.ts b/extensions/gitpod-remote/src/machineSettings.ts new file mode 100644 index 0000000000000..87249d358a9f0 --- /dev/null +++ b/extensions/gitpod-remote/src/machineSettings.ts @@ -0,0 +1,70 @@ +/*--------------------------------------------------------------------------------------------- + * Copyright (c) Gitpod. All rights reserved. + *--------------------------------------------------------------------------------------------*/ + +import * as path from 'path'; +import * as vscode from 'vscode'; +import { parse as parseJson, modify as modifyJson, applyEdits as applyEditsJson } from 'jsonc-parser'; +import { GitpodExtensionContext } from 'gitpod-shared'; +import { PortsStatus } from '@gitpod/supervisor-api-grpc/lib/status_pb'; + +export async function configureMachineSettings(context: GitpodExtensionContext, supervisorPortList: PortsStatus.AsObject[]) { + const extRemoteLogsUri: vscode.Uri = context.logUri; + const remoteUserDataPath = path.posix.dirname(path.posix.dirname(path.posix.dirname(path.posix.dirname(context.logUri.path)))); + const machineSettingsResource = extRemoteLogsUri.with({ path: path.posix.join(remoteUserDataPath, 'Machine', 'settings.json') }); + try { + let settingsStr: string = '{}'; + const fileExists = await vscode.workspace.fs.stat(machineSettingsResource).then(() => true, () => false); + if (fileExists) { + const settingsbuffer = await vscode.workspace.fs.readFile(machineSettingsResource); + settingsStr = new TextDecoder().decode(settingsbuffer); + } + + // settings.json is a json with comments file so we use jsonc-parser library + let modified = false; + const settingsJson = parseJson(settingsStr); + if (settingsJson['remote.autoForwardPortsSource'] === undefined) { + const edits = modifyJson(settingsStr, ['remote.autoForwardPortsSource'], 'process', { formattingOptions: { insertSpaces: true, tabSize: 4, insertFinalNewline: true } }); + settingsStr = applyEditsJson(settingsStr, edits); + modified = true; + } + if (settingsJson['remote.portsAttributes'] === undefined && supervisorPortList.length) { + const mapOnOpen = (onOpen?: PortsStatus.OnOpenAction) => { + switch (onOpen) { + case PortsStatus.OnOpenAction.OPEN_BROWSER: + return 'openBrowser'; + case PortsStatus.OnOpenAction.OPEN_PREVIEW: + return 'openPreview'; + case PortsStatus.OnOpenAction.IGNORE: + return 'silent'; + case PortsStatus.OnOpenAction.NOTIFY: + case PortsStatus.OnOpenAction.NOTIFY_PRIVATE: + return 'notify'; + default: + return 'notify'; + } + }; + + const portsAttributes: any = {}; + for (const port of supervisorPortList) { + const onAutoForward = mapOnOpen(port.onOpen); + if (onAutoForward !== 'notify') { + portsAttributes[port.localPort] = { + label: port.name, + onAutoForward: mapOnOpen(port.onOpen) + }; + } + } + const edits = modifyJson(settingsStr, ['remote.portsAttributes'], portsAttributes, { formattingOptions: { insertSpaces: true, tabSize: 4, insertFinalNewline: true } }); + settingsStr = applyEditsJson(settingsStr, edits); + modified = true; + } + + if (modified) { + const settingsbuffer = new TextEncoder().encode(settingsStr); + await vscode.workspace.fs.writeFile(machineSettingsResource, settingsbuffer); + } + } catch (e) { + context.logger.error(`Could not update ${machineSettingsResource.toString()} resource`, e); + } +} diff --git a/extensions/gitpod-remote/src/portViewProvider.ts b/extensions/gitpod-remote/src/portViewProvider.ts new file mode 100644 index 0000000000000..f6873450685f6 --- /dev/null +++ b/extensions/gitpod-remote/src/portViewProvider.ts @@ -0,0 +1,140 @@ +/*--------------------------------------------------------------------------------------------- + * Copyright (c) Gitpod. All rights reserved. + *--------------------------------------------------------------------------------------------*/ +import * as vscode from 'vscode'; +import { GitpodExtensionContext, ExposedServedGitpodWorkspacePort, GitpodWorkspacePort, isExposedServedGitpodWorkspacePort, isExposedServedPort, PortInfo, TunnelDescriptionI } from 'gitpod-shared'; +import { PortsStatus } from '@gitpod/supervisor-api-grpc/lib/status_pb'; + +const PortCommands = ['tunnelNetwork', 'tunnelHost', 'makePublic', 'makePrivate', 'preview', 'openBrowser', 'retryAutoExpose', 'urlCopy', 'queryPortData']; + +type PortCommand = typeof PortCommands[number]; + +export class GitpodPortViewProvider implements vscode.WebviewViewProvider { + public static readonly viewType = 'gitpod.portsView'; + + private _view?: vscode.WebviewView; + + readonly portMap = new Map(); + + private readonly onDidExposeServedPortEmitter = new vscode.EventEmitter(); + readonly onDidExposeServedPort = this.onDidExposeServedPortEmitter.event; + + + private readonly onDidChangePortsEmitter = new vscode.EventEmitter>(); + readonly onDidChangePorts = this.onDidChangePortsEmitter.event; + + constructor(private readonly context: GitpodExtensionContext) { } + + // @ts-ignore + resolveWebviewView(webviewView: vscode.WebviewView, context: vscode.WebviewViewResolveContext, token: vscode.CancellationToken): void | Thenable { + this._view = webviewView; + webviewView.webview.options = { + enableScripts: true, + localResourceRoots: [this.context.extensionUri], + }; + webviewView.webview.html = this._getHtmlForWebview(webviewView.webview); + webviewView.onDidChangeVisibility(() => { + if (!webviewView.visible) { + return; + } + this.updateHtml(); + }); + this.onHtmlCommand(); + } + + private _getHtmlForWebview(webview: vscode.Webview) { + const codiconsUri = webview.asWebviewUri(vscode.Uri.joinPath(this.context.extensionUri, 'public', 'codicon.css')); + const styleUri = webview.asWebviewUri(vscode.Uri.joinPath(this.context.extensionUri, 'public', 'portsview.css')); + const scriptUri = webview.asWebviewUri(vscode.Uri.joinPath(this.context.extensionUri, 'public', 'portsview.js')); + const nonce = getNonce(); + return ` + + + + + + + + + Gitpod Port View + + + +`; + } + + private tunnelsMap = new Map(); + updateTunnels(tunnelsMap: Map): void { + this.tunnelsMap = tunnelsMap; + this.update(); + } + + private portStatus: PortsStatus.AsObject[] | undefined; + updatePortsStatus(portsStatus: PortsStatus.AsObject[]): void { + this.portStatus = portsStatus; + this.update(); + } + + private updating = false; + private update(): void { + if (this.updating) { return; } + this.updating = true; + try { + if (!this.portStatus) { return; } + this.portStatus.forEach(e => { + const localPort = e.localPort; + const tunnel = this.tunnelsMap.get(localPort); + let gitpodPort = this.portMap.get(localPort); + const prevStatus = gitpodPort?.status; + if (!gitpodPort) { + gitpodPort = new GitpodWorkspacePort(localPort, e, tunnel); + this.portMap.set(localPort, gitpodPort); + } else { + gitpodPort.update(e, tunnel); + } + if (isExposedServedGitpodWorkspacePort(gitpodPort) && !isExposedServedPort(prevStatus)) { + this.onDidExposeServedPortEmitter.fire(gitpodPort); + } + }); + this.onDidChangePortsEmitter.fire(this.portMap); + this.updateHtml(); + } finally { + this.updating = false; + } + } + + private updateHtml(): void { + const ports = Array.from(this.portMap.values()).map(e => e.toSvelteObject()); + this._view?.webview.postMessage({ command: 'updatePorts', ports }); + } + + private onHtmlCommand() { + this._view?.webview.onDidReceiveMessage(async (message: { command: PortCommand; port: { info: PortInfo; status: PortsStatus.AsObject } }) => { + if (message.command === 'queryPortData') { + this.updateHtml(); + return; + } + const port = this.portMap.get(message.port.status.localPort); + if (!port) { return; } + if (message.command === 'urlCopy' && port.status.exposed) { + await vscode.env.clipboard.writeText(port.status.exposed.url); + this.context.fireAnalyticsEvent({ + eventName: 'vscode_execute_command_gitpod_ports', + properties: { action: 'urlCopy' } + }); + return; + } + vscode.commands.executeCommand('gitpod.ports.' + message.command, { port }); + }); + } +} + +function getNonce() { + let text = ''; + const possible = + 'ABCDEFGHIJKLMNOPQRSTUVWXYZabcdefghijklmnopqrstuvwxyz0123456789'; + for (let i = 0; i < 32; i++) { + text += possible.charAt(Math.floor(Math.random() * possible.length)); + } + return text; +} diff --git a/extensions/gitpod-remote/src/ports.ts b/extensions/gitpod-remote/src/ports.ts new file mode 100644 index 0000000000000..3e9aec6df2c78 --- /dev/null +++ b/extensions/gitpod-remote/src/ports.ts @@ -0,0 +1,123 @@ +/*--------------------------------------------------------------------------------------------- + * Copyright (c) Gitpod. All rights reserved. + *--------------------------------------------------------------------------------------------*/ +import * as vscode from 'vscode'; +import * as grpc from '@grpc/grpc-js'; +import * as util from 'util'; +import { GitpodExtensionContext, GitpodWorkspacePort } from 'gitpod-shared'; +import { PortsStatus, PortsStatusRequest, PortsStatusResponse } from '@gitpod/supervisor-api-grpc/lib/status_pb'; +import { RetryAutoExposeRequest, TunnelVisiblity } from '@gitpod/supervisor-api-grpc/lib/port_pb'; +import { isGRPCErrorStatus } from 'gitpod-shared/src/common/utils'; + +export async function getSupervisorPorts(context: GitpodExtensionContext) { + let supervisorPortList: PortsStatus.AsObject[] = []; + try { + supervisorPortList = await new Promise((resolve, reject) => { + const req = new PortsStatusRequest(); + const evts = context.supervisor.status.portsStatus(req, context.supervisor.metadata); + evts.on('error', reject); + evts.on('data', (resp: PortsStatusResponse) => resolve(resp.getPortsList().map(p => p.toObject()))); + }); + } catch (e) { + context.logger.error('Could not fetch ports info from supervisor', e); + } + return supervisorPortList; +} + +export function observePortsStatus(context: GitpodExtensionContext): [vscode.EventEmitter, { dispose: () => any }] { + const onPortUpdate = new vscode.EventEmitter(); + let run = true; + let stopUpdates: Function | undefined; + (async () => { + while (run) { + try { + const req = new PortsStatusRequest(); + req.setObserve(true); + const evts = context.supervisor.status.portsStatus(req, context.supervisor.metadata); + stopUpdates = evts.cancel.bind(evts); + + await new Promise((resolve, reject) => { + evts.on('end', resolve); + evts.on('error', reject); + evts.on('data', (resp: PortsStatusResponse) => onPortUpdate.fire(resp.getPortsList().map(p => p.toObject()))); + }); + } catch (err) { + if (!isGRPCErrorStatus(err, grpc.status.CANCELLED)) { + context.logger.error('cannot maintain connection to supervisor', err); + console.error('cannot maintain connection to supervisor', err); + } + } finally { + stopUpdates = undefined; + } + await new Promise(resolve => setTimeout(resolve, 1000)); + } + })(); + + const toDispose = { + dispose() { + onPortUpdate.dispose(); + run = false; + if (stopUpdates) { + stopUpdates(); + } + } + }; + return [onPortUpdate, toDispose]; +} + +export async function tunnelPorts(supervisorPortList: PortsStatus.AsObject[]) { + for (const port of supervisorPortList) { + if (port.served) { + await vscode.env.asExternalUri(vscode.Uri.parse(`http://localhost:${port.localPort}`)); + } + } +} + +interface PortItem { port: GitpodWorkspacePort } + +export function registerPortCommands(context: GitpodExtensionContext) { + context.subscriptions.push(vscode.commands.registerCommand('gitpod.ports.makePrivate', ({ port }: PortItem) => { + context.fireAnalyticsEvent({ + eventName: 'vscode_execute_command_gitpod_ports', + properties: { action: 'private' } + }); + context?.setPortVisibility(port.status.localPort, 'private'); + })); + context.subscriptions.push(vscode.commands.registerCommand('gitpod.ports.makePublic', ({ port }: PortItem) => { + context.fireAnalyticsEvent({ + eventName: 'vscode_execute_command_gitpod_ports', + properties: { action: 'public' } + }); + context?.setPortVisibility(port.status.localPort, 'public'); + })); + context.subscriptions.push(vscode.commands.registerCommand('gitpod.ports.tunnelNetwork', ({ port }: PortItem) => { + context?.setTunnelVisibility(port.portNumber, port.portNumber, TunnelVisiblity.NETWORK); + })); + context.subscriptions.push(vscode.commands.registerCommand('gitpod.ports.tunnelHost', async ({ port }: PortItem) => + context?.setTunnelVisibility(port.portNumber, port.portNumber, TunnelVisiblity.HOST) + )); + context.subscriptions.push(vscode.commands.registerCommand('gitpod.ports.preview', ({ port }: PortItem) => { + context.fireAnalyticsEvent({ + eventName: 'vscode_execute_command_gitpod_ports', + properties: { action: 'preview' } + }); + vscode.commands.executeCommand('simpleBrowser.api.open', port.externalUrl.toString(), { + viewColumn: vscode.ViewColumn.Beside, + preserveFocus: true + }); + })); + context.subscriptions.push(vscode.commands.registerCommand('gitpod.ports.openBrowser', ({ port }: PortItem) => { + context.fireAnalyticsEvent({ + eventName: 'vscode_execute_command_gitpod_ports', + properties: { action: 'openBrowser' } + }); + vscode.env.openExternal(vscode.Uri.parse(port.localUrl)); + })); + context.subscriptions.push(vscode.commands.registerCommand('gitpod.ports.retryAutoExpose', async ({ port }: PortItem) => { + const request = new RetryAutoExposeRequest(); + request.setPort(port.portNumber); + await util.promisify(context.supervisor.port.retryAutoExpose.bind(context.supervisor.port, request, context.supervisor.metadata, { + deadline: Date.now() + context.supervisor.deadlines.normal + }))(); + })); +} diff --git a/extensions/gitpod-remote/src/remoteExtensionInit.ts b/extensions/gitpod-remote/src/remoteExtensionInit.ts new file mode 100644 index 0000000000000..9b4e56d14bc36 --- /dev/null +++ b/extensions/gitpod-remote/src/remoteExtensionInit.ts @@ -0,0 +1,119 @@ +/*--------------------------------------------------------------------------------------------- + * Copyright (c) Gitpod. All rights reserved. + *--------------------------------------------------------------------------------------------*/ + +import * as vscode from 'vscode'; +import * as fs from 'fs'; +import * as cp from 'child_process'; +import { GitpodExtensionContext } from 'gitpod-shared'; +import * as util from 'util'; +import * as path from 'path'; + +export interface IExtensionIdentifier { + id: string; + uuid?: string; +} + +export interface ISyncExtension { + identifier: IExtensionIdentifier; + preRelease?: boolean; + version?: string; + disabled?: boolean; + installed?: boolean; + state?: Record; +} + +let vscodeProductJson: any; +async function getVSCodeProductJson() { + if (!vscodeProductJson) { + const productJsonStr = await fs.promises.readFile(path.join(vscode.env.appRoot, 'product.json'), 'utf8'); + vscodeProductJson = JSON.parse(productJsonStr); + } + + return vscodeProductJson; +} + +export async function initializeRemoteExtensions(extensions: ISyncExtension[], context: GitpodExtensionContext) { + if (!extensions || !extensions.length) { + return true; + } + + const productJson = await getVSCodeProductJson(); + const appName = productJson.applicationName || 'code'; + const codeCliPath = path.join(vscode.env.appRoot, 'bin/remote-cli', appName); + const args = extensions.map(e => '--install-extension ' + e.identifier.id).join(' '); + + try { + context.logger.trace('Trying to initialize remote extensions:', extensions.map(e => e.identifier.id).join('\n')); + const { stderr } = await util.promisify(cp.exec)(`${codeCliPath} ${args}`); + if (stderr) { + context.logger.error('Failed to initialize remote extensions:', stderr); + } + } catch (e) { + context.logger.error('Error trying to initialize remote extensions:', e); + } + + return true; +} + +export async function installInitialExtensions(context: GitpodExtensionContext) { + context.logger.info('installing initial extensions...'); + const extensions: (vscode.Uri | string)[] = []; + try { + const workspaceContextUri = vscode.Uri.parse(context.info.getWorkspaceContextUrl()); + extensions.push('redhat.vscode-yaml'); + if (/github\.com/i.test(workspaceContextUri.authority)) { + extensions.push('github.vscode-pull-request-github'); + } + + let config: { vscode?: { extensions?: string[] } } | undefined; + try { + const configUri = vscode.Uri.file(path.join(context.info.getCheckoutLocation(), '.gitpod.yml')); + const buffer = await vscode.workspace.fs.readFile(configUri); + const content = new TextDecoder().decode(buffer); + const model = new context.config.GitpodPluginModel(content); + config = model.document.toJSON(); + } catch { } + if (config?.vscode?.extensions) { + const extensionIdRegex = /^([^.]+\.[^@]+)(@(\d+\.\d+\.\d+(-.*)?))?$/; + for (const extension of config.vscode.extensions) { + let link: vscode.Uri | undefined; + try { + link = vscode.Uri.parse(extension.trim(), true); + if (link.scheme !== 'http' && link.scheme !== 'https') { + link = undefined; + } + } catch { } + if (link) { + extensions.push(link); + } else { + const normalizedExtension = extension.toLocaleLowerCase(); + if (extensionIdRegex.exec(normalizedExtension)) { + extensions.push(normalizedExtension); + } + } + } + } + } catch (e) { + context.logger.error('Failed to detect workspace context dependent extensions:', e); + } + + if (!extensions.length) { + return; + } + + const productJson = await getVSCodeProductJson(); + const appName = productJson.applicationName || 'code'; + const codeCliPath = path.join(vscode.env.appRoot, 'bin/remote-cli', appName); + const args = extensions.map(e => '--install-extension ' + e.toString()).join(' '); + + try { + context.logger.trace('Trying to initialize remote extensions from gitpod.yml:', extensions.map(e => e.toString()).join('\n')); + const { stderr } = await util.promisify(cp.exec)(`${codeCliPath} ${args}`); + if (stderr) { + context.logger.error('Failed to initialize remote extensions from gitpod.yml:', stderr); + } + } catch (e) { + context.logger.error('Error trying to initialize remote extensions from gitpod.yml:', e); + } +} diff --git a/extensions/gitpod-remote/tsconfig.json b/extensions/gitpod-remote/tsconfig.json new file mode 100644 index 0000000000000..de9aeb8d06b33 --- /dev/null +++ b/extensions/gitpod-remote/tsconfig.json @@ -0,0 +1,15 @@ +{ + "extends": "../tsconfig.base.json", + "compilerOptions": { + "outDir": "./out", + "rootDir": "./src", + "esModuleInterop": true, + "sourceMap": true, + "lib": [ + "DOM" + ], + }, + "references": [ + { "path": "../gitpod-shared" } + ] +} diff --git a/extensions/gitpod-shared/.gitignore b/extensions/gitpod-shared/.gitignore new file mode 100644 index 0000000000000..364fdec1aa19d --- /dev/null +++ b/extensions/gitpod-shared/.gitignore @@ -0,0 +1 @@ +public/ diff --git a/extensions/gitpod-shared/package.json b/extensions/gitpod-shared/package.json new file mode 100644 index 0000000000000..19ba600db9a65 --- /dev/null +++ b/extensions/gitpod-shared/package.json @@ -0,0 +1,42 @@ +{ + "private": true, + "name": "gitpod-shared", + "publisher": "gitpod", + "version": "0.0.1", + "license": "MIT", + "engines": { + "vscode": "^1.58.2" + }, + "main": "./out/extension.js", + "scripts": { + "prepare": "node scripts/inflate.js", + "test": "mocha out/test/**/*.js" + }, + "devDependencies": { + "@types/mocha": "^9.1.1", + "@types/js-yaml": "^4.0.5", + "@types/node": "16.x", + "@types/uuid": "^8.3.1", + "@types/ws": "^7.2.6", + "@tsconfig/svelte": "^2.0.0", + "@types/vscode-webview": "^1.57.0", + "mocha": "^10.0.0", + "svelte": "^3.0.0", + "svelte-check": "^2.0.0", + "svelte-preprocess": "^4.0.0" + }, + "dependencies": { + "@gitpod/gitpod-protocol": "main", + "@gitpod/supervisor-api-grpc": "jp-on-open", + "bufferutil": "^4.0.1", + "js-yaml": "^4.1.0", + "reconnecting-websocket": "^4.4.0", + "utf-8-validate": "^5.0.2", + "uuid": "^8.3.1", + "vscode-nls": "^5.0.0", + "ws": "^7.4.6", + "yaml": "^1.10.0", + "@vscode/codicons": "^0.0.31", + "@vscode/webview-ui-toolkit": "^1.0.0" + } +} diff --git a/extensions/gitpod-shared/portsview/src/App.svelte b/extensions/gitpod-shared/portsview/src/App.svelte new file mode 100644 index 0000000000000..5b4637064626e --- /dev/null +++ b/extensions/gitpod-shared/portsview/src/App.svelte @@ -0,0 +1,36 @@ + + + + + +
+ {#if innerWidth > 500} + + {:else} + + {/if} +
+ + diff --git a/extensions/gitpod-shared/portsview/src/components/ContextMenu.svelte b/extensions/gitpod-shared/portsview/src/components/ContextMenu.svelte new file mode 100644 index 0000000000000..42b54e2b24cd0 --- /dev/null +++ b/extensions/gitpod-shared/portsview/src/components/ContextMenu.svelte @@ -0,0 +1,85 @@ + + + +
+ {#if show} +
+ + + + diff --git a/extensions/gitpod-shared/portsview/src/components/HoverOptions.svelte b/extensions/gitpod-shared/portsview/src/components/HoverOptions.svelte new file mode 100644 index 0000000000000..cecfdcf92b130 --- /dev/null +++ b/extensions/gitpod-shared/portsview/src/components/HoverOptions.svelte @@ -0,0 +1,85 @@ + + + +
+
+
+ +
+ + {#each options as opt} + {#if opt.icon != null} + clickOption(opt.command)} + > + + + + + {/if} + {/each} + +
+
+ + diff --git a/extensions/gitpod-shared/portsview/src/global.d.ts b/extensions/gitpod-shared/portsview/src/global.d.ts new file mode 100644 index 0000000000000..8b8ce9866b834 --- /dev/null +++ b/extensions/gitpod-shared/portsview/src/global.d.ts @@ -0,0 +1,5 @@ +/*--------------------------------------------------------------------------------------------- + * Copyright (c) Gitpod. All rights reserved. + *--------------------------------------------------------------------------------------------*/ + +/// diff --git a/extensions/gitpod-shared/portsview/src/main.ts b/extensions/gitpod-shared/portsview/src/main.ts new file mode 100644 index 0000000000000..ab4bff5c0acfc --- /dev/null +++ b/extensions/gitpod-shared/portsview/src/main.ts @@ -0,0 +1,9 @@ +/*--------------------------------------------------------------------------------------------- + * Copyright (c) Gitpod. All rights reserved. + *--------------------------------------------------------------------------------------------*/ + +import App from './App.svelte'; + +new App({ + target: document.body +}); diff --git a/extensions/gitpod-shared/portsview/src/porttable/PortHoverActions.svelte b/extensions/gitpod-shared/portsview/src/porttable/PortHoverActions.svelte new file mode 100644 index 0000000000000..6a1731a28ce55 --- /dev/null +++ b/extensions/gitpod-shared/portsview/src/porttable/PortHoverActions.svelte @@ -0,0 +1,50 @@ + + + + { + onHoverCommand(e.detail); + }} +> + + diff --git a/extensions/gitpod-shared/portsview/src/porttable/PortInfo.svelte b/extensions/gitpod-shared/portsview/src/porttable/PortInfo.svelte new file mode 100644 index 0000000000000..cd9626b33c194 --- /dev/null +++ b/extensions/gitpod-shared/portsview/src/porttable/PortInfo.svelte @@ -0,0 +1,50 @@ + + + +
+
+ {title} + {portDetail} +
+
+ + diff --git a/extensions/gitpod-shared/portsview/src/porttable/PortList.svelte b/extensions/gitpod-shared/portsview/src/porttable/PortList.svelte new file mode 100644 index 0000000000000..42e18ef1fb516 --- /dev/null +++ b/extensions/gitpod-shared/portsview/src/porttable/PortList.svelte @@ -0,0 +1,65 @@ + + + +
+
+ {#each ports as port, i (port.status.localPort)} + { + postData(e.detail, port); + }} + > +
+
+ +
+
+ +
+
+ {port.info.description} +
+
+
+ {/each} +
+
+ + diff --git a/extensions/gitpod-shared/portsview/src/porttable/PortLocalAddress.svelte b/extensions/gitpod-shared/portsview/src/porttable/PortLocalAddress.svelte new file mode 100644 index 0000000000000..03ab500d15299 --- /dev/null +++ b/extensions/gitpod-shared/portsview/src/porttable/PortLocalAddress.svelte @@ -0,0 +1,40 @@ + + + + { console.log(e); dispatch("command", { command: e.detail, port}) }} +> + + { openAddr(); return false; }} href="javascript:void(0)">{port.status.exposed.url} + + + diff --git a/extensions/gitpod-shared/portsview/src/porttable/PortStatus.svelte b/extensions/gitpod-shared/portsview/src/porttable/PortStatus.svelte new file mode 100644 index 0000000000000..a7b6433165cd9 --- /dev/null +++ b/extensions/gitpod-shared/portsview/src/porttable/PortStatus.svelte @@ -0,0 +1,48 @@ + + + +
+
+ {#if status === "ExposureFailed"} + + {:else if circleFill} + + {:else} + + {/if} +
+
+ + diff --git a/extensions/gitpod-shared/portsview/src/porttable/PortTable.svelte b/extensions/gitpod-shared/portsview/src/porttable/PortTable.svelte new file mode 100644 index 0000000000000..f4a0a97d2aa31 --- /dev/null +++ b/extensions/gitpod-shared/portsview/src/porttable/PortTable.svelte @@ -0,0 +1,218 @@ + + + +
+ { + const command = e.detail; + postData(command, menuData.port); + closeMenu(); + }} + /> + + (tableHovered = true)} + on:mouseleave={() => (tableHovered = false)} + > + + {#each useResponsive.headers as header, i (i)} + {header} + {/each} + + {#each ports as port, i (port.status.localPort)} + onRightClick(event, port)} + > + {#if useResponsive.headers.includes("")} + + + + {/if} + + {#if useResponsive.headers.includes("Port")} + + + + {/if} + + {#if useResponsive.headers.includes("Address")} + + {#if (port.status.exposed?.url.length ?? 0) > 0} + { + const { command, port } = e.detail; + postData(command, port); + }} + {port} + /> + {/if} + + {/if} + + {#if useResponsive.headers.includes("Description")} + + {port.status.description} + + {/if} + + {#if useResponsive.headers.includes("State")} + + {port.info.description} + + {/if} + + {#if useResponsive.headers.includes("Action")} + + { postData(e.detail, port) }} /> + + {/if} + + {/each} + +
+ + { + if (menuData.show) { + menuData.show = false; + } + }} +/> + + diff --git a/extensions/gitpod-shared/portsview/src/protocol/components.ts b/extensions/gitpod-shared/portsview/src/protocol/components.ts new file mode 100644 index 0000000000000..f45d46a2266a3 --- /dev/null +++ b/extensions/gitpod-shared/portsview/src/protocol/components.ts @@ -0,0 +1,18 @@ +/*--------------------------------------------------------------------------------------------- + * Copyright (c) Gitpod. All rights reserved. + *--------------------------------------------------------------------------------------------*/ + +export interface MenuOptionI { + command: string; + label: string; + desc?: string; +} + +export type MenuOption = MenuOptionI | null; + +export interface HoverOption { + // icon name in codicons + icon: string; + title: string; + command: string; +} diff --git a/extensions/gitpod-shared/portsview/src/protocol/gitpod.ts b/extensions/gitpod-shared/portsview/src/protocol/gitpod.ts new file mode 100644 index 0000000000000..e31d87891e62d --- /dev/null +++ b/extensions/gitpod-shared/portsview/src/protocol/gitpod.ts @@ -0,0 +1,86 @@ +/*--------------------------------------------------------------------------------------------- + * Copyright (c) Gitpod. All rights reserved. + *--------------------------------------------------------------------------------------------*/ + +export type IconStatus = 'Served' | 'NotServed' | 'Detecting' | 'ExposureFailed'; + +export interface PortInfo { + label: string; + tooltip: string; + description: string; + iconStatus: IconStatus; + contextValue: string; + localUrl: string; + // iconPath?: vscode.ThemeIcon; +} + +export enum TunnelVisiblity { + NONE = 0, + HOST = 1, + NETWORK = 2, +} + +export enum PortVisibility { + PRIVATE = 0, + PUBLIC = 1, +} + +export enum OnPortExposedAction { + IGNORE = 0, + OPEN_BROWSER = 1, + OPEN_PREVIEW = 2, + NOTIFY = 3, + NOTIFY_PRIVATE = 4, +} + +export enum PortAutoExposure { + TRYING = 0, + SUCCEEDED = 1, + FAILED = 2, +} + +export enum TaskState { + OPENING = 0, + RUNNING = 1, + CLOSED = 2, +} + + +export namespace ExposedPortInfo { + export type AsObject = { + visibility: PortVisibility; + url: string; + onExposed: OnPortExposedAction; + }; +} + +export namespace TunneledPortInfo { + export type AsObject = { + targetPort: number; + visibility: TunnelVisiblity; + + clientsMap: Array<[string, number]>; + }; +} + +export namespace PortsStatus { + export type AsObject = { + localPort: number; + served: boolean; + exposed?: ExposedPortInfo.AsObject; + autoExposure: PortAutoExposure; + tunneled?: TunneledPortInfo.AsObject; + description: string; + name: string; + }; +} + + +export interface GitpodPortObject { + info: PortInfo; + status: PortsStatus.AsObject & { remotePort?: number }; +} + +export const PortCommands = ['tunnelNetwork', 'tunnelHost', 'makePublic', 'makePrivate', 'preview', 'openBrowser', 'retryAutoExpose', 'urlCopy', 'queryPortData']; + +export type PortCommand = typeof PortCommands[number]; diff --git a/extensions/gitpod-shared/portsview/src/typings/package.nls.d.ts b/extensions/gitpod-shared/portsview/src/typings/package.nls.d.ts new file mode 100644 index 0000000000000..fb97ae195e492 --- /dev/null +++ b/extensions/gitpod-shared/portsview/src/typings/package.nls.d.ts @@ -0,0 +1,4 @@ + +declare module 'package.nls.json' { + export default ({} as { [key: string]: string; }); +} diff --git a/extensions/gitpod-shared/portsview/src/utils/commands.ts b/extensions/gitpod-shared/portsview/src/utils/commands.ts new file mode 100644 index 0000000000000..0d35517bb26f1 --- /dev/null +++ b/extensions/gitpod-shared/portsview/src/utils/commands.ts @@ -0,0 +1,63 @@ +/*--------------------------------------------------------------------------------------------- + * Copyright (c) Gitpod. All rights reserved. + *--------------------------------------------------------------------------------------------*/ + +import nlsFile from 'package.nls.json'; +import type { GitpodPortObject, PortCommand } from '../protocol/gitpod'; + +// TODO: use vscode-nls +export function getNLSTitle(command: PortCommand) { + let name: string = command; + switch (name) { + case 'preview': + name = 'openPreview'; + } + return nlsFile[name] ?? command as string; +} + +export const commandIconMap: Record = { + tunnelNetwork: 'eye', + tunnelHost: 'eye-closed', + makePublic: 'lock', + makePrivate: 'unlock', + preview: 'open-preview', + openBrowser: 'globe', + retryAutoExpose: 'refresh', + urlCopy: 'copy', + queryPortData: '', +}; + +export function getCommands(port: GitpodPortObject): PortCommand[] { + return getSplitCommands(port).filter(e => !!e) as PortCommand[]; +} + +export function getSplitCommands(port: GitpodPortObject) { + const opts: Array = []; + const viewItem = port.info.contextValue; + if (viewItem.includes('host') && viewItem.includes('tunneled')) { + opts.push('tunnelNetwork'); + } + if (viewItem.includes('network') && viewItem.includes('tunneled')) { + opts.push('tunnelHost'); + } + if (opts.length > 0) { + opts.push(null); + } + if (viewItem.includes('private')) { + opts.push('makePublic'); + } + if (viewItem.includes('public')) { + opts.push('makePrivate'); + } + if (viewItem.includes('exposed') || viewItem.includes('tunneled')) { + opts.push('preview'); + opts.push('openBrowser'); + } + if (viewItem.includes('failed')) { + if (opts.length > 0) { + opts.push(null); + } + opts.push('retryAutoExpose'); + } + return opts; +} diff --git a/extensions/gitpod-shared/portsview/src/utils/vscodeApi.ts b/extensions/gitpod-shared/portsview/src/utils/vscodeApi.ts new file mode 100644 index 0000000000000..9baab80a85c2d --- /dev/null +++ b/extensions/gitpod-shared/portsview/src/utils/vscodeApi.ts @@ -0,0 +1,36 @@ +/*--------------------------------------------------------------------------------------------- + * Copyright (c) Gitpod. All rights reserved. + *--------------------------------------------------------------------------------------------*/ + +import type { WebviewApi } from 'vscode-webview'; + +class VSCodeAPIWrapper { + private readonly vsCodeApi: WebviewApi | undefined; + + constructor() { + // Check if the acquireVsCodeApi function exists in the current development + // context (i.e. VS Code development window or web browser) + if (typeof acquireVsCodeApi === 'function') { + this.vsCodeApi = acquireVsCodeApi(); + } + } + + /** + * Post a message (i.e. send arbitrary data) to the owner of the webview. + * + * @remarks When running webview code inside a web browser, postMessage will instead + * log the given message to the console. + * + * @param message Abitrary data (must be JSON serializable) to send to the extension context. + */ + public postMessage(message: unknown) { + if (this.vsCodeApi) { + this.vsCodeApi.postMessage(message); + } else { + console.log(message); + } + } +} + +// Exports class singleton to prevent multiple invocations of acquireVsCodeApi. +export const vscode = new VSCodeAPIWrapper(); diff --git a/extensions/gitpod-shared/portsview/tsconfig.json b/extensions/gitpod-shared/portsview/tsconfig.json new file mode 100644 index 0000000000000..ba57ae3da3947 --- /dev/null +++ b/extensions/gitpod-shared/portsview/tsconfig.json @@ -0,0 +1,33 @@ +{ + "extends": "@tsconfig/svelte/tsconfig.json", + "compilerOptions": { + "rootDir": "./src", + "target": "es2020", + "lib": [ + "es2018", + "ES2019.Array", + "ES2019.Object", + "ES2019.String", + "ES2019.Symbol", + "ES2020.BigInt", + "ES2020.Promise", + "ES2020.String", + "ES2020.Symbol.WellKnown", + "ES2020.Intl", + "DOM", + "DOM.Iterable" + ], + "strict": true, + "exactOptionalPropertyTypes": false, + "useUnknownInCatchVariables": false, + "alwaysStrict": true, + "noImplicitAny": true, + "noImplicitReturns": true, + "noImplicitOverride": true, + "noUnusedLocals": true, + "noUnusedParameters": true, + "forceConsistentCasingInFileNames": true, + "resolveJsonModule": true, + "sourceMap": true, + } +} diff --git a/extensions/gitpod-shared/scripts/inflate.js b/extensions/gitpod-shared/scripts/inflate.js new file mode 100644 index 0000000000000..8e9d1704c80e3 --- /dev/null +++ b/extensions/gitpod-shared/scripts/inflate.js @@ -0,0 +1,231 @@ +/*--------------------------------------------------------------------------------------------- + * Copyright (c) Gitpod. All rights reserved. + *--------------------------------------------------------------------------------------------*/ + +//@ts-check +const fs = require('fs'); +const path = require('path'); + +const nls = { + 'openDashboard': 'Gitpod: Open Dashboard', + 'openAccessControl': 'Gitpod: Open Access Control', + 'openSettings': 'Gitpod: Open Settings', + 'openContext': 'Gitpod: Open Context', + 'openDocumentation': 'Gitpod: Documentation', + 'openDiscord': 'Gitpod: Open Community Chat', + 'openTwitter': 'Gitpod: Follow us on Twitter', + 'reportIssue': 'Gitpod: Report Issue', + 'stopWorkspace': 'Gitpod: Stop Workspace', + 'upgradeSubscription': 'Gitpod: Upgrade Subscription', + 'extendTimeout': 'Gitpod: Extend Workspace Timeout', + 'takeSnapshot': 'Gitpod: Share Workspace Snapshot', + 'shareWorkspace': 'Gitpod: Share Running Workspace', + 'stopSharingWorkspace': 'Gitpod: Stop Sharing Running Workspace', + 'openInStable': 'Gitpod: Open in VS Code', + 'openInInsiders': 'Gitpod: Open in VS Code Insiders', + 'openInBrowser': 'Gitpod: Open in Browser' +}; + +const commands = [ + { + 'command': 'gitpod.stop.ws', + 'title': '%stopWorkspace%', + 'enablement': 'gitpod.inWorkspace == true && gitpod.workspaceOwned == true' + }, + { + 'command': 'gitpod.open.settings', + 'title': '%openSettings%', + 'enablement': 'gitpod.inWorkspace == true' + }, + { + 'command': 'gitpod.open.accessControl', + 'title': '%openAccessControl%', + 'enablement': 'gitpod.inWorkspace == true' + }, + { + 'command': 'gitpod.open.context', + 'title': '%openContext%', + 'enablement': 'gitpod.inWorkspace == true' + }, + { + 'command': 'gitpod.open.dashboard', + 'title': '%openDashboard%', + 'enablement': 'gitpod.inWorkspace == true' + }, + { + 'command': 'gitpod.open.documentation', + 'title': '%openDocumentation%', + 'enablement': 'gitpod.inWorkspace == true' + }, + { + 'command': 'gitpod.open.twitter', + 'title': '%openTwitter%', + 'enablement': 'gitpod.inWorkspace == true' + }, + { + 'command': 'gitpod.open.discord', + 'title': '%openDiscord%', + 'enablement': 'gitpod.inWorkspace == true' + }, + { + 'command': 'gitpod.reportIssue', + 'title': '%reportIssue%', + 'enablement': 'gitpod.inWorkspace == true' + }, + { + 'command': 'gitpod.upgradeSubscription', + 'title': '%upgradeSubscription%', + 'enablement': 'gitpod.inWorkspace == true && gitpod.workspaceOwned == true' + }, + { + 'command': 'gitpod.ExtendTimeout', + 'title': '%extendTimeout%', + 'enablement': 'gitpod.inWorkspace == true && gitpod.workspaceOwned == true' + }, + { + 'command': 'gitpod.takeSnapshot', + 'title': '%takeSnapshot%', + 'enablement': 'gitpod.inWorkspace == true && gitpod.workspaceOwned == true' + }, + { + 'command': 'gitpod.shareWorkspace', + 'title': '%shareWorkspace%', + 'enablement': 'gitpod.inWorkspace == true && gitpod.workspaceOwned == true && gitpod.workspaceShared == false' + }, + { + 'command': 'gitpod.stopSharingWorkspace', + 'title': '%stopSharingWorkspace%', + 'enablement': 'gitpod.inWorkspace == true && gitpod.workspaceOwned == true && gitpod.workspaceShared == true' + }, + { + 'command': 'gitpod.openInStable', + 'title': '%openInStable%', + 'enablement': 'gitpod.inWorkspace == true && gitpod.UIKind == \'web\'' + }, + { + 'command': 'gitpod.openInInsiders', + 'title': '%openInInsiders%', + 'enablement': 'gitpod.inWorkspace == true && gitpod.UIKind == \'web\'' + }, + { + 'command': 'gitpod.openInBrowser', + 'title': '%openInBrowser%', + 'enablement': 'gitpod.inWorkspace == true && gitpod.UIKind == \'desktop\'' + } +]; + +const remoteMenus = [ + { + 'command': 'gitpod.stop.ws', + 'group': 'remote_00_gitpod_navigation@10', + 'when': 'gitpod.inWorkspace == true && gitpod.workspaceOwned == true' + }, + { + 'command': 'gitpod.open.settings', + 'group': 'remote_00_gitpod_navigation@20', + 'when': 'gitpod.inWorkspace == true' + }, + { + 'command': 'gitpod.open.accessControl', + 'group': 'remote_00_gitpod_navigation@30', + 'when': 'gitpod.inWorkspace == true' + }, + { + 'command': 'gitpod.open.context', + 'group': 'remote_00_gitpod_navigation@40', + 'when': 'gitpod.inWorkspace == true' + }, + { + 'command': 'gitpod.open.dashboard', + 'group': 'remote_00_gitpod_navigation@50', + 'when': 'gitpod.inWorkspace == true' + }, + { + 'command': 'gitpod.open.documentation', + 'group': 'remote_00_gitpod_navigation@60', + 'when': 'gitpod.inWorkspace == true' + }, + { + 'command': 'gitpod.open.twitter', + 'group': 'remote_00_gitpod_navigation@70', + 'when': 'gitpod.inWorkspace == true' + }, + { + 'command': 'gitpod.open.discord', + 'group': 'remote_00_gitpod_navigation@80', + 'when': 'gitpod.inWorkspace == true' + }, + { + 'command': 'gitpod.reportIssue', + 'group': 'remote_00_gitpod_navigation@90', + 'when': 'gitpod.inWorkspace == true' + }, + { + 'command': 'gitpod.upgradeSubscription', + 'group': 'remote_00_gitpod_navigation@100', + 'when': 'gitpod.inWorkspace == true && gitpod.workspaceOwned == true' + }, + { + 'command': 'gitpod.ExtendTimeout', + 'group': 'remote_00_gitpod_navigation@110', + 'when': 'gitpod.inWorkspace == true && gitpod.workspaceOwned == true' + }, + { + 'command': 'gitpod.takeSnapshot', + 'group': 'remote_00_gitpod_navigation@120', + 'when': 'gitpod.inWorkspace == true && gitpod.workspaceOwned == true' + }, + { + 'command': 'gitpod.shareWorkspace', + 'group': 'remote_00_gitpod_navigation@130', + 'when': 'gitpod.inWorkspace == true && gitpod.workspaceOwned == true && gitpod.workspaceShared == false' + }, + { + 'command': 'gitpod.stopSharingWorkspace', + 'group': 'remote_00_gitpod_navigation@130', + 'when': 'gitpod.inWorkspace == true && gitpod.workspaceOwned == true && gitpod.workspaceShared == true' + }, + { + 'command': 'gitpod.openInStable', + 'group': 'remote_00_gitpod_navigation@900', + 'when': 'gitpod.inWorkspace == true && gitpod.UIKind == \'web\'' + }, + { + 'command': 'gitpod.openInInsiders', + 'group': 'remote_00_gitpod_navigation@1000', + 'when': 'gitpod.inWorkspace == true && gitpod.UIKind == \'web\'' + }, + { + 'command': 'gitpod.openInBrowser', + 'group': 'remote_00_gitpod_navigation@1000', + 'when': 'gitpod.inWorkspace == true && gitpod.UIKind == \'desktop\'' + } +]; + +function inflateManifest(manifest) { + const contributes = manifest.contributes = (manifest.contributes || {}); + contributes.commands = (contributes.commands || []).filter(c => commands.findIndex(c2 => c.command === c2.command) === -1); + contributes.commands.unshift(...commands); + const menus = contributes.menus = (contributes.menus || {}); + menus['statusBar/remoteIndicator'] = (menus['statusBar/remoteIndicator'] || []).filter(m => remoteMenus.findIndex(m2 => m.command === m2.command) === -1); + menus['statusBar/remoteIndicator'].unshift(...remoteMenus); +} + +function main() { + const workspacePath = path.resolve(__dirname, '..', '..'); + for (const name of ['gitpod-remote', 'gitpod-web']) { + const manifestPath = path.resolve(workspacePath, name, 'package.json'); + const pckContent = fs.readFileSync(manifestPath, { encoding: 'utf-8' }); + const manifest = JSON.parse(pckContent); + inflateManifest(manifest); + fs.writeFileSync(manifestPath, JSON.stringify(manifest, undefined, 2), { encoding: 'utf-8' }); + + const nlsPath = path.resolve(workspacePath, name, 'package.nls.json'); + const nlsContent = fs.readFileSync(nlsPath, { encoding: 'utf-8' }); + fs.writeFileSync(nlsPath, JSON.stringify({ + ...JSON.parse(nlsContent), ...nls + }, undefined, '\t'), { encoding: 'utf-8' }); + } +} + +main(); diff --git a/extensions/gitpod-shared/src/analytics.ts b/extensions/gitpod-shared/src/analytics.ts new file mode 100644 index 0000000000000..8abf53588ecf5 --- /dev/null +++ b/extensions/gitpod-shared/src/analytics.ts @@ -0,0 +1,71 @@ +/*--------------------------------------------------------------------------------------------- + * Copyright (c) Gitpod. All rights reserved. + *--------------------------------------------------------------------------------------------*/ + +import path from 'path'; +import * as vscode from 'vscode'; + +import { GitpodExtensionContext } from './features'; + +export interface BaseGitpodAnalyticsEventPropeties { + sessionId: string; + workspaceId: string; + instanceId: string; + appName: string; + uiKind: 'web' | 'desktop'; + devMode: boolean; + version: string; + timestamp: number; + 'common.extname': string; + 'common.extversion': string; +} + +interface GAET { + eventName: N; + properties: Omit; +} + +export type GitpodAnalyticsEvent = + GAET<'vscode_session', {}> | + GAET<'vscode_execute_command_gitpod_open_link', { + url: string; + }> | + GAET<'vscode_execute_command_gitpod_change_vscode_type', { + targetUiKind: 'web' | 'desktop'; + targetQualifier?: 'stable' | 'insiders'; + }> | + GAET<'vscode_execute_command_gitpod_workspace', { + action: 'share' | 'stop-sharing' | 'stop' | 'snapshot' | 'extend-timeout'; + }> | + GAET<'vscode_execute_command_gitpod_ports', { + action: 'private' | 'public' | 'preview' | 'openBrowser' | 'urlCopy'; + }> | + GAET<'vscode_execute_command_gitpod_config', { + action: 'remove' | 'add'; + }> | + GAET<'vscode_active_language', { + lang: string; ext?: string; + }> | + GAET<'ide_close_signal', { + clientKind: 'vscode'; + }>; + +export function registerUsageAnalytics(context: GitpodExtensionContext): void { + context.fireAnalyticsEvent({ eventName: 'vscode_session', properties: {} }); +} + +const activeLanguages = new Set(); +export function registerActiveLanguageAnalytics(context: GitpodExtensionContext): void { + const track = () => { + const e = vscode.window.activeTextEditor; + if (!e || activeLanguages.has(e.document.languageId)) { + return; + } + const lang = e.document.languageId; + activeLanguages.add(lang); + const ext = path.extname(e.document.uri.path) || undefined; + context.fireAnalyticsEvent({ eventName: 'vscode_active_language', properties: { lang, ext } }); + }; + track(); + context.subscriptions.push(vscode.window.onDidChangeActiveTextEditor(() => track())); +} diff --git a/extensions/gitpod-shared/src/common/logger.ts b/extensions/gitpod-shared/src/common/logger.ts new file mode 100644 index 0000000000000..0f71ef88014ae --- /dev/null +++ b/extensions/gitpod-shared/src/common/logger.ts @@ -0,0 +1,67 @@ +/*--------------------------------------------------------------------------------------------- + * Copyright (c) Gitpod. All rights reserved. + *--------------------------------------------------------------------------------------------*/ + +import * as vscode from 'vscode'; + +type LogLevel = 'Trace' | 'Info' | 'Error' | 'Warn' | 'Log'; + +export default class Log { + private output: vscode.OutputChannel; + + constructor(name: string) { + this.output = vscode.window.createOutputChannel(name); + } + + private data2String(data: any): string { + if (data instanceof Error) { + return data.stack || data.message; + } + if (data.success === false && data.message) { + return data.message; + } + return data.toString(); + } + + public trace(message: string, data?: any): void { + this.logLevel('Trace', message, data); + } + + public info(message: string, data?: any): void { + this.logLevel('Info', message, data); + } + + public error(message: string, data?: any): void { + this.logLevel('Error', message, data); + } + + public warn(message: string, data?: any): void { + this.logLevel('Warn', message, data); + } + + public log(message: string, data?: any): void { + this.logLevel('Log', message, data); + } + + public logLevel(level: LogLevel, message: string, data?: any): void { + this.output.appendLine(`[${level} - ${this.now()}] ${message}`); + if (data) { + this.output.appendLine(this.data2String(data)); + } + } + + private now(): string { + const now = new Date(); + return padLeft(now.getUTCHours() + '', 2, '0') + + ':' + padLeft(now.getMinutes() + '', 2, '0') + + ':' + padLeft(now.getUTCSeconds() + '', 2, '0') + '.' + now.getMilliseconds(); + } + + public show() { + this.output.show(); + } +} + +function padLeft(s: string, n: number, pad = ' ') { + return pad.repeat(Math.max(0, n - s.length)) + s; +} diff --git a/extensions/gitpod-shared/src/common/utils.ts b/extensions/gitpod-shared/src/common/utils.ts new file mode 100644 index 0000000000000..9634b43457b1d --- /dev/null +++ b/extensions/gitpod-shared/src/common/utils.ts @@ -0,0 +1,6 @@ + +import * as grpc from '@grpc/grpc-js'; + +export function isGRPCErrorStatus(err: any, status: T): boolean { + return err && typeof err === 'object' && 'code' in err && err.code === status; +} diff --git a/extensions/gitpod-shared/src/extension.ts b/extensions/gitpod-shared/src/extension.ts new file mode 100644 index 0000000000000..135f963d60b55 --- /dev/null +++ b/extensions/gitpod-shared/src/extension.ts @@ -0,0 +1,52 @@ +/*--------------------------------------------------------------------------------------------- + * Copyright (c) Gitpod. All rights reserved. + *--------------------------------------------------------------------------------------------*/ + +import * as vscode from 'vscode'; +import { registerActiveLanguageAnalytics, registerUsageAnalytics } from './analytics'; +import { createGitpodExtensionContext, GitpodExtensionContext, registerDefaultLayout, registerNotifications, registerWorkspaceCommands, registerWorkspaceSharing, registerWorkspaceTimeout } from './features'; + +export { GitpodExtensionContext, registerTasks, SupervisorConnection, registerIpcHookCli } from './features'; + +export * from './common/utils'; +export * from './gitpod-plugin-model'; +export * from './workspacePort'; + +export async function setupGitpodContext(context: vscode.ExtensionContext): Promise { + if (typeof vscode.env.remoteName === 'undefined' || context.extension.extensionKind !== vscode.ExtensionKind.Workspace) { + return undefined; + } + + const gitpodContext = await createGitpodExtensionContext(context); + vscode.commands.executeCommand('setContext', 'gitpod.inWorkspace', !!gitpodContext); + if (!gitpodContext) { + return undefined; + } + + logContextInfo(gitpodContext); + + vscode.commands.executeCommand('setContext', 'gitpod.ideAlias', gitpodContext.info.getIdeAlias()); + vscode.commands.executeCommand('setContext', 'gitpod.UIKind', vscode.env.uiKind === vscode.UIKind.Web ? 'web' : 'desktop'); + + registerUsageAnalytics(gitpodContext); + registerActiveLanguageAnalytics(gitpodContext); + registerWorkspaceCommands(gitpodContext); + registerWorkspaceSharing(gitpodContext); + registerWorkspaceTimeout(gitpodContext); + registerNotifications(gitpodContext); + registerDefaultLayout(gitpodContext); + return gitpodContext; +} + +function logContextInfo(context: GitpodExtensionContext) { + context.logger.info(`VSCODE_MACHINE_ID: ${vscode.env.machineId}`); + context.logger.info(`VSCODE_SESSION_ID: ${vscode.env.sessionId}`); + context.logger.info(`VSCODE_VERSION: ${vscode.version}`); + context.logger.info(`VSCODE_APP_NAME: ${vscode.env.appName}`); + context.logger.info(`VSCODE_APP_HOST: ${vscode.env.appHost}`); + context.logger.info(`VSCODE_UI_KIND: ${vscode.env.uiKind === vscode.UIKind.Web ? 'web' : 'desktop'}`); + + context.logger.info(`GITPOD_WORKSPACE_CONTEXT_URL: ${context.info.getWorkspaceContextUrl()}`); + context.logger.info(`GITPOD_INSTANCE_ID: ${context.info.getInstanceId()}`); + context.logger.info(`GITPOD_WORKSPACE_URL: ${context.info.getWorkspaceUrl()}`); +} diff --git a/extensions/gitpod-shared/src/features.ts b/extensions/gitpod-shared/src/features.ts new file mode 100644 index 0000000000000..8948e5ebf6cb5 --- /dev/null +++ b/extensions/gitpod-shared/src/features.ts @@ -0,0 +1,1096 @@ +/*--------------------------------------------------------------------------------------------- + * Copyright (c) Gitpod. All rights reserved. + *--------------------------------------------------------------------------------------------*/ +/// + +// TODO get rid of loading inversify and reflect-metadata +require('reflect-metadata'); +import { GitpodClient, GitpodServer, GitpodServiceImpl, WorkspaceInstanceUpdateListener } from '@gitpod/gitpod-protocol/lib/gitpod-service'; +import { JsonRpcProxyFactory } from '@gitpod/gitpod-protocol/lib/messaging/proxy-factory'; +import { NavigatorContext, User } from '@gitpod/gitpod-protocol/lib/protocol'; +import { Team } from '@gitpod/gitpod-protocol/lib/teams-projects-protocol'; +import { ErrorCodes } from '@gitpod/gitpod-protocol/lib/messaging/error'; +import { GitpodHostUrl } from '@gitpod/gitpod-protocol/lib/util/gitpod-host-url'; +import { ControlServiceClient } from '@gitpod/supervisor-api-grpc/lib/control_grpc_pb'; +import { InfoServiceClient } from '@gitpod/supervisor-api-grpc/lib/info_grpc_pb'; +import { WorkspaceInfoRequest, WorkspaceInfoResponse } from '@gitpod/supervisor-api-grpc/lib/info_pb'; +import { NotificationServiceClient } from '@gitpod/supervisor-api-grpc/lib/notification_grpc_pb'; +import { NotifyRequest, NotifyResponse, RespondRequest, SubscribeRequest, SubscribeResponse } from '@gitpod/supervisor-api-grpc/lib/notification_pb'; +import { PortServiceClient } from '@gitpod/supervisor-api-grpc/lib/port_grpc_pb'; +import { StatusServiceClient } from '@gitpod/supervisor-api-grpc/lib/status_grpc_pb'; +import { TasksStatusRequest, TasksStatusResponse, TaskState, TaskStatus } from '@gitpod/supervisor-api-grpc/lib/status_pb'; +import { TerminalServiceClient } from '@gitpod/supervisor-api-grpc/lib/terminal_grpc_pb'; +import { ListenTerminalRequest, ListenTerminalResponse, ListTerminalsRequest, SetTerminalSizeRequest, ShutdownTerminalRequest, Terminal as SupervisorTerminal, TerminalSize as SupervisorTerminalSize, WriteTerminalRequest } from '@gitpod/supervisor-api-grpc/lib/terminal_pb'; +import { TokenServiceClient } from '@gitpod/supervisor-api-grpc/lib/token_grpc_pb'; +import { GetTokenRequest } from '@gitpod/supervisor-api-grpc/lib/token_pb'; +import { PortVisibility } from '@gitpod/gitpod-protocol/lib/workspace-instance'; +import * as grpc from '@grpc/grpc-js'; +import * as fs from 'fs'; +import * as http from 'http'; +import * as path from 'path'; +import ReconnectingWebSocket from 'reconnecting-websocket'; +import { URL } from 'url'; +import * as util from 'util'; +import * as vscode from 'vscode'; +import { CancellationToken, ConsoleLogger, listen as doListen } from 'vscode-ws-jsonrpc'; +import WebSocket = require('ws'); +import { BaseGitpodAnalyticsEventPropeties, GitpodAnalyticsEvent } from './analytics'; +import * as uuid from 'uuid'; +import { RemoteTrackMessage } from '@gitpod/gitpod-protocol/lib/analytics'; +import Log from './common/logger'; +import { TunnelPortRequest, TunnelVisiblity } from '@gitpod/supervisor-api-grpc/lib/port_pb'; +import { isGRPCErrorStatus } from './common/utils'; + +export class SupervisorConnection { + readonly deadlines = { + long: 30 * 1000, + normal: 15 * 1000, + short: 5 * 1000 + }; + private readonly addr = process.env.SUPERVISOR_ADDR || 'localhost:22999'; + private readonly clientOptions: Partial; + readonly metadata = new grpc.Metadata(); + readonly status: StatusServiceClient; + readonly control: ControlServiceClient; + readonly notification: NotificationServiceClient; + readonly token: TokenServiceClient; + readonly info: InfoServiceClient; + readonly port: PortServiceClient; + readonly terminal: TerminalServiceClient; + + constructor( + context: vscode.ExtensionContext + ) { + this.clientOptions = { + 'grpc.primary_user_agent': `${vscode.env.appName}/${vscode.version} ${context.extension.id}/${context.extension.packageJSON.version}`, + }; + this.status = new StatusServiceClient(this.addr, grpc.credentials.createInsecure(), this.clientOptions); + this.control = new ControlServiceClient(this.addr, grpc.credentials.createInsecure(), this.clientOptions); + this.notification = new NotificationServiceClient(this.addr, grpc.credentials.createInsecure(), this.clientOptions); + this.token = new TokenServiceClient(this.addr, grpc.credentials.createInsecure(), this.clientOptions); + this.info = new InfoServiceClient(this.addr, grpc.credentials.createInsecure(), this.clientOptions); + this.port = new PortServiceClient(this.addr, grpc.credentials.createInsecure(), this.clientOptions); + this.terminal = new TerminalServiceClient(this.addr, grpc.credentials.createInsecure(), this.clientOptions); + } +} + +type UsedGitpodFunction = ['getWorkspace', 'openPort', 'stopWorkspace', 'setWorkspaceTimeout', 'getWorkspaceTimeout', 'getLoggedInUser', 'takeSnapshot', 'waitForSnapshot', 'controlAdmission', 'sendHeartBeat', 'trackEvent', 'getTeams']; +type Union = Tuple[number] | Union; +export type GitpodConnection = Omit, 'server'> & { + server: Pick>; +}; + +export class GitpodExtensionContext implements vscode.ExtensionContext { + + readonly sessionId = uuid.v4(); + readonly pendingActivate: Promise[] = []; + readonly workspaceContextUrl: vscode.Uri; + + constructor( + private readonly context: vscode.ExtensionContext, + readonly devMode: boolean, + readonly config: typeof import('./gitpod-plugin-model'), + readonly supervisor: SupervisorConnection, + readonly gitpod: GitpodConnection, + private readonly webSocket: Promise | undefined, + readonly pendingWillCloseSocket: (() => Promise)[], + readonly info: WorkspaceInfoResponse, + readonly owner: Promise, + readonly user: Promise, + readonly userTeams: Promise, + readonly instanceListener: Promise, + readonly workspaceOwned: Promise, + readonly logger: Log, + readonly ipcHookCli: string | undefined + ) { + this.workspaceContextUrl = vscode.Uri.parse(info.getWorkspaceContextUrl()); + } + + get active() { + Object.freeze(this.pendingActivate); + return Promise.all(this.pendingActivate.map(p => p.catch(console.error))); + } + + get subscriptions() { + return this.context.subscriptions; + } + get globalState() { + return this.context.globalState; + } + get workspaceState() { + return this.context.workspaceState; + } + get secrets() { + return this.context.secrets; + } + get extensionUri() { + return this.context.extensionUri; + } + get extensionPath() { + return this.context.extensionPath; + } + get environmentVariableCollection() { + return this.context.environmentVariableCollection; + } + asAbsolutePath(relativePath: string): string { + return this.context.asAbsolutePath(relativePath); + } + get storageUri() { + return this.context.storageUri; + } + get storagePath() { + return this.context.storagePath; + } + get globalStorageUri() { + return this.context.globalStorageUri; + } + get globalStoragePath() { + return this.context.globalStoragePath; + } + get logUri() { + return this.context.logUri; + } + get logPath() { + return this.context.logPath; + } + get extensionMode() { + return this.context.extensionMode; + } + get extension() { + return this.context.extension; + } + get extensionRuntime() { + return (this.context as any).extensionRuntime; + } + + dispose() { + const pendingWebSocket = this.webSocket; + if (!pendingWebSocket) { + return; + } + return (async () => { + try { + const webSocket = await pendingWebSocket; + await Promise.allSettled(this.pendingWillCloseSocket.map(f => f())); + webSocket.close(); + } catch (e) { + this.logger.error('failed to dispose context:', e); + console.error('failed to dispose context:', e); + } + })(); + } + + async fireAnalyticsEvent({ eventName, properties }: GitpodAnalyticsEvent): Promise { + const baseProperties: BaseGitpodAnalyticsEventPropeties = { + sessionId: this.sessionId, + workspaceId: this.info.getWorkspaceId(), + instanceId: this.info.getInstanceId(), + appName: vscode.env.appName, + uiKind: vscode.env.uiKind === vscode.UIKind.Web ? 'web' : 'desktop', + devMode: this.devMode, + version: vscode.version, + timestamp: Date.now(), + 'common.extname': this.extension.id, + 'common.extversion': this.extension.packageJSON.version + }; + const msg: RemoteTrackMessage = { + event: eventName, + properties: { + ...baseProperties, + ...properties, + } + }; + if (this.devMode && vscode.env.uiKind === vscode.UIKind.Web) { + this.logger.trace(`ANALYTICS: ${JSON.stringify(msg)} `); + return Promise.resolve(); + } + try { + await this.gitpod.server.trackEvent(msg); + } catch (e) { + this.logger.error('failed to track event:', e); + console.error('failed to track event:', e); + } + } + + async setPortVisibility(port: number, visibility: PortVisibility): Promise { + await this.gitpod.server.openPort(this.info.getWorkspaceId(), { + port, + visibility + }); + } + + async setTunnelVisibility(port: number, targetPort: number, visibility: TunnelVisiblity): Promise { + const request = new TunnelPortRequest(); + request.setPort(port); + request.setTargetPort(targetPort); + request.setVisibility(visibility); + await util.promisify(this.supervisor.port.tunnel.bind(this.supervisor.port, request, this.supervisor.metadata, { + deadline: Date.now() + this.supervisor.deadlines.normal + }))(); + } +} + +export async function createGitpodExtensionContext(context: vscode.ExtensionContext): Promise { + const logger = new Log('Gitpod Workspace'); + const devMode = context.extensionMode === vscode.ExtensionMode.Development || !!process.env['VSCODE_DEV']; + + const supervisor = new SupervisorConnection(context); + + const workspaceInfo = await util.promisify(supervisor.info.workspaceInfo.bind(supervisor.info, new WorkspaceInfoRequest(), supervisor.metadata, { + deadline: Date.now() + supervisor.deadlines.long + }))(); + + const workspaceId = workspaceInfo.getWorkspaceId(); + const gitpodHost = workspaceInfo.getGitpodHost(); + const gitpodApi = workspaceInfo.getGitpodApi()!; + + const factory = new JsonRpcProxyFactory(); + const gitpodFunctions: UsedGitpodFunction = ['getWorkspace', 'openPort', 'stopWorkspace', 'setWorkspaceTimeout', 'getWorkspaceTimeout', 'getLoggedInUser', 'takeSnapshot', 'waitForSnapshot', 'controlAdmission', 'sendHeartBeat', 'trackEvent', 'getTeams']; + const gitpodService: GitpodConnection = new GitpodServiceImpl(factory.createProxy()) as any; + const gitpodScopes = new Set([ + 'resource:workspace::' + workspaceId + '::get/update', + 'function:accessCodeSyncStorage', + ]); + for (const gitpodFunction of gitpodFunctions) { + gitpodScopes.add('function:' + gitpodFunction); + } + const pendingServerToken = (async () => { + const getTokenRequest = new GetTokenRequest(); + getTokenRequest.setKind('gitpod'); + getTokenRequest.setHost(gitpodApi.getHost()); + for (const scope of gitpodScopes) { + getTokenRequest.addScope(scope); + } + const getTokenResponse = await util.promisify(supervisor.token.getToken.bind(supervisor.token, getTokenRequest, supervisor.metadata, { + deadline: Date.now() + supervisor.deadlines.long + }))(); + return getTokenResponse.getToken(); + })(); + const pendingWillCloseSocket: (() => Promise)[] = []; + const pendignWebSocket = (async () => { + const serverToken = await pendingServerToken; + class GitpodServerWebSocket extends WebSocket { + constructor(address: string, protocols?: string | string[]) { + super(address, protocols, { + headers: { + 'Origin': new URL(gitpodHost).origin, + 'Authorization': `Bearer ${serverToken}`, + 'User-Agent': `${vscode.env.appName}/${vscode.version} ${context.extension.id}/${context.extension.packageJSON.version}`, + } + }); + } + } + const webSocket = new ReconnectingWebSocket(gitpodApi.getEndpoint(), undefined, { + maxReconnectionDelay: 10000, + minReconnectionDelay: 1000, + reconnectionDelayGrowFactor: 1.3, + connectionTimeout: 10000, + maxRetries: Infinity, + debug: false, + startClosed: false, + WebSocket: GitpodServerWebSocket + }); + webSocket.onerror = console.error; + doListen({ + webSocket, + onConnection: connection => factory.listen(connection), + logger: new ConsoleLogger() + }); + return webSocket; + })(); + + const pendingGetOwner = gitpodService.server.getLoggedInUser(); + const pendingGetUser = (async () => { + if (devMode || vscode.env.uiKind !== vscode.UIKind.Web) { + return pendingGetOwner; + } + return vscode.commands.executeCommand('gitpod.api.getLoggedInUser') as typeof pendingGetOwner; + })(); + const pendingGetUserTeams = gitpodService.server.getTeams(); + const pendingInstanceListener = gitpodService.listenToInstance(workspaceId); + const pendingWorkspaceOwned = (async () => { + const owner = await pendingGetOwner; + const user = await pendingGetUser; + const workspaceOwned = owner.id === user.id; + vscode.commands.executeCommand('setContext', 'gitpod.workspaceOwned', workspaceOwned); + return workspaceOwned; + })(); + + const ipcHookCli = installCLIProxy(context, logger); + + const config = await import('./gitpod-plugin-model'); + return new GitpodExtensionContext( + context, + devMode, + config, + supervisor, + gitpodService, + pendignWebSocket, + pendingWillCloseSocket, + workspaceInfo, + pendingGetOwner, + pendingGetUser, + pendingGetUserTeams, + pendingInstanceListener, + pendingWorkspaceOwned, + logger, + ipcHookCli + ); +} + +export async function registerWorkspaceCommands(context: GitpodExtensionContext): Promise { + context.subscriptions.push(vscode.commands.registerCommand('gitpod.open.dashboard', () => { + const url = context.info.getGitpodHost(); + context.fireAnalyticsEvent({ + eventName: 'vscode_execute_command_gitpod_open_link', + properties: { url } + }); + return vscode.env.openExternal(vscode.Uri.parse(url)); + })); + context.subscriptions.push(vscode.commands.registerCommand('gitpod.open.accessControl', () => { + const url = new GitpodHostUrl(context.info.getGitpodHost()).asAccessControl().toString(); + context.fireAnalyticsEvent({ + eventName: 'vscode_execute_command_gitpod_open_link', + properties: { url } + }); + return vscode.env.openExternal(vscode.Uri.parse(url)); + })); + context.subscriptions.push(vscode.commands.registerCommand('gitpod.open.settings', () => { + const url = new GitpodHostUrl(context.info.getGitpodHost()).asSettings().toString(); + context.fireAnalyticsEvent({ + eventName: 'vscode_execute_command_gitpod_open_link', + properties: { url } + }); + return vscode.env.openExternal(vscode.Uri.parse(url)); + })); + context.subscriptions.push(vscode.commands.registerCommand('gitpod.open.context', () => { + const url = context.workspaceContextUrl.toString(); + context.fireAnalyticsEvent({ + eventName: 'vscode_execute_command_gitpod_open_link', + properties: { url } + }); + return vscode.env.openExternal(vscode.Uri.parse(url)); + })); + context.subscriptions.push(vscode.commands.registerCommand('gitpod.open.documentation', () => { + const url = 'https://www.gitpod.io/docs'; + context.fireAnalyticsEvent({ + eventName: 'vscode_execute_command_gitpod_open_link', + properties: { url } + }); + return vscode.env.openExternal(vscode.Uri.parse(url)); + })); + context.subscriptions.push(vscode.commands.registerCommand('gitpod.open.twitter', () => { + const url = 'https://twitter.com/gitpod'; + context.fireAnalyticsEvent({ + eventName: 'vscode_execute_command_gitpod_open_link', + properties: { url } + }); + return vscode.env.openExternal(vscode.Uri.parse(url)); + })); + context.subscriptions.push(vscode.commands.registerCommand('gitpod.open.discord', () => { + const url = 'https://www.gitpod.io/chat'; + context.fireAnalyticsEvent({ + eventName: 'vscode_execute_command_gitpod_open_link', + properties: { url } + }); + return vscode.env.openExternal(vscode.Uri.parse(url)); + })); + context.subscriptions.push(vscode.commands.registerCommand('gitpod.reportIssue', () => { + const url = 'https://github.com/gitpod-io/gitpod/issues/new/choose'; + context.fireAnalyticsEvent({ + eventName: 'vscode_execute_command_gitpod_open_link', + properties: { url } + }); + return vscode.env.openExternal(vscode.Uri.parse(url)); + })); + + if (vscode.env.uiKind === vscode.UIKind.Web) { + function openDesktop(scheme: 'vscode' | 'vscode-insiders'): void { + const uri = vscode.workspace.workspaceFile || vscode.workspace.workspaceFolders?.[0]?.uri; + vscode.env.openExternal(vscode.Uri.from({ + scheme, + authority: 'gitpod.gitpod-desktop', + path: uri?.path || context.info.getWorkspaceLocationFile() || context.info.getWorkspaceLocationFolder() || context.info.getCheckoutLocation(), + query: JSON.stringify({ + instanceId: context.info.getInstanceId(), + workspaceId: context.info.getWorkspaceId(), + gitpodHost: context.info.getGitpodHost() + }) + })); + } + context.subscriptions.push(vscode.commands.registerCommand('gitpod.openInStable', () => { + context.fireAnalyticsEvent({ + eventName: 'vscode_execute_command_gitpod_change_vscode_type', + properties: { targetUiKind: 'desktop', targetQualifier: 'stable' } + }); + return openDesktop('vscode'); + })); + context.subscriptions.push(vscode.commands.registerCommand('gitpod.openInInsiders', () => { + context.fireAnalyticsEvent({ + eventName: 'vscode_execute_command_gitpod_change_vscode_type', + properties: { targetUiKind: 'desktop', targetQualifier: 'insiders' } + }); + return openDesktop('vscode-insiders'); + })); + } + if (vscode.env.uiKind === vscode.UIKind.Desktop) { + context.subscriptions.push(vscode.commands.registerCommand('gitpod.openInBrowser', () => { + const url = context.info.getWorkspaceUrl(); + context.fireAnalyticsEvent({ + eventName: 'vscode_execute_command_gitpod_change_vscode_type', + properties: { targetUiKind: 'web' } + }); + return vscode.env.openExternal(vscode.Uri.parse(url)); + })); + } + + const workspaceOwned = await context.workspaceOwned; + if (!workspaceOwned) { + return; + } + context.subscriptions.push(vscode.commands.registerCommand('gitpod.stop.ws', () => { + context.fireAnalyticsEvent({ + eventName: 'vscode_execute_command_gitpod_workspace', + properties: { action: 'stop' } + }); + return context.gitpod.server.stopWorkspace(context.info.getWorkspaceId()); + })); + context.subscriptions.push(vscode.commands.registerCommand('gitpod.upgradeSubscription', () => { + const url = new GitpodHostUrl(context.info.getGitpodHost()).asUpgradeSubscription().toString(); + context.fireAnalyticsEvent({ + eventName: 'vscode_execute_command_gitpod_open_link', + properties: { url } + }); + return vscode.env.openExternal(vscode.Uri.parse(url)); + })); + context.subscriptions.push(vscode.commands.registerCommand('gitpod.takeSnapshot', async () => { + context.fireAnalyticsEvent({ + eventName: 'vscode_execute_command_gitpod_workspace', + properties: { action: 'snapshot' } + }); + try { + let snapshotId: string | undefined = undefined; + await vscode.window.withProgress({ + location: vscode.ProgressLocation.Notification, + cancellable: true, + title: 'Capturing workspace snapshot' + }, async (_, cancelToken: CancellationToken) => { + snapshotId = await context.gitpod.server.takeSnapshot({ workspaceId: context.info.getWorkspaceId() /*, layoutData?*/, dontWait: true }); + + while (!cancelToken.isCancellationRequested) { + try { + await context.gitpod.server.waitForSnapshot(snapshotId); + return; + } catch (err) { + if (err.code === ErrorCodes.SNAPSHOT_ERROR || err.code === ErrorCodes.NOT_FOUND) { + // this is indeed an error with snapshot creation itself, break here! + throw err; + } + + // other errors (like connection errors): retry + await new Promise((resolve) => setTimeout(resolve, 3000)); + } + } + }); + if (!snapshotId) { + throw new Error('error taking snapshot'); + } + + const hostname = context.info.getGitpodApi()!.getHost(); + const uri = `https://${hostname}#snapshot/${snapshotId}`; + const copyAction = await vscode.window.showInformationMessage(`The current state is captured in a snapshot. Using [this link](${uri}) anybody can create their own copy of this workspace.`, + 'Copy URL to Clipboard'); + if (copyAction === 'Copy URL to Clipboard') { + await vscode.env.clipboard.writeText(uri); + } + } catch (err) { + console.error('cannot capture workspace snapshot', err); + await vscode.window.showErrorMessage(`Cannot capture workspace snapshot: ${err.toString()}`); + } + })); +} + +export async function registerWorkspaceSharing(context: GitpodExtensionContext): Promise { + const owner = await context.owner; + const workspaceOwned = await context.workspaceOwned; + const workspaceSharingStatusBarItem = vscode.window.createStatusBarItem('gitpod.workspaceSharing', vscode.StatusBarAlignment.Left); + workspaceSharingStatusBarItem.name = 'Workspace Sharing'; + context.subscriptions.push(workspaceSharingStatusBarItem); + function setWorkspaceShared(workspaceShared: boolean): void { + if (workspaceOwned) { + vscode.commands.executeCommand('setContext', 'gitpod.workspaceShared', workspaceShared); + if (workspaceShared) { + workspaceSharingStatusBarItem.text = '$(broadcast) Shared'; + workspaceSharingStatusBarItem.tooltip = 'Your workspace is currently shared. Anyone with the link can access this workspace.'; + workspaceSharingStatusBarItem.command = 'gitpod.stopSharingWorkspace'; + } else { + workspaceSharingStatusBarItem.text = '$(live-share) Share'; + workspaceSharingStatusBarItem.tooltip = 'Your workspace is currently not shared. Only you can access it.'; + workspaceSharingStatusBarItem.command = 'gitpod.shareWorkspace'; + } + } else { + workspaceSharingStatusBarItem.text = '$(broadcast) Shared by ' + owner.name; + workspaceSharingStatusBarItem.tooltip = `You are currently accessing the workspace shared by ${owner.name}.`; + } + workspaceSharingStatusBarItem.show(); + } + const listener = await context.instanceListener; + setWorkspaceShared(listener.info.workspace.shareable || false); + if (!workspaceOwned) { + return; + } + async function controlAdmission(level: GitpodServer.AdmissionLevel): Promise { + try { + if (level === 'everyone') { + const confirm = await vscode.window.showWarningMessage('Sharing your workspace with others also means sharing your access to your repository. Everyone with access to the workspace you share can commit in your name.', { modal: true }, 'Share'); + if (confirm !== 'Share') { + return; + } + } + await vscode.window.withProgress({ + location: vscode.ProgressLocation.Notification, + cancellable: true, + title: level === 'everyone' ? 'Sharing workspace...' : 'Stopping workspace sharing...' + }, _ => { + return context.gitpod.server.controlAdmission(context.info.getWorkspaceId(), level); + }); + setWorkspaceShared(level === 'everyone'); + if (level === 'everyone') { + const uri = context.info.getWorkspaceUrl(); + const copyToClipboard = 'Copy URL to Clipboard'; + const res = await vscode.window.showInformationMessage(`Your workspace is currently shared. Anyone with [the link](${uri}) can access this workspace.`, copyToClipboard); + if (res === copyToClipboard) { + await vscode.env.clipboard.writeText(uri); + } + } else { + await vscode.window.showInformationMessage(`Your workspace is currently not shared. Only you can access it.`); + } + } catch (err) { + console.error('cannot controlAdmission', err); + if (level === 'everyone') { + await vscode.window.showErrorMessage(`Cannot share workspace: ${err.toString()}`); + } else { + await vscode.window.showInformationMessage(`Cannot stop workspace sharing: ${err.toString()}`); + } + } + } + context.subscriptions.push(vscode.commands.registerCommand('gitpod.shareWorkspace', () => { + context.fireAnalyticsEvent({ + eventName: 'vscode_execute_command_gitpod_workspace', + properties: { action: 'share' } + }); + return controlAdmission('everyone'); + })); + context.subscriptions.push(vscode.commands.registerCommand('gitpod.stopSharingWorkspace', () => { + context.fireAnalyticsEvent({ + eventName: 'vscode_execute_command_gitpod_workspace', + properties: { action: 'stop-sharing' } + }); + return controlAdmission('owner'); + })); +} + +export async function registerWorkspaceTimeout(context: GitpodExtensionContext): Promise { + const workspaceOwned = await context.workspaceOwned; + if (!workspaceOwned) { + return; + } + + context.subscriptions.push(vscode.commands.registerCommand('gitpod.ExtendTimeout', async () => { + context.fireAnalyticsEvent({ + eventName: 'vscode_execute_command_gitpod_workspace', + properties: { + action: 'extend-timeout' + } + }); + try { + const result = await context.gitpod.server.setWorkspaceTimeout(context.info.getWorkspaceId(), '180m'); + if (result.resetTimeoutOnWorkspaces?.length > 0) { + vscode.window.showWarningMessage('Workspace timeout has been extended to three hours. This reset the workspace timeout for other workspaces.'); + } else { + vscode.window.showInformationMessage('Workspace timeout has been extended to three hours.'); + } + } catch (err) { + vscode.window.showErrorMessage(`Cannot extend workspace timeout: ${err.toString()}`); + } + })); + + const workspaceTimeout = await context.gitpod.server.getWorkspaceTimeout(context.info.getWorkspaceId()); + if (!workspaceTimeout.canChange) { + return; + } + + const listener = await context.instanceListener; + const extendTimeoutStatusBarItem = vscode.window.createStatusBarItem('gitpod.extendTimeout', vscode.StatusBarAlignment.Right, -100); + extendTimeoutStatusBarItem.name = 'Click to extend the workspace timeout.'; + context.subscriptions.push(extendTimeoutStatusBarItem); + extendTimeoutStatusBarItem.text = '$(watch)'; + extendTimeoutStatusBarItem.command = 'gitpod.ExtendTimeout'; + const update = () => { + const instance = listener.info.latestInstance; + if (!instance) { + extendTimeoutStatusBarItem.hide(); + return; + } + extendTimeoutStatusBarItem.tooltip = `Workspace Timeout: ${instance.status.timeout}. Click to extend.`; + extendTimeoutStatusBarItem.color = instance.status.timeout === '180m' ? new vscode.ThemeColor('notificationsWarningIcon.foreground') : undefined; + extendTimeoutStatusBarItem.show(); + }; + update(); + context.subscriptions.push(listener.onDidChange(update)); +} + +export function registerNotifications(context: GitpodExtensionContext): void { + function observeNotifications(): vscode.Disposable { + let run = true; + let stopUpdates: Function | undefined; + (async () => { + while (run) { + try { + const evts = context.supervisor.notification.subscribe(new SubscribeRequest(), context.supervisor.metadata); + stopUpdates = evts.cancel.bind(evts); + + await new Promise((resolve, reject) => { + evts.on('end', resolve); + evts.on('error', reject); + evts.on('data', async (result: SubscribeResponse) => { + const request = result.getRequest(); + if (request) { + const level = request.getLevel(); + const message = request.getMessage(); + const actions = request.getActionsList(); + let choice: string | undefined; + switch (level) { + case NotifyRequest.Level.ERROR: + choice = await vscode.window.showErrorMessage(message, ...actions); + break; + case NotifyRequest.Level.WARNING: + choice = await vscode.window.showWarningMessage(message, ...actions); + break; + case NotifyRequest.Level.INFO: + default: + choice = await vscode.window.showInformationMessage(message, ...actions); + } + const respondRequest = new RespondRequest(); + const notifyResponse = new NotifyResponse(); + notifyResponse.setAction(choice || ''); + respondRequest.setResponse(notifyResponse); + respondRequest.setRequestid(result.getRequestid()); + context.supervisor.notification.respond(respondRequest, context.supervisor.metadata, { + deadline: Date.now() + context.supervisor.deadlines.normal + }, (error, _) => { + if (error?.code !== grpc.status.DEADLINE_EXCEEDED) { + reject(error); + } + }); + } + }); + }); + } catch (err) { + if (isGRPCErrorStatus(err, grpc.status.UNIMPLEMENTED)) { + console.warn('supervisor does not implement the notification server'); + run = false; + } else if (!isGRPCErrorStatus(err, grpc.status.CANCELLED)) { + console.error('cannot maintain connection to supervisor', err); + } + } finally { + stopUpdates = undefined; + } + await new Promise(resolve => setTimeout(resolve, 1000)); + } + })(); + return new vscode.Disposable(() => { + run = false; + if (stopUpdates) { + stopUpdates(); + } + }); + } + context.subscriptions.push(observeNotifications()); +} + +export function registerDefaultLayout(context: GitpodExtensionContext): void { + const layoutInitializedKey = 'gitpod:layoutInitialized'; + const layoutInitialized = Boolean(context.globalState.get(layoutInitializedKey)); + if (!layoutInitialized) { + context.globalState.update(layoutInitializedKey, true); + + (async () => { + const listener = await context.instanceListener; + const workspaceContext = listener.info.workspace.context; + + if (NavigatorContext.is(workspaceContext)) { + const location = vscode.Uri.file(path.join(context.info.getCheckoutLocation(), workspaceContext.path)); + if (workspaceContext.isFile) { + vscode.window.showTextDocument(location); + } else { + vscode.commands.executeCommand('revealInExplorer', location); + } + } + })(); + } +} + +function installCLIProxy(context: vscode.ExtensionContext, logger: Log): string | undefined { + const vscodeIpcHookCli = process.env['VSCODE_IPC_HOOK_CLI']; + if (!vscodeIpcHookCli) { + return undefined; + } + const { dir, base } = path.parse(vscodeIpcHookCli); + const ipcHookCli = path.join(dir, 'gitpod-' + base); + const ipcProxy = http.createServer((req, res) => { + const chunks: string[] = []; + req.setEncoding('utf8'); + req.on('data', (d: string) => chunks.push(d)); + req.pipe(http.request({ + socketPath: vscodeIpcHookCli, + method: req.method, + headers: req.headers + }, async res2 => { + if (res2.statusCode === 404) { + const data: { type: 'preview'; url: string } | any = JSON.parse(chunks.join('')); + if (data.type === 'preview') { + // should be aligned with https://github.com/gitpod-io/vscode/blob/4d36a5dbf36870beda891e5dd94ccf087fdc7eb5/src/vs/workbench/api/node/extHostCLIServer.ts#L207-L207 + try { + const { url } = data; + await vscode.commands.executeCommand('simpleBrowser.api.open', url, { + viewColumn: vscode.ViewColumn.Beside, + preserveFocus: true + }); + res.writeHead(200, { 'content-type': 'application/json' }); + res.end(JSON.stringify('')); + } catch (e) { + console.error(e); + const message = e instanceof Error ? e.message : JSON.stringify(e); + res.writeHead(500, { 'content-type': 'application/json' }); + res.end(JSON.stringify(message)); + } + return; + } + } + res.setHeader('Content-Type', 'application/json'); + res2.pipe(res); + })); + }); + context.subscriptions.push(new vscode.Disposable(() => ipcProxy.close())); + + new Promise((_, reject) => { + ipcProxy.on('error', err => reject(err)); + ipcProxy.listen(ipcHookCli); + context.subscriptions.push(new vscode.Disposable(() => + fs.promises.unlink(ipcHookCli) + )); + }).catch(e => { + logger.error('failed to start cli proxy: ' + e); + console.error('failed to start cli proxy:' + e); + }); + + return ipcHookCli; +} + +type TerminalOpenMode = 'tab-before' | 'tab-after' | 'split-left' | 'split-right' | 'split-top' | 'split-bottom'; + +export async function registerTasks(context: GitpodExtensionContext): Promise { + const tokenSource = new vscode.CancellationTokenSource(); + const token = tokenSource.token; + context.subscriptions.push({ + dispose: () => tokenSource.cancel() + }); + + const tasks = new Map(); + let synched = false; + while (!synched) { + let listener: vscode.Disposable | undefined; + try { + const req = new TasksStatusRequest(); + req.setObserve(true); + const stream = context.supervisor.status.tasksStatus(req, context.supervisor.metadata); + const done = () => { + synched = true; + stream.cancel(); + }; + listener = token.onCancellationRequested(() => done()); + await new Promise((resolve, reject) => { + stream.on('end', resolve); + stream.on('error', reject); + stream.on('data', (response: TasksStatusResponse) => { + if (response.getTasksList().every(status => { + tasks.set(status.getTerminal(), status); + return status.getState() !== TaskState.OPENING; + })) { + done(); + } + }); + }); + } catch (err) { + if (!isGRPCErrorStatus(err, grpc.status.CANCELLED)) { + context.logger.error('code server: listening task updates failed:', err); + console.error('code server: listening task updates failed:', err); + } + } finally { + listener?.dispose(); + } + if (!synched) { + await new Promise(resolve => setTimeout(resolve, 1000)); + } + } + context.logger.trace('Task status:', [...tasks.values()].map(status => { + const stateMap = { [TaskState.OPENING]: 'CLOSED', [TaskState.RUNNING]: 'RUNNING', [TaskState.CLOSED]: 'CLOSED' }; + return `\t${status.getTerminal()} => ${stateMap[status.getState()]}`; + }).join('\n')); + + if (token.isCancellationRequested) { + return; + } + + const taskTerminals = new Map(); + try { + const response = await util.promisify(context.supervisor.terminal.list.bind(context.supervisor.terminal, new ListTerminalsRequest(), context.supervisor.metadata, { + deadline: Date.now() + context.supervisor.deadlines.long + }))(); + for (const term of response.getTerminalsList()) { + taskTerminals.set(term.getAlias(), term); + } + } catch (e) { + context.logger.error('failed to list task terminals:', e); + console.error('failed to list task terminals:', e); + } + + let prevTerminal: vscode.Terminal | undefined; + for (const [alias, taskStatus] of tasks.entries()) { + const taskTerminal = taskTerminals.get(alias); + if (taskTerminal) { + const openMode: TerminalOpenMode | undefined = taskStatus.getPresentation()?.getOpenMode() as TerminalOpenMode; + const parentTerminal = (openMode && openMode !== 'tab-before' && openMode !== 'tab-after') ? prevTerminal : undefined; + const pty = createTaskPty(alias, context, token); + + const terminal = vscode.window.createTerminal({ + name: taskTerminal.getTitle(), + pty, + iconPath: new vscode.ThemeIcon('terminal'), + location: parentTerminal ? { parentTerminal } : vscode.TerminalLocation.Panel + }); + terminal.show(); + prevTerminal = terminal; + } + } +} + +function createTaskPty(alias: string, context: GitpodExtensionContext, contextToken: vscode.CancellationToken): vscode.Pseudoterminal { + const tokenSource = new vscode.CancellationTokenSource(); + contextToken.onCancellationRequested(() => tokenSource.cancel()); + const token = tokenSource.token; + + const onDidWriteEmitter = new vscode.EventEmitter(); + const onDidCloseEmitter = new vscode.EventEmitter(); + const onDidChangeNameEmitter = new vscode.EventEmitter(); + const toDispose = vscode.Disposable.from(onDidWriteEmitter, onDidCloseEmitter, onDidChangeNameEmitter); + token.onCancellationRequested(() => toDispose.dispose()); + + let pendingWrite = Promise.resolve(); + let pendingResize = Promise.resolve(); + const pty: vscode.Pseudoterminal = { + onDidWrite: onDidWriteEmitter.event, + onDidClose: onDidCloseEmitter.event, + onDidChangeName: onDidChangeNameEmitter.event, + open: async (dimensions: vscode.TerminalDimensions | undefined) => { + if (dimensions) { + pty.setDimensions!(dimensions); + } + while (!token.isCancellationRequested) { + let notFound = false; + let exitCode: number | undefined; + let listener: vscode.Disposable | undefined; + try { + await new Promise((resolve, reject) => { + const request = new ListenTerminalRequest(); + request.setAlias(alias); + const stream = context.supervisor.terminal.listen(request, context.supervisor.metadata); + listener = token.onCancellationRequested(() => stream.cancel()); + stream.on('end', resolve); + stream.on('error', reject); + stream.on('data', (response: ListenTerminalResponse) => { + if (response.hasTitle()) { + const title = response.getTitle(); + if (title) { + onDidChangeNameEmitter.fire(title); + } + } else if (response.hasData()) { + let data = ''; + const buffer = response.getData(); + if (typeof buffer === 'string') { + data += buffer; + } else { + data += Buffer.from(buffer).toString(); + } + if (data) { + onDidWriteEmitter.fire(data); + } + } else if (response.hasExitCode()) { + exitCode = response.getExitCode(); + } + }); + }); + } catch (e) { + notFound = isGRPCErrorStatus(e, grpc.status.NOT_FOUND); + if (!token.isCancellationRequested && !notFound && !isGRPCErrorStatus(e, grpc.status.CANCELLED)) { + context.logger.error(`${alias} terminal: listening failed:`, e); + console.error(`${alias} terminal: listening failed:`, e); + } + } finally { + listener?.dispose(); + } + if (token.isCancellationRequested) { + return; + } + if (notFound) { + context.logger.trace(`${alias} terminal not found`); + onDidCloseEmitter.fire(); + tokenSource.cancel(); + return; + } + if (typeof exitCode === 'number') { + context.logger.trace(`${alias} terminal exited with ${exitCode}`); + onDidCloseEmitter.fire(exitCode); + tokenSource.cancel(); + return; + } + await new Promise(resolve => setTimeout(resolve, 2000)); + } + }, + close: async () => { + if (token.isCancellationRequested) { + return; + } + tokenSource.cancel(); + + // await to make sure that close is not cause by the extension host process termination + // in such case we don't want to stop supervisor terminals + setTimeout(async () => { + if (contextToken.isCancellationRequested) { + return; + } + // Attempt to kill the pty, it may have already been killed at this + // point but we want to make sure + try { + const request = new ShutdownTerminalRequest(); + request.setAlias(alias); + await util.promisify(context.supervisor.terminal.shutdown.bind(context.supervisor.terminal, request, context.supervisor.metadata, { + deadline: Date.now() + context.supervisor.deadlines.short + }))(); + context.logger.trace(`${alias} terminal closed`); + } catch (e) { + if (e && e.code === grpc.status.NOT_FOUND) { + // Swallow, the pty has already been killed + } else { + context.logger.error(`${alias} terminal: shutdown failed:`, e); + console.error(`${alias} terminal: shutdown failed:`, e); + } + } + }, 1000); + + }, + handleInput: async (data: string) => { + if (token.isCancellationRequested) { + return; + } + pendingWrite = pendingWrite.then(async () => { + if (token.isCancellationRequested) { + return; + } + try { + const request = new WriteTerminalRequest(); + request.setAlias(alias); + request.setStdin(Buffer.from(data, 'utf8')); + await util.promisify(context.supervisor.terminal.write.bind(context.supervisor.terminal, request, context.supervisor.metadata, { + deadline: Date.now() + context.supervisor.deadlines.short + }))(); + } catch (e) { + if (e && e.code !== grpc.status.NOT_FOUND) { + context.logger.error(`${alias} terminal: write failed:`, e); + console.error(`${alias} terminal: write failed:`, e); + } + } + }); + }, + setDimensions: (dimensions: vscode.TerminalDimensions) => { + if (token.isCancellationRequested) { + return; + } + pendingResize = pendingResize.then(async () => { + if (token.isCancellationRequested) { + return; + } + try { + const size = new SupervisorTerminalSize(); + size.setCols(dimensions.columns); + size.setRows(dimensions.rows); + + const request = new SetTerminalSizeRequest(); + request.setAlias(alias); + request.setSize(size); + request.setForce(true); + await util.promisify(context.supervisor.terminal.setSize.bind(context.supervisor.terminal, request, context.supervisor.metadata, { + deadline: Date.now() + context.supervisor.deadlines.short + }))(); + } catch (e) { + if (e && e.code !== grpc.status.NOT_FOUND) { + context.logger.error(`${alias} terminal: resize failed:`, e); + console.error(`${alias} terminal: resize failed:`, e); + } + } + }); + } + }; + + return pty; +} + +/** + * configure CLI in task terminals + */ +export function registerIpcHookCli(context: GitpodExtensionContext): void { + const ipcHookCli = context.ipcHookCli; + if (!ipcHookCli) { + return; + } + + updateIpcHookCli(context); + context.subscriptions.push(vscode.window.onDidChangeWindowState(() => updateIpcHookCli(context))); +} + + +async function updateIpcHookCli(context: GitpodExtensionContext): Promise { + if (!context.ipcHookCli) { + return; + } + + try { + await new Promise((resolve, reject) => { + const req = http.request({ + hostname: 'localhost', + port: context.devMode ? 9888 /* From code-web.js */ : context.info.getIdePort(), + protocol: 'http:', + path: `/cli/ipcHookCli/${encodeURIComponent(context.ipcHookCli!)}`, + method: vscode.window.state.focused ? 'PUT' : 'DELETE' + }, 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(undefined); + } + }); + }); + req.on('error', err => reject(err)); + req.end(); + }); + } catch (e) { + context.logger.error('Failed to update gitpod ipc hook cli:', e); + console.error('Failed to update gitpod ipc hook cli:', e); + } +} diff --git a/extensions/gitpod-shared/src/gitpod-plugin-model.ts b/extensions/gitpod-shared/src/gitpod-plugin-model.ts new file mode 100644 index 0000000000000..2ad4ea29f41de --- /dev/null +++ b/extensions/gitpod-shared/src/gitpod-plugin-model.ts @@ -0,0 +1,255 @@ +/*--------------------------------------------------------------------------------------------- + * Copyright (c) Gitpod. All rights reserved. + *--------------------------------------------------------------------------------------------*/ + +import * as os from 'os'; +import * as yaml from 'yaml'; +import * as yamlTypes from 'yaml/types'; +import * as yamlUtil from 'yaml/util'; + +yamlTypes.strOptions.fold.lineWidth = -1; +yamlTypes.strOptions.fold.minContentWidth = 0; + +type YamlAstNode = yamlTypes.Node & YamlNode; +interface YamlNode extends yamlTypes.Node { + getIn(keys: (number | string)[]): YamlAstNode | undefined | null + get(key: number | string, keepScalar: true): YamlAstNode | undefined | null + get(key: number | string, keepScalar?: boolean): YamlAstNode | string | undefined | null +} +function isYamlMap(node: YamlAstNode | undefined): node is yamlTypes.YAMLMap { + return !!node && (node.type === yamlUtil.Type.MAP || node.type === yamlUtil.Type.FLOW_MAP); +} +export function isYamlSeq(node: YamlAstNode | undefined): node is yamlTypes.YAMLSeq { + return !!node && (node.type === yamlUtil.Type.SEQ || node.type === yamlUtil.Type.FLOW_SEQ); +} +export function isYamlScalar(node: yamlTypes.Node | undefined): node is yamlTypes.Scalar { + return !!node && (node.type === yamlUtil.Type.BLOCK_FOLDED || + node.type === yamlUtil.Type.BLOCK_LITERAL || + node.type === yamlUtil.Type.PLAIN || + node.type === yamlUtil.Type.QUOTE_DOUBLE || + node.type === yamlUtil.Type.QUOTE_SINGLE); +} +type YamlDocument = yaml.Document & YamlNode; + +function getNodeIndent(node: yamlTypes.Node | yaml.Document | null): number { + const cstNode = node && node.cstNode; + const context = cstNode && cstNode.context; + return context ? context.indent : -1; +} + +export class GitpodPluginModel { + + readonly document: YamlDocument; + + constructor(content: string) { + this.document = yaml.parseDocument(content, { keepCstNodes: true }); + } + + toString(): string { + return this.document.cstNode!.toString(); + } + + add(...extensions: string[]): boolean { + if (!extensions.length) { + return false; + } + + let indent = -1; + const uniqueExtensions = new Set(extensions.map(uri => uri.trim())); + const toUriParts = (...keys: string[]) => { + const parts: string[] = []; + if (!uniqueExtensions.size) { + return parts; + } + let indentStr = ''; + for (let i = 0; i < indent; ++i) { + indentStr += ' '; + } + for (const key of keys) { + parts.push(`${indentStr}${key}:`); + indentStr += ' '; + } + for (const uri of uniqueExtensions) { + parts.push(`${indentStr}- ${uri}`); + } + return parts; + }; + + indent = getNodeIndent(this.document); + const vscodeNode = this.document.get('vscode', true); + if (vscodeNode === null) { + const contents = this.document.contents; + if (isYamlMap(contents)) { + const vscodeKeyIndex = contents.items.findIndex(i => !!i.key && ('value' in i.key) && i.key.value === 'vscode'); + const item = contents.items[vscodeKeyIndex]; + if (item && item.key && item.key.cstNode) { + this.replace(item.key.cstNode, toUriParts('vscode', 'extensions')); + + const nextKeyIndex = contents.items[vscodeKeyIndex + 1]; + if (item.key.cstNode.context && nextKeyIndex && nextKeyIndex.key && nextKeyIndex.key.cstNode) { + const parent = item.key.cstNode.context.parent as yaml.CST.Map; + if (parent.type === 'MAP') { + const startIndex = parent.items.indexOf(item.key.cstNode as any) + 1; + const endIndex = parent.items.indexOf(nextKeyIndex.key.cstNode as any) - 1; + for (let i = startIndex; i < endIndex; i++) { + this.removeNode(parent.items[i]); + } + } + } + return true; + } + } + return false; + } + if (vscodeNode === undefined) { + return this.append(this.document.cstNode, toUriParts('vscode', 'extensions')); + } + indent = getNodeIndent(vscodeNode) + 2; + if (vscodeNode.type !== 'MAP') { + return this.replace(vscodeNode.cstNode, toUriParts('extensions')); + } + const extensionsNode = vscodeNode.get('extensions', true); + if (extensionsNode === null) { + if (isYamlMap(vscodeNode)) { + const item = vscodeNode.items.find(i => !!i.key && ('value' in i.key) && i.key.value === 'extensions'); + if (item && item.key && item.key.cstNode) { + this.replace(item.key.cstNode, toUriParts('extensions')); + + if (item.key.cstNode.context) { + const parent = item.key.cstNode.context.parent as yaml.CST.Map; + if (parent.type === 'MAP') { + const startIndex = parent.items.indexOf(item.key.cstNode as any) + 1; + for (let i = startIndex; i < parent.items.length; i++) { + this.removeNode(parent.items[i]); + } + } + } + return true; + } + } + return false; + } + indent -= 2; + if (extensionsNode === undefined) { + return this.append(vscodeNode.cstNode, toUriParts('extensions')); + } + indent = getNodeIndent(extensionsNode) + 2; + if (!isYamlSeq(extensionsNode)) { + return this.replace(extensionsNode.cstNode, toUriParts()); + } + indent -= 2; + for (let i = 0; i < extensionsNode.items.length; i++) { + const extension = extensionsNode.get(i); + if (typeof extension === 'string') { + uniqueExtensions.delete(extension.trim()); + } + const extensionIndent = getNodeIndent(extensionsNode.items[i]); + if (extensionIndent > indent) { + indent = extensionIndent; + } + } + return this.append(extensionsNode.cstNode, toUriParts()); + } + + protected append(cstNode: yaml.CST.Node | undefined, parts: string[]): boolean { + if (!cstNode || !parts.length) { + return false; + } + if (cstNode.rawValue) { + parts.unshift(cstNode.rawValue); + } + return this.replace(cstNode, parts); + } + + protected replace(cstNode: yaml.CST.Node | undefined | null, parts: string[]): boolean { + if (!cstNode || !parts.length) { + return false; + } + + const { context } = cstNode; + if (!context) { + return false; + } + + if (context.atLineStart === false) { + parts.unshift(''); + } + if (context.indent > -1 && parts[0]) { + for (let i = -1; i < context.indent; i++) { + if (parts[0].startsWith(' ')) { + parts[0] = parts[0].substr(2); + } + } + } + cstNode.value = parts.join(os.EOL); + return true; + } + + protected removeNode(cstNode: yaml.CST.Node | undefined | null): boolean { + if (!cstNode) { + return false; + } + cstNode.value = ''; + if (cstNode.context) { + cstNode.context.indent = -1; + } + return true; + } + + remove(...extensions: string[]): boolean { + if (!extensions.length) { + return false; + } + + const extensionsNode = this.document.getIn(['vscode', 'extensions']); + if (!extensionsNode || extensionsNode.type !== 'SEQ') { + return false; + } + + const { cstNode } = extensionsNode; + if (!cstNode) { + return false; + } + + const toDelete = new Set(extensions.map(uri => uri.trim())); + let deleted = 0; + let firstDeleted = false; + for (let i = 0; i < extensionsNode.items.length; i++) { + const item = cstNode.items[i]; + const extension = extensionsNode.get(i); + if (typeof extension === 'string' && toDelete.has(extension.trim())) { + deleted++; + if (i === 0) { + firstDeleted = true; + } + this.removeNode(item); + } else if (item.context && firstDeleted) { + item.context.indent = -1; + firstDeleted = false; + } + } + if (deleted === extensionsNode.items.length) { + const contents = this.document.contents; + if (isYamlMap(contents)) { + const vscodeKeyIndex = contents.items.findIndex(i => !!i.key && ('value' in i.key) && i.key.value === 'vscode'); + const item = contents.items[vscodeKeyIndex]; + if (item && item.key && item.key.cstNode && item.key.cstNode.context) { + const parent = item.key.cstNode.context.parent as yaml.CST.Map; + if (parent.type === 'MAP') { + const startIndex = parent.items.indexOf(item.key.cstNode as any); + const nextKeyIndex = contents.items[vscodeKeyIndex + 1]; + let endIndex = parent.items.length; + if (nextKeyIndex && nextKeyIndex.key && nextKeyIndex.key.cstNode) { + endIndex = parent.items.indexOf(nextKeyIndex.key.cstNode as any); + } + for (let i = startIndex; i < endIndex; i++) { + this.removeNode(parent.items[i]); + } + } + } + } + } + return deleted > 0; + } + +} diff --git a/extensions/gitpod-shared/src/test/port.test.ts b/extensions/gitpod-shared/src/test/port.test.ts new file mode 100644 index 0000000000000..64179a09cb38e --- /dev/null +++ b/extensions/gitpod-shared/src/test/port.test.ts @@ -0,0 +1,262 @@ +import { ExposedPortInfo, PortAutoExposure, PortsStatus, PortVisibility, TunneledPortInfo } from '@gitpod/supervisor-api-grpc/lib/status_pb'; +import { GitpodWorkspacePort, PortInfo, TunnelDescriptionI } from '../workspacePort'; +import { TunnelVisiblity } from '@gitpod/supervisor-api-grpc/lib/port_pb'; + +import * as assert from 'assert'; + +describe('GitpodWorkspacePort', () => { + + const genPortsStatus = (obj: PortsStatus.AsObject) => { + function genExposed(obj?: ExposedPortInfo.AsObject) { + if (!obj) { + return; + } + const exposed = new ExposedPortInfo(); + exposed.setVisibility(obj.visibility); + exposed.setUrl(obj.url); + exposed.setOnExposed(obj.onExposed); + return exposed; + } + function genTunneledPortInfo(obj?: TunneledPortInfo.AsObject) { + if (!obj) { + return; + } + const tunneled = new TunneledPortInfo(); + tunneled.setTargetPort(obj.targetPort); + tunneled.setVisibility(obj.visibility); + return tunneled; + } + const ps = new PortsStatus(); + ps.setAutoExposure(obj.autoExposure); + ps.setDescription(obj.description); + ps.setName(obj.name); + ps.setExposed(genExposed(obj.exposed)); + ps.setLocalPort(obj.localPort); + ps.setServed(obj.served); + ps.setTunneled(genTunneledPortInfo(obj.tunneled)); + return ps; + }; + + interface TmpTunnel { + localAddress: { + port: number; + host: string; + } | string; + public: boolean; + } + + interface TestI { ps: Partial; tunnel?: TmpTunnel; result: PortInfo } + + it('parsePortInfo', () => { + const base = { + name: 'name', + description: 'desc', + exposed: { + visibility: PortVisibility.PRIVATE, + url: 'http://localhost:3000' + }, + localPort: 3000, + served: false, + tunneled: { + targetPort: 3000, + visibility: TunnelVisiblity.HOST, + clientsMap: [] as Array<[string, number]>, + }, + autoExposure: PortAutoExposure.TRYING, + onOpen: PortsStatus.OnOpenAction.OPEN_BROWSER, + }; + const cases: TestI[] = [ + { + ps: {}, + tunnel: { + localAddress: 'https://localhost:3001', + public: true, + }, + result: { + label: 'name: 3000:3001', + tooltip: 'name - desc', + description: 'not served', + iconStatus: 'NotServed', + contextValue: 'network-tunneled-private-exposed-port', + localUrl: 'http://localhost:3000', + } + }, + { + ps: { + exposed: Object.assign({}, base, { + visibility: PortVisibility.PUBLIC + }) as any as ExposedPortInfo.AsObject + }, + tunnel: { + localAddress: 'https://localhost:3001', + public: true, + }, + result: { + label: 'name: 3000:3001', + tooltip: 'name - desc', + description: 'not served', + iconStatus: 'NotServed', + contextValue: 'network-tunneled-public-exposed-port', + localUrl: 'http://localhost:3000', + } + }, + { + ps: { + name: undefined, + served: true, + exposed: undefined, + autoExposure: PortAutoExposure.FAILED, + }, + result: { + label: '3000', + tooltip: 'desc', + description: 'failed to expose', + iconStatus: 'ExposureFailed', + contextValue: 'failed-served-port', + localUrl: 'http://localhost:3000', + } + }, + { + ps: { + name: undefined, + served: true, + exposed: undefined, + autoExposure: PortAutoExposure.SUCCEEDED, + }, + result: { + label: '3000', + tooltip: 'desc', + description: 'detecting...', + iconStatus: 'Detecting', + contextValue: 'served-port', + localUrl: 'http://localhost:3000', + } + }, + { + ps: { + name: undefined, + served: true, + exposed: undefined, + autoExposure: PortAutoExposure.TRYING, + }, + result: { + label: '3000', + tooltip: 'desc', + description: 'detecting...', + iconStatus: 'Detecting', + contextValue: 'served-port', + localUrl: 'http://localhost:3000', + } + }, + { + ps: { + name: undefined, + served: true, + exposed: undefined, + autoExposure: PortAutoExposure.TRYING, + }, + tunnel: { + localAddress: 'http://localhost:3001', + public: true + }, + result: { + label: '3000:3001', + tooltip: 'desc', + description: 'open on all interfaces', + iconStatus: 'Served', + contextValue: 'network-tunneled-served-port', + localUrl: 'http://localhost:3000', + } + }, + { + ps: { + name: undefined, + served: true, + exposed: undefined, + autoExposure: PortAutoExposure.TRYING, + }, + tunnel: { + localAddress: 'http://localhost:3001', + public: false + }, + result: { + label: '3000:3001', + tooltip: 'desc', + description: 'open on localhost', + iconStatus: 'Served', + contextValue: 'host-tunneled-served-port', + localUrl: 'http://localhost:3000', + } + }, + { + ps: { + name: undefined, + served: true, + exposed: undefined, + autoExposure: PortAutoExposure.TRYING, + }, + tunnel: { + localAddress: 'http://localhost:3001', + public: false + }, + result: { + label: '3000:3001', + tooltip: 'desc', + description: 'open on localhost', + iconStatus: 'Served', + contextValue: 'host-tunneled-served-port', + localUrl: 'http://localhost:3000', + } + }, + { + ps: { + name: undefined, + served: true, + exposed: Object.assign({}, base, { + visibility: PortVisibility.PUBLIC + }) as any as ExposedPortInfo.AsObject, + autoExposure: PortAutoExposure.TRYING, + }, + tunnel: { + localAddress: 'http://localhost:3001', + public: false + }, + result: { + label: '3000:3001', + tooltip: 'desc', + description: 'open on localhost (public)', + iconStatus: 'Served', + contextValue: 'host-tunneled-public-exposed-served-port', + localUrl: 'http://localhost:3000', + } + }, + { + ps: { + name: undefined, + served: true, + exposed: Object.assign({}, base, { + visibility: PortVisibility.PRIVATE + }) as any as ExposedPortInfo.AsObject, + autoExposure: PortAutoExposure.TRYING, + }, + tunnel: { + localAddress: 'http://localhost:3001', + public: false + }, + result: { + label: '3000:3001', + tooltip: 'desc', + description: 'open on localhost (private)', + iconStatus: 'Served', + contextValue: 'host-tunneled-private-exposed-served-port', + localUrl: 'http://localhost:3000', + } + }, + ]; + for (const t of cases) { + const ps = genPortsStatus(Object.assign({}, base, t.ps)); + const result = new GitpodWorkspacePort(ps.getLocalPort(), ps.toObject(), t.tunnel as TunnelDescriptionI); + assert.deepEqual(result.info, t.result); + } + }); +}); diff --git a/extensions/gitpod-shared/src/workspacePort.ts b/extensions/gitpod-shared/src/workspacePort.ts new file mode 100644 index 0000000000000..27918e45f92f4 --- /dev/null +++ b/extensions/gitpod-shared/src/workspacePort.ts @@ -0,0 +1,191 @@ +/*--------------------------------------------------------------------------------------------- + * Copyright (c) Gitpod. All rights reserved. + *--------------------------------------------------------------------------------------------*/ + +import { PortsStatus, PortAutoExposure, PortVisibility, ExposedPortInfo } from '@gitpod/supervisor-api-grpc/lib/status_pb'; +import { URL } from 'url'; + +export interface ExposedPort extends PortsStatus.AsObject { + exposed: ExposedPortInfo.AsObject; +} + +export function isExposedPort(port: PortsStatus.AsObject | undefined): port is ExposedPort { + return !!port?.exposed; +} + +export interface ExposedServedPort extends ExposedPort { + served: true; +} + +export function isExposedServedPort(port: PortsStatus.AsObject | undefined): port is ExposedServedPort { + return isExposedPort(port) && !!port.served; +} + +export interface ExposedServedGitpodWorkspacePort extends GitpodWorkspacePort { + status: ExposedServedPort; +} + +export function isExposedServedGitpodWorkspacePort(port: GitpodWorkspacePort | undefined): port is ExposedServedGitpodWorkspacePort { + return port instanceof GitpodWorkspacePort && isExposedServedPort(port.status); +} + +export interface TunnelDescriptionI { + remoteAddress: { port: number; host: string }; + //The complete local address(ex. localhost:1234) + localAddress: { port: number; host: string } | string; + /** + * @deprecated Use privacy instead + */ + public?: boolean; + privacy?: string; + // If protocol is not provided it is assumed to be http, regardless of the localAddress. + protocol?: string; +} + +export type IconStatus = 'Served' | 'NotServed' | 'Detecting' | 'ExposureFailed'; + +export const iconStatusMap: Record = { + Served: { + icon: 'circle-filled', + color: 'ports.iconRunningProcessForeground', + }, + NotServed: { + icon: 'circle-outline', + }, + Detecting: { + icon: 'circle-filled', + color: 'editorWarning.foreground', + }, + ExposureFailed: { + icon: 'warning', + color: 'editorWarning.foreground', + }, +}; + +export interface PortInfo { + label: string; + tooltip: string; + description: string; + iconStatus: IconStatus; + contextValue: string; + localUrl: string; +} + +export class GitpodWorkspacePort { + public info: PortInfo; + public status: PortsStatus.AsObject; + public localUrl: string; + constructor( + readonly portNumber: number, + portStatus: PortsStatus.AsObject, + private tunnel?: TunnelDescriptionI, + ) { + this.status = portStatus; + this.tunnel = tunnel; + this.info = this.parsePortInfo(portStatus, tunnel); + this.localUrl = 'http://localhost:' + portStatus.localPort; + } + + update(portStatus: PortsStatus.AsObject, tunnel?: TunnelDescriptionI) { + this.status = portStatus; + this.tunnel = tunnel; + this.info = this.parsePortInfo(portStatus, tunnel); + } + + private parsePortInfo(portStatus: PortsStatus.AsObject, tunnel?: TunnelDescriptionI) { + const currentStatus = portStatus; + const { name, localPort, description, exposed, served } = currentStatus; + // const prevStatus = port.status; + const port: PortInfo = { + label: '', + tooltip: '', + description: '', + contextValue: '', + iconStatus: 'NotServed', + localUrl: 'http://localhost:' + localPort, + }; + port.label = name ? `${name}: ${localPort}` : `${localPort}`; + if (description) { + port.tooltip = name ? `${name} - ${description}` : description; + } + + if (this.remotePort && this.remotePort !== localPort) { + port.label += ':' + this.remotePort; + } + + const accessible = exposed || tunnel; + + // We use .public here because https://github.com/gitpod-io/openvscode-server/pull/360#discussion_r882953586 + const isPortTunnelPublic = !!tunnel?.public; + if (!served) { + port.description = 'not served'; + port.iconStatus = 'NotServed'; + } else if (!accessible) { + if (portStatus.autoExposure === PortAutoExposure.FAILED) { + port.description = 'failed to expose'; + port.iconStatus = 'ExposureFailed'; + } else { + port.description = 'detecting...'; + port.iconStatus = 'Detecting'; + } + } else { + port.description = 'open'; + if (tunnel) { + port.description += ` on ${isPortTunnelPublic ? 'all interfaces' : 'localhost'}`; + } + if (exposed) { + port.description += ` ${exposed.visibility === PortVisibility.PUBLIC ? '(public)' : '(private)'}`; + } + port.iconStatus = 'Served'; + } + + port.contextValue = 'port'; + if (served) { + port.contextValue = 'served-' + port.contextValue; + } + if (exposed) { + port.contextValue = 'exposed-' + port.contextValue; + port.contextValue = (exposed.visibility === PortVisibility.PUBLIC ? 'public-' : 'private-') + port.contextValue; + } + if (tunnel) { + port.contextValue = 'tunneled-' + port.contextValue; + port.contextValue = (isPortTunnelPublic ? 'network-' : 'host-') + port.contextValue; + } + if (!accessible && portStatus.autoExposure === PortAutoExposure.FAILED) { + port.contextValue = 'failed-' + port.contextValue; + } + return port; + } + + toSvelteObject() { + return { + info: this.info, + status: { + ...this.status, + remotePort: this.remotePort, + }, + }; + } + + get externalUrl(): string { + if (this.tunnel) { + const localAddress = typeof this.tunnel.localAddress === 'string' ? this.tunnel.localAddress : this.tunnel.localAddress.host + ':' + this.tunnel.localAddress.port; + return localAddress.startsWith('http') ? localAddress : `http://${localAddress}`; + } + return this.status?.exposed?.url || this.localUrl; + } + + get remotePort(): number | undefined { + if (this.tunnel) { + if (typeof this.tunnel.localAddress === 'string') { + try { + return Number(new URL(this.tunnel.localAddress).port); + } catch { + return undefined; + } + } + return this.tunnel.localAddress.port; + } + return undefined; + } +} diff --git a/extensions/gitpod-shared/tsconfig.json b/extensions/gitpod-shared/tsconfig.json new file mode 100644 index 0000000000000..0330c9cefb6cb --- /dev/null +++ b/extensions/gitpod-shared/tsconfig.json @@ -0,0 +1,19 @@ +{ + "extends": "../tsconfig.base.json", + "compilerOptions": { + "outDir": "./out", + "rootDir": "./src", + "composite": true, + "esModuleInterop": true, + "sourceMap": true, + "declaration": true + }, + "include": [ + "./src/**/*" + ], + "exclude": [ + "./node_modules/**", + "./public/**", + "./portsview/**" + ] +} diff --git a/extensions/gitpod-web/.gitignore b/extensions/gitpod-web/.gitignore new file mode 100644 index 0000000000000..364fdec1aa19d --- /dev/null +++ b/extensions/gitpod-web/.gitignore @@ -0,0 +1 @@ +public/ diff --git a/extensions/gitpod-web/.vscodeignore b/extensions/gitpod-web/.vscodeignore new file mode 100644 index 0000000000000..5f3c6539aa426 --- /dev/null +++ b/extensions/gitpod-web/.vscodeignore @@ -0,0 +1,11 @@ +.gitignore +src/** +out/** +build/** +portsview/** +!public/** +extension.webpack.config.js +extension-browser.webpack.config.js +tsconfig.json +yarn.lock +README.md diff --git a/extensions/gitpod-web/README.md b/extensions/gitpod-web/README.md new file mode 100644 index 0000000000000..78e1c1dad65cd --- /dev/null +++ b/extensions/gitpod-web/README.md @@ -0,0 +1,7 @@ +# Gitpod Web integration + +**Notice:** This extension is bundled with Gitpod Code. It can be disabled but not uninstalled. + +## Features + +This extension provides support for Gitpod Web integration. diff --git a/extensions/gitpod-web/extension.webpack.config.js b/extensions/gitpod-web/extension.webpack.config.js new file mode 100644 index 0000000000000..a513ac5c3b511 --- /dev/null +++ b/extensions/gitpod-web/extension.webpack.config.js @@ -0,0 +1,20 @@ +/*--------------------------------------------------------------------------------------------- + * Copyright (c) Microsoft Corporation. All rights reserved. + * Licensed under the MIT License. See License.txt in the project root for license information. + *--------------------------------------------------------------------------------------------*/ + +//@ts-check + +'use strict'; + +const withDefaults = require('../shared.webpack.config'); + +module.exports = withDefaults({ + context: __dirname, + entry: { + extension: './src/extension.ts', + }, + externals: { + 'keytar': 'commonjs keytar' + } +}); diff --git a/extensions/gitpod-web/package.json b/extensions/gitpod-web/package.json new file mode 100644 index 0000000000000..814585af63aaf --- /dev/null +++ b/extensions/gitpod-web/package.json @@ -0,0 +1,562 @@ +{ + "name": "gitpod-web", + "displayName": "%displayName%", + "description": "%description%", + "publisher": "gitpod", + "version": "0.0.1", + "license": "MIT", + "icon": "resources/gitpod.png", + "repository": { + "type": "git", + "url": "https://github.com/gitpod-io/openvscode-server.git" + }, + "bugs": { + "url": "https://github.com/gitpod-io/gitpod/issues" + }, + "engines": { + "vscode": "^1.58.2" + }, + "enabledApiProposals": [ + "resolvers", + "tunnels", + "contribViewsRemote", + "contribRemoteHelp", + "contribMenuBarHome" + ], + "categories": [ + "Other" + ], + "extensionKind": [ + "workspace" + ], + "activationEvents": [ + "*", + "onAuthenticationRequest:gitpod", + "onAuthenticationRequest:github" + ], + "main": "./out/extension.js", + "contributes": { + "commands": [ + { + "command": "gitpod.stop.ws", + "title": "%stopWorkspace%", + "enablement": "gitpod.inWorkspace == true && gitpod.workspaceOwned == true" + }, + { + "command": "gitpod.open.settings", + "title": "%openSettings%", + "enablement": "gitpod.inWorkspace == true" + }, + { + "command": "gitpod.open.accessControl", + "title": "%openAccessControl%", + "enablement": "gitpod.inWorkspace == true" + }, + { + "command": "gitpod.open.context", + "title": "%openContext%", + "enablement": "gitpod.inWorkspace == true" + }, + { + "command": "gitpod.open.dashboard", + "title": "%openDashboard%", + "enablement": "gitpod.inWorkspace == true" + }, + { + "command": "gitpod.open.documentation", + "title": "%openDocumentation%", + "enablement": "gitpod.inWorkspace == true" + }, + { + "command": "gitpod.open.twitter", + "title": "%openTwitter%", + "enablement": "gitpod.inWorkspace == true" + }, + { + "command": "gitpod.open.discord", + "title": "%openDiscord%", + "enablement": "gitpod.inWorkspace == true" + }, + { + "command": "gitpod.reportIssue", + "title": "%reportIssue%", + "enablement": "gitpod.inWorkspace == true" + }, + { + "command": "gitpod.showReleaseNotes", + "title": "%showReleaseNotes%", + "enablement": "gitpod.inWorkspace == true" + }, + { + "command": "gitpod.upgradeSubscription", + "title": "%upgradeSubscription%", + "enablement": "gitpod.inWorkspace == true && gitpod.workspaceOwned == true" + }, + { + "command": "gitpod.ExtendTimeout", + "title": "%extendTimeout%", + "enablement": "gitpod.inWorkspace == true && gitpod.workspaceOwned == true" + }, + { + "command": "gitpod.takeSnapshot", + "title": "%takeSnapshot%", + "enablement": "gitpod.inWorkspace == true && gitpod.workspaceOwned == true" + }, + { + "command": "gitpod.shareWorkspace", + "title": "%shareWorkspace%", + "enablement": "gitpod.inWorkspace == true && gitpod.workspaceOwned == true && gitpod.workspaceShared == false" + }, + { + "command": "gitpod.stopSharingWorkspace", + "title": "%stopSharingWorkspace%", + "enablement": "gitpod.inWorkspace == true && gitpod.workspaceOwned == true && gitpod.workspaceShared == true" + }, + { + "command": "gitpod.openInStable", + "title": "%openInStable%", + "enablement": "gitpod.inWorkspace == true && gitpod.UIKind == 'web'" + }, + { + "command": "gitpod.openInInsiders", + "title": "%openInInsiders%", + "enablement": "gitpod.inWorkspace == true && gitpod.UIKind == 'web'" + }, + { + "command": "gitpod.ports.openBrowser", + "title": "%openBrowser%", + "icon": "$(globe)" + }, + { + "command": "gitpod.ports.retryAutoExpose", + "title": "%retryAutoExpose%", + "icon": "$(refresh)" + }, + { + "command": "gitpod.ports.preview", + "title": "%openPreview%", + "icon": "$(open-preview)" + }, + { + "command": "gitpod.ports.makePrivate", + "title": "%makePrivate%", + "icon": "$(unlock)" + }, + { + "command": "gitpod.ports.makePublic", + "title": "%makePublic%", + "icon": "$(lock)" + }, + { + "command": "gitpod.ports.tunnelNetwork", + "title": "%tunnelNetwork%", + "icon": "$(eye)" + }, + { + "command": "gitpod.ports.tunnelHost", + "title": "%tunnelHost%", + "icon": "$(eye-closed)" + }, + { + "command": "gitpod.extensions.addToConfig", + "title": "%addToConfig%" + }, + { + "command": "gitpod.dev.connectLocalApp", + "title": "%connectLocalApp%" + }, + { + "command": "gitpod.dev.enableForwardedPortsView", + "title": "%enableForwardedPortsView%" + } + ], + "menus": { + "menuBar/home": [ + { + "command": "gitpod.open.context", + "group": "gitpod@10" + }, + { + "command": "gitpod.open.dashboard", + "group": "gitpod@20" + }, + { + "command": "gitpod.open.documentation", + "group": "gitpod@30" + }, + { + "command": "gitpod.takeSnapshot", + "group": "gitpod@40", + "when": "gitpod.workspaceOwned == true" + }, + { + "command": "gitpod.shareWorkspace", + "group": "gitpod@50", + "when": "gitpod.workspaceOwned == true && gitpod.workspaceShared == false" + }, + { + "command": "gitpod.stopSharingWorkspace", + "group": "gitpod@50", + "when": "gitpod.workspaceOwned == true && gitpod.workspaceShared == true" + }, + { + "command": "gitpod.stop.ws", + "group": "gitpod@60", + "when": "gitpod.workspaceOwned == true" + }, + { + "command": "gitpod.openInStable", + "group": "gitpod@70" + }, + { + "command": "gitpod.openInInsiders", + "group": "gitpod@80" + } + ], + "accounts/context": [ + { + "command": "gitpod.open.settings", + "group": "navigation@10" + }, + { + "command": "gitpod.open.accessControl", + "group": "navigation@20" + }, + { + "command": "gitpod.upgradeSubscription", + "group": "navigation@30", + "when": "gitpod.workspaceOwned == true" + }, + { + "command": "gitpod.ExtendTimeout", + "group": "navigation@50", + "when": "gitpod.workspaceOwned == true" + } + ], + "menuBar/help": [ + { + "command": "gitpod.open.twitter", + "group": "z_about2@10" + }, + { + "command": "gitpod.open.discord", + "group": "z_about2@20" + }, + { + "command": "gitpod.reportIssue", + "group": "z_about2@40" + }, + { + "command": "gitpod.showReleaseNotes", + "group": "z_about2@50" + } + ], + "extension/context": [ + { + "command": "gitpod.extensions.addToConfig", + "group": "2_configure" + } + ], + "view/item/context": [ + { + "command": "gitpod.ports.tunnelNetwork", + "when": "view == gitpod.workspace && viewItem =~ /host/ && viewItem =~ /tunneled/", + "group": "inline@1" + }, + { + "command": "gitpod.ports.tunnelHost", + "when": "view == gitpod.workspace && viewItem =~ /network/ && viewItem =~ /tunneled/", + "group": "inline@1" + }, + { + "command": "gitpod.ports.makePublic", + "when": "view == gitpod.workspace && viewItem =~ /private/", + "group": "inline@2" + }, + { + "command": "gitpod.ports.makePrivate", + "when": "view == gitpod.workspace && viewItem =~ /public/", + "group": "inline@2" + }, + { + "command": "gitpod.ports.preview", + "when": "view == gitpod.workspace && viewItem =~ /exposed/ || viewItem =~ /tunneled/", + "group": "inline@3" + }, + { + "command": "gitpod.ports.openBrowser", + "when": "view == gitpod.workspace && viewItem =~ /exposed/ || viewItem =~ /tunneled/", + "group": "inline@4" + }, + { + "command": "gitpod.ports.retryAutoExpose", + "when": "view == gitpod.workspace && viewItem =~ /failed/", + "group": "inline@5" + } + ], + "commandPalette": [ + { + "command": "gitpod.ports.preview", + "when": "false" + }, + { + "command": "gitpod.ports.openBrowser", + "when": "false" + }, + { + "command": "gitpod.ports.retryAutoExpose", + "when": "false" + }, + { + "command": "gitpod.ports.makePublic", + "when": "false" + }, + { + "command": "gitpod.ports.makePrivate", + "when": "false" + }, + { + "command": "gitpod.ports.tunnelNetwork", + "when": "false" + }, + { + "command": "gitpod.ports.tunnelHost", + "when": "false" + }, + { + "command": "gitpod.dev.connectLocalApp", + "when": "gitpod.localAppConnected == false" + } + ], + "statusBar/remoteIndicator": [ + { + "command": "gitpod.stop.ws", + "group": "remote_00_gitpod_navigation@10", + "when": "gitpod.inWorkspace == true && gitpod.workspaceOwned == true" + }, + { + "command": "gitpod.open.settings", + "group": "remote_00_gitpod_navigation@20", + "when": "gitpod.inWorkspace == true" + }, + { + "command": "gitpod.open.accessControl", + "group": "remote_00_gitpod_navigation@30", + "when": "gitpod.inWorkspace == true" + }, + { + "command": "gitpod.open.context", + "group": "remote_00_gitpod_navigation@40", + "when": "gitpod.inWorkspace == true" + }, + { + "command": "gitpod.open.dashboard", + "group": "remote_00_gitpod_navigation@50", + "when": "gitpod.inWorkspace == true" + }, + { + "command": "gitpod.open.documentation", + "group": "remote_00_gitpod_navigation@60", + "when": "gitpod.inWorkspace == true" + }, + { + "command": "gitpod.open.twitter", + "group": "remote_00_gitpod_navigation@70", + "when": "gitpod.inWorkspace == true" + }, + { + "command": "gitpod.open.discord", + "group": "remote_00_gitpod_navigation@80", + "when": "gitpod.inWorkspace == true" + }, + { + "command": "gitpod.reportIssue", + "group": "remote_00_gitpod_navigation@90", + "when": "gitpod.inWorkspace == true" + }, + { + "command": "gitpod.upgradeSubscription", + "group": "remote_00_gitpod_navigation@100", + "when": "gitpod.inWorkspace == true && gitpod.workspaceOwned == true" + }, + { + "command": "gitpod.ExtendTimeout", + "group": "remote_00_gitpod_navigation@110", + "when": "gitpod.inWorkspace == true && gitpod.workspaceOwned == true" + }, + { + "command": "gitpod.takeSnapshot", + "group": "remote_00_gitpod_navigation@120", + "when": "gitpod.inWorkspace == true && gitpod.workspaceOwned == true" + }, + { + "command": "gitpod.shareWorkspace", + "group": "remote_00_gitpod_navigation@130", + "when": "gitpod.inWorkspace == true && gitpod.workspaceOwned == true && gitpod.workspaceShared == false" + }, + { + "command": "gitpod.stopSharingWorkspace", + "group": "remote_00_gitpod_navigation@130", + "when": "gitpod.inWorkspace == true && gitpod.workspaceOwned == true && gitpod.workspaceShared == true" + }, + { + "command": "gitpod.showReleaseNotes", + "group": "remote_00_gitpod_navigation@140", + "when": "gitpod.inWorkspace == true" + }, + { + "command": "gitpod.openInStable", + "group": "remote_00_gitpod_navigation@900", + "when": "gitpod.inWorkspace == true && gitpod.UIKind == 'web'" + }, + { + "command": "gitpod.openInInsiders", + "group": "remote_00_gitpod_navigation@1000", + "when": "gitpod.inWorkspace == true && gitpod.UIKind == 'web'" + } + ] + }, + "views": { + "remote": [ + { + "id": "gitpod.workspace", + "name": "Gitpod Workspace", + "when": "false" + } + ], + "portsView": [ + { + "id": "gitpod.portsView", + "name": "Ports", + "type": "webview", + "icon": "$(plug)" + } + ] + }, + "authentication": [ + { + "id": "gitpod", + "label": "Gitpod" + }, + { + "id": "github", + "label": "GitHub" + } + ], + "walkthroughs": [ + { + "id": "gitpod-getstarted", + "title": "Get Started with Gitpod", + "description": "Explore what Gitpod is and how to get the most out of it.", + "steps": [ + { + "id": "whatisgitpod", + "title": "What is Gitpod?", + "description": "", + "media": { + "markdown": "resources/walkthroughs/getstarted/what-is-gitpod.md" + } + }, + { + "id": "prebuilds", + "title": "Prebuilds do the heavy lifting", + "description": "", + "media": { + "markdown": "resources/walkthroughs/getstarted/prebuilds.md" + } + }, + { + "id": "workspaces", + "title": "A new workspace per task", + "description": "", + "media": { + "markdown": "resources/walkthroughs/getstarted/workspaces.md" + } + }, + { + "id": "terminal", + "title": "Full access to a terminal", + "description": "", + "media": { + "markdown": "resources/walkthroughs/getstarted/terminal.md" + } + }, + { + "id": "ports", + "title": "Open ports", + "description": "", + "media": { + "markdown": "resources/walkthroughs/getstarted/ports.md" + } + }, + { + "id": "vscodedesktopsupport", + "title": "VS Code desktop support", + "description": "", + "media": { + "markdown": "resources/walkthroughs/getstarted/vscodedesktopsupport.md" + } + } + ] + } + ], + "configuration": { + "title": "Gitpod", + "properties": { + "gitpod.openInStable.neverPrompt": { + "type": "boolean", + "description": "Control whether to prompt to open in VS Code Desktop on page load.", + "default": false + } + } + }, + "viewsContainers": { + "panel": [ + { + "id": "portsView", + "title": "Ports", + "icon": "$(plug)" + } + ] + } + }, + "scripts": { + "vscode:prepublish": "yarn build:webview; yarn run compile --cwd ..", + "build:webview": "rollup -c", + "watch:webview": "rollup -c -w", + "start:webview": "sirv public --no-clear" + }, + "devDependencies": { + "@types/node": "16.x", + "@types/node-fetch": "^2.5.12", + "@types/uuid": "8.0.0", + "@types/yauzl": "^2.9.1", + "@types/yazl": "^2.4.2", + "@tsconfig/svelte": "^2.0.0", + "@types/vscode-webview": "^1.57.0", + "svelte": "^3.0.0", + "svelte-check": "^2.0.0", + "rollup": "^2.3.4", + "rollup-plugin-copy": "^3.4.0", + "rollup-plugin-css-only": "^3.1.0", + "rollup-plugin-livereload": "^2.0.0", + "rollup-plugin-svelte": "^7.0.0", + "rollup-plugin-terser": "^7.0.0", + "@rollup/plugin-commonjs": "^17.0.0", + "@rollup/plugin-json": "^4.1.0", + "@rollup/plugin-node-resolve": "^11.0.0", + "@rollup/plugin-typescript": "^8.0.0", + "@rollup/plugin-alias": "^3.1.9", + "svelte-preprocess": "^4.0.0", + "sirv-cli": "^2.0.0" + }, + "dependencies": { + "configcat-node": "^8.0.0", + "gitpod-shared": "0.0.1", + "node-fetch": "2.6.7", + "uuid": "8.1.0", + "vscode-nls": "^5.0.0", + "yauzl": "^2.9.2", + "yazl": "^2.5.1" + } +} diff --git a/extensions/gitpod-web/package.nls.json b/extensions/gitpod-web/package.nls.json new file mode 100644 index 0000000000000..412a9fe82625f --- /dev/null +++ b/extensions/gitpod-web/package.nls.json @@ -0,0 +1,32 @@ +{ + "displayName": "Gitpod Web", + "description": "Gitpod Web Support", + "openBrowser": "Open Browser", + "openPreview": "Open Preview", + "makePrivate": "Make Private", + "makePublic": "Make Public", + "tunnelNetwork": "Tunnel on all interfaces", + "tunnelHost": "Tunnel on localhost", + "addToConfig": "Add to .gitpod.yml", + "connectLocalApp": "Developer: Connect to Local Gitpod App", + "enableForwardedPortsView": "Developer: Enable Tunneled Ports View", + "retryAutoExpose": "Retry to expose", + "openDashboard": "Gitpod: Open Dashboard", + "openAccessControl": "Gitpod: Open Access Control", + "openSettings": "Gitpod: Open Settings", + "openContext": "Gitpod: Open Context", + "openDocumentation": "Gitpod: Documentation", + "openDiscord": "Gitpod: Open Community Chat", + "openTwitter": "Gitpod: Follow us on Twitter", + "reportIssue": "Gitpod: Report Issue", + "stopWorkspace": "Gitpod: Stop Workspace", + "showReleaseNotes": "Gitpod: Show Release Notes", + "upgradeSubscription": "Gitpod: Upgrade Subscription", + "extendTimeout": "Gitpod: Extend Workspace Timeout", + "takeSnapshot": "Gitpod: Share Workspace Snapshot", + "shareWorkspace": "Gitpod: Share Running Workspace", + "stopSharingWorkspace": "Gitpod: Stop Sharing Running Workspace", + "openInStable": "Gitpod: Open in VS Code", + "openInInsiders": "Gitpod: Open in VS Code Insiders", + "openInBrowser": "Gitpod: Open in Browser" +} diff --git a/extensions/gitpod-web/resources/gitpod.png b/extensions/gitpod-web/resources/gitpod.png new file mode 100644 index 0000000000000..d8bdece34e139 Binary files /dev/null and b/extensions/gitpod-web/resources/gitpod.png differ diff --git a/extensions/gitpod-web/resources/walkthroughs/getstarted/ports.md b/extensions/gitpod-web/resources/walkthroughs/getstarted/ports.md new file mode 100644 index 0000000000000..baa315706b84e --- /dev/null +++ b/extensions/gitpod-web/resources/walkthroughs/getstarted/ports.md @@ -0,0 +1,11 @@ +# Ports + +If your application listens on any port, e.g. a web application, Gitpod provides a URL for you to access your application. + +A list of ports is available in the "PortsView" tab near by Terminal tab. [Click here to open PortsView.](command:gitpod.portsView.focus) + +_Note_: To get back to the list of files in your project, click the "Explorer" icon on the left-hand side. + +## Documentation + +To learn more about ports and how to configure them, please [refer to the documentation](https://www.gitpod.io/docs/config-ports). diff --git a/extensions/gitpod-web/resources/walkthroughs/getstarted/prebuilds.md b/extensions/gitpod-web/resources/walkthroughs/getstarted/prebuilds.md new file mode 100644 index 0000000000000..f86ceeff6094b --- /dev/null +++ b/extensions/gitpod-web/resources/walkthroughs/getstarted/prebuilds.md @@ -0,0 +1,12 @@ +# Prebuilds + +Download & install dependencies, run build scripts, you name it - tell Gitpod which tasks to run and it takes care of them **before** you start a new developer environment so you don’t have to wait for any of that. + +When new code is pushed to the repository, Gitpod automatically prepares a new workspace based on the latest code. + +By the time you start your next workspace, everything is installed, compiled and ready for you to write code. + +## Documentation + +To learn more and how to enable prebuilds for your project, please [refer to the documentation](https://www.gitpod.io/docs/prebuilds). + diff --git a/extensions/gitpod-web/resources/walkthroughs/getstarted/terminal.md b/extensions/gitpod-web/resources/walkthroughs/getstarted/terminal.md new file mode 100644 index 0000000000000..7b4e37eb4b39e --- /dev/null +++ b/extensions/gitpod-web/resources/walkthroughs/getstarted/terminal.md @@ -0,0 +1,17 @@ +# The terminal + +Gitpod is more than just an editor - it's everything you need to develop, test & debug your application. + +With that, you also have access to a terminal, including Docker support. + +Let's open a new terminal and run the following command in the terminal: + +```bash +docker run hello-world +``` + +Alternatively, [click here](command:gitpod.welcome.createTerminalAndRunDockerCommand) to perform the above automatically. + +Just like you used the `hello-world` Docker image, you can containerize your own application right on Gitpod. + +To exit a terminal, type `exit` and hit Enter. diff --git a/extensions/gitpod-web/resources/walkthroughs/getstarted/vscodedesktopsupport.md b/extensions/gitpod-web/resources/walkthroughs/getstarted/vscodedesktopsupport.md new file mode 100644 index 0000000000000..4a3e543e2ab29 --- /dev/null +++ b/extensions/gitpod-web/resources/walkthroughs/getstarted/vscodedesktopsupport.md @@ -0,0 +1,11 @@ +# VS Code desktop support + +If you like the idea of ephemeral workspaces, but prefer to develop code in your local VS Code, we've got you covered. + +Depending on the version of VS Code you have installed locally, use one of the following links to start your VS Code and connect to the currently running workspace: +* [Open in VS Code](command:gitpod.openInStable) +* [Open in VS Code Insiders](command:gitpod.openInInsiders) + +## Documentation + +To learn more about VS Code desktop support, please [refer to the documentation](https://www.gitpod.io/docs/develop/vscode-desktop-support). diff --git a/extensions/gitpod-web/resources/walkthroughs/getstarted/what-is-gitpod.md b/extensions/gitpod-web/resources/walkthroughs/getstarted/what-is-gitpod.md new file mode 100644 index 0000000000000..970afcc966fb1 --- /dev/null +++ b/extensions/gitpod-web/resources/walkthroughs/getstarted/what-is-gitpod.md @@ -0,0 +1,35 @@ +# What is Gitpod? + +With Gitpod, you spin up fresh, automated dev environments for each task, in the cloud, in seconds. + +- Develop a new feature? Start a new workspace. +- Review a pull request? Start a new workspace. +- Fix a bug in production? Start a new workspace. + +You can even run multiple workspaces in parallel and they are 100% isolated from each other. + +## User interface + +As you notice, Gitpod looks & feels like VS Code - except it runs in a browser. On Gitpod, you can install extensions, apply themes and use a terminal just as you're used to from your local computer. + +## Features + +Use the steps on the left side to learn more about the following features: + +* Prebuilds +* Workspaces +* The terminal +* Ports +* VS Code desktop support + +## Browser extension + +To be most productive with Gitpod, it is best if you [install the browser extension](https://www.gitpod.io/docs/browser-extension). It will add a convenient "Gitpod" button when you look at your repo, an issue or a pull request. + +## Documentation + +To learn more about Gitpod, how to customize it and about best practices, please [check out the documentation](https://www.gitpod.io/docs). + +## Community + +Come [join us in our chat](https://www.gitpod.io/chat) to learn from others and share what you're up to. diff --git a/extensions/gitpod-web/resources/walkthroughs/getstarted/workspaces.md b/extensions/gitpod-web/resources/walkthroughs/getstarted/workspaces.md new file mode 100644 index 0000000000000..3653899cca825 --- /dev/null +++ b/extensions/gitpod-web/resources/walkthroughs/getstarted/workspaces.md @@ -0,0 +1,25 @@ +# Workspaces + +When you work with Gitpod, you use a new workspace for each task you work on. This is different from your local environment where you have only one and maintain it over time. + +Gitpod workspaces are automated and disposable, they are ephemeral workspaces. + +## Benefits of ephemeral workspaces + +### Latest code + +Each new workspace contains the latest code. There's no need for you to take care of that. + +As an added bonus, if you open a Gitpod workspace from an issue URL, Gitpod automatically creates a branch for you! All you need to do is write the code, commit & push. + +### Latest dependencies + +Each new workspace also has the most up-to-date versions of app & operating system dependencies. Another thing you don't have to worry about. + +### Work in parallel + +Say you're working on a feature and a team member asks you to review their pull request. Start a new workspace, review the code, close the workspace - and you're back in your feature environment where you continue to work. + +## Documentation + +To learn more about workspaces, please [refer to the documentation](https://www.gitpod.io/docs/workspaces). diff --git a/extensions/gitpod-web/rollup.config.js b/extensions/gitpod-web/rollup.config.js new file mode 100644 index 0000000000000..6747775cb43bd --- /dev/null +++ b/extensions/gitpod-web/rollup.config.js @@ -0,0 +1,106 @@ +import svelte from 'rollup-plugin-svelte'; +import commonjs from '@rollup/plugin-commonjs'; +import resolve from '@rollup/plugin-node-resolve'; +import livereload from 'rollup-plugin-livereload'; +import { terser } from 'rollup-plugin-terser'; +import sveltePreprocess from 'svelte-preprocess'; +import typescript from '@rollup/plugin-typescript'; +import alias from '@rollup/plugin-alias'; +import css from 'rollup-plugin-css-only'; +import path from 'path'; +import copy from 'rollup-plugin-copy'; +import json from '@rollup/plugin-json'; + +const production = !process.env.ROLLUP_WATCH; + +function serve() { + let server; + + function toExit() { + if (server) { server.kill(0); } + } + + return { + writeBundle() { + if (server) { return; } + server = require('child_process').spawn('npm', ['run', 'start', '--', '--dev'], { + stdio: ['ignore', 'inherit', 'inherit'], + shell: true + }); + + process.on('SIGTERM', toExit); + process.on('exit', toExit); + } + }; +} + +export default { + input: path.join(__dirname, '../gitpod-shared/portsview/src/main.ts'), + output: [ + { + sourcemap: !production, + format: 'es', + file: path.join(__dirname, './public/portsview.js'), + }, + ], + plugins: [ + alias({ + entries: [ + { find: 'package.nls.json', replacement: path.join(__dirname, 'package.nls.json') }, + ] + }), + svelte({ + preprocess: sveltePreprocess({ + typescript: { + tsconfigFile: '../gitpod-shared/portsview/tsconfig.json' + }, + sourceMap: !production + }), + compilerOptions: { + // enable run-time checks when not in production + dev: !production + } + }), + copy({ + targets: [ + { src: 'node_modules/@vscode/codicons/dist/codicon.css', dest: 'public' }, + { src: 'node_modules/@vscode/codicons/dist/codicon.ttf', dest: 'public' } + ], + }), + json({ compact: true }), + // we'll extract any component CSS out into + // a separate file - better for performance + css({ output: 'portsview.css' }), + + // If you have external dependencies installed from + // npm, you'll most likely need these plugins. In + // some cases you'll need additional configuration - + // consult the documentation for details: + // https://github.com/rollup/plugins/tree/master/packages/commonjs + resolve({ + browser: true, + dedupe: ['svelte'] + }), + commonjs(), + typescript({ + sourceMap: !production, + inlineSources: !production, + tsconfig: '../gitpod-shared/portsview/tsconfig.json' + }), + + // In dev mode, call `npm run start` once + // the bundle has been generated + // !production && serve(), + + // Watch the `public` directory and refresh the + // browser on changes when not in production + !production && livereload('public'), + + // If we're building for production (npm run build + // instead of npm run dev), minify + production && terser() + ], + watch: { + clearScreen: false + } +}; diff --git a/extensions/gitpod-web/src/experiments.ts b/extensions/gitpod-web/src/experiments.ts new file mode 100644 index 0000000000000..0b819a95b2eb6 --- /dev/null +++ b/extensions/gitpod-web/src/experiments.ts @@ -0,0 +1,89 @@ +/*--------------------------------------------------------------------------------------------- + * Copyright (c) Gitpod. All rights reserved. + *--------------------------------------------------------------------------------------------*/ + +import * as vscode from 'vscode'; +import * as configcat from 'configcat-node'; +import * as configcatcommon from 'configcat-common'; +import * as semver from 'semver'; +import Log from 'gitpod-shared/out/common/logger'; +import { URL } from 'url'; + +const EXPERTIMENTAL_SETTINGS: string[] = []; + +export class ExperimentalSettings { + private configcatClient: configcatcommon.IConfigCatClient; + private extensionVersion: semver.SemVer; + + constructor(key: string, extensionVersion: string, private logger: Log, gitpodHost: string) { + this.configcatClient = configcat.createClientWithLazyLoad(key, { + baseUrl: new URL('/configcat', process.env['VSCODE_DEV'] ? 'https://gitpod-staging.com' : gitpodHost).href, + logger: { + debug(): void { }, + log(): void { }, + info(): void { }, + warn(message: string): void { logger.warn(`ConfigCat: ${message}`); }, + error(message: string): void { logger.error(`ConfigCat: ${message}`); } + }, + requestTimeoutMs: 1500, + cacheTimeToLiveSeconds: 60 + }); + this.extensionVersion = new semver.SemVer(extensionVersion); + } + + async get(key: string, userId?: string, custom?: { [key: string]: string }): Promise { + const config = vscode.workspace.getConfiguration('gitpod'); + const values = config.inspect(key.substring('gitpod.'.length)); + if (!values || !EXPERTIMENTAL_SETTINGS.includes(key)) { + this.logger.error(`Cannot get invalid experimental setting '${key}'`); + return values?.globalValue ?? values?.defaultValue; + } + if (this.isPreRelease()) { + // PreRelease versions always have experiments enabled by default + return values.globalValue ?? values.defaultValue; + } + if (values.globalValue !== undefined) { + // User setting have priority over configcat so return early + return values.globalValue; + } + + const user = userId ? new configcatcommon.User(userId, undefined, undefined, custom) : undefined; + const configcatKey = key.replace(/\./g, '_'); // '.' are not allowed in configcat + const experimentValue = (await this.configcatClient.getValueAsync(configcatKey, undefined, user)) as T | undefined; + + return experimentValue ?? values.defaultValue; + } + + async inspect(key: string, userId?: string, custom?: { [key: string]: string }): Promise<{ key: string; defaultValue?: T; globalValue?: T; experimentValue?: T } | undefined> { + const config = vscode.workspace.getConfiguration('gitpod'); + const values = config.inspect(key.substring('gitpod.'.length)); + if (!values || !EXPERTIMENTAL_SETTINGS.includes(key)) { + this.logger.error(`Cannot inspect invalid experimental setting '${key}'`); + return values; + } + + const user = userId ? new configcatcommon.User(userId, undefined, undefined, custom) : undefined; + const configcatKey = key.replace(/\./g, '_'); // '.' are not allowed in configcat + const experimentValue = (await this.configcatClient.getValueAsync(configcatKey, undefined, user)) as T | undefined; + + return { key, defaultValue: values.defaultValue, globalValue: values.globalValue, experimentValue }; + } + + forceRefreshAsync(): Promise { + return this.configcatClient.forceRefreshAsync(); + } + + private isPreRelease() { + return this.extensionVersion.minor % 2 === 1; + } + + dispose(): void { + this.configcatClient.dispose(); + } +} + +export function isUserOverrideSetting(key: string): boolean { + const config = vscode.workspace.getConfiguration('gitpod'); + const values = config.inspect(key.substring('gitpod.'.length)); + return values?.globalValue !== undefined; +} diff --git a/extensions/gitpod-web/src/extension.ts b/extensions/gitpod-web/src/extension.ts new file mode 100644 index 0000000000000..2b9b74358c61e --- /dev/null +++ b/extensions/gitpod-web/src/extension.ts @@ -0,0 +1,973 @@ +/*--------------------------------------------------------------------------------------------- + * Copyright (c) Gitpod. All rights reserved. + *--------------------------------------------------------------------------------------------*/ + +/// +/// +/// + +import * as grpc from '@grpc/grpc-js'; +import * as fs from 'fs'; +import * as os from 'os'; +import * as uuid from 'uuid'; +import { GitpodPluginModel, GitpodExtensionContext, setupGitpodContext, registerTasks, registerIpcHookCli, ExposedServedGitpodWorkspacePort, GitpodWorkspacePort, isExposedServedGitpodWorkspacePort, isGRPCErrorStatus } from 'gitpod-shared'; +import { GetTokenRequest } from '@gitpod/supervisor-api-grpc/lib/token_pb'; +import { PortsStatus, PortsStatusRequest, PortsStatusResponse, PortVisibility } from '@gitpod/supervisor-api-grpc/lib/status_pb'; +import { TunnelVisiblity, TunnelPortRequest, RetryAutoExposeRequest, CloseTunnelRequest } from '@gitpod/supervisor-api-grpc/lib/port_pb'; +import { ExposePortRequest } from '@gitpod/supervisor-api-grpc/lib/control_pb'; +import type * as keytarType from 'keytar'; +import fetch from 'node-fetch'; +import * as path from 'path'; +import * as util from 'util'; +import * as vscode from 'vscode'; +import { ThrottledDelayer } from './util/async'; +import { download } from './util/download'; +import { getManifest } from './util/extensionManagmentUtill'; +import { ReleaseNotes } from './releaseNotes'; +import { registerWelcomeWalkthroughContribution, WELCOME_WALKTROUGH_KEY } from './welcomeWalktrough'; +import { ExperimentalSettings } from './experiments'; +import { GitpodPortViewProvider } from './portViewProvider'; + +let gitpodContext: GitpodExtensionContext | undefined; +export async function activate(context: vscode.ExtensionContext) { + gitpodContext = await setupGitpodContext(context); + if (!gitpodContext) { + return; + } + + context.globalState.setKeysForSync([WELCOME_WALKTROUGH_KEY, ReleaseNotes.RELEASE_NOTES_LAST_READ_KEY]); + + registerDesktop(); + registerAuth(gitpodContext); + registerPorts(gitpodContext); + registerTasks(gitpodContext).then(() => { + if (vscode.window.terminals.length === 0) { + // Always show a terminal if no task terminals are created + vscode.window.createTerminal(); + } + }); + + registerIpcHookCli(gitpodContext); + registerExtensionManagement(gitpodContext); + registerWelcomeWalkthroughContribution(gitpodContext); + context.subscriptions.push(new ReleaseNotes(context)); + + await gitpodContext.active; +} + +export function deactivate() { + if (!gitpodContext) { + return; + } + return gitpodContext.dispose(); +} + +function registerAuth(context: GitpodExtensionContext): void { + type Keytar = { + getPassword: typeof keytarType['getPassword']; + setPassword: typeof keytarType['setPassword']; + deletePassword: typeof keytarType['deletePassword']; + }; + interface SessionData { + id: string; + account?: { + label?: string; + displayName?: string; + id: string; + }; + scopes: string[]; + accessToken: string; + } + interface UserInfo { + id: string; + accountName: string; + } + async function resolveAuthenticationSession(data: SessionData, resolveUser: (data: SessionData) => Promise): Promise { + const needsUserInfo = !data.account; + const userInfo = needsUserInfo ? await resolveUser(data) : undefined; + return { + id: data.id, + account: { + label: data.account + ? data.account.label || data.account.displayName! + : userInfo!.accountName, + id: data.account?.id ?? userInfo!.id + }, + scopes: data.scopes, + accessToken: data.accessToken + }; + } + function hasScopes(session: vscode.AuthenticationSession, scopes?: readonly string[]): boolean { + return !scopes || scopes.every(scope => session.scopes.indexOf(scope) !== -1); + } + //#endregion + + //#region gitpod auth + context.pendingActivate.push((async () => { + const sessions: vscode.AuthenticationSession[] = []; + const onDidChangeSessionsEmitter = new vscode.EventEmitter(); + try { + const resolveGitpodUser = async () => { + const owser = await context.owner; + return { + id: owser.id, + accountName: owser.name! + }; + }; + if (vscode.env.uiKind === vscode.UIKind.Web) { + const keytar: Keytar = require('keytar'); + const value = await keytar.getPassword(`${vscode.env.uriScheme}-gitpod.login`, 'account'); + if (value) { + await keytar.deletePassword(`${vscode.env.uriScheme}-gitpod.login`, 'account'); + const sessionData: SessionData[] = JSON.parse(value); + if (sessionData.length) { + const session = await resolveAuthenticationSession(sessionData[0], resolveGitpodUser); + sessions.push(session); + } + } + } else { + const getTokenRequest = new GetTokenRequest(); + getTokenRequest.setKind('gitpod'); + getTokenRequest.setHost(context.info.getGitpodApi()!.getHost()); + const scopes = [ + 'function:accessCodeSyncStorage' + ]; + for (const scope of scopes) { + getTokenRequest.addScope(scope); + } + const getTokenResponse = await util.promisify(context.supervisor.token.getToken.bind(context.supervisor.token, getTokenRequest, context.supervisor.metadata, { + deadline: Date.now() + context.supervisor.deadlines.long + }))(); + const accessToken = getTokenResponse.getToken(); + const session = await resolveAuthenticationSession({ + // current session ID should remain stable between window reloads + // otherwise setting sync will log out + id: 'gitpod-current-session', + accessToken, + scopes + }, resolveGitpodUser); + sessions.push(session); + onDidChangeSessionsEmitter.fire({ added: [session], changed: [], removed: [] }); + } + } catch (e) { + console.error('Failed to restore Gitpod session:', e); + } + context.subscriptions.push(onDidChangeSessionsEmitter); + context.subscriptions.push(vscode.authentication.registerAuthenticationProvider('gitpod', 'Gitpod', { + onDidChangeSessions: onDidChangeSessionsEmitter.event, + getSessions: scopes => { + if (!scopes) { + return Promise.resolve(sessions); + } + return Promise.resolve(sessions.filter(session => hasScopes(session, scopes))); + }, + createSession: async () => { + throw new Error('not supported'); + }, + removeSession: async () => { + throw new Error('not supported'); + }, + }, { supportsMultipleAccounts: false })); + })()); + //#endregion gitpod auth + + //#region github auth + context.pendingActivate.push((async () => { + const onDidChangeGitHubSessionsEmitter = new vscode.EventEmitter(); + const gitHubSessionID = 'github-session'; + let gitHubSession: vscode.AuthenticationSession | undefined; + + async function resolveGitHubUser(data: SessionData): Promise { + const userResponse = await fetch('https://api.github.com/user', { + headers: { + Authorization: `token ${data.accessToken}`, + 'User-Agent': 'Gitpod-Code' + } + }); + if (!userResponse.ok) { + throw new Error(`Getting GitHub account info failed: ${userResponse.statusText}`); + } + const user = await (userResponse.json() as Promise<{ id: string; login: string }>); + return { + id: user.id, + accountName: user.login + }; + } + + async function loginGitHub(scopes?: readonly string[]): Promise { + const getTokenRequest = new GetTokenRequest(); + getTokenRequest.setKind('git'); + getTokenRequest.setHost('github.com'); + if (scopes) { + for (const scope of scopes) { + getTokenRequest.addScope(scope); + } + } + const getTokenResponse = await util.promisify(context.supervisor.token.getToken.bind(context.supervisor.token, getTokenRequest, context.supervisor.metadata, { + deadline: Date.now() + context.supervisor.deadlines.long + }))(); + const accessToken = getTokenResponse.getToken(); + gitHubSession = await resolveAuthenticationSession({ + id: gitHubSessionID, + accessToken, + scopes: getTokenResponse.getScopeList() + }, resolveGitHubUser); + onDidChangeGitHubSessionsEmitter.fire({ added: [gitHubSession], changed: [], removed: [] }); + return gitHubSession; + } + + try { + await loginGitHub(); + } catch (e) { + console.error('Failed an initial GitHub login:', e); + } + + context.subscriptions.push(vscode.authentication.registerAuthenticationProvider('github', 'GitHub', { + onDidChangeSessions: onDidChangeGitHubSessionsEmitter.event, + getSessions: scopes => { + const sessions = []; + if (gitHubSession && hasScopes(gitHubSession, scopes)) { + sessions.push(gitHubSession); + } + return Promise.resolve(sessions); + }, + createSession: async scopes => { + try { + const session = await loginGitHub(scopes); + return session; + } catch (e) { + console.error('GitHub sign in failed: ', e); + throw e; + } + }, + removeSession: async id => { + if (id === gitHubSession?.id) { + const session = gitHubSession; + gitHubSession = undefined; + onDidChangeGitHubSessionsEmitter.fire({ removed: [session], added: [], changed: [] }); + } + }, + }, { supportsMultipleAccounts: false })); + })()); +} + +interface PortItem { port: GitpodWorkspacePort } + +async function registerPorts(context: GitpodExtensionContext): Promise { + + const packageJSON = context.extension.packageJSON; + const experiments = new ExperimentalSettings('gitpod', packageJSON.version, context.logger, context.info.getGitpodHost()); + context.subscriptions.push(experiments); + + const portMap = new Map(); + const tunnelMap = new Map(); + + // register webview + const portViewProvider = new GitpodPortViewProvider(context); + context.subscriptions.push(vscode.window.registerWebviewViewProvider(GitpodPortViewProvider.viewType, portViewProvider, { webviewOptions: { retainContextWhenHidden: true } })); + + function openExternal(port: GitpodWorkspacePort) { + return vscode.env.openExternal(vscode.Uri.parse(port.localUrl)); + } + + function observePortsStatus(): vscode.Disposable { + let run = true; + let stopUpdates: Function | undefined; + (async () => { + while (run) { + try { + const req = new PortsStatusRequest(); + req.setObserve(true); + const evts = context.supervisor.status.portsStatus(req, context.supervisor.metadata); + stopUpdates = evts.cancel.bind(evts); + + await new Promise((resolve, reject) => { + evts.on('end', resolve); + evts.on('error', reject); + evts.on('data', (update: PortsStatusResponse) => { + portMap.clear(); + const portList = update.getPortsList().map(p => p.toObject()); + for (const portStatus of portList) { + portMap.set(portStatus.localPort, new GitpodWorkspacePort(portStatus.localPort, portStatus, tunnelMap.get(portStatus.localPort))); + } + portViewProvider.updatePortsStatus(portList); + }); + }); + } catch (err) { + if (!isGRPCErrorStatus(err, grpc.status.CANCELLED)) { + context.logger.error('cannot maintain connection to supervisor', err); + console.error('cannot maintain connection to supervisor', err); + } + } finally { + stopUpdates = undefined; + } + await new Promise(resolve => setTimeout(resolve, 1000)); + } + })(); + return new vscode.Disposable(() => { + run = false; + if (stopUpdates) { + stopUpdates(); + } + }); + } + + context.subscriptions.push(observePortsStatus()); + context.subscriptions.push(vscode.commands.registerCommand('gitpod.resolveExternalPort', async (portNumber: number) => { + const port = portMap.get(portNumber); + const exposed = port?.status?.exposed; + if (exposed) { + return exposed.url; + } + + const request = new ExposePortRequest(); + request.setPort(portNumber); + await util.promisify(context.supervisor.control.exposePort.bind(context.supervisor.control, request, context.supervisor.metadata, { + deadline: Date.now() + context.supervisor.deadlines.normal + }))(); + + // Just construct the port url, maybe we can add an api as to get the port url template + const externalUrl = new URL(context.info.getWorkspaceUrl()); + externalUrl.hostname = `${portNumber}-${externalUrl.hostname}`; + + return externalUrl.toString(); + })); + context.subscriptions.push(vscode.commands.registerCommand('gitpod.ports.makePrivate', ({ port }: PortItem) => { + context.fireAnalyticsEvent({ + eventName: 'vscode_execute_command_gitpod_ports', + properties: { action: 'private' } + }); + gitpodContext?.setPortVisibility(port.status.localPort, 'private'); + })); + context.subscriptions.push(vscode.commands.registerCommand('gitpod.ports.makePublic', ({ port }: PortItem) => { + context.fireAnalyticsEvent({ + eventName: 'vscode_execute_command_gitpod_ports', + properties: { action: 'public' } + }); + gitpodContext?.setPortVisibility(port.status.localPort, 'public'); + })); + context.subscriptions.push(vscode.commands.registerCommand('gitpod.ports.tunnelNetwork', ({ port }: PortItem) => { + gitpodContext?.setTunnelVisibility(port.portNumber, port.portNumber, TunnelVisiblity.NETWORK); + })); + context.subscriptions.push(vscode.commands.registerCommand('gitpod.ports.tunnelHost', async ({ port }: PortItem) => + gitpodContext?.setTunnelVisibility(port.portNumber, port.portNumber, TunnelVisiblity.HOST) + )); + context.subscriptions.push(vscode.commands.registerCommand('gitpod.ports.preview', ({ port }: PortItem) => { + context.fireAnalyticsEvent({ + eventName: 'vscode_execute_command_gitpod_ports', + properties: { action: 'preview' } + }); + return openPreview(port); + })); + context.subscriptions.push(vscode.commands.registerCommand('gitpod.ports.openBrowser', ({ port }: PortItem) => { + context.fireAnalyticsEvent({ + eventName: 'vscode_execute_command_gitpod_ports', + properties: { action: 'openBrowser' } + }); + return openExternal(port); + })); + context.subscriptions.push(vscode.commands.registerCommand('gitpod.ports.retryAutoExpose', async ({ port }: PortItem) => { + const request = new RetryAutoExposeRequest(); + request.setPort(port.portNumber); + await util.promisify(context.supervisor.port.retryAutoExpose.bind(context.supervisor.port, request, context.supervisor.metadata, { + deadline: Date.now() + context.supervisor.deadlines.normal + }))(); + })); + + const portsStatusBarItem = vscode.window.createStatusBarItem(vscode.StatusBarAlignment.Right); + context.subscriptions.push(portsStatusBarItem); + async function updateStatusBar(): Promise { + const exposedPorts: number[] = []; + + for (const port of portMap.values()) { + if (isExposedServedGitpodWorkspacePort(port)) { + exposedPorts.push(port.status.localPort); + } + } + + let text: string; + let tooltip = 'Click to open "Ports View"'; + if (exposedPorts.length) { + text = 'Ports:'; + tooltip += '\n\nPorts'; + text += ` ${exposedPorts.join(', ')}`; + tooltip += `\nPublic: ${exposedPorts.join(', ')}`; + } else { + text = '$(circle-slash) No open ports'; + } + + portsStatusBarItem.text = text; + portsStatusBarItem.tooltip = tooltip; + + portsStatusBarItem.command = 'gitpod.portsView.focus'; + portsStatusBarItem.show(); + } + updateStatusBar(); + + context.subscriptions.push(portViewProvider.onDidChangePorts(() => updateStatusBar())); + + const currentNotifications = new Set(); + async function showOpenServiceNotification(port: GitpodWorkspacePort, offerMakePublic = false): Promise { + const localPort = port.portNumber; + if (currentNotifications.has(localPort)) { + return; + } + + const makePublic = 'Make Public'; + const openAction = 'Open Preview'; + const openExternalAction = 'Open Browser'; + const actions = offerMakePublic ? [makePublic, openAction, openExternalAction] : [openAction, openExternalAction]; + + currentNotifications.add(localPort); + const result = await vscode.window.showInformationMessage('A service is available on port ' + localPort, ...actions); + currentNotifications.delete(localPort); + + if (result === makePublic) { + await gitpodContext?.setPortVisibility(port.status.localPort, 'public'); + } else if (result === openAction) { + await openPreview(port); + } else if (result === openExternalAction) { + await openExternal(port); + } + } + async function openPreview(port: GitpodWorkspacePort): Promise { + await vscode.commands.executeCommand('simpleBrowser.api.open', port.externalUrl.toString(), { + viewColumn: vscode.ViewColumn.Beside, + preserveFocus: true + }); + } + const onDidExposeServedPortListener = (port: ExposedServedGitpodWorkspacePort) => { + if (port.status.onOpen === PortsStatus.OnOpenAction.IGNORE) { + return; + } + + if (port.status.onOpen === PortsStatus.OnOpenAction.OPEN_BROWSER) { + openExternal(port); + return; + } + + if (port.status.onOpen === PortsStatus.OnOpenAction.OPEN_PREVIEW) { + openPreview(port); + return; + } + + if (port.status.onOpen === PortsStatus.OnOpenAction.NOTIFY) { + showOpenServiceNotification(port); + return; + } + + if (port.status.onOpen === PortsStatus.OnOpenAction.NOTIFY_PRIVATE) { + showOpenServiceNotification(port, port.status.exposed.visibility !== PortVisibility.PUBLIC); + return; + } + }; + context.subscriptions.push(portViewProvider.onDidExposeServedPort(onDidExposeServedPortListener)); + + let updateTunnelsTokenSource: vscode.CancellationTokenSource | undefined; + async function updateTunnels(): Promise { + if (updateTunnelsTokenSource) { + updateTunnelsTokenSource.cancel(); + } + updateTunnelsTokenSource = new vscode.CancellationTokenSource(); + const token = updateTunnelsTokenSource.token; + // not vscode.workspace.tunnels because of https://github.com/microsoft/vscode/issues/124334 + const currentTunnels = (await vscode.commands.executeCommand('gitpod.getTunnels')) as vscode.TunnelDescription[]; + if (token.isCancellationRequested) { + return; + } + tunnelMap.clear(); + currentTunnels.forEach(tunnel => { + tunnelMap.set(tunnel.remoteAddress.port, tunnel); + }); + portViewProvider.updateTunnels(tunnelMap); + } + updateTunnels(); + context.subscriptions.push(vscode.workspace.onDidChangeTunnels(() => updateTunnels())); + context.subscriptions.push(vscode.commands.registerCommand('gitpod.vscode.workspace.openTunnel', (tunnelOptions: vscode.TunnelOptions) => { + return vscode.workspace.openTunnel(tunnelOptions); + })); + context.subscriptions.push(vscode.commands.registerCommand('gitpod.api.openTunnel', async (tunnelOptions: vscode.TunnelOptions, _tunnelCreationOptions: vscode.TunnelCreationOptions) => { + const request = new TunnelPortRequest(); + request.setPort(tunnelOptions.remoteAddress.port); + request.setTargetPort(tunnelOptions.localAddressPort || tunnelOptions.remoteAddress.port); + request.setVisibility(!!tunnelOptions?.public ? TunnelVisiblity.NETWORK : TunnelVisiblity.HOST); + await util.promisify(context.supervisor.port.tunnel.bind(context.supervisor.port, request, context.supervisor.metadata, { + deadline: Date.now() + context.supervisor.deadlines.normal + }))(); + })); + context.subscriptions.push(vscode.commands.registerCommand('gitpod.api.closeTunnel', async (port: number) => { + const request = new CloseTunnelRequest(); + request.setPort(port); + await util.promisify(context.supervisor.port.closeTunnel.bind(context.supervisor.port, request, context.supervisor.metadata, { + deadline: Date.now() + context.supervisor.deadlines.normal + }))(); + })); + context.subscriptions.push(vscode.commands.registerCommand('gitpod.dev.enableForwardedPortsView', () => + vscode.commands.executeCommand('setContext', 'forwardedPortsViewEnabled', true) + )); + context.subscriptions.push(vscode.commands.registerCommand('gitpod.dev.connectLocalApp', async () => { + const apiPortInput = await vscode.window.showInputBox({ + title: 'Connect to Local App', + prompt: 'Enter Local App API port', + value: '63100', + validateInput: value => { + const port = Number(value); + if (port <= 0) { + return 'port should be greater than 0'; + } + if (port >= 65535) { + return 'port should be less than 65535'; + } + return undefined; + } + }); + if (apiPortInput) { + const apiPort = Number(apiPortInput); + vscode.commands.executeCommand('gitpod.api.connectLocalApp', apiPort); + } + })); + vscode.commands.executeCommand('setContext', 'gitpod.portsView.visible', true); +} + +interface IOpenVSXExtensionsMetadata { + name: string; + namespace: string; + version: string; + allVersions?: { [version: string]: string }; +} + +interface IOpenVSXQueryResult { + extensions: IOpenVSXExtensionsMetadata[]; +} + +async function validateExtensions(extensionsToValidate: { id: string; version?: string }[], linkToValidate: string[], token: vscode.CancellationToken) { + const allUserExtensions = vscode.extensions.all.filter(ext => !ext.packageJSON['isBuiltin'] && !ext.packageJSON['isUserBuiltin']); + + const lookup = new Set(extensionsToValidate.map(({ id }) => id)); + const uninstalled = new Set([...lookup]); + lookup.add('github.vscode-pull-request-github'); + const missingMachined = new Set(); + for (const extension of allUserExtensions) { + const id = extension.id.toLowerCase(); + const packageBytes = await vscode.workspace.fs.readFile(vscode.Uri.joinPath(extension.extensionUri, 'package.json')); + const rawPackage = JSON.parse(packageBytes.toString()); + const isMachineScoped = !!rawPackage['__metadata']?.['isMachineScoped']; + uninstalled.delete(id); + if (isMachineScoped && !lookup.has(id)) { + missingMachined.add(id); + } + + if (token.isCancellationRequested) { + return { + extensions: [], + missingMachined: [], + uninstalled: [], + links: [] + }; + } + } + + const validatedExtensions = new Set(); + for (const { id, version } of extensionsToValidate) { + const queryResult: IOpenVSXQueryResult | undefined = await fetch( + `${process.env.VSX_REGISTRY_URL || 'https://open-vsx.org'}/api/-/query`, + { + method: 'POST', + headers: { + 'Content-Type': 'application/json', + 'Accept': 'application/json' + }, + body: JSON.stringify({ + extensionId: id + }), + timeout: 2000 + } + ).then(resp => { + if (!resp.ok) { + console.error('Failed to query open-vsx while validating gitpod.yml'); + return undefined; + } + return resp.json() as Promise; + }, e => { + console.error('Fetch failed while querying open-vsx', e); + return undefined; + }); + + const openvsxExtensionMetadata = queryResult?.extensions?.[0]; + if (openvsxExtensionMetadata) { + if (!version || (openvsxExtensionMetadata.version === version || !!openvsxExtensionMetadata.allVersions?.[version])) { + validatedExtensions.add(id); + } + } + + if (token.isCancellationRequested) { + return { + extensions: [], + missingMachined: [], + uninstalled: [], + links: [] + }; + } + } + + const links = new Set(); + for (const link of linkToValidate) { + const downloadPath = path.join(os.tmpdir(), uuid.v4()); + try { + await download(link, downloadPath, token, 10000); + const manifest = await getManifest(downloadPath); + if (manifest.engines?.vscode) { + links.add(link); + } + } catch (error) { + console.error('Failed to validate vsix url', error); + } + + if (token.isCancellationRequested) { + return { + extensions: [], + missingMachined: [], + uninstalled: [], + links: [] + }; + } + } + + return { + extensions: [...validatedExtensions], + missingMachined: [...missingMachined], + uninstalled: [...uninstalled], + links: [...links] + }; +} + +function registerExtensionManagement(context: GitpodExtensionContext): void { + const { GitpodPluginModel, isYamlSeq, isYamlScalar } = context.config; + const gitpodFileUri = vscode.Uri.file(path.join(context.info.getCheckoutLocation(), '.gitpod.yml')); + async function modifyGipodPluginModel(unitOfWork: (model: GitpodPluginModel) => void): Promise { + let document: vscode.TextDocument | undefined; + let content = ''; + try { + await util.promisify(fs.access.bind(fs))(gitpodFileUri.fsPath, fs.constants.F_OK); + document = await vscode.workspace.openTextDocument(gitpodFileUri); + content = document.getText(); + } catch { /* no-op */ } + const model = new GitpodPluginModel(content); + unitOfWork(model); + const edit = new vscode.WorkspaceEdit(); + if (document) { + edit.replace(gitpodFileUri, document.validateRange(new vscode.Range( + document.positionAt(0), + document.positionAt(content.length) + )), String(model)); + } else { + edit.createFile(gitpodFileUri, { overwrite: true }); + edit.insert(gitpodFileUri, new vscode.Position(0, 0), String(model)); + } + await vscode.workspace.applyEdit(edit); + } + context.subscriptions.push(vscode.commands.registerCommand('gitpod.extensions.addToConfig', (id: string) => { + context.fireAnalyticsEvent({ + eventName: 'vscode_execute_command_gitpod_config', + properties: { action: 'add' } + }); + return modifyGipodPluginModel(model => model.add(id)); + })); + context.subscriptions.push(vscode.commands.registerCommand('gitpod.extensions.removeFromConfig', (id: string) => { + context.fireAnalyticsEvent({ + eventName: 'vscode_execute_command_gitpod_config', + properties: { action: 'remove' } + }); + return modifyGipodPluginModel(model => model.remove(id)); + })); + context.subscriptions.push(vscode.commands.registerCommand('gitpod.extensions.installFromConfig', (id: string) => vscode.commands.executeCommand('workbench.extensions.installExtension', id, { donotSync: true }))); + const deprecatedUserExtensionMessage = 'user uploaded extensions are deprecated'; + const extensionNotFoundMessageSuffix = ' extension is not found in Open VSX'; + const invalidVSIXLinkMessageSuffix = ' does not point to a valid VSIX file'; + const missingExtensionMessageSuffix = ' extension is not synced, but not added in .gitpod.yml'; + const uninstalledExtensionMessageSuffix = ' extension is not installed, but not removed from .gitpod.yml'; + const gitpodDiagnostics = vscode.languages.createDiagnosticCollection('gitpod'); + const validateGitpodFileDelayer = new ThrottledDelayer(150); + const validateExtensionseDelayer = new ThrottledDelayer(1000); /** it can be very expensive for links to big extensions */ + let validateGitpodFileTokenSource: vscode.CancellationTokenSource | undefined; + let resolveAllDeprecated: vscode.CodeAction | undefined; + function validateGitpodFile(): void { + resolveAllDeprecated = undefined; + if (validateGitpodFileTokenSource) { + validateGitpodFileTokenSource.cancel(); + } + validateGitpodFileTokenSource = new vscode.CancellationTokenSource(); + const token = validateGitpodFileTokenSource.token; + validateGitpodFileDelayer.trigger(async () => { + if (token.isCancellationRequested) { + return; + } + let diagnostics: vscode.Diagnostic[] | undefined; + function pushDiagnostic(diagnostic: vscode.Diagnostic): void { + if (!diagnostics) { + diagnostics = []; + } + diagnostics.push(diagnostic); + } + function publishDiagnostics(): void { + if (!token.isCancellationRequested) { + gitpodDiagnostics.set(gitpodFileUri, diagnostics); + } + } + try { + const toLink = new Map(); + const toFind = new Map(); + let document: vscode.TextDocument | undefined; + try { + document = await vscode.workspace.openTextDocument(gitpodFileUri); + } catch { } + if (token.isCancellationRequested) { + return; + } + const model = document && new GitpodPluginModel(document.getText()); + const extensions = model && model.document.getIn(['vscode', 'extensions'], true); + if (document && extensions && isYamlSeq(extensions)) { + resolveAllDeprecated = new vscode.CodeAction('Resolve all against Open VSX.', vscode.CodeActionKind.QuickFix); + resolveAllDeprecated.diagnostics = []; + resolveAllDeprecated.isPreferred = true; + for (let i = 0; i < extensions.items.length; i++) { + const item = extensions.items[i]; + if (!isYamlScalar(item) || !item.range) { + continue; + } + const extension = item.value; + if (!(typeof extension === 'string')) { + continue; + } + let link: vscode.Uri | undefined; + try { + link = vscode.Uri.parse(extension.trim(), true); + if (link.scheme !== 'http' && link.scheme !== 'https') { + link = undefined; + } + } catch { } + if (link) { + toLink.set(link.toString(), new vscode.Range(document.positionAt(item.range[0]), document.positionAt(item.range[1]))); + } else { + const [idAndVersion, hash] = extension.trim().split(':', 2); + if (hash) { + const hashOffset = item.range[0] + extension.indexOf(':'); + const range = new vscode.Range(document.positionAt(hashOffset), document.positionAt(item.range[1])); + + const diagnostic = new vscode.Diagnostic(range, deprecatedUserExtensionMessage, vscode.DiagnosticSeverity.Warning); + diagnostic.source = 'gitpod'; + diagnostic.tags = [vscode.DiagnosticTag.Deprecated]; + pushDiagnostic(diagnostic); + resolveAllDeprecated.diagnostics.unshift(diagnostic); + } + const [id, version] = idAndVersion.split('@', 2); + toFind.set(id.toLowerCase(), { version, range: new vscode.Range(document.positionAt(item.range[0]), document.positionAt(item.range[1])) }); + } + } + if (resolveAllDeprecated.diagnostics.length) { + resolveAllDeprecated.edit = new vscode.WorkspaceEdit(); + for (const diagnostic of resolveAllDeprecated.diagnostics) { + resolveAllDeprecated.edit.delete(gitpodFileUri, diagnostic.range); + } + } else { + resolveAllDeprecated = undefined; + } + publishDiagnostics(); + } + + await validateExtensionseDelayer.trigger(async () => { + if (token.isCancellationRequested) { + return; + } + + const extensionsToValidate = [...toFind.entries()].map(([id, { version }]) => ({ id, version })); + const linksToValidate = [...toLink.keys()]; + const result = await validateExtensions(extensionsToValidate, linksToValidate, token); + + if (token.isCancellationRequested) { + return; + } + + const notFound = new Set([...toFind.keys()]); + for (const id of result.extensions) { + notFound.delete(id.toLowerCase()); + } + for (const id of notFound) { + const { range, version } = toFind.get(id)!; + let message = id; + if (version) { + message += '@' + version; + } + message += extensionNotFoundMessageSuffix; + const diagnostic = new vscode.Diagnostic(range, message, vscode.DiagnosticSeverity.Error); + diagnostic.source = 'gitpod'; + pushDiagnostic(diagnostic); + } + + for (const link of result.links) { + toLink.delete(link); + } + for (const [link, range] of toLink) { + const diagnostic = new vscode.Diagnostic(range, link + invalidVSIXLinkMessageSuffix, vscode.DiagnosticSeverity.Error); + diagnostic.source = 'gitpod'; + pushDiagnostic(diagnostic); + } + + for (const id of result.missingMachined) { + const diagnostic = new vscode.Diagnostic(new vscode.Range(new vscode.Position(0, 0), new vscode.Position(0, 1)), id + missingExtensionMessageSuffix, vscode.DiagnosticSeverity.Warning); + diagnostic.source = 'gitpod'; + pushDiagnostic(diagnostic); + } + + for (const id of result.uninstalled) { + if (notFound.has(id)) { + continue; + } + const extension = toFind.get(id); + if (extension) { + let message = id; + if (extension.version) { + message += '@' + extension.version; + } + message += uninstalledExtensionMessageSuffix; + const diagnostic = new vscode.Diagnostic(extension.range, message, vscode.DiagnosticSeverity.Warning); + diagnostic.source = 'gitpod'; + pushDiagnostic(diagnostic); + } + } + }); + } finally { + publishDiagnostics(); + } + }); + } + function createSearchExtensionCodeAction(id: string, diagnostic: vscode.Diagnostic) { + const title = `Search for ${id} in Open VSX.`; + const codeAction = new vscode.CodeAction(title, vscode.CodeActionKind.QuickFix); + codeAction.diagnostics = [diagnostic]; + codeAction.isPreferred = true; + codeAction.command = { + title: title, + command: 'workbench.extensions.search', + arguments: ['@id:' + id] + }; + return codeAction; + } + function createAddToConfigCodeAction(id: string, diagnostic: vscode.Diagnostic) { + const title = `Add ${id} extension to .gitpod.yml.`; + const codeAction = new vscode.CodeAction(title, vscode.CodeActionKind.QuickFix); + codeAction.diagnostics = [diagnostic]; + codeAction.isPreferred = true; + codeAction.command = { + title: title, + command: 'gitpod.extensions.addToConfig', + arguments: [id] + }; + return codeAction; + } + function createRemoveFromConfigCodeAction(id: string, diagnostic: vscode.Diagnostic, document: vscode.TextDocument): vscode.CodeAction { + const title = `Remove ${id} extension from .gitpod.yml.`; + const codeAction = new vscode.CodeAction(title, vscode.CodeActionKind.QuickFix); + codeAction.diagnostics = [diagnostic]; + codeAction.isPreferred = true; + codeAction.command = { + title: title, + command: 'gitpod.extensions.removeFromConfig', + arguments: [document.getText(diagnostic.range)] + }; + return codeAction; + } + function createInstallFromConfigCodeAction(id: string, diagnostic: vscode.Diagnostic) { + const title = `Install ${id} extension from .gitpod.yml.`; + const codeAction = new vscode.CodeAction(title, vscode.CodeActionKind.QuickFix); + codeAction.diagnostics = [diagnostic]; + codeAction.isPreferred = false; + codeAction.command = { + title: title, + command: 'gitpod.extensions.installFromConfig', + arguments: [id] + }; + return codeAction; + } + function createUninstallExtensionCodeAction(id: string, diagnostic: vscode.Diagnostic) { + const title = `Uninstall ${id} extension.`; + const codeAction = new vscode.CodeAction(title, vscode.CodeActionKind.QuickFix); + codeAction.diagnostics = [diagnostic]; + codeAction.isPreferred = false; + codeAction.command = { + title: title, + command: 'workbench.extensions.uninstallExtension', + arguments: [id] + }; + return codeAction; + } + context.subscriptions.push(vscode.languages.registerCodeActionsProvider({ + pattern: gitpodFileUri.fsPath + }, { + provideCodeActions: (document, _, context) => { + const codeActions: vscode.CodeAction[] = []; + for (const diagnostic of context.diagnostics) { + if (diagnostic.message === deprecatedUserExtensionMessage) { + if (resolveAllDeprecated) { + codeActions.push(resolveAllDeprecated); + } + const codeAction = new vscode.CodeAction('Resolve against Open VSX.', vscode.CodeActionKind.QuickFix); + codeAction.diagnostics = [diagnostic]; + codeAction.isPreferred = false; + const singleEdit = new vscode.WorkspaceEdit(); + singleEdit.delete(document.uri, diagnostic.range); + codeAction.edit = singleEdit; + codeActions.push(codeAction); + } + const notFoundIndex = diagnostic.message.indexOf(extensionNotFoundMessageSuffix); + if (notFoundIndex !== -1) { + const id = diagnostic.message.substr(0, notFoundIndex); + codeActions.push(createRemoveFromConfigCodeAction(id, diagnostic, document)); + codeActions.push(createSearchExtensionCodeAction(id, diagnostic)); + } + const missingIndex = diagnostic.message.indexOf(missingExtensionMessageSuffix); + if (missingIndex !== -1) { + const id = diagnostic.message.substr(0, missingIndex); + codeActions.push(createAddToConfigCodeAction(id, diagnostic)); + codeActions.push(createUninstallExtensionCodeAction(id, diagnostic)); + } + const uninstalledIndex = diagnostic.message.indexOf(uninstalledExtensionMessageSuffix); + if (uninstalledIndex !== -1) { + const id = diagnostic.message.substr(0, uninstalledIndex); + codeActions.push(createRemoveFromConfigCodeAction(id, diagnostic, document)); + codeActions.push(createInstallFromConfigCodeAction(id, diagnostic)); + } + const invalidVSIXIndex = diagnostic.message.indexOf(invalidVSIXLinkMessageSuffix); + if (invalidVSIXIndex !== -1) { + const link = diagnostic.message.substr(0, invalidVSIXIndex); + codeActions.push(createRemoveFromConfigCodeAction(link, diagnostic, document)); + } + } + return codeActions; + } + })); + + validateGitpodFile(); + context.subscriptions.push(gitpodDiagnostics); + const gitpodFileWatcher = vscode.workspace.createFileSystemWatcher(gitpodFileUri.fsPath); + context.subscriptions.push(gitpodFileWatcher); + context.subscriptions.push(gitpodFileWatcher.onDidCreate(() => validateGitpodFile())); + context.subscriptions.push(gitpodFileWatcher.onDidChange(() => validateGitpodFile())); + context.subscriptions.push(gitpodFileWatcher.onDidDelete(() => validateGitpodFile())); + context.subscriptions.push(vscode.extensions.onDidChange(() => validateGitpodFile())); +} + +async function registerDesktop(): Promise { + const config = vscode.workspace.getConfiguration('gitpod.openInStable'); + if (config.get('neverPrompt') === true) { + return; + } + const openAction = 'Open'; + const neverAgain = 'Don\'t Show Again'; + const action = await vscode.window.showInformationMessage('Do you want to open this workspace in VS Code Desktop?', openAction, neverAgain); + if (action === openAction) { + vscode.commands.executeCommand('gitpod.openInStable'); + } else if (action === neverAgain) { + config.update('neverPrompt', true, true); + } +} diff --git a/extensions/gitpod-web/src/portViewProvider.ts b/extensions/gitpod-web/src/portViewProvider.ts new file mode 100644 index 0000000000000..b7057d771a287 --- /dev/null +++ b/extensions/gitpod-web/src/portViewProvider.ts @@ -0,0 +1,143 @@ +/*--------------------------------------------------------------------------------------------- + * Copyright (c) Gitpod. All rights reserved. + *--------------------------------------------------------------------------------------------*/ +import * as vscode from 'vscode'; +import { GitpodExtensionContext, ExposedServedGitpodWorkspacePort, GitpodWorkspacePort, isExposedServedGitpodWorkspacePort, isExposedServedPort, PortInfo } from 'gitpod-shared'; +import { PortsStatus } from '@gitpod/supervisor-api-grpc/lib/status_pb'; + +const PortCommands = ['tunnelNetwork', 'tunnelHost', 'makePublic', 'makePrivate', 'preview', 'openBrowser', 'retryAutoExpose', 'urlCopy', 'queryPortData']; + +type PortCommand = typeof PortCommands[number]; + +export class GitpodPortViewProvider implements vscode.WebviewViewProvider { + public static readonly viewType = 'gitpod.portsView'; + + private _view?: vscode.WebviewView; + + readonly portMap = new Map(); + private portList: GitpodWorkspacePort[] = []; + + private readonly onDidExposeServedPortEmitter = new vscode.EventEmitter(); + readonly onDidExposeServedPort = this.onDidExposeServedPortEmitter.event; + + + private readonly onDidChangePortsEmitter = new vscode.EventEmitter>(); + readonly onDidChangePorts = this.onDidChangePortsEmitter.event; + + constructor(private readonly context: GitpodExtensionContext) { } + + // @ts-ignore + resolveWebviewView(webviewView: vscode.WebviewView, context: vscode.WebviewViewResolveContext, token: vscode.CancellationToken): void | Thenable { + this._view = webviewView; + webviewView.webview.options = { + enableScripts: true, + localResourceRoots: [this.context.extensionUri], + }; + webviewView.webview.html = this._getHtmlForWebview(webviewView.webview); + webviewView.onDidChangeVisibility(() => { + if (!webviewView.visible) { + return; + } + this.updateHtml(); + }); + this.onHtmlCommand(); + } + + private _getHtmlForWebview(webview: vscode.Webview) { + const codiconsUri = webview.asWebviewUri(vscode.Uri.joinPath(this.context.extensionUri, 'public', 'codicon.css')); + const styleUri = webview.asWebviewUri(vscode.Uri.joinPath(this.context.extensionUri, 'public', 'portsview.css')); + const scriptUri = webview.asWebviewUri(vscode.Uri.joinPath(this.context.extensionUri, 'public', 'portsview.js')); + const nonce = getNonce(); + return ` + + + + + + + + + Gitpod Port View + + + +`; + } + + private tunnelsMap = new Map(); + updateTunnels(tunnelsMap: Map): void { + this.tunnelsMap = tunnelsMap; + this.update(); + } + + private portStatus: PortsStatus.AsObject[] | undefined; + updatePortsStatus(portsStatus: PortsStatus.AsObject[]): void { + this.portStatus = portsStatus; + this.update(); + } + + private updating = false; + private update(): void { + if (this.updating) { return; } + this.updating = true; + try { + if (!this.portStatus) { return; } + this.portList = []; + this.portStatus.forEach(e => { + const localPort = e.localPort; + const tunnel = this.tunnelsMap.get(localPort); + let gitpodPort = this.portMap.get(localPort); + const prevStatus = gitpodPort?.status; + if (!gitpodPort) { + gitpodPort = new GitpodWorkspacePort(localPort, e, tunnel); + this.portMap.set(localPort, gitpodPort); + this.portList.push(gitpodPort); + } else { + gitpodPort.update(e, tunnel); + this.portList.push(gitpodPort); + } + if (isExposedServedGitpodWorkspacePort(gitpodPort) && !isExposedServedPort(prevStatus)) { + this.onDidExposeServedPortEmitter.fire(gitpodPort); + } + }); + this.onDidChangePortsEmitter.fire(this.portMap); + this.updateHtml(); + } finally { + this.updating = false; + } + } + + private updateHtml(): void { + this._view?.webview.postMessage({ command: 'updatePorts', ports: this.portList.map(e => e.toSvelteObject()) }); + } + + private onHtmlCommand() { + this._view?.webview.onDidReceiveMessage(async (message: { command: PortCommand; port: { info: PortInfo; status: PortsStatus.AsObject } }) => { + if (message.command === 'queryPortData') { + this.updateHtml(); + return; + } + const port = this.portMap.get(message.port.status.localPort); + if (!port) { return; } + if (message.command === 'urlCopy' && port.status.exposed) { + await vscode.env.clipboard.writeText(port.status.exposed.url); + this.context.fireAnalyticsEvent({ + eventName: 'vscode_execute_command_gitpod_ports', + properties: { action: 'urlCopy' } + }); + return; + } + vscode.commands.executeCommand('gitpod.ports.' + message.command, { port }); + }); + } +} + +function getNonce() { + let text = ''; + const possible = + 'ABCDEFGHIJKLMNOPQRSTUVWXYZabcdefghijklmnopqrstuvwxyz0123456789'; + for (let i = 0; i < 32; i++) { + text += possible.charAt(Math.floor(Math.random() * possible.length)); + } + return text; +} diff --git a/extensions/gitpod-web/src/releaseNotes.ts b/extensions/gitpod-web/src/releaseNotes.ts new file mode 100644 index 0000000000000..ba5a92b5dbf9e --- /dev/null +++ b/extensions/gitpod-web/src/releaseNotes.ts @@ -0,0 +1,323 @@ +/*--------------------------------------------------------------------------------------------- + * Copyright (c) Gitpod. All rights reserved. + * Licensed under the MIT License. See License.txt in the project root for license information. + *--------------------------------------------------------------------------------------------*/ + +import fetch, { Response } from 'node-fetch'; +import * as vscode from 'vscode'; +import { load } from 'js-yaml'; +import { CacheHelper } from './util/cache'; +import { Disposable, disposeAll } from './util/dispose'; + +export class ReleaseNotes extends Disposable { + public static readonly viewType = 'gitpodReleaseNotes'; + public static readonly websiteHost = 'https://www.gitpod.io'; + public static readonly RELEASE_NOTES_LAST_READ_KEY = 'gitpod.lastReadReleaseNotesId'; + + private panel: vscode.WebviewPanel | undefined; + private panelDisposables: vscode.Disposable[] = []; + private lastReadId: string | undefined; + private cacheHelper = new CacheHelper(this.context); + + constructor( + private readonly context: vscode.ExtensionContext, + ) { + super(); + + this.lastReadId = this.context.globalState.get(ReleaseNotes.RELEASE_NOTES_LAST_READ_KEY); + + this._register(vscode.commands.registerCommand('gitpod.showReleaseNotes', () => this.createOrShow())); + + this.showIfNewRelease(this.lastReadId); + } + + private async getLastPublish() { + const url = `${ReleaseNotes.websiteHost}/changelog/latest`; + return this.cacheHelper.getOrRefresh(url, async () => { + const resp = await fetch(url); + if (!resp.ok) { + throw new Error(`Getting latest releaseId failed: ${resp.statusText}`); + } + const { releaseId } = JSON.parse(await resp.text()); + return { + value: releaseId as string, + ttl: this.getResponseCacheTime(resp), + }; + }); + } + + private getResponseCacheTime(resp: Response) { + const cacheControlHeader = resp.headers.get('Cache-Control'); + if (!cacheControlHeader) { + return undefined; + } + const match = /max-age=(\d+)/.exec(cacheControlHeader); + if (!match) { + return undefined; + } + return parseInt(match[1], 10); + } + + private async loadChangelog(releaseId: string) { + const url = `${ReleaseNotes.websiteHost}/changelog/raw-markdown?releaseId=${releaseId}`; + const md = await this.cacheHelper.getOrRefresh(url, async () => { + const resp = await fetch(url); + if (!resp.ok) { + throw new Error(`Getting raw markdown content failed: ${resp.statusText}`); + } + const md = await resp.text(); + return { + value: md, + ttl: this.getResponseCacheTime(resp), + }; + }); + + const parseInfo = (md: string) => { + if (!md.startsWith('---')) { + return; + } + const lines = md.split('\n'); + const end = lines.indexOf('---', 1); + const content = lines.slice(1, end).join('\n'); + return load(content) as { title: string; date: string; image: string; alt: string; excerpt: string }; + }; + const info = parseInfo(md); + + const content = md + .replace(/^---.*?---/gms, '') + .replace(/ + + + diff --git a/src/vs/gitpod/browser/workbench/contrib/exportLogs.contribution.ts b/src/vs/gitpod/browser/workbench/contrib/exportLogs.contribution.ts new file mode 100644 index 0000000000000..df995fc601aa7 --- /dev/null +++ b/src/vs/gitpod/browser/workbench/contrib/exportLogs.contribution.ts @@ -0,0 +1,156 @@ +/* eslint-disable code-import-patterns */ +/* eslint-disable header/header */ +/*--------------------------------------------------------------------------------------------- + * Copyright (c) Gitpod. All rights reserved. + *--------------------------------------------------------------------------------------------*/ + +import { IBrowserWorkbenchEnvironmentService } from 'vs/workbench/services/environment/browser/environmentService'; +import { IRemoteAgentService } from 'vs/workbench/services/remote/common/remoteAgentService'; +import { IFileService } from 'vs/platform/files/common/files'; +import { Action2, MenuId, registerAction2 } from 'vs/platform/actions/common/actions'; +import { ServicesAccessor } from 'vs/platform/instantiation/common/instantiation'; +import { URI } from 'vs/base/common/uri'; +import * as resources from 'vs/base/common/resources'; +import { IProgressService, ProgressLocation } from 'vs/platform/progress/common/progress'; +import { CancellationTokenSource } from 'vs/base/common/cancellation'; +import type * as zipModule from '@zip.js/zip.js'; +import { Schemas } from 'vs/base/common/network'; +import { triggerDownload } from 'vs/base/browser/dom'; + +const getZipModule = (function () { + let zip: typeof zipModule; + return async () => { + if (!zip) { + // when actually importing the module change `zip.js` to `zipjs` + // without the dot because loader.js will do a check for `.js` extension + // and it won't resolve the module path correctly + // @ts-ignore + zip = await import('@zip.js/zipjs'); + zip.configure({ + useWebWorkers: false + }); + } + return zip; + }; +})(); + +registerAction2(class ExportLogsAction extends Action2 { + constructor() { + super({ + id: 'gitpod.workbench.exportLogs', + title: { original: 'Export all logs', value: 'Export all logs' }, + category: { original: 'Gitpod', value: 'Gitpod' }, + menu: { + id: MenuId.CommandPalette + } + }); + } + + async run(accessor: ServicesAccessor): Promise { + const progressService = accessor.get(IProgressService); + const fileService = accessor.get(IFileService); + const environmentService = accessor.get(IBrowserWorkbenchEnvironmentService); + const remoteAgentService = accessor.get(IRemoteAgentService); + + const cts = new CancellationTokenSource(); + const bufferData = await progressService.withProgress( + { + location: ProgressLocation.Dialog, + title: 'Exporting logs to zip file ...', + cancellable: true, + delay: 1000 + }, + async progress => { + const token = cts.token; + + const zip = await getZipModule(); + const uint8ArrayWriter = new zip.Uint8ArrayWriter(); + const writer = new zip.ZipWriter(uint8ArrayWriter); + + if (token.isCancellationRequested) { + return undefined; + } + + const entries: { name: string; resource: URI }[] = []; + const logsPath = URI.file(environmentService.logsPath).with({ scheme: environmentService.logFile.scheme }); + const stat = await fileService.resolve(logsPath); + if (stat.children) { + entries.push(...stat.children.filter(stat => !stat.isDirectory).map(stat => ({ name: resources.basename(stat.resource), resource: stat.resource }))); + } + + if (token.isCancellationRequested) { + return undefined; + } + + const remoteEnv = await remoteAgentService.getEnvironment(); + const remoteLogsPath = remoteEnv?.logsPath; + if (remoteLogsPath) { + const remoteAgentLogFile = resources.joinPath(remoteLogsPath, 'remoteagent.log'); + if (await fileService.exists(remoteAgentLogFile)) { + entries.push({ name: resources.basename(remoteAgentLogFile), resource: remoteAgentLogFile }); + } + } + + if (token.isCancellationRequested) { + return undefined; + } + + const remoteExtHostLogsPath = remoteEnv?.extensionHostLogsPath; + if (remoteExtHostLogsPath) { + const remoteExtHostLogsFile = resources.joinPath(remoteExtHostLogsPath, 'exthost.log'); + if (await fileService.exists(remoteExtHostLogsFile)) { + entries.push({ name: resources.basename(remoteExtHostLogsFile), resource: remoteExtHostLogsFile }); + } + + let stat = await fileService.resolve(remoteExtHostLogsPath); + if (stat.children) { + const ouputLoggingDirs = stat.children.filter(stat => stat.isDirectory); + for (const outLogDir of ouputLoggingDirs) { + if (token.isCancellationRequested) { + return undefined; + } + + stat = await fileService.resolve(outLogDir.resource); + if (stat.children) { + entries.push(...stat.children.filter(stat => !stat.isDirectory).map(stat => ({ + name: `${resources.basename(outLogDir.resource)}_${resources.basename(stat.resource)}`, + resource: stat.resource + }))); + } + } + } + } + + if (token.isCancellationRequested) { + return undefined; + } + + const credentialHelperPath = URI.file('/tmp/gitpod-git-credential-helper.log').with({ scheme: Schemas.vscodeRemote }); + if (await fileService.exists(credentialHelperPath)) { + entries.push({ name: resources.basename(credentialHelperPath), resource: credentialHelperPath }); + } + + console.log('All log entries', entries); + + for (const entry of entries) { + if (token.isCancellationRequested) { + return undefined; + } + const content = await fileService.readFile(entry.resource, { atomic: true }, token); + + if (token.isCancellationRequested) { + return undefined; + } + await writer.add(entry.name, new zip.Uint8ArrayReader(content.value.buffer)); + } + + return writer.close(); + }, + () => cts.dispose(true) + ); + + if (bufferData) { + triggerDownload(bufferData, `vscode-web-logs-${new Date().toISOString().replace(/-|:|\.\d+Z$/g, '')}.zip`); + } + } +}); diff --git a/src/vs/gitpod/browser/workbench/workbench-dev.html b/src/vs/gitpod/browser/workbench/workbench-dev.html new file mode 100644 index 0000000000000..7dc7d49dee860 --- /dev/null +++ b/src/vs/gitpod/browser/workbench/workbench-dev.html @@ -0,0 +1,167 @@ + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + diff --git a/src/vs/gitpod/browser/workbench/workbench.html b/src/vs/gitpod/browser/workbench/workbench.html new file mode 100644 index 0000000000000..3008825f72fa4 --- /dev/null +++ b/src/vs/gitpod/browser/workbench/workbench.html @@ -0,0 +1,183 @@ + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + diff --git a/src/vs/gitpod/browser/workbench/workbench.ts b/src/vs/gitpod/browser/workbench/workbench.ts new file mode 100644 index 0000000000000..299adc651f3d2 --- /dev/null +++ b/src/vs/gitpod/browser/workbench/workbench.ts @@ -0,0 +1,1034 @@ +/* eslint-disable local/code-import-patterns */ +/* eslint-disable header/header */ +/*--------------------------------------------------------------------------------------------- + * Copyright (c) Gitpod. All rights reserved. + *--------------------------------------------------------------------------------------------*/ + +/// + +import type { IDEFrontendState } from '@gitpod/gitpod-protocol/lib/ide-frontend-service'; +import type { Status, TunnelStatus } from '@gitpod/local-app-api-grpcweb'; +import { isStandalone } from 'vs/base/browser/browser'; +import { CancellationToken } from 'vs/base/common/cancellation'; +import { parse } from 'vs/base/common/marshalling'; +import { Emitter, Event } from 'vs/base/common/event'; +import { Disposable, DisposableStore, IDisposable } from 'vs/base/common/lifecycle'; +import { FileAccess, Schemas } from 'vs/base/common/network'; +import { isEqual } from 'vs/base/common/resources'; +import { URI, UriComponents } from 'vs/base/common/uri'; +import { request } from 'vs/base/parts/request/browser/request'; +import { localize } from 'vs/nls'; +import product from 'vs/platform/product/common/product'; +import { isFolderToOpen, isWorkspaceToOpen } from 'vs/platform/window/common/window'; +import { commands, create } from 'vs/workbench/workbench.web.main'; +import { posix } from 'vs/base/common/path'; +import { ltrim } from 'vs/base/common/strings'; +import type { ICredentialsProvider } from 'vs/platform/credentials/common/credentials'; +import type { IURLCallbackProvider } from 'vs/workbench/services/url/browser/urlService'; +import type { ICommand, ITunnel, ITunnelProvider, IWorkbenchConstructionOptions } from 'vs/workbench/browser/web.api'; +import type { IWorkspace, IWorkspaceProvider } from 'vs/workbench/services/host/browser/browserHostService'; +import { defaultWebSocketFactory } from 'vs/platform/remote/browser/browserSocketFactory'; +import { RemoteAuthorityResolverError, RemoteAuthorityResolverErrorCode } from 'vs/platform/remote/common/remoteAuthorityResolver'; +import { extractLocalHostUriMetaDataForPortMapping, isLocalhost } from 'vs/platform/tunnel/common/tunnel'; +import { ColorScheme } from 'vs/platform/theme/common/theme'; + +const loadingGrpc = import('@improbable-eng/grpc-web'); +const loadingLocalApp = (async () => { + // load grpc-web before local-app, see https://github.com/gitpod-io/gitpod/issues/4448 + await loadingGrpc; + return import('@gitpod/local-app-api-grpcweb'); +})(); + +interface ICredential { + service: string; + account: string; + password: string; +} + +class LocalStorageCredentialsProvider implements ICredentialsProvider { + + private static readonly CREDENTIALS_STORAGE_KEY = 'credentials.provider'; + + private readonly authService: string | undefined; + + constructor() { + let authSessionInfo: { readonly id: string; readonly accessToken: string; readonly providerId: string; readonly canSignOut?: boolean; readonly scopes: string[][] } | undefined; + const authSessionElement = document.getElementById('vscode-workbench-auth-session'); + const authSessionElementAttribute = authSessionElement ? authSessionElement.getAttribute('data-settings') : undefined; + if (authSessionElementAttribute) { + try { + authSessionInfo = JSON.parse(authSessionElementAttribute); + } catch (error) { /* Invalid session is passed. Ignore. */ } + } + + if (authSessionInfo) { + // Settings Sync Entry + this.setPassword(`${product.urlProtocol}.login`, 'account', JSON.stringify(authSessionInfo)); + + // Auth extension Entry + this.authService = `${product.urlProtocol}-${authSessionInfo.providerId}.login`; + this.setPassword(this.authService, 'account', JSON.stringify(authSessionInfo.scopes.map(scopes => ({ + id: authSessionInfo!.id, + scopes, + accessToken: authSessionInfo!.accessToken + })))); + } + } + + private _credentials: ICredential[] | undefined; + private get credentials(): ICredential[] { + if (!this._credentials) { + try { + const serializedCredentials = window.localStorage.getItem(LocalStorageCredentialsProvider.CREDENTIALS_STORAGE_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_STORAGE_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); + } + + async clear(): Promise { + window.localStorage.removeItem(LocalStorageCredentialsProvider.CREDENTIALS_STORAGE_KEY); + } +} + +class LocalStorageURLCallbackProvider extends Disposable implements IURLCallbackProvider { + + private static REQUEST_ID = 0; + + private static QUERY_KEYS: ('scheme' | 'authority' | 'path' | 'query' | 'fragment')[] = [ + 'scheme', + 'authority', + 'path', + 'query', + 'fragment' + ]; + + private readonly _onCallback = this._register(new Emitter()); + readonly onCallback = this._onCallback.event; + + private pendingCallbacks = new Set(); + private lastTimeChecked = Date.now(); + private checkCallbacksTimeout: unknown | undefined = undefined; + private onDidChangeLocalStorageDisposable: IDisposable | undefined; + + constructor(private readonly _callbackRoute: string) { + super(); + } + + create(options: Partial = {}): URI { + const id = ++LocalStorageURLCallbackProvider.REQUEST_ID; + const queryParams: string[] = [`vscode-reqid=${id}`]; + + for (const key of LocalStorageURLCallbackProvider.QUERY_KEYS) { + const value = options[key]; + + if (value) { + queryParams.push(`vscode-${key}=${encodeURIComponent(value)}`); + } + } + + // TODO@joao remove eventually + // https://github.com/microsoft/vscode-dev/issues/62 + // https://github.com/microsoft/vscode/blob/159479eb5ae451a66b5dac3c12d564f32f454796/extensions/github-authentication/src/githubServer.ts#L50-L50 + if (!(options.authority === 'vscode.github-authentication' && options.path === '/dummy')) { + const key = `vscode-web.url-callbacks[${id}]`; + window.localStorage.removeItem(key); + + this.pendingCallbacks.add(id); + this.startListening(); + } + + return URI.parse(window.location.href).with({ path: this._callbackRoute, query: queryParams.join('&') }); + } + + private startListening(): void { + if (this.onDidChangeLocalStorageDisposable) { + return; + } + + const fn = () => this.onDidChangeLocalStorage(); + window.addEventListener('storage', fn); + this.onDidChangeLocalStorageDisposable = { dispose: () => window.removeEventListener('storage', fn) }; + } + + private stopListening(): void { + this.onDidChangeLocalStorageDisposable?.dispose(); + this.onDidChangeLocalStorageDisposable = undefined; + } + + // this fires every time local storage changes, but we + // don't want to check more often than once a second + private async onDidChangeLocalStorage(): Promise { + const ellapsed = Date.now() - this.lastTimeChecked; + + if (ellapsed > 1000) { + this.checkCallbacks(); + } else if (this.checkCallbacksTimeout === undefined) { + this.checkCallbacksTimeout = setTimeout(() => { + this.checkCallbacksTimeout = undefined; + this.checkCallbacks(); + }, 1000 - ellapsed); + } + } + + private checkCallbacks(): void { + let pendingCallbacks: Set | undefined; + + for (const id of this.pendingCallbacks) { + const key = `vscode-web.url-callbacks[${id}]`; + const result = window.localStorage.getItem(key); + + if (result !== null) { + try { + this._onCallback.fire(URI.revive(JSON.parse(result))); + } catch (error) { + console.error(error); + } + + pendingCallbacks = pendingCallbacks ?? new Set(this.pendingCallbacks); + pendingCallbacks.delete(id); + window.localStorage.removeItem(key); + } + } + + if (pendingCallbacks) { + this.pendingCallbacks = pendingCallbacks; + + if (this.pendingCallbacks.size === 0) { + this.stopListening(); + } + } + + this.lastTimeChecked = Date.now(); + } +} + +class WorkspaceProvider implements IWorkspaceProvider { + + private static QUERY_PARAM_EMPTY_WINDOW = 'ew'; + private static QUERY_PARAM_FOLDER = 'folder'; + private static QUERY_PARAM_WORKSPACE = 'workspace'; + + private static QUERY_PARAM_PAYLOAD = 'payload'; + + static create(config: IWorkbenchConstructionOptions & { folderUri?: UriComponents; workspaceUri?: UriComponents }) { + let foundWorkspace = false; + let workspace: IWorkspace; + let payload = Object.create(null); + + const query = new URL(document.location.href).searchParams; + query.forEach((value, key) => { + switch (key) { + + // Folder + case WorkspaceProvider.QUERY_PARAM_FOLDER: + if (config.remoteAuthority && value.startsWith(posix.sep)) { + // when connected to a remote and having a value + // that is a path (begins with a `/`), assume this + // is a vscode-remote resource as simplified URL. + workspace = { folderUri: URI.from({ scheme: Schemas.vscodeRemote, path: value, authority: config.remoteAuthority }) }; + } else { + workspace = { folderUri: URI.parse(value) }; + } + foundWorkspace = true; + break; + + // Workspace + case WorkspaceProvider.QUERY_PARAM_WORKSPACE: + if (config.remoteAuthority && value.startsWith(posix.sep)) { + // when connected to a remote and having a value + // that is a path (begins with a `/`), assume this + // is a vscode-remote resource as simplified URL. + workspace = { workspaceUri: URI.from({ scheme: Schemas.vscodeRemote, path: value, authority: config.remoteAuthority }) }; + } else { + workspace = { workspaceUri: URI.parse(value) }; + } + foundWorkspace = true; + break; + + // Empty + case WorkspaceProvider.QUERY_PARAM_EMPTY_WINDOW: + workspace = undefined; + foundWorkspace = true; + break; + + // Payload + case WorkspaceProvider.QUERY_PARAM_PAYLOAD: + try { + payload = parse(value); // use marshalling#parse() to revive potential URIs + } catch (error) { + console.error(error); // possible invalid JSON + } + break; + } + }); + + // If no workspace is provided through the URL, check for config + // attribute from server and fallback to last opened workspace + // from storage + if (!foundWorkspace) { + if (config.folderUri) { + workspace = { folderUri: URI.revive(config.folderUri) }; + } else if (config.workspaceUri) { + workspace = { workspaceUri: URI.revive(config.workspaceUri) }; + } + } + + return new WorkspaceProvider(workspace, payload, config); + } + + readonly trusted = true; + + private constructor( + readonly workspace: IWorkspace, + readonly payload: object, + private readonly config: IWorkbenchConstructionOptions + ) { + } + + 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)) { + let queryParamFolder: string; + if (this.config.remoteAuthority && workspace.folderUri.scheme === Schemas.vscodeRemote) { + // when connected to a remote and having a folder + // for that remote, only use the path as query + // value to form shorter, nicer URLs. + // ensure paths are absolute (begin with `/`) + // clipboard: ltrim(workspace.folderUri.path, posix.sep) + queryParamFolder = `${posix.sep}${ltrim(workspace.folderUri.path, posix.sep)}`; + } else { + queryParamFolder = encodeURIComponent(workspace.folderUri.toString(true)); + } + + targetHref = `${document.location.origin}${document.location.pathname}?${WorkspaceProvider.QUERY_PARAM_FOLDER}=${queryParamFolder}`; + } + + // Workspace + else if (isWorkspaceToOpen(workspace)) { + let queryParamWorkspace: string; + if (this.config.remoteAuthority && workspace.workspaceUri.scheme === Schemas.vscodeRemote) { + // when connected to a remote and having a workspace + // for that remote, only use the path as query + // value to form shorter, nicer URLs. + // ensure paths are absolute (begin with `/`) + queryParamWorkspace = `${posix.sep}${ltrim(workspace.workspaceUri.path, posix.sep)}`; + } else { + queryParamWorkspace = encodeURIComponent(workspace.workspaceUri.toString(true)); + } + + targetHref = `${document.location.origin}${document.location.pathname}?${WorkspaceProvider.QUERY_PARAM_WORKSPACE}=${queryParamWorkspace}`; + } + + // 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; + } +} + +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 }); +} + +const devMode = product.nameShort.endsWith(' Dev'); + +let _state: IDEFrontendState = 'init'; +let _failureCause: Error | undefined; +const onDidChangeEmitter = new Emitter(); +const toStop = new DisposableStore(); +toStop.add(onDidChangeEmitter); +toStop.add({ + dispose: () => { + _state = 'terminated'; + onDidChangeEmitter.fire(); + } +}); + +function start(): IDisposable { + doStart().then(toDoStop => { + toStop.add(toDoStop); + _state = 'ready'; + onDidChangeEmitter.fire(); + }, e => { + _failureCause = e; + _state = 'terminated'; + onDidChangeEmitter.fire(); + }); + return toStop; +} + +interface WorkspaceInfoResponse { + workspaceId: string; + instanceId: string; + checkoutLocation: string; + workspaceLocationFile?: string; + workspaceLocationFolder?: string; + userHome: string; + gitpodHost: string; + gitpodApi: { host: string }; + workspaceContextUrl: string; + workspaceClusterHost: string; + ideAlias: string; +} + +async function doStart(): Promise { + let supervisorHost = window.location.host; + // running from sources + if (devMode) { + supervisorHost = supervisorHost.substring(supervisorHost.indexOf('-') + 1); + } + const infoResponse = await fetch(window.location.protocol + '//' + supervisorHost + '/_supervisor/v1/info/workspace', { + credentials: 'include' + }); + if (!infoResponse.ok) { + throw new Error(`Getting workspace info failed: ${infoResponse.statusText}`); + } + if (_state === 'terminated') { + return Disposable.None; + } + + const subscriptions = new DisposableStore(); + + const info: WorkspaceInfoResponse = await infoResponse.json(); + if (_state as any === 'terminated') { + return Disposable.None; + } + + const remoteAuthority = window.location.host; + + // To make webviews work in development, go to file src/vs/workbench/contrib/webview/browser/pre/main.js + // and update `signalReady` method to bypass hostname check + const baseUri = FileAccess.asBrowserUri('', require); + const uuidUri = `${baseUri.scheme}://{{uuid}}.${info.workspaceClusterHost}${baseUri.path.replace(/^\/blobserve/, '').replace(/\/out\/$/, '')}`; + const webEndpointUrlTemplate = uuidUri; + const webviewEndpoint = devMode ? undefined : `${uuidUri}/out/vs/workbench/contrib/webview/browser/pre/`; + + const folderUri = info.workspaceLocationFolder + ? URI.from({ + scheme: Schemas.vscodeRemote, + authority: remoteAuthority, + path: info.workspaceLocationFolder + }) + : undefined; + const workspaceUri = info.workspaceLocationFile + ? URI.from({ + scheme: Schemas.vscodeRemote, + authority: remoteAuthority, + path: info.workspaceLocationFile + }) + : undefined; + + const gitpodHostURL = new URL(info.gitpodHost); + const gitpodDomain = gitpodHostURL.protocol + '//*.' + gitpodHostURL.host; + const syncStoreURL = info.gitpodHost + '/code-sync'; + + const credentialsProvider = new LocalStorageCredentialsProvider(); + interface GetTokenResponse { + token: string; + user?: string; + scope?: string[]; + } + const scopes = [ + 'function:accessCodeSyncStorage' + ]; + const tokenResponse = await fetch(window.location.protocol + '//' + supervisorHost + '/_supervisor/v1/token/gitpod/' + info.gitpodApi.host + '/' + scopes.join(','), { + credentials: 'include' + }); + if (_state as any === 'terminated') { + return Disposable.None; + } + if (!tokenResponse.ok) { + console.warn(`Getting Gitpod token failed: ${tokenResponse.statusText}`); + } else { + const getToken: GetTokenResponse = await tokenResponse.json(); + if (_state as any === 'terminated') { + return Disposable.None; + } + + // see https://github.com/gitpod-io/vscode/blob/gp-code/src/vs/workbench/services/authentication/browser/authenticationService.ts#L34 + type AuthenticationSessionInfo = { readonly id: string; readonly accessToken: string; readonly providerId: string; readonly canSignOut?: boolean }; + const currentSession: AuthenticationSessionInfo = { + // current session ID should remain stable between window reloads + // otherwise setting sync will log out + id: 'gitpod-current-session', + accessToken: getToken.token, + providerId: 'gitpod', + canSignOut: false + }; + // Settings Sync Entry + await credentialsProvider.setPassword(`${product.urlProtocol}.login`, 'account', JSON.stringify(currentSession)); + // Auth extension Entry + await credentialsProvider.setPassword(`${product.urlProtocol}-gitpod.login`, 'account', JSON.stringify([{ + id: currentSession.id, + scopes: getToken.scope || scopes, + accessToken: currentSession.accessToken + }])); + } + if (_state as any === 'terminated') { + return Disposable.None; + } + + const { grpc } = await loadingGrpc; + const { LocalAppClient, TunnelStatusRequest, TunnelVisiblity } = await loadingLocalApp; + + //#region tunnels + class Tunnel implements ITunnel { + localAddress: string; + remoteAddress: { port: number; host: string }; + public?: boolean; + + private readonly onDidDisposeEmitter = new Emitter(); + readonly onDidDispose = this.onDidDisposeEmitter.event; + private disposed = false; + constructor( + public status: TunnelStatus.AsObject + ) { + this.remoteAddress = { + host: 'localhost', + port: status.remotePort + }; + this.localAddress = 'http://localhost:' + status.localPort; + this.public = status.visibility === TunnelVisiblity.NETWORK; + } + async dispose(close = true): Promise { + if (this.disposed) { + return; + } + this.disposed = true; + if (close) { + try { + await commands.executeCommand('gitpod.api.closeTunnel', this.remoteAddress.port); + } catch (e) { + console.error('failed to close tunnel', e); + } + } + this.onDidDisposeEmitter.fire(undefined); + this.onDidDisposeEmitter.dispose(); + } + } + const tunnels = new Map(); + const onDidChangeTunnels = new Emitter(); + function observeTunneled(apiPort: number): IDisposable { + const client = new LocalAppClient('http://localhost:' + apiPort, { + transport: grpc.WebsocketTransport() + }); + commands.executeCommand('setContext', 'gitpod.localAppConnected', true); + let run = true; + let stopUpdates: Function | undefined; + let attempts = 0; + let reconnectDelay = 1000; + const maxAttempts = 5; + (async () => { + while (run) { + if (attempts === maxAttempts) { + commands.executeCommand('setContext', 'gitpod.localAppConnected', false); + console.error(`could not connect to local app ${maxAttempts} times, giving up, use 'Gitpod: Connect to Local App' command to retry`); + return; + } + let err: Error | undefined; + let status: Status | undefined; + try { + const request = new TunnelStatusRequest(); + request.setObserve(true); + request.setInstanceId(info.instanceId); + const stream = client.tunnelStatus(request); + stopUpdates = stream.cancel.bind(stream); + status = await new Promise(resolve => { + stream.on('end', resolve); + stream.on('data', response => { + attempts = 0; + reconnectDelay = 1000; + let notify = false; + const toDispose = new Set(tunnels.keys()); + for (const status of response.getTunnelsList()) { + toDispose.delete(status.getRemotePort()); + const tunnel = new Tunnel(status.toObject()); + const existing = tunnels.get(status.getRemotePort()); + if (!existing || existing.public !== tunnel.public) { + existing?.dispose(false); + tunnels.set(status.getRemotePort(), tunnel); + commands.executeCommand('gitpod.vscode.workspace.openTunnel', { + remoteAddress: tunnel.remoteAddress, + localAddressPort: tunnel.remoteAddress.port, + public: tunnel.public + }); + notify = true; + } + } + for (const port of toDispose) { + const tunnel = tunnels.get(port); + if (tunnel) { + tunnel.dispose(false); + tunnels.delete(port); + notify = true; + } + } + if (notify) { + onDidChangeTunnels.fire(undefined); + } + }); + }); + } catch (e) { + err = e; + } finally { + stopUpdates = undefined; + } + if (tunnels.size) { + for (const tunnel of tunnels.values()) { + tunnel.dispose(false); + } + tunnels.clear(); + onDidChangeTunnels.fire(undefined); + } + if (status?.code !== grpc.Code.Canceled) { + console.warn('cannot maintain connection to local app', err || status); + } + await new Promise(resolve => setTimeout(resolve, reconnectDelay)); + reconnectDelay = reconnectDelay * 1.5; + attempts++; + } + })(); + return { + dispose: () => { + run = false; + if (stopUpdates) { + stopUpdates(); + } + } + }; + } + const defaultApiPort = 63100; + let cancelObserveTunneled = observeTunneled(defaultApiPort); + subscriptions.add(cancelObserveTunneled); + const connectLocalApp: ICommand = { + id: 'gitpod.api.connectLocalApp', + handler: (apiPort: number = defaultApiPort) => { + cancelObserveTunneled.dispose(); + cancelObserveTunneled = observeTunneled(apiPort); + subscriptions.add(cancelObserveTunneled); + } + }; + const getTunnels: ICommand = { + id: 'gitpod.getTunnels', + handler: () => /* vscode.TunnelDescription[] */ { + const result: { + remoteAddress: { port: number; host: string }; + //The complete local address(ex. localhost:1234) + localAddress: { port: number; host: string } | string; + public?: boolean; + }[] = []; + for (const tunnel of tunnels.values()) { + result.push({ + remoteAddress: tunnel.remoteAddress, + localAddress: tunnel.localAddress, + public: tunnel.public + }); + } + return result; + } + }; + const tunnelProvider: ITunnelProvider = { + features: { + privacyOptions: [ + { + id: 'public', + label: 'Public', + themeIcon: 'eye' + }, + { + id: 'private', + label: 'Private', + themeIcon: 'lock' + } + ], + public: true, + elevation: false + }, + tunnelFactory: async (tunnelOptions, tunnelCreationOptions) => { + const remotePort = tunnelOptions.remoteAddress.port; + try { + if (!isLocalhost(tunnelOptions.remoteAddress.host)) { + throw new Error('only tunneling of localhost is supported, but: ' + tunnelOptions.remoteAddress.host); + } + let tunnel = tunnels.get(remotePort); + if (!tunnel) { + await commands.executeCommand('gitpod.api.openTunnel', tunnelOptions, tunnelCreationOptions); + tunnel = tunnels.get(remotePort) || await new Promise(resolve => { + const toUnsubscribe = onDidChangeTunnels.event(() => { + const resolved = tunnels.get(remotePort); + if (resolved) { + resolve(resolved); + toUnsubscribe.dispose(); + } + }); + subscriptions.add(toUnsubscribe); + }); + } + return tunnel; + } catch (e) { + console.trace(`failed to tunnel to '${tunnelOptions.remoteAddress.host}':'${remotePort}': `, e); + // actually should be external URL and this method should never throw + const tunnel = new Tunnel({ + localPort: remotePort, + remotePort: remotePort, + visibility: TunnelVisiblity.NONE + }); + // closed tunnel, invalidate in next tick + setTimeout(() => tunnel.dispose(false)); + return tunnel; + } + } + }; + //#endregion + + const getLoggedInUser: ICommand = { + id: 'gitpod.api.getLoggedInUser', + handler: () => { + if (devMode) { + throw new Error('not supported in dev mode'); + } + return window.gitpod.service.server.getLoggedInUser(); + } + }; + + subscriptions.add(create(document.body, { + remoteAuthority, + webviewEndpoint, + webSocketFactory: { + create: (url, debugLabel) => { + if (_state as any === 'terminated') { + throw new RemoteAuthorityResolverError('workspace stopped', RemoteAuthorityResolverErrorCode.NotAvailable); + } + const socket = defaultWebSocketFactory.create(url, debugLabel); + const onError = new Emitter(); + socket.onError(e => { + if (_state as any === 'terminated') { + // if workspace stopped then don't try to reconnect, regardless how websocket was closed + e = new RemoteAuthorityResolverError('workspace stopped', RemoteAuthorityResolverErrorCode.NotAvailable, e); + } + // otherwise reconnect always + if (!(e instanceof RemoteAuthorityResolverError)) { + // by default VS Code does not try to reconnect if the web socket is closed clean: + // https://github.com/gitpod-io/vscode/blob/7bb129c76b6e95b35758e3e3bc5464ed6ec6397c/src/vs/platform/remote/browser/browserSocketFactory.ts#L150-L152 + // override it as a temporary network error + e = new RemoteAuthorityResolverError('WebSocket closed', RemoteAuthorityResolverErrorCode.TemporarilyNotAvailable, e); + } + onError.fire(e); + }); + return { + onData: socket.onData, + onOpen: socket.onOpen, + onClose: socket.onClose, + onError: onError.event, + send: data => socket.send(data), + close: () => { + socket.close(); + onError.dispose(); + } + }; + } + }, + workspaceProvider: WorkspaceProvider.create({ remoteAuthority, folderUri, workspaceUri }), + resolveExternalUri: async (uri) => { + const localhost = extractLocalHostUriMetaDataForPortMapping(uri); + if (!localhost) { + return uri; + } + let externalEndpoint: URI; + const tunnel = tunnels.get(localhost.port); + if (tunnel) { + externalEndpoint = URI.parse('http://localhost:' + tunnel.status.localPort); + } else { + const publicUrl = (await commands.executeCommand('gitpod.resolveExternalPort', localhost.port)) as any as string; + externalEndpoint = URI.parse(publicUrl); + } + return externalEndpoint.with({ + path: uri.path, + query: uri.query, + fragment: uri.fragment + }); + }, + homeIndicator: { + href: info.gitpodHost, + icon: 'code', + title: localize('home', "Home") + }, + windowIndicator: { + onDidChange: Event.None, + label: `$(gitpod) Gitpod`, + tooltip: 'Editing on Gitpod' + }, + initialColorTheme: { + themeType: ColorScheme.LIGHT, + // should be aligned with https://github.com/gitpod-io/gitpod-vscode-theme + colors: { + 'statusBarItem.remoteBackground': '#FF8A00', + 'statusBarItem.remoteForeground': '#f9f9f9', + 'statusBar.background': '#F3F3F3', + 'statusBar.foreground': '#292524', + 'statusBar.noFolderBackground': '#FF8A00', + 'statusBar.debuggingBackground': '#FF8A00', + 'sideBar.background': '#fcfcfc', + 'sideBarSectionHeader.background': '#f9f9f9', + 'activityBar.background': '#f9f9f9', + 'activityBar.foreground': '#292524', + 'editor.background': '#ffffff', + 'button.background': '#FF8A00', + 'button.foreground': '#ffffff', + 'list.activeSelectionBackground': '#e7e5e4', + 'list.activeSelectionForeground': '#292524', + 'list.inactiveSelectionForeground': '#292524', + 'list.inactiveSelectionBackground': '#F9F9F9', + 'minimap.background': '#FCFCFC', + 'minimapSlider.activeBackground': '#F9F9F9', + 'tab.inactiveBackground': '#F9F9F9', + 'editor.selectionBackground': '#FFE4BC', + 'editor.inactiveSelectionBackground': '#FFE4BC', + 'textLink.foreground': '#ffb45b' + } + }, + configurationDefaults: { + 'workbench.colorTheme': 'Gitpod Light', + 'workbench.preferredLightColorTheme': 'Gitpod Light', + 'workbench.preferredDarkColorTheme': 'Gitpod Dark', + }, + urlCallbackProvider: new LocalStorageURLCallbackProvider('/vscode-extension-auth-callback'), + credentialsProvider, + productConfiguration: { + linkProtectionTrustedDomains: [ + ...(product.linkProtectionTrustedDomains || []), + gitpodDomain + ], + 'configurationSync.store': { + url: syncStoreURL, + stableUrl: syncStoreURL, + insidersUrl: syncStoreURL, + canSwitch: false, + authenticationProviders: { + gitpod: { + scopes: ['function:accessCodeSyncStorage'] + } + } + }, + 'editSessions.store': { + url: syncStoreURL, + canSwitch: false, + authenticationProviders: { + gitpod: { + scopes: ['function:accessCodeSyncStorage'] + } + } + }, + webEndpointUrlTemplate + }, + settingsSyncOptions: { + enabled: true, + extensionsSyncStateVersion: info.instanceId, + enablementHandler: enablement => { + // TODO + } + }, + tunnelProvider, + commands: [ + getTunnels, + connectLocalApp, + getLoggedInUser + ] + })); + return subscriptions; +} + +if (devMode) { + doStart(); +} else { + window.gitpod.ideService = { + get state() { + return _state; + }, + get failureCause() { + return _failureCause; + }, + onDidChange: onDidChangeEmitter.event, + start: () => start() + }; +} diff --git a/src/vs/gitpod/common/insightsHelper.ts b/src/vs/gitpod/common/insightsHelper.ts new file mode 100644 index 0000000000000..3b2c914658626 --- /dev/null +++ b/src/vs/gitpod/common/insightsHelper.ts @@ -0,0 +1,301 @@ +/* eslint-disable local/code-import-patterns */ +/* eslint-disable header/header */ +/*--------------------------------------------------------------------------------------------- + * Copyright (c) Gitpod. All rights reserved. + *--------------------------------------------------------------------------------------------*/ + +import { RemoteTrackMessage } from '@gitpod/gitpod-protocol/lib/analytics'; +import type { IDEMetric } from '@gitpod/ide-metrics-api-grpcweb/lib/index'; +import type { ErrorEvent } from 'vs/platform/telemetry/common/errorTelemetry'; + +export interface GitpodErrorEvent extends ErrorEvent { + fromBrowser?: boolean; +} + +export interface ReportErrorParam { + workspaceId: string; + instanceId: string; + errorStack: string; + userId: string; + component: string; + version: string; + properties?: Record; +} + +function getEventName(name: string) { + const str = name.replace('remoteConnection', '').replace('remoteReconnection', ''); + return str.charAt(0).toLowerCase() + str.slice(1); +} + +// const formatEventName = (str: string) => { +// return str +// .replace(/^[A-Z]/g, letter => letter.toLowerCase()) +// .replace(/[A-Z]/g, letter => `_${letter.toLowerCase()}`) +// .replace(/[^\w]/g, '_'); +// }; + +let readAccessTracked = false; +let writeAccessTracked = false; + +export function mapMetrics(source: 'window' | 'remote-server', eventName: string, data: any, extraData?: any): IDEMetric[] | undefined { + const maybeMetrics = doMapMetrics(source, eventName, data, extraData); + return maybeMetrics instanceof Array ? maybeMetrics : typeof maybeMetrics === 'object' ? [maybeMetrics] : undefined; +} + +function doMapMetrics(source: 'window' | 'remote-server', eventName: string, data: any, extraData?: any): IDEMetric[] | IDEMetric | undefined { + if (source === 'remote-server') { + if (eventName.startsWith('extensionGallery:')) { + const operation = eventName.split(':')[1]; + if (operation === 'install' || operation === 'update' || operation === 'uninstall') { + const metrics: IDEMetric[] = [{ + kind: 'counter', + name: 'gitpod_vscode_extension_gallery_operation_total', + labels: { + operation, + status: data.success ? 'success' : 'failure', + galleryHost: extraData.galleryHost + // TODO errorCode + } + }]; + if (typeof data.duration === 'number') { + metrics.push({ + kind: 'histogram', + name: 'gitpod_vscode_extension_gallery_operation_duration_seconds', + labels: { + operation, + galleryHost: extraData.galleryHost + }, + value: data.duration / 1000 + }); + } + return metrics; + } + } + if (eventName === 'galleryService:query') { + const metrics: IDEMetric[] = [{ + kind: 'counter', + name: 'gitpod_vscode_extension_gallery_query_total', + labels: { + status: data.success ? 'success' : 'failure', + statusCode: data.statusCode, + errorCode: data.errorCode, + galleryHost: extraData.galleryHost + } + }, { + kind: 'histogram', + name: 'gitpod_vscode_extension_gallery_query_duration_seconds', + labels: { + galleryHost: extraData.galleryHost + }, + value: data.duration / 1000 + }]; + return metrics; + } + } + return undefined; +} + +// please don't send same metrics from browser window and remote server +export function mapTelemetryData(source: 'window' | 'remote-server', eventName: string, data: any): RemoteTrackMessage | undefined { + if (source === 'remote-server') { + if (eventName.startsWith('extensionGallery:')) { + const operation = eventName.split(':')[1]; + if (operation === 'install' || operation === 'update' || operation === 'uninstall') { + return { + event: 'vscode_extension_gallery', + properties: { + kind: operation, + extensionId: data.id, + workspaceId: data.workspaceId, + workspaceInstanceId: data.workspaceInstanceId, + sessionID: data.sessionID, + timestamp: data.timestamp, + success: data.success, + errorCode: data.errorcode, + }, + }; + } + } + switch (eventName) { + case 'editorOpened': + if (readAccessTracked || (data.typeId) !== 'workbench.editors.files.fileEditorInput') { + return undefined; + } + readAccessTracked = true; + return { + event: 'vscode_file_access', + properties: { + kind: 'read', + workspaceId: data.workspaceId, + workspaceInstanceId: data.workspaceInstanceId, + sessionID: data.sessionID, + timestamp: data.timestamp + }, + }; + case 'filePUT': + if (writeAccessTracked) { + return undefined; + } + writeAccessTracked = true; + return { + event: 'vscode_file_access', + properties: { + kind: 'write', + workspaceId: data.workspaceId, + workspaceInstanceId: data.workspaceInstanceId, + sessionID: data.sessionID, + timestamp: data.timestamp + }, + }; + case 'notification:show': + return { + event: 'vscode_notification', + properties: { + action: 'show', + notificationId: data.id, + source: data.source, + workspaceId: data.workspaceId, + workspaceInstanceId: data.workspaceInstanceId, + sessionID: data.sessionID, + timestamp: data.timestamp + }, + }; + case 'notification:close': + return { + event: 'vscode_notification', + properties: { + action: 'close', + notificationId: data.id, + source: data.source, + workspaceId: data.workspaceId, + workspaceInstanceId: data.workspaceInstanceId, + sessionID: data.sessionID, + timestamp: data.timestamp + }, + }; + case 'notification:hide': + return { + event: 'vscode_notification', + properties: { + action: 'hide', + notificationId: data.id, + source: data.source, + workspaceId: data.workspaceId, + workspaceInstanceId: data.workspaceInstanceId, + sessionID: data.sessionID, + timestamp: data.timestamp + }, + }; + case 'notification:actionExecuted': + return { + event: 'vscode_notification', + properties: { + action: 'actionExecuted', + notificationId: data.id, + source: data.source, + actionLabel: data.actionLabel, + workspaceId: data.workspaceId, + workspaceInstanceId: data.workspaceInstanceId, + sessionID: data.sessionID, + timestamp: data.timestamp + }, + }; + case 'settingsEditor.settingModified': + return { + event: 'vscode_update_configuration', + properties: { + key: data.key, + target: data.target, + workspaceId: data.workspaceId, + workspaceInstanceId: data.workspaceInstanceId, + sessionID: data.sessionID, + timestamp: data.timestamp + }, + }; + case 'gettingStarted.ActionExecuted': + return { + event: 'vscode_getting_started', + properties: { + kind: 'action_executed', + command: data.command, + argument: data.argument, + workspaceId: data.workspaceId, + workspaceInstanceId: data.workspaceInstanceId, + sessionID: data.sessionID, + timestamp: data.timestamp + }, + }; + case 'editorClosed': + if ((data.typeId) !== 'workbench.editors.gettingStartedInput') { + return undefined; + } + return { + event: 'vscode_getting_started', + properties: { + kind: 'editor_closed', + workspaceId: data.workspaceId, + workspaceInstanceId: data.workspaceInstanceId, + sessionID: data.sessionID, + timestamp: data.timestamp + }, + }; + } + } else if (source === 'window') { + switch (eventName) { + case 'remoteConnectionSuccess': + case 'remoteConnectionFailure': + type ConnectionProperties = { + state: string; + // Time, in ms, until connected / connection failure + connectionTimeMs: number; + // Detailed error message of failure + error?: string; + }; + return { + event: 'vscode_browser_remote_connection', + properties: { + state: getEventName(eventName), + // Time, in ms, until connected / connection failure + connectionTimeMs: data.connectionTimeMs, + // Detailed error message of failure + error: data.message, + } as ConnectionProperties + }; + case 'remoteConnectionLatency': + type ConnectionLatencyProperties = { + // Latency to the remote, in milliseconds + latencyMs?: number; + }; + return { + event: 'vscode_browser_remote_connection_latency', + properties: { + latencyMs: data.latencyMs + } as ConnectionLatencyProperties + }; + case 'remoteConnectionGain': + case 'remoteConnectionLost': + case 'remoteReconnectionWait': + case 'remoteReconnectionReload': + case 'remoteReconnectionRunning': + case 'remoteReconnectionPermanentFailure': + type ReconnectionProperties = { + event: string; + reconnectionToken: string; + millisSinceLastIncomingData?: number; + attempt?: number; + handled?: boolean; + }; + return { + event: 'vscode_browser_remote_reconnection', + properties: { + event: getEventName(eventName), + reconnectionToken: data.reconnectionToken, + millisSinceLastIncomingData: data.millisSinceLastIncomingData, + attempt: data.attempt, + handled: data.handled, + } as ReconnectionProperties + }; + } + } + return undefined; +} diff --git a/src/vs/gitpod/node/customServerIntegration.ts b/src/vs/gitpod/node/customServerIntegration.ts new file mode 100644 index 0000000000000..6bac79bced607 --- /dev/null +++ b/src/vs/gitpod/node/customServerIntegration.ts @@ -0,0 +1,95 @@ +/* eslint-disable code-import-patterns */ +/* eslint-disable header/header */ +/*--------------------------------------------------------------------------------------------- + * Copyright (c) Gitpod. All rights reserved. + *--------------------------------------------------------------------------------------------*/ + +import * as http from 'http'; +import { Emitter } from 'vs/base/common/event'; +import { IDisposable } from 'vs/base/common/lifecycle'; + +const devMode = !!process.env['VSCODE_DEV']; +const supervisorAddr = process.env.SUPERVISOR_ADDR || 'localhost:22999'; + +let activeCliIpcHook: string | undefined; +const didChangeActiveCliIpcHookEmitter = new Emitter(); + +function withActiveCliIpcHook(cb: (activeCliIpcHook: string) => void): IDisposable { + if (activeCliIpcHook) { + cb(activeCliIpcHook); + return { dispose: () => { } }; + } + const listener = didChangeActiveCliIpcHookEmitter.event(() => { + if (activeCliIpcHook) { + listener.dispose(); + cb(activeCliIpcHook); + } + }); + return listener; +} + +function deleteActiveCliIpcHook(cliIpcHook: string) { + if (!activeCliIpcHook || activeCliIpcHook !== cliIpcHook) { + return; + } + activeCliIpcHook = undefined; + didChangeActiveCliIpcHookEmitter.fire(); +} + +function setActiveCliIpcHook(cliIpcHook: string): void { + if (activeCliIpcHook === cliIpcHook) { + return; + } + activeCliIpcHook = cliIpcHook; + didChangeActiveCliIpcHookEmitter.fire(); +} + +export function handleGitpodCLIRequest(pathname: string, req: http.IncomingMessage, res: http.ServerResponse) { + if (pathname.startsWith('/cli')) { + if (req.method === 'GET') { + res.writeHead(200, { 'Content-Type': 'text/plain' }); + res.end(activeCliIpcHook); + return true; + } + if (req.method === 'DELETE') { + const cliIpcHook = decodeURIComponent(pathname.substring('/cli/ipcHookCli/'.length)); + deleteActiveCliIpcHook(cliIpcHook); + res.writeHead(200); + res.end(); + return true; + } + if (req.method === 'PUT') { + const cliIpcHook = decodeURIComponent(pathname.substring('/cli/ipcHookCli/'.length)); + setActiveCliIpcHook(cliIpcHook); + res.writeHead(200); + res.end(); + return true; + } + if (req.method === 'POST') { + const listener = withActiveCliIpcHook(activeCliIpcHook => + req.pipe(http.request({ + socketPath: activeCliIpcHook, + method: req.method, + headers: req.headers + }, res2 => { + res.setHeader('Content-Type', 'application/json'); + res2.pipe(res); + })) + ); + req.on('close', () => listener.dispose()); + return true; + } + return false; + } + if (devMode && pathname.startsWith('/_supervisor')) { + const [host, port] = supervisorAddr.split(':'); + req.pipe(http.request({ + host, + port, + method: req.method, + path: pathname + }, res2 => res2.pipe(res))); + return true; + } + return false; +} diff --git a/src/vs/gitpod/node/gitpodInsightsAppender.ts b/src/vs/gitpod/node/gitpodInsightsAppender.ts new file mode 100644 index 0000000000000..db691b99fc3d3 --- /dev/null +++ b/src/vs/gitpod/node/gitpodInsightsAppender.ts @@ -0,0 +1,296 @@ +/* eslint-disable local/code-import-patterns */ +/* eslint-disable header/header */ +/*--------------------------------------------------------------------------------------------- + * Copyright (c) Gitpod. All rights reserved. + *--------------------------------------------------------------------------------------------*/ + +import { GitpodClient, GitpodServer, GitpodServiceImpl } from '@gitpod/gitpod-protocol/lib/gitpod-service'; +import { JsonRpcProxyFactory } from '@gitpod/gitpod-protocol/lib/messaging/proxy-factory'; +import { onUnexpectedError } from 'vs/base/common/errors'; +import { ITelemetryAppender, validateTelemetryData } from 'vs/platform/telemetry/common/telemetryUtils'; +import { GetTokenRequest } from '@gitpod/supervisor-api-grpc/lib/token_pb'; +import { StatusServiceClient } from '@gitpod/supervisor-api-grpc/lib/status_grpc_pb'; +import { InfoServiceClient } from '@gitpod/supervisor-api-grpc/lib/info_grpc_pb'; +import { TokenServiceClient } from '@gitpod/supervisor-api-grpc/lib/token_grpc_pb'; +import { WorkspaceInfoRequest, WorkspaceInfoResponse } from '@gitpod/supervisor-api-grpc/lib/info_pb'; +import * as ReconnectingWebSocket from 'reconnecting-websocket'; +import * as WebSocket from 'ws'; +import { ConsoleLogger, listen as doListen } from 'vscode-ws-jsonrpc'; +import * as grpc from '@grpc/grpc-js'; +import * as util from 'util'; +import { filter, mixin } from 'vs/base/common/objects'; +import { mapMetrics, mapTelemetryData } from 'vs/gitpod/common/insightsHelper'; +import { MetricsServiceClient, sendMetrics, ReportErrorRequest } from '@gitpod/ide-metrics-api-grpcweb'; +import { IGitpodPreviewConfiguration } from 'vs/base/common/product'; +import { NodeHttpTransport } from '@improbable-eng/grpc-web-node-http-transport'; +import type { ErrorEvent } from 'vs/platform/telemetry/common/errorTelemetry'; + +class SupervisorConnection { + readonly deadlines = { + long: 30 * 1000, + normal: 15 * 1000, + short: 5 * 1000 + }; + private readonly addr = process.env.SUPERVISOR_ADDR || 'localhost:22999'; + readonly metadata = new grpc.Metadata(); + readonly status: StatusServiceClient; + readonly token: TokenServiceClient; + readonly info: InfoServiceClient; + + constructor() { + this.status = new StatusServiceClient(this.addr, grpc.credentials.createInsecure()); + this.token = new TokenServiceClient(this.addr, grpc.credentials.createInsecure()); + this.info = new InfoServiceClient(this.addr, grpc.credentials.createInsecure()); + } +} + +type GitpodConnection = Omit, 'server'> & { + server: Pick; +}; + +export class GitpodInsightsAppender implements ITelemetryAppender { + + private _asyncAIClient: Promise | null; + private _defaultData: { [key: string]: any } = Object.create(null); + private _baseProperties: { appName: string; uiKind: 'web'; version: string }; + private readonly supervisor = new SupervisorConnection(); + private readonly devMode = this.productName.endsWith(' Dev'); + private gitpodUserId: string | undefined; + private galleryHost: string | undefined; + + constructor( + private productName: string, + private productVersion: string, + private readonly gitpodPreview?: IGitpodPreviewConfiguration, + readonly galleryServiceUrl?: string + ) { + this._asyncAIClient = null; + this._baseProperties = { + appName: productName, + uiKind: 'web', + version: productVersion, + }; + this.galleryHost = galleryServiceUrl ? new URL(galleryServiceUrl).host : undefined; + + this._withAIClient(async (client) => { + this.gitpodUserId = (await client.getLoggedInUser()).id; + }); + } + + private _withAIClient(callback: (aiClient: Pick) => void): void { + if (!this._asyncAIClient) { + this._asyncAIClient = this.getSupervisorData().then( + (supervisorData) => { + this._defaultData['workspaceId'] = supervisorData.workspaceId; + this._defaultData['instanceId'] = supervisorData.instanceId; + // TODO for backward compatibility with reports, we use instanceId in other places + this._defaultData['workspaceInstanceId'] = supervisorData.instanceId; + + return this.getClient(this.productName, this.productVersion, supervisorData.serverToken, supervisorData.gitpodHost, supervisorData.gitpodApiEndpoint); + } + ); + } + + this._asyncAIClient.then( + (aiClient) => { + callback(aiClient.server); + }, + (err) => { + onUnexpectedError(err); + console.error(err); + } + ); + } + + log(eventName: string, data?: any): void { + this.sendAnalytics(data, eventName); + this.sendMetrics(data, eventName); + if (eventName === 'UnhandledError') { + if (data.fromBrowser) { + return; + } + this.sendErrorReport(data as ErrorEvent); + } + } + + private async sendAnalytics(data: any, eventName: string): Promise { + try { + if (this.devMode) { + if (this.gitpodPreview?.log?.analytics) { + const mappedEvent = mapTelemetryData('remote-server', eventName, data); + if (mappedEvent) { + console.log('Gitpod Analytics: ', JSON.stringify(mappedEvent, undefined, 2)); + } + } + } else { + this._withAIClient((aiClient) => { + data = mixin(data, this._defaultData); + data = validateTelemetryData(data); + const mappedEvent = mapTelemetryData('remote-server', eventName, data.properties); + if (mappedEvent) { + mappedEvent.properties = filter(mappedEvent.properties, (_, v) => v !== undefined && v !== null); + mappedEvent.properties = { + ...mappedEvent.properties, + ...this._baseProperties, + }; + aiClient.trackEvent(mappedEvent); + } + }); + } + } catch (e) { + console.error('failed to send IDE analytics:', e); + } + } + + private async sendMetrics(data: any, eventName: string): Promise { + try { + const metrics = mapMetrics('remote-server', eventName, data, { galleryHost: this.galleryHost }); + if (!metrics || !metrics.length) { + return; + } + if (this.devMode && this.gitpodPreview?.log?.metrics) { + console.log('Gitpod Metrics: ', JSON.stringify(metrics, undefined, 2)); + } + const client = await this.getMetricsClient(); + if (client) { + await sendMetrics(client, metrics); + } + } catch (e) { + console.error('failed to send IDE metric:', e); + } + } + + private async sendErrorReport(error: ErrorEvent): Promise { + const req = new ReportErrorRequest(); + req.setWorkspaceId(this._defaultData['workspaceId']); + req.setInstanceId(this._defaultData['instanceId']); + req.setErrorStack(error.callstack); + if (this.gitpodUserId) { + req.setUserId(this.gitpodUserId); + } + req.setComponent('vscode-server'); + req.setVersion(this.productVersion); + req.getPropertiesMap().set('error_name', error.uncaught_error_name || ''); + req.getPropertiesMap().set('error_message', error.msg || ''); + req.getPropertiesMap().set('appName', this._baseProperties.appName); + req.getPropertiesMap().set('uiKind', this._baseProperties.uiKind); + req.getPropertiesMap().set('version', this._baseProperties.version); + + if (this.devMode) { + console.log('Gitpod Error Reports: ', JSON.stringify(req.toObject(), null, 2)); + } + const client = await this.getMetricsClient(); + if (client) { + client.reportError(req, (e) => { + if (e) { + console.error('failed to send IDE error report:', e); + } + }); + } + } + + flush(): Promise { + return Promise.resolve(undefined); + } + + private _metricsClient: Promise | undefined; + private getMetricsClient(): Promise { + if (this._metricsClient) { + return this._metricsClient; + } + return this._metricsClient = (async () => { + let gitpodHost: string | undefined; + if (!this.devMode) { + const info = await this.getWorkspaceInfo(); + gitpodHost = new URL(info.getGitpodHost()).host; + } else if (this.gitpodPreview) { + gitpodHost = this.gitpodPreview.host; + } + if (!gitpodHost) { + return undefined; + } + const ideMetricsEndpoint = 'https://ide.' + gitpodHost + '/metrics-api'; + return new MetricsServiceClient(ideMetricsEndpoint, { + transport: NodeHttpTransport(), + }); + })(); + } + + private _workspaceInfo: Promise | undefined; + private getWorkspaceInfo(): Promise { + if (!this._workspaceInfo) { + this._workspaceInfo = util.promisify(this.supervisor.info.workspaceInfo.bind(this.supervisor.info, new WorkspaceInfoRequest(), this.supervisor.metadata, { + deadline: Date.now() + this.supervisor.deadlines.long + }))(); + } + return this._workspaceInfo; + } + + private async getSupervisorData() { + const workspaceInfo = await this.getWorkspaceInfo(); + + const gitpodApi = workspaceInfo.getGitpodApi()!; + const gitpodApiHost = gitpodApi.getHost(); + const gitpodApiEndpoint = gitpodApi.getEndpoint(); + const gitpodHost = workspaceInfo.getGitpodHost(); + const workspaceId = workspaceInfo.getWorkspaceId(); + const instanceId = workspaceInfo.getInstanceId(); + + const getTokenRequest = new GetTokenRequest(); + getTokenRequest.setKind('gitpod'); + getTokenRequest.setHost(gitpodApiHost); + getTokenRequest.addScope('function:trackEvent'); + getTokenRequest.addScope('function:getLoggedInUser'); + + + const supervisor = this.supervisor; + const getTokenResponse = await util.promisify(supervisor.token.getToken.bind(supervisor.token, getTokenRequest, supervisor.metadata, { + deadline: Date.now() + supervisor.deadlines.long + }))(); + const serverToken = getTokenResponse.getToken(); + + return { + serverToken, + gitpodHost, + gitpodApiEndpoint, + workspaceId, + instanceId, + }; + } + + // TODO(ak) publish to Segment directly to production/staging untrusted instead, use server api only to resolve a user + private async getClient(productName: string, productVersion: string, serverToken: string, gitpodHost: string, gitpodApiEndpoint: string): Promise { + const factory = new JsonRpcProxyFactory(); + const gitpodService = new GitpodServiceImpl(factory.createProxy()) as GitpodConnection; + + const webSocket = new (ReconnectingWebSocket as any)(gitpodApiEndpoint, undefined, { + maxReconnectionDelay: 10000, + minReconnectionDelay: 1000, + reconnectionDelayGrowFactor: 1.3, + connectionTimeout: 10000, + maxRetries: Infinity, + debug: false, + startClosed: false, + WebSocket: class extends WebSocket { + constructor(address: string, protocols?: string | string[]) { + super(address, protocols, { + headers: { + 'Origin': new URL(gitpodHost).origin, + 'Authorization': `Bearer ${serverToken}`, + 'User-Agent': productName, + 'X-Client-Version': productVersion, + } + }); + } + } + }); + webSocket.onerror = console.error; + doListen({ + webSocket: webSocket as any, + onConnection: connection => factory.listen(connection), + logger: new ConsoleLogger() + }); + + return gitpodService; + } +} diff --git a/src/vs/platform/remote/browser/browserSocketFactory.ts b/src/vs/platform/remote/browser/browserSocketFactory.ts index 0998b898ea94e..06a56ff263a04 100644 --- a/src/vs/platform/remote/browser/browserSocketFactory.ts +++ b/src/vs/platform/remote/browser/browserSocketFactory.ts @@ -198,7 +198,7 @@ class BrowserWebSocket extends Disposable implements IWebSocket { } } -const defaultWebSocketFactory = new class implements IWebSocketFactory { +export const defaultWebSocketFactory = new class implements IWebSocketFactory { create(url: string, debugLabel: string): IWebSocket { return new BrowserWebSocket(url, debugLabel); } @@ -282,6 +282,3 @@ export class BrowserSocketFactory implements ISocketFactory { }); } } - - - diff --git a/src/vs/platform/request/node/proxy.ts b/src/vs/platform/request/node/proxy.ts index ee2aae8814b9b..c71c6f9f1bc11 100644 --- a/src/vs/platform/request/node/proxy.ts +++ b/src/vs/platform/request/node/proxy.ts @@ -18,6 +18,34 @@ function getSystemProxyURI(requestURL: Url, env: typeof process.env): string | n return null; } +function applySystemNoProxyRules(requestURL: Url, proxyURl: string | null, env: typeof process.env): string | null { + const noProxy = env.NO_PROXY || env.no_proxy || null; + if (!noProxy) { + return proxyURl; + } + + const rules = noProxy.split(/[\s,]+/); + if (rules[0] === '*') { + return null; + } + + for (const rule of rules) { + const ruleMatch = rule.match(/^(.+?)(?::(\d+))?$/); + if (!ruleMatch || !ruleMatch[1]) { + continue; + } + + const ruleHost = ruleMatch[1].replace(/^\.*/, '.'); + const rulePort = ruleMatch[2]; + const requestURLHost = requestURL.hostname!.replace(/^\.*/, '.'); + if (requestURLHost.endsWith(ruleHost) && (!rulePort || requestURL.port && requestURL.port === rulePort)) { + return null; + } + } + + return proxyURl; +} + export interface IOptions { proxyUrl?: string; strictSSL?: boolean; @@ -25,7 +53,7 @@ export interface IOptions { export async function getProxyAgent(rawRequestURL: string, env: typeof process.env, options: IOptions = {}): Promise { const requestURL = parseUrl(rawRequestURL); - const proxyURL = options.proxyUrl || getSystemProxyURI(requestURL, env); + const proxyURL = options.proxyUrl || applySystemNoProxyRules(requestURL, getSystemProxyURI(requestURL, env), env); if (!proxyURL) { return null; diff --git a/src/vs/platform/request/node/requestService.ts b/src/vs/platform/request/node/requestService.ts index fd5683282552d..868c43bf0c4fb 100644 --- a/src/vs/platform/request/node/requestService.ts +++ b/src/vs/platform/request/node/requestService.ts @@ -80,9 +80,7 @@ export class RequestService extends Disposable implements IRequestService { ...process.env, ...shellEnv }; - const agent = options.agent ? options.agent : await getProxyAgent(options.url || '', env, { proxyUrl, strictSSL }); - options.agent = agent; options.strictSSL = strictSSL; if (this.authorization) { @@ -93,7 +91,7 @@ export class RequestService extends Disposable implements IRequestService { } try { - const res = await this._request(options, token); + const res = await this._request(options, proxyUrl, env, token); this.logService.trace('RequestService#request (node) - success', options.url); @@ -111,7 +109,7 @@ export class RequestService extends Disposable implements IRequestService { return module.request; } - private _request(options: NodeRequestOptions, token: CancellationToken): Promise { + private _request(options: NodeRequestOptions, proxyUrl: string | undefined, env: { [key: string]: string | undefined }, token: CancellationToken): Promise { return Promises.withAsyncBody(async (c, e) => { @@ -120,6 +118,8 @@ export class RequestService extends Disposable implements IRequestService { ? options.getRawRequest(options) : await this.getNodeRequest(options); + const proxyAgent = options.agent ? options.agent : await getProxyAgent(options.url || '', env, { proxyUrl, strictSSL: options.strictSSL }); + const opts: https.RequestOptions = { hostname: endpoint.hostname, port: endpoint.port ? parseInt(endpoint.port) : (endpoint.protocol === 'https:' ? 443 : 80), @@ -127,7 +127,7 @@ export class RequestService extends Disposable implements IRequestService { path: endpoint.path, method: options.type || 'GET', headers: options.headers, - agent: options.agent, + agent: proxyAgent, rejectUnauthorized: isBoolean(options.strictSSL) ? options.strictSSL : true }; @@ -142,7 +142,7 @@ export class RequestService extends Disposable implements IRequestService { ...options, url: res.headers['location'], followRedirects: followRedirects - 1 - }, token).then(c, e); + }, proxyUrl, env, token).then(c, e); } else { let stream: streams.ReadableStreamEvents = res; diff --git a/src/vs/platform/telemetry/common/telemetryService.ts b/src/vs/platform/telemetry/common/telemetryService.ts index a691ab5da308d..c54ee9534f9eb 100644 --- a/src/vs/platform/telemetry/common/telemetryService.ts +++ b/src/vs/platform/telemetry/common/telemetryService.ts @@ -207,7 +207,8 @@ Registry.as(Extensions.Configuration).registerConfigurat 'default': TelemetryConfiguration.ON, 'restricted': true, 'scope': ConfigurationScope.APPLICATION, - 'tags': ['usesOnlineServices', 'telemetry'] + 'tags': ['usesOnlineServices', 'telemetry'], + 'included': false } } }); @@ -229,8 +230,8 @@ Registry.as(Extensions.Configuration).registerConfigurat 'restricted': true, 'markdownDeprecationMessage': localize('enableTelemetryDeprecated', "If this setting is false, no telemetry will be sent regardless of the new setting's value. Deprecated in favor of the {0} setting.", `\`#${TELEMETRY_SETTING_ID}#\``), 'scope': ConfigurationScope.APPLICATION, - 'tags': ['usesOnlineServices', 'telemetry'] + 'tags': ['usesOnlineServices', 'telemetry'], + 'included': false } } }); - diff --git a/src/vs/platform/tunnel/common/tunnel.ts b/src/vs/platform/tunnel/common/tunnel.ts index 9df9d59f1accb..1daf1f79a413a 100644 --- a/src/vs/platform/tunnel/common/tunnel.ts +++ b/src/vs/platform/tunnel/common/tunnel.ts @@ -139,14 +139,19 @@ export function extractLocalHostUriMetaDataForPortMapping(uri: URI): { address: if (uri.scheme !== 'http' && uri.scheme !== 'https') { return undefined; } - const localhostMatch = /^(localhost|127\.0\.0\.1|0\.0\.0\.0):(\d+)$/.exec(uri.authority); + const localhostMatch = /^(localhost|127(?:\.[0-9]+){0,2}\.[0-9]+|0+(?:\.0+){0,2}\.0+|\[(?:0*\:)*?:?0*1?\])(?::(\d+))?$/.exec(uri.authority); if (!localhostMatch) { return undefined; } - return { - address: localhostMatch[1], - port: +localhostMatch[2], - }; + let address = localhostMatch[1]; + if (address.startsWith('[') && address.endsWith(']')) { + address = address.substr(1, address.length - 2); + } + let port = +localhostMatch[2]; + if (Number.isNaN(port)) { + port = uri.scheme === 'http' ? 80 : 443; + } + return { address, port }; } export const LOCALHOST_ADDRESSES = ['localhost', '127.0.0.1', '0:0:0:0:0:0:0:1', '::1']; diff --git a/src/vs/platform/userDataSync/common/userDataSyncMachines.ts b/src/vs/platform/userDataSync/common/userDataSyncMachines.ts index 6460ed5792d37..f37d9ce7ef930 100644 --- a/src/vs/platform/userDataSync/common/userDataSyncMachines.ts +++ b/src/vs/platform/userDataSync/common/userDataSyncMachines.ts @@ -14,7 +14,7 @@ import { createDecorator } from 'vs/platform/instantiation/common/instantiation' import { IProductService } from 'vs/platform/product/common/productService'; import { getServiceMachineId } from 'vs/platform/externalServices/common/serviceMachineId'; import { IStorageService, StorageScope, StorageTarget } from 'vs/platform/storage/common/storage'; -import { IUserData, IUserDataManifest, IUserDataSyncLogService, IUserDataSyncStoreService } from 'vs/platform/userDataSync/common/userDataSync'; +import { IUserData, IUserDataManifest, IUserDataSyncLogService, IUserDataSyncStoreService, UserDataSyncErrorCode, UserDataSyncStoreError } from 'vs/platform/userDataSync/common/userDataSync'; interface IMachineData { id: string; @@ -110,7 +110,20 @@ export class UserDataSyncMachinesService extends Disposable implements IUserData const machineData = await this.readMachinesData(manifest); if (!machineData.machines.some(({ id }) => id === currentMachineId)) { machineData.machines.push({ id: currentMachineId, name: this.computeCurrentMachineName(machineData.machines), platform: getPlatformName() }); - await this.writeMachinesData(machineData); + let tryWrite = true; + while (tryWrite) { + try { + await this.writeMachinesData(machineData); + tryWrite = false; + } catch (e) { + // we hit max payload for machines: https://github.com/gitpod-io/gitpod/issues/3277 + // try to remove oldest machine and write again + tryWrite = e instanceof UserDataSyncStoreError && e.code === UserDataSyncErrorCode.TooLarge && !!machineData.machines.shift(); + if (!tryWrite) { + throw e; + } + } + } } } diff --git a/src/vs/server/node/extensionHostConnection.ts b/src/vs/server/node/extensionHostConnection.ts index 00e2e2a1305b4..a7fea6bf87b2c 100644 --- a/src/vs/server/node/extensionHostConnection.ts +++ b/src/vs/server/node/extensionHostConnection.ts @@ -44,7 +44,10 @@ export async function buildUserEnvironment(startParamsEnv: { [key: string]: stri VSCODE_HANDLES_UNCAUGHT_ERRORS: 'true', VSCODE_NLS_CONFIG: JSON.stringify(nlsConfig, undefined, 0) }, - ...startParamsEnv + ...startParamsEnv, + ...{ + GITPOD_CODE_HOST: environmentService.isBuilt ? processEnv['GITPOD_HOST'] : undefined + } }; const binFolder = environmentService.isBuilt ? join(environmentService.appRoot, 'bin') : join(environmentService.appRoot, 'resources', 'server', 'bin-dev'); diff --git a/src/vs/server/node/remoteAgentEnvironmentImpl.ts b/src/vs/server/node/remoteAgentEnvironmentImpl.ts index f16217b0125bc..6c1920fa9d275 100644 --- a/src/vs/server/node/remoteAgentEnvironmentImpl.ts +++ b/src/vs/server/node/remoteAgentEnvironmentImpl.ts @@ -66,6 +66,7 @@ export class RemoteAgentEnvironmentChannel implements IServerChannel { _logService.error(error); }); } + } async call(_: any, command: string, arg?: any): Promise { @@ -332,7 +333,14 @@ export class RemoteAgentEnvironmentChannel implements IServerChannel { private async _scanBuiltinExtensions(language: string): Promise { const scannedExtensions = await this._extensionsScannerService.scanSystemExtensions({ language, useCache: true }); - return scannedExtensions.map(e => toExtensionDescription(e, false)); + return scannedExtensions.map(e => toExtensionDescription(e, false)).filter(ext => { + // TODO: remove this once whe decoupled gitpod extensions from gp-code + const ignoreExtensions = [ + 'gitpod.gitpod-shared', + 'gitpod.gitpod-remote-ssh' + ]; + return !ignoreExtensions.includes(ext.identifier.value.toLowerCase()); + }); } private async _scanInstalledExtensions(language: string): Promise { diff --git a/src/vs/server/node/remoteExtensionHostAgentServer.ts b/src/vs/server/node/remoteExtensionHostAgentServer.ts index 31c0864d5fbdf..14700063e97ea 100644 --- a/src/vs/server/node/remoteExtensionHostAgentServer.ts +++ b/src/vs/server/node/remoteExtensionHostAgentServer.ts @@ -38,6 +38,8 @@ import { determineServerConnectionToken, requestHasValidConnectionToken as httpR import { IServerEnvironmentService, ServerParsedArgs } from 'vs/server/node/serverEnvironmentService'; import { setupServerServices, SocketServer } from 'vs/server/node/serverServices'; import { CacheControl, serveError, serveFile, WebClientServer } from 'vs/server/node/webClientServer'; +// eslint-disable-next-line code-import-patterns +import { handleGitpodCLIRequest } from 'vs/gitpod/node/customServerIntegration'; const SHUTDOWN_TIMEOUT = 5 * 60 * 1000; @@ -93,9 +95,9 @@ class RemoteExtensionHostAgentServer extends Disposable implements IServerAPI { public async handleRequest(req: http.IncomingMessage, res: http.ServerResponse): Promise { // Only serve GET requests - if (req.method !== 'GET') { - return serveError(req, res, 405, `Unsupported method ${req.method}`); - } + // if (req.method !== 'GET') { + // return serveError(req, res, 405, `Unsupported method ${req.method}`); + // } if (!req.url) { return serveError(req, res, 400, `Bad request.`); @@ -108,6 +110,10 @@ class RemoteExtensionHostAgentServer extends Disposable implements IServerAPI { return serveError(req, res, 400, `Bad request.`); } + if (handleGitpodCLIRequest(pathname, req, res)) { + return; + } + // for now accept all paths, with or without server root path if (pathname.startsWith(this._serverRootPath) && pathname.charCodeAt(this._serverRootPath.length) === CharCode.Slash) { pathname = pathname.substring(this._serverRootPath.length); @@ -735,7 +741,7 @@ export async function createServer(address: string | net.AddressInfo | null, arg return null; }); - const hasWebClient = fs.existsSync(FileAccess.asFileUri('vs/code/browser/workbench/workbench.html', require).fsPath); + const hasWebClient = fs.existsSync(FileAccess.asFileUri('vs/gitpod/browser/workbench/workbench.html', require).fsPath); if (hasWebClient && address && typeof address !== 'string') { // ships the web ui! diff --git a/src/vs/server/node/server.cli.ts b/src/vs/server/node/server.cli.ts index 138f6dd81304c..337b385cabc49 100644 --- a/src/vs/server/node/server.cli.ts +++ b/src/vs/server/node/server.cli.ts @@ -32,7 +32,7 @@ interface ProductDescription { executableName: string; } -interface RemoteParsedArgs extends NativeParsedArgs { 'gitCredential'?: string; 'openExternal'?: boolean } +interface RemoteParsedArgs extends NativeParsedArgs { 'gitCredential'?: string; 'openExternal'?: boolean; 'preview'?: string } const isSupportedForCmd = (optionId: keyof RemoteParsedArgs) => { @@ -85,16 +85,17 @@ const cliCommand = process.env['VSCODE_CLIENT_COMMAND'] as string; const cliCommandCwd = process.env['VSCODE_CLIENT_COMMAND_CWD'] as string; const cliRemoteAuthority = process.env['VSCODE_CLI_AUTHORITY'] as string; const cliStdInFilePath = process.env['VSCODE_STDIN_FILE_PATH'] as string; +const cliPort = !!process.env['VSCODE_DEV'] ? 9888 /* From code-web.js */ : (process.env['GITPOD_THEIA_PORT'] ? Number(process.env['GITPOD_THEIA_PORT']) : undefined); export async function main(desc: ProductDescription, args: string[]): Promise { - if (!cliPipe && !cliCommand) { - console.log('Command is only available in WSL or inside a Visual Studio Code terminal.'); + if (!cliPort && !cliCommand) { + console.log('Command is only available inside a Gitpod Code terminal.'); return; } // take the local options and remove the ones that don't apply - const options: OptionDescriptions> = { ...OPTIONS, gitCredential: { type: 'string' }, openExternal: { type: 'boolean' } }; + const options: OptionDescriptions> = { ...OPTIONS, gitCredential: { type: 'string' }, openExternal: { type: 'boolean' }, preview: { type: 'string' } }; const isSupported = cliCommand ? isSupportedForCmd : isSupportedForPipe; for (const optionId in OPTIONS) { const optId = optionId; @@ -103,8 +104,9 @@ export async function main(desc: ProductDescription, args: string[]): Promise { console.log(res); @@ -286,7 +292,7 @@ export async function main(desc: ProductDescription, args: string[]): Promise { @@ -360,6 +366,52 @@ function openInBrowser(args: string[], verbose: boolean) { } } +function openInBuiltInSimpleBrowser(url: string, verbose: boolean) { + sendToPort({ + type: 'preview', + url + }, verbose); +} + +function sendToPort(args: PipeCommand | { type: 'preview'; url: string }, verbose: boolean): Promise { + if (verbose) { + console.log(JSON.stringify(args, null, ' ')); + } + return new Promise(resolve => { + const message = JSON.stringify(args); + if (!cliPort) { + console.log('Message ' + message); + resolve(''); + return; + } + + const opts: _http.RequestOptions = { + hostname: 'localhost', + port: cliPort, + protocol: 'http:', + path: '/cli', + method: 'POST' + }; + + const req = _http.request(opts, res => { + const chunks: string[] = []; + res.setEncoding('utf8'); + res.on('data', chunk => { + chunks.push(chunk); + }); + res.on('error', (err) => fatal('Error in response.', err)); + res.on('end', () => { + resolve(chunks.join('')); + }); + }); + + req.on('error', (err) => fatal('Error in request.', err)); + req.write(message); + req.end(); + }); +} + +// @ts-ignore function sendToPipe(args: PipeCommand, verbose: boolean): Promise { if (verbose) { console.log(JSON.stringify(args, null, ' ')); diff --git a/src/vs/server/node/serverServices.ts b/src/vs/server/node/serverServices.ts index 9d94002e098de..8cdc71fc93ed4 100644 --- a/src/vs/server/node/serverServices.ts +++ b/src/vs/server/node/serverServices.ts @@ -73,6 +73,8 @@ import { ExtensionsProfileScannerService, IExtensionsProfileScannerService } fro import { IUserDataProfilesService, UserDataProfilesService } from 'vs/platform/userDataProfile/common/userDataProfile'; import { NullPolicyService } from 'vs/platform/policy/common/policy'; import { OneDataSystemAppender } from 'vs/platform/telemetry/node/1dsAppender'; +// eslint-disable-next-line local/code-import-patterns +import { GitpodInsightsAppender } from 'vs/gitpod/node/gitpodInsightsAppender'; import { LoggerService } from 'vs/platform/log/node/loggerService'; import { URI } from 'vs/base/common/uri'; import { BufferLogService } from 'vs/platform/log/common/bufferLog'; @@ -139,11 +141,13 @@ export async function setupServerServices(connectionToken: ServerConnectionToken const machineId = await getMachineId(); const isInternal = isInternalTelemetry(productService, configurationService); if (supportsTelemetry(productService, environmentService)) { - if (productService.aiConfig && productService.aiConfig.ariaKey) { + if (productService.aiConfig && productService.aiConfig.ariaKey !== 'foo') { oneDsAppender = new OneDataSystemAppender(isInternal, eventPrefix, null, productService.aiConfig.ariaKey); disposables.add(toDisposable(() => oneDsAppender?.flush())); // Ensure the AI appender is disposed so that it flushes remaining data } + oneDsAppender = new GitpodInsightsAppender(productService.nameShort, productService.version, productService.gitpodPreview, productService.extensionsGallery?.serviceUrl); + const config: ITelemetryServiceConfig = { appenders: [oneDsAppender], commonProperties: resolveCommonProperties(fileService, release(), hostname(), process.arch, productService.commit, productService.version + '-remote', machineId, isInternal, environmentService.installSourcePath, 'remoteAgent'), diff --git a/src/vs/server/node/webClientServer.ts b/src/vs/server/node/webClientServer.ts index 35cdf1e95905e..74a87b6a84e33 100644 --- a/src/vs/server/node/webClientServer.ts +++ b/src/vs/server/node/webClientServer.ts @@ -290,7 +290,7 @@ export class WebClientServer { const resolveWorkspaceURI = (defaultLocation?: string) => defaultLocation && URI.file(path.resolve(defaultLocation)).with({ scheme: Schemas.vscodeRemote, authority: remoteAuthority }); - const filePath = FileAccess.asFileUri(this._environmentService.isBuilt ? 'vs/code/browser/workbench/workbench.html' : 'vs/code/browser/workbench/workbench-dev.html', require).fsPath; + const filePath = FileAccess.asFileUri(this._environmentService.isBuilt ? 'vs/gitpod/browser/workbench/workbench.html' : 'vs/gitpod/browser/workbench/workbench-dev.html', require).fsPath; const authSessionInfo = !this._environmentService.isBuilt && this._environmentService.args['github-auth'] ? { id: generateUuid(), providerId: 'github', @@ -323,6 +323,8 @@ export class WebClientServer { const nlsBaseUrl = this._productService.extensionsGallery?.nlsBaseUrl; const values: { [key: string]: string } = { + VERSION: this._productService.version, + GITPOD_HOST: this._productService.gitpodPreview?.host || '', WORKBENCH_WEB_CONFIGURATION: asJSON(workbenchWebConfiguration), WORKBENCH_AUTH_SESSION: authSessionInfo ? asJSON(authSessionInfo) : '', WORKBENCH_WEB_BASE_URL: this._staticRoute, @@ -345,7 +347,7 @@ export class WebClientServer { 'media-src \'self\';', `script-src 'self' 'unsafe-eval' ${this._getScriptCspHashes(data).join(' ')} 'sha256-fh3TwPMflhsEIpR8g1OYTIMVWhXTLcjQ9kh2tIpmv54=' http://${remoteAuthority};`, // the sha is the same as in src/vs/workbench/services/extensions/worker/webWorkerExtensionHostIframe.html 'child-src \'self\';', - `frame-src 'self' https://*.vscode-cdn.net data:;`, + `frame-src 'self' https://*.vscode-cdn.net https://*.gitpod.io https://*.gitpod-dev.com https://*.gitpod-staging.com data:;`, 'worker-src \'self\' data:;', 'style-src \'self\' \'unsafe-inline\';', 'connect-src \'self\' ws: wss: https:;', @@ -353,9 +355,11 @@ export class WebClientServer { 'manifest-src \'self\';' ].join(' '); + const allowAllCSP = `default-src * 'unsafe-inline' 'unsafe-eval'; script-src * 'unsafe-inline' 'unsafe-eval'; connect-src * 'unsafe-inline'; img-src * data: blob: 'unsafe-inline'; frame-src *; style-src * 'unsafe-inline';`; + const headers: http.OutgoingHttpHeaders = { 'Content-Type': 'text/html', - 'Content-Security-Policy': cspDirectives + 'Content-Security-Policy': this._environmentService.isBuilt ? cspDirectives : allowAllCSP }; if (this._connectionToken.type !== ServerConnectionTokenType.None) { // At this point we know the client has a valid cookie diff --git a/src/vs/workbench/api/common/extHost.api.impl.ts b/src/vs/workbench/api/common/extHost.api.impl.ts index 67cd07bbe3917..1a7953205836c 100644 --- a/src/vs/workbench/api/common/extHost.api.impl.ts +++ b/src/vs/workbench/api/common/extHost.api.impl.ts @@ -315,7 +315,10 @@ export function createApiFactoryAndRegisterActors(accessor: ServicesAccessor): I return extHostTerminalService.onDidChangeShell; }, get isTelemetryEnabled() { - return extHostTelemetry.getTelemetryConfiguration(); + // return extHostTelemetry.getTelemetryConfiguration(); + // return always false to prevent microsoft built-in extension and + // third-party like GHPR to send telemetry data + return false; }, get onDidChangeTelemetryEnabled(): Event { return extHostTelemetry.onDidChangeTelemetryEnabled; diff --git a/src/vs/workbench/browser/media/code-icon.svg b/src/vs/workbench/browser/media/code-icon.svg index cc61f81ea5a24..5ee204487c08c 100644 --- a/src/vs/workbench/browser/media/code-icon.svg +++ b/src/vs/workbench/browser/media/code-icon.svg @@ -1 +1,6 @@ - \ No newline at end of file + + + + diff --git a/src/vs/workbench/browser/media/style.css b/src/vs/workbench/browser/media/style.css index 9eb68309dc3f7..a3867a878dbea 100644 --- a/src/vs/workbench/browser/media/style.css +++ b/src/vs/workbench/browser/media/style.css @@ -239,3 +239,9 @@ body.web { .monaco-workbench a.monaco-link:hover { text-decoration: underline; /* render underline on hover for accessibility requirements */ } + +.codicon-gitpod { + width: 12px; + height: 12px; + background-image: url(./code-icon.svg) !important; +} diff --git a/src/vs/workbench/common/webview.ts b/src/vs/workbench/common/webview.ts index 1db1d451aff9f..5039cc60e34ab 100644 --- a/src/vs/workbench/common/webview.ts +++ b/src/vs/workbench/common/webview.ts @@ -5,6 +5,7 @@ import { CharCode } from 'vs/base/common/charCode'; import { Schemas } from 'vs/base/common/network'; +import { isWeb } from 'vs/base/common/platform'; import { URI } from 'vs/base/common/uri'; export interface WebviewRemoteInfo { @@ -12,13 +13,30 @@ export interface WebviewRemoteInfo { readonly authority: string | undefined; } +// This is required so that webview resources load sucessfully in firefox +// Firefox is more strict regarding CSP rules and it will complain if we left +// the `webviewResourceBaseHost` set to 'vscode-cdn.net' as the service worker +// is served from a different domain in this case `gitpodHost`. +// This change only affects the server part as it uses `process.env`, +// for the front end there are some replace rules in gitpod blobserve config +// that will replace 'vscode-cdn.net' with the proper host value +// See https://github.com/gitpod-io/gitpod/blob/8c7cb822ed5c670c102335f76b269f00895c8876/chart/templates/blobserve-configmap.yaml#L28-L39 +// and https://github.com/gitpod-io/gitpod/blob/8c7cb822ed5c670c102335f76b269f00895c8876/installer/pkg/components/blobserve/configmap.go#L41-L61 +let gitpodHost; +if (!isWeb) { + gitpodHost = process.env['GITPOD_CODE_HOST']; + try { + gitpodHost = gitpodHost && new URL(gitpodHost).host; + } catch { } +} + /** * Root from which resources in webviews are loaded. * * This is hardcoded because we never expect to actually hit it. Instead these requests * should always go to a service worker. */ -export const webviewResourceBaseHost = 'vscode-cdn.net'; +export const webviewResourceBaseHost = gitpodHost || 'vscode-cdn.net'; export const webviewRootResourceAuthority = `vscode-resource.${webviewResourceBaseHost}`; diff --git a/src/vs/workbench/contrib/terminal/browser/terminalInstance.ts b/src/vs/workbench/contrib/terminal/browser/terminalInstance.ts index 3ef386a0319db..1e008fc66c1be 100644 --- a/src/vs/workbench/contrib/terminal/browser/terminalInstance.ts +++ b/src/vs/workbench/contrib/terminal/browser/terminalInstance.ts @@ -1975,11 +1975,8 @@ export class TerminalInstance extends Disposable implements ITerminalInstance { // Extract the file name without extension title = path.win32.parse(title).name; } else { - const firstSpaceIndex = title.indexOf(' '); if (title.startsWith('/')) { title = path.basename(title); - } else if (firstSpaceIndex > -1) { - title = title.substring(0, firstSpaceIndex); } } this._processName = title; diff --git a/src/vs/workbench/contrib/terminal/common/terminalConfiguration.ts b/src/vs/workbench/contrib/terminal/common/terminalConfiguration.ts index 4e62afac4d0f7..5b1e1741c4039 100644 --- a/src/vs/workbench/contrib/terminal/common/terminalConfiguration.ts +++ b/src/vs/workbench/contrib/terminal/common/terminalConfiguration.ts @@ -408,7 +408,7 @@ const terminalConfiguration: IConfigurationNode = { localize('terminal.integrated.environmentChangesIndicator.on', "Enable the indicator."), localize('terminal.integrated.environmentChangesIndicator.warnonly', "Only show the warning indicator when a terminal's environment is 'stale', not the information indicator that shows a terminal has had its environment modified by an extension."), ], - default: 'warnonly' + default: 'off' }, [TerminalSettingId.EnvironmentChangesRelaunch]: { markdownDescription: localize('terminal.integrated.environmentChangesRelaunch', "Whether to relaunch terminals automatically if extension want to contribute to their environment and have not been interacted with yet."), diff --git a/src/vs/workbench/contrib/update/browser/update.contribution.ts b/src/vs/workbench/contrib/update/browser/update.contribution.ts index b978dce39af31..8adb76b8998fa 100644 --- a/src/vs/workbench/contrib/update/browser/update.contribution.ts +++ b/src/vs/workbench/contrib/update/browser/update.contribution.ts @@ -18,10 +18,10 @@ import { isWindows } from 'vs/base/common/platform'; import { IFileDialogService } from 'vs/platform/dialogs/common/dialogs'; import { mnemonicButtonLabel } from 'vs/base/common/labels'; import { ShowCurrentReleaseNotesActionId } from 'vs/workbench/contrib/update/common/update'; -import { IsWebContext } from 'vs/platform/contextkey/common/contextkeys'; import { IOpenerService } from 'vs/platform/opener/common/opener'; import { IProductService } from 'vs/platform/product/common/productService'; import { URI } from 'vs/base/common/uri'; +import { RawContextKey } from 'vs/platform/contextkey/common/contextkey'; const workbench = Registry.as(WorkbenchExtensions.Workbench); @@ -70,7 +70,7 @@ export class ShowCurrentReleaseNotesAction extends Action2 { } } -registerAction2(ShowCurrentReleaseNotesAction); +// registerAction2(ShowCurrentReleaseNotesAction); // Update @@ -140,6 +140,8 @@ class RestartToUpdateAction extends Action2 { } } +const CONTEXT_DONT_SHOW_DOWNLOAD_ACTION = new RawContextKey('doNotShowDownloadAction', false); + class DownloadAction extends Action2 { static readonly ID = 'workbench.action.download'; @@ -152,11 +154,11 @@ class DownloadAction extends Action2 { value: localize('openDownloadPage', "Download {0}", product.nameLong), original: `Download ${product.downloadUrl}` }, - precondition: IsWebContext, // Only show when running in a web browser + precondition: CONTEXT_DONT_SHOW_DOWNLOAD_ACTION, // Only show when running in a web browser f1: true, menu: [{ id: MenuId.StatusBarWindowIndicatorMenu, - when: IsWebContext + when: CONTEXT_DONT_SHOW_DOWNLOAD_ACTION }] }); } diff --git a/src/vs/workbench/contrib/userDataSync/browser/userDataSync.ts b/src/vs/workbench/contrib/userDataSync/browser/userDataSync.ts index 8d573e890756a..4b3ad6b654607 100644 --- a/src/vs/workbench/contrib/userDataSync/browser/userDataSync.ts +++ b/src/vs/workbench/contrib/userDataSync/browser/userDataSync.ts @@ -267,7 +267,7 @@ export class UserDataSyncWorkbenchContribution extends Disposable implements IWo if (error.resource === SyncResource.Keybindings || error.resource === SyncResource.Settings || error.resource === SyncResource.Tasks) { this.disableSync(error.resource); const sourceArea = getSyncAreaLabel(error.resource); - this.handleTooLargeError(error.resource, localize('too large', "Disabled syncing {0} because size of the {1} file to sync is larger than {2}. Please open the file and reduce the size and enable sync", sourceArea.toLowerCase(), sourceArea.toLowerCase(), '100kb'), error); + this.handleTooLargeError(error.resource, localize('too large', "Disabled syncing {0} because size of the {1} file to sync is larger than {2}. Please open the file and reduce the size and enable sync", sourceArea.toLowerCase(), sourceArea.toLowerCase(), '1Mb'), error); } break; case UserDataSyncErrorCode.IncompatibleLocalContent: @@ -454,7 +454,7 @@ export class UserDataSyncWorkbenchContribution extends Disposable implements IWo switch (e.code) { case UserDataSyncErrorCode.TooLarge: if (e.resource === SyncResource.Keybindings || e.resource === SyncResource.Settings || e.resource === SyncResource.Tasks) { - this.handleTooLargeError(e.resource, localize('too large while starting sync', "Settings sync cannot be turned on because size of the {0} file to sync is larger than {1}. Please open the file and reduce the size and turn on sync", getSyncAreaLabel(e.resource).toLowerCase(), '100kb'), e); + this.handleTooLargeError(e.resource, localize('too large while starting sync', "Settings sync cannot be turned on because size of the {0} file to sync is larger than {1}. Please open the file and reduce the size and turn on sync", getSyncAreaLabel(e.resource).toLowerCase(), '1Mb'), e); return; } break; diff --git a/src/vs/workbench/services/actions/common/menusExtensionPoint.ts b/src/vs/workbench/services/actions/common/menusExtensionPoint.ts index 4e2614f007ce0..3da8f83455829 100644 --- a/src/vs/workbench/services/actions/common/menusExtensionPoint.ts +++ b/src/vs/workbench/services/actions/common/menusExtensionPoint.ts @@ -289,6 +289,34 @@ const apiMenus: IAPIMenu[] = [ description: localize('menus.mergeEditorResult', "The result toolbar of the merge editor"), proposed: 'contribMergeEditorMenus' }, + // Gitpod specific + // Hack: + // For now just use 'contribMenuBarHome' to avoid creating a file per extension point. + // Seems safe for now as the file will be empty either way + { + key: 'menuBar/help', + id: MenuId.MenubarHelpMenu, + description: localize('menus.help', "The top level help menu"), + proposed: 'contribMenuBarHome' + }, + { + key: 'menuBar/preferences', + id: MenuId.MenubarPreferencesMenu, + description: localize('menus.preferences', "The preferences menu"), + proposed: 'contribMenuBarHome' + }, + { + key: 'preferences/context', + id: MenuId.MenubarPreferencesMenu, + description: localize('menus.preferencesContext', "The preferences context menu"), + proposed: 'contribMenuBarHome' + }, + { + key: 'accounts/context', + id: MenuId.AccountsContext, + description: localize('menus.accountsContext', "The accounts context menu"), + proposed: 'contribMenuBarHome' + }, ]; namespace schema { diff --git a/src/vs/workbench/services/extensions/common/abstractExtensionService.ts b/src/vs/workbench/services/extensions/common/abstractExtensionService.ts index 053cc45f82200..612842d59f1e3 100644 --- a/src/vs/workbench/services/extensions/common/abstractExtensionService.ts +++ b/src/vs/workbench/services/extensions/common/abstractExtensionService.ts @@ -1482,7 +1482,7 @@ class ProposedApiController { this._envEnabledExtensions = new Set((_environmentService.extensionEnabledProposedApi ?? []).map(id => ExtensionIdentifier.toKey(id))); - this._envEnablesProposedApiForAll = + this._envEnablesProposedApiForAll = true || // always enable proposed API !_environmentService.isBuilt || // always allow proposed API when running out of sources (_environmentService.isExtensionDevelopment && productService.quality !== 'stable') || // do not allow proposed API against stable builds when developing an extension (this._envEnabledExtensions.size === 0 && Array.isArray(_environmentService.extensionEnabledProposedApi)); // always allow proposed API if --enable-proposed-api is provided without extension ID diff --git a/src/vs/workbench/services/telemetry/browser/telemetryService.ts b/src/vs/workbench/services/telemetry/browser/telemetryService.ts index 6d5739aed972e..3e4d353699193 100644 --- a/src/vs/workbench/services/telemetry/browser/telemetryService.ts +++ b/src/vs/workbench/services/telemetry/browser/telemetryService.ts @@ -19,6 +19,11 @@ import { getTelemetryLevel, isInternalTelemetry, ITelemetryAppender, NullTelemet import { IBrowserWorkbenchEnvironmentService } from 'vs/workbench/services/environment/browser/environmentService'; import { IRemoteAgentService } from 'vs/workbench/services/remote/common/remoteAgentService'; import { resolveWorkbenchCommonProperties } from 'vs/workbench/services/telemetry/browser/workbenchCommonProperties'; +// eslint-disable-next-line local/code-import-patterns +import { GitpodInsightsAppender } from 'vs/gitpod/browser/gitpodInsightsAppender'; +import { ErrorEvent } from 'vs/platform/telemetry/common/errorTelemetry'; +// eslint-disable-next-line local/code-import-patterns +import { GitpodErrorEvent } from 'vs/gitpod/common/insightsHelper'; export class TelemetryService extends Disposable implements ITelemetryService { @@ -68,6 +73,8 @@ export class TelemetryService extends Disposable implements ITelemetryService { const telemetryProvider: ITelemetryAppender = remoteAgentService.getConnection() !== null ? { log: remoteAgentService.logTelemetry.bind(remoteAgentService), flush: remoteAgentService.flushTelemetry.bind(remoteAgentService) } : new OneDataSystemWebAppender(isInternal, 'monacoworkbench', null, productService.aiConfig?.ariaKey); appenders.push(telemetryProvider); appenders.push(new TelemetryLogAppender(loggerService, environmentService)); + const gitpodTelemetryProvider: ITelemetryAppender = new GitpodInsightsAppender(productService); + appenders.push(gitpodTelemetryProvider); const config: ITelemetryServiceConfig = { appenders, commonProperties: resolveWorkbenchCommonProperties(storageService, productService.commit, productService.version, isInternal, environmentService.remoteAuthority, productService.embedderIdentifier, productService.removeTelemetryMachineId, environmentService.options && environmentService.options.resolveCommonTelemetryProperties), @@ -96,6 +103,13 @@ export class TelemetryService extends Disposable implements ITelemetryService { } publicLogError(errorEventName: string, data?: ITelemetryData): Promise { + if (errorEventName === 'UnhandledError') { + const errData: GitpodErrorEvent = { + ...(data as ErrorEvent), + fromBrowser: true, + }; + return this.impl.publicLog(errorEventName, errData); + } return this.impl.publicLog(errorEventName, data); } diff --git a/src/vs/workbench/workbench.web.main.ts b/src/vs/workbench/workbench.web.main.ts index 39abd11494106..690231192f109 100644 --- a/src/vs/workbench/workbench.web.main.ts +++ b/src/vs/workbench/workbench.web.main.ts @@ -160,6 +160,10 @@ import 'vs/workbench/contrib/splash/browser/splash.contribution'; // Offline import 'vs/workbench/contrib/offline/browser/offline.contribution'; +// Gitpod: Export Logs command +// eslint-disable-next-line local/code-import-patterns +import 'vs/gitpod/browser/workbench/contrib/exportLogs.contribution'; + //#endregion diff --git a/yarn.lock b/yarn.lock index 5810f7582cbb5..33bb3216dc105 100644 --- a/yarn.lock +++ b/yarn.lock @@ -264,10 +264,10 @@ chalk "^2.0.0" js-tokens "^4.0.0" -"@babel/parser@^7.14.7", "@babel/parser@^7.18.10": - version "7.18.10" - resolved "https://registry.yarnpkg.com/@babel/parser/-/parser-7.18.10.tgz#94b5f8522356e69e8277276adf67ed280c90ecc1" - integrity sha512-TYk3OA0HKL6qNryUayb5UUEhM/rkOQozIBEA5ITXh5DWrSp0TlUQXMyZmnWxG/DizSWBeeQ0Zbc5z8UGaaqoeg== +"@babel/parser@^7.14.7", "@babel/parser@^7.18.10", "@babel/parser@^7.18.11": + version "7.18.11" + resolved "https://registry.yarnpkg.com/@babel/parser/-/parser-7.18.11.tgz#68bb07ab3d380affa9a3f96728df07969645d2d9" + integrity sha512-9JKn5vN+hDt0Hdqn1PiJ2guflwP+B6Ga8qbDuoF0PzzVhrzsKIJo8yGqVk6CmMHiMei9w1C1Bp9IMJSIK+HPIQ== "@babel/template@^7.18.10", "@babel/template@^7.18.6": version "7.18.10" @@ -279,9 +279,9 @@ "@babel/types" "^7.18.10" "@babel/traverse@^7.18.10", "@babel/traverse@^7.18.9": - version "7.18.10" - resolved "https://registry.yarnpkg.com/@babel/traverse/-/traverse-7.18.10.tgz#37ad97d1cb00efa869b91dd5d1950f8a6cf0cb08" - integrity sha512-J7ycxg0/K9XCtLyHf0cz2DqDihonJeIo+z+HEdRe9YuT8TY4A66i+Ab2/xZCEW7Ro60bPCBBfqqboHSamoV3+g== + version "7.18.11" + resolved "https://registry.yarnpkg.com/@babel/traverse/-/traverse-7.18.11.tgz#3d51f2afbd83ecf9912bcbb5c4d94e3d2ddaa16f" + integrity sha512-TG9PiM2R/cWCAy6BPJKeHzNbu4lPzOSZpeMfeNErskGpTJx6trEvFaVCbDvpcxwy49BKWmEPwiW8mrysNiDvIQ== dependencies: "@babel/code-frame" "^7.18.6" "@babel/generator" "^7.18.10" @@ -289,7 +289,7 @@ "@babel/helper-function-name" "^7.18.9" "@babel/helper-hoist-variables" "^7.18.6" "@babel/helper-split-export-declaration" "^7.18.6" - "@babel/parser" "^7.18.10" + "@babel/parser" "^7.18.11" "@babel/types" "^7.18.10" debug "^4.1.0" globals "^11.1.0" @@ -355,6 +355,82 @@ minimatch "^3.0.4" strip-json-comments "^3.1.1" +"@gitpod/gitpod-protocol@main": + version "0.1.5-main.3615" + resolved "https://registry.yarnpkg.com/@gitpod/gitpod-protocol/-/gitpod-protocol-0.1.5-main.3615.tgz#78926871c47cacfc7e5ae22065546d46e600eba7" + integrity sha512-ckNNhsX8cWlTVAY4jekZEqSBrVEh+4C+e0PDrO+glvO1LFRPFr3kmqPrcGzOtjq7Ks3gLiGYhIWnyFo4YCMS6A== + dependencies: + "@types/react" "17.0.32" + ajv "^6.5.4" + analytics-node "^6.0.0" + cookie "^0.4.2" + inversify "^5.1.1" + jaeger-client "^3.18.1" + js-yaml "^3.10.0" + opentracing "^0.14.5" + prom-client "^13.2.0" + random-number-csprng "^1.0.2" + react "17.0.2" + react-dom "17.0.2" + reconnecting-websocket "^4.4.0" + reflect-metadata "^0.1.13" + uuid "^8.3.2" + vscode-jsonrpc "^5.0.1" + vscode-languageserver-protocol "3.17.0" + vscode-languageserver-types "3.17.0" + vscode-uri "^3.0.3" + vscode-ws-jsonrpc "^0.2.0" + ws "^7.4.6" + +"@gitpod/ide-metrics-api-grpcweb@^0.0.1-main.4780": + version "0.0.1-main.4780" + resolved "https://registry.yarnpkg.com/@gitpod/ide-metrics-api-grpcweb/-/ide-metrics-api-grpcweb-0.0.1-main.4780.tgz#2131c4d0634f243c58cc1958d403b640ee08d727" + integrity sha512-OknhowSlzKT6a+PWwdZ/Ot7gOgp60EkIT4yoyQ/0GUJSlEy+d8gDwZObLGMY/KzJC4Qrv8FN4/vLMYShh0jTpA== + dependencies: + "@improbable-eng/grpc-web" "^0.14.0" + google-protobuf "^3.19.1" + +"@gitpod/local-app-api-grpcweb@main": + version "0.1.5-main.943" + resolved "https://registry.yarnpkg.com/@gitpod/local-app-api-grpcweb/-/local-app-api-grpcweb-0.1.5-main.943.tgz#37d08d3d4336f86f816f2c1fc5503a5a4142e1e2" + integrity sha512-NJE0MXA0tZSUm5pl89OU14dm7qmuws65H4ci/VemiMcYQfB2Zh/CYqOwvb1dpp1RFd8zP03gptwWxesNxQzbEw== + dependencies: + "@gitpod/supervisor-api-grpcweb" "0.1.5-main.943" + "@improbable-eng/grpc-web" "0.14.0" + google-protobuf "^3.17.0" + +"@gitpod/supervisor-api-grpc@jp-on-open": + version "0.1.5-jp-on-open.2" + resolved "https://registry.yarnpkg.com/@gitpod/supervisor-api-grpc/-/supervisor-api-grpc-0.1.5-jp-on-open.2.tgz#69dcfee19e9a3e40fab3a1ec6094c6a6fa2c1edc" + integrity sha512-8bmV4ks8h3vBlmUlxCquMahNKgINcpC65sSXOmv8DBdIbrzWblJ1CxgLVtrjn4/adzS9itQygBP2C7VteMFCfA== + dependencies: + "@grpc/grpc-js" "^1.3.7" + google-protobuf "^3.19.1" + +"@gitpod/supervisor-api-grpcweb@0.1.5-main.943": + version "0.1.5-main.943" + resolved "https://registry.yarnpkg.com/@gitpod/supervisor-api-grpcweb/-/supervisor-api-grpcweb-0.1.5-main.943.tgz#be7091e6b0c105f3143ac948784a6f955f8aa450" + integrity sha512-5P9u9FHb1Hn7gh9piyfmyR15JPFWU7FhsrPJ32VVyit8qnYEdkrnYALefTsKqdBIHrKc7bbPO23tIHxpzLO6UQ== + +"@grpc/grpc-js@^1.3.7": + version "1.4.4" + resolved "https://registry.yarnpkg.com/@grpc/grpc-js/-/grpc-js-1.4.4.tgz#59336f13d77bc446bbdf2161564a32639288dc5b" + integrity sha512-a6222b7Dl6fIlMgzVl7e+NiRoLiZFbpcwvBH2Oli56Bn7W4/3Ld+86hK4ffPn5rx2DlDidmIcvIJiOQXyhv9gA== + dependencies: + "@grpc/proto-loader" "^0.6.4" + "@types/node" ">=12.12.47" + +"@grpc/proto-loader@^0.6.4": + version "0.6.7" + resolved "https://registry.yarnpkg.com/@grpc/proto-loader/-/proto-loader-0.6.7.tgz#e62a202f4cf5897bdd0e244dec1dbc80d84bdfa1" + integrity sha512-QzTPIyJxU0u+r2qGe8VMl3j/W2ryhEvBv7hc42OjYfthSj370fUrb7na65rG6w3YLZS/fb8p89iTBobfWGDgdw== + dependencies: + "@types/long" "^4.0.1" + lodash.camelcase "^4.3.0" + long "^4.0.0" + protobufjs "^6.10.0" + yargs "^16.1.1" + "@gulp-sourcemaps/identity-map@^2.0.1": version "2.0.1" resolved "https://registry.yarnpkg.com/@gulp-sourcemaps/identity-map/-/identity-map-2.0.1.tgz#a6e8b1abec8f790ec6be2b8c500e6e68037c0019" @@ -388,6 +464,25 @@ resolved "https://registry.yarnpkg.com/@humanwhocodes/object-schema/-/object-schema-1.2.1.tgz#b520529ec21d8e5945a1851dfd1c32e94e39ff45" integrity sha512-ZnQMnLV4e7hDlUvw8H+U8ASL02SS2Gn6+9Ac3wGGLIe7+je2AeAOxPY+izIPJDfFDb7eDjev0Us8MO1iFRN8hA== +"@improbable-eng/grpc-web-node-http-transport@^0.15.0": + version "0.15.0" + resolved "https://registry.yarnpkg.com/@improbable-eng/grpc-web-node-http-transport/-/grpc-web-node-http-transport-0.15.0.tgz#5a064472ef43489cbd075a91fb831c2abeb09d68" + integrity sha512-HLgJfVolGGpjc9DWPhmMmXJx8YGzkek7jcCFO1YYkSOoO81MWRZentPOd/JiKiZuU08wtc4BG+WNuGzsQB5jZA== + +"@improbable-eng/grpc-web@0.14.0": + version "0.14.0" + resolved "https://registry.yarnpkg.com/@improbable-eng/grpc-web/-/grpc-web-0.14.0.tgz#a71c5af471dcef6a2810798f71f93ed8d6ac3817" + integrity sha512-ag1PTMWpBZKGi6GrEcZ4lkU5Qag23Xjo10BmnK9qyx4TMmSVcWmQ3rECirfQzm2uogrM9n1M6xfOpFsJP62ivA== + dependencies: + browser-headers "^0.4.1" + +"@improbable-eng/grpc-web@^0.14.0": + version "0.14.1" + resolved "https://registry.yarnpkg.com/@improbable-eng/grpc-web/-/grpc-web-0.14.1.tgz#f4662f64dc89c0f956a94bb8a3b576556c74589c" + integrity sha512-XaIYuunepPxoiGVLLHmlnVminUGzBTnXr8Wv7khzmLWbNw4TCwJKX09GSMJlKhu/TRk6gms0ySFxewaETSBqgw== + dependencies: + browser-headers "^0.4.1" + "@istanbuljs/schema@^0.1.2": version "0.1.2" resolved "https://registry.yarnpkg.com/@istanbuljs/schema/-/schema-0.1.2.tgz#26520bf09abe4a5644cd5414e37125a8954241dd" @@ -644,6 +739,67 @@ "@types/node" "*" playwright-core "1.27.1" +"@protobufjs/aspromise@^1.1.1", "@protobufjs/aspromise@^1.1.2": + version "1.1.2" + resolved "https://registry.yarnpkg.com/@protobufjs/aspromise/-/aspromise-1.1.2.tgz#9b8b0cc663d669a7d8f6f5d0893a14d348f30fbf" + integrity sha512-j+gKExEuLmKwvz3OgROXtrJ2UG2x8Ch2YZUxahh+s1F2HZ+wAceUNLkvy6zKCPVRkU++ZWQrdxsUeQXmcg4uoQ== + +"@protobufjs/base64@^1.1.2": + version "1.1.2" + resolved "https://registry.yarnpkg.com/@protobufjs/base64/-/base64-1.1.2.tgz#4c85730e59b9a1f1f349047dbf24296034bb2735" + integrity sha512-AZkcAA5vnN/v4PDqKyMR5lx7hZttPDgClv83E//FMNhR2TMcLUhfRUBHCmSl0oi9zMgDDqRUJkSxO3wm85+XLg== + +"@protobufjs/codegen@^2.0.4": + version "2.0.4" + resolved "https://registry.yarnpkg.com/@protobufjs/codegen/-/codegen-2.0.4.tgz#7ef37f0d010fb028ad1ad59722e506d9262815cb" + integrity sha512-YyFaikqM5sH0ziFZCN3xDC7zeGaB/d0IUb9CATugHWbd1FRFwWwt4ld4OYMPWu5a3Xe01mGAULCdqhMlPl29Jg== + +"@protobufjs/eventemitter@^1.1.0": + version "1.1.0" + resolved "https://registry.yarnpkg.com/@protobufjs/eventemitter/-/eventemitter-1.1.0.tgz#355cbc98bafad5978f9ed095f397621f1d066b70" + integrity sha512-j9ednRT81vYJ9OfVuXG6ERSTdEL1xVsNgqpkxMsbIabzSo3goCjDIveeGv5d03om39ML71RdmrGNjG5SReBP/Q== + +"@protobufjs/fetch@^1.1.0": + version "1.1.0" + resolved "https://registry.yarnpkg.com/@protobufjs/fetch/-/fetch-1.1.0.tgz#ba99fb598614af65700c1619ff06d454b0d84c45" + integrity sha512-lljVXpqXebpsijW71PZaCYeIcE5on1w5DlQy5WH6GLbFryLUrBD4932W/E2BSpfRJWseIL4v/KPgBFxDOIdKpQ== + dependencies: + "@protobufjs/aspromise" "^1.1.1" + "@protobufjs/inquire" "^1.1.0" + +"@protobufjs/float@^1.0.2": + version "1.0.2" + resolved "https://registry.yarnpkg.com/@protobufjs/float/-/float-1.0.2.tgz#5e9e1abdcb73fc0a7cb8b291df78c8cbd97b87d1" + integrity sha512-Ddb+kVXlXst9d+R9PfTIxh1EdNkgoRe5tOX6t01f1lYWOvJnSPDBlG241QLzcyPdoNTsblLUdujGSE4RzrTZGQ== + +"@protobufjs/inquire@^1.1.0": + version "1.1.0" + resolved "https://registry.yarnpkg.com/@protobufjs/inquire/-/inquire-1.1.0.tgz#ff200e3e7cf2429e2dcafc1140828e8cc638f089" + integrity sha512-kdSefcPdruJiFMVSbn801t4vFK7KB/5gd2fYvrxhuJYg8ILrmn9SKSX2tZdV6V+ksulWqS7aXjBcRXl3wHoD9Q== + +"@protobufjs/path@^1.1.2": + version "1.1.2" + resolved "https://registry.yarnpkg.com/@protobufjs/path/-/path-1.1.2.tgz#6cc2b20c5c9ad6ad0dccfd21ca7673d8d7fbf68d" + integrity sha512-6JOcJ5Tm08dOHAbdR3GrvP+yUUfkjG5ePsHYczMFLq3ZmMkAD98cDgcT2iA1lJ9NVwFd4tH/iSSoe44YWkltEA== + +"@protobufjs/pool@^1.1.0": + version "1.1.0" + resolved "https://registry.yarnpkg.com/@protobufjs/pool/-/pool-1.1.0.tgz#09fd15f2d6d3abfa9b65bc366506d6ad7846ff54" + integrity sha512-0kELaGSIDBKvcgS4zkjz1PeddatrjYcmMWOlAuAPwAeccUrPHdUqo/J6LiymHHEiJT5NrF1UVwxY14f+fy4WQw== + +"@protobufjs/utf8@^1.1.0": + version "1.1.0" + resolved "https://registry.yarnpkg.com/@protobufjs/utf8/-/utf8-1.1.0.tgz#a777360b5b39a1a2e5106f8e858f2fd2d060c570" + integrity sha512-Vvn3zZrhQZkkBE8LSuW3em98c0FwgO4nxzv6OdSxPKJIEKY2bGbHn+mhGIPerzI4twdxaP8/0+06HBpwf345Lw== + +"@segment/loosely-validate-event@^2.0.0": + version "2.0.0" + resolved "https://registry.yarnpkg.com/@segment/loosely-validate-event/-/loosely-validate-event-2.0.0.tgz#87dfc979e5b4e7b82c5f1d8b722dfd5d77644681" + integrity sha512-ZMCSfztDBqwotkl848ODgVcAmN4OItEWDCkshcKz0/W6gGSQayuuCtWV/MlodFivAZD793d6UgANd6wCXUfrIw== + dependencies: + component-type "^1.2.1" + join-component "^1.1.0" + "@sindresorhus/is@^0.14.0": version "0.14.0" resolved "https://registry.yarnpkg.com/@sindresorhus/is/-/is-0.14.0.tgz#9fb3a3cf3132328151f353de4632e01e52102bea" @@ -925,6 +1081,11 @@ "@types/minimatch" "*" "@types/node" "*" +"@types/google-protobuf@^3.7.4": + version "3.7.4" + resolved "https://registry.yarnpkg.com/@types/google-protobuf/-/google-protobuf-3.7.4.tgz#1621c50ceaf5aefa699851da8e0ea606a2943a39" + integrity sha512-6PjMFKl13cgB4kRdYtvyjKl8VVa0PXS2IdVxHhQ8GEKbxBkyJtSbaIeK1eZGjDKN7dvUh4vkOvU9FMwYNv4GQQ== + "@types/graceful-fs@4.1.2": version "4.1.2" resolved "https://registry.yarnpkg.com/@types/graceful-fs/-/graceful-fs-4.1.2.tgz#fbc9575dbcc6d1d91dd768d30c5fc0c19f6c50bd" @@ -981,6 +1142,11 @@ resolved "https://registry.yarnpkg.com/@types/keytar/-/keytar-4.4.0.tgz#ca24e6ee6d0df10c003aafe26e93113b8faf0d8e" integrity sha512-cq/NkUUy6rpWD8n7PweNQQBpw2o0cf5v6fbkUVEpOB9VzzIvyPvSEId1/goIj+MciW2v1Lw5mRimKO01XgE9EA== +"@types/long@^4.0.1": + version "4.0.1" + resolved "https://registry.yarnpkg.com/@types/long/-/long-4.0.1.tgz#459c65fa1867dafe6a8f322c4c51695663cc55e9" + integrity sha512-5tXH6Bx/kNGd3MgffdmP4dy2Z+G4eaXw0SE81Tq3BNadtnMR5/ySMzX4SLEzHJzSmPNn4HIdpQsBvXMUykr58w== + "@types/minimatch@*": version "3.0.3" resolved "https://registry.yarnpkg.com/@types/minimatch/-/minimatch-3.0.3.tgz#3dca0e3f33b200fc7d1139c0cd96c1268cadfd9d" @@ -1014,6 +1180,11 @@ resolved "https://registry.yarnpkg.com/@types/node/-/node-16.18.3.tgz#d7f7ba828ad9e540270f01ce00d391c54e6e0abc" integrity sha512-jh6m0QUhIRcZpNv7Z/rpN+ZWXOicUUQbSoWks7Htkbb9IjFQj4kzcX/xFCkjstCj5flMsN8FiSvt+q+Tcs4Llg== +"@types/node@>=12.12.47", "@types/node@>=13.7.0": + version "16.11.12" + resolved "https://registry.yarnpkg.com/@types/node/-/node-16.11.12.tgz#ac7fb693ac587ee182c3780c26eb65546a1a3c10" + integrity sha512-+2Iggwg7PxoO5Kyhvsq9VarmPbIelXP070HMImEpbtGCoyWNINQj4wzjbQCXzdHTRXnqufutJb5KAURZANNBAw== + "@types/node@^10.11.7": version "10.12.21" resolved "https://registry.yarnpkg.com/@types/node/-/node-10.12.21.tgz#7e8a0c34cf29f4e17a36e9bd0ea72d45ba03908e" @@ -1024,11 +1195,30 @@ resolved "https://registry.yarnpkg.com/@types/node/-/node-16.11.31.tgz#1dad8138efee6808809bb80f9e66bbe3e46c9277" integrity sha512-wh/d0pcu/Ie2mqTIqh4tjd0mLAB4JWxOjHQtLN20HS7sjMHiV4Afr+90hITTyZcxowwha5wjv32jGEn1zkEFMg== +"@types/prop-types@*": + version "15.7.4" + resolved "https://registry.yarnpkg.com/@types/prop-types/-/prop-types-15.7.4.tgz#fcf7205c25dff795ee79af1e30da2c9790808f11" + integrity sha512-rZ5drC/jWjrArrS8BR6SIr4cWpW09RNTYt9AMZo3Jwwif+iacXAqgVjm0B0Bv/S1jhDXKHqRVNCbACkJ89RAnQ== + "@types/q@^1.5.1": version "1.5.4" resolved "https://registry.yarnpkg.com/@types/q/-/q-1.5.4.tgz#15925414e0ad2cd765bfef58842f7e26a7accb24" integrity sha512-1HcDas8SEj4z1Wc696tH56G8OlRaH/sqZOynNNB+HF0WOeXPaxTtbYzJY2oEfiUxjSKjhCKr+MvR7dCHcEelug== +"@types/react@17.0.32": + version "17.0.32" + resolved "https://registry.yarnpkg.com/@types/react/-/react-17.0.32.tgz#89a161286bbe2325d4d516420a27364a324909f4" + integrity sha512-hAm1pmwA3oZWbkB985RFwNvBRMG0F3KWSiC4/hNmanigKZMiKQoH5Q6etNw8HIDztTGfvXyOjPvdNnvBUCuaPg== + dependencies: + "@types/prop-types" "*" + "@types/scheduler" "*" + csstype "^3.0.2" + +"@types/scheduler@*": + version "0.16.2" + resolved "https://registry.yarnpkg.com/@types/scheduler/-/scheduler-0.16.2.tgz#1a62f89525723dde24ba1b01b092bf5df8ad4d39" + integrity sha512-hppQEBDmlwhFAXKJX2KnWLYu5yMfi91yazPb2l+lbJiwW+wdo1gNeRA+3RgNSO39WYX2euey41KEwnqesU2Jew== + "@types/semver@^5.4.0", "@types/semver@^5.5.0": version "5.5.0" resolved "https://registry.yarnpkg.com/@types/semver/-/semver-5.5.0.tgz#146c2a29ee7d3bae4bf2fcb274636e264c813c45" @@ -1141,6 +1331,13 @@ resolved "https://registry.yarnpkg.com/@types/winreg/-/winreg-1.2.30.tgz#91d6710e536d345b9c9b017c574cf6a8da64c518" integrity sha1-kdZxDlNtNFucmwF8V0z2qNpkxRg= +"@types/ws@^7.2.6": + version "7.4.7" + resolved "https://registry.yarnpkg.com/@types/ws/-/ws-7.4.7.tgz#f7c390a36f7a0679aa69de2d501319f4f8d9b702" + integrity sha512-JQbbmxZTZehdc2iszGKs5oC3NFnjeay7mtAWrdt7qNtAVK0g19muApzAy4bm9byz79xa2ZnO/BOBC2R8RC5Lww== + dependencies: + "@types/node" "*" + "@types/yauzl@^2.9.1": version "2.9.1" resolved "https://registry.yarnpkg.com/@types/yauzl/-/yauzl-2.9.1.tgz#d10f69f9f522eef3cf98e30afb684a1e1ec923af" @@ -1616,6 +1813,11 @@ resolved "https://registry.yarnpkg.com/@xtuc/long/-/long-4.2.2.tgz#d291c6a4e97989b5c61d9acf396ae4fe133a718d" integrity sha512-NuHqBY1PB/D8xU6s/thBgOAiAP7HOYDQ32+BFZILJ8ivkUkAHQnWfn6WhL79Owj1qmUnoN/YPhktdIoucipkAQ== +"@zip.js/zip.js@^2.4.6": + version "2.4.6" + resolved "https://registry.yarnpkg.com/@zip.js/zip.js/-/zip.js-2.4.6.tgz#eb284910f4dcbccb267c5ef76950fb84ee43bb74" + integrity sha512-gP13tvMy1bhaTWw5I/Sm3mJAOU7J8S18e4sAcscGzYY8NVUF8FRirfY17eYq+rZhRBk8SNg5bFzzWgFR47qSyw== + abbrev@1: version "1.1.1" resolved "https://registry.yarnpkg.com/abbrev/-/abbrev-1.1.1.tgz#f8f2c887ad10bf67f634f005b6987fed3179aac8" @@ -1705,7 +1907,7 @@ ajv-keywords@^3.1.0, ajv-keywords@^3.4.1, ajv-keywords@^3.5.2: resolved "https://registry.yarnpkg.com/ajv-keywords/-/ajv-keywords-3.5.2.tgz#31f29da5ab6e00d1c2d329acf7b5929614d5014d" integrity sha512-5p6WTN0DdTGVQk6VjcEju19IgaHudalcfabD7yhDGeA6bcQnmL+CpveLJq/3hvfwd1aof6L386Ougkx6RfyMIQ== -ajv@^6.1.0, ajv@^6.10.0, ajv@^6.10.2, ajv@^6.12.3, ajv@^6.12.4, ajv@^6.12.5, ajv@^6.9.1: +ajv@^6.1.0, ajv@^6.10.0, ajv@^6.10.2, ajv@^6.12.3, ajv@^6.12.4, ajv@^6.12.5, ajv@^6.5.4, ajv@^6.9.1: version "6.12.6" resolved "https://registry.yarnpkg.com/ajv/-/ajv-6.12.6.tgz#baf5a62e802b07d977034586f8c3baf5adf26df4" integrity sha512-j3fVLgvTo527anyYyJOGTYJbG+vnnQYvE0m5mmkc1TK+nxAppkCLMIL0aZ4dblVCNoGShhm+kzE4ZUykBoMg4g== @@ -1725,6 +1927,25 @@ amdefine@>=0.0.4: resolved "https://registry.yarnpkg.com/amdefine/-/amdefine-1.0.1.tgz#4a5282ac164729e93619bcfd3ad151f817ce91f5" integrity sha1-SlKCrBZHKek2Gbz9OtFR+BfOkfU= +analytics-node@^6.0.0: + version "6.0.0" + resolved "https://registry.yarnpkg.com/analytics-node/-/analytics-node-6.0.0.tgz#8dd1b9a8f966e7b0a5a5f408030f1c6a137bff9b" + integrity sha512-qhwB5Fl/ps7VTg1/RnD3qJohceSHUjzTBqNn3DCmQZu/AdgPbGPeNFYu2o3xIuIyq+xZElrv0Do0b/zuGxBL9g== + dependencies: + "@segment/loosely-validate-event" "^2.0.0" + axios "^0.21.4" + axios-retry "3.2.0" + lodash.isstring "^4.0.1" + md5 "^2.2.1" + ms "^2.0.0" + remove-trailing-slash "^0.1.0" + uuid "^8.3.2" + +ansi-color@^0.2.1: + version "0.2.1" + resolved "https://registry.yarnpkg.com/ansi-color/-/ansi-color-0.2.1.tgz#3e75c037475217544ed763a8db5709fa9ae5bf9a" + integrity sha1-PnXAN0dSF1RO12Oo21cJ+prlv5o= + ansi-colors@4.1.1: version "4.1.1" resolved "https://registry.yarnpkg.com/ansi-colors/-/ansi-colors-4.1.1.tgz#cbb9ae256bf750af1eab344f229aa27fe94ba348" @@ -2106,6 +2327,20 @@ aws4@^1.8.0: resolved "https://registry.yarnpkg.com/aws4/-/aws4-1.11.0.tgz#d61f46d83b2519250e2784daf5b09479a8b41c59" integrity sha512-xh1Rl34h6Fi1DC2WWKfxUTVqRsNnr6LsKz2+hfwDxQJWmrx8+c7ylaqBMcHfl1U1r2dsifOvKX3LQuLNZ+XSvA== +axios-retry@3.2.0: + version "3.2.0" + resolved "https://registry.yarnpkg.com/axios-retry/-/axios-retry-3.2.0.tgz#eb48e72f90b177fde62329b2896aa8476cfb90ba" + integrity sha512-RK2cLMgIsAQBDhlIsJR5dOhODPigvel18XUv1dDXW+4k1FzebyfRk+C+orot6WPZOYFKSfhLwHPwVmTVOODQ5w== + dependencies: + is-retry-allowed "^1.1.0" + +axios@^0.21.4: + version "0.21.4" + resolved "https://registry.yarnpkg.com/axios/-/axios-0.21.4.tgz#c67b90dc0568e5c1cf2b0b858c43ba28e2eda575" + integrity sha512-ut5vewkiu8jjGBdqpM44XxjuCjq9LAKeHVmoVfHVzy8eHgxxq8SbAVQNovDA8mVi05kP0Ea/n/UzcSHcTJQfNg== + dependencies: + follow-redirects "^1.14.0" + bach@^1.0.0: version "1.2.0" resolved "https://registry.yarnpkg.com/bach/-/bach-1.2.0.tgz#4b3ce96bf27134f79a1b414a51c14e34c3bd9880" @@ -2195,6 +2430,11 @@ bindings@^1.5.0: dependencies: file-uri-to-path "1.0.0" +bintrees@1.0.2: + version "1.0.2" + resolved "https://registry.yarnpkg.com/bintrees/-/bintrees-1.0.2.tgz#49f896d6e858a4a499df85c38fb399b9aff840f8" + integrity sha512-VOMgTMwjAaUG580SXn3LacVgjurrbMme7ZZNYGSSV7mmtY6QQRh0Eg3pwIcntQ77DErK1L0NxkbetjcoXzVwKw== + bl@^1.0.0: version "1.2.3" resolved "https://registry.yarnpkg.com/bl/-/bl-1.2.3.tgz#1e8dd80142eac80d7158c9dccc047fb620e035e7" @@ -2219,7 +2459,7 @@ block-stream@*: dependencies: inherits "~2.0.0" -bluebird@^3.5.5: +bluebird@^3.3.3, bluebird@^3.5.5: version "3.7.2" resolved "https://registry.yarnpkg.com/bluebird/-/bluebird-3.7.2.tgz#9f229c15be272454ffa973ace0dbee79a1b0c36f" integrity sha512-XpNj6GDQzdfW+r2Wnn7xiSAd7TM3jzkxGXBGTtWKuSXv1xUV+azxAm8jdWZN06QTQk+2N2XB9jRDkvbmQmcRtg== @@ -2287,6 +2527,11 @@ brorand@^1.0.1, brorand@^1.1.0: resolved "https://registry.yarnpkg.com/brorand/-/brorand-1.1.0.tgz#12c25efe40a45e3c323eb8675a0a0ce57b22371f" integrity sha1-EsJe/kCkXjwyPrhnWgoM5XsiNx8= +browser-headers@^0.4.1: + version "0.4.1" + resolved "https://registry.yarnpkg.com/browser-headers/-/browser-headers-0.4.1.tgz#4308a7ad3b240f4203dbb45acedb38dc2d65dd02" + integrity sha512-CA9hsySZVo9371qEHjHZtYxV2cFtVj5Wj/ZHi8ooEsrtm4vOnl9Y9HmyYWk9q+05d7K3rdoAE0j3MVEFVvtQtg== + browser-stdout@1.3.1: version "1.3.1" resolved "https://registry.yarnpkg.com/browser-stdout/-/browser-stdout-1.3.1.tgz#baa559ee14ced73452229bad7326467c61fabd60" @@ -2429,6 +2674,16 @@ buffer@^5.2.1, buffer@^5.5.0: base64-js "^1.3.1" ieee754 "^1.1.13" +bufrw@^1.3.0: + version "1.3.0" + resolved "https://registry.yarnpkg.com/bufrw/-/bufrw-1.3.0.tgz#28d6cfdaf34300376836310f5c31d57eeb40c8fa" + integrity sha512-jzQnSbdJqhIltU9O5KUiTtljP9ccw2u5ix59McQy4pV2xGhVLhRZIndY8GIrgh5HjXa6+QJ9AQhOd2QWQizJFQ== + dependencies: + ansi-color "^0.2.1" + error "^7.0.0" + hexer "^1.5.0" + xtend "^4.0.0" + builtin-status-codes@^3.0.0: version "3.0.0" resolved "https://registry.yarnpkg.com/builtin-status-codes/-/builtin-status-codes-3.0.0.tgz#85982878e21b98e1c66425e03d0174788f569ee8" @@ -2632,7 +2887,7 @@ chardet@^0.7.0: resolved "https://registry.yarnpkg.com/chardet/-/chardet-0.7.0.tgz#90094849f0937f2eedc2425d0d28a9e5f0cbad9e" integrity sha512-mT8iDcrh03qDGRRmoA2hmBJnxpllMR+0/0qlzjqZES6NdiWDcZkCNAk4rPFZ9Q85r27unkiNNg8ZOiwZXBHwcA== -charenc@~0.0.1: +charenc@0.0.2, charenc@~0.0.1: version "0.0.2" resolved "https://registry.yarnpkg.com/charenc/-/charenc-0.0.2.tgz#c0a1d2f3a7092e03774bfa83f14c0fc5790a8667" integrity sha1-wKHS86cJLgN3S/qD8UwPxXkKhmc= @@ -3011,6 +3266,11 @@ component-emitter@^1.2.1: resolved "https://registry.yarnpkg.com/component-emitter/-/component-emitter-1.3.0.tgz#16e4070fba8ae29b679f2215853ee181ab2eabc0" integrity sha512-Rd3se6QB+sO1TwqZjscQrurpEPIfO0/yYnSin6Q/rD3mOutHvUrCAhJub3r90uNb+SESBuE0QYoB90YdfatsRg== +component-type@^1.2.1: + version "1.2.1" + resolved "https://registry.yarnpkg.com/component-type/-/component-type-1.2.1.tgz#8a47901700238e4fc32269771230226f24b415a9" + integrity sha1-ikeQFwAjjk/DIml3EjAibyS0Fak= + concat-map@0.0.1: version "0.0.1" resolved "https://registry.yarnpkg.com/concat-map/-/concat-map-0.0.1.tgz#d8a96bd77fd68df7793a73036a3ba0d5405d477b" @@ -3080,6 +3340,11 @@ cookie@^0.4.0: resolved "https://registry.yarnpkg.com/cookie/-/cookie-0.4.1.tgz#afd713fe26ebd21ba95ceb61f9a8116e50a537d1" integrity sha512-ZwrFkGJxUR3EIoXtO+yVE69Eb7KlixbaeAWfBQB9vVsNn/o+Yw69gBWSSDK825hQNdN+wF8zELf3dFNl/kxkUA== +cookie@^0.4.2: + version "0.4.2" + resolved "https://registry.yarnpkg.com/cookie/-/cookie-0.4.2.tgz#0e41f24de5ecf317947c82fc789e06a884824432" + integrity sha512-aSWTXFzaKWkvHO1Ny/s+ePFpvKsPnjc551iI41v3ny/ow6tBG5Vd+FuqGNhh1LxOmVzOlGUriIlOaokOvhaStA== + cookies@~0.8.0: version "0.8.0" resolved "https://registry.yarnpkg.com/cookies/-/cookies-0.8.0.tgz#1293ce4b391740a8406e3c9870e828c4b54f3f90" @@ -3153,6 +3418,11 @@ create-ecdh@^4.0.0: bn.js "^4.1.0" elliptic "^6.5.3" +create-error@^0.3.1: + version "0.3.1" + resolved "https://registry.yarnpkg.com/create-error/-/create-error-0.3.1.tgz#69810245a629e654432bf04377360003a5351a23" + integrity sha1-aYECRaYp5lRDK/BDdzYAA6U1GiM= + create-hash@^1.1.0, create-hash@^1.1.2, create-hash@^1.2.0: version "1.2.0" resolved "https://registry.yarnpkg.com/create-hash/-/create-hash-1.2.0.tgz#889078af11a63756bcfb59bd221996be3a9ef196" @@ -3201,7 +3471,7 @@ cross-spawn@^7.0.2, cross-spawn@^7.0.3: shebang-command "^2.0.0" which "^2.0.1" -crypt@~0.0.1: +crypt@0.0.2, crypt@~0.0.1: version "0.0.2" resolved "https://registry.yarnpkg.com/crypt/-/crypt-0.0.2.tgz#88d7ff7ec0dfb86f713dc87bbb42d044d3e6c41b" integrity sha1-iNf/fsDfuG9xPch7u0LQRNPmxBs= @@ -3411,6 +3681,11 @@ csso@^4.0.2, csso@^4.2.0: dependencies: css-tree "^1.1.2" +csstype@^3.0.2: + version "3.0.10" + resolved "https://registry.yarnpkg.com/csstype/-/csstype-3.0.10.tgz#2ad3a7bed70f35b965707c092e5f30b327c290e5" + integrity sha512-2u44ZG2OcNUO9HDp/Jl8C07x6pU/eTR3ncV91SiK3dhG9TWvRVsCoJw14Ckx5DgWkzGA3waZWO3d7pgqpUI/XA== + cyclist@^1.0.1: version "1.0.1" resolved "https://registry.yarnpkg.com/cyclist/-/cyclist-1.0.1.tgz#596e9698fd0c80e12038c2b82d6eb1b35b6224d9" @@ -4006,6 +4281,21 @@ error-ex@^1.2.0, error-ex@^1.3.1: dependencies: is-arrayish "^0.2.1" +error@7.0.2: + version "7.0.2" + resolved "https://registry.yarnpkg.com/error/-/error-7.0.2.tgz#a5f75fff4d9926126ddac0ea5dc38e689153cb02" + integrity sha1-pfdf/02ZJhJt2sDqXcOOaJFTywI= + dependencies: + string-template "~0.2.1" + xtend "~4.0.0" + +error@^7.0.0: + version "7.2.1" + resolved "https://registry.yarnpkg.com/error/-/error-7.2.1.tgz#eab21a4689b5f684fc83da84a0e390de82d94894" + integrity sha512-fo9HBvWnx3NGUKMvMwB/CBCMMrfEJgbDTVDEkPygA3Bdd3lM1OyCd+rbQ8BwnpF6GdVeOLDNmyL4N5Bg80ZvdA== + dependencies: + string-template "~0.2.1" + es-abstract@^1.17.2: version "1.17.7" resolved "https://registry.yarnpkg.com/es-abstract/-/es-abstract-1.17.7.tgz#a4de61b2f66989fc7421676c1cb9787573ace54c" @@ -4847,6 +5137,11 @@ flush-write-stream@^1.0.0, flush-write-stream@^1.0.2: inherits "^2.0.3" readable-stream "^2.3.6" +follow-redirects@^1.14.0: + version "1.15.1" + resolved "https://registry.yarnpkg.com/follow-redirects/-/follow-redirects-1.15.1.tgz#0ca6a452306c9b276e4d3127483e29575e207ad5" + integrity sha512-yLAMQs+k0b2m7cVxpS1VKJVvoz7SS9Td1zss3XRwXj+ZDH00RJgnuLx7E44wx02kQLrdM3aOOy+FpzS7+8OizA== + for-in@^1.0.1, for-in@^1.0.2: version "1.0.2" resolved "https://registry.yarnpkg.com/for-in/-/for-in-1.0.2.tgz#81068d295a8142ec0ac726c6e2200c30fb6d5e80" @@ -5305,6 +5600,16 @@ glogg@^1.0.0: dependencies: sparkles "^1.0.0" +google-protobuf@^3.17.0: + version "3.17.0" + resolved "https://registry.yarnpkg.com/google-protobuf/-/google-protobuf-3.17.0.tgz#5623215f9d0345649720ae5e5e9713491e2456fc" + integrity sha512-xuxzzx6FXmkmddRZci2SmGeZcx+vykmqG60yJf/Rz+NEKaUs+nLhaiUQAWa7Z00a7bHfRjRxSIX9FcELaxWIVA== + +google-protobuf@^3.19.1: + version "3.19.1" + resolved "https://registry.yarnpkg.com/google-protobuf/-/google-protobuf-3.19.1.tgz#5af5390e8206c446d8f49febaffd4b7f4ac28f41" + integrity sha512-Isv1RlNC+IzZzilcxnlVSf+JvuhxmY7DaxYCBy+zPS9XVuJRtlTTIXR9hnZ1YL1MMusJn/7eSy2swCzZIomQSg== + got@^9.6.0: version "9.6.0" resolved "https://registry.yarnpkg.com/got/-/got-9.6.0.tgz#edf45e7d67f99545705de1f7bbeeeb121765ed85" @@ -5728,6 +6033,16 @@ hex-color-regex@^1.1.0: resolved "https://registry.yarnpkg.com/hex-color-regex/-/hex-color-regex-1.1.0.tgz#4c06fccb4602fe2602b3c93df82d7e7dbf1a8a8e" integrity sha512-l9sfDFsuqtOqKDsQdqrMRk0U85RZc0RtOR9yPI7mRVOa4FsR/BVnZ0shmQRM96Ji99kYZP/7hn1cedc1+ApsTQ== +hexer@^1.5.0: + version "1.5.0" + resolved "https://registry.yarnpkg.com/hexer/-/hexer-1.5.0.tgz#b86ce808598e8a9d1892c571f3cedd86fc9f0653" + integrity sha1-uGzoCFmOip0YksVx887dhvyfBlM= + dependencies: + ansi-color "^0.2.1" + minimist "^1.1.0" + process "^0.10.0" + xtend "^4.0.0" + hmac-drbg@^1.0.1: version "1.0.1" resolved "https://registry.yarnpkg.com/hmac-drbg/-/hmac-drbg-1.0.1.tgz#d2745701025a6c775a6c545793ed502fc0c649a1" @@ -6052,6 +6367,11 @@ interpret@^2.2.0: resolved "https://registry.yarnpkg.com/interpret/-/interpret-2.2.0.tgz#1a78a0b5965c40a5416d007ad6f50ad27c417df9" integrity sha512-Ju0Bz/cEia55xDwUWEa8+olFpCiQoypjnQySseKtmjNrnps3P+xfpUmGr90T7yjlVJmOtybRvPXhKMbHr+fWnw== +inversify@^5.1.1: + version "5.1.1" + resolved "https://registry.yarnpkg.com/inversify/-/inversify-5.1.1.tgz#6fbd668c591337404e005a1946bfe0d802c08730" + integrity sha512-j8grHGDzv1v+8T1sAQ+3boTCntFPfvxLCkNcxB1J8qA0lUN+fAlSyYd+RXKvaPRL4AGyPxViutBEJHNXOyUdFQ== + invert-kv@^1.0.0: version "1.0.0" resolved "https://registry.yarnpkg.com/invert-kv/-/invert-kv-1.0.0.tgz#104a8e4aaca6d3d8cd157a8ef8bfab2d7a3ffdb6" @@ -6137,7 +6457,7 @@ is-boolean-object@^1.1.0: dependencies: call-bind "^1.0.2" -is-buffer@^1.1.5, is-buffer@~1.1.1: +is-buffer@^1.1.5, is-buffer@~1.1.1, is-buffer@~1.1.6: version "1.1.6" resolved "https://registry.yarnpkg.com/is-buffer/-/is-buffer-1.1.6.tgz#efaa2ea9daa0d7ab2ea13a97b2b8ad51fefbe8be" integrity sha512-NcdALwpXkTm5Zvvbk7owOUSvVvBKDgKP5/ewfXEznmQFfs4ZRmanOeKBTjRVjka3QFoN6XJ+9F3USqfHqTaU5w== @@ -6383,6 +6703,11 @@ is-resolvable@^1.0.0: resolved "https://registry.yarnpkg.com/is-resolvable/-/is-resolvable-1.1.0.tgz#fb18f87ce1feb925169c9a407c19318a3206ed88" integrity sha512-qgDYXFSR5WvEfuS5dMj6oTMEbrrSaM0CrFk2Yiq/gXnBvD9pMa2jGXxyhGLfvhZpuMZe18CJpFxAt3CRs42NMg== +is-retry-allowed@^1.1.0: + version "1.2.0" + resolved "https://registry.yarnpkg.com/is-retry-allowed/-/is-retry-allowed-1.2.0.tgz#d778488bd0a4666a3be8a1482b9f2baafedea8b4" + integrity sha512-RUbUeKwvm3XG2VYamhJL1xFktgjvPzL0Hq8C+6yrWIswDy3BIXGqCxhxkc30N9jqK311gVU137K8Ei55/zVJRg== + is-stream@^1.1.0: version "1.1.0" resolved "https://registry.yarnpkg.com/is-stream/-/is-stream-1.1.0.tgz#12d4a3dd4e68e0b79ceb8dbc84173ae80d91ca44" @@ -6552,6 +6877,17 @@ istextorbinary@1.0.2: binaryextensions "~1.0.0" textextensions "~1.0.0" +jaeger-client@^3.18.1: + version "3.19.0" + resolved "https://registry.yarnpkg.com/jaeger-client/-/jaeger-client-3.19.0.tgz#9b5bd818ebd24e818616ee0f5cffe1722a53ae6e" + integrity sha512-M0c7cKHmdyEUtjemnJyx/y9uX16XHocL46yQvyqDlPdvAcwPDbHrIbKjQdBqtiE4apQ/9dmr+ZLJYYPGnurgpw== + dependencies: + node-int64 "^0.4.0" + opentracing "^0.14.4" + thriftrw "^3.5.0" + uuid "^8.3.2" + xorshift "^1.1.1" + jest-worker@^27.0.2: version "27.0.6" resolved "https://registry.yarnpkg.com/jest-worker/-/jest-worker-27.0.6.tgz#a5fdb1e14ad34eb228cfe162d9f729cdbfa28aed" @@ -6561,6 +6897,11 @@ jest-worker@^27.0.2: merge-stream "^2.0.0" supports-color "^8.0.0" +join-component@^1.1.0: + version "1.1.0" + resolved "https://registry.yarnpkg.com/join-component/-/join-component-1.1.0.tgz#b8417b750661a392bee2c2537c68b2a9d4977cd5" + integrity sha1-uEF7dQZho5K+4sJTfGiyqdSXfNU= + js-beautify@^1.8.9: version "1.8.9" resolved "https://registry.yarnpkg.com/js-beautify/-/js-beautify-1.8.9.tgz#08e3c05ead3ecfbd4f512c3895b1cda76c87d523" @@ -6572,7 +6913,7 @@ js-beautify@^1.8.9: mkdirp "~0.5.0" nopt "~4.0.1" -js-tokens@^4.0.0: +"js-tokens@^3.0.0 || ^4.0.0", js-tokens@^4.0.0: version "4.0.0" resolved "https://registry.yarnpkg.com/js-tokens/-/js-tokens-4.0.0.tgz#19203fb59991df98e3a287050d4647cdeaf32499" integrity sha512-RdJUflcE3cUzKiMqQgsCu06FPu9UdIJO0beYbPhHN4k6apgJtifcoCtT9bcxOpYBtpD2kCM6Sbzg4CausW/PKQ== @@ -6584,7 +6925,7 @@ js-yaml@4.1.0, js-yaml@^4.1.0: dependencies: argparse "^2.0.1" -js-yaml@^3.13.0, js-yaml@^3.13.1: +js-yaml@^3.10.0, js-yaml@^3.13.0, js-yaml@^3.13.1: version "3.14.1" resolved "https://registry.yarnpkg.com/js-yaml/-/js-yaml-3.14.1.tgz#dae812fdb3825fa306609a8717383c50c36a0537" integrity sha512-okMH7OXXJ7YrN9Ok3/SXrnu4iX9yOk+25nqX4imS2npuvTYDmo/QEZoqwZkYaIDk3jVvBOTOIEgEhaLOynBS9g== @@ -6968,6 +7309,11 @@ lodash.get@^4.4.2: resolved "https://registry.yarnpkg.com/lodash.get/-/lodash.get-4.4.2.tgz#2d177f652fa31e939b4438d5341499dfa3825e99" integrity sha1-LRd/ZS+jHpObRDjVNBSZ36OCXpk= +lodash.isstring@^4.0.1: + version "4.0.1" + resolved "https://registry.yarnpkg.com/lodash.isstring/-/lodash.isstring-4.0.1.tgz#d527dfb5456eca7cc9bb95d5daeaf88ba54a5451" + integrity sha1-1SfftUVuynzJu5XV2ur4i6VKVFE= + lodash.memoize@^4.1.2: version "4.1.2" resolved "https://registry.yarnpkg.com/lodash.memoize/-/lodash.memoize-4.1.2.tgz#bcc6c49a42a2840ed997f323eada5ecd182e0bfe" @@ -7001,6 +7347,23 @@ log-symbols@4.1.0: chalk "^4.1.0" is-unicode-supported "^0.1.0" +long@^2.4.0: + version "2.4.0" + resolved "https://registry.yarnpkg.com/long/-/long-2.4.0.tgz#9fa180bb1d9500cdc29c4156766a1995e1f4524f" + integrity sha1-n6GAux2VAM3CnEFWdmoZleH0Uk8= + +long@^4.0.0: + version "4.0.0" + resolved "https://registry.yarnpkg.com/long/-/long-4.0.0.tgz#9a7b71cfb7d361a194ea555241c92f7468d5bf28" + integrity sha512-XsP+KhQif4bjX1kbuSiySJFNAehNxgLb6hPRGJ9QsUr8ajHkuXGdrHmFUTUUXhDwVX2R5bY4JNZEwbUiMhV+MA== + +loose-envify@^1.1.0: + version "1.4.0" + resolved "https://registry.yarnpkg.com/loose-envify/-/loose-envify-1.4.0.tgz#71ee51fa7be4caec1a63839f7e682d8132d30caf" + integrity sha512-lyuxPGr/Wfhrlem2CL/UcnUc1zcqKAImBDzukY7Y5F/yQiNdko6+fRLevlw1HgMySw7f611UIY408EtxRSoK3Q== + dependencies: + js-tokens "^3.0.0 || ^4.0.0" + lowercase-keys@^1.0.0, lowercase-keys@^1.0.1: version "1.0.1" resolved "https://registry.yarnpkg.com/lowercase-keys/-/lowercase-keys-1.0.1.tgz#6f9e30b47084d971a7c820ff15a6c5167b74c26f" @@ -7153,6 +7516,15 @@ md5@^2.1.0: crypt "~0.0.1" is-buffer "~1.1.1" +md5@^2.2.1: + version "2.3.0" + resolved "https://registry.yarnpkg.com/md5/-/md5-2.3.0.tgz#c3da9a6aae3a30b46b7b0c349b87b110dc3bda4f" + integrity sha512-T1GITYmFaKuO91vxyoQMFETst+O71VUPEU3ze5GNzDm0OWdP8v1ziTaAEPUr/3kLsY3Sftgz242A1SetQiDL7g== + dependencies: + charenc "0.0.2" + crypt "0.0.2" + is-buffer "~1.1.6" + mdn-data@2.0.14: version "2.0.14" resolved "https://registry.yarnpkg.com/mdn-data/-/mdn-data-2.0.14.tgz#7113fc4281917d63ce29b43446f701e68c25ba50" @@ -7374,16 +7746,11 @@ minimatch@^5.0.1, minimatch@^5.1.0: dependencies: brace-expansion "^2.0.1" -minimist@^1.2.0, minimist@^1.2.7: +minimist@^1.1.0, minimist@^1.2.0, minimist@^1.2.3, minimist@^1.2.5, minimist@^1.2.6, minimist@^1.2.7: version "1.2.7" resolved "https://registry.yarnpkg.com/minimist/-/minimist-1.2.7.tgz#daa1c4d91f507390437c6a8bc01078e7000c4d18" integrity sha512-bzfL1YUZsP41gmu/qjrEk0Q6i2ix/cVeAhbCbqH9u3zYutS1cLg00qhrD0M2MVdCcx4Sc0UpP2eBWo9rotpq6g== -minimist@^1.2.3, minimist@^1.2.5, minimist@^1.2.6: - version "1.2.6" - resolved "https://registry.yarnpkg.com/minimist/-/minimist-1.2.6.tgz#8637a5b759ea0d6e98702cfb3a9283323c93af44" - integrity sha512-Jsjnk4bw3YJqYzbdyBiNsPWHPfO++UGG749Cxs6peCu5Xg4nrena6OVxOYxrQTqww0Jmwt+Ref8rggumkTLz9Q== - minipass-collect@^1.0.2: version "1.0.2" resolved "https://registry.yarnpkg.com/minipass-collect/-/minipass-collect-1.0.2.tgz#22b813bf745dc6edba2576b940022ad6edc8c617" @@ -7543,7 +7910,7 @@ ms@2.1.2: resolved "https://registry.yarnpkg.com/ms/-/ms-2.1.2.tgz#d09d1f357b443f493382a8eb3ccd183872ae6009" integrity sha512-sGkPx+VjMtmA6MX27oA4FBFELFCZZ4S4XqeGOXCv68tT+jb3vk/RyaKWP0PTKyWtmLSM0b+adUTEvbs1PEaH2w== -ms@2.1.3, ms@^2.1.1: +ms@2.1.3, ms@^2.0.0, ms@^2.1.1: version "2.1.3" resolved "https://registry.yarnpkg.com/ms/-/ms-2.1.3.tgz#574c8138ce1d2b5861f0b44579dbadd60c6615b2" integrity sha512-6FlzubTLZG3J2a/NVCAleEhjzq5oxgHyaCU9yYXvcLsvoVaHJq/s5xXI6/XXP6tz7R9xAOtHnSO/tXtF3WRTlA== @@ -7705,6 +8072,11 @@ node-gyp-build@^4.3.0: resolved "https://registry.yarnpkg.com/node-gyp-build/-/node-gyp-build-4.3.0.tgz#9f256b03e5826150be39c764bf51e993946d71a3" integrity sha512-iWjXZvmboq0ja1pUGULQBexmxq8CV4xBhX7VDOTbL7ZR4FOowwY/VOtRxBN/yKxmdGoIp4j5ysNT4u3S2pDQ3Q== +node-int64@^0.4.0: + version "0.4.0" + resolved "https://registry.yarnpkg.com/node-int64/-/node-int64-0.4.0.tgz#87a9065cdb355d3182d8f94ce11188b825c68a3b" + integrity sha1-h6kGXNs1XTGC2PlM4RGIuCXGijs= + node-libs-browser@^2.2.1: version "2.2.1" resolved "https://registry.yarnpkg.com/node-libs-browser/-/node-libs-browser-2.2.1.tgz#b64f513d18338625f90346d27b0d235e631f6425" @@ -8028,6 +8400,11 @@ only@~0.0.2: resolved "https://registry.yarnpkg.com/only/-/only-0.0.2.tgz#2afde84d03e50b9a8edc444e30610a70295edfb4" integrity sha1-Kv3oTQPlC5qO3EROMGEKcCle37Q= +opentracing@^0.14.4, opentracing@^0.14.5: + version "0.14.7" + resolved "https://registry.yarnpkg.com/opentracing/-/opentracing-0.14.7.tgz#25d472bd0296dc0b64d7b94cbc995219031428f5" + integrity sha512-vz9iS7MJ5+Bp1URw8Khvdyw1H/hGvzHWlKQ7eRrQojSCDL1/SrWfrY9QebLw97n2deyRtzHRC3MkQfVNUCo91Q== + opn@^6.0.0: version "6.0.0" resolved "https://registry.yarnpkg.com/opn/-/opn-6.0.0.tgz#3c5b0db676d5f97da1233d1ed42d182bc5a27d2d" @@ -8886,6 +9263,11 @@ process-nextick-args@^2.0.0, process-nextick-args@~2.0.0: resolved "https://registry.yarnpkg.com/process-nextick-args/-/process-nextick-args-2.0.1.tgz#7820d9b16120cc55ca9ae7792680ae7dba6d7fe2" integrity sha512-3ouUOpQhtgrbOa17J7+uxOTpITYWaGP7/AhoR3+A+/1e9skrzelGi/dXzEYyvbxubEF6Wn2ypscTKiKJFFn1ag== +process@^0.10.0: + version "0.10.1" + resolved "https://registry.yarnpkg.com/process/-/process-0.10.1.tgz#842457cc51cfed72dc775afeeafb8c6034372725" + integrity sha1-hCRXzFHP7XLcd1r+6vuMYDQ3JyU= + process@^0.11.10: version "0.11.10" resolved "https://registry.yarnpkg.com/process/-/process-0.11.10.tgz#7332300e840161bda3e69a1d1d91a7d4bc16f182" @@ -8901,6 +9283,13 @@ progress@^2.0.0, progress@^2.0.3: resolved "https://registry.yarnpkg.com/progress/-/progress-2.0.3.tgz#7e8cf8d8f5b8f239c1bc68beb4eb78567d572ef8" integrity sha512-7PiHtLll5LdnKIMw100I+8xJXR5gW2QwWYkT6iJva0bXitZKa/XMrSbdmg3r2Xnaidz9Qumd0VPaMrZlF9V9sA== +prom-client@^13.2.0: + version "13.2.0" + resolved "https://registry.yarnpkg.com/prom-client/-/prom-client-13.2.0.tgz#99d13357912dd400f8911b77df19f7b328a93e92" + integrity sha512-wGr5mlNNdRNzEhRYXgboUU2LxHWIojxscJKmtG3R8f4/KiWqyYgXTLHs0+Ted7tG3zFT7pgHJbtomzZ1L0ARaQ== + dependencies: + tdigest "^0.1.1" + promise-inflight@^1.0.1: version "1.0.1" resolved "https://registry.yarnpkg.com/promise-inflight/-/promise-inflight-1.0.1.tgz#98472870bf228132fcbdd868129bad12c3c029e3" @@ -8911,6 +9300,25 @@ proto-list@~1.2.1: resolved "https://registry.yarnpkg.com/proto-list/-/proto-list-1.2.4.tgz#212d5bfe1318306a420f6402b8e26ff39647a849" integrity sha1-IS1b/hMYMGpCD2QCuOJv85ZHqEk= +protobufjs@^6.10.0: + version "6.11.2" + resolved "https://registry.yarnpkg.com/protobufjs/-/protobufjs-6.11.2.tgz#de39fabd4ed32beaa08e9bb1e30d08544c1edf8b" + integrity sha512-4BQJoPooKJl2G9j3XftkIXjoC9C0Av2NOrWmbLWT1vH32GcSUHjM0Arra6UfTsVyfMAuFzaLucXn1sadxJydAw== + dependencies: + "@protobufjs/aspromise" "^1.1.2" + "@protobufjs/base64" "^1.1.2" + "@protobufjs/codegen" "^2.0.4" + "@protobufjs/eventemitter" "^1.1.0" + "@protobufjs/fetch" "^1.1.0" + "@protobufjs/float" "^1.0.2" + "@protobufjs/inquire" "^1.1.0" + "@protobufjs/path" "^1.1.2" + "@protobufjs/pool" "^1.1.0" + "@protobufjs/utf8" "^1.1.0" + "@types/long" "^4.0.1" + "@types/node" ">=13.7.0" + long "^4.0.0" + proxy-from-env@^1.1.0: version "1.1.0" resolved "https://registry.yarnpkg.com/proxy-from-env/-/proxy-from-env-1.1.0.tgz#e102f16ca355424865755d2c9e8ea4f24d58c3e2" @@ -9042,6 +9450,14 @@ queue@^4.2.1: dependencies: inherits "~2.0.0" +random-number-csprng@^1.0.2: + version "1.0.2" + resolved "https://registry.yarnpkg.com/random-number-csprng/-/random-number-csprng-1.0.2.tgz#fcd120e62dffc2c07674c7c3fe01e16b25f73a26" + integrity sha1-/NEg5i3/wsB2dMfD/gHhayX3OiY= + dependencies: + bluebird "^3.3.3" + create-error "^0.3.1" + randombytes@^2.0.0, randombytes@^2.0.1, randombytes@^2.0.5, randombytes@^2.1.0: version "2.1.0" resolved "https://registry.yarnpkg.com/randombytes/-/randombytes-2.1.0.tgz#df6f84372f0270dc65cdf6291349ab7a473d4f2a" @@ -9077,6 +9493,23 @@ rcedit@^1.1.0: resolved "https://registry.yarnpkg.com/rcedit/-/rcedit-1.1.0.tgz#ae21c28d4efdd78e95fcab7309a5dd084920b16a" integrity sha512-JkXJ0IrUcdupLoIx6gE4YcFaMVSGtu7kQf4NJoDJUnfBZGuATmJ2Yal2v55KTltp+WV8dGr7A0RtOzx6jmtM6Q== +react-dom@17.0.2: + version "17.0.2" + resolved "https://registry.yarnpkg.com/react-dom/-/react-dom-17.0.2.tgz#ecffb6845e3ad8dbfcdc498f0d0a939736502c23" + integrity sha512-s4h96KtLDUQlsENhMn1ar8t2bEa+q/YAtj8pPPdIjPDGBDIVNsrD9aXNWqspUe6AzKCIG0C1HZZLqLV7qpOBGA== + dependencies: + loose-envify "^1.1.0" + object-assign "^4.1.1" + scheduler "^0.20.2" + +react@17.0.2: + version "17.0.2" + resolved "https://registry.yarnpkg.com/react/-/react-17.0.2.tgz#d0b5cc516d29eb3eee383f75b62864cfb6800037" + integrity sha512-gnhPt75i/dq/z3/6q/0asP78D0u592D5L1pd7M8P+dck6Fu/jJeL6iVVK23fptSUZj8Vjf++7wXA8UNclGQcbA== + dependencies: + loose-envify "^1.1.0" + object-assign "^4.1.1" + read-pkg-up@^1.0.1: version "1.0.1" resolved "https://registry.yarnpkg.com/read-pkg-up/-/read-pkg-up-1.0.1.tgz#9d63c13276c065918d57f002a57f40a1b643fb02" @@ -9181,6 +9614,16 @@ rechoir@^0.7.0: dependencies: resolve "^1.9.0" +reconnecting-websocket@^4.4.0: + version "4.4.0" + resolved "https://registry.yarnpkg.com/reconnecting-websocket/-/reconnecting-websocket-4.4.0.tgz#3b0e5b96ef119e78a03135865b8bb0af1b948783" + integrity sha512-D2E33ceRPga0NvTDhJmphEgJ7FUYF0v4lr1ki0csq06OdlxKfugGzN0dSkxM/NfqCxYELK4KcaTOUOjTV6Dcng== + +reflect-metadata@^0.1.13: + version "0.1.13" + resolved "https://registry.yarnpkg.com/reflect-metadata/-/reflect-metadata-0.1.13.tgz#67ae3ca57c972a2aa1642b10fe363fe32d49dc08" + integrity sha512-Ts1Y/anZELhSsjMcU605fU9RE4Oi3p5ORujwbIKXfWa+0Zxs510Qrmrce5/Jowq3cHSZSJqBjypxmHarc+vEWg== + regex-not@^1.0.0, regex-not@^1.0.2: version "1.0.2" resolved "https://registry.yarnpkg.com/regex-not/-/regex-not-1.0.2.tgz#1f4ece27e00b0b65e0247a6810e6a85d83a5752c" @@ -9221,6 +9664,11 @@ remove-trailing-separator@^1.0.1, remove-trailing-separator@^1.1.0: resolved "https://registry.yarnpkg.com/remove-trailing-separator/-/remove-trailing-separator-1.1.0.tgz#c24bce2a283adad5bc3f58e0d48249b92379d8ef" integrity sha1-wkvOKig62tW8P1jg1IJJuSN52O8= +remove-trailing-slash@^0.1.0: + version "0.1.1" + resolved "https://registry.yarnpkg.com/remove-trailing-slash/-/remove-trailing-slash-0.1.1.tgz#be2285a59f39c74d1bce4f825950061915e3780d" + integrity sha512-o4S4Qh6L2jpnCy83ysZDau+VORNvnFw07CKSAymkd6ICNVEPisMyzlc00KlvvicsxKck94SEwhDnMNdICzO+tA== + repeat-element@^1.1.2: version "1.1.3" resolved "https://registry.yarnpkg.com/repeat-element/-/repeat-element-1.1.3.tgz#782e0d825c0c5a3bb39731f84efee6b742e6b1ce" @@ -9501,6 +9949,14 @@ sax@>=0.6.0, sax@~1.2.4: resolved "https://registry.yarnpkg.com/sax/-/sax-1.2.4.tgz#2816234e2378bddc4e5354fab5caa895df7100d9" integrity sha512-NqVDv9TpANUjFm0N8uM5GxL36UgKi9/atZw+x7YFnQ8ckwFGKrl4xX4yWtrey3UJm5nP1kUbnYgLopqWNSRhWw== +scheduler@^0.20.2: + version "0.20.2" + resolved "https://registry.yarnpkg.com/scheduler/-/scheduler-0.20.2.tgz#4baee39436e34aa93b4874bddcbf0fe8b8b50e91" + integrity sha512-2eWfGgAqqWFGqtdMmcL5zCMK1U8KlXv8SQFGglL3CEtd0aDVDWgeF/YoCmvln55m5zSk3J/20hTaSBeSObsQDQ== + dependencies: + loose-envify "^1.1.0" + object-assign "^4.1.1" + schema-utils@^1.0.0: version "1.0.0" resolved "https://registry.yarnpkg.com/schema-utils/-/schema-utils-1.0.0.tgz#0b79a93204d7b600d4b2850d1f66c2a34951c770" @@ -10075,6 +10531,11 @@ streamifier@^0.1.1, streamifier@~0.1.1: resolved "https://registry.yarnpkg.com/streamifier/-/streamifier-0.1.1.tgz#97e98d8fa4d105d62a2691d1dc07e820db8dfc4f" integrity sha1-l+mNj6TRBdYqJpHR3AfoINuN/E8= +string-template@~0.2.1: + version "0.2.1" + resolved "https://registry.yarnpkg.com/string-template/-/string-template-0.2.1.tgz#42932e598a352d01fc22ec3367d9d84eec6c9add" + integrity sha1-QpMuWYo1LQH8IuwzZ9nYTuxsmt0= + string-width@^1.0.1, string-width@^1.0.2: version "1.0.2" resolved "https://registry.yarnpkg.com/string-width/-/string-width-1.0.2.tgz#118bdf5b8cdc51a2a7e70d211e07e2b0b9b107d3" @@ -10435,6 +10896,13 @@ tas-client-umd@0.1.6: resolved "https://registry.yarnpkg.com/tas-client-umd/-/tas-client-umd-0.1.6.tgz#a0cf70a68f50d406773457630666224f0eb545a6" integrity sha512-eOz5IK4cuNmSZI9QlqlT0FdvgfnnHDB6rjqleFaYAbzYE4RdJzYNiM28zFIXgmOVEgESvfabMFxG8WX5M4z3HA== +tdigest@^0.1.1: + version "0.1.2" + resolved "https://registry.yarnpkg.com/tdigest/-/tdigest-0.1.2.tgz#96c64bac4ff10746b910b0e23b515794e12faced" + integrity sha512-+G0LLgjjo9BZX2MfdvPfH+MKLCrxlXSYec5DaPYP1fe6Iyhf0/fSmJ0bFiZ1F8BT6cGXl2LpltQptzjXKWEkKA== + dependencies: + bintrees "1.0.2" + temp@^0.8.3: version "0.8.4" resolved "https://registry.yarnpkg.com/temp/-/temp-0.8.4.tgz#8c97a33a4770072e0a05f919396c7665a7dd59f2" @@ -10497,6 +10965,15 @@ textextensions@~1.0.0: resolved "https://registry.yarnpkg.com/textextensions/-/textextensions-1.0.2.tgz#65486393ee1f2bb039a60cbba05b0b68bd9501d2" integrity sha1-ZUhjk+4fK7A5pgy7oFsLaL2VAdI= +thriftrw@^3.5.0: + version "3.12.0" + resolved "https://registry.yarnpkg.com/thriftrw/-/thriftrw-3.12.0.tgz#30857847755e7f036b2e0a79d11c9f55075539d9" + integrity sha512-4YZvR4DPEI41n4Opwr4jmrLGG4hndxr7387kzRFIIzxHQjarPusH4lGXrugvgb7TtPrfZVTpZCVe44/xUxowEw== + dependencies: + bufrw "^1.3.0" + error "7.0.2" + long "^2.4.0" + through2-filter@^3.0.0: version "3.0.0" resolved "https://registry.yarnpkg.com/through2-filter/-/through2-filter-3.0.0.tgz#700e786df2367c2c88cd8aa5be4cf9c1e7831254" @@ -11060,7 +11537,7 @@ uuid@^3.3.2: resolved "https://registry.yarnpkg.com/uuid/-/uuid-3.4.0.tgz#b23e4358afa8a202fe7a100af1f5f883f02007ee" integrity sha512-HjSDRw6gZE5JMggctHBcjVak08+KEVhSIiDzFnT9S9aegmp85S/bReBVTb4QTFaRNptJ9kuYaNhnbNEOkbKb/A== -uuid@^8.3.0: +uuid@^8.3.0, uuid@^8.3.2: version "8.3.2" resolved "https://registry.yarnpkg.com/uuid/-/uuid-8.3.2.tgz#80d5b5ced271bb9af6c445f21a1a04c606cefbe2" integrity sha512-+NYs2QeMWy+GWFOEm9xnn6HCDp0l7QBD7ml8zLUmJ+93Q5NF0NocErnwkTkXVFNiX3/fpC6afS8Dhb/gz7R7eg== @@ -11232,6 +11709,29 @@ vm-browserify@^1.0.1: resolved "https://registry.yarnpkg.com/vm-browserify/-/vm-browserify-1.1.2.tgz#78641c488b8e6ca91a75f511e7a3b32a86e5dda0" integrity sha512-2ham8XPWTONajOR0ohOKOHXkm3+gaBmGut3SRuu75xLd/RRaY6vqgh8NBYYk7+RW3u5AtzPQZG8F10LHkl0lAQ== +vscode-jsonrpc@8.0.0: + version "8.0.0" + resolved "https://registry.yarnpkg.com/vscode-jsonrpc/-/vscode-jsonrpc-8.0.0.tgz#43cca5b81d46ddffb8608de9d43faaa8c0eb616b" + integrity sha512-gc16lr5REIvxqCLQ9Bwf0fQMCnX5eSFoXeXymSXh80HXUtk7E3TWqT/QduFmWK6PSjruWpwc9X2mmpD1WBcS2g== + +vscode-jsonrpc@^5.0.0, vscode-jsonrpc@^5.0.1: + version "5.0.1" + resolved "https://registry.yarnpkg.com/vscode-jsonrpc/-/vscode-jsonrpc-5.0.1.tgz#9bab9c330d89f43fc8c1e8702b5c36e058a01794" + integrity sha512-JvONPptw3GAQGXlVV2utDcHx0BiY34FupW/kI6mZ5x06ER5DdPG/tXWMVHjTNULF5uKPOUUD0SaXg5QaubJL0A== + +vscode-languageserver-protocol@3.17.0: + version "3.17.0" + resolved "https://registry.yarnpkg.com/vscode-languageserver-protocol/-/vscode-languageserver-protocol-3.17.0.tgz#9fa8d366a57b7f5bca4f0bbe5ff9deeba9a1a03a" + integrity sha512-SizljNNWWcgKCoXFL8xvzQptzH599YUVmde7wS/ESxgRRzAiIf6jR7i+CoiLU6G/6ySG351MNSvc8z33ncmLNQ== + dependencies: + vscode-jsonrpc "8.0.0" + vscode-languageserver-types "3.17.0" + +vscode-languageserver-types@3.17.0: + version "3.17.0" + resolved "https://registry.yarnpkg.com/vscode-languageserver-types/-/vscode-languageserver-types-3.17.0.tgz#98f27a8855152897c9dde6127cb778ce70c7c239" + integrity sha512-ECJg27DKWEfkIUuNyjMydPsl5Lu7XX1xmwEpZ61I4oeK1qFNbfp3tSZUVmeMPPgnNjasd1rrb3on9jbSe5g3nQ== + vscode-nls-dev@^3.3.1: version "3.3.1" resolved "https://registry.yarnpkg.com/vscode-nls-dev/-/vscode-nls-dev-3.3.1.tgz#15fc03e0c9ca5a150abb838690d9554ac06f77e4" @@ -11287,7 +11787,7 @@ vscode-textmate@7.0.1: resolved "https://registry.yarnpkg.com/vscode-textmate/-/vscode-textmate-7.0.1.tgz#8118a32b02735dccd14f893b495fa5389ad7de79" integrity sha512-zQ5U/nuXAAMsh691FtV0wPz89nSkHbs+IQV8FDk+wew9BlSDhf4UmWGlWJfTR2Ti6xZv87Tj5fENzKf6Qk7aLw== -vscode-uri@^3.0.6: +vscode-uri@^3.0.3, vscode-uri@^3.0.6: version "3.0.6" resolved "https://registry.yarnpkg.com/vscode-uri/-/vscode-uri-3.0.6.tgz#5e6e2e1a4170543af30151b561a41f71db1d6f91" integrity sha512-fmL7V1eiDBFRRnu+gfRWTzyPpNIHJTc4mWnFkwBUmO9U3KPgJAmTx7oxi2bl/Rh6HLdU7+4C9wlj0k2E4AdKFQ== @@ -11299,6 +11799,13 @@ vscode-windows-ca-certs@^0.3.0: dependencies: node-addon-api "^3.0.2" +vscode-ws-jsonrpc@^0.2.0: + version "0.2.0" + resolved "https://registry.yarnpkg.com/vscode-ws-jsonrpc/-/vscode-ws-jsonrpc-0.2.0.tgz#5e9c26e10da54a1a235da7d59e74508bbcb8edd9" + integrity sha512-NE9HNRgPjCaPyTJvIudcpyIWPImxwRDtuTX16yks7SAiZgSXigxAiZOvSvVBGmD1G/OMfrFo6BblOtjVR9DdVA== + dependencies: + vscode-jsonrpc "^5.0.0" + watchpack-chokidar2@^2.0.1: version "2.0.1" resolved "https://registry.yarnpkg.com/watchpack-chokidar2/-/watchpack-chokidar2-2.0.1.tgz#38500072ee6ece66f3769936950ea1771be1c957" @@ -11608,6 +12115,11 @@ ws@^7.2.0: resolved "https://registry.yarnpkg.com/ws/-/ws-7.4.6.tgz#5654ca8ecdeee47c33a9a4bf6d28e2be2980377c" integrity sha512-YmhHDO4MzaDLB+M9ym/mDA5z0naX8j7SIlT8f8z+I0VtzsRbekxEutHSme7NPS2qE8StCYQNUnfWdXta/Yu85A== +ws@^7.4.6: + version "7.5.8" + resolved "https://registry.yarnpkg.com/ws/-/ws-7.5.8.tgz#ac2729881ab9e7cbaf8787fe3469a48c5c7f636a" + integrity sha512-ri1Id1WinAX5Jqn9HejiGb8crfRio0Qgu8+MtL36rlTA6RLsMdWt1Az/19A2Qij6uSHUMphEFaTKa4WG+UNHNw== + xml2js@^0.4.17, xml2js@^0.4.23: version "0.4.23" resolved "https://registry.yarnpkg.com/xml2js/-/xml2js-0.4.23.tgz#a0c69516752421eb2ac758ee4d4ccf58843eac66" @@ -11644,6 +12156,11 @@ xmlbuilder@~9.0.1: resolved "https://registry.yarnpkg.com/xmlbuilder/-/xmlbuilder-9.0.4.tgz#519cb4ca686d005a8420d3496f3f0caeecca580f" integrity sha1-UZy0ymhtAFqEINNJbz8MruzKWA8= +xorshift@^1.1.1: + version "1.2.0" + resolved "https://registry.yarnpkg.com/xorshift/-/xorshift-1.2.0.tgz#30a4cdd8e9f8d09d959ed2a88c42a09c660e8148" + integrity sha512-iYgNnGyeeJ4t6U11NpA/QiKy+PXn5Aa3Azg5qkwIFz1tBLllQrjjsk9yzD7IAK0naNU4JxdeDgqW9ov4u/hc4g== + xtend@^4.0.0, xtend@~4.0.0, xtend@~4.0.1: version "4.0.2" resolved "https://registry.yarnpkg.com/xtend/-/xtend-4.0.2.tgz#bb72779f5fa465186b1f438f674fa347fdb5db54" @@ -11770,7 +12287,7 @@ yargs-unparser@2.0.0: flat "^5.0.2" is-plain-obj "^2.1.0" -yargs@16.2.0: +yargs@16.2.0, yargs@^16.1.1: version "16.2.0" resolved "https://registry.yarnpkg.com/yargs/-/yargs-16.2.0.tgz#1c82bf0f6b6a66eafce7ef30e376f49a12477f66" integrity sha512-D1mvvtDG0L5ft/jGWkLpG1+m0eQxOfaBvTNELraWj22wSVUMWxZUvYgJYcKh6jGGIkJFhH4IZPQhR4TKpc8mBw==