module DEBUGGER__::UI_DAP - debug: Ruby Standard Library Documentation

# module DEBUGGER__::UI_DAP

### Constants

SHOW_PROTOCOL

### Public Class Methods

local_fs_map_set(map) click to toggle source

    # File debug-1.10.0/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.10.0/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.10.0/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.10.0/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.10.0/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) click to toggle source

    # File debug-1.10.0/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

ignore_output_on_suspend?() click to toggle source

    # File debug-1.10.0/lib/debug/server_dap.rb, line 509
    def ignore_output_on_suspend?
      true
    end

load_extensions(req) click to toggle source

    # File debug-1.10.0/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() click to toggle source

    # File debug-1.10.0/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) click to toggle source

    # File debug-1.10.0/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 = "") click to toggle source

    # File debug-1.10.0/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() click to toggle source

    # File debug-1.10.0/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) click to toggle source

called by the SESSION thread

    # File debug-1.10.0/lib/debug/server_dap.rb, line 500
    def respond req, res
      send_response(req, **res)
    end

send(**kw) click to toggle source

    # File debug-1.10.0/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) click to toggle source

    # File debug-1.10.0/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) click to toggle source

    # File debug-1.10.0/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) click to toggle source

    # File debug-1.10.0/lib/debug/server_dap.rb, line 67
    def show_protocol dir, msg
      if SHOW_PROTOCOL
        $stderr.puts "\##{Process.pid}:[#{dir}] #{msg}"
      end
    end