2 # Windows near-equivalent to twisted.internet.inotify
3 # This should only be imported on Windows.
7 from twisted.internet import reactor
8 from twisted.internet.threads import deferToThread
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
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]
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
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
30 # <http://msdn.microsoft.com/en-us/library/gg258116%28v=vs.85%29.aspx>
31 FILE_LIST_DIRECTORY = 1
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,
37 )(("CreateFileW", windll.kernel32))
39 FILE_SHARE_READ = 0x00000001
40 FILE_SHARE_WRITE = 0x00000002
41 FILE_SHARE_DELETE = 0x00000004
45 FILE_FLAG_BACKUP_SEMANTICS = 0x02000000
47 # <http://msdn.microsoft.com/en-us/library/ms724211%28v=vs.85%29.aspx>
48 CloseHandle = WINFUNCTYPE(
51 )(("CloseHandle", windll.kernel32))
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,
57 )(("ReadDirectoryChangesW", windll.kernel32))
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
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
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",
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,
91 INVALID_HANDLE_VALUE = 0xFFFFFFFF
98 * action: a FILE_ACTION_* constant (not a bit mask)
99 * filename: a Unicode string, giving the name relative to the watched directory
101 def __init__(self, action, filename):
103 self.filename = filename
106 return "Event(%r, %r)" % (_action_to_string.get(self.action, self.action), self.filename)
109 class FileNotifyInformation(object):
111 I represent a buffer containing FILE_NOTIFY_INFORMATION structures, and can
112 iterate over those structures, decoding them into Event objects.
115 def __init__(self, size=1024):
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,))
122 def read_changes(self, hDirectory, recursive, filter):
123 bytes_returned = DWORD(0)
124 r = ReadDirectoryChangesW(hDirectory,
129 byref(bytes_returned),
130 None, # NULL -> no overlapped I/O
131 None # NULL -> no completion routine
134 raise WinError(get_last_error())
135 self.data = self.buffer.raw[:bytes_returned.value]
138 # Iterator implemented as generator: <http://docs.python.org/library/stdtypes.html#generator-types>
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'))
145 next_entry_offset = self._read_dword(pos)
147 if next_entry_offset == 0:
149 pos = pos + next_entry_offset
151 def _read_dword(self, i):
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))
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
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))
177 filter = FILE_NOTIFY_CHANGE_FILE_NAME | FILE_NOTIFY_CHANGE_DIR_NAME | FILE_NOTIFY_CHANGE_LAST_WRITE
180 hDirectory = _open_directory(path_u)
181 fni = FileNotifyInformation()
184 fni.read_changes(hDirectory, recursive, filter)
190 NOT_STARTED = "NOT_STARTED"
192 STOPPING = "STOPPING"
195 class INotify(PollMixin):
197 self._state = NOT_STARTED
199 self._callbacks = None
200 self._hDirectory = None
202 self._pending = set()
203 self._pending_delay = 1.0
204 self.recursive_includes_new_subdirectories = True
206 def set_pending_delay(self, delay):
207 self._pending_delay = delay
209 def startReading(self):
210 deferToThread(self._thread)
211 return self.poll(lambda: self._state != NOT_STARTED)
213 def stopReading(self):
214 # FIXME race conditions
215 if self._state != STOPPED:
216 self._state = STOPPING
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)
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)
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)
235 self._filter = FILE_NOTIFY_CHANGE_FILE_NAME | FILE_NOTIFY_CHANGE_DIR_NAME | FILE_NOTIFY_CHANGE_LAST_WRITE
237 if mask & (IN_ACCESS | IN_CLOSE_NOWRITE | IN_OPEN):
238 self._filter = self._filter | FILE_NOTIFY_CHANGE_LAST_ACCESS
240 self._filter = self._filter | FILE_NOTIFY_CHANGE_ATTRIBUTES | FILE_NOTIFY_CHANGE_SECURITY
242 self._recursive = TRUE if recursive else FALSE
243 self._callbacks = callbacks or []
244 self._hDirectory = _open_directory(path_u)
248 _assert(self._filter is not None, "no watch set")
250 # To call Twisted or Tahoe APIs, use reactor.callFromThread as described in
251 # <http://twistedmatrix.com/documents/current/core/howto/threading.html>.
253 fni = FileNotifyInformation()
256 self._state = STARTED
257 fni.read_changes(self._hDirectory, self._recursive, self._filter)
259 if self._state == STOPPING:
260 hDirectory = self._hDirectory
261 self._callbacks = None
262 self._hDirectory = None
263 CloseHandle(hDirectory)
264 self._state = STOPPED
267 path = self._path.preauthChild(info.filename) # FilePath with Unicode path
268 #mask = _action_to_inotify_mask.get(info.action, IN_CHANGED)
270 def _maybe_notify(path):
271 if path not in self._pending:
272 self._pending.add(path)
274 self._pending.remove(path)
276 for cb in self._callbacks:
278 cb(None, path, IN_CHANGED)
281 reactor.callLater(self._pending_delay, _do_callbacks)
282 reactor.callFromThread(_maybe_notify, path)
285 self._state = STOPPED