# File debug-1.4.0/lib/debug/session.rb, line 85
def initialize ui
@ui = ui
@sr = SourceRepository.new
@bps = {} # bp.key => bp
# [file, line] => LineBreakpoint
# "Error" => CatchBreakpoint
# "Foo#bar" => MethodBreakpoint
# [:watch, ivar] => WatchIVarBreakpoint
# [:check, expr] => CheckBreakpoint
#
@tracers = {}
@th_clients = {} # {Thread => ThreadClient}
@q_evt = Queue.new
@displays = []
@tc = nil
@tc_id = 0
@preset_command = nil
@postmortem_hook = nil
@postmortem = false
@intercept_trap_sigint = false
@intercepted_sigint_cmd = 'DEFAULT'
@process_group = ProcessGroup.new
@subsession = nil
@frame_map = {} # for DAP: {id => [threadId, frame_depth]} and CDP: {id => frame_depth}
@var_map = {1 => [:globals], } # {id => ...} for DAP
@src_map = {} # {id => src}
@script_paths = [File.absolute_path($0)] # for CDP
@obj_map = {} # { object_id => ... } for CDP
@tp_thread_begin = nil
@tp_load_script = TracePoint.new(:script_compiled){|tp|
ThreadClient.current.on_load tp.instruction_sequence, tp.eval_script
}
@tp_load_script.enable
@thread_stopper = thread_stopper
activate
self.postmortem = CONFIG[:postmortem]
end
# File debug-1.4.0/lib/debug/session.rb, line 137
def activate on_fork: false
@tp_thread_begin&.disable
@tp_thread_begin = nil
if on_fork
@ui.activate self, on_fork: true
else
@ui.activate self, on_fork: false
end
q = Queue.new
@session_server = Thread.new do
Thread.current.name = 'DEBUGGER__::SESSION@server'
Thread.current.abort_on_exception = true
# Thread management
setup_threads
thc = get_thread_client Thread.current
thc.mark_as_management
if @ui.respond_to?(:reader_thread) && thc = get_thread_client(@ui.reader_thread)
thc.mark_as_management
end
@tp_thread_begin = TracePoint.new(:thread_begin) do |tp|
get_thread_client
end
@tp_thread_begin.enable
# session start
q << true
session_server_main
end
q.pop
end
# File debug-1.4.0/lib/debug/session.rb, line 129
def active?
!@q_evt.closed?
end
# File debug-1.4.0/lib/debug/session.rb, line 1232
def add_bp bp
# don't repeat commands that add breakpoints
@repl_prev_line = nil
if @bps.has_key? bp.key
unless bp.duplicable?
@ui.puts "duplicated breakpoint: #{bp}"
bp.disable
end
else
@bps[bp.key] = bp
end
end
# File debug-1.4.0/lib/debug/session.rb, line 1320
def add_catch_breakpoint pat
bp = CatchBreakpoint.new(pat)
add_bp bp
end
# File debug-1.4.0/lib/debug/session.rb, line 1325
def add_check_breakpoint expr, path
bp = CheckBreakpoint.new(expr, path)
add_bp bp
end
# File debug-1.4.0/lib/debug/session.rb, line 1339
def add_iseq_breakpoint iseq, **kw
bp = ISeqBreakpoint.new(iseq, [:line], **kw)
add_bp bp
end
# File debug-1.4.0/lib/debug/session.rb, line 1330
def add_line_breakpoint file, line, **kw
file = resolve_path(file)
bp = LineBreakpoint.new(file, line, **kw)
add_bp bp
rescue Errno::ENOENT => e
@ui.puts e.message
end
# File debug-1.4.0/lib/debug/session.rb, line 300
def add_preset_commands name, cmds, kick: true, continue: true
cs = cmds.map{|c|
c.each_line.map{|line|
line = line.strip.gsub(/\A\s*\#.*/, '').strip
line unless line.empty?
}.compact
}.flatten.compact
if @preset_command && !@preset_command.commands.empty?
@preset_command.commands += cs
else
@preset_command = PresetCommand.new(cs, name, continue)
end
ThreadClient.current.on_init name if kick
end
tracers
# File debug-1.4.0/lib/debug/session.rb, line 1346
def add_tracer tracer
# don't repeat commands that add tracers
@repl_prev_line = nil
if @tracers.has_key? tracer.key
tracer.disable
@ui.puts "Duplicated tracer: #{tracer}"
else
@tracers[tracer.key] = tracer
@ui.puts "Enable #{tracer}"
end
end
# File debug-1.4.0/lib/debug/session.rb, line 1717
def after_fork_parent
@ui.after_fork_parent
end
# File debug-1.4.0/lib/debug/session.rb, line 1174
def ask msg, default = 'Y'
opts = '[y/n]'.tr(default.downcase, default)
input = @ui.ask("#{msg} #{opts} ")
input = default if input.empty?
case input
when 'y', 'Y'
true
else
false
end
end
# File debug-1.4.0/lib/debug/session.rb, line 1711
def before_fork need_lock = true
if need_lock
@process_group.multi_process!
end
end
# File debug-1.4.0/lib/debug/session.rb, line 1209
def bp_index specific_bp_key
iterate_bps do |key, bp, i|
if key == specific_bp_key
return [bp, i]
end
end
nil
end
# File debug-1.4.0/lib/debug/session.rb, line 133
def break_at? file, line
@bps.has_key? [file, line]
end
# File debug-1.4.0/lib/debug/session.rb, line 1156
def cancel_auto_continue
if @preset_command&.auto_continue
@preset_command.auto_continue = false
end
end
# File debug-1.4.0/lib/debug/session.rb, line 1608
def capture_exception_frames *exclude_path
postmortem_hook = TracePoint.new(:raise){|tp|
exc = tp.raised_exception
frames = DEBUGGER__.capture_frames(__dir__)
exclude_path.each{|ex|
if Regexp === ex
frames.delete_if{|e| ex =~ e.path}
else
frames.delete_if{|e| e.path.start_with? ex.to_s}
end
}
exc.instance_variable_set(:@__debugger_postmortem_frames, frames)
}
postmortem_hook.enable
begin
yield
nil
rescue Exception => e
if e.instance_variable_defined? :@__debugger_postmortem_frames
e
else
raise
end
ensure
postmortem_hook.disable
end
end
# File debug-1.4.0/lib/debug/server_cdp.rb, line 560
def cdp_event args
type, req, result = args
case type
when :backtrace
result[:callFrames].each.with_index do |frame, i|
frame_id = frame[:callFrameId]
@frame_map[frame_id] = i
s_id = frame.dig(:location, :scriptId)
if File.exist?(s_id) && !@script_paths.include?(s_id)
src = File.read(s_id)
@ui.fire_event 'Debugger.scriptParsed',
scriptId: s_id,
url: frame[:url],
startLine: 0,
startColumn: 0,
endLine: src.count("\n"),
endColumn: 0,
executionContextId: @script_paths.size + 1,
hash: src.hash
@script_paths << s_id
end
frame[:scopeChain].each {|s|
oid = s.dig(:object, :objectId)
@obj_map[oid] = [s[:type], frame_id]
}
end
if oid = result.dig(:data, :objectId)
@obj_map[oid] = ['properties']
end
@ui.fire_event 'Debugger.paused', **result
when :evaluate
message = result.delete :message
if message
fail_response req,
code: INVALID_PARAMS,
message: message
else
rs = result.dig(:response, :result)
[rs].each{|obj|
if oid = obj[:objectId]
@obj_map[oid] = ['properties']
end
}
@ui.respond req, **result[:response]
out = result[:output]
if out && !out.empty?
@ui.fire_event 'Runtime.consoleAPICalled',
type: 'log',
args: [
type: out.class,
value: out
],
executionContextId: 1, # Change this number if something goes wrong.
timestamp: Time.now.to_f
end
end
when :scope
result.each{|obj|
if oid = obj.dig(:value, :objectId)
@obj_map[oid] = ['properties']
end
}
@ui.respond req, result: result
when :properties
result.each_value{|v|
v.each{|obj|
if oid = obj.dig(:value, :objectId)
@obj_map[oid] = ['properties']
end
}
}
@ui.respond req, **result
end
end
# File debug-1.4.0/lib/debug/session.rb, line 1592
def check_postmortem
if @postmortem
raise PostmortemError, "Can not use this command on postmortem mode."
end
end
# File debug-1.4.0/lib/debug/session.rb, line 1226
def clean_bps
@bps.delete_if{|_k, bp|
bp.deleted?
}
end
# File debug-1.4.0/lib/debug/session.rb, line 1122
def config_command arg
case arg
when nil
CONFIG_SET.each do |k, _|
config_show k
end
when /\Aunset\s+(.+)\z/
if CONFIG_SET[key = $1.to_sym]
CONFIG[key] = nil
end
config_show key
when /\A(\w+)\s*=\s*(.+)\z/
config_set $1, $2
when /\A\s*set\s+(\w+)\s+(.+)\z/
config_set $1, $2
when /\A(\w+)\s*<<\s*(.+)\z/
config_set $1, $2, append: true
when /\A\s*append\s+(\w+)\s+(.+)\z/
config_set $1, $2
when /\A(\w+)\z/
config_show $1
else
@ui.puts "Can not parse parameters: #{arg}"
end
end
# File debug-1.4.0/lib/debug/session.rb, line 1106
def config_set key, val, append: false
if CONFIG_SET[key = key.to_sym]
begin
if append
CONFIG.append_config(key, val)
else
CONFIG[key] = val
end
rescue => e
@ui.puts e.message
end
end
config_show key
end
# File debug-1.4.0/lib/debug/session.rb, line 1089
def config_show key
key = key.to_sym
if CONFIG_SET[key]
v = CONFIG[key]
kv = "#{key} = #{v.nil? ? '(default)' : v.inspect}"
desc = CONFIG_SET[key][1]
line = "%-30s \# %s" % [kv, desc]
if line.size > SESSION.width
@ui.puts "\# #{desc}\n#{kv}"
else
@ui.puts line
end
else
@ui.puts "Unknown configuration: #{key}. 'config' shows all configurations."
end
end
# File debug-1.4.0/lib/debug/server_dap.rb, line 524
def dap_event args
# puts({dap_event: args}.inspect)
type, req, result = args
case type
when :backtrace
result[:stackFrames].each.with_index{|fi, i|
fi[:id] = id = @frame_map.size + 1
@frame_map[id] = [req.dig('arguments', 'threadId'), i]
if fi[:source] && src = fi[:source][:sourceReference]
src_id = @src_map.size + 1
@src_map[src_id] = src
fi[:source][:sourceReference] = src_id
end
}
@ui.respond req, result
when :scopes
frame_id = req.dig('arguments', 'frameId')
local_scope = result[:scopes].first
local_scope[:variablesReference] = id = @var_map.size + 1
@var_map[id] = [:scope, frame_id]
@ui.respond req, result
when :scope
tid = result.delete :tid
register_vars result[:variables], tid
@ui.respond req, result
when :variable
tid = result.delete :tid
register_vars result[:variables], tid
@ui.respond req, result
when :evaluate
message = result.delete :message
if message
@ui.respond req, success: false, message: message
else
tid = result.delete :tid
register_var result, tid
@ui.respond req, result
end
when :completions
@ui.respond req, result
else
raise "unsupported: #{args.inspect}"
end
end
# File debug-1.4.0/lib/debug/session.rb, line 174
def deactivate
get_thread_client.deactivate
@thread_stopper.disable
@tp_load_script.disable
@tp_thread_begin.disable
@bps.each_value{|bp| bp.disable}
@th_clients.each_value{|thc| thc.close}
@tracers.values.each{|t| t.disable}
@q_evt.close
@ui&.deactivate
@ui = nil
end
# File debug-1.4.0/lib/debug/session.rb, line 1246
def delete_bp arg = nil
case arg
when nil
@bps.each{|key, bp| bp.delete}
@bps.clear
else
del_bp = nil
iterate_bps{|key, bp, i| del_bp = bp if i == arg}
if del_bp
del_bp.delete
@bps.delete del_bp.key
return [arg, del_bp]
end
end
end
# File debug-1.4.0/lib/debug/session.rb, line 1598
def enter_postmortem_session exc
return unless exc.instance_variable_defined? :@__debugger_postmortem_frames
frames = exc.instance_variable_get(:@__debugger_postmortem_frames)
@postmortem = true
ThreadClient.current.suspend :postmortem, postmortem_frames: frames, postmortem_exc: exc
ensure
@postmortem = false
end
# File debug-1.4.0/lib/debug/server_cdp.rb, line 514
def fail_response req, **result
@ui.respond_fail req, result
return :retry
end
# File debug-1.4.0/lib/debug/server_dap.rb, line 397
def find_waiting_tc id
@th_clients.each{|th, tc|
return tc if tc.id == id && tc.waiting?
}
return nil
end
can be called by other threads
# File debug-1.4.0/lib/debug/session.rb, line 1448
def get_thread_client th = Thread.current
if @th_clients.has_key? th
@th_clients[th]
else
if Thread.current == @session_server
create_thread_client th
else
ask_thread_client th
end
end
end
# File debug-1.4.0/lib/debug/session.rb, line 1525
def in_subsession?
@subsession
end
# File debug-1.4.0/lib/debug/session.rb, line 325
def inspect
"DEBUGGER__::SESSION"
end
# File debug-1.4.0/lib/debug/session.rb, line 1686
def intercept_trap_sigint flag, &b
prev = @intercept_trap_sigint
@intercept_trap_sigint = flag
yield
ensure
@intercept_trap_sigint = prev
end
# File debug-1.4.0/lib/debug/session.rb, line 1682
def intercept_trap_sigint?
@intercept_trap_sigint
end
# File debug-1.4.0/lib/debug/session.rb, line 1699
def intercept_trap_sigint_end
@intercept_trap_sigint = false
prev, @intercepted_sigint_cmd = @intercepted_sigint_cmd, nil
prev
end
# File debug-1.4.0/lib/debug/session.rb, line 1694
def intercept_trap_sigint_start prev
@intercept_trap_sigint = true
@intercepted_sigint_cmd = prev
end
breakpoint management
# File debug-1.4.0/lib/debug/session.rb, line 1188
def iterate_bps
deleted_bps = []
i = 0
@bps.each{|key, bp|
if !bp.deleted?
yield key, bp, i
i += 1
else
deleted_bps << bp
end
}
ensure
deleted_bps.each{|bp| @bps.delete bp}
end
# File debug-1.4.0/lib/debug/session.rb, line 1392
def managed_thread_clients
thcs, _unmanaged_ths = update_thread_list
thcs
end
# File debug-1.4.0/lib/debug/session.rb, line 1564
def method_added tp
b = tp.binding
if var_name = b.local_variables.first
mid = b.local_variable_get(var_name)
unresolved = false
@bps.each{|k, bp|
case bp
when MethodBreakpoint
if bp.method.nil?
if bp.sig_method_name == mid.to_s
bp.try_enable(added: true)
end
end
unresolved = true unless bp.enabled?
end
}
unless unresolved
METHOD_ADDED_TRACKER.disable
end
end
end
event
# File debug-1.4.0/lib/debug/session.rb, line 1531
def on_load iseq, src
DEBUGGER__.info "Load #{iseq.absolute_path || iseq.path}"
@sr.add iseq, src
pending_line_breakpoints = @bps.find_all do |key, bp|
LineBreakpoint === bp && !bp.iseq
end
pending_line_breakpoints.each do |_key, bp|
if bp.path == (iseq.absolute_path || iseq.path)
bp.try_activate
end
end
end
# File debug-1.4.0/lib/debug/session.rb, line 1423
def on_thread_begin th
if @th_clients.has_key? th
# TODO: NG?
else
create_thread_client th
end
end
# File debug-1.4.0/lib/debug/session.rb, line 1264
def parse_break arg
mode = :sig
expr = Hash.new{|h, k| h[k] = []}
arg.split(' ').each{|w|
if BREAK_KEYWORDS.any?{|pat| w == pat}
mode = w[0..-2].to_sym
else
expr[mode] << w
end
}
expr.default_proc = nil
expr.transform_values{|v| v.join(' ')}
end
# File debug-1.4.0/lib/debug/session.rb, line 192
def pop_event
@q_evt.pop
end
# File debug-1.4.0/lib/debug/session.rb, line 1638
def postmortem=(is_enable)
if is_enable
unless @postmortem_hook
@postmortem_hook = TracePoint.new(:raise){|tp|
exc = tp.raised_exception
frames = DEBUGGER__.capture_frames(__dir__)
exc.instance_variable_set(:@__debugger_postmortem_frames, frames)
}
at_exit{
@postmortem_hook.disable
if CONFIG[:postmortem] && (exc = $!) != nil
exc = exc.cause while exc.cause
begin
@ui.puts "Enter postmortem mode with #{exc.inspect}"
@ui.puts exc.backtrace.map{|e| ' ' + e}
@ui.puts "\n"
enter_postmortem_session exc
rescue SystemExit
exit!
rescue Exception => e
@ui = STDERR unless @ui
@ui.puts "Error while postmortem console: #{e.inspect}"
end
end
}
end
if !@postmortem_hook.enabled?
@postmortem_hook.enable
end
else
if @postmortem_hook && @postmortem_hook.enabled?
@postmortem_hook.disable
end
end
end
# File debug-1.4.0/lib/debug/session.rb, line 386
def process_command line
if line.empty?
if @repl_prev_line
line = @repl_prev_line
else
return :retry
end
else
@repl_prev_line = line
end
/([^\s]+)(?:\s+(.+))?/ =~ line
cmd, arg = $1, $2
# p cmd: [cmd, *arg]
case cmd
### Control flow
# * `s[tep]`
# * Step in. Resume the program until next breakable point.
# * `s[tep] <n>`
# * Step in, resume the program at `<n>`th breakable point.
when 's', 'step'
cancel_auto_continue
check_postmortem
step_command :in, arg
# * `n[ext]`
# * Step over. Resume the program until next line.
# * `n[ext] <n>`
# * Step over, same as `step <n>`.
when 'n', 'next'
cancel_auto_continue
check_postmortem
step_command :next, arg
# * `fin[ish]`
# * Finish this frame. Resume the program until the current frame is finished.
# * `fin[ish] <n>`
# * Finish `<n>`th frames.
when 'fin', 'finish'
cancel_auto_continue
check_postmortem
if arg&.to_i == 0
raise 'finish command with 0 does not make sense.'
end
step_command :finish, arg
# * `c[ontinue]`
# * Resume the program.
when 'c', 'continue'
cancel_auto_continue
leave_subsession :continue
# * `q[uit]` or `Ctrl-D`
# * Finish debugger (with the debuggee process on non-remote debugging).
when 'q', 'quit'
if ask 'Really quit?'
@ui.quit arg.to_i
leave_subsession :continue
else
return :retry
end
# * `q[uit]!`
# * Same as q[uit] but without the confirmation prompt.
when 'q!', 'quit!'
@ui.quit arg.to_i
leave_subsession nil
# * `kill`
# * Stop the debuggee process with `Kernel#exit!`.
when 'kill'
if ask 'Really kill?'
exit! (arg || 1).to_i
else
return :retry
end
# * `kill!`
# * Same as kill but without the confirmation prompt.
when 'kill!'
exit! (arg || 1).to_i
# * `sigint`
# * Execute SIGINT handler registered by the debuggee.
# * Note that this command should be used just after stop by `SIGINT`.
when 'sigint'
begin
case cmd = @intercepted_sigint_cmd
when nil, 'IGNORE', :IGNORE, 'DEFAULT', :DEFAULT
# ignore
when String
eval(cmd)
when Proc
cmd.call
end
leave_subsession :continue
rescue Exception => e
@ui.puts "Exception: #{e}"
@ui.puts e.backtrace.map{|line| " #{e}"}
return :retry
end
### Breakpoint
# * `b[reak]`
# * Show all breakpoints.
# * `b[reak] <line>`
# * Set breakpoint on `<line>` at the current frame's file.
# * `b[reak] <file>:<line>` or `<file> <line>`
# * Set breakpoint on `<file>:<line>`.
# * `b[reak] <class>#<name>`
# * Set breakpoint on the method `<class>#<name>`.
# * `b[reak] <expr>.<name>`
# * Set breakpoint on the method `<expr>.<name>`.
# * `b[reak] ... if: <expr>`
# * break if `<expr>` is true at specified location.
# * `b[reak] ... pre: <command>`
# * break and run `<command>` before stopping.
# * `b[reak] ... do: <command>`
# * break and run `<command>`, and continue.
# * `b[reak] ... path: <path_regexp>`
# * break if the triggering event's path matches <path_regexp>.
# * `b[reak] if: <expr>`
# * break if: `<expr>` is true at any lines.
# * Note that this feature is super slow.
when 'b', 'break'
check_postmortem
if arg == nil
show_bps
return :retry
else
case bp = repl_add_breakpoint(arg)
when :noretry
when nil
return :retry
else
show_bps bp
return :retry
end
end
# skip
when 'bv'
check_postmortem
require 'json'
h = Hash.new{|h, k| h[k] = []}
@bps.each_value{|bp|
if LineBreakpoint === bp
h[bp.path] << {lnum: bp.line}
end
}
if h.empty?
# TODO: clean?
else
open(".rdb_breakpoints.json", 'w'){|f| JSON.dump(h, f)}
end
vimsrc = File.join(__dir__, 'bp.vim')
system("vim -R -S #{vimsrc} #{@tc.location.path}")
if File.exist?(".rdb_breakpoints.json")
pp JSON.load(File.read(".rdb_breakpoints.json"))
end
return :retry
# * `catch <Error>`
# * Set breakpoint on raising `<Error>`.
# * `catch ... if: <expr>`
# * stops only if `<expr>` is true as well.
# * `catch ... pre: <command>`
# * runs `<command>` before stopping.
# * `catch ... do: <command>`
# * stops and run `<command>`, and continue.
# * `catch ... path: <path_regexp>`
# * stops if the exception is raised from a path that matches <path_regexp>.
when 'catch'
check_postmortem
if arg
bp = repl_add_catch_breakpoint arg
show_bps bp if bp
else
show_bps
end
return :retry
# * `watch @ivar`
# * Stop the execution when the result of current scope's `@ivar` is changed.
# * Note that this feature is super slow.
# * `watch ... if: <expr>`
# * stops only if `<expr>` is true as well.
# * `watch ... pre: <command>`
# * runs `<command>` before stopping.
# * `watch ... do: <command>`
# * stops and run `<command>`, and continue.
# * `watch ... path: <path_regexp>`
# * stops if the triggering event's path matches <path_regexp>.
when 'wat', 'watch'
check_postmortem
if arg && arg.match?(/\A@\w+/)
repl_add_watch_breakpoint(arg)
else
show_bps
return :retry
end
# * `del[ete]`
# * delete all breakpoints.
# * `del[ete] <bpnum>`
# * delete specified breakpoint.
when 'del', 'delete'
check_postmortem
bp =
case arg
when nil
show_bps
if ask "Remove all breakpoints?", 'N'
delete_bp
end
when /\d+/
delete_bp arg.to_i
else
nil
end
@ui.puts "deleted: \##{bp[0]} #{bp[1]}" if bp
return :retry
### Information
# * `bt` or `backtrace`
# * Show backtrace (frame) information.
# * `bt <num>` or `backtrace <num>`
# * Only shows first `<num>` frames.
# * `bt /regexp/` or `backtrace /regexp/`
# * Only shows frames with method name or location info that matches `/regexp/`.
# * `bt <num> /regexp/` or `backtrace <num> /regexp/`
# * Only shows first `<num>` frames with method name or location info that matches `/regexp/`.
when 'bt', 'backtrace'
case arg
when /\A(\d+)\z/
@tc << [:show, :backtrace, arg.to_i, nil]
when /\A\/(.*)\/\z/
pattern = $1
@tc << [:show, :backtrace, nil, Regexp.compile(pattern)]
when /\A(\d+)\s+\/(.*)\/\z/
max, pattern = $1, $2
@tc << [:show, :backtrace, max.to_i, Regexp.compile(pattern)]
else
@tc << [:show, :backtrace, nil, nil]
end
# * `l[ist]`
# * Show current frame's source code.
# * Next `list` command shows the successor lines.
# * `l[ist] -`
# * Show predecessor lines as opposed to the `list` command.
# * `l[ist] <start>` or `l[ist] <start>-<end>`
# * Show current frame's source code from the line <start> to <end> if given.
when 'l', 'list'
case arg ? arg.strip : nil
when /\A(\d+)\z/
@tc << [:show, :list, {start_line: arg.to_i - 1}]
when /\A-\z/
@tc << [:show, :list, {dir: -1}]
when /\A(\d+)-(\d+)\z/
@tc << [:show, :list, {start_line: $1.to_i - 1, end_line: $2.to_i}]
when nil
@tc << [:show, :list]
else
@ui.puts "Can not handle list argument: #{arg}"
return :retry
end
# * `edit`
# * Open the current file on the editor (use `EDITOR` environment variable).
# * Note that edited file will not be reloaded.
# * `edit <file>`
# * Open <file> on the editor.
when 'edit'
if @ui.remote?
@ui.puts "not supported on the remote console."
return :retry
end
begin
arg = resolve_path(arg) if arg
rescue Errno::ENOENT
@ui.puts "not found: #{arg}"
return :retry
end
@tc << [:show, :edit, arg]
# * `i[nfo]`
# * Show information about current frame (local/instance variables and defined constants).
# * `i[nfo] l[ocal[s]]`
# * Show information about the current frame (local variables)
# * It includes `self` as `%self` and a return value as `%return`.
# * `i[nfo] i[var[s]]` or `i[nfo] instance`
# * Show information about instance variables about `self`.
# * `i[nfo] c[onst[s]]` or `i[nfo] constant[s]`
# * Show information about accessible constants except toplevel constants.
# * `i[nfo] g[lobal[s]]`
# * Show information about global variables
# * `i[nfo] ... </pattern/>`
# * Filter the output with `</pattern/>`.
# * `i[nfo] th[read[s]]`
# * Show all threads (same as `th[read]`).
when 'i', 'info'
if /\/(.+)\/\z/ =~ arg
pat = Regexp.compile($1)
sub = $~.pre_match.strip
else
sub = arg
end
case sub
when nil
@tc << [:show, :default, pat] # something useful
when 'l', /^locals?/
@tc << [:show, :locals, pat]
when 'i', /^ivars?/i, /^instance[_ ]variables?/i
@tc << [:show, :ivars, pat]
when 'c', /^consts?/i, /^constants?/i
@tc << [:show, :consts, pat]
when 'g', /^globals?/i, /^global[_ ]variables?/i
@tc << [:show, :globals, pat]
when 'th', /threads?/
thread_list
return :retry
else
@ui.puts "unrecognized argument for info command: #{arg}"
show_help 'info'
return :retry
end
# * `o[utline]` or `ls`
# * Show you available methods, constants, local variables, and instance variables in the current scope.
# * `o[utline] <expr>` or `ls <expr>`
# * Show you available methods and instance variables of the given object.
# * If the object is a class/module, it also lists its constants.
when 'outline', 'o', 'ls'
@tc << [:show, :outline, arg]
# * `display`
# * Show display setting.
# * `display <expr>`
# * Show the result of `<expr>` at every suspended timing.
when 'display'
if arg && !arg.empty?
@displays << arg
@tc << [:eval, :try_display, @displays]
else
@tc << [:eval, :display, @displays]
end
# * `undisplay`
# * Remove all display settings.
# * `undisplay <displaynum>`
# * Remove a specified display setting.
when 'undisplay'
case arg
when /(\d+)/
if @displays[n = $1.to_i]
@displays.delete_at n
end
@tc << [:eval, :display, @displays]
when nil
if ask "clear all?", 'N'
@displays.clear
end
return :retry
end
### Frame control
# * `f[rame]`
# * Show the current frame.
# * `f[rame] <framenum>`
# * Specify a current frame. Evaluation are run on specified frame.
when 'frame', 'f'
@tc << [:frame, :set, arg]
# * `up`
# * Specify the upper frame.
when 'up'
@tc << [:frame, :up]
# * `down`
# * Specify the lower frame.
when 'down'
@tc << [:frame, :down]
### Evaluate
# * `p <expr>`
# * Evaluate like `p <expr>` on the current frame.
when 'p'
@tc << [:eval, :p, arg.to_s]
# * `pp <expr>`
# * Evaluate like `pp <expr>` on the current frame.
when 'pp'
@tc << [:eval, :pp, arg.to_s]
# * `eval <expr>`
# * Evaluate `<expr>` on the current frame.
when 'eval', 'call'
if arg == nil || arg.empty?
show_help 'eval'
@ui.puts "\nTo evaluate the variable `#{cmd}`, use `pp #{cmd}` instead."
return :retry
else
@tc << [:eval, :call, arg]
end
# * `irb`
# * Invoke `irb` on the current frame.
when 'irb'
if @ui.remote?
@ui.puts "not supported on the remote console."
return :retry
end
@tc << [:eval, :irb]
# don't repeat irb command
@repl_prev_line = nil
### Trace
# * `trace`
# * Show available tracers list.
# * `trace line`
# * Add a line tracer. It indicates line events.
# * `trace call`
# * Add a call tracer. It indicate call/return events.
# * `trace exception`
# * Add an exception tracer. It indicates raising exceptions.
# * `trace object <expr>`
# * Add an object tracer. It indicates that an object by `<expr>` is passed as a parameter or a receiver on method call.
# * `trace ... </pattern/>`
# * Indicates only matched events to `</pattern/>` (RegExp).
# * `trace ... into: <file>`
# * Save trace information into: `<file>`.
# * `trace off <num>`
# * Disable tracer specified by `<num>` (use `trace` command to check the numbers).
# * `trace off [line|call|pass]`
# * Disable all tracers. If `<type>` is provided, disable specified type tracers.
when 'trace'
if (re = /\s+into:\s*(.+)/) =~ arg
into = $1
arg.sub!(re, '')
end
if (re = /\s\/(.+)\/\z/) =~ arg
pattern = $1
arg.sub!(re, '')
end
case arg
when nil
@ui.puts 'Tracers:'
@tracers.values.each_with_index{|t, i|
@ui.puts "* \##{i} #{t}"
}
@ui.puts
return :retry
when /\Aline\z/
add_tracer LineTracer.new(@ui, pattern: pattern, into: into)
return :retry
when /\Acall\z/
add_tracer CallTracer.new(@ui, pattern: pattern, into: into)
return :retry
when /\Aexception\z/
add_tracer ExceptionTracer.new(@ui, pattern: pattern, into: into)
return :retry
when /\Aobject\s+(.+)/
@tc << [:trace, :object, $1.strip, {pattern: pattern, into: into}]
when /\Aoff\s+(\d+)\z/
if t = @tracers.values[$1.to_i]
t.disable
@ui.puts "Disable #{t.to_s}"
else
@ui.puts "Unmatched: #{$1}"
end
return :retry
when /\Aoff(\s+(line|call|exception|object))?\z/
@tracers.values.each{|t|
if $2.nil? || t.type == $2
t.disable
@ui.puts "Disable #{t.to_s}"
end
}
return :retry
else
@ui.puts "Unknown trace option: #{arg.inspect}"
return :retry
end
# Record
# * `record`
# * Show recording status.
# * `record [on|off]`
# * Start/Stop recording.
# * `step back`
# * Start replay. Step back with the last execution log.
# * `s[tep]` does stepping forward with the last log.
# * `step reset`
# * Stop replay .
when 'record'
case arg
when nil, 'on', 'off'
@tc << [:record, arg&.to_sym]
else
@ui.puts "unknown command: #{arg}"
return :retry
end
### Thread control
# * `th[read]`
# * Show all threads.
# * `th[read] <thnum>`
# * Switch thread specified by `<thnum>`.
when 'th', 'thread'
case arg
when nil, 'list', 'l'
thread_list
when /(\d+)/
switch_thread $1.to_i
else
@ui.puts "unknown thread command: #{arg}"
end
return :retry
### Configuration
# * `config`
# * Show all configuration with description.
# * `config <name>`
# * Show current configuration of <name>.
# * `config set <name> <val>` or `config <name> = <val>`
# * Set <name> to <val>.
# * `config append <name> <val>` or `config <name> << <val>`
# * Append `<val>` to `<name>` if it is an array.
# * `config unset <name>`
# * Set <name> to default.
when 'config'
config_command arg
return :retry
# * `source <file>`
# * Evaluate lines in `<file>` as debug commands.
when 'source'
if arg
begin
cmds = File.readlines(path = File.expand_path(arg))
add_preset_commands path, cmds, kick: true, continue: false
rescue Errno::ENOENT
@ui.puts "File not found: #{arg}"
end
else
show_help 'source'
end
return :retry
# * `open`
# * open debuggee port on UNIX domain socket and wait for attaching.
# * Note that `open` command is EXPERIMENTAL.
# * `open [<host>:]<port>`
# * open debuggee port on TCP/IP with given `[<host>:]<port>` and wait for attaching.
# * `open vscode`
# * open debuggee port for VSCode and launch VSCode if available.
# * `open chrome`
# * open debuggee port for Chrome and wait for attaching.
when 'open'
case arg&.downcase
when '', nil
repl_open_unix
when 'vscode'
repl_open_vscode
when /\A(.+):(\d+)\z/
repl_open_tcp $1, $2.to_i
when /\A(\d+)z/
repl_open_tcp nil, $1.to_i
when 'tcp'
repl_open_tcp CONFIG[:host], (CONFIG[:port] || 0)
when 'chrome', 'cdp'
CONFIG[:open_frontend] = 'chrome'
repl_open_tcp CONFIG[:host], (CONFIG[:port] || 0)
else
raise "Unknown arg: #{arg}"
end
return :retry
### Help
# * `h[elp]`
# * Show help for all commands.
# * `h[elp] <command>`
# * Show help for the given command.
when 'h', 'help', '?'
if arg
show_help arg
else
@ui.puts DEBUGGER__.help
end
return :retry
### END
else
@tc << [:eval, :pp, line]
=begin
@repl_prev_line = nil
@ui.puts "unknown command: #{line}"
begin
require 'did_you_mean'
spell_checker = DidYouMean::SpellChecker.new(dictionary: DEBUGGER__.commands)
correction = spell_checker.correct(line.split(/\s/).first || '')
@ui.puts "Did you mean? #{correction.join(' or ')}" unless correction.empty?
rescue LoadError
# Don't use D
end
return :retry
=end
end
rescue Interrupt
return :retry
rescue SystemExit
raise
rescue PostmortemError => e
@ui.puts e.message
return :retry
rescue Exception => e
@ui.puts "[REPL ERROR] #{e.inspect}"
@ui.puts e.backtrace.map{|e| ' ' + e}
return :retry
end
# File debug-1.4.0/lib/debug/session.rb, line 204
def process_event evt
# variable `@internal_info` is only used for test
tc, output, ev, @internal_info, *ev_args = evt
output.each{|str| @ui.puts str} if ev != :suspend
case ev
when :thread_begin # special event, tc is nil
th = ev_args.shift
q = ev_args.shift
on_thread_begin th
q << true
when :init
wait_command_loop tc
when :load
iseq, src = ev_args
on_load iseq, src
@ui.event :load
tc << :continue
when :trace
trace_id, msg = ev_args
if t = @tracers.values.find{|t| t.object_id == trace_id}
t.puts msg
end
tc << :continue
when :suspend
enter_subsession if ev_args.first != :replay
output.each{|str| @ui.puts str}
case ev_args.first
when :breakpoint
bp, i = bp_index ev_args[1]
clean_bps unless bp
@ui.event :suspend_bp, i, bp, tc.id
when :trap
@ui.event :suspend_trap, sig = ev_args[1], tc.id
if sig == :SIGINT && (@intercepted_sigint_cmd.kind_of?(Proc) || @intercepted_sigint_cmd.kind_of?(String))
@ui.puts "#{@intercepted_sigint_cmd.inspect} is registered as SIGINT handler."
@ui.puts "`sigint` command execute it."
end
else
@ui.event :suspended, tc.id
end
if @displays.empty?
wait_command_loop tc
else
tc << [:eval, :display, @displays]
end
when :result
raise "[BUG] not in subsession" unless @subsession
case ev_args.first
when :try_display
failed_results = ev_args[1]
if failed_results.size > 0
i, _msg = failed_results.last
if i+1 == @displays.size
@ui.puts "canceled: #{@displays.pop}"
end
end
when :method_breakpoint, :watch_breakpoint
bp = ev_args[1]
if bp
add_bp(bp)
show_bps bp
else
# can't make a bp
end
when :trace_pass
obj_id = ev_args[1]
obj_inspect = ev_args[2]
opt = ev_args[3]
add_tracer ObjectTracer.new(@ui, obj_id, obj_inspect, **opt)
else
# ignore
end
wait_command_loop tc
when :dap_result
dap_event ev_args # server.rb
wait_command_loop tc
when :cdp_result
cdp_event ev_args
wait_command_loop tc
end
end
# File debug-1.4.0/lib/debug/session.rb, line 1705
def process_info
if @process_group.multi?
"#{$0}\##{Process.pid}"
end
end
# File debug-1.4.0/lib/debug/server_cdp.rb, line 521
def process_protocol_request req
case req['method']
when 'Debugger.stepOver', 'Debugger.stepInto', 'Debugger.stepOut', 'Debugger.resume', 'Debugger.getScriptSource'
@tc << [:cdp, :backtrace, req]
when 'Debugger.evaluateOnCallFrame'
frame_id = req.dig('params', 'callFrameId')
if fid = @frame_map[frame_id]
expr = req.dig('params', 'expression')
@tc << [:cdp, :evaluate, req, fid, expr]
else
fail_response req,
code: INVALID_PARAMS,
message: "'callFrameId' is an invalid"
end
when 'Runtime.getProperties'
oid = req.dig('params', 'objectId')
if ref = @obj_map[oid]
case ref[0]
when 'local'
frame_id = ref[1]
fid = @frame_map[frame_id]
@tc << [:cdp, :scope, req, fid]
when 'properties'
@tc << [:cdp, :properties, req, oid]
when 'script', 'global'
# TODO: Support script and global types
@ui.respond req
return :retry
else
raise "Unknown type: #{ref.inspect}"
end
else
fail_response req,
code: INVALID_PARAMS,
message: "'objectId' is an invalid"
end
end
end
# File debug-1.4.0/lib/debug/session.rb, line 345
def prompt
if @postmortem
'(rdbg:postmortem) '
elsif @process_group.multi?
"(rdbg@#{process_info}) "
else
'(rdbg) '
end
end
# File debug-1.4.0/lib/debug/server_dap.rb, line 571
def register_var v, tid
if (tl_vid = v[:variablesReference]) > 0
vid = @var_map.size + 1
@var_map[vid] = [:variable, tid, tl_vid]
v[:variablesReference] = vid
end
end
# File debug-1.4.0/lib/debug/server_dap.rb, line 579
def register_vars vars, tid
raise tid.inspect unless tid.kind_of?(Integer)
vars.each{|v|
register_var v, tid
}
end
# File debug-1.4.0/lib/debug/session.rb, line 1218
def rehash_bps
bps = @bps.values
@bps.clear
bps.each{|bp|
add_bp bp
}
end
# File debug-1.4.0/lib/debug/session.rb, line 1278
def repl_add_breakpoint arg
expr = parse_break arg.strip
cond = expr[:if]
cmd = ['break', expr[:pre], expr[:do]] if expr[:pre] || expr[:do]
path = Regexp.compile(expr[:path]) if expr[:path]
case expr[:sig]
when /\A(\d+)\z/
add_line_breakpoint @tc.location.path, $1.to_i, cond: cond, command: cmd
when /\A(.+)[:\s+](\d+)\z/
add_line_breakpoint $1, $2.to_i, cond: cond, command: cmd
when /\A(.+)([\.\#])(.+)\z/
@tc << [:breakpoint, :method, $1, $2, $3, cond, cmd, path]
return :noretry
when nil
add_check_breakpoint cond, path
else
@ui.puts "Unknown breakpoint format: #{arg}"
@ui.puts
show_help 'b'
end
end
# File debug-1.4.0/lib/debug/session.rb, line 1301
def repl_add_catch_breakpoint arg
expr = parse_break arg.strip
cond = expr[:if]
cmd = ['catch', expr[:pre], expr[:do]] if expr[:pre] || expr[:do]
path = Regexp.compile(expr[:path]) if expr[:path]
bp = CatchBreakpoint.new(expr[:sig], cond: cond, command: cmd, path: path)
add_bp bp
end
# File debug-1.4.0/lib/debug/session.rb, line 1311
def repl_add_watch_breakpoint arg
expr = parse_break arg.strip
cond = expr[:if]
cmd = ['watch', expr[:pre], expr[:do]] if expr[:pre] || expr[:do]
path = Regexp.compile(expr[:path]) if expr[:path]
@tc << [:breakpoint, :watch, expr[:sig], cond, cmd, path]
end
# File debug-1.4.0/lib/debug/session.rb, line 1044
def repl_open_setup
@tp_thread_begin.disable
@ui.activate self
if @ui.respond_to?(:reader_thread) && thc = get_thread_client(@ui.reader_thread)
thc.mark_as_management
end
@tp_thread_begin.enable
end
# File debug-1.4.0/lib/debug/session.rb, line 1053
def repl_open_tcp host, port, **kw
DEBUGGER__.open_tcp host: host, port: port, nonstop: true, **kw
repl_open_setup
end
# File debug-1.4.0/lib/debug/session.rb, line 1058
def repl_open_unix
DEBUGGER__.open_unix nonstop: true
repl_open_setup
end
# File debug-1.4.0/lib/debug/session.rb, line 1063
def repl_open_vscode
CONFIG[:open_frontend] = 'vscode'
repl_open_unix
end
# File debug-1.4.0/lib/debug/session.rb, line 187
def reset_ui ui
@ui.deactivate
@ui = ui
end
# File debug-1.4.0/lib/debug/session.rb, line 1546
def resolve_path file
File.realpath(File.expand_path(file))
rescue Errno::ENOENT
case file
when '-e', '-'
return file
else
$LOAD_PATH.each do |lp|
libpath = File.join(lp, file)
return File.realpath(libpath)
rescue Errno::ENOENT
# next
end
end
raise
end
# File debug-1.4.0/lib/debug/session.rb, line 1677
def save_int_trap cmd
prev, @intercepted_sigint_cmd = @intercepted_sigint_cmd, cmd
prev
end
# File debug-1.4.0/lib/debug/session.rb, line 196
def session_server_main
while evt = pop_event
process_event evt
end
ensure
deactivate
end
# File debug-1.4.0/lib/debug/session.rb, line 1410
def setup_threads
prev_clients = @th_clients
@th_clients = {}
Thread.list.each{|th|
if tc = prev_clients[th]
@th_clients[th] = tc
else
create_thread_client(th)
end
}
end
# File debug-1.4.0/lib/debug/session.rb, line 1203
def show_bps specific_bp = nil
iterate_bps do |key, bp, i|
@ui.puts "#%d %s" % [i, bp.to_s] if !specific_bp || bp == specific_bp
end
end
# File debug-1.4.0/lib/debug/session.rb, line 1162
def show_help arg
DEBUGGER__.helps.each{|cat, cs|
cs.each{|ws, desc|
if ws.include? arg
@ui.puts desc
return
end
}
}
@ui.puts "not found: #{arg}"
end
# File debug-1.4.0/lib/debug/session.rb, line 317
def source iseq
if !CONFIG[:no_color]
@sr.get_colored(iseq)
else
@sr.get(iseq)
end
end
# File debug-1.4.0/lib/debug/session.rb, line 1068
def step_command type, arg
case arg
when nil, /\A\d+\z/
if type == :in && @tc.recorder&.replaying?
@tc << [:step, type, arg&.to_i]
else
leave_subsession [:step, type, arg&.to_i]
end
when /\Aback\z/, /\Areset\z/
if type != :in
@ui.puts "only `step #{arg}` is supported."
:retry
else
@tc << [:step, arg.to_sym]
end
else
@ui.puts "Unknown option: #{arg}"
:retry
end
end
# File debug-1.4.0/lib/debug/session.rb, line 1397
def switch_thread n
thcs, _unmanaged_ths = update_thread_list
if tc = thcs[n]
if tc.waiting?
@tc = tc
else
@ui.puts "#{tc.thread} is not controllable yet."
end
end
thread_list
end
# File debug-1.4.0/lib/debug/session.rb, line 1378
def thread_list
thcs, unmanaged_ths = update_thread_list
thcs.each_with_index{|thc, i|
@ui.puts "#{@tc == thc ? "--> " : " "}\##{i} #{thc}"
}
if !unmanaged_ths.empty?
@ui.puts "The following threads are not managed yet by the debugger:"
unmanaged_ths.each{|th|
@ui.puts " " + th.to_s
}
end
end
threads
# File debug-1.4.0/lib/debug/session.rb, line 1360
def update_thread_list
list = Thread.list
thcs = []
unmanaged = []
list.each{|th|
if thc = @th_clients[th]
if !thc.management?
thcs << thc
end
else
unmanaged << th
end
}
return thcs.sort_by{|thc| thc.id}, unmanaged
end
# File debug-1.4.0/lib/debug/session.rb, line 355
def wait_command
if @preset_command
if @preset_command.commands.empty?
if @preset_command.auto_continue
@preset_command = nil
leave_subsession :continue
return
else
@preset_command = nil
return :retry
end
else
line = @preset_command.commands.shift
@ui.puts "(rdbg:#{@preset_command.source}) #{line}"
end
else
@ui.puts "INTERNAL_INFO: #{JSON.generate(@internal_info)}" if ENV['RUBY_DEBUG_TEST_MODE']
line = @ui.readline prompt
end
case line
when String
process_command line
when Hash
process_protocol_request line # defined in server.rb
else
raise "unexpected input: #{line.inspect}"
end
end