debug 1.0.0.beta8 → 1.1.0

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/Rakefile CHANGED
@@ -15,7 +15,7 @@ Rake::ExtensionTask.new("debug") do |ext|
15
15
  ext.lib_dir = "lib/debug"
16
16
  end
17
17
 
18
- task :default => [:clobber, :compile, :test, 'README.md']
18
+ task :default => [:clobber, :compile, 'README.md', :test]
19
19
 
20
20
  file 'README.md' => ['lib/debug/session.rb', 'lib/debug/config.rb',
21
21
  'exe/rdbg', 'misc/README.md.erb'] do
data/TODO.md CHANGED
@@ -3,25 +3,21 @@
3
3
  ## Basic functionality
4
4
 
5
5
  * Support Ractors
6
- * Signal (SIGINT) support
6
+ * Signal (SIGINT) trap handling
7
7
 
8
8
  ## UI
9
9
 
10
+ * Completion for Ruby's code
10
11
  * Interactive breakpoint setting
12
+ * Interactive record & play debugging
11
13
  * irb integration
12
14
  * Web browser integrated UI
15
+ * History file
13
16
 
14
17
  ## Debug command
15
18
 
16
19
  * Breakpoints
17
20
  * Lightweight pending method break points with Ruby 3.1 feature (TP:method_added)
18
- * Non-stop breakpoint but runs some code.
19
21
  * Watch points
20
22
  * Lightweight watchpoints for instance variables with Ruby 3.1 features (TP:ivar_set)
21
23
  * Faster `next`/`finish` command by specifying target code.
22
- * `set`/`show` configurations
23
- * In-memory line traces
24
- * Timemachine debugging
25
-
26
- ## Tests
27
-
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
@@ -9,19 +9,16 @@ when :start
9
9
 
10
10
  libpath = File.join(File.expand_path(File.dirname(__dir__)), 'lib/debug')
11
11
  start_mode = config[:remote] ? "open" : 'start'
12
- cmd = config[:command] ? ARGV.shift : RbConfig.ruby
12
+ cmd = config[:command] ? ARGV.shift : (ENV['RUBY'] || 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,31 @@
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
4
+ LOG_LEVELS = {
5
+ UNKNOWN: 0,
6
+ FATAL: 1,
7
+ ERROR: 2,
8
+ WARN: 3,
9
+ INFO: 4,
10
+ }.freeze
32
11
 
33
12
  CONFIG_SET = {
34
13
  # UI setting
35
14
  log_level: ['RUBY_DEBUG_LOG_LEVEL', "UI: Log level same as Logger (default: WARN)", :loglevel],
36
15
  show_src_lines: ['RUBY_DEBUG_SHOW_SRC_LINES', "UI: Show n lines source code on breakpoint (default: 10 lines)", :int],
37
16
  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
- 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],
17
+ use_short_path: ['RUBY_DEBUG_USE_SHORT_PATH', "UI: Show shorten PATH (like $(Gem)/foo.rb)", :bool],
42
18
  no_color: ['RUBY_DEBUG_NO_COLOR', "UI: Do not use colorize (default: false)", :bool],
43
19
  no_sigint_hook: ['RUBY_DEBUG_NO_SIGINT_HOOK', "UI: Do not suspend on SIGINT (default: false)", :bool],
20
+ no_reline: ['RUBY_DEBUG_NO_RELINE', "UI: Do not use Reline library (default: false)", :bool],
21
+
22
+ # control setting
23
+ skip_path: ['RUBY_DEBUG_SKIP_PATH', "CONTROL: Skip showing/entering frames for given paths (default: [])", :path],
24
+ skip_nosrc: ['RUBY_DEBUG_SKIP_NOSRC', "CONTROL: Skip on no source code lines (default: false)", :bool],
25
+ keep_alloc_site:['RUBY_DEBUG_KEEP_ALLOC_SITE',"CONTROL: Keep allocation site and p, pp shows it (default: false)", :bool],
26
+ postmortem: ['RUBY_DEBUG_POSTMORTEM', "CONTROL: Enable postmortem debug (default: false)", :bool],
27
+ parent_on_fork: ['RUBY_DEBUG_PARENT_ON_FORK', "CONTROL: Keep debugging parent process on fork (default: false)", :bool],
28
+ sigdump_sig: ['RUBY_DEBUG_SIGDUMP_SIG', "CONTROL: Sigdump signal (default: disabled)"],
44
29
 
45
30
  # boot setting
46
31
  nonstop: ['RUBY_DEBUG_NONSTOP', "BOOT: Nonstop mode", :bool],
@@ -58,188 +43,388 @@ module DEBUGGER__
58
43
 
59
44
  CONFIG_MAP = CONFIG_SET.map{|k, (ev, desc)| [k, ev]}.to_h.freeze
60
45
 
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
46
+ class Config
47
+ def self.config
48
+ @config
49
+ end
69
50
 
70
- case CONFIG_SET[name][2]
71
- when :bool
72
- case valstr
73
- when '1', 'true', 'TRUE', 'T'
74
- true
75
- else
76
- false
51
+ def initialize argv
52
+ if self.class.instance_variable_defined? :@config
53
+ raise 'Can not make multiple configurations in one process'
77
54
  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
55
+
56
+ update self.class.parse_argv(argv)
57
+ end
58
+
59
+ def [](key)
60
+ config[key]
61
+ end
62
+
63
+ def []=(key, val)
64
+ set_config(key => val)
65
+ end
66
+
67
+ def set_config(**kw)
68
+ conf = config.dup
69
+ kw.each{|k, v|
70
+ if CONFIG_MAP[k]
71
+ conf[k] = parse_config_value(k, v) # TODO: ractor support
90
72
  else
91
- e
73
+ raise "Unknown configuration: #{k}"
92
74
  end
93
75
  }
94
- else
95
- valstr
76
+
77
+ update conf
96
78
  end
97
- end
98
79
 
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)
80
+ def append_config key, val
81
+ conf = self.config.dup
82
+
83
+ if CONFIG_SET[key]
84
+ if CONFIG_SET[key][2] == :path
85
+ conf[key] = [*conf[key], *parse_config_value(key, val)];
86
+ else
87
+ raise "not an Array type: #{key}"
88
+ end
89
+ else
90
+ raise "Unknown configuration: #{key}"
106
91
  end
107
- }
108
- return config if !argv || argv.empty?
109
92
 
110
- require 'optparse'
111
- require_relative 'version'
93
+ update conf
94
+ end
112
95
 
113
- opt = OptionParser.new do |o|
114
- o.banner = "#{$0} [options] -- [debuggee options]"
115
- o.separator ''
116
- o.version = ::DEBUGGER__::VERSION
96
+ def update conf
97
+ old_conf = self.class.instance_variable_get(:@config) || {}
117
98
 
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
99
+ # TODO: Use Ractor.make_shareable(conf)
100
+ self.class.instance_variable_set(:@config, conf.freeze)
122
101
 
123
- o.on('-e DEBUG_COMMAND', 'Execute debug command at the beginning of the script.') do |cmd|
124
- config[:commands] ||= ''
125
- config[:commands] += cmd + ';;'
102
+ # Post process
103
+ if_updated old_conf, conf, :keep_alloc_site do |_, new|
104
+ if new
105
+ require 'objspace'
106
+ ObjectSpace.trace_object_allocations_start
107
+ else
108
+ ObjectSpace.trace_object_allocations_stop
109
+ end
126
110
  end
127
111
 
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
112
+ if_updated old_conf, conf, :postmortem do |_, new_p|
113
+ SESSION.postmortem = new_p
139
114
  end
140
115
 
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
116
+ if_updated old_conf, conf, :sigdump_sig do |old_sig, new_sig|
117
+ setup_sigdump old_sig, new_sig
145
118
  end
119
+ end
146
120
 
147
- o.separator ''
121
+ private def if_updated old_conf, new_conf, key
122
+ old, new = old_conf[key], new_conf[key]
123
+ yield old, new if old != new
124
+ end
148
125
 
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
126
+ private def enable_sigdump sig
127
+ @sigdump_sig_prev = trap(sig) do
128
+ str = []
129
+ str << "Simple sigdump on #{Process.pid}"
130
+ Thread.list.each{|th|
131
+ str << "Thread: #{th}"
132
+ th.backtrace.each{|loc|
133
+ str << " #{loc}"
134
+ }
135
+ str << ''
136
+ }
137
+
138
+ STDERR.puts str
162
139
  end
163
- o.on('--cookie=COOKIE', 'Set a cookie for connection') do |c|
164
- config[:cookie] = c
140
+ end
141
+
142
+ private def disable_sigdump old_sig
143
+ trap(old_sig, @sigdump_sig_prev)
144
+ @sigdump_sig_prev = nil
145
+ end
146
+
147
+ # emergency simple sigdump.
148
+ # Use `sigdump` gem for more rich features.
149
+ private def setup_sigdump old_sig = nil, sig = CONFIG[:sigdump_sig]
150
+ if !old_sig && sig
151
+ enable_sigdump sig
152
+ elsif old_sig && !sig
153
+ disable_sigdump old_sig
154
+ elsif old_sig && sig
155
+ disable_sigdump old_sig
156
+ enable_sigdump sig
165
157
  end
158
+ end
166
159
 
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
160
+ private def config
161
+ self.class.config
162
+ end
163
+
164
+ private def parse_config_value name, valstr
165
+ self.class.parse_config_value name, valstr
166
+ end
167
+
168
+ def self.parse_config_value name, valstr
169
+ return valstr unless valstr.kind_of? String
170
+
171
+ case CONFIG_SET[name][2]
172
+ when :bool
173
+ case valstr
174
+ when '1', 'true', 'TRUE', 'T'
175
+ true
176
+ else
177
+ false
178
+ end
179
+ when :int
180
+ valstr.to_i
181
+ when :loglevel
182
+ if DEBUGGER__::LOG_LEVELS[s = valstr.to_sym]
183
+ s
184
+ else
185
+ raise "Unknown loglevel: #{valstr}"
186
+ end
187
+ when :path # array of String
188
+ valstr.split(/:/).map{|e|
189
+ if /\A\/(.+)\/\z/ =~ e
190
+ Regexp.compile $1
191
+ else
192
+ e
193
+ end
194
+ }
195
+ else
196
+ valstr
185
197
  end
198
+ end
199
+
200
+ def self.parse_argv argv
201
+ config = {
202
+ mode: :start,
203
+ }
204
+ CONFIG_MAP.each{|key, evname|
205
+ if val = ENV[evname]
206
+ config[key] = parse_config_value(key, val)
207
+ end
208
+ }
209
+ return config if !argv || argv.empty?
186
210
 
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
211
+ if argv.kind_of? String
212
+ require 'shellwords'
213
+ argv = Shellwords.split(argv)
203
214
  end
204
215
 
205
- o.on('--util=NAME', 'Utility mode (used by tools)') do |name|
206
- require_relative 'client'
207
- Client.new(name)
208
- exit
216
+ require 'optparse'
217
+ require_relative 'version'
218
+
219
+ opt = OptionParser.new do |o|
220
+ o.banner = "#{$0} [options] -- [debuggee options]"
221
+ o.separator ''
222
+ o.version = ::DEBUGGER__::VERSION
223
+
224
+ o.separator 'Debug console mode:'
225
+ o.on('-n', '--nonstop', 'Do not stop at the beginning of the script.') do
226
+ config[:nonstop] = '1'
227
+ end
228
+
229
+ o.on('-e DEBUG_COMMAND', 'Execute debug command at the beginning of the script.') do |cmd|
230
+ config[:commands] ||= ''
231
+ config[:commands] += cmd + ';;'
232
+ end
233
+
234
+ o.on('-x FILE', '--init-script=FILE', 'Execute debug command in the FILE.') do |file|
235
+ config[:init_script] = file
236
+ end
237
+ o.on('--no-rc', 'Ignore ~/.rdbgrc') do
238
+ config[:no_rc] = true
239
+ end
240
+ o.on('--no-color', 'Disable colorize') do
241
+ config[:no_color] = true
242
+ end
243
+ o.on('--no-sigint-hook', 'Disable to trap SIGINT') do
244
+ config[:no_sigint_hook] = true
245
+ end
246
+
247
+ o.on('-c', '--command', 'Enable command mode.',
248
+ 'The first argument should be a command name in $PATH.',
249
+ 'Example: \'rdbg -c bundle exec rake test\'') do
250
+ config[:command] = true
251
+ end
252
+
253
+ o.separator ''
254
+
255
+ o.on('-O', '--open', 'Start remote debugging with opening the network port.',
256
+ 'If TCP/IP options are not given,',
257
+ 'a UNIX domain socket will be used.') do
258
+ config[:remote] = true
259
+ end
260
+ o.on('--sock-path=SOCK_PATH', 'UNIX Domain socket path') do |path|
261
+ config[:sock_path] = path
262
+ end
263
+ o.on('--port=PORT', 'Listening TCP/IP port') do |port|
264
+ config[:port] = port
265
+ end
266
+ o.on('--host=HOST', 'Listening TCP/IP host') do |host|
267
+ config[:host] = host
268
+ end
269
+ o.on('--cookie=COOKIE', 'Set a cookie for connection') do |c|
270
+ config[:cookie] = c
271
+ end
272
+
273
+ rdbg = 'rdbg'
274
+
275
+ o.separator ''
276
+ o.separator ' Debug console mode runs Ruby program with the debug console.'
277
+ o.separator ''
278
+ o.separator " '#{rdbg} target.rb foo bar' starts like 'ruby target.rb foo bar'."
279
+ o.separator " '#{rdbg} -- -r foo -e bar' starts like 'ruby -r foo -e bar'."
280
+ o.separator " '#{rdbg} -c rake test' starts like 'rake test'."
281
+ o.separator " '#{rdbg} -c -- rake test -t' starts like 'rake test -t'."
282
+ o.separator " '#{rdbg} -c bundle exec rake test' starts like 'bundle exec rake test'."
283
+ o.separator " '#{rdbg} -O target.rb foo bar' starts and accepts attaching with UNIX domain socket."
284
+ o.separator " '#{rdbg} -O --port 1234 target.rb foo bar' starts accepts attaching with TCP/IP localhost:1234."
285
+ o.separator " '#{rdbg} -O --port 1234 -- -r foo -e bar' starts accepts attaching with TCP/IP localhost:1234."
286
+
287
+ o.separator ''
288
+ o.separator 'Attach mode:'
289
+ o.on('-A', '--attach', 'Attach to debuggee process.') do
290
+ config[:mode] = :attach
291
+ end
292
+
293
+ o.separator ''
294
+ o.separator ' Attach mode attaches the remote debug console to the debuggee process.'
295
+ o.separator ''
296
+ o.separator " '#{rdbg} -A' tries to connect via UNIX domain socket."
297
+ o.separator " #{' ' * rdbg.size} If there are multiple processes are waiting for the"
298
+ o.separator " #{' ' * rdbg.size} debugger connection, list possible debuggee names."
299
+ o.separator " '#{rdbg} -A path' tries to connect via UNIX domain socket with given path name."
300
+ o.separator " '#{rdbg} -A port' tries to connect to localhost:port via TCP/IP."
301
+ o.separator " '#{rdbg} -A host port' tries to connect to host:port via TCP/IP."
302
+
303
+ o.separator ''
304
+ o.separator 'Other options:'
305
+
306
+ o.on("-h", "--help", "Print help") do
307
+ puts o
308
+ exit
309
+ end
310
+
311
+ o.on('--util=NAME', 'Utility mode (used by tools)') do |name|
312
+ require_relative 'client'
313
+ Client.new(name)
314
+ exit
315
+ end
316
+
317
+ o.separator ''
318
+ o.separator 'NOTE'
319
+ o.separator ' All messages communicated between a debugger and a debuggee are *NOT* encrypted.'
320
+ o.separator ' Please use the remote debugging feature carefully.'
209
321
  end
210
322
 
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
323
+ opt.parse!(argv)
216
324
 
217
- opt.parse!(argv)
325
+ config
326
+ end
218
327
 
219
- config
328
+ def self.config_to_env_hash config
329
+ CONFIG_MAP.each_with_object({}){|(key, evname), env|
330
+ unless config[key].nil?
331
+ case CONFIG_SET[key][2]
332
+ when :path
333
+ valstr = config[key].map{|e| e.kind_of?(Regexp) ? e.inspect : e}.join(':')
334
+ else
335
+ valstr = config[key].to_s
336
+ end
337
+ env[evname] = valstr
338
+ end
339
+ }
340
+ end
220
341
  end
221
342
 
222
- CONFIG = ::DEBUGGER__.parse_argv(ENV['RUBY_DEBUG_OPT'])
343
+ CONFIG = Config.new ENV['RUBY_DEBUG_OPT']
223
344
 
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}"
345
+ ## Unix domain socket configuration
346
+
347
+ def self.unix_domain_socket_dir
348
+ case
349
+ when path = CONFIG[:sock_dir]
350
+ when path = ENV['XDG_RUNTIME_DIR']
351
+ when home = ENV['HOME']
352
+ path = File.join(home, '.ruby-debug-sock')
353
+
354
+ case
355
+ when !File.exist?(path)
356
+ Dir.mkdir(path, 0700)
357
+ when !File.directory?(path)
358
+ raise "#{path} is not a directory."
230
359
  end
231
- }
360
+ else
361
+ raise 'specify RUBY_DEBUG_SOCK_DIR environment variable for UNIX domain socket directory.'
362
+ end
363
+
364
+ path
232
365
  end
233
366
 
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}"
367
+ def self.create_unix_domain_socket_name_prefix(base_dir = unix_domain_socket_dir)
368
+ user = ENV['USER'] || 'ruby-debug'
369
+ File.join(base_dir, "ruby-debug-#{user}")
370
+ end
371
+
372
+ def self.create_unix_domain_socket_name(base_dir = unix_domain_socket_dir)
373
+ create_unix_domain_socket_name_prefix(base_dir) + "-#{Process.pid}"
374
+ end
375
+
376
+ ## Help
377
+
378
+ def self.parse_help
379
+ helps = Hash.new{|h, k| h[k] = []}
380
+ desc = cat = nil
381
+ cmds = Hash.new
382
+
383
+ File.read(File.join(__dir__, 'session.rb'), encoding: Encoding::UTF_8).each_line do |line|
384
+ case line
385
+ when /\A\s*### (.+)/
386
+ cat = $1
387
+ break if $1 == 'END'
388
+ when /\A when (.+)/
389
+ next unless cat
390
+ next unless desc
391
+ ws = $1.split(/,\s*/).map{|e| e.gsub('\'', '')}
392
+ helps[cat] << [ws, desc]
393
+ desc = nil
394
+ max_w = ws.max_by{|w| w.length}
395
+ ws.each{|w|
396
+ cmds[w] = max_w
397
+ }
398
+ when /\A\s+# (\s*\*.+)/
399
+ if desc
400
+ desc << "\n" + $1
401
+ else
402
+ desc = $1
403
+ end
240
404
  end
241
- else
242
- raise "Unknown configuration: #{key}"
243
405
  end
406
+ @commands = cmds
407
+ @helps = helps
408
+ end
409
+
410
+ def self.helps
411
+ (defined?(@helps) && @helps) || parse_help
412
+ end
413
+
414
+ def self.commands
415
+ (defined?(@commands) && @commands) || (parse_help; @commands)
416
+ end
417
+
418
+ def self.help
419
+ r = []
420
+ self.helps.each{|cat, cmds|
421
+ r << "### #{cat}"
422
+ r << ''
423
+ cmds.each{|ws, desc|
424
+ r << desc
425
+ }
426
+ r << ''
427
+ }
428
+ r.join("\n")
244
429
  end
245
430
  end