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
10 from pyinotify
import ProcessEvent
11 from pyinotify
import WatchManager
12 from pyinotify
import Notifier
13 from pyinotify
import EventsCodes
19 from cola
import utils
31 from PyQt4
import QtCore
33 from cola
import gitcfg
36 from cola
.compat
import set
37 from cola
.git
import STDOUT
38 from cola
.i18n
import N_
39 from cola
.interaction
import Interaction
40 from cola
.models
import main
48 cfg
= gitcfg
.instance()
49 if not cfg
.get('cola.inotify', True):
50 msg
= N_('inotify is disabled because "cola.inotify" is false')
56 msg
= N_('file notification: disabled\n'
57 'Note: install pywin32 to enable.\n')
58 elif utils
.is_linux():
59 msg
= N_('inotify: disabled\n'
60 'Note: install python-pyinotify to enable inotify.\n')
65 msg
+= N_('On Debian systems '
66 'try: sudo aptitude install python-pyinotify')
70 # Start the notification thread
71 _thread
= GitNotifier()
74 msg
= N_('File notification enabled.')
76 msg
= N_('inotify enabled.')
88 """Return True if pyinotify is available."""
89 return AVAILABLE
and _thread
and _thread
.isRunning()
93 """Queues filesystem events for broadcast"""
96 """Create an event handler"""
97 ## Timer used to prevent notification floods
99 ## Lock to protect files and timer from threading issues
103 """Broadcasts a list of all files touched since last broadcast"""
105 cmds
.do(cmds
.UpdateFileStatus
)
108 def handle(self
, path
):
109 """Queues up filesystem events for broadcast"""
111 if self
._timer
is None:
112 self
._timer
= Timer(0.888, self
.broadcast
)
116 class FileSysEvent(ProcessEvent
):
117 """Generated by GitNotifier in response to inotify events"""
120 """Maintain event state"""
121 ProcessEvent
.__init
__(self
)
122 ## Takes care of Queueing events for broadcast
123 self
._handler
= Handler()
125 def process_default(self
, event
):
126 """Queues up inotify events for broadcast"""
129 path
= os
.path
.join(event
.path
, event
.name
)
130 if os
.path
.exists(path
):
131 path
= os
.path
.relpath(path
)
132 self
._handler
.handle(path
)
135 class GitNotifier(QtCore
.QThread
):
136 """Polls inotify for changes and generates FileSysEvents"""
138 def __init__(self
, timeout
=333):
139 """Set up the pyinotify thread"""
140 QtCore
.QThread
.__init
__(self
)
141 ## Git command object
142 self
._git
= main
.model().git
144 self
._timeout
= timeout
146 self
._path
= self
._git
.worktree()
147 ## Signals thread termination
149 ## Directories to watching
150 self
._dirs
_seen
= set()
151 ## The inotify watch manager instantiated in run()
155 self
._mask
= (EventsCodes
.ALL_FLAGS
['IN_ATTRIB'] |
156 EventsCodes
.ALL_FLAGS
['IN_CLOSE_WRITE'] |
157 EventsCodes
.ALL_FLAGS
['IN_DELETE'] |
158 EventsCodes
.ALL_FLAGS
['IN_MODIFY'] |
159 EventsCodes
.ALL_FLAGS
['IN_MOVED_TO'])
161 def stop(self
, stopped
):
162 """Tells the GitNotifier to stop"""
164 self
._running
= not stopped
166 def _watch_directory(self
, directory
):
167 """Set up a directory for monitoring by inotify"""
170 directory
= core
.realpath(directory
)
171 if not core
.exists(directory
):
173 if directory
not in self
._dirs
_seen
:
174 self
._wmgr
.add_watch(directory
, self
._mask
)
175 self
._dirs
_seen
.add(directory
)
177 def _is_pyinotify_08x(self
):
178 """Is this pyinotify 0.8.x?
180 The pyinotify API changed between 0.7.x and 0.8.x.
181 This allows us to maintain backwards compatibility.
183 if hasattr(pyinotify
, '__version__'):
184 if pyinotify
.__version
__[:3] == '0.8':
189 """Create the inotify WatchManager and generate FileSysEvents"""
195 # Only capture events that git cares about
196 self
._wmgr
= WatchManager()
197 if self
._is
_pyinotify
_08x
():
198 notifier
= Notifier(self
._wmgr
, FileSysEvent(),
199 timeout
=self
._timeout
)
201 notifier
= Notifier(self
._wmgr
, FileSysEvent())
203 self
._watch
_directory
(self
._path
)
205 # Register files/directories known to git
206 for filename
in self
._git
.ls_files()[STDOUT
].splitlines():
207 filename
= core
.realpath(filename
)
208 directory
= os
.path
.dirname(filename
)
209 self
._watch
_directory
(directory
)
211 # self._running signals app termination. The timeout is a tradeoff
212 # between fast notification response and waiting too long to exit.
214 if self
._is
_pyinotify
_08x
():
215 check
= notifier
.check_events()
217 check
= notifier
.check_events(timeout
=self
._timeout
)
218 if not self
._running
:
221 notifier
.read_events()
222 notifier
.process_events()
226 """Generate notifications using pywin32"""
228 hdir
= win32file
.CreateFile(
231 win32con
.FILE_SHARE_READ | win32con
.FILE_SHARE_WRITE
,
233 win32con
.OPEN_EXISTING
,
234 win32con
.FILE_FLAG_BACKUP_SEMANTICS |
235 win32con
.FILE_FLAG_OVERLAPPED
,
238 flags
= (win32con
.FILE_NOTIFY_CHANGE_FILE_NAME |
239 win32con
.FILE_NOTIFY_CHANGE_DIR_NAME |
240 win32con
.FILE_NOTIFY_CHANGE_ATTRIBUTES |
241 win32con
.FILE_NOTIFY_CHANGE_SIZE |
242 win32con
.FILE_NOTIFY_CHANGE_LAST_WRITE |
243 win32con
.FILE_NOTIFY_CHANGE_SECURITY
)
245 buf
= win32file
.AllocateReadBuffer(8192)
246 overlapped
= pywintypes
.OVERLAPPED()
247 overlapped
.hEvent
= win32event
.CreateEvent(None, 0, 0, None)
251 win32file
.ReadDirectoryChangesW(hdir
, buf
, True, flags
, overlapped
)
253 rc
= win32event
.WaitForSingleObject(overlapped
.hEvent
,
255 if rc
!= win32event
.WAIT_OBJECT_0
:
257 nbytes
= win32file
.GetOverlappedResult(hdir
, overlapped
, True)
260 results
= win32file
.FILE_NOTIFY_INFORMATION(buf
, nbytes
)
261 for action
, path
in results
:
262 if not self
._running
:
264 path
= path
.replace('\\', '/')
265 if (not path
.startswith('.git/') and
266 '/.git/' not in path
and os
.path
.isfile(path
)):