/*
 * FAM file notification support.
 *
 * Copyright (c) James Peach 2005
 *
 * 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., 59 Temple Place, Suite 330, Boston, MA  02111-1307 USA
 */

#include "includes.h"

#ifdef HAVE_FAM_CHANGE_NOTIFY

#include <fam.h>

#if !defined(HAVE_FAM_H_FAMCODES_TYPEDEF)
/* Gamin provides this typedef which means we can't use 'enum FAMCodes' as per
 * every other FAM implementation. Phooey.
 */
typedef enum FAMCodes FAMCodes;
#endif

/* NOTE: There are multiple versions of FAM floating around the net, each with
 * slight differences from the original SGI FAM implementation. In this file,
 * we rely only on the SGI features and do not assume any extensions. For
 * example, we do not look at FAMErrno, because it is not set by the original
 * implementation.
 *
 * Random FAM links:
 *	http://oss.sgi.com/projects/fam/
 *	http://savannah.nongnu.org/projects/fam/
 *	http://sourceforge.net/projects/bsdfam/
 */

struct fam_req_info
{
    FAMRequest	    req;
    int		    generation;
    FAMCodes	    code;
    enum
    {
	/* We are waiting for an event. */
	FAM_REQ_MONITORING,
	/* An event has been receive, but we haven't been able to send it back
	 * to the client yet. It is stashed in the code member.
	 */
	FAM_REQ_FIRED
    } state;
};

/* Don't initialise this until the first register request. We want a single
 * FAM connection for each worker smbd. If we allow the master (parent) smbd to
 * open a FAM connection, multiple processes talking on the same socket will
 * undoubtedly create havoc.
 */
static FAMConnection	global_fc;
static int		global_fc_generation;

#define FAM_TRACE	8
#define FAM_TRACE_LOW	10

#define FAM_EVENT_DRAIN		    ((uint32_t)(-1))

static void * fam_register_notify(connection_struct * conn,
		    char *		path,
		    uint32		flags);

static BOOL fam_check_notify(connection_struct * conn,
		 uint16_t		vuid,
		 char *			path,
		 uint32_t		flags,
		 void *			data,
		 time_t			when);

static void fam_remove_notify(void * data);

static struct cnotify_fns global_fam_notify =
{
    fam_register_notify,
    fam_check_notify,
    fam_remove_notify,
    -1,
    -1
};

/* Turn a FAM event code into a string. Don't rely on specific code values,
 * because that might not work across all flavours of FAM.
 */
static const char *
fam_event_str(FAMCodes code)
{
    static const struct { FAMCodes code; const char * name; } evstr[] =
    {
	{ FAMChanged,		"FAMChanged"},
	{ FAMDeleted,		"FAMDeleted"},
	{ FAMStartExecuting,	"FAMStartExecuting"},
	{ FAMStopExecuting,	"FAMStopExecuting"},
	{ FAMCreated,		"FAMCreated"},
	{ FAMMoved,		"FAMMoved"},
	{ FAMAcknowledge,	"FAMAcknowledge"},
	{ FAMExists,		"FAMExists"},
	{ FAMEndExist,		"FAMEndExist"}
    };

    int i;

    for (i = 0; i < ARRAY_SIZE(evstr); ++i) {
	if (code == evstr[i].code)
	    return(evstr[i].name);
    }

    return("<unknown>");
}

static BOOL
fam_check_reconnect(void)
{
    if (FAMCONNECTION_GETFD(&global_fc) < 0) {
	fstring name;

	global_fc_generation++;
	snprintf(name, sizeof(name), "smbd (%lu)", (unsigned long)sys_getpid());

	if (FAMOpen2(&global_fc, name) < 0) {
	    DEBUG(0, ("failed to connect to FAM service\n"));
	    return(False);
	}
    }

    global_fam_notify.notification_fd = FAMCONNECTION_GETFD(&global_fc);
    return(True);
}

static BOOL
fam_monitor_path(connection_struct *	conn,
	         struct fam_req_info *	info,
		 const char *		path,
		 uint32			flags)
{
    SMB_STRUCT_STAT st;
    pstring	    fullpath;

    DEBUG(FAM_TRACE, ("requesting FAM notifications for '%s'\n", path));

    /* FAM needs an absolute pathname. */

    /* It would be better to use reduce_name() here, but reduce_name does not
     * actually return the reduced result. How utterly un-useful.
     */
    pstrcpy(fullpath, path);
    if (!canonicalize_path(conn, fullpath)) {
	DEBUG(0, ("failed to canonicalize path '%s'\n", path));
	return(False);
    }

    if (*fullpath != '/') {
	DEBUG(0, ("canonicalized path '%s' into `%s`\n", path, fullpath));
	DEBUGADD(0, ("but expected an absolute path\n"));
	return(False);
    }

    if (SMB_VFS_STAT(conn, path, &st) < 0) {
	DEBUG(0, ("stat of '%s' failed: %s\n", path, strerror(errno)));
	return(False);
    }
    /* Start monitoring this file or directory. We hand the state structure to
     * both the caller and the FAM library so we can match up the caller's
     * status requests with FAM notifications.
     */
    if (S_ISDIR(st.st_mode)) {
	FAMMonitorDirectory(&global_fc, fullpath, &(info->req), info);
    } else {
	FAMMonitorFile(&global_fc, fullpath, &(info->req), info);
    }

    /* Grr. On IRIX, neither of the monitor functions return a status. */

    /* We will stay in initialising state until we see the FAMendExist message
     * for this file.
     */
    info->state = FAM_REQ_MONITORING;
    info->generation = global_fc_generation;
    return(True);
}

static BOOL
fam_handle_event(const FAMCodes code, uint32 flags)
{
#define F_CHANGE_MASK (FILE_NOTIFY_CHANGE_FILE | \
			FILE_NOTIFY_CHANGE_ATTRIBUTES | \
			FILE_NOTIFY_CHANGE_SIZE | \
			FILE_NOTIFY_CHANGE_LAST_WRITE | \
			FILE_NOTIFY_CHANGE_LAST_ACCESS | \
			FILE_NOTIFY_CHANGE_CREATION | \
			FILE_NOTIFY_CHANGE_EA | \
			FILE_NOTIFY_CHANGE_SECURITY)

#define F_DELETE_MASK (FILE_NOTIFY_CHANGE_FILE_NAME | \
			FILE_NOTIFY_CHANGE_DIR_NAME)

#define F_CREATE_MASK (FILE_NOTIFY_CHANGE_FILE_NAME | \
			FILE_NOTIFY_CHANGE_DIR_NAME)

    switch (code) {
	case FAMChanged:
	    if (flags & F_CHANGE_MASK)
		return(True);
	    break;
	case FAMDeleted:
	    if (flags & F_DELETE_MASK)
		return(True);
	    break;
	case FAMCreated:
	    if (flags & F_CREATE_MASK)
		return(True);
	    break;
	default:
	    /* Ignore anything else. */
	    break;
    }

    return(False);

#undef F_CHANGE_MASK
#undef F_DELETE_MASK
#undef F_CREATE_MASK
}

static BOOL
fam_pump_events(struct fam_req_info * info, uint32_t flags)
{
    FAMEvent ev;

    for (;;) {

	/* If we are draining the event queue we must keep going until we find
	 * the correct FAMAcknowledge event or the connection drops. Otherwise
	 * we should stop when there are no more events pending.
	 */
	if (flags != FAM_EVENT_DRAIN && !FAMPending(&global_fc)) {
	    break;
	}

	if (FAMNextEvent(&global_fc, &ev) < 0) {
	    DEBUG(0, ("failed to fetch pending FAM event\n"));
	    DEBUGADD(0, ("resetting FAM connection\n"));
	    FAMClose(&global_fc);
	    FAMCONNECTION_GETFD(&global_fc) = -1;
	    return(False);
	}

	DEBUG(FAM_TRACE_LOW, ("FAM event %s on '%s' for request %d\n",
		    fam_event_str(ev.code), ev.filename, ev.fr.reqnum));

	switch (ev.code) {
	    case FAMAcknowledge:
		/* FAM generates an ACK event when we cancel a monitor. We need
		 * this to know when it is safe to free out request state
		 * structure.
		 */
		if (info->generation == global_fc_generation &&
		    info->req.reqnum == ev.fr.reqnum &&
		    flags == FAM_EVENT_DRAIN) {
		    return(True);
		}

	    case FAMEndExist:
	    case FAMExists:
		/* Ignore these. FAM sends these enumeration events when we
		 * start monitoring. If we are monitoring a directory, we will
		 * get a FAMExists event for each directory entry.
		 */

		/* TODO: we might be able to use these to implement recursive
		 * monitoring of entire subtrees.
		 */
	    case FAMMoved:
		/* These events never happen. A move or rename shows up as a
		 * create/delete pair.
		 */
	    case FAMStartExecuting:
	    case FAMStopExecuting:
		/* We might get these, but we just don't care. */
		break;

	    case FAMChanged:
	    case FAMDeleted:
	    case FAMCreated:
		if (info->generation != global_fc_generation) {
		    /* Ignore this; the req number can't be matched. */
		    break;
		}

		if (info->req.reqnum == ev.fr.reqnum) {
		    /* This is the event the caller was interested in. */
		    DEBUG(FAM_TRACE, ("handling FAM %s event on '%s'\n",
				fam_event_str(ev.code), ev.filename));
		    /* Ignore events if we are draining this request. */
		    if (flags != FAM_EVENT_DRAIN) {
			return(fam_handle_event(ev.code, flags));
		    }
		    break;
		} else {
		    /* Caller doesn't want this event. Stash the result so we
		     * can come back to it. Unfortunately, FAM doesn't
		     * guarantee to give us back evinfo.
		     */
		    struct fam_req_info * evinfo =
			(struct fam_req_info *)ev.userdata;

		    if (evinfo) {
			DEBUG(FAM_TRACE, ("storing FAM %s event for winter\n",
				    fam_event_str(ev.code)));
			evinfo->state = FAM_REQ_FIRED;
			evinfo->code = ev.code;
		    } else {
			DEBUG(2, ("received FAM %s notification for %s, "
				  "but userdata was unexpectedly NULL\n",
				  fam_event_str(ev.code), ev.filename));
		    }
		    break;
		}

	    default:
		DEBUG(0, ("ignoring unknown FAM event code %d for `%s`\n",
			    ev.code, ev.filename));
	}
    }

    /* No more notifications pending. */
    return(False);
}

static BOOL
fam_test_connection(void)
{
    FAMConnection fc;

    /* On IRIX FAMOpen2 leaks 960 bytes in 48 blocks. It's a deliberate leak
     * in the library and there's nothing we can do about it here.
     */
    if (FAMOpen2(&fc, "smbd probe") < 0)
	return(False);

    FAMClose(&fc);
    return(True);
}

/* ------------------------------------------------------------------------- */

static void *
fam_register_notify(connection_struct * conn,
		    char *		path,
		    uint32		flags)
{
    struct fam_req_info * info;

    if (!fam_check_reconnect()) {
	return(False);
    }

    if ((info = SMB_MALLOC_P(struct fam_req_info)) == NULL) {
	DEBUG(0, ("malloc of %u bytes failed\n", (unsigned int)sizeof(struct fam_req_info)));
	return(NULL);
    }

    if (fam_monitor_path(conn, info, path, flags)) {
	return(info);
    } else {
	SAFE_FREE(info);
	return(NULL);
    }
}

static BOOL
fam_check_notify(connection_struct *	conn,
		 uint16_t		vuid,
		 char *			path,
		 uint32_t		flags,
		 void *			data,
		 time_t			when)
{
    struct fam_req_info * info;

    info = (struct fam_req_info *)data;
    SMB_ASSERT(info != NULL);

    DEBUG(10, ("checking FAM events for `%s`\n", path));

    if (info->state == FAM_REQ_FIRED) {
	DEBUG(FAM_TRACE, ("handling previously fired FAM %s event\n",
		    fam_event_str(info->code)));
	info->state = FAM_REQ_MONITORING;
	return(fam_handle_event(info->code, flags));
    }

    if (!fam_check_reconnect()) {
	return(False);
    }

    if (info->generation != global_fc_generation) {
	DEBUG(FAM_TRACE, ("reapplying stale FAM monitor to %s\n", path));
	fam_monitor_path(conn, info, path, flags);
	return(False);
    }

    return(fam_pump_events(info, flags));
}

static void
fam_remove_notify(void * data)
{
    struct fam_req_info * info;

    if ((info = (struct fam_req_info *)data) == NULL)
	return;

    /* No need to reconnect. If the FAM connection is gone, there's no need to
     * cancel and we can safely let FAMCancelMonitor fail. If it we
     * reconnected, then the generation check will stop us cancelling the wrong
     * request.
     */

    if (info->generation == global_fc_generation) {
	DEBUG(FAM_TRACE, ("removing FAM notification for request %d\n",
		    info->req.reqnum));
	FAMCancelMonitor(&global_fc, &(info->req));

	/* Soak up all events until the FAMAcknowledge. We can't free
	 * our request state until we are sure there are no more events in
	 * flight.
	 */
	fam_pump_events(info, FAM_EVENT_DRAIN);
    }

    SAFE_FREE(info);
}

struct cnotify_fns * fam_notify_init(void)
{
    FAMCONNECTION_GETFD(&global_fc) = -1;

    if (!fam_test_connection()) {
	DEBUG(0, ("FAM file change notifications not available\n"));
	return(NULL);
    }

    DEBUG(FAM_TRACE, ("enabling FAM change notifications\n"));
    return &global_fam_notify;
}

#endif /* HAVE_FAM_CHANGE_NOTIFY */