diff --git a/bundles/com.espressif.idf.debug.gdbjtag.openocd/plugin.xml b/bundles/com.espressif.idf.debug.gdbjtag.openocd/plugin.xml index 2fd06585f..4b2d5edf7 100644 --- a/bundles/com.espressif.idf.debug.gdbjtag.openocd/plugin.xml +++ b/bundles/com.espressif.idf.debug.gdbjtag.openocd/plugin.xml @@ -228,14 +228,5 @@ after="org.eclipse.debug.ui.commonTab"> - - - - diff --git a/bundles/com.espressif.idf.debug.gdbjtag.openocd/src/com/espressif/idf/debug/gdbjtag/openocd/Activator.java b/bundles/com.espressif.idf.debug.gdbjtag.openocd/src/com/espressif/idf/debug/gdbjtag/openocd/Activator.java index 85ef2d3ab..e601702fc 100644 --- a/bundles/com.espressif.idf.debug.gdbjtag.openocd/src/com/espressif/idf/debug/gdbjtag/openocd/Activator.java +++ b/bundles/com.espressif.idf.debug.gdbjtag.openocd/src/com/espressif/idf/debug/gdbjtag/openocd/Activator.java @@ -42,6 +42,8 @@ public class Activator extends AbstractUIPlugin { // The plug-in ID public static final String PLUGIN_ID = "com.espressif.idf.debug.gdbjtag.openocd"; //$NON-NLS-1$ public static final String GDB_SERVER_LAUNCH_TIMEOUT = "fGdbServerLaunchTimeout"; //$NON-NLS-1$ + /** Must match {@code plugin.xml} statusHandler code for {@code OpenocdStatusHandler}. */ + public static final int OPENOCD_STARTUP_TIMEOUT_STATUS = 5012; @Override protected void initializeDefaultPreferences(IPreferenceStore preferenceStore) diff --git a/bundles/com.espressif.idf.debug.gdbjtag.openocd/src/com/espressif/idf/debug/gdbjtag/openocd/Configuration.java b/bundles/com.espressif.idf.debug.gdbjtag.openocd/src/com/espressif/idf/debug/gdbjtag/openocd/Configuration.java index a8cd9d3a5..5310470ed 100644 --- a/bundles/com.espressif.idf.debug.gdbjtag.openocd/src/com/espressif/idf/debug/gdbjtag/openocd/Configuration.java +++ b/bundles/com.espressif.idf.debug.gdbjtag.openocd/src/com/espressif/idf/debug/gdbjtag/openocd/Configuration.java @@ -104,13 +104,9 @@ public static String[] getGdbServerCommandLineArray(ILaunchConfiguration configu String fmtTelnetPort = useModernSyntax ? "telnet port %d" : "telnet_port %d"; //$NON-NLS-1$ //$NON-NLS-2$ String fmtTclPort = useModernSyntax ? "tcl port %d" : "tcl_port %d"; //$NON-NLS-1$ //$NON-NLS-2$ - int port = PortChecker - .getAvailablePort(DefaultPreferences.GDB_SERVER_GDB_PORT_NUMBER_DEFAULT); - - ILaunchConfigurationWorkingCopy configurationWorkingCopy = configuration.getWorkingCopy(); - configurationWorkingCopy.setAttribute(IGDBJtagConstants.ATTR_PORT_NUMBER, port); - configurationWorkingCopy.doSave(); - + int port = configuration.getAttribute(IGDBJtagConstants.ATTR_PORT_NUMBER, + DefaultPreferences.GDB_SERVER_GDB_PORT_NUMBER_DEFAULT); + ILaunchTarget activeLaunchTarget = Activator.getService(ILaunchBarManager.class).getActiveLaunchTarget(); if (activeLaunchTarget != null) { @@ -127,16 +123,14 @@ public static String[] getGdbServerCommandLineArray(ILaunchConfiguration configu lst.add("-c"); //$NON-NLS-1$ lst.add(String.format(fmtGdbPort, port)); - port = PortChecker - .getAvailablePort(configuration.getAttribute(ConfigurationAttributes.GDB_SERVER_TELNET_PORT_NUMBER, - DefaultPreferences.GDB_SERVER_TELNET_PORT_NUMBER_DEFAULT)); + port = configuration.getAttribute(ConfigurationAttributes.GDB_SERVER_TELNET_PORT_NUMBER, + DefaultPreferences.GDB_SERVER_TELNET_PORT_NUMBER_DEFAULT); lst.add("-c"); //$NON-NLS-1$ lst.add(String.format(fmtTelnetPort, port)); - port = PortChecker.getAvailablePort( - Integer.parseInt(configuration.getAttribute(ConfigurationAttributes.GDB_SERVER_TCL_PORT_NUMBER, - DefaultPreferences.GDB_SERVER_TCL_PORT_NUMBER_DEFAULT))); + port = Integer.parseInt(configuration.getAttribute(ConfigurationAttributes.GDB_SERVER_TCL_PORT_NUMBER, + DefaultPreferences.GDB_SERVER_TCL_PORT_NUMBER_DEFAULT)); lst.add("-c"); //$NON-NLS-1$ lst.add(String.format(fmtTclPort, port)); @@ -344,6 +338,27 @@ public static boolean getDoStartGdbClient(ILaunchConfiguration config) throws Co DefaultPreferences.DO_START_GDB_CLIENT_DEFAULT); } + /** + * Assigns free OpenOCD ports on an in-memory working copy (no {@code doSave()}). + * Must be called once per launch before the server command line is built. + */ + public static void allocateServerPorts(ILaunchConfigurationWorkingCopy configuration) throws CoreException + { + int gdbPort = PortChecker.getAvailablePort(configuration.getAttribute(IGDBJtagConstants.ATTR_PORT_NUMBER, + DefaultPreferences.GDB_SERVER_GDB_PORT_NUMBER_DEFAULT)); + configuration.setAttribute(IGDBJtagConstants.ATTR_PORT_NUMBER, gdbPort); + + int telnetPort = PortChecker.getAvailablePort(configuration.getAttribute( + ConfigurationAttributes.GDB_SERVER_TELNET_PORT_NUMBER, + DefaultPreferences.GDB_SERVER_TELNET_PORT_NUMBER_DEFAULT)); + configuration.setAttribute(ConfigurationAttributes.GDB_SERVER_TELNET_PORT_NUMBER, telnetPort); + + int tclPort = PortChecker.getAvailablePort(Integer.parseInt(configuration.getAttribute( + ConfigurationAttributes.GDB_SERVER_TCL_PORT_NUMBER, + DefaultPreferences.GDB_SERVER_TCL_PORT_NUMBER_DEFAULT))); + configuration.setAttribute(ConfigurationAttributes.GDB_SERVER_TCL_PORT_NUMBER, String.valueOf(tclPort)); + } + // ------------------------------------------------------------------------ private static boolean supportsAdapterUsbCommand(String executablePath) diff --git a/bundles/com.espressif.idf.debug.gdbjtag.openocd/src/com/espressif/idf/debug/gdbjtag/openocd/dsf/GdbBackend.java b/bundles/com.espressif.idf.debug.gdbjtag.openocd/src/com/espressif/idf/debug/gdbjtag/openocd/dsf/GdbBackend.java index c45374269..f78092ac7 100644 --- a/bundles/com.espressif.idf.debug.gdbjtag.openocd/src/com/espressif/idf/debug/gdbjtag/openocd/dsf/GdbBackend.java +++ b/bundles/com.espressif.idf.debug.gdbjtag.openocd/src/com/espressif/idf/debug/gdbjtag/openocd/dsf/GdbBackend.java @@ -44,6 +44,7 @@ import java.util.Properties; import java.util.concurrent.ExecutionException; import java.util.concurrent.TimeUnit; +import java.util.concurrent.atomic.AtomicBoolean; import org.eclipse.cdt.core.model.ICProject; import org.eclipse.cdt.core.parser.util.StringUtil; @@ -53,7 +54,6 @@ import org.eclipse.cdt.dsf.concurrent.ImmediateRequestMonitor; import org.eclipse.cdt.dsf.concurrent.RequestMonitor; import org.eclipse.cdt.dsf.concurrent.Sequence; -import org.eclipse.cdt.dsf.concurrent.Sequence.Step; import org.eclipse.cdt.dsf.gdb.IGDBLaunchConfigurationConstants; import org.eclipse.cdt.dsf.gdb.IGdbDebugPreferenceConstants; import org.eclipse.cdt.dsf.gdb.internal.GdbPlugin; @@ -61,11 +61,8 @@ import org.eclipse.cdt.dsf.gdb.service.IGDBBackend; import org.eclipse.cdt.dsf.gdb.service.SessionType; import org.eclipse.cdt.dsf.gdb.service.command.GDBControl.InitializationShutdownStep; -import org.eclipse.cdt.dsf.gdb.service.command.GDBControl.InitializationShutdownStep.Direction; import org.eclipse.cdt.dsf.mi.service.IMIBackend; import org.eclipse.cdt.dsf.mi.service.IMIBackend2; -import org.eclipse.cdt.dsf.mi.service.IMIBackend.BackendStateChangedEvent; -import org.eclipse.cdt.dsf.mi.service.IMIBackend.State; import org.eclipse.cdt.dsf.mi.service.command.events.MIStoppedEvent; import org.eclipse.cdt.dsf.service.AbstractDsfService; import org.eclipse.cdt.dsf.service.DsfServiceEventHandler; @@ -86,6 +83,7 @@ import org.eclipse.core.variables.VariablesPlugin; 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.ILaunchConfigurationWorkingCopy; import org.eclipse.debug.core.ILaunchManager; @@ -109,6 +107,7 @@ * * This class is taken from {@link GnuMcuGdbBackend} */ +@SuppressWarnings("restriction") public class GdbBackend extends AbstractDsfService implements IGDBBackend, IMIBackend2 { private final ILaunchConfiguration fLaunchConfiguration; @@ -570,8 +569,24 @@ public void destroy() { } // destroy() should be supported even if it's not spawner. - if (getState() == State.STARTED) { + if (getState() == State.STARTED && fProcess != null) + { fProcess.destroy(); + if (fProcess.isAlive()) + { + try + { + if (!fProcess.waitFor(2, TimeUnit.SECONDS)) + { + fProcess.destroyForcibly(); + } + } + catch (InterruptedException e) + { + Thread.currentThread().interrupt(); + fProcess.destroyForcibly(); + } + } } } @@ -618,11 +633,7 @@ public void initialize(final RequestMonitor requestMonitor) { System.out.println("GDBProcessStep.initialise()"); } - class GDBLaunchMonitor { - boolean fLaunched = false; - boolean fTimedOut = false; - } - final GDBLaunchMonitor fGDBLaunchMonitor = new GDBLaunchMonitor(); + final AtomicBoolean isFinished = new AtomicBoolean(false); final RequestMonitor gdbLaunchRequestMonitor = new RequestMonitor(getExecutor(), requestMonitor) { @Override @@ -630,9 +641,8 @@ protected void handleCompleted() { if (Activator.getInstance().isDebugging()) { System.out.println("GDBProcessStep.initialise() handleCompleted()"); } - - if (!fGDBLaunchMonitor.fTimedOut) { - fGDBLaunchMonitor.fLaunched = true; + if (isFinished.compareAndSet(false, true)) + { if (!isSuccess()) { requestMonitor.setStatus(getStatus()); } @@ -662,6 +672,11 @@ protected IStatus run(IProgressMonitor monitor) { try { fProcess = launchGDBProcess(); + ILaunch launch = (ILaunch) getSession().getModelAdapter(ILaunch.class); + if (launch != null && fProcess != null) + { + LaunchProcessDictionary.getInstance().registerBackendProcess(launch, "gdb", fProcess); //$NON-NLS-1$ + } // Need to do this on the executor for thread-safety getExecutor().submit(new DsfRunnable() { @Override @@ -679,8 +694,8 @@ public void run() { return Status.OK_STATUS; } + final StringBuilder errorBuilder = new StringBuilder(); BufferedReader inputReader = null; - BufferedReader errorReader = null; boolean success = false; try { // Read initial GDB prompt @@ -696,35 +711,55 @@ public void run() { // Failed to read initial prompt, check for error if (!success) { - errorReader = new BufferedReader(new InputStreamReader(getMIErrorStream())); - String errorInfo = errorReader.readLine(); - if (errorInfo == null) { + Thread errorDrainThread = new Thread(new Runnable() + { + @Override + public void run() + { + try (BufferedReader errReader = new BufferedReader( + new InputStreamReader(getMIErrorStream()))) + { + String errLine; + while ((errLine = errReader.readLine()) != null) + { + errorBuilder.append(errLine).append("\n"); + } + } + catch (IOException e) + { + // Stream closed or error, safely ignore + } + } + }, "GDB Error Stream Drain"); + errorDrainThread.setDaemon(true); + errorDrainThread.start(); + errorDrainThread.join(1000); + + String errorInfo = errorBuilder.toString().trim(); + + if (errorInfo.isEmpty()) + { errorInfo = "GDB prompt not read"; //$NON-NLS-1$ } gdbLaunchRequestMonitor .setStatus(new Status(IStatus.ERROR, GdbPlugin.PLUGIN_ID, -1, errorInfo, null)); } - } catch (IOException e) { + } + catch (Exception e) + { success = false; gdbLaunchRequestMonitor.setStatus( new Status(IStatus.ERROR, GdbPlugin.PLUGIN_ID, -1, "Error reading GDB output", e)); //$NON-NLS-1$ - } - - // In the case of failure, close the MI streams so - // they are not leaked. - if (!success) { - if (inputReader != null) { + } finally + { + // In the case of failure, close the MI stream so it is not leaked. + if (!success && inputReader != null) + { try { inputReader.close(); } catch (IOException e) { } } - if (errorReader != null) { - try { - errorReader.close(); - } catch (IOException e) { - } - } } gdbLaunchRequestMonitor.done(); @@ -736,10 +771,8 @@ public void run() { getExecutor().schedule(new Runnable() { @Override public void run() { - // Only process the event if we have not finished yet (hit - // the breakpoint). - if (!fGDBLaunchMonitor.fLaunched) { - fGDBLaunchMonitor.fTimedOut = true; + if (isFinished.compareAndSet(false, true)) + { Thread jobThread = startGdbJob.getThread(); if (jobThread != null) { jobThread.interrupt(); @@ -785,7 +818,8 @@ protected IStatus run(IProgressMonitor monitor) { public void run() { destroy(); - if (fMonitorJob.fMonitorExited) { + if (fMonitorJob != null && fMonitorJob.isMonitorExited()) + { // Now that we have destroyed the process, // and that the monitoring thread was // killed, @@ -917,71 +951,87 @@ protected void shutdown(RequestMonitor requestMonitor) { * the associated runtime process. */ private class MonitorJob extends Job { - boolean fMonitorExited = false; - DsfRunnable fMonitorStarted; - Process fMonProcess; + private volatile boolean fMonitorExited = false; + private final DsfRunnable fMonitorStarted; + private final Process fMonProcess; + private final Object fLock = new Object(); + + MonitorJob(Process process, DsfRunnable monitorStarted) + { + super("GDB process monitor job."); //$NON-NLS-1$ + fMonProcess = process; + fMonitorStarted = monitorStarted; + setSystem(true); + } @Override - protected IStatus run(IProgressMonitor monitor) { - synchronized (fMonProcess) { - getExecutor().submit(fMonitorStarted); - try { - fMonProcess.waitFor(); - fGDBExitValue = fMonProcess.exitValue(); + protected IStatus run(IProgressMonitor monitor) + { + getExecutor().submit(fMonitorStarted); + try + { + fMonProcess.waitFor(); + fGDBExitValue = fMonProcess.exitValue(); - if (Activator.getInstance().isDebugging()) { - System.out.println("MonitorJob.run() exitValue() " + fGDBExitValue); - } - - if(fProcess.isAlive() && Activator.getInstance().isDebugging()) + if (Activator.getInstance().isDebugging()) + { + System.out.println("MonitorJob.run() exitValue() " + fGDBExitValue); + } + + + getExecutor().submit(new DsfRunnable() + { + @Override + public void run() { - // Need to do this on the executor for thread-safety - getExecutor().submit(new DsfRunnable() { - @Override - public void run() { - if (Activator.getInstance().isDebugging()) { - System.out.println("MonitorJob.run() run() "); - } + if (Activator.getInstance().isDebugging()) + { + System.out.println("MonitorJob.run() run() "); + } - destroy(); - fBackendState = State.TERMINATED; + destroy(); + fBackendState = State.TERMINATED; - if (Activator.getInstance().isDebugging()) { - System.out.println( - "MonitorJob.run() run() dispatchEvent(BackendStateChangedEvent, TERMINATED)"); - } - getSession().dispatchEvent( - new BackendStateChangedEvent(getSession().getId(), getId(), State.TERMINATED), - getProperties()); - } - }); + if (Activator.getInstance().isDebugging()) + { + System.out.println( + "MonitorJob.run() run() dispatchEvent(BackendStateChangedEvent, TERMINATED)"); + } + getSession().dispatchEvent( + new BackendStateChangedEvent(getSession().getId(), getId(), State.TERMINATED), + getProperties()); } - - } catch (InterruptedException ie) { - // clear interrupted state - Thread.interrupted(); - } + }); - fMonitorExited = true; + } + catch (InterruptedException ie) + { + Thread.interrupted(); + } finally + { + synchronized (fLock) + { + fMonitorExited = true; + } } return Status.OK_STATUS; } - MonitorJob(Process process, DsfRunnable monitorStarted) { - super("GDB process monitor job."); //$NON-NLS-1$ - fMonProcess = process; - fMonitorStarted = monitorStarted; - setSystem(true); - } - void kill() { - synchronized (fMonProcess) { - if (!fMonitorExited) { + synchronized (fLock) + { + if (!fMonitorExited && getThread() != null) + { getThread().interrupt(); } } } + + public boolean isMonitorExited() + { + return fMonitorExited; + } } /** diff --git a/bundles/com.espressif.idf.debug.gdbjtag.openocd/src/com/espressif/idf/debug/gdbjtag/openocd/dsf/GdbConsoleCleanup.java b/bundles/com.espressif.idf.debug.gdbjtag.openocd/src/com/espressif/idf/debug/gdbjtag/openocd/dsf/GdbConsoleCleanup.java new file mode 100644 index 000000000..7749db652 --- /dev/null +++ b/bundles/com.espressif.idf.debug.gdbjtag.openocd/src/com/espressif/idf/debug/gdbjtag/openocd/dsf/GdbConsoleCleanup.java @@ -0,0 +1,97 @@ +package com.espressif.idf.debug.gdbjtag.openocd.dsf; + +import java.util.ArrayList; +import java.util.List; + +import org.eclipse.cdt.debug.ui.CDebugUIPlugin; +import org.eclipse.cdt.debug.ui.debuggerconsole.IDebuggerConsole; +import org.eclipse.core.runtime.jobs.Job; +import org.eclipse.debug.core.ILaunch; +import org.eclipse.ui.console.ConsolePlugin; +import org.eclipse.ui.console.IConsole; + +import com.espressif.idf.core.logging.Logger; + +/** + * Stops CDT GDB CLI console read jobs that otherwise busy-loop on + * {@code LargePipedInputStream} after a stuck debug termination. + *

+ * Routing through {@link CDebugUIPlugin#getDebuggerConsoleManager()} handles both + * the basic ({@code GdbBasicCliConsole}) and full ({@code GdbFullCliConsole}) GDB + * consoles, since both implement {@link IDebuggerConsole}. + */ +public final class GdbConsoleCleanup +{ + private static final String GDB_CLI_JOB_MARKER = "GDB CLI"; //$NON-NLS-1$ + private GdbConsoleCleanup() + { + } + + public static void stopConsolesForLaunch(ILaunch launch) + { + if (launch == null) + { + return; + } + + try + { + List toRemove = new ArrayList<>(); + for (IDebuggerConsole console : CDebugUIPlugin.getDebuggerConsoleManager().getConsoles()) + { + if (!launch.equals(console.getLaunch())) + { + continue; + } + + try + { + console.stop(); + } + catch (Exception e) + { + Logger.log(e); + } + + try + { + CDebugUIPlugin.getDebuggerConsoleManager().removeConsole(console); + } + catch (Exception e) + { + Logger.log(e); + } + + toRemove.add(console); + } + + if (!toRemove.isEmpty()) + { + ConsolePlugin.getDefault().getConsoleManager() + .removeConsoles(toRemove.toArray(new IConsole[0])); + } + } + catch (Exception e) + { + Logger.log(e); + } + + cancelGdbCliReadJobs(); + } + + private static void cancelGdbCliReadJobs() + { + for (Job job : Job.getJobManager().find(null)) + { + if (job == null) + { + continue; + } + String name = job.getName(); + if (name != null && name.contains(GDB_CLI_JOB_MARKER)) + { + job.cancel(); + } + } + } +} diff --git a/bundles/com.espressif.idf.debug.gdbjtag.openocd/src/com/espressif/idf/debug/gdbjtag/openocd/dsf/GdbServerBackend.java b/bundles/com.espressif.idf.debug.gdbjtag.openocd/src/com/espressif/idf/debug/gdbjtag/openocd/dsf/GdbServerBackend.java index 66ebd4eb2..1e4791f77 100644 --- a/bundles/com.espressif.idf.debug.gdbjtag.openocd/src/com/espressif/idf/debug/gdbjtag/openocd/dsf/GdbServerBackend.java +++ b/bundles/com.espressif.idf.debug.gdbjtag.openocd/src/com/espressif/idf/debug/gdbjtag/openocd/dsf/GdbServerBackend.java @@ -26,6 +26,7 @@ import org.eclipse.core.runtime.IStatus; import org.eclipse.core.runtime.Platform; import org.eclipse.core.runtime.Status; +import org.eclipse.debug.core.ILaunch; import org.eclipse.debug.core.ILaunchConfiguration; import org.eclipse.embedcdt.core.StringUtils; import org.eclipse.embedcdt.debug.gdbjtag.core.DebugUtils; @@ -241,7 +242,13 @@ protected Process launchGdbServerProcess(String[] commandLineArray) throws CoreE envList.add(entry.getKey() + "=" + entry.getValue()); } - return DebugUtils.exec(commandLineArray, envList.toArray(new String[0]), dir); + Process process = DebugUtils.exec(commandLineArray, envList.toArray(new String[0]), dir); + ILaunch launch = (ILaunch) getSession().getModelAdapter(ILaunch.class); + if (launch != null) + { + LaunchProcessDictionary.getInstance().registerBackendProcess(launch, "openocd", process); //$NON-NLS-1$ + } + return process; } private boolean supportsAdapterUsbCommand(String executablePath) diff --git a/bundles/com.espressif.idf.debug.gdbjtag.openocd/src/com/espressif/idf/debug/gdbjtag/openocd/dsf/Launch.java b/bundles/com.espressif.idf.debug.gdbjtag.openocd/src/com/espressif/idf/debug/gdbjtag/openocd/dsf/Launch.java index 4b2583375..800de8883 100644 --- a/bundles/com.espressif.idf.debug.gdbjtag.openocd/src/com/espressif/idf/debug/gdbjtag/openocd/dsf/Launch.java +++ b/bundles/com.espressif.idf.debug.gdbjtag.openocd/src/com/espressif/idf/debug/gdbjtag/openocd/dsf/Launch.java @@ -7,7 +7,7 @@ * https://www.eclipse.org/legal/epl-2.0/ * * SPDX-License-Identifier: EPL-2.0 - * + * * Contributors: * Liviu Ionescu - initial version *******************************************************************************/ @@ -17,31 +17,30 @@ import java.util.ArrayList; import java.util.HashMap; import java.util.List; -import java.util.Map; import java.util.concurrent.Callable; -import java.util.concurrent.ExecutionException; import java.util.concurrent.RejectedExecutionException; import org.eclipse.cdt.debug.gdbjtag.core.IGDBJtagConstants; -import org.eclipse.cdt.dsf.concurrent.DefaultDsfExecutor; -import org.eclipse.cdt.dsf.concurrent.DsfRunnable; -import org.eclipse.cdt.dsf.concurrent.IDsfStatusConstants; import org.eclipse.cdt.dsf.gdb.IGDBLaunchConfigurationConstants; import org.eclipse.cdt.dsf.gdb.internal.GdbPlugin; +import org.eclipse.cdt.dsf.gdb.launching.GdbLaunch; 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.IProgressMonitor; import org.eclipse.core.runtime.IStatus; import org.eclipse.core.runtime.Status; +import org.eclipse.core.runtime.jobs.Job; 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.ILaunchConfigurationWorkingCopy; import org.eclipse.debug.core.model.IProcess; import org.eclipse.debug.core.model.ISourceLocator; import org.eclipse.embedcdt.debug.gdbjtag.core.dsf.GnuMcuLaunch; +import com.espressif.idf.core.logging.Logger; import com.espressif.idf.debug.gdbjtag.openocd.Activator; import com.espressif.idf.debug.gdbjtag.openocd.Configuration; import com.espressif.idf.debug.gdbjtag.openocd.ConfigurationAttributes; @@ -52,162 +51,117 @@ public class Launch extends GnuMcuLaunch { - // ------------------------------------------------------------------------ - ILaunchConfiguration fConfig = null; - private DsfSession fSession; - private DsfServicesTracker fTracker; - private DefaultDsfExecutor fExecutor; private IProcess openOcdServerProcess; private IProcess gdbIProcess; - + + private boolean fDoStartGdbServer = true; + private boolean fDoStartGdbClient = true; + + private static final int SAFETY_NET_CLEANUP_DELAY_MS = 3000; + private static final String SERVER_PROC_KEY = "SERVER_PROC"; private static final String GDB_PROC_KEY = "GDB_PROC"; - // ------------------------------------------------------------------------ - public Launch(ILaunchConfiguration launchConfiguration, String mode, ISourceLocator locator) { - super(launchConfiguration, mode, locator); - - if (Activator.getInstance().isDebugging()) - { - System.out.println("openocd.Launch.launch(" + launchConfiguration.getName() + "," + mode + ") " + this); - } - fConfig = launchConfiguration; - fExecutor = (DefaultDsfExecutor) getDsfExecutor(); - fSession = getSession(); } - // ------------------------------------------------------------------------ + public void setDoStartGdbServer(boolean doStartGdbServer) + { + fDoStartGdbServer = doStartGdbServer; + } - @Override - public void initialize() + public boolean getDoStartGdbServer() { + return fDoStartGdbServer; + } - if (Activator.getInstance().isDebugging()) - { - System.out.println("openocd.Launch.initialize() " + this); - } + public void setDoStartGdbClient(boolean doStartGdbClient) + { + fDoStartGdbClient = doStartGdbClient; + } - super.initialize(); + public boolean getDoStartGdbClient() + { + return fDoStartGdbClient; + } + + void clearProcessReferences() + { + openOcdServerProcess = null; + gdbIProcess = null; + } - Runnable initRunnable = new DsfRunnable() + private void scheduleSafetyNetCleanup() + { + final ILaunch launch = this; + Job cleanupJob = new Job("Force debug session cleanup") { @Override - public void run() + protected IStatus run(IProgressMonitor monitor) { - fTracker = new DsfServicesTracker(GdbPlugin.getBundleContext(), fSession.getId()); - // fSession.addServiceEventListener(GdbLaunch.this, null); - - // fInitialized = true; - // fireChanged(); + if (launch instanceof GdbLaunch) + { + DsfSession session = ((GdbLaunch) launch).getSession(); + if (session != null && DsfSession.isSessionActive(session.getId())) + { + // Session is still active well after terminate(): the graceful + // shutdown is stuck. Re-kill the OS processes and close the MI + // pipes/consoles so CDT's own shutdown can finally complete. + LaunchProcessDictionary.getInstance().forceKillOsProcesses(launch); + } + } + return Status.OK_STATUS; } }; - - try - { - cleanUpOldLaunchProcesses(); - } - catch (CoreException e) - { - e.printStackTrace(); - } - - // Invoke the execution code and block waiting for the result. - try - { - fExecutor.submit(initRunnable).get(); - } - catch (InterruptedException e) - { - new Status(IStatus.ERROR, Activator.PLUGIN_ID, IDsfStatusConstants.INTERNAL_ERROR, - "Error initializing launch", e); //$NON-NLS-1$ - } - catch (ExecutionException e) - { - new Status(IStatus.ERROR, Activator.PLUGIN_ID, IDsfStatusConstants.INTERNAL_ERROR, - "Error initializing launch", e); //$NON-NLS-1$ - } - + cleanupJob.setSystem(true); + cleanupJob.schedule(SAFETY_NET_CLEANUP_DELAY_MS); } @Override protected void provideDefaults(ILaunchConfigurationWorkingCopy config) throws CoreException { - super.provideDefaults(config); if (!config.hasAttribute(IGDBJtagConstants.ATTR_IP_ADDRESS)) - { - config.setAttribute(IGDBJtagConstants.ATTR_IP_ADDRESS, "localhost"); //$NON-NLS-1$ - } + config.setAttribute(IGDBJtagConstants.ATTR_IP_ADDRESS, "localhost"); if (!config.hasAttribute(IGDBJtagConstants.ATTR_JTAG_DEVICE_ID)) - { config.setAttribute(IGDBJtagConstants.ATTR_JTAG_DEVICE_ID, ConfigurationAttributes.JTAG_DEVICE); - } if (!config.hasAttribute(IGDBJtagConstants.ATTR_PORT_NUMBER)) - { config.setAttribute(IGDBJtagConstants.ATTR_PORT_NUMBER, DefaultPreferences.GDB_SERVER_GDB_PORT_NUMBER_DEFAULT); - } if (!config.hasAttribute(IGDBLaunchConfigurationConstants.ATTR_DEBUG_NAME)) - { - DefaultPreferences fDefaultPreferences = Activator.getInstance().getDefaultPreferences(); config.setAttribute(IGDBLaunchConfigurationConstants.ATTR_DEBUG_NAME, - fDefaultPreferences.getGdbClientExecutable()); - } + Activator.getInstance().getDefaultPreferences().getGdbClientExecutable()); if (Configuration.getDoStartGdbServer(config)) - { config.setAttribute(IGDBJtagConstants.ATTR_PORT_NUMBER, DefaultPreferences.GDB_SERVER_GDB_PORT_NUMBER_DEFAULT); - } - + config.setAttribute(DebugPlugin.ATTR_PROCESS_FACTORY_ID, CustomIdfProcessFactory.ID); } - // ------------------------------------------------------------------------ - public void initializeServerConsole(IProgressMonitor monitor) throws CoreException { - - if (Activator.getInstance().isDebugging()) - { - System.out.println("openocd.Launch.initializeServerConsole()"); - } - - boolean doAddServerConsole = Configuration.getDoAddServerConsole(fConfig); - - if (doAddServerConsole) + if (Configuration.getDoAddServerConsole(fConfig)) { - - // Add the GDB server process to the launch tree openOcdServerProcess = addServerProcess(Configuration.getGdbServerCommandName(fConfig)); - LaunchProcessDictionary.getInstance().addProcessToDictionary(getLaunchConfiguration().getName(), SERVER_PROC_KEY, openOcdServerProcess); + LaunchProcessDictionary.getInstance().addProcessToDictionary(this, SERVER_PROC_KEY, openOcdServerProcess); monitor.worked(1); } } public void initializeConsoles(IProgressMonitor monitor) throws CoreException { - - if (Activator.getInstance().isDebugging()) - { - System.out.println("openocd.Launch.initializeConsoles()"); - } - - { - // Add the GDB client process to the launch tree. - gdbIProcess = addClientProcess(Configuration.getGdbClientCommandName(fConfig)); - gdbIProcess.setAttribute(IProcess.ATTR_CMDLINE, Configuration.getGdbClientCommandLine(fConfig)); - LaunchProcessDictionary.getInstance().addProcessToDictionary(getLaunchConfiguration().getName(), GDB_PROC_KEY, gdbIProcess); - monitor.worked(1); - } + gdbIProcess = addClientProcess(Configuration.getGdbClientCommandName(fConfig)); + gdbIProcess.setAttribute(IProcess.ATTR_CMDLINE, Configuration.getGdbClientCommandLine(fConfig)); + LaunchProcessDictionary.getInstance().addProcessToDictionary(this, GDB_PROC_KEY, gdbIProcess); + monitor.worked(1); } public IProcess addServerProcess(String label) throws CoreException @@ -215,97 +169,89 @@ public IProcess addServerProcess(String label) throws CoreException IProcess newProcess = null; try { - // Add the server process object to the launch. Process serverProc = getDsfExecutor().submit(new Callable() { @Override public Process call() throws CoreException { - GdbServerBackend backend = fTracker.getService(GdbServerBackend.class); - if (backend != null) + DsfServicesTracker tracker = new DsfServicesTracker(GdbPlugin.getBundleContext(), + getSession().getId()); + try + { + GdbServerBackend backend = tracker.getService(GdbServerBackend.class); + return backend != null ? backend.getServerProcess() : null; + } + finally { - return backend.getServerProcess(); + tracker.dispose(); } - return null; } }).get(); - // Need to go through DebugPlugin.newProcess so that we can use - // the overrideable process factory to allow others to override. - // First set attribute to specify we want to create the gdb process. - // Bug 210366 - Map attributes = new HashMap(); if (serverProc != null) { - newProcess = DebugPlugin.newProcess(this, serverProc, label, attributes); + newProcess = DebugPlugin.newProcess(this, serverProc, label, new HashMap()); } } - catch (InterruptedException e) + catch (Exception e) { - throw new CoreException(new Status(IStatus.ERROR, Activator.PLUGIN_ID, 0, - "Interrupted while waiting for get process callable.", e)); //$NON-NLS-1$ + throw new CoreException( + new Status(IStatus.ERROR, Activator.PLUGIN_ID, 0, "Error adding server process.", e)); } - catch (ExecutionException e) - { - throw (CoreException) e.getCause(); - } - catch (RejectedExecutionException e) - { - throw new CoreException(new Status(IStatus.ERROR, Activator.PLUGIN_ID, 0, - "Debugger shut down before launch was completed.", e)); //$NON-NLS-1$ - } - return newProcess; } - // ------------------------------------------------------------------------ - @Override public void terminate() throws DebugException { - super.terminate(); - - LaunchProcessDictionary.getInstance().killAllProcessesInLaunch(getLaunchConfiguration().getName()); + // Start CDT's normal terminate first so the framework owns ending the DSF + // session and shutting down its executor. Then force-kill the OS processes and + // close the MI pipes/consoles: that is what unblocks the graceful shutdown when + // gdb/openocd are stuck (e.g. a core-reset loop), without us pre-terminating the + // executor and triggering RejectedExecutionException in the framework. + try + { + if (!isTerminated()) + { + super.terminate(); + } + } + catch (RejectedExecutionException e) + { + // DSF executor already terminating; OS processes are force-killed below. + } + catch (Exception e) + { + Logger.log(e); + } + finally + { + LaunchProcessDictionary.getInstance().forceKillOsProcesses(this); + clearProcessReferences(); + scheduleSafetyNetCleanup(); + } } - + @Override public boolean canDisconnect() { return true; } - + @Override public boolean canTerminate() { return true; } - - + @Override public IProcess[] getProcesses() { List processes = new ArrayList<>(); - if (openOcdServerProcess != null) { - processes.add(openOcdServerProcess); - } - if (gdbIProcess != null) { - processes.add(gdbIProcess); - } - return processes.toArray(new IProcess[0]); - } - - private void cleanUpOldLaunchProcesses() throws CoreException - { - IProcess serverIProcess = LaunchProcessDictionary.getInstance().getProcessFromDictionary(getLaunchConfiguration().getName(), SERVER_PROC_KEY); - if (serverIProcess != null && !serverIProcess.isTerminated()) - { - serverIProcess.terminate(); - } - - IProcess gdbIProcess = LaunchProcessDictionary.getInstance().getProcessFromDictionary(getLaunchConfiguration().getName(), GDB_PROC_KEY); - if(gdbIProcess != null && !gdbIProcess.isTerminated()) - { - gdbIProcess.terminate(); - } + if (openOcdServerProcess != null) + processes.add(openOcdServerProcess); + if (gdbIProcess != null) + processes.add(gdbIProcess); + return processes.toArray(new IProcess[0]); } } diff --git a/bundles/com.espressif.idf.debug.gdbjtag.openocd/src/com/espressif/idf/debug/gdbjtag/openocd/dsf/LaunchConfigurationDelegate.java b/bundles/com.espressif.idf.debug.gdbjtag.openocd/src/com/espressif/idf/debug/gdbjtag/openocd/dsf/LaunchConfigurationDelegate.java index 5f59eea21..1c248a1c9 100644 --- a/bundles/com.espressif.idf.debug.gdbjtag.openocd/src/com/espressif/idf/debug/gdbjtag/openocd/dsf/LaunchConfigurationDelegate.java +++ b/bundles/com.espressif.idf.debug.gdbjtag.openocd/src/com/espressif/idf/debug/gdbjtag/openocd/dsf/LaunchConfigurationDelegate.java @@ -9,8 +9,8 @@ * SPDX-License-Identifier: EPL-2.0 * * Contributors: - * Liviu Ionescu - initial version - * Jonah Graham - fix for Neon + * Liviu Ionescu - initial version + * Jonah Graham - fix for Neon *******************************************************************************/ package com.espressif.idf.debug.gdbjtag.openocd.dsf; @@ -23,6 +23,9 @@ import java.util.concurrent.Callable; import java.util.concurrent.CancellationException; import java.util.concurrent.ExecutionException; +import java.util.concurrent.RejectedExecutionException; +import java.util.concurrent.TimeUnit; +import java.util.concurrent.TimeoutException; import org.eclipse.cdt.dsf.concurrent.DataRequestMonitor; import org.eclipse.cdt.dsf.concurrent.ImmediateExecutor; @@ -52,6 +55,7 @@ import org.eclipse.debug.core.DebugPlugin; import org.eclipse.debug.core.ILaunch; import org.eclipse.debug.core.ILaunchConfiguration; +import org.eclipse.debug.core.ILaunchConfigurationWorkingCopy; import org.eclipse.debug.core.ILaunchManager; import org.eclipse.debug.core.model.ISourceLocator; import org.eclipse.embedcdt.core.StringUtils; @@ -59,116 +63,94 @@ import org.eclipse.embedcdt.debug.gdbjtag.core.dsf.AbstractGnuMcuLaunchConfigurationDelegate; import org.eclipse.embedcdt.debug.gdbjtag.core.dsf.GnuMcuServerServicesLaunchSequence; +import com.espressif.idf.core.logging.Logger; import com.espressif.idf.debug.gdbjtag.openocd.Activator; import com.espressif.idf.debug.gdbjtag.openocd.Configuration; +import com.espressif.idf.debug.gdbjtag.openocd.ConfigurationAttributes; import com.espressif.idf.debug.gdbjtag.openocd.ui.Messages; -/** - * This class is referred in the plugin.xml as an "org.eclipse.debug.core.launchDelegates" extension point. - * - * It inherits directly from the GDB Hardware Debug plug-in. - * - * - */ @SuppressWarnings("restriction") public class LaunchConfigurationDelegate extends AbstractGnuMcuLaunchConfigurationDelegate { + private static final String NON_STOP_FIRST_VERSION = "6.8.50"; //$NON-NLS-1$ + private static final int STATUS_DLL_NOT_FOUND = -1073741515; + private static final int SERVER_STATUS_POLL_INTERVAL_MS = 250; + private static final int SERVER_STATUS_POLL_TIMEOUT_MS = 30_000; + private static final int COMPLETE_INIT_TIMEOUT_MS = 60_000; - // ------------------------------------------------------------------------ - - private final static String NON_STOP_FIRST_VERSION = "6.8.50"; //$NON-NLS-1$ - private final int STATUS_DLL_NOT_FOUND = -1073741515; + private static final ThreadLocal pendingLaunchOptions = new ThreadLocal<>(); ILaunchConfiguration fConfig = null; @SuppressWarnings("unused") private boolean fIsNonStopSession = false; - private boolean fDoStartGdbServer = false; - private boolean fDoStartGdbClient = true; - private boolean fIgnoreGdbClient = false; - - // ------------------------------------------------------------------------ @Override protected IDsfDebugServicesFactory newServiceFactory(ILaunchConfiguration config, String version) { - - if (Activator.getInstance().isDebugging()) - { - System.out.println("openocd.LaunchConfigurationDelegate.newServiceFactory(" + config.getName() + "," - + version + ") " + this); - } - fConfig = config; return new ServicesFactory(version, ILaunchManager.DEBUG_MODE); - // return new GdbJtagDebugServicesFactory(version); } protected IDsfDebugServicesFactory newServiceFactory(ILaunchConfiguration config, String version, String mode) { - - if (Activator.getInstance().isDebugging()) - { - System.out.println("openocd.LaunchConfigurationDelegate.newServiceFactory(" + config.getName() + "," - + version + "," + mode + ") " + this); - } - fConfig = config; return new ServicesFactory(version, mode); - // return new GdbJtagDebugServicesFactory(version); } - public void ignoreGdbClient() - { - fIgnoreGdbClient = true; - } - - public void doNotIngoreGdbClient() + /** + * Launch OpenOCD without starting the GDB client (application-level tracing). + */ + public void runOpenOcdOnlyLaunch(ILaunchConfiguration config, String mode, IProgressMonitor monitor) + throws CoreException { - fIgnoreGdbClient = false; + pendingLaunchOptions.set(LaunchOptions.openOcdOnly()); + try + { + config.launch(mode, monitor != null ? monitor : new NullProgressMonitor()); + } + finally + { + pendingLaunchOptions.remove(); + } } - /** - * This method is called first when starting a debug session. - */ @Override protected GdbLaunch createGdbLaunch(ILaunchConfiguration configuration, String mode, ISourceLocator locator) throws CoreException { - - if (Activator.getInstance().isDebugging()) + ILaunchConfigurationWorkingCopy wc = configuration.getWorkingCopy(); + if (Configuration.getDoStartGdbServer(wc)) { - System.out.println("openocd.LaunchConfigurationDelegate.createGdbLaunch(" + configuration.getName() + "," - + mode + ") " + this); + Configuration.allocateServerPorts(wc); } - fDoStartGdbServer = Configuration.getDoStartGdbServer(configuration); - if (!fIgnoreGdbClient) + Launch launch = new Launch(wc, mode, locator); + launch.setDoStartGdbServer(Configuration.getDoStartGdbServer(wc)); + + LaunchOptions options = pendingLaunchOptions.get(); + if (options != null && options.isOpenOcdOnly()) { - fDoStartGdbClient = Configuration.getDoStartGdbClient(configuration); + wc.setAttribute(ConfigurationAttributes.DO_START_GDB_CLIENT, false); + launch.setDoStartGdbClient(false); + } + else + { + launch.setDoStartGdbClient(Configuration.getDoStartGdbClient(wc)); } - DebugUtils.checkLaunchConfigurationStarted(configuration); - - // return new GdbLaunch(configuration, mode, locator); - return new Launch(configuration, mode, locator); + DebugUtils.checkLaunchConfigurationStarted(wc); + return launch; } @Override protected String getGDBVersion(ILaunchConfiguration config) throws CoreException { - String gdbClientCommand = Configuration.getGdbClientCommand(config, null); - String version = getGDBVersion(config, gdbClientCommand); - if (Activator.getInstance().isDebugging()) - { - System.out.println("openocd.LaunchConfigurationDelegate.getGDBVersion " + version); - } - return version; + return getGDBVersion(config, gdbClientCommand); } private String getGDBVersion(final ILaunchConfiguration configuration, String gdbClientCommand) throws CoreException { - String[] cmdArray = new String[2]; cmdArray[0] = gdbClientCommand; cmdArray[1] = "--version"; @@ -181,23 +163,17 @@ private String getGDBVersion(final ILaunchConfiguration configuration, String gd catch (IOException e) { throw new DebugException(new Status(IStatus.ERROR, Activator.PLUGIN_ID, DebugException.REQUEST_FAILED, - "Error while launching command: " + StringUtils.join(cmdArray, " "), e.getCause()));//$NON-NLS-2$ + "Error while launching command: " + StringUtils.join(cmdArray, " "), e.getCause())); } - // Start a timeout job to make sure we don't get stuck waiting for - // an answer from a gdb that is hanging - // Bug 376203 - Job timeoutJob = new Job("GDB version timeout job") //$NON-NLS-1$ + Job timeoutJob = new Job("GDB version timeout job") { { setSystem(true); } - @Override protected IStatus run(IProgressMonitor arg) { - // Took too long. Kill the gdb process and - // let things clean up. process.destroy(); return Status.OK_STATUS; } @@ -215,27 +191,16 @@ protected IStatus run(IProgressMonitor arg) String line; while ((line = reader.readLine()) != null) { - cmdOutput.append(line); - cmdOutput.append('\n'); // $NON-NLS-1$ + cmdOutput.append(line).append('\n'); } } catch (IOException e) { throw new DebugException(new Status(IStatus.ERROR, Activator.PLUGIN_ID, DebugException.REQUEST_FAILED, - "Error reading GDB STDOUT after sending: " + StringUtils.join(cmdArray, " ") + ", response: " - + cmdOutput, - e.getCause()));// $NON-NLS-1$ + "Error reading GDB STDOUT", e.getCause())); } finally { - // If we get here we are obviously not stuck so we can cancel the - // timeout job. - // Note that it may already have executed, but that is not a - // problem. timeoutJob.cancel(); - - // Cleanup to avoid leaking pipes - // Close the stream we used, and then destroy the process - // Bug 345164 if (stream != null) { try @@ -253,68 +218,80 @@ protected IStatus run(IProgressMonitor arg) if (gdbVersion == null || gdbVersion.isEmpty()) { String errorMessage = process.exitValue() == STATUS_DLL_NOT_FOUND ? Messages.DllNotFound_ExceptionMessage - : cmdOutput.toString(); + : cmdOutput.toString().trim(); + if (errorMessage.isEmpty()) + { + errorMessage = "Could not determine GDB version"; + } throw new DebugException(new Status(IStatus.ERROR, Activator.PLUGIN_ID, DebugException.REQUEST_FAILED, - "Could not determine GDB version after sending: " + StringUtils.join(cmdArray, " ") - + ", response: \n" + errorMessage + "\nERROR CODE:" + process.exitValue(), - null));// $NON-NLS-1$ // $NON-NLS-2$ + errorMessage, null)); } - return gdbVersion; } - public void launchWithoutGdbClient(ILaunchConfiguration config, String mode, ILaunch launch, - IProgressMonitor monitor) throws CoreException - { - fDoStartGdbClient = false; - launch(config, mode, launch, monitor); - } - - /** - * After Launch.initialise(), call here to effectively launch. - * - * The main reason for this is the custom launchDebugSession(). - */ @Override public void launch(ILaunchConfiguration config, String mode, ILaunch launch, IProgressMonitor monitor) throws CoreException { + org.eclipse.cdt.launch.LaunchUtils.enableActivity("org.eclipse.cdt.debug.dsfgdbActivity", true); - if (Activator.getInstance().isDebugging()) - { - System.out.println( - "openocd.LaunchConfigurationDelegate.launch(" + config.getName() + "," + mode + ") " + this); - } + final IProgressMonitor finalMonitor = monitor == null ? new NullProgressMonitor() : monitor; + final Thread mainLaunchThread = Thread.currentThread(); - org.eclipse.cdt.launch.LaunchUtils.enableActivity("org.eclipse.cdt.debug.dsfgdbActivity", true); //$NON-NLS-1$ - if (monitor == null) - { - monitor = new NullProgressMonitor(); - } + Thread cancelWatcher = new Thread(() -> { + try + { + while (!finalMonitor.isCanceled() && !launch.isTerminated()) + { + Thread.sleep(200); + } + + if (finalMonitor.isCanceled()) + { + if (!launch.isTerminated() && launch.canTerminate()) + { + try + { + launch.terminate(); + } + catch (Exception e) + { + Logger.log(e); + } + } + mainLaunchThread.interrupt(); + } + } + catch (InterruptedException e) + { + Thread.currentThread().interrupt(); + } + }); + + cancelWatcher.setDaemon(true); + cancelWatcher.setName("Launch Cancel Watcher"); + cancelWatcher.start(); - if (mode.equals(ILaunchManager.DEBUG_MODE) || mode.equals(ILaunchManager.RUN_MODE)) + try + { + if (mode.equals(ILaunchManager.DEBUG_MODE) || mode.equals(ILaunchManager.RUN_MODE)) + { + launchDebugger(config, launch, finalMonitor); + } + } finally { - launchDebugger(config, launch, monitor); + cancelWatcher.interrupt(); + Thread.interrupted(); } } private void launchDebugger(ILaunchConfiguration config, ILaunch launch, IProgressMonitor monitor) throws CoreException { + Launch idfLaunch = (Launch) launch; + int totalWork = idfLaunch.getDoStartGdbServer() ? 11 : 10; + monitor.beginTask(LaunchMessages.getString("GdbLaunchDelegate.0"), totalWork); - if (Activator.getInstance().isDebugging()) - { - System.out.println("openocd.LaunchConfigurationDelegate.launchDebugger(" + config.getName() + ") " + this); - } - - int totalWork = 10; - if (fDoStartGdbServer) - { - // Extra units due to server console - totalWork += 1; - } - - monitor.beginTask(LaunchMessages.getString("GdbLaunchDelegate.0"), totalWork); //$NON-NLS-1$ if (monitor.isCanceled()) { cleanupLaunch(launch); @@ -330,23 +307,10 @@ private void launchDebugger(ILaunchConfiguration config, ILaunch launch, IProgre } } - /** @since 4.1 */ @Override protected void launchDebugSession(final ILaunchConfiguration config, ILaunch l, IProgressMonitor monitor) throws CoreException { - - if (Activator.getInstance().isDebugging()) - { - System.out.println( - "openocd.LaunchConfigurationDelegate.launchDebugSession(" + config.getName() + ") " + this); - } - - // From here it is almost identical with the system one, except - // the console creation, explicitly marked with '+++++'. - - // -------------------------------------------------------------------- - if (monitor.isCanceled()) { cleanupLaunch(l); @@ -355,135 +319,90 @@ protected void launchDebugSession(final ILaunchConfiguration config, ILaunch l, SessionType sessionType = LaunchUtils.getSessionType(config); boolean attach = LaunchUtils.getIsAttach(config); - - final GdbLaunch launch = (GdbLaunch) l; + final Launch launch = (Launch) l; if (sessionType == SessionType.REMOTE) - { - monitor.subTask(LaunchMessages.getString("GdbLaunchDelegate.1")); //$NON-NLS-1$ - } + monitor.subTask(LaunchMessages.getString("GdbLaunchDelegate.1")); else if (sessionType == SessionType.CORE) - { - monitor.subTask(LaunchMessages.getString("GdbLaunchDelegate.2")); //$NON-NLS-1$ - } + monitor.subTask(LaunchMessages.getString("GdbLaunchDelegate.2")); else - { - assert sessionType == SessionType.LOCAL : "Unexpected session type: " + sessionType.toString(); //$NON-NLS-1$ - monitor.subTask(LaunchMessages.getString("GdbLaunchDelegate.3")); //$NON-NLS-1$ - } + monitor.subTask(LaunchMessages.getString("GdbLaunchDelegate.3")); - // An attach session does not need to necessarily have an - // executable specified. This is because: - // - In remote multi-process attach, there will be more than one - // executable - // In this case executables need to be specified differently. - // The current solution is to use the solib-search-path to specify - // the path of any executable we can attach to. - // - In local single process, GDB has the ability to find the executable - // automatically. if (!attach) - { checkBinaryDetails(config); - } monitor.worked(1); - - // Must set this here for users that call directly the deprecated - // newServiceFactory(String) fIsNonStopSession = LaunchUtils.getIsNonStopMode(config); - String gdbVersion = getGDBVersion(config); - - // First make sure non-stop is supported, if the user want to use this - // mode - if (LaunchUtils.getIsNonStopMode(config) && !isNonStopSupportedInGdbVersion(gdbVersion)) + if (launch.getDoStartGdbClient()) { - cleanupLaunch(launch); - throw new DebugException(new Status(IStatus.ERROR, Activator.PLUGIN_ID, DebugException.REQUEST_FAILED, - "Non-stop mode is not supported for GDB " + gdbVersion + ", GDB " + NON_STOP_FIRST_VERSION //$NON-NLS-1$ //$NON-NLS-2$ - + " or higher is required.", //$NON-NLS-1$ - null)); - } + String gdbVersion = getGDBVersion(config); + + if (LaunchUtils.getIsNonStopMode(config) && !isNonStopSupportedInGdbVersion(gdbVersion)) + { + cleanupLaunch(launch); + throw new DebugException(new Status(IStatus.ERROR, Activator.PLUGIN_ID, DebugException.REQUEST_FAILED, + "Non-stop mode is not supported", null)); + } + + if (LaunchUtils.getIsPostMortemTracing(config) && !isPostMortemTracingSupportedInGdbVersion(gdbVersion)) + { + cleanupLaunch(launch); + throw new DebugException(new Status(IStatus.ERROR, Activator.PLUGIN_ID, DebugException.REQUEST_FAILED, + "Post-mortem tracing is not supported", null)); + } - if (LaunchUtils.getIsPostMortemTracing(config) && !isPostMortemTracingSupportedInGdbVersion(gdbVersion)) + launch.setServiceFactory(newServiceFactory(config, gdbVersion, launch.getLaunchMode())); + } + else { - cleanupLaunch(launch); - throw new DebugException(new Status(IStatus.ERROR, Activator.PLUGIN_ID, DebugException.REQUEST_FAILED, - "Post-mortem tracing is not supported for GDB " + gdbVersion + ", GDB " + NON_STOP_FIRST_VERSION //$NON-NLS-1$ //$NON-NLS-2$ - + " or higher is required.", //$NON-NLS-1$ - null)); + launch.setServiceFactory(newServiceFactory(config, "7.0", launch.getLaunchMode())); } - launch.setServiceFactory(newServiceFactory(config, gdbVersion, launch.getLaunchMode())); - - // Time to start the DSF stuff. First initialize the launch. - // We do this here to avoid having to cleanup in case - // the launch is cancelled above. - // This initialize() call is the first thing that requires cleanup - // followed by the steps further down which also need cleanup. launch.initialize(); - // vvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvv - boolean succeed = false; - - // Assign 4 work ticks. IProgressMonitor subMonServer = new SubProgressMonitor(monitor, 4, SubProgressMonitor.PREPEND_MAIN_LABEL_TO_SUBTASK); - Sequence serverServicesLaunchSequence = getServerServicesSequence(launch.getSession(), launch, subMonServer); try { - // Execute on DSF thread and wait for it. launch.getSession().getExecutor().execute(serverServicesLaunchSequence); serverServicesLaunchSequence.get(); succeed = true; } catch (InterruptedException e1) { - throw new DebugException(new Status(IStatus.ERROR, GdbPlugin.PLUGIN_ID, DebugException.INTERNAL_ERROR, - "Interrupted Exception in dispatch thread", e1)); //$NON-NLS-1$ + if (monitor.isCanceled()) + return; + throw new DebugException( + new Status(IStatus.ERROR, GdbPlugin.PLUGIN_ID, DebugException.INTERNAL_ERROR, "Interrupted", e1)); } catch (ExecutionException e1) { - if (e1.getMessage().contains("Starting OpenOCD timed out.")) //$NON-NLS-1$ + if (e1.getMessage() != null && e1.getMessage().contains("Starting OpenOCD timed out.")) { - IStatus status = new Status(IStatus.OK, Activator.PLUGIN_ID, DebugException.REQUEST_FAILED, - "Error in services launch sequence", e1.getCause()); //$NON-NLS-1$ + IStatus status = new Status(IStatus.ERROR, Activator.PLUGIN_ID, + Activator.OPENOCD_STARTUP_TIMEOUT_STATUS, "Timeout", e1.getCause()); DebugPlugin.getDefault().getStatusHandler(status).handleStatus(status, null); throw new DebugException(Status.OK_STATUS); } - else - { - throw new DebugException(new Status(IStatus.ERROR, GdbPlugin.PLUGIN_ID, DebugException.REQUEST_FAILED, - "Error in services launch sequence", e1.getCause())); //$NON-NLS-1$ - } - + throw new DebugException(new Status(IStatus.ERROR, GdbPlugin.PLUGIN_ID, DebugException.REQUEST_FAILED, + "Error", e1.getCause())); } catch (CancellationException e1) { - // Launch aborted, so exit cleanly - if (Activator.getInstance().isDebugging()) - { - System.out.println("openocd.LaunchConfigurationDelegate.launchDebugger() aborted, so exit cleanly"); - } return; } finally { if (!succeed) - { cleanupLaunch(launch); - } } - if (fDoStartGdbServer) + if (launch.getDoStartGdbServer()) { - - // This contributes 1 work units to the monitor - ((Launch) launch).initializeServerConsole(monitor); - - // Wait for the server to be available, or to know it failed. + launch.initializeServerConsole(monitor); IStatus serverStatus; try { @@ -494,40 +413,38 @@ public IStatus call() throws CoreException { DsfServicesTracker tracker = new DsfServicesTracker(GdbPlugin.getBundleContext(), launch.getSession().getId()); - GdbServerBackend backend = tracker.getService(GdbServerBackend.class); - if (backend != null) - { - return backend.getServerExitStatus(); - } - else + try { + GdbServerBackend backend = tracker.getService(GdbServerBackend.class); + if (backend != null) + return backend.getServerExitStatus(); throw new CoreException( new Status(IStatus.ERROR, Activator.PLUGIN_ID, "Could not start GDB server.")); } + finally + { + tracker.dispose(); + } } }; - // Wait to get the server status. Being endless should not be a - // problem, the timeout will kill it if too long. + long deadline = System.currentTimeMillis() + SERVER_STATUS_POLL_TIMEOUT_MS; serverStatus = null; while (serverStatus == null) { if (monitor.isCanceled()) { - if (Activator.getInstance().isDebugging()) - { - System.out.println( - "openocd.LaunchConfigurationDelegate.launchDebugSession() sleep cancelled" + this); - } cleanupLaunch(launch); return; } - Thread.sleep(10); - serverStatus = launch.getSession().getExecutor().submit(callable).get(); - if (Activator.getInstance().isDebugging()) + if (System.currentTimeMillis() >= deadline) { - System.out.print('!'); + cleanupLaunch(launch); + throw new CoreException(new Status(IStatus.ERROR, Activator.PLUGIN_ID, + Activator.OPENOCD_STARTUP_TIMEOUT_STATUS, "OpenOCD server status timed out.", null)); } + Thread.sleep(SERVER_STATUS_POLL_INTERVAL_MS); + serverStatus = launch.getSession().getExecutor().submit(callable).get(); } if (serverStatus != Status.OK_STATUS) @@ -537,49 +454,32 @@ public IStatus call() throws CoreException cleanupLaunch(launch); return; } - if (Activator.getInstance().isDebugging()) - { - System.out.println("openocd.LaunchConfigurationDelegate.launchDebugger() " + serverStatus); - } throw new CoreException(serverStatus); } - } catch (InterruptedException e) { + if (monitor.isCanceled()) + { + cleanupLaunch(launch); + return; + } Activator.log(e); } catch (ExecutionException e) { Activator.log(e); } - - if (Activator.getInstance().isDebugging()) - { - System.out.println( - "openocd.LaunchConfigurationDelegate.launchDebugSession() * Server start confirmed. *"); - } } - if (!fDoStartGdbClient) - { - if (Activator.getInstance().isDebugging()) - { - System.out.println( - "openocd.LaunchConfigurationDelegate.launchDebugSession() No GDB client, abruptly return."); - } + if (!launch.getDoStartGdbClient()) return; - } - - // ^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^ - // Create and invoke the launch sequence to create the debug control and - // services IProgressMonitor subMon1 = new SubProgressMonitor(monitor, 4, SubProgressMonitor.PREPEND_MAIN_LABEL_TO_SUBTASK); Sequence servicesLaunchSequence = getServicesSequence(launch.getSession(), launch, subMon1); launch.getSession().getExecutor().execute(servicesLaunchSequence); - // boolean succeed = false; + succeed = false; try { servicesLaunchSequence.get(); @@ -587,24 +487,23 @@ public IStatus call() throws CoreException } catch (InterruptedException e1) { - throw new DebugException(new Status(IStatus.ERROR, Activator.PLUGIN_ID, DebugException.INTERNAL_ERROR, - "Interrupted Exception in dispatch thread", e1)); //$NON-NLS-1$ + if (monitor.isCanceled()) + return; + throw new DebugException( + new Status(IStatus.ERROR, Activator.PLUGIN_ID, DebugException.INTERNAL_ERROR, "Interrupted", e1)); } catch (ExecutionException e1) { throw new DebugException(new Status(IStatus.ERROR, Activator.PLUGIN_ID, DebugException.REQUEST_FAILED, - "Error in services launch sequence", e1.getCause())); //$NON-NLS-1$ + "Error", e1.getCause())); } catch (CancellationException e1) { - // Launch aborted, so exit cleanly return; } finally { if (!succeed) - { cleanupLaunch(launch); - } } if (monitor.isCanceled()) @@ -613,30 +512,11 @@ public IStatus call() throws CoreException return; } - // The initializeControl method should be called after the - // ICommandControlService - // is initialised in the ServicesLaunchSequence above. This is because - // it is that - // service that will trigger the launch cleanup (if we need it during - // this launch) - // through an ICommandControlShutdownDMEvent launch.initializeControl(); + launch.initializeConsoles(monitor); - // Add the GDB process object to the launch. - - // vvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvv - // launch.addCLIProcess("gdb"); //$NON-NLS-1$ - // monitor.worked(1); - - // This contributes one work units for the GDB client console - // and optionally one for the semihosting console. - ((Launch) launch).initializeConsoles(monitor); - // ^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^ - - // Create and invoke the final launch sequence to setup GDB final IProgressMonitor subMon2 = new SubProgressMonitor(monitor, 4, SubProgressMonitor.PREPEND_MAIN_LABEL_TO_SUBTASK); - Query completeLaunchQuery = new Query() { @Override @@ -652,13 +532,9 @@ protected void execute(final DataRequestMonitor rm) protected void handleCompleted() { if (isCanceled()) - { rm.cancel(); - } else - { rm.setStatus(getStatus()); - } rm.done(); } }); @@ -669,43 +545,43 @@ protected void handleCompleted() succeed = false; try { - completeLaunchQuery.get(); + // Bounded wait: completeInitialization runs the init commands (e.g. + // "mon reset halt"). If the target is unresponsive or stuck in a reset + // loop these never return, which would otherwise hang the launch forever. + completeLaunchQuery.get(COMPLETE_INIT_TIMEOUT_MS, TimeUnit.MILLISECONDS); succeed = true; } catch (InterruptedException e1) { - throw new DebugException(new Status(IStatus.ERROR, Activator.PLUGIN_ID, DebugException.INTERNAL_ERROR, - "Interrupted Exception in dispatch thread", e1)); //$NON-NLS-1$ + if (monitor.isCanceled()) + return; + throw new DebugException( + new Status(IStatus.ERROR, Activator.PLUGIN_ID, DebugException.INTERNAL_ERROR, "Interrupted", e1)); + } + catch (TimeoutException e1) + { + completeLaunchQuery.cancel(true); + throw new DebugException(new Status(IStatus.ERROR, Activator.PLUGIN_ID, DebugException.REQUEST_FAILED, + "Debugger initialization timed out. The target may be unresponsive or stuck in a reset loop.", e1)); } catch (ExecutionException e1) { throw new DebugException(new Status(IStatus.ERROR, Activator.PLUGIN_ID, DebugException.REQUEST_FAILED, - "Error in final launch sequence", e1.getCause())); //$NON-NLS-1$ + "Error", e1.getCause())); } catch (CancellationException e1) { - // Launch aborted, so exit cleanly return; } finally { if (!succeed) - { - // finalLaunchSequence failed. Shutdown the session so that all - // started - // services including any GDB process are shutdown. (bug 251486) cleanupLaunch(launch); - } } - // -------------------------------------------------------------------- } - - /** - * Perform some local validations before starting the debug session. - */ + @Override protected IPath checkBinaryDetails(final ILaunchConfiguration config) throws CoreException { - boolean doStartServer = true; try { @@ -713,13 +589,11 @@ protected IPath checkBinaryDetails(final ILaunchConfiguration config) throws Cor } catch (CoreException e) { - ; + Logger.log(e); } if (doStartServer) { - // If we should start the server, there must be a configuration - // present, otherwise refuse to start. String configOptions = ""; try { @@ -727,47 +601,76 @@ protected IPath checkBinaryDetails(final ILaunchConfiguration config) throws Cor } catch (CoreException e) { - ; + Logger.log(e); } - if (configOptions.isEmpty()) - { throw new CoreException( - new Status(IStatus.ERROR, Activator.PLUGIN_ID, "Missing mandatory configuration. " - + "Fill-in the 'Config options:' field in the Debugger tab.")); //$NON-NLS-1$ - } + new Status(IStatus.ERROR, Activator.PLUGIN_ID, "Missing mandatory configuration.")); } - - IPath path = super.checkBinaryDetails(config); - return path; + return super.checkBinaryDetails(config); } - /** - * Get a custom launch sequence, that inserts a GDB server starter. - */ @Override protected Sequence getServicesSequence(DsfSession session, ILaunch launch, IProgressMonitor progressMonitor) { - - if (Activator.getInstance().isDebugging()) - { - System.out.println("openocd.LaunchConfigurationDelegate.getServicesSequence()"); - } - return new ServicesLaunchSequence(session, (GdbLaunch) launch, progressMonitor); } protected Sequence getServerServicesSequence(DsfSession session, ILaunch launch, IProgressMonitor progressMonitor) { + return new GnuMcuServerServicesLaunchSequence(session, (GdbLaunch) launch, progressMonitor); + } - if (Activator.getInstance().isDebugging()) + @Override + protected void cleanupLaunch(final ILaunch launch) + { + try { - System.out.println("openocd.LaunchConfigurationDelegate.getServerServicesSequence()"); + LaunchProcessDictionary.getInstance().killAllProcessesInLaunch(launch); + } + catch (Exception e) + { + Logger.log(e); } - return new GnuMcuServerServicesLaunchSequence(session, (GdbLaunch) launch, progressMonitor); - } - - // ------------------------------------------------------------------------ + if (launch instanceof Launch) + { + ((Launch) launch).clearProcessReferences(); + } + Job cleanupJob = new Job("Terminating Launch") + { + @Override + protected IStatus run(IProgressMonitor m) + { + try + { + LaunchConfigurationDelegate.super.cleanupLaunch(launch); + } + catch (RejectedExecutionException e) + { + // Expected: terminate() already started the DSF executor shutdown. + } + catch (Exception e) + { + Logger.log(e); + } + try + { + DebugPlugin.getDefault().getLaunchManager().removeLaunch(launch); + } + catch (RejectedExecutionException e) + { + // Expected: the DSF session/executor is already shutting down. + } + catch (Exception e) + { + Logger.log(e); + } + return Status.OK_STATUS; + } + }; + cleanupJob.setSystem(true); + cleanupJob.schedule(); + } } diff --git a/bundles/com.espressif.idf.debug.gdbjtag.openocd/src/com/espressif/idf/debug/gdbjtag/openocd/dsf/LaunchOptions.java b/bundles/com.espressif.idf.debug.gdbjtag.openocd/src/com/espressif/idf/debug/gdbjtag/openocd/dsf/LaunchOptions.java new file mode 100644 index 000000000..d50a36fb0 --- /dev/null +++ b/bundles/com.espressif.idf.debug.gdbjtag.openocd/src/com/espressif/idf/debug/gdbjtag/openocd/dsf/LaunchOptions.java @@ -0,0 +1,25 @@ +package com.espressif.idf.debug.gdbjtag.openocd.dsf; + +/** + * Per-launch options passed from the delegate entry point to {@link Launch}. + * Stored in a {@link ThreadLocal} for the duration of a single launch call. + */ +public final class LaunchOptions +{ + private final boolean openOcdOnly; + + private LaunchOptions(boolean openOcdOnly) + { + this.openOcdOnly = openOcdOnly; + } + + public static LaunchOptions openOcdOnly() + { + return new LaunchOptions(true); + } + + public boolean isOpenOcdOnly() + { + return openOcdOnly; + } +} diff --git a/bundles/com.espressif.idf.debug.gdbjtag.openocd/src/com/espressif/idf/debug/gdbjtag/openocd/dsf/LaunchProcessDictionary.java b/bundles/com.espressif.idf.debug.gdbjtag.openocd/src/com/espressif/idf/debug/gdbjtag/openocd/dsf/LaunchProcessDictionary.java index 3199d8989..f8089efd1 100644 --- a/bundles/com.espressif.idf.debug.gdbjtag.openocd/src/com/espressif/idf/debug/gdbjtag/openocd/dsf/LaunchProcessDictionary.java +++ b/bundles/com.espressif.idf.debug.gdbjtag.openocd/src/com/espressif/idf/debug/gdbjtag/openocd/dsf/LaunchProcessDictionary.java @@ -1,73 +1,432 @@ package com.espressif.idf.debug.gdbjtag.openocd.dsf; -import java.util.HashMap; +import java.io.InputStream; +import java.io.OutputStream; +import java.util.ArrayList; +import java.util.List; import java.util.Map; +import java.util.concurrent.ConcurrentHashMap; +import java.util.concurrent.ExecutionException; +import java.util.concurrent.RejectedExecutionException; +import java.util.concurrent.TimeUnit; +import java.util.concurrent.TimeoutException; -import org.eclipse.debug.core.DebugException; +import org.eclipse.cdt.dsf.gdb.internal.GdbPlugin; +import org.eclipse.cdt.dsf.gdb.launching.GdbLaunch; +import org.eclipse.cdt.dsf.gdb.service.IGDBBackend; +import org.eclipse.cdt.dsf.service.DsfServicesTracker; +import org.eclipse.cdt.dsf.service.DsfSession; +import org.eclipse.core.runtime.Platform; +import org.eclipse.core.runtime.jobs.Job; +import org.eclipse.debug.core.ILaunch; import org.eclipse.debug.core.model.IProcess; +import com.espressif.idf.core.logging.Logger; +import com.espressif.idf.debug.gdbjtag.openocd.dsf.process.IdfRuntimeProcess; + +/** + * Tracks debug processes per {@link ILaunch} and terminates only those belonging + * to the given launch. Does not use system-wide kill or DSF executor shutdown. + */ public class LaunchProcessDictionary { - private static LaunchProcessDictionary instance; - - private Map> processDictionary; - + private static final int PROCESS_DESTROY_TIMEOUT_SECONDS = 5; + + private static final int BACKEND_QUERY_TIMEOUT_SECONDS = 5; + + private static final LaunchProcessDictionary instance = new LaunchProcessDictionary(); + private final Map> processDictionary; + private final Map> backendPidDictionary; + private LaunchProcessDictionary() { - processDictionary = new HashMap<>(); + processDictionary = new ConcurrentHashMap<>(); + backendPidDictionary = new ConcurrentHashMap<>(); } - + public static LaunchProcessDictionary getInstance() { - if(instance == null) - instance = new LaunchProcessDictionary(); - return instance; } - - public void addProcessToDictionary(String launchName, String procName, IProcess process) + + public void addProcessToDictionary(ILaunch launch, String procName, IProcess process) + { + if (launch == null || process == null) + { + return; + } + processDictionary.computeIfAbsent(launch, k -> new ConcurrentHashMap<>()).put(procName, process); + } + + /** + * Records the OS pid when OpenOCD/GDB is spawned so cleanup can kill the process + * even if DSF services are already torn down. + */ + public void registerBackendProcess(ILaunch launch, String procName, Process process) + { + if (launch == null || process == null) + { + return; + } + backendPidDictionary.computeIfAbsent(launch, k -> new ConcurrentHashMap<>()).put(procName, process.pid()); + } + + /** + * Immediately kills OS processes for this launch (stream monitors, tracked pids, + * DSF backend processes). Does not shut down the DSF session. + */ + public void forceKillOsProcesses(ILaunch launch) + { + if (launch == null) + { + return; + } + + if (launch instanceof GdbLaunch) + { + cancelSessionJobs((GdbLaunch) launch); + } + + terminateTrackedProcesses(launch); + forceDestroyRegisteredPids(launch); + + if (launch instanceof GdbLaunch) + { + GdbConsoleCleanup.stopConsolesForLaunch(launch); + forceDestroyBackendProcessesOnExecutor((GdbLaunch) launch); + } + } + + /** + * Stops Eclipse stream monitors for tracked {@link IProcess} wrappers only. + */ + public void terminateProcessMonitors(ILaunch launch) + { + if (launch == null) + { + return; + } + + Map processMap = processDictionary.remove(launch); + if (processMap == null) + { + return; + } + + for (IProcess process : processMap.values()) + { + terminateProcessWrapper(process, false); + } + } + + /** + * Terminates OpenOCD/GDB processes tracked for this launch, force-kills any + * stuck OS processes, and shuts down the DSF session. Safe to call multiple times. + */ + public void killAllProcessesInLaunch(ILaunch launch) + { + forceTerminateLaunch(launch); + } + + /** + * Force cleanup when graceful DSF shutdown is stuck (e.g. OpenOCD reset loop). + * Kills stream monitors, OS processes and GDB consoles, then lets the framework + * end the DSF session and shut down its executor. + *

+ * This intentionally does not call {@code DsfSession.endSession} or + * {@code executor.shutdown()}. Those are owned by CDT + * ({@code GdbLaunch#launchRemoved} / {@code DsfTerminateCommand}); pre-terminating + * the executor here makes the framework's own teardown throw + * {@link RejectedExecutionException}. Force-killing the OS process and closing the + * MI pipes is what unblocks CDT's normal shutdown so it completes on its own. + */ + public void forceTerminateLaunch(ILaunch launch) + { + forceKillOsProcesses(launch); + } + + private void terminateTrackedProcesses(ILaunch launch) + { + Map processMap = processDictionary.remove(launch); + if (processMap == null) + { + return; + } + + for (IProcess process : processMap.values()) + { + terminateProcessWrapper(process, true); + } + } + + /** + * Stop {@link IProcess} stream monitors first so Eclipse "Input Stream Monitor" + * threads exit, then force-destroy the OS process if it is still alive. + */ + private void terminateProcessWrapper(IProcess process, boolean force) + { + if (process == null || process.isTerminated()) + { + return; + } + + if (process instanceof IdfRuntimeProcess) + { + ((IdfRuntimeProcess) process).forceTerminateWithoutWait(); + return; + } + + try + { + process.terminate(); + } + catch (Exception e) + { + Logger.log(e); + } + + if (force) + { + forceDestroySystemProcess(process.getAdapter(Process.class)); + } + } + + private void cancelSessionJobs(GdbLaunch launch) + { + DsfSession session = launch.getSession(); + if (session == null) + { + return; + } + + Job.getJobManager().cancel(session); + Job.getJobManager().cancel(session.getId()); + Job.getJobManager().cancel(launch); + } + + private void forceDestroyRegisteredPids(ILaunch launch) + { + Map pidMap = backendPidDictionary.remove(launch); + if (pidMap == null) + { + return; + } + + for (Long pid : pidMap.values()) + { + if (pid != null) + { + forceDestroyPid(pid.longValue()); + } + } + } + + private void forceDestroyBackendProcessesOnExecutor(GdbLaunch launch) { - if (!processDictionary.containsKey(launchName)) + DsfSession session = launch.getSession(); + if (session == null || session.getExecutor() == null) { - Map processMap = new HashMap<>(); - processMap.put(procName, process); - processDictionary.put(launchName, processMap); return; } - - Map processMap = processDictionary.get(launchName); - processMap.put(procName, process); - processDictionary.put(launchName, processMap); + + try + { + session.getExecutor().submit(() -> { + DsfServicesTracker tracker = new DsfServicesTracker(GdbPlugin.getBundleContext(), session.getId()); + try + { + forceDestroyBackendProcesses(tracker); + } + finally + { + tracker.dispose(); + } + }).get(BACKEND_QUERY_TIMEOUT_SECONDS, TimeUnit.SECONDS); + } + catch (TimeoutException e) + { + Logger.log(new Exception("Timed out querying DSF backend processes for termination", e)); //$NON-NLS-1$ + } + catch (InterruptedException e) + { + Thread.currentThread().interrupt(); + Logger.log(e); + } + catch (RejectedExecutionException e) + { + // Executor already terminating; registered pids were force-killed above. + } + catch (ExecutionException e) + { + Logger.log(e); + } } - - public IProcess getProcessFromDictionary(String launchName, String procName) + + private void forceDestroyBackendProcesses(DsfServicesTracker tracker) { - if (!processDictionary.containsKey(launchName)) + GdbServerBackend serverBackend = tracker.getService(GdbServerBackend.class); + if (serverBackend != null) { - return null; + forceDestroySystemProcess(serverBackend.getServerProcess()); + } + + IGDBBackend gdbBackend = tracker.getService(IGDBBackend.class); + if (gdbBackend != null) + { + forceDestroySystemProcess(gdbBackend.getProcess()); } - - return processDictionary.get(launchName).get(procName); } - - public void killAllProcessesInLaunch(String launchName) + + private void forceDestroySystemProcess(Process process) { - if(!processDictionary.containsKey(launchName)) + if (process == null || !process.isAlive()) { return; } - - for (IProcess process : processDictionary.get(launchName).values()) + + forceDestroyPid(process.pid()); + closeStreamSafely(process.getInputStream()); + closeStreamSafely(process.getOutputStream()); + closeStreamSafely(process.getErrorStream()); + } + + private void forceDestroyPid(long pid) + { + if (pid <= 0) + { + return; + } + + ProcessHandle handle = ProcessHandle.of(pid).orElse(null); + if (handle == null) + { + return; + } + + // Snapshot the whole process tree up front. Once a process dies its + // parent/child links are gone, and on Windows children are never killed + // together with the parent. An orphaned openocd would keep holding the + // JTAG/USB adapter and break the next debug session. + List tree = new ArrayList<>(); + handle.descendants().forEach(tree::add); + tree.add(handle); + + // Cross-platform termination via the JVM: destroy() maps to SIGTERM / + // TerminateProcess, destroyForcibly() to SIGKILL / TerminateProcess. + destroyTree(tree, false); + if (isAnyAlive(tree)) + { + destroyTree(tree, true); + } + + // Last resort: OS-native tree kill for anything ProcessHandle missed. + if (isAnyAlive(tree)) + { + killTreeViaOsCommand(pid); + } + + if (isAnyAlive(tree)) + { + Logger.log(new Exception("Debug process tree (root pid " + pid + ") still alive after forced termination")); //$NON-NLS-1$ //$NON-NLS-2$ + } + } + + private void destroyTree(List tree, boolean forcibly) + { + for (ProcessHandle ph : tree) + { + if (!ph.isAlive()) + { + continue; + } + if (forcibly) + { + ph.destroyForcibly(); + } + else + { + ph.destroy(); + } + } + + for (ProcessHandle ph : tree) { try { - process.terminate(); + ph.onExit().get(PROCESS_DESTROY_TIMEOUT_SECONDS, TimeUnit.SECONDS); } - catch (DebugException e) + catch (TimeoutException e) { - e.printStackTrace(); + // Escalation is handled by the caller (destroyForcibly / OS fallback). + } + catch (InterruptedException e) + { + Thread.currentThread().interrupt(); + return; + } + catch (ExecutionException e) + { + Logger.log(e); } } } + private boolean isAnyAlive(List tree) + { + for (ProcessHandle ph : tree) + { + if (ph.isAlive()) + { + return true; + } + } + return false; + } + + private void killTreeViaOsCommand(long pid) + { + String[] command; + if (Platform.OS_WIN32.equals(Platform.getOS())) + { + // /T terminates the process tree, /F forces it. + command = new String[] { "taskkill", "/F", "/T", "/PID", Long.toString(pid) }; //$NON-NLS-1$ //$NON-NLS-2$ //$NON-NLS-3$ //$NON-NLS-4$ + } + else + { + command = new String[] { "kill", "-9", Long.toString(pid) }; //$NON-NLS-1$ //$NON-NLS-2$ + } + + try + { + Process killProcess = Runtime.getRuntime().exec(command); + killProcess.waitFor(2, TimeUnit.SECONDS); + } + catch (Exception e) + { + Logger.log(e); + } + } + + private void closeStreamSafely(Object stream) + { + if (stream == null) + { + return; + } + try + { + if (stream instanceof InputStream) + { + ((InputStream) stream).close(); + } + else if (stream instanceof OutputStream) + { + ((OutputStream) stream).close(); + } + } + catch (Exception e) + { + Logger.log(e); + } + } } diff --git a/bundles/com.espressif.idf.debug.gdbjtag.openocd/src/com/espressif/idf/debug/gdbjtag/openocd/dsf/process/IdfRuntimeProcess.java b/bundles/com.espressif.idf.debug.gdbjtag.openocd/src/com/espressif/idf/debug/gdbjtag/openocd/dsf/process/IdfRuntimeProcess.java index 9152dd45c..d189fbb21 100644 --- a/bundles/com.espressif.idf.debug.gdbjtag.openocd/src/com/espressif/idf/debug/gdbjtag/openocd/dsf/process/IdfRuntimeProcess.java +++ b/bundles/com.espressif.idf.debug.gdbjtag.openocd/src/com/espressif/idf/debug/gdbjtag/openocd/dsf/process/IdfRuntimeProcess.java @@ -10,6 +10,7 @@ import java.util.Map; import org.eclipse.cdt.dsf.gdb.launching.GDBProcess; +import org.eclipse.cdt.dsf.mi.service.command.AbstractCLIProcess; import org.eclipse.core.runtime.CoreException; import org.eclipse.debug.core.DebugException; import org.eclipse.debug.core.DebugPlugin; @@ -75,8 +76,56 @@ protected IStreamsProxy createStreamsProxy() @Override public void terminate() throws DebugException { - super.terminate(); - streamsProxy.kill(); + forceTerminateWithoutWait(); + } + + /** + * Stops monitors and marks the process terminated without blocking on + * {@link org.eclipse.cdt.dsf.mi.service.command.MIBackendCLIProcess#waitFor()}. + */ + public void forceTerminateWithoutWait() + { + if (isTerminated()) + { + return; + } + + killStreamMonitors(); + + Process process = getSystemProcess(); + if (process instanceof AbstractCLIProcess cliProcess) + { + // Closes the GDB MI pipes so the "GDB CLI ... output Job" reader threads + // stop, even while gdb/openocd are stuck (e.g. a core-reset loop). Covers + // both MIBackendCLIProcess and GDBBackendCLIProcess. + cliProcess.destroy(); + cliProcess.dispose(); + } + else if (process != null) + { + process.destroy(); + if (process.isAlive()) + { + process.destroyForcibly(); + } + } + + // Do not call terminated() here. Destroying/disposing the system process + // unblocks RuntimeProcess.ProcessMonitorThread, which marks this IProcess + // terminated exactly once. Calling terminated() ourselves races with that + // thread and triggers NullPointerException in RuntimeProcess.terminated(). + } + + /** + * Stops console stream monitors without waiting for the OS process to exit. + * Required when OpenOCD/GDB are stuck (e.g. during target reset loops). + */ + public void killStreamMonitors() + { + if (streamsProxy != null) + { + streamsProxy.kill(); + } } private T getAttributeSafe(AttributeGetter getter, String attribute, T defaultValue) diff --git a/bundles/com.espressif.idf.debug.gdbjtag.openocd/src/com/espressif/idf/debug/gdbjtag/openocd/ui/AppLvlTracingHandler.java b/bundles/com.espressif.idf.debug.gdbjtag.openocd/src/com/espressif/idf/debug/gdbjtag/openocd/ui/AppLvlTracingHandler.java index 30d616757..1900c6f80 100644 --- a/bundles/com.espressif.idf.debug.gdbjtag.openocd/src/com/espressif/idf/debug/gdbjtag/openocd/ui/AppLvlTracingHandler.java +++ b/bundles/com.espressif.idf.debug.gdbjtag.openocd/src/com/espressif/idf/debug/gdbjtag/openocd/ui/AppLvlTracingHandler.java @@ -104,9 +104,7 @@ private void launchOpenocdFromLaunchConfiguration(LaunchConfiguration config) th if (!DebugUtils.isLaunchConfigurationStarted(config)) { LaunchConfigurationDelegate debugDelegate = (LaunchConfigurationDelegate) config .getPreferredLaunchDelegate(ICDTLaunchConfigurationConstants.DEBUGGER_MODE_RUN); - debugDelegate.ignoreGdbClient(); - config.launch(ICDTLaunchConfigurationConstants.DEBUGGER_MODE_RUN, null, false); - debugDelegate.doNotIngoreGdbClient(); + debugDelegate.runOpenOcdOnlyLaunch(config, ICDTLaunchConfigurationConstants.DEBUGGER_MODE_RUN, null); } } };