]> granicus.if.org Git - transmission/commitdiff
#5663: Rework directory watching in daemon
authorMike Gelfand <mikedld@mikedld.com>
Sat, 2 Jan 2016 14:28:59 +0000 (14:28 +0000)
committerMike Gelfand <mikedld@mikedld.com>
Sat, 2 Jan 2016 14:28:59 +0000 (14:28 +0000)
Implement BSD/Darwin (kqueue) and Windows (ReadDirectoryChanges) mechanisms
for receiving directory change notifications. Use events instead of polling
for changes. Retry file parsing up to 3 times before giving up.

Huge thanks to missionsix for preparing first two versions of the patch.

17 files changed:
Transmission.xcodeproj/project.pbxproj
configure.ac
daemon/CMakeLists.txt
daemon/Makefile.am
daemon/daemon.c
daemon/watch.c [deleted file]
daemon/watch.h [deleted file]
libtransmission/CMakeLists.txt
libtransmission/Makefile.am
libtransmission/watchdir-common.h [new file with mode: 0644]
libtransmission/watchdir-generic.c [new file with mode: 0644]
libtransmission/watchdir-inotify.c [new file with mode: 0644]
libtransmission/watchdir-kqueue.c [new file with mode: 0644]
libtransmission/watchdir-test.c [new file with mode: 0644]
libtransmission/watchdir-win32.c [new file with mode: 0644]
libtransmission/watchdir.c [new file with mode: 0644]
libtransmission/watchdir.h [new file with mode: 0644]

index 52e19395cee8a69a7fef5646781ba2e8646b4dd6..6839b131cd0173dd068315e139e1e192cbd58c89 100644 (file)
                A2D22A130D65EEE700007D5F /* verify.c in Sources */ = {isa = PBXBuildFile; fileRef = A2D22A100D65EED100007D5F /* verify.c */; };
                A2D307A40D9EC6870051FD27 /* BlocklistDownloader.m in Sources */ = {isa = PBXBuildFile; fileRef = A2D307A30D9EC6870051FD27 /* BlocklistDownloader.m */; };
                A2D307B10D9EC9F50051FD27 /* BlocklistStatusWindow.xib in Resources */ = {isa = PBXBuildFile; fileRef = A2D307B00D9EC9F50051FD27 /* BlocklistStatusWindow.xib */; };
-               A2D5972A0F5AE49E0001AB3C /* watch.c in Sources */ = {isa = PBXBuildFile; fileRef = A2D597280F5AE49E0001AB3C /* watch.c */; };
                A2D77451154CC25700A62B93 /* WebSeedTableView.h in Headers */ = {isa = PBXBuildFile; fileRef = A2D7744F154CC25700A62B93 /* WebSeedTableView.h */; };
                A2D77452154CC25700A62B93 /* WebSeedTableView.m in Sources */ = {isa = PBXBuildFile; fileRef = A2D77450154CC25700A62B93 /* WebSeedTableView.m */; };
                A2D77453154CC72B00A62B93 /* WebSeedTableView.m in Sources */ = {isa = PBXBuildFile; fileRef = A2D77450154CC25700A62B93 /* WebSeedTableView.m */; };
                C1639A7C1A55F57200E42033 /* cdecode.h in Headers */ = {isa = PBXBuildFile; fileRef = C1639A7A1A55F57200E42033 /* cdecode.h */; };
                C1639A7D1A55F57200E42033 /* cencode.h in Headers */ = {isa = PBXBuildFile; fileRef = C1639A7B1A55F57200E42033 /* cencode.h */; };
                C1F690FD1AD0627500D95CF0 /* daemon-posix.c in Sources */ = {isa = PBXBuildFile; fileRef = C1F690FC1AD0627500D95CF0 /* daemon-posix.c */; };
+               C1FEE5771C3223CC00D62832 /* watchdir-common.h in Headers */ = {isa = PBXBuildFile; fileRef = C1FEE5721C3223CC00D62832 /* watchdir-common.h */; };
+               C1FEE5781C3223CC00D62832 /* watchdir-generic.c in Sources */ = {isa = PBXBuildFile; fileRef = C1FEE5731C3223CC00D62832 /* watchdir-generic.c */; };
+               C1FEE5791C3223CC00D62832 /* watchdir-kqueue.c in Sources */ = {isa = PBXBuildFile; fileRef = C1FEE5741C3223CC00D62832 /* watchdir-kqueue.c */; };
+               C1FEE57A1C3223CC00D62832 /* watchdir.c in Sources */ = {isa = PBXBuildFile; fileRef = C1FEE5751C3223CC00D62832 /* watchdir.c */; };
+               C1FEE57B1C3223CC00D62832 /* watchdir.h in Headers */ = {isa = PBXBuildFile; fileRef = C1FEE5761C3223CC00D62832 /* watchdir.h */; };
                D4AF3B2F0C41F7A500D46B6B /* list.c in Sources */ = {isa = PBXBuildFile; fileRef = D4AF3B2D0C41F7A500D46B6B /* list.c */; };
                D4AF3B300C41F7A600D46B6B /* list.h in Headers */ = {isa = PBXBuildFile; fileRef = D4AF3B2E0C41F7A500D46B6B /* list.h */; };
                E138A9780C04D88F00C5426C /* ProgressGradients.m in Sources */ = {isa = PBXBuildFile; fileRef = E138A9760C04D88F00C5426C /* ProgressGradients.m */; };
                A2D307A20D9EC6870051FD27 /* BlocklistDownloader.h */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.c.h; name = BlocklistDownloader.h; path = macosx/BlocklistDownloader.h; sourceTree = "<group>"; };
                A2D307A30D9EC6870051FD27 /* BlocklistDownloader.m */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.c.objc; name = BlocklistDownloader.m; path = macosx/BlocklistDownloader.m; sourceTree = "<group>"; };
                A2D307B00D9EC9F50051FD27 /* BlocklistStatusWindow.xib */ = {isa = PBXFileReference; lastKnownFileType = file.xib; name = BlocklistStatusWindow.xib; path = macosx/BlocklistStatusWindow.xib; sourceTree = "<group>"; };
-               A2D597280F5AE49E0001AB3C /* watch.c */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.c.c; name = watch.c; path = daemon/watch.c; sourceTree = "<group>"; };
-               A2D597290F5AE49E0001AB3C /* watch.h */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.c.h; name = watch.h; path = daemon/watch.h; sourceTree = "<group>"; };
                A2D7744F154CC25700A62B93 /* WebSeedTableView.h */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.c.h; name = WebSeedTableView.h; path = macosx/WebSeedTableView.h; sourceTree = "<group>"; };
                A2D77450154CC25700A62B93 /* WebSeedTableView.m */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.c.objc; name = WebSeedTableView.m; path = macosx/WebSeedTableView.m; sourceTree = "<group>"; };
                A2D8CFBF15FA177A0056E93D /* ru */ = {isa = PBXFileReference; lastKnownFileType = text.plist.strings; name = ru; path = macosx/QuickLookPlugin/ru.lproj/Localizable.strings; sourceTree = SOURCE_ROOT; };
                C1639A7B1A55F57200E42033 /* cencode.h */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.c.h; name = cencode.h; path = "third-party/libb64/b64/cencode.h"; sourceTree = "<group>"; };
                C1F690FC1AD0627500D95CF0 /* daemon-posix.c */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.c.c; name = "daemon-posix.c"; path = "daemon/daemon-posix.c"; sourceTree = "<group>"; };
                C1F690FE1AD0628400D95CF0 /* daemon.h */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.c.h; name = daemon.h; path = daemon/daemon.h; sourceTree = "<group>"; };
+               C1FEE5721C3223CC00D62832 /* watchdir-common.h */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.c.h; name = "watchdir-common.h"; path = "libtransmission/watchdir-common.h"; sourceTree = "<group>"; };
+               C1FEE5731C3223CC00D62832 /* watchdir-generic.c */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.c.c; name = "watchdir-generic.c"; path = "libtransmission/watchdir-generic.c"; sourceTree = "<group>"; };
+               C1FEE5741C3223CC00D62832 /* watchdir-kqueue.c */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.c.c; name = "watchdir-kqueue.c"; path = "libtransmission/watchdir-kqueue.c"; sourceTree = "<group>"; };
+               C1FEE5751C3223CC00D62832 /* watchdir.c */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.c.c; name = watchdir.c; path = libtransmission/watchdir.c; sourceTree = "<group>"; };
+               C1FEE5761C3223CC00D62832 /* watchdir.h */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.c.h; name = watchdir.h; path = libtransmission/watchdir.h; sourceTree = "<group>"; };
                D4AF3B2D0C41F7A500D46B6B /* list.c */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.c.c; name = list.c; path = libtransmission/list.c; sourceTree = "<group>"; };
                D4AF3B2E0C41F7A500D46B6B /* list.h */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.c.h; name = list.h; path = libtransmission/list.h; sourceTree = "<group>"; };
                E138A9750C04D88F00C5426C /* ProgressGradients.h */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.c.h; name = ProgressGradients.h; path = macosx/ProgressGradients.h; sourceTree = "<group>"; };
                                A2A4EA0B0DE106E8000CE197 /* ConvertUTF.h */,
                                A2A4EA0A0DE106E8000CE197 /* ConvertUTF.c */,
                                4DB74F070E8CD75100AEB1A8 /* wildmat.c */,
+                               C1FEE5751C3223CC00D62832 /* watchdir.c */,
+                               C1FEE5761C3223CC00D62832 /* watchdir.h */,
+                               C1FEE5731C3223CC00D62832 /* watchdir-generic.c */,
+                               C1FEE5741C3223CC00D62832 /* watchdir-kqueue.c */,
+                               C1FEE5721C3223CC00D62832 /* watchdir-common.h */,
                        );
                        name = libtransmission;
                        sourceTree = "<group>";
                                C1F690FE1AD0628400D95CF0 /* daemon.h */,
                                C1F690FC1AD0627500D95CF0 /* daemon-posix.c */,
                                BEFC1C140C07756200B0BB3C /* remote.c */,
-                               A2D597280F5AE49E0001AB3C /* watch.c */,
-                               A2D597290F5AE49E0001AB3C /* watch.h */,
                        );
                        name = daemon;
                        sourceTree = "<group>";
                                BEFC1E450C07861A00B0BB3C /* net.h in Headers */,
                                BEFC1E490C07861A00B0BB3C /* metainfo.h in Headers */,
                                BEFC1E4D0C07861A00B0BB3C /* session.h in Headers */,
+                               C1FEE5771C3223CC00D62832 /* watchdir-common.h in Headers */,
                                BEFC1E4E0C07861A00B0BB3C /* inout.h in Headers */,
                                BEFC1E520C07861A00B0BB3C /* fdlimit.h in Headers */,
                                BEFC1E550C07861A00B0BB3C /* completion.h in Headers */,
                                A29DF8BA0DB2544C00D04E5A /* resume.h in Headers */,
                                A29DF8BB0DB2544C00D04E5A /* torrent.h in Headers */,
                                A29DF8BE0DB2545F00D04E5A /* verify.h in Headers */,
+                               C1FEE57B1C3223CC00D62832 /* watchdir.h in Headers */,
                                A2AAB6650DE0D08B00E04DDA /* blocklist.h in Headers */,
                                A2A4E9210DE0F7E9000CE197 /* web.h in Headers */,
                                A2A4EA0F0DE106EE000CE197 /* ConvertUTF.h in Headers */,
                                BEFC1E4A0C07861A00B0BB3C /* metainfo.c in Sources */,
                                BEFC1E4F0C07861A00B0BB3C /* inout.c in Sources */,
                                BEFC1E530C07861A00B0BB3C /* fdlimit.c in Sources */,
+                               C1FEE5781C3223CC00D62832 /* watchdir-generic.c in Sources */,
                                BEFC1E560C07861A00B0BB3C /* completion.c in Sources */,
                                BEFC1E580C07861A00B0BB3C /* clients.c in Sources */,
                                A2BE9C520C1E4AF5002D16E6 /* makemeta.c in Sources */,
                                4D80185910BBC0B0008A4AF2 /* magnet.c in Sources */,
                                A209EE5C1144B51E002B02D1 /* history.c in Sources */,
                                A220EC5B118C8A060022B4BE /* tr-lpd.c in Sources */,
+                               C1FEE57A1C3223CC00D62832 /* watchdir.c in Sources */,
                                A23547E211CD0B090046EAE6 /* cache.c in Sources */,
                                A284214412DA663E00FBDDBB /* tr-udp.c in Sources */,
                                A2679294130E00A000CB7464 /* tr-utp.c in Sources */,
                                A23F29A2132A447400E9A83B /* announcer-http.c in Sources */,
+                               C1FEE5791C3223CC00D62832 /* watchdir-kqueue.c in Sources */,
                                A2AA9BE1132CAC8E00FA131E /* announcer-udp.c in Sources */,
                                A2D77452154CC25700A62B93 /* WebSeedTableView.m in Sources */,
                                A2A7B32A164F87D400B98C65 /* jsonsl.c in Sources */,
                        buildActionMask = 2147483647;
                        files = (
                                BEFC1C1A0C07756200B0BB3C /* daemon.c in Sources */,
-                               A2D5972A0F5AE49E0001AB3C /* watch.c in Sources */,
                                C1F690FD1AD0627500D95CF0 /* daemon-posix.c in Sources */,
                        );
                        runOnlyForDeploymentPostprocessing = 0;
index 45580dbcd2abcfdfb3d9513119578c3d32b5d950..1e5bc02de3972b8a633abcdc34dc23fc784e6da2 100644 (file)
@@ -204,35 +204,32 @@ dnl ----------------------------------------------------------------------------
 dnl
 dnl file monitoring for the daemon
 
-AC_CHECK_HEADER([sys/inotify.h],
-                [AC_CHECK_FUNC([inotify_init],[have_inotify="yes"],[have_inotify="no"])],
-                [have_inotify="no"])
 AC_ARG_WITH([inotify],
-            [AS_HELP_STRING([--with-inotify],[Enable inotify support (default=auto)])],
-            [want_inotify=${withval}],
-            [want_inotify=${have_inotify}])
-if test "x$want_inotify" = "xyes" ; then
-    if test "x$have_inotify" = "xyes"; then
-      AC_DEFINE([WITH_INOTIFY],[1])
-    else
-      AC_MSG_ERROR("inotify not found!")
-    fi
-fi
+            [AS_HELP_STRING([--with-inotify], [Enable inotify support (default=auto)])],
+            [WANT_INOTIFY=${withval}],
+            [WANT_INOTIFY=auto])
+HAVE_INOTIFY=0
+AS_IF([test "x$WANT_INOTIFY" != "xno"],
+      [AC_CHECK_HEADER([sys/inotify.h],
+                       [AC_CHECK_FUNC([inotify_init],
+                                      [HAVE_INOTIFY=1])],
+                       [AS_IF([test "x$WANT_INOTIFY" = "xyes"],
+                              [AC_MSG_ERROR("inotify not found!")])])])
+AM_CONDITIONAL([USE_INOTIFY], [test "x$WANT_INOTIFY" != "xno" -a $HAVE_INOTIFY -eq 1])
 
-AC_CHECK_HEADER([sys/event.h],
-                [AC_CHECK_FUNC([kqueue],[have_kqueue="yes"],[have_kqueue="no"])],
-                [have_kqueue="no"])
 AC_ARG_WITH([kqueue],
             [AS_HELP_STRING([--with-kqueue],[Enable kqueue support (default=auto)])],
-            [want_kqueue=${withval}],
-            [want_kqueue=${have_kqueue}])
-if test "x$want_kqueue" = "xyes" ; then
-    if test "x$have_kqueue" = "xyes"; then
-      AC_DEFINE([WITH_KQUEUE],[1])
-    else
-      AC_MSG_ERROR("kqueue not found!")
-    fi
-fi
+            [WITH_KQUEUE=${withval}],
+            [WITH_KQUEUE=auto])
+HAVE_KQUEUE=0
+AS_IF([test "x$WITH_KQUEUE" != "xno"],
+      [AC_CHECK_HEADER([sys/event.h],
+                       [AC_CHECK_FUNC([kqueue],
+                                      [HAVE_KQUEUE=1])],
+                       [AS_IF([test "x$WANT_KQUEUE" = "xyes"],
+                              [AC_MSG_ERROR("kqueue not found!")])])])
+AM_CONDITIONAL([USE_KQUEUE], [test "x$WITH_KQUEUE" != "xno" -a $HAVE_KQUEUE -eq 1])
+
 
 AC_CHECK_HEADERS([sys/statvfs.h \
                   xfs/xfs.h])
index 56b0d1fccbd04c901889257399c5b8996a0a69f1..e8f8b348306d1deb6e1d4ec265bdb097b3b02a3a 100644 (file)
@@ -1,13 +1,5 @@
 project(trdaemon)
 
-if(WITH_INOTIFY)
-    add_definitions(-DWITH_INOTIFY)
-endif()
-
-if(WITH_KQUEUE)
-    add_definitions(-DWITH_KQUEUE)
-endif()
-
 if(WITH_SYSTEMD)
     add_definitions(-DUSE_SYSTEMD_DAEMON)
 endif()
@@ -22,7 +14,6 @@ set(${PROJECT_NAME}_SOURCES
     daemon.c
     daemon-posix.c
     daemon-win32.c
-    watch.c
 )
 
 if(WIN32)
@@ -33,7 +24,6 @@ endif()
 
 set(${PROJECT_NAME}_HEADERS
     daemon.h
-    watch.h
 )
 
 tr_win32_app_info(${PROJECT_NAME}_WIN32_RC_FILE
index fc0b7b1992256e1345265005f876a1609ef932e6..677f765c587d320576059d7cb963867e636105f1 100644 (file)
@@ -34,11 +34,9 @@ LDADD = \
     @PTHREAD_LIBS@ \
     ${LIBM}
 
-noinst_HEADERS = \
-    daemon.h \
-    watch.h
+noinst_HEADERS = daemon.h
 
-transmission_daemon_SOURCES = daemon.c watch.c
+transmission_daemon_SOURCES = daemon.c
 transmission_remote_SOURCES = remote.c
 
 if WIN32
index 149668b8cd622b796744a57454d10a0a5ee0cfde..cefd7ab62ed7e626ff05d24abab76282704b4a52 100644 (file)
@@ -21,7 +21,6 @@
  #include <unistd.h> /* getpid */
 #endif
 
-#include <event2/buffer.h>
 #include <event2/event.h>
 
 #include <libtransmission/transmission.h>
@@ -32,6 +31,7 @@
 #include <libtransmission/utils.h>
 #include <libtransmission/variant.h>
 #include <libtransmission/version.h>
+#include <libtransmission/watchdir.h>
 
 #ifdef USE_SYSTEMD_DAEMON
  #include <systemd/sd-daemon.h>
@@ -41,7 +41,6 @@
 #endif
 
 #include "daemon.h"
-#include "watch.h"
 
 #define MY_NAME "transmission-daemon"
 
@@ -185,10 +184,17 @@ getConfigDir (int argc, const char * const * argv)
     return configDir;
 }
 
-static void
-onFileAdded (tr_session * session, const char * dir, const char * file)
+static tr_watchdir_status
+onFileAdded (tr_watchdir_t   dir,
+             const char    * name,
+             void          * context)
 {
-    char * filename = tr_buildPath (dir, file, NULL);
+    tr_session * session = context;
+
+    if (!tr_str_has_suffix (name, ".torrent"))
+        return TR_WATCHDIR_IGNORE;
+
+    char * filename = tr_buildPath (tr_watchdir_get_path (dir), name, NULL);
     tr_ctor * ctor = tr_ctorNew (session);
     int err = tr_ctorSetMetainfoFromFile (ctor, filename);
 
@@ -197,19 +203,19 @@ onFileAdded (tr_session * session, const char * dir, const char * file)
         tr_torrentNew (ctor, &err, NULL);
 
         if (err == TR_PARSE_ERR)
-            tr_logAddError ("Error parsing .torrent file \"%s\"", file);
+            tr_logAddError ("Error parsing .torrent file \"%s\"", name);
         else
         {
             bool trash = false;
             const bool test = tr_ctorGetDeleteSource (ctor, &trash);
 
-            tr_logAddInfo ("Parsing .torrent file successful \"%s\"", file);
+            tr_logAddInfo ("Parsing .torrent file successful \"%s\"", name);
 
             if (test && trash)
             {
                 tr_error * error = NULL;
 
-                tr_logAddInfo ("Deleting input .torrent file \"%s\"", file);
+                tr_logAddInfo ("Deleting input .torrent file \"%s\"", name);
                 if (!tr_sys_path_remove (filename, &error))
                 {
                     tr_logAddError ("Error deleting .torrent file: %s", error->message);
@@ -224,9 +230,15 @@ onFileAdded (tr_session * session, const char * dir, const char * file)
             }
         }
     }
+    else
+    {
+        err = TR_PARSE_ERR;
+    }
 
     tr_ctorFree (ctor);
     tr_free (filename);
+
+    return err == TR_PARSE_ERR ? TR_WATCHDIR_RETRY : TR_WATCHDIR_ACCEPT;
 }
 
 static void
@@ -293,12 +305,11 @@ reportStatus (void)
 }
 
 static void
-periodicUpdate (evutil_socket_t fd UNUSED, short what UNUSED, void *watchdir)
+periodicUpdate (evutil_socket_t   fd UNUSED,
+                short             what UNUSED,
+                void            * context UNUSED)
 {
-    dtr_watchdir_update (watchdir);
-
     pumpLogMessages (logfile);
-
     reportStatus ();
 }
 
@@ -479,10 +490,10 @@ daemon_start (void * raw_arg,
 {
     bool boolVal;
     const char * pid_filename;
-    dtr_watchdir * watchdir = NULL;
     bool pidfile_created = false;
     tr_session * session = NULL;
-    struct event *status_ev;
+    struct event * status_ev = NULL;
+    tr_watchdir_t watchdir = NULL;
 
     struct daemon_data * const arg = raw_arg;
     tr_variant * const settings = &arg->settings;
@@ -558,7 +569,8 @@ daemon_start (void * raw_arg,
             && *dir)
         {
             tr_logAddInfo ("Watching \"%s\" for new .torrent files", dir);
-            watchdir = dtr_watchdir_new (mySession, dir, onFileAdded);
+            if ((watchdir = tr_watchdir_new (dir, &onFileAdded, mySession, ev_base)) == NULL)
+                goto cleanup;
         }
     }
 
@@ -581,7 +593,7 @@ daemon_start (void * raw_arg,
     /* Create new timer event to report daemon status */
     {
         struct timeval one_sec = { 1, 0 };
-        status_ev = event_new(ev_base, -1, EV_PERSIST, &periodicUpdate, watchdir);
+        status_ev = event_new(ev_base, -1, EV_PERSIST, &periodicUpdate, NULL);
         if (status_ev == NULL)
         {
             tr_logAddError("Failed to create status event %s", tr_strerror(errno));
@@ -607,6 +619,8 @@ cleanup:
     sd_notify( 0, "STATUS=Closing transmission session...\n" );
     printf ("Closing transmission session...");
 
+    tr_watchdir_free (watchdir);
+
     if (status_ev)
     {
         event_del(status_ev);
@@ -615,7 +629,6 @@ cleanup:
     event_base_free(ev_base);
 
     tr_sessionSaveSettings (mySession, configDir, settings);
-    dtr_watchdir_free (watchdir);
     tr_sessionClose (mySession);
     pumpLogMessages (logfile);
     printf (" done.\n");
diff --git a/daemon/watch.c b/daemon/watch.c
deleted file mode 100644 (file)
index 4fee35c..0000000
+++ /dev/null
@@ -1,267 +0,0 @@
-/*
- * This file Copyright (C) 2009-2014 Mnemosyne LLC
- *
- * It may be used under the GNU GPL versions 2 or 3
- * or any future license endorsed by Mnemosyne LLC.
- *
- * $Id$
- */
-
-#ifdef WITH_INOTIFY
-  #include <sys/inotify.h>
-  #include <sys/select.h>
-  #include <unistd.h> /* close */
-#else
-  #include <event2/buffer.h> /* evbuffer */
-#endif
-
-#include <errno.h>
-#include <string.h> /* strlen () */
-#include <stdio.h> /* perror () */
-
-#include <libtransmission/transmission.h>
-#include <libtransmission/file.h>
-#include <libtransmission/log.h>
-#include <libtransmission/utils.h> /* tr_buildPath (), tr_logAddInfo () */
-#include "watch.h"
-
-struct dtr_watchdir
-{
-    tr_session * session;
-    char * dir;
-    dtr_watchdir_callback * callback;
-#ifdef WITH_INOTIFY
-    int inotify_fd;
-#else /* readdir implementation */
-    time_t lastTimeChecked;
-    struct evbuffer * lastFiles;
-#endif
-};
-
-/***
-****  INOTIFY IMPLEMENTATION
-***/
-
-#if defined (WITH_INOTIFY)
-
-/* how many inotify events to try to batch into a single read */
-#define EVENT_BATCH_COUNT 50
-/* size of the event structure, not counting name */
-#define EVENT_SIZE (sizeof (struct inotify_event))
-/* reasonable guess as to size of 50 events */
-#define BUF_LEN (EVENT_BATCH_COUNT * (EVENT_SIZE + 16) + 2048)
-
-#define DTR_INOTIFY_MASK (IN_CLOSE_WRITE|IN_MOVED_TO|IN_CREATE|IN_ONLYDIR)
-
-static void
-watchdir_new_impl (dtr_watchdir * w)
-{
-    int i;
-    tr_sys_dir_t odir;
-    w->inotify_fd = inotify_init ();
-
-    if (w->inotify_fd < 0)
-    {
-        i = -1;
-    }
-    else
-    {
-        tr_logAddInfo ("Using inotify to watch directory \"%s\"", w->dir);
-        i = inotify_add_watch (w->inotify_fd, w->dir, DTR_INOTIFY_MASK);
-    }
-
-    if (i < 0)
-    {
-        tr_logAddError ("Unable to watch \"%s\": %s", w->dir, tr_strerror (errno));
-    }
-    else if ((odir = tr_sys_dir_open (w->dir, NULL)) != TR_BAD_SYS_DIR)
-    {
-        const char * name;
-        while ((name = tr_sys_dir_read_name (odir, NULL)) != NULL)
-        {
-            if (!tr_str_has_suffix (name, ".torrent")) /* skip non-torrents */
-                continue;
-
-            tr_logAddInfo ("Found new .torrent file \"%s\" in watchdir \"%s\"", name, w->dir);
-            w->callback (w->session, w->dir, name);
-        }
-
-        tr_sys_dir_close (odir, NULL);
-    }
-
-}
-static void
-watchdir_free_impl (dtr_watchdir * w)
-{
-    if (w->inotify_fd >= 0)
-    {
-        inotify_rm_watch (w->inotify_fd, DTR_INOTIFY_MASK);
-
-        close (w->inotify_fd);
-    }
-}
-static void
-watchdir_update_impl (dtr_watchdir * w)
-{
-    int ret;
-    fd_set rfds;
-    struct timeval time;
-    const int fd = w->inotify_fd;
-
-    /* timeout after one second */
-    time.tv_sec = 1;
-    time.tv_usec = 0;
-
-    /* make the fd_set hold the inotify fd */
-    FD_ZERO (&rfds);
-    FD_SET (fd, &rfds);
-
-    /* check for added files */
-    ret = select (fd+1, &rfds, NULL, NULL, &time);
-    if (ret < 0) {
-        perror ("select");
-    } else if (!ret) {
-        /* timed out! */
-    } else if (FD_ISSET (fd, &rfds)) {
-        int i = 0;
-        char buf[BUF_LEN];
-        int len = read (fd, buf, sizeof (buf));
-        while (i < len) {
-            struct inotify_event * event = (struct inotify_event *) &buf[i];
-            const char * name = event->name;
-            if (tr_str_has_suffix (name, ".torrent"))
-            {
-                tr_logAddInfo ("Found new .torrent file \"%s\" in watchdir \"%s\"", name, w->dir);
-                w->callback (w->session, w->dir, name);
-            }
-            i += EVENT_SIZE +  event->len;
-        }
-    }
-}
-
-#else /* WITH_INOTIFY */
-
-/***
-****  READDIR IMPLEMENTATION
-***/
-
-#define WATCHDIR_POLL_INTERVAL_SECS 10
-
-#define FILE_DELIMITER '\t'
-
-static void
-watchdir_new_impl (dtr_watchdir * w UNUSED)
-{
-    tr_logAddInfo ("Using readdir to watch directory \"%s\"", w->dir);
-    w->lastFiles = evbuffer_new ();
-}
-static void
-watchdir_free_impl (dtr_watchdir * w)
-{
-    evbuffer_free (w->lastFiles);
-}
-
-static char*
-get_key_from_file (const char * filename, const size_t len)
-{
-    return tr_strdup_printf ("%c%*.*s%d", FILE_DELIMITER, (int)len, (int)len, filename, FILE_DELIMITER);
-}
-
-static void
-add_file_to_list (struct evbuffer * buf, const char * filename, size_t len)
-{
-    char * key = get_key_from_file (filename, len);
-    evbuffer_add (buf, key, strlen (key));
-    tr_free (key);
-}
-static bool
-is_file_in_list (struct evbuffer * buf, const char * filename, size_t len)
-{
-    bool in_list;
-    struct evbuffer_ptr ptr;
-    char * key = get_key_from_file (filename, len);
-
-    ptr = evbuffer_search (buf, key, strlen (key), NULL);
-    in_list = ptr.pos != -1;
-
-    tr_free (key);
-    return in_list;
-}
-static void
-watchdir_update_impl (dtr_watchdir * w)
-{
-    tr_sys_path_info info;
-    tr_sys_dir_t odir;
-    const time_t oldTime = w->lastTimeChecked;
-    const char * dirname = w->dir;
-    struct evbuffer * curFiles = evbuffer_new ();
-
-    if (oldTime + WATCHDIR_POLL_INTERVAL_SECS < time (NULL) &&
-        tr_sys_path_get_info (dirname, 0, &info, NULL) &&
-        info.type == TR_SYS_PATH_IS_DIRECTORY &&
-        (odir = tr_sys_dir_open (dirname, NULL)) != TR_BAD_SYS_DIR)
-    {
-        const char * name;
-        while ((name = tr_sys_dir_read_name (odir, NULL)) != NULL)
-        {
-            size_t len;
-
-            if (*name == '.') /* skip dotfiles */
-                continue;
-            if (!tr_str_has_suffix (name, ".torrent")) /* skip non-torrents */
-                continue;
-
-            len = strlen (name);
-            add_file_to_list (curFiles, name, len);
-
-            /* if this file wasn't here last time, try adding it */
-            if (!is_file_in_list (w->lastFiles, name, len)) {
-                tr_logAddInfo ("Found new .torrent file \"%s\" in watchdir \"%s\"", name, w->dir);
-                w->callback (w->session, w->dir, name);
-            }
-        }
-
-        tr_sys_dir_close (odir, NULL);
-        w->lastTimeChecked = time (NULL);
-        evbuffer_free (w->lastFiles);
-        w->lastFiles = curFiles;
-    }
-}
-
-#endif
-
-/***
-****
-***/
-
-dtr_watchdir*
-dtr_watchdir_new (tr_session * session, const char * dir, dtr_watchdir_callback callback)
-{
-    dtr_watchdir * w = tr_new0 (dtr_watchdir, 1);
-
-    w->session = session;
-    w->dir = tr_strdup (dir);
-    w->callback = callback;
-
-    watchdir_new_impl (w);
-
-    return w;
-}
-
-void
-dtr_watchdir_update (dtr_watchdir * w)
-{
-    if (w != NULL)
-        watchdir_update_impl (w);
-}
-
-void
-dtr_watchdir_free (dtr_watchdir * w)
-{
-    if (w != NULL)
-    {
-        watchdir_free_impl (w);
-        tr_free (w->dir);
-        tr_free (w);
-    }
-}
diff --git a/daemon/watch.h b/daemon/watch.h
deleted file mode 100644 (file)
index dc108c0..0000000
+++ /dev/null
@@ -1,23 +0,0 @@
-/*
- * This file Copyright (C) 2009-2014 Mnemosyne LLC
- *
- * It may be used under the GNU GPL versions 2 or 3
- * or any future license endorsed by Mnemosyne LLC.
- *
- * $Id$
- */
-
-#ifndef DTR_WATCH_H
-#define DTR_WATCH_H
-
-typedef struct dtr_watchdir dtr_watchdir;
-
-typedef void (dtr_watchdir_callback)(tr_session * session, const char * dir, const char * file);
-
-dtr_watchdir* dtr_watchdir_new (tr_session * session, const char * dir, dtr_watchdir_callback cb);
-
-void dtr_watchdir_update (dtr_watchdir * w);
-
-void dtr_watchdir_free (dtr_watchdir * w);
-
-#endif
index d8ef7f37d7b6b52b531b4fd6b0bf193c3ed795eb..7589223e66ef9c4de68d07a196b52cb58174a27a 100644 (file)
@@ -62,6 +62,11 @@ set(${PROJECT_NAME}_SOURCES
     variant.c
     variant-json.c
     verify.c
+    watchdir.c
+    watchdir-generic.c
+    watchdir-inotify.c
+    watchdir-kqueue.c
+    watchdir-win32.c
     web.c
     webseed.c
     wildmat.c
@@ -74,10 +79,22 @@ foreach(CP cyassl openssl polarssl)
     endif()
 endforeach()
 
+if(WITH_INOTIFY)
+    add_definitions(-DWITH_INOTIFY)
+else()
+    set_source_files_properties(watchdir-inotify.c PROPERTIES HEADER_FILE_ONLY ON)
+endif()
+
+if(WITH_KQUEUE)
+    add_definitions(-DWITH_KQUEUE)
+else()
+    set_source_files_properties(watchdir-kqueue.c PROPERTIES HEADER_FILE_ONLY ON)
+endif()
+
 if(WIN32)
     set_source_files_properties(file-posix.c PROPERTIES HEADER_FILE_ONLY ON)
 else()
-    set_source_files_properties(file-win32.c PROPERTIES HEADER_FILE_ONLY ON)
+    set_source_files_properties(file-win32.c watchdir-win32.c PROPERTIES HEADER_FILE_ONLY ON)
 endif()
 
 set(${PROJECT_NAME}_PUBLIC_HEADERS
@@ -92,6 +109,7 @@ set(${PROJECT_NAME}_PUBLIC_HEADERS
     transmission.h
     utils.h
     variant.h
+    watchdir.h
     web.h
     ${PROJECT_BINARY_DIR}/version.h
 )
@@ -140,6 +158,7 @@ set(${PROJECT_NAME}_PRIVATE_HEADERS
     variant-common.h
     verify.h
     version.h
+    watchdir-common.h
     webseed.h
 )
 
@@ -240,7 +259,8 @@ if(ENABLE_TESTS)
 
     set(crypto-test_ADD_SOURCES crypto-test-ref.h)
 
-    foreach(T bitfield blocklist clients crypto error file history json magnet metainfo move peer-msgs quark rename rpc session tr-getopt utils variant)
+    foreach(T bitfield blocklist clients crypto error file history json magnet metainfo move peer-msgs quark rename rpc session
+              tr-getopt utils variant watchdir)
         set(TP ${TR_NAME}-test-${T})
         add_executable(${TP} ${T}-test.c ${${T}-test_ADD_SOURCES})
         target_link_libraries(${TP} ${TR_NAME} ${TR_NAME}-test)
index a77fafbe12f717838793b3b1dc74bc9c4c652a12..275b76734516f818a93d44f1fbabf4cd7884dc8c 100644 (file)
@@ -72,12 +72,22 @@ libtransmission_a_SOURCES = \
   variant-benc.c \
   variant-json.c \
   verify.c \
+  watchdir.c \
+  watchdir-generic.c \
   web.c \
   webseed.c \
   wildmat.c
 
+if USE_INOTIFY
+libtransmission_a_SOURCES += watchdir-inotify.c
+endif
+
+if USE_KQUEUE
+libtransmission_a_SOURCES += watchdir-kqueue.c
+endif
+
 if WIN32
-libtransmission_a_SOURCES += file-win32.c
+libtransmission_a_SOURCES += file-win32.c watchdir-win32.c
 else
 libtransmission_a_SOURCES += file-posix.c
 endif
@@ -150,6 +160,8 @@ noinst_HEADERS = \
   variant-common.h \
   verify.h \
   version.h \
+  watchdir.h \
+  watchdir-common.h \
   web.h \
   webseed.h
 
@@ -173,7 +185,8 @@ TESTS = \
   session-test \
   tr-getopt-test \
   utils-test \
-  variant-test
+  variant-test \
+  watchdir-test
 
 noinst_PROGRAMS = $(TESTS)
 
@@ -270,6 +283,10 @@ variant_test_SOURCES = variant-test.c $(TEST_SOURCES)
 variant_test_LDADD = ${apps_ldadd}
 variant_test_LDFLAGS = ${apps_ldflags}
 
+watchdir_test_SOURCES = watchdir-test.c $(TEST_SOURCES)
+watchdir_test_LDADD = ${apps_ldadd}
+watchdir_test_LDFLAGS = ${apps_ldflags}
+
 rename_test_SOURCES = rename-test.c $(TEST_SOURCES)
 rename_test_LDADD = ${apps_ldadd}
 rename_test_LDFLAGS = ${apps_ldflags}
diff --git a/libtransmission/watchdir-common.h b/libtransmission/watchdir-common.h
new file mode 100644 (file)
index 0000000..56695b9
--- /dev/null
@@ -0,0 +1,55 @@
+/*
+ * This file Copyright (C) 2015-2016 Mnemosyne LLC
+ *
+ * It may be used under the GNU GPL versions 2 or 3
+ * or any future license endorsed by Mnemosyne LLC.
+ *
+ * $Id$
+ */
+
+#ifndef TR_WATCHDIR_COMMON_H
+#define TR_WATCHDIR_COMMON_H
+
+#ifndef __LIBTRANSMISSION_WATCHDIR_MODULE__
+ #error only the libtransmission watchdir module should #include this header.
+#endif
+
+struct tr_ptrArray;
+
+typedef struct tr_watchdir_backend
+{
+  void (* free_func) (struct tr_watchdir_backend *);
+}
+tr_watchdir_backend;
+
+#define BACKEND_DOWNCAST(b) ((tr_watchdir_backend *) (b))
+
+/* ... */
+
+tr_watchdir_backend * tr_watchdir_get_backend    (tr_watchdir_t        handle);
+
+struct event_base   * tr_watchdir_get_event_base (tr_watchdir_t        handle);
+
+/* ... */
+
+void                  tr_watchdir_process        (tr_watchdir_t        handle,
+                                                  const char         * name);
+
+void                  tr_watchdir_scan           (tr_watchdir_t        handle,
+                                                  struct tr_ptrArray * dir_entries);
+
+/* ... */
+
+tr_watchdir_backend * tr_watchdir_generic_new    (tr_watchdir_t        handle);
+
+#ifdef WITH_INOTIFY
+tr_watchdir_backend * tr_watchdir_inotify_new    (tr_watchdir_t        handle);
+#endif
+#ifdef WITH_KQUEUE
+tr_watchdir_backend * tr_watchdir_kqueue_new     (tr_watchdir_t        handle);
+#endif
+#ifdef _WIN32
+tr_watchdir_backend * tr_watchdir_win32_new      (tr_watchdir_t        handle);
+#endif
+
+#endif /* TR_WATCHDIR_COMMON_H */
diff --git a/libtransmission/watchdir-generic.c b/libtransmission/watchdir-generic.c
new file mode 100644 (file)
index 0000000..275bdd0
--- /dev/null
@@ -0,0 +1,114 @@
+/*
+ * This file Copyright (C) 2015-2016 Mnemosyne LLC
+ *
+ * It may be used under the GNU GPL versions 2 or 3
+ * or any future license endorsed by Mnemosyne LLC.
+ *
+ * $Id$
+ */
+
+#include <assert.h>
+#include <errno.h>
+
+#include <event2/event.h>
+
+#define __LIBTRANSMISSION_WATCHDIR_MODULE__
+
+#include "transmission.h"
+#include "log.h"
+#include "ptrarray.h"
+#include "utils.h"
+#include "watchdir.h"
+#include "watchdir-common.h"
+
+/***
+****
+***/
+
+#define log_error(...) (!tr_logLevelIsActive (TR_LOG_ERROR) ? (void) 0 : \
+  tr_logAddMessage (__FILE__, __LINE__, TR_LOG_ERROR, "watchdir:generic", __VA_ARGS__))
+
+/***
+****
+***/
+
+typedef struct tr_watchdir_generic
+{
+  tr_watchdir_backend   base;
+
+  struct event        * event;
+  tr_ptrArray           dir_entries;
+}
+tr_watchdir_generic;
+
+#define BACKEND_UPCAST(b) ((tr_watchdir_generic *) (b))
+
+/* Non-static and mutable for unit tests */
+struct timeval tr_watchdir_generic_interval = { 10, 0 };
+
+/***
+****
+***/
+
+static void
+tr_watchdir_generic_on_event (evutil_socket_t   fd UNUSED,
+                              short             type UNUSED,
+                              void            * context)
+{
+  const tr_watchdir_t handle = context;
+  tr_watchdir_generic * const backend = BACKEND_UPCAST (tr_watchdir_get_backend (handle));
+
+  tr_watchdir_scan (handle, &backend->dir_entries);
+}
+
+static void
+tr_watchdir_generic_free (tr_watchdir_backend * backend_base)
+{
+  tr_watchdir_generic * const backend = BACKEND_UPCAST (backend_base);
+
+  if (backend == NULL)
+    return;
+
+  assert (backend->base.free_func == &tr_watchdir_generic_free);
+
+  if (backend->event != NULL)
+    {
+      event_del (backend->event);
+      event_free (backend->event);
+    }
+
+  tr_ptrArrayDestruct (&backend->dir_entries, &tr_free);
+
+  tr_free (backend);
+}
+
+tr_watchdir_backend *
+tr_watchdir_generic_new (tr_watchdir_t handle)
+{
+  tr_watchdir_generic * backend;
+
+  backend = tr_new0 (tr_watchdir_generic, 1);
+  backend->base.free_func = &tr_watchdir_generic_free;
+
+  if ((backend->event = event_new (tr_watchdir_get_event_base (handle), -1, EV_PERSIST,
+                                   &tr_watchdir_generic_on_event, handle)) == NULL)
+    {
+      log_error ("Failed to create event: %s", tr_strerror (errno));
+      goto fail;
+    }
+
+  if (event_add (backend->event, &tr_watchdir_generic_interval) == -1)
+    {
+      log_error ("Failed to add event: %s", tr_strerror (errno));
+      goto fail;
+    }
+
+  /* Run initial scan on startup */
+  event_active (backend->event, EV_READ, 0);
+
+  return BACKEND_DOWNCAST (backend);
+
+fail:
+  tr_watchdir_generic_free (BACKEND_DOWNCAST (backend));
+  return NULL;
+}
diff --git a/libtransmission/watchdir-inotify.c b/libtransmission/watchdir-inotify.c
new file mode 100644 (file)
index 0000000..a7a2515
--- /dev/null
@@ -0,0 +1,203 @@
+/*
+ * This file Copyright (C) 2015-2016 Mnemosyne LLC
+ *
+ * It may be used under the GNU GPL versions 2 or 3
+ * or any future license endorsed by Mnemosyne LLC.
+ *
+ * $Id$
+ */
+
+#include <assert.h>
+#include <errno.h>
+#include <limits.h> /* NAME_MAX */
+#include <stdlib.h> /* realloc () */
+
+#include <unistd.h> /* close () */
+
+#include <sys/inotify.h>
+
+#include <event2/bufferevent.h>
+#include <event2/event.h>
+
+#define __LIBTRANSMISSION_WATCHDIR_MODULE__
+
+#include "transmission.h"
+#include "log.h"
+#include "utils.h"
+#include "watchdir.h"
+#include "watchdir-common.h"
+
+/***
+****
+***/
+
+#define log_error(...) (!tr_logLevelIsActive (TR_LOG_ERROR) ? (void) 0 : \
+  tr_logAddMessage (__FILE__, __LINE__, TR_LOG_ERROR, "watchdir:inotify", __VA_ARGS__))
+
+/***
+****
+***/
+
+typedef struct tr_watchdir_inotify
+{
+  tr_watchdir_backend   base;
+
+  int                   infd;
+  int                   inwd;
+  struct bufferevent  * event;
+}
+tr_watchdir_inotify;
+
+#define BACKEND_UPCAST(b) ((tr_watchdir_inotify *) (b))
+
+#define INOTIFY_WATCH_MASK (IN_CLOSE_WRITE | IN_MOVED_TO | IN_CREATE)
+
+/***
+****
+***/
+
+static void
+tr_watchdir_inotify_on_first_scan (evutil_socket_t   fd UNUSED,
+                                   short             type UNUSED,
+                                   void            * context)
+{
+  const tr_watchdir_t handle = context;
+
+  tr_watchdir_scan (handle, NULL);
+}
+
+static void
+tr_watchdir_inotify_on_event (struct bufferevent * event,
+                              void               * context)
+{
+  assert (context != NULL);
+
+  const tr_watchdir_t handle = context;
+  tr_watchdir_inotify * const backend = BACKEND_UPCAST (tr_watchdir_get_backend (handle));
+  struct inotify_event ev;
+  size_t nread;
+  size_t name_size = NAME_MAX + 1;
+  char * name = tr_new (char, name_size);
+
+  /* Read the size of the struct excluding name into buf. Guaranteed to have at
+     least sizeof (ev) available */
+  while ((nread = bufferevent_read (event, &ev, sizeof (ev))) != 0)
+    {
+      if (nread == (size_t) -1)
+        {
+          log_error ("Failed to read inotify event: %s", tr_strerror (errno));
+          break;
+        }
+
+      if (nread != sizeof (ev))
+        {
+          log_error ("Failed to read inotify event: expected %zu, got %zu bytes.",
+                     sizeof (ev), nread);
+          break;
+        }
+
+      assert (ev.wd == backend->inwd);
+      assert ((ev.mask & INOTIFY_WATCH_MASK) != 0);
+      assert (ev.len > 0);
+
+      if (ev.len > name_size)
+        {
+          name_size = ev.len;
+          name = tr_renew (char, name, name_size);
+        }
+
+      /* Consume entire name into buffer */
+      if ((nread = bufferevent_read (event, name, ev.len)) == (size_t) -1)
+        {
+          log_error ("Failed to read inotify name: %s", tr_strerror (errno));
+          break;
+        }
+
+      if (nread != ev.len)
+        {
+          log_error ("Failed to read inotify name: expected %" PRIu32 ", got %zu bytes.",
+                     ev.len, nread);
+          break;
+        }
+
+      tr_watchdir_process (handle, name);
+    }
+
+  tr_free (name);
+}
+
+static void
+tr_watchdir_inotify_free (tr_watchdir_backend * backend_base)
+{
+  tr_watchdir_inotify * const backend = BACKEND_UPCAST (backend_base);
+
+  if (backend == NULL)
+    return;
+
+  assert (backend->base.free_func == &tr_watchdir_inotify_free);
+
+  if (backend->event != NULL)
+    {
+       bufferevent_disable (backend->event, EV_READ);
+       bufferevent_free (backend->event);
+    }
+
+  if (backend->infd != -1)
+    {
+      if (backend->inwd != -1)
+        inotify_rm_watch (backend->infd, backend->inwd);
+      close (backend->infd);
+    }
+
+  tr_free (backend);
+}
+
+tr_watchdir_backend *
+tr_watchdir_inotify_new (tr_watchdir_t handle)
+{
+  const char * const path = tr_watchdir_get_path (handle);
+  tr_watchdir_inotify * backend;
+
+  backend = tr_new0 (tr_watchdir_inotify, 1);
+  backend->base.free_func = &tr_watchdir_inotify_free;
+  backend->infd = -1;
+  backend->inwd = -1;
+
+  if ((backend->infd = inotify_init ()) == -1)
+    {
+      log_error ("Unable to inotify_init: %s", tr_strerror (errno));
+      goto fail;
+    }
+
+  if ((backend->inwd = inotify_add_watch (backend->infd, path,
+                                          INOTIFY_WATCH_MASK | IN_ONLYDIR)) == -1)
+    {
+      log_error ("Failed to setup watchdir \"%s\": %s (%d)", path,
+                 tr_strerror (errno), errno);
+      goto fail;
+    }
+
+  if ((backend->event = bufferevent_socket_new (tr_watchdir_get_event_base (handle),
+                                                backend->infd, 0)) == NULL)
+    {
+      log_error ("Failed to create event buffer: %s", tr_strerror (errno));
+      goto fail;
+    }
+
+  /* Guarantees at least the sizeof an inotify event will be available in the
+     event buffer */
+  bufferevent_setwatermark (backend->event, EV_READ, sizeof (struct inotify_event), 0);
+  bufferevent_setcb (backend->event, &tr_watchdir_inotify_on_event, NULL, NULL, handle);
+  bufferevent_enable (backend->event, EV_READ);
+
+  /* Perform an initial scan on the directory */
+  if (event_base_once (tr_watchdir_get_event_base (handle), -1, EV_TIMEOUT,
+                       &tr_watchdir_inotify_on_first_scan, handle, NULL) == -1)
+    log_error ("Failed to perform initial scan: %s", tr_strerror (errno));
+
+  return BACKEND_DOWNCAST (backend);
+
+fail:
+  tr_watchdir_inotify_free (BACKEND_DOWNCAST (backend));
+  return NULL;
+}
diff --git a/libtransmission/watchdir-kqueue.c b/libtransmission/watchdir-kqueue.c
new file mode 100644 (file)
index 0000000..bf4f418
--- /dev/null
@@ -0,0 +1,170 @@
+/*
+ * This file Copyright (C) 2015-2016 Mnemosyne LLC
+ *
+ * It may be used under the GNU GPL versions 2 or 3
+ * or any future license endorsed by Mnemosyne LLC.
+ *
+ * $Id$
+ */
+
+#include <assert.h>
+#include <errno.h>
+#include <string.h> /* strcmp () */
+
+#include <fcntl.h> /* open () */
+#include <unistd.h> /* close () */
+
+#include <sys/types.h>
+#include <sys/event.h>
+
+#ifndef O_EVTONLY
+ #define O_EVTONLY O_RDONLY
+#endif
+
+#include <event2/event.h>
+
+#define __LIBTRANSMISSION_WATCHDIR_MODULE__
+
+#include "transmission.h"
+#include "log.h"
+#include "ptrarray.h"
+#include "utils.h"
+#include "watchdir.h"
+#include "watchdir-common.h"
+
+/***
+****
+***/
+
+#define log_error(...) (!tr_logLevelIsActive (TR_LOG_ERROR) ? (void) 0 : \
+  tr_logAddMessage (__FILE__, __LINE__, TR_LOG_ERROR, "watchdir:kqueue", __VA_ARGS__))
+
+/***
+****
+***/
+
+typedef struct tr_watchdir_kqueue
+{
+  tr_watchdir_backend   base;
+
+  int                   kq;
+  int                   dirfd;
+  struct event        * event;
+  tr_ptrArray           dir_entries;
+}
+tr_watchdir_kqueue;
+
+#define BACKEND_UPCAST(b) ((tr_watchdir_kqueue *) (b))
+
+#define KQUEUE_WATCH_MASK (NOTE_WRITE | NOTE_EXTEND)
+
+/***
+****
+***/
+
+static void
+tr_watchdir_kqueue_on_event (evutil_socket_t   fd UNUSED,
+                             short             type UNUSED,
+                             void            * context)
+{
+  const tr_watchdir_t handle = context;
+  tr_watchdir_kqueue * const backend = BACKEND_UPCAST (tr_watchdir_get_backend (handle));
+  struct kevent ke;
+  const struct timespec ts = { 0, 0 };
+
+  if (kevent (backend->kq, NULL, 0, &ke, 1, &ts) == -1)
+    {
+      log_error ("Failed to fetch kevent: %s", tr_strerror (errno));
+      return;
+    }
+
+  /* Read directory with generic scan */
+  tr_watchdir_scan (handle, &backend->dir_entries);
+}
+
+static void
+tr_watchdir_kqueue_free (tr_watchdir_backend * backend_base)
+{
+  tr_watchdir_kqueue * const backend = BACKEND_UPCAST (backend_base);
+
+  if (backend == NULL)
+    return;
+
+  assert (backend->base.free_func == &tr_watchdir_kqueue_free);
+
+  if (backend->event != NULL)
+    {
+      event_del (backend->event);
+      event_free (backend->event);
+    }
+
+  if (backend->kq != -1)
+    close (backend->kq);
+  if (backend->dirfd != -1)
+    close (backend->dirfd);
+
+  tr_ptrArrayDestruct (&backend->dir_entries, &tr_free);
+
+  tr_free (backend);
+}
+
+tr_watchdir_backend *
+tr_watchdir_kqueue_new (tr_watchdir_t handle)
+{
+  const char * const path = tr_watchdir_get_path (handle);
+  struct kevent ke;
+  tr_watchdir_kqueue * backend;
+
+  backend = tr_new0 (tr_watchdir_kqueue, 1);
+  backend->base.free_func = &tr_watchdir_kqueue_free;
+  backend->kq = -1;
+  backend->dirfd = -1;
+
+  if ((backend->kq = kqueue ()) == -1)
+    {
+      log_error ("Failed to start kqueue");
+      goto fail;
+    }
+
+  /* Open fd for watching */
+  if ((backend->dirfd = open (path, O_RDONLY | O_EVTONLY)) == -1)
+    {
+      log_error ("Failed to passively watch directory \"%s\": %s", path,
+                 tr_strerror (errno));
+      goto fail;
+    }
+
+  /* Register kevent filter with kqueue descriptor */
+  EV_SET (&ke, backend->dirfd, EVFILT_VNODE, EV_ADD | EV_ENABLE | EV_CLEAR,
+          KQUEUE_WATCH_MASK, 0, NULL);
+  if (kevent (backend->kq, &ke, 1, NULL, 0, NULL) == -1)
+    {
+      log_error ("Failed to set directory event filter with fd %d: %s", backend->kq,
+                 tr_strerror (errno));
+      goto fail;
+    }
+
+  /* Create libevent task for event descriptor */
+  if ((backend->event = event_new (tr_watchdir_get_event_base (handle), backend->kq,
+                                   EV_READ | EV_ET | EV_PERSIST,
+                                   &tr_watchdir_kqueue_on_event, handle)) == NULL)
+    {
+      log_error ("Failed to create event: %s", tr_strerror (errno));
+      goto fail;
+    }
+
+  if (event_add (backend->event, NULL) == -1)
+    {
+      log_error ("Failed to add event: %s", tr_strerror (errno));
+      goto fail;
+    }
+
+  /* Trigger one event for the initial scan */
+  event_active (backend->event, EV_READ, 0);
+
+  return BACKEND_DOWNCAST (backend);
+
+fail:
+  tr_watchdir_kqueue_free (BACKEND_DOWNCAST (backend));
+  return NULL;
+}
diff --git a/libtransmission/watchdir-test.c b/libtransmission/watchdir-test.c
new file mode 100644 (file)
index 0000000..d77b7bf
--- /dev/null
@@ -0,0 +1,395 @@
+/*
+ * This file Copyright (C) 2015-2016 Mnemosyne LLC
+ *
+ * It may be used under the GNU GPL versions 2 or 3
+ * or any future license endorsed by Mnemosyne LLC.
+ *
+ * $Id$
+ */
+
+#include <event2/event.h>
+
+#include "transmission.h"
+#include "file.h"
+#include "net.h"
+#include "utils.h"
+#include "watchdir.h"
+
+#include "libtransmission-test.h"
+
+/***
+****
+***/
+
+typedef struct callback_data
+{
+  tr_watchdir_t        dir;
+  char               * name;
+  tr_watchdir_status   result;
+}
+callback_data;
+
+#define CB_DATA_STATIC_INIT { NULL, NULL, 0 }
+
+struct event_base * ev_base = NULL;
+
+extern struct timeval tr_watchdir_generic_interval;
+extern unsigned int   tr_watchdir_retry_limit;
+extern struct timeval tr_watchdir_retry_start_interval;
+extern struct timeval tr_watchdir_retry_max_interval;
+
+const struct timeval FIFTY_MSEC       = { 0,  50000 };
+const struct timeval ONE_HUNDRED_MSEC = { 0, 100000 };
+const struct timeval TWO_HUNDRED_MSEC = { 0, 200000 };
+
+static void
+process_events (void)
+{
+  event_base_loopexit (ev_base, &TWO_HUNDRED_MSEC);
+  event_base_dispatch (ev_base);
+}
+
+static tr_watchdir_status
+callback (tr_watchdir_t   dir,
+          const char    * name,
+          void          * context)
+{
+  callback_data * const data = context;
+
+  if (data->result != TR_WATCHDIR_RETRY)
+    {
+      data->dir = dir;
+
+      if (data->name != NULL)
+        tr_free (data->name);
+      data->name = tr_strdup (name);
+    }
+
+  return data->result;
+}
+
+static void
+reset_callback_data (callback_data      * data,
+                     tr_watchdir_status   result)
+{
+  tr_free (data->name);
+
+  data->dir = NULL;
+  data->name = NULL;
+  data->result = result;
+}
+
+static void
+create_file (const char * parent_dir,
+             const char * name)
+{
+  char * const path = tr_buildPath (parent_dir, name, NULL);
+  libtest_create_file_with_string_contents (path, "");
+  tr_free (path);
+}
+
+static void
+create_dir (const char * parent_dir,
+            const char * name)
+{
+  char * const path = tr_buildPath (parent_dir, name, NULL);
+  tr_sys_dir_create (path, 0, 0700, NULL);
+  tr_free (path);
+}
+
+/***
+****
+***/
+
+static int
+test_construct (void)
+{
+  char * const test_dir = libtest_sandbox_create ();
+  tr_watchdir_t wd;
+
+  ev_base = event_base_new();
+
+  wd = tr_watchdir_new (test_dir, &callback, NULL, ev_base);
+  check (wd != NULL);
+  check (tr_sys_path_is_same (test_dir, tr_watchdir_get_path (wd), NULL));
+
+  tr_watchdir_free (wd);
+
+  event_base_free (ev_base);
+
+  libtest_sandbox_destroy (test_dir);
+  tr_free (test_dir);
+  return 0;
+}
+
+static int
+test_initial_scan (void)
+{
+  char * const test_dir = libtest_sandbox_create ();
+
+  ev_base = event_base_new();
+
+  /* Speed up generic implementation */
+  tr_watchdir_generic_interval = ONE_HUNDRED_MSEC;
+
+  {
+    callback_data wd_data = CB_DATA_STATIC_INIT;
+    reset_callback_data (&wd_data, TR_WATCHDIR_ACCEPT);
+
+    tr_watchdir_t wd = tr_watchdir_new (test_dir, &callback, &wd_data, ev_base);
+    check (wd != NULL);
+
+    process_events ();
+    check_ptr_eq (NULL, wd_data.dir);
+    check_ptr_eq (NULL, wd_data.name);
+
+    tr_watchdir_free (wd);
+    reset_callback_data (&wd_data, TR_WATCHDIR_ACCEPT);
+  }
+
+  create_file (test_dir, "test");
+
+  {
+    callback_data wd_data = CB_DATA_STATIC_INIT;
+    reset_callback_data (&wd_data, TR_WATCHDIR_ACCEPT);
+
+    tr_watchdir_t wd = tr_watchdir_new (test_dir, &callback, &wd_data, ev_base);
+    check (wd != NULL);
+
+    process_events ();
+    check_ptr_eq (wd, wd_data.dir);
+    check_streq ("test", wd_data.name);
+
+    tr_watchdir_free (wd);
+    reset_callback_data (&wd_data, TR_WATCHDIR_ACCEPT);
+  }
+
+  event_base_free (ev_base);
+
+  libtest_sandbox_destroy (test_dir);
+  tr_free (test_dir);
+  return 0;
+}
+
+static int
+test_watch (void)
+{
+  char * const test_dir = libtest_sandbox_create ();
+  callback_data wd_data = CB_DATA_STATIC_INIT;
+  tr_watchdir_t wd;
+
+  ev_base = event_base_new();
+
+  /* Speed up generic implementation */
+  tr_watchdir_generic_interval = ONE_HUNDRED_MSEC;
+
+  reset_callback_data (&wd_data, TR_WATCHDIR_ACCEPT);
+  wd = tr_watchdir_new (test_dir, &callback, &wd_data, ev_base);
+  check (wd != NULL);
+
+  process_events ();
+  check_ptr_eq (NULL, wd_data.dir);
+  check_ptr_eq (NULL, wd_data.name);
+
+  create_file (test_dir, "test");
+
+  process_events ();
+  check_ptr_eq (wd, wd_data.dir);
+  check_streq ("test", wd_data.name);
+
+  reset_callback_data (&wd_data, TR_WATCHDIR_IGNORE);
+  create_file (test_dir, "test2");
+
+  process_events ();
+  check_ptr_eq (wd, wd_data.dir);
+  check_streq ("test2", wd_data.name);
+
+  reset_callback_data (&wd_data, TR_WATCHDIR_IGNORE);
+  create_dir (test_dir, "test3");
+
+  process_events ();
+  check_ptr_eq (NULL, wd_data.dir);
+  check_ptr_eq (NULL, wd_data.name);
+
+  tr_watchdir_free (wd);
+  reset_callback_data (&wd_data, TR_WATCHDIR_ACCEPT);
+
+  event_base_free (ev_base);
+
+  libtest_sandbox_destroy (test_dir);
+  tr_free (test_dir);
+  return 0;
+}
+
+static int
+test_watch_two_dirs (void)
+{
+  char * const test_dir = libtest_sandbox_create ();
+  char * const dir1 = tr_buildPath (test_dir, "a", NULL);
+  char * const dir2 = tr_buildPath (test_dir, "b", NULL);
+  callback_data wd1_data = CB_DATA_STATIC_INIT, wd2_data = CB_DATA_STATIC_INIT;
+  tr_watchdir_t wd1, wd2;
+
+  ev_base = event_base_new();
+
+  /* Speed up generic implementation */
+  tr_watchdir_generic_interval = ONE_HUNDRED_MSEC;
+
+  create_dir (dir1, NULL);
+  create_dir (dir2, NULL);
+
+  reset_callback_data (&wd1_data, TR_WATCHDIR_ACCEPT);
+  wd1 = tr_watchdir_new (dir1, &callback, &wd1_data, ev_base);
+  check (wd1 != NULL);
+
+  reset_callback_data (&wd2_data, TR_WATCHDIR_ACCEPT);
+  wd2 = tr_watchdir_new (dir2, &callback, &wd2_data, ev_base);
+  check (wd2 != NULL);
+
+  process_events ();
+  check_ptr_eq (NULL, wd1_data.dir);
+  check_ptr_eq (NULL, wd1_data.name);
+  check_ptr_eq (NULL, wd2_data.dir);
+  check_ptr_eq (NULL, wd2_data.name);
+
+  create_file (dir1, "test");
+
+  process_events ();
+  check_ptr_eq (wd1, wd1_data.dir);
+  check_streq ("test", wd1_data.name);
+  check_ptr_eq (NULL, wd2_data.dir);
+  check_ptr_eq (NULL, wd2_data.name);
+
+  reset_callback_data (&wd1_data, TR_WATCHDIR_ACCEPT);
+  reset_callback_data (&wd2_data, TR_WATCHDIR_ACCEPT);
+  create_file (dir2, "test2");
+
+  process_events ();
+  check_ptr_eq (NULL, wd1_data.dir);
+  check_ptr_eq (NULL, wd1_data.name);
+  check_ptr_eq (wd2, wd2_data.dir);
+  check_streq ("test2", wd2_data.name);
+
+  reset_callback_data (&wd1_data, TR_WATCHDIR_IGNORE);
+  reset_callback_data (&wd2_data, TR_WATCHDIR_IGNORE);
+  create_file (dir1, "test3");
+  create_file (dir2, "test4");
+
+  process_events ();
+  check_ptr_eq (wd1, wd1_data.dir);
+  check_streq ("test3", wd1_data.name);
+  check_ptr_eq (wd2, wd2_data.dir);
+  check_streq ("test4", wd2_data.name);
+
+  reset_callback_data (&wd1_data, TR_WATCHDIR_ACCEPT);
+  reset_callback_data (&wd2_data, TR_WATCHDIR_ACCEPT);
+  create_file (dir1, "test5");
+  create_dir (dir2, "test5");
+
+  process_events ();
+  check_ptr_eq (wd1, wd1_data.dir);
+  check_streq ("test5", wd1_data.name);
+  check_ptr_eq (NULL, wd2_data.dir);
+  check_ptr_eq (NULL, wd2_data.name);
+
+  reset_callback_data (&wd1_data, TR_WATCHDIR_ACCEPT);
+  reset_callback_data (&wd2_data, TR_WATCHDIR_ACCEPT);
+  create_dir (dir1, "test6");
+  create_file (dir2, "test6");
+
+  process_events ();
+  check_ptr_eq (NULL, wd1_data.dir);
+  check_ptr_eq (NULL, wd1_data.name);
+  check_ptr_eq (wd2, wd2_data.dir);
+  check_streq ("test6", wd2_data.name);
+
+  reset_callback_data (&wd1_data, TR_WATCHDIR_ACCEPT);
+  reset_callback_data (&wd2_data, TR_WATCHDIR_ACCEPT);
+  create_dir (dir1, "test7");
+  create_dir (dir2, "test7");
+
+  process_events ();
+  check_ptr_eq (NULL, wd1_data.dir);
+  check_ptr_eq (NULL, wd1_data.name);
+  check_ptr_eq (NULL, wd2_data.dir);
+  check_ptr_eq (NULL, wd2_data.name);
+
+  tr_watchdir_free (wd2);
+  reset_callback_data (&wd2_data, TR_WATCHDIR_ACCEPT);
+
+  tr_watchdir_free (wd1);
+  reset_callback_data (&wd1_data, TR_WATCHDIR_ACCEPT);
+
+  event_base_free (ev_base);
+
+  tr_free (dir2);
+  tr_free (dir1);
+  libtest_sandbox_destroy (test_dir);
+  tr_free (test_dir);
+  return 0;
+}
+
+static int
+test_retry (void)
+{
+  char * const test_dir = libtest_sandbox_create ();
+  callback_data wd_data = CB_DATA_STATIC_INIT;
+  tr_watchdir_t wd;
+
+  ev_base = event_base_new();
+
+  /* Speed up generic implementation */
+  tr_watchdir_generic_interval = ONE_HUNDRED_MSEC;
+
+  /* Tune retry logic */
+  tr_watchdir_retry_limit = 10;
+  tr_watchdir_retry_start_interval = FIFTY_MSEC;
+  tr_watchdir_retry_max_interval = tr_watchdir_retry_start_interval;
+
+  reset_callback_data (&wd_data, TR_WATCHDIR_RETRY);
+  wd = tr_watchdir_new (test_dir, &callback, &wd_data, ev_base);
+  check (wd != NULL);
+
+  process_events ();
+  check_ptr_eq (NULL, wd_data.dir);
+  check_ptr_eq (NULL, wd_data.name);
+
+  create_file (test_dir, "test");
+
+  process_events ();
+  check_ptr_eq (NULL, wd_data.dir);
+  check_ptr_eq (NULL, wd_data.name);
+
+  reset_callback_data (&wd_data, TR_WATCHDIR_ACCEPT);
+
+  process_events ();
+  check_ptr_eq (wd, wd_data.dir);
+  check_streq ("test", wd_data.name);
+
+  tr_watchdir_free (wd);
+  reset_callback_data (&wd_data, TR_WATCHDIR_ACCEPT);
+
+  event_base_free (ev_base);
+
+  libtest_sandbox_destroy (test_dir);
+  tr_free (test_dir);
+  return 0;
+}
+
+/***
+****
+***/
+
+int
+main (void)
+{
+  const testFunc tests[] = { test_construct,
+                             test_initial_scan,
+                             test_watch,
+                             test_watch_two_dirs,
+                             test_retry };
+
+  tr_net_init ();
+
+  return runTests (tests, NUM_TESTS (tests));
+}
diff --git a/libtransmission/watchdir-win32.c b/libtransmission/watchdir-win32.c
new file mode 100644 (file)
index 0000000..cdaa52c
--- /dev/null
@@ -0,0 +1,286 @@
+/*
+ * This file Copyright (C) 2015-2016 Mnemosyne LLC
+ *
+ * It may be used under the GNU GPL versions 2 or 3
+ * or any future license endorsed by Mnemosyne LLC.
+ *
+ * $Id$
+ */
+
+#include <assert.h>
+#include <errno.h>
+#include <stddef.h> /* offsetof */
+#include <stdlib.h> /* realloc () */
+
+#include <process.h> /* _beginthreadex () */
+
+#include <windows.h>
+
+#include <event2/bufferevent.h>
+#include <event2/event.h>
+#include <event2/util.h>
+
+#define __LIBTRANSMISSION_WATCHDIR_MODULE__
+
+#include "transmission.h"
+#include "log.h"
+#include "net.h"
+#include "utils.h"
+#include "watchdir.h"
+#include "watchdir-common.h"
+
+/***
+****
+***/
+
+#define log_error(...) (!tr_logLevelIsActive (TR_LOG_ERROR) ? (void) 0 : \
+  tr_logAddMessage (__FILE__, __LINE__, TR_LOG_ERROR, "watchdir:win32", __VA_ARGS__))
+
+/***
+****
+***/
+
+typedef struct tr_watchdir_win32
+{
+  tr_watchdir_backend   base;
+
+  HANDLE                fd;
+  OVERLAPPED            overlapped;
+  DWORD                 buffer[8 * 1024 / sizeof (DWORD)];
+  evutil_socket_t       notify_pipe[2];
+  struct bufferevent  * event;
+  HANDLE                thread;
+}
+tr_watchdir_win32;
+
+#define BACKEND_UPCAST(b) ((tr_watchdir_win32 *) (b))
+
+#define WIN32_WATCH_MASK (FILE_NOTIFY_CHANGE_FILE_NAME | FILE_NOTIFY_CHANGE_SIZE | FILE_NOTIFY_CHANGE_LAST_WRITE)
+
+/***
+****
+***/
+
+static unsigned int __stdcall
+tr_watchdir_win32_thread (void * context)
+{
+  const tr_watchdir_t handle = context;
+  tr_watchdir_win32 * const backend = BACKEND_UPCAST (tr_watchdir_get_backend (handle));
+  DWORD bytes_transferred;
+
+  while (GetOverlappedResultEx (backend->fd, &backend->overlapped, &bytes_transferred,
+                                INFINITE, FALSE))
+    {
+      PFILE_NOTIFY_INFORMATION info = (PFILE_NOTIFY_INFORMATION) backend->buffer;
+
+      while (info->NextEntryOffset != 0)
+        *((BYTE **) &info) += info->NextEntryOffset;
+
+      info->NextEntryOffset = bytes_transferred - ((BYTE *) info - (BYTE *) backend->buffer);
+
+      send (backend->notify_pipe[1], (const char *) backend->buffer, bytes_transferred, 0);
+
+      if (!ReadDirectoryChangesW (backend->fd, backend->buffer, sizeof (backend->buffer), FALSE,
+                                  WIN32_WATCH_MASK, NULL, &backend->overlapped, NULL))
+        {
+          log_error ("Failed to read directory changes");
+          return 0;
+        }
+    }
+
+  if (GetLastError () != ERROR_OPERATION_ABORTED)
+    log_error ("Failed to wait for directory changes");
+
+  return 0;
+}
+
+static void
+tr_watchdir_win32_on_first_scan (evutil_socket_t   fd UNUSED,
+                                 short             type UNUSED,
+                                 void            * context)
+{
+  const tr_watchdir_t handle = context;
+
+  tr_watchdir_scan (handle, NULL);
+}
+
+static void
+tr_watchdir_win32_on_event (struct bufferevent * event,
+                            void               * context)
+{
+  const tr_watchdir_t handle = context;
+  size_t nread;
+  size_t name_size = MAX_PATH * sizeof (WCHAR);
+  char * buffer = tr_malloc (sizeof (FILE_NOTIFY_INFORMATION) + name_size);
+  PFILE_NOTIFY_INFORMATION ev = (PFILE_NOTIFY_INFORMATION) buffer;
+  const size_t header_size = offsetof (FILE_NOTIFY_INFORMATION, FileName);
+
+  /* Read the size of the struct excluding name into buf. Guaranteed to have at
+     least sizeof (*ev) available */
+  while ((nread = bufferevent_read (event, ev, header_size)) != 0)
+    {
+      if (nread == (size_t) -1)
+        {
+          log_error ("Failed to read event: %s", tr_strerror (errno));
+          break;
+        }
+
+      if (nread != header_size)
+        {
+          log_error ("Failed to read event: expected %zu, got %zu bytes.",
+                     header_size, nread);
+          break;
+        }
+
+      const size_t nleft = ev->NextEntryOffset - nread;
+
+      assert (ev->FileNameLength % sizeof (WCHAR) == 0);
+      assert (ev->FileNameLength > 0);
+      assert (ev->FileNameLength <= nleft);
+
+      if (nleft > name_size)
+        {
+          name_size = nleft;
+          buffer = tr_realloc (buffer, sizeof (FILE_NOTIFY_INFORMATION) + name_size);
+          ev = (PFILE_NOTIFY_INFORMATION) buffer;
+        }
+
+      /* Consume entire name into buffer */
+      if ((nread = bufferevent_read (event, buffer + header_size, nleft)) == (size_t) -1)
+        {
+          log_error ("Failed to read name: %s", tr_strerror (errno));
+          break;
+        }
+
+      if (nread != nleft)
+        {
+          log_error ("Failed to read name: expected %zu, got %zu bytes.", nleft, nread);
+          break;
+        }
+
+      if (ev->Action == FILE_ACTION_ADDED ||
+          ev->Action == FILE_ACTION_MODIFIED ||
+          ev->Action == FILE_ACTION_RENAMED_NEW_NAME)
+        {
+          char * name = tr_win32_native_to_utf8 (ev->FileName,
+                                                 ev->FileNameLength / sizeof (WCHAR));
+          if (name != NULL)
+            {
+              tr_watchdir_process (handle, name);
+              tr_free (name);
+            }
+        }
+    }
+
+  tr_free (buffer);
+}
+
+static void
+tr_watchdir_win32_free (tr_watchdir_backend * backend_base)
+{
+  tr_watchdir_win32 * const backend = BACKEND_UPCAST (backend_base);
+
+  if (backend == NULL)
+    return;
+
+  assert (backend->base.free_func == &tr_watchdir_win32_free);
+
+  if (backend->fd != INVALID_HANDLE_VALUE)
+    CancelIoEx (backend->fd, &backend->overlapped);
+
+  if (backend->thread != NULL)
+    {
+      WaitForSingleObject (backend->thread, INFINITE);
+      CloseHandle (backend->thread);
+    }
+
+  if (backend->event != NULL)
+    bufferevent_free (backend->event);
+
+  if (backend->notify_pipe[0] != TR_BAD_SOCKET)
+    evutil_closesocket (backend->notify_pipe[0]);
+  if (backend->notify_pipe[1] != TR_BAD_SOCKET)
+    evutil_closesocket (backend->notify_pipe[1]);
+
+  if (backend->fd != INVALID_HANDLE_VALUE)
+    CloseHandle (backend->fd);
+
+  tr_free (backend);
+}
+
+tr_watchdir_backend *
+tr_watchdir_win32_new (tr_watchdir_t handle)
+{
+  const char * const path = tr_watchdir_get_path (handle);
+  wchar_t * wide_path;
+  tr_watchdir_win32 * backend;
+
+  backend = tr_new0 (tr_watchdir_win32, 1);
+  backend->base.free_func = &tr_watchdir_win32_free;
+  backend->fd = INVALID_HANDLE_VALUE;
+  backend->notify_pipe[0] = backend->notify_pipe[1] = TR_BAD_SOCKET;
+
+  if ((wide_path = tr_win32_utf8_to_native (path, -1)) == NULL)
+    {
+      log_error ("Failed to convert \"%s\" to native path", path);
+      goto fail;
+    }
+
+  if ((backend->fd = CreateFileW (wide_path, FILE_LIST_DIRECTORY,
+                                  FILE_SHARE_READ | FILE_SHARE_WRITE | FILE_SHARE_DELETE,
+                                  NULL, OPEN_EXISTING,
+                                  FILE_FLAG_BACKUP_SEMANTICS | FILE_FLAG_OVERLAPPED,
+                                  NULL)) == INVALID_HANDLE_VALUE)
+    {
+      log_error ("Failed to open directory \"%s\"", path);
+      goto fail;
+    }
+
+  tr_free (wide_path);
+  wide_path = NULL;
+
+  backend->overlapped.Pointer = handle;
+
+  if (!ReadDirectoryChangesW (backend->fd, backend->buffer, sizeof (backend->buffer), FALSE,
+                              WIN32_WATCH_MASK, NULL, &backend->overlapped, NULL))
+    {
+      log_error ("Failed to read directory changes");
+      goto fail;
+    }
+
+  if (evutil_socketpair (AF_INET, SOCK_STREAM, 0, backend->notify_pipe) == -1)
+    {
+      log_error ("Failed to create notify pipe: %s", tr_strerror (errno));
+      goto fail;
+    }
+
+  if ((backend->event = bufferevent_socket_new (tr_watchdir_get_event_base (handle),
+                                                backend->notify_pipe[0], 0)) == NULL)
+    {
+      log_error ("Failed to create event buffer: %s", tr_strerror (errno));
+      goto fail;
+    }
+
+  bufferevent_setwatermark (backend->event, EV_READ, sizeof (FILE_NOTIFY_INFORMATION), 0);
+  bufferevent_setcb (backend->event, &tr_watchdir_win32_on_event, NULL, NULL, handle);
+  bufferevent_enable (backend->event, EV_READ);
+
+  if ((backend->thread = (HANDLE) _beginthreadex (NULL, 0, &tr_watchdir_win32_thread,
+                                                  handle, 0, NULL)) == NULL)
+    {
+      log_error ("Failed to create thread");
+      goto fail;
+    }
+
+  /* Perform an initial scan on the directory */
+  if (event_base_once (tr_watchdir_get_event_base (handle), -1, EV_TIMEOUT,
+                       &tr_watchdir_win32_on_first_scan, handle, NULL) == -1)
+    log_error ("Failed to perform initial scan: %s", tr_strerror (errno));
+
+  return BACKEND_DOWNCAST (backend);
+
+fail:
+  tr_watchdir_win32_free (BACKEND_DOWNCAST (backend));
+  tr_free (wide_path);
+  return NULL;
+}
diff --git a/libtransmission/watchdir.c b/libtransmission/watchdir.c
new file mode 100644 (file)
index 0000000..b513688
--- /dev/null
@@ -0,0 +1,384 @@
+/*
+ * This file Copyright (C) 2015-2016 Mnemosyne LLC
+ *
+ * It may be used under the GNU GPL versions 2 or 3
+ * or any future license endorsed by Mnemosyne LLC.
+ *
+ * $Id$
+ */
+
+#include <assert.h>
+#include <string.h> /* strcmp () */
+
+#include <event2/event.h>
+#include <event2/util.h>
+
+#define __LIBTRANSMISSION_WATCHDIR_MODULE__
+
+#include "transmission.h"
+#include "error.h"
+#include "error-types.h"
+#include "file.h"
+#include "log.h"
+#include "ptrarray.h"
+#include "utils.h"
+#include "watchdir.h"
+#include "watchdir-common.h"
+
+/***
+****
+***/
+
+#define log_debug(...) (!tr_logLevelIsActive (TR_LOG_DEBUG) ? (void) 0 : \
+  tr_logAddMessage (__FILE__, __LINE__, TR_LOG_DEBUG, "watchdir", __VA_ARGS__))
+
+#define log_error(...) (!tr_logLevelIsActive (TR_LOG_ERROR) ? (void) 0 : \
+  tr_logAddMessage (__FILE__, __LINE__, TR_LOG_ERROR, "watchdir", __VA_ARGS__))
+
+/***
+****
+***/
+
+struct tr_watchdir
+{
+  char                * path;
+  tr_watchdir_cb        callback;
+  void                * callback_user_data;
+  struct event_base   * event_base;
+  tr_watchdir_backend * backend;
+  tr_ptrArray           active_retries;
+};
+
+/***
+****
+***/
+
+static bool
+is_regular_file (const char * dir,
+                 const char * name)
+{
+  char * const path = tr_buildPath (dir, name, NULL);
+  tr_sys_path_info path_info;
+  tr_error * error = NULL;
+  bool ret;
+
+  if ((ret = tr_sys_path_get_info (path, 0, &path_info, &error)))
+    {
+      ret = path_info.type == TR_SYS_PATH_IS_FILE;
+    }
+  else
+    {
+      if (!TR_ERROR_IS_ENOENT (error->code))
+        log_error ("Failed to get type of \"%s\" (%d): %s", path, error->code,
+                   error->message);
+      tr_error_free (error);
+    }
+
+  tr_free (path);
+  return ret;
+}
+
+static const char *
+watchdir_status_to_string (tr_watchdir_status status)
+{
+  switch (status)
+    {
+      case TR_WATCHDIR_ACCEPT:
+        return "accept";
+      case TR_WATCHDIR_IGNORE:
+        return "ignore";
+      case TR_WATCHDIR_RETRY:
+        return "retry";
+      default:
+        return "???";
+    }
+}
+
+static tr_watchdir_status
+tr_watchdir_process_impl (tr_watchdir_t   handle,
+                          const char    * name)
+{
+  /* File may be gone while we're retrying */
+  if (!is_regular_file (tr_watchdir_get_path (handle), name))
+    return TR_WATCHDIR_IGNORE;
+
+  const tr_watchdir_status ret = handle->callback (handle, name, handle->callback_user_data);
+
+  assert (ret == TR_WATCHDIR_ACCEPT ||
+          ret == TR_WATCHDIR_IGNORE ||
+          ret == TR_WATCHDIR_RETRY);
+
+  log_debug ("Callback decided to %s file \"%s\"", watchdir_status_to_string (ret), name);
+
+  return ret;
+}
+
+/***
+****
+***/
+
+typedef struct tr_watchdir_retry
+{
+  tr_watchdir_t    handle;
+  char           * name;
+  unsigned int     counter;
+  struct event   * timer;
+  struct timeval   interval;
+}
+tr_watchdir_retry;
+
+/* Non-static and mutable for unit tests */
+unsigned int   tr_watchdir_retry_limit          = 3;
+struct timeval tr_watchdir_retry_start_interval = { 1, 0 };
+struct timeval tr_watchdir_retry_max_interval   = { 10, 0 };
+
+#define tr_watchdir_retries_init(r)      (void) 0
+#define tr_watchdir_retries_destroy(r)   tr_ptrArrayDestruct ((r), (PtrArrayForeachFunc) &tr_watchdir_retry_free)
+#define tr_watchdir_retries_insert(r, v) tr_ptrArrayInsertSorted ((r), (v), &compare_retry_names)
+#define tr_watchdir_retries_remove(r, v) tr_ptrArrayRemoveSortedPointer ((r), (v), &compare_retry_names)
+#define tr_watchdir_retries_find(r, v)   tr_ptrArrayFindSorted ((r), (v), &compare_retry_names)
+
+static int
+compare_retry_names (const void * a,
+                     const void * b)
+{
+  return strcmp (((tr_watchdir_retry *) a)->name, ((tr_watchdir_retry *) b)->name);
+}
+
+static void
+tr_watchdir_retry_free (tr_watchdir_retry * retry);
+
+static void
+tr_watchdir_on_retry_timer (evutil_socket_t   fd UNUSED,
+                            short             type UNUSED,
+                            void            * context)
+{
+  assert (context != NULL);
+
+  tr_watchdir_retry * const retry = context;
+  const tr_watchdir_t handle = retry->handle;
+
+  if (tr_watchdir_process_impl (handle, retry->name) == TR_WATCHDIR_RETRY)
+    {
+      if (++retry->counter < tr_watchdir_retry_limit)
+        {
+          evutil_timeradd (&retry->interval, &retry->interval, &retry->interval);
+          if (evutil_timercmp (&retry->interval, &tr_watchdir_retry_max_interval, >))
+            retry->interval = tr_watchdir_retry_max_interval;
+
+          evtimer_del (retry->timer);
+          evtimer_add (retry->timer, &retry->interval);
+          return;
+        }
+
+      log_error ("Failed to add (corrupted?) torrent file: %s", retry->name);
+    }
+
+  tr_watchdir_retries_remove (&handle->active_retries, retry);
+  tr_watchdir_retry_free (retry);
+}
+
+static tr_watchdir_retry *
+tr_watchdir_retry_new (tr_watchdir_t   handle,
+                       const char    * name)
+{
+  tr_watchdir_retry * retry;
+
+  retry = tr_new0 (tr_watchdir_retry, 1);
+  retry->handle = handle;
+  retry->name = tr_strdup (name);
+  retry->timer = evtimer_new (handle->event_base, &tr_watchdir_on_retry_timer, retry);
+  retry->interval = tr_watchdir_retry_start_interval;
+
+  evtimer_add (retry->timer, &retry->interval);
+
+  return retry;
+}
+
+static void
+tr_watchdir_retry_free (tr_watchdir_retry * retry)
+{
+  if (retry == NULL)
+    return;
+
+  if (retry->timer != NULL)
+    {
+      evtimer_del (retry->timer);
+      event_free (retry->timer);
+    }
+
+  tr_free (retry->name);
+  tr_free (retry);
+}
+
+static void
+tr_watchdir_retry_restart (tr_watchdir_retry * retry)
+{
+  assert (retry != NULL);
+
+  evtimer_del (retry->timer);
+
+  retry->counter = 0;
+  retry->interval = tr_watchdir_retry_start_interval;
+
+  evtimer_add (retry->timer, &retry->interval);
+}
+
+/***
+****
+***/
+
+tr_watchdir_t
+tr_watchdir_new (const char        * path,
+                 tr_watchdir_cb      callback,
+                 void              * callback_user_data,
+                 struct event_base * event_base)
+{
+  tr_watchdir_t handle;
+
+  handle = tr_new0 (struct tr_watchdir, 1);
+  handle->path = tr_strdup (path);
+  handle->callback = callback;
+  handle->callback_user_data = callback_user_data;
+  handle->event_base = event_base;
+  tr_watchdir_retries_init (&handle->active_retries);
+
+#ifdef WITH_INOTIFY
+  if (handle->backend == NULL)
+    handle->backend = tr_watchdir_inotify_new (handle);
+#endif
+#ifdef WITH_KQUEUE
+  if (handle->backend == NULL)
+    handle->backend = tr_watchdir_kqueue_new (handle);
+#endif
+#ifdef _WIN32
+  if (handle->backend == NULL)
+    handle->backend = tr_watchdir_win32_new (handle);
+#endif
+
+  if (handle->backend == NULL)
+    handle->backend = tr_watchdir_generic_new (handle);
+
+  if (handle->backend == NULL)
+    {
+      tr_watchdir_free (handle);
+      handle = NULL;
+    }
+  else
+    {
+      assert (handle->backend->free_func != NULL);
+    }
+
+  return handle;
+}
+
+void
+tr_watchdir_free (tr_watchdir_t handle)
+{
+  if (handle == NULL)
+    return;
+
+  tr_watchdir_retries_destroy (&handle->active_retries);
+
+  if (handle->backend != NULL)
+    handle->backend->free_func (handle->backend);
+
+  tr_free (handle->path);
+  tr_free (handle);
+}
+
+const char *
+tr_watchdir_get_path (tr_watchdir_t handle)
+{
+  assert (handle != NULL);
+  return handle->path;
+}
+
+tr_watchdir_backend *
+tr_watchdir_get_backend (tr_watchdir_t handle)
+{
+  assert (handle != NULL);
+  return handle->backend;
+}
+
+struct event_base *
+tr_watchdir_get_event_base (tr_watchdir_t handle)
+{
+  assert (handle != NULL);
+  return handle->event_base;
+}
+
+/***
+****
+***/
+
+void
+tr_watchdir_process (tr_watchdir_t   handle,
+                     const char    * name)
+{
+  const tr_watchdir_retry search_key = { .name = (char *) name };
+  tr_watchdir_retry * existing_retry;
+
+  assert (handle != NULL);
+
+  if ((existing_retry = tr_watchdir_retries_find (&handle->active_retries, &search_key)) != NULL)
+    {
+      tr_watchdir_retry_restart (existing_retry);
+      return;
+    }
+
+  if (tr_watchdir_process_impl (handle, name) == TR_WATCHDIR_RETRY)
+    {
+      tr_watchdir_retry * retry = tr_watchdir_retry_new (handle, name);
+      tr_watchdir_retries_insert (&handle->active_retries, retry);
+    }
+}
+
+void
+tr_watchdir_scan (tr_watchdir_t   handle,
+                  tr_ptrArray   * dir_entries)
+{
+  tr_sys_dir_t dir;
+  const char * name;
+  tr_ptrArray new_dir_entries = TR_PTR_ARRAY_INIT_STATIC;
+  const PtrArrayCompareFunc name_compare_func = (PtrArrayCompareFunc) &strcmp;
+  tr_error * error = NULL;
+
+  if ((dir = tr_sys_dir_open (handle->path, &error)) == TR_BAD_SYS_DIR)
+    {
+      log_error ("Failed to open directory \"%s\" (%d): %s", handle->path,
+                 error->code, error->message);
+      tr_error_free (error);
+      return;
+    }
+
+  while ((name = tr_sys_dir_read_name (dir, &error)) != NULL)
+    {
+      if (strcmp (name, ".") == 0 || strcmp (name, "..") == 0)
+        continue;
+
+      if (dir_entries != NULL)
+        {
+          tr_ptrArrayInsertSorted (&new_dir_entries, tr_strdup (name), name_compare_func);
+          if (tr_ptrArrayFindSorted (dir_entries, name, name_compare_func) != NULL)
+            continue;
+        }
+
+      tr_watchdir_process (handle, name);
+    }
+
+  if (error != NULL)
+    {
+      log_error ("Failed to read directory \"%s\" (%d): %s", handle->path,
+                 error->code, error->message);
+      tr_error_free (error);
+    }
+
+  tr_sys_dir_close (dir, NULL);
+
+  if (dir_entries != NULL)
+    {
+      tr_ptrArrayDestruct (dir_entries, &tr_free);
+      *dir_entries = new_dir_entries;
+    }
+}
diff --git a/libtransmission/watchdir.h b/libtransmission/watchdir.h
new file mode 100644 (file)
index 0000000..766abf3
--- /dev/null
@@ -0,0 +1,48 @@
+/*
+ * This file Copyright (C) 2015-2016 Mnemosyne LLC
+ *
+ * It may be used under the GNU GPL versions 2 or 3
+ * or any future license endorsed by Mnemosyne LLC.
+ *
+ * $Id$
+ */
+
+#ifndef TR_WATCHDIR_H
+#define TR_WATCHDIR_H
+
+#ifdef __cplusplus
+extern "C" {
+#endif
+
+struct event_base;
+
+typedef struct tr_watchdir * tr_watchdir_t;
+
+typedef enum
+{
+  TR_WATCHDIR_ACCEPT,
+  TR_WATCHDIR_IGNORE,
+  TR_WATCHDIR_RETRY
+}
+tr_watchdir_status;
+
+typedef tr_watchdir_status (* tr_watchdir_cb) (tr_watchdir_t   handle,
+                                               const char    * name,
+                                               void          * user_data);
+
+/* ... */
+
+tr_watchdir_t   tr_watchdir_new      (const char        * path,
+                                      tr_watchdir_cb      callback,
+                                      void              * callback_user_data,
+                                      struct event_base * event_base);
+
+void            tr_watchdir_free     (tr_watchdir_t       handle);
+
+const char    * tr_watchdir_get_path (tr_watchdir_t       handle);
+
+#ifdef __cplusplus
+}
+#endif
+
+#endif /* TR_WATCHDIR_H */