Skip to content

wip: add android studio support#810

Draft
brunoalr wants to merge 23 commits intomainfrom
experimental/android-studio
Draft

wip: add android studio support#810
brunoalr wants to merge 23 commits intomainfrom
experimental/android-studio

Conversation

@brunoalr
Copy link
Copy Markdown
Member

@brunoalr brunoalr commented Jan 12, 2026

PR Type

Enhancement, Bug fix


Description

  • Add LLDB-safe APIs and Android Studio integration support

    • Implement oid_initialize_safe(), oid_set_available_symbols_safe(), oid_plot_buffer_safe() to avoid Python C API calls from event loop thread
    • Add process event listener for Android Studio debugging without stop hooks
    • Cache LLDB mode detection to prevent unsafe Python C API calls from non-main threads
  • Fix Python 3.13 compatibility and string handling issues

    • Replace PyUnicode_AsEncodedString() with PyUnicode_AsUTF8() for safer UTF-8 conversion
    • Use oid_initialize_safe() with C strings instead of Python dictionaries to avoid PyUnicode_CheckExact assertion
    • Normalize script paths using os.fsdecode() for proper Unicode handling
  • Improve process startup and connection reliability

    • Add timeout handling to waitForStart() and process startup with error checking
    • Increase TCP connection timeout to 30 seconds and add retry logic
    • Use QHostAddress::LocalHost for server binding to match client connections
    • Add 500ms delay after process start for Qt event loop initialization
  • Enhance message handling and socket state management

    • Process all available messages in loop instead of single message per call
    • Track connection state to prevent premature quit on failed initial connections
    • Add client connection validation before sending messages

Diagram Walkthrough

flowchart LR
  A["Python Code<br/>LLDB/GDB"] -->|"oid_initialize_safe<br/>C string path"| B["OidBridge<br/>C++"]
  A -->|"oid_set_available_symbols_safe<br/>C string array"| B
  A -->|"oid_plot_buffer_safe<br/>Individual params"| B
  B -->|"get_current_library_path<br/>Unix/Win32"| C["Library Path<br/>Detection"]
  B -->|"PyGILRAII<br/>Cached LLDB mode"| D["GIL Management<br/>Thread-safe"]
  B -->|"Process Listener<br/>Event Queue"| E["Android Studio<br/>Integration"]
  F["Qt Window<br/>oidwindow"] -->|"TCP Connection<br/>LocalHost:Port"| B
Loading

File Walkthrough

Relevant files
Enhancement, bug fix
2 files
oid_bridge.cpp
Add LLDB-safe APIs and improve connection handling             
+493/-43
oidwindow.py
Add safe C API functions and buffer cache management         
+341/-26
Enhancement
6 files
oid_bridge.h
Define safe API functions for LLDB mode                                   
+67/-0   
library_path.h
Add library path detection interface                                         
+42/-0   
library_path_unix.cpp
Implement Unix library path detection                                       
+47/-0   
library_path_win32.cpp
Implement Windows library path detection                                 
+47/-0   
oid.py
Add Android Studio IDE detection and error handling           
+42/-19 
android_studio.py
New Android Studio integration module                                       
+177/-0 
Bug fix
8 files
python_native_interface.cpp
Replace unsafe UTF-8 string conversion method                       
+2/-7     
preprocessor_directives.h
Simplify exception macro for LLDB compatibility                   
+3/-5     
process.cpp
Add timeout and sleep to process startup                                 
+21/-2   
process_win32.cpp
Improve Windows process startup with error handling           
+37/-2   
main_window_initializer.cpp
Use async connection with timer for event loop                     
+14/-3   
message_handler.cpp
Process all messages in loop and track connection state   
+54/-37 
message_handler.h
Add connection state tracking flag                                             
+1/-0     
events.py
Defer symbol list updates and improve window readiness     
+17/-8   
Error handling
1 files
oid_window.cpp
Add exception handling to main window                                       
+26/-17 
Miscellaneous
1 files
main_window.cpp
Add includes for file operations                                                 
+4/-1     
Configuration changes
1 files
CMakeLists.txt
Add platform-specific library path source files                   
+3/-1     
Bug fix, enhancement
1 files
lldbbridge.py
Add process listener and deduplication for stop events     
+186/-49

@qodo-code-review
Copy link
Copy Markdown

qodo-code-review bot commented Jan 12, 2026

PR Compliance Guide 🔍

(Compliance updated until commit 6e25cc4)

Below is a summary of compliance checks for this PR:

Security Compliance
🔴
Hardcoded debug logging

Description: The new "agent log" blocks write detailed runtime data (including filesystem paths and
potentially debugger/session context) to a hardcoded local file
c:\Users\bruno\ws\OpenImageDebugger.cursor\debug_server.log, which can unintentionally
expose sensitive information and introduces an unexpected file-write side effect in
production/debugger environments.
oidwindow.py [29-45]

Referred Code
try:
    with open(r'c:\Users\bruno\ws\OpenImageDebugger\.cursor\debug_server.log', 'a', encoding='utf-8') as f:
        import json
        f.write(json.dumps({"sessionId": "debug-session", "runId": "run1", "hypothesisId": "A", "location": "oidwindow.py:27", "message": "__init__ entry", "data": {"script_path_type": str(type(script_path)), "script_path_repr": repr(script_path), "script_path_value": str(script_path) if script_path else None}, "timestamp": int(time.time() * 1000)}) + '\n')
except: pass
# #endregion
self._bridge = bridge
# Normalize script_path to ensure it's a proper Unicode string for Python 3.13 compatibility
# os.fsdecode() properly handles both bytes and string paths on all platforms
self._script_path = os.fsdecode(script_path) if script_path else str(script_path)
# #region agent log
try:
    with open(r'c:\Users\bruno\ws\OpenImageDebugger\.cursor\debug_server.log', 'a', encoding='utf-8') as f:
        import json
        f.write(json.dumps({"sessionId": "debug-session", "runId": "run1", "hypothesisId": "B", "location": "oidwindow.py:31", "message": "After normalization", "data": {"normalized_type": str(type(self._script_path)), "normalized_repr": repr(self._script_path), "is_str": isinstance(self._script_path, str), "is_unicode": isinstance(self._script_path, str) and hasattr(self._script_path, 'encode')}, "timestamp": int(time.time() * 1000)}) + '\n')
except: pass
# #endregion
DLL search path hijack

Description: On Windows, the code adds DLL search directories from script_path and from the
environment-controlled Qt6_DIR (and its bin), which can enable DLL preloading/hijacking if
an attacker can influence those paths or place malicious DLLs in those directories.
oidwindow.py [49-59]

Referred Code
if PLATFORM_NAME == 'windows':
    os.add_dll_directory(script_path)
    qt6_dir = os.getenv('Qt6_DIR')
    if qt6_dir:
        # Strip trailing semicolons and path separators that might be in the env var
        qt6_dir = qt6_dir.rstrip(';:')
        if os.path.exists(qt6_dir):
            bin_path = os.path.join(qt6_dir, 'bin')
            if os.path.exists(bin_path):
                os.add_dll_directory(bin_path)
Ticket Compliance
🎫 No ticket provided
  • Create ticket/issue
Codebase Duplication Compliance
Codebase context is not defined

Follow the guide to enable codebase context checks.

Custom Compliance
🔴
Generic: Robust Error Handling and Edge Case Management

Objective: Ensure comprehensive error handling that provides meaningful context and graceful
degradation

Status:
Silent exception swallowing: Multiple broad except: pass blocks and swallowed exceptions prevent actionable diagnostics
and can mask runtime failures during initialization and logging.

Referred Code
try:
    with open(r'c:\Users\bruno\ws\OpenImageDebugger\.cursor\debug_server.log', 'a', encoding='utf-8') as f:
        import json
        f.write(json.dumps({"sessionId": "debug-session", "runId": "run1", "hypothesisId": "A", "location": "oidwindow.py:27", "message": "__init__ entry", "data": {"script_path_type": str(type(script_path)), "script_path_repr": repr(script_path), "script_path_value": str(script_path) if script_path else None}, "timestamp": int(time.time() * 1000)}) + '\n')
except: pass
# #endregion
self._bridge = bridge
# Normalize script_path to ensure it's a proper Unicode string for Python 3.13 compatibility
# os.fsdecode() properly handles both bytes and string paths on all platforms
self._script_path = os.fsdecode(script_path) if script_path else str(script_path)
# #region agent log
try:
    with open(r'c:\Users\bruno\ws\OpenImageDebugger\.cursor\debug_server.log', 'a', encoding='utf-8') as f:
        import json
        f.write(json.dumps({"sessionId": "debug-session", "runId": "run1", "hypothesisId": "B", "location": "oidwindow.py:31", "message": "After normalization", "data": {"normalized_type": str(type(self._script_path)), "normalized_repr": repr(self._script_path), "is_str": isinstance(self._script_path, str), "is_unicode": isinstance(self._script_path, str) and hasattr(self._script_path, 'encode')}, "timestamp": int(time.time() * 1000)}) + '\n')
except: pass
# #endregion

Learn more about managing compliance generic rules or creating your own custom rules

Generic: Secure Logging Practices

Objective: To ensure logs are useful for debugging and auditing without exposing sensitive
information like PII, PHI, or cardholder data.

Status:
Hardcoded PII path: The PR writes JSON logs to a hardcoded user-specific Windows path (c:\Users\bruno...) and
logs potentially sensitive local path data (script_path), violating secure logging
practices.

Referred Code
    with open(r'c:\Users\bruno\ws\OpenImageDebugger\.cursor\debug_server.log', 'a', encoding='utf-8') as f:
        import json
        f.write(json.dumps({"sessionId": "debug-session", "runId": "run1", "hypothesisId": "A", "location": "oidwindow.py:27", "message": "__init__ entry", "data": {"script_path_type": str(type(script_path)), "script_path_repr": repr(script_path), "script_path_value": str(script_path) if script_path else None}, "timestamp": int(time.time() * 1000)}) + '\n')
except: pass

Learn more about managing compliance generic rules or creating your own custom rules

Generic: Comprehensive Audit Trails

Objective: To create a detailed and reliable record of critical system actions for security analysis
and compliance.

Status:
Ad-hoc local logging: The PR adds a custom local file log that is not a centralized audit trail and does not
include a user identity, so it is unclear whether required auditability for critical
actions is met.

Referred Code
# #region agent log
try:
    with open(r'c:\Users\bruno\ws\OpenImageDebugger\.cursor\debug_server.log', 'a', encoding='utf-8') as f:
        import json
        f.write(json.dumps({"sessionId": "debug-session", "runId": "run1", "hypothesisId": "A", "location": "oidwindow.py:27", "message": "__init__ entry", "data": {"script_path_type": str(type(script_path)), "script_path_repr": repr(script_path), "script_path_value": str(script_path) if script_path else None}, "timestamp": int(time.time() * 1000)}) + '\n')
except: pass
# #endregion

Learn more about managing compliance generic rules or creating your own custom rules

Generic: Meaningful Naming and Self-Documenting Code

Objective: Ensure all identifiers clearly express their purpose and intent, making code
self-documenting

Status:
Ambiguous state flag: The boolean member _has_gil is used to mean both "GIL already held externally"
and "we acquired it and must release", which is easy to misread and may benefit
from a clearer name.

Referred Code
          _has_gil = true; // Don't try to acquire/release - it's already held
      } else {
          // Normal case: try PyGILState_Ensure()
          _py_gil_state = PyGILState_Ensure();
          _has_gil      = false; // We acquired it, so we need to release it
      }
  }

  PyGILRAII(const PyGILRAII&)            = delete;
  PyGILRAII(PyGILRAII&&)                 = delete;
  PyGILRAII& operator=(const PyGILRAII&) = delete;
  PyGILRAII& operator=(PyGILRAII&&)      = delete;

  ~PyGILRAII() noexcept
  {
      if (!_has_gil) {
          PyGILState_Release(_py_gil_state);
      }
  }

private:


 ... (clipped 3 lines)

Learn more about managing compliance generic rules or creating your own custom rules

Generic: Secure Error Handling

Objective: To prevent the leakage of sensitive system information through error messages while
providing sufficient detail for internal debugging.

Status:
Internal paths in errors: Errors printed to stderr include detailed filesystem exception messages (ex.what()), which
may disclose internal paths depending on distribution and who can view the console output.

Referred Code
} catch (const std::filesystem::filesystem_error& ex) {
    // canonical() failed (e.g., path doesn't exist) - try
    // absolute() as fallback
    std::cerr << "[OpenImageDebugger] Failed to get canonical "
                 "library path: "
              << ex.what() << std::endl;
}

if (!path_resolved) {
    try {
        const auto absolute_path =
            std::filesystem::absolute(library_path);
        const auto lib_dir = absolute_path.parent_path();
        oid_path_to_use    = lib_dir.string();
    } catch (const std::filesystem::filesystem_error& ex) {
        // Both canonical() and absolute() failed - continue
        // with empty oid_path_to_use (will use fallback or fail
        // later if needed)
        const auto error_msg = ex.what();
        std::cerr
            << "[OpenImageDebugger] Failed to get absolute "


 ... (clipped 3 lines)

Learn more about managing compliance generic rules or creating your own custom rules

Generic: Security-First Input Validation and Data Handling

Objective: Ensure all data inputs are validated, sanitized, and handled securely to prevent
vulnerabilities

Status:
Missing range validation: oid_plot_buffer_safe() accepts multiple external numeric parameters (e.g., type,
row_stride, height, channels) without explicit range checks before casting/size
computations, so correctness and safety depend on callers.

Referred Code
void oid_plot_buffer_safe(AppHandler handler,
                          const char* variable_name,
                          const char* display_name,
                          const void* buffer_ptr,
                          size_t buffer_size,
                          int width,
                          int height,
                          int channels,
                          int type,
                          int row_stride,
                          const char* pixel_layout,
                          int transpose_buffer)
{

    const auto app = static_cast<OidBridge*>(handler);

    if (app == nullptr) [[unlikely]] {
        return;
    }

    if (buffer_ptr == nullptr) [[unlikely]] {


 ... (clipped 30 lines)

Learn more about managing compliance generic rules or creating your own custom rules

  • Update
Compliance status legend 🟢 - Fully Compliant
🟡 - Partial Compliant
🔴 - Not Compliant
⚪ - Requires Further Human Verification
🏷️ - Compliance label

Previous compliance checks

Compliance check up to commit f4c27d7
Security Compliance
Unauthenticated TCP service

Description: The TCP server listens on QHostAddress::Any without any authentication/authorization,
which can expose the debugger control channel to other hosts on the network and allow
unauthorized clients to connect and send UI messages (e.g., triggering buffer
requests/updates).
oid_bridge.cpp [187-223]

Referred Code
        if (!server_.listen(QHostAddress::Any)) {
            std::cerr << "[OpenImageDebugger] Could not start TCP server"
                      << std::endl;
            return false;
        }

        // H14: Fallback to detect library path when oid_path_ is empty (LLDB
        // Python bug - __file__ not set)
        std::string oid_path_to_use = this->oid_path_;
        if (oid_path_to_use.empty()) {

#if defined(__unix__) || defined(__APPLE__)
            // Use dladdr to get the path of the current library
            Dl_info info;
            if (dladdr(reinterpret_cast<void*>(&oid_initialize), &info) != 0 &&
                info.dli_fname != nullptr) {
                char resolved_path[PATH_MAX];
                if (realpath(info.dli_fname, resolved_path) != nullptr) {
                    // Get directory of the library
                    char* lib_dir   = dirname(resolved_path);
                    oid_path_to_use = lib_dir;


 ... (clipped 16 lines)
Integer overflow OOB

Description: oid_plot_buffer_safe computes buff_size_expected via chained multiplications (row_stride *
height * channels * type_size) without overflow checks, so an integer overflow could make
the size check pass incorrectly and lead to out-of-bounds reads when the span is later
used for plotting/sending.
oid_bridge.cpp [891-942]

Referred Code
void oid_plot_buffer_safe(AppHandler handler,
                          const char* variable_name,
                          const char* display_name,
                          const void* buffer_ptr,
                          size_t buffer_size,
                          int width,
                          int height,
                          int channels,
                          int type,
                          int row_stride,
                          const char* pixel_layout,
                          int transpose_buffer)
{

    const auto app = static_cast<OidBridge*>(handler);

    if (app == nullptr) [[unlikely]] {
        return;
    }

    if (buffer_ptr == nullptr) [[unlikely]] {


 ... (clipped 31 lines)
Path-based execution

Description: The UI binary path is derived from oid_path_ (potentially influenced by Python-provided
oid_path or by dladdr/realpath fallback) and then used to spawn oidwindow, which can
enable unintended execution of a different binary if the path is attacker-controlled or
points to a tampered directory.
oid_bridge.cpp [193-224]

Referred Code
        // H14: Fallback to detect library path when oid_path_ is empty (LLDB
        // Python bug - __file__ not set)
        std::string oid_path_to_use = this->oid_path_;
        if (oid_path_to_use.empty()) {

#if defined(__unix__) || defined(__APPLE__)
            // Use dladdr to get the path of the current library
            Dl_info info;
            if (dladdr(reinterpret_cast<void*>(&oid_initialize), &info) != 0 &&
                info.dli_fname != nullptr) {
                char resolved_path[PATH_MAX];
                if (realpath(info.dli_fname, resolved_path) != nullptr) {
                    // Get directory of the library
                    char* lib_dir   = dirname(resolved_path);
                    oid_path_to_use = lib_dir;

                } else {
                    // realpath failed - continue with empty oid_path_to_use
                    // (will use fallback or fail later if needed)
                }
            } else {


 ... (clipped 11 lines)
Ticket Compliance
🎫 No ticket provided
  • Create ticket/issue
Codebase Duplication Compliance
Codebase context is not defined

Follow the guide to enable codebase context checks.

Custom Compliance
🔴
Generic: Robust Error Handling and Edge Case Management

Objective: Ensure comprehensive error handling that provides meaningful context and graceful
degradation

Status:
Swallowed exceptions: Multiple new except Exception: pass blocks silently ignore failures (listener
registration/thread loop), making runtime issues non-actionable and hard to debug.

Referred Code
                except Exception:
                    time.sleep(0.1)

        self._listener_thread = threading.Thread(target=event_monitor_thread,
                                                 daemon=True)
        self._listener_thread.start()
except Exception:
    pass

Learn more about managing compliance generic rules or creating your own custom rules

Generic: Secure Logging Practices

Objective: To ensure logs are useful for debugging and auditing without exposing sensitive
information like PII, PHI, or cardholder data.

Status:
Unstructured logging: New error logs are emitted via std::cerr with free-form strings (not structured logging),
reducing auditability and making monitoring/parsing difficult.

Referred Code
std::cerr << "[Error] Failed to read message header: "
          << socket_.errorString().toStdString() << std::endl;

// Handle critical errors that indicate connection is broken
if (error == QAbstractSocket::RemoteHostClosedError ||
    error == QAbstractSocket::NetworkError ||
    error == QAbstractSocket::ConnectionRefusedError ||
    error == QAbstractSocket::SocketTimeoutError) [[unlikely]] {
    std::cerr << "[Error] Critical socket error detected. Closing "
                 "connection."
              << std::endl;
    socket_.close();

Learn more about managing compliance generic rules or creating your own custom rules

@qodo-code-review
Copy link
Copy Markdown

qodo-code-review bot commented Jan 12, 2026

PR Code Suggestions ✨

Latest suggestions up to 6e25cc4

CategorySuggestion                                                                                                                                    Impact
Security
Gate debug file logging

Remove the hardcoded debug logging that writes to a user-specific path
(c:\Users\bruno...). If logging is needed, make it opt-in via an environment
variable and use a generic path.

resources/oidscripts/oidwindow.py [29-33]

-try:
-    with open(r'c:\Users\bruno\ws\OpenImageDebugger\.cursor\debug_server.log', 'a', encoding='utf-8') as f:
+if os.getenv("OID_DEBUG_LOG") == "1":
+    try:
         import json
-        f.write(json.dumps({"sessionId": "debug-session", "runId": "run1", "hypothesisId": "A", "location": "oidwindow.py:27", "message": "__init__ entry", "data": {"script_path_type": str(type(script_path)), "script_path_repr": repr(script_path), "script_path_value": str(script_path) if script_path else None}, "timestamp": int(time.time() * 1000)}) + '\n')
-except: pass
+        log_path = os.path.join(os.path.expanduser("~"), ".oid_debug.log")
+        with open(log_path, "a", encoding="utf-8") as f:
+            f.write(
+                json.dumps(
+                    {
+                        "location": "oidwindow.py:__init__",
+                        "message": "__init__ entry",
+                        "data": {
+                            "script_path_type": str(type(script_path)),
+                            "script_path_repr": repr(script_path),
+                        },
+                        "timestamp": int(time.time() * 1000),
+                    }
+                )
+                + "\n"
+            )
+    except Exception:
+        pass
  • Apply / Chat
Suggestion importance[1-10]: 9

__

Why: The suggestion correctly identifies hardcoded, user-specific debug logging code that will fail for other users and leaks local file paths.

High
Possible issue
Resolve correct module path

Fix the library path detection on Windows by using GetModuleHandleExA with
GET_MODULE_HANDLE_EX_FLAG_FROM_ADDRESS to get the handle of the current DLL,
instead of GetModuleHandle(nullptr) which incorrectly returns the handle of the
host executable.

src/oidbridge/library_path_win32.cpp [35-45]

 std::filesystem::path get_current_library_path()
 {
-    // Use GetModuleFileName to get the path of the current library
-    if (HMODULE hModule = GetModuleHandle(nullptr); hModule != nullptr) {
+    HMODULE hModule = nullptr;
+    if (GetModuleHandleExA(GET_MODULE_HANDLE_EX_FLAG_FROM_ADDRESS |
+                               GET_MODULE_HANDLE_EX_FLAG_UNCHANGED_REFCOUNT,
+                           reinterpret_cast<LPCSTR>(&get_current_library_path),
+                           &hModule) != 0 &&
+        hModule != nullptr) {
         char module_path[MAX_PATH];
         if (GetModuleFileNameA(hModule, module_path, MAX_PATH) != 0) {
             return std::filesystem::path{module_path};
         }
     }
     return std::filesystem::path{};
 }
  • Apply / Chat
Suggestion importance[1-10]: 9

__

Why: The suggestion correctly identifies a critical bug where GetModuleHandle(nullptr) gets the host executable's path instead of the current library's path, which defeats the purpose of the fallback logic.

High
Correct buffer size validation

Correct the buffer size validation in oid_plot_buffer_safe by removing the
multiplication by channels from the expected size calculation, as row_stride is
typically in pixels and already accounts for channel data within the row.

src/oidbridge/oid_bridge.cpp [1080-1086]

-const auto buff_size_expected =
-    std::size_t{static_cast<size_t>(row_stride * height * channels) *
-                oid::type_size(static_cast<oid::BufferType>(type))};
-
-if (buff_span.size() < buff_size_expected) [[unlikely]] {
+if (buffer_size == 0 || width <= 0 || height <= 0 || channels <= 0 || row_stride <= 0) {
     return;
 }
 
+const auto type_sz = oid::type_size(static_cast<oid::BufferType>(type));
+const auto min_expected =
+    static_cast<size_t>(row_stride) * static_cast<size_t>(height) *
+    static_cast<size_t>(type_sz);
+
+if (buff_span.size() < min_expected) [[unlikely]] {
+    return;
+}
+
  • Apply / Chat
Suggestion importance[1-10]: 8

__

Why: The suggestion correctly identifies that the buffer size validation logic is likely flawed by multiplying by channels, which could cause valid buffers to be rejected.

Medium
Ensure GIL when raising

Restore GIL protection in the RAISE_PY_EXCEPTION macro by wrapping
PyErr_SetString with PyGILState_Ensure and PyGILState_Release to prevent crashes
when raising exceptions from non-Python threads.

src/debuggerinterface/preprocessor_directives.h [29-32]

-#define RAISE_PY_EXCEPTION(exception_type, msg) \
-    do {                                        \
-        PyErr_SetString(exception_type, msg);   \
+#define RAISE_PY_EXCEPTION(exception_type, msg)        \
+    do {                                               \
+        PyGILState_STATE gstate = PyGILState_Ensure(); \
+        PyErr_SetString(exception_type, msg);          \
+        PyGILState_Release(gstate);                    \
     } while (0)
  • Apply / Chat
Suggestion importance[1-10]: 8

__

Why: The suggestion correctly identifies that removing GIL management from the RAISE_PY_EXCEPTION macro makes it unsafe, as it could be called from threads without the GIL, leading to crashes.

Medium
Use normalized DLL search path

Use the normalized self._script_path when calling os.add_dll_directory() instead
of the raw script_path argument to ensure the correct path type is used.

resources/oidscripts/oidwindow.py [49-58]

 if PLATFORM_NAME == 'windows':
-    os.add_dll_directory(script_path)
+    os.add_dll_directory(self._script_path)
     qt6_dir = os.getenv('Qt6_DIR')
     if qt6_dir:
-        # Strip trailing semicolons and path separators that might be in the env var
         qt6_dir = qt6_dir.rstrip(';:')
         if os.path.exists(qt6_dir):
             bin_path = os.path.join(qt6_dir, 'bin')
             if os.path.exists(bin_path):
                 os.add_dll_directory(bin_path)
  • Apply / Chat
Suggestion importance[1-10]: 7

__

Why: The suggestion correctly points out that the normalized self._script_path should be used for consistency, as the PR explicitly normalizes script_path to handle potential type issues.

Medium
  • Update

Previous suggestions

Suggestions up to commit f4c27d7
CategorySuggestion                                                                                                                                    Impact
Possible issue
Reset listener flag on process exit

In lldbbridge.py, reset the _listener_registered flag to False within
event_monitor_thread when the debugged process exits to allow re-registration in
subsequent debug sessions.

resources/oidscripts/debuggers/lldbbridge.py [262-286]

 def event_monitor_thread():
     event = lldb.SBEvent()
     last_state = None
     while True:
         try:
             if self._process_listener.WaitForEventForBroadcasterWithType(
                     1, broadcaster,
                     lldb.SBProcess.eBroadcastBitStateChanged, event):
                 if process.IsValid():
                     state = process.GetState()
 
                     if state == lldb.eStateStopped and last_state != lldb.eStateStopped:
                         from oidscripts.logger import log
                         log.debug('LldbBridge: Process stopped, queuing stop event')
                         with self._lock:
                             self._event_queue.append('stop')
                     last_state = state
 
                     if state == lldb.eStateExited or state == lldb.eStateDetached:
+                        self._listener_registered = False
                         break
             else:
                 if not process.IsValid():
+                    self._listener_registered = False
                     break
         except Exception:
             time.sleep(0.1)
Suggestion importance[1-10]: 8

__

Why: The suggestion correctly identifies a bug in the new listener logic where the listener would not be re-registered for a new debug session, and the proposed fix is accurate.

Medium
Allow re-initialization of the event listener

In android_studio.py, modify _register_process_listener to be re-entrant for
multiple debug sessions by checking if an event monitor thread is already
running before starting a new one.

resources/oidscripts/ides/android_studio.py [86-110]

-def event_monitor_thread():
-    import time
-    event = lldb.SBEvent()
-    last_state = None
-    while True:
-        try:
-            if listener.WaitForEventForBroadcasterWithType(1, broadcaster,
-                                                            lldb.SBProcess.eBroadcastBitStateChanged,
-                                                            event):
-                if process.IsValid():
-                    state = process.GetState()
+def _register_process_listener(process, event_handler, debugger):
+    # type: (lldb.SBProcess, OpenImageDebuggerEvents, BridgeInterface) -> None
+    """
+    Register an event listener for the given process to detect state changes.
+    """
+    import lldb
+    import threading
+    from oidscripts.logger import log
 
-                    if state == lldb.eStateStopped and last_state != lldb.eStateStopped:
-                        log.debug('Android Studio: Process stopped, queuing stop handler')
-                        debugger.queue_request(lambda: event_handler.stop_handler())
-                    last_state = state
+    # Use listener from the debugger bridge if it exists
+    if hasattr(debugger, '_process_listener') and debugger._process_listener is not None:
+        listener = debugger._process_listener
+    else:
+        listener = lldb.SBListener('oid-android-studio-listener')
+        if hasattr(debugger, '_process_listener'):
+            debugger._process_listener = listener
 
-                    if state == lldb.eStateExited or state == lldb.eStateDetached:
-                        break
-            else:
-                if not process.IsValid():
-                    break
-        except Exception as e:
-            log.debug('Android Studio: Event listener error: %s', str(e))
-            time.sleep(0.1)
+    # Check if a monitor thread is already running for this debugger instance
+    if hasattr(debugger, '_listener_thread') and debugger._listener_thread is not None and debugger._listener_thread.is_alive():
+        log.debug('Android Studio: Event monitor thread already running.')
+        return
 
+    try:
+        broadcaster = process.GetBroadcaster()
+        if not broadcaster.IsValid():
+            log.warning('Android Studio: Process broadcaster is not valid')
+            return
+
+        rc = broadcaster.AddListener(listener,
+                                    lldb.SBProcess.eBroadcastBitStateChanged)
+        if not rc:
+            log.warning('Android Studio: Failed to add listener to process broadcaster')
+            return
+
+        log.info('Android Studio: Event listener registered successfully')
+
+        def event_monitor_thread():
+            import time
+            event = lldb.SBEvent()
+            last_state = None
+            while True:
+                try:
+                    if listener.WaitForEventForBroadcasterWithType(1, broadcaster,
+                                                                    lldb.SBProcess.eBroadcastBitStateChanged,
+                                                                    event):
+                        if process.IsValid():
+                            state = process.GetState()
+
+                            if state == lldb.eStateStopped and last_state != lldb.eStateStopped:
+                                log.debug('Android Studio: Process stopped, queuing stop handler')
+                                debugger.queue_request(lambda: event_handler.stop_handler())
+                            last_state = state
+
+                            if state == lldb.eStateExited or state == lldb.eStateDetached:
+                                break
+                    else:
+                        if not process.IsValid():
+                            break
+                except Exception as e:
+                    log.debug('Android Studio: Event listener error: %s', str(e))
+                    time.sleep(0.1)
+            log.info('Android Studio: Event monitor thread stopped.')
+
+
+        monitor_thread = threading.Thread(target=event_monitor_thread,
+                                          daemon=True)
+        # Store thread on the debugger bridge instance
+        if hasattr(debugger, '_listener_thread'):
+            debugger._listener_thread = monitor_thread
+        monitor_thread.start()
+        log.info('Android Studio: Event monitor thread started')
+
+    except Exception as e:
+        log.warning('Android Studio: Failed to register process listener: %s',
+                   str(e))
+
Suggestion importance[1-10]: 8

__

Why: The suggestion correctly identifies a bug where the event monitor for Android Studio would not work for subsequent debug sessions and proposes a valid fix to make it re-entrant.

Medium
Define ctypes restype for buffers

In oidwindow.py, define the argtypes and restype for the
oid_get_observed_buffers C function to ensure ctypes correctly interprets the
return value as a Python object.

resources/oidwindow.py [212-230]

-# after loading the C++ library and defining argtypes for plot_buffer...
-# missing definition for oid_get_observed_buffers
+self._lib.oid_get_observed_buffers.argtypes = [ctypes.c_void_p]
+self._lib.oid_get_observed_buffers.restype = ctypes.py_object
Suggestion importance[1-10]: 7

__

Why: The suggestion correctly identifies that the restype for oid_get_observed_buffers is missing, which is necessary for correct ctypes behavior, though the PR's logic already handles the None case.

Medium
General
Prevent partial header reads

In decode_incoming_messages, check if socket_.bytesAvailable() is greater than
or equal to the header size before attempting to read the header to prevent
partial reads.

src/ui/main_window/message_processing.cpp [276-315]

-while (socket_.bytesAvailable() > 0) {
-    auto header = MessageType{};
-    if (!socket_.read(std::bit_cast<char*>(&header),
-                      static_cast<qint64>(sizeof(header)))) [[unlikely]] {
+while (socket_.bytesAvailable() >= static_cast<qint64>(sizeof(MessageType))) {
+    MessageType header;
+    qint64 hdr_size = static_cast<qint64>(sizeof(header));
+    if (!socket_.read(reinterpret_cast<char*>(&header), hdr_size)) [[unlikely]] {
         // ...
     }
     socket_.waitForReadyRead(100);
     switch (header) {
     ...
Suggestion importance[1-10]: 6

__

Why: The suggestion correctly identifies a potential issue with reading partial message headers from the socket and provides a robust fix to prevent read errors.

Low
Limit exception catch and log

In lldb_stop_hook_handler, replace the broad except Exception with more specific
exceptions like ImportError and AttributeError, and log other unexpected errors
for better diagnostics.

resources/oid.py [35-41]

 def lldb_stop_hook_handler(debugger, command, result, dict):
     try:
         from oidscripts.debuggers import lldbbridge
         if lldbbridge.instance is not None:
             lldbbridge.instance.stop_hook(debugger, command, result, dict)
-    except Exception:
+    except ImportError:
+        # LLDB bridge module not available
         pass
+    except AttributeError as e:
+        log.debug('LLDB bridge missing instance: %s', e)
Suggestion importance[1-10]: 5

__

Why: The suggestion correctly points out that catching broad exceptions silently is bad practice and proposes a more specific and robust error handling approach.

Low

@brunoalr brunoalr force-pushed the experimental/android-studio branch from c286da8 to aa4d940 Compare January 13, 2026 20:22
This reverts commit aa4d940.
- Prevent Android Studio integration from registering duplicate listener
  when using LldbBridge (which already has its own listener)
- Fix _check_frame_modification to suppress frame change events on
  initial stop when listener is registered (listener handles stops)
- Fix event_loop to properly copy event queue list to avoid reference issues

This fixes duplicate stop_handler() calls that occurred when debugging
with Android Studio, ensuring each process stop results in exactly one
stop_handler() invocation.
- Add __pycache__/ and *.pyc to .gitignore
- Remove tracked .pyc files from git repository
… support and essential bug fixes

- Remove previous_session_available_vars and last_known_available_vars persistence
- Revert TODO comment cleanup in process_unix.cpp
- Add __pycache__/ and *.pyc to .gitignore
- Keep all essential bug fixes (GIL, string handling, timeout, socket errors)
- Keep Android Studio support and LLDB-safe APIs
- Refactor library path detection to use OS-agnostic std::filesystem
- Create platform-specific implementations (library_path_unix.cpp, library_path_win32.cpp)
- Remove all #ifdef directives from main code
- Use std::filesystem::path instead of std::string for type safety
- Add proper exception handling with error logging
- Update CMakeLists.txt to conditionally compile platform-specific files
@brunoalr
Copy link
Copy Markdown
Member Author

/describe

@qodo-code-review
Copy link
Copy Markdown

PR Description updated to latest commit (de6a28a)

- Remove unreferenced variable 'e' in catch clause (oid_bridge.cpp:592)
- Fix formatting in RAISE_PY_EXCEPTION macro (preprocessor_directives.h)

Fixes Windows build error C4101 and formatting check failure
@brunoalr
Copy link
Copy Markdown
Member Author

brunoalr commented Feb 3, 2026

/describe
--pr_description.extra_instructions="
For the title, use the format [type]: [summary]
"

@brunoalr
Copy link
Copy Markdown
Member Author

brunoalr commented Feb 3, 2026

/review

@brunoalr
Copy link
Copy Markdown
Member Author

brunoalr commented Feb 3, 2026

/compliance

@qodo-code-review
Copy link
Copy Markdown

PR Description updated to latest commit (6e25cc4)

@brunoalr
Copy link
Copy Markdown
Member Author

brunoalr commented Feb 3, 2026

/improve

@qodo-code-review
Copy link
Copy Markdown

PR Reviewer Guide 🔍

Here are some key observations to aid the review process:

⏱️ Estimated effort to review: 5 🔵🔵🔵🔵🔵
🧪 No relevant tests
🔒 Security concerns

Sensitive information exposure:
resources/oidscripts/oidwindow.py writes logs to a hardcoded path (c:\Users\bruno\...) that includes a username and local workspace structure. This can leak sensitive local information and may write files unexpectedly on user systems.

⚡ Recommended focus areas for review

Sensitive Logging

The new debug “agent log” blocks write JSON logs to a hardcoded, user-specific absolute Windows path and swallow all exceptions. This can leak local filesystem/usernames, create unexpected files in production, and break on non-Windows machines. Replace with the project logger, guard behind an explicit debug flag/env var, and avoid hardcoded paths.

        # #region agent log
        try:
            with open(r'c:\Users\bruno\ws\OpenImageDebugger\.cursor\debug_server.log', 'a', encoding='utf-8') as f:
                import json
                f.write(json.dumps({"sessionId": "debug-session", "runId": "run1", "hypothesisId": "A", "location": "oidwindow.py:27", "message": "__init__ entry", "data": {"script_path_type": str(type(script_path)), "script_path_repr": repr(script_path), "script_path_value": str(script_path) if script_path else None}, "timestamp": int(time.time() * 1000)}) + '\n')
        except: pass
        # #endregion
        self._bridge = bridge
        # Normalize script_path to ensure it's a proper Unicode string for Python 3.13 compatibility
        # os.fsdecode() properly handles both bytes and string paths on all platforms
        self._script_path = os.fsdecode(script_path) if script_path else str(script_path)
        # #region agent log
        try:
            with open(r'c:\Users\bruno\ws\OpenImageDebugger\.cursor\debug_server.log', 'a', encoding='utf-8') as f:
                import json
                f.write(json.dumps({"sessionId": "debug-session", "runId": "run1", "hypothesisId": "B", "location": "oidwindow.py:31", "message": "After normalization", "data": {"normalized_type": str(type(self._script_path)), "normalized_repr": repr(self._script_path), "is_str": isinstance(self._script_path, str), "is_unicode": isinstance(self._script_path, str) and hasattr(self._script_path, 'encode')}, "timestamp": int(time.time() * 1000)}) + '\n')
        except: pass
        # #endregion
        # Cache of observed buffer names for LLDB mode where get_observed_buffers() doesn't work
        self._observed_buffers_cache = set()

        if PLATFORM_NAME == 'windows':
            os.add_dll_directory(script_path)
            qt6_dir = os.getenv('Qt6_DIR')
            if qt6_dir:
                # Strip trailing semicolons and path separators that might be in the env var
                qt6_dir = qt6_dir.rstrip(';:')
                if os.path.exists(qt6_dir):
                    bin_path = os.path.join(qt6_dir, 'bin')
                    if os.path.exists(bin_path):
                        os.add_dll_directory(bin_path)

        # Request ctypes to load libGL before the native oidwindow does; this
        # fixes an issue on Ubuntu machines with nvidia drivers. For more
        # information, please refer to
        # https://github.com/csantosbh/gdb-imagewatch/issues/28
        lib_opengl = 'opengl32' if PLATFORM_NAME == 'windows' else 'GL'
        ctypes.CDLL(ctypes.util.find_library(lib_opengl), ctypes.RTLD_GLOBAL)

        # Load OpenImageDebugger library and set up its API
        self._lib = ctypes.cdll.LoadLibrary(
            script_path + '/' + OpenImageDebuggerWindow.__get_library_name())

        # lib OID API
        self._lib.oid_initialize.argtypes = [FETCH_BUFFER_CBK_TYPE, ctypes.py_object]
        self._lib.oid_initialize.restype = ctypes.c_void_p

        # Safe version that takes C string instead of dictionary (Python 3.13+ compatible)
        self._lib.oid_initialize_safe.argtypes = [FETCH_BUFFER_CBK_TYPE, ctypes.c_char_p]
        self._lib.oid_initialize_safe.restype = ctypes.c_void_p

        self._lib.oid_cleanup.argtypes = [ctypes.c_void_p]
        self._lib.oid_cleanup.restype = None

        self._lib.oid_exec.argtypes = [ctypes.c_void_p]
        self._lib.oid_exec.restype = None

        self._lib.oid_is_window_ready.argtypes = [ctypes.c_void_p]
        self._lib.oid_is_window_ready.restype = ctypes.c_bool

        self._lib.oid_get_observed_buffers.argtypes = [ctypes.c_void_p]
        self._lib.oid_get_observed_buffers.restype = ctypes.py_object

        # Safe version that takes C string array instead of Python list
        self._lib.oid_set_available_symbols_safe.argtypes = [
            ctypes.c_void_p,  # handler
            ctypes.POINTER(ctypes.c_char_p),  # available_vars array
            ctypes.c_size_t  # count
        ]
        self._lib.oid_set_available_symbols_safe.restype = None

        self._lib.oid_run_event_loop.argtypes = [ctypes.c_void_p]
        self._lib.oid_run_event_loop.restype = None

        # Safe version that takes individual parameters
        self._lib.oid_plot_buffer_safe.argtypes = [
            ctypes.c_void_p,  # handler
            ctypes.c_char_p,  # variable_name
            ctypes.c_char_p,  # display_name
            ctypes.c_void_p,  # buffer_ptr
            ctypes.c_size_t,  # buffer_size
            ctypes.c_int,  # width
            ctypes.c_int,  # height
            ctypes.c_int,  # channels
            ctypes.c_int,  # type
            ctypes.c_int,  # row_stride
            ctypes.c_char_p,  # pixel_layout
            ctypes.c_int,  # transpose_buffer
        ]
        self._lib.oid_plot_buffer_safe.restype = None

#UI handler
        self._native_handler = None
        self._event_loop_wait_time = 1.0/30.0
        self._previous_evloop_time = OpenImageDebuggerWindow.__get_time_ms()
        self._plot_variable_c_callback = FETCH_BUFFER_CBK_TYPE(self.plot_variable)


    @staticmethod
    def __get_time_ms():
        return int(time.time() * 1000.0)

    @staticmethod
    def __get_library_name():
        """
        Return the name of the binary library, including its extension, which
        is platform dependent.
        """
        if PLATFORM_NAME == 'linux' or PLATFORM_NAME == 'darwin':
            return 'liboidbridge.so'
        elif PLATFORM_NAME == 'windows':
            return 'oidbridge.dll'
        return None

    def plot_variable(self, requested_symbol):
        """
        Plot a variable whose name is 'requested_symbol'.

        This will result in the creation of a callable object of type
        DeferredVariablePlotter, where the actual code for plotting the buffer
        will be executed. This object will be given to the debugger bridge so
        that it can schedule its execution in a thread safe context.
        """
        if self._bridge is None:
            log.info(f"Could not plot symbol {requested_symbol}: Not a debugging session")
            return 0

        try:
            if not isinstance(requested_symbol, str):
                variable = requested_symbol.decode('utf-8')
            else:
                variable = requested_symbol

            # Add to cache immediately - will be removed if plotting fails
            self._observed_buffers_cache.add(variable)

            plot_callable = DeferredVariablePlotter(variable,
                                                    self._lib,
                                                    self._bridge,
                                                    self._native_handler,
                                                    self)  # Pass window reference

            self._bridge.queue_request(plot_callable)

            return 1
        except Exception as err:
            log.error('Could not plot variable')
            log.error(err)
            # Remove from cache if plotting failed
            if isinstance(requested_symbol, str):
                self._observed_buffers_cache.discard(requested_symbol)
            else:
                self._observed_buffers_cache.discard(requested_symbol.decode('utf-8'))

        return 0

    def is_ready(self):
        """
        Returns True if the OpenImageDebugger window has been loaded; False otherwise.
        """
        if self._native_handler is None:
            return False

        return self._lib.oid_is_window_ready(self._native_handler)

    def terminate(self):
        """
        Request OID to terminate application and close all windows
        """
        self._lib.oid_cleanup(self._native_handler)

    def set_available_symbols(self, observable_symbols):
        """
        Set the autocomplete list of symbols with the list of string
        'observable_symbols'
        """
        # Perform two-level sorting:
        # 1. By amount of nodes.
        # 2. By variable name.
        sorted_observable_symbols = sorted(observable_symbols, key=lambda symbol: (symbol.count('.'), symbol))

        # Always use the safe version that takes C string array
        # instead of Python list to avoid Python C API calls from event loop thread
        count = len(sorted_observable_symbols)

        # Encode all strings to bytes and keep references to prevent GC
        encoded_symbols = [
            symbol.encode('utf-8') if isinstance(symbol, str) else symbol
            for symbol in sorted_observable_symbols
        ]

        # Create array of C strings
        c_string_array = (ctypes.c_char_p * count)()
        for i, encoded in enumerate(encoded_symbols):
            c_string_array[i] = encoded

        # Keep references to prevent garbage collection
        if not hasattr(self, '_symbol_array_refs'):
            self._symbol_array_refs = []
        self._symbol_array_refs.append(c_string_array)
        self._symbol_array_refs.append(encoded_symbols)

        self._lib.oid_set_available_symbols_safe(
            self._native_handler, c_string_array, count)

    def run_event_loop(self):
        """
        Run the debugger-side event loop, which consists of checking for new
        user requests coming from the UI
        """
        current_time = OpenImageDebuggerWindow.__get_time_ms()
        dt = current_time - self._previous_evloop_time
        if dt > self._event_loop_wait_time:
            self._lib.oid_run_event_loop(self._native_handler)
            self._previous_evloop_time = current_time

            # Schedule next run of the event loop
        self._bridge.queue_request(self.run_event_loop)

    def get_observed_buffers(self):
        """
        Get a list with the currently observed symbols in the OID window

        In LLDB mode, oid_get_observed_buffers returns Py_None because Python
        objects can't be created. In this case, we fall back to the Python-side
        cache of observed buffers.
        """
        import sys
        is_lldb_mode = 'lldb' in sys.modules

        # In LLDB mode, use the cache since C++ can't return the list
        if is_lldb_mode:
            return list(self._observed_buffers_cache)

        # Non-LLDB mode: try to get from C++ side
        try:
            result = self._lib.oid_get_observed_buffers(self._native_handler)
            # If result is None or not a list, fall back to cache
            if result is None or not isinstance(result, list):
                return list(self._observed_buffers_cache)
            # Update cache with results from C++ side
            self._observed_buffers_cache = set(result)
            return result
        except Exception:
            # If call fails, fall back to cache
            return list(self._observed_buffers_cache)

    def initialize_window(self):
        # Initialize OID lib
        try:
            # script_path is already normalized to Unicode string in __init__
            # #region agent log
            # Try creating a fresh string to ensure it's a proper Unicode string for Python 3.13
            script_path_fresh = str(self._script_path)
            # Also try creating from bytes to ensure proper encoding
            if isinstance(self._script_path, str):
                script_path_bytes = self._script_path.encode('utf-8')
                script_path_from_bytes = script_path_bytes.decode('utf-8')
            else:
                script_path_from_bytes = str(self._script_path)
            try:
                with open(r'c:\Users\bruno\ws\OpenImageDebugger\.cursor\debug_server.log', 'a', encoding='utf-8') as f:
                    import json
                    f.write(json.dumps({"sessionId": "debug-session", "runId": "run1", "hypothesisId": "C", "location": "oidwindow.py:275", "message": "Before dict creation", "data": {"original_type": str(type(self._script_path)), "fresh_str_type": str(type(script_path_fresh)), "from_bytes_type": str(type(script_path_from_bytes)), "original_id": id(self._script_path), "fresh_id": id(script_path_fresh), "from_bytes_id": id(script_path_from_bytes)}, "timestamp": int(time.time() * 1000)}) + '\n')
            except: pass
            # #endregion
            # Python 3.13 workaround: PyDict_Next() in dictobject.c:439 uses PyUnicode_CheckExact
            # which fails if dictionary keys aren't exact Unicode objects. The issue occurs when
            # Python 3.13's dictionary internal state isn't properly initialized.
            # Solution: Create dictionary and copy it to ensure it's in combined-table mode
            # and all internal structures are properly initialized for Python 3.13.
            # #region agent log
            try:
                with open(r'c:\Users\bruno\ws\OpenImageDebugger\.cursor\debug_server.log', 'a', encoding='utf-8') as f:
                    import json
                    f.write(json.dumps({"sessionId": "debug-session", "runId": "run1", "hypothesisId": "I", "location": "oidwindow.py:292", "message": "Creating dict for Python 3.13 workaround", "data": {"python_version": list(sys.version_info[:3])}, "timestamp": int(time.time() * 1000)}) + '\n')
            except: pass
            # #endregion
            # Python 3.13 fix: Use oid_initialize_safe() which takes a C string directly,
            # avoiding Python C API dictionary access that triggers PyUnicode_CheckExact
            # assertion in dictobject.c:439. This matches the pattern used by other
            # _safe functions (oid_plot_buffer_safe, oid_set_available_symbols_safe).
            # #region agent log
            try:
                with open(r'c:\Users\bruno\ws\OpenImageDebugger\.cursor\debug_server.log', 'a', encoding='utf-8') as f:
                    import json
                    f.write(json.dumps({"sessionId": "debug-session", "runId": "run1", "hypothesisId": "FIX", "location": "oidwindow.py:302", "message": "Using oid_initialize_safe with C string", "data": {"python_version": list(sys.version_info[:3]), "path": script_path_from_bytes}, "timestamp": int(time.time() * 1000)}) + '\n')
            except: pass
            # #endregion
            # Use safe version: extract path in Python and pass as C string
            # This avoids dictionary access in C++ which triggers the assertion
            # ctypes.c_char_p accepts bytes or None (which becomes nullptr in C++)
            path_bytes = script_path_from_bytes.encode('utf-8') if script_path_from_bytes else None
            # #region agent log
            try:
                with open(r'c:\Users\bruno\ws\OpenImageDebugger\.cursor\debug_server.log', 'a', encoding='utf-8') as f:
                    import json
                    f.write(json.dumps({"sessionId": "debug-session", "runId": "run1", "hypothesisId": "H1", "location": "oidwindow.py:319", "message": "Before oid_initialize_safe call", "data": {"script_path": script_path_from_bytes, "path_bytes": path_bytes.decode('utf-8') if path_bytes else None, "path_bytes_type": str(type(path_bytes)), "path_bytes_len": len(path_bytes) if path_bytes else 0}, "timestamp": int(time.time() * 1000)}) + '\n')
            except: pass
            # #endregion
            self._native_handler = self._lib.oid_initialize_safe(
                self._plot_variable_c_callback,
                path_bytes if path_bytes else None)
            # #region agent log
            try:
                with open(r'c:\Users\bruno\ws\OpenImageDebugger\.cursor\debug_server.log', 'a', encoding='utf-8') as f:
                    import json
                    f.write(json.dumps({"sessionId": "debug-session", "runId": "run1", "hypothesisId": "H2", "location": "oidwindow.py:327", "message": "After oid_initialize_safe call", "data": {"handler": str(self._native_handler) if self._native_handler else None}, "timestamp": int(time.time() * 1000)}) + '\n')
            except: pass
            # #endregion
            # #region agent log
            try:
                with open(r'c:\Users\bruno\ws\OpenImageDebugger\.cursor\debug_server.log', 'a', encoding='utf-8') as f:
                    import json
                    f.write(json.dumps({"sessionId": "debug-session", "runId": "run1", "hypothesisId": "D", "location": "oidwindow.py:340", "message": "After oid_initialize call", "data": {"success": True, "handler": str(self._native_handler) if self._native_handler else None}, "timestamp": int(time.time() * 1000)}) + '\n')
            except: pass
            # #endregion
        except Exception as e:
            # #region agent log
            try:
                with open(r'c:\Users\bruno\ws\OpenImageDebugger\.cursor\debug_server.log', 'a', encoding='utf-8') as f:
                    import json
                    f.write(json.dumps({"sessionId": "debug-session", "runId": "run1", "hypothesisId": "E", "location": "oidwindow.py:347", "message": "Exception in initialize_window", "data": {"exception_type": str(type(e)), "exception_msg": str(e), "exception_repr": repr(e)}, "timestamp": int(time.time() * 1000)}) + '\n')
            except: pass
            # #endregion
            raise
Thread Safety

LLDB-mode detection caching in PyGIL management uses shared static state without clear synchronization and mixes “cached” and “derived” conditions. If multiple threads construct the guard concurrently, mode detection and subsequent Python C-API calls may be inconsistent. Consider using std::once_flag/atomic for the cached mode decision, and ensure the “skip GIL” path cannot accidentally call Python APIs on non-main threads.

class PyGILRAII
{
  private:
    // H17: Class-level static variable for LLDB mode detection (cached,
    // thread-safe)
    static bool s_is_lldb_mode;

  public:
    // H17: Check if we're in LLDB mode (cached, thread-safe)
    static bool is_lldb_mode()
    {
        // Access the cached value - will be set on first PyGILRAII construction
        return s_is_lldb_mode;
    }

    PyGILRAII()
    {
        // H16: Cache LLDB mode detection to avoid calling unsafe Python C API
        // functions (PyGILState_GetThisThreadState, Py_IsInitialized) from
        // event loop thread. These calls crash in LLDB's embedded Python when
        // called from non-main threads, even when we skip GIL management.
        static bool lldb_mode_cached = false;

        PyThreadState* tstate_before = nullptr;
        bool py_init_before          = false;

        if (!lldb_mode_cached) {
            // First call: detect LLDB mode from main thread (safe)
            tstate_before    = PyGILState_GetThisThreadState();
            py_init_before   = Py_IsInitialized() != 0;
            s_is_lldb_mode   = (!tstate_before && !py_init_before);
            lldb_mode_cached = true;
        } else {
            // Use cached result - no more Python C API calls needed
            if (s_is_lldb_mode) {
                // Use cached LLDB mode - don't call Python C API functions
                tstate_before  = nullptr;
                py_init_before = false;
            } else {
                // Normal mode - safe to call these functions
                tstate_before  = PyGILState_GetThisThreadState();
                py_init_before = Py_IsInitialized() != 0;
            }
        }

        // If we have no thread state AND Python reports not initialized,
        // but we're being called from Python code, this is LLDB's
        // sub-interpreter. In this case:
        // 1. We're called from Python via ctypes, so GIL is already held
        // 2. PyGILState_Ensure() aborts because it can't register thread state
        // 3. But Python APIs work (PyDict_Check succeeds), so GIL IS held
        // Solution: Skip PyGILState_Ensure()/Release() - GIL is managed by
        // Python/LLDB, not by us. We can use Python APIs directly.
        if (s_is_lldb_mode || (!tstate_before && !py_init_before)) {
            // LLDB sub-interpreter: GIL is held by Python/LLDB, but thread
            // state isn't registered with GILState API. We can use Python APIs
            // but cannot call PyGILState_Ensure()/Release(). Skip GIL
            // management.
            _has_gil = true; // Don't try to acquire/release - it's already held
        } else {
            // Normal case: try PyGILState_Ensure()
            _py_gil_state = PyGILState_Ensure();
            _has_gil      = false; // We acquired it, so we need to release it
        }
    }

    PyGILRAII(const PyGILRAII&)            = delete;
    PyGILRAII(PyGILRAII&&)                 = delete;
    PyGILRAII& operator=(const PyGILRAII&) = delete;
    PyGILRAII& operator=(PyGILRAII&&)      = delete;

    ~PyGILRAII() noexcept
    {
        if (!_has_gil) {
            PyGILState_Release(_py_gil_state);
        }
    }

  private:
    PyGILState_STATE _py_gil_state{};
    bool _has_gil{false};
};

// H17: Static member definition for LLDB mode detection
bool PyGILRAII::s_is_lldb_mode = false;
Wrong Module Path

Windows library path detection uses GetModuleHandle(nullptr), which returns the executable module, not the DLL/shared library that contains the code. This can cause incorrect path resolution when the bridge is a DLL loaded by another process. Prefer GetModuleHandleEx with GET_MODULE_HANDLE_EX_FLAG_FROM_ADDRESS using an address inside the library, or store the HMODULE from DllMain if applicable.

std::filesystem::path get_current_library_path()
{
    // Use GetModuleFileName to get the path of the current library
    if (HMODULE hModule = GetModuleHandle(nullptr); hModule != nullptr) {
        char module_path[MAX_PATH];
        if (GetModuleFileNameA(hModule, module_path, MAX_PATH) != 0) {
            return std::filesystem::path{module_path};
        }
    }
    return std::filesystem::path{};

@brunoalr
Copy link
Copy Markdown
Member Author

brunoalr commented Feb 3, 2026

/analyze

@qodo-code-review
Copy link
Copy Markdown

PR Analysis 🔬

  • This screen contains a list of code components that were changed in this PR.
  • You can initiate specific actions for each component, by checking the relevant boxes.
  • Results will appear as a comment on the PR, typically after 30-60 seconds.
fileChanged components
oid.py
  • Test
  • Docs
  • Improve
  • Similar
 
lldb_stop_hook_handler
(function)
 
+6/-2
 
  • Test
  • Docs
  • Improve
  • Similar
 
__lldb_init_module
(function)
 
+26/-8
 
  • Test
  • Docs
  • Improve
  • Similar
 
register_ide_hooks
(function)
 
+10/-2
 
  • Test
  • Docs
  • Improve
  • Similar
 
main
(function)
 
+2/-2
 
lldbbridge.py
  • Test
  • Docs
  • Improve
  • Similar
 
__init__
(method of LldbBridge)
 
+5/-1
 
  • Test
  • Docs
  • Improve
  • Similar
 
get_backend_name
(method of LldbBridge)
 
+1/-1
 
  • Test
  • Docs
  • Improve
  • Similar
 
_check_frame_modification
(method of LldbBridge)
 
+40/-7
 
  • Test
  • Docs
  • Improve
  • Similar
 
event_loop
(method of LldbBridge)
 
+3/-3
 
  • Test
  • Docs
  • Improve
  • Similar
 
queue_request
(method of LldbBridge)
 
+18/-3
 
  • Test
  • Docs
  • Improve
  • Similar
 
_get_thread
(method of LldbBridge)
 
+5/-3
 
  • Test
  • Docs
  • Improve
  • Similar
 
get_buffer_metadata
(method of LldbBridge)
 
+15/-13
 
  • Test
  • Docs
  • Improve
  • Similar
 
_get_observable_children_members
(method of LldbBridge)
 
+14/-11
 
  • Test
  • Docs
  • Improve
  • Similar
 
get_available_symbols
(method of LldbBridge)
 
+8/-7
 
  • Test
  • Docs
  • Improve
  • Similar
 
stop_hook
(method of LldbBridge)
 
+1/-1
 
  • Test
  • Docs
  • Improve
  • Similar
 
_try_register_process_listener
(method of LldbBridge)
 
+74/-0
 
  • Test
  • Docs
  • Improve
  • Similar
 
__getitem__
(method of SymbolWrapper)
 
+6/-3
 
events.py
  • Test
  • Docs
  • Improve
  • Similar
 
_set_symbol_complete_list
(method of OpenImageDebuggerEvents)
 
+9/-2
 
  • Test
  • Docs
  • Improve
  • Similar
 
stop_handler
(method of OpenImageDebuggerEvents)
 
+8/-7
 
android_studio.py
  • Test
  • Docs
  • Improve
  • Similar
 
prevents_stop_hook
(function)
 
+18/-0
 
  • Test
  • Docs
  • Improve
  • Similar
 
register_symbol_fetch_hook
(function)
 
+79/-0
 
  • Test
  • Docs
  • Improve
  • Similar
 
_register_process_listener
(function)
 
+67/-0
 
oidwindow.py
  • Test
  • Docs
  • Improve
  • Similar
 
__init__
(method of OpenImageDebuggerWindow)
 
+54/-12
 
  • Test
  • Docs
  • Improve
  • Similar
 
plot_variable
(method of OpenImageDebuggerWindow)
 
+13/-2
 
  • Test
  • Docs
  • Improve
  • Similar
 
set_available_symbols
(method of OpenImageDebuggerWindow)
 
+25/-6
 
  • Test
  • Docs
  • Improve
  • Similar
 
run_event_loop
(method of OpenImageDebuggerWindow)
 
+2/-2
 
  • Test
  • Docs
  • Improve
  • Similar
 
get_observed_buffers
(method of OpenImageDebuggerWindow)
 
+23/-1
 
  • Test
  • Docs
  • Improve
  • Similar
 
initialize_window
(method of OpenImageDebuggerWindow)
 
+78/-4
 
  • Test
  • Docs
  • Improve
  • Similar
 
DeferredSymbolListSetter
(class)
 
+21/-0
 
  • Test
  • Docs
  • Improve
  • Similar
 
__call__
(method of DeferredVariablePlotter)
 
+125/-2
 
python_native_interface.cpp
  • Test
  • Docs
  • Improve
  • Similar
 
copy_py_string
(function)
 
+3/-5
 

@brunoalr
Copy link
Copy Markdown
Member Author

brunoalr commented Feb 3, 2026

/checks
"https://github.com/{org_name}/{repo_name}/actions/runs/{run_id}/job/{job_id}"

@qodo-code-review
Copy link
Copy Markdown

Persistent suggestions updated to latest commit 6e25cc4

import json
f.write(json.dumps({"sessionId": "debug-session", "runId": "run1", "hypothesisId": "A", "location": "oidwindow.py:27", "message": "__init__ entry", "data": {"script_path_type": str(type(script_path)), "script_path_repr": repr(script_path), "script_path_value": str(script_path) if script_path else None}, "timestamp": int(time.time() * 1000)}) + '\n')
except: pass
# #endregion
Copy link
Copy Markdown

Choose a reason for hiding this comment

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

Debug logging with hardcoded path accidentally committed

High Severity

Multiple debug logging blocks with a hardcoded Windows path (c:\Users\bruno\ws\OpenImageDebugger\.cursor\debug_server.log) are left in the production code. These #region agent log blocks write JSON debug information to a developer's local machine path, which will fail silently on any other machine and expose internal debugging patterns to production users. These should be removed before merging.

Additional Locations (1)

Fix in Cursor Fix in Web


#Python cache files
__pycache__ /
*.pyc
Copy link
Copy Markdown

Choose a reason for hiding this comment

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

Gitignore patterns broken by whitespace corruption

High Severity

The .gitignore file has been corrupted with spurious whitespace that breaks all ignore patterns. Patterns like build / (with trailing space), .vscode / (with leading spaces and trailing space), and CMakeLists.txt followed by .user on separate lines will not match intended files. The original build/, .vscode/, CMakeLists.txt.user, and .DS_Store patterns have been split or whitespace-corrupted, making them non-functional.

Fix in Cursor Fix in Web

if (GetModuleFileNameA(hModule, module_path, MAX_PATH) != 0) {
return std::filesystem::path{module_path};
}
}
Copy link
Copy Markdown

Choose a reason for hiding this comment

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

Windows library path returns executable not library

Medium Severity

GetModuleHandle(nullptr) returns the main executable's module handle, not the oidbridge library's handle. This causes get_current_library_path() to return the path to the parent executable (e.g., Python interpreter or debugger), not the oidbridge DLL. The Unix implementation correctly uses dladdr(&oid_initialize) to get the containing library's path.

Fix in Cursor Fix in Web

# Keep reference to prevent garbage collection
if not hasattr(self, '_buffer_refs'):
self._buffer_refs = []
self._buffer_refs.append(mv)
Copy link
Copy Markdown

Choose a reason for hiding this comment

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

Buffer reference stored on callable may be garbage collected

Medium Severity

In DeferredVariablePlotter.__call__, buffer references (bytes_copy, c_array, mv) are stored on self._buffer_refs to prevent garbage collection. However, self is a temporary callable object that gets garbage collected after the call completes (when popped from _pending_requests). The buffer pointer passed to oid_plot_buffer_safe may become invalid if the C++ code doesn't immediately copy the data.

Fix in Cursor Fix in Web

if not hasattr(self, '_symbol_array_refs'):
self._symbol_array_refs = []
self._symbol_array_refs.append(c_string_array)
self._symbol_array_refs.append(encoded_symbols)
Copy link
Copy Markdown

Choose a reason for hiding this comment

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

Unbounded memory growth from accumulated symbol array references

Medium Severity

The _symbol_array_refs list is appended to on every call to set_available_symbols() (which occurs on each debugger stop event) but is never cleared. Over a long debugging session with many breakpoint hits, this list grows unboundedly, causing a memory leak. The references are kept to prevent garbage collection during the C function call, but they can be safely cleared after the call returns or replaced on subsequent calls.

Fix in Cursor Fix in Web

state == lldb.eStateExited
or state == lldb.eStateDetached
):
break
Copy link
Copy Markdown

Choose a reason for hiding this comment

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

Listener registration state not reset on process exit

Medium Severity

When the monitored process exits or detaches, the listener thread breaks out of its loop but _listener_registered is never reset to False. If a user then starts debugging a new process, _try_register_process_listener returns early at the if self._listener_registered check, preventing listener registration for the new process. This causes stop events to be missed for all subsequent debugging sessions after the first process exits.

Fix in Cursor Fix in Web

@brunoalr
Copy link
Copy Markdown
Member Author

brunoalr commented Feb 4, 2026

/describe
--pr_description.extra_instructions="
For the title, use the format [type]: [summary]
"

@qodo-code-review
Copy link
Copy Markdown

PR Description updated to latest commit (5c609d4)

Copy link
Copy Markdown

@cursor cursor bot left a comment

Choose a reason for hiding this comment

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

Cursor Bugbot has reviewed your changes and found 3 potential issues.

Bugbot Free Tier Details

You are on the Bugbot Free tier. On this plan, Bugbot will review limited PRs each billing cycle.

To receive Bugbot reviews on all of your PRs, visit the Cursor dashboard to activate Pro and start your 14-day free trial.

Bugbot Autofix is OFF. To automatically fix reported issues with cloud agents, enable autofix in the Cursor dashboard.

self._previous_evloop_time = current_time

# Schedule next run of the event loop
# Schedule next run of the event loop
Copy link
Copy Markdown

Choose a reason for hiding this comment

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

Event loop always reschedules regardless of time check

Medium Severity

The comment # Schedule next run of the event loop was moved inside the if dt > self._event_loop_wait_time: block (at 12-space indent), but the actual scheduling call self._bridge.queue_request(self.run_event_loop) remains outside it (at 8-space indent). This means the event loop always reschedules itself on every call, even when the time threshold hasn't been met, flooding the request queue with redundant run_event_loop callbacks. The misaligned comment suggests the intent was to move both inside the if.

Fix in Cursor Fix in Web

if (!deps_.socket.waitForConnected(30000)) {
// Connection failed - window will show but won't be able to communicate
}
});
Copy link
Copy Markdown

Choose a reason for hiding this comment

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

Dangling this pointer in deferred networking lambda

Medium Severity

The QTimer::singleShot lambda captures [this] and then calls deps_.socket.waitForConnected(30000), which blocks the Qt event loop thread for up to 30 seconds after it starts. The previous code blocked during initialization (before exec()), which was expected. Now the blocking happens inside the running event loop, potentially freezing the oidwindow UI for up to 30 seconds while waiting for the bridge process to connect.

Fix in Cursor Fix in Web

# Only add stop event if frame changed AND process was already stopped
# (not just now stopped, which the listener already handled)
if frame_was_updated and self._last_process_state != lldb.eStateStopped:
frame_was_updated = False # Suppress this frame change event
Copy link
Copy Markdown

Choose a reason for hiding this comment

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

First stop event lost when process listener registers

High Severity

When _try_register_process_listener succeeds on the same _check_frame_modification call where the process is first seen as stopped, the stop event is silently dropped. The listener sets _listener_registered = True at line 299, then the code at line 89 suppresses frame_was_updated because _last_process_state is still None (not yet eStateStopped). Meanwhile, the newly spawned listener thread won't catch this stop either — it only detects state transitions, and the transition already happened before registration. This means the very first breakpoint hit can go undetected.

Additional Locations (1)

Fix in Cursor Fix in Web

@brunoalr
Copy link
Copy Markdown
Member Author

/describe
--pr_description.extra_instructions="
For the title, use the format [type]: [summary]
"

@brunoalr
Copy link
Copy Markdown
Member Author

/review
--pr_reviewer.inline_code_comments=true

@qodo-code-review
Copy link
Copy Markdown

qodo-code-review bot commented Mar 13, 2026

Code Review by Qodo

Grey Divider

New Review Started

This review has been superseded by a new analysis

Grey Divider

ⓘ The new review experience is currently in Beta. Learn more

Grey Divider

Qodo Logo

@qodo-code-review
Copy link
Copy Markdown

PR Description updated to latest commit (764f6ea)

@brunoalr
Copy link
Copy Markdown
Member Author

/improve

@sonarqubecloud
Copy link
Copy Markdown

Quality Gate Failed Quality Gate failed

Failed conditions
1 Security Hotspot

See analysis details on SonarQube Cloud

@qodo-code-review
Copy link
Copy Markdown

qodo-code-review bot commented Mar 13, 2026

Code Review by Qodo

🐞 Bugs (6) 📘 Rule violations (0) 📎 Requirement gaps (0)

Grey Divider


Action required

1. Hardcoded debug log path 🐞 Bug ⛨ Security
Description
OpenImageDebuggerWindow writes session data to a hardcoded, user-specific Windows path
(c:\Users\bruno\...) on every init/initialize_window call and silently swallows failures, which is
unintended I/O and leaks local environment details into the codebase.
Code

resources/oidscripts/oidwindow.py[R28-34]

+        # #region agent log
+        try:
+            with open(r'c:\Users\bruno\ws\OpenImageDebugger\.cursor\debug_server.log', 'a', encoding='utf-8') as f:
+                import json
+                f.write(json.dumps({"sessionId": "debug-session", "runId": "run1", "hypothesisId": "A", "location": "oidwindow.py:27", "message": "__init__ entry", "data": {"script_path_type": str(type(script_path)), "script_path_repr": repr(script_path), "script_path_value": str(script_path) if script_path else None}, "timestamp": int(time.time() * 1000)}) + '\n')
+        except: pass
+        # #endregion
Evidence
The PR introduces multiple unconditional file writes to a developer-local absolute path inside
runtime code paths (__init__ and initialize_window), with broad exception swallowing that hides
failures while still attempting the I/O.

resources/oidscripts/oidwindow.py[27-45]
resources/oidscripts/oidwindow.py[275-315]

Agent prompt
The issue below was found during a code review. Follow the provided context and guidance below and implement a solution

### Issue description
`resources/oidscripts/oidwindow.py` contains committed instrumentation that writes to `c:\\Users\\bruno\\...\\debug_server.log` during normal execution.

### Issue Context
This is user-specific and will create unexpected I/O attempts and leak local path details into the project.

### Fix Focus Areas
- resources/oidscripts/oidwindow.py[27-45]
- resources/oidscripts/oidwindow.py[275-352]

ⓘ Copy this prompt and use it to remediate the issue with your preferred AI generation tools


2. Normalized path ignored 🐞 Bug ✓ Correctness
Description
OpenImageDebuggerWindow normalizes script_path into self._script_path but still uses the
original script_path for os.add_dll_directory() and ctypes.cdll.LoadLibrary(), so the Python
3.13 path/bytes fixes can still fail when callers pass non-str paths.
Code

resources/oidscripts/oidwindow.py[R36-61]

+        # Normalize script_path to ensure it's a proper Unicode string for Python 3.13 compatibility
+        # os.fsdecode() properly handles both bytes and string paths on all platforms
+        self._script_path = os.fsdecode(script_path) if script_path else str(script_path)
+        # #region agent log
+        try:
+            with open(r'c:\Users\bruno\ws\OpenImageDebugger\.cursor\debug_server.log', 'a', encoding='utf-8') as f:
+                import json
+                f.write(json.dumps({"sessionId": "debug-session", "runId": "run1", "hypothesisId": "B", "location": "oidwindow.py:31", "message": "After normalization", "data": {"normalized_type": str(type(self._script_path)), "normalized_repr": repr(self._script_path), "is_str": isinstance(self._script_path, str), "is_unicode": isinstance(self._script_path, str) and hasattr(self._script_path, 'encode')}, "timestamp": int(time.time() * 1000)}) + '\n')
+        except: pass
+        # #endregion
+        # Cache of observed buffer names for LLDB mode where get_observed_buffers() doesn't work
+        self._observed_buffers_cache = set()

        if PLATFORM_NAME == 'windows':
            os.add_dll_directory(script_path)
-            os.add_dll_directory(os.getenv('Qt5_Dir') + '/' + 'bin')
+            qt6_dir = os.getenv('Qt6_DIR')
+            if qt6_dir:
+                # Strip trailing semicolons and path separators that might be in the env var
+                qt6_dir = qt6_dir.rstrip(';:')
+                if os.path.exists(qt6_dir):
+                    bin_path = os.path.join(qt6_dir, 'bin')
+                    if os.path.exists(bin_path):
+                        os.add_dll_directory(bin_path)

        # Request ctypes to load libGL before the native oidwindow does; this
        # fixes an issue on Ubuntu machines with nvidia drivers. For more
Evidence
The code explicitly normalizes script_path but then continues to use the unnormalized parameter
for Windows DLL search path and for building the library path string for LoadLibrary.

resources/oidscripts/oidwindow.py[35-70]

Agent prompt
The issue below was found during a code review. Follow the provided context and guidance below and implement a solution

### Issue description
Path normalization is computed but not used for critical filesystem/DLL operations.

### Issue Context
This defeats the intended Python 3.13/Unicode safety changes and can still break on bytes-path inputs.

### Fix Focus Areas
- resources/oidscripts/oidwindow.py[35-70]

ⓘ Copy this prompt and use it to remediate the issue with your preferred AI generation tools


3. Symbol refs leak 🐞 Bug ➹ Performance
Description
set_available_symbols() appends each generated ctypes symbol array and encoded symbol list into
_symbol_array_refs without ever clearing/replacing it, causing unbounded memory growth as symbols
are refreshed on each stop/event.
Code

resources/oidscripts/oidwindow.py[R213-231]

+        # Encode all strings to bytes and keep references to prevent GC
+        encoded_symbols = [
+            symbol.encode('utf-8') if isinstance(symbol, str) else symbol
+            for symbol in sorted_observable_symbols
+        ]
+
+        # Create array of C strings
+        c_string_array = (ctypes.c_char_p * count)()
+        for i, encoded in enumerate(encoded_symbols):
+            c_string_array[i] = encoded
+
+        # Keep references to prevent garbage collection
+        if not hasattr(self, '_symbol_array_refs'):
+            self._symbol_array_refs = []
+        self._symbol_array_refs.append(c_string_array)
+        self._symbol_array_refs.append(encoded_symbols)
+
+        self._lib.oid_set_available_symbols_safe(
+            self._native_handler, c_string_array, count)
Evidence
Python retains every previous array/list even though the C++ safe API immediately copies the strings
into a deque, so these references are not required after the call and will accumulate indefinitely.

resources/oidscripts/oidwindow.py[199-232]
src/oidbridge/oid_bridge.cpp[859-881]

Agent prompt
The issue below was found during a code review. Follow the provided context and guidance below and implement a solution

### Issue description
`_symbol_array_refs` grows without bound and retains old symbol arrays.

### Issue Context
The C++ safe API copies strings immediately, so retaining old arrays is unnecessary.

### Fix Focus Areas
- resources/oidscripts/oidwindow.py[199-232]
- src/oidbridge/oid_bridge.cpp[859-881]

ⓘ Copy this prompt and use it to remediate the issue with your preferred AI generation tools


View more (3)
4. Buffer refs leak 🐞 Bug ➹ Performance
Description
DeferredVariablePlotter stores every bytearray buffer copy, ctypes array, and memoryview into
self._buffer_refs and never clears it, which can retain large plotted buffers indefinitely and
exhaust memory in long debugging sessions.
Code

resources/oidscripts/oidwindow.py[R461-479]

+                        # For bytes (read-only), we need to create a writable copy
+                        # to get a pointer. This is necessary because ctypes.from_buffer
+                        # requires writable buffers, and bytes are immutable.
+                        # Create a bytearray (writable) from the memoryview
+                        bytes_copy = bytearray(mv)
+                        # Now we can use from_buffer on the writable bytearray
+                        c_array = (ctypes.c_ubyte * len(bytes_copy)).from_buffer(bytes_copy)
+                        buffer_ptr = ctypes.addressof(c_array)
+                        # Keep reference to prevent garbage collection
+                        if not hasattr(self, '_buffer_refs'):
+                            self._buffer_refs = []
+                        self._buffer_refs.append(bytes_copy)
+                        self._buffer_refs.append(c_array)
+
+                    buffer_size = mv.nbytes
+                    # Keep reference to prevent garbage collection
+                    if not hasattr(self, '_buffer_refs'):
+                        self._buffer_refs = []
+                    self._buffer_refs.append(mv)
Evidence
C++ message sending uses a non-owning span that must remain valid only during
MessageComposer::send(), which is synchronous within the oid_plot_buffer_safe call chain, so
retaining buffers beyond the call is unnecessary and harmful.

resources/oidscripts/oidwindow.py[448-480]
src/ipc/message_exchange.h[131-214]
src/oidbridge/oid_bridge.cpp[343-378]

Agent prompt
The issue below was found during a code review. Follow the provided context and guidance below and implement a solution

### Issue description
`DeferredVariablePlotter` retains plotted buffers indefinitely via `_buffer_refs`.

### Issue Context
The C++ side sends synchronously; buffer lifetime only needs to cover the duration of the call chain.

### Fix Focus Areas
- resources/oidscripts/oidwindow.py[448-525]
- src/ipc/message_exchange.h[131-214]
- src/oidbridge/oid_bridge.cpp[343-378]

ⓘ Copy this prompt and use it to remediate the issue with your preferred AI generation tools


5. Unready window triggers assert 🐞 Bug ⛯ Reliability
Description
OpenImageDebuggerEvents.stop_handler() continues after its timeout even if the window is still not
ready, then calls get_observed_buffers() which (in non-LLDB mode) invokes C++ get_observed_symbols()
that asserts !client_.isNull() and can abort the process when the UI never connected.
Code

resources/oidscripts/events.py[R45-56]

        if not self._window.is_ready():
-            self._window.initialize_window()
-            while not self._window.is_ready():
+            max_wait = 50
+            waited = 0
+            while not self._window.is_ready() and waited < max_wait:
                time.sleep(0.1)
+                waited += 1

-        # Update buffers being visualized
        observed_buffers = self._window.get_observed_buffers()
-        for buffer_name in observed_buffers:
-            self._window.plot_variable(buffer_name)
+        if observed_buffers:
+            for buffer_name in observed_buffers:
+                self._window.plot_variable(buffer_name)
Evidence
The Python handler does not bail out when readiness isn't reached; the C++ bridge uses a hard assert
for client presence when fetching observed symbols, so this path can crash hard instead of failing
gracefully.

resources/oidscripts/events.py[40-58]
resources/oidscripts/oidwindow.py[247-270]
src/oidbridge/oid_bridge.cpp[270-276]

Agent prompt
The issue below was found during a code review. Follow the provided context and guidance below and implement a solution

### Issue description
The stop handler proceeds even when the UI never became ready, and the C++ bridge asserts on null client.

### Issue Context
This can abort the debugger session (assert) instead of failing gracefully.

### Fix Focus Areas
- resources/oidscripts/events.py[40-58]
- resources/oidscripts/oidwindow.py[247-273]
- src/oidbridge/oid_bridge.cpp[270-287]

ⓘ Copy this prompt and use it to remediate the issue with your preferred AI generation tools


6. Wrong Windows module path 🐞 Bug ✓ Correctness
Description
Windows get_current_library_path() uses GetModuleHandle(nullptr)/GetModuleFileNameA, which returns
the process module path rather than the oidbridge module path, so the OidBridge fallback used to
locate oidwindow.exe can resolve to the wrong directory when oid_path is empty.
Code

src/oidbridge/library_path_win32.cpp[R35-44]

+std::filesystem::path get_current_library_path()
+{
+    // Use GetModuleFileName to get the path of the current library
+    if (HMODULE hModule = GetModuleHandle(nullptr); hModule != nullptr) {
+        char module_path[MAX_PATH];
+        if (GetModuleFileNameA(hModule, module_path, MAX_PATH) != 0) {
+            return std::filesystem::path{module_path};
+        }
+    }
+    return std::filesystem::path{};
Evidence
The header contract says this function returns the current library/module path, but the
implementation queries the null module handle; OidBridge uses this value specifically to build the
path to the UI executable when oid_path_ is empty.

src/oidbridge/library_path.h[34-38]
src/oidbridge/library_path_win32.cpp[35-44]
src/oidbridge/oid_bridge.cpp[190-241]

Agent prompt
The issue below was found during a code review. Follow the provided context and guidance below and implement a solution

### Issue description
Windows fallback path detection does not meet the function contract and can break oidwindow.exe discovery.

### Issue Context
OidBridge uses this path to launch the UI when `oid_path_` is empty.

### Fix Focus Areas
- src/oidbridge/library_path_win32.cpp[35-44]
- src/oidbridge/oid_bridge.cpp[190-241]
- src/oidbridge/library_path.h[34-38]

ⓘ Copy this prompt and use it to remediate the issue with your preferred AI generation tools


Grey Divider

ⓘ The new review experience is currently in Beta. Learn more

Grey Divider

Qodo Logo

Comment on lines +28 to +34
# #region agent log
try:
with open(r'c:\Users\bruno\ws\OpenImageDebugger\.cursor\debug_server.log', 'a', encoding='utf-8') as f:
import json
f.write(json.dumps({"sessionId": "debug-session", "runId": "run1", "hypothesisId": "A", "location": "oidwindow.py:27", "message": "__init__ entry", "data": {"script_path_type": str(type(script_path)), "script_path_repr": repr(script_path), "script_path_value": str(script_path) if script_path else None}, "timestamp": int(time.time() * 1000)}) + '\n')
except: pass
# #endregion
Copy link
Copy Markdown

Choose a reason for hiding this comment

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

Action required

1. Hardcoded debug log path 🐞 Bug ⛨ Security

OpenImageDebuggerWindow writes session data to a hardcoded, user-specific Windows path
(c:\Users\bruno\...) on every init/initialize_window call and silently swallows failures, which is
unintended I/O and leaks local environment details into the codebase.
Agent Prompt
### Issue description
`resources/oidscripts/oidwindow.py` contains committed instrumentation that writes to `c:\\Users\\bruno\\...\\debug_server.log` during normal execution.

### Issue Context
This is user-specific and will create unexpected I/O attempts and leak local path details into the project.

### Fix Focus Areas
- resources/oidscripts/oidwindow.py[27-45]
- resources/oidscripts/oidwindow.py[275-352]

ⓘ Copy this prompt and use it to remediate the issue with your preferred AI generation tools

Comment on lines +36 to 61
# Normalize script_path to ensure it's a proper Unicode string for Python 3.13 compatibility
# os.fsdecode() properly handles both bytes and string paths on all platforms
self._script_path = os.fsdecode(script_path) if script_path else str(script_path)
# #region agent log
try:
with open(r'c:\Users\bruno\ws\OpenImageDebugger\.cursor\debug_server.log', 'a', encoding='utf-8') as f:
import json
f.write(json.dumps({"sessionId": "debug-session", "runId": "run1", "hypothesisId": "B", "location": "oidwindow.py:31", "message": "After normalization", "data": {"normalized_type": str(type(self._script_path)), "normalized_repr": repr(self._script_path), "is_str": isinstance(self._script_path, str), "is_unicode": isinstance(self._script_path, str) and hasattr(self._script_path, 'encode')}, "timestamp": int(time.time() * 1000)}) + '\n')
except: pass
# #endregion
# Cache of observed buffer names for LLDB mode where get_observed_buffers() doesn't work
self._observed_buffers_cache = set()

if PLATFORM_NAME == 'windows':
os.add_dll_directory(script_path)
os.add_dll_directory(os.getenv('Qt5_Dir') + '/' + 'bin')
qt6_dir = os.getenv('Qt6_DIR')
if qt6_dir:
# Strip trailing semicolons and path separators that might be in the env var
qt6_dir = qt6_dir.rstrip(';:')
if os.path.exists(qt6_dir):
bin_path = os.path.join(qt6_dir, 'bin')
if os.path.exists(bin_path):
os.add_dll_directory(bin_path)

# Request ctypes to load libGL before the native oidwindow does; this
# fixes an issue on Ubuntu machines with nvidia drivers. For more
Copy link
Copy Markdown

Choose a reason for hiding this comment

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

Action required

2. Normalized path ignored 🐞 Bug ✓ Correctness

OpenImageDebuggerWindow normalizes script_path into self._script_path but still uses the
original script_path for os.add_dll_directory() and ctypes.cdll.LoadLibrary(), so the Python
3.13 path/bytes fixes can still fail when callers pass non-str paths.
Agent Prompt
### Issue description
Path normalization is computed but not used for critical filesystem/DLL operations.

### Issue Context
This defeats the intended Python 3.13/Unicode safety changes and can still break on bytes-path inputs.

### Fix Focus Areas
- resources/oidscripts/oidwindow.py[35-70]

ⓘ Copy this prompt and use it to remediate the issue with your preferred AI generation tools

Comment on lines +213 to +231
# Encode all strings to bytes and keep references to prevent GC
encoded_symbols = [
symbol.encode('utf-8') if isinstance(symbol, str) else symbol
for symbol in sorted_observable_symbols
]

# Create array of C strings
c_string_array = (ctypes.c_char_p * count)()
for i, encoded in enumerate(encoded_symbols):
c_string_array[i] = encoded

# Keep references to prevent garbage collection
if not hasattr(self, '_symbol_array_refs'):
self._symbol_array_refs = []
self._symbol_array_refs.append(c_string_array)
self._symbol_array_refs.append(encoded_symbols)

self._lib.oid_set_available_symbols_safe(
self._native_handler, c_string_array, count)
Copy link
Copy Markdown

Choose a reason for hiding this comment

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

Action required

3. Symbol refs leak 🐞 Bug ➹ Performance

set_available_symbols() appends each generated ctypes symbol array and encoded symbol list into
_symbol_array_refs without ever clearing/replacing it, causing unbounded memory growth as symbols
are refreshed on each stop/event.
Agent Prompt
### Issue description
`_symbol_array_refs` grows without bound and retains old symbol arrays.

### Issue Context
The C++ safe API copies strings immediately, so retaining old arrays is unnecessary.

### Fix Focus Areas
- resources/oidscripts/oidwindow.py[199-232]
- src/oidbridge/oid_bridge.cpp[859-881]

ⓘ Copy this prompt and use it to remediate the issue with your preferred AI generation tools

Comment on lines +461 to +479
# For bytes (read-only), we need to create a writable copy
# to get a pointer. This is necessary because ctypes.from_buffer
# requires writable buffers, and bytes are immutable.
# Create a bytearray (writable) from the memoryview
bytes_copy = bytearray(mv)
# Now we can use from_buffer on the writable bytearray
c_array = (ctypes.c_ubyte * len(bytes_copy)).from_buffer(bytes_copy)
buffer_ptr = ctypes.addressof(c_array)
# Keep reference to prevent garbage collection
if not hasattr(self, '_buffer_refs'):
self._buffer_refs = []
self._buffer_refs.append(bytes_copy)
self._buffer_refs.append(c_array)

buffer_size = mv.nbytes
# Keep reference to prevent garbage collection
if not hasattr(self, '_buffer_refs'):
self._buffer_refs = []
self._buffer_refs.append(mv)
Copy link
Copy Markdown

Choose a reason for hiding this comment

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

Action required

4. Buffer refs leak 🐞 Bug ➹ Performance

DeferredVariablePlotter stores every bytearray buffer copy, ctypes array, and memoryview into
self._buffer_refs and never clears it, which can retain large plotted buffers indefinitely and
exhaust memory in long debugging sessions.
Agent Prompt
### Issue description
`DeferredVariablePlotter` retains plotted buffers indefinitely via `_buffer_refs`.

### Issue Context
The C++ side sends synchronously; buffer lifetime only needs to cover the duration of the call chain.

### Fix Focus Areas
- resources/oidscripts/oidwindow.py[448-525]
- src/ipc/message_exchange.h[131-214]
- src/oidbridge/oid_bridge.cpp[343-378]

ⓘ Copy this prompt and use it to remediate the issue with your preferred AI generation tools

Comment on lines 45 to 56
if not self._window.is_ready():
self._window.initialize_window()
while not self._window.is_ready():
max_wait = 50
waited = 0
while not self._window.is_ready() and waited < max_wait:
time.sleep(0.1)
waited += 1

# Update buffers being visualized
observed_buffers = self._window.get_observed_buffers()
for buffer_name in observed_buffers:
self._window.plot_variable(buffer_name)
if observed_buffers:
for buffer_name in observed_buffers:
self._window.plot_variable(buffer_name)

Copy link
Copy Markdown

Choose a reason for hiding this comment

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

Action required

5. Unready window triggers assert 🐞 Bug ⛯ Reliability

OpenImageDebuggerEvents.stop_handler() continues after its timeout even if the window is still not
ready, then calls get_observed_buffers() which (in non-LLDB mode) invokes C++ get_observed_symbols()
that asserts !client_.isNull() and can abort the process when the UI never connected.
Agent Prompt
### Issue description
The stop handler proceeds even when the UI never became ready, and the C++ bridge asserts on null client.

### Issue Context
This can abort the debugger session (assert) instead of failing gracefully.

### Fix Focus Areas
- resources/oidscripts/events.py[40-58]
- resources/oidscripts/oidwindow.py[247-273]
- src/oidbridge/oid_bridge.cpp[270-287]

ⓘ Copy this prompt and use it to remediate the issue with your preferred AI generation tools

Comment on lines +35 to +44
std::filesystem::path get_current_library_path()
{
// Use GetModuleFileName to get the path of the current library
if (HMODULE hModule = GetModuleHandle(nullptr); hModule != nullptr) {
char module_path[MAX_PATH];
if (GetModuleFileNameA(hModule, module_path, MAX_PATH) != 0) {
return std::filesystem::path{module_path};
}
}
return std::filesystem::path{};
Copy link
Copy Markdown

Choose a reason for hiding this comment

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

Action required

6. Wrong windows module path 🐞 Bug ✓ Correctness

Windows get_current_library_path() uses GetModuleHandle(nullptr)/GetModuleFileNameA, which returns
the process module path rather than the oidbridge module path, so the OidBridge fallback used to
locate oidwindow.exe can resolve to the wrong directory when oid_path is empty.
Agent Prompt
### Issue description
Windows fallback path detection does not meet the function contract and can break oidwindow.exe discovery.

### Issue Context
OidBridge uses this path to launch the UI when `oid_path_` is empty.

### Fix Focus Areas
- src/oidbridge/library_path_win32.cpp[35-44]
- src/oidbridge/oid_bridge.cpp[190-241]
- src/oidbridge/library_path.h[34-38]

ⓘ Copy this prompt and use it to remediate the issue with your preferred AI generation tools

@brunoalr brunoalr marked this pull request as draft March 14, 2026 13:55
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.

1 participant