/* Filtering of data through a subprocess.
   Copyright (C) 2001-2003, 2008-2025 Free Software Foundation, Inc.
   Written by Bruno Haible <bruno@clisp.org>, 2009.

   This program is free software: you can redistribute it and/or modify
   it under the terms of the GNU General Public License as published by
   the Free Software Foundation, either version 3 of the License, or
   (at your option) any later version.

   This program is distributed in the hope that it will be useful,
   but WITHOUT ANY WARRANTY; without even the implied warranty of
   MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE.  See the
   GNU General Public License for more details.

   You should have received a copy of the GNU General Public License
   along with this program.  If not, see <https://www.gnu.org/licenses/>.  */

#include <config.h>

#include "pipe-filter.h"

#include <errno.h>
#include <fcntl.h>
#include <stdint.h>
#include <stdlib.h>
#include <unistd.h>
#if defined _WIN32 && ! defined __CYGWIN__
# include <windows.h>
# include <process.h> /* _beginthreadex, _endthreadex */
#elif defined __KLIBC__
# define INCL_DOS
# include <os2.h>

/* Simple implementation of Win32 APIs */

# define WINAPI

typedef struct _HANDLE
{
  TID tid;
  HEV hevDone;
  unsigned int WINAPI (*start) (void *);
  void *arg;
} *HANDLE;

typedef ULONG DWORD;

static void
start_wrapper (void *arg)
{
  HANDLE h = (HANDLE) arg;

  h->start (h->arg);

  DosPostEventSem (h->hevDone);
  _endthread ();
}

static HANDLE
_beginthreadex (void *s, unsigned n, unsigned int WINAPI (*start) (void *),
                void *arg, unsigned fl, unsigned *th)
{
  HANDLE h;

  h = malloc (sizeof (*h));
  if (!h)
    return NULL;

  if (DosCreateEventSem (NULL, &h->hevDone, 0, FALSE))
    goto exit_free;

  h->start = start;
  h->arg = arg;

  h->tid = _beginthread (start_wrapper, NULL, n, (void *) h);
  if (h->tid == -1)
    goto exit_close_event_sem;

  return h;

 exit_close_event_sem:
  DosCloseEventSem (h->hevDone);

 exit_free:
  free (h);

  return NULL;
}

static BOOL
CloseHandle (HANDLE h)
{
  DosCloseEventSem (h->hevDone);
  free (h);
}

# define _endthreadex(x) return (x)
# define TerminateThread(h, e) DosKillThread (h->tid)

# define GetLastError()  (-1)

# ifndef ERROR_NO_DATA
#  define ERROR_NO_DATA 232
# endif

# define INFINITE SEM_INDEFINITE_WAIT
# define WAIT_OBJECT_0  0

static DWORD
WaitForSingleObject (HANDLE h, DWORD ms)
{
  return DosWaitEventSem (h->hevDone, ms) == 0 ? WAIT_OBJECT_0 : (DWORD) -1;
}

static DWORD
WaitForMultipleObjects (DWORD nCount, const HANDLE *pHandles, BOOL bWaitAll,
                        DWORD ms)
{
  HMUX hmux;
  PSEMRECORD psr;
  ULONG ulUser;
  ULONG rc = (ULONG) -1;
  DWORD i;

  psr = malloc (sizeof (*psr) * nCount);
  if (!psr)
    goto exit_return;

  for (i = 0; i < nCount; ++i)
    {
      psr[i].hsemCur = (HSEM) pHandles[i]->hevDone;
      psr[i].ulUser  = WAIT_OBJECT_0 + i;
    }

  if (DosCreateMuxWaitSem (NULL, &hmux, nCount, psr,
                           bWaitAll ? DCMW_WAIT_ALL : DCMW_WAIT_ANY))
    goto exit_free;

  rc = DosWaitMuxWaitSem (hmux, ms, &ulUser);
  DosCloseMuxWaitSem (hmux);

 exit_free:
  free (psr);

 exit_return:
  if (rc)
    return (DWORD) -1;

  return ulUser;
}
#else
# include <signal.h>
# include <sys/select.h>
#endif

#include <error.h>
#include "spawn-pipe.h"
#include "wait-process.h"
#include "gettext.h"

#define _(msgid) dgettext ("gnulib", msgid)

#include "pipe-filter-aux.h"

#if (defined _WIN32 && ! defined __CYGWIN__) || defined __KLIBC__

struct locals
{
  /* Arguments passed to pipe_filter_ii_execute.  */
  prepare_write_fn prepare_write;
  done_write_fn done_write;
  prepare_read_fn prepare_read;
  done_read_fn done_read;

  /* Management of the subprocess.  */
  void *private_data;
  int fd[2];

  /* Status of the writer part.  */
  volatile bool writer_terminated;
  volatile int writer_errno;
  /* Status of the reader part.  */
  volatile bool reader_terminated;
  volatile int reader_errno;
};

static unsigned int WINAPI
writer_thread_func (void *thread_arg)
{
  struct locals *l = (struct locals *) thread_arg;

  for (;;)
    {
      size_t bufsize;
      const void *buf = l->prepare_write (&bufsize, l->private_data);
      if (buf != NULL)
        {
          ssize_t nwritten =
            write (l->fd[1], buf, bufsize > SSIZE_MAX ? SSIZE_MAX : bufsize);
          if (nwritten < 0)
            {
              /* Don't assume that the gnulib modules 'write' and 'sigpipe' are
                 used.  */
              if (GetLastError () == ERROR_NO_DATA)
                errno = EPIPE;
              l->writer_errno = errno;
              break;
            }
          else if (nwritten > 0)
            l->done_write ((void *) buf, nwritten, l->private_data);
        }
      else
        break;
    }

  l->writer_terminated = true;
  _endthreadex (0); /* calls ExitThread (0) */
  abort ();
}

static unsigned int WINAPI
reader_thread_func (void *thread_arg)
{
  struct locals *l = (struct locals *) thread_arg;

  for (;;)
    {
      size_t bufsize;
      void *buf = l->prepare_read (&bufsize, l->private_data);
      if (!(buf != NULL && bufsize > 0))
        /* prepare_read returned wrong values.  */
        abort ();
      {
        ssize_t nread =
          read (l->fd[0], buf, bufsize > SSIZE_MAX ? SSIZE_MAX : bufsize);
        if (nread < 0)
          {
            l->reader_errno = errno;
            break;
          }
        else if (nread > 0)
          l->done_read (buf, nread, l->private_data);
        else /* nread == 0 */
          break;
      }
    }

  l->reader_terminated = true;
  _endthreadex (0); /* calls ExitThread (0) */
  abort ();
}

#endif

int
pipe_filter_ii_execute (const char *progname,
                        const char *prog_path, const char * const *prog_argv,
                        bool null_stderr, bool exit_on_error,
                        prepare_write_fn prepare_write,
                        done_write_fn done_write,
                        prepare_read_fn prepare_read,
                        done_read_fn done_read,
                        void *private_data)
{
  pid_t child;
  int fd[2];
#if !((defined _WIN32 && ! defined __CYGWIN__) || defined __KLIBC__)
  struct sigaction orig_sigpipe_action;
#endif

  /* Open a bidirectional pipe to a subprocess.  */
  child = create_pipe_bidi (progname, prog_path, prog_argv, NULL,
                            NULL, null_stderr, true, exit_on_error,
                            fd);
  if (child == -1)
    return -1;

#if (defined _WIN32 && ! defined __CYGWIN__) || defined __KLIBC__
  /* Native Windows API.  */
  /* Pipes have a non-blocking mode, see function SetNamedPipeHandleState and
     the article "Named Pipe Type, Read, and Wait Modes", but Microsoft's
     documentation discourages its use.  So don't use it.
     Asynchronous I/O is also not suitable because it notifies the caller only
     about completion of the I/O request, not about intermediate progress.
     So do the writing and the reading in separate threads.  */
  {
    struct locals l;
    HANDLE handles[2];
    #define writer_thread_handle handles[0]
    #define reader_thread_handle handles[1]
    bool writer_cleaned_up;
    bool reader_cleaned_up;

    l.prepare_write = prepare_write;
    l.done_write = done_write;
    l.prepare_read = prepare_read;
    l.done_read = done_read;
    l.private_data = private_data;
    l.fd[0] = fd[0];
    l.fd[1] = fd[1];
    l.writer_terminated = false;
    l.writer_errno = 0;
    l.reader_terminated = false;
    l.reader_errno = 0;

    writer_thread_handle =
      (HANDLE) _beginthreadex (NULL, 100000, writer_thread_func, &l, 0, NULL);
    reader_thread_handle =
      (HANDLE) _beginthreadex (NULL, 100000, reader_thread_func, &l, 0, NULL);
    if (writer_thread_handle == NULL || reader_thread_handle == NULL)
      {
        if (exit_on_error)
          error (EXIT_FAILURE, 0, _("creation of threads failed"));
        if (reader_thread_handle != NULL)
          CloseHandle (reader_thread_handle);
        if (writer_thread_handle != NULL)
          CloseHandle (writer_thread_handle);
        goto fail;
      }
    writer_cleaned_up = false;
    reader_cleaned_up = false;
    for (;;)
      {
        DWORD ret;

        /* Here !(writer_cleaned_up && reader_cleaned_up).  */
        if (writer_cleaned_up)
          ret = WaitForSingleObject (reader_thread_handle, INFINITE);
        else if (reader_cleaned_up)
          ret = WaitForSingleObject (writer_thread_handle, INFINITE);
        else
          ret = WaitForMultipleObjects (2, handles, FALSE, INFINITE);
        if (!(ret == WAIT_OBJECT_0 + 0 || ret == WAIT_OBJECT_0 + 1))
          abort ();

        if (l.writer_terminated)
          {
            /* The writer thread has just terminated.  */
            l.writer_terminated = false;
            CloseHandle (writer_thread_handle);
            if (l.writer_errno)
              {
                if (exit_on_error)
                  error (EXIT_FAILURE, l.writer_errno,
                         _("write to %s subprocess failed"), progname);
                if (!reader_cleaned_up)
                  {
                    TerminateThread (reader_thread_handle, 1);
                    CloseHandle (reader_thread_handle);
                  }
                goto fail;
              }
            /* Tell the child there is nothing more the parent will send.  */
            close (fd[1]);
            writer_cleaned_up = true;
          }
        if (l.reader_terminated)
          {
            /* The reader thread has just terminated.  */
            l.reader_terminated = false;
            CloseHandle (reader_thread_handle);
            if (l.reader_errno)
              {
                if (exit_on_error)
                  error (EXIT_FAILURE, l.reader_errno,
                         _("read from %s subprocess failed"), progname);
                if (!writer_cleaned_up)
                  {
                    TerminateThread (writer_thread_handle, 1);
                    CloseHandle (writer_thread_handle);
                  }
                goto fail;
              }
            reader_cleaned_up = true;
          }
        if (writer_cleaned_up && reader_cleaned_up)
          break;
      }
  }
#else
  /* When we write to the child process and it has just terminated,
     we don't want to die from a SIGPIPE signal.  So set the SIGPIPE
     handler to SIG_IGN, and handle EPIPE error codes in write().  */
  {
    struct sigaction sigpipe_action;

    sigpipe_action.sa_handler = SIG_IGN;
    sigpipe_action.sa_flags = 0;
    sigemptyset (&sigpipe_action.sa_mask);
    if (sigaction (SIGPIPE, &sigpipe_action, &orig_sigpipe_action) < 0)
      abort ();
  }

  {
# if HAVE_SELECT
    fd_set readfds;  /* All bits except fd[0] are always cleared.  */
    fd_set writefds; /* All bits except fd[1] are always cleared.  */
# endif
    bool done_writing;

    /* Enable non-blocking I/O.  This permits the read() and write() calls
       to return -1/EAGAIN without blocking; this is important for polling
       if HAVE_SELECT is not defined.  It also permits the read() and write()
       calls to return after partial reads/writes; this is important if
       HAVE_SELECT is defined, because select() only says that some data
       can be read or written, not how many.  Without non-blocking I/O,
       Linux 2.2.17 and BSD systems prefer to block instead of returning
       with partial results.  */
    {
      int fcntl_flags;

      if ((fcntl_flags = fcntl (fd[1], F_GETFL, 0)) < 0
          || fcntl (fd[1], F_SETFL, fcntl_flags | O_NONBLOCK) == -1
          || (fcntl_flags = fcntl (fd[0], F_GETFL, 0)) < 0
          || fcntl (fd[0], F_SETFL, fcntl_flags | O_NONBLOCK) == -1)
        {
          if (exit_on_error)
            error (EXIT_FAILURE, errno,
                   _("cannot set up nonblocking I/O to %s subprocess"),
                   progname);
          goto fail;
        }
    }

# if HAVE_SELECT
    FD_ZERO (&readfds);
    FD_ZERO (&writefds);
# endif
    done_writing = false;
    for (;;)
      {
# if HAVE_SELECT
        int n, retval;

        FD_SET (fd[0], &readfds);
        n = fd[0] + 1;
        if (!done_writing)
          {
            FD_SET (fd[1], &writefds);
            if (n <= fd[1])
              n = fd[1] + 1;
          }

        /* Do EINTR handling here instead of in pipe-filter-aux.h,
           because select() cannot be referred to from an inline
           function on AIX 7.1.  */
        do
          retval = select (n, &readfds, (!done_writing ? &writefds : NULL),
                           NULL, NULL);
        while (retval < 0 && errno == EINTR);
        n = retval;

        if (n < 0)
          {
            if (exit_on_error)
              error (EXIT_FAILURE, errno,
                     _("communication with %s subprocess failed"), progname);
            goto fail;
          }
        if (!done_writing && FD_ISSET (fd[1], &writefds))
          goto try_write;
        if (FD_ISSET (fd[0], &readfds))
          goto try_read;
        /* How could select() return if none of the two descriptors is ready?  */
        abort ();
# endif

        /* Attempt to write.  */
# if HAVE_SELECT
      try_write:
# endif
        if (!done_writing)
          {
            size_t bufsize;
            const void *buf = prepare_write (&bufsize, private_data);
            if (buf != NULL)
              {
                /* Writing to a pipe in non-blocking mode is tricky: The
                   write() call may fail with EAGAIN, simply because sufficient
                   space is not available in the pipe. See POSIX:2008
                   <https://pubs.opengroup.org/onlinepubs/9699919799/functions/write.html>.
                   This happens actually on AIX and IRIX, when bufsize >= 8192
                   (even though PIPE_BUF and pathconf ("/", _PC_PIPE_BUF) are
                   both 32768).  */
                size_t attempt_to_write =
                  (bufsize > SSIZE_MAX ? SSIZE_MAX : bufsize);
                for (;;)
                  {
                    ssize_t nwritten = write (fd[1], buf, attempt_to_write);
                    if (nwritten < 0)
                      {
                        if (errno == EAGAIN)
                          {
                            attempt_to_write = attempt_to_write / 2;
                            if (attempt_to_write == 0)
                              break;
                          }
                        else if (!IS_EAGAIN (errno))
                          {
                            if (exit_on_error)
                              error (EXIT_FAILURE, errno,
                                     _("write to %s subprocess failed"),
                                     progname);
                            goto fail;
                          }
                      }
                    else
                      {
                        if (nwritten > 0)
                          done_write ((void *) buf, nwritten, private_data);
                        break;
                      }
                  }
              }
            else
              {
                /* Tell the child there is nothing more the parent will send.  */
                close (fd[1]);
                done_writing = true;
              }
          }
# if HAVE_SELECT
        continue;
# endif

        /* Attempt to read.  */
# if HAVE_SELECT
      try_read:
# endif
        {
          size_t bufsize;
          void *buf = prepare_read (&bufsize, private_data);
          if (!(buf != NULL && bufsize > 0))
            /* prepare_read returned wrong values.  */
            abort ();
          {
            ssize_t nread =
              read (fd[0], buf, bufsize > SSIZE_MAX ? SSIZE_MAX : bufsize);
            if (nread < 0)
              {
                if (!IS_EAGAIN (errno))
                  {
                    if (exit_on_error)
                      error (EXIT_FAILURE, errno,
                             _("read from %s subprocess failed"), progname);
                    goto fail;
                  }
              }
            else if (nread > 0)
              done_read (buf, nread, private_data);
            else /* nread == 0 */
              {
                if (done_writing)
                  break;
              }
          }
        }
# if HAVE_SELECT
        continue;
# endif
      }
  }

  /* Restore SIGPIPE signal handler.  */
  if (sigaction (SIGPIPE, &orig_sigpipe_action, NULL) < 0)
    abort ();
#endif

  close (fd[0]);

  /* Remove zombie process from process list.  */
  {
    int exitstatus =
      wait_subprocess (child, progname, false, null_stderr,
                       true, exit_on_error, NULL);
    if (exitstatus != 0 && exit_on_error)
      error (EXIT_FAILURE, 0, _("%s subprocess terminated with exit code %d"),
             progname, exitstatus);
    return exitstatus;
  }

 fail:
  {
    int saved_errno = errno;
    close (fd[1]);
#if !((defined _WIN32 && ! defined __CYGWIN__) || defined __KLIBC__)
    if (sigaction (SIGPIPE, &orig_sigpipe_action, NULL) < 0)
      abort ();
#endif
    close (fd[0]);
    wait_subprocess (child, progname, true, true, true, false, NULL);
    errno = saved_errno;
    return -1;
  }
}
