module DEBUGGER__::UI_DAP

Constants

SHOW_PROTOCOL

Public Class Methods

setup(sock_path) click to toggle source
# File debug-1.4.0/lib/debug/server_dap.rb, line 13
    def self.setup sock_path
      dir = Dir.mktmpdir("ruby-debug-vscode-")
      at_exit{
        CONFIG[:skip_path] = [//] # skip all
        FileUtils.rm_rf dir
      }
      Dir.chdir(dir) do
        Dir.mkdir('.vscode')
        open('README.rb', 'w'){|f|
          f.puts <<~MSG
          # Wait for starting the attaching to the Ruby process
          # This file will be removed at the end of the debuggee process.
          #
          # Note that vscode-rdbg extension is needed. Please install if you don't have.
          MSG
        }
        open('.vscode/launch.json', 'w'){|f|
          f.puts JSON.pretty_generate({
            version: '0.2.0',
            configurations: [
            {
              type: "rdbg",
              name: "Attach with rdbg",
              request: "attach",
              rdbgPath: File.expand_path('../../exe/rdbg', __dir__),
              debugPort: sock_path,
              autoAttach: true,
            }
            ]
          })
        }
      end

      cmds = ['code', "#{dir}/", "#{dir}/README.rb"]
      cmdline = cmds.join(' ')
      ssh_cmdline = "code --remote ssh-remote+[SSH hostname] #{dir}/ #{dir}/README.rb"

      STDERR.puts "Launching: #{cmdline}"
      env = ENV.delete_if{|k, h| /RUBY/ =~ k}.to_h

      unless system(env, *cmds)
        DEBUGGER__.warn <<~MESSAGE
        Can not invoke the command.
        Use the command-line on your terminal (with modification if you need).

          #{cmdline}

        If your application is running on a SSH remote host, please try:

          #{ssh_cmdline}

        MESSAGE
      end
    end

Public Instance Methods

dap_setup(bytes) click to toggle source
# File debug-1.4.0/lib/debug/server_dap.rb, line 74
def dap_setup bytes
  CONFIG.set_config no_color: true
  @seq = 0

  show_protocol :>, bytes
  req = JSON.load(bytes)

  # capability
  send_response(req,
         ## Supported
         supportsConfigurationDoneRequest: true,
         supportsFunctionBreakpoints: true,
         supportsConditionalBreakpoints: true,
         supportTerminateDebuggee: true,
         supportsTerminateRequest: true,
         exceptionBreakpointFilters: [
           {
             filter: 'any',
             label: 'rescue any exception',
             #supportsCondition: true,
             #conditionDescription: '',
           },
           {
             filter: 'RuntimeError',
             label: 'rescue RuntimeError',
             default: true,
             #supportsCondition: true,
             #conditionDescription: '',
           },
         ],
         supportsExceptionFilterOptions: true,
         supportsStepBack: true,
         supportsEvaluateForHovers: true,
         supportsCompletionsRequest: true,

         ## Will be supported
         # supportsExceptionOptions: true,
         # supportsHitConditionalBreakpoints:
         # supportsSetVariable: true,
         # supportSuspendDebuggee:
         # supportsLogPoints:
         # supportsLoadedSourcesRequest:
         # supportsDataBreakpoints:
         # supportsBreakpointLocationsRequest:

         ## Possible?
         # supportsRestartFrame:
         # completionTriggerCharacters:
         # supportsModulesRequest:
         # additionalModuleColumns:
         # supportedChecksumAlgorithms:
         # supportsRestartRequest:
         # supportsValueFormattingOptions:
         # supportsExceptionInfoRequest:
         # supportsDelayedStackTraceLoading:
         # supportsTerminateThreadsRequest:
         # supportsSetExpression:
         # supportsClipboardContext:

         ## Never
         # supportsGotoTargetsRequest:
         # supportsStepInTargetsRequest:
         # supportsReadMemoryRequest:
         # supportsDisassembleRequest:
         # supportsCancelRequest:
         # supportsSteppingGranularity:
         # supportsInstructionBreakpoints:
  )
  send_event 'initialized'
end
event(type, *args) click to toggle source
# File debug-1.4.0/lib/debug/server_dap.rb, line 365
def event type, *args
  case type
  when :suspend_bp
    _i, bp, tid = *args
    if bp.kind_of?(CatchBreakpoint)
      reason = 'exception'
      text = bp.description
    else
      reason = 'breakpoint'
      text = bp ? bp.description : 'temporary bp'
    end

    send_event 'stopped', reason: reason,
                          description: text,
                          text: text,
                          threadId: tid,
                          allThreadsStopped: true
  when :suspend_trap
    _sig, tid = *args
    send_event 'stopped', reason: 'pause',
                          threadId: tid,
                          allThreadsStopped: true
  when :suspended
    tid, = *args
    send_event 'stopped', reason: 'step',
                          threadId: tid,
                          allThreadsStopped: true
  end
end
process() click to toggle source
# File debug-1.4.0/lib/debug/server_dap.rb, line 204
def process
  while req = recv_request
    raise "not a request: #{req.inpsect}" unless req['type'] == 'request'
    args = req.dig('arguments')

    case req['command']

    ## boot/configuration
    when 'launch'
      send_response req
      @is_attach = false
    when 'attach'
      send_response req
      Process.kill(:SIGURG, Process.pid)
      @is_attach = true
    when 'setBreakpoints'
      path = args.dig('source', 'path')
      bp_args = args['breakpoints']
      bps = []
      bp_args.each{|bp|
        line = bp['line']
        if cond = bp['condition']
          bps << SESSION.add_line_breakpoint(path, line, cond: cond)
        else
          bps << SESSION.add_line_breakpoint(path, line)
        end
      }
      send_response req, breakpoints: (bps.map do |bp| {verified: true,} end)
    when 'setFunctionBreakpoints'
      send_response req
    when 'setExceptionBreakpoints'
      process_filter = ->(filter_id) {
        case filter_id
        when 'any'
          bp = SESSION.add_catch_breakpoint 'Exception'
        when 'RuntimeError'
          bp = SESSION.add_catch_breakpoint 'RuntimeError'
        else
          bp = nil
        end
        {
          verified: bp ? true : false,
          message: bp.inspect,
        }
      }

      filters = args.fetch('filters').map {|filter_id|
        process_filter.call(filter_id)
      }

      filters += args.fetch('filterOptions', {}).map{|bp_info|
        process_filter.call(bp_info.dig('filterId'))
      }

      send_response req, breakpoints: filters
    when 'configurationDone'
      send_response req
      if defined?(@is_attach) && @is_attach
        @q_msg << 'p'
        send_event 'stopped', reason: 'pause',
                              threadId: 1,
                              allThreadsStopped: true
      else
        @q_msg << 'continue'
      end
    when 'disconnect'
      if args.fetch("terminateDebuggee", false)
        @q_msg << 'kill!'
      else
        @q_msg << 'continue'
      end
      send_response req

    ## control
    when 'continue'
      @q_msg << 'c'
      send_response req, allThreadsContinued: true
    when 'next'
      begin
        @session.check_postmortem
        @q_msg << 'n'
        send_response req
      rescue PostmortemError
        send_response req,
                      success: false, message: 'postmortem mode',
                      result: "'Next' is not supported while postmortem mode"
      end
    when 'stepIn'
      begin
        @session.check_postmortem
        @q_msg << 's'
        send_response req
      rescue PostmortemError
        send_response req,
                      success: false, message: 'postmortem mode',
                      result: "'stepIn' is not supported while postmortem mode"
      end
    when 'stepOut'
      begin
        @session.check_postmortem
        @q_msg << 'fin'
        send_response req
      rescue PostmortemError
        send_response req,
                      success: false, message: 'postmortem mode',
                      result: "'stepOut' is not supported while postmortem mode"
      end
    when 'terminate'
      send_response req
      exit
    when 'pause'
      send_response req
      Process.kill(:SIGURG, Process.pid)
    when 'reverseContinue'
      send_response req,
                    success: false, message: 'cancelled',
                    result: "Reverse Continue is not supported. Only \"Step back\" is supported."
    when 'stepBack'
      @q_msg << req

    ## query
    when 'threads'
      send_response req, threads: SESSION.managed_thread_clients.map{|tc|
        { id: tc.id,
          name: tc.name,
        }
      }

    when 'stackTrace',
         'scopes',
         'variables',
         'evaluate',
         'source',
         'completions'
      @q_msg << req

    else
      raise "Unknown request: #{req.inspect}"
    end
  end
end
puts(result) click to toggle source
# File debug-1.4.0/lib/debug/server_dap.rb, line 360
def puts result
  # STDERR.puts "puts: #{result}"
  # send_event 'output', category: 'stderr', output: "PUTS!!: " + result.to_s
end
readline(prompt) click to toggle source

called by the SESSION thread

# File debug-1.4.0/lib/debug/server_dap.rb, line 348
def readline prompt
  @q_msg.pop || 'kill!'
end
recv_request() click to toggle source
# File debug-1.4.0/lib/debug/server_dap.rb, line 180
def recv_request
  r = IO.select([@sock])

  @session.process_group.sync do
    raise RetryBecauseCantRead unless IO.select([@sock], nil, nil, 0)

    case header = @sock.gets
    when /Content-Length: (\d+)/
      b = @sock.read(2)
      raise b.inspect unless b == "\r\n"

      l = @sock.read(s = $1.to_i)
      show_protocol :>, l
      JSON.load(l)
    when nil
      nil
    else
      raise "unrecognized line: #{l} (#{l.size} bytes)"
    end
  end
rescue RetryBecauseCantRead
  retry
end
respond(req, res) click to toggle source
# File debug-1.4.0/lib/debug/server_dap.rb, line 356
def respond req, res
  send_response(req, **res)
end
send(**kw) click to toggle source
# File debug-1.4.0/lib/debug/server_dap.rb, line 145
def send **kw
  kw[:seq] = @seq += 1
  str = JSON.dump(kw)
  show_protocol '<', str
  @sock.write "Content-Length: #{str.bytesize}\r\n\r\n#{str}"
end
send_event(name, **kw) click to toggle source
# File debug-1.4.0/lib/debug/server_dap.rb, line 169
def send_event name, **kw
  if kw.empty?
    send type: 'event', event: name
  else
    send type: 'event', event: name, body: kw
  end
end
send_response(req, success: true, message: nil, **kw) click to toggle source
# File debug-1.4.0/lib/debug/server_dap.rb, line 152
def send_response req, success: true, message: nil, **kw
  if kw.empty?
    send type: 'response',
         command: req['command'],
         request_seq: req['seq'],
         success: success,
         message: message || (success ? 'Success' : 'Failed')
  else
    send type: 'response',
         command: req['command'],
         request_seq: req['seq'],
         success: success,
         message: message || (success ? 'Success' : 'Failed'),
         body: kw
  end
end
show_protocol(dir, msg) click to toggle source
# File debug-1.4.0/lib/debug/server_dap.rb, line 68
def show_protocol dir, msg
  if SHOW_PROTOCOL
    $stderr.puts "\##{Process.pid}:[#{dir}] #{msg}"
  end
end
sock(skip: false) { |$stderr| ... } click to toggle source
# File debug-1.4.0/lib/debug/server_dap.rb, line 352
def sock skip: false
  yield $stderr
end