/*
 * Copyright (C) 2009      Citrix Ltd.
 * Author Vincent Hanquez <vincent.hanquez@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"

/*====================== Domain suspend =======================*/

int libxl__domain_suspend_init(libxl__egc *egc,
                               libxl__domain_suspend_state *dsps,
                               libxl_domain_type type)
{
    STATE_AO_GC(dsps->ao);
    int rc = ERROR_FAIL;
    int port;

    /* Convenience aliases */
    const uint32_t domid = dsps->domid;

    libxl__xswait_init(&dsps->pvcontrol);
    libxl__ev_evtchn_init(&dsps->guest_evtchn);
    libxl__ev_xswatch_init(&dsps->guest_watch);
    libxl__ev_time_init(&dsps->guest_timeout);
    libxl__ev_qmp_init(&dsps->qmp);

    if (type == LIBXL_DOMAIN_TYPE_INVALID) goto out;
    dsps->type = type;

    dsps->guest_evtchn.port = -1;
    dsps->guest_evtchn_lockfd = -1;
    dsps->guest_responded = 0;
    dsps->dm_savefile = libxl__device_model_savefile(gc, domid);

    port = xs_suspend_evtchn_port(domid);

    if (port >= 0) {
        rc = libxl__ctx_evtchn_init(gc);
        if (rc) goto out;

        dsps->guest_evtchn.port =
            xc_suspend_evtchn_init_exclusive(CTX->xch, CTX->xce,
                                    domid, port, &dsps->guest_evtchn_lockfd);

        if (dsps->guest_evtchn.port < 0) {
            LOGD(WARN, domid, "Suspend event channel initialization failed");
            rc = ERROR_FAIL;
            goto out;
        }
    }

    rc = 0;

out:
    return rc;
}

void libxl__domain_suspend_dispose(libxl__gc *gc,
                                   libxl__domain_suspend_state  *dsps)
{
    libxl__xswait_stop(gc, &dsps->pvcontrol);
    libxl__ev_evtchn_cancel(gc, &dsps->guest_evtchn);
    libxl__ev_xswatch_deregister(gc, &dsps->guest_watch);
    libxl__ev_time_deregister(gc, &dsps->guest_timeout);
    libxl__ev_qmp_dispose(gc, &dsps->qmp);
}

/*----- callbacks, called by xc_domain_save -----*/

void libxl__domain_suspend_device_model(libxl__egc *egc,
                                       libxl__domain_suspend_state *dsps)
{
    STATE_AO_GC(dsps->ao);
    int rc = 0;
    uint32_t const domid = dsps->domid;
    const char *const filename = dsps->dm_savefile;

    switch (libxl__device_model_version_running(gc, domid)) {
    case LIBXL_DEVICE_MODEL_VERSION_QEMU_XEN_TRADITIONAL: {
        LOGD(DEBUG, domid, "Saving device model state to %s", filename);
        libxl__qemu_traditional_cmd(gc, domid, "save");
        libxl__wait_for_device_model_deprecated(gc, domid, "paused", NULL, NULL, NULL);
        break;
    }
    case LIBXL_DEVICE_MODEL_VERSION_QEMU_XEN:
        /* calls dsps->callback_device_model_done when done */
        libxl__qmp_suspend_save(egc, dsps); /* must be last */
        return;
    default:
        rc = ERROR_INVAL;
        goto out;
    }

out:
    if (rc)
        LOGD(ERROR, dsps->domid,
             "failed to suspend device model, rc=%d", rc);
    dsps->callback_device_model_done(egc, dsps, rc); /* must be last */
}

static void domain_suspend_common_wait_guest(libxl__egc *egc,
                                             libxl__domain_suspend_state *dsps);
static void domain_suspend_common_guest_suspended(libxl__egc *egc,
                                         libxl__domain_suspend_state *dsps);

static void domain_suspend_common_pvcontrol_suspending(libxl__egc *egc,
      libxl__xswait_state *xswa, int rc, const char *state);
static void domain_suspend_common_wait_guest_evtchn(libxl__egc *egc,
        libxl__ev_evtchn *evev);
static void suspend_common_wait_guest_watch(libxl__egc *egc,
      libxl__ev_xswatch *xsw, const char *watch_path, const char *event_path);
static void suspend_common_wait_guest_check(libxl__egc *egc,
        libxl__domain_suspend_state *dsps);
static void suspend_common_wait_guest_timeout(libxl__egc *egc,
      libxl__ev_time *ev, const struct timeval *requested_abs, int rc);

static void domain_suspend_common_done(libxl__egc *egc,
                                       libxl__domain_suspend_state *dsps,
                                       int rc);

static void domain_suspend_callback_common(libxl__egc *egc,
                                           libxl__domain_suspend_state *dsps);
static void domain_suspend_callback_common_done(libxl__egc *egc,
                                libxl__domain_suspend_state *dsps, int rc);

/* calls dsps->callback_common_done when done */
void libxl__domain_suspend(libxl__egc *egc,
                           libxl__domain_suspend_state *dsps)
{
    domain_suspend_callback_common(egc, dsps);
}

static bool domain_suspend_pvcontrol_acked(const char *state) {
    /* any value other than "suspend", including ENOENT (i.e. !state), is OK */
    if (!state) return 1;
    return strcmp(state,"suspend");
}

/* calls dsps->callback_common_done when done */
static void domain_suspend_callback_common(libxl__egc *egc,
                                           libxl__domain_suspend_state *dsps)
{
    STATE_AO_GC(dsps->ao);
    uint64_t hvm_s_state = 0, hvm_pvdrv = 0;
    int ret, rc;

    /* Convenience aliases */
    const uint32_t domid = dsps->domid;

    if (dsps->type != LIBXL_DOMAIN_TYPE_PV) {
        xc_hvm_param_get(CTX->xch, domid, HVM_PARAM_CALLBACK_IRQ, &hvm_pvdrv);
        xc_hvm_param_get(CTX->xch, domid, HVM_PARAM_ACPI_S_STATE, &hvm_s_state);
    }

    if ((hvm_s_state == 0) && (dsps->guest_evtchn.port >= 0)) {
        LOGD(DEBUG, domid, "issuing %s suspend request via event channel",
            dsps->type != LIBXL_DOMAIN_TYPE_PV ? "PVH/HVM" : "PV");
        ret = xenevtchn_notify(CTX->xce, dsps->guest_evtchn.port);
        if (ret < 0) {
            LOGD(ERROR, domid, "xenevtchn_notify failed ret=%d", ret);
            rc = ERROR_FAIL;
            goto err;
        }

        dsps->guest_evtchn.callback = domain_suspend_common_wait_guest_evtchn;
        rc = libxl__ev_evtchn_wait(gc, &dsps->guest_evtchn);
        if (rc) goto err;

        rc = libxl__ev_time_register_rel(ao, &dsps->guest_timeout,
                                         suspend_common_wait_guest_timeout,
                                         60*1000);
        if (rc) goto err;

        return;
    }

    if (dsps->type == LIBXL_DOMAIN_TYPE_HVM && (!hvm_pvdrv || hvm_s_state)) {
        LOGD(DEBUG, domid, "Calling xc_domain_shutdown on HVM domain");
        ret = xc_domain_shutdown(CTX->xch, domid, SHUTDOWN_suspend);
        if (ret < 0) {
            LOGED(ERROR, domid, "xc_domain_shutdown failed");
            rc = ERROR_FAIL;
            goto err;
        }
        /* The guest does not (need to) respond to this sort of request. */
        dsps->guest_responded = 1;
        domain_suspend_common_wait_guest(egc, dsps);
        return;
    }

    LOGD(DEBUG, domid, "issuing %s suspend request via XenBus control node",
        dsps->type != LIBXL_DOMAIN_TYPE_PV ? "PVH/HVM" : "PV");

    dsps->pvcontrol.ao = ao;
    dsps->pvcontrol.callback = domain_suspend_common_pvcontrol_suspending;
    rc = libxl__domain_pvcontrol(egc, &dsps->pvcontrol, domid, "suspend");
    if (rc) goto err;

    return;

 err:
    domain_suspend_common_done(egc, dsps, rc);
}

static void domain_suspend_common_wait_guest_evtchn(libxl__egc *egc,
        libxl__ev_evtchn *evev)
{
    libxl__domain_suspend_state *dsps = CONTAINER_OF(evev, *dsps, guest_evtchn);
    STATE_AO_GC(dsps->ao);
    /* If we should be done waiting, suspend_common_wait_guest_check
     * will end up calling domain_suspend_common_guest_suspended or
     * domain_suspend_common_done, both of which cancel the evtchn
     * wait as needed.  So re-enable it now. */
    libxl__ev_evtchn_wait(gc, &dsps->guest_evtchn);
    suspend_common_wait_guest_check(egc, dsps);
}

static void domain_suspend_common_pvcontrol_suspending(libxl__egc *egc,
      libxl__xswait_state *xswa, int rc, const char *state)
{
    libxl__domain_suspend_state *dsps = CONTAINER_OF(xswa, *dsps, pvcontrol);
    STATE_AO_GC(dsps->ao);
    xs_transaction_t t = 0;

    if (!rc && !domain_suspend_pvcontrol_acked(state))
        /* keep waiting */
        return;

    libxl__xswait_stop(gc, &dsps->pvcontrol);

    if (rc == ERROR_TIMEDOUT) {
        /*
         * Guest appears to not be responding. Cancel the suspend
         * request.
         *
         * We re-read the suspend node and clear it within a
         * transaction in order to handle the case where we race
         * against the guest catching up and acknowledging the request
         * at the last minute.
         */
        for (;;) {
            rc = libxl__xs_transaction_start(gc, &t);
            if (rc) goto err;

            rc = libxl__xs_read_checked(gc, t, xswa->path, &state);
            if (rc) goto err;

            if (domain_suspend_pvcontrol_acked(state))
                /* last minute ack */
                break;

            rc = libxl__xs_write_checked(gc, t, xswa->path, "");
            if (rc) goto err;

            rc = libxl__xs_transaction_commit(gc, &t);
            if (!rc) {
                LOGD(ERROR, dsps->domid,
                     "guest didn't acknowledge suspend, cancelling request");
                goto err;
            }
            if (rc<0) goto err;
        }
    } else if (rc) {
        /* some error in xswait's read of xenstore, already logged */
        goto err;
    }

    assert(domain_suspend_pvcontrol_acked(state));
    LOGD(DEBUG, dsps->domid, "guest acknowledged suspend request");

    libxl__xs_transaction_abort(gc, &t);
    dsps->guest_responded = 1;
    domain_suspend_common_wait_guest(egc,dsps);
    return;

 err:
    libxl__xs_transaction_abort(gc, &t);
    domain_suspend_common_done(egc, dsps, rc);
    return;
}

static void domain_suspend_common_wait_guest(libxl__egc *egc,
                                             libxl__domain_suspend_state *dsps)
{
    STATE_AO_GC(dsps->ao);
    int rc;

    LOGD(DEBUG, dsps->domid, "wait for the guest to suspend");

    rc = libxl__ev_xswatch_register(gc, &dsps->guest_watch,
                                    suspend_common_wait_guest_watch,
                                    "@releaseDomain");
    if (rc) goto err;

    rc = libxl__ev_time_register_rel(ao, &dsps->guest_timeout,
                                     suspend_common_wait_guest_timeout,
                                     60*1000);
    if (rc) goto err;
    return;

 err:
    domain_suspend_common_done(egc, dsps, rc);
}

static void suspend_common_wait_guest_watch(libxl__egc *egc,
      libxl__ev_xswatch *xsw, const char *watch_path, const char *event_path)
{
    libxl__domain_suspend_state *dsps = CONTAINER_OF(xsw, *dsps, guest_watch);
    suspend_common_wait_guest_check(egc, dsps);
}

static void suspend_common_wait_guest_check(libxl__egc *egc,
        libxl__domain_suspend_state *dsps)
{
    STATE_AO_GC(dsps->ao);
    xc_domaininfo_t info;
    int ret;
    int shutdown_reason;

    /* Convenience aliases */
    const uint32_t domid = dsps->domid;

    ret = xc_domain_getinfo_single(CTX->xch, domid, &info);
    if (ret < 0) {
        LOGED(ERROR, domid, "guest we were suspending has been destroyed");
        goto err;
    }

    if (!(info.flags & XEN_DOMINF_shutdown))
        /* keep waiting */
        return;

    shutdown_reason = (info.flags >> XEN_DOMINF_shutdownshift)
        & XEN_DOMINF_shutdownmask;
    if (shutdown_reason != SHUTDOWN_suspend) {
        LOGD(DEBUG, domid, "guest we were suspending has shut down"
             " with unexpected reason code %d", shutdown_reason);
        goto err;
    }

    LOGD(DEBUG, domid, "guest has suspended");
    domain_suspend_common_guest_suspended(egc, dsps);
    return;

 err:
    domain_suspend_common_done(egc, dsps, ERROR_FAIL);
}

static void suspend_common_wait_guest_timeout(libxl__egc *egc,
      libxl__ev_time *ev, const struct timeval *requested_abs, int rc)
{
    libxl__domain_suspend_state *dsps = CONTAINER_OF(ev, *dsps, guest_timeout);
    STATE_AO_GC(dsps->ao);
    if (rc == ERROR_TIMEDOUT) {
        LOGD(ERROR, dsps->domid, "guest did not suspend, timed out");
        rc = ERROR_GUEST_TIMEDOUT;
    }
    domain_suspend_common_done(egc, dsps, rc);
}

static void domain_suspend_common_guest_suspended(libxl__egc *egc,
                                         libxl__domain_suspend_state *dsps)
{
    STATE_AO_GC(dsps->ao);

    libxl__ev_evtchn_cancel(gc, &dsps->guest_evtchn);
    libxl__ev_xswatch_deregister(gc, &dsps->guest_watch);
    libxl__ev_time_deregister(gc, &dsps->guest_timeout);

    if (dsps->type == LIBXL_DOMAIN_TYPE_HVM) {
        dsps->callback_device_model_done = domain_suspend_common_done;
        libxl__domain_suspend_device_model(egc, dsps); /* must be last */
        return;
    }
    domain_suspend_common_done(egc, dsps, 0);
}

static void domain_suspend_common_done(libxl__egc *egc,
                                       libxl__domain_suspend_state *dsps,
                                       int rc)
{
    EGC_GC;
    assert(!libxl__xswait_inuse(&dsps->pvcontrol));
    libxl__domain_suspend_dispose(gc, dsps);
    dsps->callback_common_done(egc, dsps, rc);
}

void libxl__domain_suspend_callback(void *data)
{
    libxl__save_helper_state *shs = data;
    libxl__egc *egc = shs->egc;
    libxl__domain_save_state *dss = shs->caller_state;
    libxl__domain_suspend_state *dsps = &dss->dsps;

    dsps->callback_common_done = domain_suspend_callback_common_done;
    domain_suspend_callback_common(egc, dsps);
}

static void domain_suspend_callback_common_done(libxl__egc *egc,
                                libxl__domain_suspend_state *dsps, int rc)
{
    libxl__domain_save_state *dss = CONTAINER_OF(dsps, *dss, dsps);
    dss->rc = rc;
    libxl__xc_domain_saverestore_async_callback_done(egc, &dss->sws.shs, !rc);
}

/*======================= Domain resume ========================*/

int libxl__domain_resume_device_model_deprecated(libxl__gc *gc, uint32_t domid)
{
    const char *path, *state;

    switch (libxl__device_model_version_running(gc, domid)) {
    case LIBXL_DEVICE_MODEL_VERSION_QEMU_XEN_TRADITIONAL: {
        uint32_t dm_domid = libxl_get_stubdom_id(CTX, domid);

        path = DEVICE_MODEL_XS_PATH(gc, dm_domid, domid, "/state");
        state = libxl__xs_read(gc, XBT_NULL, path);
        if (state != NULL && !strcmp(state, "paused")) {
            libxl__qemu_traditional_cmd(gc, domid, "continue");
            libxl__wait_for_device_model_deprecated(gc, domid, "running",
                                                    NULL, NULL, NULL);
        }
        break;
    }
    case LIBXL_DEVICE_MODEL_VERSION_QEMU_XEN:
        if (libxl__qmp_resume(gc, domid))
            return ERROR_FAIL;
        break;
    default:
        return ERROR_INVAL;
    }

    return 0;
}

int libxl__domain_resume_deprecated(libxl__gc *gc, uint32_t domid, int suspend_cancel)
{
    int rc = 0;

    libxl_domain_type type = libxl__domain_type(gc, domid);
    if (type == LIBXL_DOMAIN_TYPE_INVALID) {
        rc = ERROR_FAIL;
        goto out;
    }

    if (type == LIBXL_DOMAIN_TYPE_HVM) {
        rc = libxl__domain_resume_device_model_deprecated(gc, domid);
        if (rc) {
            LOGD(ERROR, domid, "failed to resume device model:%d", rc);
            goto out;
        }
    }

    if (xc_domain_resume(CTX->xch, domid, suspend_cancel)) {
        LOGED(ERROR, domid, "xc_domain_resume failed");
        rc = ERROR_FAIL;
        goto out;
    }

    if (!xs_resume_domain(CTX->xsh, domid)) {
        LOGED(ERROR, domid, "xs_resume_domain failed");
        rc = ERROR_FAIL;
    }
out:
    return rc;
}

static void dm_resume_init(libxl__dm_resume_state *dmrs)
{
    libxl__ev_qmp_init(&dmrs->qmp);
    libxl__ev_time_init(&dmrs->time);
    libxl__ev_xswatch_init(&dmrs->watch);
}

static void dm_resume_dispose(libxl__gc *gc,
                              libxl__dm_resume_state *dmrs)
{
    libxl__ev_qmp_dispose(gc, &dmrs->qmp);
    libxl__ev_time_deregister(gc, &dmrs->time);
    libxl__ev_xswatch_deregister(gc, &dmrs->watch);
}

static void dm_resume_xswatch_cb(libxl__egc *egc,
    libxl__ev_xswatch *, const char *watch_path, const char *);
static void dm_resume_qmp_done(libxl__egc *egc,
    libxl__ev_qmp *qmp, const libxl__json_object *, int rc);
static void dm_resume_timeout(libxl__egc *egc,
    libxl__ev_time *, const struct timeval *, int rc);
static void dm_resume_done(libxl__egc *egc,
    libxl__dm_resume_state *dmrs, int rc);

void libxl__dm_resume(libxl__egc *egc,
                      libxl__dm_resume_state *dmrs)
{
    STATE_AO_GC(dmrs->ao);
    int rc = 0;

    /* Convenience aliases */
    libxl_domid domid = dmrs->domid;
    libxl__ev_qmp *qmp = &dmrs->qmp;

    dm_resume_init(dmrs);

    rc = libxl__ev_time_register_rel(dmrs->ao,
                                     &dmrs->time,
                                     dm_resume_timeout,
                                     LIBXL_DEVICE_MODEL_START_TIMEOUT * 1000);
    if (rc) goto out;

    switch (libxl__device_model_version_running(gc, domid)) {
    case LIBXL_DEVICE_MODEL_VERSION_QEMU_XEN_TRADITIONAL: {
        uint32_t dm_domid = libxl_get_stubdom_id(CTX, domid);
        const char *path, *state;

        path = DEVICE_MODEL_XS_PATH(gc, dm_domid, domid, "/state");
        rc = libxl__xs_read_checked(gc, XBT_NULL, path, &state);
        if (rc) goto out;
        if (!state || strcmp(state, "paused")) {
            /* already running */
            rc = 0;
            goto out;
        }

        rc = libxl__qemu_traditional_cmd(gc, domid, "continue");
        if (rc) goto out;
        rc = libxl__ev_xswatch_register(gc, &dmrs->watch,
                                        dm_resume_xswatch_cb,
                                        path);
        if (rc) goto out;
        break;
    }
    case LIBXL_DEVICE_MODEL_VERSION_QEMU_XEN:
        qmp->ao = dmrs->ao;
        qmp->domid = domid;
        qmp->callback = dm_resume_qmp_done;
        qmp->payload_fd = -1;
        rc = libxl__ev_qmp_send(egc, qmp, "cont", NULL);
        if (rc) goto out;
        break;
    default:
        rc = ERROR_INVAL;
        goto out;
    }

    return;

out:
    dm_resume_done(egc, dmrs, rc);
}

static void dm_resume_xswatch_cb(libxl__egc *egc,
                                 libxl__ev_xswatch *xsw,
                                 const char *watch_path,
                                 const char *event_path)
{
    EGC_GC;
    libxl__dm_resume_state *dmrs = CONTAINER_OF(xsw, *dmrs, watch);
    int rc;
    const char *value;

    rc = libxl__xs_read_checked(gc, XBT_NULL, watch_path, &value);
    if (rc) goto out;

    if (!value || strcmp(value, "running"))
        return;

    rc = 0;
out:
    dm_resume_done(egc, dmrs, rc);
}

static void dm_resume_qmp_done(libxl__egc *egc,
                               libxl__ev_qmp *qmp,
                               const libxl__json_object *response,
                               int rc)
{
    libxl__dm_resume_state *dmrs = CONTAINER_OF(qmp, *dmrs, qmp);
    dm_resume_done(egc, dmrs, rc);
}

static void dm_resume_timeout(libxl__egc *egc,
                              libxl__ev_time *ev,
                              const struct timeval *requested_abs,
                              int rc)
{
    libxl__dm_resume_state *dmrs = CONTAINER_OF(ev, *dmrs, time);
    dm_resume_done(egc, dmrs, rc);
}

static void dm_resume_done(libxl__egc *egc,
                           libxl__dm_resume_state *dmrs,
                           int rc)
{
    EGC_GC;

    if (rc) {
        LOGD(ERROR, dmrs->domid,
             "Failed to resume device model: rc=%d", rc);
    }

    dm_resume_dispose(gc, dmrs);
    dmrs->dm_resumed_callback(egc, dmrs, rc);
}


static void domain_resume_done(libxl__egc *egc,
                               libxl__dm_resume_state *dmrs, int rc);

void libxl__domain_resume(libxl__egc *egc,
                          libxl__dm_resume_state *dmrs,
                          bool suspend_cancel)
{
    STATE_AO_GC(dmrs->ao);
    int rc = 0;
    libxl_domain_type type = libxl__domain_type(gc, dmrs->domid);

    if (type == LIBXL_DOMAIN_TYPE_INVALID) {
        rc = ERROR_FAIL;
        goto out;
    }

    dmrs->suspend_cancel = suspend_cancel;

    if (type != LIBXL_DOMAIN_TYPE_HVM) {
        rc = 0;
        goto out;
    }

    dmrs->dm_resumed_callback = domain_resume_done;
    libxl__dm_resume(egc, dmrs); /* must be last */
    return;

out:
    domain_resume_done(egc, dmrs, rc);
}

static void domain_resume_done(libxl__egc *egc,
                               libxl__dm_resume_state *dmrs, int rc)
{
    EGC_GC;

    /* Convenience aliases */
    libxl_domid domid = dmrs->domid;

    if (rc) goto out;

    if (xc_domain_resume(CTX->xch, domid, dmrs->suspend_cancel)) {
        LOGED(ERROR, domid, "xc_domain_resume failed");
        rc = ERROR_FAIL;
        goto out;
    }

    if (!xs_resume_domain(CTX->xsh, domid)) {
        LOGED(ERROR, domid, "xs_resume_domain failed");
        rc = ERROR_FAIL;
    }
out:
    dmrs->callback(egc, dmrs, rc);
}

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