/*
   Unix SMB/Netbios implementation.
   Version 3.0
   change notify handling - linux kernel based implementation
   Copyright (C) Andrew Tridgell 2000
   Copyright (C) Volker Lendecke 2007

   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 2 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, write to the Free Software
   Foundation, Inc., 675 Mass Ave, Cambridge, MA 02139, USA.
*/

#include "includes.h"

#if HAVE_KERNEL_CHANGE_NOTIFY

#ifndef DN_ACCESS
#define DN_ACCESS       0x00000001      /* File accessed in directory */
#define DN_MODIFY       0x00000002      /* File modified in directory */
#define DN_CREATE       0x00000004      /* File created in directory */
#define DN_DELETE       0x00000008      /* File removed from directory */
#define DN_RENAME       0x00000010      /* File renamed in directory */
#define DN_ATTRIB       0x00000020      /* File changed attribute */
#define DN_MULTISHOT    0x80000000      /* Don't remove notifier */
#endif


#ifndef RT_SIGNAL_NOTIFY
#define RT_SIGNAL_NOTIFY (SIGRTMIN+2)
#endif

#ifndef F_SETSIG
#define F_SETSIG 10
#endif

#ifndef F_NOTIFY
#define F_NOTIFY 1026
#endif

/****************************************************************************
 This is the structure to keep the information needed to
 determine if a directory has changed.
*****************************************************************************/

struct dnotify_ctx {
	struct dnotify_ctx *prev, *next;

	int fd;
	files_struct *fsp;
};

static struct dnotify_ctx *dnotify_list;
static int dnotify_signal_pipe[2];

/****************************************************************************
 The signal handler for change notify.
 The Linux kernel has a bug in that we should be able to block any
 further delivery of RT signals until the kernel_check_notify() function
 unblocks them, but it seems that any signal mask we're setting here is
 being overwritten on exit from this handler. I should create a standalone
 test case for the kernel hackers. JRA.
*****************************************************************************/

static void dnotify_signal_handler(int sig, siginfo_t *info, void *unused)
{
	int saved_errno;

	/*
	 * According to http://www.opengroup.org/onlinepubs/009695399/ write
	 * to a pipe either writes all or nothing, so we can safely write a
	 * full sizeof(int) and not risk the pipe to become out of sync with
	 * the receiving end.
	 *
	 * We don't care about the result of the write() call. If the pipe is
	 * full, then this signal is lost, we can't do anything about it.
	 */

	saved_errno = errno;
	write(dnotify_signal_pipe[1], (const void *)&info->si_fd, sizeof(int));
	errno = saved_errno;

	sys_select_signal(RT_SIGNAL_NOTIFY);
}

/****************************************************************************
 The upper level handler informed when the pipe is ready for reading
*****************************************************************************/

static void dnotify_pipe_handler(struct event_context *event_ctx,
				 struct fd_event *event,
				 uint16 flags,
				 void *private_data)
{
	int res, fd;
	struct dnotify_ctx *ctx;

	res = read(dnotify_signal_pipe[0], (void *)&fd, sizeof(int));

	if (res == -1) {
		DEBUG(0, ("Read from the dnotify pipe failed: %s\n",
			  strerror(errno)));
		TALLOC_FREE(event); /* Don't try again */
		return;
	}

	if (res != sizeof(int)) {
		smb_panic("read from dnotify pipe gave wrong number of "
			  "bytes\n");
	}

	for (ctx = dnotify_list; ctx; ctx = ctx->next) {
		if (ctx->fd == fd) {
			notify_fsp(ctx->fsp, 0, NULL);
		}
	}
}

/****************************************************************************
 Register a change notify request.
*****************************************************************************/

static int kernel_register_notify(connection_struct *conn, char *path,
				  uint32 flags)
{
	int fd;
	unsigned long kernel_flags;
	
	fd = sys_open(path,O_RDONLY, 0);

	if (fd == -1) {
		DEBUG(3,("Failed to open directory %s for change notify\n",
			 path));
		return -1;
	}

	if (sys_fcntl_long(fd, F_SETSIG, RT_SIGNAL_NOTIFY) == -1) {
		DEBUG(3,("Failed to set signal handler for change notify\n"));
		close(fd);
		return -1;
	}

	kernel_flags = DN_CREATE|DN_DELETE|DN_RENAME; /* creation/deletion
						       * changes
						       * everything! */
	if (flags & FILE_NOTIFY_CHANGE_FILE_NAME)   kernel_flags |= DN_MODIFY;
	if (flags & FILE_NOTIFY_CHANGE_DIR_NAME)    kernel_flags
							    |= DN_RENAME
							    |DN_DELETE;
	if (flags & FILE_NOTIFY_CHANGE_ATTRIBUTES)  kernel_flags |= DN_ATTRIB;
	if (flags & FILE_NOTIFY_CHANGE_SIZE)        kernel_flags |= DN_MODIFY;
	if (flags & FILE_NOTIFY_CHANGE_LAST_WRITE)  kernel_flags |= DN_MODIFY;
	if (flags & FILE_NOTIFY_CHANGE_LAST_ACCESS) kernel_flags |= DN_ACCESS;
	if (flags & FILE_NOTIFY_CHANGE_CREATION)    kernel_flags |= DN_CREATE;
	if (flags & FILE_NOTIFY_CHANGE_SECURITY)    kernel_flags |= DN_ATTRIB;
	if (flags & FILE_NOTIFY_CHANGE_EA)          kernel_flags |= DN_ATTRIB;
	if (flags & FILE_NOTIFY_CHANGE_FILE_NAME)   kernel_flags
							    |= DN_RENAME
							    |DN_DELETE;

	if (sys_fcntl_long(fd, F_NOTIFY, kernel_flags) == -1) {
		DEBUG(3,("Failed to set async flag for change notify\n"));
		close(fd);
		return -1;
	}

	DEBUG(3,("kernel change notify on %s (ntflags=0x%x flags=0x%x) "
		 "fd=%d\n", path, (int)flags, (int)kernel_flags, fd));

	return fd;
}

/****************************************************************************
 See if the kernel supports change notify.
****************************************************************************/

static BOOL kernel_notify_available(void) 
{
	int fd, ret;
	fd = open("/tmp", O_RDONLY);
	if (fd == -1)
		return False; /* uggh! */
	ret = sys_fcntl_long(fd, F_NOTIFY, 0);
	close(fd);
	return ret == 0;
}

static int dnotify_ctx_destructor(struct dnotify_ctx *ctx)
{
	close(ctx->fd);
	DLIST_REMOVE(dnotify_list, ctx);
	return 0;
}

static void *kernel_notify_add(TALLOC_CTX *mem_ctx,
			       struct event_context *event_ctx,
			       files_struct *fsp,
			       uint32 *filter)
{
	struct dnotify_ctx *ctx;

	if (!(ctx = TALLOC_P(mem_ctx, struct dnotify_ctx))) {
		DEBUG(0, ("talloc failed\n"));
		return NULL;
	}

	ctx->fsp = fsp;
	ctx->fd = kernel_register_notify(fsp->conn, fsp->fsp_name, *filter);

	if (ctx->fd == -1) {
		TALLOC_FREE(ctx);
		return NULL;
	}

	DLIST_ADD(dnotify_list, ctx);
	talloc_set_destructor(ctx, dnotify_ctx_destructor);

	return ctx;
}

/****************************************************************************
 Setup kernel based change notify.
****************************************************************************/

struct cnotify_fns *kernel_notify_init(struct event_context *event_ctx)
{
	static struct cnotify_fns cnotify;
        struct sigaction act;

	if (pipe(dnotify_signal_pipe) == -1) {
		DEBUG(0, ("Failed to create signal pipe: %s\n",
			  strerror(errno)));
		return NULL;
	}

	if ((set_blocking(dnotify_signal_pipe[0], False) == -1)
	    || (set_blocking(dnotify_signal_pipe[1], False) == -1)) {
		DEBUG(0, ("Failed to set signal pipe to non-blocking: %s\n",
			  strerror(errno)));
		close(dnotify_signal_pipe[0]);
		close(dnotify_signal_pipe[1]);
		return NULL;
	}

	if (event_add_fd(event_ctx, NULL, dnotify_signal_pipe[0],
			 EVENT_FD_READ, dnotify_pipe_handler, NULL) == NULL) {
		DEBUG(0, ("Failed to set signal event handler\n"));
		close(dnotify_signal_pipe[0]);
		close(dnotify_signal_pipe[1]);
		return NULL;
	}

	ZERO_STRUCT(act);

	act.sa_sigaction = dnotify_signal_handler;
	act.sa_flags = SA_SIGINFO;
	sigemptyset( &act.sa_mask );
	if (sigaction(RT_SIGNAL_NOTIFY, &act, NULL) != 0) {
		DEBUG(0,("Failed to setup RT_SIGNAL_NOTIFY handler\n"));
		return NULL;
	}

	if (!kernel_notify_available())
		return NULL;

	cnotify.notify_add = kernel_notify_add;

	/* the signal can start off blocked due to a bug in bash */
	BlockSignals(False, RT_SIGNAL_NOTIFY);

	return &cnotify;
}

#else
 void notify_kernel_dummy(void);

 void notify_kernel_dummy(void) {}
#endif /* HAVE_KERNEL_CHANGE_NOTIFY */