/* $Id: e2_fs_FAM_inotify.c 2416 2011-12-20 20:10:15Z tpgww $

Copyright (C) 2005-2011 tooar <tooar@emelfm2.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, see http://www.gnu.org/licenses.
*/

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

A connection to the kernel's inotify apparatus is established at session-start,
and closed at session-end. Some associated setup and cleanup happens, respectivley.

Whenever a filelist-directory is opened in the application, a "watch" is set
for that directory (except if it is already watched in the other pane). Similarly,
if the appropriate option is in effect, a watch is set on the application
config-data file.

Feedback from the kernel about any watch is detected by periodic polling (every
QPOLL_MSEC) of the kernel's event-reports-queue length. We don't use select() or
poll() because we don't want to block (in a separate thread), and we do want to
be able to abort/suspend without delay. After QUIET_SECONDS without any report,
the poll-timer is turned off, to be restarted by any subsequent gtk event on the
main window (TODO or any new inotify report).

All such feedback is logged as quickly as possible, and later processed according
to the context. A watch-specific timer is used to defer such processing.

When a report is receieved, and the corresponding timer is inactive, then we start
the timer with a short interval (IN_SHORTMSEC) to allow accumulation of any
further reports (which often come in blocks).

When the short timer ends, if refresh is enabled, initiate refresh for the subject,
clean up inotify data, and restart the timer with a longer interval (IN_LONGMSEC)
during which further reports will be logged but not processed. This is to prevent
rapid un-necessary repeating of refreshes.

When the long timer ends, if refresh is enabled, and if report(s) have been logged,
process them, clean up inotify data, and restart the long timer. If no report
has been logged, do not restart, pending further report.

When a timer ends and refresh is disabled, do not immediately clean up reports/
inotify data.

When refresh is enabled or requested, if short timer is running, do nothing. If
long timer is running, stop it, and treat as for end of long timer, as above.

Count(s) of logged event-reports of are accessed only during timer callbacks,
which should not overlap. So no protection against contemporary access is provided.
*/

#include "emelfm2.h"
#ifdef E2_FAM_INOTIFY

#include <sys/inotify.h>
#include <sys/ioctl.h>
#include <fcntl.h>
#include <string.h>
#include "e2_filelist.h"

//#define EXTRA_MESSAGES

//glibc 2.4 omits this ?
#ifndef IN_MASK_ADD
# define IN_MASK_ADD 0x20000000
#endif
/*
flags used when normal refreshing is in effect.
when monitoring a dir we ignore these inotify events:
IN_CLOSE_WRITE this should be covered by IN_MODIFY
IN_CLOSE_NOWRITE
IN_OPEN we're monitoring dirs and config file and don't care about opening
and we always get these, so don't need to ask
IN_UNMOUNT
IN_Q_OVERFLOW
IN_IGNORED (item no longer watched, likely due to monitor-cancellation, or
item deletion (or replacement))
*/
#define INDIR_FLAGS \
IN_ACCESS | IN_MODIFY | IN_ATTRIB | IN_MOVED_FROM | IN_MOVED_TO | \
IN_CREATE | IN_DELETE | IN_DELETE_SELF | IN_MOVE_SELF
/* flags used when limited refreshing is in effect (i.e. no IN_ACCESS) */
#define INDIR_SHORT_FLAGS \
IN_MODIFY | IN_ATTRIB | IN_MOVED_FROM | IN_MOVED_TO | \
IN_CREATE | IN_DELETE | IN_DELETE_SELF | IN_MOVE_SELF
/* flags used to supplement INDIR_SHORT_FLAGS to get back to full operation */
#define INDIR_XTRA_FLAGS IN_ACCESS

/*for the config file we're interested only in events that would trigger a
reload of that file, ie modification or creation. Not deletion, as a replacement
may not yet be available.
If something renames the file (whether or not it then creates a replacement version),
the renamed file will continue to be monotored unless IN_MOVED* are checked.
However, in normal course of events, the file is deleted and then replaced, the
deletion causes a IN_IGNORED report to be generated, and monitoring ceases to be
effective on the monitored inode - so no IN_CREATE report is generated, normally.
*/
#define INCFG_FLAGS \
IN_MODIFY | IN_MOVE_SELF

//timer interval between inotify queue polls
#define QPOLL_MSEC 350
//short timer interval (between any 'initial' event-report and refresh-request)
//a bit longer than QPOLL_MSEC
#define IN_SHORTMSEC 400
//long timer interval (between any refresh-request and another one, if needed)
#define IN_LONGMSEC 3000

/* Size (bytes) of kernel-space reports-queue 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 1000 items, say, would trigger 4000 reports !! but that's ok as the
buffer is re-cycled */
#define READ_BUFFER_SIZE 8192
/* Sometimes (e.g. for /tmp) there's a flood of reports and we need to curtail
the report-reading process so that a refresh can be done. This sets the approx.
max no. of received reports before triggering a refresh. Hitting this limit will
usually result in more queued reports to process after the dir in question ceases
to be monitored, but such reports are ignored.
*/
#define IN_BATCHLIMIT 1000

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

//the connection to inotify
static gint inotify_fd = 0;

//for deferring processing after any report is received from inotify
static guint target0_timerid = 0;
static guint target1_timerid = 0;
static guint target2_timerid = 0;
//for logging whether respective timers are in long or short phase
static volatile gint target0_timer_interval = 0;
static volatile gint target1_timer_interval = 0;
static volatile gint target2_timer_interval = 0;
//for distinguishing between types of IN_IGNORED reports
static gboolean suspend_IGNORE = FALSE;

extern volatile gint p1dirty, p2dirty, cfgdirty;
extern time_t last_work_time;

//array-columns enumerator
enum { REPORT_WD, REPORT_COUNT, REPORT_ROWS };
//FAMreq value for blocking use of a watch value, must be < 0
#define WD_BUSY -2
//array REPORT_WD value for an error
#define WD_ERR -1
//array REPORT_WD value for an unused row, intentionally the same as value from
//a failed inotify function-call
#define WD_UNUSED -1
/*store for logging reports and associated stuff for up to 3 watches
(1 or 2 pane-dirs and config file NOTE pane1 not necessarily target/row 0, pane2 not nec. target/row 1)
Rows are used as per the enum above*/
static gint watch_reports[3][REPORT_ROWS] = {{WD_UNUSED}, {WD_UNUSED}, {WD_UNUSED}};
//cached strings for each row of watch_reports array. Contains the path of
//respective watched item, used for checking path is still relevant when timer
//finishes. For speed, mManipulated with glibc funcs, not glib equivalents.
static gchar *watch_paths [3];
//local store for event-reports transferred from inotify queue
static gpointer inotify_buffer = NULL;
//path of monitored config-file, cached to avoid repeated create+free in timer callback
static gchar *config_file;

/* FIXME find a way for inotify to always trigger a queue-read to clear the
   inotify queue before it's full */

/**
@brief timer callback after any 'short' or 'long' interval between refresh requests
If event-report(s) have been logged, and if refresh is enabled, process the
event(s), clean up inotify data, and start or continue the long timer. If no
report has been logged, abort timer pending further reporting.
@param user_data pointerised enumerator 0..2, for row in watch_reports array

@return TRUE if this is a long timer callback, and refresh has been initiated while here
*/
static gboolean _e2_fs_FAM_inotify_timeout (gpointer user_data)
{
#ifdef EXTRA_MESSAGES
	const gchar *period = "un-assigned";
#endif
	gboolean more = FALSE; //default
	guint target_id = GPOINTER_TO_UINT (user_data);
	printd (DEBUG, "In _e2_fs_FAM_inotify_timeout, for target %u", target_id);
	gchar *target_path = (watch_reports[target_id][REPORT_WD] != WD_UNUSED) ?
		watch_paths[target_id] : NULL;
	if (G_LIKELY (target_path != NULL))
	{
		if (watch_reports[target_id][REPORT_COUNT] > 0) //report(s) available
		{
#ifdef EXTRA_MESSAGES
			printd (DEBUG, " target %d (watch %d) watch-reports count = %d",
				target_id, watch_reports[target_id][REPORT_WD], watch_reports[target_id][REPORT_COUNT]);
#endif
			if (strcmp (target_path, app.pane1.path) == 0)
			{
#ifdef E2_REFRESH_DEBUG
				printd (DEBUG,"dirty set TRUE for pane 1");
#endif
				g_atomic_int_set (&p1dirty, 1);	//set shared flag to trigger refresh
				more = TRUE;
			}
			//both panes may be the same
			if (strcmp (target_path, app.pane2.path) == 0)
			{
#ifdef E2_REFRESH_DEBUG
				printd (DEBUG,"dirty set TRUE for pane 2");
#endif
				g_atomic_int_set (&p2dirty, 1);
				more = TRUE;
			}
			if (!more //if a monitored dir now has same path as config file, no config update is possible
				&& config_file != NULL //config monitoring is happening
				&& strcmp (target_path, config_file) == 0)
			{
#ifdef E2_REFRESH_DEBUG
				printd (DEBUG,"dirty set TRUE for config");
#endif
				g_atomic_int_set (&cfgdirty, 1);	//log this for the config-monitor process to handle
				more = TRUE;
			}

			if (more)
			{
				last_work_time = time (NULL);	//refresh suspension threshold, in case downstream processing is deferred
				e2_filelist_check_dirty (GINT_TO_POINTER (1)); //start a refresh ASAP
			}
#ifdef EXTRA_MESSAGES
			printd (DEBUG, "clear reports-count for %s", watch_paths[target_id]);
#endif
//			printall ("after requesting refresh for %s (watch %d), reports-count (%d) is zeroed",
//				watch_paths [target_id],
//				watch_reports[target_id][REPORT_WD], watch_reports[target_id][REPORT_COUNT]);
			E2_BLOCK
			watch_reports[target_id][REPORT_COUNT] = 0;
			E2_UNBLOCK
		}
	}
	else //target_path == NULL, i.e. cached data is for some item not monitored now
		if (watch_reports[target_id][REPORT_WD] != WD_UNUSED)
	{
		printd (DEBUG, "OOPS no cached path for target");
		//clear data
		E2_BLOCK
		watch_reports[target_id][REPORT_WD] = WD_UNUSED;
		watch_reports[target_id][REPORT_COUNT] = 0;
		E2_UNBLOCK
	}

	switch (target_id)
	{
		case 0:
			if (more)
			{
				if (target0_timer_interval < IN_LONGMSEC)
				{
					target0_timer_interval = IN_LONGMSEC;
					//start long timer, and later return FALSE to kill this one
					target0_timerid = g_timeout_add (IN_LONGMSEC,
						_e2_fs_FAM_inotify_timeout, GUINT_TO_POINTER (0));
					more = FALSE;
				}
#ifdef EXTRA_MESSAGES
				period = "long";
#endif
			}
			else
			{	//no more reports to process, this timer will be killed
//				printd (DEBUG, "clear timer parameters");
				target0_timer_interval = 0;
				target0_timerid = 0;
#ifdef EXTRA_MESSAGES
				period = "cancelled";
#endif
			}
			break;
		case 1:
			if (more)
			{
				if (target1_timer_interval < IN_LONGMSEC)
				{
					target1_timer_interval = IN_LONGMSEC;
					//start long timer, and later return FALSE to kill this one
					target1_timerid = g_timeout_add (IN_LONGMSEC,
						_e2_fs_FAM_inotify_timeout, GUINT_TO_POINTER (1));
					more = FALSE;
				}
#ifdef EXTRA_MESSAGES
				period = "long";
#endif
			}
			else
			{
				target1_timer_interval = 0;
				target1_timerid = 0;
#ifdef EXTRA_MESSAGES
				period = "cancelled";
#endif
			}
			break;
		case 2:
			if (more)
			{
				if (target2_timer_interval < IN_LONGMSEC)
				{
					target2_timer_interval = IN_LONGMSEC;
					//start long timer, and later return FALSE to kill this one
					target2_timerid = g_timeout_add (IN_LONGMSEC,
						_e2_fs_FAM_inotify_timeout, GUINT_TO_POINTER (2));
					more = FALSE;
				}
#ifdef EXTRA_MESSAGES
				period = "long";
#endif
			}
			else
			{
				target2_timer_interval = 0;
				target2_timerid = 0;
#ifdef EXTRA_MESSAGES
				period = "cancelled";
#endif
			}
			break;
		default:
			more = FALSE;
			break;
	}
#ifdef EXTRA_MESSAGES
	printd (DEBUG, " inotify %s timer will %s", period, (more)? "continue":"stop now");
#endif
	return more; //continue long timer if a refresh was requested
}
/**
@brief cleanup cached data for @a wd
If @a path is non-NULL, this stops after cearing the first match of both @a path
and @a wd in the watch_reports table. If @a path is NULL, data for all instances
of @a wd are cleared.
@param path UTF-8 path string, for comparison with watch_paths[]
@param wd watch-descriptor number
@return
*/
static void _e2_fs_FAM_clean_local (const gchar *path, gint wd)
{
	E2_BLOCK
	gint i;
	for (i=0; i<3; i++)
	{
		if (watch_reports[i][REPORT_WD] == wd)
		{
			if (path == NULL || strcmp (path, watch_paths[i]) == 0)
			{
				watch_reports[i][REPORT_WD] = WD_UNUSED;
				free (watch_paths[i]);
				watch_paths[i] = NULL;
				if (path != NULL)
					break;
			}
		}
	}
	E2_UNBLOCK
}
/**
@brief get any inotify reports, and record the essential data for later processing.
This is the callback func for timer app.timers[DIRTYCHECK_T].
It  must not do any processing per se - that takes too long and risks loss of reports
@param userdata UNUSED pointer specified when the timer was set up
@return TRUE, or FALSE to suspend timer after extended period of inactivity
*/
static gboolean _e2_fs_FAM_inotify_read (gpointer userdata)
{
	//counter used to reduce frequency of access-reports
	static guint access_counter = 0;

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

	for (count = 0; count < MAX_CHECKS; count++)
	{
		//check current size of the event-queue
		if (ioctl (inotify_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)
	{	//nothing to get
		if (time (NULL) >= last_work_time + QUIET_SECONDS)
		{
			printd (DEBUG, "Suspend operation of inotify Q-poll timer");
			app.timers[DIRTYCHECK_T] = 0;
			//arrange to restart upon activity
			g_signal_connect (G_OBJECT (app.main_window), "event",
				G_CALLBACK (e2_filelist_repoll), GUINT_TO_POINTER (DIRTYCHECK_T));
			return FALSE;
		}
		return TRUE;
	}

	//transcribe change-reports to user-space
	void *store = inotify_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 = inotify_buffer;
	gboolean batchflag = FALSE; //for aborting after lots of reports
	gint items = 0;	//for debug reporting
	//loop to read all the queued inotify reports
	do
	{
		//read into full or partial buffer
		//do NOT use e2_fs_read(), that will block until reports are sufficient
		//to fill the buffer
		buffer_bytes = read (inotify_fd, store, store_size);
		if (buffer_bytes == 0)
			break;
		else if (buffer_bytes == -1)
		{
			printd (WARN, "_e2_fs_inotify_read: error reading inotify data");
			return TRUE;
		}

		gsize buffer_ptr = 0;
		//get the real no. of bytes in the buffer
		buffer_bytes += (store - inotify_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 *) (inotify_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 directory: %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 directory");
				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++)
					watch_reports[i][REPORT_COUNT]++;
				E2_UNBLOCK
				printd (DEBUG, "inotify reported queue overflow");
			}
			else if (report->mask & IN_IGNORED)
			{
#ifdef EXTRA_MESSAGES
				printd (DEBUG, "IN_IGNORED report(s) received for wd %d", report->wd);
#endif
				gint cfgwatch = g_atomic_int_get (&app.FAMreq);
				if (report->wd == cfgwatch)
				{	//config file deleted, or its monitoring cancelled
					if (suspend_IGNORE)
					{ //this follows a manual suspension of monitoring
#ifdef EXTRA_MESSAGES
						printd (DEBUG, " and ignored");
#endif
						suspend_IGNORE = FALSE;
					}
					else
					{ //this is a normal timer callback, assumed a deletion
						_e2_fs_FAM_clean_local (config_file, cfgwatch); //clean up cached data
						if (e2_fs_FAM_monitor_config ()) //try to setup monitoring of new file
						{
							//log a change for new wd
							cfgwatch = g_atomic_int_get (&app.FAMreq); //watch-value different now
							E2_BLOCK
							register gint i, r = cfgwatch;
							for (i=0; i<3; i++)
							{
								if (watch_reports[i][REPORT_WD] == r)
								{
#ifdef EXTRA_MESSAGES
									printd (DEBUG, " and report logged (for config file)");
#endif
									watch_reports[i][REPORT_COUNT] = 1;
									break;
								}
							}
							E2_UNBLOCK
						}
					}
				}
			}
			else // !IN_Q_OVERFLOW and !IN_IGNORED)
			{
#ifdef EXTRA_MESSAGES
				printd (DEBUG, " watch %d mask = 0x%x", report->wd, report->mask);
#endif
				/*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++)
				{
//#ifdef EXTRA_MESSAGES
//					printd (DEBUG, " watch %d reports count before this one = %d",
//						watch_reports[i][REPORT_WD], watch_reports[i][REPORT_COUNT]);
//#endif
					if (watch_reports[i][REPORT_WD] == r)
					{
						if (report->mask & IN_ACCESS)
						{
							if ((report->mask & !IN_ACCESS) > 0
									|| access_counter == 0)
								watch_reports[i][REPORT_COUNT]++;
						}
						else
							watch_reports[i][REPORT_COUNT]++;
						if (watch_reports[i][REPORT_COUNT] > IN_BATCHLIMIT)
							batchflag = TRUE;
						items++;
#ifdef EXTRA_MESSAGES
						printd (DEBUG, "%d reports logged for wd %d",
							watch_reports[i][REPORT_COUNT], report->wd);
#endif
						break;
					}
				}
				E2_UNBLOCK
			}
			if (batchflag)
				break;
		}
		//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 = inotify_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 + inotify_buffer - (void *)report;
			memmove (inotify_buffer, report, remnant);
			//begin next read after the end of moved stuff
			store = inotify_buffer + remnant;
			store_size = READ_BUFFER_SIZE - remnant;
		}
	} while (buffer_bytes > 0 && !batchflag);

//	if (items > 0)
//		printd (DEBUG, "inotify reported %d events from %d scans", items, count+1);
	if (++access_counter == 5)	//report on accesses every 5th check
		access_counter = 0;

	/* when report(s) received, and the corresponding timer is inactive, start
	   the timer with a short interval */
	if (watch_reports[0][REPORT_COUNT] > 0 &&
		g_atomic_int_compare_and_exchange (&target0_timer_interval, 0, IN_SHORTMSEC)) //flag that short delay is happening
	{
		//timer-callback data = pointerised enumerator of array row
		target0_timerid = g_timeout_add (IN_SHORTMSEC,
			_e2_fs_FAM_inotify_timeout, GUINT_TO_POINTER (0));
	}
	if (watch_reports[1][REPORT_COUNT] > 0 &&
		g_atomic_int_compare_and_exchange (&target1_timer_interval, 0, IN_SHORTMSEC))
	{
		target1_timerid = g_timeout_add (IN_SHORTMSEC,
			_e2_fs_FAM_inotify_timeout, GUINT_TO_POINTER (1));
	}
	if (watch_reports[2][REPORT_COUNT] > 0 &&
		g_atomic_int_compare_and_exchange (&target2_timer_interval, 0, IN_SHORTMSEC))
	{
		target2_timerid = g_timeout_add (IN_SHORTMSEC,
			_e2_fs_FAM_inotify_timeout, GUINT_TO_POINTER (2));
	}

	return TRUE;
}
/**
@brief register @a path with inotify
This assumes @a path will not be registered more than once at any time.
The first-available row in watch_reports array is used, so watch_reports[0] is
not necessarily for pane1, etc.
This func can be called from within a threaded cd
@param path UTF-8 string with path of item to be monitored
@param mask flag(s) indicating the types of reports wanted
@return the watch descriptor established by inotify, <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_fd, local, (u_int32_t) mask);
	F_FREE (local, path);

	if (wd >= 0)
	{
		//setup the reports log
		gint i;
		E2_BLOCK
		for (i=0; i<3; i++)
		{
			if (watch_reports[i][REPORT_WD] == WD_UNUSED)
			{
				watch_reports[i][REPORT_WD] = wd;
				watch_reports[i][REPORT_COUNT] = 0;
				watch_paths[i] = strdup (path);
				printd (DEBUG, "Added inotify watch %d for %s", wd, path);
//				printall ("Added inotify watch %d for %s", wd, path);
				break;
			}
		}
		E2_UNBLOCK
#ifdef EXTRA_MESSAGES
		if (i == 3)
			printd (WARN, "Failed to add inotify watch %d for %s\n%s", wd, path, "No space in reports-table");
		printd (DEBUG, "Tabled watches now are %d, %d, %d",
			watch_reports[0][REPORT_WD], watch_reports[1][REPORT_WD], watch_reports[2][REPORT_WD]);
#endif
//		printall ("Tabled watches now are %d, %d, %d",
//			watch_reports[0][REPORT_WD], watch_reports[1][REPORT_WD], watch_reports[2][REPORT_WD]);
	}
	else
		printd (WARN, "Failed to add inotify watch for %s\n%s", path, strerror (errno));

	return wd;
}
/**
@brief change the scope of inotify-monitoring for @a path
Called during threaded refresh.
@param path UTF-8 string with path of item to be changed, to be compared with rt->path's
@param mask bitflags for changed monitoring scope
@return
*/
static void _e2_fs_FAM_inotify_remonitor_item (gchar *path, guint mask)
{
	gint *req, *oreq;
	gint old, new, other;

#ifdef E2_VFSTMP
	//FIXME path for non-mounted dirs
#endif
again:
	if (strcmp (path, curr_pane->path) == 0)
	{
		req = &curr_pane->FAMreq;
		oreq = &other_pane->FAMreq;
	}
	else if (strcmp (path, other_pane->path) == 0)
	{
		req = &other_pane->FAMreq;
		oreq = &curr_pane->FAMreq;
	}
	else
		return;

	if ((old = g_atomic_int_get (req)) == WD_BUSY ||
		(other = g_atomic_int_get (oreq)) == WD_BUSY)
	{
		usleep (2000); //threaded, blocking is ok
		goto again;
	}
	if (G_LIKELY (old != WD_UNUSED))
		g_atomic_int_set (req, WD_BUSY); //block other threads from messing with it
	else
		return;

	gchar *local = F_FILENAME_TO_LOCALE (path);
	new = inotify_add_watch (inotify_fd, local, (u_int32_t) mask);
	F_FREE (local, path);

	if (new == old)
		g_atomic_int_set (req, old); //unblock it
	else
	{
		//conform the reports array
		gint i;
		E2_BLOCK
		for (i=0; i<3; i++)
		{
			if (watch_reports[i][REPORT_WD] == old)
			{
				watch_reports[i][REPORT_WD] = new;
				if (new < 0)
				{
					watch_reports[i][REPORT_COUNT] = 0;
					free (watch_paths[i]);
					watch_paths[i] = NULL;
				}
			}
		}
		E2_UNBLOCK
#ifdef DEBUG_MESSAGES
		printd (DEBUG, "inotify watch number changed from %d to %d when reports scope changed",
			old, new);
# ifdef EXTRA_MESSAGES
		printd (DEBUG, "Tabled watches now are %d, %d, %d",
			watch_reports[0][REPORT_WD], watch_reports[1][REPORT_WD], watch_reports[2][REPORT_WD]);
# endif
#endif
//		printall ("After changing monotor scope for %s\n  tabled watches now are %d, %d, %d",
//		    path,
//			watch_reports[0][REPORT_WD], watch_reports[1][REPORT_WD], watch_reports[2][REPORT_WD]);

		//alter watch data CHECKME also if new < 0?
		g_atomic_int_set (req, new);
		if (other == old)
			g_atomic_int_set (oreq, new);
//		printall ("  and FAMreq's are %d, %d", curr_pane->FAMreq, other_pane->FAMreq);
	}
}
/**
@brief de-register @a path from inotify
This func can be called from within a threaded refresh.
Do NOT call this when the same watch applies to more than one directory.
@param path UTF-8 string with path of item no longer to be monitored
@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, gint wd)
{
	gboolean retval;
	if (inotify_rm_watch (inotify_fd, wd) >= 0)
	{
		printd (DEBUG, "Removed inotify %d watch for %s", wd, path);
		retval = TRUE;
	}
	else
	{	//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 %d for %s", strerror (errno), wd, path);
		retval = FALSE;
	}

	//no need to flush the reports queue here
	_e2_fs_FAM_clean_local (path, wd);
#ifdef EXTRA_MESSAGES
	printd (DEBUG, "Tabled watches now are %d, %d, %d",
		watch_reports[0][REPORT_WD], watch_reports[1][REPORT_WD], watch_reports[2][REPORT_WD]);
#endif
//	printall ("After cancelling watch for %s\n  tabled watches now are %d, %d, %d",
//	    path,
//		watch_reports[0][REPORT_WD], watch_reports[1][REPORT_WD], watch_reports[2][REPORT_WD]);

	return retval;
}
/**
@brief Initialize inotify monitoring.

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

	if (inotify_fd < 0)
	{
		printd (WARN, "Could not initialize inotify");
		return FALSE;
	}
	//prevent blocking during Q reads
	gint flags = fcntl (inotify_fd, F_GETFL);
#ifdef EXTRA_MESSAGES
	printd (DEBUG, "inotify fd flags are 0x%x", flags);
#endif
	if (flags != EBADF)
		fcntl (inotify_fd, F_SETFL, flags | O_NONBLOCK);

#ifdef E2_REFRESH_DEBUG
	printd (DEBUG, "start Q-poll timer @ init");
#endif
	/* Use one of the 'main' timers so its suspension can be managed like others
	   This one would be used for 'default' polling if kernel-fam were not available
	   Some cleanup code assumes the same timer for both modes - do not change it
	   BUT shutting this down will also block config updating */
	app.timers[DIRTYCHECK_T] = g_timeout_add (QPOLL_MSEC, _e2_fs_FAM_inotify_read, NULL);

	return TRUE;
}
/**
@brief cleanup as part of ending inotify monitoring.

@return
*/
static void _e2_fs_FAM_inotify_abandon (void)
{
	if (app.timers[DIRTYCHECK_T] != 0)
	{
		g_source_remove (app.timers[DIRTYCHECK_T]);
		app.timers[DIRTYCHECK_T] = 0;
	}
	if (target0_timerid != 0)
	{
		g_source_remove (target0_timerid);
//		target0_timerid = 0;
	}
	if (target1_timerid != 0)
	{
		g_source_remove (target1_timerid);
//		target1_timerid = 0;
	}
	if (target2_timerid != 0)
	{
		g_source_remove (target2_timerid);
//		target2_timerid = 0;
	}

	//cancel all monitoring that's in force
	guint i;
	for (i=0; i<3; i++)
	{
		if (watch_reports[i][REPORT_WD] > 0)
		{
			inotify_rm_watch (inotify_fd, watch_reports[i][REPORT_WD]);
//			watch_reports[i][REPORT_WD] = WD_UNUSED;
		}
		if (watch_paths[i] != NULL)
		{
			free (watch_paths[i]);
//			watch_paths[i] = NULL;
		}
	}
	//any queued pending reports are cleaned by kernel
	//do not close inotify_fd, it's not a valid file-descriptor

	g_free (inotify_buffer);	//cleanup session reports-buffer
	inotify_buffer = NULL;
}

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

/**
@brief establish inotify connection

@return
*/
void e2_fs_FAM_connect (void)
{
	if (G_LIKELY (inotify_buffer == NULL))
	{
		inotify_buffer = g_try_malloc (READ_BUFFER_SIZE);	//cleared during disconnection
#if (CHECKALLOCATEDWARN)
		CHECKALLOCATEDWARN (inotify_buffer, app.monitor_type = E2_MONITOR_DEFAULT; return;)
#else
		if (inotify_buffer == NULL)
		{
			printd (WARN, "e2_fs_FAM_connect: unable to create inotify read-buffer");
			app.monitor_type = E2_MONITOR_DEFAULT;
			return;
		}
#endif
	}

	if (_e2_fs_FAM_inotify_init ())
		app.monitor_type = E2_MONITOR_FAM;
	else
	{
		app.monitor_type = E2_MONITOR_DEFAULT;
		g_free (inotify_buffer);
		inotify_buffer = NULL;
	}
}
/**
@brief terminate inotify connection

Call at session-end

@return
*/
void e2_fs_FAM_disconnect (void)
{
	if (app.monitor_type == E2_MONITOR_FAM)
		_e2_fs_FAM_inotify_abandon ();
	if (config_file != NULL)
	{
		g_free (config_file);
		config_file = NULL;
	}
}
/**
@brief put inotify monitoring to 'sleep' i.e. suspend polling activity
@return
*/
void e2_fs_FAM_suspend (void)
{
	if (app.timers[DIRTYCHECK_T] != 0)
	{
		g_source_remove (app.timers[DIRTYCHECK_T]);
		app.timers[DIRTYCHECK_T] = 0;
	}
}
/**
@brief wake up inotify monitoring
@return
*/
void e2_fs_FAM_resume (void)
{
	if (app.timers[DIRTYCHECK_T] == 0)
		app.timers[DIRTYCHECK_T] = g_timeout_add (QPOLL_MSEC, _e2_fs_FAM_inotify_read, NULL);
}
/**
@brief change monitored directory from @a oldir to @a rt ->view.dir
Called during threaded cd.
If @a olddir not also in the other pane, cancel old-dir monitoring.
If the dir for @a rt (new-dir) is not also in other pane, start monitoring new-dir.
If new-dir is not monitored (FAM error) the FAM request for the pane is set to WD_UNUSED

@a olddir needs trailing '/', for valid comparing with rt->view.dir 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->view.dir

@return
*/
void e2_fs_FAM_change (gchar *olddir, E2_PaneRuntime *rt)
{
	if (G_LIKELY (app.monitor_type == E2_MONITOR_FAM))
	{	//inotify connection set up
		E2_PaneRuntime *ort;
		gint old, new, other;
again:
		ort = (rt == curr_pane) ? other_pane : curr_pane;
		//atomic access cuz' other threads may want to use FAMreq(s)
		if ((old = g_atomic_int_get (&rt->FAMreq)) == WD_BUSY ||
			(other = g_atomic_int_get (&ort->FAMreq)) == WD_BUSY)
		{
			usleep (2000);
			goto again;
		}

		g_atomic_int_set (&rt->FAMreq, WD_BUSY); //block other threads from messing with it
		//if both panes are now using the same watch (FAMreq), or this pane is
		//unwatched, we can't cancel the watch for olddir
		if (!(old == other || old == WD_UNUSED))
			_e2_fs_FAM_inotify_demonitor_item (olddir, old);
//#ifdef EXTRA_MESSAGES
//		else
//			printd (DEBUG, "skip demonitor, old wd = %d, other wd = %d", old, other);
//#endif
//		else
//			printall ("During CD, skip demonitor, old wd = %d, other wd = %d", old, other);

		//if both panes will be using the same watch, no need to start a new
		//watch, just replicate data
		if (strcmp (rt->view.dir, ort->view.dir) == 0 && other > 0)
		{
			new = other;
//#ifdef EXTRA_MESSAGES
//			printd (DEBUG, "Set pane FAMreq = %d from other pane", new);
//#endif
		}
		else
		{
			new = _e2_fs_FAM_inotify_monitor_item (rt->view.dir, INDIR_FLAGS);
//#ifdef EXTRA_MESSAGES
//			printd (DEBUG, "Set pane FAMreq = %d from new watch", new);
//#endif
		}
		g_atomic_int_set (&rt->FAMreq, new);
//		printall ("After inotify setup during CD, FAMreq's are %d, %d",
//			curr_pane->FAMreq, other_pane->FAMreq);
	}
}
/**
@brief finish or suspend monitoring of dir related to @a rt
@return TRUE if the connection was successfully removed
*/
gboolean e2_fs_FAM_cancel_monitor_dir (E2_PaneRuntime *rt)
{
	gboolean result;
	gint old = g_atomic_int_get (&rt->FAMreq);
	if (old > 0)
	{
		result = _e2_fs_FAM_inotify_demonitor_item (rt->path, old);
		if (result)
		{
			g_atomic_int_set (&rt->FAMreq, WD_UNUSED);
/*#ifdef DEBUG_MESSAGES
			printd (DEBUG, "FAM cancel for %s succeeded", path);
#endif */
		}
/*#ifdef DEBUG_MESSAGES
		else
			printd (WARN, "FAM cancel for %s failed", path);
#endif
*/
	}
	else
		result = FALSE;
	return result;
}
/**
@brief reduce the scope of event-monitoring for @a path
Essentially this abandons IN_ACCESS reporting, normally used for a directory.
Any prior reports logged for the dir are not affected.
@param path UTF-8 string with path of dir whose watch is to be altered
@return
*/
void e2_fs_FAM_less_monitor_dir (gchar *path)
{
#ifdef EXTRA_MESSAGES
	printd (DEBUG, "Cancel ACCESS event-reports for %s", path);
#endif
	//there's no 'remove mask' API, so we reset the watch
	_e2_fs_FAM_inotify_remonitor_item (path, INDIR_SHORT_FLAGS);
}
/**
@brief resume normal scope of event-monitoring for @a path
This is needed after e2_fs_FAM_less_monitor_dir(), normally used for a directory.
Essentially this resumes IN_ACCESS reporting.
Any prior event-reports for the dir are not affected.
@param path UTF-8 string with path of dir whose watch is to be altered
@return
*/
void e2_fs_FAM_more_monitor_dir (gchar *path)
{
#ifdef EXTRA_MESSAGES
	printd (DEBUG, "Resume ACCESS event-reports for %s", path);
#endif
	_e2_fs_FAM_inotify_remonitor_item (path, INDIR_XTRA_FLAGS | IN_MASK_ADD);
}
/**
@brief begin or resume monitoring of config file

@return TRUE if the connection was successfully established
*/
gboolean e2_fs_FAM_monitor_config (void)
{
	if (config_file != NULL)
		g_free (config_file);
	config_file = g_build_filename (e2_cl_options.config_dir, default_config_file, NULL);

	gint new = _e2_fs_FAM_inotify_monitor_item (config_file, INCFG_FLAGS);
	//atomic access cuz' other threads may want to use the variable
	g_atomic_int_set (&app.FAMreq, new);

	return (new > 0);
}
/**
@brief finish or suspend monitoring of config file

@return TRUE if the connection was successfully removed
*/
gboolean e2_fs_FAM_cancel_monitor_config (void)
{
	gboolean result;
	suspend_IGNORE = TRUE; //guide for handling IGNORED reports, cleared by the reader
//	printd (DEBUG, "goto _e2_fs_FAM_inotify_demonitor_item() for %s", config_file);
	//this spits a non-fatal error if called after the config file has been deleted,
	//including deletion as part of a replacement
	gint old = g_atomic_int_get (&app.FAMreq);
	if (old > 0)
	{
		result = _e2_fs_FAM_inotify_demonitor_item (config_file, old);
		if (result)
		{
			g_atomic_int_set (&app.FAMreq, WD_UNUSED);
//			printd (DEBUG, "FAM cancel for %s succeeded", config_file);
		}
/*#ifdef DEBUG_MESSAGES
		else
			printd (WARN, "FAM cancel for %s failed", config_file);
#endif
*/
	}
	else
		result = FALSE;
	return result;
}

#ifdef EXTRA_MESSAGES
# undef EXTRA_MESSAGES
#endif

#endif //def E2_FAM_INOTIFY
