From 01a16359306f22bc5ea1bdd0ddf51dddfbb2c446 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?=E5=B0=8F=E9=87=8E=20=E7=9B=B4=E4=BA=BA?= Date: Tue, 19 Oct 2021 11:04:28 +0900 Subject: [PATCH 1/4] Rename `WebSocket` to `WebSocketServer` --- lib/debug/server.rb | 4 ++-- lib/debug/server_cdp.rb | 12 ++++++------ 2 files changed, 8 insertions(+), 8 deletions(-) diff --git a/lib/debug/server.rb b/lib/debug/server.rb index 4efe33741..53363159d 100644 --- a/lib/debug/server.rb +++ b/lib/debug/server.rb @@ -118,8 +118,8 @@ def greeting @repl = false CONFIG.set_config no_color: true - @web_sock = UI_CDP::WebSocket.new(@sock) - @web_sock.handshake + @ws_server = UI_CDP::WebSocketServer.new(@sock) + @ws_server.handshake else raise "Greeting message error: #{g}" end diff --git a/lib/debug/server_cdp.rb b/lib/debug/server_cdp.rb index 5e3f0f742..b3f603abc 100644 --- a/lib/debug/server_cdp.rb +++ b/lib/debug/server_cdp.rb @@ -13,7 +13,7 @@ module UI_CDP class Detach < StandardError end - class WebSocket + class WebSocketServer def initialize s @sock = s end @@ -86,17 +86,17 @@ def extract_data def send_response req, **res if res.empty? - @web_sock.send id: req['id'], result: {} + @ws_server.send id: req['id'], result: {} else - @web_sock.send id: req['id'], result: res + @ws_server.send id: req['id'], result: res end end def send_event method, **params if params.empty? - @web_sock.send method: method, params: {} + @ws_server.send method: method, params: {} else - @web_sock.send method: method, params: params + @ws_server.send method: method, params: params end end @@ -104,7 +104,7 @@ def process bps = {} @src_map = {} loop do - req = @web_sock.extract_data + req = @ws_server.extract_data $stderr.puts '[>]' + req.inspect if SHOW_PROTOCOL case req['method'] From abcf427c018d7964e947ead7e7fb53c2d48bfc63 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?=E5=B0=8F=E9=87=8E=20=E7=9B=B4=E4=BA=BA?= Date: Tue, 19 Oct 2021 14:08:03 +0900 Subject: [PATCH 2/4] Change the local variable `addr` to an instance variable --- lib/debug/server.rb | 8 ++++---- 1 file changed, 4 insertions(+), 4 deletions(-) diff --git a/lib/debug/server.rb b/lib/debug/server.rb index 53363159d..870cb9bbd 100644 --- a/lib/debug/server.rb +++ b/lib/debug/server.rb @@ -336,21 +336,21 @@ def accept begin Socket.tcp_server_sockets @host, @port do |socks| - addr = socks[0].local_address.inspect_sockaddr # Change this part if `socks` are multiple. + @addr = socks[0].local_address.inspect_sockaddr # Change this part if `socks` are multiple. rdbg = File.expand_path('../../exe/rdbg', __dir__) - DEBUGGER__.warn "Debugger can attach via TCP/IP (#{addr})" + DEBUGGER__.warn "Debugger can attach via TCP/IP (#{@addr})" DEBUGGER__.info <<~EOS With rdbg, use the following command line: # - # #{rdbg} --attach #{addr.split(':').join(' ')} + # #{rdbg} --attach #{@addr.split(':').join(' ')} # EOS DEBUGGER__.warn <<~EOS if CONFIG[:open_frontend] == 'chrome' With Chrome browser, type the following URL in the address-bar: - devtools://devtools/bundled/inspector.html?ws=#{addr} + devtools://devtools/bundled/inspector.html?ws=#{@addr} EOS From 71bc5b5bc3d794516b72e1771d23bb1578ab94fd Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?=E5=B0=8F=E9=87=8E=20=E7=9B=B4=E4=BA=BA?= Date: Tue, 19 Oct 2021 14:17:34 +0900 Subject: [PATCH 3/4] Open Chrome automatically --- lib/debug/config.rb | 1 + lib/debug/server.rb | 36 ++++++---- lib/debug/server_cdp.rb | 151 ++++++++++++++++++++++++++++++++++++++++ 3 files changed, 176 insertions(+), 12 deletions(-) diff --git a/lib/debug/config.rb b/lib/debug/config.rb index 565fcf22b..dc9331bb1 100644 --- a/lib/debug/config.rb +++ b/lib/debug/config.rb @@ -43,6 +43,7 @@ module DEBUGGER__ sock_dir: ['RUBY_DEBUG_SOCK_DIR', "REMOTE: UNIX Domain Socket remote debugging: socket directory"], cookie: ['RUBY_DEBUG_COOKIE', "REMOTE: Cookie for negotiation"], open_frontend: ['RUBY_DEBUG_OPEN_FRONTEND',"REMOTE: frontend used by open command (vscode, chrome, default: rdbg)."], + chrome_path: ['RUBY_DEBUG_CHROME_PATH', "REMOTE: Platform dependent path of Chrome (For more information, See [here](https://github.com/ruby/debug/pull/334/files#diff-5fc3d0a901379a95bc111b86cf0090b03f857edfd0b99a0c1537e26735698453R55-R64))"], # obsolete parent_on_fork: ['RUBY_DEBUG_PARENT_ON_FORK', "OBSOLETE: Keep debugging parent process on fork (default: false)", :bool], diff --git a/lib/debug/server.rb b/lib/debug/server.rb index 870cb9bbd..26dae7f60 100644 --- a/lib/debug/server.rb +++ b/lib/debug/server.rb @@ -75,12 +75,7 @@ def activate session, on_fork: false DEBUGGER__.warn "ReaderThreadError: #{e}" pp e.backtrace ensure - DEBUGGER__.warn "Disconnected." - @sock = nil - @q_msg.close - @q_msg = nil - @q_ans.close - @q_ans = nil + cleanup_reader end # accept rescue Terminate @@ -88,6 +83,15 @@ def activate session, on_fork: false end end + def cleanup_reader + DEBUGGER__.warn "Disconnected." + @sock = nil + @q_msg.close + @q_msg = nil + @q_ans.close + @q_ans = nil + end + def greeting case g = @sock.gets when /^version:\s+(.+)\s+width: (\d+) cookie:\s+(.*)$/ @@ -330,6 +334,19 @@ def initialize host: nil, port: nil super() end + def chrome_setup + require_relative 'server_cdp' + + unless @chrome_pid = UI_CDP.setup_chrome(@addr) + DEBUGGER__.warn <<~EOS if CONFIG[:open_frontend] == 'chrome' + With Chrome browser, type the following URL in the address-bar: + + devtools://devtools/bundled/inspector.html?ws=#{@addr} + + EOS + end + end + def accept retry_cnt = 0 super # for fork @@ -347,12 +364,7 @@ def accept # EOS - DEBUGGER__.warn <<~EOS if CONFIG[:open_frontend] == 'chrome' - With Chrome browser, type the following URL in the address-bar: - - devtools://devtools/bundled/inspector.html?ws=#{@addr} - - EOS + chrome_setup if CONFIG[:open_frontend] == 'chrome' Socket.accept_loop(socks) do |sock, client| @client_addr = client diff --git a/lib/debug/server_cdp.rb b/lib/debug/server_cdp.rb index b3f603abc..953b82bc6 100644 --- a/lib/debug/server_cdp.rb +++ b/lib/debug/server_cdp.rb @@ -5,11 +5,158 @@ require 'base64' require 'securerandom' require 'stringio' +require 'open3' +require 'tmpdir' module DEBUGGER__ module UI_CDP SHOW_PROTOCOL = ENV['RUBY_DEBUG_CDP_SHOW_PROTOCOL'] == '1' + class << self + def setup_chrome addr + port, path, pid = run_new_chrome + begin + s = Socket.tcp '127.0.0.1', port + rescue Errno::ECONNREFUSED + return + end + + ws_client = WebSocketClient.new(s) + ws_client.handshake port, path + ws_client.send id: 1, method: 'Target.getTargets' + + 3.times do + res = ws_client.extract_data + case + when res['id'] == 1 && target_info = res.dig('result', 'targetInfos') + page = target_info.find{|t| t['type'] == 'page'} + ws_client.send id: 2, method: 'Target.attachToTarget', + params: { + targetId: page['targetId'], + flatten: true + } + when res['id'] == 2 + s_id = res.dig('result', 'sessionId') + sleep 0.1 + ws_client.send sessionId: s_id, id: 1, + method: 'Page.navigate', + params: { + url: "devtools://devtools/bundled/inspector.html?ws=#{addr}" + } + end + end + pid + end + + def get_chrome_path + return CONFIG[:chrome_path] if CONFIG[:chrome_path] + + # The process to check OS is based on `selenium` project. + case RbConfig::CONFIG['host_os'] + when /mswin|msys|mingw|cygwin|emc/ + 'C:\Program Files (x86)\Google\Chrome\Application\chrome.exe' + when /darwin|mac os/ + '/Applications/Google\ Chrome.app/Contents/MacOS/Google\ Chrome' + when /linux/ + 'google-chrome' + else + raise "Unsupported OS" + end + end + + def run_new_chrome + dir = Dir.mktmpdir + at_exit{ + FileUtils.rm_rf dir + } + # The command line flags are based on: https://developer.mozilla.org/en-US/docs/Tools/Remote_Debugging/Chrome_Desktop#connecting + stdin, stdout, stderr, wait_thr = *Open3.popen3("#{get_chrome_path} --remote-debugging-port=0 --no-first-run --no-default-browser-check --user-data-dir=#{dir}") + stdin.close + stdout.close + + data = stderr.readpartial 4096 + if data.match /DevTools listening on ws:\/\/127.0.0.1:(\d+)(.*)/ + port = $1 + path = $2 + end + stderr.close + [port, path, wait_thr.pid] + end + end + + class WebSocketClient + def initialize s + @sock = s + end + + def handshake port, path + key = SecureRandom.hex(11) + @sock.print "GET #{path} HTTP/1.1\r\nHost: 127.0.0.1:#{port}\r\nConnection: Upgrade\r\nUpgrade: websocket\r\nSec-WebSocket-Version: 13\r\nSec-WebSocket-Key: #{key}==\r\n\r\n" + res = @sock.readpartial 4092 + $stderr.puts '[>]' + res if SHOW_PROTOCOL + + if res.match /^Sec-WebSocket-Accept: (.*)\r\n/ + correct_key = Base64.strict_encode64 Digest::SHA1.digest "#{key}==258EAFA5-E914-47DA-95CA-C5AB0DC85B11" + raise "The Sec-WebSocket-Accept value: #{$1} is not valid" unless $1 == correct_key + else + raise "Unknown response: #{res}" + end + end + + def send **msg + msg = JSON.generate(msg) + frame = [] + fin = 0b10000000 + opcode = 0b00000001 + frame << fin + opcode + + mask = 0b10000000 # A client must mask all frames in a WebSocket Protocol. + bytesize = msg.bytesize + if bytesize < 126 + payload_len = bytesize + elsif bytesize < 2 ** 16 + payload_len = 0b01111110 + ex_payload_len = [bytesize].pack('n*').bytes + else + payload_len = 0b01111111 + ex_payload_len = [bytesize].pack('Q>').bytes + end + + frame << mask + payload_len + frame.push *ex_payload_len if ex_payload_len + + frame.push *masking_key = 4.times.map{rand(1..255)} + masked = [] + msg.bytes.each_with_index do |b, i| + masked << (b ^ masking_key[i % 4]) + end + + frame.push *masked + @sock.print frame.pack 'c*' + end + + def extract_data + first_group = @sock.getbyte + fin = first_group & 0b10000000 != 128 + raise 'Unsupported' if fin + opcode = first_group & 0b00001111 + raise "Unsupported: #{opcode}" unless opcode == 1 + + second_group = @sock.getbyte + mask = second_group & 0b10000000 == 128 + raise 'The server must not mask any frames' if mask + payload_len = second_group & 0b01111111 + # TODO: Support other payload_lengths + if payload_len == 126 + payload_len = @sock.read(2).unpack('n*')[0] + end + + data = JSON.parse @sock.read payload_len + $stderr.puts '[>]' + data.inspect if SHOW_PROTOCOL + data + end + end + class Detach < StandardError end @@ -288,6 +435,10 @@ def deactivate_bp @q_ans << 'y' end + def cleanup_reader + Process.kill :KILL, @chrome_pid + end + ## Called by the SESSION thread def readline prompt From 5d2ab5e4a27241f65edd8b601ee2da4667a5e813 Mon Sep 17 00:00:00 2001 From: ono-max Date: Wed, 8 Dec 2021 17:35:15 +0900 Subject: [PATCH 4/4] Update README --- README.md | 1 + 1 file changed, 1 insertion(+) diff --git a/README.md b/README.md index c6adf0a0c..756ccd7eb 100644 --- a/README.md +++ b/README.md @@ -487,6 +487,7 @@ config set no_color true * `RUBY_DEBUG_SOCK_DIR` (`sock_dir`): UNIX Domain Socket remote debugging: socket directory * `RUBY_DEBUG_COOKIE` (`cookie`): Cookie for negotiation * `RUBY_DEBUG_OPEN_FRONTEND` (`open_frontend`): frontend used by open command (vscode, chrome, default: rdbg). + * `RUBY_DEBUG_CHROME_PATH` (`chrome_path`): Platform dependent path of Chrome (For more information, See [here](https://github.com/ruby/debug/pull/334/files#diff-5fc3d0a901379a95bc111b86cf0090b03f857edfd0b99a0c1537e26735698453R55-R64)) * OBSOLETE * `RUBY_DEBUG_PARENT_ON_FORK` (`parent_on_fork`): Keep debugging parent process on fork (default: false)