1 # Copyright (c) 2008 David Aguilar
2 """Provides an inotify plugin for Linux and other systems with pyinotify"""
3 from __future__
import division
, absolute_import
, unicode_literals
6 from threading
import Timer
7 from threading
import Lock
11 from pyinotify
import ProcessEvent
12 from pyinotify
import WatchManager
13 from pyinotify
import WatchManagerError
14 from pyinotify
import Notifier
15 from pyinotify
import EventsCodes
21 from cola
import utils
33 from PyQt4
import QtCore
35 from cola
import gitcfg
37 from cola
.compat
import ustr
, PY3
38 from cola
.git
import STDOUT
39 from cola
.i18n
import N_
40 from cola
.interaction
import Interaction
41 from cola
.models
import main
55 cfg
= gitcfg
.current()
56 if not cfg
.get('cola.inotify', True):
57 msg
= N_('inotify is disabled because "cola.inotify" is false')
63 msg
= N_('file notification: disabled\n'
64 'Note: install pywin32 to enable.\n')
65 elif utils
.is_linux():
66 msg
= N_('inotify: disabled\n'
67 'Note: install python-pyinotify to enable inotify.\n')
72 msg
+= N_('On Debian-based systems '
73 'try: sudo apt-get install python-pyinotify')
77 # Start the notification thread
78 _thread
= GitNotifier()
81 msg
= N_('File notification enabled.')
83 msg
= N_('inotify enabled.')
95 """Return True if pyinotify is available."""
96 return AVAILABLE
and _thread
and _thread
.isRunning()
100 """Queues filesystem events for broadcast"""
103 """Create an event handler"""
104 ## Timer used to prevent notification floods
106 ## Lock to protect files and timer from threading issues
110 """Broadcasts a list of all files touched since last broadcast"""
112 for observer
in _observers
:
116 def handle(self
, path
):
117 """Queues up filesystem events for broadcast"""
119 if self
._timer
is None:
120 self
._timer
= Timer(0.888, self
.broadcast
)
124 class FileSysEvent(ProcessEvent
):
125 """Generated by GitNotifier in response to inotify events"""
128 """Maintain event state"""
129 ProcessEvent
.__init
__(self
)
130 ## Takes care of Queueing events for broadcast
131 self
._handler
= Handler()
133 def process_default(self
, event
):
134 """Queues up inotify events for broadcast"""
137 path
= os
.path
.relpath(os
.path
.join(event
.path
, event
.name
))
138 self
._handler
.handle(path
)
141 class GitNotifier(QtCore
.QThread
):
142 """Polls inotify for changes and generates FileSysEvents"""
144 def __init__(self
, timeout
=333):
145 """Set up the pyinotify thread"""
146 QtCore
.QThread
.__init
__(self
)
147 ## Git command object
148 self
._git
= main
.model().git
150 self
._timeout
= timeout
152 self
._path
= self
._git
.worktree()
153 ## Signals thread termination
155 ## Directories to watching
156 self
._dirs
_seen
= set()
157 ## The inotify watch manager instantiated in run()
159 ## Has add_watch() failed?
160 self
._add
_watch
_failed
= False
163 self
._mask
= (EventsCodes
.ALL_FLAGS
['IN_ATTRIB'] |
164 EventsCodes
.ALL_FLAGS
['IN_CLOSE_WRITE'] |
165 EventsCodes
.ALL_FLAGS
['IN_CREATE'] |
166 EventsCodes
.ALL_FLAGS
['IN_DELETE'] |
167 EventsCodes
.ALL_FLAGS
['IN_MODIFY'] |
168 EventsCodes
.ALL_FLAGS
['IN_MOVED_TO'])
170 def stop(self
, stopped
):
171 """Tells the GitNotifier to stop"""
173 self
._running
= not stopped
175 def _watch_directory(self
, directory
):
176 """Set up a directory for monitoring by inotify"""
177 if self
._wmgr
is None or self
._add
_watch
_failed
:
179 directory
= core
.realpath(directory
)
180 if directory
in self
._dirs
_seen
:
182 self
._dirs
_seen
.add(directory
)
183 if core
.exists(directory
):
184 dir_arg
= directory
if PY3
else core
.encode(directory
)
186 self
._wmgr
.add_watch(dir_arg
, self
._mask
, quiet
=False)
187 except WatchManagerError
as e
:
188 self
._add
_watch
_failed
= True
189 self
._add
_watch
_failed
_warning
(directory
, e
)
191 def _add_watch_failed_warning(self
, directory
, e
):
192 core
.stderr('inotify: failed to watch "%s"' % directory
)
195 core
.stderr('If you have run out of watches then you may be able to')
196 core
.stderr('increase the number of allowed watches by running:')
198 core
.stderr(' echo fs.inotify.max_user_watches=100000 |')
199 core
.stderr(' sudo tee -a /etc/sysctl.conf &&')
200 core
.stderr(' sudo sysctl -p\n')
202 def _is_pyinotify_08x(self
):
203 """Is this pyinotify 0.8.x?
205 The pyinotify API changed between 0.7.x and 0.8.x.
206 This allows us to maintain backwards compatibility.
208 if hasattr(pyinotify
, '__version__'):
209 if pyinotify
.__version
__[:3] < '0.8':
214 """Create the inotify WatchManager and generate FileSysEvents"""
220 # Only capture events that git cares about
221 self
._wmgr
= WatchManager()
222 if self
._is
_pyinotify
_08x
():
223 notifier
= Notifier(self
._wmgr
, FileSysEvent(),
224 timeout
=self
._timeout
)
226 notifier
= Notifier(self
._wmgr
, FileSysEvent())
228 self
._watch
_directory
(self
._path
)
230 # Register files/directories known to git
231 for filename
in self
._git
.ls_files()[STDOUT
].splitlines():
232 filename
= core
.realpath(filename
)
233 directory
= os
.path
.dirname(filename
)
234 self
._watch
_directory
(directory
)
236 # self._running signals app termination. The timeout is a tradeoff
237 # between fast notification response and waiting too long to exit.
239 if self
._is
_pyinotify
_08x
():
240 check
= notifier
.check_events()
242 check
= notifier
.check_events(timeout
=self
._timeout
)
243 if not self
._running
:
246 notifier
.read_events()
247 notifier
.process_events()
251 """Generate notifications using pywin32"""
253 hdir
= win32file
.CreateFile(
256 win32con
.FILE_SHARE_READ | win32con
.FILE_SHARE_WRITE
,
258 win32con
.OPEN_EXISTING
,
259 win32con
.FILE_FLAG_BACKUP_SEMANTICS |
260 win32con
.FILE_FLAG_OVERLAPPED
,
263 flags
= (win32con
.FILE_NOTIFY_CHANGE_FILE_NAME |
264 win32con
.FILE_NOTIFY_CHANGE_DIR_NAME |
265 win32con
.FILE_NOTIFY_CHANGE_ATTRIBUTES |
266 win32con
.FILE_NOTIFY_CHANGE_SIZE |
267 win32con
.FILE_NOTIFY_CHANGE_LAST_WRITE |
268 win32con
.FILE_NOTIFY_CHANGE_SECURITY
)
270 buf
= win32file
.AllocateReadBuffer(8192)
271 overlapped
= pywintypes
.OVERLAPPED()
272 overlapped
.hEvent
= win32event
.CreateEvent(None, 0, 0, None)
276 win32file
.ReadDirectoryChangesW(hdir
, buf
, True, flags
, overlapped
)
278 rc
= win32event
.WaitForSingleObject(overlapped
.hEvent
,
280 if rc
!= win32event
.WAIT_OBJECT_0
:
282 nbytes
= win32file
.GetOverlappedResult(hdir
, overlapped
, True)
285 results
= win32file
.FILE_NOTIFY_INFORMATION(buf
, nbytes
)
286 for action
, path
in results
:
287 if not self
._running
:
289 path
= path
.replace('\\', '/')
290 if (not path
.startswith('.git/') and
291 '/.git/' not in path
and os
.path
.isfile(path
)):