Skip to content

Commit 124004f

Browse files
author
小野 直人
committed
Open Chrome automatically
1 parent 52c40d2 commit 124004f

File tree

3 files changed

+151
-9
lines changed

3 files changed

+151
-9
lines changed

lib/debug/config.rb

+1
Original file line numberDiff line numberDiff line change
@@ -43,6 +43,7 @@ module DEBUGGER__
4343
sock_dir: ['RUBY_DEBUG_SOCK_DIR', "REMOTE: UNIX Domain Socket remote debugging: socket directory"],
4444
cookie: ['RUBY_DEBUG_COOKIE', "REMOTE: Cookie for negotiation"],
4545
open_frontend: ['RUBY_DEBUG_OPEN_FRONTEND',"REMOTE: frontend used by open command (vscode, chrome, default: rdbg)."],
46+
chrome_path: ['RUBY_DEBUG_CHROME_PATH', "REMOTE: Current path in Google Chrome"],
4647

4748
# obsolete
4849
parent_on_fork: ['RUBY_DEBUG_PARENT_ON_FORK', "OBSOLETE: Keep debugging parent process on fork (default: false)", :bool],

lib/debug/server.rb

+13-9
Original file line numberDiff line numberDiff line change
@@ -81,6 +81,10 @@ def activate session, on_fork: false
8181
@q_msg = nil
8282
@q_ans.close
8383
@q_ans = nil
84+
if CONFIG[:open_frontend] == 'chrome'
85+
Process.kill(:KILL, @wait_thr.pid)
86+
FileUtils.remove_entry_secure @dir
87+
end
8488
end # accept
8589

8690
rescue Terminate
@@ -112,9 +116,6 @@ def greeting
112116
@repl = false
113117
dap_setup @sock.read($1.to_i)
114118
when /^GET \/ HTTP\/1.1/
115-
require_relative 'server_cdp'
116-
117-
self.extend(UI_CDP)
118119
@repl = false
119120
@ws_server = UI_CDP::WebSocketServer.new(@sock)
120121
@ws_server.handshake
@@ -328,6 +329,13 @@ def initialize host: nil, port: nil
328329
super()
329330
end
330331

332+
def chrome_setup
333+
require_relative 'server_cdp'
334+
335+
self.extend(UI_CDP)
336+
setup_chrome
337+
end
338+
331339
def accept
332340
retry_cnt = 0
333341
super # for fork
@@ -345,12 +353,7 @@ def accept
345353
#
346354
EOS
347355

348-
DEBUGGER__.warn <<~EOS if CONFIG[:open_frontend] == 'chrome'
349-
With Chrome browser, type the following URL in the address-bar:
350-
351-
devtools://devtools/bundled/inspector.html?ws=#{@addr}
352-
353-
EOS
356+
chrome_setup if CONFIG[:open_frontend] == 'chrome'
354357

355358
Socket.accept_loop(socks) do |sock, client|
356359
@client_addr = client
@@ -374,6 +377,7 @@ def accept
374377
end
375378
ensure
376379
@sock_for_fork = nil
380+
Process.kill(:KILL, @wait_thr.pid) if CONFIG[:open_frontend] == 'chrome'
377381
end
378382
end
379383

lib/debug/server_cdp.rb

+137
Original file line numberDiff line numberDiff line change
@@ -9,6 +9,143 @@ module DEBUGGER__
99
module UI_CDP
1010
SHOW_PROTOCOL = ENV['RUBY_DEBUG_CDP_SHOW_PROTOCOL'] == '1'
1111

12+
def setup_chrome
13+
require 'open3'
14+
require 'tmpdir'
15+
16+
set_chrome_path
17+
@dir = Dir.mktmpdir
18+
# The command line flags are based on: https://developer.mozilla.org/en-US/docs/Tools/Remote_Debugging/Chrome_Desktop#connecting
19+
stdin, stdout, stderr, @wait_thr = *Open3.popen3("#{CONFIG[:chrome_path]} --remote-debugging-port=0 --no-first-run --no-default-browser-check --user-data-dir=#{@dir}")
20+
stdin.close
21+
stdout.close
22+
23+
data = stderr.readpartial 4096
24+
if data.match /DevTools listening on ws:\/\/127.0.0.1:(\d+)(.*)/
25+
port = $1
26+
path = $2
27+
else
28+
if File.exist? "#{@dir}/DevToolsActivePort"
29+
port, path = File.read("#{@dir}/DevToolsActivePort").split("\n")
30+
else
31+
raise "Can't open Chrome browser"
32+
end
33+
end
34+
35+
s = Socket.tcp "127.0.0.1", port
36+
ws_client = WebSocketClient.new(s)
37+
ws_client.handshake port, path
38+
ws_client.send id: 1, method: 'Target.getTargets'
39+
40+
3.times do
41+
res = ws_client.extract_data
42+
case
43+
when res['id'] == 1 && target_info = res.dig('result', 'targetInfos')
44+
p = target_info.find{|t| t['type'] == 'page'}
45+
ws_client.send id: 2, method: 'Target.attachToTarget',
46+
params: {
47+
targetId: p['targetId'],
48+
flatten: true
49+
}
50+
when res['id'] == 2
51+
s_id = res.dig('result', 'sessionId')
52+
sleep 0.1
53+
ws_client.send sessionId: s_id, id: 1,
54+
method: 'Page.navigate',
55+
params: {
56+
url: "devtools://devtools/bundled/inspector.html?ws=#{@addr}"
57+
}
58+
end
59+
end
60+
end
61+
62+
def set_chrome_path
63+
# The process to check OS is based on `selenium` project.
64+
case RbConfig::CONFIG['host_os']
65+
when /mswin|msys|mingw|cygwin|bccwin|wince|emc/
66+
CONFIG[:chrome_path] ||= 'C:\Program Files (x86)\Google\Chrome\Application\chrome.exe'
67+
when /darwin|mac os/
68+
CONFIG[:chrome_path] ||= '/Applications/Google\ Chrome.app/Contents/MacOS/Google\ Chrome'
69+
when /linux/
70+
CONFIG[:chrome_path] ||= 'google-chrome'
71+
else
72+
raise "Unsupported OS"
73+
end
74+
end
75+
76+
class WebSocketClient
77+
def initialize s
78+
@sock = s
79+
end
80+
81+
def handshake port, path
82+
key = SecureRandom.hex(11)
83+
@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"
84+
res = @sock.readpartial 4092
85+
$stderr.puts '[>]' + res if SHOW_PROTOCOL
86+
87+
if res.match /^Sec-WebSocket-Accept: (.*)\r\n/
88+
correct_key = Base64.strict_encode64 Digest::SHA1.digest "#{key}==258EAFA5-E914-47DA-95CA-C5AB0DC85B11"
89+
raise "The Sec-WebSocket-Accept value: #{$1} is not valid" unless $1 == correct_key
90+
else
91+
raise "Unknown response: #{res}"
92+
end
93+
end
94+
95+
def send **msg
96+
msg = JSON.generate(msg)
97+
frame = []
98+
fin = 0b10000000
99+
opcode = 0b00000001
100+
frame << fin + opcode
101+
102+
mask = 0b10000000 # A client must mask all frames in a WebSocket Protocol.
103+
bytesize = msg.bytesize
104+
if bytesize < 126
105+
payload_len = bytesize
106+
elsif bytesize < 2 ** 16
107+
payload_len = 0b01111110
108+
ex_payload_len = [bytesize].pack('n*').bytes
109+
else
110+
payload_len = 0b01111111
111+
ex_payload_len = [bytesize].pack('Q>').bytes
112+
end
113+
114+
frame << mask + payload_len
115+
frame.push *ex_payload_len if ex_payload_len
116+
117+
frame.push *masking_key = 4.times.map{rand(1..255)}
118+
masked = []
119+
msg.bytes.each_with_index do |b, i|
120+
masked << (b ^ masking_key[i % 4])
121+
end
122+
123+
frame.push *masked
124+
@sock.print frame.pack 'c*'
125+
end
126+
127+
def extract_data
128+
first_group = @sock.getbyte
129+
fin = first_group & 0b10000000 != 128
130+
raise 'Unsupported' if fin
131+
opcode = first_group & 0b00001111
132+
raise "Unsupported: #{opcode}" unless opcode == 1
133+
134+
second_group = @sock.getbyte
135+
mask = second_group & 0b10000000 == 128
136+
raise 'The server must not mask any frames' if mask
137+
payload_len = second_group & 0b01111111
138+
# TODO: Support other payload_lengths
139+
if payload_len == 126
140+
payload_len = @sock.read(2).unpack('n*')[0]
141+
end
142+
143+
data = JSON.parse @sock.read payload_len
144+
$stderr.puts '[>]' + data.inspect if SHOW_PROTOCOL
145+
data
146+
end
147+
end
148+
12149
class WebSocketServer
13150
def initialize s
14151
@sock = s

0 commit comments

Comments
 (0)