%%
%% %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(erlsrv).
-moduledoc false.

%% Purpose : Control the external erlsrv program.

%%-compile(export_all).
-export([get_all_services/0,get_service/1,get_service/2,store_service/1,
	 store_service/2,
	 new_service/3, new_service/4, disable_service/2,
	 enable_service/2, disable_service/1, enable_service/1, 
	 remove_service/1, erlsrv/1, rename_service/2,
	 rename_service/3]).

erlsrv(EVer) ->
    Root = code:root_dir(),
    filename:join([Root, "erts-" ++ EVer, "bin", "erlsrv.exe"]).

current_version() ->
    hd(string:lexemes(erlang:system_info(version),"_ ")).

%%% Returns {ok, Output} | failed | {error, Reason}
run_erlsrv(Command) ->
    run_erlsrv(current_version(),Command).
run_erlsrv(EVer, Command) ->
    case catch(open_port({spawn, "\"" ++ erlsrv(EVer) ++ "\" " ++ Command},
			 [{line,1000}, in, eof])) of
	{'EXIT',{Reason,_}} ->
	    {port_error, Reason};
	Port ->
	    case read_all_data(Port) of
		[] ->
		    failed;
		 X ->
		    {ok, X}
	    end
    end.

run_erlsrv_interactive(EVer, Commands) ->
    case catch(open_port({spawn, "\""++ erlsrv(EVer) ++ "\" readargs"},
			 [{line,1000}, eof])) of
	{'EXIT',{Reason,_}} ->
	    {port_error, Reason};
	Port ->
	    write_all_data(Port, Commands),
	    case read_all_data(Port) of
		[] ->
		    failed;
		 X ->
		    {ok, X}
	    end
    end.

write_all_data(Port,[]) ->
    Port ! {self(), {command, io_lib:nl()}},
    ok;
write_all_data(Port,[H|T]) ->
    Port ! {self(), {command, unicode:characters_to_binary([H,io_lib:nl()])}},
    write_all_data(Port,T).

read_all_data(Port) ->
    Data0 = lists:reverse(read_all_data(Port,[],[])),
    %% Convert from utf8 to a list of chars
    [unicode:characters_to_list(list_to_binary(Data)) || Data <- Data0].

read_all_data(Port,Line,Lines) ->
    receive
	{Port, {data, {noeol,Data}}} ->
	    read_all_data(Port,Line++Data,Lines);
	{Port, {data, {eol,Data}}} ->
	    read_all_data(Port,[],[Line++Data|Lines]);
	{Port,_Other} ->
	    Port ! {self(), close},
	    receive
		{Port, closed} ->
		    case Line of
			[] -> Lines;
			_ -> [Line|Lines]
		    end
	    end
    end.


%%% Get all registered erlsrv services.
get_all_services() ->
    case run_erlsrv("list") of
	failed ->
	    [];
	{ok, [_]} ->
	    [];
	{ok, [_H|T]} ->
	    F = fun(X) ->
			hd(string:lexemes(X,"\t "))
		end,
	    lists:map(F,T);
	_ ->
	    {error, external_program_failed}
    end.

disable_service(ServiceName) ->
    disable_service(current_version(), ServiceName).
disable_service(EVer, ServiceName) ->
    run_erlsrv(EVer, "disable " ++ ServiceName).
enable_service(ServiceName) ->
    enable_service(current_version(), ServiceName).
enable_service(EVer, ServiceName) ->
    run_erlsrv(EVer, "enable " ++ ServiceName).
remove_service(ServiceName) ->
    run_erlsrv("remove " ++ ServiceName).
rename_service(FromName, ToName) ->
    rename_service(current_version(), FromName, ToName).
rename_service(EVer, FromName, ToName) ->
    run_erlsrv(EVer, "rename " ++ FromName ++ " " ++ ToName).

%%% Get all information about a service
%%% Returns [{Field,Value | []} ...]
%%% Field is one of:
%%% servicename : The service name (equal to parameter...)
%%% stopaction : The erlang expression that shall stop the node
%%% onfail : Action to take when erlang fails unexpectedly
%%% machine : Full pathname of the erlang machine or start_erl program
%%% workdir : The initial working directory of the erlang machine
%%% sname | name : The short name of the node
%%% priority : The OS priority of the erlang process
%%% args : All arguments correctly parsed into a list of strings
%%% comment : The service description
%%% internalservicename : The windows internal service name
%%% env : A list of environment variables and values [{"VAR", "VALUE"}]
%%% Example:
%%% [{servicename,"kalle_R4A"},
%%%  {stopaction,"erlang:halt()."},
%%%  {args,["-boot", "nisse","--","-reldir", "c:\myapproot"]}
%%%  {env,[{"FOO","BAR"},{"VEGETABLE","TOMATO"}]}]

get_service(ServiceName) ->
    get_service(current_version(), ServiceName).
get_service(EVer, ServiceName) ->   
    case run_erlsrv(EVer, "list " ++ ServiceName) of
	failed ->
	    {error, no_such_service};
	{port_error, Reason} ->
	    {error, {port_error, Reason}};
	{ok, Data} ->
	    Table = [{"Service name",servicename,[]},
		     {"StopAction",stopaction, []},
		     {"OnFail",onfail, "ignore"},
		     {"Machine",machine, []},
		     {"WorkDir",workdir, []},
		     {"SName",sname, []},
		     {"Name",name, []},
		     {"Priority",priority, "default"},
		     {"DebugType",debugtype, "none"},
		     {"Args",args,[]},
		     {"InternalServiceName",internalservicename,[]},
		     {"Comment",comment,[]}], 
	    %% Env has special treatment...
	    F = fun(X) ->
			{Name,Value} = splitline(X),
			case lists:keysearch(Name,1,Table) of
			    {value,{Name,_Atom,Value}} ->
				[];
			    {value,{Name,Atom,_}} ->
				{Atom,Value};
			    _ ->
				[]
			end
		end,
            %%% First split by Env:
	    {Before, After} = split_by_env(Data),
	    FirstPass = lists:flatten(lists:map(F,Before)),
	    %%% If the arguments are there, split them to
	    SecondPass = split_arglist(FirstPass),
	    %%% And now, if After contains anything, that is vwat to
	    %%% have in the environment list...
	    EnvParts = lists:map(
			 fun(S) ->
				 X = string:trim(S, leading, "$\t"),
				 case hd(string:lexemes(X,"=")) of
				     X ->
					 %% Can this happen?
					 {X,""};
				     Y ->
					 {Y,
					  lists:sublist(X,length(Y)+2,
							length(X))}
				 end
			 end,
			 After),
	    case EnvParts of
		[] ->
		    SecondPass;
		_ ->
		    lists:append(SecondPass,[{env,EnvParts}])
	    end
    end.


store_service(Service) ->
    store_service(current_version(),Service).
store_service(EmulatorVersion,Service) ->
    case lists:keysearch(servicename,1,Service) of
	false ->
	    {error, no_servicename};
	{value, {_,Name}} ->
	    {Action,Service1} = case get_service(EmulatorVersion,Name) of
			 {error, no_such_service} ->
			     {"add",Service};
			 _ ->
			     {"set",
			      lists:keydelete(internalservicename,1,Service)}
		     end,
	    Commands = [Action | build_commands(Name, Service1)],
	    case run_erlsrv_interactive(EmulatorVersion,Commands) of
		{ok, _} ->
		    ok;
		X ->
		    {error, X}
	    end;
	_ ->
	    {error, malformed_description}
    end.

build_commands(Action, Service) ->
    [ Action | lists:reverse(build_commands2(Service,[]))].

build_commands2([],A) ->
    A;
build_commands2([{env,[]}|T],A) ->
    build_commands2(T,A);
build_commands2([{env,[{Var,Val}|Et]}|T],A) ->
    build_commands2([{env,Et}|T],[Var ++ "=" ++ Val, "-env" | A]);
build_commands2([{servicename,_}|T],A) ->
    build_commands2(T,A);
build_commands2([{Atom,[]} | T],A) ->
    build_commands2(T,["-" ++ atom_to_list(Atom) | A]);
build_commands2([{args,L}|T],A) ->
    build_commands2(T,[concat_args(L),"-args"| A]);
build_commands2([{Atom,Value} | T],A) ->
    build_commands2(T,[Value, "-" ++ atom_to_list(Atom) | A]).

concat_args([H|T]) ->
    H ++ concat_args2(T).
concat_args2([]) ->
    "";
concat_args2([H|T]) ->
    " " ++ H ++ concat_args2(T).


new_service(NewServiceName, OldService, Data) ->
    new_service(NewServiceName, OldService, Data, []).
new_service(NewServiceName, OldService, Data, RestartName) -> 
    Tmp0 = lists:keydelete(internalservicename,1,OldService), %Remove when 
						% creating new service from 
						% old.
    Tmp1 = lists:keyreplace(servicename, 1, Tmp0, 
			    {servicename, NewServiceName}),
    Tmp = case lists:keysearch(env,1,Tmp1) of
	      {value, {env,Env0}} ->
		  Env1 = lists:keydelete("ERLSRV_SERVICE_NAME",1,Env0),
		  lists:keyreplace(env,1,Tmp1,
				   {env, [{"ERLSRV_SERVICE_NAME", 
					   RestartName} | 
					  Env1]});
	      _ ->
		  Tmp1
	  end,

    ArgsTmp = case lists:keysearch(args, 1, Tmp) of
	       false ->
		   [];
	       {value, {args, OldArgs}} ->
		   OldArgs
	   end,
    Args = backstrip(ArgsTmp,"++"), %% Remove trailing ++, has no meaning
    {Found, Tail} = lists:foldr(fun(A,{Flag,AccIn}) -> 
					case {Flag, A} of 
					    {true, _} -> {Flag,AccIn}; 
					    {false, "++"} -> {true, AccIn};
					    _ -> {false, [A|AccIn]} 
					end 
				end, {false,[]}, Args),
    
    {OtherFlags, _DataDir} = case Found of
				true ->
				    check_tail(Tail);
				false ->
				    {[], false} 
			    end,
    NewArgs1 = case Data of
		   [] ->
		       OtherFlags;
		   _ -> 
		       ["-data", Data| OtherFlags]
	       end,
    case Found of
	false ->
	    A = case NewArgs1 of
		    [] ->
			[];
		    _ ->
			["++" | NewArgs1]
		end,
	    case {Args,A} of
		{[],[]} ->
		    Tmp;
		{[],_} ->
		    Tmp ++ [{args, A}];
		{_,_} ->
		    lists:keyreplace(args, 1, Tmp, {args, Args ++ A})
	    end;
	true ->
	    StripArgs = backstrip(Args,["++"|Tail]),
	    NewArgs2 = case NewArgs1 of
			   [] ->
			       [];
			   _ ->
			       ["++" |NewArgs1]
		       end,
	    NewArgs = StripArgs ++ NewArgs2,
	    lists:keyreplace(args, 1, Tmp, {args, NewArgs})
    end.
    

backstrip(List,Tail) ->
    lists:reverse(backstrip2(lists:reverse(List),lists:reverse(Tail))).
backstrip2([A|T1],[A|T2]) ->
    backstrip2(T1,T2);
backstrip2(L,_) ->
    L.

check_tail(Tail) ->
    {A,B} = check_tail(Tail, [], false),
    {lists:reverse(A),B}.

check_tail([], OtherFlags, DataDir) ->
    {OtherFlags, DataDir};
check_tail(["-data", TheDataDir|T], OtherFlags, _DataDir) ->
    check_tail(T, OtherFlags, TheDataDir);
check_tail([H|T],OtherFlags,DataDir) ->
    check_tail(T,[H|OtherFlags],DataDir).




%%% Recursive, The list is small
split_arglist([]) ->
    [];
split_arglist([{args,Str}|T]) ->    
    [{args,parse_arglist(Str)}|T];
split_arglist([H|T]) ->
    [H|split_arglist(T)].

%% Not recursive, may be long...
parse_arglist(Str) ->
    lists:reverse(parse_arglist(Str,[])).
parse_arglist(Str,Accum) ->
    Stripped = string:trim(Str, leading),
    case length(Stripped) of
	0 ->
	    Accum;
	_ ->
	    {Next, Rest} = pick_argument(Str),
	    parse_arglist(Rest,[Next | Accum])
    end.

pick_argument(Str) ->
    {Rev,Rest} = pick_argument(normal,Str,[]),
    {lists:reverse(Rev),Rest}.

pick_argument(_,[],Acc) ->
    {Acc, ""};
pick_argument(normal,[$ |T],Acc) ->
    {Acc,T};
pick_argument(normal,[$\\|T],Acc) ->
    pick_argument(normal_escaped,T,[$\\|Acc]);
pick_argument(normal,[$"|T],Acc) ->
    pick_argument(quoted,T,[$"|Acc]);
pick_argument(normal_escaped,[$"|T],Acc) ->
    pick_argument(bquoted,T,[$"|Acc]);
pick_argument(normal_escaped,[A|T],Acc) ->
    pick_argument(normal,T,[A|Acc]);
pick_argument(quoted_escaped,[H|T],Acc) ->
    pick_argument(quoted,T,[H|Acc]);
pick_argument(quoted,[$"|T],Acc) ->
    pick_argument(normal,T,[$"|Acc]);
pick_argument(quoted,[$\\|T],Acc) ->
    pick_argument(quoted_escaped,T,[$\\|Acc]);
pick_argument(quoted,[H|T],Acc) ->
    pick_argument(quoted,T,[H|Acc]);
pick_argument(bquoted_escaped,[$"|T],Acc) ->
    pick_argument(normal,T,[$"|Acc]);
pick_argument(bquoted_escaped,[H|T],Acc) ->
    pick_argument(bquoted,T,[H|Acc]);
pick_argument(bquoted,[$\\|T],Acc) ->
    pick_argument(bquoted_escaped,T,[$\\|Acc]);
pick_argument(bquoted,[H|T],Acc) ->
    pick_argument(bquoted,T,[H|Acc]);
pick_argument(normal,[H|T],Acc) ->
    pick_argument(normal,T,[H|Acc]).

split_helper("Env:",{Where,0}) ->
    {Where + 1, Where};
split_helper(_, {Where,Pos}) ->
    {Where + 1, Pos}.

split_by_env(Data) ->
    %%% Find Env...
    case lists:foldl(fun split_helper/2,{0,0},Data) of
	{_,0} ->
	    %% Not found, hmmmm....
	    {Data,[]};
	{Len,Pos} ->
	    {lists:sublist(Data,Pos),lists:sublist(Data,Pos+2,Len)}
    end.
			

splitline(Line) ->
    case string:split(Line, ":") of
        [_] ->
	    {Line, ""};
        [N, V] ->
            {N, string:slice(V, 1)}
    end.
