diff --git a/plugins/com.google.cloud.tools.eclipse.appengine.localserver/plugin.xml b/plugins/com.google.cloud.tools.eclipse.appengine.localserver/plugin.xml index 626e02d190..e9fbcb3450 100644 --- a/plugins/com.google.cloud.tools.eclipse.appengine.localserver/plugin.xml +++ b/plugins/com.google.cloud.tools.eclipse.appengine.localserver/plugin.xml @@ -42,7 +42,8 @@ runtime="true" runtimeTypeId="com.google.cloud.tools.eclipse.appengine.standard.runtime" startBeforePublish="false" - supportsRemoteHosts="true"> + supportsRemoteHosts="true" + synchronousStart="true"> @@ -322,4 +323,13 @@ id="com.google.cloud.tools.eclipse.appengine.localserver.cloudSdkDebugTarget"> + + + + diff --git a/plugins/com.google.cloud.tools.eclipse.appengine.localserver/src/com/google/cloud/tools/eclipse/appengine/localserver/messages.properties b/plugins/com.google.cloud.tools.eclipse.appengine.localserver/src/com/google/cloud/tools/eclipse/appengine/localserver/messages.properties index a455800bc2..628bf64821 100644 --- a/plugins/com.google.cloud.tools.eclipse.appengine.localserver/src/com/google/cloud/tools/eclipse/appengine/localserver/messages.properties +++ b/plugins/com.google.cloud.tools.eclipse.appengine.localserver/src/com/google/cloud/tools/eclipse/appengine/localserver/messages.properties @@ -19,6 +19,8 @@ CANNOT_DETERMINE_EXECUTION_CONTEXT=Cannot determine server execution context SERVER_ALREADY_RUNNING=Server is already running SERVER_ALREADY_RUNNING_IN_MODE=Server is already running in "{0}" mode UNABLE_TO_LAUNCH=Unable to launch App Engine server +STALE_RESOURCES_DETECTED=Stale resources detected +STALE_RESOURCES_LAUNCH_CONFIRMATION=Some recently changed resources are not yet published. Continue with launch? target.terminated={0} cloudsdk.server.description=Google Cloud SDK Dev App Server diff --git a/plugins/com.google.cloud.tools.eclipse.appengine.localserver/src/com/google/cloud/tools/eclipse/appengine/localserver/server/LocalAppEngineServerBehaviour.java b/plugins/com.google.cloud.tools.eclipse.appengine.localserver/src/com/google/cloud/tools/eclipse/appengine/localserver/server/LocalAppEngineServerBehaviour.java index 058b414fdc..d7d2a460c3 100644 --- a/plugins/com.google.cloud.tools.eclipse.appengine.localserver/src/com/google/cloud/tools/eclipse/appengine/localserver/server/LocalAppEngineServerBehaviour.java +++ b/plugins/com.google.cloud.tools.eclipse.appengine.localserver/src/com/google/cloud/tools/eclipse/appengine/localserver/server/LocalAppEngineServerBehaviour.java @@ -199,6 +199,21 @@ public final void setModulePublishState2(IModule[] module, int state) { setModulePublishState(module, state); } + @Override + protected void publishFinish(IProgressMonitor monitor) throws CoreException { + boolean allPublished = true; + IServer server = getServer(); + IModule[] modules = server.getModules(); + for (IModule module : modules) { + if (server.getModulePublishState(new IModule[] {module}) != IServer.PUBLISH_STATE_NONE) { + allPublished = false; + } + } + if (allPublished) { + setServerPublishState(IServer.PUBLISH_STATE_NONE); + } + } + private static IStatus newErrorStatus(String message) { return new Status(IStatus.ERROR, Activator.PLUGIN_ID, message); } diff --git a/plugins/com.google.cloud.tools.eclipse.appengine.localserver/src/com/google/cloud/tools/eclipse/appengine/localserver/server/LocalAppEngineServerLaunchConfigurationDelegate.java b/plugins/com.google.cloud.tools.eclipse.appengine.localserver/src/com/google/cloud/tools/eclipse/appengine/localserver/server/LocalAppEngineServerLaunchConfigurationDelegate.java index 90c958045f..e491d283a6 100644 --- a/plugins/com.google.cloud.tools.eclipse.appengine.localserver/src/com/google/cloud/tools/eclipse/appengine/localserver/server/LocalAppEngineServerLaunchConfigurationDelegate.java +++ b/plugins/com.google.cloud.tools.eclipse.appengine.localserver/src/com/google/cloud/tools/eclipse/appengine/localserver/server/LocalAppEngineServerLaunchConfigurationDelegate.java @@ -25,6 +25,7 @@ import com.google.cloud.tools.eclipse.appengine.localserver.Messages; import com.google.cloud.tools.eclipse.appengine.localserver.PreferencesInitializer; import com.google.cloud.tools.eclipse.appengine.localserver.ui.LocalAppEngineConsole; +import com.google.cloud.tools.eclipse.appengine.localserver.ui.StaleResourcesStatusHandler; import com.google.cloud.tools.eclipse.ui.util.MessageConsoleUtilities; import com.google.cloud.tools.eclipse.ui.util.WorkbenchUtil; import com.google.cloud.tools.eclipse.usagetracker.AnalyticsEvents; @@ -50,6 +51,7 @@ import java.util.logging.Logger; import org.eclipse.core.resources.IMarkerDelta; import org.eclipse.core.resources.IProject; +import org.eclipse.core.resources.ResourcesPlugin; import org.eclipse.core.runtime.CoreException; import org.eclipse.core.runtime.IPath; import org.eclipse.core.runtime.IProgressMonitor; @@ -57,6 +59,8 @@ import org.eclipse.core.runtime.MultiStatus; import org.eclipse.core.runtime.Platform; import org.eclipse.core.runtime.Status; +import org.eclipse.core.runtime.SubMonitor; +import org.eclipse.core.runtime.jobs.Job; import org.eclipse.debug.core.DebugEvent; import org.eclipse.debug.core.DebugException; import org.eclipse.debug.core.DebugPlugin; @@ -65,6 +69,7 @@ import org.eclipse.debug.core.ILaunchConfigurationType; import org.eclipse.debug.core.ILaunchManager; import org.eclipse.debug.core.ILaunchesListener2; +import org.eclipse.debug.core.IStatusHandler; import org.eclipse.debug.core.model.DebugElement; import org.eclipse.debug.core.model.IBreakpoint; import org.eclipse.debug.core.model.IDebugTarget; @@ -82,6 +87,7 @@ import org.eclipse.wst.server.core.IModule; import org.eclipse.wst.server.core.IServer; import org.eclipse.wst.server.core.IServerListener; +import org.eclipse.wst.server.core.ServerCore; import org.eclipse.wst.server.core.ServerEvent; import org.eclipse.wst.server.core.ServerUtil; @@ -128,6 +134,65 @@ public ILaunch getLaunch(ILaunchConfiguration configuration, String mode) throws return super.getLaunch(configuration, mode); } + @Override + public boolean finalLaunchCheck(ILaunchConfiguration configuration, String mode, + IProgressMonitor monitor) throws CoreException { + SubMonitor progress = SubMonitor.convert(monitor, 40); + if (!super.finalLaunchCheck(configuration, mode, progress.newChild(20))) { + return false; + } + + // If we're auto-publishing before launch, check if there may be stale + // resources not yet published. See + // https://github.com/GoogleCloudPlatform/google-cloud-eclipse/issues/1832 + if (ServerCore.isAutoPublishing() && ResourcesPlugin.getWorkspace().isAutoBuilding()) { + // Must wait for any current autobuild to complete so resource changes are triggered + // and WTP will kick off ResourceChangeJobs. Note that there may be builds + // pending that are unrelated to our resource changes, so simply checking + // JobManager.find(FAMILY_AUTO_BUILD).length > 0 produces too many + // false positives. + try { + Job.getJobManager().join(ResourcesPlugin.FAMILY_AUTO_BUILD, progress.newChild(20)); + } catch (InterruptedException ex) { + /* ignore */ + } + IServer server = ServerUtil.getServer(configuration); + if (server.shouldPublish() || hasPendingChangesToPublish()) { + IStatusHandler prompter = DebugPlugin.getDefault().getStatusHandler(promptStatus); + if (prompter != null) { + Object continueLaunch = prompter + .handleStatus(StaleResourcesStatusHandler.CONTINUE_LAUNCH_REQUEST, configuration); + if (!(Boolean) continueLaunch) { + // cancel the launch so Server.StartJob won't raise an error dialog, since the + // server won't have been started + monitor.setCanceled(true); + return false; + } + return true; + } + } + } + return true; + } + + /** + * Check if there are pending changes to be published: this is a nasty condition that can occur if + * the user saves changes as part of launching the server. + */ + private boolean hasPendingChangesToPublish() { + Job[] serverJobs = Job.getJobManager().find(ServerUtil.SERVER_JOB_FAMILY); + Job currentJob = Job.getJobManager().currentJob(); + for (Job job : serverJobs) { + // Launching from Server#start() means this will be running within a + // Server.StartJob. All other jobs should be ResourceChangeJob or + // PublishJob, both of which indicate unpublished changes. + if (job != currentJob) { + return true; + } + } + return false; + } + @VisibleForTesting void checkConflictingLaunches(ILaunchConfigurationType launchConfigType, DefaultRunConfiguration runConfig, ILaunch[] launches) throws CoreException { diff --git a/plugins/com.google.cloud.tools.eclipse.appengine.localserver/src/com/google/cloud/tools/eclipse/appengine/localserver/ui/StaleResourcesStatusHandler.java b/plugins/com.google.cloud.tools.eclipse.appengine.localserver/src/com/google/cloud/tools/eclipse/appengine/localserver/ui/StaleResourcesStatusHandler.java new file mode 100644 index 0000000000..96d8980c74 --- /dev/null +++ b/plugins/com.google.cloud.tools.eclipse.appengine.localserver/src/com/google/cloud/tools/eclipse/appengine/localserver/ui/StaleResourcesStatusHandler.java @@ -0,0 +1,62 @@ +/* + * Copyright 2017 Google Inc. + * + * 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. + */ + +package com.google.cloud.tools.eclipse.appengine.localserver.ui; + +import com.google.cloud.tools.eclipse.appengine.localserver.Messages; +import org.eclipse.core.runtime.CoreException; +import org.eclipse.core.runtime.IStatus; +import org.eclipse.core.runtime.Status; +import org.eclipse.debug.core.ILaunchConfiguration; +import org.eclipse.debug.core.IStatusHandler; +import org.eclipse.debug.internal.ui.DebugUIPlugin; +import org.eclipse.debug.ui.DebugUITools; +import org.eclipse.jface.dialogs.MessageDialog; +import org.eclipse.swt.widgets.Shell; + +/** + * A simple prompter for the Platform/Debug framework to prompt the user to confirm whether the + * launch should continue when possibly stale resources have been found. + */ +public class StaleResourcesStatusHandler implements IStatusHandler { + /** + * The error code indicating that there may be stale resources. Used with the plugin ID to + * uniquely identify this prompter. + */ + static final int CONFIRM_LAUNCH_CODE = 255; + + /** + * A specially crafted status message that is pass into the Debug Prompter class to obtain our + * confirmation prompter. + */ + public static final IStatus CONTINUE_LAUNCH_REQUEST = new Status(IStatus.INFO, + "com.google.cloud.tools.eclipse.appengine.localserver", CONFIRM_LAUNCH_CODE, "", null); + + @Override + public Object handleStatus(IStatus status, Object source) throws CoreException { + if (source instanceof ILaunchConfiguration) { + ILaunchConfiguration config = (ILaunchConfiguration) source; + if (DebugUITools.isPrivate(config)) { + return Boolean.FALSE; + } + } + + Shell activeShell = DebugUIPlugin.getShell(); + // should consider using MessageDialogWithToggle? + return MessageDialog.openQuestion(activeShell, Messages.getString("STALE_RESOURCES_DETECTED"), + Messages.getString("STALE_RESOURCES_LAUNCH_CONFIRMATION")); + } +} diff --git a/plugins/com.google.cloud.tools.eclipse.integration.appengine/src/com/google/cloud/tools/eclipse/integration/appengine/BaseProjectTest.java b/plugins/com.google.cloud.tools.eclipse.integration.appengine/src/com/google/cloud/tools/eclipse/integration/appengine/BaseProjectTest.java index f36e234edf..db6036af80 100644 --- a/plugins/com.google.cloud.tools.eclipse.integration.appengine/src/com/google/cloud/tools/eclipse/integration/appengine/BaseProjectTest.java +++ b/plugins/com.google.cloud.tools.eclipse.integration.appengine/src/com/google/cloud/tools/eclipse/integration/appengine/BaseProjectTest.java @@ -51,6 +51,9 @@ public static void setUp() throws Exception { @After public void tearDown() { if (project != null) { + // close editors, so no property changes are dispatched on delete + bot.closeAllEditors(); + // ensure there are no jobs SwtBotWorkbenchActions.waitForProjects(bot, project); try { diff --git a/plugins/com.google.cloud.tools.eclipse.integration.appengine/src/com/google/cloud/tools/eclipse/integration/appengine/DebugNativeAppEngineStandardProjectTest.java b/plugins/com.google.cloud.tools.eclipse.integration.appengine/src/com/google/cloud/tools/eclipse/integration/appengine/DebugNativeAppEngineStandardProjectTest.java index 1b57b89345..1f3aed344b 100644 --- a/plugins/com.google.cloud.tools.eclipse.integration.appengine/src/com/google/cloud/tools/eclipse/integration/appengine/DebugNativeAppEngineStandardProjectTest.java +++ b/plugins/com.google.cloud.tools.eclipse.integration.appengine/src/com/google/cloud/tools/eclipse/integration/appengine/DebugNativeAppEngineStandardProjectTest.java @@ -20,6 +20,7 @@ import static org.junit.Assert.assertEquals; import static org.junit.Assert.assertFalse; import static org.junit.Assert.assertNotNull; +import static org.junit.Assert.assertThat; import static org.junit.Assert.assertTrue; import static org.junit.Assert.fail; @@ -27,7 +28,12 @@ import com.google.cloud.tools.eclipse.swtbot.SwtBotTestingUtilities; import com.google.cloud.tools.eclipse.swtbot.SwtBotTreeUtilities; import com.google.cloud.tools.eclipse.test.util.ThreadDumpingWatchdog; - +import java.io.IOException; +import java.io.InputStreamReader; +import java.net.URL; +import java.net.URLConnection; +import java.nio.charset.StandardCharsets; +import java.util.concurrent.TimeUnit; import org.eclipse.core.runtime.preferences.InstanceScope; import org.eclipse.swt.custom.StyledText; import org.eclipse.swt.widgets.Tree; @@ -38,18 +44,12 @@ import org.eclipse.swtbot.swt.finder.widgets.SWTBotToolbarButton; import org.eclipse.swtbot.swt.finder.widgets.SWTBotTree; import org.eclipse.swtbot.swt.finder.widgets.SWTBotTreeItem; +import org.hamcrest.Matchers; import org.junit.Rule; import org.junit.Test; import org.junit.runner.RunWith; import org.osgi.service.prefs.Preferences; -import java.io.IOException; -import java.io.InputStreamReader; -import java.net.URL; -import java.net.URLConnection; -import java.nio.charset.StandardCharsets; -import java.util.concurrent.TimeUnit; - /** * Create a native App Engine Standard project, launch in debug mode, verify working, and then * terminate. @@ -101,16 +101,21 @@ public void run() { SWTBotTree launchTree = new SWTBotTree(bot.widget(widgetOfType(Tree.class), debugView.getWidget())); + + // avoid any stray processes that may be lying around + launchTree.contextMenu("Remove All Terminated").click(); + SwtBotTreeUtilities.waitUntilTreeHasItems(bot, launchTree); SWTBotTreeItem[] allItems = launchTree.getAllItems(); SwtBotTreeUtilities.waitUntilTreeHasText(bot, allItems[0]); - assertTrue("No App Engine launch found", - allItems[0].getText().contains("App Engine Standard at localhost")); + assertThat("No App Engine launch found", allItems[0].getText(), + Matchers.containsString("App Engine Standard at localhost")); SWTBotView consoleView = bot.viewById("org.eclipse.ui.console.ConsoleView"); // IConsoleConstants.ID_CONSOLE_VIEW consoleView.show(); - assertTrue("App Engine console not active", consoleView.getViewReference() - .getContentDescription().contains("App Engine Standard at localhost")); + assertThat("App Engine console not active", + consoleView.getViewReference().getContentDescription(), + Matchers.containsString("App Engine Standard at localhost")); final SWTBotStyledText consoleContents = new SWTBotStyledText(bot.widget(widgetOfType(StyledText.class), consoleView.getWidget())); SwtBotTestingUtilities.waitUntilStyledTextContains(bot,