ActiveState Code

Recipe 217829: Watching a directory tree under Linux


Inspired by another snippet here that watches a directory... Here is a directory watcher that only works on Linux, as it uses the asyncronous directory notify feature. It will generate a SIGIO on directory change. You should define a signal handler that calls this back (actaully, calls the instance).

Python
 1
 2
 3
 4
 5
 6
 7
 8
 9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
################################################################
# DirectoryNotifier encapslates the code required for watching directory entry
# contents. Subclass this and override the entry_added() and entry_removed()
# methods. then, instantiate it with a directory name, and register it with the
# sighandler instance.  NOTE: this only works with Python 2.2 or greater on Linux. 
# Example:
#	dn = DirectoryNotifier(os.environ["HOME"])
#	manager.register(dn)

class DirectoryNotifier:
	def __init__(self, dirname):
		if not os.path.isdir(dirname):
			raise RuntimeError, "you can only watch a directory."
		self.dirname = dirname
		self.fd = os.open(dirname, 0)
		self.currentcontents = os.listdir(dirname)
		self.oldsig = fcntl.fcntl(self.fd, fcntl.F_GETSIG)
		fcntl.fcntl(self.fd, fcntl.F_SETSIG, 0)
		fcntl.fcntl(self.fd, fcntl.F_NOTIFY, fcntl.DN_DELETE|fcntl.DN_CREATE|fcntl.DN_MULTISHOT)

	def __del__(self):
#		fcntl.fcntl(self.fd, fcntl.F_SETSIG, self.oldsig)
		os.close(self.fd)
	
	def __str__(self):
		return "%s watching %s" % (self.__class__.__name__, self.dirname)

	# there are lots of race conditions here, but we'll live with that for now.
	def __call__(self, frame):
		newcontents = os.listdir(self.dirname)
		if len(newcontents) > len(self.currentcontents):
			new = filter(lambda item: item not in self.currentcontents, newcontents)
			self.entry_added(new)
		elif len(newcontents) < len(self.currentcontents):
			rem = filter(lambda item: item not in newcontents, self.currentcontents)
			self.entry_removed(rem)
		else:
			self.no_change()
		self.currentcontents = newcontents

	# override these in a subclass
	def entry_added(self, added):
		print added, "added to", self.dirname

	def entry_removed(self, removed):
		print removed, "removed from", self.dirname

	def no_change(self):
		print "No change in", self.dirname

Comments

  1. 1. At 7:33 p.m. on 9 jul 2005, Bill Anderson said:

    To use this recipe ...

    import signal
    import fcntl
    import os
    

    These are needed by the class.

    Next, the "sighandler" he mentions is here: http://aspn.activestate.com/ASPN/Cookbook/Python/Recipe/217830

    Also note that the argument passed to the object when a directory's contents change is a list, not a single filename. If you want to perform some action on each file, you will need to iterate over the argument "added" or "removed".

Sign in to comment