blob: 39e1984f768b5cf4bc6de85bf2b0111d38e7fb7b [file] [log] [blame]
#!/usr/bin/python
# Copyright 2014 Google Inc. All Rights Reserved.
#
# Licensed under the Apache License, Version 2.0 (the "License");
# you may not use this file except in compliance with the License.
# You may obtain a copy of the License at
#
# http://www.apache.org/licenses/LICENSE-2.0
#
# Unless required by applicable law or agreed to in writing, software
# distributed under the License is distributed on an "AS IS" BASIS,
# WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
# See the License for the specific language governing permissions and
# limitations under the License.
#
# pylint:disable=invalid-name
"""Simple real-time notifications of file changes."""
import os
import os.path
import garbage
import mainloop
import pyinotify
Error = pyinotify.WatchManagerError
class FileNotifier(object):
"""A class for managing file notifications in a tornado ioloop."""
# In some future world, there may be more than one kind of filenotifier
# implementation (eg. if inotify isn't available). Make our error base
# class accessible to anyone who has an instance of our FileNotifier so
# that they can try/except on it safely for any variant.
Error = pyinotify.WatchManagerError
def __init__(self, loop):
self.loop = loop
self.watches = {}
self.wm = pyinotify.WatchManager()
self.tornado_notifier = pyinotify.TornadoAsyncNotifier(
self.wm, self.loop.ioloop)
def __del__(self):
with garbage.GcIgnorer():
self.wm = None
self.loop = None
if getattr(self, 'tornado_notifier', None):
self.tornado_notifier.stop()
self.tornado_notifier = None
def WatchObj(self, filename, callback):
"""Returns a filenotifier.Watch object, which calls Add() and Del()."""
return Watch(self, filename, callback)
def Add(self, filename, callback):
"""Register a callback for the given filename.
Args:
filename: the filename which, when created, deleted, or
closed-after-write, will trigger the given callback.
callback: the function to call whenever the file changes.
Raises:
pyinotify.WatchManagerError: if the containing directory does
not exist.
"""
path, name = os.path.split(filename)
pathdata = self.watches.get(path, None)
if pathdata:
wd, files = pathdata
else:
files = {}
with garbage.GcIgnorer():
wddict = self.wm.add_watch(
path,
(pyinotify.IN_CLOSE_WRITE |
pyinotify.IN_MOVED_FROM |
pyinotify.IN_MOVED_TO |
pyinotify.IN_DELETE),
lambda ev: self._Notified(ev, files),
quiet=False)
wd = wddict[path]
self.watches[path] = (wd, files)
filecalls = files.get(name, None)
if not filecalls:
filecalls = files[name] = []
filecalls.append(callback)
def Del(self, filename, callback):
path, name = os.path.split(filename)
wd, files = self.watches[path]
filecalls = files[name]
filecalls.remove(callback)
if not filecalls:
del files[name]
if not files:
with garbage.GcIgnorer():
self.wm.rm_watch(wd, quiet=False)
del self.watches[path]
def _Notified(self, ev, files):
filecalls = files.get(ev.name, None)
if not filecalls: return
for i in filecalls:
i()
class Watch(object):
"""A lifecycle handler for FileNotifier watches.
Automatically calls filenotifier.Add and .Del when the object comes
into and out of existence.
"""
def __init__(self, filenotifier, filename, callback):
self.filenotifier = filenotifier
self.filename = filename
self.callback = callback
self.registered = False
self.filenotifier.Add(self.filename, self.callback)
self.registered = True
def __del__(self):
if self.registered:
self.filenotifier.Del(self.filename, self.callback)
def main():
def MeCall():
print 'mecall'
def MeCall2():
print 'mecall2'
def MeCall22():
print 'mecall22'
loop = mainloop.MainLoop()
n = FileNotifier(loop)
n.Add('/tmp/whatzit', MeCall)
n.Add('/tmp/whatzit2', MeCall2)
if not os.path.isdir('/tmp/d'):
os.makedirs('/tmp/d')
n.Add('/tmp/d/whatzit2', MeCall22)
loop.Start()
if __name__ == '__main__':
main()