package com.mobilesorcery.sdk.html5; import java.io.IOException; import java.net.InetAddress; import java.net.URL; import java.text.MessageFormat; import java.util.ArrayList; import java.util.Collection; import java.util.Collections; import java.util.HashMap; import java.util.HashSet; import java.util.List; import org.eclipse.core.resources.IFile; import org.eclipse.core.resources.IProject; import org.eclipse.core.resources.IProjectDescription; import org.eclipse.core.resources.IResource; import org.eclipse.core.runtime.CoreException; import org.eclipse.core.runtime.IPath; import org.eclipse.core.runtime.IStatus; import org.eclipse.core.runtime.NullProgressMonitor; import org.eclipse.core.runtime.Path; import org.eclipse.core.runtime.Status; import org.eclipse.core.runtime.preferences.DefaultScope; import org.eclipse.core.runtime.preferences.IEclipsePreferences; import org.eclipse.core.runtime.preferences.InstanceScope; import org.eclipse.core.runtime.preferences.IEclipsePreferences.IPreferenceChangeListener; import org.eclipse.core.runtime.preferences.IEclipsePreferences.PreferenceChangeEvent; import org.eclipse.debug.core.DebugPlugin; import org.eclipse.debug.core.ILaunch; import org.eclipse.debug.core.ILaunchConfiguration; import org.eclipse.debug.core.ILaunchesListener2; import org.eclipse.jface.dialogs.MessageDialog; import org.eclipse.jface.util.Policy; import org.eclipse.swt.widgets.Display; import org.eclipse.swt.widgets.Shell; import org.eclipse.ui.IStartup; import org.eclipse.ui.IWorkbench; import org.eclipse.ui.PlatformUI; import org.eclipse.ui.internal.util.Util; import org.eclipse.ui.plugin.AbstractUIPlugin; import org.eclipse.wst.jsdt.core.IJavaScriptProject; import org.eclipse.wst.jsdt.core.JavaScriptCore; import org.eclipse.wst.jsdt.core.LibrarySuperType; import org.eclipse.wst.jsdt.debug.internal.core.Constants; import org.eclipse.wst.jsdt.debug.internal.core.JavaScriptDebugPlugin; import org.eclipse.wst.jsdt.internal.core.JavaProject; import org.osgi.framework.BundleContext; import com.mobilesorcery.sdk.core.BuildVariant; import com.mobilesorcery.sdk.core.CoreMoSyncPlugin; import com.mobilesorcery.sdk.core.IBuildVariant; import com.mobilesorcery.sdk.core.IProcessConsole; import com.mobilesorcery.sdk.core.IPropertyOwner; import com.mobilesorcery.sdk.core.MoSyncBuilder; import com.mobilesorcery.sdk.core.MoSyncProject; import com.mobilesorcery.sdk.core.PrivilegedAccess; import com.mobilesorcery.sdk.core.PropertyUtil; import com.mobilesorcery.sdk.core.build.BuildSequence; import com.mobilesorcery.sdk.core.build.IBuildStepFactory; import com.mobilesorcery.sdk.core.build.ResourceBuildStep; import com.mobilesorcery.sdk.html5.debug.JSODDLaunchConfigurationDelegate; import com.mobilesorcery.sdk.html5.debug.JSODDSupport; import com.mobilesorcery.sdk.html5.debug.ReloadVirtualMachine; import com.mobilesorcery.sdk.html5.live.ILiveServerListener; import com.mobilesorcery.sdk.html5.live.JSODDServer; import com.mobilesorcery.sdk.html5.live.ReloadManager; import com.mobilesorcery.sdk.html5.ui.DebuggingEnableTester; import com.mobilesorcery.sdk.html5.ui.JSODDConnectDialog; import com.mobilesorcery.sdk.html5.ui.JSODDTimeoutDialog; import com.mobilesorcery.sdk.internal.launch.EmulatorLaunchConfigurationDelegate; import com.mobilesorcery.sdk.profiles.filter.DeviceCapabilitiesFilter; import com.mobilesorcery.sdk.ui.IWorkbenchStartupListener; import com.mobilesorcery.sdk.ui.MosyncUIPlugin; import com.mobilesorcery.sdk.ui.UIUtils; import com.mobilesorcery.sdk.ui.targetphone.ITargetPhoneTransportListener; import com.mobilesorcery.sdk.ui.targetphone.TargetPhonePlugin; import com.mobilesorcery.sdk.ui.targetphone.TargetPhoneTransportEvent; /** * The activator class controls the plug-in life cycle */ public class Html5Plugin extends AbstractUIPlugin implements IStartup, ITargetPhoneTransportListener, ILaunchesListener2 { // The plug-in ID public static final String PLUGIN_ID = "com.mobilesorcery.sdk.html5"; //$NON-NLS-1$ public static final String JS_PROJECT_SUPPORT_PROP = PLUGIN_ID + ".support"; public static final String HTML5_TEMPLATE_TYPE = "html5"; public static final String ODD_SUPPORT_PROP = PLUGIN_ID + ".odd"; public static final String ANONYMOUS_FUNCTION = "<anonymous>"; static final String RELOAD_STRATEGY_PREF = "reload.strategy"; static final String SOURCE_CHANGE_STRATEGY_PREF = "source.change.strategy"; static final String SHOULD_FETCH_REMOTELY_PREF = "fetch.remotely"; static final String ODD_SUPPORT_PREF = "odd.support"; static final String USE_DEFAULT_SERVER_URL_PREF = "use.default.server"; static final String SERVER_URL_PREF = "server"; public static final String TIMEOUT_PREF = "timeout"; public static final int DEFAULT_TIMEOUT = 5; public static final int MINIMUM_TIMEOUT = 2; public static final int DO_NOTHING = 0; public static final int RELOAD = 1; public static final int HOT_CODE_REPLACE = 2; public static final String TERMINATE_TOKEN_LAUNCH_ATTR = PLUGIN_ID + "t.t"; public static final String SUPPRESS_TIMEOUT_LAUNCH_ATTR = PLUGIN_ID + "s.t"; // The shared instance private static Html5Plugin plugin; private ReloadManager reloadManager; private JSODDServer server; private final HashMap<IProject, JSODDSupport> jsOddSupport = new HashMap<IProject, JSODDSupport>(); private HashSet<String> supportedFeatures; private IPreferenceChangeListener breakOnExceptionsListener; // private HashMap<Object, AtomicInteger> timeoutSuppressions = new // HashMap<Object, AtomicInteger>(); /** * The constructor */ public Html5Plugin() { } /* * (non-Javadoc) * * @see * org.eclipse.ui.plugin.AbstractUIPlugin#start(org.osgi.framework.BundleContext * ) */ @Override public void start(BundleContext context) throws Exception { super.start(context); initSupportedFeatures(); initBreakpointOnExceptionListener(); // We do not yet have the eclipse fix #54993 MosyncUIPlugin.getDefault().awaitWorkbenchStartup( new IWorkbenchStartupListener() { @Override public void started(IWorkbench workbench) { DebugPlugin.getDefault().getBreakpointManager() .getBreakpoints(); } }); plugin = this; // Since we are an IStartup, this will work. TargetPhonePlugin.getDefault().addTargetPhoneTransportListener(this); DebugPlugin.getDefault().getLaunchManager().addLaunchListener(this); initReloadManager(); } /* * (non-Javadoc) * * @see * org.eclipse.ui.plugin.AbstractUIPlugin#stop(org.osgi.framework.BundleContext * ) */ @Override public void stop(BundleContext context) throws Exception { plugin = null; super.stop(context); TargetPhonePlugin.getDefault().removeTargetPhoneTransportListener(this); DebugPlugin.getDefault().getLaunchManager().removeLaunchListener(this); removeBreakpointOnExceptionListener(); disposeReloadManager(); } public JSODDServer getReloadServer() { if (server == null) { server = new JSODDServer(); // TODO:MOVE UI STUFF server.addListener(new ILiveServerListener() { @Override public void timeout(final ReloadVirtualMachine vm) { IProcessConsole console = CoreMoSyncPlugin.getDefault() .createConsole(MoSyncBuilder.CONSOLE_ID); console.addMessage( IProcessConsole.ERR, MessageFormat .format("*** A timeout occurred. The device being debugged (at {0}) seems to have been disconnected. ***", vm.getRemoteAddr())); ILaunch launch = vm.getJavaScriptDebugTarget().getLaunch(); String terminateToken = launch == null ? null : launch .getAttribute(TERMINATE_TOKEN_LAUNCH_ATTR); if (!Boolean.parseBoolean(launch .getAttribute(SUPPRESS_TIMEOUT_LAUNCH_ATTR))) { Display d = PlatformUI.getWorkbench().getDisplay(); UIUtils.onUiThread(d, new Runnable() { @Override public void run() { Shell shell = PlatformUI.getWorkbench() .getActiveWorkbenchWindow().getShell(); JSODDTimeoutDialog.openIfNecessary(shell, vm); } }, false); } else { if (CoreMoSyncPlugin.getDefault().isDebugging()) { CoreMoSyncPlugin.trace( "Suppressed timeout dialog for {0}.", terminateToken); } } } @Override public void inited(ReloadVirtualMachine vm, boolean reset) { } }); } return server; } private void initReloadManager() { this.reloadManager = new ReloadManager(); } private void disposeReloadManager() { if (reloadManager != null) { reloadManager.dispose(); } this.reloadManager = null; } /** * @deprecated Only to get hold of file id , refactor!!! * @return */ @Deprecated public ReloadManager getReloadManager() { return reloadManager; } /** * Returns the shared instance * * @return the shared instance */ public static Html5Plugin getDefault() { return plugin; } /** * Adds HTML5 support to a {@link MoSyncProject}. * * @param configureForODD * * @throws CoreException */ public void addHTML5Support(MoSyncProject project, boolean configureForODD) throws CoreException { try { BuildSequence sequence = new BuildSequence(project); List<IBuildStepFactory> factories = sequence .getBuildStepFactories(); ArrayList<IBuildStepFactory> newFactories = new ArrayList<IBuildStepFactory>(); for (IBuildStepFactory factory : factories) { if (ResourceBuildStep.ID.equals(factory.getId())) { newFactories.add(createHTML5PackagerBuildStep()); } newFactories.add(factory); } sequence.apply(newFactories); PrivilegedAccess.getInstance().grantAccess(project, true); PropertyUtil.setBoolean(project, JS_PROJECT_SUPPORT_PROP, true); configureForJSDT(project); } catch (Exception e) { throw new CoreException(new Status(IStatus.ERROR, PLUGIN_ID, "Could not create JavaScript/HTML5 project", e)); } } private void configureForJSDT(MoSyncProject mosyncProject) throws CoreException { IProject project = mosyncProject.getWrappedProject(); addJavaScriptNature(project); IJavaScriptProject jsProject = JavaScriptCore.create(project); if (jsProject instanceof JavaProject) { JavaProject jsProject1 = (JavaProject) jsProject; jsProject1.setCommonSuperType(new LibrarySuperType(new Path( "org.eclipse.wst.jsdt.launching.baseBrowserLibrary"), jsProject1, "Window")); } else { CoreMoSyncPlugin .getDefault() .logOnce( new IllegalStateException("Invalid JSDT version!!"), "JSDT"); } // Add HTML5 capability filter! DeviceCapabilitiesFilter oldFilter = DeviceCapabilitiesFilter .extractFilterFromProject(mosyncProject); HashSet<String> newCapabilities = new HashSet<String>( oldFilter.getRequiredCapabilities()); newCapabilities.add("HTML5"); DeviceCapabilitiesFilter newFilter = DeviceCapabilitiesFilter.create( newCapabilities.toArray(new String[0]), new String[0]); DeviceCapabilitiesFilter.setFilter(mosyncProject, newFilter); } private void addJavaScriptNature(IProject project) throws CoreException { if (project.hasNature(JavaScriptCore.NATURE_ID)) { return; } IProjectDescription description = project.getDescription(); String[] natures = description.getNatureIds(); String[] newNatures = new String[natures.length + 1]; System.arraycopy(natures, 0, newNatures, 0, natures.length); newNatures[newNatures.length - 1] = JavaScriptCore.NATURE_ID; description.setNatureIds(newNatures); project.setDescription(description, new NullProgressMonitor()); } public boolean hasHTML5Support(MoSyncProject project) { if (project == null) { return false; } DeviceCapabilitiesFilter filter = DeviceCapabilitiesFilter .extractFilterFromProject(project); boolean hasHTML5Capability = filter != null && filter.getRequiredCapabilities().contains("HTML5"); boolean hasSupport = hasHTML5Capability && PropertyUtil.getBoolean(project, JS_PROJECT_SUPPORT_PROP); return hasSupport; } public boolean hasHTML5PackagerBuildStep(MoSyncProject project) { BuildSequence seq = BuildSequence.getCached(project); return !seq.getBuildStepFactories( HTML5DebugSupportBuildStep.Factory.class).isEmpty(); } private IBuildStepFactory createHTML5PackagerBuildStep() { /* * BundleBuildStep.Factory factory = new BundleBuildStep.Factory(); * factory.setFailOnError(true); * factory.setName("HTML5/JavaScript bundling"); * factory.setInFile("%current-project%/LocalFiles"); * factory.setOutFile("%current-project%/Resources/LocalFiles.bin"); * return factory; */ // BAH -- We do NOT always want to copy LocalFiles to package etc HTML5DebugSupportBuildStep.Factory factory = new HTML5DebugSupportBuildStep.Factory(); return factory; } public synchronized JSODDSupport getJSODDSupport(IProject project) { if (!DebuggingEnableTester.hasDebugSupport(MoSyncProject .create(project))) { return null; } JSODDSupport result = jsOddSupport.get(project); if (result == null) { result = new JSODDSupport(project); jsOddSupport.put(project, result); } return result; } public boolean hasJSODDSupport(IProject project) { return getJSODDSupport(project) != null; } @Override public void earlyStartup() { // Just to activate the bundle. } public Collection<IProject> getProjectsWithJSODDSupport() { return Collections.unmodifiableCollection(jsOddSupport.keySet()); } /** * Returns the path where HTML5 content is stored. The path is project * relative. * * @param wrappedProject * @return */ public static IPath getHTML5Folder(IProject project) { // I guess this might always be constant... return new Path("LocalFiles"); } /** * Returns the 'local path' of a file, i e where it is located related to * its LocalFiles directory. * * @param file * @return */ public IPath getLocalPath(IFile file) { IPath root = file.getProject() .getFolder(Html5Plugin.getHTML5Folder(file.getProject())) .getFullPath(); if (root.isPrefixOf(file.getFullPath())) { return file.getFullPath().removeFirstSegments(root.segmentCount()); } return null; } public IResource getLocalFile(IProject project, IPath path) { return project.getFolder(getHTML5Folder(project)).findMember(path); } public int getTimeout() { int result = getPreferenceStore().getInt(TIMEOUT_PREF); if (result < MINIMUM_TIMEOUT) { result = MINIMUM_TIMEOUT; // Minimum; regardless of store value } return result; } public void setTimeout(int timeout) { getPreferenceStore().setValue(TIMEOUT_PREF, timeout); } public int getReloadStrategy() { // TODO. Default = 0 = UNDEFINED return getPreferenceStore().getInt(RELOAD_STRATEGY_PREF); } public void setReloadStrategy(int reloadStrategy) { getPreferenceStore().setDefault(RELOAD_STRATEGY_PREF, reloadStrategy); } public void setSourceChangeStrategy(int sourceChangeStrategy) { getPreferenceStore().setValue(SOURCE_CHANGE_STRATEGY_PREF, sourceChangeStrategy); } public int getSourceChangeStrategy() { return getPreferenceStore().getInt(SOURCE_CHANGE_STRATEGY_PREF); } public boolean shouldFetchRemotely() { boolean shouldFetchRemotely = getPreferenceStore().getBoolean( SHOULD_FETCH_REMOTELY_PREF); return shouldFetchRemotely || getSourceChangeStrategy() != DO_NOTHING; } public void setShouldFetchRemotely(boolean shouldFetchRemotely) { getPreferenceStore().setValue(SHOULD_FETCH_REMOTELY_PREF, shouldFetchRemotely); } public boolean isJSODDEnabled(MoSyncProject project) { return PropertyUtil.getBoolean(project, ODD_SUPPORT_PREF); } public void setJSODDEnabled(MoSyncProject project, boolean enabled) { PropertyUtil.setBoolean(project, ODD_SUPPORT_PREF, enabled); } @Override public void launchesRemoved(ILaunch[] launches) { // We don't care. } @Override public void launchesAdded(ILaunch[] launches) { // We do care :) for (ILaunch launch : launches) { ILaunchConfiguration cfg = launch.getLaunchConfiguration(); if (cfg != null) { try { String cfgId = cfg.getType().getIdentifier(); if (!EmulatorLaunchConfigurationDelegate.ID .equals(cfgId)) { return; } MoSyncProject project = MoSyncProject .create(EmulatorLaunchConfigurationDelegate .getProject(cfg)); IBuildVariant variant = EmulatorLaunchConfigurationDelegate .getVariant(cfg, launch.getLaunchMode()); if ("debug".equals(launch.getLaunchMode())) { launchJSODD(project, variant, false, BuildVariant.toString(variant)); } else if ("run".equals(launch.getLaunchMode()) && canLaunchJSODD(project, variant)) { launch.terminate(); Display d = PlatformUI.getWorkbench().getDisplay(); d.asyncExec(new Runnable() { @Override public void run() { MessageDialog .openError(PlatformUI.getWorkbench() .getModalDialogShellProvider() .getShell(), "Cannot launch", "JavaScript On-Device Debug can only be run in debug mode."); } }); } } catch (Exception e) { // Who cares? CoreMoSyncPlugin.getDefault().log(e); } } } } @Override public void launchesChanged(ILaunch[] launches) { // We don't care } @Override public void launchesTerminated(ILaunch[] launches) { } @Override public void handleEvent(TargetPhoneTransportEvent event) { // Launch the debug server if sending package in debug mode if (TargetPhoneTransportEvent.isType( TargetPhoneTransportEvent.ABOUT_TO_LAUNCH, event)) { MoSyncProject project = event.project; IBuildVariant variant = event.variant; launchJSODD(project, variant, true, event.phone.getName()); } } private void launchJSODD(final MoSyncProject project, final IBuildVariant variant, final boolean onDevice, final String terminateToken) { if (canLaunchJSODD(project, variant)) { new Thread(new Runnable() { public void run() { try { boolean wasLaunched = JSODDLaunchConfigurationDelegate .launchDefault(project, terminateToken); int result = JSODDConnectDialog.show(project, variant, onDevice, null); if (result == JSODDConnectDialog.CANCEL) { JSODDLaunchConfigurationDelegate .killLaunch(terminateToken); } } catch (CoreException e) { Policy.getStatusHandler() .show(e.getStatus(), "Could not launch JavaScript On-Device Debug Server"); } } }).start(); } } private boolean canLaunchJSODD(MoSyncProject project, IBuildVariant variant) { if (!DebuggingEnableTester.hasDebugSupport(project)) { return false; } if (!isJSODDEnabled(project)) { return false; } IPropertyOwner properties = MoSyncBuilder.getPropertyOwner(project, variant.getConfigurationId()); return PropertyUtil.getBoolean(properties, MoSyncBuilder.USE_DEBUG_RUNTIME_LIBS); } /* * private int incTimeoutSuppression(Object terminateToken, int increment) { * // We don't scare users with timeouts if we just // started another * session for potentially the same // device / app (since a timeout is * expected // and not an error condition. AtomicInteger suppressionCount = * timeoutSuppressions.get(terminateToken); if (suppressionCount == null) { * suppressionCount = new AtomicInteger(); * timeoutSuppressions.put(terminateToken, suppressionCount); } int result = * suppressionCount.addAndGet(increment); if (result == 0) { * timeoutSuppressions.remove(terminateToken); } return result; } */ public URL getServerURL() throws IOException { if (useDefaultServerURL()) { return getDefaultServerURL(); } else { return new URL(getPreferenceStore().getString(SERVER_URL_PREF)); } } public boolean useDefaultServerURL() { return getPreferenceStore().getBoolean(USE_DEFAULT_SERVER_URL_PREF); } public void setServerURL(String addr, boolean useDefault) throws IOException { // Just to get an exception. URL url = new URL(addr); getPreferenceStore().setValue(USE_DEFAULT_SERVER_URL_PREF, useDefault); getPreferenceStore().setValue(SERVER_URL_PREF, addr); } public URL getDefaultServerURL() throws IOException { InetAddress localHost = InetAddress.getLocalHost(); String host = localHost.getHostAddress(); return new URL("http", host, 8511, ""); } private void initSupportedFeatures() { supportedFeatures = new HashSet<String>(); supportedFeatures.add(JSODDSupport.LINE_BREAKPOINTS); supportedFeatures.add(JSODDSupport.ARTIFICIAL_STACK); supportedFeatures.add(JSODDSupport.DROP_TO_FRAME); String supportedFeaturesArg = System.getProperty(Html5Plugin.PLUGIN_ID + ".jsodd.features"); if (supportedFeaturesArg != null) { String[] supportedFeatures = supportedFeaturesArg.split(",\\s"); for (String supportedFeature : supportedFeatures) { boolean remove = supportedFeature.startsWith("-"); boolean explicitAdd = supportedFeature.startsWith("+"); if (remove || explicitAdd) { supportedFeature = supportedFeature.substring(1); } if (remove) { this.supportedFeatures.remove(supportedFeature); } else { this.supportedFeatures.add(supportedFeature); } } } } public boolean isFeatureSupported(String feature) { // Ok, one more tricky thing left: binding of function defined // within function - then we can enable this. // return !JSODDSupport.EDIT_AND_CONTINUE.equals(feature); return supportedFeatures.contains(feature); } private void initBreakpointOnExceptionListener() { // Ok, this is a ridiculous workaround -- JSDT does not react to // property changes // where the property value is null. The problem is that enabling // exceptions // is the default value, which just happens to be propagated as null in // the // preference change event. Did anyone even test that functionality?? final IEclipsePreferences node = InstanceScope.INSTANCE .getNode(JavaScriptDebugPlugin.PLUGIN_ID); final IEclipsePreferences defaultNode = DefaultScope.INSTANCE .getNode(JavaScriptDebugPlugin.PLUGIN_ID); breakOnExceptionsListener = new IPreferenceChangeListener() { private boolean inChange = false; @Override public void preferenceChange(PreferenceChangeEvent event) { if (Constants.SUSPEND_ON_THROWN_EXCEPTION .equals(event.getKey())) { if (!inChange && event.getNewValue() == null) { inChange = true; try { // This is the default value -- to trigger this so // JSDT understands it is horrible // but works. defaultNode.putBoolean( Constants.SUSPEND_ON_THROWN_EXCEPTION, false); node.putBoolean( Constants.SUSPEND_ON_THROWN_EXCEPTION, true); defaultNode .putBoolean( Constants.SUSPEND_ON_THROWN_EXCEPTION, true); } finally { inChange = false; } } } } }; node.addPreferenceChangeListener(breakOnExceptionsListener); } private void removeBreakpointOnExceptionListener() { IEclipsePreferences node = InstanceScope.INSTANCE .getNode(JavaScriptDebugPlugin.PLUGIN_ID); node.removePreferenceChangeListener(breakOnExceptionsListener); } }