debug 1.0.0.beta8 → 1.0.0.rc1

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.
data/debug.gemspec CHANGED
@@ -26,4 +26,5 @@ Gem::Specification.new do |spec|
26
26
  spec.extensions = ['ext/debug/extconf.rb']
27
27
 
28
28
  spec.add_dependency "irb" # for its color_printer class, which was added after 1.3
29
+ spec.add_dependency "reline", ">= 0.2.7"
29
30
  end
data/exe/rdbg CHANGED
@@ -1,7 +1,7 @@
1
1
  #!/usr/bin/env ruby
2
2
 
3
3
  require_relative '../lib/debug/config'
4
- config = DEBUGGER__.parse_argv(ARGV)
4
+ config = DEBUGGER__::Config::parse_argv(ARGV)
5
5
 
6
6
  case config[:mode]
7
7
  when :start
@@ -11,17 +11,14 @@ when :start
11
11
  start_mode = config[:remote] ? "open" : 'start'
12
12
  cmd = config[:command] ? ARGV.shift : RbConfig.ruby
13
13
 
14
- env = ::DEBUGGER__.config_to_env_hash(config)
14
+ env = ::DEBUGGER__::Config.config_to_env_hash(config)
15
15
  env['RUBYOPT'] = "-r #{libpath}/#{start_mode}"
16
16
 
17
17
  exec(env, cmd, *ARGV)
18
18
 
19
19
  when :attach
20
20
  require_relative "../lib/debug/client"
21
-
22
- config.each{|k, v|
23
- DEBUGGER__::CONFIG[k] = v
24
- }
21
+ ::DEBUGGER__::CONFIG.update config
25
22
 
26
23
  begin
27
24
  if ARGV.empty? && config[:port]
data/ext/debug/debug.c CHANGED
@@ -22,7 +22,17 @@ static VALUE rb_cFrameInfo;
22
22
  static VALUE
23
23
  di_entry(VALUE loc, VALUE self, VALUE binding, VALUE iseq, VALUE klass, VALUE depth)
24
24
  {
25
- return rb_struct_new(rb_cFrameInfo, loc, self, binding, iseq, klass, depth, Qnil, Qnil, Qnil, Qnil, Qnil);
25
+ return rb_struct_new(rb_cFrameInfo,
26
+ // :location, :self, :binding, :iseq, :class, :frame_depth,
27
+ loc, self, binding, iseq, klass, depth,
28
+ // :has_return_value, :return_value,
29
+ Qnil, Qnil,
30
+ // :has_raised_exception, :raised_exception,
31
+ Qnil, Qnil,
32
+ // :show_line, :local_variables
33
+ Qnil,
34
+ // :_local_variables, :_callee # for recorder
35
+ Qnil, Qnil);
26
36
  }
27
37
 
28
38
  static int
@@ -82,6 +82,11 @@ module DEBUGGER__
82
82
  end
83
83
  end
84
84
 
85
+ if RUBY_VERSION.to_f <= 2.7
86
+ # workaround for https://bugs.ruby-lang.org/issues/17302
87
+ TracePoint.new(:line){}.enable{}
88
+ end
89
+
85
90
  class LineBreakpoint < Breakpoint
86
91
  attr_reader :path, :line, :iseq
87
92
 
@@ -113,7 +118,6 @@ module DEBUGGER__
113
118
  next unless safe_eval tp.binding, @cond
114
119
  end
115
120
  delete if @oneshot
116
-
117
121
  suspend
118
122
  end
119
123
  end
@@ -231,19 +235,25 @@ module DEBUGGER__
231
235
 
232
236
  class CatchBreakpoint < Breakpoint
233
237
  attr_reader :last_exc
238
+ include SkipPathHelper
234
239
 
235
- def initialize pat
240
+ def initialize pat, cond: nil, command: nil
236
241
  @pat = pat.freeze
237
242
  @key = [:catch, @pat].freeze
238
243
  @last_exc = nil
239
244
 
245
+ @cond = cond
246
+ @command = command
247
+
240
248
  super()
241
249
  end
242
250
 
243
251
  def setup
244
252
  @tp = TracePoint.new(:raise){|tp|
253
+ next if skip_path?(tp.path)
245
254
  exc = tp.raised_exception
246
255
  next if SystemExit === exc
256
+ next if !safe_eval(tp.binding, @cond) if @cond
247
257
  should_suspend = false
248
258
 
249
259
  exc.class.ancestors.each{|cls|
@@ -278,8 +288,6 @@ module DEBUGGER__
278
288
  @tp = TracePoint.new(:line){|tp|
279
289
  next if tp.path.start_with? __dir__
280
290
  next if tp.path.start_with? '<internal:'
281
- # Skip when `JSON.generate` is called during tests
282
- next if tp.defined_class.to_s == '#<Class:JSON>' and ENV['RUBY_DEBUG_TEST_MODE']
283
291
 
284
292
  if safe_eval tp.binding, @expr
285
293
  suspend
@@ -340,15 +348,17 @@ module DEBUGGER__
340
348
  class MethodBreakpoint < Breakpoint
341
349
  attr_reader :sig_method_name, :method
342
350
 
343
- def initialize b, klass_name, op, method_name, cond, command: nil
351
+ def initialize b, klass_name, op, method_name, cond: nil, command: nil
344
352
  @sig_klass_name = klass_name
345
353
  @sig_op = op
346
354
  @sig_method_name = method_name
347
355
  @klass_eval_binding = b
356
+ @override_method = false
348
357
 
349
358
  @klass = nil
350
359
  @method = nil
351
360
  @cond = cond
361
+ @cond_class = nil
352
362
  @command = command
353
363
  @key = "#{klass_name}#{op}#{method_name}".freeze
354
364
 
@@ -356,16 +366,13 @@ module DEBUGGER__
356
366
  end
357
367
 
358
368
  def setup
359
- if @cond
360
- @tp = TracePoint.new(:call){|tp|
361
- next unless safe_eval tp.binding, @cond
362
- suspend
363
- }
364
- else
365
- @tp = TracePoint.new(:call){|tp|
366
- suspend
367
- }
368
- end
369
+ @tp = TracePoint.new(:call){|tp|
370
+ next if !safe_eval(tp.binding, @cond) if @cond
371
+ next if @cond_class && !tp.self.kind_of?(@cond_class)
372
+ next if @override_method ? (caller_locations(2, 1).first.to_s.start_with?(__dir__)) : tp.path.start_with?(__dir__)
373
+
374
+ suspend
375
+ }
369
376
  end
370
377
 
371
378
  def eval_class_name
@@ -390,31 +397,57 @@ module DEBUGGER__
390
397
  try_enable
391
398
  end
392
399
 
400
+ if RUBY_VERSION.to_f <= 2.6
401
+ def override klass
402
+ sig_method_name = @sig_method_name
403
+ klass.prepend Module.new{
404
+ define_method(sig_method_name) do |*args, &block|
405
+ super(*args, &block)
406
+ end
407
+ }
408
+ end
409
+ else
410
+ def override klass
411
+ sig_method_name = @sig_method_name
412
+ klass.prepend Module.new{
413
+ define_method(sig_method_name) do |*args, **kw, &block|
414
+ super(*args, **kw, &block)
415
+ end
416
+ }
417
+ end
418
+ end
419
+
393
420
  def try_enable added: false
394
421
  eval_class_name
395
422
  search_method
396
423
 
397
424
  begin
398
425
  retried = false
426
+
399
427
  @tp.enable(target: @method)
400
428
  DEBUGGER__.warn "#{self} is activated." if added
401
429
 
430
+ if @sig_op == '#'
431
+ @cond_class = @klass if @method.owner != @klass
432
+ else # '.'
433
+ @cond_class = @klass.singleton_class if @method.owner != @klass.singleton_class
434
+ end
435
+
402
436
  rescue ArgumentError
403
437
  raise if retried
404
438
  retried = true
405
- sig_method_name = @sig_method_name
406
439
 
407
440
  # maybe C method
408
- @klass.module_eval do
409
- orig_name = sig_method_name + '__orig__'
410
- alias_method orig_name, sig_method_name
411
- define_method(sig_method_name) do |*args|
412
- send(orig_name, *args)
413
- end
441
+ case @sig_op
442
+ when '.'
443
+ override @klass.singleton_class
444
+ when '#'
445
+ override @klass
414
446
  end
415
447
 
416
448
  # re-collect the method object after the above patch
417
449
  search_method
450
+ @override_method = true if @method
418
451
  retry
419
452
  end
420
453
  rescue Exception
data/lib/debug/client.rb CHANGED
@@ -5,6 +5,7 @@ require 'io/console/size'
5
5
 
6
6
  require_relative 'config'
7
7
  require_relative 'version'
8
+ require_relative 'console'
8
9
 
9
10
  # $VERBOSE = true
10
11
 
@@ -12,21 +13,11 @@ module DEBUGGER__
12
13
  class CommandLineOptionError < Exception; end
13
14
 
14
15
  class Client
15
- begin
16
- require 'readline'
17
- def readline
18
- Readline.readline("\n(rdbg:remote) ", true)
19
- end
20
- rescue LoadError
21
- def readline
22
- print "\n(rdbg:remote) "
23
- gets
24
- end
25
- end
26
-
27
16
  def initialize argv
28
17
  return util(argv) if String === argv
29
18
 
19
+ @console = Console.new
20
+
30
21
  case argv.size
31
22
  when 0
32
23
  connect_unix
@@ -53,6 +44,10 @@ module DEBUGGER__
53
44
  send "version: #{VERSION} width: #{@width} cookie: #{CONFIG[:cookie]}"
54
45
  end
55
46
 
47
+ def readline
48
+ @console.readline "(rdbg:remote) "
49
+ end
50
+
56
51
  def util name
57
52
  case name
58
53
  when 'gen-sockpath'
data/lib/debug/color.rb CHANGED
@@ -2,6 +2,13 @@
2
2
 
3
3
  begin
4
4
  require 'irb/color'
5
+
6
+ module IRB
7
+ module Color
8
+ DIM = 2 unless defined? DIM
9
+ end
10
+ end
11
+
5
12
  require "irb/color_printer"
6
13
  rescue LoadError
7
14
  warn "DEBUGGER: can not load newer irb for coloring. Write 'gem \"debug\" in your Gemfile."
@@ -24,7 +31,7 @@ module DEBUGGER__
24
31
  end
25
32
 
26
33
  if defined? IRB::ColorPrinter.pp
27
- def color_pp obj, width = SESSION.width
34
+ def color_pp obj, width
28
35
  if !CONFIG[:no_color]
29
36
  IRB::ColorPrinter.pp(obj, "".dup, width)
30
37
  else
@@ -32,14 +39,14 @@ module DEBUGGER__
32
39
  end
33
40
  end
34
41
  else
35
- def color_pp obj
42
+ def color_pp obj, width
36
43
  obj.pretty_inspect
37
44
  end
38
45
  end
39
46
 
40
- def colored_inspect obj, no_color: false
47
+ def colored_inspect obj, width: SESSION.width, no_color: false
41
48
  if !no_color
42
- color_pp obj
49
+ color_pp obj, width
43
50
  else
44
51
  obj.pretty_inspect
45
52
  end
@@ -72,5 +79,13 @@ module DEBUGGER__
72
79
  def colorize_blue(str)
73
80
  colorize(str, [:BLUE, :BOLD])
74
81
  end
82
+
83
+ def colorize_magenta(str)
84
+ colorize(str, [:MAGENTA, :BOLD])
85
+ end
86
+
87
+ def colorize_dim(str)
88
+ colorize(str, [:DIM])
89
+ end
75
90
  end
76
91
  end
data/lib/debug/config.rb CHANGED
@@ -1,46 +1,23 @@
1
1
  # frozen_string_literal: true
2
2
 
3
3
  module DEBUGGER__
4
- def self.unix_domain_socket_dir
5
- case
6
- when path = ::DEBUGGER__::CONFIG[:sock_dir]
7
- when path = ENV['XDG_RUNTIME_DIR']
8
- when home = ENV['HOME']
9
- path = File.join(home, '.ruby-debug-sock')
10
-
11
- case
12
- when !File.exist?(path)
13
- Dir.mkdir(path, 0700)
14
- when !File.directory?(path)
15
- raise "#{path} is not a directory."
16
- end
17
- else
18
- raise 'specify RUBY_DEBUG_SOCK_DIR environment variable for UNIX domain socket directory.'
19
- end
20
-
21
- path
22
- end
23
-
24
- def self.create_unix_domain_socket_name_prefix(base_dir = unix_domain_socket_dir)
25
- user = ENV['USER'] || 'ruby-debug'
26
- File.join(base_dir, "ruby-debug-#{user}")
27
- end
28
-
29
- def self.create_unix_domain_socket_name(base_dir = unix_domain_socket_dir)
30
- create_unix_domain_socket_name_prefix(base_dir) + "-#{Process.pid}"
31
- end
32
-
33
4
  CONFIG_SET = {
34
5
  # UI setting
35
6
  log_level: ['RUBY_DEBUG_LOG_LEVEL', "UI: Log level same as Logger (default: WARN)", :loglevel],
36
7
  show_src_lines: ['RUBY_DEBUG_SHOW_SRC_LINES', "UI: Show n lines source code on breakpoint (default: 10 lines)", :int],
37
8
  show_frames: ['RUBY_DEBUG_SHOW_FRAMES', "UI: Show n frames on breakpoint (default: 2 frames)", :int],
38
- show_info_lines:['RUBY_DEBUG_SHOW_INFO_LINES',"UI: Show n lines on info command (default: 10 lines, 0 for unlimited)", :int],
39
9
  use_short_path: ['RUBY_DEBUG_USE_SHORT_PATH', "UI: Show shoten PATH (like $(Gem)/foo.rb)", :bool],
40
- skip_nosrc: ['RUBY_DEBUG_SKIP_NOSRC', "UI: Skip on no source code lines (default: false)", :bool],
41
- skip_path: ['RUBY_DEBUG_SKIP_PATH', "UI: Skip showing frames for given paths (default: [])", :path],
42
10
  no_color: ['RUBY_DEBUG_NO_COLOR', "UI: Do not use colorize (default: false)", :bool],
43
11
  no_sigint_hook: ['RUBY_DEBUG_NO_SIGINT_HOOK', "UI: Do not suspend on SIGINT (default: false)", :bool],
12
+ no_reline: ['RUBY_DEBUG_NO_RELINE', "UI: Do not use Reline library (default: false)", :bool],
13
+
14
+ # control setting
15
+ skip_path: ['RUBY_DEBUG_SKIP_PATH', "CONTROL: Skip showing/entering frames for given paths (default: [])", :path],
16
+ skip_nosrc: ['RUBY_DEBUG_SKIP_NOSRC', "CONTROL: Skip on no source code lines (default: false)", :bool],
17
+ keep_alloc_site:['RUBY_DEBUG_KEEP_ALLOC_SITE',"CONTROL: Keep allocation site and p, pp shows it (default: false)", :bool],
18
+ postmortem: ['RUBY_DEBUG_POSTMORTEM', "CONTROL: Enable postmortem debug (default: false)", :bool],
19
+ parent_on_fork: ['RUBY_DEBUG_PARENT_ON_FORK', "CONTROL: Keep debugging parent process on fork (default: false)", :bool],
20
+ sigdump_sig: ['RUBY_DEBUG_SIGDUMP_SIG', "CONTROL: Sigdump signal (default: disabled)"],
44
21
 
45
22
  # boot setting
46
23
  nonstop: ['RUBY_DEBUG_NONSTOP', "BOOT: Nonstop mode", :bool],
@@ -58,188 +35,388 @@ module DEBUGGER__
58
35
 
59
36
  CONFIG_MAP = CONFIG_SET.map{|k, (ev, desc)| [k, ev]}.to_h.freeze
60
37
 
61
- def self.config_to_env_hash config
62
- CONFIG_MAP.each_with_object({}){|(key, evname), env|
63
- env[evname] = config[key].to_s if config[key]
64
- }
65
- end
66
-
67
- def self.parse_config_value name, valstr
68
- return valstr unless valstr.kind_of? String
38
+ class Config
39
+ def self.config
40
+ @config
41
+ end
69
42
 
70
- case CONFIG_SET[name][2]
71
- when :bool
72
- case valstr
73
- when '1', 'true', 'TRUE', 'T'
74
- true
75
- else
76
- false
43
+ def initialize argv
44
+ if self.class.instance_variable_defined? :@config
45
+ raise 'Can not make multiple configurations in one process'
77
46
  end
78
- when :int
79
- valstr.to_i
80
- when :loglevel
81
- if DEBUGGER__::LOG_LEVELS[s = valstr.to_sym]
82
- s
83
- else
84
- raise "Unknown loglevel: #{valstr}"
85
- end
86
- when :path # array of String
87
- valstr.split(/:/).map{|e|
88
- if /\A\/(.+)\/\z/ =~ e
89
- Regexp.compile $1
47
+
48
+ update self.class.parse_argv(argv)
49
+ end
50
+
51
+ def [](key)
52
+ config[key]
53
+ end
54
+
55
+ def []=(key, val)
56
+ set_config(key => val)
57
+ end
58
+
59
+ def set_config(**kw)
60
+ conf = config.dup
61
+ kw.each{|k, v|
62
+ if CONFIG_MAP[k]
63
+ conf[k] = parse_config_value(k, v) # TODO: ractor support
90
64
  else
91
- e
65
+ raise "Unknown configuration: #{k}"
92
66
  end
93
67
  }
94
- else
95
- valstr
68
+
69
+ update conf
96
70
  end
97
- end
98
71
 
99
- def self.parse_argv argv
100
- config = {
101
- mode: :start,
102
- }
103
- CONFIG_MAP.each{|key, evname|
104
- if val = ENV[evname]
105
- config[key] = parse_config_value(key, val)
72
+ def append_config key, val
73
+ conf = self.config.dup
74
+
75
+ if CONFIG_SET[key]
76
+ if CONFIG_SET[key][2] == :path
77
+ conf[key] = [*conf[key], *parse_config_value(key, val)];
78
+ else
79
+ raise "not an Array type: #{key}"
80
+ end
81
+ else
82
+ raise "Unknown configuration: #{key}"
106
83
  end
107
- }
108
- return config if !argv || argv.empty?
109
84
 
110
- require 'optparse'
111
- require_relative 'version'
85
+ update conf
86
+ end
112
87
 
113
- opt = OptionParser.new do |o|
114
- o.banner = "#{$0} [options] -- [debuggee options]"
115
- o.separator ''
116
- o.version = ::DEBUGGER__::VERSION
88
+ def update conf
89
+ old_conf = self.class.instance_variable_get(:@config) || {}
117
90
 
118
- o.separator 'Debug console mode:'
119
- o.on('-n', '--nonstop', 'Do not stop at the beginning of the script.') do
120
- config[:nonstop] = '1'
121
- end
91
+ # TODO: Use Ractor.make_shareable(conf)
92
+ self.class.instance_variable_set(:@config, conf.freeze)
122
93
 
123
- o.on('-e DEBUG_COMMAND', 'Execute debug command at the beginning of the script.') do |cmd|
124
- config[:commands] ||= ''
125
- config[:commands] += cmd + ';;'
94
+ # Post process
95
+ if_updated old_conf, conf, :keep_alloc_site do |_, new|
96
+ if new
97
+ require 'objspace'
98
+ ObjectSpace.trace_object_allocations_start
99
+ else
100
+ ObjectSpace.trace_object_allocations_stop
101
+ end
126
102
  end
127
103
 
128
- o.on('-x FILE', '--init-script=FILE', 'Execute debug command in the FILE.') do |file|
129
- config[:init_script] = file
130
- end
131
- o.on('--no-rc', 'Ignore ~/.rdbgrc') do
132
- config[:no_rc] = true
133
- end
134
- o.on('--no-color', 'Disable colorize') do
135
- config[:no_color] = true
136
- end
137
- o.on('--no-sigint-hook', 'Disable to trap SIGINT') do
138
- config[:no_sigint_hook] = true
104
+ if_updated old_conf, conf, :postmortem do |_, new_p|
105
+ SESSION.postmortem = new_p
139
106
  end
140
107
 
141
- o.on('-c', '--command', 'Enable command mode.',
142
- 'The first argument should be a command name in $PATH.',
143
- 'Example: \'rdbg -c bundle exec rake test\'') do
144
- config[:command] = true
108
+ if_updated old_conf, conf, :sigdump_sig do |old_sig, new_sig|
109
+ setup_sigdump old_sig, new_sig
145
110
  end
111
+ end
146
112
 
147
- o.separator ''
113
+ private def if_updated old_conf, new_conf, key
114
+ old, new = old_conf[key], new_conf[key]
115
+ yield old, new if old != new
116
+ end
148
117
 
149
- o.on('-O', '--open', 'Start remote debugging with opening the network port.',
150
- 'If TCP/IP options are not given,',
151
- 'a UNIX domain socket will be used.') do
152
- config[:remote] = true
153
- end
154
- o.on('--sock-path=SOCK_PATH', 'UNIX Doman socket path') do |path|
155
- config[:sock_path] = path
156
- end
157
- o.on('--port=PORT', 'Listening TCP/IP port') do |port|
158
- config[:port] = port
159
- end
160
- o.on('--host=HOST', 'Listening TCP/IP host') do |host|
161
- config[:host] = host
118
+ private def enable_sigdump sig
119
+ @sigdump_sig_prev = trap(sig) do
120
+ str = []
121
+ str << "Simple sigdump on #{Process.pid}"
122
+ Thread.list.each{|th|
123
+ str << "Thread: #{th}"
124
+ th.backtrace.each{|loc|
125
+ str << " #{loc}"
126
+ }
127
+ str << ''
128
+ }
129
+
130
+ STDERR.puts str
162
131
  end
163
- o.on('--cookie=COOKIE', 'Set a cookie for connection') do |c|
164
- config[:cookie] = c
132
+ end
133
+
134
+ private def disable_sigdump old_sig
135
+ trap(old_sig, @sigdump_sig_prev)
136
+ @sigdump_sig_prev = nil
137
+ end
138
+
139
+ # emergency simple sigdump.
140
+ # Use `sigdump` gem for more rich features.
141
+ private def setup_sigdump old_sig = nil, sig = CONFIG[:sigdump_sig]
142
+ if !old_sig && sig
143
+ enable_sigdump sig
144
+ elsif old_sig && !sig
145
+ disable_sigdump old_sig
146
+ elsif old_sig && sig
147
+ disable_sigdump old_sig
148
+ enable_sigdump sig
165
149
  end
150
+ end
166
151
 
167
- rdbg = 'rdbg'
168
-
169
- o.separator ''
170
- o.separator ' Debug console mode runs Ruby program with the debug console.'
171
- o.separator ''
172
- o.separator " '#{rdbg} target.rb foo bar' starts like 'ruby target.rb foo bar'."
173
- o.separator " '#{rdbg} -- -r foo -e bar' starts like 'ruby -r foo -e bar'."
174
- o.separator " '#{rdbg} -c rake test' starts like 'rake test'."
175
- o.separator " '#{rdbg} -c -- rake test -t' starts like 'rake test -t'."
176
- o.separator " '#{rdbg} -c bundle exec rake test' starts like 'bundle exec rake test'."
177
- o.separator " '#{rdbg} -O target.rb foo bar' starts and accepts attaching with UNIX domain socket."
178
- o.separator " '#{rdbg} -O --port 1234 target.rb foo bar' starts accepts attaching with TCP/IP localhost:1234."
179
- o.separator " '#{rdbg} -O --port 1234 -- -r foo -e bar' starts accepts attaching with TCP/IP localhost:1234."
180
-
181
- o.separator ''
182
- o.separator 'Attach mode:'
183
- o.on('-A', '--attach', 'Attach to debuggee process.') do
184
- config[:mode] = :attach
152
+ private def config
153
+ self.class.config
154
+ end
155
+
156
+ private def parse_config_value name, valstr
157
+ self.class.parse_config_value name, valstr
158
+ end
159
+
160
+ def self.parse_config_value name, valstr
161
+ return valstr unless valstr.kind_of? String
162
+
163
+ case CONFIG_SET[name][2]
164
+ when :bool
165
+ case valstr
166
+ when '1', 'true', 'TRUE', 'T'
167
+ true
168
+ else
169
+ false
170
+ end
171
+ when :int
172
+ valstr.to_i
173
+ when :loglevel
174
+ if DEBUGGER__::LOG_LEVELS[s = valstr.to_sym]
175
+ s
176
+ else
177
+ raise "Unknown loglevel: #{valstr}"
178
+ end
179
+ when :path # array of String
180
+ valstr.split(/:/).map{|e|
181
+ if /\A\/(.+)\/\z/ =~ e
182
+ Regexp.compile $1
183
+ else
184
+ e
185
+ end
186
+ }
187
+ else
188
+ valstr
185
189
  end
190
+ end
191
+
192
+ def self.parse_argv argv
193
+ config = {
194
+ mode: :start,
195
+ }
196
+ CONFIG_MAP.each{|key, evname|
197
+ if val = ENV[evname]
198
+ config[key] = parse_config_value(key, val)
199
+ end
200
+ }
201
+ return config if !argv || argv.empty?
186
202
 
187
- o.separator ''
188
- o.separator ' Attach mode attaches the remote debug console to the debuggee process.'
189
- o.separator ''
190
- o.separator " '#{rdbg} -A' tries to connect via UNIX domain socket."
191
- o.separator " #{' ' * rdbg.size} If there are multiple processes are waiting for the"
192
- o.separator " #{' ' * rdbg.size} debugger connection, list possible debuggee names."
193
- o.separator " '#{rdbg} -A path' tries to connect via UNIX domain socket with given path name."
194
- o.separator " '#{rdbg} -A port' tries to connect to localhost:port via TCP/IP."
195
- o.separator " '#{rdbg} -A host port' tries to connect to host:port via TCP/IP."
196
-
197
- o.separator ''
198
- o.separator 'Other options:'
199
-
200
- o.on("-h", "--help", "Print help") do
201
- puts o
202
- exit
203
+ if argv.kind_of? String
204
+ require 'shellwords'
205
+ argv = Shellwords.split(argv)
203
206
  end
204
207
 
205
- o.on('--util=NAME', 'Utility mode (used by tools)') do |name|
206
- require_relative 'client'
207
- Client.new(name)
208
- exit
208
+ require 'optparse'
209
+ require_relative 'version'
210
+
211
+ opt = OptionParser.new do |o|
212
+ o.banner = "#{$0} [options] -- [debuggee options]"
213
+ o.separator ''
214
+ o.version = ::DEBUGGER__::VERSION
215
+
216
+ o.separator 'Debug console mode:'
217
+ o.on('-n', '--nonstop', 'Do not stop at the beginning of the script.') do
218
+ config[:nonstop] = '1'
219
+ end
220
+
221
+ o.on('-e DEBUG_COMMAND', 'Execute debug command at the beginning of the script.') do |cmd|
222
+ config[:commands] ||= ''
223
+ config[:commands] += cmd + ';;'
224
+ end
225
+
226
+ o.on('-x FILE', '--init-script=FILE', 'Execute debug command in the FILE.') do |file|
227
+ config[:init_script] = file
228
+ end
229
+ o.on('--no-rc', 'Ignore ~/.rdbgrc') do
230
+ config[:no_rc] = true
231
+ end
232
+ o.on('--no-color', 'Disable colorize') do
233
+ config[:no_color] = true
234
+ end
235
+ o.on('--no-sigint-hook', 'Disable to trap SIGINT') do
236
+ config[:no_sigint_hook] = true
237
+ end
238
+
239
+ o.on('-c', '--command', 'Enable command mode.',
240
+ 'The first argument should be a command name in $PATH.',
241
+ 'Example: \'rdbg -c bundle exec rake test\'') do
242
+ config[:command] = true
243
+ end
244
+
245
+ o.separator ''
246
+
247
+ o.on('-O', '--open', 'Start remote debugging with opening the network port.',
248
+ 'If TCP/IP options are not given,',
249
+ 'a UNIX domain socket will be used.') do
250
+ config[:remote] = true
251
+ end
252
+ o.on('--sock-path=SOCK_PATH', 'UNIX Domain socket path') do |path|
253
+ config[:sock_path] = path
254
+ end
255
+ o.on('--port=PORT', 'Listening TCP/IP port') do |port|
256
+ config[:port] = port
257
+ end
258
+ o.on('--host=HOST', 'Listening TCP/IP host') do |host|
259
+ config[:host] = host
260
+ end
261
+ o.on('--cookie=COOKIE', 'Set a cookie for connection') do |c|
262
+ config[:cookie] = c
263
+ end
264
+
265
+ rdbg = 'rdbg'
266
+
267
+ o.separator ''
268
+ o.separator ' Debug console mode runs Ruby program with the debug console.'
269
+ o.separator ''
270
+ o.separator " '#{rdbg} target.rb foo bar' starts like 'ruby target.rb foo bar'."
271
+ o.separator " '#{rdbg} -- -r foo -e bar' starts like 'ruby -r foo -e bar'."
272
+ o.separator " '#{rdbg} -c rake test' starts like 'rake test'."
273
+ o.separator " '#{rdbg} -c -- rake test -t' starts like 'rake test -t'."
274
+ o.separator " '#{rdbg} -c bundle exec rake test' starts like 'bundle exec rake test'."
275
+ o.separator " '#{rdbg} -O target.rb foo bar' starts and accepts attaching with UNIX domain socket."
276
+ o.separator " '#{rdbg} -O --port 1234 target.rb foo bar' starts accepts attaching with TCP/IP localhost:1234."
277
+ o.separator " '#{rdbg} -O --port 1234 -- -r foo -e bar' starts accepts attaching with TCP/IP localhost:1234."
278
+
279
+ o.separator ''
280
+ o.separator 'Attach mode:'
281
+ o.on('-A', '--attach', 'Attach to debuggee process.') do
282
+ config[:mode] = :attach
283
+ end
284
+
285
+ o.separator ''
286
+ o.separator ' Attach mode attaches the remote debug console to the debuggee process.'
287
+ o.separator ''
288
+ o.separator " '#{rdbg} -A' tries to connect via UNIX domain socket."
289
+ o.separator " #{' ' * rdbg.size} If there are multiple processes are waiting for the"
290
+ o.separator " #{' ' * rdbg.size} debugger connection, list possible debuggee names."
291
+ o.separator " '#{rdbg} -A path' tries to connect via UNIX domain socket with given path name."
292
+ o.separator " '#{rdbg} -A port' tries to connect to localhost:port via TCP/IP."
293
+ o.separator " '#{rdbg} -A host port' tries to connect to host:port via TCP/IP."
294
+
295
+ o.separator ''
296
+ o.separator 'Other options:'
297
+
298
+ o.on("-h", "--help", "Print help") do
299
+ puts o
300
+ exit
301
+ end
302
+
303
+ o.on('--util=NAME', 'Utility mode (used by tools)') do |name|
304
+ require_relative 'client'
305
+ Client.new(name)
306
+ exit
307
+ end
308
+
309
+ o.separator ''
310
+ o.separator 'NOTE'
311
+ o.separator ' All messages communicated between a debugger and a debuggee are *NOT* encrypted.'
312
+ o.separator ' Please use the remote debugging feature carefully.'
209
313
  end
210
314
 
211
- o.separator ''
212
- o.separator 'NOTE'
213
- o.separator ' All messages communicated between a debugger and a debuggee are *NOT* encrypted.'
214
- o.separator ' Please use the remote debugging feature carefully.'
215
- end
315
+ opt.parse!(argv)
216
316
 
217
- opt.parse!(argv)
317
+ config
318
+ end
218
319
 
219
- config
320
+ def self.config_to_env_hash config
321
+ CONFIG_MAP.each_with_object({}){|(key, evname), env|
322
+ unless config[key].nil?
323
+ case CONFIG_SET[key][2]
324
+ when :path
325
+ valstr = config[key].map{|e| e.kind_of?(Regexp) ? e.inspect : e}.join(':')
326
+ else
327
+ valstr = config[key].to_s
328
+ end
329
+ env[evname] = valstr
330
+ end
331
+ }
332
+ end
220
333
  end
221
334
 
222
- CONFIG = ::DEBUGGER__.parse_argv(ENV['RUBY_DEBUG_OPT'])
335
+ CONFIG = Config.new ENV['RUBY_DEBUG_OPT']
223
336
 
224
- def self.set_config kw
225
- kw.each{|k, v|
226
- if CONFIG_MAP[k]
227
- CONFIG[k] = parse_config_value(k, v) # TODO: ractor support
228
- else
229
- raise "Unknown configuration: #{k}"
337
+ ## Unix domain socket configuration
338
+
339
+ def self.unix_domain_socket_dir
340
+ case
341
+ when path = CONFIG[:sock_dir]
342
+ when path = ENV['XDG_RUNTIME_DIR']
343
+ when home = ENV['HOME']
344
+ path = File.join(home, '.ruby-debug-sock')
345
+
346
+ case
347
+ when !File.exist?(path)
348
+ Dir.mkdir(path, 0700)
349
+ when !File.directory?(path)
350
+ raise "#{path} is not a directory."
230
351
  end
231
- }
352
+ else
353
+ raise 'specify RUBY_DEBUG_SOCK_DIR environment variable for UNIX domain socket directory.'
354
+ end
355
+
356
+ path
232
357
  end
233
358
 
234
- def self.append_config key, val
235
- if CONFIG_SET[key]
236
- if CONFIG_SET[key][2] == :path
237
- CONFIG[key] = [*CONFIG[key], *parse_config_value(key, val)];
238
- else
239
- raise "not an Array type: #{key}"
359
+ def self.create_unix_domain_socket_name_prefix(base_dir = unix_domain_socket_dir)
360
+ user = ENV['USER'] || 'ruby-debug'
361
+ File.join(base_dir, "ruby-debug-#{user}")
362
+ end
363
+
364
+ def self.create_unix_domain_socket_name(base_dir = unix_domain_socket_dir)
365
+ create_unix_domain_socket_name_prefix(base_dir) + "-#{Process.pid}"
366
+ end
367
+
368
+ ## Help
369
+
370
+ def self.parse_help
371
+ helps = Hash.new{|h, k| h[k] = []}
372
+ desc = cat = nil
373
+ cmds = Hash.new
374
+
375
+ File.read(File.join(__dir__, 'session.rb')).each_line do |line|
376
+ case line
377
+ when /\A\s*### (.+)/
378
+ cat = $1
379
+ break if $1 == 'END'
380
+ when /\A when (.+)/
381
+ next unless cat
382
+ next unless desc
383
+ ws = $1.split(/,\s*/).map{|e| e.gsub('\'', '')}
384
+ helps[cat] << [ws, desc]
385
+ desc = nil
386
+ max_w = ws.max_by{|w| w.length}
387
+ ws.each{|w|
388
+ cmds[w] = max_w
389
+ }
390
+ when /\A\s+# (\s*\*.+)/
391
+ if desc
392
+ desc << "\n" + $1
393
+ else
394
+ desc = $1
395
+ end
240
396
  end
241
- else
242
- raise "Unknown configuration: #{key}"
243
397
  end
398
+ @commands = cmds
399
+ @helps = helps
400
+ end
401
+
402
+ def self.helps
403
+ (defined?(@helps) && @helps) || parse_help
404
+ end
405
+
406
+ def self.commands
407
+ (defined?(@commands) && @commands) || (parse_help; @commands)
408
+ end
409
+
410
+ def self.help
411
+ r = []
412
+ self.helps.each{|cat, cmds|
413
+ r << "### #{cat}"
414
+ r << ''
415
+ cmds.each{|ws, desc|
416
+ r << desc
417
+ }
418
+ r << ''
419
+ }
420
+ r.join("\n")
244
421
  end
245
422
  end