%%
%% %CopyrightBegin%
%%
%% Copyright Ericsson AB 1997-2013. All Rights Reserved.
%%
%% The contents of this file are subject to the Erlang Public License,
%% Version 1.1, (the "License"); you may not use this file except in
%% compliance with the License. You should have received a copy of the
%% Erlang Public License along with this software. If not, it can be
%% retrieved online at http://www.erlang.org/.
%%
%% Software distributed under the License is distributed on an "AS IS"
%% basis, WITHOUT WARRANTY OF ANY KIND, either express or implied. See
%% the License for the specific language governing rights and limitations
%% under the License.
%%
%% %CopyrightEnd%
%%
-module(os).

%% Provides a common operating system interface.

-export([type/0, version/0, cmd/1, find_executable/1, find_executable/2]).

-include("file.hrl").

%%% BIFs

-export([getenv/0, getenv/1, getpid/0, putenv/2, timestamp/0, unsetenv/1]).

-spec getenv() -> [string()].

getenv() -> erlang:nif_error(undef).

-spec getenv(VarName) -> Value | false when
      VarName :: string(),
      Value :: string().

getenv(_) ->
    erlang:nif_error(undef).

-spec getpid() -> Value when
      Value :: string().

getpid() ->
    erlang:nif_error(undef).

-spec putenv(VarName, Value) -> true when
      VarName :: string(),
      Value :: string().

putenv(_, _) ->
    erlang:nif_error(undef).

-spec timestamp() -> Timestamp when
      Timestamp :: erlang:timestamp().

timestamp() ->
    erlang:nif_error(undef).

-spec unsetenv(VarName) -> true when
      VarName :: string().

unsetenv(_) ->
    erlang:nif_error(undef).

%%% End of BIFs

-spec type() -> {Osfamily, Osname} when
      Osfamily :: unix | win32 | ose,
      Osname :: atom().

type() ->
    erlang:system_info(os_type).

-spec version() -> VersionString | {Major, Minor, Release} when
      VersionString :: string(),
      Major :: non_neg_integer(),
      Minor :: non_neg_integer(),
      Release :: non_neg_integer().
version() ->
    erlang:system_info(os_version).

-spec find_executable(Name) -> Filename | 'false' when
      Name :: string(),
      Filename :: string().
find_executable(Name) ->
    case os:getenv("PATH") of
	false -> find_executable(Name, []);
	Path  -> find_executable(Name, Path)
    end.

-spec find_executable(Name, Path) -> Filename | 'false' when
      Name :: string(),
      Path :: string(),
      Filename :: string().
find_executable(Name, Path) ->
    Extensions = extensions(),
    case filename:pathtype(Name) of
	relative ->
	    find_executable1(Name, split_path(Path), Extensions);
	_ ->
	    case verify_executable(Name, Extensions, Extensions) of
		{ok, Complete} ->
		    Complete;
		error ->
		    false
	    end
    end.

find_executable1(Name, [Base|Rest], Extensions) ->
    Complete0 = filename:join(Base, Name),
    case verify_executable(Complete0, Extensions, Extensions) of
	{ok, Complete} ->
	    Complete;
	error ->
	    find_executable1(Name, Rest, Extensions)
    end;
find_executable1(_Name, [], _Extensions) ->
    false.

verify_executable(Name0, [Ext|Rest], OrigExtensions) ->
    Name1 = Name0 ++ Ext,
    case file:read_file_info(Name1) of
	{ok, #file_info{type=regular,mode=Mode}}
	when Mode band 8#111 =/= 0 ->
	    %% XXX This test for execution permission is not fool-proof
	    %% on Unix, since we test if any execution bit is set.
	    {ok, Name1};
	_ ->
	    verify_executable(Name0, Rest, OrigExtensions)
    end;
verify_executable(Name, [], OrigExtensions) when OrigExtensions =/= [""] -> %% Windows
    %% Will only happen on windows, hence case insensitivity
    case can_be_full_name(string:to_lower(Name),OrigExtensions) of
	true ->
	    verify_executable(Name,[""],[""]);
	_ ->
	    error
    end;
verify_executable(_, [], _) ->
    error.

can_be_full_name(_Name,[]) ->
    false;
can_be_full_name(Name,[H|T]) ->
    case lists:suffix(H,Name) of %% Name is in lowercase, cause this is a windows thing
	true ->
	    true;
	_ ->
	    can_be_full_name(Name,T)
    end.

split_path(Path) ->
    case type() of
	{win32, _} ->
	    {ok,Curr} = file:get_cwd(),
	    split_path(Path, $;, [], [Curr]);
	_ ->
	    split_path(Path, $:, [], [])
    end.

split_path([Sep|Rest], Sep, Current, Path) ->
    split_path(Rest, Sep, [], [reverse_element(Current)|Path]);
split_path([C|Rest], Sep, Current, Path) ->
    split_path(Rest, Sep, [C|Current], Path);
split_path([], _, Current, Path) ->
    lists:reverse(Path, [reverse_element(Current)]).

reverse_element([]) -> ".";
reverse_element([$"|T]) ->	%"
    case lists:reverse(T) of
	[$"|List] -> List;	%"
	List -> List ++ [$"]	%"
    end;
reverse_element(List) ->
    lists:reverse(List).

-spec extensions() -> [string(),...].
%% Extensions in lower case
extensions() ->
    case type() of
	{win32, _} -> [".exe",".com",".cmd",".bat"];
	{unix, _} -> [""]
    end.

%% Executes the given command in the default shell for the operating system.
-spec cmd(Command) -> string() when
      Command :: atom() | io_lib:chars().
cmd(Cmd) ->
    validate(Cmd),
    Bytes = case type() of
		{unix, _} ->
		    unix_cmd(Cmd);
		{win32, Wtype} ->
		    Command0 = case {os:getenv("COMSPEC"),Wtype} of
				  {false,windows} -> lists:concat(["command.com /c", Cmd]);
				  {false,_} -> lists:concat(["cmd /c", Cmd]);
				  {Cspec,_} -> lists:concat([Cspec," /c",Cmd])
			      end,
		    %% open_port/2 awaits string() in Command, but io_lib:chars() can be
		    %% deep lists according to io_lib module description.
		    Command = lists:flatten(Command0),
		    Port = open_port({spawn, Command}, [stream, in, eof, hide]),
		    get_data(Port, [])
	    end,
    String = unicode:characters_to_list(list_to_binary(Bytes)),
    if  %% Convert to unicode list if possible otherwise return bytes
	is_list(String) -> String;
	true -> Bytes
    end.

unix_cmd(Cmd) ->
    Tag = make_ref(),
    {Pid,Mref} = erlang:spawn_monitor(
		   fun() ->
			   process_flag(trap_exit, true),
			   Port = start_port(),
			   erlang:port_command(Port, mk_cmd(Cmd)),
			   exit({Tag,unix_get_data(Port)})
		   end),
    receive
	{'DOWN',Mref,_,Pid,{Tag,Result}} ->
	    Result;
	{'DOWN',Mref,_,Pid,Reason} ->
	    exit(Reason)
    end.

%% The -s flag implies that only the positional parameters are set,
%% and the commands are read from standard input. We set the
%% $1 parameter for easy identification of the resident shell.
%%
-define(ROOT,          "/").
-define(ROOT_ANDROID,  "/system").
-define(SHELL, "bin/sh -s unix:cmd 2>&1").
-define(PORT_CREATOR_NAME, os_cmd_port_creator).

%%
%% Serializing open_port through a process to avoid smp lock contention
%% when many concurrent os:cmd() want to do vfork (OTP-7890).
%%
-spec start_port() -> port().
start_port() ->
    Ref = make_ref(),
    Request = {Ref,self()},
    {Pid, Mon} = case whereis(?PORT_CREATOR_NAME) of
		     undefined ->
			 spawn_monitor(fun() ->
					       start_port_srv(Request)
				       end);
		     P ->
			 P ! Request,
			 M = erlang:monitor(process, P),
			 {P, M}
		 end,
    receive
	{Ref, Port} when is_port(Port) ->
	    erlang:demonitor(Mon, [flush]),
	    Port;
	{Ref, Error} ->
	    erlang:demonitor(Mon, [flush]),
	    exit(Error);
	{'DOWN', Mon, process, Pid, _Reason} ->
	    start_port()
    end.

start_port_srv(Request) ->
    %% We don't want a group leader of some random application. Use
    %% kernel_sup's group leader.
    {group_leader, GL} = process_info(whereis(kernel_sup),
				      group_leader),
    true = group_leader(GL, self()),
    process_flag(trap_exit, true),
    StayAlive = try register(?PORT_CREATOR_NAME, self())
		catch
		    error:_ -> false
		end,
    start_port_srv_handle(Request),
    case StayAlive of
	true -> start_port_srv_loop();
	false -> exiting
    end.

start_port_srv_handle({Ref,Client}) ->
    Path  = case lists:reverse(erlang:system_info(system_architecture)) of
      % androideabi
      "ibaediordna" ++ _ -> filename:join([?ROOT_ANDROID, ?SHELL]);
      _ -> filename:join([?ROOT, ?SHELL])
    end,
    Reply = try open_port({spawn, Path},[stream]) of
		Port when is_port(Port) ->
		    (catch port_connect(Port, Client)),
		    unlink(Port),
		    Port
	    catch
		error:Reason ->
		    {Reason,erlang:get_stacktrace()}
	    end,
    Client ! {Ref,Reply},
    ok.

start_port_srv_loop() ->
    receive
	{Ref, Client} = Request when is_reference(Ref),
				     is_pid(Client) ->
	    start_port_srv_handle(Request);
	_Junk ->
	    ok
    end,
    start_port_srv_loop().

%%
%%  unix_get_data(Port) -> Result
%%
unix_get_data(Port) ->
    unix_get_data(Port, []).

unix_get_data(Port, Sofar) ->
    receive
	{Port,{data, Bytes}} ->
	    case eot(Bytes) of
		{done, Last} ->
		    lists:flatten([Sofar|Last]);
		more  ->
		    unix_get_data(Port, [Sofar|Bytes])
	    end;
	{'EXIT', Port, _} ->
	    lists:flatten(Sofar)
    end.

%%
%% eot(String) -> more | {done, Result}
%%
eot(Bs) ->
    eot(Bs, []).

eot([4| _Bs], As) ->
    {done, lists:reverse(As)};
eot([B| Bs], As) ->
    eot(Bs, [B| As]);
eot([], _As) ->
    more.

%%
%% mk_cmd(Cmd) -> {ok, ShellCommandString} | {error, ErrorString}
%%
%% We do not allow any input to Cmd (hence commands that want
%% to read from standard input will return immediately).
%% Standard error is redirected to standard output.
%%
%% We use ^D (= EOT = 4) to mark the end of the stream.
%%
mk_cmd(Cmd) when is_atom(Cmd) ->		% backward comp.
    mk_cmd(atom_to_list(Cmd));
mk_cmd(Cmd) ->
    %% We insert a new line after the command, in case the command
    %% contains a comment character.
    [$(, unicode:characters_to_binary(Cmd), "\n) </dev/null; echo  \"\^D\"\n"].


validate(Atom) when is_atom(Atom) ->
    ok;
validate(List) when is_list(List) ->
    validate1(List).

validate1([C|Rest]) when is_integer(C) ->
    validate1(Rest);
validate1([List|Rest]) when is_list(List) ->
    validate1(List),
    validate1(Rest);
validate1([]) ->
    ok.

get_data(Port, Sofar) ->
    receive
	{Port, {data, Bytes}} ->
	    get_data(Port, [Sofar|Bytes]);
	{Port, eof} ->
	    Port ! {self(), close},
	    receive
		{Port, closed} ->
		    true
	    end,
	    receive
		{'EXIT',  Port,  _} ->
		    ok
	    after 1 ->				% force context switch
		    ok
	    end,
	    lists:flatten(Sofar)
    end.
