Module: DebugMcp::TcpSessionDiscovery
- Defined in:
- lib/debug_mcp/tcp_session_discovery.rb
Class Method Summary collapse
-
.container_web_ports(debug_port, host: "localhost") ⇒ Object
Find web server host ports for a Docker container identified by its debug port.
-
.discover ⇒ Object
Discover TCP debug sessions from Docker containers and local processes.
-
.docker_available? ⇒ Boolean
— Private helpers —.
-
.docker_sessions ⇒ Object
Discover debug sessions from Docker containers with RUBY_DEBUG_PORT.
- .inspect_container(container_id) ⇒ Object
- .inspect_local_process(environ_path) ⇒ Object
-
.local_tcp_sessions ⇒ Object
Discover debug sessions from local processes with RUBY_DEBUG_PORT in /proc/*/environ.
- .process_name(pid) ⇒ Object
- .resolve_host_port(container, container_port) ⇒ Object
-
.tcp_connectable?(host, port, timeout: 2) ⇒ Boolean
Check if a TCP host:port is connectable.
Class Method Details
.container_web_ports(debug_port, host: "localhost") ⇒ Object
Find web server host ports for a Docker container identified by its debug port. Reverse-looks up which container has RUBY_DEBUG_PORT matching debug_port, then returns all other host-mapped ports that are TCP-connectable. Returns an array of port numbers (e.g., [3000]) or [] if not found.
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 |
# File 'lib/debug_mcp/tcp_session_discovery.rb', line 76 def container_web_ports(debug_port, host: "localhost") return [] unless docker_available? container_ids = `docker ps -q 2>/dev/null`.strip.split("\n") container_ids.each do |id| json_str = `docker inspect #{id} 2>/dev/null` data = JSON.parse(json_str) container = data[0] next unless container # Check if this container has the matching debug port env_list = container.dig("Config", "Env") || [] container_debug_port = env_list .find { |e| e.start_with?("RUBY_DEBUG_PORT=") } &.split("=", 2)&.last&.to_i next unless container_debug_port == debug_port # Extract all host-mapped ports except the debug port itself port_bindings = container.dig("HostConfig", "PortBindings") || {} web_ports = [] port_bindings.each do |key, bindings| container_port = key.split("/").first.to_i next if container_port == debug_port next unless bindings.is_a?(Array) && !bindings.empty? host_port = bindings[0]["HostPort"]&.to_i next unless host_port&.positive? next unless tcp_connectable?(host, host_port, timeout: 1) web_ports << host_port end return web_ports.sort end [] rescue StandardError [] end |
.discover ⇒ Object
Discover TCP debug sessions from Docker containers and local processes. Returns array of { host:, port:, name:, source: } hashes.
12 13 14 15 16 |
# File 'lib/debug_mcp/tcp_session_discovery.rb', line 12 def discover (docker_sessions + local_tcp_sessions).uniq { |s| [s[:host], s[:port]] } rescue StandardError [] end |
.docker_available? ⇒ Boolean
— Private helpers —
118 119 120 121 122 |
# File 'lib/debug_mcp/tcp_session_discovery.rb', line 118 def docker_available? system("docker", "info", out: File::NULL, err: File::NULL) rescue StandardError false end |
.docker_sessions ⇒ Object
Discover debug sessions from Docker containers with RUBY_DEBUG_PORT.
19 20 21 22 23 24 25 26 27 28 29 30 31 32 33 |
# File 'lib/debug_mcp/tcp_session_discovery.rb', line 19 def docker_sessions return [] unless docker_available? container_ids = `docker ps -q 2>/dev/null`.strip.split("\n") return [] if container_ids.empty? sessions = [] container_ids.each do |id| session = inspect_container(id) sessions << session if session end sessions rescue StandardError [] end |
.inspect_container(container_id) ⇒ Object
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/tcp_session_discovery.rb', line 124 def inspect_container(container_id) json_str = `docker inspect #{container_id} 2>/dev/null` return nil if json_str.strip.empty? data = JSON.parse(json_str) container = data[0] return nil unless container env_list = container.dig("Config", "Env") || [] debug_port = nil debug_host = nil env_list.each do |env| key, value = env.split("=", 2) case key when "RUBY_DEBUG_PORT" debug_port = value&.to_i when "RUBY_DEBUG_HOST" debug_host = value end end return nil unless debug_port host_port = resolve_host_port(container, debug_port) return nil unless host_port host, port = host_port return nil unless tcp_connectable?(host, port) name = container.fetch("Name", "").sub(%r{\A/}, "") name = container_id[0, 12] if name.empty? { host: host, port: port, name: name, source: :docker } rescue StandardError nil end |
.inspect_local_process(environ_path) ⇒ Object
184 185 186 187 188 189 190 191 192 193 194 195 196 197 198 199 200 201 202 203 204 205 206 207 208 209 210 211 |
# File 'lib/debug_mcp/tcp_session_discovery.rb', line 184 def inspect_local_process(environ_path) environ = File.read(environ_path) envs = environ.split("\0") debug_port = nil envs.each do |env| key, value = env.split("=", 2) if key == "RUBY_DEBUG_PORT" debug_port = value&.to_i break end end return nil unless debug_port pid = environ_path[%r{/proc/(\d+)/}, 1] return nil unless pid # Skip if this process is ourselves return nil if pid.to_i == Process.pid host = "localhost" return nil unless tcp_connectable?(host, debug_port) name = process_name(pid) { host: host, port: debug_port, name: name, source: :local } rescue StandardError nil end |
.local_tcp_sessions ⇒ Object
Discover debug sessions from local processes with RUBY_DEBUG_PORT in /proc/*/environ.
36 37 38 39 40 41 42 43 44 45 46 47 48 49 |
# File 'lib/debug_mcp/tcp_session_discovery.rb', line 36 def local_tcp_sessions return [] unless File.directory?("/proc") sessions = [] Dir.glob("/proc/[0-9]*/environ").each do |environ_path| session = inspect_local_process(environ_path) sessions << session if session rescue Errno::EACCES, Errno::ENOENT next end sessions rescue StandardError [] end |
.process_name(pid) ⇒ Object
213 214 215 216 217 218 219 220 221 222 223 224 |
# File 'lib/debug_mcp/tcp_session_discovery.rb', line 213 def process_name(pid) cmdline = File.read("/proc/#{pid}/cmdline").split("\0") # Find the Ruby script name from the command line ruby_idx = cmdline.index { |arg| arg.match?(/ruby|rails|rdbg|bundle/) } if ruby_idx && cmdline[ruby_idx + 1] File.basename(cmdline[ruby_idx + 1]) else "pid-#{pid}" end rescue StandardError "pid-#{pid}" end |
.resolve_host_port(container, container_port) ⇒ Object
161 162 163 164 165 166 167 168 169 170 171 172 173 174 175 176 177 178 179 180 181 182 |
# File 'lib/debug_mcp/tcp_session_discovery.rb', line 161 def resolve_host_port(container, container_port) port_bindings = container.dig("HostConfig", "PortBindings") || {} # Look for a binding matching the debug port (e.g., "12345/tcp") binding_key = port_bindings.keys.find { |k| k.start_with?("#{container_port}/") } if binding_key bindings = port_bindings[binding_key] if bindings.is_a?(Array) && !bindings.empty? host_port = bindings[0]["HostPort"]&.to_i return ["localhost", host_port] if host_port && host_port > 0 end end # Fallback: NetworkSettings — use container IP directly networks = container.dig("NetworkSettings", "Networks") || {} networks.each_value do |net| ip = net["IPAddress"] return [ip, container_port] if ip && !ip.empty? end nil end |
.tcp_connectable?(host, port, timeout: 2) ⇒ Boolean
Check if a TCP host:port is connectable.
52 53 54 55 56 57 58 59 60 61 62 63 64 65 66 67 68 69 70 |
# File 'lib/debug_mcp/tcp_session_discovery.rb', line 52 def tcp_connectable?(host, port, timeout: 2) addr = Socket.getaddrinfo(host, nil, nil, :STREAM) sockaddr = Socket.sockaddr_in(port, addr[0][3]) socket = Socket.new(Socket.const_get(addr[0][0]), Socket::SOCK_STREAM, 0) begin socket.connect_nonblock(sockaddr) rescue IO::WaitWritable IO.select(nil, [socket], nil, timeout) ? socket.connect_nonblock(sockaddr) : (return false) rescue Errno::EISCONN # Already connected — success end true rescue StandardError false ensure socket&.close end |