1: %%
    2: %% %CopyrightBegin%
    3: %%
    4: %% Copyright Ericsson AB 2003-2012. All Rights Reserved.
    5: %%
    6: %% The contents of this file are subject to the Erlang Public License,
    7: %% Version 1.1, (the "License"); you may not use this file except in
    8: %% compliance with the License. You should have received a copy of the
    9: %% Erlang Public License along with this software. If not, it can be
   10: %% retrieved online at http://www.erlang.org/.
   11: %%
   12: %% Software distributed under the License is distributed on an "AS IS"
   13: %% basis, WITHOUT WARRANTY OF ANY KIND, either express or implied. See
   14: %% the License for the specific language governing rights and limitations
   15: %% under the License.
   16: %%
   17: %% %CopyrightEnd%
   18: %%
   19: 
   20: -module(crashdump_viewer_SUITE).
   21: 
   22: %% Test functions
   23: -export([all/0, suite/0,groups/0,init_per_group/2,end_per_group/2,
   24: 	 translate/1,start/1,fini/1,load_file/1,
   25: 	 non_existing/1,not_a_crashdump/1,old_crashdump/1]).
   26: -export([init_per_suite/1, end_per_suite/1]).
   27: -export([init_per_testcase/2, end_per_testcase/2]).
   28: 
   29: -include_lib("common_test/include/ct.hrl").
   30: -include("test_server_line.hrl").
   31: -include_lib("kernel/include/file.hrl").
   32: 
   33: -include_lib("stdlib/include/ms_transform.hrl").
   34: 
   35: -define(default_timeout, ?t:minutes(30)).
   36: -define(sl_alloc_vsns,[r9b]).
   37: -define(failed_file,"failed-cases.txt").
   38: 
   39: init_per_testcase(_Case, Config) ->
   40:     DataDir = ?config(data_dir,Config),
   41:     Fs = filelib:wildcard(filename:join(DataDir,"*translated*")),
   42:     lists:foreach(fun(F) -> file:delete(F) end,Fs),
   43:     catch crashdump_viewer:stop(),
   44:     Dog = ?t:timetrap(?default_timeout),
   45:     [{watchdog, Dog}|Config].
   46: end_per_testcase(Case, Config) ->
   47:     Dog=?config(watchdog, Config),
   48:     ?t:timetrap_cancel(Dog),
   49:     case ?config(tc_status,Config) of
   50: 	ok ->
   51: 	    ok;
   52: 	_Fail ->
   53: 	    File = filename:join(?config(data_dir,Config),?failed_file),
   54: 	    {ok,Fd}=file:open(File,[append]),
   55: 	    file:write(Fd,io_lib:format("~w.~n",[Case])),
   56: 	    file:close(Fd)
   57:     end,
   58:     ok.
   59: 
   60: suite() -> [{ct_hooks,[ts_install_cth]}].
   61: 
   62: all() -> 
   63:     [translate, load_file, non_existing, not_a_crashdump,
   64:      old_crashdump].
   65: 
   66: groups() -> 
   67:     [].
   68: 
   69: init_per_group(_GroupName, Config) ->
   70:     Config.
   71: 
   72: end_per_group(_GroupName, Config) ->
   73:     Config.
   74: 
   75: 
   76: init_per_suite(doc) ->
   77:     ["Create a lot of crashdumps which can be used in the testcases below"];
   78: init_per_suite(Config) when is_list(Config) ->
   79:     Dog = ?t:timetrap(?default_timeout),
   80:     delete_saved(Config),
   81:     application:start(inets), % will be using the http client later
   82:     httpc:set_options([{ipfamily,inet6fb4}]),
   83:     DataDir = ?config(data_dir,Config),
   84:     Rels = [R || R <- [r14b,r15b], ?t:is_release_available(R)] ++ [current],
   85:     io:format("Creating crash dumps for the following releases: ~p", [Rels]),
   86:     AllDumps = create_dumps(DataDir,Rels),
   87:     ?t:timetrap_cancel(Dog),
   88:     [{dumps,AllDumps}|Config].
   89: 
   90: delete_saved(Config) ->
   91:     DataDir = ?config(data_dir,Config),
   92:     file:delete(filename:join(DataDir,?failed_file)),
   93:     SaveDir = filename:join(DataDir,"save"),
   94:     Dumps = filelib:wildcard(filename:join(SaveDir,"*")),
   95:     lists:foreach(fun(F) -> file:delete(F) end, Dumps),
   96:     file:del_dir(SaveDir),
   97:     ok.
   98: 
   99: 
  100: translate(suite) ->
  101:     [];
  102: translate(doc) ->
  103:     ["Test that crash dumps from OTP R9B can be translated"];
  104: translate(Config) when is_list(Config) ->
  105:     DataDir = ?config(data_dir,Config),
  106:     OutFile = filename:join(DataDir,"translated"),
  107:     
  108:     R9BFiles = filelib:wildcard(filename:join(DataDir,"r9b_dump.*")),
  109:     AllFiles = R9BFiles,
  110:     lists:foreach(
  111:       fun(File) ->
  112: 	      io:format("Translating file: ~s~n",[File]),
  113: 	      ok = crashdump_translate:old2new(File,OutFile),
  114: 	      check_result(File,OutFile)
  115:       end,
  116:       AllFiles),
  117:     ok.
  118: 
  119: start(suite) ->
  120:     [];
  121: start(doc) ->
  122:     ["Test start and stop of the Crashdump Viewer"];
  123: start(Config) when is_list(Config) ->
  124:     %% Set a much shorter timeout here... We don't have all the time in world.
  125:     AngryDog = ?t:timetrap(?t:seconds(30)),
  126:     Port = start_cdv(),
  127:     true = is_pid(whereis(crashdump_viewer_server)),
  128:     true = is_pid(whereis(web_tool)),
  129:     Html = contents(Port,"start_page"),
  130:     "Welcome to the Web BasedErlang Crash Dump Analyser" = strip(Html),
  131:     ok = crashdump_viewer:stop(),
  132:     timer:sleep(10), % give some time to stop
  133:     undefined = whereis(crashdump_viewer_server),
  134:     undefined = whereis(web_tool),
  135:     Url = cdv_url(Port,"start_page"),
  136:     {error,_} = httpc:request(Url),
  137: %    exit(whereis(httpc_manager),kill),
  138:     ?t:timetrap_cancel(AngryDog),
  139:     ok.
  140: 
  141: fini(Config) when is_list(Config) ->
  142:     ok.
  143: 
  144: load_file(suite) ->
  145:     [];
  146: load_file(doc) ->
  147:     ["Load files into the tool and view all pages"];
  148: load_file(Config) when is_list(Config) ->
  149:     case ?t:is_debug() of
  150: 	true ->
  151: 	    {skip,"Debug-compiled emulator -- far too slow"};
  152: 	false ->
  153: 	    load_file_1(Config)
  154:     end.
  155: 
  156: 
  157: load_file_1(Config) ->
  158:     DataDir = ?config(data_dir,Config),
  159:     Port = start_cdv(),
  160: 
  161:     AllFiles = filelib:wildcard(filename:join(DataDir,"r*_dump.*")),
  162:     lists:foreach(
  163:       fun(File) ->
  164: 	      browse_file(Port,File),
  165: 	      special(Port,File)
  166:       end,
  167:       AllFiles),
  168:     ok = crashdump_viewer:stop().
  169: 
  170: non_existing(suite) ->
  171:     [];
  172: non_existing(doc) ->
  173:     ["Try to load nonexisting file"];
  174: non_existing(Config) when is_list(Config) ->
  175:     Port = start_cdv(),
  176:     Url = "http://localhost:"++Port++"/cdv_erl/crashdump_viewer/read_file",
  177:     Html = request_sync(post,{Url,[],[],"path=nonexistingfile"}),
  178:     "Please wait..." = title(Html),
  179:     "An error occured:nonexistingfile is not an Erlang crash dump" = 
  180: 	strip(wait(10,Port,"redirect")),
  181:     ok = crashdump_viewer:stop().
  182: 
  183: old_crashdump(doc) ->
  184:     ["Try to load nonexisting file"];
  185: old_crashdump(Config) when is_list(Config) ->
  186:     Port = start_cdv(),
  187:     DataDir = ?config(data_dir, Config),
  188:     OldCrashDump = filename:join(DataDir, "old_format.dump"),
  189:     Url = "http://localhost:"++Port++"/cdv_erl/crashdump_viewer/read_file",
  190:     Html = request_sync(post,{Url,[],[],"path="++OldCrashDump}),
  191:     "Please wait..." = title(Html),
  192:     Str = "An error occured:The crashdump "++OldCrashDump++
  193: 	" is in the pre-R10B format, which is no longer supported.",
  194:     Str = strip(wait(10,Port,"redirect")),
  195:     ok = crashdump_viewer:stop().
  196: 
  197: 
  198: not_a_crashdump(suite) ->
  199:     [];
  200: not_a_crashdump(doc) ->
  201:     ["Try to load a file which is not an erlang crashdump"];
  202: not_a_crashdump(Config) when is_list(Config) ->
  203:     Port = start_cdv(),
  204:     NoCrashdump = code:which(?MODULE),
  205:     Url = "http://localhost:"++Port++"/cdv_erl/crashdump_viewer/read_file",
  206:     Html = request_sync(post,{Url,[],[],"path="++NoCrashdump}),
  207:     "Please wait..." = title(Html),
  208:     Str = "An error occured:"++NoCrashdump++" is not an Erlang crash dump",
  209:     Str = strip(wait(10,Port,"redirect")),
  210:     ok = crashdump_viewer:stop(),
  211: %    exit(whereis(httpc_manager),kill),
  212:     ok.
  213:     
  214: 
  215: 
  216: end_per_suite(doc) ->
  217:     ["Remove generated crashdumps"];
  218: end_per_suite(Config) when is_list(Config) ->
  219:     Dumps = ?config(dumps,Config),
  220:     DataDir = ?config(data_dir,Config),
  221:     FailedFile = filename:join(DataDir,?failed_file),
  222:     case filelib:is_file(FailedFile) of
  223: 	true ->
  224: 	    SaveDir = filename:join(DataDir,"save"),
  225: 	    file:make_dir(SaveDir),
  226: 	    file:copy(FailedFile,filename:join(SaveDir,?failed_file)),
  227: 	    lists:foreach(
  228: 	      fun(CD) ->
  229: 		      File = filename:basename(CD),
  230: 		      New = filename:join(SaveDir,File),
  231: 		      file:copy(CD,New)
  232: 	      end, Dumps);
  233: 	false ->
  234: 	    ok
  235:     end,
  236:     file:delete(FailedFile),
  237:     lists:foreach(fun(CD) -> ok = file:delete(CD) end,Dumps),
  238:     lists:keydelete(dumps,1,Config).
  239: 
  240: 
  241: %%%-----------------------------------------------------------------
  242: %%% Internal
  243: start_cdv() ->
  244:     ?t:capture_start(),
  245:     ok = crashdump_viewer:start(),
  246:     "WebTool is available at http://localhost:" ++ Where = 
  247: 	lists:flatten(?t:capture_get()),
  248:     ?t:capture_stop(),
  249:     [Port|_] = string:tokens(Where,"/"),
  250:     Port.
  251: 
  252: 
  253: check_result(File,OutFile) ->
  254:     {ok,#file_info{size=FS}} = file:read_file_info(File),
  255:     {ok,#file_info{size=OFS}} = file:read_file_info(OutFile),
  256:     Rel = 
  257: 	if OFS > 0 -> FS/OFS;
  258: 	   true -> 1.25
  259: 	end,
  260:     if Rel>0.75, Rel<1.25 -> ok;
  261:        true -> ?t:fail({unreasonable_size,File,FS,OFS})
  262:     end,
  263:     {ok,Fd} = file:open(OutFile,[read]),
  264:     "=erl_crash_dump:0.0\n" = io:get_line(Fd,''),
  265:     case is_truncated(File) of
  266: 	true ->
  267: 	    ok;
  268: 	false ->
  269: 	    {ok,_} = file:position(Fd,{eof,-5}),
  270: 	    case io:get_line(Fd,'') of
  271: 		"=end\n"  -> ok;
  272: 		Other -> ?t:fail({truncated,File,Other})
  273: 	    end
  274:     end,
  275:     ok = file:close(Fd).
  276: 
  277: 
  278: %% Read a page and check that the page title matches Title
  279: contents(Port,Link) ->
  280:     Url = cdv_url(Port,Link),
  281:     request_sync(get,{Url,[]}).
  282: 
  283: cdv_url(Port,Link) ->
  284:     "http://localhost:" ++ Port ++ "/cdv_erl/crashdump_viewer/" ++ Link.
  285: 
  286: request_sync(Method,HTTPReqCont) ->
  287:     case httpc:request(Method,
  288: 		      HTTPReqCont,
  289: 		      [{timeout,30000}],
  290: 		      [{full_result, false}]) of
  291: 	{ok,{200,Html}} ->
  292: 	    Html;
  293: 	{ok,{Code,Html}} ->
  294: 	    io:format("~s\n", [Html]),
  295: 	    io:format("Received ~w from httpc:request(...) with\nMethod=~w\n"
  296: 		      "HTTPReqCont=~p\n",
  297: 		      [Code,Method,HTTPReqCont]),
  298: 	    ?t:fail();
  299: 	Other ->
  300: 	    io:format(
  301: 	      "Received ~w from httpc:request(...) with\nMethod=~w\n"
  302: 	      "HTTPReqCont=~p\n",
  303: 	      [Other,Method,HTTPReqCont]),
  304: 	    ?t:fail()
  305:     end.
  306: 
  307: 
  308: 
  309: 
  310: strip([$<|Html]) ->
  311:     strip(drop_tag(Html));
  312: strip([$\n|Html]) -> 
  313:     strip(Html);
  314: strip([X|Html]) ->
  315:     [X|strip(Html)];
  316: strip([]) ->
  317:     [].
  318: drop_tag([$>|Html]) ->
  319:     Html;
  320: drop_tag([_|Html]) ->
  321:     drop_tag(Html).
  322: 
  323: title(Port,Link,Title) ->
  324:     Html = contents(Port,Link),
  325:     Title = title(Html).
  326: 
  327: wait(0,_Port,Link) ->
  328:     ?t:fail({wait,Link,timeout});
  329: wait(Time,Port,Link) ->
  330:     Html = contents(Port,Link),
  331:     case title(Html) of
  332: 	"Please wait..." ->
  333: 	    timer:sleep(1000),
  334: 	    wait(Time-1,Port,Link);
  335: 	_Title ->
  336: 	    Html
  337:     end.
  338: 
  339: title([$<,$T,$I,$T,$L,$E,$>|Html]) ->
  340:     title_end(Html);
  341: title([_|Html]) ->
  342:     title(Html);
  343: title([]) ->
  344:     [].
  345: 
  346: title_end([$<,$/,$T,$I,$T,$L,$E,$>|_]) ->
  347:     [];
  348: title_end([X|Html]) ->
  349:     [X|title_end(Html)].
  350: 
  351: 
  352: %%%-----------------------------------------------------------------
  353: %%% General check of what is displayed for a dump
  354: browse_file(Port,File) ->
  355:     io:format("Browsing file: ~s~n",[File]),
  356: 
  357:     %% The page where a filename can be entered
  358:     title(Port,"read_file_frame","Read File"),
  359: 
  360:     %% Load a file
  361:     Url = "http://localhost:"++Port++"/cdv_erl/crashdump_viewer/read_file",
  362:     Html = request_sync(post,{Url,[],[],"path="++File}),
  363:     "Please wait..." = title(Html),
  364:     "Crashdump Viewer Start Page" = title(wait(10,Port,"start_page")),
  365:     
  366:     %% The frame with the initial information for a dump
  367:     title(Port,"initial_info_frame","General Information"),
  368:     
  369:     %% Topmost frame of the page
  370:     FilenameFrame = contents(Port,"filename_frame"),
  371:     Match = "FilenameCrashdump currently viewed:" ++ File,
  372:     true = lists:prefix(Match,strip(FilenameFrame)),
  373:     
  374:     %% Toggle a menu item and check that it explodes/collapses
  375:     title(Port,"menu_frame","Menu"),
  376:     exploded = toggle_menu(Port),
  377:     collapsed = toggle_menu(Port),
  378:     
  379:     %% Open each page in menu and check that correct title is shown
  380:     title(Port,"general_info","General Information"),
  381:     title(Port,"processes","Process Information"),
  382:     title(Port,"sort_procs?sort=state","Process Information"),
  383:     title(Port,"sort_procs?sort=state","Process Information"),
  384:     title(Port,"sort_procs?sort=pid","Process Information"),
  385:     title(Port,"sort_procs?sort=pid","Process Information"),
  386:     title(Port,"sort_procs?sort=msg_q_len","Process Information"),
  387:     title(Port,"sort_procs?sort=msg_q_len","Process Information"),
  388:     title(Port,"sort_procs?sort=reds","Process Information"),
  389:     title(Port,"sort_procs?sort=reds","Process Information"),
  390:     title(Port,"sort_procs?sort=mem","Process Information"),
  391:     title(Port,"sort_procs?sort=mem","Process Information"),
  392:     title(Port,"sort_procs?sort=name","Process Information"),
  393:     title(Port,"sort_procs?sort=name","Process Information"),
  394:     title(Port,"sort_procs?sort=init_func","Process Information"),
  395:     title(Port,"sort_procs?sort=init_func","Process Information"),
  396:     title(Port,"ports","Port Information"),
  397:     title(Port,"ets_tables","ETS Table Information"),
  398:     title(Port,"timers","Timer Information"),
  399:     title(Port,"fun_table","Fun Information"),
  400:     title(Port,"atoms","Atoms"),
  401:     title(Port,"dist_info","Distribution Information"),
  402:     title(Port,"loaded_modules","Loaded Modules Information"),
  403:     title(Port,"hash_tables","Hash Table Information"),
  404:     title(Port,"index_tables","Index Table Information"),
  405:     title(Port,"memory","Memory Information"),
  406:     title(Port,"allocated_areas","Information about allocated areas"),
  407:     title(Port,"allocator_info","Allocator Information"),
  408:     
  409:     case is_truncated(File) of
  410: 	true ->
  411: 	    ok;
  412: 	_ ->
  413: 	    proc_details(Port),
  414: 	    port_details(Port),
  415: 	    title(Port,"loaded_mod_details?mod=kernel","kernel")
  416:     end,
  417:     
  418:     ok.
  419: 
  420: 
  421: special(Port,File) ->
  422:     case filename:extension(File) of
  423: 	".full_dist" ->
  424: 	    contents(Port,"processes"),
  425: 	    AllProcs = contents(Port,"sort_procs?sort=name"),
  426: 
  427: 	    %% I registered a process as aaaaaaaa in the full_dist dumps 
  428: 	    %% to make sure it will be the first in the list when sorted
  429: 	    %% on names. There are some special data here, so I'll thoroughly
  430: 	    %% read the process details for this process. Other processes
  431: 	    %% are just briefly traversed.
  432: 	    {Pid,Rest1} = get_first_process(AllProcs),
  433: 	    
  434: 	    ProcDetails = contents(Port,"proc_details?pid=" ++ Pid),
  435: 	    ProcTitle = "Process " ++ Pid,
  436: 	    ProcTitle = title(ProcDetails),
  437: 	    title(Port,"ets_tables?pid="++Pid,"ETS Tables for Process "++Pid),
  438: 	    title(Port,"timers?pid="++Pid,"Timers for Process "++Pid),
  439: 
  440: 	    case filename:basename(File) of
  441: 		"r10b_dump.full_dist" ->
  442: 		    [MsgQueueLink,DictLink,StackDumpLink] = 
  443: 			expand_memory_links(ProcDetails),
  444: 		    MsgQueue = contents(Port,MsgQueueLink),
  445: 		    "MsgQueue" = title(MsgQueue),
  446: 		    title(Port,DictLink,"Dictionary"),
  447: 		    title(Port,StackDumpLink,"StackDump"),
  448: 		    
  449: 		    ExpandBinaryLink = expand_binary_link(MsgQueue),
  450: 		    title(Port,ExpandBinaryLink,"Expanded binary"),
  451: 		    lookat_all_pids(Port,Rest1);
  452: 		_ ->
  453: 		    ok
  454: 	    end;
  455: 	".strangemodname" ->
  456: 	    AllMods = contents(Port,"loaded_modules"),
  457: 	    open_all_modules(Port,AllMods),
  458: 	    ok;
  459: 	%%! No longer needed - all atoms are shown on one page!!
  460: 	%% ".250atoms" ->
  461: 	%%     Html1 = contents(Port,"atoms"),
  462: 	%%     NextLink1 = next_link(Html1),
  463: 	%%     "Atoms" = title(Html1),
  464: 	%%     Html2 = contents(Port,NextLink1),
  465: 	%%     NextLink2 = next_link(Html2),
  466: 	%%     "Atoms" = title(Html2),
  467: 	%%     Html3 = contents(Port,NextLink2),
  468: 	%%     "" = next_link(Html3),
  469: 	%%     "Atoms" = title(Html3);
  470: 	_ ->
  471: 	    ok
  472:     end,
  473:     case filename:basename(File) of
  474: 	"r10b_dump." ++ _ ->
  475: 	    lookat_all_pids(Port,contents(Port,"processes"));
  476: 	"r11b_dump." ++ _ ->
  477: 	    lookat_all_pids(Port,contents(Port,"processes"));
  478: 	_ ->
  479: 	    ok
  480:     end,
  481:     ok.
  482: 
  483: 
  484: lookat_all_pids(Port,Pids) ->
  485:     case get_first_process(Pids) of
  486: 	{Pid,Rest} ->
  487: 	    ProcDetails = contents(Port,"proc_details?pid=" ++ Pid),
  488: 	    ProcTitle = "Process " ++ Pid,
  489: 	    ProcTitle = title(ProcDetails),
  490: 	    title(Port,"ets_tables?pid="++Pid,"ETS Tables for Process "++Pid),
  491: 	    title(Port,"timers?pid="++Pid,"Timers for Process "++Pid),
  492: 	    
  493: 	    MemoryLinks = expand_memory_links(ProcDetails),
  494: 	    lists:foreach(
  495: 	      fun(Link) ->
  496: 		      Cont = contents(Port,Link),
  497: 		      true = lists:member(title(Cont),
  498: 					  ["MsgQueue",
  499: 					   "Dictionary",
  500: 					   "StackDump"])
  501: 	      end,
  502: 	      MemoryLinks),
  503: 	    lookat_all_pids(Port,Rest);
  504: 	false ->
  505: 	    ok
  506:     end.
  507: 
  508: 
  509: get_first_process([]) ->
  510:     false;
  511: get_first_process(Html) ->
  512:     case Html of
  513: 	"<TD><A HREF=\"./proc_details?pid=" ++ Rest ->
  514: 	    {string:sub_word(Rest,1,$"),Rest};
  515: 	[_H|T] ->
  516: 	    get_first_process(T)
  517:     end.
  518: 	
  519: expand_memory_links(Html) ->
  520:     case Html of
  521: 	"<B>MsgQueue</B></TD><TD COLSPAN=3><A HREF=\"./" ++ Rest ->
  522: 	    [string:sub_word(Rest,1,$")|expand_memory_links(Rest)];
  523: 	"<B>Dictionary</B></TD><TD COLSPAN=3><A HREF=\"./" ++ Rest ->
  524: 	    [string:sub_word(Rest,1,$")|expand_memory_links(Rest)];
  525: 	"<B>StackDump</B></TD><TD COLSPAN=3><A HREF=\"./" ++ Rest ->
  526: 	    [string:sub_word(Rest,1,$")];
  527: 	[_H|T] ->
  528: 	    expand_memory_links(T);
  529: 	[] ->
  530: 	    []
  531:     end.
  532: 
  533: expand_binary_link(Html) ->
  534:     case Html of
  535: 	"<A HREF=\"./expand_binary?pos=" ++ Rest ->
  536: 	    "expand_binary?pos=" ++ string:sub_word(Rest,1,$");
  537: 	[_H|T] ->
  538: 	    expand_binary_link(T)
  539:     end.
  540: 
  541: open_all_modules(Port,Modules) ->
  542:     case get_first_module(Modules) of
  543: 	{Module,Rest} ->
  544: 	    ModuleDetails = contents(Port,"loaded_mod_details?mod=" ++ Module),
  545:             ModTitle = http_uri:decode(Module),
  546: 	    ModTitle = title(ModuleDetails),
  547: 	    open_all_modules(Port,Rest);
  548: 	false ->
  549: 	    ok
  550:     end.
  551: 
  552: get_first_module([]) ->
  553:     false;
  554: get_first_module(Html) ->
  555:     case Html of
  556: 	"<TD><A HREF=\"loaded_mod_details?mod=" ++ Rest ->
  557: 	    {string:sub_word(Rest,1,$"),Rest};
  558: 	 [_H|T] ->
  559: 	    get_first_module(T)
  560:     end.
  561: 
  562: %% next_link(Html) ->
  563: %%     case Html of
  564: %% 	"<A HREF=\"./next?pos=" ++ Rest ->
  565: %% 	    "next?pos=" ++ string:sub_word(Rest,1,$");
  566: %% 	[_H|T] ->
  567: %% 	    next_link(T);
  568: %% 	[] ->
  569: %% 	    []
  570: %%     end.
  571: 
  572: 
  573: 
  574: toggle_menu(Port) ->
  575:     Html = contents(Port,"toggle?index=4"),
  576:     check_toggle(Html).
  577: 
  578: check_toggle(Html) ->
  579:     case Html of
  580: 	"<A HREF=\"./toggle?index=4\"><IMG SRC=\"/crashdump_viewer/collapsd.gif\"" ++ _ ->
  581: 	    collapsed;
  582: 	"<A HREF=\"./toggle?index=4\"><IMG SRC=\"/crashdump_viewer/exploded.gif\"" ++ _ ->
  583: 	    exploded;
  584: 	[_H|T] ->
  585: 	    check_toggle(T)
  586:     end.
  587: 	    
  588: 
  589: proc_details(Port) ->
  590:     ProcDetails = contents(Port,"proc_details?pid=<0.0.0>"),
  591:     "Process <0.0.0>" = title(ProcDetails),
  592: 
  593:     ExpandLink = expand_link(ProcDetails),
  594:     title(Port,ExpandLink,"StackDump"),
  595: 
  596:     Unknown = contents(Port,"proc_details?pid=<0.9999.0>"),
  597:     "Could not find process: <0.9999.0>" = title(Unknown).
  598: 
  599: expand_link(Html) ->
  600:     case Html of
  601: 	"<B>StackDump</B></TD><TD COLSPAN=3><A HREF=\"./" ++ Rest ->
  602: 	    string:sub_word(Rest,1,$");
  603: 	[_H|T] ->
  604: 	    expand_link(T)
  605:     end.
  606: 
  607: 
  608: port_details(Port) ->
  609:     Port0 = contents(Port,"port?port=Port<0.0>"),
  610:     Port1 = contents(Port,"port?port=Port<0.1>"),
  611:     case title(Port0) of
  612: 	"#Port<0.0>" -> % R16 or later
  613: 	    "Could not find port: #Port<0.1>" = title(Port1);
  614: 	"Could not find port: #Port<0.0>" -> % R15 or earlier
  615: 	    "#Port<0.1>" = title(Port1)
  616:     end.
  617: 
  618: is_truncated(File) ->
  619:     case filename:extension(filename:rootname(File)) of
  620: 	".trunc" -> true;
  621: 	_ -> false
  622:     end.
  623: 
  624: 
  625: %%%-----------------------------------------------------------------
  626: %%% 
  627: create_dumps(DataDir,Rels) ->
  628:     create_dumps(DataDir,Rels,[]).
  629: create_dumps(DataDir,[Rel|Rels],Acc) ->
  630:     Fun = fun() -> do_create_dumps(DataDir,Rel) end,
  631:     Pa = filename:dirname(code:which(?MODULE)),
  632:     {SlAllocDumps,Dumps,DosDump} = 
  633: 	?t:run_on_shielded_node(Fun, compat_rel(Rel) ++ "-pa \"" ++ Pa ++ "\""),
  634:     create_dumps(DataDir,Rels,SlAllocDumps ++ Dumps ++ Acc ++ DosDump);
  635: create_dumps(_DataDir,[],Acc) ->
  636:     Acc.
  637: 
  638: do_create_dumps(DataDir,Rel) ->
  639:     SlAllocDumps = 
  640: 	case lists:member(Rel,?sl_alloc_vsns) of
  641: 	    true ->
  642: 		[dump_with_args(DataDir,Rel,"no_sl_alloc","+Se false"),
  643: 		 dump_with_args(DataDir,Rel,"sl_alloc_1","+Se true +Sr 1"),
  644: 		 dump_with_args(DataDir,Rel,"sl_alloc_2","+Se true +Sr 2")];
  645: 	    false ->
  646: 		[]
  647: 	end,
  648:     CD1 = full_dist_dump(DataDir,Rel),
  649:     CD2 = dump_with_args(DataDir,Rel,"port_is_unix_fd","-oldshell"),
  650:     DosDump = 
  651: 	case os:type() of
  652: 	    {unix,sunos} -> dos_dump(DataDir,Rel,CD1);
  653: 	    _ -> []
  654: 	end,
  655:     case Rel of
  656: 	current ->
  657: 	    CD3 = dump_with_args(DataDir,Rel,"instr","+Mim true"),
  658: 	    CD4 = dump_with_strange_module_name(DataDir,Rel,"strangemodname"),
  659: 	    {SlAllocDumps, [CD1,CD2,CD3,CD4], DosDump};
  660: 	_ ->
  661: 	    {SlAllocDumps, [CD1,CD2], DosDump}
  662:     end.
  663: 
  664: 
  665: %% Create a dump which has two visible nodes, one hidden and one
  666: %% not connected node, and with monitors and links between nodes.
  667: full_dist_dump(DataDir,Rel) ->
  668:     Opt = rel_opt(Rel),
  669:     Pz = "-pz \"" ++ filename:dirname(code:which(?MODULE)) ++ "\"",
  670:     PzOpt = [{args,Pz}],
  671:     {ok,N1} = ?t:start_node(n1,peer,Opt ++ PzOpt),
  672:     {ok,N2} = ?t:start_node(n2,peer,Opt ++ PzOpt),
  673:     {ok,N3} = ?t:start_node(n3,peer,Opt ++ PzOpt),
  674:     {ok,N4} = ?t:start_node(n4,peer,Opt ++ [{args,"-hidden " ++ Pz}]),
  675:     Creator = self(),
  676: 
  677:     HelperMod = crashdump_helper,
  678:     
  679:     P1 = rpc:call(N1,HelperMod,n1_proc,[N2,Creator]),
  680:     P2 = rpc:call(N2,HelperMod,remote_proc,[P1,Creator]),
  681:     P3 = rpc:call(N3,HelperMod,remote_proc,[P1,Creator]),
  682:     P4 = rpc:call(N4,HelperMod,remote_proc,[P1,Creator]),
  683:     
  684:     get_response(P2),
  685:     get_response(P3),
  686:     get_response(P4),
  687:     get_response(P1),
  688: 
  689:     L = lists:seq(0,255),
  690:     BigMsg = {message,list_to_binary(L),L},    
  691:     Port = hd(erlang:ports()),
  692:     {aaaaaaaa,N1} ! {short,message,1,2.5,"hello world",Port,{}},
  693:     {aaaaaaaa,N1} ! BigMsg,
  694:     
  695:     ?t:stop_node(N3),
  696:     DumpName = "full_dist",
  697:     CD = dump(N1,DataDir,Rel,DumpName),
  698: 
  699:     ?t:stop_node(N2),
  700:     ?t:stop_node(N4),
  701:     CD.
  702: 
  703: get_response(P) ->
  704:     receive {P,done} -> ok
  705:     after 3000 -> ?t:fail({get_response_timeout,P,node(P)})
  706:     end.
  707: 
  708: 
  709: dump_with_args(DataDir,Rel,DumpName,Args) ->
  710:     RelOpt = rel_opt(Rel),
  711:     Opt = RelOpt ++ [{args,Args}],
  712:     {ok,N1} = ?t:start_node(n1,peer,Opt),
  713:     CD = dump(N1,DataDir,Rel,DumpName),
  714:     ?t:stop_node(n1),
  715:     CD.
  716: 
  717: %% This dump is added to test OTP-10090 - regarding URL encoding of
  718: %% module names in the module detail link.
  719: dump_with_strange_module_name(DataDir,Rel,DumpName) ->
  720:     Opt = rel_opt(Rel),
  721:     {ok,N1} = ?t:start_node(n1,peer,Opt),
  722: 
  723:     Mod = '<mod ule#with?strange%name>',
  724:     File = atom_to_list(Mod) ++ ".erl",
  725:     Forms = [{attribute,1,file,{File,1}},
  726: 	     {attribute,1,module,Mod},
  727: 	     {eof,4}],
  728:     {ok,Mod,Bin} = rpc:call(N1,compile,forms,[Forms,[binary]]),
  729:     {module,Mod} = rpc:call(N1,code,load_binary,[Mod,File,Bin]),
  730:     CD = dump(N1,DataDir,Rel,DumpName),
  731:     ?t:stop_node(n1),
  732:     CD.
  733: 
  734: dump(Node,DataDir,Rel,DumpName) ->
  735:     case Rel of
  736: 	_ when Rel<r15b, Rel=/=current ->
  737: 	    rpc:call(Node,os,putenv,["ERL_CRASH_DUMP_SECONDS","600"]);
  738: 	_ ->
  739: 	    ok
  740:     end,
  741:     rpc:call(Node,erlang,halt,[DumpName]),
  742:     Crashdump0 = filename:join(filename:dirname(code:which(?t)),
  743: 			       "erl_crash_dump.n1"),
  744:     Crashdump1 = filename:join(DataDir, dump_prefix(Rel)++DumpName),
  745:     ok = rename(Crashdump0,Crashdump1),
  746:     Crashdump1.
  747: 
  748: rename(From,To) ->
  749:     ok = check_complete(From),
  750:     case file:rename(From,To) of
  751: 	{error,exdev} ->
  752: 	    {ok,_} = file:copy(From,To),
  753: 	    ok = file:delete(From);
  754: 	ok ->
  755: 	    ok
  756:     end.
  757: 
  758: check_complete(File) ->
  759:     check_complete1(File,10).
  760: 
  761: check_complete1(_File,0) ->
  762:     {error,enoent};
  763: check_complete1(File,N) ->
  764:     case file:read_file_info(File) of
  765: 	{error,enoent} ->
  766: 	    timer:sleep(500),
  767: 	    check_complete1(File,N-1);
  768: 	{ok,#file_info{size=Size}} ->
  769: 	    check_complete2(File,Size)
  770:     end.
  771: 
  772: check_complete2(File,Size) ->
  773:     timer:sleep(500),
  774:     case file:read_file_info(File) of
  775: 	{ok,#file_info{size=Size}} ->
  776: 	    ok;
  777: 	{ok,#file_info{size=OtherSize}} ->
  778: 	    check_complete2(File,OtherSize)
  779:     end.
  780: 
  781: dos_dump(DataDir,Rel,Dump) ->
  782:     DosDumpName = filename:join(DataDir,dump_prefix(Rel)++"dos"),
  783:     Cmd = "unix2dos " ++ Dump ++ " > " ++ DosDumpName,
  784:     Port = open_port({spawn,Cmd},[exit_status]),
  785:     receive
  786: 	{Port,{exit_status,0}} -> 
  787: 	    [DosDumpName];
  788: 	{Port,{exit_status,_Error}} ->
  789: 	    ?t:comment("Couldn't run \'unix2dos\'"),
  790: 	    []
  791:     end.
  792: 
  793: rel_opt(Rel) ->
  794:     case Rel of
  795: 	r9b -> [{erl,[{release,"r9b_patched"}]}];
  796: 	r9c -> [{erl,[{release,"r9c_patched"}]}];
  797: 	r10b -> [{erl,[{release,"r10b_patched"}]}];
  798: 	r11b -> [{erl,[{release,"r11b_patched"}]}];
  799: 	r12b -> [{erl,[{release,"r12b_patched"}]}];
  800: 	r13b -> [{erl,[{release,"r13b_patched"}]}];
  801: 	r14b -> [{erl,[{release,"r14b_latest"}]}]; %naming convention changed
  802: 	r15b -> [{erl,[{release,"r15b_latest"}]}];
  803: 	current -> []
  804:     end.
  805: 
  806: dump_prefix(Rel) ->
  807:     case Rel of
  808: 	r9b -> "r9b_dump.";
  809: 	r9c -> "r9c_dump.";
  810: 	r10b -> "r10b_dump.";
  811: 	r11b -> "r11b_dump.";
  812: 	r12b -> "r12b_dump.";
  813: 	r13b -> "r13b_dump.";
  814: 	r14b -> "r14b_dump.";
  815: 	r15b -> "r15b_dump.";
  816: 	current -> "r16b_dump."
  817:     end.
  818: 
  819: compat_rel(Rel) ->
  820:     case Rel of
  821: 	r9b -> "+R9 ";
  822: 	r9c -> "+R9 ";
  823: 	r10b -> "+R10 ";
  824: 	r11b -> "+R11 ";
  825: 	r12b -> "+R12 ";
  826: 	r13b -> "+R13 ";
  827: 	r14b -> "+R14 ";
  828: 	r15b -> "+R15 ";
  829: 	current -> ""
  830:     end.