├── .gitignore ├── LICENSE ├── README.md ├── c_src └── fdlink.c ├── rebar.config ├── rebar.lock └── src ├── erlsh.app.src ├── erlsh.erl ├── erlsh_app.erl ├── erlsh_path.erl └── erlsh_sup.erl /.gitignore: -------------------------------------------------------------------------------- 1 | /_build 2 | /doc 3 | /priv/fdlink 4 | .rebar3 5 | c_src/fdlink.d 6 | c_src/fdlink.o 7 | ebin 8 | -------------------------------------------------------------------------------- /LICENSE: -------------------------------------------------------------------------------- 1 | ISC License 2 | 3 | Copyright (c) 2013 Vladimir Kirillov 4 | 5 | Permission to use, copy, modify, and/or distribute this software for any 6 | purpose with or without fee is hereby granted, provided that the above 7 | copyright notice and this permission notice appear in all copies. 8 | 9 | THE SOFTWARE IS PROVIDED "AS IS" AND THE AUTHOR DISCLAIMS ALL WARRANTIES 10 | WITH REGARD TO THIS SOFTWARE INCLUDING ALL IMPLIED WARRANTIES OF 11 | MERCHANTABILITY AND FITNESS. IN NO EVENT SHALL THE AUTHOR BE LIABLE FOR 12 | ANY SPECIAL, DIRECT, INDIRECT, OR CONSEQUENTIAL DAMAGES OR ANY DAMAGES 13 | WHATSOEVER RESULTING FROM LOSS OF USE, DATA OR PROFITS, WHETHER IN AN 14 | ACTION OF CONTRACT, NEGLIGENCE OR OTHER TORTIOUS ACTION, ARISING OUT OF 15 | OR IN CONNECTION WITH THE USE OR PERFORMANCE OF THIS SOFTWARE. 16 | -------------------------------------------------------------------------------- /README.md: -------------------------------------------------------------------------------- 1 | ## erlsh 2 | 3 | Family of functions and ports involving interacting with the system shell, paths and external programs. 4 | 5 | Reason why not `os:cmd/1`: 6 | 7 | ```erlang 8 | > Email = "hacker+/somepath&reboot#@example.com". % this is a valid email! 9 | > os:cmd(["mkdir -p ", Email]). 10 | % path clobbering and a reboot may happen here! 11 | ``` 12 | 13 | Examples with `erlsh:run/1,2,3,4`, `erlsh:oneliner/1,2`, `erlsh_path:escape/1`: 14 | 15 | ```erlang 16 | > erlsh:oneliner("uname -v"). % oneliner/1,2 funs do not include newlines 17 | {done,0, 18 | <<"Darwin Kernel Version 12.4.0: Wed May 1 17:57:12 PDT 2013; root:xnu-2050.24.15~1/RELEASE_X86_64">>} 19 | 20 | > erlsh:oneliner("git describe --always"). 21 | {done,128,<<"fatal: Not a valid object name HEAD">>} 22 | 23 | > erlsh:oneliner("git describe --always", "/tank/proger/vxz/otp"). 24 | {done,0,<<"OTP_R16B01">>} 25 | 26 | > erlsh:run(["git", "clone", "https://github.com/proger/darwinkit.git"], binary, "/tmp"). 27 | {done,0,<<"Cloning into 'darwinkit'...\n">>} 28 | 29 | > UserUrl = "https://github.com/proger/darwinkit.git". 30 | "https://github.com/proger/darwinkit.git" 31 | > erlsh:run(["git", "clone", UserUrl], binary, "/tmp"). 32 | {done,128, 33 | <<"fatal: destination path 'darwinkit' already exists and is not an empty directory.\n">>} 34 | 35 | > Path = erlsh_path:escape("email+=/subdir@example.com"). 36 | "email+=%2Fsubdir@example.com" 37 | 38 | > erlsh:oneliner(["touch", filename:join("/tmp/", Path)]). 39 | {done,0,<<>>} 40 | 41 | > erlsh:run(["ifconfig"], "/tmp/output.log", "/tank/proger/vxz/otp"). 42 | {done,0,"/tmp/output.log"} 43 | 44 | % cat /tmp/output.log 45 | >>> {{2013,8,28},{8,39,14}} /sbin/ifconfig 46 | lo0: flags=8049 mtu 16384 47 | options=3 48 | inet6 fe80::1%lo0 prefixlen 64 scopeid 0x1 49 | inet 127.0.0.1 netmask 0xff000000 50 | inet6 ::1 prefixlen 128 51 | gif0: flags=8010 mtu 1280 52 | stf0: flags=0<> mtu 1280 53 | en0: flags=8863 mtu 1500 54 | ether 7c:d1:c3:e9:24:65 55 | inet6 fe80::7ed1:c3ff:fee9:2465%en0 prefixlen 64 scopeid 0x4 56 | inet 192.168.63.163 netmask 0xfffffc00 broadcast 192.168.63.255 57 | media: autoselect 58 | status: active 59 | p2p0: flags=8843 mtu 2304 60 | ether 0e:d1:c3:e9:24:65 61 | media: autoselect 62 | status: inactive 63 | >>> {{2013,8,28},{8,39,14}} exit status: 0 64 | ``` 65 | 66 | ### fdlink port 67 | 68 | Consider a case of spawning a port that does not actually read its standard input (e.g. `socat` that bridges `AF_UNIX` with `AF_INET`): 69 | 70 | ``` shell 71 | # pstree -A -a $(pgrep make) 72 | make run 73 | `-sh -c... 74 | `-beam.smp -- -root /usr/lib/erlang -progname erl -- -home /root -- -pa ebin -config run/sys.config -eval[ok = application: 75 | |-socat tcp-listen:32133,reuseaddr,bind=127.0.0.1 unix-connect:/var/run/docker.sock 76 | `-16*[{beam.smp}] 77 | ``` 78 | 79 | If you terminate the node, `beam` will close the port but the process will still remain alive (thus, it will leak). 80 | 81 | To mitigate this issue, you can use `fdlink` that will track `stdin` availability for you: 82 | 83 | ``` shell 84 | # pstree -A -a $(pgrep make) 85 | make run 86 | `-sh -c... 87 | `-beam.smp -- -root /usr/lib/erlang -progname erl -- -home /root -- -pa ebin -config run/sys.config -eval[ok = application: 88 | |-fdlink /usr/bin/socat tcp-listen:32133,reuseaddr,bind=127.0.0.1 unix-connect:/var/run/docker.sock 89 | | `-socat tcp-listen:32133,reuseaddr,bind=127.0.0.1 unix-connect:/var/run/docker.sock 90 | `-16*[{beam.smp}] 91 | ``` 92 | 93 | Using `fdlink` is easy: 94 | 95 | ```erlang 96 | > Fdlink = erlsh:fdlink_executable(). % make sure your app dir is setup correctly 97 | > Fdlink = filename:join("./priv", "fdlink"). % in case you're running directly from erlsh root 98 | 99 | > erlang:open_port({spawn_executable, Fdlink}, [stream, exit_status, {args, ["/usr/bin/socat"|RestOfArgs]}). 100 | ``` 101 | 102 | `fdlink` will also close the standard input of its child process. 103 | -------------------------------------------------------------------------------- /c_src/fdlink.c: -------------------------------------------------------------------------------- 1 | /* 2 | * fdlink spawns a process and links it to the availability of the inherited stdin (fd 0). 3 | * If stdin is closed, the spawned process receives SIGHUP. 4 | */ 5 | #include 6 | #include 7 | #include 8 | #include 9 | #include 10 | #include 11 | #include 12 | #include 13 | #include 14 | #include 15 | 16 | #define safe(expr, error) do { if (!(expr)) { perror(error); exit(1); } } while (0) 17 | 18 | static sig_atomic_t exited; 19 | 20 | static void 21 | sighandler(int sig) 22 | { 23 | if (sig == SIGCHLD) 24 | exited = 1; 25 | } 26 | 27 | int 28 | main(int argc, char **argv) 29 | { 30 | pid_t pid; 31 | 32 | if (argc < 2) { 33 | fprintf(stderr, "usage: fdlink bin args...\n"); 34 | exit(2); 35 | } 36 | 37 | signal(SIGCHLD, sighandler); 38 | 39 | if ((pid = fork()) == 0) { 40 | close(0); 41 | safe(execv(argv[1], argv + 1) != -1, "fdlink execv"); 42 | /* NOTREACHED */ 43 | } else { 44 | assert(pid != -1); 45 | safe(fcntl(0, F_SETFL, O_NONBLOCK) != -1, "fdlink fcntl"); 46 | 47 | int nfds; 48 | 49 | do { 50 | if (exited == 1) { 51 | int status; 52 | if (waitpid(pid, &status, WNOHANG) != -1) { 53 | exit(WEXITSTATUS(status)); 54 | }; 55 | exited = 0; 56 | } 57 | 58 | fd_set fdset_r; FD_ZERO(&fdset_r); FD_SET(0, &fdset_r); 59 | fd_set fdset_e; FD_ZERO(&fdset_e); FD_SET(0, &fdset_e); 60 | 61 | nfds = select(64, &fdset_r, NULL, &fdset_e, NULL); 62 | if (nfds == -1 && (errno == EAGAIN || errno == EINTR)) 63 | continue; 64 | else if (nfds == -1) { 65 | perror("fdlink select"); 66 | exit(1); 67 | } 68 | 69 | if (FD_ISSET(0, &fdset_r) || FD_ISSET(0, &fdset_e)) { 70 | char buf[1024]; 71 | while (1) { 72 | int nread = read(0, &buf, sizeof(buf)); 73 | if (nread == -1 && errno == EINTR) 74 | continue; 75 | else if (nread == -1 && errno == EAGAIN) 76 | break; 77 | else if (nread == -1) { 78 | perror("fdlink read"); 79 | exit(1); 80 | } else if (nread == 0) { 81 | kill(pid, SIGHUP); 82 | exit(0); 83 | } 84 | } 85 | } 86 | 87 | } while (1); 88 | } 89 | 90 | return 0; 91 | } 92 | -------------------------------------------------------------------------------- /rebar.config: -------------------------------------------------------------------------------- 1 | {port_specs, [ 2 | {"priv/fdlink", ["c_src/fdlink.c"]} 3 | ]}. 4 | 5 | {plugins, [pc]}. 6 | 7 | {provider_hooks, 8 | [ 9 | {pre, 10 | [ 11 | {compile, {pc, compile}}, 12 | {clean, {pc, clean}} 13 | ] 14 | } 15 | ] 16 | }. 17 | -------------------------------------------------------------------------------- /rebar.lock: -------------------------------------------------------------------------------- 1 | []. 2 | -------------------------------------------------------------------------------- /src/erlsh.app.src: -------------------------------------------------------------------------------- 1 | {application, erlsh, 2 | [ 3 | {description, "Family of functions and ports involving interacting with the system shell, paths and external programs."}, 4 | {vsn, "0.1.0"}, 5 | {registered, []}, 6 | {applications, [ 7 | kernel, 8 | stdlib 9 | ]}, 10 | {mod, { erlsh_app, []}}, 11 | {env, []} 12 | ]}. 13 | -------------------------------------------------------------------------------- /src/erlsh.erl: -------------------------------------------------------------------------------- 1 | -module(erlsh). 2 | -export([oneliner/1, oneliner/2, run/1, run/2, run/3, run/4, fdlink_executable/0]). 3 | 4 | fdlink_executable() -> 5 | filename:absname(filename:join(code:priv_dir(erlsh), "fdlink")). 6 | 7 | oneliner(C) -> 8 | run(C, ignoreeol, "."). 9 | 10 | oneliner(C, Cwd) -> 11 | run(C, ignoreeol, Cwd). 12 | 13 | run(C) -> 14 | run(C, binary, "."). 15 | 16 | run(C, Log) -> 17 | run(C, Log, "."). 18 | 19 | run([C|Args], Log, Cwd) when is_list(C) -> 20 | Executable = case filename:pathtype(C) of 21 | absolute -> C; 22 | relative -> case filename:split(C) of 23 | [C] -> os:find_executable(C); 24 | _ -> C % smth like deps/erlsh/priv/fdlink 25 | end; 26 | _ -> C 27 | end, 28 | run(Executable, Args, Log, Cwd); 29 | run(Command, Log, Cwd) when is_list(Command) -> 30 | run("/bin/sh", ["-c", Command], Log, Cwd). 31 | 32 | run(Command, Args, ignoreeol, Cwd) -> 33 | Port = erlang:open_port({spawn_executable, Command}, 34 | [stream, stderr_to_stdout, binary, exit_status, 35 | {args, Args}, {cd, Cwd}, {line, 16384}]), 36 | sh_loop(Port, fun({_, Chunk}, Acc) -> [Chunk|Acc] end, []); 37 | 38 | run(Command, Args, binary, Cwd) -> 39 | Port = erlang:open_port({spawn_executable, Command}, 40 | [stream, stderr_to_stdout, binary, exit_status, 41 | {args, Args}, {cd, Cwd}]), 42 | sh_loop(Port, binary); 43 | 44 | run(Command, Args, Log, Cwd) -> 45 | {ok, File} = file:open(Log, [append, raw]), 46 | file:write(File, [">>> ", ts(), " ", Command, " ", [[A, " "] || A <- Args], "\n"]), 47 | 48 | Port = erlang:open_port({spawn_executable, Command}, 49 | [stream, stderr_to_stdout, binary, exit_status, 50 | {args, Args}, {cd, Cwd}]), 51 | 52 | {done, Status, _} = sh_loop(Port, fun(Chunk, _Acc) -> file:write(File, Chunk), [] end, []), 53 | file:write(File, [">>> ", ts(), " exit status: ", integer_to_list(Status), "\n"]), 54 | {done, Status, Log}. 55 | 56 | % 57 | % private functions 58 | % 59 | 60 | sh_loop(Port, binary) -> 61 | sh_loop(Port, fun(Chunk, Acc) -> [Chunk|Acc] end, []). 62 | 63 | sh_loop(Port, Fun, Acc) when is_function(Fun) -> 64 | sh_loop(Port, Fun, Acc, fun erlang:iolist_to_binary/1). 65 | 66 | sh_loop(Port, Fun, Acc, Flatten) when is_function(Fun) -> 67 | receive 68 | {Port, {data, {eol, Line}}} -> 69 | sh_loop(Port, Fun, Fun({eol, Line}, Acc), Flatten); 70 | {Port, {data, {noeol, Line}}} -> 71 | sh_loop(Port, Fun, Fun({noeol, Line}, Acc), Flatten); 72 | {Port, {data, Data}} -> 73 | sh_loop(Port, Fun, Fun(Data, Acc), Flatten); 74 | {Port, {exit_status, Status}} -> 75 | {done, Status, Flatten(lists:reverse(Acc))} 76 | end. 77 | 78 | %t() -> dbg:tracer(), dbg:p(self(), m), dbg:p(new, m), run("false", "/tmp/1", "/tmp"). 79 | 80 | ts() -> 81 | Ts = {{_Y,_M,_D},{_H,_Min,_S}} = calendar:now_to_datetime(os:timestamp()), 82 | io_lib:format("~p", [Ts]). 83 | -------------------------------------------------------------------------------- /src/erlsh_app.erl: -------------------------------------------------------------------------------- 1 | -module(erlsh_app). 2 | 3 | -behaviour(application). 4 | 5 | %% Application callbacks 6 | -export([start/2, stop/1]). 7 | 8 | %% =================================================================== 9 | %% Application callbacks 10 | %% =================================================================== 11 | 12 | start(_StartType, _StartArgs) -> 13 | erlsh_sup:start_link(). 14 | 15 | stop(_State) -> 16 | ok. 17 | -------------------------------------------------------------------------------- /src/erlsh_path.erl: -------------------------------------------------------------------------------- 1 | -module(erlsh_path). 2 | -export([escape/1, unescape/1]). 3 | 4 | escape(Path) -> 5 | R = reserved(), 6 | lists:append([char_encode(Char, R) || Char <- Path]). 7 | 8 | unescape(Str) -> 9 | http_uri:decode(Str). 10 | 11 | reserved() -> 12 | sets:from_list([$/, $\\, $:, $%]). 13 | 14 | char_encode(Char, Reserved) -> 15 | case sets:is_element(Char, Reserved) of 16 | true -> 17 | [$% | http_util:integer_to_hexlist(Char)]; 18 | false -> 19 | [Char] 20 | end. 21 | -------------------------------------------------------------------------------- /src/erlsh_sup.erl: -------------------------------------------------------------------------------- 1 | 2 | -module(erlsh_sup). 3 | 4 | -behaviour(supervisor). 5 | 6 | %% API 7 | -export([start_link/0]). 8 | 9 | %% Supervisor callbacks 10 | -export([init/1]). 11 | 12 | %% Helper macro for declaring children of supervisor 13 | -define(CHILD(I, Type), {I, {I, start_link, []}, permanent, 5000, Type, [I]}). 14 | 15 | %% =================================================================== 16 | %% API functions 17 | %% =================================================================== 18 | 19 | start_link() -> 20 | supervisor:start_link({local, ?MODULE}, ?MODULE, []). 21 | 22 | %% =================================================================== 23 | %% Supervisor callbacks 24 | %% =================================================================== 25 | 26 | init([]) -> 27 | {ok, { {one_for_one, 5, 10}, []} }. 28 | 29 | --------------------------------------------------------------------------------