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