123456789_123456789_123456789_123456789_123456789_

Class: DEBUGGER__::UI_ServerBase

Relationships & Source Files
Namespace Children
Exceptions:
Extension / Inclusion / Inheritance Descendants
Subclasses:
Super Chains via Extension / Inclusion / Inheritance
Class Chain:
self, UI_Base
Instance Chain:
self, UI_Base
Inherits: DEBUGGER__::UI_Base
Defined in: lib/debug/server.rb

Class Method Summary

Instance Attribute Summary

Instance Method Summary

Constructor Details

.newUI_ServerBase

[ GitHub ]

  
# File 'lib/debug/server.rb', line 10

def initialize
  @sock = @sock_for_fork = nil
  @accept_m = Mutex.new
  @accept_cv = ConditionVariable.new
  @client_addr = nil
  @q_msg = nil
  @q_ans = nil
  @unsent_messages = []
  @width = 80
  @repl = true
  @session = nil
end

Instance Attribute Details

#reader_thread (readonly)

[ GitHub ]

  
# File 'lib/debug/server.rb', line 274

attr_reader :reader_thread

#remote?Boolean (readonly)

[ GitHub ]

  
# File 'lib/debug/server.rb', line 221

def remote?
  true
end

Instance Method Details

#accept

[ GitHub ]

  
# File 'lib/debug/server.rb', line 31

def accept
  if @sock_for_fork
    begin
      yield @sock_for_fork, already_connected: true
    ensure
      @sock_for_fork.close
      @sock_for_fork = nil
    end
  end
end

#activate(session, on_fork: false)

[ GitHub ]

  
# File 'lib/debug/server.rb', line 42

def activate session, on_fork: false
  @session = session
  @reader_thread = Thread.new do
    # An error on this thread should break the system.
    Thread.current.abort_on_exception = true
    Thread.current.name = 'DEBUGGER__::Server::reader'

    accept do |server, already_connected: false|
      DEBUGGER__.warn "Connected."
      greeting_done = false
      @need_pause_at_first = true

      @accept_m.synchronize{
        @sock = server
        greeting
        greeting_done = true

        @accept_cv.signal

        # flush unsent messages
        @unsent_messages.each{|m|
          @sock.puts m
        } if @repl
        @unsent_messages.clear

        @q_msg = Queue.new
        @q_ans = Queue.new
      } unless already_connected

      setup_interrupt do
        pause if !already_connected && @need_pause_at_first
        process
      end

    rescue GreetingError => e
      DEBUGGER__.warn "GreetingError: #{e.message}"
      next
    rescue Terminate
      raise # should catch at outer scope
    rescue => e
      DEBUGGER__.warn "ReaderThreadError: #{e}"
      pp e.backtrace
    ensure
      DEBUGGER__.warn "Disconnected."
      cleanup_reader if greeting_done
    end # accept

  rescue Terminate
    # ignore
  end
end

#after_fork_parent

[ GitHub ]

  
# File 'lib/debug/server.rb', line 371

def after_fork_parent
  # do nothing
end

#ask(prompt)

[ GitHub ]

  
# File 'lib/debug/server.rb', line 307

def ask prompt
  sock do |s|
    s.puts "ask #{Process.pid} #{prompt}"
    @q_ans.pop
  end
end

#cleanup_reader

[ GitHub ]

  
# File 'lib/debug/server.rb', line 94

def cleanup_reader
  @sock.close if @sock
  @sock = nil
  @q_msg.close
  @q_msg = nil
  @q_ans.close
  @q_ans = nil
end

#deactivate

[ GitHub ]

  
# File 'lib/debug/server.rb', line 26

def deactivate
  @reader_thread.raise Terminate
  @reader_thread.join
end

#greeting

[ GitHub ]

  
# File 'lib/debug/server.rb', line 128

def greeting
  case g = @sock.gets
  when /^info cookie:\s+(.*)$/
    check_cookie $1
    @sock.puts "PID: #{Process.pid}, $0: #{$0}"
    @sock.puts "debug #{VERSION} on #{RUBY_DESCRIPTION}"
    @sock.puts "uname: #{Etc.uname.inspect}"
    @sock.close
    raise GreetingError, "HEAD request"

  when /^version:\s(\S)\s(.)$/
    v, params = $1, $2

    # TODO: protocol version
    if v != VERSION
      raise GreetingError, "Incompatible version (server:#{VERSION} and client:#{$1})"
    end
    parse_option(params)

    puts "DEBUGGER (client): Connected. PID:#{Process.pid}, $0:#{$0}"
    puts "DEBUGGER (client): Type `Ctrl-C` to enter the debug console." unless @need_pause_at_first
    puts

  when /^Content-Length: (\d+)/
    require_relative 'server_dap'

    raise unless @sock.read(2) == "\r\n"
    self.extend(UI_DAP)
    @repl = false
    @need_pause_at_first = false
    dap_setup @sock.read($1.to_i)

  when /^GET \/.* HTTP\/1.1/
    require_relative 'server_cdp'

    self.extend(UI_CDP)
    @repl = false
    @need_pause_at_first = false
    CONFIG.set_config no_color: true

    @ws_server = UI_CDP::WebSocketServer.new(@sock)
    @ws_server.handshake
  else
    raise GreetingError, "Unknown greeting message: #{g}"
  end
end

#parse_option(params)

[ GitHub ]

  
# File 'lib/debug/server.rb', line 110

def parse_option params
  case params.strip
  when /width:\s(\d)/
    @width = $1.to_i
    parse_option $~.post_match
  when /cookie:\s(\S)/
    check_cookie $1 if $1 != '-'
    parse_option $~.post_match
  when /nonstop: (true|false)/
    @need_pause_at_first = false if $1 == 'true'
    parse_option $~.post_match
  when /(.):(.)/
    raise GreetingError, "Unkown option: #{params}"
  else
    # OK
  end
end

#pause

[ GitHub ]

  
# File 'lib/debug/server.rb', line 359

def pause
  # $stderr.puts "DEBUG: pause request"
  Process.kill(TRAP_SIGNAL, Process.pid)
end

#process

[ GitHub ]

  
# File 'lib/debug/server.rb', line 175

def process
  while true
    DEBUGGER__.info "sleep IO.select"
    r = IO.select([@sock])
    DEBUGGER__.info "wakeup IO.select"

    line = @session.process_group.sync do
      unless IO.select([@sock], nil, nil, 0)
        DEBUGGER__.info "UI_Server can not read"
        break :can_not_read
      end
      @sock.gets&.chomp.tap{|line|
        DEBUGGER__.info "UI_Server received: #{line}"
      }
    end

    return unless line
    next if line == :can_not_read

    case line
    when /\Apause/
      pause
    when /\Acommand (\d+) (\d+) ?(.+)/
      raise "not in subsession, but received: #{line.inspect}" unless @session.in_subsession?

      if $1.to_i == Process.pid
        @width = $2.to_i
        @q_msg << $3
      else
        raise "pid:#{Process.pid} but get #{line}"
      end
    when /\Aanswer (\d+) (.*)/
      raise "not in subsession, but received: #{line.inspect}" unless @session.in_subsession?

      if $1.to_i == Process.pid
        @q_ans << $2
      else
        raise "pid:#{Process.pid} but get #{line}"
      end
    else
      STDERR.puts "unsupported: #{line.inspect}"
      exit!
    end
  end
end

#puts(str = nil)

[ GitHub ]

  
# File 'lib/debug/server.rb', line 314

def puts str = nil
  case str
  when Array
    enum = str.each
  when String
    enum = str.each_line
  when nil
    enum = [''].each
  end

  sock skip: true do |s|
    enum.each do |line|
      msg = "out #{line.chomp}"
      if s
        s.puts msg
      else
        @unsent_messages << msg
      end
    end
  end
end

#quit(n)

[ GitHub ]

  
# File 'lib/debug/server.rb', line 364

def quit n
  # ignore n
  sock do |s|
    s.puts "quit"
  end
end

#readline(prompt)

[ GitHub ]

  
# File 'lib/debug/server.rb', line 336

def readline prompt
  input = (sock(skip: CONFIG[:skip_bp]) do |s|
    next unless s

    if @repl
      raise "not in subsession, but received: #{line.inspect}" unless @session.in_subsession?
      line = "input #{Process.pid}"
      DEBUGGER__.info "send: #{line}"
      s.puts line
    end
    sleep 0.01 until @q_msg
    @q_msg.pop.tap{|msg|
      DEBUGGER__.info "readline: #{msg.inspect}"
    }
  end || 'continue')

  if input.is_a?(String)
    input.strip
  else
    input
  end
end

#setup_interrupt

[ GitHub ]

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

def setup_interrupt
  prev_handler = trap(TRAP_SIGNAL) do
    # $stderr.puts "trapped SIGINT"
    ThreadClient.current.on_trap TRAP_SIGNAL

    case prev_handler
    when Proc
      prev_handler.call
    else
      # ignore
    end
  end

  if sigurg_overridden?(prev_handler)
    DEBUGGER__.warn "SIGURG handler is overridden by the debugger."
  end
  yield
ensure
  trap(TRAP_SIGNAL, prev_handler)
end

#sigurg_overridden?(prev_handler) ⇒ Boolean

[ GitHub ]

  
# File 'lib/debug/server.rb', line 229

def sigurg_overridden? prev_handler
  case prev_handler
  when "SYSTEM_DEFAULT", "DEFAULT"
    false
  when Proc
    if prev_handler.source_location[0] == __FILE__
      false
    else
      true
    end
  else
    true
  end
end

#sock(skip: false)

[ GitHub ]

  
# File 'lib/debug/server.rb', line 278

def sock skip: false
  if s = @sock         # already connection
    # ok
  elsif skip == true   # skip process
    no_sock = true
    r = @accept_m.synchronize do
      if @sock
        no_sock = false
      else
        yield nil
      end
    end
    return r if no_sock
  else                 # wait for connection
    until s = @sock
      @accept_m.synchronize{
        unless @sock
          DEBUGGER__.warn "wait for debugger connection..."
          @accept_cv.wait(@accept_m)
        end
      }
    end
  end

  yield s
rescue Errno::EPIPE
  # ignore
end

#vscode_setup(debug_port)

[ GitHub ]

  
# File 'lib/debug/server.rb', line 375

def vscode_setup debug_port
  require_relative 'server_dap'
  UI_DAP.setup debug_port
end

#width

[ GitHub ]

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

def width
  @width
end