Module: DebugMcp::ExitMessageBuilder
- Defined in:
- lib/debug_mcp/exit_message_builder.rb
Class Method Summary collapse
-
.build_exit_message(header, final_output, client) ⇒ Object
Build a detailed exit message with exception detection.
-
.build_rerun_hint(client) ⇒ Object
Build a concrete run_script hint with the exact file/args from the session.
-
.detect_exception(output) ⇒ Object
Detect Ruby exception from output text.
-
.wait_for_process(client) ⇒ Object
Wait for the spawned process to exit (up to 5 seconds).
Class Method Details
.build_exit_message(header, final_output, client) ⇒ Object
Build a detailed exit message with exception detection. Parses stderr and debugger output to determine whether the program exited normally or due to an unhandled exception.
10 11 12 13 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 |
# File 'lib/debug_mcp/exit_message_builder.rb', line 10 def (header, final_output, client) # Wait for the process to fully exit so all output is flushed to files. # wait_thread is set by run_script; nil for connect sessions. exit_status = wait_for_process(client) stderr = client&.read_stderr_output stdout = client&.read_stdout_output # Try to detect exception from stderr first, then fall back to debugger output exception_info = detect_exception(stderr) || detect_exception(final_output) parts = [] # Build a clear header with exit status if exit_status if exit_status.success? parts << "#{header}\nExit status: 0 (success)" elsif exit_status.signaled? parts << "#{header}\nKilled by signal #{exit_status.termsig}" else parts << "#{header}\nExit status: #{exit_status.exitstatus} (error)" end else parts << header end if exception_info parts << "Unhandled exception: #{exception_info}" end parts << "Debugger output:\n#{final_output}" if final_output parts << "Program output (stdout):\n#{stdout}" if stdout parts << "Process stderr:\n#{stderr}" if stderr if stdout.nil? && stderr.nil? # Connect session: no captured output, guide toward run_script tip = "stdout/stderr are not captured for sessions started with 'connect'." if exception_info tip += "\nCheck the terminal where the debug process was started for the full stack trace." else tip += "\nThe program may have exited due to an unhandled exception — " \ "check the terminal where the debug process was started for details." end tip += "\n\nTo get better diagnostics next time:\n" \ " - Use 'run_script' instead of 'connect' to capture stdout/stderr automatically\n" \ " - Use set_breakpoint(exception_class: 'NoMethodError') to stop BEFORE " \ "an exception crashes the process" parts << tip else # run_script session: session is over, guide toward restart tip = "This debug session has ended." rerun_hint = build_rerun_hint(client) if exception_info exc_class = exception_info.split(":").first tip += "\n\nTo debug the crash:\n" \ " 1. #{rerun_hint}\n" \ " 2. set_breakpoint(exception_class: '#{exc_class}') to catch the exception before it crashes" else tip += "\n\nTo restart: #{rerun_hint}" end parts << tip end parts.join("\n\n") end |
.build_rerun_hint(client) ⇒ Object
Build a concrete run_script hint with the exact file/args from the session.
88 89 90 91 92 93 94 |
# File 'lib/debug_mcp/exit_message_builder.rb', line 88 def build_rerun_hint(client) script_file = client&.script_file return "run_script(file: '...', restore_breakpoints: true)" unless script_file args_part = client.script_args&.any? ? ", args: #{client.script_args.inspect}" : "" "run_script(file: '#{script_file}'#{args_part}, restore_breakpoints: true)" end |
.detect_exception(output) ⇒ Object
Detect Ruby exception from output text. Returns “ExceptionClass: message” string, or nil if no exception found.
98 99 100 101 102 103 104 105 106 107 108 109 110 |
# File 'lib/debug_mcp/exit_message_builder.rb', line 98 def detect_exception(output) return nil unless output && !output.empty? # Ruby stack trace format: # /path/to/file.rb:10:in `method': message (ExceptionClass) if output =~ /:\d+:in `.+': (.+) \((\w+(?:::\w+)*)\)/ "#{$2}: #{$1}" # Alternative format (e.g., from raise without stack trace context): # ExceptionClass: message elsif output =~ /\A\s*((?:\w+::)*\w+(?:Error|Exception)): (.+)/ "#{$1}: #{$2.strip}" end end |
.wait_for_process(client) ⇒ Object
Wait for the spawned process to exit (up to 5 seconds). Returns Process::Status or nil.
78 79 80 81 82 83 84 85 |
# File 'lib/debug_mcp/exit_message_builder.rb', line 78 def wait_for_process(client) return nil unless client&.wait_thread client.wait_thread.join(5) client.wait_thread.value rescue StandardError nil end |