# 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 31 2005-03-25 15:34:22Z 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 instantiate a Watcher. Use the
watcher's methods to your advantage. For example, watch(...) to watch a
directory or file.  To get events, use get_next_event(...)
which you should call on a loop and vacate quickly to avoid queue
overflows. See the documentation for the Watcher class 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:

ACCESS: File was accessed
MODIFY: File was modified
ATTRIB: File had its attributes changed
CLOSE_WRITE: File (opened for writing) was closed
CLOSE_NOWRITE: File (opened read-only) was closed
OPEN: File was opened
MOVED_FROM: File was moved from...
MOVED_TO: ...file was moved to (always preceded by MOVED_FROM)
DELETE_SUBDIR: File was deleted (a subdir of a watched directory)
DELETE_FILE: File was deleted (a file on a watched directory)
CREATE_SUBDIR: File was created (a subdir of a watched directory)
CREATE_FILE: File was created (in a watched directory)
DELETE_SELF: File was deleted (while being watched)
UNMOUNT: Backing filesystem for file was unmounted
Q_OVERFLOW: Event queue overflowed
ALL_EVENTS: Special mask, meaning all events
CLOSE: Special mask, meaning all file closed events
"""

try: import _inotify
except OSError: raise ImportError,"This operating system does not have inotify support"
import select
import os
import logging

ACCESS = _inotify.IN_ACCESS
MODIFY = _inotify.IN_MODIFY
ATTRIB = _inotify.IN_ATTRIB
CLOSE_WRITE = _inotify.IN_CLOSE_WRITE
CLOSE_NOWRITE = _inotify.IN_CLOSE_NOWRITE
OPEN = _inotify.IN_OPEN
MOVED_FROM = _inotify.IN_MOVED_FROM
MOVED_TO = _inotify.IN_MOVED_TO
DELETE_SUBDIR = _inotify.IN_DELETE_SUBDIR
DELETE_FILE = _inotify.IN_DELETE_FILE
CREATE_SUBDIR = _inotify.IN_CREATE_SUBDIR
CREATE_FILE = _inotify.IN_CREATE_FILE
DELETE_SELF = _inotify.IN_DELETE_SELF
UNMOUNT = _inotify.IN_UNMOUNT
Q_OVERFLOW = _inotify.IN_Q_OVERFLOW
_IGNORED = _inotify.IN_IGNORED
ALL_EVENTS = _inotify.IN_ALL_EVENTS
CLOSE = _inotify.IN_CLOSE


def sysfs_get(key):

	key = os.path.join("/sys",key.replace(".","/"))
	try:
		try:
			f = file(key,"rb")
			val = f.readlines()[0].strip()
		finally: f.close()

		# if it is an int, return an int
		try: val = int(val)
		except: pass

		# else return a string
		return val
	except: pass

def sysfs_set(key,val):

	if val is None: raise ValueError, "Value cannot be None"
	key = os.path.join("/sys",key.replace(".","/"))
	f = file(key,"wb")
	f.write(str(val))
	f.close()

def _dup_watch_limit():
	"""Duplicate the watch limit.  Returns new maximum value, or None
	if it failed."""
	try:
		val = sysfs_get("class.misc.inotify.max_user_watches")
		val = val * 2
		sysfs_set("class.misc.inotify.max_user_watches",val)
		return val
	except: pass

def _dup_device_limit():
	"""Duplicate the device limit.  Returns new maximum value, or None
	if it failed."""
	try:
		val = sysfs_get("class.misc.inotify.max_user_devices")
		val = val * 2
		sysfs_set("class.misc.inotify.max_user_devices",val)
		return val
	except: pass

def _set_queue_limit(val):
	"""Duplicate the device limit.  Returns new maximum value, or None
	if it failed."""
	try:
		sysfs_set("class.misc.inotify.max_queued_events",val)
		return val
	except: pass

# we hopelessly try to do this!
_set_queue_limit(16384)

class Event:
	"""Represents an inotify event.
	
	self.filename: either None, or the filename involved in the event.
	self.watched_filename: either None, or the path to the originally
			       watched file that prompted the event.
	self.event_mask: a logical OR of this module's constants.
	
	To test the type of event, you can use the "&" operator:
		if event & inotify.CLOSE: ... do stuff
	You cannot test for equality since an event can have several
	types.
	"""
	
	watched_filename = None
	filename = None
	event_mask = None
	
	def __init__(self):
		self.watched_filename = None
		self.filename = None
		self.event_mask = None
	
	def __str__(self):
		eventmask = []
		if self.event_mask & ACCESS: eventmask.append("accessed")
		if self.event_mask & MODIFY: eventmask.append("modified")
		if self.event_mask & ATTRIB: eventmask.append("changed attributes")
		if self.event_mask & CLOSE_WRITE: eventmask.append("closed (opened as writable)")
		if self.event_mask & CLOSE_NOWRITE: eventmask.append("closed (opened as read-only)")
		if self.event_mask & OPEN : eventmask.append("opened")
		if self.event_mask & MOVED_FROM : eventmask.append("moved from")
		if self.event_mask & MOVED_TO: eventmask.append("moved to")
		if self.event_mask & DELETE_SUBDIR: eventmask.append("deleted (subdirectory)")
		if self.event_mask & DELETE_FILE : eventmask.append("deleted (file)")
		if self.event_mask & CREATE_SUBDIR: eventmask.append("created (subdirectory)")
		if self.event_mask & CREATE_FILE: eventmask.append("created (file)")
		if self.event_mask & DELETE_SELF : eventmask.append("deleted")
		if self.event_mask & UNMOUNT : eventmask.append("backing FS unmounted")
		if self.event_mask & Q_OVERFLOW : eventmask.append("queue overflow")
		return "inotify event %s for %s"%(eventmask,self.filename)

	def __and__(self,other):
		return self.event_mask & other
	
	def as_string(self):
		"""Returns an easily parsable textual representation of the
		event.  This representation is designed for use in command-
		line clients"""

		eventmask = []
		if self.event_mask & ACCESS: eventmask.append("ACCESS")
		if self.event_mask & MODIFY: eventmask.append("MODIFY")
		if self.event_mask & ATTRIB: eventmask.append("ATTRIB")
		if self.event_mask & CLOSE_WRITE: eventmask.append("CLOSE_WRITE")
		if self.event_mask & CLOSE_NOWRITE: eventmask.append("CLOSE_NOWRITE")
		if self.event_mask & OPEN : eventmask.append("OPEN")
		if self.event_mask & MOVED_FROM : eventmask.append("MOVED_FROM")
		if self.event_mask & MOVED_TO: eventmask.append("MOVED_TO")
		if self.event_mask & DELETE_SUBDIR: eventmask.append("DELETE_SUBDIR")
		if self.event_mask & DELETE_FILE : eventmask.append("DELETE_FILE")
		if self.event_mask & CREATE_SUBDIR: eventmask.append("CREATE_SUBDIR")
		if self.event_mask & CREATE_FILE: eventmask.append("CREATE_FILE")
		if self.event_mask & DELETE_SELF : eventmask.append("DELETE_SELF")
		if self.event_mask & UNMOUNT : eventmask.append("UNMOUNT")
		if self.event_mask & Q_OVERFLOW : eventmask.append("Q_OVERFLOW")
		if self.filename is None: return "%s"%("|".join(eventmask))
		return "%s:%s"%("|".join(eventmask),self.filename)

from FSTree import FSTreeNode,FSTreeRoot
from Queue import Queue

from threading import RLock
class Watcher:
	"""An inotify watcher.  Use this to monitor files and
	directories for events.
	
	Use watch() to watch a file or directory, ignore() to stop
	watching a file or directory, and get_next_event() to
	get Events in the queue.
	
	There is a system limit to how many watchers a user can hold.
	By	default, it is 8. This class raises the limit automatically
	whenever you instantiate one Watcher too many - if it cannot
	raise the limit, IOError 28 (ENOSPC - No space left on device)
	will be raised.  Normally, only root is allowed to raise	the
	limit, so for most user-space applications, you can expect
	the IOError to be raised if the limit is hit.

	There is another system limit of approx 1022 open watchers,
	consistent with the max open files limitation.  This may vary,
	but hitting it usually raises IOError 24 (Too many open files)."""

	def __init__(self):
		self.__tree = FSTreeRoot()
		self.__map_wd_to_node = {}
		try: self.__f = file("/dev/inotify","rb")
		except IOError,e:
			if e.errno == 28:
				# dupe the limit...
				new_limit = _dup_device_limit()
				if new_limit is None: raise
				# ...and retry the call, yes sir!
				self.__f = file("/dev/inotify","rb")
			else: raise
			
		#do it!
		self.__lock = RLock()
		self.__event_queue = Queue()
		
		from threading import Thread
		self.__thread = Thread(target=self._process_events)
		self.__thread.setDaemon(True)
		self.__thread.start()
	
	_spy_mask = _IGNORED|MOVED_FROM|MOVED_TO|UNMOUNT
	
	def watch(self,filename,mask=ALL_EVENTS):
		"""Watch a file (filename) for events matching a mask (integer,
		ALL_EVENTS by default).
		
		The mask is an OR (|) combination of the integer constants
		defined in this module.
		
		If the file was already being watched, keep watching it, and update
		its event mask.

		Once a file is deleted, it will not be watched for anymore, and
		will be removed from the watch list automatically.
		
		As with any other file I/O operations, other IOErrors
		may be raised (Permission denied, etc.).
		
		There is a limit on the number of files/directories that can be
		watched.  In standard Linux inotify, it is 16384.  This can be
		increased to your needs by using the sysfs control
		/sys/class/misc/inotify/max_user_watches.  This class raises the
		limit automatically whenever you watch() one file too many - if it
		cannot raise the limit, IOError 28 (ENOSPC - No space left on
		device) will be raised.  Normally, only root is allowed to raise
		the limit, so for most user-space applications, you can expect
		the IOError to be raised if watching an entire hard disk.

		Just as a reference:
		16384 watches appear to take about 1MB of kernel memory, and my
		single-user Linux system has way more than 16000 directories, so
		do the math.  
		"""
		self.__lock.acquire()
		try:
			filename = os.path.abspath(filename)
			themask = mask | self._spy_mask
			
			try:
				try: wd = _inotify.watch(self.__f.fileno(),filename,themask)
				except IOError,e:
					if e.errno == 28:
						if _dup_watch_limit() is None: raise
						wd = _inotify.watch(self.__f.fileno(),filename,themask)
					else: raise
				node = self.__tree.lookup(filename,True)
				node.wd = wd
				node.user_mask = mask
				self.__map_wd_to_node[wd] = node

				ancestors = node.get_ancestors()

				for ancestor in ancestors:
					filename = ancestor.get_path()
					if hasattr(ancestor,"wd"): continue
					try: wd = _inotify.watch(self.__f.fileno(),filename,self._spy_mask)
					except IOError,e:
						if e.errno == 28:
							if _dup_watch_limit() is None: raise
							wd = _inotify.watch(self.__f.fileno(),filename,self._spy_mask)
						else: raise
					node = self.__tree.lookup(filename,True)
					node.wd = wd
					self.__map_wd_to_node[wd] = node
			except:
				# Call failed, do nothing because we're fucked
				raise

		finally:
			self.__lock.release()
		
	def ignore(self,filename):
		"""Stop watching the specified file (filename)."""
		
		self.__lock.acquire()
		try:
			m_filename = filename
			filename = os.path.abspath(filename)
			try: node = self.__tree.lookup(filename)
			except LookupError: raise KeyError,m_filename
			try:
				wd = node.wd # check if it is watched
				mask = node.user_mask # and if it is watched *by the user*
			except AttributeError: raise KeyError,m_filename
			delattr(node,"user_mask")
			# now, instead of deleting it, we update the mask to the spy mask
			_inotify.watch(self.__f.fileno(),filename,self._spy_mask)
		finally:
			self.__lock.release()
	
	def get_watching(self):
		"""Return the list of watched files."""

		self.__lock.acquire()
		keys = [ a.get_path() for a in self.__map_wd_to_node.values() if hasattr(a,"user_mask") ]
		self.__lock.release()
		return keys
		
	def is_watching(self,filename):
		"""Return True or False if filename is/isn't being watched."""
	
		self.__lock.acquire()
		try:
			m_filename = filename
			filename = os.path.abspath(filename)
			is_wat = True
			try:
				node = self.__tree.lookup(filename) # check if its in the tree
				wd = node.wd # check if it is watched
				mask = node.user_mask # and if it is watched *by the user*
			except LookupError: is_wat = False
			except AttributeError: is_wat = False
		finally:
			self.__lock.release()
		return is_wat

	def _process_events(self):
		# to ensure no shit goes down
		try:
			while True: self._get_pending_events(block=True)
		except:
			import logging
			logging.getLogger("inotify").error("Error while processing inotify events",exc_info=True)
		
	def get_next_event(self,block=True,timeout=None):
		"""Remove and return an item from the queue. If
		optional args block is true and timeout is None
		(the default), block if necessary until an item
		is available. If timeout is a positive number,
		it blocks at most timeout seconds and raises
		the Queue.Empty exception if no item was available
		within that time. Otherwise (block is false),
		return an item if one is immediately available,
		else raise the Empty exception (timeout is ignored
		in that case)."""
		return self.__event_queue.get(block,timeout)
		
	def _get_pending_events(self,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 = self.__f.fileno()
		# we're resorting to a python implementation of select, interruptible by KeyboardInterrupts
		try: fds = select.select([fd],[],[],timeout)
		except select.error,e:
			# some weird interrupted syscall shit, that's busting my balls, we'll just return shit
			if e[0] == 4: return []
			else: raise
		if not fds[0]:
			return [] # nothing to see here, move along!
		
		self.__lock.acquire()
		thelist = []
		try:
			# now we truly request the extension module for those files
			events = _inotify.get_pending_events(fd,0)
			
			for ev in events:
				wd = ev[0]
# 				print "Event with wd",wd
				filename = ev[1]
				mask = ev[2]
				if mask & Q_OVERFLOW: # we're fucked
					raise Exception,"Queue overflowed"
				if wd != -1:

					if mask & _IGNORED: # no longer watched
						# get the node
						try: node = self.__map_wd_to_node[wd]
						except KeyError: continue
						# remove the node
						node.reparent(None)
						# delete the wd
						del self.__map_wd_to_node[wd]
						continue
					if mask & MOVED_FROM:
# 						print "Moved from"
						self.moved_from = (wd,filename)
					elif mask & MOVED_TO:
						try:
							moved_from_wd,moved_from_name = self.moved_from
							moved_to_wd,moved_to_name = wd,filename
							sourcedir	= self.__map_wd_to_node[moved_from_wd]
							destdir	= self.__map_wd_to_node[moved_to_wd]
							
							node = sourcedir.children[moved_from_name]
							node.reparent(None)
							node.rename(moved_to_name)
							destdir.children.append(node)
						except AttributeError:
							# the file just appeared, we weren't watching the source directory
							pass
						except KeyError:
							# its destination directory wasn't being watched, just pass
							pass
					else: # last moved file vanished into non-watched space
						if hasattr(self,"moved_from"):
							moved_from_wd,moved_from_name = self.moved_from
							del self.moved_from
							sourcedir = self.__map_wd_to_node[moved_from_wd]
							node = None
							try:
								node = sourcedir.children[moved_from_name]
							except KeyError: pass
							if node:
								wds = [ n.wd for n in [node] + node.get_descendents() if hasattr(n,"wd") ]
								for wd in wds:
									_inotify.ignore(self.__f.fileno(),wd)
									del self.__map_wd_to_node[wd]
								node.reparent(None)

				try: node = self.__map_wd_to_node[wd]
				except KeyError: node = None
				# let's check if the event matches the user mask
				if not hasattr(node,"user_mask"): continue
				if not (node.user_mask & mask): continue
				
				e = Event()
				e.event_mask = mask
				if node:
					e.watched_filename = node.get_path()
					if filename: e.filename = os.path.join(e.watched_filename,filename)
					else: e.filename = e.watched_filename
				self.__event_queue.put(e,block=True)
		finally:
			self.__lock.release()

		return thelist

	def _dump(self):
		print "--------------------"
		for a,b in self.__map_wd_to_node.iteritems():
			print "%s: %s"%(a,b)
		print "--------------------"

	def ignore_all(self):
		"""Stop watching all watched files and directories."""
		self.__lock.acquire()
		try:
			wds = self.__map_wd_to_node.keys()
			for wd in wds:
				_inotify.ignore(self.__f.fileno(),wd)
			self.__tree = FSTreeRoot()
		finally:
			self.__lock.release()
	
	def __del__(self):
		import logging
		try:
			logging.getLogger("inotify."+str(self.__f.fileno())).error("Successfully finished up")
			self.ignore_all()
		except:
			logging.getLogger("inotify."+str(self.__f.fileno())).error("Error while finishing up",exc_info=True)
	
