/*******************************************************************************
* Copyright (c) 2008, 2011 Ericsson 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:
* Ericsson - initial API and implementation
* Marc Khouzam (Ericsson) - Add support for multi-attach (Bug 293679)
*******************************************************************************/
package org.eclipse.cdt.dsf.gdb.internal.ui.actions;
import java.util.ArrayList;
import java.util.Arrays;
import java.util.HashMap;
import java.util.List;
import java.util.Map;
import java.util.concurrent.ExecutionException;
import java.util.concurrent.RejectedExecutionException;
import org.eclipse.cdt.dsf.concurrent.CountingRequestMonitor;
import org.eclipse.cdt.dsf.concurrent.DataRequestMonitor;
import org.eclipse.cdt.dsf.concurrent.DsfExecutor;
import org.eclipse.cdt.dsf.concurrent.DsfRunnable;
import org.eclipse.cdt.dsf.concurrent.IDsfStatusConstants;
import org.eclipse.cdt.dsf.concurrent.ImmediateExecutor;
import org.eclipse.cdt.dsf.concurrent.Query;
import org.eclipse.cdt.dsf.concurrent.RequestMonitor;
import org.eclipse.cdt.dsf.datamodel.IDMContext;
import org.eclipse.cdt.dsf.debug.service.IProcesses;
import org.eclipse.cdt.dsf.debug.service.IProcesses.IProcessDMContext;
import org.eclipse.cdt.dsf.debug.service.IProcesses.IThreadDMData;
import org.eclipse.cdt.dsf.debug.service.command.ICommandControlService;
import org.eclipse.cdt.dsf.debug.service.command.ICommandControlService.ICommandControlDMContext;
import org.eclipse.cdt.dsf.gdb.actions.IConnect;
import org.eclipse.cdt.dsf.gdb.internal.ui.GdbUIPlugin;
import org.eclipse.cdt.dsf.gdb.internal.ui.launching.LaunchUIMessages;
import org.eclipse.cdt.dsf.gdb.internal.ui.launching.ProcessPrompter.PrompterInfo;
import org.eclipse.cdt.dsf.gdb.launching.IProcessExtendedInfo;
import org.eclipse.cdt.dsf.gdb.launching.LaunchMessages;
import org.eclipse.cdt.dsf.gdb.service.IGDBBackend;
import org.eclipse.cdt.dsf.gdb.service.IGDBProcesses;
import org.eclipse.cdt.dsf.gdb.service.IGDBProcesses.IGdbThreadDMData;
import org.eclipse.cdt.dsf.gdb.service.SessionType;
import org.eclipse.cdt.dsf.service.DsfServicesTracker;
import org.eclipse.cdt.dsf.service.DsfSession;
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.core.runtime.jobs.Job;
import org.eclipse.debug.core.DebugPlugin;
import org.eclipse.debug.core.IStatusHandler;
import org.eclipse.swt.SWT;
import org.eclipse.swt.widgets.Display;
import org.eclipse.swt.widgets.FileDialog;
import org.eclipse.swt.widgets.Shell;
import org.eclipse.ui.progress.UIJob;
public class GdbConnectCommand implements IConnect {
private final DsfExecutor fExecutor;
private final DsfServicesTracker fTracker;
// A map of processName to path, that allows us to remember the path to the binary file
// for a process with a particular name. We can then re-use the same binary for another
// process with the same name. This allows a user to connect to multiple processes
// with the same name without having to be prompted each time for a path.
// This map is associated to the current debug session only, therefore the user can
// reset it by using a new debug session.
// This map is only needed for remote sessions, since we don't need to specify
// the binary location for a local attach session.
private Map<String, String> fProcessNameToBinaryMap = new HashMap<String, String>();
public GdbConnectCommand(DsfSession session) {
fExecutor = session.getExecutor();
fTracker = new DsfServicesTracker(GdbUIPlugin.getBundleContext(), session.getId());
}
public void dispose() {
fTracker.dispose();
}
public boolean canConnect() {
Query<Boolean> canConnectQuery = new Query<Boolean>() {
@Override
public void execute(DataRequestMonitor<Boolean> rm) {
IProcesses procService = fTracker.getService(IProcesses.class);
ICommandControlService commandControl = fTracker.getService(ICommandControlService.class);
if (procService != null && commandControl != null) {
procService.isDebuggerAttachSupported(commandControl.getContext(), rm);
} else {
rm.setData(false);
rm.done();
}
}
};
try {
fExecutor.execute(canConnectQuery);
return canConnectQuery.get();
} catch (InterruptedException e) {
} catch (ExecutionException e) {
} catch (RejectedExecutionException e) {
// Can be thrown if the session is shutdown
}
return false;
}
/**
* This job will prompt the user to select a set of processes
* to attach too.
* We need a job because prompter.handleStatus will block and
* we don't want to block the executor.
*/
protected class PromptForPidJob extends Job {
// The list of processes used in the case of an ATTACH session
IProcessExtendedInfo[] fProcessList = null;
DataRequestMonitor<Object> fRequestMonitor;
boolean fNewProcessSupported;
public PromptForPidJob(String name, boolean newProcessSupported, IProcessExtendedInfo[] procs, DataRequestMonitor<Object> rm) {
super(name);
fNewProcessSupported = newProcessSupported;
fProcessList = procs;
fRequestMonitor = rm;
}
@Override
protected IStatus run(IProgressMonitor monitor) {
IStatus promptStatus = new Status(IStatus.INFO, "org.eclipse.debug.ui", 200/*STATUS_HANDLER_PROMPT*/, "", null); //$NON-NLS-1$//$NON-NLS-2$
final IStatus processPromptStatus = new Status(IStatus.INFO, "org.eclipse.cdt.dsf.gdb.ui", 100, "", null); //$NON-NLS-1$//$NON-NLS-2$
final IStatusHandler prompter = DebugPlugin.getDefault().getStatusHandler(promptStatus);
final Status NO_PID_STATUS = new Status(IStatus.ERROR, GdbUIPlugin.PLUGIN_ID, -1,
LaunchMessages.getString("LocalAttachLaunchDelegate.No_Process_ID_selected"), //$NON-NLS-1$
null);
if (prompter == null) {
fRequestMonitor.setStatus(NO_PID_STATUS);
fRequestMonitor.done();
return Status.OK_STATUS;
}
try {
PrompterInfo info = new PrompterInfo(fNewProcessSupported, fProcessList);
Object result = prompter.handleStatus(processPromptStatus, info);
if (result == null) {
fRequestMonitor.cancel();
} else if (result instanceof IProcessExtendedInfo[] || result instanceof String) {
fRequestMonitor.setData(result);
} else {
fRequestMonitor.setStatus(NO_PID_STATUS);
}
} catch (CoreException e) {
fRequestMonitor.setStatus(NO_PID_STATUS);
}
fRequestMonitor.done();
return Status.OK_STATUS;
}
};
/**
* This job will prompt the user for a path to the binary to use,
* and then will attach to the process.
* We need a job to free the executor while we prompt the user for
* a binary path. Bug 344892
*/
private class PromptAndAttachToProcessJob extends UIJob {
private final String fPid;
private final RequestMonitor fRm;
private final String fTitle;
private final String fProcName;
public PromptAndAttachToProcessJob(String pid, String title, String procName, RequestMonitor rm) {
super(""); //$NON-NLS-1$
fPid = pid;
fTitle = title;
fProcName = procName;
fRm = rm;
}
@Override
public IStatus runInUIThread(IProgressMonitor monitor) {
// Have we already see the binary for a process with this name?
String binaryPath = fProcessNameToBinaryMap.get(fProcName);
if (binaryPath == null) {
// prompt for the binary path
Shell shell = Display.getCurrent().getActiveShell();
if (shell != null) {
FileDialog fd = new FileDialog(shell, SWT.NONE);
fd.setText(fTitle);
binaryPath = fd.open();
}
}
if (binaryPath == null) {
// The user pressed the cancel button, so we cancel the attach gracefully
fRm.done();
} else {
final String finalBinaryPath = binaryPath;
fExecutor.execute(new DsfRunnable() {
public void run() {
IGDBProcesses procService = fTracker.getService(IGDBProcesses.class);
ICommandControlService commandControl = fTracker.getService(ICommandControlService.class);
if (procService != null && commandControl != null) {
IProcessDMContext procDmc = procService.createProcessContext(commandControl.getContext(), fPid);
procService.attachDebuggerToProcess(procDmc, finalBinaryPath, new DataRequestMonitor<IDMContext>(fExecutor, fRm) {
@Override
protected void handleSuccess() {
// Store the path of the binary so we can use it again for another process
// with the same name. Only do this on success, to avoid being stuck with
// a path that is invalid
fProcessNameToBinaryMap.put(fProcName, finalBinaryPath);
fRm.done();
};
});
} else {
fRm.setStatus(new Status(IStatus.ERROR, GdbUIPlugin.PLUGIN_ID, IDsfStatusConstants.INTERNAL_ERROR, "Cannot find services", null)); //$NON-NLS-1$
fRm.done();
}
}
});
}
return Status.OK_STATUS;
}
}
public void connect(RequestMonitor requestMonitor)
{
// Create a fake rm to avoid null pointer exceptions
final RequestMonitor rm;
if (requestMonitor == null) {
rm = new RequestMonitor(fExecutor, null);
} else {
rm = requestMonitor;
}
// Don't wait for the operation to finish because this
// method can be called from the UI thread, and it will
// block it, which is bad, because we need to use the UI
// thread to prompt the user for the process to choose.
// This is why we simply use a DsfRunnable.
fExecutor.execute(new DsfRunnable() {
public void run() {
final IProcesses procService = fTracker.getService(IProcesses.class);
ICommandControlService commandControl = fTracker.getService(ICommandControlService.class);
if (procService != null && commandControl != null) {
final ICommandControlDMContext controlCtx = commandControl.getContext();
procService.isDebugNewProcessSupported(controlCtx, new DataRequestMonitor<Boolean>(fExecutor, null) {
@Override
protected void handleCompleted() {
final boolean newProcessSupported = isSuccess() && getData();
procService.getRunningProcesses(
controlCtx,
new DataRequestMonitor<IProcessDMContext[]>(fExecutor, rm) {
@Override
protected void handleSuccess() {
final List<IProcessExtendedInfo> procInfoList = new ArrayList<IProcessExtendedInfo>();
final CountingRequestMonitor countingRm =
new CountingRequestMonitor(fExecutor, rm) {
@Override
protected void handleSuccess() {
new PromptForPidJob(
LaunchUIMessages.getString("ProcessPrompter.PromptJob"), newProcessSupported, procInfoList.toArray(new IProcessExtendedInfo[0]), //$NON-NLS-1$
new DataRequestMonitor<Object>(fExecutor, rm) {
@Override
protected void handleCancel() {
rm.cancel();
rm.done();
}
@Override
protected void handleSuccess() {
Object data = getData();
if (data instanceof String) {
// User wants to start a new process
startNewProcess(controlCtx, (String)data, rm);
} else if (data instanceof IProcessExtendedInfo[]) {
attachToProcesses(controlCtx, (IProcessExtendedInfo[])data, rm);
} else {
rm.setStatus(new Status(IStatus.ERROR, GdbUIPlugin.PLUGIN_ID, IDsfStatusConstants.INTERNAL_ERROR, "Invalid return type for process prompter", null)); //$NON-NLS-1$
rm.done();
}
}
}).schedule();
}
};
if (getData().length > 0 && getData()[0] instanceof IThreadDMData) {
// The list of running processes also contains the name of the processes
// This is much more efficient. Let's use it.
for (IProcessDMContext processCtx : getData()) {
IThreadDMData processData = (IThreadDMData) processCtx;
int pid = 0;
try {
pid = Integer.parseInt(processData.getId());
} catch (NumberFormatException e) {
}
String[] cores = null;
String owner = null;
if (processData instanceof IGdbThreadDMData) {
cores = ((IGdbThreadDMData)processData).getCores();
owner = ((IGdbThreadDMData)processData).getOwner();
}
procInfoList.add(new ProcessInfo(pid, processData.getName(), cores, owner));
}
// Re-use the counting monitor and trigger it right away.
// No need to call done() in this case.
countingRm.setDoneCount(0);
} else {
// The list of running processes does not contain the names, so
// we must obtain it individually
// For each process, obtain its name
// Once all the names are obtained, prompt the user for the pid to use
// New cycle, look for service again
final IProcesses procService = fTracker.getService(IProcesses.class);
if (procService != null) {
countingRm.setDoneCount(getData().length);
for (IProcessDMContext processCtx : getData()) {
procService.getExecutionData(
processCtx,
new DataRequestMonitor<IThreadDMData> (fExecutor, countingRm) {
@Override
protected void handleSuccess() {
IThreadDMData processData = getData();
int pid = 0;
try {
pid = Integer.parseInt(processData.getId());
} catch (NumberFormatException e) {
}
String[] cores = null;
String owner = null;
if (processData instanceof IGdbThreadDMData) {
cores = ((IGdbThreadDMData)processData).getCores();
owner = ((IGdbThreadDMData)processData).getOwner();
}
procInfoList.add(new ProcessInfo(pid, processData.getName(), cores, owner));
countingRm.done();
}
});
}
} else {
// Trigger right away. No need to call done() in this case.
countingRm.setDoneCount(0);
}
}
}
});
}
});
} else {
rm.done();
}
}
});
}
private void startNewProcess(ICommandControlDMContext controlDmc, String binaryPath, RequestMonitor rm) {
final IGDBProcesses procService = fTracker.getService(IGDBProcesses.class);
procService.debugNewProcess(
controlDmc, binaryPath,
new HashMap<String, Object>(), new DataRequestMonitor<IDMContext>(fExecutor, rm));
}
private void attachToProcesses(final ICommandControlDMContext controlDmc, IProcessExtendedInfo[] processes, final RequestMonitor rm) {
// For a local attach, GDB can figure out the binary automatically,
// so we don't need to prompt for it.
final IGDBProcesses procService = fTracker.getService(IGDBProcesses.class);
final IGDBBackend backend = fTracker.getService(IGDBBackend.class);
if (procService != null && backend != null) {
// Attach to each process in a sequential fashion. We must do this
// to be able to check if we are allowed to attach to the next process.
// Attaching to all of them in parallel would assume that all attach are supported.
// Create a list of all our processes so we can attach to one at a time.
// We need to create a new list so that we can remove elements from it.
final List<IProcessExtendedInfo> procList = new ArrayList<IProcessExtendedInfo>(Arrays.asList(processes));
class AttachToProcessRequestMonitor extends DataRequestMonitor<IDMContext> {
public AttachToProcessRequestMonitor() {
super(ImmediateExecutor.getInstance(), null);
}
@Override
protected void handleCompleted() {
if (!isSuccess()) {
// Failed to attach to a process. Just ignore it and move on.
}
// Check that we have a process to attach to
if (procList.size() > 0) {
// Check that we can actually attach to the process.
// This is because some backends may not support multi-process.
// If the backend does not support multi-process, we only attach to the first process.
procService.isDebuggerAttachSupported(controlDmc, new DataRequestMonitor<Boolean>(ImmediateExecutor.getInstance(), null) {
@Override
protected void handleCompleted() {
if (isSuccess() && getData()) {
// Can attach to process
// Remove process from list and attach to it.
IProcessExtendedInfo process = procList.remove(0);
String pidStr = Integer.toString(process.getPid());
if (backend.getSessionType() == SessionType.REMOTE) {
// For remote attach, we must set the binary first so we need to prompt the user.
// If this is the very first attach of a remote session, check if the user
// specified the binary in the launch. If so, let's add it to our map to
// avoid having to prompt the user for that binary.
// This would be particularly annoying since we didn't use to have
// to do that before we supported multi-process.
// Must do this here to be in the executor
// Bug 350365
if (fProcessNameToBinaryMap.isEmpty()) {
IPath binaryPath = backend.getProgramPath();
if (binaryPath != null && !binaryPath.isEmpty()) {
fProcessNameToBinaryMap.put(binaryPath.lastSegment(), binaryPath.toOSString());
}
}
// Because the prompt is a very long operation, we need to run outside the
// executor, so we don't lock it.
// Bug 344892
IPath processPath = new Path(process.getName());
String processShortName = processPath.lastSegment();
new PromptAndAttachToProcessJob(pidStr,
LaunchUIMessages.getString("ProcessPrompterDialog.TitlePrefix") + process.getName(), //$NON-NLS-1$
processShortName, new AttachToProcessRequestMonitor()).schedule();
} else {
IProcessDMContext procDmc = procService.createProcessContext(controlDmc, pidStr);
procService.attachDebuggerToProcess(procDmc, null, new AttachToProcessRequestMonitor());
}
} else {
// Not allowed to attach to another process. Just stop.
rm.done();
}
}
});
} else {
// No other process to attach to
rm.done();
}
}
};
// Trigger the first attach.
new AttachToProcessRequestMonitor().done();
} else {
rm.setStatus(new Status(IStatus.ERROR, GdbUIPlugin.PLUGIN_ID, IDsfStatusConstants.INTERNAL_ERROR, "Cannot find service", null)); //$NON-NLS-1$
rm.done();
}
}
}