%% %% %CopyrightBegin% %% %% Copyright Ericsson AB 2010-2011. All Rights Reserved. %% %% The contents of this file are subject to the Erlang Public License, %% Version 1.1, (the "License"); you may not use this file except in %% compliance with the License. You should have received a copy of the %% Erlang Public License along with this software. If not, it can be %% retrieved online at http://www.erlang.org/. %% %% Software distributed under the License is distributed on an "AS IS" %% basis, WITHOUT WARRANTY OF ANY KIND, either express or implied. See %% the License for the specific language governing rights and limitations %% under the License. %% %% %CopyrightEnd% %% -module(diameter_info). -export([usage/1, format/1, format/2, format/3, format/4, table/2, tables/1, tables/2, split/2, split/3, tab2list/1, modules/1, versions/1, version_info/1, attrs/2, compiled/1, procs/1, latest/1, list/1]). %% Support for rolling your own. -export([sep/0, sep/1, widest/1, p/1, p/3]). -compile({no_auto_import,[max/2]}). -export([collect/2]). -define(LONG_TIMEOUT, 30000). -define(VALUES(Rec), tl(tuple_to_list(Rec))). %%% ---------------------------------------------------------- %%% # usage(String) %%% ---------------------------------------------------------- usage(Usage) -> sep($+), io:format("+ ~p~n", [?MODULE]), io:format("~n~s~n~n", [compact(Usage)]), sep($+). %%% %%% The function format/3, for pretty-printing tables, comes in %%% several flavours. %%% %%% ---------------------------------------------------------- %%% # format(TableName, Fields, SplitFun) %%% %%% Input: TableName = atom() name of table. %%% %%% Fields = List of field names for the records maintained %%% in the specified table. Can be empty, in which %%% case entries are listed unadorned of field names %%% and SplitFun is unused. %%% | Integer, equivalent to a list with this many '' atoms. %%% | Arity 1 fun mapping a table entry to a Fields list %%% or a tuple {Fields, Values} of lists of the same %%% length. %%% %%% If Fields is a list then its length must be the same %%% as or one less than the size of the tuples contained %%% in the table. (The values printed then being those %%% in the tuple or record in question.) %%% %%% SplitFun = Arity 3 fun applied as %%% %%% SplitFun(TableName, Fields, Values) %%% %%% in order to obtain a tuple %%% %%% {Field, RestFields, Value, RestValues} %%% %%% for which Field/Value will be formatted on %%% STDOUT. (This is to allow a value to be %%% transformed before being output by returning a %%% new value and/or replacing the remainder of %%% the list.) The returned lists must have the %%% same length and Field here is an atom, '' causing %%% a value to be listed unadorned of the field name. %%% %%% Field can also be list of field names, in %%% which case Value must be a record of the %%% corresponding type. %%% %%% | Arity 2 fun applied as SplitFun(Fields, Values). %%% %%% Output: Count | undefined %%% %%% Count = Number of entries output. %%% %%% Description: Pretty-print records in a named table. %%% ---------------------------------------------------------- format(Table, Fields, SFun) when is_atom(Table), is_function(SFun, 2) -> ft(ets:info(Table), Table, SFun, Fields); format(Table, Fields, SFun) when is_atom(Table), is_function(SFun, 3) -> format(Table, Fields, fun(Fs,Vs) -> SFun(Table, Fs, Vs) end); %%% ---------------------------------------------------------- %%% # format(Recs, Fields, SplitFun) %%% %%% Input: Recs = list of records/tuples %%% Fields = As for format(Table, Fields, SplitFun), a table %%% entry there being a member of Recs. %%% SplitFun = Arity 3 fun applied as above but with the TableName %%% replaced by the first element of the records in %%% question. %%% | Arity 2 fun as for format/3. %%% %%% Output: length(Recs) %%% %%% Description: Pretty print records/tuples. %%% ---------------------------------------------------------- format(Recs, Fields, SFun) when is_list(Recs), is_function(SFun, 3) -> lists:foldl(fun(R,A) -> f(recsplit(SFun, R), 0, Fields, R, A) end, 0, Recs); format(Recs, Fields, SFun) when is_list(Recs), is_function(SFun, 2) -> lists:foldl(fun(R,A) -> f(SFun, 0, Fields, R, A) end, 0, Recs); %%% ---------------------------------------------------------- %%% # format(Tables, SplitFun, CollectFun) %%% %%% Input: Tables = list of {TableName, Fields}. %%% SplitFun = As for format(Table, Fields, SplitFun). %%% CollectFun = arity 1 fun mapping a table name to a list %%% of elements. A non-list can be returned to indicate %%% that the table in question doesn't exist. %%% %%% Output: Number of entries output. %%% %%% Description: Pretty-print records in a named tables as collected %%% from known nodes. Each table listing is preceeded by %%% a banner. %%% ---------------------------------------------------------- format(Tables, SFun, CFun) when is_list(Tables), is_function(CFun, 1) -> format_remote(Tables, SFun, rpc:multicall(nodes(known), ?MODULE, collect, [CFun, lists:map(fun({T,_}) -> T end, Tables)], ?LONG_TIMEOUT)); %%% ---------------------------------------------------------- %%% # format(LocalTables, RemoteTables, SplitFun, CollectFun) %%% # format(LocalTables, RemoteTables, SplitFun) %%% %%% Input: LocalTables = list of {TableName, Fields}. %%% | list of {TableName, Recs, Fields} %%% RemoteTable = list of {TableName, Fields}. %%% SplitFun, CollectFun = As for format(Table, CollectFun, SplitFun). %%% %%% Output: Number of entries output. %%% %%% Description: Pretty-print records in a named tables as collected %%% from local and remote nodes. Each table listing is %%% preceeded by a banner. %%% ---------------------------------------------------------- format(Local, Remote, SFun) -> format(Local, Remote, SFun, fun tab2list/1). format(Local, Remote, SFun, CFun) when is_list(Local), is_list(Remote), is_function(CFun, 1) -> format_local(Local, SFun) + format(Remote, SFun, CFun). %%% ---------------------------------------------------------- %%% # format(Tables, SplitFun) %%% ---------------------------------------------------------- format(Tables, SFun) when is_list(Tables), (is_function(SFun, 2) or is_function(SFun, 3)) -> format(Tables, SFun, fun tab2list/1); format(Tables, CFun) when is_list(Tables), is_function(CFun, 1) -> format(Tables, fun split/2, CFun). %%% ---------------------------------------------------------- %%% # format(Table|Tables) %%% ---------------------------------------------------------- format(Table) when is_atom(Table) -> format(Table, [], fun split/2); format(Tables) when is_list(Tables) -> format(Tables, fun split/2, fun tab2list/1). %%% ---------------------------------------------------------- %%% # split(TableName, Fields, Values) %%% %%% Description: format/3 SplitFun that does nothing special. %%% ---------------------------------------------------------- split([F|FT], [V|VT]) -> {F, FT, V, VT}. split(_, Fs, Vs) -> split(Fs, Vs). %%% ---------------------------------------------------------- %%% # tab2list(TableName) %%% %%% Description: format/4 CollectFun that extracts records from an %%% existing ets table. %%% ---------------------------------------------------------- tab2list(Table) -> case ets:info(Table) of undefined = No -> No; _ -> ets:tab2list(Table) end. list(Table) -> l(tab2list(Table)). l(undefined = No) -> No; l(List) when is_list(List) -> io:format("~p~n", [List]), length(List). %%% ---------------------------------------------------------- %%% # table(TableName, Fields) %%% ---------------------------------------------------------- table(Table, Fields) -> format(Table, Fields, fun split/2). %%% ---------------------------------------------------------- %%% # tables(LocalTables, RemoteTables) %%% ---------------------------------------------------------- tables(Local, Remote) -> format(Local, Remote, fun split/2). %%% ---------------------------------------------------------- %%% # tables(Tables) %%% ---------------------------------------------------------- tables(Tables) -> format(Tables, fun split/2). %%% ---------------------------------------------------------- %%% # modules(Prefix|Prefixes) %%% %%% Input: Prefix = atom() %%% %%% Description: Return the list of all loaded modules with the %%% specified prefix. %%% ---------------------------------------------------------- modules(Prefix) when is_atom(Prefix) -> lists:sort(mods(Prefix)); modules(Prefixes) when is_list(Prefixes) -> lists:sort(lists:flatmap(fun modules/1, Prefixes)). mods(Prefix) -> P = atom_to_list(Prefix), lists:filter(fun(M) -> lists:prefix(P, atom_to_list(M)) end, erlang:loaded()). %%% ---------------------------------------------------------- %%% # versions(Modules|Prefix) %%% %%% Output: Number of modules listed. %%% %%% Description: List the versions of the specified modules. %%% ---------------------------------------------------------- versions(Modules) -> {SysInfo, OsInfo, ModInfo} = version_info(Modules), sep(), print_sys_info(SysInfo), sep(), print_os_info(OsInfo), sep(), print_mod_info(ModInfo), sep(). %%% ---------------------------------------------------------- %%% # attrs(Modules|Prefix, Attr|FormatFun) %%% %%% Output: Number of modules listed. %%% %%% Description: List an attribute from module_info. %%% ---------------------------------------------------------- attrs(Modules, Attr) when is_atom(Attr) -> attrs(Modules, fun(W,M) -> attr(W, M, Attr, fun attr/1) end); attrs(Modules, Fun) when is_list(Modules) -> sep(), W = 2 + widest(Modules), N = lists:foldl(fun(M,A) -> Fun(W,M), A+1 end, 0, Modules), sep(), N; attrs(Prefix, Fun) -> attrs(modules(Prefix), Fun). %% attr/1 attr(T) when is_atom(T) -> atom_to_list(T); attr(N) when is_integer(N) -> integer_to_list(N); attr(V) -> case is_list(V) andalso lists:all(fun is_char/1, V) of true -> %% string V; false -> io_lib:format("~p", [V]) end. is_char(C) -> 0 =< C andalso C < 256. %% attr/4 attr(Width, Mod, Attr, VFun) -> io:format(": ~*s~s~n", [-Width, Mod, attr(Mod, Attr, VFun)]). attr(Mod, Attr, VFun) -> Key = key(Attr), try VFun(val(Attr, keyfetch(Attr, Mod:module_info(Key)))) catch _:_ -> "-" end. attr(Mod, Attr) -> attr(Mod, Attr, fun attr/1). key(time) -> compile; key(_) -> attributes. val(time, {_,_,_,_,_,_} = T) -> lists:flatten(io_lib:format("~p-~2..0B-~2..0B ~2..0B:~2..0B:~2..0B", tuple_to_list(T))); val(_, [V]) -> V. %%% ---------------------------------------------------------- %%% # compiled(Modules|Prefix) %%% %%% Output: Number of modules listed. %%% %%% Description: List the compile times of the specified modules. %%% ---------------------------------------------------------- compiled(Modules) when is_list(Modules) -> attrs(Modules, fun compiled/2); compiled(Prefix) -> compiled(modules(Prefix)). compiled(Width, Mod) -> io:format(": ~*s~19s ~s~n", [-Width, Mod, attr(Mod, time), opt(attr(Mod, date))]). opt("-") -> ""; opt(D) -> "(" ++ D ++ ")". %%% ---------------------------------------------------------- %%% # procs(Pred|Prefix|Prefixes|Pid|Pids) %%% %%% Input: Pred = arity 2 fun returning true|false when applied to a %%% pid and its process info. %%% %%% Output: Number of processes listed. %%% %%% Description: List process info for all local processes that test %%% true with the specified predicate. With the prefix %%% form, those processes that are either currently %%% executing in, started executing in, or have a %%% registered name with a specified prefix are listed. %%% With the pid forms, only those process that are local %%% are listed and those that are dead list only the pid %%% itself. %%% ---------------------------------------------------------- procs(Pred) when is_function(Pred, 2) -> procs(Pred, erlang:processes()); procs([]) -> 0; procs(Prefix) when is_atom(Prefix) -> procs(fun(_,I) -> info(fun pre1/2, I, atom_to_list(Prefix)) end); procs(Prefixes) when is_atom(hd(Prefixes)) -> procs(fun(_,I) -> info(fun pre/2, I, Prefixes) end); procs(Pid) when is_pid(Pid) -> procs(fun true2/2, [Pid]); procs(Pids) when is_list(Pids) -> procs(fun true2/2, Pids). true2(_,_) -> true. %% procs/2 procs(Pred, Pids) -> Procs = lists:foldl(fun(P,A) -> procs_acc(Pred, P, catch process_info(P), A) end, [], Pids), sep(0 < length(Procs)), lists:foldl(fun(T,N) -> p(T), sep(), N+1 end, 0, Procs). procs_acc(_, Pid, undefined, Acc) -> %% dead [[{pid, Pid}] | Acc]; procs_acc(Pred, Pid, Info, Acc) when is_list(Info) -> p_acc(Pred(Pid, Info), Pid, Info, Acc); procs_acc(_, _, _, Acc) -> Acc. p_acc(true, Pid, Info, Acc) -> [[{pid, Pid} | Info] | Acc]; p_acc(false, _, _, Acc) -> Acc. %% info/3 info(Pred, Info, T) -> lists:any(fun(I) -> i(Pred, I, T) end, Info). i(Pred, {K, {M,_,_}}, T) when K == current_function; K == initial_call -> Pred(M,T); i(Pred, {registered_name, N}, T) -> Pred(N,T); i(_,_,_) -> false. pre1(A, Pre) -> lists:prefix(Pre, atom_to_list(A)). pre(A, Prefixes) -> lists:any(fun(P) -> pre1(A, atom_to_list(P)) end, Prefixes). %%% ---------------------------------------------------------- %%% # latest(Modules|Prefix) %%% %%% Output: {Mod, {Y,M,D,HH,MM,SS}, Version} %%% %%% Description: Return the compile time of the most recently compiled %%% module from the specified non-empty list. The modules %%% are assumed to exist. %%% ---------------------------------------------------------- latest(Prefix) when is_atom(Prefix) -> latest(modules(Prefix)); latest([_|_] = Modules) -> {Mod, T} = hd(lists:sort(fun latest/2, lists:map(fun compile_time/1, Modules))), {Mod, T, app_vsn(Mod)}. app_vsn(Mod) -> keyfetch(app_vsn, Mod:module_info(attributes)). compile_time(Mod) -> T = keyfetch(time, Mod:module_info(compile)), {Mod, T}. latest({_,T1},{_,T2}) -> T1 > T2. %%% ---------------------------------------------------------- %%% version_info(Modules|Prefix) %%% %%% Output: {SysInfo, OSInfo, [ModInfo]} %%% %%% SysInfo = {Arch, Vers} %%% OSInfo = {Vers, {Fam, Name}} %%% ModInfo = {Vsn, AppVsn, Time, CompilerVsn} %%% ---------------------------------------------------------- version_info(Prefix) when is_atom(Prefix) -> version_info(modules(Prefix)); version_info(Mods) when is_list(Mods) -> {sys_info(), os_info(), [{M, mod_version_info(M)} || M <- Mods]}. mod_version_info(Mod) -> try Info = Mod:module_info(), [[Vsn], AppVsn] = get_values(attributes, [vsn, app_vsn], Info), [Ver, Time] = get_values(compile, [version, time], Info), [Vsn, AppVsn, Ver, Time] catch _:_ -> [] end. get_values(Attr, Keys, Info) -> As = proplists:get_value(Attr, Info), [proplists:get_value(K, As, "?") || K <- Keys]. sys_info() -> [A,V] = [chomp(erlang:system_info(K)) || K <- [system_architecture, system_version]], {A,V}. os_info() -> {os:version(), case os:type() of {_Fam, _Name} = T -> T; Fam -> {Fam, ""} end}. chomp(S) -> string:strip(S, right, $\n). print_sys_info({Arch, Ver}) -> io:format("System info:~n" " architecture : ~s~n" " version : ~s~n", [Arch, Ver]). print_os_info({Vsn, {Fam, Name}}) -> io:format("OS info:~n" " family : ~s ~s~n" " version : ~s~n", [str(Fam), bkt(str(Name)), vsn(Vsn)]). print_mod_info(Mods) -> io:format("Module info:~n", []), lists:foreach(fun print_mod/1, Mods). print_mod({Mod, []}) -> io:format(" ~w:~n", [Mod]); print_mod({Mod, [Vsn, AppVsn, Ver, {Year, Month, Day, Hour, Min, Sec}]}) -> Time = io_lib:format("~w-~2..0w-~2..0w ~2..0w:~2..0w:~2..0w", [Year, Month, Day, Hour, Min, Sec]), io:format(" ~w:~n" " vsn : ~s~n" " app_vsn : ~s~n" " compiled : ~s~n" " compiler : ~s~n", [Mod, str(Vsn), str(AppVsn), Time, Ver]). str(A) when is_atom(A) -> atom_to_list(A); str(S) when is_list(S) -> S; str(T) -> io_lib:format("~p", [T]). bkt("" = S) -> S; bkt(S) -> [$[, S, $]]. vsn(T) when is_tuple(T) -> case [[$., integer_to_list(N)] || N <- tuple_to_list(T)] of [[$.,S] | Rest] -> [S | Rest]; [] = S -> S end; vsn(T) -> str(T). %%% ---------------------------------------------------------- %%% ---------------------------------------------------------- %% p/1 p(Info) -> W = 2 + widest([K || {K,_} <- Info]), lists:foreach(fun({K,V}) -> p(W,K,V) end, Info). p(Width, Key, Value) -> io:format(": ~*s: ~p~n", [-Width, Key, Value]). %% sep/[01] sep() -> sep($#). sep(true) -> sep(); sep(false) -> ok; sep(Ch) -> io:format("~c~65c~n", [Ch, $-]). %% widest/1 widest(List) -> lists:foldl(fun widest/2, 0, List). widest(T, Max) when is_atom(T) -> widest(atom_to_list(T), Max); widest(T, Max) when is_integer(T) -> widest(integer_to_list(T), Max); widest(T, Max) when is_list(T) -> %% string max(length(T), Max). pt(T) -> io:format(": ~p~n", [T]). recsplit(SFun, Rec) -> fun(Fs,Vs) -> SFun(element(1, Rec), Fs, Vs) end. max(A, B) -> if A > B -> A; true -> B end. keyfetch(Key, List) -> {Key,V} = lists:keyfind(Key, 1, List), V. %% ft/4 ft(undefined = No, _, _, _) -> No; ft(_, Table, SFun, Fields) -> ets:foldl(fun(R,A) -> f(SFun, 0, Fields, R, A) end, 0, Table). %% f/5 f(SFun, Width, Fields, Rec, Count) -> ff(SFun, Width, fields(Fields, Rec), Rec, Count). ff(SFun, Width, Fields, Rec, Count) -> sep(0 == Count), f(SFun, Width, Fields, Rec), sep(), Count+1. fields(N, _) when is_integer(N), N >= 0 -> lists:duplicate(N, ''); %% list values unadorned fields(Fields, R) when is_function(Fields, 1) -> fields(Fields(R), R); fields({Fields, Values} = T, _) when length(Fields) == length(Values) -> T; fields(Fields, _) when is_list(Fields) -> Fields. %% list field/value pairs, or tuples if [] %% f/4 %% Empty fields list: just print the entry. f(_, _, [], Rec) when is_tuple(Rec) -> pt(Rec); %% Otherwise list field names/values. f(SFun, Width, {Fields, Values}, _) -> f(SFun, Width, Fields, Values); f(SFun, Width, Fields, Rec) when is_tuple(Rec) -> f(SFun, Width, Fields, values(Fields, Rec)); f(_, _, [], []) -> ok; f(SFun, Width, [HF | _] = Fields, Values) -> {F, FT, V, VT} = SFun(Fields, Values), if is_list(F) -> %% V is a record break($>, HF), f(SFun, Width, F, values(F,V)), break($<, HF), f(SFun, Width, FT, VT); F == '' -> %% no field name: just list value pt(V), f(SFun, Width, FT, VT); true -> %% list field/value. W = max(Width, 1 + widest(Fields)), p(W, F, V), f(SFun, W, FT, VT) end. values(Fields, Rec) when length(Fields) == size(Rec) - 1 -> ?VALUES(Rec); values(Fields, T) when length(Fields) == size(T) -> tuple_to_list(T). %% format_local/2 format_local(Tables, SFun) -> lists:foldl(fun(T,A) -> fl(SFun, T, A) end, 0, Tables). fl(SFun, {Table, Recs, Fields}, Count) -> sep(), io:format("# ~p~n", [Table]), N = fmt(Recs, Fields, SFun), sep(0 == N), Count + N; fl(SFun, {Table, Fields}, Count) -> fl(SFun, {Table, Table, Fields}, Count). %% fmt/3 fmt(T, Fields, SFun) -> case format(T, Fields, SFun) of undefined -> 0; N -> N end. %% break/2 break(C, T) -> io:format("~c ~p~n", [C, T]). %% collect/2 %% %% Output: {[{TableName, Recs}, ...], node()} collect(CFun, TableNames) -> {lists:foldl(fun(N,A) -> c(CFun, N, A) end, [], TableNames), node()}. c(CFun, TableName, Acc) -> case CFun(TableName) of Recs when is_list(Recs) -> [{TableName, Recs} | Acc]; _ -> Acc end. %% format_remote/3 format_remote(Tables, SFun, {Replies, BadNodes}) -> N = lists:foldl(fun(T,A) -> fr(Tables, SFun, T, A) end, 0, Replies), sep(0 == N andalso [] /= BadNodes), lists:foreach(fun(Node) -> io:format("# no reply from ~p~n", [Node]) end, BadNodes), sep([] /= BadNodes), N. fr(Tables, SFun, {List, Node}, Count) when is_list(List) -> %% guard against {badrpc, Reason} lists:foldl(fun({T,Recs}, C) -> fr(Tables, SFun, Node, T, Recs,C) end, Count, List); fr(_, _, _, Count) -> Count. fr(Tables, SFun, Node, Table, Recs, Count) -> Fields = keyfetch(Table, Tables), sep(), io:format("# ~p@~p~n", [Table, Node]), N = format(Recs, Fields, tblsplit(SFun, Table)), sep(0 == N), Count + N. tblsplit(SFun, Table) when is_function(SFun, 3) -> fun(Fs,Vs) -> SFun(Table, Fs, Vs) end; tblsplit(SFun, _) when is_function(SFun, 2) -> SFun. %% compact/1 %% %% Strip whitespace from both ends of a string. compact(Str) -> compact(Str, true). compact([Ch|Rest], B) when Ch == $\n; Ch == $ ; Ch == $\t; Ch == $\v; Ch == $\r -> compact(Rest, B); compact(Str, false) -> Str; compact(Str, true) -> lists:reverse(compact(lists:reverse(Str), false)).