| /******************************************************************************* |
| * Copyright (c) 2007, 2016 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 |
| * 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(); |
| } |
| } |
| StringBuffer buffer = new StringBuffer(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(); |
| } |
| |
| } |
| } |