
#include <stdio.h>
#include <sys/types.h>
#include <signal.h>
#include <assert.h>
#include <unistd.h>

#include "type.h"

#include "remote_process.h"
#include "string_collection.h"

#include "process_watcher.h"

static void remote_process_closeFDs (RemoteProcess *instance);

struct s_RemoteProcess
{
    int pid;

    char *executable;

    StringCollection *arguments;

    StringCollection *environment;

    int stdin;
    int stdout;
    int stderr;

    bool exited;
    int exitCode;

    pthread_mutex_t processMutex;
};

RemoteProcess *
remote_process_new (const char *executable)
{
    RemoteProcess *instance = (RemoteProcess *) malloc (sizeof (RemoteProcess));

    /* make sure the executable is not NULL */
    assert (executable != NULL);
    /* make sure the executable is not an empty string */
    assert (*executable != 0);

    instance->executable = strdup (executable);

    instance->pid = -1;
    instance->stdin = -1;
    instance->stdout = -1;
    instance->stderr = -1;

    instance->exitCode = 0;
    instance->exited = FALSE;

    instance->arguments = string_collection_new();

    instance->environment = string_collection_new();

    string_collection_addString (instance->arguments, instance->executable);

    pthread_mutex_init (&instance->processMutex, NULL);

    return (instance);
}

void
remote_process_destroy (void *ptr)
{
    RemoteProcess *instance = (RemoteProcess *) ptr;

    process_watcher_removeWatchPid (process_watcher_getInstance(),
            instance->pid);

    pthread_mutex_lock (&instance->processMutex);

    string_collection_destroy (instance->arguments);
    string_collection_destroy (instance->environment);

    if (instance->exited == FALSE)
    {
        kill (instance->pid, SIGKILL);
    }

    remote_process_closeFDs (instance);

    pthread_mutex_unlock (&instance->processMutex);

    pthread_mutex_destroy (&instance->processMutex);

    free (instance->executable);

    free (instance);
}

static void
remote_process_closeFDs (RemoteProcess *instance)
{
    if (instance->stdin != -1)
    {
        close (instance->stdin);

        instance->stdin = -1;
    }

    if (instance->stdout != -1)
    {
        close (instance->stdout);

        instance->stdout = -1;
    }

    if (instance->stderr != -1)
    {
        close (instance->stderr);

        instance->stderr = -1;
    }
}

const char *
remote_process_get_executable (RemoteProcess *instance)
{
    return (instance->executable);
}

void
remote_process_addEnvironment (RemoteProcess *instance, const char *variable)
{
    pthread_mutex_lock (&instance->processMutex);

    string_collection_addString (instance->environment, variable);

    pthread_mutex_unlock (&instance->processMutex);
}

void
remote_process_addEnvironmentData (RemoteProcess *instance, const char *variable, int len)
{
    pthread_mutex_lock (&instance->processMutex);

    string_collection_addData (instance->environment, variable, len);

    pthread_mutex_unlock (&instance->processMutex);
}

void 
remote_process_addArgument (RemoteProcess *instance, const char *argument)
{
    pthread_mutex_lock (&instance->processMutex);

    string_collection_addString (instance->arguments, argument);

    pthread_mutex_unlock (&instance->processMutex);
}

int
remote_process_getStdIn (RemoteProcess *instance)
{
    int fd;

    pthread_mutex_lock (&instance->processMutex);

    fd = instance->stdin;

    pthread_mutex_unlock (&instance->processMutex);

    return (fd);
}

int
remote_process_getStdOut (RemoteProcess *instance)
{
    int fd;

    pthread_mutex_lock (&instance->processMutex);

    fd = instance->stdout;

    pthread_mutex_unlock (&instance->processMutex);

    return (fd);
}

bool
remote_process_closeStdOut (RemoteProcess *instance)
{
    bool ret;

    pthread_mutex_lock (&instance->processMutex);

    if (instance->stdout != -1)
    {
        close (instance->stdout);

        instance->stdout = -1;

        ret = TRUE;
    }
    else
    {
        ret = FALSE;
    }

    pthread_mutex_unlock (&instance->processMutex);

    return (ret);
}
    
bool
remote_process_closeStdIn (RemoteProcess *instance)
{
    bool ret;

    pthread_mutex_lock (&instance->processMutex);

    if (instance->stdin != -1)
    {
        close (instance->stdin);

        instance->stdin = -1;

        ret = TRUE;
    }
    else
    {
        ret = FALSE;
    }

    pthread_mutex_unlock (&instance->processMutex);

    return (ret);
}

int
remote_process_getStdErr (RemoteProcess *instance)
{
    int fd;

    pthread_mutex_lock (&instance->processMutex);

    fd = instance->stderr;

    pthread_mutex_unlock (&instance->processMutex);

    return (fd);
}

static void
remote_process_cleanupProcess (void *context, int status)
{
    RemoteProcess *instance = (RemoteProcess *) context;

    pthread_mutex_lock (&instance->processMutex);

    instance->exited = TRUE;

    if (WIFEXITED (status))
    {
        instance->exitCode = WEXITSTATUS (status);
    }

    pthread_mutex_unlock (&instance->processMutex);
}

bool
remote_process_start (RemoteProcess *instance)
{
    int stdin_pipe[2];
    int stdout_pipe[2];
    int stderr_pipe[2];
    int pid;
    struct stat statb;

    pthread_mutex_lock (&instance->processMutex);

    if (stat (instance->executable, &statb) == -1)
    {
        perror ("stat");

        pthread_mutex_unlock (&instance->processMutex);

        return (FALSE);
    }

    if (pipe (stdin_pipe) != 0)
    {
        perror ("pipe");

        pthread_mutex_unlock (&instance->processMutex);
        return (FALSE);
    }

    if (pipe (stdout_pipe) != 0)
    {
        perror ("pipe");

        close (stdin_pipe[0]);
        close (stdin_pipe[1]);

        pthread_mutex_unlock (&instance->processMutex);

        return (FALSE);
    }

    if (pipe (stderr_pipe) != 0)
    {
        perror ("pipe");

        close (stdin_pipe[0]);
        close (stdin_pipe[1]);
        close (stdout_pipe[0]);
        close (stdout_pipe[1]);

        pthread_mutex_unlock (&instance->processMutex);

        return (FALSE);
    }

    if ((pid = fork()) == -1)
    {
        /* we couldn't fork */
        perror ("fork");

        close (stdin_pipe[0]);
        close (stdin_pipe[1]);
        close (stdout_pipe[0]);
        close (stdout_pipe[1]);
        close (stderr_pipe[0]);
        close (stderr_pipe[1]);

        pthread_mutex_unlock (&instance->processMutex);

        return (FALSE);
    }

    if (pid == 0)
    {
        /* 
         * HACK: syncronize with the calling process by reading a single char
         * from stdin
         */
        {
            char buf;

            if (read (stdin_pipe[0], &buf, 1) != 1)
            {
                perror ("read");
            }
        }

        char *dir = strFcopy (instance->executable);

        char *lastSlash = rindex (dir, '/');

        if (lastSlash != NULL)
        {
            *lastSlash = 0;

            chdir (dir);
        }
        else
        {
            logFmsg (CONT_LOG_WARN, "Could not change directory for: %s", instance->executable);
        }

        /* close the write side of the stdin fd */
        close (stdin_pipe[1]);

        /* close the read side of stdout fd */
        close (stdout_pipe[0]);

        /* close the read side of stderr fd */
        close (stderr_pipe[0]);

        /* set our stdin pipe to STDIN */
        if (dup2 (stdin_pipe[0], STDIN_FILENO) == -1)
        {
            perror ("dup2");
            return (errno);
        }

        /* close the "higher" fd num */
        close (stdin_pipe[0]);

        /* set our stdout pipe to STDOUT */
        if (dup2 (stdout_pipe[1], STDOUT_FILENO) == -1)
        {
            perror ("dup2");
            return (errno);
        }

        /* close the "higher" fd num */
        close (stdout_pipe[1]);

        /* set our stderr pipe to STDERR */
        if (dup2 (stderr_pipe[1], STDERR_FILENO) == -1)
        {
            perror ("dup2");
            return (errno);
        }

        /* close the "higher" fd num */
        close (stderr_pipe[1]);

        if (getuid() == 0)
        {
            if (setregid (statb.st_gid, statb.st_gid) != 0)
            {
                int e = errno;

                perror ("setregid");

                return (e);
            }

            if (setreuid (statb.st_uid, statb.st_uid) != 0)
            {
                int e = errno;

                perror ("setreuid");

                return (e);
            }
        }

        execve (instance->executable, 
                string_collection_getInternal (instance->arguments),
                string_collection_getInternal (instance->environment));

        /* we should never reach this */

        perror (instance->executable);

        return (errno);
    }
    else
    {
        instance->pid = pid;

        process_watcher_watchPid (process_watcher_getInstance(),
                instance->pid, remote_process_cleanupProcess, instance);

        /* close the read side of the stdin fd */
        close (stdin_pipe[0]);

        /* save the write side of the stdin fd */
        instance->stdin = stdin_pipe[1];

        /* mark this fd to be closed if we fork again */
        fleFset_close_exec (instance->stdin);

        /* close the write side of stdout fd */
        close (stdout_pipe[1]);

        /* save the read side of the stdout fd */
        instance->stdout = stdout_pipe[0];

        /* mark this fd to be closed if we fork again */
        fleFset_close_exec (instance->stdout);

        /* close the write side of stderr fd */
        close (stderr_pipe[1]);

        /* save the read side of the stderr fd */
        instance->stderr = stderr_pipe[0];

        /* mark this fd to be closed if we fork again */
        fleFset_close_exec (instance->stderr);

        /* 
         * HACK: syncronize with the child process by writing a single char
         * to stdin -- the child should block until this is written
         */
        {
            char buf = '1';

            if (write (instance->stdin, &buf, 1) != 1)
            {
                perror ("write");

                return (FALSE);
            }
        }

        pthread_mutex_unlock (&instance->processMutex);

        return (TRUE);
    }
}
