Class: DebugMcp::Tools::Disconnect

Inherits:
MCP::Tool
  • Object
show all
Defined in:
lib/debug_mcp/tools/disconnect.rb

Constant Summary collapse

CLEANUP_DEADLINE =
3

Class Method Summary collapse

Class Method Details

.call(session_id: nil, force: nil, server_context:) ⇒ Object



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
79
80
81
82
83
84
85
86
87
88
89
90
91
92
93
94
95
96
97
98
99
100
101
102
103
104
105
106
107
108
109
110
111
112
113
114
115
116
117
118
119
120
121
122
123
124
125
126
127
128
129
130
131
132
133
134
135
136
137
138
139
140
141
142
143
144
145
146
147
148
149
150
151
152
153
154
155
156
157
158
159
# File 'lib/debug_mcp/tools/disconnect.rb', line 39

def call(session_id: nil, force: nil, server_context:)
  manager = server_context[:session_manager]

  # Get session info before disconnecting
  begin
    client = manager.client(session_id)
    pid = client.pid
    has_process = !client.wait_thread.nil?
  rescue DebugMcp::Error
    return MCP::Tool::Response.new([{ type: "text",
      text: "No active session to disconnect." }])
  end

  if force
    # Force disconnect: skip all cleanup, just close the socket immediately.
    # Use when the process is unresponsive and normal disconnect hangs.

    # For run_script sessions, still attempt to kill the spawned process
    # (Process.kill is non-blocking, so it won't hang even if the process is stuck).
    process_killed = false
    if has_process && pid
      begin
        Process.kill("TERM", pid.to_i)
        process_killed = true
      rescue Errno::ESRCH, Errno::EPERM
        # Process already exited
      end
    end

    manager.disconnect(session_id)

    text = "Force-disconnected from session (cleanup skipped)."
    text += " Process #{pid} terminated." if process_killed
    if has_process && !process_killed
      text += "\n\nWARNING: The spawned process (PID #{pid}) was NOT terminated and may still be running."
    else
      text += "\n\nWARNING: Breakpoints were NOT removed and the process was NOT resumed. " \
              "The target process may be left in a paused state."
    end
    text += "\n\nUse 'run_script' or 'connect' to start a new debug session."
    return MCP::Tool::Response.new([{ type: "text", text: text }])
  end

  # Kill the target process if launched via run_script
  process_killed = false
  if has_process && pid
    begin
      Process.kill("TERM", pid.to_i)
      process_killed = true
    rescue Errno::ESRCH, Errno::EPERM
      # Process already exited
    end
  else
    # For connect sessions: best-effort cleanup (restore SIGINT,
    # delete BPs, continue) bounded by a hard deadline.
    # If not paused, try to re-pause before cleanup.
    unless client.paused
      # 1. Try repause() first — works for both remote (TCP/Docker) and local
      begin
        client.repause(timeout: 3)
      rescue DebugMcp::Error
        # Best-effort
      end

      # 2. Fall back to interrupt_and_wait (local SIGINT only)
      unless client.paused
        begin
          client.interrupt_and_wait(timeout: 3)
        rescue DebugMcp::Error
          # Best-effort
        end
      end

      # 3. For remote connections, try HTTP wake + check_paused
      #    (repause already sent the pause message at step 1 — avoid
      #    sending more to prevent stale messages after disconnect)
      unless client.paused
        if client.remote
          if client.listen_ports&.any?
            begin
              client.wake_io_blocked_process(client.listen_ports.first)
              sleep DebugClient::HTTP_WAKE_SETTLE_TIME
              client.check_paused(timeout: 5)
            rescue DebugMcp::Error
              # Best-effort
            end
          else
            begin
              client.check_paused(timeout: 5)
            rescue DebugMcp::Error
              # Best-effort
            end
          end
        end
      end
    end

    if client.paused
      best_effort_cleanup(client)
    else
      # Both repause and interrupt failed — best-effort resume to prevent stuck process
      begin
        client.send_command_no_wait("c", force: true)
      rescue StandardError
        # Best-effort
      end
      force_warning = "Could not re-pause the process for cleanup. " \
                      "Breakpoints may remain. A resume command was sent to prevent the process from getting stuck."
    end
  end

  # Disconnect the session (closes socket, cleans up temp files)
  manager.disconnect(session_id)

  text = "Disconnected from session."
  text += " Process #{pid} terminated." if process_killed
  text += "\n\nWARNING: #{force_warning}" if force_warning
  text += "\n\nUse 'run_script' or 'connect' to start a new debug session."

  MCP::Tool::Response.new([{ type: "text", text: text }])
end