blob: 2fb1c2324305470dc97ae5f0ac2ee5d011815f75 [file] [log] [blame]
/*******************************************************************************
* Copyright (c) 2010 IBM Corporation and others.
* All rights reserved. This program and the accompanying materials
* are made available under the terms of the Eclipse Public License v1.0
* which accompanies this distribution, and is available at
* http://www.eclipse.org/legal/epl-v10.html
*
* Contributors:
* IBM Corporation - initial API and implementation
*******************************************************************************/
package org.eclipse.wst.jsdt.debug.internal.rhino.ui.launching;
import java.io.File;
import java.io.FileFilter;
import java.io.IOException;
import java.text.CharacterIterator;
import java.text.DateFormat;
import java.text.StringCharacterIterator;
import java.util.ArrayList;
import java.util.Date;
import java.util.HashMap;
import java.util.Iterator;
import java.util.List;
import java.util.Map;
import java.util.Map.Entry;
import org.eclipse.core.resources.IFile;
import org.eclipse.core.resources.IMarker;
import org.eclipse.core.resources.IResource;
import org.eclipse.core.resources.ResourcesPlugin;
import org.eclipse.core.runtime.CoreException;
import org.eclipse.core.runtime.FileLocator;
import org.eclipse.core.runtime.IProgressMonitor;
import org.eclipse.core.runtime.IStatus;
import org.eclipse.core.runtime.Path;
import org.eclipse.core.runtime.Platform;
import org.eclipse.core.runtime.Status;
import org.eclipse.core.runtime.SubMonitor;
import org.eclipse.core.runtime.URIUtil;
import org.eclipse.debug.core.DebugPlugin;
import org.eclipse.debug.core.ILaunch;
import org.eclipse.debug.core.ILaunchConfiguration;
import org.eclipse.debug.core.model.ILaunchConfigurationDelegate2;
import org.eclipse.debug.core.model.IProcess;
import org.eclipse.jdt.internal.launching.StandardVMType;
import org.eclipse.jdt.launching.IVMInstall;
import org.eclipse.jdt.launching.JavaRuntime;
import org.eclipse.jdt.launching.SocketUtil;
import org.eclipse.osgi.util.NLS;
import org.eclipse.wst.jsdt.core.IJavaScriptModelMarker;
import org.eclipse.wst.jsdt.core.ITypeRoot;
import org.eclipse.wst.jsdt.core.JavaScriptCore;
import org.eclipse.wst.jsdt.debug.core.jsdi.VirtualMachine;
import org.eclipse.wst.jsdt.debug.core.jsdi.connect.AttachingConnector;
import org.eclipse.wst.jsdt.debug.core.jsdi.connect.Connector;
import org.eclipse.wst.jsdt.debug.internal.core.model.JavaScriptDebugTarget;
import org.eclipse.wst.jsdt.debug.internal.rhino.jsdi.connect.HostArgument;
import org.eclipse.wst.jsdt.debug.internal.rhino.jsdi.connect.PortArgument;
import org.eclipse.wst.jsdt.debug.internal.rhino.jsdi.connect.RhinoAttachingConnector;
import org.eclipse.wst.jsdt.debug.internal.rhino.ui.ILaunchConstants;
import org.eclipse.wst.jsdt.debug.internal.rhino.ui.RhinoUIPlugin;
import org.eclipse.wst.jsdt.debug.internal.rhino.ui.refactoring.Refactoring;
import org.mozilla.javascript.JavaScriptException;
import org.osgi.framework.Bundle;
/**
* A launch delegate to support launching scripts in a single configuration launch
*
* @since 1.0
*/
public class RhinoLocalLaunchDelegate implements ILaunchConfigurationDelegate2 {
/**
* Polls for connecting to the Rhino interpreter
*/
class ConnectRunnable implements Runnable {
VirtualMachine vm = null;
Exception exception = null;
private AttachingConnector connector = null;
private Map args = null;
ConnectRunnable(AttachingConnector connector, Map args) {
this.connector = connector;
this.args = args;
}
/* (non-Javadoc)
* @see java.lang.Runnable#run()
*/
public void run() {
try {
long start = System.currentTimeMillis();
Exception inner = null;
do {
try {
vm = connector.attach(args);
}
catch(Exception e) {
inner = e;
}
} while(vm == null && System.currentTimeMillis() < start + 15000);
if(vm == null) {
throw inner;
}
}
catch(Exception e) {
exception = e;
}
}
}
class Filter implements FileFilter {
public boolean accept(File pathname) {
return pathname.isDirectory() || JavaScriptCore.isJavaScriptLikeFileName(pathname.getName());
}
}
/**
* The name of the main class to run
* <br><br>
* Value is: <code>"org.eclipse.wst.jsdt.debug.rhino.debugger.shell.DebugShell"</code>
*/
public static final String DEBUG_SHELL_CLASS = "org.eclipse.wst.jsdt.debug.rhino.debugger.shell.DebugMain"; //$NON-NLS-1$
/**
* The name of the main class to run
* <br><br>
* Value is: <code>"org.mozilla.javascript.tools.shell.Main"</code>
*/
public static final String RHINO_MAIN_CLASS = "org.mozilla.javascript.tools.shell.Main"; //$NON-NLS-1$
/**
* The symbolic name of the Mozilla Rhino bundle
* <br><br>
* Value is: <code>org.mozilla.javascript</code>
*/
public static final String MOZILLA_JAVASCRIPT_BUNDLE = "org.mozilla.javascript"; //$NON-NLS-1$
/**
* The symbolic name of the debug transport bundle
* <br><br>
* Value is: <code>org.eclipse.wst.jsdt.debug.transport</code>
*/
public static final String DEBUG_TRANSPORT_BUNDLE = "org.eclipse.wst.jsdt.debug.transport"; //$NON-NLS-1$
/**
* The symbolic name of the Rhino debug interface bundle
* <br><br>
* Value is: <code>org.eclipse.wst.jsdt.debug.rhino.debugger</code>
*/
private static final String RHINO_DEBUGGER_BUNDLE = "org.eclipse.wst.jsdt.debug.rhino.debugger"; //$NON-NLS-1$
/**
* Array of the bundles required to launch
*/
public static final String[] REQUIRED_BUNDLES = {MOZILLA_JAVASCRIPT_BUNDLE, DEBUG_TRANSPORT_BUNDLE, RHINO_DEBUGGER_BUNDLE};
private ArrayList scope = null;
private ITypeRoot script = null;
synchronized ITypeRoot getScript(ILaunchConfiguration configuration) throws CoreException {
if(this.script == null) {
IResource resource = Refactoring.getScript(configuration);
if(resource != null) {
this.script = (ITypeRoot) JavaScriptCore.create((IFile)resource);
}
}
return this.script;
}
/* (non-Javadoc)
* @see org.eclipse.debug.core.model.ILaunchConfigurationDelegate#launch(org.eclipse.debug.core.ILaunchConfiguration, java.lang.String, org.eclipse.debug.core.ILaunch, org.eclipse.core.runtime.IProgressMonitor)
*/
public void launch(ILaunchConfiguration configuration, String mode, ILaunch launch, IProgressMonitor monitor) throws CoreException {
SubMonitor localmonitor = SubMonitor.convert(monitor, NLS.bind(Messages.launching__, configuration.getName()), 8);
int port = SocketUtil.findFreePort();
localmonitor.subTask(Messages.starting_rhino_interpreter);
//launch the interpreter
if(localmonitor.isCanceled()) {
return;
}
localmonitor.worked(1);
VirtualMachine vm = null;
Process p = null;
try {
localmonitor.subTask(Messages.configuring_rhino_debugger);
if(localmonitor.isCanceled()) {
return;
}
localmonitor.worked(1);
//compute commandline args
ArrayList cmdargs = new ArrayList();
addVMArgs(configuration, launch, cmdargs);
addConnectionArgs(port, cmdargs);
addECMAVersion(configuration, cmdargs);
addEncoding(launch, cmdargs);
addOptLevel(configuration, cmdargs);
addStrictMode(configuration, cmdargs);
addFArg(cmdargs);
p = DebugPlugin.exec((String[]) cmdargs.toArray(new String[cmdargs.size()]), null);
HashMap args = new HashMap();
args.put(HostArgument.HOST, "localhost"); //$NON-NLS-1$
args.put(PortArgument.PORT, Integer.toString(port));
localmonitor.subTask(Messages.creating_rhino_vm);
if(localmonitor.isCanceled()) {
cancel(vm, p);
return;
}
localmonitor.worked(1);
RhinoAttachingConnector connector = new RhinoAttachingConnector();
ConnectRunnable runnable = new ConnectRunnable(connector, args);
Thread thread = new Thread(runnable, Messages.connect_thread);
thread.setDaemon(true);
thread.start();
while(thread.isAlive()) {
if(localmonitor.isCanceled()) {
cancel(vm, p);
thread.interrupt();
}
try {
Thread.sleep(100);
}
catch (Exception e) {
}
}
if(runnable.exception != null) {
throw runnable.exception;
}
if(runnable.vm == null) {
throw new IOException("Failed to connect to Rhino interpreter."); //$NON-NLS-1$
}
vm = runnable.vm;
localmonitor.subTask(Messages.starting_rhino_process);
if(localmonitor.isCanceled()) {
cancel(vm, p);
return;
}
localmonitor.worked(1);
RhinoProcess process = new RhinoProcess(launch, p, computeProcessName(connector, args));
process.setAttribute(IProcess.ATTR_CMDLINE, formatCommandlineArgs(cmdargs));
launch.addProcess(process);
localmonitor.subTask(Messages.creating_js_debug_target);
if(localmonitor.isCanceled()) {
cancel(vm, p);
return;
}
localmonitor.worked(1);
JavaScriptDebugTarget target = new JavaScriptDebugTarget(vm, process, launch, true, false);
launch.addDebugTarget(target);
if(localmonitor.isCanceled()) {
cancel(vm, p);
return;
}
localmonitor.worked(1);
return;
}
catch(IOException ioe) {
cancel(vm, p);
RhinoUIPlugin.log(ioe);
}
catch(Exception e) {
if(e instanceof JavaScriptException) {
//ignore certain exceptions from code
if(configuration.getAttribute(ILaunchConstants.ATTR_LOG_INTERPRETER_EXCEPTIONS, true)) {
RhinoUIPlugin.log(e);
}
return;
}
cancel(vm, p);
RhinoUIPlugin.log(e);
}
finally {
if(scope != null) {
scope.clear();
scope = null;
}
script = null;
}
launch.terminate();
return;
}
/**
* Stops + cleans up on cancellation / failure
*
* @param vm
* @param process
*/
void cancel(VirtualMachine vm, Process process) {
if(process != null) {
process.destroy();
}
if(vm != null) {
vm.terminate();
}
}
/**
* Adds the script scope to the args
*
* @param args
*/
void addFArg(ArrayList args) {
args.add("-f"); //$NON-NLS-1$
args.addAll(scope);
}
/**
* Adds the VM executable location and classpath entries to the given arguments list
*
* @param configuration
* @param launch
* @param args
* @throws CoreException
* @throws IOException
*/
void addVMArgs(ILaunchConfiguration configuration, ILaunch launch, ArrayList args) throws CoreException, IOException {
IVMInstall vm = JavaRuntime.getDefaultVMInstall();
File loc = vm.getInstallLocation();
if(loc == null) {
throw new CoreException(new Status(IStatus.ERROR, RhinoUIPlugin.PLUGIN_ID, "Could not locate the default VM install")); //$NON-NLS-1$
}
File exe = StandardVMType.findJavaExecutable(vm.getInstallLocation());
if(exe == null) {
throw new CoreException(new Status(IStatus.ERROR, RhinoUIPlugin.PLUGIN_ID, "Could not find the Java executable for the default VM install")); //$NON-NLS-1$
}
args.add(exe.getAbsolutePath());
StringBuffer buffer = new StringBuffer();
for (int i = 0; i < REQUIRED_BUNDLES.length; i++) {
Bundle bundle = Platform.getBundle(REQUIRED_BUNDLES[i]);
if(bundle == null) {
throw new CoreException(new Status(IStatus.ERROR, RhinoUIPlugin.PLUGIN_ID, NLS.bind("Could not locate the {0} bundle", REQUIRED_BUNDLES[i]))); //$NON-NLS-1$
}
File file = FileLocator.getBundleFile(bundle);
if(file.isDirectory()) {
if(MOZILLA_JAVASCRIPT_BUNDLE.equals(REQUIRED_BUNDLES[i])) {
buffer.append(escapePath(file, false));
}
else {
//mozilla uses the project as the class file output dir
//so we only have to include bin directories for the other ones
file = new File(file, "bin"); //$NON-NLS-1$
if(file.exists()) {
buffer.append(escapePath(file, false));
}
}
}
else {
buffer.append(escapePath(file, false));
}
if(i < REQUIRED_BUNDLES.length-1) {
appendSep(buffer);
}
}
String encoding = getEncoding(launch);
if(encoding != null) {
args.add(encoding);
}
args.add("-cp"); //$NON-NLS-1$
args.add(buffer.toString());
args.add(DEBUG_SHELL_CLASS);
}
/**
* Return the <code>-Dfile.encoding</code> VM argument to use or <code>null</code> it no special encoding has been specified
*
* @param configuration
* @return the file encoding string or <code>null</code>
*/
String getEncoding(ILaunch launch) {
String encoding = launch.getAttribute(DebugPlugin.ATTR_CONSOLE_ENCODING);
if(encoding != null) {
StringBuffer buffer = new StringBuffer("-Dfile.encoding="); //$NON-NLS-1$
buffer.append(encoding);
return buffer.toString();
}
return null;
}
/**
* Escapes the path of the given file.
*
* @param file the file to escape the path for
* @return the escaped path
*/
String escapePath(File file, boolean singlequote) {
String path = file.getAbsolutePath();
StringCharacterIterator iter = new StringCharacterIterator(path);
StringBuffer buffer = new StringBuffer();
boolean hasspace = false;
char c = iter.current();
while(c != CharacterIterator.DONE) {
if(c == '\\') {
buffer.append("/"); //$NON-NLS-1$
}
else if(c == '"') {
buffer.append("\""); //$NON-NLS-1$
}
else if(c == ' ') {
hasspace = true;
buffer.append(c);
}
else {
buffer.append(c);
}
c = iter.next();
}
/*if(singlequote) {
buffer.insert(0, '\'');
buffer.append('\'');
return buffer.toString();
}
else */if(hasspace && !singlequote){
buffer.insert(0, "\""); //$NON-NLS-1$
buffer.append("\""); //$NON-NLS-1$
return buffer.toString();
}
return path;
}
/**
* Appends the correct version of a classpath separator to the given buffer
*
* @param buffer the buffer to add the separator to
*/
void appendSep(StringBuffer buffer) {
if(Platform.getOS().equals(Platform.OS_WIN32)) {
buffer.append(';');
}
else {
buffer.append(':');
}
}
/**
* Adds the default connection string to the command line arguments
*
* @param port
* @param args
*/
void addConnectionArgs(int port, ArrayList args) {
args.add("-port"); //$NON-NLS-1$
args.add(Integer.toString(port));
args.add("-suspend"); //$NON-NLS-1$
args.add("y"); //$NON-NLS-1$
}
/**
* Adds the -version NNN ECMA version to the interpreter command line options
*
* @param configuration
* @param args
* @throws CoreException
*/
void addECMAVersion(ILaunchConfiguration configuration, ArrayList args) throws CoreException {
args.add("-version"); //$NON-NLS-1$
args.add(configuration.getAttribute(ILaunchConstants.ATTR_ECMA_VERSION, "170")); //$NON-NLS-1$
}
/**
* Adds the encoding from the launch to the command line for the interpreter
*
* @param launch
* @param args
*/
void addEncoding(ILaunch launch, ArrayList args) {
String encoding = launch.getAttribute(DebugPlugin.ATTR_CONSOLE_ENCODING);
if(encoding != null) {
args.add("-encoding"); //$NON-NLS-1$
args.add(encoding);
}
}
/**
* Adds the optimization level to the interpreter
* @param configuration
* @param args
* @throws CoreException
*/
void addOptLevel(ILaunchConfiguration configuration, ArrayList args) throws CoreException {
int opt = configuration.getAttribute(ILaunchConstants.ATTR_OPT_LEVEL, -1);
args.add("-opt"); //$NON-NLS-1$
args.add(Integer.toString(opt));
}
/**
* Adds the <code>-strict</code> flag if set in the configuration
*
* @param configuration
* @param args
* @throws CoreException
*/
void addStrictMode(ILaunchConfiguration configuration, ArrayList args) throws CoreException {
boolean strict = configuration.getAttribute(ILaunchConstants.ATTR_STRICT_MODE, false);
if(strict) {
args.add("-strict"); //$NON-NLS-1$
}
}
/**
* Computes the complete list of scripts to load, with the last loaded script
* being the one desired
*
* @param configuration
* @param monitor
* @throws CoreException
*/
synchronized void computeScriptScope(ILaunchConfiguration configuration, IProgressMonitor monitor) throws CoreException {
//for now we only load the script you want to debug and the ones reported from the JS project as source containers
if(this.scope == null) {
this.scope = new ArrayList();
List list = configuration.getAttribute(ILaunchConstants.ATTR_INCLUDE_PATH, (List)null);
if(list != null) {
String entry = null;
for (Iterator i = list.iterator(); i.hasNext();) {
entry = (String) i.next();
int kind = Integer.parseInt(entry.substring(0, 1));
switch(kind) {
case IncludeEntry.LOCAL_SCRIPT: {
String value = entry.substring(1);
IFile ifile = (IFile) ResourcesPlugin.getWorkspace().getRoot().findMember(value);
if(ifile != null && ifile.exists()) {
File file = URIUtil.toFile(ifile.getLocationURI());
value = escapePath(file, true);
if(!scope.contains(value)) {
scope.add(value);
}
}
continue;
}
case IncludeEntry.EXT_SCRIPT: {
String f = entry.substring(1);
File file = new File(f);
if(file.exists() && !scope.contains(file.getAbsolutePath())) {
scope.add(file.getAbsolutePath());
}
continue;
}
}
}
}
addScriptAttribute(configuration, scope);
}
}
/**
* Adds the absolute path of the script specified in the launch configuration to the listing of arguments
*
* @param configuration
* @param args
* @throws CoreException
*/
void addScriptAttribute(ILaunchConfiguration configuration, ArrayList args) throws CoreException {
ITypeRoot root = getScript(configuration);
File file = URIUtil.toFile(root.getResource().getLocationURI());
String value = escapePath(file, true);
args.remove(value);
//want to make sure it is interpreted last
args.add(value);
}
/* (non-Javadoc)
* @see org.eclipse.debug.core.model.ILaunchConfigurationDelegate2#getLaunch(org.eclipse.debug.core.ILaunchConfiguration, java.lang.String)
*/
public ILaunch getLaunch(ILaunchConfiguration configuration, String mode) throws CoreException {
return null;
}
/* (non-Javadoc)
* @see org.eclipse.debug.core.model.ILaunchConfigurationDelegate2#buildForLaunch(org.eclipse.debug.core.ILaunchConfiguration, java.lang.String, org.eclipse.core.runtime.IProgressMonitor)
*/
public boolean buildForLaunch(ILaunchConfiguration configuration, String mode, IProgressMonitor monitor) throws CoreException {
return false;
}
/* (non-Javadoc)
* @see org.eclipse.debug.core.model.ILaunchConfigurationDelegate2#finalLaunchCheck(org.eclipse.debug.core.ILaunchConfiguration, java.lang.String, org.eclipse.core.runtime.IProgressMonitor)
*/
public boolean finalLaunchCheck(ILaunchConfiguration configuration, String mode, IProgressMonitor monitor) throws CoreException {
return true;
}
/* (non-Javadoc)
* @see org.eclipse.debug.core.model.ILaunchConfigurationDelegate2#preLaunchCheck(org.eclipse.debug.core.ILaunchConfiguration, java.lang.String, org.eclipse.core.runtime.IProgressMonitor)
*/
public boolean preLaunchCheck(ILaunchConfiguration configuration, String mode, IProgressMonitor monitor) throws CoreException {
monitor.subTask(Messages.computing_script_scope);
computeScriptScope(configuration, monitor);
if(this.scope.isEmpty()) {
this.scope = null;
throw new CoreException(new Status(IStatus.ERROR, RhinoUIPlugin.PLUGIN_ID, Messages.failed_to_compute_scope));
}
return true;
}
/**
* Returns if the script to launch has any JavaScript problems
*
* @param configuration
* @return
* @throws CoreException
*/
boolean hasProblems(ILaunchConfiguration configuration) throws CoreException {
//TODO this should be expanded to check the entire script scope
String name = configuration.getAttribute(ILaunchConstants.ATTR_SCRIPT, ILaunchConstants.EMPTY_STRING);
IResource script = ResourcesPlugin.getWorkspace().getRoot().findMember(new Path(name));
if(script.isAccessible()) {
IMarker[] problems = script.findMarkers(IJavaScriptModelMarker.JAVASCRIPT_MODEL_PROBLEM_MARKER, true, IResource.DEPTH_INFINITE);
return problems.length > 0;
}
return false;
}
/**
* Turns the command line argument list into a string
*
* @param args
* @return the command line list as a string
*/
String formatCommandlineArgs(ArrayList args) {
StringBuffer buffer = new StringBuffer();
for (int i = 0; i < args.size(); i++) {
buffer.append(args.get(i));
if(i < args.size()-1) {
buffer.append(" "); //$NON-NLS-1$
}
}
return buffer.toString();
}
/**
* Computes the command line to set on the process attribute
* @param args
* @return
*/
String formatConnectionArguments(Map args) {
StringBuffer buffer = new StringBuffer();
Map.Entry entry = null;
for(Iterator iter = args.entrySet().iterator(); iter.hasNext();) {
entry = (Entry) iter.next();
buffer.append(entry.getKey()).append(':').append(entry.getValue());
if(iter.hasNext()) {
buffer.append(',');
}
}
return buffer.toString();
}
/**
* Computes the display name for the {@link IProcess} given the connector
* @param connector
* @return the name for the process
*/
String computeProcessName(Connector connector, Map args) {
StringBuffer buffer = new StringBuffer(connector.name());
String timestamp = DateFormat.getDateTimeInstance(DateFormat.MEDIUM, DateFormat.MEDIUM).format(new Date(System.currentTimeMillis()));
return NLS.bind(Messages.process_label, new String[] {buffer.toString(), formatConnectionArguments(args), timestamp});
}
}