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.
- checksums.yaml +7 -0
- data/CONTRIBUTING.md +573 -0
- data/Gemfile +10 -0
- data/LICENSE.txt +22 -0
- data/README.md +996 -0
- data/Rakefile +57 -0
- data/TODO.md +23 -0
- data/debug.gemspec +33 -0
- data/exe/rdbg +53 -0
- data/ext/debug/debug.c +228 -0
- data/ext/debug/extconf.rb +27 -0
- data/ext/debug/iseq_collector.c +93 -0
- data/lib/debug/3.0/debug.bundle +0 -0
- data/lib/debug/3.1/debug.bundle +0 -0
- data/lib/debug/3.2/debug.bundle +0 -0
- data/lib/debug/3.3/debug.bundle +0 -0
- data/lib/debug/3.4/debug.bundle +0 -0
- data/lib/debug/abbrev_command.rb +77 -0
- data/lib/debug/breakpoint.rb +556 -0
- data/lib/debug/client.rb +263 -0
- data/lib/debug/color.rb +123 -0
- data/lib/debug/config.rb +592 -0
- data/lib/debug/console.rb +224 -0
- data/lib/debug/dap_custom/traceInspector.rb +336 -0
- data/lib/debug/frame_info.rb +191 -0
- data/lib/debug/irb_integration.rb +37 -0
- data/lib/debug/local.rb +115 -0
- data/lib/debug/open.rb +13 -0
- data/lib/debug/open_nonstop.rb +15 -0
- data/lib/debug/prelude.rb +50 -0
- data/lib/debug/server.rb +534 -0
- data/lib/debug/server_cdp.rb +1348 -0
- data/lib/debug/server_dap.rb +1108 -0
- data/lib/debug/session.rb +2667 -0
- data/lib/debug/source_repository.rb +150 -0
- data/lib/debug/start.rb +5 -0
- data/lib/debug/thread_client.rb +1457 -0
- data/lib/debug/tracer.rb +241 -0
- data/lib/debug/version.rb +5 -0
- data/lib/debug.rb +9 -0
- data/misc/README.md.erb +660 -0
- metadata +117 -0
data/lib/debug/server.rb
ADDED
@@ -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
|