Skip to content
New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

Open Chrome automatically #334

Merged
merged 4 commits into from
Dec 14, 2021
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Jump to
Jump to file
Failed to load files.
Diff view
Diff view
1 change: 1 addition & 0 deletions README.md
Original file line number Diff line number Diff line change
Expand Up @@ -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)
Expand Down
1 change: 1 addition & 0 deletions lib/debug/config.rb
Original file line number Diff line number Diff line change
Expand Up @@ -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],
Expand Down
46 changes: 29 additions & 17 deletions lib/debug/server.rb
Original file line number Diff line number Diff line change
Expand Up @@ -75,19 +75,23 @@ 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
# ignore
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+(.*)$/
Expand Down Expand Up @@ -118,8 +122,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
Expand Down Expand Up @@ -330,29 +334,37 @@ def initialize host: nil, port: nil
super()
end

def chrome_setup
require_relative 'server_cdp'
ono-max marked this conversation as resolved.
Show resolved Hide resolved

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

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}

EOS
chrome_setup if CONFIG[:open_frontend] == 'chrome'

Socket.accept_loop(socks) do |sock, client|
@client_addr = client
Expand Down
163 changes: 157 additions & 6 deletions lib/debug/server_cdp.rb
Original file line number Diff line number Diff line change
Expand Up @@ -5,15 +5,162 @@
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

class WebSocket
class WebSocketServer
def initialize s
@sock = s
end
Expand Down Expand Up @@ -86,25 +233,25 @@ 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

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']
Expand Down Expand Up @@ -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
Expand Down