#include "byebug.h" static VALUE mByebug; /* Ruby Byebug Module object */ static VALUE tracing = Qfalse; static VALUE post_mortem = Qfalse; static VALUE verbose = Qfalse; static VALUE catchpoints = Qnil; static VALUE breakpoints = Qnil; static VALUE tracepoints = Qnil; static VALUE raised_exception = Qnil; static ID idPuts; static ID idEmpty; /* Hash table with active threads and their associated contexts */ VALUE threads = Qnil; /* * call-seq: * Byebug.breakpoints -> array * * Returns an array of breakpoints. */ static VALUE Breakpoints(VALUE self) { UNUSED(self); if (NIL_P(breakpoints)) breakpoints = rb_ary_new(); return breakpoints; } /* * call-seq: * Byebug.catchpoints -> array * * Returns an array of catchpoints. */ static VALUE Catchpoints(VALUE self) { UNUSED(self); return catchpoints; } /* * call-seq: * Byebug.raised_exception -> exception * * Returns raised exception when in post_mortem mode. */ static VALUE Raised_exception(VALUE self) { UNUSED(self); return raised_exception; } #define IS_STARTED (!NIL_P(catchpoints)) static void check_started() { if (!IS_STARTED) { rb_raise(rb_eRuntimeError, "Byebug is not started yet."); } } static void trace_print(rb_trace_arg_t * trace_arg, debug_context_t * dc, const char *file_filter, const char *debug_msg) { char *fullpath = NULL; const char *basename; int filtered = 0; const char *event = rb_id2name(SYM2ID(rb_tracearg_event(trace_arg))); VALUE rb_path = rb_tracearg_path(trace_arg); const char *path = NIL_P(rb_path) ? "" : RSTRING_PTR(rb_path); int line = NUM2INT(rb_tracearg_lineno(trace_arg)); VALUE rb_mid = rb_tracearg_method_id(trace_arg); const char *mid = NIL_P(rb_mid) ? "(top level)" : rb_id2name(SYM2ID(rb_mid)); VALUE rb_cl = rb_tracearg_defined_class(trace_arg); VALUE rb_cl_name = NIL_P(rb_cl) ? rb_cl : rb_mod_name(rb_cl); const char *defined_class = NIL_P(rb_cl_name) ? "" : RSTRING_PTR(rb_cl_name); if (!trace_arg) return; if (file_filter) { #ifndef _WIN32 fullpath = realpath(path, NULL); #endif basename = fullpath ? strrchr(fullpath, '/') : path; if (!basename || strncmp(basename + 1, file_filter, strlen(file_filter))) filtered = 1; #ifndef _WIN32 free(fullpath); #endif } if (!filtered) { if (debug_msg) rb_funcall(mByebug, idPuts, 1, rb_sprintf("[#%d] %s\n", dc->thnum, debug_msg)); else rb_funcall(mByebug, idPuts, 1, rb_sprintf("%*s [#%d] %s@%s:%d %s#%s\n", dc->calced_stack_size, "", dc->thnum, event, path, line, defined_class, mid)); } } static void cleanup(debug_context_t * dc) { dc->stop_reason = CTX_STOP_NONE; release_lock(); } #define EVENT_TEARDOWN cleanup(dc); #define EVENT_SETUP \ debug_context_t *dc; \ VALUE context; \ rb_trace_arg_t *trace_arg; \ \ UNUSED(data); \ \ if (!is_living_thread(rb_thread_current())) \ return; \ \ thread_context_lookup(rb_thread_current(), &context); \ Data_Get_Struct(context, debug_context_t, dc); \ \ trace_arg = rb_tracearg_from_tracepoint(trace_point); \ if (verbose == Qtrue) \ trace_print(trace_arg, dc, 0, 0); \ \ if (CTX_FL_TEST(dc, CTX_FL_IGNORE)) \ return; \ \ acquire_lock(dc); \ \ #define CALL_EVENT_SETUP \ dc->calced_stack_size++; \ dc->steps_out = dc->steps_out < 0 ? -1 : dc->steps_out + 1; #define RETURN_EVENT_SETUP \ dc->calced_stack_size--; \ \ if (dc->steps_out == 1) \ dc->steps = 1; #define RETURN_EVENT_TEARDOWN \ dc->steps_out = dc->steps_out <= 0 ? -1 : dc->steps_out - 1; /* Functions that return control to byebug after the different events */ static VALUE call_at(VALUE ctx, debug_context_t * dc, ID mid, int argc, VALUE arg) { struct call_with_inspection_data cwi; VALUE argv[1]; argv[0] = arg; cwi.dc = dc; cwi.ctx = ctx; cwi.id = mid; cwi.argc = argc; cwi.argv = &argv[0]; return call_with_debug_inspector(&cwi); } static VALUE call_at_line(VALUE ctx, debug_context_t * dc) { return call_at(ctx, dc, rb_intern("at_line"), 0, Qnil); } static VALUE call_at_tracing(VALUE ctx, debug_context_t * dc) { return call_at(ctx, dc, rb_intern("at_tracing"), 0, Qnil); } static VALUE call_at_breakpoint(VALUE ctx, debug_context_t * dc, VALUE breakpoint) { dc->stop_reason = CTX_STOP_BREAKPOINT; return call_at(ctx, dc, rb_intern("at_breakpoint"), 1, breakpoint); } static VALUE call_at_catchpoint(VALUE ctx, debug_context_t * dc, VALUE exp) { dc->stop_reason = CTX_STOP_CATCHPOINT; return call_at(ctx, dc, rb_intern("at_catchpoint"), 1, exp); } static VALUE call_at_return(VALUE ctx, debug_context_t * dc, VALUE return_value) { dc->stop_reason = CTX_STOP_BREAKPOINT; return call_at(ctx, dc, rb_intern("at_return"), 1, return_value); } static VALUE call_at_end(VALUE ctx, debug_context_t * dc) { dc->stop_reason = CTX_STOP_BREAKPOINT; return call_at(ctx, dc, rb_intern("at_end"), 0, Qnil); } static void call_at_line_check(VALUE ctx, debug_context_t * dc, VALUE breakpoint) { dc->stop_reason = CTX_STOP_STEP; if (!NIL_P(breakpoint)) call_at_breakpoint(ctx, dc, breakpoint); reset_stepping_stop_points(dc); call_at_line(ctx, dc); } /* TracePoint API event handlers */ static void line_event(VALUE trace_point, void *data) { VALUE brkpnt, file, line, binding; EVENT_SETUP; file = rb_tracearg_path(trace_arg); line = rb_tracearg_lineno(trace_arg); binding = rb_tracearg_binding(trace_arg); if (RTEST(tracing)) call_at_tracing(context, dc); if (!CTX_FL_TEST(dc, CTX_FL_IGNORE_STEPS)) dc->steps = dc->steps <= 0 ? -1 : dc->steps - 1; if (dc->calced_stack_size <= dc->dest_frame) { dc->dest_frame = dc->calced_stack_size; CTX_FL_UNSET(dc, CTX_FL_IGNORE_STEPS); dc->lines = dc->lines <= 0 ? -1 : dc->lines - 1; } if (dc->steps == 0 || dc->lines == 0) call_at_line_check(context, dc, Qnil); else { brkpnt = Qnil; if (!NIL_P(breakpoints)) brkpnt = find_breakpoint_by_pos(breakpoints, file, line, binding); if (!NIL_P(brkpnt)) call_at_line_check(context, dc, brkpnt); } EVENT_TEARDOWN; } static void call_event(VALUE trace_point, void *data) { VALUE brkpnt, klass, msym, mid, binding, self; EVENT_SETUP; if (dc->calced_stack_size <= dc->dest_frame) CTX_FL_UNSET(dc, CTX_FL_IGNORE_STEPS); CALL_EVENT_SETUP; /* nil method_id means we are at top level so there can't be a method * breakpoint here. Just leave then. */ msym = rb_tracearg_method_id(trace_arg); if (NIL_P(msym)) { EVENT_TEARDOWN; return; } mid = SYM2ID(msym); klass = rb_tracearg_defined_class(trace_arg); binding = rb_tracearg_binding(trace_arg); self = rb_tracearg_self(trace_arg); brkpnt = Qnil; if (!NIL_P(breakpoints)) brkpnt = find_breakpoint_by_method(breakpoints, klass, mid, binding, self); if (!NIL_P(brkpnt)) { call_at_breakpoint(context, dc, brkpnt); call_at_line(context, dc); } EVENT_TEARDOWN; } static void return_event(VALUE trace_point, void *data) { VALUE brkpnt, file, line, binding; EVENT_SETUP; RETURN_EVENT_SETUP; if ((dc->steps_out == 0) && (CTX_FL_TEST(dc, CTX_FL_STOP_ON_RET))) { reset_stepping_stop_points(dc); call_at_return(context, dc, rb_tracearg_return_value(trace_arg)); } else if (!NIL_P(breakpoints)) { file = rb_tracearg_path(trace_arg); /* * TODO: Sometimes the TracePoint API gives some return events without * file:line information, so we need to guard for nil until we know what's * going on. This happens, for example, with active_support core extensions: * * [#7] call@.../core_ext/numeric/conversions.rb:124 Fixnum#to_s * [#7] b_call@.../core_ext/numeric/conversions.rb:124 BigDecimal#to_s * [#7] line@.../core_ext/numeric/conversions.rb:125 BigDecimal#to_s * [#7] c_call@.../core_ext/numeric/conversions.rb:125 Kernel#is_a? * [#7] c_return@.../core_ext/numeric/conversions.rb:125 Kernel#is_a? * [#7] line@.../core_ext/numeric/conversions.rb:131 BigDecimal#to_s * [#7] c_call@.../core_ext/numeric/conversions.rb:131 Fixnum#to_default_s * [#7] c_return@.../core_ext/numeric/conversions.rb:131 Fixnum#to_default_s * [#7] b_return@/hort/core_ext/numeric/conversions.rb:133 BigDecimal#to_s * [#7] return@:0 Fixnum#to_s # => This guy... */ if (!NIL_P(file)) { line = rb_tracearg_lineno(trace_arg); binding = rb_tracearg_binding(trace_arg); brkpnt = find_breakpoint_by_pos(breakpoints, file, line, binding); if (!NIL_P(brkpnt)) call_at_return(context, dc, rb_tracearg_return_value(trace_arg)); } } RETURN_EVENT_TEARDOWN; EVENT_TEARDOWN; } static void end_event(VALUE trace_point, void *data) { EVENT_SETUP; RETURN_EVENT_SETUP; if ((dc->steps_out == 0) && (CTX_FL_TEST(dc, CTX_FL_STOP_ON_RET))) { reset_stepping_stop_points(dc); call_at_end(context, dc); } RETURN_EVENT_TEARDOWN; EVENT_TEARDOWN; } static void raw_call_event(VALUE trace_point, void *data) { EVENT_SETUP; CALL_EVENT_SETUP; EVENT_TEARDOWN; } static void raw_return_event(VALUE trace_point, void *data) { EVENT_SETUP; RETURN_EVENT_SETUP; RETURN_EVENT_TEARDOWN; EVENT_TEARDOWN; } static void raise_event(VALUE trace_point, void *data) { VALUE expn_class, ancestors, pm_context; int i; debug_context_t *new_dc; EVENT_SETUP; raised_exception = rb_tracearg_raised_exception(trace_arg); if (post_mortem == Qtrue) { pm_context = context_dup(dc); rb_ivar_set(raised_exception, rb_intern("@__bb_context"), pm_context); Data_Get_Struct(pm_context, debug_context_t, new_dc); rb_debug_inspector_open(context_backtrace_set, (void *)new_dc); } if (NIL_P(catchpoints) || dc->calced_stack_size == 0 || RHASH_TBL(catchpoints)->num_entries == 0) { EVENT_TEARDOWN; return; } expn_class = rb_obj_class(raised_exception); ancestors = rb_mod_ancestors(expn_class); for (i = 0; i < RARRAY_LENINT(ancestors); i++) { VALUE ancestor_class, module_name, hit_count; ancestor_class = rb_ary_entry(ancestors, i); module_name = rb_mod_name(ancestor_class); hit_count = rb_hash_aref(catchpoints, module_name); /* increment exception */ if (!NIL_P(hit_count)) { rb_hash_aset(catchpoints, module_name, INT2FIX(FIX2INT(hit_count) + 1)); call_at_catchpoint(context, dc, raised_exception); call_at_line(context, dc); break; } } EVENT_TEARDOWN; } /* Setup TracePoint functionality */ static void register_tracepoints(VALUE self) { int i; VALUE traces = tracepoints; UNUSED(self); if (NIL_P(traces)) { int line_msk = RUBY_EVENT_LINE; int call_msk = RUBY_EVENT_CALL; int ret_msk = RUBY_EVENT_RETURN | RUBY_EVENT_B_RETURN; int end_msk = RUBY_EVENT_END; int raw_call_msk = RUBY_EVENT_C_CALL | RUBY_EVENT_B_CALL | RUBY_EVENT_CLASS; int raw_ret_msk = RUBY_EVENT_C_RETURN; int raise_msk = RUBY_EVENT_RAISE; VALUE tpLine = rb_tracepoint_new(Qnil, line_msk, line_event, 0); VALUE tpCall = rb_tracepoint_new(Qnil, call_msk, call_event, 0); VALUE tpReturn = rb_tracepoint_new(Qnil, ret_msk, return_event, 0); VALUE tpEnd = rb_tracepoint_new(Qnil, end_msk, end_event, 0); VALUE tpCCall = rb_tracepoint_new(Qnil, raw_call_msk, raw_call_event, 0); VALUE tpCReturn = rb_tracepoint_new(Qnil, raw_ret_msk, raw_return_event, 0); VALUE tpRaise = rb_tracepoint_new(Qnil, raise_msk, raise_event, 0); traces = rb_ary_new(); rb_ary_push(traces, tpLine); rb_ary_push(traces, tpCall); rb_ary_push(traces, tpReturn); rb_ary_push(traces, tpEnd); rb_ary_push(traces, tpCCall); rb_ary_push(traces, tpCReturn); rb_ary_push(traces, tpRaise); tracepoints = traces; } for (i = 0; i < RARRAY_LENINT(traces); i++) rb_tracepoint_enable(rb_ary_entry(traces, i)); } static void clear_tracepoints(VALUE self) { int i; UNUSED(self); for (i = RARRAY_LENINT(tracepoints) - 1; i >= 0; i--) rb_tracepoint_disable(rb_ary_entry(tracepoints, i)); } /* Byebug's Public API */ /* * call-seq: * Byebug.contexts -> array * * Returns an array of all contexts. */ static VALUE Contexts(VALUE self) { volatile VALUE list; volatile VALUE new_list; VALUE context; threads_table_t *t_tbl; debug_context_t *dc; int i; UNUSED(self); check_started(); new_list = rb_ary_new(); list = rb_funcall(rb_cThread, rb_intern("list"), 0); for (i = 0; i < RARRAY_LENINT(list); i++) { VALUE thread = rb_ary_entry(list, i); thread_context_lookup(thread, &context); rb_ary_push(new_list, context); } Data_Get_Struct(threads, threads_table_t, t_tbl); st_clear(t_tbl->tbl); for (i = 0; i < RARRAY_LENINT(new_list); i++) { context = rb_ary_entry(new_list, i); Data_Get_Struct(context, debug_context_t, dc); st_insert(t_tbl->tbl, dc->thread, context); } return new_list; } /* * call-seq: * Byebug.thread_context(thread) -> context * * Returns context of the thread passed as an argument. */ static VALUE Thread_context(VALUE self, VALUE thread) { VALUE context; UNUSED(self); check_started(); thread_context_lookup(thread, &context); return context; } /* * call-seq: * Byebug.current_context -> context * * Returns the current context. * Note: Byebug.current_context.thread == Thread.current */ static VALUE Current_context(VALUE self) { VALUE context; UNUSED(self); thread_context_lookup(rb_thread_current(), &context); return context; } /* * call-seq: * Byebug.started? -> bool * * Returns +true+ byebug is started. */ static VALUE Started(VALUE self) { UNUSED(self); return IS_STARTED ? Qtrue : Qfalse; } /* * call-seq: * Byebug.stop -> bool * * This method disables byebug. It returns +true+ if byebug was already * disabled, otherwise it returns +false+. */ static VALUE Stop(VALUE self) { UNUSED(self); if (IS_STARTED) { clear_tracepoints(self); breakpoints = Qnil; catchpoints = Qnil; return Qfalse; } return Qtrue; } static VALUE Stoppable(VALUE self) { VALUE context; debug_context_t *dc; if (!IS_STARTED) return Qfalse; if (!NIL_P(breakpoints) && rb_funcall(breakpoints, idEmpty, 0) == Qfalse) return Qfalse; if (!NIL_P(catchpoints) && rb_funcall(catchpoints, idEmpty, 0) == Qfalse) return Qfalse; if (post_mortem == Qtrue) return Qfalse; context = Current_context(self); if (!NIL_P(context)) { Data_Get_Struct(context, debug_context_t, dc); if (dc->steps > 0) return Qfalse; } return Qtrue; } /* * call-seq: * Byebug.start -> bool * * The return value is the value of !Byebug.started? before issuing the * +start+; That is, +true+ is returned, unless byebug was previously started. */ static VALUE Start(VALUE self) { if (IS_STARTED) return Qfalse; catchpoints = rb_hash_new(); threads = create_threads_table(); register_tracepoints(self); return Qtrue; } /* * call-seq: * Byebug.debug_load(file, stop = false) -> nil * * Same as Kernel#load but resets current context's frames. * +stop+ parameter forces byebug to stop at the first line of code in +file+ */ static VALUE Debug_load(int argc, VALUE * argv, VALUE self) { VALUE file, stop, context; debug_context_t *dc; VALUE status = Qnil; int state = 0; UNUSED(self); if (rb_scan_args(argc, argv, "11", &file, &stop) == 1) stop = Qfalse; Start(self); context = Current_context(self); Data_Get_Struct(context, debug_context_t, dc); dc->calced_stack_size = 1; if (RTEST(stop)) dc->steps = 1; rb_load_protect(file, 0, &state); if (0 != state) { status = rb_errinfo(); reset_stepping_stop_points(dc); } return status; } /* * call-seq: * Byebug.tracing? -> bool * * Returns +true+ if global tracing is enabled. */ static VALUE Tracing(VALUE self) { UNUSED(self); return tracing; } /* * call-seq: * Byebug.tracing = bool * * Sets the global tracing flag. */ static VALUE Set_tracing(VALUE self, VALUE value) { UNUSED(self); tracing = RTEST(value) ? Qtrue : Qfalse; return value; } /* * call-seq: * Byebug.verbose? -> bool * * Returns +true+ if global verbose flag for TracePoint API events is enabled. */ static VALUE Verbose(VALUE self) { UNUSED(self); return verbose; } /* * call-seq: * Byebug.verbose = bool * * Sets the global verbose flag for TracePoint API events is enabled. */ static VALUE Set_verbose(VALUE self, VALUE value) { UNUSED(self); verbose = RTEST(value) ? Qtrue : Qfalse; return value; } /* * call-seq: * Byebug.post_mortem? -> bool * * Returns +true+ if post-mortem debugging is enabled. */ static VALUE Post_mortem(VALUE self) { UNUSED(self); return post_mortem; } /* * call-seq: * Byebug.post_mortem = bool * * Sets post-moterm flag. */ static VALUE Set_post_mortem(VALUE self, VALUE value) { UNUSED(self); post_mortem = RTEST(value) ? Qtrue : Qfalse; return value; } /* * call-seq: * Byebug.add_catchpoint(exception) -> exception * * Adds a new exception to the catchpoints array. */ static VALUE Add_catchpoint(VALUE self, VALUE value) { UNUSED(self); if (TYPE(value) != T_STRING) rb_raise(rb_eTypeError, "value of a catchpoint must be String"); rb_hash_aset(catchpoints, rb_str_dup(value), INT2FIX(0)); return value; } /* * Document-class: Byebug * * == Summary * * This is a singleton class allows controlling byebug. Use it to start/stop * byebug, set/remove breakpoints, etc. */ void Init_byebug() { mByebug = rb_define_module("Byebug"); rb_define_module_function(mByebug, "add_catchpoint", Add_catchpoint, 1); rb_define_module_function(mByebug, "breakpoints", Breakpoints, 0); rb_define_module_function(mByebug, "catchpoints", Catchpoints, 0); rb_define_module_function(mByebug, "contexts", Contexts, 0); rb_define_module_function(mByebug, "current_context", Current_context, 0); rb_define_module_function(mByebug, "debug_load", Debug_load, -1); rb_define_module_function(mByebug, "post_mortem?", Post_mortem, 0); rb_define_module_function(mByebug, "post_mortem=", Set_post_mortem, 1); rb_define_module_function(mByebug, "raised_exception", Raised_exception, 0); rb_define_module_function(mByebug, "start", Start, 0); rb_define_module_function(mByebug, "started?", Started, 0); rb_define_module_function(mByebug, "stop", Stop, 0); rb_define_module_function(mByebug, "stoppable?", Stoppable, 0); rb_define_module_function(mByebug, "thread_context", Thread_context, 1); rb_define_module_function(mByebug, "tracing?", Tracing, 0); rb_define_module_function(mByebug, "tracing=", Set_tracing, 1); rb_define_module_function(mByebug, "verbose?", Verbose, 0); rb_define_module_function(mByebug, "verbose=", Set_verbose, 1); Init_threads_table(mByebug); Init_context(mByebug); Init_breakpoint(mByebug); rb_global_variable(&breakpoints); rb_global_variable(&catchpoints); rb_global_variable(&tracepoints); rb_global_variable(&raised_exception); rb_global_variable(&threads); idPuts = rb_intern("puts"); idEmpty = rb_intern("empty?"); }