Module: DebugMcp::ClientCleanup
- Defined in:
- lib/debug_mcp/client_cleanup.rb
Overview
Shared cleanup logic for graceful disconnect. Used by Disconnect tool and SessionManager reaper to avoid ~100 lines of duplication. Performs: stdout restore, breakpoint deletion, SIGINT handler restore, process resume, and stale pause defense.
Class Method Summary collapse
-
.cleanup_and_resume(client, deadline:, max_stale_retries: 2) ⇒ Object
Perform best-effort cleanup and resume a paused debug client.
-
.delete_all_breakpoints(client, deadline) ⇒ Object
Delete all breakpoints from the debug session.
Class Method Details
.cleanup_and_resume(client, deadline:, max_stale_retries: 2) ⇒ Object
Perform best-effort cleanup and resume a paused debug client. The client MUST be paused before calling this method.
14 15 16 17 18 19 20 21 22 23 24 25 26 27 28 29 30 31 32 33 34 35 36 37 38 39 40 41 42 43 44 45 46 47 48 49 50 51 52 53 54 55 56 57 58 59 60 61 62 63 64 65 66 67 68 69 70 71 72 73 74 75 76 77 78 |
# File 'lib/debug_mcp/client_cleanup.rb', line 14 def self.cleanup_and_resume(client, deadline:, max_stale_retries: 2) # Restore $stdout if evaluate_code left it redirected (its ensure block # fails when send_command timeout sets @paused=false). remaining = deadline - Time.now if remaining > 0 begin client.send_command( '$stdout = STDOUT if $stdout != STDOUT', timeout: [remaining, 1].min, ) rescue DebugMcp::Error # Best-effort end end # Delete all breakpoints FIRST — this is the most critical step. # If breakpoints remain and the process resumes without a client, # it will hit a breakpoint and become stuck with no way to continue. delete_all_breakpoints(client, deadline) # Restore original SIGINT handler remaining = deadline - Time.now if remaining > 0 begin client.send_command( "p $_debug_mcp_orig_int ? (trap('INT',$_debug_mcp_orig_int);$_debug_mcp_orig_int=nil;:ok) : nil", timeout: [remaining, 2].min, ) rescue DebugMcp::Error # Best-effort end end # Resume the process. If a cleanup command timed out (setting # @paused=false even though the process is actually still paused), # use force: true to bypass the @paused check. client.send_command_no_wait("c", force: true) # Wait for the debug gem to settle after 'c'. After SIGINT recovery, # the main thread needs to finish the interrupted eval and re-enter # the command loop (sending `input PID`). If we close the socket # before this completes, the debug gem's cleanup_reader closes @q_msg # while the main thread is still pushing results, leaving it stuck # on a futex. Draining here gives the debug gem time to settle. client.ensure_paused(timeout: 2) # Stale pause defense: after 'c' → ensure_paused, the process might # have been re-paused by a stale `pause` message left in the debug # gem's socket buffer. If still paused, delete remaining BPs and # send 'c' again (bounded retries to prevent infinite loop). stale_retries = 0 while client.paused && stale_retries < max_stale_retries stale_retries += 1 remaining = deadline - Time.now break if remaining <= 0 delete_all_breakpoints(client, deadline) remaining = deadline - Time.now break if remaining <= 0 client.send_command_no_wait("c", force: true) client.ensure_paused(timeout: [remaining, 1].min) end end |
.delete_all_breakpoints(client, deadline) ⇒ Object
Delete all breakpoints from the debug session.
83 84 85 86 87 88 89 90 91 92 93 94 95 96 97 98 99 100 |
# File 'lib/debug_mcp/client_cleanup.rb', line 83 def self.delete_all_breakpoints(client, deadline) remaining = deadline - Time.now return if remaining <= 0 bp_output = client.send_command("info breakpoints", timeout: [remaining, 2].min) return if bp_output.strip.empty? bp_output.each_line do |line| remaining = deadline - Time.now break if remaining <= 0 if (match = line.match(/#(\d+)/)) client.send_command("delete #{match[1]}", timeout: [remaining, 2].min) rescue nil end end rescue DebugMcp::Error # Best-effort end |