/* * Copyright (c) 2013, the Dart project authors. * * Licensed under the Eclipse Public License v1.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.eclipse.org/legal/epl-v10.html * * 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.dart.tools.debug.core.configs; import com.google.dart.engine.utilities.instrumentation.InstrumentationBuilder; import com.google.dart.tools.core.DartCore; import com.google.dart.tools.core.model.DartSdkManager; import com.google.dart.tools.core.utilities.net.NetUtils; import com.google.dart.tools.debug.core.DartDebugCorePlugin; import com.google.dart.tools.debug.core.DartLaunchConfigWrapper; import com.google.dart.tools.debug.core.DartLaunchConfigurationDelegate; import com.google.dart.tools.debug.core.DebugUIHelper; import com.google.dart.tools.debug.core.dartium.DartiumDebugTarget; import com.google.dart.tools.debug.core.util.BrowserManager; import com.google.dart.tools.debug.core.util.IResourceResolver; import com.google.dart.tools.debug.core.webkit.ChromiumConnector; import com.google.dart.tools.debug.core.webkit.ChromiumTabInfo; import com.google.dart.tools.debug.core.webkit.WebkitConnection; import org.eclipse.core.resources.IContainer; import org.eclipse.core.resources.IFile; import org.eclipse.core.resources.IResource; import org.eclipse.core.resources.ResourcesPlugin; import org.eclipse.core.runtime.CoreException; import org.eclipse.core.runtime.IProgressMonitor; import org.eclipse.core.runtime.IStatus; import org.eclipse.core.runtime.Status; import org.eclipse.debug.core.DebugException; import org.eclipse.debug.core.DebugPlugin; import org.eclipse.debug.core.ILaunch; import org.eclipse.debug.core.ILaunchConfiguration; import org.eclipse.debug.core.ILaunchManager; import org.eclipse.debug.core.model.IProcess; import java.io.File; import java.io.IOException; import java.util.ArrayList; import java.util.HashMap; import java.util.List; import java.util.Map; //[ { // "title": "New Tab", // "type": "page", // "url": "chrome://newtab/", //}, { // "title": "chrome-extension://becjelbpddbpmopbobpojhgneicbhlgj/_generated_background_page.html", // "type": "other", // "url": "chrome-extension://becjelbpddbpmopbobpojhgneicbhlgj/_generated_background_page.html", //}, { // "title": "Packy", // "type": "other", // "url": "chrome-extension://becjelbpddbpmopbobpojhgneicbhlgj/packy.html", //} ] /** * A ILaunchConfigurationDelegate implementation that can launch Chrome applications. We * conceptually launch the manifest.json file which specifies a Chrome app. We currently send * Dartium the path to the manifest file's parent directory via the --load-extension flag. */ public class ChromeAppLaunchConfigurationDelegate extends DartLaunchConfigurationDelegate { private static class ChromeAppResourceResolver implements IResourceResolver { private IContainer container; String prefix; public ChromeAppResourceResolver(IContainer container, ChromiumTabInfo tab) { this.container = container; prefix = tab.getUrl(); int index = prefix.indexOf("//"); if (index != -1) { index = prefix.indexOf('/', index + 2); if (index != -1) { prefix = prefix.substring(0, index + 1); } } } @Override public String getUrlForFile(File file) { IFile[] files = ResourcesPlugin.getWorkspace().getRoot().findFilesForLocationURI(file.toURI()); if (files.length > 0) { return getUrlForResource(files[0]); } else { return null; } } @Override public String getUrlForResource(IResource resource) { String relPath = calcRelPath(container, resource); if (relPath != null) { return prefix + relPath; } else { return null; } } @Override public String getUrlRegexForResource(IResource resource) { //final String PACKAGES = "/packages/"; String relPath = calcRelPath(container, resource); if (relPath != null) { return relPath; } return resource.getFullPath().toString(); // if (resourcePath.contains(PACKAGES)) { // int index = resourcePath.indexOf(PACKAGES); // // return resourcePath.substring(index + PACKAGES.length()); // } // // return null; } @Override public IResource resolveUrl(String url) { if (url.startsWith(prefix)) { return container.findMember(url.substring(prefix.length())); } else { return null; } } private String calcRelPath(IContainer container, IResource resource) { if (container == null) { return null; } String containerPath = container.getFullPath().toString(); String resourcePath = resource.getFullPath().toString(); if (resourcePath.startsWith(containerPath)) { String relPath = resourcePath.substring(containerPath.length()); if (relPath.startsWith("/")) { return relPath.substring(1); } else { return relPath; } } else { return null; } } } private static final int DEFAULT_DEBUGGER_PORT = 9422; private static Process chromeAppBrowserProcess; /** * Create a new ChromeAppLaunchConfigurationDelegate. */ public ChromeAppLaunchConfigurationDelegate() { } @Override public void doLaunch(ILaunchConfiguration configuration, String mode, ILaunch launch, IProgressMonitor monitor, InstrumentationBuilder instrumentation) throws CoreException { if (!ILaunchManager.RUN_MODE.equals(mode) && !ILaunchManager.DEBUG_MODE.equals(mode)) { throw new CoreException(DartDebugCorePlugin.createErrorStatus("Execution mode '" + mode + "' is not supported.")); } boolean enableDebugging = ILaunchManager.DEBUG_MODE.equals(mode); File dartium = DartSdkManager.getManager().getDartiumExecutable(); if (dartium == null) { throw new CoreException(new Status( IStatus.ERROR, DartDebugCorePlugin.PLUGIN_ID, "Could not find Dartium")); } DartLaunchConfigWrapper wrapper = new DartLaunchConfigWrapper(configuration); wrapper.markAsLaunched(); IResource jsonResource = wrapper.getApplicationResource(); if (jsonResource == null) { throw newDebugException("No file specified to launch"); } File cwd = getWorkingDirectory(jsonResource); String extensionPath = jsonResource.getParent().getLocation().toFile().getAbsolutePath(); List<String> commandsList = new ArrayList<String>(); commandsList.add(dartium.getAbsolutePath()); commandsList.add("--user-data-dir=" + BrowserManager.getCreateUserDataDirectoryPath("chrome-apps")); commandsList.add("--no-first-run"); commandsList.add("--no-default-browser-check"); // This is currently only supported on the mac. if (DartCore.isMac()) { commandsList.add("--no-startup-window"); } commandsList.add("--load-and-launch-app=" + extensionPath); for (String arg : wrapper.getArgumentsAsArray()) { commandsList.add(arg); } int devToolsPortNumber = DEFAULT_DEBUGGER_PORT; if (enableDebugging) { devToolsPortNumber = NetUtils.findUnusedPort(DEFAULT_DEBUGGER_PORT); commandsList.add("--remote-debugging-port=" + devToolsPortNumber); } monitor.beginTask("Dartium", IProgressMonitor.UNKNOWN); terminatePreviousLaunch(); String[] commands = commandsList.toArray(new String[commandsList.size()]); ProcessBuilder processBuilder = new ProcessBuilder(commands); processBuilder.directory(cwd); Map<String, String> wrapperEnv = wrapper.getEnvironment(); if (!wrapperEnv.isEmpty()) { Map<String, String> env = processBuilder.environment(); for (String key : wrapperEnv.keySet()) { env.put(key, wrapperEnv.get(key)); } } // Add the environment variable DART_FLAGS="--enable-checked-mode" to enable asserts and type // checks. Default to false for Chrome apps. if (wrapper.getCheckedMode(false)) { Map<String, String> env = processBuilder.environment(); env.put("DART_FLAGS", "--enable-checked-mode"); } Process runtimeProcess = null; try { runtimeProcess = processBuilder.start(); } catch (IOException ioe) { throw newDebugException(ioe); } saveLaunchedProcess(runtimeProcess); if (enableDebugging) { try { // Poll until we find a good tab to connect to. ChromiumTabInfo tab = getChromiumTab(runtimeProcess, devToolsPortNumber); if (tab != null && tab.getWebSocketDebuggerUrl() != null) { WebkitConnection connection = new WebkitConnection( tab.getHost(), tab.getPort(), tab.getWebSocketDebuggerFile()); final DartiumDebugTarget debugTarget = new DartiumDebugTarget( dartium.getName(), connection, launch, runtimeProcess, new ChromeAppResourceResolver(jsonResource.getParent(), tab), true, false); monitor.worked(1); launch.setAttribute(DebugPlugin.ATTR_CONSOLE_ENCODING, "UTF-8"); launch.addDebugTarget(debugTarget); launch.addProcess(debugTarget.getProcess()); try { debugTarget.openConnection(); } catch (IOException ioe) { DartDebugCorePlugin.logError(ioe); } } // Give the app a little time to open the main window. sleep(500); DebugUIHelper.getHelper().activateApplication(dartium, "Chromium"); } catch (CoreException ce) { DartDebugCorePlugin.logError(ce); } } else { Map<String, String> processAttributes = new HashMap<String, String>(); processAttributes.put(IProcess.ATTR_PROCESS_TYPE, "Dartium"); IProcess eclipseProcess = DebugPlugin.newProcess( launch, runtimeProcess, configuration.getName(), processAttributes); if (eclipseProcess == null) { throw newDebugException("Error starting Dartium"); } // We need to wait until the process is started before we can try and activate the window. sleep(1000); DebugUIHelper.getHelper().activateApplication(dartium, "Chromium"); } monitor.done(); } private ChromiumTabInfo findTargetTab(List<ChromiumTabInfo> tabs) { for (ChromiumTabInfo tab : tabs) { if (tab.getTitle().startsWith("chrome-extension://")) { continue; } // chrome-extension://kohcodfehgoaolndkcophkcmhjenpfmc/_generated_background_page.html if (tab.getTitle().endsWith("_generated_background_page.html")) { continue; } // chrome-extension://nkeimhogjdpnpccoofpliimaahmaaome/background.html if (tab.getUrl().endsWith("_generated_background_page.html") || tab.getUrl().endsWith("/background.html")) { continue; } if (tab.getUrl().startsWith("chrome-extension://") && tab.getTitle().length() > 0) { return tab; } } return null; } private ChromiumTabInfo getChromiumTab(Process process, int port) throws CoreException { // Give Chromium 10 seconds to start up. final int maxStartupDelay = 10 * 1000; long endTime = System.currentTimeMillis() + maxStartupDelay; while (true) { if (isProcessTerminated(process)) { throw new CoreException(new Status( IStatus.ERROR, DartDebugCorePlugin.PLUGIN_ID, "Could not launch browser - process terminated while trying to connect. " + "Try closing any running Dartium instances.")); } try { List<ChromiumTabInfo> tabs = ChromiumConnector.getAvailableTabs(port); ChromiumTabInfo targetTab = findTargetTab(tabs); if (targetTab != null) { for (ChromiumTabInfo tab : tabs) { DartDebugCorePlugin.log("Found: " + tab.toString()); } DartDebugCorePlugin.log("Choosing: " + targetTab); return targetTab; } } catch (IOException exception) { if (System.currentTimeMillis() > endTime) { throw new CoreException(new Status( IStatus.ERROR, DartDebugCorePlugin.PLUGIN_ID, "Could not connect to Dartium", exception)); } } if (System.currentTimeMillis() > endTime) { throw new CoreException(new Status( IStatus.ERROR, DartDebugCorePlugin.PLUGIN_ID, "Timed out trying to connect to Dartium")); } sleep(250); } } /** * Return the parent of the Chrome app directory. The Chrome app directory contains the given * manifest.json file. * * @param jsonResource * @return */ private File getWorkingDirectory(IResource jsonResource) { IContainer containingDir = jsonResource.getParent(); File containingFile = containingDir.getLocation().toFile(); // Return the parent of this directory. return containingFile.getParentFile(); } private boolean isProcessTerminated(Process process) { try { if (process != null) { process.exitValue(); } return true; } catch (IllegalThreadStateException ex) { return false; } } private DebugException newDebugException(String message) { return new DebugException(new Status(IStatus.ERROR, DartDebugCorePlugin.PLUGIN_ID, message)); } private DebugException newDebugException(Throwable t) { return new DebugException(new Status( IStatus.ERROR, DartDebugCorePlugin.PLUGIN_ID, t.toString(), t)); } /** * Store the successfully launched process into a static variable; * * @param process */ private void saveLaunchedProcess(Process process) { chromeAppBrowserProcess = process; } private void sleep(int millis) { try { Thread.sleep(millis); } catch (Exception exception) { } } private void terminatePreviousLaunch() { if (chromeAppBrowserProcess != null) { try { chromeAppBrowserProcess.exitValue(); chromeAppBrowserProcess = null; } catch (IllegalThreadStateException ex) { // exitValue() will throw if the process has not yet stopped. In that case, we ask it to. chromeAppBrowserProcess.destroy(); chromeAppBrowserProcess = null; // Delay a bit. sleep(100); } } } }