blob: a0714fa9978e608e42398fa27ec1ee044c199f6b [file] [log] [blame]
/*******************************************************************************
* Copyright (c) 2007, 2016 IBM Corporation and others.
*
* This program and the accompanying materials
* are made available under the terms of the Eclipse Public License 2.0
* which accompanies this distribution, and is available at
* https://www.eclipse.org/legal/epl-2.0/
*
* SPDX-License-Identifier: EPL-2.0
*
* Contributors:
* IBM Corporation - initial API and implementation
* Google Inc - add support for accepting multiple connections
*******************************************************************************/
package org.eclipse.jdt.internal.launching;
import java.io.IOException;
import java.io.PrintWriter;
import java.io.StringWriter;
import java.util.Map;
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.core.runtime.jobs.IJobChangeEvent;
import org.eclipse.core.runtime.jobs.Job;
import org.eclipse.core.runtime.jobs.JobChangeAdapter;
import org.eclipse.debug.core.DebugEvent;
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.model.IDebugTarget;
import org.eclipse.debug.core.model.IProcess;
import org.eclipse.debug.core.model.IStreamsProxy;
import org.eclipse.jdi.TimeoutException;
import org.eclipse.jdt.debug.core.JDIDebugModel;
import org.eclipse.jdt.launching.IJavaLaunchConfigurationConstants;
import org.eclipse.osgi.util.NLS;
import com.sun.jdi.VMDisconnectedException;
import com.sun.jdi.VirtualMachine;
import com.sun.jdi.connect.Connector;
import com.sun.jdi.connect.IllegalConnectorArgumentsException;
import com.sun.jdi.connect.ListeningConnector;
import com.sun.jdi.connect.TransportTimeoutException;
/**
* A process that represents a VM listening connector that is waiting for some VM(s) to remotely connect. Allows the user to see the status of the
* connection and terminate it. If a successful connection occurs, the debug target is added to the launch and, if a configured number of connections
* have been reached, then this process is removed.
*
* @since 3.4
* @see SocketListenConnector
*/
public class SocketListenConnectorProcess implements IProcess {
/**
* Whether this process has been terminated.
*/
private boolean fTerminated = false;
/**
* The launch this process belongs to
*/
private ILaunch fLaunch;
/**
* The port this connector will listen on.
*/
private String fPort;
/**
* The number of incoming connections to accept (0 = unlimited). Setting to 1 mimics previous behaviour.
*/
private int fConnectionLimit;
/** The number of connections accepted so far. */
private int fAccepted = 0;
/**
* The system job that will wait for incoming VM connections.
*/
private WaitForConnectionJob fWaitForConnectionJob;
/** Time when this instance was created (milliseconds) */
private long fStartTime;
/**
* Creates this process. The label for this process will state
* the port the connector is listening at.
* @param launch the launch this process belongs to
* @param port the port the connector will wait on
* @param connectionLimit the number of incoming connections to accept (0 = unlimited)
*/
public SocketListenConnectorProcess(ILaunch launch, String port, int connectionLimit){
fLaunch = launch;
fPort = port;
fConnectionLimit = connectionLimit;
}
/**
* Starts a job that will accept a VM remotely connecting to the
* given connector. The #startListening() method must have been
* called on the connector with the same arguments before calling
* this method. The 'port' argument in the map should have the same
* value as the port specified in this process' constructor.
*
* @param connector the connector that will accept incoming connections
* @param arguments map of arguments that are used by the connector
* @throws CoreException if a problem occurs trying to accept a connection
* @see SocketListenConnector
*/
public void waitForConnection(ListeningConnector connector, Map<String, Connector.Argument> arguments) throws CoreException{
if (isTerminated()){
throw new CoreException(getStatus(LaunchingMessages.SocketListenConnectorProcess_0, null, IJavaLaunchConfigurationConstants.ERR_REMOTE_VM_CONNECTION_FAILED));
}
fStartTime = System.currentTimeMillis();
fAccepted = 0;
// If the connector does not support multiple connections, accept a single connection
try {
if (!connector.supportsMultipleConnections()) {
fConnectionLimit = 1;
}
}
catch (IOException | IllegalConnectorArgumentsException ex) {
fConnectionLimit = 1;
}
fLaunch.addProcess(this);
fWaitForConnectionJob = new WaitForConnectionJob(connector,arguments);
fWaitForConnectionJob.setPriority(Job.SHORT);
fWaitForConnectionJob.setSystem(true);
fWaitForConnectionJob.addJobChangeListener(new JobChangeAdapter(){
@Override
public void running(IJobChangeEvent event) {
fireReadyToAcceptEvent();
}
@Override
public void done(IJobChangeEvent event) {
if (event.getResult().isOK() && continueListening()) {
fWaitForConnectionJob.schedule();
} else {
try{
terminate();
} catch (DebugException e){}
}
}
});
fWaitForConnectionJob.schedule();
}
/**
* Return true if this connector should continue listening for further connections.
*/
protected boolean continueListening() {
return !isTerminated() && (fWaitForConnectionJob != null && !fWaitForConnectionJob.fListeningStopped)
&& (fConnectionLimit <= 0 || fConnectionLimit - fAccepted > 0);
}
/**
* Returns an error status using the passed parameters.
*
* @param message the status message
* @param exception lower level exception associated with the
* error, or <code>null</code> if none
* @param code error code
* @return the new {@link IStatus}
*/
protected static IStatus getStatus(String message, Throwable exception, int code) {
return new Status(IStatus.ERROR, LaunchingPlugin.getUniqueIdentifier(), code, message, exception);
}
/* (non-Javadoc)
* @see org.eclipse.debug.core.model.IProcess#getExitValue()
*/
@Override
public int getExitValue() throws DebugException {
return 0;
}
/* (non-Javadoc)
* @see org.eclipse.debug.core.model.IProcess#getLabel()
*/
@Override
public String getLabel() {
return NLS.bind(LaunchingMessages.SocketListenConnectorProcess_1, new String[]{fPort});
}
/* (non-Javadoc)
* @see org.eclipse.debug.core.model.IProcess#getLaunch()
*/
@Override
public ILaunch getLaunch() {
return fLaunch;
}
/* (non-Javadoc)
* @see org.eclipse.debug.core.model.ITerminate#canTerminate()
*/
@Override
public boolean canTerminate() {
return !fTerminated;
}
/* (non-Javadoc)
* @see org.eclipse.debug.core.model.ITerminate#isTerminated()
*/
@Override
public boolean isTerminated() {
return fTerminated;
}
/* (non-Javadoc)
* @see org.eclipse.debug.core.model.ITerminate#terminate()
*/
@Override
public void terminate() throws DebugException {
if (!fTerminated){
fTerminated = true;
fLaunch.removeProcess(this);
if (fWaitForConnectionJob != null){
fWaitForConnectionJob.cancel();
fWaitForConnectionJob.stopListening();
fWaitForConnectionJob = null;
}
fireTerminateEvent();
}
}
/**
* Fires a terminate event.
*/
protected void fireTerminateEvent() {
DebugPlugin manager= DebugPlugin.getDefault();
if (manager != null) {
manager.fireDebugEventSet(new DebugEvent[]{new DebugEvent(this, DebugEvent.TERMINATE)});
}
}
/**
* Fires a custom model specific event when this connector is ready to accept incoming
* connections from a remote VM.
*/
protected void fireReadyToAcceptEvent(){
DebugPlugin manager= DebugPlugin.getDefault();
if (manager != null) {
manager.fireDebugEventSet(new DebugEvent[]{new DebugEvent(this, DebugEvent.MODEL_SPECIFIC, IJavaLaunchConfigurationConstants.DETAIL_CONFIG_READY_TO_ACCEPT_REMOTE_VM_CONNECTION)});
}
}
/* (non-Javadoc)
* @see org.eclipse.debug.core.model.IProcess#getStreamsProxy()
*/
@Override
public IStreamsProxy getStreamsProxy() {
return null;
}
/* (non-Javadoc)
* @see org.eclipse.debug.core.model.IProcess#getAttribute(java.lang.String)
*/
@Override
public String getAttribute(String key) {
return null;
}
/* (non-Javadoc)
* @see org.eclipse.debug.core.model.IProcess#setAttribute(java.lang.String, java.lang.String)
*/
@Override
public void setAttribute(String key, String value) {
}
/* (non-Javadoc)
* @see org.eclipse.core.runtime.IAdaptable#getAdapter(java.lang.Class)
*/
@Override
public <T> T getAdapter(Class<T> adapter) {
return null;
}
/**
* Return the time since this connector was started.
*/
private String getRunningTime() {
long total = System.currentTimeMillis() - fStartTime;
StringWriter result = new StringWriter();
PrintWriter writer = new PrintWriter(result);
int minutes = (int) (total / 60 / 1000);
int seconds = (int) (total / 1000) % 60;
int milliseconds = (int) (total / 1000) % 1000;
writer.printf("%02d:%02d.%03d", minutes, seconds, milliseconds).close(); //$NON-NLS-1$
return result.toString();
}
/**
* Job that waits for incoming VM connections. When a remote VM connection is accepted, a debug target is created.
*/
class WaitForConnectionJob extends Job{
private ListeningConnector fConnector;
private Map<String, Connector.Argument> fArguments;
/**
* Flag that can be set to tell this job that waiting
* for incoming connections has been cancelled. If true,
* IOExceptions will be ignored, allowing other threads
* to close the socket without generating an error.
*/
private boolean fListeningStopped = false;
public WaitForConnectionJob(ListeningConnector connector, Map<String, Connector.Argument> arguments) {
super(getLabel());
fConnector = connector;
fArguments = arguments;
}
@Override
protected IStatus run(IProgressMonitor monitor) {
try{
// The following code sets a timeout (not officially supported in Sun's spec).
// Allows polling for job cancellation. If the implementation does not support timeout
// the job cannot be cancelled (but the launch can still be terminated).
Connector.Argument timeout = fArguments.get("timeout"); //$NON-NLS-1$
if (timeout != null){
timeout.setValue("3000"); //$NON-NLS-1$
}
VirtualMachine vm = null;
while (vm == null && !monitor.isCanceled()){
try {
vm = fConnector.accept(fArguments);
} catch (TransportTimeoutException e){
}
}
if (monitor.isCanceled()){
fConnector.stopListening(fArguments);
return Status.CANCEL_STATUS;
}
ILaunchConfiguration configuration = fLaunch.getLaunchConfiguration();
boolean allowTerminate = false;
if (configuration != null) {
try{
allowTerminate = configuration.getAttribute(IJavaLaunchConfigurationConstants.ATTR_ALLOW_TERMINATE, false);
} catch (CoreException e) {
LaunchingPlugin.log(e);
}
}
Connector.Argument portArg= fArguments.get("port"); //$NON-NLS-1$
String vmLabel = constructVMLabel(vm, portArg.value(), fLaunch.getLaunchConfiguration());
IDebugTarget debugTarget= JDIDebugModel.newDebugTarget(fLaunch, vm, vmLabel, null, allowTerminate, true);
fLaunch.addDebugTarget(debugTarget);
fAccepted++;
return Status.OK_STATUS;
} catch (IOException e) {
if (fListeningStopped){
return Status.CANCEL_STATUS;
}
return getStatus(LaunchingMessages.SocketListenConnectorProcess_4, e, IJavaLaunchConfigurationConstants.ERR_REMOTE_VM_CONNECTION_FAILED);
} catch (IllegalConnectorArgumentsException e) {
return getStatus(LaunchingMessages.SocketListenConnectorProcess_4, e, IJavaLaunchConfigurationConstants.ERR_REMOTE_VM_CONNECTION_FAILED);
}
}
/* (non-Javadoc)
* @see org.eclipse.core.runtime.jobs.Job#canceling()
*/
@Override
protected void canceling() {
stopListening();
}
/**
* Tells the listening connector to stop listening. Ensures
* that the socket is closed and the port released. Sets a flag
* so that the IOException thrown by the connector's accept method
* will be ignored.
*/
protected void stopListening() {
if (!fListeningStopped){
try{
fListeningStopped = true;
fConnector.stopListening(fArguments);
} catch (IOException e) {
done(getStatus(LaunchingMessages.SocketListenConnectorProcess_5, e, IJavaLaunchConfigurationConstants.ERR_REMOTE_VM_CONNECTION_FAILED));
} catch (IllegalConnectorArgumentsException e) {
done(getStatus(LaunchingMessages.SocketListenConnectorProcess_5, e, IJavaLaunchConfigurationConstants.ERR_REMOTE_VM_CONNECTION_FAILED));
}
}
}
/**
* Helper method that constructs a human-readable label for a remote VM.
* @param vm the VM
* @param port the port
* @param configuration the configuration
* @return the new VM label
*/
protected String constructVMLabel(VirtualMachine vm, String port, ILaunchConfiguration configuration) {
String name = null;
try {
name = vm.name();
} catch (TimeoutException e) {
// do nothing
} catch (VMDisconnectedException e) {
// do nothing
}
if (name == null) {
if (configuration == null) {
name = ""; //$NON-NLS-1$
} else {
name = configuration.getName();
}
}
StringBuilder buffer = new StringBuilder(name);
if (fConnectionLimit != 1) {
// if we're accepting multiple incoming connections,
// append the time when each connection was accepted
buffer.append('<').append(getRunningTime()).append('>');
}
buffer.append('[');
buffer.append(port);
buffer.append(']');
return buffer.toString();
}
}
}