%%% vim:ts=4:sw=4:et %%%----------------------------------------------------------------------------- %%% @doc OS supporting commands %%% @author Serge Aleynikov %%% @end %%%----------------------------------------------------------------------------- %%% Date: 2015-12-10 %%%----------------------------------------------------------------------------- %%% Copyright (c) 2015 Serge Aleynikov %%% %%% Permission is hereby granted, free of charge, to any person %%% obtaining a copy of this software and associated documentation %%% files (the "Software"), to deal in the Software without restriction, %%% including without limitation the rights to use, copy, modify, merge, %%% publish, distribute, sublicense, and/or sell copies of the Software, %%% and to permit persons to whom the Software is furnished to do %%% so, subject to the following conditions: %%% %%% The above copyright notice and this permission notice shall be included %%% in all copies or substantial portions of the Software. %%% %%% THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, %%% EXPRESS OR IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF %%% MERCHANTABILITY, FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. %%% IN NO EVENT SHALL THE AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY %%% CLAIM, DAMAGES OR OTHER LIABILITY, WHETHER IN AN ACTION OF CONTRACT, %%% TORT OR OTHERWISE, ARISING FROM, OUT OF OR IN CONNECTION WITH THE %%% SOFTWARE OR THE USE OR OTHER DEALINGS IN THE SOFTWARE. %%%----------------------------------------------------------------------------- -module(osx). -author('saleyn@gmail.com'). -export([command/1, command/2, command/3, status/1]). -export([realpath/1, normalpath/1]). -ifdef(TEST). -include_lib("eunit/include/eunit.hrl"). -endif. %%%----------------------------------------------------------------------------- %%% External API %%%----------------------------------------------------------------------------- -spec command(string()) -> {integer(), list()}. command(Cmd) -> command(Cmd, [], undefined). -spec command(string(), list()|undefined|fun((list(),any()) -> any())) -> {integer(), any()}. command(Cmd, Fun) when is_function(Fun, 2) -> command(Cmd, [], Fun); command(Cmd, Opt) when is_list(Opt) -> command(Cmd, Opt, undefined). -spec command(string(), list(), undefined|fun((list(),any()) -> any())) -> {integer(), any()}. command(Cmd, Opt, Fun) when is_list(Opt) -> command(Cmd, Opt, Fun, 600000). -spec command(string(), list(), undefined|fun((list(),any()) -> any()), integer()) -> {integer(), any()}. command(Cmd, Opt, Fun, Timeout) when is_list(Opt) , (Fun=:=undefined orelse is_function(Fun, 2)) , (is_integer(Timeout) orelse Timeout==infinity) -> Opts = Opt ++ [stream, exit_status, use_stdio, in, hide, eof], P = open_port({spawn, Cmd}, Opts), Ref = erlang:monitor(port, P), Res = get_data(P, Fun, [], Ref, Timeout), _ = demonitor(Ref, [flush]), Res. -spec status(integer()) -> {status, ExitStatus :: integer()} | {signal, Singnal :: integer(), Core :: boolean()}. status(Status) when is_integer(Status) -> TermSignal = Status band 16#7F, IfSignaled = ((TermSignal + 1) bsr 1) > 0, ExitStatus = (Status band 16#FF00) bsr 8, case IfSignaled of true -> CoreDump = (Status band 16#80) =:= 16#80, {signal, TermSignal, CoreDump}; false -> {status, ExitStatus} end. %%%----------------------------------------------------------------------------- %%% Internal functions %%%----------------------------------------------------------------------------- get_data(P, Fun, D, Ref, Timeout) -> receive {P, {data, {eol, Line}}} when Fun =:= undefined -> get_data(P, Fun, [Line|D], Ref, Timeout); {P, {data, {eol, Line}}} when is_function(Fun, 2) -> get_data(P, Fun, Fun(eol, {Line, D}), Ref, Timeout); {P, {data, {noeol, Line}}} when Fun =:= undefined -> get_data(P, Fun, [Line|D], Ref, Timeout); {P, {data, {noeol, Line}}} when is_function(Fun, 2) -> get_data(P, Fun, Fun(noeol, {Line, D}), Ref, Timeout); {P, {data, D1}} when Fun =:= undefined -> get_data(P, Fun, [D1|D], Ref, Timeout); {P, {data, D1}} when is_function(Fun, 2) -> get_data(P, Fun, Fun(data, {D1, D}), Ref, Timeout); {P, eof} -> catch port_close(P), flush_until_down(P, Ref), receive {P, {exit_status, 0}} when is_function(Fun, 2) -> {ok, Fun(eof, D)}; {P, {exit_status, N}} when is_function(Fun, 2) -> {error, {N, Fun(eof, D)}}; {P, {exit_status, 0}} -> {ok, lists:reverse(D)}; {P, {exit_status, N}} -> {error, {N, lists:reverse(D)}} after 5000 -> if is_function(Fun, 2) -> throw({no_exit_status, Fun(eof, D)}); true -> throw({no_exit_status, timeout_waiting_for_output}) end end; {'DOWN', Ref, _, _, Reason} -> flush_exit(P), exit({error, Reason, lists:flatten(lists:reverse(D))}) after Timeout -> exit(timeout) end. %% @doc %% Return a canonicalized pathname, having resolved symlinks to their %% destination. Modelled on realpath(3). %% @end %% Derived from https://github.com/mk270/realpath %% Copyright 2020 Martin Keegan -spec realpath(string()) -> string(). realpath(Path) when is_list(Path) -> check_canonical(Path, 20); realpath(Path) when is_binary(Path) -> list_to_binary(realpath(binary_to_list(Path))). check_canonical(S, TTL) -> Fragments = make_fragments(S), check_fragments(Fragments, [], TTL). check_fragments(_, _, 0) -> throw(loop_detected); check_fragments([], AlreadyChecked, _) -> AlreadyChecked; check_fragments([Head|Tail], AlreadyChecked, TTL) -> case is_symlink(AlreadyChecked, Head) of false -> check_fragments(Tail, filename:join(AlreadyChecked, Head), TTL); {true, Referent} -> TailJoined = join_non_null(Tail), AllJoined = filename:join(Referent, TailJoined), check_canonical(AllJoined, TTL - 1) end. is_symlink(Dirname, Basename) -> Path = filename:join(Dirname, Basename), case file:read_link(Path) of {ok, Name} -> case Name of % absolute link [$/|_] -> {true, Name}; % relative link _ -> {true, filename:join(Dirname, Name)} end; _ -> false end. make_fragments(S) -> filename:split(S). join_non_null([]) -> ""; join_non_null(SS) -> filename:join(SS). %% @doc %% Return a path where the use of ".." to indicate parent directory has %% been resolved. Currently does not accept relative paths. %% @end %% Derived from https://github.com/mk270/realpath %% Copyright 2020 Martin Keegan -spec normalpath(list()) -> string(). normalpath(S=[$/|_]) when is_list(S)-> normalpath2(S); normalpath(S) when is_list(S) -> normalpath2(filename:absname(S)); normalpath(B) when is_binary(B) -> list_to_binary(normalpath(binary_to_list(B))). normalpath2(S) when is_list(S) -> Parts = filename:split(S), filename:join(lists:reverse(normalize(Parts, []))). normalize([], Path) -> Path; normalize([".."|T], Path) -> {_H, Rest} = pop(Path), normalize(T, Rest); normalize([H|T], Path) -> Rest = push(H, Path), normalize(T, Rest). pop([]) -> {"/", []}; pop(["/"]) -> {"/", ["/"]}; pop([H|T]) -> {H,T}. push(H,T) -> [H|T]. flush_until_down(Port, MonRef) -> receive {Port, {data, _Bytes}} -> flush_until_down(Port, MonRef); {'DOWN', MonRef, _, _, _} -> flush_exit(Port) end. %% The exit signal is always delivered before %% the down signal, so try to clean up the mailbox. flush_exit(Port) -> receive {'EXIT', Port, _} -> ok after 0 -> ok end. %%%----------------------------------------------------------------------------- %%% Tests %%%----------------------------------------------------------------------------- -ifdef(EUNIT). command_test() -> {ok, ["ok\n"]} = osx:command("echo ok"), {error, {1, ""}} = osx:command("false"), {ok, ok} = osx:command("echo ok", fun(data, {"ok\n", []}) -> []; (eof, []) -> ok end), {ok,["a","b","c"]} = osx:command("echo -en 'a\nb\nc\n'", [{line, 80}]), %{error, {143,[]}} = osx:command("kill $$"), {signal,15,true} = status(143), {status,0} = status(0), ok. make_fragments_test_data() -> [{"/usr/local/bin", ["/", "usr", "local", "bin"]}, {"usr/local/bin/bash", ["usr", "local", "bin", "bash"]}, {"/usr/local/bin/", ["/", "usr", "local", "bin"]}]. make_fragments_test_() -> [ ?_assertEqual(Expected, make_fragments(Observed)) || {Observed, Expected} <- make_fragments_test_data() ]. -endif.