Module: DEBUGGER__::UI_DAP
Relationships & Source Files | |
Namespace Children | |
Exceptions:
| |
Defined in: | lib/debug/server_dap.rb |
Constant Summary
-
SHOW_PROTOCOL =
# File 'lib/debug/server_dap.rb', line 10ENV['DEBUG_DAP_SHOW_PROTOCOL'] == '1' || ENV['RUBY_DEBUG_DAP_SHOW_PROTOCOL'] == '1'
Class Method Summary
Instance Attribute Summary
- #ignore_output_on_suspend? ⇒ Boolean readonly
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. ('../../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 477
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 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 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
#process
[ GitHub ]# File '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)
[ GitHub ]# File '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
[ GitHub ]# File '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)
called by the SESSION thread
# File 'lib/debug/server_dap.rb', line 468
def respond req, res send_response(req, **res) end
#send(**kw)
[ GitHub ]# File '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)
[ GitHub ]#send_response(req, success: true, message: nil, **kw)
[ GitHub ]# File '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: || (success ? 'Success' : 'Failed') else send type: 'response', command: req['command'], request_seq: req['seq'], success: success, 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