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
14 from cola
.decorators
import memoize
28 elif utils
.is_linux():
30 from cola
import inotify
36 from PyQt4
import QtCore
37 from PyQt4
.QtCore
import SIGNAL
40 from cola
import gitcfg
41 from cola
import gitcmds
42 from cola
.compat
import bchr
43 from cola
.git
import git
44 from cola
.i18n
import N_
45 from cola
.interaction
import Interaction
48 class _Monitor(QtCore
.QObject
):
49 def __init__(self
, thread_class
):
50 QtCore
.QObject
.__init
__(self
)
51 self
._thread
_class
= thread_class
54 def start(self
, refs_only
=False):
55 if self
._thread
_class
is not None:
56 assert self
._thread
is None
57 self
._thread
= self
._thread
_class
(self
, refs_only
)
61 if self
._thread
_class
is not None:
62 assert self
._thread
is not None
68 if self
._thread
is not None:
69 self
._thread
.refresh()
72 class _BaseThread(QtCore
.QThread
):
73 #: The delay, in milliseconds, between detecting file system modification
74 #: and triggering the 'files_changed' signal, to coalesce multiple
75 #: modifications into a single signal.
76 _NOTIFICATION_DELAY
= 888
78 def __init__(self
, monitor
, refs_only
):
79 QtCore
.QThread
.__init
__(self
)
80 self
._monitor
= monitor
81 self
._refs
_only
= refs_only
86 """Do any housekeeping necessary in response to repository changes."""
90 """Notifies all observers"""
92 self
._monitor
.emit(SIGNAL('files_changed'))
95 def _log_enabled_message():
96 msg
= N_('File system change monitoring: enabled.\n')
97 Interaction
.safe_log(msg
)
100 if AVAILABLE
== 'inotify':
101 class _InotifyThread(_BaseThread
):
104 inotify
.IN_CLOSE_WRITE |
108 inotify
.IN_MOVED_FROM |
113 inotify
.IN_EXCL_UNLINK |
117 def __init__(self
, monitor
, refs_only
):
118 _BaseThread
.__init
__(self
, monitor
, refs_only
)
122 worktree
= git
.worktree()
123 if worktree
is not None:
124 worktree
= core
.abspath(worktree
)
125 self
._worktree
= worktree
126 self
._git
_dir
= git
.git_path()
128 self
._inotify
_fd
= None
131 self
._worktree
_wds
= set()
132 self
._worktree
_wd
_map
= {}
133 self
._git
_dir
_wds
= set()
134 self
._git
_dir
_wd
_map
= {}
135 self
._git
_dir
_wd
= None
138 def _log_out_of_wds_message():
139 msg
= N_('File system change monitoring: disabled because the'
140 ' limit on the total number of inotify watches was'
141 ' reached. You may be able to increase the limit on'
142 ' the number of watches by running:\n'
144 ' echo fs.inotify.max_user_watches=100000 |'
145 ' sudo tee -a /etc/sysctl.conf &&'
147 Interaction
.safe_log(msg
)
152 self
._inotify
_fd
= inotify
.init()
153 self
._pipe
_r
, self
._pipe
_w
= os
.pipe()
155 poll_obj
= select
.poll()
156 poll_obj
.register(self
._inotify
_fd
, select
.POLLIN
)
157 poll_obj
.register(self
._pipe
_r
, select
.POLLIN
)
161 self
._log
_enabled
_message
()
165 timeout
= self
._NOTIFICATION
_DELAY
169 events
= poll_obj
.poll(timeout
)
171 if e
.errno
== errno
.EINTR
:
176 if not self
._running
:
181 for fd
, event
in events
:
182 if fd
== self
._inotify
_fd
:
183 self
._handle
_events
()
186 if self
._inotify
_fd
is not None:
187 os
.close(self
._inotify
_fd
)
188 self
._inotify
_fd
= None
189 if self
._pipe
_r
is not None:
190 os
.close(self
._pipe
_r
)
192 os
.close(self
._pipe
_w
)
197 if self
._inotify
_fd
is None:
200 if self
._worktree
is not None:
202 os
.path
.dirname(os
.path
.join(self
._worktree
,
204 for path
in gitcmds
.tracked_files())
205 self
._refresh
_watches
(tracked_dirs
, self
._worktree
_wds
,
206 self
._worktree
_wd
_map
)
208 git_dirs
.add(self
._git
_dir
)
209 for dirpath
, dirnames
, filenames
in core
.walk(
210 os
.path
.join(self
._git
_dir
, 'refs')):
211 git_dirs
.add(dirpath
)
212 self
._refresh
_watches
(git_dirs
, self
._git
_dir
_wds
,
213 self
._git
_dir
_wd
_map
)
214 self
._git
_dir
_wd
= self
._git
_dir
_wd
_map
[self
._git
_dir
]
216 if e
.errno
== errno
.ENOSPC
:
217 self
._log
_out
_of
_wds
_message
()
218 self
._running
= False
222 def _refresh_watches(self
, paths_to_watch
, wd_set
, wd_map
):
223 watched_paths
= set(wd_map
)
224 for path
in watched_paths
- paths_to_watch
:
225 wd
= wd_map
.pop(path
)
228 inotify
.rm_watch(self
._inotify
_fd
, wd
)
230 if e
.errno
== errno
.EINVAL
:
231 # This error can occur if the target of the wd was
232 # removed on the filesystem before we call
233 # inotify.rm_watch() so ignore it.
237 for path
in paths_to_watch
- watched_paths
:
239 wd
= inotify
.add_watch(self
._inotify
_fd
, core
.encode(path
),
242 if e
.errno
in (errno
.ENOENT
, errno
.ENOTDIR
):
243 # These two errors should only occur as a result of
244 # race conditions: the first if the directory
245 # referenced by path was removed or renamed before the
246 # call to inotify.add_watch(); the second if the
247 # directory referenced by path was replaced with a file
248 # before the call to inotify.add_watch(). Therefore we
249 # simply ignore them.
257 def _event_is_relevant(self
, wd
, mask
, name
):
258 if mask
& inotify
.IN_Q_OVERFLOW
:
260 elif not mask
& self
._TRIGGER
_MASK
:
262 elif wd
in self
._worktree
_wds
:
264 elif mask
& inotify
.IN_ISDIR
:
266 elif wd
== self
._git
_dir
_wd
:
267 name
= core
.decode(name
)
270 elif not self
._refs
_only
and name
== 'index':
272 elif (wd
in self
._git
_dir
_wds
and
273 not core
.decode(name
).endswith('.lock')):
278 def _handle_events(self
):
279 for wd
, mask
, cookie
, name
in \
280 inotify
.read_events(self
._inotify
_fd
):
281 if self
._event
_is
_relevant
(wd
, mask
, name
):
285 self
._running
= False
287 if self
._pipe
_w
is not None:
288 os
.write(self
._pipe
_w
, bchr(0))
292 if AVAILABLE
== 'pywin32':
294 class _Win32Watch(object):
296 def __init__(self
, path
, flags
):
303 self
.handle
= win32file
.CreateFileW(
305 0x0001, # FILE_LIST_DIRECTORY
306 win32con
.FILE_SHARE_READ | win32con
.FILE_SHARE_WRITE
,
308 win32con
.OPEN_EXISTING
,
309 win32con
.FILE_FLAG_BACKUP_SEMANTICS |
310 win32con
.FILE_FLAG_OVERLAPPED
,
313 self
.buffer = win32file
.AllocateReadBuffer(8192)
314 self
.event
= win32event
.CreateEvent(None, True, False, None)
315 self
.overlapped
= pywintypes
.OVERLAPPED()
316 self
.overlapped
.hEvent
= self
.event
323 win32file
.ReadDirectoryChangesW(self
.handle
, self
.buffer, True,
324 self
.flags
, self
.overlapped
)
327 if win32event
.WaitForSingleObject(self
.event
, 0) \
328 == win32event
.WAIT_TIMEOUT
:
331 nbytes
= win32file
.GetOverlappedResult(self
.handle
,
332 self
.overlapped
, False)
333 result
= win32file
.FILE_NOTIFY_INFORMATION(self
.buffer, nbytes
)
338 if self
.handle
is not None:
339 win32file
.CancelIo(self
.handle
)
340 win32file
.CloseHandle(self
.handle
)
341 if self
.event
is not None:
342 win32file
.CloseHandle(self
.event
)
344 class _Win32Thread(_BaseThread
):
345 _FLAGS
= (win32con
.FILE_NOTIFY_CHANGE_FILE_NAME |
346 win32con
.FILE_NOTIFY_CHANGE_DIR_NAME |
347 win32con
.FILE_NOTIFY_CHANGE_ATTRIBUTES |
348 win32con
.FILE_NOTIFY_CHANGE_SIZE |
349 win32con
.FILE_NOTIFY_CHANGE_LAST_WRITE |
350 win32con
.FILE_NOTIFY_CHANGE_SECURITY
)
352 def __init__(self
, monitor
, refs_only
):
353 _BaseThread
.__init
__(self
, monitor
, refs_only
)
357 worktree
= git
.worktree()
358 if worktree
is not None:
359 worktree
= self
._transform
_path
(core
.abspath(worktree
))
360 self
._worktree
= worktree
361 self
._worktree
_watch
= None
362 self
._git
_dir
= self
._transform
_path
(core
.abspath(git
.git_path()))
363 self
._git
_dir
_watch
= None
364 self
._stop
_event
_lock
= Lock()
365 self
._stop
_event
= None
368 def _transform_path(path
):
369 return path
.replace('\\', '/').lower()
371 def _read_watch(self
, watch
):
372 if win32event
.WaitForSingleObject(watch
.event
, 0) \
373 == win32event
.WAIT_TIMEOUT
:
376 nbytes
= win32file
.GetOverlappedResult(watch
.handle
,
377 watch
.overlapped
, False)
378 return win32file
.FILE_NOTIFY_INFORMATION(watch
.buffer, nbytes
)
382 with self
._stop
_event
_lock
:
383 self
._stop
_event
= win32event
.CreateEvent(None, True,
386 events
= [self
._stop
_event
]
388 if self
._worktree
is not None:
389 self
._worktree
_watch
= _Win32Watch(self
._worktree
,
391 events
.append(self
._worktree
_watch
.event
)
393 self
._git
_dir
_watch
= _Win32Watch(self
._git
_dir
, self
._FLAGS
)
394 events
.append(self
._git
_dir
_watch
.event
)
396 self
._log
_enabled
_message
()
400 timeout
= self
._NOTIFICATION
_DELAY
402 timeout
= win32event
.INFINITE
403 rc
= win32event
.WaitForMultipleObjects(events
, False,
405 if not self
._running
:
407 elif rc
== win32event
.WAIT_TIMEOUT
:
410 self
._handle
_results
()
412 with self
._stop
_event
_lock
:
413 if self
._stop
_event
is not None:
414 win32file
.CloseHandle(self
._stop
_event
)
415 self
._stop
_event
= None
416 if self
._worktree
_watch
is not None:
417 self
._worktree
_watch
.close()
418 if self
._git
_dir
_watch
is not None:
419 self
._git
_dir
_watch
.close()
421 def _handle_results(self
):
422 if self
._worktree
_watch
is not None:
423 for action
, path
in self
._worktree
_watch
.read():
424 if not self
._running
:
426 path
= self
._worktree
+ '/' + self
._transform
_path
(path
)
427 if (path
!= self
._git
_dir
and
428 not path
.startswith(self
._git
_dir
+ '/')):
430 for action
, path
in self
._git
_dir
_watch
.read():
431 if not self
._running
:
433 path
= self
._transform
_path
(path
)
434 if path
.endswith('.lock'):
436 if path
== 'head' or path
.startswith('refs/'):
438 elif not self
._refs
_only
and path
== 'index':
442 self
._running
= False
443 with self
._stop
_event
_lock
:
444 if self
._stop
_event
is not None:
445 win32event
.SetEvent(self
._stop
_event
)
451 return _create_instance()
454 def _create_instance():
456 cfg
= gitcfg
.current()
457 if not cfg
.get('cola.inotify', True):
458 msg
= N_('File system change monitoring: disabled because'
459 ' "cola.inotify" is false.\n')
461 elif AVAILABLE
== 'inotify':
462 thread_class
= _InotifyThread
463 elif AVAILABLE
== 'pywin32':
464 thread_class
= _Win32Thread
467 msg
= N_('File system change monitoring: disabled because pywin32'
468 ' is not installed.\n')
470 elif utils
.is_linux():
471 msg
= N_('File system change monitoring: disabled because libc'
472 ' does not support the inotify system calls.\n')
474 return _Monitor(thread_class
)