/* Copyright (C) 2009 Mobile Sorcery AB This program is free software; you can redistribute it and/or modify it under the terms of the Eclipse Public License v1.0. This program is distributed in the hope that it will be useful, but WITHOUT ANY WARRANTY; without even the implied warranty of MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the Eclipse Public License v1.0 for more details. You should have received a copy of the Eclipse Public License v1.0 along with this program. It is also available at http://www.eclipse.org/legal/epl-v10.html */ package com.mobilesorcery.sdk.internal.launch; import java.text.MessageFormat; import java.util.Arrays; import java.util.HashMap; import java.util.Map; import java.util.SortedSet; import java.util.TreeSet; import org.eclipse.cdt.core.model.ICModelMarker; import org.eclipse.cdt.debug.ui.CDebugUIPlugin; import org.eclipse.core.resources.IMarker; import org.eclipse.core.resources.IProject; import org.eclipse.core.resources.IResource; import org.eclipse.core.runtime.CoreException; import org.eclipse.core.runtime.IPath; import org.eclipse.core.runtime.IProgressMonitor; import org.eclipse.core.runtime.IStatus; import org.eclipse.core.runtime.Status; import org.eclipse.core.runtime.jobs.Job; import org.eclipse.debug.core.DebugPlugin; import org.eclipse.debug.core.ILaunch; import org.eclipse.debug.core.ILaunchConfiguration; import org.eclipse.debug.core.ILaunchConfigurationWorkingCopy; import org.eclipse.debug.core.Launch; import org.eclipse.debug.core.model.ILaunchConfigurationDelegate2; import org.eclipse.debug.core.model.IPersistableSourceLocator; import org.eclipse.debug.core.model.LaunchConfigurationDelegate; import org.eclipse.debug.core.sourcelookup.AbstractSourceLookupDirector; import org.eclipse.jface.dialogs.MessageDialog; import org.eclipse.swt.widgets.Display; import org.eclipse.swt.widgets.Shell; import org.eclipse.ui.PlatformUI; import org.eclipse.ui.statushandlers.StatusManager; import com.mobilesorcery.sdk.core.CoreMoSyncPlugin; import com.mobilesorcery.sdk.core.IBuildConfiguration; import com.mobilesorcery.sdk.core.IBuildSession; import com.mobilesorcery.sdk.core.IBuildVariant; import com.mobilesorcery.sdk.core.ILaunchConstants; import com.mobilesorcery.sdk.core.MoSyncBuildJob; import com.mobilesorcery.sdk.core.MoSyncBuilder; import com.mobilesorcery.sdk.core.MoSyncNature; import com.mobilesorcery.sdk.core.MoSyncProject; import com.mobilesorcery.sdk.core.Util; import com.mobilesorcery.sdk.core.launch.IEmulatorLauncher; import com.mobilesorcery.sdk.core.launch.MoReLauncher; import com.mobilesorcery.sdk.internal.BuildSession; public class EmulatorLaunchConfigurationDelegate extends LaunchConfigurationDelegate implements ILaunchConfigurationDelegate2 { private static int GLOBAL_ID = 1; public static String ID = "com.mobilesorcery.launchconfigurationtype"; private static HashMap<ILaunchConfiguration, Map<String, Object>> overriddenAttributes = new HashMap<ILaunchConfiguration, Map<String, Object>>(); @Override public void launch(final ILaunchConfiguration launchConfig, final String mode, final ILaunch launch, final IProgressMonitor monitor) throws CoreException { IProject project = getProject(launchConfig); // We use a job just to let all current build jobs finish - but we need to spawn // a new thread within the job to avoid this job to block other operations that // we may want perform as the emulator is running. Job job = new Job("Launching") { @Override public IStatus run(IProgressMonitor monitor) { final int emulatorId = getNextId(); launchAsync(launchConfig, mode, launch, emulatorId, monitor); return Status.OK_STATUS; } }; job.setRule(project); job.setSystem(true); job.schedule(); } @Override public boolean preLaunchCheck(ILaunchConfiguration configuration, String mode, IProgressMonitor monitor) throws CoreException { // Clear temporary attributes clearTemporaryAttributes(configuration); boolean result = true; IProject project = getProject(configuration); if (!shouldAutoSwitch(configuration, mode)) { MoSyncProject mosyncProject = MoSyncProject.create(project); IBuildConfiguration activeCfg = mosyncProject.getActiveBuildConfiguration(); String[] preferredTypes = getRequiredBuildConfigTypes(mode); if (activeCfg == null || !activeCfg.getTypes().containsAll(Arrays.asList(preferredTypes))) { result = showSwitchConfigDialog(mosyncProject, mode, activeCfg, preferredTypes); } } IEmulatorLauncher launcher = getEmulatorLauncher(configuration, mode); int tries = 0; while (launcher.isLaunchable(configuration, mode) == IEmulatorLauncher.REQUIRES_CONFIGURATION && tries < 2) { tries++; IEmulatorLauncher fallbackLauncher = launcher.configure(configuration, mode); if (fallbackLauncher == null) { return false; } else if (!fallbackLauncher.getId().equals(launcher.getId())) { launcher = fallbackLauncher; setTemporaryAttribute(configuration, ILaunchConstants.LAUNCH_DELEGATE_ID, fallbackLauncher.getId()); } } // And a final check in case configuration failed. assertLaunchable(launcher, configuration, mode); return result && super.preLaunchCheck(configuration, mode, monitor); } private static void clearTemporaryAttributes(ILaunchConfiguration configuration) { overriddenAttributes.remove(configuration); } private static void setTemporaryAttribute(ILaunchConfiguration configuration, String attr, Object value) { Map<String, Object> attributes = overriddenAttributes.get(configuration); if (attributes == null) { attributes = new HashMap<String, Object>(); overriddenAttributes.put(configuration, attributes); } attributes.put(attr, value); } private static Object getTemporaryAttribute(ILaunchConfiguration configuration, String attr) { Map<String, Object> attributes = overriddenAttributes.get(configuration); return attributes == null ? null : attributes.get(attr); } public static IEmulatorLauncher getSessionLauncher(ILaunchConfiguration config) { String launcherId = (String) getTemporaryAttribute(config, ILaunchConstants.LAUNCH_DELEGATE_ID); return CoreMoSyncPlugin.getDefault().getEmulatorLauncher(launcherId); } private void assertLaunchable(IEmulatorLauncher launcher, ILaunchConfiguration launchConfig, String mode) throws CoreException { if (launcher.isLaunchable(launchConfig, mode) != IEmulatorLauncher.LAUNCHABLE) { IBuildVariant variant = getVariant(launchConfig, mode); throw new CoreException( new Status( IStatus.ERROR, CoreMoSyncPlugin.PLUGIN_ID, MessageFormat .format("Cannot use {0} in execution mode \"{1}\" on platform {2}.", launcher.getName(), mode, variant.getProfile()))); } } private boolean showSwitchConfigDialog(MoSyncProject mosyncProject, String mode, final IBuildConfiguration activeCfg, String[] requiredTypes) { if (isDebugMode(mode)) { Display d = PlatformUI.getWorkbench().getDisplay(); final boolean[] result = new boolean[1]; d.syncExec(new Runnable() { @Override public void run() { Shell shell = PlatformUI.getWorkbench().getActiveWorkbenchWindow().getShell(); MessageDialog dialog = new MessageDialog(shell, "Incompatible build configuration", null, MessageFormat.format("The build configuration \"{0}\" is not intended for debugging. Debug anyway?", activeCfg.getId()), MessageDialog.WARNING, new String[] { "Debug", "Cancel" }, 1); result[0] = dialog.open() == 0; } }); return result[0]; } return true; } /** * Clients may override. This method returns the set of required * build configuration types for this launch. (Used to determine * whether to show a dialog to user). * @param mode * @return */ protected String[] getRequiredBuildConfigTypes(String mode) { return new String[] { isDebugMode(mode) ? IBuildConfiguration.DEBUG_TYPE : IBuildConfiguration.RELEASE_TYPE }; } /** * Returns the default build configuration to use for a given mode (debug or launch). * @param mode * @return */ public static String getDefaultBuildConfiguration(MoSyncProject project, String mode) { String type = "debug".equals(mode) ? IBuildConfiguration.DEBUG_TYPE : IBuildConfiguration.RELEASE_TYPE; SortedSet<String> candidateCfgs = project == null ? new TreeSet<String>() : project.getBuildConfigurationsOfType(type); if (candidateCfgs.isEmpty()) { return "debug".equals(mode) ? IBuildConfiguration.DEBUG_ID : IBuildConfiguration.RELEASE_ID; } else { return candidateCfgs.first(); } } public void launchAsync(final ILaunchConfiguration launchConfig, final String mode, final ILaunch launch, final int emulatorId, final IProgressMonitor monitor) { Thread t = new Thread(new Runnable() { @Override public void run() { try { launchSync(launchConfig, mode, launch, emulatorId, monitor); } catch (CoreException e) { StatusManager.getManager().handle(e.getStatus(), StatusManager.SHOW); } } }, MessageFormat.format("Emulator {0}", emulatorId)); t.setDaemon(true); t.start(); } public void launchSync(ILaunchConfiguration launchConfig, String mode, ILaunch launch, int emulatorId, IProgressMonitor monitor) throws CoreException { IProject project = getProject(launchConfig); IBuildVariant variant = getVariant(launchConfig, mode); if (!MoSyncNature.hasNature(project) && MoSyncNature.isCompatible(project)) { throw new CoreException(new Status(IStatus.ERROR, CoreMoSyncPlugin.PLUGIN_ID, MessageFormat.format( "Could not launch ''{0}'' - please upgrade this project to the new MoSync project type (available in the context menu)", project.getName()))); } if (project.findMaxProblemSeverity(ICModelMarker.C_MODEL_PROBLEM_MARKER, true, IResource.DEPTH_INFINITE) == IMarker.SEVERITY_ERROR) { throw new CoreException(new Status(IStatus.ERROR, CoreMoSyncPlugin.PLUGIN_ID, MessageFormat.format("Could not launch; build errors in project {0}", project.getName()))); } if (!getLaunchDir(MoSyncProject.create(project), variant).toFile().exists()) { throw new CoreException(new Status(IStatus.ERROR, CoreMoSyncPlugin.PLUGIN_ID, "Could not find build directory - please make sure your project is built")); } MoSyncProject mosyncProject = MoSyncProject.create(project); if (MoSyncBuilder.isLib(mosyncProject)) { throw new CoreException(new Status(IStatus.ERROR, CoreMoSyncPlugin.PLUGIN_ID, "Cannot execute a library; please compile as application")); } if (MoSyncBuilder.isExtension(mosyncProject)) { throw new CoreException(new Status(IStatus.ERROR, CoreMoSyncPlugin.PLUGIN_ID, "Cannot execute an extension; please compile as application")); } launchDelegate(launchConfig, mode, launch, emulatorId, monitor); } public void launchDelegate(ILaunchConfiguration launchConfig, String mode, ILaunch launch, int emulatorId, IProgressMonitor monitor) throws CoreException { IEmulatorLauncher launcher = getEmulatorLauncher(launchConfig, mode); launcher.launch(launchConfig, mode, launch, emulatorId, monitor); } public static IEmulatorLauncher getEmulatorLauncher(ILaunchConfiguration launchConfig, String mode) throws CoreException { IEmulatorLauncher launcher = getSessionLauncher(launchConfig); if (launcher != null) { return launcher; } String delegateId = getLaunchDelegateId(launchConfig, mode, true); launcher = CoreMoSyncPlugin.getDefault().getEmulatorLauncher(delegateId); if (launcher == null) { throw new CoreException(new Status(IStatus.ERROR, CoreMoSyncPlugin.PLUGIN_ID, "Could not find emulator for launching.")); } return launcher; } protected String getLaunchDelegateId(ILaunchConfiguration launchConfig, String mode) throws CoreException { return getLaunchDelegateId(launchConfig, mode, allowsExternalEmulators()); } protected static String getLaunchDelegateId(ILaunchConfiguration launchConfig, String mode, boolean allowExternalEmulators) throws CoreException { if (allowExternalEmulators) { String delegateId = launchConfig.getAttribute(ILaunchConstants.LAUNCH_DELEGATE_ID, MoReLauncher.ID); String temporaryDelegateId = (String) getTemporaryAttribute(launchConfig, ILaunchConstants.LAUNCH_DELEGATE_ID); return temporaryDelegateId == null ? delegateId : temporaryDelegateId; } else { // Fallback is MoRe. return MoReLauncher.ID; } } /** * Returns whether this launch configuration allows other emulators * than the default. Clients may override. * @return */ protected boolean allowsExternalEmulators() { return true; } /** * Returns the launch directory of this launch. * @param project * @return */ public static IPath getLaunchDir(MoSyncProject project, IBuildVariant variant) { return MoSyncBuilder.getOutputPath(project.getWrappedProject(), variant); } public static IBuildVariant getVariant(ILaunchConfiguration launchConfig, String mode) throws CoreException { IEmulatorLauncher emulatorLauncher = getEmulatorLauncher(launchConfig, mode); IBuildVariant variant = emulatorLauncher.getVariant(launchConfig, mode); return variant; } public static IProject getProject(ILaunchConfiguration launchConfig) throws CoreException { return MoSyncBuilder.getProject(launchConfig); } private int getNextId() { synchronized (EmulatorLaunchConfigurationDelegate.class) { GLOBAL_ID %= 256; return GLOBAL_ID++; } } @Override public ILaunch getLaunch(ILaunchConfiguration configuration, String mode) throws CoreException { //ISourceLookupDirector commonSourceLookupDirector = CDebugCorePlugin.getDefault().getCommonSourceLookupDirector(); Launch launch = new Launch(configuration, mode, null); // We implement this ourselves so we can add source lookup (and hence niceties like // clicking on stack trace -> open editor setDefaultSourceLocator(launch, configuration); return launch; } protected void setDefaultSourceLocator(ILaunch launch, ILaunchConfiguration configuration) throws CoreException { if (launch.getSourceLocator() == null) { IPersistableSourceLocator sourceLocator; String id = configuration.getAttribute(ILaunchConfiguration.ATTR_SOURCE_LOCATOR_ID, (String)null); if (id == null) { sourceLocator = CDebugUIPlugin.createDefaultSourceLocator(); if (sourceLocator instanceof AbstractSourceLookupDirector) { ((AbstractSourceLookupDirector)sourceLocator).setId(CDebugUIPlugin.getDefaultSourceLocatorID()); } sourceLocator.initializeDefaults(configuration); } else { sourceLocator = DebugPlugin.getDefault().getLaunchManager().newSourceLocator(id); String memento = configuration.getAttribute(ILaunchConfiguration.ATTR_SOURCE_LOCATOR_MEMENTO, (String)null); if (memento == null) { sourceLocator.initializeDefaults(configuration); } else { sourceLocator.initializeFromMemento(memento); } } launch.setSourceLocator(sourceLocator); } } public static void configureLaunchConfigForSourceLookup(ILaunchConfigurationWorkingCopy wc) { wc.setAttribute(ILaunchConfiguration.ATTR_SOURCE_LOCATOR_ID, CDebugUIPlugin.getDefaultSourceLocatorID()); } @Override public boolean buildForLaunch(ILaunchConfiguration configuration, String mode, IProgressMonitor monitor) throws CoreException { final IProject project = getProject(configuration); IBuildVariant variant = getVariant(configuration, mode); IBuildSession session = new BuildSession(Arrays.asList(variant), BuildSession.DO_SAVE_DIRTY_EDITORS | BuildSession.DO_BUILD_RESOURCES | BuildSession.DO_LINK | BuildSession.DO_PACK); // No dialogs should pop up. Job job = new MoSyncBuildJob(MoSyncProject.create(project), session, variant); job.setName("Prelaunch build"); job.schedule(); return false; } protected static boolean shouldAutoSwitch(ILaunchConfiguration configuration, String mode) throws CoreException { String autoChangeConfigKey = isDebugMode(mode) ? ILaunchConstants.AUTO_CHANGE_CONFIG_DEBUG : ILaunchConstants.AUTO_CHANGE_CONFIG; return configuration.getAttribute(autoChangeConfigKey, false); } public static IBuildConfiguration getAutoSwitchBuildConfiguration(ILaunchConfiguration configuration, String mode) throws CoreException { IProject project = getProject(configuration); MoSyncProject mosyncProject = MoSyncProject.create(project); // We'll let non-mosync projects slip through; they'll be handled in launchSync if (mosyncProject != null && mosyncProject.areBuildConfigurationsSupported()) { String buildConfigKey = isDebugMode(mode) ? ILaunchConstants.BUILD_CONFIG_DEBUG : ILaunchConstants.BUILD_CONFIG; if (shouldAutoSwitch(configuration, mode)) { String buildConfig = configuration.getAttribute(buildConfigKey, getDefaultBuildConfiguration(mosyncProject, mode)); IBuildConfiguration activeBuildConfig = mosyncProject.getActiveBuildConfiguration(); String activeBuildConfigId = activeBuildConfig == null ? null : activeBuildConfig.getId(); if (buildConfig != null && !buildConfig.equals(activeBuildConfigId) && mosyncProject.getBuildConfiguration(buildConfig) != null) { return mosyncProject.getBuildConfiguration(buildConfig); } } } return mosyncProject.getActiveBuildConfiguration(); } public static boolean isDebugMode(String mode) { boolean isDebugMode = "debug".equals(mode); return isDebugMode; } public static boolean doesConfigMatch(ILaunchConfiguration config, IProject project, String mode, Map<String, Object> matchingParams) throws CoreException { boolean sameProject = config.getAttribute(ILaunchConstants.PROJECT, "").equals(project.getName()); if (!sameProject) { return false; } // else, interesting... IEmulatorLauncher emulator = EmulatorLaunchConfigurationDelegate.getEmulatorLauncher(config, mode); if (emulator.isLaunchable(config, mode) == IEmulatorLauncher.UNLAUNCHABLE) { return false; } if (matchingParams != null) { Map configParams = config.getAttributes(); for (Map.Entry<String, Object> matchingParam : matchingParams.entrySet()) { Object configValue = configParams.get(matchingParam.getKey()); if (!Util.equals(configValue, matchingParam.getValue())) { return false; } } } return true; } }