commitmsg: hide the menu indicator
[git-cola.git] / cola / inotify.py
blob8734d8e9358cdc07c059b6116b900eb37a4e001d
1 # Copyright (c) 2008 David Aguilar
2 """Provides an inotify plugin for Linux and other systems with pyinotify"""
4 import os
5 from threading import Timer
6 from threading import Lock
8 try:
9 import pyinotify
10 from pyinotify import ProcessEvent
11 from pyinotify import WatchManager
12 from pyinotify import Notifier
13 from pyinotify import EventsCodes
14 AVAILABLE = True
15 except ImportError:
16 ProcessEvent = object
17 AVAILABLE = False
19 from cola import utils
20 if utils.is_win32():
21 try:
22 import win32file
23 import win32con
24 import pywintypes
25 import win32event
26 AVAILABLE = True
27 except ImportError:
28 ProcessEvent = object
29 AVAILABLE = False
31 from PyQt4 import QtCore
33 from cola import gitcfg
34 from cola import cmds
35 from cola import core
36 from cola.compat import set
37 from cola.git import STDOUT
38 from cola.i18n import N_
39 from cola.interaction import Interaction
40 from cola.models import main
43 _thread = None
45 def start():
46 global _thread
48 cfg = gitcfg.instance()
49 if not cfg.get('cola.inotify', True):
50 msg = N_('inotify is disabled because "cola.inotify" is false')
51 Interaction.log(msg)
52 return
54 if not AVAILABLE:
55 if utils.is_win32():
56 msg = N_('file notification: disabled\n'
57 'Note: install pywin32 to enable.\n')
58 elif utils.is_linux():
59 msg = N_('inotify: disabled\n'
60 'Note: install python-pyinotify to enable inotify.\n')
61 else:
62 return
64 if utils.is_debian():
65 msg += N_('On Debian systems '
66 'try: sudo aptitude install python-pyinotify')
67 Interaction.log(msg)
68 return
70 # Start the notification thread
71 _thread = GitNotifier()
72 _thread.start()
73 if utils.is_win32():
74 msg = N_('File notification enabled.')
75 else:
76 msg = N_('inotify enabled.')
77 Interaction.log(msg)
80 def stop():
81 if not has_inotify():
82 return
83 _thread.stop(True)
84 _thread.wait()
87 def has_inotify():
88 """Return True if pyinotify is available."""
89 return AVAILABLE and _thread and _thread.isRunning()
92 class Handler():
93 """Queues filesystem events for broadcast"""
95 def __init__(self):
96 """Create an event handler"""
97 ## Timer used to prevent notification floods
98 self._timer = None
99 ## Lock to protect files and timer from threading issues
100 self._lock = Lock()
102 def broadcast(self):
103 """Broadcasts a list of all files touched since last broadcast"""
104 with self._lock:
105 cmds.do(cmds.UpdateFileStatus)
106 self._timer = None
108 def handle(self, path):
109 """Queues up filesystem events for broadcast"""
110 with self._lock:
111 if self._timer is None:
112 self._timer = Timer(0.888, self.broadcast)
113 self._timer.start()
116 class FileSysEvent(ProcessEvent):
117 """Generated by GitNotifier in response to inotify events"""
119 def __init__(self):
120 """Maintain event state"""
121 ProcessEvent.__init__(self)
122 ## Takes care of Queueing events for broadcast
123 self._handler = Handler()
125 def process_default(self, event):
126 """Queues up inotify events for broadcast"""
127 if not event.name:
128 return
129 path = os.path.join(event.path, event.name)
130 if os.path.exists(path):
131 path = os.path.relpath(path)
132 self._handler.handle(path)
135 class GitNotifier(QtCore.QThread):
136 """Polls inotify for changes and generates FileSysEvents"""
138 def __init__(self, timeout=333):
139 """Set up the pyinotify thread"""
140 QtCore.QThread.__init__(self)
141 ## Git command object
142 self._git = main.model().git
143 ## pyinotify timeout
144 self._timeout = timeout
145 ## Path to monitor
146 self._path = self._git.worktree()
147 ## Signals thread termination
148 self._running = True
149 ## Directories to watching
150 self._dirs_seen = set()
151 ## The inotify watch manager instantiated in run()
152 self._wmgr = None
153 ## Events to capture
154 if utils.is_linux():
155 self._mask = (EventsCodes.ALL_FLAGS['IN_ATTRIB'] |
156 EventsCodes.ALL_FLAGS['IN_CLOSE_WRITE'] |
157 EventsCodes.ALL_FLAGS['IN_DELETE'] |
158 EventsCodes.ALL_FLAGS['IN_MODIFY'] |
159 EventsCodes.ALL_FLAGS['IN_MOVED_TO'])
161 def stop(self, stopped):
162 """Tells the GitNotifier to stop"""
163 self._timeout = 0
164 self._running = not stopped
166 def _watch_directory(self, directory):
167 """Set up a directory for monitoring by inotify"""
168 if not self._wmgr:
169 return
170 directory = core.realpath(directory)
171 if not core.exists(directory):
172 return
173 if directory not in self._dirs_seen:
174 self._wmgr.add_watch(directory, self._mask)
175 self._dirs_seen.add(directory)
177 def _is_pyinotify_08x(self):
178 """Is this pyinotify 0.8.x?
180 The pyinotify API changed between 0.7.x and 0.8.x.
181 This allows us to maintain backwards compatibility.
183 if hasattr(pyinotify, '__version__'):
184 if pyinotify.__version__[:3] == '0.8':
185 return True
186 return False
188 def run(self):
189 """Create the inotify WatchManager and generate FileSysEvents"""
191 if utils.is_win32():
192 self.run_win32()
193 return
195 # Only capture events that git cares about
196 self._wmgr = WatchManager()
197 if self._is_pyinotify_08x():
198 notifier = Notifier(self._wmgr, FileSysEvent(),
199 timeout=self._timeout)
200 else:
201 notifier = Notifier(self._wmgr, FileSysEvent())
203 self._watch_directory(self._path)
205 # Register files/directories known to git
206 for filename in self._git.ls_files()[STDOUT].splitlines():
207 filename = core.realpath(filename)
208 directory = os.path.dirname(filename)
209 self._watch_directory(directory)
211 # self._running signals app termination. The timeout is a tradeoff
212 # between fast notification response and waiting too long to exit.
213 while self._running:
214 if self._is_pyinotify_08x():
215 check = notifier.check_events()
216 else:
217 check = notifier.check_events(timeout=self._timeout)
218 if not self._running:
219 break
220 if check:
221 notifier.read_events()
222 notifier.process_events()
223 notifier.stop()
225 def run_win32(self):
226 """Generate notifications using pywin32"""
228 hdir = win32file.CreateFile(
229 self._path,
230 0x0001,
231 win32con.FILE_SHARE_READ | win32con.FILE_SHARE_WRITE,
232 None,
233 win32con.OPEN_EXISTING,
234 win32con.FILE_FLAG_BACKUP_SEMANTICS |
235 win32con.FILE_FLAG_OVERLAPPED,
236 None)
238 flags = (win32con.FILE_NOTIFY_CHANGE_FILE_NAME |
239 win32con.FILE_NOTIFY_CHANGE_DIR_NAME |
240 win32con.FILE_NOTIFY_CHANGE_ATTRIBUTES |
241 win32con.FILE_NOTIFY_CHANGE_SIZE |
242 win32con.FILE_NOTIFY_CHANGE_LAST_WRITE |
243 win32con.FILE_NOTIFY_CHANGE_SECURITY)
245 buf = win32file.AllocateReadBuffer(8192)
246 overlapped = pywintypes.OVERLAPPED()
247 overlapped.hEvent = win32event.CreateEvent(None, 0, 0, None)
249 handler = Handler()
250 while self._running:
251 win32file.ReadDirectoryChangesW(hdir, buf, True, flags, overlapped)
253 rc = win32event.WaitForSingleObject(overlapped.hEvent,
254 self._timeout)
255 if rc != win32event.WAIT_OBJECT_0:
256 continue
257 nbytes = win32file.GetOverlappedResult(hdir, overlapped, True)
258 if not nbytes:
259 continue
260 results = win32file.FILE_NOTIFY_INFORMATION(buf, nbytes)
261 for action, path in results:
262 if not self._running:
263 break
264 path = path.replace('\\', '/')
265 if (not path.startswith('.git/') and
266 '/.git/' not in path and os.path.isfile(path)):
267 handler.handle(path)