%%
%% %CopyrightBegin%
%%
%% SPDX-License-Identifier: Apache-2.0
%%
%% Copyright Ericsson AB 1998-2025. 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(code_server).
-moduledoc false.

-compile(nowarn_deprecated_catch).

%% This file holds the server part of the code_server.

-export([start_link/1,
	 call/1, absname/1, get_mode/0,
	 is_loaded/1, is_sticky/1,
	 system_code_change/4,
	 error_msg/2, info_msg/2
	]).

-include_lib("kernel/include/file.hrl").
-include_lib("stdlib/include/ms_transform.hrl").

-import(lists, [foreach/2]).
-define(moddb, code_server).

-record(state, {supervisor :: pid(),
		root :: file:name_all(),
		path :: [{file:name_all(), nocache | integer()}],
                path_cache = #{} :: #{integer() => #{string() => []}},
		moddb :: ets:table(),
		namedb :: ets:table(),
                on_load = #{} :: #{module() => {on_load_file(), client_pid(), on_load_pid()}},
                loading = #{} :: #{module() => [{loading_action(), client_pid()}]}}).
-type state() :: #state{}.
-type loading_action() :: load_module | get_object_code | finish_on_load.

%% Note: this type comes from code:load_binary/3 (type was not exported)
-type on_load_file() :: 'cover_compiled' | 'preloaded' | file:filename().

%% client-side pid() (i.e., process that call code:load_binary/et al),
%% to which 'code_server' needs to send a response when on_load finishes.
-type client_pid() :: pid().

%% This pid() refers to the spawned process calling
%% 'erlang:call_on_load_function(Mod)'
-type on_load_pid() :: pid().

-spec start_link([term()]) -> {'ok', pid()}.
start_link(Args) ->
    Ref = make_ref(),
    Parent = self(),
    Init = fun() -> init(Ref, Parent, Args) end,
    spawn_link(Init),
    receive 
	{Ref,Res} -> Res
    end.

is_loaded(Mod) ->
    case ets:lookup(?moddb, Mod) of
        [{Mod,File}] -> {file,File};
        [] -> false
    end.

is_sticky(Mod) ->
    is_sticky(Mod, ?moddb).

get_mode() ->
    persistent_term:get(?MODULE).

%% -----------------------------------------------------------
%% Init the code_server process.
%% -----------------------------------------------------------

init(Ref, Parent, [Root,Mode]) ->
    register(?MODULE, self()),
    process_flag(trap_exit, true),

    Db = ets:whereis(ets:new(?moddb, [named_table, protected])),
    foreach(fun (M) ->
		    %% Pre-loaded modules are always sticky.
		    ets:insert(Db, [{M,preloaded},{{sticky,M},true}])
	    end, erlang:pre_loaded()),
    Loaded0 = init:fetch_loaded(),
    Loaded = [{M,filename:join([P])} || {M,P} <- Loaded0], %Normalize.
    ets:insert(Db, Loaded),

    IPath =
	case Mode of
	    interactive ->
		LibDir = filename:append(Root, "lib"),
		{ok,Dirs} = erl_prim_loader:list_dir(LibDir),
		Paths = make_path(LibDir, Dirs),
		UserLibPaths = get_user_lib_dirs(),
		["."] ++ UserLibPaths ++ Paths;
	    _ ->
		[]
	end,

    Path = add_loader_path(IPath, Mode),
    State = #state{supervisor = Parent,
		   root = Root,
		   path = Path,
		   moddb = Db,
		   namedb = create_namedb(Path, Root)},

    persistent_term:put(?MODULE, Mode),
    Parent ! {Ref,{ok,self()}},
    loop(State).

get_user_lib_dirs() ->
    case os:getenv("ERL_LIBS") of
	LibDirs0 when is_list(LibDirs0) ->
	    Sep =
		case os:type() of
		    {win32, _} -> $;;
		    _          -> $:
		end,
	    LibDirs = split_paths(LibDirs0, Sep, [], []),
	    get_user_lib_dirs_1(LibDirs);
	false ->
	    []
    end.

get_user_lib_dirs_1([Dir|DirList]) ->
    case erl_prim_loader:list_dir(Dir) of
	{ok, Dirs} ->
	    Paths = make_path(Dir, Dirs),
	    %% Only add paths trailing with ./ebin.
	    [P || P <- Paths, filename:basename(P) =:= "ebin"] ++
		get_user_lib_dirs_1(DirList);
	error ->
	    get_user_lib_dirs_1(DirList)
    end;
get_user_lib_dirs_1([]) -> [].


split_paths([S|T], S, Path, Paths) ->
    split_paths(T, S, [], [lists:reverse(Path) | Paths]);
split_paths([C|T], S, Path, Paths) ->
    split_paths(T, S, [C|Path], Paths);
split_paths([], _S, Path, Paths) ->
    lists:reverse(Paths, [lists:reverse(Path)]).

-spec call(term()) -> term().
call(Req) ->
    Ref = erlang:monitor(process, ?MODULE),
    ?MODULE ! {code_call, self(), Req},
    receive 
	{?MODULE, Reply} ->
            erlang:demonitor(Ref,[flush]),
	    Reply;
        {'DOWN',Ref,process,_,_} ->
            exit({'DOWN',code_server,Req})
    end.

reply(Pid, Res) ->
    Pid ! {?MODULE, Res}.

loop(#state{supervisor=Supervisor}=State0) ->
    receive 
	{code_call, Pid, Req} ->
	    case handle_call(Req, Pid, State0) of
		{reply, Res, State} ->
		    _ = reply(Pid, Res),
		    loop(State);
		{noreply, State} ->
		    loop(State);
		{stop, Why, stopped, State} ->
		    system_terminate(Why, Supervisor, [], State)
	    end;
	{'EXIT', Supervisor, Reason} ->
	    system_terminate(Reason, Supervisor, [], State0);
	{system, From, Msg} ->
	    handle_system_msg(running,Msg, From, Supervisor, State0);
        {{'LOADER_DOWN', Mod}, _Ref, process, _Pid, _Res} ->
            State = run_loader_next(Mod, State0),
            loop(State);
        {{'ON_LOAD_DOWN', Mod}, _Ref, process, _Pid, Res} ->
            State = on_load_down(Mod, Res, State0),
            loop(State);
	_Msg ->
	    loop(State0)
    end.

%%%%%%%%%%%%%%%%%%%%%%%%%%%
%% System upgrade

handle_system_msg(SysState, Msg, From, Parent, Misc) ->
    case do_sys_cmd(SysState, Msg, Parent, Misc) of
	{suspended, Reply, NMisc} ->
	    gen_reply(From, Reply),
	    suspend_loop(suspended, Parent, NMisc);
	{running, Reply, NMisc} ->
	    gen_reply(From, Reply),
	    system_continue(Parent, [], NMisc)
    end.

gen_reply({To, Tag}, Reply) ->
    catch To ! {Tag, Reply}.

%%-----------------------------------------------------------------
%% When a process is suspended, it can only respond to system
%% messages.
%%-----------------------------------------------------------------
suspend_loop(SysState, Parent, Misc) ->
    receive
	{system, From, Msg} ->
	    handle_system_msg(SysState, Msg, From, Parent, Misc);
	{'EXIT', Parent, Reason} ->
	    system_terminate(Reason, Parent, [], Misc)
    end.

do_sys_cmd(_, suspend, _Parent, Misc) ->
    {suspended, ok, Misc};
do_sys_cmd(_, resume, _Parent, Misc) ->
    {running, ok, Misc};
do_sys_cmd(SysState, get_status, Parent, Misc) ->
    Status = {status, self(), {module, ?MODULE},
	      [get(), SysState, Parent, [], Misc]},
    {SysState, Status, Misc};
do_sys_cmd(SysState, {debug, _What}, _Parent, Misc) ->
    {SysState,ok,Misc};
do_sys_cmd(suspended, {change_code, Module, Vsn, Extra}, _Parent, Misc0) ->
    {Res, Misc} = 
	case catch ?MODULE:system_code_change(Misc0, Module, Vsn, Extra)  of
	    {ok, _} = Ok -> Ok;
	    Else -> {{error, Else}, Misc0}
	end,
    {suspended, Res, Misc};
do_sys_cmd(SysState, Other, _Parent, Misc) ->
    {SysState, {error, {unknown_system_msg, Other}}, Misc}.

system_continue(_Parent, _Debug, State) ->
    loop(State).

system_terminate(_Reason, _Parent, _Debug, _State) ->
    %% error_msg("~p terminating: ~p~n ", [?MODULE, Reason]),
    exit(shutdown).

-spec system_code_change(state(), module(), term(), term()) -> {'ok', state()}.
system_code_change(State, _Module, _OldVsn, _Extra) ->
    {ok, State}.

%%
%% The gen_server call back functions.
%%

handle_call({stick_dir,Dir}, _From, S) ->
    {reply,stick_dir(Dir, true, S),S};

handle_call({unstick_dir,Dir}, _From, S) ->
    {reply,stick_dir(Dir, false, S),S};

handle_call({stick_mod,Mod}, _From, S) ->
    {reply,stick_mod(Mod, true, S),S};

handle_call({unstick_mod,Mod}, _From, S) ->
    {reply,stick_mod(Mod, false, S),S};

handle_call({dir,Dir}, _From, S) ->
    Root = S#state.root,
    Resp = do_dir(Root,Dir,S#state.namedb),
    {reply,Resp,S};

handle_call({add_path,Where,Dir0,Control}, _From,
	    #state{namedb=Namedb,path=Path0,path_cache=Cache0}=S) ->
    {Resp,Path,Cache} = add_path(Where, Dir0, Path0, Control, Cache0, Namedb),
    {reply,Resp,S#state{path=Path,path_cache=Cache}};

handle_call({add_paths,Where,Dirs0,Control}, _From,
	    #state{namedb=Namedb,path=Path0,path_cache=Cache0}=S) ->
    {Resp,Path,Cache} = add_paths(Where, Dirs0, Path0, Control, Cache0, Namedb),
    {reply,Resp,S#state{path=Path,path_cache=Cache}};

handle_call({set_path,PathList,Control}, _From,
	    #state{namedb=Namedb,root=Root,path=Path0}=S) ->
    {Resp,Path,NewDb} = set_path(PathList, Path0, Control, Namedb, Root),
    {reply,Resp,S#state{namedb=NewDb,path=Path,path_cache=#{}}};

handle_call({del_path,Name}, _From,
	    #state{namedb=Namedb,path=Path0,path_cache=Cache0}=S) ->
    {Resp,Path,Cache} = del_path(Name, Path0, Cache0, Namedb),
    {reply,Resp,S#state{path=Path,path_cache=Cache}};

handle_call({del_paths,Names}, _From,
            #state{namedb=Namedb,path=Path0,path_cache=Cache0}=S) ->
    {Resp,Path,Cache} = del_paths(Names, Path0, Cache0, Namedb),
    {reply,Resp,S#state{path=Path,path_cache=Cache}};

handle_call({replace_path,Name,Dir,Control}, _From,
	    #state{namedb=Namedb,path=Path0,path_cache=Cache0}=S) ->
    {Resp,Path,Cache} = replace_path(Name, Dir, Path0, Control, Cache0, Namedb),
    {reply,Resp,S#state{path=Path,path_cache=Cache}};

handle_call(get_path, _From, S) ->
    {reply,[P || {P, _Cache} <- S#state.path],S};

handle_call({where_is_file,File}, _From,
            #state{path=Path,path_cache=Cache0}=S) ->
    {Resp,Cache} = where_is_file(Path, File, Cache0),
    {reply,Resp,S#state{path_cache=Cache}};

handle_call(clear_cache, _From, S) ->
    {reply,ok,S#state{path_cache=#{}}};

handle_call({load_module,PC,Mod,File,Purge,EnsureLoaded}, From, S)
  when is_atom(Mod) ->
    case Purge andalso erlang:module_loaded(Mod) of
	true -> do_purge(Mod);
	false -> ok
    end,
    schedule_or_run_loader({load_module,PC,File,EnsureLoaded}, From, Mod, S);

handle_call({load_ok,PC,Mod,File,Ref}, From, S) ->
    erlang:demonitor(Ref, [flush]),
    {noreply, run_loader({load_module,PC,File,true}, From, Mod, S)};

handle_call({load_error,Mod,Ref}, _From, S) ->
    erlang:demonitor(Ref, [flush]),
    {reply,ok,run_loader_next(Mod, S)};

handle_call({delete,Mod}, _From, St) when is_atom(Mod) ->
    case catch erlang:delete_module(Mod) of
	true ->
	    ets:delete(St#state.moddb, Mod),
	    {reply,true,St};
	_ ->
	    {reply,false,St}
    end;

handle_call({purge,Mod}, _From, St) when is_atom(Mod) ->
    {reply,do_purge(Mod),St};

handle_call({soft_purge,Mod}, _From, St) when is_atom(Mod) ->
    {reply,do_soft_purge(Mod),St};

handle_call(all_loaded, _From, S) ->
    Db = S#state.moddb,
    {reply,all_loaded(Db),S};

handle_call({get_object_code,Mod}, _From, St0) when is_atom(Mod) ->
    case get_object_code(St0, Mod) of
	{Bin,FName,St1} -> {reply,{Mod,Bin,FName},St1};
	{error,St1} -> {reply,error,St1}
    end;

handle_call({get_object_code_for_loading,Mod}, From, St0) when is_atom(Mod) ->
    schedule_or_run_loader(get_object_code, From, Mod, St0);

handle_call(stop,_From, S) ->
    {stop,normal,stopped,S};

handle_call({finish_loading,Prepared,EnsureLoaded}, _From, S) ->
    {reply,finish_loading(Prepared, EnsureLoaded, S),S};

handle_call(Other,_From, S) ->
    error_msg(" ** Codeserver*** ignoring ~w~n ",[Other]),
    {noreply,S}.

%% --------------------------------------------------------------
%% Path handling functions.
%% --------------------------------------------------------------

%%
%% Create the initial path. 
%%
make_path(BundleDir, Bundles0) ->
    Bundles = choose_bundles(Bundles0),
    make_path(BundleDir, Bundles, []).

choose_bundles(Bundles) ->
    ArchiveExt = archive_extension(),
    Bs = lists:sort([create_bundle(B, ArchiveExt) || B <- Bundles]),
    [FullName || {_Name,_NumVsn,FullName} <-
		     choose(lists:reverse(Bs), [], ArchiveExt)].

create_bundle(FullName, ArchiveExt) ->
    BaseName = filename:basename(FullName, ArchiveExt),
    case split_base(BaseName) of
	{Name, VsnStr} ->
	    case vsn_to_num(VsnStr) of
		{ok, VsnNum} ->
		    {Name,VsnNum,FullName};
		false ->
		    {FullName,[0],FullName}
	    end;
	_ ->
	    {FullName,[0],FullName}
    end.

%% Convert "X.Y.Z. ..." to [K, L, M| ...]
vsn_to_num(Vsn) ->
    case is_vsn(Vsn) of
	true ->
	    {ok, [list_to_integer(S) || S <- split(Vsn, ".")]};
	_  ->
	    false
    end.

is_vsn(Str) when is_list(Str) ->
    Vsns = split(Str, "."),
    lists:all(fun is_numstr/1, Vsns).

is_numstr(Cs) ->
    lists:all(fun (C) when $0 =< C, C =< $9 -> true; 
		  (_)                       -> false
	      end, Cs).

split(Cs, S) ->
    split1(Cs, S, []).

split1([C|S], Seps, Toks) ->
    case lists:member(C, Seps) of
	true -> split1(S, Seps, Toks);
	false -> split2(S, Seps, Toks, [C])
    end;
split1([], _Seps, Toks) ->
    lists:reverse(Toks).

split2([C|S], Seps, Toks, Cs) ->
    case lists:member(C, Seps) of
	true -> split1(S, Seps, [lists:reverse(Cs)|Toks]);
	false -> split2(S, Seps, Toks, [C|Cs])
    end;
split2([], _Seps, Toks, Cs) ->
    lists:reverse([lists:reverse(Cs)|Toks]).
   
join([H1, H2| T], S) ->
    H1 ++ S ++ join([H2| T], S);
join([H], _) ->
    H;
join([], _) ->
    [].

choose([{Name,NumVsn,NewFullName}=New|Bs], Acc, ArchiveExt) ->
    case lists:keyfind(Name, 1, Acc) of
	{_, NV, OldFullName} when NV =:= NumVsn ->
	    case filename:extension(OldFullName) =:= ArchiveExt of
		false ->
		    choose(Bs,Acc, ArchiveExt);
		true ->
		    Acc2 = lists:keystore(Name, 1, Acc, New),
		    choose(Bs,Acc2, ArchiveExt)
	    end;
	{_, _, _} ->
	    choose(Bs,Acc, ArchiveExt);
	false ->
	    choose(Bs,[{Name,NumVsn,NewFullName}|Acc], ArchiveExt)
    end;
choose([],Acc, _ArchiveExt) ->
    Acc.

make_path(_, [], Res) ->
    Res;
make_path(BundleDir, [Bundle|Tail], Res) ->
    Dir = filename:append(BundleDir, Bundle),
    Ebin = filename:append(Dir, "ebin"),
    %% First try with /ebin
    case is_dir(Ebin) of
	true ->
	    make_path(BundleDir, Tail, [Ebin|Res]);
	false ->
	    %% Second try with archive
	    Ext = archive_extension(),
	    Base = filename:basename(Bundle, Ext),
	    Ebin2 = filename:join([BundleDir, Base ++ Ext, Base, "ebin"]),
	    Ebins = 
		case split_base(Base) of
		    {AppName,_} ->
			Ebin3 = filename:join([BundleDir, Base ++ Ext,
					       AppName, "ebin"]),
			[Ebin3, Ebin2, Dir];
		    _ ->
			[Ebin2, Dir]
		end,
	    case try_ebin_dirs(Ebins) of
		{ok,FoundEbin} ->
		    make_path(BundleDir, Tail, [FoundEbin|Res]);
		error ->
		    make_path(BundleDir, Tail, Res)
	    end
    end.

try_ebin_dirs([Ebin|Ebins]) ->
    case is_dir(Ebin) of
	true -> {ok,Ebin};
	false -> try_ebin_dirs(Ebins)
    end;
try_ebin_dirs([]) ->
    error.


%%
%% Add the erl_prim_loader path.
%%
add_loader_path(IPath0,Mode) ->
    {ok,PrimP0} = erl_prim_loader:get_path(),

    %% All boot paths except for "." are cached by default but this can be disabled.
    %% -pa and -pz are never cached by default.
    case Mode of
        embedded ->
            cache_path(strip_path(PrimP0, Mode));  % i.e. only normalize
        _ ->
            Pa0 = get_arg(pa),
            Pz0 = get_arg(pz),

            Pa = patch_path(Pa0),
            Pz = patch_path(Pz0),
            PrimP = patch_path(PrimP0),
            IPath = patch_path(IPath0),

            Path0 = exclude_pa_pz(PrimP,Pa,Pz),
            Path1 = strip_path(Path0, Mode),
            Path2 = merge_path(Path1, IPath, []),
            Path3 = cache_path(Path2),
            add_pa_pz(Path3,Pa,Pz)
    end.

cache_path(Path) ->
    Default = cache_boot_paths(),
    [{P, do_cache_path(P, Default)} || P <- Path].

do_cache_path(".", _Default) -> nocache;
do_cache_path(_Path, Default) -> cache_key(Default).

cache_key(nocache) -> nocache;
cache_key(cache) -> erlang:unique_integer().

cache_boot_paths() ->
    case init:get_argument(cache_boot_paths) of
        {ok,[["false"]]} -> nocache;
        _ -> cache
    end.

patch_path(Path) ->
    case check_path(Path) of
	{ok, NewPath} -> NewPath;
	{error, _Reason} -> Path
    end.

%% As the erl_prim_loader path includes the -pa and -pz
%% directories they have to be removed first !!
exclude_pa_pz(P0,Pa,[]) ->
    P0 -- Pa;
exclude_pa_pz(P0,Pa,Pz) ->
    lists:reverse(lists:reverse(P0 -- Pa) -- Pz).

%%
%% Keep only 'valid' paths in code server.
%% Only if mode is interactive, in an embedded
%% system we can't rely on file.
%%

strip_path([P0|Ps], Mode) ->
    P = filename:join([P0]), % Normalize
    case check_path([P]) of
	{ok, [NewP]} ->
	    [NewP|strip_path(Ps, Mode)];
	_ when Mode =:= embedded ->
	    [P|strip_path(Ps, Mode)];
	_ ->
	    strip_path(Ps, Mode)
    end;
strip_path(_, _) ->
    [].
    
%%
%% Add only non-existing paths.
%% Also delete other versions of directories,
%% e.g. .../test-3.2/ebin should exclude .../test-*/ebin (and .../test/ebin).
%% Put the Path directories first in resulting path.
%%
merge_path(Path,["."|IPath],Acc) ->
    RPath = merge_path1(Path,IPath,Acc),
    ["."|lists:delete(".",RPath)];
merge_path(Path,IPath,Acc) ->
    merge_path1(Path,IPath,Acc).

merge_path1([P|Path],IPath,Acc) ->
    case lists:member(P,Acc) of
	true ->
	    merge_path1(Path,IPath,Acc); % Already added
	false ->
	    IPath1 = exclude(P,IPath),
	    merge_path1(Path,IPath1,[P|Acc])
    end;
merge_path1(_,IPath,Acc) ->
    lists:reverse(Acc) ++ IPath.

add_pa_pz(Path0, Patha, Pathz) ->
    {_,Path1,_Cache1} = add_paths(first,Patha,Path0,nocache,#{},false),
    case Pathz of
        [] ->
            Path1;
        _ ->
            {_,Path2,_Cache2} = add_paths(first,Pathz,lists:reverse(Path1),nocache,#{},false),
            lists:reverse(Path2)
    end.

get_arg(Arg) ->
    case init:get_argument(Arg) of
	{ok, Values} ->
	    lists:append(Values);
	_ ->
	    []
    end.

%%
%% Exclude other versions of Dir or duplicates.
%% Return a new Path.
%%
exclude(Dir,Path) ->
    Name = get_name(Dir),
    [D || D <- Path, 
	  D =/= Dir, 
	  get_name(D) =/= Name].

%%
%% Get the "Name" of a directory. A directory in the code server path
%% have the following form: .../Name-Vsn or .../Name
%% where Vsn is any sortable term (the newest directory is sorted as
%% the greatest term).
%%
%%
get_name(Dir) ->
    get_name_from_splitted(filename:split(Dir)).

get_name_from_splitted([DirName,"ebin"]) ->
    discard_after_hyphen(DirName);
get_name_from_splitted([DirName]) ->
    discard_after_hyphen(DirName);
get_name_from_splitted([_|T]) ->
    get_name_from_splitted(T);
get_name_from_splitted([]) ->
    "".						%No name.

discard_after_hyphen("-"++_) ->
    [];
discard_after_hyphen([H|T]) ->
    [H|discard_after_hyphen(T)];
discard_after_hyphen([]) ->
    [].

split_base(BaseName) ->
    case split(BaseName, "-") of
	[_, _|_] = Toks ->
	    Vsn = lists:last(Toks),
	    AllButLast = lists:droplast(Toks),
	    {join(AllButLast, "-"),Vsn};
	[_|_] ->
	    BaseName
    end.

check_path(Path) ->
    PathChoice = init:code_path_choice(),
    ArchiveExt = archive_extension(),
    do_check_path(Path, PathChoice, ArchiveExt, []).
    
do_check_path([], _PathChoice, _ArchiveExt, Acc) -> 
    {ok, lists:reverse(Acc)};
do_check_path([Dir | Tail], PathChoice, ArchiveExt, Acc) ->
    case is_dir(Dir) of
	true ->
	    do_check_path(Tail, PathChoice, ArchiveExt, [Dir | Acc]);
	false when PathChoice =:= strict ->
	    %% Be strict. Only use dir as explicitly stated
	    {error, bad_directory};
	false when PathChoice =:= relaxed ->
	    %% Be relaxed
	    case catch lists:reverse(filename:split(Dir)) of
		{'EXIT', _} ->
		    {error, bad_directory};
		["ebin", App] ->
		    Dir2 = filename:join([App ++ ArchiveExt, App, "ebin"]),
		    case is_dir(Dir2) of
			true ->
			    do_check_path(Tail, PathChoice, ArchiveExt, [Dir2 | Acc]);
			false ->
			    {error, bad_directory}
		    end;
		["ebin", App, OptArchive | RevTop] ->
		    Ext = filename:extension(OptArchive),
		    Base = filename:basename(OptArchive, Ext),
		    Dir2 = 
			if
			    Ext =:= ArchiveExt, Base =:= App ->
				%% .../app-vsn.ez/app-vsn/ebin
				Top = lists:reverse(RevTop),
				filename:join(Top ++ [App, "ebin"]);
			    Ext =:= ArchiveExt ->
				%% .../app-vsn.ez/xxx/ebin
				{error, bad_directory};
			    true ->
				%% .../app-vsn/ebin
				Top = lists:reverse([OptArchive | RevTop]),
				filename:join(Top ++ [App ++ ArchiveExt, App, "ebin"])
			end,
		    case is_dir(Dir2) of
			true ->
			    do_check_path(Tail, PathChoice, ArchiveExt, [Dir2 | Acc]);
			false ->
			    {error, bad_directory}
		    end;
		_ ->
		    {error, bad_directory}
	    end
    end.

%%
%% Add new path(s).
%%
add_path(Where,Dir0,Path,Control,Cache,NameDb) when is_list(Dir0) ->
    Dir = filename:join([Dir0]), % Normalize
    case check_path([Dir]) of
	{ok, [NewDir]} ->
	    do_add(Where,NewDir,Path,Control,Cache,NameDb);
	Error ->
	    {Error, Path, Cache}
    end;
add_path(_,_,Path,_,Cache,_) ->
    {{error, bad_directory}, Path, Cache}.

%%
%% If the new directory is added first or if the directory didn't exist
%% the name-directory table must be updated.
%% If NameDb is false we should NOT update NameDb as it is done later
%% then the table is created :-)
%%
do_add(first,Dir,Path0,Control,Cache,NameDb) ->
    update(Dir, NameDb),
    Pair = {Dir, cache_key(Control)},

    case lists:keytake(Dir,1,Path0) of
        false ->
            {true, [Pair|Path0], Cache};

        {value, {_Dir, CacheKey}, Path1} ->
            {true, [Pair|Path1], maps:remove(CacheKey, Cache)}
    end;
do_add(last,Dir,Path,Control,Cache,NameDb) ->
    Pair = {Dir, cache_key(Control)},

    case lists:keyfind(Dir,1,Path) of
	{_Dir, CacheKey} ->
	    {true, lists:keyreplace(Dir,1,Path,Pair), maps:remove(CacheKey, Cache)};
	false ->
	    maybe_update(Dir, NameDb),
	    {true, Path ++ [Pair], Cache}
    end.

%% Do not update if the same name already exists !
maybe_update(Dir, NameDb) ->
    (lookup_name(get_name(Dir), NameDb) =:= false) andalso update(Dir, NameDb).

update(_Dir, false) ->
    true;
update(Dir, NameDb) ->
    replace_name(Dir, NameDb).

%%
%% Set a completely new path.
%%
set_path(NewPath, OldPath, Control, NameDb, Root) ->
    case check_path(NewPath) of
	{ok, NewPath2} ->
	    ets:delete(NameDb),
            NewPath3 = [{P, cache_key(Control)} || P <- NewPath2],
	    NewDb = create_namedb(NewPath3, Root),
	    {true, NewPath3, NewDb};
	Error ->
	    {Error, OldPath, NameDb}
    end.

%% Handle a table of name-directory pairs.
%% The priv_dir/1 and lib_dir/1 functions will have
%% an O(1) lookup.
create_namedb(Path, Root) ->
    Db = ets:new(code_names,[]),
    init_namedb(lists:reverse(Path), Db),

    case lookup_name("erts", Db) of
        {ok, _, _, _} ->
            %% erts is part of code path
            ok;
        false ->
            %% No erts in code path, check if this is a source
            %% repo and if so use that.
            ErtsDir = filename:join(Root, "erts"),
            case erl_prim_loader:read_file_info(ErtsDir) of
                error ->
                    ok;
                _ ->
                    do_insert_name("erts", ErtsDir, Db)
            end
    end,
    Db.

init_namedb([{P, _Cache}|Path], Db) ->
    insert_dir(P, Db),
    init_namedb(Path, Db);
init_namedb([], _) ->
    ok.

-ifdef(NOTUSED).
clear_namedb([P|Path], Db) ->
    delete_name_dir(P, Db),
    clear_namedb(Path, Db);
clear_namedb([], _) ->
    ok.
-endif.

%% Dir must be a complete pathname (not only a name).
insert_dir(Dir, Db) ->
    Splitted = filename:split(Dir),
    case get_name_from_splitted(Splitted) of
	Name when Name /= "ebin", Name /= "." ->
	    Name;
	_ ->
	    SplittedAbsName = filename:split(absname(Dir)),
	    Name = get_name_from_splitted(SplittedAbsName)
    end,
    AppDir = filename:join(del_ebin_1(Splitted)),
    do_insert_name(Name, AppDir, Db).

insert_name(Name, Dir, Db) ->
    AppDir = del_ebin(Dir),
    do_insert_name(Name, AppDir, Db).

do_insert_name(".", _, _) ->
    %% "." is most likely not an archive, avoid looking for subdirs
    true;
do_insert_name(Name, AppDir, Db) ->
    {Base, SubDirs} = archive_subdirs(AppDir),
    ets:insert(Db, {Name, AppDir, Base, SubDirs}),
    true.

archive_subdirs(AppDir) ->
    Base = filename:basename(AppDir),
    Dirs = case split_base(Base) of
	       {Name, _} -> [Name, Base];
	    _ -> [Base]
	end,
    Ext = archive_extension(),
    try_archive_subdirs(AppDir ++ Ext, Base, Dirs).

try_archive_subdirs(Archive, Base, [Dir | Dirs]) ->
    ArchiveDir = filename:append(Archive, Dir),
    case erl_prim_loader:list_dir(ArchiveDir) of
	{ok, Files} ->
	    IsDir = fun(RelFile) ->
			    File = filename:append(ArchiveDir, RelFile),
			    is_dir(File)
		    end,
	    {Dir, lists:filter(IsDir, Files)};
	_ ->
	    try_archive_subdirs(Archive, Base, Dirs)
    end;
try_archive_subdirs(_Archive, Base, []) ->
    {Base, []}.

%%
%% Delete a directory from Path.
%% Name can be either the the name in .../Name[-*] or
%% the complete directory name.
%%
del_path(Name0,Path,Cache,NameDb) ->
    case catch filename:join([to_list(Name0)]) of
	{'EXIT',_} ->
	    {{error,bad_name},Path};
	Name ->
	    case del_path1(Name,Path,Cache,NameDb,[]) of
                {NPath, NCache} -> {true, NPath, NCache};
                false -> {false, Path, Cache}
            end
    end.

del_path1(Name,[{P, CacheKey}|Path],Cache,NameDb,Acc) ->
    case get_name(P) of
	Name ->
	    delete_name(Name, NameDb),
	    insert_old_shadowed(Name, Path, NameDb),
	    {lists:reverse(Acc, Path), maps:remove(CacheKey, Cache)};
	_ when Name =:= P ->
	    case delete_name_dir(Name, NameDb) of
		true -> insert_old_shadowed(get_name(Name), Path, NameDb);
		false -> ok
	    end,
            {lists:reverse(Acc, Path), maps:remove(CacheKey, Cache)};
	_ ->
            del_path1(Name, Path, Cache, NameDb, [{P, CacheKey} | Acc])
    end;
del_path1(_,[],_,_,_) ->
    false.

insert_old_shadowed(Name, [{P, _Cache}|Path], NameDb) ->
    case get_name(P) of
	Name -> insert_name(Name, P, NameDb);
	_    -> insert_old_shadowed(Name, Path, NameDb)
    end;
insert_old_shadowed(_, [], _) ->
    ok.

%%
%% Replace an old occurrence of an directory with name .../Name[-*].
%% If it does not exist, put the new directory last in Path.
%%
replace_path(Name,Dir,Path,Control,Cache,NameDb) ->
    case catch check_pars(Name,Dir) of
	{ok,N,D} ->
	    replace_path1(N,D,Path,Control,Cache,NameDb,[]);
	{'EXIT',_} ->
	    {{error,{badarg,[Name,Dir]}},Path,Cache};
	Error ->
	    {Error,Path,Cache}
    end.

replace_path1(Name,Dir,[{P, CacheKey}|Path],Control,Cache,NameDb,Acc) ->
    case get_name(P) of
	Name ->
	    insert_name(Name, Dir, NameDb),
            NPath = lists:reverse(Acc, [{Dir, cache_key(Control)}|Path]),
	    {true, NPath, maps:remove(CacheKey, Cache)};
	_ ->
            NAcc = [{P, CacheKey}|Acc],
	    replace_path1(Name,Dir,Path,Control,Cache,NameDb,NAcc)
    end;
replace_path1(Name,Dir,[],Control,Cache,NameDb,Acc) ->
    insert_name(Name, Dir, NameDb),
    NPath = lists:reverse(Acc, [{Dir, cache_key(Control)}]),
    {true, NPath, Cache}.

check_pars(Name,Dir) ->
    N = to_list(Name),
    D = filename:join([to_list(Dir)]), % Normalize
    case get_name(Dir) of
	N ->
	    case check_path([D]) of
		{ok, [NewD]} ->
		    {ok,N,NewD};
		Error ->
		    Error
	    end;
	_ ->
	    {error,bad_name}
    end.

del_ebin(Dir) ->
    filename:join(del_ebin_1(filename:split(Dir))).

del_ebin_1([Parent,App,"ebin"]) ->
    case filename:basename(Parent) of
	[] ->
	    %% Parent is the root directory
	    [Parent,App];
	_ ->
	    Ext = archive_extension(),
	    case filename:basename(Parent, Ext) of
		Parent ->
		    %% Plain directory.
		    [Parent,App];
		Archive ->
		    %% Archive.
		    [Archive]
	    end
    end;
del_ebin_1(Path = [_App,"ebin"]) ->
    del_ebin_1(filename:split(absname(filename:join(Path))));
del_ebin_1(["ebin"]) ->
    del_ebin_1(filename:split(absname("ebin")));
del_ebin_1([H|T]) ->
    [H|del_ebin_1(T)];
del_ebin_1([]) ->
    [].

replace_name(Dir, Db) ->
    case get_name(Dir) of
	Dir ->
	    false;
	Name ->
	    delete_name(Name, Db),
	    insert_name(Name, Dir, Db)
    end.

delete_name(Name, Db) ->
    ets:delete(Db, Name).

delete_name_dir(Dir, Db) ->
    case get_name(Dir) of
	Dir  -> false;
	Name ->
	    Dir0 = del_ebin(Dir),
	    case lookup_name(Name, Db) of
		{ok, Dir0, _Base, _SubDirs} ->
		    ets:delete(Db, Name), 
		    true;
		_ -> false
	    end
    end.

lookup_name(Name, Db) ->
    case ets:lookup(Db, Name) of
	[{Name, Dir, Base, SubDirs}] -> {ok, Dir, Base, SubDirs};
	_ -> false
    end.

%%
%% Fetch a directory.
%%
do_dir(Root,lib_dir,_) ->
    filename:append(Root, "lib");
do_dir(Root,root_dir,_) ->
    Root;
do_dir(_Root,compiler_dir,NameDb) ->
    case lookup_name("compiler", NameDb) of
	{ok, Dir, _Base, _SubDirs} -> Dir;
	_  -> ""
    end;
do_dir(_Root,{lib_dir,Name},NameDb) ->
    case catch lookup_name(to_list(Name), NameDb) of
	{ok, Dir, _Base, _SubDirs} -> Dir;
	_         -> {error, bad_name}
    end;
do_dir(_Root,{lib_dir,Name,SubDir0},NameDb) ->
    SubDir = atom_to_list(SubDir0),
    case catch lookup_name(to_list(Name), NameDb) of
	{ok, Dir, Base, SubDirs} ->
	    case lists:member(SubDir, SubDirs) of
		true ->
		    %% Subdir is in archive
		    filename:join([Dir ++ archive_extension(),
				   Base,
				   SubDir]);
		false ->
		    %% Subdir is regular directory
		    filename:join([Dir, SubDir])
	    end;
	_  -> 
	    {error, bad_name}
    end;
do_dir(_Root,{priv_dir,Name},NameDb) ->
    do_dir(_Root,{lib_dir,Name,priv},NameDb);
do_dir(_, _, _) ->
    'bad request to code'.

stick_dir(Dir, Stick, St) ->
    case erl_prim_loader:list_dir(Dir) of
	{ok,Listing} ->
	    Mods = get_mods(Listing, objfile_extension()),
	    Db = St#state.moddb,
	    case Stick of
		true ->
		    foreach(fun (M) -> ets:insert(Db, {{sticky,M},true}) end, Mods);
		false ->
		    foreach(fun (M) -> ets:delete(Db, {sticky,M}) end, Mods)
	    end;
	Error -> 
	    Error
    end.

stick_mod(M, Stick, St) ->
    Db = St#state.moddb,
    case Stick of
	true ->
	    ets:insert(Db, {{sticky,M},true});
	false ->
	    ets:delete(Db, {sticky,M})
    end.

get_mods([File|Tail], Extension) ->
    case filename:extension(File) of
	Extension ->
	    [list_to_atom(filename:basename(File, Extension)) |
	     get_mods(Tail, Extension)];
	_ ->
	    get_mods(Tail, Extension)
    end;
get_mods([], _) -> [].

is_sticky(Mod, Db) ->
    erlang:module_loaded(Mod) andalso (ets:lookup(Db, {sticky, Mod}) =/= []).

add_paths(Where,[Dir|Tail],Path,Control,Cache,NameDb) ->
    {_,NPath,NCache} = add_path(Where,Dir,Path,Control,Cache,NameDb),
    add_paths(Where,Tail,NPath,Control,NCache,NameDb);
add_paths(_,_,Path,_,Cache,_) ->
    {ok,Path,Cache}.

del_paths([Name | Names],Path,Cache,NameDb) ->
    {_,NPath,NCache} = del_path(Name, Path, Cache, NameDb),
    del_paths(Names,NPath,NCache,NameDb);
del_paths(_,Path,Cache,_) ->
    {ok,Path,Cache}.

get_object_code(#state{path=Path,path_cache=Cache} = St, Mod) when is_atom(Mod) ->
    ModStr = atom_to_list(Mod),
    case erl_prim_loader:is_basename(ModStr) of
        true ->
            case mod_to_bin(Path, ModStr ++ objfile_extension(), Cache) of
                {Binary, File, NCache} ->
                    {Binary, File, St#state{path_cache=NCache}};

                {error, NCache} ->
                    {error, St#state{path_cache=NCache}}
            end;

        false ->
            {error, St}
    end.

mod_to_bin([{Dir, nocache}|Tail], ModFile, Cache) ->
    File = filename:append(Dir, ModFile),

    case erl_prim_loader:read_file(File) of
        error -> mod_to_bin(Tail, ModFile, Cache);
        {ok,Bin} -> {Bin, absname_when_relative(File), Cache}
    end;
mod_to_bin([{Dir, CacheKey}|Tail], ModFile, Cache) when is_integer(CacheKey) ->
    case with_cache(CacheKey, Dir, ModFile, Cache) of
        {true, Cache1} ->
            File = filename:append(Dir, ModFile),
            {missing, absname_when_relative(File), Cache1};
        {false, Cache1} ->
            mod_to_bin(Tail, ModFile, Cache1)
    end;
mod_to_bin([], _ModFile, Cache) ->
    {error, Cache}.

with_cache(CacheKey, Dir, ModFile, Cache) ->
    case Cache of
        #{CacheKey := Set} ->
            {is_map_key(ModFile, Set), Cache};

        #{} ->
            case erl_prim_loader:list_dir(Dir) of
                {ok, Entries} ->
                    Set = maps:from_keys(Entries, []),
                    {is_map_key(ModFile, Set), maps:put(CacheKey, Set, Cache)};
                error ->
                    {false, Cache}
            end
    end.

absname_when_relative(File) ->
    case filename:pathtype(File) of
        absolute -> File;
        _ -> absname(File)
    end.

absname(File) ->
    case erl_prim_loader:get_cwd() of
	{ok,Cwd} -> absname(File, Cwd);
	_Error -> File
    end.

absname(Name, AbsBase) ->
    case filename:pathtype(Name) of
	relative ->
	    filename:absname_join(AbsBase, Name);
	absolute ->
	    %% We must flatten the filename before passing it into join/1,
	    %% or we will get slashes inserted into the wrong places.
	    filename:join([filename:flatten(Name)]);
	volumerelative ->
	    absname_vr(filename:split(Name), filename:split(AbsBase), AbsBase)
    end.

%% Handles volumerelative names (on Windows only).

absname_vr(["/"|Rest1], [Volume|_], _AbsBase) ->
    %% Absolute path on current drive.
    filename:join([Volume|Rest1]);
absname_vr([[X, $:]|Rest1], [[X|_]|_], AbsBase) ->
    %% Relative to current directory on current drive.
    absname(filename:join(Rest1), AbsBase);
absname_vr([[X, $:]|Name], _, _AbsBase) ->
    %% Relative to current directory on another drive.
    Dcwd =
	case erl_prim_loader:get_cwd([X, $:]) of
	    {ok, Dir}  -> Dir;
	    error -> [X, $:, $/]
    end,
    absname(filename:join(Name), Dcwd).

do_purge(Mod) ->
    {_WasOld, DidKill} = erts_code_purger:purge(Mod),
    DidKill.

do_soft_purge(Mod) ->
    erts_code_purger:soft_purge(Mod).

is_dir(Path) ->
    case erl_prim_loader:read_file_info(Path) of
	{ok,#file_info{type=directory}} -> true;
	_ -> false
    end.

%%%
%%% Loading of multiple modules in parallel.
%%%

finish_loading(Prepared, EnsureLoaded, #state{moddb=Db}=St) ->
    Ps = [fun(L) -> finish_loading_ensure(L, EnsureLoaded) end,
	  fun(L) -> abort_if_pending_on_load(L, St) end,
	  fun(L) -> abort_if_sticky(L, Db) end,
	  fun(L) -> do_finish_loading(L, St) end],
    run(Ps, Prepared).

finish_loading_ensure(Prepared, true) ->
    {ok,[P || {M,_}=P <- Prepared, not erlang:module_loaded(M)]};
finish_loading_ensure(Prepared, false) ->
    {ok,Prepared}.

abort_if_pending_on_load(L, #state{on_load=OnLoad}) when map_size(OnLoad) =:= 0 ->
    {ok,L};
abort_if_pending_on_load(L, #state{on_load=OnLoad}) ->
    Pending = [{M,pending_on_load} ||
		  {M,_} <- L,
		  is_map_key(M, OnLoad)],
    case Pending of
	[] -> {ok,L};
	[_|_] -> {error,Pending}
    end.

abort_if_sticky(L, Db) ->
    Sticky = [{M,sticky_directory} || {M,_} <- L, is_sticky(M, Db)],
    case Sticky of
	[] -> {ok,L};
	[_|_] -> {error,Sticky}
    end.

do_finish_loading(Prepared, #state{moddb=Db}) ->
    MagicBins = [B || {_,{B,_}} <- Prepared],
    case erlang:finish_loading(MagicBins) of
	ok ->
	    MFs = [{M,F} || {M,{_,F}} <- Prepared],
	    true = ets:insert(Db, MFs),
	    ok;
	{Reason,Ms} ->
	    {error,[{M,Reason} || M <- Ms]}
    end.

run([F], Data) ->
    F(Data);
run([F|Fs], Data0) ->
    case F(Data0) of
	{ok,Data} ->
	    run(Fs, Data);
	{error,_}=Error ->
	    Error
    end.

%% -------------------------------------------------------
%% The module loading and on_load functionality.
%% -------------------------------------------------------

%% Because can be loaded by the client and the server,
%% we need to track which modules are being loaded to
%% avoid concurrent loading of them. The code server is
%% also responsible for "finish"ing modules and running
%% on_load callback, which we track here. To do this,
%% we queue loaders for a given module and either reply
%% to them or run them if a previous loader succeeded.

%% Schedules or runs a given loader action for a module.
schedule_or_run_loader(Action, Pid, Mod, #state{loading=Loading0} = St0) ->
    case Loading0 of
        #{Mod := Waiting} ->
            #state{on_load=OnLoad} = St0,
            case OnLoad of
                #{Mod := {_, _, Pid}} ->
                    {reply, {error, deadlock}, St0};
                _ ->
                    Loading = Loading0#{Mod := [{Action, Pid} | Waiting]},
                    {noreply, St0#state{loading=Loading}}
            end;
        _ ->
            Loading = Loading0#{Mod => []},
            St = St0#state{loading=Loading},
            {noreply, run_loader(Action, Pid, Mod, St)}
    end.

%% Runs the loader. If keep is returned,
%% it does not immediately start the next loader
%% because the current one is still running.
run_loader(Action, Pid, Mod, St0) ->
    case handle_loader(Action, Pid, Mod, St0) of
        {keep, St} ->
            St;
        {next, Reply, St} ->
            _ = reply(Pid, Reply),
            run_loader_next(Mod, St)
    end.

run_loader_next(Mod, #state{loading=Loading0} = St0) ->
    case Loading0 of
        #{Mod := [{Action, Pid} | Waiting]} ->
            Loading = Loading0#{Mod := Waiting},
            St = St0#state{loading=Loading},
            run_loader(Action, Pid, Mod, St);
        #{Mod := []} ->
            St0#state{loading=maps:remove(Mod, Loading0)}
    end.

handle_loader(get_object_code, Pid, Mod, St0) ->
    case erlang:module_loaded(Mod) of
        true ->
            {next, {module, Mod}, St0};
        false ->
            case get_object_code(St0, Mod) of
                {Bin, FName, St1} ->
                    Tag = {'LOADER_DOWN', Mod},
                    Ref = erlang:monitor(process, Pid, [{tag, Tag}]),
                    _ = reply(Pid, {Bin, FName, Ref}),
                    {keep, St1};
                {error, St1} ->
                    {next, {error, nofile}, St1}
            end
    end;

handle_loader({load_module, PC, File, EnsureLoaded}, Pid, Mod, St0) ->
    case EnsureLoaded andalso erlang:module_loaded(Mod) of
        true ->
            {next, {module, Mod}, St0};
        false ->
            case is_sticky(Mod, St0#state.moddb) of
                true ->
                    error_msg("Can't load module '~w' that resides in sticky dir\n", [Mod]),
                    {next, {error,sticky_directory}, St0};
                false ->
                    case erlang:finish_loading([PC]) of
                        ok ->
                            store_module_and_reply(File, Mod, St0);
                        {on_load,[Mod]} ->
                            schedule_on_load(File, Pid, Mod, St0);
                        {Error, [Mod]} ->
                            error_msg("Loading of ~ts failed: ~p\n", [File, Error]),
                            {next, {error, Error}, St0}
                    end
            end
    end;

handle_loader({finish_on_load, File, OnLoadRes}, _Pid, Mod, St0) ->
    Keep = OnLoadRes =:= ok,
    erts_code_purger:finish_after_on_load(Mod, Keep),
    case Keep of
        true ->
            store_module_and_reply(File, Mod, St0);
        false ->
            _ = finish_on_load_report(Mod, OnLoadRes),
            {next, {error, on_load_failure}, St0}
    end.

store_module_and_reply(File, Mod, St0) ->
    #state{moddb=Db, loading=Loading0} = St0,
    ets:insert(Db, {Mod, File}),
    %% Optimization: go ahead and notify all get_object_code
    %% loader actions that it has succeeded.
    Waiting = lists:filter(fun
                               ({get_object_code, Pid}) ->
                                   _ = reply(Pid, {module, Mod}),
                                   false;
                               ({_Action, _Pid}) ->
                                   true
                           end, maps:get(Mod, Loading0)),
    Loading = maps:put(Mod, Waiting, Loading0),
    {next, {module, Mod}, St0#state{loading=Loading}}.

schedule_on_load(File, Pid, Mod, St0) ->
    %% We use a separate state for on_load because we
    %% need to track them for abort_if_pending_on_load
    %% and we also need to be able to detect deadlocks.
    #state{on_load=OnLoad0} = St0,
    Fun = fun() ->
        Res = erlang:call_on_load_function(Mod),
        exit(Res)
    end,
    Tag = {'ON_LOAD_DOWN', Mod},
    {Loader, _} = spawn_opt(Fun, [{monitor, [{tag, Tag}]}]),
    OnLoad = maps:put(Mod, {File, Pid, Loader}, OnLoad0),
    {keep, St0#state{on_load=OnLoad}}.

on_load_down(Mod, OnLoadRes, #state{on_load=OnLoad0}=St0) ->
    {{File, Pid, _Loader}, OnLoad} = maps:take(Mod, OnLoad0),
    St = St0#state{on_load=OnLoad},
    run_loader({finish_on_load, File, OnLoadRes}, Pid, Mod, St).

finish_on_load_report(_Mod, Atom) when is_atom(Atom) ->
    %% No error reports for atoms.
    ok;
finish_on_load_report(Mod, Term) ->
    %% Play it very safe here. The error_logger module and
    %% modules it depend on may not be loaded yet and there
    %% would be a dead-lock if we called it directly
    %% from the code_server process.
    spawn(fun() ->
		  F = "The on_load function for module "
		      "~s returned:~n~P\n",

		  %% Express the call as an apply to simplify
		  %% the ext_mod_dep/1 test case.
		  E = error_logger,
		  E:warning_msg(F, [Mod,Term,10])
	  end).

%% -------------------------------------------------------
%% Internal functions.
%% -------------------------------------------------------

all_loaded(Db) ->
    Ms = ets:fun2ms(fun({M,_}=T) when is_atom(M) -> T end),
    ets:select(Db, Ms).

-spec error_msg(io:format(), [term()]) -> 'ok'.
error_msg(Format, Args) ->
    %% This is equal to calling logger:error/3 which we don't want to
    %% do from code_server. We don't want to call logger:timestamp()
    %% either.
    _ = try
            logger ! {log,error,Format,Args,
                      #{pid=>self(),
                        gl=>group_leader(),
                        time=>os:system_time(microsecond),
                        error_logger=>#{tag=>error}}}
        catch _:_ ->
                erlang:display({?MODULE,error}),
                erlang:display({Format,Args})
        end,
    ok.

-spec info_msg(io:format(), [term()]) -> 'ok'.
info_msg(Format, Args) ->
    %% This is equal to calling logger:info/3 which we don't want to
    %% do from code_server. We don't want to call logger:timestamp()
    %% either.
    catch logger ! {log,info,Format,Args,
                    #{pid=>self(),
                      gl=>group_leader(),
                      time=>os:system_time(microsecond),
                      error_logger=>#{tag=>info_msg}}},
    ok.

objfile_extension() ->
    init:objfile_extension().

archive_extension() ->
    init:archive_extension().

to_list(X) when is_list(X) -> X;
to_list(X) when is_atom(X) -> atom_to_list(X).

where_is_file([], _File, Cache) ->
    {non_existing, Cache};
where_is_file([{Dir, nocache} | Tail], File, Cache) ->
    Full = filename:append(Dir, File),
    case erl_prim_loader:read_file_info(Full) of
        {ok,_} ->
            {Full, Cache};
        _Error ->
            where_is_file(Tail, File, Cache)
    end;
where_is_file([{Dir, CacheKey} | Tail], File, Cache) when is_integer(CacheKey) ->
    case with_cache(CacheKey, Dir, File, Cache) of
        {true, Cache1} ->
            Full = filename:append(Dir, File),
            {Full, Cache1};
        {false, Cache1} ->
            where_is_file(Tail, File, Cache1)
    end.
