Skip to content

Commit 7178fcf

Browse files
author
小野 直人
committed
WIP
1 parent 3a26a6c commit 7178fcf

File tree

2 files changed

+136
-8
lines changed

2 files changed

+136
-8
lines changed

lib/debug/server.rb

Lines changed: 65 additions & 8 deletions
Original file line numberDiff line numberDiff line change
@@ -77,6 +77,10 @@ def activate session, on_fork: false
7777
@q_msg = nil
7878
@q_ans.close
7979
@q_ans = nil
80+
if CONFIG[:open_frontend] == 'chrome'
81+
Process.kill(:KILL, @wait_thr.pid)
82+
FileUtils.remove_entry_secure @dir
83+
end
8084
end # accept
8185

8286
rescue Terminate
@@ -108,8 +112,6 @@ def greeting
108112
@repl = false
109113
dap_setup @sock.read($1.to_i)
110114
when /^GET \/ HTTP\/1.1/
111-
require_relative 'server_cdp'
112-
113115
self.extend(UI_CDP)
114116
@repl = false
115117
@ws_server = UI_CDP::WebSocketServer.new(@sock)
@@ -271,6 +273,66 @@ def initialize host: nil, port: nil
271273
super()
272274
end
273275

276+
def chrome_setup
277+
require 'open3'
278+
require 'socket'
279+
require 'tmpdir'
280+
281+
require_relative 'server_cdp'
282+
283+
# The process to check OS is based on `selenium` project.
284+
case RbConfig::CONFIG['host_os']
285+
when /mswin|msys|mingw|cygwin|bccwin|wince|emc/
286+
chrome_path = 'chrome.exe'
287+
when /darwin|mac os/
288+
chrome_path = '/Applications/Google\ Chrome.app/Contents/MacOS/Google\ Chrome'
289+
when /linux/
290+
chrome_path = 'google-chrome'
291+
else
292+
raise "Unsupported OS"
293+
end
294+
295+
@dir = Dir.mktmpdir
296+
# The command line flags are based on: https://developer.mozilla.org/en-US/docs/Tools/Remote_Debugging/Chrome_Desktop#connecting
297+
stdin, stdout, stderr, @wait_thr = *Open3.popen3("#{chrome_path} --remote-debugging-port=0 --no-first-run --no-default-browser-check --user-data-dir=#{@dir}")
298+
stdin.close
299+
stdout.close
300+
301+
data = stderr.readpartial 4096
302+
if data.match /DevTools listening on ws:\/\/127.0.0.1:(\d+)\/(.*)/
303+
port = $1
304+
path = $2
305+
else
306+
raise "Can't open Chrome browser: #{data}"
307+
end
308+
309+
s = Socket.tcp "127.0.0.1", port
310+
ws_client = UI_CDP::WebSocketClient.new(s)
311+
ws_client.handshake port, path
312+
ws_client.send id: 1, method: 'Target.getTargets'
313+
314+
3.times do
315+
res = ws_client.extract_data
316+
case
317+
when res['id'] == 1 && target_info = res.dig('result', 'targetInfos')
318+
p = target_info.find{|t| t['type'] == 'page'}
319+
ws_client.send id: 2, method: 'Target.attachToTarget',
320+
params: {
321+
targetId: p['targetId'],
322+
flatten: true
323+
}
324+
when res['id'] == 2
325+
sleep 0.1
326+
s_id = res.dig('result', 'sessionId')
327+
ws_client.send sessionId: s_id, id: 3,
328+
method: 'Page.navigate',
329+
params: {
330+
url: "devtools://devtools/bundled/inspector.html?ws=#{@addr}"
331+
}
332+
end
333+
end
334+
end
335+
274336
def accept
275337
retry_cnt = 0
276338
super # for fork
@@ -288,12 +350,7 @@ def accept
288350
#
289351
EOS
290352

291-
DEBUGGER__.warn <<~EOS if CONFIG[:open_frontend] == 'chrome'
292-
With Chrome browser, type the following URL in the address-bar:
293-
294-
devtools://devtools/bundled/inspector.html?ws=#{@addr}
295-
296-
EOS
353+
chrome_setup if CONFIG[:open_frontend] == 'chrome'
297354

298355
Socket.accept_loop(socks) do |sock, client|
299356
@client_addr = client

lib/debug/server_cdp.rb

Lines changed: 71 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -9,6 +9,77 @@ module DEBUGGER__
99
module UI_CDP
1010
SHOW_PROTOCOL = ENV['RUBY_DEBUG_CDP_SHOW_PROTOCOL'] == '1'
1111

12+
class WebSocketClient
13+
def initialize s
14+
@sock = s
15+
end
16+
17+
def handshake port, path
18+
key = SecureRandom.hex(11)
19+
@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"
20+
res = @sock.readpartial 4092
21+
if res.match /^Sec-WebSocket-Accept: (.*)\r\n/
22+
correct_key = Base64.strict_encode64 Digest::SHA1.digest "#{key}==258EAFA5-E914-47DA-95CA-C5AB0DC85B11"
23+
raise "The Sec-WebSocket-Accept value: #{$1} is not valid" unless $1 == correct_key
24+
else
25+
raise "Unknown response: #{res}"
26+
end
27+
end
28+
29+
def send **msg
30+
msg = JSON.generate(msg)
31+
frame = []
32+
fin = 0b10000000
33+
opcode = 0b00000001
34+
frame << fin + opcode
35+
36+
mask = 0b10000000 # A client must mask all frames in a WebSocket Protocol.
37+
bytesize = msg.bytesize
38+
if bytesize < 126
39+
payload_len = bytesize
40+
elsif bytesize < 2 ** 16
41+
payload_len = 0b01111110
42+
ex_payload_len = [bytesize].pack('n*').bytes
43+
else
44+
payload_len = 0b01111111
45+
ex_payload_len = [bytesize].pack('Q>').bytes
46+
end
47+
48+
frame << mask + payload_len
49+
frame.push *ex_payload_len if ex_payload_len
50+
51+
frame.push *masking_key = 4.times.map{rand(1..255)}
52+
masked = []
53+
msg.bytes.each_with_index do |b, i|
54+
masked << (b ^ masking_key[i % 4])
55+
end
56+
57+
frame.push *masked
58+
@sock.print frame.pack 'c*'
59+
end
60+
61+
def extract_data
62+
first_group = @sock.getbyte
63+
fin = first_group & 0b10000000 != 128
64+
raise 'Unsupported' if fin
65+
opcode = first_group & 0b00001111
66+
raise "Unsupported: #{opcode}" unless opcode == 1
67+
68+
second_group = @sock.getbyte
69+
mask = second_group & 0b10000000 == 128
70+
raise 'The server must not mask any frames' if mask
71+
payload_len = second_group & 0b01111111
72+
# TODO: Support other payload_lengths
73+
if payload_len == 126
74+
payload_len = @sock.read(2).unpack('n*')[0]
75+
end
76+
77+
data = JSON.parse @sock.read payload_len
78+
$stderr.puts '[>]' + data.inspect if SHOW_PROTOCOL
79+
data
80+
end
81+
end
82+
1283
class WebSocketServer
1384
def initialize s
1485
@sock = s

0 commit comments

Comments
 (0)