doc: Do not mention unused cola.geometry variable
[git-cola.git] / cola / inotify.py
blobb71e8a215b65908f8fc5bbe5b5d996f3c8839fc1
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
25 INOTIFY_EVENT = QtCore.QEvent.User + 0
28 _thread = None
29 def start():
30 global _thread
31 if not AVAILABLE:
32 if not utils.is_linux():
33 return
34 msg = ('inotify: disabled\n'
35 'Note: install python-pyinotify to enable inotify.\n')
37 if utils.is_debian():
38 msg += ('On Debian systems '
39 'try: sudo aptitude install python-pyinotify')
40 cola.notifier().broadcast(signals.log_cmd, 0, msg)
41 return
43 # Start the notification thread
44 _thread = GitNotifier()
45 _thread.start()
46 msg = 'inotify support: enabled'
47 cola.notifier().broadcast(signals.log_cmd, 0, msg)
48 cola.notifier().broadcast(signals.inotify, True)
51 def stop():
52 if not has_inotify():
53 return
54 _thread.set_abort(True)
55 _thread.quit()
56 _thread.wait()
59 def has_inotify():
60 """Return True if pyinotify is available."""
61 return AVAILABLE and _thread and _thread.isRunning()
64 class EventReceiver(QtCore.QObject):
65 def event(self, msg):
66 """Overrides event() to handle custom inotify events."""
67 if not AVAILABLE:
68 return
69 if msg.type() == INOTIFY_EVENT:
70 cola.notifier().broadcast(signals.rescan)
71 return True
72 else:
73 return False
76 class FileSysEvent(ProcessEvent):
77 """Generated by GitNotifier in response to inotify events"""
79 def __init__(self, parent):
80 """Keep a reference to the QThread parent and maintain event state"""
81 ProcessEvent.__init__(self)
82 ## The Qt parent
83 self._parent = parent
84 ## Timer used to prevents notification floods
85 self._last_event_time = time.time()
87 def process_default(self, event):
88 """Notifies the Qt parent when actions occur"""
89 ## Limit the notification frequency to 1 per second
90 if time.time() - self._last_event_time > 1.0:
91 self._parent.notify()
92 self._last_event_time = time.time()
95 class GitNotifier(QtCore.QThread):
96 """Polls inotify for changes and generates FileSysEvents"""
98 def __init__(self, timeout=250):
99 """Set up the pyinotify thread"""
100 QtCore.QThread.__init__(self)
101 self.setTerminationEnabled(True)
103 ## QApplication receiver of Qt events
104 self._receiver = EventReceiver()
105 ## Git command object
106 self._git = cola.model().git
107 ## pyinotify timeout
108 self._timeout = timeout
109 ## Path to monitor
110 self._path = self._git.worktree()
111 ## Signals thread termination
112 self._abort = False
113 ## Directories to watching
114 self._dirs_seen = set()
115 ## The inotify watch manager instantiated in run()
116 self._wmgr = None
117 ## Events to capture
118 self._mask = (EventsCodes.ALL_FLAGS['IN_CREATE'] |
119 EventsCodes.ALL_FLAGS['IN_DELETE'] |
120 EventsCodes.ALL_FLAGS['IN_MODIFY'] |
121 EventsCodes.ALL_FLAGS['IN_MOVED_TO'])
123 def set_abort(self, abort):
124 """Tells the GitNotifier to abort"""
125 self._abort = abort
127 def notify(self):
128 """Post a Qt event in response to inotify updates"""
129 if not self._abort:
130 event_type = QtCore.QEvent.Type(INOTIFY_EVENT)
131 event = QtCore.QEvent(event_type)
132 QtCore.QCoreApplication.postEvent(self._receiver, event)
134 def _watch_directory(self, directory):
135 """Set up a directory for monitoring by inotify"""
136 if not self._wmgr:
137 return
138 directory = os.path.realpath(directory)
139 if directory not in self._dirs_seen:
140 self._wmgr.add_watch(directory, self._mask)
141 self._dirs_seen.add(directory)
143 def _is_pyinotify_08x(self):
144 """Is this pyinotify 0.8.x?
146 The pyinotify API changed between 0.7.x and 0.8.x.
147 This allows us to maintain backwards compatibility.
149 if hasattr(pyinotify, '__version__'):
150 if pyinotify.__version__[:3] == '0.8':
151 return True
152 return False
154 def run(self):
155 """Create the inotify WatchManager and generate FileSysEvents"""
156 # Only capture events that git cares about
157 self._wmgr = WatchManager()
158 if self._is_pyinotify_08x():
159 notifier = Notifier(self._wmgr, FileSysEvent(self),
160 timeout=self._timeout)
161 else:
162 notifier = Notifier(self._wmgr, FileSysEvent(self))
163 dirs_seen = set()
164 added_flag = False
165 # self._abort signals app termination. The timeout is a tradeoff
166 # between fast notification response and waiting too long to exit.
167 while not self._abort:
168 if not added_flag:
169 self._watch_directory(self._path)
170 # Register files/directories known to git
171 for filename in self._git.ls_files().splitlines():
172 directory = os.path.dirname(filename)
173 self._watch_directory(directory)
174 added_flag = True
175 notifier.process_events()
176 if self._is_pyinotify_08x():
177 check = notifier.check_events()
178 else:
179 check = notifier.check_events(timeout=self._timeout)
180 if check:
181 notifier.read_events()
182 notifier.stop()