ed-precompiled_debug 1.11.0-arm64-darwin

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
@@ -0,0 +1,534 @@
1
+ # frozen_string_literal: true
2
+
3
+ require 'socket'
4
+ require_relative 'config'
5
+ require_relative 'version'
6
+
7
+ module DEBUGGER__
8
+ class UI_ServerBase < UI_Base
9
+ def initialize
10
+ @sock = @sock_for_fork = nil
11
+ @accept_m = Mutex.new
12
+ @accept_cv = ConditionVariable.new
13
+ @client_addr = nil
14
+ @q_msg = nil
15
+ @q_ans = nil
16
+ @unsent_messages = []
17
+ @width = 80
18
+ @repl = true
19
+ @session = nil
20
+ end
21
+
22
+ class Terminate < StandardError; end
23
+ class GreetingError < StandardError; end
24
+ class RetryConnection < StandardError; end
25
+
26
+ def deactivate
27
+ @reader_thread.raise Terminate
28
+ @reader_thread.join
29
+ end
30
+
31
+ def accept
32
+ if @sock_for_fork
33
+ begin
34
+ yield @sock_for_fork, already_connected: true
35
+ ensure
36
+ @sock_for_fork.close
37
+ @sock_for_fork = nil
38
+ end
39
+ end
40
+ end
41
+
42
+ def activate session, on_fork: false
43
+ @session = session
44
+ @reader_thread = Thread.new do
45
+ # An error on this thread should break the system.
46
+ Thread.current.abort_on_exception = true
47
+ Thread.current.name = 'DEBUGGER__::Server::reader'
48
+
49
+ accept do |server, already_connected: false|
50
+ DEBUGGER__.warn "Connected."
51
+ greeting_done = false
52
+ @need_pause_at_first = true
53
+
54
+ @accept_m.synchronize{
55
+ @sock = server
56
+ greeting
57
+ greeting_done = true
58
+
59
+ @accept_cv.signal
60
+
61
+ # flush unsent messages
62
+ @unsent_messages.each{|m|
63
+ @sock.puts m
64
+ } if @repl
65
+ @unsent_messages.clear
66
+
67
+ @q_msg = Queue.new
68
+ @q_ans = Queue.new
69
+ } unless already_connected
70
+
71
+ setup_interrupt do
72
+ pause if !already_connected && @need_pause_at_first
73
+ process
74
+ end
75
+
76
+ rescue GreetingError => e
77
+ DEBUGGER__.warn "GreetingError: #{e.message}"
78
+ next
79
+ rescue Terminate
80
+ raise # should catch at outer scope
81
+ rescue RetryConnection
82
+ next
83
+ rescue => e
84
+ DEBUGGER__.warn "ReaderThreadError: #{e}"
85
+ pp e.backtrace
86
+ ensure
87
+ DEBUGGER__.warn "Disconnected."
88
+ cleanup_reader if greeting_done
89
+ end # accept
90
+
91
+ rescue Terminate
92
+ # ignore
93
+ end
94
+ end
95
+
96
+ def cleanup_reader
97
+ @sock.close if @sock
98
+ @sock = nil
99
+ @q_msg.close
100
+ @q_msg = nil
101
+ @q_ans.close
102
+ @q_ans = nil
103
+ end
104
+
105
+ def check_cookie c
106
+ cookie = CONFIG[:cookie]
107
+ if cookie && cookie != c
108
+ raise GreetingError, "Cookie mismatch (#{$2.inspect} was sent)"
109
+ end
110
+ end
111
+
112
+ def parse_option params
113
+ case params.strip
114
+ when /width:\s+(\d+)/
115
+ @width = $1.to_i
116
+ parse_option $~.post_match
117
+ when /cookie:\s+(\S+)/
118
+ check_cookie $1 if $1 != '-'
119
+ parse_option $~.post_match
120
+ when /nonstop: (true|false)/
121
+ @need_pause_at_first = false if $1 == 'true'
122
+ parse_option $~.post_match
123
+ when /(.+):(.+)/
124
+ raise GreetingError, "Unkown option: #{params}"
125
+ else
126
+ # OK
127
+ end
128
+ end
129
+
130
+ def greeting
131
+ case g = @sock.gets
132
+ when /^info cookie:\s+(.*)$/
133
+ require 'etc'
134
+
135
+ check_cookie $1
136
+ @sock.puts "PID: #{Process.pid}, $0: #{$0}, session_name: #{CONFIG[:session_name]}"
137
+ @sock.puts "debug #{VERSION} on #{RUBY_DESCRIPTION}"
138
+ @sock.puts "uname: #{Etc.uname.inspect}"
139
+ @sock.close
140
+ raise GreetingError, "HEAD request"
141
+
142
+ when /^version:\s+(\S+)\s+(.+)$/
143
+ v, params = $1, $2
144
+
145
+ # TODO: protocol version
146
+ if v != VERSION
147
+ @sock.puts msg = "out DEBUGGER: Incompatible version (server:#{VERSION} and client:#{$1})"
148
+ raise GreetingError, msg
149
+ end
150
+ parse_option(params)
151
+
152
+ session_name = CONFIG[:session_name]
153
+ session_name_str = ", session_name:#{session_name}" if session_name
154
+ puts "DEBUGGER (client): Connected. PID:#{Process.pid}, $0:#{$0}#{session_name_str}"
155
+ puts "DEBUGGER (client): Type `Ctrl-C` to enter the debug console." unless @need_pause_at_first
156
+ puts
157
+
158
+ when /^Content-Length: (\d+)/
159
+ require_relative 'server_dap'
160
+
161
+ raise unless @sock.read(2) == "\r\n"
162
+ self.extend(UI_DAP)
163
+ @repl = false
164
+ @need_pause_at_first = false
165
+ dap_setup @sock.read($1.to_i)
166
+
167
+ when /^GET\s\/json\sHTTP\/1.1/, /^GET\s\/json\/version\sHTTP\/1.1/, /^GET\s\/\w{8}-\w{4}-\w{4}-\w{4}-\w{12}\sHTTP\/1.1/
168
+ # The reason for not using @uuid here is @uuid is nil if users run debugger without `--open=chrome`.
169
+
170
+ require_relative 'server_cdp'
171
+
172
+ self.extend(UI_CDP)
173
+ send_chrome_response g
174
+ else
175
+ raise GreetingError, "Unknown greeting message: #{g}"
176
+ end
177
+ end
178
+
179
+ def process
180
+ while true
181
+ DEBUGGER__.debug{ "sleep IO.select" }
182
+ _r = IO.select([@sock])
183
+ DEBUGGER__.debug{ "wakeup IO.select" }
184
+
185
+ line = @session.process_group.sync do
186
+ unless IO.select([@sock], nil, nil, 0)
187
+ DEBUGGER__.debug{ "UI_Server can not read" }
188
+ break :can_not_read
189
+ end
190
+ @sock.gets&.chomp.tap{|line|
191
+ DEBUGGER__.debug{ "UI_Server received: #{line}" }
192
+ }
193
+ end
194
+
195
+ return unless line
196
+ next if line == :can_not_read
197
+
198
+ case line
199
+ when /\Apause/
200
+ pause
201
+ when /\Acommand (\d+) (\d+) ?(.+)/
202
+ raise "not in subsession, but received: #{line.inspect}" unless @session.in_subsession?
203
+
204
+ if $1.to_i == Process.pid
205
+ @width = $2.to_i
206
+ @q_msg << $3
207
+ else
208
+ raise "pid:#{Process.pid} but get #{line}"
209
+ end
210
+ when /\Aanswer (\d+) (.*)/
211
+ raise "not in subsession, but received: #{line.inspect}" unless @session.in_subsession?
212
+
213
+ if $1.to_i == Process.pid
214
+ @q_ans << $2
215
+ else
216
+ raise "pid:#{Process.pid} but get #{line}"
217
+ end
218
+ else
219
+ STDERR.puts "unsupported: #{line.inspect}"
220
+ exit!
221
+ end
222
+ end
223
+ end
224
+
225
+ def remote?
226
+ true
227
+ end
228
+
229
+ def width
230
+ @width
231
+ end
232
+
233
+ def sigurg_overridden? prev_handler
234
+ case prev_handler
235
+ when "SYSTEM_DEFAULT", "DEFAULT"
236
+ false
237
+ when Proc
238
+ if prev_handler.source_location[0] == __FILE__
239
+ false
240
+ else
241
+ true
242
+ end
243
+ else
244
+ true
245
+ end
246
+ end
247
+
248
+ begin
249
+ prev = trap(:SIGURG, nil)
250
+ trap(:SIGURG, prev)
251
+ TRAP_SIGNAL = :SIGURG
252
+ rescue ArgumentError
253
+ # maybe Windows?
254
+ TRAP_SIGNAL = :SIGINT
255
+ end
256
+
257
+ def setup_interrupt
258
+ prev_handler = trap(TRAP_SIGNAL) do
259
+ # $stderr.puts "trapped SIGINT"
260
+ ThreadClient.current.on_trap TRAP_SIGNAL
261
+
262
+ case prev_handler
263
+ when Proc
264
+ prev_handler.call
265
+ else
266
+ # ignore
267
+ end
268
+ end
269
+
270
+ if sigurg_overridden?(prev_handler)
271
+ DEBUGGER__.warn "SIGURG handler is overridden by the debugger."
272
+ end
273
+ yield
274
+ ensure
275
+ trap(TRAP_SIGNAL, prev_handler)
276
+ end
277
+
278
+ attr_reader :reader_thread
279
+
280
+ class NoRemoteError < Exception; end
281
+
282
+ def sock skip: false
283
+ if s = @sock # already connection
284
+ # ok
285
+ elsif skip == true # skip process
286
+ no_sock = true
287
+ r = @accept_m.synchronize do
288
+ if @sock
289
+ no_sock = false
290
+ else
291
+ yield nil
292
+ end
293
+ end
294
+ return r if no_sock
295
+ else # wait for connection
296
+ until s = @sock
297
+ @accept_m.synchronize{
298
+ unless @sock
299
+ DEBUGGER__.warn "wait for debugger connection..."
300
+ @accept_cv.wait(@accept_m)
301
+ end
302
+ }
303
+ end
304
+ end
305
+
306
+ yield s
307
+ rescue Errno::EPIPE
308
+ # ignore
309
+ end
310
+
311
+ def ask prompt
312
+ sock do |s|
313
+ s.puts "ask #{Process.pid} #{prompt}"
314
+ @q_ans.pop
315
+ end
316
+ end
317
+
318
+ def puts str = nil
319
+ case str
320
+ when Array
321
+ enum = str.each
322
+ when String
323
+ enum = str.each_line
324
+ when nil
325
+ enum = [''].each
326
+ end
327
+
328
+ sock skip: true do |s|
329
+ enum.each do |line|
330
+ msg = "out #{line.chomp}"
331
+ if s
332
+ s.puts msg
333
+ else
334
+ @unsent_messages << msg
335
+ end
336
+ end
337
+ end
338
+ end
339
+
340
+ def readline prompt
341
+ input = (sock(skip: CONFIG[:skip_bp]) do |s|
342
+ next unless s
343
+
344
+ if @repl
345
+ raise "not in subsession, but received: #{line.inspect}" unless @session.in_subsession?
346
+ line = "input #{Process.pid}"
347
+ DEBUGGER__.debug{ "send: #{line}" }
348
+ s.puts line
349
+ end
350
+ sleep 0.01 until @q_msg
351
+ @q_msg.pop.tap{|msg|
352
+ DEBUGGER__.debug{ "readline: #{msg.inspect}" }
353
+ }
354
+ end || 'continue')
355
+
356
+ if input.is_a?(String)
357
+ input.strip
358
+ else
359
+ input
360
+ end
361
+ end
362
+
363
+ def pause
364
+ # $stderr.puts "DEBUG: pause request"
365
+ Process.kill(TRAP_SIGNAL, Process.pid)
366
+ end
367
+
368
+ def quit n, &_b
369
+ # ignore n
370
+ sock do |s|
371
+ s.puts "quit"
372
+ end
373
+ end
374
+
375
+ def after_fork_parent
376
+ # do nothing
377
+ end
378
+
379
+ def vscode_setup debug_port
380
+ require_relative 'server_dap'
381
+ UI_DAP.setup debug_port
382
+ end
383
+ end
384
+
385
+ class UI_TcpServer < UI_ServerBase
386
+ def initialize host: nil, port: nil
387
+ @local_addr = nil
388
+ @host = host || CONFIG[:host]
389
+ @port_save_file = nil
390
+ @port = begin
391
+ port_str = (port && port.to_s) || CONFIG[:port] || raise("Specify listening port by RUBY_DEBUG_PORT environment variable.")
392
+ case port_str
393
+ when /\A\d+\z/
394
+ port_str.to_i
395
+ when /\A(\d+):(.+)\z/
396
+ @port_save_file = $2
397
+ $1.to_i
398
+ else
399
+ raise "Specify digits for port number"
400
+ end
401
+ end
402
+ @port_range = if @port.zero?
403
+ 0
404
+ else
405
+ port_range_str = (CONFIG[:port_range] || "0").to_s
406
+ raise "Specify a positive integer <=16 for port range" unless port_range_str.match?(/\A\d+\z/) && port_range_str.to_i <= 16
407
+ port_range_str.to_i
408
+ end
409
+ @uuid = nil # for CDP
410
+
411
+ super()
412
+ end
413
+
414
+ def chrome_setup
415
+ require_relative 'server_cdp'
416
+
417
+ @uuid = SecureRandom.uuid
418
+ @chrome_pid = UI_CDP.setup_chrome(@local_addr.inspect_sockaddr, @uuid)
419
+ DEBUGGER__.warn <<~EOS
420
+ With Chrome browser, type the following URL in the address-bar:
421
+
422
+ devtools://devtools/bundled/inspector.html?v8only=true&panel=sources&noJavaScriptCompletion=true&ws=#{@local_addr.inspect_sockaddr}/#{@uuid}
423
+
424
+ EOS
425
+ end
426
+
427
+ def accept
428
+ retry_cnt = 0
429
+ super # for fork
430
+
431
+ begin
432
+ Socket.tcp_server_sockets @host, @port do |socks|
433
+ @local_addr = socks.first.local_address # Change this part if `socks` are multiple.
434
+ rdbg = File.expand_path('../../exe/rdbg', __dir__)
435
+ DEBUGGER__.warn "Debugger can attach via TCP/IP (#{@local_addr.inspect_sockaddr})"
436
+
437
+ if @port_save_file
438
+ File.write(@port_save_file, "#{socks[0].local_address.ip_port.to_s}\n")
439
+ DEBUGGER__.warn "Port is saved into #{@port_save_file}"
440
+ end
441
+
442
+ DEBUGGER__.info <<~EOS
443
+ With rdbg, use the following command line:
444
+ #
445
+ # #{rdbg} --attach #{@local_addr.ip_address} #{@local_addr.ip_port}
446
+ #
447
+ EOS
448
+
449
+ case CONFIG[:open]
450
+ when 'chrome'
451
+ chrome_setup
452
+ when 'vscode'
453
+ vscode_setup @local_addr.inspect_sockaddr
454
+ end
455
+
456
+ Socket.accept_loop(socks) do |sock, client|
457
+ @client_addr = client
458
+ yield @sock_for_fork = sock
459
+ end
460
+ end
461
+ rescue Errno::EADDRINUSE
462
+ number_of_retries = @port_range.zero? ? 10 : @port_range
463
+ if retry_cnt < number_of_retries
464
+ @port += 1 unless @port_range.zero?
465
+ retry_cnt += 1
466
+ sleep 0.1
467
+ retry
468
+ else
469
+ raise
470
+ end
471
+ rescue Terminate
472
+ # OK
473
+ rescue => e
474
+ $stderr.puts e.inspect, e.message
475
+ pp e.backtrace
476
+ exit
477
+ end
478
+ ensure
479
+ @sock_for_fork = nil
480
+
481
+ if @port_save_file && File.exist?(@port_save_file)
482
+ File.unlink(@port_save_file)
483
+ end
484
+ end
485
+ end
486
+
487
+ class UI_UnixDomainServer < UI_ServerBase
488
+ def initialize sock_dir: nil, sock_path: nil
489
+ @sock_path = sock_path
490
+ @sock_dir = sock_dir || DEBUGGER__.unix_domain_socket_dir
491
+ @sock_for_fork = nil
492
+
493
+ super()
494
+ end
495
+
496
+ def accept
497
+ super # for fork
498
+
499
+ case
500
+ when @sock_path
501
+ when sp = CONFIG[:sock_path]
502
+ @sock_path = sp
503
+ else
504
+ @sock_path = DEBUGGER__.create_unix_domain_socket_name(@sock_dir)
505
+ end
506
+
507
+ ::DEBUGGER__.warn "Debugger can attach via UNIX domain socket (#{@sock_path})"
508
+ vscode_setup @sock_path if CONFIG[:open] == 'vscode'
509
+
510
+ begin
511
+ Socket.unix_server_loop @sock_path do |sock, client|
512
+ @sock_for_fork = sock
513
+ @client_addr = client
514
+
515
+ yield sock
516
+ ensure
517
+ sock.close
518
+ @sock_for_fork = nil
519
+ end
520
+ rescue Errno::ECONNREFUSED => _e
521
+ ::DEBUGGER__.warn "#{_e.message} (socket path: #{@sock_path})"
522
+
523
+ if @sock_path.start_with? Config.unix_domain_socket_tmpdir
524
+ # try on homedir
525
+ @sock_path = Config.create_unix_domain_socket_name(unix_domain_socket_homedir)
526
+ ::DEBUGGER__.warn "retry with #{@sock_path}"
527
+ retry
528
+ else
529
+ raise
530
+ end
531
+ end
532
+ end
533
+ end
534
+ end