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
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(
111 ['git', 'check-ignore', '--verbose', '--non-matching', '-z', '--stdin']
113 path_list
= bchr(0).join(core
.encode(path
) 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
156 | inotify
.IN_MOVED_TO
158 _ADD_MASK
= _TRIGGER_MASK | inotify
.IN_EXCL_UNLINK | inotify
.IN_ONLYDIR
160 def __init__(self
, context
, monitor
):
161 _BaseThread
.__init
__(self
, context
, monitor
)
163 worktree
= git
.worktree()
164 if worktree
is not None:
165 worktree
= core
.abspath(worktree
)
166 self
._worktree
= worktree
167 self
._git
_dir
= git
.git_path()
169 self
._inotify
_fd
= None
172 self
._worktree
_wd
_to
_path
_map
= {}
173 self
._worktree
_path
_to
_wd
_map
= {}
174 self
._git
_dir
_wd
_to
_path
_map
= {}
175 self
._git
_dir
_path
_to
_wd
_map
= {}
176 self
._git
_dir
_wd
= None
179 def _log_out_of_wds_message():
181 'File system change monitoring: disabled because the'
182 ' limit on the total number of inotify watches was'
183 ' reached. You may be able to increase the limit on'
184 ' the number of watches by running:\n'
186 ' echo fs.inotify.max_user_watches=100000 |'
187 ' sudo tee -a /etc/sysctl.conf &&'
196 self
._inotify
_fd
= inotify
.init()
198 self
._inotify
_fd
= None
199 self
._running
= False
200 if e
.errno
== errno
.EMFILE
:
201 self
._log
_out
_of
_wds
_message
()
203 self
._pipe
_r
, self
._pipe
_w
= os
.pipe()
205 # pylint: disable=no-member
206 poll_obj
= select
.poll()
207 poll_obj
.register(self
._inotify
_fd
, select
.POLLIN
)
208 poll_obj
.register(self
._pipe
_r
, select
.POLLIN
)
213 self
._log
_enabled
_message
()
214 self
._process
_events
(poll_obj
)
218 def _process_events(self
, poll_obj
):
221 timeout
= self
._NOTIFICATION
_DELAY
225 events
= poll_obj
.poll(timeout
)
226 # pylint: disable=duplicate-except
227 except (OSError, select
.error
):
230 if not self
._running
:
235 for (fd
, _
) in events
:
236 if fd
== self
._inotify
_fd
:
237 self
._handle
_events
()
239 def _close_fds(self
):
241 if self
._inotify
_fd
is not None:
242 os
.close(self
._inotify
_fd
)
243 self
._inotify
_fd
= None
244 if self
._pipe
_r
is not None:
245 os
.close(self
._pipe
_r
)
247 os
.close(self
._pipe
_w
)
255 if self
._inotify
_fd
is None:
257 context
= self
.context
259 if self
._worktree
is not None:
261 os
.path
.dirname(os
.path
.join(self
._worktree
, path
))
262 for path
in gitcmds
.tracked_files(context
)
264 self
._refresh
_watches
(
266 self
._worktree
_wd
_to
_path
_map
,
267 self
._worktree
_path
_to
_wd
_map
,
270 git_dirs
.add(self
._git
_dir
)
271 for dirpath
, _
, _
in core
.walk(os
.path
.join(self
._git
_dir
, 'refs')):
272 git_dirs
.add(dirpath
)
273 self
._refresh
_watches
(
274 git_dirs
, self
._git
_dir
_wd
_to
_path
_map
, self
._git
_dir
_path
_to
_wd
_map
276 self
._git
_dir
_wd
= self
._git
_dir
_path
_to
_wd
_map
.get(self
._git
_dir
)
278 if e
.errno
in (errno
.ENOSPC
, errno
.EMFILE
):
279 self
._log
_out
_of
_wds
_message
()
280 self
._running
= False
284 def _refresh_watches(self
, paths_to_watch
, wd_to_path_map
, path_to_wd_map
):
285 watched_paths
= set(path_to_wd_map
)
286 for path
in watched_paths
- paths_to_watch
:
287 wd
= path_to_wd_map
.pop(path
)
288 wd_to_path_map
.pop(wd
)
290 inotify
.rm_watch(self
._inotify
_fd
, wd
)
292 if e
.errno
== errno
.EINVAL
:
293 # This error can occur if the target of the wd was
294 # removed on the filesystem before we call
295 # inotify.rm_watch() so ignore it.
298 for path
in paths_to_watch
- watched_paths
:
300 wd
= inotify
.add_watch(
301 self
._inotify
_fd
, core
.encode(path
), self
._ADD
_MASK
304 if e
.errno
in (errno
.ENOENT
, errno
.ENOTDIR
):
305 # These two errors should only occur as a result of
306 # race conditions: the first if the directory
307 # referenced by path was removed or renamed before the
308 # call to inotify.add_watch(); the second if the
309 # directory referenced by path was replaced with a file
310 # before the call to inotify.add_watch(). Therefore we
311 # simply ignore them.
314 wd_to_path_map
[wd
] = path
315 path_to_wd_map
[path
] = wd
317 def _check_event(self
, wd
, mask
, name
):
318 if mask
& inotify
.IN_Q_OVERFLOW
:
319 self
._force
_notify
= True
320 elif not mask
& self
._TRIGGER
_MASK
:
322 elif mask
& inotify
.IN_ISDIR
:
324 elif wd
in self
._worktree
_wd
_to
_path
_map
:
325 if self
._use
_check
_ignore
and name
:
327 self
._worktree
_wd
_to
_path
_map
[wd
], core
.decode(name
)
329 self
._file
_paths
.add(path
)
331 self
._force
_notify
= True
332 elif wd
== self
._git
_dir
_wd
:
333 name
= core
.decode(name
)
334 if name
in ('HEAD', 'index'):
335 self
._force
_notify
= True
336 elif name
== 'config':
337 self
._force
_config
= True
338 elif wd
in self
._git
_dir
_wd
_to
_path
_map
and not core
.decode(name
).endswith(
341 self
._force
_notify
= True
343 def _handle_events(self
):
344 for wd
, mask
, _
, name
in inotify
.read_events(self
._inotify
_fd
):
345 if not self
._force
_notify
:
346 self
._check
_event
(wd
, mask
, name
)
349 self
._running
= False
351 if self
._pipe
_w
is not None:
352 os
.write(self
._pipe
_w
, bchr(0))
356 if AVAILABLE
== 'pywin32':
358 class _Win32Watch(object):
359 def __init__(self
, path
, flags
):
366 self
.handle
= win32file
.CreateFileW(
368 0x0001, # FILE_LIST_DIRECTORY
369 win32con
.FILE_SHARE_READ | win32con
.FILE_SHARE_WRITE
,
371 win32con
.OPEN_EXISTING
,
372 win32con
.FILE_FLAG_BACKUP_SEMANTICS | win32con
.FILE_FLAG_OVERLAPPED
,
376 self
.buffer = win32file
.AllocateReadBuffer(8192)
377 self
.event
= win32event
.CreateEvent(None, True, False, None)
378 self
.overlapped
= pywintypes
.OVERLAPPED()
379 self
.overlapped
.hEvent
= self
.event
386 win32file
.ReadDirectoryChangesW(
387 self
.handle
, self
.buffer, True, self
.flags
, self
.overlapped
391 if win32event
.WaitForSingleObject(self
.event
, 0) == win32event
.WAIT_TIMEOUT
:
394 nbytes
= win32file
.GetOverlappedResult(
395 self
.handle
, self
.overlapped
, False
397 result
= win32file
.FILE_NOTIFY_INFORMATION(self
.buffer, nbytes
)
402 if self
.handle
is not None:
403 win32file
.CancelIo(self
.handle
)
404 win32file
.CloseHandle(self
.handle
)
405 if self
.event
is not None:
406 win32file
.CloseHandle(self
.event
)
408 class _Win32Thread(_BaseThread
):
410 win32con
.FILE_NOTIFY_CHANGE_FILE_NAME
411 | win32con
.FILE_NOTIFY_CHANGE_DIR_NAME
412 | win32con
.FILE_NOTIFY_CHANGE_ATTRIBUTES
413 | win32con
.FILE_NOTIFY_CHANGE_SIZE
414 | win32con
.FILE_NOTIFY_CHANGE_LAST_WRITE
415 | win32con
.FILE_NOTIFY_CHANGE_SECURITY
418 def __init__(self
, context
, monitor
):
419 _BaseThread
.__init
__(self
, context
, monitor
)
421 worktree
= git
.worktree()
422 if worktree
is not None:
423 worktree
= self
._transform
_path
(core
.abspath(worktree
))
424 self
._worktree
= worktree
425 self
._worktree
_watch
= None
426 self
._git
_dir
= self
._transform
_path
(core
.abspath(git
.git_path()))
427 self
._git
_dir
_watch
= None
428 self
._stop
_event
_lock
= Lock()
429 self
._stop
_event
= None
432 def _transform_path(path
):
433 return path
.replace('\\', '/').lower()
437 with self
._stop
_event
_lock
:
438 self
._stop
_event
= win32event
.CreateEvent(None, True, False, None)
440 events
= [self
._stop
_event
]
442 if self
._worktree
is not None:
443 self
._worktree
_watch
= _Win32Watch(self
._worktree
, self
._FLAGS
)
444 events
.append(self
._worktree
_watch
.event
)
446 self
._git
_dir
_watch
= _Win32Watch(self
._git
_dir
, self
._FLAGS
)
447 events
.append(self
._git
_dir
_watch
.event
)
449 self
._log
_enabled
_message
()
453 timeout
= self
._NOTIFICATION
_DELAY
455 timeout
= win32event
.INFINITE
456 status
= win32event
.WaitForMultipleObjects(events
, False, timeout
)
457 if not self
._running
:
459 if status
== win32event
.WAIT_TIMEOUT
:
462 self
._handle
_results
()
464 with self
._stop
_event
_lock
:
465 if self
._stop
_event
is not None:
466 win32file
.CloseHandle(self
._stop
_event
)
467 self
._stop
_event
= None
468 if self
._worktree
_watch
is not None:
469 self
._worktree
_watch
.close()
470 if self
._git
_dir
_watch
is not None:
471 self
._git
_dir
_watch
.close()
473 def _handle_results(self
):
474 if self
._worktree
_watch
is not None:
475 for _
, path
in self
._worktree
_watch
.read():
476 if not self
._running
:
478 if self
._force
_notify
:
480 path
= self
._worktree
+ '/' + self
._transform
_path
(path
)
482 path
!= self
._git
_dir
483 and not path
.startswith(self
._git
_dir
+ '/')
484 and not os
.path
.isdir(path
)
486 if self
._use
_check
_ignore
:
487 self
._file
_paths
.add(path
)
489 self
._force
_notify
= True
490 for _
, path
in self
._git
_dir
_watch
.read():
491 if not self
._running
:
493 if self
._force
_notify
:
495 path
= self
._transform
_path
(path
)
496 if path
.endswith('.lock'):
499 self
._force
_config
= True
501 if path
== 'head' or path
== 'index' or path
.startswith('refs/'):
502 self
._force
_notify
= True
505 self
._running
= False
506 with self
._stop
_event
_lock
:
507 if self
._stop
_event
is not None:
508 win32event
.SetEvent(self
._stop
_event
)
515 if not cfg
.get('cola.inotify', default
=True):
517 'File system change monitoring: disabled because'
518 ' "cola.inotify" is false.\n'
521 elif AVAILABLE
== 'inotify':
522 thread_class
= _InotifyThread
523 elif AVAILABLE
== 'pywin32':
524 thread_class
= _Win32Thread
528 'File system change monitoring: disabled because pywin32'
529 ' is not installed.\n'
532 elif utils
.is_linux():
534 'File system change monitoring: disabled because libc'
535 ' does not support the inotify system calls.\n'
538 return _Monitor(context
, thread_class
)