require 'stringio'
require 'cgi'
require 'monitor'
module Debugger
module OverflowMessageType
NIL_MESSAGE = lambda {|e| nil}
EXCEPTION_MESSAGE = lambda {|e| e.message}
SPECIAL_SYMBOL_MESSAGE = lambda {|e| '>'}
end
class ExecError
attr_reader :message
attr_reader :backtrace
def initialize(message, backtrace = [])
@message = message
@backtrace = backtrace
end
end
class SimpleTimeLimitError < StandardError
attr_reader :message
def initialize(message)
@message = message
end
end
class MemoryLimitError < ExecError;
end
class TimeLimitError < ExecError;
end
class XmlPrinter # :nodoc:
class ExceptionProxy
instance_methods.each {|m| undef_method m unless m =~ /(^__|^send$|^object_id$|^instance_variables$|^instance_eval$)/}
def initialize(exception)
@exception = exception
@message = exception.message
@backtrace = Debugger.cleanup_backtrace(exception.backtrace)
end
private
def method_missing(called, *args, &block)
@exception.__send__(called, *args, &block)
end
end
def self.protect(mname)
return if instance_methods.include?("__#{mname}")
alias_method "__#{mname}", mname
class_eval %{
def #{mname}(*args, &block)
@@monitor.synchronize do
return unless @interface
__#{mname}(*args, &block)
end
end
}
end
@@monitor = Monitor.new
attr_accessor :interface
def initialize(interface)
@interface = interface
end
def print_msg(*args)
msg, *args = args
xml_message = CGI.escapeHTML(msg % args)
print "#{xml_message}"
end
# Sends debug message to the frontend if XML debug logging flag (--xml-debug) is on.
def print_debug(*args)
Debugger.print_debug(*args)
if Debugger.xml_debug
msg, *args = args
xml_message = CGI.escapeHTML(msg % args)
@interface.print("#{xml_message}")
end
end
def print_error(*args)
print_element("error") do
msg, *args = args
print CGI.escapeHTML(msg % args)
end
end
def print_frames(context, current_frame_id)
print_element("frames") do
(0...context.stack_size).each do |id|
print_frame(context, id, current_frame_id)
end
end
end
def print_current_frame(frame_pos)
print_debug "Selected frame no #{frame_pos}"
end
def print_frame(context, frame_id, current_frame_id)
# idx + 1: one-based numbering as classic-debugger
file = context.frame_file(frame_id)
print "",
frame_id + 1, CGI.escapeHTML(File.expand_path(file)), context.frame_line(frame_id)
end
def print_contexts(contexts)
print_element("threads") do
contexts.each do |c|
print_context(c) unless c.ignored?
end
end
end
def print_context(context)
print "", context.thnum, context.thread.status, Process.pid
end
def print_variables(vars, kind)
print_element("variables") do
# print self at top position
print_variable('self', yield('self'), kind) if vars.include?('self')
vars.sort.each do |v|
print_variable(v, yield(v), kind) unless v == 'self'
end
end
end
def print_array(array)
print_element("variables") do
index = 0
array.each {|e|
print_variable('[' + index.to_s + ']', e, 'instance')
index += 1
}
end
end
def print_hash(hash)
print_element("variables") do
hash.keys.each {|k|
if k.class.name == "String"
name = '\'' + k + '\''
else
name = exec_with_allocation_control(k, :to_s, OverflowMessageType::EXCEPTION_MESSAGE)
end
print_variable(name, hash[k], 'instance')
}
end
end
def print_string(string)
print_element("variables") do
if string.respond_to?('bytes')
bytes = string.bytes.to_a
InspectCommand.reference_result(bytes)
print_variable('bytes', bytes, 'instance')
end
print_variable('encoding', string.encoding, 'instance') if string.respond_to?('encoding')
end
end
def exec_with_timeout(sec, error_message)
return yield if sec == nil or sec.zero?
if Thread.respond_to?(:critical) and Thread.critical
raise ThreadError, "timeout within critical session"
end
begin
x = Thread.current
y = DebugThread.start {
sleep sec
x.raise SimpleTimeLimitError.new(error_message) if x.alive?
}
yield sec
ensure
y.kill if y and y.alive?
end
end
def exec_with_allocation_control(value, exec_method, overflow_message_type)
return value.send exec_method unless Debugger.trace_to_s
memory_limit = Debugger.debugger_memory_limit
time_limit = Debugger.inspect_time_limit
if defined?(JRUBY_VERSION) || RUBY_VERSION < '2.0' || memory_limit <= 0
return exec_with_timeout(time_limit * 1e-3, "Timeout: evaluation of #{exec_method} took longer than #{time_limit}ms.") { value.send exec_method }
end
require 'objspace'
trace_queue = Queue.new
inspect_thread = DebugThread.start do
start_alloc_size = ObjectSpace.memsize_of_all
start_time = Time.now.to_f
trace_point = TracePoint.new(:c_call, :call) do |tp|
curr_time = Time.now.to_f
if (curr_time - start_time) * 1e3 > time_limit
trace_queue << TimeLimitError.new("Timeout: evaluation of #{exec_method} took longer than #{time_limit}ms.", caller.to_a)
trace_point.disable
inspect_thread.kill
end
next unless rand > 0.75
curr_alloc_size = ObjectSpace.memsize_of_all
start_alloc_size = curr_alloc_size if curr_alloc_size < start_alloc_size
if curr_alloc_size - start_alloc_size > 1e6 * memory_limit
trace_queue << MemoryLimitError.new("Out of memory: evaluation of #{exec_method} took more than #{memory_limit}mb.", caller.to_a)
trace_point.disable
inspect_thread.kill
end
end
trace_point.enable
result = value.send exec_method
trace_queue << result
trace_point.disable
end
while(mes = trace_queue.pop)
if(mes.is_a? TimeLimitError or mes.is_a? MemoryLimitError)
print_debug(mes.message + "\n" + mes.backtrace.map {|l| "\t#{l}"}.join("\n"))
return overflow_message_type.call(mes)
else
return mes
end
end
rescue SimpleTimeLimitError => e
print_debug(e.message)
return overflow_message_type.call(e)
end
def print_variable(name, value, kind)
name = name.to_s
if value.nil?
print("", CGI.escapeHTML(name), kind)
return
end
if value.is_a?(Array) || value.is_a?(Hash)
has_children = !value.empty?
if has_children
size = value.size
value_str = "#{value.class} (#{value.size} element#{size > 1 ? "s" : "" })"
else
value_str = "Empty #{value.class}"
end
elsif value.is_a?(String)
has_children = value.respond_to?('bytes') || value.respond_to?('encoding')
value_str = value
else
has_children = !value.instance_variables.empty? || !value.class.class_variables.empty?
value_str = exec_with_allocation_control(value, :to_s, OverflowMessageType::EXCEPTION_MESSAGE) || 'nil' rescue "<#to_s method raised exception: #{$!}>"
unless value_str.is_a?(String)
value_str = "ERROR: #{value.class}.to_s method returns #{value_str.class}. Should return String."
end
end
if value_str.respond_to?('encode')
# noinspection RubyEmptyRescueBlockInspection
begin
value_str = value_str.encode("UTF-8")
rescue
end
end
value_str = handle_binary_data(value_str)
escaped_value_str = CGI.escapeHTML(value_str)
print("",
CGI.escapeHTML(name), build_compact_value_attr(value, value_str), kind,
build_value_attr(escaped_value_str), value.class,
has_children, value.respond_to?(:object_id) ? value.object_id : value.id)
print("", escaped_value_str) if Debugger.value_as_nested_element
print('')
rescue StandardError => e
print_debug "Unexpected exception \"%s\"\n%s", e.to_s, e.backtrace.join("\n")
print("",
CGI.escapeHTML(name), kind, CGI.escapeHTML(safe_to_string(value)))
end
def print_file_included(file)
print("", file)
end
def print_file_excluded(file)
print("", file)
end
def print_file_filter_status(status)
print("", status)
end
def print_breakpoints(breakpoints)
print_element 'breakpoints' do
breakpoints.sort_by {|b| b.id}.each do |b|
print "", b.id, CGI.escapeHTML(b.source), b.pos.to_s
end
end
end
def print_breakpoint_added(b)
print "", b.id, CGI.escapeHTML(b.source), b.pos
end
def print_breakpoint_deleted(b)
print "", b.id
end
def print_breakpoint_enabled(b)
print "", b.id
end
def print_breakpoint_disabled(b)
print "", b.id
end
def print_contdition_set(bp_id)
print "", bp_id
end
def print_catchpoint_set(exception_class_name)
print "", exception_class_name
end
def print_catchpoint_deleted(exception_class_name)
if Debugger.catchpoint_deleted_event
print "", exception_class_name
else
print_catchpoint_set(exception_class_name)
end
end
def print_expressions(exps)
print_element "expressions" do
exps.each_with_index do |(exp, value), idx|
print_expression(exp, value, idx + 1)
end
end unless exps.empty?
end
def print_expression(exp, value, idx)
print "", exp, value, idx
end
def print_expression_info(incomplete, prompt, indent)
print "",
incomplete, CGI.escapeHTML(prompt), indent
end
def print_eval(exp, value)
print "", CGI.escapeHTML(exp), value
end
def print_pp(value)
print value
end
def print_list(b, e, file, line)
print "[%d, %d] in %s\n", b, e, file
if (lines = Debugger.source_for(file))
b.upto(e) do |n|
if n > 0 && lines[n - 1]
if n == line
print "=> %d %s\n", n, lines[n - 1].chomp
else
print " %d %s\n", n, lines[n - 1].chomp
end
end
end
else
print "No source-file available for %s\n", file
end
end
def print_methods(methods)
print_element "methods" do
methods.each do |method|
print "", method
end
end
end
# Events
def print_breakpoint(_, breakpoint)
print("",
CGI.escapeHTML(breakpoint.source), breakpoint.pos, Debugger.current_context.thnum)
end
def print_catchpoint(exception)
context = Debugger.current_context
print("",
CGI.escapeHTML(context.frame_file(0)), context.frame_line(0), exception.class, CGI.escapeHTML(exception.to_s), context.thnum)
end
def print_trace(context, file, line)
Debugger::print_debug "trace: location=\"%s:%s\", threadId=%d", file, line, context.thnum
# TBD: do we want to clog fronend with the elements? There are tons of them.
# print "", file, line, context.thnum
end
def print_at_line(context, file, line)
print "",
CGI.escapeHTML(File.expand_path(file)), line, context.thnum, context.stack_size
end
def print_exception(exception, _)
print_element("variables") do
proxy = ExceptionProxy.new(exception)
InspectCommand.reference_result(proxy)
print_variable('error', proxy, 'exception')
end
rescue Exception
print "",
exception.class, CGI.escapeHTML(exception.to_s)
end
def print_inspect(eval_result)
print_element("variables") do
print_variable("eval_result", eval_result, 'local')
end
end
def print_load_result(file, exception = nil)
if exception
print("", file, exception.class, CGI.escapeHTML(exception.to_s))
else
print("", file)
end
end
def print_element(name)
print("<#{name}>")
begin
yield
ensure
print("#{name}>")
end
end
private
def print(*params)
Debugger::print_debug(*params)
@interface.print(*params)
end
def handle_binary_data(value)
return '[Binary Data]' if (value.respond_to?('is_binary_data?') && value.is_binary_data?)
return '[Invalid encoding]' if (value.respond_to?('valid_encoding?') && !value.valid_encoding?)
value
end
def current_thread_attr(context)
if context.thread == Thread.current
'current="yes"'
else
''
end
end
def build_compact_name(value, value_str)
return compact_array_str(value) if value.is_a?(Array)
return compact_hash_str(value) if value.is_a?(Hash)
return value_str[0..max_compact_name_size - 3] + '...' if value_str.size > max_compact_name_size
nil
rescue ::Exception => e
print_debug(e)
nil
end
def max_compact_name_size
# todo: do we want to configure it?
50
end
def compact_array_str(value)
slice = value[0..10]
compact = exec_with_allocation_control(slice, :inspect, OverflowMessageType::NIL_MESSAGE)
if compact && value.size != slice.size
compact[0..compact.size - 2] + ", ...]"
end
compact
end
def compact_hash_str(value)
keys_strings = Hash.new
slice = value.sort_by do |k, _|
keys_string = exec_with_allocation_control(k, :to_s, OverflowMessageType::SPECIAL_SYMBOL_MESSAGE)
keys_strings[k] = keys_string
keys_string
end[0..5]
compact = slice.map do |kv|
key_string = keys_strings[kv[0]]
value_string = exec_with_allocation_control(kv[1], :to_s, OverflowMessageType::SPECIAL_SYMBOL_MESSAGE)
"#{key_string}: #{handle_binary_data(value_string)}"
end.join(", ")
"{" + compact + (slice.size != value.size ? ", ..." : "") + "}"
end
def build_compact_value_attr(value, value_str)
compact_value_str = build_compact_name(value, value_str)
compact_value_str.nil? ? '' : "compactValue=\"#{CGI.escapeHTML(compact_value_str)}\""
end
def safe_to_string(value)
begin
str = value.to_s
rescue NoMethodError
str = "(Object doesn't support #to_s)"
end
return str unless str.nil?
string_io = StringIO.new
string_io.write(value)
string_io.string
end
def build_value_attr(escaped_value_str)
Debugger.value_as_nested_element ? '' : "value=\"#{escaped_value_str}\""
end
instance_methods.each do |m|
if m.to_s.index('print_') == 0
protect m
end
end
end
end