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
97 # pylint: disable=no-self-use
99 """Do any housekeeping necessary in response to repository changes."""
103 """Notifies all observers"""
106 if self
._force
_config
:
108 if self
._force
_notify
:
110 elif self
._file
_paths
:
111 proc
= core
.start_command(
112 ['git', 'check-ignore', '--verbose', '--non-matching', '-z', '--stdin']
114 path_list
= bchr(0).join(core
.encode(path
) for path
in self
._file
_paths
)
115 out
, _
= proc
.communicate(path_list
)
119 # Each output record is four fields separated by NULL
120 # characters (records are also separated by NULL characters):
121 # <source> <NULL> <linenum> <NULL> <pattern> <NULL> <pathname>
122 # For paths which are not ignored, all fields will be empty
123 # except for <pathname>. So to see if we have any non-ignored
124 # files, we simply check every fourth field to see if any of
126 source_fields
= out
.split(bchr(0))[0:-1:4]
127 do_notify
= not all(source_fields
)
128 self
._force
_notify
= False
129 self
._force
_config
= False
130 self
._file
_paths
= set()
132 # "files changed" is a bigger hammer than "config changed".
133 # and is a superset relative to what is done in response to the
134 # signal. Thus, the "elif" below avoids repeated work that
135 # would be done if it were a simple "if" check instead.
137 self
._monitor
.files_changed
.emit()
139 self
._monitor
.config_changed
.emit()
142 def _log_enabled_message():
143 msg
= N_('File system change monitoring: enabled.\n')
147 if AVAILABLE
== 'inotify':
149 class _InotifyThread(_BaseThread
):
152 | inotify
.IN_CLOSE_WRITE
156 | inotify
.IN_MOVED_FROM
157 | inotify
.IN_MOVED_TO
159 _ADD_MASK
= _TRIGGER_MASK | inotify
.IN_EXCL_UNLINK | inotify
.IN_ONLYDIR
161 def __init__(self
, context
, monitor
):
162 _BaseThread
.__init
__(self
, context
, monitor
)
164 worktree
= git
.worktree()
165 if worktree
is not None:
166 worktree
= core
.abspath(worktree
)
167 self
._worktree
= worktree
168 self
._git
_dir
= git
.git_path()
170 self
._inotify
_fd
= None
173 self
._worktree
_wd
_to
_path
_map
= {}
174 self
._worktree
_path
_to
_wd
_map
= {}
175 self
._git
_dir
_wd
_to
_path
_map
= {}
176 self
._git
_dir
_path
_to
_wd
_map
= {}
177 self
._git
_dir
_wd
= None
180 def _log_out_of_wds_message():
182 'File system change monitoring: disabled because the'
183 ' limit on the total number of inotify watches was'
184 ' reached. You may be able to increase the limit on'
185 ' the number of watches by running:\n'
187 ' echo fs.inotify.max_user_watches=100000 |'
188 ' sudo tee -a /etc/sysctl.conf &&'
197 self
._inotify
_fd
= inotify
.init()
199 self
._inotify
_fd
= None
200 self
._running
= False
201 if e
.errno
== errno
.EMFILE
:
202 self
._log
_out
_of
_wds
_message
()
204 self
._pipe
_r
, self
._pipe
_w
= os
.pipe()
206 # pylint: disable=no-member
207 poll_obj
= select
.poll()
208 poll_obj
.register(self
._inotify
_fd
, select
.POLLIN
)
209 poll_obj
.register(self
._pipe
_r
, select
.POLLIN
)
214 self
._log
_enabled
_message
()
215 self
._process
_events
(poll_obj
)
219 def _process_events(self
, poll_obj
):
222 timeout
= self
._NOTIFICATION
_DELAY
226 events
= poll_obj
.poll(timeout
)
227 # pylint: disable=duplicate-except
228 except (OSError, select
.error
):
231 if not self
._running
:
236 for (fd
, _
) in events
:
237 if fd
== self
._inotify
_fd
:
238 self
._handle
_events
()
240 def _close_fds(self
):
242 if self
._inotify
_fd
is not None:
243 os
.close(self
._inotify
_fd
)
244 self
._inotify
_fd
= None
245 if self
._pipe
_r
is not None:
246 os
.close(self
._pipe
_r
)
248 os
.close(self
._pipe
_w
)
256 if self
._inotify
_fd
is None:
258 context
= self
.context
260 if self
._worktree
is not None:
263 os
.path
.dirname(os
.path
.join(self
._worktree
, path
))
264 for path
in gitcmds
.tracked_files(context
)
267 self
._refresh
_watches
(
269 self
._worktree
_wd
_to
_path
_map
,
270 self
._worktree
_path
_to
_wd
_map
,
273 git_dirs
.add(self
._git
_dir
)
274 for dirpath
, _
, _
in core
.walk(os
.path
.join(self
._git
_dir
, 'refs')):
275 git_dirs
.add(dirpath
)
276 self
._refresh
_watches
(
277 git_dirs
, self
._git
_dir
_wd
_to
_path
_map
, self
._git
_dir
_path
_to
_wd
_map
279 self
._git
_dir
_wd
= self
._git
_dir
_path
_to
_wd
_map
.get(self
._git
_dir
)
281 if e
.errno
in (errno
.ENOSPC
, errno
.EMFILE
):
282 self
._log
_out
_of
_wds
_message
()
283 self
._running
= False
287 def _refresh_watches(self
, paths_to_watch
, wd_to_path_map
, path_to_wd_map
):
288 watched_paths
= set(path_to_wd_map
)
289 for path
in watched_paths
- paths_to_watch
:
290 wd
= path_to_wd_map
.pop(path
)
291 wd_to_path_map
.pop(wd
)
293 inotify
.rm_watch(self
._inotify
_fd
, wd
)
295 if e
.errno
== errno
.EINVAL
:
296 # This error can occur if the target of the wd was
297 # removed on the filesystem before we call
298 # inotify.rm_watch() so ignore it.
301 for path
in paths_to_watch
- watched_paths
:
303 wd
= inotify
.add_watch(
304 self
._inotify
_fd
, core
.encode(path
), self
._ADD
_MASK
307 if e
.errno
in (errno
.ENOENT
, errno
.ENOTDIR
):
308 # These two errors should only occur as a result of
309 # race conditions: the first if the directory
310 # referenced by path was removed or renamed before the
311 # call to inotify.add_watch(); the second if the
312 # directory referenced by path was replaced with a file
313 # before the call to inotify.add_watch(). Therefore we
314 # simply ignore them.
318 wd_to_path_map
[wd
] = path
319 path_to_wd_map
[path
] = wd
321 def _check_event(self
, wd
, mask
, name
):
322 if mask
& inotify
.IN_Q_OVERFLOW
:
323 self
._force
_notify
= True
324 elif not mask
& self
._TRIGGER
_MASK
:
326 elif mask
& inotify
.IN_ISDIR
:
328 elif wd
in self
._worktree
_wd
_to
_path
_map
:
329 if self
._use
_check
_ignore
and name
:
331 self
._worktree
_wd
_to
_path
_map
[wd
], core
.decode(name
)
333 self
._file
_paths
.add(path
)
335 self
._force
_notify
= True
336 elif wd
== self
._git
_dir
_wd
:
337 name
= core
.decode(name
)
338 if name
in ('HEAD', 'index'):
339 self
._force
_notify
= True
340 elif name
== 'config':
341 self
._force
_config
= True
342 elif wd
in self
._git
_dir
_wd
_to
_path
_map
and not core
.decode(name
).endswith(
345 self
._force
_notify
= True
347 def _handle_events(self
):
348 for wd
, mask
, _
, name
in inotify
.read_events(self
._inotify
_fd
):
349 if not self
._force
_notify
:
350 self
._check
_event
(wd
, mask
, name
)
353 self
._running
= False
355 if self
._pipe
_w
is not None:
356 os
.write(self
._pipe
_w
, bchr(0))
360 if AVAILABLE
== 'pywin32':
362 class _Win32Watch(object):
363 def __init__(self
, path
, flags
):
370 self
.handle
= win32file
.CreateFileW(
372 0x0001, # FILE_LIST_DIRECTORY
373 win32con
.FILE_SHARE_READ | win32con
.FILE_SHARE_WRITE
,
375 win32con
.OPEN_EXISTING
,
376 win32con
.FILE_FLAG_BACKUP_SEMANTICS | win32con
.FILE_FLAG_OVERLAPPED
,
380 self
.buffer = win32file
.AllocateReadBuffer(8192)
381 self
.event
= win32event
.CreateEvent(None, True, False, None)
382 self
.overlapped
= pywintypes
.OVERLAPPED()
383 self
.overlapped
.hEvent
= self
.event
390 win32file
.ReadDirectoryChangesW(
391 self
.handle
, self
.buffer, True, self
.flags
, self
.overlapped
395 if win32event
.WaitForSingleObject(self
.event
, 0) == win32event
.WAIT_TIMEOUT
:
398 nbytes
= win32file
.GetOverlappedResult(
399 self
.handle
, self
.overlapped
, False
401 result
= win32file
.FILE_NOTIFY_INFORMATION(self
.buffer, nbytes
)
406 if self
.handle
is not None:
407 win32file
.CancelIo(self
.handle
)
408 win32file
.CloseHandle(self
.handle
)
409 if self
.event
is not None:
410 win32file
.CloseHandle(self
.event
)
412 class _Win32Thread(_BaseThread
):
414 win32con
.FILE_NOTIFY_CHANGE_FILE_NAME
415 | win32con
.FILE_NOTIFY_CHANGE_DIR_NAME
416 | win32con
.FILE_NOTIFY_CHANGE_ATTRIBUTES
417 | win32con
.FILE_NOTIFY_CHANGE_SIZE
418 | win32con
.FILE_NOTIFY_CHANGE_LAST_WRITE
419 | win32con
.FILE_NOTIFY_CHANGE_SECURITY
422 def __init__(self
, context
, monitor
):
423 _BaseThread
.__init
__(self
, context
, monitor
)
425 worktree
= git
.worktree()
426 if worktree
is not None:
427 worktree
= self
._transform
_path
(core
.abspath(worktree
))
428 self
._worktree
= worktree
429 self
._worktree
_watch
= None
430 self
._git
_dir
= self
._transform
_path
(core
.abspath(git
.git_path()))
431 self
._git
_dir
_watch
= None
432 self
._stop
_event
_lock
= Lock()
433 self
._stop
_event
= None
436 def _transform_path(path
):
437 return path
.replace('\\', '/').lower()
441 with self
._stop
_event
_lock
:
442 self
._stop
_event
= win32event
.CreateEvent(None, True, False, None)
444 events
= [self
._stop
_event
]
446 if self
._worktree
is not None:
447 self
._worktree
_watch
= _Win32Watch(self
._worktree
, self
._FLAGS
)
448 events
.append(self
._worktree
_watch
.event
)
450 self
._git
_dir
_watch
= _Win32Watch(self
._git
_dir
, self
._FLAGS
)
451 events
.append(self
._git
_dir
_watch
.event
)
453 self
._log
_enabled
_message
()
457 timeout
= self
._NOTIFICATION
_DELAY
459 timeout
= win32event
.INFINITE
460 rc
= win32event
.WaitForMultipleObjects(events
, False, timeout
)
461 if not self
._running
:
463 if rc
== win32event
.WAIT_TIMEOUT
:
466 self
._handle
_results
()
468 with self
._stop
_event
_lock
:
469 if self
._stop
_event
is not None:
470 win32file
.CloseHandle(self
._stop
_event
)
471 self
._stop
_event
= None
472 if self
._worktree
_watch
is not None:
473 self
._worktree
_watch
.close()
474 if self
._git
_dir
_watch
is not None:
475 self
._git
_dir
_watch
.close()
477 def _handle_results(self
):
478 if self
._worktree
_watch
is not None:
479 for _
, path
in self
._worktree
_watch
.read():
480 if not self
._running
:
482 if self
._force
_notify
:
484 path
= self
._worktree
+ '/' + self
._transform
_path
(path
)
486 path
!= self
._git
_dir
487 and not path
.startswith(self
._git
_dir
+ '/')
488 and not os
.path
.isdir(path
)
490 if self
._use
_check
_ignore
:
491 self
._file
_paths
.add(path
)
493 self
._force
_notify
= True
494 for _
, path
in self
._git
_dir
_watch
.read():
495 if not self
._running
:
497 if self
._force
_notify
:
499 path
= self
._transform
_path
(path
)
500 if path
.endswith('.lock'):
503 self
._force
_config
= True
505 if path
== 'head' or path
== 'index' or path
.startswith('refs/'):
506 self
._force
_notify
= True
509 self
._running
= False
510 with self
._stop
_event
_lock
:
511 if self
._stop
_event
is not None:
512 win32event
.SetEvent(self
._stop
_event
)
519 if not cfg
.get('cola.inotify', default
=True):
521 'File system change monitoring: disabled because'
522 ' "cola.inotify" is false.\n'
525 elif AVAILABLE
== 'inotify':
526 thread_class
= _InotifyThread
527 elif AVAILABLE
== 'pywin32':
528 thread_class
= _Win32Thread
532 'File system change monitoring: disabled because pywin32'
533 ' is not installed.\n'
536 elif utils
.is_linux():
538 'File system change monitoring: disabled because libc'
539 ' does not support the inotify system calls.\n'
542 return _Monitor(context
, thread_class
)