/*
 * Copyright (C) 2001-2004 Sistina Software, Inc. All rights reserved.
 * Copyright (C) 2004-2014 Red Hat, Inc. All rights reserved.
 *
 * This file is part of LVM2.
 *
 * This copyrighted material is made available to anyone wishing to use,
 * modify, copy, or redistribute it subject to the terms and conditions
 * of the GNU Lesser General Public License v.2.1.
 *
 * You should have received a copy of the GNU Lesser General Public License
 * along with this program; if not, write to the Free Software Foundation,
 * Inc., 51 Franklin Street, Fifth Floor, Boston, MA 02110-1301 USA
 */

#include "lib/misc/lib.h"
#include "lib/config/config.h"
#include "lib/misc/lvm-flock.h"
#include "lib/misc/lvm-signal.h"
#include "lib/locking/locking.h"

#include <sys/file.h>
#include <fcntl.h>

struct lock_list {
	struct dm_list list;
	int lf;
	char *res;
};

static struct dm_list _lock_list;
static int _prioritise_write_locks;

/* Drop lock known to be shared with another file descriptor. */
static void _drop_shared_flock(const char *file, int fd)
{
	log_debug_locking("_drop_shared_flock %s.", file);

	if (close(fd) < 0)
		log_sys_debug("close", file);
}

static void _undo_flock(const char *file, int fd)
{
	struct stat buf1, buf2;

	log_debug_locking("_undo_flock %s", file);
	if (!flock(fd, LOCK_NB | LOCK_EX) &&
	    !stat(file, &buf1) &&
	    !fstat(fd, &buf2) &&
	    is_same_inode(buf1, buf2))
		if (unlink(file) && (errno != ENOENT))
			log_sys_debug("unlink", file);

	if (close(fd) < 0)
		log_sys_debug("close", file);
}

static struct lock_list *_get_lock_list_entry(const char *file)
{
	struct lock_list *ll;
	struct dm_list *llh;

	dm_list_iterate(llh, &_lock_list) {
		ll = dm_list_item(llh, struct lock_list);

		if (!strcmp(ll->res, file))
			return ll;
	}
	return NULL;
}

static int _release_lock(const char *file, int unlock)
{
	struct lock_list *ll;
	struct dm_list *llh, *llt;

	dm_list_iterate_safe(llh, llt, &_lock_list) {
		ll = dm_list_item(llh, struct lock_list);

		if (!file || !strcmp(ll->res, file)) {
			dm_list_del(llh);
			if (unlock) {
				log_very_verbose("Unlocking %s", ll->res);
				if (flock(ll->lf, LOCK_NB | LOCK_UN))
					log_sys_debug("flock", ll->res);
				_undo_flock(ll->res, ll->lf);
			} else
				_drop_shared_flock(ll->res, ll->lf);

			free(ll->res);
			free(llh);

			if (file)
				return 1;
		}
	}

	return 0;
}

void release_flocks(int unlock)
{
	_release_lock(NULL, unlock);
}

static int _do_flock(const char *file, int *fd, int operation, uint32_t nonblock)
{
	int r;
	int old_errno;
	struct stat buf1, buf2;

	log_debug_locking("_do_flock %s %c%c", file,
			  operation == LOCK_EX ? 'W' : 'R', nonblock ? ' ' : 'B');
	do {
		if ((*fd > -1) && close(*fd))
			log_sys_debug("close", file);

		if ((*fd = open(file, O_CREAT | O_APPEND | O_RDWR, 0777)) < 0) {
			log_sys_error("open", file);
			return 0;
		}

		if (nonblock)
			operation |= LOCK_NB;
		else
			sigint_allow();

		r = flock(*fd, operation);
		old_errno = errno;
		if (!nonblock) {
			sigint_restore();
			if (sigint_caught()) {
				log_error("Giving up waiting for lock.");
				break;
			}
		}

		if (r) {
			errno = old_errno;
			log_sys_error("flock", file);
			break;
		}

		if (!stat(file, &buf1) && !fstat(*fd, &buf2) &&
		    is_same_inode(buf1, buf2))
			return 1;
	} while (!nonblock);

	if (close(*fd))
		log_sys_debug("close", file);
	*fd = -1;

	return_0;
}

#define AUX_LOCK_SUFFIX ":aux"

static int _do_write_priority_flock(const char *file, int *fd, int operation, uint32_t nonblock)
{
	int r, fd_aux = -1;
	char file_aux[PATH_MAX];

	snprintf(file_aux, sizeof(file_aux), "%s%s", file, AUX_LOCK_SUFFIX);

	if ((r = _do_flock(file_aux, &fd_aux, LOCK_EX, nonblock))) {
		if (operation == LOCK_EX) {
			r = _do_flock(file, fd, operation, nonblock);
			_undo_flock(file_aux, fd_aux);
		} else {
			_undo_flock(file_aux, fd_aux);
			r = _do_flock(file, fd, operation, nonblock);
		}
	}

	return r;
}

int lock_file(const char *file, uint32_t flags)
{
	int operation;
	uint32_t nonblock = flags & LCK_NONBLOCK;
	uint32_t convert = flags & LCK_CONVERT;
	int r;
	struct lock_list *ll;
	char state;

	switch (flags & LCK_TYPE_MASK) {
	case LCK_READ:
		operation = LOCK_SH;
		state = 'R';
		break;
	case LCK_WRITE:
		operation = LOCK_EX;
		state = 'W';
		break;
	case LCK_UNLOCK:
		return _release_lock(file, 1);
	default:
		log_error("Unrecognised lock type: %d", flags & LCK_TYPE_MASK);
		return 0;
	}

	if (convert) {
		if (nonblock)
			operation |= LOCK_NB;
		if (!(ll = _get_lock_list_entry(file)))
			return 0;
		log_very_verbose("Locking %s %c%c convert", ll->res, state,
			 	 nonblock ? ' ' : 'B');
		r = flock(ll->lf, operation);
		if (!r)
			return 1;
		log_error("Failed to convert flock on %s %d", file, errno);
		return 0;
	}

	if (!(ll = malloc(sizeof(struct lock_list))))
		return_0;

	if (!(ll->res = strdup(file))) {
		free(ll);
		return_0;
	}

	ll->lf = -1;

	log_very_verbose("Locking %s %c%c", ll->res, state,
			 nonblock ? ' ' : 'B');

	(void) dm_prepare_selinux_context(file, S_IFREG);
	if (_prioritise_write_locks)
		r = _do_write_priority_flock(file, &ll->lf, operation, nonblock);
	else 
		r = _do_flock(file, &ll->lf, operation, nonblock);
	(void) dm_prepare_selinux_context(NULL, 0);

	if (r)
		dm_list_add(&_lock_list, &ll->list);
	else {
		free(ll->res);
		free(ll);
		stack;
	}

	return r;
}

void init_flock(struct cmd_context *cmd)
{
	dm_list_init(&_lock_list);

	_prioritise_write_locks =
	    find_config_tree_bool(cmd, global_prioritise_write_locks_CFG, NULL);
}