/* 
   Unix SMB/CIFS implementation.

   POSIX NTVFS backend - locking

   Copyright (C) Andrew Tridgell 2004

   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/>.
*/

#include "includes.h"
#include "vfs_posix.h"
#include "system/time.h"
#include "../lib/util/dlinklist.h"
#include "messaging/messaging.h"


/*
  check if we can perform IO on a range that might be locked
*/
NTSTATUS pvfs_check_lock(struct pvfs_state *pvfs,
			 struct pvfs_file *f,
			 uint32_t smbpid,
			 uint64_t offset, uint64_t count,
			 enum brl_type rw)
{
	if (!(pvfs->flags & PVFS_FLAG_STRICT_LOCKING)) {
		return NT_STATUS_OK;
	}

	return brl_locktest(pvfs->brl_context,
			    f->brl_handle,
			    smbpid,
			    offset, count, rw);
}

/* this state structure holds information about a lock we are waiting on */
struct pvfs_pending_lock {
	struct pvfs_pending_lock *next, *prev;
	struct pvfs_state *pvfs;
	union smb_lock *lck;
	struct pvfs_file *f;
	struct ntvfs_request *req;
	int pending_lock;
	struct pvfs_wait *wait_handle;
	struct timeval end_time;
};

/*
  a secondary attempt to setup a lock has failed - back out
  the locks we did get and send an error
*/
static void pvfs_lock_async_failed(struct pvfs_state *pvfs,
				   struct ntvfs_request *req,
				   struct pvfs_file *f,
				   struct smb_lock_entry *locks,
				   int i,
				   NTSTATUS status)
{
	/* undo the locks we just did */
	for (i--;i>=0;i--) {
		brl_unlock(pvfs->brl_context,
			   f->brl_handle,
			   locks[i].pid,
			   locks[i].offset,
			   locks[i].count);
		f->lock_count--;
	}
	req->async_states->status = status;
	req->async_states->send_fn(req);
}


/*
  called when we receive a pending lock notification. It means that
  either our lock timed out or someone else has unlocked a overlapping
  range, so we should try the lock again. Note that on timeout we
  do retry the lock, giving it a last chance.
*/
static void pvfs_pending_lock_continue(void *private_data, enum pvfs_wait_notice reason)
{
	struct pvfs_pending_lock *pending = talloc_get_type(private_data,
					    struct pvfs_pending_lock);
	struct pvfs_state *pvfs = pending->pvfs;
	struct pvfs_file *f = pending->f;
	struct ntvfs_request *req = pending->req;
	union smb_lock *lck = pending->lck;
	struct smb_lock_entry *locks;
	enum brl_type rw;
	NTSTATUS status;
	int i;
	bool timed_out;

	timed_out = (reason != PVFS_WAIT_EVENT);

	locks = lck->lockx.in.locks + lck->lockx.in.ulock_cnt;

	if (lck->lockx.in.mode & LOCKING_ANDX_SHARED_LOCK) {
		rw = READ_LOCK;
	} else {
		rw = WRITE_LOCK;
	}

	DLIST_REMOVE(f->pending_list, pending);

	/* we don't retry on a cancel */
	if (reason == PVFS_WAIT_CANCEL) {
		if (pvfs->ntvfs->ctx->protocol != PROTOCOL_SMB2) {
			status = NT_STATUS_FILE_LOCK_CONFLICT;
		} else {
			status = NT_STATUS_CANCELLED;
		}
	} else {
		/* 
		 * here it's important to pass the pending pointer
		 * because with this we'll get the correct error code
		 * FILE_LOCK_CONFLICT in the error case
		 */
		status = brl_lock(pvfs->brl_context,
				  f->brl_handle,
				  locks[pending->pending_lock].pid,
				  locks[pending->pending_lock].offset,
				  locks[pending->pending_lock].count,
				  rw, pending);
	}
	if (NT_STATUS_IS_OK(status)) {
		f->lock_count++;
		timed_out = false;
	}

	/* if we have failed and timed out, or succeeded, then we
	   don't need the pending lock any more */
	if (NT_STATUS_IS_OK(status) || timed_out) {
		NTSTATUS status2;
		status2 = brl_remove_pending(pvfs->brl_context, 
					     f->brl_handle, pending);
		if (!NT_STATUS_IS_OK(status2)) {
			DEBUG(0,("pvfs_lock: failed to remove pending lock - %s\n", nt_errstr(status2)));
		}
		talloc_free(pending->wait_handle);
	}

	if (!NT_STATUS_IS_OK(status)) {
		if (timed_out) {
			/* no more chances */
			pvfs_lock_async_failed(pvfs, req, f, locks, pending->pending_lock, status);
			talloc_free(pending);
		} else {
			/* we can try again */
			DLIST_ADD(f->pending_list, pending);
		}
		return;
	}

	/* if we haven't timed out yet, then we can do more pending locks */
	if (rw == READ_LOCK) {
		rw = PENDING_READ_LOCK;
	} else {
		rw = PENDING_WRITE_LOCK;
	}

	/* we've now got the pending lock. try and get the rest, which might
	   lead to more pending locks */
	for (i=pending->pending_lock+1;i<lck->lockx.in.lock_cnt;i++) {		
		if (pending) {
			pending->pending_lock = i;
		}

		status = brl_lock(pvfs->brl_context,
				  f->brl_handle,
				  locks[i].pid,
				  locks[i].offset,
				  locks[i].count,
				  rw, pending);
		if (!NT_STATUS_IS_OK(status)) {
			if (pending) {
				/* a timed lock failed - setup a wait message to handle
				   the pending lock notification or a timeout */
				pending->wait_handle = pvfs_wait_message(pvfs, req, MSG_BRL_RETRY, 
									 pending->end_time,
									 pvfs_pending_lock_continue,
									 pending);
				if (pending->wait_handle == NULL) {
					pvfs_lock_async_failed(pvfs, req, f, locks, i, NT_STATUS_NO_MEMORY);
					talloc_free(pending);
				} else {
					talloc_steal(pending, pending->wait_handle);
					DLIST_ADD(f->pending_list, pending);
				}
				return;
			}
			pvfs_lock_async_failed(pvfs, req, f, locks, i, status);
			talloc_free(pending);
			return;
		}

		f->lock_count++;
	}

	/* we've managed to get all the locks. Tell the client */
	req->async_states->status = NT_STATUS_OK;
	req->async_states->send_fn(req);
	talloc_free(pending);
}


/*
  called when we close a file that might have locks
*/
void pvfs_lock_close(struct pvfs_state *pvfs, struct pvfs_file *f)
{
	struct pvfs_pending_lock *p, *next;

	if (f->lock_count || f->pending_list) {
		DEBUG(5,("pvfs_lock: removing %.0f locks on close\n", 
			 (double)f->lock_count));
		brl_close(f->pvfs->brl_context, f->brl_handle);
		f->lock_count = 0;
	}

	/* reply to all the pending lock requests, telling them the 
	   lock failed */
	for (p=f->pending_list;p;p=next) {
		next = p->next;
		DLIST_REMOVE(f->pending_list, p);
		p->req->async_states->status = NT_STATUS_RANGE_NOT_LOCKED;
		p->req->async_states->send_fn(p->req);
	}
}


/*
  cancel a set of locks
*/
static NTSTATUS pvfs_lock_cancel(struct pvfs_state *pvfs, struct ntvfs_request *req, union smb_lock *lck,
				 struct pvfs_file *f)
{
	struct pvfs_pending_lock *p;

	for (p=f->pending_list;p;p=p->next) {
		/* check if the lock request matches exactly - you can only cancel with exact matches */
		if (p->lck->lockx.in.ulock_cnt == lck->lockx.in.ulock_cnt &&
		    p->lck->lockx.in.lock_cnt  == lck->lockx.in.lock_cnt &&
		    p->lck->lockx.in.file.ntvfs== lck->lockx.in.file.ntvfs &&
		    p->lck->lockx.in.mode      == (lck->lockx.in.mode & ~LOCKING_ANDX_CANCEL_LOCK)) {
			int i;

			for (i=0;i<lck->lockx.in.ulock_cnt + lck->lockx.in.lock_cnt;i++) {
				if (p->lck->lockx.in.locks[i].pid != lck->lockx.in.locks[i].pid ||
				    p->lck->lockx.in.locks[i].offset != lck->lockx.in.locks[i].offset ||
				    p->lck->lockx.in.locks[i].count != lck->lockx.in.locks[i].count) {
					break;
				}
			}
			if (i < lck->lockx.in.ulock_cnt) continue;

			/* an exact match! we can cancel it, which is equivalent
			   to triggering the timeout early */
			pvfs_pending_lock_continue(p, PVFS_WAIT_TIMEOUT);
			return NT_STATUS_OK;
		}
	}

	return NT_STATUS_DOS(ERRDOS, ERRcancelviolation);
}


/*
  lock or unlock a byte range
*/
NTSTATUS pvfs_lock(struct ntvfs_module_context *ntvfs,
		   struct ntvfs_request *req, union smb_lock *lck)
{
	struct pvfs_state *pvfs = talloc_get_type(ntvfs->private_data,
				  struct pvfs_state);
	struct pvfs_file *f;
	struct smb_lock_entry *locks;
	int i;
	enum brl_type rw;
	struct pvfs_pending_lock *pending = NULL;
	NTSTATUS status;

	if (lck->generic.level != RAW_LOCK_GENERIC) {
		return ntvfs_map_lock(ntvfs, req, lck);
	}

	if (lck->lockx.in.mode & LOCKING_ANDX_OPLOCK_RELEASE) {
		return pvfs_oplock_release(ntvfs, req, lck);
	}

	f = pvfs_find_fd(pvfs, req, lck->lockx.in.file.ntvfs);
	if (!f) {
		return NT_STATUS_INVALID_HANDLE;
	}

	if (f->handle->fd == -1) {
		return NT_STATUS_FILE_IS_A_DIRECTORY;
	}

	status = pvfs_break_level2_oplocks(f);
	NT_STATUS_NOT_OK_RETURN(status);

	if (lck->lockx.in.timeout != 0 && 
	    (req->async_states->state & NTVFS_ASYNC_STATE_MAY_ASYNC)) {
		pending = talloc(f, struct pvfs_pending_lock);
		if (pending == NULL) {
			return NT_STATUS_NO_MEMORY;
		}

		pending->pvfs = pvfs;
		pending->lck = lck;
		pending->f = f;
		pending->req = req;

		pending->end_time = 
			timeval_current_ofs(lck->lockx.in.timeout/1000,
					    1000*(lck->lockx.in.timeout%1000));
	}

	if (lck->lockx.in.mode & LOCKING_ANDX_SHARED_LOCK) {
		rw = pending? PENDING_READ_LOCK : READ_LOCK;
	} else {
		rw = pending? PENDING_WRITE_LOCK : WRITE_LOCK;
	}

	if (lck->lockx.in.mode & LOCKING_ANDX_CANCEL_LOCK) {
		talloc_free(pending);
		return pvfs_lock_cancel(pvfs, req, lck, f);
	}

	if (lck->lockx.in.mode & LOCKING_ANDX_CHANGE_LOCKTYPE) {
		/* this seems to not be supported by any windows server,
		   or used by any clients */
		talloc_free(pending);
		return NT_STATUS_DOS(ERRDOS, ERRnoatomiclocks);
	}

	/* the unlocks happen first */
	locks = lck->lockx.in.locks;

	for (i=0;i<lck->lockx.in.ulock_cnt;i++) {
		status = brl_unlock(pvfs->brl_context,
				    f->brl_handle,
				    locks[i].pid,
				    locks[i].offset,
				    locks[i].count);
		if (!NT_STATUS_IS_OK(status)) {
			talloc_free(pending);
			return status;
		}
		f->lock_count--;
	}

	locks += i;

	for (i=0;i<lck->lockx.in.lock_cnt;i++) {
		if (pending) {
			pending->pending_lock = i;
		}

		status = brl_lock(pvfs->brl_context,
				  f->brl_handle,
				  locks[i].pid,
				  locks[i].offset,
				  locks[i].count,
				  rw, pending);
		if (!NT_STATUS_IS_OK(status)) {
			if (pending) {
				/* a timed lock failed - setup a wait message to handle
				   the pending lock notification or a timeout */
				pending->wait_handle = pvfs_wait_message(pvfs, req, MSG_BRL_RETRY, 
									 pending->end_time,
									 pvfs_pending_lock_continue,
									 pending);
				if (pending->wait_handle == NULL) {
					talloc_free(pending);
					return NT_STATUS_NO_MEMORY;
				}
				talloc_steal(pending, pending->wait_handle);
				DLIST_ADD(f->pending_list, pending);
				return NT_STATUS_OK;
			}

			/* undo the locks we just did */
			for (i--;i>=0;i--) {
				brl_unlock(pvfs->brl_context,
					   f->brl_handle,
					   locks[i].pid,
					   locks[i].offset,
					   locks[i].count);
				f->lock_count--;
			}
			talloc_free(pending);
			return status;
		}
		f->lock_count++;
	}

	talloc_free(pending);
	return NT_STATUS_OK;
}
