/*
   Unix SMB/CIFS implementation.

   Copyright (C) Stefan Metzmacher 2008

   This program is free software; you can redistribute it and/or modify
   it under the terms of the GNU General Public License as published by
   the Free Software Foundation; either version 3 of the License, or
   (at your option) any later version.

   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 General Public License for more details.

   You should have received a copy of the GNU General Public License
   along with this program.  If not, see <http://www.gnu.org/licenses/>.
*/

/*
  lease (oplock) implementation using fcntl F_SETLEASE on linux
*/

#include "includes.h"
#include <tevent.h>
#include "system/filesys.h"
#include "ntvfs/sysdep/sys_lease.h"
#include "ntvfs/ntvfs.h"
#include "librpc/gen_ndr/ndr_opendb.h"
#include "../lib/util/dlinklist.h"
#include "cluster/cluster.h"

NTSTATUS sys_lease_linux_init(void);

#define LINUX_LEASE_RT_SIGNAL (SIGRTMIN+1)

struct linux_lease_pending {
	struct linux_lease_pending *prev, *next;
	struct sys_lease_context *ctx;
	struct opendb_entry e;
};

/* the global linked list of pending leases */
static struct linux_lease_pending *leases;

static void linux_lease_signal_handler(struct tevent_context *ev_ctx,
				       struct tevent_signal *se,
				       int signum, int count,
				       void *_info, void *private_data)
{
	struct sys_lease_context *ctx = talloc_get_type(private_data,
					struct sys_lease_context);
	siginfo_t *info = (siginfo_t *)_info;
	struct linux_lease_pending *c;
	int got_fd = info->si_fd;

	for (c = leases; c; c = c->next) {
		int *fd = (int *)c->e.fd;

		if (got_fd == *fd) {
			break;
		}
	}

	if (!c) {
		return;
	}

	ctx->break_send(ctx->msg_ctx, &c->e, OPLOCK_BREAK_TO_NONE);
}

static int linux_lease_pending_destructor(struct linux_lease_pending *p)
{
	int ret;
	int *fd = (int *)p->e.fd;

	DLIST_REMOVE(leases, p);

	if (*fd == -1) {
		return 0;
	}

	ret = fcntl(*fd, F_SETLEASE, F_UNLCK);
	if (ret == -1) {
		DEBUG(0,("%s: failed to remove oplock: %s\n",
			__FUNCTION__, strerror(errno)));
	}

	return 0;
}

static NTSTATUS linux_lease_init(struct sys_lease_context *ctx)
{
	struct tevent_signal *se;

	se = tevent_add_signal(ctx->event_ctx, ctx,
			       LINUX_LEASE_RT_SIGNAL, SA_SIGINFO,
			       linux_lease_signal_handler, ctx);
	NT_STATUS_HAVE_NO_MEMORY(se);

	return NT_STATUS_OK;
}

static NTSTATUS linux_lease_setup(struct sys_lease_context *ctx,
				  struct opendb_entry *e)
{
	int ret;
	int *fd = (int *)e->fd;
	struct linux_lease_pending *p;

	if (e->oplock_level == OPLOCK_NONE) {
		e->fd = NULL;
		return NT_STATUS_OK;
	} else if (e->oplock_level == OPLOCK_LEVEL_II) {
		/*
		 * the linux kernel doesn't support level2 oplocks
		 * so fix up the granted oplock level
		 */
		e->oplock_level = OPLOCK_NONE;
		e->allow_level_II_oplock = false;
		e->fd = NULL;
		return NT_STATUS_OK;
	}

	p = talloc(ctx, struct linux_lease_pending);
	NT_STATUS_HAVE_NO_MEMORY(p);

	p->ctx = ctx;
	p->e = *e;

	ret = fcntl(*fd, F_SETSIG, LINUX_LEASE_RT_SIGNAL);
	if (ret == -1) {
		talloc_free(p);
		return map_nt_error_from_unix_common(errno);
	}

	ret = fcntl(*fd, F_SETLEASE, F_WRLCK);
	if (ret == -1) {
		talloc_free(p);
		return map_nt_error_from_unix_common(errno);
	}

	DLIST_ADD(leases, p);

	talloc_set_destructor(p, linux_lease_pending_destructor);

	return NT_STATUS_OK;
}

static NTSTATUS linux_lease_remove(struct sys_lease_context *ctx,
				   struct opendb_entry *e);

static NTSTATUS linux_lease_update(struct sys_lease_context *ctx,
				   struct opendb_entry *e)
{
	struct linux_lease_pending *c;

	for (c = leases; c; c = c->next) {
		if (c->e.fd == e->fd) {
			break;
		}
	}

	if (!c) {
		return NT_STATUS_OBJECT_NAME_NOT_FOUND;
	}

	/*
	 * set the fd pointer to NULL so that the caller
	 * will not call the remove function as the oplock
	 * is already removed
	 */
	e->fd = NULL;

	talloc_free(c);

	return NT_STATUS_OK;
}

static NTSTATUS linux_lease_remove(struct sys_lease_context *ctx,
				   struct opendb_entry *e)
{
	struct linux_lease_pending *c;

	for (c = leases; c; c = c->next) {
		if (c->e.fd == e->fd) {
			break;
		}
	}

	if (!c) {
		return NT_STATUS_OBJECT_NAME_NOT_FOUND;
	}

	talloc_free(c);

	return NT_STATUS_OK;
}

static struct sys_lease_ops linux_lease_ops = {
	.name	= "linux",
	.init	= linux_lease_init,
	.setup	= linux_lease_setup,
	.update	= linux_lease_update,
	.remove	= linux_lease_remove
};

/*
  initialialise the linux lease module
 */
NTSTATUS sys_lease_linux_init(void)
{
	/* register ourselves as a system lease module */
	return sys_lease_register(&linux_lease_ops);
}
