%%% -*- erlang-indent-level: 2 -*- %%%------------------------------------------------------------------- %%% File : redbug_targ.erl %%% Author : Mats Cronqvist %%% Description : %%% %%% Created : 18 Jan 2007 by Mats Cronqvist %%%------------------------------------------------------------------- %%% @hidden -module(redbug_targ). -export([start/2]). -export([init/1]). -export([get_rec_fields/2]). -record(ld, {buffering, count, dest, file, flags, host_pid, loop_fun, maxqueue, maxsize, port_no, procs, queue_size, asts, trace_patterns, style, time, timer, tracer, records, where, wrap_count, wrap_size}). %%% runs on host %%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%% %% unless the target has up to date versions of redbug code, we send it over %% and load it. Then we start the rarget process. start(Node, Cnf) -> assert_loaded(Node, [?MODULE, redbug_compiler]), do_start(Node, mk_ld(Cnf)). mk_ld(Props) -> #ld{host_pid = self(), time = proplists:get_value(time, Props), flags = proplists:get_value(flags, Props), asts = proplists:get_value(asts, Props), procs = proplists:get_value(procs, Props), records = proplists:get_value(records, Props), where = proplists:get_value(where, Props)}. assert_loaded(Node, Modules) -> lists:foreach(fun(M) -> assert_load(Node, M) end, Modules). assert_load(nonode@nohost, Mod) -> {module, Mod} = c:l(Mod); assert_load(Node, Mod) -> case rpc:call(Node, Mod, module_info, [compile]) of {badrpc, {'EXIT', {undef, _}}} -> %no code netload(Node, Mod), assert_load(Node, Mod); {badrpc, _} -> ok; CompInfo when is_list(CompInfo) -> case {ftime(CompInfo), ftime(Mod:module_info(compile))} of {interpreted, _} -> ok; {TargT, HostT} when TargT < HostT -> %old code on target netload(Node, Mod), assert_load(Node, Mod); _ -> ok end end. netload(Node, Mod) -> {Mod, Bin, Fname} = code:get_object_code(Mod), {module, Mod} = rpc:call(Node, code, load_binary, [Mod, Fname, Bin]). ftime([]) -> interpreted; ftime([{time, T}|_]) -> T; ftime([_|T]) -> ftime(T). do_start(Node, LD) -> case Node =:= nonode@nohost orelse net_adm:ping(Node) =:= pong of true -> Pid = spawn_link(Node, fun init/0), Pid ! LD, Pid; false -> exit(node_down) end. %%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%% %% trace control process %%% #ld.where = {buffer, Pid, Count, MaxQueue, MaxSize} | %%% {stream, Pid, Count, MaxQueue, MaxSize} | %%% {discard, Pid, Count, MaxQueue, MaxSize} | %%% {file, File, Size, Count} | %%% {ip, Port, Queue} %%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%% -spec init() -> no_return(). init() -> receive LD -> init(LD) end. -define(pipe(A, B, C, D, E, F), F(E(D(C(B(A)))))). init(LD0) -> unset_tps(), LD = ?pipe(LD0, codegen, maybe_load_mods, expand_underscores, fix_procs, consumer), [send_meta({recs, get_rec_fields(Mod)}) || Mod <- LD#ld.records], NoProcs = start_trace(LD), untrace(family(redbug)++family(?MODULE), LD#ld.flags), NoFuncs = set_tps(LD#ld.trace_patterns), assert_trace_targets(NoProcs, NoFuncs, LD#ld.flags, LD#ld.procs), LD#ld.host_pid ! {{starting, self(), NoProcs, NoFuncs}}, exit((LD#ld.loop_fun)()). codegen(LD) -> LD#ld{trace_patterns = lists:map(fun redbug_compiler:generate/1, LD#ld.asts)}. -define(fold_field(Rec, Field, Fun, Acc0), Rec#ld{Field = lists:foldl(Fun, Acc0, Rec#ld.Field)}). maybe_load_mods(LD) -> ?fold_field(LD, trace_patterns, fun maybe_load_mod/2, []). maybe_load_mod({{M, _, _}, _, _} = Rtp, O) -> try case code:which(M) of preloaded -> ok; non_existing -> throw(non_existing_module); L when is_list(L) -> [c:l(M) || false =:= code:is_loaded(M)] end, [Rtp|O] catch _:_ -> O end. expand_underscores(LD) -> ?fold_field(LD, trace_patterns, fun expand_underscore/2, []). expand_underscore({{'_', '_', '_'}, MatchSpec, Flags}, O) -> lists:foldl(mk_expand_module(MatchSpec, Flags), O, modules()); expand_underscore({{M, '_', '_'}, MatchSpec, Flags}, O) -> lists:foldl(mk_expand_function(M, MatchSpec, Flags), O, functions(M)); expand_underscore({{M, F, '_'}, MatchSpec, Flags}, O) -> lists:foldl(mk_expand_arity(M, F, MatchSpec, Flags), O, arities(M, F)); expand_underscore(ExpandedRtp, O) -> [ExpandedRtp|O]. mk_expand_module(MatchSpec, Flags) -> fun(M, A) -> expand_underscore({{M, '_', '_'}, MatchSpec, Flags}, A) end. mk_expand_function(M, MatchSpec, Flags) -> fun({F, Ari}, A) -> expand_underscore({{M, F, Ari}, MatchSpec, Flags}, A) end. mk_expand_arity(M, F, MatchSpec, Flags) -> fun(Ari, A) -> expand_underscore({{M, F, Ari}, MatchSpec, Flags}, A) end. modules() -> [M || {M, F} <- code:all_loaded(), is_list(F), filelib:is_regular(F)]. functions(M) -> locals(M)++globals(M). arities(M, F) -> [Ari || {Fun, Ari} <- functions(M), Fun =:= F]. locals(M) -> case code:get_object_code(M) of error -> []; {_,Bin,_} -> case beam_lib:chunks(Bin,[locals]) of {ok,{M,[{locals,Locals}]}} -> Locals; {error, beam_lib, _} -> [] end end. globals(M) -> M:module_info(exports). fix_procs(LD) -> ?fold_field(LD, procs, fun mk_prc/2, []). mk_prc(Ps, A) when Ps =:= running; Ps =:= all; Ps =:= new -> [Ps|A]; mk_prc(Reg, A) when is_atom(Reg) -> case whereis(Reg) of Pid when is_pid(Pid) -> mk_prc(Pid, A); undefined -> A end; mk_prc({pid, P1, P2}, A) when is_integer(P1), is_integer(P2) -> mk_prc(c:pid(0, P1, P2), A); mk_prc(Pid, A) when is_pid(Pid) -> case is_process_alive(Pid) of true -> [Pid|A]; false-> A end. family(Daddy) -> try D = whereis(Daddy), [D|element(2, process_info(D, links))] catch _:_-> [] end. untrace(Pids, Flags) -> [try erlang:trace(P, false, Flags) catch _:R -> exit({untrace_error, {R, erlang:trace_info(P, flags)}}) end || P <- Pids, is_pid(P), node(P) =:= node(), {flags, []} =/= erlang:trace_info(P, flags)]. set_tps(TPs) -> lists:foldl(fun set_tp/2, 0, TPs). set_tp({MFA, MatchSpec, Flags}, A) -> Count = erlang:trace_pattern(MFA, MatchSpec, Flags), A+Count. assert_trace_targets(NoProcs, NoFuncs, Flags, Ps) -> case 0 < NoProcs orelse is_new_pidspec(Ps) of true -> ok; false-> exit(no_matching_processes) end, case 0 < NoFuncs orelse is_message_trace(Flags) of true -> ok; false-> exit(no_matching_functions) end. is_new_pidspec(Ps) -> lists:member(new, Ps). is_message_trace(Flags) -> (lists:member(send, Flags) orelse lists:member('receive', Flags)). start_trace(LD) -> Flags = [{tracer, LD#ld.tracer}|LD#ld.flags], lists:sum([erlang:trace(P, true, Flags) || P <- LD#ld.procs]). stop_trace(LD) -> erlang:trace(all, false, LD#ld.flags). unset_tps() -> erlang:trace_pattern({'_', '_', '_'}, false, [local, call_count, call_time]), erlang:trace_pattern({'_', '_', '_'}, false, [global]). send_meta(Msg) -> self() ! {meta, Msg}. %%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%% consumer(LD = #ld{where = {Buffering, Pid, Cnt, MaxQueue, MaxSize}}) -> init_local_pid(LD#ld{count = Cnt, maxsize = MaxSize, maxqueue = MaxQueue, dest = Pid, buffering = Buffering}); consumer(LD = #ld{where = {file, File, Size, WrapCount}}) -> init_local_port(LD#ld{style = file, file = File, wrap_size = Size, wrap_count = WrapCount}); consumer(LD = #ld{where = {ip, Port, QueueSize}}) -> init_local_port(LD#ld{style = ip, port_no = Port, queue_size = QueueSize}). %%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%% %%% local consumer process for port-style tracing. %%% writes trace messages directly to an erlang port. %%% flushes and quits when; %%% it gets a stop from the controller %%% timeout %%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%% init_local_port(LD0) -> LD = LD0#ld{tracer = mk_port(LD0), timer = erlang:start_timer(LD0#ld.time, self(), nul)}, LD#ld{loop_fun = fun() -> loop_local_port(LD) end}. loop_local_port(LD = #ld{timer = Timer}) -> receive stop -> stop_trace_port(LD), stop; {timeout, Timer, _} -> stop_trace_port(LD), timeout end. stop_trace_port(LD) -> stop_trace(LD), unset_tps(), dbg:deliver_and_flush(LD#ld.tracer). mk_port(LD) -> (dbg:trace_port(LD#ld.style, trace_port_data(LD)))(). trace_port_data(#ld{style = ip, port_no = PortNo, queue_size = QueueSize}) -> {PortNo, QueueSize}; trace_port_data(#ld{style = file, file = F, wrap_count = C, wrap_size = S}) -> Suffix = ".trc", {F, wrap, Suffix, S*1024*1024, C}. %%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%% %%% local consumer process for pid-style tracing. %%% buffers trace messages, and flushes them when; %%% it gets a stop from the controller %%% reaches count = 0 %%% timeout %%% message queue too long %%% a trace message is too big %%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%% init_local_pid(LD0) -> LD = LD0#ld{timer = erlang:start_timer(LD0#ld.time, self(), nul), tracer = self()}, LD#ld{loop_fun = fun() -> loop_lp(LD, buffering(LD), LD#ld.count) end}. buffering(#ld{buffering = Buff}) -> case Buff of buffer -> []; stream -> no; discard -> discard end. loop_lp(LD, Buff, Count) -> maybe_exit_queue(LD), maybe_exit_count(LD, Buff, Count), receive {trace_ts, Pid, Tag, A, TS} -> Buf = msg(Buff, LD, {Tag, Pid, TS, A}), loop_lp(LD, Buf, Count-1); {trace_ts, Pid, Tag, A, B, TS} -> Buf = msg(Buff, LD, {Tag, Pid, TS, {A, B}}), loop_lp(LD, Buf, Count-1); {meta, Meta} -> loop_lp(LD, buff(Buff, LD, {meta, Meta}), Count); {timeout, _, _} -> stop_trace_lp(LD, Buff), timeout; stop -> stop_trace_lp(LD, Buff), stop end. stop_trace_lp(LD, Buff) -> stop_trace(LD), flush(LD, Buff), unset_tps(). msg(Buff, LD, Item) -> maybe_exit_size(LD, Item), buff(Buff, LD, Item). buff(discard, _, _) -> discard; buff(no, LD, Item) -> send_one(LD, Item), no; buff(Buff, _LD, Item) -> [Item|Buff]. maybe_exit_count(LD, Buff, Count) -> case 0 < Count of true -> ok; false-> flush(LD, Buff), exit(msg_count) end. %% check queue length maybe_exit_queue(#ld{maxqueue = MaxQlen}) -> case process_info(self(), message_queue_len) of {_, Qlen} when Qlen > MaxQlen -> exit({msg_queue, Qlen}); _ -> ok end. %% check size %% recurse through the term %% exit if there is a long list or a large binary maybe_exit_size(#ld{maxsize = MS}, {_, _, _, Payload}) -> maybe_exit_size_loop(MS, Payload). maybe_exit_size_loop(MS, T) when is_tuple(T) -> maybe_exit_size_loop(MS, tuple_to_list(T)); maybe_exit_size_loop(MS, L) when length(L) < MS -> lists:foreach(fun(E) -> maybe_exit_size_loop(MS, E)end, L); maybe_exit_size_loop(MS, L) when MS =< length(L) -> exit({arg_length, length(L)}); maybe_exit_size_loop(MS, B) when MS < byte_size(B) -> exit({arg_size, byte_size(B)}); maybe_exit_size_loop(_, _) -> ok. send_one(LD, Msg) -> LD#ld.dest ! [msg(Msg)]. flush(LD, Buffer) -> case is_list(Buffer) of true -> LD#ld.dest ! lists:map(fun msg/1, lists:reverse(Buffer)); false -> ok end, lists:foreach(mk_flush_time_count(LD#ld.dest), LD#ld.trace_patterns). mk_flush_time_count(Where) -> fun({MFA, _MatchSpec, Flags}) -> case [time_count(MFA, Flag) || Flag <- Flags, is_counter(Flag)] of [] -> ok; Msgs -> Where ! Msgs end end. is_counter(Flag) -> (Flag =:= call_count) orelse (Flag =:= call_time). time_count(MFA, Flag) -> {Flag, {MFA, element(2, erlang:trace_info(MFA, Flag))}, [], ts(ts())}. msg({'meta', Meta}) -> {'meta', Meta, undefined, ts(ts())}; msg({'send', Pid, TS, {Msg, To}}) -> {'send', {Msg, pi(To)}, pi(Pid), ts(TS)}; msg({'receive', Pid, TS, Msg}) -> {'recv', Msg, pi(Pid), ts(TS)}; msg({'return_from', Pid, TS, {MFA, V}}) -> {'retn', {MFA, V}, pi(Pid), ts(TS)}; msg({'exception_from', Pid, TS, {MFA, V}}) -> {'retn', {MFA, V}, pi(Pid), ts(TS)}; msg({'call', Pid, TS, {MFA, B}}) -> {'call', {MFA, B}, pi(Pid), ts(TS)}; msg({'call', Pid, TS, MFA}) -> {'call', {MFA, <<>>}, pi(Pid), ts(TS)}. pi(P) when is_pid(P) -> try process_info(P, registered_name) of [] -> {P, mk_unreg_name(P)}; {_, Name} -> {P, Name}; undefined -> {P, dead} catch error:badarg -> {P, node(P)} end; pi(P) when is_port(P) -> try {name, N} = erlang:port_info(P, name), [Hd|_] = string:tokens(N, " "), {P, lists:reverse(hd(string:tokens(lists:reverse(Hd), "/")))} catch _:_ -> {P, dead} end; pi(R) when is_atom(R) -> R; pi({R, Node}) when is_atom(R), Node =:= node() -> R; pi({R, Node}) when is_atom(R), is_atom(Node) -> {R, Node}. mk_unreg_name(P) -> case process_info(P, initial_call) of {_, {proc_lib, init_p, 5}} -> proc_lib:translate_initial_call(P); {_, MFA} -> MFA; undefined -> dead end. ts(Nw) -> {_, {H, M, S}} = calendar:now_to_local_time(Nw), {H, M, S, element(3, Nw)}. %%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%% %% records. records are identified by the {module name, record name} tuple. %% we get the record info (i.e. the list of field names) from the beam file. get_rec_fields(Mod, Rec) -> case lists:keyfind(Rec, 1, get_rec_fields(Mod)) of false -> die("no such record", Rec); {Rec, Fs} -> Fs end. get_rec_fields(Mod) -> get_recs_fields(get_ast(Mod)). get_ast(Mod) -> try {ok, _, Chunks} = beam_lib:all_chunks(code:which(Mod)), find_ast(Chunks) catch _:{_, {_, _, {file_error, _, enoent}}} -> die("no such module", Mod); _:_ -> die("no debug info", Mod) end. %% beam file format switched from Abst to Dbgi ~20. find_ast([{"Dbgi", Dbgi}|_]) -> {debug_info_v1, _, {AST, _}} = binary_to_term(Dbgi), AST; find_ast([{"Abst", Abst}|_]) -> {raw_abstract_v1, AST} = binary_to_term(Abst), AST; find_ast([_|T]) -> find_ast(T). get_recs_fields(AST) -> [{R, lists:map(fun get_field/1, F)} || {attribute, _, record, {R, F}} <- AST]. %% there are 4 kinds of record field info; typed/untyped and initialized/not get_field({typed_record_field, RecordField, _}) -> get_field(RecordField); get_field({record_field, _, {atom, _, F}}) -> F; get_field({record_field, _, {atom, _, F}, _}) -> F. die(Str, Term) -> exit({gen_error, lists:flatten(io_lib:format(Str++": ~p", [Term]))}). -ifdef(USE_NOW). ts() -> erlang:now(). -else. ts() -> erlang:timestamp(). -endif. %%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%% -include_lib("eunit/include/eunit.hrl"). start_dist() -> os:cmd("epmd -daemon"), net_kernel:start([eunit_master, shortnames]). netload_test_() -> [fun netload0/0, fun netload1/0, fun netload2/0]. netload0() -> ?assertMatch(ok, assert_load(foo, bla)), ?assertMatch({module, redbug}, assert_load(nonode@nohost, redbug)). netload1() -> [start_dist() || node() =:= nonode@nohost], Opts = [{kill_if_fail, true}, {monitor_master, true}, {boot_timeout, 5}], SlaveName = eunit_inferior, {ok, Slave} = ct_slave:start(SlaveName, Opts), ?assertMatch(ok, assert_load(Slave, redbug_dist_eunit)), stop_slave(Slave, SlaveName). netload2() -> [start_dist() || node() =:= nonode@nohost], Opts = [{kill_if_fail, true}, {monitor_master, true}, {boot_timeout, 5}], SlaveName = eunit_inferior, {ok, Slave} = ct_slave:start(SlaveName, Opts), Ebin = filename:dirname(code:which(redbug)), Test = re:replace(re:replace(Ebin, "default", "test"), "ebin", "test"), code:add_patha(unicode:characters_to_list(Test)), ?assertMatch(ok, assert_load(Slave, redbug_dist_eunit)), stop_slave(Slave, SlaveName). -ifdef(OTP_RELEASE). stop_slave(Slave, _) -> {ok, Slave} = ct_slave:stop(Slave). -else. stop_slave(Slave, SlaveName) -> {ok, Slave} = ct_slave:stop(SlaveName). -endif.