blob: 538bc46e8d9d4a8e0760bb59a0c175c36c82eb03 [file] [log] [blame]
/*******************************************************************************
* Copyright (c) 2008, 2016 Ericsson 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:
* Ericsson - initial API and implementation
* Marc Khouzam (Ericsson) - Add support for multi-attach (Bug 293679)
*******************************************************************************/
package org.eclipse.cdt.dsf.gdb.internal.ui.commands;
import java.util.ArrayList;
import java.util.Arrays;
import java.util.HashMap;
import java.util.LinkedList;
import java.util.List;
import java.util.Map;
import java.util.concurrent.CancellationException;
import java.util.concurrent.ExecutionException;
import java.util.concurrent.Executor;
import java.util.concurrent.RejectedExecutionException;
import org.eclipse.cdt.debug.core.model.IConnectHandler;
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.ImmediateDataRequestMonitor;
import org.eclipse.cdt.dsf.concurrent.ImmediateExecutor;
import org.eclipse.cdt.dsf.concurrent.ImmediateInDsfExecutor;
import org.eclipse.cdt.dsf.concurrent.Query;
import org.eclipse.cdt.dsf.concurrent.RequestMonitor;
import org.eclipse.cdt.dsf.datamodel.DMContexts;
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.actions.ProcessInfo;
import org.eclipse.cdt.dsf.gdb.internal.ui.launching.LaunchUIMessages;
import org.eclipse.cdt.dsf.gdb.internal.ui.launching.ProcessPrompter;
import org.eclipse.cdt.dsf.gdb.internal.ui.launching.ProcessPrompter.PrompterInfo;
import org.eclipse.cdt.dsf.gdb.launching.GdbLaunch;
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.IGDBProcesses.IGdbThreadDMData2;
import org.eclipse.cdt.dsf.gdb.service.SessionType;
import org.eclipse.cdt.dsf.mi.service.IMIProcessDMContext;
import org.eclipse.cdt.dsf.service.DsfServicesTracker;
import org.eclipse.cdt.dsf.service.DsfSession;
import org.eclipse.cdt.dsf.ui.viewmodel.datamodel.IDMVMContext;
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.debug.core.ILaunch;
import org.eclipse.debug.core.IRequest;
import org.eclipse.debug.core.commands.IDebugCommandRequest;
import org.eclipse.debug.core.commands.IEnabledStateRequest;
import org.eclipse.swt.SWT;
import org.eclipse.swt.widgets.FileDialog;
import org.eclipse.swt.widgets.Shell;
import org.eclipse.ui.progress.UIJob;
public class GdbConnectCommand extends RefreshableDebugCommand implements IConnectHandler, IConnect {
private final ILaunch fLaunch;
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<>();
public GdbConnectCommand(DsfSession session, ILaunch launch) {
fLaunch = launch;
fExecutor = session.getExecutor();
fTracker = new DsfServicesTracker(GdbUIPlugin.getBundleContext(), session.getId());
}
public void dispose() {
fTracker.dispose();
}
@Override
protected boolean isExecutable(Object[] targets, IProgressMonitor monitor, IEnabledStateRequest request)
throws CoreException {
return canConnect();
}
/*
* This method should not be called from the UI thread.
* (non-Javadoc)
* @see org.eclipse.cdt.dsf.gdb.actions.IConnect#canConnect()
*/
@Override
public boolean canConnect() {
Query<Boolean> canConnectQuery = new Query<>() {
@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 the ProcessPrompter will block and
* we don't want to block the executor.
*/
protected class PromptForPidJob extends UIJob {
// The list of processes used in the case of an ATTACH session
IProcessExtendedInfo[] fProcessList = null;
DataRequestMonitor<Object> fRequestMonitor;
private List<String> fDebuggedProcesses;
public PromptForPidJob(String name, IProcessExtendedInfo[] procs, List<String> debuggedProcesses,
DataRequestMonitor<Object> rm) {
super(name);
fProcessList = procs;
fRequestMonitor = rm;
fDebuggedProcesses = debuggedProcesses;
}
@Override
public IStatus runInUIThread(IProgressMonitor monitor) {
final Status NO_PID_STATUS = new Status(IStatus.ERROR, GdbUIPlugin.PLUGIN_ID, -1,
LaunchMessages.getString("LocalAttachLaunchDelegate.No_Process_ID_selected"), //$NON-NLS-1$
null);
try {
PrompterInfo info = new PrompterInfo(fProcessList, fDebuggedProcesses);
Object result = new ProcessPrompter().handleStatus(null, info);
if (result == null) {
fRequestMonitor.cancel();
} else if (result instanceof IProcessExtendedInfo[]) {
fRequestMonitor.setData(result);
} else if (result instanceof Integer) {
// This is the case where the user typed in a pid number directly
fRequestMonitor.setData(new IProcessExtendedInfo[] { new ProcessInfo((Integer) result, "") }); //$NON-NLS-1$
} 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 = GdbUIPlugin.getShell();
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() {
@Override
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.
if (fProcName != null && !fProcName.isEmpty()) {
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;
}
}
@Override
protected void doExecute(Object[] targets, IProgressMonitor monitor, final IRequest request) throws CoreException {
Query<Boolean> connectQuery = new Query<>() {
@Override
public void execute(final DataRequestMonitor<Boolean> rm) {
connect(new RequestMonitor(fExecutor, rm) {
@Override
protected void handleCompleted() {
// pass any error to the caller
if (!isSuccess()) {
request.setStatus(getStatus());
}
rm.done();
}
});
}
};
try {
fExecutor.execute(connectQuery);
connectQuery.get();
} catch (InterruptedException e) {
} catch (ExecutionException e) {
} catch (CancellationException e) {
// Nothing to do, just ignore the command since the user
// cancelled it.
} catch (RejectedExecutionException e) {
// Can be thrown if the session is shutdown
} finally {
updateEnablement();
}
}
/**
* Get already debugged processes from all compatible sessions.
* "compatible" in current implementation means all sessions on local machine.
*
* @param currentCtx current session context
* @param allSessions true if all session to be queried, false to return result only for current execution session context
* @param drm where result to be returned
*/
private void getAllDebuggedProcesses(final IDMContext currentCtx, boolean allSessions,
final DataRequestMonitor<List<String>> drm) {
SessionType sessionType = fTracker.getService(IGDBBackend.class).getSessionType();
final List<String> result = new LinkedList<>();
final List<DsfSession> sessions = new LinkedList<>();
// Only for local session types search in all debug sessions
if (allSessions && sessionType == SessionType.LOCAL) {
sessions.addAll(Arrays.asList(DsfSession.getActiveSessions()));
} else {
// For remote session just query current context.
//
// cannot reliably match two remote debug session that are connected to same target machine.
// see https://bugs.eclipse.org/bugs/show_bug.cgi?id=486408#c7
sessions.add(DsfSession.getSession(currentCtx.getSessionId()));
}
// Query each sessions for existing processes in a sequential fashion.
// We must do this as each session will require different executor.
final class ProcessRequestMonitor extends DataRequestMonitor<IDMContext[]> {
public ProcessRequestMonitor(Executor executor) {
super(executor, null);
}
public ProcessRequestMonitor(DsfExecutor executor) {
super(new ImmediateInDsfExecutor(executor), null);
}
@Override
protected void handleCompleted() {
// if succeeded and has data, add process ids to result,
// otherwise proceed to next debug session (aka DsfSession)
if (isSuccess() && getData() != null) {
for (IDMContext dmc : getData()) {
IMIProcessDMContext procDmc = DMContexts.getAncestorOfType(dmc, IMIProcessDMContext.class);
if (procDmc != null) {
result.add(procDmc.getProcId());
}
}
}
if (!sessions.isEmpty()) {
final DsfSession nextSession = sessions.remove(0);
final boolean sameSession = currentCtx.getSessionId().equals(nextSession.getId());
nextSession.getExecutor().execute(new DsfRunnable() {
@Override
public void run() {
DsfServicesTracker nextTracker = new DsfServicesTracker(GdbUIPlugin.getBundleContext(),
nextSession.getId());
IGDBBackend nextSessionBackend = nextTracker.getService(IGDBBackend.class);
if (sameSession || nextSessionBackend.getSessionType() == SessionType.LOCAL) {
ICommandControlService nextCommandControl = nextTracker
.getService(ICommandControlService.class);
IProcesses nextProcService = nextTracker.getService(IProcesses.class);
nextProcService.getProcessesBeingDebugged(nextCommandControl.getContext(),
new ProcessRequestMonitor(nextSession.getExecutor()));
} else {
// proceed to next session context query passing an error (that will be ignored)
new ProcessRequestMonitor(nextSession.getExecutor())
.done(new Status(IStatus.ERROR, GdbUIPlugin.PLUGIN_ID,
IDsfStatusConstants.NOT_SUPPORTED, "Only local session", null)); //$NON-NLS-1$
}
nextTracker.dispose();
}
});
} else {
// done with querying all session. Copy the result
drm.done(result);
}
}
}
// Trigger the first query
new ProcessRequestMonitor(ImmediateExecutor.getInstance()).done();
}
/*
* This method should not be called from the UI thread.
* (non-Javadoc)
* @see org.eclipse.cdt.dsf.gdb.actions.IConnect#canConnect()
*/
@Override
public void connect(final RequestMonitor rm) {
fExecutor.execute(new DsfRunnable() {
@Override
public void run() {
final IProcesses procService = fTracker.getService(IProcesses.class);
ICommandControlService commandControl = fTracker.getService(ICommandControlService.class);
IGDBBackend backend = fTracker.getService(IGDBBackend.class);
if (procService != null && commandControl != null && backend != null) {
final ICommandControlDMContext controlCtx = commandControl.getContext();
// Now get the list of all processes
procService.getRunningProcesses(controlCtx,
new DataRequestMonitor<IProcessDMContext[]>(fExecutor, rm) {
@Override
protected void handleSuccess() {
final List<IProcessExtendedInfo> procInfoList = new ArrayList<>();
final CountingRequestMonitor countingRm = new CountingRequestMonitor(fExecutor,
rm) {
@Override
protected void handleSuccess() {
getAllDebuggedProcesses(controlCtx, true,
new ImmediateDataRequestMonitor<List<String>>(rm) {
@Override
protected void handleSuccess() {
connectToProcesses(controlCtx, procInfoList, getData(), rm);
}
});
}
};
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();
}
String description = null;
if (processData instanceof IGdbThreadDMData2) {
description = ((IGdbThreadDMData2) processData).getDescription();
}
procInfoList.add(new ProcessInfo(pid, processData.getName(), cores, owner,
description));
}
// 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();
}
String description = null;
if (processData instanceof IGdbThreadDMData2) {
description = ((IGdbThreadDMData2) processData)
.getDescription();
}
procInfoList
.add(new ProcessInfo(pid, processData.getName(),
cores, owner, description));
countingRm.done();
}
});
}
} else {
// Trigger right away. No need to call done() in this case.
countingRm.setDoneCount(0);
}
}
}
});
} else {
rm.done();
}
}
});
}
/**
* This method is called by {@link #connect(RequestMonitor)}.<br>
* The default implementation schedules a {@link PromptForPidJob}, which calls
* {@link #attachToProcesses(ICommandControlDMContext, IProcessExtendedInfo[], RequestMonitor)} on success.
*
* @param controlDmc
* @param processes
* @param dbgPids
* @param rm
*/
protected void connectToProcesses(final ICommandControlDMContext controlDmc,
final List<IProcessExtendedInfo> processes, List<String> debuggedProcesses, final RequestMonitor rm) {
// Prompt the user to choose one or more processes
new PromptForPidJob(LaunchUIMessages.getString("ProcessPrompter.PromptJob"), //$NON-NLS-1$
processes.toArray(new IProcessExtendedInfo[processes.size()]), debuggedProcesses,
new DataRequestMonitor<>(fExecutor, rm) {
@Override
protected void handleCancel() {
rm.cancel();
rm.done();
}
@Override
protected void handleSuccess() {
Object data = getData();
if (data instanceof IProcessExtendedInfo[]) {
attachToProcesses(controlDmc, (IProcessExtendedInfo[]) data, rm);
} else {
rm.done(new Status(IStatus.ERROR, GdbUIPlugin.PLUGIN_ID, IDsfStatusConstants.INTERNAL_ERROR,
"Invalid return type for process prompter", //$NON-NLS-1$
null));
}
}
}).schedule();
}
protected 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);
final StringBuilder errors = new StringBuilder();
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<>(Arrays.asList(processes));
// Create a one element array to remember what process we are trying to attach to, so that we can
// use it in case of error.
final IProcessExtendedInfo[] previousProcAttempt = new IProcessExtendedInfo[1];
class AttachToProcessRequestMonitor extends ImmediateDataRequestMonitor<IDMContext> {
public AttachToProcessRequestMonitor() {
super();
}
@Override
protected void handleCompleted() {
// Failed to attach to a process. Remember the error message.
if (!isSuccess()) {
formatErrorMessage(errors, previousProcAttempt[0], getStatus().getMessage());
}
// Check that we have a process to attach to
if (!procList.isEmpty()) {
// 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 ImmediateDataRequestMonitor<Boolean>() {
@Override
protected void handleCompleted() {
if (isSuccess() && getData()) {
// Can attach to process
// Remove process from list and attach to it.
IProcessExtendedInfo process = procList.remove(0);
// Store process in case of error
previousProcAttempt[0] = process;
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") //$NON-NLS-1$
+ process.getName(),
processShortName, new AttachToProcessRequestMonitor()).schedule();
} else {
// For a local attach, we can attach directly without looking for the binary
// since GDB will figure it out by itself
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 {
// If there were errors, pass them-on to the caller
if (errors.length() != 0) {
rm.setStatus(new Status(IStatus.ERROR, GdbUIPlugin.PLUGIN_ID, errors.toString()));
}
// No other process to attach to
rm.done();
}
}
}
// Trigger the first attach.
new AttachToProcessRequestMonitor().done();
} else {
rm.done(new Status(IStatus.ERROR, GdbUIPlugin.PLUGIN_ID, IDsfStatusConstants.INTERNAL_ERROR,
"Cannot find service", null)); //$NON-NLS-1$
}
}
@Override
protected Object getTarget(Object element) {
if (element instanceof GdbLaunch || element instanceof IDMVMContext) {
return element;
}
return null;
}
@Override
protected boolean isRemainEnabled(IDebugCommandRequest request) {
return false;
}
private void formatErrorMessage(StringBuilder errors, IProcessExtendedInfo process, String errorMsg) {
// Extract process name from full path.
// On windows host, paths of style "sendmail:", "udisk-daemon:"
// is treated as device id with no path segments
String name;
IPath path = new Path(process.getName());
if (path.lastSegment() == null) {
name = process.getName();
} else {
name = path.lastSegment();
}
if (errors.length() != 0) {
errors.append(System.lineSeparator()).append(System.lineSeparator());
}
errors.append(Messages.GdbConnectCommand_FailureMessage).append(" ") //$NON-NLS-1$
.append(name).append(" [").append(process.getPid()).append("]") //$NON-NLS-1$ //$NON-NLS-2$
.append(System.lineSeparator()).append(Messages.GdbConnectCommand_Error).append(System.lineSeparator())
.append(errorMsg);
}
}