Skip to content

WIP: IEP-1771 Deadlock when terminating a stuck OpenOCD/GDB launch sequence#1466

Open
sigmaaa wants to merge 2 commits into
masterfrom
IEP-1771
Open

WIP: IEP-1771 Deadlock when terminating a stuck OpenOCD/GDB launch sequence#1466
sigmaaa wants to merge 2 commits into
masterfrom
IEP-1771

Conversation

@sigmaaa
Copy link
Copy Markdown
Collaborator

@sigmaaa sigmaaa commented May 20, 2026

Description

Please include a summary of the change and which issue is fixed.

Fixes # (IEP-XXX)

Type of change

Please delete options that are not relevant.

  • Bug fix (non-breaking change which fixes an issue)
  • New feature (non-breaking change which adds functionality)
  • Breaking change (fix or feature that would cause existing functionality to not work as expected)
  • This change requires a documentation update

How has this been tested?

Please describe the tests that you ran to verify your changes. Provide instructions so we can reproduce. Please also list any relevant details for your test configuration

  • Test A
  • Test B

Test Configuration:

  • ESP-IDF Version:
  • OS (Windows,Linux and macOS):

Dependent components impacted by this PR:

  • Component 1
  • Component 2

Checklist

  • PR Self Reviewed
  • Applied Code formatting
  • Added Documentation
  • Added Unit Test
  • Verified on all platforms - Windows,Linux and macOS

Summary by CodeRabbit

  • Chores

    • Updated minimum Java version requirement to Java 21.
  • Bug Fixes

    • Improved process termination and cleanup robustness for debug sessions.
    • Enhanced thread safety in process management.
  • New Features

    • Added graceful launch cancellation support with improved responsiveness to user cancellation requests.

Review Change Stack

@sigmaaa sigmaaa self-assigned this May 20, 2026
@coderabbitai
Copy link
Copy Markdown

coderabbitai Bot commented May 20, 2026

📝 Walkthrough

Walkthrough

This PR upgrades the Espressif IDF Eclipse Plugin GDB/OpenOCD debug bundle to Java 21 and refactors its launch lifecycle management with improved process tracking, concurrent data structures, and virtual-thread-based cancellation handling.

Changes

Launch Lifecycle and Process Management Upgrade

Layer / File(s) Summary
Java 21 version upgrade configuration
bundles/com.espressif.idf.debug.gdbjtag.openocd/.classpath, .settings/org.eclipse.jdt.core.prefs, META-INF/MANIFEST.MF
Eclipse project configuration is updated to target, compile, and require Java 21 instead of Java 17 across build settings, JDT compiler options, and bundle manifest.
Launch process management and termination refactoring
src/com/espressif/idf/debug/gdbjtag/openocd/dsf/Launch.java, LaunchProcessDictionary.java
Launch.java is refactored to improve process lifecycle: terminate() kills tracked processes before calling superclass termination and wraps the flow in exception logging, getProcesses() uses modern var declarations and method references for array construction, and cleanUpOldLaunchProcesses() simplifies conditionals while preserving semantics. LaunchProcessDictionary changes from non-thread-safe lazy HashMap to eager ConcurrentHashMap singleton with atomic computeIfAbsent insertion and force-aware async shutdown.
Launch configuration delegate lifecycle and GDB version handling
src/com/espressif/idf/debug/gdbjtag/openocd/dsf/LaunchConfigurationDelegate.java
GDB version retrieval is modernized to execute the GDB command with a fixed 10-second timeout, read full UTF-8 stdout, and forcibly destroy the process on timeout/error. The launch() method now runs with a virtual-thread cancel watcher that terminates the launch when progress cancellation occurs. A new cleanupLaunch() override synchronously kills processes, then asynchronously invokes superclass cleanup on a virtual thread, and removes the launch from the manager if needed.

Estimated code review effort

🎯 3 (Moderate) | ⏱️ ~25 minutes

Possibly related PRs

  • espressif/idf-eclipse-plugin#1023: Directly overlaps with this PR's refactor of the gdb/openocd launch lifecycle in Launch.java, LaunchConfigurationDelegate.cleanupLaunch(...), and LaunchProcessDictionary shutdown mechanics to prevent debug hangs.

Suggested reviewers

  • AndriiFilippov
  • kolipakakondal
  • alirana01

Poem

🐰 From Java's seventeen, we leap to twenty-one,
With concurrent maps and watchers swift as sun,
Process cleanup flows through virtual threads so light,
GDB shakes hands faster—Eclipse shines bright!
Espressif's debugger now runs true and strong! ✨

🚥 Pre-merge checks | ✅ 4 | ❌ 1

❌ Failed checks (1 warning)

Check name Status Explanation Resolution
Docstring Coverage ⚠️ Warning Docstring coverage is 10.00% which is insufficient. The required threshold is 80.00%. Write docstrings for the functions missing them to satisfy the coverage threshold.
✅ Passed checks (4 passed)
Check name Status Explanation
Description Check ✅ Passed Check skipped - CodeRabbit’s high-level summary is enabled.
Title check ✅ Passed The title accurately reflects the main objective of the PR—fixing a deadlock issue when terminating a stuck OpenOCD/GDB launch sequence (IEP-1771), which is directly supported by the code changes across multiple files.
Linked Issues check ✅ Passed Check skipped because no linked issues were found for this pull request.
Out of Scope Changes check ✅ Passed Check skipped because no linked issues were found for this pull request.

✏️ Tip: You can configure your own custom pre-merge checks in the settings.

✨ Finishing Touches
📝 Generate docstrings
  • Create stacked PR
  • Commit on current branch
🧪 Generate unit tests (beta)
  • Create PR with unit tests
  • Commit unit tests in branch IEP-1771

Thanks for using CodeRabbit! It's free for OSS, and your support helps us grow. If you like it, consider giving us a shout-out.

❤️ Share

Comment @coderabbitai help to get the list of available commands and usage tips.

Copy link
Copy Markdown

@coderabbitai coderabbitai Bot left a comment

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Actionable comments posted: 4

🤖 Prompt for all review comments with AI agents
Verify each finding against current code. Fix only still-valid issues, skip the
rest with a brief reason, keep changes minimal, and validate.

Inline comments:
In `@bundles/com.espressif.idf.debug.gdbjtag.openocd/META-INF/MANIFEST.MF`:
- Line 27: The manifest currently sets Bundle-RequiredExecutionEnvironment to
JavaSE-21 which violates repo policy; update the MANIFEST.MF entry
Bundle-RequiredExecutionEnvironment back to JavaSE-17 and, if this bundle
defines plugin.xml extensions, ensure the manifest contains a
Bundle-SymbolicName entry with the suffix ;singleton:=true (add or adjust the
Bundle-SymbolicName key accordingly) so the bundle meets the repository's
execution-environment and singleton requirements.

In
`@bundles/com.espressif.idf.debug.gdbjtag.openocd/src/com/espressif/idf/debug/gdbjtag/openocd/dsf/Launch.java`:
- Around line 268-278: In Launch.terminate() do not swallow failures: inside the
try/catch around the isTerminated() check and super.terminate(), after logging
the caught Exception via Logger.log(e) rethrow it as a DebugException if it
already is one (or throw a new DebugException wrapping the original Exception)
so callers see termination failures; ensure the catch distinguishes
DebugException vs other Exceptions and rethrows appropriately after logging
instead of silently returning.

In
`@bundles/com.espressif.idf.debug.gdbjtag.openocd/src/com/espressif/idf/debug/gdbjtag/openocd/dsf/LaunchConfigurationDelegate.java`:
- Around line 169-170: The throw in LaunchConfigurationDelegate currently wraps
e.getCause() which can be null and loses context; change the DebugException
construction to pass the original IOException (the caught variable, likely named
e) as the nested throwable instead of e.getCause(), i.e. when throwing new
DebugException(new Status(..., "Error launching command", e.getCause())),
replace e.getCause() with e so the original exception and stack trace are
preserved.
- Around line 226-243: The code unconditionally calls
mainLaunchThread.interrupt() after the wait loop, causing spurious interrupts on
normal termination; change the logic so that mainLaunchThread.interrupt() is
only invoked when the wait ended due to cancellation (finalMonitor.isCanceled())
or when you explicitly forced termination—i.e., wrap the launch.terminate() and
mainLaunchThread.interrupt() calls in the cancellation/forced-termination branch
and do not interrupt when launch.isTerminated() returned true normally; update
the block that uses launch.canTerminate(), launch.terminate(), and
mainLaunchThread.interrupt() so the interrupt is only executed on
cancellation/forced termination and keep the existing exception handling.
🪄 Autofix (Beta)

Fix all unresolved CodeRabbit comments on this PR:

  • Push a commit to this branch (recommended)
  • Create a new PR with the fixes

ℹ️ Review info
⚙️ Run configuration

Configuration used: defaults

Review profile: CHILL

Plan: Pro

Run ID: c1ebf45c-79f8-4f14-9b83-b74a40ca4a6a

📥 Commits

Reviewing files that changed from the base of the PR and between 4c1aac3 and 8fad857.

📒 Files selected for processing (6)
  • bundles/com.espressif.idf.debug.gdbjtag.openocd/.classpath
  • bundles/com.espressif.idf.debug.gdbjtag.openocd/.settings/org.eclipse.jdt.core.prefs
  • bundles/com.espressif.idf.debug.gdbjtag.openocd/META-INF/MANIFEST.MF
  • bundles/com.espressif.idf.debug.gdbjtag.openocd/src/com/espressif/idf/debug/gdbjtag/openocd/dsf/Launch.java
  • bundles/com.espressif.idf.debug.gdbjtag.openocd/src/com/espressif/idf/debug/gdbjtag/openocd/dsf/LaunchConfigurationDelegate.java
  • bundles/com.espressif.idf.debug.gdbjtag.openocd/src/com/espressif/idf/debug/gdbjtag/openocd/dsf/LaunchProcessDictionary.java

org.eclipse.embedcdt.ui,
org.eclipse.ui.console
Bundle-RequiredExecutionEnvironment: JavaSE-17
Bundle-RequiredExecutionEnvironment: JavaSE-21
Copy link
Copy Markdown

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

⚠️ Potential issue | 🟠 Major | ⚡ Quick win

Keep required execution environment at JavaSE-17 per repo policy.

Changing to JavaSE-21 breaks the bundle-level execution-environment requirement defined for this repo and can invalidate target-platform compatibility checks.

As per coding guidelines, "Bundle MANIFEST.MF must set Bundle-SymbolicName with singleton:=true if it has plugin.xml extensions and Bundle-RequiredExecutionEnvironment: JavaSE-17".

🤖 Prompt for AI Agents
Verify each finding against current code. Fix only still-valid issues, skip the
rest with a brief reason, keep changes minimal, and validate.

In `@bundles/com.espressif.idf.debug.gdbjtag.openocd/META-INF/MANIFEST.MF` at line
27, The manifest currently sets Bundle-RequiredExecutionEnvironment to JavaSE-21
which violates repo policy; update the MANIFEST.MF entry
Bundle-RequiredExecutionEnvironment back to JavaSE-17 and, if this bundle
defines plugin.xml extensions, ensure the manifest contains a
Bundle-SymbolicName entry with the suffix ;singleton:=true (add or adjust the
Bundle-SymbolicName key accordingly) so the bundle meets the repository's
execution-environment and singleton requirements.

Comment on lines +268 to +278
try
{
if (!this.isTerminated())
{
super.terminate();
}
}
catch (Exception e)
{
Logger.log(e);
}
Copy link
Copy Markdown

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

⚠️ Potential issue | 🟠 Major | ⚡ Quick win

Do not swallow termination failures in terminate().

Catching broad Exception and only logging makes termination look successful even when it fails, which can leave the launch lifecycle inconsistent. Re-throw DebugException (or wrap non-debug exceptions) after logging.

Suggested fix
 		try
 		{
 			if (!this.isTerminated())
 			{
 				super.terminate();
 			}
 		}
-		catch (Exception e)
+		catch (DebugException e)
+		{
+			Logger.log(e);
+			throw e;
+		}
+		catch (Exception e)
 		{
 			Logger.log(e);
+			throw new DebugException(new Status(IStatus.ERROR, Activator.PLUGIN_ID,
+					IDsfStatusConstants.INTERNAL_ERROR, "Error terminating launch", e));
 		}
📝 Committable suggestion

‼️ IMPORTANT
Carefully review the code before committing. Ensure that it accurately replaces the highlighted code, contains no missing lines, and has no issues with indentation. Thoroughly test & benchmark the code to ensure it meets the requirements.

Suggested change
try
{
if (!this.isTerminated())
{
super.terminate();
}
}
catch (Exception e)
{
Logger.log(e);
}
try
{
if (!this.isTerminated())
{
super.terminate();
}
}
catch (DebugException e)
{
Logger.log(e);
throw e;
}
catch (Exception e)
{
Logger.log(e);
throw new DebugException(new Status(IStatus.ERROR, Activator.PLUGIN_ID,
IDsfStatusConstants.INTERNAL_ERROR, "Error terminating launch", e));
}
🤖 Prompt for AI Agents
Verify each finding against current code. Fix only still-valid issues, skip the
rest with a brief reason, keep changes minimal, and validate.

In
`@bundles/com.espressif.idf.debug.gdbjtag.openocd/src/com/espressif/idf/debug/gdbjtag/openocd/dsf/Launch.java`
around lines 268 - 278, In Launch.terminate() do not swallow failures: inside
the try/catch around the isTerminated() check and super.terminate(), after
logging the caught Exception via Logger.log(e) rethrow it as a DebugException if
it already is one (or throw a new DebugException wrapping the original
Exception) so callers see termination failures; ensure the catch distinguishes
DebugException vs other Exceptions and rethrows appropriately after logging
instead of silently returning.

Comment on lines 169 to +170
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 launching command", e.getCause()));
Copy link
Copy Markdown

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

⚠️ Potential issue | 🟠 Major | ⚡ Quick win

Wrap the original IOException, not e.getCause().

Using e.getCause() here often passes null and drops the real failure context.

Suggested fix
 		catch (IOException e)
 		{
 			throw new DebugException(new Status(IStatus.ERROR, Activator.PLUGIN_ID, DebugException.REQUEST_FAILED,
-					"Error launching command", e.getCause()));
+					"Error launching command", e));
 		}
📝 Committable suggestion

‼️ IMPORTANT
Carefully review the code before committing. Ensure that it accurately replaces the highlighted code, contains no missing lines, and has no issues with indentation. Thoroughly test & benchmark the code to ensure it meets the requirements.

Suggested change
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 launching command", e.getCause()));
catch (IOException e)
{
throw new DebugException(new Status(IStatus.ERROR, Activator.PLUGIN_ID, DebugException.REQUEST_FAILED,
"Error launching command", e));
}
🤖 Prompt for AI Agents
Verify each finding against current code. Fix only still-valid issues, skip the
rest with a brief reason, keep changes minimal, and validate.

In
`@bundles/com.espressif.idf.debug.gdbjtag.openocd/src/com/espressif/idf/debug/gdbjtag/openocd/dsf/LaunchConfigurationDelegate.java`
around lines 169 - 170, The throw in LaunchConfigurationDelegate currently wraps
e.getCause() which can be null and loses context; change the DebugException
construction to pass the original IOException (the caught variable, likely named
e) as the nested throwable instead of e.getCause(), i.e. when throwing new
DebugException(new Status(..., "Error launching command", e.getCause())),
replace e.getCause() with e so the original exception and stack trace are
preserved.

Comment on lines +226 to +243
while (!finalMonitor.isCanceled() && !launch.isTerminated())
{
Thread.sleep(200);
}

if (!launch.isTerminated() && launch.canTerminate())
{
try
{
launch.terminate();
}
catch (Exception ignore)
{
}
}

mainLaunchThread.interrupt();
}
Copy link
Copy Markdown

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

⚠️ Potential issue | 🟠 Major | ⚡ Quick win

Cancel watcher interrupts the launch thread even on normal termination.

The loop exits when launch.isTerminated() becomes true, but then still interrupts mainLaunchThread. That can inject spurious interrupts into normal launch flow.

Suggested fix
-				while (!finalMonitor.isCanceled() && !launch.isTerminated())
+				while (!finalMonitor.isCanceled() && !launch.isTerminated())
 				{
 					Thread.sleep(200);
 				}
 
-				if (!launch.isTerminated() && launch.canTerminate())
+				if (finalMonitor.isCanceled() && !launch.isTerminated() && launch.canTerminate())
 				{
 					try
 					{
 						launch.terminate();
 					}
 					catch (Exception ignore)
 					{
 					}
-				}
-
-				mainLaunchThread.interrupt();
+					mainLaunchThread.interrupt();
+				}
📝 Committable suggestion

‼️ IMPORTANT
Carefully review the code before committing. Ensure that it accurately replaces the highlighted code, contains no missing lines, and has no issues with indentation. Thoroughly test & benchmark the code to ensure it meets the requirements.

Suggested change
while (!finalMonitor.isCanceled() && !launch.isTerminated())
{
Thread.sleep(200);
}
if (!launch.isTerminated() && launch.canTerminate())
{
try
{
launch.terminate();
}
catch (Exception ignore)
{
}
}
mainLaunchThread.interrupt();
}
while (!finalMonitor.isCanceled() && !launch.isTerminated())
{
Thread.sleep(200);
}
if (finalMonitor.isCanceled() && !launch.isTerminated() && launch.canTerminate())
{
try
{
launch.terminate();
}
catch (Exception ignore)
{
}
mainLaunchThread.interrupt();
}
🤖 Prompt for AI Agents
Verify each finding against current code. Fix only still-valid issues, skip the
rest with a brief reason, keep changes minimal, and validate.

In
`@bundles/com.espressif.idf.debug.gdbjtag.openocd/src/com/espressif/idf/debug/gdbjtag/openocd/dsf/LaunchConfigurationDelegate.java`
around lines 226 - 243, The code unconditionally calls
mainLaunchThread.interrupt() after the wait loop, causing spurious interrupts on
normal termination; change the logic so that mainLaunchThread.interrupt() is
only invoked when the wait ended due to cancellation (finalMonitor.isCanceled())
or when you explicitly forced termination—i.e., wrap the launch.terminate() and
mainLaunchThread.interrupt() calls in the cancellation/forced-termination branch
and do not interrupt when launch.isTerminated() returned true normally; update
the block that uses launch.canTerminate(), launch.terminate(), and
mainLaunchThread.interrupt() so the interrupt is only executed on
cancellation/forced termination and keep the existing exception handling.

Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Labels

None yet

Projects

None yet

Development

Successfully merging this pull request may close these issues.

1 participant