<gh_stars>1-10
#!/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()
