# The interface to the Linux inotify file notification system.
# This code is (C) 2005 Manuel Amador <rudd-o@amautacorp.com>
# and is being made available under the terms of the GNU GPL, version 2.
#
# $Id: inotify.py 12 2005-03-12 10:20:03Z rudd-o $

"""An interface to the Linux inotify file notification system.
Written by Manuel Amador (Rudd-O) <rudd-o@amautacorp.com>.

To use this, import the module, and call watch(...) to watch a
directory or file.  To monitor for events, use get_pending_events(...)
which you should call on a loop to avoid queue overflows.  See the
documentation for these functions for more information.

This module also defines several integer constants used throughout
the module.  These constants are meant to be ORed (|).  Here are the
meanings of each constant:

IN_ACCESS: File was accessed
IN_MODIFY: File was modified
IN_ATTRIB: File had its attributes changed
IN_CLOSE_WRITE: File (opened for writing) was closed
IN_CLOSE_NOWRITE: File (opened read-only) was closed
IN_OPEN: File was opened
IN_MOVED_FROM: File was moved from...
IN_MOVED_TO: ...file was moved to (always preceded by IN_MOVED_FROM)
IN_DELETE_SUBDIR: File was deleted (a subdir of a watched directory)
IN_DELETE_FILE: File was deleted (a file on a watched directory)
IN_CREATE_SUBDIR: File was created (a subdir of a watched directory)
IN_CREATE_FILE: File was created (in a watched directory)
IN_DELETE_SELF: File was deleted (while being watched)
IN_UNMOUNT: Backing filesystem for file was unmounted
IN_Q_OVERFLOW: Event queue overflowed
IN_IGNORED: (I myself ignore what's the purpose of this)
IN_ALL_EVENTS: Special mask, meaning all events
IN_CLOSE: Special mask, meaning all file closed events
"""

import _inotify
import select

IN_ACCESS = _inotify.IN_ACCESS
IN_MODIFY = _inotify.IN_MODIFY
IN_ATTRIB = _inotify.IN_ATTRIB
IN_CLOSE_WRITE = _inotify.IN_CLOSE_WRITE
IN_CLOSE_NOWRITE = _inotify.IN_CLOSE_NOWRITE
IN_OPEN = _inotify.IN_OPEN
IN_MOVED_FROM = _inotify.IN_MOVED_FROM
IN_MOVED_TO = _inotify.IN_MOVED_TO
IN_DELETE_SUBDIR = _inotify.IN_DELETE_SUBDIR
IN_DELETE_FILE = _inotify.IN_DELETE_FILE
IN_CREATE_SUBDIR = _inotify.IN_CREATE_SUBDIR
IN_CREATE_FILE = _inotify.IN_CREATE_FILE
IN_DELETE_SELF = _inotify.IN_DELETE_SELF
IN_UNMOUNT = _inotify.IN_UNMOUNT
IN_Q_OVERFLOW = _inotify.IN_Q_OVERFLOW
IN_IGNORED = _inotify.IN_IGNORED
IN_ALL_EVENTS = _inotify.IN_ALL_EVENTS
IN_CLOSE = _inotify.IN_CLOSE

__map_wd_to_filename = {}
__map_filename_to_wd = {}

def watch(filename,mask=IN_ALL_EVENTS):
	"""Watch a file (filename) for events matching a mask (integer,
	IN_ALL_EVENTS by default).
	
	The mask is an OR (|) combination of the integer constants IN_*
	defined in this module.
	
	If the file was already being watched, keep watching it, and update
	its event mask."""
	
	wd = _inotify.watch(filename,mask)
	__map_wd_to_filename[wd] = filename
	__map_filename_to_wd[filename] = wd
	
def ignore(filename):
	"""Stop watching the specified file (filename)."""
	
	wd = __map_filename_to_wd[filename]
	_inotify.ignore(wd)
	del __map_filename_to_wd[filename]
	del __map_wd_to_filename[wd]

class InotifyEvent:
	"""Represents an inotify event.
	
	self.filename: either None, or the path to the filename.
	self.event_mask: a logical OR of this module's IN_* constants."""
	
	filename = None
	
	event_mask = None
	
	def __str__(self):
		eventmask = []
		if self.event_mask & IN_ACCESS: eventmask.append("accessed")
		if self.event_mask & IN_MODIFY: eventmask.append("modified")
		if self.event_mask & IN_ATTRIB: eventmask.append("changed attributes")
		if self.event_mask & IN_CLOSE_WRITE: eventmask.append("closed (opened as writable)")
		if self.event_mask & IN_CLOSE_NOWRITE: eventmask.append("closed (opened as read-only)")
		if self.event_mask & IN_OPEN : eventmask.append("opened")
		if self.event_mask & IN_MOVED_FROM : eventmask.append("moved from")
		if self.event_mask & IN_MOVED_TO: eventmask.append("moved to")
		if self.event_mask & IN_DELETE_SUBDIR: eventmask.append("deleted (subdirectory)")
		if self.event_mask & IN_DELETE_FILE : eventmask.append("deleted (file)")
		if self.event_mask & IN_CREATE_SUBDIR: eventmask.append("created (subdirectory)")
		if self.event_mask & IN_CREATE_FILE: eventmask.append("created (file)")
		if self.event_mask & IN_DELETE_SELF : eventmask.append("deleted")
		if self.event_mask & IN_UNMOUNT : eventmask.append("backing FS unmounted")
		if self.event_mask & IN_Q_OVERFLOW : eventmask.append("queue overflow")
		if self.event_mask & IN_IGNORED : eventmask.append("ignored")
		return "inotify event %s for %s"%(eventmask,self.filename)

import os
def get_pending_events(block=False,timeout=0.0):
	"""Watch the event queue for at most 'timeout' seconds (float, 0.0
	by default).  Returns a list of events.  Each event is an instance
	of InotifyEvent.
	
	If block is False (the default), return immediately, whether any
	events were collected or not.  If block is True, wait forever until
	an event is received, or (optionally) for at most 'timeout' seconds."""
	
	if not type(timeout) in (int,float): raise ValueError,"timeout must be a non-negative float"
	if timeout < 0.0: raise ValueError,"timeout must be a non-negative float"
	
	if block:
		if timeout == 0.0: timeout = None
		else: pass
	else: timeout = 0.0
	
	fd = _inotify.fileno()
	# we're resorting to a python implementation of select, interruptible by KeyboardInterrupts
	fds = select.select([fd],[],[],timeout)
	if not fds[0]: return [] # nothing to see here, move along!
	# now we truly request the extension module for those files
	events = _inotify.get_pending_events(0)
	
	thelist = []
	for ev in events:
		wd = ev[0]
		filename = ev[1]
		mask = ev[2]
		e = InotifyEvent()
		e.filename = os.path.join(__map_wd_to_filename[wd],filename)
		e.event_mask = mask
		thelist.append(e)
	return thelist
	
