module DEBUGGER__::UI_DAP

Constants

SHOW_PROTOCOL

Public Class Methods

local_fs_map_set(map) click to toggle source
# File debug-1.7.1/lib/debug/server_dap.rb, line 112
def self.local_fs_map_set map
  return if @local_fs_map # already setup

  case map
  when String
    @local_fs_map = map.split(',').map{|e| e.split(':').map{|path| path.delete_suffix('/') + '/'}}
  when true
    @local_fs_map = map
  when nil
    @local_fs_map = CONFIG[:local_fs_map]
  end
end
local_to_remote_path(path) click to toggle source
# File debug-1.7.1/lib/debug/server_dap.rb, line 95
def self.local_to_remote_path path
  case @local_fs_map
  when nil
    nil
  when true
    path
  else # Array
    @local_fs_map.each do |(remote_path_prefix, local_path_prefix)|
      if path.start_with? local_path_prefix
        return path.sub(local_path_prefix){ remote_path_prefix }
      end
    end

    nil
  end
end
remote_to_local_path(path) click to toggle source
# File debug-1.7.1/lib/debug/server_dap.rb, line 78
def self.remote_to_local_path path
  case @local_fs_map
  when nil
    nil
  when true
    path
  else # Array
    @local_fs_map.each do |(remote_path_prefix, local_path_prefix)|
      if path.start_with? remote_path_prefix
        return path.sub(remote_path_prefix){ local_path_prefix }
      end
    end

    nil
  end
end
setup(debug_port) click to toggle source
# File debug-1.7.1/lib/debug/server_dap.rb, line 12
    def self.setup debug_port
      if File.directory? '.vscode'
        dir = Dir.pwd
      else
        dir = Dir.mktmpdir("ruby-debug-vscode-")
        tempdir = true
      end

      at_exit do
        DEBUGGER__.skip_all
        FileUtils.rm_rf dir if tempdir
      end

      key = rand.to_s

      Dir.chdir(dir) do
        Dir.mkdir('.vscode') if tempdir

        # vscode-rdbg 0.0.9 or later is needed
        open('.vscode/rdbg_autoattach.json', 'w') do |f|
          f.puts JSON.pretty_generate({
            type: "rdbg",
            name: "Attach with rdbg",
            request: "attach",
            rdbgPath: File.expand_path('../../exe/rdbg', __dir__),
            debugPort: debug_port,
            localfs: true,
            autoAttach: key,
          })
        end
      end

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

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

      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.7.1/lib/debug/server_dap.rb, line 125
    def dap_setup bytes
      CONFIG.set_config no_color: true
      @seq = 0

      case self
      when UI_UnixDomainServer
        # If the user specified a mapping, respect it, otherwise, make sure that no mapping is used
        UI_DAP.local_fs_map_set CONFIG[:local_fs_map] || true
      when UI_TcpServer
        # TODO: loopback address can be used to connect other FS env, like Docker containers
        # UI_DAP.local_fs_set if @local_addr.ipv4_loopback? || @local_addr.ipv6_loopback?
      end

      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',
                 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'
      puts <<~WELCOME
        Ruby REPL: You can run any Ruby expression here.
        Note that output to the STDOUT/ERR printed on the TERMINAL.
        [experimental]
          `,COMMAND` runs `COMMAND` debug command (ex: `,info`).
          `,help` to list all debug commands.
      WELCOME
    end
event(type, *args) click to toggle source
# File debug-1.7.1/lib/debug/server_dap.rb, line 481
def event type, *args
  case type
  when :load
    file_path, reloaded = *args

    if file_path
      send_event 'loadedSource',
                 reason: (reloaded ? :changed : :new),
                 source: {
                   path: file_path,
                 }
    end
  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
ignore_output_on_suspend?() click to toggle source
# File debug-1.7.1/lib/debug/server_dap.rb, line 477
def ignore_output_on_suspend?
  true
end
process() click to toggle source
# File debug-1.7.1/lib/debug/server_dap.rb, line 272
def process
  while req = recv_request
    raise "not a request: #{req.inspect}" unless req['type'] == 'request'
    args = req.dig('arguments')

    case req['command']

    ## boot/configuration
    when 'launch'
      send_response req
      # `launch` runs on debuggee on the same file system
      UI_DAP.local_fs_map_set req.dig('arguments', 'localfs') || req.dig('arguments', 'localfsMap') || true
      @nonstop = true

    when 'attach'
      send_response req
      UI_DAP.local_fs_map_set req.dig('arguments', 'localfs') || req.dig('arguments', 'localfsMap')

      if req.dig('arguments', 'nonstop') == true
        @nonstop = true
      else
        @nonstop = false
      end

    when 'configurationDone'
      send_response req

      if @nonstop
        @q_msg << 'continue'
      else
        if SESSION.in_subsession?
          send_event 'stopped', reason: 'pause',
                                threadId: 1, # maybe ...
                                allThreadsStopped: true
        end
      end

    when 'setBreakpoints'
      req_path = args.dig('source', 'path')
      path = UI_DAP.local_to_remote_path(req_path)

      if path
        SESSION.clear_line_breakpoints path

        bps = []
        args['breakpoints'].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)
      else
        send_response req, success: false, message: "#{req_path} is not available"
      end

    when 'setFunctionBreakpoints'
      send_response req

    when 'setExceptionBreakpoints'
      process_filter = ->(filter_id, cond = nil) {
        bp =
          case filter_id
          when 'any'
            SESSION.add_catch_breakpoint 'Exception', cond: cond
          when 'RuntimeError'
            SESSION.add_catch_breakpoint 'RuntimeError', cond: cond
          else
            nil
          end
          {
            verified: !bp.nil?,
            message: bp.inspect,
          }
        }

        SESSION.clear_catch_breakpoints 'Exception', 'RuntimeError'

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

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

      send_response req, breakpoints: filters

    when 'disconnect'
      terminate = args.fetch("terminateDebuggee", false)

      SESSION.clear_all_breakpoints
      send_response req

      if SESSION.in_subsession?
        if terminate
          @q_msg << 'kill!'
        else
          @q_msg << 'continue'
        end
      else
        if terminate
          @q_msg << 'kill!'
          pause
        end
      end

    ## 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(UI_ServerBase::TRAP_SIGNAL, 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 'evaluate'
      expr = req.dig('arguments', 'expression')
      if /\A\s*,(.+)\z/ =~ expr
        dbg_expr = $1
        send_response req,
                      result: "",
                      variablesReference: 0
        debugger do: dbg_expr
      else
        @q_msg << req
      end
    when 'stackTrace',
         'scopes',
         'variables',
         'source',
         'completions'
      @q_msg << req

    else
      if respond_to? mid = "request_#{req['command']}"
        send mid, req
      else
        raise "Unknown request: #{req.inspect}"
      end
    end
  end
ensure
  send_event :terminated unless @sock.closed?
end
puts(result) click to toggle source
# File debug-1.7.1/lib/debug/server_dap.rb, line 472
def puts result
  # STDERR.puts "puts: #{result}"
  send_event 'output', category: 'console', output: "#{result&.chomp}\n"
end
recv_request() click to toggle source
# File debug-1.7.1/lib/debug/server_dap.rb, line 248
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

called by the SESSION thread

# File debug-1.7.1/lib/debug/server_dap.rb, line 468
def respond req, res
  send_response(req, **res)
end
send(**kw) click to toggle source
# File debug-1.7.1/lib/debug/server_dap.rb, line 211
def send **kw
  if sock = @sock
    kw[:seq] = @seq += 1
    str = JSON.dump(kw)
    sock.write "Content-Length: #{str.bytesize}\r\n\r\n#{str}"
    show_protocol '<', str
  end
end
send_event(name, **kw) click to toggle source
# File debug-1.7.1/lib/debug/server_dap.rb, line 237
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.7.1/lib/debug/server_dap.rb, line 220
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.7.1/lib/debug/server_dap.rb, line 67
def show_protocol dir, msg
  if SHOW_PROTOCOL
    $stderr.puts "\##{Process.pid}:[#{dir}] #{msg}"
  end
end