1%%
2%% %CopyrightBegin%
3%%
4%% Copyright Ericsson AB 2003-2018. All Rights Reserved.
5%%
6%% Licensed under the Apache License, Version 2.0 (the "License");
7%% you may not use this file except in compliance with the License.
8%% You may obtain a copy of the License at
9%%
10%%     http://www.apache.org/licenses/LICENSE-2.0
11%%
12%% Unless required by applicable law or agreed to in writing, software
13%% distributed under the License is distributed on an "AS IS" BASIS,
14%% WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
15%% See the License for the specific language governing permissions and
16%% limitations under the License.
17%%
18%% %CopyrightEnd%
19%%
20
21-module(crashdump_viewer_SUITE).
22
23-include_lib("observer/src/crashdump_viewer.hrl").
24
25%% Test functions
26-export([all/0, suite/0,groups/0,init_per_group/2,end_per_group/2,
27	 start_stop/1,load_file/1,not_found_items/1,
28	 non_existing/1,not_a_crashdump/1,old_crashdump/1,new_crashdump/1]).
29-export([init_per_suite/1, end_per_suite/1]).
30-export([init_per_testcase/2, end_per_testcase/2]).
31
32-include_lib("common_test/include/ct.hrl").
33-include_lib("kernel/include/file.hrl").
34
35-define(failed_file,"failed-cases.txt").
36-define(helper_mod,crashdump_helper).
37
38%% -define(P(F),    print(F)).
39-define(P(F, A), print(F, A)).
40
41
42init_per_testcase(start_stop, Config) ->
43    catch crashdump_viewer:stop(),
44    try
45	case os:type() of
46	    {unix,darwin} ->
47		exit("Can not test on MacOSX");
48	    {unix, _} ->
49		io:format("DISPLAY ~s~n", [os:getenv("DISPLAY")]),
50		case ct:get_config(xserver, none) of
51		    none -> ignore;
52		    Server -> os:putenv("DISPLAY", Server)
53		end;
54	    _ -> ignore
55	end,
56	wx:new(),
57	wx:destroy(),
58	Config
59    catch
60	_:undef ->
61	    {skipped, "No wx compiled for this platform"};
62	_:Reason ->
63	    SkipReason = io_lib:format("Start wx failed: ~p", [Reason]),
64	    {skipped, lists:flatten(SkipReason)}
65    end;
66init_per_testcase(_Case, Config) ->
67    catch crashdump_viewer:stop(),
68    Config.
69end_per_testcase(Case, Config) ->
70    case ?config(tc_status,Config) of
71	ok ->
72	    ok;
73	_Fail ->
74	    File = filename:join(?config(data_dir,Config),?failed_file),
75	    {ok,Fd}=file:open(File,[append]),
76	    file:write(Fd,io_lib:format("~w.~n",[Case])),
77	    file:close(Fd)
78    end,
79    ok.
80
81suite() -> [].
82
83all() ->
84    [start_stop,
85     non_existing,
86     not_a_crashdump,
87     old_crashdump,
88     new_crashdump,
89     load_file,
90     not_found_items
91    ].
92
93groups() ->
94    [].
95
96init_per_group(_GroupName, Config) ->
97    Config.
98
99end_per_group(_GroupName, Config) ->
100    Config.
101
102
103%% Create a lot of crashdumps which can be used in the testcases below
104init_per_suite(Config) when is_list(Config) ->
105    delete_saved(Config),
106    DataDir = ?config(data_dir,Config),
107    CurrVsn = list_to_integer(erlang:system_info(otp_release)),
108    OldRels = [R || R <- [CurrVsn-2,CurrVsn-1],
109		    test_server:is_release_available(list_to_atom(integer_to_list(R)))],
110    Rels = OldRels ++ [current],
111    io:format("Creating crash dumps for the following releases: ~p", [Rels]),
112    AllDumps = create_dumps(DataDir,Rels),
113    [{dumps,AllDumps}|Config].
114
115delete_saved(Config) ->
116    DataDir = ?config(data_dir,Config),
117    file:delete(filename:join(DataDir,?failed_file)),
118    SaveDir = filename:join(DataDir,"save"),
119    Dumps = filelib:wildcard(filename:join(SaveDir,"*")),
120    lists:foreach(fun(F) -> file:delete(F) end, Dumps),
121    file:del_dir(SaveDir),
122    ok.
123
124
125start_stop(Config) when is_list(Config) ->
126    Dump = hd(?config(dumps,Config)),
127    timer:sleep(1000),
128
129    ProcsBefore = processes(),
130    NumProcsBefore = length(ProcsBefore),
131    ok = crashdump_viewer:start(Dump),
132    ExpectedRegistered = [crashdump_viewer_server,
133			  cdv_wx,
134			  cdv_proc_cb,
135			  cdv_proc_cb__holder,
136			  cdv_port_cb,
137			  cdv_port_cb__holder,
138			  cdv_ets_cb,
139			  cdv_ets_cb__holder,
140			  cdv_timer_cb,
141			  cdv_timer_cb__holder,
142			  cdv_fun_cb,
143			  cdv_fun_cb__holder,
144			  cdv_atom_cb,
145			  cdv_atom_cb__holder,
146			  cdv_dist_cb,
147			  cdv_dist_cb__holder,
148			  cdv_mod_cb,
149			  cdv_mod_cb__holder],
150    Regs=[begin
151	      P=whereis(N),
152	      {P,N,erlang:monitor(process,P)}
153	  end || N <- ExpectedRegistered],
154    ct:log("CDV procs: ~n~p~n",[Regs]),
155    [true=is_pid(P) || {P,_,_} <- Regs],
156    timer:sleep(5000), % give some time to live
157    ct:log("try stop crashdump viewer (async)~n"),
158    ok = crashdump_viewer:stop(),
159    ct:log("await crashdump viewer processes termination~n"),
160    recv_downs(Regs),
161    ct:log("sleep some~n"),
162    timer:sleep(2000),
163    ct:log("try get all processes~n"),
164    ProcsAfter = processes(),
165    NumProcsAfter = length(ProcsAfter),
166    ct:log("try verify crashdump viewer stopped~n"),
167    if (NumProcsAfter =/= NumProcsBefore) ->
168	    ct:log("Leaking processes: "
169                   "~n   Before but not after:"
170                   "~n      ~p"
171                   "~n   After but not before:"
172                   "~n      ~p",
173		   [
174                    [{P,process_info(P)} || P <- ProcsBefore -- ProcsAfter],
175                    [{P,process_info(P)} || P <- ProcsAfter -- ProcsBefore]
176                   ]);
177       true ->
178	    ok
179    end,
180    ok.
181
182recv_downs([]) ->
183    ct:log("'DOWN' received from all registered proceses~n", []),
184    ok;
185recv_downs(Regs) ->
186    receive
187	{'DOWN', Ref, process, _Pid, _} ->
188            Regs2 = lists:keydelete(Ref, 3, Regs),
189	    ct:log("Got 'DOWN' for process ~p (~w procs remaining)~n",
190                   [_Pid, length(Regs2)]),
191	    recv_downs(Regs2)
192    after 30000 ->
193	    ct:log("Timeout waiting for down:~n~p~n",
194		   [[{Reg,process_info(P)} || {P,_,_}=Reg <- Regs]]),
195	    ct:log("Message queue:~n~p~n",[process_info(self(),messages)])
196    end.
197
198%% Try to load nonexisting file
199non_existing(Config) when is_list(Config) ->
200    ExpectedReason = "non-existing-file is not an Erlang crash dump\n",
201    {error, ExpectedReason} = start_backend("non-existing-file"),
202    ok = crashdump_viewer:stop().
203
204%% Try to load a crashdump of old (earlier than OTP R10B) format
205old_crashdump(Config) when is_list(Config) ->
206    DataDir = ?config(data_dir,Config),
207    OldFile = filename:join(DataDir,"old_format.dump"),
208    ExpectedReason = "The crashdump " ++ OldFile ++
209	" is in the pre-R10B format, which is no longer supported.\n",
210    {error, ExpectedReason} = start_backend(OldFile),
211    ok = crashdump_viewer:stop().
212
213%% Try to load a file which is not an erlang crashdump
214not_a_crashdump(Config) when is_list(Config) ->
215    PrivDir = ?config(priv_dir,Config),
216    F1 = filename:join(PrivDir,"f1"),
217    F2 = filename:join(PrivDir,"f2"),
218
219    file:write_file(F1,"=unexpected_tag:xyz"),
220    file:write_file(F2,""),
221
222    ExpReason1 = F1 ++ " is not an Erlang crash dump\n",
223    ExpReason2 = F2 ++ " is not an Erlang crash dump\n",
224
225    {error,ExpReason1} = start_backend(F1),
226    {error,ExpReason2} = start_backend(F2),
227
228    ok = crashdump_viewer:stop().
229
230%% Try to load a file with newer version than this crashdump viewer can handle
231new_crashdump(Config) ->
232    Dump = hd(?config(dumps,Config)),
233    ok = start_backend(Dump),
234    {ok,{MaxVsn,CurrentVsn}} = crashdump_viewer:get_dump_versions(),
235    if MaxVsn =/= CurrentVsn ->
236            ct:fail("Current dump version is not equal to cdv's max version");
237       true ->
238            ok
239    end,
240    ok = crashdump_viewer:stop(),
241    NewerVsn = lists:join($.,[integer_to_list(X+1) || X <- MaxVsn]),
242    PrivDir = ?config(priv_dir,Config),
243    NewDump = filename:join(PrivDir,"new_erl_crash.dump"),
244    ok = file:write_file(NewDump,"=erl_crash_dump:"++NewerVsn++"\n"),
245    {error, Reason} = start_backend(NewDump),
246    "This Crashdump Viewer is too old" ++_ = Reason,
247    ok = crashdump_viewer:stop().
248
249%% Load files into the tool and view all pages
250load_file(Config) when is_list(Config) ->
251    case test_server:is_debug() of
252	true ->
253	    {skip,"Debug-compiled emulator -- far too slow"};
254	false ->
255	    load_file_1(Config)
256    end.
257
258
259load_file_1(Config) ->
260    DataDir = ?config(data_dir,Config),
261    crashdump_viewer:start_link(),
262
263    %% Read both created and predefined dumps
264    AllFiles = filelib:wildcard(filename:join(DataDir,"r*_dump.*")),
265    lists:foreach(
266      fun(File) ->
267	      Content = browse_file(File),
268	      special(File,Content)
269      end,
270      AllFiles),
271    ok = crashdump_viewer:stop().
272
273%% Try to lookup nonexisting process, port and node
274not_found_items(Config) ->
275    Dump = hd(?config(dumps,Config)),
276
277    ok = start_backend(Dump),
278
279    {ok,#general_info{},_} = crashdump_viewer:general_info(),
280
281    {error,not_found} = crashdump_viewer:proc_details("<1111.1111.1111>"),
282    {error,not_found} = crashdump_viewer:port("#Port<1111.1111>"),
283    {error,not_found} = crashdump_viewer:node_info("1111"),
284
285    ok = crashdump_viewer:stop().
286
287%% Remove generated crashdumps
288end_per_suite(Config) when is_list(Config) ->
289    Dumps = ?config(dumps,Config),
290    DataDir = ?config(data_dir,Config),
291    FailedFile = filename:join(DataDir,?failed_file),
292    case filelib:is_file(FailedFile) of
293	true ->
294	    SaveDir = filename:join(DataDir,"save"),
295	    file:make_dir(SaveDir),
296	    file:copy(FailedFile,filename:join(SaveDir,?failed_file)),
297	    lists:foreach(
298	      fun(CD) ->
299		      File = filename:basename(CD),
300		      New = filename:join(SaveDir,File),
301		      file:copy(CD,New)
302	      end, Dumps);
303	false ->
304	    ok
305    end,
306    file:delete(FailedFile),
307    lists:foreach(fun(CD) -> ok = file:delete(CD) end,Dumps),
308    lists:keydelete(dumps,1,Config).
309
310
311%%%-----------------------------------------------------------------
312%%% Internal
313%%%-----------------------------------------------------------------
314%%% Start the crashdump_viewer backend and load a dump
315start_backend(File) ->
316    crashdump_viewer:start_link(),
317    register_progress_handler(),
318    ok = crashdump_viewer:read_file(File),
319    wait_for_progress_done().
320
321%%%-----------------------------------------------------------------
322%%% Simulate the progress handler in observer_lib
323register_progress_handler() ->
324    register(cdv_progress_handler,self()).
325
326wait_for_progress_done() ->
327    receive
328	{progress,{error,Reason}} ->
329	    unregister(cdv_progress_handler),
330	    {error,lists:flatten(Reason)};
331	{progress,{ok,done}} ->
332	    unregister(cdv_progress_handler),
333	    ok;
334	{progress,_} ->
335	    wait_for_progress_done()
336    end.
337
338%%%-----------------------------------------------------------------
339%%% General check of what is displayed for a dump
340browse_file(File) ->
341    io:format("~n[~s] Browsing file: ~s", [formated_timestamp(), File]),
342    %% io:format("~nBrowsing file: ~s",[File]),
343
344    ok = start_backend(File),
345
346    io:format("  backend started",[]),
347
348    {ok,_GI=#general_info{},_GenTW} = crashdump_viewer:general_info(),
349    {ok,Procs,_ProcsTW} = crashdump_viewer:processes(),
350    {ok,Ports,_PortsTW} = crashdump_viewer:ports(),
351    {ok,_Ets,_EtsTW} = crashdump_viewer:ets_tables(all),
352    {ok,_IntEts,_IntEtsTW} = crashdump_viewer:internal_ets_tables(),
353    {ok,_Timers,_TimersTW} = crashdump_viewer:timers(all),
354    {ok,_Funs,_FunsTW} = crashdump_viewer:funs(),
355    {ok,_Atoms,_AtomsTW} = crashdump_viewer:atoms(),
356    {ok,Nodes,_NodesTW} = crashdump_viewer:dist_info(),
357    {ok,Mods,_ModsTW} = crashdump_viewer:loaded_modules(),
358    {ok,_Mem,_MemTW} = crashdump_viewer:memory(),
359    {ok,_AllocAreas,_AreaTW} = crashdump_viewer:allocated_areas(),
360    {ok,_AllocINfo,_AllocInfoTW} = crashdump_viewer:allocator_info(),
361    {ok,_HashTabs,_HashTabsTW} = crashdump_viewer:hash_tables(),
362    {ok,_IndexTabs,_IndexTabsTW} = crashdump_viewer:index_tables(),
363    {ok,_PTs,_PTsTW} = crashdump_viewer:persistent_terms(),
364
365    io:format("  info read",[]),
366
367    lookat_all_pids(Procs,is_truncated(File),incomplete_allowed(File)),
368    io:format("  pids ok",[]),
369    lookat_all_ports(Ports),
370    io:format("  ports ok",[]),
371    lookat_all_mods(Mods),
372    io:format("  mods ok",[]),
373    lookat_all_nodes(Nodes),
374    io:format("  nodes ok",[]),
375
376    Procs. % used as second arg to special/2
377
378is_truncated(File) ->
379    case filename:extension(File) of
380        ".trunc"++_ ->
381            true;
382        _ ->
383            false
384    end.
385
386incomplete_allowed(File) ->
387    %% Incomplete heap is allowed for pre OTP-20 (really pre 20.2)
388    %% releases, since literals were not dumped at all then.
389    Rel = get_rel_from_dump_name(File),
390    Rel < 20.
391
392special(File,Procs) ->
393    case filename:extension(File) of
394	".full_dist" ->
395	    %% I registered a process as aaaaaaaa in the full_dist dumps
396	    %% to make sure it will be the first in the list when sorted
397	    %% on names. There are some special data here, so I'll thoroughly
398	    %% read the process details for this process. Other processes
399	    %% are just briefly traversed.
400	    [#proc{pid=Pid0}|_Rest] = lists:keysort(#proc.name,Procs),
401	    Pid = pid_to_list(Pid0),
402	    {ok,ProcDetails=#proc{},[]} = crashdump_viewer:proc_details(Pid),
403	    io:format("  process details ok",[]),
404
405	    #proc{dict=Dict} = ProcDetails,
406
407	    ['#CDVBin',Offset,Size,Pos] = proplists:get_value(bin,Dict),
408	    {ok,<<_:Size/binary>>} =
409		crashdump_viewer:expand_binary({Offset,Size,Pos}),
410	    {ok,'#CDVTruncatedBinary'} =
411		crashdump_viewer:expand_binary({Offset,Size+1,Pos}),
412	    ['#CDVBin',SOffset,SSize,SPos] = proplists:get_value(sub_bin,Dict),
413	    {ok,<<_:SSize/binary>>} =
414		crashdump_viewer:expand_binary({SOffset,SSize,SPos}),
415	    io:format("  expand binary ok",[]),
416
417            ProcBins = proplists:get_value(proc_bins,Dict),
418            {['#CDVBin',0,65,ProcBin],
419             ['#CDVBin',65,65,ProcBin],
420             ['#CDVBin',130,125,ProcBin]} = ProcBins,
421            io:format("  ProcBins ok",[]),
422
423
424            Binaries = crashdump_helper:create_binaries(),
425            verify_binaries(Binaries, proplists:get_value(bins,Dict)),
426	    io:format("  binaries ok",[]),
427
428            SubBinaries = crashdump_helper:create_sub_binaries(Binaries),
429            verify_binaries(SubBinaries, proplists:get_value(sub_bins,Dict)),
430	    io:format("  sub binaries ok",[]),
431
432	    #proc{last_calls=LastCalls} = ProcDetails,
433            true = length(LastCalls) =< 4,
434
435	    ['#CDVPid',X1,Y1,Z1] = proplists:get_value(ext_pid,Dict),
436	    ChannelStr1 = integer_to_list(X1),
437	    ExtPid =
438		"<" ++ ChannelStr1 ++ "." ++
439		integer_to_list(Y1) ++ "." ++
440		integer_to_list(Z1) ++ ">",
441	    {error,{other_node,ChannelStr1}} =
442		crashdump_viewer:proc_details(ExtPid),
443	    io:format("  process details external ok",[]),
444
445	    ['#CDVPort',X2,Y2] = proplists:get_value(port,Dict),
446	    ChannelStr2 = integer_to_list(X2),
447	    Port = "#Port<"++ChannelStr2++"."++integer_to_list(Y2)++">",
448	    {ok,_PortDetails=#port{},[]} = crashdump_viewer:port(Port),
449	    io:format("  port details ok",[]),
450
451	    ['#CDVPort',X3,Y3] = proplists:get_value(ext_port,Dict),
452	    ChannelStr3 = integer_to_list(X3),
453	    ExtPort = "#Port<"++ChannelStr3++"."++integer_to_list(Y3)++">",
454	    {error,{other_node,ChannelStr3}} = crashdump_viewer:port(ExtPort),
455	    io:format("  port details external ok",[]),
456
457	    {ok,[_Ets=#ets_table{}],[]} = crashdump_viewer:ets_tables(Pid),
458	    io:format("  ets tables ok",[]),
459
460	    {ok,[#timer{pid=Pid0,name=undefined},
461		 #timer{pid=Pid0,name="aaaaaaaa"}],[]} =
462		crashdump_viewer:timers(Pid),
463	    {ok,AllTimers,_TimersTW} = crashdump_viewer:timers(all),
464	    #timer{name="noexistproc"} =
465		lists:keyfind(undefined,#timer.pid,AllTimers),
466	    io:format("  timers ok:",[]),
467
468	    {ok,Mod1=#loaded_mod{},[]} =
469		crashdump_viewer:loaded_mod_details(atom_to_list(?helper_mod)),
470	    io:format("  modules ok",[]),
471	    #loaded_mod{current_size=CS, old_size=OS,
472			old_attrib=A,old_comp_info=C}=Mod1,
473	    true = is_integer(CS),
474	    true = (CS==OS),
475	    true = (A=/=undefined),
476	    true = (C=/=undefined),
477	    {ok,Mod2=#loaded_mod{},[]} =
478		crashdump_viewer:loaded_mod_details("application"),
479	    io:format("  module details ok",[]),
480	    #loaded_mod{old_size="No old code exists",
481			old_attrib=undefined,
482			old_comp_info=undefined}=Mod2,
483	    ok;
484        ".trunc_mod" ->
485            ModName = atom_to_list(?helper_mod),
486            {ok,Mod=#loaded_mod{},[TW]} =
487                crashdump_viewer:loaded_mod_details(ModName),
488            "WARNING: The crash dump is truncated here."++_ = TW,
489            #loaded_mod{current_attrib=CA,current_comp_info=CCI,
490                        old_attrib=OA,old_comp_info=OCI} = Mod,
491            case lists:all(fun(undefined) ->
492                                   true;
493                              (S) when is_list(S) ->
494                                   io_lib:printable_unicode_list(lists:flatten(S));
495                              (_) -> false
496                           end,
497                           [CA,CCI,OA,OCI]) of
498                true ->
499                    ok;
500                false ->
501                    ct:fail({should_be_printable_strings_or_undefined,
502                             {CA,CCI,OA,OCI}})
503            end,
504            ok;
505	".trunc_bin1" ->
506            %% This is 'full_dist' truncated after the first
507            %% "=binary:"
508            %% i.e. no binary exist in the dump
509	    [#proc{pid=Pid0}|_Rest] = lists:keysort(#proc.name,Procs),
510	    Pid = pid_to_list(Pid0),
511            %%WarnIncompleteHeap = ["WARNING: This process has an incomplete heap. Some information might be missing."],
512	    {ok,ProcDetails=#proc{},[]} =
513                crashdump_viewer:proc_details(Pid),
514	    io:format("  process details ok",[]),
515
516	    #proc{dict=Dict} = ProcDetails,
517
518	    '#CDVNonexistingBinary' = proplists:get_value(bin,Dict),
519	    '#CDVNonexistingBinary' = proplists:get_value(sub_bin,Dict),
520
521	    io:format("  nonexisting binaries ok",[]),
522            ok;
523	".trunc_bin2" ->
524            %% This is 'full_dist' truncated after the first
525            %% "=binary:Addr\n
526            %%  Size"
527            %% i.e. binaries are truncated
528	    [#proc{pid=Pid0}|_Rest] = lists:keysort(#proc.name,Procs),
529	    Pid = pid_to_list(Pid0),
530	    {ok,ProcDetails=#proc{},[]} = crashdump_viewer:proc_details(Pid),
531	    io:format("  process details ok",[]),
532
533	    #proc{dict=Dict} = ProcDetails,
534
535	    ['#CDVBin',Offset,Size,Pos] = proplists:get_value(bin,Dict),
536            {ok,'#CDVTruncatedBinary'} =
537		crashdump_viewer:expand_binary({Offset,Size,Pos}),
538	    ['#CDVBin',SOffset,SSize,SPos] = proplists:get_value(sub_bin,Dict),
539            {ok,'#CDVTruncatedBinary'} =
540		crashdump_viewer:expand_binary({SOffset,SSize,SPos}),
541
542	    io:format("  expand truncated binary ok",[]),
543            ok;
544	".trunc_bin3" ->
545            %% This is 'full_dist' truncated after the first
546            %% "=binary:Addr\n
547            %%  Size:"
548            %% i.e. same as 'trunc_bin2', except the colon exists also
549	    [#proc{pid=Pid0}|_Rest] = lists:keysort(#proc.name,Procs),
550	    Pid = pid_to_list(Pid0),
551	    {ok,ProcDetails=#proc{},[]} = crashdump_viewer:proc_details(Pid),
552	    io:format("  process details ok",[]),
553
554	    #proc{dict=Dict} = ProcDetails,
555
556	    ['#CDVBin',Offset,Size,Pos] = proplists:get_value(bin,Dict),
557            {ok,'#CDVTruncatedBinary'} =
558		crashdump_viewer:expand_binary({Offset,Size,Pos}),
559	    ['#CDVBin',SOffset,SSize,SPos] = proplists:get_value(sub_bin,Dict),
560            {ok,'#CDVTruncatedBinary'} =
561		crashdump_viewer:expand_binary({SOffset,SSize,SPos}),
562
563	    io:format("  expand truncated binary ok",[]),
564            ok;
565	".trunc_bin4" ->
566            %% This is 'full_dist' truncated after the first
567            %% "=binary:Addr\n
568            %%  Size:BinaryMissinOneByte"
569            %% i.e. the full binary is truncated, but the sub binary is complete
570	    [#proc{pid=Pid0}|_Rest] = lists:keysort(#proc.name,Procs),
571	    Pid = pid_to_list(Pid0),
572	    {ok,ProcDetails=#proc{},[]} = crashdump_viewer:proc_details(Pid),
573	    io:format("  process details ok",[]),
574
575	    #proc{dict=Dict} = ProcDetails,
576
577	    ['#CDVBin',Offset,Size,Pos] = proplists:get_value(bin,Dict),
578            {ok,'#CDVTruncatedBinary'} =
579		crashdump_viewer:expand_binary({Offset,Size,Pos}),
580	    io:format("  expand truncated binary ok",[]),
581	    ['#CDVBin',SOffset,SSize,SPos] = proplists:get_value(sub_bin,Dict),
582            {ok,<<_:SSize/binary>>} =
583		crashdump_viewer:expand_binary({SOffset,SSize,SPos}),
584	    io:format("  expand complete sub binary ok",[]),
585
586            ok;
587        ".trunc_bytes" ->
588            {ok,_,[TW]} = crashdump_viewer:general_info(),
589            {match,_} = re:run(TW,"CRASH DUMP SIZE LIMIT REACHED"),
590	    io:format("  size limit information ok",[]),
591            ok;
592        ".unicode" ->
593            #proc{pid=Pid0} =
594                lists:keyfind("'unicode_reg_name_αβ'",#proc.name,Procs),
595            Pid = pid_to_list(Pid0),
596	    {ok,Proc,[]} = crashdump_viewer:proc_details(Pid),
597            #proc{last_calls=LastCalls,stack_dump=Stk} = Proc,
598            io:format("  unicode registered name ok",[]),
599
600            ["crashdump_helper_unicode:'спутник'/0",
601             "ets:new/2"|_] = lists:reverse(LastCalls),
602            io:format("  last calls ok",[]),
603
604            verify_unicode_stack(Stk),
605            io:format("  unicode stack values ok",[]),
606
607	    {ok,[#ets_table{id="'tab_αβ'",name="'tab_αβ'"}],[]} =
608                crashdump_viewer:ets_tables(Pid),
609            io:format("  unicode table name ok",[]),
610
611            ok;
612        ".maps" ->
613	    %% I registered a process as aaaaaaaa_maps in the map dump
614	    %% to make sure it will be the first in the list when sorted
615	    %% on names.
616	    [#proc{pid=Pid0,name=Name}|_Rest] = lists:keysort(#proc.name,Procs),
617            "aaaaaaaa_maps" = Name,
618	    Pid = pid_to_list(Pid0),
619	    {ok,ProcDetails=#proc{},[]} = crashdump_viewer:proc_details(Pid),
620	    io:format("  process details ok",[]),
621
622	    #proc{dict=Dict} = ProcDetails,
623            %% io:format("~p\n", [Dict]),
624            Maps = crashdump_helper:create_maps(),
625            Maps = proplists:get_value(maps,Dict),
626            io:format("  maps ok",[]),
627            ok;
628        ".persistent_terms" ->
629	    %% I registered a process as aaaaaaaa_persistent_term in
630	    %% the dump to make sure it will be the first in the list
631	    %% when sorted on names.
632	    [#proc{pid=Pid0,name=Name}|_Rest] = lists:keysort(#proc.name,Procs),
633            "aaaaaaaa_persistent_terms" = Name,
634	    Pid = pid_to_list(Pid0),
635	    {ok,ProcDetails=#proc{},[]} = crashdump_viewer:proc_details(Pid),
636	    io:format("  process details ok",[]),
637
638	    #proc{dict=Dict} = ProcDetails,
639            %% io:format("~p\n", [Dict]),
640            Pts = crashdump_helper:create_persistent_terms(),
641            Pts = proplists:get_value(pts,Dict),
642            io:format("  persistent terms ok",[]),
643            ok;
644        ".global_literals" ->
645	    %% I registered a process as aaaaaaaa_global_literals in
646	    %% the dump to make sure it will be the first in the list
647	    %% when sorted on names.
648	    [#proc{pid=Pid0,name=Name}|_Rest] = lists:keysort(#proc.name,Procs),
649            "aaaaaaaa_global_literals" = Name,
650	    Pid = pid_to_list(Pid0),
651	    {ok,ProcDetails=#proc{},[]} = crashdump_viewer:proc_details(Pid),
652	    io:format("  process details ok",[]),
653
654	    #proc{dict=Dict} = ProcDetails,
655            Globals = proplists:get_value(global_literals,Dict),
656            Globals = {os:type(),os:version()},
657            io:format("  global_literals ok",[]),
658            ok;
659	_ ->
660	    ok
661    end,
662    ok.
663
664verify_unicode_stack([{_,{state,Str,Atom,Bin,LongBin}}|_]) ->
665    'unicode_atom_αβ' = Atom,
666    "unicode_string_αβ" = Str,
667    <<"bin αβ"/utf8>> = Bin,
668    <<"long bin αβ - a utf8 binary which can be expanded αβ"/utf8>> = LongBin,
669    ok;
670verify_unicode_stack([_|T]) ->
671    verify_unicode_stack(T).
672
673verify_binaries([H|T1], [H|T2]) ->
674    %% Heap binary.
675    verify_binaries(T1, T2);
676verify_binaries([Bin|T1], [['#CDVBin',Offset,Size,Pos]|T2]) ->
677    %% Refc binary.
678    {ok,<<Bin:Size/binary>>} = crashdump_viewer:expand_binary({Offset,Size,Pos}),
679    verify_binaries(T1, T2);
680verify_binaries([], []) ->
681    ok.
682
683lookat_all_pids([],_,_) ->
684    ok;
685lookat_all_pids([#proc{pid=Pid0}|Procs],TruncAllowed,IncompAllowed) ->
686    Pid = pid_to_list(Pid0),
687    {ok,_ProcDetails=#proc{},ProcTW} = crashdump_viewer:proc_details(Pid),
688    {ok,_Ets,EtsTW} = crashdump_viewer:ets_tables(Pid),
689    {ok,_Timers,TimersTW} = crashdump_viewer:timers(Pid),
690    case {ProcTW,EtsTW,TimersTW} of
691        {[],[],[]} ->
692            ok;
693        {["WARNING: This process has an incomplete heap."++_],[],[]}
694	when IncompAllowed ->
695            ok;  % native libs, literals might not be included in dump
696        _ when TruncAllowed ->
697            ok; % truncated dump
698        TWs ->
699	    ?P("lookat_all_pids -> unexpected warning"
700	       "~n   Pid:           ~s"
701	       "~n   IncompAllowed: ~p"
702	       "~n   TruncAllowed:  ~p"
703	       "~n   ~p", [Pid, IncompAllowed, TruncAllowed, TWs]),
704            ct:fail({unexpected_warning, Pid, TWs, IncompAllowed, TruncAllowed})
705    end,
706    lookat_all_pids(Procs,TruncAllowed,IncompAllowed).
707
708lookat_all_ports([]) ->
709    ok;
710lookat_all_ports([#port{id=Port0}|Procs]) ->
711    Port = cdv_port_cb:format(Port0),
712    {ok,_PortDetails=#port{},_PortTW} = crashdump_viewer:port(Port),
713    lookat_all_ports(Procs).
714
715lookat_all_mods([]) ->
716    ok;
717lookat_all_mods([#loaded_mod{mod=ModId}|Mods]) ->
718    ModName = cdv_mod_cb:format(ModId),
719    {ok,_Mod=#loaded_mod{},_ModTW} = crashdump_viewer:loaded_mod_details(ModName),
720    lookat_all_mods(Mods).
721
722lookat_all_nodes([]) ->
723    ok;
724lookat_all_nodes([#nod{channel=Channel0}|Nodes]) ->
725    Channel = integer_to_list(Channel0),
726    {ok,_Node=#nod{},_NodeTW} = crashdump_viewer:node_info(Channel),
727    lookat_all_nodes(Nodes).
728
729%%%-----------------------------------------------------------------
730%%%
731create_dumps(DataDir,Rels) ->
732    create_dumps(DataDir,Rels,[]).
733create_dumps(DataDir,[Rel|Rels],Acc) ->
734    Fun = fun() -> do_create_dumps(DataDir,Rel) end,
735    Pa = filename:dirname(code:which(?MODULE)),
736    {Dumps,DosDump} =
737	test_server:run_on_shielded_node(Fun, compat_rel(Rel) ++ "-pa \"" ++ Pa ++ "\""),
738    create_dumps(DataDir,Rels,Dumps ++ Acc ++ DosDump);
739create_dumps(_DataDir,[],Acc) ->
740    Acc.
741
742do_create_dumps(DataDir,Rel) ->
743    CD1 = full_dist_dump(DataDir,Rel),
744    CD2 = dump_with_args(DataDir,Rel,"port_is_unix_fd","-oldshell"),
745    DosDump =
746	case os:type() of
747	    {unix,sunos} -> dos_dump(DataDir,Rel,CD1);
748	    _ -> []
749	end,
750    case Rel of
751	current ->
752	    CD3 = dump_with_args(DataDir,Rel,"instr","+Muatags true"),
753	    CD4 = dump_with_strange_module_name(DataDir,Rel,"strangemodname"),
754            CD5 = dump_with_size_limit_reached(DataDir,Rel,"trunc_bytes"),
755            CD6 = dump_with_unicode_atoms(DataDir,Rel,"unicode"),
756            CD7 = dump_with_maps(DataDir,Rel,"maps"),
757            CD8 = dump_with_persistent_terms(DataDir,Rel,"persistent_terms"),
758            CD9 = dump_with_global_literals(DataDir,Rel,"global_literals"),
759            TruncDumpMod = truncate_dump_mod(CD1),
760            TruncatedDumpsBinary = truncate_dump_binary(CD1),
761	    {[CD1,CD2,CD3,CD4,CD5,CD6,CD7,CD8,CD9,
762              TruncDumpMod|TruncatedDumpsBinary],
763             DosDump};
764	_ ->
765	    {[CD1,CD2], DosDump}
766    end.
767
768truncate_dump_mod(File) ->
769    {ok,Bin} = file:read_file(File),
770    ModNameBin = atom_to_binary(?helper_mod,latin1),
771    NewLine = case os:type() of
772                  {win32,_} -> <<"\r\n">>;
773                  _ -> <<"\n">>
774              end,
775    RE = <<NewLine/binary,"=mod:",ModNameBin/binary,
776           NewLine/binary,"Current size: [0-9]*",
777           NewLine/binary,"Current attributes: ...">>,
778    {match,[{Pos,Len}]} = re:run(Bin,RE),
779    Size = Pos + Len,
780    <<Truncated:Size/binary,_/binary>> = Bin,
781    DumpName = filename:rootname(File) ++ ".trunc_mod",
782    file:write_file(DumpName,Truncated),
783    DumpName.
784
785truncate_dump_binary(File) ->
786    {ok,Bin} = file:read_file(File),
787    BinTag = <<"\n=binary:">>,
788    Colon = <<":">>,
789    NewLine = case os:type() of
790                  {win32,_} -> <<"\r\n">>;
791                  _ -> <<"\n">>
792              end,
793    %% Split after "our binary" created by crashdump_helper
794    %% (it may not be the first binary).
795    RE = <<"\n=binary:(?=[0-9A-Z]+",NewLine/binary,"FF:AQID)">>,
796    [StartBin,AfterTag] = re:split(Bin,RE,[{parts,2}]),
797    [AddrAndSize,BinaryAndRest] = binary:split(AfterTag,Colon),
798    [Binary,_Rest] = binary:split(BinaryAndRest,NewLine),
799    TruncSize = byte_size(Binary) - 2,
800    <<TruncBinary:TruncSize/binary,_/binary>> = Binary,
801    TruncName = filename:rootname(File) ++ ".trunc_bin",
802    write_trunc_files(TruncName,StartBin,
803                      [BinTag,AddrAndSize,Colon,TruncBinary],1).
804
805write_trunc_files(TruncName0,Bin,[Part|Parts],N) ->
806    TruncName = TruncName0++integer_to_list(N),
807    Bin1 = <<Bin/binary,Part/binary>>,
808    ok = file:write_file(TruncName,Bin1),
809    [TruncName|write_trunc_files(TruncName0,Bin1,Parts,N+1)];
810write_trunc_files(_,_,[],_) ->
811    [].
812
813
814%% Create a dump which has three visible nodes, one hidden and one
815%% not connected node, and with monitors and links between nodes.
816%% One of the visible nodes is stopped and started again in order to
817%% get multiple creations.
818full_dist_dump(DataDir,Rel) ->
819    Opt = rel_opt(Rel),
820    Pz = "-pz \"" ++ filename:dirname(code:which(?MODULE)) ++ "\"",
821    PzOpt = [{args,Pz}],
822    {ok,N1} = test_server:start_node(n1,peer,Opt ++ PzOpt),
823    {ok,N2} = test_server:start_node(n2,peer,Opt ++ PzOpt),
824    {ok,N3} = test_server:start_node(n3,peer,Opt ++ PzOpt),
825    {ok,N4} = test_server:start_node(n4,peer,Opt ++ [{args,"-hidden " ++ Pz}]),
826    Creator = self(),
827
828    P1 = rpc:call(N1,?helper_mod,n1_proc,[N2,Creator]),
829    P2 = rpc:call(N2,?helper_mod,remote_proc,[P1,Creator]),
830    P3 = rpc:call(N3,?helper_mod,remote_proc,[P1,Creator]),
831    P4 = rpc:call(N4,?helper_mod,remote_proc,[P1,Creator]),
832
833    get_response(P2),
834    get_response(P3),
835    get_response(P4),
836    get_response(P1),
837
838    %% start, stop and start a node in order to get multiple 'creations'
839    {ok,N5} = test_server:start_node(n5,peer,Opt ++ PzOpt),
840    P51 = rpc:call(N5,?helper_mod,remote_proc,[P1,Creator]),
841    get_response(P51),
842    test_server:stop_node(N5),
843    {ok,N5} = test_server:start_node(n5,peer,Opt ++ PzOpt),
844    P52 = rpc:call(N5,?helper_mod,remote_proc,[P1,Creator]),
845    get_response(P52),
846
847    {aaaaaaaa,N1} ! {hello,from,other,node}, % distribution message
848
849    test_server:stop_node(N3),
850    DumpName = "full_dist",
851    CD = dump(N1,DataDir,Rel,DumpName),
852
853    test_server:stop_node(N2),
854    test_server:stop_node(N4),
855    test_server:stop_node(N5),
856    CD.
857
858get_response(P) ->
859    receive {P,done} -> ok
860    after 3000 -> ct:fail({get_response_timeout,P,node(P)})
861    end.
862
863
864dump_with_args(DataDir,Rel,DumpName,Args) ->
865    RelOpt = rel_opt(Rel),
866    Opt = RelOpt ++ [{args,Args}],
867    {ok,N1} = test_server:start_node(n1,peer,Opt),
868    CD = dump(N1,DataDir,Rel,DumpName),
869    test_server:stop_node(n1),
870    CD.
871
872%% This dump is added to test OTP-10090 - regarding URL encoding of
873%% module names in the module detail link.
874dump_with_strange_module_name(DataDir,Rel,DumpName) ->
875    Opt = rel_opt(Rel),
876    {ok,N1} = test_server:start_node(n1,peer,Opt),
877
878    Mod = '<mod ule#with?strange%name>',
879    File = atom_to_list(Mod) ++ ".erl",
880    Forms = [{attribute,1,file,{File,1}},
881	     {attribute,1,module,Mod},
882	     {eof,4}],
883    {ok,Mod,Bin} = rpc:call(N1,compile,forms,[Forms,[binary]]),
884    {module,Mod} = rpc:call(N1,code,load_binary,[Mod,File,Bin]),
885    CD = dump(N1,DataDir,Rel,DumpName),
886    test_server:stop_node(n1),
887    CD.
888
889dump_with_size_limit_reached(DataDir,Rel,DumpName) ->
890    Tmp = dump_with_args(DataDir,Rel,DumpName,""),
891    {ok,#file_info{size=Max}} = file:read_file_info(Tmp),
892    ok = file:delete(Tmp),
893    dump_with_size_limit_reached(DataDir,Rel,DumpName,Max).
894
895dump_with_size_limit_reached(DataDir,Rel,DumpName,Max) ->
896    Bytes = max(15,rand:uniform(Max)),
897    CD = dump_with_args(DataDir,Rel,DumpName,
898                        "-env ERL_CRASH_DUMP_BYTES " ++
899                            integer_to_list(Bytes)),
900    {ok,#file_info{size=Size}} = file:read_file_info(CD),
901    if Size =< Bytes ->
902            %% This means that the dump was actually smaller than the
903            %% randomly selected truncation size, so we'll just do it
904            %% again with a smaller number
905            ok = file:delete(CD),
906            dump_with_size_limit_reached(DataDir,Rel,DumpName,Size-3);
907       true ->
908            CD
909    end.
910
911dump_with_unicode_atoms(DataDir,Rel,DumpName) ->
912    Opt = rel_opt(Rel),
913    Pz = "-pz \"" ++ filename:dirname(code:which(?MODULE)) ++ "\"",
914    PzOpt = [{args,Pz}],
915    {ok,N1} = test_server:start_node(n1,peer,Opt ++ PzOpt),
916    {ok,_Pid} = rpc:call(N1,crashdump_helper_unicode,start,[]),
917    CD = dump(N1,DataDir,Rel,DumpName),
918    test_server:stop_node(n1),
919    CD.
920
921dump_with_maps(DataDir,Rel,DumpName) ->
922    Opt = rel_opt(Rel),
923    Pz = "-pz \"" ++ filename:dirname(code:which(?MODULE)) ++ "\"",
924    PzOpt = [{args,Pz}],
925    {ok,N1} = test_server:start_node(n1,peer,Opt ++ PzOpt),
926    {ok,_Pid} = rpc:call(N1,crashdump_helper,dump_maps,[]),
927    CD = dump(N1,DataDir,Rel,DumpName),
928    test_server:stop_node(n1),
929    CD.
930
931dump_with_persistent_terms(DataDir,Rel,DumpName) ->
932    Opt = rel_opt(Rel),
933    Pz = "-pz \"" ++ filename:dirname(code:which(?MODULE)) ++ "\"",
934    PzOpt = [{args,Pz}],
935    {ok,N1} = test_server:start_node(n1,peer,Opt ++ PzOpt),
936    {ok,_Pid} = rpc:call(N1,crashdump_helper,dump_persistent_terms,[]),
937    CD = dump(N1,DataDir,Rel,DumpName),
938    test_server:stop_node(n1),
939    CD.
940
941dump_with_global_literals(DataDir,Rel,DumpName) ->
942    Opt = rel_opt(Rel),
943    Pz = "-pz \"" ++ filename:dirname(code:which(?MODULE)) ++ "\"",
944    PzOpt = [{args,Pz}],
945    {ok,N1} = test_server:start_node(n1,peer,Opt ++ PzOpt),
946    {ok,_Pid} = rpc:call(N1,crashdump_helper,dump_global_literals,[]),
947    CD = dump(N1,DataDir,Rel,DumpName),
948    test_server:stop_node(n1),
949    CD.
950
951dump(Node,DataDir,Rel,DumpName) ->
952    Crashdump = filename:join(DataDir, dump_prefix(Rel)++DumpName),
953    rpc:call(Node,os,putenv,["ERL_CRASH_DUMP",Crashdump]),
954    rpc:call(Node,erlang,halt,[DumpName]),
955    ok = check_complete(Crashdump),
956    Crashdump.
957
958check_complete(File) ->
959    check_complete1(File,10).
960
961check_complete1(_File,0) ->
962    {error,enoent};
963check_complete1(File,N) ->
964    case file:read_file_info(File) of
965	{error,enoent} ->
966	    timer:sleep(500),
967	    check_complete1(File,N-1);
968	{ok,#file_info{size=Size}} ->
969	    check_complete2(File,Size)
970    end.
971
972check_complete2(File,Size) ->
973    timer:sleep(500),
974    case file:read_file_info(File) of
975	{ok,#file_info{size=Size}} ->
976	    ok;
977	{ok,#file_info{size=OtherSize}} ->
978	    check_complete2(File,OtherSize)
979    end.
980
981dos_dump(DataDir,Rel,Dump) ->
982    DosDumpName = filename:join(DataDir,dump_prefix(Rel)++"dos"),
983    Cmd = "unix2dos " ++ Dump ++ " > " ++ DosDumpName,
984    Port = open_port({spawn,Cmd},[exit_status]),
985    receive
986	{Port,{exit_status,0}} ->
987	    [DosDumpName];
988	{Port,{exit_status,_Error}} ->
989	    test_server:comment("Couldn't run \'unix2dos\'"),
990	    []
991    end.
992
993rel_opt(current) ->
994    [];
995rel_opt(Rel) ->
996    [{erl,[{release,lists:concat([Rel,"_latest"])}]}].
997
998dump_prefix(current) ->
999    dump_prefix(erlang:system_info(otp_release));
1000dump_prefix(Rel) ->
1001    lists:concat(["r",Rel,"_dump."]).
1002
1003get_rel_from_dump_name(File) ->
1004    Name = filename:basename(File),
1005    ["r"++Rel|_] = string:split(Name,"_"),
1006    list_to_integer(Rel).
1007
1008compat_rel(current) ->
1009    "";
1010compat_rel(Rel) ->
1011    lists:concat(["+R",Rel," "]).
1012
1013
1014%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%
1015
1016%% f(F, A) ->
1017%%     lists:flatten(io_lib:format(F, A)).
1018
1019formated_timestamp() ->
1020    format_timestamp(os:timestamp()).
1021
1022format_timestamp({_N1, _N2, N3} = TS) ->
1023    {_Date, Time}   = calendar:now_to_local_time(TS),
1024    {Hour, Min, Sec} = Time,
1025    FormatTS = io_lib:format("~.2.0w:~.2.0w:~.2.0w.~.3.0w",
1026                             [Hour, Min, Sec, N3 div 1000]),
1027    lists:flatten(FormatTS).
1028
1029%% print(F) ->
1030%%     print(F, []).
1031
1032print(F, A) ->
1033    io:format("~s ~p " ++ F ++ "~n", [formated_timestamp(), self() | A]).
1034
1035