/* pggexe.c - GnuPG execution
 *      Copyright (C) 1999 Michael Roth <mroth@gnupg.org>
 *
 * This file is part of PGG (Privacy Guard Glue).
 *
 * PGG 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 2 of the License, or
 * (at your option) any later version.
 *
 * PGG 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, write to the Free Software
 * Foundation, Inc., 59 Temple Place - Suite 330, Boston, MA 02111-1307, USA
 */


#include <includes.h>
#include <pgg.h>
#include <pggdebug.h>
#include <pggexe.h>


/*
 * Type casts
 */
#define exe		((PggExePtr)(_exe))
#define old_exe		((PggExePtr)(_old_exe))



#define DEFAULT_BUF_SIZE	2048


#if 0
#define ASSERT_CONSISTENCE(exe)						\
    {	int i;								\
	assert(exe);							\
	assert(exe->argv);						\
	assert(exe->argv_used >= 0);					\
	assert(exe->argv_capacity > 0);					\
	assert(exe->argv_used <= exe->argv_capacity);			\
	for(i=0; i<exe->argv_used; ++i)					\
	    assert(exe->argv[i]);					\
	assert(exe->state >= 0);					\
	assert(exe->state <= 2);					\
	assert(exe->status_buffer_size >= 0);				\
	assert(exe->status_buffer_used >= 0);				\
	if (exe->status_buffer_size) {					\
	    assert(exe->status_buffer);					\
	    assert(exe->status_arg);					\
	}								\
    }
#endif




/*
 *  TODO
 * ======
 * 
 * 1.) Change callbacks that they have events for abort and release.
 *     Possible only one callback with different callback events should
 *     be nice enough.
 *
 * 2.) Use only one buffer for read and write callbacks. Don't store the
 *     length of the buffer in the data structure.
 *
 * 3.) Rename the callbacks to be more clear and unique.
 *
 * 4.) Checkout how to reuse a PggExe after GnuPG finished. What about
 *     a pgg_exe_reset() function? Or should fetching the return code
 *     result in a reset to state 0?
 *
 * 5.) Extend the _arg interface to enable removing and inserting
 *     arguments. Check out if it makes sense to use a PggBuffer for
 *     this purpose.
 *
 * 6.) Don't use a extra buffer for the status argument and don't copy
 *     the status argument between buffers. Make sure, that the
 *     _get_status_arg() return NULL if their wasn't a status argument.
 *
 * 7.) Rename some functions: _request_*()
 */




static void cleanup(PggExePtr exeptr);
static void killgpg(PggExePtr exeptr);



PggExe pgg_exe_new(PggErrenv errenv)
{
    PggExePtr		new_exe;
    
    PGG_CHECK_SKIP_ARG(NULL);
    
    if (!( new_exe = _malloc(PggExe) ))
        PGG_RETURN_ERR_ARG(RESOURCE, MEMORY, NULL);
    
    memset(new_exe, 0, _size(PggExe));
    
    new_exe->magic         = PggExeMAGIC;
    new_exe->refcounter    = 1;
    new_exe->argv_capacity = 32;
    
    new_exe->child_status_pipe[0] = -1;
    new_exe->child_status_pipe[1] = -1;
    new_exe->child_stdout_pipe[0] = -1;
    new_exe->child_stdout_pipe[1] = -1;
    new_exe->child_stdin_pipe[0] = -1;
    new_exe->child_stdin_pipe[1] = -1;
    
    if (!( new_exe->argv = (char **)malloc(sizeof(char *) * new_exe->argv_capacity) )) {
        free(new_exe);
        PGG_RETURN_ERR_ARG(RESOURCE, MEMORY, NULL);
    }
    
    return _hd(PggExe, new_exe);
}


PggExe pgg_exe_clone(PggExe _old_exe, PggErrenv errenv)
{
    int			i;
    PggExePtr		new_exe;
    
    PGG_STD_ASSERT_ARG(PggExe, old_exe, NULL);
    PGG_ASSERT_ARG(old_exe->state == 0, ARGUMENT, STATE, NULL);
    
    if (!( new_exe = _malloc(PggExe) ))
        PGG_RETURN_ERR_ARG(RESOURCE, MEMORY, NULL);
    
    memset(new_exe, 0, _size(PggExe));
    
    new_exe->magic         = PggExeMAGIC;
    new_exe->refcounter    = 1;
    new_exe->argv_capacity = old_exe->argv_capacity;
    
    new_exe->child_status_pipe[0] = -1;
    new_exe->child_status_pipe[1] = -1;
    new_exe->child_stdout_pipe[0] = -1;
    new_exe->child_stdout_pipe[1] = -1;
    new_exe->child_stdin_pipe[0] = -1;
    new_exe->child_stdin_pipe[1] = -1;
    
    if (old_exe->gpgpath && !(new_exe->gpgpath = strdup(old_exe->gpgpath)) ) {
        free(new_exe);
        PGG_RETURN_ERR_ARG(RESOURCE, MEMORY, NULL);
    }
    
    if (!( new_exe->argv = (char **)malloc(sizeof(char *) * new_exe->argv_capacity) )) {
        if (new_exe->gpgpath)
            free(new_exe->gpgpath);
        free(new_exe);
        PGG_RETURN_ERR_ARG(RESOURCE, MEMORY, NULL);
    }
    
    for(i=0; i<old_exe->argv_used; ++i)
        if (!( new_exe->argv[i] = strdup(old_exe->argv[i]) )) {
            for(i=0; new_exe->argv[i]; ++i)
                free(new_exe->argv[i]);
            
            free(new_exe->argv);
            
            if (new_exe->gpgpath)
                free(new_exe->gpgpath);
            
            free(new_exe);
            
            PGG_RETURN_ERR_ARG(RESOURCE, MEMORY, NULL);
        }
    
    return _hd(PggExe, new_exe);
}


void pgg_exe_addref(PggExe _exe, PggErrenv errenv)
{
    PGG_STD_ASSERT(PggExe, exe);
    exe->refcounter++;
}


void pgg_exe_release(PggExe _exe, PggErrenv errenv)
{
    int i;
    
    PGG_STD_ASSERT(PggExe, exe);
    
    if (!--exe->refcounter) {
        killgpg(exe);
        cleanup(exe);
        
        for(i=0; i<exe->argv_used; ++i)
            free(exe->argv[i]);
        free(exe->argv);
        
        if (exe->gpgpath)
            free(exe->gpgpath);
        
        free(exe);
    }
}


void pgg_exe_add_arg(PggExe _exe, char *arg, PggErrenv errenv)
{
    char *		tmp;
    
    PGG_STD_ASSERT(PggExe, exe);
    PGG_ASSERT(arg, ARGUMENT, NULLPTR);
    
    if (exe->state != 0)
        PGG_RETURN_ERR(REQUEST, STATE);
    
    if (exe->argv_used == exe->argv_capacity) {
        char ** new_argv;
        
        if (!( new_argv = (char **)malloc(sizeof(char *) * 2 * exe->argv_capacity) ))
            PGG_RETURN_ERR(RESOURCE, MEMORY);
        
        exe->argv = new_argv;
        exe->argv_capacity *= 2;
    }
    
    if (!( tmp = strdup(arg) ))
        PGG_RETURN_ERR(RESOURCE, MEMORY);
    
    exe->argv[exe->argv_used++] = tmp;
}


void pgg_exe_request_status(PggExe _exe, PggErrenv errenv)
{
    PGG_STD_ASSERT(PggExe, exe);
    
    if (exe->state != 0)
        PGG_RETURN_ERR(REQUEST, STATE);
    
    exe->use_status = 1;
}


void pgg_exe_request_shm(PggExe _exe, PggErrenv errenv)
{
    PGG_STD_ASSERT(PggExe, exe);
    
    if (exe->state != 0)
        PGG_RETURN_ERR(REQUEST, STATE);
    
    exe->use_shm = 1;
}


/*
 * create_status_buffer() sets up the status_buffer and status_arg
 * used for reading status messages
 */
static int create_status_buffer(PggExePtr exeptr)
{
    exeptr->status_buffer_used = 0;
    exeptr->status_buffer_size = 2048;
    
    if (!( exeptr->status_buffer = (char *)malloc(exeptr->status_buffer_size) )) {
        PGG_DEBUG(("malloc for status_buffer failed"));
        return -1;
    }
    
    *(exeptr->status_buffer) = 0;
    
    if (!( exeptr->status_arg = (char *)malloc(exeptr->status_buffer_size) )) {
        PGG_DEBUG(("malloc for status_arg failed"));
        return -1;
    }
    
    *(exeptr->status_arg) = 0;
    
    return 0;
}


/*
 * Frees all resources (memory, pipes) used by pgg_exe_start()
 */
static void cleanup(PggExePtr exeptr)
{
    /*
     * First, close all open pipes
     */
    if (exeptr->child_status_pipe[0] != -1) {
        close(exeptr->child_status_pipe[0]);
        exeptr->child_status_pipe[0] = -1;
    }
    
    if (exeptr->child_status_pipe[1] != -1) {
        close(exeptr->child_status_pipe[1]);
        exeptr->child_status_pipe[1] = -1;
    }
    
    if (exeptr->child_stdin_pipe[0] != -1) {
        close(exeptr->child_stdin_pipe[0]);
        exeptr->child_stdin_pipe[0] = -1;
    }
    
    if (exeptr->child_stdin_pipe[1] != -1) {
        close(exeptr->child_stdin_pipe[1]);
        exeptr->child_stdin_pipe[1] = -1;
    }
    
    if (exeptr->child_stdout_pipe[0] != -1) {
        close(exeptr->child_stdout_pipe[0]);
        exeptr->child_stdout_pipe[0] = -1;
    }
    
    if (exeptr->child_stdout_pipe[1] != -1) {
        close(exeptr->child_stdout_pipe[1]);
        exeptr->child_stdout_pipe[1] = -1;
    }
    
    if (exeptr->status_buffer) {
        free(exeptr->status_buffer);
        exeptr->status_buffer = NULL;
    }
    
    if (exeptr->status_arg) {
        free(exeptr->status_arg);
        exeptr->status_arg = NULL;
    }
    
    if (exeptr->read_buffer) {
        free(exeptr->read_buffer);
        exeptr->read_buffer = NULL;
    }
    
    if (exeptr->write_buffer) {
        free(exeptr->write_buffer);
        exeptr->write_buffer = NULL;
    }
    
    if (exeptr->shm_buf) {
        if (shmdt(exeptr->shm_buf) == -1)
            PGG_DEBUG(("shmdt() failed!?!?"));
        exeptr->shm_buf = NULL;
        exeptr->shm_size = 0;
    }
}


void pgg_exe_start(PggExe _exe, PggErrenv errenv)
{
    int			i;
    char **		argv;
    int			args;
    int			nullfd;
    char		buf_status_pipe[8];
    PggErrenv		local_errenv;
    
    PGG_STD_ASSERT(PggExe, exe);
    PGG_ASSERT(exe->state == 0, REQUEST, STATE);
    PGG_ASSERT(exe->gpgpath, REQUEST, STATE);
    
    pgg_errenv_reset(local_errenv);
    
    /*
     * Calculate the number of arguments needed for GnuPG
     */
    args = exe->argv_used;		/* args supplied by the caller */
    
    if (exe->use_status || exe->use_shm)
        args += 2;			/* request for --status-fd */
    
    if (exe->use_shm)
        args += 2;			/* request for shared memory protocoll */
    
    args += 5;				/* program name, no batch, no tty, multiple locks, end terminator */
    
    if (!( argv = (char **)malloc(sizeof(char *) * args) ))
        PGG_RETURN_ERR(RESOURCE, MEMORY);
    
    /*
     * Build the argv vector for GnuPG
     */
    args = 0;
    argv[args++] = exe->gpgpath;
    argv[args++] = "--no-batch";
    argv[args++] = "--no-tty";
    argv[args++] = "--lock-multiple";
    
    /*
     * Create status_pipe, status_buffer and add status options to GnuPG if necessary
     */
    if (exe->use_status || exe->use_shm) {
        PGG_DEBUG(("we need a status pipe"));
        
        /*
         * When we need a status_pipe, of course we need the status_buffer, too
         */
        if ( create_status_buffer(exe)) {
            PGG_DEBUG(("create_status_buffer() failed"));
            cleanup(exe);
            free(argv);
            PGG_RETURN_ERR(RESOURCE, MEMORY);
        }
        
        if (pipe(exe->child_status_pipe) == -1) {
            PGG_DEBUG(("can't create child_status_pipe"));
            cleanup(exe);
            free(argv);
            PGG_RETURN_ERR(RESOURCE, PIPE);
        }
        
        sprintf(buf_status_pipe, "%d", exe->child_status_pipe[1]);
        
        argv[args++] = "--status-fd";
        argv[args++] = buf_status_pipe;
    }
    else {
        /* Clear status_pipe */
        PGG_DEBUG(("don't need a status pipe"));
        exe->child_status_pipe[0] = -1;
        exe->child_status_pipe[1] = -1;
    }
    
    /*
     * Request the usage of shared memory coprocessing if necessary
     */
    if (exe->use_shm) {
        argv[args++] = "--run-as-shm-coprocess";
        argv[args++] = "0";
    }
    
    /*
     * Copy the user supplied options to the argv vector
     */
    for(i=0; i<exe->argv_used; ++i)
        argv[args++] = exe->argv[i];
    
    #ifndef NDEBUG
    for(i=0; i<args; ++i) {
        PGG_ASSERT(argv[i], INTERNAL, NONE);
        PGG_DEBUG(("argv[%d]: \"%s\"", i, argv[i]));
    }
    #endif
    
    /*
     * Terminate the argv vector with NULL 
     */
    argv[args] = NULL;
    
    /*
     * Create read_buffer and child_stdout_pipe if needed
     */
    if (exe->reader) {
        if (!( exe->read_buffer = (char *)malloc(DEFAULT_BUF_SIZE) )) {
            PGG_DEBUG(("malloc() for read_buffer failed"));
            cleanup(exe);
            free(argv);
            PGG_RETURN_ERR(RESOURCE, MEMORY);
        }
        
        if (pipe(exe->child_stdout_pipe)) {
            PGG_DEBUG(("can't create child_stdout_pipe"));
            cleanup(exe);
            free(argv);
            PGG_RETURN_ERR(RESOURCE, MEMORY);
        }
    }
    else {
        exe->child_stdout_pipe[0] = -1;
        exe->child_stdout_pipe[1] = -1;
    }
    
    /*
     * Create write_buffer and child_stdin_pipe if needed
     */
    if (exe->writer) {
        if (!( exe->write_buffer = (char *)malloc(DEFAULT_BUF_SIZE) )) {
            PGG_DEBUG(("malloc() for write_buffer failed"));
            cleanup(exe);
            free(argv);
            PGG_RETURN_ERR(RESOURCE, MEMORY);
        }
        
        if (pipe(exe->child_stdin_pipe)) {
            PGG_DEBUG(("can't create child_stdin_pipe"));
            cleanup(exe);
            free(argv);
            PGG_RETURN_ERR(UNKNOWN, NONE);
        }
    }
    else {
        exe->child_stdin_pipe[0] = -1;
        exe->child_stdin_pipe[1] = -1;
    }
    
    /*
     * Open /dev/null for redirecting stderr, and if necessary stdin and/or stdout
     */    
    if ((nullfd = open("/dev/null", O_RDWR)) == -1) {
        PGG_DEBUG(("can't open /dev/null"));
        cleanup(exe);
        free(argv);
        PGG_RETURN_ERR(UNKNOWN, NONE);
    }
    
    /*
     * start GnuPG
     */
    switch ((exe->child_pid = fork())) {
        case -1:
            PGG_DEBUG(("fork() failed"));
            cleanup(exe);
            free(argv);
            PGG_RETURN_ERR(RESOURCE, PROCESS);
        
        case 0:					/* child process */
            if (exe->child_status_pipe[0] != -1)
                close(exe->child_status_pipe[0]);
            
            if (exe->child_stdin_pipe[1] != -1)
                close(exe->child_stdin_pipe[1]);
            
            if (exe->child_stdout_pipe[0] != -1)
                close(exe->child_stdout_pipe[0]);
            
            /*
             * Setup stdin of GnuPG
             */
            if (exe->child_stdin_pipe[0] != -1) {
                dup2(exe->child_stdin_pipe[0], 0);
                close(exe->child_stdin_pipe[0]);
            }    
            else
                dup2(nullfd, 0);
            
            /*
             * Setup stdout of GnuPG
             */
            if (exe->child_stdout_pipe[1] != -1) {
                dup2(exe->child_stdout_pipe[1], 1);
                close(exe->child_stdout_pipe[1]);
            }
            else
                dup2(nullfd, 1);
            
            /*
             * Redirect stderr to /dev/null
             */
            dup2(nullfd, 2);
            close(nullfd);
            
            /*
             * Execute GnuPG. Return code 10 if the execution failed.
             */
            execv(argv[0], argv);
            exit(10);
    }
    
    /*
     * child process successfully started. Cleanup the environment
     */
    
    if (exe->child_status_pipe[1] != -1)
        close(exe->child_status_pipe[1]);
    
    if (exe->child_stdin_pipe[0] != -1)
        close(exe->child_stdin_pipe[0]);
    
    if (exe->child_stdout_pipe[1] != -1)
        close(exe->child_stdout_pipe[1]);
    
    close(nullfd);
    
    free(argv);
    
    PGG_DEBUG(("GnuPG started"));
    
    exe->state = 1;
}




#define ENTRY(name) { #name, PGG_EXE_STATUS_ ## name },
static struct { char *text; short code; } status_table[] =
{
    ENTRY(GOODSIG)
    ENTRY(BADSIG)
    ENTRY(ERRSIG)
    ENTRY(VALIDSIG)
    ENTRY(SIG_ID)
    ENTRY(ENC_TO)
    ENTRY(NODATA)
    ENTRY(TRUST_UNDEFINED)
    ENTRY(TRUST_NEVER)
    ENTRY(TRUST_MARGINAL)
    ENTRY(TRUST_FULLY)
    ENTRY(TRUST_ULTIMATE)
    ENTRY(SIGEXPIRED)
    ENTRY(KEYREVOKED)
    ENTRY(BADARMOR)
    ENTRY(RSA_OR_IDEA)
    ENTRY(SHM_INFO)
    ENTRY(SHM_GET)
    ENTRY(SHM_GET_BOOL)
    ENTRY(SHM_GET_HIDDEN)
    ENTRY(NEED_PASSPHRASE)
    ENTRY(NEED_PASSPHRASE_SYM)
    ENTRY(MISSING_PASSPHRASE)
    ENTRY(BAD_PASSPHRASE)
    ENTRY(GOOD_PASSPHRASE)
    ENTRY(DECRYPTION_FAILED)
    ENTRY(DECRYPTION_OKAY)
    ENTRY(NO_PUBKEY)
    ENTRY(NO_SECKEY)
    { NULL, PGG_EXE_STATUS_UNKNOWN }
};
#undef ENTRY




/*
 * Fill the status_buffer from the status_pipe
 */
static void fill_status_buffer(PggExePtr exeptr)
{
    int			len;
    
    if (exeptr->status_buffer_used < exeptr->status_buffer_size) {
        len = read(exeptr->child_status_pipe[0],
                   exeptr->status_buffer + exeptr->status_buffer_used,
                   exeptr->status_buffer_size - exeptr->status_buffer_used);
        
        switch (len) {
            case 0:
                PGG_DEBUG(("read from status_pipe returned no more data. Closing status pipe"));
                close(exeptr->child_status_pipe[0]);
                exeptr->child_status_pipe[0] = -1;
                break;
            
            case -1: /* error */
                PGG_DEBUG(("unhandled error condition on reading from status pipe"));
                break;			/* FIXME: handle error */
            
            default: /* data available */
                PGG_DEBUG(("read %d new bytes from status pipe to status_buffer"));
                exeptr->status_buffer_used += len;
                exeptr->status_buffer[exeptr->status_buffer_used] = 0;	/* terminate string */
        }
    }
    else
        PGG_DEBUG(("can't fill status buffer because buffer is full?!?!"));
}


/*
 * Tries to fetch the next status message from the status_buffer
 */
static void process_status_buffer(PggExePtr exeptr)
{
    int			i;
    char *		status_end;
    char *		arg_start;
    
    /*
     * Check if a status message line is completly available
     */
    if (!( status_end = strchr(exeptr->status_buffer, '\n') )) {
        PGG_DEBUG(("status_buffer contains an incomplete message"));
        exeptr->status_code = PGG_EXE_STATUS_NONE;
        return;
    }
    
    *status_end++ = 0;
    
    PGG_DEBUG(("next status line to process: \"%s\"", exeptr->status_buffer));
    
    /*
     * Check if the status line has a known format
     */
    if (strncmp(exeptr->status_buffer, "[GNUPG:] ", 9)) {
        PGG_DEBUG(("unknown garbage in status_buffer"));
        exeptr->status_code = -1; /* error */
        return;
    }
    
    /*
     * If the status message has an argument, copy the argument to
     * the status_arg buffer
     */
    arg_start = strchr(exeptr->status_buffer + 9, ' ');
    if (arg_start) {
        strcpy(exeptr->status_arg, arg_start+1);
        *arg_start = 0;
    } else {
        *(exeptr->status_arg) = 0;
    }
    
    /*
     * Search in a table for known status messages to substitute a
     * return code.
     */
    exeptr->status_code = PGG_EXE_STATUS_UNKNOWN;
    i = 0;
    
    while (exeptr->status_code == PGG_EXE_STATUS_UNKNOWN && status_table[i].text)
        if (strcmp(exeptr->status_buffer + 9, status_table[i].text) == 0 ) {
            PGG_DEBUG(("known status code found: %s", status_table[i].text));
            exeptr->status_code = status_table[i].code;
        }
        else
            ++i;
    
    if (exeptr->status_code == PGG_EXE_STATUS_UNKNOWN) {
        PGG_DEBUG(("unknown status message read! (\"%s\")", exeptr->status_buffer + 9));
        exeptr->status_code = -1;			/* FIXME: How to handle unknown status messages? */
        return;
    }
    
    /*
     * Adjust status_buffer and status_buffer_used to point to the next
     * status line if their is any.
     */
    if (status_end < exeptr->status_buffer + exeptr->status_buffer_used) {
        memmove(exeptr->status_buffer, status_end, exeptr->status_buffer + exeptr->status_buffer_used - status_end + 1);
        exeptr->status_buffer_used -= status_end - exeptr->status_buffer;
    } else {
        exeptr->status_buffer_used = 0;
        *(exeptr->status_buffer) = 0;
    }
    
    PGG_DEBUG(("status argument: \"%s\"", exeptr->status_arg));
}




static int init_shm(PggExePtr exeptr)
{
    int			proto;
    int			pid;
    int			shmid;
    int			size;
    int			locked;
    
    if (sscanf(exeptr->status_arg, "pv=%d pid=%d shmid=%d sz=%d lz=%d", 
               &proto, &pid, &shmid, &size, &locked) != 5) {
        PGG_DEBUG(("error parsing shm info"));
        return -1;
    }
    
    if (proto != 1 || pid != exeptr->child_pid || size == 0) {
        PGG_DEBUG(("invalid shm info"));
        return -1;
    }
    
    if (!( exeptr->shm_buf = shmat(shmid, NULL, 0) )) {
        PGG_DEBUG(("can't attach to shared memory of GnuPG"));
        return -1;
    }
    
    exeptr->shm_size = size;
    
    return 0;
}




PggExe_Event pgg_exe_wait_event(PggExe _exe, PggErrenv errenv)
{
    fd_set		readset, writeset;
    int			max_fd;
    PggErrenv		local_errenv;
    
    PGG_STD_ASSERT_ARG(PggExe, exe, PGG_EXE_EVENT_ERROR);
    PGG_ASSERT_ARG(exe->state == 1, REQUEST, STATE, PGG_EXE_EVENT_ERROR);
    
    pgg_errenv_reset(local_errenv);
    
    PGG_DEBUG(("fetch next event..."));
    
    /*
     * We loop until we got a real event
     */
    while (1) {
        /*
         * First check for pending status events
         */
        if (exe->use_status || exe->use_shm) {
            process_status_buffer(exe);
            switch (exe->status_code) {
                case PGG_EXE_STATUS_ERROR:	/* problems on read status messages (format, data...) */
                    PGG_DEBUG(("process_status_buffer returned with an error"));
                    PGG_RETURN_ERR_ARG(UNKNOWN, NONE, PGG_EXE_EVENT_ERROR);
                
                case PGG_EXE_STATUS_NONE:	/* we need to read more data */
                    break;
                
                default:
                    /*
                     * Process SHM status events if necessary
                     */
                    if (exe->use_shm)
                        switch (exe->status_code) {
                            case PGG_EXE_STATUS_SHM_INFO:
                                if (init_shm(exe)) {
                                    PGG_DEBUG(("init_shm() failed."));
                                    PGG_RETURN_ERR_ARG(UNKNOWN, NONE, PGG_EXE_EVENT_ERROR);
                                }
                                continue;
                            
                            case PGG_EXE_STATUS_SHM_GET:
                            case PGG_EXE_STATUS_SHM_GET_BOOL:
                            case PGG_EXE_STATUS_SHM_GET_HIDDEN:
                                return PGG_EXE_EVENT_STATUS;
                            
                            default:
                                break;
                        }
                    
                    /*
                     * Return all normal status events without SHM events if necessary
                     */
                    if (exe->use_status)
                        switch (exe->status_code) {
                            case PGG_EXE_STATUS_SHM_INFO:
                            case PGG_EXE_STATUS_SHM_GET:
                            case PGG_EXE_STATUS_SHM_GET_BOOL:
                            case PGG_EXE_STATUS_SHM_GET_HIDDEN:
                                break;
                            
                            default:
                                return PGG_EXE_EVENT_STATUS;
                        }
                    
            }
        } /* pending status events */
        
        
        /*
         * Check for new data on important FDs
         */
        if (exe->child_status_pipe[0] != -1
                || exe->child_stdin_pipe[1] != -1
                || exe->child_stdout_pipe[0] != -1) {
            FD_ZERO(&readset);
            FD_ZERO(&writeset);
            max_fd = -1;
            
            /*
             * Add status pipe to the list of watching for events if necessary
             */
            if (exe->child_status_pipe[0] != -1) {
                FD_SET(exe->child_status_pipe[0], &readset);
                max_fd = max_fd > exe->child_status_pipe[0] ? max_fd : exe->child_status_pipe[0];
            }
            
            /*
             * Add stdin pipe if necessary
             */
            if (exe->child_stdin_pipe[1] != -1) {
                FD_SET(exe->child_stdin_pipe[1], &writeset);
                max_fd = max_fd > exe->child_stdin_pipe[1] ? max_fd : exe->child_stdin_pipe[1];
            }
            
            /*
             * Add stdout pipe if necessary
             */
            if (exe->child_stdout_pipe[0] != -1) {
                FD_SET(exe->child_stdout_pipe[0], &readset);
                max_fd = max_fd > exe->child_stdout_pipe[0] ? max_fd : exe->child_stdout_pipe[0];
            }
            
            /*
             * Wait for a new event to occur
             */
            PGG_DEBUG(("calling select() to wait for an event"));
            if (select(max_fd + 1, &readset, &writeset, NULL, NULL) == -1) {
                switch(errno) {
                    case EAGAIN:	/* FIXME: AFAIK this could occur on some systems */
                        PGG_DEBUG(("select() EAGAIN"));
                        break;
                    
                    case EINTR:		/* We were interrupted. Just ignore this case and try again */
                        PGG_DEBUG(("select() EINTR"));
                        break;
                    
                    default:
                        PGG_DEBUG(("select(): unknown error: %s\n", strerror(errno)));
                        PGG_RETURN_ERR_ARG(UNKNOWN, NONE, PGG_EXE_EVENT_ERROR);
                }
            }
            
            PGG_DEBUG(("select() returned with an new event"));
            
            /*
             * Check if events on the status pipe are pending
             */
            if (exe->child_status_pipe[0] != -1)
                if (FD_ISSET(exe->child_status_pipe[0], &readset)) {
                    /*
                     * A new status message arrived from GnuPG.
                     */
                    PGG_DEBUG(("event to read data from status_pipe arrived"));
                    fill_status_buffer(exe);
                }
            
            /*
             * Check if events on the child_stdout_pipe are pending
             */
            if (exe->child_stdout_pipe[0] != -1)
                if (FD_ISSET(exe->child_stdout_pipe[0], &readset)) {
                    /*
                     * New data to read arrived
                     */
                    long len;
                    
                    PGG_DEBUG(("event to read data from GnuPG stdout arrived"));
                    
                    if ((len = read(exe->child_stdout_pipe[0], exe->read_buffer, DEFAULT_BUF_SIZE)) == -1) {
                        PGG_DEBUG(("read data from GnuPG stdout_pipe failed with an error"));
                        PGG_RETURN_ERR_ARG(UNKNOWN, NONE, PGG_EXE_EVENT_ERROR);
                    }
                    
                    if (len == 0) {	/* length of 0 means no more data in the future */
                        PGG_DEBUG(("read from GnuPG stdout returned 0 bytes. Closing pipe"));
                        exe->reader(exe->reader_opaque, exe->read_buffer, 0);	/* signal closing */
                        close(exe->child_stdout_pipe[0]);
                        exe->child_stdout_pipe[0] = -1;
                    }
                    else {
                        PGG_DEBUG(("calling reader callback to consume %d bytes", len));
                        if (exe->reader(exe->reader_opaque, exe->read_buffer, len) != len) {	/* FIXME: We assume full reads */
                            PGG_DEBUG(("reader callback failed"));
                            PGG_RETURN_ERR_ARG(UNKNOWN, NONE, PGG_EXE_EVENT_ERROR);
                        }
                    }
                }
            
            /*
             * Check if events on the child_stdin_pipe are pending
             */
            if (exe->child_stdin_pipe[1] != -1)
                if (FD_ISSET(exe->child_stdin_pipe[1], &writeset)) {
                    /*
                     * We could write more data to GnuPG
                     */
                    long len;
                    
                    PGG_DEBUG(("space is available to write new data into GnuPG stdin_pipe"));
                    
                    /*
                     * First check, if we need to fill our buffer with new data
                     */
                    if (exe->write_buffer_used == 0) {
                        PGG_DEBUG(("write_buffer is empty. Try to fill it by calling the writer callback"));
                        
                        if ((len = exe->writer(exe->writer_opaque, exe->write_buffer, DEFAULT_BUF_SIZE)) == -1) {
                            PGG_DEBUG(("writer callback failed"));
                            PGG_RETURN_ERR_ARG(UNKNOWN, NONE, PGG_EXE_EVENT_ERROR);
                        }
                        
                        PGG_DEBUG(("writer callback returned %d bytes", len));
                        
                        /*
                         * If their is no more data to write, close the pipe
                         */
                        if (len == 0) {
                            PGG_DEBUG(("closing stdin pipe of GnuPG because no more data to write"));
                            close(exe->child_stdin_pipe[1]);
                            exe->child_stdin_pipe[1] = -1;
                        }
                        
                        exe->write_buffer_used = len;
                    }
                    
                    /*
                     * If more data is available, try to write the data to GnuPG
                     */
                    if (exe->write_buffer_used > 0) {
                        PGG_DEBUG(("try to write %d bytes to stdin of GnuPG", exe->write_buffer_used));
                        if ((len = write(exe->child_stdin_pipe[1], exe->write_buffer, exe->write_buffer_used)) == -1) {
                            PGG_DEBUG(("write returned with an error"));
                            PGG_RETURN_ERR_ARG(UNKNOWN, NONE, PGG_EXE_EVENT_ERROR);
                        }
                        
                        if (len < exe->write_buffer_used) {
                            PGG_DEBUG(("partial write occured. Only wrote %d bytes", len));
                            memmove(exe->write_buffer, exe->write_buffer + len, exe->write_buffer_used - len);
                        }
                        else
                            PGG_DEBUG(("wrote all %d bytes to GnuPG", len));
                        
                        exe->write_buffer_used -= len;
                    }
                }
        }
        else {
            /*
             * Because, their are no more data to read from or write to GnuPG,
             * we could only wait until GnuPG is finished.
             */
            PGG_DEBUG(("no more data available to write or read from GnuPG. Wait for GnuPG exiting."));
            waitpid(exe->child_pid, &(exe->child_status), 0);
            exe->child_pid = 0;
            exe->state = 2;				/* FIXME: should we switch back to state 0 ??? */
            return PGG_EXE_EVENT_FINISHED;
        }
    }
    
    /*
     * It's an error if we reach these lines.
     */
    PGG_DEBUG(("unexpected line reached!?!?"));
    PGG_RETURN_ERR_ARG(INTERNAL, NONE, PGG_EXE_EVENT_ERROR);
}




PggExe_Status pgg_exe_get_status_code(PggExe _exe, PggErrenv errenv)
{
    PGG_STD_ASSERT_ARG(PggExe, exe, PGG_EXE_STATUS_ERROR);
    PGG_ASSERT_ARG(exe->state == 1, REQUEST, STATE, PGG_EXE_STATUS_ERROR);
    return exe->status_code;
}




char * pgg_exe_get_status_arg(PggExe _exe, PggErrenv errenv)
{
    PGG_STD_ASSERT_ARG(PggExe, exe, NULL);
    PGG_ASSERT_ARG(exe->state == 1, REQUEST, STATE, NULL);
    return exe->status_arg;
}




void pgg_exe_reply_bool(PggExe _exe, int yesno, PggErrenv errenv)
{
    int			offset;
    
    PGG_STD_ASSERT(PggExe, exe);
    
    if (exe->state != 1)
        PGG_RETURN_ERR(REQUEST, STATE);
    
    if (exe->shm_buf[2] != 1 || exe->shm_buf[3] != 0)
        PGG_RETURN_ERR(UNKNOWN, NONE);
    
    offset = (exe->shm_buf[0] << 8) + exe->shm_buf[1];
    PGG_ASSERT(offset == 32, INTERNAL, NONE);		/* FIXME: this could change in the future... */
    
    /* Length descriptor */
    exe->shm_buf[offset + 0] = 0;
    exe->shm_buf[offset + 1] = 1;
    
    /* yesno flag */
    exe->shm_buf[offset + 2] = yesno ? 1 : 0;
    
    /* set reply trigger flag */
    exe->shm_buf[3] = 1;
    
    /* wakeup GnuPG process */
    kill(exe->child_pid, SIGUSR1);
}


void pgg_exe_reply_str(PggExe _exe, const char * arg, PggErrenv errenv)
{
    int                 offset;
    int			len;
    
    PGG_STD_ASSERT(PggExe, exe);
    PGG_ASSERT(arg, ARGUMENT, NULLPTR);
    PGG_ASSERT(exe->state == 1, REQUEST, STATE);
    
    if (exe->shm_buf[2] != 1 || exe->shm_buf[3] != 0)
        PGG_RETURN_ERR(UNKNOWN, NONE);
    
    offset = (exe->shm_buf[0] << 8) + exe->shm_buf[1];
    PGG_ASSERT(offset == 32, INTERNAL, NONE);               /* FIXME: this could change in the future... */
    
    len = strlen(arg);
    
    /*
     * Catch empty and long strings
     */
    if (len == 0)
        PGG_RETURN_ERR(ARGUMENT, TOSMALL);
    
    if (offset + 2 + len > exe->shm_size)
        PGG_RETURN_ERR(ARGUMENT, TOLARGE);
    
    
    /* Length descriptor */
    exe->shm_buf[offset + 0] = len >> 8;
    exe->shm_buf[offset + 1] = len & 0xff;
    
    /* copy string to shared memory */
    memcpy(exe->shm_buf + offset + 2, arg, len);
    
    /* set reply trigger flag */
    exe->shm_buf[3] = 1;
    
    /* wakeup GnuPG process */
    kill(exe->child_pid, SIGUSR1);
}


void pgg_exe_set_read_cb(PggExe _exe, PggExe_ReadCB cb, void *opaque, PggErrenv errenv)
{
    PGG_STD_ASSERT(PggExe, exe);
    
    if (exe->state != 0)
        PGG_RETURN_ERR(REQUEST, STATE);
    
    if (cb) {
        exe->reader = cb;
        exe->reader_opaque = opaque;
    }
    else {
        exe->reader = NULL;
        exe->reader_opaque = NULL;
    }
}


void pgg_exe_set_write_cb(PggExe _exe, PggExe_WriteCB cb, void *opaque, PggErrenv errenv)
{
    PGG_STD_ASSERT(PggExe, exe);
    
    if (exe->state != 0)
        PGG_RETURN_ERR(REQUEST, STATE);
    
    if (cb) {
        exe->writer = cb;
        exe->writer_opaque = opaque;
    }
    else {
        exe->writer = NULL;
        exe->writer_opaque = opaque;
    }
}


void pgg_exe_set_gpgpath(PggExe _exe, const char *gpgpath, PggErrenv errenv)
{
    char *		tmp;
    
    PGG_STD_ASSERT(PggExe, exe);
    PGG_ASSERT(gpgpath, ARGUMENT, NULLPTR);
    PGG_ASSERT(exe->state == 0, REQUEST, STATE);
    
    if (!( tmp = strdup(gpgpath) ))
        PGG_RETURN_ERR(RESOURCE, MEMORY);    
    
    free(exe->gpgpath);
    
    exe->gpgpath = tmp;
}


const char * pgg_exe_get_gpgpath(PggExe _exe, PggErrenv errenv)
{
    PGG_STD_ASSERT_ARG(PggExe, exe, NULL);
    PGG_ASSERT_ARG(exe->gpgpath, REQUEST, NOTSET, NULL);
    return exe->gpgpath;
}


int pgg_exe_get_returnCode(PggExe _exe, PggErrenv errenv)
{
    PGG_STD_ASSERT_ARG(PggExe, exe, -1);
    PGG_ASSERT_ARG(exe->state == 2, REQUEST, STATE, -1);	/* FIXME: how to reset from state 2??? */
    return WIFEXITED(exe->child_status) ? WEXITSTATUS(exe->child_status) : -1;
}


void pgg_exe_abort(PggExe _exe, PggErrenv errenv)
{
    PGG_STD_ASSERT(PggExe, exe);
    killgpg(exe);
    cleanup(exe);
    exe->state = 0;
}


static void killgpg(PggExePtr exeptr)
{
    if (exeptr->child_pid > 0) {
        kill(exeptr->child_pid, SIGTERM);		/* Normal request to terminate */
        kill(exeptr->child_pid, SIGUSR1);		/* Don't know if we need this. But the SHM protocoll use this. */
        waitpid(exeptr->child_pid, NULL, 0);		/* Wait until gpg exit */
        exeptr->child_pid = 0;
    }
}


