Skip to content
New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

IEP-1265: Debug Process Hangs #1023

Merged
merged 16 commits into from
Nov 27, 2024
Merged

IEP-1265: Debug Process Hangs #1023

merged 16 commits into from
Nov 27, 2024

Conversation

alirana01
Copy link
Collaborator

@alirana01 alirana01 commented Jul 22, 2024

Description

The port handling logic was flawed added proper code to verify and assign ports with a logic for retries as well.
Also changed the debugger tab so that the gdb client controls are only enabled when the openOCD server is not started from the IDE.

Fixes # (IEP-1265)

Type of change

Please delete options that are not relevant.

  • Bug fix (non-breaking change which fixes an issue)

How has this been tested?

Try debugging on mac os and verify that openocd is able to connect. Also start openocd only from cli and try to connect debugger

Test Configuration:

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

Checklist

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

Summary by CodeRabbit

Summary by CodeRabbit

  • Bug Fixes

    • Enhanced port availability checking for improved reliability and logging.
    • Adjusted how the launch configuration handles port assignments, ensuring ports are only set when necessary.
    • Improved resource cleanup during launch cancellation or failure.
  • New Features

    • Introduced a new user interface component for GDB client controls, improving organization and user experience.
    • Updated logic for GDB server configuration to simplify port retrieval, enhancing usability.
    • Improved interaction logic between the GDB server and client configurations for better user experience.
    • Added functionality to disable GDB client controls when the server is running, preventing user errors.
    • Enhanced GDB backend functionality with improved error handling and process management.
    • Introduced a singleton class for managing processes associated with different launch names, streamlining process management.
    • Enhanced process management capabilities in the launch process, including initialization and termination of processes.

@alirana01 alirana01 self-assigned this Jul 22, 2024
Copy link

coderabbitai bot commented Jul 22, 2024

Walkthrough

The recent modifications involve enhancing the PortChecker class in PortChecker.java to improve port availability checking by using a ServerSocket. The Configuration class in Configuration.java has been updated to simplify the retrieval of GDB server port numbers by using default values instead of launch configuration attributes. The Launch class in Launch.java now conditionally sets the GDB server port based on configuration, and the TabDebugger class in TabDebugger.java has been organized with a new gdbClientGroup to manage GDB client controls more effectively. Additionally, a new cleanup method has been introduced in LaunchConfigurationDelegate.java to handle resource cleanup during launch failures. The GdbBackend class has undergone significant structural changes to improve error handling and asynchronous processing. A new LaunchProcessDictionary class has been added to manage processes associated with different launch names.

Changes

File Path Change Summary
bundles/com.espressif.idf.core/src/com/espressif/idf/core/util/PortChecker.java Enhanced isPortAvailable method to check port availability using a ServerSocket.
bundles/com.espressif.idf.debug.gdbjtag.openocd/src/com/espressif/idf/debug/gdbjtag/openocd/Configuration.java Modified the getGdbServerCommandLineArray method to simplify port retrieval using default values.
bundles/com.espressif.idf.debug.gdbjtag.openocd/src/com/espressif/idf/debug/gdbjtag/openocd/dsf/Launch.java Added a conditional check to set the GDB server port based on configuration instead of dynamic allocation.
bundles/com.espressif.idf.debug.gdbjtag.openocd/src/com/espressif/idf/debug/gdbjtag/openocd/ui/TabDebugger.java Introduced a new gdbClientGroup for better organization of GDB client controls in the UI.
bundles/com.espressif.idf.debug.gdbjtag.openocd/src/com/espressif/idf/debug/gdbjtag/openocd/dsf/LaunchConfigurationDelegate.java Added a new method cleanupLaunch for resource cleanup during launch cancellation or failure.
bundles/com.espressif.idf.debug.gdbjtag.openocd/src/com/espressif/idf/debug/gdbjtag/openocd/dsf/GdbBackend.java Overhauled the GdbBackend class to implement new interfaces and improve error handling and processing sequences.
bundles/com.espressif.idf.debug.gdbjtag.openocd/src/com/espressif/idf/debug/gdbjtag/openocd/dsf/process/IdfRuntimeProcess.java Modified createStreamsProxy method to include conditional checks for GDB process labels.
bundles/com.espressif.idf.debug.gdbjtag.openocd/src/com/espressif/idf/debug/gdbjtag/openocd/dsf/LaunchProcessDictionary.java Introduced a singleton class for managing processes associated with launch names.

Possibly related PRs

  • fix: Address issue with the cdt-lsp-2.0.0 #1021: The changes in the Launch class and the PortChecker class are related through the use of port management, as the Launch class previously utilized PortChecker.getAvailablePort() for determining port availability, which is now streamlined in the main PR's isPortAvailable(int port) method.

Poem

🐇 In the land of code where rabbits play,
Changes hop in, brightening the day.
With extensions silenced, a new path is spun,
Debugging adventures just waiting for fun!
With a twitch of my nose, I’ll bound through the flow,
Celebrating adjustments, let the processes glow! ✨


Thank you for using CodeRabbit. We offer it for free to the OSS community and would appreciate your support in helping us grow. If you find it useful, would you consider giving us a shout-out on your favorite social media?

❤️ Share
🪧 Tips

Chat

There are 3 ways to chat with CodeRabbit:

  • Review comments: Directly reply to a review comment made by CodeRabbit. Example:
    • I pushed a fix in commit <commit_id>, please review it.
    • Generate unit testing code for this file.
    • Open a follow-up GitHub issue for this discussion.
  • Files and specific lines of code (under the "Files changed" tab): Tag @coderabbitai in a new review comment at the desired location with your query. Examples:
    • @coderabbitai generate unit testing code for this file.
    • @coderabbitai modularize this function.
  • PR comments: Tag @coderabbitai in a new PR comment to ask questions about the PR branch. For the best results, please provide a very specific query, as very limited context is provided in this mode. Examples:
    • @coderabbitai gather interesting stats about this repository and render them as a table. Additionally, render a pie chart showing the language distribution in the codebase.
    • @coderabbitai read src/utils.ts and generate unit testing code.
    • @coderabbitai read the files in the src/scheduler package and generate a class diagram using mermaid and a README in the markdown format.
    • @coderabbitai help me debug CodeRabbit configuration file.

Note: Be mindful of the bot's finite context window. It's strongly recommended to break down tasks such as reading entire modules into smaller chunks. For a focused discussion, use review comments to chat about specific files and their changes, instead of using the PR comments.

CodeRabbit Commands (Invoked using PR comments)

  • @coderabbitai pause to pause the reviews on a PR.
  • @coderabbitai resume to resume the paused reviews.
  • @coderabbitai review to trigger an incremental review. This is useful when automatic reviews are disabled for the repository.
  • @coderabbitai full review to do a full review from scratch and review all the files again.
  • @coderabbitai summary to regenerate the summary of the PR.
  • @coderabbitai resolve resolve all the CodeRabbit review comments.
  • @coderabbitai configuration to show the current CodeRabbit configuration for the repository.
  • @coderabbitai help to get help.

Other keywords and placeholders

  • Add @coderabbitai ignore anywhere in the PR description to prevent this PR from being reviewed.
  • Add @coderabbitai summary to generate the high-level summary at a specific location in the PR description.
  • Add @coderabbitai anywhere in the PR title to generate the title automatically.

CodeRabbit Configuration File (.coderabbit.yaml)

  • You can programmatically configure CodeRabbit by adding a .coderabbit.yaml file to the root of your repository.
  • Please see the configuration documentation for more information.
  • If your editor has YAML language server enabled, you can add the path at the top of this file to enable auto-completion and validation: # yaml-language-server: $schema=https://coderabbit.ai/integrations/schema.v2.json

Documentation and Community

  • Visit our Documentation for detailed information on how to use CodeRabbit.
  • Join our Discord Community to get help, request features, and share feedback.
  • Follow us on X/Twitter for updates and announcements.

Copy link

@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: 0

Outside diff range, codebase verification and nitpick comments (1)
bundles/com.espressif.idf.debug.gdbjtag.openocd/plugin.xml (1)

Verify the impact of disabling process and console factories.

The <extension> block related to processFactory and consoleFactory has been commented out, which temporarily disables these debugging functionalities. The search results indicate that CustomIdfProcessFactory and IdfProcessConsoleFactory are referenced in several Java files, suggesting dependencies on these classes. Commenting out these extensions could potentially break functionality related to process creation and console management in the debugging environment.

  • Files with references:
    • 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/process/CustomIdfProcessFactory.java
    • bundles/com.espressif.idf.debug.gdbjtag.openocd/src/com/espressif/idf/debug/gdbjtag/openocd/dsf/console/IdfProcessConsoleFactory.java
    • bundles/com.espressif.idf.debug.gdbjtag.openocd/src/com/espressif/idf/debug/gdbjtag/openocd/dsf/process/StreamListener.java

Further verification or testing is recommended to ensure that the debugging capabilities are not adversely affected by these changes.

Analysis chain

Line range hint 155-168:
Verify the impact of disabling process and console factories.

The <extension> block related to processFactory and consoleFactory has been commented out, which temporarily disables these debugging functionalities. Ensure that this change does not adversely affect the debugging capabilities required for your development workflow.

Scripts executed

The following scripts were executed for the analysis:

Script:

#!/bin/bash
# Description: Verify the impact of commenting out the process and console factories.

# Test: Search for the usage of `CustomIdfProcessFactory` and `IdfProcessConsoleFactory`. Expect: No critical dependencies.
rg --type xml -A 5 $'CustomIdfProcessFactory'
rg --type xml -A 5 $'IdfProcessConsoleFactory'

Length of output: 1306


Script:

#!/bin/bash
# Description: Verify the impact of commenting out the process and console factories in Java files.

# Test: Search for the usage of `CustomIdfProcessFactory` and `IdfProcessConsoleFactory` in Java files. Expect: No critical dependencies.
rg --type java -A 5 $'CustomIdfProcessFactory'
rg --type java -A 5 $'IdfProcessConsoleFactory'

Length of output: 6195

Review details

Configuration used: CodeRabbit UI
Review profile: CHILL

Commits

Files that changed from the base of the PR and between e97546f and c5803ff.

Files selected for processing (2)
  • bundles/com.espressif.idf.debug.gdbjtag.openocd/plugin.xml (2 hunks)
  • bundles/com.espressif.idf.debug.gdbjtag.openocd/src/com/espressif/idf/debug/gdbjtag/openocd/dsf/Launch.java (1 hunks)
Files skipped from review due to trivial changes (1)
  • bundles/com.espressif.idf.debug.gdbjtag.openocd/src/com/espressif/idf/debug/gdbjtag/openocd/dsf/Launch.java

@kolipakakondal
Copy link
Collaborator

@alirana01 Having the same issue with the PR as well.
Screenshot 2024-07-26 at 5 03 20 PM

stack.log

Copy link

@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: 0

Review details

Configuration used: CodeRabbit UI
Review profile: CHILL

Commits

Files that changed from the base of the PR and between c5803ff and 560de09.

Files selected for processing (2)
  • bundles/com.espressif.idf.debug.gdbjtag.openocd/plugin.xml (2 hunks)
  • bundles/com.espressif.idf.debug.gdbjtag.openocd/src/com/espressif/idf/debug/gdbjtag/openocd/dsf/Launch.java (1 hunks)
Files skipped from review due to trivial changes (1)
  • bundles/com.espressif.idf.debug.gdbjtag.openocd/plugin.xml
Additional comments not posted (1)
bundles/com.espressif.idf.debug.gdbjtag.openocd/src/com/espressif/idf/debug/gdbjtag/openocd/dsf/Launch.java (1)

154-154: Potential Impact of Commenting Out Process Factory ID Setting

Commenting out the line that sets the DebugPlugin.ATTR_PROCESS_FACTORY_ID could significantly alter how processes are managed during debugging. This change is likely an attempt to troubleshoot the hanging issue, but it's crucial to verify that this does not introduce other issues in the debugging process.

Please ensure to test this change across all supported platforms to confirm that it resolves the hanging issue without adverse effects. Consider re-enabling this line conditionally based on further findings or implementing a more targeted fix if this proves to be too broad.

Copy link

@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: 1

Review details

Configuration used: CodeRabbit UI
Review profile: CHILL

Commits

Files that changed from the base of the PR and between 560de09 and 5d4bfad.

Files selected for processing (2)
  • bundles/com.espressif.idf.core/src/com/espressif/idf/core/util/PortChecker.java (2 hunks)
  • bundles/com.espressif.idf.debug.gdbjtag.openocd/src/com/espressif/idf/debug/gdbjtag/openocd/dsf/Launch.java (1 hunks)
Additional comments not posted (2)
bundles/com.espressif.idf.core/src/com/espressif/idf/core/util/PortChecker.java (1)

28-66: The retry mechanism, logging, and exception handling are well-implemented.

The retry mechanism is a good addition to handle transient issues that may cause temporary port unavailability. The maximum number of retries (3) and the delay between retries (200ms) seem reasonable. The method correctly returns false if the port is still not available after exhausting all retries.

The logging statements provide useful information for debugging and monitoring purposes. The log messages are clear and informative.

The method handles InterruptedException during the sleep period by restoring the interrupt status and logging an appropriate message. This ensures that the method behaves correctly when interrupted and doesn't silently swallow the exception.

bundles/com.espressif.idf.debug.gdbjtag.openocd/src/com/espressif/idf/debug/gdbjtag/openocd/dsf/Launch.java (1)

150-155: LGTM!

The conditional check is a good addition to ensure that the port number is only set when the GDB server is intended to be started. This change prevents unnecessary port assignments and enhances the control flow.

}
return true; //Fallback not reachable
Copy link

Choose a reason for hiding this comment

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

Remove the unreachable fallback return statement.

The fallback return statement return true; at line 66 is unreachable. This statement serves no purpose and can be removed.

Apply this diff to remove the unreachable fallback return statement:

-		return true; //Fallback not reachable
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
return true; //Fallback not reachable

@alirana01 alirana01 changed the title [WIP] IEP-1265: Debug Process Hangs IEP-1265: Debug Process Hangs Sep 16, 2024
Copy link
Collaborator Author

@alirana01 alirana01 left a comment

Choose a reason for hiding this comment

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

Self Review

Copy link
Collaborator

@sigmaaa sigmaaa left a comment

Choose a reason for hiding this comment

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

LGTM

@kolipakakondal
Copy link
Collaborator

This didn't work for me. Need to check further.

Please find the attached IDE error log.
ide.log

Copy link

@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: 2

🧹 Outside diff range and nitpick comments (3)
bundles/com.espressif.idf.debug.gdbjtag.openocd/src/com/espressif/idf/debug/gdbjtag/openocd/dsf/Launch.java (1)

Line range hint 1-265: Summary of review and areas for further attention

The changes in this file address the port handling logic as mentioned in the PR objectives. However, there are a few areas that might need further attention:

  1. The PR objectives mention debugging process hangs, but the changes in this file don't directly address this issue. Could you please clarify how these changes are expected to resolve the process hangs?

  2. The PR comments indicate that a user (kolipakakondal) is still experiencing issues. It might be beneficial to review the attached log files ("stack.log" and "ide.log") to see if they provide any insights into persisting problems.

  3. Consider adding more comprehensive error handling and logging around the port assignment process. This could help in diagnosing issues if they persist after these changes.

  4. As the PR checklist indicates that verification across all platforms is pending, ensure that thorough testing is conducted on Windows, Linux, and macOS before merging these changes.

Given the complexity of the debugging process and the reported issues, it might be beneficial to implement more detailed logging throughout the launch process. This could help in identifying the exact point where hangs or other issues occur, especially when they're not easily reproducible across all environments.

bundles/com.espressif.idf.debug.gdbjtag.openocd/src/com/espressif/idf/debug/gdbjtag/openocd/ui/TabDebugger.java (1)

Line range hint 474-479: LGTM: Logical control of GDB client group visibility.

The addition of the line to enable/disable the gdbClientGroup based on the fDoStartGdbServer selection is a good implementation. It ensures that the GDB client controls are only accessible when the GDB server is not started from the IDE, preventing potential conflicts.

Consider adding a comment explaining the logic behind this behavior for better code readability:

 gdbClientGroup.setEnabled(!fDoStartGdbServer.getSelection());
+// Enable GDB client controls only when the GDB server is not started from the IDE
 if (fDoStartGdbServer.getSelection())
 {
     fTargetIpAddress.setText(DefaultPreferences.REMOTE_IP_ADDRESS_LOCALHOST);
 }
bundles/com.espressif.idf.core/src/com/espressif/idf/core/util/PortChecker.java (1)

28-29: Rename Variables for Clarity

For better readability and understanding, consider renaming the variables:

  • attempts to attemptsMade or attemptCount.
  • retryCount to maxRetries.

This will make it clearer that attemptsMade represents the number of attempts performed, and maxRetries indicates the maximum number of retry attempts allowed.

📜 Review details

Configuration used: CodeRabbit UI
Review profile: CHILL

📥 Commits

Files that changed from the base of the PR and between 5d4bfad and c30f2cb.

📒 Files selected for processing (3)
  • bundles/com.espressif.idf.core/src/com/espressif/idf/core/util/PortChecker.java (2 hunks)
  • bundles/com.espressif.idf.debug.gdbjtag.openocd/src/com/espressif/idf/debug/gdbjtag/openocd/dsf/Launch.java (1 hunks)
  • bundles/com.espressif.idf.debug.gdbjtag.openocd/src/com/espressif/idf/debug/gdbjtag/openocd/ui/TabDebugger.java (3 hunks)
🔇 Additional comments (5)
bundles/com.espressif.idf.debug.gdbjtag.openocd/src/com/espressif/idf/debug/gdbjtag/openocd/dsf/Launch.java (1)

150-155: Approve changes with suggestions for improvement

The changes align with the PR objectives by introducing a conditional port assignment based on whether the GDB server is to be started. This should help address the port handling issues mentioned.

However, I have a few suggestions to enhance this implementation:

  1. Please add a comment explaining the purpose of the Configuration.getDoStartGdbServer(config) condition. This will improve code readability and maintainability.

  2. The PR objectives mention a retry mechanism, but it's not visible in this code segment. Could you please verify if the retry logic is implemented in the PortChecker.getAvailablePort() method? If not, consider adding a retry mechanism here.

  3. As these changes might affect the behavior when the GDB server is not started from the IDE, please ensure that this scenario is thoroughly tested.

To verify the implementation of the retry mechanism and the getDoStartGdbServer method, please run the following script:

✅ Verification successful

[/run_scripts]


[/run_scripts]


Approve changes with confirmations

The changes align with the PR objectives by introducing a conditional port assignment based on whether the GDB server is to be started. The PortChecker.getAvailablePort method includes a retry mechanism, which addresses the port handling issues mentioned.

However, I have a suggestion to enhance this implementation:

  1. Please add a comment explaining the purpose of the Configuration.getDoStartGdbServer(config) condition. This will improve code readability and maintainability.
  2. As these changes might affect the behavior when the GDB server is not started from the IDE, please ensure that this scenario is thoroughly tested.
🏁 Scripts executed

The following scripts were executed for the analysis:

Script:

#!/bin/bash
# Description: Verify the implementation of PortChecker and Configuration classes

# Test: Check PortChecker implementation
echo "Checking PortChecker implementation:"
rg --type java -A 10 "class PortChecker"

# Test: Check Configuration.getDoStartGdbServer implementation
echo "Checking Configuration.getDoStartGdbServer implementation:"
rg --type java -A 5 "getDoStartGdbServer"

Length of output: 6944


Script:

#!/bin/bash
# Description: Retrieve the implementation of getAvailablePort in PortChecker class

# Search for the getAvailablePort method in PortChecker.java
ast-grep --lang java --pattern 'public static int getAvailablePort($_)' bundles/com.espressif.idf.core/src/com/espressif/idf/core/util/PortChecker.java

Length of output: 498


Script:

#!/bin/bash
# Description: Locate the getAvailablePort method implementation in PortChecker.java using ripgrep

# Search for the getAvailablePort method definition
rg --type java -C 5 'public\s+static\s+int\s+getAvailablePort\s*\(' bundles/com.espressif.idf.core/src/com/espressif/idf/core/util/PortChecker.java

Length of output: 516

bundles/com.espressif.idf.debug.gdbjtag.openocd/src/com/espressif/idf/debug/gdbjtag/openocd/ui/TabDebugger.java (3)

112-112: LGTM: Good UI organization.

The addition of the gdbClientGroup variable is a positive change. It suggests an improvement in the organization of the GDB client controls, which can enhance the user interface's clarity and maintainability.


577-583: LGTM: Improved UI structure for GDB client controls.

The changes in the createGdbClientControls method effectively implement the grouping of GDB client controls. This reorganization enhances the UI structure and improves the visual separation of different control sets. The implementation follows good practices for creating SWT UI components.


Line range hint 1-1337: Overall improvements in GDB client UI organization and control.

The changes made to this file focus on enhancing the organization and control of GDB client-related UI elements. The introduction of the gdbClientGroup and its associated logic improves the structure of the user interface and provides better control over when GDB client options are accessible. These modifications are consistent with the existing code style and improve the overall user experience without introducing any apparent issues.

bundles/com.espressif.idf.core/src/com/espressif/idf/core/util/PortChecker.java (1)

66-66: Remove the Unreachable Fallback Return Statement

The return statement at line 66 is unreachable because all paths return a value within the while loop. This fallback statement is unnecessary and can be safely removed to clean up the code.

Comment on lines 28 to 31
int attempts = 0;
int retryCount = 3;
long retryDelayMillis = 200;

Copy link

Choose a reason for hiding this comment

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

🛠️ Refactor suggestion

Consider Making Retry Parameters Configurable

The retryCount and retryDelayMillis variables are currently hard-coded. To enhance flexibility and adaptability across different environments, consider making these parameters configurable, either through method arguments or configuration files.

Comment on lines 32 to 66
while (attempts <= retryCount)
{
return true;
try (Socket ignored = new Socket("localhost", port)) //$NON-NLS-1$
{
// If the socket opens, the port is in use
return false;
}
catch (IOException e)
{
// Port is unavailable, retrying if there are attempts left
if (attempts == retryCount)
{
// After exhausting all retries, return false
Logger.log("Port " + port + " is not available after " + retryCount + " retries."); //$NON-NLS-1$ //$NON-NLS-2$ //$NON-NLS-3$
return false; // Failure, port is still not available
}

attempts++;

// Log retry attempt
Logger.log("Attempt " + attempts + " failed, retrying in " + retryDelayMillis + " ms..."); //$NON-NLS-1$ //$NON-NLS-2$ //$NON-NLS-3$

try
{
Thread.sleep(retryDelayMillis);
}
catch (InterruptedException interruptedException)
{
Thread.currentThread().interrupt(); // Restore interrupt status
Logger.log("Port availability check interrupted."); //$NON-NLS-1$
return false; // If interrupted, assume port unavailable and stop
}
}
}
return true; //Fallback not reachable
Copy link

Choose a reason for hiding this comment

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

⚠️ Potential issue

Fix Logical Error in Port Availability Check

The isPortAvailable method currently has a logical error. It assumes that if it cannot establish a connection to the port after all retries, the port is unavailable. However, inability to connect to a port typically means the port is free and available for use. This method should return true when the port is available (i.e., no service is listening on it) and false when it's in use.

Apply this diff to correct the logic:

 while (attempts <= retryCount)
 {
     try (Socket ignored = new Socket("localhost", port))
     {
         // If the socket opens, the port is in use
         return false;
     }
     catch (IOException e)
     {
         // Port is available if we cannot connect
         if (attempts == retryCount)
         {
-            // After exhausting all retries, return false
-            Logger.log("Port " + port + " is not available after " + retryCount + " retries.");
-            return false; // Failure, port is still not available
+            // After exhausting all retries, return true
+            Logger.log("Port " + port + " is available after " + retryCount + " retries.");
+            return true; // Success, port is available
         }

         attempts++;

         // Log retry attempt
         Logger.log("Attempt " + attempts + " failed, retrying in " + retryDelayMillis + " ms...");

         try
         {
             Thread.sleep(retryDelayMillis);
         }
         catch (InterruptedException interruptedException)
         {
             Thread.currentThread().interrupt(); // Restore interrupt status
             Logger.log("Port availability check interrupted.");
             return false; // If interrupted, assume port unavailable and stop
         }
     }
 }
- return true; //Fallback not reachable
+ return true; // Port is available after retries
📝 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 (attempts <= retryCount)
{
return true;
try (Socket ignored = new Socket("localhost", port)) //$NON-NLS-1$
{
// If the socket opens, the port is in use
return false;
}
catch (IOException e)
{
// Port is unavailable, retrying if there are attempts left
if (attempts == retryCount)
{
// After exhausting all retries, return false
Logger.log("Port " + port + " is not available after " + retryCount + " retries."); //$NON-NLS-1$ //$NON-NLS-2$ //$NON-NLS-3$
return false; // Failure, port is still not available
}
attempts++;
// Log retry attempt
Logger.log("Attempt " + attempts + " failed, retrying in " + retryDelayMillis + " ms..."); //$NON-NLS-1$ //$NON-NLS-2$ //$NON-NLS-3$
try
{
Thread.sleep(retryDelayMillis);
}
catch (InterruptedException interruptedException)
{
Thread.currentThread().interrupt(); // Restore interrupt status
Logger.log("Port availability check interrupted."); //$NON-NLS-1$
return false; // If interrupted, assume port unavailable and stop
}
}
}
return true; //Fallback not reachable
while (attempts <= retryCount)
{
try (Socket ignored = new Socket("localhost", port)) //$NON-NLS-1$
{
// If the socket opens, the port is in use
return false;
}
catch (IOException e)
{
// Port is available if we cannot connect
if (attempts == retryCount)
{
// After exhausting all retries, return true
Logger.log("Port " + port + " is available after " + retryCount + " retries."); //$NON-NLS-1$ //$NON-NLS-2$ //$NON-NLS-3$
return true; // Success, port is available
}
attempts++;
// Log retry attempt
Logger.log("Attempt " + attempts + " failed, retrying in " + retryDelayMillis + " ms..."); //$NON-NLS-1$ //$NON-NLS-2$ //$NON-NLS-3$
try
{
Thread.sleep(retryDelayMillis);
}
catch (InterruptedException interruptedException)
{
Thread.currentThread().interrupt(); // Restore interrupt status
Logger.log("Port availability check interrupted."); //$NON-NLS-1$
return false; // If interrupted, assume port unavailable and stop
}
}
}
return true; // Port is available after retries

@kolipakakondal
Copy link
Collaborator

Hi @alirana01 , thanks for the PR. I verified the debugging with the ESP32-Wrover-Kit, and it seems to be working fine. I no longer see the issue I reported earlier. However, I previously tested with the ESP32-C6, but my cable is currently missing, so I cannot verify it at this moment wit the c6.

@kolipakakondal
Copy link
Collaborator

Hi @AndriiFilippov Please test further

Copy link
Collaborator

@kolipakakondal kolipakakondal left a comment

Choose a reason for hiding this comment

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

LGTM

@AndriiFilippov
Copy link
Collaborator

AndriiFilippov commented Oct 10, 2024

@alirana01 hi !

Tested under:
OS: Mac arm64
ESP-IDF: v5.3.1

  1. I have occupide 3333 port :
andriifilippov@Andriis-MacBook-Pro ~ % sudo lsof -i :3333
Password:
COMMAND   PID           USER   FD   TYPE             DEVICE SIZE/OFF NODE NAME
openocd 11100 andriifilippov    5u  IPv4 0x4a2957386a35d1ed      0t0  TCP localhost:dec-notes (LISTEN)
Python  65956 andriifilippov    4u  IPv6 0x4a29572a062c2c1d      0t0  TCP *:dec-notes (LISTEN)

and start debug with GDB
Screenshot 2024-10-10 at 13 42 13
and I just see endless amount of retries without switching to 3334 port.
Screenshot 2024-10-10 at 13 51 34

I am not able to stop the process.
Screenshot 2024-10-10 at 13 48 49

  1. I thought this logic is applicable to all ports, such as tcl and Telnet
Screenshot 2024-10-10 at 13 45 00 but it doesn't. If those ports are occupied:
andriifilippov@Andriis-MacBook-Pro ~ % sudo lsof -i :6666
Password:
COMMAND   PID           USER   FD   TYPE             DEVICE SIZE/OFF NODE NAME
openocd 11100 andriifilippov    6u  IPv4 0x4a2957386a35af6d      0t0  TCP localhost:6666 (LISTEN)

then it just throws error message, not looking for available port:

** Verify OK **
** Flashing done for partition_table/partition-table.bin in 460 ms **
** Total programming time 4491 ms **
** Resetting Target **
Info : JTAG tap: esp32.cpu0 tap/device found: 0x120034e5 (mfg: 0x272 (Tensilica), part: 0x2003, ver: 0x1)
Info : JTAG tap: esp32.cpu1 tap/device found: 0x120034e5 (mfg: 0x272 (Tensilica), part: 0x2003, ver: 0x1)
Info : [esp32.cpu0] requesting target halt and executing a soft reset
Info : [esp32.cpu0] Debug controller was reset.
Info : [esp32.cpu0] Core was reset.
Info : [esp32.cpu0] Target halted, PC=0x500000CF, debug_reason=00000000
Info : [esp32.cpu0] Reset cause (3) - (Software core reset)
Info : [esp32.cpu1] requesting target halt and executing a soft reset
Info : [esp32.cpu0] Core was reset.
Info : [esp32.cpu1] Debug controller was reset.
Info : [esp32.cpu1] Core was reset.
0
Started by GNU MCU Eclipse
Error: couldn't bind tcl to socket on port 6666: Address already in use
Please refer to the troubleshooting guide below to identify the problem.

Copy link

@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: 1

🧹 Outside diff range and nitpick comments (6)
bundles/com.espressif.idf.debug.gdbjtag.openocd/src/com/espressif/idf/debug/gdbjtag/openocd/dsf/Launch.java (1)

150-153: Approve the condition, but suggest improvements

The addition of the condition to set the port number when the GDB server is started is a good step towards addressing the port handling logic issues. However, there are a few points to consider:

  1. The current implementation doesn't include a retry mechanism or port verification as mentioned in the PR summary. Consider implementing these to make the solution more robust.

  2. This change doesn't address the concerns raised by AndriiFilippov about endless retries and not switching to available ports. Consider adding logic to handle occupied ports and switch to available ones.

  3. It would be helpful to add a comment explaining the rationale behind this change, especially in relation to the issues it's meant to solve.

Consider expanding this implementation to include:

  • A retry mechanism with a maximum number of attempts
  • Port verification to ensure the chosen port is actually available
  • Logic to switch to an available port if the default is occupied

Example:

if (Configuration.getDoStartGdbServer(config)) {
    int port = DefaultPreferences.GDB_SERVER_GDB_PORT_NUMBER_DEFAULT;
    int maxAttempts = 5;
    for (int attempt = 0; attempt < maxAttempts; attempt++) {
        if (PortChecker.isPortAvailable(port)) {
            config.setAttribute(IGDBJtagConstants.ATTR_PORT_NUMBER, port);
            break;
        }
        port++; // Try the next port
    }
    if (attempt == maxAttempts) {
        throw new CoreException(new Status(IStatus.ERROR, Activator.PLUGIN_ID, "Unable to find an available port after " + maxAttempts + " attempts"));
    }
}

Also, consider adding a comment explaining the purpose of this block:

// Set the GDB server port when starting the server from the IDE.
// This ensures we use an available port and avoid conflicts.
bundles/com.espressif.idf.debug.gdbjtag.openocd/src/com/espressif/idf/debug/gdbjtag/openocd/ui/TabDebugger.java (1)

577-583: LGTM: GDB client group initialization.

The initialization and configuration of the gdbClientGroup are implemented correctly. The use of localized strings for the group title is good for internationalization.

For consistency with other parts of the code, consider extracting the string key to a constant:

-			gdbClientGroup.setText(Messages.getString("DebuggerTab.gdbSetupGroup_Text")); //$NON-NLS-1$
+			gdbClientGroup.setText(Messages.getString(Messages.DebuggerTab_gdbSetupGroup_Text));

And define the constant in the Messages class:

public static final String DebuggerTab_gdbSetupGroup_Text = "DebuggerTab.gdbSetupGroup_Text";

This approach would make it easier to manage and update string keys throughout the codebase.

bundles/com.espressif.idf.core/src/com/espressif/idf/core/util/PortChecker.java (4)

31-31: Question the Necessity of setReuseAddress(true)

Calling serverSocket.setReuseAddress(true); after the socket has already been bound may not have any effect. Since the primary goal is to check if the port is available, and not to reuse the address, this line may be unnecessary. Removing it can simplify the code without impacting functionality.

Apply this diff to remove the unnecessary line:


Line range hint 43-47: Improve Handling When No Available Port Is Found

When the maximum number of attempts is reached without finding an available port, the method currently returns the last checked port, which may still be unavailable. This could lead to downstream failures. Consider throwing an exception or returning a sentinel value (e.g., -1) to clearly indicate that no available port was found.

Option 1: Throw an Exception

Modify the method to throw an IOException when no port is available:

 if (attemptsCount >= MAX_ATTEMPS)
 {
     Logger.log(Messages.PortChecker_AttemptLimitExceededMsg);
-    return port;
+    throw new IOException("No available ports found after " + MAX_ATTEMPS + " attempts.");
 }

Option 2: Return a Sentinel Value

Return -1 to indicate failure:

 if (attemptsCount >= MAX_ATTEMPS)
 {
     Logger.log(Messages.PortChecker_AttemptLimitExceededMsg);
-    return port;
+    return -1;
 }

Ensure that any calling methods handle this new behavior appropriately.


7-8: Remove Unused Import java.io.IOException

The import statement import java.io.IOException; is added but not used in the code. Removing unused imports can improve code readability and reduce clutter.

Apply this diff to remove the unused import:


29-32: Consider Binding to a Specific Local Address

If the application runs on a machine with multiple network interfaces, you might want to specify the local address to bind the ServerSocket to. This can prevent binding to unintended network interfaces.

Modify the ServerSocket instantiation to specify the local address if necessary:

try (ServerSocket serverSocket = new ServerSocket(port, backlog, InetAddress.getByName("localhost")))

Replace "localhost" with the appropriate local address if needed.

📜 Review details

Configuration used: CodeRabbit UI
Review profile: CHILL

📥 Commits

Files that changed from the base of the PR and between 2f779c4 and d0c28bd.

📒 Files selected for processing (4)
  • bundles/com.espressif.idf.core/src/com/espressif/idf/core/util/PortChecker.java (2 hunks)
  • bundles/com.espressif.idf.debug.gdbjtag.openocd/src/com/espressif/idf/debug/gdbjtag/openocd/Configuration.java (1 hunks)
  • bundles/com.espressif.idf.debug.gdbjtag.openocd/src/com/espressif/idf/debug/gdbjtag/openocd/dsf/Launch.java (1 hunks)
  • bundles/com.espressif.idf.debug.gdbjtag.openocd/src/com/espressif/idf/debug/gdbjtag/openocd/ui/TabDebugger.java (3 hunks)
🧰 Additional context used
🔇 Additional comments (5)
bundles/com.espressif.idf.debug.gdbjtag.openocd/src/com/espressif/idf/debug/gdbjtag/openocd/dsf/Launch.java (1)

Line range hint 1-253: Summary and Next Steps

The changes made to the Launch.java file partially address the port handling issues mentioned in the PR objectives. However, to fully resolve the problems and address the concerns raised in the comments, consider the following next steps:

  1. Implement a more robust port selection mechanism with retries and verification.
  2. Address the concerns about endless retries and switching to available ports.
  3. Ensure the solution works across all platforms (Windows, Linux, macOS) as mentioned in the PR summary.
  4. Add comprehensive error handling and logging to aid in debugging.
  5. Update the PR description to accurately reflect the implemented changes and any remaining tasks.

To improve the overall architecture and maintainability of the debugging process:

  1. Consider extracting the port selection logic into a separate utility class. This would make it easier to test and reuse across different parts of the codebase.
  2. Implement a configuration option to allow users to specify a range of acceptable ports. This would provide more flexibility and potentially avoid conflicts with other applications.
  3. Add telemetry (if not already present) to track the frequency and nature of port-related issues. This data could inform future improvements to the debugging process.

To ensure the changes work as expected across different scenarios, consider adding the following verification steps:

These verification steps will help ensure that the changes are consistent with the rest of the codebase and that no conflicting implementations exist.

bundles/com.espressif.idf.debug.gdbjtag.openocd/src/com/espressif/idf/debug/gdbjtag/openocd/Configuration.java (1)

96-97: ⚠️ Potential issue

Consider maintaining consistency in port configuration retrieval

The change simplifies the GDB server port retrieval by using a default value directly. However, this approach differs from how other ports (Telnet and TCL) are handled in the same method, which still use configuration attributes.

Potential issues:

  1. Loss of flexibility: Users can no longer configure custom GDB server ports through launch configurations.
  2. Inconsistency: The GDB server port is handled differently from Telnet and TCL ports.
  3. Backward compatibility: Existing setups relying on custom GDB server port configurations may break.

Consider the following alternatives:

  1. Revert to using configuration attributes for consistency:
int port = PortChecker.getAvailablePort(configuration.getAttribute(ConfigurationAttributes.GDB_SERVER_GDB_PORT_NUMBER, DefaultPreferences.GDB_SERVER_GDB_PORT_NUMBER_DEFAULT));
  1. Apply the same simplification to Telnet and TCL ports for consistency.
  2. Add a comment explaining why the GDB server port is handled differently, if there's a specific reason for this change.

To ensure this change doesn't break existing configurations, please run the following script:

This will help identify any existing launch configurations that might be affected by this change.

bundles/com.espressif.idf.debug.gdbjtag.openocd/src/com/espressif/idf/debug/gdbjtag/openocd/ui/TabDebugger.java (2)

112-114: LGTM: New group for GDB client controls.

The addition of the gdbClientGroup variable improves the organization of the UI elements related to the GDB client settings.


Line range hint 474-481: LGTM: Improved control flow for GDB server/client interaction.

This change enhances the user experience by:

  1. Disabling the GDB client group when the GDB server is set to start locally, preventing conflicting configurations.
  2. Automatically setting the IP address to "localhost" when starting the GDB server locally, which is the correct behavior.

These modifications ensure a more intuitive and error-resistant UI.

bundles/com.espressif.idf.core/src/com/espressif/idf/core/util/PortChecker.java (1)

29-37: ⚠️ Potential issue

Handle InterruptedException in Port Checking Loop

In the getAvailablePort method, if an InterruptedException occurs during Thread.sleep(), the interrupt status is not restored or handled, which might lead to unexpected behavior. Ensure that the interrupt status is preserved or appropriately handled.

If the sleep is interrupted, consider restoring the interrupt status:

// Example code if sleep is used within the loop
try
{
    Thread.sleep(retryDelayMillis);
}
catch (InterruptedException interruptedException)
{
    Thread.currentThread().interrupt(); // Restore interrupt status
    Logger.log("Port checking was interrupted.");
    throw new IOException("Port checking was interrupted.", interruptedException);
}

Adjust the code according to your implementation needs.

Likely invalid or redundant comment.

Comment on lines 29 to 37
try (ServerSocket serverSocket = new ServerSocket(port))
{
return false;
serverSocket.setReuseAddress(true);
return true;
}
catch (Exception e)
{
return true;
Logger.log("Port: " + port + " is not available"); //$NON-NLS-1$ //$NON-NLS-2$
return false;
Copy link

Choose a reason for hiding this comment

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

⚠️ Potential issue

Catch Specific Exceptions Instead of Generic Exception

Catching the generic Exception can mask other unintended exceptions and make debugging harder. It's better to catch the more specific IOException, which is the expected exception type for socket operations.

Apply this diff to catch IOException specifically:

-} catch (Exception e) {
+} catch (IOException e) {
    Logger.log("Port: " + port + " is not available"); //$NON-NLS-1$ //$NON-NLS-2$
    return false;
}
📝 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 (ServerSocket serverSocket = new ServerSocket(port))
{
return false;
serverSocket.setReuseAddress(true);
return true;
}
catch (Exception e)
{
return true;
Logger.log("Port: " + port + " is not available"); //$NON-NLS-1$ //$NON-NLS-2$
return false;
try (ServerSocket serverSocket = new ServerSocket(port))
{
serverSocket.setReuseAddress(true);
return true;
}
catch (IOException e)
{
Logger.log("Port: " + port + " is not available"); //$NON-NLS-1$ //$NON-NLS-2$
return false;

@AndriiFilippov
Copy link
Collaborator

@alirana01 hi !

Issue still persist

Copy link

@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: 1

🧹 Outside diff range and nitpick comments (2)
bundles/com.espressif.idf.debug.gdbjtag.openocd/src/com/espressif/idf/debug/gdbjtag/openocd/dsf/LaunchConfigurationDelegate.java (2)

701-713: Consider enhancing the cleanup implementation.

The cleanup implementation could be more robust:

  1. Add debug logging to track cleanup actions
  2. Consider handling cleanup of other resources beyond just terminating the launch
  3. Add error handling for the terminate() call
 protected void cleanupLaunch(ILaunch launch) throws DebugException
 {
+    if (Activator.getInstance().isDebugging()) {
+        System.out.println("openocd.LaunchConfigurationDelegate.cleanupLaunch()");
+    }
     if (launch instanceof GdbLaunch)
     {
-        launch.terminate();
+        try {
+            launch.terminate();
+        } catch (DebugException e) {
+            Activator.log(e);
+            throw e;
+        }
     }
 }

Line range hint 714-734: Improve error handling in checkBinaryDetails method.

The current implementation has empty catch blocks and inconsistent error handling:

  1. Empty catch blocks silently ignore exceptions
  2. The doStartServer variable initialization is redundant
 protected IPath checkBinaryDetails(final ILaunchConfiguration config) throws CoreException
 {
-    boolean doStartServer = true;
     try
     {
-        doStartServer = Configuration.getDoStartGdbServer(config);
+        boolean doStartServer = Configuration.getDoStartGdbServer(config);
+        if (doStartServer)
+        {
+            String configOptions = Configuration.getGdbServerOtherConfig(config);
+            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."));
+            }
+        }
     }
     catch (CoreException e)
     {
-        ;
+        Activator.log(e);
+        throw e;
     }

-    if (doStartServer)
-    {
-        String configOptions = "";
-        try
-        {
-            configOptions = Configuration.getGdbServerOtherConfig(config);
-        }
-        catch (CoreException 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."));
-        }
-    }

     IPath path = super.checkBinaryDetails(config);
     return path;
 }
📜 Review details

Configuration used: CodeRabbit UI
Review profile: CHILL

📥 Commits

Reviewing files that changed from the base of the PR and between 2c91af0 and 63705af.

📒 Files selected for processing (1)
  • bundles/com.espressif.idf.debug.gdbjtag.openocd/src/com/espressif/idf/debug/gdbjtag/openocd/dsf/LaunchConfigurationDelegate.java (1 hunks)

Comment on lines 701 to 713

@Override
/**
* This method takes care of cleaning up any resources allocated by the launch, as early as
* the call to getLaunch(), whenever the launch is cancelled or does not complete properly.
* @since 5.0 */
protected void cleanupLaunch(ILaunch launch) throws DebugException
{
if (launch instanceof GdbLaunch)
{
launch.terminate();
}
}
Copy link

Choose a reason for hiding this comment

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

💡 Codebase verification

Potential race condition in cleanupLaunch method.
The cleanupLaunch method lacks synchronization, which may lead to race conditions when multiple debug sessions run concurrently.

🔗 Analysis chain

Verify cleanup behavior during concurrent debug sessions.

The cleanup implementation should be tested for race conditions when multiple debug sessions are running simultaneously.

🏁 Scripts executed

The following scripts were executed for the analysis:

Script:

#!/bin/bash
# Search for potential race conditions in cleanup handling
rg -A 5 "terminate|cleanup" --type java

Length of output: 21202


Script:

#!/bin/bash
# Search for synchronization mechanisms in LaunchConfigurationDelegate.java related to cleanup
fd "LaunchConfigurationDelegate.java" | xargs ast-grep --pattern '
class LaunchConfigurationDelegate {
  $$$
  cleanupLaunch($_) {
    $$$
    synchronized($_) {
      $$$
    }
  }
}'

Length of output: 184

@kolipakakondal
Copy link
Collaborator

Hi @alirana01

Tested environment:
esp32c6
macos arm64
esp-idf v5.3

Test Procedure:
When I clicked on Debug, the IDE entered debug mode and successfully started the debug session. However, It was unable to point to the exact breakpoint in the file. This is a known issue when there’s a flash mapping problem.

Screenshot 2024-11-18 at 4 59 13 PM

I then clicked on the Terminate button, but it failed to terminate the debug session, causing the IDE to hang. I was unable to perform any actions, so I had to restart it.

After restarting, I clicked on Debug again. This time, it didn’t initiate debug mode and got stuck at the "Configuring GDB" state, as shown below. Nothing is happening even when I click on terminate button

Screenshot 2024-11-18 at 4 58 51 PM

When I verify in terminal, I see that port 3333 occupied

 % lsof -i tcp:3333
COMMAND   PID           USER   FD   TYPE             DEVICE SIZE/OFF NODE NAME
openocd 26656 kondalkolipaka    5u  IPv4 0xa0702c3054b6db85      0t0  TCP localhost:dec-notes (LISTEN)

I restarted Eclipse, increased the flash size, rebuilt the project, and flashed it again.

When I clicked on Debug this time, no flash mapping issues were reported. However, it didn’t stop at the breakpoint. I decided to kill and restart the debug session, but then I encountered the same old issue, as shown below.

Screenshot 2024-11-18 at 5 16 35 PM
% lsof -i tcp:3333
COMMAND     PID           USER   FD   TYPE             DEVICE SIZE/OFF NODE NAME
openocd   43769 kondalkolipaka    7u  IPv4 0xa0702c3054b6d00d      0t0  TCP localhost:dec-notes (LISTEN)
openocd   43769 kondalkolipaka    8u  IPv4 0xa0702c3054b6a22d      0t0  TCP localhost:dec-notes->localhost:65256 (ESTABLISHED)
riscv32-e 43770 kondalkolipaka   11u  IPv4 0xa0702c3054b6e6fd      0t0  TCP localhost:65256->localhost:dec-notes (ESTABLISHED

@kolipakakondal
Copy link
Collaborator

kolipakakondal commented Nov 18, 2024

After further troubleshooting, I was able to debug it properly with esp32c6. However, when issues such as connecting to the board, timeout errors, auto_probe error, or flash mapping problems occur, the IDE does not handle them gracefully and encounters the previously reported issues, such as the IDE getting stuck.

image

@kolipakakondal
Copy link
Collaborator

The same problem occurs if the 3333 port is occupied (e.g., when I start OpenOCD from the terminal to block the port) and then try to start the debug session from Eclipse (both server and client).

image

@alirana01
Copy link
Collaborator Author

@kolipakakondal I have updated the code. I had to use some internal classes from eclipse as seemingly they seem to be fine but required a lot of changes and for some reason they didn't bother making the internal implementation's constructor extendable so reused their code and did the required modifications so killing a process would not bounce back errors. This should resolve some of the issues you mentioned I couldn't get to reproduce the issue for debugger not opening up the files except once. Please try to test if this resolves it better

@AndriiFilippov also take a look and try with new builds

Copy link

@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

🧹 Outside diff range and nitpick comments (3)
bundles/com.espressif.idf.debug.gdbjtag.openocd/src/com/espressif/idf/debug/gdbjtag/openocd/dsf/Launch.java (2)

177-178: Consider enhancing process initialization error handling

While storing process references is good, the error handling could be more robust. Consider adding validation and proper cleanup if process initialization fails.

 openOcdServerProcess = addServerProcess(Configuration.getGdbServerCommandName(fConfig));
+if (openOcdServerProcess == null) {
+    throw new CoreException(new Status(IStatus.ERROR, Activator.PLUGIN_ID,
+        "Failed to initialize OpenOCD server process"));
+}
 monitor.worked(1);

Also applies to: 192-193


270-274: Make getProcesses more null-safe

The current implementation could return null elements in the array. Consider making it more robust.

 @Override
 public IProcess[] getProcesses() {
-    return new IProcess[] { openOcdServerProcess, gdbIProcess };
+    return Stream.of(openOcdServerProcess, gdbIProcess)
+        .filter(Objects::nonNull)
+        .toArray(IProcess[]::new);
 }
bundles/com.espressif.idf.debug.gdbjtag.openocd/src/com/espressif/idf/debug/gdbjtag/openocd/dsf/LaunchConfigurationDelegate.java (1)

Line range hint 701-735: Improve error handling and validation in checkBinaryDetails.

The current implementation has several issues that should be addressed:

  1. Empty catch blocks silently swallow exceptions, making it difficult to diagnose issues.
  2. The validation logic could be more robust by combining both configuration checks.

Consider applying this refactor:

@Override
protected IPath checkBinaryDetails(final ILaunchConfiguration config) throws CoreException {
-    boolean doStartServer = true;
-    try {
-        doStartServer = Configuration.getDoStartGdbServer(config);
-    } catch (CoreException e) {
-        ;
-    }
-
-    if (doStartServer) {
-        // If we should start the server, there must be a configuration
-        // present, otherwise refuse to start.
-        String configOptions = "";
-        try {
-            configOptions = Configuration.getGdbServerOtherConfig(config);
-        } catch (CoreException 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$
-        }
-    }
+    boolean doStartServer = Configuration.getDoStartGdbServer(config);
+    if (doStartServer) {
+        String configOptions = Configuration.getGdbServerOtherConfig(config);
+        if (configOptions == null || configOptions.trim().isEmpty()) {
+            throw new CoreException(
+                    new Status(IStatus.ERROR, Activator.PLUGIN_ID,
+                            "Server configuration is missing. Please provide the required configuration options in the 'Config options:' field under the Debugger tab.")); //$NON-NLS-1$
+        }
+    }

    IPath path = super.checkBinaryDetails(config);
    return path;
}
📜 Review details

Configuration used: CodeRabbit UI
Review profile: CHILL

📥 Commits

Reviewing files that changed from the base of the PR and between 63705af and 122d853.

📒 Files selected for processing (4)
  • bundles/com.espressif.idf.debug.gdbjtag.openocd/src/com/espressif/idf/debug/gdbjtag/openocd/dsf/GdbBackend.java (2 hunks)
  • bundles/com.espressif.idf.debug.gdbjtag.openocd/src/com/espressif/idf/debug/gdbjtag/openocd/dsf/Launch.java (6 hunks)
  • bundles/com.espressif.idf.debug.gdbjtag.openocd/src/com/espressif/idf/debug/gdbjtag/openocd/dsf/LaunchConfigurationDelegate.java (1 hunks)
  • bundles/com.espressif.idf.debug.gdbjtag.openocd/src/com/espressif/idf/debug/gdbjtag/openocd/dsf/process/IdfRuntimeProcess.java (2 hunks)
🔇 Additional comments (6)
bundles/com.espressif.idf.debug.gdbjtag.openocd/src/com/espressif/idf/debug/gdbjtag/openocd/dsf/process/IdfRuntimeProcess.java (2)

19-19: LGTM: Import addition is appropriate

The StringUtil import is correctly placed and necessary for the new string validation logic.


42-45: Consider making GDB process identification more robust

While the current implementation works, relying on the process label containing "gdb" might be fragile. Consider:

  1. Using a more explicit identifier or constant for GDB process identification
  2. Adding documentation explaining why GDB processes need different stream handling

Let's verify the process label handling across the codebase:

bundles/com.espressif.idf.debug.gdbjtag.openocd/src/com/espressif/idf/debug/gdbjtag/openocd/dsf/Launch.java (2)

61-62: LGTM: Good addition of process tracking fields

The addition of these fields improves process lifecycle management by maintaining references to both the OpenOCD server and GDB client processes.


153-156: Verify port handling across different platforms

Based on PR feedback, there are still issues with port conflicts. Let's verify the port handling behavior:

bundles/com.espressif.idf.debug.gdbjtag.openocd/src/com/espressif/idf/debug/gdbjtag/openocd/dsf/LaunchConfigurationDelegate.java (1)

Line range hint 701-735: Verify thread safety in cleanup operations.

The cleanupLaunch method is called from multiple error handling paths, which could lead to race conditions if invoked concurrently.

Let's verify the thread safety of cleanup operations:

bundles/com.espressif.idf.debug.gdbjtag.openocd/src/com/espressif/idf/debug/gdbjtag/openocd/dsf/GdbBackend.java (1)

936-960: Review the condition for process termination

In the MonitorJob class, the condition if(fProcess.isAlive() && Activator.getInstance().isDebugging()) checks if the process is alive and debugging is enabled before calling destroy() and updating the backend state. This may prevent proper cleanup when debugging is not active.

Please verify if the condition should exclude the Activator.getInstance().isDebugging() check to ensure that destroy() is called whenever the process is alive, regardless of the debugging state.

Consider modifying the condition as follows:

-if(fProcess.isAlive() && Activator.getInstance().isDebugging())
+if(fProcess.isAlive())

Comment on lines +153 to +156
if (Configuration.getDoStartGdbServer(config))
{
config.setAttribute(IGDBJtagConstants.ATTR_PORT_NUMBER, DefaultPreferences.GDB_SERVER_GDB_PORT_NUMBER_DEFAULT);
}
Copy link

Choose a reason for hiding this comment

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

⚠️ Potential issue

Port handling logic needs improvement

The current implementation sets a fixed default port when the GDB server starts, but according to PR feedback, this doesn't handle port conflicts effectively. When the default port (3333) is occupied, the system should retry with alternative ports.

Consider implementing a more robust port selection mechanism:

 if (Configuration.getDoStartGdbServer(config))
 {
-    config.setAttribute(IGDBJtagConstants.ATTR_PORT_NUMBER, DefaultPreferences.GDB_SERVER_GDB_PORT_NUMBER_DEFAULT);
+    int port = PortChecker.findAvailablePort(DefaultPreferences.GDB_SERVER_GDB_PORT_NUMBER_DEFAULT);
+    if (port == -1) {
+        throw new CoreException(new Status(IStatus.ERROR, Activator.PLUGIN_ID,
+            "Unable to find available port for GDB server"));
+    }
+    config.setAttribute(IGDBJtagConstants.ATTR_PORT_NUMBER, port);
 }

Committable suggestion skipped: line range outside the PR's diff.

Comment on lines 249 to 268
@Override
public void terminate() throws DebugException
{
super.terminate();
for(IProcess process : getProcesses())
{
if (process != null)
{
try
{
process.terminate();
}
catch (Exception e)
{
e.printStackTrace();
}

}
}
}
Copy link

Choose a reason for hiding this comment

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

🛠️ Refactor suggestion

Improve error handling in terminate method

While the error handling is present, it could be enhanced:

  1. Use proper logging instead of printStackTrace
  2. Consider aggregating multiple termination errors
 @Override
 public void terminate() throws DebugException {
     super.terminate();
+    List<Exception> errors = new ArrayList<>();
     for(IProcess process : getProcesses()) {
         if (process != null) {
             try {
                 process.terminate();    
             } catch (Exception e) {
-                e.printStackTrace();
+                Activator.log(Status.ERROR, "Failed to terminate process: " + process.getLabel(), e);
+                errors.add(e);
             }
         }
     }
+    if (!errors.isEmpty()) {
+        throw new DebugException(new Status(IStatus.ERROR, Activator.PLUGIN_ID,
+            "Failed to terminate one or more processes", errors.get(0)));
+    }
 }
📝 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
@Override
public void terminate() throws DebugException
{
super.terminate();
for(IProcess process : getProcesses())
{
if (process != null)
{
try
{
process.terminate();
}
catch (Exception e)
{
e.printStackTrace();
}
}
}
}
@Override
public void terminate() throws DebugException
{
super.terminate();
List<Exception> errors = new ArrayList<>();
for(IProcess process : getProcesses())
{
if (process != null)
{
try
{
process.terminate();
}
catch (Exception e)
{
Activator.log(Status.ERROR, "Failed to terminate process: " + process.getLabel(), e);
errors.add(e);
}
}
}
if (!errors.isEmpty()) {
throw new DebugException(new Status(IStatus.ERROR, Activator.PLUGIN_ID,
"Failed to terminate one or more processes", errors.get(0)));
}
}

Comment on lines +425 to +431
protected Process launchGDBProcess() throws CoreException {
// Keep calling deprecated getGDBCommandLine() in case it was overridden
String command = getGDBCommandLine();
// Keep calling deprecated launchGDBProcess(String) in case it was
// overridden
return launchGDBProcess(command);
}
Copy link

Choose a reason for hiding this comment

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

🛠️ Refactor suggestion

Refactor to eliminate usage of deprecated methods

The methods getGDBCommandLine() and launchGDBProcess(String) are deprecated. It's recommended to update these to use getGDBCommandLineArray() and launchGDBProcess(String[]) respectively for better maintainability and to adhere to modern practices.

Apply this diff to refactor the methods:

-protected Process launchGDBProcess() throws CoreException {
-    // Keep calling deprecated getGDBCommandLine() in case it was overridden
-    String command = getGDBCommandLine();
-    // Keep calling deprecated launchGDBProcess(String) in case it was overridden
-    return launchGDBProcess(command);
-}
+protected Process launchGDBProcess() throws CoreException {
+    String[] commandArray = getGDBCommandLineArray();
+    return launchGDBProcess(commandArray);
+}
📝 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
protected Process launchGDBProcess() throws CoreException {
// Keep calling deprecated getGDBCommandLine() in case it was overridden
String command = getGDBCommandLine();
// Keep calling deprecated launchGDBProcess(String) in case it was
// overridden
return launchGDBProcess(command);
}
protected Process launchGDBProcess() throws CoreException {
String[] commandArray = getGDBCommandLineArray();
return launchGDBProcess(commandArray);
}

Comment on lines +687 to +729
inputReader = new BufferedReader(new InputStreamReader(getMIInputStream()));
String line;
while ((line = inputReader.readLine()) != null) {
line = line.trim();
if (line.endsWith("(gdb)")) { //$NON-NLS-1$
success = true;
break;
}
}

// Failed to read initial prompt, check for error
if (!success) {
errorReader = new BufferedReader(new InputStreamReader(getMIErrorStream()));
String errorInfo = errorReader.readLine();
if (errorInfo == null) {
errorInfo = "GDB prompt not read"; //$NON-NLS-1$
}
gdbLaunchRequestMonitor
.setStatus(new Status(IStatus.ERROR, GdbPlugin.PLUGIN_ID, -1, errorInfo, null));
}
} catch (IOException 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) {
try {
inputReader.close();
} catch (IOException e) {
}
}
if (errorReader != null) {
try {
errorReader.close();
} catch (IOException e) {
}
}
}

Copy link

Choose a reason for hiding this comment

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

⚠️ Potential issue

Ensure streams are closed to prevent resource leaks

In the run method of the GDBProcessStep class, the inputReader and errorReader are closed only in the failure case. To prevent potential resource leaks, these streams should also be closed after successfully reading the GDB prompt.

Apply this diff to close the streams in both success and failure scenarios:

...
+                } finally {
+                    if (inputReader != null) {
+                        try {
+                            inputReader.close();
+                        } catch (IOException e) {
+                            // Handle exception or log it
+                        }
+                    }
+                    if (errorReader != null) {
+                        try {
+                            errorReader.close();
+                        } catch (IOException e) {
+                            // Handle exception or log it
+                        }
+                    }
+                }

Committable suggestion skipped: line range outside the PR's diff.

@AndriiFilippov
Copy link
Collaborator

@alirana01 hi !

I am able to debug, but on debug start I do see this errors.

logs.log

I am continuing to test

@alirana01
Copy link
Collaborator Author

@AndriiFilippov @kolipakakondal I have updated with recent changes please take a look again

@AndriiFilippov
Copy link
Collaborator

AndriiFilippov commented Nov 19, 2024

@alirana01
Now there is only this errors on Debug start.
logsDebugStart.log

I am able to Debug but when I stop process sometime (random) it is throws this error

org.xml.sax.SAXParseException; lineNumber: 65; columnNumber: 1; Content is not allowed in trailing section. occurred while reading launch configuration file: /Users/andriifilippov/workspaceTestthisfancypropernocdgdb1111/.metadata/.plugins/org.eclipse.debug.core/.launches/we21er Debug.launch.
org.xml.sax.SAXParseException; lineNumber: 65; columnNumber: 1; Content is not allowed in trailing section.
	at java.xml/com.sun.org.apache.xerces.internal.util.ErrorHandlerWrapper.createSAXParseException(ErrorHandlerWrapper.java:204)
	at java.xml/com.sun.org.apache.xerces.internal.util.ErrorHandlerWrapper.fatalError(ErrorHandlerWrapper.java:178)
	at java.xml/com.sun.org.apache.xerces.internal.impl.XMLErrorReporter.reportError(XMLErrorReporter.java:400)

and delete Debug config
Screenshot 2024-11-19 at 15 48 41

OR sometime it stops process successfully (Activity Monitor shows no openOCD or GDB process) + port 3333 free as well, but it gets stuck on the next execution. without any error or warning and I am not able to terminate process. just openocd process occupied port 3333.

@alirana01
Copy link
Collaborator Author

@alirana01 Now there is only this errors on Debug start. logsDebugStart.log

I am able to Debug but when I stop process sometime (random) it is throws this error

@kolipakakondal @sigmaaa can you guys please verify this on your macs please I cannot find a consistent way to reproduce this

Refactored the dictionary handling in the process manager to organize
processes based on their respective launches. This improves code
readability and reduces bugs when managing multiple launches.
Copy link

@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

🧹 Outside diff range and nitpick comments (1)
bundles/com.espressif.idf.debug.gdbjtag.openocd/src/com/espressif/idf/debug/gdbjtag/openocd/dsf/LaunchProcessDictionary.java (1)

8-51: Consider enhancing process lifecycle management.

To prevent process leaks and improve debugging capabilities, consider these architectural improvements:

  1. Add automatic cleanup of terminated processes
  2. Implement process status monitoring
  3. Add event notifications for process state changes

Consider implementing an observer pattern to notify interested parties about process state changes:

public interface IProcessListener {
    void onProcessTerminated(String launchName, String procName);
    void onProcessStarted(String launchName, String procName);
}

// Add to LaunchProcessDictionary:
private List<IProcessListener> listeners = new CopyOnWriteArrayList<>();

public void addProcessListener(IProcessListener listener) {
    listeners.add(listener);
}

public void removeProcessListener(IProcessListener listener) {
    listeners.remove(listener);
}

// Implement periodic checking of process status
private void checkProcessStatus() {
    processDictionary.forEach((launchName, processMap) -> {
        processMap.forEach((procName, process) -> {
            if (process.isTerminated()) {
                listeners.forEach(l -> l.onProcessTerminated(launchName, procName));
                removeProcess(launchName, procName);
            }
        });
    });
}
📜 Review details

Configuration used: CodeRabbit UI
Review profile: CHILL

📥 Commits

Reviewing files that changed from the base of the PR and between 122d853 and ea0c653.

📒 Files selected for processing (3)
  • bundles/com.espressif.idf.debug.gdbjtag.openocd/src/com/espressif/idf/debug/gdbjtag/openocd/dsf/Launch.java (9 hunks)
  • bundles/com.espressif.idf.debug.gdbjtag.openocd/src/com/espressif/idf/debug/gdbjtag/openocd/dsf/LaunchProcessDictionary.java (1 hunks)
  • bundles/com.espressif.idf.debug.gdbjtag.openocd/src/com/espressif/idf/debug/gdbjtag/openocd/dsf/process/IdfRuntimeProcess.java (1 hunks)
🚧 Files skipped from review as they are similar to previous changes (1)
  • bundles/com.espressif.idf.debug.gdbjtag.openocd/src/com/espressif/idf/debug/gdbjtag/openocd/dsf/process/IdfRuntimeProcess.java
🔇 Additional comments (3)
bundles/com.espressif.idf.debug.gdbjtag.openocd/src/com/espressif/idf/debug/gdbjtag/openocd/dsf/Launch.java (3)

17-19: LGTM: Well-structured field additions

The new fields and imports are appropriately organized and follow good encapsulation practices.

Also applies to: 61-65


190-191: LGTM: Robust process management implementation

Good implementation of process tracking with proper separation of server and client processes.

Also applies to: 206-208


297-310: LGTM: Well-implemented process cleanup

The cleanup method properly checks process state before termination and handles both server and client processes.

Comment on lines +42 to +50
public IProcess getProcessFromDictionary(String launchName, String procName)
{
if (!processDictionary.containsKey(launchName))
{
return null;
}

return processDictionary.get(launchName).get(procName);
}
Copy link

Choose a reason for hiding this comment

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

🛠️ Refactor suggestion

Add thread safety to getProcessFromDictionary and implement missing operations.

The method needs thread safety and the class is missing essential operations for process management.

  1. Add thread safety to the existing method:
 public IProcess getProcessFromDictionary(String launchName, String procName) {
+    if (launchName == null || procName == null) {
+        throw new IllegalArgumentException("Parameters cannot be null");
+    }
+    
+    synchronized (processDictionary) {
         if (!processDictionary.containsKey(launchName)) {
             return null;
         }
         return processDictionary.get(launchName).get(procName);
+    }
 }
  1. Add these essential operations:
public void removeProcess(String launchName, String procName) {
    if (launchName == null || procName == null) {
        throw new IllegalArgumentException("Parameters cannot be null");
    }
    
    synchronized (processDictionary) {
        if (processDictionary.containsKey(launchName)) {
            processDictionary.get(launchName).remove(procName);
        }
    }
}

public void clearProcesses(String launchName) {
    if (launchName == null) {
        throw new IllegalArgumentException("Launch name cannot be null");
    }
    
    synchronized (processDictionary) {
        processDictionary.remove(launchName);
    }
}

Comment on lines +27 to +40
public void addProcessToDictionary(String launchName, String procName, IProcess process)
{
if (!processDictionary.containsKey(launchName))
{
Map<String, IProcess> processMap = new HashMap<>();
processMap.put(procName, process);
processDictionary.put(launchName, processMap);
return;
}

Map<String, IProcess> processMap = processDictionary.get(launchName);
processMap.put(procName, process);
processDictionary.put(launchName, processMap);
}
Copy link

Choose a reason for hiding this comment

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

🛠️ Refactor suggestion

Optimize addProcessToDictionary method and add parameter validation.

The current implementation has redundant operations and lacks parameter validation.

Apply these improvements:

 public void addProcessToDictionary(String launchName, String procName, IProcess process) {
+    if (launchName == null || procName == null || process == null) {
+        throw new IllegalArgumentException("Parameters cannot be null");
+    }
+    
+    synchronized (processDictionary) {
         if (!processDictionary.containsKey(launchName)) {
             Map<String, IProcess> processMap = new HashMap<>();
             processMap.put(procName, process);
             processDictionary.put(launchName, processMap);
-            return;
         }
-        
-        Map<String, IProcess> processMap = processDictionary.get(launchName);
-        processMap.put(procName, process);
-        processDictionary.put(launchName, processMap);
+        else {
+            processDictionary.get(launchName).put(procName, process);
+        }
+    }
 }
📝 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
public void addProcessToDictionary(String launchName, String procName, IProcess process)
{
if (!processDictionary.containsKey(launchName))
{
Map<String, IProcess> processMap = new HashMap<>();
processMap.put(procName, process);
processDictionary.put(launchName, processMap);
return;
}
Map<String, IProcess> processMap = processDictionary.get(launchName);
processMap.put(procName, process);
processDictionary.put(launchName, processMap);
}
public void addProcessToDictionary(String launchName, String procName, IProcess process)
{
if (launchName == null || procName == null || process == null) {
throw new IllegalArgumentException("Parameters cannot be null");
}
synchronized (processDictionary) {
if (!processDictionary.containsKey(launchName)) {
Map<String, IProcess> processMap = new HashMap<>();
processMap.put(procName, process);
processDictionary.put(launchName, processMap);
}
else {
processDictionary.get(launchName).put(procName, process);
}
}
}

Comment on lines +8 to +25
public class LaunchProcessDictionary
{
private static LaunchProcessDictionary instance;

private Map<String, Map<String, IProcess>> processDictionary;

private LaunchProcessDictionary()
{
processDictionary = new HashMap<>();
}

public static LaunchProcessDictionary getInstance()
{
if(instance == null)
instance = new LaunchProcessDictionary();

return instance;
}
Copy link

Choose a reason for hiding this comment

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

⚠️ Potential issue

Strengthen the singleton implementation for thread safety and security.

The current singleton implementation has several potential issues:

  1. Not thread-safe - multiple instances could be created in a concurrent environment
  2. Vulnerable to reflection and serialization attacks

Consider applying this more robust implementation:

-public class LaunchProcessDictionary
+public class LaunchProcessDictionary implements Serializable {
+    private static final long serialVersionUID = 1L;
+    private static volatile LaunchProcessDictionary instance;
+    private static final Object LOCK = new Object();
 
-    private static LaunchProcessDictionary instance;
     private Map<String, Map<String, IProcess>> processDictionary;
 
     private LaunchProcessDictionary() {
+        if (instance != null) {
+            throw new IllegalStateException("Already instantiated");
+        }
         processDictionary = new HashMap<>();
     }
 
+    protected Object readResolve() {
+        return getInstance();
+    }
 
     public static LaunchProcessDictionary getInstance() {
-        if(instance == null)
-            instance = new LaunchProcessDictionary();
-        return instance;
+        if (instance == null) {
+            synchronized (LOCK) {
+                if (instance == null) {
+                    instance = new LaunchProcessDictionary();
+                }
+            }
+        }
+        return instance;
     }
📝 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
public class LaunchProcessDictionary
{
private static LaunchProcessDictionary instance;
private Map<String, Map<String, IProcess>> processDictionary;
private LaunchProcessDictionary()
{
processDictionary = new HashMap<>();
}
public static LaunchProcessDictionary getInstance()
{
if(instance == null)
instance = new LaunchProcessDictionary();
return instance;
}
public class LaunchProcessDictionary implements Serializable {
private static final long serialVersionUID = 1L;
private static volatile LaunchProcessDictionary instance;
private static final Object LOCK = new Object();
private Map<String, Map<String, IProcess>> processDictionary;
private LaunchProcessDictionary()
{
if (instance != null) {
throw new IllegalStateException("Already instantiated");
}
processDictionary = new HashMap<>();
}
protected Object readResolve() {
return getInstance();
}
public static LaunchProcessDictionary getInstance()
{
if (instance == null) {
synchronized (LOCK) {
if (instance == null) {
instance = new LaunchProcessDictionary();
}
}
}
return instance;
}

Comment on lines +110 to +117
try
{
cleanUpOldLaunchProcesses();
}
catch (CoreException e)
{
e.printStackTrace();
}
Copy link

Choose a reason for hiding this comment

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

🛠️ Refactor suggestion

Improve error handling in initialization

Using printStackTrace() is not recommended for production code. Consider proper logging and error propagation.

 try
 {
     cleanUpOldLaunchProcesses();
 }
 catch (CoreException e)
 {
-    e.printStackTrace();
+    Activator.log(Status.ERROR, "Failed to clean up old launch processes", e);
+    throw new CoreException(new Status(IStatus.ERROR, Activator.PLUGIN_ID,
+        "Failed to initialize launch: " + e.getMessage(), 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
{
cleanUpOldLaunchProcesses();
}
catch (CoreException e)
{
e.printStackTrace();
}
try
{
cleanUpOldLaunchProcesses();
}
catch (CoreException e)
{
Activator.log(Status.ERROR, "Failed to clean up old launch processes", e);
throw new CoreException(new Status(IStatus.ERROR, Activator.PLUGIN_ID,
"Failed to initialize launch: " + e.getMessage(), e));
}

Copy link

@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: 2

🧹 Outside diff range and nitpick comments (3)
bundles/com.espressif.idf.debug.gdbjtag.openocd/src/com/espressif/idf/debug/gdbjtag/openocd/dsf/Launch.java (1)

264-269: Add error aggregation to terminate method

Consider collecting and aggregating errors during termination to provide better feedback.

 @Override
 public void terminate() throws DebugException {
     super.terminate();
+    List<Exception> errors = new ArrayList<>();
+    try {
         LaunchProcessDictionary.getInstance().killAllProcessesInLaunch(getLaunchConfiguration().getName());
+    } catch (Exception e) {
+        errors.add(e);
+        Activator.log(Status.ERROR, "Failed to terminate processes", e);
+    }
+    if (!errors.isEmpty()) {
+        throw new DebugException(new Status(IStatus.ERROR, Activator.PLUGIN_ID,
+            "Failed to terminate one or more processes", errors.get(0)));
+    }
 }
bundles/com.espressif.idf.debug.gdbjtag.openocd/src/com/espressif/idf/debug/gdbjtag/openocd/dsf/LaunchProcessDictionary.java (2)

53-71: Remove the launch entry from processDictionary after terminating processes

After terminating all processes in killAllProcessesInLaunch, consider removing the entry for launchName from processDictionary. This helps prevent potential memory leaks and ensures that the dictionary reflects the current state of active launches.

Apply this diff to remove the entry after processes are terminated:

     for (IProcess process : processDictionary.get(launchName).values())
     {
         try
         {
             process.terminate();
         }
         catch (DebugException e)
         {
             e.printStackTrace();
         }
     }
+    processDictionary.remove(launchName);
 }

66-68: Use logging instead of printing stack traces

Replace e.printStackTrace(); with appropriate logging to handle exceptions more effectively and maintain consistency with best practices.

Apply this diff to utilize a logging framework:

 import org.eclipse.debug.core.DebugException;
+import org.eclipse.core.runtime.Platform;
+import org.eclipse.core.runtime.ILog;
+import org.eclipse.core.runtime.Status;

 public class LaunchProcessDictionary
 {
+    private static final ILog LOGGER = Platform.getLog(LaunchProcessDictionary.class);
     private static LaunchProcessDictionary instance;

     // ...

     catch (DebugException e)
     {
-        e.printStackTrace();
+        LOGGER.log(new Status(Status.ERROR, "YourPluginID", "Failed to terminate process for launch: " + launchName, e));
     }

Ensure to replace "YourPluginID" with the actual plugin ID.

📜 Review details

Configuration used: CodeRabbit UI
Review profile: CHILL

📥 Commits

Reviewing files that changed from the base of the PR and between ea0c653 and 9fde565.

📒 Files selected for processing (2)
  • bundles/com.espressif.idf.debug.gdbjtag.openocd/src/com/espressif/idf/debug/gdbjtag/openocd/dsf/Launch.java (9 hunks)
  • bundles/com.espressif.idf.debug.gdbjtag.openocd/src/com/espressif/idf/debug/gdbjtag/openocd/dsf/LaunchProcessDictionary.java (1 hunks)
🔇 Additional comments (1)
bundles/com.espressif.idf.debug.gdbjtag.openocd/src/com/espressif/idf/debug/gdbjtag/openocd/dsf/Launch.java (1)

61-65: LGTM: Well-structured process management additions

The new instance variables and constants improve process tracking and follow good naming conventions.

Comment on lines +190 to +191
openOcdServerProcess = addServerProcess(Configuration.getGdbServerCommandName(fConfig));
LaunchProcessDictionary.getInstance().addProcessToDictionary(getLaunchConfiguration().getName(), SERVER_PROC_KEY, openOcdServerProcess);
Copy link

Choose a reason for hiding this comment

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

💡 Codebase verification

Process cleanup implementation needs improvement

The verification reveals several concerns with process cleanup:

  • cleanUpOldLaunchProcesses() is called in a try-catch block that only prints stack trace, potentially swallowing critical cleanup errors
  • LaunchProcessDictionary lacks process removal after termination, leading to stale entries in the dictionary
  • Error handling in killAllProcessesInLaunch() only prints stack trace without proper error propagation
  • No cleanup in finally blocks to ensure processes are terminated even if an exception occurs during launch
🔗 Analysis chain

Verify process cleanup in error scenarios

While the process tracking is improved with LaunchProcessDictionary, we should verify proper cleanup in error scenarios.

Also applies to: 206-208

🏁 Scripts executed

The following scripts were executed for the analysis:

Script:

#!/bin/bash
# Description: Check for potential process leaks in error handling paths

# Search for process creation without corresponding cleanup in catch blocks
rg -A 5 "addProcessToDictionary" | rg -B 10 "catch"

# Search for error handling paths that might skip cleanup
ast-grep --pattern 'try {
  $$$
  addProcessToDictionary($$$)
  $$$
} catch ($_) {
  $$$
}'

Length of output: 143


Script:

#!/bin/bash
# Let's try a different approach to verify process cleanup

# Check for process cleanup patterns
rg -B 5 -A 5 "removeProcessFromDictionary|cleanUpOldLaunchProcesses"

# Look for error handling patterns around process management
rg -B 10 -A 5 "openOcdServerProcess|gdbServerProcess" --type java | rg -C 5 "catch|finally"

# Check the LaunchProcessDictionary implementation
fd "LaunchProcessDictionary.java" --exec cat {}

Length of output: 4714

Comment on lines +297 to +310
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();
}
}
Copy link

Choose a reason for hiding this comment

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

🛠️ Refactor suggestion

Add timeout mechanism to process termination

Consider adding a timeout mechanism to prevent hanging during process cleanup.

 private void cleanUpOldLaunchProcesses() throws CoreException {
+    final int TERMINATION_TIMEOUT_MS = 5000; // 5 seconds timeout
+    
     IProcess serverIProcess = LaunchProcessDictionary.getInstance().getProcessFromDictionary(getLaunchConfiguration().getName(), SERVER_PROC_KEY);
     if (serverIProcess != null && !serverIProcess.isTerminated()) {
-        serverIProcess.terminate();
+        serverIProcess.terminate();
+        long startTime = System.currentTimeMillis();
+        while (!serverIProcess.isTerminated() && 
+               System.currentTimeMillis() - startTime < TERMINATION_TIMEOUT_MS) {
+            try {
+                Thread.sleep(100);
+            } catch (InterruptedException e) {
+                Thread.currentThread().interrupt();
+                break;
+            }
+        }
+        if (!serverIProcess.isTerminated()) {
+            throw new CoreException(new Status(IStatus.ERROR, Activator.PLUGIN_ID,
+                "Failed to terminate server process within timeout"));
+        }
     }
     
     IProcess gdbIProcess = LaunchProcessDictionary.getInstance().getProcessFromDictionary(getLaunchConfiguration().getName(), GDB_PROC_KEY);
     if(gdbIProcess != null && !gdbIProcess.isTerminated()) {
-        gdbIProcess.terminate();
+        gdbIProcess.terminate();
+        long startTime = System.currentTimeMillis();
+        while (!gdbIProcess.isTerminated() && 
+               System.currentTimeMillis() - startTime < TERMINATION_TIMEOUT_MS) {
+            try {
+                Thread.sleep(100);
+            } catch (InterruptedException e) {
+                Thread.currentThread().interrupt();
+                break;
+            }
+        }
+        if (!gdbIProcess.isTerminated()) {
+            throw new CoreException(new Status(IStatus.ERROR, Activator.PLUGIN_ID,
+                "Failed to terminate GDB process within timeout"));
+        }
     }
 }
📝 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
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();
}
}
private void cleanUpOldLaunchProcesses() throws CoreException
{
final int TERMINATION_TIMEOUT_MS = 5000; // 5 seconds timeout
IProcess serverIProcess = LaunchProcessDictionary.getInstance().getProcessFromDictionary(getLaunchConfiguration().getName(), SERVER_PROC_KEY);
if (serverIProcess != null && !serverIProcess.isTerminated())
{
serverIProcess.terminate();
long startTime = System.currentTimeMillis();
while (!serverIProcess.isTerminated() &&
System.currentTimeMillis() - startTime < TERMINATION_TIMEOUT_MS) {
try {
Thread.sleep(100);
} catch (InterruptedException e) {
Thread.currentThread().interrupt();
break;
}
}
if (!serverIProcess.isTerminated()) {
throw new CoreException(new Status(IStatus.ERROR, Activator.PLUGIN_ID,
"Failed to terminate server process within timeout"));
}
}
IProcess gdbIProcess = LaunchProcessDictionary.getInstance().getProcessFromDictionary(getLaunchConfiguration().getName(), GDB_PROC_KEY);
if(gdbIProcess != null && !gdbIProcess.isTerminated())
{
gdbIProcess.terminate();
long startTime = System.currentTimeMillis();
while (!gdbIProcess.isTerminated() &&
System.currentTimeMillis() - startTime < TERMINATION_TIMEOUT_MS) {
try {
Thread.sleep(100);
} catch (InterruptedException e) {
Thread.currentThread().interrupt();
break;
}
}
if (!gdbIProcess.isTerminated()) {
throw new CoreException(new Status(IStatus.ERROR, Activator.PLUGIN_ID,
"Failed to terminate GDB process within timeout"));
}
}
}

@AndriiFilippov
Copy link
Collaborator

@alirana01 hi !

Tested:
OS - Mac arm64
ESP-IDF: v5.3.1

I am still encountering issues with starting and stopping the debugging process:
I start the debugger and then stop the process, ensuring I wait for the process to fully terminate.
I monitor the Activity Monitor to confirm that both the OpenOCD and GDB processes have completely stopped. Once both processes are terminated, I attempt to start debugging again, but it works inconsistently. The debugger sometimes freezes, and I cannot stop it by clicking the Stop button.
During this test, I did not add breakpoints or close any files or debugging windows. The sequence was simply: start debugging, stop debugging.

@AndriiFilippov
Copy link
Collaborator

AndriiFilippov commented Nov 22, 2024

  1. A similar situation occurs when port 3333 is occupied by another process. The debugger starts on port 3334, but with inconsistent results. It generally works, but it is not stable.
Screenshot 2024-11-22 at 11 54 29
  1. I cannot see the Variables.
Screenshot 2024-11-22 at 11 23 29
  1. I wanted to ask if the information about the port change is important for the user. If so, I would make the line about the port change more prominent. Now there is only brief note above:
Screenshot 2024-11-22 at 11 51 15

@alirana01
Copy link
Collaborator Author

alirana01 commented Nov 22, 2024

@AndriiFilippov

  1. A similar situation occurs when port 3333 is occupied by another process. The debugger starts on port 3334, but with inconsistent results. It generally works, but it is not stable.
Screenshot 2024-11-22 at 11 54 29

I see that you have 2 openocd processes that will always be inconsistent until you specify different serial IDs manually not an issue

  1. I cannot see the Variables.
Screenshot 2024-11-22 at 11 23 29

Known issue would require restart and manual cleanup of the processes. (sometimes a workspace cleanup)

  1. I wanted to ask if the information about the port change is important for the user. If so, I would make the line about the port change more prominent. Now there is only brief note above:
Screenshot 2024-11-22 at 11 51 15

As we discussed you can create a new ticket because that is not part of the scope for this ticket

@AndriiFilippov
Copy link
Collaborator

AndriiFilippov commented Nov 26, 2024

@alirana01 hi !
Tested under:
OS: Windows 10/11 - Mac arm64 - Linux Ubuntu
Board: ESP32 Ethernet kit

if GDB port 3333 is occupied - then switch to port 3334 ✅
Set Breakpoints ✅
Start / Stop / Terminate / Restart Debugging Session ✅
Step Through Code ✅
Variables not visible 🛑
Evaluate Expressions ✅
In general this PR makes debug process more stable even if it stacks sometime

However, when I set the GDB port to a different number instead of 3333, it still starts GDB on port 3333. 🛑
image
image

@AndriiFilippov
Copy link
Collaborator

@alirana01
apart from that, everything else works.
LGTM 👍

@AndriiFilippov
Copy link
Collaborator

@sigmaaa @kolipakakondal please, review.

Copy link
Collaborator

@sigmaaa sigmaaa left a comment

Choose a reason for hiding this comment

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

LGTM

@AndriiFilippov AndriiFilippov merged commit 6db7938 into master Nov 27, 2024
5 of 6 checks passed
@alirana01 alirana01 deleted the IEP-1265 branch November 28, 2024 09:53
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment
Projects
None yet
Development

Successfully merging this pull request may close these issues.

4 participants