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
43 elif utils
.is_linux():
52 class _Monitor(QtCore
.QObject
):
53 files_changed
= Signal()
54 config_changed
= Signal()
56 def __init__(self
, context
, thread_class
):
57 QtCore
.QObject
.__init
__(self
)
58 self
.context
= context
59 self
._thread
_class
= thread_class
63 if self
._thread
_class
is not None:
64 assert self
._thread
is None
65 self
._thread
= self
._thread
_class
(self
.context
, self
)
69 if self
._thread
_class
is not None:
70 assert self
._thread
is not None
76 if self
._thread
is not None:
77 self
._thread
.refresh()
80 class _BaseThread(QtCore
.QThread
):
81 #: The delay, in milliseconds, between detecting file system modification
82 #: and triggering the 'files_changed' signal, to coalesce multiple
83 #: modifications into a single signal.
84 _NOTIFICATION_DELAY
= 888
86 def __init__(self
, context
, monitor
):
87 QtCore
.QThread
.__init
__(self
)
88 self
.context
= context
89 self
._monitor
= monitor
91 self
._use
_check
_ignore
= version
.check_git(context
, 'check-ignore')
92 self
._force
_notify
= False
93 self
._force
_config
= False
94 self
._file
_paths
= set()
98 return self
._force
_notify
or self
._file
_paths
or self
._force
_config
101 """Do any housekeeping necessary in response to repository changes."""
105 """Notifies all observers"""
108 if self
._force
_config
:
110 if self
._force
_notify
:
112 elif self
._file
_paths
:
113 proc
= core
.start_command(
114 ['git', 'check-ignore', '--verbose', '--non-matching', '-z', '--stdin']
116 path_list
= bchr(0).join(core
.encode(path
) for path
in self
._file
_paths
)
117 out
, _
= proc
.communicate(path_list
)
121 # Each output record is four fields separated by NULL
122 # characters (records are also separated by NULL characters):
123 # <source> <NULL> <linenum> <NULL> <pattern> <NULL> <pathname>
124 # For paths which are not ignored, all fields will be empty
125 # except for <pathname>. So to see if we have any non-ignored
126 # files, we simply check every fourth field to see if any of
128 source_fields
= out
.split(bchr(0))[0:-1:4]
129 do_notify
= not all(source_fields
)
130 self
._force
_notify
= False
131 self
._force
_config
= False
132 self
._file
_paths
= set()
134 # "files changed" is a bigger hammer than "config changed".
135 # and is a superset relative to what is done in response to the
136 # signal. Thus, the "elif" below avoids repeated work that
137 # would be done if it were a simple "if" check instead.
139 self
._monitor
.files_changed
.emit()
141 self
._monitor
.config_changed
.emit()
144 def _log_enabled_message():
145 msg
= N_('File system change monitoring: enabled.\n')
149 if AVAILABLE
== 'inotify':
151 class _InotifyThread(_BaseThread
):
154 | inotify
.IN_CLOSE_WRITE
158 | inotify
.IN_MOVED_FROM
159 | inotify
.IN_MOVED_TO
161 _ADD_MASK
= _TRIGGER_MASK | inotify
.IN_EXCL_UNLINK | inotify
.IN_ONLYDIR
163 def __init__(self
, context
, monitor
):
164 _BaseThread
.__init
__(self
, context
, monitor
)
166 worktree
= git
.worktree()
167 if worktree
is not None:
168 worktree
= core
.abspath(worktree
)
169 self
._worktree
= worktree
170 self
._git
_dir
= git
.git_path()
172 self
._inotify
_fd
= None
175 self
._worktree
_wd
_to
_path
_map
= {}
176 self
._worktree
_path
_to
_wd
_map
= {}
177 self
._git
_dir
_wd
_to
_path
_map
= {}
178 self
._git
_dir
_path
_to
_wd
_map
= {}
179 self
._git
_dir
_wd
= None
182 def _log_out_of_wds_message():
184 'File system change monitoring: disabled because the'
185 ' limit on the total number of inotify watches was'
186 ' reached. You may be able to increase the limit on'
187 ' the number of watches by running:\n'
189 ' echo fs.inotify.max_user_watches=100000 |'
190 ' sudo tee -a /etc/sysctl.conf &&'
199 self
._inotify
_fd
= inotify
.init()
201 self
._inotify
_fd
= None
202 self
._running
= False
203 if e
.errno
== errno
.EMFILE
:
204 self
._log
_out
_of
_wds
_message
()
206 self
._pipe
_r
, self
._pipe
_w
= os
.pipe()
208 # pylint: disable=no-member
209 poll_obj
= select
.poll()
210 poll_obj
.register(self
._inotify
_fd
, select
.POLLIN
)
211 poll_obj
.register(self
._pipe
_r
, select
.POLLIN
)
216 self
._log
_enabled
_message
()
217 self
._process
_events
(poll_obj
)
221 def _process_events(self
, poll_obj
):
224 timeout
= self
._NOTIFICATION
_DELAY
228 events
= poll_obj
.poll(timeout
)
229 # pylint: disable=duplicate-except
233 if not self
._running
:
239 if fd
== self
._inotify
_fd
:
240 self
._handle
_events
()
242 def _close_fds(self
):
244 if self
._inotify
_fd
is not None:
245 os
.close(self
._inotify
_fd
)
246 self
._inotify
_fd
= None
247 if self
._pipe
_r
is not None:
248 os
.close(self
._pipe
_r
)
250 os
.close(self
._pipe
_w
)
258 if self
._inotify
_fd
is None:
260 context
= self
.context
262 if self
._worktree
is not None:
264 os
.path
.dirname(os
.path
.join(self
._worktree
, path
))
265 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.
317 wd_to_path_map
[wd
] = path
318 path_to_wd_map
[path
] = wd
320 def _check_event(self
, wd
, mask
, name
):
321 if mask
& inotify
.IN_Q_OVERFLOW
:
322 self
._force
_notify
= True
323 elif not mask
& self
._TRIGGER
_MASK
:
325 elif mask
& inotify
.IN_ISDIR
:
327 elif wd
in self
._worktree
_wd
_to
_path
_map
:
328 if self
._use
_check
_ignore
and name
:
330 self
._worktree
_wd
_to
_path
_map
[wd
], core
.decode(name
)
332 self
._file
_paths
.add(path
)
334 self
._force
_notify
= True
335 elif wd
== self
._git
_dir
_wd
:
336 name
= core
.decode(name
)
337 if name
in ('HEAD', 'index'):
338 self
._force
_notify
= True
339 elif name
== 'config':
340 self
._force
_config
= True
341 elif wd
in self
._git
_dir
_wd
_to
_path
_map
and not core
.decode(name
).endswith(
344 self
._force
_notify
= True
346 def _handle_events(self
):
347 for wd
, mask
, _
, name
in inotify
.read_events(self
._inotify
_fd
):
348 if not self
._force
_notify
:
349 self
._check
_event
(wd
, mask
, name
)
352 self
._running
= False
354 if self
._pipe
_w
is not None:
355 os
.write(self
._pipe
_w
, bchr(0))
359 if AVAILABLE
== 'pywin32':
362 def __init__(self
, path
, flags
):
369 self
.handle
= win32file
.CreateFileW(
371 0x0001, # FILE_LIST_DIRECTORY
372 win32con
.FILE_SHARE_READ | win32con
.FILE_SHARE_WRITE
,
374 win32con
.OPEN_EXISTING
,
375 win32con
.FILE_FLAG_BACKUP_SEMANTICS | win32con
.FILE_FLAG_OVERLAPPED
,
379 self
.buffer = win32file
.AllocateReadBuffer(8192)
380 self
.event
= win32event
.CreateEvent(None, True, False, None)
381 self
.overlapped
= pywintypes
.OVERLAPPED()
382 self
.overlapped
.hEvent
= self
.event
384 except Exception: # pylint: disable=broad-exception-caught,broad-except
387 def append(self
, events
):
388 """Append our event to the events list when valid"""
389 if self
.event
is not None:
390 events
.append(self
.event
)
393 if self
.handle
is None:
395 win32file
.ReadDirectoryChangesW(
396 self
.handle
, self
.buffer, True, self
.flags
, self
.overlapped
400 if self
.handle
is None or self
.event
is None:
402 if win32event
.WaitForSingleObject(self
.event
, 0) == win32event
.WAIT_TIMEOUT
:
405 nbytes
= win32file
.GetOverlappedResult(
406 self
.handle
, self
.overlapped
, False
408 result
= win32file
.FILE_NOTIFY_INFORMATION(self
.buffer, nbytes
)
413 if self
.handle
is not None:
414 win32file
.CancelIo(self
.handle
)
415 win32file
.CloseHandle(self
.handle
)
416 if self
.event
is not None:
417 win32file
.CloseHandle(self
.event
)
419 class _Win32Thread(_BaseThread
):
421 win32con
.FILE_NOTIFY_CHANGE_FILE_NAME
422 | win32con
.FILE_NOTIFY_CHANGE_DIR_NAME
423 | win32con
.FILE_NOTIFY_CHANGE_ATTRIBUTES
424 | win32con
.FILE_NOTIFY_CHANGE_SIZE
425 | win32con
.FILE_NOTIFY_CHANGE_LAST_WRITE
426 | win32con
.FILE_NOTIFY_CHANGE_SECURITY
429 def __init__(self
, context
, monitor
):
430 _BaseThread
.__init
__(self
, context
, monitor
)
432 worktree
= git
.worktree()
433 if worktree
is not None:
434 worktree
= self
._transform
_path
(core
.abspath(worktree
))
435 self
._worktree
= worktree
436 self
._worktree
_watch
= None
437 self
._git
_dir
= self
._transform
_path
(core
.abspath(git
.git_path()))
438 self
._git
_dir
_watch
= None
439 self
._stop
_event
_lock
= Lock()
440 self
._stop
_event
= None
443 def _transform_path(path
):
444 return path
.replace('\\', '/').lower()
448 with self
._stop
_event
_lock
:
449 self
._stop
_event
= win32event
.CreateEvent(None, True, False, None)
451 events
= [self
._stop
_event
]
453 if self
._worktree
is not None:
454 self
._worktree
_watch
= _Win32Watch(self
._worktree
, self
._FLAGS
)
455 self
._worktree
_watch
.append(events
)
457 self
._git
_dir
_watch
= _Win32Watch(self
._git
_dir
, self
._FLAGS
)
458 self
._git
_dir
_watch
.append(events
)
460 self
._log
_enabled
_message
()
464 timeout
= self
._NOTIFICATION
_DELAY
466 timeout
= win32event
.INFINITE
467 status
= win32event
.WaitForMultipleObjects(events
, False, timeout
)
468 if not self
._running
:
470 if status
== win32event
.WAIT_TIMEOUT
:
473 self
._handle
_results
()
475 with self
._stop
_event
_lock
:
476 if self
._stop
_event
is not None:
477 win32file
.CloseHandle(self
._stop
_event
)
478 self
._stop
_event
= None
479 if self
._worktree
_watch
is not None:
480 self
._worktree
_watch
.close()
481 if self
._git
_dir
_watch
is not None:
482 self
._git
_dir
_watch
.close()
484 def _handle_results(self
):
485 if self
._worktree
_watch
is not None:
486 for _
, path
in self
._worktree
_watch
.read():
487 if not self
._running
:
489 if self
._force
_notify
:
491 path
= self
._worktree
+ '/' + self
._transform
_path
(path
)
493 path
!= self
._git
_dir
494 and not path
.startswith(self
._git
_dir
+ '/')
495 and not os
.path
.isdir(path
)
497 if self
._use
_check
_ignore
:
498 self
._file
_paths
.add(path
)
500 self
._force
_notify
= True
501 for _
, path
in self
._git
_dir
_watch
.read():
502 if not self
._running
:
504 if self
._force
_notify
:
506 path
= self
._transform
_path
(path
)
507 if path
.endswith('.lock'):
510 self
._force
_config
= True
512 if path
== 'head' or path
== 'index' or path
.startswith('refs/'):
513 self
._force
_notify
= True
516 self
._running
= False
517 with self
._stop
_event
_lock
:
518 if self
._stop
_event
is not None:
519 win32event
.SetEvent(self
._stop
_event
)
526 if not cfg
.get('cola.inotify', default
=True):
528 'File system change monitoring: disabled because'
529 ' "cola.inotify" is false.\n'
532 elif AVAILABLE
== 'inotify':
533 thread_class
= _InotifyThread
534 elif AVAILABLE
== 'pywin32':
535 thread_class
= _Win32Thread
539 'File system change monitoring: disabled because pywin32'
540 ' is not installed.\n'
543 elif utils
.is_linux():
545 'File system change monitoring: disabled because libc'
546 ' does not support the inotify system calls.\n'
549 return _Monitor(context
, thread_class
)