1 # Copyright (C) 2008-2017 David Aguilar
2 # Copyright (C) 2015 Daniel Harding
3 """Filesystem monitor for Linux and Windows
5 Linux monitoring uses using inotify.
6 Windows monitoring uses pywin32 and the ReadDirectoryChanges function.
9 from __future__
import division
, absolute_import
, unicode_literals
14 from threading
import Lock
16 from qtpy
import QtCore
17 from qtpy
.QtCore
import Signal
23 from .compat
import bchr
25 from .interaction
import Interaction
39 elif utils
.is_linux():
48 class _Monitor(QtCore
.QObject
):
50 files_changed
= Signal()
51 config_changed
= Signal()
53 def __init__(self
, context
, thread_class
):
54 QtCore
.QObject
.__init
__(self
)
55 self
.context
= context
56 self
._thread
_class
= thread_class
60 if self
._thread
_class
is not None:
61 assert self
._thread
is None
62 self
._thread
= self
._thread
_class
(self
.context
, self
)
66 if self
._thread
_class
is not None:
67 assert self
._thread
is not None
73 if self
._thread
is not None:
74 self
._thread
.refresh()
77 class _BaseThread(QtCore
.QThread
):
78 #: The delay, in milliseconds, between detecting file system modification
79 #: and triggering the 'files_changed' signal, to coalesce multiple
80 #: modifications into a single signal.
81 _NOTIFICATION_DELAY
= 888
83 def __init__(self
, context
, monitor
):
84 QtCore
.QThread
.__init
__(self
)
85 self
.context
= context
86 self
._monitor
= monitor
88 self
._use
_check
_ignore
= version
.check_git(context
, 'check-ignore')
89 self
._force
_notify
= False
90 self
._force
_config
= False
91 self
._file
_paths
= set()
95 return self
._force
_notify
or self
._file
_paths
or self
._force
_config
98 """Do any housekeeping necessary in response to repository changes."""
102 """Notifies all observers"""
105 if self
._force
_config
:
107 if self
._force
_notify
:
109 elif self
._file
_paths
:
110 proc
= core
.start_command(['git', 'check-ignore', '--verbose',
111 '--non-matching', '-z', '--stdin'])
112 path_list
= bchr(0).join(core
.encode(path
)
113 for path
in self
._file
_paths
)
114 out
, _
= proc
.communicate(path_list
)
118 # Each output record is four fields separated by NULL
119 # characters (records are also separated by NULL characters):
120 # <source> <NULL> <linenum> <NULL> <pattern> <NULL> <pathname>
121 # For paths which are not ignored, all fields will be empty
122 # except for <pathname>. So to see if we have any non-ignored
123 # files, we simply check every fourth field to see if any of
125 source_fields
= out
.split(bchr(0))[0:-1:4]
126 do_notify
= not all(source_fields
)
127 self
._force
_notify
= False
128 self
._force
_config
= False
129 self
._file
_paths
= set()
131 # "files changed" is a bigger hammer than "config changed".
132 # and is a superset relative to what is done in response to the
133 # signal. Thus, the "elif" below avoids repeated work that
134 # would be done if it were a simple "if" check instead.
136 self
._monitor
.files_changed
.emit()
138 self
._monitor
.config_changed
.emit()
141 def _log_enabled_message():
142 msg
= N_('File system change monitoring: enabled.\n')
146 if AVAILABLE
== 'inotify':
148 class _InotifyThread(_BaseThread
):
151 inotify
.IN_CLOSE_WRITE |
155 inotify
.IN_MOVED_FROM |
160 inotify
.IN_EXCL_UNLINK |
164 def __init__(self
, context
, monitor
):
165 _BaseThread
.__init
__(self
, context
, monitor
)
167 worktree
= git
.worktree()
168 if worktree
is not None:
169 worktree
= core
.abspath(worktree
)
170 self
._worktree
= worktree
171 self
._git
_dir
= git
.git_path()
173 self
._inotify
_fd
= None
176 self
._worktree
_wd
_to
_path
_map
= {}
177 self
._worktree
_path
_to
_wd
_map
= {}
178 self
._git
_dir
_wd
_to
_path
_map
= {}
179 self
._git
_dir
_path
_to
_wd
_map
= {}
180 self
._git
_dir
_wd
= None
183 def _log_out_of_wds_message():
184 msg
= N_('File system change monitoring: disabled because the'
185 ' limit on the total number of inotify watches was'
186 ' reached. You may be able to increase the limit on'
187 ' the number of watches by running:\n'
189 ' echo fs.inotify.max_user_watches=100000 |'
190 ' sudo tee -a /etc/sysctl.conf &&'
197 self
._inotify
_fd
= inotify
.init()
198 self
._pipe
_r
, self
._pipe
_w
= os
.pipe()
200 poll_obj
= select
.poll()
201 poll_obj
.register(self
._inotify
_fd
, select
.POLLIN
)
202 poll_obj
.register(self
._pipe
_r
, select
.POLLIN
)
206 self
._log
_enabled
_message
()
210 timeout
= self
._NOTIFICATION
_DELAY
214 events
= poll_obj
.poll(timeout
)
216 if e
.errno
== errno
.EINTR
:
223 if not self
._running
:
228 for (fd
, _
) in events
:
229 if fd
== self
._inotify
_fd
:
230 self
._handle
_events
()
233 if self
._inotify
_fd
is not None:
234 os
.close(self
._inotify
_fd
)
235 self
._inotify
_fd
= None
236 if self
._pipe
_r
is not None:
237 os
.close(self
._pipe
_r
)
239 os
.close(self
._pipe
_w
)
247 if self
._inotify
_fd
is None:
249 context
= self
.context
251 if self
._worktree
is not None:
253 os
.path
.dirname(os
.path
.join(self
._worktree
, path
))
254 for path
in gitcmds
.tracked_files(context
)])
255 self
._refresh
_watches
(tracked_dirs
,
256 self
._worktree
_wd
_to
_path
_map
,
257 self
._worktree
_path
_to
_wd
_map
)
259 git_dirs
.add(self
._git
_dir
)
260 for dirpath
, _
, _
in core
.walk(
261 os
.path
.join(self
._git
_dir
, 'refs')):
262 git_dirs
.add(dirpath
)
263 self
._refresh
_watches
(git_dirs
,
264 self
._git
_dir
_wd
_to
_path
_map
,
265 self
._git
_dir
_path
_to
_wd
_map
)
267 self
._git
_dir
_path
_to
_wd
_map
.get(self
._git
_dir
)
269 if e
.errno
== errno
.ENOSPC
:
270 self
._log
_out
_of
_wds
_message
()
271 self
._running
= False
275 def _refresh_watches(self
, paths_to_watch
, wd_to_path_map
,
277 watched_paths
= set(path_to_wd_map
)
278 for path
in watched_paths
- paths_to_watch
:
279 wd
= path_to_wd_map
.pop(path
)
280 wd_to_path_map
.pop(wd
)
282 inotify
.rm_watch(self
._inotify
_fd
, wd
)
284 if e
.errno
== errno
.EINVAL
:
285 # This error can occur if the target of the wd was
286 # removed on the filesystem before we call
287 # inotify.rm_watch() so ignore it.
291 for path
in paths_to_watch
- watched_paths
:
293 wd
= inotify
.add_watch(self
._inotify
_fd
, core
.encode(path
),
296 if e
.errno
in (errno
.ENOENT
, errno
.ENOTDIR
):
297 # These two errors should only occur as a result of
298 # race conditions: the first if the directory
299 # referenced by path was removed or renamed before the
300 # call to inotify.add_watch(); the second if the
301 # directory referenced by path was replaced with a file
302 # before the call to inotify.add_watch(). Therefore we
303 # simply ignore them.
308 wd_to_path_map
[wd
] = path
309 path_to_wd_map
[path
] = wd
311 def _check_event(self
, wd
, mask
, name
):
312 if mask
& inotify
.IN_Q_OVERFLOW
:
313 self
._force
_notify
= True
314 elif not mask
& self
._TRIGGER
_MASK
:
316 elif mask
& inotify
.IN_ISDIR
:
318 elif wd
in self
._worktree
_wd
_to
_path
_map
:
319 if self
._use
_check
_ignore
and name
:
320 path
= os
.path
.join(self
._worktree
_wd
_to
_path
_map
[wd
],
322 self
._file
_paths
.add(path
)
324 self
._force
_notify
= True
325 elif wd
== self
._git
_dir
_wd
:
326 name
= core
.decode(name
)
327 if name
== 'HEAD' or name
== 'index':
328 self
._force
_notify
= True
329 elif name
== 'config':
330 self
._force
_config
= True
331 elif (wd
in self
._git
_dir
_wd
_to
_path
_map
332 and not core
.decode(name
).endswith('.lock')):
333 self
._force
_notify
= True
335 def _handle_events(self
):
336 for wd
, mask
, _
, name
in \
337 inotify
.read_events(self
._inotify
_fd
):
338 if not self
._force
_notify
:
339 self
._check
_event
(wd
, mask
, name
)
342 self
._running
= False
344 if self
._pipe
_w
is not None:
345 os
.write(self
._pipe
_w
, bchr(0))
349 if AVAILABLE
== 'pywin32':
351 class _Win32Watch(object):
353 def __init__(self
, path
, flags
):
360 self
.handle
= win32file
.CreateFileW(
362 0x0001, # FILE_LIST_DIRECTORY
363 win32con
.FILE_SHARE_READ | win32con
.FILE_SHARE_WRITE
,
365 win32con
.OPEN_EXISTING
,
366 win32con
.FILE_FLAG_BACKUP_SEMANTICS |
367 win32con
.FILE_FLAG_OVERLAPPED
,
370 self
.buffer = win32file
.AllocateReadBuffer(8192)
371 self
.event
= win32event
.CreateEvent(None, True, False, None)
372 self
.overlapped
= pywintypes
.OVERLAPPED()
373 self
.overlapped
.hEvent
= self
.event
380 win32file
.ReadDirectoryChangesW(self
.handle
, self
.buffer, True,
381 self
.flags
, self
.overlapped
)
384 if win32event
.WaitForSingleObject(self
.event
, 0) \
385 == win32event
.WAIT_TIMEOUT
:
388 nbytes
= win32file
.GetOverlappedResult(self
.handle
,
389 self
.overlapped
, False)
390 result
= win32file
.FILE_NOTIFY_INFORMATION(self
.buffer, nbytes
)
395 if self
.handle
is not None:
396 win32file
.CancelIo(self
.handle
)
397 win32file
.CloseHandle(self
.handle
)
398 if self
.event
is not None:
399 win32file
.CloseHandle(self
.event
)
401 class _Win32Thread(_BaseThread
):
402 _FLAGS
= (win32con
.FILE_NOTIFY_CHANGE_FILE_NAME |
403 win32con
.FILE_NOTIFY_CHANGE_DIR_NAME |
404 win32con
.FILE_NOTIFY_CHANGE_ATTRIBUTES |
405 win32con
.FILE_NOTIFY_CHANGE_SIZE |
406 win32con
.FILE_NOTIFY_CHANGE_LAST_WRITE |
407 win32con
.FILE_NOTIFY_CHANGE_SECURITY
)
409 def __init__(self
, context
, monitor
):
410 _BaseThread
.__init
__(self
, context
, monitor
)
412 worktree
= git
.worktree()
413 if worktree
is not None:
414 worktree
= self
._transform
_path
(core
.abspath(worktree
))
415 self
._worktree
= worktree
416 self
._worktree
_watch
= None
417 self
._git
_dir
= self
._transform
_path
(core
.abspath(git
.git_path()))
418 self
._git
_dir
_watch
= None
419 self
._stop
_event
_lock
= Lock()
420 self
._stop
_event
= None
423 def _transform_path(path
):
424 return path
.replace('\\', '/').lower()
426 def _read_watch(self
, watch
):
427 if win32event
.WaitForSingleObject(watch
.event
, 0) \
428 == win32event
.WAIT_TIMEOUT
:
431 nbytes
= win32file
.GetOverlappedResult(watch
.handle
,
432 watch
.overlapped
, False)
433 return win32file
.FILE_NOTIFY_INFORMATION(watch
.buffer, nbytes
)
437 with self
._stop
_event
_lock
:
438 self
._stop
_event
= win32event
.CreateEvent(None, True,
441 events
= [self
._stop
_event
]
443 if self
._worktree
is not None:
444 self
._worktree
_watch
= _Win32Watch(self
._worktree
,
446 events
.append(self
._worktree
_watch
.event
)
448 self
._git
_dir
_watch
= _Win32Watch(self
._git
_dir
, self
._FLAGS
)
449 events
.append(self
._git
_dir
_watch
.event
)
451 self
._log
_enabled
_message
()
455 timeout
= self
._NOTIFICATION
_DELAY
457 timeout
= win32event
.INFINITE
458 rc
= win32event
.WaitForMultipleObjects(events
, False,
460 if not self
._running
:
462 elif rc
== win32event
.WAIT_TIMEOUT
:
465 self
._handle
_results
()
467 with self
._stop
_event
_lock
:
468 if self
._stop
_event
is not None:
469 win32file
.CloseHandle(self
._stop
_event
)
470 self
._stop
_event
= None
471 if self
._worktree
_watch
is not None:
472 self
._worktree
_watch
.close()
473 if self
._git
_dir
_watch
is not None:
474 self
._git
_dir
_watch
.close()
476 def _handle_results(self
):
477 if self
._worktree
_watch
is not None:
478 for _
, path
in self
._worktree
_watch
.read():
479 if not self
._running
:
481 if self
._force
_notify
:
483 path
= self
._worktree
+ '/' + self
._transform
_path
(path
)
484 if (path
!= self
._git
_dir
485 and not path
.startswith(self
._git
_dir
+ '/')
486 and not os
.path
.isdir(path
)):
487 if self
._use
_check
_ignore
:
488 self
._file
_paths
.add(path
)
490 self
._force
_notify
= True
491 for _
, path
in self
._git
_dir
_watch
.read():
492 if not self
._running
:
494 if self
._force
_notify
:
496 path
= self
._transform
_path
(path
)
497 if path
.endswith('.lock'):
500 self
._force
_config
= True
504 or path
.startswith('refs/')):
505 self
._force
_notify
= True
508 self
._running
= False
509 with self
._stop
_event
_lock
:
510 if self
._stop
_event
is not None:
511 win32event
.SetEvent(self
._stop
_event
)
518 if not cfg
.get('cola.inotify', default
=True):
519 msg
= N_('File system change monitoring: disabled because'
520 ' "cola.inotify" is false.\n')
522 elif AVAILABLE
== 'inotify':
523 thread_class
= _InotifyThread
524 elif AVAILABLE
== 'pywin32':
525 thread_class
= _Win32Thread
528 msg
= N_('File system change monitoring: disabled because pywin32'
529 ' is not installed.\n')
531 elif utils
.is_linux():
532 msg
= N_('File system change monitoring: disabled because libc'
533 ' does not support the inotify system calls.\n')
535 return _Monitor(context
, thread_class
)