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
15 from .decorators
import memoize
29 elif utils
.is_linux():
37 from qtpy
import QtCore
38 from qtpy
.QtCore
import Signal
43 from .compat
import bchr
46 from .interaction
import Interaction
49 class _Monitor(QtCore
.QObject
):
51 files_changed
= Signal()
53 def __init__(self
, thread_class
):
54 QtCore
.QObject
.__init
__(self
)
55 self
._thread
_class
= thread_class
59 if self
._thread
_class
is not None:
60 assert self
._thread
is None
61 self
._thread
= self
._thread
_class
(self
)
65 if self
._thread
_class
is not None:
66 assert self
._thread
is not None
72 if self
._thread
is not None:
73 self
._thread
.refresh()
76 class _BaseThread(QtCore
.QThread
):
77 #: The delay, in milliseconds, between detecting file system modification
78 #: and triggering the 'files_changed' signal, to coalesce multiple
79 #: modifications into a single signal.
80 _NOTIFICATION_DELAY
= 888
82 def __init__(self
, monitor
):
83 QtCore
.QThread
.__init
__(self
)
84 self
._monitor
= monitor
86 self
._use
_check
_ignore
= version
.check('check-ignore',
87 version
.git_version())
88 self
._force
_notify
= False
89 self
._file
_paths
= set()
93 return self
._force
_notify
or self
._file
_paths
96 """Do any housekeeping necessary in response to repository changes."""
100 """Notifies all observers"""
102 if self
._force
_notify
:
104 elif self
._file
_paths
:
105 proc
= core
.start_command(['git', 'check-ignore', '--verbose',
106 '--non-matching', '-z', '--stdin'])
107 path_list
= bchr(0).join(core
.encode(path
)
108 for path
in self
._file
_paths
)
109 out
, err
= proc
.communicate(path_list
)
113 # Each output record is four fields separated by NULL
114 # characters (records are also separated by NULL characters):
115 # <source> <NULL> <linenum> <NULL> <pattern> <NULL> <pathname>
116 # For paths which are not ignored, all fields will be empty
117 # except for <pathname>. So to see if we have any non-ignored
118 # files, we simply check every fourth field to see if any of
120 source_fields
= out
.split(bchr(0))[0:-1:4]
121 do_notify
= not all(source_fields
)
122 self
._force
_notify
= False
123 self
._file
_paths
= set()
125 self
._monitor
.files_changed
.emit()
128 def _log_enabled_message():
129 msg
= N_('File system change monitoring: enabled.\n')
130 Interaction
.safe_log(msg
)
133 if AVAILABLE
== 'inotify':
135 class _InotifyThread(_BaseThread
):
138 inotify
.IN_CLOSE_WRITE |
142 inotify
.IN_MOVED_FROM |
147 inotify
.IN_EXCL_UNLINK |
151 def __init__(self
, monitor
):
152 _BaseThread
.__init
__(self
, monitor
)
153 worktree
= git
.worktree()
154 if worktree
is not None:
155 worktree
= core
.abspath(worktree
)
156 self
._worktree
= worktree
157 self
._git
_dir
= git
.git_path()
159 self
._inotify
_fd
= None
162 self
._worktree
_wd
_to
_path
_map
= {}
163 self
._worktree
_path
_to
_wd
_map
= {}
164 self
._git
_dir
_wd
_to
_path
_map
= {}
165 self
._git
_dir
_path
_to
_wd
_map
= {}
166 self
._git
_dir
_wd
= None
169 def _log_out_of_wds_message():
170 msg
= N_('File system change monitoring: disabled because the'
171 ' limit on the total number of inotify watches was'
172 ' reached. You may be able to increase the limit on'
173 ' the number of watches by running:\n'
175 ' echo fs.inotify.max_user_watches=100000 |'
176 ' sudo tee -a /etc/sysctl.conf &&'
178 Interaction
.safe_log(msg
)
183 self
._inotify
_fd
= inotify
.init()
184 self
._pipe
_r
, self
._pipe
_w
= os
.pipe()
186 poll_obj
= select
.poll()
187 poll_obj
.register(self
._inotify
_fd
, select
.POLLIN
)
188 poll_obj
.register(self
._pipe
_r
, select
.POLLIN
)
192 self
._log
_enabled
_message
()
196 timeout
= self
._NOTIFICATION
_DELAY
200 events
= poll_obj
.poll(timeout
)
202 if e
.errno
== errno
.EINTR
:
209 if not self
._running
:
214 for fd
, event
in events
:
215 if fd
== self
._inotify
_fd
:
216 self
._handle
_events
()
219 if self
._inotify
_fd
is not None:
220 os
.close(self
._inotify
_fd
)
221 self
._inotify
_fd
= None
222 if self
._pipe
_r
is not None:
223 os
.close(self
._pipe
_r
)
225 os
.close(self
._pipe
_w
)
230 if self
._inotify
_fd
is None:
233 if self
._worktree
is not None:
235 os
.path
.dirname(os
.path
.join(self
._worktree
,
237 for path
in gitcmds
.tracked_files())
238 self
._refresh
_watches
(tracked_dirs
,
239 self
._worktree
_wd
_to
_path
_map
,
240 self
._worktree
_path
_to
_wd
_map
)
242 git_dirs
.add(self
._git
_dir
)
243 for dirpath
, dirnames
, filenames
in core
.walk(
244 os
.path
.join(self
._git
_dir
, 'refs')):
245 git_dirs
.add(dirpath
)
246 self
._refresh
_watches
(git_dirs
,
247 self
._git
_dir
_wd
_to
_path
_map
,
248 self
._git
_dir
_path
_to
_wd
_map
)
250 self
._git
_dir
_path
_to
_wd
_map
[self
._git
_dir
]
252 if e
.errno
== errno
.ENOSPC
:
253 self
._log
_out
_of
_wds
_message
()
254 self
._running
= False
258 def _refresh_watches(self
, paths_to_watch
, wd_to_path_map
,
260 watched_paths
= set(path_to_wd_map
)
261 for path
in watched_paths
- paths_to_watch
:
262 wd
= path_to_wd_map
.pop(path
)
263 wd_to_path_map
.pop(wd
)
265 inotify
.rm_watch(self
._inotify
_fd
, wd
)
267 if e
.errno
== errno
.EINVAL
:
268 # This error can occur if the target of the wd was
269 # removed on the filesystem before we call
270 # inotify.rm_watch() so ignore it.
274 for path
in paths_to_watch
- watched_paths
:
276 wd
= inotify
.add_watch(self
._inotify
_fd
, core
.encode(path
),
279 if e
.errno
in (errno
.ENOENT
, errno
.ENOTDIR
):
280 # These two errors should only occur as a result of
281 # race conditions: the first if the directory
282 # referenced by path was removed or renamed before the
283 # call to inotify.add_watch(); the second if the
284 # directory referenced by path was replaced with a file
285 # before the call to inotify.add_watch(). Therefore we
286 # simply ignore them.
291 wd_to_path_map
[wd
] = path
292 path_to_wd_map
[path
] = wd
294 def _check_event(self
, wd
, mask
, name
):
295 if mask
& inotify
.IN_Q_OVERFLOW
:
296 self
._force
_notify
= True
297 elif not mask
& self
._TRIGGER
_MASK
:
299 elif mask
& inotify
.IN_ISDIR
:
301 elif wd
in self
._worktree
_wd
_to
_path
_map
:
302 if self
._use
_check
_ignore
:
303 self
._file
_paths
.add(
304 os
.path
.join(self
._worktree
_wd
_to
_path
_map
[wd
],
307 self
._force
_notify
= True
308 elif wd
== self
._git
_dir
_wd
:
309 name
= core
.decode(name
)
310 if name
== 'HEAD' or name
== 'index':
311 self
._force
_notify
= True
312 elif (wd
in self
._git
_dir
_wd
_to
_path
_map
313 and not core
.decode(name
).endswith('.lock')):
314 self
._force
_notify
= True
316 def _handle_events(self
):
317 for wd
, mask
, cookie
, name
in \
318 inotify
.read_events(self
._inotify
_fd
):
319 if not self
._force
_notify
:
320 self
._check
_event
(wd
, mask
, name
)
323 self
._running
= False
325 if self
._pipe
_w
is not None:
326 os
.write(self
._pipe
_w
, bchr(0))
330 if AVAILABLE
== 'pywin32':
332 class _Win32Watch(object):
334 def __init__(self
, path
, flags
):
341 self
.handle
= win32file
.CreateFileW(
343 0x0001, # FILE_LIST_DIRECTORY
344 win32con
.FILE_SHARE_READ | win32con
.FILE_SHARE_WRITE
,
346 win32con
.OPEN_EXISTING
,
347 win32con
.FILE_FLAG_BACKUP_SEMANTICS |
348 win32con
.FILE_FLAG_OVERLAPPED
,
351 self
.buffer = win32file
.AllocateReadBuffer(8192)
352 self
.event
= win32event
.CreateEvent(None, True, False, None)
353 self
.overlapped
= pywintypes
.OVERLAPPED()
354 self
.overlapped
.hEvent
= self
.event
361 win32file
.ReadDirectoryChangesW(self
.handle
, self
.buffer, True,
362 self
.flags
, self
.overlapped
)
365 if win32event
.WaitForSingleObject(self
.event
, 0) \
366 == win32event
.WAIT_TIMEOUT
:
369 nbytes
= win32file
.GetOverlappedResult(self
.handle
,
370 self
.overlapped
, False)
371 result
= win32file
.FILE_NOTIFY_INFORMATION(self
.buffer, nbytes
)
376 if self
.handle
is not None:
377 win32file
.CancelIo(self
.handle
)
378 win32file
.CloseHandle(self
.handle
)
379 if self
.event
is not None:
380 win32file
.CloseHandle(self
.event
)
382 class _Win32Thread(_BaseThread
):
383 _FLAGS
= (win32con
.FILE_NOTIFY_CHANGE_FILE_NAME |
384 win32con
.FILE_NOTIFY_CHANGE_DIR_NAME |
385 win32con
.FILE_NOTIFY_CHANGE_ATTRIBUTES |
386 win32con
.FILE_NOTIFY_CHANGE_SIZE |
387 win32con
.FILE_NOTIFY_CHANGE_LAST_WRITE |
388 win32con
.FILE_NOTIFY_CHANGE_SECURITY
)
390 def __init__(self
, monitor
):
391 _BaseThread
.__init
__(self
, monitor
)
392 worktree
= git
.worktree()
393 if worktree
is not None:
394 worktree
= self
._transform
_path
(core
.abspath(worktree
))
395 self
._worktree
= worktree
396 self
._worktree
_watch
= None
397 self
._git
_dir
= self
._transform
_path
(core
.abspath(git
.git_path()))
398 self
._git
_dir
_watch
= None
399 self
._stop
_event
_lock
= Lock()
400 self
._stop
_event
= None
403 def _transform_path(path
):
404 return path
.replace('\\', '/').lower()
406 def _read_watch(self
, watch
):
407 if win32event
.WaitForSingleObject(watch
.event
, 0) \
408 == win32event
.WAIT_TIMEOUT
:
411 nbytes
= win32file
.GetOverlappedResult(watch
.handle
,
412 watch
.overlapped
, False)
413 return win32file
.FILE_NOTIFY_INFORMATION(watch
.buffer, nbytes
)
417 with self
._stop
_event
_lock
:
418 self
._stop
_event
= win32event
.CreateEvent(None, True,
421 events
= [self
._stop
_event
]
423 if self
._worktree
is not None:
424 self
._worktree
_watch
= _Win32Watch(self
._worktree
,
426 events
.append(self
._worktree
_watch
.event
)
428 self
._git
_dir
_watch
= _Win32Watch(self
._git
_dir
, self
._FLAGS
)
429 events
.append(self
._git
_dir
_watch
.event
)
431 self
._log
_enabled
_message
()
435 timeout
= self
._NOTIFICATION
_DELAY
437 timeout
= win32event
.INFINITE
438 rc
= win32event
.WaitForMultipleObjects(events
, False,
440 if not self
._running
:
442 elif rc
== win32event
.WAIT_TIMEOUT
:
445 self
._handle
_results
()
447 with self
._stop
_event
_lock
:
448 if self
._stop
_event
is not None:
449 win32file
.CloseHandle(self
._stop
_event
)
450 self
._stop
_event
= None
451 if self
._worktree
_watch
is not None:
452 self
._worktree
_watch
.close()
453 if self
._git
_dir
_watch
is not None:
454 self
._git
_dir
_watch
.close()
456 def _handle_results(self
):
457 if self
._worktree
_watch
is not None:
458 for action
, path
in self
._worktree
_watch
.read():
459 if not self
._running
:
461 if self
._force
_notify
:
463 path
= self
._worktree
+ '/' + self
._transform
_path
(path
)
464 if (path
!= self
._git
_dir
465 and not path
.startswith(self
._git
_dir
+ '/')
466 and not os
.path
.isdir(path
)
468 if self
._use
_check
_ignore
:
469 self
._file
_paths
.add(path
)
471 self
._force
_notify
= True
472 for action
, path
in self
._git
_dir
_watch
.read():
473 if not self
._running
:
475 if self
._force
_notify
:
477 path
= self
._transform
_path
(path
)
478 if path
.endswith('.lock'):
482 or path
.startswith('refs/')
484 self
._force
_notify
= True
487 self
._running
= False
488 with self
._stop
_event
_lock
:
489 if self
._stop
_event
is not None:
490 win32event
.SetEvent(self
._stop
_event
)
496 return _create_instance()
499 def _create_instance():
501 cfg
= gitcfg
.current()
502 if not cfg
.get('cola.inotify', True):
503 msg
= N_('File system change monitoring: disabled because'
504 ' "cola.inotify" is false.\n')
506 elif AVAILABLE
== 'inotify':
507 thread_class
= _InotifyThread
508 elif AVAILABLE
== 'pywin32':
509 thread_class
= _Win32Thread
512 msg
= N_('File system change monitoring: disabled because pywin32'
513 ' is not installed.\n')
515 elif utils
.is_linux():
516 msg
= N_('File system change monitoring: disabled because libc'
517 ' does not support the inotify system calls.\n')
519 return _Monitor(thread_class
)