/*
 * kernel/power/tuxonice_storage.c
 *
 * Copyright (C) 2005-2015 Nigel Cunningham (nigel at nigelcunningham com au)
 *
 * This file is released under the GPLv2.
 *
 * Routines for talking to a userspace program that manages storage.
 *
 * The kernel side:
 * - starts the userspace program;
 * - sends messages telling it when to open and close the connection;
 * - tells it when to quit;
 *
 * The user space side:
 * - passes messages regarding status;
 *
 */

#include <linux/suspend.h>
#include <linux/freezer.h>

#include "tuxonice_sysfs.h"
#include "tuxonice_modules.h"
#include "tuxonice_netlink.h"
#include "tuxonice_storage.h"
#include "tuxonice_ui.h"

static struct user_helper_data usm_helper_data;
static struct toi_module_ops usm_ops;
static int message_received, usm_prepare_count;
static int storage_manager_last_action, storage_manager_action;

static int usm_user_rcv_msg(struct sk_buff *skb, struct nlmsghdr *nlh)
{
        int type;
        int *data;

        type = nlh->nlmsg_type;

        /* A control message: ignore them */
        if (type < NETLINK_MSG_BASE)
                return 0;

        /* Unknown message: reply with EINVAL */
        if (type >= USM_MSG_MAX)
                return -EINVAL;

        /* All operations require privileges, even GET */
        if (!capable(CAP_NET_ADMIN))
                return -EPERM;

        /* Only allow one task to receive NOFREEZE privileges */
        if (type == NETLINK_MSG_NOFREEZE_ME && usm_helper_data.pid != -1)
                return -EBUSY;

        data = (int *) NLMSG_DATA(nlh);

        switch (type) {
        case USM_MSG_SUCCESS:
        case USM_MSG_FAILED:
                message_received = type;
                complete(&usm_helper_data.wait_for_process);
                break;
        default:
                printk(KERN_INFO "Storage manager doesn't recognise "
                                "message %d.\n", type);
        }

        return 1;
}

#ifdef CONFIG_NET
static int activations;

int toi_activate_storage(int force)
{
        int tries = 1;

        if (usm_helper_data.pid == -1 || !usm_ops.enabled)
                return 0;

        message_received = 0;
        activations++;

        if (activations > 1 && !force)
                return 0;

        while ((!message_received || message_received == USM_MSG_FAILED) &&
                        tries < 2) {
                toi_prepare_status(DONT_CLEAR_BAR, "Activate storage attempt "
                                "%d.\n", tries);

                init_completion(&usm_helper_data.wait_for_process);

                toi_send_netlink_message(&usm_helper_data,
                        USM_MSG_CONNECT,
                        NULL, 0);

                /* Wait 2 seconds for the userspace process to make contact */
                wait_for_completion_timeout(&usm_helper_data.wait_for_process,
                                2*HZ);

                tries++;
        }

        return 0;
}

int toi_deactivate_storage(int force)
{
        if (usm_helper_data.pid == -1 || !usm_ops.enabled)
                return 0;

        message_received = 0;
        activations--;

        if (activations && !force)
                return 0;

        init_completion(&usm_helper_data.wait_for_process);

        toi_send_netlink_message(&usm_helper_data,
                        USM_MSG_DISCONNECT,
                        NULL, 0);

        wait_for_completion_timeout(&usm_helper_data.wait_for_process, 2*HZ);

        if (!message_received || message_received == USM_MSG_FAILED) {
                printk(KERN_INFO "Returning failure disconnecting storage.\n");
                return 1;
        }

        return 0;
}
#endif

static void storage_manager_simulate(void)
{
        printk(KERN_INFO "--- Storage manager simulate ---\n");
        toi_prepare_usm();
        schedule();
        printk(KERN_INFO "--- Activate storage 1 ---\n");
        toi_activate_storage(1);
        schedule();
        printk(KERN_INFO "--- Deactivate storage 1 ---\n");
        toi_deactivate_storage(1);
        schedule();
        printk(KERN_INFO "--- Cleanup usm ---\n");
        toi_cleanup_usm();
        schedule();
        printk(KERN_INFO "--- Storage manager simulate ends ---\n");
}

static int usm_storage_needed(void)
{
        return sizeof(int) + strlen(usm_helper_data.program) + 1;
}

static int usm_save_config_info(char *buf)
{
        int len = strlen(usm_helper_data.program);
        memcpy(buf, usm_helper_data.program, len + 1);
        return sizeof(int) + len + 1;
}

static void usm_load_config_info(char *buf, int size)
{
        /* Don't load the saved path if one has already been set */
        if (usm_helper_data.program[0])
                return;

        memcpy(usm_helper_data.program, buf + sizeof(int), *((int *) buf));
}

static int usm_memory_needed(void)
{
        /* ball park figure of 32 pages */
        return 32 * PAGE_SIZE;
}

/* toi_prepare_usm
 */
int toi_prepare_usm(void)
{
        usm_prepare_count++;

        if (usm_prepare_count > 1 || !usm_ops.enabled)
                return 0;

        usm_helper_data.pid = -1;

        if (!*usm_helper_data.program)
                return 0;

        toi_netlink_setup(&usm_helper_data);

        if (usm_helper_data.pid == -1)
                printk(KERN_INFO "TuxOnIce Storage Manager wanted, but couldn't"
                                " start it.\n");

        toi_activate_storage(0);

        return usm_helper_data.pid != -1;
}

void toi_cleanup_usm(void)
{
        usm_prepare_count--;

        if (usm_helper_data.pid > -1 && !usm_prepare_count) {
                toi_deactivate_storage(0);
                toi_netlink_close(&usm_helper_data);
        }
}

static void storage_manager_activate(void)
{
        if (storage_manager_action == storage_manager_last_action)
                return;

        if (storage_manager_action)
                toi_prepare_usm();
        else
                toi_cleanup_usm();

        storage_manager_last_action = storage_manager_action;
}

/*
 * User interface specific /sys/power/tuxonice entries.
 */

static struct toi_sysfs_data sysfs_params[] = {
        SYSFS_NONE("simulate_atomic_copy", storage_manager_simulate),
        SYSFS_INT("enabled", SYSFS_RW, &usm_ops.enabled, 0, 1, 0, NULL),
        SYSFS_STRING("program", SYSFS_RW, usm_helper_data.program, 254, 0,
                NULL),
        SYSFS_INT("activate_storage", SYSFS_RW , &storage_manager_action, 0, 1,
                        0, storage_manager_activate)
};

static struct toi_module_ops usm_ops = {
        .type                                = MISC_MODULE,
        .name                                = "usm",
        .directory                        = "storage_manager",
        .module                                = THIS_MODULE,
        .storage_needed                        = usm_storage_needed,
        .save_config_info                = usm_save_config_info,
        .load_config_info                = usm_load_config_info,
        .memory_needed                        = usm_memory_needed,

        .sysfs_data                        = sysfs_params,
        .num_sysfs_entries                = sizeof(sysfs_params) /
                sizeof(struct toi_sysfs_data),
};

/* toi_usm_sysfs_init
 * Description: Boot time initialisation for user interface.
 */
int toi_usm_init(void)
{
        usm_helper_data.nl = NULL;
        usm_helper_data.program[0] = '\0';
        usm_helper_data.pid = -1;
        usm_helper_data.skb_size = 0;
        usm_helper_data.pool_limit = 6;
        usm_helper_data.netlink_id = NETLINK_TOI_USM;
        usm_helper_data.name = "userspace storage manager";
        usm_helper_data.rcv_msg = usm_user_rcv_msg;
        usm_helper_data.interface_version = 2;
        usm_helper_data.must_init = 0;
        init_completion(&usm_helper_data.wait_for_process);

        return toi_register_module(&usm_ops);
}

void toi_usm_exit(void)
{
        toi_netlink_close_complete(&usm_helper_data);
        toi_unregister_module(&usm_ops);
}