Browse Source

sh v1.9 init

221V 3 years ago
parent
commit
8472c65efe
10 changed files with 331 additions and 1 deletions
  1. 2 0
      .gitignore
  2. 13 0
      LICENSE
  3. 123 1
      README.md
  4. 87 0
      c_src/fdlink.c
  5. 9 0
      ebin/sh.app
  6. 13 0
      rebar.config
  7. 59 0
      src/sh.erl
  8. 6 0
      src/sh_app.erl
  9. 10 0
      src/sh_path.erl
  10. 9 0
      src/sh_sup.erl

+ 2 - 0
.gitignore

@@ -0,0 +1,2 @@
+ebin/*.beam
+/priv/fdlink

+ 13 - 0
LICENSE

@@ -0,0 +1,13 @@
+Copyright (c) 2013 Vladimir Kirillov <proger@hackndev.com>
+
+Permission to use, copy, modify, and/or distribute this software for any
+purpose with or without fee is hereby granted, provided that the above
+copyright notice and this permission notice appear in all copies.
+
+THE SOFTWARE IS PROVIDED "AS IS" AND THE AUTHOR DISCLAIMS ALL WARRANTIES
+WITH REGARD TO THIS SOFTWARE INCLUDING ALL IMPLIED WARRANTIES OF
+MERCHANTABILITY AND FITNESS. IN NO EVENT SHALL THE AUTHOR BE LIABLE FOR
+ANY SPECIAL, DIRECT, INDIRECT, OR CONSEQUENTIAL DAMAGES OR ANY DAMAGES
+WHATSOEVER RESULTING FROM LOSS OF USE, DATA OR PROFITS, WHETHER IN AN
+ACTION OF CONTRACT, NEGLIGENCE OR OTHER TORTIOUS ACTION, ARISING OUT OF
+OR IN CONNECTION WITH THE USE OR PERFORMANCE OF THIS SOFTWARE.

+ 123 - 1
README.md

@@ -1,2 +1,124 @@
-# sh
+SH Executor - https://github.com/synrc/sh (exe) v1.9 fork
+===========
 
+Family of functions and ports involving interacting with the system shell,
+paths and external programs.
+
+Reason
+------
+
+```erlang
+> Email = "hacker+/somepath&reboot#@example.com". % this is a valid email!
+> os:cmd(["mkdir -p ", Email]).
+% path clobbering and a reboot may happen here!
+```
+
+Examples
+--------
+
+### Onliners
+
+```erlang
+> sh:oneliner(["touch", filename:join("/tmp/", Path)]).
+{done,0,<<>>}
+
+> sh:oneliner("uname -v"). % oneliner/1,2 funs do not include newlines
+{done,0,
+      <<"Darwin Kernel Version 12.4.0: Wed May  1 17:57:12 PDT 2013; root:xnu-2050.24.15~1/RELEASE_X86_64">>}
+
+> sh:oneliner("git describe --always").
+{done,128,<<"fatal: Not a valid object name HEAD">>}
+
+> sh:oneliner("git describe --always", "/tank/proger/vxz/otp").
+{done,0,<<"OTP_R16B01">>}
+```
+
+### Escaping
+
+```erlang
+> Path = sh_path:escape("email+=/subdir@example.com").
+"email+=%2Fsubdir@example.com"
+```
+
+### Run
+
+```erlang
+> sh:run(["git", "clone", "https://github.com/proger/darwinkit.git"], binary, "/tmp").
+{done,0,<<"Cloning into 'darwinkit'...\n">>}
+
+> UserUrl = "https://github.com/proger/darwinkit.git".
+"https://github.com/proger/darwinkit.git"
+> sh:run(["git", "clone", UserUrl], binary, "/tmp").
+{done,128,
+      <<"fatal: destination path 'darwinkit' already exists and is not an empty directory.\n">>}
+
+> sh:run(["ifconfig"], "/tmp/output.log", "/tank/proger/vxz/otp").
+{done,0,"/tmp/output.log"}
+
+% cat /tmp/output.log
+>>> {{2013,8,28},{8,39,14}} /sbin/ifconfig
+lo0: flags=8049<UP,LOOPBACK,RUNNING,MULTICAST> mtu 16384
+	options=3<RXCSUM,TXCSUM>
+	inet6 fe80::1%lo0 prefixlen 64 scopeid 0x1
+	inet 127.0.0.1 netmask 0xff000000
+	inet6 ::1 prefixlen 128
+gif0: flags=8010<POINTOPOINT,MULTICAST> mtu 1280
+stf0: flags=0<> mtu 1280
+en0: flags=8863<UP,BROADCAST,SMART,RUNNING,SIMPLEX,MULTICAST> mtu 1500
+	ether 7c:d1:c3:e9:24:65
+	inet6 fe80::7ed1:c3ff:fee9:2465%en0 prefixlen 64 scopeid 0x4
+	inet 192.168.63.163 netmask 0xfffffc00 broadcast 192.168.63.255
+	media: autoselect
+	status: active
+p2p0: flags=8843<UP,BROADCAST,RUNNING,SIMPLEX,MULTICAST> mtu 2304
+	ether 0e:d1:c3:e9:24:65
+	media: autoselect
+	status: inactive
+>>> {{2013,8,28},{8,39,14}} exit status: 0
+```
+
+fdlink Port
+-----------
+
+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`):
+
+```shell
+# pstree -A -a $(pgrep make)
+make run
+  `-sh -c...
+      `-beam.smp -- -root /usr/lib/erlang -progname erl -- -home /root -- -pa ebin -config run/sys.config -eval[ok = application:
+          |-socat tcp-listen:32133,reuseaddr,bind=127.0.0.1 unix-connect:/var/run/docker.sock
+          `-16*[{beam.smp}]
+```
+
+If you terminate the node, `beam` will close the port but the process
+will still remain alive (thus, it will leak). To mitigate this issue,
+you can use `fdlink` that will track `stdin` availability for you:
+
+``` shell
+# pstree -A -a $(pgrep make)
+make run
+  `-sh -c...
+      `-beam.smp -- -root /usr/lib/erlang -progname erl -- -home /root -- -pa ebin -config run/sys.config -eval[ok = application:
+          |-fdlink /usr/bin/socat tcp-listen:32133,reuseaddr,bind=127.0.0.1 unix-connect:/var/run/docker.sock
+          |   `-socat tcp-listen:32133,reuseaddr,bind=127.0.0.1 unix-connect:/var/run/docker.sock
+          `-16*[{beam.smp}]
+```
+
+### Using
+
+```erlang
+> Fdlink = sh:fdlink_executable().               % make sure your app dir is setup correctly
+> Fdlink = filename:join("./priv", "fdlink").    % in case you're running directly from erlsh root
+> erlang:open_port({spawn_executable, Fdlink}, [stream, exit_status, {args, ["/usr/bin/socat"|RestOfArgs]}).
+```
+
+`fdlink` will also close the standard input of its child process.
+
+Credits
+-------
+
+* Vladimir Kirillov
+
+OM A HUM

+ 87 - 0
c_src/fdlink.c

@@ -0,0 +1,87 @@
+#include <sys/select.h>
+#include <sys/types.h>
+#include <sys/wait.h>
+#include <unistd.h>
+#include <stdio.h>
+#include <fcntl.h>
+#include <signal.h>
+#include <errno.h>
+#include <stdlib.h>
+#include <assert.h>
+
+#define safe(expr, error) do { if (!(expr)) { perror(error); exit(1); } } while (0)
+
+static sig_atomic_t exited;
+
+static void
+sighandler(int sig)
+{
+	if (sig == SIGCHLD)
+		exited = 1;
+}
+
+int
+main(int argc, char **argv)
+{
+	pid_t pid;
+
+	if (argc < 2) {
+		fprintf(stderr, "usage: fdlink bin args...\n");
+		exit(2);
+	}
+
+	signal(SIGCHLD, sighandler);
+
+	if ((pid = fork()) == 0) {
+		close(0);
+		safe(execv(argv[1], argv + 1) != -1, "fdlink execv");
+		/* NOTREACHED */
+	} else {
+		assert(pid != -1);
+		safe(fcntl(0, F_SETFL, O_NONBLOCK) != -1, "fdlink fcntl");
+
+		int nfds;
+
+		do {
+			if (exited == 1) {
+				int status;
+				if (waitpid(pid, &status, WNOHANG) != -1) {
+					exit(WEXITSTATUS(status));
+				};
+				exited = 0;
+			}
+
+			fd_set fdset_r; FD_ZERO(&fdset_r); FD_SET(0, &fdset_r);
+			fd_set fdset_e; FD_ZERO(&fdset_e); FD_SET(0, &fdset_e);
+
+			nfds = select(64, &fdset_r, NULL, &fdset_e, NULL);
+			if (nfds == -1 && (errno == EAGAIN || errno == EINTR))
+				continue;
+			else if (nfds == -1) {
+				perror("fdlink select");
+				exit(1);
+			}
+
+			if (FD_ISSET(0, &fdset_r) || FD_ISSET(0, &fdset_e)) {
+				char buf[1024];
+				while (1) { 
+					int nread = read(0, &buf, sizeof(buf));
+					if (nread == -1 && errno == EINTR)
+						continue;
+					else if (nread == -1 && errno == EAGAIN)
+						break;
+					else if (nread == -1) {
+						perror("fdlink read");
+						exit(1);
+					} else if (nread == 0) {
+						kill(pid, SIGHUP);
+						exit(0);
+					}
+				}
+			}
+
+		} while (1);
+	}
+
+	return 0;
+}

+ 9 - 0
ebin/sh.app

@@ -0,0 +1,9 @@
+{application, sh,
+ [{description, "SH VXZ Executor"},
+  {vsn, "1.9"},
+  {registered, [sh_sup]},
+  {applications, [kernel,stdlib]},
+  {modules, [sh_app, sh_path, sh_sup, sh]},
+  {mod, { sh_app, []}},
+  {env, []}
+ ]}.

+ 13 - 0
rebar.config

@@ -0,0 +1,13 @@
+{port_env,
+ [{"darwin", "LDFLAGS", "-framework CoreFoundation -framework CoreServices"},
+  {"darwin", "CC", "clang"},
+  {"darwin", "CFLAGS", "-Wno-deprecated-declarations"},
+  {"linux", "LDFLAGS", ""},
+  {"linux", "CC", "gcc"},
+  {"linux", "CFLAGS", ""}]
+}.
+
+{port_specs, [
+        {"darwin", "priv/fdlink", ["c_src/*.c"]},
+        {"linux", "priv/fdlink", ["c_src/*.c"]}
+]}.

+ 59 - 0
src/sh.erl

@@ -0,0 +1,59 @@
+-module(sh).
+-compile([export_all, nowarn_export_all]).
+
+fdlink_executable() -> filename:absname(filename:join(code:priv_dir(sh), "fdlink")).
+oneliner(C) -> run(C, ignoreeol, ".").
+oneliner(C, Cwd) -> run(C, ignoreeol, Cwd).
+run(C) -> run(C, binary, ".").
+run(C, Log) -> run(C, Log, ".").
+executable(C) ->
+    case filename:pathtype(C) of
+        absolute -> C;
+        relative -> case filename:split(C) of
+                [C] -> os:find_executable(C);
+                _ -> C end;
+        _ -> C
+    end.
+
+run([C|Args], Log, Cwd) when is_list(C)      -> run(executable(C), Args, Log, Cwd);
+run(Command, Log, Cwd) when is_list(Command) -> run("/bin/sh", ["-c", Command], Log, Cwd).
+
+run(Command, Args, ignoreeol, Cwd) ->
+    Port = erlang:open_port({spawn_executable, Command},
+        [stream, stderr_to_stdout, binary, exit_status,
+            {args, Args}, {cd, Cwd}, {line, 16384}]),
+    sh_loop(Port, fun({_, Chunk}, Acc) -> [Chunk|Acc] end, []);
+
+run(Command, Args, binary, Cwd) -> run(Command, Args, binary, Cwd, []);
+
+run(Command, Args, Log, Cwd) ->
+    {ok, File} = file:open(Log, [append, raw]),
+    file:write(File, [">>> ", ts(), " ", Command, " ", [[A, " "] || A <- Args], "\n"]),
+
+    Port = erlang:open_port({spawn_executable, Command},
+        [stream, stderr_to_stdout, binary, exit_status,
+            {args, Args}, {cd, Cwd}]),
+
+    {done, Status, _} = sh_loop(Port, fun(Chunk, _Acc) -> file:write(File, Chunk), [] end, []),
+    file:write(File, [">>> ", ts(), " exit status: ", integer_to_list(Status), "\n"]),
+    {done, Status, Log}.
+
+run(Command, Args, _Log, Cwd, Env) ->
+    Port = erlang:open_port({spawn_executable, executable(Command)},
+        [stream, stderr_to_stdout, binary, exit_status,
+            {args, Args}, {cd, Cwd}, {env, Env}]),
+    sh_loop(Port, binary).
+
+sh_loop(Port, binary) -> sh_loop(Port, fun(Chunk, Acc) -> [Chunk|Acc] end, []).
+sh_loop(Port, Fun, Acc) when is_function(Fun) -> sh_loop(Port, Fun, Acc, fun erlang:iolist_to_binary/1).
+sh_loop(Port, Fun, Acc, Flatten) when is_function(Fun) ->
+    receive
+        {Port, {data, {eol, Line}}} -> sh_loop(Port, Fun, Fun({eol, Line}, Acc), Flatten);
+        {Port, {data, {noeol, Line}}} -> sh_loop(Port, Fun, Fun({noeol, Line}, Acc), Flatten);
+        {Port, {data, Data}} -> sh_loop(Port, Fun, Fun(Data, Acc), Flatten);
+        {Port, {exit_status, Status}} -> {done, Status, Flatten(lists:reverse(Acc))}
+    end.
+
+ts() ->
+    Ts = {{_Y,_M,_D},{_H,_Min,_S}} = calendar:now_to_datetime(erlang:timestamp()),
+    io_lib:format("~p", [Ts]).

+ 6 - 0
src/sh_app.erl

@@ -0,0 +1,6 @@
+-module(sh_app).
+-behaviour(application).
+-export([start/2, stop/1]).
+
+start(_StartType, _StartArgs) -> sh_sup:start_link().
+stop(_State) -> ok.

+ 10 - 0
src/sh_path.erl

@@ -0,0 +1,10 @@
+-module(sh_path).
+-export([escape/1, unescape/1]).
+
+escape(Path) -> R = reserved(), lists:append([char_encode(Char, R) || Char <- Path]).
+unescape(Str) -> http_uri:decode(Str).
+reserved() -> sets:from_list([$/, $\\, $:, $%]).
+char_encode(Char, Reserved) ->
+    case sets:is_element(Char, Reserved) of
+        true -> [$% | http_util:integer_to_hexlist(Char)];
+        false -> [Char] end.

+ 9 - 0
src/sh_sup.erl

@@ -0,0 +1,9 @@
+-module(sh_sup).
+-behaviour(supervisor).
+-export([start_link/0]).
+-export([init/1]).
+-define(CHILD(I, Type), {I, {I, start_link, []}, permanent, 5000, Type, [I]}).
+
+start_link() -> supervisor:start_link({local, ?MODULE}, ?MODULE, []).
+init([]) -> {ok, { {one_for_one, 5, 10}, []} }.
+