/*
 * zdev - Modify and display the persistent configuration of devices
 *
 * Copyright IBM Corp. 2016, 2017
 *
 * s390-tools is free software; you can redistribute it and/or modify
 * it under the terms of the MIT license. See LICENSE for details.
 */

#include <dirent.h>
#include <stdlib.h>
#include <string.h>
#include <sys/stat.h>
#include <sys/types.h>

#include "lib/util_path.h"

#include "attrib.h"
#include "misc.h"
#include "module.h"
#include "path.h"
#include "setting.h"
#include "udev.h"

static struct util_list *tried_loading;
static int suppress_module_load;

/* Allow suppression of module loading. */
void module_load_suppress(int state)
{
	suppress_module_load = state;
}

/* Check if a module is currently loaded. */
bool module_loaded(const char *mod)
{
	char *path = path_get_sys_module(mod);
	bool rc;

	rc = util_path_is_dir(path);
	free(path);

	return rc;
}

static int module_get_refcnt(const char *mod)
{
	char *path, *refcnt_path, *text;
	int refcnt = 0;

	path = path_get_sys_module(mod);
	refcnt_path = misc_asprintf("%s/refcnt", path);
	text = misc_read_text_file(refcnt_path, 1, err_delayed_print);
	if (text)
		refcnt = atoi(text);
	free(refcnt_path);
	free(path);

	return refcnt;
}

/* Attempt to unload specified kernel module. */
static exit_code_t module_unload(const char *mod, err_t err)
{
	char *mp;
	int rc;

	if (!dryrun && module_get_refcnt(mod) > 0) {
		err_t_print(err, "Cannot unload module %s: Module is in use\n",
			    mod);
		return EXIT_MOD_BUSY;
	}

	mp = path_get_modprobe();
	rc = misc_system(err, "%s -r %s", mp, mod);
	free(mp);
	if (rc != 0)
		return EXIT_MOD_UNLOAD_FAILED;

	return EXIT_OK;
}

/* Attempt to load kernel module with specified parameters. PARAMS may be
 * NULL to load the module with default parameters. */
static exit_code_t do_load(const char *mod, char *params, err_t err)
{
	char *empty_file;
	char *mp;
	exit_code_t rc;

	rc = EXIT_OK;
	mp = path_get_modprobe();
	if (params) {
		/* Note: We need to pass an empty configuration file to
		 * modprobe or the persistent parameters in /etc/modprobe.d
		 * would always overwrite the specified parameters. */
		rc = misc_mktemp(&empty_file, NULL);
		if (rc)
			goto out;
		if (misc_system(err, "%s %s %s -C %s %s", mp, mod, params,
				empty_file,
				err == err_ignore ? " 2>/dev/null" : ""))
			rc = EXIT_MOD_LOAD_FAILED;
		remove_file(empty_file);
		free(empty_file);
	} else {
		if (misc_system(err, "%s %s %s", mp, mod,
				err == err_ignore ? " 2>/dev/null" : ""))
			rc = EXIT_MOD_LOAD_FAILED;
	}

out:
	free(mp);

	return rc;
}

/* Apply kernel module parameters. */
exit_code_t module_load(const char *mod, const char **deps,
			struct setting_list *settings, err_t err)
{
	char *params;
	struct util_list *unloaded = NULL;
	struct strlist_node *s;
	exit_code_t rc;
	int i;

	if (suppress_module_load)
		return EXIT_OK;

	/* Unload modules depending on mod. */
	if (deps) {
		unloaded = strlist_new();
		for (i = 0; deps[i]; i++) {
			if (!module_loaded(deps[i]) && !dryrun)
				continue;
			rc = module_unload(deps[i], err);
			if (rc)
				goto out;
			strlist_add(unloaded, "%s", deps[i]);
		}
	}

	if (module_loaded(mod) || dryrun) {
		rc = module_unload(mod, err);
		if (rc)
			goto out;
	}

	params = settings ? setting_list_flatten(settings) : NULL;
	rc = do_load(mod, params, err);
	free(params);

	if (rc == EXIT_OK && unloaded) {
		/* Re-load modules depending on mod. */
		util_list_iterate(unloaded, s) {
			rc = do_load(s->str, "", err);
			if (rc)
				break;
		}
	}
out:
	strlist_free(unloaded);

	return rc;
}

struct add_setting_data {
	struct setting_list *list;
	struct attrib **attribs;
};

/* Add all attribute values to settings list. */
static exit_code_t add_setting(const char *path, const char *name, void *data)
{
	struct add_setting_data *sdata = data;
	struct setting_list *list = sdata->list;
	struct attrib **attribs = sdata->attribs;
	struct attrib *a;
	char *value;

	value = misc_read_text_file(path, 1, err_print);
	if (!value)
		return EXIT_RUNTIME_ERROR;
	if (strcmp(value, "(null)") == 0)
		goto out;
	a = attrib_find(attribs, name);
	setting_list_apply_actual(list, a, name, value);
out:
	free(value);

	return EXIT_OK;
}

/* Retrieve currently active module parameters. */
exit_code_t module_get_params(const char *mod, struct attrib **attribs,
			      struct setting_list **settings)
{
	struct add_setting_data data;
	struct setting_list *list = NULL;
	char *path;
	exit_code_t rc = EXIT_OK;

	if (!module_loaded(mod))
		goto out;
	list = setting_list_new();

	path = path_get_sys_module_param(mod, NULL);
	data.list = list;
	data.attribs = attribs;
	rc = path_for_each(path, add_setting, &data);
	free(path);

out:
	if (rc == EXIT_OK)
		*settings = list;
	else
		setting_list_free(list);

	return rc;
}

/* Try to load a kernel module once. If @path is not %NULL, don't try loading
 * the module if @path exists. */
void module_try_load_once(const char *mod, const char *path)
{
	if (suppress_module_load)
		return;
	if (tried_loading) {
		if (strlist_find(tried_loading, mod))
			return;
	} else
		tried_loading = strlist_new();
	strlist_add(tried_loading, mod);
	if (path && util_path_exists(path))
		return;
	if (module_loaded(mod))
		return;

	verb("Loading required kernel module: %s\n", mod);
	if (module_load(mod, NULL, NULL, err_ignore))
		verb("Failed to load kernel module: %s\n", mod);
	else {
		/* Let udev rules apply. */
		udev_settle();
	}
}

/* Release any global memory. */
void module_exit(void)
{
	strlist_free(tried_loading);
}

/* Apply module parameters for kernel module @mod found in @settings via
 * /sys/module/../parameters. Return %true if all modified settings
 * could be set via this method. */
bool module_set_params(const char *mod, struct setting_list *settings)
{
	struct setting *s;
	char *path;
	const char *value;
	bool readonly;
	exit_code_t rc;

	/* First check if all modified settings can be applied this way. */
	util_list_iterate(&settings->list, s) {
		if (!s->modified && !s->removed) {
			/* Nothing to do. */
			continue;
		}
		if (!s->attrib || !s->attrib->nounload) {
			/* Attribute does not support setting via Sysfs. */
			return false;
		}
		if (s->removed && !s->attrib->defval) {
			/* Cannot remove setting with no default value. */
			return false;
		}
		path = path_get_sys_module_param(mod, s->name);
		readonly = util_path_is_readonly_file("%s", path);
		free(path);
		if (readonly) {
			/* Sysfs file is not writable. */
			return false;
		}
	}

	/* Apply settings. */
	util_list_iterate(&settings->list, s) {
		if (s->removed)
			value = s->attrib->defval;
		else if (s->modified)
			value = s->value;
		else {
			/* Nothing to do. */
			continue;
		}

		path = path_get_sys_module_param(mod, s->name);
		rc = misc_write_text_file(path, value, err_ignore);
		free(path);
		if (rc)
			return false;
	}

	return true;
}
