class Byebug::DAP::Command

Implementation of a DAP command. @abstract Subclasses must implement {#execute}

Constants

EVAL_ERROR

The error message returned when a variable or expression cannot be evaluated.

Public Class Methods

command() click to toggle source

The DAP command assocated with the receiver. @return [std:String]

# File lib/byebug/dap/command.rb, line 12
def self.command
  return @command_name if defined?(@command_name)

  raise "Not a command" if self == Byebug::DAP::Command
  raise "Not a command" unless self < Byebug::DAP::Command
  raise "Not a command" unless self.name.start_with?('Byebug::DAP::Command::')

  last = self.name.split('::').last
  @command_name = "#{last[0].downcase}#{last[1..]}"
end
execute(session, request, *args) click to toggle source

Resolve and execute the requested command. The command is {.resolve! resolved}, {#initialize initialized}, and {#safe_execute safely executed}. @param session [Session] the debug session @param request [Protocol::Request] the DAP request @param args [std:Array] additional arguments for {#initialize} @return the return value of {#safe_execute}

# File lib/byebug/dap/command.rb, line 46
def self.execute(session, request, *args)
  return unless command = resolve!(session, request)

  command.new(session, request, *args).safe_execute
end
new(session, request) click to toggle source

Create a new instance of the receiver. @param session [Session] the debug session @param request [Protocol::Request] the DAP request

# File lib/byebug/dap/command.rb, line 55
def initialize(session, request)
  @session = session
  @request = request
end
register!() click to toggle source

Register the receiver as a DAP command.

# File lib/byebug/dap/command.rb, line 24
def self.register!
  (@@commands ||= {})[command] = self
end
resolve!(session, request) click to toggle source

Resolve the requested command. Calls {Session#respond!} indicating a failed request if the command cannot be found. @param session [Session] the debug session @param request [Protocol::Request] the DAP request @return [std:Class] the {Command} class

# File lib/byebug/dap/command.rb, line 33
def self.resolve!(session, request)
  cls = @@commands[request.command]
  return cls if cls

  session.respond! request, success: false, message: 'Invalid command'
end

Public Instance Methods

log(*args) click to toggle source

(see Session#log)

# File lib/byebug/dap/command.rb, line 61
def log(*args)
  @session.log(*args)
end
safe_execute() click to toggle source

Call {#execute} safely, handling any errors that arise. @return the return value of {#execute}

# File lib/byebug/dap/command.rb, line 67
def safe_execute
  execute

rescue InvalidRequestArgumentError => e
  message =
    case e.error
    when String
      e.error

    when :missing_argument
      "Argument is unspecified: #{e.scope}"

    when :missing_entry
      "Cannot locate #{e.scope} ##{e.value}"

    when :invalid_entry
      "Error resolving #{e.scope}: #{e.value}"

    else
      log "#{e.message} (#{e.class})", *e.backtrace
      "An internal error occured"
    end

  respond! success: false, message: message

rescue IOError, Errno::EPIPE, Errno::ECONNRESET, Errno::ECONNABORTED
  :disconnected

rescue CommandProcessor::TimeoutError => e
  respond! success: false, message: "Debugger on thread ##{e.context.thnum} is not responding"

rescue StandardError => e
  respond! success: false, message: "An internal error occured"
  log "#{e.message} (#{e.class})", *e.backtrace
end

Private Instance Methods

args() click to toggle source
# File lib/byebug/dap/command.rb, line 135
def args
  @request.arguments
end
can_read_file!(path) click to toggle source
# File lib/byebug/dap/command.rb, line 220
def can_read_file!(path)
  path = File.realpath(path)
  return path if File.readable?(path)

  if File.exist?(path)
    respond! success: false, message: "Source file '#{path}' exists but cannot be read"
  else
    respond! success: false, message: "No source file available for '#{path}'"
  end

  return nil
end
convert_breakpoint_condition(condition) click to toggle source
# File lib/byebug/dap/command.rb, line 239
def convert_breakpoint_condition(condition)
  return nil if condition.nil? || condition.empty?
  return nil unless condition.is_a?(String)
  return condition
end
convert_breakpoint_hit_condition(condition) click to toggle source
# File lib/byebug/dap/command.rb, line 245
def convert_breakpoint_hit_condition(condition)
  # Because of https://github.com/deivid-rodriguez/byebug/issues/739,
  # Breakpoint#hit_condition can't be set to nil.
  return :ge, 0 if condition.nil? || condition.empty?
  return :ge, 0 unless condition.is_a?(String)

  m = /^(?<op><|<=|=|==|===|=>|>|%)?\s*(?<value>[0-9]+)$/.match(condition)
  raise InvalidRequestArgumentError.new("'#{condition}' is not a valid hit condition") unless m

  v = m[:value].to_i
  case m[:op]
  when nil, '=', '==', '==='
    return :eq, v

  when '>'
    return :ge, v - 1

  when '>='
    return :ge, v

  when '%'
    return :mod, v

  else
    raise InvalidRequestArgumentError.new("Byebug does not support hit conditions using '#{m[:op]}'") unless m
  end
end
event!(*args, **values) click to toggle source
# File lib/byebug/dap/command.rb, line 105
def event!(*args, **values)
  @session.event! *args, **values
  return
end
exception_description(ex) click to toggle source
# File lib/byebug/dap/command.rb, line 139
def exception_description(ex)
  safe(-> { "#{ex.message} (#{ex.class.name})" }, :call) { EVAL_ERROR }
end
execute_on_thread(thnum, block, &on_error) click to toggle source

Execute a code block on the specified thread, {SafeHelpers#safe safely}. @param thnum [std:Integer] the thread number @param block [std:Proc] the code block @yield called on error @yieldparam ex [std:Exception] the execution error @api private @!visibility public

# File lib/byebug/dap/command.rb, line 150
def execute_on_thread(thnum, block, &on_error)
  return safe(block, :call, &on_error) if thnum == 0 || @context&.thnum == thnum

  p = find_thread(thnum).processor
  safe(-> { p.execute(&block) }, :call, &on_error)
end
find_frame(ctx, frnum) click to toggle source
# File lib/byebug/dap/command.rb, line 166
def find_frame(ctx, frnum)
  raise InvalidRequestArgumentError.new(:missing_entry, value: frnum, scope: 'frame') unless frnum < ctx.stack_size

  ::Byebug::Frame.new(ctx, frnum)
end
find_or_add_breakpoint(verified, existing, source, pos) click to toggle source
# File lib/byebug/dap/command.rb, line 273
def find_or_add_breakpoint(verified, existing, source, pos)
  if bp = verified.find { |bp| bp.source == source && bp.pos == pos }
    return bp
  end

  if bp = existing.find { |bp| bp.source == source && bp.pos == pos }
    existing.delete(bp)
  else
    bp = Byebug::Breakpoint.add(source, pos.is_a?(String) ? pos.to_sym : pos)
  end

  verified << bp
  bp
end
find_thread(thnum) click to toggle source
# File lib/byebug/dap/command.rb, line 157
def find_thread(thnum)
  raise InvalidRequestArgumentError.new(:missing_argument, scope: 'thread ID') unless thnum

  ctx = Byebug.contexts.find { |c| c.thnum == thnum }
  raise InvalidRequestArgumentError.new(:missing_entry, value: thnum, scope: 'thread') unless ctx

  ctx
end
potential_breakpoint_lines(path) { |e| ... } click to toggle source
# File lib/byebug/dap/command.rb, line 233
def potential_breakpoint_lines(path)
  ::Byebug::Breakpoint.potential_lines(path)
rescue ScriptError, StandardError => e
  yield(e)
end
resolve_frame_id(id) click to toggle source
# File lib/byebug/dap/command.rb, line 172
def resolve_frame_id(id)
  raise InvalidRequestArgumentError.new(:missing_argument, scope: 'frame ID') unless id

  entry = @session.restore_frame(id)
  raise InvalidRequestArgumentError.new(:missing_entry, value: id, scope: 'frame ID') unless entry

  thnum, frnum = entry
  ctx = find_thread(thnum)
  frame = find_frame(ctx, frnum)
  return frame, thnum, frnum
end
resolve_variables_reference(ref) click to toggle source
# File lib/byebug/dap/command.rb, line 184
def resolve_variables_reference(ref)
  raise InvalidRequestArgumentError.new(:missing_argument, scope: 'variables reference') unless ref

  entry = @session.restore_variables(ref)
  raise InvalidRequestArgumentError.new(:missing_entry, value: ref, scope: 'variables reference') unless entry

  thnum, frnum, kind, *entry = entry

  case kind
  when :locals
    frame = find_frame(find_thread(thnum), frnum)
    named, indexed = entry[0], []
    get = ->(key) {
      return frame._self if key == :self
      return frame.context.processor.last_exception if key == :$!
      values ||= frame.locals
      values[key]
    }

  when :globals
    frame = find_frame(find_thread(thnum), frnum)
    named, indexed = entry[0], []
    get = ->(key) { frame._binding.eval(key.to_s) }

  when :variable, :evaluate
    value, named, indexed = entry
    get = ->(key) { value.instance_eval { binding }.eval(key.to_s) }
    index = ->(key) { value[key] }

  else
    raise InvalidRequestArgumentError.new(:invalid_entry, value: kind, scope: 'variable scope')
  end

  return thnum, frnum, named.map { |k| [k, get] }, indexed.map { |k| [k, index] }
end
respond!(*args, **values) click to toggle source
# File lib/byebug/dap/command.rb, line 110
def respond!(*args, **values)
  raise "Cannot respond without a request" unless @request

  @session.respond! @request, *args, **values
  return
end
started!() click to toggle source

Raises an error unless the debugger is running @api private @!visibility public

# File lib/byebug/dap/command.rb, line 129
def started!
  return if Byebug.started?

  respond! success: false, message: "Cannot #{@request.command} - debugger is not running"
end
stopped!() click to toggle source

Raises an error if the debugger is running @api private @!visibility public

# File lib/byebug/dap/command.rb, line 120
def stopped!
  return if !Byebug.started?

  respond! success: false, message: "Cannot #{@request.command} - debugger is already running"
end