/*
**  L2 - OSSP Logging Library
**  Copyright (c) 2001 The OSSP Project (http://www.ossp.org/)
**  Copyright (c) 2001 Cable & Wireless Deutschland (http://www.cw.com/de/)
**
**  This file is part of OSSP L2, a flexible logging library which
**  can be found at http://www.ossp.org/pkg/l2/.
**
**  Permission to use, copy, modify, and distribute this software for
**  any purpose with or without fee is hereby granted, provided that
**  the above copyright notice and this permission notice appear in all
**  copies.
**
**  THIS SOFTWARE IS PROVIDED ``AS IS'' AND ANY EXPRESSED 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 THE AUTHORS AND COPYRIGHT HOLDERS AND THEIR
**  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.
**
**  l2_ch_pipe.c: pipe channel implementation
*/

#include "l2.h"
#include "l2_p.h"              /* for TRACE() */

#include <unistd.h>
#include <signal.h>
#include <sys/wait.h>
#include <fcntl.h>

#define L2_PIPE_EXECMODE_DIRECT    1 /* direct command execution             */
#define L2_PIPE_EXECMODE_SHELL     2 /* shell  command execution             */
#define L2_PIPE_RUNTIME_CONTINU    3 /* continuous pipe command processing   */
#define L2_PIPE_RUNTIME_ONESHOT    4 /* oneshot  pipe command processing     */
#define L2_PIPE_WRITEFAIL          6 /* how long before failure occurs       */
#define L2_PIPE_MAXARGS          256 /* how many args can piped command have */

/* declare private channel configuration */
typedef struct {
    pid_t            Pid;        /* process id of child command          */
    int              iWritefail; /* counter to failed write() operations */
    int              piFd[2];    /* pipe file descriptor                 */
    int              iNulldev;   /* null device file descriptor          */
    int              iMode;      /* execution mode direct or shell       */
    int              iRtme;      /* runtime mode continuous or oneshot   */
    char            *szCmdpath;  /* path to command and arguments        */
    struct sigaction sigchld;    /* initial state of chld signal handler */
    struct sigaction sigpipe;    /* initial state of pipe signal handler */
} l2_ch_pipe_t;

static void catchsignal(int sig, ...)
{
    pid_t Pid;         /* for wait() */
    int   iStatus = 0; /* for wait() */

    if (sig == SIGCHLD) {
        TRACE("SIGCHLD caught");
        Pid = waitpid(-1, &iStatus, WUNTRACED | WNOHANG);
        if (WIFEXITED(iStatus))
            TRACE("EXITED child");     /* child finished and returned       */
        else if (WIFSIGNALED(iStatus))
            TRACE("SIGNALED child");   /* child finished due to a signal    */
        else if (WIFSTOPPED(iStatus))
            TRACE("STOPPED child");    /* child stopped due to a signal     */
        else
            TRACE("SIGNAL Unknown");   /* child stopped due to a signal     */
    }
    else if (sig == SIGPIPE);          /* noop for now                      */
}

/* 
 * The lifecycle of the command executed by the pipe channel handler
 * depends on the runtime option selected during configuration. The
 * option 'continuous' describes subsequent behavior which rebuilds an
 * (un)intentionally severed child-controlled pipe. In this case, when
 * the pipe handler encounters a failed write operation, it will attempt
 * the reconnection a user-defined number of times until failure is assumed.
 *
 * The option 'oneshot' describes subsequent behavior which also rebuilds
 * such a severed pipe. The difference is that the pipe handler will not
 * spawn the child process until writing actually occurs. The channel will
 * also block until the child process finishes and terminates on its own.
 *
 * The lifecycle of a pipe command process is illustrated in the
 * following figure:
 *
 * -----BEGIN EMBEDDED OBJECT-----
 * Content-editor: xfig %s
 * Content-encoding: gzip:9 base64
 * Content-type: application/fig
 * Content-viewer: xfig %s
 * Description: L2 Pipe Channel Command Lifecycle
 * Filename: l2_ch_pipe.fig
 * Last-modified: 2000-09-28/14:40
 * Name: l2_ch_pipe
 * Version: eo/1.0
 * H4sIAIW5uTsCA61ZUW/cNgx+3v0KAX3YCjSBJEuy/VxsQ4EMG9bubS+Oo+SM+uyb
 * 7WuWfz+Ssi352iTSrW3jir7jJ0qk+FHMm18+/Mqya7m7qbq7sa6OdvfedpMddr/Z
 * aWjq3Y2dQGJsJzi/5nz3sekeWru7kjshOWdyx1km2Zua/uwMk0ZqprjmTPAi10yD
 * vBMsY/ACfjjT+JDwADR6h//DmxweWpWcGYn/vCxzAyPN8xgYmJS7b884wQsYEpJU
 * MUiZ2Rrk5cwkGKTVFsbLWiXAKL2F8bLSCTC53MJ4OZcJMIXYwni5EAkwJa1MqAXG
 * yyU3ONJxMOUZzCqXZQKMybYwgZwtMJIgFPzkDuZqVmeLgNBy90MQvz5wQqct2KrU
 * 4Qg/Be0wSlYrch2OZq/xcISfgnZulm/SaHYMD0f0aaH1ut9eu1Tr22AjF+35PKGZ
 * VwbOe1nCIRcKDr2QGQwlYO6U32fUWIOYiQy+V9A55TjUoPFXdwcJpuuHQ9Wyuhnq
 * 02Gcqq6247u/ORcvg0nhwRC3YjeS/dEcLXu/r7rOtuyxmfZssG1T3TZtMz3FYGYL
 * puSwsOk0dPaO9R1gtS1YOrHOwoupZ1P12bKqew2UA5Is9QIq4Tt1Pwy2npovoA//
 * ATjAHaqmm+CHNdPI6n3T3sVYm6/ACoZ1fzhALmfHoYctHK/Zz/9Wh2Nr2bjvH7tr
 * AvSeUxK1pcFMDZH2iufyfNkYpeBD57nxaOvm/7lOGXh8F9fxYtkMhcn+zHWDtWDf
 * bduMe9riI8wWAZrpBVQj/t1pABIEe+/b6hHQH4dmsqw/2qEiR96dLDgzAlcXywbo
 * TGDs1m0/AiB5njXd8TSRic/7kEIpk3j6BCUcPH0uSQlWYJISzyUpzFFyyQ4zjopQ
 * Fk4ZB+Ia0yZ+CiFE1UGIOVuEmQJB8S9UCh5VPmsSpRynTjnJLdVQjqFgx8JCFrDU
 * 2MKCHkFhMT8MPkRCYYHf3hQW9MIVFjLPVDSVBwZ5mag81iCiJwijDV05GelKZCae
 * ygNrvExUHmsNkVwA42WqlmJhiO2CRXmZqqXYRREZBtZ4mcqcWGuIFQNrvEz1Saw1
 * RJ4hzCpTfUIwUYWFEEthMe+sD5zNaI6CzQg/Be3Qvd6KzYg+XaKbz6fPpSlT4MEE
 * itegrvRr+d0s6c0g1XzaW3YagTAO1RNk4n9OzQBZs7NXkNSm2FQMs86Y5NNbu6++
 * NP1wzT4Age4bYM1qtO9gaCGLEov8OEbASr7CYilTjWMPnDZhLp7pdKX+8Vg9dhGQ
 * rlgiSExXt21ffx7ZqZuadjYPk/zM0gzuWIemgxmjrDWLteTE+6E/EJ0BN7C2f2jq
 * bxLGKxynVnOxZnR2IrMh1d3ah6aDUuKT31ZcQ3dGRFg4urQt81wn8pCL6xnm+9CQ
 * i2SxHJhUGqIj4dRxOC8Ua6wZGCsvWcrNORBmu7eQDhT8SI2PAgi/Hiy4+dwj52p4
 * bCHUMyqpSa3v7puH0/CqJsyTgabKzKwJhUkXqURlCClRTROpZeSiRfVL5NIoJ1y0
 * tLy8YGlFecnSynyZKnZpGqsUbhZv30HBOfRPwUHBQNAaq2lBd+VcRMePNvkl8ePU
 * Lokf0kyNH1JK2WRyTbIWucYtLdk1pBa6JjLIk42kIE/WogA/03IsTMaXBZX7eJWH
 * oojwihBPXOucFwEe0ZvBEdx+Aiue16Kajec4MHzhwG/rbazHykAInA5NY8MJWLk5
 * fL3u4mvnoCYur8jATaDYAfG8pocHA3mL9ARsCBB0ylzknvO5Cq8nA72Cu9yvS+oI
 * wWTLdfV3qGE+Yg1z09zb+qluzz2m+OIwzAmvTJJht4AmAXJf5/iTbsFA6Osc7CfX
 * Mnkb4c41CPDMvBADz/iSMtklvpS5usiX0ohkX4qsfN6XL0Q4VrqbCPd+y0S5OA5P
 * Y6z1dGkJrY9wTIbrSD6dWZFfdjopuVziUSWKlBOTcTUHM11WXormweIV4G2KMUSj
 * oTGeYSmLOIYtNI9mWMpa6Qzr1C5hWNJMZVhSSq7QnJGpFdpFSyMCS10alQHJS6My
 * IGlpvgwgta8rNKz9DV+vDhhKiXeZQhZ8wcm+02WGMGeLYJh6mTFlNt/vcbS01GBv
 * 3J2eF1hOZHl8S43sCVpqbs3YUitkmdBSw29vWmr0wrXUikzE/64uMMjL1H2KNYia
 * JwGMl6kzFwtD+T+A8TI1sWJhqMERwHiZmlixMNS0CmC8TE2saJjyDGaVqYkVC0MN
 * iwDGy9RujIWhlhaFdND8dDI2P02pYltq6+/q5nDxHvdO24zcLPl8pECbxvOG8nDk
 * jpzS4cid4jnOCzqH/wEp6AKCgx8AAA==
 * -----END EMBEDDED OBJECT-----
 */

/* create channel */
static l2_result_t hook_create(l2_context_t *ctx, l2_channel_t *ch)
{
    l2_ch_pipe_t *cfg;

    /* allocate private channel configuration */
    if ((cfg = (l2_ch_pipe_t *)malloc(sizeof(l2_ch_pipe_t))) == NULL)
        return L2_ERR_ARG;

    /* initialize configuration with reasonable defaults */
    cfg->Pid        = -1;
    cfg->iWritefail =  0; 
    cfg->piFd[0]    = -1; 
    cfg->piFd[1]    = -1; 
    cfg->iNulldev   = -1; 
    cfg->iMode      = -1; 
    cfg->iRtme      = -1; 
    cfg->szCmdpath  = NULL;
    memset(&cfg->sigchld, 0, sizeof(cfg->sigchld));
    memset(&cfg->sigpipe, 0, sizeof(cfg->sigpipe));

    /* link private channel configuration into channel context */
    ctx->vp = cfg;

    return L2_OK;
}

/* configure channel */
static l2_result_t hook_configure(l2_context_t *ctx, l2_channel_t *ch, const char *fmt, va_list ap)
{
    l2_ch_pipe_t *cfg = (l2_ch_pipe_t *)ctx->vp;
    l2_param_t pa[4]; 
    l2_result_t rv;
    char *szMode = NULL;
    char *szRel  = NULL;

    /* feed and call generic parameter parsing engine */
    L2_PARAM_SET(pa[0], execmode, CHARPTR, &szMode); /* mode direct or shell  */
    L2_PARAM_SET(pa[1], runtime,  CHARPTR, &szRel);  /* continuous or oneshot */
    L2_PARAM_SET(pa[2], path,     STRING,  &cfg->szCmdpath); /* path of cmd   */
    L2_PARAM_END(pa[3]);
    if ((rv = l2_util_setparams(pa, fmt, ap)) != L2_OK)
        return rv;

    if (szMode != NULL) {
        if (strcmp(szMode, "direct") == 0)
            cfg->iMode = L2_PIPE_EXECMODE_DIRECT;
        else if (strcmp(szMode, "shell") == 0)
            cfg->iMode = L2_PIPE_EXECMODE_SHELL;
        else
            return L2_ERR_ARG;
    }

    if (szRel != NULL) {
        if (strncmp(szRel, "continuous", strlen("cont")) == 0)
            cfg->iRtme = L2_PIPE_RUNTIME_CONTINU;
        else if (strncmp(szMode, "oneshot", strlen("one")) == 0)
            cfg->iRtme = L2_PIPE_RUNTIME_ONESHOT;
        else
            return L2_ERR_ARG;
    }

    return L2_OK;
}

/**********************************************************
 * parse_cmdpath: Helper method to spawn_command          *
 *   Parses szBuf into an argv-style string vector szArgs *
 **********************************************************/
static l2_result_t parse_cmdpath(char *szBuf, char *szArgs[]) {
    int iCnt = 0;

    if (szBuf == NULL)     /* check for bad input before we  */
        return L2_ERR_ARG; /* dereference and throw a SIGSEV */

    while ((iCnt++ < L2_PIPE_MAXARGS) && (*szBuf != '\0')) {
        while ((*szBuf == ' ') || (*szBuf == '\t'))
            *szBuf++ = '\0'; /* overwrite whitespace with EOL  */
        *szArgs++ = szBuf;   /* found the start of a new token */
        while ((*szBuf != '\0') && (*szBuf != ' ') && (*szBuf != '\t'))
            szBuf++;
    }
    *szArgs = '\0'; /* add a NULL to mark the end of the chain */

    if (iCnt <= L2_PIPE_MAXARGS)
        return L2_OK;
    else
        return L2_ERR_ARG;
}

/************************************************************
 * spawn_command: Helper method to hook_open and hook_write *
 *   Forks a new process, and copies the command executable *
 ************************************************************/
static l2_result_t spawn_command(l2_ch_pipe_t *cfg)
{
    char *pVec[L2_PIPE_MAXARGS];
    char *sz = NULL;
    l2_result_t rv;

    /* initialize auto vars before using them */
    memset(pVec, 0, sizeof(pVec));

    /* spawn a child process to be later overwritten by the user command  */
    if ((cfg->Pid = fork()) > 0) {            /* parent process           */
        free(sz);                             /* no exec() in parent      */
        close(cfg->piFd[0]);                  /* half-duplex (no reading) */
        cfg->piFd[0] = -1;
        return L2_OK;
    }
    else if (cfg->Pid == 0) {                 /* child process            */
        close(cfg->piFd[1]);                  /* close the writing end,   */
        cfg->piFd[1] = -1;                    /* because we don't use it  */
        dup2(cfg->piFd[0], fileno(stdin));    /* copy the reading end     */

        /* redirection of child's stdout and stdin */
        cfg->iNulldev = open("/dev/null", O_RDWR);
        dup2(cfg->iNulldev, fileno(stdout));      /* redirect stdout to null  */
        dup2(cfg->iNulldev, fileno(stderr));      /* redirect stderr to null  */

        /* the distinction between modes is necessary, because only executing */
        /* commands in a shell environment allows usage of variables and such */
        if (cfg->iMode == L2_PIPE_EXECMODE_SHELL) {
            pVec[0] = "/bin/sh";
            pVec[1] = "-c";
            pVec[2] = cfg->szCmdpath;
            pVec[3] = NULL;        /* add a NULL to mark the end of the chain */
        }
        else { /* plain direct command execution */
            sz = strdup(cfg->szCmdpath);
            if ((rv = parse_cmdpath(sz, pVec)) != L2_OK) {
                free(sz);
                return rv;
            }
        }

        if (execvp(*pVec, pVec) == -1) {      /* launch                   */
            TRACE("execvp in child returned -1");
            free(sz);                         /* cleanup in case we fail  */
            close(cfg->piFd[0]);
            cfg->piFd[0] = -1; /* if execvp() doesn't swap our context or */
            return L2_ERR_SYS; /* if child returns, we have an error      */
        }
        else
            return L2_OK; /* NOTREACHED */
    }
    else /* fork failed  */
        return L2_ERR_SYS;
}

/* open channel */
static l2_result_t hook_open(l2_context_t *ctx, l2_channel_t *ch)
{
    l2_ch_pipe_t *cfg = (l2_ch_pipe_t *)ctx->vp;
    struct sigaction locact;

    /* consistency check */
    if (cfg->szCmdpath == NULL)
        return L2_ERR_USE;

    /* initialize auto vars before using them */
    memset(&locact, 0, sizeof(locact));

    locact.sa_handler = (void(*)())catchsignal;
    sigemptyset(&locact.sa_mask);
    locact.sa_flags = 0;

    /* save old signal context before replacing with our own */
    if (sigaction(SIGCHLD, &locact, &cfg->sigchld) < 0)
        return L2_ERR_SYS;
    if (sigaction(SIGPIPE, &locact, &cfg->sigpipe) < 0)
        return L2_ERR_SYS;

    if (pipe(cfg->piFd) == -1) /* open the pipe */
        return L2_ERR_SYS;

    /* short circuit hack, if in oneshot mode and not yet opened then return */
    if ((cfg->iRtme == L2_PIPE_RUNTIME_ONESHOT) && (ch->state != L2_CHSTATE_OPENED))
        return L2_OK;
    else
        return spawn_command(cfg); /* spawn the command process */
}

/* write to channel, possibly recursively */
static l2_result_t hook_write(l2_context_t *ctx, l2_channel_t *ch,
                              l2_level_t level, const char *buf, size_t buf_size)
{
    l2_result_t   rv      = L2_OK;
    l2_ch_pipe_t *cfg     = (l2_ch_pipe_t *)ctx->vp;

    /* spawn the child command process if we are in oneshot mode */
    if ((cfg->iRtme == L2_PIPE_RUNTIME_ONESHOT) && (cfg->Pid == -1))
        if (spawn_command(cfg) != L2_OK)
            return L2_ERR_SYS; /* immediate return if we can't spawn command */

    /* write message to channel pipe */
    if (write(cfg->piFd[1], buf, buf_size) == -1) {
        if ((errno == EPIPE) && (cfg->iWritefail++ < L2_PIPE_WRITEFAIL)) {
            if ((rv = l2_channel_close(ch)) != L2_OK)
                return rv;
            if ((rv = l2_channel_open(ch)) != L2_OK)
                return rv;
            return hook_write(ctx, ch, level, buf, buf_size);
        }
        else { /* not broken pipe problem or over the fail limit, so panic */
            cfg->iWritefail = 0;             /* reset pipe failure counter */
            rv = L2_ERR_SYS;
        }
    }
    else                     /* write() to pipe succeeded  */
        cfg->iWritefail = 0; /* reset pipe failure counter */

    /* block until child terminates if in oneshot execmode */
    if ((cfg->iRtme == L2_PIPE_RUNTIME_ONESHOT) && (cfg->Pid != -1))
        cfg->Pid = waitpid(cfg->Pid, NULL, WUNTRACED | WNOHANG);

    return rv;
}

/* close channel */
static l2_result_t hook_close(l2_context_t *ctx, l2_channel_t *ch)
{
    l2_result_t   rv      = L2_OK;
    l2_ch_pipe_t *cfg     = (l2_ch_pipe_t *)ctx->vp;

    /* close null device */
    if (cfg->iNulldev != -1) {
        close(cfg->iNulldev);
        cfg->iNulldev = -1;
    }

    /* close output pipe for parent */
    if (cfg->piFd[1] != -1) {
        close(cfg->piFd[1]);
        cfg->piFd[1] = -1;
    }

    /* restore previous signal context, but only if it was saved and replaced */
    if (&cfg->sigchld.sa_handler) {
        if (sigaction(SIGCHLD, &cfg->sigchld, 0) < 0)
            rv = L2_ERR_SYS;
        if (sigaction(SIGPIPE, &cfg->sigpipe, 0) < 0)
            rv = L2_ERR_SYS;
    }

    /* kill child process if still running */
    if (cfg->Pid != -1) {
        kill(cfg->Pid, SIGTERM);
        cfg->Pid = waitpid(cfg->Pid, NULL, WUNTRACED | WNOHANG);
        cfg->Pid = -1;
    }

    return rv;
}

/* destroy channel */
static l2_result_t hook_destroy(l2_context_t *ctx, l2_channel_t *ch)
{
    l2_ch_pipe_t *cfg = (l2_ch_pipe_t *)ctx->vp;

    /* destroy channel configuration */
    free(cfg->szCmdpath);
    cfg->szCmdpath = NULL;
    free(cfg);

    return L2_OK;
}

/* exported channel handler structure */
l2_handler_t l2_handler_pipe = {
    L2_CHANNEL_OUTPUT,
    hook_create,
    hook_configure,
    hook_open,
    hook_write,
    NULL,
    hook_close,
    hook_destroy
};

