1 # Copyright (c) 2008 David Aguilar
2 # Copyright (c) 2015 Daniel Harding
3 """Provides an filesystem monitoring for Linux (via inotify) and for Windows
4 (via pywin32 and the ReadDirectoryChanges function)"""
5 from __future__
import division
, absolute_import
, unicode_literals
11 from threading
import Lock
13 from cola
import utils
27 elif utils
.is_linux():
29 from cola
import inotify
35 from PyQt4
import QtCore
36 from PyQt4
.QtCore
import SIGNAL
39 from cola
import gitcfg
40 from cola
import gitcmds
41 from cola
.compat
import bchr
42 from cola
.git
import git
43 from cola
.i18n
import N_
44 from cola
.interaction
import Interaction
47 class _Monitor(QtCore
.QObject
):
48 def __init__(self
, thread_class
):
49 QtCore
.QObject
.__init
__(self
)
50 self
._thread
_class
= thread_class
54 if self
._thread
_class
is not None:
55 assert self
._thread
is None
56 self
._thread
= self
._thread
_class
(self
)
60 if self
._thread
_class
is not None:
61 assert self
._thread
is not None
67 if self
._thread
is not None:
68 self
._thread
.refresh()
71 class _BaseThread(QtCore
.QThread
):
72 #: The delay, in milliseconds, between detecting file system modification
73 #: and triggering the 'files_changed' signal, to coalesce multiple
74 #: modifications into a single signal.
75 _NOTIFICATION_DELAY
= 888
77 def __init__(self
, monitor
):
78 QtCore
.QThread
.__init
__(self
)
79 self
._monitor
= monitor
84 """Do any housekeeping necessary in response to repository changes."""
88 """Notifies all observers"""
90 self
._monitor
.emit(SIGNAL('files_changed'))
93 def _log_enabled_message():
94 msg
= N_('File system change monitoring: enabled.\n')
95 Interaction
.safe_log(msg
)
98 if AVAILABLE
== 'inotify':
99 class _InotifyThread(_BaseThread
):
102 inotify
.IN_CLOSE_WRITE |
106 inotify
.IN_MOVED_FROM |
111 inotify
.IN_EXCL_UNLINK |
115 def __init__(self
, monitor
):
116 _BaseThread
.__init
__(self
, monitor
)
117 self
._worktree
= core
.abspath(git
.worktree())
118 self
._git
_dir
= git
.git_dir()
120 self
._inotify
_fd
= None
123 self
._worktree
_wds
= set()
124 self
._worktree
_wd
_map
= {}
125 self
._git
_dir
_wds
= set()
126 self
._git
_dir
_wd
_map
= {}
129 def _log_out_of_wds_message():
130 msg
= N_('File system change monitoring: disabled because the'
131 ' limit on the total number of inotify watches was'
132 ' reached. You may be able to increase the limit on'
133 ' the number of watches by running:\n'
135 ' echo fs.inotify.max_user_watches=100000 |'
136 ' sudo tee -a /etc/sysctl.conf &&'
138 Interaction
.safe_log(msg
)
143 self
._inotify
_fd
= inotify
.init()
144 self
._pipe
_r
, self
._pipe
_w
= os
.pipe()
146 poll_obj
= select
.poll()
147 poll_obj
.register(self
._inotify
_fd
, select
.POLLIN
)
148 poll_obj
.register(self
._pipe
_r
, select
.POLLIN
)
152 self
._log
_enabled
_message
()
156 timeout
= self
._NOTIFICATION
_DELAY
160 events
= poll_obj
.poll(timeout
)
162 if e
.errno
== errno
.EINTR
:
167 if not self
._running
:
172 for fd
, event
in events
:
173 if fd
== self
._inotify
_fd
:
174 self
._handle
_events
()
177 if self
._inotify
_fd
is not None:
178 os
.close(self
._inotify
_fd
)
179 self
._inotify
_fd
= None
180 if self
._pipe
_r
is not None:
181 os
.close(self
._pipe
_r
)
183 os
.close(self
._pipe
_w
)
188 if self
._inotify
_fd
is None:
191 tracked_dirs
= set(os
.path
.dirname(
192 os
.path
.join(self
._worktree
, path
))
193 for path
in gitcmds
.tracked_files())
194 self
._refresh
_watches
(tracked_dirs
, self
._worktree
_wds
,
195 self
._worktree
_wd
_map
)
197 git_dirs
.add(self
._git
_dir
)
198 for dirpath
, dirnames
, filenames
in core
.walk(
199 os
.path
.join(self
._git
_dir
, 'refs')):
200 git_dirs
.add(dirpath
)
201 self
._refresh
_watches
(git_dirs
, self
._git
_dir
_wds
,
202 self
._git
_dir
_wd
_map
)
204 if e
.errno
== errno
.ENOSPC
:
205 self
._log
_out
_of
_wds
_message
()
206 self
._running
= False
210 def _refresh_watches(self
, paths_to_watch
, wd_set
, wd_map
):
211 watched_paths
= set(wd_map
)
212 for path
in watched_paths
- paths_to_watch
:
213 wd
= wd_map
.pop(path
)
216 inotify
.rm_watch(self
._inotify
_fd
, wd
)
218 if e
.errno
== errno
.EINVAL
:
219 # This error can occur if the target of the wd was
220 # removed on the filesystem before we call
221 # inotify.rm_watch() so ignore it.
225 for path
in paths_to_watch
- watched_paths
:
227 wd
= inotify
.add_watch(self
._inotify
_fd
, core
.encode(path
),
230 if e
.errno
in (errno
.ENOENT
, errno
.ENOTDIR
):
231 # These two errors should only occur as a result of
232 # race conditions: the first if the directory
233 # referenced by path was removed or renamed before the
234 # call to inotify.add_watch(); the second if the
235 # directory referenced by path was replaced with a file
236 # before the call to inotify.add_watch(). Therefore we
237 # simply ignore them.
245 def _filter_event(self
, wd
, mask
, name
):
246 # An event is relevant iff:
247 # 1) it is an event queue overflow
248 # 2) the wd is for the worktree
249 # 3) the wd is for the git dir and
250 # a) the event is for a file, and
251 # b) the file name does not end with ".lock"
252 if mask
& inotify
.IN_Q_OVERFLOW
:
254 if mask
& self
._TRIGGER
_MASK
:
255 if wd
in self
._worktree
_wds
:
257 if (wd
in self
._git
_dir
_wds
258 and not mask
& inotify
.IN_ISDIR
259 and not core
.decode(name
).endswith('.lock')):
263 def _handle_events(self
):
264 for wd
, mask
, cookie
, name
in \
265 inotify
.read_events(self
._inotify
_fd
):
266 if self
._filter
_event
(wd
, mask
, name
):
270 self
._running
= False
272 if self
._pipe
_w
is not None:
273 os
.write(self
._pipe
_w
, bchr(0))
277 if AVAILABLE
== 'pywin32':
278 class _Win32Watch(object):
279 def __init__(self
, path
, flags
):
286 self
.handle
= win32file
.CreateFileW(
288 0x0001, # FILE_LIST_DIRECTORY
289 win32con
.FILE_SHARE_READ | win32con
.FILE_SHARE_WRITE
,
291 win32con
.OPEN_EXISTING
,
292 win32con
.FILE_FLAG_BACKUP_SEMANTICS |
293 win32con
.FILE_FLAG_OVERLAPPED
,
296 self
.buffer = win32file
.AllocateReadBuffer(8192)
297 self
.event
= win32event
.CreateEvent(None, True, False, None)
298 self
.overlapped
= pywintypes
.OVERLAPPED()
299 self
.overlapped
.hEvent
= self
.event
306 win32file
.ReadDirectoryChangesW(self
.handle
, self
.buffer, True,
307 self
.flags
, self
.overlapped
)
310 if win32event
.WaitForSingleObject(self
.event
, 0) \
311 == win32event
.WAIT_TIMEOUT
:
314 nbytes
= win32file
.GetOverlappedResult(self
.handle
,
315 self
.overlapped
, False)
316 result
= win32file
.FILE_NOTIFY_INFORMATION(self
.buffer, nbytes
)
321 if self
.handle
is not None:
322 win32file
.CancelIo(self
.handle
)
323 win32file
.CloseHandle(self
.handle
)
324 if self
.event
is not None:
325 win32file
.CloseHandle(self
.event
)
328 class _Win32Thread(_BaseThread
):
329 _FLAGS
= (win32con
.FILE_NOTIFY_CHANGE_FILE_NAME |
330 win32con
.FILE_NOTIFY_CHANGE_DIR_NAME |
331 win32con
.FILE_NOTIFY_CHANGE_ATTRIBUTES |
332 win32con
.FILE_NOTIFY_CHANGE_SIZE |
333 win32con
.FILE_NOTIFY_CHANGE_LAST_WRITE |
334 win32con
.FILE_NOTIFY_CHANGE_SECURITY
)
336 def __init__(self
, monitor
):
337 _BaseThread
.__init
__(self
, monitor
)
338 self
._worktree
= self
._transform
_path
(core
.abspath(git
.worktree()))
339 self
._worktree
_watch
= None
340 self
._git
_dir
= self
._transform
_path
(core
.abspath(git
.git_dir()))
341 self
._git
_dir
_watch
= None
342 self
._stop
_event
_lock
= Lock()
343 self
._stop
_event
= None
346 def _transform_path(path
):
347 return path
.replace('\\', '/').lower()
349 def _read_watch(self
, watch
):
350 if win32event
.WaitForSingleObject(watch
.event
, 0) \
351 == win32event
.WAIT_TIMEOUT
:
354 nbytes
= win32file
.GetOverlappedResult(watch
.handle
,
355 watch
.overlapped
, False)
356 return win32file
.FILE_NOTIFY_INFORMATION(watch
.buffer, nbytes
)
360 with self
._stop
_event
_lock
:
361 self
._stop
_event
= win32event
.CreateEvent(None, True,
364 self
._worktree
_watch
= _Win32Watch(self
._worktree
, self
._FLAGS
)
365 self
._git
_dir
_watch
= _Win32Watch(self
._git
_dir
, self
._FLAGS
)
367 self
._log
_enabled
_message
()
369 events
= [self
._worktree
_watch
.event
,
370 self
._git
_dir
_watch
.event
,
374 timeout
= self
._NOTIFICATION
_DELAY
376 timeout
= win32event
.INFINITE
377 rc
= win32event
.WaitForMultipleObjects(events
, False,
379 if not self
._running
:
381 elif rc
== win32event
.WAIT_TIMEOUT
:
384 self
._handle
_results
()
386 with self
._stop
_event
_lock
:
387 if self
._stop
_event
is not None:
388 win32file
.CloseHandle(self
._stop
_event
)
389 self
._stop
_event
= None
390 if self
._worktree
_watch
is not None:
391 self
._worktree
_watch
.close()
392 if self
._git
_dir
_watch
is not None:
393 self
._git
_dir
_watch
.close()
395 def _handle_results(self
):
396 for action
, path
in self
._worktree
_watch
.read():
397 if not self
._running
:
399 path
= self
._worktree
+ '/' + self
._transform
_path
(path
)
400 if (path
!= self
._git
_dir
401 and not path
.startswith(self
._git
_dir
+ '/')):
403 for action
, path
in self
._git
_dir
_watch
.read():
404 if not self
._running
:
406 if not path
.endswith('.lock'):
410 self
._running
= False
411 with self
._stop
_event
_lock
:
412 if self
._stop
_event
is not None:
413 win32event
.SetEvent(self
._stop
_event
)
421 if _instance
is None:
422 _instance
= _create_instance()
426 def _create_instance():
428 cfg
= gitcfg
.current()
429 if not cfg
.get('cola.inotify', True):
430 msg
= N_('File system change monitoring: disabled because'
431 ' "cola.inotify" is false.\n')
433 elif AVAILABLE
== 'inotify':
434 thread_class
= _InotifyThread
435 elif AVAILABLE
== 'pywin32':
436 thread_class
= _Win32Thread
439 msg
= N_('File system change monitoring: disabled because pywin32'
440 ' is not installed.\n')
442 elif utils
.is_linux():
443 msg
= N_('File system change monitoring: disabled because libc'
444 ' does not support the inotify system calls.\n')
446 return _Monitor(thread_class
)