require 'byebug' require 'thread' class CommandServer < Messaging::Server def initialize super @debug_thread_group = nil @access = Mutex.new @waiting = false @mutex = Mutex.new @resource = ConditionVariable.new @command_queue = [] end def start @debug_thread_group = group = ThreadGroup.new thread = Byebug::DebugThread.new do listen('0.0.0.0', 4444) end group.add(thread) group.enclose end remote! def threads @access.synchronize { # TODO: Better sync while !@waiting end threads = [] Thread.list.each do |t| next if ignored? t backtrace = [] context = Byebug.thread_context(t) stack_size = context.calced_stack_size (0...stack_size).each do |i| path = File.expand_path(context.frame_file(i)) line = context.frame_line(i) klass = context.frame_class(i).to_s klass = nil if klass.empty? method = context.frame_method(i) backtrace << { path: path, line: line, class: klass, method: method, } end threads << { id: context.thnum, main: t == Thread.main, status: t.status, alive: t.alive?, priority: t.priority, safe_level: t.safe_level, backtrace: backtrace } end threads } end remote! def process @access.synchronize { { id: Process.pid, argv: ARGV, script: $0, env: ENV.to_h, config: RbConfig::CONFIG } } end remote! def locals @access.synchronize { while !@waiting end vars = {} context = Byebug.thread_context(Thread.main) syms = context.frame_binding.eval('local_variables') syms.each do |sym| name = sym.to_s vars[name] = context.frame_binding.eval(name).inspect end vars } end remote! def eval(expr: nil, frame: nil) @access.synchronize { while !@waiting end context = Byebug.thread_context(Thread.main) begin binding = context.frame_binding(frame) value = Inspector.inspect(binding.eval(expr)) return { success: true, value: value } rescue Exception => e return { success: false, class: e.class.name, message: e.message } end } end remote! def breakpoints @access.synchronize { Byebug.breakpoints.map do |bp| { id: bp.id, line: bp.pos, path: bp.source } end } end remote! def add_breakpoint(file: nil, line: nil) @access.synchronize { breakpoint = Byebug::Breakpoint.add(file, line) broadcast.breakpoint_created breakpoint.id } end remote! def remove_breakpoint(id: nil) @access.synchronize { breakpoint = Byebug::Breakpoint.remove(id) broadcast.breakpoint_deleted true } end remote! def pause @access.synchronize { context = Byebug.thread_context(Thread.main) context.interrupt false } end remote! def resume @access.synchronize { @command_queue << proc { |context| true } @mutex.synchronize { @resource.signal } true } end remote! def step_in @access.synchronize { @command_queue << proc { |context| context.step_into(1) true } @mutex.synchronize { @resource.signal } true } end remote! def step_over @access.synchronize { @command_queue << proc { |context| context.step_over(1) true } @mutex.synchronize { @resource.signal } true } end remote! def step_out @access.synchronize { @command_queue << proc { |context| context.step_out(1) true } @mutex.synchronize { @resource.signal } true } end def next_command if @command_queue.empty? @mutex.synchronize { @waiting = true # TODO: Fix this synchronization. Using a condition variable here # triggers Ruby's deadlock detection, so for now, we use a limited # sleep with repeated checking. while @command_queue.empty? sleep 0.1 end @waiting = false } end return @command_queue.shift end remote! def running? @access.synchronize { @running } end def running=(running) @running = running end private def ignored?(thread) thread.group == @debug_thread_group end end class RemoteCommandProcessor < Byebug::Processor def initialize(interface = Byebug::LocalInterface.new) super(interface) @server = CommandServer.new @server.start end def at_breakpoint(context, breakpoint) end def at_catchpoint(context, excpt) end def at_tracing(context, file, line) end def at_line(context, file, line) @server.broadcast.break process_commands(context, file, line) end def at_return(context, file, line) process_commands(context, file, line) end def process_commands(context, file, line) @server.running = false loop do command = @server.next_command break if command.call(context) end @server.running = true end end