Tarantool development patches archive
 help / color / mirror / Atom feed
* [Tarantool-patches] [PATCH v6 0/4] popen: Add ability to run external process
@ 2019-12-17 12:54 Cyrill Gorcunov
  2019-12-17 12:54 ` [Tarantool-patches] [PATCH v6 1/4] coio: Export helpers and provide coio_read_fd_timeout Cyrill Gorcunov
                   ` (3 more replies)
  0 siblings, 4 replies; 16+ messages in thread
From: Cyrill Gorcunov @ 2019-12-17 12:54 UTC (permalink / raw)
  To: tml

In this series we provide a way to execute external binaries
from inside of Lua scripts, control children process and
communicate with their stdin/out/err streams.

In v6 I reworked the API using coio as main nonblocking
i/o engine. (v3,4,5 are kept internal for testing reasons).

Probably the best way to start read this series is
the last patch where real examples of usage are
present.

Note the tests won't pass because they require our
test-run engine to unblock 'popen' module. Still
if do these commands inside tarantool console they
work as expected and give enough information about
API.

Issue https://github.com/tarantool/tarantool/issues/4031
Branch https://github.com/tarantool/tarantool/tree/gorcunov/gh-4031-popen-6

Cyrill Gorcunov (4):
  coio: Export helpers and provide coio_read_fd_timeout
  popen: Introduce a backend engine
  popen/lua: Add popen module
  popen/test: Add base test cases

 src/CMakeLists.txt          |    2 +
 src/box/applier.cc          |    2 +-
 src/lib/core/CMakeLists.txt |    1 +
 src/lib/core/coio.cc        |   41 ++
 src/lib/core/coio.h         |   31 +-
 src/lib/core/popen.c        | 1204 +++++++++++++++++++++++++++++++++++
 src/lib/core/popen.h        |  207 ++++++
 src/lua/init.c              |    4 +
 src/lua/popen.c             |  483 ++++++++++++++
 src/lua/popen.h             |   44 ++
 src/lua/popen.lua           |  516 +++++++++++++++
 src/main.cc                 |    4 +
 test/app/popen.result       |  234 +++++++
 test/app/popen.test.lua     |   91 +++
 14 files changed, 2858 insertions(+), 6 deletions(-)
 create mode 100644 src/lib/core/popen.c
 create mode 100644 src/lib/core/popen.h
 create mode 100644 src/lua/popen.c
 create mode 100644 src/lua/popen.h
 create mode 100644 src/lua/popen.lua
 create mode 100644 test/app/popen.result
 create mode 100644 test/app/popen.test.lua

-- 
2.20.1

^ permalink raw reply	[flat|nested] 16+ messages in thread

* [Tarantool-patches] [PATCH v6 1/4] coio: Export helpers and provide coio_read_fd_timeout
  2019-12-17 12:54 [Tarantool-patches] [PATCH v6 0/4] popen: Add ability to run external process Cyrill Gorcunov
@ 2019-12-17 12:54 ` Cyrill Gorcunov
  2019-12-20  7:48   ` Konstantin Osipov
  2019-12-17 12:54 ` [Tarantool-patches] [PATCH v6 2/4] popen: Introduce a backend engine Cyrill Gorcunov
                   ` (2 subsequent siblings)
  3 siblings, 1 reply; 16+ messages in thread
From: Cyrill Gorcunov @ 2019-12-17 12:54 UTC (permalink / raw)
  To: tml

There is no reason to hide functions. In particular
we will use coio_write_fd_timeout and coio_read_fd_timeout
for popen.

Part-of #4031

Signed-off-by: Cyrill Gorcunov <gorcunov@gmail.com>
---
 src/box/applier.cc   |  2 +-
 src/lib/core/coio.cc | 41 +++++++++++++++++++++++++++++++++++++++++
 src/lib/core/coio.h  | 31 ++++++++++++++++++++++++++-----
 3 files changed, 68 insertions(+), 6 deletions(-)

diff --git a/src/box/applier.cc b/src/box/applier.cc
index 42374f886..a44c6693f 100644
--- a/src/box/applier.cc
+++ b/src/box/applier.cc
@@ -935,7 +935,7 @@ applier_disconnect(struct applier *applier, enum applier_state state)
 		applier->writer = NULL;
 	}
 
-	coio_close(loop(), &applier->io);
+	coio_close_io(loop(), &applier->io);
 	/* Clear all unparsed input. */
 	ibuf_reinit(&applier->ibuf);
 	fiber_gc();
diff --git a/src/lib/core/coio.cc b/src/lib/core/coio.cc
index e88d724d5..b16497b7b 100644
--- a/src/lib/core/coio.cc
+++ b/src/lib/core/coio.cc
@@ -740,6 +740,47 @@ int coio_close(int fd)
 	return close(fd);
 }
 
+ssize_t
+coio_read_fd_timeout(int fd, void *data, size_t size, ev_tstamp timeout)
+{
+	ev_tstamp start, delay;
+	ev_loop *loop = loop();
+	ssize_t pos = 0;
+
+	evio_timeout_init(loop, &start, &delay, timeout);
+
+	while (size > 0) {
+		ssize_t rc = read(fd, data, size);
+		if (rc > 0) {
+			size -= (size_t)rc;
+			pos += rc;
+			data = (char *)data + rc;
+			continue;
+		} else if (rc == 0) {
+			/*
+			 * Socket peer could be closed
+			 * or shot down.
+			 */
+			break;
+		}
+
+		if (delay <= 0) {
+			diag_set(TimedOut);
+			return -1;
+		}
+
+		if (errno == EAGAIN || errno == EWOULDBLOCK) {
+			coio_wait(fd, COIO_READ, delay);
+			evio_timeout_update(loop, &start, &delay);
+		} else if (errno != EINTR) {
+			diag_set(SocketError, sio_socketname(fd), "read");
+			return -1;
+		}
+	}
+
+	return pos;
+}
+
 int
 coio_write_fd_timeout(int fd, const void *data, size_t size, ev_tstamp timeout)
 {
diff --git a/src/lib/core/coio.h b/src/lib/core/coio.h
index 6a2337689..e1b80ad68 100644
--- a/src/lib/core/coio.h
+++ b/src/lib/core/coio.h
@@ -32,9 +32,16 @@
  */
 #include "fiber.h"
 #include "trivia/util.h"
-#if defined(__cplusplus)
+
 #include "evio.h"
 
+#if defined(__cplusplus)
+extern "C" {
+#endif /* defined(__cplusplus) */
+
+struct sockaddr;
+struct uri;
+
 /**
  * Co-operative I/O
  * Yield the current fiber until IO is ready.
@@ -70,8 +77,12 @@ coio_accept(struct ev_io *coio, struct sockaddr *addr, socklen_t addrlen,
 void
 coio_create(struct ev_io *coio, int fd);
 
+/*
+ * Due to name conflict with coio_close in API_EXPORT
+ * we have to use coio_close_io() instead of plain coio_close().
+ */
 static inline void
-coio_close(ev_loop *loop, struct ev_io *coio)
+coio_close_io(ev_loop *loop, struct ev_io *coio)
 {
 	return evio_close(loop, coio);
 }
@@ -185,9 +196,6 @@ coio_stat_stat_timeout(ev_stat *stat, ev_tstamp delay);
 int
 coio_waitpid(pid_t pid);
 
-extern "C" {
-#endif /* defined(__cplusplus) */
-
 /** \cond public */
 
 enum {
@@ -232,6 +240,19 @@ coio_close(int fd);
 int
 coio_write_fd_timeout(int fd, const void *data, size_t size, ev_tstamp timeout);
 
+/**
+ * Read from a socket in at most @a timeout seconds.
+ * @param fd Socket descriptor.
+ * @param data Data to read.
+ * @param size Size of @a data.
+ * @param timeout Timeout on the operation.
+ *
+ * @retval >= 0 Read bytes (0 treated as EOF).
+ * @retval -1 Timeout or socket error.
+ */
+ssize_t
+coio_read_fd_timeout(int fd, void *data, size_t size, ev_tstamp timeout);
+
 #if defined(__cplusplus)
 } /* extern "C" */
 #endif /* defined(__cplusplus) */
-- 
2.20.1

^ permalink raw reply	[flat|nested] 16+ messages in thread

* [Tarantool-patches] [PATCH v6 2/4] popen: Introduce a backend engine
  2019-12-17 12:54 [Tarantool-patches] [PATCH v6 0/4] popen: Add ability to run external process Cyrill Gorcunov
  2019-12-17 12:54 ` [Tarantool-patches] [PATCH v6 1/4] coio: Export helpers and provide coio_read_fd_timeout Cyrill Gorcunov
@ 2019-12-17 12:54 ` Cyrill Gorcunov
  2019-12-20  8:11   ` Konstantin Osipov
  2019-12-26  7:14   ` Konstantin Osipov
  2019-12-17 12:54 ` [Tarantool-patches] [PATCH v6 3/4] popen/lua: Add popen module Cyrill Gorcunov
  2019-12-17 12:54 ` [Tarantool-patches] [PATCH v6 4/4] popen/test: Add base test cases Cyrill Gorcunov
  3 siblings, 2 replies; 16+ messages in thread
From: Cyrill Gorcunov @ 2019-12-17 12:54 UTC (permalink / raw)
  To: tml

In the patch we introduce popen backend engine which provides
a way to execute external programs and communicate with their
stdin/stdout/stderr streams.

It is possible to run a child process with:

 a) completely closed stdX descriptors
 b) provide /dev/null descriptors to appropritate stdX
 c) pass new transport into a child (currently we use
    pipes for this sake, but may extend to tty/sockets)
 d) inherit stdX from a parent, iow do nothing

On tarantool start we create @popen_pids_map hash which maps
created processes PIDs to popen_handle structure, this structure
keeps everything needed to control and communicate with the children.
The hash will allow us to find a hild process quickly from inside
of a signal handler.

Each handle links into @popen_head list, which is need to be able
to destory children processes on exit procedure (ie when we exit
tarantool and need to cleanup the resources used).

Every new process is born by vfork() call - we can't use fork()
because of at_fork() handlers in libeio which cause deadlocking
in internal mutex usage. Thus the caller waits until vfork()
finishes its work and runs exec (or exit with error).

Because children processes are running without any limitations
they can exit by self or can be killed by some other third side
(say user of a hw node), we need to watch their state which is
done by setting a hook with ev_child_start() helper. This helper
allows us to catch SIGCHLD when a child get exited/signaled
and unregister it from a pool or currently running children.
Note the libev wait() reaps child zomby by self. Another
interesting detail is that libev catches signal in async way
but our SIGCHLD hook is called in sync way before child reap.

This engine provides the following API:
 - popen_init
	to initialize engine
 - popen_free
	to finalize engine and free all reasources
	allocated so far
 - popen_new
	to create a new child process and start it
 - popen_delete
	to release resources occupied and
	terminate a child process
 - popen_stat
	to fetch statistics about a child process
 - popen_command
	to fetch command line string formerly used
	on the popen object creation
 - popen_write_timeout
	to write data into child's stdin with
	timeout
 - popen_read_timeout
	to read data from child's stdout/stderr
	with timeout
 - popen_state
	to fetch state (alive, exited or killed) and
	exit code of a child process
 - popen_state_str
	to get state of a child process in string
	form, for Lua usage mostly
 - popen_send_signal
	to send signal to a child process (for
	example to kill it)

Known issues to fix in next series:

 - environment variables for non-linux systems do not support
   inheritance for now due to lack of testing on my side;

 - for linux base systems we use popen2 system call passing
   O_CLOEXEC flag so that two concurrent popen_create calls
   would not affect each other with pipes inheritance (while
   currently we don't have a case where concurrent calls could
   be done as far as I know, still better to be on a safe side
   from the beginning);

 - there are some files (such as xlog) which tarantool opens
   for own needs without setting O_CLOEXEC flag and it get
   propagated to a children process; for linux based systems
   we use close_inherited_fds helper which walks over opened
   files of a process and close them. But for other targets
   like MachO or FreeBSD this helper just zapped simply because
   I don't have such machines to experimant with; we should
   investigate this moment in more details later once base
   code is merged in;

 - need to consider a case where we will be using piping for
   descriptors (for example we might be writting into stdin
   of a child from another pipe, for this sake we could use
   splice() syscall which gonna be a way faster than copying
   data inside kernel between process). Still the question
   is -- do we really need it? Since we use interanal flags
   in popen handle this should not be a big problem to extend
   this interfaces;

   this particular feature is considered to have a very low
   priority but I left it here just to not forget.

Part-of #4031

Signed-off-by: Cyrill Gorcunov <gorcunov@gmail.com>
---
 src/lib/core/CMakeLists.txt |    1 +
 src/lib/core/popen.c        | 1204 +++++++++++++++++++++++++++++++++++
 src/lib/core/popen.h        |  207 ++++++
 src/main.cc                 |    4 +
 4 files changed, 1416 insertions(+)
 create mode 100644 src/lib/core/popen.c
 create mode 100644 src/lib/core/popen.h

diff --git a/src/lib/core/CMakeLists.txt b/src/lib/core/CMakeLists.txt
index 8623aa0de..3f13ff904 100644
--- a/src/lib/core/CMakeLists.txt
+++ b/src/lib/core/CMakeLists.txt
@@ -15,6 +15,7 @@ set(core_sources
     coio.cc
     coio_task.c
     coio_file.c
+    popen.c
     coio_buf.cc
     fio.c
     exception.cc
diff --git a/src/lib/core/popen.c b/src/lib/core/popen.c
new file mode 100644
index 000000000..d0cf61f35
--- /dev/null
+++ b/src/lib/core/popen.c
@@ -0,0 +1,1204 @@
+#include <stdlib.h>
+#include <string.h>
+#include <unistd.h>
+#include <fcntl.h>
+#include <paths.h>
+#include <signal.h>
+#include <poll.h>
+
+#include <sys/types.h>
+#include <dirent.h>
+#include <sys/wait.h>
+
+#include "popen.h"
+#include "fiber.h"
+#include "assoc.h"
+#include "coio.h"
+#include "say.h"
+
+/* Children pids map popen_handle map */
+static struct mh_i32ptr_t *popen_pids_map = NULL;
+
+/* All popen handles to be able to cleanup them on exit */
+static RLIST_HEAD(popen_head);
+
+/* /dev/null to be used inside children if requested */
+static int dev_null_fd_ro = -1;
+static int dev_null_fd_wr = -1;
+
+/**
+ * popen_register - register popen handle in a pids map
+ * @handle:	handle to register
+ */
+static void
+popen_register(struct popen_handle *handle)
+{
+	struct mh_i32ptr_node_t node = {
+		.key	= handle->pid,
+		.val	= handle,
+	};
+	say_debug("popen: register %d", handle->pid);
+	mh_i32ptr_put(popen_pids_map, &node, NULL, NULL);
+}
+
+/**
+ * popen_find - find popen handler by its pid
+ * @pid:	pid of a handler
+ *
+ * Returns a handle if found or NULL otherwise.
+ */
+static struct popen_handle *
+popen_find(pid_t pid)
+{
+	mh_int_t k = mh_i32ptr_find(popen_pids_map, pid, NULL);
+	if (k == mh_end(popen_pids_map))
+		return NULL;
+	return mh_i32ptr_node(popen_pids_map, k)->val;
+}
+
+/**
+ * popen_unregister - remove popen handler from a pids map
+ * @handle:	handle to remove
+ */
+static void
+popen_unregister(struct popen_handle *handle)
+{
+	struct mh_i32ptr_node_t node = {
+		.key	= handle->pid,
+		.val	= NULL,
+	};
+	say_debug("popen: unregister %d", handle->pid);
+	mh_i32ptr_remove(popen_pids_map, &node, NULL);
+}
+
+/**
+ * command_new - allocates a command string from argv array
+ * @argv:	an array with pointers to argv strings
+ * @nr_argv:	number of elements in the @argv
+ *
+ * Returns a new string or NULL on error.
+ */
+static inline char *
+command_new(char **argv, size_t nr_argv)
+{
+	size_t size = 0, i;
+	char *command;
+	char *pos;
+
+	assert(argv != NULL && nr_argv > 0);
+
+	for (i = 0; i < nr_argv; i++) {
+		if (argv[i] == NULL)
+			continue;
+		size += strlen(argv[i]) + 1;
+	}
+
+	command = malloc(size);
+	if (!command) {
+		say_syserror("popen: Can't allocate command");
+		return NULL;
+	}
+
+	pos = command;
+	for (i = 0; i < nr_argv-1; i++) {
+		if (argv[i] == NULL)
+			continue;
+		strcpy(pos, argv[i]);
+		pos += strlen(argv[i]);
+		*pos++ = ' ';
+	}
+	pos[-1] = '\0';
+
+	return command;
+}
+
+/**
+ * command_free - free memory allocated for command
+ * @command:	pointer to free
+ *
+ * To pair with command_new().
+ */
+static inline void
+command_free(char *command)
+{
+	free(command);
+}
+
+/**
+ * handle_new - allocate new popen hanldle with flags specified
+ * @opts:	pointer to options to be used
+ * @command:	command line string
+ *
+ * Returns pointer to a new initialized popen object
+ * or NULL on error.
+ */
+static struct popen_handle *
+handle_new(struct popen_opts *opts, char *command)
+{
+	struct popen_handle *handle;
+
+	handle = malloc(sizeof(*handle));
+	if (!handle) {
+		say_syserror("popen: Can't allocate handle");
+		return NULL;
+	}
+
+	handle->command	= command;
+	handle->wstatus	= 0;
+	handle->pid	= -1;
+	handle->flags	= opts->flags;
+
+	rlist_create(&handle->list);
+
+	/* all fds to -1 */
+	memset(handle->fds, 0xff, sizeof(handle->fds));
+
+	say_debug("popen: handle %p alloc", handle);
+	return handle;
+}
+
+/**
+ * handle_free - free memory allocated for a handle
+ * @handle:	popen handle to free
+ *
+ * Just to match handle_new().
+ */
+static void
+handle_free(struct popen_handle *handle)
+{
+	say_debug("popen: handle %p free %p", handle);
+	free(handle);
+}
+
+/**
+ * popen_may_io - test if handle can run io operation
+ * @handle:	popen handle
+ * @idx:	index of a file descriptor to operate on
+ * @io_flags:	popen_flag_bits flags
+ *
+ * Returns true if IO is allowed and false otherwise
+ * (setting @errno).
+ */
+static inline bool
+popen_may_io(struct popen_handle *handle, unsigned int idx,
+	     unsigned int io_flags)
+{
+	if (!handle) {
+		errno = ESRCH;
+		return false;
+	}
+
+	if (!(io_flags & handle->flags)) {
+		errno = EINVAL;
+		return false;
+	}
+
+	if (handle->fds[idx] < 0) {
+		errno = EPIPE;
+		return false;
+	}
+
+	return true;
+}
+
+/**
+ * popen_may_pidop - test if handle is valid for pid related operations
+ * @handle:	popen handle
+ *
+ * This is shortcut to test if handle is not nil and still have
+ * a valid child process.
+ *
+ * Returns true if ops are allowed and false otherwise
+ * (setting @errno).
+ */
+static inline bool
+popen_may_pidop(struct popen_handle *handle)
+{
+	if (!handle || handle->pid == -1) {
+		errno = ESRCH;
+		return false;
+	}
+	return true;
+}
+
+/**
+ * popen_stat - fill popen object statistics
+ * @handle:	popen handle
+ * @st:		destination popen_stat to fill
+ *
+ * Returns 0 on succes, -1 otherwise (setting @errno).
+ */
+int
+popen_stat(struct popen_handle *handle, struct popen_stat *st)
+{
+	if (!handle) {
+		errno = ESRCH;
+		return -1;
+	}
+
+	st->pid		= handle->pid;
+	st->flags	= handle->flags;
+
+	static_assert(lengthof(st->fds) == lengthof(handle->fds),
+		      "Statistics fds are screwed");
+
+	memcpy(st->fds, handle->fds, sizeof(st->fds));
+	return 0;
+}
+
+/**
+ * popen_command - get a pointer to former command line
+ * @handle:	popen handle
+ *
+ * Returns pointer to a former command line for executiion.
+ * Since it might be big enough it is moved out of popen_stat.
+ *
+ * On error NULL returned.
+ */
+const char *
+popen_command(struct popen_handle *handle)
+{
+	if (!handle) {
+		errno = ESRCH;
+		return NULL;
+	}
+
+	return (const char *)handle->command;
+}
+
+/**
+ * stdX_str - get stdX descriptor string representation
+ * @index:	descriptor index
+ */
+static inline const char *
+stdX_str(unsigned int index)
+{
+	static const char * stdX_names[] = {
+		[STDIN_FILENO]	= "stdin",
+		[STDOUT_FILENO]	= "stdout",
+		[STDERR_FILENO]	= "stderr",
+	};
+
+	return index < lengthof(stdX_names) ?
+		stdX_names[index] : "unknown";
+}
+
+/**
+ * popen_write - write data to the child stdin
+ * @handle:	popen handle
+ * @buf:	data to write
+ * @count:	number of bytes to write
+ * @flags:	a flag representing stdin peer
+ * @timeout:	timeout in seconds; ignored if negative
+ *
+ * Returns 0 on succes or -1 on error (setting @errno).
+ */
+int
+popen_write_timeout(struct popen_handle *handle, void *buf,
+		    size_t count, unsigned int flags,
+		    ev_tstamp timeout)
+{
+	int idx = STDIN_FILENO;
+
+	if (!popen_may_io(handle, STDIN_FILENO, flags))
+		return -1;
+
+	if (count > (size_t)SSIZE_MAX) {
+		errno = E2BIG;
+		return -1;
+	}
+
+	say_debug("popen: %d: write idx [%s:%d] buf %p count %zu "
+		  "fds %d timeout %.9g",
+		  handle->pid, stdX_str(idx), idx, buf, count,
+		  handle->fds[idx], timeout);
+
+	return coio_write_fd_timeout(handle->fds[idx],
+				     buf, count, timeout);
+}
+
+/**
+ * popen_read_timeout - read data from a child's peer with timeout
+ * @handle:	popen handle
+ * @buf:	destination buffer
+ * @count:	number of bytes to read
+ * @flags:	POPEN_FLAG_FD_STDOUT or POPEN_FLAG_FD_STDERR
+ * @timeout:	timeout in seconds; ignored if negative
+ *
+ * Returns number of bytes read, otherwise -1 returned and
+ * @errno set accordingly.
+ */
+ssize_t
+popen_read_timeout(struct popen_handle *handle, void *buf,
+		   size_t count, unsigned int flags,
+		   ev_tstamp timeout)
+{
+	int idx = flags & POPEN_FLAG_FD_STDOUT ?
+		STDOUT_FILENO : STDERR_FILENO;
+
+	if (!popen_may_io(handle, idx, flags))
+		return -1;
+
+	if (count > (size_t)SSIZE_MAX) {
+		errno = E2BIG;
+		return -1;
+	}
+
+	if (timeout < 0.)
+		timeout = TIMEOUT_INFINITY;
+
+	say_debug("popen: %d: read idx [%s:%d] buf %p count %zu "
+		  "fds %d timeout %.9g",
+		  handle->pid, stdX_str(idx), idx, buf, count,
+		  handle->fds[idx], timeout);
+
+	return coio_read_fd_timeout(handle->fds[idx],
+				    buf, count, timeout);
+}
+
+/**
+ * wstatus_str - encode signal status into human readable form
+ * @buf:	destination buffer
+ * @size:	buffer size
+ * @wstatus:	status to encode
+ *
+ * Operates on S_DEBUG level only simply because snprintf
+ * is pretty heavy in performance, otherwise @buf remains
+ * untouched.
+ *
+ * Returns pointer to @buf with encoded string.
+ */
+static char *
+wstatus_str(char *buf, size_t size, int wstatus)
+{
+	static const char fmt[] =
+		"wstatus %#x exited %d status %d "
+		"signaled %d wtermsig %d "
+		"stopped %d stopsig %d "
+		"coredump %d continued %d";
+
+	assert(size > 0);
+
+	if (say_log_level_is_enabled(S_DEBUG)) {
+		snprintf(buf, size, fmt, wstatus,
+			 WIFEXITED(wstatus),
+			 WIFEXITED(wstatus) ?
+			 WEXITSTATUS(wstatus) : -1,
+			 WIFSIGNALED(wstatus),
+			 WIFSIGNALED(wstatus) ?
+			 WTERMSIG(wstatus) : -1,
+			 WIFSTOPPED(wstatus),
+			 WIFSTOPPED(wstatus) ?
+			 WSTOPSIG(wstatus) : -1,
+			 WCOREDUMP(wstatus),
+			 WIFCONTINUED(wstatus));
+	}
+
+	return buf;
+}
+
+/**
+ * popen_notify_sigchld - notify popen subsistem about SIGCHLD event
+ * @pid:	PID of a process which changed its state
+ * @wstatus:	signal status of a process
+ *
+ * The function is called from global SIGCHLD watcher in libev so
+ * we need to figure out if it is our process which possibly been
+ * terminated.
+ *
+ * Note the libev calls for wait() by self so we don't need
+ * to reap children.
+ */
+static void
+popen_notify_sigchld(pid_t pid, int wstatus)
+{
+	struct popen_handle *handle;
+	char buf[128];
+
+	say_debug("popen: sigchld notify %d (%s)",
+		  pid, wstatus_str(buf, sizeof(buf), wstatus));
+
+	handle = popen_find(pid);
+	if (!handle)
+		return;
+
+	handle->wstatus = wstatus;
+	if (WIFEXITED(wstatus) || WIFSIGNALED(wstatus)) {
+		assert(handle->pid == pid);
+		/*
+		 * libev calls for waitpid by self so
+		 * we don't have to wait here.
+		 */
+		popen_unregister(handle);
+		/*
+		 * Since SIGCHLD may come to us not
+		 * due to exit/kill reason (consider
+		 * a case when someone stopped a child
+		 * process) we should continue wathcing
+		 * state changes, thus we stop monitoring
+		 * dead children only.
+		 */
+		say_debug("popen: ev_child_stop %d", handle->pid);
+		ev_child_stop(EV_DEFAULT_ &handle->ev_sigchld);
+		handle->pid = -1;
+	}
+}
+
+/**
+ * ev_sigchld_cb - handle SIGCHLD from a child process
+ * @w:		a child exited
+ * @revents:	unused
+ */
+static void
+ev_sigchld_cb(EV_P_ ev_child *w, int revents)
+{
+	(void)revents;
+	/*
+	 * Stop watching this child, libev will
+	 * remove it from own hashtable.
+	 */
+	ev_child_stop(EV_A_ w);
+
+	/*
+	 * The reason for a separate helper is that
+	 * we might need to notify more subsystems
+	 * in future.
+	 */
+	popen_notify_sigchld(w->rpid, w->rstatus);
+}
+
+/**
+ * popen_state - get current child state
+ * @handle:	popen handle
+ * @state:	state of a child process
+ * @exit_code:	exit code of a child process
+ *
+ * On success returns 0 sets @state to  POPEN_STATE_
+ * constant and @exit_code. On error -1 returned with @errno.
+ */
+int
+popen_state(struct popen_handle *handle, int *state, int *exit_code)
+{
+	if (!handle) {
+		errno = ESRCH;
+		return -1;
+	}
+
+	if (handle->pid != -1) {
+		*state = POPEN_STATE_ALIVE;
+		*exit_code = 0;
+	} else {
+		if (WIFEXITED(handle->wstatus)) {
+			*state = POPEN_STATE_EXITED;
+			*exit_code = WEXITSTATUS(handle->wstatus);
+		} else {
+			*state = POPEN_STATE_SIGNALED;
+			*exit_code = WTERMSIG(handle->wstatus);
+		}
+	}
+
+	return 0;
+}
+
+/**
+ * popen_state_str - get process state string representation
+ * @state:	process state
+ */
+const char *
+popen_state_str(unsigned int state)
+{
+	/*
+	 * A process may be in a number of states,
+	 * running/sleeping/disk sleep/stopped and etc
+	 * but we are interested only if it is alive
+	 * or dead (via plain exit or kill signal).
+	 *
+	 * Thus while it present in a system and not
+	 * yet reaped we call it "alive".
+	 *
+	 * Note this is API for lua, so change with
+	 * caution if needed.
+	 */
+	static const char * state_str[POPEN_STATE_MAX] = {
+		[POPEN_STATE_NONE]	= "none",
+		[POPEN_STATE_ALIVE]	= "alive",
+		[POPEN_STATE_EXITED]	= "exited",
+		[POPEN_STATE_SIGNALED]	= "signaled",
+	};
+
+	return state < POPEN_STATE_MAX ? state_str[state] : "unknown";
+}
+
+/**
+ * popen_send_signal - send a signal to a child process
+ * @handle:	popen handle
+ * @signo:	signal number
+ *
+ * Returns 0 on success, -1 otherwise.
+ */
+int
+popen_send_signal(struct popen_handle *handle, int signo)
+{
+	int ret;
+
+	/*
+	 * A child may be killed or exited already.
+	 */
+	if (!popen_may_pidop(handle))
+		return -1;
+
+	/*
+	 * SIGKILL and SIGTERM are special, they are
+	 * invoked when need to terminate the whole
+	 * group of children processes.
+	 */
+	if (handle->flags & POPEN_FLAGS_SETSID) {
+		if (signo == SIGKILL || signo == SIGTERM) {
+			say_debug("popen: killpg %d signo %d",
+				  handle->pid, signo);
+			ret = killpg(handle->pid, signo);
+			if (ret < 0) {
+				say_syserror("popen: killpg %d signo %d",
+					     handle->pid, signo);
+			}
+			return ret;
+		}
+	}
+
+	say_debug("popen: kill %d signo %d", handle->pid, signo);
+	ret = kill(handle->pid, signo);
+	if (ret < 0) {
+		say_syserror("popen: kill %d signo %d",
+			     handle->pid, signo);
+	}
+	return ret;
+}
+
+/**
+ * popen_delete - delete a popen handle
+ * @handle:	a popen handle to delete
+ *
+ * The function kills a child process and
+ * close all fds and remove the handle from
+ * a living list and finally frees the handle.
+ *
+ * Returns 0 on success, -1 otherwise.
+ */
+int
+popen_delete(struct popen_handle *handle)
+{
+	size_t i;
+
+	if (!handle) {
+		errno = ESRCH;
+		return -1;
+	}
+
+	if (popen_send_signal(handle, SIGKILL) && errno != ESRCH)
+		return -1;
+
+	for (i = 0; i < lengthof(handle->fds); i++) {
+		if (handle->fds[i] != -1) {
+			/*
+			 * We might did some i/o on
+			 * this fd, so make sure
+			 * libev removed it from
+			 * watching before close.
+			 *
+			 * Calling coio_close on
+			 * fd which never been watched
+			 * if safe.
+			 */
+			coio_close(handle->fds[i]);
+		}
+	}
+
+	/*
+	 * We won't be wathcing this child anymore if
+	 * kill signal is not yet delivered.
+	 */
+	if (handle->pid != -1) {
+		say_debug("popen: ev_child_stop %d", handle->pid);
+		ev_child_stop(EV_DEFAULT_ &handle->ev_sigchld);
+	}
+
+	rlist_del(&handle->list);
+	command_free(handle->command);
+	handle_free(handle);
+	return 0;
+}
+
+/**
+ * make_pipe - create O_CLOEXEC pipe
+ * @pfd:	pipe ends to setup
+ *
+ * Returns 0 on success, -1 on error.
+ */
+static int
+make_pipe(int pfd[2])
+{
+#ifdef TARGET_OS_LINUX
+	if (pipe2(pfd, O_CLOEXEC)) {
+		say_syserror("popen: Can't create pipe2");
+		return -1;
+	}
+#else
+	if (pipe(pfd)) {
+		say_syserror("popen: Can't create pipe");
+		return -1;
+	}
+	if (fcntl(pfd[0], F_SETFL, O_CLOEXEC) ||
+	    fcntl(pfd[1], F_SETFL, O_CLOEXEC)) {
+		int saved_errno = errno;
+		say_syserror("popen: Can't unblock pipe");
+		close(pfd[0]), pfd[0] = -1;
+		close(pfd[1]), pfd[1] = -1;
+		errno = saved_errno;
+		return -1;
+	}
+#endif
+	return 0;
+}
+
+/**
+ * close_inherited_fds - close inherited file descriptors
+ * @skip_fds:		an array of descriptors which should
+ * 			be kept opened
+ * @nr_skip_fds:	number of elements in @skip_fds
+ *
+ * Returns 0 on success, -1 otherwise.
+ */
+static int
+close_inherited_fds(int *skip_fds, size_t nr_skip_fds)
+{
+#ifdef TARGET_OS_LINUX
+	static const char path[] = "/proc/self/fd";
+	struct dirent *de;
+	int fd_no, fd_dir;
+	DIR *dir;
+	size_t i;
+
+	dir = opendir(path);
+	if (!dir) {
+		say_syserror("popen: fdin: Can't open %s", path);
+		return -1;
+	}
+	fd_dir = dirfd(dir);
+
+	for (de = readdir(dir); de; de = readdir(dir)) {
+		if (!strcmp(de->d_name, ".") ||
+		    !strcmp(de->d_name, ".."))
+			continue;
+
+		fd_no = atoi(de->d_name);
+
+		if (fd_no == fd_dir)
+			continue;
+
+		/* We don't expect many numbers here */
+		for (i = 0; i < nr_skip_fds; i++) {
+			if (fd_no == skip_fds[i]) {
+				fd_no = -1;
+				break;
+			}
+		}
+
+		if (fd_no == -1)
+			continue;
+
+		if (close(fd_no)) {
+			int saved_errno = errno;
+			say_syserror("popen: fdin: Can't close %d", fd_no);
+			closedir(dir);
+			errno = saved_errno;
+			return -1;
+		}
+	}
+
+	if (closedir(dir)) {
+		say_syserror("popen: fdin: Can't close %s", path);
+		return -1;
+	}
+#else
+	/* FIXME: What about FreeBSD/MachO? */
+	(void)skip_fds;
+	(void)nr_skip_fds;
+
+	static bool said = false;
+	if (!said) {
+		say_warn("popen: fdin: Skip closing inherited");
+		said = true;
+	}
+#endif
+	return 0;
+}
+
+#ifdef TARGET_OS_LINUX
+extern char **environ;
+#endif
+
+static inline char **
+get_envp(struct popen_opts *opts)
+{
+	if (!opts->env) {
+#ifdef TARGET_OS_LINUX
+		return environ;
+#else
+		static const char **empty_envp[] = { NULL };
+		static bool said = false;
+		if (!said)
+			say_warn("popen: Environment inheritance "
+				 "unsupported, passing empty");
+		return (char *)empty_envp;
+#endif
+	}
+	return opts->env;
+}
+
+/**
+ * signal_reset - reset signals to default before executing a program
+ *
+ * FIXME: This is a code duplication fomr main.cc. Need to rework
+ * signal handing otherwise it will become utter crap very fast.
+ */
+static void
+signal_reset(void)
+{
+	struct sigaction sa = { };
+	sigset_t sigset;
+
+	/* Reset all signals to their defaults. */
+	sigemptyset(&sa.sa_mask);
+	sa.sa_handler = SIG_DFL;
+
+	if (sigaction(SIGUSR1, &sa, NULL) == -1 ||
+	    sigaction(SIGINT, &sa, NULL) == -1 ||
+	    sigaction(SIGTERM, &sa, NULL) == -1 ||
+	    sigaction(SIGHUP, &sa, NULL) == -1 ||
+	    sigaction(SIGWINCH, &sa, NULL) == -1 ||
+	    sigaction(SIGSEGV, &sa, NULL) == -1 ||
+	    sigaction(SIGFPE, &sa, NULL) == -1)
+		_exit(errno);
+
+	/* Unblock any signals blocked by libev */
+	sigfillset(&sigset);
+	if (sigprocmask(SIG_UNBLOCK, &sigset, NULL) == -1)
+		_exit(errno);
+}
+
+/**
+ * popen_new - Create new popen handle
+ * @opts: options for popen handle
+ *
+ * This function creates a new child process and passes it
+ * pipe ends to communicate with child's stdin/stdout/stderr
+ * depending on @opts:flags. Where @opts:flags could be
+ * the bitwise "OR" for the following values:
+ *
+ *  POPEN_FLAG_FD_STDIN		- to write to stdin
+ *  POPEN_FLAG_FD_STDOUT	- to read from stdout
+ *  POPEN_FLAG_FD_STDERR	- to read from stderr
+ *
+ * When need to pass /dev/null descriptor into a child
+ * the following values can be used:
+ *
+ *  POPEN_FLAG_FD_STDIN_DEVNULL
+ *  POPEN_FLAG_FD_STDOUT_DEVNULL
+ *  POPEN_FLAG_FD_STDERR_DEVNULL
+ *
+ * These flags have no effect if appropriate POPEN_FLAG_FD_STDx
+ * flags are set.
+ *
+ * When need to completely close the descriptors the
+ * following values can be used:
+ *
+ *  POPEN_FLAG_FD_STDIN_CLOSE
+ *  POPEN_FLAG_FD_STDOUT_CLOSE
+ *  POPEN_FLAG_FD_STDERR_CLOSE
+ *
+ * These flags have no effect if appropriate POPEN_FLAG_FD_STDx
+ * flags are set.
+ *
+ * If none of POPEN_FLAG_FD_STDx flags are specified the child
+ * process will run with all files inherited from a parent.
+ *
+ * Returns pointer to a new popen handle on success,
+ * otherwise NULL returned setting @errno.
+ */
+struct popen_handle *
+popen_new(struct popen_opts *opts)
+{
+	struct popen_handle *handle = NULL;
+	char *command = NULL;
+
+	int pfd[POPEN_FLAG_FD_STDEND_BIT][2] = {
+		{-1, -1}, {-1, -1}, {-1, -1},
+	};
+
+	char **envp = get_envp(opts);
+	int saved_errno;
+	size_t i;
+
+	static const struct {
+		unsigned int	mask;
+		unsigned int	mask_devnull;
+		unsigned int	mask_close;
+		int		fileno;
+		int		*dev_null_fd;
+		int		parent_idx;
+		int		child_idx;
+		bool		nonblock;
+	} pfd_map[POPEN_FLAG_FD_STDEND_BIT] = {
+		{
+			.mask		= POPEN_FLAG_FD_STDIN,
+			.mask_devnull	= POPEN_FLAG_FD_STDIN_DEVNULL,
+			.mask_close	= POPEN_FLAG_FD_STDIN_CLOSE,
+			.fileno		= STDIN_FILENO,
+			.dev_null_fd	= &dev_null_fd_ro,
+			.parent_idx	= 1,
+			.child_idx	= 0,
+		}, {
+			.mask		= POPEN_FLAG_FD_STDOUT,
+			.mask_devnull	= POPEN_FLAG_FD_STDOUT_DEVNULL,
+			.mask_close	= POPEN_FLAG_FD_STDOUT_CLOSE,
+			.fileno		= STDOUT_FILENO,
+			.dev_null_fd	= &dev_null_fd_wr,
+			.parent_idx	= 0,
+			.child_idx	= 1,
+		}, {
+			.mask		= POPEN_FLAG_FD_STDERR,
+			.mask_devnull	= POPEN_FLAG_FD_STDERR_DEVNULL,
+			.mask_close	= POPEN_FLAG_FD_STDERR_CLOSE,
+			.fileno		= STDERR_FILENO,
+			.dev_null_fd	= &dev_null_fd_wr,
+			.parent_idx	= 0,
+			.child_idx	= 1,
+		},
+	};
+	/*
+	 * At max we could be skipping each pipe end
+	 * plus dev/null variants.
+	 */
+	int skip_fds[POPEN_FLAG_FD_STDEND_BIT * 2 + 2];
+	size_t nr_skip_fds = 0;
+
+	/*
+	 * A caller must preserve space for this.
+	 */
+	if (opts->flags & POPEN_FLAG_SHELL) {
+		opts->argv[0] = "sh";
+		opts->argv[1] = "-c";
+	}
+
+	/*
+	 * Carry a copy for information sake.
+	 */
+	command = command_new(opts->argv, opts->nr_argv);
+
+	say_debug("popen: command '%s' flags %#x", command, opts->flags);
+
+	static_assert(STDIN_FILENO == 0 &&
+		      STDOUT_FILENO == 1 &&
+		      STDERR_FILENO == 2,
+		      "stdin/out/err are not posix compatible");
+
+	static_assert(lengthof(pfd) == lengthof(pfd_map),
+		      "Pipes number does not map to fd bits");
+
+	static_assert(POPEN_FLAG_FD_STDIN_BIT == STDIN_FILENO &&
+		      POPEN_FLAG_FD_STDOUT_BIT == STDOUT_FILENO &&
+		      POPEN_FLAG_FD_STDERR_BIT == STDERR_FILENO,
+		      "Popen flags do not match stdX");
+
+	handle = handle_new(opts, command);
+	if (!handle)
+		return NULL;
+
+	skip_fds[nr_skip_fds++] = dev_null_fd_ro;
+	skip_fds[nr_skip_fds++] = dev_null_fd_wr;
+	assert(nr_skip_fds <= lengthof(skip_fds));
+
+	for (i = 0; i < lengthof(pfd_map); i++) {
+		if (opts->flags & pfd_map[i].mask) {
+			if (make_pipe(pfd[i]))
+				goto out_err;
+
+			/*
+			 * FIXME: Rather force make_pipe
+			 * to allocate new fds with higher
+			 * number.
+			 */
+			if (pfd[i][0] <= STDERR_FILENO ||
+			    pfd[i][1] <= STDERR_FILENO) {
+				say_error("popen: low fds [%s:%d:%d]",
+					  stdX_str(i), pfd[i][0],
+					  pfd[i][1]);
+				errno = EBADF;
+				goto out_err;
+			}
+
+			skip_fds[nr_skip_fds++] = pfd[i][0];
+			skip_fds[nr_skip_fds++] = pfd[i][1];
+			assert(nr_skip_fds <= lengthof(skip_fds));
+
+			say_debug("popen: created pipe [%s:%d:%d]",
+				  stdX_str(i), pfd[i][0], pfd[i][1]);
+		} else if (!(opts->flags & pfd_map[i].mask_devnull) &&
+			   !(opts->flags & pfd_map[i].mask_close)) {
+			skip_fds[nr_skip_fds++] = pfd_map[i].fileno;
+
+			say_debug("popen: inherit [%s:%d]",
+				  stdX_str(i), pfd_map[i].fileno);
+		}
+	}
+
+	/*
+	 * We have to use vfork here because libev has own
+	 * at_fork helpers with mutex, so we will have double
+	 * lock here and stuck forever otherwise.
+	 *
+	 * The good news that this affect tx only the
+	 * other tarantoll threads are not waiting for
+	 * vfork to complete. Also we try to do as minimum
+	 * operations before the exec() as possible.
+	 */
+	handle->pid = vfork();
+	if (handle->pid < 0) {
+		goto out_err;
+	} else if (handle->pid == 0) {
+		/*
+		 * The documentation for libev says that
+		 * each new fork should call ev_loop_fork(EV_DEFAULT)
+		 * on every new child process, but we're going
+		 * to execute bew binary anyway thus everything
+		 * related to OS resources will be eliminated except
+		 * file descriptors we use for piping. Thus don't
+		 * do anything special.
+		 */
+
+		/*
+		 * Also don't forget to drop signal handlers
+		 * to default inside a child process since we're
+		 * inheriting them from a caller process.
+		 */
+		if (opts->flags & POPEN_FLAGS_RESTORE_SIGNALS)
+			signal_reset();
+
+		/*
+		 * We have to be a session leader otherwise
+		 * won't be able to kill a group of children.
+		 */
+		if (opts->flags & POPEN_FLAGS_SETSID) {
+			if (setsid() == -1)
+				goto exit_child;
+		}
+
+		if (opts->flags & POPEN_FLAG_CLOSE_FDS) {
+			if (close_inherited_fds(skip_fds, nr_skip_fds))
+				goto exit_child;
+		}
+
+		for (i = 0; i < lengthof(pfd_map); i++) {
+			int fileno = pfd_map[i].fileno;
+			/*
+			 * Pass pipe peer to a child.
+			 */
+			if (opts->flags & pfd_map[i].mask) {
+				int child_idx = pfd_map[i].child_idx;
+
+				/* put child peer end at known place */
+				if (dup2(pfd[i][child_idx], fileno) < 0)
+					goto exit_child;
+
+				/* parent's pipe no longer needed */
+				if (close(pfd[i][0]) ||
+				    close(pfd[i][1]))
+					goto exit_child;
+				continue;
+			}
+
+			/*
+			 * Use /dev/null if requested.
+			 */
+			if (opts->flags & pfd_map[i].mask_devnull) {
+				if (dup2(*pfd_map[i].dev_null_fd,
+					 fileno) < 0) {
+					goto exit_child;
+				}
+				continue;
+			}
+
+			/*
+			 * Or close the destination completely, since
+			 * we don't if the file in question is already
+			 * closed by some other code we don't care if
+			 * EBADF happens.
+			 */
+			if (opts->flags & pfd_map[i].mask_close) {
+				if (close(fileno) && errno != EBADF)
+					goto exit_child;
+				continue;
+			}
+
+			/*
+			 * Otherwise inherit file descriptor
+			 * from a parent.
+			 */
+		}
+
+		if (close(dev_null_fd_ro) || close(dev_null_fd_wr))
+			goto exit_child;
+
+		if (opts->flags & POPEN_FLAG_SHELL)
+			execve(_PATH_BSHELL, opts->argv, envp);
+		else
+			execve(opts->argv[2], &opts->argv[2], envp);
+exit_child:
+		_exit(errno);
+		unreachable();
+	}
+
+	for (i = 0; i < lengthof(pfd_map); i++) {
+		if (opts->flags & pfd_map[i].mask) {
+			int parent_idx = pfd_map[i].parent_idx;
+			int child_idx = pfd_map[i].child_idx;
+
+			handle->fds[i] = pfd[i][parent_idx];
+			if (fcntl(handle->fds[i], F_SETFL, O_NONBLOCK)) {
+				say_syserror("popen: nonblock [%s:%d]",
+					     stdX_str(i), handle->fds[i]);
+				goto out_err;
+			}
+
+			say_debug("popen: keep pipe [%s:%d]",
+				  stdX_str(i), handle->fds[i]);
+
+			if (close(pfd[i][child_idx])) {
+				say_syserror("popen: close child [%s:%d]",
+					     stdX_str(i),
+					     pfd[i][child_idx]);
+				goto out_err;
+			}
+
+			pfd[i][child_idx] = -1;
+		}
+	}
+
+	/*
+	 * Link it into global list for force
+	 * cleanup on exit.
+	 */
+	rlist_add(&popen_head, &handle->list);
+
+	/*
+	 * To watch when a child get exited.
+	 */
+	popen_register(handle);
+
+	say_debug("popen: ev_child_start %d", handle->pid);
+	ev_child_init(&handle->ev_sigchld, ev_sigchld_cb, handle->pid, 0);
+	ev_child_start(EV_DEFAULT_ &handle->ev_sigchld);
+
+	say_debug("popen: created child %d", handle->pid);
+
+	return handle;
+
+out_err:
+	saved_errno = errno;
+	popen_delete(handle);
+	for (i = 0; i < lengthof(pfd); i++) {
+		if (pfd[i][0] != -1)
+			close(pfd[i][0]);
+		if (pfd[i][1] != -1)
+			close(pfd[i][1]);
+	}
+	errno = saved_errno;
+	return NULL;
+}
+
+/**
+ * popen_init - initialize popen subsystem
+ *
+ * Allocates resource needed for popen management.
+ */
+void
+popen_init(void)
+{
+	static const int flags = O_CLOEXEC;
+	static const char dev_null_path[] = "/dev/null";
+
+	say_debug("popen: initialize subsystem");
+	popen_pids_map = mh_i32ptr_new();
+
+	dev_null_fd_ro = open(dev_null_path, O_RDONLY | flags);
+	if (dev_null_fd_ro < 0)
+		goto out_err;
+	dev_null_fd_wr = open(dev_null_path, O_WRONLY | flags);
+	if (dev_null_fd_wr < 0)
+		goto out_err;
+
+	/*
+	 * FIXME: We should allocate them somewhere
+	 * after STDERR_FILENO so the child would be
+	 * able to find these fd numbers not occupied.
+	 * Other option is to use unix scm and send
+	 * them to the child on demand.
+	 *
+	 * For now left as is since we don't close
+	 * our main stdX descriptors and they are
+	 * inherited when we call first vfork.
+	 */
+	if (dev_null_fd_ro <= STDERR_FILENO ||
+	    dev_null_fd_wr <= STDERR_FILENO) {
+		say_error("popen: /dev/null %d %d numbers are too low",
+			  dev_null_fd_ro, dev_null_fd_wr);
+		goto out_err;
+	}
+
+	return;
+
+out_err:
+	say_syserror("popen: Can't open %s", dev_null_path);
+	if (dev_null_fd_ro >= 0)
+		close(dev_null_fd_ro);
+	if (dev_null_fd_wr >= 0)
+		close(dev_null_fd_wr);
+	mh_i32ptr_delete(popen_pids_map);
+	exit(EXIT_FAILURE);
+}
+
+/**
+ * popen_free - free popen subsystem
+ *
+ * Kills all running children and frees resources.
+ */
+void
+popen_free(void)
+{
+	struct popen_handle *handle, *tmp;
+
+	say_debug("popen: free subsystem");
+
+	close(dev_null_fd_ro);
+	close(dev_null_fd_wr);
+	dev_null_fd_ro = -1;
+	dev_null_fd_wr = -1;
+
+	rlist_foreach_entry_safe(handle, &popen_head, list, tmp) {
+		/*
+		 * If children are still running we should move
+		 * them out of the pool and kill them all then.
+		 * Note though that we don't do an explicit wait
+		 * here, OS will reap them anyway, just release
+		 * the memory occupied for popen handles.
+		 */
+		if (popen_may_pidop(handle))
+			popen_unregister(handle);
+		popen_delete(handle);
+	}
+
+	if (popen_pids_map) {
+		mh_i32ptr_delete(popen_pids_map);
+		popen_pids_map = NULL;
+	}
+}
diff --git a/src/lib/core/popen.h b/src/lib/core/popen.h
new file mode 100644
index 000000000..3199eb55a
--- /dev/null
+++ b/src/lib/core/popen.h
@@ -0,0 +1,207 @@
+#ifndef TARANTOOL_LIB_CORE_POPEN_H_INCLUDED
+#define TARANTOOL_LIB_CORE_POPEN_H_INCLUDED
+
+#if defined(__cplusplus)
+extern "C" {
+#endif
+
+#include <signal.h>
+#include <errno.h>
+#include <stdbool.h>
+#include <sys/types.h>
+
+#include <small/rlist.h>
+
+#include "trivia/util.h"
+#include "third_party/tarantool_ev.h"
+
+/*
+ * Describes popen object creation. This is API with Lua.
+ */
+enum popen_flag_bits {
+	POPEN_FLAG_NONE			= (0 << 0),
+
+	/*
+	 * Which fd we should handle as new pipes.
+	 */
+	POPEN_FLAG_FD_STDIN_BIT		= 0,
+	POPEN_FLAG_FD_STDIN		= (1 << POPEN_FLAG_FD_STDIN_BIT),
+
+	POPEN_FLAG_FD_STDOUT_BIT	= 1,
+	POPEN_FLAG_FD_STDOUT		= (1 << POPEN_FLAG_FD_STDOUT_BIT),
+
+	POPEN_FLAG_FD_STDERR_BIT	= 2,
+	POPEN_FLAG_FD_STDERR		= (1 << POPEN_FLAG_FD_STDERR_BIT),
+
+	/*
+	 * Number of bits occupied for stdX descriptors.
+	 */
+	POPEN_FLAG_FD_STDEND_BIT	= POPEN_FLAG_FD_STDERR_BIT + 1,
+
+	/*
+	 * Instead of inheriting fds from a parent
+	 * rather use /dev/null.
+	 */
+	POPEN_FLAG_FD_STDIN_DEVNULL_BIT	= 3,
+	POPEN_FLAG_FD_STDIN_DEVNULL	= (1 << POPEN_FLAG_FD_STDIN_DEVNULL_BIT),
+	POPEN_FLAG_FD_STDOUT_DEVNULL_BIT= 4,
+	POPEN_FLAG_FD_STDOUT_DEVNULL	= (1 << POPEN_FLAG_FD_STDOUT_DEVNULL_BIT),
+	POPEN_FLAG_FD_STDERR_DEVNULL_BIT= 5,
+	POPEN_FLAG_FD_STDERR_DEVNULL	= (1 << POPEN_FLAG_FD_STDERR_DEVNULL_BIT),
+
+	/*
+	 * Instead of inheriting fds from a parent
+	 * close fds completely.
+	 */
+	POPEN_FLAG_FD_STDIN_CLOSE_BIT	= 6,
+	POPEN_FLAG_FD_STDIN_CLOSE	= (1 << POPEN_FLAG_FD_STDIN_CLOSE_BIT),
+	POPEN_FLAG_FD_STDOUT_CLOSE_BIT	= 7,
+	POPEN_FLAG_FD_STDOUT_CLOSE	= (1 << POPEN_FLAG_FD_STDOUT_CLOSE_BIT),
+	POPEN_FLAG_FD_STDERR_CLOSE_BIT	= 8,
+	POPEN_FLAG_FD_STDERR_CLOSE	= (1 << POPEN_FLAG_FD_STDERR_CLOSE_BIT),
+
+	/*
+	 * Reserved for a case where we will handle
+	 * other fds as a source for stdin/out/err.
+	 * Ie piping from child process to our side
+	 * via splices and etc.
+	 */
+	POPEN_FLAG_FD_STDIN_EPIPE_BIT	= 9,
+	POPEN_FLAG_FD_STDIN_EPIPE	= (1 << POPEN_FLAG_FD_STDIN_EPIPE_BIT),
+	POPEN_FLAG_FD_STDOUT_EPIPE_BIT	= 10,
+	POPEN_FLAG_FD_STDOUT_EPIPE	= (1 << POPEN_FLAG_FD_STDOUT_EPIPE_BIT),
+	POPEN_FLAG_FD_STDERR_EPIPE_BIT	= 11,
+	POPEN_FLAG_FD_STDERR_EPIPE	= (1 << POPEN_FLAG_FD_STDERR_EPIPE_BIT),
+
+	/*
+	 * Call exec directly or via shell.
+	 */
+	POPEN_FLAG_SHELL_BIT		= 12,
+	POPEN_FLAG_SHELL		= (1 << POPEN_FLAG_SHELL_BIT),
+
+	/*
+	 * Create a new session.
+	 */
+	POPEN_FLAGS_SETSID_BIT		= 13,
+	POPEN_FLAGS_SETSID		= (1 << POPEN_FLAGS_SETSID_BIT),
+
+	/*
+	 * Close all inherited fds except stdin/out/err.
+	 */
+	POPEN_FLAG_CLOSE_FDS_BIT	= 14,
+	POPEN_FLAG_CLOSE_FDS		= (1 << POPEN_FLAG_CLOSE_FDS_BIT),
+
+	/*
+	 * Restore signal handlers to default.
+	 */
+	POPEN_FLAGS_RESTORE_SIGNALS_BIT	= 15,
+	POPEN_FLAGS_RESTORE_SIGNALS	= (1 << POPEN_FLAGS_RESTORE_SIGNALS_BIT),
+};
+
+/*
+ * Popen object states. This is API with Lua.
+ */
+enum popen_states {
+	POPEN_STATE_NONE		= 0,
+	POPEN_STATE_ALIVE		= 1,
+	POPEN_STATE_EXITED		= 2,
+	POPEN_STATE_SIGNALED		= 3,
+
+	POPEN_STATE_MAX,
+};
+
+/**
+ * struct popen_handle - an instance of popen object
+ *
+ * @pid:	pid of a child process
+ * @command:	copy of a command line for statistics
+ * @wstatus:	exit status of a child process
+ * @ev_sigchld:	needed by the libev to watch children
+ * @flags:	popen_flag_bits
+ * @fds:	std(in|out|err)
+ */
+struct popen_handle {
+	pid_t			pid;
+	char			*command;
+	int			wstatus;
+	ev_child		ev_sigchld;
+	struct rlist		list;
+	unsigned int		flags;
+	int			fds[POPEN_FLAG_FD_STDEND_BIT];
+};
+
+/**
+ * struct popen_opts - options for popen creation
+ *
+ * @argv:	argv to execute
+ * @nr_argv:	number of elements in @argv
+ * @env:	environment (can be empty)
+ * @flags:	popen_flag_bits
+ */
+struct popen_opts {
+	char			**argv;
+	size_t			nr_argv;
+	char			**env;
+	unsigned int		flags;
+};
+
+/**
+ * struct popen_stat - popen object statistics
+ *
+ * @pid:	pid of a child process
+ * @flags:	popen_flag_bits
+ * @fds:	std(in|out|err)
+ *
+ * This is a short version of struct popen_handle which should
+ * be used by external code and which should be changed/extended
+ * with extreme caution since it is used in Lua code. Consider it
+ * as API for external modules.
+ */
+struct popen_stat {
+	pid_t			pid;
+	unsigned int		flags;
+	int			fds[POPEN_FLAG_FD_STDEND_BIT];
+};
+
+extern int
+popen_stat(struct popen_handle *handle, struct popen_stat *st);
+
+extern const char *
+popen_command(struct popen_handle *handle);
+
+extern int
+popen_write_timeout(struct popen_handle *handle, void *buf,
+		    size_t count, unsigned int flags,
+		    ev_tstamp timeout);
+
+extern ssize_t
+popen_read_timeout(struct popen_handle *handle, void *buf,
+		   size_t count, unsigned int flags,
+		   ev_tstamp timeout);
+
+extern int
+popen_state(struct popen_handle *handle, int *state, int *exit_code);
+
+extern const char *
+popen_state_str(unsigned int state);
+
+extern int
+popen_send_signal(struct popen_handle *handle, int signo);
+
+extern int
+popen_delete(struct popen_handle *handle);
+
+extern struct popen_handle *
+popen_new(struct popen_opts *opts);
+
+extern void
+popen_init(void);
+
+extern void
+popen_free(void);
+
+#if defined(__cplusplus)
+} /* extern "C" */
+#endif
+
+#endif /* TARANTOOL_LIB_CORE_POPEN_H_INCLUDED */
diff --git a/src/main.cc b/src/main.cc
index e674d85b1..8df94c9e4 100644
--- a/src/main.cc
+++ b/src/main.cc
@@ -77,6 +77,7 @@
 #include "box/session.h"
 #include "systemd.h"
 #include "crypto/crypto.h"
+#include "core/popen.h"
 
 static pid_t master_pid = getpid();
 static struct pidfh *pid_file_handle;
@@ -642,6 +643,8 @@ tarantool_free(void)
 
 	title_free(main_argc, main_argv);
 
+	popen_free();
+
 	/* unlink pidfile. */
 	if (pid_file_handle != NULL && pidfile_remove(pid_file_handle) == -1)
 		say_syserror("failed to remove pid file '%s'", pid_file);
@@ -819,6 +822,7 @@ main(int argc, char **argv)
 	exception_init();
 
 	fiber_init(fiber_cxx_invoke);
+	popen_init();
 	coio_init();
 	coio_enable();
 	signal_init();
-- 
2.20.1

^ permalink raw reply	[flat|nested] 16+ messages in thread

* [Tarantool-patches] [PATCH v6 3/4] popen/lua: Add popen module
  2019-12-17 12:54 [Tarantool-patches] [PATCH v6 0/4] popen: Add ability to run external process Cyrill Gorcunov
  2019-12-17 12:54 ` [Tarantool-patches] [PATCH v6 1/4] coio: Export helpers and provide coio_read_fd_timeout Cyrill Gorcunov
  2019-12-17 12:54 ` [Tarantool-patches] [PATCH v6 2/4] popen: Introduce a backend engine Cyrill Gorcunov
@ 2019-12-17 12:54 ` Cyrill Gorcunov
  2019-12-20 15:41   ` Maxim Melentiev
  2019-12-17 12:54 ` [Tarantool-patches] [PATCH v6 4/4] popen/test: Add base test cases Cyrill Gorcunov
  3 siblings, 1 reply; 16+ messages in thread
From: Cyrill Gorcunov @ 2019-12-17 12:54 UTC (permalink / raw)
  To: tml

@TarantoolBot document
Title: popen module

Overview
========

Tarantool supports execution of external programs similarly
to well known Python's `subprocess` or Ruby's `Open3`. Note
though the `popen` module does not match one to one to the
helpers these languages provide and provides only basic
functions. The popen object creation is implemented via
`vfork()` system call which means the caller thread is
blocked until execution of a child process begins.

Functions
=========

The `popen` module provides two functions to create that named
popen object `popen.posix` which is the similar to libc `posix`
syscall and `popen.popen` to create popen object with more
specific options.

`handler, err = popen.posix(command, mode)`
-------------------------------------------

Creates a new child process and executes a command as the "sh -c command".

Parameters
```
command     is the command to run

mode        'r' - to grab child's stdout for read
            'w' - to write into child's stdin stream
            'rw' - combined mode to read and write
```

If `mode` has `r` then `stderr` output stream is always opened to be
able to redirect `stderr` to `stdout` by well known `2>&1` statement.
If `mode` is not specified at all then non of the std stram will be
opened inside a child process.

On success returns popen object handler setting `err` to nil,
otherwise error reported.

__Example 1__
```
ph = require('popen').posix("date", "r")
ph:read()
ph:close()
```

Executes "sh -c date" then reads the output and closes the popen object.

`handler, err = popen.popen(opts)`
----------------------------------

Creates a new child process and execute a program inside. The `opts` is
options table with the following keys

```
argv        an array containing the binary to execute together
            with command line arguments; this is the only mandatory
            key in the options table

env         an array of environment variables, if not set then they
            are inherited; if env=nil then environment will be empty

flags       a dictionary to describe communication pipes and other
            parameters of a process to run

            stdin=true
                to write into STDIN_FILENO of a child process
            stdin=false
                to close STDIN_FILENO inside a child process, default
            stdin="devnull"
                a child will get /dev/null as STDIN_FILENO

            stdout=true
                to read STDOUT_FILENO of a child process
            stdout=false
                to close STDOUT_FILENO inside a child process, default
            stdout="devnull"
                a child will get /dev/null as STDOUT_FILENO

            stderr=true
                to read STDERR_FILENO of a child process
            stderr=false
                to close STDERR_FILENO inside a child process, default
            stderr="devnull"
                a child will get /dev/null as STDERR_FILENO

            shell=true
                runs a child process via "sh -c", default
            shell=false
                invokes a child process executable directly

            close_fds=true
                close all inherited fds from a parent, default

            restore_signals=true
                all signals modified by a caller reset
                to default handler, default

            start_new_session=true
                start executable inside a new session, default
```

On success returns popen object handler setting `err` to nil,
otherwise error reported.

__Example 2__
```
ph, err = require('popen').popen({argv = {"/usr/bin/echo", "-n", "hello"},
                                 flags = {stdout=true, shell=false}})
```

`str, err = popen:read([stderr=true|false,timeout])`
----------------------------------------------------

Reads data from `stdout` or `stderr` streams with `timeout`. By default
it reads from `stdout` stream, to read from `stderr` stream set
`stderr=true`.

On success returns string `str` read and `err=nil`, otherwise `err ~= nil`.

__Example 3__
```
ph = require('popen').popen({argv = {"/usr/bin/echo", "-n", "hello"},
                            flags = {stdout=true, shell=false}})
ph:read()
---
- hello
```

`err = popen:write(str[,timeout])`
----------------------------------

Writes string `str` to `stdin` stream of a child process. Since the internal
buffer on the reader side is limited in size and depends on OS settings the `write`
may hang forever if buffer is full. For this wake `timeout` (float number) parameter
allows to specify number of seconds which once elapsed interrupt `write` attempts
and exit from this function. When not provided the `write` blocks until data is
written or error happened.

On success returns `err = nil`, otherwise `err ~= nil`.

__Example 4__
```
script = "prompt=''; read -n 5 prompt && echo -n $prompt;"
ph = require('popen').posix(script, "rw")
ph:write("12345")
ph:read()
---
- '12345'
```

`err = popen:write2(opts)`
--------------------------

Writes data from a buffer to a stream of a child process. `opts` is
a table of options with the following keys

```
buf     buffer with raw data to write
size    number of bytes to write
flags   dictionary to specify the input stream
        stdin=true
            to write into stdin stream
timeout write timeout in seconds, optional
```

Basically the `popen:write` is a wrapper over `popen:write2` call
with `opts` similar to `{timeout = -1, {flags = {stdin = true}}`.

On success `err = nil`, otherwise `err ~= nil`.

The reason for passing `{flags = {stdin = true}}` while we have only
one `stdin` is that we reserve the interface for future where we
might extend it without breaking backward compatibility.

`bytes, err = popen:read2(opts)`
--------------------------------

Reads data from a stream of a child process to a buffer. `opts` is
a table of options with the following keys

```
buf     destination buffer
size    number of bytes to read
flags   dictionary to specify the input stream
        stdout=true
            to read from stdout stream
        stderr=true
            to read from stderr stream
timeout read timeout in seconds, optional
```

Basically the `popen:read` is a wrapper over `popen:read2` call
with `opts` similar to `{timeout = -1, {flags = {stdout = true}}`.

On success returns number of `bytes` read and `err = nil`,
otherwise `err ~= nil`.

__Example 5__
```
buffer = require('buffer')
popen = require('popen')
ffi = require('ffi')

script = "echo -n 1 2 3 4 5 1>&2"
buf = buffer.ibuf()

ph = popen.posix(script, "rw")
ph:wait()
size = 128
dst = buf:reserve(size)
res, err = ph:read2({buf = dst, size = size, nil, flags = {stderr = true}})
res = buf:alloc(res)
ffi.string(buf.rpos, buf:size())
---
- 1 2 3 4 5
...
buf:recycle()
ph:close()
```

`res, err = popen:terminate()`
-----------------------------

Terminates a child process in a soft way sending `SIGTERM` signal.
On success returns `res = true, err = nil`, `err ~= nil` otherwise.

`res, err = popen:kill()`
-------------------------

Terminates a child process in a soft way sending `SIGKILL` signal.
On success returns `res = true, err = nil`, `err ~= nil` otherwise.

`res, err = popen:send_signal(signo)`
-------------------------------------

Sends signal with number `signo` to a child process.
On success returns `res = true, err = nil`, `err ~= nil`
otherwise.

`info, err = popen:info()`
--------------------------

Returns information about a child process in form of table
with the following keys

```
pid         PID of a child process, if already terminated
            the it set to -1

flags       flags been used to create the popen object; this
            is bitmap of popen.c.flag constants

stdout      file descriptor number associated with stdout
            stream on a parent side, -1 if not set

stderr      file descriptor number associated with stderr
            stream on a parent side, -1 if not set

stdint      file descriptor number associated with stdin
            stream on a parent side, -1 if not set

state       state of a child proces: "alive" if process
            is running, "exited" if finished execution,
            "signaled" is terminated by a signal

exit_code   if process is in "signaled" state then this
            number represent the signal number process
            been terminated with, if process is in "exited"
            state then it represent the exit code
```

__Example 6__
```
ph = require('popen').posix("echo -n 1 2 3 4 5 1>&2", "r")
ph:info()
---
- stdout: 12
  command: sh -c echo -n 1 2 3 4 5 1>&2
  stderr: 23
  flags: 61510
  state: exited
  stdin: -1
  exit_code: 0
  pid: -1
```

`err, state, exit_code = popen:state()`
--------------------------------------

Returns the current state of a chile process.

On success returns `err = nil`, `state` is one of the
constants `popen.c.state.ALIVE=1` when process is running,
`popen.c.state.EXITED=2` when it exited by self or
`popen.c.state.SIGNALED=3` when terminated via signal.
In case if the child process is terminated by a signal
the `exit_code` contains the signal number, otherwise
it is an error code the child set by self.

__Example 7__
```
ph = require('popen').posix("echo -n 1 2 3 4 5 1>&2", "r")
ph:state()
---
- null
- 2
- 0
```

`err, state, exit_code = popen:wait()`
--------------------------------------

Waits until a child process get exited or terminated.
Returns the same data as `popen:state()`. Basically
the `popen:wait()` is simply polling for the child state
with `popen:wait()` in a cycle.

`err = popen:close`
-------------------

Closes popen object releasing all resources occupied.

If a child process is running then it get killed first. Once
popen object is closed it no longed usable and any attempt
to call function over will cause an error.

On success `err = nil`.

Part-of #4031

Signed-off-by: Cyrill Gorcunov <gorcunov@gmail.com>
---
 src/CMakeLists.txt |   2 +
 src/lua/init.c     |   4 +
 src/lua/popen.c    | 483 ++++++++++++++++++++++++++++++++++++++++++
 src/lua/popen.h    |  44 ++++
 src/lua/popen.lua  | 516 +++++++++++++++++++++++++++++++++++++++++++++
 5 files changed, 1049 insertions(+)
 create mode 100644 src/lua/popen.c
 create mode 100644 src/lua/popen.h
 create mode 100644 src/lua/popen.lua

diff --git a/src/CMakeLists.txt b/src/CMakeLists.txt
index e12de6005..e68030c5e 100644
--- a/src/CMakeLists.txt
+++ b/src/CMakeLists.txt
@@ -38,6 +38,7 @@ lua_source(lua_sources lua/help.lua)
 lua_source(lua_sources lua/help_en_US.lua)
 lua_source(lua_sources lua/tap.lua)
 lua_source(lua_sources lua/fio.lua)
+lua_source(lua_sources lua/popen.lua)
 lua_source(lua_sources lua/csv.lua)
 lua_source(lua_sources lua/strict.lua)
 lua_source(lua_sources lua/clock.lua)
@@ -114,6 +115,7 @@ set (server_sources
      lua/socket.c
      lua/pickle.c
      lua/fio.c
+     lua/popen.c
      lua/httpc.c
      lua/utf8.c
      lua/info.c
diff --git a/src/lua/init.c b/src/lua/init.c
index 097dd8495..09f34d614 100644
--- a/src/lua/init.c
+++ b/src/lua/init.c
@@ -56,6 +56,7 @@
 #include "lua/msgpack.h"
 #include "lua/pickle.h"
 #include "lua/fio.h"
+#include "lua/popen.h"
 #include "lua/httpc.h"
 #include "lua/utf8.h"
 #include "lua/swim.h"
@@ -99,6 +100,7 @@ extern char strict_lua[],
 	help_en_US_lua[],
 	tap_lua[],
 	fio_lua[],
+	popen_lua[],
 	error_lua[],
 	argparse_lua[],
 	iconv_lua[],
@@ -141,6 +143,7 @@ static const char *lua_modules[] = {
 	"log", log_lua,
 	"uri", uri_lua,
 	"fio", fio_lua,
+	"popen", popen_lua,
 	"error", error_lua,
 	"csv", csv_lua,
 	"clock", clock_lua,
@@ -455,6 +458,7 @@ tarantool_lua_init(const char *tarantool_bin, int argc, char **argv)
 	tarantool_lua_errno_init(L);
 	tarantool_lua_error_init(L);
 	tarantool_lua_fio_init(L);
+	tarantool_lua_popen_init(L);
 	tarantool_lua_socket_init(L);
 	tarantool_lua_pickle_init(L);
 	tarantool_lua_digest_init(L);
diff --git a/src/lua/popen.c b/src/lua/popen.c
new file mode 100644
index 000000000..219d9ce6d
--- /dev/null
+++ b/src/lua/popen.c
@@ -0,0 +1,483 @@
+/*
+ * Copyright 2010-2019, Tarantool AUTHORS, please see AUTHORS file.
+ *
+ * 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.
+ *
+ * THIS SOFTWARE IS PROVIDED BY <COPYRIGHT HOLDER> ``AS IS'' AND
+ * ANY EXPRESS 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
+ * <COPYRIGHT HOLDER> OR CONTRIBUTORS 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.
+ */
+
+#include <sys/types.h>
+
+#include <lua.h>
+#include <lauxlib.h>
+#include <lualib.h>
+
+#include "diag.h"
+#include "core/popen.h"
+
+#include "lua/utils.h"
+#include "lua/popen.h"
+
+static inline int
+lbox_pushsyserror(struct lua_State *L)
+{
+	diag_set(SystemError, "popen: %s", strerror(errno));
+	return luaT_push_nil_and_error(L);
+}
+
+static inline int
+lbox_push_error(struct lua_State *L)
+{
+	diag_set(SystemError, "popen: %s", strerror(errno));
+	struct error *e = diag_last_error(diag_get());
+	assert(e != NULL);
+	luaT_pusherror(L, e);
+	return 1;
+}
+
+static inline int
+lbox_pushbool(struct lua_State *L, bool res)
+{
+	lua_pushboolean(L, res);
+	if (!res) {
+		lbox_push_error(L);
+		return 2;
+	}
+	return 1;
+}
+
+/**
+ * lbox_fio_popen_new - creates a new popen handle and runs a command inside
+ * @command:	a command to run
+ * @flags:	popen_flag_bits
+ *
+ * Returns pair @handle = data, @err = nil on success,
+ * @handle = nil, err ~= nil on error.
+ */
+static int
+lbox_popen_new(struct lua_State *L)
+{
+	struct popen_handle *handle;
+	struct popen_opts opts = { };
+	size_t i, argv_size;
+	ssize_t nr_env;
+
+	if (lua_gettop(L) < 1 || !lua_istable(L, 1))
+		luaL_error(L, "Usage: fio.run({opts}])");
+
+	lua_pushstring(L, "argv");
+	lua_gettable(L, -2);
+	if (!lua_istable(L, -1))
+		luaL_error(L, "fio.run: {argv=...} is not a table");
+	lua_pop(L, 1);
+
+	lua_pushstring(L, "flags");
+	lua_gettable(L, -2);
+	if (!lua_isnumber(L, -1))
+		luaL_error(L, "fio.run: {flags=...} is not a number");
+	opts.flags = lua_tonumber(L, -1);
+	lua_pop(L, 1);
+
+	lua_pushstring(L, "argc");
+	lua_gettable(L, -2);
+	if (!lua_isnumber(L, -1))
+		luaL_error(L, "fio.run: {argc=...} is not a number");
+	opts.nr_argv = lua_tonumber(L, -1);
+	lua_pop(L, 1);
+
+	if (opts.nr_argv < 1)
+		luaL_error(L, "fio.run: {argc} is too small");
+
+	/*
+	 * argv array should contain NULL element at the
+	 * end and probably "sh", "-c" at the start, so
+	 * reserve enough space for any flags.
+	 */
+	opts.nr_argv += 3;
+	argv_size = sizeof(char *) * opts.nr_argv;
+	opts.argv = malloc(argv_size);
+	if (!opts.argv)
+		luaL_error(L, "fio.run: can't allocate argv");
+
+	lua_pushstring(L, "argv");
+	lua_gettable(L, -2);
+	lua_pushnil(L);
+	for (i = 2; lua_next(L, -2) != 0; i++) {
+		assert(i < opts.nr_argv);
+		opts.argv[i] = (char *)lua_tostring(L, -1);
+		lua_pop(L, 1);
+	}
+	lua_pop(L, 1);
+
+	opts.argv[0] = NULL;
+	opts.argv[1] = NULL;
+	opts.argv[opts.nr_argv - 1] = NULL;
+
+	/*
+	 * Environment can be filled, empty
+	 * to inherit or contain one NULL to
+	 * be zapped.
+	 */
+	lua_pushstring(L, "envc");
+	lua_gettable(L, -2);
+	if (!lua_isnumber(L, -1)) {
+		free(opts.argv);
+		luaL_error(L, "fio.run: {envc=...} is not a number");
+	}
+	nr_env = lua_tonumber(L, -1);
+	lua_pop(L, 1);
+
+	if (nr_env >= 0) {
+		/* Should be NULL terminating */
+		opts.env = malloc((nr_env + 1) * sizeof(char *));
+		if (!opts.env) {
+			free(opts.argv);
+			luaL_error(L, "fio.run: can't allocate env");
+		}
+
+		lua_pushstring(L, "env");
+		lua_gettable(L, -2);
+		if (!lua_istable(L, -1)) {
+			free(opts.argv);
+			free(opts.env);
+			luaL_error(L, "fio.run: {env=...} is not a table");
+		}
+		lua_pushnil(L);
+		for (i = 0; lua_next(L, -2) != 0; i++) {
+			assert((ssize_t)i <= nr_env);
+			opts.env[i] = (char *)lua_tostring(L, -1);
+			lua_pop(L, 1);
+		}
+		lua_pop(L, 1);
+
+		opts.env[nr_env] = NULL;
+	} else {
+		/*
+		 * Just zap it to nil, the popen will
+		 * process inheriting by self.
+		 */
+		opts.env = NULL;
+	}
+
+	handle = popen_new(&opts);
+
+	free(opts.argv);
+	free(opts.env);
+
+	if (!handle)
+		return lbox_pushsyserror(L);
+
+	lua_pushlightuserdata(L, handle);
+	return 1;
+}
+
+/**
+ * lbox_fio_popen_kill - kill popen's child process
+ * @handle:	a handle carries child process to kill
+ *
+ * Returns true if process is killed and false
+ * otherwise. Note the process is simply signaled
+ * and it doesn't mean it is killed immediately,
+ * Poll lbox_fio_pstatus if need to find out when
+ * exactly the child is reaped out.
+ */
+static int
+lbox_popen_kill(struct lua_State *L)
+{
+	struct popen_handle *p = lua_touserdata(L, 1);
+	return lbox_pushbool(L, popen_send_signal(p, SIGKILL) == 0);
+}
+
+/**
+ * lbox_fio_popen_term - terminate popen's child process
+ * @handle:	a handle carries child process to terminate
+ *
+ * Returns true if process is terminated and false
+ * otherwise.
+ */
+static int
+lbox_popen_term(struct lua_State *L)
+{
+	struct popen_handle *p = lua_touserdata(L, 1);
+	return lbox_pushbool(L, popen_send_signal(p, SIGTERM) == 0);
+}
+
+/**
+ * lbox_fio_popen_signal - send signal to a child process
+ * @handle:	a handle carries child process to terminate
+ * @signo:	signal number to send
+ *
+ * Returns true if signal is sent.
+ */
+static int
+lbox_popen_signal(struct lua_State *L)
+{
+	struct popen_handle *p = lua_touserdata(L, 1);
+	int signo = lua_tonumber(L, 2);
+	return lbox_pushbool(L, popen_send_signal(p, signo) == 0);
+}
+
+/**
+ * lbox_popen_state - fetch popen child process status
+ * @handle:	a handle to fetch status from
+ *
+ * Returns @err = nil, @reason = POPEN_STATE_x,
+ * @exit_code = 'number' on success, @err ~= nil on error.
+ */
+static int
+lbox_popen_state(struct lua_State *L)
+{
+	struct popen_handle *p = lua_touserdata(L, 1);
+	int state, exit_code, ret;
+
+	ret = popen_state(p, &state, &exit_code);
+	if (ret < 0)
+		return lbox_push_error(L);
+
+	lua_pushnil(L);
+	lua_pushinteger(L, state);
+	lua_pushinteger(L, exit_code);
+	return 3;
+}
+
+/**
+ * lbox_popen_read - read data from a child peer
+ * @handle:	handle of a child process
+ * @buf:	destination buffer
+ * @count:	number of bytes to read
+ * @flags:	which peer to read (stdout,stderr)
+ * @timeout:	timeout in seconds; ignored if negative
+ *
+ * Returns @size = 'read bytes', @err = nil on success,
+ * @size = nil, @err ~= nil on error.
+ */
+static int
+lbox_popen_read(struct lua_State *L)
+{
+	struct popen_handle *handle = lua_touserdata(L, 1);
+	uint32_t ctypeid;
+	void *buf =  *(char **)luaL_checkcdata(L, 2, &ctypeid);
+	size_t count = lua_tonumber(L, 3);
+	unsigned int flags = lua_tonumber(L, 4);
+	ev_tstamp timeout = lua_tonumber(L, 5);
+	ssize_t ret;
+
+	ret = popen_read_timeout(handle, buf, count,
+				 flags, timeout);
+	if (ret < 0)
+		return lbox_pushsyserror(L);
+
+	lua_pushinteger(L, ret);
+	return 1;
+}
+
+/**
+ * lbox_popen_write - write data to a child peer
+ * @handle:	a handle of a child process
+ * @buf:	source buffer
+ * @count:	number of bytes to write
+ * @flags:	which peer to write (stdin)
+ * @timeout:	timeout in seconds; ignored if negative
+ *
+ * Returns @err = nil on succes, @err ~= nil on error.
+ */
+static int
+lbox_popen_write(struct lua_State *L)
+{
+	struct popen_handle *handle = lua_touserdata(L, 1);
+	void *buf = (void *)lua_tostring(L, 2);
+	uint32_t ctypeid = 0;
+	if (buf == NULL)
+		buf =  *(char **)luaL_checkcdata(L, 2, &ctypeid);
+	size_t count = lua_tonumber(L, 3);
+	unsigned int flags = lua_tonumber(L, 4);
+	ev_tstamp timeout = lua_tonumber(L, 5);
+	ssize_t ret;
+
+	ret = popen_write_timeout(handle, buf, count, flags, timeout);
+	if (ret < 0)
+		return lbox_pushsyserror(L);
+	return lbox_pushbool(L, ret == 0);
+}
+
+/**
+ * lbox_popen_info - return information about popen handle
+ * @handle:	a handle of a child process
+ *
+ * Returns a @table ~= nil, @err = nil on success,
+ * @table = nil, @err ~= nil on error.
+ */
+static int
+lbox_popen_info(struct lua_State *L)
+{
+	struct popen_handle *handle = lua_touserdata(L, 1);
+	int state, exit_code, ret;
+	struct popen_stat st = { };
+
+	if (popen_stat(handle, &st))
+		return lbox_pushsyserror(L);
+
+	ret = popen_state(handle, &state, &exit_code);
+	if (ret < 0)
+		return lbox_pushsyserror(L);
+
+	assert(state < POPEN_STATE_MAX);
+
+	lua_newtable(L);
+
+	lua_pushliteral(L, "pid");
+	lua_pushinteger(L, st.pid);
+	lua_settable(L, -3);
+
+	lua_pushliteral(L, "command");
+	lua_pushstring(L, popen_command(handle));
+	lua_settable(L, -3);
+
+	lua_pushliteral(L, "flags");
+	lua_pushinteger(L, st.flags);
+	lua_settable(L, -3);
+
+	lua_pushliteral(L, "state");
+	lua_pushstring(L, popen_state_str(state));
+	lua_settable(L, -3);
+
+	lua_pushliteral(L, "exit_code");
+	lua_pushinteger(L, exit_code);
+	lua_settable(L, -3);
+
+	lua_pushliteral(L, "stdin");
+	lua_pushinteger(L, st.fds[STDIN_FILENO]);
+	lua_settable(L, -3);
+
+	lua_pushliteral(L, "stdout");
+	lua_pushinteger(L, st.fds[STDOUT_FILENO]);
+	lua_settable(L, -3);
+
+	lua_pushliteral(L, "stderr");
+	lua_pushinteger(L, st.fds[STDERR_FILENO]);
+	lua_settable(L, -3);
+
+	return 1;
+}
+
+/**
+ * lbox_popen_delete - close a popen handle
+ * @handle:	a handle to close
+ *
+ * If there is a running child it get killed first.
+ *
+ * Returns true if a handle is closeed, false otherwise.
+ */
+static int
+lbox_popen_delete(struct lua_State *L)
+{
+	void *handle = lua_touserdata(L, 1);
+	return lbox_pushbool(L, popen_delete(handle) == 0);
+}
+
+/**
+ * tarantool_lua_popen_init - Create popen methods
+ */
+void
+tarantool_lua_popen_init(struct lua_State *L)
+{
+	static const struct luaL_Reg popen_methods[] = {
+		{ },
+	};
+
+	/* public methods */
+	luaL_register_module(L, "popen", popen_methods);
+
+	static const struct luaL_Reg builtin_methods[] = {
+		{ "new",		lbox_popen_new,		},
+		{ "delete",		lbox_popen_delete,	},
+		{ "kill",		lbox_popen_kill,	},
+		{ "term",		lbox_popen_term,	},
+		{ "signal",		lbox_popen_signal,	},
+		{ "state",		lbox_popen_state,	},
+		{ "read",		lbox_popen_read,	},
+		{ "write",		lbox_popen_write,	},
+		{ "info",		lbox_popen_info,	},
+		{ },
+	};
+
+	/* builtin methods */
+	lua_pushliteral(L, "builtin");
+	lua_newtable(L);
+
+	luaL_register(L, NULL, builtin_methods);
+	lua_settable(L, -3);
+
+	/*
+	 * Popen constants.
+	 */
+#define lua_gen_const(_n, _v)		\
+	lua_pushliteral(L, _n);		\
+	lua_pushinteger(L, _v);		\
+	lua_settable(L, -3)
+
+	lua_pushliteral(L, "c");
+	lua_newtable(L);
+
+	/*
+	 * Flag masks.
+	 */
+	lua_pushliteral(L, "flag");
+	lua_newtable(L);
+
+	lua_gen_const("NONE",			POPEN_FLAG_NONE);
+
+	lua_gen_const("STDIN",			POPEN_FLAG_FD_STDIN);
+	lua_gen_const("STDOUT",			POPEN_FLAG_FD_STDOUT);
+	lua_gen_const("STDERR",			POPEN_FLAG_FD_STDERR);
+
+	lua_gen_const("STDIN_DEVNULL",		POPEN_FLAG_FD_STDIN_DEVNULL);
+	lua_gen_const("STDOUT_DEVNULL",		POPEN_FLAG_FD_STDOUT_DEVNULL);
+	lua_gen_const("STDERR_DEVNULL",		POPEN_FLAG_FD_STDERR_DEVNULL);
+
+	lua_gen_const("STDIN_CLOSE",		POPEN_FLAG_FD_STDIN_CLOSE);
+	lua_gen_const("STDOUT_CLOSE",		POPEN_FLAG_FD_STDOUT_CLOSE);
+	lua_gen_const("STDERR_CLOSE",		POPEN_FLAG_FD_STDERR_CLOSE);
+
+	lua_gen_const("SHELL",			POPEN_FLAG_SHELL);
+	lua_gen_const("SETSID",			POPEN_FLAGS_SETSID);
+	lua_gen_const("CLOSE_FDS",		POPEN_FLAG_CLOSE_FDS);
+	lua_gen_const("RESTORE_SIGNALS",	POPEN_FLAGS_RESTORE_SIGNALS);
+	lua_settable(L, -3);
+
+	lua_pushliteral(L, "state");
+	lua_newtable(L);
+
+	lua_gen_const("ALIVE",			POPEN_STATE_ALIVE);
+	lua_gen_const("EXITED",			POPEN_STATE_EXITED);
+	lua_gen_const("SIGNALED",		POPEN_STATE_SIGNALED);
+	lua_settable(L, -3);
+
+#undef lua_gen_const
+
+	lua_settable(L, -3);
+	lua_pop(L, 1);
+}
diff --git a/src/lua/popen.h b/src/lua/popen.h
new file mode 100644
index 000000000..4e3f137c5
--- /dev/null
+++ b/src/lua/popen.h
@@ -0,0 +1,44 @@
+#ifndef INCLUDES_TARANTOOL_LUA_POPEN_H
+#define INCLUDES_TARANTOOL_LUA_POPEN_H
+/*
+ * Copyright 2010-2019, Tarantool AUTHORS, please see AUTHORS file.
+ *
+ * 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.
+ *
+ * THIS SOFTWARE IS PROVIDED BY <COPYRIGHT HOLDER> ``AS IS'' AND
+ * ANY EXPRESS 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
+ * <COPYRIGHT HOLDER> OR CONTRIBUTORS 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.
+ */
+#if defined(__cplusplus)
+extern "C" {
+#endif /* defined(__cplusplus) */
+
+struct lua_State;
+void tarantool_lua_popen_init(struct lua_State *L);
+
+#if defined(__cplusplus)
+} /* extern "C" */
+#endif /* defined(__cplusplus) */
+
+#endif /* INCLUDES_TARANTOOL_LUA_POPEN_H */
diff --git a/src/lua/popen.lua b/src/lua/popen.lua
new file mode 100644
index 000000000..31af26cc6
--- /dev/null
+++ b/src/lua/popen.lua
@@ -0,0 +1,516 @@
+-- popen.lua (builtin file)
+--
+-- vim: ts=4 sw=4 et
+
+local buffer = require('buffer')
+local popen = require('popen')
+local fiber = require('fiber')
+local ffi = require('ffi')
+local bit = require('bit')
+
+local const_char_ptr_t = ffi.typeof('const char *')
+
+local builtin = popen.builtin
+popen.builtin = nil
+
+local popen_methods = { }
+
+local function default_flags()
+    local flags = popen.c.flag.NONE
+
+    -- default flags: close everything and use shell
+    flags = bit.bor(flags, popen.c.flag.STDIN_CLOSE)
+    flags = bit.bor(flags, popen.c.flag.STDOUT_CLOSE)
+    flags = bit.bor(flags, popen.c.flag.STDERR_CLOSE)
+    flags = bit.bor(flags, popen.c.flag.SHELL)
+    flags = bit.bor(flags, popen.c.flag.SETSID)
+    flags = bit.bor(flags, popen.c.flag.CLOSE_FDS)
+    flags = bit.bor(flags, popen.c.flag.RESTORE_SIGNALS)
+
+    return flags
+end
+
+--
+-- A map for popen option keys into tfn ('true|false|nil') values
+-- where bits are just set without additional manipulations.
+--
+-- For example stdin=true means to open stdin end for write,
+-- stdin=false to close the end, finally stdin[=nil] means to
+-- provide /dev/null into a child peer.
+local flags_map_tfn = {
+    stdin = {
+        popen.c.flag.STDIN,
+        popen.c.flag.STDIN_CLOSE,
+        popen.c.flag.STDIN_DEVNULL,
+    },
+    stdout = {
+        popen.c.flag.STDOUT,
+        popen.c.flag.STDOUT_CLOSE,
+        popen.c.flag.STDOUT_DEVNULL,
+    },
+    stderr = {
+        popen.c.flag.STDERR,
+        popen.c.flag.STDERR_CLOSE,
+        popen.c.flag.STDERR_DEVNULL,
+    },
+}
+
+--
+-- A map for popen option keys into tf ('true|false') values
+-- where bits are set on 'true' and clear on 'false'.
+local flags_map_tf = {
+    shell = {
+        popen.c.flag.SHELL,
+    },
+    close_fds = {
+        popen.c.flag.CLOSE_FDS
+    },
+    restore_signals = {
+        popen.c.flag.RESTORE_SIGNALS
+    },
+    start_new_session = {
+        popen.c.flag.SETSID
+    },
+}
+
+--
+-- Parses flags options from flags_map_tfn and
+-- flags_map_tf tables.
+local function parse_flags(epfx, flags, opts)
+    if opts == nil then
+        return flags
+    end
+    for k,v in pairs(opts) do
+        if flags_map_tfn[k] == nil then
+            if flags_map_tf[k] == nil then
+                error(string.format("%s: Unknown key %s", epfx, k))
+            end
+            if v == true then
+                flags = bit.bor(flags, flags_map_tf[k][1])
+            elseif v == false then
+                flags = bit.band(flags, bit.bnot(flags_map_tf[k][1]))
+            else
+                error(string.format("%s: Unknown value %s", epfx, v))
+            end
+        else
+            if v == true then
+                flags = bit.band(flags, bit.bnot(flags_map_tfn[k][2]))
+                flags = bit.band(flags, bit.bnot(flags_map_tfn[k][3]))
+                flags = bit.bor(flags, flags_map_tfn[k][1])
+            elseif v == false then
+                flags = bit.band(flags, bit.bnot(flags_map_tfn[k][1]))
+                flags = bit.band(flags, bit.bnot(flags_map_tfn[k][3]))
+                flags = bit.bor(flags, flags_map_tfn[k][2])
+            elseif v == "devnull" then
+                flags = bit.band(flags, bit.bnot(flags_map_tfn[k][1]))
+                flags = bit.band(flags, bit.bnot(flags_map_tfn[k][2]))
+                flags = bit.bor(flags, flags_map_tfn[k][3])
+            else
+                error(string.format("%s: Unknown value %s", epfx, v))
+            end
+        end
+    end
+    return flags
+end
+
+--
+-- Parse "mode" string to flags
+local function parse_mode(epfx, flags, mode)
+    if mode == nil then
+        return flags
+    end
+    for i = 1, #mode do
+        local c = mode:sub(i, i)
+        if c == 'r' then
+            flags = bit.band(flags, bit.bnot(popen.c.flag.STDOUT_CLOSE))
+            flags = bit.bor(flags, popen.c.flag.STDOUT)
+            flags = bit.band(flags, bit.bnot(popen.c.flag.STDERR_CLOSE))
+            flags = bit.bor(flags, popen.c.flag.STDERR)
+        elseif c == 'w' then
+            flags = bit.band(flags, bit.bnot(popen.c.flag.STDIN_CLOSE))
+            flags = bit.bor(flags, popen.c.flag.STDIN)
+        else
+            error(string.format("%s: Unknown mode %s", epfx, c))
+        end
+    end
+    return flags
+end
+
+--
+-- Close a popen object and release all resources.
+-- In case if there is a running child process
+-- it will be killed.
+--
+-- Returns @ret = true if popen object is closed, and
+-- @ret = false, @err ~= nil on error.
+popen_methods.close = function(self)
+    local ret, err = builtin.delete(self.cdata)
+    if err ~= nil then
+        return false, err
+    end
+    self.cdata = nil
+    return true
+end
+
+--
+-- Kill a child process
+--
+-- Returns @ret = true on success,
+-- @ret = false, @err ~= nil on error.
+popen_methods.kill = function(self)
+    return builtin.kill(self.cdata)
+end
+
+--
+-- Terminate a child process
+--
+-- Returns @ret = true on success,
+-- @ret = false, @err ~= nil on error.
+popen_methods.terminate = function(self)
+    return builtin.term(self.cdata)
+end
+
+--
+-- Send signal with number @signo to a child process
+--
+-- Returns @ret = true on success,
+-- @ret = false, @err ~= nil on error.
+popen_methods.send_signal = function(self, signo)
+    return builtin.signal(self.cdata, signo)
+end
+
+--
+-- Fetch a child process state
+--
+-- Returns @err = nil, @state = popen.c.state, @exit_code = num,
+-- otherwise @err ~= nil.
+popen_methods.state = function(self)
+    return builtin.state(self.cdata)
+end
+
+--
+-- Wait until a child process get exited.
+--
+-- Returns the same as popen_methods.status.
+popen_methods.wait = function(self)
+    local err, state, code
+    while true do
+        err, state, code = builtin.state(self.cdata)
+        if err or state ~= popen.c.state.ALIVE then
+            break
+        end
+        fiber.sleep(self.wait_secs)
+    end
+    return err, state, code
+end
+
+--
+-- popen:read2 - read a stream of a child process
+-- @opts:       options table
+--
+-- The options should have the following keys
+--
+-- @buf:        const_char_ptr_t buffer
+-- @size:       size of the buffer
+-- @flags:      stdout=true or stderr=true
+-- @timeout:    read timeout in seconds, < 0 to ignore
+--
+-- Returns @res = bytes, err = nil in case if read processed
+-- without errors, @res = nil, @err ~= nil otherwise.
+popen_methods.read2 = function(self, opts)
+    local flags = parse_flags("popen:read2",
+                              popen.c.flag.NONE,
+                              opts['flags'])
+    local timeout = -1
+
+    if opts['buf'] == nil then
+        error("popen:read2 {'buf'} key is missed")
+    elseif opts['size'] == nil then
+        error("popen:read2 {'size'} key is missed")
+    elseif opts['timeout'] ~= nil then
+        timeout = tonumber(opts['timeout'])
+    end
+
+    return builtin.read(self.cdata, opts['buf'],
+                        tonumber(opts['size']),
+                        flags, timeout)
+end
+
+--
+-- popen:write2 - write to a child's streem
+-- @opts:       options table
+--
+-- The options should have the following keys
+--
+-- @buf:        const_char_ptr_t buffer
+-- @size:       size of the buffer
+-- @flags:      stdin=true
+-- @timeout:    write timeout in seconds, < 0 to ignore
+--
+-- Returns @err = nil on success, @err ~= nil otherwise.
+popen_methods.write2 = function(self, opts)
+    local flags = parse_flags("popen:write2",
+                              popen.c.flag.NONE,
+                              opts['flags'])
+    local timeout = -1
+
+    if opts['buf'] == nil then
+        error("popen:write2 {'buf'} key is missed")
+    elseif opts['size'] == nil then
+        error("popen:write2 {'size'} key is missed")
+    elseif opts['timeout'] ~= nil then
+        timeout = tonumber(opts['timeout'])
+    end
+
+    return builtin.write(self.cdata, opts['buf'],
+                         tonumber(opts['size']),
+                         flags, timeout)
+end
+
+--
+-- popen:read - read string from a stream
+-- @stderr:     set to true to read from stderr, optional
+-- @timeout:    timeout in seconds, optional
+--
+-- Returns a result string, or res = nil, @err ~= nil on error.
+popen_methods.read = function(self, stderr, timeout)
+    local ibuf = buffer.ibuf()
+    local buf = ibuf:reserve(self.read_size)
+    local flags
+
+    if stderr ~= nil then
+        flags = { stderr = true }
+    else
+        flags = { stdout = true }
+    end
+
+    if timeout == nil then
+        timeout = -1
+    end
+
+    local res, err = self:read2({
+        buf     = buf,
+        size    = self.read_size,
+        flags   = flags,
+        timeout = timeout,
+    })
+
+    if err ~= nil then
+        ibuf:recycle()
+        return nil, err
+    end
+
+    ibuf:alloc(res)
+    res = ffi.string(ibuf.rpos, ibuf:size())
+    ibuf:recycle()
+
+    return res
+end
+
+--
+-- popen:write - write string @str to stdin stream
+-- @str:        string to write
+-- @timeout:    timeout in seconds, optional
+--
+-- Returns @err = nil on success, @err ~= nil on error.
+popen_methods.write = function(self, str, timeout)
+    if timeout == nil then
+        timeout = -1
+    end
+    return self:write2({
+        buf     = str,
+        size    = #str,
+        flags   = { stdin = true },
+        timeout = timeout,
+    })
+end
+
+--
+-- popen:info -- obtain information about a popen object
+--
+-- Returns @info table, err == nil on success,
+-- @info = nil, @err ~= nil otherwise.
+--
+-- The @info table contains the following keys:
+--
+-- @pid:        pid of a child process
+-- @flags:      flags associated (popen.c.flag bitmap)
+-- @stdout:     parent peer for stdout, -1 if closed
+-- @stderr:     parent peer for stderr, -1 if closed
+-- @stdin:      parent peer for stdin, -1 if closed
+-- @state:      alive | exited | signaled
+-- @exit_code:  exit code of a child process if been
+--              exited or killed by a signal
+--
+-- The child should never be in "unknown" state, reserved
+-- for unexpected errors.
+--
+popen_methods.info = function(self)
+    return builtin.info(self.cdata)
+end
+
+--
+-- Create a new popen object from options
+local function popen_reify(opts)
+    local cdata, err = builtin.new(opts)
+    if err ~= nil then
+        return nil, err
+    end
+
+    local handle = {
+        -- a handle itself for future use
+        cdata           = cdata,
+
+        -- sleeping period for the @wait method
+        wait_secs       = 0.3,
+
+        -- size of a read buffer to allocate
+        -- in case of implicit read, this number
+        -- is taken from luatest repo to fit the
+        -- value people are familiar with
+        read_size       = 4096,
+    }
+
+    setmetatable(handle, {
+        __index     = popen_methods,
+    })
+
+    return handle
+end
+
+--
+-- popen.posix - create a new child process and execute a command inside
+-- @command:    a command to run
+-- @mode:       r - to grab child's stdout for read
+--              (stderr kept opened as well for 2>&1 redirection)
+--              w - to obtain stdin for write
+--
+-- Note: Since there are two options only the following parameters
+-- are implied (see default_flags): all fds except stdin/out/err
+-- are closed, signals are restored to default handlers, the command
+-- is executed in a new session.
+--
+-- Examples:
+--
+--  ph = require('popen').posix("date", "r")
+--      runs date as "sh -c date" to read the output,
+--      closing all file descriptors except stdout/err
+--      inside a child process
+--
+popen.posix = function(command, mode)
+    local flags = default_flags()
+
+    if type(command) ~= 'string' then
+        error("Usage: popen.posix(command[, rw])")
+    end
+
+    -- Mode gives simplified flags
+    flags = parse_mode("popen.posix", flags, mode)
+
+    local opts = {
+        argv    = { command },
+        argc    = 1,
+        flags   = flags,
+        envc    = -1,
+    }
+
+    return popen_reify(opts)
+end
+
+-- popen.popen - execute a child program in a new process
+-- @opt:    options table
+--
+-- @opts carries of the following options:
+--
+--  @argv:  an array of a program to run with
+--          command line options, mandatory
+--
+--  @env:   an array of environment variables to
+--          be used inside a process, if not
+--          set then the current environment is
+--          inherited, if set to an empty array
+--          then the environment will be dropped
+--
+--  @flags: a dictionary to describe communication pipes
+--          and other parameters of a process to run
+--
+--      stdin=true      to write into STDIN_FILENO of a child process
+--      stdin=false     to close STDIN_FILENO inside a child process [*]
+--      stdin="devnull" a child will get /dev/null as STDIN_FILENO
+--
+--      stdout=true     to read STDOUT_FILENO of a child process
+--      stdout=false    to close STDOUT_FILENO inside a child process [*]
+--      stdout="devnull" a child will get /dev/null as STDOUT_FILENO
+--
+--      stderr=true     to read STDERR_FILENO from a child process
+--      stderr=false    to close STDERR_FILENO inside a child process [*]
+--      stderr="devnull" a child will get /dev/null as STDERR_FILENO
+--
+--      shell=true      runs a child process via "sh -c" [*]
+--      shell=false     invokes a child process executable directly
+--
+--      close_fds=true  close all inherited fds from a parent [*]
+--
+--      restore_signals=true
+--                      all signals modified by a caller reset
+--                      to default handler [*]
+--
+--      start_new_session=true
+--                      start executable inside a new session [*]
+--
+-- [*] are default values
+--
+-- Examples:
+--
+--  ph = require('popen').popen({argv = {"date"}, flags = {stdout=true}})
+--  ph:read()
+--  ph:close()
+--
+--      Execute 'date' command inside a shell, read the result
+--      and close the popen object
+--
+--  ph = require('popen').popen({argv = {"/usr/bin/echo", "-n", "hello"},
+--                               flags = {stdout=true, shell=false}})
+--  ph:read()
+--  ph:close()
+--
+--      Execute /usr/bin/echo with arguments '-n','hello' directly
+--      without using a shell, read the result from stdout and close
+--      the popen object
+--
+popen.popen = function(opts)
+    local flags = default_flags()
+
+    if opts == nil or type(opts) ~= 'table' then
+        error("Usage: popen({argv={}[, envp={}, flags={}]")
+    end
+
+    -- Test for required arguments
+    if opts["argv"] == nil then
+        error("popen: argv key is missing")
+    end
+
+    -- Process flags and save updated mask
+    -- to pass into engine (in case of missing
+    -- flags we just use defaults).
+    if opts["flags"] ~= nil then
+        flags = parse_flags("popen", flags, opts["flags"])
+    end
+    opts["flags"] = flags
+
+    -- We will need a number of args for speed sake
+    opts["argc"] = #opts["argv"]
+
+    -- Same applies to the environment (note though
+    -- that env={} is pretty valid and env=nil
+    -- as well.
+    if opts["env"] ~= nil then
+        opts["envc"] = #opts["env"]
+    else
+        opts["envc"] = -1
+    end
+
+    return popen_reify(opts)
+end
+
+return popen
-- 
2.20.1

^ permalink raw reply	[flat|nested] 16+ messages in thread

* [Tarantool-patches] [PATCH v6 4/4] popen/test: Add base test cases
  2019-12-17 12:54 [Tarantool-patches] [PATCH v6 0/4] popen: Add ability to run external process Cyrill Gorcunov
                   ` (2 preceding siblings ...)
  2019-12-17 12:54 ` [Tarantool-patches] [PATCH v6 3/4] popen/lua: Add popen module Cyrill Gorcunov
@ 2019-12-17 12:54 ` Cyrill Gorcunov
  3 siblings, 0 replies; 16+ messages in thread
From: Cyrill Gorcunov @ 2019-12-17 12:54 UTC (permalink / raw)
  To: tml

Fixes #4031

Signed-off-by: Cyrill Gorcunov <gorcunov@gmail.com>
---
 test/app/popen.result   | 234 ++++++++++++++++++++++++++++++++++++++++
 test/app/popen.test.lua |  91 ++++++++++++++++
 2 files changed, 325 insertions(+)
 create mode 100644 test/app/popen.result
 create mode 100644 test/app/popen.test.lua

diff --git a/test/app/popen.result b/test/app/popen.result
new file mode 100644
index 000000000..dd60f4833
--- /dev/null
+++ b/test/app/popen.result
@@ -0,0 +1,234 @@
+-- test-run result file version 2
+-- Test popen engine
+--
+-- vim: ts=4 sw=4 et
+
+buffer = require('buffer')
+ | ---
+ | ...
+popen = require('popen')
+ | ---
+ | ...
+ffi = require('ffi')
+ | ---
+ | ...
+
+test_run = require('test_run').new()
+ | ---
+ | ...
+
+buf = buffer.ibuf()
+ | ---
+ | ...
+
+--
+-- Trivial echo output
+--
+script = "echo -n 1 2 3 4 5"
+ | ---
+ | ...
+ph = popen.posix(script, "r")
+ | ---
+ | ...
+ph:wait()   -- wait echo to finish
+ | ---
+ | - null
+ | - 2
+ | - 0
+ | ...
+ph:read()   -- read the output
+ | ---
+ | - 1 2 3 4 5
+ | ...
+ph:close()  -- release the popen
+ | ---
+ | - true
+ | ...
+
+--
+-- Test info and force killing of a child process
+--
+script = "while [ 1 ]; do sleep 10; done"
+ | ---
+ | ...
+ph = popen.posix(script, "r")
+ | ---
+ | ...
+ph:kill()
+ | ---
+ | - true
+ | ...
+--
+-- Killing child may be queued and depends on
+-- system load, so we may get ESRCH here.
+err, reason, exit_code = ph:wait()
+ | ---
+ | ...
+ph:state()
+ | ---
+ | - null
+ | - 3
+ | - 9
+ | ...
+info = ph:info()
+ | ---
+ | ...
+info["command"]
+ | ---
+ | - sh -c while [ 1 ]; do sleep 10; done
+ | ...
+info["state"]
+ | ---
+ | - signaled
+ | ...
+info["flags"]
+ | ---
+ | - 61510
+ | ...
+info["exit_code"]
+ | ---
+ | - 9
+ | ...
+ph:close()
+ | ---
+ | - true
+ | ...
+
+--
+-- Test info and soft killing of a child process
+--
+script = "while [ 1 ]; do sleep 10; done"
+ | ---
+ | ...
+ph = popen.posix(script, "r")
+ | ---
+ | ...
+ph:terminate()
+ | ---
+ | - true
+ | ...
+--
+-- Killing child may be queued and depends on
+-- system load, so we may get ESRCH here.
+err, reason, exit_code = ph:wait()
+ | ---
+ | ...
+ph:state()
+ | ---
+ | - null
+ | - 3
+ | - 15
+ | ...
+info = ph:info()
+ | ---
+ | ...
+info["command"]
+ | ---
+ | - sh -c while [ 1 ]; do sleep 10; done
+ | ...
+info["state"]
+ | ---
+ | - signaled
+ | ...
+info["flags"]
+ | ---
+ | - 61510
+ | ...
+info["exit_code"]
+ | ---
+ | - 15
+ | ...
+ph:close()
+ | ---
+ | - true
+ | ...
+
+--
+-- Test for stdin/out stream
+--
+script="prompt=''; read -n 5 prompt; echo -n $prompt"
+ | ---
+ | ...
+ph = popen.posix(script, "rw")
+ | ---
+ | ...
+ph:write("input")
+ | ---
+ | - true
+ | ...
+ph:read()
+ | ---
+ | - input
+ | ...
+ph:close()
+ | ---
+ | - true
+ | ...
+
+--
+-- Test reading stderr (simply redirect stdout to stderr)
+--
+script = "echo -n 1 2 3 4 5 1>&2"
+ | ---
+ | ...
+ph = popen.posix(script, "rw")
+ | ---
+ | ...
+ph:wait()
+ | ---
+ | - null
+ | - 2
+ | - 0
+ | ...
+size = 128
+ | ---
+ | ...
+dst = buf:reserve(size)
+ | ---
+ | ...
+res, err = ph:read2({buf = dst, size = size, nil, flags = {stderr = true}})
+ | ---
+ | ...
+res = buf:alloc(res)
+ | ---
+ | ...
+ffi.string(buf.rpos, buf:size())
+ | ---
+ | - 1 2 3 4 5
+ | ...
+buf:recycle()
+ | ---
+ | ...
+ph:close()
+ | ---
+ | - true
+ | ...
+
+--
+-- Test timeouts: just wait for 0.1 second
+-- to elapse, then write data and re-read
+-- for sure.
+--
+script = "prompt=''; read -n 5 prompt && echo -n $prompt;"
+ | ---
+ | ...
+ph = popen.posix(script, "rw")
+ | ---
+ | ...
+ph:read(nil, 0.1)
+ | ---
+ | - null
+ | - 'popen: Resource temporarily unavailable'
+ | ...
+ph:write("input")
+ | ---
+ | - true
+ | ...
+ph:read()
+ | ---
+ | - input
+ | ...
+ph:close()
+ | ---
+ | - true
+ | ...
diff --git a/test/app/popen.test.lua b/test/app/popen.test.lua
new file mode 100644
index 000000000..87b9dbdc8
--- /dev/null
+++ b/test/app/popen.test.lua
@@ -0,0 +1,91 @@
+-- Test popen engine
+--
+-- vim: ts=4 sw=4 et
+
+buffer = require('buffer')
+popen = require('popen')
+ffi = require('ffi')
+
+test_run = require('test_run').new()
+
+buf = buffer.ibuf()
+
+--
+-- Trivial echo output
+--
+script = "echo -n 1 2 3 4 5"
+ph = popen.posix(script, "r")
+ph:wait()   -- wait echo to finish
+ph:read()   -- read the output
+ph:close()  -- release the popen
+
+--
+-- Test info and force killing of a child process
+--
+script = "while [ 1 ]; do sleep 10; done"
+ph = popen.posix(script, "r")
+ph:kill()
+--
+-- Killing child may be queued and depends on
+-- system load, so we may get ESRCH here.
+err, reason, exit_code = ph:wait()
+ph:state()
+info = ph:info()
+info["command"]
+info["state"]
+info["flags"]
+info["exit_code"]
+ph:close()
+
+--
+-- Test info and soft killing of a child process
+--
+script = "while [ 1 ]; do sleep 10; done"
+ph = popen.posix(script, "r")
+ph:terminate()
+--
+-- Killing child may be queued and depends on
+-- system load, so we may get ESRCH here.
+err, reason, exit_code = ph:wait()
+ph:state()
+info = ph:info()
+info["command"]
+info["state"]
+info["flags"]
+info["exit_code"]
+ph:close()
+
+--
+-- Test for stdin/out stream
+--
+script="prompt=''; read -n 5 prompt; echo -n $prompt"
+ph = popen.posix(script, "rw")
+ph:write("input")
+ph:read()
+ph:close()
+
+--
+-- Test reading stderr (simply redirect stdout to stderr)
+--
+script = "echo -n 1 2 3 4 5 1>&2"
+ph = popen.posix(script, "rw")
+ph:wait()
+size = 128
+dst = buf:reserve(size)
+res, err = ph:read2({buf = dst, size = size, nil, flags = {stderr = true}})
+res = buf:alloc(res)
+ffi.string(buf.rpos, buf:size())
+buf:recycle()
+ph:close()
+
+--
+-- Test timeouts: just wait for 0.1 second
+-- to elapse, then write data and re-read
+-- for sure.
+--
+script = "prompt=''; read -n 5 prompt && echo -n $prompt;"
+ph = popen.posix(script, "rw")
+ph:read(nil, 0.1)
+ph:write("input")
+ph:read()
+ph:close()
-- 
2.20.1

^ permalink raw reply	[flat|nested] 16+ messages in thread

* Re: [Tarantool-patches] [PATCH v6 1/4] coio: Export helpers and provide coio_read_fd_timeout
  2019-12-17 12:54 ` [Tarantool-patches] [PATCH v6 1/4] coio: Export helpers and provide coio_read_fd_timeout Cyrill Gorcunov
@ 2019-12-20  7:48   ` Konstantin Osipov
  2019-12-20 14:50     ` Cyrill Gorcunov
  0 siblings, 1 reply; 16+ messages in thread
From: Konstantin Osipov @ 2019-12-20  7:48 UTC (permalink / raw)
  To: Cyrill Gorcunov; +Cc: tml

* Cyrill Gorcunov <gorcunov@gmail.com> [19/12/17 15:57]:
> There is no reason to hide functions. In particular
> we will use coio_write_fd_timeout and coio_read_fd_timeout
> for popen.

AFAIU the only difference between the new function and
coio_read_timeout() is that the new function works without 
struct coio* object, but uses coio_wait(), which creates/
destroys this object on demand.

Could you provide a rationale for this? coio_wait has to call 
EPOLL_CTL_ADD/EPOLL_CTL_DEL on every wait, so it tripples the 
number of syscalls per wait. 

On the other hand I realize that it's not super important
for popen IO, but still I don't understand *why* you need this.


-- 
Konstantin Osipov, Moscow, Russia

^ permalink raw reply	[flat|nested] 16+ messages in thread

* Re: [Tarantool-patches] [PATCH v6 2/4] popen: Introduce a backend engine
  2019-12-17 12:54 ` [Tarantool-patches] [PATCH v6 2/4] popen: Introduce a backend engine Cyrill Gorcunov
@ 2019-12-20  8:11   ` Konstantin Osipov
  2019-12-20 11:52     ` Cyrill Gorcunov
  2019-12-20 12:11     ` Alexander Turenko
  2019-12-26  7:14   ` Konstantin Osipov
  1 sibling, 2 replies; 16+ messages in thread
From: Konstantin Osipov @ 2019-12-20  8:11 UTC (permalink / raw)
  To: Cyrill Gorcunov; +Cc: tml

* Cyrill Gorcunov <gorcunov@gmail.com> [19/12/17 15:57]:
> +/* Children pids map popen_handle map */

map popen_handle map?
Even if I remove double "map" I don't undesrtand the ocmment.

Is it pid -> popen_handle() map? How is it used? For waitpid?
Please explain in the comment.

> +static struct mh_i32ptr_t *popen_pids_map = NULL;
> +
> +/* All popen handles to be able to cleanup them on exit */
at exit

> +static RLIST_HEAD(popen_head);
> +
> +/* /dev/null to be used inside children if requested */

This is a bit too sketchy. Please explain: how it is requested,
what it is used for, etc. I can of course guess that it's used
as children stdin/stdout, if there is an option to popen(), but
that's it.

> +static int dev_null_fd_ro = -1;
> +static int dev_null_fd_wr = -1;

> +/**
> + * handle_free - free memory allocated for a handle
> + * @handle:	popen handle to free
> + *
> + * Just to match handle_new().
> + */
> +static void
> +handle_free(struct popen_handle *handle)
> +{
> +	say_debug("popen: handle %p free %p", handle);
> +	free(handle);
> +}

I don't understand who owns char *command and why 
it's not freed here. Could you please clarify?

> +	return coio_write_fd_timeout(handle->fds[idx],
> +				     buf, count, timeout);

You could store struct coio for each child stream in the handle,
it's pretty cheap. I don't have a strong opinion on this,
your implementation looks tidy (although has some overhead).

> +/**
> + * popen_notify_sigchld - notify popen subsistem about SIGCHLD event
subsystem

> +	if (WIFEXITED(wstatus) || WIFSIGNALED(wstatus)) {
> +		assert(handle->pid == pid);
> +		/*
> +		 * libev calls for waitpid by self so
> +		 * we don't have to wait here.
> +		 */
> +		popen_unregister(handle);
> +		/*
> +		 * Since SIGCHLD may come to us not
> +		 * due to exit/kill reason (consider
> +		 * a case when someone stopped a child
> +		 * process) we should continue wathcing
> +		 * state changes, thus we stop monitoring
> +		 * dead children only.
> +		 */
> +		say_debug("popen: ev_child_stop %d", handle->pid);
> +		ev_child_stop(EV_DEFAULT_ &handle->ev_sigchld);
> +		handle->pid = -1;
> +	}

I don't understand how the code matches the comment.

You write "we should continue watching state changes" inside
a branch that handles termination. How is this comment relevant to
what you are doing in this branch?

Why do you have to cast handle->ev_sigchld to EV_DEFAULT_?


> +}
> +
> +/**
> + * ev_sigchld_cb - handle SIGCHLD from a child process
> + * @w:		a child exited
> + * @revents:	unused
> + */
> +static void
> +ev_sigchld_cb(EV_P_ ev_child *w, int revents)
> +{
> +	(void)revents;
> +	/*
> +	 * Stop watching this child, libev will
> +	 * remove it from own hashtable.
> +	 */
> +	ev_child_stop(EV_A_ w);
> +
> +	/*
> +	 * The reason for a separate helper is that
> +	 * we might need to notify more subsystems
> +	 * in future.
> +	 */
> +	popen_notify_sigchld(w->rpid, w->rstatus);

I think ev_sigchld_cb is a too general name. Besides, why do you
need two functions, and call one from another?

> +	if (handle->flags & POPEN_FLAGS_SETSID) {
> +		if (signo == SIGKILL || signo == SIGTERM) {
> +			say_debug("popen: killpg %d signo %d",
> +				  handle->pid, signo);
> +			ret = killpg(handle->pid, signo);

I don't understand why you need to use killpg() here.

Could you please explain?
I suspect that you don't, you're just trying to be safe,
and there is no need to.

> +		if (handle->fds[i] != -1) {
> +			/*
> +			 * We might did some i/o on
> +			 * this fd, so make sure
> +			 * libev removed it from
> +			 * watching before close.
> +			 *
> +			 * Calling coio_close on
> +			 * fd which never been watched
> +			 * if safe.
> +			 */
> +			coio_close(handle->fds[i]);

Ugh. This definitely suggests you need a coio object 
in popen_handle(), this coio_close() looks like a hack.

-- 
Konstantin Osipov, Moscow, Russia

^ permalink raw reply	[flat|nested] 16+ messages in thread

* Re: [Tarantool-patches] [PATCH v6 2/4] popen: Introduce a backend engine
  2019-12-20  8:11   ` Konstantin Osipov
@ 2019-12-20 11:52     ` Cyrill Gorcunov
  2019-12-20 12:04       ` Konstantin Osipov
  2019-12-20 12:11     ` Alexander Turenko
  1 sibling, 1 reply; 16+ messages in thread
From: Cyrill Gorcunov @ 2019-12-20 11:52 UTC (permalink / raw)
  To: Konstantin Osipov; +Cc: tml

On Fri, Dec 20, 2019 at 11:11:59AM +0300, Konstantin Osipov wrote:
> * Cyrill Gorcunov <gorcunov@gmail.com> [19/12/17 15:57]:
> > +/* Children pids map popen_handle map */
> 
> map popen_handle map?
> Even if I remove double "map" I don't undesrtand the ocmment.
> 
> Is it pid -> popen_handle() map? How is it used? For waitpid?
> Please explain in the comment.

Yes, it was s typo, should be "Mapping of children pids to popen_handle".
The map may be used for several reasons: 1) to waitpid 2) to fetch
information about specific pid (lua interface). I don't think we should
point every reason for this pid <=> handle mapping here in comment,
but instead the comment (probably) should be "Mapping of children pids
to popen_handle for fast lookup"?

> > +static struct mh_i32ptr_t *popen_pids_map = NULL;
> > +
> > +/* All popen handles to be able to cleanup them on exit */
> at exit

Both variants are correct, but I'll update to match atexit
libc helper in a meaning.

> > +/* /dev/null to be used inside children if requested */
> 
> This is a bit too sketchy. Please explain: how it is requested,
> what it is used for, etc. I can of course guess that it's used
> as children stdin/stdout, if there is an option to popen(), but
> that's it.

Yes, exactly, it is used as stdX. When first attempt to implement popen
hit the mailing list there were comments that we should provide /dev/null
as stdX (I presume because some of programs might require stdX presence
even if they're not writting anything). I'll update the comment to
point the reason. Thanks!

> > +static void
> > +handle_free(struct popen_handle *handle)
> > +{
> > +	say_debug("popen: handle %p free %p", handle);
> > +	free(handle);
> > +}
> 
> I don't understand who owns char *command and why 
> it's not freed here. Could you please clarify?

Look, the command is compiled from argv array into
a single string dynamically allocated. The handle
just carries a reference to it. If you think it will
be more suitable to allocate command together with
handle itself in one place then sure I'll update.

> 
> > +	return coio_write_fd_timeout(handle->fds[idx],
> > +				     buf, count, timeout);
> 
> You could store struct coio for each child stream in the handle,
> it's pretty cheap. I don't have a strong opinion on this,
> your implementation looks tidy (although has some overhead).

I'll take a look. Thanks!

> > +	if (WIFEXITED(wstatus) || WIFSIGNALED(wstatus)) {
> > +		assert(handle->pid == pid);
> > +		/*
> > +		 * libev calls for waitpid by self so
> > +		 * we don't have to wait here.
> > +		 */
> > +		popen_unregister(handle);
> > +		/*
> > +		 * Since SIGCHLD may come to us not
> > +		 * due to exit/kill reason (consider
> > +		 * a case when someone stopped a child
> > +		 * process) we should continue wathcing
> > +		 * state changes, thus we stop monitoring
> > +		 * dead children only.
> > +		 */
> > +		say_debug("popen: ev_child_stop %d", handle->pid);
> > +		ev_child_stop(EV_DEFAULT_ &handle->ev_sigchld);
> > +		handle->pid = -1;
> > +	}
> 
> I don't understand how the code matches the comment.
> 
> You write "we should continue watching state changes" inside
> a branch that handles termination. How is this comment relevant to
> what you are doing in this branch?

You know, internally every child inside libev is kept in own hashmap.
When SIGCHLD happens libev walks over the hash and findis a child
process. When I said "we should continue watching state changes"
I mean that we should not affect any other watcher libev carries
and should stop watching only our pid (remember that in future
we might need to create other child process for some reason,
unrelated to popen). Thus we should call libev's "stop" on the
pid we're interested in, and in result libev will drop own
hash entry matching to this pid. I'll rework the comment.

At first I tried to make libev to watch every children
pid but this caused problems with compilation. Maybe I should
look into this area again.

> 
> Why do you have to cast handle->ev_sigchld to EV_DEFAULT_?

Otherwise it doesn't compile.

> > +}
> > +
> > +/**
> > + * ev_sigchld_cb - handle SIGCHLD from a child process
> > + * @w:		a child exited
> > + * @revents:	unused
> > + */
> > +static void
> > +ev_sigchld_cb(EV_P_ ev_child *w, int revents)
> > +{
> > +	(void)revents;
> > +	/*
> > +	 * Stop watching this child, libev will
> > +	 * remove it from own hashtable.
> > +	 */
> > +	ev_child_stop(EV_A_ w);
> > +
> > +	/*
> > +	 * The reason for a separate helper is that
> > +	 * we might need to notify more subsystems
> > +	 * in future.
> > +	 */
> > +	popen_notify_sigchld(w->rpid, w->rstatus);
> 
> I think ev_sigchld_cb is a too general name. Besides, why do you
> need two functions, and call one from another?

Would handle_child_exit() sound better? As to nesting -- this is
just in case if we need to send more notifiers in future. I can
inline popen_notify_sigchld here if you prefer.

Or even rename ev_sigchld_cb to popen_notify_sigchld and use
it instead.

> 
> > +	if (handle->flags & POPEN_FLAGS_SETSID) {
> > +		if (signo == SIGKILL || signo == SIGTERM) {
> > +			say_debug("popen: killpg %d signo %d",
> > +				  handle->pid, signo);
> > +			ret = killpg(handle->pid, signo);
> 
> I don't understand why you need to use killpg() here.
> 
> Could you please explain?
> I suspect that you don't, you're just trying to be safe,
> and there is no need to.

After thinking more I think you're right, plain kill should be enough.

> 
> > +		if (handle->fds[i] != -1) {
> > +			/*
> > +			 * We might did some i/o on
> > +			 * this fd, so make sure
> > +			 * libev removed it from
> > +			 * watching before close.
> > +			 *
> > +			 * Calling coio_close on
> > +			 * fd which never been watched
> > +			 * if safe.
> > +			 */
> > +			coio_close(handle->fds[i]);
> 
> Ugh. This definitely suggests you need a coio object 
> in popen_handle(), this coio_close() looks like a hack.

Will look into this. Thanks a huge for review, Kostya!

	Cyrill

^ permalink raw reply	[flat|nested] 16+ messages in thread

* Re: [Tarantool-patches] [PATCH v6 2/4] popen: Introduce a backend engine
  2019-12-20 11:52     ` Cyrill Gorcunov
@ 2019-12-20 12:04       ` Konstantin Osipov
  2019-12-20 12:10         ` Cyrill Gorcunov
  0 siblings, 1 reply; 16+ messages in thread
From: Konstantin Osipov @ 2019-12-20 12:04 UTC (permalink / raw)
  To: Cyrill Gorcunov; +Cc: tml

* Cyrill Gorcunov <gorcunov@gmail.com> [19/12/20 14:56]:
> > > +static void
> > > +handle_free(struct popen_handle *handle)
> > > +{
> > > +	say_debug("popen: handle %p free %p", handle);
> > > +	free(handle);
> > > +}
> > 
> > I don't understand who owns char *command and why 
> > it's not freed here. Could you please clarify?
> 
> Look, the command is compiled from argv array into
> a single string dynamically allocated. The handle
> just carries a reference to it. If you think it will
> be more suitable to allocate command together with
> handle itself in one place then sure I'll update.

Where is it freed?


-- 
Konstantin Osipov, Moscow, Russia

^ permalink raw reply	[flat|nested] 16+ messages in thread

* Re: [Tarantool-patches] [PATCH v6 2/4] popen: Introduce a backend engine
  2019-12-20 12:04       ` Konstantin Osipov
@ 2019-12-20 12:10         ` Cyrill Gorcunov
  0 siblings, 0 replies; 16+ messages in thread
From: Cyrill Gorcunov @ 2019-12-20 12:10 UTC (permalink / raw)
  To: Konstantin Osipov; +Cc: tml

On Fri, Dec 20, 2019 at 03:04:46PM +0300, Konstantin Osipov wrote:
> * Cyrill Gorcunov <gorcunov@gmail.com> [19/12/20 14:56]:
> > > > +static void
> > > > +handle_free(struct popen_handle *handle)
> > > > +{
> > > > +	say_debug("popen: handle %p free %p", handle);
> > > > +	free(handle);
> > > > +}
> > > 
> > > I don't understand who owns char *command and why 
> > > it's not freed here. Could you please clarify?
> > 
> > Look, the command is compiled from argv array into
> > a single string dynamically allocated. The handle
> > just carries a reference to it. If you think it will
> > be more suitable to allocate command together with
> > handle itself in one place then sure I'll update.
> 
> Where is it freed?

popen_delete -> command_free

^ permalink raw reply	[flat|nested] 16+ messages in thread

* Re: [Tarantool-patches] [PATCH v6 2/4] popen: Introduce a backend engine
  2019-12-20  8:11   ` Konstantin Osipov
  2019-12-20 11:52     ` Cyrill Gorcunov
@ 2019-12-20 12:11     ` Alexander Turenko
  1 sibling, 0 replies; 16+ messages in thread
From: Alexander Turenko @ 2019-12-20 12:11 UTC (permalink / raw)
  To: Konstantin Osipov, Cyrill Gorcunov, tml, Kirill Yukhin, Igor Munkin

> > +	if (handle->flags & POPEN_FLAGS_SETSID) {
> > +		if (signo == SIGKILL || signo == SIGTERM) {
> > +			say_debug("popen: killpg %d signo %d",
> > +				  handle->pid, signo);
> > +			ret = killpg(handle->pid, signo);
> 
> I don't understand why you need to use killpg() here.
> 
> Could you please explain?
> I suspect that you don't, you're just trying to be safe,
> and there is no need to.

I guess it is quite common to call setsid() in a child to then able to
kill a process group at whole, based on the note:

> Note: If you need to modify the environment for the child use the env
> parameter rather than doing it in a preexec_fn. The start_new_session
> parameter can take the place of a previously common use of preexec_fn
> to call os.setsid() in the child. 

https://docs.python.org/3.9/library/subprocess.html

It is quite useful when a shell is called: a non-interactive shell does
not perform job control and so does not create a separate process group
for a command pipeline.

I googled a bit to verify that os.setsid() is actually often suggested
with subprocess.Popen() and, yep, this is so:
https://stackoverflow.com/a/4791612/1598057

I think that it is okay to be a default behaviour (because it is
harmless even for a singleton command). I guess that Python's API does
not enable it default for historical reasons.

OTOH, this behaviour means that we need to kill all spawned groups at
tarantool exit, because they will not be killed with, say, SIGINT from a
terminal (I didn't verify whether the current implementation does it).

WBR, Alexander Turenko.

^ permalink raw reply	[flat|nested] 16+ messages in thread

* Re: [Tarantool-patches] [PATCH v6 1/4] coio: Export helpers and provide coio_read_fd_timeout
  2019-12-20  7:48   ` Konstantin Osipov
@ 2019-12-20 14:50     ` Cyrill Gorcunov
  0 siblings, 0 replies; 16+ messages in thread
From: Cyrill Gorcunov @ 2019-12-20 14:50 UTC (permalink / raw)
  To: Konstantin Osipov; +Cc: tml

On Fri, Dec 20, 2019 at 10:48:47AM +0300, Konstantin Osipov wrote:
> * Cyrill Gorcunov <gorcunov@gmail.com> [19/12/17 15:57]:
> > There is no reason to hide functions. In particular
> > we will use coio_write_fd_timeout and coio_read_fd_timeout
> > for popen.
> 
> AFAIU the only difference between the new function and
> coio_read_timeout() is that the new function works without 
> struct coio* object, but uses coio_wait(), which creates/
> destroys this object on demand.

Yes. This allows us to reuse it instead of code duplication.

> 
> Could you provide a rationale for this? coio_wait has to call 
> EPOLL_CTL_ADD/EPOLL_CTL_DEL on every wait, so it tripples the 
> number of syscalls per wait. 

But how else we can do that? libev uses poll internally to find
out if there some "event" in the event queue (timer event, io
event and such). Could you please elaborate which idea you have?

Indeed every time we start io on fd, the libev puts this fd into
watchee list to be notified if there some event to handle. I must
admit I don't see an other way.

> 
> On the other hand I realize that it's not super important
> for popen IO, but still I don't understand *why* you need this.

^ permalink raw reply	[flat|nested] 16+ messages in thread

* Re: [Tarantool-patches] [PATCH v6 3/4] popen/lua: Add popen module
  2019-12-17 12:54 ` [Tarantool-patches] [PATCH v6 3/4] popen/lua: Add popen module Cyrill Gorcunov
@ 2019-12-20 15:41   ` Maxim Melentiev
  0 siblings, 0 replies; 16+ messages in thread
From: Maxim Melentiev @ 2019-12-20 15:41 UTC (permalink / raw)
  To: tarantool-patches

0) Should we change the module name to `subprocess`?

1) Let's change doc's overview to
```
This module allows to start child processes and gives asynchronous 
access to their stdin, stdout, stderr.
```

2) Errors should be returned with `nil, err`. Successful results - 
`val1, val2, val3`.
Please use `err` in all code examples when apliable to make people copy 
the code in the right way:
```
process, err = subprocess.popen(...)
str, err = process:read()
```

3) I suggest using `subprocess.` for module-level functions and 
`process:` for process-instance methods in docs to avoid ambiguity.

4) Let's rename `.posix` -> `.popen` (because it actually conforms with 
this syscall) and `.popen` -> `.spawn`.

5) Let's move values out of `flags` to the outer options for all 
methods: `.spawn({argv = {...}, stdin = false, shell = false, ...})`.

6) `env` option should be map-table but not an array (`{WORKDIR = 
'some/path'}`).

7) Should not `start_new_session` be disabled by default? By default 
child processes should exit if parent exits.

8) `chdir` option to cwd before running a process.

9) Standard fds should not be closed by default to make it possible to 
read/write to spawned process. With closed fds `.popen({argv = {'echo', 
'test'}})` will fail because it will not be able to write to closed fd. 
Moreover it should be possible to set child fds to parent ones and this 
should be the default. Let's consider following options (constants are 
cdata values):
   - `true/subprocess.PIPE` - create pipe between parent and child for 
particular fd;
   - `false` - close fd;
   - `subprocess.DEVNULL` - forward to `/dev/null`;
   - `io/fio object` - use fd from this object;
   - `nil` (default) - use corresponding fd from parent process.


10) As of std* can be available in the child process in some cases and 
not available in others, there will be corner cases for `:read/:write`. 
I suggest to keep this straightforward and keep this checks in user 
code: create `.stdin, .stdout, .stderr` fields in process only when 
relevant pipe is created. There is no need for`stder=true|false` 
argument anymore.


11) Let's keep `write2` and `read2` private (or remove them if they no 
longer required).

12) Let's make `:read` api consistent with existing apis. Although this 
isn't easy while `fio:read` does not have `timeout` option and 
`socket:read` has complicated `delimiter` option and does not have 
`buffer` option. I think `:read({
   timeout=sec[0 - no wait],
   limit=at_most_bytes_to_read_without_yield[nil - no limit]
})` should be enough. I don't know if `buffer` from `fio:read` is 
useful, anyway it can be added later.

13) Clarify when `:read` yields. It'll be great if with `timeout = 0` it 
would not yield.

14) Clarify when `:write` yields, how/when it retries writes 
(SIGPIPE/epol/interval/etc.).

15) What if it has written just a part of payload before timeout? I 
think it should always return number of bytes has been written and in 
case of timeout error should also be provided along with the result.

16) Signal constants need to be provided in lua api. This is because 
some of signals have different values in different OS (ex., `SIGSTOP`, 
it's used in cartridge tests). Then let's keep only single `kill(signal 
= 15)` method.

17) I think current `popen.c.` values can be skipped. It's better to 
return original flags table in `:info().flags` o even skip this field as 
well. And string value from `:state()` because there is no static 
checker for constants in lua. This also will be similar to `fiber:status()`.

18) Let's not provide separate `:state` method because `:info` has the 
same data.


On 17/12/2019 15:54, Cyrill Gorcunov wrote:
> @TarantoolBot document
> Title: popen module
>
> Overview
> ========
>
> Tarantool supports execution of external programs similarly
> to well known Python's `subprocess` or Ruby's `Open3`. Note
> though the `popen` module does not match one to one to the
> helpers these languages provide and provides only basic
> functions. The popen object creation is implemented via
> `vfork()` system call which means the caller thread is
> blocked until execution of a child process begins.
>
> Functions
> =========
>
> The `popen` module provides two functions to create that named
> popen object `popen.posix` which is the similar to libc `posix`
> syscall and `popen.popen` to create popen object with more
> specific options.
>
> `handler, err = popen.posix(command, mode)`
> -------------------------------------------
>
> Creates a new child process and executes a command as the "sh -c command".
>
> Parameters
> ```
> command     is the command to run
>
> mode        'r' - to grab child's stdout for read
>              'w' - to write into child's stdin stream
>              'rw' - combined mode to read and write
> ```
>
> If `mode` has `r` then `stderr` output stream is always opened to be
> able to redirect `stderr` to `stdout` by well known `2>&1` statement.
> If `mode` is not specified at all then non of the std stram will be
> opened inside a child process.
>
> On success returns popen object handler setting `err` to nil,
> otherwise error reported.
>
> __Example 1__
> ```
> ph = require('popen').posix("date", "r")
> ph:read()
> ph:close()
> ```
>
> Executes "sh -c date" then reads the output and closes the popen object.
>
> `handler, err = popen.popen(opts)`
> ----------------------------------
>
> Creates a new child process and execute a program inside. The `opts` is
> options table with the following keys
>
> ```
> argv        an array containing the binary to execute together
>              with command line arguments; this is the only mandatory
>              key in the options table
>
> env         an array of environment variables, if not set then they
>              are inherited; if env=nil then environment will be empty
>
> flags       a dictionary to describe communication pipes and other
>              parameters of a process to run
>
>              stdin=true
>                  to write into STDIN_FILENO of a child process
>              stdin=false
>                  to close STDIN_FILENO inside a child process, default
>              stdin="devnull"
>                  a child will get /dev/null as STDIN_FILENO
>
>              stdout=true
>                  to read STDOUT_FILENO of a child process
>              stdout=false
>                  to close STDOUT_FILENO inside a child process, default
>              stdout="devnull"
>                  a child will get /dev/null as STDOUT_FILENO
>
>              stderr=true
>                  to read STDERR_FILENO of a child process
>              stderr=false
>                  to close STDERR_FILENO inside a child process, default
>              stderr="devnull"
>                  a child will get /dev/null as STDERR_FILENO
>
>              shell=true
>                  runs a child process via "sh -c", default
>              shell=false
>                  invokes a child process executable directly
>
>              close_fds=true
>                  close all inherited fds from a parent, default
>
>              restore_signals=true
>                  all signals modified by a caller reset
>                  to default handler, default
>
>              start_new_session=true
>                  start executable inside a new session, default
> ```
>
> On success returns popen object handler setting `err` to nil,
> otherwise error reported.
>
> __Example 2__
> ```
> ph, err = require('popen').popen({argv = {"/usr/bin/echo", "-n", "hello"},
>                                   flags = {stdout=true, shell=false}})
> ```
>
> `str, err = popen:read([stderr=true|false,timeout])`
> ----------------------------------------------------
>
> Reads data from `stdout` or `stderr` streams with `timeout`. By default
> it reads from `stdout` stream, to read from `stderr` stream set
> `stderr=true`.
>
> On success returns string `str` read and `err=nil`, otherwise `err ~= nil`.
>
> __Example 3__
> ```
> ph = require('popen').popen({argv = {"/usr/bin/echo", "-n", "hello"},
>                              flags = {stdout=true, shell=false}})
> ph:read()
> ---
> - hello
> ```
>
> `err = popen:write(str[,timeout])`
> ----------------------------------
>
> Writes string `str` to `stdin` stream of a child process. Since the internal
> buffer on the reader side is limited in size and depends on OS settings the `write`
> may hang forever if buffer is full. For this wake `timeout` (float number) parameter
> allows to specify number of seconds which once elapsed interrupt `write` attempts
> and exit from this function. When not provided the `write` blocks until data is
> written or error happened.
>
> On success returns `err = nil`, otherwise `err ~= nil`.
>
> __Example 4__
> ```
> script = "prompt=''; read -n 5 prompt && echo -n $prompt;"
> ph = require('popen').posix(script, "rw")
> ph:write("12345")
> ph:read()
> ---
> - '12345'
> ```
>
> `err = popen:write2(opts)`
> --------------------------
>
> Writes data from a buffer to a stream of a child process. `opts` is
> a table of options with the following keys
>
> ```
> buf     buffer with raw data to write
> size    number of bytes to write
> flags   dictionary to specify the input stream
>          stdin=true
>              to write into stdin stream
> timeout write timeout in seconds, optional
> ```
>
> Basically the `popen:write` is a wrapper over `popen:write2` call
> with `opts` similar to `{timeout = -1, {flags = {stdin = true}}`.
>
> On success `err = nil`, otherwise `err ~= nil`.
>
> The reason for passing `{flags = {stdin = true}}` while we have only
> one `stdin` is that we reserve the interface for future where we
> might extend it without breaking backward compatibility.
>
> `bytes, err = popen:read2(opts)`
> --------------------------------
>
> Reads data from a stream of a child process to a buffer. `opts` is
> a table of options with the following keys
>
> ```
> buf     destination buffer
> size    number of bytes to read
> flags   dictionary to specify the input stream
>          stdout=true
>              to read from stdout stream
>          stderr=true
>              to read from stderr stream
> timeout read timeout in seconds, optional
> ```
>
> Basically the `popen:read` is a wrapper over `popen:read2` call
> with `opts` similar to `{timeout = -1, {flags = {stdout = true}}`.
>
> On success returns number of `bytes` read and `err = nil`,
> otherwise `err ~= nil`.
>
> __Example 5__
> ```
> buffer = require('buffer')
> popen = require('popen')
> ffi = require('ffi')
>
> script = "echo -n 1 2 3 4 5 1>&2"
> buf = buffer.ibuf()
>
> ph = popen.posix(script, "rw")
> ph:wait()
> size = 128
> dst = buf:reserve(size)
> res, err = ph:read2({buf = dst, size = size, nil, flags = {stderr = true}})
> res = buf:alloc(res)
> ffi.string(buf.rpos, buf:size())
> ---
> - 1 2 3 4 5
> ...
> buf:recycle()
> ph:close()
> ```
>
> `res, err = popen:terminate()`
> -----------------------------
>
> Terminates a child process in a soft way sending `SIGTERM` signal.
> On success returns `res = true, err = nil`, `err ~= nil` otherwise.
>
> `res, err = popen:kill()`
> -------------------------
>
> Terminates a child process in a soft way sending `SIGKILL` signal.
> On success returns `res = true, err = nil`, `err ~= nil` otherwise.
>
> `res, err = popen:send_signal(signo)`
> -------------------------------------
>
> Sends signal with number `signo` to a child process.
> On success returns `res = true, err = nil`, `err ~= nil`
> otherwise.
>
> `info, err = popen:info()`
> --------------------------
>
> Returns information about a child process in form of table
> with the following keys
>
> ```
> pid         PID of a child process, if already terminated
>              the it set to -1
>
> flags       flags been used to create the popen object; this
>              is bitmap of popen.c.flag constants
>
> stdout      file descriptor number associated with stdout
>              stream on a parent side, -1 if not set
>
> stderr      file descriptor number associated with stderr
>              stream on a parent side, -1 if not set
>
> stdint      file descriptor number associated with stdin
>              stream on a parent side, -1 if not set
>
> state       state of a child proces: "alive" if process
>              is running, "exited" if finished execution,
>              "signaled" is terminated by a signal
>
> exit_code   if process is in "signaled" state then this
>              number represent the signal number process
>              been terminated with, if process is in "exited"
>              state then it represent the exit code
> ```
>
> __Example 6__
> ```
> ph = require('popen').posix("echo -n 1 2 3 4 5 1>&2", "r")
> ph:info()
> ---
> - stdout: 12
>    command: sh -c echo -n 1 2 3 4 5 1>&2
>    stderr: 23
>    flags: 61510
>    state: exited
>    stdin: -1
>    exit_code: 0
>    pid: -1
> ```
>
> `err, state, exit_code = popen:state()`
> --------------------------------------
>
> Returns the current state of a chile process.
>
> On success returns `err = nil`, `state` is one of the
> constants `popen.c.state.ALIVE=1` when process is running,
> `popen.c.state.EXITED=2` when it exited by self or
> `popen.c.state.SIGNALED=3` when terminated via signal.
> In case if the child process is terminated by a signal
> the `exit_code` contains the signal number, otherwise
> it is an error code the child set by self.
>
> __Example 7__
> ```
> ph = require('popen').posix("echo -n 1 2 3 4 5 1>&2", "r")
> ph:state()
> ---
> - null
> - 2
> - 0
> ```
>
> `err, state, exit_code = popen:wait()`
> --------------------------------------
>
> Waits until a child process get exited or terminated.
> Returns the same data as `popen:state()`. Basically
> the `popen:wait()` is simply polling for the child state
> with `popen:wait()` in a cycle.
>
> `err = popen:close`
> -------------------
>
> Closes popen object releasing all resources occupied.
>
> If a child process is running then it get killed first. Once
> popen object is closed it no longed usable and any attempt
> to call function over will cause an error.
>
> On success `err = nil`.
>
> Part-of #4031
>
> Signed-off-by: Cyrill Gorcunov <gorcunov@gmail.com>
> ---
>   src/CMakeLists.txt |   2 +
>   src/lua/init.c     |   4 +
>   src/lua/popen.c    | 483 ++++++++++++++++++++++++++++++++++++++++++
>   src/lua/popen.h    |  44 ++++
>   src/lua/popen.lua  | 516 +++++++++++++++++++++++++++++++++++++++++++++
>   5 files changed, 1049 insertions(+)
>   create mode 100644 src/lua/popen.c
>   create mode 100644 src/lua/popen.h
>   create mode 100644 src/lua/popen.lua
>
> diff --git a/src/CMakeLists.txt b/src/CMakeLists.txt
> index e12de6005..e68030c5e 100644
> --- a/src/CMakeLists.txt
> +++ b/src/CMakeLists.txt
> @@ -38,6 +38,7 @@ lua_source(lua_sources lua/help.lua)
>   lua_source(lua_sources lua/help_en_US.lua)
>   lua_source(lua_sources lua/tap.lua)
>   lua_source(lua_sources lua/fio.lua)
> +lua_source(lua_sources lua/popen.lua)
>   lua_source(lua_sources lua/csv.lua)
>   lua_source(lua_sources lua/strict.lua)
>   lua_source(lua_sources lua/clock.lua)
> @@ -114,6 +115,7 @@ set (server_sources
>        lua/socket.c
>        lua/pickle.c
>        lua/fio.c
> +     lua/popen.c
>        lua/httpc.c
>        lua/utf8.c
>        lua/info.c
> diff --git a/src/lua/init.c b/src/lua/init.c
> index 097dd8495..09f34d614 100644
> --- a/src/lua/init.c
> +++ b/src/lua/init.c
> @@ -56,6 +56,7 @@
>   #include "lua/msgpack.h"
>   #include "lua/pickle.h"
>   #include "lua/fio.h"
> +#include "lua/popen.h"
>   #include "lua/httpc.h"
>   #include "lua/utf8.h"
>   #include "lua/swim.h"
> @@ -99,6 +100,7 @@ extern char strict_lua[],
>   	help_en_US_lua[],
>   	tap_lua[],
>   	fio_lua[],
> +	popen_lua[],
>   	error_lua[],
>   	argparse_lua[],
>   	iconv_lua[],
> @@ -141,6 +143,7 @@ static const char *lua_modules[] = {
>   	"log", log_lua,
>   	"uri", uri_lua,
>   	"fio", fio_lua,
> +	"popen", popen_lua,
>   	"error", error_lua,
>   	"csv", csv_lua,
>   	"clock", clock_lua,
> @@ -455,6 +458,7 @@ tarantool_lua_init(const char *tarantool_bin, int argc, char **argv)
>   	tarantool_lua_errno_init(L);
>   	tarantool_lua_error_init(L);
>   	tarantool_lua_fio_init(L);
> +	tarantool_lua_popen_init(L);
>   	tarantool_lua_socket_init(L);
>   	tarantool_lua_pickle_init(L);
>   	tarantool_lua_digest_init(L);
> diff --git a/src/lua/popen.c b/src/lua/popen.c
> new file mode 100644
> index 000000000..219d9ce6d
> --- /dev/null
> +++ b/src/lua/popen.c
> @@ -0,0 +1,483 @@
> +/*
> + * Copyright 2010-2019, Tarantool AUTHORS, please see AUTHORS file.
> + *
> + * 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.
> + *
> + * THIS SOFTWARE IS PROVIDED BY <COPYRIGHT HOLDER> ``AS IS'' AND
> + * ANY EXPRESS 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
> + * <COPYRIGHT HOLDER> OR CONTRIBUTORS 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.
> + */
> +
> +#include <sys/types.h>
> +
> +#include <lua.h>
> +#include <lauxlib.h>
> +#include <lualib.h>
> +
> +#include "diag.h"
> +#include "core/popen.h"
> +
> +#include "lua/utils.h"
> +#include "lua/popen.h"
> +
> +static inline int
> +lbox_pushsyserror(struct lua_State *L)
> +{
> +	diag_set(SystemError, "popen: %s", strerror(errno));
> +	return luaT_push_nil_and_error(L);
> +}
> +
> +static inline int
> +lbox_push_error(struct lua_State *L)
> +{
> +	diag_set(SystemError, "popen: %s", strerror(errno));
> +	struct error *e = diag_last_error(diag_get());
> +	assert(e != NULL);
> +	luaT_pusherror(L, e);
> +	return 1;
> +}
> +
> +static inline int
> +lbox_pushbool(struct lua_State *L, bool res)
> +{
> +	lua_pushboolean(L, res);
> +	if (!res) {
> +		lbox_push_error(L);
> +		return 2;
> +	}
> +	return 1;
> +}
> +
> +/**
> + * lbox_fio_popen_new - creates a new popen handle and runs a command inside
> + * @command:	a command to run
> + * @flags:	popen_flag_bits
> + *
> + * Returns pair @handle = data, @err = nil on success,
> + * @handle = nil, err ~= nil on error.
> + */
> +static int
> +lbox_popen_new(struct lua_State *L)
> +{
> +	struct popen_handle *handle;
> +	struct popen_opts opts = { };
> +	size_t i, argv_size;
> +	ssize_t nr_env;
> +
> +	if (lua_gettop(L) < 1 || !lua_istable(L, 1))
> +		luaL_error(L, "Usage: fio.run({opts}])");
> +
> +	lua_pushstring(L, "argv");
> +	lua_gettable(L, -2);
> +	if (!lua_istable(L, -1))
> +		luaL_error(L, "fio.run: {argv=...} is not a table");
> +	lua_pop(L, 1);
> +
> +	lua_pushstring(L, "flags");
> +	lua_gettable(L, -2);
> +	if (!lua_isnumber(L, -1))
> +		luaL_error(L, "fio.run: {flags=...} is not a number");
> +	opts.flags = lua_tonumber(L, -1);
> +	lua_pop(L, 1);
> +
> +	lua_pushstring(L, "argc");
> +	lua_gettable(L, -2);
> +	if (!lua_isnumber(L, -1))
> +		luaL_error(L, "fio.run: {argc=...} is not a number");
> +	opts.nr_argv = lua_tonumber(L, -1);
> +	lua_pop(L, 1);
> +
> +	if (opts.nr_argv < 1)
> +		luaL_error(L, "fio.run: {argc} is too small");
> +
> +	/*
> +	 * argv array should contain NULL element at the
> +	 * end and probably "sh", "-c" at the start, so
> +	 * reserve enough space for any flags.
> +	 */
> +	opts.nr_argv += 3;
> +	argv_size = sizeof(char *) * opts.nr_argv;
> +	opts.argv = malloc(argv_size);
> +	if (!opts.argv)
> +		luaL_error(L, "fio.run: can't allocate argv");
> +
> +	lua_pushstring(L, "argv");
> +	lua_gettable(L, -2);
> +	lua_pushnil(L);
> +	for (i = 2; lua_next(L, -2) != 0; i++) {
> +		assert(i < opts.nr_argv);
> +		opts.argv[i] = (char *)lua_tostring(L, -1);
> +		lua_pop(L, 1);
> +	}
> +	lua_pop(L, 1);
> +
> +	opts.argv[0] = NULL;
> +	opts.argv[1] = NULL;
> +	opts.argv[opts.nr_argv - 1] = NULL;
> +
> +	/*
> +	 * Environment can be filled, empty
> +	 * to inherit or contain one NULL to
> +	 * be zapped.
> +	 */
> +	lua_pushstring(L, "envc");
> +	lua_gettable(L, -2);
> +	if (!lua_isnumber(L, -1)) {
> +		free(opts.argv);
> +		luaL_error(L, "fio.run: {envc=...} is not a number");
> +	}
> +	nr_env = lua_tonumber(L, -1);
> +	lua_pop(L, 1);
> +
> +	if (nr_env >= 0) {
> +		/* Should be NULL terminating */
> +		opts.env = malloc((nr_env + 1) * sizeof(char *));
> +		if (!opts.env) {
> +			free(opts.argv);
> +			luaL_error(L, "fio.run: can't allocate env");
> +		}
> +
> +		lua_pushstring(L, "env");
> +		lua_gettable(L, -2);
> +		if (!lua_istable(L, -1)) {
> +			free(opts.argv);
> +			free(opts.env);
> +			luaL_error(L, "fio.run: {env=...} is not a table");
> +		}
> +		lua_pushnil(L);
> +		for (i = 0; lua_next(L, -2) != 0; i++) {
> +			assert((ssize_t)i <= nr_env);
> +			opts.env[i] = (char *)lua_tostring(L, -1);
> +			lua_pop(L, 1);
> +		}
> +		lua_pop(L, 1);
> +
> +		opts.env[nr_env] = NULL;
> +	} else {
> +		/*
> +		 * Just zap it to nil, the popen will
> +		 * process inheriting by self.
> +		 */
> +		opts.env = NULL;
> +	}
> +
> +	handle = popen_new(&opts);
> +
> +	free(opts.argv);
> +	free(opts.env);
> +
> +	if (!handle)
> +		return lbox_pushsyserror(L);
> +
> +	lua_pushlightuserdata(L, handle);
> +	return 1;
> +}
> +
> +/**
> + * lbox_fio_popen_kill - kill popen's child process
> + * @handle:	a handle carries child process to kill
> + *
> + * Returns true if process is killed and false
> + * otherwise. Note the process is simply signaled
> + * and it doesn't mean it is killed immediately,
> + * Poll lbox_fio_pstatus if need to find out when
> + * exactly the child is reaped out.
> + */
> +static int
> +lbox_popen_kill(struct lua_State *L)
> +{
> +	struct popen_handle *p = lua_touserdata(L, 1);
> +	return lbox_pushbool(L, popen_send_signal(p, SIGKILL) == 0);
> +}
> +
> +/**
> + * lbox_fio_popen_term - terminate popen's child process
> + * @handle:	a handle carries child process to terminate
> + *
> + * Returns true if process is terminated and false
> + * otherwise.
> + */
> +static int
> +lbox_popen_term(struct lua_State *L)
> +{
> +	struct popen_handle *p = lua_touserdata(L, 1);
> +	return lbox_pushbool(L, popen_send_signal(p, SIGTERM) == 0);
> +}
> +
> +/**
> + * lbox_fio_popen_signal - send signal to a child process
> + * @handle:	a handle carries child process to terminate
> + * @signo:	signal number to send
> + *
> + * Returns true if signal is sent.
> + */
> +static int
> +lbox_popen_signal(struct lua_State *L)
> +{
> +	struct popen_handle *p = lua_touserdata(L, 1);
> +	int signo = lua_tonumber(L, 2);
> +	return lbox_pushbool(L, popen_send_signal(p, signo) == 0);
> +}
> +
> +/**
> + * lbox_popen_state - fetch popen child process status
> + * @handle:	a handle to fetch status from
> + *
> + * Returns @err = nil, @reason = POPEN_STATE_x,
> + * @exit_code = 'number' on success, @err ~= nil on error.
> + */
> +static int
> +lbox_popen_state(struct lua_State *L)
> +{
> +	struct popen_handle *p = lua_touserdata(L, 1);
> +	int state, exit_code, ret;
> +
> +	ret = popen_state(p, &state, &exit_code);
> +	if (ret < 0)
> +		return lbox_push_error(L);
> +
> +	lua_pushnil(L);
> +	lua_pushinteger(L, state);
> +	lua_pushinteger(L, exit_code);
> +	return 3;
> +}
> +
> +/**
> + * lbox_popen_read - read data from a child peer
> + * @handle:	handle of a child process
> + * @buf:	destination buffer
> + * @count:	number of bytes to read
> + * @flags:	which peer to read (stdout,stderr)
> + * @timeout:	timeout in seconds; ignored if negative
> + *
> + * Returns @size = 'read bytes', @err = nil on success,
> + * @size = nil, @err ~= nil on error.
> + */
> +static int
> +lbox_popen_read(struct lua_State *L)
> +{
> +	struct popen_handle *handle = lua_touserdata(L, 1);
> +	uint32_t ctypeid;
> +	void *buf =  *(char **)luaL_checkcdata(L, 2, &ctypeid);
> +	size_t count = lua_tonumber(L, 3);
> +	unsigned int flags = lua_tonumber(L, 4);
> +	ev_tstamp timeout = lua_tonumber(L, 5);
> +	ssize_t ret;
> +
> +	ret = popen_read_timeout(handle, buf, count,
> +				 flags, timeout);
> +	if (ret < 0)
> +		return lbox_pushsyserror(L);
> +
> +	lua_pushinteger(L, ret);
> +	return 1;
> +}
> +
> +/**
> + * lbox_popen_write - write data to a child peer
> + * @handle:	a handle of a child process
> + * @buf:	source buffer
> + * @count:	number of bytes to write
> + * @flags:	which peer to write (stdin)
> + * @timeout:	timeout in seconds; ignored if negative
> + *
> + * Returns @err = nil on succes, @err ~= nil on error.
> + */
> +static int
> +lbox_popen_write(struct lua_State *L)
> +{
> +	struct popen_handle *handle = lua_touserdata(L, 1);
> +	void *buf = (void *)lua_tostring(L, 2);
> +	uint32_t ctypeid = 0;
> +	if (buf == NULL)
> +		buf =  *(char **)luaL_checkcdata(L, 2, &ctypeid);
> +	size_t count = lua_tonumber(L, 3);
> +	unsigned int flags = lua_tonumber(L, 4);
> +	ev_tstamp timeout = lua_tonumber(L, 5);
> +	ssize_t ret;
> +
> +	ret = popen_write_timeout(handle, buf, count, flags, timeout);
> +	if (ret < 0)
> +		return lbox_pushsyserror(L);
> +	return lbox_pushbool(L, ret == 0);
> +}
> +
> +/**
> + * lbox_popen_info - return information about popen handle
> + * @handle:	a handle of a child process
> + *
> + * Returns a @table ~= nil, @err = nil on success,
> + * @table = nil, @err ~= nil on error.
> + */
> +static int
> +lbox_popen_info(struct lua_State *L)
> +{
> +	struct popen_handle *handle = lua_touserdata(L, 1);
> +	int state, exit_code, ret;
> +	struct popen_stat st = { };
> +
> +	if (popen_stat(handle, &st))
> +		return lbox_pushsyserror(L);
> +
> +	ret = popen_state(handle, &state, &exit_code);
> +	if (ret < 0)
> +		return lbox_pushsyserror(L);
> +
> +	assert(state < POPEN_STATE_MAX);
> +
> +	lua_newtable(L);
> +
> +	lua_pushliteral(L, "pid");
> +	lua_pushinteger(L, st.pid);
> +	lua_settable(L, -3);
> +
> +	lua_pushliteral(L, "command");
> +	lua_pushstring(L, popen_command(handle));
> +	lua_settable(L, -3);
> +
> +	lua_pushliteral(L, "flags");
> +	lua_pushinteger(L, st.flags);
> +	lua_settable(L, -3);
> +
> +	lua_pushliteral(L, "state");
> +	lua_pushstring(L, popen_state_str(state));
> +	lua_settable(L, -3);
> +
> +	lua_pushliteral(L, "exit_code");
> +	lua_pushinteger(L, exit_code);
> +	lua_settable(L, -3);
> +
> +	lua_pushliteral(L, "stdin");
> +	lua_pushinteger(L, st.fds[STDIN_FILENO]);
> +	lua_settable(L, -3);
> +
> +	lua_pushliteral(L, "stdout");
> +	lua_pushinteger(L, st.fds[STDOUT_FILENO]);
> +	lua_settable(L, -3);
> +
> +	lua_pushliteral(L, "stderr");
> +	lua_pushinteger(L, st.fds[STDERR_FILENO]);
> +	lua_settable(L, -3);
> +
> +	return 1;
> +}
> +
> +/**
> + * lbox_popen_delete - close a popen handle
> + * @handle:	a handle to close
> + *
> + * If there is a running child it get killed first.
> + *
> + * Returns true if a handle is closeed, false otherwise.
> + */
> +static int
> +lbox_popen_delete(struct lua_State *L)
> +{
> +	void *handle = lua_touserdata(L, 1);
> +	return lbox_pushbool(L, popen_delete(handle) == 0);
> +}
> +
> +/**
> + * tarantool_lua_popen_init - Create popen methods
> + */
> +void
> +tarantool_lua_popen_init(struct lua_State *L)
> +{
> +	static const struct luaL_Reg popen_methods[] = {
> +		{ },
> +	};
> +
> +	/* public methods */
> +	luaL_register_module(L, "popen", popen_methods);
> +
> +	static const struct luaL_Reg builtin_methods[] = {
> +		{ "new",		lbox_popen_new,		},
> +		{ "delete",		lbox_popen_delete,	},
> +		{ "kill",		lbox_popen_kill,	},
> +		{ "term",		lbox_popen_term,	},
> +		{ "signal",		lbox_popen_signal,	},
> +		{ "state",		lbox_popen_state,	},
> +		{ "read",		lbox_popen_read,	},
> +		{ "write",		lbox_popen_write,	},
> +		{ "info",		lbox_popen_info,	},
> +		{ },
> +	};
> +
> +	/* builtin methods */
> +	lua_pushliteral(L, "builtin");
> +	lua_newtable(L);
> +
> +	luaL_register(L, NULL, builtin_methods);
> +	lua_settable(L, -3);
> +
> +	/*
> +	 * Popen constants.
> +	 */
> +#define lua_gen_const(_n, _v)		\
> +	lua_pushliteral(L, _n);		\
> +	lua_pushinteger(L, _v);		\
> +	lua_settable(L, -3)
> +
> +	lua_pushliteral(L, "c");
> +	lua_newtable(L);
> +
> +	/*
> +	 * Flag masks.
> +	 */
> +	lua_pushliteral(L, "flag");
> +	lua_newtable(L);
> +
> +	lua_gen_const("NONE",			POPEN_FLAG_NONE);
> +
> +	lua_gen_const("STDIN",			POPEN_FLAG_FD_STDIN);
> +	lua_gen_const("STDOUT",			POPEN_FLAG_FD_STDOUT);
> +	lua_gen_const("STDERR",			POPEN_FLAG_FD_STDERR);
> +
> +	lua_gen_const("STDIN_DEVNULL",		POPEN_FLAG_FD_STDIN_DEVNULL);
> +	lua_gen_const("STDOUT_DEVNULL",		POPEN_FLAG_FD_STDOUT_DEVNULL);
> +	lua_gen_const("STDERR_DEVNULL",		POPEN_FLAG_FD_STDERR_DEVNULL);
> +
> +	lua_gen_const("STDIN_CLOSE",		POPEN_FLAG_FD_STDIN_CLOSE);
> +	lua_gen_const("STDOUT_CLOSE",		POPEN_FLAG_FD_STDOUT_CLOSE);
> +	lua_gen_const("STDERR_CLOSE",		POPEN_FLAG_FD_STDERR_CLOSE);
> +
> +	lua_gen_const("SHELL",			POPEN_FLAG_SHELL);
> +	lua_gen_const("SETSID",			POPEN_FLAGS_SETSID);
> +	lua_gen_const("CLOSE_FDS",		POPEN_FLAG_CLOSE_FDS);
> +	lua_gen_const("RESTORE_SIGNALS",	POPEN_FLAGS_RESTORE_SIGNALS);
> +	lua_settable(L, -3);
> +
> +	lua_pushliteral(L, "state");
> +	lua_newtable(L);
> +
> +	lua_gen_const("ALIVE",			POPEN_STATE_ALIVE);
> +	lua_gen_const("EXITED",			POPEN_STATE_EXITED);
> +	lua_gen_const("SIGNALED",		POPEN_STATE_SIGNALED);
> +	lua_settable(L, -3);
> +
> +#undef lua_gen_const
> +
> +	lua_settable(L, -3);
> +	lua_pop(L, 1);
> +}
> diff --git a/src/lua/popen.h b/src/lua/popen.h
> new file mode 100644
> index 000000000..4e3f137c5
> --- /dev/null
> +++ b/src/lua/popen.h
> @@ -0,0 +1,44 @@
> +#ifndef INCLUDES_TARANTOOL_LUA_POPEN_H
> +#define INCLUDES_TARANTOOL_LUA_POPEN_H
> +/*
> + * Copyright 2010-2019, Tarantool AUTHORS, please see AUTHORS file.
> + *
> + * 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.
> + *
> + * THIS SOFTWARE IS PROVIDED BY <COPYRIGHT HOLDER> ``AS IS'' AND
> + * ANY EXPRESS 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
> + * <COPYRIGHT HOLDER> OR CONTRIBUTORS 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.
> + */
> +#if defined(__cplusplus)
> +extern "C" {
> +#endif /* defined(__cplusplus) */
> +
> +struct lua_State;
> +void tarantool_lua_popen_init(struct lua_State *L);
> +
> +#if defined(__cplusplus)
> +} /* extern "C" */
> +#endif /* defined(__cplusplus) */
> +
> +#endif /* INCLUDES_TARANTOOL_LUA_POPEN_H */
> diff --git a/src/lua/popen.lua b/src/lua/popen.lua
> new file mode 100644
> index 000000000..31af26cc6
> --- /dev/null
> +++ b/src/lua/popen.lua
> @@ -0,0 +1,516 @@
> +-- popen.lua (builtin file)
> +--
> +-- vim: ts=4 sw=4 et
> +
> +local buffer = require('buffer')
> +local popen = require('popen')
> +local fiber = require('fiber')
> +local ffi = require('ffi')
> +local bit = require('bit')
> +
> +local const_char_ptr_t = ffi.typeof('const char *')
> +
> +local builtin = popen.builtin
> +popen.builtin = nil
> +
> +local popen_methods = { }
> +
> +local function default_flags()
> +    local flags = popen.c.flag.NONE
> +
> +    -- default flags: close everything and use shell
> +    flags = bit.bor(flags, popen.c.flag.STDIN_CLOSE)
> +    flags = bit.bor(flags, popen.c.flag.STDOUT_CLOSE)
> +    flags = bit.bor(flags, popen.c.flag.STDERR_CLOSE)
> +    flags = bit.bor(flags, popen.c.flag.SHELL)
> +    flags = bit.bor(flags, popen.c.flag.SETSID)
> +    flags = bit.bor(flags, popen.c.flag.CLOSE_FDS)
> +    flags = bit.bor(flags, popen.c.flag.RESTORE_SIGNALS)
> +
> +    return flags
> +end
> +
> +--
> +-- A map for popen option keys into tfn ('true|false|nil') values
> +-- where bits are just set without additional manipulations.
> +--
> +-- For example stdin=true means to open stdin end for write,
> +-- stdin=false to close the end, finally stdin[=nil] means to
> +-- provide /dev/null into a child peer.
> +local flags_map_tfn = {
> +    stdin = {
> +        popen.c.flag.STDIN,
> +        popen.c.flag.STDIN_CLOSE,
> +        popen.c.flag.STDIN_DEVNULL,
> +    },
> +    stdout = {
> +        popen.c.flag.STDOUT,
> +        popen.c.flag.STDOUT_CLOSE,
> +        popen.c.flag.STDOUT_DEVNULL,
> +    },
> +    stderr = {
> +        popen.c.flag.STDERR,
> +        popen.c.flag.STDERR_CLOSE,
> +        popen.c.flag.STDERR_DEVNULL,
> +    },
> +}
> +
> +--
> +-- A map for popen option keys into tf ('true|false') values
> +-- where bits are set on 'true' and clear on 'false'.
> +local flags_map_tf = {
> +    shell = {
> +        popen.c.flag.SHELL,
> +    },
> +    close_fds = {
> +        popen.c.flag.CLOSE_FDS
> +    },
> +    restore_signals = {
> +        popen.c.flag.RESTORE_SIGNALS
> +    },
> +    start_new_session = {
> +        popen.c.flag.SETSID
> +    },
> +}
> +
> +--
> +-- Parses flags options from flags_map_tfn and
> +-- flags_map_tf tables.
> +local function parse_flags(epfx, flags, opts)
> +    if opts == nil then
> +        return flags
> +    end
> +    for k,v in pairs(opts) do
> +        if flags_map_tfn[k] == nil then
> +            if flags_map_tf[k] == nil then
> +                error(string.format("%s: Unknown key %s", epfx, k))
> +            end
> +            if v == true then
> +                flags = bit.bor(flags, flags_map_tf[k][1])
> +            elseif v == false then
> +                flags = bit.band(flags, bit.bnot(flags_map_tf[k][1]))
> +            else
> +                error(string.format("%s: Unknown value %s", epfx, v))
> +            end
> +        else
> +            if v == true then
> +                flags = bit.band(flags, bit.bnot(flags_map_tfn[k][2]))
> +                flags = bit.band(flags, bit.bnot(flags_map_tfn[k][3]))
> +                flags = bit.bor(flags, flags_map_tfn[k][1])
> +            elseif v == false then
> +                flags = bit.band(flags, bit.bnot(flags_map_tfn[k][1]))
> +                flags = bit.band(flags, bit.bnot(flags_map_tfn[k][3]))
> +                flags = bit.bor(flags, flags_map_tfn[k][2])
> +            elseif v == "devnull" then
> +                flags = bit.band(flags, bit.bnot(flags_map_tfn[k][1]))
> +                flags = bit.band(flags, bit.bnot(flags_map_tfn[k][2]))
> +                flags = bit.bor(flags, flags_map_tfn[k][3])
> +            else
> +                error(string.format("%s: Unknown value %s", epfx, v))
> +            end
> +        end
> +    end
> +    return flags
> +end
> +
> +--
> +-- Parse "mode" string to flags
> +local function parse_mode(epfx, flags, mode)
> +    if mode == nil then
> +        return flags
> +    end
> +    for i = 1, #mode do
> +        local c = mode:sub(i, i)
> +        if c == 'r' then
> +            flags = bit.band(flags, bit.bnot(popen.c.flag.STDOUT_CLOSE))
> +            flags = bit.bor(flags, popen.c.flag.STDOUT)
> +            flags = bit.band(flags, bit.bnot(popen.c.flag.STDERR_CLOSE))
> +            flags = bit.bor(flags, popen.c.flag.STDERR)
> +        elseif c == 'w' then
> +            flags = bit.band(flags, bit.bnot(popen.c.flag.STDIN_CLOSE))
> +            flags = bit.bor(flags, popen.c.flag.STDIN)
> +        else
> +            error(string.format("%s: Unknown mode %s", epfx, c))
> +        end
> +    end
> +    return flags
> +end
> +
> +--
> +-- Close a popen object and release all resources.
> +-- In case if there is a running child process
> +-- it will be killed.
> +--
> +-- Returns @ret = true if popen object is closed, and
> +-- @ret = false, @err ~= nil on error.
> +popen_methods.close = function(self)
> +    local ret, err = builtin.delete(self.cdata)
> +    if err ~= nil then
> +        return false, err
> +    end
> +    self.cdata = nil
> +    return true
> +end
> +
> +--
> +-- Kill a child process
> +--
> +-- Returns @ret = true on success,
> +-- @ret = false, @err ~= nil on error.
> +popen_methods.kill = function(self)
> +    return builtin.kill(self.cdata)
> +end
> +
> +--
> +-- Terminate a child process
> +--
> +-- Returns @ret = true on success,
> +-- @ret = false, @err ~= nil on error.
> +popen_methods.terminate = function(self)
> +    return builtin.term(self.cdata)
> +end
> +
> +--
> +-- Send signal with number @signo to a child process
> +--
> +-- Returns @ret = true on success,
> +-- @ret = false, @err ~= nil on error.
> +popen_methods.send_signal = function(self, signo)
> +    return builtin.signal(self.cdata, signo)
> +end
> +
> +--
> +-- Fetch a child process state
> +--
> +-- Returns @err = nil, @state = popen.c.state, @exit_code = num,
> +-- otherwise @err ~= nil.
> +popen_methods.state = function(self)
> +    return builtin.state(self.cdata)
> +end
> +
> +--
> +-- Wait until a child process get exited.
> +--
> +-- Returns the same as popen_methods.status.
> +popen_methods.wait = function(self)
> +    local err, state, code
> +    while true do
> +        err, state, code = builtin.state(self.cdata)
> +        if err or state ~= popen.c.state.ALIVE then
> +            break
> +        end
> +        fiber.sleep(self.wait_secs)
> +    end
> +    return err, state, code
> +end
> +
> +--
> +-- popen:read2 - read a stream of a child process
> +-- @opts:       options table
> +--
> +-- The options should have the following keys
> +--
> +-- @buf:        const_char_ptr_t buffer
> +-- @size:       size of the buffer
> +-- @flags:      stdout=true or stderr=true
> +-- @timeout:    read timeout in seconds, < 0 to ignore
> +--
> +-- Returns @res = bytes, err = nil in case if read processed
> +-- without errors, @res = nil, @err ~= nil otherwise.
> +popen_methods.read2 = function(self, opts)
> +    local flags = parse_flags("popen:read2",
> +                              popen.c.flag.NONE,
> +                              opts['flags'])
> +    local timeout = -1
> +
> +    if opts['buf'] == nil then
> +        error("popen:read2 {'buf'} key is missed")
> +    elseif opts['size'] == nil then
> +        error("popen:read2 {'size'} key is missed")
> +    elseif opts['timeout'] ~= nil then
> +        timeout = tonumber(opts['timeout'])
> +    end
> +
> +    return builtin.read(self.cdata, opts['buf'],
> +                        tonumber(opts['size']),
> +                        flags, timeout)
> +end
> +
> +--
> +-- popen:write2 - write to a child's streem
> +-- @opts:       options table
> +--
> +-- The options should have the following keys
> +--
> +-- @buf:        const_char_ptr_t buffer
> +-- @size:       size of the buffer
> +-- @flags:      stdin=true
> +-- @timeout:    write timeout in seconds, < 0 to ignore
> +--
> +-- Returns @err = nil on success, @err ~= nil otherwise.
> +popen_methods.write2 = function(self, opts)
> +    local flags = parse_flags("popen:write2",
> +                              popen.c.flag.NONE,
> +                              opts['flags'])
> +    local timeout = -1
> +
> +    if opts['buf'] == nil then
> +        error("popen:write2 {'buf'} key is missed")
> +    elseif opts['size'] == nil then
> +        error("popen:write2 {'size'} key is missed")
> +    elseif opts['timeout'] ~= nil then
> +        timeout = tonumber(opts['timeout'])
> +    end
> +
> +    return builtin.write(self.cdata, opts['buf'],
> +                         tonumber(opts['size']),
> +                         flags, timeout)
> +end
> +
> +--
> +-- popen:read - read string from a stream
> +-- @stderr:     set to true to read from stderr, optional
> +-- @timeout:    timeout in seconds, optional
> +--
> +-- Returns a result string, or res = nil, @err ~= nil on error.
> +popen_methods.read = function(self, stderr, timeout)
> +    local ibuf = buffer.ibuf()
> +    local buf = ibuf:reserve(self.read_size)
> +    local flags
> +
> +    if stderr ~= nil then
> +        flags = { stderr = true }
> +    else
> +        flags = { stdout = true }
> +    end
> +
> +    if timeout == nil then
> +        timeout = -1
> +    end
> +
> +    local res, err = self:read2({
> +        buf     = buf,
> +        size    = self.read_size,
> +        flags   = flags,
> +        timeout = timeout,
> +    })
> +
> +    if err ~= nil then
> +        ibuf:recycle()
> +        return nil, err
> +    end
> +
> +    ibuf:alloc(res)
> +    res = ffi.string(ibuf.rpos, ibuf:size())
> +    ibuf:recycle()
> +
> +    return res
> +end
> +
> +--
> +-- popen:write - write string @str to stdin stream
> +-- @str:        string to write
> +-- @timeout:    timeout in seconds, optional
> +--
> +-- Returns @err = nil on success, @err ~= nil on error.
> +popen_methods.write = function(self, str, timeout)
> +    if timeout == nil then
> +        timeout = -1
> +    end
> +    return self:write2({
> +        buf     = str,
> +        size    = #str,
> +        flags   = { stdin = true },
> +        timeout = timeout,
> +    })
> +end
> +
> +--
> +-- popen:info -- obtain information about a popen object
> +--
> +-- Returns @info table, err == nil on success,
> +-- @info = nil, @err ~= nil otherwise.
> +--
> +-- The @info table contains the following keys:
> +--
> +-- @pid:        pid of a child process
> +-- @flags:      flags associated (popen.c.flag bitmap)
> +-- @stdout:     parent peer for stdout, -1 if closed
> +-- @stderr:     parent peer for stderr, -1 if closed
> +-- @stdin:      parent peer for stdin, -1 if closed
> +-- @state:      alive | exited | signaled
> +-- @exit_code:  exit code of a child process if been
> +--              exited or killed by a signal
> +--
> +-- The child should never be in "unknown" state, reserved
> +-- for unexpected errors.
> +--
> +popen_methods.info = function(self)
> +    return builtin.info(self.cdata)
> +end
> +
> +--
> +-- Create a new popen object from options
> +local function popen_reify(opts)
> +    local cdata, err = builtin.new(opts)
> +    if err ~= nil then
> +        return nil, err
> +    end
> +
> +    local handle = {
> +        -- a handle itself for future use
> +        cdata           = cdata,
> +
> +        -- sleeping period for the @wait method
> +        wait_secs       = 0.3,
> +
> +        -- size of a read buffer to allocate
> +        -- in case of implicit read, this number
> +        -- is taken from luatest repo to fit the
> +        -- value people are familiar with
> +        read_size       = 4096,
> +    }
> +
> +    setmetatable(handle, {
> +        __index     = popen_methods,
> +    })
> +
> +    return handle
> +end
> +
> +--
> +-- popen.posix - create a new child process and execute a command inside
> +-- @command:    a command to run
> +-- @mode:       r - to grab child's stdout for read
> +--              (stderr kept opened as well for 2>&1 redirection)
> +--              w - to obtain stdin for write
> +--
> +-- Note: Since there are two options only the following parameters
> +-- are implied (see default_flags): all fds except stdin/out/err
> +-- are closed, signals are restored to default handlers, the command
> +-- is executed in a new session.
> +--
> +-- Examples:
> +--
> +--  ph = require('popen').posix("date", "r")
> +--      runs date as "sh -c date" to read the output,
> +--      closing all file descriptors except stdout/err
> +--      inside a child process
> +--
> +popen.posix = function(command, mode)
> +    local flags = default_flags()
> +
> +    if type(command) ~= 'string' then
> +        error("Usage: popen.posix(command[, rw])")
> +    end
> +
> +    -- Mode gives simplified flags
> +    flags = parse_mode("popen.posix", flags, mode)
> +
> +    local opts = {
> +        argv    = { command },
> +        argc    = 1,
> +        flags   = flags,
> +        envc    = -1,
> +    }
> +
> +    return popen_reify(opts)
> +end
> +
> +-- popen.popen - execute a child program in a new process
> +-- @opt:    options table
> +--
> +-- @opts carries of the following options:
> +--
> +--  @argv:  an array of a program to run with
> +--          command line options, mandatory
> +--
> +--  @env:   an array of environment variables to
> +--          be used inside a process, if not
> +--          set then the current environment is
> +--          inherited, if set to an empty array
> +--          then the environment will be dropped
> +--
> +--  @flags: a dictionary to describe communication pipes
> +--          and other parameters of a process to run
> +--
> +--      stdin=true      to write into STDIN_FILENO of a child process
> +--      stdin=false     to close STDIN_FILENO inside a child process [*]
> +--      stdin="devnull" a child will get /dev/null as STDIN_FILENO
> +--
> +--      stdout=true     to read STDOUT_FILENO of a child process
> +--      stdout=false    to close STDOUT_FILENO inside a child process [*]
> +--      stdout="devnull" a child will get /dev/null as STDOUT_FILENO
> +--
> +--      stderr=true     to read STDERR_FILENO from a child process
> +--      stderr=false    to close STDERR_FILENO inside a child process [*]
> +--      stderr="devnull" a child will get /dev/null as STDERR_FILENO
> +--
> +--      shell=true      runs a child process via "sh -c" [*]
> +--      shell=false     invokes a child process executable directly
> +--
> +--      close_fds=true  close all inherited fds from a parent [*]
> +--
> +--      restore_signals=true
> +--                      all signals modified by a caller reset
> +--                      to default handler [*]
> +--
> +--      start_new_session=true
> +--                      start executable inside a new session [*]
> +--
> +-- [*] are default values
> +--
> +-- Examples:
> +--
> +--  ph = require('popen').popen({argv = {"date"}, flags = {stdout=true}})
> +--  ph:read()
> +--  ph:close()
> +--
> +--      Execute 'date' command inside a shell, read the result
> +--      and close the popen object
> +--
> +--  ph = require('popen').popen({argv = {"/usr/bin/echo", "-n", "hello"},
> +--                               flags = {stdout=true, shell=false}})
> +--  ph:read()
> +--  ph:close()
> +--
> +--      Execute /usr/bin/echo with arguments '-n','hello' directly
> +--      without using a shell, read the result from stdout and close
> +--      the popen object
> +--
> +popen.popen = function(opts)
> +    local flags = default_flags()
> +
> +    if opts == nil or type(opts) ~= 'table' then
> +        error("Usage: popen({argv={}[, envp={}, flags={}]")
> +    end
> +
> +    -- Test for required arguments
> +    if opts["argv"] == nil then
> +        error("popen: argv key is missing")
> +    end
> +
> +    -- Process flags and save updated mask
> +    -- to pass into engine (in case of missing
> +    -- flags we just use defaults).
> +    if opts["flags"] ~= nil then
> +        flags = parse_flags("popen", flags, opts["flags"])
> +    end
> +    opts["flags"] = flags
> +
> +    -- We will need a number of args for speed sake
> +    opts["argc"] = #opts["argv"]
> +
> +    -- Same applies to the environment (note though
> +    -- that env={} is pretty valid and env=nil
> +    -- as well.
> +    if opts["env"] ~= nil then
> +        opts["envc"] = #opts["env"]
> +    else
> +        opts["envc"] = -1
> +    end
> +
> +    return popen_reify(opts)
> +end
> +
> +return popen

^ permalink raw reply	[flat|nested] 16+ messages in thread

* Re: [Tarantool-patches] [PATCH v6 2/4] popen: Introduce a backend engine
  2019-12-17 12:54 ` [Tarantool-patches] [PATCH v6 2/4] popen: Introduce a backend engine Cyrill Gorcunov
  2019-12-20  8:11   ` Konstantin Osipov
@ 2019-12-26  7:14   ` Konstantin Osipov
  2019-12-26  7:19     ` Cyrill Gorcunov
  2020-01-09 11:23     ` Cyrill Gorcunov
  1 sibling, 2 replies; 16+ messages in thread
From: Konstantin Osipov @ 2019-12-26  7:14 UTC (permalink / raw)
  To: Cyrill Gorcunov; +Cc: tml

* Cyrill Gorcunov <gorcunov@gmail.com> [19/12/17 15:57]:
> +ssize_t
> +popen_read_timeout(struct popen_handle *handle, void *buf,
> +		   size_t count, unsigned int flags,
> +		   ev_tstamp timeout)
> +{
> +	int idx = flags & POPEN_FLAG_FD_STDOUT ?
> +		STDOUT_FILENO : STDERR_FILENO;
> +
> +	if (!popen_may_io(handle, idx, flags))
> +		return -1;
> +
> +	if (count > (size_t)SSIZE_MAX) {
> +		errno = E2BIG;
> +		return -1;
> +	}
> +
> +	if (timeout < 0.)
> +		timeout = TIMEOUT_INFINITY;
> +
> +	say_debug("popen: %d: read idx [%s:%d] buf %p count %zu "
> +		  "fds %d timeout %.9g",
> +		  handle->pid, stdX_str(idx), idx, buf, count,
> +		  handle->fds[idx], timeout);
> +
> +	return coio_read_fd_timeout(handle->fds[idx],
> +				    buf, count, timeout);
> +}

Right, so could you please use struct coio here, and not
coio_read_fd_timeout? I.e. convert handle->fds to coio?


-- 
Konstantin Osipov, Moscow, Russia

^ permalink raw reply	[flat|nested] 16+ messages in thread

* Re: [Tarantool-patches] [PATCH v6 2/4] popen: Introduce a backend engine
  2019-12-26  7:14   ` Konstantin Osipov
@ 2019-12-26  7:19     ` Cyrill Gorcunov
  2020-01-09 11:23     ` Cyrill Gorcunov
  1 sibling, 0 replies; 16+ messages in thread
From: Cyrill Gorcunov @ 2019-12-26  7:19 UTC (permalink / raw)
  To: Konstantin Osipov; +Cc: tml

On Thu, Dec 26, 2019 at 10:14:41AM +0300, Konstantin Osipov wrote:
...
> > +	say_debug("popen: %d: read idx [%s:%d] buf %p count %zu "
> > +		  "fds %d timeout %.9g",
> > +		  handle->pid, stdX_str(idx), idx, buf, count,
> > +		  handle->fds[idx], timeout);
> > +
> > +	return coio_read_fd_timeout(handle->fds[idx],
> > +				    buf, count, timeout);
> > +}
> 
> Right, so could you please use struct coio here, and not
> coio_read_fd_timeout? I.e. convert handle->fds to coio?

I'll try. Thanks Kostya!

^ permalink raw reply	[flat|nested] 16+ messages in thread

* Re: [Tarantool-patches] [PATCH v6 2/4] popen: Introduce a backend engine
  2019-12-26  7:14   ` Konstantin Osipov
  2019-12-26  7:19     ` Cyrill Gorcunov
@ 2020-01-09 11:23     ` Cyrill Gorcunov
  1 sibling, 0 replies; 16+ messages in thread
From: Cyrill Gorcunov @ 2020-01-09 11:23 UTC (permalink / raw)
  To: Konstantin Osipov; +Cc: tml

On Thu, Dec 26, 2019 at 10:14:41AM +0300, Konstantin Osipov wrote:
> * Cyrill Gorcunov <gorcunov@gmail.com> [19/12/17 15:57]:
> > +ssize_t
> > +popen_read_timeout(struct popen_handle *handle, void *buf,
> > +		   size_t count, unsigned int flags,
> > +		   ev_tstamp timeout)
> > +{
> > +	int idx = flags & POPEN_FLAG_FD_STDOUT ?
> > +		STDOUT_FILENO : STDERR_FILENO;
> > +
> > +	if (!popen_may_io(handle, idx, flags))
> > +		return -1;
> > +
> > +	if (count > (size_t)SSIZE_MAX) {
> > +		errno = E2BIG;
> > +		return -1;
> > +	}
> > +
> > +	if (timeout < 0.)
> > +		timeout = TIMEOUT_INFINITY;
> > +
> > +	say_debug("popen: %d: read idx [%s:%d] buf %p count %zu "
> > +		  "fds %d timeout %.9g",
> > +		  handle->pid, stdX_str(idx), idx, buf, count,
> > +		  handle->fds[idx], timeout);
> > +
> > +	return coio_read_fd_timeout(handle->fds[idx],
> > +				    buf, count, timeout);
> > +}
> 
> Right, so could you please use struct coio here, and not
> coio_read_fd_timeout? I.e. convert handle->fds to coio?

Kostya, could you please elaborate this moment, since I
suspect I'm missing something.

1) coio service data, such as coio_wdata, coio_wait_cb is
   static to coio.cc file, you propose to make it exportable
   to other parts of tarantool code?

2) the case where timeout is really needed is when pipes
   are either empty (thus we will wait with timout until
   data appear, and adding fd into event loop doesn't look
   like a hot path) or full (where we will wait for data
   to be read first by another peer).

3) assuming we somehow exported data from (1) I don't really
   see how could we code the way we would put fd into backend
   once for all timelife of the popen object. Could you point
   please?

^ permalink raw reply	[flat|nested] 16+ messages in thread

end of thread, other threads:[~2020-01-09 11:23 UTC | newest]

Thread overview: 16+ messages (download: mbox.gz / follow: Atom feed)
-- links below jump to the message on this page --
2019-12-17 12:54 [Tarantool-patches] [PATCH v6 0/4] popen: Add ability to run external process Cyrill Gorcunov
2019-12-17 12:54 ` [Tarantool-patches] [PATCH v6 1/4] coio: Export helpers and provide coio_read_fd_timeout Cyrill Gorcunov
2019-12-20  7:48   ` Konstantin Osipov
2019-12-20 14:50     ` Cyrill Gorcunov
2019-12-17 12:54 ` [Tarantool-patches] [PATCH v6 2/4] popen: Introduce a backend engine Cyrill Gorcunov
2019-12-20  8:11   ` Konstantin Osipov
2019-12-20 11:52     ` Cyrill Gorcunov
2019-12-20 12:04       ` Konstantin Osipov
2019-12-20 12:10         ` Cyrill Gorcunov
2019-12-20 12:11     ` Alexander Turenko
2019-12-26  7:14   ` Konstantin Osipov
2019-12-26  7:19     ` Cyrill Gorcunov
2020-01-09 11:23     ` Cyrill Gorcunov
2019-12-17 12:54 ` [Tarantool-patches] [PATCH v6 3/4] popen/lua: Add popen module Cyrill Gorcunov
2019-12-20 15:41   ` Maxim Melentiev
2019-12-17 12:54 ` [Tarantool-patches] [PATCH v6 4/4] popen/test: Add base test cases Cyrill Gorcunov

This is a public inbox, see mirroring instructions
for how to clone and mirror all data and code used for this inbox