123456789_123456789_123456789_123456789_123456789_

Module: DEBUGGER__::UI_DAP

Relationships & Source Files
Namespace Children
Exceptions:
Defined in: lib/debug/server_dap.rb

Constant Summary

Class Method Summary

Instance Attribute Summary

Instance Method Summary

Class Method Details

.local_fs_map_set(map)

[ GitHub ]

  
# File '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)

[ GitHub ]

  
# File '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)

[ GitHub ]

  
# File '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)

[ GitHub ]

  
# File '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

Instance Attribute Details

#ignore_output_on_suspend?Boolean (readonly)

[ GitHub ]

  
# File 'lib/debug/server_dap.rb', line 509

def ignore_output_on_suspend?
  true
end

Instance Method Details

#dap_setup(bytes)

[ GitHub ]

  
# File 'lib/debug/server_dap.rb', line 125

def dap_setup bytes
  CONFIG.set_config no_color: true
  @seq = 0
  @send_lock = Mutex.new

  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)

[ GitHub ]

  
# File 'lib/debug/server_dap.rb', line 513

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

#load_extensions(req)

[ GitHub ]

  
# File 'lib/debug/server_dap.rb', line 277

def load_extensions req
  if exts = req.dig('arguments', 'rdbgExtensions')
    exts.each{|ext|
      require_relative "dap_custom/#{File.basename(ext)}"
    }
  end

  if scripts = req.dig('arguments', 'rdbgInitialScripts')
    scripts.each do |script|
      begin
        eval(script)
      rescue Exception => e
        puts e.message
        puts e.backtrace.inspect
      end
    end
  end
end

#process

[ GitHub ]

  
# File 'lib/debug/server_dap.rb', line 296

def process
  while req = recv_request
    process_request(req)
  end
ensure
  send_event :terminated unless @sock.closed?
end

#process_request(req)

[ GitHub ]

  
# File 'lib/debug/server_dap.rb', line 304

def process_request req
  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

    load_extensions req

  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

    load_extensions req

  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.strip
      dbg_expr.split(';;') { |cmd| @q_msg << cmd }

      send_response req,
                    result: "(rdbg:command) #{dbg_expr}",
                    variablesReference: 0
    else
      @q_msg << req
    end
  when 'stackTrace',
       'scopes',
       'variables',
       'source',
       'completions'
    @q_msg << req

  else
    if respond_to? mid = "custom_dap_request_#{req['command']}"
      __send__ mid, req
    else
      raise "Unknown request: #{req.inspect}"
    end
  end
end

#puts(result)

[ GitHub ]

  
# File 'lib/debug/server_dap.rb', line 504

def puts result
  # STDERR.puts "puts: #{result}"
  send_event 'output', category: 'console', output: "#{result&.chomp}\n"
end

#recv_request

[ GitHub ]

  
# File 'lib/debug/server_dap.rb', line 253

def recv_request
  IO.select([@sock])

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

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

      l = @sock.read($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)

called by the SESSION thread

[ GitHub ]

  
# File 'lib/debug/server_dap.rb', line 500

def respond req, res
  send_response(req, **res)
end

#send(**kw)

[ GitHub ]

  
# File 'lib/debug/server_dap.rb', line 212

def send **kw
  if sock = @sock
    kw[:seq] = @seq += 1
    str = JSON.dump(kw)
    @send_lock.synchronize do
      sock.write "Content-Length: #{str.bytesize}\r\n\r\n#{str}"
    end
    show_protocol '<', str
  end
rescue Errno::EPIPE => e
  $stderr.puts "#{e.inspect} rescued during sending message"
end

#send_event(name, **kw)

[ GitHub ]

  
# File 'lib/debug/server_dap.rb', line 242

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)

[ GitHub ]

  
# File 'lib/debug/server_dap.rb', line 225

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)

[ GitHub ]

  
# File 'lib/debug/server_dap.rb', line 67

def show_protocol dir, msg
  if SHOW_PROTOCOL
    $stderr.puts "\##{Process.pid}:[#{dir}] #{msg}"
  end
end