widgets.completion: Close the popup when the parent widget goes away
[git-cola.git] / cola / inotify.py
blobf365adfc19c6a1b8384c5604deb0f707fd0a4d07
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
7 from cola import utils
9 try:
10 import pyinotify
11 from pyinotify import ProcessEvent
12 from pyinotify import WatchManager
13 from pyinotify import Notifier
14 from pyinotify import EventsCodes
15 AVAILABLE = True
16 except ImportError:
17 ProcessEvent = object
18 AVAILABLE = False
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 import cola
34 from cola import core
35 from cola import signals
36 from cola.compat import set
38 _thread = None
39 def start():
40 global _thread
41 if not AVAILABLE:
42 if utils.is_win32():
43 msg = ('file notification: disabled\n'
44 'Note: install pywin32 to enable.\n')
45 elif utils.is_linux():
46 msg = ('inotify: disabled\n'
47 'Note: install python-pyinotify to enable inotify.\n')
48 else:
49 return
51 if utils.is_debian():
52 msg += ('On Debian systems '
53 'try: sudo aptitude install python-pyinotify')
54 cola.notifier().broadcast(signals.log_cmd, 0, msg)
55 return
57 # Start the notification thread
58 _thread = GitNotifier()
59 _thread.start()
60 if utils.is_win32():
61 msg = 'file notification: enabled'
62 else:
63 msg = 'inotify support: enabled'
64 cola.notifier().broadcast(signals.log_cmd, 0, msg)
66 def stop():
67 if not has_inotify():
68 return
69 _thread.stop(True)
70 _thread.wait()
73 def has_inotify():
74 """Return True if pyinotify is available."""
75 return AVAILABLE and _thread and _thread.isRunning()
78 class Handler():
79 """Queues filesystem events for broadcast"""
81 def __init__(self):
82 """Create an event handler"""
83 ## Timer used to prevent notification floods
84 self._timer = None
85 ## Lock to protect files and timer from threading issues
86 self._lock = Lock()
88 def broadcast(self):
89 """Broadcasts a list of all files touched since last broadcast"""
90 with self._lock:
91 cola.notifier().broadcast(signals.update_file_status)
92 self._timer = None
94 def handle(self, path):
95 """Queues up filesystem events for broadcast"""
96 with self._lock:
97 if self._timer is None:
98 self._timer = Timer(0.333, self.broadcast)
99 self._timer.start()
102 class FileSysEvent(ProcessEvent):
103 """Generated by GitNotifier in response to inotify events"""
105 def __init__(self):
106 """Maintain event state"""
107 ProcessEvent.__init__(self)
108 ## Takes care of Queueing events for broadcast
109 self._handler = Handler()
111 def process_default(self, event):
112 """Queues up inotify events for broadcast"""
113 if event.name is not None:
114 path = os.path.join(event.path, event.name)
115 path = os.path.relpath(path)
116 self._handler.handle(path)
119 class GitNotifier(QtCore.QThread):
120 """Polls inotify for changes and generates FileSysEvents"""
122 def __init__(self, timeout=333):
123 """Set up the pyinotify thread"""
124 QtCore.QThread.__init__(self)
125 ## Git command object
126 self._git = cola.model().git
127 ## pyinotify timeout
128 self._timeout = timeout
129 ## Path to monitor
130 self._path = self._git.worktree()
131 ## Signals thread termination
132 self._running = True
133 ## Directories to watching
134 self._dirs_seen = set()
135 ## The inotify watch manager instantiated in run()
136 self._wmgr = None
137 ## Events to capture
138 if utils.is_linux():
139 self._mask = (EventsCodes.ALL_FLAGS['IN_ATTRIB'] |
140 EventsCodes.ALL_FLAGS['IN_CLOSE_WRITE'] |
141 EventsCodes.ALL_FLAGS['IN_DELETE'] |
142 EventsCodes.ALL_FLAGS['IN_MODIFY'] |
143 EventsCodes.ALL_FLAGS['IN_MOVED_TO'])
145 def stop(self, stopped):
146 """Tells the GitNotifier to stop"""
147 self._timeout = 0
148 self._running = not stopped
150 def _watch_directory(self, directory):
151 """Set up a directory for monitoring by inotify"""
152 if not self._wmgr:
153 return
154 directory = os.path.realpath(directory)
155 if not os.path.exists(directory):
156 return
157 if directory not in self._dirs_seen:
158 self._wmgr.add_watch(directory, self._mask)
159 self._dirs_seen.add(directory)
161 def _is_pyinotify_08x(self):
162 """Is this pyinotify 0.8.x?
164 The pyinotify API changed between 0.7.x and 0.8.x.
165 This allows us to maintain backwards compatibility.
167 if hasattr(pyinotify, '__version__'):
168 if pyinotify.__version__[:3] == '0.8':
169 return True
170 return False
172 def run(self):
173 """Create the inotify WatchManager and generate FileSysEvents"""
175 if utils.is_win32():
176 self.run_win32()
177 return
179 # Only capture events that git cares about
180 self._wmgr = WatchManager()
181 if self._is_pyinotify_08x():
182 notifier = Notifier(self._wmgr, FileSysEvent(),
183 timeout=self._timeout)
184 else:
185 notifier = Notifier(self._wmgr, FileSysEvent())
187 self._watch_directory(self._path)
189 # Register files/directories known to git
190 for filename in core.decode(self._git.ls_files()).splitlines():
191 filename = os.path.realpath(filename)
192 directory = os.path.dirname(filename)
193 self._watch_directory(directory)
195 # self._running signals app termination. The timeout is a tradeoff
196 # between fast notification response and waiting too long to exit.
197 while self._running:
198 if self._is_pyinotify_08x():
199 check = notifier.check_events()
200 else:
201 check = notifier.check_events(timeout=self._timeout)
202 if not self._running:
203 break
204 if check:
205 notifier.read_events()
206 notifier.process_events()
207 notifier.stop()
209 def run_win32(self):
210 """Generate notifications using pywin32"""
212 hDir = win32file.CreateFile(
213 self._path,
214 0x0001,
215 win32con.FILE_SHARE_READ | win32con.FILE_SHARE_WRITE,
216 None,
217 win32con.OPEN_EXISTING,
218 win32con.FILE_FLAG_BACKUP_SEMANTICS |
219 win32con.FILE_FLAG_OVERLAPPED,
220 None)
222 flags = (win32con.FILE_NOTIFY_CHANGE_FILE_NAME |
223 win32con.FILE_NOTIFY_CHANGE_DIR_NAME |
224 win32con.FILE_NOTIFY_CHANGE_ATTRIBUTES |
225 win32con.FILE_NOTIFY_CHANGE_SIZE |
226 win32con.FILE_NOTIFY_CHANGE_LAST_WRITE |
227 win32con.FILE_NOTIFY_CHANGE_SECURITY)
229 buf = win32file.AllocateReadBuffer(8192)
230 overlapped = pywintypes.OVERLAPPED()
231 overlapped.hEvent = win32event.CreateEvent(None, 0, 0, None)
233 handler = Handler()
234 while self._running:
235 win32file.ReadDirectoryChangesW(hDir,
236 buf,
237 True,
238 flags,
239 overlapped)
241 rc = win32event.WaitForSingleObject(overlapped.hEvent, self._timeout)
242 if rc == win32event.WAIT_OBJECT_0:
243 nbytes = win32file.GetOverlappedResult(hDir, overlapped, True)
244 if nbytes:
245 results = win32file.FILE_NOTIFY_INFORMATION(buf, nbytes)
246 for action, path in results:
247 if not self._running:
248 break
250 path = path.replace('\\', '/')
251 if not path.startswith('.git/') and os.path.isfile(path):
252 handler.handle(path)