%%
%% %CopyrightBegin%
%%
%% Copyright Ericsson AB 1996-2021. All Rights Reserved.
%%
%% Licensed under the Apache License, Version 2.0 (the "License");
%% you may not use this file except in compliance with the License.
%% You may obtain a copy of the License at
%%
%%     http://www.apache.org/licenses/LICENSE-2.0
%%
%% Unless required by applicable law or agreed to in writing, software
%% distributed under the License is distributed on an "AS IS" BASIS,
%% WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
%% See the License for the specific language governing permissions and
%% limitations under the License.
%%
%% %CopyrightEnd%
%%
-module(erl_prim_loader_SUITE).

-include_lib("kernel/include/file.hrl").
-include_lib("common_test/include/ct.hrl").

-export([all/0, suite/0,groups/0,init_per_suite/1, end_per_suite/1, 
	 init_per_testcase/2,end_per_testcase/2,
	 init_per_group/2,end_per_group/2]).

-export([get_path/1, set_path/1, get_file/1, normalize_and_backslash/1,
	 inet_existing/1, inet_coming_up/1, inet_disconnects/1,
	 multiple_slaves/1, file_requests/1,
	 local_archive/1, remote_archive/1,
	 primary_archive/1, virtual_dir_in_archive/1,
	 get_modules/1]).

-define(PRIM_FILE, prim_file).

%%-----------------------------------------------------------------
%% Test suite for erl_prim_loader. (Most code is run during system start/stop.)
%%-----------------------------------------------------------------

suite() ->
    [{ct_hooks,[ts_install_cth]},
     {timetrap,{minutes,3}}].

all() -> 
    [get_path, set_path, get_file,
     normalize_and_backslash, inet_existing,
     inet_coming_up, inet_disconnects, multiple_slaves,
     file_requests, local_archive, remote_archive,
     primary_archive, virtual_dir_in_archive,
     get_modules].

groups() -> 
    [].

init_per_suite(Config) ->
    Config.

end_per_suite(_Config) ->
    ok.

init_per_group(_GroupName, Config) ->
    Config.

end_per_group(_GroupName, Config) ->
    Config.


init_per_testcase(_Func, Config) ->
    Config.

end_per_testcase(_Func, _Config) ->
    ok.

get_path(Config) when is_list(Config) ->
    case erl_prim_loader:get_path() of
	{ok, Path} when is_list(Path) ->
	    ok;
	_ ->
	    ct:fail(get_path)
    end,
    ok.

set_path(Config) when is_list(Config) ->
    {ok, Path} = erl_prim_loader:get_path(),
    ok = erl_prim_loader:set_path(Path),
    {ok, Path} = erl_prim_loader:get_path(),
    NewPath = Path ++ ["dummy_dir","/dummy_dir/dummy_dir"],
    ok = erl_prim_loader:set_path(NewPath),
    {ok, NewPath} = erl_prim_loader:get_path(),

    ok = erl_prim_loader:set_path(Path), % Reset path.
    {ok, Path} = erl_prim_loader:get_path(),

    {'EXIT',_} = (catch erl_prim_loader:set_path(not_a_list)),
    {ok, Path} = erl_prim_loader:get_path(),
    ok.

get_file(Config) when is_list(Config) ->
    case erl_prim_loader:get_file("lists" ++ code:objfile_extension()) of
	{ok,Bin,File} when is_binary(Bin), is_list(File) ->
	    ok;
	_ ->
	    ct:fail(get_valid_file)
    end,
    error = erl_prim_loader:get_file("duuuuuuummmy_file"),
    error = erl_prim_loader:get_file(duuuuuuummmy_file),
    error = erl_prim_loader:get_file({dummy}),
    ok.

get_modules(Config) ->
    case test_server:is_cover() of
	false -> do_get_modules(Config);
	true -> {skip,"Cover"}
    end.

do_get_modules(Config) ->
    PrivDir = proplists:get_value(priv_dir, Config),
    NotADir = atom_to_list(?FUNCTION_NAME) ++ "_not_a_dir",
    ok = file:write_file(filename:join(PrivDir, NotADir), <<>>),
    ok = file:set_cwd(PrivDir),

    MsGood = lists:sort([lists,gen_server,gb_trees,code_server]),
    Ms = [certainly_not_existing|MsGood],
    SuccExp = [begin
		   F = code:which(M),
		   {ok,Code} = file:read_file(F),
		   {M,{F,erlang:md5(Code)}}
	       end || M <- MsGood],
    FailExp = [{certainly_not_existing,enoent}],

    io:format("SuccExp = ~p\n", [SuccExp]),
    io:format("FailExp = ~p\n", [FailExp]),

    Path = code:get_path(),
    Process = fun(_, F, Code) -> {ok,{F,erlang:md5(Code)}} end,
    {ok,{SuccExp,FailExp}} = get_modules_sorted(Ms, Process, Path),

    %% Test that an 'enotdir' error can be handled.
    {ok,{SuccExp,FailExp}} = get_modules_sorted(Ms, Process, [NotADir|Path]),

    {ok, Peer, Node, BootPid} = complete_start_node(),
    ThisDir = filename:dirname(code:which(?MODULE)),
    true = rpc:call(Node, code, add_patha, [ThisDir]),
    _ = rpc:call(Node, code, ensure_loaded, [?MODULE]),
    {ok,{InetSucc,FailExp}} = rpc:call(Node, erl_prim_loader,
				       get_modules, [Ms,Process,Path]),
    SuccExp = lists:sort(InetSucc),

    peer:stop(Peer),
    unlink(BootPid),
    exit(BootPid, kill),

    ok.

get_modules_sorted(Ms, Process, Path) ->
    case erl_prim_loader:get_modules(Ms, Process, Path) of
	{ok,{Succ,FailExp}} ->
	    {ok,{lists:sort(Succ),lists:sort(FailExp)}};
	Other ->
	    Other
    end.

normalize_and_backslash(Config) ->
    %% Test OTP-11170
    case os:type() of
	{win32,_} ->
	    {skip, "not on windows"};
	_ ->
	    test_normalize_and_backslash(Config)
    end.
test_normalize_and_backslash(Config) ->
    PrivDir = proplists:get_value(priv_dir,Config),
    Dir = filename:join(PrivDir,"\\"),
    File = filename:join(Dir,"file-OTP-11170"),
    ok = file:make_dir(Dir),
    ok = file:write_file(File,"a file to test OTP-11170"),
    {ok,["file-OTP-11170"]} = file:list_dir(Dir),
    {ok,["file-OTP-11170"]} = erl_prim_loader:list_dir(Dir),
    ok = file:delete(File),
    ok = file:del_dir(Dir),
    ok.

%% Start a node using the 'inet' loading method,
%% from an already started boot server.
inet_existing(Config) when is_list(Config) ->
    BootPid = start_boot_server(),
    {ok, Peer, Node} = ?CT_PEER(start_node_using_inet_args()),
    {ok,[["inet"]]} = rpc:call(Node, init, get_argument, [loader]),
    peer:stop(Peer),
    unlink(BootPid),
    exit(BootPid, kill),
    ok.

%% Start a node using the 'inet' loading method,
%% but start the boot server afterwards.
inet_coming_up(Config) when is_list(Config) ->
    {ok, Peer} = ?CT_PEER(#{wait_boot => false, connection => standard_io,
        args => start_node_using_inet_args()}),

    %% Wait a while, then start boot server, and wait for node to start.
    ct:sleep({seconds,6}),
    BootPid = start_boot_server(),
    Node = wait_really_started(Peer, 25),

    %% Check loader argument, then cleanup.
    {ok,[["inet"]]} = rpc:call(Node, init, get_argument, [loader]),
    peer:stop(Peer),
    unlink(BootPid),
    exit(BootPid, kill),
    ok.

wait_really_started(Peer, 0) ->
    ct:fail({not_booted,Peer});
wait_really_started(Peer, N) ->
    case (catch peer:call(Peer, init, get_status, [])) of
        {started, _} ->
            peer:call(Peer, erlang, node, []);
        _ ->
            ct:sleep(1000),
            wait_really_started(Peer, N - 1)
    end.

%% Start a node using the 'inet' loading method,
%% then lose the connection.
inet_disconnects(Config) when is_list(Config) ->
    BootPid = start_boot_server(),
    unlink(BootPid),
    Self = self(),

    %% This process shuts down the boot server during loading.
    Stopper = spawn_link(fun() -> stop_boot(BootPid, Self) end),
    receive
        {Stopper,ready} -> ok
    end,

    %% Let the loading begin...
    {ok,Peer} = ?CT_PEER(#{wait_boot => false, connection => 0,
        args => start_node_using_inet_args()}),

    %% When the stopper is ready, the slave node should be
    %% looking for a boot server again.
    receive
        {Stopper,ok} ->
            ok;
        {Stopper,{error,Reason}} ->
            ct:fail(Reason)
    after 60000 ->
            ct:fail(stopper_died)
    end,

    %% Start new boot server to see that loading is continued.
    BootPid2 = start_boot_server(),
    Node = wait_really_started(Peer, 25),
    {ok,[["inet"]]} = rpc:call(Node, init, get_argument, [loader]),
    peer:stop(Peer),
    unlink(BootPid2),
    exit(BootPid2, kill),
    ok.

%% Trace boot server calls and stop the server before loading is finished.
stop_boot(BootPid, Super) ->
    erlang:trace(all, true, [call]),
    1 = erlang:trace_pattern({erl_boot_server,send_file_result,3}, true, [local]),
    BootRef = erlang:monitor(process, BootPid),
    Super ! {self(),ready},
    Result = get_calls(100, BootPid),
    exit(BootPid, kill),
    erlang:trace_pattern({erl_boot_server,send_file_result,3}, false, [local]),
    erlang:trace(all, false, [call]),
    receive
	{'DOWN',BootRef,_,_, killed} -> ok
    end,
    Super ! {self(),Result}.    

get_calls(0, _) ->
    ok;
get_calls(Count, Pid) ->
    receive
	{trace,_,call,_MFA} ->
	    get_calls(Count-1, Pid)
    after 10000 ->
	    {error,{trace_msg_timeout,Count}}
    end.	

%% Start nodes in parallel, all using the 'inet' loading method;
%% verify that the boot server manages.
multiple_slaves(Config) when is_list(Config) ->
    Args = start_node_using_inet_args(),

    NoOfNodes = 10,			% no of slave nodes to be started

    Peers = [?CT_PEER(#{wait_boot => {self(), tag}, args => Args}) || _ <- lists:seq(1, NoOfNodes)],

    %% "queue up" the nodes to wait for the boot server to respond
    %% (note: test_server supervises each node start by accept()
    %% on a socket, the timeout value for the accept has to be quite 
    %% long for this test to work).
    ct:sleep({seconds,5}),
    %% start the code loading circus!
    BootPid = start_boot_server(),
    %% give the nodes a chance to boot up before attempting to stop them
    ct:sleep({seconds,10}),

    %% wait for all nodes to complete boot process, get their names:
    [
        receive
            {tag, {started, Node, Peer}} ->
                {ok,[["inet"]]} = rpc:call(Node, init, get_argument, [loader])
        end
        || {ok, Peer} <- Peers
    ],
    [peer:stop(Peer) || {ok, Peer} <- Peers],

    unlink(BootPid),
    exit(BootPid, kill),
    ok.


%% Start a node using the 'inet' loading method,
%% verify that the boot server responds to file requests.
file_requests(Config) when is_list(Config) ->
    {ok, Peer, Node, BootPid} = complete_start_node(),

    %% compare with results from file server calls (the
    %% boot server uses the same file sys and cwd)
    {ok,Files} = file:list_dir("."),
    io:format("Files: ~p~n",[Files]),
    {ok,Files} = rpc:call(Node, erl_prim_loader, list_dir, ["."]),
    {ok,Info} = file:read_file_info(code:which(test_server)),
    {ok,Info} = rpc:call(Node, erl_prim_loader, read_file_info,
			 [code:which(test_server)]),

    PrivDir = proplists:get_value(priv_dir,Config),
    Dir = filename:join(PrivDir,?MODULE_STRING++"_file_requests"),
    ok = file:make_dir(Dir),
    Alias = filename:join(Dir,"symlink"),
    case file:make_symlink(code:which(test_server), Alias) of
	{error, enotsup} ->
	    %% Links not supported on this platform
	    ok;
	{error, eperm} ->
	    {win32,_} = os:type(),
	    %% Windows user not privileged to create symlinks"
	    ok;
	ok ->
	    %% Reading file info for link should return file info for
	    %% link target
	    {ok,Info} = rpc:call(Node, erl_prim_loader, read_file_info,
				 [Alias]),
	    #file_info{type=regular} = Info,
	    {ok,#file_info{type=symlink}} =
		rpc:call(Node, erl_prim_loader, read_link_info,
			 [Alias])
    end,

    {ok,Cwd} = file:get_cwd(),
    {ok,Cwd} = rpc:call(Node, erl_prim_loader, get_cwd, []),
    case file:get_cwd("C:") of
	{error,enotsup} ->
	    ok;
	{ok,DCwd} ->
	    {ok,DCwd} = rpc:call(Node, erl_prim_loader, get_cwd, ["C:"])
    end,

    peer:stop(Peer),
    unlink(BootPid),
    exit(BootPid, kill),
    ok.

%% Read files from local archive.
local_archive(Config) when is_list(Config) ->
    PrivDir = proplists:get_value(priv_dir, Config),
    KernelDir = filename:basename(code:lib_dir(kernel)),
    Archive = filename:join([PrivDir, KernelDir ++ init:archive_extension()]),
    file:delete(Archive),
    {ok, Archive} = create_archive(Archive, [KernelDir]),

    Node = node(),
    BeamName = "inet.beam",
    ok = test_archive(Node, Archive, KernelDir, BeamName),

    %% Cleanup
    ok = rpc:call(Node, erl_prim_loader, purge_archive_cache, []),
    ok = file:delete(Archive),
    ok.

%% Read files from remote archive.
remote_archive(Config) when is_list(Config) ->
    PrivDir = proplists:get_value(priv_dir, Config),
    KernelDir = filename:basename(code:lib_dir(kernel)),
    Archive = filename:join([PrivDir, KernelDir ++ init:archive_extension()]),
    file:delete(Archive),
    {ok, Archive} = create_archive(Archive, [KernelDir]),

    {ok, Peer, Node, BootPid} = complete_start_node(),

    BeamName = "inet.beam",
    ok = test_archive(Node, Archive, KernelDir, BeamName),

    %% Cleanup
    peer:stop(Peer),
    unlink(BootPid),
    exit(BootPid, kill),
    ok.

%% Read files from primary archive.
primary_archive(Config) when is_list(Config) ->
    %% Copy the orig files to priv_dir
    PrivDir = proplists:get_value(priv_dir, Config),
    Archive = filename:join([PrivDir, "primary_archive.zip"]),
    file:delete(Archive),
    DataDir = proplists:get_value(data_dir, Config),
    {ok, _} = zip:create(Archive, ["primary_archive"],
			 [{compress, []}, {cwd, DataDir}]),
    {ok, _} = zip:extract(Archive, [{cwd, PrivDir}]),
    TopDir = filename:join([PrivDir, "primary_archive"]),

    %% Compile the code
    DictDir = "primary_archive_dict-1.0",
    DummyDir = "primary_archive_dummy",
    ok = compile_app(TopDir, DictDir),
    ok = compile_app(TopDir, DummyDir),

    %% Create the archive
    {ok, TopFiles} = file:list_dir(TopDir),
    {ok, {_, ArchiveBin}} = zip:create(Archive, TopFiles,
				       [memory, {compress, []}, {cwd, TopDir}]),

    %% Use temporary node to simplify cleanup
    {ok,Peer,Node} = ?CT_PEER(),
    {_,_,_} = rpc:call(Node, erlang, date, []),

    %% Set primary archive 
    ExpectedEbins = [Archive, DictDir ++ "/ebin", DummyDir ++ "/ebin"],
    io:format("ExpectedEbins: ~p\n", [ExpectedEbins]),
    {ok, FileInfo} = ?PRIM_FILE:read_file_info(Archive),
    {ok, Ebins} = rpc:call(Node, erl_prim_loader, set_primary_archive,
			   [Archive, ArchiveBin, FileInfo,
			    fun escript:parse_file/1]),
    ExpectedEbins = lists:sort(Ebins), % assert

    {ok, TopFiles2} = rpc:call(Node, erl_prim_loader, list_dir, [Archive]),
    [DictDir, DummyDir] = lists:sort(TopFiles2),
    BeamName = "primary_archive_dict_app.beam",
    ok = test_archive(Node, Archive, DictDir, BeamName),

    %% Cleanup
    {ok, []} = rpc:call(Node, erl_prim_loader, set_primary_archive,
			[undefined, undefined, undefined,
			 fun escript:parse_file/1]),
    peer:stop(Peer),
    ok = file:delete(Archive),
    ok.

test_archive(Node, TopDir, AppDir, BeamName) ->
    %% List dir
    io:format("test_archive: ~p\n", [rpc:call(Node, erl_prim_loader, list_dir, [TopDir])]),
    {ok, TopFiles} = rpc:call(Node, erl_prim_loader, list_dir, [TopDir]),
    true = lists:member(AppDir, TopFiles),
    AbsAppDir = TopDir ++ "/" ++ AppDir,
    {ok, AppFiles} = rpc:call(Node, erl_prim_loader, list_dir, [AbsAppDir]),
    true = lists:member("ebin", AppFiles),
    Ebin = AbsAppDir ++ "/ebin",
    {ok, EbinFiles} = rpc:call(Node, erl_prim_loader, list_dir, [Ebin]),
    Beam = Ebin ++ "/" ++ BeamName,
    true = lists:member(BeamName, EbinFiles),
    error = rpc:call(Node, erl_prim_loader, list_dir, [TopDir ++ "/no_such_file"]),
    error = rpc:call(Node, erl_prim_loader, list_dir, [TopDir ++ "/ebin/no_such_file"]),

    %% File info
    {ok, #file_info{type = directory}} =
	rpc:call(Node, erl_prim_loader, read_file_info, [TopDir]),
    {ok, #file_info{type = directory}} =
	rpc:call(Node, erl_prim_loader, read_file_info, [Ebin]),
    {ok, #file_info{type = regular} = FI}  =
	rpc:call(Node, erl_prim_loader, read_file_info, [Beam]),
    error = rpc:call(Node, erl_prim_loader, read_file_info, [TopDir ++ "/no_such_file"]),
    error = rpc:call(Node, erl_prim_loader, read_file_info, [TopDir ++ "/ebin/no_such_file"]),

    %% Get file
    {ok, Bin, Beam} = rpc:call(Node, erl_prim_loader, get_file, [Beam]),
    if
	FI#file_info.size =:= byte_size(Bin) -> ok;
	true -> exit({FI#file_info.size, byte_size(Bin)})
    end,
    error = rpc:call(Node, erl_prim_loader, get_file, ["/no_such_file"]),
    error = rpc:call(Node, erl_prim_loader, get_file, ["/ebin/no_such_file"]),
    ok.

create_archive(Archive, AppDirs) ->
    LibDir = code:lib_dir(),
    Opts = [{compress, []}, {cwd, LibDir}],
    io:format("zip:create(~p,\n\t~p,\n\t~p).\n", [Archive, AppDirs, Opts]),
    zip:create(Archive, AppDirs, Opts).


%% Read virtual directories from archive.
virtual_dir_in_archive(Config) when is_list(Config) ->
    PrivDir = proplists:get_value(priv_dir, Config),
    Data = <<"A little piece of data.">>,
    ArchiveBase = "archive_with_virtual_dirs",
    Archive = filename:join([PrivDir, ArchiveBase ++ init:archive_extension()]),
    FileBase = "a_data_file.beam",
    EbinBase = "ebin",
    FileInArchive = filename:join([ArchiveBase, EbinBase, FileBase]),
    BinFiles = [{FileInArchive, Data}],
    Opts = [{compress, []}],
    file:delete(Archive),
    io:format("zip:create(~p,\n\t~p,\n\t~p).\n", [Archive, BinFiles, Opts]),
    {ok, Archive} = zip:create(Archive, BinFiles, Opts),

    %% Verify that there is no directories
    {ok, BinFiles} = zip:unzip(Archive, [memory]),

    FullPath = filename:join([Archive, FileInArchive]),
    {ok, _} = erl_prim_loader:read_file_info(FullPath),

    %% Read one virtual dir
    EbinDir = filename:dirname(FullPath),
    {ok, _} = erl_prim_loader:read_file_info(EbinDir),
    {ok, [FileBase]} = erl_prim_loader:list_dir(EbinDir),

    %% Read another virtual dir
    AppDir = filename:dirname(EbinDir),
    {ok, _} = erl_prim_loader:read_file_info(AppDir),
    {ok, [EbinBase]} = erl_prim_loader:list_dir(AppDir),

    %% Cleanup
    ok = erl_prim_loader:purge_archive_cache(),
    ok = file:delete(Archive),
    ok.

%%%
%%% Helper functions.
%%%

complete_start_node() ->
    BootPid = start_boot_server(),
    {ok, Peer, Node} = ?CT_PEER(start_node_using_inet_args()),
    {ok, Peer, Node, BootPid}.

start_boot_server() ->
    %% Many linux systems define:
    %%   127.0.0.1 localhost
    %%   127.0.1.1 somehostname
    %% Therefore, to allow the tests to work on those kind of systems,
    %% also include "localhost" in the list of allowed hosts.

    Hosts = [host(),ip_str("localhost")],
    {ok,BootPid} = erl_boot_server:start_link(Hosts),
    BootPid.

start_node_using_inet_args() ->
    IpStr = ip_str(host()),
    ["-loader", "inet", "-hosts", IpStr].


ip_str({A, B, C, D}) ->
    lists:concat([A, ".", B, ".", C, ".", D]);
ip_str(Host) ->
    {ok,Ip} = inet:getaddr(Host, inet),
    ip_str(Ip).

host() ->
    {ok,Host} = inet:gethostname(),
    Host.

compile_app(TopDir, AppName) ->
    AppDir = filename:join([TopDir, AppName]),
    SrcDir = filename:join([AppDir, "src"]),
    OutDir = filename:join([AppDir, "ebin"]),
    {ok, Files} = file:list_dir(SrcDir),
    compile_files(Files, SrcDir, OutDir).

compile_files([File | Files], SrcDir, OutDir) ->
    case filename:extension(File) of
	".erl" ->
	    AbsFile = filename:join([SrcDir, File]),
	    case compile:file(AbsFile, [{outdir, OutDir}]) of
		{ok, _Mod} ->
		    compile_files(Files, SrcDir, OutDir);
		Error ->
		    {compilation_error, AbsFile, OutDir, Error}
	    end;
	_ ->
	    compile_files(Files, SrcDir, OutDir)
    end;
compile_files([], _, _) ->
    ok.

