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 absolute_import
, division
, print_function
, 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:
262 os
.path
.dirname(os
.path
.join(self
._worktree
, path
))
263 for path
in gitcmds
.tracked_files(context
)
265 self
._refresh
_watches
(
267 self
._worktree
_wd
_to
_path
_map
,
268 self
._worktree
_path
_to
_wd
_map
,
271 git_dirs
.add(self
._git
_dir
)
272 for dirpath
, _
, _
in core
.walk(os
.path
.join(self
._git
_dir
, 'refs')):
273 git_dirs
.add(dirpath
)
274 self
._refresh
_watches
(
275 git_dirs
, self
._git
_dir
_wd
_to
_path
_map
, self
._git
_dir
_path
_to
_wd
_map
277 self
._git
_dir
_wd
= self
._git
_dir
_path
_to
_wd
_map
.get(self
._git
_dir
)
279 if e
.errno
in (errno
.ENOSPC
, errno
.EMFILE
):
280 self
._log
_out
_of
_wds
_message
()
281 self
._running
= False
285 def _refresh_watches(self
, paths_to_watch
, wd_to_path_map
, path_to_wd_map
):
286 watched_paths
= set(path_to_wd_map
)
287 for path
in watched_paths
- paths_to_watch
:
288 wd
= path_to_wd_map
.pop(path
)
289 wd_to_path_map
.pop(wd
)
291 inotify
.rm_watch(self
._inotify
_fd
, wd
)
293 if e
.errno
== errno
.EINVAL
:
294 # This error can occur if the target of the wd was
295 # removed on the filesystem before we call
296 # inotify.rm_watch() so ignore it.
299 for path
in paths_to_watch
- watched_paths
:
301 wd
= inotify
.add_watch(
302 self
._inotify
_fd
, core
.encode(path
), self
._ADD
_MASK
305 if e
.errno
in (errno
.ENOENT
, errno
.ENOTDIR
):
306 # These two errors should only occur as a result of
307 # race conditions: the first if the directory
308 # referenced by path was removed or renamed before the
309 # call to inotify.add_watch(); the second if the
310 # directory referenced by path was replaced with a file
311 # before the call to inotify.add_watch(). Therefore we
312 # simply ignore them.
316 wd_to_path_map
[wd
] = path
317 path_to_wd_map
[path
] = wd
319 def _check_event(self
, wd
, mask
, name
):
320 if mask
& inotify
.IN_Q_OVERFLOW
:
321 self
._force
_notify
= True
322 elif not mask
& self
._TRIGGER
_MASK
:
324 elif mask
& inotify
.IN_ISDIR
:
326 elif wd
in self
._worktree
_wd
_to
_path
_map
:
327 if self
._use
_check
_ignore
and name
:
329 self
._worktree
_wd
_to
_path
_map
[wd
], core
.decode(name
)
331 self
._file
_paths
.add(path
)
333 self
._force
_notify
= True
334 elif wd
== self
._git
_dir
_wd
:
335 name
= core
.decode(name
)
336 if name
in ('HEAD', 'index'):
337 self
._force
_notify
= True
338 elif name
== 'config':
339 self
._force
_config
= True
340 elif wd
in self
._git
_dir
_wd
_to
_path
_map
and not core
.decode(name
).endswith(
343 self
._force
_notify
= True
345 def _handle_events(self
):
346 for wd
, mask
, _
, name
in inotify
.read_events(self
._inotify
_fd
):
347 if not self
._force
_notify
:
348 self
._check
_event
(wd
, mask
, name
)
351 self
._running
= False
353 if self
._pipe
_w
is not None:
354 os
.write(self
._pipe
_w
, bchr(0))
358 if AVAILABLE
== 'pywin32':
360 class _Win32Watch(object):
361 def __init__(self
, path
, flags
):
368 self
.handle
= win32file
.CreateFileW(
370 0x0001, # FILE_LIST_DIRECTORY
371 win32con
.FILE_SHARE_READ | win32con
.FILE_SHARE_WRITE
,
373 win32con
.OPEN_EXISTING
,
374 win32con
.FILE_FLAG_BACKUP_SEMANTICS | win32con
.FILE_FLAG_OVERLAPPED
,
378 self
.buffer = win32file
.AllocateReadBuffer(8192)
379 self
.event
= win32event
.CreateEvent(None, True, False, None)
380 self
.overlapped
= pywintypes
.OVERLAPPED()
381 self
.overlapped
.hEvent
= self
.event
388 win32file
.ReadDirectoryChangesW(
389 self
.handle
, self
.buffer, True, self
.flags
, self
.overlapped
393 if win32event
.WaitForSingleObject(self
.event
, 0) == win32event
.WAIT_TIMEOUT
:
396 nbytes
= win32file
.GetOverlappedResult(
397 self
.handle
, self
.overlapped
, False
399 result
= win32file
.FILE_NOTIFY_INFORMATION(self
.buffer, nbytes
)
404 if self
.handle
is not None:
405 win32file
.CancelIo(self
.handle
)
406 win32file
.CloseHandle(self
.handle
)
407 if self
.event
is not None:
408 win32file
.CloseHandle(self
.event
)
410 class _Win32Thread(_BaseThread
):
412 win32con
.FILE_NOTIFY_CHANGE_FILE_NAME
413 | win32con
.FILE_NOTIFY_CHANGE_DIR_NAME
414 | win32con
.FILE_NOTIFY_CHANGE_ATTRIBUTES
415 | win32con
.FILE_NOTIFY_CHANGE_SIZE
416 | win32con
.FILE_NOTIFY_CHANGE_LAST_WRITE
417 | win32con
.FILE_NOTIFY_CHANGE_SECURITY
420 def __init__(self
, context
, monitor
):
421 _BaseThread
.__init
__(self
, context
, monitor
)
423 worktree
= git
.worktree()
424 if worktree
is not None:
425 worktree
= self
._transform
_path
(core
.abspath(worktree
))
426 self
._worktree
= worktree
427 self
._worktree
_watch
= None
428 self
._git
_dir
= self
._transform
_path
(core
.abspath(git
.git_path()))
429 self
._git
_dir
_watch
= None
430 self
._stop
_event
_lock
= Lock()
431 self
._stop
_event
= None
434 def _transform_path(path
):
435 return path
.replace('\\', '/').lower()
439 with self
._stop
_event
_lock
:
440 self
._stop
_event
= win32event
.CreateEvent(None, True, False, None)
442 events
= [self
._stop
_event
]
444 if self
._worktree
is not None:
445 self
._worktree
_watch
= _Win32Watch(self
._worktree
, self
._FLAGS
)
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, timeout
)
459 if not self
._running
:
461 if rc
== win32event
.WAIT_TIMEOUT
:
464 self
._handle
_results
()
466 with self
._stop
_event
_lock
:
467 if self
._stop
_event
is not None:
468 win32file
.CloseHandle(self
._stop
_event
)
469 self
._stop
_event
= None
470 if self
._worktree
_watch
is not None:
471 self
._worktree
_watch
.close()
472 if self
._git
_dir
_watch
is not None:
473 self
._git
_dir
_watch
.close()
475 def _handle_results(self
):
476 if self
._worktree
_watch
is not None:
477 for _
, path
in self
._worktree
_watch
.read():
478 if not self
._running
:
480 if self
._force
_notify
:
482 path
= self
._worktree
+ '/' + self
._transform
_path
(path
)
484 path
!= self
._git
_dir
485 and not path
.startswith(self
._git
_dir
+ '/')
486 and not os
.path
.isdir(path
)
488 if self
._use
_check
_ignore
:
489 self
._file
_paths
.add(path
)
491 self
._force
_notify
= True
492 for _
, path
in self
._git
_dir
_watch
.read():
493 if not self
._running
:
495 if self
._force
_notify
:
497 path
= self
._transform
_path
(path
)
498 if path
.endswith('.lock'):
501 self
._force
_config
= True
503 if path
== 'head' or path
== 'index' or path
.startswith('refs/'):
504 self
._force
_notify
= True
507 self
._running
= False
508 with self
._stop
_event
_lock
:
509 if self
._stop
_event
is not None:
510 win32event
.SetEvent(self
._stop
_event
)
517 if not cfg
.get('cola.inotify', default
=True):
519 'File system change monitoring: disabled because'
520 ' "cola.inotify" is false.\n'
523 elif AVAILABLE
== 'inotify':
524 thread_class
= _InotifyThread
525 elif AVAILABLE
== 'pywin32':
526 thread_class
= _Win32Thread
530 'File system change monitoring: disabled because pywin32'
531 ' is not installed.\n'
534 elif utils
.is_linux():
536 'File system change monitoring: disabled because libc'
537 ' does not support the inotify system calls.\n'
540 return _Monitor(context
, thread_class
)