Tarantool development patches archive
 help / color / mirror / Atom feed
* [Tarantool-patches] [PATCH 0/2] Popen Lua module
@ 2020-04-18  4:13 Alexander Turenko
  2020-04-18  4:13 ` [Tarantool-patches] [PATCH 1/2] popen: always free resources in popen_delete() Alexander Turenko
                   ` (2 more replies)
  0 siblings, 3 replies; 13+ messages in thread
From: Alexander Turenko @ 2020-04-18  4:13 UTC (permalink / raw)
  To: Cyrill Gorcunov, Igor Munkin; +Cc: tarantool-patches

The patchset implements popen Lua module upward existing backend popen
engine.

The first patch changes popen_delete() behaviour to always free
resources even when killing a process (or a process group) fails. We
(Alexander, Cyrill and Igor) revisited the contract for closing the
handle after observing killpg() behaviour on a group of zombie processes
on Mac OS. I added those details to API documentation comments to don't
surprise a user with this.

The first patch continues previous preliminary popen engine series:

https://lists.tarantool.org/pipermail/tarantool-patches/2020-April/015609.html
https://lists.tarantool.org/pipermail/tarantool-patches/2020-April/015888.html

The second patch implements the Lua API for popen. It is quite
strightforward and should be considered as very basic implementation. We
plan to enhance it further in the upcoming releases (issues are to be
filed).

The main goal of the module it to provide popen implementation that is
integrated into our event loop (similar to fio and socket modules). Side
goal is to provide ability to feed data to a child process input and
read data from its output and so work with streaming programs (like
`grep`) and even interactive programs (like `python -i` interpreter).

Let's consider the API of module as beta: it may be change in
backward-incompatible manner in future releases if it will be valuable
enough.

It seems the main application of the module it to write various testing
code and so the API should be extended in the future with convenient
shortcuts: developers usually don't very like writting tests and it
should be at least more-or-less convenient.

I plan to extend the API for reading with ability to read certain amount
of bytes, to read line-by-line (or based on other delimiter), to read
into a buffer (ibuf): like it is implemented in the socket module. I
plan to share an RFC document about read streams.

ph:wait() should gain ability to set a timeout and should be rewritten
to use triggers / fiber conds instead of check-yield-again loop.

----

https://github.com/tarantool/tarantool/issues/4031
https://github.com/tarantool/tarantool/tree/Totktonada/gh-4031-popen-13-full-ci

Changelog entry:

## Functionality added or changed
### Lua

* Added `popen` built-in module (gh-4031).

  The module provides popen implementation that is integrated with
  tarantool's event loop (like built-in `fio` and `socket` modules).

  It support bidirectional communication with a process: the module can
  feed input to a process and capture its output. This way it allows to
  run streaming programs (like `grep`) and even work interactively with
  outside REPL (say, `python -i`).

  A key feature of the implementation is that it uses vfork() under hood
  and so does not copy virtual memory tables. Copying of them may be
  quite time consuming: os.execute() takes ~2.5 seconds when 80 GiB is
  allocated for memtx. Moreover, when memory overcommit is disabled
  (which is default) it would be not possible to fork a process when
  more then half of available physical memory is mapped to tarantool's
  process.

  The API should be considered as beta: it is quite basic and will be
  extended with convenience features. On the other hand, it may be
  changed in a backward-incompatible manner in the future releases if it
  will be valuable enough.

Alexander Turenko (2):
  popen: always free resources in popen_delete()
  popen: add popen Lua module

 src/CMakeLists.txt          |    1 +
 src/lib/core/popen.c        |   88 +-
 src/lua/init.c              |    2 +
 src/lua/popen.c             | 2457 +++++++++++++++++++++++++++++++++++
 src/lua/popen.h             |   44 +
 test/app-tap/popen.test.lua |  605 +++++++++
 6 files changed, 3182 insertions(+), 15 deletions(-)
 create mode 100644 src/lua/popen.c
 create mode 100644 src/lua/popen.h
 create mode 100755 test/app-tap/popen.test.lua

-- 
2.25.0

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

* [Tarantool-patches] [PATCH 1/2] popen: always free resources in popen_delete()
  2020-04-18  4:13 [Tarantool-patches] [PATCH 0/2] Popen Lua module Alexander Turenko
@ 2020-04-18  4:13 ` Alexander Turenko
  2020-04-18  6:55   ` Cyrill Gorcunov
  2020-04-18  4:13 ` [Tarantool-patches] [PATCH 2/2] popen: add popen Lua module Alexander Turenko
  2020-04-20  7:52 ` [Tarantool-patches] [PATCH 0/2] Popen " Kirill Yukhin
  2 siblings, 1 reply; 13+ messages in thread
From: Alexander Turenko @ 2020-04-18  4:13 UTC (permalink / raw)
  To: Cyrill Gorcunov, Igor Munkin; +Cc: tarantool-patches

The function still set a diagnostics when a signal sending fails and
returns -1, but it is purely informational result: for logging or so. It
reflects notes about dealing with failures in Linux's `man 2 close`:

 | Note, however, that a failure return should be used only for
 | diagnostic purposes <...> or remedial purposes <...>.
 |
 | <...> Linux  kernel always releases the file descriptor early in the
 | close operation, freeing it for reuse; the steps that may return an
 | error <...> occur only later in the close operation.
 |
 | Many other implementations similarly always close the file descriptor
 | <...> even if they subsequently report an error on return from
 | close(). POSIX.1 is currently silent on this point, but there are
 | plans to mandate this behavior in the next major release of the
 | standard.

When kill or killpg returns EPERM a caller usually unable to overcome it
somehow: retrying is not sufficient here. So there are no needs to keep
the handle: a caller refuses the handle and don't want to perform any
other operation on it.  The open engine do its best to kill a child
process or a process group, but when it is not possible, just set the a
diagnostic and free handle resources anyway.

Left comments about observed Mac OS behaviour regarding killing a
process group, where all processes are zombies (or just when a process
group leader is zombie, don't sure): it gives EPERM instead of ESRCH
from killpg(). This result should not surprise a user, so it should be
documented. See [1] for another description of the problem (I don't find
any official information about this).

[1]: https://bugzilla.mozilla.org/show_bug.cgi?id=1329528

Part of #4031
---
 src/lib/core/popen.c | 88 ++++++++++++++++++++++++++++++++++++--------
 1 file changed, 73 insertions(+), 15 deletions(-)

diff --git a/src/lib/core/popen.c b/src/lib/core/popen.c
index 3a12a439b..da4b89757 100644
--- a/src/lib/core/popen.c
+++ b/src/lib/core/popen.c
@@ -688,11 +688,27 @@ popen_state_str(unsigned int state)
  * Possible errors:
  *
  * - SystemError: a process does not exists anymore
+ *
+ *                Aside of a non-exist process it is also
+ *                set for a zombie process or when all
+ *                processes in a group are zombies (but
+ *                see note re Mac OS below).
+ *
  * - SystemError: invalid signal number
+ *
  * - SystemError: no permission to send a signal to
  *                a process or a process group
  *
- * Set errno to ESRCH when a process does not exist.
+ *                It is set on Mac OS when a signal is sent
+ *                to a process group, where a group leader
+ *                is zombie (or when all processes in it
+ *                are zombies, don't sure).
+ *
+ *                Whether it may appear due to other
+ *                reasons is unclear.
+ *
+ * Set errno to ESRCH when a process does not exist or is
+ * zombie.
  */
 int
 popen_send_signal(struct popen_handle *handle, int signo)
@@ -732,39 +748,74 @@ popen_send_signal(struct popen_handle *handle, int signo)
 /**
  * Delete a popen handle.
  *
- * The function performs the following actions:
+ * Send SIGKILL and free the handle.
  *
- * - Kill a child process or a process group depending of
- *   POPEN_FLAG_GROUP_SIGNAL (if the process is alive and
- *   POPEN_FLAG_KEEP_CHILD flag is not set).
- * - Close all fds occupied by the handle.
- * - Remove the handle from a living list.
- * - Free all occupied memory.
+ * Details about signaling:
  *
- * @see popen_send_signal() for note about ..._GROUP_SIGNAL.
+ * - The signal is sent only when ...KEEP_CHILD is not set.
+ * - The signal is sent only when a process is alive according
+ *   to the information available on current even loop iteration.
+ *   (There is a gap here: a zombie may be signaled; it is
+ *   harmless.)
+ * - The signal is sent to a process or a grocess group depending
+ *   of ..._GROUP_SIGNAL flag. @see popen_send_signal() for note
+ *   about ..._GROUP_SIGNAL.
  *
- * Return 0 at success and -1 at failure (and set a diag).
+ * Resources are released disregarding of whether a signal
+ * sending succeeds: all fds occupied by the handle are closed,
+ * the handle is removed from a living list, all occupied memory
+ * is freed.
  *
- * Possible errors:
+ * The function may return 0 or -1 (and set a diag) as usual,
+ * but it always frees the handle resources. So any return
+ * value usually means success for a caller. The return
+ * value and diagnostics are purely informational: it is
+ * for logging or same kind of reporting.
+ *
+ * Possible diagnostics (don't consider them as errors):
  *
  * - SystemError: no permission to send a signal to
  *                a process or a process group
+ *
+ *                This error may appear due to Mac OS
+ *                behaviour on zombies when
+ *                ..._GROUP_SIGNAL is set,
+ *                @see popen_send_signal().
+ *
+ *                Whether it may appear due to other
+ *                reasons is unclear.
+ *
+ * Always return 0 when a process is known as dead: no signal
+ * will be send, so no 'failure' may appear.
  */
 int
 popen_delete(struct popen_handle *handle)
 {
+	/*
+	 * Save a result and a failure reason of the
+	 * popen_send_signal() call.
+	 */
+	int rc = 0;
+	struct diag *diag = diag_get();
+	struct error *e = NULL;
+
 	size_t i;
 
 	assert(handle != NULL);
 
 	if ((handle->flags & POPEN_FLAG_KEEP_CHILD) == 0) {
 		/*
-		 * Unable to kill the process -- give an error.
+		 * Unable to kill the process -- save the error
+		 * and pass over.
 		 * The process is not exist already -- pass over.
 		 */
 		if (popen_send_signal(handle, SIGKILL) != 0 &&
-		    errno != ESRCH)
-			return -1;
+		    errno != ESRCH) {
+			rc = -1;
+			e = diag_last_error(diag);
+			assert(e != NULL);
+			error_ref(e);
+		}
 	}
 
 	for (i = 0; i < lengthof(handle->ios); i++) {
@@ -787,7 +838,14 @@ popen_delete(struct popen_handle *handle)
 
 	rlist_del(&handle->list);
 	handle_free(handle);
-	return 0;
+
+	/* Restore an error from popen_send_signal() if any. */
+	if (rc != 0) {
+		diag_set_error(diag, e);
+		error_unref(e);
+	}
+
+	return rc;
 }
 
 /**
-- 
2.25.0

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

* [Tarantool-patches] [PATCH 2/2] popen: add popen Lua module
  2020-04-18  4:13 [Tarantool-patches] [PATCH 0/2] Popen Lua module Alexander Turenko
  2020-04-18  4:13 ` [Tarantool-patches] [PATCH 1/2] popen: always free resources in popen_delete() Alexander Turenko
@ 2020-04-18  4:13 ` Alexander Turenko
  2020-04-18  6:57   ` Cyrill Gorcunov
                     ` (2 more replies)
  2020-04-20  7:52 ` [Tarantool-patches] [PATCH 0/2] Popen " Kirill Yukhin
  2 siblings, 3 replies; 13+ messages in thread
From: Alexander Turenko @ 2020-04-18  4:13 UTC (permalink / raw)
  To: Cyrill Gorcunov, Igor Munkin; +Cc: tarantool-patches

Co-developed-by: Cyrill Gorcunov <gorcunov@gmail.com>
Co-developed-by: Igor Munkin <imun@tarantool.org>

Fixes #4031

@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.

Module functions
================

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

`popen.shell(command[, mode]) -> handle, err`
---------------------------------------------

Execute a shell command.

@param command  a command to run, mandatory
@param mode     communication mode, optional
                'w'    to use ph:write()
                'r'    to use ph:read()
                'R'    to use ph:read({stderr = true})
                nil    inherit parent's std* file descriptors

Several mode characters can be set together: 'rw', 'rRw', etc.

This function is just shortcut for popen.new({command}, opts)
with opts.{shell,setsid,group_signal} set to `true` and
and opts.{stdin,stdout,stderr} set based on `mode` parameter.

All std* streams are inherited from parent by default if it is
not changed using mode: 'r' for stdout, 'R' for stderr, 'w' for
stdin.

Raise an error on incorrect parameters:

- IllegalParams: incorrect type or value of a parameter.

Return a popen handle on success.

Return `nil, err` on a failure.
@see popen.new() for possible reasons.

Example:

 | local popen = require('popen')
 |
 | local ph = popen.shell('date', 'r')
 | local date = ph:read():rstrip()
 | ph:close()
 | print(date)

Execute 'sh -c date' command, read the output and close the
popen object.

`popen.new(argv[, opts]) -> handle, err`
----------------------------------------

Execute a child program in a new process.

@param argv  an array of a program to run with
             command line options, mandatory;
             absolute path to the program is required

@param opts  table of options

@param opts.stdin   action on STDIN_FILENO
@param opts.stdout  action on STDOUT_FILENO
@param opts.stderr  action on STDERR_FILENO

File descriptor actions:

    popen.opts.INHERIT  (== 'inherit') [default]
                        inherit the fd from the parent
    popen.opts.DEVNULL  (== 'devnull')
                        open /dev/null on the fd
    popen.opts.CLOSE    (== 'close')
                        close the fd
    popen.opts.PIPE     (== 'pipe')
                        feed data from/to the fd to parent
                        using a pipe

@param opts.env  an array of environment variables to
                 be used inside a process;
                 - when is not set then the current
                   environment is inherited;
                 - if set then the environment will be
                   replaced
                 - if set to an empty array then the
                   environment will be dropped

@param opts.shell            (boolean, default: false)
       true                  run a child process via
                             'sh -c "${opts.argv}"'
       false                 call the executable directly

@param opts.setsid           (boolean, default: false)
       true                  start executable inside a new
                             session
       false                 don't do that

@param opts.close_fds        (boolean, default: true)
       true                  close all inherited fds from a
                             parent
       false                 don't do that

@param opts.restore_signals  (boolean, default: true)
       true                  reset all signal actions
                             modified in parent's process
       false                 inherit changed actions

@param opts.group_signal     (boolean, default: false)
       true                  send signal to a child process
                             group (only when opts.setsid is
                             enabled)
       false                 send signal to a child process
                             only

@param opts.keep_child       (boolean, default: false)
       true                  don't send SIGKILL to a child
                             process at freeing (by :close()
                             or Lua GC)
       false                 send SIGKILL to a child process
                             (or a process group if
                             opts.group_signal is enabled) at
                             :close() or collecting of the
                             handle by Lua GC

The returned handle is subject to garbage collection, but
it may be freed explicitly using :close(). SIGKILL is sent
to the child process at :close() / GC if it is alive and
opts.keep_child is not set.

It is recommended to use opts.setsid + opts.group_signal
if a child process may spawn its own childs and they all
should be killed together.

Note: A signal will not be sent if the child process is
already dead: otherwise we might kill another process that
occupies the same PID later. This means that if the child
process dies before its own childs, the function will not
send a signal to the process group even when opts.setsid and
opts.group_signal are set.

Use os.environ() to pass copy of current environment with
several replacements (see example 2 below).

Raise an error on incorrect parameters:

- IllegalParams: incorrect type or value of a parameter.
- IllegalParams: group signal is set, while setsid is not.

Return a popen handle on success.

Return `nil, err` on a failure. Possible reasons:

- SystemError: dup(), fcntl(), pipe(), vfork() or close()
               fails in the parent process.
- SystemError: (temporary restriction) the parent process
               has closed stdin, stdout or stderr.
- OutOfMemory: unable to allocate the handle or a temporary
               buffer.

Example 1:

 | local popen = require('popen')
 |
 | local ph = popen.new({'/bin/date'}, {
 |     stdout = popen.opts.PIPE,
 | })
 | local date = ph:read():rstrip()
 | ph:close()
 | print(date) -- Thu 16 Apr 2020 01:40:56 AM MSK

Execute 'date' command, read the result and close the
popen object.

Example 2:

 | local popen = require('popen')
 |
 | local env = os.environ()
 | env['FOO'] = 'bar'
 |
 | local ph = popen.new({'echo "${FOO}"'}, {
 |     stdout = popen.opts.PIPE,
 |     shell = true,
 |     env = env,
 | })
 | local res = ph:read():rstrip()
 | ph:close()
 | print(res) -- bar

It is quite similar to the previous one, but sets the
environment variable and uses shell builtin 'echo' to
show it.

Example 3:

 | local popen = require('popen')
 |
 | local ph = popen.new({'echo hello >&2'}, { -- !!
 |     stderr = popen.opts.PIPE,              -- !!
 |     shell = true,
 | })
 | local res = ph:read({stderr = true}):rstrip()
 | ph:close()
 | print(res) -- hello

This example demonstrates how to capture child's stderr.

Example 4:

 | local function call_jq(stdin, filter)
 |     -- Start jq process, connect to stdin, stdout and stderr.
 |     local jq_argv = {'/usr/bin/jq', '-M', '--unbuffered', filter}
 |     local ph, err = popen.new(jq_argv, {
 |         stdin = popen.opts.PIPE,
 |         stdout = popen.opts.PIPE,
 |         stderr = popen.opts.PIPE,
 |     })
 |     if ph == nil then return nil, err end
 |
 |     -- Write input data to child's stdin and send EOF.
 |     local ok, err = ph:write(stdin)
 |     if not ok then return nil, err end
 |     ph:shutdown({stdin = true})
 |
 |     -- Read everything until EOF.
 |     local chunks = {}
 |     while true do
 |         local chunk, err = ph:read()
 |         if chunk == nil then
 |             ph:close()
 |             return nil, err
 |         end
 |         if chunk == '' then break end -- EOF
 |         table.insert(chunks, chunk)
 |     end
 |
 |     -- Read diagnostics from stderr if any.
 |     local err = ph:read({stderr = true})
 |     if err ~= '' then
 |         ph:close()
 |         return nil, err
 |     end
 |
 |     -- Glue all chunks, strip trailing newline.
 |     return table.concat(chunks):rstrip()
 | end

Demonstrates how to run a stream program (like `grep`, `sed`
and so), write to its stdin and read from its stdout.

The example assumes that input data are small enough to fit
a pipe buffer (typically 64 KiB, but depends on a platform
and its configuration). It will stuck in :write() for large
data. How to handle this case: call :read() in a loop in
another fiber (start it before a first :write()).

If a process writes large text to stderr, it may fill out
stderr pipe buffer and block in write(2, ...). So we need to
read stderr too in another fiber to handle this case.

Handle methods
==============

`popen_handle:read([opts]) -> str, err`
---------------------------------------

Read data from a child peer.

@param handle        handle of a child process
@param opts          an options table
@param opts.stdout   whether to read from stdout, boolean
                     (default: true)
@param opts.stderr   whether to read from stderr, boolean
                     (default: false)
@param opts.timeout  time quota in seconds
                     (default: 100 years)

Read data from stdout or stderr streams with @a timeout.
By default it reads from stdout. Set @a opts.stderr to
`true` to read from stderr.

Raise an error on incorrect parameters or when the fiber is
cancelled:

- IllegalParams:    incorrect type or value of a parameter.
- IllegalParams:    called on a closed handle.
- IllegalParams:    opts.stdout and opts.stderr are set both
- IllegalParams:    a requested IO operation is not supported
                    by the handle (stdout / stderr is not
                    piped).
- IllegalParams:    attempt to operate on a closed file
                    descriptor.
- FiberIsCancelled: cancelled by an outside code.

Return a string on success, an empty string at EOF.

Return `nil, err` on a failure. Possible reasons:

- SocketError: an IO error occurs at read().
- TimedOut:    @a timeout quota is exceeded.
- OutOfMemory: no memory space for a buffer to read into.
- LuajitError: ("not enough memory"): no memory space for
               the Lua string.

`popen_handle:write(str[, opts]) -> str, err`
---------------------------------------------

Write data to a child peer.

@param handle        a handle of a child process
@param str           a string to write
@param opts          table of options
@param opts.timeout  time quota in seconds
                     (default: 100 years)

Write string @a str to stdin stream of a child process.

The function may yield forever if a child process does
not read data from stdin and a pipe buffer becomes full.
Size of this buffer depends on a platform. Use
@a opts.timeout when unsure.

When @a opts.timeout is not set, the function blocks
(yields the fiber) until all data is written or an error
happened.

Raise an error on incorrect parameters or when the fiber is
cancelled:

- IllegalParams:    incorrect type or value of a parameter.
- IllegalParams:    called on a closed handle.
- IllegalParams:    string length is greater then SSIZE_MAX.
- IllegalParams:    a requested IO operation is not supported
                    by the handle (stdin is not piped).
- IllegalParams:    attempt to operate on a closed file
                    descriptor.
- FiberIsCancelled: cancelled by an outside code.

Return `true` on success.

Return `nil, err` on a failure. Possible reasons:

- SocketError: an IO error occurs at write().
- TimedOut:    @a timeout quota is exceeded.

`popen_handle:shutdown(opts) -> true`
------------------------------------------

Close parent's ends of std* fds.

@param handle        handle of a child process
@param opts          an options table
@param opts.stdin    close parent's end of stdin, boolean
@param opts.stdout   close parent's end of stdout, boolean
@param opts.stderr   close parent's end of stderr, boolean

The main reason to use this function is to send EOF to
child's stdin. However parent's end of stdout / stderr
may be closed too.

The function does not fail on already closed fds (idempotence).
However it fails on attempt to close the end of a pipe that was
never exist. In other words, only those std* options that
were set to popen.opts.PIPE at a handle creation may be used
here (for popen.shell: 'r' corresponds to stdout, 'R' to stderr
and 'w' to stdin).

The function does not close any fds on a failure: either all
requested fds are closed or neither of them.

Example:

 | local popen = require('popen')
 |
 | local ph = popen.shell('sed s/foo/bar/', 'rw')
 | ph:write('lorem foo ipsum')
 | ph:shutdown({stdin = true})
 | local res = ph:read()
 | ph:close()
 | print(res) -- lorem bar ipsum

Raise an error on incorrect parameters:

- IllegalParams:  an incorrect handle parameter.
- IllegalParams:  called on a closed handle.
- IllegalParams:  neither stdin, stdout nor stderr is choosen.
- IllegalParams:  a requested IO operation is not supported
                  by the handle (one of std* is not piped).

Return `true` on success.

`popen_handle:terminate() -> ok, err`
-------------------------------------

Send SIGTERM signal to a child process.

@param handle  a handle carries child process to terminate

The function only sends SIGTERM signal and does NOT
free any resources (popen handle memory and file
descriptors).

@see popen_handle:signal() for errors and return values.

`popen_handle:kill() -> ok, err`
--------------------------------

Send SIGKILL signal to a child process.

@param handle  a handle carries child process to kill

The function only sends SIGKILL signal and does NOT
free any resources (popen handle memory and file
descriptors).

@see popen_handle:signal() for errors and return values.

`popen_handle:signal(signo) -> ok, err`
---------------------------------------

Send signal to a child process.

@param handle  a handle carries child process to be signaled
@param signo   signal number to send

When opts.setsid and opts.group_signal are set on the handle
the signal is sent to the process group rather than to the
process. @see popen.new() for details about group
signaling.

Note: The module offers popen.signal.SIG* constants, because
some signals have different numbers on different platforms.

Raise an error on incorrect parameters:

- IllegalParams:    an incorrect handle parameter.
- IllegalParams:    called on a closed handle.

Return `true` if signal is sent.

Return `nil, err` on a failure. Possible reasons:

- SystemError: a process does not exists anymore

               Aside of a non-exist process it is also
               returned for a zombie process or when all
               processes in a group are zombies (but
               see note re Mac OS below).

- SystemError: invalid signal number

- SystemError: no permission to send a signal to
               a process or a process group

               It is returned on Mac OS when a signal is
               sent to a process group, where a group leader
               is zombie (or when all processes in it
               are zombies, don't sure).

               Whether it may appear due to other
               reasons is unclear.

`popen_handle:info() -> res, err`
---------------------------------

Return information about popen handle.

@param handle  a handle of a child process

Raise an error on incorrect parameters:

- IllegalParams: an incorrect handle parameter.
- IllegalParams: called on a closed handle.

Return information about the handle in the following
format:

    {
        pid = <number> or <nil>,
        command = <string>,
        opts = <table>,
        status = <table>,
        stdin = one-of(
            popen.stream.OPENED (== 'opened'),
            popen.stream.CLOSED (== 'closed'),
            nil,
        ),
        stdout = one-of(
            popen.stream.OPENED (== 'opened'),
            popen.stream.CLOSED (== 'closed'),
            nil,
        ),
        stderr = one-of(
            popen.stream.OPENED (== 'opened'),
            popen.stream.CLOSED (== 'closed'),
            nil,
        ),
    }

`pid` is a process id of the process when it is alive,
otherwise `pid` is nil.

`command` is a concatenation of space separated arguments
that were passed to execve(). Multiword arguments are quoted.
Quotes inside arguments are not escaped.

`opts` is a table of handle options in the format of
popen.new() `opts` parameter. `opts.env` is not shown here,
because the environment variables map is not stored in a
handle.

`status` is a table that represents a process status in the
following format:

    {
        state = one-of(
            popen.state.ALIVE    (== 'alive'),
            popen.state.EXITED   (== 'exited'),
            popen.state.SIGNALED (== 'signaled'),
        )

        -- Present when `state` is 'exited'.
        exit_code = <number>,

        -- Present when `state` is 'signaled'.
        signo = <number>,
        signame = <string>,
    }

`stdin`, `stdout`, `stderr` reflect status of parent's end
of a piped stream. When a stream is not piped the field is
not present (`nil`). When it is piped, the status may be
one of the following:

- popen.stream.OPENED  (== 'opened')
- popen.stream.CLOSED  (== 'closed')

The status may be changed from 'opened' to 'closed'
by :shutdown({std... = true}) call.

Example 1 (tarantool console):

 | tarantool> require('popen').new({'/usr/bin/touch', '/tmp/foo'})
 | ---
 | - command: /usr/bin/touch /tmp/foo
 |   status:
 |     state: alive
 |   opts:
 |     stdout: inherit
 |     stdin: inherit
 |     group_signal: false
 |     keep_child: false
 |     close_fds: true
 |     restore_signals: true
 |     shell: false
 |     setsid: false
 |     stderr: inherit
 |   pid: 9499
 | ...

Example 2 (tarantool console):

 | tarantool> require('popen').shell('grep foo', 'wrR')
 | ---
 | - stdout: opened
 |   command: sh -c 'grep foo'
 |   stderr: opened
 |   status:
 |     state: alive
 |   stdin: opened
 |   opts:
 |     stdout: pipe
 |     stdin: pipe
 |     group_signal: true
 |     keep_child: false
 |     close_fds: true
 |     restore_signals: true
 |     shell: true
 |     setsid: true
 |     stderr: pipe
 |   pid: 10497
 | ...

`popen_handle:wait() -> res, err`
---------------------------------

Wait until a child process get exited or signaled.

@param handle  a handle of process to wait

Raise an error on incorrect parameters or when the fiber is
cancelled:

- IllegalParams:    an incorrect handle parameter.
- IllegalParams:    called on a closed handle.
- FiberIsCancelled: cancelled by an outside code.

Return a process status table (the same as ph.status and
ph.info().status). @see popen_handle:info() for the format
of the table.

`popen_handle:close() -> ok, err`
---------------------------------

Close a popen handle.

@param handle  a handle to close

Basically it kills a process using SIGKILL and releases all
resources assosiated with the popen handle.

Details about signaling:

- The signal is sent only when opts.keep_child is not set.
- The signal is sent only when a process is alive according
  to the information available on current even loop iteration.
  (There is a gap here: a zombie may be signaled; it is
  harmless.)
- The signal is sent to a process or a grocess group depending
  of opts.group_signal. (@see lbox_popen_new() for details of
  group signaling).

Resources are released disregarding of whether a signal
sending succeeds: fds are closed, memory is released,
the handle is marked as closed.

No operation is possible on a closed handle except
:close(), which always successful on closed handle
(idempotence).

Raise an error on incorrect parameters:

- IllegalParams: an incorrect handle parameter.

The function may return `true` or `nil, err`, but it always
frees the handle resources. So any return value usually
means success for a caller. The return values are purely
informational: it is for logging or same kind of reporting.

Possible diagnostics (don't consider them as errors):

- SystemError: no permission to send a signal to
               a process or a process group

               This diagnostics may appear due to
               Mac OS behaviour on zombies when
               opts.group_signal is set,
               @see lbox_popen_signal().

               Whether it may appear due to other
               reasons is unclear.

Always return `true` when a process is known as dead (say,
after ph:wait()): no signal will be send, so no 'failure'
may appear.

Handle fields
=============

- popen_handle.pid
- popen_handle.command
- popen_handle.opts
- popen_handle.status
- popen_handle.stdin
- popen_handle.stdout
- popen_handle.stderr

See popen_handle:info() for description of those fields.

Module constants
================

- popen.opts
  - INHERIT (== 'inherit')
  - DEVNULL (== 'devnull')
  - CLOSE   (== 'close')
  - PIPE    (== 'pipe')

- popen.signal
  - SIGTERM (== 9)
  - SIGKILL (== 15)
  - ...

- popen.state
  - ALIVE    (== 'alive')
  - EXITED   (== 'exited')
  - SIGNALED (== 'signaled')

- popen.stream
  - OPENED  (== 'opened')
  - CLOSED  (== 'closed')
```
---
 src/CMakeLists.txt          |    1 +
 src/lua/init.c              |    2 +
 src/lua/popen.c             | 2457 +++++++++++++++++++++++++++++++++++
 src/lua/popen.h             |   44 +
 test/app-tap/popen.test.lua |  605 +++++++++
 5 files changed, 3109 insertions(+)
 create mode 100644 src/lua/popen.c
 create mode 100644 src/lua/popen.h
 create mode 100755 test/app-tap/popen.test.lua

diff --git a/src/CMakeLists.txt b/src/CMakeLists.txt
index de9680bcc..2b5ea1b87 100644
--- a/src/CMakeLists.txt
+++ b/src/CMakeLists.txt
@@ -114,6 +114,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 28b6b2d62..5b4b5b463 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"
@@ -455,6 +456,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..40f4343eb
--- /dev/null
+++ b/src/lua/popen.c
@@ -0,0 +1,2457 @@
+/*
+ * Copyright 2010-2020, 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 <small/region.h>
+
+#include "diag.h"
+#include "core/popen.h"
+#include "core/fiber.h"
+#include "core/exception.h"
+#include "tarantool_ev.h"
+
+#include "lua/utils.h"
+#include "lua/fiber.h"
+#include "lua/popen.h"
+
+/* {{{ Constants */
+
+static const char *popen_handle_uname = "popen_handle";
+static const char *popen_handle_closed_uname = "popen_handle_closed";
+
+#define POPEN_LUA_READ_BUF_SIZE        4096
+#define POPEN_LUA_WAIT_DELAY           0.1
+#define POPEN_LUA_ENV_CAPACITY_DEFAULT 256
+
+/**
+ * Helper map for transformation between std* popen.new() options
+ * and popen backend engine flags.
+ */
+static const struct {
+	/* Name for error messages. */
+	const char *option_name;
+
+	unsigned int mask_devnull;
+	unsigned int mask_close;
+	unsigned int mask_pipe;
+} pfd_map[POPEN_FLAG_FD_STDEND_BIT] = {
+	{
+		.option_name	= "opts.stdin",
+		.mask_devnull	= POPEN_FLAG_FD_STDIN_DEVNULL,
+		.mask_close	= POPEN_FLAG_FD_STDIN_CLOSE,
+		.mask_pipe	= POPEN_FLAG_FD_STDIN,
+	}, {
+		.option_name	= "opts.stdout",
+		.mask_devnull	= POPEN_FLAG_FD_STDOUT_DEVNULL,
+		.mask_close	= POPEN_FLAG_FD_STDOUT_CLOSE,
+		.mask_pipe	= POPEN_FLAG_FD_STDOUT,
+	}, {
+		.option_name	= "opts.stderr",
+		.mask_devnull	= POPEN_FLAG_FD_STDERR_DEVNULL,
+		.mask_close	= POPEN_FLAG_FD_STDERR_CLOSE,
+		.mask_pipe	= POPEN_FLAG_FD_STDERR,
+	},
+};
+
+/* }}} */
+
+/* {{{ Signals */
+
+static const struct {
+	const char *signame;
+	int signo;
+} popen_lua_signals[] = {
+#ifdef SIGHUP
+	{"SIGHUP", SIGHUP},
+#endif
+#ifdef SIGINT
+	{"SIGINT", SIGINT},
+#endif
+#ifdef SIGQUIT
+	{"SIGQUIT", SIGQUIT},
+#endif
+#ifdef SIGILL
+	{"SIGILL", SIGILL},
+#endif
+#ifdef SIGTRAP
+	{"SIGTRAP", SIGTRAP},
+#endif
+#ifdef SIGABRT
+	{"SIGABRT", SIGABRT},
+#endif
+#ifdef SIGIOT
+	{"SIGIOT", SIGIOT},
+#endif
+#ifdef SIGBUS
+	{"SIGBUS", SIGBUS},
+#endif
+#ifdef SIGFPE
+	{"SIGFPE", SIGFPE},
+#endif
+#ifdef SIGKILL
+	{"SIGKILL", SIGKILL},
+#endif
+#ifdef SIGUSR1
+	{"SIGUSR1", SIGUSR1},
+#endif
+#ifdef SIGSEGV
+	{"SIGSEGV", SIGSEGV},
+#endif
+#ifdef SIGUSR2
+	{"SIGUSR2", SIGUSR2},
+#endif
+#ifdef SIGPIPE
+	{"SIGPIPE", SIGPIPE},
+#endif
+#ifdef SIGALRM
+	{"SIGALRM", SIGALRM},
+#endif
+#ifdef SIGTERM
+	{"SIGTERM", SIGTERM},
+#endif
+#ifdef SIGSTKFLT
+	{"SIGSTKFLT", SIGSTKFLT},
+#endif
+#ifdef SIGCHLD
+	{"SIGCHLD", SIGCHLD},
+#endif
+#ifdef SIGCONT
+	{"SIGCONT", SIGCONT},
+#endif
+#ifdef SIGSTOP
+	{"SIGSTOP", SIGSTOP},
+#endif
+#ifdef SIGTSTP
+	{"SIGTSTP", SIGTSTP},
+#endif
+#ifdef SIGTTIN
+	{"SIGTTIN", SIGTTIN},
+#endif
+#ifdef SIGTTOU
+	{"SIGTTOU", SIGTTOU},
+#endif
+#ifdef SIGURG
+	{"SIGURG", SIGURG},
+#endif
+#ifdef SIGXCPU
+	{"SIGXCPU", SIGXCPU},
+#endif
+#ifdef SIGXFSZ
+	{"SIGXFSZ", SIGXFSZ},
+#endif
+#ifdef SIGVTALRM
+	{"SIGVTALRM", SIGVTALRM},
+#endif
+#ifdef SIGPROF
+	{"SIGPROF", SIGPROF},
+#endif
+#ifdef SIGWINCH
+	{"SIGWINCH", SIGWINCH},
+#endif
+#ifdef SIGIO
+	{"SIGIO", SIGIO},
+#endif
+#ifdef SIGPOLL
+	{"SIGPOLL", SIGPOLL},
+#endif
+#ifdef SIGPWR
+	{"SIGPWR", SIGPWR},
+#endif
+#ifdef SIGSYS
+	{"SIGSYS", SIGSYS},
+#endif
+	{NULL, 0},
+};
+
+/* }}} */
+
+/* {{{ Stream actions */
+
+#define POPEN_LUA_STREAM_INHERIT	"inherit"
+#define POPEN_LUA_STREAM_DEVNULL	"devnull"
+#define POPEN_LUA_STREAM_CLOSE		"close"
+#define POPEN_LUA_STREAM_PIPE		"pipe"
+
+static const struct {
+	const char *name;
+	const char *value;
+	bool devnull;
+	bool close;
+	bool pipe;
+} popen_lua_actions[] = {
+	{
+		.name		= "INHERIT",
+		.value		= POPEN_LUA_STREAM_INHERIT,
+		.devnull	= false,
+		.close		= false,
+		.pipe		= false,
+	},
+	{
+		.name		= "DEVNULL",
+		.value		= POPEN_LUA_STREAM_DEVNULL,
+		.devnull	= true,
+		.close		= false,
+		.pipe		= false,
+	},
+	{
+		.name		= "CLOSE",
+		.value		= POPEN_LUA_STREAM_CLOSE,
+		.devnull	= false,
+		.close		= true,
+		.pipe		= false,
+	},
+	{
+		.name		= "PIPE",
+		.value		= POPEN_LUA_STREAM_PIPE,
+		.devnull	= false,
+		.close		= false,
+		.pipe		= true,
+	},
+	{NULL, NULL, false, false, false},
+};
+
+/* }}} */
+
+/* {{{ Stream statuses */
+
+#define POPEN_LUA_STREAM_STATUS_OPENED "opened"
+#define POPEN_LUA_STREAM_STATUS_CLOSED "closed"
+
+static const struct {
+	const char *name;
+	const char *value;
+} popen_lua_stream_statuses[] = {
+	{"OPENED",	POPEN_LUA_STREAM_STATUS_OPENED},
+	{"CLOSED",	POPEN_LUA_STREAM_STATUS_CLOSED},
+	{NULL, NULL},
+};
+
+/* }}} */
+
+/* {{{ Process states */
+
+#define POPEN_LUA_STATE_ALIVE "alive"
+#define POPEN_LUA_STATE_EXITED "exited"
+#define POPEN_LUA_STATE_SIGNALED "signaled"
+
+static const struct {
+	const char *name;
+	const char *value;
+} popen_lua_states[] = {
+	{"ALIVE",	POPEN_LUA_STATE_ALIVE},
+	{"EXITED",	POPEN_LUA_STATE_EXITED},
+	{"SIGNALED",	POPEN_LUA_STATE_SIGNALED},
+	{NULL, NULL},
+};
+
+/* }}} */
+
+/* {{{ General-purpose Lua helpers */
+
+/**
+ * Extract a string from the Lua stack.
+ *
+ * Return (const char *) if so, otherwise return NULL.
+ *
+ * Unlike luaL_checkstring() it accepts only a string and does not
+ * accept a number.
+ *
+ * Unlike luaL_checkstring() it does not raise an error, but
+ * returns NULL when a type is not what is excepted.
+ */
+static const char *
+luaL_check_string_strict(struct lua_State *L, int idx, size_t *len_ptr)
+{
+	if (lua_type(L, idx) != LUA_TSTRING)
+		return NULL;
+
+	const char *res = lua_tolstring(L, idx, len_ptr);
+	assert(res != NULL);
+	return res;
+}
+
+/**
+ * Extract a timeout value from the Lua stack.
+ *
+ * Return -1.0 when error occurs.
+ */
+static ev_tstamp
+luaT_check_timeout(struct lua_State *L, int idx)
+{
+	if (lua_type(L, idx) == LUA_TNUMBER)
+		return lua_tonumber(L, idx);
+	/* FIXME: Support cdata<int64_t> and cdata<uint64_t>. */
+	return -1.0;
+}
+
+/**
+ * Helper for luaT_push_string_noxc().
+ */
+static int
+luaT_push_string_noxc_wrapper(struct lua_State *L)
+{
+	char *str = (char *)lua_topointer(L, 1);
+	size_t len = lua_tointeger(L, 2);
+	lua_pushlstring(L, str, len);
+	return 1;
+}
+
+/**
+ * Push a string to the Lua stack.
+ *
+ * Return 0 at success, -1 at failure and set a diag.
+ *
+ * Possible errors:
+ *
+ * - LuajitError ("not enough memory"): no memory space for the
+ *   Lua string.
+ */
+static int
+luaT_push_string_noxc(struct lua_State *L, char *str, size_t len)
+{
+	lua_pushcfunction(L, luaT_push_string_noxc_wrapper);
+	lua_pushlightuserdata(L, str);
+	lua_pushinteger(L, len);
+	return luaT_call(L, 2, 1);
+}
+
+/* }}} */
+
+/* {{{ Popen handle userdata manipulations */
+
+/**
+ * Extract popen handle from the Lua stack.
+ *
+ * Return NULL in case of unexpected type.
+ */
+static struct popen_handle *
+luaT_check_popen_handle(struct lua_State *L, int idx, bool *is_closed_ptr)
+{
+	struct popen_handle **handle_ptr =
+		luaL_testudata(L, idx, popen_handle_uname);
+	bool is_closed = false;
+
+	if (handle_ptr == NULL) {
+		handle_ptr = luaL_testudata(L, idx, popen_handle_closed_uname);
+		is_closed = true;
+	}
+
+	if (handle_ptr == NULL)
+		return NULL;
+	assert(*handle_ptr != NULL);
+
+	if (is_closed_ptr != NULL)
+		*is_closed_ptr = is_closed;
+	return *handle_ptr;
+}
+
+/**
+ * Push popen handle into the Lua stack.
+ *
+ * Return 1 -- amount of pushed values.
+ */
+static int
+luaT_push_popen_handle(struct lua_State *L, struct popen_handle *handle)
+{
+	*(struct popen_handle **)lua_newuserdata(L, sizeof(handle)) = handle;
+	luaL_getmetatable(L, popen_handle_uname);
+	lua_setmetatable(L, -2);
+	return 1;
+}
+
+/**
+ * Mark popen handle as closed.
+ *
+ * Does not perform any checks whether @a idx points
+ * to a popen handle.
+ *
+ * The closed state is needed primarily to protect a
+ * handle from double freeing.
+ */
+static void
+luaT_mark_popen_handle_closed(struct lua_State *L, int idx)
+{
+	luaL_getmetatable(L, popen_handle_closed_uname);
+	lua_setmetatable(L, idx);
+}
+
+/* }}} */
+
+/* {{{ Push popen handle info to the Lua stack */
+
+/**
+ * Convert ...FD_STD* flags to a popen.opts.<...> constant.
+ *
+ * If flags are invalid, push 'invalid' string.
+ *
+ * Push the result onto the Lua stack.
+ */
+static int
+luaT_push_popen_stdX_action(struct lua_State *L, int fd, unsigned int flags)
+{
+	for (size_t i = 0; popen_lua_actions[i].name != NULL; ++i) {
+		bool devnull	= (flags & pfd_map[fd].mask_devnull) != 0;
+		bool close	= (flags & pfd_map[fd].mask_close) != 0;
+		bool pipe	= (flags & pfd_map[fd].mask_pipe) != 0;
+
+		if (devnull == popen_lua_actions[i].devnull &&
+		    close   == popen_lua_actions[i].close &&
+		    pipe    == popen_lua_actions[i].pipe) {
+			lua_pushstring(L, popen_lua_actions[i].value);
+			return 1;
+		}
+	}
+
+	lua_pushliteral(L, "invalid");
+	return 1;
+}
+
+/**
+ * Push a piped stream status (opened or closed) to the Lua stack.
+ */
+static int
+luaT_push_popen_stdX_status(struct lua_State *L, struct popen_handle *handle,
+			    int idx)
+{
+	if ((handle->flags & pfd_map[idx].mask_pipe) == 0) {
+		/* Stream action: INHERIT, DEVNULL or CLOSE. */
+		lua_pushnil(L);
+		return 1;
+	}
+
+	/* Stream action: PIPE. */
+	if (handle->ios[idx].fd < 0)
+		lua_pushliteral(L, POPEN_LUA_STREAM_STATUS_CLOSED);
+	else
+		lua_pushliteral(L, POPEN_LUA_STREAM_STATUS_OPENED);
+
+	return 1;
+}
+
+/**
+ * Push popen options as a Lua table.
+ *
+ * Environment variables are not stored in a popen handle and so
+ * missed here.
+ */
+static int
+luaT_push_popen_opts(struct lua_State *L, unsigned int flags)
+{
+	lua_createtable(L, 0, 8);
+
+	luaT_push_popen_stdX_action(L, STDIN_FILENO, flags);
+	lua_setfield(L, -2, "stdin");
+
+	luaT_push_popen_stdX_action(L, STDOUT_FILENO, flags);
+	lua_setfield(L, -2, "stdout");
+
+	luaT_push_popen_stdX_action(L, STDERR_FILENO, flags);
+	lua_setfield(L, -2, "stderr");
+
+	/* env is skipped */
+
+	lua_pushboolean(L, (flags & POPEN_FLAG_SHELL) != 0);
+	lua_setfield(L, -2, "shell");
+
+	lua_pushboolean(L, (flags & POPEN_FLAG_SETSID) != 0);
+	lua_setfield(L, -2, "setsid");
+
+	lua_pushboolean(L, (flags & POPEN_FLAG_CLOSE_FDS) != 0);
+	lua_setfield(L, -2, "close_fds");
+
+	lua_pushboolean(L, (flags & POPEN_FLAG_RESTORE_SIGNALS) != 0);
+	lua_setfield(L, -2, "restore_signals");
+
+	lua_pushboolean(L, (flags & POPEN_FLAG_GROUP_SIGNAL) != 0);
+	lua_setfield(L, -2, "group_signal");
+
+	lua_pushboolean(L, (flags & POPEN_FLAG_KEEP_CHILD) != 0);
+	lua_setfield(L, -2, "keep_child");
+
+	return 1;
+}
+
+/**
+ * Push a process status to the Lua stack as a table.
+ *
+ * The format of the resulting table:
+ *
+ *     {
+ *         state = one-of(
+ *             popen.state.ALIVE    (== 'alive'),
+ *             popen.state.EXITED   (== 'exited'),
+ *             popen.state.SIGNALED (== 'signaled'),
+ *         )
+ *
+ *         -- Present when `state` is 'exited'.
+ *         exit_code = <number>,
+ *
+ *         -- Present when `state` is 'signaled'.
+ *         signo = <number>,
+ *         signame = <string>,
+ *     }
+ *
+ * @param state POPEN_STATE_{ALIVE,EXITED,SIGNALED}
+ *
+ * @param exit_code is exit code when the process is exited and a
+ * signal number when a process is signaled.
+ *
+ * @see enum popen_states
+ * @see popen_state()
+ */
+static int
+luaT_push_popen_process_status(struct lua_State *L, int state, int exit_code)
+{
+	lua_createtable(L, 0, 3);
+
+	switch (state) {
+	case POPEN_STATE_ALIVE:
+		lua_pushliteral(L, POPEN_LUA_STATE_ALIVE);
+		lua_setfield(L, -2, "state");
+		break;
+	case POPEN_STATE_EXITED:
+		lua_pushliteral(L, POPEN_LUA_STATE_EXITED);
+		lua_setfield(L, -2, "state");
+		lua_pushinteger(L, exit_code);
+		lua_setfield(L, -2, "exit_code");
+		break;
+	case POPEN_STATE_SIGNALED:
+		lua_pushliteral(L, POPEN_LUA_STATE_SIGNALED);
+		lua_setfield(L, -2, "state");
+		lua_pushinteger(L, exit_code);
+		lua_setfield(L, -2, "signo");
+
+		/*
+		 * FIXME: Preallocate signo -> signal name
+		 * mapping.
+		 */
+		const char *signame = "unknown";
+		for (int i = 0; popen_lua_signals[i].signame != NULL; ++i) {
+			if (popen_lua_signals[i].signo == exit_code)
+				signame = popen_lua_signals[i].signame;
+		}
+		lua_pushstring(L, signame);
+		lua_setfield(L, -2, "signame");
+
+		break;
+	default:
+		unreachable();
+	}
+
+	return 1;
+}
+
+/* }}} */
+
+/* {{{ Errors */
+
+/**
+ * Raise IllegalParams error re closed popen handle.
+ */
+static int
+luaT_popen_handle_closed_error(struct lua_State *L)
+{
+	diag_set(IllegalParams, "popen: attempt to operate on a closed handle");
+	return luaT_error(L);
+}
+
+/**
+ * Raise IllegalParams error re wrong parameter.
+ */
+static int
+luaT_popen_param_value_error(struct lua_State *L, const char *got,
+			     const char *func_name, const char *param,
+			     const char *exp)
+{
+	static const char *fmt =
+		"%s: wrong parameter \"%s\": expected %s, got %s";
+	diag_set(IllegalParams, fmt, func_name, param, exp, got);
+	return luaT_error(L);
+}
+
+/**
+ * Raise IllegalParams error re wrong parameter type.
+ */
+static int
+luaT_popen_param_type_error(struct lua_State *L, int idx, const char *func_name,
+			    const char *param, const char *exp)
+{
+	const char *typename = idx == 0 ?
+		"<unknown>" : lua_typename(L, lua_type(L, idx));
+	static const char *fmt =
+		"%s: wrong parameter \"%s\": expected %s, got %s";
+	diag_set(IllegalParams, fmt, func_name, param, exp, typename);
+	return luaT_error(L);
+}
+
+/**
+ * Raise IllegalParams error re wrong parameter type in an array.
+ */
+static int
+luaT_popen_array_elem_type_error(struct lua_State *L, int idx,
+				 const char *func_name, const char *param,
+				 int num, const char *exp)
+{
+	const char *typename = idx == 0 ?
+		"<unknown>" : lua_typename(L, lua_type(L, idx));
+	static const char *fmt =
+		"%s: wrong parameter \"%s[%d]\": expected %s, got %s";
+	diag_set(IllegalParams, fmt, func_name, param, num, exp, typename);
+	return luaT_error(L);
+}
+
+/* }}} */
+
+/* {{{ Parameter parsing */
+
+/**
+ * Parse popen.new() "opts.{stdin,stdout,stderr}" parameter.
+ *
+ * Result: @a flags_p is updated.
+ *
+ * Raise an error in case of the incorrect parameter.
+ */
+static void
+luaT_popen_parse_stdX(struct lua_State *L, int idx, int fd,
+		      unsigned int *flags_p)
+{
+	const char *action;
+	size_t action_len;
+	if ((action = luaL_check_string_strict(L, idx, &action_len)) == NULL)
+		luaT_popen_param_type_error(L, idx, "popen.new",
+					    pfd_map[fd].option_name,
+					    "string or nil");
+
+	unsigned int flags = *flags_p;
+
+	/* See popen_lua_actions. */
+	if (strncmp(action, POPEN_LUA_STREAM_INHERIT, action_len) == 0) {
+		flags &= ~pfd_map[fd].mask_devnull;
+		flags &= ~pfd_map[fd].mask_close;
+		flags &= ~pfd_map[fd].mask_pipe;
+	} else if (strncmp(action, POPEN_LUA_STREAM_DEVNULL, action_len) == 0) {
+		flags |= pfd_map[fd].mask_devnull;
+		flags &= ~pfd_map[fd].mask_close;
+		flags &= ~pfd_map[fd].mask_pipe;
+	} else if (strncmp(action, POPEN_LUA_STREAM_CLOSE, action_len) == 0) {
+		flags &= ~pfd_map[fd].mask_devnull;
+		flags |= pfd_map[fd].mask_close;
+		flags &= ~pfd_map[fd].mask_pipe;
+	} else if (strncmp(action, POPEN_LUA_STREAM_PIPE, action_len) == 0) {
+		flags &= ~pfd_map[fd].mask_devnull;
+		flags &= ~pfd_map[fd].mask_close;
+		flags |= pfd_map[fd].mask_pipe;
+	} else {
+		luaT_popen_param_value_error(L, action, "popen.new",
+					     pfd_map[fd].option_name,
+					     "popen.opts.<...> constant");
+		unreachable();
+	}
+
+	*flags_p = flags;
+}
+
+/**
+ * Glue key and value on the Lua stack into "key=value" entry.
+ *
+ * Raise an error in case of the incorrect parameter.
+ *
+ * Return NULL in case of an allocation error and set a diag
+ * (OutOfMemory).
+ */
+static char *
+luaT_popen_parse_env_entry(struct lua_State *L, int key_idx, int value_idx,
+			   struct region *region)
+{
+	size_t key_len;
+	size_t value_len;
+	const char *key = luaL_check_string_strict(L, key_idx, &key_len);
+	const char *value = luaL_check_string_strict(L, value_idx, &value_len);
+	if (key == NULL || value == NULL) {
+		luaT_popen_param_value_error(L, "a non-string key or value",
+					     "popen.new", "opts.env",
+					     "{[<string>] = <string>, ...}");
+		unreachable();
+		return NULL;
+	}
+
+	/*
+	 * FIXME: Don't sure, but maybe it would be right to
+	 * validate key and value here against '=', '\0' and
+	 * maybe other symbols.
+	 */
+
+	/* entry = "${key}=${value}" */
+	size_t entry_size = key_len + value_len + 2;
+	char *entry = region_alloc(region, entry_size);
+	if (entry == NULL) {
+		diag_set(OutOfMemory, entry_size, "region_alloc", "env entry");
+		return NULL;
+	}
+	memcpy(entry, key, key_len);
+	size_t pos = key_len;
+	entry[pos++] = '=';
+	memcpy(entry + pos, value, value_len);
+	pos += value_len;
+	entry[pos++] = '\0';
+	assert(pos == entry_size);
+
+	return entry;
+}
+
+/**
+ * Parse popen.new() "opts.env" parameter.
+ *
+ * Return a new array in the `extern char **environ` format
+ * (NULL terminated array of "foo=bar" strings).
+ *
+ * Strings and array of them are allocated on the provided
+ * region. A caller should call region_used() before invoking
+ * this function and call region_truncate() when the result
+ * is not needed anymore. Alternatively a caller may assume
+ * that fiber_gc() will collect this memory eventually, but
+ * it is recommended to do so only for rare paths.
+ *
+ * Raise an error in case of the incorrect parameter.
+ *
+ * Return NULL in case of an allocation error and set a diag
+ * (OutOfMemory).
+ */
+static char **
+luaT_popen_parse_env(struct lua_State *L, int idx, struct region *region)
+{
+	if (lua_type(L, idx) != LUA_TTABLE) {
+		luaT_popen_param_type_error(L, idx, "popen.new", "opts.env",
+					    "table or nil");
+		unreachable();
+		return NULL;
+	}
+
+	/* Calculate absolute value in the stack. */
+	if (idx < 0)
+		idx = lua_gettop(L) + idx + 1;
+
+	size_t capacity = POPEN_LUA_ENV_CAPACITY_DEFAULT;
+	size_t size = capacity * sizeof(char *);
+	size_t region_svp = region_used(region);
+	char **env = region_alloc(region, size);
+	if (env == NULL) {
+		diag_set(OutOfMemory, size, "region_alloc", "env array");
+		return NULL;
+	}
+	size_t nr_env = 0;
+
+	bool only_count = false;
+
+	/*
+	 * Traverse over the table and fill `env` array. If
+	 * default `env` capacity is not enough, discard
+	 * everything, but continue iterating to count entries.
+	 */
+	lua_pushnil(L);
+	while (lua_next(L, idx) != 0) {
+		/*
+		 * Can we store the next entry and trailing NULL?
+		 */
+		if (nr_env >= capacity - 1) {
+			env = NULL;
+			region_truncate(region, region_svp);
+			only_count = true;
+		}
+		if (only_count) {
+			++nr_env;
+			lua_pop(L, 1);
+			continue;
+		}
+		char *entry = luaT_popen_parse_env_entry(L, -2, -1, region);
+		if (entry == NULL) {
+			region_truncate(region, region_svp);
+			return NULL;
+		}
+		env[nr_env++] = entry;
+		lua_pop(L, 1);
+	}
+
+	if (! only_count) {
+		assert(nr_env < capacity);
+		env[nr_env] = NULL;
+		return env;
+	}
+
+	/*
+	 * Now we know exact amount of elements. Run
+	 * the traverse again and fill `env` array.
+	 */
+	capacity = nr_env + 1;
+	size = capacity * sizeof(char *);
+	if ((env = region_alloc(region, size)) == NULL) {
+		region_truncate(region, region_svp);
+		diag_set(OutOfMemory, size, "region_alloc", "env array");
+		return NULL;
+	}
+	nr_env = 0;
+	lua_pushnil(L);
+	while (lua_next(L, idx) != 0) {
+		char *entry = luaT_popen_parse_env_entry(L, -2, -1, region);
+		if (entry == NULL) {
+			region_truncate(region, region_svp);
+			return NULL;
+		}
+		assert(nr_env < capacity - 1);
+		env[nr_env++] = entry;
+		lua_pop(L, 1);
+	}
+	assert(nr_env == capacity - 1);
+	env[nr_env] = NULL;
+	return env;
+}
+
+/**
+ * Parse popen.new() "opts" parameter.
+ *
+ * Prerequisite: @a opts should be zero filled.
+ *
+ * Result: @a opts structure is filled.
+ *
+ * Raise an error in case of the incorrect parameter.
+ *
+ * Return 0 at success. Allocates opts->env on @a region if
+ * needed. @see luaT_popen_parse_env() for details how to
+ * free it.
+ *
+ * Return -1 in case of an allocation error and set a diag
+ * (OutOfMemory).
+ */
+static int
+luaT_popen_parse_opts(struct lua_State *L, int idx, struct popen_opts *opts,
+		      struct region *region)
+{
+	/*
+	 * Default flags: inherit std*, close other fds,
+	 * restore signals.
+	 */
+	opts->flags = POPEN_FLAG_NONE		|
+		POPEN_FLAG_CLOSE_FDS		|
+		POPEN_FLAG_RESTORE_SIGNALS;
+
+	/* Parse options. */
+	if (lua_type(L, idx) == LUA_TTABLE) {
+		lua_getfield(L, idx, "stdin");
+		if (! lua_isnil(L, -1)) {
+			luaT_popen_parse_stdX(L, -1, STDIN_FILENO,
+					      &opts->flags);
+		}
+		lua_pop(L, 1);
+
+		lua_getfield(L, idx, "stdout");
+		if (! lua_isnil(L, -1))
+			luaT_popen_parse_stdX(L, -1, STDOUT_FILENO,
+					      &opts->flags);
+		lua_pop(L, 1);
+
+		lua_getfield(L, idx, "stderr");
+		if (! lua_isnil(L, -1))
+			luaT_popen_parse_stdX(L, -1, STDERR_FILENO,
+					      &opts->flags);
+		lua_pop(L, 1);
+
+		lua_getfield(L, idx, "env");
+		if (! lua_isnil(L, -1)) {
+			opts->env = luaT_popen_parse_env(L, -1, region);
+			if (opts->env == NULL)
+				return -1;
+		}
+		lua_pop(L, 1);
+
+		lua_getfield(L, idx, "shell");
+		if (! lua_isnil(L, -1)) {
+			if (lua_type(L, -1) != LUA_TBOOLEAN)
+				luaT_popen_param_type_error(L, -1, "popen.new",
+							    "opts.shell",
+							    "boolean or nil");
+			if (lua_toboolean(L, -1) == 0)
+				opts->flags &= ~POPEN_FLAG_SHELL;
+			else
+				opts->flags |= POPEN_FLAG_SHELL;
+		}
+		lua_pop(L, 1);
+
+		lua_getfield(L, idx, "setsid");
+		if (! lua_isnil(L, -1)) {
+			if (lua_type(L, -1) != LUA_TBOOLEAN)
+				luaT_popen_param_type_error(L, -1, "popen.new",
+							    "opts.setsid",
+							    "boolean or nil");
+			if (lua_toboolean(L, -1) == 0)
+				opts->flags &= ~POPEN_FLAG_SETSID;
+			else
+				opts->flags |= POPEN_FLAG_SETSID;
+		}
+		lua_pop(L, 1);
+
+		lua_getfield(L, idx, "close_fds");
+		if (! lua_isnil(L, -1)) {
+			if (lua_type(L, -1) != LUA_TBOOLEAN)
+				luaT_popen_param_type_error(L, -1, "popen.new",
+							    "opts.close_fds",
+							    "boolean or nil");
+			if (lua_toboolean(L, -1) == 0)
+				opts->flags &= ~POPEN_FLAG_CLOSE_FDS;
+			else
+				opts->flags |= POPEN_FLAG_CLOSE_FDS;
+		}
+		lua_pop(L, 1);
+
+		lua_getfield(L, idx, "restore_signals");
+		if (! lua_isnil(L, -1)) {
+			if (lua_type(L, -1) != LUA_TBOOLEAN)
+				luaT_popen_param_type_error(
+					L, -1, "popen.new",
+					"opts.restore_signals",
+					"boolean or nil");
+			if (lua_toboolean(L, -1) == 0)
+				opts->flags &= ~POPEN_FLAG_RESTORE_SIGNALS;
+			else
+				opts->flags |= POPEN_FLAG_RESTORE_SIGNALS;
+		}
+		lua_pop(L, 1);
+
+		lua_getfield(L, idx, "group_signal");
+		if (! lua_isnil(L, -1)) {
+			if (lua_type(L, -1) != LUA_TBOOLEAN)
+				luaT_popen_param_type_error(L, -1, "popen.new",
+							    "opts.group_signal",
+							    "boolean or nil");
+			if (lua_toboolean(L, -1) == 0)
+				opts->flags &= ~POPEN_FLAG_GROUP_SIGNAL;
+			else
+				opts->flags |= POPEN_FLAG_GROUP_SIGNAL;
+		}
+		lua_pop(L, 1);
+
+		lua_getfield(L, idx, "keep_child");
+		if (! lua_isnil(L, -1)) {
+			if (lua_type(L, -1) != LUA_TBOOLEAN)
+				luaT_popen_param_type_error(L, -1, "popen.new",
+							    "opts.keep_child",
+							    "boolean or nil");
+			if (lua_toboolean(L, -1) == 0)
+				opts->flags &= ~POPEN_FLAG_KEEP_CHILD;
+			else
+				opts->flags |= POPEN_FLAG_KEEP_CHILD;
+		}
+		lua_pop(L, 1);
+	}
+
+	return 0;
+}
+
+/**
+ * Parse popen.new() "argv" parameter.
+ *
+ * Prerequisite: opts->flags & POPEN_FLAG_SHELL should be
+ * the same in a call of this function and when paired
+ * popen_new() is invoked.
+ *
+ * Raise an error in case of the incorrect parameter.
+ *
+ * Return 0 at success. Sets opts->args and opts->nr_args.
+ * Allocates opts->argv on @a region (@see
+ * luaT_popen_parse_env() for details how to free it).
+ *
+ * Return -1 in case of an allocation error and set a diag
+ * (OutOfMemory).
+ */
+static int
+luaT_popen_parse_argv(struct lua_State *L, int idx, struct popen_opts *opts,
+		      struct region *region)
+{
+	size_t region_svp = region_used(region);
+	size_t argv_len = lua_objlen(L, idx);
+
+	/* ["sh", "-c", ]..., NULL. */
+	opts->nr_argv = argv_len + 1;
+	if (opts->flags & POPEN_FLAG_SHELL)
+		opts->nr_argv += 2;
+
+	size_t size = sizeof(char *) * opts->nr_argv;
+	opts->argv = region_alloc(region, size);
+	if (opts->argv == NULL) {
+		diag_set(OutOfMemory, size, "region_alloc", "argv");
+		return -1;
+	}
+
+	const char **to = (const char **)opts->argv;
+	if (opts->flags & POPEN_FLAG_SHELL) {
+		opts->argv[0] = NULL;
+		opts->argv[1] = NULL;
+		to += 2;
+	}
+
+	for (size_t i = 0; i < argv_len; ++i) {
+		lua_rawgeti(L, idx, i + 1);
+		const char *arg = luaL_check_string_strict(L, -1, NULL);
+		if (arg == NULL) {
+			region_truncate(region, region_svp);
+			return luaT_popen_array_elem_type_error(
+				L, -1, "popen.new", "argv", i + 1, "string");
+		}
+		*to++ = arg;
+		lua_pop(L, 1);
+	}
+	*to++ = NULL;
+	assert((const char **)opts->argv + opts->nr_argv == to);
+
+	return 0;
+}
+
+/**
+ * Parse popen.shell() "mode" parameter.
+ *
+ * Convert "mode" parameter into options table for popen.new().
+ * Push the table to the Lua stack.
+ *
+ * Raise an error in case of the incorrect parameter.
+ */
+static void
+luaT_popen_parse_mode(struct lua_State *L, int idx)
+{
+	if (lua_type(L, idx) != LUA_TSTRING &&
+	    lua_type(L, idx) != LUA_TNONE &&
+	    lua_type(L, idx) != LUA_TNIL)
+		luaT_popen_param_type_error(L, idx, "popen.shell", "mode",
+					    "string or nil");
+
+	/*
+	 * Create options table for popen.new().
+	 *
+	 * Preallocate space for shell, setsid, group_signal and
+	 * std{in,out,err} options.
+	 */
+	lua_createtable(L, 0, 5);
+
+	lua_pushboolean(L, true);
+	lua_setfield(L, -2, "shell");
+
+	lua_pushboolean(L, true);
+	lua_setfield(L, -2, "setsid");
+
+	lua_pushboolean(L, true);
+	lua_setfield(L, -2, "group_signal");
+
+	/*
+	 * When mode is nil, left std* params default, which means
+	 * to inherit parent's file descriptors in a child
+	 * process.
+	 */
+	if (lua_isnoneornil(L, idx))
+		return;
+
+	size_t mode_len;
+	const char *mode = lua_tolstring(L, idx, &mode_len);
+	for (size_t i = 0; i < mode_len; ++i) {
+		switch (mode[i]) {
+		case 'r':
+			lua_pushstring(L, POPEN_LUA_STREAM_PIPE);
+			lua_setfield(L, -2, "stdout");
+			break;
+		case 'R':
+			lua_pushstring(L, POPEN_LUA_STREAM_PIPE);
+			lua_setfield(L, -2, "stderr");
+			break;
+		case 'w':
+			lua_pushstring(L, POPEN_LUA_STREAM_PIPE);
+			lua_setfield(L, -2, "stdin");
+			break;
+		default:
+			luaT_popen_param_value_error(
+				L, mode, "popen.shell", "mode",
+				"'r', 'w', 'R' or its combination");
+		}
+	}
+}
+
+/* }}} */
+
+/* {{{ Lua API functions and methods */
+
+/**
+ * Execute a child program in a new process.
+ *
+ * @param argv  an array of a program to run with
+ *              command line options, mandatory;
+ *              absolute path to the program is required
+ *
+ * @param opts  table of options
+ *
+ * @param opts.stdin   action on STDIN_FILENO
+ * @param opts.stdout  action on STDOUT_FILENO
+ * @param opts.stderr  action on STDERR_FILENO
+ *
+ * File descriptor actions:
+ *
+ *     popen.opts.INHERIT  (== 'inherit') [default]
+ *                         inherit the fd from the parent
+ *     popen.opts.DEVNULL  (== 'devnull')
+ *                         open /dev/null on the fd
+ *     popen.opts.CLOSE    (== 'close')
+ *                         close the fd
+ *     popen.opts.PIPE     (== 'pipe')
+ *                         feed data from/to the fd to parent
+ *                         using a pipe
+ *
+ * @param opts.env  an array of environment variables to
+ *                  be used inside a process;
+ *                  - when is not set then the current
+ *                    environment is inherited;
+ *                  - if set then the environment will be
+ *                    replaced
+ *                  - if set to an empty array then the
+ *                    environment will be dropped
+ *
+ * @param opts.shell            (boolean, default: false)
+ *        true                  run a child process via
+ *                              'sh -c "${opts.argv}"'
+ *        false                 call the executable directly
+ *
+ * @param opts.setsid           (boolean, default: false)
+ *        true                  start executable inside a new
+ *                              session
+ *        false                 don't do that
+ *
+ * @param opts.close_fds        (boolean, default: true)
+ *        true                  close all inherited fds from a
+ *                              parent
+ *        false                 don't do that
+ *
+ * @param opts.restore_signals  (boolean, default: true)
+ *        true                  reset all signal actions
+ *                              modified in parent's process
+ *        false                 inherit changed actions
+ *
+ * @param opts.group_signal     (boolean, default: false)
+ *        true                  send signal to a child process
+ *                              group (only when opts.setsid is
+ *                              enabled)
+ *        false                 send signal to a child process
+ *                              only
+ *
+ * @param opts.keep_child       (boolean, default: false)
+ *        true                  don't send SIGKILL to a child
+ *                              process at freeing (by :close()
+ *                              or Lua GC)
+ *        false                 send SIGKILL to a child process
+ *                              (or a process group if
+ *                              opts.group_signal is enabled) at
+ *                              :close() or collecting of the
+ *                              handle by Lua GC
+ *
+ * The returned handle is subject to garbage collection, but
+ * it may be freed explicitly using :close(). SIGKILL is sent
+ * to the child process at :close() / GC if it is alive and
+ * opts.keep_child is not set.
+ *
+ * It is recommended to use opts.setsid + opts.group_signal
+ * if a child process may spawn its own childs and they all
+ * should be killed together.
+ *
+ * Note: A signal will not be sent if the child process is
+ * already dead: otherwise we might kill another process that
+ * occupies the same PID later. This means that if the child
+ * process dies before its own childs, the function will not
+ * send a signal to the process group even when opts.setsid and
+ * opts.group_signal are set.
+ *
+ * Use os.environ() to pass copy of current environment with
+ * several replacements (see example 2 below).
+ *
+ * Raise an error on incorrect parameters:
+ *
+ * - IllegalParams: incorrect type or value of a parameter.
+ * - IllegalParams: group signal is set, while setsid is not.
+ *
+ * Return a popen handle on success.
+ *
+ * Return `nil, err` on a failure. Possible reasons:
+ *
+ * - SystemError: dup(), fcntl(), pipe(), vfork() or close()
+ *                fails in the parent process.
+ * - SystemError: (temporary restriction) the parent process
+ *                has closed stdin, stdout or stderr.
+ * - OutOfMemory: unable to allocate the handle or a temporary
+ *                buffer.
+ *
+ * Example 1:
+ *
+ *  | local popen = require('popen')
+ *  |
+ *  | local ph = popen.new({'/bin/date'}, {
+ *  |     stdout = popen.opts.PIPE,
+ *  | })
+ *  | local date = ph:read():rstrip()
+ *  | ph:close()
+ *  | print(date) -- Thu 16 Apr 2020 01:40:56 AM MSK
+ *
+ * Execute 'date' command, read the result and close the
+ * popen object.
+ *
+ * Example 2:
+ *
+ *  | local popen = require('popen')
+ *  |
+ *  | local env = os.environ()
+ *  | env['FOO'] = 'bar'
+ *  |
+ *  | local ph = popen.new({'echo "${FOO}"'}, {
+ *  |     stdout = popen.opts.PIPE,
+ *  |     shell = true,
+ *  |     env = env,
+ *  | })
+ *  | local res = ph:read():rstrip()
+ *  | ph:close()
+ *  | print(res) -- bar
+ *
+ * It is quite similar to the previous one, but sets the
+ * environment variable and uses shell builtin 'echo' to
+ * show it.
+ *
+ * Example 3:
+ *
+ *  | local popen = require('popen')
+ *  |
+ *  | local ph = popen.new({'echo hello >&2'}, { -- !!
+ *  |     stderr = popen.opts.PIPE,              -- !!
+ *  |     shell = true,
+ *  | })
+ *  | local res = ph:read({stderr = true}):rstrip()
+ *  | ph:close()
+ *  | print(res) -- hello
+ *
+ * This example demonstrates how to capture child's stderr.
+ *
+ * Example 4:
+ *
+ *  | local function call_jq(stdin, filter)
+ *  |     -- Start jq process, connect to stdin, stdout and stderr.
+ *  |     local jq_argv = {'/usr/bin/jq', '-M', '--unbuffered', filter}
+ *  |     local ph, err = popen.new(jq_argv, {
+ *  |         stdin = popen.opts.PIPE,
+ *  |         stdout = popen.opts.PIPE,
+ *  |         stderr = popen.opts.PIPE,
+ *  |     })
+ *  |     if ph == nil then return nil, err end
+ *  |
+ *  |     -- Write input data to child's stdin and send EOF.
+ *  |     local ok, err = ph:write(stdin)
+ *  |     if not ok then return nil, err end
+ *  |     ph:shutdown({stdin = true})
+ *  |
+ *  |     -- Read everything until EOF.
+ *  |     local chunks = {}
+ *  |     while true do
+ *  |         local chunk, err = ph:read()
+ *  |         if chunk == nil then
+ *  |             ph:close()
+ *  |             return nil, err
+ *  |         end
+ *  |         if chunk == '' then break end -- EOF
+ *  |         table.insert(chunks, chunk)
+ *  |     end
+ *  |
+ *  |     -- Read diagnostics from stderr if any.
+ *  |     local err = ph:read({stderr = true})
+ *  |     if err ~= '' then
+ *  |         ph:close()
+ *  |         return nil, err
+ *  |     end
+ *  |
+ *  |     -- Glue all chunks, strip trailing newline.
+ *  |     return table.concat(chunks):rstrip()
+ *  | end
+ *
+ * Demonstrates how to run a stream program (like `grep`, `sed`
+ * and so), write to its stdin and read from its stdout.
+ *
+ * The example assumes that input data are small enough to fit
+ * a pipe buffer (typically 64 KiB, but depends on a platform
+ * and its configuration). It will stuck in :write() for large
+ * data. How to handle this case: call :read() in a loop in
+ * another fiber (start it before a first :write()).
+ *
+ * If a process writes large text to stderr, it may fill out
+ * stderr pipe buffer and block in write(2, ...). So we need to
+ * read stderr too in another fiber to handle this case.
+ */
+static int
+lbox_popen_new(struct lua_State *L)
+{
+	struct region *region = &fiber()->gc;
+	size_t region_svp = region_used(region);
+
+	if (lua_type(L, 1) != LUA_TTABLE)
+		return luaT_popen_param_type_error(L, 1, "popen.new", "argv",
+						   "table");
+	else if (lua_type(L, 2) != LUA_TTABLE &&
+		 lua_type(L, 2) != LUA_TNONE &&
+		 lua_type(L, 2) != LUA_TNIL)
+		return luaT_popen_param_type_error(L, 2, "popen.new", "opts",
+						   "table or nil");
+
+	/* Parse opts and argv. */
+	struct popen_opts opts = {};
+	int rc = luaT_popen_parse_opts(L, 2, &opts, region);
+	if (rc != 0)
+		goto err;
+	rc = luaT_popen_parse_argv(L, 1, &opts, region);
+	if (rc != 0)
+		goto err;
+
+	struct popen_handle *handle = popen_new(&opts);
+
+	if (handle == NULL)
+		goto err;
+
+	region_truncate(region, region_svp);
+	luaT_push_popen_handle(L, handle);
+	return 1;
+
+err:
+	region_truncate(region, region_svp);
+	struct error *e = diag_last_error(diag_get());
+	if (e->type == &type_IllegalParams)
+		return luaT_error(L);
+	return luaT_push_nil_and_error(L);
+}
+
+/**
+ * Execute a shell command.
+ *
+ * @param command  a command to run, mandatory
+ * @param mode     communication mode, optional
+ *                 'w'    to use ph:write()
+ *                 'r'    to use ph:read()
+ *                 'R'    to use ph:read({stderr = true})
+ *                 nil    inherit parent's std* file descriptors
+ *
+ * Several mode characters can be set together: 'rw', 'rRw', etc.
+ *
+ * This function is just shortcut for popen.new({command}, opts)
+ * with opts.{shell,setsid,group_signal} set to `true` and
+ * and opts.{stdin,stdout,stderr} set based on `mode` parameter.
+ *
+ * All std* streams are inherited from parent by default if it is
+ * not changed using mode: 'r' for stdout, 'R' for stderr, 'w' for
+ * stdin.
+ *
+ * Raise an error on incorrect parameters:
+ *
+ * - IllegalParams: incorrect type or value of a parameter.
+ *
+ * Return a popen handle on success.
+ *
+ * Return `nil, err` on a failure.
+ * @see lbox_popen_new() for possible reasons.
+ *
+ * Example:
+ *
+ *  | local popen = require('popen')
+ *  |
+ *  | local ph = popen.shell('date', 'r')
+ *  | local date = ph:read():rstrip()
+ *  | ph:close()
+ *  | print(date)
+ *
+ * Execute 'sh -c date' command, read the output and close the
+ * popen object.
+ */
+static int
+lbox_popen_shell(struct lua_State *L)
+{
+	if (lua_type(L, 1) != LUA_TSTRING)
+		return luaT_popen_param_type_error(L, 1, "popen.shell",
+						   "command", "string");
+
+	/*
+	 * Ensure that at least two stack slots are occupied.
+	 *
+	 * Otherwise we can pass `top` as `idx` to lua_replace().
+	 * lua_replace() on `top` index copies a value to itself
+	 * first and then pops it from the stack.
+	 */
+	if (lua_gettop(L) == 1)
+		lua_pushnil(L);
+
+	/* Create argv table for popen.new(). */
+	lua_createtable(L, 1, 0);
+	/* argv[1] = command */
+	lua_pushvalue(L, 1);
+	lua_rawseti(L, -2, 1);
+	/* {...}[1] == argv */
+	lua_replace(L, 1);
+
+	/* opts = parse_mode(mode) */
+	luaT_popen_parse_mode(L, 2);
+	/* {...}[2] == opts */
+	lua_replace(L, 2);
+
+	return lbox_popen_new(L);
+}
+
+/**
+ * Send signal to a child process.
+ *
+ * @param handle  a handle carries child process to be signaled
+ * @param signo   signal number to send
+ *
+ * When opts.setsid and opts.group_signal are set on the handle
+ * the signal is sent to the process group rather than to the
+ * process. @see lbox_popen_new() for details about group
+ * signaling.
+ *
+ * Note: The module offers popen.signal.SIG* constants, because
+ * some signals have different numbers on different platforms.
+ *
+ * Raise an error on incorrect parameters:
+ *
+ * - IllegalParams:    an incorrect handle parameter.
+ * - IllegalParams:    called on a closed handle.
+ *
+ * Return `true` if signal is sent.
+ *
+ * Return `nil, err` on a failure. Possible reasons:
+ *
+ * - SystemError: a process does not exists anymore
+ *
+ *                Aside of a non-exist process it is also
+ *                returned for a zombie process or when all
+ *                processes in a group are zombies (but
+ *                see note re Mac OS below).
+ *
+ * - SystemError: invalid signal number
+ *
+ * - SystemError: no permission to send a signal to
+ *                a process or a process group
+ *
+ *                It is returned on Mac OS when a signal is
+ *                sent to a process group, where a group leader
+ *                is zombie (or when all processes in it
+ *                are zombies, don't sure).
+ *
+ *                Whether it may appear due to other
+ *                reasons is unclear.
+ */
+static int
+lbox_popen_signal(struct lua_State *L)
+{
+	struct popen_handle *handle;
+	bool is_closed;
+	if ((handle = luaT_check_popen_handle(L, 1, &is_closed)) == NULL ||
+	    !lua_isnumber(L, 2)) {
+		diag_set(IllegalParams, "Bad params, use: ph:signal(signo)");
+		return luaT_error(L);
+	}
+	if (is_closed)
+		return luaT_popen_handle_closed_error(L);
+
+	int signo = lua_tonumber(L, 2);
+
+	if (popen_send_signal(handle, signo) != 0)
+		return luaT_push_nil_and_error(L);
+
+	lua_pushboolean(L, true);
+	return 1;
+}
+
+/**
+ * Send SIGTERM signal to a child process.
+ *
+ * @param handle  a handle carries child process to terminate
+ *
+ * The function only sends SIGTERM signal and does NOT
+ * free any resources (popen handle memory and file
+ * descriptors).
+ *
+ * @see lbox_popen_signal() for errors and return values.
+ */
+static int
+lbox_popen_terminate(struct lua_State *L)
+{
+	struct popen_handle *handle;
+	bool is_closed;
+	if ((handle = luaT_check_popen_handle(L, 1, &is_closed)) == NULL) {
+		diag_set(IllegalParams, "Bad params, use: ph:terminate()");
+		return luaT_error(L);
+	}
+	if (is_closed)
+		return luaT_popen_handle_closed_error(L);
+
+	int signo = SIGTERM;
+
+	if (popen_send_signal(handle, signo) != 0)
+		return luaT_push_nil_and_error(L);
+
+	lua_pushboolean(L, true);
+	return 1;
+}
+
+/**
+ * Send SIGKILL signal to a child process.
+ *
+ * @param handle  a handle carries child process to kill
+ *
+ * The function only sends SIGKILL signal and does NOT
+ * free any resources (popen handle memory and file
+ * descriptors).
+ *
+ * @see lbox_popen_signal() for errors and return values.
+ */
+static int
+lbox_popen_kill(struct lua_State *L)
+{
+	struct popen_handle *handle;
+	bool is_closed;
+	if ((handle = luaT_check_popen_handle(L, 1, &is_closed)) == NULL) {
+		diag_set(IllegalParams, "Bad params, use: ph:kill()");
+		return luaT_error(L);
+	}
+	if (is_closed)
+		return luaT_popen_handle_closed_error(L);
+
+	int signo = SIGKILL;
+
+	if (popen_send_signal(handle, signo) != 0)
+		return luaT_push_nil_and_error(L);
+
+	lua_pushboolean(L, true);
+	return 1;
+}
+
+/**
+ * Wait until a child process get exited or signaled.
+ *
+ * @param handle  a handle of process to wait
+ *
+ * Raise an error on incorrect parameters or when the fiber is
+ * cancelled:
+ *
+ * - IllegalParams:    an incorrect handle parameter.
+ * - IllegalParams:    called on a closed handle.
+ * - FiberIsCancelled: cancelled by an outside code.
+ *
+ * Return a process status table (the same as ph.status and
+ * ph.info().status). @see lbox_popen_info() for the format
+ * of the table.
+ */
+static int
+lbox_popen_wait(struct lua_State *L)
+{
+	/*
+	 * FIXME: Use trigger or fiber conds to sleep and wake up.
+	 * FIXME: Add timeout option: ph:wait({timeout = <...>})
+	 */
+	struct popen_handle *handle;
+	bool is_closed;
+	if ((handle = luaT_check_popen_handle(L, 1, &is_closed)) == NULL) {
+		diag_set(IllegalParams, "Bad params, use: ph:wait()");
+		return luaT_error(L);
+	}
+	if (is_closed)
+		return luaT_popen_handle_closed_error(L);
+
+	int state;
+	int exit_code;
+
+	while (true) {
+		popen_state(handle, &state, &exit_code);
+		assert(state < POPEN_STATE_MAX);
+		if (state != POPEN_STATE_ALIVE)
+			break;
+		fiber_sleep(POPEN_LUA_WAIT_DELAY);
+		luaL_testcancel(L);
+	}
+
+	return luaT_push_popen_process_status(L, state, exit_code);
+}
+
+/**
+ * Read data from a child peer.
+ *
+ * @param handle        handle of a child process
+ * @param opts          an options table
+ * @param opts.stdout   whether to read from stdout, boolean
+ *                      (default: true)
+ * @param opts.stderr   whether to read from stderr, boolean
+ *                      (default: false)
+ * @param opts.timeout  time quota in seconds
+ *                      (default: 100 years)
+ *
+ * Read data from stdout or stderr streams with @a timeout.
+ * By default it reads from stdout. Set @a opts.stderr to
+ * `true` to read from stderr.
+ *
+ * Raise an error on incorrect parameters or when the fiber is
+ * cancelled:
+ *
+ * - IllegalParams:    incorrect type or value of a parameter.
+ * - IllegalParams:    called on a closed handle.
+ * - IllegalParams:    opts.stdout and opts.stderr are set both
+ * - IllegalParams:    a requested IO operation is not supported
+ *                     by the handle (stdout / stderr is not
+ *                     piped).
+ * - IllegalParams:    attempt to operate on a closed file
+ *                     descriptor.
+ * - FiberIsCancelled: cancelled by an outside code.
+ *
+ * Return a string on success, an empty string at EOF.
+ *
+ * Return `nil, err` on a failure. Possible reasons:
+ *
+ * - SocketError: an IO error occurs at read().
+ * - TimedOut:    @a timeout quota is exceeded.
+ * - OutOfMemory: no memory space for a buffer to read into.
+ * - LuajitError: ("not enough memory"): no memory space for
+ *                the Lua string.
+ */
+static int
+lbox_popen_read(struct lua_State *L)
+{
+	struct popen_handle *handle;
+	bool is_closed;
+	unsigned int flags = POPEN_FLAG_NONE;
+	ev_tstamp timeout = TIMEOUT_INFINITY;
+	struct region *region = &fiber()->gc;
+	size_t region_svp = region_used(region);
+
+	/* Extract handle. */
+	if ((handle = luaT_check_popen_handle(L, 1, &is_closed)) == NULL)
+		goto usage;
+	if (is_closed)
+		return luaT_popen_handle_closed_error(L);
+
+	/* Extract options. */
+	if (!lua_isnoneornil(L, 2)) {
+		if (lua_type(L, 2) != LUA_TTABLE)
+			goto usage;
+
+		lua_getfield(L, 2, "stdout");
+		if (!lua_isnil(L, -1)) {
+			if (lua_type(L, -1) != LUA_TBOOLEAN)
+				goto usage;
+			if (lua_toboolean(L, -1) == 0)
+				flags &= ~POPEN_FLAG_FD_STDOUT;
+			else
+				flags |= POPEN_FLAG_FD_STDOUT;
+		}
+		lua_pop(L, 1);
+
+		lua_getfield(L, 2, "stderr");
+		if (!lua_isnil(L, -1)) {
+			if (lua_type(L, -1) != LUA_TBOOLEAN)
+				goto usage;
+			if (lua_toboolean(L, -1) == 0)
+				flags &= ~POPEN_FLAG_FD_STDERR;
+			else
+				flags |= POPEN_FLAG_FD_STDERR;
+		}
+		lua_pop(L, 1);
+
+		lua_getfield(L, 2, "timeout");
+		if (!lua_isnil(L, -1) &&
+		    (timeout = luaT_check_timeout(L, -1)) < 0.0)
+			goto usage;
+		lua_pop(L, 1);
+	}
+
+	/* Read from stdout by default. */
+	if (!(flags & (POPEN_FLAG_FD_STDOUT | POPEN_FLAG_FD_STDERR)))
+		flags |= POPEN_FLAG_FD_STDOUT;
+
+	size_t size = POPEN_LUA_READ_BUF_SIZE;
+	char *buf = region_alloc(region, size);
+	if (buf == NULL) {
+		diag_set(OutOfMemory, size, "region_alloc", "read buffer");
+		return luaT_push_nil_and_error(L);
+	}
+
+	static_assert(POPEN_LUA_READ_BUF_SIZE <= SSIZE_MAX,
+		      "popen: read buffer is too big");
+	ssize_t rc = popen_read_timeout(handle, buf, size, flags, timeout);
+	if (rc < 0 || luaT_push_string_noxc(L, buf, rc) != 0)
+		goto err;
+	region_truncate(region, region_svp);
+	return 1;
+
+usage:
+	diag_set(IllegalParams, "Bad params, use: ph:read([{"
+		 "stdout = <boolean>, "
+		 "stderr = <boolean>, "
+		 "timeout = <number>}])");
+	return luaT_error(L);
+err:
+	region_truncate(region, region_svp);
+	struct error *e = diag_last_error(diag_get());
+	if (e->type == &type_IllegalParams ||
+	    e->type == &type_FiberIsCancelled)
+		return luaT_error(L);
+	return luaT_push_nil_and_error(L);
+}
+
+/**
+ * Write data to a child peer.
+ *
+ * @param handle        a handle of a child process
+ * @param str           a string to write
+ * @param opts          table of options
+ * @param opts.timeout  time quota in seconds
+ *                      (default: 100 years)
+ *
+ * Write string @a str to stdin stream of a child process.
+ *
+ * The function may yield forever if a child process does
+ * not read data from stdin and a pipe buffer becomes full.
+ * Size of this buffer depends on a platform. Use
+ * @a opts.timeout when unsure.
+ *
+ * When @a opts.timeout is not set, the function blocks
+ * (yields the fiber) until all data is written or an error
+ * happened.
+ *
+ * Raise an error on incorrect parameters or when the fiber is
+ * cancelled:
+ *
+ * - IllegalParams:    incorrect type or value of a parameter.
+ * - IllegalParams:    called on a closed handle.
+ * - IllegalParams:    string length is greater then SSIZE_MAX.
+ * - IllegalParams:    a requested IO operation is not supported
+ *                     by the handle (stdin is not piped).
+ * - IllegalParams:    attempt to operate on a closed file
+ *                     descriptor.
+ * - FiberIsCancelled: cancelled by an outside code.
+ *
+ * Return `true` on success.
+ *
+ * Return `nil, err` on a failure. Possible reasons:
+ *
+ * - SocketError: an IO error occurs at write().
+ * - TimedOut:    @a timeout quota is exceeded.
+ */
+static int
+lbox_popen_write(struct lua_State *L)
+{
+	struct popen_handle *handle;
+	bool is_closed;
+	const char *str;
+	size_t len;
+	ev_tstamp timeout = TIMEOUT_INFINITY;
+
+	/* Extract handle and string to write. */
+	if ((handle = luaT_check_popen_handle(L, 1, &is_closed)) == NULL ||
+	    (str = luaL_check_string_strict(L, 2, &len)) == NULL)
+		goto usage;
+	if (is_closed)
+		return luaT_popen_handle_closed_error(L);
+
+	/* Extract options. */
+	if (!lua_isnoneornil(L, 3)) {
+		if (lua_type(L, 3) != LUA_TTABLE)
+			goto usage;
+
+		lua_getfield(L, 3, "timeout");
+		if (!lua_isnil(L, -1) &&
+		    (timeout = luaT_check_timeout(L, -1)) < 0.0)
+			goto usage;
+		lua_pop(L, 1);
+	}
+
+	unsigned int flags = POPEN_FLAG_FD_STDIN;
+	ssize_t rc = popen_write_timeout(handle, str, len, flags, timeout);
+	assert(rc < 0 || rc == (ssize_t)len);
+	if (rc < 0) {
+		struct error *e = diag_last_error(diag_get());
+		if (e->type == &type_IllegalParams ||
+		    e->type == &type_FiberIsCancelled)
+			return luaT_error(L);
+		return luaT_push_nil_and_error(L);
+	}
+	lua_pushboolean(L, true);
+	return 1;
+
+usage:
+	diag_set(IllegalParams, "Bad params, use: ph:write(str[, {"
+		 "timeout = <number>}])");
+	return luaT_error(L);
+}
+
+/**
+ * Close parent's ends of std* fds.
+ *
+ * @param handle        handle of a child process
+ * @param opts          an options table
+ * @param opts.stdin    close parent's end of stdin, boolean
+ * @param opts.stdout   close parent's end of stdout, boolean
+ * @param opts.stderr   close parent's end of stderr, boolean
+ *
+ * The main reason to use this function is to send EOF to
+ * child's stdin. However parent's end of stdout / stderr
+ * may be closed too.
+ *
+ * The function does not fail on already closed fds (idempotence).
+ * However it fails on attempt to close the end of a pipe that was
+ * never exist. In other words, only those std* options that
+ * were set to popen.opts.PIPE at a handle creation may be used
+ * here (for popen.shell: 'r' corresponds to stdout, 'R' to stderr
+ * and 'w' to stdin).
+ *
+ * The function does not close any fds on a failure: either all
+ * requested fds are closed or neither of them.
+ *
+ * Example:
+ *
+ *  | local popen = require('popen')
+ *  |
+ *  | local ph = popen.shell('sed s/foo/bar/', 'rw')
+ *  | ph:write('lorem foo ipsum')
+ *  | ph:shutdown({stdin = true})
+ *  | local res = ph:read()
+ *  | ph:close()
+ *  | print(res) -- lorem bar ipsum
+ *
+ * Raise an error on incorrect parameters:
+ *
+ * - IllegalParams:  an incorrect handle parameter.
+ * - IllegalParams:  called on a closed handle.
+ * - IllegalParams:  neither stdin, stdout nor stderr is choosen.
+ * - IllegalParams:  a requested IO operation is not supported
+ *                   by the handle (one of std* is not piped).
+ *
+ * Return `true` on success.
+ */
+static int
+lbox_popen_shutdown(struct lua_State *L)
+{
+	struct popen_handle *handle;
+	bool is_closed;
+
+	/* Extract handle. */
+	if ((handle = luaT_check_popen_handle(L, 1, &is_closed)) == NULL)
+		goto usage;
+	if (is_closed)
+		return luaT_popen_handle_closed_error(L);
+
+	unsigned int flags = POPEN_FLAG_NONE;
+
+	/* Extract options. */
+	if (!lua_isnoneornil(L, 2)) {
+		if (lua_type(L, 2) != LUA_TTABLE)
+			goto usage;
+
+		/*
+		 * FIXME: Those blocks duplicates ones from
+		 * lbox_popen_read().
+		 *
+		 * Let's introduce a helper like
+		 * luaT_popen_parse_stdX() but about boolean
+		 * flags rather than stream actions.
+		 */
+
+		lua_getfield(L, 2, "stdin");
+		if (!lua_isnil(L, -1)) {
+			if (lua_type(L, -1) != LUA_TBOOLEAN)
+				goto usage;
+			if (lua_toboolean(L, -1) == 0)
+				flags &= ~POPEN_FLAG_FD_STDIN;
+			else
+				flags |= POPEN_FLAG_FD_STDIN;
+		}
+		lua_pop(L, 1);
+
+		lua_getfield(L, 2, "stdout");
+		if (!lua_isnil(L, -1)) {
+			if (lua_type(L, -1) != LUA_TBOOLEAN)
+				goto usage;
+			if (lua_toboolean(L, -1) == 0)
+				flags &= ~POPEN_FLAG_FD_STDOUT;
+			else
+				flags |= POPEN_FLAG_FD_STDOUT;
+		}
+		lua_pop(L, 1);
+
+		lua_getfield(L, 2, "stderr");
+		if (!lua_isnil(L, -1)) {
+			if (lua_type(L, -1) != LUA_TBOOLEAN)
+				goto usage;
+			if (lua_toboolean(L, -1) == 0)
+				flags &= ~POPEN_FLAG_FD_STDERR;
+			else
+				flags |= POPEN_FLAG_FD_STDERR;
+		}
+		lua_pop(L, 1);
+	}
+
+	if (popen_shutdown(handle, flags) != 0) {
+		/*
+		 * FIXME: This block is often duplicated,
+		 * let's extract it to a helper.
+		 */
+		struct error *e = diag_last_error(diag_get());
+		if (e->type == &type_IllegalParams)
+			return luaT_error(L);
+		return luaT_push_nil_and_error(L);
+	}
+
+	lua_pushboolean(L, true);
+	return 1;
+
+usage:
+	diag_set(IllegalParams, "Bad params, use: ph:shutdown({"
+		 "stdin = <boolean>, "
+		 "stdout = <boolean>, "
+		 "stderr = <boolean>})");
+	return luaT_error(L);
+
+}
+
+/**
+ * Return information about popen handle.
+ *
+ * @param handle  a handle of a child process
+ *
+ * Raise an error on incorrect parameters:
+ *
+ * - IllegalParams: an incorrect handle parameter.
+ * - IllegalParams: called on a closed handle.
+ *
+ * Return information about the handle in the following
+ * format:
+ *
+ *     {
+ *         pid = <number> or <nil>,
+ *         command = <string>,
+ *         opts = <table>,
+ *         status = <table>,
+ *         stdin = one-of(
+ *             popen.stream.OPENED (== 'opened'),
+ *             popen.stream.CLOSED (== 'closed'),
+ *             nil,
+ *         ),
+ *         stdout = one-of(
+ *             popen.stream.OPENED (== 'opened'),
+ *             popen.stream.CLOSED (== 'closed'),
+ *             nil,
+ *         ),
+ *         stderr = one-of(
+ *             popen.stream.OPENED (== 'opened'),
+ *             popen.stream.CLOSED (== 'closed'),
+ *             nil,
+ *         ),
+ *     }
+ *
+ * `pid` is a process id of the process when it is alive,
+ * otherwise `pid` is nil.
+ *
+ * `command` is a concatenation of space separated arguments
+ * that were passed to execve(). Multiword arguments are quoted.
+ * Quotes inside arguments are not escaped.
+ *
+ * `opts` is a table of handle options in the format of
+ * popen.new() `opts` parameter. `opts.env` is not shown here,
+ * because the environment variables map is not stored in a
+ * handle.
+ *
+ * `status` is a table that represents a process status in the
+ * following format:
+ *
+ *     {
+ *         state = one-of(
+ *             popen.state.ALIVE    (== 'alive'),
+ *             popen.state.EXITED   (== 'exited'),
+ *             popen.state.SIGNALED (== 'signaled'),
+ *         )
+ *
+ *         -- Present when `state` is 'exited'.
+ *         exit_code = <number>,
+ *
+ *         -- Present when `state` is 'signaled'.
+ *         signo = <number>,
+ *         signame = <string>,
+ *     }
+ *
+ * `stdin`, `stdout`, `stderr` reflect status of parent's end
+ * of a piped stream. When a stream is not piped the field is
+ * not present (`nil`). When it is piped, the status may be
+ * one of the following:
+ *
+ * - popen.stream.OPENED  (== 'opened')
+ * - popen.stream.CLOSED  (== 'closed')
+ *
+ * The status may be changed from 'opened' to 'closed'
+ * by :shutdown({std... = true}) call.
+ *
+ * @see luaT_push_popen_opts()
+ * @see luaT_push_popen_process_status()
+ *
+ * Example 1 (tarantool console):
+ *
+ *  | tarantool> require('popen').new({'/usr/bin/touch', '/tmp/foo'})
+ *  | ---
+ *  | - command: /usr/bin/touch /tmp/foo
+ *  |   status:
+ *  |     state: alive
+ *  |   opts:
+ *  |     stdout: inherit
+ *  |     stdin: inherit
+ *  |     group_signal: false
+ *  |     keep_child: false
+ *  |     close_fds: true
+ *  |     restore_signals: true
+ *  |     shell: false
+ *  |     setsid: false
+ *  |     stderr: inherit
+ *  |   pid: 9499
+ *  | ...
+ *
+ * Example 2 (tarantool console):
+ *
+ *  | tarantool> require('popen').shell('grep foo', 'wrR')
+ *  | ---
+ *  | - stdout: opened
+ *  |   command: sh -c 'grep foo'
+ *  |   stderr: opened
+ *  |   status:
+ *  |     state: alive
+ *  |   stdin: opened
+ *  |   opts:
+ *  |     stdout: pipe
+ *  |     stdin: pipe
+ *  |     group_signal: true
+ *  |     keep_child: false
+ *  |     close_fds: true
+ *  |     restore_signals: true
+ *  |     shell: true
+ *  |     setsid: true
+ *  |     stderr: pipe
+ *  |   pid: 10497
+ *  | ...
+ */
+static int
+lbox_popen_info(struct lua_State *L)
+{
+	struct popen_handle *handle;
+	bool is_closed;
+
+	if ((handle = luaT_check_popen_handle(L, 1, &is_closed)) == NULL) {
+		diag_set(IllegalParams, "Bad params, use: ph:info()");
+		return luaT_error(L);
+	}
+	if (is_closed)
+		return luaT_popen_handle_closed_error(L);
+
+	struct popen_stat st = {};
+
+	popen_stat(handle, &st);
+
+	lua_createtable(L, 0, 7);
+
+	if (st.pid >= 0) {
+		lua_pushinteger(L, st.pid);
+		lua_setfield(L, -2, "pid");
+	}
+
+	lua_pushstring(L, popen_command(handle));
+	lua_setfield(L, -2, "command");
+
+	luaT_push_popen_opts(L, st.flags);
+	lua_setfield(L, -2, "opts");
+
+	int state;
+	int exit_code;
+	popen_state(handle, &state, &exit_code);
+	assert(state < POPEN_STATE_MAX);
+	luaT_push_popen_process_status(L, state, exit_code);
+	lua_setfield(L, -2, "status");
+
+	luaT_push_popen_stdX_status(L, handle, STDIN_FILENO);
+	lua_setfield(L, -2, "stdin");
+
+	luaT_push_popen_stdX_status(L, handle, STDOUT_FILENO);
+	lua_setfield(L, -2, "stdout");
+
+	luaT_push_popen_stdX_status(L, handle, STDERR_FILENO);
+	lua_setfield(L, -2, "stderr");
+
+	return 1;
+}
+
+/**
+ * Close a popen handle.
+ *
+ * @param handle  a handle to close
+ *
+ * Basically it kills a process using SIGKILL and releases all
+ * resources assosiated with the popen handle.
+ *
+ * Details about signaling:
+ *
+ * - The signal is sent only when opts.keep_child is not set.
+ * - The signal is sent only when a process is alive according
+ *   to the information available on current even loop iteration.
+ *   (There is a gap here: a zombie may be signaled; it is
+ *   harmless.)
+ * - The signal is sent to a process or a grocess group depending
+ *   of opts.group_signal. (@see lbox_popen_new() for details of
+ *   group signaling).
+ *
+ * Resources are released disregarding of whether a signal
+ * sending succeeds: fds are closed, memory is released,
+ * the handle is marked as closed.
+ *
+ * No operation is possible on a closed handle except
+ * :close(), which always successful on closed handle
+ * (idempotence).
+ *
+ * Raise an error on incorrect parameters:
+ *
+ * - IllegalParams: an incorrect handle parameter.
+ *
+ * The function may return `true` or `nil, err`, but it always
+ * frees the handle resources. So any return value usually
+ * means success for a caller. The return values are purely
+ * informational: it is for logging or same kind of reporting.
+ *
+ * Possible diagnostics (don't consider them as errors):
+ *
+ * - SystemError: no permission to send a signal to
+ *                a process or a process group
+ *
+ *                This diagnostics may appear due to
+ *                Mac OS behaviour on zombies when
+ *                opts.group_signal is set,
+ *                @see lbox_popen_signal().
+ *
+ *                Whether it may appear due to other
+ *                reasons is unclear.
+ *
+ * Always return `true` when a process is known as dead (say,
+ * after ph:wait()): no signal will be send, so no 'failure'
+ * may appear.
+ */
+static int
+lbox_popen_close(struct lua_State *L)
+{
+	struct popen_handle *handle;
+	bool is_closed;
+	if ((handle = luaT_check_popen_handle(L, 1, &is_closed)) == NULL) {
+		diag_set(IllegalParams, "Bad params, use: ph:close()");
+		return luaT_error(L);
+	}
+
+	/* Do nothing on a closed handle. */
+	if (is_closed) {
+		lua_pushboolean(L, true);
+		return 1;
+	}
+
+	if (popen_delete(handle) != 0)
+		return luaT_push_nil_and_error(L);
+
+	luaT_mark_popen_handle_closed(L, 1);
+
+	lua_pushboolean(L, true);
+	return 1;
+}
+
+/**
+ * Get a field from a handle.
+ *
+ * @param handle  a handle of a child process
+ * @param key     a field name, string
+ *
+ * The function performs the following steps.
+ *
+ * Raise an error on incorrect parameters:
+ *
+ * - IllegalParams: incorrect type or value of a parameter.
+ *
+ * If there is a handle method with @a key name, return it.
+ *
+ * Raise an error on closed popen handle:
+ *
+ * - IllegalParams: called on a closed handle.
+ *
+ * If a @key is one of the following, return a value for it:
+ *
+ * - pid
+ * - command
+ * - opts
+ * - status
+ * - stdin
+ * - stdout
+ * - stderr
+ *
+ * @see lbox_popen_info() for description of those fields.
+ *
+ * Otherwise return `nil`.
+ */
+static int
+lbox_popen_index(struct lua_State *L)
+{
+	struct popen_handle *handle;
+	bool is_closed;
+	const char *key;
+
+	if ((handle = luaT_check_popen_handle(L, 1, &is_closed)) == NULL ||
+	    (key = luaL_check_string_strict(L, 2, NULL)) == NULL) {
+		diag_set(IllegalParams,
+			 "Bad params, use __index(ph, <string>)");
+		return luaT_error(L);
+	}
+
+	/* Get a value from the metatable. */
+	lua_getmetatable(L, 1);
+	lua_pushvalue(L, 2);
+	lua_rawget(L, -2);
+	if (! lua_isnil(L, -1))
+		return 1;
+
+	if (is_closed) {
+		diag_set(IllegalParams,
+			 "Attempt to index a closed popen handle");
+		return luaT_error(L);
+	}
+
+	if (strcmp(key, "pid") == 0) {
+		if (handle->pid >= 0)
+			lua_pushinteger(L, handle->pid);
+		else
+			lua_pushnil(L);
+		return 1;
+	}
+
+	if (strcmp(key, "command") == 0) {
+		lua_pushstring(L, popen_command(handle));
+		return 1;
+	}
+
+	if (strcmp(key, "opts") == 0) {
+		luaT_push_popen_opts(L, handle->flags);
+		return 1;
+	}
+
+	int state;
+	int exit_code;
+	popen_state(handle, &state, &exit_code);
+	assert(state < POPEN_STATE_MAX);
+	if (strcmp(key, "status") == 0)
+		return luaT_push_popen_process_status(L, state, exit_code);
+
+	if (strcmp(key, "stdin") == 0)
+		return luaT_push_popen_stdX_status(L, handle, STDIN_FILENO);
+
+	if (strcmp(key, "stdout") == 0)
+		return luaT_push_popen_stdX_status(L, handle, STDOUT_FILENO);
+
+	if (strcmp(key, "stderr") == 0)
+		return luaT_push_popen_stdX_status(L, handle, STDERR_FILENO);
+
+	lua_pushnil(L);
+	return 1;
+}
+
+/**
+ * Popen handle representation for REPL (console).
+ *
+ * @param handle  a handle of a child process
+ *
+ * The function just calls lbox_popen_info() for a
+ * handle when it is not closed.
+ *
+ * Return '<closed popen handle>' string for a closed
+ * handle.
+ *
+ * @see lbox_popen_info()
+ */
+static int
+lbox_popen_serialize(struct lua_State *L)
+{
+	struct popen_handle *handle;
+	bool is_closed;
+
+	if ((handle = luaT_check_popen_handle(L, 1, &is_closed)) == NULL) {
+		diag_set(IllegalParams, "Bad params, use: __serialize(ph)");
+		return luaT_error(L);
+	}
+
+	if (is_closed) {
+		lua_pushliteral(L, "<closed popen handle>");
+		return 1;
+	}
+
+	return lbox_popen_info(L);
+}
+
+/**
+ * Free popen handle resources.
+ *
+ * @param handle  a handle to free
+ *
+ * The same as lbox_popen_close(), but silently exits on any
+ * failure.
+ *
+ * The method may be called manually from Lua, so it is able to
+ * proceed with an incorrect and a closed handle. It also marks
+ * a handle as closed to don't free resources twice if the
+ * handle is collected by Lua GC after a manual call of the
+ * method.
+ *
+ * Don't return a value.
+ */
+static int
+lbox_popen_gc(struct lua_State *L)
+{
+	bool is_closed;
+	struct popen_handle *handle = luaT_check_popen_handle(L, 1, &is_closed);
+	if (handle == NULL || is_closed)
+		return 0;
+	popen_delete(handle);
+	luaT_mark_popen_handle_closed(L, 1);
+	return 0;
+}
+
+/* }}} */
+
+/* {{{ Module initialization */
+
+/**
+ * Create popen functions and methods.
+ *
+ * Module functions
+ * ----------------
+ *
+ * - popen.new()
+ * - popen.shell()
+ *
+ * Module constants
+ * ----------------
+ *
+ * - popen.opts
+ *   - INHERIT (== 'inherit')
+ *   - DEVNULL (== 'devnull')
+ *   - CLOSE   (== 'close')
+ *   - PIPE    (== 'pipe')
+ *
+ * - popen.signal
+ *   - SIGTERM (== 9)
+ *   - SIGKILL (== 15)
+ *   - ...
+ *
+ * - popen.state
+ *   - ALIVE    (== 'alive')
+ *   - EXITED   (== 'exited')
+ *   - SIGNALED (== 'signaled')
+ *
+ * - popen.stream
+ *   - OPENED  (== 'opened')
+ *   - CLOSED  (== 'closed')
+ */
+void
+tarantool_lua_popen_init(struct lua_State *L)
+{
+	/* Popen module methods. */
+	static const struct luaL_Reg popen_methods[] = {
+		{"new",		lbox_popen_new,		},
+		{"shell",	lbox_popen_shell,	},
+		{NULL, NULL},
+	};
+	luaL_register_module(L, "popen", popen_methods);
+
+	/*
+	 * Popen handle methods and metamethods.
+	 *
+	 * Usual and closed popen handle userdata types have
+	 * the same set of methods and metamethods.
+	 */
+	static const struct luaL_Reg popen_handle_methods[] = {
+		{"signal",		lbox_popen_signal,	},
+		{"terminate",		lbox_popen_terminate,	},
+		{"kill",		lbox_popen_kill,	},
+		{"wait",		lbox_popen_wait,	},
+		{"read",		lbox_popen_read,	},
+		{"write",		lbox_popen_write,	},
+		{"shutdown",		lbox_popen_shutdown,	},
+		{"info",		lbox_popen_info,	},
+		{"close",		lbox_popen_close,	},
+		{"__index",		lbox_popen_index	},
+		{"__serialize",		lbox_popen_serialize	},
+		{"__gc",		lbox_popen_gc		},
+		{NULL, NULL},
+	};
+	luaL_register_type(L, popen_handle_uname, popen_handle_methods);
+	luaL_register_type(L, popen_handle_closed_uname, popen_handle_methods);
+
+	/* Signals. */
+	lua_newtable(L);
+	for (int i = 0; popen_lua_signals[i].signame != NULL; ++i) {
+		lua_pushinteger(L, popen_lua_signals[i].signo);
+		lua_setfield(L, -2, popen_lua_signals[i].signame);
+	}
+	lua_setfield(L, -2, "signal");
+
+	/* Stream actions. */
+	lua_newtable(L);
+	for (int i = 0; popen_lua_actions[i].name != NULL; ++i) {
+		lua_pushstring(L, popen_lua_actions[i].value);
+		lua_setfield(L, -2, popen_lua_actions[i].name);
+	}
+	lua_setfield(L, -2, "opts");
+
+	/* Stream statuses. */
+	lua_newtable(L);
+	for (int i = 0; popen_lua_stream_statuses[i].name != NULL; ++i) {
+		lua_pushstring(L, popen_lua_stream_statuses[i].value);
+		lua_setfield(L, -2, popen_lua_stream_statuses[i].name);
+	}
+	lua_setfield(L, -2, "stream");
+
+	/* Process states. */
+	lua_newtable(L);
+	for (int i = 0; popen_lua_states[i].name != NULL; ++i) {
+		lua_pushstring(L, popen_lua_states[i].value);
+		lua_setfield(L, -2, popen_lua_states[i].name);
+	}
+	lua_setfield(L, -2, "state");
+}
+
+/* }}} */
diff --git a/src/lua/popen.h b/src/lua/popen.h
new file mode 100644
index 000000000..e23c5d7e5
--- /dev/null
+++ b/src/lua/popen.h
@@ -0,0 +1,44 @@
+#ifndef TARANTOOL_LUA_POPEN_H_INCLUDED
+#define TARANTOOL_LUA_POPEN_H_INCLUDED
+/*
+ * Copyright 2010-2020, 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 /* TARANTOOL_LUA_POPEN_H_INCLUDED */
diff --git a/test/app-tap/popen.test.lua b/test/app-tap/popen.test.lua
new file mode 100755
index 000000000..65fdc28ac
--- /dev/null
+++ b/test/app-tap/popen.test.lua
@@ -0,0 +1,605 @@
+#!/usr/bin/env tarantool
+
+local popen = require('popen')
+local ffi = require('ffi')
+local errno = require('errno')
+local fiber = require('fiber')
+local clock = require('clock')
+local tap = require('tap')
+local fun = require('fun')
+
+-- For process_is_alive().
+ffi.cdef([[
+    int
+    kill(pid_t pid, int signo);
+]])
+
+-- {{{ Helpers
+
+--
+-- Verify whether a process is alive.
+--
+local function process_is_alive(pid)
+    local rc = ffi.C.kill(pid, 0)
+    return rc == 0 or errno() ~= errno.ESRCH
+end
+
+--
+-- Verify whether a process is dead or not exist.
+--
+local function process_is_dead(pid)
+    return not process_is_alive(pid)
+end
+
+--
+-- Yield the current fiber until a condition becomes true or
+-- timeout (60 seconds) exceeds.
+--
+-- Don't use test-run's function to allow to run the test w/o
+-- test-run. It is often convenient during debugging.
+--
+local function wait_cond(func, ...)
+    local timeout = 60
+    local delay = 0.1
+
+    local deadline = clock.monotonic() + timeout
+    local res
+
+    while true do
+        res = {func(...)}
+        -- Success or timeout.
+        if res[1] or clock.monotonic() > deadline then break end
+        fiber.sleep(delay)
+    end
+
+    return unpack(res, 1, table.maxn(res))
+end
+
+-- }}}
+
+--
+-- Trivial echo output.
+--
+local function test_trivial_echo_output(test)
+    test:plan(6)
+
+    local script = 'printf "1 2 3 4 5"'
+    local exp_script_output = '1 2 3 4 5'
+
+    -- Start printf, wait it to finish, read the output and close
+    -- the handler.
+    local ph = popen.shell(script, 'r')
+    local pid = ph.pid
+    local exp_status = {
+        state = popen.state.EXITED,
+        exit_code = 0,
+    }
+    local status = ph:wait()
+    test:is_deeply(status, exp_status, 'verify process status')
+    local script_output = ph:read()
+    test:is(script_output, exp_script_output, 'verify script output')
+
+    -- Note: EPERM due to opts.group_signal and a zombie process
+    -- on Mac OS is not possible here, because we waited for the
+    -- process: it is not zombie, but not exists at all.
+    local res, err = ph:close()
+    test:is_deeply({res, err}, {true, nil}, 'close() successful')
+
+    -- Verify that the process is actually killed.
+    local is_dead = wait_cond(process_is_dead, pid)
+    test:ok(is_dead, 'the process is killed after close()')
+
+    -- Verify that :close() is idempotent.
+    local res, err = ph:close()
+    test:is_deeply({res, err}, {true, nil}, 'close() is idempotent')
+
+    -- Sending a signal using a closed handle gives an error.
+    local exp_err = 'popen: attempt to operate on a closed handle'
+    local ok, err = pcall(ph.signal, ph, popen.signal.SIGTERM)
+    test:is_deeply({ok, err.type, tostring(err)},
+                   {false, 'IllegalParams', exp_err},
+                   'signal() on closed handle gives an error')
+end
+
+--
+-- Test info and force killing of a child process.
+--
+local function test_kill_child_process(test)
+    test:plan(10)
+
+    -- Run and kill a process.
+    local script = 'while true; do sleep 10; done'
+    local ph = popen.shell(script, 'r')
+    local res, err = ph:kill()
+    test:is_deeply({res, err}, {true, nil}, 'kill() successful')
+
+    local exp_status = {
+        state = popen.state.SIGNALED,
+        signo = popen.signal.SIGKILL,
+        signame = 'SIGKILL',
+    }
+
+    -- Wait for the process termination, verify wait() return
+    -- values.
+    local status = ph:wait()
+    test:is_deeply(status, exp_status, 'wait() return values')
+
+    -- Verify status() return values for a terminated process.
+    test:is_deeply(ph.status, exp_status, 'status() return values')
+
+    -- Verify info for a terminated process.
+    local info = ph:info()
+    test:is(info.pid, nil, 'info.pid is nil')
+
+    local exp_command = ("sh -c '%s'"):format(script)
+    test:is(info.command, exp_command, 'verify info.script')
+
+    local exp_opts = {
+        stdin = popen.opts.INHERIT,
+        stdout = popen.opts.PIPE,
+        stderr = popen.opts.INHERIT,
+        shell = true,
+        setsid = true,
+        close_fds = true,
+        restore_signals = true,
+        group_signal = true,
+        keep_child = false,
+    }
+    test:is_deeply(info.opts, exp_opts, 'verify info.opts')
+
+    test:is_deeply(info.status, exp_status, 'verify info.status')
+
+    test:is(info.stdin, nil, 'verify info.stdin')
+    test:is(info.stdout, popen.stream.OPENED, 'verify info.stdout')
+    test:is(info.stderr, nil, 'verify info.stderr')
+
+    ph:close()
+end
+
+--
+-- Test that a loss handle does not leak (at least the
+-- corresponding process is killed).
+--
+local function test_gc(test)
+    test:plan(1)
+
+    -- Run a process, verify that it exists.
+    local script = 'while true; do sleep 10; done'
+    local ph = popen.shell(script, 'r')
+    local pid = ph.pid
+    assert(process_is_alive(pid))
+
+    -- Loss the handle.
+    ph = nil -- luacheck: no unused
+    collectgarbage()
+
+    -- Verify that the process is actually killed.
+    local is_dead = wait_cond(process_is_dead, pid)
+    test:ok(is_dead, 'the process is killed when the handle is collected')
+end
+
+--
+-- Simple read() / write() test.
+--
+local function test_read_write(test)
+    test:plan(8)
+
+    local payload = 'hello'
+
+    -- The script copies data from stdin to stdout.
+    local script = 'prompt=""; read -r prompt; printf "$prompt"'
+    local ph = popen.shell(script, 'rw')
+
+    -- Write to stdin, read from stdout.
+    local res, err = ph:write(payload .. '\n')
+    test:is_deeply({res, err}, {true, nil}, 'write() succeeds')
+    local ok = ph:shutdown({stdin = true})
+    test:ok(ok, 'shutdown() succeeds')
+    local res, err = ph:read()
+    test:is_deeply({res, err}, {payload, nil}, 'read() from stdout succeeds')
+
+    ph:close()
+
+    -- The script copies data from stdin to stderr.
+    local script = 'prompt=""; read -r prompt; printf "$prompt" 1>&2'
+    local ph = popen.shell(script, 'Rw')
+
+    -- Write to stdin, read from stderr.
+    local res, err = ph:write(payload .. '\n')
+    test:is_deeply({res, err}, {true, nil}, 'write() succeeds')
+    local ok = ph:shutdown({stdin = true})
+    test:ok(ok, 'shutdown() succeeds')
+    local res, err = ph:read({stderr = true})
+    test:is_deeply({res, err}, {payload, nil}, 'read() from stderr succeeds')
+
+    ph:close()
+
+    -- The same script: copy from stdin to stderr.
+    local script = 'prompt=""; read -r prompt; printf "$prompt" 1>&2'
+    local ph = popen.shell(script, 'Rw')
+
+    -- Ensure that read waits for data and does not return
+    -- prematurely.
+    local res_w, err_w
+    fiber.create(function()
+        fiber.sleep(0.1)
+        res_w, err_w = ph:write(payload .. '\n')
+        ph:shutdown({stdin = true})
+    end)
+    local res, err = ph:read({stderr = true})
+    test:is_deeply({res_w, err_w}, {true, nil}, 'write() succeeds')
+    test:is_deeply({res, err}, {payload, nil}, 'read() from stderr succeeds')
+
+    ph:close()
+end
+
+--
+-- Test timeouts: just wait for 0.1 second to elapse, then write
+-- data and re-read for sure.
+--
+local function test_read_timeout(test)
+    test:plan(3)
+
+    local payload = 'hello'
+    local script = 'prompt=""; read -r prompt; printf "$prompt"'
+    local ph = popen.shell(script, 'rw')
+
+    -- Read and get a timeout error.
+    local exp_err = 'timed out'
+    local res, err = ph:read({timeout = 0.1})
+    test:is_deeply({res, err.type, tostring(err)}, {nil, 'TimedOut', exp_err},
+                   'timeout error')
+
+    -- Write and read after the timeout error.
+    local res, err = ph:write(payload .. '\n')
+    test:is_deeply({res, err}, {true, nil}, 'write data')
+    ph:shutdown({stdin = true})
+    local res, err = ph:read()
+    test:is_deeply({res, err}, {payload, nil}, 'read data')
+
+    ph:close()
+end
+
+--
+-- Ensure that read() returns when some data is available (even if
+-- it is one byte).
+--
+local function test_read_chunk(test)
+    test:plan(1)
+
+    local payload = 'hello'
+    local script = ('printf "%s"; sleep 120'):format(payload)
+    local ph = popen.shell(script, 'r')
+
+    -- When a first byte is available, read() should return all
+    -- bytes arrived at the time.
+    local latch = fiber.channel(1)
+    local res, err
+    fiber.create(function()
+        res, err = ph:read()
+        latch:put(true)
+    end)
+    -- Wait 1 second at max.
+    latch:get(1)
+    test:is_deeply({res, err}, {payload, nil}, 'data available prior to EOF')
+
+    ph:close()
+end
+
+--
+-- Ensure that shutdown() closes asked streams: at least
+-- it is reflected in a handle information.
+--
+local function test_shutdown(test)
+    test:plan(9)
+
+    -- Verify std* statuses.
+    local function test_stream_statuses(test, ph, pstream, exp_pstream)
+        test:plan(6)
+        local info = ph:info()
+        for _, s in ipairs({'stdin', 'stdout', 'stderr'}) do
+            local exp_status = s == pstream and exp_pstream or nil
+            test:is(ph[s], exp_status, ('%s opened'):format(s))
+            test:is(info[s], exp_status, ('%s opened'):format(s))
+        end
+    end
+
+    -- Create, verify pstream status, shutdown it,
+    -- verify status again.
+    for _, pstream in ipairs({'stdin', 'stdout', 'stderr'}) do
+        local ph = popen.new({'/bin/true'}, {[pstream] = popen.opts.PIPE})
+
+        test:test(('%s before shutdown'):format(pstream),
+                  test_stream_statuses, ph, pstream,
+                  popen.stream.OPENED)
+
+        local ok = ph:shutdown({[pstream] = true})
+        test:ok(ok, ('shutdown({%s = true}) successful'):format(pstream))
+
+        test:test(('%s after shutdown'):format(pstream),
+                  test_stream_statuses, ph, pstream,
+                  popen.stream.CLOSED)
+
+        -- FIXME: Verify that read / write from pstream gives
+        -- certain error.
+
+        ph:close()
+    end
+end
+
+local function test_shell_invalid_args(test)
+    local function argerr(slot, _)
+        if slot == 1 then
+            return 'popen.shell: wrong parameter'
+        elseif slot == 2 then
+            return 'popen.shell: wrong parameter'
+        else
+            error('Invalid argument check')
+        end
+    end
+
+    -- 1st parameter.
+    local cases1 = {
+        [{nil}]                              = argerr(1, 'no value'),
+        [{true}]                             = argerr(1, 'boolean'),
+        [{false}]                            = argerr(1, 'boolean'),
+        [{0}]                                = argerr(1, 'number'),
+        -- A string is ok.
+        [{''}]                               = nil,
+        [{{}}]                               = argerr(1, 'table'),
+        [{popen.shell}]                      = argerr(1, 'function'),
+        [{io.stdin}]                         = argerr(1, 'userdata'),
+        [{coroutine.create(function() end)}] = argerr(1, 'thread'),
+        [{require('ffi').new('void *')}]     = argerr(1, 'cdata'),
+    }
+
+    -- 2nd parameter.
+    local cases2 = {
+        -- nil is ok ('wrR' is optional).
+        [{nil}]                              = nil,
+        [{true}]                             = argerr(2, 'boolean'),
+        [{false}]                            = argerr(2, 'boolean'),
+        [{0}]                                = argerr(2, 'number'),
+        -- A string is ok.
+        [{''}]                               = nil,
+        [{{}}]                               = argerr(2, 'table'),
+        [{popen.shell}]                      = argerr(2, 'function'),
+        [{io.stdin}]                         = argerr(2, 'userdata'),
+        [{coroutine.create(function() end)}] = argerr(2, 'thread'),
+        [{require('ffi').new('void *')}]     = argerr(2, 'cdata'),
+    }
+
+    test:plan(fun.iter(cases1):length() * 2 + fun.iter(cases2):length() * 2)
+
+    -- Call popen.shell() with
+    for args, err in pairs(cases1) do
+        local arg = unpack(args)
+        local ok, res = pcall(popen.shell, arg)
+        test:ok(not ok, ('command (ok): expected string, got %s')
+                        :format(type(arg)))
+        test:ok(res:match(err), ('command (err): expected string, got %s')
+                                :format(type(arg)))
+    end
+
+    for args, err in pairs(cases2) do
+        local arg = unpack(args)
+        local ok, res = pcall(popen.shell, 'printf test', arg)
+        test:ok(not ok, ('mode (ok): expected string, got %s')
+                        :format(type(arg)))
+        test:ok(res:match(err), ('mode (err): expected string, got %s')
+                                :format(type(arg)))
+    end
+end
+
+local function test_new_invalid_args(test)
+    local function argerr(arg, typename)
+        if arg == 'argv' then
+            return ('popen.new: wrong parameter "%s": expected table, got %s')
+                :format(arg, typename)
+        else
+            error('Invalid argument check')
+        end
+    end
+
+    -- 1st parameter.
+    local cases1 = {
+        [{nil}]                              = argerr('argv', 'nil'),
+        [{true}]                             = argerr('argv', 'boolean'),
+        [{false}]                            = argerr('argv', 'boolean'),
+        [{0}]                                = argerr('argv', 'number'),
+        [{''}]                               = argerr('argv', 'string'),
+        -- FIXME: A table is ok, but not an empty one.
+        [{{}}]                               = nil,
+        [{popen.shell}]                      = argerr('argv', 'function'),
+        [{io.stdin}]                         = argerr('argv', 'userdata'),
+        [{coroutine.create(function() end)}] = argerr('argv', 'thread'),
+        [{require('ffi').new('void *')}]     = argerr('argv', 'cdata'),
+    }
+
+    test:plan(fun.iter(cases1):length() * 2)
+
+    -- Call popen.new() with wrong "argv" parameter.
+    for args, err in pairs(cases1) do
+        local arg = unpack(args)
+        local ok, res = pcall(popen.new, arg)
+        test:ok(not ok, ('new argv (ok): expected table, got %s')
+                        :format(type(arg)))
+        test:ok(res:match(err), ('new argv (err): expected table, got %s')
+                                :format(type(arg)))
+    end
+end
+
+local function test_methods_on_closed_handle(test)
+    local methods = {
+        signal    = {popen.signal.SIGTERM},
+        terminate = {},
+        kill      = {},
+        wait      = {},
+        read      = {},
+        write     = {'hello'},
+        info      = {},
+        -- Close call is idempotent one.
+        close     = nil,
+    }
+
+    test:plan(fun.iter(methods):length() * 2)
+
+    local ph = popen.shell('printf "1 2 3 4 5"', 'r')
+    ph:close()
+
+    -- Call methods on a closed handle.
+    for method, args in pairs(methods) do
+        local ok, err = pcall(ph[method], ph, unpack(args))
+        test:ok(not ok, ('%s (ok) on closed handle'):format(method))
+        test:ok(err:match('popen: attempt to operate on a closed handle'),
+                ('%s (err) on closed handle'):format(method))
+    end
+end
+
+local function test_methods_on_invalid_handle(test)
+    local methods = {
+        signal    = {popen.signal.SIGTERM},
+        terminate = {},
+        kill      = {},
+        wait      = {},
+        read      = {},
+        write     = {'hello'},
+        info      = {},
+        close     = {},
+    }
+
+    test:plan(fun.iter(methods):length() * 4)
+
+    local ph = popen.shell('printf "1 2 3 4 5"', 'r')
+
+    -- Call methods without parameters.
+    for method in pairs(methods) do
+        local ok, err = pcall(ph[method])
+        test:ok(not ok, ('%s (ok) no handle and args'):format(method))
+        test:ok(err:match('Bad params, use: ph:' .. method),
+                ('%s (err) no handle and args'):format(method))
+    end
+
+    ph:close()
+
+    -- A table looks like a totally bad handler.
+    local bh = {}
+
+    -- Call methods on a bad handle.
+    for method, args in pairs(methods) do
+        local ok, err = pcall(ph[method], bh, unpack(args))
+        test:ok(not ok, ('%s (ok) on invalid handle'):format(method))
+        test:ok(err:match('Bad params, use: ph:' .. method),
+                ('%s (err) on invalid handle'):format(method))
+    end
+end
+
+local test = tap.test('popen')
+test:plan(11)
+
+test:test('trivial_echo_output', test_trivial_echo_output)
+test:test('kill_child_process', test_kill_child_process)
+test:test('gc', test_gc)
+test:test('read_write', test_read_write)
+test:test('read_timeout', test_read_timeout)
+test:test('read_chunk', test_read_chunk)
+test:test('test_shutdown', test_shutdown)
+test:test('shell_invalid_args', test_shell_invalid_args)
+test:test('new_invalid_args', test_new_invalid_args)
+test:test('methods_on_closed_handle', test_methods_on_closed_handle)
+test:test('methods_on_invalid_handle', test_methods_on_invalid_handle)
+
+-- Testing plan
+--
+-- FIXME: Implement this plan.
+--
+-- - api usage
+--   - new
+--     - no argv / nil argv
+--     - bad argv
+--       - wrong type
+--       - hole in the table (nil in a middle)
+--       - item
+--         - wrong type
+--       - zero size (w/ / w/o shell)
+--     - bad opts
+--       - wrong type
+--       - {stdin,stdout,stderr}
+--         - wrong type
+--         - wrong string value
+--       - env
+--         - wrong type
+--         - env item
+--           - wrong key type
+--           - wrong value type
+--           - '=' in key
+--           - '\0' in key
+--           - '=' in value
+--           - '\0' in value
+--       - (boolean options)
+--         - wrong type
+--         - conflicting options (!setsid && signal_group)
+--   - shell
+--     - bad handle
+--     - bad mode
+--   - signal
+--     - signal
+--       - wrong type
+--       - unknown value
+--   - read
+--     - FIXME: more cases
+--   - write
+--     - FIXME: more cases
+--   - __index
+--     - zero args (no even handle)
+--     - bad handle
+--     - FIXME: more cases
+--   - __serialize
+--     - zero args (no even handle)
+--     - bad handle
+--   - __gc
+--     - zero args (no even handle)
+--     - bad handle
+--
+-- - verify behaviour
+--   - popen.new: effect of boolean options
+--   - info: verify all four opts.std* actions
+--   - info: get both true and false for each opts.<...> boolean
+--     option
+--   - FiberIsCancelled is raised from read(), write() and wait()
+--
+-- - verify dubious code paths
+--   - popen.new
+--     - env: pass os.environ() with one replaced value
+--     - env: reallocation of env array
+--     - argv construction with and without shell option
+--     - std* actions actual behaviour
+--     - boolean opts: verify true, false and default values has expected
+--       effect (eps.: explicit false when it is default is not considered as
+--       true); at least look at then in :info()
+--   - read / write
+--     - write that needs several write() calls
+--     - feed large input over a process: write it, read in
+--       several chunks; process should terminate on EOF
+--     - child process die during read / write
+--     - boolean opts: verify true, false and default values has expected
+--       effect (eps.: explicit false when it is default is not considered as
+--       true)
+--     - FIXME: more cases
+--   - no extra fds are seen when close_fds is set
+--     verify with different std* options
+--
+-- - protect against inquisitive persons
+--   - ph.__gc(1)
+--   - ph.__gc(ph); ph = nil; collectgarbage()
+--
+-- - unsorted
+--   - test read / write after shutdown
+--   - shutdown
+--     - should be idempotent
+--     - fails w/o opts or w/ empty opts; i.e. at least one stream should be
+--       chosen
+--     - does not close any fds on a failure
+--     - fails when one piped and one non-piped fd are chosen
+
+os.exit(test:check() and 0 or 1)
-- 
2.25.0

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

* Re: [Tarantool-patches] [PATCH 1/2] popen: always free resources in popen_delete()
  2020-04-18  4:13 ` [Tarantool-patches] [PATCH 1/2] popen: always free resources in popen_delete() Alexander Turenko
@ 2020-04-18  6:55   ` Cyrill Gorcunov
  0 siblings, 0 replies; 13+ messages in thread
From: Cyrill Gorcunov @ 2020-04-18  6:55 UTC (permalink / raw)
  To: Alexander Turenko; +Cc: tarantool-patches

On Sat, Apr 18, 2020 at 07:13:54AM +0300, Alexander Turenko wrote:
...
> 
> [1]: https://bugzilla.mozilla.org/show_bug.cgi?id=1329528
> 
> Part of #4031
Acked-by: Cyrill Gorcunov <gorcunov@gmail.com>

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

* Re: [Tarantool-patches] [PATCH 2/2] popen: add popen Lua module
  2020-04-18  4:13 ` [Tarantool-patches] [PATCH 2/2] popen: add popen Lua module Alexander Turenko
@ 2020-04-18  6:57   ` Cyrill Gorcunov
  2020-04-19 12:38   ` Igor Munkin
  2020-04-20  0:57   ` Igor Munkin
  2 siblings, 0 replies; 13+ messages in thread
From: Cyrill Gorcunov @ 2020-04-18  6:57 UTC (permalink / raw)
  To: Alexander Turenko; +Cc: tarantool-patches

On Sat, Apr 18, 2020 at 07:13:55AM +0300, Alexander Turenko wrote:
> Co-developed-by: Cyrill Gorcunov <gorcunov@gmail.com>
> Co-developed-by: Igor Munkin <imun@tarantool.org>
> 
> Fixes #4031
> 

Brilliant!

Acked-by: Cyrill Gorcunov <gorcunov@gmail.com>

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

* Re: [Tarantool-patches] [PATCH 2/2] popen: add popen Lua module
  2020-04-18  4:13 ` [Tarantool-patches] [PATCH 2/2] popen: add popen Lua module Alexander Turenko
  2020-04-18  6:57   ` Cyrill Gorcunov
@ 2020-04-19 12:38   ` Igor Munkin
  2020-04-19 22:24     ` Alexander Turenko
  2020-04-20  0:57   ` Igor Munkin
  2 siblings, 1 reply; 13+ messages in thread
From: Igor Munkin @ 2020-04-19 12:38 UTC (permalink / raw)
  To: Alexander Turenko; +Cc: tarantool-patches

Sasha,

Thanks for the patch! I left several comments regarding a docbot request
with API description and usage, though they can be freely ignored.

On 18.04.20, Alexander Turenko wrote:
> Co-developed-by: Cyrill Gorcunov <gorcunov@gmail.com>
> Co-developed-by: Igor Munkin <imun@tarantool.org>
> 
> Fixes #4031
> 
> @TarantoolBot document
> Title: popen module
> 

<snipped>

> 
> Module functions
> ================
> 
> The `popen` module provides two functions to create that named popen
> object `popen.shell` which is the similar to libc `popen` syscall and

Typo: I guess a colon is missing here.

> `popen.new` to create popen object with more specific options.
> 
> `popen.shell(command[, mode]) -> handle, err`
> ---------------------------------------------
> 
> Execute a shell command.
> 
> @param command  a command to run, mandatory
> @param mode     communication mode, optional
>                 'w'    to use ph:write()
>                 'r'    to use ph:read()
>                 'R'    to use ph:read({stderr = true})
>                 nil    inherit parent's std* file descriptors
> 
> Several mode characters can be set together: 'rw', 'rRw', etc.
> 
> This function is just shortcut for popen.new({command}, opts)
> with opts.{shell,setsid,group_signal} set to `true` and
> and opts.{stdin,stdout,stderr} set based on `mode` parameter.
> 
> All std* streams are inherited from parent by default if it is
> not changed using mode: 'r' for stdout, 'R' for stderr, 'w' for
> stdin.
> 
> Raise an error on incorrect parameters:
> 
> - IllegalParams: incorrect type or value of a parameter.
> 
> Return a popen handle on success.
> 
> Return `nil, err` on a failure.
> @see popen.new() for possible reasons.
> 
> Example:
> 
>  | local popen = require('popen')
>  |
>  | local ph = popen.shell('date', 'r')
>  | local date = ph:read():rstrip()

Minor: IMHO <rstrip> call might lead to a misuse, considering there is
no word about it in description below. I guess you can drop it.

>  | ph:close()
>  | print(date)
> 
> Execute 'sh -c date' command, read the output and close the
> popen object.
> 
> `popen.new(argv[, opts]) -> handle, err`
> ----------------------------------------
> 
> Execute a child program in a new process.
> 
> @param argv  an array of a program to run with
>              command line options, mandatory;
>              absolute path to the program is required

Absolute path is not required when shell option is set to <true>.

> 
> @param opts  table of options
> 
> @param opts.stdin   action on STDIN_FILENO
> @param opts.stdout  action on STDOUT_FILENO
> @param opts.stderr  action on STDERR_FILENO
> 
> File descriptor actions:
> 
>     popen.opts.INHERIT  (== 'inherit') [default]
>                         inherit the fd from the parent
>     popen.opts.DEVNULL  (== 'devnull')
>                         open /dev/null on the fd
>     popen.opts.CLOSE    (== 'close')
>                         close the fd
>     popen.opts.PIPE     (== 'pipe')
>                         feed data from/to the fd to parent
>                         using a pipe
> 
> @param opts.env  an array of environment variables to
>                  be used inside a process;
>                  - when is not set then the current
>                    environment is inherited;
>                  - if set then the environment will be
>                    replaced
>                  - if set to an empty array then the
>                    environment will be dropped

Minor: I looks more convenient to sort the lines above the following
way:
* when is not set <...>
* if set to an empty table <...>
* if set to a non-empty table <...>

> 
> @param opts.shell            (boolean, default: false)
>        true                  run a child process via
>                              'sh -c "${opts.argv}"'
>        false                 call the executable directly
> 
> @param opts.setsid           (boolean, default: false)
>        true                  start executable inside a new
>                              session
>        false                 don't do that

Minor: I reword it in more clear way below:
| @param opts.setsid           (boolean, default: false)
|        true                  run the program in a new
|                              session
|        false                 run the program in the
|                              tarantool instance's
|                              session

> 
> @param opts.close_fds        (boolean, default: true)
>        true                  close all inherited fds from a
>                              parent
>        false                 don't do that
> 
> @param opts.restore_signals  (boolean, default: true)
>        true                  reset all signal actions
>                              modified in parent's process
>        false                 inherit changed actions
> 
> @param opts.group_signal     (boolean, default: false)
>        true                  send signal to a child process
>                              group (only when opts.setsid is
>                              enabled)
>        false                 send signal to a child process
>                              only
> 
> @param opts.keep_child       (boolean, default: false)
>        true                  don't send SIGKILL to a child
>                              process at freeing (by :close()
>                              or Lua GC)
>        false                 send SIGKILL to a child process
>                              (or a process group if
>                              opts.group_signal is enabled) at
>                              :close() or collecting of the
>                              handle by Lua GC
> 
> The returned handle is subject to garbage collection, but
> it may be freed explicitly using :close(). SIGKILL is sent
> to the child process at :close() / GC if it is alive and
> opts.keep_child is not set.

Minor: Let's adjust the terminology. What do you mean speaking about
"handle"? If it's a GC object, then there is no way to release it
manually or "explicitly", only via platform GC. If it's a GC object
*payload* then it's not managed by Lua GC and need to be explicitly
released via special method or the corresponding __gc metamethod.

> 
> It is recommended to use opts.setsid + opts.group_signal
> if a child process may spawn its own childs and they all
> should be killed together.
> 
> Note: A signal will not be sent if the child process is
> already dead: otherwise we might kill another process that
> occupies the same PID later. This means that if the child
> process dies before its own childs, the function will not
> send a signal to the process group even when opts.setsid and
> opts.group_signal are set.
> 
> Use os.environ() to pass copy of current environment with
> several replacements (see example 2 below).
> 
> Raise an error on incorrect parameters:
> 
> - IllegalParams: incorrect type or value of a parameter.
> - IllegalParams: group signal is set, while setsid is not.
> 
> Return a popen handle on success.
> 
> Return `nil, err` on a failure. Possible reasons:
> 
> - SystemError: dup(), fcntl(), pipe(), vfork() or close()
>                fails in the parent process.
> - SystemError: (temporary restriction) the parent process
>                has closed stdin, stdout or stderr.
> - OutOfMemory: unable to allocate the handle or a temporary
>                buffer.
> 
> Example 1:
> 
>  | local popen = require('popen')
>  |
>  | local ph = popen.new({'/bin/date'}, {
>  |     stdout = popen.opts.PIPE,
>  | })
>  | local date = ph:read():rstrip()
>  | ph:close()
>  | print(date) -- Thu 16 Apr 2020 01:40:56 AM MSK
> 
> Execute 'date' command, read the result and close the
> popen object.

Minor: Again, no mention about rstrip.

> 
> Example 2:
> 
>  | local popen = require('popen')
>  |
>  | local env = os.environ()
>  | env['FOO'] = 'bar'
>  |
>  | local ph = popen.new({'echo "${FOO}"'}, {
>  |     stdout = popen.opts.PIPE,
>  |     shell = true,
>  |     env = env,
>  | })
>  | local res = ph:read():rstrip()
>  | ph:close()
>  | print(res) -- bar
> 
> It is quite similar to the previous one, but sets the
> environment variable and uses shell builtin 'echo' to
> show it.
> 
> Example 3:
> 
>  | local popen = require('popen')
>  |
>  | local ph = popen.new({'echo hello >&2'}, { -- !!
>  |     stderr = popen.opts.PIPE,              -- !!
>  |     shell = true,
>  | })
>  | local res = ph:read({stderr = true}):rstrip()
>  | ph:close()
>  | print(res) -- hello
> 
> This example demonstrates how to capture child's stderr.
> 
> Example 4:
> 
>  | local function call_jq(stdin, filter)

Minor: <stdin> look a bit misleading. What about <input>, <json>, etc?

>  |     -- Start jq process, connect to stdin, stdout and stderr.
>  |     local jq_argv = {'/usr/bin/jq', '-M', '--unbuffered', filter}
>  |     local ph, err = popen.new(jq_argv, {
>  |         stdin = popen.opts.PIPE,
>  |         stdout = popen.opts.PIPE,
>  |         stderr = popen.opts.PIPE,
>  |     })
>  |     if ph == nil then return nil, err end
>  |
>  |     -- Write input data to child's stdin and send EOF.
>  |     local ok, err = ph:write(stdin)
>  |     if not ok then return nil, err end
>  |     ph:shutdown({stdin = true})
>  |
>  |     -- Read everything until EOF.
>  |     local chunks = {}
>  |     while true do
>  |         local chunk, err = ph:read()
>  |         if chunk == nil then
>  |             ph:close()
>  |             return nil, err
>  |         end
>  |         if chunk == '' then break end -- EOF
>  |         table.insert(chunks, chunk)
>  |     end
>  |
>  |     -- Read diagnostics from stderr if any.
>  |     local err = ph:read({stderr = true})
>  |     if err ~= '' then
>  |         ph:close()
>  |         return nil, err
>  |     end
>  |
>  |     -- Glue all chunks, strip trailing newline.

Side note: This comment is definitely needed near <rstrip> calls above.

>  |     return table.concat(chunks):rstrip()
>  | end
> 
> Demonstrates how to run a stream program (like `grep`, `sed`
> and so), write to its stdin and read from its stdout.
> 
> The example assumes that input data are small enough to fit
> a pipe buffer (typically 64 KiB, but depends on a platform
> and its configuration). It will stuck in :write() for large
> data. How to handle this case: call :read() in a loop in
> another fiber (start it before a first :write()).
> 
> If a process writes large text to stderr, it may fill out
> stderr pipe buffer and block in write(2, ...). So we need to
> read stderr too in another fiber to handle this case.

Typo: s/stderr too in another fiber/stderr in another fiber too/.

> 
> Handle methods
> ==============
> 
> `popen_handle:read([opts]) -> str, err`
> ---------------------------------------
> 
> Read data from a child peer.
> 
> @param handle        handle of a child process
> @param opts          an options table
> @param opts.stdout   whether to read from stdout, boolean
>                      (default: true)
> @param opts.stderr   whether to read from stderr, boolean
>                      (default: false)
> @param opts.timeout  time quota in seconds
>                      (default: 100 years)
> 
> Read data from stdout or stderr streams with @a timeout.
> By default it reads from stdout. Set @a opts.stderr to
> `true` to read from stderr.

If only <stderr> is set to true, is stdout output also captured?

> 
> Raise an error on incorrect parameters or when the fiber is
> cancelled:
> 
> - IllegalParams:    incorrect type or value of a parameter.
> - IllegalParams:    called on a closed handle.
> - IllegalParams:    opts.stdout and opts.stderr are set both
> - IllegalParams:    a requested IO operation is not supported
>                     by the handle (stdout / stderr is not
>                     piped).
> - IllegalParams:    attempt to operate on a closed file
>                     descriptor.
> - FiberIsCancelled: cancelled by an outside code.
> 
> Return a string on success, an empty string at EOF.
> 
> Return `nil, err` on a failure. Possible reasons:
> 
> - SocketError: an IO error occurs at read().

SocketError on pipe looks ridiculous to me. Maybe an IO one?

> - TimedOut:    @a timeout quota is exceeded.
> - OutOfMemory: no memory space for a buffer to read into.
> - LuajitError: ("not enough memory"): no memory space for
>                the Lua string.
> 

<snipped>

> 
> `popen_handle:shutdown(opts) -> true`

Minor: Considering the method description, <eof> looks more convenient
name here. On the other hand <eof> name might be misguiding one
considering feof(3) standart C function.

> ------------------------------------------
> 
> Close parent's ends of std* fds.
> 
> @param handle        handle of a child process
> @param opts          an options table
> @param opts.stdin    close parent's end of stdin, boolean
> @param opts.stdout   close parent's end of stdout, boolean
> @param opts.stderr   close parent's end of stderr, boolean
> 
> The main reason to use this function is to send EOF to
> child's stdin. However parent's end of stdout / stderr
> may be closed too.
> 
> The function does not fail on already closed fds (idempotence).
> However it fails on attempt to close the end of a pipe that was
> never exist. In other words, only those std* options that
> were set to popen.opts.PIPE at a handle creation may be used
> here (for popen.shell: 'r' corresponds to stdout, 'R' to stderr
> and 'w' to stdin).
> 
> The function does not close any fds on a failure: either all
> requested fds are closed or neither of them.
> 
> Example:
> 
>  | local popen = require('popen')
>  |
>  | local ph = popen.shell('sed s/foo/bar/', 'rw')
>  | ph:write('lorem foo ipsum')
>  | ph:shutdown({stdin = true})
>  | local res = ph:read()
>  | ph:close()
>  | print(res) -- lorem bar ipsum
> 
> Raise an error on incorrect parameters:
> 
> - IllegalParams:  an incorrect handle parameter.
> - IllegalParams:  called on a closed handle.
> - IllegalParams:  neither stdin, stdout nor stderr is choosen.
> - IllegalParams:  a requested IO operation is not supported
>                   by the handle (one of std* is not piped).
> 
> Return `true` on success.
> 

<snipped>

> 
> `popen_handle:info() -> res, err`

<err> is never returned.

> ---------------------------------
> 
> Return information about popen handle.
> 
> @param handle  a handle of a child process
> 
> Raise an error on incorrect parameters:
> 
> - IllegalParams: an incorrect handle parameter.
> - IllegalParams: called on a closed handle.
> 
> Return information about the handle in the following
> format:
> 
>     {
>         pid = <number> or <nil>,
>         command = <string>,
>         opts = <table>,
>         status = <table>,
>         stdin = one-of(
>             popen.stream.OPENED (== 'opened'),
>             popen.stream.CLOSED (== 'closed'),
>             nil,
>         ),
>         stdout = one-of(
>             popen.stream.OPENED (== 'opened'),
>             popen.stream.CLOSED (== 'closed'),
>             nil,
>         ),
>         stderr = one-of(
>             popen.stream.OPENED (== 'opened'),
>             popen.stream.CLOSED (== 'closed'),
>             nil,
>         ),
>     }
> 
> `pid` is a process id of the process when it is alive,
> otherwise `pid` is nil.
> 
> `command` is a concatenation of space separated arguments
> that were passed to execve(). Multiword arguments are quoted.
> Quotes inside arguments are not escaped.
> 
> `opts` is a table of handle options in the format of
> popen.new() `opts` parameter. `opts.env` is not shown here,
> because the environment variables map is not stored in a
> handle.
> 
> `status` is a table that represents a process status in the
> following format:
> 
>     {
>         state = one-of(
>             popen.state.ALIVE    (== 'alive'),
>             popen.state.EXITED   (== 'exited'),
>             popen.state.SIGNALED (== 'signaled'),
>         )
> 
>         -- Present when `state` is 'exited'.
>         exit_code = <number>,
> 
>         -- Present when `state` is 'signaled'.
>         signo = <number>,
>         signame = <string>,
>     }
> 
> `stdin`, `stdout`, `stderr` reflect status of parent's end
> of a piped stream. When a stream is not piped the field is
> not present (`nil`). When it is piped, the status may be
> one of the following:
> 
> - popen.stream.OPENED  (== 'opened')
> - popen.stream.CLOSED  (== 'closed')
> 
> The status may be changed from 'opened' to 'closed'
> by :shutdown({std... = true}) call.
> 
> Example 1 (tarantool console):
> 
>  | tarantool> require('popen').new({'/usr/bin/touch', '/tmp/foo'})
>  | ---
>  | - command: /usr/bin/touch /tmp/foo
>  |   status:
>  |     state: alive
>  |   opts:
>  |     stdout: inherit
>  |     stdin: inherit
>  |     group_signal: false
>  |     keep_child: false
>  |     close_fds: true
>  |     restore_signals: true
>  |     shell: false
>  |     setsid: false
>  |     stderr: inherit
>  |   pid: 9499
>  | ...
> 
> Example 2 (tarantool console):
> 
>  | tarantool> require('popen').shell('grep foo', 'wrR')
>  | ---
>  | - stdout: opened
>  |   command: sh -c 'grep foo'
>  |   stderr: opened
>  |   status:
>  |     state: alive
>  |   stdin: opened
>  |   opts:
>  |     stdout: pipe
>  |     stdin: pipe
>  |     group_signal: true
>  |     keep_child: false
>  |     close_fds: true
>  |     restore_signals: true
>  |     shell: true
>  |     setsid: true
>  |     stderr: pipe
>  |   pid: 10497
>  | ...
> 
> `popen_handle:wait() -> res, err`

<err> is never returned.

> ---------------------------------
> 
> Wait until a child process get exited or signaled.
> 
> @param handle  a handle of process to wait
> 
> Raise an error on incorrect parameters or when the fiber is
> cancelled:
> 
> - IllegalParams:    an incorrect handle parameter.
> - IllegalParams:    called on a closed handle.
> - FiberIsCancelled: cancelled by an outside code.
> 
> Return a process status table (the same as ph.status and
> ph.info().status). @see popen_handle:info() for the format
> of the table.
> 

<snipped>

> ---

<snipped>

> -- 
> 2.25.0
> 

-- 
Best regards,
IM

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

* Re: [Tarantool-patches] [PATCH 2/2] popen: add popen Lua module
  2020-04-19 12:38   ` Igor Munkin
@ 2020-04-19 22:24     ` Alexander Turenko
  2020-04-20  1:21       ` Igor Munkin
  0 siblings, 1 reply; 13+ messages in thread
From: Alexander Turenko @ 2020-04-19 22:24 UTC (permalink / raw)
  To: Igor Munkin; +Cc: tarantool-patches

Thanks for the careful proofreading!

Aside of changes below I changed `popen.stream.OPENED (== 'opened')` to
`popen.stream.OPEN (== 'open')` to match english grammar. Same for
statuses -> status (yep, 'statuses' is allowed in English, but it is
ugly).

Force-pushed Totktonada/gh-4031-popen-13-full-ci.

WBR, Alexander Turenko.

----

> > The `popen` module provides two functions to create that named popen
> > object `popen.shell` which is the similar to libc `popen` syscall and
> 
> Typo: I guess a colon is missing here.

Thanks! Fixed.

> > Example:
> > 
> >  | local popen = require('popen')
> >  |
> >  | local ph = popen.shell('date', 'r')
> >  | local date = ph:read():rstrip()
> 
> Minor: IMHO <rstrip> call might lead to a misuse, considering there is
> no word about it in description below. I guess you can drop it.

It is quite minor IMHO, but I don't mind.

Added the explanation to .shell() function description in the code and
the commit message:

diff --git a/src/lua/popen.c b/src/lua/popen.c
index 40f4343eb..8ae01b3f1 100644
--- a/src/lua/popen.c
+++ b/src/lua/popen.c
@@ -1408,6 +1396,18 @@ err:
  *
  * Execute 'sh -c date' command, read the output and close the
  * popen object.
+ *
+ * Unix defines a text file as a sequence of lines, each ends
+ * with the newline symbol. The same convention is usually
+ * applied for a text output of a command (so when it is
+ * redirected to a file, the file will be correct).
+ *
+ * However internally an application usually operates on
+ * strings, which are NOT newline terminated (e.g. literals
+ * for error messages). The newline is usually added right
+ * before a string is written to the outside world (stdout,
+ * console or log). :rstrip() in the example above is shown
+ * for this sake.
  */
 static int
 lbox_popen_shell(struct lua_State *L)

I left .new() description intact: it should be described once and
.shell() going first in the documentation request (commit message).

> > Execute a child program in a new process.
> > 
> > @param argv  an array of a program to run with
> >              command line options, mandatory;
> >              absolute path to the program is required
> 
> Absolute path is not required when shell option is set to <true>.

Good catch!

Added to the comment in the code and to the commit message:

diff --git a/src/lua/popen.c b/src/lua/popen.c
index 2aa0a5851..bf06bbfa7 100644
--- a/src/lua/popen.c
+++ b/src/lua/popen.c
@@ -1114,6 +1114,7 @@ luaT_popen_parse_mode(struct lua_State *L, int idx)
  * @param argv  an array of a program to run with
  *              command line options, mandatory;
  *              absolute path to the program is required
+ *              when @a opts.shell is false (default)
  *
  * @param opts  table of options
  *

> > @param opts.env  an array of environment variables to
> >                  be used inside a process;
> >                  - when is not set then the current
> >                    environment is inherited;
> >                  - if set then the environment will be
> >                    replaced
> >                  - if set to an empty array then the
> >                    environment will be dropped
> 
> Minor: I looks more convenient to sort the lines above the following
> way:
> * when is not set <...>
> * if set to an empty table <...>
> * if set to a non-empty table <...>

I don't mind.

Reordered in the code and the commit message.

Also I rephrased the description a bit to highlight the fact that the
parameter is key-value mapping, not an array.

diff --git a/src/lua/popen.c b/src/lua/popen.c
index bf06bbfa7..f17e3faf1 100644
--- a/src/lua/popen.c
+++ b/src/lua/popen.c
@@ -1134,14 +1134,16 @@ luaT_popen_parse_mode(struct lua_State *L, int idx)
  *                         feed data from/to the fd to parent
  *                         using a pipe
  *
- * @param opts.env  an array of environment variables to
- *                  be used inside a process;
+ * @param opts.env  a table of environment variables to
+ *                  be used inside a process; key is a
+ *                  variable name, value is a variable
+ *                  value.
  *                  - when is not set then the current
  *                    environment is inherited;
+ *                  - if set to an empty table then the
+ *                    environment will be dropped
  *                  - if set then the environment will be
  *                    replaced
- *                  - if set to an empty array then the
- *                    environment will be dropped
  *
  * @param opts.shell            (boolean, default: false)
  *        true                  run a child process via

> > @param opts.setsid           (boolean, default: false)
> >        true                  start executable inside a new
> >                              session
> >        false                 don't do that
> 
> Minor: I reword it in more clear way below:
> | @param opts.setsid           (boolean, default: false)
> |        true                  run the program in a new
> |                              session
> |        false                 run the program in the
> |                              tarantool instance's
> |                              session

So I would add "...and process group". Changed:

diff --git a/src/lua/popen.c b/src/lua/popen.c
index f17e3faf1..ec8c95662 100644
--- a/src/lua/popen.c
+++ b/src/lua/popen.c
@@ -1151,9 +1151,11 @@ luaT_popen_parse_mode(struct lua_State *L, int idx)
  *        false                 call the executable directly
  *
  * @param opts.setsid           (boolean, default: false)
- *        true                  start executable inside a new
+ *        true                  run the program in a new
  *                              session
- *        false                 don't do that
+ *        false                 run the program in the
+ *                              tarantool instance's
+ *                              session and process group
  *
  * @param opts.close_fds        (boolean, default: true)
  *        true                  close all inherited fds from a

> > The returned handle is subject to garbage collection, but
> > it may be freed explicitly using :close(). SIGKILL is sent
> > to the child process at :close() / GC if it is alive and
> > opts.keep_child is not set.
> 
> Minor: Let's adjust the terminology. What do you mean speaking about
> "handle"? If it's a GC object, then there is no way to release it
> manually or "explicitly", only via platform GC. If it's a GC object
> *payload* then it's not managed by Lua GC and need to be explicitly
> released via special method or the corresponding __gc metamethod.

My intention is to give two ideas here:

* Feel free to use explicit :close() or lean on Lua GC.
* :close() / GC handler kills the child process.

But being strict in terms and wording is goal for me too, so I reword
the paragraph to eliminate phrases, which requires a GC object / a GC
object payload as an object / a subject.

Changed in the code and the commit:

diff --git a/src/lua/popen.c b/src/lua/popen.c
index ec8c95662..31b4bc2b0 100644
--- a/src/lua/popen.c
+++ b/src/lua/popen.c
@@ -1184,10 +1184,11 @@ luaT_popen_parse_mode(struct lua_State *L, int idx)
  *                              :close() or collecting of the
  *                              handle by Lua GC
  *
- * The returned handle is subject to garbage collection, but
- * it may be freed explicitly using :close(). SIGKILL is sent
- * to the child process at :close() / GC if it is alive and
- * opts.keep_child is not set.
+ * The returned handle provides :close() method to explicitly
+ * release all occupied resources (including the child process
+ * itself if @a opts.keep_child is not set). However if the
+ * method is not called for a handle during its lifetime, the
+ * same freeing actions will be triggered by Lua GC.
  *
  * It is recommended to use opts.setsid + opts.group_signal
  * if a child process may spawn its own childs and they all

> > Example 1:
> > 
> >  | local popen = require('popen')
> >  |
> >  | local ph = popen.new({'/bin/date'}, {
> >  |     stdout = popen.opts.PIPE,
> >  | })
> >  | local date = ph:read():rstrip()
> >  | ph:close()
> >  | print(date) -- Thu 16 Apr 2020 01:40:56 AM MSK
> > 
> > Execute 'date' command, read the result and close the
> > popen object.
> 
> Minor: Again, no mention about rstrip.

I think once is enough. Added the popen.shell() description.

> > Example 4:
> > 
> >  | local function call_jq(stdin, filter)
> 
> Minor: <stdin> look a bit misleading. What about <input>, <json>, etc?

Okay, 'input'.

diff --git a/src/lua/popen.c b/src/lua/popen.c
index 31b4bc2b0..bf79a5d29 100644
--- a/src/lua/popen.c
+++ b/src/lua/popen.c
@@ -1270,7 +1270,7 @@ luaT_popen_parse_mode(struct lua_State *L, int idx)
  *
  * Example 4:
  *
- *  | local function call_jq(stdin, filter)
+ *  | local function call_jq(input, filter)
  *  |     -- Start jq process, connect to stdin, stdout and stderr.
  *  |     local jq_argv = {'/usr/bin/jq', '-M', '--unbuffered', filter}
  *  |     local ph, err = popen.new(jq_argv, {
@@ -1281,7 +1281,7 @@ luaT_popen_parse_mode(struct lua_State *L, int idx)
  *  |     if ph == nil then return nil, err end
  *  |
  *  |     -- Write input data to child's stdin and send EOF.
- *  |     local ok, err = ph:write(stdin)
+ *  |     local ok, err = ph:write(input)
  *  |     if not ok then return nil, err end
  *  |     ph:shutdown({stdin = true})
  *  |

> >  |     -- Read diagnostics from stderr if any.
> >  |     local err = ph:read({stderr = true})
> >  |     if err ~= '' then
> >  |         ph:close()
> >  |         return nil, err
> >  |     end
> >  |
> >  |     -- Glue all chunks, strip trailing newline.
> 
> Side note: This comment is definitely needed near <rstrip> calls above.

Added once, to popen.shell() example:

diff --git a/src/lua/popen.c b/src/lua/popen.c
index bf79a5d29..6cfb268a1 100644
--- a/src/lua/popen.c
+++ b/src/lua/popen.c
@@ -1395,9 +1395,16 @@ err:
  *
  *  | local popen = require('popen')
  *  |
+ *  | -- Run the program and save its handle.
  *  | local ph = popen.shell('date', 'r')
+ *  |
+ *  | -- Read program's output, strip trailing newline.
  *  | local date = ph:read():rstrip()
+ *  |
+ *  | -- Free resources. The process is killed (but 'date'
+ *  | -- exits itself anyway).
  *  | ph:close()
+ *  |
  *  | print(date)
  *
  * Execute 'sh -c date' command, read the output and close the

> > If a process writes large text to stderr, it may fill out
> > stderr pipe buffer and block in write(2, ...). So we need to
> > read stderr too in another fiber to handle this case.
> 
> Typo: s/stderr too in another fiber/stderr in another fiber too/.

I want to avoid possible understanding as 'read stderr here and in
another fiber too'. I can just remove 'too'. I also changed 'block' to
'stuck':

diff --git a/src/lua/popen.c b/src/lua/popen.c
index 6cfb268a1..6835286c7 100644
--- a/src/lua/popen.c
+++ b/src/lua/popen.c
@@ -1318,8 +1318,8 @@ luaT_popen_parse_mode(struct lua_State *L, int idx)
  * another fiber (start it before a first :write()).
  *
  * If a process writes large text to stderr, it may fill out
- * stderr pipe buffer and block in write(2, ...). So we need to
- * read stderr too in another fiber to handle this case.
+ * stderr pipe buffer and stuck in write(2, ...). So we need
+ * to read stderr in a separate fiber to handle this case.
  */
 static int
 lbox_popen_new(struct lua_State *L)

> > Read data from stdout or stderr streams with @a timeout.
> > By default it reads from stdout. Set @a opts.stderr to
> > `true` to read from stderr.
> 
> If only <stderr> is set to true, is stdout output also captured?

Nope. There is an error about it in the list below, but, yep, it worth
to highlight it. Added the note:

diff --git a/src/lua/popen.c b/src/lua/popen.c
index 6835286c7..a071065b4 100644
--- a/src/lua/popen.c
+++ b/src/lua/popen.c
@@ -1647,6 +1647,9 @@ lbox_popen_wait(struct lua_State *L)
  * By default it reads from stdout. Set @a opts.stderr to
  * `true` to read from stderr.
  *
+ * It is not possible to read from stdout and stderr both in
+ * one call. Set either @a opts.stdout or @a opts.stderr.
+ *
  * Raise an error on incorrect parameters or when the fiber is
  * cancelled:
  *

> > Return `nil, err` on a failure. Possible reasons:
> > 
> > - SocketError: an IO error occurs at read().
> 
> SocketError on pipe looks ridiculous to me. Maybe an IO one?

It comes from coio.cc. It would be good to refactor it someday, but not
tonight :)

> > `popen_handle:shutdown(opts) -> true`
> 
> Minor: Considering the method description, <eof> looks more convenient
> name here. On the other hand <eof> name might be misguiding one
> considering feof(3) standart C function.

:close() for ph.{stdin,stdout,stderr} would be ideal, but we don't have
something like separate read / write streams now. We have bidirectional
handle for reading and writing, so the nearest counterpart would be
socket's shutdown().

More on this: closing of stdin is EOF for a child, but closing of stdout
/ stderr will lead to SIGPIPE (+EPIPE in errno) at write() in the child.

I think the name we have now is good.

> > `popen_handle:info() -> res, err`
> 
> <err> is never returned.

Good catch!

Fixed.

> > `popen_handle:wait() -> res, err`
> 
> <err> is never returned.

Thanks!

Fixed.

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

* Re: [Tarantool-patches] [PATCH 2/2] popen: add popen Lua module
  2020-04-18  4:13 ` [Tarantool-patches] [PATCH 2/2] popen: add popen Lua module Alexander Turenko
  2020-04-18  6:57   ` Cyrill Gorcunov
  2020-04-19 12:38   ` Igor Munkin
@ 2020-04-20  0:57   ` Igor Munkin
  2020-04-20  6:38     ` Alexander Turenko
  2 siblings, 1 reply; 13+ messages in thread
From: Igor Munkin @ 2020-04-20  0:57 UTC (permalink / raw)
  To: Alexander Turenko; +Cc: tarantool-patches

Sasha,

Thanks for the patch! I left several nits, please consider them.
Otherwise, LGTM.

On 18.04.20, Alexander Turenko wrote:
> Co-developed-by: Cyrill Gorcunov <gorcunov@gmail.com>
> Co-developed-by: Igor Munkin <imun@tarantool.org>
> 
> Fixes #4031

<snipped>

> ---
>  src/CMakeLists.txt          |    1 +
>  src/lua/init.c              |    2 +
>  src/lua/popen.c             | 2457 +++++++++++++++++++++++++++++++++++
>  src/lua/popen.h             |   44 +
>  test/app-tap/popen.test.lua |  605 +++++++++
>  5 files changed, 3109 insertions(+)
>  create mode 100644 src/lua/popen.c
>  create mode 100644 src/lua/popen.h
>  create mode 100755 test/app-tap/popen.test.lua
> 

<snipped>

> diff --git a/src/lua/popen.c b/src/lua/popen.c
> new file mode 100644
> index 000000000..40f4343eb
> --- /dev/null
> +++ b/src/lua/popen.c
> @@ -0,0 +1,2457 @@

<snipped>

> +
> +/* {{{ General-purpose Lua helpers */
> +
> +/**
> + * Extract a string from the Lua stack.
> + *
> + * Return (const char *) if so, otherwise return NULL.
> + *
> + * Unlike luaL_checkstring() it accepts only a string and does not
> + * accept a number.
> + *
> + * Unlike luaL_checkstring() it does not raise an error, but
> + * returns NULL when a type is not what is excepted.
> + */
> +static const char *
> +luaL_check_string_strict(struct lua_State *L, int idx, size_t *len_ptr)

Minor: I propose the following names: <luaL_tolstring_strict>,
<luaL_checkstring_nothrow> or <luaL_checkstring_noxc>.

Side note: Sorry, but I can't pass by it. Sasha, we both know each
other's opinion regarding luaL_* prefix usage, and I'm not going to
argue about it within this review. I'll try to live with it since it's
a static one, but I see no reasons to not try to name it other way.

> +{
> +	if (lua_type(L, idx) != LUA_TSTRING)
> +		return NULL;
> +
> +	const char *res = lua_tolstring(L, idx, len_ptr);
> +	assert(res != NULL);
> +	return res;
> +}
> +

<snipped>

> +
> +/**
> + * Helper for luaT_push_string_noxc().
> + */
> +static int
> +luaT_push_string_noxc_wrapper(struct lua_State *L)
> +{
> +	char *str = (char *)lua_topointer(L, 1);
> +	size_t len = lua_tointeger(L, 2);
> +	lua_pushlstring(L, str, len);
> +	return 1;
> +}
> +
> +/**
> + * Push a string to the Lua stack.
> + *
> + * Return 0 at success, -1 at failure and set a diag.
> + *
> + * Possible errors:
> + *
> + * - LuajitError ("not enough memory"): no memory space for the
> + *   Lua string.
> + */
> +static int
> +luaT_push_string_noxc(struct lua_State *L, char *str, size_t len)
> +{
> +	lua_pushcfunction(L, luaT_push_string_noxc_wrapper);
> +	lua_pushlightuserdata(L, str);
> +	lua_pushinteger(L, len);
> +	return luaT_call(L, 2, 1);
> +}

Minor: IMHO luaT_pushstring_* is more Lua-like naming.

> +
> +/* }}} */
> +
> +/* {{{ Popen handle userdata manipulations */
> +
> +/**
> + * Extract popen handle from the Lua stack.
> + *
> + * Return NULL in case of unexpected type.
> + */
> +static struct popen_handle *
> +luaT_check_popen_handle(struct lua_State *L, int idx, bool *is_closed_ptr)
> +{
> +	struct popen_handle **handle_ptr =
> +		luaL_testudata(L, idx, popen_handle_uname);
> +	bool is_closed = false;
> +
> +	if (handle_ptr == NULL) {
> +		handle_ptr = luaL_testudata(L, idx, popen_handle_closed_uname);
> +		is_closed = true;
> +	}
> +
> +	if (handle_ptr == NULL)
> +		return NULL;

Minor: If NULL is returned, then is_closed_ptr payload is undefined. It
would be nice to mention it in contract above.

> +	assert(*handle_ptr != NULL);
> +
> +	if (is_closed_ptr != NULL)

Minor: Considering function usage this check is excess.

> +		*is_closed_ptr = is_closed;
> +	return *handle_ptr;
> +}
> +

<snipped>

> +
> +/* }}} */
> +
> +/* {{{ Push popen handle info to the Lua stack */
> +

<snipped>

> +
> +/**
> + * Push popen options as a Lua table.
> + *
> + * Environment variables are not stored in a popen handle and so
> + * missed here.
> + */
> +static int
> +luaT_push_popen_opts(struct lua_State *L, unsigned int flags)
> +{
> +	lua_createtable(L, 0, 8);
> +
> +	luaT_push_popen_stdX_action(L, STDIN_FILENO, flags);
> +	lua_setfield(L, -2, "stdin");
> +
> +	luaT_push_popen_stdX_action(L, STDOUT_FILENO, flags);
> +	lua_setfield(L, -2, "stdout");
> +
> +	luaT_push_popen_stdX_action(L, STDERR_FILENO, flags);
> +	lua_setfield(L, -2, "stderr");

Minor: This can be turned into a loop.

> +
> +	/* env is skipped */
> +
> +	lua_pushboolean(L, (flags & POPEN_FLAG_SHELL) != 0);
> +	lua_setfield(L, -2, "shell");
> +
> +	lua_pushboolean(L, (flags & POPEN_FLAG_SETSID) != 0);
> +	lua_setfield(L, -2, "setsid");
> +
> +	lua_pushboolean(L, (flags & POPEN_FLAG_CLOSE_FDS) != 0);
> +	lua_setfield(L, -2, "close_fds");
> +
> +	lua_pushboolean(L, (flags & POPEN_FLAG_RESTORE_SIGNALS) != 0);
> +	lua_setfield(L, -2, "restore_signals");
> +
> +	lua_pushboolean(L, (flags & POPEN_FLAG_GROUP_SIGNAL) != 0);
> +	lua_setfield(L, -2, "group_signal");
> +
> +	lua_pushboolean(L, (flags & POPEN_FLAG_KEEP_CHILD) != 0);
> +	lua_setfield(L, -2, "keep_child");

Minor: This can be also turned into a loop.

> +
> +	return 1;
> +}
> +

<snipped>

> +
> +/* }}} */
> +
> +/* {{{ Parameter parsing */
> +

<snipped>

> +
> +/**
> + * Parse popen.new() "opts" parameter.
> + *
> + * Prerequisite: @a opts should be zero filled.
> + *
> + * Result: @a opts structure is filled.
> + *
> + * Raise an error in case of the incorrect parameter.
> + *
> + * Return 0 at success. Allocates opts->env on @a region if
> + * needed. @see luaT_popen_parse_env() for details how to
> + * free it.
> + *
> + * Return -1 in case of an allocation error and set a diag
> + * (OutOfMemory).
> + */
> +static int
> +luaT_popen_parse_opts(struct lua_State *L, int idx, struct popen_opts *opts,
> +		      struct region *region)

Minor: I rewrote it a bit, consider the diff below:

================================================================================

diff --git a/src/lua/popen.c b/src/lua/popen.c
index 40f4343eb..f5d651401 100644
--- a/src/lua/popen.c
+++ b/src/lua/popen.c
@@ -869,24 +869,19 @@ luaT_popen_parse_opts(struct lua_State *L, int idx, struct popen_opts *opts,
 
 	/* Parse options. */
 	if (lua_type(L, idx) == LUA_TTABLE) {
-		lua_getfield(L, idx, "stdin");
-		if (! lua_isnil(L, -1)) {
-			luaT_popen_parse_stdX(L, -1, STDIN_FILENO,
-					      &opts->flags);
-		}
-		lua_pop(L, 1);
 
-		lua_getfield(L, idx, "stdout");
-		if (! lua_isnil(L, -1))
-			luaT_popen_parse_stdX(L, -1, STDOUT_FILENO,
-					      &opts->flags);
-		lua_pop(L, 1);
+#define setstdX(L, idx, stream, fileno) do {                        \
+	lua_getfield(L, idx, stream);                               \
+	if (!lua_isnil(L, -1))                                      \
+		luaT_popen_parse_stdX(L, -1, fileno, &opts->flags); \
+	lua_pop(L, 1);                                              \
+} while(0)
 
-		lua_getfield(L, idx, "stderr");
-		if (! lua_isnil(L, -1))
-			luaT_popen_parse_stdX(L, -1, STDERR_FILENO,
-					      &opts->flags);
-		lua_pop(L, 1);
+		setstdX(L, idx, "stdin", STDIN_FILENO);
+		setstdX(L, idx, "stdout", STDOUT_FILENO);
+		setstdX(L, idx, "stderr", STDERR_FILENO);
+
+#undef setstdX
 
 		lua_getfield(L, idx, "env");
 		if (! lua_isnil(L, -1)) {
@@ -896,84 +891,30 @@ luaT_popen_parse_opts(struct lua_State *L, int idx, struct popen_opts *opts,
 		}
 		lua_pop(L, 1);
 
-		lua_getfield(L, idx, "shell");
-		if (! lua_isnil(L, -1)) {
-			if (lua_type(L, -1) != LUA_TBOOLEAN)
-				luaT_popen_param_type_error(L, -1, "popen.new",
-							    "opts.shell",
-							    "boolean or nil");
-			if (lua_toboolean(L, -1) == 0)
-				opts->flags &= ~POPEN_FLAG_SHELL;
-			else
-				opts->flags |= POPEN_FLAG_SHELL;
-		}
-		lua_pop(L, 1);
-
-		lua_getfield(L, idx, "setsid");
-		if (! lua_isnil(L, -1)) {
-			if (lua_type(L, -1) != LUA_TBOOLEAN)
-				luaT_popen_param_type_error(L, -1, "popen.new",
-							    "opts.setsid",
-							    "boolean or nil");
-			if (lua_toboolean(L, -1) == 0)
-				opts->flags &= ~POPEN_FLAG_SETSID;
-			else
-				opts->flags |= POPEN_FLAG_SETSID;
-		}
-		lua_pop(L, 1);
-
-		lua_getfield(L, idx, "close_fds");
-		if (! lua_isnil(L, -1)) {
-			if (lua_type(L, -1) != LUA_TBOOLEAN)
-				luaT_popen_param_type_error(L, -1, "popen.new",
-							    "opts.close_fds",
-							    "boolean or nil");
-			if (lua_toboolean(L, -1) == 0)
-				opts->flags &= ~POPEN_FLAG_CLOSE_FDS;
-			else
-				opts->flags |= POPEN_FLAG_CLOSE_FDS;
-		}
-		lua_pop(L, 1);
-
-		lua_getfield(L, idx, "restore_signals");
-		if (! lua_isnil(L, -1)) {
-			if (lua_type(L, -1) != LUA_TBOOLEAN)
-				luaT_popen_param_type_error(
-					L, -1, "popen.new",
-					"opts.restore_signals",
-					"boolean or nil");
-			if (lua_toboolean(L, -1) == 0)
-				opts->flags &= ~POPEN_FLAG_RESTORE_SIGNALS;
-			else
-				opts->flags |= POPEN_FLAG_RESTORE_SIGNALS;
-		}
-		lua_pop(L, 1);
+#define setflag(L, idx, key, flag) do {                                 \
+	lua_getfield(L, idx, key);                                      \
+	if (!lua_isnil(L, -1)) {                                        \
+		if (lua_type(L, -1) != LUA_TBOOLEAN)                    \
+			luaT_popen_param_type_error(L, -1, "popen.new", \
+						    "opts." key,        \
+						    "boolean or nil");  \
+		if (lua_toboolean(L, -1) == 0)                          \
+			opts->flags &= ~flag;                           \
+		else                                                    \
+			opts->flags |= flag;                            \
+	}                                                               \
+	lua_pop(L, 1);                                                  \
+} while(0)
+
+		setflag(L, idx, "shell", POPEN_FLAG_SHELL);
+		setflag(L, idx, "setsid", POPEN_FLAG_SETSID);
+		setflag(L, idx, "close_fds", POPEN_FLAG_CLOSE_FDS);
+		setflag(L, idx, "restore_signals", POPEN_FLAG_RESTORE_SIGNALS);
+		setflag(L, idx, "group_signal", POPEN_FLAG_GROUP_SIGNAL);
+		setflag(L, idx, "keep_child", POPEN_FLAG_KEEP_CHILD);
+
+#undef setflag
 
-		lua_getfield(L, idx, "group_signal");
-		if (! lua_isnil(L, -1)) {
-			if (lua_type(L, -1) != LUA_TBOOLEAN)
-				luaT_popen_param_type_error(L, -1, "popen.new",
-							    "opts.group_signal",
-							    "boolean or nil");
-			if (lua_toboolean(L, -1) == 0)
-				opts->flags &= ~POPEN_FLAG_GROUP_SIGNAL;
-			else
-				opts->flags |= POPEN_FLAG_GROUP_SIGNAL;
-		}
-		lua_pop(L, 1);
-
-		lua_getfield(L, idx, "keep_child");
-		if (! lua_isnil(L, -1)) {
-			if (lua_type(L, -1) != LUA_TBOOLEAN)
-				luaT_popen_param_type_error(L, -1, "popen.new",
-							    "opts.keep_child",
-							    "boolean or nil");
-			if (lua_toboolean(L, -1) == 0)
-				opts->flags &= ~POPEN_FLAG_KEEP_CHILD;
-			else
-				opts->flags |= POPEN_FLAG_KEEP_CHILD;
-		}
-		lua_pop(L, 1);
 	}
 
 	return 0;

================================================================================

> +{
> +	/*
> +	 * Default flags: inherit std*, close other fds,
> +	 * restore signals.
> +	 */
> +	opts->flags = POPEN_FLAG_NONE		|
> +		POPEN_FLAG_CLOSE_FDS		|
> +		POPEN_FLAG_RESTORE_SIGNALS;
> +
> +	/* Parse options. */
> +	if (lua_type(L, idx) == LUA_TTABLE) {
> +		lua_getfield(L, idx, "stdin");
> +		if (! lua_isnil(L, -1)) {
> +			luaT_popen_parse_stdX(L, -1, STDIN_FILENO,
> +					      &opts->flags);
> +		}
> +		lua_pop(L, 1);
> +
> +		lua_getfield(L, idx, "stdout");
> +		if (! lua_isnil(L, -1))
> +			luaT_popen_parse_stdX(L, -1, STDOUT_FILENO,
> +					      &opts->flags);
> +		lua_pop(L, 1);
> +
> +		lua_getfield(L, idx, "stderr");
> +		if (! lua_isnil(L, -1))
> +			luaT_popen_parse_stdX(L, -1, STDERR_FILENO,
> +					      &opts->flags);
> +		lua_pop(L, 1);
> +
> +		lua_getfield(L, idx, "env");
> +		if (! lua_isnil(L, -1)) {
> +			opts->env = luaT_popen_parse_env(L, -1, region);
> +			if (opts->env == NULL)
> +				return -1;
> +		}
> +		lua_pop(L, 1);
> +
> +		lua_getfield(L, idx, "shell");
> +		if (! lua_isnil(L, -1)) {
> +			if (lua_type(L, -1) != LUA_TBOOLEAN)
> +				luaT_popen_param_type_error(L, -1, "popen.new",
> +							    "opts.shell",
> +							    "boolean or nil");
> +			if (lua_toboolean(L, -1) == 0)
> +				opts->flags &= ~POPEN_FLAG_SHELL;
> +			else
> +				opts->flags |= POPEN_FLAG_SHELL;
> +		}
> +		lua_pop(L, 1);
> +
> +		lua_getfield(L, idx, "setsid");
> +		if (! lua_isnil(L, -1)) {
> +			if (lua_type(L, -1) != LUA_TBOOLEAN)
> +				luaT_popen_param_type_error(L, -1, "popen.new",
> +							    "opts.setsid",
> +							    "boolean or nil");
> +			if (lua_toboolean(L, -1) == 0)
> +				opts->flags &= ~POPEN_FLAG_SETSID;
> +			else
> +				opts->flags |= POPEN_FLAG_SETSID;
> +		}
> +		lua_pop(L, 1);
> +
> +		lua_getfield(L, idx, "close_fds");
> +		if (! lua_isnil(L, -1)) {
> +			if (lua_type(L, -1) != LUA_TBOOLEAN)
> +				luaT_popen_param_type_error(L, -1, "popen.new",
> +							    "opts.close_fds",
> +							    "boolean or nil");
> +			if (lua_toboolean(L, -1) == 0)
> +				opts->flags &= ~POPEN_FLAG_CLOSE_FDS;
> +			else
> +				opts->flags |= POPEN_FLAG_CLOSE_FDS;
> +		}
> +		lua_pop(L, 1);
> +
> +		lua_getfield(L, idx, "restore_signals");
> +		if (! lua_isnil(L, -1)) {
> +			if (lua_type(L, -1) != LUA_TBOOLEAN)
> +				luaT_popen_param_type_error(
> +					L, -1, "popen.new",
> +					"opts.restore_signals",
> +					"boolean or nil");
> +			if (lua_toboolean(L, -1) == 0)
> +				opts->flags &= ~POPEN_FLAG_RESTORE_SIGNALS;
> +			else
> +				opts->flags |= POPEN_FLAG_RESTORE_SIGNALS;
> +		}
> +		lua_pop(L, 1);
> +
> +		lua_getfield(L, idx, "group_signal");
> +		if (! lua_isnil(L, -1)) {
> +			if (lua_type(L, -1) != LUA_TBOOLEAN)
> +				luaT_popen_param_type_error(L, -1, "popen.new",
> +							    "opts.group_signal",
> +							    "boolean or nil");
> +			if (lua_toboolean(L, -1) == 0)
> +				opts->flags &= ~POPEN_FLAG_GROUP_SIGNAL;
> +			else
> +				opts->flags |= POPEN_FLAG_GROUP_SIGNAL;
> +		}
> +		lua_pop(L, 1);
> +
> +		lua_getfield(L, idx, "keep_child");
> +		if (! lua_isnil(L, -1)) {
> +			if (lua_type(L, -1) != LUA_TBOOLEAN)
> +				luaT_popen_param_type_error(L, -1, "popen.new",
> +							    "opts.keep_child",
> +							    "boolean or nil");
> +			if (lua_toboolean(L, -1) == 0)
> +				opts->flags &= ~POPEN_FLAG_KEEP_CHILD;
> +			else
> +				opts->flags |= POPEN_FLAG_KEEP_CHILD;
> +		}
> +		lua_pop(L, 1);
> +	}
> +
> +	return 0;
> +}
> +
> +/**
> + * Parse popen.new() "argv" parameter.
> + *
> + * Prerequisite: opts->flags & POPEN_FLAG_SHELL should be
> + * the same in a call of this function and when paired
> + * popen_new() is invoked.
> + *
> + * Raise an error in case of the incorrect parameter.
> + *
> + * Return 0 at success. Sets opts->args and opts->nr_args.

Typo: s/args/argv/g.

Minor: argc looks to be a good complement to argv field.

> + * Allocates opts->argv on @a region (@see
> + * luaT_popen_parse_env() for details how to free it).
> + *
> + * Return -1 in case of an allocation error and set a diag
> + * (OutOfMemory).
> + */
> +static int
> +luaT_popen_parse_argv(struct lua_State *L, int idx, struct popen_opts *opts,
> +		      struct region *region)
> +{
> +	size_t region_svp = region_used(region);
> +	size_t argv_len = lua_objlen(L, idx);

There are no guarantees the table doesn't have gaps. Consider the
comment from lj_tab.c[1] and the following example:
| $ ./luajit
| LuaJIT 2.1.0-beta3 -- Copyright (C) 2005-2017 Mike Pall.
| http://luajit.org/
| JIT: ON SSE2 SSE3 SSE4.1 BMI2 fold cse dce fwd dse narrow loop abc sink
| fuse
| > print(#{ 'echo', 'QQ', nil, 'rm' })
| 4
Thereby the safe way to parse popen argv is iterating by numeric indices
until lua_rawgeti yields nil.

> +
> +	/* ["sh", "-c", ]..., NULL. */
> +	opts->nr_argv = argv_len + 1;
> +	if (opts->flags & POPEN_FLAG_SHELL)
> +		opts->nr_argv += 2;
> +
> +	size_t size = sizeof(char *) * opts->nr_argv;
> +	opts->argv = region_alloc(region, size);
> +	if (opts->argv == NULL) {
> +		diag_set(OutOfMemory, size, "region_alloc", "argv");
> +		return -1;
> +	}
> +
> +	const char **to = (const char **)opts->argv;
> +	if (opts->flags & POPEN_FLAG_SHELL) {
> +		opts->argv[0] = NULL;
> +		opts->argv[1] = NULL;

Minor: Please leave a comment why these slots are NULLs. I don't get it
from the given context.

> +		to += 2;
> +	}
> +
> +	for (size_t i = 0; i < argv_len; ++i) {
> +		lua_rawgeti(L, idx, i + 1);
> +		const char *arg = luaL_check_string_strict(L, -1, NULL);

Minor: OK, now I see. Please leave a comment here regarding the gap
check and benefits we have omitting dynamic reallocation, as we
discussed.

> +		if (arg == NULL) {
> +			region_truncate(region, region_svp);
> +			return luaT_popen_array_elem_type_error(
> +				L, -1, "popen.new", "argv", i + 1, "string");
> +		}
> +		*to++ = arg;
> +		lua_pop(L, 1);
> +	}
> +	*to++ = NULL;
> +	assert((const char **)opts->argv + opts->nr_argv == to);
> +
> +	return 0;
> +}
> +

<snipped>

> +
> +/* }}} */
> +
> +/* {{{ Lua API functions and methods */
> +

<snipped>

> +
> +/**
> + * Send SIGTERM signal to a child process.
> + *
> + * @param handle  a handle carries child process to terminate
> + *
> + * The function only sends SIGTERM signal and does NOT
> + * free any resources (popen handle memory and file
> + * descriptors).
> + *
> + * @see lbox_popen_signal() for errors and return values.
> + */
> +static int
> +lbox_popen_terminate(struct lua_State *L)
> +{
> +	struct popen_handle *handle;
> +	bool is_closed;
> +	if ((handle = luaT_check_popen_handle(L, 1, &is_closed)) == NULL) {
> +		diag_set(IllegalParams, "Bad params, use: ph:terminate()");
> +		return luaT_error(L);
> +	}
> +	if (is_closed)
> +		return luaT_popen_handle_closed_error(L);

Minor: In almost all cases you just throw an error due to popen handle
is closed right in a next line after obtaining the handle. I guess you
definitely need a routine for it.

> +
> +	int signo = SIGTERM;

Minor: This variable is excess considering its usage and function name.

> +
> +	if (popen_send_signal(handle, signo) != 0)
> +		return luaT_push_nil_and_error(L);
> +
> +	lua_pushboolean(L, true);
> +	return 1;
> +}
> +
> +/**
> + * Send SIGKILL signal to a child process.
> + *
> + * @param handle  a handle carries child process to kill
> + *
> + * The function only sends SIGKILL signal and does NOT
> + * free any resources (popen handle memory and file
> + * descriptors).
> + *
> + * @see lbox_popen_signal() for errors and return values.
> + */
> +static int
> +lbox_popen_kill(struct lua_State *L)
> +{
> +	struct popen_handle *handle;
> +	bool is_closed;
> +	if ((handle = luaT_check_popen_handle(L, 1, &is_closed)) == NULL) {
> +		diag_set(IllegalParams, "Bad params, use: ph:kill()");
> +		return luaT_error(L);
> +	}
> +	if (is_closed)
> +		return luaT_popen_handle_closed_error(L);
> +
> +	int signo = SIGKILL;

Minor: This variable is excess considering its usage and function name.

> +
> +	if (popen_send_signal(handle, signo) != 0)
> +		return luaT_push_nil_and_error(L);
> +
> +	lua_pushboolean(L, true);
> +	return 1;
> +}
> +

<snipped>

> +
> +/**
> + * Read data from a child peer.
> + *
> + * @param handle        handle of a child process
> + * @param opts          an options table
> + * @param opts.stdout   whether to read from stdout, boolean
> + *                      (default: true)
> + * @param opts.stderr   whether to read from stderr, boolean
> + *                      (default: false)
> + * @param opts.timeout  time quota in seconds
> + *                      (default: 100 years)
> + *
> + * Read data from stdout or stderr streams with @a timeout.
> + * By default it reads from stdout. Set @a opts.stderr to
> + * `true` to read from stderr.
> + *
> + * Raise an error on incorrect parameters or when the fiber is
> + * cancelled:
> + *
> + * - IllegalParams:    incorrect type or value of a parameter.
> + * - IllegalParams:    called on a closed handle.
> + * - IllegalParams:    opts.stdout and opts.stderr are set both
> + * - IllegalParams:    a requested IO operation is not supported
> + *                     by the handle (stdout / stderr is not
> + *                     piped).
> + * - IllegalParams:    attempt to operate on a closed file
> + *                     descriptor.
> + * - FiberIsCancelled: cancelled by an outside code.
> + *
> + * Return a string on success, an empty string at EOF.
> + *
> + * Return `nil, err` on a failure. Possible reasons:
> + *
> + * - SocketError: an IO error occurs at read().
> + * - TimedOut:    @a timeout quota is exceeded.
> + * - OutOfMemory: no memory space for a buffer to read into.
> + * - LuajitError: ("not enough memory"): no memory space for
> + *                the Lua string.
> + */
> +static int
> +lbox_popen_read(struct lua_State *L)
> +{
> +	struct popen_handle *handle;
> +	bool is_closed;
> +	unsigned int flags = POPEN_FLAG_NONE;
> +	ev_tstamp timeout = TIMEOUT_INFINITY;
> +	struct region *region = &fiber()->gc;
> +	size_t region_svp = region_used(region);
> +
> +	/* Extract handle. */
> +	if ((handle = luaT_check_popen_handle(L, 1, &is_closed)) == NULL)
> +		goto usage;
> +	if (is_closed)
> +		return luaT_popen_handle_closed_error(L);
> +
> +	/* Extract options. */
> +	if (!lua_isnoneornil(L, 2)) {
> +		if (lua_type(L, 2) != LUA_TTABLE)
> +			goto usage;
> +
> +		lua_getfield(L, 2, "stdout");
> +		if (!lua_isnil(L, -1)) {
> +			if (lua_type(L, -1) != LUA_TBOOLEAN)
> +				goto usage;
> +			if (lua_toboolean(L, -1) == 0)
> +				flags &= ~POPEN_FLAG_FD_STDOUT;
> +			else
> +				flags |= POPEN_FLAG_FD_STDOUT;
> +		}
> +		lua_pop(L, 1);
> +
> +		lua_getfield(L, 2, "stderr");
> +		if (!lua_isnil(L, -1)) {
> +			if (lua_type(L, -1) != LUA_TBOOLEAN)
> +				goto usage;
> +			if (lua_toboolean(L, -1) == 0)
> +				flags &= ~POPEN_FLAG_FD_STDERR;
> +			else
> +				flags |= POPEN_FLAG_FD_STDERR;
> +		}
> +		lua_pop(L, 1);

Minor: <stdout> and <stderr> options handling can be turned into a loop,
function or macro. Please choose the more convenient way to you.

> +
> +		lua_getfield(L, 2, "timeout");
> +		if (!lua_isnil(L, -1) &&
> +		    (timeout = luaT_check_timeout(L, -1)) < 0.0)
> +			goto usage;
> +		lua_pop(L, 1);

Minor: I see this passage only in two similar places, so I propose to
move everything to a timeout-related helper.

> +	}
> +
> +	/* Read from stdout by default. */
> +	if (!(flags & (POPEN_FLAG_FD_STDOUT | POPEN_FLAG_FD_STDERR)))
> +		flags |= POPEN_FLAG_FD_STDOUT;

Minor: Why not to hoist defaults initialization to drop this heavy
condition?

> +
> +	size_t size = POPEN_LUA_READ_BUF_SIZE;
> +	char *buf = region_alloc(region, size);
> +	if (buf == NULL) {
> +		diag_set(OutOfMemory, size, "region_alloc", "read buffer");
> +		return luaT_push_nil_and_error(L);
> +	}
> +
> +	static_assert(POPEN_LUA_READ_BUF_SIZE <= SSIZE_MAX,
> +		      "popen: read buffer is too big");
> +	ssize_t rc = popen_read_timeout(handle, buf, size, flags, timeout);
> +	if (rc < 0 || luaT_push_string_noxc(L, buf, rc) != 0)
> +		goto err;
> +	region_truncate(region, region_svp);
> +	return 1;
> +
> +usage:
> +	diag_set(IllegalParams, "Bad params, use: ph:read([{"
> +		 "stdout = <boolean>, "
> +		 "stderr = <boolean>, "
> +		 "timeout = <number>}])");
> +	return luaT_error(L);
> +err:
> +	region_truncate(region, region_svp);
> +	struct error *e = diag_last_error(diag_get());
> +	if (e->type == &type_IllegalParams ||
> +	    e->type == &type_FiberIsCancelled)
> +		return luaT_error(L);
> +	return luaT_push_nil_and_error(L);
> +}
> +
> +/**
> + * Write data to a child peer.
> + *
> + * @param handle        a handle of a child process
> + * @param str           a string to write
> + * @param opts          table of options
> + * @param opts.timeout  time quota in seconds
> + *                      (default: 100 years)
> + *
> + * Write string @a str to stdin stream of a child process.
> + *
> + * The function may yield forever if a child process does
> + * not read data from stdin and a pipe buffer becomes full.
> + * Size of this buffer depends on a platform. Use
> + * @a opts.timeout when unsure.
> + *
> + * When @a opts.timeout is not set, the function blocks
> + * (yields the fiber) until all data is written or an error
> + * happened.
> + *
> + * Raise an error on incorrect parameters or when the fiber is
> + * cancelled:
> + *
> + * - IllegalParams:    incorrect type or value of a parameter.
> + * - IllegalParams:    called on a closed handle.
> + * - IllegalParams:    string length is greater then SSIZE_MAX.
> + * - IllegalParams:    a requested IO operation is not supported
> + *                     by the handle (stdin is not piped).
> + * - IllegalParams:    attempt to operate on a closed file
> + *                     descriptor.
> + * - FiberIsCancelled: cancelled by an outside code.
> + *
> + * Return `true` on success.
> + *
> + * Return `nil, err` on a failure. Possible reasons:
> + *
> + * - SocketError: an IO error occurs at write().
> + * - TimedOut:    @a timeout quota is exceeded.
> + */
> +static int
> +lbox_popen_write(struct lua_State *L)
> +{
> +	struct popen_handle *handle;
> +	bool is_closed;
> +	const char *str;
> +	size_t len;
> +	ev_tstamp timeout = TIMEOUT_INFINITY;
> +
> +	/* Extract handle and string to write. */
> +	if ((handle = luaT_check_popen_handle(L, 1, &is_closed)) == NULL ||
> +	    (str = luaL_check_string_strict(L, 2, &len)) == NULL)
> +		goto usage;
> +	if (is_closed)
> +		return luaT_popen_handle_closed_error(L);
> +
> +	/* Extract options. */
> +	if (!lua_isnoneornil(L, 3)) {
> +		if (lua_type(L, 3) != LUA_TTABLE)
> +			goto usage;
> +
> +		lua_getfield(L, 3, "timeout");
> +		if (!lua_isnil(L, -1) &&
> +		    (timeout = luaT_check_timeout(L, -1)) < 0.0)
> +			goto usage;
> +		lua_pop(L, 1);
> +	}
> +
> +	unsigned int flags = POPEN_FLAG_FD_STDIN;

Minor: This variable is excess considering its usage.

> +	ssize_t rc = popen_write_timeout(handle, str, len, flags, timeout);
> +	assert(rc < 0 || rc == (ssize_t)len);
> +	if (rc < 0) {
> +		struct error *e = diag_last_error(diag_get());
> +		if (e->type == &type_IllegalParams ||
> +		    e->type == &type_FiberIsCancelled)
> +			return luaT_error(L);
> +		return luaT_push_nil_and_error(L);
> +	}
> +	lua_pushboolean(L, true);
> +	return 1;
> +
> +usage:
> +	diag_set(IllegalParams, "Bad params, use: ph:write(str[, {"
> +		 "timeout = <number>}])");
> +	return luaT_error(L);
> +}

<snipped>

> +
> +/**
> + * Get a field from a handle.
> + *
> + * @param handle  a handle of a child process
> + * @param key     a field name, string
> + *
> + * The function performs the following steps.
> + *
> + * Raise an error on incorrect parameters:
> + *
> + * - IllegalParams: incorrect type or value of a parameter.
> + *
> + * If there is a handle method with @a key name, return it.
> + *
> + * Raise an error on closed popen handle:
> + *
> + * - IllegalParams: called on a closed handle.
> + *
> + * If a @key is one of the following, return a value for it:
> + *
> + * - pid
> + * - command
> + * - opts
> + * - status
> + * - stdin
> + * - stdout
> + * - stderr
> + *
> + * @see lbox_popen_info() for description of those fields.
> + *
> + * Otherwise return `nil`.
> + */
> +static int
> +lbox_popen_index(struct lua_State *L)
> +{
> +	struct popen_handle *handle;
> +	bool is_closed;
> +	const char *key;
> +
> +	if ((handle = luaT_check_popen_handle(L, 1, &is_closed)) == NULL ||
> +	    (key = luaL_check_string_strict(L, 2, NULL)) == NULL) {
> +		diag_set(IllegalParams,
> +			 "Bad params, use __index(ph, <string>)");

Minor: "Bad params, use ph[<string>]" is more clear.

> +		return luaT_error(L);
> +	}
> +
> +	/* Get a value from the metatable. */
> +	lua_getmetatable(L, 1);
> +	lua_pushvalue(L, 2);
> +	lua_rawget(L, -2);
> +	if (! lua_isnil(L, -1))
> +		return 1;
> +
> +	if (is_closed) {
> +		diag_set(IllegalParams,
> +			 "Attempt to index a closed popen handle");
> +		return luaT_error(L);
> +	}

Minor: It's worth to leave a comment regarding __serialize method for
closed handle, since it's the only reason against hoisting this check
(like in all other methods).

> +
> +	if (strcmp(key, "pid") == 0) {
> +		if (handle->pid >= 0)
> +			lua_pushinteger(L, handle->pid);
> +		else
> +			lua_pushnil(L);
> +		return 1;
> +	}
> +
> +	if (strcmp(key, "command") == 0) {
> +		lua_pushstring(L, popen_command(handle));
> +		return 1;
> +	}
> +
> +	if (strcmp(key, "opts") == 0) {
> +		luaT_push_popen_opts(L, handle->flags);
> +		return 1;
> +	}
> +
> +	int state;
> +	int exit_code;
> +	popen_state(handle, &state, &exit_code);
> +	assert(state < POPEN_STATE_MAX);
> +	if (strcmp(key, "status") == 0)
> +		return luaT_push_popen_process_status(L, state, exit_code);
> +
> +	if (strcmp(key, "stdin") == 0)
> +		return luaT_push_popen_stdX_status(L, handle, STDIN_FILENO);
> +
> +	if (strcmp(key, "stdout") == 0)
> +		return luaT_push_popen_stdX_status(L, handle, STDOUT_FILENO);
> +
> +	if (strcmp(key, "stderr") == 0)
> +		return luaT_push_popen_stdX_status(L, handle, STDERR_FILENO);
> +
> +	lua_pushnil(L);
> +	return 1;
> +}
> +

<snipped>

> +
> +/* }}} */
> +
> +/* {{{ Module initialization */
> +

<snipped>

> +void
> +tarantool_lua_popen_init(struct lua_State *L)
> +{

<snipped>

> +
> +	/* Signals. */
> +	lua_newtable(L);
> +	for (int i = 0; popen_lua_signals[i].signame != NULL; ++i) {
> +		lua_pushinteger(L, popen_lua_signals[i].signo);
> +		lua_setfield(L, -2, popen_lua_signals[i].signame);
> +	}
> +	lua_setfield(L, -2, "signal");
> +
> +	/* Stream actions. */
> +	lua_newtable(L);
> +	for (int i = 0; popen_lua_actions[i].name != NULL; ++i) {
> +		lua_pushstring(L, popen_lua_actions[i].value);
> +		lua_setfield(L, -2, popen_lua_actions[i].name);
> +	}
> +	lua_setfield(L, -2, "opts");
> +
> +	/* Stream statuses. */
> +	lua_newtable(L);
> +	for (int i = 0; popen_lua_stream_statuses[i].name != NULL; ++i) {
> +		lua_pushstring(L, popen_lua_stream_statuses[i].value);
> +		lua_setfield(L, -2, popen_lua_stream_statuses[i].name);
> +	}
> +	lua_setfield(L, -2, "stream");
> +
> +	/* Process states. */
> +	lua_newtable(L);
> +	for (int i = 0; popen_lua_states[i].name != NULL; ++i) {
> +		lua_pushstring(L, popen_lua_states[i].value);
> +		lua_setfield(L, -2, popen_lua_states[i].name);
> +	}
> +	lua_setfield(L, -2, "state");

Minor: four similar loops above can be replaced with four macro calls.
Consider the following diff:

================================================================================

diff --git a/src/lua/popen.c b/src/lua/popen.c
index 40f4343eb..319ab3d77 100644
--- a/src/lua/popen.c
+++ b/src/lua/popen.c
@@ -2421,36 +2421,24 @@ tarantool_lua_popen_init(struct lua_State *L)
 	luaL_register_type(L, popen_handle_uname, popen_handle_methods);
 	luaL_register_type(L, popen_handle_closed_uname, popen_handle_methods);
 
+#define setconsts(L, namespace, consts, kfield, vtype, vfield) do { \
+	lua_newtable(L);                                            \
+	for (int i = 0; consts[i].kfield != NULL; ++i) {            \
+		lua_push ## vtype(L, consts[i].vfield);             \
+		lua_setfield(L, -2, consts[i].kfield);              \
+	}                                                           \
+	lua_setfield(L, -2, namespace);                             \
+} while(0)
+
 	/* Signals. */
-	lua_newtable(L);
-	for (int i = 0; popen_lua_signals[i].signame != NULL; ++i) {
-		lua_pushinteger(L, popen_lua_signals[i].signo);
-		lua_setfield(L, -2, popen_lua_signals[i].signame);
-	}
-	lua_setfield(L, -2, "signal");
-
+	setconsts(L, "signal", popen_lua_signals, signame, number, signo);
 	/* Stream actions. */
-	lua_newtable(L);
-	for (int i = 0; popen_lua_actions[i].name != NULL; ++i) {
-		lua_pushstring(L, popen_lua_actions[i].value);
-		lua_setfield(L, -2, popen_lua_actions[i].name);
-	}
-	lua_setfield(L, -2, "opts");
-
+	setconsts(L, "opts", popen_lua_actions, name, string, value);
 	/* Stream statuses. */
-	lua_newtable(L);
-	for (int i = 0; popen_lua_stream_statuses[i].name != NULL; ++i) {
-		lua_pushstring(L, popen_lua_stream_statuses[i].value);
-		lua_setfield(L, -2, popen_lua_stream_statuses[i].name);
-	}
-	lua_setfield(L, -2, "stream");
-
+	setconsts(L, "stream", popen_lua_stream_statuses, name, string, value);
 	/* Process states. */
-	lua_newtable(L);
-	for (int i = 0; popen_lua_states[i].name != NULL; ++i) {
-		lua_pushstring(L, popen_lua_states[i].value);
-		lua_setfield(L, -2, popen_lua_states[i].name);
-	}
-	lua_setfield(L, -2, "state");
+	setconsts(L, "state", popen_lua_states, name, string, value);
+
+#undef setconsts
 }
 

================================================================================

> +}
> +
> +/* }}} */
> diff --git a/src/lua/popen.h b/src/lua/popen.h
> new file mode 100644
> index 000000000..e23c5d7e5
> --- /dev/null
> +++ b/src/lua/popen.h
> @@ -0,0 +1,44 @@
> +#ifndef TARANTOOL_LUA_POPEN_H_INCLUDED
> +#define TARANTOOL_LUA_POPEN_H_INCLUDED

Minor: AFAIR, we decided to use pragma, but I see no word about it in
our C style guide[2].

<snipped>

> -- 
> 2.25.0
> 

[1]: https://github.com/tarantool/luajit/blob/tarantool/src/lj_tab.c#L694695
[2]: https://www.tarantool.io/en/doc/2.2/dev_guide/c_style_guide/

-- 
Best regards,
IM

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

* Re: [Tarantool-patches] [PATCH 2/2] popen: add popen Lua module
  2020-04-19 22:24     ` Alexander Turenko
@ 2020-04-20  1:21       ` Igor Munkin
  0 siblings, 0 replies; 13+ messages in thread
From: Igor Munkin @ 2020-04-20  1:21 UTC (permalink / raw)
  To: Alexander Turenko; +Cc: tarantool-patches

Sasha,

On 20.04.20, Alexander Turenko wrote:
> Thanks for the careful proofreading!
> 
> Aside of changes below I changed `popen.stream.OPENED (== 'opened')` to
> `popen.stream.OPEN (== 'open')` to match english grammar. Same for
> statuses -> status (yep, 'statuses' is allowed in English, but it is
> ugly).

Yep, that's the way much better.

> 
> Force-pushed Totktonada/gh-4031-popen-13-full-ci.
> 
> WBR, Alexander Turenko.
> 
> ----
> 

<snipped>

> 
> > > @param opts.env  an array of environment variables to
> > >                  be used inside a process;
> > >                  - when is not set then the current
> > >                    environment is inherited;
> > >                  - if set then the environment will be
> > >                    replaced
> > >                  - if set to an empty array then the
> > >                    environment will be dropped
> > 
> > Minor: I looks more convenient to sort the lines above the following
> > way:
> > * when is not set <...>
> > * if set to an empty table <...>
> > * if set to a non-empty table <...>
> 
> I don't mind.
> 
> Reordered in the code and the commit message.
> 
> Also I rephrased the description a bit to highlight the fact that the
> parameter is key-value mapping, not an array.

Good remark, thanks.

> 

<snipped>

> 
> > > The returned handle is subject to garbage collection, but
> > > it may be freed explicitly using :close(). SIGKILL is sent
> > > to the child process at :close() / GC if it is alive and
> > > opts.keep_child is not set.
> > 
> > Minor: Let's adjust the terminology. What do you mean speaking about
> > "handle"? If it's a GC object, then there is no way to release it
> > manually or "explicitly", only via platform GC. If it's a GC object
> > *payload* then it's not managed by Lua GC and need to be explicitly
> > released via special method or the corresponding __gc metamethod.
> 
> My intention is to give two ideas here:
> 
> * Feel free to use explicit :close() or lean on Lua GC.
> * :close() / GC handler kills the child process.
> 
> But being strict in terms and wording is goal for me too, so I reword
> the paragraph to eliminate phrases, which requires a GC object / a GC
> object payload as an object / a subject.
> 
> Changed in the code and the commit:
> 
> diff --git a/src/lua/popen.c b/src/lua/popen.c
> index ec8c95662..31b4bc2b0 100644
> --- a/src/lua/popen.c
> +++ b/src/lua/popen.c
> @@ -1184,10 +1184,11 @@ luaT_popen_parse_mode(struct lua_State *L, int idx)
>   *                              :close() or collecting of the
>   *                              handle by Lua GC
>   *
> - * The returned handle is subject to garbage collection, but
> - * it may be freed explicitly using :close(). SIGKILL is sent
> - * to the child process at :close() / GC if it is alive and
> - * opts.keep_child is not set.
> + * The returned handle provides :close() method to explicitly
> + * release all occupied resources (including the child process
> + * itself if @a opts.keep_child is not set). However if the
> + * method is not called for a handle during its lifetime, the
> + * same freeing actions will be triggered by Lua GC.
>   *
>   * It is recommended to use opts.setsid + opts.group_signal
>   * if a child process may spawn its own childs and they all

Nice, now it's clear, thanks!

> 

<snipped>

> 
> > > Return `nil, err` on a failure. Possible reasons:
> > > 
> > > - SocketError: an IO error occurs at read().
> > 
> > SocketError on pipe looks ridiculous to me. Maybe an IO one?
> 
> It comes from coio.cc. It would be good to refactor it someday, but not
> tonight :)

And there is only one thing we say to Refactoring: “not today” :)

> 

<snipped>

-- 
Best regards,
IM

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

* Re: [Tarantool-patches] [PATCH 2/2] popen: add popen Lua module
  2020-04-20  0:57   ` Igor Munkin
@ 2020-04-20  6:38     ` Alexander Turenko
  2020-04-20 11:57       ` Igor Munkin
  0 siblings, 1 reply; 13+ messages in thread
From: Alexander Turenko @ 2020-04-20  6:38 UTC (permalink / raw)
  To: Igor Munkin; +Cc: tarantool-patches

Thank you, Igor!

I made simple changes and postponed ones where I need a bit more time.
Hope, you don't mind.

Marked postponed questions as (postponed) in the email.

My answers are below.

WBR, Alexande Turenko.

> > +/* {{{ General-purpose Lua helpers */
> > +
> > +/**
> > + * Extract a string from the Lua stack.
> > + *
> > + * Return (const char *) if so, otherwise return NULL.
> > + *
> > + * Unlike luaL_checkstring() it accepts only a string and does not
> > + * accept a number.
> > + *
> > + * Unlike luaL_checkstring() it does not raise an error, but
> > + * returns NULL when a type is not what is excepted.
> > + */
> > +static const char *
> > +luaL_check_string_strict(struct lua_State *L, int idx, size_t *len_ptr)
> 
> Minor: I propose the following names: <luaL_tolstring_strict>,
> <luaL_checkstring_nothrow> or <luaL_checkstring_noxc>.

Thanks! luaL_tolstring_strict() is much better.

Changed. Adjusted the comment:

 /**
  * Extract a string from the Lua stack.
  *
- * Return (const char *) if so, otherwise return NULL.
+ * Return (const char *) for a string, otherwise return NULL.
  *
- * Unlike luaL_checkstring() it accepts only a string and does not
+ * Unlike luaL_tolstring() it accepts only a string and does not
  * accept a number.
- *
- * Unlike luaL_checkstring() it does not raise an error, but
- * returns NULL when a type is not what is excepted.
  */

> 
> Side note: Sorry, but I can't pass by it. Sasha, we both know each
> other's opinion regarding luaL_* prefix usage, and I'm not going to
> argue about it within this review. I'll try to live with it since it's
> a static one, but I see no reasons to not try to name it other way.

We are not decided anything certain, so I continue using past agreements
for now. It would be worse to try to use some new namespace and make a
wrong guess.

And, yep, those functions are static: we're free to change its names
when (if) we'll do it for all names across tarantool code.

> > +static int
> > +luaT_push_string_noxc(struct lua_State *L, char *str, size_t len)
> > +{
> > +	lua_pushcfunction(L, luaT_push_string_noxc_wrapper);
> > +	lua_pushlightuserdata(L, str);
> > +	lua_pushinteger(L, len);
> > +	return luaT_call(L, 2, 1);
> > +}
> 
> Minor: IMHO luaT_pushstring_* is more Lua-like naming.

It is from past agreements too: we discussed this with Vladimir D. and
agreed to split 'foo' and 'bar' in 'lua*_foo_bar_baz()', when there is
'baz'. When there is no 'baz', it is 'lua*_foobar()'.

> 
> > +
> > +/* }}} */
> > +
> > +/* {{{ Popen handle userdata manipulations */
> > +
> > +/**
> > + * Extract popen handle from the Lua stack.
> > + *
> > + * Return NULL in case of unexpected type.
> > + */
> > +static struct popen_handle *
> > +luaT_check_popen_handle(struct lua_State *L, int idx, bool *is_closed_ptr)
> > +{
> > +	struct popen_handle **handle_ptr =
> > +		luaL_testudata(L, idx, popen_handle_uname);
> > +	bool is_closed = false;
> > +
> > +	if (handle_ptr == NULL) {
> > +		handle_ptr = luaL_testudata(L, idx, popen_handle_closed_uname);
> > +		is_closed = true;
> > +	}
> > +
> > +	if (handle_ptr == NULL)
> > +		return NULL;
> 
> Minor: If NULL is returned, then is_closed_ptr payload is undefined. It
> would be nice to mention it in contract above.

It is unchanged, not undefined. I find it good style to left output
parameters intact when a function fails. However I don't mention it in
the comment, because I think that good code should not lean on this
assumption: there are many such functions that may clobber output
parameters at failure.

It seems it is somewhat similar to particular application of 'robustness
principle'.

> 
> > +	assert(*handle_ptr != NULL);
> > +
> > +	if (is_closed_ptr != NULL)
> 
> Minor: Considering function usage this check is excess.

I prefer to follow the same pattern for those functions: it just a line
of code.

> > +
> > +/**
> > + * Push popen options as a Lua table.
> > + *
> > + * Environment variables are not stored in a popen handle and so
> > + * missed here.
> > + */
> > +static int
> > +luaT_push_popen_opts(struct lua_State *L, unsigned int flags)
> > +{
> > +	lua_createtable(L, 0, 8);
> > +
> > +	luaT_push_popen_stdX_action(L, STDIN_FILENO, flags);
> > +	lua_setfield(L, -2, "stdin");
> > +
> > +	luaT_push_popen_stdX_action(L, STDOUT_FILENO, flags);
> > +	lua_setfield(L, -2, "stdout");
> > +
> > +	luaT_push_popen_stdX_action(L, STDERR_FILENO, flags);
> > +	lua_setfield(L, -2, "stderr");
> 
> Minor: This can be turned into a loop.

Don't sure it would be easier to read if would be written in this way.

However, yep, if I'll create some mapping for those options to reduce
code duplication in some other place, it'll make sense to loop over this
mapping here too.

Added the comment:

@@ -469,6 +469,20 @@ luaT_push_popen_opts(struct lua_State *L, unsigned int flags)
 {
        lua_createtable(L, 0, 8);
 
+       /*
+        * FIXME: Loop over a static array of stdX options.
+        *
+        * static const struct {
+        *      const char *option_name;
+        *      int fd;
+        * } popen_lua_stdX_options = {
+        *      {"stdin",       STDIN_FILENO    },
+        *      {"stdout",      STDOUT_FILENO   },
+        *      {"stderr",      STDERR_FILENO   },
+        *      {NULL,          -1              },
+        * };
+        */
+
        luaT_push_popen_stdX_action(L, STDIN_FILENO, flags);
        lua_setfield(L, -2, "stdin");

(postponed)

> 
> > +
> > +	/* env is skipped */
> > +
> > +	lua_pushboolean(L, (flags & POPEN_FLAG_SHELL) != 0);
> > +	lua_setfield(L, -2, "shell");
> > +
> > +	lua_pushboolean(L, (flags & POPEN_FLAG_SETSID) != 0);
> > +	lua_setfield(L, -2, "setsid");
> > +
> > +	lua_pushboolean(L, (flags & POPEN_FLAG_CLOSE_FDS) != 0);
> > +	lua_setfield(L, -2, "close_fds");
> > +
> > +	lua_pushboolean(L, (flags & POPEN_FLAG_RESTORE_SIGNALS) != 0);
> > +	lua_setfield(L, -2, "restore_signals");
> > +
> > +	lua_pushboolean(L, (flags & POPEN_FLAG_GROUP_SIGNAL) != 0);
> > +	lua_setfield(L, -2, "group_signal");
> > +
> > +	lua_pushboolean(L, (flags & POPEN_FLAG_KEEP_CHILD) != 0);
> > +	lua_setfield(L, -2, "keep_child");
> 
> Minor: This can be also turned into a loop.

Same as above.

If we'll introduce some mapping from/to boolean option names and flags
it will make sense.

Added the comment:

@@ -480,6 +494,8 @@ luaT_push_popen_opts(struct lua_State *L, unsigned int flags)
 
        /* env is skipped */
 
+       /* FIXME: Loop over a static array of boolean options. */
+
        lua_pushboolean(L, (flags & POPEN_FLAG_SHELL) != 0);
        lua_setfield(L, -2, "shell");

(postponed)

> > +/**
> > + * Parse popen.new() "opts" parameter.
> > + *
> > + * Prerequisite: @a opts should be zero filled.
> > + *
> > + * Result: @a opts structure is filled.
> > + *
> > + * Raise an error in case of the incorrect parameter.
> > + *
> > + * Return 0 at success. Allocates opts->env on @a region if
> > + * needed. @see luaT_popen_parse_env() for details how to
> > + * free it.
> > + *
> > + * Return -1 in case of an allocation error and set a diag
> > + * (OutOfMemory).
> > + */
> > +static int
> > +luaT_popen_parse_opts(struct lua_State *L, int idx, struct popen_opts *opts,
> > +		      struct region *region)
> 
> Minor: I rewrote it a bit, consider the diff below:
> 
> +#define setstdX(L, idx, stream, fileno) do {                        \
> +	lua_getfield(L, idx, stream);                               \
> +	if (!lua_isnil(L, -1))                                      \
> +		luaT_popen_parse_stdX(L, -1, fileno, &opts->flags); \
> +	lua_pop(L, 1);                                              \
> +} while(0)
> +		setstdX(L, idx, "stdin", STDIN_FILENO);
> +		setstdX(L, idx, "stdout", STDOUT_FILENO);
> +		setstdX(L, idx, "stderr", STDERR_FILENO);
> +
> +#undef setstdX
>  
> +#define setflag(L, idx, key, flag) do {                                 \
> +	lua_getfield(L, idx, key);                                      \
> +	if (!lua_isnil(L, -1)) {                                        \
> +		if (lua_type(L, -1) != LUA_TBOOLEAN)                    \
> +			luaT_popen_param_type_error(L, -1, "popen.new", \
> +						    "opts." key,        \
> +						    "boolean or nil");  \
> +		if (lua_toboolean(L, -1) == 0)                          \
> +			opts->flags &= ~flag;                           \
> +		else                                                    \
> +			opts->flags |= flag;                            \
> +	}                                                               \
> +	lua_pop(L, 1);                                                  \
> +} while(0)
> +
> +		setflag(L, idx, "shell", POPEN_FLAG_SHELL);
> +		setflag(L, idx, "setsid", POPEN_FLAG_SETSID);
> +		setflag(L, idx, "close_fds", POPEN_FLAG_CLOSE_FDS);
> +		setflag(L, idx, "restore_signals", POPEN_FLAG_RESTORE_SIGNALS);
> +		setflag(L, idx, "group_signal", POPEN_FLAG_GROUP_SIGNAL);
> +		setflag(L, idx, "keep_child", POPEN_FLAG_KEEP_CHILD);
> +
> +#undef setflag
>  
> ================================================================================

(Cut removed lines from your diff above.)

I see, yep, it is much smaller. I'll postpone actions for reducing code
duplication (to make them under less tight deadline).

I would try to avoid a macro: use a function or maybe list all those
boolean options in a static array and loop over them.

Anyway, I'll postpone deduplication (there are a couple of FIXMEs in the
code for this). Hope you don't mind.

Added the comments:

@@ -866,6 +882,11 @@ luaT_popen_parse_opts(struct lua_State *L, int idx, struct popen_opts *opts,
 
        /* Parse options. */
        if (lua_type(L, idx) == LUA_TTABLE) {
+               /*
+                * FIXME: Loop over a static array of stdX
+                * options.
+                */
+
                lua_getfield(L, idx, "stdin");
                if (! lua_isnil(L, -1)) {
                        luaT_popen_parse_stdX(L, -1, STDIN_FILENO,
@@ -893,6 +914,11 @@ luaT_popen_parse_opts(struct lua_State *L, int idx, struct popen_opts *opts,
                }
                lua_pop(L, 1);
 
+               /*
+                * FIXME: Loop over a static array of boolean
+                * options.
+                */
+
                lua_getfield(L, idx, "shell");
                if (! lua_isnil(L, -1)) {
                        if (lua_type(L, -1) != LUA_TBOOLEAN)

(postponed)

> 
> > +{
> > +	/*
> > +	 * Default flags: inherit std*, close other fds,
> > +	 * restore signals.
> > +	 */
> > +	opts->flags = POPEN_FLAG_NONE		|
> > +		POPEN_FLAG_CLOSE_FDS		|
> > +		POPEN_FLAG_RESTORE_SIGNALS;
> > +
> > +	/* Parse options. */
> > +	if (lua_type(L, idx) == LUA_TTABLE) {
> > +		lua_getfield(L, idx, "stdin");
> > +		if (! lua_isnil(L, -1)) {
> > +			luaT_popen_parse_stdX(L, -1, STDIN_FILENO,
> > +					      &opts->flags);
> > +		}
> > +		lua_pop(L, 1);
> > +
> > +		lua_getfield(L, idx, "stdout");
> > +		if (! lua_isnil(L, -1))
> > +			luaT_popen_parse_stdX(L, -1, STDOUT_FILENO,
> > +					      &opts->flags);
> > +		lua_pop(L, 1);
> > +
> > +		lua_getfield(L, idx, "stderr");
> > +		if (! lua_isnil(L, -1))
> > +			luaT_popen_parse_stdX(L, -1, STDERR_FILENO,
> > +					      &opts->flags);
> > +		lua_pop(L, 1);
> > +
> > +		lua_getfield(L, idx, "env");
> > +		if (! lua_isnil(L, -1)) {
> > +			opts->env = luaT_popen_parse_env(L, -1, region);
> > +			if (opts->env == NULL)
> > +				return -1;
> > +		}
> > +		lua_pop(L, 1);
> > +
> > +		lua_getfield(L, idx, "shell");
> > +		if (! lua_isnil(L, -1)) {
> > +			if (lua_type(L, -1) != LUA_TBOOLEAN)
> > +				luaT_popen_param_type_error(L, -1, "popen.new",
> > +							    "opts.shell",
> > +							    "boolean or nil");
> > +			if (lua_toboolean(L, -1) == 0)
> > +				opts->flags &= ~POPEN_FLAG_SHELL;
> > +			else
> > +				opts->flags |= POPEN_FLAG_SHELL;
> > +		}
> > +		lua_pop(L, 1);
> > +
> > +		lua_getfield(L, idx, "setsid");
> > +		if (! lua_isnil(L, -1)) {
> > +			if (lua_type(L, -1) != LUA_TBOOLEAN)
> > +				luaT_popen_param_type_error(L, -1, "popen.new",
> > +							    "opts.setsid",
> > +							    "boolean or nil");
> > +			if (lua_toboolean(L, -1) == 0)
> > +				opts->flags &= ~POPEN_FLAG_SETSID;
> > +			else
> > +				opts->flags |= POPEN_FLAG_SETSID;
> > +		}
> > +		lua_pop(L, 1);
> > +
> > +		lua_getfield(L, idx, "close_fds");
> > +		if (! lua_isnil(L, -1)) {
> > +			if (lua_type(L, -1) != LUA_TBOOLEAN)
> > +				luaT_popen_param_type_error(L, -1, "popen.new",
> > +							    "opts.close_fds",
> > +							    "boolean or nil");
> > +			if (lua_toboolean(L, -1) == 0)
> > +				opts->flags &= ~POPEN_FLAG_CLOSE_FDS;
> > +			else
> > +				opts->flags |= POPEN_FLAG_CLOSE_FDS;
> > +		}
> > +		lua_pop(L, 1);
> > +
> > +		lua_getfield(L, idx, "restore_signals");
> > +		if (! lua_isnil(L, -1)) {
> > +			if (lua_type(L, -1) != LUA_TBOOLEAN)
> > +				luaT_popen_param_type_error(
> > +					L, -1, "popen.new",
> > +					"opts.restore_signals",
> > +					"boolean or nil");
> > +			if (lua_toboolean(L, -1) == 0)
> > +				opts->flags &= ~POPEN_FLAG_RESTORE_SIGNALS;
> > +			else
> > +				opts->flags |= POPEN_FLAG_RESTORE_SIGNALS;
> > +		}
> > +		lua_pop(L, 1);
> > +
> > +		lua_getfield(L, idx, "group_signal");
> > +		if (! lua_isnil(L, -1)) {
> > +			if (lua_type(L, -1) != LUA_TBOOLEAN)
> > +				luaT_popen_param_type_error(L, -1, "popen.new",
> > +							    "opts.group_signal",
> > +							    "boolean or nil");
> > +			if (lua_toboolean(L, -1) == 0)
> > +				opts->flags &= ~POPEN_FLAG_GROUP_SIGNAL;
> > +			else
> > +				opts->flags |= POPEN_FLAG_GROUP_SIGNAL;
> > +		}
> > +		lua_pop(L, 1);
> > +
> > +		lua_getfield(L, idx, "keep_child");
> > +		if (! lua_isnil(L, -1)) {
> > +			if (lua_type(L, -1) != LUA_TBOOLEAN)
> > +				luaT_popen_param_type_error(L, -1, "popen.new",
> > +							    "opts.keep_child",
> > +							    "boolean or nil");
> > +			if (lua_toboolean(L, -1) == 0)
> > +				opts->flags &= ~POPEN_FLAG_KEEP_CHILD;
> > +			else
> > +				opts->flags |= POPEN_FLAG_KEEP_CHILD;
> > +		}
> > +		lua_pop(L, 1);
> > +	}
> > +
> > +	return 0;
> > +}
> > +
> > +/**
> > + * Parse popen.new() "argv" parameter.
> > + *
> > + * Prerequisite: opts->flags & POPEN_FLAG_SHELL should be
> > + * the same in a call of this function and when paired
> > + * popen_new() is invoked.
> > + *
> > + * Raise an error in case of the incorrect parameter.
> > + *
> > + * Return 0 at success. Sets opts->args and opts->nr_args.
> 
> Typo: s/args/argv/g.

Thanks! Fixed.

> 
> Minor: argc looks to be a good complement to argv field.

'struct popen_opts' is from backend. I tring to be conservative in
changes of everything that is already in master.

> 
> > + * Allocates opts->argv on @a region (@see
> > + * luaT_popen_parse_env() for details how to free it).
> > + *
> > + * Return -1 in case of an allocation error and set a diag
> > + * (OutOfMemory).
> > + */
> > +static int
> > +luaT_popen_parse_argv(struct lua_State *L, int idx, struct popen_opts *opts,
> > +		      struct region *region)
> > +{
> > +	size_t region_svp = region_used(region);
> > +	size_t argv_len = lua_objlen(L, idx);
> 
> There are no guarantees the table doesn't have gaps. Consider the
> comment from lj_tab.c[1] and the following example:
> | $ ./luajit
> | LuaJIT 2.1.0-beta3 -- Copyright (C) 2005-2017 Mike Pall.
> | http://luajit.org/
> | JIT: ON SSE2 SSE3 SSE4.1 BMI2 fold cse dce fwd dse narrow loop abc sink
> | fuse
> | > print(#{ 'echo', 'QQ', nil, 'rm' })
> | 4
> Thereby the safe way to parse popen argv is iterating by numeric indices
> until lua_rawgeti yields nil.

Added the comment:

@@ -1023,6 +1023,15 @@ luaT_popen_parse_argv(struct lua_State *L, int idx, struct popen_opts *opts,
                      struct region *region)
 {
        size_t region_svp = region_used(region);
+
+       /*
+        * We need to know exact size of the array to allocate
+        * a memory for opts->argv without reallocations.
+        *
+        * lua_objlen() does not guarantee that there are no
+        * holes in the array, but we check it within a loop
+        * later.
+        */
        size_t argv_len = lua_objlen(L, idx);
 
        /* ["sh", "-c", ]..., NULL. */

> 
> > +
> > +	/* ["sh", "-c", ]..., NULL. */
> > +	opts->nr_argv = argv_len + 1;
> > +	if (opts->flags & POPEN_FLAG_SHELL)
> > +		opts->nr_argv += 2;
> > +
> > +	size_t size = sizeof(char *) * opts->nr_argv;
> > +	opts->argv = region_alloc(region, size);
> > +	if (opts->argv == NULL) {
> > +		diag_set(OutOfMemory, size, "region_alloc", "argv");
> > +		return -1;
> > +	}
> > +
> > +	const char **to = (const char **)opts->argv;
> > +	if (opts->flags & POPEN_FLAG_SHELL) {
> > +		opts->argv[0] = NULL;
> > +		opts->argv[1] = NULL;
> 
> Minor: Please leave a comment why these slots are NULLs. I don't get it
> from the given context.

Okay.

@@ -1046,6 +1046,7 @@ luaT_popen_parse_argv(struct lua_State *L, int idx, struct popen_opts *opts,
                return -1;
        }

+       /* Keep place for "sh", "-c" as popen_new() expects. */
        const char **to = (const char **)opts->argv;
        if (opts->flags & POPEN_FLAG_SHELL) {
                opts->argv[0] = NULL;

> 
> > +		to += 2;
> > +	}
> > +
> > +	for (size_t i = 0; i < argv_len; ++i) {
> > +		lua_rawgeti(L, idx, i + 1);
> > +		const char *arg = luaL_check_string_strict(L, -1, NULL);
> 
> Minor: OK, now I see. Please leave a comment here regarding the gap
> check and benefits we have omitting dynamic reallocation, as we
> discussed.

I said 'without reallocations' (see above) and I guess it is enough.

> > +static int
> > +lbox_popen_terminate(struct lua_State *L)
> > +{
> > +	struct popen_handle *handle;
> > +	bool is_closed;
> > +	if ((handle = luaT_check_popen_handle(L, 1, &is_closed)) == NULL) {
> > +		diag_set(IllegalParams, "Bad params, use: ph:terminate()");
> > +		return luaT_error(L);
> > +	}
> > +	if (is_closed)
> > +		return luaT_popen_handle_closed_error(L);
> 
> Minor: In almost all cases you just throw an error due to popen handle
> is closed right in a next line after obtaining the handle. I guess you
> definitely need a routine for it.

Added the comment:

@@ -1533,6 +1533,11 @@ lbox_popen_shell(struct lua_State *L)
 static int
 lbox_popen_signal(struct lua_State *L)
 {
+       /*
+        * FIXME: Extracting a handle and raising an error when
+        * it is closed is repeating pattern within the file. It
+        * worth to extract it to a function.
+        */
        struct popen_handle *handle;
        bool is_closed;
        if ((handle = luaT_check_popen_handle(L, 1, &is_closed)) == NULL ||

(postponed)

> 
> > +
> > +	int signo = SIGTERM;
> 
> Minor: This variable is excess considering its usage and function name.

This way the code for :signal(), :kill() and :terminate() looks more
similar and I like this property.

> > +static int
> > +lbox_popen_kill(struct lua_State *L)
> > +{
> > +	struct popen_handle *handle;
> > +	bool is_closed;
> > +	if ((handle = luaT_check_popen_handle(L, 1, &is_closed)) == NULL) {
> > +		diag_set(IllegalParams, "Bad params, use: ph:kill()");
> > +		return luaT_error(L);
> > +	}
> > +	if (is_closed)
> > +		return luaT_popen_handle_closed_error(L);
> > +
> > +	int signo = SIGKILL;
> 
> Minor: This variable is excess considering its usage and function name.

Same.

> > +		lua_getfield(L, 2, "stderr");
> > +		if (!lua_isnil(L, -1)) {
> > +			if (lua_type(L, -1) != LUA_TBOOLEAN)
> > +				goto usage;
> > +			if (lua_toboolean(L, -1) == 0)
> > +				flags &= ~POPEN_FLAG_FD_STDERR;
> > +			else
> > +				flags |= POPEN_FLAG_FD_STDERR;
> > +		}
> > +		lua_pop(L, 1);
> 
> Minor: <stdout> and <stderr> options handling can be turned into a loop,
> function or macro. Please choose the more convenient way to you.

Added the comment:

@@ -1731,6 +1731,8 @@ lbox_popen_read(struct lua_State *L)
                if (lua_type(L, 2) != LUA_TTABLE)
                        goto usage;
 
+               /* FIXME: Shorten boolean options parsing. */
+
                lua_getfield(L, 2, "stdout");
                if (!lua_isnil(L, -1)) {
                        if (lua_type(L, -1) != LUA_TBOOLEAN)

> > +
> > +		lua_getfield(L, 2, "timeout");
> > +		if (!lua_isnil(L, -1) &&
> > +		    (timeout = luaT_check_timeout(L, -1)) < 0.0)
> > +			goto usage;
> > +		lua_pop(L, 1);
> 
> Minor: I see this passage only in two similar places, so I propose to
> move everything to a timeout-related helper.

luaT_check_timeout() is already this helper. Don't sure it worth to add
one more wrapping level.

> 
> > +	}
> > +
> > +	/* Read from stdout by default. */
> > +	if (!(flags & (POPEN_FLAG_FD_STDOUT | POPEN_FLAG_FD_STDERR)))
> > +		flags |= POPEN_FLAG_FD_STDOUT;
> 
> Minor: Why not to hoist defaults initialization to drop this heavy
> condition?

Added the comment:

@@ -1715,7 +1715,13 @@ lbox_popen_read(struct lua_State *L)
 {
        struct popen_handle *handle;
        bool is_closed;
+
+       /*
+        * Actual default is POPEN_FLAG_FD_STDOUT, but
+        * it is set only when no std* option is passed.
+        */
        unsigned int flags = POPEN_FLAG_NONE;
+
        ev_tstamp timeout = TIMEOUT_INFINITY;
        struct region *region = &fiber()->gc;
        size_t region_svp = region_used(region);

> > +	/* Extract options. */
> > +	if (!lua_isnoneornil(L, 3)) {
> > +		if (lua_type(L, 3) != LUA_TTABLE)
> > +			goto usage;
> > +
> > +		lua_getfield(L, 3, "timeout");
> > +		if (!lua_isnil(L, -1) &&
> > +		    (timeout = luaT_check_timeout(L, -1)) < 0.0)
> > +			goto usage;
> > +		lua_pop(L, 1);
> > +	}
> > +
> > +	unsigned int flags = POPEN_FLAG_FD_STDIN;
> 
> Minor: This variable is excess considering its usage.

I like this way to make calls look similar: popen_{read,write}_timeout()
in the case. It is also used in the backend, see popen_write_timeout().

In fact I borrow the approach from the popen backend code, because found
it useful.

> > +static int
> > +lbox_popen_index(struct lua_State *L)
> > +{
> > +	struct popen_handle *handle;
> > +	bool is_closed;
> > +	const char *key;
> > +
> > +	if ((handle = luaT_check_popen_handle(L, 1, &is_closed)) == NULL ||
> > +	    (key = luaL_check_string_strict(L, 2, NULL)) == NULL) {
> > +		diag_set(IllegalParams,
> > +			 "Bad params, use __index(ph, <string>)");
> 
> Minor: "Bad params, use ph[<string>]" is more clear.

I would even suggest ph.<field>, but it is not possible to receive this
error when a property is acquired this way. When a user got this error
it likely doing something with the metamethod itself and it looks logical
to show its "name" ("__index") explicitly. However ph[1] is possible too.

Don't sure here, so I'll left it intact for now. At least now it is
consistent with __serialize.

> 
> > +		return luaT_error(L);
> > +	}
> > +
> > +	/* Get a value from the metatable. */
> > +	lua_getmetatable(L, 1);
> > +	lua_pushvalue(L, 2);
> > +	lua_rawget(L, -2);
> > +	if (! lua_isnil(L, -1))
> > +		return 1;
> > +
> > +	if (is_closed) {
> > +		diag_set(IllegalParams,
> > +			 "Attempt to index a closed popen handle");
> > +		return luaT_error(L);
> > +	}
> 
> Minor: It's worth to leave a comment regarding __serialize method for
> closed handle, since it's the only reason against hoisting this check
> (like in all other methods).

I also prefer to keep arguments verification within methods be 'fair'.
It is better to wrap open/closed check to write it shorter, then move it
here, IMHO.

The __index metamethod does not look for me as the point, where all
common validation should occur.

Added the comment:

@@ -2312,13 +2312,24 @@ lbox_popen_index(struct lua_State *L)
                return luaT_error(L);
        }

-       /* Get a value from the metatable. */
+       /*
+        * If `key` is a method name, return it.
+        *
+        * The __index metamethod performs only checks that
+        * it needs on its own. Despite there are common parts
+        * across methods, it is better to validate all
+        * parameters within a method itself.
+        *
+        * In particular, methods should perform a check for
+        * closed handles.
+        */
        lua_getmetatable(L, 1);
        lua_pushvalue(L, 2);
        lua_rawget(L, -2);
        if (! lua_isnil(L, -1))
                return 1;

+       /* Does not allow to get a field from a closed handle. */
        if (is_closed) {
                diag_set(IllegalParams,
                         "Attempt to index a closed popen handle");

> > +	/* Signals. */
> > +	lua_newtable(L);
> > +	for (int i = 0; popen_lua_signals[i].signame != NULL; ++i) {
> > +		lua_pushinteger(L, popen_lua_signals[i].signo);
> > +		lua_setfield(L, -2, popen_lua_signals[i].signame);
> > +	}
> > +	lua_setfield(L, -2, "signal");
> > +
> > +	/* Stream actions. */
> > +	lua_newtable(L);
> > +	for (int i = 0; popen_lua_actions[i].name != NULL; ++i) {
> > +		lua_pushstring(L, popen_lua_actions[i].value);
> > +		lua_setfield(L, -2, popen_lua_actions[i].name);
> > +	}
> > +	lua_setfield(L, -2, "opts");
> > +
> > +	/* Stream statuses. */
> > +	lua_newtable(L);
> > +	for (int i = 0; popen_lua_stream_statuses[i].name != NULL; ++i) {
> > +		lua_pushstring(L, popen_lua_stream_statuses[i].value);
> > +		lua_setfield(L, -2, popen_lua_stream_statuses[i].name);
> > +	}
> > +	lua_setfield(L, -2, "stream");
> > +
> > +	/* Process states. */
> > +	lua_newtable(L);
> > +	for (int i = 0; popen_lua_states[i].name != NULL; ++i) {
> > +		lua_pushstring(L, popen_lua_states[i].value);
> > +		lua_setfield(L, -2, popen_lua_states[i].name);
> > +	}
> > +	lua_setfield(L, -2, "state");
> 
> Minor: four similar loops above can be replaced with four macro calls.
> Consider the following diff:
> 
> ================================================================================
> 
> diff --git a/src/lua/popen.c b/src/lua/popen.c
> index 40f4343eb..319ab3d77 100644
> --- a/src/lua/popen.c
> +++ b/src/lua/popen.c
> @@ -2421,36 +2421,24 @@ tarantool_lua_popen_init(struct lua_State *L)
>  	luaL_register_type(L, popen_handle_uname, popen_handle_methods);
>  	luaL_register_type(L, popen_handle_closed_uname, popen_handle_methods);
>  
> +#define setconsts(L, namespace, consts, kfield, vtype, vfield) do { \
> +	lua_newtable(L);                                            \
> +	for (int i = 0; consts[i].kfield != NULL; ++i) {            \
> +		lua_push ## vtype(L, consts[i].vfield);             \
> +		lua_setfield(L, -2, consts[i].kfield);              \
> +	}                                                           \
> +	lua_setfield(L, -2, namespace);                             \
> +} while(0)
> +
>  	/* Signals. */
> -	lua_newtable(L);
> -	for (int i = 0; popen_lua_signals[i].signame != NULL; ++i) {
> -		lua_pushinteger(L, popen_lua_signals[i].signo);
> -		lua_setfield(L, -2, popen_lua_signals[i].signame);
> -	}
> -	lua_setfield(L, -2, "signal");
> -
> +	setconsts(L, "signal", popen_lua_signals, signame, number, signo);
>  	/* Stream actions. */
> -	lua_newtable(L);
> -	for (int i = 0; popen_lua_actions[i].name != NULL; ++i) {
> -		lua_pushstring(L, popen_lua_actions[i].value);
> -		lua_setfield(L, -2, popen_lua_actions[i].name);
> -	}
> -	lua_setfield(L, -2, "opts");
> -
> +	setconsts(L, "opts", popen_lua_actions, name, string, value);
>  	/* Stream statuses. */
> -	lua_newtable(L);
> -	for (int i = 0; popen_lua_stream_statuses[i].name != NULL; ++i) {
> -		lua_pushstring(L, popen_lua_stream_statuses[i].value);
> -		lua_setfield(L, -2, popen_lua_stream_statuses[i].name);
> -	}
> -	lua_setfield(L, -2, "stream");
> -
> +	setconsts(L, "stream", popen_lua_stream_statuses, name, string, value);
>  	/* Process states. */
> -	lua_newtable(L);
> -	for (int i = 0; popen_lua_states[i].name != NULL; ++i) {
> -		lua_pushstring(L, popen_lua_states[i].value);
> -		lua_setfield(L, -2, popen_lua_states[i].name);
> -	}
> -	lua_setfield(L, -2, "state");
> +	setconsts(L, "state", popen_lua_states, name, string, value);
> +
> +#undef setconsts
>  }

You know, I'm very suspectful for macros 'to safe typing'. Whether it'll
ease reading? Don't sure.

If you don't mind, I'll think about this later.

(postponed)

> > diff --git a/src/lua/popen.h b/src/lua/popen.h
> > new file mode 100644
> > index 000000000..e23c5d7e5
> > --- /dev/null
> > +++ b/src/lua/popen.h
> > @@ -0,0 +1,44 @@
> > +#ifndef TARANTOOL_LUA_POPEN_H_INCLUDED
> > +#define TARANTOOL_LUA_POPEN_H_INCLUDED
> 
> Minor: AFAIR, we decided to use pragma, but I see no word about it in
> our C style guide[2].

There were some discussions, but:

* Nothing was changed in the guide.
* Other code was not changed to fit this style.

Since personally I don't like 'pragma once' and the style document says
'Use header guards' I prefer to keep current way for the new code.

> [1]: https://github.com/tarantool/luajit/blob/tarantool/src/lj_tab.c#L694695
> [2]: https://www.tarantool.io/en/doc/2.2/dev_guide/c_style_guide/

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

* Re: [Tarantool-patches] [PATCH 0/2] Popen Lua module
  2020-04-18  4:13 [Tarantool-patches] [PATCH 0/2] Popen Lua module Alexander Turenko
  2020-04-18  4:13 ` [Tarantool-patches] [PATCH 1/2] popen: always free resources in popen_delete() Alexander Turenko
  2020-04-18  4:13 ` [Tarantool-patches] [PATCH 2/2] popen: add popen Lua module Alexander Turenko
@ 2020-04-20  7:52 ` Kirill Yukhin
  2 siblings, 0 replies; 13+ messages in thread
From: Kirill Yukhin @ 2020-04-20  7:52 UTC (permalink / raw)
  To: Alexander Turenko; +Cc: tarantool-patches

Hello,

On 18 апр 07:13, Alexander Turenko wrote:
> The patchset implements popen Lua module upward existing backend popen
> engine.
> 
> The first patch changes popen_delete() behaviour to always free
> resources even when killing a process (or a process group) fails. We
> (Alexander, Cyrill and Igor) revisited the contract for closing the
> handle after observing killpg() behaviour on a group of zombie processes
> on Mac OS. I added those details to API documentation comments to don't
> surprise a user with this.
> 
> The first patch continues previous preliminary popen engine series:
> 
> https://lists.tarantool.org/pipermail/tarantool-patches/2020-April/015609.html
> https://lists.tarantool.org/pipermail/tarantool-patches/2020-April/015888.html
> 
> The second patch implements the Lua API for popen. It is quite
> strightforward and should be considered as very basic implementation. We
> plan to enhance it further in the upcoming releases (issues are to be
> filed).
> 
> The main goal of the module it to provide popen implementation that is
> integrated into our event loop (similar to fio and socket modules). Side
> goal is to provide ability to feed data to a child process input and
> read data from its output and so work with streaming programs (like
> `grep`) and even interactive programs (like `python -i` interpreter).
> 
> Let's consider the API of module as beta: it may be change in
> backward-incompatible manner in future releases if it will be valuable
> enough.
> 
> It seems the main application of the module it to write various testing
> code and so the API should be extended in the future with convenient
> shortcuts: developers usually don't very like writting tests and it
> should be at least more-or-less convenient.
> 
> I plan to extend the API for reading with ability to read certain amount
> of bytes, to read line-by-line (or based on other delimiter), to read
> into a buffer (ibuf): like it is implemented in the socket module. I
> plan to share an RFC document about read streams.
> 
> ph:wait() should gain ability to set a timeout and should be rewritten
> to use triggers / fiber conds instead of check-yield-again loop.
> 
> ----
> 
> https://github.com/tarantool/tarantool/issues/4031
> https://github.com/tarantool/tarantool/tree/Totktonada/gh-4031-popen-13-full-ci

I've checked your patchset into master.

--
Regards, Kirill Yukhin

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

* Re: [Tarantool-patches] [PATCH 2/2] popen: add popen Lua module
  2020-04-20  6:38     ` Alexander Turenko
@ 2020-04-20 11:57       ` Igor Munkin
  2020-04-21 13:38         ` Alexander Turenko
  0 siblings, 1 reply; 13+ messages in thread
From: Igor Munkin @ 2020-04-20 11:57 UTC (permalink / raw)
  To: Alexander Turenko; +Cc: tarantool-patches

Sasha,

On 20.04.20, Alexander Turenko wrote:
> Thank you, Igor!
> 
> I made simple changes and postponed ones where I need a bit more time.
> Hope, you don't mind.

Totally OK with it.

> 
> Marked postponed questions as (postponed) in the email.
> 
> My answers are below.
> 
> WBR, Alexande Turenko.
> 

<snipped>

> 
> > > +static int
> > > +luaT_push_string_noxc(struct lua_State *L, char *str, size_t len)
> > > +{
> > > +	lua_pushcfunction(L, luaT_push_string_noxc_wrapper);
> > > +	lua_pushlightuserdata(L, str);
> > > +	lua_pushinteger(L, len);
> > > +	return luaT_call(L, 2, 1);
> > > +}
> > 
> > Minor: IMHO luaT_pushstring_* is more Lua-like naming.
> 
> It is from past agreements too: we discussed this with Vladimir D. and
> agreed to split 'foo' and 'bar' in 'lua*_foo_bar_baz()', when there is
> 'baz'. When there is no 'baz', it is 'lua*_foobar()'.

Are there any docs/guides/notes as a result of this discussion?

> 

<snipped>

> 
> > > +
> > > +		lua_getfield(L, 2, "timeout");
> > > +		if (!lua_isnil(L, -1) &&
> > > +		    (timeout = luaT_check_timeout(L, -1)) < 0.0)
> > > +			goto usage;
> > > +		lua_pop(L, 1);
> > 
> > Minor: I see this passage only in two similar places, so I propose to
> > move everything to a timeout-related helper.
> 
> luaT_check_timeout() is already this helper. Don't sure it worth to add
> one more wrapping level.

I mean you can also move there all table manipulations and checks.

> 

<snipped>

-- 
Best regards,
IM

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

* Re: [Tarantool-patches] [PATCH 2/2] popen: add popen Lua module
  2020-04-20 11:57       ` Igor Munkin
@ 2020-04-21 13:38         ` Alexander Turenko
  0 siblings, 0 replies; 13+ messages in thread
From: Alexander Turenko @ 2020-04-21 13:38 UTC (permalink / raw)
  To: Igor Munkin; +Cc: tarantool-patches

> > > > +static int
> > > > +luaT_push_string_noxc(struct lua_State *L, char *str, size_t len)
> > > > +{
> > > > +	lua_pushcfunction(L, luaT_push_string_noxc_wrapper);
> > > > +	lua_pushlightuserdata(L, str);
> > > > +	lua_pushinteger(L, len);
> > > > +	return luaT_call(L, 2, 1);
> > > > +}
> > > 
> > > Minor: IMHO luaT_pushstring_* is more Lua-like naming.
> > 
> > It is from past agreements too: we discussed this with Vladimir D. and
> > agreed to split 'foo' and 'bar' in 'lua*_foo_bar_baz()', when there is
> > 'baz'. When there is no 'baz', it is 'lua*_foobar()'.
> 
> Are there any docs/guides/notes as a result of this discussion?

Nope.

> > > > +
> > > > +		lua_getfield(L, 2, "timeout");
> > > > +		if (!lua_isnil(L, -1) &&
> > > > +		    (timeout = luaT_check_timeout(L, -1)) < 0.0)
> > > > +			goto usage;
> > > > +		lua_pop(L, 1);
> > > 
> > > Minor: I see this passage only in two similar places, so I propose to
> > > move everything to a timeout-related helper.
> > 
> > luaT_check_timeout() is already this helper. Don't sure it worth to add
> > one more wrapping level.
> 
> I mean you can also move there all table manipulations and checks.

But fail branch do a function specific logic. I don't sure here. It
seems we can pass an error string to the function.

However lua*_check_foo() pattern is used across tarantool (see
luaL_checkibuf() for example) and timeout helper may become common
sooner or later.

WBR, Alexander Turenko.

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

end of thread, other threads:[~2020-04-21 13:38 UTC | newest]

Thread overview: 13+ messages (download: mbox.gz / follow: Atom feed)
-- links below jump to the message on this page --
2020-04-18  4:13 [Tarantool-patches] [PATCH 0/2] Popen Lua module Alexander Turenko
2020-04-18  4:13 ` [Tarantool-patches] [PATCH 1/2] popen: always free resources in popen_delete() Alexander Turenko
2020-04-18  6:55   ` Cyrill Gorcunov
2020-04-18  4:13 ` [Tarantool-patches] [PATCH 2/2] popen: add popen Lua module Alexander Turenko
2020-04-18  6:57   ` Cyrill Gorcunov
2020-04-19 12:38   ` Igor Munkin
2020-04-19 22:24     ` Alexander Turenko
2020-04-20  1:21       ` Igor Munkin
2020-04-20  0:57   ` Igor Munkin
2020-04-20  6:38     ` Alexander Turenko
2020-04-20 11:57       ` Igor Munkin
2020-04-21 13:38         ` Alexander Turenko
2020-04-20  7:52 ` [Tarantool-patches] [PATCH 0/2] Popen " Kirill Yukhin

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