Ticket #1431: drop-upload-including-windows.darcs.patch

File drop-upload-including-windows.darcs.patch, 72.1 KB (added by davidsarah, at 2011-08-07T20:43:16Z)

Prototype drop-upload implementation for Windows (includes code and test patches from #1429).

Line 
13 patches for repository http://tahoe-lafs.org/source/tahoe-lafs/trunk:
2
3Wed Jul 27 01:05:41 BST 2011  david-sarah@jacaranda.org
4  * Drop upload frontend (updated), with more tests. Tests now pass on Windows. refs #1429
5
6Wed Jul 27 03:30:03 BST 2011  david-sarah@jacaranda.org
7  * drop-upload: make counts visible on the statistics page, and disable some debugging. refs #1429
8
9Sun Aug  7 20:22:50 BST 2011  david-sarah@jacaranda.org
10  * Prototype Windows implementation of drop-uploader. refs #1431
11
12New patches:
13
14[Drop upload frontend (updated), with more tests. Tests now pass on Windows. refs #1429
15david-sarah@jacaranda.org**20110727000541
16 Ignore-this: d67c37a4db86c3d37a1c4b16ff299df5
17] {
18hunk ./src/allmydata/_auto_deps.py 22
19     "zope.interface == 3.3.1, == 3.5.3, == 3.6.1",
20 
21     # On Windows we need at least Twisted 9.0 to avoid an indirect dependency on pywin32.
22+    # On Linux we need at least Twisted 10.1.0 for inotify support used by the drop-upload
23+    # frontend.
24     # We also need Twisted 10.1 for the FTP frontend in order for Twisted's FTP server to
25     # support asynchronous close.
26     "Twisted >= 10.1.0",
27hunk ./src/allmydata/client.py 153
28         # ControlServer and Helper are attached after Tub startup
29         self.init_ftp_server()
30         self.init_sftp_server()
31+        self.init_drop_uploader()
32 
33         hotline_file = os.path.join(self.basedir,
34                                     self.SUICIDE_PREVENTION_HOTLINE_FILE)
35hunk ./src/allmydata/client.py 425
36                                  sftp_portstr, pubkey_file, privkey_file)
37             s.setServiceParent(self)
38 
39+    def init_drop_uploader(self):
40+        if self.get_config("drop_upload", "enabled", False, boolean=True):
41+            upload_uri = self.get_config("drop_upload", "upload.uri", None)
42+            local_dir_utf8 = self.get_config("drop_upload", "local.directory", None)
43+
44+            if upload_uri and local_dir_utf8:
45+                try:
46+                    from allmydata.frontends import drop_upload
47+                    s = drop_upload.DropUploader(self, upload_uri, local_dir_utf8)
48+                    s.setServiceParent(self)
49+                except Exception, e:
50+                    self.log("couldn't start drop-uploader: %r", args=(e,))
51+            else:
52+                self.log("couldn't start drop-uploader: upload.uri or local.directory not specified")
53+
54     def _check_hotline(self, hotline_file):
55         if os.path.exists(hotline_file):
56             mtime = os.stat(hotline_file)[stat.ST_MTIME]
57addfile ./src/allmydata/frontends/drop_upload.py
58hunk ./src/allmydata/frontends/drop_upload.py 1
59+
60+import os, sys
61+
62+from twisted.internet import defer
63+from twisted.python.filepath import FilePath
64+from twisted.application import service
65+
66+from allmydata.interfaces import IDirectoryNode
67+
68+from allmydata.util.encodingutil import quote_output
69+from allmydata.immutable.upload import FileName
70+
71+
72+class DropUploader(service.MultiService):
73+    def __init__(self, client, upload_uri, local_dir_utf8, inotify=None):
74+        service.MultiService.__init__(self)
75+
76+        try:
77+            local_dir = os.path.expanduser(local_dir_utf8.decode('utf-8').encode(sys.getfilesystemencoding()))
78+        except (UnicodeEncodeError, UnicodeDecodeError):
79+            raise AssertionError("The drop-upload path %r was not valid UTF-8 or could not be represented in the filesystem encoding."
80+                                 % quote_output(local_dir_utf8))
81+
82+        self._client = client
83+        self._convergence = client.convergence
84+        self._local_path = FilePath(local_dir)
85+        self.uploaded = 0
86+        self.failed = 0
87+        self.disappeared = 0
88+
89+        if inotify is None:
90+            from twisted.internet import inotify
91+        self._inotify = inotify
92+
93+        if not self._local_path.isdir():
94+            raise AssertionError("The drop-upload local path %r was not an existing directory." % quote_output(local_dir))
95+
96+        # TODO: allow a path rather than an URI.
97+        self._parent = self._client.create_node_from_uri(upload_uri)
98+        if not IDirectoryNode.providedBy(self._parent):
99+            raise AssertionError("The drop-upload remote URI is not a directory URI.")
100+        if self._parent.is_unknown() or self._parent.is_readonly():
101+            raise AssertionError("The drop-upload remote URI does not refer to a writeable directory.")
102+
103+        self._uploaded_callback = lambda ign: None
104+
105+        self._notifier = inotify.INotify()
106+        self._notifier.startReading()
107+
108+        # We don't watch for IN_CREATE, because that would cause us to read and upload a
109+        # possibly-incomplete file before the application has closed it. There should always
110+        # be an IN_CLOSE_WRITE after an IN_CREATE (I think).
111+        # TODO: what about IN_MOVE_SELF?
112+        mask = inotify.IN_CLOSE_WRITE | inotify.IN_MOVED_TO
113+        self._notifier.watch(self._local_path, mask=mask, callbacks=[self._notify])
114+
115+    def _notify(self, opaque, path, events_mask):
116+        self._log("inotify event %r, %r, %r\n" % (opaque, path, ', '.join(self._inotify.humanReadableMask(events_mask))))
117+
118+        d = defer.succeed(None)
119+
120+        # FIXME: if this already exists as a mutable file, we replace the directory entry,
121+        # but we should probably modify the file (as the SFTP frontend does).
122+        def _add_file(ign):
123+            name = path.basename().decode(sys.getfilesystemencoding())
124+            u = FileName(path.path, self._convergence)
125+            return self._parent.add_file(name, u)
126+        d.addCallback(_add_file)
127+
128+        def _succeeded(ign):
129+            self.uploaded += 1
130+        def _failed(f):
131+            if path.exists():
132+                self._log("drop-upload: %r failed to upload due to %r" % (path.path, f))
133+                self.failed += 1
134+                return f
135+            else:
136+                self._log("drop-upload: notified file %r disappeared "
137+                          "(this is normal for temporary files): %r" % (path.path, f))
138+                self.disappeared += 1
139+                return None
140+        d.addCallbacks(_succeeded, _failed)
141+        d.addBoth(self._uploaded_callback)
142+        return d
143+
144+    def set_uploaded_callback(self, callback):
145+        """This sets a function that will be called after a file has been uploaded."""
146+        self._uploaded_callback = callback
147+
148+    def finish(self):
149+        self._notifier.stopReading()
150+
151+    def _log(self, msg):
152+        self._client.log(msg)
153+        open("events", "ab+").write(msg)
154hunk ./src/allmydata/scripts/create_node.py 155
155     c.write("enabled = false\n")
156     c.write("\n")
157 
158+    c.write("[drop_upload]\n")
159+    c.write("# Shall this node automatically upload files created or modified in a local directory?\n")
160+    c.write("enabled = false\n")
161+    c.write("# This must be an URI for a writeable directory.\n")
162+    c.write("upload.uri =\n")
163+    c.write("local.directory = ~/drop_upload\n")
164+    c.write("\n")
165+
166     c.close()
167 
168     from allmydata.util import fileutil
169addfile ./src/allmydata/test/fake_inotify.py
170hunk ./src/allmydata/test/fake_inotify.py 1
171+
172+# Most of this is copied from Twisted 11.0. The reason for this hack is that
173+# twisted.internet.inotify can't be imported when the platform does not support inotify.
174+
175+
176+# from /usr/src/linux/include/linux/inotify.h
177+
178+IN_ACCESS = 0x00000001L         # File was accessed
179+IN_MODIFY = 0x00000002L         # File was modified
180+IN_ATTRIB = 0x00000004L         # Metadata changed
181+IN_CLOSE_WRITE = 0x00000008L    # Writeable file was closed
182+IN_CLOSE_NOWRITE = 0x00000010L  # Unwriteable file closed
183+IN_OPEN = 0x00000020L           # File was opened
184+IN_MOVED_FROM = 0x00000040L     # File was moved from X
185+IN_MOVED_TO = 0x00000080L       # File was moved to Y
186+IN_CREATE = 0x00000100L         # Subfile was created
187+IN_DELETE = 0x00000200L         # Subfile was delete
188+IN_DELETE_SELF = 0x00000400L    # Self was deleted
189+IN_MOVE_SELF = 0x00000800L      # Self was moved
190+IN_UNMOUNT = 0x00002000L        # Backing fs was unmounted
191+IN_Q_OVERFLOW = 0x00004000L     # Event queued overflowed
192+IN_IGNORED = 0x00008000L        # File was ignored
193+
194+IN_ONLYDIR = 0x01000000         # only watch the path if it is a directory
195+IN_DONT_FOLLOW = 0x02000000     # don't follow a sym link
196+IN_MASK_ADD = 0x20000000        # add to the mask of an already existing watch
197+IN_ISDIR = 0x40000000           # event occurred against dir
198+IN_ONESHOT = 0x80000000         # only send event once
199+
200+IN_CLOSE = IN_CLOSE_WRITE | IN_CLOSE_NOWRITE     # closes
201+IN_MOVED = IN_MOVED_FROM | IN_MOVED_TO           # moves
202+IN_CHANGED = IN_MODIFY | IN_ATTRIB               # changes
203+
204+IN_WATCH_MASK = (IN_MODIFY | IN_ATTRIB |
205+                 IN_CREATE | IN_DELETE |
206+                 IN_DELETE_SELF | IN_MOVE_SELF |
207+                 IN_UNMOUNT | IN_MOVED_FROM | IN_MOVED_TO)
208+
209+
210+_FLAG_TO_HUMAN = [
211+    (IN_ACCESS, 'access'),
212+    (IN_MODIFY, 'modify'),
213+    (IN_ATTRIB, 'attrib'),
214+    (IN_CLOSE_WRITE, 'close_write'),
215+    (IN_CLOSE_NOWRITE, 'close_nowrite'),
216+    (IN_OPEN, 'open'),
217+    (IN_MOVED_FROM, 'moved_from'),
218+    (IN_MOVED_TO, 'moved_to'),
219+    (IN_CREATE, 'create'),
220+    (IN_DELETE, 'delete'),
221+    (IN_DELETE_SELF, 'delete_self'),
222+    (IN_MOVE_SELF, 'move_self'),
223+    (IN_UNMOUNT, 'unmount'),
224+    (IN_Q_OVERFLOW, 'queue_overflow'),
225+    (IN_IGNORED, 'ignored'),
226+    (IN_ONLYDIR, 'only_dir'),
227+    (IN_DONT_FOLLOW, 'dont_follow'),
228+    (IN_MASK_ADD, 'mask_add'),
229+    (IN_ISDIR, 'is_dir'),
230+    (IN_ONESHOT, 'one_shot')
231+]
232+
233+
234+
235+def humanReadableMask(mask):
236+    """
237+    Auxiliary function that converts an hexadecimal mask into a series
238+    of human readable flags.
239+    """
240+    s = []
241+    for k, v in _FLAG_TO_HUMAN:
242+        if k & mask:
243+            s.append(v)
244+    return s
245+
246+
247+# This class is not copied from Twisted; it acts as a mock.
248+class INotify(object):
249+    def startReading(self):
250+        pass
251+
252+    def stopReading(self):
253+        pass
254+
255+    def watch(self, filepath, mask=IN_WATCH_MASK, autoAdd=False, callbacks=None, recursive=False):
256+        self.callbacks = callbacks
257+
258+    def event(self, filepath, mask):
259+        for cb in self.callbacks:
260+            cb(None, filepath, mask)
261+
262+
263+__all__ = ["INotify", "humanReadableMask", "IN_WATCH_MASK", "IN_ACCESS",
264+           "IN_MODIFY", "IN_ATTRIB", "IN_CLOSE_NOWRITE", "IN_CLOSE_WRITE",
265+           "IN_OPEN", "IN_MOVED_FROM", "IN_MOVED_TO", "IN_CREATE",
266+           "IN_DELETE", "IN_DELETE_SELF", "IN_MOVE_SELF", "IN_UNMOUNT",
267+           "IN_Q_OVERFLOW", "IN_IGNORED", "IN_ONLYDIR", "IN_DONT_FOLLOW",
268+           "IN_MASK_ADD", "IN_ISDIR", "IN_ONESHOT", "IN_CLOSE",
269+           "IN_MOVED", "IN_CHANGED"]
270addfile ./src/allmydata/test/test_drop_upload.py
271hunk ./src/allmydata/test/test_drop_upload.py 1
272+
273+import os, sys, platform
274+
275+from twisted.trial import unittest
276+from twisted.python import filepath, runtime
277+from twisted.internet import defer, base
278+
279+from allmydata.interfaces import IDirectoryNode, NoSuchChildError
280+
281+from allmydata.util import fileutil
282+from allmydata.util.consumer import download_to_data
283+from allmydata.test.no_network import GridTestMixin
284+from allmydata.test.common_util import ReallyEqualMixin
285+from allmydata.test.common import ShouldFailMixin
286+from allmydata.test import fake_inotify
287+
288+from allmydata.frontends.drop_upload import DropUploader
289+
290+
291+class DropUploadTestMixin(GridTestMixin, ShouldFailMixin, ReallyEqualMixin):
292+    """
293+    These tests will be run both with a mock notifier, and (on platforms that support it)
294+    with the real INotify.
295+    """
296+
297+    def _test(self):
298+        self.uploader = None
299+        self.set_up_grid()
300+        self.local_dir = os.path.join(self.basedir, "local_dir")
301+        os.mkdir(self.local_dir)
302+
303+        self.client = self.g.clients[0]
304+        d = self.client.create_dirnode()
305+        def _made_upload_dir(n):
306+            self.failUnless(IDirectoryNode.providedBy(n))
307+            self.upload_dirnode = n
308+            self.upload_uri = n.get_uri()
309+            self.uploader = DropUploader(self.client, self.upload_uri, self.local_dir, inotify=self.inotify)
310+        d.addCallback(_made_upload_dir)
311+
312+        # Write something short enough for a LIT file.
313+        d.addCallback(lambda ign: self._test_file("short", "test"))
314+
315+        # Write to the same file again with different data.
316+        d.addCallback(lambda ign: self._test_file("short", "different"))
317+
318+        # Test that temporary files are not uploaded.
319+        d.addCallback(lambda ign: self._test_file("tempfile", "test", temporary=True))
320+
321+        # Test that we tolerate creation of a subdirectory.
322+        d.addCallback(lambda ign: os.mkdir(os.path.join(self.local_dir, "directory")))
323+
324+        # Write something longer, and also try to test a Unicode name if the fs can represent it.
325+        try:
326+            name = u"l\u00F8ng".encode(sys.getfilesystemencoding())
327+        except UnicodeEncodeError:
328+            name = "long"
329+        d.addCallback(lambda ign: self._test_file(name, "test"*100))
330+
331+        # TODO: test that causes an upload failure.
332+        d.addCallback(lambda ign: self.failUnlessReallyEqual(self.uploader.failed, 0))
333+
334+        # Prevent unclean reactor errors.
335+        def _cleanup(res):
336+            if self.uploader is not None:
337+                self.uploader.finish()
338+            return res
339+        d.addBoth(_cleanup)
340+        return d
341+
342+    def _test_file(self, name, data, temporary=False):
343+        previously_uploaded = self.uploader.uploaded
344+        previously_disappeared = self.uploader.disappeared
345+
346+        d = defer.Deferred()
347+
348+        # Note: this relies on the fact that we only get one IN_CLOSE_WRITE notification per file
349+        # (otherwise we would get a defer.AlreadyCalledError). Should we be relying on that?
350+        self.uploader.set_uploaded_callback(d.callback)
351+
352+        path = filepath.FilePath(os.path.join(self.local_dir, name))
353+        unicode_name = name.decode(sys.getfilesystemencoding())
354+
355+        f = open(path.path, "wb")
356+        try:
357+            if temporary and sys.platform != "win32":
358+                os.unlink(path.path)
359+            f.write(data)
360+        finally:
361+            f.close()
362+        if temporary and sys.platform == "win32":
363+            os.unlink(path.path)
364+        self.notify_close_write(path)
365+
366+        if temporary:
367+            d.addCallback(lambda ign: self.shouldFail(NoSuchChildError, 'temp file not uploaded', None,
368+                                                      self.upload_dirnode.get, unicode_name))
369+            d.addCallback(lambda ign: self.failUnlessReallyEqual(self.uploader.disappeared, previously_disappeared + 1))
370+        else:
371+            d.addCallback(lambda ign: self.upload_dirnode.get(unicode_name))
372+            d.addCallback(download_to_data)
373+            d.addCallback(lambda actual_data: self.failUnlessReallyEqual(actual_data, data))
374+            d.addCallback(lambda ign: self.failUnlessReallyEqual(self.uploader.uploaded, previously_uploaded + 1))
375+        return d
376+
377+
378+class MockTest(DropUploadTestMixin, unittest.TestCase):
379+    """This can run on any platform, and even if twisted.internet.inotify can't be imported."""
380+
381+    def test_errors(self):
382+        self.basedir = "drop_upload.MockTest.test_errors"
383+        self.set_up_grid()
384+        errors_dir = os.path.join(self.basedir, "errors_dir")
385+        os.mkdir(errors_dir)
386+
387+        client = self.g.clients[0]
388+        d = client.create_dirnode()
389+        def _made_upload_dir(n):
390+            self.failUnless(IDirectoryNode.providedBy(n))
391+            upload_uri = n.get_uri()
392+            readonly_uri = n.get_readonly_uri()
393+
394+            self.shouldFail(AssertionError, 'invalid local dir', 'could not be represented',
395+                            DropUploader, client, upload_uri, '\xFF', inotify=fake_inotify)
396+            self.shouldFail(AssertionError, 'non-existant local dir', 'not an existing directory',
397+                            DropUploader, client, upload_uri, os.path.join(self.basedir, "Laputa"), inotify=fake_inotify)
398+
399+            self.shouldFail(AssertionError, 'bad URI', 'not a directory URI',
400+                            DropUploader, client, 'bad', errors_dir, inotify=fake_inotify)
401+            self.shouldFail(AssertionError, 'non-directory URI', 'not a directory URI',
402+                            DropUploader, client, 'URI:LIT:foo', errors_dir, inotify=fake_inotify)
403+            self.shouldFail(AssertionError, 'readonly directory URI', 'does not refer to a writeable directory',
404+                            DropUploader, client, readonly_uri, errors_dir, inotify=fake_inotify)
405+        d.addCallback(_made_upload_dir)
406+        return d
407+
408+    def test_drop_upload(self):
409+        self.inotify = fake_inotify
410+        self.basedir = "drop_upload.MockTest.test_drop_upload"
411+        return self._test()
412+
413+    def notify_close_write(self, path):
414+        self.uploader._notifier.event(path, self.inotify.IN_CLOSE_WRITE)
415+
416+
417+class RealTest(DropUploadTestMixin, unittest.TestCase):
418+    """This is skipped unless both Twisted and the platform support inotify."""
419+
420+    def test_drop_upload(self):
421+        # We should always have runtime.platform.supportsINotify, because we're using
422+        # Twisted >= 10.1.
423+        if not runtime.platform.supportsINotify():
424+            raise unittest.SkipTest("Drop-upload support can only be tested for-real on an OS that supports inotify.")
425+
426+        self.inotify = None  # use the real twisted.internet.inotify
427+        self.basedir = "drop_upload.RealTest.test_drop_upload"
428+        return self._test()
429+
430+    def notify_close_write(self, path):
431+        # Writing to the file causes the notification.
432+        pass
433hunk ./src/allmydata/test/test_runner.py 256
434                 self.failUnless(re.search(r"\n\[storage\]\n#.*\nenabled = true\n", content), content)
435                 self.failUnless("\nreserved_space = 1G\n" in content)
436 
437+            self.failUnless(re.search(r"\n\[drop_upload\]\n#.*\nenabled = false\n", content), content)
438+
439         # creating the node a second time should be rejected
440         rc, out, err = self.run_tahoe(argv)
441         self.failIfEqual(rc, 0, str((out, err, rc)))
442}
443[drop-upload: make counts visible on the statistics page, and disable some debugging. refs #1429
444david-sarah@jacaranda.org**20110727023003
445 Ignore-this: 4e25022cca41d6012da067e96fadb1bf
446] {
447hunk ./src/allmydata/frontends/drop_upload.py 7
448 from twisted.internet import defer
449 from twisted.python.filepath import FilePath
450 from twisted.application import service
451+from foolscap.api import eventually
452 
453 from allmydata.interfaces import IDirectoryNode
454 
455hunk ./src/allmydata/frontends/drop_upload.py 26
456                                  % quote_output(local_dir_utf8))
457 
458         self._client = client
459+        self._stats_provider = client.stats_provider
460         self._convergence = client.convergence
461         self._local_path = FilePath(local_dir)
462hunk ./src/allmydata/frontends/drop_upload.py 29
463-        self.uploaded = 0
464-        self.failed = 0
465-        self.disappeared = 0
466 
467         if inotify is None:
468             from twisted.internet import inotify
469hunk ./src/allmydata/frontends/drop_upload.py 48
470 
471         self._notifier = inotify.INotify()
472         self._notifier.startReading()
473+        self._stats_provider.count('drop_upload.dirs_monitored', 1)
474 
475         # We don't watch for IN_CREATE, because that would cause us to read and upload a
476         # possibly-incomplete file before the application has closed it. There should always
477hunk ./src/allmydata/frontends/drop_upload.py 60
478     def _notify(self, opaque, path, events_mask):
479         self._log("inotify event %r, %r, %r\n" % (opaque, path, ', '.join(self._inotify.humanReadableMask(events_mask))))
480 
481+        self._stats_provider.count('drop_upload.files_queued', 1)
482+        eventually(self._process, opaque, path, events_mask)
483+
484+    def _process(self, opaque, path, events_mask):
485         d = defer.succeed(None)
486 
487         # FIXME: if this already exists as a mutable file, we replace the directory entry,
488hunk ./src/allmydata/frontends/drop_upload.py 75
489         d.addCallback(_add_file)
490 
491         def _succeeded(ign):
492-            self.uploaded += 1
493+            self._stats_provider.count('drop_upload.files_queued', -1)
494+            self._stats_provider.count('drop_upload.files_uploaded', 1)
495         def _failed(f):
496hunk ./src/allmydata/frontends/drop_upload.py 78
497+            self._stats_provider.count('drop_upload.files_queued', -1)
498             if path.exists():
499                 self._log("drop-upload: %r failed to upload due to %r" % (path.path, f))
500hunk ./src/allmydata/frontends/drop_upload.py 81
501-                self.failed += 1
502+                self._stats_provider.count('drop_upload.files_failed', 1)
503                 return f
504             else:
505                 self._log("drop-upload: notified file %r disappeared "
506hunk ./src/allmydata/frontends/drop_upload.py 86
507                           "(this is normal for temporary files): %r" % (path.path, f))
508-                self.disappeared += 1
509+                self._stats_provider.count('drop_upload.files_disappeared', 1)
510                 return None
511         d.addCallbacks(_succeeded, _failed)
512         d.addBoth(self._uploaded_callback)
513hunk ./src/allmydata/frontends/drop_upload.py 98
514 
515     def finish(self):
516         self._notifier.stopReading()
517+        self._stats_provider.count('drop_upload.dirs_monitored', -1)
518 
519     def _log(self, msg):
520         self._client.log(msg)
521hunk ./src/allmydata/frontends/drop_upload.py 102
522-        open("events", "ab+").write(msg)
523+        #open("events", "ab+").write(msg)
524hunk ./src/allmydata/test/test_drop_upload.py 26
525     with the real INotify.
526     """
527 
528+    def _get_count(self, name):
529+        return self.stats_provider.get_stats()["counters"].get(name, 0)
530+
531     def _test(self):
532         self.uploader = None
533         self.set_up_grid()
534hunk ./src/allmydata/test/test_drop_upload.py 36
535         os.mkdir(self.local_dir)
536 
537         self.client = self.g.clients[0]
538+        self.stats_provider = self.client.stats_provider
539+
540         d = self.client.create_dirnode()
541         def _made_upload_dir(n):
542             self.failUnless(IDirectoryNode.providedBy(n))
543hunk ./src/allmydata/test/test_drop_upload.py 66
544         d.addCallback(lambda ign: self._test_file(name, "test"*100))
545 
546         # TODO: test that causes an upload failure.
547-        d.addCallback(lambda ign: self.failUnlessReallyEqual(self.uploader.failed, 0))
548+        d.addCallback(lambda ign: self.failUnlessReallyEqual(self._get_count('drop_upload.files_failed'), 0))
549 
550         # Prevent unclean reactor errors.
551         def _cleanup(res):
552hunk ./src/allmydata/test/test_drop_upload.py 77
553         return d
554 
555     def _test_file(self, name, data, temporary=False):
556-        previously_uploaded = self.uploader.uploaded
557-        previously_disappeared = self.uploader.disappeared
558+        previously_uploaded = self._get_count('drop_upload.files_uploaded')
559+        previously_disappeared = self._get_count('drop_upload.files_disappeared')
560 
561         d = defer.Deferred()
562 
563hunk ./src/allmydata/test/test_drop_upload.py 103
564         if temporary:
565             d.addCallback(lambda ign: self.shouldFail(NoSuchChildError, 'temp file not uploaded', None,
566                                                       self.upload_dirnode.get, unicode_name))
567-            d.addCallback(lambda ign: self.failUnlessReallyEqual(self.uploader.disappeared, previously_disappeared + 1))
568+            d.addCallback(lambda ign: self.failUnlessReallyEqual(self._get_count('drop_upload.files_disappeared'),
569+                                                                 previously_disappeared + 1))
570         else:
571             d.addCallback(lambda ign: self.upload_dirnode.get(unicode_name))
572             d.addCallback(download_to_data)
573hunk ./src/allmydata/test/test_drop_upload.py 109
574             d.addCallback(lambda actual_data: self.failUnlessReallyEqual(actual_data, data))
575-            d.addCallback(lambda ign: self.failUnlessReallyEqual(self.uploader.uploaded, previously_uploaded + 1))
576+            d.addCallback(lambda ign: self.failUnlessReallyEqual(self._get_count('drop_upload.files_uploaded'),
577+                                                                 previously_uploaded + 1))
578+
579+        d.addCallback(lambda ign: self.failUnlessReallyEqual(self._get_count('drop_upload.files_queued'), 0))
580         return d
581 
582 
583hunk ./src/allmydata/web/statistics.xhtml 12
584 
585 <h1>Node Statistics</h1>
586 
587+<h2>General</h2>
588+
589 <ul>
590   <li>Load Average: <span n:render="load_average" /></li>
591   <li>Peak Load: <span n:render="peak_load" /></li>
592hunk ./src/allmydata/web/statistics.xhtml 23
593   <li>Files Retrieved (mutable): <span n:render="retrieves" /></li>
594 </ul>
595 
596+<h2>Drop-Uploader</h2>
597+
598+<ul>
599+  <li>Local Directories Monitored: <span n:render="drop_monitored" /></li>
600+  <li>Files Uploaded: <span n:render="drop_uploads" /></li>
601+  <li>File Changes Queued: <span n:render="drop_queued" /></li>
602+  <li>Failed Uploads: <span n:render="drop_failed" /></li>
603+</ul>
604+
605 <h2>Raw Stats:</h2>
606 <pre n:render="raw" />
607 
608hunk ./src/allmydata/web/status.py 1293
609         return "%s files / %s bytes (%s)" % (files, bytes,
610                                              abbreviate_size(bytes))
611 
612+    def render_drop_monitored(self, ctx, data):
613+        dirs = data["counters"].get("drop_upload.dirs_monitored", 0)
614+        return "%s directories" % (dirs,)
615+
616+    def render_drop_uploads(self, ctx, data):
617+        # TODO: bytes uploaded
618+        files = data["counters"].get("drop_upload.files_uploaded", 0)
619+        return "%s files" % (files,)
620+
621+    def render_drop_queued(self, ctx, data):
622+        files = data["counters"].get("drop_upload.files_queued", 0)
623+        return "%s files" % (files,)
624+
625+    def render_drop_failed(self, ctx, data):
626+        files = data["counters"].get("drop_upload.files_failed", 0)
627+        return "%s files" % (files,)
628+
629     def render_raw(self, ctx, data):
630         raw = pprint.pformat(data)
631         return ctx.tag[raw]
632}
633[Prototype Windows implementation of drop-uploader. refs #1431
634david-sarah@jacaranda.org**20110807192250
635 Ignore-this: 16937b2dd661d42056d528c08846f0bb
636] {
637move ./src/allmydata/test/fake_inotify.py ./src/allmydata/util/fake_inotify.py
638hunk ./src/allmydata/client.py 435
639                     from allmydata.frontends import drop_upload
640                     s = drop_upload.DropUploader(self, upload_uri, local_dir_utf8)
641                     s.setServiceParent(self)
642+                    s.start()
643                 except Exception, e:
644                     self.log("couldn't start drop-uploader: %r", args=(e,))
645             else:
646hunk ./src/allmydata/frontends/drop_upload.py 16
647 
648 
649 class DropUploader(service.MultiService):
650-    def __init__(self, client, upload_uri, local_dir_utf8, inotify=None):
651+    def __init__(self, client, upload_uri, local_dir_utf8, inotify=None, pending_delay=1.0):
652         service.MultiService.__init__(self)
653 
654         try:
655hunk ./src/allmydata/frontends/drop_upload.py 31
656         self._local_path = FilePath(local_dir)
657 
658         if inotify is None:
659-            from twisted.internet import inotify
660+            if sys.platform == "win32":
661+                from allmydata.windows import inotify
662+            else:
663+                from twisted.internet import inotify
664         self._inotify = inotify
665 
666         if not self._local_path.isdir():
667hunk ./src/allmydata/frontends/drop_upload.py 50
668         self._uploaded_callback = lambda ign: None
669 
670         self._notifier = inotify.INotify()
671-        self._notifier.startReading()
672-        self._stats_provider.count('drop_upload.dirs_monitored', 1)
673+        if hasattr(self._notifier, 'set_pending_delay'):
674+            self._notifier.set_pending_delay(pending_delay)
675 
676         # We don't watch for IN_CREATE, because that would cause us to read and upload a
677         # possibly-incomplete file before the application has closed it. There should always
678hunk ./src/allmydata/frontends/drop_upload.py 56
679         # be an IN_CLOSE_WRITE after an IN_CREATE (I think).
680-        # TODO: what about IN_MOVE_SELF?
681-        mask = inotify.IN_CLOSE_WRITE | inotify.IN_MOVED_TO
682+        # TODO: what about IN_MOVE_SELF or IN_UNMOUNT?
683+        mask = inotify.IN_CLOSE_WRITE | inotify.IN_MOVED_TO | inotify.IN_ONLYDIR
684         self._notifier.watch(self._local_path, mask=mask, callbacks=[self._notify])
685 
686hunk ./src/allmydata/frontends/drop_upload.py 60
687+    def start(self):
688+        d = self._notifier.startReading()
689+        self._stats_provider.count('drop_upload.dirs_monitored', 1)
690+        return d
691+
692     def _notify(self, opaque, path, events_mask):
693         self._log("inotify event %r, %r, %r\n" % (opaque, path, ', '.join(self._inotify.humanReadableMask(events_mask))))
694 
695hunk ./src/allmydata/frontends/drop_upload.py 77
696         # FIXME: if this already exists as a mutable file, we replace the directory entry,
697         # but we should probably modify the file (as the SFTP frontend does).
698         def _add_file(ign):
699-            name = path.basename().decode(sys.getfilesystemencoding())
700+            name = path.basename()
701+            # on Windows the name is already Unicode
702+            if not isinstance(name, unicode):
703+                name = name.decode(sys.getfilesystemencoding())
704+
705             u = FileName(path.path, self._convergence)
706             return self._parent.add_file(name, u)
707         d.addCallback(_add_file)
708hunk ./src/allmydata/frontends/drop_upload.py 108
709         """This sets a function that will be called after a file has been uploaded."""
710         self._uploaded_callback = callback
711 
712-    def finish(self):
713+    def finish(self, for_tests=False):
714         self._notifier.stopReading()
715         self._stats_provider.count('drop_upload.dirs_monitored', -1)
716hunk ./src/allmydata/frontends/drop_upload.py 111
717+        if for_tests and hasattr(self._notifier, 'wait_until_stopped'):
718+            return self._notifier.wait_until_stopped()
719+        else:
720+            return defer.succeed(None)
721 
722     def _log(self, msg):
723         self._client.log(msg)
724hunk ./src/allmydata/test/test_drop_upload.py 10
725 
726 from allmydata.interfaces import IDirectoryNode, NoSuchChildError
727 
728-from allmydata.util import fileutil
729+from allmydata.util import fileutil, fake_inotify
730 from allmydata.util.consumer import download_to_data
731 from allmydata.test.no_network import GridTestMixin
732 from allmydata.test.common_util import ReallyEqualMixin
733hunk ./src/allmydata/test/test_drop_upload.py 15
734 from allmydata.test.common import ShouldFailMixin
735-from allmydata.test import fake_inotify
736 
737 from allmydata.frontends.drop_upload import DropUploader
738 
739hunk ./src/allmydata/test/test_drop_upload.py 42
740             self.failUnless(IDirectoryNode.providedBy(n))
741             self.upload_dirnode = n
742             self.upload_uri = n.get_uri()
743-            self.uploader = DropUploader(self.client, self.upload_uri, self.local_dir, inotify=self.inotify)
744+            self.uploader = DropUploader(self.client, self.upload_uri, self.local_dir,
745+                                         inotify=self.inotify, pending_delay=0.2)
746+            return self.uploader.start()
747         d.addCallback(_made_upload_dir)
748 
749         # Write something short enough for a LIT file.
750hunk ./src/allmydata/test/test_drop_upload.py 71
751 
752         # Prevent unclean reactor errors.
753         def _cleanup(res):
754+            d = defer.succeed(None)
755             if self.uploader is not None:
756hunk ./src/allmydata/test/test_drop_upload.py 73
757-                self.uploader.finish()
758-            return res
759+                d.addCallback(lambda ign: self.uploader.finish(for_tests=True))
760+            d.addCallback(lambda ign: res)
761+            return d
762         d.addBoth(_cleanup)
763         return d
764 
765hunk ./src/allmydata/test/test_drop_upload.py 101
766             f.close()
767         if temporary and sys.platform == "win32":
768             os.unlink(path.path)
769+        fileutil.flush_volume(path.path)
770         self.notify_close_write(path)
771 
772         if temporary:
773hunk ./src/allmydata/test/test_drop_upload.py 165
774     def test_drop_upload(self):
775         # We should always have runtime.platform.supportsINotify, because we're using
776         # Twisted >= 10.1.
777-        if not runtime.platform.supportsINotify():
778-            raise unittest.SkipTest("Drop-upload support can only be tested for-real on an OS that supports inotify.")
779+        if not sys.platform == "win32" and not runtime.platform.supportsINotify():
780+            raise unittest.SkipTest("Drop-upload support can only be tested for-real on an OS that supports inotify or equivalent.")
781 
782hunk ./src/allmydata/test/test_drop_upload.py 168
783-        self.inotify = None  # use the real twisted.internet.inotify
784+        self.inotify = None  # use the appropriate inotify for the platform
785         self.basedir = "drop_upload.RealTest.test_drop_upload"
786         return self._test()
787 
788hunk ./src/allmydata/util/fileutil.py 422
789         log.msg("OS call to get disk statistics failed")
790         return 0
791 
792+
793+if sys.platform == "win32":
794+    from ctypes import WINFUNCTYPE, windll, WinError
795+    from ctypes.wintypes import BOOL, HANDLE, DWORD, LPCWSTR, LPVOID
796+
797+    # <http://msdn.microsoft.com/en-us/library/aa363858%28v=vs.85%29.aspx>
798+    CreateFileW = WINFUNCTYPE(HANDLE, LPCWSTR, DWORD, DWORD, LPVOID, DWORD, DWORD, HANDLE) \
799+                      (("CreateFileW", windll.kernel32))
800+
801+    GENERIC_WRITE        = 0x40000000
802+    FILE_SHARE_READ      = 0x00000001
803+    FILE_SHARE_WRITE     = 0x00000002
804+    OPEN_EXISTING        = 3
805+    INVALID_HANDLE_VALUE = 0xFFFFFFFF
806+
807+    # <http://msdn.microsoft.com/en-us/library/aa364439%28v=vs.85%29.aspx>
808+    FlushFileBuffers = WINFUNCTYPE(BOOL, HANDLE)(("FlushFileBuffers", windll.kernel32))
809+
810+    # <http://msdn.microsoft.com/en-us/library/ms724211%28v=vs.85%29.aspx>
811+    CloseHandle = WINFUNCTYPE(BOOL, HANDLE)(("CloseHandle", windll.kernel32))
812+
813+    # <http://social.msdn.microsoft.com/forums/en-US/netfxbcl/thread/4465cafb-f4ed-434f-89d8-c85ced6ffaa8/>
814+    def flush_volume(path):
815+        drive = os.path.splitdrive(os.path.realpath(path))[0]
816+
817+        hVolume = CreateFileW(u"\\\\.\\" + drive,
818+                              GENERIC_WRITE,
819+                              FILE_SHARE_READ | FILE_SHARE_WRITE,
820+                              None,
821+                              OPEN_EXISTING,
822+                              0,
823+                              None
824+                             )
825+        if hVolume == INVALID_HANDLE_VALUE:
826+            raise WinError()
827+
828+        if FlushFileBuffers(hVolume) == 0:
829+            raise WinError()
830+
831+        CloseHandle(hVolume)
832+else:
833+    def flush_volume(path):
834+        # use sync()?
835+        pass
836+
837addfile ./src/allmydata/windows/inotify.py
838hunk ./src/allmydata/windows/inotify.py 1
839+
840+# Windows near-equivalent to twisted.internet.inotify
841+# This should only be imported on Windows.
842+
843+import os, sys
844+
845+from twisted.internet import reactor
846+from twisted.internet.threads import deferToThread
847+
848+from allmydata.util.fake_inotify import *
849+from allmydata.util.encodingutil import quote_output
850+from allmydata.util import log, fileutil
851+from allmydata.util.pollmixin import PollMixin
852+
853+from ctypes import WINFUNCTYPE, WinError, windll, POINTER, byref, create_string_buffer, addressof
854+from ctypes.wintypes import BOOL, HANDLE, DWORD, LPCWSTR, LPVOID
855+
856+# <http://msdn.microsoft.com/en-us/library/gg258116%28v=vs.85%29.aspx>
857+FILE_LIST_DIRECTORY              = 1
858+
859+# <http://msdn.microsoft.com/en-us/library/aa363858%28v=vs.85%29.aspx>
860+CreateFileW = WINFUNCTYPE(HANDLE, LPCWSTR, DWORD, DWORD, LPVOID, DWORD, DWORD, HANDLE) \
861+                  (("CreateFileW", windll.kernel32))
862+
863+FILE_SHARE_READ                  = 0x00000001
864+FILE_SHARE_WRITE                 = 0x00000002
865+FILE_SHARE_DELETE                = 0x00000004
866+
867+OPEN_EXISTING                    = 3
868+
869+FILE_FLAG_BACKUP_SEMANTICS       = 0x02000000
870+
871+# <http://msdn.microsoft.com/en-us/library/ms724211%28v=vs.85%29.aspx>
872+CloseHandle = WINFUNCTYPE(BOOL, HANDLE)(("CloseHandle", windll.kernel32))
873+
874+# <http://msdn.microsoft.com/en-us/library/aa365465%28v=vs.85%29.aspx>
875+ReadDirectoryChangesW = WINFUNCTYPE(BOOL, HANDLE, LPVOID, DWORD, BOOL, DWORD, POINTER(DWORD), LPVOID, LPVOID) \
876+                            (("ReadDirectoryChangesW", windll.kernel32))
877+
878+FILE_NOTIFY_CHANGE_FILE_NAME     = 0x00000001
879+FILE_NOTIFY_CHANGE_DIR_NAME      = 0x00000002
880+FILE_NOTIFY_CHANGE_ATTRIBUTES    = 0x00000004
881+#FILE_NOTIFY_CHANGE_SIZE         = 0x00000008
882+FILE_NOTIFY_CHANGE_LAST_WRITE    = 0x00000010
883+FILE_NOTIFY_CHANGE_LAST_ACCESS   = 0x00000020
884+#FILE_NOTIFY_CHANGE_CREATION     = 0x00000040
885+FILE_NOTIFY_CHANGE_SECURITY      = 0x00000100
886+
887+# <http://msdn.microsoft.com/en-us/library/aa364391%28v=vs.85%29.aspx>
888+FILE_ACTION_ADDED                = 0x00000001
889+FILE_ACTION_REMOVED              = 0x00000002
890+FILE_ACTION_MODIFIED             = 0x00000003
891+FILE_ACTION_RENAMED_OLD_NAME     = 0x00000004
892+FILE_ACTION_RENAMED_NEW_NAME     = 0x00000005
893+
894+_action_to_string = {
895+    FILE_ACTION_ADDED            : "FILE_ACTION_ADDED",
896+    FILE_ACTION_REMOVED          : "FILE_ACTION_REMOVED",
897+    FILE_ACTION_MODIFIED         : "FILE_ACTION_MODIFIED",
898+    FILE_ACTION_RENAMED_OLD_NAME : "FILE_ACTION_RENAMED_OLD_NAME",
899+    FILE_ACTION_RENAMED_NEW_NAME : "FILE_ACTION_RENAMED_NEW_NAME",
900+}
901+
902+_action_to_inotify_mask = {
903+    FILE_ACTION_ADDED            : IN_CREATE,
904+    FILE_ACTION_REMOVED          : IN_DELETE,
905+    FILE_ACTION_MODIFIED         : IN_CHANGED,
906+    FILE_ACTION_RENAMED_OLD_NAME : IN_MOVED_FROM,
907+    FILE_ACTION_RENAMED_NEW_NAME : IN_MOVED_TO,
908+}
909+
910+INVALID_HANDLE_VALUE             = 0xFFFFFFFF
911+
912+
913+class Event(object):
914+    """
915+    * action:   a FILE_ACTION_* constant (not a bit mask)
916+    * filename: a Unicode string, giving the name relative to the watched directory
917+    """
918+    def __init__(self, action, filename):
919+        self.action = action
920+        self.filename = filename
921+
922+    def __repr__(self):
923+        return "Event(%r, %r)" % (_action_to_string.get(self.action, self.action), self.filename)
924+
925+
926+class FileNotifyInformation(object):
927+    """
928+    I represent a buffer containing FILE_NOTIFY_INFORMATION structures, and can
929+    iterate over those structures, decoding them into Event objects.
930+    """
931+
932+    def __init__(self, size=1024):
933+        self.size = size
934+        self.buffer = create_string_buffer(size)
935+        address = addressof(self.buffer)
936+        assert address & 3 == 0, "address 0x%X returned by create_string_buffer is not DWORD-aligned" % (address,)
937+        self.data = None
938+
939+    def read_changes(self, hDirectory, recursive, filter):
940+        bytes_returned = DWORD(0)
941+        r = ReadDirectoryChangesW(hDirectory,
942+                                  self.buffer,
943+                                  self.size,
944+                                  recursive,
945+                                  filter,
946+                                  byref(bytes_returned),
947+                                  None,  # NULL -> no overlapped I/O
948+                                  None   # NULL -> no completion routine
949+                                 )
950+        if r == 0:
951+            raise WinError()
952+        self.data = self.buffer.raw[:bytes_returned.value]
953+
954+    def __iter__(self):
955+        # Iterator implemented as generator: <http://docs.python.org/library/stdtypes.html#generator-types>
956+        pos = 0
957+        while True:
958+            bytes = self._read_dword(pos+8)
959+            s = Event(self._read_dword(pos+4),
960+                      self.data[pos+12 : pos+12+bytes].decode('utf-16-le'))
961+
962+            next_entry_offset = self._read_dword(pos)
963+            yield s
964+            if next_entry_offset == 0:
965+                break
966+            pos = pos + next_entry_offset
967+
968+    def _read_dword(self, i):
969+        # little-endian
970+        return ( ord(self.data[i])          |
971+                (ord(self.data[i+1]) <<  8) |
972+                (ord(self.data[i+2]) << 16) |
973+                (ord(self.data[i+3]) << 24))
974+
975+
976+def _open_directory(path_u):
977+    hDirectory = CreateFileW(path_u,
978+                             FILE_LIST_DIRECTORY,         # access rights
979+                             FILE_SHARE_READ | FILE_SHARE_WRITE | FILE_SHARE_DELETE,
980+                                                          # don't prevent other processes from accessing
981+                             None,                        # no security descriptor
982+                             OPEN_EXISTING,               # directory must already exist
983+                             FILE_FLAG_BACKUP_SEMANTICS,  # necessary to open a directory
984+                             None                         # no template file
985+                            )
986+    if hDirectory == INVALID_HANDLE_VALUE:
987+        e = WinError()
988+        raise OSError("Opening directory %s gave Windows error %r: %s" % (quote_output(path_u), e.args[0], e.args[1]))
989+    return hDirectory
990+
991+
992+def simple_test():
993+    path_u = u"test"
994+    filter = FILE_NOTIFY_CHANGE_FILE_NAME | FILE_NOTIFY_CHANGE_DIR_NAME | FILE_NOTIFY_CHANGE_LAST_WRITE
995+    recursive = False
996+
997+    hDirectory = _open_directory(path_u)
998+    fni = FileNotifyInformation()
999+    print "Waiting..."
1000+    while True:
1001+        fni.read_changes(hDirectory, recursive, filter)
1002+        print repr(fni.data)
1003+        for info in fni:
1004+            print info
1005+
1006+
1007+class INotify(PollMixin):
1008+    def __init__(self):
1009+        self._stop = None
1010+        self._filter = None
1011+        self._callbacks = None
1012+        self._hDirectory = None
1013+        self._path = None
1014+        self._pending = set()
1015+        self._pending_delay = 1.0
1016+
1017+    def set_pending_delay(self, delay):
1018+        self._pending_delay = delay
1019+
1020+    def startReading(self):
1021+        deferToThread(self._thread)
1022+        return self.poll(lambda: self._stop == False)
1023+
1024+    def stopReading(self):
1025+        if self._stop is not None:
1026+            self._stop = True
1027+
1028+    def wait_until_stopped(self):
1029+        fileutil.write(os.path.join(self._path.path, u".ignore-me"), "")
1030+        return self.poll(lambda: self._stop is None)
1031+
1032+    def watch(self, path, mask=IN_WATCH_MASK, autoAdd=False, callbacks=None, recursive=False):
1033+        assert self._stop is None, "watch() can only be called before startReading()"
1034+        assert self._filter is None, "only one watch is supported"
1035+        assert isinstance(autoAdd, bool), autoAdd
1036+        assert isinstance(recursive, bool), recursive
1037+        assert autoAdd == recursive, ("autoAdd = %r, recursive = %r, but we need them to be the same"
1038+                                      % (autoAdd, recursive))
1039+
1040+        self._path = path
1041+        path_u = path.path
1042+        if not isinstance(path_u, unicode):
1043+            path_u = path_u.decode(sys.getfilesystemencoding())
1044+            assert isinstance(path_u, unicode), path_u
1045+
1046+        self._filter = FILE_NOTIFY_CHANGE_FILE_NAME | FILE_NOTIFY_CHANGE_DIR_NAME | FILE_NOTIFY_CHANGE_LAST_WRITE
1047+
1048+        if mask & (IN_ACCESS | IN_CLOSE_NOWRITE | IN_OPEN):
1049+            self._filter = self._filter | FILE_NOTIFY_CHANGE_LAST_ACCESS
1050+        if mask & IN_ATTRIB:
1051+            self._filter = self._filter | FILE_NOTIFY_CHANGE_ATTRIBUTES | FILE_NOTIFY_CHANGE_SECURITY
1052+
1053+        self._recursive = recursive
1054+        self._callbacks = callbacks or []
1055+        self._hDirectory = _open_directory(path_u)
1056+
1057+    def _thread(self):
1058+        try:
1059+            assert self._filter is not None, "no watch set"
1060+
1061+            # To call Twisted or Tahoe APIs, use reactor.callFromThread as described in
1062+            # <http://twistedmatrix.com/documents/current/core/howto/threading.html>.
1063+
1064+            fni = FileNotifyInformation()
1065+
1066+            while True:
1067+                self._stop = False
1068+                fni.read_changes(self._hDirectory, self._recursive, self._filter)
1069+                for info in fni:
1070+                    if self._stop:
1071+                        hDirectory = self._hDirectory
1072+                        self._callbacks = None
1073+                        self._hDirectory = None
1074+                        CloseHandle(hDirectory)
1075+                        self._stop = None
1076+                        return
1077+
1078+                    path = self._path.preauthChild(info.filename)  # FilePath with Unicode path
1079+                    mask = _action_to_inotify_mask.get(info.action, IN_CHANGED)
1080+
1081+                    def _maybe_notify():
1082+                        event = (path, mask)
1083+                        if event not in self._pending:
1084+                            self._pending.add(event)
1085+                            def _do_callbacks():
1086+                                self._pending.remove(event)
1087+                                for cb in self._callbacks:
1088+                                    try:
1089+                                        cb(None, path, mask)
1090+                                    except Exception, e:
1091+                                        log.msg(e)
1092+                            reactor.callLater(self._pending_delay, _do_callbacks)
1093+                    reactor.callFromThread(_maybe_notify)
1094+        except Exception, e:
1095+            log.msg(e)
1096+            self.stop = False  # pretend we started
1097+            raise
1098}
1099
1100Context:
1101
1102[node.py: ensure that client and introducer nodes record their port number and use that port on the next restart, fixing a regression caused by #1385. fixes #1469.
1103david-sarah@jacaranda.org**20110806221934
1104 Ignore-this: 1aa9d340b6570320ab2f9edc89c9e0a8
1105]
1106[test_runner.py: fix a race condition in the test when NODE_URL_FILE is written before PORTNUM_FILE. refs #1469
1107david-sarah@jacaranda.org**20110806231842
1108 Ignore-this: ab01ae7cec3a073e29eec473e64052a0
1109]
1110[test_runner.py: cleanups of HOTLINE_FILE writing and removal.
1111david-sarah@jacaranda.org**20110806231652
1112 Ignore-this: 25f5c5d6f5d8faebb26a4ce80110a335
1113]
1114[test_runner.py: remove an unused constant.
1115david-sarah@jacaranda.org**20110806221416
1116 Ignore-this: eade2695cbabbea9cafeaa8debe410bb
1117]
1118[node.py: fix the error path for a missing config option so that it works for a Unicode base directory.
1119david-sarah@jacaranda.org**20110806221007
1120 Ignore-this: 4eb9cc04b2ce05182a274a0d69dafaf3
1121]
1122[test_runner.py: test that client and introducer nodes record their port number and use that port on the next restart. This tests for a regression caused by ref #1385.
1123david-sarah@jacaranda.org**20110806220635
1124 Ignore-this: 40a0c040b142dbddd47e69b3c3712f5
1125]
1126[test_runner.py: fix a bug in CreateNode.do_create introduced in changeset [5114] when the tahoe.cfg file has been written with CRLF line endings. refs #1385
1127david-sarah@jacaranda.org**20110804003032
1128 Ignore-this: 7b7afdcf99da6671afac2d42828883eb
1129]
1130[test_client.py: repair Basic.test_error_on_old_config_files. refs #1385
1131david-sarah@jacaranda.org**20110803235036
1132 Ignore-this: 31e2a9c3febe55948de7e144353663e
1133]
1134[test_checker.py: increase timeout for TooParallel.test_immutable again. The ARM buildslave took 38 seconds, so 40 seconds is too close to the edge; make it 80.
1135david-sarah@jacaranda.org**20110803214042
1136 Ignore-this: 2d8026a6b25534e01738f78d6c7495cb
1137]
1138[test_runner.py: fix RunNode.test_introducer to not rely on the mtime of introducer.furl to detect when the node has restarted. Instead we detect when node.url has been written. refs #1385
1139david-sarah@jacaranda.org**20110803180917
1140 Ignore-this: 11ddc43b107beca42cb78af88c5c394c
1141]
1142[Further improve error message about old config files. refs #1385
1143david-sarah@jacaranda.org**20110803174546
1144 Ignore-this: 9d6cc3c288d9863dce58faafb3855917
1145]
1146[Slightly improve error message about old config files (avoid unnecessary Unicode escaping). refs #1385
1147david-sarah@jacaranda.org**20110803163848
1148 Ignore-this: a3e3930fba7ccf90b8db3d2ed5829df4
1149]
1150[test_checker.py: increase timeout for TooParallel.test_immutable (was consistently failing on ARM buildslave).
1151david-sarah@jacaranda.org**20110803163213
1152 Ignore-this: d0efceaf12628e8791862b80c85b5d56
1153]
1154[Fix the bug that prevents an introducer from starting when introducer.furl already exists. Also remove some dead code that used to read old config files, and rename 'warn_about_old_config_files' to reflect that it's not a warning. refs #1385
1155david-sarah@jacaranda.org**20110803013212
1156 Ignore-this: 2d6cd14bd06a7493b26f2027aff78f4d
1157]
1158[test_runner.py: modify RunNode.test_introducer to test that starting an introducer works when the introducer.furl file already exists. refs #1385
1159david-sarah@jacaranda.org**20110803012704
1160 Ignore-this: 8cf7f27ac4bfbb5ad8ca4a974106d437
1161]
1162[verifier: correct a bug introduced in changeset [5106] that caused us to only verify the first block of a file. refs #1395
1163david-sarah@jacaranda.org**20110802172437
1164 Ignore-this: 87fb77854a839ff217dce73544775b11
1165]
1166[test_repairer: add a deterministic test of share data corruption that always flips the bits of the last byte of the share data. refs #1395
1167david-sarah@jacaranda.org**20110802175841
1168 Ignore-this: 72f54603785007e88220c8d979e08be7
1169]
1170[verifier: serialize the fetching of blocks within a share so that we don't use too much RAM
1171zooko@zooko.com**20110802063703
1172 Ignore-this: debd9bac07dcbb6803f835a9e2eabaa1
1173 
1174 Shares are still verified in parallel, but within a share, don't request a
1175 block until the previous block has been verified and the memory we used to hold
1176 it has been freed up.
1177 
1178 Patch originally due to Brian. This version has a mockery-patchery-style test
1179 which is "low tech" (it implements the patching inline in the test code instead
1180 of using an extension of the mock.patch() function from the mock library) and
1181 which unpatches in case of exception.
1182 
1183 fixes #1395
1184]
1185[add docs about timing-channel attacks
1186Brian Warner <warner@lothar.com>**20110802044541
1187 Ignore-this: 73114d5f5ed9ce252597b707dba3a194
1188]
1189['test-coverage' now needs PYTHONPATH=. to find TOP/twisted/plugins/
1190Brian Warner <warner@lothar.com>**20110802041952
1191 Ignore-this: d40f1f4cb426ea1c362fc961baedde2
1192]
1193[remove nodeid from WriteBucketProxy classes and customers
1194warner@lothar.com**20110801224317
1195 Ignore-this: e55334bb0095de11711eeb3af827e8e8
1196 refs #1363
1197]
1198[remove get_serverid() from ReadBucketProxy and customers, including Checker
1199warner@lothar.com**20110801224307
1200 Ignore-this: 837aba457bc853e4fd413ab1a94519cb
1201 and debug.py dump-share commands
1202 refs #1363
1203]
1204[reject old-style (pre-Tahoe-LAFS-v1.3) configuration files
1205zooko@zooko.com**20110801232423
1206 Ignore-this: b58218fcc064cc75ad8f05ed0c38902b
1207 Check for the existence of any of them and if any are found raise exception which will abort the startup of the node.
1208 This is a backwards-incompatible change for anyone who is still using old-style configuration files.
1209 fixes #1385
1210]
1211[whitespace-cleanup
1212zooko@zooko.com**20110725015546
1213 Ignore-this: 442970d0545183b97adc7bd66657876c
1214]
1215[tests: use fileutil.write() instead of open() to ensure timely close even without CPython-style reference counting
1216zooko@zooko.com**20110331145427
1217 Ignore-this: 75aae4ab8e5fa0ad698f998aaa1888ce
1218 Some of these already had an explicit close() but I went ahead and replaced them with fileutil.write() as well for the sake of uniformity.
1219]
1220[Address Kevan's comment in #776 about Options classes missed when adding 'self.command_name'. refs #776, #1359
1221david-sarah@jacaranda.org**20110801221317
1222 Ignore-this: 8881d42cf7e6a1d15468291b0cb8fab9
1223]
1224[docs/frontends/webapi.rst: change some more instances of 'delete' or 'remove' to 'unlink', change some section titles, and use two blank lines between all sections. refs #776, #1104
1225david-sarah@jacaranda.org**20110801220919
1226 Ignore-this: 572327591137bb05c24c44812d4b163f
1227]
1228[cleanup: implement rm as a synonym for unlink rather than vice-versa. refs #776
1229david-sarah@jacaranda.org**20110801220108
1230 Ignore-this: 598dcbed870f4f6bb9df62de9111b343
1231]
1232[docs/webapi.rst: address Kevan's comments about use of 'delete' on ref #1104
1233david-sarah@jacaranda.org**20110801205356
1234 Ignore-this: 4fbf03864934753c951ddeff64392491
1235]
1236[docs: some changes of 'delete' or 'rm' to 'unlink'. refs #1104
1237david-sarah@jacaranda.org**20110713002722
1238 Ignore-this: 304d2a330d5e6e77d5f1feed7814b21c
1239]
1240[WUI: change the label of the button to unlink a file from 'del' to 'unlink'. Also change some internal names to 'unlink', and allow 't=unlink' as a synonym for 't=delete' in the web-API interface. Incidentally, improve a test to check for the rename button as well as the unlink button. fixes #1104
1241david-sarah@jacaranda.org**20110713001218
1242 Ignore-this: 3eef6b3f81b94a9c0020a38eb20aa069
1243]
1244[src/allmydata/web/filenode.py: delete a stale comment that was made incorrect by changeset [3133].
1245david-sarah@jacaranda.org**20110801203009
1246 Ignore-this: b3912e95a874647027efdc97822dd10e
1247]
1248[fix typo introduced during rebasing of 'remove get_serverid from
1249Brian Warner <warner@lothar.com>**20110801200341
1250 Ignore-this: 4235b0f585c0533892193941dbbd89a8
1251 DownloadStatus.add_dyhb_request and customers' patch, to fix test failure.
1252]
1253[remove get_serverid from DownloadStatus.add_dyhb_request and customers
1254zooko@zooko.com**20110801185401
1255 Ignore-this: db188c18566d2d0ab39a80c9dc8f6be6
1256 This patch is a rebase of a patch originally written by Brian. I didn't change any of the intent of Brian's patch, just ported it to current trunk.
1257 refs #1363
1258]
1259[remove get_serverid from DownloadStatus.add_block_request and customers
1260zooko@zooko.com**20110801185344
1261 Ignore-this: 8bfa8201d6147f69b0fbe31beea9c1e
1262 This is a rebase of a patch Brian originally wrote. I haven't changed the intent of that patch, just ported it to trunk.
1263 refs #1363
1264]
1265[apply zooko's advice: storage_client get_known_servers() returns a frozenset, caller sorts
1266warner@lothar.com**20110801174452
1267 Ignore-this: 2aa13ea6cbed4e9084bd604bf8633692
1268 refs #1363
1269]
1270[test_immutable.Test: rewrite to use NoNetworkGrid, now takes 2.7s not 97s
1271warner@lothar.com**20110801174444
1272 Ignore-this: 54f30b5d7461d2b3514e2a0172f3a98c
1273 remove now-unused ShareManglingMixin
1274 refs #1363
1275]
1276[DownloadStatus.add_known_share wants to be used by Finder, web.status
1277warner@lothar.com**20110801174436
1278 Ignore-this: 1433bcd73099a579abe449f697f35f9
1279 refs #1363
1280]
1281[replace IServer.name() with get_name(), and get_longname()
1282warner@lothar.com**20110801174428
1283 Ignore-this: e5a6f7f6687fd7732ddf41cfdd7c491b
1284 
1285 This patch was originally written by Brian, but was re-recorded by Zooko to use
1286 darcs replace instead of hunks for any file in which it would result in fewer
1287 total hunks.
1288 refs #1363
1289]
1290[upload.py: apply David-Sarah's advice rename (un)contacted(2) trackers to first_pass/second_pass/next_pass
1291zooko@zooko.com**20110801174143
1292 Ignore-this: e36e1420bba0620a0107bd90032a5198
1293 This patch was written by Brian but was re-recorded by Zooko (with David-Sarah looking on) to use darcs replace instead of editing to rename the three variables to their new names.
1294 refs #1363
1295]
1296[Coalesce multiple Share.loop() calls, make downloads faster. Closes #1268.
1297Brian Warner <warner@lothar.com>**20110801151834
1298 Ignore-this: 48530fce36c01c0ff708f61c2de7e67a
1299]
1300[src/allmydata/_auto_deps.py: 'i686' is another way of spelling x86.
1301david-sarah@jacaranda.org**20110801034035
1302 Ignore-this: 6971e0621db2fba794d86395b4d51038
1303]
1304[tahoe_rm.py: better error message when there is no path. refs #1292
1305david-sarah@jacaranda.org**20110122064212
1306 Ignore-this: ff3bb2c9f376250e5fd77eb009e09018
1307]
1308[test_cli.py: Test for error message when 'tahoe rm' is invoked without a path. refs #1292
1309david-sarah@jacaranda.org**20110104105108
1310 Ignore-this: 29ec2f2e0251e446db96db002ad5dd7d
1311]
1312[src/allmydata/__init__.py: suppress a spurious warning from 'bin/tahoe --version[-and-path]' about twisted-web and twisted-core packages.
1313david-sarah@jacaranda.org**20110801005209
1314 Ignore-this: 50e7cd53cca57b1870d9df0361c7c709
1315]
1316[test_cli.py: use to_str on fields loaded using simplejson.loads in new tests. refs #1304
1317david-sarah@jacaranda.org**20110730032521
1318 Ignore-this: d1d6dfaefd1b4e733181bf127c79c00b
1319]
1320[cli: make 'tahoe cp' overwrite mutable files in-place
1321Kevan Carstensen <kevan@isnotajoke.com>**20110729202039
1322 Ignore-this: b2ad21a19439722f05c49bfd35b01855
1323]
1324[SFTP: write an error message to standard error for unrecognized shell commands. Change the existing message for shell sessions to be written to standard error, and refactor some duplicated code. Also change the lines of the error messages to end in CRLF, and take into account Kevan's review comments. fixes #1442, #1446
1325david-sarah@jacaranda.org**20110729233102
1326 Ignore-this: d2f2bb4664f25007d1602bf7333e2cdd
1327]
1328[src/allmydata/scripts/cli.py: fix pyflakes warning.
1329david-sarah@jacaranda.org**20110728021402
1330 Ignore-this: 94050140ddb99865295973f49927c509
1331]
1332[Fix the help synopses of CLI commands to include [options] in the right place. fixes #1359, fixes #636
1333david-sarah@jacaranda.org**20110724225440
1334 Ignore-this: 2a8e488a5f63dabfa9db9efd83768a5
1335]
1336[encodingutil: argv and output encodings are always the same on all platforms. Lose the unnecessary generality of them being different. fixes #1120
1337david-sarah@jacaranda.org**20110629185356
1338 Ignore-this: 5ebacbe6903dfa83ffd3ff8436a97787
1339]
1340[docs/man/tahoe.1: add man page. fixes #1420
1341david-sarah@jacaranda.org**20110724171728
1342 Ignore-this: fc7601ec7f25494288d6141d0ae0004c
1343]
1344[Update the dependency on zope.interface to fix an incompatiblity between Nevow and zope.interface 3.6.4. fixes #1435
1345david-sarah@jacaranda.org**20110721234941
1346 Ignore-this: 2ff3fcfc030fca1a4d4c7f1fed0f2aa9
1347]
1348[frontends/ftpd.py: remove the check for IWriteFile.close since we're now guaranteed to be using Twisted >= 10.1 which has it.
1349david-sarah@jacaranda.org**20110722000320
1350 Ignore-this: 55cd558b791526113db3f83c00ec328a
1351]
1352[Update the dependency on Twisted to >= 10.1. This allows us to simplify some documentation: it's no longer necessary to install pywin32 on Windows, or apply a patch to Twisted in order to use the FTP frontend. fixes #1274, #1438. refs #1429
1353david-sarah@jacaranda.org**20110721233658
1354 Ignore-this: 81b41745477163c9b39c0b59db91cc62
1355]
1356[misc/build_helpers/run_trial.py: undo change to block pywin32 (it didn't work because run_trial.py is no longer used). refs #1334
1357david-sarah@jacaranda.org**20110722035402
1358 Ignore-this: 5d03f544c4154f088e26c7107494bf39
1359]
1360[misc/build_helpers/run_trial.py: ensure that pywin32 is not on the sys.path when running the test suite. Includes some temporary debugging printouts that will be removed. refs #1334
1361david-sarah@jacaranda.org**20110722024907
1362 Ignore-this: 5141a9f83a4085ed4ca21f0bbb20bb9c
1363]
1364[docs/running.rst: use 'tahoe run ~/.tahoe' instead of 'tahoe run' (the default is the current directory, unlike 'tahoe start').
1365david-sarah@jacaranda.org**20110718005949
1366 Ignore-this: 81837fbce073e93d88a3e7ae3122458c
1367]
1368[docs/running.rst: say to put the introducer.furl in tahoe.cfg.
1369david-sarah@jacaranda.org**20110717194315
1370 Ignore-this: 954cc4c08e413e8c62685d58ff3e11f3
1371]
1372[README.txt: say that quickstart.rst is in the docs directory.
1373david-sarah@jacaranda.org**20110717192400
1374 Ignore-this: bc6d35a85c496b77dbef7570677ea42a
1375]
1376[setup: remove the dependency on foolscap's "secure_connections" extra, add a dependency on pyOpenSSL
1377zooko@zooko.com**20110717114226
1378 Ignore-this: df222120d41447ce4102616921626c82
1379 fixes #1383
1380]
1381[test_sftp.py cleanup: remove a redundant definition of failUnlessReallyEqual.
1382david-sarah@jacaranda.org**20110716181813
1383 Ignore-this: 50113380b368c573f07ac6fe2eb1e97f
1384]
1385[docs: add missing link in NEWS.rst
1386zooko@zooko.com**20110712153307
1387 Ignore-this: be7b7eb81c03700b739daa1027d72b35
1388]
1389[contrib: remove the contributed fuse modules and the entire contrib/ directory, which is now empty
1390zooko@zooko.com**20110712153229
1391 Ignore-this: 723c4f9e2211027c79d711715d972c5
1392 Also remove a couple of vestigial references to figleaf, which is long gone.
1393 fixes #1409 (remove contrib/fuse)
1394]
1395[add Protovis.js-based download-status timeline visualization
1396Brian Warner <warner@lothar.com>**20110629222606
1397 Ignore-this: 477ccef5c51b30e246f5b6e04ab4a127
1398 
1399 provide status overlap info on the webapi t=json output, add decode/decrypt
1400 rate tooltips, add zoomin/zoomout buttons
1401]
1402[add more download-status data, fix tests
1403Brian Warner <warner@lothar.com>**20110629222555
1404 Ignore-this: e9e0b7e0163f1e95858aa646b9b17b8c
1405]
1406[prepare for viz: improve DownloadStatus events
1407Brian Warner <warner@lothar.com>**20110629222542
1408 Ignore-this: 16d0bde6b734bb501aa6f1174b2b57be
1409 
1410 consolidate IDownloadStatusHandlingConsumer stuff into DownloadNode
1411]
1412[docs: fix error in crypto specification that was noticed by Taylor R Campbell <campbell+tahoe@mumble.net>
1413zooko@zooko.com**20110629185711
1414 Ignore-this: b921ed60c1c8ba3c390737fbcbe47a67
1415]
1416[setup.py: don't make bin/tahoe.pyscript executable. fixes #1347
1417david-sarah@jacaranda.org**20110130235809
1418 Ignore-this: 3454c8b5d9c2c77ace03de3ef2d9398a
1419]
1420[Makefile: remove targets relating to 'setup.py check_auto_deps' which no longer exists. fixes #1345
1421david-sarah@jacaranda.org**20110626054124
1422 Ignore-this: abb864427a1b91bd10d5132b4589fd90
1423]
1424[Makefile: add 'make check' as an alias for 'make test'. Also remove an unnecessary dependency of 'test' on 'build' and 'src/allmydata/_version.py'. fixes #1344
1425david-sarah@jacaranda.org**20110623205528
1426 Ignore-this: c63e23146c39195de52fb17c7c49b2da
1427]
1428[Rename test_package_initialization.py to (much shorter) test_import.py .
1429Brian Warner <warner@lothar.com>**20110611190234
1430 Ignore-this: 3eb3dbac73600eeff5cfa6b65d65822
1431 
1432 The former name was making my 'ls' listings hard to read, by forcing them
1433 down to just two columns.
1434]
1435[tests: fix tests to accomodate [20110611153758-92b7f-0ba5e4726fb6318dac28fb762a6512a003f4c430]
1436zooko@zooko.com**20110611163741
1437 Ignore-this: 64073a5f39e7937e8e5e1314c1a302d1
1438 Apparently none of the two authors (stercor, terrell), three reviewers (warner, davidsarah, terrell), or one committer (me) actually ran the tests. This is presumably due to #20.
1439 fixes #1412
1440]
1441[wui: right-align the size column in the WUI
1442zooko@zooko.com**20110611153758
1443 Ignore-this: 492bdaf4373c96f59f90581c7daf7cd7
1444 Thanks to Ted "stercor" Rolle Jr. and Terrell Russell.
1445 fixes #1412
1446]
1447[docs: three minor fixes
1448zooko@zooko.com**20110610121656
1449 Ignore-this: fec96579eb95aceb2ad5fc01a814c8a2
1450 CREDITS for arc for stats tweak
1451 fix link to .zip file in quickstart.rst (thanks to ChosenOne for noticing)
1452 English usage tweak
1453]
1454[docs/running.rst: fix stray HTML (not .rst) link noticed by ChosenOne.
1455david-sarah@jacaranda.org**20110609223719
1456 Ignore-this: fc50ac9c94792dcac6f1067df8ac0d4a
1457]
1458[server.py:  get_latencies now reports percentiles _only_ if there are sufficient observations for the interpretation of the percentile to be unambiguous.
1459wilcoxjg@gmail.com**20110527120135
1460 Ignore-this: 2e7029764bffc60e26f471d7c2b6611e
1461 interfaces.py:  modified the return type of RIStatsProvider.get_stats to allow for None as a return value
1462 NEWS.rst, stats.py: documentation of change to get_latencies
1463 stats.rst: now documents percentile modification in get_latencies
1464 test_storage.py:  test_latencies now expects None in output categories that contain too few samples for the associated percentile to be unambiguously reported.
1465 fixes #1392
1466]
1467[docs: revert link in relnotes.txt from NEWS.rst to NEWS, since the former did not exist at revision 5000.
1468david-sarah@jacaranda.org**20110517011214
1469 Ignore-this: 6a5be6e70241e3ec0575641f64343df7
1470]
1471[docs: convert NEWS to NEWS.rst and change all references to it.
1472david-sarah@jacaranda.org**20110517010255
1473 Ignore-this: a820b93ea10577c77e9c8206dbfe770d
1474]
1475[docs: remove out-of-date docs/testgrid/introducer.furl and containing directory. fixes #1404
1476david-sarah@jacaranda.org**20110512140559
1477 Ignore-this: 784548fc5367fac5450df1c46890876d
1478]
1479[scripts/common.py: don't assume that the default alias is always 'tahoe' (it is, but the API of get_alias doesn't say so). refs #1342
1480david-sarah@jacaranda.org**20110130164923
1481 Ignore-this: a271e77ce81d84bb4c43645b891d92eb
1482]
1483[setup: don't catch all Exception from check_requirement(), but only PackagingError and ImportError
1484zooko@zooko.com**20110128142006
1485 Ignore-this: 57d4bc9298b711e4bc9dc832c75295de
1486 I noticed this because I had accidentally inserted a bug which caused AssertionError to be raised from check_requirement().
1487]
1488[M-x whitespace-cleanup
1489zooko@zooko.com**20110510193653
1490 Ignore-this: dea02f831298c0f65ad096960e7df5c7
1491]
1492[docs: fix typo in running.rst, thanks to arch_o_median
1493zooko@zooko.com**20110510193633
1494 Ignore-this: ca06de166a46abbc61140513918e79e8
1495]
1496[relnotes.txt: don't claim to work on Cygwin (which has been untested for some time). refs #1342
1497david-sarah@jacaranda.org**20110204204902
1498 Ignore-this: 85ef118a48453d93fa4cddc32d65b25b
1499]
1500[relnotes.txt: forseeable -> foreseeable. refs #1342
1501david-sarah@jacaranda.org**20110204204116
1502 Ignore-this: 746debc4d82f4031ebf75ab4031b3a9
1503]
1504[replace remaining .html docs with .rst docs
1505zooko@zooko.com**20110510191650
1506 Ignore-this: d557d960a986d4ac8216d1677d236399
1507 Remove install.html (long since deprecated).
1508 Also replace some obsolete references to install.html with references to quickstart.rst.
1509 Fix some broken internal references within docs/historical/historical_known_issues.txt.
1510 Thanks to Ravi Pinjala and Patrick McDonald.
1511 refs #1227
1512]
1513[docs: FTP-and-SFTP.rst: fix a minor error and update the information about which version of Twisted fixes #1297
1514zooko@zooko.com**20110428055232
1515 Ignore-this: b63cfb4ebdbe32fb3b5f885255db4d39
1516]
1517[munin tahoe_files plugin: fix incorrect file count
1518francois@ctrlaltdel.ch**20110428055312
1519 Ignore-this: 334ba49a0bbd93b4a7b06a25697aba34
1520 fixes #1391
1521]
1522[corrected "k must never be smaller than N" to "k must never be greater than N"
1523secorp@allmydata.org**20110425010308
1524 Ignore-this: 233129505d6c70860087f22541805eac
1525]
1526[Fix a test failure in test_package_initialization on Python 2.4.x due to exceptions being stringified differently than in later versions of Python. refs #1389
1527david-sarah@jacaranda.org**20110411190738
1528 Ignore-this: 7847d26bc117c328c679f08a7baee519
1529]
1530[tests: add test for including the ImportError message and traceback entry in the summary of errors from importing dependencies. refs #1389
1531david-sarah@jacaranda.org**20110410155844
1532 Ignore-this: fbecdbeb0d06a0f875fe8d4030aabafa
1533]
1534[allmydata/__init__.py: preserve the message and last traceback entry (file, line number, function, and source line) of ImportErrors in the package versions string. fixes #1389
1535david-sarah@jacaranda.org**20110410155705
1536 Ignore-this: 2f87b8b327906cf8bfca9440a0904900
1537]
1538[remove unused variable detected by pyflakes
1539zooko@zooko.com**20110407172231
1540 Ignore-this: 7344652d5e0720af822070d91f03daf9
1541]
1542[allmydata/__init__.py: Nicer reporting of unparseable version numbers in dependencies. fixes #1388
1543david-sarah@jacaranda.org**20110401202750
1544 Ignore-this: 9c6bd599259d2405e1caadbb3e0d8c7f
1545]
1546[update FTP-and-SFTP.rst: the necessary patch is included in Twisted-10.1
1547Brian Warner <warner@lothar.com>**20110325232511
1548 Ignore-this: d5307faa6900f143193bfbe14e0f01a
1549]
1550[control.py: remove all uses of s.get_serverid()
1551warner@lothar.com**20110227011203
1552 Ignore-this: f80a787953bd7fa3d40e828bde00e855
1553]
1554[web: remove some uses of s.get_serverid(), not all
1555warner@lothar.com**20110227011159
1556 Ignore-this: a9347d9cf6436537a47edc6efde9f8be
1557]
1558[immutable/downloader/fetcher.py: remove all get_serverid() calls
1559warner@lothar.com**20110227011156
1560 Ignore-this: fb5ef018ade1749348b546ec24f7f09a
1561]
1562[immutable/downloader/fetcher.py: fix diversity bug in server-response handling
1563warner@lothar.com**20110227011153
1564 Ignore-this: bcd62232c9159371ae8a16ff63d22c1b
1565 
1566 When blocks terminate (either COMPLETE or CORRUPT/DEAD/BADSEGNUM), the
1567 _shares_from_server dict was being popped incorrectly (using shnum as the
1568 index instead of serverid). I'm still thinking through the consequences of
1569 this bug. It was probably benign and really hard to detect. I think it would
1570 cause us to incorrectly believe that we're pulling too many shares from a
1571 server, and thus prefer a different server rather than asking for a second
1572 share from the first server. The diversity code is intended to spread out the
1573 number of shares simultaneously being requested from each server, but with
1574 this bug, it might be spreading out the total number of shares requested at
1575 all, not just simultaneously. (note that SegmentFetcher is scoped to a single
1576 segment, so the effect doesn't last very long).
1577]
1578[immutable/downloader/share.py: reduce get_serverid(), one left, update ext deps
1579warner@lothar.com**20110227011150
1580 Ignore-this: d8d56dd8e7b280792b40105e13664554
1581 
1582 test_download.py: create+check MyShare instances better, make sure they share
1583 Server objects, now that finder.py cares
1584]
1585[immutable/downloader/finder.py: reduce use of get_serverid(), one left
1586warner@lothar.com**20110227011146
1587 Ignore-this: 5785be173b491ae8a78faf5142892020
1588]
1589[immutable/offloaded.py: reduce use of get_serverid() a bit more
1590warner@lothar.com**20110227011142
1591 Ignore-this: b48acc1b2ae1b311da7f3ba4ffba38f
1592]
1593[immutable/upload.py: reduce use of get_serverid()
1594warner@lothar.com**20110227011138
1595 Ignore-this: ffdd7ff32bca890782119a6e9f1495f6
1596]
1597[immutable/checker.py: remove some uses of s.get_serverid(), not all
1598warner@lothar.com**20110227011134
1599 Ignore-this: e480a37efa9e94e8016d826c492f626e
1600]
1601[add remaining get_* methods to storage_client.Server, NoNetworkServer, and
1602warner@lothar.com**20110227011132
1603 Ignore-this: 6078279ddf42b179996a4b53bee8c421
1604 MockIServer stubs
1605]
1606[upload.py: rearrange _make_trackers a bit, no behavior changes
1607warner@lothar.com**20110227011128
1608 Ignore-this: 296d4819e2af452b107177aef6ebb40f
1609]
1610[happinessutil.py: finally rename merge_peers to merge_servers
1611warner@lothar.com**20110227011124
1612 Ignore-this: c8cd381fea1dd888899cb71e4f86de6e
1613]
1614[test_upload.py: factor out FakeServerTracker
1615warner@lothar.com**20110227011120
1616 Ignore-this: 6c182cba90e908221099472cc159325b
1617]
1618[test_upload.py: server-vs-tracker cleanup
1619warner@lothar.com**20110227011115
1620 Ignore-this: 2915133be1a3ba456e8603885437e03
1621]
1622[happinessutil.py: server-vs-tracker cleanup
1623warner@lothar.com**20110227011111
1624 Ignore-this: b856c84033562d7d718cae7cb01085a9
1625]
1626[upload.py: more tracker-vs-server cleanup
1627warner@lothar.com**20110227011107
1628 Ignore-this: bb75ed2afef55e47c085b35def2de315
1629]
1630[upload.py: fix var names to avoid confusion between 'trackers' and 'servers'
1631warner@lothar.com**20110227011103
1632 Ignore-this: 5d5e3415b7d2732d92f42413c25d205d
1633]
1634[refactor: s/peer/server/ in immutable/upload, happinessutil.py, test_upload
1635warner@lothar.com**20110227011100
1636 Ignore-this: 7ea858755cbe5896ac212a925840fe68
1637 
1638 No behavioral changes, just updating variable/method names and log messages.
1639 The effects outside these three files should be minimal: some exception
1640 messages changed (to say "server" instead of "peer"), and some internal class
1641 names were changed. A few things still use "peer" to minimize external
1642 changes, like UploadResults.timings["peer_selection"] and
1643 happinessutil.merge_peers, which can be changed later.
1644]
1645[storage_client.py: clean up test_add_server/test_add_descriptor, remove .test_servers
1646warner@lothar.com**20110227011056
1647 Ignore-this: efad933e78179d3d5fdcd6d1ef2b19cc
1648]
1649[test_client.py, upload.py:: remove KiB/MiB/etc constants, and other dead code
1650warner@lothar.com**20110227011051
1651 Ignore-this: dc83c5794c2afc4f81e592f689c0dc2d
1652]
1653[test: increase timeout on a network test because Francois's ARM machine hit that timeout
1654zooko@zooko.com**20110317165909
1655 Ignore-this: 380c345cdcbd196268ca5b65664ac85b
1656 I'm skeptical that the test was proceeding correctly but ran out of time. It seems more likely that it had gotten hung. But if we raise the timeout to an even more extravagant number then we can be even more certain that the test was never going to finish.
1657]
1658[docs/configuration.rst: add a "Frontend Configuration" section
1659Brian Warner <warner@lothar.com>**20110222014323
1660 Ignore-this: 657018aa501fe4f0efef9851628444ca
1661 
1662 this points to docs/frontends/*.rst, which were previously underlinked
1663]
1664[web/filenode.py: avoid calling req.finish() on closed HTTP connections. Closes #1366
1665"Brian Warner <warner@lothar.com>"**20110221061544
1666 Ignore-this: 799d4de19933f2309b3c0c19a63bb888
1667]
1668[Add unit tests for cross_check_pkg_resources_versus_import, and a regression test for ref #1355. This requires a little refactoring to make it testable.
1669david-sarah@jacaranda.org**20110221015817
1670 Ignore-this: 51d181698f8c20d3aca58b057e9c475a
1671]
1672[allmydata/__init__.py: .name was used in place of the correct .__name__ when printing an exception. Also, robustify string formatting by using %r instead of %s in some places. fixes #1355.
1673david-sarah@jacaranda.org**20110221020125
1674 Ignore-this: b0744ed58f161bf188e037bad077fc48
1675]
1676[Refactor StorageFarmBroker handling of servers
1677Brian Warner <warner@lothar.com>**20110221015804
1678 Ignore-this: 842144ed92f5717699b8f580eab32a51
1679 
1680 Pass around IServer instance instead of (peerid, rref) tuple. Replace
1681 "descriptor" with "server". Other replacements:
1682 
1683  get_all_servers -> get_connected_servers/get_known_servers
1684  get_servers_for_index -> get_servers_for_psi (now returns IServers)
1685 
1686 This change still needs to be pushed further down: lots of code is now
1687 getting the IServer and then distributing (peerid, rref) internally.
1688 Instead, it ought to distribute the IServer internally and delay
1689 extracting a serverid or rref until the last moment.
1690 
1691 no_network.py was updated to retain parallelism.
1692]
1693[TAG allmydata-tahoe-1.8.2
1694warner@lothar.com**20110131020101]
1695Patch bundle hash:
1696bf7fdfd73b42a2d3785f2f9045f89e153c9e909f