/* * Copyright (c) 2012, 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.util; 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.DebugUIHelper; import com.google.dart.tools.debug.core.dartium.DartiumDebugTarget; import com.google.dart.tools.debug.core.util.ListeningStream.StreamListener; 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.DefaultChromiumTabChooser; import com.google.dart.tools.debug.core.webkit.IChromiumTabChooser; import com.google.dart.tools.debug.core.webkit.WebkitConnection; import org.eclipse.core.resources.IFile; 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.Path; 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.model.IDebugTarget; import org.eclipse.debug.core.model.IProcess; import java.io.File; import java.io.IOException; import java.io.InputStream; import java.util.ArrayList; import java.util.List; import java.util.Map; /** * A manager that launches and manages configured browsers. */ public class BrowserManager { /** The initial page to navigate to. */ private static final String INITIAL_PAGE = "chrome://version/"; private static final int DEVTOOLS_PORT_NUMBER = 9322; private static BrowserManager manager = new BrowserManager(); private static Process browserProcess = null; /** * Create a Chrome user data directory, and return the path to that directory. * * @return the user data directory path */ public static String getCreateUserDataDirectoryPath(String baseName) { String dataDirPath = System.getProperty("user.home") + File.separator + "." + baseName; File dataDir = new File(dataDirPath); if (!dataDir.exists()) { dataDir.mkdir(); } else { // Remove the "<dataDir>/Default/Current Tabs" file if it exists - it can cause old tabs to // restore themselves when we launch the browser. File defaultDir = new File(dataDir, "Default"); if (defaultDir.exists()) { File tabInfoFile = new File(defaultDir, "Current Tabs"); if (tabInfoFile.exists()) { tabInfoFile.delete(); } File sessionInfoFile = new File(defaultDir, "Current Session"); if (sessionInfoFile.exists()) { sessionInfoFile.delete(); } } } return dataDirPath; } public static BrowserManager getManager() { return manager; } private int devToolsPortNumber; private IChromiumTabChooser tabChooser; public BrowserManager() { this.tabChooser = new DefaultChromiumTabChooser(); } public void dispose() { if (!isProcessTerminated(browserProcess)) { browserProcess.destroy(); } } /** * Launch the browser and open the given file. If debug mode also connect to browser. */ public void launchBrowser(ILaunch launch, DartLaunchConfigWrapper launchConfig, IFile file, IProgressMonitor monitor, boolean enableDebugging, IResourceResolver resolver) throws CoreException { launchBrowser(launch, launchConfig, file, null, monitor, enableDebugging, resolver); } /** * Launch the browser and open the given url. If debug mode also connect to browser. */ public void launchBrowser(ILaunch launch, DartLaunchConfigWrapper launchConfig, String url, IProgressMonitor monitor, boolean enableDebugging, IResourceResolver resolver) throws CoreException { launchBrowser(launch, launchConfig, null, url, monitor, enableDebugging, resolver); } public IDebugTarget performRemoteConnection(IChromiumTabChooser tabChooser, String host, int port, IProgressMonitor monitor, IResourceResolver resourceResolver) throws CoreException { ILaunch launch = null; monitor.beginTask("Opening Connection...", IProgressMonitor.UNKNOWN); try { List<ChromiumTabInfo> tabs = ChromiumConnector.getAvailableTabs(host, port); ChromiumTabInfo tab = findTargetTab(tabChooser, tabs); if (tab == null || tab.getWebSocketDebuggerUrl() == null) { throw new DebugException(new Status( IStatus.ERROR, DartDebugCorePlugin.PLUGIN_ID, "Unable to connect to debugger in Chromium, make sure browser is open.")); } monitor.worked(1); launch = CoreLaunchUtils.createTemporaryLaunch( DartDebugCorePlugin.DARTIUM_LAUNCH_CONFIG_ID, host + "[" + port + "]"); CoreLaunchUtils.addLaunch(launch); WebkitConnection connection = new WebkitConnection( tab.getHost(), tab.getPort(), tab.getWebSocketDebuggerFile()); final DartiumDebugTarget debugTarget = new DartiumDebugTarget( "Remote", connection, launch, null, resourceResolver, true, true); launch.setAttribute(DebugPlugin.ATTR_CONSOLE_ENCODING, "UTF-8"); launch.addDebugTarget(debugTarget); launch.addProcess(debugTarget.getProcess()); debugTarget.openConnection(); monitor.worked(1); return debugTarget; } catch (IOException e) { if (launch != null) { CoreLaunchUtils.removeLaunch(launch); } throw new CoreException(new Status( IStatus.ERROR, DartDebugCorePlugin.PLUGIN_ID, "Could not connect to remote browser \n" + e.toString(), e)); } finally { monitor.done(); } } protected void launchBrowser(ILaunch launch, DartLaunchConfigWrapper launchConfig, IFile file, String url, IProgressMonitor monitor, boolean enableDebugging, IResourceResolver resolver) throws CoreException { // For now, we always start a debugging connection, even when we're not really debugging. boolean enableBreakpoints = enableDebugging; monitor.beginTask("Launching Dartium...", enableDebugging ? 7 : 2); File dartium = DartSdkManager.getManager().getDartiumExecutable(); if (dartium == null) { throw new CoreException(new Status( IStatus.ERROR, DartDebugCorePlugin.PLUGIN_ID, "Could not find Dartium executable in " + DartSdkManager.getManager().getDartiumWorkingDirectory() + ". Download and install Dartium from http://www.dartlang.org/tools/dartium/.")); } IPath browserLocation = new Path(dartium.getAbsolutePath()); String browserName = dartium.getName(); // avg: 0.434 sec (old: 0.597) LogTimer timer = new LogTimer("Dartium debug startup"); // avg: 55ms timer.startTask(browserName + " startup"); if (!launchConfig.getUsePubServe()) { // TODO(keertip): if file is passed in, url is null. Modify the method // to return a url that makes sense in this case. url = resolveLaunchUrl(file, url, resolver); } url = launchConfig.appendQueryParams(url); // for now, check if browser is open, and connection is alive boolean restart = browserProcess == null || isProcessTerminated(browserProcess) || DartiumDebugTarget.getActiveTarget() == null || !DartiumDebugTarget.getActiveTarget().canTerminate(); // we only re-cycle the debug connection if we're launching the same launch configuration if (!restart) { if (!DartiumDebugTarget.getActiveTarget().getLaunch().getLaunchConfiguration().equals( launch.getLaunchConfiguration())) { restart = true; } } if (!restart) { if (enableDebugging != DartiumDebugTarget.getActiveTarget().getEnableBreakpoints()) { restart = true; } } CoreLaunchUtils.removeTerminatedLaunches(); if (!restart) { DebugPlugin.getDefault().getLaunchManager().removeLaunch(launch); try { DartiumDebugTarget.getActiveTarget().navigateToUrl( launch.getLaunchConfiguration(), url, enableBreakpoints, resolver); } catch (IOException e) { DartDebugCorePlugin.logError(e); } } else { terminateExistingBrowserProcess(); StringBuilder processDescription = new StringBuilder(); ListeningStream dartiumOutput = startNewBrowserProcess( launchConfig, url, monitor, enableDebugging, browserLocation, browserName, processDescription); sleep(100); monitor.worked(1); if (isProcessTerminated(browserProcess)) { DartDebugCorePlugin.logError("Dartium output: " + dartiumOutput.toString()); throw new CoreException(new Status( IStatus.ERROR, DartDebugCorePlugin.PLUGIN_ID, "Could not launch browser - process terminated on startup" + getProcessStreamMessage(dartiumOutput.toString()))); } connectToChromiumDebug( browserName, launch, launchConfig, url, monitor, browserProcess, timer, enableBreakpoints, devToolsPortNumber, dartiumOutput, processDescription.toString(), resolver); } DebugUIHelper.getHelper().activateApplication(dartium, "Chromium"); timer.stopTask(); timer.stopTimer(); monitor.done(); } /** * Launch browser and open file url. If debug mode also connect to browser. */ void connectToChromiumDebug(String browserName, ILaunch launch, DartLaunchConfigWrapper launchConfig, String url, IProgressMonitor monitor, Process runtimeProcess, LogTimer timer, boolean enableBreakpoints, int devToolsPortNumber, ListeningStream dartiumOutput, String processDescription, IResourceResolver resolver) throws CoreException { monitor.worked(1); try { // avg: 383ms timer.startTask("get chromium tabs"); ChromiumTabInfo tab = getChromiumTab(runtimeProcess, devToolsPortNumber, dartiumOutput); monitor.worked(2); timer.stopTask(); // avg: 46ms timer.startTask("open WIP connection"); if (tab == null || tab.getWebSocketDebuggerUrl() == null) { throw new DebugException(new Status( IStatus.ERROR, DartDebugCorePlugin.PLUGIN_ID, "Unable to connect to Chromium")); } // Even when Dartium has reported all the debuggable tabs to us, the debug server // may not yet have started up. Delay a small fixed amount of time. sleep(100); WebkitConnection connection = new WebkitConnection( tab.getHost(), tab.getPort(), tab.getWebSocketDebuggerFile()); final DartiumDebugTarget debugTarget = new DartiumDebugTarget( browserName, connection, launch, runtimeProcess, resolver, enableBreakpoints, false); monitor.worked(1); launch.setAttribute(DebugPlugin.ATTR_CONSOLE_ENCODING, "UTF-8"); launch.addDebugTarget(debugTarget); launch.addProcess(debugTarget.getProcess()); debugTarget.getProcess().setAttribute(IProcess.ATTR_CMDLINE, processDescription); if (launchConfig.getShowLaunchOutput()) { dartiumOutput.setListener(new StreamListener() { @Override public void handleStreamData(String data) { debugTarget.writeToStdout(data); } }); } debugTarget.openConnection(url, true); if (DartDebugCorePlugin.LOGGING) { System.out.println("Connected to WIP debug agent on port " + devToolsPortNumber); } timer.stopTask(); } catch (IOException e) { DebugPlugin.getDefault().getLaunchManager().removeLaunch(launch); IStatus status; // Clean up the error message on certain connection failures to Dartium. // http://code.google.com/p/dart/issues/detail?id=4435 if (e.toString().indexOf("connection failed: unknown status code 500") != -1) { DartDebugCorePlugin.logError(e); status = new Status( IStatus.ERROR, DartDebugCorePlugin.PLUGIN_ID, "Unable to connect to Dartium"); } else { status = new Status(IStatus.ERROR, DartDebugCorePlugin.PLUGIN_ID, e.toString(), e); } throw new CoreException(status); } monitor.worked(1); } private List<String> buildArgumentsList(DartLaunchConfigWrapper launchConfig, IPath browserLocation, String url, boolean enableDebugging, int devToolsPortNumber) { List<String> arguments = new ArrayList<String>(); arguments.add(browserLocation.toOSString()); // Enable remote debug over HTTP on the specified port. arguments.add("--remote-debugging-port=" + devToolsPortNumber); // In order to start up multiple Chrome processes, we need to specify a different user dir. arguments.add("--user-data-dir=" + getCreateUserDataDirectoryPath("dartium")); // Whether or not it's actually the first run. arguments.add("--no-first-run"); // Disables the default browser check. arguments.add("--no-default-browser-check"); // Bypass the error dialog when the profile lock couldn't be attained. arguments.add("--no-process-singleton-dialog"); for (String arg : launchConfig.getArgumentsAsArray()) { arguments.add(arg); } if (enableDebugging) { // Start up with a blank page. arguments.add(INITIAL_PAGE); } else { arguments.add(url); } return arguments; } private void describe(List<String> arguments, StringBuilder builder) { for (int i = 0; i < arguments.size(); i++) { if (i > 0) { builder.append(" "); } builder.append(arguments.get(i)); } } private ChromiumTabInfo findTargetTab(IChromiumTabChooser tabChooser, List<ChromiumTabInfo> tabs) { ChromiumTabInfo chromeTab = tabChooser.chooseTab(tabs); if (chromeTab != null) { for (ChromiumTabInfo tab : tabs) { DartDebugCorePlugin.log("Found: " + tab.toString()); } DartDebugCorePlugin.log("Choosing: " + chromeTab); return chromeTab; } StringBuilder builder = new StringBuilder("unable to locate target dartium tab [" + tabs.size() + " tabs]\n"); for (ChromiumTabInfo tab : tabs) { builder.append(" " + tab.getUrl() + " [" + tab.getTitle() + "]\n"); } DartDebugCorePlugin.logError(builder.toString().trim()); return null; } private ChromiumTabInfo getChromiumTab(Process runtimeProcess, int port, ListeningStream dartiumOutput) throws IOException, CoreException { // Give Chromium 20 seconds to start up. final int maxStartupDelay = 20 * 1000; long endTime = System.currentTimeMillis() + maxStartupDelay; while (true) { if (isProcessTerminated(runtimeProcess)) { 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." + getProcessStreamMessage(dartiumOutput.toString()))); } try { List<ChromiumTabInfo> tabs = ChromiumConnector.getAvailableTabs(port); ChromiumTabInfo targetTab = findTargetTab(tabChooser, tabs); if (targetTab != null) { return targetTab; } } catch (IOException exception) { if (System.currentTimeMillis() > endTime) { throw exception; } } if (System.currentTimeMillis() > endTime) { throw new IOException("Timed out trying to connect to Dartium"); } sleep(25); } } private String getProcessStreamMessage(String output) { StringBuilder msg = new StringBuilder(); if (output.length() != 0) { msg.append("Dartium stdout: ").append(output).append("\n"); } boolean expired = false; if (output.length() != 0) { if (output.indexOf("Dartium build has expired") != -1) { expired = true; } if (expired) { msg.append("\nThis build of Dartium has expired.\n\n"); msg.append("Please download a new Dart Editor or Dartium build from \n"); msg.append("https://www.dartlang.org/tools/dartium/"); } } if (DartCore.isLinux() && !expired) { msg.append("\nFor information on how to setup your machine to run Dartium visit "); msg.append("http://code.google.com/p/dart/wiki/PreparingYourMachine#Linux"); } if (msg.length() != 0) { msg.insert(0, ":\n\n"); } else { msg.append("."); } return msg.toString(); } private boolean isProcessTerminated(Process process) { try { if (process != null) { process.exitValue(); } return true; } catch (IllegalThreadStateException ex) { return false; } } private ListeningStream readFromProcessPipes(final String processName, final InputStream in) { final ListeningStream output = new ListeningStream(); Thread thread = new Thread(new Runnable() { @Override public void run() { byte[] buffer = new byte[2048]; try { int count = in.read(buffer); while (count != -1) { if (count > 0) { String str = new String(buffer, 0, count); // Log any browser process output to stdout. if (DartDebugCorePlugin.LOGGING) { System.out.print(str); } output.appendData(str); } count = in.read(buffer); } in.close(); } catch (IOException ioe) { // When the process closes, we do not want to print any errors. } } }, "Read from " + processName); thread.start(); return output; } /** * @param file * @throws CoreException */ private String resolveLaunchUrl(IFile file, String url, IResourceResolver resolver) throws CoreException { if (file != null) { return resolver.getUrlForResource(file); } return url; } private void sleep(int millis) { try { Thread.sleep(millis); } catch (Exception exception) { } } /** * @param launchConfig * @param url * @param monitor * @param enableDebugging * @param browserLocation * @param browserName * @throws CoreException */ private ListeningStream startNewBrowserProcess(DartLaunchConfigWrapper launchConfig, String url, IProgressMonitor monitor, boolean enableDebugging, IPath browserLocation, String browserName, StringBuilder argDescription) throws CoreException { Process process = null; monitor.worked(1); ProcessBuilder builder = new ProcessBuilder(); Map<String, String> env = builder.environment(); // Due to differences in 32bit and 64 bit environments, dartium 32bit launch does not work on // linux with this property. env.remove("LD_LIBRARY_PATH"); // Prepare the environment variable DART_FLAGS. String dartFlags = ""; // Enable asserts and type checks. if (launchConfig.getCheckedMode()) { dartFlags += " --enable-checked-mode"; } env.put("DART_FLAGS", dartFlags); devToolsPortNumber = DEVTOOLS_PORT_NUMBER; if (enableDebugging) { devToolsPortNumber = NetUtils.findUnusedPort(DEVTOOLS_PORT_NUMBER); if (devToolsPortNumber == -1) { throw new CoreException(new Status( IStatus.ERROR, DartDebugCorePlugin.PLUGIN_ID, "Unable to locate an available port for the Dartium debugger")); } } List<String> arguments = buildArgumentsList( launchConfig, browserLocation, url, true, devToolsPortNumber); builder.command(arguments); builder.redirectErrorStream(true); describe(arguments, argDescription); try { process = builder.start(); } catch (IOException e) { DartDebugCorePlugin.logError("Exception while starting Dartium", e); throw new CoreException(new Status( IStatus.ERROR, DartDebugCorePlugin.PLUGIN_ID, "Could not launch browser: " + e.toString())); } browserProcess = process; return readFromProcessPipes(browserName, browserProcess.getInputStream()); } private void terminateExistingBrowserProcess() { if (browserProcess != null) { if (!isProcessTerminated(browserProcess)) { // TODO(devoncarew): try and use an OS mechanism to send it a graceful shutdown request? // This could avoid the problem w/ Chrome displaying the crashed message on the next run. browserProcess.destroy(); // The process needs time to exit. waitForProcessToTerminate(browserProcess, 200); //sleep(100); } browserProcess = null; } } private void waitForProcessToTerminate(Process process, int maxWaitTimeMs) { long startTime = System.currentTimeMillis(); while ((System.currentTimeMillis() - startTime) < maxWaitTimeMs) { if (isProcessTerminated(process)) { return; } sleep(10); } } }