Skip to content

Commit

Permalink
POC for avoiding waiting on vscode-microprofile
Browse files Browse the repository at this point in the history
Work in progress.
Instead of directly depending on vscode-microprofile,
send the user a notification with a button to install the extension.
The user can reject the installation.
They will be free to use:
 * qute syntax highlight
 * properties file highlight
 * project generator
but no new language features will appear,
and they will be prompted to install vscode-microprofile when attempting
to run commands that require it.

Since vscode-quarkus doesn't wait on vscode-microprofile starting
anymore, it can start the project generator without starting
vscode-microprofile or even vscode-java. This allows for it to work in
rootless mode.

Closes redhat-developer#323

Signed-off-by: David Thompson <[email protected]>
  • Loading branch information
datho7561 committed Jun 22, 2021
1 parent 68dc8f8 commit b0f62cd
Show file tree
Hide file tree
Showing 8 changed files with 192 additions and 31 deletions.
2 changes: 1 addition & 1 deletion package-lock.json

Some generated files are not rendered by default. Learn more about how customized files appear on GitHub.

1 change: 0 additions & 1 deletion package.json
Original file line number Diff line number Diff line change
Expand Up @@ -50,7 +50,6 @@
],
"main": "./dist/extension",
"extensionDependencies": [
"redhat.vscode-microprofile",
"redhat.java",
"vscjava.vscode-java-debug",
"redhat.vscode-commons"
Expand Down
12 changes: 11 additions & 1 deletion src/commands/registerCommands.ts
Original file line number Diff line number Diff line change
@@ -1,13 +1,14 @@
import { commands, ExtensionContext, window } from "vscode";
import { VSCodeCommands } from "../definitions/constants";
import { ProjectLabelInfo } from "../definitions/ProjectLabelInfo";
import { installMPExtForCommand, isToolsForMicroProfileInstalled, microProfileToolsStarted } from "../requirements/toolsForMicroProfile";
import { requestStandardMode } from "../utils/requestStandardMode";
import { sendCommandFailedTelemetry, sendCommandSucceededTelemetry } from "../utils/telemetryUtils";
import { WelcomeWebview } from "../webviews/WelcomeWebview";
import { addExtensionsWizard } from "../wizards/addExtensions/addExtensionsWizard";
import { startDebugging } from "../wizards/debugging/startDebugging";
import { generateProjectWizard } from "../wizards/generateProject/generationWizard";
import { deployToOpenShift } from "../wizards/deployToOpenShift/deployToOpenShift";
import { generateProjectWizard } from "../wizards/generateProject/generationWizard";

const NOT_A_QUARKUS_PROJECT = new Error('No Quarkus projects were detected in this folder');
const STANDARD_MODE_REQUEST_FAILED = new Error('Error occurred while requesting standard mode from the Java language server');
Expand Down Expand Up @@ -89,6 +90,15 @@ async function registerCommandWithTelemetry(context: ExtensionContext, commandNa
*/
function withStandardMode(commandAction: () => Promise<any>, commandDescription: string): () => Promise<void> {
return async () => {
if (!isToolsForMicroProfileInstalled()) {
await installMPExtForCommand(commandDescription);
// You need to reload the window after installing Tools for MicroProfile
// before any of the features are available.
// Return early instead of attempting to run the command.
return;
} else {
await microProfileToolsStarted();
}
let isStandardMode = false;
try {
isStandardMode = await requestStandardMode(commandDescription);
Expand Down
3 changes: 3 additions & 0 deletions src/extension.ts
Original file line number Diff line number Diff line change
Expand Up @@ -22,6 +22,7 @@ import { ProjectLabelInfo } from './definitions/ProjectLabelInfo';
import { PropertiesLanguageMismatch, QuarkusConfig } from './QuarkusConfig';
import { QuarkusContext } from './QuarkusContext';
import quarkusProjectListener from './QuarkusProjectListener';
import { installMPExtOnStartup } from './requirements/toolsForMicroProfile';
import { terminalCommandRunner } from './terminal/terminalCommandRunner';
import { WelcomeWebview } from './webviews/WelcomeWebview';
import { createTerminateDebugListener } from './wizards/debugging/terminateProcess';
Expand All @@ -35,6 +36,8 @@ export async function activate(context: ExtensionContext) {
displayWelcomePageIfNeeded(context);
commands.executeCommand('setContext', 'quarkusProjectExistsOrLightWeight', true);

installMPExtOnStartup();

context.subscriptions.push(createTerminateDebugListener());
quarkusProjectListener.getQuarkusProjectListener().then((disposableListener: Disposable) => {
context.subscriptions.push(disposableListener);
Expand Down
108 changes: 108 additions & 0 deletions src/requirements/toolsForMicroProfile.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1,108 @@
/**
* Copyright 2021 Red Hat, Inc. and others.
*
* Licensed under the Apache License, Version 2.0 (the "License");
* you may not use this file except in compliance with the License.
* You may obtain a copy of the License at
*
* http://www.apache.org/licenses/LICENSE-2.0
*
* Unless required by applicable law or agreed to in writing, software
* distributed under the License is distributed on an "AS IS" BASIS,
* WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
* See the License for the specific language governing permissions and
* limitations under the License.
*/
import { extensions, window } from "vscode";
import { QuarkusContext } from "../QuarkusContext";
import { installExtension, isExtensionInstalled } from "../utils/extensionInstallationUtils";

const TOOLS_FOR_MICRO_PROFILE_EXT = 'redhat.vscode-microprofile';

const STARTUP_INSTALL_MEMO = 'mpExtInstallOnStartup.isIgnored';

/**
* Prompts the user to install Tools for MicroProfile if they don't have it installed
*
* Allows the user to silence this prompt in the future by clicking on a button.
* Warns the user that only some functionality is available if they choose not to install vscode-microprofile.
*
* @returns when the user has installed Tools for MicroProfile,
* or the user has chosen not to install Tools for MicroProfile,
* or its detected that they've silenced this popup
*/
export async function installMPExtOnStartup(): Promise<void> {
if (isExtensionInstalled(TOOLS_FOR_MICRO_PROFILE_EXT)) {
return;
}
const installOnStartupIsIgnored = QuarkusContext.getExtensionContext().globalState.get(STARTUP_INSTALL_MEMO, false);
if (installOnStartupIsIgnored) {
return;
}
const YES = 'Install';
const NO = 'Don\'t install';
const NOT_AGAIN = 'Don\'t ask me again';
const result = await window.showWarningMessage('vscode-quarkus depends on Tools for MicroProfile for many of its features, '
+ 'but can provide some functionality without it. '
+ 'Install Tools for MicroProfile now? '
+ 'You will need to reload the window after the installation.', YES, NO, NOT_AGAIN);
if (result === YES) {
try {
await installExtension(TOOLS_FOR_MICRO_PROFILE_EXT);
} catch (e) {
window.showErrorMessage(e);
}
} else if (result === NOT_AGAIN) {
QuarkusContext.getExtensionContext().globalState.update(STARTUP_INSTALL_MEMO, true);
limitedFunctionalityWarning();
} else {
limitedFunctionalityWarning();
}
}

/**
* Installs Tools for MicroProfile with the user's permission, in order to use a given command
*
* @param commandDescription description of the command that requires Tools for MicroProfile in order to be used
* @returns when the user refuses to install,
* or when the install succeeds,
* or when the install fails
*/
export async function installMPExtForCommand(commandDescription: string) {
const YES = 'Install';
const NO = `Cancel ${commandDescription}`;
const result = await window.showWarningMessage(`${commandDescription} requires Tools for MicroProfile. Install it now? `
+ 'You will need to reload the window after the installation.',
YES, NO);
if (result === YES) {
try {
await installExtension(TOOLS_FOR_MICRO_PROFILE_EXT);
} catch (e) {
window.showErrorMessage(e);
}
} else {
window.showErrorMessage(`${commandDescription} requires Tools for MicroProfile, so it can't be run.`);
}
}

/**
* Returns true if Tools for MicroProfile is installed, and false otherwise
*
* @returns true if Tools for MicroProfile is installed, and false otherwise
*/
export function isToolsForMicroProfileInstalled(): boolean {
return isExtensionInstalled(TOOLS_FOR_MICRO_PROFILE_EXT);
}

/**
* Returns when Tools for MicroProfile has started
*
* @returns when Tools for MicroProfile has started
*/
export async function microProfileToolsStarted(): Promise<void> {
await extensions.getExtension(TOOLS_FOR_MICRO_PROFILE_EXT).activate();
}

async function limitedFunctionalityWarning(): Promise<void> {
await window.showInformationMessage('vscode-quarkus will run with limited functionality');
}
54 changes: 54 additions & 0 deletions src/utils/extensionInstallationUtils.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1,54 @@
/**
* Copyright 2021 Red Hat, Inc. and others.
*
* Licensed under the Apache License, Version 2.0 (the "License");
* you may not use this file except in compliance with the License.
* You may obtain a copy of the License at
*
* http://www.apache.org/licenses/LICENSE-2.0
*
* Unless required by applicable law or agreed to in writing, software
* distributed under the License is distributed on an "AS IS" BASIS,
* WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
* See the License for the specific language governing permissions and
* limitations under the License.
*/
import { commands, Disposable, extensions } from "vscode";

export const EXT_DOWNLOAD_TIMEOUT_ERROR = new Error('Extension installation is taking a while');

const DOWNLOAD_TIMEOUT = 60000;

/**
* Installs the extension with the given id
*
* @param extensionId the id (`"${publisher}.${name}"`) of the extension to check
* @returns when the extension is installed
* @throws `EXT_DOWNLOAD_TIMEOUT_ERROR` when the extension installation takes a while,
* or the extension installation fails
*/
export async function installExtension(extensionId: string): Promise<void> {
let installListenerDisposable: Disposable;
return new Promise<void>((resolve, reject) => {
installListenerDisposable = extensions.onDidChange(() => {
if (isExtensionInstalled(extensionId)) {
resolve();
}
});
commands.executeCommand("workbench.extensions.installExtension", extensionId)
.then((_unused: any) => { }, reject);
setTimeout(reject, DOWNLOAD_TIMEOUT, EXT_DOWNLOAD_TIMEOUT_ERROR);
}).finally(() => {
installListenerDisposable.dispose();
});
}

/**
* Returns true if the extension is installed and false otherwise
*
* @param extensionId the id (`"${publisher}.${name}"`) of the extension to check
* @returns true if the extension is installed and false otherwise
*/
export function isExtensionInstalled(extensionId: string): boolean {
return !!extensions.getExtension(extensionId);
}
36 changes: 11 additions & 25 deletions src/utils/openShiftConnectorUtils.ts
Original file line number Diff line number Diff line change
Expand Up @@ -13,20 +13,11 @@
* See the License for the specific language governing permissions and
* limitations under the License.
*/
import { commands, Disposable, Extension, extensions, ProgressLocation, Uri, window } from "vscode";
import { commands, Extension, extensions, ProgressLocation, Uri, window } from "vscode";
import { EXT_DOWNLOAD_TIMEOUT_ERROR, installExtension, isExtensionInstalled } from "./extensionInstallationUtils";

export const OPENSHIFT_CONNECTOR_EXTENSION_ID = 'redhat.vscode-openshift-connector';
export const OPENSHIFT_CONNECTOR = 'OpenShift Connector extension';
const DOWNLOAD_TIMEOUT = 60000; // Timeout for downloading VSCode OpenShift Connector, in milliseconds

/**
* Returns true if the OpenShift connector extension is installed, and false otherwise
*
* @returns true if the OpenShift connector extension is installed, and false otherwise
*/
export function isOpenShiftConnectorInstalled(): boolean {
return !!extensions.getExtension(OPENSHIFT_CONNECTOR_EXTENSION_ID);
}

/**
* Returns the OpenShift Connector extension API
Expand All @@ -35,7 +26,7 @@ export function isOpenShiftConnectorInstalled(): boolean {
* @returns the OpenShift Connector extension API
*/
export async function getOpenShiftConnector(): Promise<any> {
if (!isOpenShiftConnectorInstalled()) {
if (!isExtensionInstalled(OPENSHIFT_CONNECTOR_EXTENSION_ID)) {
throw new Error(`${OPENSHIFT_CONNECTOR} is not installed`);
}
const openShiftConnector: Extension<any> = extensions.getExtension(OPENSHIFT_CONNECTOR_EXTENSION_ID);
Expand All @@ -52,19 +43,14 @@ export async function getOpenShiftConnector(): Promise<any> {
* @throws if the user refuses to install the extension, or if the extension does not get installed within a timeout period
*/
async function installOpenShiftConnector(): Promise<void> {
let installListenerDisposable: Disposable;
return new Promise<void>((resolve, reject) => {
installListenerDisposable = extensions.onDidChange(() => {
if (isOpenShiftConnectorInstalled()) {
resolve();
}
});
commands.executeCommand("workbench.extensions.installExtension", OPENSHIFT_CONNECTOR_EXTENSION_ID)
.then((_unused: any) => { }, reject);
setTimeout(reject, DOWNLOAD_TIMEOUT, new Error(`${OPENSHIFT_CONNECTOR} installation is taking a while. Cancelling 'Deploy to OpenShift'. Please retry after the OpenShift Connector installation has finished`));
}).finally(() => {
installListenerDisposable.dispose();
});
try {
installExtension(OPENSHIFT_CONNECTOR_EXTENSION_ID);
} catch (e) {
if (e === EXT_DOWNLOAD_TIMEOUT_ERROR) {
throw new Error(`${OPENSHIFT_CONNECTOR} installation is taking a while. Cancelling 'Deploy to OpenShift'. Please retry after the OpenShift Connector installation has finished`);
}
throw e;
}
}

/**
Expand Down
7 changes: 4 additions & 3 deletions src/wizards/deployToOpenShift/deployToOpenShift.ts
Original file line number Diff line number Diff line change
Expand Up @@ -15,7 +15,8 @@
*/
import { Uri, window } from "vscode";
import { ProjectLabelInfo } from "../../definitions/ProjectLabelInfo";
import { deployQuarkusProject, getOpenShiftConnector, installOpenShiftConnectorWithProgress, isOpenShiftConnectorInstalled, OPENSHIFT_CONNECTOR } from "../../utils/openShiftConnectorUtils";
import { isExtensionInstalled } from "../../utils/extensionInstallationUtils";
import { deployQuarkusProject, getOpenShiftConnector, installOpenShiftConnectorWithProgress, OPENSHIFT_CONNECTOR, OPENSHIFT_CONNECTOR_EXTENSION_ID } from "../../utils/openShiftConnectorUtils";
import { getQuarkusProject } from "../getQuarkusProject";

/**
Expand All @@ -38,7 +39,7 @@ export async function deployToOpenShift(): Promise<void> {
* @returns the OpenShift Connector extension API
*/
async function installOpenShiftConnectorIfNeeded(): Promise<any> {
if (isOpenShiftConnectorInstalled()) {
if (isExtensionInstalled(OPENSHIFT_CONNECTOR_EXTENSION_ID)) {
return getOpenShiftConnector();
}
return askToInstallOpenShiftConnector();
Expand All @@ -57,7 +58,7 @@ async function askToInstallOpenShiftConnector(): Promise<any> {
if (response === YES) {
try {
await installOpenShiftConnectorWithProgress();
if (isOpenShiftConnectorInstalled()) {
if (isExtensionInstalled(OPENSHIFT_CONNECTOR_EXTENSION_ID)) {
return getOpenShiftConnector();
}
} catch (e) {
Expand Down

0 comments on commit b0f62cd

Please sign in to comment.