From 349918c05e890822755b9ec3328cdc049eabd10e Mon Sep 17 00:00:00 2001 From: Andrew Bowley Date: Thu, 6 Feb 2020 11:51:08 +1100 Subject: [PATCH] Preference control misbehaves #164 & Moving forward on portability #169 Signed-off-by: Andrew Bowley * Two new OS-specific packages org.eclipse.dartboard.os.linux/windows * New PlatformUtil class hides portability solution * New DartSdkChecker and SdkLocator classes support preferences access to SDK installations * SDKLocator (note different to SdkLocator) no longer needed, so removed * Solved preference field editor issues with executing shell command in validation eg. re-entry caused by SWT event handling * Improvements DartPreferencePageTest including new WaitCondition for transition from displaying error to clearing the error --- org.eclipse.dartboard.releng/pom.xml | 4 +- .../preference/DartPreferencePageTest.java | 67 +++++- .../preference/PreferenceTestConstants.java | 42 ++++ .../test/preference/ValidPreferenceState.java | 26 +++ .../test/preference/WaitForValidState.java | 58 +++++ .../os/linux/PlatformDartSdkChecker.java | 58 +++++ .../dartboard/os/linux/PlatformFactory.java | 52 +++++ .../os/linux/PlatformSdkLocator.java | 64 +++++ .../os/windows/PlatformDartSdkChecker.java | 58 +++++ .../dartboard/os/windows/PlatformFactory.java | 52 +++++ .../os/windows/PlatformSdkLocator.java | 47 ++++ .../DartPreferenceInitializer.java | 56 +++-- .../DartSDKLocationFieldEditor.java | 221 ++++++++++++------ .../FlutterSDKLocationFieldEditor.java | 213 +++++++++++++---- .../eclipse/dartboard/util/BusyCursor.java | 172 ++++++++++++++ .../dartboard/util/DartSdkChecker.java | 178 ++++++++++++++ .../dartboard/util/IPlatformFactory.java | 36 +++ .../eclipse/dartboard/util/PlatformUtil.java | 140 +++++++++++ .../eclipse/dartboard/util/SDKLocator.java | 81 ------- .../eclipse/dartboard/util/SdkLocator.java | 135 +++++++++++ 20 files changed, 1527 insertions(+), 233 deletions(-) create mode 100644 org.eclipse.dartboard.test/src/org/eclipse/dartboard/test/preference/PreferenceTestConstants.java create mode 100644 org.eclipse.dartboard.test/src/org/eclipse/dartboard/test/preference/ValidPreferenceState.java create mode 100644 org.eclipse.dartboard.test/src/org/eclipse/dartboard/test/preference/WaitForValidState.java create mode 100644 org.eclipse.dartboard/src/org/eclipse/dartboard/os/linux/PlatformDartSdkChecker.java create mode 100644 org.eclipse.dartboard/src/org/eclipse/dartboard/os/linux/PlatformFactory.java create mode 100644 org.eclipse.dartboard/src/org/eclipse/dartboard/os/linux/PlatformSdkLocator.java create mode 100644 org.eclipse.dartboard/src/org/eclipse/dartboard/os/windows/PlatformDartSdkChecker.java create mode 100644 org.eclipse.dartboard/src/org/eclipse/dartboard/os/windows/PlatformFactory.java create mode 100644 org.eclipse.dartboard/src/org/eclipse/dartboard/os/windows/PlatformSdkLocator.java create mode 100644 org.eclipse.dartboard/src/org/eclipse/dartboard/util/BusyCursor.java create mode 100644 org.eclipse.dartboard/src/org/eclipse/dartboard/util/DartSdkChecker.java create mode 100644 org.eclipse.dartboard/src/org/eclipse/dartboard/util/IPlatformFactory.java create mode 100644 org.eclipse.dartboard/src/org/eclipse/dartboard/util/PlatformUtil.java delete mode 100644 org.eclipse.dartboard/src/org/eclipse/dartboard/util/SDKLocator.java create mode 100644 org.eclipse.dartboard/src/org/eclipse/dartboard/util/SdkLocator.java diff --git a/org.eclipse.dartboard.releng/pom.xml b/org.eclipse.dartboard.releng/pom.xml index b07fc25..a3507df 100644 --- a/org.eclipse.dartboard.releng/pom.xml +++ b/org.eclipse.dartboard.releng/pom.xml @@ -19,7 +19,7 @@ pom - 1.6.0-SNAPSHOT + 1.6.0 UTF-8 http://download.eclipse.org/releases/2020-03 @@ -27,7 +27,7 @@ http://download.eclipse.org/lsp4e/releases/latest/ http://download.eclipse.org/wildwebdeveloper/snapshots https://download.eclipse.org/tools/orbit/downloads/drops/R20191126223242/repository - 1.6.0-SNAPSHOT + 1.6.0 diff --git a/org.eclipse.dartboard.test/src/org/eclipse/dartboard/test/preference/DartPreferencePageTest.java b/org.eclipse.dartboard.test/src/org/eclipse/dartboard/test/preference/DartPreferencePageTest.java index e6f9a2f..51b44f2 100644 --- a/org.eclipse.dartboard.test/src/org/eclipse/dartboard/test/preference/DartPreferencePageTest.java +++ b/org.eclipse.dartboard.test/src/org/eclipse/dartboard/test/preference/DartPreferencePageTest.java @@ -16,15 +16,17 @@ import static org.junit.Assert.assertEquals; import static org.junit.Assert.assertFalse; import static org.junit.Assert.assertTrue; +import static org.junit.Assume.assumeTrue; -import org.eclipse.core.runtime.Platform; import org.eclipse.dartboard.test.util.DefaultPreferences; +import org.eclipse.reddeer.common.wait.WaitUntil; import org.eclipse.reddeer.core.reference.ReferencedComposite; import org.eclipse.reddeer.jface.preference.PreferenceDialog; import org.eclipse.reddeer.jface.preference.PreferencePage; import org.eclipse.reddeer.junit.runner.RedDeerSuite; import org.eclipse.reddeer.swt.impl.button.CheckBox; import org.eclipse.reddeer.swt.impl.button.RadioButton; +import org.eclipse.reddeer.swt.impl.clabel.DefaultCLabel; import org.eclipse.reddeer.swt.impl.text.DefaultText; import org.eclipse.reddeer.swt.impl.text.LabeledText; import org.eclipse.reddeer.workbench.ui.dialogs.WorkbenchPreferenceDialog; @@ -35,16 +37,29 @@ @RunWith(RedDeerSuite.class) public class DartPreferencePageTest { - /** Dart SDK location is operating system specific. Here catering for Linuz and Windows */ - private static String DART_SDK_LOC; + static private final String DIALOG_TITLE = "Dart and Flutter"; + /** + * Dart SDK location is operating system specific. Here catering for Linux and + * Windows + */ private PreferenceDialog preferenceDialog; private DartPreferencePage preferencePage; @Before public void setup() { - DART_SDK_LOC = Platform.getOS().equals(Platform.OS_WIN32) ? "C:\\Program Files\\Dart\\dart-sdk" : "/usr/lib/dart"; + boolean firstTime = preferenceDialog == null; + if (firstTime) {// First time - clear settings from previous test session + DefaultPreferences.resetPreferences(); + } + preferenceDialog = new WorkbenchPreferenceDialog(); + if (firstTime) { + // Make sure preferences dialog is closed before test commences + if (preferenceDialog.isOpen()) { + preferenceDialog.cancel(); + } + } preferencePage = new DartPreferencePage(preferenceDialog); preferenceDialog.open(); preferenceDialog.select(preferencePage); @@ -56,29 +71,46 @@ public void tearDown() { if (preferenceDialog.isOpen()) { preferenceDialog.cancel(); } + } + public void doAllTests() throws Exception { + dartPreferencePage__DefaultPreferences__CorrectDefaultsAreDisplayed(); + dartPreferencePage__InvalidToValidSDKLocation_PageIsNotValidThenOk(); } @Test public void dartPreferencePage__DefaultPreferences__CorrectDefaultsAreDisplayed() throws Exception { + assumeTrue(PreferenceTestConstants.DEFAULT_FLUTTER_LOCATION != null); + assertEquals(PreferenceTestConstants.DEFAULT_FLUTTER_LOCATION, preferencePage.getFlutterSDKLocation()); assertTrue("Auto pub synchronization not selected", preferencePage.isAutoPubSynchronization()); assertFalse("Use offline pub is selected", preferencePage.isUseOfflinePub()); preferencePage.setPluginMode("Dart"); - assertEquals(DART_SDK_LOC, preferencePage.getSDKLocation()); + assertEquals(PreferenceTestConstants.DEFAULT_DART_LOCATION, preferencePage.getDartSDKLocation()); } @Test - public void dartPreferencePage__InvalidSDKLocation__PageIsNotValid() throws Exception { + public void dartPreferencePage__InvalidToValidSDKLocation_PageIsNotValidThenOk() throws Exception { preferencePage.setPluginMode("Dart"); - preferencePage.setSDKLocation("some-random-test-location/path-segment"); + String result = preferencePage.getDartSDKLocation(); + assertTrue(PreferenceTestConstants.DEFAULT_DART_LOCATION.equals(result)); + // Change away from default so it can be changed back + preferencePage.setSDKLocation(PreferenceTestConstants.INVALID_SDK_LOCATION); assertTrue(preferencePage.isShowingSDKInvalidError()); + // Always leave an open preference page set to a valid value or a modal dialog pops up + // warning the page has an invalid value. RedDeer is unable to close the modal + // dialog because it is native. + preferencePage.setSDKLocation(PreferenceTestConstants.DEFAULT_DART_LOCATION); + new WaitUntil(new WaitForValidState(preferencePage, DIALOG_TITLE)); + result = preferencePage.getDartSDKLocation(); + assertTrue(PreferenceTestConstants.DEFAULT_DART_LOCATION.equals(result)); } - public class DartPreferencePage extends PreferencePage { + + public class DartPreferencePage extends PreferencePage implements ValidPreferenceState { public DartPreferencePage(ReferencedComposite referencedComposite) { - super(referencedComposite, "Dart and Flutter"); + super(referencedComposite, DIALOG_TITLE); } public DartPreferencePage setSDKLocation(String text) { @@ -86,10 +118,14 @@ public DartPreferencePage setSDKLocation(String text) { return this; } - public String getSDKLocation() { + public String getDartSDKLocation() { return new LabeledText("Dart SDK Location:").getText(); } + public String getFlutterSDKLocation() { + return new LabeledText("Flutter SDK Location:").getText(); + } + public DartPreferencePage setAutoPubSynchronization(boolean value) { new CheckBox("Automatic Pub dependency synchronization").toggle(value); return this; @@ -125,6 +161,15 @@ public boolean isShowingSDKInvalidError() { return false; } } + + @Override + public boolean isValid() { + try { + new DefaultCLabel(DIALOG_TITLE); + return true; + } catch (Exception e) { + return false; + } + } } - } diff --git a/org.eclipse.dartboard.test/src/org/eclipse/dartboard/test/preference/PreferenceTestConstants.java b/org.eclipse.dartboard.test/src/org/eclipse/dartboard/test/preference/PreferenceTestConstants.java new file mode 100644 index 0000000..6614709 --- /dev/null +++ b/org.eclipse.dartboard.test/src/org/eclipse/dartboard/test/preference/PreferenceTestConstants.java @@ -0,0 +1,42 @@ +/******************************************************************************* + * Copyright (c) 2020 vogella GmbH and others. + * + * This program and the accompanying materials + * are made available under the terms of the Eclipse Public License 2.0 + * which accompanies this distribution, and is available at + * https://www.eclipse.org/legal/epl-2.0/ + * + * SPDX-License-Identifier: EPL-2.0 + * + * Contributors: + * Jonas Hungershausen - initial API and implementation + *******************************************************************************/ +package org.eclipse.dartboard.test.preference; + +import java.nio.file.Path; +import java.util.Optional; +import java.util.concurrent.ExecutionException; + +import org.eclipse.dartboard.util.PlatformUtil; + +public class PreferenceTestConstants { + + private static String OS = System.getProperty("os.name").toLowerCase(); + + public static final String DEFAULT_DART_LOCATION; + public static final String INVALID_SDK_LOCATION = "some-random-test-location/path-segment"; + + public static final String DEFAULT_FLUTTER_LOCATION; + + static { + boolean isWindows = OS.indexOf("win") >= 0; + DEFAULT_DART_LOCATION = isWindows ? "C:\\Program Files\\Dart\\dart-sdk" : "/usr/lib/dart"; + Optional flutterSdkLocation = null; + try { + flutterSdkLocation = PlatformUtil.getInstance().getLocation("flutter"); + } catch (ExecutionException e) { + } + boolean isFlutterAvailable = (flutterSdkLocation != null) && flutterSdkLocation.isPresent(); + DEFAULT_FLUTTER_LOCATION = isFlutterAvailable ? flutterSdkLocation.get().toString() : null; + } +} \ No newline at end of file diff --git a/org.eclipse.dartboard.test/src/org/eclipse/dartboard/test/preference/ValidPreferenceState.java b/org.eclipse.dartboard.test/src/org/eclipse/dartboard/test/preference/ValidPreferenceState.java new file mode 100644 index 0000000..246293e --- /dev/null +++ b/org.eclipse.dartboard.test/src/org/eclipse/dartboard/test/preference/ValidPreferenceState.java @@ -0,0 +1,26 @@ +/******************************************************************************* + * Copyright (c) 2020 vogella GmbH and others. + * + * This program and the accompanying materials + * are made available under the terms of the Eclipse Public License 2.0 + * which accompanies this distribution, and is available at + * https://www.eclipse.org/legal/epl-2.0/ + * + * SPDX-License-Identifier: EPL-2.0 + * + * Contributors: + * Andrew Bowley + *******************************************************************************/ +package org.eclipse.dartboard.test.preference; + +/** + * Interface for preference dialog which can flag when it is displaying it's + * valid state + * + * @author Andrew Bowley + * + */ +public interface ValidPreferenceState { + + boolean isValid(); +} diff --git a/org.eclipse.dartboard.test/src/org/eclipse/dartboard/test/preference/WaitForValidState.java b/org.eclipse.dartboard.test/src/org/eclipse/dartboard/test/preference/WaitForValidState.java new file mode 100644 index 0000000..9d813a1 --- /dev/null +++ b/org.eclipse.dartboard.test/src/org/eclipse/dartboard/test/preference/WaitForValidState.java @@ -0,0 +1,58 @@ +package org.eclipse.dartboard.test.preference; + +import org.eclipse.reddeer.common.condition.WaitCondition; + +/** + * RedDeer WaitCondition implementation for wait for preference dialog to + * display it's valid state. This normally means the dialog header displays a + * title rather than an error message. + * + * @author Andrew Bowley + * + */ +public class WaitForValidState implements WaitCondition { + + private final String title; + private final ValidPreferenceState preferencePage; + private boolean isValid; + + /** + * Construct WaitForValidState object + * + * @param preferencePage Preference page implementing ValidPreferenceState + * interface + * @param title Title displayed in dialog header + */ + public WaitForValidState(ValidPreferenceState preferencePage, String title) { + this.preferencePage = preferencePage; + this.title = title; + } + + @Override + public boolean test() { + isValid = preferencePage.isValid(); + return isValid; + } + + @SuppressWarnings("unchecked") + @Override + public Boolean getResult() { + return isValid; + } + + @Override + public String description() { + return title + " preference dialog transition to valid state"; + } + + @Override + public String errorMessageWhile() { + return "Waiting for " + description(); + } + + @Override + public String errorMessageUntil() { + return "Until " + description(); + } + +} diff --git a/org.eclipse.dartboard/src/org/eclipse/dartboard/os/linux/PlatformDartSdkChecker.java b/org.eclipse.dartboard/src/org/eclipse/dartboard/os/linux/PlatformDartSdkChecker.java new file mode 100644 index 0000000..1cb03fa --- /dev/null +++ b/org.eclipse.dartboard/src/org/eclipse/dartboard/os/linux/PlatformDartSdkChecker.java @@ -0,0 +1,58 @@ +/******************************************************************************* + * Copyright (c) 2020 vogella GmbH and others. + * + * This program and the accompanying materials + * are made available under the terms of the Eclipse Public License 2.0 + * which accompanies this distribution, and is available at + * https://www.eclipse.org/legal/epl-2.0/ + * + * SPDX-License-Identifier: EPL-2.0 + * + * Contributors: + * Andrew Bowley + *******************************************************************************/ +package org.eclipse.dartboard.os.linux; + +import java.io.File; +import java.util.List; + +import org.eclipse.dartboard.util.DartSdkChecker; +import org.eclipse.swt.widgets.Shell; + +import com.google.common.collect.Lists; + +/** + * Checks if a given Linux location contains a Dart SDK + * + * @author Andrew Bowley + * + */ +@SuppressWarnings("nls") +public class PlatformDartSdkChecker extends DartSdkChecker { + + /** + * Construct LinuxDartSdkChecker object + * + * @param shell Parent shell of owner or null if none + * @param isFlutter Flag set true if Dart SDK is inside Flutter + */ + public PlatformDartSdkChecker(Shell shell, boolean isFlutter) { + super(shell, !isFlutter ? "bin" + : "bin" + File.separator + "cache" + File.separator + "dart-sdk" + File.separator + "bin"); + } + + @Override + public String[] getDartVersionCommands(String executablePath) { + return new String[] { "/bin/bash", "-c", executablePath + " --version" }; + } + + @Override + public List getBlacklist() { + return Lists.newArrayList("/bin/dart", "/usr/bin/dart"); + } + + @Override + public String getDartExecutable() { + return "dart"; //$NON-NLS-1$ + } +} diff --git a/org.eclipse.dartboard/src/org/eclipse/dartboard/os/linux/PlatformFactory.java b/org.eclipse.dartboard/src/org/eclipse/dartboard/os/linux/PlatformFactory.java new file mode 100644 index 0000000..70c5295 --- /dev/null +++ b/org.eclipse.dartboard/src/org/eclipse/dartboard/os/linux/PlatformFactory.java @@ -0,0 +1,52 @@ +/******************************************************************************* + * Copyright (c) 2020 vogella GmbH and others. + * + * This program and the accompanying materials + * are made available under the terms of the Eclipse Public License 2.0 + * which accompanies this distribution, and is available at + * https://www.eclipse.org/legal/epl-2.0/ + * + * SPDX-License-Identifier: EPL-2.0 + * + * Contributors: + * Andrew Bowley + *******************************************************************************/ +package org.eclipse.dartboard.os.linux; + +import org.eclipse.dartboard.util.DartSdkChecker; +import org.eclipse.dartboard.util.IPlatformFactory; +import org.eclipse.dartboard.util.SdkLocator; +import org.eclipse.swt.widgets.Shell; + + public class PlatformFactory implements IPlatformFactory { + + private final PlatformSdkLocator sdkLocator; + + public PlatformFactory() { + sdkLocator = new PlatformSdkLocator(); + } + + @Override + public DartSdkChecker getDartSdkChecker(Shell shell, boolean isFlutter) { + + /** + * Returns a Dart SDK checker + * + * @param shell Parent shell of owner or null if none + * @param isFlutter Flag set true if Dart SDK is inside Flutter + * @return DartSdkChecker object + */ + return new PlatformDartSdkChecker(shell, isFlutter); + + } + + /** + * Returns support for locating SDK artifacts + * + * @return SdkLocator + */ + @Override + public SdkLocator getSdkLocator() { + return sdkLocator; + } +} diff --git a/org.eclipse.dartboard/src/org/eclipse/dartboard/os/linux/PlatformSdkLocator.java b/org.eclipse.dartboard/src/org/eclipse/dartboard/os/linux/PlatformSdkLocator.java new file mode 100644 index 0000000..70c37bb --- /dev/null +++ b/org.eclipse.dartboard/src/org/eclipse/dartboard/os/linux/PlatformSdkLocator.java @@ -0,0 +1,64 @@ +/******************************************************************************* + * Copyright (c) 2020 vogella GmbH and others. + * + * This program and the accompanying materials + * are made available under the terms of the Eclipse Public License 2.0 + * which accompanies this distribution, and is available at + * https://www.eclipse.org/legal/epl-2.0/ + * + * SPDX-License-Identifier: EPL-2.0 + * + * Contributors: + * Andrew Bowley + *******************************************************************************/ +package org.eclipse.dartboard.os.linux; + +import java.io.BufferedReader; +import java.io.File; +import java.io.IOException; +import java.io.InputStreamReader; +import java.util.concurrent.ExecutionException; + +import org.eclipse.dartboard.util.SdkLocator; + +public class PlatformSdkLocator extends SdkLocator { + + public PlatformSdkLocator() { + } + + @Override + public String resolveToolPath(String sdkLocation, String name) { + return sdkLocation + File.separator + "bin" + File.separator + name; //$NON-NLS-1$ + } + + @Override + public String resolveExecutablePath(String sdkLocation, String name) { + return resolveToolPath(sdkLocation, name); + } + + @Override + public String[] getLocationCommand(String program, boolean interactive) throws ExecutionException { + String shell = null; + try { + shell = getShell(); + } catch (IOException | InterruptedException e) { + throw new ExecutionException("Error obtaining shell command", e); //$NON-NLS-1$ + } + if (interactive) { + return new String[] { shell, "-i", "-c", "which " + program }; //$NON-NLS-1$ //$NON-NLS-2$ //$NON-NLS-3$ + } else { + return new String[] { shell, "-c", "which " + program }; //$NON-NLS-1$ //$NON-NLS-2$ + } + } + + public static String getShell() throws IOException, InterruptedException { + ProcessBuilder builder = new ProcessBuilder("/bin/sh", "-c", "echo $SHELL"); //$NON-NLS-1$ //$NON-NLS-2$ //$NON-NLS-3$ + Process process = builder.start(); + process.waitFor(); + try (BufferedReader reader = new BufferedReader(new InputStreamReader(process.getInputStream()))) { + String location = reader.readLine(); + return location; + } + } + +} diff --git a/org.eclipse.dartboard/src/org/eclipse/dartboard/os/windows/PlatformDartSdkChecker.java b/org.eclipse.dartboard/src/org/eclipse/dartboard/os/windows/PlatformDartSdkChecker.java new file mode 100644 index 0000000..2477292 --- /dev/null +++ b/org.eclipse.dartboard/src/org/eclipse/dartboard/os/windows/PlatformDartSdkChecker.java @@ -0,0 +1,58 @@ +/******************************************************************************* + * Copyright (c) 2020 vogella GmbH and others. + * + * This program and the accompanying materials + * are made available under the terms of the Eclipse Public License 2.0 + * which accompanies this distribution, and is available at + * https://www.eclipse.org/legal/epl-2.0/ + * + * SPDX-License-Identifier: EPL-2.0 + * + * Contributors: + * Andrew Bowley + *******************************************************************************/ +package org.eclipse.dartboard.os.windows; + +import java.io.File; +import java.util.Collections; +import java.util.List; + +import org.eclipse.dartboard.util.DartSdkChecker; +import org.eclipse.swt.widgets.Shell; + +/** + * Checks if a given Windows location contains a Dart SDK + * + * @author Andrew Bowley + * + */ +@SuppressWarnings("nls") +public class PlatformDartSdkChecker extends DartSdkChecker { + + /** + * Construct PlatformDartSdkChecker object + * + * @param shell Parent shell of owner or null if none + * @param isFlutter Flag set true if Dart SDK is inside Flutter + */ + public PlatformDartSdkChecker(Shell shell, boolean isFlutter) { + super(shell, !isFlutter ? "bin" + : "bin" + File.separator + "cache" + File.separator + "dart-sdk" + File.separator + "bin"); + } + + @Override + public String[] getDartVersionCommands(String executablePath) { + return new String[] { "cmd", "/c", executablePath, "--version" }; + } + + @Override + public List getBlacklist() { + return Collections.emptyList(); + } + + @Override + public String getDartExecutable() { + return "dart.exe"; //$NON-NLS-1$ + } + +} diff --git a/org.eclipse.dartboard/src/org/eclipse/dartboard/os/windows/PlatformFactory.java b/org.eclipse.dartboard/src/org/eclipse/dartboard/os/windows/PlatformFactory.java new file mode 100644 index 0000000..7a76e73 --- /dev/null +++ b/org.eclipse.dartboard/src/org/eclipse/dartboard/os/windows/PlatformFactory.java @@ -0,0 +1,52 @@ +/******************************************************************************* + * Copyright (c) 2020 vogella GmbH and others. + * + * This program and the accompanying materials + * are made available under the terms of the Eclipse Public License 2.0 + * which accompanies this distribution, and is available at + * https://www.eclipse.org/legal/epl-2.0/ + * + * SPDX-License-Identifier: EPL-2.0 + * + * Contributors: + * Andrew Bowley + *******************************************************************************/ +package org.eclipse.dartboard.os.windows; + +import org.eclipse.dartboard.util.DartSdkChecker; +import org.eclipse.dartboard.util.IPlatformFactory; +import org.eclipse.dartboard.util.SdkLocator; +import org.eclipse.swt.widgets.Shell; + + public class PlatformFactory implements IPlatformFactory { + + private final PlatformSdkLocator sdkLocator; + + public PlatformFactory() { + sdkLocator = new PlatformSdkLocator(); + } + + @Override + public DartSdkChecker getDartSdkChecker(Shell shell, boolean isFlutter) { + + /** + * Returns a Dart SDK checker + * + * @param shell Parent shell of owner or null if none + * @param isFlutter Flag set true if Dart SDK is inside Flutter + * @return DartSdkChecker object + */ + return new PlatformDartSdkChecker(shell, isFlutter); + + } + + /** + * Returns support for locating SDK artifacts + * + * @return SdkLocator + */ + @Override + public SdkLocator getSdkLocator() { + return sdkLocator; + } +} diff --git a/org.eclipse.dartboard/src/org/eclipse/dartboard/os/windows/PlatformSdkLocator.java b/org.eclipse.dartboard/src/org/eclipse/dartboard/os/windows/PlatformSdkLocator.java new file mode 100644 index 0000000..d2fc78e --- /dev/null +++ b/org.eclipse.dartboard/src/org/eclipse/dartboard/os/windows/PlatformSdkLocator.java @@ -0,0 +1,47 @@ +/******************************************************************************* + * Copyright (c) 2020 vogella GmbH and others. + * + * This program and the accompanying materials + * are made available under the terms of the Eclipse Public License 2.0 + * which accompanies this distribution, and is available at + * https://www.eclipse.org/legal/epl-2.0/ + * + * SPDX-License-Identifier: EPL-2.0 + * + * Contributors: + * Andrew Bowley + *******************************************************************************/ +package org.eclipse.dartboard.os.windows; + +import java.io.File; +import java.util.concurrent.ExecutionException; + +import org.eclipse.dartboard.util.SdkLocator; + +@SuppressWarnings("nls") +public class PlatformSdkLocator extends SdkLocator { + + public PlatformSdkLocator() { + } + + @Override + public String resolveToolPath(String sdkLocation, String name) { + if (!name.endsWith(".bat")) + name += ".bat"; + return sdkLocation + File.separator + "bin" + File.separator + name; + } + + @Override + public String resolveExecutablePath(String sdkLocation, String name) { + if (!name.endsWith(".exe")) + name += ".exe"; + return sdkLocation + File.separator + "bin" + File.separator + name; + } + + @Override + public String[] getLocationCommand(String program, boolean interactive) throws ExecutionException { + return new String[] { "cmd", "/c", "where " + program }; + } + + +} diff --git a/org.eclipse.dartboard/src/org/eclipse/dartboard/preferences/DartPreferenceInitializer.java b/org.eclipse.dartboard/src/org/eclipse/dartboard/preferences/DartPreferenceInitializer.java index 1216f5e..c0bed8d 100644 --- a/org.eclipse.dartboard/src/org/eclipse/dartboard/preferences/DartPreferenceInitializer.java +++ b/org.eclipse.dartboard/src/org/eclipse/dartboard/preferences/DartPreferenceInitializer.java @@ -13,9 +13,9 @@ *******************************************************************************/ package org.eclipse.dartboard.preferences; -import java.io.IOException; import java.nio.file.Path; import java.util.Optional; +import java.util.concurrent.ExecutionException; import org.eclipse.core.runtime.ILog; import org.eclipse.core.runtime.Platform; @@ -23,7 +23,7 @@ import org.eclipse.dartboard.logging.DartLog; import org.eclipse.dartboard.messages.Messages; import org.eclipse.dartboard.util.GlobalConstants; -import org.eclipse.dartboard.util.SDKLocator; +import org.eclipse.dartboard.util.PlatformUtil; import org.eclipse.jface.dialogs.MessageDialog; import org.eclipse.ui.preferences.ScopedPreferenceStore; @@ -46,15 +46,15 @@ public void initializeDefaultPreferences() { boolean anySdkFound = false; if (scopedPreferenceStore.getString(GlobalConstants.P_SDK_LOCATION_FLUTTER).isEmpty()) { - Optional binLocation; + Optional sdkLocation; try { - binLocation = SDKLocator.getFlutterLocation(); // $NON-NLS-1$ - } catch (IOException | InterruptedException e) { - LOG.log(DartLog.createError("Could not retrieve flutter location", e)); //$NON-NLS-1$ - binLocation = Optional.empty(); + sdkLocation = getFlutterLocation(); // $NON-NLS-1$ + } catch (ExecutionException e) { + LOG.log(DartLog.createError("Could not retrieve flutter location", e.getCause())); //$NON-NLS-1$ + sdkLocation = Optional.empty(); } - if (binLocation.isPresent()) { - Path sdkPath = binLocation.get().getParent(); + if (sdkLocation.isPresent()) { + Path sdkPath = sdkLocation.get(); scopedPreferenceStore.setDefault(GlobalConstants.P_SDK_LOCATION_FLUTTER, sdkPath.toString()); scopedPreferenceStore.setValue(GlobalConstants.P_FLUTTER_ENABLED, true); anySdkFound = true; @@ -67,15 +67,15 @@ public void initializeDefaultPreferences() { anySdkFound = true; } if (scopedPreferenceStore.getString(GlobalConstants.P_SDK_LOCATION_DART).isEmpty()) { - Optional binLocation; + Optional sdkLocation; try { - binLocation = SDKLocator.getDartLocation(); // $NON-NLS-1$ - } catch (IOException | InterruptedException e) { - LOG.log(DartLog.createError("Could not retrieve flutter location", e)); //$NON-NLS-1$ - binLocation = Optional.empty(); + sdkLocation = getDartLocation(); // $NON-NLS-1$ + } catch (ExecutionException e) { + LOG.log(DartLog.createError("Could not retrieve flutter location", e.getCause())); //$NON-NLS-1$ + sdkLocation = Optional.empty(); } - if (binLocation.isPresent()) { - Path sdkPath = binLocation.get().getParent(); + if (sdkLocation.isPresent()) { + Path sdkPath = sdkLocation.get(); scopedPreferenceStore.setDefault(GlobalConstants.P_SDK_LOCATION_DART, sdkPath.toString()); anySdkFound = true; } @@ -88,4 +88,28 @@ public void initializeDefaultPreferences() { scopedPreferenceStore.setDefault(GlobalConstants.P_SYNC_PUB, true); scopedPreferenceStore.setDefault(GlobalConstants.P_OFFLINE_PUB, false); } + + /** + * Returns a {@link Path} containing the location of the Dart SDK folder. + * + * This method finds the location of the Dart SDK on the system, if installed. + * On *nix based systems it tries to locate the Dart binary by using the + * {@code which} command. Typically the output is a symbolic link to the actual + * binary. Since the Dart SDK installation folder contains more binaries that we + * need, we resolve the symbolic link and return the path to the parent of the + * /bin directory inside the SDK installation folder. + * + * On Windows this method uses the where command to locate the binary. + * + * @return - An {@link Optional} of {@link Path} containing the path to the + * {@code /bin} folder inside the Dart SDK installation directory or + * empty if the SDK is not found on the host machine. + */ + public static Optional getDartLocation() throws ExecutionException { + return PlatformUtil.getInstance().getLocation("dart", false); //$NON-NLS-1$ + } + + public static Optional getFlutterLocation() throws ExecutionException { + return PlatformUtil.getInstance().getLocation("flutter", true); //$NON-NLS-1$ + } } diff --git a/org.eclipse.dartboard/src/org/eclipse/dartboard/preferences/DartSDKLocationFieldEditor.java b/org.eclipse.dartboard/src/org/eclipse/dartboard/preferences/DartSDKLocationFieldEditor.java index 0cdd02c..421925d 100644 --- a/org.eclipse.dartboard/src/org/eclipse/dartboard/preferences/DartSDKLocationFieldEditor.java +++ b/org.eclipse.dartboard/src/org/eclipse/dartboard/preferences/DartSDKLocationFieldEditor.java @@ -10,54 +10,178 @@ * * Contributors: * Jonas Hungershausen + * Andrew Bowley *******************************************************************************/ package org.eclipse.dartboard.preferences; -import java.io.BufferedReader; -import java.io.File; -import java.io.IOException; -import java.io.InputStreamReader; -import java.nio.file.Files; -import java.nio.file.InvalidPathException; import java.nio.file.Path; -import java.nio.file.Paths; -import java.util.List; +import java.util.concurrent.ExecutionException; +import java.util.concurrent.Semaphore; import org.eclipse.core.runtime.ILog; import org.eclipse.core.runtime.Platform; import org.eclipse.dartboard.logging.DartLog; import org.eclipse.dartboard.messages.Messages; +import org.eclipse.dartboard.util.DartSdkChecker; import org.eclipse.dartboard.util.GlobalConstants; +import org.eclipse.dartboard.util.PlatformUtil; import org.eclipse.jface.preference.DirectoryFieldEditor; +import org.eclipse.jface.preference.IPreferenceStore; +import org.eclipse.swt.events.ModifyEvent; import org.eclipse.swt.events.ModifyListener; import org.eclipse.swt.widgets.Composite; +import org.eclipse.swt.widgets.Shell; -import com.google.common.collect.Lists; - +/** + * Field editor to configure Dart SDK. Combines a text control with a browse + * button to pop up a directory selection dialog. The user can type or paste + * into the text control as an alternative to browsing to the directory. + * + * @author jonas + * @author Andrew Bowley + */ public class DartSDKLocationFieldEditor extends DirectoryFieldEditor { - private static final ILog LOG = Platform.getLog(DartSDKLocationFieldEditor.class); + private static final ILog LOG = Platform.getLog(DartSDKLocationFieldEditor.class); + + /** Dart SDK location checker */ + private final DartSdkChecker dartSdkChecker; + + /** Flag to disable validation while directory browsing in progress */ + private volatile boolean inChangeDirectory; + /** + * Valid status flag replaces same flag in super class so enable/disable 'Apply' + * buttons work correctly + */ + private boolean isValid; + /** + * Directory Dialog returned value. The 'inChangeDirectory' flag is cleared when + * this variable is set . + */ + private String newDirectory; - public DartSDKLocationFieldEditor(String preferencesKey, String label, Composite parent) { + /** Semaphore used to prevent validation reentry */ + private Semaphore validationGuard; + + /** + * Construct DartSDKLocationFieldEditor object + * + * @param preferencesKey Storage key + * @param labelText the label text of the field editor + * @param parent the parent of the field editor's control + */ + public DartSDKLocationFieldEditor(String preferencesKey, String label, Composite parent) { super(preferencesKey, label, parent); + Shell shell = parent.getShell(); + dartSdkChecker = PlatformUtil.getInstance().getDartSdkChecker(shell, false); setValidateStrategy(VALIDATE_ON_KEY_STROKE); + // Automatically invalidate an empty value + setEmptyStringAllowed(false); + inChangeDirectory = false; + // Semaphore used to prevent validation reentry, which may costly as it may + // include executing a shell command + validationGuard = new Semaphore(1); } + public void setEnabled(boolean enabled) { + getTextControl().setEnabled(enabled); + } + + /** + * Returns valid flag. Super isValid() has too much latency to be useable. + * + * @return boolean + */ + @Override + public boolean isValid() { + return isValid; + } + + /** + * Sets the preference store used by this field editor. + * + * @param store the preference store, or null if none + * @see #getPreferenceStore + */ + @Override + public void setPreferenceStore(IPreferenceStore store) { + super.setPreferenceStore(store); + if (store != null) + // Check if default exists + isValid = doCheckState(); + else + isValid = true; + } + + /** + * Handle Browse button pressed + * + * @return Directory Dialog, which may be null if the user cancels + */ + @Override + protected String changePressed() { + // Flag Directory Dialog being displayed for doCheckState() logic. + // The focus change triggered by dismissing the dialog can preempt the update + // of the text field + newDirectory = null; + inChangeDirectory = true; + newDirectory = super.changePressed(); + if (newDirectory == null) + inChangeDirectory = false; + return newDirectory; + } + + /** + * Checks Flutter SDK location selected/entered by user and returns valid flag + * + * @return boolean + */ @Override protected boolean doCheckState() { if (getPreferenceStore().getBoolean(GlobalConstants.P_FLUTTER_ENABLED) && getTextControl().getText().isEmpty()) { return true; } - String location = getTextControl().getText(); - boolean isValid = isValidDartSDK(location); - if (!isValid) { + if (inChangeDirectory) { + // Waiting for user to select directory + if (newDirectory != null) { + inChangeDirectory = false; + isValid = !newDirectory.isEmpty() && isValidDartSDK(newDirectory); + } else + // Do not display error message while waiting for result + return false; + } else { + String location = getTextControl().getText(); + isValid = !location.isEmpty() && isValidDartSDK(location); + } + if (isValid) { + setErrorMessage(null); + showMessage(null); + } else { setErrorMessage(Messages.Preference_SDKNotFound_Message); showErrorMessage(); } return isValid; } + /** + * Adds text control text modification listener + * + * @param listener ModifyListener + */ + protected void addModifyListener(ModifyListener listener) { + // Filter modifications made from using the Browse button. + ModifyListener modifyListener = new ModifyListener() { + + @Override + public void modifyText(ModifyEvent e) { + if (!inChangeDirectory) + listener.modifyText(e); + } + }; + getTextControl().addModifyListener(modifyListener); + } + /** * Checks if a given path is the root directory of a Dart SDK installation. * @@ -80,68 +204,17 @@ && getTextControl().getText().isEmpty()) { * @return false if the location is not a Dart SDK root directory, * true otherwise. */ - @SuppressWarnings("nls") private boolean isValidDartSDK(String location) { - if (location.isEmpty()) { - return false; - } - boolean isWindows = Platform.OS_WIN32.equals(Platform.getOS()); - - Path path = null; - // On Windows if a certain wrong combination of characters are entered a - // InvalidPathException is thrown. In that case we can assume that the location - // entered is not a valid Dart SDK directory either. try { - path = Paths.get(location).resolve("bin" + File.separator + (isWindows ? "dart.exe" : "dart")); - } catch (InvalidPathException e) { + if (!validationGuard.tryAcquire()) + // Validation already in progress so just return an optimistic interim result + return true; + return dartSdkChecker.isValidDartSDK(location); + } catch (ExecutionException e) { + LOG.log(DartLog.createError("Error verifying Dart SDK", e)); //$NON-NLS-1$ return false; + } finally { + validationGuard.release(); } - - // See https://github.com/eclipse/dartboard/issues/103 - List blacklist = Lists.newArrayList("/bin/dart", "/usr/bin/dart"); - // If the entered file doesn't exist, there is no need to run it - // Similarly if the file is a directory it can't be the dart executable - if (!Files.exists(path) || Files.isDirectory(path) - || (!isWindows && blacklist.contains(path.toString().toLowerCase()))) { - return false; - } - // Follow symbolic links - try { - path = path.toRealPath(); - } catch (IOException e1) { - LOG.log(DartLog.createError("Couldn't follow symlink", e1)); - return false; - } - - String executablePath = path.toAbsolutePath().toString(); - - String[] commands; - if (isWindows) { - commands = new String[] { "cmd", "/c", executablePath, "--version" }; - } else { - commands = new String[] { "/bin/bash", "-c", executablePath + " --version" }; - } - - ProcessBuilder processBuilder = new ProcessBuilder(commands); - - processBuilder.redirectErrorStream(true); - String version = null; - try (BufferedReader reader = new BufferedReader( - new InputStreamReader(processBuilder.start().getInputStream()))) { - version = reader.readLine(); - } catch (IOException e) { - return false; - } - - return version.startsWith("Dart VM version"); } - - protected void addModifyListener(ModifyListener listener) { - getTextControl().addModifyListener(listener); - } - - public void setEnabled(boolean enabled) { - getTextControl().setEnabled(enabled); - } - } diff --git a/org.eclipse.dartboard/src/org/eclipse/dartboard/preferences/FlutterSDKLocationFieldEditor.java b/org.eclipse.dartboard/src/org/eclipse/dartboard/preferences/FlutterSDKLocationFieldEditor.java index 2cf80f9..ceaa06f 100644 --- a/org.eclipse.dartboard/src/org/eclipse/dartboard/preferences/FlutterSDKLocationFieldEditor.java +++ b/org.eclipse.dartboard/src/org/eclipse/dartboard/preferences/FlutterSDKLocationFieldEditor.java @@ -10,95 +10,210 @@ * * Contributors: * Jonas Hungershausen + * Andrew Bowley *******************************************************************************/ package org.eclipse.dartboard.preferences; -import java.io.File; -import java.io.IOException; -import java.nio.file.Files; -import java.nio.file.InvalidPathException; import java.nio.file.Path; -import java.nio.file.Paths; -import java.util.Optional; +import java.util.concurrent.ExecutionException; +import java.util.concurrent.Semaphore; import org.eclipse.core.runtime.ILog; import org.eclipse.core.runtime.Platform; import org.eclipse.dartboard.logging.DartLog; import org.eclipse.dartboard.messages.Messages; +import org.eclipse.dartboard.util.DartSdkChecker; import org.eclipse.dartboard.util.GlobalConstants; -import org.eclipse.dartboard.util.PlatformUIUtil; +import org.eclipse.dartboard.util.PlatformUtil; import org.eclipse.jface.preference.DirectoryFieldEditor; +import org.eclipse.jface.preference.IPreferenceStore; +import org.eclipse.swt.events.ModifyEvent; import org.eclipse.swt.events.ModifyListener; import org.eclipse.swt.widgets.Composite; +import org.eclipse.swt.widgets.Shell; /** - * @author jonas + * Field editor to configure Flutter SDK. Combines a text control with a browse + * button to pop up a directory selection dialog. The user can type or paste + * into the text control as an alternative to browsing to the directory. * + * @author jonas + * @author Andrew Bowley */ public class FlutterSDKLocationFieldEditor extends DirectoryFieldEditor { private static final ILog LOG = Platform.getLog(FlutterSDKLocationFieldEditor.class); + /** Dart SDK location checker */ + private final DartSdkChecker dartSdkChecker; + /** Flag to disable validation while directory browsing in progress */ + private volatile boolean inChangeDirectory; + /** Semaphore used to prevent validation reentry */ + private Semaphore validationGuard; + + /** + * Valid status flag replaces same flag in super class so enable/disable 'Apply' + * buttons work correctly + */ + private boolean isValid; + /** + * Directory Dialog returned value. The 'inChangeDirectory' flag is cleared when + * this variable is set . + */ + private String newDirectory; + + /** + * Construct FlutterSDKLocationFieldEditor object + * + * @param preferencesKey Storage key + * @param labelText the label text of the field editor + * @param parent the parent of the field editor's control + */ public FlutterSDKLocationFieldEditor(String preferencesKey, String label, Composite parent) { super(preferencesKey, label, parent); + Shell shell = parent.getShell(); + dartSdkChecker = PlatformUtil.getInstance().getDartSdkChecker(shell, true); setValidateStrategy(VALIDATE_ON_KEY_STROKE); + // Automatically invalidate an empty value + setEmptyStringAllowed(false); + inChangeDirectory = false; + // Semaphore used to prevent validation reentry, which may costly as it may + // include executing a shell command + validationGuard = new Semaphore(1); + } + + public void setEnabled(boolean enabled) { + getTextControl().setEnabled(enabled); + } + + /** + * Returns valid flag. Super isValid() has too much latency to be useable. + * + * @return boolean + */ + @Override + public boolean isValid() { + return isValid; + } + + /** + * Sets the preference store used by this field editor. + * + * @param store the preference store, or null if none + * @see #getPreferenceStore + */ + @Override + public void setPreferenceStore(IPreferenceStore store) { + super.setPreferenceStore(store); + if (store != null) + // Check if default exists + isValid = doCheckState(); + else + isValid = true; + } + + /** + * Handle Browse button pressed + * + * @return Directory Dialog, whiech may be null if the user cancels + */ + @Override + protected String changePressed() { + // Flag Directory Dialog being displayed for doCheckState() logic. + // The focus change triggered by dismissing the dialog can preempt the update + // of the text field + newDirectory = null; + inChangeDirectory = true; + newDirectory = super.changePressed(); + if (newDirectory == null) + inChangeDirectory = false; + return newDirectory; } + /** + * Checks Flutter SDK location selected/entered by user and returns valid flag + * + * @return boolean + */ @Override protected boolean doCheckState() { if (!getPreferenceStore().getBoolean(GlobalConstants.P_FLUTTER_ENABLED)) { return true; } - String location = getTextControl().getText(); - Optional optionalPath = getPath(location); - if (!optionalPath.isPresent()) { + if (inChangeDirectory) { + // Waiting for user to select directory + if (newDirectory != null) { + inChangeDirectory = false; + isValid = !newDirectory.isEmpty() && isValidFlutterSDK(newDirectory); + } else + // Do not display error message while waiting for result + return false; + } else { + String location = getTextControl().getText(); + isValid = !location.isEmpty() && isValidFlutterSDK(location); + } + if (isValid) { + setErrorMessage(null); + showMessage(null); + } else { setErrorMessage(Messages.Preference_SDKNotFound_Message); showErrorMessage(); - return false; } - return true; + return isValid; } - private Optional getPath(String location) { - - if (location.isEmpty()) { - return Optional.empty(); - } + /** + * Adds text control text modification listener + * + * @param listener ModifyListener + */ + protected void addModifyListener(ModifyListener listener) { + // Filter modifications made from using the Browse button. + ModifyListener modifyListener = new ModifyListener() { - Path path = null; - // On Windows if a certain wrong combination of characters are entered a - // InvalidPathException is thrown. In that case we can assume that the location - // entered is not a valid Dart SDK directory either. - try { - path = Paths.get(location) - .resolve("bin" + File.separator + (PlatformUIUtil.IS_WINDOWS ? "flutter.bat" : "flutter")); //$NON-NLS-1$ //$NON-NLS-2$ //$NON-NLS-3$ - } catch (InvalidPathException e) { - return Optional.empty(); - } + @Override + public void modifyText(ModifyEvent e) { + if (!inChangeDirectory) + listener.modifyText(e); + } + }; + getTextControl().addModifyListener(modifyListener); + } - // See https://github.com/eclipse/dartboard/issues/103 - // List blacklist = Lists.newArrayList("/bin/dart", "/usr/bin/dart"); - // If the entered file doesn't exist, there is no need to run it - // Similarly if the file is a directory it can't be the dart executable - if (!Files.exists(path) || Files.isDirectory(path)) { - return Optional.empty(); - } - // Follow symbolic links + /** + * Checks if a given path is the root directory of a Dart SDK installation. + * + * Returns false if the path does not exist or the given location can not be + * converted to a {@link Path}. + * + * Similarly if the Path is not a directory, false is returned. + * + * If the location is a symbolic link but it can not be resolved, false is + * returned. + * + * If the process to test the version string returned by the Dart executable can + * not be executed, false is returned. + * + * Finally, if the returned version string does not start with "Dart VM + * version", false is returned. + * + * @param location - A {@link String} that should be checked to be a Dart SDK + * root directory. + * @return false if the location is not a Dart SDK root directory, + * true otherwise. + */ + private boolean isValidFlutterSDK(String location) { try { - path = path.toRealPath(); - } catch (IOException e1) { - LOG.log(DartLog.createError("Couldn't follow symlink", e1)); //$NON-NLS-1$ - return Optional.empty(); + if (!validationGuard.tryAcquire()) + // Validation already in progress so just return an optimistic interim result + return true; + return dartSdkChecker.isValidDartSDK(location); + } catch (ExecutionException e) { + LOG.log(DartLog.createError("Error verifying Dart SDK", e)); //$NON-NLS-1$ + return false; + } finally { + validationGuard.release(); } - return Optional.of(path); - } - - public void setEnabled(boolean enabled) { - getTextControl().setEnabled(enabled); - } - - protected void addModifyListener(ModifyListener listener) { - getTextControl().addModifyListener(listener); } } diff --git a/org.eclipse.dartboard/src/org/eclipse/dartboard/util/BusyCursor.java b/org.eclipse.dartboard/src/org/eclipse/dartboard/util/BusyCursor.java new file mode 100644 index 0000000..3b09c06 --- /dev/null +++ b/org.eclipse.dartboard/src/org/eclipse/dartboard/util/BusyCursor.java @@ -0,0 +1,172 @@ +/******************************************************************************* + * Copyright (c) 2020 vogella GmbH and others. + * + * This program and the accompanying materials + * are made available under the terms of the Eclipse Public License 2.0 + * which accompanies this distribution, and is available at + * https://www.eclipse.org/legal/epl-2.0/ + * + * SPDX-License-Identifier: EPL-2.0 + * + * Contributors: + * Andrew Bowley + *******************************************************************************/ +package org.eclipse.dartboard.util; + +import java.util.concurrent.Callable; +import java.util.concurrent.ExecutionException; + +import org.eclipse.swt.SWT; +import org.eclipse.swt.custom.BusyIndicator; +import org.eclipse.swt.widgets.Display; +import org.eclipse.swt.widgets.Shell; + +/** + * Shows a Busy Cursor during a long running process. Based on SWT BusyIndicator + * example. + * + * @author Andrew Bowley + */ +public class BusyCursor { + + /** + * Runnable which shows busy cursor pending arrival of a return object. + * @param the return object type + */ + static final class BusyRunnable implements Runnable { + + private final Shell shell; + private final Callable supplier; + private T value; + private Throwable caught; + private volatile boolean terminate; + + public BusyRunnable(Shell shell, Callable supplier) { + this.shell = shell; + this.supplier = supplier; + terminate = false; + } + + public T getValue() { + return value; + } + + public Throwable getCaught() { + return caught; + } + + @Override + public void run() { + Display display = shell.getDisplay(); + Thread thread = new Thread(() -> { + try { + value = supplier.call(); + terminate = true; + } catch (Exception e) { + caught = e; + } + display.wake(); + }); + thread.start(); + while (!terminate && (caught == null) && !shell.isDisposed()) { + if (!display.readAndDispatch()) + display.sleep(); + } + if (shell.isDisposed()) + caught = new IllegalStateException("Task cancelled by user"); //$NON-NLS-1$ + } + } + + /** + * Runnable to execute busy cursor display + */ + static final class SyncRunner implements Runnable { + + private BusyRunnable busyRunnable; + private Display display; + + public SyncRunner(BusyRunnable busyRunnable, Display display) { + this.busyRunnable = busyRunnable; + this.display = display; + } + + @Override + public void run() { + BusyIndicator.showWhile(display, busyRunnable); + } + + } + + /** Temporary shell created when active is not available */ + private Shell ownShell; + /** Active shell of application. May be null if one not yet created */ + private Shell activeShell; + + /** + * Construct BusyCursor object + * @param shell Active shell or null if not available + */ + public BusyCursor(Shell shell) { + if (shell == null) { + // Create temporary shell to use until the shell is updated by calling #setShell() + Display.getDefault().syncExec(new Runnable() { + + @Override + public void run() { + ownShell = new Shell(SWT.TOOL | SWT.NO_TRIM); + }}); + } else { + activeShell = shell; + } + } + + /** + * Sets shell for case active shell only available post-construction + * @param shell + */ + public void setShell(Shell shell) { + if (ownShell != null) { + ownShell.getDisplay().syncExec(new Runnable() { + + @Override + public void run() { + ownShell.close(); + }}); + ownShell = null; + } + activeShell = shell; + } + + /** + * Wait for object to be supplied calling from non-UI thread + * @param Object type + * @param supplier Object supplier + * @return object + * @throws ExecutionException + */ + public T syncWaitForObject(Callable supplier) throws ExecutionException { + Shell shell = activeShell == null ? ownShell : activeShell; + BusyRunnable busyRunnable = new BusyRunnable<>(shell, supplier); + Display display = shell.getDisplay(); + display.syncExec(new SyncRunner<>(busyRunnable, display)); + if (busyRunnable.getCaught() != null) + throw new ExecutionException("Task failed", busyRunnable.getCaught()); //$NON-NLS-1$ + return busyRunnable.getValue(); + } + + /** + * Wait for object to be supplied calling from UI thread + * @param Object type + * @param supplier Object supplier + * @return object + * @throws ExecutionException + */ + public T waitForObject(Callable supplier) throws ExecutionException { + Shell shell = activeShell == null ? ownShell : activeShell; + BusyRunnable busyRunnable = new BusyRunnable<>(shell, supplier); + BusyIndicator.showWhile(shell.getDisplay(), busyRunnable); + if (busyRunnable.getCaught() != null) + throw new ExecutionException("Task failed", busyRunnable.getCaught()); //$NON-NLS-1$ + return busyRunnable.getValue(); + } +} diff --git a/org.eclipse.dartboard/src/org/eclipse/dartboard/util/DartSdkChecker.java b/org.eclipse.dartboard/src/org/eclipse/dartboard/util/DartSdkChecker.java new file mode 100644 index 0000000..ee05766 --- /dev/null +++ b/org.eclipse.dartboard/src/org/eclipse/dartboard/util/DartSdkChecker.java @@ -0,0 +1,178 @@ +/******************************************************************************* + * Copyright (c) 2020 vogella GmbH and others. + * + * This program and the accompanying materials + * are made available under the terms of the Eclipse Public License 2.0 + * which accompanies this distribution, and is available at + * https://www.eclipse.org/legal/epl-2.0/ + * + * SPDX-License-Identifier: EPL-2.0 + * + * Contributors: + * Andrew Bowley + *******************************************************************************/ +package org.eclipse.dartboard.util; + +import java.io.BufferedReader; +import java.io.File; +import java.io.IOException; +import java.io.InputStreamReader; +import java.nio.file.Files; +import java.nio.file.InvalidPathException; +import java.nio.file.Path; +import java.nio.file.Paths; +import java.util.List; +import java.util.concurrent.Callable; +import java.util.concurrent.ExecutionException; + +import org.eclipse.swt.widgets.Shell; + +/** + * Base for checking if a given location contains a Dart SDK. Values subject to + * context (Dart or Flutter) and operating system (Linux or Windows) are to be + * provided by sub classes. + * + * @author Andrew Bowley + * + */ +public abstract class DartSdkChecker { + + private static final String DART_PREFIX = "Dart VM version"; //$NON-NLS-1$ + + /** Parent shell required by busy cursor */ + private final Shell shell; + /** Relative path from SDK root to Dart executable, no terminating path separator */ + private final String relativePath; + + /** + * Construct DartSdkChecker object + * + * @param shell Parent shell or null if not available + * @param relativePath Relative path from SDK root to Dart executable + */ + protected DartSdkChecker(Shell shell, String relativePath) { + this.shell = shell; + this.relativePath = relativePath; + } + + /** + * Returns arguments to execute a query in a command shell which returns the + * Dart version + * + * @param executablePath Absolute path to Dart executable + * @return String[] + */ + public abstract String[] getDartVersionCommands(String executablePath); + + /** + * Return black list, or empty list if none. See + * https://github.com/eclipse/dartboard/issues/103 + * + * @return List + */ + public abstract List getBlacklist(); + + /** + * Returns name of Dart executable + * + * @return filename + */ + public abstract String getDartExecutable(); + + /** + * Checks if a given path is the root directory of a Dart SDK installation. + * + * Returns false if the path does not exist or the given location can not be + * converted to a {@link Path}. + * + * Similarly if the Path is not a directory, false is returned. + * + * If the location is a symbolic link but it can not be resolved, false is + * returned. + * + * If the process to test the version string returned by the Dart executable can + * not be executed, false is returned. + * + * Finally, if the returned version string does not start with "Dart VM + * version", false is returned. + * + * @param location - A {@link String} that should be checked to be a Dart SDK + * root directory. + * @return false if the location is not a Dart SDK root directory, + * true otherwise. + */ + @SuppressWarnings("nls") + public boolean isValidDartSDK(String location) throws ExecutionException { + if (location.isEmpty()) { + + return false; + } + Path path = null; + // On Windows if a certain wrong combination of characters are entered a + // InvalidPathException is thrown. In that case we can assume that the location + // entered is not a valid Dart SDK directory either. + String internalPath = + relativePath.isEmpty() ? + getDartExecutable() : + relativePath + File.separator + getDartExecutable(); + try { + path = Paths.get(location).resolve(internalPath); + } catch (InvalidPathException e) { + + return false; + } + + // See https://github.com/eclipse/dartboard/issues/103 + List blacklist = getBlacklist(); + if (!blacklist.isEmpty() && blacklist.contains(path.toString().toLowerCase())) { + + return false; + } + + // If the entered file doesn't exist, there is no need to run it + // Similarly if the file is a directory it can't be the dart executable + if (!Files.exists(path) || Files.isDirectory(path)) { + + return false; + } + + // Follow symbolic links + try { + path = path.toRealPath(); + } catch (IOException e1) { + throw new ExecutionException("Couldn't follow symlink", e1); + } + + // Show busy cursor while running command shell to verify Dart SDK + BusyCursor busyCursor = new BusyCursor(shell); + final String executablePath = path.toAbsolutePath().toString(); + + return busyCursor.waitForObject(new Callable() { + + @Override + public Boolean call() throws Exception { + return verifyDartSdk(getProcessBuilder(executablePath)); + } + }); + } + + protected ProcessBuilder getProcessBuilder(String executablePath) { + String[] commands = getDartVersionCommands(executablePath); + ProcessBuilder processBuilder = new ProcessBuilder(commands); + processBuilder.redirectErrorStream(true); + + return processBuilder; + } + + private boolean verifyDartSdk(ProcessBuilder processBuilder) { + String version = null; + try (BufferedReader reader = new BufferedReader( + new InputStreamReader(processBuilder.start().getInputStream()))) { + version = reader.readLine(); + } catch (IOException e) { + return false; + } + + return (version != null) && version.startsWith(DART_PREFIX); + } +} diff --git a/org.eclipse.dartboard/src/org/eclipse/dartboard/util/IPlatformFactory.java b/org.eclipse.dartboard/src/org/eclipse/dartboard/util/IPlatformFactory.java new file mode 100644 index 0000000..de04f1b --- /dev/null +++ b/org.eclipse.dartboard/src/org/eclipse/dartboard/util/IPlatformFactory.java @@ -0,0 +1,36 @@ +/******************************************************************************* + * Copyright (c) 2020 vogella GmbH and others. + * + * This program and the accompanying materials + * are made available under the terms of the Eclipse Public License 2.0 + * which accompanies this distribution, and is available at + * https://www.eclipse.org/legal/epl-2.0/ + * + * SPDX-License-Identifier: EPL-2.0 + * + * Contributors: + * Andrew Bowley + *******************************************************************************/ +package org.eclipse.dartboard.util; + +import org.eclipse.swt.widgets.Shell; + + public interface IPlatformFactory { + + /** + * Returns a Dart SDK checker + * + * @param shell Parent shell of owner or null if none + * @param isFlutter Flag set true if Dart SDK is inside Flutter + * @return DartSdkChecker object + */ + DartSdkChecker getDartSdkChecker(Shell shell, boolean isFlutter); + + /** + * Returns support for locating SDK artifacts + * + * @return SdkLocator + */ + SdkLocator getSdkLocator(); + +} \ No newline at end of file diff --git a/org.eclipse.dartboard/src/org/eclipse/dartboard/util/PlatformUtil.java b/org.eclipse.dartboard/src/org/eclipse/dartboard/util/PlatformUtil.java new file mode 100644 index 0000000..0a5e1b8 --- /dev/null +++ b/org.eclipse.dartboard/src/org/eclipse/dartboard/util/PlatformUtil.java @@ -0,0 +1,140 @@ +/******************************************************************************* + * Copyright (c) 2020 vogella GmbH and others. + * + * This program and the accompanying materials + * are made available under the terms of the Eclipse Public License 2.0 + * which accompanies this distribution, and is available at + * https://www.eclipse.org/legal/epl-2.0/ + * + * SPDX-License-Identifier: EPL-2.0 + * + * Contributors: + * Andrew Bowley + *******************************************************************************/ +package org.eclipse.dartboard.util; + +import java.nio.file.Path; +import java.util.Optional; +import java.util.concurrent.ExecutionException; + +import org.eclipse.core.runtime.ILog; +import org.eclipse.core.runtime.Platform; +import org.eclipse.dartboard.logging.DartLog; +import org.eclipse.swt.widgets.Shell; + +/** + * Platform utilities + * + * @author Andrew Bowley + * + */ +public class PlatformUtil { + + private static final ILog LOG = Platform.getLog(PlatformUtil.class); + + private IPlatformFactory platformFactory; + + private static volatile PlatformUtil instance; + + private PlatformUtil() { + boolean isWindows = Platform.OS_WIN32.equals(Platform.getOS()); + platformFactory = isWindows ? new org.eclipse.dartboard.os.windows.PlatformFactory() + : new org.eclipse.dartboard.os.linux.PlatformFactory(); + } + + /** + * Returns a Dart SDK checker + * + * @param shell Parent shell of owner or null if none + * @param isFlutter Flag set true if Dart SDK is inside Flutter + * @return DartSdkChecker object + */ + public DartSdkChecker getDartSdkChecker(Shell shell, boolean isFlutter) { + return platformFactory.getDartSdkChecker(shell, isFlutter); + } + + /** + * Returns path to given command or null if not found + * + * @param program Command as entered by user + * @return Optional object of parametric type Path + * @throws ExecutionException + */ + public Optional getLocation(String program) throws ExecutionException { + return platformFactory.getSdkLocator().getLocation(program); + } + + /** + * Returns path to given command or null if not found + * + * @param program Command as entered by user + * @param interactive Flag if set true allows the user to type commands. Not + * supported on Windows. + * @return Optional object of parametric type Path + * @throws ExecutionException + */ + public Optional getLocation(String program, boolean interactive) throws ExecutionException { + return platformFactory.getSdkLocator().getLocation(program, interactive); + } + + /** + * Returns the path to an executable within the Dart SDK bin directory in a + * system agnostic format. + * + * The difference to {@link #getTool(String)} is that this method returns the + * path to an .exe file (on windows). + * + * These executables are: dart and dartaotruntime + * + * @param sdkLocation Absolute location of Dart SDK + * @param name - The name of the executable + * @return The path to the executable valid for the host operating system + */ + public String getExecutable(String sdkLocation, String name) { + return platformFactory.getSdkLocator().resolveExecutablePath(sdkLocation, name); + } + + /** + * Returns the path to a tool within the Dart SDK bin directory in a system + * agnostic format. The appropriate file extension, if any, is also accepted eg. + * ".bat" on Windows. + * + * A tool in the Dart SDK bin directory is any of the various executables, that + * are .bat files on the windows version of the SDK. + * + * These tools are: dart2aot, dart2js, dartanalyzer, dartdevc, dartdoc, dartfmt, + * pub + * + * @param sdkLocation Absolute location of Dart SDK + * @param name - The name of the tool + * @return The path to the tool valid for the host operating system + */ + public String resolveToolPath(String sdkLocation, String name) { + return platformFactory.getSdkLocator().resolveToolPath(sdkLocation, name); + } + + public static PlatformUtil getInstance() { + if (instance == null) { + + synchronized (PlatformUtil.class) { + // The following null check is supposedly thread safe when 'instance' is + // volatile + if (instance == null) + instance = new PlatformUtil(); + } + } + + return instance; + } + + private static Object getClass(String nameClass) { + try { + return (Class.forName(nameClass)).newInstance(); + } catch (InstantiationException | IllegalAccessException | ClassNotFoundException e) { + LOG.log(DartLog.createError("Class.forName() failed", e)); //$NON-NLS-1$ + e.printStackTrace(); + return null; + } + } + +} diff --git a/org.eclipse.dartboard/src/org/eclipse/dartboard/util/SDKLocator.java b/org.eclipse.dartboard/src/org/eclipse/dartboard/util/SDKLocator.java deleted file mode 100644 index a97f730..0000000 --- a/org.eclipse.dartboard/src/org/eclipse/dartboard/util/SDKLocator.java +++ /dev/null @@ -1,81 +0,0 @@ -package org.eclipse.dartboard.util; - -import java.io.BufferedReader; -import java.io.IOException; -import java.io.InputStreamReader; -import java.nio.file.Path; -import java.nio.file.Paths; -import java.util.Optional; - -import org.eclipse.core.runtime.Platform; - -public class SDKLocator { - - public static final boolean IS_WINDOWS = Platform.OS_WIN32.equals(Platform.getOS()); - - /** - * Returns a {@link Path} containing the location of the Dart SDK folder. - * - * This method finds the location of the Dart SDK on the system, if installed. - * On *nix based systems it tries to locate the Dart binary by using the - * {@code which} command. Typically the output is a symbolic link to the actual - * binary. Since the Dart SDK installation folder contains more binaries that we - * need, we resolve the symbolic link and return the path to the /bin directory - * inside the SDK installation folder. - * - * On Windows this method uses the where command to locate the binary. - * - * @return - An {@link Optional} of {@link Path} containing the path to the - * {@code /bin} folder inside the Dart SDK installation directory or - * empty if the SDK is not found on the host machine. - */ - public static Optional getDartLocation() throws IOException, InterruptedException { - return getLocation("dart", false); //$NON-NLS-1$ - } - - public static Optional getFlutterLocation() throws IOException, InterruptedException { - return getLocation("flutter", true); //$NON-NLS-1$ - } - - public static Optional getLocation(String program, boolean interactive) - throws IOException, InterruptedException { - Path path = null; - String[] command; - if (IS_WINDOWS) { - command = new String[] { "cmd", "/c", "where " + program }; //$NON-NLS-1$ //$NON-NLS-2$ //$NON-NLS-3$ - } else { - String shell = getShell(); - if (interactive) { - command = new String[] { shell, "-i", "-c", "which " + program }; //$NON-NLS-1$ //$NON-NLS-2$ //$NON-NLS-3$ - } else { - command = new String[] { shell, "-c", "which " + program }; //$NON-NLS-1$ //$NON-NLS-2$ - } - } - - ProcessBuilder processBuilder = new ProcessBuilder(); - processBuilder.command(command); - Process process = processBuilder.start(); - process.waitFor(); - try (BufferedReader reader = new BufferedReader(new InputStreamReader(process.getInputStream()))) { - String location = reader.readLine(); - - if (location != null) { - path = Paths.get(location); - path = path.toRealPath().getParent(); - } - } - - // TODO: Try different default installs (need to collect them) - return Optional.ofNullable(path); - } - - public static String getShell() throws IOException, InterruptedException { - ProcessBuilder builder = new ProcessBuilder("/bin/sh", "-c", "echo $SHELL"); //$NON-NLS-1$ //$NON-NLS-2$ //$NON-NLS-3$ - Process process = builder.start(); - process.waitFor(); - try (BufferedReader reader = new BufferedReader(new InputStreamReader(process.getInputStream()))) { - String location = reader.readLine(); - return location; - } - } -} diff --git a/org.eclipse.dartboard/src/org/eclipse/dartboard/util/SdkLocator.java b/org.eclipse.dartboard/src/org/eclipse/dartboard/util/SdkLocator.java new file mode 100644 index 0000000..ef6bb4a --- /dev/null +++ b/org.eclipse.dartboard/src/org/eclipse/dartboard/util/SdkLocator.java @@ -0,0 +1,135 @@ +/******************************************************************************* + * Copyright (c) 2020 vogella GmbH and others. + * + * This program and the accompanying materials + * are made available under the terms of the Eclipse Public License 2.0 + * which accompanies this distribution, and is available at + * https://www.eclipse.org/legal/epl-2.0/ + * + * SPDX-License-Identifier: EPL-2.0 + * + * Contributors: + * Andrew Bowley + *******************************************************************************/ +package org.eclipse.dartboard.util; + +import java.io.BufferedReader; +import java.io.IOException; +import java.io.InputStreamReader; +import java.nio.file.InvalidPathException; +import java.nio.file.Path; +import java.nio.file.Paths; +import java.util.Optional; +import java.util.concurrent.ExecutionException; + +/** + * Base support for locating SDK artifacts + * + * @author Andrew Bowley + * + */ +public abstract class SdkLocator { + + protected SdkLocator() { + } + + /** + * Returns arguments to execute a query in a command shell which returns the + * path to a given command + * + * @param program Command to locate + * @param interactive Flag if set true allows the user to type commands. May be + * ignored if no OS support. + * @return String[] + * @throws ExecutionException + */ + public abstract String[] getLocationCommand(String program, boolean interactive) throws ExecutionException; + + /** + * Returns the path to a tool within the SDK bin directory in a system agnostic + * format. The appropriate file extension, if any, is also accepted eg. ".bat" + * on Windows. + * + * A tool in the Dart SDK bin directory is any of the various executables, that + * are .bat files on the windows version of the SDK. + * + * These tools are: dart2aot, dart2js, dartanalyzer, dartdevc, dartdoc, dartfmt, + * pub + * + * @param sdkLocation Absolute location of Dart SDK + * @param name - The name of the tool + * @return The path to the tool valid for the host operating system + */ + public abstract String resolveToolPath(String sdkLocation, String name); + + /** + * Returns the path to an executable within the SDK bin directory in a system + * agnostic format. + * + * The difference to {@link #resolveToolPath(String)} is that, on Windows, this + * method returns the path to an .exe file. + * + * These executables are: dart and dartaotruntime + * + * @param sdkLocation Absolute location of Dart SDK + * @param name - The name of the executable + * @return The path to the executable valid for the host operating system + */ + public abstract String resolveExecutablePath(String sdkLocation, String name); + + /** + * Returns path to given command or null if not found + * + * @param program Command as entered by user + * @return Optional object of parametric type Path + * @throws ExecutionException + */ + public Optional getLocation(String program) throws ExecutionException { + return getLocation(program, false); + } + + /** + * Returns path to SDK home of given command or null if not found. Assumes the + * command is in the 'bin' folder directly under home or in home itself + * + * @param program Command as entered by user + * @param interactive Flag if set true allows the user to type commands. Not + * supported on Windows. + * @return Optional object of parametric type Path + * @throws ExecutionException + */ + public Optional getLocation(String program, boolean interactive) throws ExecutionException { + + Path path = null; + String[] command = getLocationCommand(program, interactive); + ProcessBuilder processBuilder = new ProcessBuilder(); + processBuilder.command(command); + Process process; + try { + process = processBuilder.start(); + process.waitFor(); + try (BufferedReader reader = new BufferedReader(new InputStreamReader(process.getInputStream()))) { + String location = reader.readLine(); + if (location != null) { + try { + path = Paths.get(location); + } catch (InvalidPathException e) { + return Optional.ofNullable(null); + } + path = path.toRealPath().getParent(); + if ((path == null) || (path.getFileName() == null)) + return Optional.ofNullable(null); + if (path.getFileName().toString().equals("bin")) { //$NON-NLS-1$ + path = path.getParent(); + if (path == null) + return Optional.ofNullable(null); + } + } + // TODO: Try different default installs (need to collect them) + return Optional.ofNullable(path); + } + } catch (IOException | InterruptedException e) { + throw new ExecutionException(String.format("Error while locating command %s", program), e); //$NON-NLS-1$ + } + } +}