doc: Document the cola.qt module
[git-cola.git] / cola / inotify.py
blob3025211d411f29854671abac3b9cac59740f7675
1 # Copyright (c) 2008 David Aguilar
2 """Provides an inotify plugin for Linux and other systems with pyinotify"""
4 import os
5 import time
7 try:
8 import pyinotify
9 from pyinotify import ProcessEvent
10 from pyinotify import WatchManager
11 from pyinotify import Notifier
12 from pyinotify import EventsCodes
13 AVAILABLE = True
14 except ImportError:
15 ProcessEvent = object
16 AVAILABLE = False
17 pass
19 from PyQt4 import QtCore
21 import cola
22 from cola import signals
23 from cola import utils
24 from cola.compat import set
26 INOTIFY_EVENT = QtCore.QEvent.User + 0
29 _thread = None
30 def start():
31 global _thread
32 if not AVAILABLE:
33 if not utils.is_linux():
34 return
35 msg = ('inotify: disabled\n'
36 'Note: install python-pyinotify to enable inotify.\n')
38 if utils.is_debian():
39 msg += ('On Debian systems '
40 'try: sudo aptitude install python-pyinotify')
41 cola.notifier().broadcast(signals.log_cmd, 0, msg)
42 return
44 # Start the notification thread
45 _thread = GitNotifier()
46 _thread.start()
47 msg = 'inotify support: enabled'
48 cola.notifier().broadcast(signals.log_cmd, 0, msg)
49 cola.notifier().broadcast(signals.inotify, True)
52 def stop():
53 if not has_inotify():
54 return
55 _thread.set_abort(True)
56 _thread.quit()
57 _thread.wait()
60 def has_inotify():
61 """Return True if pyinotify is available."""
62 return AVAILABLE and _thread and _thread.isRunning()
65 class EventReceiver(QtCore.QObject):
66 def event(self, msg):
67 """Overrides event() to handle custom inotify events."""
68 if not AVAILABLE:
69 return
70 if msg.type() == INOTIFY_EVENT:
71 cola.notifier().broadcast(signals.rescan)
72 return True
73 else:
74 return False
77 class FileSysEvent(ProcessEvent):
78 """Generated by GitNotifier in response to inotify events"""
80 def __init__(self, parent):
81 """Keep a reference to the QThread parent and maintain event state"""
82 ProcessEvent.__init__(self)
83 ## The Qt parent
84 self._parent = parent
85 ## Timer used to prevents notification floods
86 self._last_event_time = time.time()
88 def process_default(self, event):
89 """Notifies the Qt parent when actions occur"""
90 ## Limit the notification frequency to 1 per second
91 if time.time() - self._last_event_time > 1.0:
92 self._parent.notify()
93 self._last_event_time = time.time()
96 class GitNotifier(QtCore.QThread):
97 """Polls inotify for changes and generates FileSysEvents"""
99 def __init__(self, timeout=250):
100 """Set up the pyinotify thread"""
101 QtCore.QThread.__init__(self)
102 self.setTerminationEnabled(True)
104 ## QApplication receiver of Qt events
105 self._receiver = EventReceiver()
106 ## Git command object
107 self._git = cola.model().git
108 ## pyinotify timeout
109 self._timeout = timeout
110 ## Path to monitor
111 self._path = self._git.worktree()
112 ## Signals thread termination
113 self._abort = False
114 ## Directories to watching
115 self._dirs_seen = set()
116 ## The inotify watch manager instantiated in run()
117 self._wmgr = None
118 ## Events to capture
119 self._mask = (EventsCodes.ALL_FLAGS['IN_CREATE'] |
120 EventsCodes.ALL_FLAGS['IN_DELETE'] |
121 EventsCodes.ALL_FLAGS['IN_MODIFY'] |
122 EventsCodes.ALL_FLAGS['IN_MOVED_TO'])
124 def set_abort(self, abort):
125 """Tells the GitNotifier to abort"""
126 self._abort = abort
128 def notify(self):
129 """Post a Qt event in response to inotify updates"""
130 if not self._abort:
131 event_type = QtCore.QEvent.Type(INOTIFY_EVENT)
132 event = QtCore.QEvent(event_type)
133 QtCore.QCoreApplication.postEvent(self._receiver, event)
135 def _watch_directory(self, directory):
136 """Set up a directory for monitoring by inotify"""
137 if not self._wmgr:
138 return
139 directory = os.path.realpath(directory)
140 if not os.path.exists(directory):
141 return
142 if directory not in self._dirs_seen:
143 self._wmgr.add_watch(directory, self._mask)
144 self._dirs_seen.add(directory)
146 def _is_pyinotify_08x(self):
147 """Is this pyinotify 0.8.x?
149 The pyinotify API changed between 0.7.x and 0.8.x.
150 This allows us to maintain backwards compatibility.
152 if hasattr(pyinotify, '__version__'):
153 if pyinotify.__version__[:3] == '0.8':
154 return True
155 return False
157 def run(self):
158 """Create the inotify WatchManager and generate FileSysEvents"""
159 # Only capture events that git cares about
160 self._wmgr = WatchManager()
161 if self._is_pyinotify_08x():
162 notifier = Notifier(self._wmgr, FileSysEvent(self),
163 timeout=self._timeout)
164 else:
165 notifier = Notifier(self._wmgr, FileSysEvent(self))
166 dirs_seen = set()
167 added_flag = False
168 # self._abort signals app termination. The timeout is a tradeoff
169 # between fast notification response and waiting too long to exit.
170 while not self._abort:
171 if not added_flag:
172 self._watch_directory(self._path)
173 # Register files/directories known to git
174 for filename in self._git.ls_files().splitlines():
175 directory = os.path.dirname(filename)
176 self._watch_directory(directory)
177 added_flag = True
178 notifier.process_events()
179 if self._is_pyinotify_08x():
180 check = notifier.check_events()
181 else:
182 check = notifier.check_events(timeout=self._timeout)
183 if check:
184 notifier.read_events()
185 notifier.stop()