]> git.rkrishnan.org Git - tahoe-lafs/tahoe-lafs.git/blob - src/allmydata/windows/inotify.py
1a719f9afd9a2fb51fdd96e0ef97ad2c0a270cc1
[tahoe-lafs/tahoe-lafs.git] / src / allmydata / windows / inotify.py
1
2 # Windows near-equivalent to twisted.internet.inotify
3 # This should only be imported on Windows.
4
5 import os, sys
6
7 from twisted.internet import reactor
8 from twisted.internet.threads import deferToThread
9
10 from allmydata.util.fake_inotify import humanReadableMask, \
11     IN_WATCH_MASK, IN_ACCESS, IN_MODIFY, IN_ATTRIB, IN_CLOSE_NOWRITE, IN_CLOSE_WRITE, \
12     IN_OPEN, IN_MOVED_FROM, IN_MOVED_TO, IN_CREATE, IN_DELETE, IN_DELETE_SELF, \
13     IN_MOVE_SELF, IN_UNMOUNT, IN_Q_OVERFLOW, IN_IGNORED, IN_ONLYDIR, IN_DONT_FOLLOW, \
14     IN_MASK_ADD, IN_ISDIR, IN_ONESHOT, IN_CLOSE, IN_MOVED, IN_CHANGED
15 [humanReadableMask, \
16     IN_WATCH_MASK, IN_ACCESS, IN_MODIFY, IN_ATTRIB, IN_CLOSE_NOWRITE, IN_CLOSE_WRITE, \
17     IN_OPEN, IN_MOVED_FROM, IN_MOVED_TO, IN_CREATE, IN_DELETE, IN_DELETE_SELF, \
18     IN_MOVE_SELF, IN_UNMOUNT, IN_Q_OVERFLOW, IN_IGNORED, IN_ONLYDIR, IN_DONT_FOLLOW, \
19     IN_MASK_ADD, IN_ISDIR, IN_ONESHOT, IN_CLOSE, IN_MOVED, IN_CHANGED]
20
21 from allmydata.util.assertutil import _assert, precondition
22 from allmydata.util.encodingutil import quote_output
23 from allmydata.util import log, fileutil
24 from allmydata.util.pollmixin import PollMixin
25
26 from ctypes import WINFUNCTYPE, WinError, windll, POINTER, byref, create_string_buffer, addressof
27 from ctypes.wintypes import BOOL, HANDLE, DWORD, LPCWSTR, LPVOID
28
29 # <http://msdn.microsoft.com/en-us/library/gg258116%28v=vs.85%29.aspx>
30 FILE_LIST_DIRECTORY              = 1
31
32 # <http://msdn.microsoft.com/en-us/library/aa363858%28v=vs.85%29.aspx>
33 CreateFileW = WINFUNCTYPE(HANDLE, LPCWSTR, DWORD, DWORD, LPVOID, DWORD, DWORD, HANDLE) \
34                   (("CreateFileW", windll.kernel32))
35
36 FILE_SHARE_READ                  = 0x00000001
37 FILE_SHARE_WRITE                 = 0x00000002
38 FILE_SHARE_DELETE                = 0x00000004
39
40 OPEN_EXISTING                    = 3
41
42 FILE_FLAG_BACKUP_SEMANTICS       = 0x02000000
43
44 # <http://msdn.microsoft.com/en-us/library/ms724211%28v=vs.85%29.aspx>
45 CloseHandle = WINFUNCTYPE(BOOL, HANDLE)(("CloseHandle", windll.kernel32))
46
47 # <http://msdn.microsoft.com/en-us/library/aa365465%28v=vs.85%29.aspx>
48 ReadDirectoryChangesW = WINFUNCTYPE(BOOL, HANDLE, LPVOID, DWORD, BOOL, DWORD, POINTER(DWORD), LPVOID, LPVOID) \
49                             (("ReadDirectoryChangesW", windll.kernel32))
50
51 FILE_NOTIFY_CHANGE_FILE_NAME     = 0x00000001
52 FILE_NOTIFY_CHANGE_DIR_NAME      = 0x00000002
53 FILE_NOTIFY_CHANGE_ATTRIBUTES    = 0x00000004
54 #FILE_NOTIFY_CHANGE_SIZE         = 0x00000008
55 FILE_NOTIFY_CHANGE_LAST_WRITE    = 0x00000010
56 FILE_NOTIFY_CHANGE_LAST_ACCESS   = 0x00000020
57 #FILE_NOTIFY_CHANGE_CREATION     = 0x00000040
58 FILE_NOTIFY_CHANGE_SECURITY      = 0x00000100
59
60 # <http://msdn.microsoft.com/en-us/library/aa364391%28v=vs.85%29.aspx>
61 FILE_ACTION_ADDED                = 0x00000001
62 FILE_ACTION_REMOVED              = 0x00000002
63 FILE_ACTION_MODIFIED             = 0x00000003
64 FILE_ACTION_RENAMED_OLD_NAME     = 0x00000004
65 FILE_ACTION_RENAMED_NEW_NAME     = 0x00000005
66
67 _action_to_string = {
68     FILE_ACTION_ADDED            : "FILE_ACTION_ADDED",
69     FILE_ACTION_REMOVED          : "FILE_ACTION_REMOVED",
70     FILE_ACTION_MODIFIED         : "FILE_ACTION_MODIFIED",
71     FILE_ACTION_RENAMED_OLD_NAME : "FILE_ACTION_RENAMED_OLD_NAME",
72     FILE_ACTION_RENAMED_NEW_NAME : "FILE_ACTION_RENAMED_NEW_NAME",
73 }
74
75 _action_to_inotify_mask = {
76     FILE_ACTION_ADDED            : IN_CREATE,
77     FILE_ACTION_REMOVED          : IN_DELETE,
78     FILE_ACTION_MODIFIED         : IN_CHANGED,
79     FILE_ACTION_RENAMED_OLD_NAME : IN_MOVED_FROM,
80     FILE_ACTION_RENAMED_NEW_NAME : IN_MOVED_TO,
81 }
82
83 INVALID_HANDLE_VALUE             = 0xFFFFFFFF
84
85
86 class Event(object):
87     """
88     * action:   a FILE_ACTION_* constant (not a bit mask)
89     * filename: a Unicode string, giving the name relative to the watched directory
90     """
91     def __init__(self, action, filename):
92         self.action = action
93         self.filename = filename
94
95     def __repr__(self):
96         return "Event(%r, %r)" % (_action_to_string.get(self.action, self.action), self.filename)
97
98
99 class FileNotifyInformation(object):
100     """
101     I represent a buffer containing FILE_NOTIFY_INFORMATION structures, and can
102     iterate over those structures, decoding them into Event objects.
103     """
104
105     def __init__(self, size=1024):
106         self.size = size
107         self.buffer = create_string_buffer(size)
108         address = addressof(self.buffer)
109         _assert(address & 3 == 0, "address 0x%X returned by create_string_buffer is not DWORD-aligned" % (address,))
110         self.data = None
111
112     def read_changes(self, hDirectory, recursive, filter):
113         bytes_returned = DWORD(0)
114         r = ReadDirectoryChangesW(hDirectory,
115                                   self.buffer,
116                                   self.size,
117                                   recursive,
118                                   filter,
119                                   byref(bytes_returned),
120                                   None,  # NULL -> no overlapped I/O
121                                   None   # NULL -> no completion routine
122                                  )
123         if r == 0:
124             raise WinError()
125         self.data = self.buffer.raw[:bytes_returned.value]
126
127     def __iter__(self):
128         # Iterator implemented as generator: <http://docs.python.org/library/stdtypes.html#generator-types>
129         pos = 0
130         while True:
131             bytes = self._read_dword(pos+8)
132             s = Event(self._read_dword(pos+4),
133                       self.data[pos+12 : pos+12+bytes].decode('utf-16-le'))
134
135             next_entry_offset = self._read_dword(pos)
136             yield s
137             if next_entry_offset == 0:
138                 break
139             pos = pos + next_entry_offset
140
141     def _read_dword(self, i):
142         # little-endian
143         return ( ord(self.data[i])          |
144                 (ord(self.data[i+1]) <<  8) |
145                 (ord(self.data[i+2]) << 16) |
146                 (ord(self.data[i+3]) << 24))
147
148
149 def _open_directory(path_u):
150     hDirectory = CreateFileW(path_u,
151                              FILE_LIST_DIRECTORY,         # access rights
152                              FILE_SHARE_READ | FILE_SHARE_WRITE | FILE_SHARE_DELETE,
153                                                           # don't prevent other processes from accessing
154                              None,                        # no security descriptor
155                              OPEN_EXISTING,               # directory must already exist
156                              FILE_FLAG_BACKUP_SEMANTICS,  # necessary to open a directory
157                              None                         # no template file
158                             )
159     if hDirectory == INVALID_HANDLE_VALUE:
160         e = WinError()
161         raise OSError("Opening directory %s gave Windows error %r: %s" % (quote_output(path_u), e.args[0], e.args[1]))
162     return hDirectory
163
164
165 def simple_test():
166     path_u = u"test"
167     filter = FILE_NOTIFY_CHANGE_FILE_NAME | FILE_NOTIFY_CHANGE_DIR_NAME | FILE_NOTIFY_CHANGE_LAST_WRITE
168     recursive = False
169
170     hDirectory = _open_directory(path_u)
171     fni = FileNotifyInformation()
172     print "Waiting..."
173     while True:
174         fni.read_changes(hDirectory, recursive, filter)
175         print repr(fni.data)
176         for info in fni:
177             print info
178
179
180 NOT_STARTED = "NOT_STARTED"
181 STARTED     = "STARTED"
182 STOPPING    = "STOPPING"
183 STOPPED     = "STOPPED"
184
185 class INotify(PollMixin):
186     def __init__(self):
187         self._state = NOT_STARTED
188         self._filter = None
189         self._callbacks = None
190         self._hDirectory = None
191         self._path = None
192         self._pending = set()
193         self._pending_delay = 1.0
194
195     def set_pending_delay(self, delay):
196         self._pending_delay = delay
197
198     def startReading(self):
199         deferToThread(self._thread)
200         return self.poll(lambda: self._state != NOT_STARTED)
201
202     def stopReading(self):
203         # FIXME race conditions
204         if self._state != STOPPED:
205             self._state = STOPPING
206
207     def wait_until_stopped(self):
208         fileutil.write(os.path.join(self._path.path, u".ignore-me"), "")
209         return self.poll(lambda: self._state == STOPPED)
210
211     def watch(self, path, mask=IN_WATCH_MASK, autoAdd=False, callbacks=None, recursive=False):
212         precondition(self._state == NOT_STARTED, "watch() can only be called before startReading()", state=self._state)
213         precondition(self._filter is None, "only one watch is supported")
214         precondition(isinstance(autoAdd, bool), autoAdd=autoAdd)
215         precondition(isinstance(recursive, bool), recursive=recursive)
216         precondition(autoAdd == recursive, "need autoAdd and recursive to be the same", autoAdd=autoAdd, recursive=recursive)
217
218         self._path = path
219         path_u = path.path
220         if not isinstance(path_u, unicode):
221             path_u = path_u.decode(sys.getfilesystemencoding())
222             _assert(isinstance(path_u, unicode), path_u=path_u)
223
224         self._filter = FILE_NOTIFY_CHANGE_FILE_NAME | FILE_NOTIFY_CHANGE_DIR_NAME | FILE_NOTIFY_CHANGE_LAST_WRITE
225
226         if mask & (IN_ACCESS | IN_CLOSE_NOWRITE | IN_OPEN):
227             self._filter = self._filter | FILE_NOTIFY_CHANGE_LAST_ACCESS
228         if mask & IN_ATTRIB:
229             self._filter = self._filter | FILE_NOTIFY_CHANGE_ATTRIBUTES | FILE_NOTIFY_CHANGE_SECURITY
230
231         self._recursive = recursive
232         self._callbacks = callbacks or []
233         self._hDirectory = _open_directory(path_u)
234
235     def _thread(self):
236         try:
237             _assert(self._filter is not None, "no watch set")
238
239             # To call Twisted or Tahoe APIs, use reactor.callFromThread as described in
240             # <http://twistedmatrix.com/documents/current/core/howto/threading.html>.
241
242             fni = FileNotifyInformation()
243
244             while True:
245                 self._state = STARTED
246                 fni.read_changes(self._hDirectory, self._recursive, self._filter)
247                 for info in fni:
248                     if self._state == STOPPING:
249                         hDirectory = self._hDirectory
250                         self._callbacks = None
251                         self._hDirectory = None
252                         CloseHandle(hDirectory)
253                         self._state = STOPPED
254                         return
255
256                     path = self._path.preauthChild(info.filename)  # FilePath with Unicode path
257                     #mask = _action_to_inotify_mask.get(info.action, IN_CHANGED)
258
259                     def _maybe_notify(path):
260                         if path not in self._pending:
261                             self._pending.add(path)
262                             def _do_callbacks():
263                                 self._pending.remove(path)
264                                 for cb in self._callbacks:
265                                     try:
266                                         cb(None, path, IN_CHANGED)
267                                     except Exception, e:
268                                         log.err(e)
269                             reactor.callLater(self._pending_delay, _do_callbacks)
270                     reactor.callFromThread(_maybe_notify, path)
271         except Exception, e:
272             log.err(e)
273             self._state = STOPPED
274             raise