/*
* DO NOT ALTER OR REMOVE COPYRIGHT NOTICES OR THIS HEADER.
*
* Copyright 1997-2010 Oracle and/or its affiliates. All rights reserved.
*
* Oracle and Java are registered trademarks of Oracle and/or its affiliates.
* Other names may be trademarks of their respective owners.
*
* The contents of this file are subject to the terms of either the GNU
* General Public License Version 2 only ("GPL") or the Common
* Development and Distribution License("CDDL") (collectively, the
* "License"). You may not use this file except in compliance with the
* License. You can obtain a copy of the License at
* http://www.netbeans.org/cddl-gplv2.html
* or nbbuild/licenses/CDDL-GPL-2-CP. See the License for the
* specific language governing permissions and limitations under the
* License. When distributing the software, include this License Header
* Notice in each file and include the License file at
* nbbuild/licenses/CDDL-GPL-2-CP. Oracle designates this
* particular file as subject to the "Classpath" exception as provided
* by Oracle in the GPL Version 2 section of the License file that
* accompanied this code. If applicable, add the following below the
* License Header, with the fields enclosed by brackets [] replaced by
* your own identifying information:
* "Portions Copyrighted [year] [name of copyright owner]"
*
* Contributor(s):
*
* The Original Software is NetBeans. The Initial Developer of the Original
* Software is Sun Microsystems, Inc. Portions Copyright 1997-2008 Sun
* Microsystems, Inc. All Rights Reserved.
*
* If you wish your version of this file to be governed by only the CDDL
* or only the GPL Version 2, indicate your decision by adding
* "[Contributor] elects to include this software in this distribution
* under the [CDDL or GPL Version 2] license." If you do not indicate a
* single choice of license, a recipient has the option to distribute
* your version of this file under either the CDDL, the GPL Version 2 or
* to extend the choice of license to its licensees as provided above.
* However, if you add GPL Version 2 code and therefore, elected the GPL
* Version 2 license, then the option applies only if the new code is
* made subject to such option by the copyright holder.
*/
package org.netbeans.modules.ruby.debugger;
import java.awt.EventQueue;
import java.io.File;
import java.io.IOException;
import java.util.ArrayList;
import java.util.Arrays;
import java.util.HashMap;
import java.util.List;
import java.util.Map;
import java.util.logging.Level;
import java.util.logging.Logger;
import org.netbeans.api.debugger.ActionsManager;
import org.netbeans.api.debugger.DebuggerEngine;
import org.netbeans.api.debugger.DebuggerInfo;
import org.netbeans.api.debugger.DebuggerManager;
import org.netbeans.api.debugger.Session;
import org.netbeans.api.extexecution.print.LineConvertors.FileLocator;
import org.netbeans.api.ruby.platform.RubyPlatform;
import org.netbeans.modules.ruby.debugger.Util.FastDebugInstallationResult;
import org.netbeans.modules.ruby.debugger.breakpoints.RubyBreakpointManager;
import org.netbeans.modules.ruby.platform.execution.ExecutionUtils;
import org.netbeans.modules.ruby.platform.execution.RubyExecutionDescriptor;
import org.netbeans.modules.ruby.platform.gems.GemManager;
import org.netbeans.modules.ruby.platform.spi.RubyDebuggerImplementation;
import org.netbeans.spi.debugger.SessionProvider;
import org.openide.filesystems.FileObject;
import org.openide.modules.InstalledFileLocator;
import org.openide.util.NbBundle;
import org.rubyforge.debugcommons.RubyDebuggerFactory;
import org.rubyforge.debugcommons.RubyDebuggerException;
import org.rubyforge.debugcommons.RubyDebuggerProxy;
import static org.netbeans.modules.ruby.debugger.Util.FastDebugInstallationResult.*;
import org.rubyforge.debugcommons.model.RubyDebugTarget;
/**
* Implementation of {@link RubyDebuggerImplementation} SPI, providing an entry
* to point to the Ruby debugging.
*/
@org.openide.util.lookup.ServiceProvider(service=org.netbeans.modules.ruby.platform.spi.RubyDebuggerImplementation.class)
public final class RubyDebugger implements RubyDebuggerImplementation {
private static final Logger LOGGER = Logger.getLogger(RubyDebugger.class.getName());
private static final String PATH_TO_CLASSIC_DEBUG_DIR;
private RubyExecutionDescriptor descriptor;
private RubySession rubySession;
/**
* Used for remote debugging.
* <P>
* TODO: Replace by valid RubyFileLocator.
*/
private static final FileLocator DUMMY_FILE_LOCATOR = new FileLocator() {
public FileObject find(String filename) {
return null;
}
};
private final static String CLASSIC_DEBUGGER_PATH = "ruby/debug-commons-0.9.5/classic-debug.rb"; // NOI18N
static {
File classicDebug = InstalledFileLocator.getDefault().locate(
CLASSIC_DEBUGGER_PATH, "org.netbeans.modules.ruby.debugger", false); // NOI18N
if (classicDebug == null || !classicDebug.isFile()) {
PATH_TO_CLASSIC_DEBUG_DIR = null;
} else {
PATH_TO_CLASSIC_DEBUG_DIR = classicDebug.getParentFile().getAbsolutePath();
}
}
public void describeProcess(RubyExecutionDescriptor descriptor) {
this.descriptor = descriptor;
}
public boolean prepare() {
if (descriptor.getPlatform().isRubinius()) {
return false;
}
if (!checkAndTuneSettings(descriptor)) {
return false;
}
final RubyPlatform platform = descriptor.getPlatform();
if (!platform.hasFastDebuggerInstalled() && PATH_TO_CLASSIC_DEBUG_DIR == null) {
LOGGER.severe("Cannot locate classic debugger in NetBeans Ruby cluster (" + // NOI18N
CLASSIC_DEBUGGER_PATH + "). Neither fast debugger is available. Cannot debug."); // NOI18N
return false;
}
return true;
}
public Process debug() {
try {
rubySession = startDebugging(descriptor);
if (rubySession != null) {
rubySession.getProxy().attach(RubyBreakpointManager.getBreakpoints());
return rubySession.getProxy().getDebugTarget().getProcess();
} else {
throw new IllegalStateException("Unable to create Ruby debugging session");
}
} catch (IOException e) {
getFinishAction().run();
throw new RuntimeException(e);
} catch (RubyDebuggerException e) {
getFinishAction().run();
throw new RuntimeException(e);
}
}
public void attach(final String host, final int port, final int timeout) {
assert !EventQueue.isDispatchThread() : "do not call attach from EDT";
try {
RubyDebuggerProxy proxy = new RubyDebuggerProxy(RubyDebuggerProxy.RUBY_DEBUG, timeout);
RubyDebuggerProxy.PROXIES.add(proxy);
RubyDebugTarget debugTarget = new RubyDebugTarget(proxy, host, port);
proxy.setDebugTarget(debugTarget);
rubySession = intializeIDEDebuggerEngine(proxy, DUMMY_FILE_LOCATOR);
proxy.attach(RubyBreakpointManager.getBreakpoints());
} catch (IOException e) {
getFinishAction().run();
problemOccurred(e);
} catch (RubyDebuggerException e) {
getFinishAction().run();
problemOccurred(e);
}
}
/** @see RubyDebuggerImplementation#getFinishAction */
public Runnable getFinishAction() {
return new Runnable() {
public void run() {
if (rubySession != null) { // #131563
rubySession.getActionProvider().doAction(ActionsManager.ACTION_KILL);
rubySession = null;
}
}
};
}
private static void problemOccurred(final Exception e) {
String message = NbBundle.getMessage(RubyDebugger.class, "RubyDebugger.startup.problem", e.getLocalizedMessage());
// logging as INFO (instead of WARNING) since WARNING pops up the exception dialog
// at least in dev builds, which we don't want since we display a dialog for this
// in any case
LOGGER.log(Level.INFO, message, e);
Util.showWarning(message);
}
/**
* Starts debugging of the given script using the given interpreter.
*
* @param descriptor description of the process to be debugged
* @return debugger {@link java.lang.Process process}. Might be
* <tt>null</tt> if debugging cannot be started for some reason.
* E.g. interpreter cannot be obtained from preferences.
* @throws java.io.IOException
* @throws org.rubyforge.debugcommons.RubyDebuggerException
*/
static RubySession startDebugging(final RubyExecutionDescriptor descriptor)
throws IOException, RubyDebuggerException {
final RubyPlatform platform = descriptor.getPlatform();
boolean jrubySet = platform.isJRuby();
RubyDebuggerFactory.Descriptor debugDesc = new RubyDebuggerFactory.Descriptor();
debugDesc.useDefaultPort(false);
debugDesc.setJRuby(jrubySet);
debugDesc.setDebuggeePath(descriptor.getScript());
if(descriptor.useInterpreter()) {
List<String> additionalOptions = new ArrayList<String>();
if (descriptor.getInitialArgs() != null) {
additionalOptions.addAll(Arrays.asList(descriptor.getInitialArgs()));
}
if (jrubySet && descriptor.getJVMArguments() != null) {
for (String jvmArg : descriptor.getJVMArguments()) {
additionalOptions.add("-J" + jvmArg); // NOI18N
}
}
if (!additionalOptions.isEmpty()) {
debugDesc.setAdditionalOptions(additionalOptions);
}
}
debugDesc.setScriptArguments(descriptor.getAdditionalArgs());
debugDesc.setSynchronizedOutput(true);
if (descriptor.getPwd() != null) {
debugDesc.setBaseDirectory(descriptor.getPwd());
}
Map<String, String> env = new HashMap<String, String>();
GemManager.adjustEnvironment(platform, env);
if (descriptor.getAdditionalEnvironment() != null) {
env.putAll(descriptor.getAdditionalEnvironment());
}
if (jrubySet) {
env.putAll(getJRubyEnvironment(descriptor));
}
debugDesc.setEnvironment(env);
RubyDebuggerProxy proxy;
int timeout = Integer.getInteger("org.netbeans.modules.ruby.debugger.timeout", 15); // NOI18N
LOGGER.finer("Using timeout: " + timeout + 's'); // NOI18N
String interpreter = platform.getInterpreter();
if (!platform.hasFastDebuggerInstalled()) {
assert PATH_TO_CLASSIC_DEBUG_DIR != null : "PATH_TO_CLASSIC_DEBUG_DIR should be checked before";
LOGGER.fine("Running classic(slow) debugger...");
proxy = RubyDebuggerFactory.startClassicDebugger(debugDesc,
PATH_TO_CLASSIC_DEBUG_DIR, interpreter, timeout);
} else { // ruby-debug
String version = platform.getLatestAvailableValidRDebugIDEVersions();
debugDesc.setRubyDebugIDEVersion(version);
LOGGER.fine("Running fast debugger...");
File rDebugF = new File(Util.findRDebugExecutable(platform));
if(descriptor.useInterpreter()) {
proxy = RubyDebuggerFactory.startRubyDebug(debugDesc,
rDebugF.getAbsolutePath(), interpreter, timeout);
} else { // use 'java' executable
List<String> cmd = new ArrayList<String>(20);
cmd.add(descriptor.getCmd().getAbsolutePath());
assert jrubySet : "jruby is used";
if (descriptor.getJVMArguments() != null) {
cmd.addAll(Arrays.asList(descriptor.getJVMArguments()));
}
cmd.addAll(Arrays.asList(descriptor.getInitialArgs()));
proxy = RubyDebuggerFactory.startRubyDebug(
debugDesc, cmd, rDebugF.getAbsolutePath(), timeout);
}
}
return intializeIDEDebuggerEngine(proxy, descriptor.getFileLocator());
}
private static Map<String, String> getJRubyEnvironment(final RubyExecutionDescriptor descriptor) {
Map<String, String> env = new HashMap<String, String>();
if (descriptor.getClassPath() != null) {
env.put("CLASSPATH", ExecutionUtils.getExtraClassPath(descriptor.getClassPath())); // NOI18N
}
return env;
}
/** Package private for unit test. */
static boolean checkAndTuneSettings(final RubyExecutionDescriptor descriptor) {
final RubyPlatform platform = descriptor.getPlatform();
if (platform.isRubinius()) { // no debugger support for Rubinius yet
return false;
}
assert platform.isValid() : platform + " is a valid platform";
boolean jrubySet = platform.isJRuby();
boolean fastDebuggerRequired = descriptor.isFastDebugRequired();
// Offers to install only for fast native Ruby debugger. Installation
// does not work for jruby ruby-debug-base yet.
if (!jrubySet) {
FastDebugInstallationResult result = Util.offerToInstallFastDebugger(platform);
if (result == CANCELLED || result == FAILED) {
return false;
}
}
if (fastDebuggerRequired) { // NOI18N
FastDebugInstallationResult result = Util.ensureRubyDebuggerIsPresent(
platform, true, "RubyDebugger.wrong.fast.debugger.required"); // NOI18N
if (result != INSTALLED) {
if (jrubySet) {
Util.showMessage(NbBundle.getMessage(RubyDebugger.class,
"RubyDebugger.instructionsToInstallJRubyDebugger", // NOI18N
platform.getFastDebuggerProblemsInHTML()));
}
return false;
}
}
if (platform.hasFastDebuggerInstalled()) {
FastDebugInstallationResult result = Util.ensureRubyDebuggerIsPresent(
platform, true, "RubyDebugger.requiredMessage"); // NOI18N
if (result != INSTALLED) {
return false;
}
String rDebugPath = Util.findRDebugExecutable(platform);
if (rDebugPath == null) {
Util.showMessage(NbBundle.getMessage(RubyDebugger.class,
"RubyDebugger.wrong.rdebug-ide", // NOI18N
platform.getInfo().getLongDescription(),
platform.getInterpreter(),
Util.rdebugPattern()));
return false;
}
}
return true;
}
private static RubySession intializeIDEDebuggerEngine(final RubyDebuggerProxy proxy, final FileLocator fileLocator) {
RubySession rubySession = new RubySession(proxy, fileLocator);
SessionProvider sp = rubySession.createSessionProvider();
DebuggerInfo di = DebuggerInfo.create(
"RubyDebuggerInfo", new Object[] { sp, rubySession }); // NOI18N
DebuggerManager dm = DebuggerManager.getDebuggerManager();
DebuggerEngine[] de = dm.startDebugging(di);
assert de.length == 1 : "one debugger engine";
Session session = de[0].lookupFirst(null, Session.class);
assert session != null : "non-null Session in the lookup";
rubySession.setSession(session);
RubyDebuggerActionProvider provider =
de[0].lookupFirst(null, RubyDebuggerActionProvider.class);
assert provider != null;
rubySession.setActionProvider(provider);
proxy.addRubyDebugEventListener(provider);
return rubySession;
}
}