
/*
 * Copyright (C) 2009      Citrix Ltd.
 * Author Vincent Hanquez <vincent.hanquez@eu.citrix.com>
 * Author Stefano Stabellini <stefano.stabellini@eu.citrix.com>
 *
 * This program is free software; you can redistribute it and/or modify
 * it under the terms of the GNU Lesser General Public License as published
 * by the Free Software Foundation; version 2.1 only. with the special
 * exception on linking described in file LICENSE.
 *
 * 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 Lesser General Public License for more details.
 */

#include "libxl_osdeps.h" /* must come before any other headers */

#include "libxl_internal.h"

static void check_open_fds(const char *what)
{
    const char *env_debug;
    int debug;
    int i, flags, badness = 0;

    env_debug = getenv("_LIBXL_DEBUG_EXEC_FDS");
    if (!env_debug) return;

    debug = strtol(env_debug, (char **) NULL, 10);
    if (debug <= 0) return;

    for (i = 4; i < 256; i++) {
#ifdef __linux__
        ssize_t len;
        char path[PATH_MAX];
        char linkpath[PATH_MAX+1];
#endif
        flags = fcntl(i, F_GETFD);
        if ( flags == -1 ) {
            if ( errno != EBADF )
                fprintf(stderr, "libxl: execing %s: fd %d flags returned %s (%d)\n",
                        what, i, strerror(errno), errno);
            continue;
        }

        if ( flags & FD_CLOEXEC )
            continue;

        badness++;

#ifdef __linux__
        snprintf(path, PATH_MAX, "/proc/%d/fd/%d", getpid(), i);
        len = readlink(path, linkpath, PATH_MAX);
        if (len > 0) {
            linkpath[len] = '\0';
            fprintf(stderr, "libxl: execing %s: fd %d is open to %s with flags %#x\n",
                    what, i, linkpath, flags);
        } else
#endif
            fprintf(stderr, "libxl: execing %s: fd %d is open with flags %#x\n",
                    what, i, flags);
    }
    if (debug < 2) return;
    if (badness) abort();
}

void libxl__exec(libxl__gc *gc, int stdinfd, int stdoutfd, int stderrfd,
                 const char *arg0, char *const args[], char *const env[])
     /* call this in the child */
{
    if (stdinfd != -1)
        dup2(stdinfd, STDIN_FILENO);
    if (stdoutfd != -1)
        dup2(stdoutfd, STDOUT_FILENO);
    if (stderrfd != -1)
        dup2(stderrfd, STDERR_FILENO);

    if (stdinfd > 2)
        close(stdinfd);
    if (stdoutfd > 2 && stdoutfd != stdinfd)
        close(stdoutfd);
    if (stderrfd > 2 && stderrfd != stdinfd && stderrfd != stdoutfd)
        close(stderrfd);

    check_open_fds(arg0);

    signal(SIGPIPE, SIG_DFL);
    /* in case our caller set it to IGN.  subprocesses are entitled
     * to assume they got DFL. */

    if (env != NULL) {
        for (int i = 0; env[i] != NULL && env[i+1] != NULL; i += 2) {
            if (setenv(env[i], env[i+1], 1) < 0) {
                LOGEV(ERROR, errno, "setting env vars (%s = %s)",
                                    env[i], env[i+1]);
                goto out;
            }
        }
    }
    execvp(arg0, args);

out:
    fprintf(stderr, "libxl: cannot execute %s: %s\n", arg0, strerror(errno));
    _exit(-1);
}

void libxl_report_child_exitstatus(libxl_ctx *ctx,
                                   xentoollog_level level,
                                   const char *what, pid_t pid, int status)
{

    if (WIFEXITED(status)) {
        int st = WEXITSTATUS(status);
        if (st)
            LIBXL__LOG(ctx, level, "%s [%ld] exited"
                   " with error status %d", what, (unsigned long)pid, st);
        else
            LIBXL__LOG(ctx, level, "%s [%ld] unexpectedly"
                   " exited status zero", what, (unsigned long)pid);
    } else if (WIFSIGNALED(status)) {
        int sig = WTERMSIG(status);
        const char *str = strsignal(sig);
        const char *coredump = WCOREDUMP(status) ? " (core dumped)" : "";
        if (str)
            LIBXL__LOG(ctx, level, "%s [%ld] died due to"
                   " fatal signal %s%s", what, (unsigned long)pid,
                   str, coredump);
        else
            LIBXL__LOG(ctx, level, "%s [%ld] died due to unknown"
                   " fatal signal number %d%s", what, (unsigned long)pid,
                   sig, coredump);
    } else {
        LIBXL__LOG(ctx, level, "%s [%ld] gave unknown"
               " wait status 0x%x", what, (unsigned long)pid, status);
    }
}

int libxl__spawn_record_pid(libxl__gc *gc, libxl__spawn_state *spawn, pid_t pid)
{
    int r, rc;

    rc = libxl__ev_child_xenstore_reopen(gc, spawn->what);
    if (rc) goto out;

    r = libxl__xs_printf(gc, XBT_NULL, spawn->pidpath, "%d", pid);
    if (r) {
        LOGE(ERROR,
             "write %s = %d: xenstore write failed", spawn->pidpath, pid);
        rc = ERROR_FAIL;  goto out;
    }

    rc = 0;

out:
    return rc ? SIGTERM : 0;
}

/*----- spawn implementation -----*/

/*
 * Full set of possible states of a libxl__spawn_state and its _detachable:
 *
 *                   detaching rc      mid     timeout      xswatch
 *  - Undefined         undef   undef   -        undef        undef
 *  - Idle              any     any     Idle     Idle         Idle
 *  - Attached OK       0       0       Active   Active       Active
 *  - Attached Failed   0       non-0   Active   Idle         Idle
 *  - Detaching         1       maybe   Active   Idle         Idle
 *  - Partial           any     any     Idle     Active/Idle  Active/Idle
 *
 * When in states Detaching or Attached Failed, the middle process has
 * been sent a SIGKILL.
 *
 * The difference between Attached OK and Attached Failed is not
 * directly visible to callers - callers see these two the same,
 * although of course Attached OK will hopefully eventually result in
 * a call to detached_cb, whereas Attached Failed will end up
 * in a call to failure_cb.
 */

/* Event callbacks. */
static void spawn_watch_event(libxl__egc *egc, libxl__xswait_state *xswa,
                              int rc, const char *xsdata);
static void spawn_middle_death(libxl__egc *egc, libxl__ev_child *childw,
                               pid_t pid, int status);

/* Precondition: Partial.  Results: Idle. */
static void spawn_cleanup(libxl__gc *gc, libxl__spawn_state *ss);

/* Precondition: Attached or Detaching; caller has logged failure reason.
 * Results: Detaching, or Attached Failed */
static void spawn_fail(libxl__egc *egc, libxl__spawn_state *ss, int rc);

void libxl__spawn_init(libxl__spawn_state *ss)
{
    libxl__ev_child_init(&ss->mid);
    libxl__xswait_init(&ss->xswait);
}

int libxl__spawn_spawn(libxl__egc *egc, libxl__spawn_state *ss)
{
    STATE_AO_GC(ss->ao);
    int r;
    pid_t child;
    int status, rc;

    libxl__spawn_init(ss);
    ss->rc = ss->detaching = 0;

    ss->xswait.ao = ao;
    ss->xswait.what = GCSPRINTF("%s startup", ss->what);
    ss->xswait.path = ss->xspath;
    ss->xswait.timeout_ms = ss->timeout_ms;
    ss->xswait.callback = spawn_watch_event;
    rc = libxl__xswait_start(gc, &ss->xswait);
    if (rc) goto out_err;

    pid_t middle = libxl__ev_child_fork(gc, &ss->mid, spawn_middle_death);
    if (middle ==-1) { rc = ERROR_FAIL; goto out_err; }

    if (middle) {
        /* parent */
        return 1;
    }

    /* we are now the middle process */

    pid_t (*fork_replacement)(void*) =
        CTX->childproc_hooks
        ? CTX->childproc_hooks->fork_replacement
        : 0;
    child =
        fork_replacement
        ? fork_replacement(CTX->childproc_user)
        : fork();

    if (child == -1)
        exit(255);
    if (!child) {
        return 0; /* caller runs child code */
    }

    int failsig = ss->midproc_cb(gc, ss, child);
    if (failsig) {
        kill(child, failsig);
        _exit(127);
    }

    for (;;) {
        pid_t got = waitpid(child, &status, 0);
        if (got == -1) {
            assert(errno == EINTR);
            continue;
        }
        assert(got == child);
        break;
    }

    r = (WIFEXITED(status) && WEXITSTATUS(status) <= 127 ? WEXITSTATUS(status) :
         WIFSIGNALED(status) && WTERMSIG(status) < 127 ? WTERMSIG(status)+128 :
         -1);
    _exit(r);

 out_err:
    spawn_cleanup(gc, ss);
    return rc;
}

static void spawn_cleanup(libxl__gc *gc, libxl__spawn_state *ss)
{
    assert(!libxl__ev_child_inuse(&ss->mid));
    libxl__xswait_stop(gc, &ss->xswait);
}

static void spawn_detach(libxl__gc *gc, libxl__spawn_state *ss)
/* Precondition: Attached or Detaching, but caller must have just set
 * at least one of detaching or rc.
 * Results: Detaching or Attached Failed */
{
    int r;

    assert(libxl__ev_child_inuse(&ss->mid));
    assert(ss->detaching || ss->rc);
    libxl__xswait_stop(gc, &ss->xswait);

    pid_t child = ss->mid.pid;
    r = kill(child, SIGKILL);
    if (r && errno != ESRCH)
        LOGE(WARN, "%s: failed to kill intermediate child (pid=%lu)",
             ss->what, (unsigned long)child);
}

void libxl__spawn_initiate_detach(libxl__gc *gc, libxl__spawn_state *ss)
{
    ss->detaching = 1;
    spawn_detach(gc, ss);
}

void libxl__spawn_initiate_failure(libxl__egc *egc, libxl__spawn_state *ss,
                                   int rc)
/* The spawn state must be Attached on entry and will be Attached Failed
 * on return.  */
{
    spawn_fail(egc, ss, rc);
}

static void spawn_fail(libxl__egc *egc, libxl__spawn_state *ss, int rc)
/* Caller must have logged.  Must be last thing in calling function,
 * as it may make the callback.  Precondition: Attached or Detaching. */
{
    EGC_GC;
    assert(rc);
    if (!ss->rc)
        ss->rc = rc;
    spawn_detach(gc, ss);
}

static void spawn_watch_event(libxl__egc *egc, libxl__xswait_state *xswa,
                              int rc, const char *p)
{
    /* On entry, is Attached. */
    EGC_GC;
    libxl__spawn_state *ss = CONTAINER_OF(xswa, *ss, xswait);
    if (rc) {
        if (rc == ERROR_TIMEDOUT)
            LOG(ERROR, "%s: startup timed out", ss->what);
        spawn_fail(egc, ss, rc); /* must be last */
        return;
    }
    LOG(DEBUG, "%s: spawn watch p=%s", ss->what, p);
    ss->confirm_cb(egc, ss, p); /* must be last */
}

static void spawn_middle_death(libxl__egc *egc, libxl__ev_child *childw,
                               pid_t pid, int status)
    /* On entry, is Attached or Detaching */
{
    EGC_GC;
    libxl__spawn_state *ss = CONTAINER_OF(childw, *ss, mid);

    if ((ss->rc || ss->detaching) &&
        ((WIFEXITED(status) && WEXITSTATUS(status)==0) ||
         (WIFSIGNALED(status) && WTERMSIG(status)==SIGKILL))) {
        /* as expected */
        const char *what = GCSPRINTF("%s (dying as expected)", ss->what);
        libxl_report_child_exitstatus(CTX, XTL_DEBUG, what, pid, status);
    } else if (!WIFEXITED(status)) {
        int loglevel = ss->detaching ? XTL_WARN : XTL_ERROR;
        const char *what =
            GCSPRINTF("%s intermediate process (startup monitor)", ss->what);
        libxl_report_child_exitstatus(CTX, loglevel, what, pid, status);
        ss->rc = ERROR_FAIL;
    } else {
        if (!status)
            LOG(ERROR, "%s [%ld]: unexpectedly exited with exit status 0,"
                " when we were waiting for it to confirm startup",
                ss->what, (unsigned long)pid);
        else if (status <= 127)
            LOG(ERROR, "%s [%ld]: failed startup with non-zero exit status %d",
                ss->what, (unsigned long)pid, status);
        else if (status < 255) {
            int sig = status - 128;
            const char *str = strsignal(sig);
            if (str)
                LOG(ERROR, "%s [%ld]: died during startup due to fatal"
                    " signal %s", ss->what, (unsigned long)pid, str);
            else
                LOG(ERROR, "%s [%ld]: died during startup due to unknown fatal"
                    " signal number %d", ss->what, (unsigned long)pid, sig);
        }
        ss->rc = ERROR_FAIL;
    }

    spawn_cleanup(gc, ss);

    if (ss->rc && !ss->detaching) {
        ss->failure_cb(egc, ss, ss->rc); /* must be last */
        return;
    }
    
    if (ss->rc && ss->detaching)
        LOG(WARN,"%s underlying machinery seemed to fail (%d),"
            " but its function seems to have been successful",
            ss->what, ss->rc);

    assert(ss->detaching);
    ss->detached_cb(egc, ss);
}

/*
 * Local variables:
 * mode: C
 * c-basic-offset: 4
 * indent-tabs-mode: nil
 * End:
 */
