From ea483602122824a37e172caa21dd0b54b2f3f765 Mon Sep 17 00:00:00 2001 From: Mike Gelfand Date: Sat, 2 Jan 2016 14:28:59 +0000 Subject: [PATCH] #5663: Rework directory watching in daemon 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. --- Transmission.xcodeproj/project.pbxproj | 26 +- configure.ac | 47 ++- daemon/CMakeLists.txt | 10 - daemon/Makefile.am | 6 +- daemon/daemon.c | 47 +-- daemon/watch.c | 267 ----------------- daemon/watch.h | 23 -- libtransmission/CMakeLists.txt | 24 +- libtransmission/Makefile.am | 21 +- libtransmission/watchdir-common.h | 55 ++++ libtransmission/watchdir-generic.c | 114 +++++++ libtransmission/watchdir-inotify.c | 203 +++++++++++++ libtransmission/watchdir-kqueue.c | 170 +++++++++++ libtransmission/watchdir-test.c | 395 +++++++++++++++++++++++++ libtransmission/watchdir-win32.c | 286 ++++++++++++++++++ libtransmission/watchdir.c | 384 ++++++++++++++++++++++++ libtransmission/watchdir.h | 48 +++ 17 files changed, 1770 insertions(+), 356 deletions(-) delete mode 100644 daemon/watch.c delete mode 100644 daemon/watch.h create mode 100644 libtransmission/watchdir-common.h create mode 100644 libtransmission/watchdir-generic.c create mode 100644 libtransmission/watchdir-inotify.c create mode 100644 libtransmission/watchdir-kqueue.c create mode 100644 libtransmission/watchdir-test.c create mode 100644 libtransmission/watchdir-win32.c create mode 100644 libtransmission/watchdir.c create mode 100644 libtransmission/watchdir.h diff --git a/Transmission.xcodeproj/project.pbxproj b/Transmission.xcodeproj/project.pbxproj index 52e19395c..6839b131c 100644 --- a/Transmission.xcodeproj/project.pbxproj +++ b/Transmission.xcodeproj/project.pbxproj @@ -293,7 +293,6 @@ 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 */; }; @@ -473,6 +472,11 @@ 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 */; }; @@ -1034,8 +1038,6 @@ A2D307A20D9EC6870051FD27 /* BlocklistDownloader.h */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.c.h; name = BlocklistDownloader.h; path = macosx/BlocklistDownloader.h; sourceTree = ""; }; A2D307A30D9EC6870051FD27 /* BlocklistDownloader.m */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.c.objc; name = BlocklistDownloader.m; path = macosx/BlocklistDownloader.m; sourceTree = ""; }; A2D307B00D9EC9F50051FD27 /* BlocklistStatusWindow.xib */ = {isa = PBXFileReference; lastKnownFileType = file.xib; name = BlocklistStatusWindow.xib; path = macosx/BlocklistStatusWindow.xib; sourceTree = ""; }; - A2D597280F5AE49E0001AB3C /* watch.c */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.c.c; name = watch.c; path = daemon/watch.c; sourceTree = ""; }; - A2D597290F5AE49E0001AB3C /* watch.h */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.c.h; name = watch.h; path = daemon/watch.h; sourceTree = ""; }; A2D7744F154CC25700A62B93 /* WebSeedTableView.h */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.c.h; name = WebSeedTableView.h; path = macosx/WebSeedTableView.h; sourceTree = ""; }; A2D77450154CC25700A62B93 /* WebSeedTableView.m */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.c.objc; name = WebSeedTableView.m; path = macosx/WebSeedTableView.m; sourceTree = ""; }; A2D8CFBF15FA177A0056E93D /* ru */ = {isa = PBXFileReference; lastKnownFileType = text.plist.strings; name = ru; path = macosx/QuickLookPlugin/ru.lproj/Localizable.strings; sourceTree = SOURCE_ROOT; }; @@ -1230,6 +1232,11 @@ C1639A7B1A55F57200E42033 /* cencode.h */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.c.h; name = cencode.h; path = "third-party/libb64/b64/cencode.h"; sourceTree = ""; }; C1F690FC1AD0627500D95CF0 /* daemon-posix.c */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.c.c; name = "daemon-posix.c"; path = "daemon/daemon-posix.c"; sourceTree = ""; }; C1F690FE1AD0628400D95CF0 /* daemon.h */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.c.h; name = daemon.h; path = daemon/daemon.h; sourceTree = ""; }; + C1FEE5721C3223CC00D62832 /* watchdir-common.h */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.c.h; name = "watchdir-common.h"; path = "libtransmission/watchdir-common.h"; sourceTree = ""; }; + C1FEE5731C3223CC00D62832 /* watchdir-generic.c */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.c.c; name = "watchdir-generic.c"; path = "libtransmission/watchdir-generic.c"; sourceTree = ""; }; + C1FEE5741C3223CC00D62832 /* watchdir-kqueue.c */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.c.c; name = "watchdir-kqueue.c"; path = "libtransmission/watchdir-kqueue.c"; sourceTree = ""; }; + C1FEE5751C3223CC00D62832 /* watchdir.c */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.c.c; name = watchdir.c; path = libtransmission/watchdir.c; sourceTree = ""; }; + C1FEE5761C3223CC00D62832 /* watchdir.h */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.c.h; name = watchdir.h; path = libtransmission/watchdir.h; sourceTree = ""; }; D4AF3B2D0C41F7A500D46B6B /* list.c */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.c.c; name = list.c; path = libtransmission/list.c; sourceTree = ""; }; D4AF3B2E0C41F7A500D46B6B /* list.h */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.c.h; name = list.h; path = libtransmission/list.h; sourceTree = ""; }; E138A9750C04D88F00C5426C /* ProgressGradients.h */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.c.h; name = ProgressGradients.h; path = macosx/ProgressGradients.h; sourceTree = ""; }; @@ -1810,6 +1817,11 @@ 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 = ""; @@ -2012,8 +2024,6 @@ C1F690FE1AD0628400D95CF0 /* daemon.h */, C1F690FC1AD0627500D95CF0 /* daemon-posix.c */, BEFC1C140C07756200B0BB3C /* remote.c */, - A2D597280F5AE49E0001AB3C /* watch.c */, - A2D597290F5AE49E0001AB3C /* watch.h */, ); name = daemon; sourceTree = ""; @@ -2122,6 +2132,7 @@ 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 */, @@ -2140,6 +2151,7 @@ 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 */, @@ -2753,6 +2765,7 @@ 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 */, @@ -2786,10 +2799,12 @@ 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 */, @@ -2966,7 +2981,6 @@ buildActionMask = 2147483647; files = ( BEFC1C1A0C07756200B0BB3C /* daemon.c in Sources */, - A2D5972A0F5AE49E0001AB3C /* watch.c in Sources */, C1F690FD1AD0627500D95CF0 /* daemon-posix.c in Sources */, ); runOnlyForDeploymentPostprocessing = 0; diff --git a/configure.ac b/configure.ac index 45580dbcd..1e5bc02de 100644 --- a/configure.ac +++ b/configure.ac @@ -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]) diff --git a/daemon/CMakeLists.txt b/daemon/CMakeLists.txt index 56b0d1fcc..e8f8b3483 100644 --- a/daemon/CMakeLists.txt +++ b/daemon/CMakeLists.txt @@ -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 diff --git a/daemon/Makefile.am b/daemon/Makefile.am index fc0b7b199..677f765c5 100644 --- a/daemon/Makefile.am +++ b/daemon/Makefile.am @@ -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 diff --git a/daemon/daemon.c b/daemon/daemon.c index 149668b8c..cefd7ab62 100644 --- a/daemon/daemon.c +++ b/daemon/daemon.c @@ -21,7 +21,6 @@ #include /* getpid */ #endif -#include #include #include @@ -32,6 +31,7 @@ #include #include #include +#include #ifdef USE_SYSTEMD_DAEMON #include @@ -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 index 4fee35c21..000000000 --- a/daemon/watch.c +++ /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 - #include - #include /* close */ -#else - #include /* evbuffer */ -#endif - -#include -#include /* strlen () */ -#include /* perror () */ - -#include -#include -#include -#include /* 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 index dc108c013..000000000 --- a/daemon/watch.h +++ /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 diff --git a/libtransmission/CMakeLists.txt b/libtransmission/CMakeLists.txt index d8ef7f37d..7589223e6 100644 --- a/libtransmission/CMakeLists.txt +++ b/libtransmission/CMakeLists.txt @@ -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) diff --git a/libtransmission/Makefile.am b/libtransmission/Makefile.am index a77fafbe1..275b76734 100644 --- a/libtransmission/Makefile.am +++ b/libtransmission/Makefile.am @@ -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 index 000000000..56695b944 --- /dev/null +++ b/libtransmission/watchdir-common.h @@ -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 index 000000000..275bdd08a --- /dev/null +++ b/libtransmission/watchdir-generic.c @@ -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 +#include + +#include + +#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 index 000000000..a7a251549 --- /dev/null +++ b/libtransmission/watchdir-inotify.c @@ -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 +#include +#include /* NAME_MAX */ +#include /* realloc () */ + +#include /* close () */ + +#include + +#include +#include + +#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 index 000000000..bf4f418e1 --- /dev/null +++ b/libtransmission/watchdir-kqueue.c @@ -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 +#include +#include /* strcmp () */ + +#include /* open () */ +#include /* close () */ + +#include +#include + +#ifndef O_EVTONLY + #define O_EVTONLY O_RDONLY +#endif + +#include + +#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 index 000000000..d77b7bf73 --- /dev/null +++ b/libtransmission/watchdir-test.c @@ -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 + +#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 index 000000000..cdaa52c5b --- /dev/null +++ b/libtransmission/watchdir-win32.c @@ -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 +#include +#include /* offsetof */ +#include /* realloc () */ + +#include /* _beginthreadex () */ + +#include + +#include +#include +#include + +#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 index 000000000..b51368892 --- /dev/null +++ b/libtransmission/watchdir.c @@ -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 +#include /* strcmp () */ + +#include +#include + +#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 index 000000000..766abf3ef --- /dev/null +++ b/libtransmission/watchdir.h @@ -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 */ -- 2.40.0