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.
13 from threading
import Lock
15 from qtpy
import QtCore
16 from qtpy
.QtCore
import Signal
22 from .compat
import bchr
24 from .interaction
import Interaction
38 elif utils
.is_linux():
47 class _Monitor(QtCore
.QObject
):
48 files_changed
= Signal()
49 config_changed
= Signal()
51 def __init__(self
, context
, thread_class
):
52 QtCore
.QObject
.__init
__(self
)
53 self
.context
= context
54 self
._thread
_class
= thread_class
58 if self
._thread
_class
is not None:
59 assert self
._thread
is None
60 self
._thread
= self
._thread
_class
(self
.context
, self
)
64 if self
._thread
_class
is not None:
65 assert self
._thread
is not None
71 if self
._thread
is not None:
72 self
._thread
.refresh()
75 class _BaseThread(QtCore
.QThread
):
76 #: The delay, in milliseconds, between detecting file system modification
77 #: and triggering the 'files_changed' signal, to coalesce multiple
78 #: modifications into a single signal.
79 _NOTIFICATION_DELAY
= 888
81 def __init__(self
, context
, monitor
):
82 QtCore
.QThread
.__init
__(self
)
83 self
.context
= context
84 self
._monitor
= monitor
86 self
._use
_check
_ignore
= version
.check_git(context
, 'check-ignore')
87 self
._force
_notify
= False
88 self
._force
_config
= False
89 self
._file
_paths
= set()
93 return self
._force
_notify
or self
._file
_paths
or self
._force
_config
96 """Do any housekeeping necessary in response to repository changes."""
100 """Notifies all observers"""
103 if self
._force
_config
:
105 if self
._force
_notify
:
107 elif self
._file
_paths
:
108 proc
= core
.start_command(
109 ['git', 'check-ignore', '--verbose', '--non-matching', '-z', '--stdin']
111 path_list
= bchr(0).join(core
.encode(path
) for path
in self
._file
_paths
)
112 out
, _
= proc
.communicate(path_list
)
116 # Each output record is four fields separated by NULL
117 # characters (records are also separated by NULL characters):
118 # <source> <NULL> <linenum> <NULL> <pattern> <NULL> <pathname>
119 # For paths which are not ignored, all fields will be empty
120 # except for <pathname>. So to see if we have any non-ignored
121 # files, we simply check every fourth field to see if any of
123 source_fields
= out
.split(bchr(0))[0:-1:4]
124 do_notify
= not all(source_fields
)
125 self
._force
_notify
= False
126 self
._force
_config
= False
127 self
._file
_paths
= set()
129 # "files changed" is a bigger hammer than "config changed".
130 # and is a superset relative to what is done in response to the
131 # signal. Thus, the "elif" below avoids repeated work that
132 # would be done if it were a simple "if" check instead.
134 self
._monitor
.files_changed
.emit()
136 self
._monitor
.config_changed
.emit()
139 def _log_enabled_message():
140 msg
= N_('File system change monitoring: enabled.\n')
144 if AVAILABLE
== 'inotify':
146 class _InotifyThread(_BaseThread
):
149 | inotify
.IN_CLOSE_WRITE
153 | inotify
.IN_MOVED_FROM
154 | inotify
.IN_MOVED_TO
156 _ADD_MASK
= _TRIGGER_MASK | inotify
.IN_EXCL_UNLINK | inotify
.IN_ONLYDIR
158 def __init__(self
, context
, monitor
):
159 _BaseThread
.__init
__(self
, context
, monitor
)
161 worktree
= git
.worktree()
162 if worktree
is not None:
163 worktree
= core
.abspath(worktree
)
164 self
._worktree
= worktree
165 self
._git
_dir
= git
.git_path()
167 self
._inotify
_fd
= None
170 self
._worktree
_wd
_to
_path
_map
= {}
171 self
._worktree
_path
_to
_wd
_map
= {}
172 self
._git
_dir
_wd
_to
_path
_map
= {}
173 self
._git
_dir
_path
_to
_wd
_map
= {}
174 self
._git
_dir
_wd
= None
177 def _log_out_of_wds_message():
179 'File system change monitoring: disabled because the'
180 ' limit on the total number of inotify watches was'
181 ' reached. You may be able to increase the limit on'
182 ' the number of watches by running:\n'
184 ' echo fs.inotify.max_user_watches=100000 |'
185 ' sudo tee -a /etc/sysctl.conf &&'
194 self
._inotify
_fd
= inotify
.init()
196 self
._inotify
_fd
= None
197 self
._running
= False
198 if e
.errno
== errno
.EMFILE
:
199 self
._log
_out
_of
_wds
_message
()
201 self
._pipe
_r
, self
._pipe
_w
= os
.pipe()
203 # pylint: disable=no-member
204 poll_obj
= select
.poll()
205 poll_obj
.register(self
._inotify
_fd
, select
.POLLIN
)
206 poll_obj
.register(self
._pipe
_r
, select
.POLLIN
)
211 self
._log
_enabled
_message
()
212 self
._process
_events
(poll_obj
)
216 def _process_events(self
, poll_obj
):
219 timeout
= self
._NOTIFICATION
_DELAY
223 events
= poll_obj
.poll(timeout
)
224 # pylint: disable=duplicate-except
228 if not self
._running
:
234 if fd
== self
._inotify
_fd
:
235 self
._handle
_events
()
237 def _close_fds(self
):
239 if self
._inotify
_fd
is not None:
240 os
.close(self
._inotify
_fd
)
241 self
._inotify
_fd
= None
242 if self
._pipe
_r
is not None:
243 os
.close(self
._pipe
_r
)
245 os
.close(self
._pipe
_w
)
253 if self
._inotify
_fd
is None:
255 context
= self
.context
257 if self
._worktree
is not None:
259 os
.path
.dirname(os
.path
.join(self
._worktree
, path
))
260 for path
in gitcmds
.tracked_files(context
)
262 self
._refresh
_watches
(
264 self
._worktree
_wd
_to
_path
_map
,
265 self
._worktree
_path
_to
_wd
_map
,
268 git_dirs
.add(self
._git
_dir
)
269 for dirpath
, _
, _
in core
.walk(os
.path
.join(self
._git
_dir
, 'refs')):
270 git_dirs
.add(dirpath
)
271 self
._refresh
_watches
(
272 git_dirs
, self
._git
_dir
_wd
_to
_path
_map
, self
._git
_dir
_path
_to
_wd
_map
274 self
._git
_dir
_wd
= self
._git
_dir
_path
_to
_wd
_map
.get(self
._git
_dir
)
276 if e
.errno
in (errno
.ENOSPC
, errno
.EMFILE
):
277 self
._log
_out
_of
_wds
_message
()
278 self
._running
= False
282 def _refresh_watches(self
, paths_to_watch
, wd_to_path_map
, path_to_wd_map
):
283 watched_paths
= set(path_to_wd_map
)
284 for path
in watched_paths
- paths_to_watch
:
285 wd
= path_to_wd_map
.pop(path
)
286 wd_to_path_map
.pop(wd
)
288 inotify
.rm_watch(self
._inotify
_fd
, wd
)
290 if e
.errno
== errno
.EINVAL
:
291 # This error can occur if the target of the wd was
292 # removed on the filesystem before we call
293 # inotify.rm_watch() so ignore it.
296 for path
in paths_to_watch
- watched_paths
:
298 wd
= inotify
.add_watch(
299 self
._inotify
_fd
, core
.encode(path
), self
._ADD
_MASK
302 if e
.errno
in (errno
.ENOENT
, errno
.ENOTDIR
):
303 # These two errors should only occur as a result of
304 # race conditions: the first if the directory
305 # referenced by path was removed or renamed before the
306 # call to inotify.add_watch(); the second if the
307 # directory referenced by path was replaced with a file
308 # before the call to inotify.add_watch(). Therefore we
309 # simply ignore them.
312 wd_to_path_map
[wd
] = path
313 path_to_wd_map
[path
] = wd
315 def _check_event(self
, wd
, mask
, name
):
316 if mask
& inotify
.IN_Q_OVERFLOW
:
317 self
._force
_notify
= True
318 elif not mask
& self
._TRIGGER
_MASK
:
320 elif mask
& inotify
.IN_ISDIR
:
322 elif wd
in self
._worktree
_wd
_to
_path
_map
:
323 if self
._use
_check
_ignore
and name
:
325 self
._worktree
_wd
_to
_path
_map
[wd
], core
.decode(name
)
327 self
._file
_paths
.add(path
)
329 self
._force
_notify
= True
330 elif wd
== self
._git
_dir
_wd
:
331 name
= core
.decode(name
)
332 if name
in ('HEAD', 'index'):
333 self
._force
_notify
= True
334 elif name
== 'config':
335 self
._force
_config
= True
336 elif wd
in self
._git
_dir
_wd
_to
_path
_map
and not core
.decode(name
).endswith(
339 self
._force
_notify
= True
341 def _handle_events(self
):
342 for wd
, mask
, _
, name
in inotify
.read_events(self
._inotify
_fd
):
343 if not self
._force
_notify
:
344 self
._check
_event
(wd
, mask
, name
)
347 self
._running
= False
349 if self
._pipe
_w
is not None:
350 os
.write(self
._pipe
_w
, bchr(0))
354 if AVAILABLE
== 'pywin32':
357 def __init__(self
, path
, flags
):
364 self
.handle
= win32file
.CreateFileW(
366 0x0001, # FILE_LIST_DIRECTORY
367 win32con
.FILE_SHARE_READ | win32con
.FILE_SHARE_WRITE
,
369 win32con
.OPEN_EXISTING
,
370 win32con
.FILE_FLAG_BACKUP_SEMANTICS | win32con
.FILE_FLAG_OVERLAPPED
,
374 self
.buffer = win32file
.AllocateReadBuffer(8192)
375 self
.event
= win32event
.CreateEvent(None, True, False, None)
376 self
.overlapped
= pywintypes
.OVERLAPPED()
377 self
.overlapped
.hEvent
= self
.event
384 win32file
.ReadDirectoryChangesW(
385 self
.handle
, self
.buffer, True, self
.flags
, self
.overlapped
389 if win32event
.WaitForSingleObject(self
.event
, 0) == win32event
.WAIT_TIMEOUT
:
392 nbytes
= win32file
.GetOverlappedResult(
393 self
.handle
, self
.overlapped
, False
395 result
= win32file
.FILE_NOTIFY_INFORMATION(self
.buffer, nbytes
)
400 if self
.handle
is not None:
401 win32file
.CancelIo(self
.handle
)
402 win32file
.CloseHandle(self
.handle
)
403 if self
.event
is not None:
404 win32file
.CloseHandle(self
.event
)
406 class _Win32Thread(_BaseThread
):
408 win32con
.FILE_NOTIFY_CHANGE_FILE_NAME
409 | win32con
.FILE_NOTIFY_CHANGE_DIR_NAME
410 | win32con
.FILE_NOTIFY_CHANGE_ATTRIBUTES
411 | win32con
.FILE_NOTIFY_CHANGE_SIZE
412 | win32con
.FILE_NOTIFY_CHANGE_LAST_WRITE
413 | win32con
.FILE_NOTIFY_CHANGE_SECURITY
416 def __init__(self
, context
, monitor
):
417 _BaseThread
.__init
__(self
, context
, monitor
)
419 worktree
= git
.worktree()
420 if worktree
is not None:
421 worktree
= self
._transform
_path
(core
.abspath(worktree
))
422 self
._worktree
= worktree
423 self
._worktree
_watch
= None
424 self
._git
_dir
= self
._transform
_path
(core
.abspath(git
.git_path()))
425 self
._git
_dir
_watch
= None
426 self
._stop
_event
_lock
= Lock()
427 self
._stop
_event
= None
430 def _transform_path(path
):
431 return path
.replace('\\', '/').lower()
435 with self
._stop
_event
_lock
:
436 self
._stop
_event
= win32event
.CreateEvent(None, True, False, None)
438 events
= [self
._stop
_event
]
440 if self
._worktree
is not None:
441 self
._worktree
_watch
= _Win32Watch(self
._worktree
, self
._FLAGS
)
442 events
.append(self
._worktree
_watch
.event
)
444 self
._git
_dir
_watch
= _Win32Watch(self
._git
_dir
, self
._FLAGS
)
445 events
.append(self
._git
_dir
_watch
.event
)
447 self
._log
_enabled
_message
()
451 timeout
= self
._NOTIFICATION
_DELAY
453 timeout
= win32event
.INFINITE
454 status
= win32event
.WaitForMultipleObjects(events
, False, timeout
)
455 if not self
._running
:
457 if status
== win32event
.WAIT_TIMEOUT
:
460 self
._handle
_results
()
462 with self
._stop
_event
_lock
:
463 if self
._stop
_event
is not None:
464 win32file
.CloseHandle(self
._stop
_event
)
465 self
._stop
_event
= None
466 if self
._worktree
_watch
is not None:
467 self
._worktree
_watch
.close()
468 if self
._git
_dir
_watch
is not None:
469 self
._git
_dir
_watch
.close()
471 def _handle_results(self
):
472 if self
._worktree
_watch
is not None:
473 for _
, path
in self
._worktree
_watch
.read():
474 if not self
._running
:
476 if self
._force
_notify
:
478 path
= self
._worktree
+ '/' + self
._transform
_path
(path
)
480 path
!= self
._git
_dir
481 and not path
.startswith(self
._git
_dir
+ '/')
482 and not os
.path
.isdir(path
)
484 if self
._use
_check
_ignore
:
485 self
._file
_paths
.add(path
)
487 self
._force
_notify
= True
488 for _
, path
in self
._git
_dir
_watch
.read():
489 if not self
._running
:
491 if self
._force
_notify
:
493 path
= self
._transform
_path
(path
)
494 if path
.endswith('.lock'):
497 self
._force
_config
= True
499 if path
== 'head' or path
== 'index' or path
.startswith('refs/'):
500 self
._force
_notify
= True
503 self
._running
= False
504 with self
._stop
_event
_lock
:
505 if self
._stop
_event
is not None:
506 win32event
.SetEvent(self
._stop
_event
)
513 if not cfg
.get('cola.inotify', default
=True):
515 'File system change monitoring: disabled because'
516 ' "cola.inotify" is false.\n'
519 elif AVAILABLE
== 'inotify':
520 thread_class
= _InotifyThread
521 elif AVAILABLE
== 'pywin32':
522 thread_class
= _Win32Thread
526 'File system change monitoring: disabled because pywin32'
527 ' is not installed.\n'
530 elif utils
.is_linux():
532 'File system change monitoring: disabled because libc'
533 ' does not support the inotify system calls.\n'
536 return _Monitor(context
, thread_class
)