diff --git a/plugins/workspace-plugin/devfile.yaml b/plugins/workspace-plugin/devfile.yaml new file mode 100644 index 000000000..8466c7012 --- /dev/null +++ b/plugins/workspace-plugin/devfile.yaml @@ -0,0 +1,72 @@ +apiVersion: 1.0.0 + +metadata: + generateName: che-theia-workspace-plugin- + +projects: + - name: che-theia + source: + location: 'https://github.com/eclipse/che-theia.git' + type: git + +components: + - mountSources: true + command: + - tail + - '-f' + - /dev/null + memoryLimit: 512Gi + type: dockerimage + image: 'quay.io/eclipse/che-theia:next' + alias: che-theia-next-dev + env: + - value: 0.0.0.0 + name: THEIA_HOST + - value: '3130' + name: THEIA_PORT + - value: '0' + name: NODE_TLS_REJECT_UNAUTHORIZED + + - mountSources: true + memoryLimit: 3Gi + type: dockerimage + image: 'quay.io/eclipse/che-theia-dev:next' + alias: che-dev + + - id: redhat/vscode-yaml/latest + type: chePlugin + + - id: che-incubator/typescript/latest + memoryLimit: 2048M + type: chePlugin + + - id: ms-vscode/vscode-github-pullrequest/latest + type: chePlugin + +commands: + + - name: build ... workspace-plugin + actions: + - workdir: /projects/che-theia/plugins/workspace-plugin + type: exec + command: | + killall node; yarn || (yarn lint:fix && yarn format:fix && yarn) && echo -e "\e[32mDone.\e[0m build ... workspace-plugin" + component: che-dev + + - name: test-watch ... workspace-plugin + actions: + - workdir: /projects/che-theia/plugins/workspace-plugin + type: exec + command: | + killall node; yarn test:watch + component: che-dev + + - name: run ... che-theia + workspace-plugin + previewUrl: + port: 3130 + actions: + - workdir: /home/theia + type: exec + command: | + rm /default-theia-plugins/eclipse_che_workspace_plugin.theia; mkdir -p /tmp/theiadev_projects && export CHE_PROJECTS_ROOT=/tmp/theiadev_projects && cp /projects/che-theia/plugins/workspace-plugin/eclipse_che_workspace_plugin.theia /default-theia-plugins/ && /entrypoint.sh + component: che-theia-next-dev diff --git a/plugins/workspace-plugin/package.json b/plugins/workspace-plugin/package.json index f0257de2b..b82f49ad7 100644 --- a/plugins/workspace-plugin/package.json +++ b/plugins/workspace-plugin/package.json @@ -10,6 +10,7 @@ "src" ], "dependencies": { + "async-mutex": "^0.2.6", "fs-extra": "7.0.1" }, "devDependencies": { diff --git a/plugins/workspace-plugin/src/askpass-prompt-manager.ts b/plugins/workspace-plugin/src/askpass-prompt-manager.ts new file mode 100644 index 000000000..fe8bb2d13 --- /dev/null +++ b/plugins/workspace-plugin/src/askpass-prompt-manager.ts @@ -0,0 +1,40 @@ +/********************************************************************** + * Copyright (c) 2019-2021 Red Hat, Inc. + * + * This program and the accompanying materials are made + * available under the terms of the Eclipse Public License 2.0 + * which is available at https://www.eclipse.org/legal/epl-2.0/ + * + * SPDX-License-Identifier: EPL-2.0 + ***********************************************************************/ +import { Mutex } from 'async-mutex'; + +export class PromptManager { + private askPassResults: Map = new Map(); + private mutex: Mutex = new Mutex(); + + constructor(promptLauncher: (host: string, placeHolder: string) => PromiseLike) { + this.askPassPromptLauncher = promptLauncher; + } + + async askPass(host: string, placeHolder: string): Promise { + const release = await this.mutex.acquire(); + try { + const key = getKey(host, placeHolder); + if (this.askPassResults.has(key)) { + return this.askPassResults.get(key) || ''; + } + const result = (await this.askPassPromptLauncher(host, placeHolder)) || ''; + this.askPassResults.set(key, result); + return result; + } finally { + release(); + } + } + + askPassPromptLauncher: (host: string, placeHolder: string) => PromiseLike; +} + +function getKey(host: string, placeHolder: string) { + return Symbol.for(`key[${host}:${placeHolder}]`); +} diff --git a/plugins/workspace-plugin/src/askpass.ts b/plugins/workspace-plugin/src/askpass.ts index 4b7b0358a..c075f7477 100644 --- a/plugins/workspace-plugin/src/askpass.ts +++ b/plugins/workspace-plugin/src/askpass.ts @@ -12,6 +12,8 @@ import * as os from 'os'; import * as path from 'path'; import * as theia from '@theia/plugin'; +import { PromptManager } from './askpass-prompt-manager'; + const randomBytes = denodeify(crypto.randomBytes); export interface AskpassEnvironment { @@ -46,8 +48,19 @@ export class Askpass implements theia.Disposable { private ipcHandlePath: string | undefined; private enabled = true; + private promptManager: PromptManager; + constructor() { this.server = http.createServer((req, res) => this.onRequest(req, res)); + this.promptManager = new PromptManager((host: string, placeHolder: string) => { + const options: theia.InputBoxOptions = { + password: /password/i.test(placeHolder), + placeHolder: placeHolder, + prompt: `Git: ${host}`, + ignoreFocusOut: true, + }; + return theia.window.showInputBox(options); + }); this.ipcHandlePathPromise = this.setup().catch(err => { console.error(err); return ''; @@ -78,7 +91,7 @@ export class Askpass implements theia.Disposable { req.on('end', () => { const { request, host } = JSON.parse(chunks.join('')); - this.prompt(host, request).then( + this.promptManager.askPass(host, request).then( result => { res.writeHead(200); res.end(JSON.stringify(result)); @@ -91,17 +104,6 @@ export class Askpass implements theia.Disposable { }); } - private async prompt(host: string, request: string): Promise { - const options: theia.InputBoxOptions = { - password: /password/i.test(request), - placeHolder: request, - prompt: `Git: ${host}`, - ignoreFocusOut: true, - }; - - return (await theia.window.showInputBox(options)) || ''; - } - async getEnv(): Promise { if (!this.enabled) { return { diff --git a/plugins/workspace-plugin/tests/askpass-prompt-manager.spec.ts b/plugins/workspace-plugin/tests/askpass-prompt-manager.spec.ts new file mode 100644 index 000000000..803677a23 --- /dev/null +++ b/plugins/workspace-plugin/tests/askpass-prompt-manager.spec.ts @@ -0,0 +1,52 @@ +/********************************************************************** + * Copyright (c) 2019-2021 Red Hat, Inc. + * + * This program and the accompanying materials are made + * available under the terms of the Eclipse Public License 2.0 + * which is available at https://www.eclipse.org/legal/epl-2.0/ + * + * SPDX-License-Identifier: EPL-2.0 + ***********************************************************************/ + +import { PromptManager } from '../src/askpass-prompt-manager'; + +describe('testing async prompt request', () => { + let iteration: number; + let promptManager: PromptManager; + + beforeEach(() => { + iteration = 0; + promptManager = new PromptManager( + (host: string, placeHolder: string) => + new Promise(resolve => { + iteration += 1; + setTimeout(resolve, Math.floor(Math.random() * 500) + 1000); + console.log(`resolving ${host} ${placeHolder} iteration: ${iteration}`); + resolve(`hello ${host} ${placeHolder} - iteration: ${iteration}`); + }) + ); + }); + + test('the prompt promise should be executed in sequence in the call order', async () => { + const askpass1promise = promptManager.askPass('host', 'username'); + const askpass2promise = promptManager.askPass('host', 'password'); + expect(await askpass2promise).toBe('hello host password - iteration: 2'); + expect(await askpass1promise).toBe('hello host username - iteration: 1'); + }); + + test('a prompt with the same host and placeHolder should return the same value and not be executed twice', async () => { + const askpass1promise = promptManager.askPass('host', 'username'); + const askpass2promise = promptManager.askPass('host', 'password'); + const askpass1promise_bis = promptManager.askPass('host', 'username'); + const askpass3promise = promptManager.askPass('host2', 'username'); + const askpass4promise = promptManager.askPass('host2', 'password'); + const askpass3promise_bis = promptManager.askPass('host2', 'username'); + + expect(await askpass2promise).toBe('hello host password - iteration: 2'); + expect(await askpass3promise).toBe('hello host2 username - iteration: 3'); + expect(await askpass1promise).toBe('hello host username - iteration: 1'); + expect(await askpass1promise_bis).toBe('hello host username - iteration: 1'); + expect(await askpass4promise).toBe('hello host2 password - iteration: 4'); + expect(await askpass3promise_bis).toBe('hello host2 username - iteration: 3'); + }); +}); diff --git a/yarn.lock b/yarn.lock index c2935e8f9..3fd6f80b8 100644 --- a/yarn.lock +++ b/yarn.lock @@ -4156,6 +4156,13 @@ async-limiter@~1.0.0: resolved "https://registry.yarnpkg.com/async-limiter/-/async-limiter-1.0.1.tgz#dd379e94f0db8310b08291f9d64c3209766617fd" integrity sha512-csOlWGAcRFJaI6m+F2WKdnMKr4HhdhFVBk0H/QbJFMCr+uO2kwohwXQPxw/9OCxp05r5ghVBFSyioixx3gfkNQ== +async-mutex@^0.2.6: + version "0.2.6" + resolved "https://registry.yarnpkg.com/async-mutex/-/async-mutex-0.2.6.tgz#0d7a3deb978bc2b984d5908a2038e1ae2e54ff40" + integrity sha512-Hs4R+4SPgamu6rSGW8C7cV9gaWUKEHykfzCCvIRuaVv636Ju10ZdeUbvb4TBEW0INuq2DHZqXbK4Nd3yG4RaRw== + dependencies: + tslib "^2.0.0" + async@^2.0.0, async@^2.6.2: version "2.6.3" resolved "https://registry.yarnpkg.com/async/-/async-2.6.3.tgz#d72625e2344a3656e3a3ad4fa749fa83299d82ff" @@ -14828,6 +14835,11 @@ tslib@^1.10.0, tslib@^1.8.1, tslib@^1.9.0: resolved "https://registry.yarnpkg.com/tslib/-/tslib-1.14.1.tgz#cf2d38bdc34a134bcaf1091c41f6619e2f672d00" integrity sha512-Xni35NKzjgMrwevysHTCArtLDpPvye8zV/0E4EyYn43P7/7qvQwPh9BGkHewbMulVntbigmcT7rdX3BNo9wRJg== +tslib@^2.0.0: + version "2.1.0" + resolved "https://registry.yarnpkg.com/tslib/-/tslib-2.1.0.tgz#da60860f1c2ecaa5703ab7d39bc05b6bf988b97a" + integrity sha512-hcVC3wYEziELGGmEEXue7D75zbwIIVUMWAVbHItGPx0ziyXxrOMQx4rQEVEV45Ut/1IotuEvwqPopzIOkDMf0A== + tsutils@^3.0.0, tsutils@^3.17.1: version "3.18.0" resolved "https://registry.yarnpkg.com/tsutils/-/tsutils-3.18.0.tgz#38add50a28ec97e988cb43c5b32e55d1ff4a222a"