/*
 * w32pipe:  win32 clone of npopen.c, utilizes native pipes (not temp files).
 * Written by Clark Morgan for vile (december 1997).
 *
 * Background
 * ==========
 * The techniques used in w32_npclose() and w32_inout_popen() are derived
 * from much trial and error and support "pipe" I/O in both a console and
 * GUI environment.  You may _think_ you have a better way of effecting the
 * functionality provided in this module and that may well be the case.
 * But be sure you test your new code with at least these versions of Win32:
 *
 *      win95 (original version), OSR2, NT 4.0
 *
 * For each HOST, be sure to test read pipes, write pipes, and filters (and
 * test repeatedly within the same vile session).
 *
 *
 * Acknowledgments
 * ===============
 * Until I read Steve Kirkendall's code for the Win32 version of elvis, I
 * did not realize that attempting to redirect stdin to a device is a
 * _not_ a good strategy.
 *
 *
 * Caveats
 * =======
 *
 * -- This code has not been tested with NT 3.51 .
 *
 * -- The MSDN Knowledge Base has example code that uses anonymous pipes
 *    to redirect a spawned process's stdin, stdout, and stderr.  Don't go
 *    there.
 *
 * -- The original Win95 console shell (command.com) accesses the floppy
 *    drive each and every time a process communicates with it via a pipe
 *    and the OS R2 shell abruptly hangs under similar conditions.  By
 *    default, then, on a WinNT host, vile's pipes are implemented using
 *    native pipes (i.e., with the code in this module), while Win95 hosts
 *    fall back to temp file communication.  If the user's replacement
 *    Win95 shell does not exhibit communication problems similar to
 *    those described above (e.g., Thompson Toolkit Shell), vile may be
 *    forced to use native Win32 pipes by setting the global mode
 *    "w32pipes" (e.g., "se w32pipes").  For further details, including
 *    a description of "force-console" mode, refer to the file
 *    doc/w32modes.doc .
 *
 * -- This module's native pipes implementation exhibits various problems
 *    when a 16-bit console app is exec'd.  On a win95 host, the editor
 *    and shell generally hang.  WinNT does better, but winvile creates
 *    "background" shell windows that require manual closure.
 *
 * -- This module configures read pipes so that the exec'd app reads
 *    it's input from an empty file.  That's a necessity, not a bug.
 *    Consequently, if an attempt is made to read data from an app
 *    that itself reads input (why would you do that?), the app will
 *    appear to hang if it reopens stdin on the console (because vile's
 *    stdin is not available to the app--another necessity).  In this
 *    situation, kill the app by typing ^C (and then please apply for a
 *    QA position with a certain Redmond company).
 *
 * $Header: /usr/build/vile/vile/RCS/w32pipe.c,v 1.15 1998/09/23 00:13:34 tom Exp $
 */

#include <windows.h>
#include <io.h>
#ifdef __BORLANDC__
#define __MSC
#include <sys/stat.h>
#endif
#include <share.h>
#include <process.h>
#include <assert.h>
#include <ctype.h>

#define HAVE_FCNTL_H 1

#include "estruct.h"
#include "edef.h"

#define BAD_FD          (-1)
#define BAD_PROC_HANDLE (INVALID_HANDLE_VALUE)
#define PIPESIZ         (4096)
#define SHELL_ERR_MSG   \
          "error: shell process \"%s\" failed, check COMSPEC env var\n"

static HANDLE proc_handle;
static char   *tmpin_name;

/* ------------------------------------------------------------------ */

static void
global_cleanup(void)
{
    if (tmpin_name)
    {
        (void) remove(tmpin_name);
        (void) free(tmpin_name);
        tmpin_name = NULL;
    }
    restore_console_title();
#if DISP_NTWIN
    if (global_g_val(GMDFORCE_CONSOLE))
        FreeConsole();
#endif
}



static HANDLE
exec_shell(char *cmd, HANDLE *handles, int hide_child)
{
    char                 *cmdstr;
#if DISP_NTWIN
    HWND                 fgnd;
    int                  force_console = global_g_val(GMDFORCE_CONSOLE);
#endif
    int                  freestr;
    PROCESS_INFORMATION  pi;
    STARTUPINFO          si;

    proc_handle = BAD_PROC_HANDLE;  /* in case of failure */
    if ((cmdstr = mk_shell_cmd_str(cmd, &freestr, TRUE)) == NULL)
    {
        /* heap exhausted! */

        mlforce("insufficient memory to invoke shell");

        /* Give user a chance to read message--more will surely follow. */
        Sleep(3000);
        return (proc_handle);
    }

    memset(&si, 0, sizeof(si));
    si.cb          = sizeof(si);
    si.dwFlags     = STARTF_USESTDHANDLES;
    si.hStdInput   = handles[0];
    si.hStdOutput  = handles[1];
    si.hStdError   = handles[2];
#if DISP_NTWIN
    if (force_console)
    {
        if (hide_child)
            fgnd = GetForegroundWindow();
        AllocConsole();
    }
    else if (hide_child)
    {
        si.dwFlags     |= STARTF_USESHOWWINDOW;
        si.wShowWindow  = SW_HIDE;
    }
#endif
    if (CreateProcess(NULL,
                      cmdstr,
                      NULL,
                      NULL,
                      TRUE,       /* Inherit handles */
#if DISP_NTWIN
                      force_console ? 0 : CREATE_NEW_CONSOLE,
#else
                      0,
#endif
                      NULL,
                      NULL,
                      &si,
                      &pi))
    {
        /* Success */

#if DISP_NTWIN
        if (force_console && hide_child)
            SetForegroundWindow(fgnd);
#endif
        CloseHandle(pi.hThread);
        proc_handle = pi.hProcess;
    }
    if (freestr)
        free(cmdstr);
    return (proc_handle);
}



int
w32_inout_popen(FILE **fr, FILE **fw, char *cmd)
{
    HANDLE handles[3];
    int    i, rc, rp[2], tmpin_fd, wp[2];

    proc_handle  = BAD_PROC_HANDLE;
    rp[0]        = rp[1]      = wp[0]      = wp[1] = BAD_FD;
    handles[0]   = handles[1] = handles[2] = INVALID_HANDLE_VALUE;
    tmpin_fd     = BAD_FD;
    tmpin_name   = NULL;
    set_console_title(cmd);
    if (is_win95())
    {
        char *cmdp;

        /*
         * If w32pipes is set on a win95 host, you don't ever want slowreadf()
         * to periodically update the display while an intrinisic, high
         * bandwidth DOS command is in progress.  Reason:  the win95 shell,
         * command.com, will simply hang in the following scenario:
         *
         *    ^X!<high_bandwidth_cmd>
         *
         *         and
         *
         *    PIPESIZ < output of <high_bandwidth_cmd>
         *
         * I'm assuming that what's going on here is that command.com is
         * written in ASM and is using very low level BIOS/DOS calls to
         * effect output and, furthermore, that these low level calls don't
         * block when the input consumer is busy.
         */

        for (cmdp = cmd; *cmdp && isspace(*cmdp); cmdp++)
            ;
        nowait_pipe_cmd = (strnicmp(cmdp, "dir", 3) == 0)  ||
                          (strnicmp(cmdp, "type", 4) == 0);
    }
    do
    {
        if (fr)
        {
            *fr = NULL;

            /*
             * Open (parent's) input pipe in TEXT mode, which will force
             * translation of the child's CR/LF record delimiters to NL
             * and keep the dreaded ^M chars from temporarily appearing
             * in a vile buffer (ugly).
             */
            if (_pipe(rp, PIPESIZ, O_TEXT|O_NOINHERIT) == -1)
                break;
            if (! DuplicateHandle(GetCurrentProcess(),
                                  (HANDLE) _get_osfhandle(rp[1]),
                                  GetCurrentProcess(),
                                  handles + 1,
                                  0,
                                  TRUE,
                                  DUPLICATE_SAME_ACCESS|DUPLICATE_CLOSE_SOURCE))
            {
                break;
            }
            handles[2] = handles[1];
            rp[1]      = BAD_FD;   /* closed by DuplicateHandle() */
            if (! fw)
            {
                /*
                 * This is a read pipe (only).  Connect child's stdin to
                 * an empty file.  Under no circumstances should the
                 * child's stdin be connected to a device (else lots of
                 * screwy things will occur).  In particular, connecting
                 * the child's stdin to the parent's stdin will cause
                 * aborts and hangs on the various Win32 hosts.  You've
                 * been warned.
                 */

                if ((tmpin_name = _tempnam(getenv("TEMP"), "vile")) == NULL)
                    break;
                if ((tmpin_fd = open(tmpin_name,
                                     O_RDONLY|O_CREAT|O_TRUNC,
                                     _S_IWRITE|_S_IREAD)) == BAD_FD)
                {
                    break;
                }
                handles[0] = (HANDLE) _get_osfhandle(tmpin_fd);
            }
            if (! (*fr = fdopen(rp[0], "r")))
                break;
        }
        if (fw)
        {
            *fw = NULL;

            /*
             * Open (child's) output pipe in binary mode, which will
             * prevent translation of the parent's CR/LF record delimiters
             * to NL.  Apparently, many apps want those delimiters :-) .
             */
            if (_pipe(wp, PIPESIZ, O_BINARY|O_NOINHERIT) == -1)
                break;
            if (! DuplicateHandle(GetCurrentProcess(),
                                  (HANDLE) _get_osfhandle(wp[0]),
                                  GetCurrentProcess(),
                                  handles + 0,
                                  0,
                                  TRUE,
                                  DUPLICATE_SAME_ACCESS|DUPLICATE_CLOSE_SOURCE))
            {
                break;
            }
            wp[0] = BAD_FD;     /* closed by DuplicateHandle() */
            if (! fr)
                handles[1] = handles[2] = GetStdHandle(STD_OUTPUT_HANDLE);
            if (! (*fw = fdopen(wp[1], "w")))
                break;
        }
        rc = (exec_shell(cmd,
                         handles,
                         fr != NULL  /* Child wdw hidden unless write pipe. */
                         ) == BAD_PROC_HANDLE) ? FALSE : TRUE;
        if (fw)
        {
            if (! rc)
            {
                 /* Shell process failed, put complaint in user's face. */

                fputc('\n', stdout);
                printf(SHELL_ERR_MSG, get_shell());
                fflush(stdout);
            }
            CloseHandle(handles[0]);
        }
        if (fr)
        {
            if (! rc)
            {
                char  buf[200];
                DWORD dummy, len;

                /* Shell process failed, put complaint in user's buffer. */

                len = lsprintf(buf, SHELL_ERR_MSG, get_shell()) - buf;
                (void) WriteFile(handles[1], buf, len, &dummy, NULL);
                FlushFileBuffers(handles[1]);
            }
            CloseHandle(handles[1]);
            if (tmpin_fd != BAD_FD)
                close(tmpin_fd);
        }
        return (rc);
    }
    while (FALSE);

    /* If we get here -- some operation has failed.  Clean up. */

    if (wp[0] != BAD_FD)
        close(wp[0]);
    if (wp[1] != BAD_FD)
        close(wp[1]);
    if (rp[0] != BAD_FD)
        close(rp[0]);
    if (rp[1] != BAD_FD)
        close(rp[1]);
    if (tmpin_fd != BAD_FD)
        close(tmpin_fd);
    for (i = 0; i < 3; i++)
    {
        if (handles[i] != INVALID_HANDLE_VALUE)
            CloseHandle(handles[i]);
    }
    global_cleanup();
    return (FALSE);
}



void
w32_npclose(FILE *fp)
{
    int term_status;

    (void) fflush(fp);
    (void) fclose(fp);
    if (proc_handle != BAD_PROC_HANDLE)
    {
        (void) _cwait(&term_status, (int) proc_handle, 0);
        (void) CloseHandle(proc_handle);
        proc_handle = BAD_PROC_HANDLE;
    }
    global_cleanup();
}
