Skip to content

fix(core): enrich watchpoint/exception events with stacktrace and detailMessage, and validate watchpoint fields - #119

Merged
PatilShreyas merged 1 commit into
mainfrom
fix/issue-111-watchpoint-exception-enrichment
Oct 26, 2026
Merged

PatilShreyas merged 1 commit into
mainfrom
fix/issue-111-watchpoint-exception-enrichment

Conversation

@PatilShreyas

Copy link
Copy Markdown
Owner

Closes #111

Summary

This PR resolves four issues in JdiSession event handling and watchpoint/exception breakpoint binding:

  1. Populate stacktrace on Watchpoint Hits (WATCHPOINT_ACCESS_HIT & WATCHPOINT_MODIFY_HIT):

    • Updated handleAccessWatchpointEvent and handleModificationWatchpointEvent to populate stacktrace = getFramesSafely(event.thread()) on DebugEvent, matching BREAKPOINT_HIT, STEP_COMPLETE, and EXCEPTION_HIT.
    • debroid poll <sessionId> <cursor> --with-stacktrace now includes the full call stack when a watchpoint triggers.
  2. Include detailMessage in EXCEPTION_HIT exceptionMessage:

    • Added formatExceptionMessage(exceptionObj) in JdiSession to inspect the detailMessage field on event.exception(): ObjectReference.
    • Formats exceptionMessage as "<ExceptionClassName>: <detailMessage>" when detailMessage is a non-null StringReference, or falls back to "<ExceptionClassName>" when detailMessage is null or inaccessible.
  3. Coordinate VM Resume for Filtered ExceptionEvents via runEventListener:

    • Removed the direct event.virtualMachine().resume() call inside handleExceptionEvent when client-side exception class filtering rejects an event.
    • Changed handleExceptionEvent to return Boolean (true if the exception matched and suspended, false if filtered out) and updated processJdiEvent accordingly so runEventListener coordinates eventSet.resume() safely across multi-event EventSets.
  4. Validate Watchpoint Field Existence & Bind Across All Matching ReferenceTypes:

    • Added ErrorCode.INVALID_ARGUMENT in Models.kt.
    • In setWatchpoint, when refTypes.isNotEmpty(), validates that at least one loaded ReferenceType contains fieldName; otherwise throws DebugException(ErrorCode.INVALID_ARGUMENT, "Field '$fieldName' not found in class '$className'").
    • Updated both setWatchpoint and setExceptionBreakpoint (exceptionRequests changed to ConcurrentHashMap<String, MutableList<ExceptionRequest>>) to bind requests across all loaded ReferenceTypes matching className instead of only refTypes.first().

Tests Performed

1. Automated Unit Tests & Static Analysis (./gradlew build)

  • Added unit tests in JdiSessionTest:
    • handleAccessWatchpointEvent populates stacktrace on WATCHPOINT_ACCESS_HIT
    • handleModificationWatchpointEvent populates stacktrace on WATCHPOINT_MODIFY_HIT
    • handleExceptionEvent formats exceptionMessage with detailMessage when present
    • handleExceptionEvent formats exceptionMessage with class name only when detailMessage is null
    • handleExceptionEvent filters non-matching exception and returns false without calling vm resume directly
    • setWatchpoint throws INVALID_ARGUMENT when class is loaded but field does not exist
    • setWatchpoint binds across all loaded ReferenceTypes with matching field
    • setExceptionBreakpoint binds across all loaded ReferenceTypes
  • Verified ./gradlew build (detekt, :core:test, :cli:test, :core:jacocoTestReport) passes with 0 issues.

2. End-to-End Live Emulator Verification (sample-app + android-cli)

Tested against com.example.sampledebugapp running on Pixel_10_Pro_Fold (emulator-5554):

  • Invalid Watchpoint Field Validation:

    debroid watch sess_100 com.example.sampledebugapp.data.DefaultDataRepository nonExistentField --pretty

    Output:

    {
        "errorCode": "INVALID_ARGUMENT",
        "message": "Field 'nonExistentField' not found in class 'com.example.sampledebugapp.data.DefaultDataRepository'",
        "retryable": false
    }
  • Watchpoint Access & Modification Stacktraces (WATCHPOINT_ACCESS_HIT & WATCHPOINT_MODIFY_HIT):
    Set watchpoint on DefaultDataRepository.totalOrdersProcessed, tapped "Process Gold Order ($100)", and polled with --with-stacktrace:

    {
        "eventType": "WATCHPOINT_ACCESS_HIT",
        "sessionId": "sess_100",
        "threadId": "1",
        "threadName": "main",
        "location": "DataRepository.kt:57",
        "className": "com.example.sampledebugapp.data.DefaultDataRepository",
        "exceptionMessage": "Field totalOrdersProcessed accessed (current value: 0)",
        "stacktrace": [
            {
                "frameIndex": 0,
                "methodName": "processOrder",
                "declaringClass": "com.example.sampledebugapp.data.DefaultDataRepository",
                "sourceFile": "DataRepository.kt",
                "lineNumber": 57
            },
            ...
        ]
    }

    Resumed and polled WATCHPOINT_MODIFY_HIT with --with-stacktrace:

    {
        "eventType": "WATCHPOINT_MODIFY_HIT",
        "sessionId": "sess_100",
        "threadId": "1",
        "threadName": "main",
        "location": "DataRepository.kt:57",
        "className": "com.example.sampledebugapp.data.DefaultDataRepository",
        "exceptionMessage": "Field totalOrdersProcessed modified to 1",
        "stacktrace": [
            {
                "frameIndex": 0,
                "methodName": "processOrder",
                "declaringClass": "com.example.sampledebugapp.data.DefaultDataRepository",
                "sourceFile": "DataRepository.kt",
                "lineNumber": 57
            },
            ...
        ]
    }
  • Exception Breakpoint detailMessage & Stacktrace (EXCEPTION_HIT):
    Set exception breakpoint via debroid catch-exception sess_100 java.lang.IllegalArgumentException --caught --uncaught, tapped "Trigger Exception Order", and polled with --with-stacktrace:

    {
        "eventType": "EXCEPTION_HIT",
        "sessionId": "sess_100",
        "threadId": "1",
        "threadName": "main",
        "location": "DataRepository.kt:25",
        "className": "com.example.sampledebugapp.data.OrderProcessor",
        "exceptionMessage": "java.lang.IllegalArgumentException: Unknown customer type: UNKNOWN_TYPE",
        "stacktrace": [
            {
                "frameIndex": 0,
                "methodName": "calculateTotal",
                "declaringClass": "com.example.sampledebugapp.data.OrderProcessor",
                "sourceFile": "DataRepository.kt",
                "lineNumber": 25
            },
            ...
        ]
    }

@PatilShreyas
PatilShreyas enabled auto-merge (squash) October 26, 2026 12:33
@PatilShreyas
PatilShreyas merged commit e5055d8 into main Oct 26, 2026
2 checks passed
@PatilShreyas
PatilShreyas deleted the fix/issue-111-watchpoint-exception-enrichment branch October 26, 2026 12:35
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Labels

None yet

Projects

None yet

Development

Successfully merging this pull request may close these issues.

fix(core): enrich watchpoint/exception events with stacktrace and detailMessage, and validate watchpoint fields

1 participant