1 # Copyright (c) 2008 David Aguilar
2 """Provides an inotify plugin for Linux and other systems with pyinotify"""
5 from threading
import Timer
6 from threading
import Lock
11 from pyinotify
import ProcessEvent
12 from pyinotify
import WatchManager
13 from pyinotify
import Notifier
14 from pyinotify
import EventsCodes
31 from PyQt4
import QtCore
35 from cola
import signals
36 from cola
.compat
import set
43 msg
= ('file notification: disabled\n'
44 'Note: install pywin32 to enable.\n')
45 elif utils
.is_linux():
46 msg
= ('inotify: disabled\n'
47 'Note: install python-pyinotify to enable inotify.\n')
52 msg
+= ('On Debian systems '
53 'try: sudo aptitude install python-pyinotify')
54 cola
.notifier().broadcast(signals
.log_cmd
, 0, msg
)
57 # Start the notification thread
58 _thread
= GitNotifier()
61 msg
= 'file notification: enabled'
63 msg
= 'inotify support: enabled'
64 cola
.notifier().broadcast(signals
.log_cmd
, 0, msg
)
74 """Return True if pyinotify is available."""
75 return AVAILABLE
and _thread
and _thread
.isRunning()
79 """Queues filesystem events for broadcast"""
82 """Create an event handler"""
83 ## Timer used to prevent notification floods
85 ## Lock to protect files and timer from threading issues
89 """Broadcasts a list of all files touched since last broadcast"""
91 cola
.notifier().broadcast(signals
.update_file_status
)
94 def handle(self
, path
):
95 """Queues up filesystem events for broadcast"""
97 if self
._timer
is None:
98 self
._timer
= Timer(0.333, self
.broadcast
)
102 class FileSysEvent(ProcessEvent
):
103 """Generated by GitNotifier in response to inotify events"""
106 """Maintain event state"""
107 ProcessEvent
.__init
__(self
)
108 ## Takes care of Queueing events for broadcast
109 self
._handler
= Handler()
111 def process_default(self
, event
):
112 """Queues up inotify events for broadcast"""
113 if event
.name
is not None:
114 path
= os
.path
.join(event
.path
, event
.name
)
115 path
= os
.path
.relpath(path
)
116 self
._handler
.handle(path
)
119 class GitNotifier(QtCore
.QThread
):
120 """Polls inotify for changes and generates FileSysEvents"""
122 def __init__(self
, timeout
=333):
123 """Set up the pyinotify thread"""
124 QtCore
.QThread
.__init
__(self
)
125 ## Git command object
126 self
._git
= cola
.model().git
128 self
._timeout
= timeout
130 self
._path
= self
._git
.worktree()
131 ## Signals thread termination
133 ## Directories to watching
134 self
._dirs
_seen
= set()
135 ## The inotify watch manager instantiated in run()
139 self
._mask
= (EventsCodes
.ALL_FLAGS
['IN_ATTRIB'] |
140 EventsCodes
.ALL_FLAGS
['IN_CLOSE_WRITE'] |
141 EventsCodes
.ALL_FLAGS
['IN_DELETE'] |
142 EventsCodes
.ALL_FLAGS
['IN_MODIFY'] |
143 EventsCodes
.ALL_FLAGS
['IN_MOVED_TO'])
145 def stop(self
, stopped
):
146 """Tells the GitNotifier to stop"""
148 self
._running
= not stopped
150 def _watch_directory(self
, directory
):
151 """Set up a directory for monitoring by inotify"""
154 directory
= os
.path
.realpath(directory
)
155 if not os
.path
.exists(directory
):
157 if directory
not in self
._dirs
_seen
:
158 self
._wmgr
.add_watch(directory
, self
._mask
)
159 self
._dirs
_seen
.add(directory
)
161 def _is_pyinotify_08x(self
):
162 """Is this pyinotify 0.8.x?
164 The pyinotify API changed between 0.7.x and 0.8.x.
165 This allows us to maintain backwards compatibility.
167 if hasattr(pyinotify
, '__version__'):
168 if pyinotify
.__version
__[:3] == '0.8':
173 """Create the inotify WatchManager and generate FileSysEvents"""
179 # Only capture events that git cares about
180 self
._wmgr
= WatchManager()
181 if self
._is
_pyinotify
_08x
():
182 notifier
= Notifier(self
._wmgr
, FileSysEvent(),
183 timeout
=self
._timeout
)
185 notifier
= Notifier(self
._wmgr
, FileSysEvent())
187 self
._watch
_directory
(self
._path
)
189 # Register files/directories known to git
190 for filename
in core
.decode(self
._git
.ls_files()).splitlines():
191 filename
= os
.path
.realpath(filename
)
192 directory
= os
.path
.dirname(filename
)
193 self
._watch
_directory
(directory
)
195 # self._running signals app termination. The timeout is a tradeoff
196 # between fast notification response and waiting too long to exit.
198 if self
._is
_pyinotify
_08x
():
199 check
= notifier
.check_events()
201 check
= notifier
.check_events(timeout
=self
._timeout
)
202 if not self
._running
:
205 notifier
.read_events()
206 notifier
.process_events()
210 """Generate notifications using pywin32"""
212 hDir
= win32file
.CreateFile(
215 win32con
.FILE_SHARE_READ | win32con
.FILE_SHARE_WRITE
,
217 win32con
.OPEN_EXISTING
,
218 win32con
.FILE_FLAG_BACKUP_SEMANTICS |
219 win32con
.FILE_FLAG_OVERLAPPED
,
222 flags
= (win32con
.FILE_NOTIFY_CHANGE_FILE_NAME |
223 win32con
.FILE_NOTIFY_CHANGE_DIR_NAME |
224 win32con
.FILE_NOTIFY_CHANGE_ATTRIBUTES |
225 win32con
.FILE_NOTIFY_CHANGE_SIZE |
226 win32con
.FILE_NOTIFY_CHANGE_LAST_WRITE |
227 win32con
.FILE_NOTIFY_CHANGE_SECURITY
)
229 buf
= win32file
.AllocateReadBuffer(8192)
230 overlapped
= pywintypes
.OVERLAPPED()
231 overlapped
.hEvent
= win32event
.CreateEvent(None, 0, 0, None)
235 win32file
.ReadDirectoryChangesW(hDir
,
241 rc
= win32event
.WaitForSingleObject(overlapped
.hEvent
, self
._timeout
)
242 if rc
== win32event
.WAIT_OBJECT_0
:
243 nbytes
= win32file
.GetOverlappedResult(hDir
, overlapped
, True)
245 results
= win32file
.FILE_NOTIFY_INFORMATION(buf
, nbytes
)
246 for action
, path
in results
:
247 if not self
._running
:
250 path
= path
.replace('\\', '/')
251 if not path
.startswith('.git/') and os
.path
.isfile(path
):