debug 1.0.0.beta8 → 1.1.0

Sign up to get free protection for your applications and to get access to all the features.
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