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.
315 wd_to_path_map
[wd
] = path
316 path_to_wd_map
[path
] = wd
318 def _check_event(self
, wd
, mask
, name
):
319 if mask
& inotify
.IN_Q_OVERFLOW
:
320 self
._force
_notify
= True
321 elif not mask
& self
._TRIGGER
_MASK
:
323 elif mask
& inotify
.IN_ISDIR
:
325 elif wd
in self
._worktree
_wd
_to
_path
_map
:
326 if self
._use
_check
_ignore
and name
:
328 self
._worktree
_wd
_to
_path
_map
[wd
], core
.decode(name
)
330 self
._file
_paths
.add(path
)
332 self
._force
_notify
= True
333 elif wd
== self
._git
_dir
_wd
:
334 name
= core
.decode(name
)
335 if name
in ('HEAD', 'index'):
336 self
._force
_notify
= True
337 elif name
== 'config':
338 self
._force
_config
= True
339 elif wd
in self
._git
_dir
_wd
_to
_path
_map
and not core
.decode(name
).endswith(
342 self
._force
_notify
= True
344 def _handle_events(self
):
345 for wd
, mask
, _
, name
in inotify
.read_events(self
._inotify
_fd
):
346 if not self
._force
_notify
:
347 self
._check
_event
(wd
, mask
, name
)
350 self
._running
= False
352 if self
._pipe
_w
is not None:
353 os
.write(self
._pipe
_w
, bchr(0))
357 if AVAILABLE
== 'pywin32':
359 class _Win32Watch(object):
360 def __init__(self
, path
, flags
):
367 self
.handle
= win32file
.CreateFileW(
369 0x0001, # FILE_LIST_DIRECTORY
370 win32con
.FILE_SHARE_READ | win32con
.FILE_SHARE_WRITE
,
372 win32con
.OPEN_EXISTING
,
373 win32con
.FILE_FLAG_BACKUP_SEMANTICS | win32con
.FILE_FLAG_OVERLAPPED
,
377 self
.buffer = win32file
.AllocateReadBuffer(8192)
378 self
.event
= win32event
.CreateEvent(None, True, False, None)
379 self
.overlapped
= pywintypes
.OVERLAPPED()
380 self
.overlapped
.hEvent
= self
.event
387 win32file
.ReadDirectoryChangesW(
388 self
.handle
, self
.buffer, True, self
.flags
, self
.overlapped
392 if win32event
.WaitForSingleObject(self
.event
, 0) == win32event
.WAIT_TIMEOUT
:
395 nbytes
= win32file
.GetOverlappedResult(
396 self
.handle
, self
.overlapped
, False
398 result
= win32file
.FILE_NOTIFY_INFORMATION(self
.buffer, nbytes
)
403 if self
.handle
is not None:
404 win32file
.CancelIo(self
.handle
)
405 win32file
.CloseHandle(self
.handle
)
406 if self
.event
is not None:
407 win32file
.CloseHandle(self
.event
)
409 class _Win32Thread(_BaseThread
):
411 win32con
.FILE_NOTIFY_CHANGE_FILE_NAME
412 | win32con
.FILE_NOTIFY_CHANGE_DIR_NAME
413 | win32con
.FILE_NOTIFY_CHANGE_ATTRIBUTES
414 | win32con
.FILE_NOTIFY_CHANGE_SIZE
415 | win32con
.FILE_NOTIFY_CHANGE_LAST_WRITE
416 | win32con
.FILE_NOTIFY_CHANGE_SECURITY
419 def __init__(self
, context
, monitor
):
420 _BaseThread
.__init
__(self
, context
, monitor
)
422 worktree
= git
.worktree()
423 if worktree
is not None:
424 worktree
= self
._transform
_path
(core
.abspath(worktree
))
425 self
._worktree
= worktree
426 self
._worktree
_watch
= None
427 self
._git
_dir
= self
._transform
_path
(core
.abspath(git
.git_path()))
428 self
._git
_dir
_watch
= None
429 self
._stop
_event
_lock
= Lock()
430 self
._stop
_event
= None
433 def _transform_path(path
):
434 return path
.replace('\\', '/').lower()
438 with self
._stop
_event
_lock
:
439 self
._stop
_event
= win32event
.CreateEvent(None, True, False, None)
441 events
= [self
._stop
_event
]
443 if self
._worktree
is not None:
444 self
._worktree
_watch
= _Win32Watch(self
._worktree
, self
._FLAGS
)
445 events
.append(self
._worktree
_watch
.event
)
447 self
._git
_dir
_watch
= _Win32Watch(self
._git
_dir
, self
._FLAGS
)
448 events
.append(self
._git
_dir
_watch
.event
)
450 self
._log
_enabled
_message
()
454 timeout
= self
._NOTIFICATION
_DELAY
456 timeout
= win32event
.INFINITE
457 status
= win32event
.WaitForMultipleObjects(events
, False, timeout
)
458 if not self
._running
:
460 if status
== win32event
.WAIT_TIMEOUT
:
463 self
._handle
_results
()
465 with self
._stop
_event
_lock
:
466 if self
._stop
_event
is not None:
467 win32file
.CloseHandle(self
._stop
_event
)
468 self
._stop
_event
= None
469 if self
._worktree
_watch
is not None:
470 self
._worktree
_watch
.close()
471 if self
._git
_dir
_watch
is not None:
472 self
._git
_dir
_watch
.close()
474 def _handle_results(self
):
475 if self
._worktree
_watch
is not None:
476 for _
, path
in self
._worktree
_watch
.read():
477 if not self
._running
:
479 if self
._force
_notify
:
481 path
= self
._worktree
+ '/' + self
._transform
_path
(path
)
483 path
!= self
._git
_dir
484 and not path
.startswith(self
._git
_dir
+ '/')
485 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
502 if path
== 'head' or path
== 'index' or path
.startswith('refs/'):
503 self
._force
_notify
= True
506 self
._running
= False
507 with self
._stop
_event
_lock
:
508 if self
._stop
_event
is not None:
509 win32event
.SetEvent(self
._stop
_event
)
516 if not cfg
.get('cola.inotify', default
=True):
518 'File system change monitoring: disabled because'
519 ' "cola.inotify" is false.\n'
522 elif AVAILABLE
== 'inotify':
523 thread_class
= _InotifyThread
524 elif AVAILABLE
== 'pywin32':
525 thread_class
= _Win32Thread
529 'File system change monitoring: disabled because pywin32'
530 ' is not installed.\n'
533 elif utils
.is_linux():
535 'File system change monitoring: disabled because libc'
536 ' does not support the inotify system calls.\n'
539 return _Monitor(context
, thread_class
)