/* $Id: e2_fs_FAM_inotify.c 469 2007-07-06 22:58:30Z tpgww $

Copyright (C) 2005-2007 tooar <tooar@gmx.net>

This file is part of emelFM2.
emelFM2 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, or (at your option)
any later version.

emelFM2 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 emelFM2; see the file GPL. If not, contact the Free Software
Foundation, 51 Franklin Street, Fifth Floor, Boston, MA 02110-1301 USA
*/

/**
@file src/filesystem/e2_fs_FAM_inotify.c
@brief functions related to file-alteration monitoring using inotify on linux
*/

#include "emelfm2.h"
#ifdef E2_FAM_INOTIFY

#include <string.h>
#include <sys/ioctl.h>
#include "e2_fs_FAM_inotify.h"

//#define EXTRA_MESSAGES

//timer interval between inotify queue polls
#define POLL_MILLISECONDS 200

/* Size (bytes) of kernel-space reports-quueue which triggers
transcription to user-space. A queue can hold 16384 reports,
each at least 16,and possibly up to (16+MAX_NAME), bytes.
So, to avoid queue overflow, we conservatively need to transcribe
before the size reaches 256k.However, we probably worry
about memory paging before the queue gets to that size.
For now, use 128k */
#define INQUEUE_SIZE 131072

/*Size (bytes) of local buffer for transcribing inotify reports
When monitoring a dir, each reported inotify_event struct
ends with a gchar stub, for the item name (without its path)
So each such report is of indeterminate length.
If there is a name, it will have a trailing \0 and then be
padded out to a multiple of 16 bytes (informative but irrelevant..).
NOTE that each relevant bit-flag for a reported item is sent
as a separate report, so copying a 1000 items, say, would
trigger 4000 reports !! but that is ok as the buffer is re-cycled */
#define READ_BUFFER_SIZE 8192

/*Max. no of sequential loops to check for anything pending
in the inotify report queue */
#define MAX_CHECKS 5

/* When looping to check for queued pending reports, we stop
iterating if the incremental size of the queue is less than
GROWTH_FACTOR * 2^n, where n is the 0-based loop counter.
This is essentially the approach taken by beagle */
#define GROWTH_FACTOR 32

//delay between sequential inotify checks
#define PAUSE_MICROSECONDS 2000

/*typedef struct _E2_FAMReport
{
	gboolean *p1altered;
	gboolean *p2altered;
	gboolean *cfgaltered;
} E2_FAMReport; */

//since we don't detect what types of reports are provided, need
//a statbuf for deciding whether a config file change is of interest
/*static gchar *localconfig;
static struct stat cfgstatbuf; */

static gint inotify_device_fd;

//value to be stored in array to signal an unused column
#define WD_UNUSED -1
//this is where we log the wd of any useful reports received from inotify
enum { REPORT_WD, REPORT_COUNT, ACCESS_COUNT, REPORT_ROWS };
/*store for logging reports and associated stuff for up to 3 fd's
(1 or 2 pane-dirs and config file)
Rows are used as per the enum above*/
static gint reports [3][REPORT_ROWS] = {{WD_UNUSED}, {WD_UNUSED}, {WD_UNUSED}};
//static GHashTable *report_hash;

//this is where we log monitored wd's
//using the wd for the key and path for the value
static GHashTable *wd_hash;
//we get inotify reports via this
static GIOChannel *inotify_ioc;
//id of timer for polling the inotify queue
//static guint inotify_id;
//FIXME to reduce polling, find a way for inotify to trigger
//a queue-read to clear the inotify queue before it's full

//counter used to reduce frequency of access-only reports
static gint access_counter;

/**
@brief get any inotify reports, and record the essential data for later processing
This is a timer callback fn, as well as being called manually
It  must not do any processing per se - that takes too long and risks loss of reports
@param user_data UNUSED pointer to data specified when the source was created
@return TRUE always, so timer is never cancelled
*/
static gboolean _e2_fs_FAM_inotify_read (gpointer user_data)
{
	static gpointer buffer = NULL;

	if (buffer == NULL)
	{
		buffer = g_try_malloc (READ_BUFFER_SIZE);	//never cleared
		CHECKALLOCATEDWARN (buffer, );
		if (buffer == NULL)
		{
			printd (WARN, "_e2_fs_inotify_read_events: unable to create read buffer");
			return TRUE;
		}
	}

	//check a few times for pending queued inotify-reports
	gint count;
	guint pending, prev_pending = 0;

	for (count=0; count < MAX_CHECKS; count++)
	{
		if (ioctl (inotify_device_fd, FIONREAD, &pending) == -1)
		{
			pending = 0;	//ensure we exit immediately
			break;
		}

		//stop checking now if the reports-queue is big enough
		if (pending > INQUEUE_SIZE)
			break;

		//or stop checking now if queue accretion-rate is
		//not still growing fast enough
		//(i.e. < GROWTH_FACTOR * 2^n extra queued bytes in loop n)
		//CHECKME merits of this ??
		if ((pending - prev_pending) < (guint)(GROWTH_FACTOR * (1 << count)))
			break;

		prev_pending = pending;

		//short pause before checking again
		g_usleep (PAUSE_MICROSECONDS);
	}

	if (pending == 0)
		return TRUE;	//nothing to get

	//now transcribe the reports to user-space
	void *store = buffer;
	gsize store_size = READ_BUFFER_SIZE;
	//must stop getting data if we can't fit another report header
	gsize buffer_limit = READ_BUFFER_SIZE - sizeof(struct inotify_event);
	gsize buffer_bytes;
	struct inotify_event *report = buffer;
	gint items = 0;	//for debug reporting

	//loop to read all the queued inotify reports
	do
	{
		//read into full or partial buffer
		GIOStatus result = g_io_channel_read_chars
			(inotify_ioc, store, store_size, &buffer_bytes, NULL);
		if (result == G_IO_STATUS_EOF || result == G_IO_STATUS_AGAIN)
			break;
		else if (result == G_IO_STATUS_ERROR)
		{
			printd (WARN, "_e2_fs_inotify_read_events: error reading inotify data");
			return TRUE;
		}

		gsize buffer_ptr = 0;
		//get the real no. of bytes in the buffer
		buffer_bytes += (store - buffer);
		//loop until we run out of data, or out of room
		while (buffer_ptr < buffer_bytes && buffer_ptr <= buffer_limit)
		{	//we always scan from the real start of the buffer
			report = (struct inotify_event *) (buffer+buffer_ptr);
			buffer_ptr += sizeof (struct inotify_event) + report->len;
			//buffer_ptr may now be <,=.> buffer_bytes
			if (buffer_ptr > buffer_bytes)	//if this happens, would be > READ_BUFFER_SIZE
				break;	//can't read all of this report's data yet

#ifdef EXTRA_MESSAGES
			if (report->len > 0)
			{	//there is a name provided
				if (report->mask & IN_ISDIR)
					printd (DEBUG, "inotify reported a direcory: %s", report->name);
				else
					printd (DEBUG, "inotify reported wd %d mask 0x%x for item %s",
						report->wd, report->mask, report->name);
			}
			else
			{
				if (report->mask & IN_ISDIR)
					printd (DEBUG, "inotify reported a direcory");
				else
					printd (DEBUG, "inotify reported wd %d mask 0x%x", report->wd, report->mask);
			}
#endif
			if (report->mask & IN_Q_OVERFLOW)
			{	//OOPS we lost something, don't know what ..
				//just trigger a refresh for everything
				register gint i;
				E2_BLOCK
				for (i=0; i<3; i++)
					reports[i][REPORT_COUNT]++;
				E2_UNBLOCK
				printd (DEBUG, "inotify reported queue overflow");
			}
			else if (report->mask & IN_IGNORED)
			{
#ifdef EXTRA_MESSAGES
				printd (DEBUG, "IN_IGNORE report(s) logged for wd %d", report->wd);
#endif
				if (report->wd == app.FAMreq)
				{
					//monitored config file probably was deleted
					e2_fs_FAM_cancel_monitor_config ();
					e2_fs_FAM_monitor_config ();
					//log change for new wd
					E2_BLOCK
					register gint i, r = app.FAMreq;
					for (i=0; i<3; i++)
					{
						if (reports[i][REPORT_WD] == r)
						{
							reports[i][REPORT_COUNT] = 1;
							break;
						}
					}
					E2_UNBLOCK
				}
			}
			else //if (!(report->mask & IN_IGNORED))
			{
				/*we don't care about any item name, because we only want to
				record changes to a monitored dir or the config file, not any item
				in a dir. So we just do a quick & clean report-count ref
				This also ignores any reports in the queue but relating to a
				watch that has been cancelled */
//				g_hash_table_replace (report_hash, (gpointer)report->wd, NULL);	//(gpointer)event->mask);
//				items++;
				//log the report
				E2_BLOCK
				register gint i, r = report->wd;
				for (i=0; i<3; i++)
				{
					if (reports[i][REPORT_WD] == r)
					{
						if (report->mask & IN_ACCESS)
							reports[i][ACCESS_COUNT]++;
						else
							reports[i][REPORT_COUNT]++;
						items++;
#ifdef EXTRA_MESSAGES
						printd (DEBUG, "%d reports logged for wd %d", reports[i][REPORT_COUNT] + reports[i][ACCESS_COUNT], report->wd);
#endif
						break;
					}
				}
				E2_UNBLOCK
			}
		}
		//decide how to play the next read ...
		if (buffer_ptr <= buffer_limit)
		//buffer not full, this time, so nothing more to read
			break;
		else if (buffer_ptr == buffer_bytes)
		{
			//the last-parsed report exactly-filled the buffer
			//so next read uses whole buffer
			store = buffer;
			store_size = READ_BUFFER_SIZE;
		}
		else
		{
			//(buffer_ptr > buffer_limit or buffer_ptr <> buffer_bytes)
			//  in practice this means buffer_ptr > buffer_limit, as the buffer will be full
			//move partial-report to start of buffer
			//CHECKME does it matter when report not updated, i.e.
			//buffer_ptr > buffer_limit or buffer_ptr >= buffer_bytes
			gsize remnant = READ_BUFFER_SIZE + buffer - (void *)report;
			memcpy (buffer, report, remnant);
			//begin next read after the end of moved stuff
			store = buffer + remnant;
			store_size = READ_BUFFER_SIZE - remnant;
		}
	} while (buffer_bytes > 0);

//	if (items > 0)
//		printd (DEBUG, "inotify reported %d events from %d scans", items, count+1);

	return TRUE;
}
/**
@brief helper fn for finding the hash-table entry for a monitored wd
@param key a hash key, pointerised wd that is being monitored
@param value UNUSED value associated with @a key
@param thiswd pointerised wd to be found
@return TRUE if @a key matches @a thiswd
*/
gboolean _e2_fs_FAM_inotify_find_wd (gpointer key, gpointer value,
	gpointer thiswd)
{
	return (key == thiswd);
}
/**
@brief process and clear all hashed inotify reports
This is a helper fn for hash-walking
@param key a hash key, a pointerised wd reported by inotify
@param value UNUSED the value associated with @a key
@param results data passed to g_hash_table_remove().

@return TRUE so that the key/value will be removed from the hash
*/
/*static gboolean _e2_fs_FAM_inotify_poll (gpointer key,
	gpointer value, E2_FAMReport *results)
{
	gint wd = GPOINTER_TO_INT (key);
	if (wd == app.pane1.FAMreq)
		*results->p1altered = TRUE;
	else if (wd == app.pane2.FAMreq)
		*results->p2altered = TRUE;
	else if (wd == app.FAMreq)
		*results->cfgaltered = TRUE;

	return TRUE;
} */
/**
@brief register @a path with inotify
This assumes any item will not be registered more than once
This func can be called from within a threaded refresh
@param path utf-8 string with path of item to be monitored
@return the watch descriptor established by inotfy, <0 for an error
*/
static gint _e2_fs_FAM_inotify_monitor_item (gchar *path, guint mask)
{
	gchar *local = F_FILENAME_TO_LOCALE (path);
	gint wd = inotify_add_watch (inotify_device_fd, local, (__u32) mask);
	F_FREE (local);

	if (wd >= 0)
	{
		//setup the reports log
		gint i;
		E2_BLOCK
		for (i=0; i<3; i++)
		{
			if (reports[i][REPORT_WD] == WD_UNUSED)
			{
				reports[i][REPORT_WD] = wd;
				reports[i][REPORT_COUNT] = 0;
				reports[i][ACCESS_COUNT] = 0;
				break;
			}
		}
		//remember it, for cleanup if inotify is killed
		g_hash_table_replace (wd_hash, GINT_TO_POINTER (wd), g_strdup (path));
		E2_UNBLOCK
		printd (DEBUG, "Added inotify watch %d for %s", wd, path);
	}
	else
		printd (WARN, "Failed to add inotify watch for %s\n%s", path, strerror (errno));

	return wd;
}
/**
@brief de-register @a path with inotify
This func can be called from within a threaded refresh
@param path utf-8 string with path of item no longer to be monitored, used in debug messages
@param wd watch descriptor number that is to be cancelled
@return TRUE if successfully cancelled
*/
static gboolean _e2_fs_FAM_inotify_demonitor_item (gchar *path, __u32 wd)
{
	gboolean retval = TRUE;
	if (inotify_rm_watch (inotify_device_fd, wd) < 0)
	{	//error may be because monitored item (e.g. config file or unmounted dir) is gone now
		printd (DEBUG, "%s error when trying to remove inotify watch for %s", strerror (errno), path);
		retval = FALSE;
	}
	//flush the reports queue
	_e2_fs_FAM_inotify_read (NULL);

	//free up the corresponding reports-log
	E2_BLOCK
	gint i, r = (gint)wd;
	for (i=0; i<3; i++)
	{
		if (reports[i][REPORT_WD] == r)
		{
			reports[i][REPORT_WD] = WD_UNUSED;
			break;
		}
	}
	//no need to remember wd, for cleanup if inotify is killed
	g_hash_table_remove (wd_hash, GINT_TO_POINTER (wd));
	E2_UNBLOCK
	printd (DEBUG, "Removed inotify watch for %s", path);
	return retval;
}
/**
@brief Initialize inotify monitoring.

@return TRUE if initialization succeeded
*/
static gboolean _e2_fs_FAM_inotify_init (void)
{
	inotify_device_fd = inotify_init ();

	if (inotify_device_fd < 0)
	{
		printd (WARN, "Could not initialize inotify");
		return FALSE;
	}

	inotify_ioc = g_io_channel_unix_new (inotify_device_fd);
	g_io_channel_set_encoding (inotify_ioc, NULL, NULL);
	g_io_channel_set_flags (inotify_ioc, G_IO_FLAG_NONBLOCK, NULL);

/*	inotify_src = g_io_create_watch (inotify_ioc,
			G_IO_IN | G_IO_HUP | G_IO_ERR);
	g_source_set_callback (inotify_src, _e2_fs_FAM_inotify_read, NULL, NULL);
	g_source_set_priority (inotify_src, 300);
	g_source_set_can_recurse (inotify_src, FALSE);
	inotify_src_id = g_source_attach (inotify_src, NULL); */
	app.timers[FAMPOLL_T] = g_timeout_add (POLL_MILLISECONDS, _e2_fs_FAM_inotify_read, NULL);

//	report_hash = g_hash_table_new (g_direct_hash, g_direct_equal);
	wd_hash = g_hash_table_new_full (g_direct_hash, g_direct_equal, NULL, g_free);

	return TRUE;
}
/**
@brief helper fn for cancelling all monitoring
@param key a hash key, a pointerised wd for the monitored item
@param value value associated with @a key, a copy of path string for the monitored item
@param data UNUSED pointer to data given to the foreach fn
@return
*/
void _e2_fs_FAM_inotify_cancel_wd (gpointer key, gpointer value,
	gpointer data)
{
	inotify_rm_watch (inotify_device_fd, (__u32) key);	//cancel without changing the hash
	//don't bother with data cleanups at session-end
}
/**
@brief cleanup after ending inotify monitoring.

@return
*/
static void _e2_fs_FAM_inotify_abandon (void)
{
	//cancel all monitoring that's in force
	g_hash_table_foreach (wd_hash, (GHFunc) _e2_fs_FAM_inotify_cancel_wd, NULL);

	GError *err = NULL;
	g_io_channel_shutdown (inotify_ioc, FALSE, &err);
//	g_source_destroy (inotify_src);
	//don't worry about hash
}

  /**********************/
 /** public functions **/
/**********************/

/**
@brief establish inotify connection

@return
*/
void e2_fs_FAM_connect (void)
{
	app.monitor_type = (_e2_fs_FAM_inotify_init ()) ?
		E2_MONITOR_FAM : E2_MONITOR_DEFAULT ;
}
/**
@brief terminate inotify connection

session-end, no data freeing

@return
*/
void e2_fs_FAM_disconnect (void)
{
	if (app.monitor_type == E2_MONITOR_FAM)
		_e2_fs_FAM_inotify_abandon ();
}
/**
@brief change monitored directory

if current-dir not also in the other pane, cancel current-dir
monitoring
if new-dir not also in other pane, start monitoring new-dir
If new-dir is not monitored (FAM error or dir is already monitored
in other pane) the FAM request for the pane is set to -1

@a olddir needs trailing '/', for valid comparing with rt->path etc

@param olddir utf-8 string with absolute path of dir to stop monitoring
@param rt data struct for the pane to which the cd applies, including the new dir in rt->path

@return
*/
void e2_fs_FAM_change (gchar *olddir, E2_PaneRuntime *rt)
{
	if (app.monitor_type == E2_MONITOR_DEFAULT)
		return;
	//cancel monitoring of current dir if that was happening
	//at session-start, both panes have rt->FAMreq == -1
	if (rt->FAMreq != -1)
	{
		//use the request for this pane
		_e2_fs_FAM_inotify_demonitor_item (olddir, (__u32) rt->FAMreq);
		//default flag = no-monitoring this pane
		rt->FAMreq = -1;
	}
	E2_PaneRuntime *ort = (rt == curr_pane) ? other_pane : curr_pane;
#ifdef E2_VFSTMP
	//FIXME path for non-mounted dirs
#else
	if (g_str_equal (olddir, ort->path))
#endif
	{	//panes are showing same dir now
		//connect to the other pane if need be
		if (ort->FAMreq == -1)
		{
#ifdef E2_VFSTMP
	//FIXME path for non-mounted dirs
#else
			ort->FAMreq = _e2_fs_FAM_inotify_monitor_item (ort->path, INDIR_FLAGS);
#endif
		}
	}
	//now hoookup to the new dir, if it's not going already
#ifdef E2_VFSTMP
	//FIXME path for non-mounted dirs
#else
	if (!g_str_equal (ort->path, rt->path))	//NB panes may be same at session start
#endif
	{	//new dir not already monitored
#ifdef E2_VFSTMP
	//FIXME path for non-mounted dirs
#else
		rt->FAMreq = _e2_fs_FAM_inotify_monitor_item (rt->path, INDIR_FLAGS);
#endif
	}
	else
		rt->FAMreq = -1;
}
/**
@brief get rid of one pending change-report for directory @a path
Used as part of filelist refreshing
There is no actual change if the wd for the relevant pane is -1
which means there is no monitoring anyway
@param path utf-8 string with path of dir to be processed
@return
*/
void e2_fs_FAM_clean_reports (gchar *path)
{
	if (app.monitor_type == E2_MONITOR_DEFAULT)
		return;
	//suspend periodic polling of pending-reports quene
	if (app.timers[FAMPOLL_T] > 0)
	{
		g_source_remove (app.timers[FAMPOLL_T]);
		app.timers[FAMPOLL_T] = 0;
	}
	//and poll now
	_e2_fs_FAM_inotify_read (NULL);

	gint wd =
#ifdef E2_VFSTMP
	//FIXME path for non-mounted dirs
#else
		(g_str_equal (curr_pane->path, path)) ?
#endif
		curr_pane->FAMreq : other_pane->FAMreq;
	if (wd != -1)
	{	//dir was being monitored before
		//decrement its reports log
		gint i;
		E2_BLOCK
		for (i=0; i<3; i++)
		{
			if (reports[i][REPORT_WD] == wd)
			{ //FIXME ACCESS reports need to be considered
//				if (reports[i][REPORT_COUNT] > 0)
//					reports[i][REPORT_COUNT]--;
				if (reports[i][ACCESS_COUNT] > 0)
					reports[i][ACCESS_COUNT]--;
				break;
			}
		}
		E2_UNBLOCK
	}
	//resume polling the queue
	app.timers[FAMPOLL_T] = g_timeout_add (POLL_MILLISECONDS,
		 (GSourceFunc) _e2_fs_FAM_inotify_read, NULL);
}
/**
@brief begin inotify monitoring of directory @a path
Used as part of filelist refreshing
There is no actual change if the fd for the relevant pane is -1
which means there is no monitoring anyway
@param path utf-8 string with path of dir to be resumed
@return
*/
/*void e2_fs_FAM_monitor_dir (gchar *path)
{
	E2_PaneRuntime *rt =
#ifdef E2_VFSTMP
	//FIXME path for non-mounted dirs
#else
		(g_str_equal (curr_pane->path, path)) ?
#endif
		curr_pane : other_pane;
	if (rt->FAMreq != -1)
	{
		//dir was being monitored before
		rt->FAMreq = _e2_fs_FAM_inotify_monitor_item (path, INDIR_FLAGS);
//		printd (DEBUG, "(as part of a resumption)");
	}
} */
/**
@brief cancel inotify monitoring of directory @a path
Used as part of filelist refreshing
There is no actual change if the fd for the relevant pane is -1
which means there is no monitoring anyway
Any prior reports for the dir are cleared
@param path utf-8 string with path of dir to be suspended
@return
*/
/*void e2_fs_FAM_cancel_monitor_dir (gchar *path)
{
	E2_PaneRuntime *rt =
#ifdef E2_VFSTMP
	//FIXME path for non-mounted dirs
#else
		(g_str_equal (curr_pane->path, path)) ?
#endif
		curr_pane : other_pane;
	if (rt->FAMreq != -1)
	{
		_e2_fs_FAM_inotify_demonitor_item (path, (__u32) rt->FAMreq);
//		printd (DEBUG, "(as part of a suspension)");
	}
} */
/**
@brief setup monitoring of config file
Set up with data = 3, to distinguish this from panes 1 & 2 monitoring

@return TRUE if the connection was successfully established
*/
gboolean e2_fs_FAM_monitor_config (void)
{
	gchar *config_file = g_build_filename (e2_cl_options.config_dir, default_config_file, NULL);
	app.FAMreq = _e2_fs_FAM_inotify_monitor_item (config_file, INCFG_FLAGS);
	//setup to check for irrelevant reports on this file
//	if (localconfig == NULL)
//		localconfig = D_FILENAME_TO_LOCALE (config_file);
//	stat (localconfig, &cfgstatbuf);	//ok to traverse a link FIXME vfs

//	gboolean result = (app.FAMreq < 0);

//	if (result)
//		printd (DEBUG, "FAM init for %s succeeded", config_file);
//	else
//		printd (WARN, "FAM init for %s failed", config_file);
	g_free (config_file);

//	return result;
	return (app.FAMreq < 0);
}
/**
@brief cancel monitoring of config file

@return TRUE if the connection was successfully removed
*/
gboolean e2_fs_FAM_cancel_monitor_config (void)
{
	gchar *config_file = g_build_filename (e2_cl_options.config_dir, default_config_file, NULL);
	gboolean result = _e2_fs_FAM_inotify_demonitor_item (config_file, (__u32) app.FAMreq);
	if (result)
	{
		printd (DEBUG, "FAM cancel for %s succeeded", config_file);
	}
	else
		printd (WARN, "FAM cancel for %s failed", config_file);
	g_free (config_file);
	return result;
}
/**
@brief poll monitor to check if any monitored thing has changed

This just updates flags, to signal that refresh is needed
For a pane, update is needed if any directory content has
changed, or if the parent dir is not accessible/readable or gone
A missing config file is not reported (as we can't reload it anyway)

@param p1altered pointer to location to store T/F for whether pane 1 needs refresh
@param p2altered pointer to location to store T/F for whether pane 2 needs refresh
@param cfgaltered pointer to location to store T/F for whether config needs refresh

@return
*/
void e2_fs_FAM_poll (gboolean *p1altered, gboolean *p2altered, gboolean *cfgaltered)
{
//	static E2_FAMReport *results = NULL;

	if (++access_counter == 5)	//report on accesses every 5th check
		access_counter = 0;

	//suspend periodic polling of pending reports quene
	if (app.timers[FAMPOLL_T] > 0)
	{
		g_source_remove (app.timers[FAMPOLL_T]);
		app.timers[FAMPOLL_T] = 0;
	}
	//and poll now, under our control
	_e2_fs_FAM_inotify_read (NULL);

	*p1altered = *p2altered = *cfgaltered = FALSE;
//	if (g_hash_table_size (report_hash) == 0)
	E2_BLOCK
	gboolean none = (
		 reports[0][REPORT_COUNT] == 0
	  && reports[1][REPORT_COUNT] == 0
	  && reports[2][REPORT_COUNT] == 0
	  && reports[0][ACCESS_COUNT] == 0
	  && reports[1][ACCESS_COUNT] == 0
	  && reports[2][ACCESS_COUNT] == 0
	);
	E2_UNBLOCK

	if (none)
	{
		//resume polling the queue
		app.timers[FAMPOLL_T] = g_timeout_add (POLL_MILLISECONDS,
			 (GSourceFunc) _e2_fs_FAM_inotify_read, NULL);
		return;
	}

/*	if (results == NULL)
	{
		results = ALLOCATE (E2_FAMReport);
		CHECKALLOCATEDWARN (results, return;)
	}
	results->p1altered = p1altered;
	results->p2altered = p2altered;
	results->cfgaltered = cfgaltered;

	g_hash_table_foreach_remove (report_hash,
		(GHRFunc) _e2_fs_FAM_inotify_poll, results);
*/
	//interrogate reports logs
	gint i;
	E2_BLOCK
	for (i=0; i<3; i++)
	{
		if (
			(reports[i][REPORT_COUNT] > 0 || (access_counter == 0 && reports[i][ACCESS_COUNT] > 0))
			&& reports[i][REPORT_WD] != WD_UNUSED)
		{
#ifdef EXTRA_MESSAGES
			printd (DEBUG, "%d total reports for wd %d", reports[i][REPORT_COUNT]+reports[i][ACCESS_COUNT], reports[i][REPORT_WD]);
#endif
			gint wd = reports[i][REPORT_WD];
			if (wd == app.pane1.FAMreq)
			{
				*p1altered = TRUE;
				//mirrored panes get the same status
				if (app.pane2.FAMreq == -1)
					*p2altered = TRUE;
			}
			else if (wd == app.pane2.FAMreq)
			{
				*p2altered = TRUE;
				//mirrored panes get the same status
				if (app.pane1.FAMreq == -1)
					*p1altered = TRUE;
			}
			else if (wd == app.FAMreq)
			{
				*cfgaltered = TRUE;
			}
			reports[i][REPORT_COUNT] = 0;
			if (access_counter == 0)
				reports[i][ACCESS_COUNT] = 0;
		}
	}
	E2_UNBLOCK

	app.timers[FAMPOLL_T] = g_timeout_add (POLL_MILLISECONDS,
		 (GSourceFunc) _e2_fs_FAM_inotify_read, NULL);

	if (*p1altered)
		printd (DEBUG, "pane-1 change reported");
	if (*p2altered)
		printd (DEBUG, "pane-2 change reported");
//	if (*cfgaltered)
//		printd (DEBUG, "config change reported");
	return;
}

#endif //def E2_FAM_INOTIFY
