/****************************************************************************
**
**  This file is part of GAP, a system for computational discrete algebra.
**
**  Copyright of GAP belongs to its developers, whose names are too numerous
**  to list here. Please refer to the COPYRIGHT file for details.
**
**  SPDX-License-Identifier: GPL-2.0-or-later
**
**
**  This file will contains the functions for communicating with other
**  processes via ptys and sockets
**
**  The eventual intent is that there will be InputOutputStreams at the GAP
**  level with some API to be defined, and two ways of creating them. One is
**  like Process except that the external process is left running, the other
**  connects as client to a specified socket.
**
**  At this level, we provide the two interfaces separately. For each we have
**  an integer identifier for each open connection, and creation, read and
**  write functions, and possibly some sort of probe function.
**
*/

#include "iostream.h"

#include "bool.h"
#include "error.h"
#include "integer.h"
#include "io.h"
#include "lists.h"
#include "modules.h"
#include "stringobj.h"
#include "sysenv.h"
#include "sysfiles.h"
#include "sysopt.h"

#include "hpc/thread.h"

#include "config.h"

#ifndef GAP_DISABLE_SUBPROCESS_CODE

#include <errno.h>
#include <fcntl.h>
#include <signal.h>
#include <termios.h>
#include <unistd.h>

#ifdef HAVE_SYS_WAIT_H
#include <sys/wait.h>
#endif

#ifdef HAVE_OPENPTY
  #if defined(HAVE_UTIL_H)
    #include <util.h>     // for openpty() on macOS, OpenBSD and NetBSD
  #elif defined(HAVE_LIBUTIL_H)
    #include <libutil.h>  // for openpty() on FreeBSD
  #elif defined(HAVE_PTY_H)
    #include <pty.h>      // for openpty() on Cygwin, Interix, OSF/1 4 and 5
  #endif
#endif

#ifdef HAVE_SPAWN_H
#define __USE_GNU    // ensures glibc exports posix_spawn_file_actions_addchdir_np
#include <spawn.h>
#endif

#ifdef SYS_IS_CYGWIN32
#include <process.h>
#endif

#ifdef HAVE_SELECT
#include <sys/time.h>
#endif

// LOCKING
// In HPC-GAP, be sure to HashLock PtyIOStreams before accessing any of
// the IOStream related variables, including FreeptyIOStreams

typedef struct {
  pid_t childPID;   // Also used as a link to make a linked free list
  int ptyFD;      // GAP reading from external prog
  int inuse;      /* we need to scan all the "live" structures when we have
                     had SIGCHLD so, for now, we just walk the array for
                     the ones marked in use */
  int changed;    /* set non-zero by the signal handler if our child has
                     done something -- stopped or exited */
  int status;     // status from wait3 -- meaningful only if changed is 1
  int blocked;    // we have already reported a problem, which is still there
  int alive;      /* gets set after waiting for a child actually fails
                     implying that the child has vanished under our noses */
} PtyIOStream;

enum {
    // maximal number of pseudo ttys we will allocate
    MAX_PTYS = 64,

    // maximal length of argument string for CREATE_PTY_IOSTREAM
    MAX_ARGS = 1000
};

static PtyIOStream PtyIOStreams[MAX_PTYS];

// FreePtyIOStreams is the index of the first unused slot of the PtyIOStreams
// array, or else -1 if there is none. The childPID field of each free slot in
// turn is set to the position of the next free slot (or -1), so that the free
// slots form a linked list.
static Int FreePtyIOStreams;

static Int NewStream(void)
{
    Int stream = -1;
    if (FreePtyIOStreams != -1) {
        stream = FreePtyIOStreams;
        FreePtyIOStreams = PtyIOStreams[stream].childPID;
    }
    return stream;
}

static void FreeStream(UInt stream)
{
    PtyIOStreams[stream].childPID = FreePtyIOStreams;
    FreePtyIOStreams = stream;
}

/****************************************************************************
**
*F  SignalChild(<stream>) . .. . . . . . . . . .  interrupt the child process
*/
static void SignalChild(UInt stream, UInt sig)
{
    if (PtyIOStreams[stream].childPID != -1) {
        kill(PtyIOStreams[stream].childPID, sig);
    }
}

/****************************************************************************
**
*F  KillChild(<stream>) . . . . . . . . . . . . . . .  kill the child process
*/
static void KillChild(UInt stream)
{
    if (PtyIOStreams[stream].childPID != -1) {
        close(PtyIOStreams[stream].ptyFD);
        SignalChild(stream, SIGKILL);
    }
}


/****************************************************************************
**
*/
#define PErr(msg)                                                            \
    Pr(msg ": %s (errno %d)\n", (Int)strerror(errno), (Int)errno);

/****************************************************************************
**
*F  OpenPty( <parent>, <child> ) . . . . . . . . open a pseudo terminal
*/

#ifdef HAVE_OPENPTY

static UInt OpenPty(int * parent, int * child)
{
    /* openpty is available on OpenBSD, NetBSD and FreeBSD, macOS,
       Cygwin, Interix, OSF/1 4 and 5, and glibc (since 1998), and hence
       on most modern Linux systems. See also:
       https://www.gnu.org/software/gnulib/manual/html_node/openpty.html */
    return (openpty(parent, child, NULL, NULL, NULL) < 0);
}

#elif defined(HAVE_POSIX_OPENPT)

static UInt OpenPty(int * parent, int * child)
{
    /* Attempt to use POSIX 98 pseudo ttys. Opening a parent tty is done
       via posix_openpt, which is available on virtually every current
       UNIX system; indeed, according to gnulib, it is available on at
       least the following systems:
         - glibc >= 2.2.1 (released January 2001; but is a stub on GNU/Hurd),
         - macOS >= 10.4 (released April 2005),
         - FreeBSD >= 5.1 (released June 2003),
         - NetBSD >= 3.0 (released December 2005),
         - AIX >= 5.2 (released October 2002),
         - HP-UX >= 11.31 (released February 2007),
         - Solaris >= 10 (released January 2005),
         - Cygwin >= 1.7 (released December 2009).
       Systems lacking posix_openpt (in addition to older versions of
       the systems listed above) include:
         - OpenBSD
         - Minix 3.1.8
         - IRIX 6.5
         - OSF/1 5.1
         - mingw
         - MSVC 9
         - Interix 3.5
         - BeOS
       */
    *parent = posix_openpt(O_RDWR | O_NOCTTY);
    if (*parent < 0) {
        PErr("OpenPty: posix_openpt failed");
        return 1;
    }

    if (grantpt(*parent)) {
        PErr("OpenPty: grantpt failed");
        goto error;
    }
    if (unlockpt(*parent)) {
        close(*parent);
        PErr("OpenPty: unlockpt failed");
        goto error;
    }

    *child = open(ptsname(*parent), O_RDWR, 0);
    if (*child < 0) {
        PErr("OpenPty: opening child tty failed");
        goto error;
    }
    return 0;

error:
    close(*parent);
    return 1;
}

#else

static UInt OpenPty(int * parent, int * child)
{
    Pr("no pseudo tty support available\n", 0, 0);
    return 1;
}

#endif


/****************************************************************************
**
*F  StartChildProcess( <dir>, <name>, <args> )
**  Start a subprocess using ptys. Returns the stream number of the IOStream
**  that is connected to the new process
*/


// Clean up a signalled or exited child process
// CheckChildStatusChanged must be called by libraries which replace GAP's
// signal handler, or call 'waitpid'.
// The function should be passed a PID, and the return value of waitpid.
// Returns 1 if that PID was a child owned by GAP, or 0 otherwise.
int CheckChildStatusChanged(int childPID, int status)
{
    GAP_ASSERT(childPID > 0);
    GAP_ASSERT((WIFEXITED(status) || WIFSIGNALED(status)));
    HashLock(PtyIOStreams);
    for (UInt i = 0; i < MAX_PTYS; i++) {
        if (PtyIOStreams[i].inuse && PtyIOStreams[i].childPID == childPID) {
            PtyIOStreams[i].changed = 1;
            PtyIOStreams[i].status = status;
            PtyIOStreams[i].blocked = 0;
            HashUnlock(PtyIOStreams);
            return 1;
        }
    }
    HashUnlock(PtyIOStreams);
    return 0;
}

static void ChildStatusChanged(int whichsig)
{
    UInt i;
    int  status;
    int  retcode;
    assert(whichsig == SIGCHLD);
    HashLock(PtyIOStreams);
    for (i = 0; i < MAX_PTYS; i++) {
        if (PtyIOStreams[i].inuse) {
            retcode = waitpid(PtyIOStreams[i].childPID, &status,
                              WNOHANG | WUNTRACED);
            if (retcode != -1 && retcode != 0 &&
                (WIFEXITED(status) || WIFSIGNALED(status))) {
                PtyIOStreams[i].changed = 1;
                PtyIOStreams[i].status = status;
                PtyIOStreams[i].blocked = 0;
            }
        }
    }
    HashUnlock(PtyIOStreams);

#if !defined(HPCGAP)
    signal(SIGCHLD, ChildStatusChanged);
#endif
}

#ifdef HPCGAP
static Obj FuncDEFAULT_SIGCHLD_HANDLER(Obj self)
{
    ChildStatusChanged(SIGCHLD);
    return (Obj)0;
}
#endif


//
// posix_spawn_file_actions_addchdir was only "recently" added to POSIX, and
// most implementations therefore do not yet use this final name, but instead
// provide the functionality under the alternate name
// posix_spawn_file_actions_addchdir_np.
//
// For most platforms, configure checks are sufficient to choose between the
// standard and _np names. macOS is trickier: recent SDKs declare the standard
// name, but a binary built against them may still target an older macOS which
// only provides the _np symbol at runtime, possibly leading to crashes.
// See issue <https://github.com/gap-system/gap/issues/6118> for details.
// To preserve that compatibility, we continue to prefer the _np API on Apple
// for now and locally suppress its deprecation warning in newer SDKs. If
// Apple ever removes the _np symbol, we can revisit this and switch to a more
// complex runtime selection scheme.
#if defined(HAVE_POSIX_SPAWN_FILE_ACTIONS_ADDCHDIR) ||                       \
    defined(HAVE_POSIX_SPAWN_FILE_ACTIONS_ADDCHDIR_NP)

#if !defined(HAVE_POSIX_SPAWN_FILE_ACTIONS_ADDCHDIR)
#define HAVE_POSIX_SPAWN_FILE_ACTIONS_ADDCHDIR
#endif

static int posix_spawn_file_actions_addchdir_func(
    posix_spawn_file_actions_t * file_actions, const char * dir)
{
#if defined(HAVE_POSIX_SPAWN_FILE_ACTIONS_ADDCHDIR_NP) && defined(__APPLE__)
#pragma GCC diagnostic push
#pragma GCC diagnostic ignored "-Wdeprecated-declarations"
    return posix_spawn_file_actions_addchdir_np(file_actions, dir);
#pragma GCC diagnostic pop
#elif defined(HAVE_POSIX_SPAWN_FILE_ACTIONS_ADDCHDIR_NP)
    return posix_spawn_file_actions_addchdir_np(file_actions, dir);
#else
    return posix_spawn_file_actions_addchdir(file_actions, dir);
#endif
}

#endif


// Disable posix_spawn in HPC-GAP if we can't use it in a thread-safe manner
#if defined(HPCGAP) && !defined(HAVE_POSIX_SPAWN_FILE_ACTIONS_ADDCHDIR)
#undef HAVE_POSIX_SPAWN
#endif

// if neither posix_spawn_file_actions_addchdir nor O_CLOEXEC are available,
// our posix_spawn code should not be used. This affects e.g. Linux systems
// with an old glibc, see https://github.com/gap-system/gap/issues/3918.
#if !defined(HAVE_POSIX_SPAWN_FILE_ACTIONS_ADDCHDIR) && !defined(O_CLOEXEC)
#undef HAVE_POSIX_SPAWN
#endif


#ifdef HAVE_POSIX_SPAWN

// O_DIRECTORY is not available in old Linux / glibc versions, but the way we
// use it below, it is optional anyway
#ifndef O_DIRECTORY
#define O_DIRECTORY 0
#endif


// The following function behaves like posix_spawn, but also
// changes the working directory temporarily to 'dir'.
static int posix_spawn_with_dir(pid_t *                      pid,
                                const char *                 path,
                                posix_spawn_file_actions_t * file_actions,
                                const posix_spawnattr_t *    attrp,
                                char * const                 argv[],
                                char * const                 envp[],
                                const char *                 dir)
{
    // For systems that don't support fork+exec well (e.g. embedded systems,
    // but also Windows), the POSIX Issue 6 (~2001) added posix_spawn() and
    // friends. These can be used as a replacement for fork+exec in many
    // applications. However, they curiously miss one very common demand when
    // launching subprocesses: there is no way to override the working
    // directory for the child process. This well-known deficiency has finally
    // been realized as a problem by POSIX in 2018, just about 17 years after
    // posix_spawn was first put into the standard, and so we might see a
    // proper fix for this soon, i.e., possibly even within the next decade!
    // See also <http://austingroupbugs.net/view.php?id=1208>.
    //
    // UPDATE: musl libc 1.1.24, glibc 2.29 and macOS 10.15 added preview
    // versions of these new APIs (with suffix `_np` to indicate it), so we
    // can actually start using them.
    //
    // UPDATE: the proposed change was accepted and applied to the draft of
    // the next POSIX revision in mid-2020. When this will appear in public
    // is anyones guess. On the upside, also OpenBSD, FreeBSD, and Solaris
    // implement the _np versions of the API.
    //
    // UPDATE: For now we still prefer the _np version if available, to
    // avoid issues in macOS 26 (see <https://github.com/gap-system/gap/issues/6118>.

#ifdef HAVE_POSIX_SPAWN_FILE_ACTIONS_ADDCHDIR
    if (posix_spawn_file_actions_addchdir_func(file_actions, dir)) {
        PErr("posix_spawn_with_dir: addchdir failed");
        return 1;
    }
#else
    // For older systems that lack posix_spawn_file_actions_addchdir or
    // posix_spawn_file_actions_addchdir_np, we use the following fallback
    // code.
    //
    // WARNING: This is not thread safe! As explained above, until recently,
    // there was no portable way to do this race free, without using an
    // external shim executable which sets the wd and then calls the actually
    // target executable.
    int oldwd = open(".", O_RDONLY | O_DIRECTORY | O_CLOEXEC);
    if (oldwd == -1) {
        PErr("posix_spawn_with_dir: cannot open current working directory");
        return 1;
    }
    if (chdir(dir) == -1) {
        PErr("posix_spawn_with_dir: cannot change working "
             "directory for subprocess");
        return 1;
    }
#endif

    // spawn subprocess
    int res = posix_spawn(pid, path, file_actions, attrp, argv, envp);
    if (res) {
        PErr("StartChildProcess: posix_spawn failed");
    }

#ifndef HAVE_POSIX_SPAWN_FILE_ACTIONS_ADDCHDIR
    // restore working directory
    if (fchdir(oldwd)) {
        PErr("StartChildProcess: failed to restore working dir");
    }
    close(oldwd);    // ignore error
#endif
    return res;
}

#endif


static Int
StartChildProcess(const Char * dir, const Char * prg, Char * args[])
{
    int child; // pipe to child
    Int stream;

    struct termios tst; // old and new terminal state

    HashLock(PtyIOStreams);

    // Get a stream record
    stream = NewStream();
    if (stream == -1) {
        HashUnlock(PtyIOStreams);
        return -1;
    }

    // open pseudo terminal for communication with gap
    if (OpenPty(&PtyIOStreams[stream].ptyFD, &child)) {
        PErr("StartChildProcess: open pseudo tty failed");
        FreeStream(stream);
        HashUnlock(PtyIOStreams);
        return -1;
    }

    // Now fiddle with the terminal sessions on the pty
    if (tcgetattr(child, &tst) == -1) {
        PErr("StartChildProcess: tcgetattr on child pty failed");
        goto cleanup;
    }
    tst.c_cc[VINTR] = 0377;
    tst.c_cc[VQUIT] = 0377;
    tst.c_iflag    &= ~(INLCR|ICRNL);
    tst.c_cc[VMIN]  = 1;
    tst.c_cc[VTIME] = 0;
    tst.c_lflag    &= ~(ECHO|ICANON);
    tst.c_oflag    &= ~(ONLCR);
    if (tcsetattr(child, TCSANOW, &tst) == -1) {
        PErr("StartChildProcess: tcsetattr on child pty failed");
        goto cleanup;
    }

    // set input to non blocking operation
    // Not any more

    PtyIOStreams[stream].inuse = 1;
    PtyIOStreams[stream].alive = 1;
    PtyIOStreams[stream].blocked = 0;
    PtyIOStreams[stream].changed = 0;
    // fork
#ifdef HAVE_POSIX_SPAWN
    posix_spawn_file_actions_t file_actions;

    // setup file actions
    if (posix_spawn_file_actions_init(&file_actions)) {
        PErr("StartChildProcess: posix_spawn_file_actions_init failed");
        goto cleanup;
    }

    if (posix_spawn_file_actions_addclose(&file_actions,
                                          PtyIOStreams[stream].ptyFD)) {
        PErr("StartChildProcess: addclose failed");
        posix_spawn_file_actions_destroy(&file_actions);
        goto cleanup;
    }

    if (posix_spawn_file_actions_adddup2(&file_actions, child, 0)) {
        PErr("StartChildProcess: adddup2(child, 0) failed");
        posix_spawn_file_actions_destroy(&file_actions);
        goto cleanup;
    }

    if (posix_spawn_file_actions_adddup2(&file_actions, child, 1)) {
        PErr("StartChildProcess: adddup2(child, 1) failed");
        posix_spawn_file_actions_destroy(&file_actions);
        goto cleanup;
    }

    // spawn subprocess
    if (posix_spawn_with_dir(&PtyIOStreams[stream].childPID, prg,
                             &file_actions, 0, args, environ, dir)) {
        PErr("StartChildProcess: posix_spawn_with_dir failed");
        goto cleanup;
    }

    // cleanup
    if (posix_spawn_file_actions_destroy(&file_actions)) {
        PErr("StartChildProcess: posix_spawn_file_actions_destroy failed");
        goto cleanup;
    }
#else
    PtyIOStreams[stream].childPID = fork();
    if (PtyIOStreams[stream].childPID == 0) {
        // Set up the child
        close(PtyIOStreams[stream].ptyFD);
        if (dup2(child, 0) == -1)
            _exit(-1);
        fcntl(0, F_SETFD, 0);

        if (dup2(child, 1) == -1)
            _exit(-1);
        fcntl(1, F_SETFD, 0);

        if (chdir(dir) == -1) {
            _exit(-1);
        }

#ifdef HAVE_SETPGID
        setpgid(0, 0);
#endif

        execv(prg, args);

        // This should never happen
        close(child);
        _exit(1);
    }
#endif

    // Now we're back in the parent
    // check if the fork was successful
    if (PtyIOStreams[stream].childPID == -1) {
        PErr("StartChildProcess: cannot fork to subprocess");
        goto cleanup;
    }
    close(child);


    HashUnlock(PtyIOStreams);
    return stream;

cleanup:
    close(child);
    close(PtyIOStreams[stream].ptyFD);
    PtyIOStreams[stream].inuse = 0;
    FreeStream(stream);
    HashUnlock(PtyIOStreams);
    return -1;
}


#ifndef GAP_DISABLE_SUBPROCESS_CODE

/****************************************************************************
**
*F  SyExecuteProcess( <dir>, <prg>, <in>, <out>, <args> ) . . . . new process
**
**  Start  <prg> in  directory <dir>  with  standard input connected to <in>,
**  standard  output  connected to <out>   and arguments.  No  path search is
**  performed, the return  value of the process  is returned if the operation
**  system supports such a concept.
*/

/****************************************************************************
**
*f  SyExecuteProcess( <dir>, <prg>, <in>, <out>, <args> )
*/
#if defined(HAVE_FORK) || defined(HAVE_VFORK)

#ifndef WEXITSTATUS
#define WEXITSTATUS(stat_val) ((unsigned)(stat_val) >> 8)
#endif
#ifndef WIFEXITED
#define WIFEXITED(stat_val) (((stat_val)&255) == 0)
#endif

#ifdef SYS_IS_CYGWIN32

static UInt
SyExecuteProcess(Char * dir, Char * prg, Int in, Int out, Char * args[])
{
    int savestdin, savestdout;
    Int tin, tout;
    int res;

    // change the working directory
    if (chdir(dir) == -1)
        return -1;

    // if <in> is -1 open "/dev/null"
    if (in == -1)
        tin = open("/dev/null", O_RDONLY);
    else
        tin = SyBufFileno(in);
    if (tin == -1)
        return -1;

    // if <out> is -1 open "/dev/null"
    if (out == -1)
        tout = open("/dev/null", O_WRONLY);
    else
        tout = SyBufFileno(out);
    if (tout == -1) {
        if (in == -1)
            close(tin);
        return -1;
    }

    // set standard input to <in>, standard output to <out>
    savestdin = -1;    // Just to please the compiler
    if (tin != 0) {
        savestdin = dup(0);
        if (savestdin == -1 || dup2(tin, 0) == -1) {
            if (out == -1)
                close(tout);
            if (in == -1)
                close(tin);
            return -1;
        }
        fcntl(0, F_SETFD, 0);
    }

    if (tout != 1) {
        savestdout = dup(1);
        if (savestdout == -1 || dup2(tout, 1) == -1) {
            if (tin != 0) {
                close(0);
                dup2(savestdin, 0);
                close(savestdin);
            }
            if (out == -1)
                close(tout);
            if (in == -1)
                close(tin);
            return -1;
        }
        fcntl(1, F_SETFD, 0);
    }

    FreezeStdin = 1;
    // now try to execute the program
    res = spawnve(_P_WAIT, prg, (const char * const *)args,
                  (const char * const *)environ);

    // Now repair the open file descriptors:
    if (tout != 1) {
        close(1);
        dup2(savestdout, 1);
        close(savestdout);
    }
    if (tin != 0) {
        close(0);
        dup2(savestdin, 0);
        close(savestdin);
    }
    if (out == -1)
        close(tout);
    if (in == -1)
        close(tin);

    FreezeStdin = 0;

    // Report result:
    if (res < 0)
        return -1;
    return WEXITSTATUS(res);
}

#else

typedef void sig_handler_t(int);

static void NullSignalHandler(int scratch)
{
}

static UInt
SyExecuteProcess(Char * dir, Char * prg, Int in, Int out, Char * args[])
{
    pid_t pid;    // process id
    pid_t wait_pid;
    int   status;    // do not use `Int'
    Int   tin;       // temp in
    Int   tout;      // temp out
    sig_handler_t * volatile func2;


    // turn off the SIGCHLD handling, so that we can be sure to collect this
    // child `After that, we call the old signal handler, in case any other
    // children have died in the meantime. This resets the handler

    func2 = signal(SIGCHLD, SIG_DFL);

    // This may return SIG_DFL (0x0) or SIG_IGN (0x1) if the previous handler
    // was set to the default or 'ignore'. In these cases (or if SIG_ERR is
    // returned), just use a null signal handler - the default on most systems
    // is to do nothing
    if (func2 == SIG_ERR || func2 == SIG_DFL || func2 == SIG_IGN)
        func2 = &NullSignalHandler;

    // clone the process
    pid = fork();
    if (pid == -1) {
        return -1;
    }

    // we are the parent
    if (pid != 0) {
        // Stop trying to read input
        FreezeStdin = 1;

        // ignore a CTRL-C
        struct sigaction sa;
        struct sigaction oldsa;

        sa.sa_handler = SIG_IGN;
        sigemptyset(&(sa.sa_mask));
        sa.sa_flags = 0;
        sigaction(SIGINT, &sa, &oldsa);

        // wait for some action
        wait_pid = waitpid(pid, &status, 0);
        FreezeStdin = 0;
        sigaction(SIGINT, &oldsa, NULL);
        (*func2)(SIGCHLD);
        if (wait_pid == -1) {
            return -1;
        }
        if (WIFSIGNALED(status)) {
            return -1;
        }
        return WEXITSTATUS(status);
    }

    // we are the child
    else {

        // change the working directory
        if (chdir(dir) == -1) {
            _exit(-1);
        }

        // if <in> is -1 open "/dev/null"
        if (in == -1) {
            tin = open("/dev/null", O_RDONLY);
        }
        else {
            tin = SyBufFileno(in);
        }
        if (tin == -1) {
            _exit(-1);
        }

        // if <out> is -1 open "/dev/null"
        if (out == -1) {
            tout = open("/dev/null", O_WRONLY);
        }
        else {
            tout = SyBufFileno(out);
        }
        if (tout == -1) {
            _exit(-1);
        }

        // set standard input to <in>, standard output to <out>
        if (tin != 0) {
            if (dup2(tin, 0) == -1) {
                _exit(-1);
            }
        }
        fcntl(0, F_SETFD, 0);

        if (tout != 1) {
            if (dup2(tout, 1) == -1) {
                _exit(-1);
            }
        }
        fcntl(1, F_SETFD, 0);

        // now try to execute the program
        execve(prg, args, environ);
        _exit(-1);
    }

    // this should not happen
    return -1;
}
#endif

#endif

#endif    // !GAP_DISABLE_SUBPROCESS_CODE

// This function assumes that the caller invoked HashLock(PtyIOStreams).
// It unlocks just before throwing any error.
static void HandleChildStatusChanges(UInt pty)
{
    // common error handling, when we are asked to read or write to a stopped
    // or dead child
    if (PtyIOStreams[pty].alive == 0) {
        PtyIOStreams[pty].changed = 0;
        PtyIOStreams[pty].blocked = 0;
        HashUnlock(PtyIOStreams);
        ErrorQuit("Child Process is unexpectedly dead", 0, 0);
    }
    else if (PtyIOStreams[pty].blocked) {
        HashUnlock(PtyIOStreams);
        ErrorQuit("Child Process is still dead", 0, 0);
    }
    else if (PtyIOStreams[pty].changed) {
        PtyIOStreams[pty].blocked = 1;
        PtyIOStreams[pty].changed = 0;
        Int cPID = PtyIOStreams[pty].childPID;
        Int status = PtyIOStreams[pty].status;
        HashUnlock(PtyIOStreams);
        ErrorQuit("Child Process %d has stopped or died, status %d", cPID,
                  status);
    }
}

static Obj FuncCREATE_PTY_IOSTREAM(Obj self, Obj dir, Obj prog, Obj args)
{
    Obj    allargs[MAX_ARGS + 1];
    Char * argv[MAX_ARGS + 2];
    UInt   i, len;
    Int    pty;
    len = LEN_LIST(args);
    if (len > MAX_ARGS)
        ErrorQuit("Too many arguments", 0, 0);
    ConvString(dir);
    ConvString(prog);
    for (i = 1; i <= len; i++) {
        allargs[i] = ELM_LIST(args, i);
        ConvString(allargs[i]);
    }
    // From here we cannot afford to have a garbage collection
    argv[0] = CSTR_STRING(prog);
    for (i = 1; i <= len; i++) {
        argv[i] = CSTR_STRING(allargs[i]);
    }
    argv[i] = (Char *)0;
    pty = StartChildProcess(CONST_CSTR_STRING(dir), CONST_CSTR_STRING(prog),
                            argv);
    if (pty < 0)
        return Fail;
    else
        return ObjInt_Int(pty);
}


static Int ReadFromPty2(UInt stream, Char * buf, Int maxlen, UInt block)
{
    // read at most maxlen bytes from stream, into buf. If block is non-zero
    // then wait for at least one byte to be available. Otherwise don't.
    // Return the number of bytes read, or -1 for error. A blocking return
    // having read zero bytes definitely indicates an end of file

    Int nread = 0;
    int ret;

    while (maxlen > 0) {
#ifdef HAVE_SELECT
        if (!block || nread > 0) {
            fd_set         set;
            struct timeval tv;
            do {
                FD_ZERO(&set);
                FD_SET(PtyIOStreams[stream].ptyFD, &set);
                tv.tv_sec = 0;
                tv.tv_usec = 0;
                ret = select(PtyIOStreams[stream].ptyFD + 1, &set, NULL, NULL,
                             &tv);
            } while (ret == -1 && errno == EAGAIN);
            if (ret == -1 && nread == 0)
                return -1;
            if (ret < 1)
                return nread ? nread : -1;
        }
#endif
        do {
            ret = read(PtyIOStreams[stream].ptyFD, buf, maxlen);
        } while (ret == -1 && errno == EAGAIN);
        if (ret == -1 && nread == 0)
            return -1;
        if (ret < 1)
            return nread;
        nread += ret;
        buf += ret;
        maxlen -= ret;
    }
    return nread;
}


static UInt WriteToPty(UInt stream, Char * buf, Int len)
{
    Int res;
    Int old;
    if (len < 0) {
        // FIXME: why allow 'len' to be negative here? To allow
        // invoking a "raw" version of `write` perhaps? But we don't
        // seem to use that anywhere. So perhaps get rid of it or
        // even turn it into an error?!
        return write(PtyIOStreams[stream].ptyFD, buf, -len);
    }
    old = len;
    while (0 < len) {
        res = write(PtyIOStreams[stream].ptyFD, buf, len);
        if (res < 0) {
            HandleChildStatusChanges(stream);
            if (errno == EAGAIN) {
                continue;
            }
            else
                // FIXME: by returning errno, we make it impossible for the
                // caller to detect errors.
                return errno;
        }
        len -= res;
        buf += res;
    }
    return old;
}

static UInt HashLockStreamIfAvailable(Obj stream)
{
    UInt pty = INT_INTOBJ(stream);
    HashLock(PtyIOStreams);
    if (!PtyIOStreams[pty].inuse) {
        HashUnlock(PtyIOStreams);
        ErrorMayQuit("IOSTREAM %d is not in use", pty, 0);
    }
    return pty;
}

static Obj FuncWRITE_IOSTREAM(Obj self, Obj stream, Obj string, Obj len)
{
    UInt pty = HashLockStreamIfAvailable(stream);

    HandleChildStatusChanges(pty);
    ConvString(string);
    UInt result = WriteToPty(pty, CSTR_STRING(string), INT_INTOBJ(len));
    HashUnlock(PtyIOStreams);
    return ObjInt_Int(result);
}

static Obj FuncREAD_IOSTREAM(Obj self, Obj stream, Obj len)
{
    UInt pty = HashLockStreamIfAvailable(stream);

    /* HandleChildStatusChanges(pty);   Omit this to allow picking up
     * "trailing" bytes*/
    Obj string = NEW_STRING(INT_INTOBJ(len));
    Int ret = ReadFromPty2(pty, CSTR_STRING(string), INT_INTOBJ(len), 1);
    HashUnlock(PtyIOStreams);
    if (ret == -1)
        return Fail;
    SET_LEN_STRING(string, ret);
    ResizeBag(string, SIZEBAG_STRINGLEN(ret));
    return string;
}

static Obj FuncREAD_IOSTREAM_NOWAIT(Obj self, Obj stream, Obj len)
{
    UInt pty = HashLockStreamIfAvailable(stream);

    /* HandleChildStatusChanges(pty);   Omit this to allow picking up
     * "trailing" bytes*/
    Obj string = NEW_STRING(INT_INTOBJ(len));
    Int ret = ReadFromPty2(pty, CSTR_STRING(string), INT_INTOBJ(len), 0);
    HashUnlock(PtyIOStreams);
    if (ret == -1)
        return Fail;
    SET_LEN_STRING(string, ret);
    ResizeBag(string, SIZEBAG_STRINGLEN(ret));
    return string;
}

static Obj FuncKILL_CHILD_IOSTREAM(Obj self, Obj stream)
{
    UInt pty = HashLockStreamIfAvailable(stream);

    // Don't check for child having changes status
    KillChild(pty);

    HashUnlock(PtyIOStreams);
    return 0;
}

static Obj FuncSIGNAL_CHILD_IOSTREAM(Obj self, Obj stream, Obj sig)
{
    UInt pty = HashLockStreamIfAvailable(stream);

    // Don't check for child having changes status
    SignalChild(pty, INT_INTOBJ(sig));

    HashUnlock(PtyIOStreams);
    return 0;
}

static Obj FuncCLOSE_PTY_IOSTREAM(Obj self, Obj stream)
{
    UInt pty = HashLockStreamIfAvailable(stream);
    int status, retcode;

    // Close down the child
    kill(PtyIOStreams[pty].childPID, SIGTERM);

    // GAP (or another library) might wait on this PID before
    // we handle it. If that happens, waitpid will return -1.
    retcode = waitpid(PtyIOStreams[pty].childPID, &status, WNOHANG);
    if (retcode == 0) {
        // Give process a second to quit
        sleep(1);
        retcode = waitpid(PtyIOStreams[pty].childPID, &status, WNOHANG);
    }
    if (retcode == 0) {
        // Hard kill process
        kill(PtyIOStreams[pty].childPID, SIGKILL);
        retcode = waitpid(PtyIOStreams[pty].childPID, &status, 0);
    }

    retcode = close(PtyIOStreams[pty].ptyFD);
    if (retcode)
        Pr("Strange close return code %d\n", retcode, 0);

    PtyIOStreams[pty].inuse = 0;

    FreeStream(pty);
    HashUnlock(PtyIOStreams);
    return 0;
}

static Obj FuncIS_BLOCKED_IOSTREAM(Obj self, Obj stream)
{
    UInt pty = HashLockStreamIfAvailable(stream);

    int isBlocked = (PtyIOStreams[pty].blocked || PtyIOStreams[pty].changed ||
                     !PtyIOStreams[pty].alive);
    HashUnlock(PtyIOStreams);
    return isBlocked ? True : False;
}

static Obj FuncFD_OF_IOSTREAM(Obj self, Obj stream)
{
    UInt pty = HashLockStreamIfAvailable(stream);

    Obj result = ObjInt_Int(PtyIOStreams[pty].ptyFD);
    HashUnlock(PtyIOStreams);
    return result;
}

static Obj
FuncExecuteProcess(Obj self, Obj dir, Obj prg, Obj in, Obj out, Obj args)
{
    Obj    ExecArgs[1024];
    Char * ExecCArgs[1024];

    Obj tmp;
    Int res;
    Int i;

    RequireStringRep(SELF_NAME, dir);
    RequireStringRep(SELF_NAME, prg);
    Int iin = GetSmallInt(SELF_NAME, in);
    Int iout = GetSmallInt(SELF_NAME, out);
    RequireSmallList(SELF_NAME, args);

    // create an argument array
    for (i = 1; i <= LEN_LIST(args); i++) {
        if (i == 1023)
            break;
        tmp = ELM_LIST(args, i);
        RequireStringRep(SELF_NAME, tmp);
        ExecArgs[i] = tmp;
    }
    ExecCArgs[0] = CSTR_STRING(prg);
    ExecCArgs[i] = 0;
    for (i--; 0 < i; i--) {
        ExecCArgs[i] = CSTR_STRING(ExecArgs[i]);
    }
    if (SyWindow && out == INTOBJ_INT(1))    // standard output
        syWinPut(INT_INTOBJ(out), "@z", "");

    // execute the process
    res = SyExecuteProcess(CSTR_STRING(dir), CSTR_STRING(prg), iin, iout,
                           ExecCArgs);

    if (SyWindow && out == INTOBJ_INT(1))    // standard output
        syWinPut(INT_INTOBJ(out), "@mAgIc", "");
    return res == 255 ? Fail : INTOBJ_INT(res);
}

#else // !defined(GAP_DISABLE_SUBPROCESS_CODE)

int CheckChildStatusChanged(int childPID, int status)
{
    return 0;
}

static Obj FuncCREATE_PTY_IOSTREAM(Obj self, Obj dir, Obj prog, Obj args)
{
    return Fail;
}

static Obj FuncWRITE_IOSTREAM(Obj self, Obj stream, Obj string, Obj len)
{
    return Fail;
}

static Obj FuncREAD_IOSTREAM(Obj self, Obj stream, Obj len)
{
    return Fail;
}

static Obj FuncREAD_IOSTREAM_NOWAIT(Obj self, Obj stream, Obj len)
{
    return Fail;
}

static Obj FuncKILL_CHILD_IOSTREAM(Obj self, Obj stream)
{
    return 0;
}

static Obj FuncSIGNAL_CHILD_IOSTREAM(Obj self, Obj stream, Obj sig)
{
    return 0;
}

static Obj FuncCLOSE_PTY_IOSTREAM(Obj self, Obj stream)
{
    return 0;
}

static Obj FuncIS_BLOCKED_IOSTREAM(Obj self, Obj stream)
{
    return Fail;
}

static Obj FuncFD_OF_IOSTREAM(Obj self, Obj stream)
{
    return Fail;
}

static Obj
FuncExecuteProcess(Obj self, Obj dir, Obj prg, Obj in, Obj out, Obj args)
{
    return Fail;
}

#endif


/****************************************************************************
**
*F * * * * * * * * * * * * * initialize module * * * * * * * * * * * * * * *
*/

/****************************************************************************
**
*V  GVarFuncs . . . . . . . . . . . . . . . . . . list of functions to export
*/
static StructGVarFunc GVarFuncs[] = {

    GVAR_FUNC_3ARGS(CREATE_PTY_IOSTREAM, dir, prog, args),
    GVAR_FUNC_3ARGS(WRITE_IOSTREAM, stream, string, len),
    GVAR_FUNC_2ARGS(READ_IOSTREAM, stream, len),
    GVAR_FUNC_2ARGS(READ_IOSTREAM_NOWAIT, stream, len),
    GVAR_FUNC_1ARGS(KILL_CHILD_IOSTREAM, stream),
    GVAR_FUNC_1ARGS(CLOSE_PTY_IOSTREAM, stream),
    GVAR_FUNC_2ARGS(SIGNAL_CHILD_IOSTREAM, stream, signal),
    GVAR_FUNC_1ARGS(IS_BLOCKED_IOSTREAM, stream),
    GVAR_FUNC_1ARGS(FD_OF_IOSTREAM, stream),
#ifdef HPCGAP
    GVAR_FUNC_0ARGS(DEFAULT_SIGCHLD_HANDLER),
#endif
    GVAR_FUNC_5ARGS(ExecuteProcess, dir, prg, in, out, args),

    { 0, 0, 0, 0, 0 }
};

// FIXME/TODO: should probably do some checks preSave for open files etc and
// refuse to save if any are found

/****************************************************************************
**
*F  InitKernel( <module> ) . . . . . . .  initialise kernel data structures
*/
static Int InitKernel(StructInitInfo * module)
{
#ifndef GAP_DISABLE_SUBPROCESS_CODE
    UInt i;
    PtyIOStreams[0].childPID = -1;
    for (i = 1; i < MAX_PTYS; i++) {
        PtyIOStreams[i].childPID = i - 1;
        PtyIOStreams[i].inuse = 0;
    }
    FreePtyIOStreams = MAX_PTYS - 1;

#if !defined(HPCGAP)
    // Set up the trap to detect future dying children
    signal(SIGCHLD, ChildStatusChanged);
#endif

#endif

    // init filters and functions
    InitHdlrFuncsFromTable(GVarFuncs);

    return 0;
}

/****************************************************************************
**
*F  InitLibrary( <module> ) . . . . . . .  initialise library data structures
*/
static Int InitLibrary(StructInitInfo * module)
{
    // init filters and functions
    InitGVarFuncsFromTable(GVarFuncs);

    return 0;
}

/****************************************************************************
**
*F  InitInfoSysFiles()  . . . . . . . . . . . . . . . table of init functions
*/
static StructInitInfo module = {
    // init struct using C99 designated initializers; for a full list of
    // fields, please refer to the definition of StructInitInfo
    .type = MODULE_BUILTIN,
    .name = "iostream",
    .initKernel = InitKernel,
    .initLibrary = InitLibrary,
};

StructInitInfo * InitInfoIOStream(void)
{
    return &module;
}
