debug 1.0.0.beta7 → 1.0.0

Sign up to get free protection for your applications and to get access to all the features.
data/Rakefile CHANGED
@@ -17,7 +17,8 @@ end
17
17
 
18
18
  task :default => [:clobber, :compile, :test, 'README.md']
19
19
 
20
- file 'README.md' => ['lib/debug/session.rb', 'exe/rdbg', 'misc/README.md.erb'] do
20
+ file 'README.md' => ['lib/debug/session.rb', 'lib/debug/config.rb',
21
+ 'exe/rdbg', 'misc/README.md.erb'] do
21
22
  require_relative 'lib/debug/session'
22
23
  require 'erb'
23
24
  File.write 'README.md', ERB.new(File.read('misc/README.md.erb')).result
data/TODO.md CHANGED
@@ -3,11 +3,13 @@
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
13
15
 
@@ -15,13 +17,6 @@
15
17
 
16
18
  * Breakpoints
17
19
  * Lightweight pending method break points with Ruby 3.1 feature (TP:method_added)
18
- * Non-stop breakpoint but runs some code.
19
20
  * Watch points
20
21
  * Lightweight watchpoints for instance variables with Ruby 3.1 features (TP:ivar_set)
21
22
  * 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,27 +1,24 @@
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
8
8
  require 'rbconfig'
9
9
 
10
10
  libpath = File.join(File.expand_path(File.dirname(__dir__)), 'lib/debug')
11
- start_mode = config[:remote] ? "open" : 'run'
12
- cmd = config[:command] ? ARGV.shift : RbConfig.ruby
11
+ start_mode = config[:remote] ? "open" : 'start'
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,53 +1,29 @@
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],
47
24
  init_script: ['RUBY_DEBUG_INIT_SCRIPT', "BOOT: debug command script path loaded at first stop"],
48
25
  commands: ['RUBY_DEBUG_COMMANDS', "BOOT: debug commands invoked at first stop. commands should be separated by ';;'"],
49
26
  no_rc: ['RUBY_DEBUG_NO_RC', "BOOT: ignore loading ~/.rdbgrc(.rb)", :bool],
50
- history: ['RUBY_DEBUG_HISTORY', "BOOT: save and load history file (default: ~/.rdbg_history)"],
51
27
 
52
28
  # remote setting
53
29
  port: ['RUBY_DEBUG_PORT', "REMOTE: TCP/IP remote debugging: port"],
@@ -59,185 +35,388 @@ module DEBUGGER__
59
35
 
60
36
  CONFIG_MAP = CONFIG_SET.map{|k, (ev, desc)| [k, ev]}.to_h.freeze
61
37
 
62
- def self.config_to_env_hash config
63
- CONFIG_MAP.each_with_object({}){|(key, evname), env|
64
- env[evname] = config[key].to_s if config[key]
65
- }
66
- end
67
-
68
- def self.parse_config_value name, valstr
69
- return valstr unless valstr.kind_of? String
38
+ class Config
39
+ def self.config
40
+ @config
41
+ end
70
42
 
71
- case CONFIG_SET[name][2]
72
- when :bool
73
- case valstr
74
- when '1', 'true', 'TRUE', 'T'
75
- true
76
- else
77
- false
43
+ def initialize argv
44
+ if self.class.instance_variable_defined? :@config
45
+ raise 'Can not make multiple configurations in one process'
78
46
  end
79
- when :int
80
- valstr.to_i
81
- when :loglevel
82
- if DEBUGGER__::LOG_LEVELS[s = valstr.to_sym]
83
- s
84
- else
85
- raise "Unknown loglevel: #{valstr}"
86
- end
87
- when :path # array of String
88
- valstr.split(/:/).map{|e|
89
- if /\A\/(.+)\/\z/ =~ e
90
- 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
91
64
  else
92
- e
65
+ raise "Unknown configuration: #{k}"
93
66
  end
94
67
  }
95
- else
96
- valstr
68
+
69
+ update conf
97
70
  end
98
- end
99
71
 
100
- def self.parse_argv argv
101
- config = {
102
- mode: :start,
103
- }
104
- CONFIG_MAP.each{|key, evname|
105
- if val = ENV[evname]
106
- 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}"
107
83
  end
108
- }
109
- return config if !argv || argv.empty?
110
84
 
111
- require 'optparse'
112
- require_relative 'version'
85
+ update conf
86
+ end
113
87
 
114
- opt = OptionParser.new do |o|
115
- o.banner = "#{$0} [options] -- [debuggee options]"
116
- o.separator ''
117
- o.version = ::DEBUGGER__::VERSION
88
+ def update conf
89
+ old_conf = self.class.instance_variable_get(:@config) || {}
118
90
 
119
- o.separator 'Debug console mode:'
120
- o.on('-n', '--nonstop', 'Do not stop at the beginning of the script.') do
121
- config[:nonstop] = '1'
122
- end
91
+ # TODO: Use Ractor.make_shareable(conf)
92
+ self.class.instance_variable_set(:@config, conf.freeze)
123
93
 
124
- o.on('-e DEBUG_COMMAND', 'Execute debug command at the beginning of the script.') do |cmd|
125
- config[:commands] ||= ''
126
- 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
127
102
  end
128
103
 
129
- o.on('-x FILE', '--init-script=FILE', 'Execute debug command in the FILE.') do |file|
130
- config[:init_script] = file
131
- end
132
- o.on('--no-rc', 'Ignore ~/.rdbgrc') do
133
- config[:no_rc] = true
134
- end
135
- o.on('--no-color', 'Disable colorize') do
136
- config[:no_color] = true
104
+ if_updated old_conf, conf, :postmortem do |_, new_p|
105
+ SESSION.postmortem = new_p
137
106
  end
138
107
 
139
- o.on('-c', '--command', 'Enable command mode.',
140
- 'The first argument should be a command name in $PATH.',
141
- 'Example: \'rdbg -c bundle exec rake test\'') do
142
- config[:command] = true
108
+ if_updated old_conf, conf, :sigdump_sig do |old_sig, new_sig|
109
+ setup_sigdump old_sig, new_sig
143
110
  end
111
+ end
144
112
 
145
- 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
146
117
 
147
- o.on('-O', '--open', 'Start remote debugging with opening the network port.',
148
- 'If TCP/IP options are not given,',
149
- 'a UNIX domain socket will be used.') do
150
- config[:remote] = true
151
- end
152
- o.on('--sock-path=SOCK_PATH', 'UNIX Doman socket path') do |path|
153
- config[:sock_path] = path
154
- end
155
- o.on('--port=PORT', 'Listening TCP/IP port') do |port|
156
- config[:port] = port
157
- end
158
- o.on('--host=HOST', 'Listening TCP/IP host') do |host|
159
- 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
160
131
  end
161
- o.on('--cookie=COOKIE', 'Set a cookie for connection') do |c|
162
- 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
163
149
  end
150
+ end
164
151
 
165
- rdbg = 'rdbg'
166
-
167
- o.separator ''
168
- o.separator ' Debug console mode runs Ruby program with the debug console.'
169
- o.separator ''
170
- o.separator " '#{rdbg} target.rb foo bar' starts like 'ruby target.rb foo bar'."
171
- o.separator " '#{rdbg} -- -r foo -e bar' starts like 'ruby -r foo -e bar'."
172
- o.separator " '#{rdbg} -c rake test' starts like 'rake test'."
173
- o.separator " '#{rdbg} -c -- rake test -t' starts like 'rake test -t'."
174
- o.separator " '#{rdbg} -c bundle exec rake test' starts like 'bundle exec rake test'."
175
- o.separator " '#{rdbg} -O target.rb foo bar' starts and accepts attaching with UNIX domain socket."
176
- o.separator " '#{rdbg} -O --port 1234 target.rb foo bar' starts accepts attaching with TCP/IP localhost:1234."
177
- o.separator " '#{rdbg} -O --port 1234 -- -r foo -e bar' starts accepts attaching with TCP/IP localhost:1234."
178
-
179
- o.separator ''
180
- o.separator 'Attach mode:'
181
- o.on('-A', '--attach', 'Attach to debuggee process.') do
182
- 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
183
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?
184
202
 
185
- o.separator ''
186
- o.separator ' Attach mode attaches the remote debug console to the debuggee process.'
187
- o.separator ''
188
- o.separator " '#{rdbg} -A' tries to connect via UNIX domain socket."
189
- o.separator " #{' ' * rdbg.size} If there are multiple processes are waiting for the"
190
- o.separator " #{' ' * rdbg.size} debugger connection, list possible debuggee names."
191
- o.separator " '#{rdbg} -A path' tries to connect via UNIX domain socket with given path name."
192
- o.separator " '#{rdbg} -A port' tries to connect to localhost:port via TCP/IP."
193
- o.separator " '#{rdbg} -A host port' tries to connect to host:port via TCP/IP."
194
-
195
- o.separator ''
196
- o.separator 'Other options:'
197
-
198
- o.on("-h", "--help", "Print help") do
199
- puts o
200
- exit
203
+ if argv.kind_of? String
204
+ require 'shellwords'
205
+ argv = Shellwords.split(argv)
201
206
  end
202
207
 
203
- o.on('--util=NAME', 'Utility mode (used by tools)') do |name|
204
- require_relative 'client'
205
- Client.new(name)
206
- 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.'
207
313
  end
208
314
 
209
- o.separator ''
210
- o.separator 'NOTE'
211
- o.separator ' All messages communicated between a debugger and a debuggee are *NOT* encrypted.'
212
- o.separator ' Please use the remote debugging feature carefully.'
213
- end
315
+ opt.parse!(argv)
214
316
 
215
- opt.parse!(argv)
317
+ config
318
+ end
216
319
 
217
- 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
218
333
  end
219
334
 
220
- CONFIG = ::DEBUGGER__.parse_argv(ENV['RUBY_DEBUG_OPT'])
335
+ CONFIG = Config.new ENV['RUBY_DEBUG_OPT']
221
336
 
222
- def self.set_config kw
223
- kw.each{|k, v|
224
- if CONFIG_MAP[k]
225
- CONFIG[k] = parse_config_value(k, v) # TODO: ractor support
226
- else
227
- 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."
228
351
  end
229
- }
352
+ else
353
+ raise 'specify RUBY_DEBUG_SOCK_DIR environment variable for UNIX domain socket directory.'
354
+ end
355
+
356
+ path
230
357
  end
231
358
 
232
- def self.append_config key, val
233
- if CONFIG_SET[key]
234
- if CONFIG_SET[key][2] == :path
235
- CONFIG[key] = [*CONFIG[key], *parse_config_value(key, val)];
236
- else
237
- 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
238
396
  end
239
- else
240
- raise "Unknown configuration: #{key}"
241
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")
242
421
  end
243
422
  end