diff options
author | Magnus Feuer <mfeuer@jaguarlandrover.com> | 2015-10-01 16:32:37 -0700 |
---|---|---|
committer | Magnus Feuer <mfeuer@jaguarlandrover.com> | 2015-10-01 16:32:37 -0700 |
commit | 2811f64c8bd2c3179debbaf687c8db67c2839409 (patch) | |
tree | 625ac1836cab4e66aae5c71eb900dd6706ed7e85 /deps | |
parent | 1c619870a9b2165b7c1d9733156219852af524fa (diff) | |
parent | be63c09e8032db6ed820ee92292174449092c40f (diff) | |
download | rvi_core-2811f64c8bd2c3179debbaf687c8db67c2839409.tar.gz |
Merge pull request #54 from magnusfeuer/master
Now builds under tizen
Diffstat (limited to 'deps')
-rw-r--r-- | deps/base64url/LICENSE.txt | 18 | ||||
-rw-r--r-- | deps/base64url/Makefile | 29 | ||||
-rw-r--r-- | deps/base64url/README.md | 58 | ||||
-rw-r--r-- | deps/base64url/rebar.config | 11 | ||||
-rw-r--r-- | deps/base64url/src/base64url.app.src | 14 | ||||
-rw-r--r-- | deps/base64url/src/base64url.erl | 98 | ||||
-rw-r--r-- | deps/exec/.travis.yml | 4 | ||||
-rw-r--r-- | deps/exec/AUTHORS | 6 | ||||
-rw-r--r-- | deps/exec/LICENSE | 30 | ||||
-rw-r--r-- | deps/exec/Makefile | 67 | ||||
-rw-r--r-- | deps/exec/README | 50 | ||||
-rw-r--r-- | deps/exec/TODO | 2 | ||||
-rw-r--r-- | deps/exec/c_src/ei++.cpp | 354 | ||||
-rw-r--r-- | deps/exec/c_src/ei++.hpp | 585 | ||||
-rw-r--r-- | deps/exec/c_src/exec.cpp | 2111 | ||||
-rw-r--r-- | deps/exec/include/exec.hrl | 8 | ||||
-rwxr-xr-x | deps/exec/priv/x86_64-pc-linux-gnu/exec-port | bin | 0 -> 450870 bytes | |||
-rw-r--r-- | deps/exec/rebar.config | 6 | ||||
-rw-r--r-- | deps/exec/rebar.config.script | 44 | ||||
-rw-r--r-- | deps/exec/src/edoc.css | 144 | ||||
-rw-r--r-- | deps/exec/src/exec.app.src | 16 | ||||
-rw-r--r-- | deps/exec/src/exec.erl | 1130 | ||||
-rw-r--r-- | deps/exec/src/exec_app.erl | 78 | ||||
-rw-r--r-- | deps/exec/src/overview.edoc | 305 |
24 files changed, 5168 insertions, 0 deletions
diff --git a/deps/base64url/LICENSE.txt b/deps/base64url/LICENSE.txt new file mode 100644 index 0000000..c3f0a46 --- /dev/null +++ b/deps/base64url/LICENSE.txt @@ -0,0 +1,18 @@ +Copyright (c) 2013 Vladimir Dronnikov <dronnikov@gmail.com> + +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. diff --git a/deps/base64url/Makefile b/deps/base64url/Makefile new file mode 100644 index 0000000..7c582b2 --- /dev/null +++ b/deps/base64url/Makefile @@ -0,0 +1,29 @@ +all: deps compile check test + +deps: + rebar get-deps + +compile: + rebar compile + +run: compile + sh start.sh + +clean: + rebar clean + rm -fr ebin .ct test/*.beam + +check: + rebar eunit skip_deps=true + +test: deps compile check + ##rebar ct + #mkdir -p .ct + #ct_run -dir test -logdir .ct -pa ebin + +dist: deps compile + echo TODO + +.PHONY: all deps compile check test run clean dist +.SILENT: + diff --git a/deps/base64url/README.md b/deps/base64url/README.md new file mode 100644 index 0000000..165a5dd --- /dev/null +++ b/deps/base64url/README.md @@ -0,0 +1,58 @@ +Base64Url +============== + +[![Hex.pm](https://img.shields.io/hexpm/v/base64url.svg)](https://hex.pm/packages/base64url) + +Standalone [URL safe](http://tools.ietf.org/html/rfc4648) base64-compatible codec. + +Usage +-------------- + +URL-Safe base64 encoding: +```erlang +base64url:encode(<<255,127,254,252>>). +<<"_3_-_A">> +base64url:decode(<<"_3_-_A">>). +<<255,127,254,252>> +``` + +Vanilla base64 encoding: +```erlang +base64:encode(<<255,127,254,252>>). +<<"/3/+/A==">> +``` + +Some systems in the wild use base64 URL encoding, but keep the padding for MIME compatibility (base64 Content-Transfer-Encoding). To interact with such systems, use: +```erlang +base64url:encode_mime(<<255,127,254,252>>). +<<"_3_-_A==">> +base64url:decode(<<"_3_-_A==">>). +<<255,127,254,252>> +``` + +Thanks +-------------- + +To authors of [this](https://github.com/basho/riak_control/blob/master/src/base64url.erl) and [this](https://github.com/mochi/mochiweb/blob/master/src/mochiweb_base64url.erl). + +[License](base64url/blob/master/LICENSE.txt) +------- + +Copyright (c) 2013 Vladimir Dronnikov <dronnikov@gmail.com> + +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. diff --git a/deps/base64url/rebar.config b/deps/base64url/rebar.config new file mode 100644 index 0000000..b3028a1 --- /dev/null +++ b/deps/base64url/rebar.config @@ -0,0 +1,11 @@ +{lib_dirs, ["deps"]}. + +{erl_opts, [ + debug_info, + warn_format, + warn_export_vars, + warn_obsolete_guard, + warn_bif_clash +]}. + +{cover_enabled, true}. diff --git a/deps/base64url/src/base64url.app.src b/deps/base64url/src/base64url.app.src new file mode 100644 index 0000000..b59259c --- /dev/null +++ b/deps/base64url/src/base64url.app.src @@ -0,0 +1,14 @@ +{application, base64url, [ + {description, "URL safe base64-compatible codec"}, + {vsn, "0.0.1"}, + {id, "repo"}, + {registered, []}, + {applications, [ + kernel, + stdlib + ]}, + {env, []}, + {contributors, ["Vladimir Dronnikov"]}, + {licenses, ["MIT"]} +%% {links, [{"Github", "https://github.com/dvv/base64url"}]} +]}. diff --git a/deps/base64url/src/base64url.erl b/deps/base64url/src/base64url.erl new file mode 100644 index 0000000..fa38269 --- /dev/null +++ b/deps/base64url/src/base64url.erl @@ -0,0 +1,98 @@ +%% +%% @doc URL safe base64-compatible codec. +%% +%% Based heavily on the code extracted from: +%% https://github.com/basho/riak_control/blob/master/src/base64url.erl and +%% https://github.com/mochi/mochiweb/blob/master/src/mochiweb_base64url.erl. +%% + +-module(base64url). +-author('Vladimir Dronnikov <dronnikov@gmail.com>'). + +-export([ + decode/1, + encode/1, + encode_mime/1 + ]). + +-spec encode( + binary() | iolist() + ) -> binary(). + +encode(Bin) when is_binary(Bin) -> + << << (urlencode_digit(D)) >> || <<D>> <= base64:encode(Bin), D =/= $= >>; +encode(L) when is_list(L) -> + encode(iolist_to_binary(L)). + +-spec encode_mime( + binary() | iolist() + ) -> binary(). +encode_mime(Bin) when is_binary(Bin) -> + << << (urlencode_digit(D)) >> || <<D>> <= base64:encode(Bin) >>; +encode_mime(L) when is_list(L) -> + encode_mime(iolist_to_binary(L)). + +-spec decode( + binary() | iolist() + ) -> binary(). + +decode(Bin) when is_binary(Bin) -> + Bin2 = case byte_size(Bin) rem 4 of + % 1 -> << Bin/binary, "===" >>; + 2 -> << Bin/binary, "==" >>; + 3 -> << Bin/binary, "=" >>; + _ -> Bin + end, + base64:decode(<< << (urldecode_digit(D)) >> || <<D>> <= Bin2 >>); +decode(L) when is_list(L) -> + decode(iolist_to_binary(L)). + +urlencode_digit($/) -> $_; +urlencode_digit($+) -> $-; +urlencode_digit(D) -> D. + +urldecode_digit($_) -> $/; +urldecode_digit($-) -> $+; +urldecode_digit(D) -> D. + +-ifdef(TEST). +-include_lib("eunit/include/eunit.hrl"). + +aim_test() -> + % vanilla base64 produce URL unsafe output + ?assertNotEqual( + binary:match(base64:encode([255,127,254,252]), [<<"=">>, <<"/">>, <<"+">>]), + nomatch), + % this codec produce URL safe output + ?assertEqual( + binary:match(encode([255,127,254,252]), [<<"=">>, <<"/">>, <<"+">>]), + nomatch), + % the mime codec produces URL unsafe output, but only because of padding + ?assertEqual( + binary:match(encode_mime([255,127,254,252]), [<<"/">>, <<"+">>]), + nomatch), + ?assertNotEqual( + binary:match(encode_mime([255,127,254,252]), [<<"=">>]), + nomatch). + +codec_test() -> + % codec is lossless with or without padding + ?assertEqual(decode(encode(<<"foo">>)), <<"foo">>), + ?assertEqual(decode(encode(<<"foo1">>)), <<"foo1">>), + ?assertEqual(decode(encode(<<"foo12">>)), <<"foo12">>), + ?assertEqual(decode(encode(<<"foo123">>)), <<"foo123">>), + ?assertEqual(decode(encode_mime(<<"foo">>)), <<"foo">>), + ?assertEqual(decode(encode_mime(<<"foo1">>)), <<"foo1">>), + ?assertEqual(decode(encode_mime(<<"foo12">>)), <<"foo12">>), + ?assertEqual(decode(encode_mime(<<"foo123">>)), <<"foo123">>). + +iolist_test() -> + % codec supports iolists + ?assertEqual(decode(encode("foo")), <<"foo">>), + ?assertEqual(decode(encode(["fo", "o1"])), <<"foo1">>), + ?assertEqual(decode(encode([255,127,254,252])), <<255,127,254,252>>), + ?assertEqual(decode(encode_mime("foo")), <<"foo">>), + ?assertEqual(decode(encode_mime(["fo", "o1"])), <<"foo1">>), + ?assertEqual(decode(encode_mime([255,127,254,252])), <<255,127,254,252>>). + +-endif. diff --git a/deps/exec/.travis.yml b/deps/exec/.travis.yml new file mode 100644 index 0000000..73c5f9c --- /dev/null +++ b/deps/exec/.travis.yml @@ -0,0 +1,4 @@ +language: erlang +otp_release: + - R16B02 + - R16B01 diff --git a/deps/exec/AUTHORS b/deps/exec/AUTHORS new file mode 100644 index 0000000..97f1bdf --- /dev/null +++ b/deps/exec/AUTHORS @@ -0,0 +1,6 @@ +Authors: + Serge Aleynikov <saleyn@gmail.com> + +Contributors: + Dmitry Kargapolov <dmitriy.kargapolov@gmail.com> + - Autotools support (deprecated) diff --git a/deps/exec/LICENSE b/deps/exec/LICENSE new file mode 100644 index 0000000..67bf740 --- /dev/null +++ b/deps/exec/LICENSE @@ -0,0 +1,30 @@ +BSD LICENSE
+===========
+
+Copyright (C) 2003 Serge Aleynikov <saleyn@gmail.com>
+
+All rights reserved.
+
+Redistribution and use in source and binary forms, with or without
+modification, are permitted provided that the following conditions are met:
+
+ 1. Redistributions of source code must retain the above copyright notice,
+ this list of conditions and the following disclaimer.
+
+ 2. Redistributions in binary form must reproduce the above copyright
+ notice, this list of conditions and the following disclaimer in
+ the documentation and/or other materials provided with the distribution.
+
+ 3. The names of the authors may not be used to endorse or promote products
+ derived from this software without specific prior written permission.
+
+THIS SOFTWARE IS PROVIDED ``AS IS'' AND ANY EXPRESSED OR IMPLIED WARRANTIES,
+INCLUDING, BUT NOT LIMITED TO, THE IMPLIED WARRANTIES OF MERCHANTABILITY AND
+FITNESS FOR A PARTICULAR PURPOSE ARE DISCLAIMED. IN NO EVENT SHALL JCRAFT,
+INC. OR ANY CONTRIBUTORS TO THIS SOFTWARE BE LIABLE FOR ANY DIRECT, INDIRECT,
+INCIDENTAL, SPECIAL, EXEMPLARY, OR CONSEQUENTIAL DAMAGES (INCLUDING, BUT NOT
+LIMITED TO, PROCUREMENT OF SUBSTITUTE GOODS OR SERVICES; LOSS OF USE, DATA,
+OR PROFITS; OR BUSINESS INTERRUPTION) HOWEVER CAUSED AND ON ANY THEORY OF
+LIABILITY, WHETHER IN CONTRACT, STRICT LIABILITY, OR TORT (INCLUDING
+NEGLIGENCE OR OTHERWISE) ARISING IN ANY WAY OUT OF THE USE OF THIS SOFTWARE,
+EVEN IF ADVISED OF THE POSSIBILITY OF SUCH DAMAGE.
diff --git a/deps/exec/Makefile b/deps/exec/Makefile new file mode 100644 index 0000000..9c13ad9 --- /dev/null +++ b/deps/exec/Makefile @@ -0,0 +1,67 @@ +# See LICENSE for licensing information. + +VSN = $(shell git describe --always --tags --abbrev=0 | sed 's/^v//') +PROJECT = $(notdir $(PWD)) +TARBALL = $(PROJECT)-$(VSN) + +DIALYZER = dialyzer +REBAR = rebar + +.PHONY : all clean test docs doc clean-docs github-docs dialyzer + +all: + @$(REBAR) compile + +clean: + @$(REBAR) clean + @rm -fr ebin doc + +docs: doc ebin clean-docs + @$(REBAR) doc skip_deps=true + +doc ebin: + mkdir -p $@ + +test: + @$(REBAR) eunit + +clean-docs: + rm -f doc/*.{css,html,png} doc/edoc-info + +github-docs: + @if git branch | grep -q gh-pages ; then \ + git checkout gh-pages; \ + else \ + git checkout -b gh-pages; \ + fi + git checkout master -- src include + git checkout master -- Makefile rebar.* + make docs + mv doc/*.* . + make clean + rm -fr src c_src include Makefile erl_crash.dump priv rebar.* README* + @FILES=`git st -uall --porcelain | sed -n '/^?? [A-Za-z0-9]/{s/?? //p}'`; \ + for f in $$FILES ; do \ + echo "Adding $$f"; git add $$f; \ + done + @sh -c "ret=0; set +e; \ + if git commit -a --amend -m 'Documentation updated'; \ + then git push origin +gh-pages; echo 'Pushed gh-pages to origin'; \ + else ret=1; git reset --hard; \ + fi; \ + set -e; git checkout master && echo 'Switched to master'; exit $$ret" + +tar: + @rm -f $(TARBALL).tgz; \ + cd ..; \ + tar zcf $(TARBALL).tgz --exclude="core*" --exclude="erl_crash.dump" \ + --exclude="*.tgz" --exclude="*.swp" --exclude="c_src" \ + --exclude="Makefile" --exclude="rebar.*" --exclude="*.mk" \ + --exclude="*.o" --exclude=".git*" $(PROJECT) && \ + mv $(TARBALL).tgz $(PROJECT)/ && echo "Created $(TARBALL).tgz" + +dialyzer: build.plt + $(DIALYZER) -nn --plt $< ebin + +build.plt: + $(DIALYZER) -q --build_plt --apps erts kernel stdlib --output_plt $@ diff --git a/deps/exec/README b/deps/exec/README new file mode 100644 index 0000000..17be7a7 --- /dev/null +++ b/deps/exec/README @@ -0,0 +1,50 @@ +erlexec +======= + + Execute and control OS processes from Erlang/OTP. + + This project implements a C++ port program and Erlang application + that gives light-weight Erlang processes fine-grain control over + execution of OS processes. + + It makes possible for an Erlang process to start, stop an OS process, + send POSIX signals, know process IDs of the started OS process, set up + a monitor and/or link to it, run interactive commands with psudo + terminals. This application provides better control + over OS processes than built-in erlang:open_port/2 command with a + {spawn, Command} option, and performs proper OS child process cleanup + when the emulator exits. + + See [http://saleyn.github.com/erlexec] for more information. + +SUPPORTED OS's +============== + Linux, Solaris, MacOS X + +BUILDING +======== + Make sure you have rebar (http://github.com/basho/rebar) installed + locally and the rebar script is in the path. + + If you are deploying the application on Linux and would like to + take advantage of exec-port running tasks using effective user IDs + different from the real user ID that started exec-port, then + make sure that libcap-dev[el] library is installed. + + OS-specific libcap-dev installation instructions: + + Fedora, CentOS: "yum install libcap-devel" + Ubuntu: "apt-get install libcap-dev" + + $ git clone git@github.com:saleyn/erlexec.git + $ make + +DEPLOYING +========= + Run "make tar". This produces a tarball which you can deploy to your + destination environment and untar the content. + +LICENSE +======= + The program is distributed under BSD license. + Copyright (c) 2003 Serge Aleynikov <saleyn at gmail dot com> diff --git a/deps/exec/TODO b/deps/exec/TODO new file mode 100644 index 0000000..1e3954c --- /dev/null +++ b/deps/exec/TODO @@ -0,0 +1,2 @@ +1. Rewrite non-portable sigwaitinfo and sigpending implementation to using + a dedicated pipe and streaming signals through it to the main select() loop diff --git a/deps/exec/c_src/ei++.cpp b/deps/exec/c_src/ei++.cpp new file mode 100644 index 0000000..35517b3 --- /dev/null +++ b/deps/exec/c_src/ei++.cpp @@ -0,0 +1,354 @@ +#include <unistd.h> +#include <fcntl.h> +#include <sstream> +#include <iomanip> +#include "ei++.hpp" + +using namespace ei; + +//----------------------------------------------------------------------------- +bool ei::dump ( const char* header, std::ostream& os, const ei_x_buff& buf, bool condition ) +{ + if ( !condition ) os << (header ? header : "") << buf; + return condition; +} + +//----------------------------------------------------------------------------- +std::ostream& ei::dump (std::ostream& os, const unsigned char* buf, int n, bool eol) +{ + std::stringstream s; + for(int i=0; i < n; i++) + s << (i == 0 ? "<<" : ",") << (int) (buf[i]); + s << (n == 0 ? "<<>>" : ">>"); + if (eol) s << std::endl; + return os << s.str(); +} + +//----------------------------------------------------------------------------- +std::ostream& ei::operator<< (std::ostream& os, const ei_x_buff& buf) +{ + return dump(os, (const unsigned char*)buf.buff, buf.index); +} + +//----------------------------------------------------------------------------- +int ei::stringIndex(const char** cmds, const std::string& cmd, int firstIdx, int size) +{ + for (int i=firstIdx; cmds != NULL && i < size; i++, cmds++) + if (cmd == *cmds) + return i; + return firstIdx-1; +} + +//----------------------------------------------------------------------------- +std::ostream& ei::Serializer::dump (std::ostream& os, bool outWriteBuffer) +{ + if (outWriteBuffer) { + size_t len = m_wbuf.read_header(); + if (!len) len = m_wIdx; + os << "--Erl-<-C--[" << std::setw(len < 10000 ? 4 : 9) << len << "]: "; + return ::dump(os, (const unsigned char*)&m_wbuf, len, false) << "\r\n"; + } else { + size_t len = m_rbuf.read_header(); + os << "--Erl->-C--[" << std::setw(len < 10000 ? 4 : 9) << len << "]: "; + return ::dump(os, (const unsigned char*)&m_rbuf, len, false) << "\r\n"; + } +} + +//----------------------------------------------------------------------------- +int ei::Serializer::print (std::ostream& os, const std::string& header) +{ + char* s = NULL; + int idx = 0; + if (ei_s_print_term(&s, &m_rbuf, &idx) < 0) + return -1; + if (!header.empty()) + os << header << s << std::endl; + else + os << s << std::endl; + + if (s) + free(s); + + return 0; +} + +//----------------------------------------------------------------------------- +TimeVal ei::operator- (const TimeVal& t1, const TimeVal& t2) { + TimeVal t = t1; t -= t2; + return t; +} + +//----------------------------------------------------------------------------- +TimeVal ei::operator+ (const TimeVal& t1, const TimeVal& t2) { + TimeVal t = t1; t += t2; + return t; +} + +//----------------------------------------------------------------------------- +TimeVal::TimeVal(TimeType tp, int _s, int _us) +{ + switch (tp) { + case NOW: + gettimeofday(&m_tv, NULL); + break; + case RELATIVE: + new (this) TimeVal(); + } + if (_s != 0 || _us != 0) add(_s, _us); +} + +//----------------------------------------------------------------------------- +int Serializer::set_handles(int in, int out, bool non_blocking) +{ + m_fin = in; + m_fout = out; + if (non_blocking) { + return fcntl(m_fin, F_SETFL, fcntl(m_fin, F_GETFL) | O_NONBLOCK) + || fcntl(m_fout, F_SETFL, fcntl(m_fout, F_GETFL) | O_NONBLOCK); + } else + return 0; +} + +//----------------------------------------------------------------------------- +int Serializer::read() +{ + if (m_readPacketSz == 0) { + int size = m_rbuf.headerSize(); + if (read_exact(m_fin, &m_rbuf.c_str()[-size], size, m_readOffset) < size) + return -1; + + m_readPacketSz = m_rbuf.read_header(); + m_readOffset = 0; + + if (m_debug) + std::cerr << "Serializer::read() - message size: " << m_readPacketSz << '\r' << std::endl; + + if (!m_rbuf.resize(m_readPacketSz)) + return -2; + } + + int total = m_readPacketSz - m_readOffset; + if (read_exact(m_fin, &m_rbuf, m_readPacketSz, m_readOffset) < total) + return -3; + + m_rIdx = 0; + + if (m_debug) + dump(std::cerr, false); + + int len = m_readPacketSz; + m_readOffset = m_readPacketSz = 0; + + /* Ensure that we are receiving the binary term by reading and + * stripping the version byte */ + int version; + if (ei_decode_version(&m_rbuf, &m_rIdx, &version)) + return -4; + + return len; +} + +//----------------------------------------------------------------------------- +int Serializer::write() +{ + if (m_writePacketSz == 0) { + m_wbuf.write_header(static_cast<size_t>(m_wIdx)); + if (m_debug) + dump(std::cerr, true); + + m_writePacketSz = m_wIdx+m_wbuf.headerSize(); + m_writeOffset = 0; + } + + int total = m_writePacketSz - m_writeOffset; + if (write_exact(m_fout, m_wbuf.header(), m_writePacketSz, m_writeOffset) < total) + return -1; + + int len = m_writePacketSz; + m_writeOffset = m_writePacketSz = 0; + + return len; +} + +//----------------------------------------------------------------------------- +int Serializer::read_exact(int fd, char *buf, size_t len, size_t& got) +{ + int i; + + while (got < len) { + int size = len-got; + while ((i = ::read(fd, (void*)(buf+got), size)) < size && errno == EINTR) + if (i > 0) + got += i; + + if (i <= 0) + return i; + got += i; + } + + return len; +} + +//----------------------------------------------------------------------------- +int Serializer::write_exact(int fd, const char *buf, size_t len, size_t& wrote) +{ + int i; + + while (wrote < len) { + int size = len-wrote; + while ((i = ::write(fd, buf+wrote, size)) < size && errno == EINTR) + if (i > 0) + wrote += i; + + if (i <= 0) + return i; + wrote += i; + } + + return wrote; +} + + +#define get8(s) ((s) += 1, ((unsigned char *)(s))[-1] & 0xff) +#define put8(s,n) do { (s)[0] = (char)((n) & 0xff); (s) += 1; } while (0) + +#define put64be(s,n) do { \ + (s)[0] = ((n) >> 56) & 0xff; \ + (s)[1] = ((n) >> 48) & 0xff; \ + (s)[2] = ((n) >> 40) & 0xff; \ + (s)[3] = ((n) >> 32) & 0xff; \ + (s)[4] = ((n) >> 24) & 0xff; \ + (s)[5] = ((n) >> 16) & 0xff; \ + (s)[6] = ((n) >> 8) & 0xff; \ + (s)[7] = (n) & 0xff; \ + (s) += 8; \ + } while (0) + +#define get64be(s) \ + ((s) += 8, \ + (((unsigned long long)((unsigned char *)(s))[-8] << 56) | \ + ((unsigned long long)((unsigned char *)(s))[-7] << 48) | \ + ((unsigned long long)((unsigned char *)(s))[-6] << 40) | \ + ((unsigned long long)((unsigned char *)(s))[-5] << 32) | \ + ((unsigned long long)((unsigned char *)(s))[-4] << 24) | \ + ((unsigned long long)((unsigned char *)(s))[-3] << 16) | \ + ((unsigned long long)((unsigned char *)(s))[-2] << 8) | \ + (unsigned long long)((unsigned char *)(s))[-1])) + +int Serializer::ei_decode_double(const char *buf, int *index, double *p) +{ + const char *s = buf + *index; + const char *s0 = s; + double f; + + switch (get8(s)) { + case ERL_FLOAT_EXT: + if (sscanf(s, "%lf", &f) != 1) return -1; + s += 31; + break; + case NEW_FLOAT_EXT: { + // IEEE 754 decoder + const unsigned int bits = 64; + const unsigned int expbits = 11; + const unsigned int significantbits = bits - expbits - 1; // -1 for sign bit + unsigned long long i = get64be(s); + long long shift; + unsigned bias; + + if (!p) + break; + else if (i == 0) + f = 0.0; + else { + // get the significant + f = (i & ((1LL << significantbits)-1)); // mask + f /= (1LL << significantbits); // convert back to float + f += 1.0f; // add the one back on + + // get the exponent + bias = (1 << (expbits-1)) - 1; + shift = ((i >> significantbits) & ((1LL << expbits)-1)) - bias; + while (shift > 0) { f *= 2.0; shift--; } + while (shift < 0) { f /= 2.0; shift++; } + + // signness + f *= (i >> (bits-1)) & 1 ? -1.0: 1.0; + } + break; + } + default: + return -1; + } + + if (p) *p = f; + *index += s-s0; + return 0; +} + +int Serializer::ei_encode_double(char *buf, int *index, double p) +{ + char *s = buf + *index; + char *s0 = s; + + if (!buf) + s = s+9; + else { /* use IEEE 754 format */ + const unsigned int bits = 64; + const unsigned int expbits = 11; + const unsigned int significantbits = bits - expbits - 1; // -1 for sign bit + long long sign, exp, significant; + long double norm; + int shift; + + put8(s, NEW_FLOAT_EXT); + memset(s, 0, 8); + + if (p == 0.0) + s += 8; + else { + // check sign and begin normalization + if (p < 0) { sign = 1; norm = -p; } + else { sign = 0; norm = p; } + + // get the normalized form of p and track the exponent + shift = 0; + while(norm >= 2.0) { norm /= 2.0; shift++; } + while(norm < 1.0) { norm *= 2.0; shift--; } + norm = norm - 1.0; + + // calculate the binary form (non-float) of the significant data + significant = (long long) ( norm * ((1LL << significantbits) + 0.5f) ); + + // get the biased exponent + exp = shift + ((1 << (expbits-1)) - 1); // shift + bias + + // get the final answer + exp = (sign << (bits-1)) | (exp << (bits-expbits-1)) | significant; + put64be(s, exp); + } + } + + *index += s-s0; + return 0; +} + +int x_fix_buff(ei_x_buff* x, int szneeded) +{ + int sz = szneeded + 100; + if (sz > x->buffsz) { + sz += 100; /* to avoid reallocating each and every time */ + x->buffsz = sz; + x->buff = (char*)realloc(x->buff, sz); + } + return x->buff != NULL; +} + +int Serializer::ei_x_encode_double(ei_x_buff* x, double dbl) +{ + int i = x->index; + ei_encode_double(NULL, &i, dbl); + if (!x_fix_buff(x, i)) + return -1; + return ei_encode_double(x->buff, &x->index, dbl); +} + diff --git a/deps/exec/c_src/ei++.hpp b/deps/exec/c_src/ei++.hpp new file mode 100644 index 0000000..888015d --- /dev/null +++ b/deps/exec/c_src/ei++.hpp @@ -0,0 +1,585 @@ +/* + ei++.h + + Author: Serge Aleynikov + Created: 2003/07/10 + + Description: + ============ + C++ wrapper around C ei library distributed with Erlang. + + LICENSE: + ======== + Copyright (C) 2003 Serge Aleynikov <saleyn@gmail.com> + + All rights reserved. + + Redistribution and use in source and binary forms, with or without + modification, are permitted provided that the following conditions are met: + + 1. Redistributions of source code must retain the above copyright notice, + this list of conditions and the following disclaimer. + + 2. Redistributions in binary form must reproduce the above copyright + notice, this list of conditions and the following disclaimer in + the documentation and/or other materials provided with the distribution. + + 3. The names of the authors may not be used to endorse or promote products + derived from this software without specific prior written permission. + + THIS SOFTWARE IS PROVIDED ``AS IS'' AND ANY EXPRESSED OR IMPLIED WARRANTIES, + INCLUDING, BUT NOT LIMITED TO, THE IMPLIED WARRANTIES OF MERCHANTABILITY AND + FITNESS FOR A PARTICULAR PURPOSE ARE DISCLAIMED. IN NO EVENT SHALL JCRAFT, + INC. OR ANY CONTRIBUTORS TO THIS SOFTWARE BE LIABLE FOR ANY DIRECT, INDIRECT, + INCIDENTAL, SPECIAL, EXEMPLARY, OR CONSEQUENTIAL DAMAGES (INCLUDING, BUT NOT + LIMITED TO, PROCUREMENT OF SUBSTITUTE GOODS OR SERVICES; LOSS OF USE, DATA, + OR PROFITS; OR BUSINESS INTERRUPTION) HOWEVER CAUSED AND ON ANY THEORY OF + LIABILITY, WHETHER IN CONTRACT, STRICT LIABILITY, OR TORT (INCLUDING + NEGLIGENCE OR OTHERWISE) ARISING IN ANY WAY OUT OF THE USE OF THIS SOFTWARE, + EVEN IF ADVISED OF THE POSSIBILITY OF SUCH DAMAGE. +*/ + +#ifndef _EMARSHAL_H_ +#define _EMARSHAL_H_ + +#include <ei.h> +#include <stdarg.h> +#include <stdio.h> +#include <unistd.h> +#include <string.h> +#include <string> +#include <algorithm> +#include <iostream> +#include <sys/time.h> +#include <sys/resource.h> +#include <limits.h> +#include <assert.h> + +#define NEW_FLOAT_EXT 'F' + +namespace ei { + typedef unsigned char byte; + + /// Looks up a <cmd> in the <cmds> array. + /// @param <cmds> an array that either ends with a NULL element or has <size> number of elements. + /// @param <cmd> string to find. + /// @param <firstIdx> the mapping of the first element in the array. If != 0, then the return + /// value will be based on this starting index. + /// @param <size> optional size of the <cmds> array. + /// @return an offset <cmd> in the cmds array starting with <firstIdx> value. On failure + /// returns <firstIdx>-1. + int stringIndex(const char** cmds, const std::string& cmd, int firstIdx = 0, int size = INT_MAX); + + /// Class for stack-based (and on-demand heap based) memory allocation + /// of string buffers. It's very efficient for strings not exceeding <N> + /// bytes as it doesn't allocate heap memory. + template < int N, class Allocator = std::allocator<char> > + class StringBuffer + { + char m_buff[N]; + char* m_buffer; + size_t m_size; + int m_minAlloc; + int m_headerSize; + size_t m_maxMsgSize; + Allocator m_alloc; // allocator to use + + char* base() { return m_buffer + m_headerSize; } + const char* base() const { return m_buffer + m_headerSize; } + char* write( int pos, const char* fmt, va_list vargs ) { + char s[512]; + vsnprintf(s, sizeof(s), fmt, vargs); + return copy( s, pos ); + } + + public: + enum { DEF_QUANTUM = 512 }; + + StringBuffer(int _headerSz = 0, int _quantumSz = DEF_QUANTUM) + : m_buffer(m_buff), m_size(N), m_minAlloc(_quantumSz) + { m_buff[0] = '\0'; packetHeaderSize(_headerSz); } + + StringBuffer( const char (&s)[N] ) + : m_buffer(m_buff), m_size(N), m_minAlloc(DEF_QUANTUM), m_headerSize(0), m_maxMsgSize(0) + { copy(s); } + + StringBuffer( const std::string& s) + : m_buffer(m_buff), m_size(N), m_minAlloc(DEF_QUANTUM), m_headerSize(0), m_maxMsgSize(0) + { copy(s.c_str()); } + + ~StringBuffer() { reset(); } + + /// Buffer allocation quantum + int quantum() const { return m_minAlloc; } + void quantum(int n) { m_minAlloc = n; } + /// Defines a prefix space in the buffer used for encoding packet size. + int packetHeaderSize() { return m_headerSize; } + void packetHeaderSize(size_t sz) { + assert(sz == 0 || sz == 1 || sz == 2 || sz == 4); + m_headerSize = sz; + m_maxMsgSize = (1u << (8*m_headerSize)) - 1; + } + /// Does the buffer have memory allocated on heap? + bool allocated() const { return m_buffer != m_buff; } + size_t capacity() const { return m_size - m_headerSize; } + size_t length() const { return strlen(base()); } + void clear() { m_buffer[m_headerSize] = '\0'; } + /// Free heap allocated memory and shrink buffer to original statically allocated size. + void reset() { if (allocated()) m_alloc.deallocate(m_buffer, m_size); m_buffer = m_buff; clear(); } + /// Pointer to a mutable char string of size <capacity()>. + const char* c_str() const { return base(); } + char* c_str() { return base(); } + char* append( const char* s ) { return copy( s, length() ); } + char* append( const std::string& s ) { return copy( s.c_str(), length() ); } + char* append( const char* fmt, ... ) { + va_list vargs; + va_start (vargs, fmt); + char* ret = write(length(), fmt, vargs); + va_end (vargs); + return ret; + } + + char* operator[] ( int i ) { assert( i < (m_size-m_headerSize) ); return base()[i]; } + char* operator() () { return base(); } + char* operator& () { return base(); } + const char* operator& () const { return base(); } + bool operator== ( const char* rhs ) const { return strncmp(base(), rhs, m_size) == 0; } + bool operator== ( const std::string& rhs ) const { return operator== ( rhs.c_str() ); } + bool operator!= ( const std::string& rhs ) const { return !operator== ( rhs.c_str() ); } + bool operator!= ( const char* rhs ) const { return !operator== ( rhs ); } + + size_t headerSize() const { return m_headerSize; } + const char* header() { return m_buffer; } + + size_t read_header() { + size_t sz = (byte)m_buffer[m_headerSize-1]; + for(int i=m_headerSize-2; i >= 0; i--) + sz |= (byte)m_buffer[i] << (8*(m_headerSize-i-1)); + return sz; + } + + int write_header(size_t sz) { + if (sz > m_maxMsgSize) + return -1; + byte b[4] = { (byte)((sz >> 24) & 0xff), (byte)((sz >> 16) & 0xff), + (byte)((sz >> 8) & 0xff), (byte)(sz & 0xff) }; + memcpy(m_buffer, b + 4 - m_headerSize, m_headerSize); + return 0; + } + + char* write( const char* fmt, ... ) { + va_list vargs; + va_start (vargs, fmt); + char* ret = write(0, fmt, vargs); + va_end (vargs); + return ret; + } + + char* copy( const char* s, size_t pos=0 ) + { + if ( resize( strlen(s) + pos + 1, pos != 0 ) == NULL ) + return NULL; + assert( pos < m_size ); + strcpy( base() + pos, s ); + return base(); + } + char* copy( const std::string& s, size_t pos=0 ) + { + if ( resize( length(s) + pos + 1, pos != 0 ) == NULL ) + return NULL; + assert( pos < m_size ); + strcpy( base() + pos, s.c_str() ); + return base(); + } + + char* copy( const char* s, size_t pos, size_t len) + { + assert( pos > 0 && len > 0 && (pos+len) < m_size ); + if ( resize( len + pos + 1, pos != 0 ) == NULL ) + return NULL; + memcpy( base() + pos, s, len ); + return base(); + } + + char* resize( size_t size, bool reallocate = false ) + { + char* old = m_buffer; + const size_t old_sz = m_size; + const size_t new_sz = size + m_headerSize; + + if ( new_sz <= m_size ) { + return m_buffer; + } else + m_size = std::max((const size_t)m_size + m_headerSize + m_minAlloc, new_sz); + + if ( (m_buffer = m_alloc.allocate(m_size)) == NULL ) { + m_buffer = old; + m_size = old_sz; + return (char*) NULL; + } + //fprintf(stderr, "Allocated: x1 = %p, x2=%p (m_size=%d)\r\n", m_buffer, m_buff, m_size); + if ( reallocate && old != m_buffer ) + memcpy(m_buffer, old, old_sz); + if ( old != m_buff ) { + m_alloc.deallocate(old, old_sz); + } + return base(); + } + + }; + + template<int N> std::ostream& operator<< ( std::ostream& os, StringBuffer<N>& buf ) { + return os << buf.c_str(); + } + + template<int N> StringBuffer<N>& operator<< ( StringBuffer<N>& buf, const std::string& s ) { + size_t n = buf.length(); + buf.resize( n + s.size() + 1 ); + strcpy( buf.c_str() + n, s.c_str() ); + return buf; + } + + /// A helper class for dealing with 'struct timeval' structure. This class adds ability + /// to perform arithmetic with the structure leaving the same footprint. + class TimeVal + { + struct timeval m_tv; + + void normalize() { + if (m_tv.tv_usec >= 1000000) + do { ++m_tv.tv_sec; m_tv.tv_usec -= 1000000; } while (m_tv.tv_usec >= 1000000); + else if (m_tv.tv_usec <= -1000000) + do { --m_tv.tv_sec; m_tv.tv_usec += 1000000; } while (m_tv.tv_usec <= -1000000); + + if (m_tv.tv_sec >= 1 && m_tv.tv_usec < 0) { --m_tv.tv_sec; m_tv.tv_usec += 1000000; } + else if (m_tv.tv_sec < 0 && m_tv.tv_usec > 0) { ++m_tv.tv_sec; m_tv.tv_usec -= 1000000; } + } + + public: + enum TimeType { NOW, RELATIVE }; + + TimeVal() { m_tv.tv_sec=0; m_tv.tv_usec=0; } + TimeVal(int _s, int _us) { m_tv.tv_sec=_s; m_tv.tv_usec=_us; normalize(); } + TimeVal(const TimeVal& tv, int _s=0, int _us=0) { set(tv, _s, _us); } + TimeVal(const struct timeval& tv) { m_tv.tv_sec=tv.tv_sec; m_tv.tv_usec=tv.tv_usec; normalize(); } + TimeVal(TimeType tp, int _s=0, int _us=0); + + struct timeval& timeval() { return m_tv; } + const struct timeval& timeval() const { return m_tv; } + int32_t sec() const { return m_tv.tv_sec; } + int32_t usec() const { return m_tv.tv_usec; } + int64_t microsec() const { return (int64_t)m_tv.tv_sec*1000000ull + (int64_t)m_tv.tv_usec; } + void sec (int32_t _sec) { m_tv.tv_sec = _sec; } + void usec(int32_t _usec) { m_tv.tv_usec = _usec; normalize(); } + void microsec(int32_t _m) { m_tv.tv_sec = _m / 1000000ull; m_tv.tv_usec = _m % 1000000ull; } + + void set(const TimeVal& tv, int _s=0, int _us=0) { + m_tv.tv_sec = tv.sec() + _s; m_tv.tv_usec = tv.usec() + _us; normalize(); + } + + double diff(const TimeVal& t) const { + TimeVal tv(this->timeval()); + tv -= t; + return (double)tv.sec() + (double)tv.usec() / 1000000.0; + } + + void clear() { m_tv.tv_sec = 0; m_tv.tv_usec = 0; } + bool zero() { return sec() == 0 && usec() == 0; } + void add(int _sec, int _us) { m_tv.tv_sec += _sec; m_tv.tv_usec += _us; if (_sec || _us) normalize(); } + TimeVal& now(int addS=0, int addUS=0) { gettimeofday(&m_tv, NULL); add(addS, addUS); return *this; } + + void operator-= (const TimeVal& tv) { + m_tv.tv_sec -= tv.sec(); m_tv.tv_usec -= tv.usec(); normalize(); + } + void operator+= (const TimeVal& tv) { + m_tv.tv_sec += tv.sec(); m_tv.tv_usec += tv.usec(); normalize(); + } + void operator+= (int32_t _sec) { m_tv.tv_sec += _sec; } + void operator+= (int64_t _microsec) { + m_tv.tv_sec += (_microsec / 1000000ll); + m_tv.tv_usec += (_microsec % 1000000ll); + normalize(); + } + TimeVal& operator= (const TimeVal& t) { m_tv.tv_sec = t.sec(); m_tv.tv_usec = t.usec(); return *this; } + struct timeval* operator& () { return &m_tv; } + bool operator== (const TimeVal& tv) const { return sec() == tv.sec() && usec() == tv.usec(); } + bool operator!= (const TimeVal& tv) const { return !operator== (tv); } + bool operator< (const TimeVal& tv) const { + return sec() < tv.sec() || (sec() == tv.sec() && usec() < tv.usec()); + } + bool operator<= (const TimeVal& tv) const { + return sec() <= tv.sec() && usec() <= tv.usec(); + } + }; + + TimeVal operator- (const TimeVal& t1, const TimeVal& t2); + TimeVal operator+ (const TimeVal& t1, const TimeVal& t2); + + struct atom_t: public std::string { + typedef std::string BaseT; + atom_t() : BaseT() {} + atom_t(const char* s) : BaseT(s) {} + atom_t(const atom_t& a) : BaseT(reinterpret_cast<const BaseT&>(a)) {} + atom_t(const std::string& s): BaseT(s) {} + }; + + enum ErlTypeT { + etSmallInt = ERL_SMALL_INTEGER_EXT // 'a' + , etInt = ERL_INTEGER_EXT // 'b' + , etFloatOld = ERL_FLOAT_EXT // 'c' + , etFloat = NEW_FLOAT_EXT // 'F' + , etAtom = ERL_ATOM_EXT // 'd' + , etRefOld = ERL_REFERENCE_EXT // 'e' + , etRef = ERL_NEW_REFERENCE_EXT // 'r' + , etPort = ERL_PORT_EXT // 'f' + , etPid = ERL_PID_EXT // 'g' + , etTuple = ERL_SMALL_TUPLE_EXT // 'h' + , etTupleLarge = ERL_LARGE_TUPLE_EXT // 'i' + , etNil = ERL_NIL_EXT // 'j' + , etString = ERL_STRING_EXT // 'k' + , etList = ERL_LIST_EXT // 'l' + , etBinary = ERL_BINARY_EXT // 'm' + , etBignum = ERL_SMALL_BIG_EXT // 'n' + , etBignumLarge = ERL_LARGE_BIG_EXT // 'o' + , etFun = ERL_NEW_FUN_EXT // 'p' + , etFunOld = ERL_FUN_EXT // 'u' + , etNewCache = ERL_NEW_CACHE // 'N' /* c nodes don't know these two */ + , etAtomCached = ERL_CACHED_ATOM // 'C' + }; + + /// Erlang term serializer/deserializer C++ wrapper around C ei library included in + /// Erlang distribution. + class Serializer + { + StringBuffer<1024> m_wbuf; // for writing output commands + StringBuffer<1024> m_rbuf; // for reading input commands + size_t m_readOffset, m_writeOffset; + size_t m_readPacketSz, m_writePacketSz; + int m_wIdx, m_rIdx; + int m_fin, m_fout; + bool m_debug; + + void wcheck(int n) { + if (m_wbuf.resize(m_wIdx + n + 16, true) == NULL) + throw "out of memory"; + } + static int ei_decode_double(const char *buf, int *m_wIdx, double *p); + static int ei_encode_double(char *buf, int *m_wIdx, double p); + static int ei_x_encode_double(ei_x_buff* x, double d); + static int read_exact (int fd, char *buf, size_t len, size_t& offset); + static int write_exact(int fd, const char *buf, size_t len, size_t& offset); + public: + + Serializer(int _headerSz = 2) + : m_wbuf(_headerSz), m_rbuf(_headerSz) + , m_readOffset(0), m_writeOffset(0) + , m_readPacketSz(0), m_writePacketSz(0) + , m_wIdx(0), m_rIdx(0) + , m_fin(0), m_fout(1), m_debug(false) + , tuple(*this) + { + ei_encode_version(&m_wbuf, &m_wIdx); + } + + void reset_rbuf(bool _saveVersion=true) { + m_rIdx = _saveVersion ? 1 : 0; + m_readPacketSz = m_readOffset = 0; + } + void reset_wbuf(bool _saveVersion=true) { + m_wIdx = _saveVersion ? 1 : 0; + m_writePacketSz = m_writeOffset = 0; + } + void reset(bool _saveVersion=true) { + reset_rbuf(_saveVersion); + reset_wbuf(_saveVersion); + } + void debug(bool _enable) { m_debug = _enable; } + + // This is a helper class for encoding tuples using streaming operator. + // Example: encode {ok, 123, "test"} + // + // Serializer ser; + // ser.tuple << atom_t("ok") << 123 << "test"; + // + class Tuple { + Serializer& m_parent; + + class Temp { + Tuple& m_tuple; + mutable int m_idx; // offset to the tuple's size in m_parent.m_wbuf + mutable int m_size; + mutable bool m_last; + public: + template<typename T> + Temp(Tuple& t, const T& v) + : m_tuple(t), m_idx(m_tuple.m_parent.m_wIdx+1), m_size(1), m_last(true) + { + m_tuple.m_parent.encodeTupleSize(1); + m_tuple.m_parent.encode(v); + } + + Temp(const Temp& o) + : m_tuple(o.m_tuple), m_idx(o.m_idx), m_size(o.m_size+1), m_last(o.m_last) + { + o.m_last = false; + } + + ~Temp() { + if (m_last) { + // This is the end of the tuple being streamed to this class. Update tuple size. + if (m_size > 255) + throw "Use of operator<< only allowed for tuples with less than 256 items!"; + else if (m_size > 1) { + char* sz = &m_tuple.m_parent.m_wbuf + m_idx; + *sz = m_size; + } + } + } + template<typename T> + Temp operator<< (const T& v) { + Temp t(*this); + m_tuple.m_parent.encode(v); + return t; + } + }; + + public: + Tuple(Serializer& s) : m_parent(s) {} + + template<typename T> + Temp operator<< (const T& v) { + Temp t(*this, v); + return t; + } + }; + + /// Helper class for encoding/decoding tuples using streaming operator. + Tuple tuple; + + void encode(const char* s) { wcheck(strlen(s)+1); ei_encode_string(&m_wbuf, &m_wIdx, s); } + void encode(char v) { wcheck(2); ei_encode_char(&m_wbuf, &m_wIdx, v); } + void encode(int i) { wcheck(sizeof(i)); ei_encode_long(&m_wbuf, &m_wIdx, i); } + void encode(unsigned int i) { wcheck(8); ei_encode_ulong(&m_wbuf, &m_wIdx, i); } + void encode(long l) { wcheck(sizeof(l)); ei_encode_long(&m_wbuf, &m_wIdx, l); } + void encode(unsigned long l) { wcheck(sizeof(l)); ei_encode_ulong(&m_wbuf, &m_wIdx, l); } + void encode(long long i) { int n=0; ei_encode_longlong (NULL,&n,i); wcheck(n); ei_encode_longlong(&m_wbuf,&m_wIdx,i); } + void encode(unsigned long long i) { int n=0; ei_encode_ulonglong(NULL,&n,i); wcheck(n); ei_encode_ulonglong(&m_wbuf,&m_wIdx,i); } + void encode(bool b) { wcheck(8); ei_encode_boolean(&m_wbuf, &m_wIdx, b); } + void encode(double v) { wcheck(9); ei_encode_double(&m_wbuf, &m_wIdx, v); } + void encode(const std::string& s) { wcheck(s.size()+1); ei_encode_string(&m_wbuf, &m_wIdx, s.c_str()); } + void encode(const atom_t& a) { wcheck(a.size()+1); ei_encode_atom(&m_wbuf, &m_wIdx, a.c_str()); } + void encode(const erlang_pid& p) { int n=0; ei_encode_pid(NULL, &n, &p); wcheck(n); ei_encode_pid(&m_wbuf, &m_wIdx, &p); } + void encode(const void* p, int sz) { wcheck(sz+4); ei_encode_binary(&m_wbuf, &m_wIdx, p, sz); } + void encodeTupleSize(int sz) { wcheck(5); ei_encode_tuple_header(&m_wbuf, &m_wIdx, sz); } + void encodeListSize(int sz) { wcheck(5); ei_encode_list_header(&m_wbuf, &m_wIdx, sz); } + void encodeListEnd() { wcheck(1); ei_encode_empty_list(&m_wbuf, &m_wIdx); } + + int encodeListBegin() { wcheck(5); int n=m_wIdx; ei_encode_list_header(&m_wbuf, &m_wIdx, 1); return n; } + /// This function for encoding the list size after all elements are encoded. + /// @param sz is the number of elements in the list. + /// @param idx is the index position of the beginning of the list. + // E.g. + // Serializer se; + // int n = 0; + // int idx = se.encodeListBegin(); + // se.encode(1); n++; + // se.encode("abc"); n++; + // se.encodeListEnd(n, idx); + void encodeListEnd(int sz,int idx) { ei_encode_list_header(&m_wbuf, &idx, sz); encodeListEnd(); } + + ErlTypeT decodeType(int& size) { int t; return (ErlTypeT)(ei_get_type(&m_rbuf, &m_rIdx, &t, &size) < 0 ? -1 : t); } + int decodeInt(int& v) { long l, ret = decodeInt(l); v = l; return ret; } + int decodeInt(long& v) { return (ei_decode_long(&m_rbuf, &m_rIdx, &v) < 0) ? -1 : 0; } + int decodeUInt(unsigned int& v) { unsigned long l, ret = decodeUInt(l); v = l; return ret; } + int decodeUInt(unsigned long& v) { return (ei_decode_ulong(&m_rbuf, &m_rIdx, &v) < 0) ? -1 : 0; } + int decodeTupleSize() { int v; return (ei_decode_tuple_header(&m_rbuf,&m_rIdx,&v) < 0) ? -1 : v; } + int decodeListSize() { int v; return (ei_decode_list_header(&m_rbuf,&m_rIdx,&v) < 0) ? -1 : v; } + int decodeListEnd() { bool b = *(m_rbuf.c_str()+m_rIdx) == ERL_NIL_EXT; if (b) { m_rIdx++; return 0; } else return -1; } + int decodeAtom(std::string& a) { char s[MAXATOMLEN]; if (ei_decode_atom(&m_rbuf,&m_rIdx,s) < 0) return -1; a=s; return 0; } + int decodeBool(bool& a) { + std::string s; + if (decodeAtom(s) < 0) return -1; + else if (s == "true") { a = true; return 0; } + else if (s == "false") { a = false; return 0; } + else return -1; + } + + int decodeString(std::string& a) { + StringBuffer<256> s; + if (decodeString(s) < 0) + return -1; + a = s.c_str(); + return 0; + } + template <int N> + int decodeString(StringBuffer<N>& s) { + int size; + if (decodeType(size) != etString || !s.resize(size) || ei_decode_string(&m_rbuf, &m_rIdx, s.c_str())) + return -1; + return size; + } + + int decodeBinary(std::string& data) { + int size; + if (decodeType(size) != etBinary) return -1; + data.resize(size); + long sz; + if (ei_decode_binary(&m_rbuf, &m_rIdx, (void*)data.c_str(), &sz) < 0) return -1; + return sz; + } + + /// Print input buffer to stream + int print(std::ostream& os, const std::string& header = ""); + + /// Assumes the command is encoded as an atom. This function takes an + /// array of strings and matches the atom to it. The index of the matched + /// string in the <cmds> array is returned. + template <int M> + int decodeAtomIndex(const char* (&cmds)[M], std::string& cmd, int firstIdx = 0) { + if (decodeAtom(cmd) < 0) return -1; + return stringIndex(cmds, cmd, firstIdx, M); + } + + /// Same as previous version but <cmds> array must have the last element being NULL + int decodeAtomIndex(const char** cmds, std::string& cmd, int firstIdx = 0) { + if (decodeAtom(cmd) < 0) return -1; + return stringIndex(cmds, cmd, firstIdx); + } + + int set_handles(int in, int out, bool non_blocking = false); + void close_handles() { ::close(m_fin); ::close(m_fout); } + + int read_handle() { return m_fin; } + int write_handle() { return m_fout; } + + const char* read_buffer() const { return &m_rbuf; } + const char* write_buffer() const { return &m_wbuf; } + int* read_index() { return &m_rIdx; } + int* write_index() { return &m_wIdx; } + int read_idx() const { return m_rIdx; } + int write_idx() const { return m_wIdx; } + + /// Read command from <m_fin> into the internal buffer + int read(); + /// Write command from <m_fout> into the internal buffer + int write(); + + /// Copy the content of write buffer from another serializer + int wcopy( const Serializer& ser) { return m_wbuf.copy( ser.write_buffer(), 0, ser.write_idx()) != 0 ? 0 : -1; } + /// Copy the content of read buffer from another serializer + int rcopy( const Serializer& ser) { return m_rbuf.copy( ser.read_buffer(), 0, ser.read_idx() ) != 0 ? 0 : -1; } + + /// dump read/write buffer's content to stream + std::ostream& dump(std::ostream& os, bool outWriteBuffer); + }; + + /// Dump content of internal buffer to stream. + std::ostream& dump(std::ostream& out, const unsigned char* a_buf = NULL, int n = 0, bool eol = true); + // Write ei_x_buff to stream + std::ostream& operator<< (std::ostream& os, const ei_x_buff& buf); + bool dump(const char* header, std::ostream& out, const ei_x_buff& buf, bool condition); + +} // namespace + +#endif + diff --git a/deps/exec/c_src/exec.cpp b/deps/exec/c_src/exec.cpp new file mode 100644 index 0000000..c1281c1 --- /dev/null +++ b/deps/exec/c_src/exec.cpp @@ -0,0 +1,2111 @@ +/* + exec.cpp + + Author: Serge Aleynikov + Created: 2003/07/10 + + Description: + ============ + + Erlang port program for spawning and controlling OS tasks. + It listens for commands sent from Erlang and executes them until + the pipe connecting it to Erlang VM is closed or the program + receives SIGINT or SIGTERM. At that point it kills all processes + it forked by issuing SIGTERM followed by SIGKILL in 6 seconds. + + Marshalling protocol: + Erlang C++ + | ---- {TransId::integer(), Instruction::tuple()} ---> | + | <----------- {TransId::integer(), Reply} ----------- | + + Instruction = {manage, OsPid::integer(), Options} | + {run, Cmd::string(), Options} | + {list} | + {stop, OsPid::integer()} | + {kill, OsPid::integer(), Signal::integer()} | + {stdin, OsPid::integer(), Data::binary()} + + Options = [Option] + Option = {cd, Dir::string()} | + {env, [string() | {string(), string()}]} | + {kill, Cmd::string()} | + {kill_timeout, Sec::integer()} | + kill_group | + {group, integer() | string()} | + {user, User::string()} | + {nice, Priority::integer()} | + stdin | {stdin, null | close | File::string()} | + stdout | {stdout, Device::string()} | + stderr | {stderr, Device::string()} | + pty | {success_exit_code, N::integer()} + + Device = close | null | stderr | stdout | File::string() | {append, File::string()} + + Reply = ok | // For kill/stop commands + {ok, OsPid} | // For run command + {ok, [OsPid]} | // For list command + {error, Reason} | + {exit_status, OsPid, Status} // OsPid terminated with Status + + Reason = atom() | string() + OsPid = integer() + Status = integer() +*/ + +#include <stdio.h> +#include <stdlib.h> +#include <errno.h> +#include <signal.h> +#include <unistd.h> +#include <signal.h> +#include <termios.h> +#include <sys/ioctl.h> + +#ifdef HAVE_CAP +#include <sys/prctl.h> +#include <sys/capability.h> +#endif + +#include <assert.h> +#include <sys/types.h> +#include <sys/wait.h> +#include <sys/time.h> +#include <sys/resource.h> +#include <setjmp.h> +#include <limits.h> +#include <grp.h> +#include <pwd.h> +#include <fcntl.h> +#include <map> +#include <list> +#include <deque> +#include <set> +#include <sstream> + +#include <ei.h> +#include "ei++.hpp" + +#if defined(__CYGWIN__) || defined(__WIN32) || defined(__APPLE__) \ + || (defined(__sun) && defined(__SVR4)) +# define NO_SIGTIMEDWAIT +# define sigtimedwait(a, b, c) 0 +# define sigisemptyset(s) \ + !(sigismember(s, SIGCHLD) || sigismember(s, SIGPIPE) || \ + sigismember(s, SIGTERM) || sigismember(s, SIGINT) || \ + sigismember(s, SIGHUP)) +#endif + +using namespace ei; + +//------------------------------------------------------------------------- +// Defines +//------------------------------------------------------------------------- + +#define BUF_SIZE 2048 + +// In the event we have tried to kill something, wait this many +// seconds and then *really* kill it with SIGKILL if needs be +#define KILL_TIMEOUT_SEC 5 + +// Max number of seconds to sleep in the select() call +#define SLEEP_TIMEOUT_SEC 5 + +// Number of seconds allowed for cleanup before exit +#define FINALIZE_DEADLINE_SEC 10 + +//------------------------------------------------------------------------- +// Global variables +//------------------------------------------------------------------------- + +extern char **environ; // process environment + +ei::Serializer eis(/* packet header size */ 2); + +sigjmp_buf jbuf; +static int alarm_max_time = FINALIZE_DEADLINE_SEC + 2; +static int debug = 0; +static bool oktojump = false; +static int terminated = 0; // indicates that we got a SIGINT / SIGTERM event +static bool superuser = false; +static bool pipe_valid = true; +static int max_fds; +static int dev_null; + +static const int DEF_MODE = S_IRUSR | S_IWUSR | S_IRGRP | S_IROTH; +static const char* CS_DEV_NULL = "/dev/null"; + +//------------------------------------------------------------------------- +// Types & variables +//------------------------------------------------------------------------- + +struct CmdInfo; + +typedef unsigned char byte; +typedef int exit_status_t; +typedef pid_t kill_cmd_pid_t; +typedef std::list<std::string> CmdArgsList; +typedef std::pair<pid_t, exit_status_t> PidStatusT; +typedef std::pair<pid_t, CmdInfo> PidInfoT; +typedef std::map <pid_t, CmdInfo> MapChildrenT; +typedef std::pair<kill_cmd_pid_t, pid_t> KillPidStatusT; +typedef std::map <kill_cmd_pid_t, pid_t> MapKillPidT; +typedef std::map<std::string, std::string> MapEnv; +typedef MapEnv::iterator MapEnvIterator; +typedef std::map<pid_t, exit_status_t> ExitedChildrenT; + +MapChildrenT children; // Map containing all managed processes started by this port program. +MapKillPidT transient_pids; // Map of pids of custom kill commands. +ExitedChildrenT exited_children;// Set of processed SIGCHLD events + +#define SIGCHLD_MAX_SIZE 4096 + +enum RedirectType { + REDIRECT_STDOUT = -1, // Redirect to stdout + REDIRECT_STDERR = -2, // Redirect to stderr + REDIRECT_NONE = -3, // No output redirection + REDIRECT_CLOSE = -4, // Close output file descriptor + REDIRECT_ERL = -5, // Redirect output back to Erlang + REDIRECT_FILE = -6, // Redirect output to file + REDIRECT_NULL = -7 // Redirect input/output to /dev/null +}; + +std::string fd_type(int tp) { + switch (tp) { + case REDIRECT_STDOUT: return "stdout"; + case REDIRECT_STDERR: return "stderr"; + case REDIRECT_NONE: return "none"; + case REDIRECT_CLOSE: return "close"; + case REDIRECT_ERL: return "erlang"; + case REDIRECT_FILE: return "file"; + case REDIRECT_NULL: return "null"; + default: { + std::stringstream s; + if (tp == dev_null) + s << "null(fd:" << tp << ')'; + else + s << "fd:" << tp; + return s.str(); + } + } + return std::string(); // Keep the compiler happy +} + +struct CmdOptions; + +//------------------------------------------------------------------------- +// Local Functions +//------------------------------------------------------------------------- + +int send_ok(int transId, pid_t pid = -1); +int send_pid_status_term(const PidStatusT& stat); +int send_error_str(int transId, bool asAtom, const char* fmt, ...); +int send_pid_list(int transId, const MapChildrenT& children); +int send_ospid_output(int pid, const char* type, const char* data, int len); + +pid_t start_child(CmdOptions& op, std::string& err); +int kill_child(pid_t pid, int sig, int transId, bool notify=true); +int check_children(const TimeVal& now, int& isTerminated, bool notify = true); +bool process_pid_input(CmdInfo& ci); +void process_pid_output(CmdInfo& ci, int maxsize = 4096); +void stop_child(pid_t pid, int transId, const TimeVal& now); +int stop_child(CmdInfo& ci, int transId, const TimeVal& now, bool notify = true); +void erase_child(MapChildrenT::iterator& it); + +int process_command(); +void initialize(int userid, bool use_alt_fds); +int finalize(); +int set_nonblock_flag(pid_t pid, int fd, bool value); +int erl_exec_kill(pid_t pid, int signal); +int open_file(const char* file, bool append, const char* stream, + ei::StringBuffer<128>& err, int mode = DEF_MODE); +int open_pipe(int fds[2], const char* stream, ei::StringBuffer<128>& err); + +//------------------------------------------------------------------------- +// Types +//------------------------------------------------------------------------- + +struct CmdOptions { +private: + ei::StringBuffer<256> m_tmp; + std::stringstream m_err; + bool m_shell; + bool m_pty; + std::string m_executable; + CmdArgsList m_cmd; + std::string m_cd; + std::string m_kill_cmd; + int m_kill_timeout; + bool m_kill_group; + MapEnv m_env; + const char** m_cenv; + long m_nice; // niceness level + int m_group; // used in setgid() + int m_user; // run as + int m_success_exit_code; + std::string m_std_stream[3]; + bool m_std_stream_append[3]; + int m_std_stream_fd[3]; + int m_std_stream_mode[3]; + + void init_streams() { + for (int i=STDIN_FILENO; i <= STDERR_FILENO; i++) { + m_std_stream_append[i] = false; + m_std_stream_mode[i] = DEF_MODE; + m_std_stream_fd[i] = REDIRECT_NULL; + m_std_stream[i] = CS_DEV_NULL; + } + } + +public: + CmdOptions() + : m_tmp(0, 256), m_shell(true), m_pty(false) + , m_kill_timeout(KILL_TIMEOUT_SEC) + , m_kill_group(false) + , m_cenv(NULL), m_nice(INT_MAX) + , m_group(INT_MAX), m_user(INT_MAX) + , m_success_exit_code(0) + { + init_streams(); + } + CmdOptions(const CmdArgsList& cmd, const char* cd = NULL, const char** env = NULL, + int user = INT_MAX, int nice = INT_MAX, int group = INT_MAX) + : m_shell(true), m_pty(false), m_cmd(cmd), m_cd(cd ? cd : "") + , m_kill_timeout(KILL_TIMEOUT_SEC) + , m_kill_group(false) + , m_cenv(NULL), m_nice(INT_MAX) + , m_group(group), m_user(user) + { + init_streams(); + } + ~CmdOptions() { + if (m_cenv != (const char**)environ) delete [] m_cenv; + m_cenv = NULL; + } + + const char* strerror() const { return m_err.str().c_str(); } + const std::string& executable() const { return m_executable; } + const CmdArgsList& cmd() const { return m_cmd; } + bool shell() const { return m_shell; } + bool pty() const { return m_pty; } + const char* cd() const { return m_cd.c_str(); } + char* const* env() const { return (char* const*)m_cenv; } + const char* kill_cmd() const { return m_kill_cmd.c_str(); } + int kill_timeout() const { return m_kill_timeout; } + bool kill_group() const { return m_kill_group; } + int group() const { return m_group; } + int user() const { return m_user; } + int success_exit_code() const { return m_success_exit_code; } + int nice() const { return m_nice; } + const char* stream_file(int i) const { return m_std_stream[i].c_str(); } + bool stream_append(int i) const { return m_std_stream_append[i]; } + int stream_mode(int i) const { return m_std_stream_mode[i]; } + int stream_fd(int i) const { return m_std_stream_fd[i]; } + int& stream_fd(int i) { return m_std_stream_fd[i]; } + std::string stream_fd_type(int i) const { return fd_type(stream_fd(i)); } + + void executable(const std::string& s) { m_executable = s; } + + void stream_file(int i, const std::string& file, bool append = false, int mode = DEF_MODE) { + m_std_stream_fd[i] = REDIRECT_FILE; + m_std_stream_append[i] = append; + m_std_stream_mode[i] = mode; + m_std_stream[i] = file; + } + + void stream_null(int i) { + m_std_stream_fd[i] = REDIRECT_NULL; + m_std_stream_append[i] = false; + m_std_stream[i] = CS_DEV_NULL; + } + + void stream_redirect(int i, RedirectType type) { + m_std_stream_fd[i] = type; + m_std_stream_append[i] = false; + m_std_stream[i].clear(); + } + + int ei_decode(ei::Serializer& ei, bool getCmd = false); + int init_cenv(); +}; + +/// Contains run-time info of a child OS process. +/// When a user provides a custom command to kill a process this +/// structure will contain its run-time information. +struct CmdInfo { + CmdArgsList cmd; // Executed command + pid_t cmd_pid; // Pid of the custom kill command + pid_t cmd_gid; // Command's group ID + std::string kill_cmd; // Kill command to use (default: use SIGTERM) + kill_cmd_pid_t kill_cmd_pid; // Pid of the command that <pid> is supposed to kill + ei::TimeVal deadline; // Time when the <cmd_pid> is supposed to be killed using SIGTERM. + bool sigterm; // <true> if sigterm was issued. + bool sigkill; // <true> if sigkill was issued. + int kill_timeout; // Pid shutdown interval in sec before it's killed with SIGKILL + bool kill_group; // Indicates if at exit the whole group needs to be killed + int success_code; // Exit code to use on success + bool managed; // <true> if this pid is started externally, but managed by erlexec + int stream_fd[3]; // Pipe fd getting process's stdin/stdout/stderr + int stdin_wr_pos; // Offset of the unwritten portion of the head item of stdin_queue + std::list<std::string> stdin_queue; + + CmdInfo() { + new (this) CmdInfo(CmdArgsList(), "", 0, INT_MAX, 0); + } + CmdInfo(const CmdInfo& ci) { + new (this) CmdInfo(ci.cmd, ci.kill_cmd.c_str(), ci.cmd_pid, ci.cmd_gid, + ci.success_code, ci.managed, + ci.stream_fd[STDIN_FILENO], ci.stream_fd[STDOUT_FILENO], + ci.stream_fd[STDERR_FILENO], ci.kill_timeout, ci.kill_group); + } + CmdInfo(bool managed, const char* _kill_cmd, pid_t _cmd_pid, int _ok_code, + bool _kill_group = false) { + new (this) CmdInfo(cmd, _kill_cmd, _cmd_pid, getpgid(_cmd_pid), _ok_code, managed); + kill_group = _kill_group; + } + CmdInfo(const CmdArgsList& _cmd, const char* _kill_cmd, pid_t _cmd_pid, pid_t _cmd_gid, + int _success_code, + bool _managed = false, + int _stdin_fd = REDIRECT_NULL, + int _stdout_fd = REDIRECT_NONE, + int _stderr_fd = REDIRECT_NONE, + int _kill_timeout = KILL_TIMEOUT_SEC, + bool _kill_group = false) + : cmd(_cmd) + , cmd_pid(_cmd_pid) + , cmd_gid(_cmd_gid) + , kill_cmd(_kill_cmd), kill_cmd_pid(-1) + , sigterm(false), sigkill(false) + , kill_timeout(_kill_timeout) + , kill_group(_kill_group) + , success_code(_success_code) + , managed(_managed), stdin_wr_pos(0) + { + stream_fd[STDIN_FILENO] = _stdin_fd; + stream_fd[STDOUT_FILENO] = _stdout_fd; + stream_fd[STDERR_FILENO] = _stderr_fd; + } + + void include_stream_fd(int i, int& maxfd, fd_set* readfds, fd_set* writefds) { + bool ok; + fd_set* fds; + + if (i == STDIN_FILENO) { + ok = stream_fd[i] >= 0 && stdin_wr_pos > 0; + if (ok && debug > 2) + fprintf(stderr, "Pid %d adding stdin available notification (fd=%d, pos=%d)\r\n", + cmd_pid, stream_fd[i], stdin_wr_pos); + fds = writefds; + } else { + ok = stream_fd[i] >= 0; + if (ok && debug > 2) + fprintf(stderr, "Pid %d adding stdout checking (fd=%d)\r\n", cmd_pid, stream_fd[i]); + fds = readfds; + } + + if (ok) { + FD_SET(stream_fd[i], fds); + if (stream_fd[i] > maxfd) maxfd = stream_fd[i]; + } + } + + void process_stream_data(int i, fd_set* readfds, fd_set* writefds) { + int fd = stream_fd[i]; + fd_set* fds = i == STDIN_FILENO ? writefds : readfds; + + if (fd < 0 || !FD_ISSET(fd, fds)) return; + + if (i == STDIN_FILENO) + process_pid_input(*this); + else + process_pid_output(*this); + } +}; + +//------------------------------------------------------------------------- +// Local Functions +//------------------------------------------------------------------------- + +const char* stream_name(int i) { + switch (i) { + case STDIN_FILENO: return "stdin"; + case STDOUT_FILENO: return "stdout"; + case STDERR_FILENO: return "stderr"; + default: return "<unknown>"; + } +} + +void gotsignal(int signal) +{ + if (signal == SIGTERM || signal == SIGINT || signal == SIGPIPE) + terminated = 1; + if (signal == SIGPIPE) + pipe_valid = false; + if (debug) + fprintf(stderr, "Got signal: %d (oktojump=%d)\r\n", signal, oktojump); + if (oktojump) siglongjmp(jbuf, 1); +} + +void check_child(pid_t pid, int signal = -1) +{ + int status = 0; + pid_t ret; + + if (pid == getpid()) // Safety check. Never kill itself + return; + + if (exited_children.find(pid) != exited_children.end()) + return; + + while ((ret = waitpid(pid, &status, WNOHANG)) < 0 && errno == EINTR); + + if (debug) + fprintf(stderr, + "* Process %d (ret=%d, status=%d, sig=%d, " + "oktojump=%d, exited_count=%ld%s%s)\r\n", + pid, ret, status, signal, oktojump, exited_children.size(), + ret > 0 && WIFEXITED(status) ? " [exited]":"", + ret > 0 && WIFSIGNALED(status) ? " [signaled]":""); + + if (ret < 0 && errno == ECHILD) { + if (erl_exec_kill(pid, 0) == 0) // process likely forked and is alive + status = 0; + if (status != 0) + exited_children.insert(std::make_pair(pid <= 0 ? ret : pid, status)); + } else if (pid <= 0 && ret > 0) { + exited_children.insert(std::make_pair(ret, status == 0 && signal == -1 ? 1 : status)); + } else if (ret == pid || WIFEXITED(status) || WIFSIGNALED(status)) { + if (ret > 0) + exited_children.insert(std::make_pair(pid, status)); + } + + if (oktojump) siglongjmp(jbuf, 1); +} + +void gotsigchild(int signal, siginfo_t* si, void* context) +{ + // If someone used kill() to send SIGCHLD ignore the event + if (si->si_code == SI_USER || signal != SIGCHLD) + return; + + pid_t pid = si->si_pid; + + if (debug) + fprintf(stderr, "Child process %d exited\r\n", pid); + + check_child(pid, signal); +} + +void add_exited_child(pid_t pid, exit_status_t status) { + std::pair<pid_t, exit_status_t> value = std::make_pair(pid, status); + // Note the following function doesn't insert anything if the element + // with given key was already present in the map + exited_children.insert(value); +} + +void check_pending() +{ + #if !defined(NO_SIGTIMEDWAIT) + static const struct timespec timeout = {0, 0}; + #endif + + sigset_t set; + siginfo_t info; + int sig; + sigemptyset(&set); + if (sigpending(&set) == 0 && !sigisemptyset(&set)) { + if (debug > 1) + fprintf(stderr, "Detected pending signals\r\n"); + + while ((sig = sigtimedwait(&set, &info, &timeout)) > 0 || errno == EINTR) + switch (sig) { + case SIGCHLD: gotsigchild(sig, &info, NULL); break; + case SIGPIPE: pipe_valid = false; /* intentionally follow through */ + case SIGTERM: + case SIGINT: + case SIGHUP: gotsignal(sig); break; + default: break; + } + } +} + +int set_nice(pid_t pid,int nice, std::string& error) +{ + ei::StringBuffer<128> err; + + if (nice != INT_MAX && setpriority(PRIO_PROCESS, pid, nice) < 0) { + err.write("Cannot set priority of pid %d to %d", pid, nice); + error = err.c_str(); + if (debug) + fprintf(stderr, "%s\r\n", error.c_str()); + return -1; + } + return 0; +} + +void usage(char* progname) { + fprintf(stderr, + "Usage:\n" + " %s [-n] [-alarm N] [-debug [Level]] [-user User]\n" + "Options:\n" + " -n - Use marshaling file descriptors 3&4 instead of default 0&1.\n" + " -alarm N - Allow up to <N> seconds to live after receiving SIGTERM/SIGINT (default %d)\n" + " -debug [Level] - Turn on debug mode (default Level: 1)\n" + " -user User - If started by root, run as User\n" + "Description:\n" + " This is a port program intended to be started by an Erlang\n" + " virtual machine. It can start/kill/list OS processes\n" + " as requested by the virtual machine.\n", + progname, alarm_max_time); + exit(1); +} + +//------------------------------------------------------------------------- +// MAIN +//------------------------------------------------------------------------- + +int main(int argc, char* argv[]) +{ + fd_set readfds, writefds; + struct sigaction sact, sterm; + int userid = 0; + bool use_alt_fds = false; + + sterm.sa_handler = gotsignal; + sigemptyset(&sterm.sa_mask); + sigaddset(&sterm.sa_mask, SIGCHLD); + sterm.sa_flags = 0; + sigaction(SIGINT, &sterm, NULL); + sigaction(SIGTERM, &sterm, NULL); + sigaction(SIGHUP, &sterm, NULL); + sigaction(SIGPIPE, &sterm, NULL); + + sact.sa_handler = NULL; + sact.sa_sigaction = gotsigchild; + sigemptyset(&sact.sa_mask); + sact.sa_flags = SA_SIGINFO | SA_RESTART | SA_NOCLDSTOP; // | SA_NODEFER; + sigaction(SIGCHLD, &sact, NULL); + + if (argc > 1) { + int res; + for(res = 1; res < argc; res++) { + if (strcmp(argv[res], "-h") == 0 || strcmp(argv[res], "--help") == 0) { + usage(argv[0]); + } else if (strcmp(argv[res], "-debug") == 0) { + debug = (res+1 < argc && argv[res+1][0] != '-') ? atoi(argv[++res]) : 1; + if (debug > 3) + eis.debug(true); + } else if (strcmp(argv[res], "-alarm") == 0 && res+1 < argc) { + if (argv[res+1][0] != '-') + alarm_max_time = atoi(argv[++res]); + else + usage(argv[0]); + } else if (strcmp(argv[res], "-n") == 0) { + use_alt_fds = true; + } else if (strcmp(argv[res], "-user") == 0 && res+1 < argc && argv[res+1][0] != '-') { + char* run_as_user = argv[++res]; + struct passwd *pw = NULL; + if ((pw = getpwnam(run_as_user)) == NULL) { + fprintf(stderr, "User %s not found!\r\n", run_as_user); + exit(3); + } + userid = pw->pw_uid; + } + } + } + + initialize(userid, use_alt_fds); + + while (!terminated) { + + sigsetjmp(jbuf, 1); oktojump = 0; + + FD_ZERO (&writefds); + FD_ZERO (&readfds); + + FD_SET (eis.read_handle(), &readfds); + + int maxfd = eis.read_handle(); + + TimeVal now(TimeVal::NOW); + + while (!terminated && !exited_children.empty()) { + if (check_children(now, terminated) < 0) + break; + } + + double wakeup = SLEEP_TIMEOUT_SEC; + + // Set up all stdout/stderr input streams that we need to monitor and redirect to Erlang + for(MapChildrenT::iterator it=children.begin(), end=children.end(); it != end; ++it) + for (int i=STDIN_FILENO; i <= STDERR_FILENO; i++) { + it->second.include_stream_fd(i, maxfd, &readfds, &writefds); + if (!it->second.deadline.zero()) + wakeup = std::max(0.0, std::min(wakeup, it->second.deadline.diff(now))); + } + + check_pending(); // Check for pending signals arrived while we were in the signal handler + + if (terminated || wakeup < 0) break; + + oktojump = 1; + ei::TimeVal timeout((int)wakeup, (wakeup - (int)wakeup) * 1000000); + + if (debug > 2) + fprintf(stderr, "Selecting maxfd=%d (sleep={%ds,%dus})\r\n", + maxfd, timeout.sec(), timeout.usec()); + + int cnt = select (maxfd+1, &readfds, &writefds, (fd_set *) 0, &timeout.timeval()); + int interrupted = (cnt < 0 && errno == EINTR); + oktojump = 0; + + if (debug > 2) + fprintf(stderr, "Select got %d events (maxfd=%d)\r\n", cnt, maxfd); + + if (interrupted || cnt == 0) { + now.now(); + if (check_children(now, terminated) < 0) { + terminated = 11; + break; + } + } else if (cnt < 0) { + if (errno == EBADF) { + if (debug) + fprintf(stderr, "Error EBADF(9) in select: %s (terminated=%d)\r\n", + strerror(errno), terminated); + continue; + } + fprintf(stderr, "Error %d in select: %s\r\n", errno, strerror(errno)); + terminated = 12; + break; + } else if ( FD_ISSET (eis.read_handle(), &readfds) ) { + /* Read from input stream a command sent by Erlang */ + if (process_command() < 0) { + break; + } + } else { + // Check if any stdout/stderr streams have data + for(MapChildrenT::iterator it=children.begin(), end=children.end(); it != end; ++it) + for (int i=STDIN_FILENO; i <= STDERR_FILENO; i++) + it->second.process_stream_data(i, &readfds, &writefds); + } + } + + sigsetjmp(jbuf, 1); oktojump = 0; + + return finalize(); + +} + +int process_command() +{ + int err, arity; + long transId; + std::string command; + + // Note that if we were using non-blocking reads, we'd also need to check + // for errno EWOULDBLOCK. + if ((err = eis.read()) < 0) { + if (debug) + fprintf(stderr, "Broken Erlang command pipe (%d): %s\r\n", + errno, strerror(errno)); + terminated = errno; + return -1; + } + + /* Our marshalling spec is that we are expecting a tuple + * TransId, {Cmd::atom(), Arg1, Arg2, ...}} */ + if (eis.decodeTupleSize() != 2 || + (eis.decodeInt(transId)) < 0 || + (arity = eis.decodeTupleSize()) < 1) + { + terminated = 12; + return -1; + } + + enum CmdTypeT { MANAGE, RUN, STOP, KILL, LIST, SHUTDOWN, STDIN } cmd; + const char* cmds[] = { "manage","run","stop","kill","list","shutdown","stdin" }; + + /* Determine the command */ + if ((int)(cmd = (CmdTypeT) eis.decodeAtomIndex(cmds, command)) < 0) { + if (send_error_str(transId, false, "Unknown command: %s", command.c_str()) < 0) { + terminated = 13; + return -1; + } + return 0; + } + + switch (cmd) { + case SHUTDOWN: { + terminated = 0; + return -1; + } + case MANAGE: { + // {manage, Cmd::string(), Options::list()} + CmdOptions po; + long pid; + pid_t realpid; + + if (arity != 3 || (eis.decodeInt(pid)) < 0 || po.ei_decode(eis) < 0) { + send_error_str(transId, true, "badarg"); + return 0; + } + realpid = pid; + + CmdInfo ci(true, po.kill_cmd(), realpid, po.success_exit_code(), po.kill_group()); + ci.kill_timeout = po.kill_timeout(); + children[realpid] = ci; + + // Set nice priority for managed process if option is present + std::string error; + set_nice(realpid,po.nice(),error); + + send_ok(transId, pid); + break; + } + case RUN: { + // {run, Cmd::string(), Options::list()} + CmdOptions po; + + if (arity != 3 || po.ei_decode(eis, true) < 0) { + send_error_str(transId, false, po.strerror()); + break; + } + + pid_t pid; + std::string err; + if ((pid = start_child(po, err)) < 0) + send_error_str(transId, false, "Couldn't start pid: %s", err.c_str()); + else { + CmdInfo ci(po.cmd(), po.kill_cmd(), pid, + getpgid(pid), + po.success_exit_code(), false, + po.stream_fd(STDIN_FILENO), + po.stream_fd(STDOUT_FILENO), + po.stream_fd(STDERR_FILENO), + po.kill_timeout(), + po.kill_group()); + children[pid] = ci; + send_ok(transId, pid); + } + break; + } + case STOP: { + // {stop, OsPid::integer()} + long pid; + if (arity != 2 || eis.decodeInt(pid) < 0) { + send_error_str(transId, true, "badarg"); + break; + } + stop_child(pid, transId, TimeVal(TimeVal::NOW)); + break; + } + case KILL: { + // {kill, OsPid::integer(), Signal::integer()} + long pid, sig; + if (arity != 3 || eis.decodeInt(pid) < 0 || eis.decodeInt(sig) < 0 || pid == -1) { + send_error_str(transId, true, "badarg"); + break; + } else if (pid < 0) { + send_error_str(transId, false, "Not allowed to send signal to all processes"); + break; + } else if (superuser && children.find(pid) == children.end()) { + send_error_str(transId, false, "Cannot kill a pid not managed by this application"); + break; + } + kill_child(pid, sig, transId); + break; + } + case LIST: { + // {list} + if (arity != 1) { + send_error_str(transId, true, "badarg"); + break; + } + send_pid_list(transId, children); + break; + } + case STDIN: { + // {stdin, OsPid::integer(), Data::binary()} + long pid; + std::string data; + if (arity != 3 || eis.decodeInt(pid) < 0 || eis.decodeBinary(data) < 0) { + send_error_str(transId, true, "badarg"); + break; + } + + MapChildrenT::iterator it = children.find(pid); + if (it == children.end()) { + if (debug) + fprintf(stderr, "Stdin (%ld bytes) cannot be sent to non-existing pid %ld\r\n", + data.size(), pid); + break; + } + it->second.stdin_queue.push_front(data); + process_pid_input(it->second); + break; + } + } + return 0; +} + +void initialize(int userid, bool use_alt_fds) +{ + // If we are root, switch to non-root user and set capabilities + // to be able to adjust niceness and run commands as other users. + if (getuid() == 0) { + superuser = true; + if (userid == 0) { + fprintf(stderr, "When running as root, \"-user User\" option must be provided!\r\n"); + exit(4); + } + + #ifdef HAVE_CAP + if (prctl(PR_SET_KEEPCAPS, 1) < 0) { + perror("Failed to call prctl to keep capabilities"); + exit(5); + } + #endif + + if ( + #ifdef HAVE_SETRESUID + setresuid(-1, userid, geteuid()) // glibc, FreeBSD, OpenBSD, HP-UX + #elif HAVE_SETREUID + setreuid(-1, userid) // MacOSX, NetBSD, AIX, IRIX, Solaris>=2.5, OSF/1, Cygwin + #else + #error setresuid(3) not supported! + #endif + < 0) { + perror("Failed to set userid"); + exit(6); + } + + struct passwd* pw; + if (debug && (pw = getpwuid(geteuid())) != NULL) + fprintf(stderr, "exec: running as: %s (euid=%d)\r\n", pw->pw_name, geteuid()); + + if (geteuid() == 0) { + fprintf(stderr, "exec: failed to set effective userid to a non-root user %s (uid=%d)\r\n", + pw ? pw->pw_name : "", geteuid()); + exit(7); + } + + #ifdef HAVE_CAP + cap_t cur; + if ((cur = cap_from_text("cap_setuid=eip cap_kill=eip cap_sys_nice=eip")) == 0) { + fprintf(stderr, "exec: failed to convert cap_setuid & cap_sys_nice from text"); + exit(8); + } + if (cap_set_proc(cur) < 0) { + fprintf(stderr, "exec: failed to set cap_setuid & cap_sys_nice"); + exit(9); + } + cap_free(cur); + + if (debug && (cur = cap_get_proc()) != NULL) { + fprintf(stderr, "exec: current capabilities: %s\r\n", cap_to_text(cur, NULL)); + cap_free(cur); + } + #else + if (debug) + fprintf(stderr, "exec: capability feature is not implemented for this plaform!\r\n"); + #endif + + if (!getenv("SHELL") || strncmp(getenv("SHELL"), "", 1) == 0) { + fprintf(stderr, "exec: SHELL variable is not set!\r\n"); + exit(10); + } + + } + + #if !defined(NO_SYSCONF) + max_fds = sysconf(_SC_OPEN_MAX); + #else + max_fds = OPEN_MAX; + #endif + if (max_fds < 1024) max_fds = 1024; + + dev_null = open(CS_DEV_NULL, O_RDWR); + + if (dev_null < 0) { + fprintf(stderr, "exec: cannot open %s: %s\r\n", CS_DEV_NULL, strerror(errno)); + exit(10); + } + + if (use_alt_fds) { + // TODO: when closing stdin/stdout we need to ensure that redirected + // streams in the forked children have FDs different from 0,1,2 or else + // wrong file handles get closed. Therefore for now just leave + // stdin/stdout open even when not needed + + //eis.close_handles(); // Close stdin, stdout + eis.set_handles(3, 4); + } +} + +int finalize() +{ + if (debug) fprintf(stderr, "Setting alarm to %d seconds\r\n", alarm_max_time); + alarm(alarm_max_time); // Die in <alarm_max_time> seconds if not done + + int old_terminated = terminated; + terminated = 0; + + kill(0, SIGTERM); // Kill all children in our process group + + TimeVal now(TimeVal::NOW); + TimeVal deadline(now, FINALIZE_DEADLINE_SEC, 0); + + while (children.size() > 0) { + sigsetjmp(jbuf, 1); + + now.now(); + if (children.size() > 0 || !exited_children.empty()) { + int term = 0; + check_children(now, term, pipe_valid); + } + + for(MapChildrenT::iterator it=children.begin(), end=children.end(); it != end; ++it) + stop_child(it->second, 0, now, false); + + for(MapKillPidT::iterator it=transient_pids.begin(), end=transient_pids.end(); it != end; ++it) { + erl_exec_kill(it->first, SIGKILL); + transient_pids.erase(it); + } + + if (children.size() == 0) + break; + + TimeVal timeout(TimeVal::NOW); + if (timeout < deadline) { + timeout = deadline - timeout; + + oktojump = 1; + while (select(0, (fd_set *)0, (fd_set *)0, (fd_set *)0, &timeout) < 0 && errno == EINTR); + oktojump = 0; + } + } + + if (debug) + fprintf(stderr, "Exiting (%d)\r\n", old_terminated); + + return old_terminated; +} + +static int getpty(int& fdmp, int& fdsp, ei::StringBuffer<128>& err) { + int fdm, fds; + int rc; + + fdm = posix_openpt(O_RDWR); + if (fdm < 0) { + err.write("error %d on posix_openpt: %s\n", errno, strerror(errno)); + return -1; + } + + rc = grantpt(fdm); + if (rc != 0) { + close(fdm); + err.write("error %d on grantpt: %s\n", errno, strerror(errno)); + return -1; + } + + rc = unlockpt(fdm); + if (rc != 0) { + close(fdm); + err.write("error %d on unlockpt: %s\n", errno, strerror(errno)); + return -1; + } + + fds = open(ptsname(fdm), O_RDWR); + + if (fds < 0) { + close(fdm); + err.write("error %d on open pty slave: %s\n", errno, strerror(errno)); + return -1; + } + + fdmp = fdm; + fdsp = fds; + + if (debug) + fprintf(stderr, " Opened PTY pair (master=%d, slave=%d)\r\n", + fdm, fds); + + return 0; +} + +pid_t start_child(CmdOptions& op, std::string& error) +{ + enum { RD = 0, WR = 1 }; + + int stream_fd[][2] = { + // ChildReadFD ChildWriteFD + { REDIRECT_NULL, REDIRECT_NONE }, + { REDIRECT_NONE, REDIRECT_NULL }, + { REDIRECT_NONE, REDIRECT_NULL } + }; + + ei::StringBuffer<128> err; + + // Optionally setup pseudoterminal + int fdm, fds; + + if (op.pty()) { + if (getpty(fdm, fds, err) < 0) { + error = err.c_str(); + return -1; + } + } + + // Optionally setup stdin/stdout/stderr redirect + for (int i=STDIN_FILENO; i <= STDERR_FILENO; i++) { + int crw = i==0 ? RD : WR; + int cfd = op.stream_fd(i); + int* sfd = stream_fd[i]; + + // Optionally setup stdout redirect + switch (cfd) { + case REDIRECT_CLOSE: + sfd[RD] = cfd; + sfd[WR] = cfd; + if (debug) + fprintf(stderr, " Closing %s\r\n", stream_name(i)); + break; + case REDIRECT_STDOUT: + case REDIRECT_STDERR: + sfd[crw] = cfd; + if (debug) + fprintf(stderr, " Redirecting [%s -> %s]\r\n", stream_name(i), + fd_type(cfd).c_str()); + break; + case REDIRECT_ERL: + if (op.pty()) { + if (i == STDIN_FILENO) { + sfd[RD] = fds; + sfd[WR] = fdm; + } else { + sfd[WR] = fds; + sfd[RD] = fdm; + } + if (debug) + fprintf(stderr, " Redirecting [%s -> pipe:{r=%d,w=%d}] (PTY)\r\n", + stream_name(i), sfd[0], sfd[1]); + } else if (open_pipe(sfd, stream_name(i), err) < 0) { + error = err.c_str(); + return -1; + } + break; + case REDIRECT_NULL: + sfd[crw] = dev_null; + if (debug) + fprintf(stderr, " Redirecting [%s -> null]\r\n", + stream_name(i)); + break; + case REDIRECT_FILE: { + sfd[crw] = open_file(op.stream_file(i), op.stream_append(i), + stream_name(i), err, op.stream_mode(i)); + if (sfd[crw] < 0) { + error = err.c_str(); + return -1; + } + break; + } + } + } + + if (debug) { + fprintf(stderr, "Starting child: '%s'\r\n" + " child = (stdin=%s, stdout=%s, stderr=%s)\r\n" + " parent = (stdin=%s, stdout=%s, stderr=%s)\r\n", + op.cmd().front().c_str(), + fd_type(stream_fd[STDIN_FILENO ][RD]).c_str(), + fd_type(stream_fd[STDOUT_FILENO][WR]).c_str(), + fd_type(stream_fd[STDERR_FILENO][WR]).c_str(), + fd_type(stream_fd[STDIN_FILENO ][WR]).c_str(), + fd_type(stream_fd[STDOUT_FILENO][RD]).c_str(), + fd_type(stream_fd[STDERR_FILENO][RD]).c_str() + ); + if (!op.executable().empty()) + fprintf(stderr, " Executable: %s\r\n", op.executable().c_str()); + if (op.cmd().size() > 0) { + int i = 0; + if (op.shell()) { + const char* s = getenv("SHELL"); + fprintf(stderr, " Args[%d]: %s\r\n", i++, s ? s : "(null)"); + fprintf(stderr, " Args[%d]: -c\r\n", i++); + } + typedef CmdArgsList::const_iterator const_iter; + for(const_iter it = op.cmd().begin(), end = op.cmd().end(); it != end; ++it) + fprintf(stderr, " Args[%d]: %s\r\n", i++, it->c_str()); + } + } + + pid_t pid = fork(); + + if (pid < 0) { + error = strerror(errno); + return pid; + } else if (pid == 0) { + // I am the child + + // Setup stdin/stdout/stderr redirect + for (int fd=STDIN_FILENO; fd <= STDERR_FILENO; fd++) { + int (&sfd)[2] = stream_fd[fd]; + int crw = fd==STDIN_FILENO ? RD : WR; + int prw = fd==STDIN_FILENO ? WR : RD; + + if (sfd[prw] >= 0) + close(sfd[prw]); // Close parent end of child pipes + + if (sfd[crw] == REDIRECT_CLOSE) + close(fd); + else if (sfd[crw] >= 0) { // Child end of the parent pipe + dup2(sfd[crw], fd); + // Don't close sfd[crw] here, since if the same fd is used for redirecting + // stdout and stdin (e.g. /dev/null) if won't work correctly. Instead the loop + // following this one will close all extra fds. + + //setlinebuf(stdout); // Set line buffering + } + } + + // See if we need to redirect STDOUT <-> STDERR + if (stream_fd[STDOUT_FILENO][WR] == REDIRECT_STDERR) + dup2(STDERR_FILENO, STDOUT_FILENO); + if (stream_fd[STDERR_FILENO][WR] == REDIRECT_STDOUT) + dup2(STDOUT_FILENO, STDERR_FILENO); + + for(int i=STDERR_FILENO+1; i < max_fds; i++) + close(i); + + if (op.pty()) { + struct termios ios; + tcgetattr(STDIN_FILENO, &ios); + // Disable the ECHO mode + ios.c_lflag &= ~(ECHO | ECHONL | ECHOE | ECHOK); + // We don't check if it succeeded because if the STDIN is not a terminal + // it won't be able to disable the ECHO anyway. + tcsetattr(STDIN_FILENO, TCSANOW, &ios); + + // Make the current process a new session leader + setsid(); + + // as a session leader, set the controlling terminal to be the + // slave side + ioctl(STDIN_FILENO, TIOCSCTTY, 1); + } + + #if !defined(__CYGWIN__) && !defined(__WIN32) + if (op.user() != INT_MAX && + #ifdef HAVE_SETRESUID + setresuid(op.user(), op.user(), op.user()) + #elif HAVE_SETREUID + setreuid(op.user(), op.user()) + #else + #error setresuid(3) not supported! + #endif + < 0) { + err.write("Cannot set effective user to %d", op.user()); + perror(err.c_str()); + exit(EXIT_FAILURE); + } + #endif + + if (op.group() != INT_MAX && setpgid(0, op.group()) < 0) { + err.write("Cannot set effective group to %d", op.group()); + perror(err.c_str()); + exit(EXIT_FAILURE); + } + + // Build the command arguments list + size_t sz = op.cmd().size() + 1 + (op.shell() ? 2 : 0); + const char** argv = new const char*[sz]; + const char** p = argv; + + if (op.shell()) { + *p++ = getenv("SHELL"); + *p++ = "-c"; + } + + for (CmdArgsList::const_iterator + it = op.cmd().begin(), end = op.cmd().end(); it != end; ++it) + *p++ = it->c_str(); + + *p++ = (char*)NULL; + + if (op.cd() != NULL && op.cd()[0] != '\0' && chdir(op.cd()) < 0) { + err.write("Cannot chdir to '%s'", op.cd()); + perror(err.c_str()); + exit(EXIT_FAILURE); + } + + // Setup process environment + if (op.init_cenv() < 0) { + perror(err.c_str()); + exit(EXIT_FAILURE); + } + + const char* executable = op.executable().empty() + ? (const char*)argv[0] : op.executable().c_str(); + + // Execute the process + if (execve(executable, (char* const*)argv, op.env()) < 0) { + err.write("Pid %d: cannot execute '%s'", getpid(), executable); + perror(err.c_str()); + exit(EXIT_FAILURE); + } + // On success execve never returns + exit(EXIT_FAILURE); + } + + // I am the parent + + if (debug > 1) + fprintf(stderr, "Spawned child pid %d\r\n", pid); + + // Either the parent or the child could use setpgid() to change + // the process group ID of the child. However, because the scheduling + // of the parent and child is indeterminate after a fork(), we can’t + // rely on the parent changing the child’s process group ID before the + // child does an exec(); nor can we rely on the child changing its + // process group ID before the parent tries to send any job-control + // signals to it (dependence on either one of these behaviors would + // result in a race condition). Therefore, here the parent and the + // child process both call setpgid() to change the child’s process + // group ID to the same value immediately after a fork(), and the + // parent ignores any occurrence of the EACCES error on the setpgid() call. + + if (op.group() != INT_MAX) { + pid_t gid = op.group() ? op.group() : pid; + if (setpgid(pid, gid) == -1 && errno != EACCES && debug) + fprintf(stderr, " Parent failed to set group of pid %d to %d: %s\r\n", + pid, gid, strerror(errno)); + else if (debug) + fprintf(stderr, " Set group of pid %d to %d\r\n", pid, gid); + } + + for (int i=STDIN_FILENO; i <= STDERR_FILENO; i++) { + int wr = i==STDIN_FILENO ? WR : RD; + int& cfd = op.stream_fd(i); + int* sfd = stream_fd[i]; + + int fd = sfd[i==0 ? RD : WR]; + if (fd >= 0 && fd != dev_null) { + if (debug) + fprintf(stderr, " Parent closing pid %d pipe %s end (fd=%d)\r\n", + pid, i==STDIN_FILENO ? "reading" : "writing", fd); + close(fd); // Close stdin/reading or stdout(err)/writing end of the child pipe + } + + if (sfd[wr] >= 0 && sfd[wr] != dev_null) { + cfd = sfd[wr]; + // Make sure the writing end is non-blocking + set_nonblock_flag(pid, cfd, true); + + if (debug) + fprintf(stderr, " Setup %s end of pid %d %s redirection (fd=%d%s)\r\n", + i==STDIN_FILENO ? "writing" : "reading", pid, stream_name(i), cfd, + (fcntl(cfd, F_GETFL, 0) & O_NONBLOCK) == O_NONBLOCK ? " [non-block]" : ""); + } + } + + set_nice(pid,op.nice(),error); + + return pid; +} + +int stop_child(CmdInfo& ci, int transId, const TimeVal& now, bool notify) +{ + bool use_kill = false; + + if (ci.sigkill) // Kill signal already sent + return 0; + else if (ci.kill_cmd_pid > 0 || ci.sigterm) { + // There was already an attempt to kill it. + if (ci.sigterm && now.diff(ci.deadline) > 0) { + // More than KILL_TIMEOUT_SEC secs elapsed since the last kill attempt + erl_exec_kill(ci.kill_group ? -ci.cmd_gid : ci.cmd_pid, SIGKILL); + if (ci.kill_cmd_pid > 0) + erl_exec_kill(ci.kill_cmd_pid, SIGKILL); + + ci.sigkill = true; + } + if (notify) send_ok(transId); + return 0; + } else if (!ci.kill_cmd.empty()) { + // This is the first attempt to kill this pid and kill command is provided. + CmdArgsList kill_cmd; + kill_cmd.push_front(ci.kill_cmd.c_str()); + CmdOptions co(kill_cmd); + std::string err; + ci.kill_cmd_pid = start_child(co, err); + if (!err.empty() && debug) + fprintf(stderr, "Error executing kill command '%s': %s\r\r", + ci.kill_cmd.c_str(), err.c_str()); + + if (ci.kill_cmd_pid > 0) { + transient_pids[ci.kill_cmd_pid] = ci.cmd_pid; + ci.deadline.set(now, ci.kill_timeout); + if (notify) send_ok(transId); + return 0; + } else { + if (notify) send_error_str(transId, false, "bad kill command - using SIGTERM"); + use_kill = true; + notify = false; + } + } else { + // This is the first attempt to kill this pid and no kill command is provided. + use_kill = true; + } + + if (use_kill) { + // Use SIGTERM / SIGKILL to nuke the pid + pid_t pid = ci.kill_group ? -ci.cmd_gid : ci.cmd_pid; + const char* spid = ci.kill_group ? "gid" : "pid"; + int n; + if (!ci.sigterm && (n = kill_child(pid, SIGTERM, transId, notify)) == 0) { + if (debug) + fprintf(stderr, "Sent SIGTERM to %s %d (timeout=%ds)\r\n", + spid, abs(pid), ci.kill_timeout); + ci.deadline.set(now, ci.kill_timeout); + } else if (!ci.sigkill && (n = kill_child(pid, SIGKILL, 0, false)) == 0) { + if (debug) + fprintf(stderr, "Sent SIGKILL to %s %d\r\n", spid, abs(pid)); + ci.deadline.clear(); + ci.sigkill = true; + } else { + n = 0; // FIXME + // Failed to send SIGTERM & SIGKILL to the process - give up + ci.deadline.clear(); + ci.sigkill = true; + if (debug) + fprintf(stderr, "Failed to kill %s %d - leaving a zombie\r\n", spid, abs(pid)); + MapChildrenT::iterator it = children.find(ci.cmd_pid); + if (it != children.end()) + erase_child(it); + } + ci.sigterm = true; + return n; + } + return 0; +} + +void stop_child(pid_t pid, int transId, const TimeVal& now) +{ + int n = 0; + + MapChildrenT::iterator it = children.find(pid); + if (it == children.end()) { + send_error_str(transId, false, "pid not alive"); + return; + } else if ((n = erl_exec_kill(pid, 0)) < 0) { + send_error_str(transId, false, "pid not alive (err: %d)", n); + return; + } + stop_child(it->second, transId, now); +} + +int send_std_error(int err, bool notify, int transId) +{ + if (err == 0) { + if (notify) send_ok(transId); + return 0; + } + + switch (errno) { + case EACCES: + if (notify) send_error_str(transId, true, "eacces"); + break; + case EINVAL: + if (notify) send_error_str(transId, true, "einval"); + break; + case ESRCH: + if (notify) send_error_str(transId, true, "esrch"); + break; + case EPERM: + if (notify) send_error_str(transId, true, "eperm"); + break; + default: + if (notify) send_error_str(transId, false, strerror(errno)); + break; + } + return err; +} + +int kill_child(pid_t pid, int signal, int transId, bool notify) +{ + // We can't use -pid here to kill the whole process group, because our process is + // the group leader. + int err = erl_exec_kill(pid, signal); + switch (err) { + case EINVAL: + if (notify) send_error_str(transId, false, "Invalid signal: %d", signal); + break; + default: + send_std_error(err, notify, transId); + break; + } + return err; +} + +bool process_pid_input(CmdInfo& ci) +{ + int& fd = ci.stream_fd[STDIN_FILENO]; + + if (fd < 0) return true; + + while (!ci.stdin_queue.empty()) { + std::string& s = ci.stdin_queue.back(); + + const void* p = s.c_str() + ci.stdin_wr_pos; + int n, len = s.size() - ci.stdin_wr_pos; + + while ((n = write(fd, p, len)) < 0 && errno == EINTR); + + if (debug) { + if (n < 0) + fprintf(stderr, "Error writing %d bytes to stdin (fd=%d) of pid %d: %s\r\n", + len, fd, ci.cmd_pid, strerror(errno)); + else + fprintf(stderr, "Wrote %d/%d bytes to stdin (fd=%d) of pid %d\r\n", + n, len, fd, ci.cmd_pid); + } + + if (n > 0 && n < len) { + ci.stdin_wr_pos += n; + return false; + } else if (n < 0 && errno == EAGAIN) { + break; + } else if (n <= 0) { + if (debug) + fprintf(stderr, "Eof writing pid %d's stdin, closing fd=%d: %s\r\n", + ci.cmd_pid, fd, strerror(errno)); + ci.stdin_wr_pos = 0; + close(fd); + fd = REDIRECT_CLOSE; + ci.stdin_queue.clear(); + return true; + } + + ci.stdin_queue.pop_back(); + ci.stdin_wr_pos = 0; + } + + return true; +} + +void process_pid_output(CmdInfo& ci, int maxsize) +{ + char buf[4096]; + bool dead = false; + + for (int i=STDOUT_FILENO; i <= STDERR_FILENO; i++) { + int& fd = ci.stream_fd[i]; + + if (fd >= 0) { + for(int got = 0, n = sizeof(buf); got < maxsize && n == sizeof(buf); got += n) { + while ((n = read(fd, buf, sizeof(buf))) < 0 && errno == EINTR); + if (debug > 1) + fprintf(stderr, "Read %d bytes from pid %d's %s (fd=%d): %s\r\n", + n, ci.cmd_pid, stream_name(i), fd, n > 0 ? "ok" : strerror(errno)); + if (n > 0) { + send_ospid_output(ci.cmd_pid, stream_name(i), buf, n); + if (n < (int)sizeof(buf)) + break; + } else if (n < 0 && errno == EAGAIN) + break; + else if (n <= 0) { + if (debug) + fprintf(stderr, "Eof reading pid %d's %s, closing fd=%d: %s\r\n", + ci.cmd_pid, stream_name(i), fd, strerror(errno)); + close(fd); + fd = REDIRECT_CLOSE; + dead = true; + break; + } + } + } + } + + if (dead) + check_child(ci.cmd_pid); +} + +void erase_child(MapChildrenT::iterator& it) +{ + for (int i=STDIN_FILENO; i<=STDERR_FILENO; i++) + if (it->second.stream_fd[i] >= 0) { + if (debug) + fprintf(stderr, "Closing pid %d's %s\r\n", it->first, stream_name(i)); + close(it->second.stream_fd[i]); + } + + children.erase(it); +} + +int check_children(const TimeVal& now, int& isTerminated, bool notify) +{ + if (debug > 2) + fprintf(stderr, "Checking %ld running children (exited count=%ld)\r\n", + children.size(), exited_children.size()); + + for (MapChildrenT::iterator it=children.begin(), end=children.end(); it != end; ++it) { + int status = ECHILD; + pid_t pid = it->first; + int n = erl_exec_kill(pid, 0); + + if (n == 0) { // process is alive + /* If a deadline has been set, and we're over it, wack it. */ + if (!it->second.deadline.zero() && it->second.deadline.diff(now) <= 0) { + stop_child(it->second, 0, now, false); + it->second.deadline.clear(); + } + + while ((n = waitpid(pid, &status, WNOHANG)) < 0 && errno == EINTR); + + if (n > 0) { + if (WIFEXITED(status) || WIFSIGNALED(status)) { + add_exited_child(pid <= 0 ? n : pid, status); + } else if (WIFSTOPPED(status)) { + if (debug) + fprintf(stderr, "Pid %d %swas stopped by delivery of a signal %d\r\n", + pid, it->second.managed ? "(managed) " : "", WSTOPSIG(status)); + } else if (WIFCONTINUED(status)) { + if (debug) + fprintf(stderr, "Pid %d %swas resumed by delivery of SIGCONT\r\n", + pid, it->second.managed ? "(managed) " : ""); + } + } + } else if (n < 0 && errno == ESRCH) { + add_exited_child(pid, -1); + } + } + + if (debug > 2) + fprintf(stderr, "Checking %ld exited children (notify=%d)\r\n", + exited_children.size(), notify); + + // For each process info in the <exited_children> queue deliver it to the Erlang VM + // and remove it from the managed <children> map. + for (ExitedChildrenT::iterator it=exited_children.begin(); !isTerminated && it!=exited_children.end();) + { + MapChildrenT::iterator i = children.find(it->first); + MapKillPidT::iterator j; + + if (i != children.end()) { + process_pid_output(i->second, INT_MAX); + // Override status code if termination was requested by Erlang + PidStatusT ps(it->first, + i->second.sigterm + ? 0 // Override status code if termination was requested by Erlang + : i->second.success_code && !it->second + ? i->second.success_code // Override success status code + : it->second); + // The process exited and it requires to kill all other processes in the group + if (i->second.kill_group && i->second.cmd_gid != INT_MAX && i->second.cmd_gid) + erl_exec_kill(-(i->second.cmd_gid), SIGTERM); // Kill all children in this group + + if (notify && send_pid_status_term(ps) < 0) { + isTerminated = 1; + return -1; + } + erase_child(i); + } else if ((j = transient_pids.find(it->first)) != transient_pids.end()) { + // the pid is one of the custom 'kill' commands started by us. + transient_pids.erase(j); + } + + exited_children.erase(it++); + } + + return 0; +} + +int send_pid_list(int transId, const MapChildrenT& children) +{ + // Reply: {TransId, [OsPid::integer()]} + eis.reset(); + eis.encodeTupleSize(2); + eis.encode(transId); + eis.encodeListSize(children.size()); + for(MapChildrenT::const_iterator it=children.begin(), end=children.end(); it != end; ++it) + eis.encode(it->first); + eis.encodeListEnd(); + return eis.write(); +} + +int send_error_str(int transId, bool asAtom, const char* fmt, ...) +{ + char str[MAXATOMLEN]; + va_list vargs; + va_start (vargs, fmt); + vsnprintf(str, sizeof(str), fmt, vargs); + va_end (vargs); + + eis.reset(); + eis.encodeTupleSize(2); + eis.encode(transId); + eis.encodeTupleSize(2); + eis.encode(atom_t("error")); + (asAtom) ? eis.encode(atom_t(str)) : eis.encode(str); + return eis.write(); +} + +int send_ok(int transId, pid_t pid) +{ + eis.reset(); + eis.encodeTupleSize(2); + eis.encode(transId); + if (pid < 0) + eis.encode(atom_t("ok")); + else { + eis.encodeTupleSize(2); + eis.encode(atom_t("ok")); + eis.encode(pid); + } + return eis.write(); +} + +int send_pid_status_term(const PidStatusT& stat) +{ + eis.reset(); + eis.encodeTupleSize(2); + eis.encode(0); + eis.encodeTupleSize(3); + eis.encode(atom_t("exit_status")); + eis.encode(stat.first); + eis.encode(stat.second); + return eis.write(); +} + +int send_ospid_output(int pid, const char* type, const char* data, int len) +{ + eis.reset(); + eis.encodeTupleSize(2); + eis.encode(0); + eis.encodeTupleSize(3); + eis.encode(atom_t(type)); + eis.encode(pid); + eis.encode(data, len); + return eis.write(); +} + +int open_file(const char* file, bool append, const char* stream, + ei::StringBuffer<128>& err, int mode) +{ + int flags = O_RDWR | O_CREAT | (append ? O_APPEND : O_TRUNC); + int fd = open(file, flags, mode); + if (fd < 0) { + err.write("Failed to redirect %s to file: %s", stream, strerror(errno)); + return -1; + } + if (debug) + fprintf(stderr, " Redirecting [%s -> {file:%s, fd:%d}]\r\n", + stream, file, fd); + + return fd; +} + +int open_pipe(int fds[2], const char* stream, ei::StringBuffer<128>& err) +{ + if (pipe(fds) < 0) { + err.write("Failed to create a pipe for %s: %s", stream, strerror(errno)); + return -1; + } + if (fds[1] > max_fds) { + close(fds[0]); + close(fds[1]); + err.write("Exceeded number of available file descriptors (fd=%d)", fds[1]); + return -1; + } + if (debug) + fprintf(stderr, " Redirecting [%s -> pipe:{r=%d,w=%d}]\r\n", stream, fds[0], fds[1]); + + return 0; +} + +/* This exists just to make sure that we don't inadvertently do a + * kill(-1, SIGKILL), which will cause all kinds of bad things to + * happen. */ + +int erl_exec_kill(pid_t pid, int signal) { + if (pid == -1 || pid == 0) { + if (debug) + fprintf(stderr, "kill(%d, %d) attempt prohibited!\r\n", pid, signal); + return -1; + } + + int r = kill(pid, signal); + + if (debug && signal > 0) + fprintf(stderr, "Called kill(pid=%d, sig=%d) -> %d\r\n", pid, signal, r); + + return r; +} + +int set_nonblock_flag(pid_t pid, int fd, bool value) +{ + int oldflags = fcntl(fd, F_GETFL, 0); + if (oldflags < 0) + return oldflags; + if (value != 0) + oldflags |= O_NONBLOCK; + else + oldflags &= ~O_NONBLOCK; + + int ret = fcntl(fd, F_SETFL, oldflags); + if (debug > 3) { + oldflags = fcntl(fd, F_GETFL, 0); + fprintf(stderr, " Set pid %d's fd=%d to non-blocking mode (flags=%x)\r\n", + pid, fd, oldflags); + } + + return ret; +} + +int CmdOptions::ei_decode(ei::Serializer& ei, bool getCmd) +{ + // {Cmd::string(), [Option]} + // Option = {env, Strings} | {cd, Dir} | {kill, Cmd} + int sz; + std::string op, val; + + m_err.str(""); + m_cmd.clear(); + m_kill_cmd.clear(); + m_env.clear(); + + m_nice = INT_MAX; + + if (getCmd) { + std::string s; + + if (eis.decodeString(s) != -1) { + m_cmd.push_front(s); + m_shell=true; + } else if ((sz = eis.decodeListSize()) > 0) { + for (int i=0; i < sz; i++) { + if (eis.decodeString(s) < 0) { + m_err << "badarg: invalid command argument #" << i; + return -1; + } + m_cmd.push_back(s); + } + eis.decodeListEnd(); + m_shell = false; + } else { + m_err << "badarg: cmd string or non-empty list is expected"; + return -1; + } + } + + if ((sz = eis.decodeListSize()) < 0) { + m_err << "option list expected"; + return -1; + } + + // Note: The STDIN, STDOUT, STDERR enums must occupy positions 0, 1, 2!!! + enum OptionT { + STDIN, STDOUT, STDERR, + PTY, SUCCESS_EXIT_CODE, CD, ENV, + EXECUTABLE, KILL, KILL_TIMEOUT, + KILL_GROUP, NICE, USER, GROUP + } opt; + const char* opts[] = { + "stdin", "stdout", "stderr", + "pty", "success_exit_code", "cd", "env", + "executable", "kill", "kill_timeout", + "kill_group", "nice", "user", "group" + }; + + bool seen_opt[sizeof(opts) / sizeof(char*)] = {false}; + + for(int i=0; i < sz; i++) { + int arity, type = eis.decodeType(arity); + + if (type == etAtom && (int)(opt = (OptionT)eis.decodeAtomIndex(opts, op)) >= 0) + arity = 1; + else if (type != etTuple || ((arity = eis.decodeTupleSize()) != 2 && arity != 3)) { + m_err << "badarg: option must be {Cmd, Opt} or {Cmd, Opt, Args} or atom " + "(got tp=" << (char)type << ", arity=" << arity << ')'; + return -1; + } else if ((int)(opt = (OptionT)eis.decodeAtomIndex(opts, op)) < 0) { + m_err << "badarg: invalid cmd option tuple"; + return -1; + } + + if (seen_opt[opt]) { + m_err << "duplicate " << op << " option specified"; + return -1; + } + seen_opt[opt] = true; + + switch (opt) { + case EXECUTABLE: + if (eis.decodeString(m_executable) < 0) { + m_err << op << " - bad option value"; return -1; + } + break; + + case CD: + // {cd, Dir::string()} + if (eis.decodeString(m_cd) < 0) { + m_err << op << " - bad option value"; return -1; + } + break; + + case KILL: + // {kill, Cmd::string()} + if (eis.decodeString(m_kill_cmd) < 0) { + m_err << op << " - bad option value"; return -1; + } + break; + + case GROUP: { + // {group, integer() | string()} + type = eis.decodeType(arity); + if (type == etString) { + if (eis.decodeString(val) < 0) { + m_err << op << " - bad group value"; return -1; + } + struct group g; + char buf[1024]; + struct group* res; + if (getgrnam_r(val.c_str(), &g, buf, sizeof(buf), &res) < 0) { + m_err << op << " - invalid group name: " << val; + return -1; + } + m_group = g.gr_gid; + } else if (eis.decodeInt(m_group) < 0) { + m_err << op << " - bad group value type (expected int or string)"; + return -1; + } + break; + } + case USER: + // {user, Dir::string()} | {kill, Cmd::string()} + if (eis.decodeString(val) < 0) { + m_err << op << " - bad option value"; return -1; + } + if (opt == CD) m_cd = val; + else if (opt == KILL) m_kill_cmd = val; + else if (opt == USER) { + struct passwd *pw = getpwnam(val.c_str()); + if (pw == NULL) { + m_err << "Invalid user " << val << ": " << ::strerror(errno); + return -1; + } + m_user = pw->pw_uid; + } + break; + + case KILL_TIMEOUT: + // {kill_timeout, Timeout::int()} + if (eis.decodeInt(m_kill_timeout) < 0) { + m_err << op << " - invalid value"; + return -1; + } + break; + + case KILL_GROUP: + m_kill_group = true; + break; + + case NICE: + // {nice, Level::int()} + if (eis.decodeInt(m_nice) < 0 || m_nice < -20 || m_nice > 20) { + m_err << "nice option must be an integer between -20 and 20"; + return -1; + } + break; + + case ENV: { + // {env, [NameEqualsValue::string()]} + // passed in env variables are appended to the existing ones + // obtained from environ global var + int opt_env_sz = eis.decodeListSize(); + if (opt_env_sz < 0) { + m_err << op << " - list expected"; + return -1; + } + + for (int i=0; i < opt_env_sz; i++) { + int sz, type = eis.decodeType(sz); + bool res = false; + std::string s, key; + + if (type == ERL_STRING_EXT) { + res = !eis.decodeString(s); + if (res) { + size_t pos = s.find_first_of('='); + if (pos == std::string::npos) + res = false; + else + key = s.substr(0, pos); + } + } else if (type == ERL_SMALL_TUPLE_EXT && sz == 2) { + eis.decodeTupleSize(); + std::string s2; + if (eis.decodeString(key) == 0 && eis.decodeString(s2) == 0) { + res = true; + s = key + "=" + s2; + } + } + + if (!res) { + m_err << op << " - invalid argument #" << i; + return -1; + } + m_env[key] = s; + } + eis.decodeListEnd(); + break; + } + + case PTY: + m_pty = true; + break; + + case SUCCESS_EXIT_CODE: + if (eis.decodeInt(m_success_exit_code) < 0 || + m_success_exit_code < 0 || + m_success_exit_code > 255) + { + m_err << "success exit code must be an integer between 0 and 255"; + return -1; + } + break; + + case STDIN: + case STDOUT: + case STDERR: { + int& fdr = stream_fd(opt); + + if (arity == 1) + stream_redirect(opt, REDIRECT_ERL); + else if (arity == 2) { + int type = 0, sz; + std::string s, fop; + type = eis.decodeType(sz); + + if (type == ERL_ATOM_EXT) + eis.decodeAtom(s); + else if (type == ERL_STRING_EXT) + eis.decodeString(s); + else { + m_err << op << " - atom or string value in tuple required"; + return -1; + } + + if (s == "null") { + stream_null(opt); + fdr = REDIRECT_NULL; + } else if (s == "close") { + stream_redirect(opt, REDIRECT_CLOSE); + } else if (s == "stderr" && opt == STDOUT) + stream_redirect(opt, REDIRECT_STDERR); + else if (s == "stdout" && opt == STDERR) + stream_redirect(opt, REDIRECT_STDOUT); + else if (!s.empty()) { + stream_file(opt, s); + } + } else if (arity == 3) { + int n, sz, mode = DEF_MODE; + bool append = false; + std::string s, a, fop; + if (eis.decodeString(s) < 0) { + m_err << "filename must be a string for option " << op; + return -1; + } + if ((n = eis.decodeListSize()) < 0) { + m_err << "option " << op << " requires a list of file options" << op; + return -1; + } + for(int i=0; i < n; i++) { + int tp = eis.decodeType(sz); + if (eis.decodeAtom(a) >= 0) { + if (a == "append") + append = true; + else { + m_err << "option " << op << ": unsupported file option '" << a << "'"; + return -1; + } + } + else if (tp != etTuple || eis.decodeTupleSize() != 2 || + eis.decodeAtom(a) < 0 || a != "mode" || eis.decodeInt(mode) < 0) { + m_err << "option " << op << ": unsupported file option '" << a << "'"; + return -1; + + } + } + eis.decodeListEnd(); + + stream_file(opt, s, append, mode); + } + + if (opt == STDIN && + !(fdr == REDIRECT_NONE || fdr == REDIRECT_ERL || + fdr == REDIRECT_CLOSE || fdr == REDIRECT_NULL || fdr == REDIRECT_FILE)) { + m_err << "invalid " << op << " redirection option: '" << op << "'"; + return -1; + } + break; + } + default: + m_err << "bad option: " << op; return -1; + } + } + + eis.decodeListEnd(); + + for (int i=STDOUT_FILENO; i <= STDERR_FILENO; i++) + if (stream_fd(i) == (i == STDOUT_FILENO ? REDIRECT_STDOUT : REDIRECT_STDERR)) { + m_err << "self-reference of " << stream_fd_type(i); + return -1; + } + + if (stream_fd(STDOUT_FILENO) == REDIRECT_STDERR && + stream_fd(STDERR_FILENO) == REDIRECT_STDOUT) + { + m_err << "circular reference of stdout and stderr"; + return -1; + } + + //if (cmd_is_list && m_shell) + // m_shell = false; + + if (debug > 1) { + fprintf(stderr, "Parsed cmd '%s' options\r\n (stdin=%s, stdout=%s, stderr=%s)\r\n", + m_cmd.front().c_str(), + stream_fd_type(0).c_str(), stream_fd_type(1).c_str(), stream_fd_type(2).c_str()); + } + + return 0; +} + +int CmdOptions::init_cenv() +{ + if (m_env.empty()) { + m_cenv = (const char**)environ; + return 0; + } + + // Copy environment of the caller process + for (char **env_ptr = environ; *env_ptr; env_ptr++) { + std::string s(*env_ptr), key(s.substr(0, s.find_first_of('='))); + MapEnvIterator it = m_env.find(key); + if (it == m_env.end()) + m_env[key] = s; + } + + if ((m_cenv = (const char**) new char* [m_env.size()+1]) == NULL) { + m_err << "Cannot allocate memory for " << m_env.size()+1 << " environment entries"; + return -1; + } + + int i = 0; + for (MapEnvIterator it = m_env.begin(), end = m_env.end(); it != end; ++it, ++i) + m_cenv[i] = it->second.c_str(); + m_cenv[i] = NULL; + + return 0; +} diff --git a/deps/exec/include/exec.hrl b/deps/exec/include/exec.hrl new file mode 100644 index 0000000..0ee625f --- /dev/null +++ b/deps/exec/include/exec.hrl @@ -0,0 +1,8 @@ +-define(SIGHUP, -1). +-define(SIGINT, -2). +-define(SIGKILL, -9). +-define(SIGTERM, -15). +-define(SIGUSR1, -10). +-define(SIGUSR2, -12). + +-define(FMT(Fmt, Args), lists:flatten(io_lib:format(Fmt, Args))). diff --git a/deps/exec/priv/x86_64-pc-linux-gnu/exec-port b/deps/exec/priv/x86_64-pc-linux-gnu/exec-port Binary files differnew file mode 100755 index 0000000..555048c --- /dev/null +++ b/deps/exec/priv/x86_64-pc-linux-gnu/exec-port diff --git a/deps/exec/rebar.config b/deps/exec/rebar.config new file mode 100644 index 0000000..5db9612 --- /dev/null +++ b/deps/exec/rebar.config @@ -0,0 +1,6 @@ +{erl_opts, [ + warnings_as_errors, + warn_export_all +]}. + +{pre_hooks, [{clean, "rm -fr ebin priv erl_crash.dump"}]}. diff --git a/deps/exec/rebar.config.script b/deps/exec/rebar.config.script new file mode 100644 index 0000000..4c7fc65 --- /dev/null +++ b/deps/exec/rebar.config.script @@ -0,0 +1,44 @@ +Arch = erlang:system_info(system_architecture), +Vsn = string:strip(os:cmd("git describe --always --tags --abbrev=0 | sed 's/^v//'"), right, $\n), +%% Check for Linux capability API (Install package: libcap-devel). +{LinCXX, LinLD} = + case file:read_file_info("/usr/include/sys/capability.h") of + {ok, _} -> + io:put_chars("INFO: Detected support of linux capabilities.\n"), + {" -DHAVE_CAP", " -lcap"}; + _ -> + {"", ""} + end, + +X64 = case Arch of + "x86_64" ++ _E -> " -m64"; + _ -> "" + end, + +% Replace configuration options read from rebar.config with those dynamically set below +lists:keymerge(1, + lists:keysort(1, [ + {port_env, [{"solaris", "CXXFLAGS", "$CXXFLAGS -DHAVE_SETREUID -DHAVE_PTRACE" ++ X64}, + {"solaris", "LDFLAGS", "$LDFLAGS -lrt" ++ X64}, + + {"darwin", "CXXFLAGS", "$CXXFLAGS -DHAVE_SETREUID -DHAVE_PTRACE" ++ X64}, + {"darwin", "LDFLAGS", "$LDFLAGS" ++ X64}, + + {"linux", "CXXFLAGS", "$CXXFLAGS -DHAVE_SETRESUID -DHAVE_PTRACE" ++ LinCXX}, + {"linux", "LDFLAGS", "$LDFLAGS" ++ LinLD}, + + {"CC", "g++"}, + {"CXX", "g++"}, + {"CXXFLAGS", "$CXXFLAGS -O0"} + ]}, + + {port_specs,[{filename:join(["priv", Arch, "exec-port"]), ["c_src/*.cpp"]}]}, + {edoc_opts, [{overview, "src/overview.edoc"}, + {title, "The exec application"}, + {includes, ["include"]}, + {def, {vsn, Vsn}}, + {stylesheet_file, "src/edoc.css"}, + {app_default, "http://www.erlang.org/doc/man"}]} + ]), + lists:keysort(1, CONFIG)). + diff --git a/deps/exec/src/edoc.css b/deps/exec/src/edoc.css new file mode 100644 index 0000000..b98c948 --- /dev/null +++ b/deps/exec/src/edoc.css @@ -0,0 +1,144 @@ +/* Baseline rhythm */ +body { + font-size: 16px; + font-family: Helvetica, sans-serif; + margin: 8px; +} + +p { + font-size: 1em; /* 16px */ + line-height: 1.5em; /* 24px */ + margin: 0 0 1.5em 0; +} + +h1 { + font-size: 1.5em; /* 24px */ + line-height: 1em; /* 24px */ + margin-top: 1em; + margin-bottom: 0em; +} + +h2 { + font-size: 1.375em; /* 22px */ + line-height: 1.0909em; /* 24px */ + margin-top: 1.0909em; + margin-bottom: 0em; +} + +h3 { + font-size: 1.25em; /* 20px */ + line-height: 1.2em; /* 24px */ + margin-top: 1.2em; + margin-bottom: 0em; +} + +h4 { + font-size: 1.125em; /* 18px */ + line-height: 1.3333em; /* 24px */ + margin-top: 1.3333em; + margin-bottom: 0em; +} + +.class-for-16px { + font-size: 1em; /* 16px */ + line-height: 1.5em; /* 24px */ + margin-top: 1.5em; + margin-bottom: 0em; +} + +.class-for-14px { + font-size: 0.875em; /* 14px */ + line-height: 1.7143em; /* 24px */ + margin-top: 1.7143em; + margin-bottom: 0em; +} + +ul { + margin: 0 0 1.5em 0; + list-style-position: outside; +} + +/* Customizations */ +body { + color: #333; +} + +tt, code, pre { font-size: 0.95em } + +pre { + font-size: 0.875em; /* 14px */ + margin: 0 1em 1.7143em; + padding: 0 1em; + background: #E0F0FF; +} + +.navbar img, hr { + display: none; +} + +.navbar { + background-color: #85C2FF; +} + +table { + border-collapse: collapse; +} + +h1 { + border-left: 0.5em solid #85C2FF; + padding-left: 0.5em; + background-color: #85C2FF; +} + +h2.indextitle { + font-size: 1.25em; /* 20px */ + line-height: 1.2em; /* 24px */ + margin: -8px -8px 0.6em; + background-color: #85C2FF; + padding: 0.3em; +} + +ul.index { + list-style: none; + margin-left: 0em; + padding-left: 0; +} + +ul.index li { + display: inline; + padding-right: 0.75em +} + +ul.definitions { + background-color: #E0F0FF; +} + +dt { + font-weight: 600; +} + +div.spec p { + margin-bottom: 0; + padding-left: 1.25em; + background-color: #E0F0FF; +} + +h3.function { + border-left: 0.5em solid #85C2FF; + padding-left: 0.5em; + background-color: #BECFE0; +} +a, a:visited, a:hover, a:active { color: #06C } +h2 a, h3 a { color: #333 } + +h3.typedecl { + background-color: #BECFE0; +} + +i { + font-size: 0.875em; /* 14px */ + line-height: 1.7143em; /* 24px */ + margin-top: 1.7143em; + margin-bottom: 0em; + font-style: normal; +} diff --git a/deps/exec/src/exec.app.src b/deps/exec/src/exec.app.src new file mode 100644 index 0000000..b6a178e --- /dev/null +++ b/deps/exec/src/exec.app.src @@ -0,0 +1,16 @@ +{application, exec, + [ + {description, "OS Process Manager"}, + {vsn, git}, + {id, "exec"}, + {modules, []}, + {registered, [ exec ] }, + %% NOTE: do not list applications which are load-only! + {applications, [ kernel, stdlib ] }, + %% + %% mod: Specify the module name to start the application, plus args + %% + {mod, {exec_app, []}}, + {env, []} + ] +}. diff --git a/deps/exec/src/exec.erl b/deps/exec/src/exec.erl new file mode 100644 index 0000000..6ac17e5 --- /dev/null +++ b/deps/exec/src/exec.erl @@ -0,0 +1,1130 @@ +%%%------------------------------------------------------------------------ +%%% File: $Id$ +%%%------------------------------------------------------------------------ +%%% @doc OS shell command runner. +%%% It communicates with a separate C++ port process `exec-port' +%%% spawned by this module, which is responsible +%%% for starting, killing, listing, terminating, and notifying of +%%% state changes. +%%% +%%% The port program serves as a middle-man between +%%% the OS and the virtual machine to carry out OS-specific low-level +%%% process control. The Erlang/C++ protocol is described in the +%%% `exec.cpp' file. On platforms/environments which permit +%%% setting the suid bit on the `exec-port' executable, it can +%%% run external tasks by impersonating a different user. When +%%% suid bit is on, the application requires `exec:start_link/2' +%%% to be given the `{user, User}' option so that `exec-port' +%%% will not run as root. Before changing the effective `User', +%%% it sets the kernel capabilities so that it's able to start +%%% processes as other users and adjust process priorities. +%%% +%%% At exit the port program makes its best effort to perform +%%% clean shutdown of all child OS processes. +%%% Every started OS process is linked to a spawned light-weight +%%% Erlang process returned by the run/2, run_link/2 command. +%%% The application ensures that termination of spawned OsPid +%%% leads to termination of the associated Erlang Pid, and vice +%%% versa. +%%% +%%% @author Serge Aleynikov <saleyn@gmail.com> +%%% @version {@vsn} +%%% @end +%%%------------------------------------------------------------------------ +%%% Created: 2003-06-10 by Serge Aleynikov <saleyn@gmail.com> +%%% $Header$ +%%%------------------------------------------------------------------------ +-module(exec). +-author('saleyn@gmail.com'). + +-behaviour(gen_server). + +%% External exports +-export([ + start/0, start/1, start_link/1, run/2, run_link/2, manage/2, send/2, + which_children/0, kill/2, setpgid/2, stop/1, stop_and_wait/2, + ospid/1, pid/1, status/1, signal/1 +]). + +%% Internal exports +-export([default/0, default/1]). + +%% gen_server callbacks +-export([init/1, handle_call/3, handle_cast/2, handle_info/2, + code_change/3, terminate/2]). + +-include("exec.hrl"). + +-ifdef(TEST). +-include_lib("eunit/include/eunit.hrl"). +-endif. + +-record(state, { + port, + last_trans = 0, % Last transaction number sent to port + trans = queue:new(), % Queue of outstanding transactions sent to port + limit_users = [], % Restricted list of users allowed to run commands + registry, % Pids to notify when an OsPid exits + debug = false +}). + +-type exec_options() :: [exec_option()]. +-type exec_option() :: + debug + | {debug, integer()} + | verbose + | {args, [string(), ...]} + | {alarm, non_neg_integer()} + | {user, string()} + | {limit_users, [string(), ...]} + | {portexe, string()} + | {env, [{string(), string()}, ...]}. +%% Options passed to the exec process at startup. +%% <dl> +%% <dt>debug</dt><dd>Same as {debug, 1}</dd> +%% <dt>{debug, Level}</dt><dd>Enable port-programs debug trace at `Level'.</dd> +%% <dt>verbose</dt><dd>Enable verbose prints of the Erlang process.</dd> +%% <dt>{args, Args}</dt><dd>Append `Args' to the port command.</dd> +%% <dt>{alarm, Secs}</dt> +%% <dd>Give `Secs' deadline for the port program to clean up +%% child pids before exiting</dd> +%% <dt>{user, User}</dt> +%% <dd>When the port program was compiled with capability (Linux) +%% support enabled, and is owned by root with a a suid bit set, +%% this option must be specified so that upon startup the port +%% program is running under the effective user different from root. +%% This is a security measure that will also prevent the port program +%% to execute root commands.</dd> +%% <dt>{limit_users, LimitUsers}</dt> +%% <dd>Limit execution of external commands to these set of users. +%% This option is only valid when the port program is owned +%% by root.</dd> +%% <dt>{portexe, Exe}</dt> +%% <dd>Provide an alternative location of the port program. +%% This option is useful when this application is stored +%% on NFS and the port program needs to be copied locally +%% so that root suid bit can be set.</dd> +%% <dt>{env, Env}</dt> +%% <dd>Extend environment of the port program by using `Env' specification. +%% `Env' should be a list of tuples `{Name, Val}', where Name is the +%% name of an environment variable, and Val is the value it is to have +%% in the spawned port process.</dd> +%% </dl>. + +-type cmd() :: string() | [string()]. +%% Command to be executed. If specified as a string, the specified command +%% will be executed through the shell. The current shell is obtained +%% from environtment variable `SHELL'. This can be useful if you +%% are using Erlang primarily for the enhanced control flow it +%% offers over most system shells and still want convenient +%% access to other shell features such as shell pipes, filename +%% wildcards, environment variable expansion, and expansion of +%% `~' to a user's home directory. All command arguments must +%% be properly escaped including whitespace and shell +%% metacharacters. +%% +%% <ul> +%% <b><u>Warning:</u></b> Executing shell commands that +%% incorporate unsanitized input from an untrusted source makes +%% a program vulnerable to +%% [http://en.wikipedia.org/wiki/Shell_injection#Shell_injection shell injection], +%% a serious security flaw which can result in arbitrary command +%% execution. For this reason, the use of `shell' is strongly +%% discouraged in cases where the command string is constructed +%% from external input: +%% </ul> +%% +%% ``` +%% 1> {ok, Filename} = io:read("Enter filename: "). +%% Enter filename: "non_existent; rm -rf / #". +%% {ok, "non_existent; rm -rf / #"} +%% 2> exec(Filename, []) % Argh!!! This is not good! +%% ''' +%% +%% When command is given in the form of a list of strings, +%% it is passed to `execve(3)' library call directly without +%% involving the shell process, so the list of strings +%% represents the program to be executed with arguments. +%% In this case all shell-based features are disabled +%% and there's no shell injection vulnerability. + +-type cmd_options() :: [cmd_option()]. +-type cmd_option() :: + monitor + | sync + | {executable, string()} + | {cd, WorkDir::string()} + | {env, [string() | {Name :: string(), Value :: string()}, ...]} + | {kill, KillCmd::string()} + | {kill_timeout, Sec::non_neg_integer()} + | kill_group + | {group, GID :: string() | integer()} + | {user, RunAsUser :: string()} + | {nice, Priority :: integer()} + | {success_exit_code, ExitCode :: integer() } + | stdin | {stdin, null | close | string()} + | stdout | stderr + | {stdout, stderr | output_dev_opt()} + | {stderr, stdout | output_dev_opt()} + | {stdout | stderr, string(), [output_file_opt()]} + | pty. +%% Command options: +%% <dl> +%% <dt>monitor</dt><dd>Set up a monitor for the spawned process</dd> +%% <dt>sync</dt><dd>Block the caller until the OS command exits</dd> +%% <dt>{executable, Executable::string()}</dt> +%% <dd>Specifies a replacement program to execute. It is very seldomly +%% needed. When the port program executes a child process using +%% `execve(3)' call, the call takes the following arguments: +%% `(Executable, Args, Env)'. When `Cmd' argument passed to the +%% `run/2' function is specified as the list of strings, +%% the executable replaces the first paramter in the call, and +%% the original args provided in the `Cmd' parameter are passed as +%% as the second parameter. Most programs treat the program +%% specified by args as the command name, which can then be different +%% from the program actually executed. On Unix, the args name becomes +%% the display name for the executable in utilities such as `ps'. +%% +%% If `Cmd' argument passed to the `run/2' function is given as a +%% string, on Unix the `Executable' specifies a replacement shell +%% for the default `/bin/sh'.</dd> +%% <dt>{cd, WorkDir}</dt><dd>Working directory</dd> +%% <dt>{env, Env}</dt> +%% <dd>List of "VAR=VALUE" environment variables or +%% list of {Var, Value} tuples. Both representations are +%% used in other parts of Erlang/OTP +%% (e.g. os:getenv/0, erlang:open_port/2)</dd> +%% <dt>{kill, KillCmd}</dt> +%% <dd>This command will be used for killing the process. After +%% a 5-sec timeout if the process is still alive, it'll be +%% killed with SIGTERM followed by SIGKILL. By default +%% SIGTERM/SIGKILL combination is used for process +%% termination.</dd> +%% <dt>{kill_timeout, Sec::integer()}</dt> +%% <dd>Number of seconds to wait after issueing a SIGTERM or +%% executing the custom `kill' command (if specified) before +%% killing the process with the `SIGKILL' signal</dd> +%% <dt>kill_group</dt> +%% <dd>At process exit kill the whole process group associated with this pid. +%% The process group is obtained by the call to getpgid(3).</dd> +%% <dt>{group, GID}</dt> +%% <dd>Sets the effective group ID of the spawned process. The value 0 +%% means to create a new group ID equal to the OS pid of the process.</dd> +%% <dt>{user, RunAsUser}</dt> +%% <dd>When exec-port was compiled with capability (Linux) support +%% enabled and has a suid bit set, it's capable of running +%% commands with a different RunAsUser effective user. Passing +%% "root" value of `RunAsUser' is prohibited.</dd> +%% <dt>{success_exit_code, IntExitCode}</dt> +%% <dd>On success use `IntExitCode' return value instead of default 0.</dd> +%% <dt>{nice, Priority}</dt> +%% <dd>Set process priority between -20 and 20. Note that +%% negative values can be specified only when `exec-port' +%% is started with a root suid bit set.</dd> +%% <dt>stdin | {stdin, null | close | Filename}</dt> +%% <dd>Enable communication with an OS process via its `stdin'. The +%% input to the process is sent by `exec:send(OsPid, Data)'. +%% When specified as a tuple, `null' means redirection from `/dev/null', +%% `close' means to close `stdin' stream, and `Filename' means to +%% take input from file.</dd> +%% <dt>stdout</dt> +%% <dd>Same as `{stdout, self()}'.</dd> +%% <dt>stderr</dt> +%% <dd>Same as `{stderr, self()}'.</dd> +%% <dt>{stdout, output_device()}</dt> +%% <dd>Redirect process's standard output stream</dd> +%% <dt>{stderr, output_device()}</dt> +%% <dd>Redirect process's standard error stream</dd> +%% <dt>{stdout | stderr, Filename::string(), [output_dev_opt()]}</dt> +%% <dd>Redirect process's stdout/stderr stream to file</dd> +%% <dt>pty</dt> +%% <dd>Use pseudo terminal for the process's stdin, stdout and stderr</dd> +%% </dl> + +-type output_dev_opt() :: null | close | print | string() | pid() + | fun((stdout | stderr, integer(), binary()) -> none()). +%% Output device option: +%% <dl> +%% <dt>null</dt><dd>Suppress output.</dd> +%% <dt>close</dt><dd>Close file descriptor for writing.</dd> +%% <dt>print</dt> +%% <dd>A debugging convenience device that prints the output to the +%% console shell</dd> +%% <dt>Filename</dt><dd>Save output to file by overwriting it.</dd> +%% <dt>pid()</dt><dd>Redirect output to this pid.</dd> +%% <dt>fun((Stream, OsPid, Data) -> none())</dt> +%% <dd>Execute this callback on receiving output data</dd> +%% </dl> + +-type output_file_opt() :: append | {mode, Mode::integer()}. +%% Defines file opening attributes: +%% <dl> +%% <dt>append</dt><dd>Open the file in `append' mode</dd> +%% <dt>{mode, Mode}</dt> +%% <dd>File creation access mode <b>specified in base 8</b> (e.g. 8#0644)</dd> +%% </dl> + +-type ospid() :: integer(). +%% Representation of OS process ID. +-type osgid() :: integer(). +%% Representation of OS group ID. + +%%------------------------------------------------------------------------- +%% @doc Supervised start an external program manager. +%% @end +%%------------------------------------------------------------------------- +-spec start_link(exec_options()) -> {ok, pid()} | {error, any()}. +start_link(Options) when is_list(Options) -> + gen_server:start_link({local, ?MODULE}, ?MODULE, [Options], []). + +%%------------------------------------------------------------------------- +%% @equiv start_link/1 +%% @doc Start of an external program manager without supervision. +%% @end +%%------------------------------------------------------------------------- +-spec start() -> {ok, pid()} | {error, any()}. +start() -> + start([]). + +-spec start(exec_options()) -> {ok, pid()} | {error, any()}. +start(Options) when is_list(Options) -> + gen_server:start({local, ?MODULE}, ?MODULE, [Options], []). + +%%------------------------------------------------------------------------- +%% @doc Run an external program. `OsPid' is the OS process identifier of +%% the new process. If `sync' is specified in `Options' the return +%% value is `{ok, Status}' where `Status' is OS process exit status. +%% @end +%%------------------------------------------------------------------------- +-spec run(cmd(), cmd_options()) -> + {ok, pid(), ospid()} | {ok, [{stdout | stderr, [binary()]}]} | {error, any()}. +run(Exe, Options) when is_list(Exe), is_list(Options) -> + do_run({run, Exe, Options}, Options). + +%%------------------------------------------------------------------------- +%% @equiv run/2 +%% @doc Run an external program and link to the OsPid. If OsPid exits, +%% the calling process will be killed or if it's trapping exits, +%% it'll get {'EXIT', OsPid, Status} message. If the calling process +%% dies the OsPid will be killed. +%% @end +%%------------------------------------------------------------------------- +-spec run_link(cmd(), cmd_options()) -> + {ok, pid(), ospid()} | {ok, [{stdout | stderr, [binary()]}]} | {error, any()}. +run_link(Exe, Options) when is_list(Exe), is_list(Options) -> + do_run({run, Exe, Options}, [link | Options]). + +%%------------------------------------------------------------------------- +%% @doc Manage an existing external process. `OsPid' is the OS process +%% identifier of the external OS process or an Erlang `Port' that +%% would be managed by erlexec. +%% @end +%%------------------------------------------------------------------------- +-spec manage(ospid() | port(), Options::cmd_options()) -> + {ok, pid(), ospid()} | {error, any()}. +manage(Pid, Options) when is_integer(Pid) -> + do_run({manage, Pid, Options}, Options); +manage(Port, Options) when is_port(Port) -> + {os_pid, OsPid} = erlang:port_info(Port, os_pid), + manage(OsPid, Options). + +%%------------------------------------------------------------------------- +%% @doc Get a list of children managed by port program. +%% @end +%%------------------------------------------------------------------------- +-spec which_children() -> [ospid(), ...]. +which_children() -> + gen_server:call(?MODULE, {port, {list}}). + +%%------------------------------------------------------------------------- +%% @doc Send a `Signal' to a child `Pid', `OsPid' or an Erlang `Port'. +%% @end +%%------------------------------------------------------------------------- +-spec kill(pid() | ospid(), integer()) -> ok | {error, any()}. +kill(Pid, Signal) when is_pid(Pid); is_integer(Pid) -> + gen_server:call(?MODULE, {port, {kill, Pid, Signal}}); +kill(Port, Signal) when is_port(Port) -> + {os_pid, Pid} = erlang:port_info(Port, os_pid), + kill(Pid, Signal). + +%%------------------------------------------------------------------------- +%% @doc Change group ID of a given `OsPid' to `Gid'. +%% @end +%%------------------------------------------------------------------------- +-spec setpgid(ospid(), osgid()) -> ok | {error, any()}. +setpgid(OsPid, Gid) when is_integer(OsPid), is_integer(Gid) -> + gen_server:call(?MODULE, {port, {setpgid, OsPid, Gid}}). + +%%------------------------------------------------------------------------- +%% @doc Terminate a managed `Pid', `OsPid', or `Port' process. The OS process is +%% terminated gracefully. If it was given a `{kill, Cmd}' option at +%% startup, that command is executed and a timer is started. If +%% the program doesn't exit, then the default termination is +%% performed. Default termination implies sending a `SIGTERM' command +%% followed by `SIGKILL' in 5 seconds, if the program doesn't get +%% killed. +%% @end +%%------------------------------------------------------------------------- +-spec stop(pid() | ospid() | port()) -> ok | {error, any()}. +stop(Pid) when is_pid(Pid); is_integer(Pid) -> + gen_server:call(?MODULE, {port, {stop, Pid}}, 30000); +stop(Port) when is_port(Port) -> + {os_pid, Pid} = erlang:port_info(Port, os_pid), + stop(Pid). + +%%------------------------------------------------------------------------- +%% @doc Terminate a managed `Pid', `OsPid', or `Port' process, like +%% `stop/1', and wait for it to exit. +%% @end +%%------------------------------------------------------------------------- + +-spec stop_and_wait(pid() | ospid() | port(), integer()) -> term() | {error, any()}. +stop_and_wait(Port, Timeout) when is_port(Port) -> + {os_pid, OsPid} = erlang:port_info(Port, os_pid), + stop_and_wait(OsPid, Timeout); + +stop_and_wait(OsPid, Timeout) when is_integer(OsPid) -> + [{_, Pid}] = ets:lookup(exec_mon, OsPid), + stop_and_wait(Pid, Timeout); + +stop_and_wait(Pid, Timeout) when is_pid(Pid) -> + gen_server:call(?MODULE, {port, {stop, Pid}}, Timeout), + receive + {'DOWN', _Ref, process, Pid, ExitStatus} -> ExitStatus + after Timeout -> {error, timeout} + end; + +stop_and_wait(Port, Timeout) when is_port(Port) -> + {os_pid, Pid} = erlang:port_info(Port, os_pid), + stop_and_wait(Pid, Timeout). + +%%------------------------------------------------------------------------- +%% @doc Get `OsPid' of the given Erlang `Pid'. The `Pid' must be created +%% previously by running the run/2 or run_link/2 commands. +%% @end +%%------------------------------------------------------------------------- +-spec ospid(pid()) -> ospid() | {error, Reason::any()}. +ospid(Pid) when is_pid(Pid) -> + Ref = make_ref(), + Pid ! {{self(), Ref}, ospid}, + receive + {Ref, Reply} -> Reply; + Other -> Other + after 5000 -> {error, timeout} + end. + +%%------------------------------------------------------------------------- +%% @doc Get `Pid' of the given `OsPid'. The `OsPid' must be created +%% previously by running the run/2 or run_link/2 commands. +%% @end +%%------------------------------------------------------------------------- +-spec pid(OsPid::ospid()) -> pid() | undefined | {error, timeout}. +pid(OsPid) when is_integer(OsPid) -> + gen_server:call(?MODULE, {pid, OsPid}). + +%%------------------------------------------------------------------------- +%% @doc Send `Data' to stdin of the OS process identified by `OsPid'. +%% @end +%%------------------------------------------------------------------------- +-spec send(OsPid :: ospid() | pid(), binary()) -> ok. +send(OsPid, Data) when (is_integer(OsPid) orelse is_pid(OsPid)) andalso is_binary(Data) -> + gen_server:call(?MODULE, {port, {send, OsPid, Data}}). + +%%------------------------------------------------------------------------- +%% @doc Decode the program's exit_status. If the program exited by signal +%% the function returns `{signal, Signal, Core}' where the `Signal' +%% is the signal number or atom, and `Core' indicates if the core file +%% was generated. +%% @end +%%------------------------------------------------------------------------- +-spec status(integer()) -> + {status, ExitStatus :: integer()} | + {signal, Singnal :: integer() | atom(), 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, signal(TermSignal), CoreDump}; + false -> + {status, ExitStatus} + end. + +%%------------------------------------------------------------------------- +%% @doc Convert a signal number to atom +%% @end +%%------------------------------------------------------------------------- +-spec signal(integer()) -> atom() | integer(). +signal( 1) -> sighup; +signal( 2) -> sigint; +signal( 3) -> sigquit; +signal( 4) -> sigill; +signal( 5) -> sigtrap; +signal( 6) -> sigabrt; +signal( 7) -> sigbus; +signal( 8) -> sigfpe; +signal( 9) -> sigkill; +signal(11) -> sigsegv; +signal(13) -> sigpipe; +signal(14) -> sigalrm; +signal(15) -> sigterm; +signal(16) -> sigstkflt; +signal(17) -> sigchld; +signal(18) -> sigcont; +signal(19) -> sigstop; +signal(20) -> sigtstp; +signal(21) -> sigttin; +signal(22) -> sigttou; +signal(23) -> sigurg; +signal(24) -> sigxcpu; +signal(25) -> sigxfsz; +signal(26) -> sigvtalrm; +signal(27) -> sigprof; +signal(28) -> sigwinch; +signal(29) -> sigio; +signal(30) -> sigpwr; +signal(31) -> sigsys; +signal(34) -> sigrtmin; +signal(64) -> sigrtmax; +signal(Num) when is_integer(Num) -> Num. + +%%------------------------------------------------------------------------- +%% @private +%% @spec () -> Default::exec_options() +%% @doc Provide default value of a given option. +%% @end +%%------------------------------------------------------------------------- +default() -> + [{debug, 0}, % Debug mode of the port program. + {verbose, false}, % Verbose print of events on the Erlang side. + {args, ""}, % Extra arguments that can be passed to port program + {alarm, 12}, + {user, ""}, % Run port program as this user + {limit_users, []}, % Restricted list of users allowed to run commands + {portexe, default(portexe)}]. + +%% @private +default(portexe) -> + % Get architecture (e.g. i386-linux) + Dir = filename:dirname(filename:dirname(code:which(?MODULE))), + filename:join([Dir, "priv", erlang:system_info(system_architecture), "exec-port"]); +default(Option) -> + proplists:get_value(Option, default()). + +%%%---------------------------------------------------------------------- +%%% Callback functions from gen_server +%%%---------------------------------------------------------------------- + +%%----------------------------------------------------------------------- +%% Func: init/1 +%% Returns: {ok, State} | +%% {ok, State, Timeout} | +%% ignore | +%% {stop, Reason} +%% @private +%%----------------------------------------------------------------------- +init([Options]) -> + process_flag(trap_exit, true), + Opts0 = proplists:normalize(Options, + [{expand, [{debug, {debug, 1}}, + {verbose, {verbose, true}}]}]), + Opts1 = [T || T = {O,_} <- Opts0, + lists:member(O, [debug, verbose, args, alarm, user])], + Opts = proplists:normalize(Opts1, [{aliases, [{args, ''}]}]), + Args = lists:foldl( + fun({Opt, I}, Acc) when is_list(I), I =/= "" -> + [" -"++atom_to_list(Opt)++" "++I | Acc]; + ({Opt, I}, Acc) when is_integer(I) -> + [" -"++atom_to_list(Opt)++" "++integer_to_list(I) | Acc]; + (_, Acc) -> Acc + end, [], Opts), + Exe = proplists:get_value(portexe, Options, default(portexe)) ++ lists:flatten([" -n"|Args]), + Users = proplists:get_value(limit_users, Options, default(limit_users)), + Debug = proplists:get_value(verbose, Options, default(verbose)), + Env = case proplists:get_value(env, Options) of + undefined -> []; + Other -> [{env, Other}] + end, + try + debug(Debug, "exec: port program: ~s\n env: ~p\n", [Exe, Env]), + PortOpts = Env ++ [binary, exit_status, {packet, 2}, nouse_stdio, hide], + Port = erlang:open_port({spawn, Exe}, PortOpts), + Tab = ets:new(exec_mon, [protected,named_table]), + {ok, #state{port=Port, limit_users=Users, debug=Debug, registry=Tab}} + catch _:Reason -> + {stop, ?FMT("Error starting port '~s': ~200p", [Exe, Reason])} + end. + +%%---------------------------------------------------------------------- +%% Func: handle_call/3 +%% Returns: {reply, Reply, State} | +%% {reply, Reply, State, Timeout} | +%% {noreply, State} | +%% {noreply, State, Timeout} | +%% {stop, Reason, Reply, State} | (terminate/2 is called) +%% {stop, Reason, State} (terminate/2 is called) +%% @private +%%---------------------------------------------------------------------- +handle_call({port, Instruction}, From, #state{last_trans=Last} = State) -> + try is_port_command(Instruction, element(1, From), State) of + {ok, Term} -> + erlang:port_command(State#state.port, term_to_binary({0, Term})), + {reply, ok, State}; + {ok, Term, Link, PidOpts} -> + Next = next_trans(Last), + erlang:port_command(State#state.port, term_to_binary({Next, Term})), + {noreply, State#state{trans = queue:in({Next, From, Link, PidOpts}, State#state.trans)}} + catch _:{error, Why} -> + {reply, {error, Why}, State} + end; + +handle_call({pid, OsPid}, _From, State) -> + case ets:lookup(exec_mon, OsPid) of + [{_, Pid}] -> {reply, Pid, State}; + _ -> {reply, undefined, State} + end; + +handle_call(Request, _From, _State) -> + {stop, {not_implemented, Request}}. + +%%---------------------------------------------------------------------- +%% Func: handle_cast/2 +%% Returns: {noreply, State} | +%% {noreply, State, Timeout} | +%% {stop, Reason, State} (terminate/2 is called) +%% @private +%%---------------------------------------------------------------------- +handle_cast(_Msg, State) -> + {noreply, State}. + +%%---------------------------------------------------------------------- +%% Func: handle_info/2 +%% Returns: {noreply, State} | +%% {noreply, State, Timeout} | +%% {stop, Reason, State} (terminate/2 is called) +%% @private +%%---------------------------------------------------------------------- +handle_info({Port, {data, Bin}}, #state{port=Port, debug=Debug} = State) -> + Msg = binary_to_term(Bin), + debug(Debug, "~w got msg from port: ~p\n", [?MODULE, Msg]), + case Msg of + {N, Reply} when N =/= 0 -> + case get_transaction(State#state.trans, N) of + {true, {Pid,_} = From, MonType, PidOpts, Q} -> + NewReply = maybe_add_monitor(Reply, Pid, MonType, PidOpts, Debug), + gen_server:reply(From, NewReply); + {false, Q} -> + ok + end, + {noreply, State#state{trans=Q}}; + {0, {Stream, OsPid, Data}} when Stream =:= stdout; Stream =:= stderr -> + send_to_ospid_owner(OsPid, {Stream, Data}), + {noreply, State}; + {0, {exit_status, OsPid, Status}} -> + debug(Debug, "Pid ~w exited with status: ~s{~w,~w}\n", + [OsPid, if (((Status band 16#7F)+1) bsr 1) > 0 -> "signaled "; true -> "" end, + (Status band 16#FF00 bsr 8), Status band 127]), + notify_ospid_owner(OsPid, Status), + {noreply, State}; + {0, Ignore} -> + error_logger:warning_msg("~w [~w] unknown msg: ~p\n", [self(), ?MODULE, Ignore]), + {noreply, State} + end; + +handle_info({Port, {exit_status, 0}}, #state{port=Port} = State) -> + {stop, normal, State}; +handle_info({Port, {exit_status, Status}}, #state{port=Port} = State) -> + {stop, {exit_status, Status}, State}; +handle_info({'EXIT', Port, Reason}, #state{port=Port} = State) -> + {stop, Reason, State}; +handle_info({'EXIT', Pid, Reason}, State) -> + % OsPid's Pid owner died. Kill linked OsPid. + do_unlink_ospid(Pid, Reason, State), + {noreply, State}; +handle_info(_Info, State) -> + error_logger:info_msg("~w - unhandled message: ~p\n", [?MODULE, _Info]), + {noreply, State}. + +%%---------------------------------------------------------------------- +%% Func: code_change/3 +%% Purpose: Convert process state when code is changed +%% Returns: {ok, NewState} +%% @private +%%---------------------------------------------------------------------- +code_change(_OldVsn, State, _Extra) -> + {ok, State}. + +%%---------------------------------------------------------------------- +%% Func: terminate/2 +%% Purpose: Shutdown the server +%% Returns: any (ignored by gen_server) +%% @private +%%---------------------------------------------------------------------- +terminate(_Reason, State) -> + try + erlang:port_command(State#state.port, term_to_binary({0, {shutdown}})), + case wait_port_exit(State#state.port) of + 0 -> ok; + S -> error_logger:warning_msg("~w - exec process terminated (status: ~w)\n", + [self(), S]) + end + catch _:_ -> + ok + end. + +wait_port_exit(Port) -> + receive + {Port,{exit_status,Status}} -> + Status; + _ -> + wait_port_exit(Port) + end. + +%%%--------------------------------------------------------------------- +%%% Internal functions +%%%--------------------------------------------------------------------- + +-spec do_run(Cmd::any(), Options::cmd_options()) -> + {ok, pid(), ospid()} | {ok, [{stdout | stderr, [binary()]}]} | {error, any()}. +do_run(Cmd, Options) -> + Sync = proplists:get_value(sync, Options, false), + Mon = Sync =:= true orelse proplists:get_value(monitor, Options), + Link = case proplists:get_value(link, Options) of + true -> link; + _ -> nolink + end, + Cmd2 = {port, {Cmd, Link}}, + case {Mon, gen_server:call(?MODULE, Cmd2, 30000)} of + {true, {ok, Pid, OsPid} = R} -> + Ref = monitor(process, Pid), + case Sync of + true -> wait_for_ospid_exit(OsPid, Ref, [], []); + _ -> R + end; + {_, R} -> + R + end. + +wait_for_ospid_exit(OsPid, Ref, OutAcc, ErrAcc) -> + receive + {stdout, OsPid, Data} -> + wait_for_ospid_exit(OsPid, Ref, [Data | OutAcc], ErrAcc); + {stderr, OsPid, Data} -> + wait_for_ospid_exit(OsPid, Ref, OutAcc, [Data | ErrAcc]); + {'DOWN', Ref, process, _, normal} -> + {ok, sync_res(OutAcc, ErrAcc)}; + {'DOWN', Ref, process, _, noproc} -> + {ok, sync_res(OutAcc, ErrAcc)}; + {'DOWN', Ref, process, _, {exit_status,_}=R} -> + {error, [R | sync_res(OutAcc, ErrAcc)]}; + Other -> + {error, [{reason, Other} | sync_res(OutAcc, ErrAcc)]} + end. + +sync_res([], []) -> []; +sync_res([], L) -> [{stderr, lists:reverse(L)}]; +sync_res(LO, LE) -> [{stdout, lists:reverse(LO)} | sync_res([], LE)]. + +%% Add a link for Pid to OsPid if requested. +maybe_add_monitor({ok, OsPid}, Pid, MonType, PidOpts, Debug) when is_integer(OsPid) -> + % This is a reply to a run/run_link command. The port program indicates + % of creating a new OsPid process. + % Spawn a light-weight process responsible for monitoring this OsPid + Self = self(), + LWP = spawn_link(fun() -> ospid_init(Pid, OsPid, MonType, Self, PidOpts, Debug) end), + ets:insert(exec_mon, [{OsPid, LWP}, {LWP, OsPid}]), + {ok, LWP, OsPid}; +maybe_add_monitor(Reply, _Pid, _MonType, _PidOpts, _Debug) -> + Reply. + +%%---------------------------------------------------------------------- +%% @spec (Pid, OsPid::integer(), LinkType, Parent, PidOpts::list(), Debug::boolean()) -> +%% void() +%% @doc Every OsPid is associated with an Erlang process started with +%% this function. The `Parent' is the ?MODULE port manager that +%% spawned this process and linked to it. `Pid' is the process +%% that ran an OS command associated with OsPid. If that process +%% requested a link (LinkType = 'link') we'll link to it. +%% @end +%% @private +%%---------------------------------------------------------------------- +ospid_init(Pid, OsPid, LinkType, Parent, PidOpts, Debug) -> + process_flag(trap_exit, true), + StdOut = proplists:get_value(stdout, PidOpts), + StdErr = proplists:get_value(stderr, PidOpts), + case LinkType of + link -> link(Pid); % The caller pid that requested to run the OsPid command & link to it. + _ -> ok + end, + ospid_loop({Pid, OsPid, Parent, StdOut, StdErr, Debug}). + +ospid_loop({Pid, OsPid, Parent, StdOut, StdErr, Debug} = State) -> + receive + {{From, Ref}, ospid} -> + From ! {Ref, OsPid}, + ospid_loop(State); + {stdout, Data} when is_binary(Data) -> + ospid_deliver_output(StdOut, {stdout, OsPid, Data}), + ospid_loop(State); + {stderr, Data} when is_binary(Data) -> + ospid_deliver_output(StdErr, {stderr, OsPid, Data}), + ospid_loop(State); + {'DOWN', OsPid, {exit_status, Status}} -> + debug(Debug, "~w ~w got down message (~w)\n", [self(), OsPid, status(Status)]), + % OS process died + case Status of + 0 -> exit(normal); + _ -> exit({exit_status, Status}) + end; + {'EXIT', Pid, Reason} -> + % Pid died + debug(Debug, "~w ~w got exit from linked ~w: ~p\n", [self(), OsPid, Pid, Reason]), + exit({owner_died, Reason}); + {'EXIT', Parent, Reason} -> + % Port program died + debug(Debug, "~w ~w got exit from parent ~w: ~p\n", [self(), OsPid, Parent, Reason]), + exit({port_closed, Reason}); + Other -> + error_logger:warning_msg("~w - unknown msg: ~p\n", [self(), Other]), + ospid_loop(State) + end. + +ospid_deliver_output(DestPid, Msg) when is_pid(DestPid) -> + DestPid ! Msg; +ospid_deliver_output(DestFun, {Stream, OsPid, Data}) when is_function(DestFun) -> + DestFun(Stream, OsPid, Data). + +notify_ospid_owner(OsPid, Status) -> + % See if there is a Pid owner of this OsPid. If so, sent the 'DOWN' message. + case ets:lookup(exec_mon, OsPid) of + [{_OsPid, Pid}] -> + unlink(Pid), + Pid ! {'DOWN', OsPid, {exit_status, Status}}, + ets:delete(exec_mon, Pid), + ets:delete(exec_mon, OsPid); + [] -> + %error_logger:warning_msg("Owner ~w not found\n", [OsPid]), + ok + end. + +send_to_ospid_owner(OsPid, Msg) -> + case ets:lookup(exec_mon, OsPid) of + [{_, Pid}] -> Pid ! Msg; + _ -> ok + end. + +debug(false, _, _) -> + ok; +debug(true, Fmt, Args) -> + io:format(Fmt, Args). + +%%---------------------------------------------------------------------- +%% @spec (Pid::pid(), Action, State::#state{}) -> +%% {ok, LastTok::integer(), LeftLinks::integer()} +%% @doc Pid died or requested to unlink - remove linked Pid records and +%% optionally kill all OsPids linked to the Pid. +%% @end +%%---------------------------------------------------------------------- +do_unlink_ospid(Pid, _Reason, State) -> + case ets:lookup(exec_mon, Pid) of + [{_Pid, OsPid}] when is_integer(OsPid) -> + debug(State#state.debug, "Pid ~p died. Killing linked OsPid ~w\n", [Pid, OsPid]), + ets:delete(exec_mon, Pid), + ets:delete(exec_mon, OsPid), + erlang:port_command(State#state.port, term_to_binary({0, {stop, OsPid}})); + _ -> + ok + end. + +get_transaction(Q, I) -> + get_transaction(Q, I, Q). +get_transaction(Q, I, OldQ) -> + case queue:out(Q) of + {{value, {I, From, LinkType, PidOpts}}, Q2} -> + {true, From, LinkType, PidOpts, Q2}; + {empty, _} -> + {false, OldQ}; + {_, Q2} -> + get_transaction(Q2, I, OldQ) + end. + +is_port_command({{run, Cmd, Options}, Link}, Pid, State) -> + {PortOpts, Other} = check_cmd_options(Options, Pid, State, [], []), + {ok, {run, Cmd, PortOpts}, Link, Other}; +is_port_command({list} = T, _Pid, _State) -> + {ok, T, undefined, []}; +is_port_command({stop, OsPid}=T, _Pid, _State) when is_integer(OsPid) -> + {ok, T, undefined, []}; +is_port_command({stop, Pid}, _Pid, _State) when is_pid(Pid) -> + case ets:lookup(exec_mon, Pid) of + [{_StoredPid, OsPid}] -> {ok, {stop, OsPid}, undefined, []}; + [] -> throw({error, no_process}) + end; +is_port_command({{manage, OsPid, Options}, Link}, Pid, State) when is_integer(OsPid) -> + {PortOpts, _Other} = check_cmd_options(Options, Pid, State, [], []), + {ok, {manage, OsPid, PortOpts}, Link, []}; +is_port_command({send, Pid, Data}, _Pid, _State) when is_pid(Pid), is_binary(Data) -> + case ets:lookup(exec_mon, Pid) of + [{Pid, OsPid}] -> {ok, {stdin, OsPid, Data}}; + [] -> throw({error, no_process}) + end; +is_port_command({send, OsPid, Data}, _Pid, _State) when is_integer(OsPid), is_binary(Data) -> + {ok, {stdin, OsPid, Data}}; +is_port_command({kill, OsPid, Sig}=T, _Pid, _State) when is_integer(OsPid),is_integer(Sig) -> + {ok, T, undefined, []}; +is_port_command({setpgid, OsPid, Gid}=T, _Pid, _State) when is_integer(OsPid),is_integer(Gid) -> + {ok, T, undefined, []}; +is_port_command({kill, Pid, Sig}, _Pid, _State) when is_pid(Pid),is_integer(Sig) -> + case ets:lookup(exec_mon, Pid) of + [{Pid, OsPid}] -> {ok, {kill, OsPid, Sig}, undefined, []}; + [] -> throw({error, no_process}) + end. + +check_cmd_options([monitor|T], Pid, State, PortOpts, OtherOpts) -> + check_cmd_options(T, Pid, State, PortOpts, OtherOpts); +check_cmd_options([sync|T], Pid, State, PortOpts, OtherOpts) -> + check_cmd_options(T, Pid, State, PortOpts, OtherOpts); +check_cmd_options([link|T], Pid, State, PortOpts, OtherOpts) -> + check_cmd_options(T, Pid, State, PortOpts, OtherOpts); +check_cmd_options([{executable,V}=H|T], Pid, State, PortOpts, OtherOpts) when is_list(V) -> + check_cmd_options(T, Pid, State, [H|PortOpts], OtherOpts); +check_cmd_options([{cd, Dir}=H|T], Pid, State, PortOpts, OtherOpts) when is_list(Dir) -> + check_cmd_options(T, Pid, State, [H|PortOpts], OtherOpts); +check_cmd_options([{env, Env}=H|T], Pid, State, PortOpts, OtherOpts) when is_list(Env) -> + case lists:filter(fun(S) when is_list(S) -> false; + ({S1,S2}) when is_list(S1), is_list(S2) -> false; + (_) -> true + end, Env) of + [] -> check_cmd_options(T, Pid, State, [H|PortOpts], OtherOpts); + L -> throw({error, {invalid_env_value, L}}) + end; +check_cmd_options([{kill, Cmd}=H|T], Pid, State, PortOpts, OtherOpts) when is_list(Cmd) -> + check_cmd_options(T, Pid, State, [H|PortOpts], OtherOpts); +check_cmd_options([{kill_timeout, I}=H|T], Pid, State, PortOpts, OtherOpts) when is_integer(I), I >= 0 -> + check_cmd_options(T, Pid, State, [H|PortOpts], OtherOpts); +check_cmd_options([kill_group=H|T], Pid, State, PortOpts, OtherOpts) -> + check_cmd_options(T, Pid, State, [H|PortOpts], OtherOpts); +check_cmd_options([{nice, I}=H|T], Pid, State, PortOpts, OtherOpts) when is_integer(I), I >= -20, I =< 20 -> + check_cmd_options(T, Pid, State, [H|PortOpts], OtherOpts); +check_cmd_options([{success_exit_code, I}=H|T], Pid, State, PortOpts, OtherOpts) + when is_integer(I), I >= 0, I < 256 -> + check_cmd_options(T, Pid, State, [H|PortOpts], OtherOpts); +check_cmd_options([H|T], Pid, State, PortOpts, OtherOpts) when H=:=stdin; H=:=stdout; H=:=stderr -> + check_cmd_options(T, Pid, State, [H|PortOpts], [{H, Pid}|OtherOpts]); +check_cmd_options([H|T], Pid, State, PortOpts, OtherOpts) when H=:=pty -> + check_cmd_options(T, Pid, State, [H|PortOpts], [{H, Pid}|OtherOpts]); +check_cmd_options([{stdin, I}=H|T], Pid, State, PortOpts, OtherOpts) + when I=:=null; I=:=close; is_list(I) -> + check_cmd_options(T, Pid, State, [H|PortOpts], OtherOpts); +check_cmd_options([{Std, I, Opts}=H|T], Pid, State, PortOpts, OtherOpts) + when (Std=:=stdout orelse Std=:=stderr), is_list(Opts) -> + io_lib:printable_list(I) orelse + throw({error, ?FMT("Invalid ~w filename: ~200p", [Std, I])}), + lists:foreach(fun + (append) -> ok; + ({mode, Mode}) when is_integer(Mode) -> ok; + (Other) -> throw({error, ?FMT("Invalid ~w option: ~p", [Std, Other])}) + end, Opts), + check_cmd_options(T, Pid, State, [H|PortOpts], OtherOpts); +check_cmd_options([{Std, I}=H|T], Pid, State, PortOpts, OtherOpts) + when Std=:=stderr, I=/=Std; Std=:=stdout, I=/=Std -> + if + I=:=null; I=:=close; I=:=stderr; I=:=stdout; is_list(I) -> + check_cmd_options(T, Pid, State, [H|PortOpts], OtherOpts); + I=:=print -> + check_cmd_options(T, Pid, State, [Std | PortOpts], [{Std, fun print/3} | OtherOpts]); + is_pid(I) -> + check_cmd_options(T, Pid, State, [Std | PortOpts], [H|OtherOpts]); + is_function(I) -> + {arity, 3} =:= erlang:fun_info(I, arity) + orelse throw({error, ?FMT("Invalid ~w option ~p: expected Fun/3", [Std, I])}), + check_cmd_options(T, Pid, State, [Std | PortOpts], [H|OtherOpts]); + true -> + throw({error, ?FMT("Invalid ~w option ~p", [Std, I])}) + end; +check_cmd_options([{group, I}=H|T], Pid, State, PortOpts, OtherOpts) when is_integer(I), I >= 0; is_list(I) -> + check_cmd_options(T, Pid, State, [H|PortOpts], OtherOpts); +check_cmd_options([{user, U}=H|T], Pid, State, PortOpts, OtherOpts) when is_list(U), U =/= "" -> + case lists:member(U, State#state.limit_users) of + true -> check_cmd_options(T, Pid, State, [H|PortOpts], OtherOpts); + false -> throw({error, ?FMT("User ~s is not allowed to run commands!", [U])}) + end; +check_cmd_options([Other|_], _Pid, _State, _PortOpts, _OtherOpts) -> + throw({error, {invalid_option, Other}}); +check_cmd_options([], _Pid, _State, PortOpts, OtherOpts) -> + {PortOpts, OtherOpts}. + +next_trans(I) when I =< 134217727 -> + I+1; +next_trans(_) -> + 1. + +print(Stream, OsPid, Data) -> + io:format("Got ~w from ~w: ~p\n", [Stream, OsPid, Data]). + +%%%--------------------------------------------------------------------- +%%% Unit testing +%%%--------------------------------------------------------------------- + +-ifdef(EUNIT). + +-define(receiveMatch(A, Timeout), + (fun() -> + receive + _M -> ?assertMatch(A, _M) + after Timeout -> + ?assertMatch(A, timeout) + end + end)()). + +-define(tt(F), {timeout, 20, ?_test(F)}). + +temp_file() -> + Dir = case os:getenv("TEMP") of + false -> "/tmp"; + Path -> Path + end, + {I1, I2, I3} = now(), + filename:join(Dir, io_lib:format("exec_temp_~w_~w_~w", [I1, I2, I3])). + +exec_test_() -> + {setup, + fun() -> {ok, Pid} = exec:start([{debug, 3}]), Pid end, + fun(Pid) -> exit(Pid, kill) end, + [ + ?tt(test_monitor()), + ?tt(test_sync()), + ?tt(test_stdin()), + ?tt(test_std(stdout)), + ?tt(test_std(stderr)), + ?tt(test_cmd()), + ?tt(test_executable()), + ?tt(test_redirect()), + ?tt(test_env()), + ?tt(test_kill_timeout()), + ?tt(test_setpgid()), + ?tt(test_pty()) + ] + }. + +test_monitor() -> + {ok, P, _} = exec:run("echo ok", [{stdout, null}, monitor]), + ?receiveMatch({'DOWN', _, process, P, normal}, 5000). + +test_sync() -> + ?assertMatch({ok, [{stdout, [<<"Test\n">>]}, {stderr, [<<"ERR\n">>]}]}, + exec:run("echo Test; echo ERR 1>&2", [stdout, stderr, sync])). + +test_stdin() -> + {ok, P, I} = exec:run("read x; echo \"Got: $x\"", [stdin, stdout, monitor]), + ok = exec:send(I, <<"Test data\n">>), + ?receiveMatch({stdout,I,<<"Got: Test data\n">>}, 3000), + ?receiveMatch({'DOWN', _, process, P, normal}, 5000). + +test_std(Stream) -> + Suffix = case Stream of + stderr -> " 1>&2"; + stdout -> "" + end, + {ok, _, I} = exec:run("for i in 1 2; do echo TEST$i; sleep 0.05; done" ++ Suffix, [Stream]), + ?receiveMatch({Stream,I,<<"TEST1\n">>}, 5000), + ?receiveMatch({Stream,I,<<"TEST2\n">>}, 5000), + + Filename = temp_file(), + try + ?assertMatch({ok, []}, exec:run("echo Test"++Suffix, [{Stream, Filename}, sync])), + ?assertMatch({ok, <<"Test\n">>}, file:read_file(Filename)), + + ?assertMatch({ok, []}, exec:run("echo Test"++Suffix, [{Stream, Filename}, sync])), + ?assertMatch({ok, <<"Test\n">>}, file:read_file(Filename)), + + ?assertMatch({ok, []}, exec:run("echo Test2"++Suffix, [{Stream, Filename, [append]}, sync])), + ?assertMatch({ok, <<"Test\nTest2\n">>}, file:read_file(Filename)) + + after + ?assertEqual(ok, file:delete(Filename)) + end. + +test_cmd() -> + % Cmd given as string + ?assertMatch( + {ok, [{stdout, [<<"ok\n">>]}]}, + exec:run("/bin/echo ok", [sync, stdout])), + % Cmd given as list + ?assertMatch( + {ok, [{stdout, [<<"ok\n">>]}]}, + exec:run(["/bin/bash", "-c", "echo ok"], [sync, stdout])), + ?assertMatch( + {ok, [{stdout, [<<"ok\n">>]}]}, + exec:run(["/bin/echo", "ok"], [sync, stdout])). + +test_executable() -> + % Cmd given as string + ?assertMatch( + [<<"Pid ", _/binary>>, <<" cannot execute '00kuku00': No such file or directory\n">>], + begin + {error, [{exit_status,_}, {stderr, [E]}]} = + exec:run("ls", [sync, {executable, "00kuku00"}, stdout, stderr]), + binary:split(E, <<":">>) + end), + + ?assertMatch( + {ok, [{stdout,[<<"ok\n">>]}]}, + exec:run("echo ok", [sync, {executable, "/bin/sh"}, stdout, stderr])), + + % Cmd given as list + ?assertMatch( + {ok, [{stdout,[<<"ok\n">>]}]}, + exec:run(["/bin/bash", "-c", "/bin/echo ok"], + [sync, {executable, "/bin/sh"}, stdout, stderr])), + ?assertMatch( + {ok, [{stdout,[<<"XYZ\n">>]}]}, + exec:run(["/bin/echoXXXX abc", "XYZ"], + [sync, {executable, "/bin/echo"}, stdout, stderr])). + +test_redirect() -> + ?assertMatch({ok,[{stderr,[<<"TEST1\n">>]}]}, + exec:run("echo TEST1", [stderr, {stdout, stderr}, sync])), + ?assertMatch({ok,[{stdout,[<<"TEST2\n">>]}]}, + exec:run("echo TEST2 1>&2", [stdout, {stderr, stdout}, sync])), + ok. + +test_env() -> + ?assertMatch({ok, [{stdout, [<<"X\n">>]}]}, + exec:run("echo $XXX", [stdout, {env, [{"XXX", "X"}]}, sync])). + +test_kill_timeout() -> + {ok, P, I} = exec:run("trap '' SIGTERM; sleep 30", [{kill_timeout, 1}, monitor]), + exec:stop(I), + ?receiveMatch({'DOWN', _, process, P, normal}, 5000). + +test_setpgid() -> + % Cmd given as string + {ok, P0, P} = exec:run("sleep 1", [{group, 0}, kill_group, monitor]), + {ok, P1, _} = exec:run("sleep 15", [{group, P}, monitor]), + {ok, P2, _} = exec:run("sleep 15", [{group, P}, monitor]), + ?receiveMatch({'DOWN',_,process, P0, normal}, 5000), + ?receiveMatch({'DOWN',_,process, P1, {exit_status, 15}}, 5000), + ?receiveMatch({'DOWN',_,process, P2, {exit_status, 15}}, 5000). + +test_pty() -> + ?assertMatch({error,[{exit_status,256},{stdout,[<<"not a tty\n">>]}]}, + exec:run("tty", [stdin, stdout, sync])), + ?assertMatch({ok,[{stdout,[<<"/dev/pts/", _/binary>>]}]}, + exec:run("tty", [stdin, stdout, pty, sync])), + {ok, P, I} = exec:run("/bin/bash --norc -i", [stdin, stdout, pty, monitor]), + exec:send(I, <<"echo ok\n">>), + receive + {stdout, I, <<"echo ok\r\n">>} -> + ?receiveMatch({stdout, I, <<"ok\r\n">>}, 1000); + {stdout, I, <<"ok\r\n">>} -> + ok + after 1000 -> + ?assertMatch({stdout, I, <<"ok\r\n">>}, timeout) + end, + exec:send(I, <<"exit\n">>), + ?receiveMatch({'DOWN', _, process, P, normal}, 1000). + +-endif. diff --git a/deps/exec/src/exec_app.erl b/deps/exec/src/exec_app.erl new file mode 100644 index 0000000..94077ba --- /dev/null +++ b/deps/exec/src/exec_app.erl @@ -0,0 +1,78 @@ +%%%------------------------------------------------------------------------ +%%% File: $Id$ +%%%------------------------------------------------------------------------ +%%% @doc This module implements application and supervisor behaviors +%%% of the `exec' application. +%%% @author Serge Aleynikov <saleyn@gmail.com> +%%% @version $Revision: 1.1 $ +%%% @end +%%%---------------------------------------------------------------------- +%%% Created: 2003-06-25 by Serge Aleynikov <saleyn@gmail.com> +%%% $URL$ +%%%------------------------------------------------------------------------ +-module(exec_app). +-author('saleyn@gmail.com'). +-id ("$Id$"). + +-behaviour(application). +-behaviour(supervisor). + +%% application and supervisor callbacks +-export([start/2, stop/1, init/1]). + +%%%---------------------------------------------------------------------- +%%% API +%%%---------------------------------------------------------------------- + +%%---------------------------------------------------------------------- +%% This is the entry module for your application. It contains the +%% start function and some other stuff. You identify this module +%% using the 'mod' attribute in the .app file. +%% +%% The start function is called by the application controller. +%% It normally returns {ok,Pid}, i.e. the same as gen_server and +%% supervisor. Here, we simply call the start function in our supervisor. +%% One can also return {ok, Pid, State}, where State is reused in stop(State). +%% +%% Type can be 'normal' or {takeover,FromNode}. If the 'start_phases' +%% attribute is present in the .app file, Type can also be {failover,FromNode}. +%% This is an odd compatibility thing. +%% @private +%%---------------------------------------------------------------------- +start(_Type, _Args) -> + supervisor:start_link({local, ?MODULE}, ?MODULE, []). + +%%---------------------------------------------------------------------- +%% stop(State) is called when the application has been terminated, and +%% all processes are gone. The return value is ignored. +%% @private +%%---------------------------------------------------------------------- +stop(_S) -> + ok. + +%%%--------------------------------------------------------------------- +%%% Supervisor behaviour callbacks +%%%--------------------------------------------------------------------- + +%% @private +init([]) -> + Options = + lists:foldl( + fun(I, Acc) -> add_option(I, Acc) end, + [], [I || {I, _} <- exec:default()]), + {ok, { + {one_for_one, 3, 30}, % Allow MaxR restarts within MaxT seconds + [{ exec, % Id = internal id + {exec, start_link, [Options]}, % StartFun = {M, F, A} + permanent, % Restart = permanent | transient | temporary + 10000, % Shutdown - wait 10 seconds, to give child processes time to be killed off. + worker, % Type = worker | supervisor + [exec] % Modules = [Module] | dynamic + }] + }}. + +add_option(Option, Acc) -> + case application:get_env(exec, Option) of + {ok, Value} -> [{Option, Value} | Acc]; + undefined -> Acc + end. diff --git a/deps/exec/src/overview.edoc b/deps/exec/src/overview.edoc new file mode 100644 index 0000000..79a32ad --- /dev/null +++ b/deps/exec/src/overview.edoc @@ -0,0 +1,305 @@ + + Exec - OS Process Manager for Erlang VM. + +@author Serge Aleynikov <saleyn at gmail dot com> +@version {@vsn} +@title Exec - OS Process Manager for Erlang VM. + +@doc This application implements a manager of OS processes. + +It's designed to address the shortcomings of Erlang's +`os:cmd/1' and `erlang:open_port/2' that allow to execute external +OS processes. + +== Contents == +<ol> + <li>{@section Download}</li> + <li>{@section Features}</li> + <li>{@section Supported Platforms}</li> + <li>{@section Architecture}</li> + <li>{@section Configuration Options}</li> + <li>{@section Examples}</li> +</ol> + + +== Download == + +<ul> +<li>Project's repository: [https://github.com/saleyn/erlexec]</li> +<li>Git clone command: `git clone https://github.com/saleyn/erlexec.git'</li> +</ul> + +== Features == + +<ol> +<li>Starting, stopping OS commands and getting their OS process IDs.</li> +<li>Setting OS command working directory, environment, effective user, + process priority.</li> +<li>Providing custom termination command for killing a process or relying + on default SIGTERM/SIGKILL behavior. Specifying custom timeout + for SIGKILL after the termination command or SIGTERM was executed.</li> +<li>Terminating all processing beloging to a process group</li> +<li>Ability to link Erlang processes to OS processes (via intermediate + Erlang Pids that are linked to an associated OS process).</li> +<li>Ability to monitor the termination of OS processes.</li> +<li>Ability to execute OS processes synchronously and asynchronously.</li> +<li>Proper cleanup of OS processes at port program termination time.</li> +<li>Communicating with an OS process via its STDIN.</li> +<li>Redirecting STDOUT and STDERR of an OS process to a file, erlang process, + or a custom function. When redirected to a file, the file can be + open in append/truncate mode, and given creation access mask.</li> +<li>Running interactive processes with psudo-terminal pty support.</li> +</ol> + +== Supported Platforms == + +Linux, Solaris, and MacOS X. + +== Architecture == +``` + *-------------------------* + | +----+ +----+ +----+ | + | |Pid1| |Pid2| |PidN| | Erlang light-weight Pids associated + | +----+ +----+ +----+ | one-to-one with managed OsPids + | \ | / | + | \ | / | + | \ | / (links) | + | +------+ | + | | exec | | Exec application running in Erlang VM + | +------+ | + | Erlang VM | | + *-------------+-----------* + | + +-----------+ + | exec-port | Port program (separate OS process) + +-----------+ + / | \ + (optional stdin/stdout/stderr pipes) + / | \ + +------+ +------+ +------+ + |OsPid1| |OsPid2| |OsPidN| Managed Child OS processes + +------+ +------+ +------+ +''' + +== Configuration Options == + +See description of types in {@link exec:exec_options()}. + +== Examples == + +=== Starting/stopping an OS process === +``` +1> exec:start([]). % Start the port program. +{ok,<0.32.0>} +2> {ok, _, I} = exec:run_link("sleep 1000", []). % Run a shell command to sleep for 1000s. +{ok,<0.34.0>,23584} +3> exec:stop(I). % Kill the shell command. +ok % Note that this could also be accomplished + % by doing exec:stop(pid(0,34,0)). +''' + +=== Killing an OS process === + +Note that killing a process can be accomplished by running kill(3) command +in an external shell, or by executing exec:kill/2. +``` +1> f(I), {ok, _, I} = exec:run_link("sleep 1000", []). +{ok,<0.37.0>,2350} +2> exec:kill(I, 15). +ok +** exception error: {exit_status,15} % Our shell died because we linked to the + % killed shell process via exec:run_link/2. + +3> exec:status(15). % Examine the exit status. +{signal,15,false} % The program got SIGTERM signal and produced + % no core file. +''' + +=== Using a custom success return code === +``` +1> exec:start_link([]). +{ok,<0.35.0>} +2> exec:run_link("sleep 1", [{success_exit_code, 0}, sync]). +{ok,[]} +3> exec:run("sleep 1", [{success_exit_code, 1}, sync]). +{error,[{exit_status,1}]} % Note that the command returns exit code 1 +''' + +=== Redirecting OS process stdout to a file === +``` +7> f(I), {ok, _, I} = exec:run_link("for i in 1 2 3; do echo \"Test$i\"; done", + [{stdout, "/tmp/output"}]). +8> io:format("~s", [binary_to_list(element(2, file:read_file("/tmp/output")))]), + file:delete("/tmp/output"). +Test1 +Test2 +Test3 +ok +''' + +=== Redirecting OS process stdout to screen, an Erlang process or a custom function === +``` +9> exec:run("echo Test", [{stdout, print}]). +{ok,<0.119.0>,29651} +Got stdout from 29651: <<"Test\n">> + +10> exec:run("for i in 1 2 3; do sleep 1; echo \"Iter$i\"; done", + [{stdout, fun(S,OsPid,D) -> io:format("Got ~w from ~w: ~p\n", [S,OsPid,D]) end}]). +{ok,<0.121.0>,29652} +Got stdout from 29652: <<"Iter1\n">> +Got stdout from 29652: <<"Iter2\n">> +Got stdout from 29652: <<"Iter3\n">> + +% Note that stdout/stderr options are equivanet to {stdout, self()}, {stderr, self()} +11> exec:run("echo Hello World!; echo ERR!! 1>&2", [stdout, stderr]). +{ok,<0.244.0>,18382} +12> flush(). +Shell got {stdout,18382,<<"Hello World!\n">>} +Shell got {stderr,18382,<<"ERR!!\n">>} +ok +''' + +=== Appending OS process stdout to a file === +``` +13> exec:run("for i in 1 2 3; do echo TEST$i; done", + [{stdout, "/tmp/out", [append, {mode, 8#600}]}, sync]), + file:read_file("/tmp/out"). +{ok,<<"TEST1\nTEST2\nTEST3\n">>} +14> exec:run("echo Test4; done", [{stdout, "/tmp/out", [append, {mode, 8#600}]}, sync]), + file:read_file("/tmp/out"). +{ok,<<"TEST1\nTEST2\nTEST3\nTest4\n">>} +15> file:delete("/tmp/out"). +''' + +=== Setting up a monitor for the OS process === +``` +> f(I), f(P), {ok, P, I} = exec:run("echo ok", [{stdout, self()}, monitor]). +{ok,<0.263.0>,18950} +16> flush(). +Shell got {stdout,18950,<<"ok\n">>} +Shell got {'DOWN',#Ref<0.0.0.1651>,process,<0.263.0>,normal} +ok +''' + +=== Managing an externally started OS process === +This command allows to instruct erlexec to begin monitoring given OS process +and notify Erlang when the process exits. It is also able to send signals to +the process and kill it. +``` +% Start an externally managed OS process and retrieve its OS PID: +17> spawn(fun() -> os:cmd("echo $$ > /tmp/pid; sleep 15") end). +<0.330.0> +18> f(P), P = list_to_integer(lists:reverse(tl(lists:reverse(binary_to_list(element(2, +file:read_file("/tmp/pid"))))))). +19355 + +% Manage the process and get notified by a monitor when it exits: +19> exec:manage(P, [monitor]). +{ok,<0.334.0>,19355} + +% Wait for monitor notification +20> f(M), receive M -> M end. +{'DOWN',#Ref<0.0.0.2205>,process,<0.334.0>,{exit_status,10}} +ok +21> file:delete("/tmp/pid"). +ok +''' + +=== Specifying custom process shutdown delay in seconds === +``` +% Execute an OS process (script) that blocks SIGTERM with custom kill timeout, and monitor +22> f(I), {ok, _, I} = exec:run("trap '' SIGTERM; sleep 30", [{kill_timeout, 3}, monitor]). +{ok,<0.399.0>,26347} +% Attempt to stop the OS process +23> exec:stop(I). +ok +% Wait for its completion +24> f(M), receive M -> M after 10000 -> timeout end. +{'DOWN',#Ref<0.0.0.1671>,process,<0.403.0>,normal} +''' + +=== Communicating with an OS process via STDIN === +``` +% Execute an OS process (script) that reads STDIN and echoes it back to Erlang +25> f(I), {ok, _, I} = exec:run("read x; echo \"Got: $x\"", [stdin, stdout, monitor]). +{ok,<0.427.0>,26431} +% Send the OS process some data via its stdin +26> exec:send(I, <<"Test data\n">>). +ok +% Get the response written to processes stdout +27> f(M), receive M -> M after 10000 -> timeout end. +{stdout,26431,<<"Got: Test data\n">>} +% Confirm that the process exited +28> f(M), receive M -> M after 10000 -> timeout end. +{'DOWN',#Ref<0.0.0.1837>,process,<0.427.0>,normal} +''' + +=== Running OS commands synchronously === +``` +% Execute an shell script that blocks for 1 second and return its termination code +29> exec:run("sleep 1; echo Test", [sync]). +% By default all I/O is redirected to /dev/null, so no output is captured +{ok,[]} + +% 'stdout' option instructs the port program to capture stdout and return it to caller +30> exec:run("sleep 1; echo Test", [stdout, sync]). +{ok,[{stdout, [<<"Test\n">>]}]} + +% Execute a non-existing command +31> exec:run("echo1 Test", [sync, stdout, stderr]). +{error,[{exit_status,32512}, + {stderr,[<<"/bin/bash: echo1: command not found\n">>]}]} + +% Capture stdout/stderr of the executed command +32> exec:run("echo Test; echo Err 1>&2", [sync, stdout, stderr]). +{ok,[{stdout,[<<"Test\n">>]},{stderr,[<<"Err\n">>]}]} + +% Redirect stderr to stdout +33> exec:run("echo Test 1>&2", [{stderr, stdout}, stdout, sync]). +{ok, [{stdout, [<<"Test\n">>]}]} +''' + +=== Running OS commands with/without shell === +``` +% Execute a command by an OS shell interpreter +34> exec:run("/bin/echo ok", [sync, stdout]). +{ok, [{stdout, [<<"ok\n">>]}]} + +% Execute an executable without a shell +35> exec:run(["/bin/echo", "ok"], [sync, stdout])). +{ok, [{stdout, [<<"ok\n">>]}]} + +% Execute a shell with custom options +36> exec:run(["/bin/bash", "-c", "echo ok"], [sync, stdout])). +{ok, [{stdout, [<<"ok\n">>]}]} +''' + +=== Running OS commands with pseudo terminal (pty) === +``` +% Execute a command without a pty +37> exec:run("echo hello", [sync, stdout]). +{ok, [{stdout,[<<"hello\n">>]}]} + +% Execute a command with a pty +38> exec:run("echo hello", [sync, stdout, pty]). +{ok,[{stdout,[<<"hello">>,<<"\r\n">>]}]} +''' + +=== Kill a process group at process exit === +``` +% In the following scenario the process P0 will create a new process group +% equal to the OS pid of that process (value = GID). The next two commands +% are assigned to the same process group GID. As soon as the P0 process exits +% P1 and P2 will also get terminated by signal 15 (SIGTERM): +39> {ok, P0, GID} = exec:run("sleep 10", [{group, 0}, kill_group]). +{ok,<0.37.0>,25306} +40> {ok, P1, _} = exec:run("sleep 15", [{group, GID}, monitor]). +{ok,<0.39.0>,25307} +41> {ok, P2, _} = exec:run("sleep 15", [{group, GID}, monitor]). +{ok,<0.41.0>,25308} +42> flush(). +Shell got {'DOWN',#Ref<0.0.0.42>,process,<0.39.0>,{exit_status,15}} +Shell got {'DOWN',#Ref<0.0.0.48>,process,<0.41.0>,{exit_status,15}} +ok +''' +@end |