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 division
, absolute_import
, 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(['git', 'check-ignore', '--verbose',
112 '--non-matching', '-z', '--stdin'])
113 path_list
= bchr(0).join(core
.encode(path
)
114 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 |
161 inotify
.IN_EXCL_UNLINK |
165 def __init__(self
, context
, monitor
):
166 _BaseThread
.__init
__(self
, context
, monitor
)
168 worktree
= git
.worktree()
169 if worktree
is not None:
170 worktree
= core
.abspath(worktree
)
171 self
._worktree
= worktree
172 self
._git
_dir
= git
.git_path()
174 self
._inotify
_fd
= None
177 self
._worktree
_wd
_to
_path
_map
= {}
178 self
._worktree
_path
_to
_wd
_map
= {}
179 self
._git
_dir
_wd
_to
_path
_map
= {}
180 self
._git
_dir
_path
_to
_wd
_map
= {}
181 self
._git
_dir
_wd
= None
184 def _log_out_of_wds_message():
185 msg
= N_('File system change monitoring: disabled because the'
186 ' limit on the total number of inotify watches was'
187 ' reached. You may be able to increase the limit on'
188 ' the number of watches by running:\n'
190 ' echo fs.inotify.max_user_watches=100000 |'
191 ' sudo tee -a /etc/sysctl.conf &&'
198 self
._inotify
_fd
= inotify
.init()
199 self
._pipe
_r
, self
._pipe
_w
= os
.pipe()
201 poll_obj
= select
.poll()
202 poll_obj
.register(self
._inotify
_fd
, select
.POLLIN
)
203 poll_obj
.register(self
._pipe
_r
, select
.POLLIN
)
207 self
._log
_enabled
_message
()
208 self
._process
_events
(poll_obj
)
212 def _process_events(self
, poll_obj
):
215 timeout
= self
._NOTIFICATION
_DELAY
219 events
= poll_obj
.poll(timeout
)
220 # pylint: disable=duplicate-except
221 except (OSError, select
.error
):
224 if not self
._running
:
229 for (fd
, _
) in events
:
230 if fd
== self
._inotify
_fd
:
231 self
._handle
_events
()
233 def _close_fds(self
):
235 if self
._inotify
_fd
is not None:
236 os
.close(self
._inotify
_fd
)
237 self
._inotify
_fd
= None
238 if self
._pipe
_r
is not None:
239 os
.close(self
._pipe
_r
)
241 os
.close(self
._pipe
_w
)
249 if self
._inotify
_fd
is None:
251 context
= self
.context
253 if self
._worktree
is not None:
255 os
.path
.dirname(os
.path
.join(self
._worktree
, path
))
256 for path
in gitcmds
.tracked_files(context
)])
257 self
._refresh
_watches
(tracked_dirs
,
258 self
._worktree
_wd
_to
_path
_map
,
259 self
._worktree
_path
_to
_wd
_map
)
261 git_dirs
.add(self
._git
_dir
)
262 for dirpath
, _
, _
in core
.walk(
263 os
.path
.join(self
._git
_dir
, 'refs')):
264 git_dirs
.add(dirpath
)
265 self
._refresh
_watches
(git_dirs
,
266 self
._git
_dir
_wd
_to
_path
_map
,
267 self
._git
_dir
_path
_to
_wd
_map
)
269 self
._git
_dir
_path
_to
_wd
_map
.get(self
._git
_dir
)
271 if e
.errno
== errno
.ENOSPC
:
272 self
._log
_out
_of
_wds
_message
()
273 self
._running
= False
277 def _refresh_watches(self
, paths_to_watch
, wd_to_path_map
,
279 watched_paths
= set(path_to_wd_map
)
280 for path
in watched_paths
- paths_to_watch
:
281 wd
= path_to_wd_map
.pop(path
)
282 wd_to_path_map
.pop(wd
)
284 inotify
.rm_watch(self
._inotify
_fd
, wd
)
286 if e
.errno
== errno
.EINVAL
:
287 # This error can occur if the target of the wd was
288 # removed on the filesystem before we call
289 # inotify.rm_watch() so ignore it.
292 for path
in paths_to_watch
- watched_paths
:
294 wd
= inotify
.add_watch(self
._inotify
_fd
, core
.encode(path
),
297 if e
.errno
in (errno
.ENOENT
, errno
.ENOTDIR
):
298 # These two errors should only occur as a result of
299 # race conditions: the first if the directory
300 # referenced by path was removed or renamed before the
301 # call to inotify.add_watch(); the second if the
302 # directory referenced by path was replaced with a file
303 # before the call to inotify.add_watch(). Therefore we
304 # simply ignore them.
308 wd_to_path_map
[wd
] = path
309 path_to_wd_map
[path
] = wd
311 def _check_event(self
, wd
, mask
, name
):
312 if mask
& inotify
.IN_Q_OVERFLOW
:
313 self
._force
_notify
= True
314 elif not mask
& self
._TRIGGER
_MASK
:
316 elif mask
& inotify
.IN_ISDIR
:
318 elif wd
in self
._worktree
_wd
_to
_path
_map
:
319 if self
._use
_check
_ignore
and name
:
320 path
= os
.path
.join(self
._worktree
_wd
_to
_path
_map
[wd
],
322 self
._file
_paths
.add(path
)
324 self
._force
_notify
= True
325 elif wd
== self
._git
_dir
_wd
:
326 name
= core
.decode(name
)
327 if name
in ('HEAD', 'index'):
328 self
._force
_notify
= True
329 elif name
== 'config':
330 self
._force
_config
= True
331 elif (wd
in self
._git
_dir
_wd
_to
_path
_map
332 and not core
.decode(name
).endswith('.lock')):
333 self
._force
_notify
= True
335 def _handle_events(self
):
336 for wd
, mask
, _
, name
in \
337 inotify
.read_events(self
._inotify
_fd
):
338 if not self
._force
_notify
:
339 self
._check
_event
(wd
, mask
, name
)
342 self
._running
= False
344 if self
._pipe
_w
is not None:
345 os
.write(self
._pipe
_w
, bchr(0))
349 if AVAILABLE
== 'pywin32':
351 class _Win32Watch(object):
353 def __init__(self
, path
, flags
):
360 self
.handle
= win32file
.CreateFileW(
362 0x0001, # FILE_LIST_DIRECTORY
363 win32con
.FILE_SHARE_READ | win32con
.FILE_SHARE_WRITE
,
365 win32con
.OPEN_EXISTING
,
366 win32con
.FILE_FLAG_BACKUP_SEMANTICS |
367 win32con
.FILE_FLAG_OVERLAPPED
,
370 self
.buffer = win32file
.AllocateReadBuffer(8192)
371 self
.event
= win32event
.CreateEvent(None, True, False, None)
372 self
.overlapped
= pywintypes
.OVERLAPPED()
373 self
.overlapped
.hEvent
= self
.event
380 win32file
.ReadDirectoryChangesW(self
.handle
, self
.buffer, True,
381 self
.flags
, self
.overlapped
)
384 if win32event
.WaitForSingleObject(self
.event
, 0) \
385 == win32event
.WAIT_TIMEOUT
:
388 nbytes
= win32file
.GetOverlappedResult(self
.handle
,
389 self
.overlapped
, False)
390 result
= win32file
.FILE_NOTIFY_INFORMATION(self
.buffer, nbytes
)
395 if self
.handle
is not None:
396 win32file
.CancelIo(self
.handle
)
397 win32file
.CloseHandle(self
.handle
)
398 if self
.event
is not None:
399 win32file
.CloseHandle(self
.event
)
401 class _Win32Thread(_BaseThread
):
402 _FLAGS
= (win32con
.FILE_NOTIFY_CHANGE_FILE_NAME |
403 win32con
.FILE_NOTIFY_CHANGE_DIR_NAME |
404 win32con
.FILE_NOTIFY_CHANGE_ATTRIBUTES |
405 win32con
.FILE_NOTIFY_CHANGE_SIZE |
406 win32con
.FILE_NOTIFY_CHANGE_LAST_WRITE |
407 win32con
.FILE_NOTIFY_CHANGE_SECURITY
)
409 def __init__(self
, context
, monitor
):
410 _BaseThread
.__init
__(self
, context
, monitor
)
412 worktree
= git
.worktree()
413 if worktree
is not None:
414 worktree
= self
._transform
_path
(core
.abspath(worktree
))
415 self
._worktree
= worktree
416 self
._worktree
_watch
= None
417 self
._git
_dir
= self
._transform
_path
(core
.abspath(git
.git_path()))
418 self
._git
_dir
_watch
= None
419 self
._stop
_event
_lock
= Lock()
420 self
._stop
_event
= None
423 def _transform_path(path
):
424 return path
.replace('\\', '/').lower()
428 with self
._stop
_event
_lock
:
429 self
._stop
_event
= win32event
.CreateEvent(None, True,
432 events
= [self
._stop
_event
]
434 if self
._worktree
is not None:
435 self
._worktree
_watch
= _Win32Watch(self
._worktree
,
437 events
.append(self
._worktree
_watch
.event
)
439 self
._git
_dir
_watch
= _Win32Watch(self
._git
_dir
, self
._FLAGS
)
440 events
.append(self
._git
_dir
_watch
.event
)
442 self
._log
_enabled
_message
()
446 timeout
= self
._NOTIFICATION
_DELAY
448 timeout
= win32event
.INFINITE
449 rc
= win32event
.WaitForMultipleObjects(events
, False,
451 if not self
._running
:
453 elif rc
== win32event
.WAIT_TIMEOUT
:
456 self
._handle
_results
()
458 with self
._stop
_event
_lock
:
459 if self
._stop
_event
is not None:
460 win32file
.CloseHandle(self
._stop
_event
)
461 self
._stop
_event
= None
462 if self
._worktree
_watch
is not None:
463 self
._worktree
_watch
.close()
464 if self
._git
_dir
_watch
is not None:
465 self
._git
_dir
_watch
.close()
467 def _handle_results(self
):
468 if self
._worktree
_watch
is not None:
469 for _
, path
in self
._worktree
_watch
.read():
470 if not self
._running
:
472 if self
._force
_notify
:
474 path
= self
._worktree
+ '/' + self
._transform
_path
(path
)
475 if (path
!= self
._git
_dir
476 and not path
.startswith(self
._git
_dir
+ '/')
477 and not os
.path
.isdir(path
)):
478 if self
._use
_check
_ignore
:
479 self
._file
_paths
.add(path
)
481 self
._force
_notify
= True
482 for _
, path
in self
._git
_dir
_watch
.read():
483 if not self
._running
:
485 if self
._force
_notify
:
487 path
= self
._transform
_path
(path
)
488 if path
.endswith('.lock'):
491 self
._force
_config
= True
495 or path
.startswith('refs/')):
496 self
._force
_notify
= True
499 self
._running
= False
500 with self
._stop
_event
_lock
:
501 if self
._stop
_event
is not None:
502 win32event
.SetEvent(self
._stop
_event
)
509 if not cfg
.get('cola.inotify', default
=True):
510 msg
= N_('File system change monitoring: disabled because'
511 ' "cola.inotify" is false.\n')
513 elif AVAILABLE
== 'inotify':
514 thread_class
= _InotifyThread
515 elif AVAILABLE
== 'pywin32':
516 thread_class
= _Win32Thread
519 msg
= N_('File system change monitoring: disabled because pywin32'
520 ' is not installed.\n')
522 elif utils
.is_linux():
523 msg
= N_('File system change monitoring: disabled because libc'
524 ' does not support the inotify system calls.\n')
526 return _Monitor(context
, thread_class
)