diff -Nru lsyncd-2.1.6/ChangeLog lsyncd-2.2.3/ChangeLog --- lsyncd-2.1.6/ChangeLog 2015-10-15 06:29:47.000000000 +0000 +++ lsyncd-2.2.3/ChangeLog 2018-03-09 12:39:11.000000000 +0000 @@ -1,4 +1,73 @@ -15-10-2015: 2.1.6 +2018-03-09: 2.2.3 + enhaencement: supporting includes with new filter and filterFrom options + change: needing now at least Lua 5.2 (Lua 5.1 no longer supported, Lua5.3 supported) + change: if the target/targetdir ends with a ':' do not append + a trailing '/' to it, since that would change it from homedir to rootdir! + add: example for Amazon S3 Bucket (Daniel Miranda) + fix: setting stdout/stderr to linebuffer mode. + fix: Lua5.3 compatiblity, using load() instead of loadstring() + fix: cmake lua detection, will resort to "lua" and "luac" binaries only if + more specific suffixes (e.g. luac5.3) are not available + fix: test suit, Lua5.3 compatibility (table.unpack) + +2017-02-16: 2.2.2 + fix: checkgauge 'insist' + fix: no partial path exlusion tests + fix: write pid of forked process in pidfile + fix: crash on not reachable target + workaround: + changed back to filter style rsync calling + until https://bugzilla.samba.org/show_bug.cgi?id=12569 + is fixed and released. + +2017-01-05: 2.2.1 + enhancement: now always using filter lists with rysnc + instead of include/exclude lists taking advantage of the new --delete-missing-args + parameter to delete files on target. + >>> Thus Lsyncd 2.2.1 needs rsync >= 3.1.0 + change: added "tests" make target to run all the tests. + fix: crash due to typo in changed ^path, ^pathdir, ^pathname + +2017-01-04: 2.2.0 + enhancement: add rsync options: + "append", + "append_verify", + "backup", + "backup_dir", + "chmod", + "chown", + "copy_dirlinks", + "existing", + "groupmap", + "omit_dir_times", + "omit_link_times", + "suffix," + "usermap", + enhancement: settings{ } now checks for unknown entries and errors if so. + change: Level3 scripts ^path,^pathdir and ^pathname now don't start with a slash. + change: Lsyncd now writes a startup log message before daemonizing + does in case logging fails, it is recognized before it cannot + message anything about it, since it deamonized + change: compatible with Lua5.3 (along with 5.1 and 5.2) + change: _verbatim forced for 'exitcodes' entry. + change: manpage is not rebuild by default. + it is provided precompiled. + change: faulty/deprecated config files that use settings = { ... }, with equal sign + are no longer worked around. + change: default.direct now calls copy with -p + fix: potential race conditions: + default.rsyncssh will now channel deletes also through rsync and treats moves + as blocking events. + fix: ']' is not escaped for rsync rules, since rsync only applies + doesn't applie pattern matching if no other pattern chars + are found. + fix: Shell injection hole close for default.direct on mv commands. (Marcin Szewczyk) + fix: Crash of default-direct when source doesn't exit (Michael Ploujnikov) + fix: fixed faulty event replacement, + a race condition noticed by extensive default.rsyncssh testing + changed Delays were not reflected in Events + +2015-10-15: 2.1.6 enhancement: Lsyncd now locks its pidfile enhancement: added ssh.identifyFile and ssh.options options enhancement: added rsync inplace option @@ -12,7 +81,7 @@ change: now removes its pidfile on INT and TERM signals change: changed build system from autotools to cmake -07-06-2013: 2.1.5 +2013-06-07: 2.1.5 enhancement: Added rsync options: bwlimit, timeout fix: Specifying ssh port no longer overwrites the last rsync option fix: rsync option password_file is now accepted @@ -25,22 +94,22 @@ change: a2x is no longer checked by configure script. should not be needed when building from tarball -24-11-2012: 2.1.4 +2012-11-24: 2.1.4 fix: making ssh custom port changes work with ssh and rsync -23-11-2012: 2.1.3 +2012-11-23: 2.1.3 fix: fixed 2 crash conditions due to failure to read 'uSettings' -03-11-2012: 2.1.2 +2012-11-03: 2.1.2 fix: added excludeFrom to checkgauge (thx to DavidWittman) fix: fixed rsync option computation enhancement: added password_file file option to rsync -27-10-2012: 2.1.1 +2012-10-27: 2.1.1 fix: fix rsync.rsh, rsync.rsync_path, rsync.tmp_dir, rsync._extra parameters thanks go to Birger Schmidt for this fix. -23-10-2012: 2.1.0 +2012-10-23: 2.1.0 fix: fail startup if settings.inist is false and one of the target hosts fails fix: in case of waiting for processes during restart only logs this state now once a minute rather than filling the log crazy about it @@ -55,7 +124,7 @@ change: Lsyncd now exits with exitcode 143 on TERM signal change: settings is now be used as call like settings{...} instead of settings = {...} -04-04-2012: 2.0.7 +2012-04-04: 2.0.7 fix: closed a memory leak due to not correct configured weak tables fix: default.direct, do not use on OSX unrecognized option -t on modify fix: default.direct, typo leading to compile error @@ -65,7 +134,7 @@ change: removed --with-default-runner since it was broken, and will be replaced by something more generic in future -16-02-2012: 2.0.6 +2012-02-16: 2.0.6 fix: no longer stops syslogging on HUP signals fix: OSX event watcher no longer misses moves into and out of the watch tree fix: not refinding a relative path to the config file in case of HUP. @@ -96,7 +165,7 @@ default.rsyncssh: does not add --delete to rsync, and does not use rm via ssh tunnel default.direct: does not add --delete to startup rsync and does not use rm -25-08-2011: 2.0.5 +2011-08-25: 2.0.5 fix: Lsyncd will now terminate if it inotify watching exceeds its preset limit. fix: rsync error exit code 12 now results in retries. @@ -125,14 +194,14 @@ enhancement: readdir(path) is available to userscripts, reads the contents of a directory. -27-03-2011: 2.0.4 +2011-03-27: 2.0.4 enhancement: new setting options logident, logfacility fix: moving filenames with spaces through ssh fix: excludes containing chars % $ ( ) . [ ] + - fix: various typos change: api, settings.statusInterval instead of settings.statusIntervall -25-02-2011: 2.0.3 +2011-02-25: 2.0.3 enhancement: new default target --direct using /bin/ binaries to keep to local dirs in sync (and by default not preserving ownership) @@ -147,7 +216,7 @@ change: leave lua apichecking enabled by default. -20-01-2011: 2.0.2 +2011-01-20: 2.0.2 fix: exclude rules not terminated with '/' now match a file or dir named exactly the same not starting with. fix: pass exclude rules to the startup sync @@ -155,7 +224,7 @@ partial path than on syncs fix: properly close pipes that needed more than one write. -11-01-2011: 2.0.1 +2011-01-11: 2.0.1 fix: write pidfile after daemonize() fix: fixed weak tables that allowed garbage collector to collect event lists too eraly. @@ -163,7 +232,7 @@ change: added OSX fsevents interface, disabled in autoconf by default since still very experimental and limited to OSX 10.5 only. -02-12-2010: 2.0.0 +2010-12-02: 2.0.0 a complete recoding! change: format of command line arguments changed completly. @@ -185,7 +254,7 @@ change: manpage is now written in asciidoc change: most more complex logic of Lsyncd is now written in Lua. -04-10-2010: 1.39 +2010-10-04: 1.39 enhancement: call action for multiple targets simultanously fix: correctly accept from config xml fix: correctly close and free the inotify file descriptor in case of restart @@ -193,7 +262,7 @@ fix: when delay=0 a bug always called rsync file filter even when in directory mode -01-09-2010: 1.38 +2010-09-01: 1.38 enhancement: implemented file filters for singular operations enhancement: added --singular parameter for single file calls fix: fixed --dryrun messages @@ -201,7 +270,7 @@ being kill -HUPed internal: printout the actual binary called when --debug specified -05-08-2010: 1.37 +2010-08-05: 1.37 enhancement: react on HUP signals (interpreted as complete restart) enhancement: inotifies are configureable enhancement: --no-startup skips the startup calls @@ -212,22 +281,22 @@ internal: removed the need of the "tosync" stack internal: use more pointers instead of indexes -11-07-2010: 1.34 +2010-07-11: 1.34 fix: logging segfault on 64bit systems changed: man page location, spellings -05-06-2010: 1.33 +2010-06-05: 1.33 fix: exlude file argument passing to rsync fix: allow exlude files specified for individual sources fix/enhancement: exlusions will be compared with extended path files allowing sub dirs to be excluded. enhancement: allow delays and call aggregation -05-01-2009: Release of lsyncd 1.26 +2009-01-05: Release of lsyncd 1.26 fix: segfault on multitargets changed meaning of "version" tag in lsyncd.conf.xml -14-12-2008: Release of lsyncd 1.25 +2008-12-14: Release of lsyncd 1.25 fix: mv dir and cp -r working fix: working with reiserfs enhancement: config files @@ -238,5 +307,5 @@ lots of smaller stuff here and there ... Thanks to all contributers! -05-12-2007: Release of lsyncd 1.0 +2007-12-05: Release of lsyncd 1.0 diff -Nru lsyncd-2.1.6/cmake/FindLua.cmake lsyncd-2.2.3/cmake/FindLua.cmake --- lsyncd-2.1.6/cmake/FindLua.cmake 2015-10-15 06:29:47.000000000 +0000 +++ lsyncd-2.2.3/cmake/FindLua.cmake 2018-03-09 12:39:11.000000000 +0000 @@ -1,7 +1,7 @@ # Locate Lua library # This module defines # LUA_EXECUTABLE, if found -# LUA_FOUND, if false, do not try to link to Lua +# LUA_FOUND, if false, do not try to link to Lua # LUA_LIBRARIES # LUA_INCLUDE_DIR, where to find lua.h # LUA_VERSION_STRING, the version of Lua found (since CMake 2.8.8) @@ -27,26 +27,22 @@ # (To distribute this file outside of CMake, substitute the full # License text for the above reference.) # -# The required version of Lua can be specified using the -# standard syntax, e.g. FIND_PACKAGE(Lua 5.1) -# Otherwise the module will search for any available Lua implementation +# This module will try to find the newest Lua version down to 5.2 # Always search for non-versioned lua first (recommended) SET(_POSSIBLE_LUA_INCLUDE include include/lua) -SET(_POSSIBLE_LUA_EXECUTABLE lua) -SET(_POSSIBLE_LUA_LIBRARY lua) +#SET(_POSSIBLE_LUA_EXECUTABLE lua) +#SET(_POSSIBLE_LUA_COMPILER luac) +#SET(_POSSIBLE_LUA_LIBRARY lua) # Determine possible naming suffixes (there is no standard for this) -IF(Lua_FIND_VERSION_MAJOR AND Lua_FIND_VERSION_MINOR) - SET(_POSSIBLE_SUFFIXES "${Lua_FIND_VERSION_MAJOR}${Lua_FIND_VERSION_MINOR}" "${Lua_FIND_VERSION_MAJOR}.${Lua_FIND_VERSION_MINOR}" "-${Lua_FIND_VERSION_MAJOR}.${Lua_FIND_VERSION_MINOR}") -ELSE(Lua_FIND_VERSION_MAJOR AND Lua_FIND_VERSION_MINOR) - SET(_POSSIBLE_SUFFIXES "52" "5.2" "-5.2" "51" "5.1" "-5.1") -ENDIF(Lua_FIND_VERSION_MAJOR AND Lua_FIND_VERSION_MINOR) +SET(_POSSIBLE_SUFFIXES "52" "5.2" "-5.2" "53" "5.3" "-5.3" "") # Set up possible search names and locations FOREACH(_SUFFIX ${_POSSIBLE_SUFFIXES}) LIST(APPEND _POSSIBLE_LUA_INCLUDE "include/lua${_SUFFIX}") LIST(APPEND _POSSIBLE_LUA_EXECUTABLE "lua${_SUFFIX}") + LIST(APPEND _POSSIBLE_LUA_COMPILER "luac${_SUFFIX}") LIST(APPEND _POSSIBLE_LUA_LIBRARY "lua${_SUFFIX}") ENDFOREACH(_SUFFIX) @@ -55,6 +51,11 @@ NAMES ${_POSSIBLE_LUA_EXECUTABLE} ) +# Find the lua executable +FIND_PROGRAM(LUA_COMPILER + NAMES luac5.3 ${_POSSIBLE_LUA_COMPILER} +) + # Find the lua header FIND_PATH(LUA_INCLUDE_DIR lua.h HINTS @@ -72,7 +73,7 @@ ) # Find the lua library -FIND_LIBRARY(LUA_LIBRARY +FIND_LIBRARY(LUA_LIBRARY NAMES ${_POSSIBLE_LUA_LIBRARY} HINTS $ENV{LUA_DIR} @@ -108,11 +109,11 @@ ENDIF() INCLUDE(FindPackageHandleStandardArgs) -# handle the QUIETLY and REQUIRED arguments and set LUA_FOUND to TRUE if +# handle the QUIETLY and REQUIRED arguments and set LUA_FOUND to TRUE if # all listed variables are TRUE FIND_PACKAGE_HANDLE_STANDARD_ARGS(Lua REQUIRED_VARS LUA_LIBRARIES LUA_INCLUDE_DIR VERSION_VAR LUA_VERSION_STRING) -MARK_AS_ADVANCED(LUA_INCLUDE_DIR LUA_LIBRARIES LUA_LIBRARY LUA_MATH_LIBRARY LUA_EXECUTABLE) +MARK_AS_ADVANCED(LUA_INCLUDE_DIR LUA_LIBRARIES LUA_LIBRARY LUA_MATH_LIBRARY LUA_EXECUTABLE LUA_COMPILER) diff -Nru lsyncd-2.1.6/CMakeLists.txt lsyncd-2.2.3/CMakeLists.txt --- lsyncd-2.1.6/CMakeLists.txt 2015-10-15 06:29:47.000000000 +0000 +++ lsyncd-2.2.3/CMakeLists.txt 2018-03-09 12:39:11.000000000 +0000 @@ -1,12 +1,12 @@ # preamble project( Lsyncd ) cmake_minimum_required( VERSION 2.8 ) -set( LSYNCD_VERSION 2.1.6 ) +set( LSYNCD_VERSION 2.2.3 ) set( CMAKE_MODULE_PATH ${CMAKE_MODULE_PATH} "${CMAKE_SOURCE_DIR}/cmake/" ) # finding Lua -find_package(Lua REQUIRED) +find_package( Lua REQUIRED ) include_directories ( ${LUA_INCLUDE_DIR} ) @@ -45,26 +45,26 @@ # building and compiling the part of lsyncd written in Lua # also called "runner" -add_custom_command( OUTPUT runner.c - COMMAND ${CMAKE_COMMAND} -E echo "Generating built-in runner linkable" +add_custom_command( OUTPUT runner.c + COMMAND ${CMAKE_COMMAND} -E echo "Generating built-in runner linkable" COMMAND ${LUA_EXECUTABLE} ${PROJECT_SOURCE_DIR}/bin2carray.lua runner.out runner runner.c - DEPENDS runner.out -) + DEPENDS runner.out +) # this supposes the Lua compiler 'luac' is sitting right next to the Lua interpreter 'lua' add_custom_command( OUTPUT runner.out - COMMAND ${CMAKE_COMMAND} -E echo "Compiling built-in runner" - COMMAND ${LUA_EXECUTABLE}c -o runner.out ${PROJECT_SOURCE_DIR}/lsyncd.lua - DEPENDS ${PROJECT_SOURCE_DIR}/lsyncd.lua -) + COMMAND ${CMAKE_COMMAND} -E echo "Compiling built-in runner" + COMMAND ${LUA_COMPILER} -o runner.out ${PROJECT_SOURCE_DIR}/lsyncd.lua + DEPENDS ${PROJECT_SOURCE_DIR}/lsyncd.lua +) # building and compiling the built-in default configs: # rsync rysnc-ssh and direct -add_custom_command( OUTPUT defaults.c - COMMAND ${CMAKE_COMMAND} -E echo "Generating built-in default configs" +add_custom_command( OUTPUT defaults.c + COMMAND ${CMAKE_COMMAND} -E echo "Generating built-in default configs" COMMAND ${LUA_EXECUTABLE} ${PROJECT_SOURCE_DIR}/bin2carray.lua defaults.out defaults defaults.c - DEPENDS defaults.out -) + DEPENDS defaults.out +) set( DEFAULT_CONFIGS ${PROJECT_SOURCE_DIR}/default.lua @@ -74,22 +74,38 @@ ) add_custom_command( OUTPUT defaults.out - COMMAND ${CMAKE_COMMAND} -E echo "Compiling built-in default configs" - COMMAND ${LUA_EXECUTABLE}c -o defaults.out ${DEFAULT_CONFIGS} - DEPENDS ${DEFAULT_CONFIGS} -) + COMMAND ${CMAKE_COMMAND} -E echo "Compiling built-in default configs" + COMMAND ${LUA_COMPILER} -o defaults.out ${DEFAULT_CONFIGS} + DEPENDS ${DEFAULT_CONFIGS} +) # the manpage -add_custom_command( OUTPUT doc/lsyncd.1 - COMMAND ${CMAKE_COMMAND} -E echo "Updating the manpage" - COMMAND a2x --format=manpage doc/lsyncd.1.txt - DEPENDS doc/lsyncd.1.txt +add_custom_target( manpage + COMMAND ${CMAKE_COMMAND} -E echo "Updating the manpage" + COMMAND a2x --format=manpage doc/manpage/lsyncd.1.txt + DEPENDS doc/manpage/lsyncd.1.txt +) + +add_custom_target( tests + COMMAND echo "Running the tests" + COMMAND echo "Note you are expected to:" + COMMAND echo " * have lua-posix installed" + COMMAND echo " * have a passwordless ssh access to localhost" + COMMAND ${LUA_EXECUTABLE} tests/schedule.lua + COMMAND ${LUA_EXECUTABLE} tests/l4rsyncdata.lua + COMMAND ${LUA_EXECUTABLE} tests/filter-rsync.lua + COMMAND ${LUA_EXECUTABLE} tests/exclude-rsync.lua + COMMAND ${LUA_EXECUTABLE} tests/exclude-rsyncssh.lua + COMMAND ${LUA_EXECUTABLE} tests/churn-rsync.lua + COMMAND ${LUA_EXECUTABLE} tests/churn-rsyncssh.lua + COMMAND ${LUA_EXECUTABLE} tests/churn-direct.lua + COMMAND echo "Finished all successfull!" ) -add_custom_target( manpage ALL DEPENDS doc/lsyncd.1 ) # compiling and linking it all together add_executable( lsyncd ${LSYNCD_SRC} ) target_link_libraries( lsyncd ${LUA_LIBRARIES} ) install( TARGETS lsyncd RUNTIME DESTINATION bin ) -install( FILES doc/lsyncd.1 DESTINATION man) +install( FILES doc/manpage/lsyncd.1 DESTINATION man ) + diff -Nru lsyncd-2.1.6/debian/changelog lsyncd-2.2.3/debian/changelog --- lsyncd-2.1.6/debian/changelog 2016-12-18 19:21:28.000000000 +0000 +++ lsyncd-2.2.3/debian/changelog 2018-12-31 12:25:54.000000000 +0000 @@ -1,3 +1,23 @@ +lsyncd (2.2.3-1) unstable; urgency=medium + + [ Ondřej Nový ] + * d/copyright: Use https protocol in Format field + * d/control: Set Vcs-* to salsa.debian.org + + [ Jan Dittberner ] + * New upstream version (Closes: #862747) + * Refresh debian/patches/out-of-tree-manpage-build.patch + * Update lua dependencies to lua 5.3 + * Fix paths in debian/lsyncd.docs + * Add DEB_BUILD_MAINT_OPTIONS = hardening=+all to debian/rules to + improve hardening + * debian/control: bump Standards-Version to 4.3.0 (no changes) + * Update debian/copyright + * debian/control: Update to debhelper >= 10, use compat level 10 in + debian/compat + + -- Jan Dittberner Mon, 31 Dec 2018 13:25:54 +0100 + lsyncd (2.1.6-1) unstable; urgency=medium * New upstream release diff -Nru lsyncd-2.1.6/debian/compat lsyncd-2.2.3/debian/compat --- lsyncd-2.1.6/debian/compat 2016-12-18 19:21:28.000000000 +0000 +++ lsyncd-2.2.3/debian/compat 2018-12-31 12:25:54.000000000 +0000 @@ -1 +1 @@ -9 +10 diff -Nru lsyncd-2.1.6/debian/control lsyncd-2.2.3/debian/control --- lsyncd-2.1.6/debian/control 2016-12-18 19:21:28.000000000 +0000 +++ lsyncd-2.2.3/debian/control 2018-12-31 12:25:54.000000000 +0000 @@ -2,26 +2,26 @@ Section: admin Priority: optional Maintainer: Jan Dittberner -Standards-Version: 3.9.8 +Standards-Version: 4.3.0 Build-Depends: asciidoc, cmake, - debhelper (>= 9), + debhelper (>= 10), docbook-xml, docbook-xsl, dpkg-dev (>= 1.16.1~), - liblua5.1-0-dev, + liblua5.3-dev, libxml2-dev, libxml2-utils, - lua5.1, + lua5.3, pkg-config, xsltproc Homepage: https://github.com/axkibe/lsyncd -Vcs-Git: git://anonscm.debian.org/collab-maint/lsyncd.git -Vcs-Browser: http://anonscm.debian.org/gitweb/?p=collab-maint/lsyncd.git +Vcs-Git: https://salsa.debian.org/debian/lsyncd.git +Vcs-Browser: https://salsa.debian.org/debian/lsyncd Package: lsyncd Architecture: any -Depends: lsb-base (>= 3.0-6), lua5.1, rsync, ${misc:Depends}, ${shlibs:Depends} +Depends: lsb-base (>= 3.0-6), lua5.3, rsync, ${misc:Depends}, ${shlibs:Depends} Description: daemon to synchronize local directories using rsync Lsyncd (Live syncing mirror daemon) uses rsync to synchronize local directories with a remote machine running rsyncd. Lsyncd watches diff -Nru lsyncd-2.1.6/debian/copyright lsyncd-2.2.3/debian/copyright --- lsyncd-2.1.6/debian/copyright 2016-12-18 19:21:28.000000000 +0000 +++ lsyncd-2.2.3/debian/copyright 2018-12-31 12:25:54.000000000 +0000 @@ -1,10 +1,10 @@ -Format: http://www.debian.org/doc/packaging-manuals/copyright-format/1.0/ +Format: https://www.debian.org/doc/packaging-manuals/copyright-format/1.0/ Upstream-Name: lsyncd Upstream-Contact: Axel Kittenberger Source: https://github.com/axkibe/lsyncd Files: * -Copyright: 2007-2015, Axel Kittenberger +Copyright: 2007-2018, Axel Kittenberger Semi Malinen Jürgen "README" Mangler Eugene Sanivsky @@ -12,7 +12,7 @@ License: GPL-2+ Files: debian/* -Copyright: 2009-2016, Jan Dittberner +Copyright: 2009-2018, Jan Dittberner License: GPL-2+ License: GPL-2+ diff -Nru lsyncd-2.1.6/debian/lsyncd.docs lsyncd-2.2.3/debian/lsyncd.docs --- lsyncd-2.1.6/debian/lsyncd.docs 2016-12-18 19:21:28.000000000 +0000 +++ lsyncd-2.2.3/debian/lsyncd.docs 2018-12-31 12:25:54.000000000 +0000 @@ -1 +1,2 @@ -doc/lsyncd.1.txt +doc/manpage/lsyncd.1.txt +README.md diff -Nru lsyncd-2.1.6/debian/patches/out-of-tree-manpage-build.patch lsyncd-2.2.3/debian/patches/out-of-tree-manpage-build.patch --- lsyncd-2.1.6/debian/patches/out-of-tree-manpage-build.patch 2016-12-18 19:21:28.000000000 +0000 +++ lsyncd-2.2.3/debian/patches/out-of-tree-manpage-build.patch 2018-12-31 12:25:54.000000000 +0000 @@ -5,19 +5,20 @@ +++ b/CMakeLists.txt @@ -82,8 +82,9 @@ # the manpage - add_custom_command( OUTPUT doc/lsyncd.1 - COMMAND ${CMAKE_COMMAND} -E echo "Updating the manpage" -- COMMAND a2x --format=manpage doc/lsyncd.1.txt -- DEPENDS doc/lsyncd.1.txt -+ COMMAND ${CMAKE_COMMAND} -E make_directory doc -+ COMMAND a2x --format=manpage --destination-dir=doc "${PROJECT_SOURCE_DIR}/doc/lsyncd.1.txt" -+ DEPENDS "${PROJECT_SOURCE_DIR}/doc/lsyncd.1.txt" + add_custom_target( manpage + COMMAND ${CMAKE_COMMAND} -E echo "Updating the manpage" +- COMMAND a2x --format=manpage doc/manpage/lsyncd.1.txt +- DEPENDS doc/manpage/lsyncd.1.txt ++ COMMAND ${CMAKE_COMMAND} -E make_directory doc/manpage ++ COMMAND a2x --format=manpage --destination-dir=doc/manpage ${PROJECT_SOURCE_DIR}/doc/manpage/lsyncd.1.txt ++ DEPENDS ${PROJECT_SOURCE_DIR}/doc/manpage/lsyncd.1.txt ) - add_custom_target( manpage ALL DEPENDS doc/lsyncd.1 ) -@@ -92,4 +93,4 @@ + add_custom_target( tests +@@ -107,5 +108,5 @@ target_link_libraries( lsyncd ${LUA_LIBRARIES} ) install( TARGETS lsyncd RUNTIME DESTINATION bin ) --install( FILES doc/lsyncd.1 DESTINATION man) -+install( FILES doc/lsyncd.1 DESTINATION share/man/man1) +-install( FILES doc/manpage/lsyncd.1 DESTINATION man ) ++install( FILES doc/manpage/lsyncd.1 DESTINATION share/man/man1 ) + diff -Nru lsyncd-2.1.6/debian/rules lsyncd-2.2.3/debian/rules --- lsyncd-2.1.6/debian/rules 2016-12-18 19:21:28.000000000 +0000 +++ lsyncd-2.2.3/debian/rules 2018-12-31 12:25:54.000000000 +0000 @@ -1,7 +1,10 @@ #!/usr/bin/make -f +DEB_BUILD_MAINT_OPTIONS = hardening=+all DPKG_EXPORT_BUILDFLAGS = 1 include /usr/share/dpkg/buildflags.mk +# Uncomment this to turn on verbose mode. +#export DH_VERBOSE=1 override_dh_auto_configure: dh_auto_configure $@ diff -Nru lsyncd-2.1.6/default-direct.lua lsyncd-2.2.3/default-direct.lua --- lsyncd-2.1.6/default-direct.lua 2015-10-15 06:29:47.000000000 +0000 +++ lsyncd-2.2.3/default-direct.lua 2018-03-09 12:39:11.000000000 +0000 @@ -62,6 +62,7 @@ spawn( event, '/bin/mkdir', + '--', event.targetPath ) else @@ -69,6 +70,8 @@ spawn( event, '/bin/cp', + '-p', + '--', event.sourcePath, event.targetPathdir ) @@ -79,6 +82,8 @@ end spawn(event, '/bin/cp', + '-p', + '--', event.sourcePath, event.targetPathdir ) @@ -99,7 +104,7 @@ error('Refusing to erase your harddisk!') end - spawn(event, '/bin/rm', '-rf', tp) + spawn(event, '/bin/rm', '-rf', '--', tp) elseif event.etype == 'Move' then local tp = event.targetPath @@ -109,13 +114,13 @@ error('Refusing to erase your harddisk!') end - local command = '/bin/mv $1 $2 || /bin/rm -rf $1' + local command = '/bin/mv -- "$1" "$2" || /bin/rm -rf -- "$1"' if config.delete ~= true and config.delete ~= 'running' then - command = '/bin/mv $1 $2' + command = '/bin/mv -- "$1" "$2"' end spawnShell( @@ -142,8 +147,10 @@ local rc = config.rsyncExitCodes[exitcode] if rc == 'ok' then log('Normal', 'Startup of "',agent.source,'" finished: ', exitcode) - elseif rc == 'again' then - if settings.insist then + elseif rc == 'again' + then + if settings( 'insist' ) + then log('Normal', 'Retrying startup of "',agent.source,'": ', exitcode) else log('Error', 'Temporary or permanent failure on startup of "', diff -Nru lsyncd-2.1.6/default.lua lsyncd-2.2.3/default.lua --- lsyncd-2.1.6/default.lua 2015-10-15 06:29:47.000000000 +0000 +++ lsyncd-2.2.3/default.lua 2018-03-09 12:39:11.000000000 +0000 @@ -8,7 +8,8 @@ -- Authors: Axel Kittenberger --============================================================================ -if default then +if default +then error( 'default already loaded' ) end @@ -56,21 +57,26 @@ -- -- On default action the user's on*** scripts are called. -- -default.action = function( inlet ) - +default.action = function +( + inlet -- the inlet of the active sync. +) -- in case of moves getEvent returns the origin and dest of the move local event, event2 = inlet.getEvent( ) + local config = inlet.getConfig( ) local func = config[ 'on'.. event.etype ] - if type( func ) == 'function' then + if type( func ) == 'function' + then func( event, event2 ) end -- if function didnt change the wait status its not interested -- in this event -> drop it. - if event.status == 'wait' then + if event.status == 'wait' + then inlet.discardEvent( event ) end @@ -82,31 +88,51 @@ -- -- Called when collecting a finished child process -- -default.collect = function( agent, exitcode ) - +default.collect = function +( + agent, -- event or event list being collected + exitcode -- the exitcode of the spawned process +) local config = agent.config + local rc - if config.exitcodes then - rc = config.exitcodes[exitcode] - elseif exitcode == 0 then + if config.exitcodes + then + rc = config.exitcodes[ exitcode ] + elseif exitcode == 0 + then rc = 'ok' else rc = 'die' end -- TODO synchronize with similar code before - if not agent.isList and agent.etype == 'Init' then - if rc == 'ok' then - log('Normal', 'Startup of "',agent.source,'" finished.') + if not agent.isList and agent.etype == 'Init' + then + if rc == 'ok' + then + log( + 'Normal', + 'Startup of ', + agent.source, + ' -> ', + agent.target, + ' finished.' + ) + return 'ok' - elseif rc == 'again' then - if settings('insist') then + elseif rc == 'again' + then + if settings( 'insist' ) + then log( 'Normal', - 'Retrying startup of "', + 'Retrying startup of ', agent.source, - '": ', + ' -> ', + agent.target, + ': ', exitcode ) @@ -114,19 +140,24 @@ else log( 'Error', - 'Temporary or permanent failure on startup of "', + 'Temporary or permanent failure on startup of ', agent.source, - '". Terminating since "insist" is not set.' + ' -> ', + agent.target, + '. Terminating since "insist" is not set.' ) terminate( -1 ) end - elseif rc == 'die' then + elseif rc == 'die' + then log( 'Error', - 'Failure on startup of "', + 'Failure on startup of ', agent.source, - '".' + ' -> ', + agent.target, + '.' ) terminate( -1 ) @@ -135,31 +166,37 @@ 'Error', 'Unknown exitcode "', exitcode, - '" on startup of "', + '" on startup of ', agent.source, - '".' + ' -> ', + agent.target, + '.' ) return 'die' end end - if agent.isList then - if rc == 'ok' then + if agent.isList + then + if rc == 'ok' + then log( 'Normal', 'Finished a list after exitcode: ', exitcode ) - elseif rc == 'again' then + elseif rc == 'again' + then log( 'Normal', 'Retrying a list after exitcode = ', exitcode ) - elseif rc == 'die' then + elseif rc == 'die' + then log( 'Error', - 'Failure with a list width exitcode = ', + 'Failure with a list with exitcode = ', exitcode ) else @@ -171,15 +208,52 @@ rc = 'die' end else - if rc == 'ok' then - log('Normal', 'Finished ',agent.etype,' on ',agent.sourcePath,' = ',exitcode) - elseif rc == 'again' then - log('Normal', 'Retrying ',agent.etype,' on ',agent.sourcePath,' = ',exitcode) - elseif rc == 'die' then - log('Error', 'Failure with ',agent.etype,' on ',agent.sourcePath,' = ',exitcode) + if rc == 'ok' + then + log( + 'Normal', + 'Finished ', + agent.etype, + ' on ', + agent.sourcePath, + ' = ', + exitcode + ) + elseif rc == 'again' + then + log( + 'Normal', + 'Retrying ', + agent.etype, + ' on ', + agent.sourcePath, + ' = ', + exitcode + ) + elseif rc == 'die' + then + log( + 'Error', + 'Failure with ', + agent.etype, + ' on ', + agent.sourcePath, + ' = ', + exitcode + ) else - log('Normal', 'Unknown exitcode "',exitcode,'" with ', agent.etype, - ' on ',agent.sourcePath,' = ',exitcode) + log( + 'Normal', + 'Unknown exitcode "', + exitcode, + '" with ', + agent.etype, + ' on ', + agent.sourcePath, + ' = ', + exitcode + ) + rc = 'die' end end @@ -192,29 +266,35 @@ -- Called on the Init event sent -- on (re)initialization of Lsyncd for every sync -- -default.init = function(event) +default.init = function +( + event -- the precreated init event. +) local config = event.config + local inlet = event.inlet -- user functions -- calls a startup if given by user script. - if type(config.onStartup) == 'function' then - local startup = config.onStartup(event) + if type( config.onStartup ) == 'function' + then + config.onStartup( event ) -- TODO honor some return codes of startup like "warmstart". end - if event.status == 'wait' then + if event.status == 'wait' + then -- user script did not spawn anything -- thus the blanket event is deleted again. - inlet.discardEvent(event) + inlet.discardEvent( event ) end end -- -- The collapsor tries not to have more than these delays. --- So it dealy stack does not grow too large, --- since calculation for stacking events is n*log(n) (or so) +-- So the delay queue does not grow too large +-- since calculation for stacking events is n*log( n ) (or so) -- default.maxDelays = 1000 @@ -273,8 +353,8 @@ -- -- Exitcodes of ssh and what to do. -- -default.sshExitCodes = { - +default.sshExitCodes = +{ -- -- if another config provides the same table -- this will not be inherited (merged) into that one @@ -291,46 +371,40 @@ -- --- Minimum seconds between two writes of a status file. +-- Minimum seconds between two writes of the status file. -- default.statusInterval = 10 -- --- checks all keys to be in the checkgauge +-- Checks all keys to be in the checkgauge. -- - -local function check( +local function check +( config, gauge, subtable, level ) - for k, v in pairs( config ) do - - if not gauge[k] then + for k, v in pairs( config ) + do + if not gauge[ k ] + then error( - 'Parameter "' - .. subtable - .. k - .. '" unknown.' - .. ' (if this is not a typo add it to checkgauge)', + 'Parameter "' .. subtable .. k .. '" unknown.' + .. ' ( if this is not a typo add it to checkgauge )', level ); end - if type( gauge [ k ] ) == 'table' then - - if type( v ) ~= 'table' then - + if type( gauge [ k ] ) == 'table' + then + if type( v ) ~= 'table' + then error( - 'Parameter "' - .. subtable - .. k - .. '" must be a table.', + 'Parameter "' .. subtable .. k .. '" must be a table.', level ) - end check( @@ -339,18 +413,20 @@ subtable .. k .. '.', level + 1 ) - end end end -default.prepare = function( config, level ) + +default.prepare = function +( + config, -- the config to prepare for + level -- current callback level for error reporting +) local gauge = config.checkgauge - if not gauge then - return - end + if not gauge then return end check( config, gauge, '', level + 1 ) end diff -Nru lsyncd-2.1.6/default-rsync.lua lsyncd-2.2.3/default-rsync.lua --- lsyncd-2.1.6/default-rsync.lua 2015-10-15 06:29:47.000000000 +0000 +++ lsyncd-2.2.3/default-rsync.lua 2018-03-09 12:39:11.000000000 +0000 @@ -16,16 +16,9 @@ --~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~ -if not default -then - error( 'default not loaded' ) -end - +if not default then error( 'default not loaded' ) end -if default.rsync -then - error( 'default-rsync already loaded' ) -end +if default.rsync then error( 'default-rsync already loaded' ) end local rsync = { } @@ -49,20 +42,31 @@ delete = true, exclude = true, excludeFrom = true, + filter = true, + filterFrom = true, target = true, rsync = { acls = true, + append = true, + append_verify = true, archive = true, + backup = true, + backup_dir = true, binary = true, bwlimit = true, checksum = true, + chown = true, + chmod = true, compress = true, + copy_dirlinks = true, copy_links = true, cvs_exclude = true, dry_run = true, executability = true, + existing = true, group = true, + groupmap = true, hard_links = true, ignore_times = true, inplace = true, @@ -71,6 +75,8 @@ keep_dirlinks = true, links = true, one_file_system = true, + omit_dir_times = true, + omit_link_times = true, owner = true, password_file = true, perms = true, @@ -80,10 +86,12 @@ rsh = true, rsync_path = true, sparse = true, + suffix = true, temp_dir = true, timeout = true, times = true, update = true, + usermap = true, verbose = true, whole_file = true, xattrs = true, @@ -93,29 +101,44 @@ -- +-- Returns true for non Init and Blanket events. +-- +local eventNotInitBlank = + function +( + event +) + return event.etype ~= 'Init' and event.etype ~= 'Blanket' +end + + +-- -- Spawns rsync for a list of events -- --- Exlcusions are already handled by not having +-- Exclusions are already handled by not having -- events for them. -- -rsync.action = function( inlet ) +rsync.action = function +( + inlet +) + local config = inlet.getConfig( ) - -- -- gets all events ready for syncing - -- - local elist = inlet.getEvents( - function(event) - return event.etype ~= 'Init' and event.etype ~= 'Blanket' - end - ) + local elist = inlet.getEvents( eventNotInitBlank ) + + -- gets the list of paths for the event list + -- deletes create multi match patterns + local paths = elist.getPaths( ) -- -- Replaces what rsync would consider filter rules by literals -- - local function sub( p ) - if not p then - return - end + local function sub + ( + p -- pattern + ) + if not p then return end return p: gsub( '%?', '\\?' ): @@ -130,8 +153,14 @@ -- Deletes create multi match patterns -- local paths = elist.getPaths( - function( etype, path1, path2 ) - if string.byte( path1, -1 ) == 47 and etype == 'Delete' then + function + ( + etype, -- event type + path1, -- path + path2 -- path to for move events + ) + if string.byte( path1, -1 ) == 47 and etype == 'Delete' + then return sub( path1 )..'***', sub( path2 ) else return sub( path1 ), sub( path2 ) @@ -139,74 +168,65 @@ end ) - -- -- stores all filters by integer index - -- local filterI = { } - -- - -- Stores all filters with path index - -- + -- stores all filters with path index local filterP = { } - -- - -- Adds one path to the filter - -- - local function addToFilter( path ) - - if filterP[ path ] then - return - end + -- adds one path to the filter + local function addToFilter + ( + path + ) + if filterP[ path ] then return end filterP[ path ] = true table.insert( filterI, path ) end + -- adds a path to the filter. -- - -- Adds a path to the filter. - -- - -- Rsync needs to have entries for all steps in the path, + -- rsync needs to have entries for all steps in the path, -- so the file for example d1/d2/d3/f1 needs following filters: -- 'd1/', 'd1/d2/', 'd1/d2/d3/' and 'd1/d2/d3/f1' - -- - for _, path in ipairs( paths ) do - - if path and path ~= '' then - - addToFilter(path) + for _, path in ipairs( paths ) + do + if path and path ~= '' + then + addToFilter( path ) local pp = string.match( path, '^(.*/)[^/]+/?' ) - while pp do - addToFilter(pp) + while pp + do + addToFilter( pp ) + pp = string.match( pp, '^(.*/)[^/]+/?' ) end - end - end - local filterS = table.concat( filterI, '\n' ) - local filter0 = table.concat( filterI, '\000' ) - log( 'Normal', 'Calling rsync with filter-list of new/modified files/dirs\n', - filterS + table.concat( filterI, '\n' ) ) local config = inlet.getConfig( ) + local delete = nil - if config.delete == true or config.delete == 'running' then + if config.delete == true or config.delete == 'running' + then delete = { '--delete', '--ignore-errors' } end spawn( elist, config.rsync.binary, - '<', filter0, + '<', table.concat( filterI, '\000' ), config.rsync._computed, '-r', delete, @@ -217,35 +237,108 @@ config.source, config.target ) - end +---- +---- NOTE: This optimized version can be used once +---- https://bugzilla.samba.org/show_bug.cgi?id=12569 +---- is fixed. +---- +---- Spawns rsync for a list of events +---- +---- Exclusions are already handled by not having +---- events for them. +---- +--rsync.action = function +--( +-- inlet +--) +-- local config = inlet.getConfig( ) +-- +-- -- gets all events ready for syncing +-- local elist = inlet.getEvents( eventNotInitBlank ) +-- +-- -- gets the list of paths for the event list +-- -- deletes create multi match patterns +-- local paths = elist.getPaths( ) +-- +-- -- removes trailing slashes from dirs. +-- for k, v in ipairs( paths ) +-- do +-- if string.byte( v, -1 ) == 47 +-- then +-- paths[ k ] = string.sub( v, 1, -2 ) +-- end +-- end -- --- Spawns the recursive startup sync +-- log( +-- 'Normal', +-- 'Calling rsync with filter-list of new/modified files/dirs\n', +-- table.concat( paths, '\n' ) +-- ) -- -rsync.init = function(event) +-- local delete = nil +-- +-- if config.delete == true +-- or config.delete == 'running' +-- then +-- delete = { '--delete-missing-args', '--ignore-errors' } +-- end +-- +-- spawn( +-- elist, +-- config.rsync.binary, +-- '<', table.concat( paths, '\000' ), +-- config.rsync._computed, +-- delete, +-- '--force', +-- '--from0', +-- '--files-from=-', +-- config.source, +-- config.target +-- ) +--end + +-- +-- Spawns the recursive startup sync. +-- +rsync.init = function +( + event +) local config = event.config + local inlet = event.inlet + local excludes = inlet.getExcludes( ) + + local filters = inlet.hasFilters( ) and inlet.getFilters( ) + local delete = nil + local target = config.target - if not target then - if not config.host then + if not target + then + if not config.host + then error('Internal fail, Neither target nor host is configured') end target = config.host .. ':' .. config.targetdir end - if config.delete == true or config.delete == 'startup' then + if config.delete == true + or config.delete == 'startup' + then delete = { '--delete', '--ignore-errors' } end - if #excludes == 0 then - -- start rsync without any excludes + if not filters and #excludes == 0 + then + -- starts rsync without any filters or excludes log( 'Normal', 'recursive startup rsync: ', @@ -264,8 +357,9 @@ target ) - else - -- start rsync providing an exclude list + elseif not filters + then + -- starts rsync providing an exclusion list -- on stdin local exS = table.concat( excludes, '\n' ) @@ -290,6 +384,32 @@ config.source, target ) + else + -- starts rsync providing a filter list + -- on stdin + local fS = table.concat( filters, '\n' ) + + log( + 'Normal', + 'recursive startup rsync: ', + config.source, + ' -> ', + target, + ' filtering\n', + fS + ) + + spawn( + event, + config.rsync.binary, + '<', fS, + '--filter=. -', + delete, + config.rsync._computed, + '-r', + config.source, + target + ) end end @@ -297,12 +417,12 @@ -- -- Prepares and checks a syncs configuration on startup. -- -rsync.prepare = - function( - config, -- the configuration - level, -- additional error level for inherited use ( by rsyncssh ) - skipTarget -- used by rsyncssh, do not check for target - ) +rsync.prepare = function +( + config, -- the configuration + level, -- additional error level for inherited use ( by rsyncssh ) + skipTarget -- used by rsyncssh, do not check for target +) -- First let default.prepare test the checkgauge default.prepare( config, level + 6 ) @@ -315,56 +435,6 @@ ) end - if config.rsyncOps - then - error( - '"rsyncOps" is outdated please use the new rsync = { ... } syntax.', - level - ) - end - - if config.rsyncOpts and config.rsync._extra - then - error( - '"rsyncOpts" is outdated in favor of the new rsync = { ... } syntax\n"' + - 'for which you provided the _extra attribute as well.\n"' + - 'Please remove rsyncOpts from your config.', - level - ) - end - - if config.rsyncOpts - then - log( - 'Warn', - '"rsyncOpts" is outdated. Please use the new rsync = { ... } syntax."' - ) - - config.rsync._extra = config.rsyncOpts - config.rsyncOpts = nil - end - - if config.rsyncBinary and config.rsync.binary - then - error( - '"rsyncBinary is outdated in favor of the new rsync = { ... } syntax\n"'+ - 'for which you provided the binary attribute as well.\n"' + - "Please remove rsyncBinary from your config.'", - level - ) - end - - if config.rsyncBinary - then - log( - 'Warn', - '"rsyncBinary" is outdated. Please use the new rsync = { ... } syntax."' - ) - - config.rsync.binary = config.rsyncBinary - config.rsyncOpts = nil - end - -- checks if the _computed argument exists already if config.rsync._computed then @@ -404,15 +474,18 @@ end end - crsync._computed = { true } + local computed = crsync._computed + local computedN = 2 local shortFlags = { acls = 'A', + backup = 'b', checksum = 'c', compress = 'z', + copy_dirlinks = 'k', copy_links = 'L', cvs_exclude = 'C', dry_run = 'n', @@ -425,6 +498,8 @@ keep_dirlinks = 'K', links = 'l', one_file_system = 'x', + omit_dir_times = 'O', + omit_link_times = 'J', owner = 'o', perms = 'p', protect_args = 's', @@ -477,12 +552,54 @@ end end + if crsync.append + then + computed[ computedN ] = '--append' + computedN = computedN + 1 + end + + if crsync.append_verify + then + computed[ computedN ] = '--append-verify' + computedN = computedN + 1 + end + + if crsync.backup_dir + then + computed[ computedN ] = '--backup-dir=' .. crsync.backup_dir + computedN = computedN + 1 + end + if crsync.bwlimit then computed[ computedN ] = '--bwlimit=' .. crsync.bwlimit computedN = computedN + 1 end + if crsync.chmod + then + computed[ computedN ] = '--chmod=' .. crsync.chmod + computedN = computedN + 1 + end + + if crsync.chown + then + computed[ computedN ] = '--chown=' .. crsync.chown + computedN = computedN + 1 + end + + if crsync.groupmap + then + computed[ computedN ] = '--groupmap=' .. crsync.groupmap + computedN = computedN + 1 + end + + if crsync.existing + then + computed[ computedN ] = '--existing' + computedN = computedN + 1 + end + if crsync.inplace then computed[ computedN ] = '--inplace' @@ -507,6 +624,12 @@ computedN = computedN + 1 end + if crsync.suffix + then + computed[ computedN ] = '--suffix=' .. crsync.suffix + computedN = computedN + 1 + end + if crsync.temp_dir then computed[ computedN ] = '--temp-dir=' .. crsync.temp_dir @@ -519,6 +642,12 @@ computedN = computedN + 1 end + if crsync.usermap + then + computed[ computedN ] = '--usermap=' .. crsync.usermap + computedN = computedN + 1 + end + if shortsN ~= 2 then computed[ 1 ] = table.concat( shorts, '' ) @@ -527,11 +656,13 @@ end -- appends a / to target if not present - if not skipTarget and string.sub(config.target, -1) ~= '/' + -- and not a ':' for home dir. + if not skipTarget + and string.sub( config.target, -1 ) ~= '/' + and string.sub( config.target, -1 ) ~= ':' then config.target = config.target..'/' end - end diff -Nru lsyncd-2.1.6/default-rsyncssh.lua lsyncd-2.2.3/default-rsyncssh.lua --- lsyncd-2.1.6/default-rsyncssh.lua 2015-10-15 06:29:47.000000000 +0000 +++ lsyncd-2.2.3/default-rsyncssh.lua 2018-03-09 12:39:11.000000000 +0000 @@ -15,15 +15,18 @@ -- -- -if not default then +if not default +then error( 'default not loaded' ); end -if not default.rsync then +if not default.rsync +then error( 'default.rsync not loaded' ); end -if default.rsyncssh then +if default.rsyncssh +then error( 'default-rsyncssh already loaded' ); end @@ -31,6 +34,7 @@ -- rsyncssh extends default.rsync -- local rsyncssh = { default.rsync } + default.rsyncssh = rsyncssh -- @@ -56,29 +60,67 @@ port = true, _extra = true }, - - -- xargs settings - xargs = { - binary = true, - delimiter = true, - _extra = true - } } + -- --- Spawns rsync for a list of events +-- Returns true for non Init, Blanket and Move events. -- -rsyncssh.action = function( inlet ) +local eventNotInitBlankMove = + function +( + event +) + -- TODO use a table + if event.etype == 'Move' + or event.etype == 'Init' + or event.etype == 'Blanket' + then + return 'break' + else + return true + end +end - local event, event2 = inlet.getEvent( ) +-- +-- Replaces what rsync would consider filter rules by literals. +-- +local replaceRsyncFilter = + function +( + path +) + if not path then return end + + return( + path + :gsub( '%?', '\\?' ) + :gsub( '%*', '\\*' ) + :gsub( '%[', '\\[' ) + ) +end + + +-- +-- Spawns rsync for a list of events +-- +rsyncssh.action = function +( + inlet +) local config = inlet.getConfig( ) + local event, event2 = inlet.getEvent( ) + -- makes move local on target host -- if the move fails, it deletes the source - if event.etype == 'Move' then + if event.etype == 'Move' + then local path1 = config.targetdir .. event.path + local path2 = config.targetdir .. event2.path + path1 = "'" .. path1:gsub ('\'', '\'"\'"\'') .. "'" path2 = "'" .. path2:gsub ('\'', '\'"\'"\'') .. "'" @@ -105,169 +147,283 @@ return end - -- uses ssh to delete files on remote host - -- instead of constructing rsync filters + -- otherwise a rsync is spawned + local elist = inlet.getEvents( eventNotInitBlankMove ) - if event.etype == 'Delete' then - - if - config.delete ~= true and - config.delete ~= 'running' - then - inlet.discardEvent(event) - return - end + -- gets the list of paths for the event list + -- deletes create multi match patterns + local paths = elist.getPaths( ) - -- gets all other deletes ready to be - -- executed - local elist = inlet.getEvents( - function( e ) - return e.etype == 'Delete' - end - ) + -- + -- Replaces what rsync would consider filter rules by literals + -- + local function sub( p ) + if not p then return end - -- returns the paths of the delete list - local paths = elist.getPaths( - function( etype, path1, path2 ) - if path2 then - return config.targetdir..path1, config.targetdir..path2 - else - return config.targetdir..path1 - end - end - ) + return p: + gsub( '%?', '\\?' ): + gsub( '%*', '\\*' ): + gsub( '%[', '\\[' ): + gsub( '%]', '\\]' ) + end - -- ensures none of the paths is '/' - for _, v in pairs( paths ) do - if string.match(v, '^%s*/+%s*$') then - log('Error', 'refusing to `rm -rf /` the target!') - terminate(-1) -- ERRNO + -- + -- Gets the list of paths for the event list + -- + -- Deletes create multi match patterns + -- + local paths = elist.getPaths( + function( etype, path1, path2 ) + if string.byte( path1, -1 ) == 47 and etype == 'Delete' then + return sub( path1 )..'***', sub( path2 ) + else + return sub( path1 ), sub( path2 ) end end + ) - log( - 'Normal', - 'Deleting list\n', - table.concat( paths, '\n' ) - ) + -- stores all filters by integer index + local filterI = { } - local params = { } + -- stores all filters with path index + local filterP = { } - spawn( - elist, - config.ssh.binary, - '<', table.concat(paths, config.xargs.delimiter), - params, - config.ssh._computed, - config.host, - config.xargs.binary, - config.xargs._extra - ) + -- adds one path to the filter + local function addToFilter( path ) + if filterP[ path ] then return end - return + filterP[ path ] = true + + table.insert( filterI, path ) end + -- adds a path to the filter. -- - -- for everything else a rsync is spawned - -- - local elist = inlet.getEvents( - function( e ) - -- TODO use a table - return e.etype ~= 'Move' and - e.etype ~= 'Delete' and - e.etype ~= 'Init' and - e.etype ~= 'Blanket' - end - ) + -- rsync needs to have entries for all steps in the path, + -- so the file for example d1/d2/d3/f1 needs following filters: + -- 'd1/', 'd1/d2/', 'd1/d2/d3/' and 'd1/d2/d3/f1' + for _, path in ipairs( paths ) + do + if path and path ~= '' + then + addToFilter(path) - local paths = elist.getPaths( ) + local pp = string.match( path, '^(.*/)[^/]+/?' ) + + while pp + do + addToFilter(pp) + pp = string.match( pp, '^(.*/)[^/]+/?' ) + end - -- - -- removes trailing slashes from dirs. - -- - for k, v in ipairs( paths ) do - if string.byte( v, -1 ) == 47 then - paths[k] = string.sub( v, 1, -2 ) end - end - local sPaths = table.concat( paths, '\n' ) - local zPaths = table.concat( paths, '\000' ) + end log( 'Normal', - 'Rsyncing list\n', - sPaths + 'Calling rsync with filter-list of new/modified files/dirs\n', + table.concat( filterI, '\n' ) ) + local config = inlet.getConfig( ) + + local delete = nil + + if config.delete == true or config.delete == 'running' + then + delete = { '--delete', '--ignore-errors' } + end + spawn( elist, config.rsync.binary, - '<', zPaths, + '<', table.concat( filterI, '\000' ), config.rsync._computed, + '-r', + delete, + '--force', '--from0', - '--files-from=-', + '--include-from=-', + '--exclude=*', config.source, config.host .. ':' .. config.targetdir ) end ------ --- Called when collecting a finished child process + +---- +---- NOTE: This optimized version can be used once +---- https://bugzilla.samba.org/show_bug.cgi?id=12569 +---- is fixed. +---- +-- +-- Spawns rsync for a list of events -- -rsyncssh.collect = function( agent, exitcode ) +--rsyncssh.action = function +--( +-- inlet +--) +-- local config = inlet.getConfig( ) +-- +-- local event, event2 = inlet.getEvent( ) +-- +-- -- makes move local on target host +-- -- if the move fails, it deletes the source +-- if event.etype == 'Move' +-- then +-- local path1 = config.targetdir .. event.path +-- +-- local path2 = config.targetdir .. event2.path +-- +-- path1 = "'" .. path1:gsub ('\'', '\'"\'"\'') .. "'" +-- path2 = "'" .. path2:gsub ('\'', '\'"\'"\'') .. "'" +-- +-- log( +-- 'Normal', +-- 'Moving ', +-- event.path, +-- ' -> ', +-- event2.path +-- ) +-- +-- spawn( +-- event, +-- config.ssh.binary, +-- config.ssh._computed, +-- config.host, +-- 'mv', +-- path1, +-- path2, +-- '||', 'rm', '-rf', +-- path1 +-- ) +-- +-- return +-- end +-- +-- -- otherwise a rsync is spawned +-- local elist = inlet.getEvents( eventNotInitBlankMove ) +-- +-- -- gets the list of paths for the event list +-- -- deletes create multi match patterns +-- local paths = elist.getPaths( ) +-- +-- -- removes trailing slashes from dirs. +-- for k, v in ipairs( paths ) +-- do +-- if string.byte( v, -1 ) == 47 +-- then +-- paths[ k ] = string.sub( v, 1, -2 ) +-- end +-- end +-- +-- log( +-- 'Normal', +-- 'Rsyncing list\n', +-- table.concat( paths, '\n' ) +-- ) +-- +-- local delete = nil +-- +-- if config.delete == true +-- or config.delete == 'running' +-- then +-- delete = { '--delete-missing-args', '--ignore-errors' } +-- end +-- +-- spawn( +-- elist, +-- config.rsync.binary, +-- '<', table.concat( paths, '\000' ), +-- config.rsync._computed, +-- delete, +-- '--force', +-- '--from0', +-- '--files-from=-', +-- config.source, +-- config.host .. ':' .. config.targetdir +-- ) +--end + +-- +-- Called when collecting a finished child process +-- +rsyncssh.collect = function +( + agent, + exitcode +) local config = agent.config - if not agent.isList and agent.etype == 'Init' then + if not agent.isList and agent.etype == 'Init' + then local rc = config.rsyncExitCodes[exitcode] - if rc == 'ok' then - log('Normal', 'Startup of "',agent.source,'" finished: ', exitcode) - elseif rc == 'again' then - if settings('insist') then - log('Normal', 'Retrying startup of "',agent.source,'": ', exitcode) + if rc == 'ok' + then + log( 'Normal', 'Startup of "', agent.source, '" finished: ', exitcode ) + elseif rc == 'again' + then + if settings('insist') + then + log( 'Normal', 'Retrying startup of "', agent.source, '": ', exitcode ) else - log('Error', 'Temporary or permanent failure on startup of "', - agent.source, '". Terminating since "insist" is not set.'); - terminate(-1) -- ERRNO - end + log( + 'Error', + 'Temporary or permanent failure on startup of "', + agent.source, '". Terminating since "insist" is not set.' + ) - elseif rc == 'die' then - log('Error', 'Failure on startup of "',agent.source,'": ', exitcode) + terminate( -1 ) -- ERRNO + end + elseif rc == 'die' + then + log( 'Error', 'Failure on startup of "', agent.source, '": ', exitcode ) else - log('Error', 'Unknown exitcode on startup of "', agent.source,': "',exitcode) + log( 'Error', 'Unknown exitcode on startup of "', agent.source, ': "', exitcode ) + rc = 'die' end return rc - end - if agent.isList then - local rc = config.rsyncExitCodes[exitcode] - if rc == 'ok' then - log('Normal', 'Finished (list): ',exitcode) - elseif rc == 'again' then - log('Normal', 'Retrying (list): ',exitcode) - elseif rc == 'die' then - log('Error', 'Failure (list): ', exitcode) + if agent.isList + then + local rc = config.rsyncExitCodes[ exitcode ] + + if rc == 'ok' + then + log( 'Normal', 'Finished (list): ', exitcode ) + elseif rc == 'again' + then + log( 'Normal', 'Retrying (list): ', exitcode ) + elseif rc == 'die' + then + log( 'Error', 'Failure (list): ', exitcode ) else - log('Error', 'Unknown exitcode (list): ',exitcode) + log( 'Error', 'Unknown exitcode (list): ', exitcode ) + rc = 'die' end return rc else local rc = config.sshExitCodes[exitcode] - if rc == 'ok' then - log('Normal', 'Finished ',agent.etype,' ',agent.sourcePath,': ',exitcode) - elseif rc == 'again' then - log('Normal', 'Retrying ',agent.etype,' ',agent.sourcePath,': ',exitcode) - elseif rc == 'die' then - log('Normal', 'Failure ',agent.etype,' ',agent.sourcePath,': ',exitcode) + if rc == 'ok' + then + log( 'Normal', 'Finished ', agent.etype,' ', agent.sourcePath, ': ', exitcode ) + elseif rc == 'again' + then + log( 'Normal', 'Retrying ', agent.etype, ' ', agent.sourcePath, ': ', exitcode ) + elseif rc == 'die' + then + log( 'Normal', 'Failure ', agent.etype, ' ', agent.sourcePath, ': ', exitcode ) else - log('Error', 'Unknown exitcode ',agent.etype,' ',agent.sourcePath,': ',exitcode) + log( 'Error', 'Unknown exitcode ', agent.etype,' ', agent.sourcePath,': ', exitcode ) + rc = 'die' end @@ -279,65 +435,59 @@ -- -- checks the configuration. -- -rsyncssh.prepare = function( config, level ) - +rsyncssh.prepare = function +( + config, + level +) default.rsync.prepare( config, level + 1, true ) - if not config.host then - error( - 'default.rsyncssh needs "host" configured', - level - ) + if not config.host + then + error( 'default.rsyncssh needs "host" configured', level ) end - if not config.targetdir then - error( - 'default.rsyncssh needs "targetdir" configured', - level - ) + if not config.targetdir + then + error( 'default.rsyncssh needs "targetdir" configured', level ) end -- -- computes the ssh options -- - if config.ssh._computed then - error( - 'please do not use the internal rsync._computed parameter', - level - ) + if config.ssh._computed + then + error( 'please do not use the internal rsync._computed parameter', level ) end - local cssh = - config.ssh; + if config.maxProcesses ~= 1 + then + error( 'default.rsyncssh must have maxProcesses set to 1.', level ) + end - cssh._computed = - { } + local cssh = config.ssh; - local computed = - cssh._computed + cssh._computed = { } - local computedN = - 1 + local computed = cssh._computed - local rsyncc = - config.rsync._computed + local computedN = 1 - if cssh.identityFile then - computed[ computedN ] = - '-i' + local rsyncc = config.rsync._computed - computed[ computedN + 1 ] = - cssh.identityFile + if cssh.identityFile + then + computed[ computedN ] = '-i' - computedN = - computedN + 2 + computed[ computedN + 1 ] = cssh.identityFile - if not config.rsync._rshIndex then - config.rsync._rshIndex = - #rsyncc + 1 + computedN = computedN + 2 - rsyncc[ config.rsync._rshIndex ] = - '--rsh=ssh' + if not config.rsync._rshIndex + then + config.rsync._rshIndex = #rsyncc + 1 + + rsyncc[ config.rsync._rshIndex ] = '--rsh=ssh' end rsyncc[ config.rsync._rshIndex ] = @@ -346,26 +496,21 @@ cssh.identityFile end - if cssh.options then - - - for k, v in pairs( cssh.options ) do + if cssh.options + then + for k, v in pairs( cssh.options ) + do + computed[ computedN ] = '-o' - computed[ computedN ] = - '-o' + computed[ computedN + 1 ] = k .. '=' .. v - computed[ computedN + 1 ] = - k .. '=' .. v + computedN = computedN + 2 - computedN = - computedN + 2 + if not config.rsync._rshIndex + then + config.rsync._rshIndex = #rsyncc + 1 - if not config.rsync._rshIndex then - config.rsync._rshIndex = - #rsyncc + 1 - - rsyncc[ config.rsync._rshIndex ] = - '--rsh=ssh' + rsyncc[ config.rsync._rshIndex ] = '--rsh=ssh' end rsyncc[ config.rsync._rshIndex ] = @@ -382,42 +527,41 @@ end end - if cssh.port then - computed[ computedN ] = - '-p' - - computed[ computedN + 1 ] = - cssh.port - - computedN = - computedN + 2 - - if not config.rsync._rshIndex then - config.rsync._rshIndex = - #rsyncc + 1 + if cssh.port + then + computed[ computedN ] = '-p' - rsyncc[ config.rsync._rshIndex ] = - '--rsh=ssh' + computed[ computedN + 1 ] = cssh.port + + computedN = computedN + 2 + + if not config.rsync._rshIndex + then + config.rsync._rshIndex = #rsyncc + 1 + + rsyncc[ config.rsync._rshIndex ] = '--rsh=ssh' end rsyncc[ config.rsync._rshIndex ] = rsyncc[ config.rsync._rshIndex ] .. ' -p ' .. cssh.port end - if cssh._extra then - for k, v in ipairs( cssh._extra ) do - computed[ computedN ] = - v + if cssh._extra + then + for k, v in ipairs( cssh._extra ) + do + computed[ computedN ] = v - computedN = - computedN + 1 + computedN = computedN + 1 end end -- appends a slash to the targetdir if missing - if string.sub( config.targetdir, -1 ) ~= '/' then - config.targetdir = - config.targetdir .. '/' + -- and is not ':' for home dir + if string.sub( config.targetdir, -1 ) ~= '/' + and string.sub( config.targetdir, -1 ) ~= ':' + then + config.targetdir = config.targetdir .. '/' end end @@ -425,64 +569,34 @@ -- -- allow processes -- -rsyncssh.maxProcesses = - 1 +rsyncssh.maxProcesses = 1 -- -- The core should not split move events -- -rsyncssh.onMove = - true +rsyncssh.onMove = true -- -- default delay -- -rsyncssh.delay = - 15 +rsyncssh.delay = 15 -- -- no default exit codes -- -rsyncssh.exitcodes = - false +rsyncssh.exitcodes = false -- -- rsync exit codes -- -rsyncssh.rsyncExitCodes = - default.rsyncExitCodes +rsyncssh.rsyncExitCodes = default.rsyncExitCodes -- -- ssh exit codes -- -rsyncssh.sshExitCodes = - default.sshExitCodes - --- --- xargs calls configuration --- --- xargs is used to delete multiple remote files, when ssh access is --- available this is simpler than to build filters for rsync for this. --- -rsyncssh.xargs = { +rsyncssh.sshExitCodes = default.sshExitCodes - -- - -- the binary called (on target host) - binary = - '/usr/bin/xargs', - - -- - -- delimiter, uses null by default, you might want to override this for older - -- by for example '\n' - delimiter = - '\000', - - -- - -- extra parameters - _extra = - { '-0', 'rm -rf' } -} -- -- ssh calls configuration @@ -494,31 +608,26 @@ -- -- the binary called -- - binary = - '/usr/bin/ssh', + binary = '/usr/bin/ssh', -- -- if set adds this key to ssh -- - identityFile = - nil, + identityFile = nil, -- -- if set adds this special options to ssh -- - options = - nil, + options = nil, -- -- if set connect to this port -- - port = - nil, + port = nil, -- -- extra parameters -- - _extra = - { } + _extra = { } } diff -Nru lsyncd-2.1.6/distclean.sh lsyncd-2.2.3/distclean.sh --- lsyncd-2.1.6/distclean.sh 1970-01-01 00:00:00.000000000 +0000 +++ lsyncd-2.2.3/distclean.sh 2018-03-09 12:39:11.000000000 +0000 @@ -0,0 +1,3 @@ +#!/bin/sh +# removes all stuff generated by cmake / make +rm -rf AdditionalInfo.txt config.h Makefile build/ CMakeCache.txt CMakeFiles/ cmake_install.cmake install_manifest.txt defaults.c runner.c *.o *.out lsyncd diff -Nru lsyncd-2.1.6/doc/lsyncd.1 lsyncd-2.2.3/doc/lsyncd.1 --- lsyncd-2.1.6/doc/lsyncd.1 2015-10-15 06:29:47.000000000 +0000 +++ lsyncd-2.2.3/doc/lsyncd.1 1970-01-01 00:00:00.000000000 +0000 @@ -1,157 +0,0 @@ -'\" t -.\" Title: lsyncd -.\" Author: [see the "AUTHOR" section] -.\" Generator: DocBook XSL Stylesheets v1.78.1 -.\" Date: April 2012 -.\" Manual: Lsyncd -.\" Source: Lsyncd 2.0.7 -.\" Language: English -.\" -.TH "LSYNCD" "1" "April 2012" "Lsyncd 2\&.0\&.7" "Lsyncd" -.\" ----------------------------------------------------------------- -.\" * Define some portability stuff -.\" ----------------------------------------------------------------- -.\" ~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~ -.\" http://bugs.debian.org/507673 -.\" http://lists.gnu.org/archive/html/groff/2009-02/msg00013.html -.\" ~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~ -.ie \n(.g .ds Aq \(aq -.el .ds Aq ' -.\" ----------------------------------------------------------------- -.\" * set default formatting -.\" ----------------------------------------------------------------- -.\" disable hyphenation -.nh -.\" disable justification (adjust text to left margin only) -.ad l -.\" ----------------------------------------------------------------- -.\" * MAIN CONTENT STARTS HERE * -.\" ----------------------------------------------------------------- -.SH "NAME" -lsyncd \- a daemon to continuously synchronize directory trees -.SH "SYNOPSIS" -.PP -config file -.RS 4 -\ \& -\fBlsyncd\fR -[\fIOPTIONS\fR] -\fICONFIG\-FILE\fR -.RE -.PP -default rsync behaviour -.RS 4 -\ \& -\fBlsyncd\fR -[\fIOPTIONS\fR] \-rsync -\fISOURCEDIR\fR\fITARGET\fR -\&... -.RE -.PP -default rync+ssh behaviour (moves and deletes through ssh) -.RS 4 -\ \& -\fBlsyncd\fR -[\fIOPTIONS\fR] \-rsyncssh -\fISOURCEDIR\fR\fITARGETHOST\fR\fITARGETDIR\fR -\&... -.RE -.PP -default direct behaviour (local file operations/rsync) -.RS 4 -\ \& -\fBlsyncd\fR -[\fIOPTIONS\fR] \-direct -\fISOURCEDIR\fR\fITARGETDIR\fR -\&... -.RE -.SH "DESCRIPTION" -.sp -Lsyncd(1) watches local directory trees through an event monitor interface (inotify, fsevents)\&. It aggregates and combines events for a few seconds and then spawns one or more processes to synchronize the changes\&. By default this is rsync(1)\&. Lsyncd is thus a light\-weight asynchronous live mirror solution that is comparatively easy to install not requiring new filesystems or block devices and does not hamper local filesystem performance\&. -.sp -Rsync+ssh is an advanced action configuration that uses a SSH(1) to act file and directory moves directly on the target instead of re\-transmitting the move destination over the wire\&. -.sp -Fine\-grained customization can be achieved through the CONFIG\-FILE\&. Custom action configs can even be written from scratch in cascading layers ranging from shell scripts to code written in the LUA(1) language\&. This way simplicity can be balanced with powerfulness\&. See the online manual for details on the CONFIG\-FILE https://github\&.com/axkibe/lsyncd/wiki/Manual\-to\-Lsyncd\-2\&.0\&.x \&. -.sp -Note that under normal configuration Lsyncd will delete pre\-existing files in the target directories that are not present in the respective source directory\&. -.SH "OPTIONS" -.PP -\fB\-delay\fR \fISECS\fR -.RS 4 -Overrides the default delay times\&. -.RE -.PP -\fB\-help\fR -.RS 4 -Show a help message\&. -.RE -.PP -\fB\-insist\fR -.RS 4 -Continues start up even if rsync cannot connect\&. -.RE -.PP -\fB\-log\fR \fILEVEL\fR -.RS 4 -Controls which kind of events are logged\&. By default Lsyncd logs -\fINormal\fR -and -\fIError\fR -Messages\&. -\fB\-log scarce\fR -will make Lsyncd log Error messages only\&. -\fB\-log all\fR -will log all debug messages\&. -.RE -.PP -\fB\-log\fR \fICategory\fR -.RS 4 -Turns on a specific debug message\&. E\&.g\&. -\fB\-log Exec\fR -will log all processes as they are spawned\&. -.RE -.PP -\fB\-nodaemon\fR -.RS 4 -Lsyncd will not detach from the invoker and log as well to stdout/err\&. -.RE -.PP -\fB\-pidfile\fR \fIFILE\fR -.RS 4 -Lsyncd will write its process ID in -\fIFILE\fR\&. -.RE -.PP -\fB\-runner\fR \fIFILE\fR -.RS 4 -Makes the Lsyncd core load the part of Lsyncd written in Lua from -\fIFILE\fR\&. -.RE -.PP -\fB\-version\fR -.RS 4 -Writes version information and exits\&. -.RE -.SH "EXIT STATUS" -.PP -\fB0\fR -.RS 4 -Terminated on a TERM signal(7) -.RE -.PP -\fB\-1\fR -.RS 4 -Failure (syntax, unrecoverable error condition, internal failure) -.RE -.SH "SEE ALSO" -.sp -Online Manual: https://github\&.com/axkibe/lsyncd/wiki/Lsyncd\-2\&.1\&.x\-%E2%80%96\-What\(cqs\-New%3F -.SH "VERSION" -.sp -This man page is for lsyncd(1) version 2\&.0\&.7 -.SH "AUTHOR" -.sp -Axel Kittenberger, 2010\-2012 -.SH "COPYING" -.sp -Copyright (C) 2010\-2012 Axel Kittenberger\&. Free use of this software is granted under the terms of the GNU General Public License (GPL) version 2, or any later version\&. Free redistrubition of this Documentation (/doc directory) is granted under the terms of the Creative Commons 3\&.0 Attribution License (CC\-3\&.0\-BY)\&. diff -Nru lsyncd-2.1.6/doc/lsyncd.1.txt lsyncd-2.2.3/doc/lsyncd.1.txt --- lsyncd-2.1.6/doc/lsyncd.1.txt 2015-10-15 06:29:47.000000000 +0000 +++ lsyncd-2.2.3/doc/lsyncd.1.txt 1970-01-01 00:00:00.000000000 +0000 @@ -1,107 +0,0 @@ -lsyncd(1) -========= -:doctype: manpage -:man source: Lsyncd -:man manual: Lsyncd -:man version: 2.0.7 -:date: April 2012 - -NAME ----- -lsyncd - a daemon to continuously synchronize directory trees - -SYNOPSIS --------- -config file::: -{nbsp} *lsyncd* ['OPTIONS'] 'CONFIG-FILE' - -default rsync behaviour::: -{nbsp} *lsyncd* ['OPTIONS'] -rsync 'SOURCEDIR' 'TARGET' ... - -default rync+ssh behaviour (moves and deletes through ssh)::: -{nbsp} *lsyncd* ['OPTIONS'] -rsyncssh 'SOURCEDIR' 'TARGETHOST' 'TARGETDIR' ... - -default direct behaviour (local file operations/rsync)::: -{nbsp} *lsyncd* ['OPTIONS'] -direct 'SOURCEDIR' 'TARGETDIR' ... - -DESCRIPTION ------------- -Lsyncd(1) watches local directory trees through an event monitor interface -(inotify, fsevents). It aggregates and combines events for a few seconds and -then spawns one or more processes to synchronize the changes. By default this -is rsync(1). Lsyncd is thus a light-weight asynchronous live mirror solution -that is comparatively easy to install not requiring new filesystems or -block devices and does not hamper local filesystem performance. - -Rsync+ssh is an advanced action configuration that uses a SSH(1) to act file -and directory moves directly on the target instead of re-transmitting the move -destination over the wire. - -Fine-grained customization can be achieved through the CONFIG-FILE. Custom -action configs can even be written from scratch in cascading layers ranging -from shell scripts to code written in the LUA(1) language. This way simplicity -can be balanced with powerfulness. See the online manual for details on the -CONFIG-FILE https://github.com/axkibe/lsyncd/wiki/Manual-to-Lsyncd-2.0.x . - -Note that under normal configuration Lsyncd will delete pre-existing files in -the target directories that are not present in the respective source directory. - -OPTIONS -------- -*-delay* 'SECS':: - Overrides the default delay times. - -*-help*:: - Show a help message. - -*-insist*:: - Continues start up even if rsync cannot connect. - -*-log* 'LEVEL':: - Controls which kind of events are logged. By default Lsyncd logs 'Normal' - and 'Error' Messages. *-log scarce* will make Lsyncd log Error messages - only. *-log all* will log all debug messages. - -*-log* 'Category':: - Turns on a specific debug message. E.g. *-log Exec* will log - all processes as they are spawned. - -*-nodaemon*:: - Lsyncd will not detach from the invoker and log as well to stdout/err. - -*-pidfile* 'FILE':: - Lsyncd will write its process ID in 'FILE'. - -*-runner* 'FILE':: - Makes the Lsyncd core load the part of Lsyncd written in Lua from 'FILE'. - -*-version*:: - Writes version information and exits. - -EXIT STATUS ------------ -*0*:: - Terminated on a TERM signal(7) - -*-1*:: - Failure (syntax, unrecoverable error condition, internal failure) - -SEE ALSO --------- -Online Manual: https://github.com/axkibe/lsyncd/wiki/Lsyncd-2.1.x-%E2%80%96-What's-New%3F - -VERSION ------- -This man page is for lsyncd(1) version 2.0.7 - -AUTHOR ------- -Axel Kittenberger, 2010-2012 - -COPYING -------- -Copyright \(C) 2010-2012 Axel Kittenberger. Free use of this software is granted -under the terms of the GNU General Public License (GPL) version 2, or any later -version. Free redistrubition of this Documentation (/doc directory) is granted -under the terms of the Creative Commons 3.0 Attribution License (CC-3.0-BY). - diff -Nru lsyncd-2.1.6/doc/manpage/lsyncd.1 lsyncd-2.2.3/doc/manpage/lsyncd.1 --- lsyncd-2.1.6/doc/manpage/lsyncd.1 1970-01-01 00:00:00.000000000 +0000 +++ lsyncd-2.2.3/doc/manpage/lsyncd.1 2018-03-09 12:39:11.000000000 +0000 @@ -0,0 +1,161 @@ +'\" t +.\" Title: lsyncd +.\" Author: [see the "AUTHOR" section] +.\" Generator: DocBook XSL Stylesheets v1.79.1 +.\" Date: January 2017 +.\" Manual: Lsyncd +.\" Source: Lsyncd 2.2.1 +.\" Language: English +.\" +.TH "LSYNCD" "1" "January 2017" "Lsyncd 2\&.2\&.1" "Lsyncd" +.\" ----------------------------------------------------------------- +.\" * Define some portability stuff +.\" ----------------------------------------------------------------- +.\" ~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~ +.\" http://bugs.debian.org/507673 +.\" http://lists.gnu.org/archive/html/groff/2009-02/msg00013.html +.\" ~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~ +.ie \n(.g .ds Aq \(aq +.el .ds Aq ' +.\" ----------------------------------------------------------------- +.\" * set default formatting +.\" ----------------------------------------------------------------- +.\" disable hyphenation +.nh +.\" disable justification (adjust text to left margin only) +.ad l +.\" ----------------------------------------------------------------- +.\" * MAIN CONTENT STARTS HERE * +.\" ----------------------------------------------------------------- +.SH "NAME" +lsyncd \- a daemon to continuously synchronize directory trees +.SH "SYNOPSIS" +.PP +config file +.RS 4 +\ \& +\fBlsyncd\fR +[\fIOPTIONS\fR] +\fICONFIG\-FILE\fR +.RE +.PP +default rsync behaviour +.RS 4 +\ \& +\fBlsyncd\fR +[\fIOPTIONS\fR] \-rsync +\fISOURCEDIR\fR +\fITARGET\fR +\&... +.RE +.PP +default rync+ssh behaviour (moves and deletes through ssh) +.RS 4 +\ \& +\fBlsyncd\fR +[\fIOPTIONS\fR] \-rsyncssh +\fISOURCEDIR\fR +\fITARGETHOST\fR +\fITARGETDIR\fR +\&... +.RE +.PP +default direct behaviour (local file operations/rsync) +.RS 4 +\ \& +\fBlsyncd\fR +[\fIOPTIONS\fR] \-direct +\fISOURCEDIR\fR +\fITARGETDIR\fR +\&... +.RE +.SH "DESCRIPTION" +.sp +Lsyncd(1) watches local directory trees through an event monitor interface (inotify, fsevents)\&. It aggregates and combines events for a few seconds and then spawns one or more processes to synchronize the changes\&. By default this is rsync(1)\&. Lsyncd is thus a light\-weight asynchronous live mirror solution that is comparatively easy to install not requiring new filesystems or block devices and does not hamper local filesystem performance\&. +.sp +Rsync+ssh is an advanced action configuration that uses a SSH(1) to act file and directory moves directly on the target instead of re\-transmitting the move destination over the wire\&. +.sp +Fine\-grained customization can be achieved through the CONFIG\-FILE\&. Custom action configs can even be written from scratch in cascading layers ranging from shell scripts to code written in the LUA(1) language\&. This way simplicity can be balanced with powerfulness\&. See the online manual for details on the CONFIG\-FILE https://axkibe\&.github\&.io/lsyncd/manual/config/file/ \&. +.sp +Note that under normal configuration Lsyncd will delete pre\-existing files in the target directories that are not present in the respective source directory\&. +.SH "OPTIONS" +.PP +\fB\-delay\fR \fISECS\fR +.RS 4 +Overrides the default delay times\&. +.RE +.PP +\fB\-help\fR +.RS 4 +Show a help message\&. +.RE +.PP +\fB\-insist\fR +.RS 4 +Continues start up even if rsync cannot connect\&. +.RE +.PP +\fB\-log\fR \fILEVEL\fR +.RS 4 +Controls which kind of events are logged\&. By default Lsyncd logs +\fINormal\fR +and +\fIError\fR +Messages\&. +\fB\-log scarce\fR +will make Lsyncd log Error messages only\&. +\fB\-log all\fR +will log all debug messages\&. +.RE +.PP +\fB\-log\fR \fICategory\fR +.RS 4 +Turns on a specific debug message\&. E\&.g\&. +\fB\-log Exec\fR +will log all processes as they are spawned\&. +.RE +.PP +\fB\-nodaemon\fR +.RS 4 +Lsyncd will not detach from the invoker and log as well to stdout/err\&. +.RE +.PP +\fB\-pidfile\fR \fIFILE\fR +.RS 4 +Lsyncd will write its process ID in +\fIFILE\fR\&. +.RE +.PP +\fB\-runner\fR \fIFILE\fR +.RS 4 +Makes the Lsyncd core load the part of Lsyncd written in Lua from +\fIFILE\fR\&. +.RE +.PP +\fB\-version\fR +.RS 4 +Writes version information and exits\&. +.RE +.SH "EXIT STATUS" +.PP +\fB(128+SIGNUM)\fR +.RS 4 +Terminated by Signal (143 by TERM) +.RE +.PP +\fB\-1\fR +.RS 4 +Failure (syntax, unrecoverable error condition, internal failure) +.RE +.SH "SEE ALSO" +.sp +Online Manual: https://axkibe\&.github\&.io/lsyncd/ +.SH "VERSION" +.sp +This man page is for lsyncd(1) version 2\&.2\&.0 +.SH "AUTHOR" +.sp +Axel Kittenberger, 2010\-2017 +.SH "COPYING" +.sp +Copyright (C) 2010\-2017 Axel Kittenberger\&. Free use of this software is granted under the terms of the GNU General Public License (GPL) version 2, or any later version\&. Free redistrubition of this Documentation (/doc directory) is granted under the terms of the Creative Commons 3\&.0 Attribution License (CC\-3\&.0\-BY)\&. diff -Nru lsyncd-2.1.6/doc/manpage/lsyncd.1.txt lsyncd-2.2.3/doc/manpage/lsyncd.1.txt --- lsyncd-2.1.6/doc/manpage/lsyncd.1.txt 1970-01-01 00:00:00.000000000 +0000 +++ lsyncd-2.2.3/doc/manpage/lsyncd.1.txt 2018-03-09 12:39:11.000000000 +0000 @@ -0,0 +1,108 @@ +lsyncd(1) +========= +:doctype: manpage +:man source: Lsyncd +:man manual: Lsyncd +:man version: 2.2.1 +:date: January 2017 + +NAME +---- +lsyncd - a daemon to continuously synchronize directory trees + +SYNOPSIS +-------- +config file::: +{nbsp} *lsyncd* ['OPTIONS'] 'CONFIG-FILE' + +default rsync behaviour::: +{nbsp} *lsyncd* ['OPTIONS'] -rsync 'SOURCEDIR' 'TARGET' ... + +default rync+ssh behaviour (moves and deletes through ssh)::: +{nbsp} *lsyncd* ['OPTIONS'] -rsyncssh 'SOURCEDIR' 'TARGETHOST' 'TARGETDIR' ... + +default direct behaviour (local file operations/rsync)::: +{nbsp} *lsyncd* ['OPTIONS'] -direct 'SOURCEDIR' 'TARGETDIR' ... + +DESCRIPTION +------------ +Lsyncd(1) watches local directory trees through an event monitor interface +(inotify, fsevents). It aggregates and combines events for a few seconds and +then spawns one or more processes to synchronize the changes. By default this +is rsync(1). Lsyncd is thus a light-weight asynchronous live mirror solution +that is comparatively easy to install not requiring new filesystems or +block devices and does not hamper local filesystem performance. + +Rsync+ssh is an advanced action configuration that uses a SSH(1) to act file +and directory moves directly on the target instead of re-transmitting the move +destination over the wire. + +Fine-grained customization can be achieved through the CONFIG-FILE. Custom +action configs can even be written from scratch in cascading layers ranging +from shell scripts to code written in the LUA(1) language. This way simplicity +can be balanced with powerfulness. See the online manual for details on the +CONFIG-FILE https://axkibe.github.io/lsyncd/manual/config/file/ . + +Note that under normal configuration Lsyncd will delete pre-existing files in +the target directories that are not present in the respective source directory. + +OPTIONS +------- +*-delay* 'SECS':: + Overrides the default delay times. + +*-help*:: + Show a help message. + +*-insist*:: + Continues start up even if rsync cannot connect. + +*-log* 'LEVEL':: + Controls which kind of events are logged. By default Lsyncd logs 'Normal' + and 'Error' Messages. *-log scarce* will make Lsyncd log Error messages + only. *-log all* will log all debug messages. + +*-log* 'Category':: + Turns on a specific debug message. E.g. *-log Exec* will log + all processes as they are spawned. + +*-nodaemon*:: + Lsyncd will not detach from the invoker and log as well to stdout/err. + +*-pidfile* 'FILE':: + Lsyncd will write its process ID in 'FILE'. + +*-runner* 'FILE':: + Makes the Lsyncd core load the part of Lsyncd written in Lua from 'FILE'. + +*-version*:: + Writes version information and exits. + +EXIT STATUS +----------- +*(128+SIGNUM)*:: + Terminated by Signal (143 by TERM) + +*-1*:: + Failure (syntax, unrecoverable error condition, internal failure) + + +SEE ALSO +-------- +Online Manual: https://axkibe.github.io/lsyncd/ + +VERSION +------ +This man page is for lsyncd(1) version 2.2.0 + +AUTHOR +------ +Axel Kittenberger, 2010-2017 + +COPYING +------- +Copyright \(C) 2010-2017 Axel Kittenberger. Free use of this software is granted +under the terms of the GNU General Public License (GPL) version 2, or any later +version. Free redistrubition of this Documentation (/doc directory) is granted +under the terms of the Creative Commons 3.0 Attribution License (CC-3.0-BY). + diff -Nru lsyncd-2.1.6/examples/lbash.lua lsyncd-2.2.3/examples/lbash.lua --- lsyncd-2.1.6/examples/lbash.lua 2015-10-15 06:29:47.000000000 +0000 +++ lsyncd-2.2.3/examples/lbash.lua 2018-03-09 12:39:11.000000000 +0000 @@ -4,7 +4,7 @@ -- This example uses local bash commands to keep two local -- directory trees in sync. -- -settings = { +settings { logfile = "/tmp/lsyncd.log", statusFile = "/tmp/lsyncd.stat", statusIntervall = 1, diff -Nru lsyncd-2.1.6/examples/lpostcmd.lua lsyncd-2.2.3/examples/lpostcmd.lua --- lsyncd-2.1.6/examples/lpostcmd.lua 2015-10-15 06:29:47.000000000 +0000 +++ lsyncd-2.2.3/examples/lpostcmd.lua 2018-03-09 12:39:11.000000000 +0000 @@ -24,24 +24,30 @@ maxProcesses = 1, -- called whenever something is to be done - action = function(inlet) - local event = inlet.getEvent() - local config = inlet.getConfig() + action = function + ( + inlet + ) + local event = inlet.getEvent( ) + local config = inlet.getConfig( ) -- if the event is a blanket event and not the startup, -- its there to spawn the webservice restart at the target. - if event.etype == "Blanket" then + if event.etype == 'Blanket' + then -- uses rawget to test if "isPostcmd" has been set without -- triggering an error if not. - local isPostcmd = rawget(event, "isPostcmd") - if isPostcmd then + local isPostcmd = rawget( event, 'isPostcmd' ) + + if isPostcmd + then spawn(event, "/usr/bin/ssh", config.host, config.postcmd) - return + return else - -- this is the startup, forwards it to default routine. - return default.rsync.action(inlet) - end - error("this should never be reached") + -- this is the startup, forwards it to default routine. + return default.rsync.action(inlet) + end + error( 'this should never be reached' ) end -- for any other event, a blanket event is created that -- will stack on the queue and do the postcmd when its finished @@ -53,33 +59,49 @@ -- called when a process exited. -- this can be a rsync command, the startup rsync or the postcmd - collect = function(agent, exitcode) + collect = function + ( + agent, + exitcode + ) -- for the ssh commands 255 is network error -> try again - local isPostcmd = rawget(agent, "isPostcmd") - if not agent.isList and agent.etype == "Blanket" and isPostcmd then - if exitcode == 255 then - return "again" - end + local isPostcmd = rawget( agent, 'isPostcmd' ) + + if not agent.isList and agent.etype == "Blanket" and isPostcmd + then + if exitcode == 255 then return 'again' end + return else --- everything else, forward to default collection handler - return default.collect(agent,exitcode) + return default.collect( agent,exitcode ) end - error("this should never be reached") + error( 'this should never be reached' ) end, -- called before anything else -- builds the target from host and targetdir - prepare = function(config, level, skipTarget) - if not config.host then - error("rsyncpostcmd neets 'host' configured", 4) + prepare = function + ( + config, + level, + skipTarget + ) + if not config.host + then + error( 'rsyncpostcmd needs "host" configured', 4 ) end - if not config.targetdir then - error("rsyncpostcmd needs 'targetdir' configured", 4) + + if not config.targetdir + then + error( 'rsyncpostcmd needs "targetdir" configured', 4) end - if not config.target then + + if not config.target + then config.target = config.host .. ":" .. config.targetdir end + return default.rsync.prepare(config, level, skipTarget) end } @@ -87,9 +109,10 @@ sync { rsyncpostcmd, - source = "src", - host = "beetle", - targetdir = "/path/to/trg", - postcmd = "/usr/local/bin/restart-servelt.sh", + delay = 3, + source = '/path/to/src', + host = 'localhost', + targetdir = '/path/to/trg', + postcmd = '/usr/local/bin/dopostcmd', } diff -Nru lsyncd-2.1.6/examples/lrsync.lua lsyncd-2.2.3/examples/lrsync.lua --- lsyncd-2.1.6/examples/lrsync.lua 2015-10-15 06:29:47.000000000 +0000 +++ lsyncd-2.2.3/examples/lrsync.lua 2018-03-09 12:39:11.000000000 +0000 @@ -3,7 +3,7 @@ -- -- Simple example for default rsync. -- -settings = { +settings { statusFile = "/tmp/lsyncd.stat", statusInterval = 1, } diff -Nru lsyncd-2.1.6/examples/ls3.lua lsyncd-2.2.3/examples/ls3.lua --- lsyncd-2.1.6/examples/ls3.lua 1970-01-01 00:00:00.000000000 +0000 +++ lsyncd-2.2.3/examples/ls3.lua 2018-03-09 12:39:11.000000000 +0000 @@ -0,0 +1,222 @@ +---- +-- Example lsyncd configuration for syncing with an Amazon S3 bucket +-- +-- This requires the official AWS CLI to be available, and that credentials +-- bet set up through some external method, such as environment variables, +-- IAM profiles or the AWS SDK configuration. +-- +-- The AWS CLI sync exclude rules are not as powerful as the ones supported by +-- lsyncd. Hence, some of the do not translate perfectly. For example, '*' +-- (asterisk) matches slashes, while it does not in lsyncd. Hence it is a good +-- idea to only use exclude patterns for full directories, either by using a +-- trailing / (slash) or ** (double asterisk), as those will be correctly +-- translated. +-- +-- An initialSync options is provided as a convenience, since it's not easy to +-- make sure exclusion rules match when doing it manually. It will *pull* from +-- the target bucket to the local dir (the opposite of the regular behavior) +-- then exit immediately. +-- +-- Author: Daniel Miranda +-- +s3 = {} + +s3.checkgauge = { + onCreate = false, + onModify = false, + onDelete = false, + onStartup = false, + onMove = false, + + delete = true, + exclude = true, + excludeFrom = true, + target = true, + + s3 = { + -- Path to the AWS CLI binary + awscliBinary = true, + -- Extra options to pass to the AWS CLI (as a list) + awscliOptions = true, + -- Whether to do a dry-run, and not make any real changes + dryrun = true, + -- Do an initial pull from the bucket and exit immediately. + initialSync = true + } +} + +-- Generate a list of exclude flags for the AWS CLI based on the lsyncd +-- patterns provided. Cache it to avoid re-generating it every time. + +local s3Excludes = function(config, excludes) + if config.s3._excludes == nil then + config.s3._excludes = {} + for _, pat in ipairs(excludes) do + pat = pat:gsub('%*%*', '[[ANY]]') + pat = pat:gsub('%?', '[[ANY_BUT_SLASH_ONCE]]') + pat = pat:gsub('/$', '/*') + pat = pat:gsub('%[%[ANY%]%]', '*') + pat = pat:gsub('%[%[ANY_BUT_SLASH_ONCE%]%]', '[^/]') + + if pat:match('^/') then + pat = pat:sub(2, -1) + else + pat = '*/' .. pat + end + + table.insert(config.s3._excludes, '--exclude') + table.insert(config.s3._excludes, pat) + end + + log('s3Excludes', table.concat(config.s3._excludes, '\n')) + end + + return config.s3._excludes +end + +-- Generates a command line to call the AWS CLI as configured, with the provided +-- S3 action (such as cp, mv, rm or sync). +-- Returns a tuple of (binaryPath, arguments) +local awscliCommand = function(verb, config) + local bin = config.s3.awscliBinary + local args = {'s3', verb, '--only-show-errors'} + if config.s3.dryrun then + table.insert(args, '--dryrun') + end + + if verb == 'sync' + and (config.delete == true or config.delete == 'startup') + then + table.insert(args, '--delete') + end + + for _, opt in ipairs(config.s3.awscliOptions) do + table.insert(args, opt) + end + + return bin, args +end + +s3.action = function(inlet) + local event, event2 = inlet.getEvent() + -- S3 never actually deals with directories - they are just an illusion + -- created based on the common prefixes of objects. Hence discard any events + -- that do not concern files. + if event.isdir then + inlet.discardEvent(event) + return + end + + local config = inlet.getConfig() + if event.etype == 'Create' or event.etype == 'Modify' then + local bin, args = awscliCommand('cp', config) + spawn( + event, + bin, + args, + event.sourcePath, + event.targetPath + ) + elseif event.etype == 'Delete' then + if config.delete ~= true and config.delete ~= 'running' then + inlet.discardEvent(event) + return + end + + local bin, args = awscliCommand('rm', config) + spawn( + event, + bin, + args, + event.targetPath + ) + elseif event.etype == 'Move' then + local bin, args = awscliCommand('mv', config) + spawn( + event, + bin, + args, + event.targetPath, + event2.targetPath + ) + else + log('Warn', 'ignored an event of type "', event.etype, '"') + inlet.discardEvent(event) + end +end + +s3.init = function(event) + local config = event.config + local inlet = event.inlet + local excludes = s3Excludes(config, inlet.getExcludes()) + local bin, args = awscliCommand('sync', config) + + -- Do a pull when initialSync is enabled. + if config.s3.initialSync then + spawn( + event, + bin, + args, + excludes, + config.target, + event.sourcePath + ) + -- And a push, as usual, otherwise + else + spawn( + event, + bin, + args, + excludes, + event.sourcePath, + config.target + ) + end +end + +-- Define a collect callback so we can terminate immediately when initialSync +-- is enabled +s3.collect = function(agent, exitcode) + local config = agent.config + if not agent.isList and agent.etype == 'Init' and config.s3.initialSync then + terminate(exitcode == 0 and 0 or -1) + end + + return +end + +s3.prepare = function(config, level) + default.prepare(config, level + 1) + + config.target = config.target:gsub('/+$', '') + if not config.target:match('^s3://') then + config.target = 's3://' .. config.target + end +end + +s3.s3 = { + awscliBinary = '/usr/bin/aws', + awscliOptions = {}, + dryrun = false, + initialSync = false +} +s3.delete = false +s3.delay = 10 +s3.maxProcesses = 1 + +sync { + s3, + source = '/my/dir', + target = 's3://my-bucket/my-path', + delay = 30, + delete = true, + maxProcesses = 2, + exclude = { + '/sub/folder/', + }, + s3 = { + awscliBinary = '/usr/local/bin/aws', + awscliOptions = {'--acl', 'public-read'}, + dryrun = false + } +} diff -Nru lsyncd-2.1.6/inotify.c lsyncd-2.2.3/inotify.c --- lsyncd-2.1.6/inotify.c 2015-10-15 06:29:47.000000000 +0000 +++ lsyncd-2.2.3/inotify.c 2018-03-09 12:39:11.000000000 +0000 @@ -124,9 +124,9 @@ // kernel call to create the inotify watch int wd = inotify_add_watch( inotify_fd, path, mask ); - if (wd < 0) + if( wd < 0 ) { - if (errno == ENOSPC) + if( errno == ENOSPC ) { printlogf( L, "Error", @@ -154,18 +154,18 @@ /* - * Removes an inotify watch. - * - * param dir (Lua stack) numeric watch descriptor - * - * return nil - */ +* Removes an inotify watch. +* +* param dir (Lua stack) numeric watch descriptor +* +* return nil +*/ static int -l_rmwatch(lua_State *L) +l_rmwatch( lua_State *L ) { - int wd = luaL_checkinteger(L, 1); - inotify_rm_watch(inotify_fd, wd); - printlogf(L, "Inotify", "rmwatch()<-%d", wd); + int wd = luaL_checkinteger( L, 1 ); + inotify_rm_watch( inotify_fd, wd ); + printlogf( L, "Inotify", "rmwatch()<-%d", wd ); return 0; } @@ -173,7 +173,8 @@ /* | Lsyncd's core's inotify functions. */ -static const luaL_Reg linotfylib[] = { +static const luaL_Reg linotfylib[ ] = +{ { "addwatch", l_addwatch }, { "rmwatch", l_rmwatch }, { NULL, NULL} @@ -215,6 +216,7 @@ // used to execute two events in case of unmatched MOVE_FROM buffer struct inotify_event *after_buf = NULL; + if( event && ( IN_Q_OVERFLOW & event->mask ) ) { // and overflow happened, tells the runner @@ -224,8 +226,11 @@ { exit( -1 ); } + lua_pop( L, 1 ); + hup = 1; + return; } @@ -251,10 +256,10 @@ move_event = false; } else if( - move_event && - ( - !( IN_MOVED_TO & event->mask ) || - event->cookie != move_event_buf->cookie + move_event + && ( + !( IN_MOVED_TO & event->mask ) + || event->cookie != move_event_buf->cookie ) ) { @@ -263,18 +268,21 @@ logstring( "Inotify", "icore, changing unary MOVE_FROM into DELETE" - ) + ); + after_buf = event; + event = move_event_buf; + event_type = "Delete"; + move_event = false; } else if( - move_event && - ( - IN_MOVED_TO & event->mask ) && - event->cookie == move_event_buf->cookie - ) + move_event + && ( IN_MOVED_TO & event->mask ) + && event->cookie == move_event_buf->cookie + ) { // this is indeed a matched move */ event_type = "Move"; @@ -290,10 +298,14 @@ if( move_event_buf_size < el ) { move_event_buf_size = el; + move_event_buf = s_realloc( move_event_buf, el ); } + memcpy( move_event_buf, event, el ); + move_event = true; + return; } @@ -346,6 +358,7 @@ } lua_pushstring( L, event_type ); + if( event_type != MOVE ) { lua_pushnumber( L, event->wd ); @@ -390,6 +403,7 @@ | buffer to read inotify events into */ static size_t readbuf_size = 2048; + static char * readbuf = NULL; @@ -495,7 +509,7 @@ extern void register_inotify( lua_State *L ) { - luaL_register( L, LSYNCD_INOTIFYLIBNAME, linotfylib ); + lua_compat_register( L, LSYNCD_INOTIFYLIBNAME, linotfylib ); } @@ -511,11 +525,14 @@ "Error", "internal failure: inotify_fd != ob->fd" ); + exit( -1 ); } close( inotify_fd ); + free( readbuf ); + readbuf = NULL; } diff -Nru lsyncd-2.1.6/INSTALL lsyncd-2.2.3/INSTALL --- lsyncd-2.1.6/INSTALL 2015-10-15 06:29:47.000000000 +0000 +++ lsyncd-2.2.3/INSTALL 2018-03-09 12:39:11.000000000 +0000 @@ -15,22 +15,22 @@ Lua For building Lsyncd the Lua interpreter 'lua' - and the Lua compiler 'luac' are needed. - They aren't needed in the deployed binary tough. - - Use Lua 5.1 or Lua 5.2 at your choice. + and the Lua compiler 'luac' are needed. + They aren't needed in the deployed binary though. + + Use Lua 5.2 or later. Liblua The lua library. - Note that you likely need the package "liblua-dev" - or something like that. + Note that you likely need the package "liblua-dev" + or something like that. - Use Lua 5.1 or Lua 5.2 at your choice. + Use Lua 5.2 or later. - Note, this has to be exactly the same Version as the - lua compiler used above! + Note, this has to be exactly the same Version as the + lua compiler used above! Building @@ -41,16 +41,16 @@ mkdir build cd build cmake .. - make + make sudo make install Building intree: cmake . - make + make On OSX you yet need to get the xnu sources. For example: cmake -DWITH_INOTIFY=OFF -DWITH_FSEVENTS=ON -DXNU_DIR=/path/to/xnu-VERSION - make + make FIXME make install not yet done diff -Nru lsyncd-2.1.6/lsyncd.c lsyncd-2.2.3/lsyncd.c --- lsyncd-2.1.6/lsyncd.c 2015-10-15 06:29:47.000000000 +0000 +++ lsyncd-2.2.3/lsyncd.c 2018-03-09 12:39:11.000000000 +0000 @@ -80,6 +80,7 @@ NULL, }; + /** | Configuration parameters that matter to the core */ @@ -92,16 +93,19 @@ .nodaemon = false, }; + /* | True when Lsyncd daemonized itself. */ static bool is_daemon = false; + /* | The config file loaded by Lsyncd. */ char * lsyncd_config_file = NULL; + /* | False after first time Lsyncd started up. | @@ -114,6 +118,7 @@ */ static bool first_time = true; + /* | Set by TERM or HUP signal handler | telling Lsyncd should end or reset ASAP. @@ -123,11 +128,13 @@ volatile sig_atomic_t sigcode = 0; int pidfile_fd = 0; + /* | The kernel's clock ticks per second. */ static long clocks_per_sec; + /** * signal handler */ @@ -136,6 +143,7 @@ // nothing } + /** * signal handler */ @@ -156,6 +164,7 @@ } } + /* | Non glibc builds need a real tms structure for the times( ) call */ @@ -166,6 +175,7 @@ static struct tms * dummy_tms = &_dummy_tms; #endif + /* | Returns the absolute path of a path. | @@ -191,7 +201,6 @@ } - /*~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~* ( Logging ) *~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~*/ @@ -256,18 +265,22 @@ if( !strcmp( "all", name ) ) { settings.log_level = 99; + return true; } if( !strcmp( "scarce", name ) ) { settings.log_level = LOG_WARNING; + return true; } // categories must start with a capital letter. if( name[ 0 ] < 'A' || name[ 0 ] > 'Z' ) - { return false; } + { + return false; + } if( !logcats[ name[ 0 ]- 'A' ] ) { @@ -344,6 +357,7 @@ char ct[ 255 ]; // gets current timestamp hour:minute:second time_t mtime; + time( &mtime ); strftime( ct, sizeof( ct ), "%T", localtime( &mtime ) ); @@ -358,18 +372,21 @@ } // writes to file if configured so - if (settings.log_file) + if( settings.log_file ) { FILE * flog = fopen( settings.log_file, "a" ); + char * ct; + time_t mtime; // gets current timestamp day-time-year time( &mtime ); + ct = ctime( &mtime ); // cuts trailing linefeed - ct[ strlen( ct ) - 1] = 0; + ct[ strlen( ct ) - 1] = 0; if( flog == NULL ) { @@ -378,6 +395,7 @@ "Cannot open logfile [%s]!\n", settings.log_file ); + exit( -1 ); } @@ -420,7 +438,6 @@ } - /*~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~* ( Simple memory management ) *~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~*/ @@ -596,6 +613,7 @@ */ static int runner; + /* | Dummy variable of which it's address is used as | the cores index n the lua registry to @@ -612,9 +630,9 @@ { int flags; - flags = fcntl( fd, F_GETFD ); + flags = fcntl( fd, F_GETFD ); - if( flags == -1 ) + if( flags == -1 ) { logstring( "Error", "cannot get descriptor flags!" ); exit( -1 ); @@ -638,9 +656,9 @@ { int flags; - flags = fcntl( fd, F_GETFL ); + flags = fcntl( fd, F_GETFL ); - if( flags == -1 ) + if( flags == -1 ) { logstring( "Error", "cannot get status flags!" ); exit( -1 ); @@ -659,7 +677,8 @@ | Writes a pid file. */ static void -write_pidfile( +write_pidfile +( lua_State *L, const char *pidfile ) @@ -952,12 +971,13 @@ } -/*~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~* -( Library calls for the runner ) - *~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~*/ +/******************************. +* Library calls for the runner * +'******************************/ -static void daemonize( lua_State *L ); +static void daemonize( lua_State *L, const char *pidfile ); + int l_stackdump( lua_State* L ); @@ -981,7 +1001,9 @@ // skips filtered messages if( priority > settings.log_level ) - { return 0; } + { + return 0; + } // replaces non string values { @@ -1212,9 +1234,14 @@ // prepares the arguments argv = s_calloc( argc + 2, sizeof( char * ) ); + argv[ 0 ] = binary; + for( i = 1; i <= argc; i++ ) - { argv[i] = luaL_checkstring( L, i + li ); } + { + argv[i] = luaL_checkstring( L, i + li ); + } + argv[ i ] = NULL; // the fork! @@ -1224,7 +1251,9 @@ { // replaces stdin for pipes if( pipe_text ) - { dup2( pipefd[ 0 ], STDIN_FILENO ); } + { + dup2( pipefd[ 0 ], STDIN_FILENO ); + } // if lsyncd runs as a daemon and has a logfile it will redirect // stdout/stderr of child processes to the logfile. @@ -1325,36 +1354,56 @@ const char *rdir = luaL_checkstring(L, 1); char *adir = get_realpath(rdir); - if (!adir) { - printlogf(L, "Error", "failure getting absolute path of [%s]", rdir); + if( !adir ) + { + printlogf( + L, "Error", + "failure getting absolute path of [%s]", + rdir + ); + return 0; } { // makes sure its a directory struct stat st; - if (stat(adir, &st)) { - printlogf(L, "Error", - "cannot get absolute path of dir '%s': %s", rdir, strerror(errno)); - free(adir); + if( stat( adir, &st ) ) + { + printlogf( + L, "Error", + "cannot get absolute path of dir '%s': %s", + rdir, + strerror( errno ) + ); + + free( adir ); + return 0; } - if (!S_ISDIR(st.st_mode)) { - printlogf(L, "Error", - "cannot get absolute path of dir '%s': is not a directory", rdir); - free(adir); + if( !S_ISDIR( st.st_mode ) ) + { + printlogf( + L, "Error", + "cannot get absolute path of dir '%s': is not a directory", + rdir + ); + + free( adir ); + return 0; } } // returns absolute path with a concated '/' - luaL_buffinit(L, &b); - luaL_addstring(&b, adir); - luaL_addchar(&b, '/'); - luaL_pushresult(&b); + luaL_buffinit( L, &b ); + luaL_addstring( &b, adir ); + luaL_addchar( &b, '/' ); + luaL_pushresult( &b ); + + free( adir ); - free(adir); return 1; } @@ -1380,35 +1429,43 @@ switch( t ) { case LUA_TSTRING: + printlogf( L, "Debug", "%d string: '%s'", i, lua_tostring( L, i ) ); + break; case LUA_TBOOLEAN: + printlogf( L, "Debug", "%d boolean %s", i, lua_toboolean( L, i ) ? "true" : "false" ); + break; case LUA_TNUMBER: + printlogf( L, "Debug", "%d number: %g", i, lua_tonumber( L, i ) ); + break; default: + printlogf( L, "Debug", "%d %s", i, lua_typename( L, t ) ); + break; } } @@ -1428,12 +1485,14 @@ | values are boolean true on dirs. */ static int -l_readdir ( lua_State *L ) +l_readdir( lua_State *L ) { const char * dirname = luaL_checkstring( L, 1 ); + DIR *d; d = opendir( dirname ); + if( d == NULL ) { printlogf( @@ -1451,16 +1510,15 @@ struct dirent *de = readdir( d ); bool isdir; - if( de == NULL ) + if( de == NULL ) // finished { - // finished break; } // ignores . and .. if( - !strcmp( de->d_name, "." ) || - !strcmp( de->d_name, ".." ) + !strcmp( de->d_name, "." ) + || !strcmp( de->d_name, ".." ) ) { continue; @@ -1475,6 +1533,7 @@ strlen( de->d_name ) + 2 ); + struct stat st; strcpy( entry, dirname ); @@ -1500,6 +1559,7 @@ } closedir( d ); + return 1; } @@ -1514,7 +1574,7 @@ | */ int -l_terminate(lua_State *L) +l_terminate( lua_State *L ) { int exitcode = luaL_checkinteger( L, 1 ); @@ -1534,6 +1594,7 @@ l_configure( lua_State *L ) { const char * command = luaL_checkstring( L, 1 ); + if( !strcmp( command, "running" ) ) { // set by runner after first initialize @@ -1546,22 +1607,22 @@ settings.log_syslog = true; const char * log_ident = - settings.log_ident ? - settings.log_ident : - "lsyncd"; + settings.log_ident + ? settings.log_ident + : "lsyncd"; openlog( log_ident, 0, settings.log_facility ); } if( !settings.nodaemon && !is_daemon ) { - logstring( "Debug", "daemonizing now." ); - daemonize( L ); - } + logstring( "Normal", "--- Startup, daemonizing ---" ); - if( settings.pidfile ) + daemonize( L, settings.pidfile ); + } + else { - write_pidfile( L, settings.pidfile ); + logstring( "Normal", "--- Startup ---" ); } } @@ -1590,8 +1651,7 @@ free( settings.pidfile ); } - settings.pidfile = - s_strdup( file ); + settings.pidfile = s_strdup( file ); } else if( !strcmp( command, "logfacility" ) ) { @@ -1635,8 +1695,10 @@ { const char * ident = luaL_checkstring( L, 2 ); - if (settings.log_ident) - { free(settings.log_ident); } + if( settings.log_ident ) + { + free( settings.log_ident ); + } settings.log_ident = s_strdup( ident ); } @@ -1767,6 +1829,7 @@ return 0; } + /* | The Lsnycd's core library */ @@ -1785,6 +1848,7 @@ { NULL, NULL } }; + /* | Adds a number in seconds to a jiffy timestamp. */ @@ -1817,6 +1881,7 @@ } } + /* | Subracts two jiffy timestamps resulting in a number in seconds | or substracts a jiffy by a number in seconds resulting a jiffy timestamp. @@ -1849,6 +1914,7 @@ return 1; } + /* | Compares two jiffy timestamps */ @@ -1863,6 +1929,7 @@ return 1; } + /* * True if jiffy1 timestamp is eariler than jiffy2 timestamp */ @@ -1877,6 +1944,7 @@ return 1; } + /* | True if jiffy1 before or equals jiffy2 */ @@ -1897,7 +1965,7 @@ void register_lsyncd( lua_State *L ) { - luaL_register( L, LSYNCD_LIBNAME, lsyncdlib ); + lua_compat_register( L, LSYNCD_LIBNAME, lsyncdlib ); lua_setglobal( L, LSYNCD_LIBNAME ); // creates the metatable for the jiffies ( timestamps ) userdata @@ -1971,6 +2039,7 @@ lua_remove( L, -2 ); } + /* | Daemonizes. | @@ -1981,7 +2050,10 @@ | might actually close the monitors fd! */ static void -daemonize( lua_State *L ) +daemonize( + lua_State *L, // the lua state + const char *pidfile // if not NULL write pidfile +) { pid_t pid, sid; @@ -1998,14 +2070,20 @@ exit( -1 ); } - if (pid > 0) + if( pid > 0 ) { // parent process returns to shell exit( 0 ); } + if( pidfile ) + { + write_pidfile( L, pidfile ); + } + // detaches the new process from the parent process sid = setsid( ); + if( sid < 0 ) { printlogf( @@ -2338,6 +2416,7 @@ } } + /* | The effective main for one run. | @@ -2356,6 +2435,7 @@ // load Lua L = luaL_newstate( ); + luaL_openlibs( L ); { // checks the lua version @@ -2433,7 +2513,6 @@ // registers Lsycnd's core library register_lsyncd( L ); - if( check_logcat( "Debug" ) <= settings.log_level ) { // printlogf doesnt support %ld :-( @@ -2591,7 +2670,6 @@ } } - // checks if there is a "-help" or "--help" { int i; @@ -2719,15 +2797,11 @@ } #ifdef WITH_INOTIFY - open_inotify( L ); - #endif #ifdef WITH_FSEVENTS - open_fsevents( L ); - #endif // adds signal handlers @@ -2805,6 +2879,7 @@ return 0; } + /* | Main */ @@ -2814,6 +2889,9 @@ // gets a kernel parameter clocks_per_sec = sysconf( _SC_CLK_TCK ); + setlinebuf( stdout ); + setlinebuf( stderr ); + while( !term ) { main1( argc, argv ); } diff -Nru lsyncd-2.1.6/lsyncd.h lsyncd-2.2.3/lsyncd.h --- lsyncd-2.1.6/lsyncd.h 2015-10-15 06:29:47.000000000 +0000 +++ lsyncd-2.2.3/lsyncd.h 2018-03-09 12:39:11.000000000 +0000 @@ -12,11 +12,12 @@ #define LSYNCD_H // some older machines need this to see pselect -#define _BSD_SOURCE 1 +#define _DEFAULT_SOURCE 1 #define _XOPEN_SOURCE 700 #define _DARWIN_C_SOURCE 1 #define LUA_COMPAT_ALL +#define LUA_COMPAT_5_1 // includes needed for headerfile #include "config.h" @@ -31,6 +32,20 @@ #define LSYNCD_LIBNAME "lsyncd" #define LSYNCD_INOTIFYLIBNAME "inotify" +/* +| Workaround to register a library for different lua versions. +*/ +#if LUA_VERSION_NUM > 502 + #define lua_compat_register( L, name, lib ) \ + { \ + lua_newtable((L)); \ + luaL_setfuncs((L), (lib), 0); \ + } +#else + #define lua_compat_register( L, name, lib ) \ + {luaL_register( (L), (name), (lib) );} +#endif + /** * Lsyncd runtime configuration */ diff -Nru lsyncd-2.1.6/lsyncd.lua lsyncd-2.2.3/lsyncd.lua --- lsyncd-2.1.6/lsyncd.lua 2015-10-15 06:29:47.000000000 +0000 +++ lsyncd-2.2.3/lsyncd.lua 2018-03-09 12:39:11.000000000 +0000 @@ -12,34 +12,32 @@ -- --~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~ --- require('profiler') --- profiler.start() -- -- A security measurement. -- The core will exit if version ids mismatch. -- -if lsyncd_version then - +if lsyncd_version +then -- ensures the runner is not being loaded twice - lsyncd.log( - 'Error', - 'You cannot use the lsyncd runner as configuration file!' - ) + lsyncd.log( 'Error', 'You cannot use the lsyncd runner as configuration file!' ) lsyncd.terminate( -1 ) end -lsyncd_version = '2.1.6' +lsyncd_version = '2.2.3' + -- -- Hides the core interface from user scripts. -- local _l = lsyncd lsyncd = nil + local lsyncd = _l _l = nil + -- -- Shortcuts (which user is supposed to be able to use them as well) -- @@ -48,33 +46,58 @@ now = lsyncd.now readdir = lsyncd.readdir + -- --- Coping globals to ensure userscripts don't change this. +-- Coping globals to ensure userscripts cannot change this. -- local log = log local terminate = terminate local now = now +local readdir = readdir -- --- Predeclarations +-- Predeclarations. -- local Monitors + -- --- Global: total number of processess running +-- Global: total number of processess running. -- local processCount = 0 + +-- +-- All valid entries in a settings{} call. +-- +local settingsCheckgauge = +{ + logfile = true, + pidfile = true, + nodaemon = true, + statusFile = true, + statusInterval = true, + logfacility = true, + logident = true, + insist = true, + inotifyMode = true, + maxProcesses = true, + maxDelays = true, +} + + -- -- Settings specified by command line. -- local clSettings = { } + -- -- Settings specified by config scripts. -- local uSettings = { } + -- -- A copy of the settings function to see if the -- user script replaced the settings() by a table @@ -82,38 +105,64 @@ -- local settingsSafe + --============================================================================ -- Lsyncd Prototypes --============================================================================ + -- -- Array tables error if accessed with a non-number. -- -local Array = ( function( ) - - -- Metatable +local Array = ( function +( ) + -- + -- Metatable. + -- local mt = { } - -- on accessing a nil index. - mt.__index = function( t, k ) - if type(k) ~= 'number' then + -- + -- On accessing a nil index. + -- + mt.__index = function + ( + t, -- table accessed + k -- key value accessed + ) + if type(k) ~= 'number' + then error( 'Key "'..k..'" invalid for Array', 2 ) end + return rawget( t, k ) end - -- on assigning a new index. - mt.__newindex = function( t, k, v ) - if type( k ) ~= 'number' then + -- + -- On assigning a new index. + -- + mt.__newindex = function + ( + t, -- table getting a new index assigned + k, -- key value to assign to + v -- value to assign + ) + if type( k ) ~= 'number' + then error( 'Key "'..k..'" invalid for Array', 2 ) end + rawset( t, k, v ) end - -- creates a new object - local function new( ) + -- + -- Creates a new object + -- + local function new + ( ) local o = { } + setmetatable( o, mt ) + return o end @@ -121,7 +170,6 @@ -- Public interface -- return { new = new } - end )( ) @@ -132,8 +180,8 @@ -- since Lua's # operator does not work on tables whose key values are not -- strictly linear. -- -local CountArray = ( function( ) - +local CountArray = ( function +( ) -- -- Metatable -- @@ -147,228 +195,425 @@ -- -- On accessing a nil index. -- - mt.__index = function( t, k ) - if type( k ) ~= 'number' then - error( 'Key "'..k..'" invalid for CountArray', 2 ) + mt.__index = function + ( + t, -- table being accessed + k -- key used to access + ) + if type( k ) ~= 'number' + then + error( 'Key "' .. k .. '" invalid for CountArray', 2 ) end + return t[ k_nt ][ k ] end -- -- On assigning a new index. -- - mt.__newindex = function( t, k, v ) - - if type(k) ~= 'number' then + mt.__newindex = function + ( + t, -- table getting a new index assigned + k, -- key value to assign to + v -- value to assign + ) + if type( k ) ~= 'number' + then error( 'Key "'..k..'" invalid for CountArray', 2 ) end -- value before local vb = t[ k_nt ][ k ] - if v and not vb then + + if v and not vb + then t._size = t._size + 1 - elseif not v and vb then + elseif not v and vb + then t._size = t._size - 1 end + t[ k_nt ][ k ] = v end -- -- Walks through all entries in any order. -- - local function walk( self ) + local function walk + ( + self -- the count array + ) return pairs( self[ k_nt ] ) end -- - -- Returns the count + -- Returns the count. -- - local function size( self ) + local function size + ( + self -- the count array + ) return self._size end -- -- Creates a new count array -- - local function new( ) - - -- k_nt is native table, private for this object. - local o = { + local function new + ( ) + -- k_nt is a native table, private to this object. + local o = + { _size = 0, walk = walk, size = size, - [k_nt] = { } + [ k_nt ] = { } } - setmetatable(o, mt) + setmetatable( o, mt ) + return o end - -- -- Public interface -- return { new = new } end )( ) + -- --- A queue is optimized for pushing on the right and poping on the left. +-- A queue is optimized for pushing and poping. +-- TODO: make this an object -- -Queue = ( function( ) +Queue = ( function +( ) + -- + -- Metatable + -- + local mt = { } + -- - -- Creates a new queue. + -- Key to native table -- - local function new( ) - return { - first = 1, - last = 0, - size = 0 - }; + local k_nt = { } + + + -- + -- On accessing a nil index. + -- + mt.__index = function + ( + t, -- table being accessed + k -- key used to access + ) + if type( k ) ~= 'number' + then + error( 'Key "' .. k .. '" invalid for Queue', 2 ) + end + + return t[ k_nt ][ k ] end + + -- + -- On assigning a new index. + -- + mt.__newindex = function + ( + t, -- table getting a new index assigned + k, -- key value to assign to + v -- value to assign + ) + error( 'Queues are not directly assignable.', 2 ) + end + + -- + -- Returns the first item of the Queue. + -- + local function first + ( + self + ) + local nt = self[ k_nt ] + + return nt[ nt.first ] + end + + -- + -- Returns the last item of the Queue. + -- + local function last + ( + self + ) + local nt = self[ k_nt ] + + return nt[ nt.last ] + end + + -- + -- Returns the size of the queue. + -- + local function size + ( + self + ) + return self[ k_nt ].size + end + + -- -- Pushes a value on the queue. -- Returns the last value -- - local function push( list, value ) - - if not value then - error('Queue pushing nil value', 2) + local function push + ( + self, -- queue to push to + value -- value to push + ) + if not value + then + error( 'Queue pushing nil value', 2 ) end - local last = list.last + 1 - list.last = last - list[ last ] = value - list.size = list.size + 1 + local nt = self[ k_nt ] + + local last = nt.last + 1 + + nt.last = last + + nt[ last ] = value + + nt.size = nt.size + 1 + return last end + -- -- Removes an item at pos from the Queue. -- - local function remove( list, pos ) + local function remove + ( + self, -- the queue + pos -- position to remove + ) + local nt = self[ k_nt ] - if list[ pos ] == nil then - error('Removing nonexisting item in Queue', 2) + if nt[ pos ] == nil + then + error( 'Removing nonexisting item in Queue', 2 ) end - list[ pos ] = nil + nt[ pos ] = nil -- if removing first or last element, -- the queue limits are adjusted. - if pos == list.first then - - local last = list.last + if pos == nt.first + then + local last = nt.last - while list[ pos ] == nil and pos <= list.last do + while nt[ pos ] == nil and pos <= last + do pos = pos + 1 end - list.first = pos + nt.first = pos - elseif pos == list.last then + elseif pos == nt.last + then + local first = nt.first - while list[ pos ] == nil and pos >= list.first do + while nt[ pos ] == nil and pos >= first + do pos = pos - 1 end - list.last = pos - + nt.last = pos end -- reset the indizies if the queue is empty - if list.last < list.first then - list.first = 1 - list.last = 0 + if nt.last < nt.first + then + nt.first = 1 + + nt.last = 0 end - list.size = list.size - 1 + nt.size = nt.size - 1 end -- - -- Queue iterator (stateless) + -- Replaces a value. -- - local function iter( list, pos ) + local function replace + ( + self, -- the queue + pos, -- position to replace + value -- the new entry + ) + local nt = self[ k_nt ] + + if nt[ pos ] == nil + then + error( 'Trying to replace an unset Queue entry.' ) + end + + nt[ pos ] = value + end + + -- + -- Queue iterator ( stateless ) + -- TODO rename next + -- + local function iter + ( + self, -- queue to iterate + pos -- current position + ) + local nt = self[ k_nt ] pos = pos + 1 - while list[ pos ] == nil and pos <= list.last do + while nt[ pos ] == nil and pos <= nt.last + do pos = pos + 1 end - if pos > list.last then + if pos > nt.last + then return nil end - return pos, list[ pos ] + return pos, nt[ pos ] end + -- -- Reverse queue iterator (stateless) + -- TODO rename prev -- - local function iterReverse( list, pos ) + local function iterReverse + ( + self, -- queue to iterate + pos -- current position + ) + local nt = self[ k_nt ] pos = pos - 1 - while list[pos] == nil and pos >= list.first do + while nt[ pos ] == nil and pos >= nt.first + do pos = pos - 1 end - if pos < list.first then + if pos < nt.first + then return nil end - return pos, list[ pos ] + return pos, nt[ pos ] end + -- -- Iteraters through the queue -- returning all non-nil pos-value entries. -- - local function qpairs( list ) - return iter, list, list.first - 1 + local function qpairs + ( + self + ) + return iter, self, self[ k_nt ].first - 1 end + -- -- Iteraters backwards through the queue -- returning all non-nil pos-value entries. -- - local function qpairsReverse( list ) - return iterReverse, list, list.last + 1 + local function qpairsReverse + ( + self + ) + return iterReverse, self, self[ k_nt ].last + 1 end - return { - new = new, - push = push, - remove = remove, - qpairs = qpairs, - qpairsReverse = qpairsReverse - } + -- + -- Creates a new queue. + -- + local function new + ( ) + local q = { + first = first, + last = last, + push = push, + qpairs = qpairs, + qpairsReverse = qpairsReverse, + remove = remove, + replace = replace, + size = size, + + [ k_nt ] = + { + first = 1, + last = 0, + size = 0 + } + } + + setmetatable( q, mt ) + + return q + end + + -- + -- Public interface + -- + return { new = new } end )( ) + -- --- Locks globals, --- No more globals can be created after this +-- Locks globals. -- -local function lockGlobals( ) - +-- No more globals can be created after this! +-- +local function lockGlobals +( ) local t = _G + local mt = getmetatable( t ) or { } -- TODO try to remove the underscore exceptions - mt.__index = function( t, k ) - if k ~= '_' and string.sub(k, 1, 2) ~= '__' then - error( 'Access of non-existing global "'..k..'"', 2 ) + mt.__index = function + ( + t, -- table being accessed + k -- key used to access + ) + if k ~= '_' and string.sub( k, 1, 2 ) ~= '__' + then + error( 'Access of non-existing global "' .. k ..'"', 2 ) else rawget( t, k ) end end - mt.__newindex = function( t, k, v ) - if k ~= '_' and string.sub( k, 1, 2 ) ~= '__' then - error('Lsyncd does not allow GLOBALS to be created on the fly. '.. - 'Declare "'..k..'" local or declare global on load.', 2) + mt.__newindex = function + ( + t, -- table getting a new index assigned + k, -- key value to assign to + v -- value to assign + ) + if k ~= '_' and string.sub( k, 1, 2 ) ~= '__' + then + error( + 'Lsyncd does not allow GLOBALS to be created on the fly. ' + .. 'Declare "' .. k.. '" local or declare global on load.', + 2 + ) else rawset( t, k, v ) end @@ -377,163 +622,186 @@ setmetatable( t, mt ) end + -- -- Holds the information about a delayed event for one Sync. -- -local Delay = ( function( ) - - -- - -- Creates a new delay. +-- Valid stati of an delay are: +-- 'wait' ... the event is ready to be handled. +-- 'active' ... there is process running catering for this event. +-- 'blocked' ... this event waits for another to be handled first. +-- +local Delay = ( function +( ) -- - -- Params see below. + -- Metatable. -- - local function new( etype, sync, alarm, path, path2 ) - - local o = { - -- - -- Type of event. - -- Can be 'Create', 'Modify', 'Attrib', 'Delete' and 'Move' - -- - etype = etype, - - -- - -- the Sync this delay belongs to - -- - sync = sync, - - -- - -- Latest point in time this should be catered for. - -- This value is in kernel ticks, return of the C's - -- times(NULL) call. - alarm = alarm, - - -- - -- Path and filename or dirname of the delay relative - -- to the syncs root. - -- - -- for the directories it contains a trailing slash - -- - path = path, - - -- - -- Used only for Moves. - -- Path and file/dirname of a move destination. - -- - path2 = path2, - - -- - -- Status of the event. Valid stati are: - -- - -- 'wait' ... the event is ready to be handled. - -- - -- 'active' ... there is process running catering for this event. - -- - -- 'blocked' ... this event waits for another to be handled first. - -- - -- 'done' ... event has been collected. This should never be - -- visible as all references should be droped on - -- collection, nevertheless the seperate status is - -- used as insurrance everything is running correctly. - status = 'wait', - - -- - -- Position in the queue - -- - dpos = -1, - } - - return o - end - + local mt = { } -- - -- Public interface + -- Secret key to native table -- - return { new = new } - -end )( ) + local k_nt = { } --- --- Combines delays --- -local Combiner = ( function( ) + local assignAble = + { + dpos = true, + etype = true, + path = true, + path2 = true, + status = true, + } -- - -- The new delay is absorbed by an older one. + -- On accessing a nil index. -- - local function abso( d1, d2 ) - - log( - 'Delay', - d2.etype, ':',d2.path, - ' absorbed by ', - d1.etype,':',d1.path - ) + mt.__index = function + ( + t, -- table accessed + k -- key value accessed + ) + return t[ k_nt ][ k ] + end - return 'absorb' + -- + -- On assigning a new index. + -- + mt.__newindex = function + ( + t, -- table getting a new index assigned + k, -- key value to assign to + v -- value to assign + ) + if not assignAble[ k ] + then + error( 'Cannot assign new key "' .. k .. '" to Delay' ) + end + t[ k_nt ][ k ] = v end -- - -- The new delay replaces the old one if it's a file + -- This delay is being blocked by another delay -- - local function refi( d1, d2 ) - - -- but a directory blocks - if d2.path:byte( -1 ) == 47 then + local function blockedBy + ( + self, -- this delay + delay -- the blocking delay + ) + self[ k_nt ].status = 'block' - log( - 'Delay', - d2.etype,':',d2.path, - ' blocked by ', - d1.etype,':',d1.path - ) + local blocks = delay[ k_nt ].blocks - return 'stack' + if not blocks + then + blocks = { } + delay[ k_nt ].blocks = blocks end - log( - 'Delay', - d2.etype, ':', d2.path, - ' replaces ', - d1.etype, ':', d1.path - ) + table.insert( blocks, self ) + end - return 'replace' + -- + -- Sets the delay status to 'active'. + -- + local function setActive + ( + self + ) + self[ k_nt ].status = 'active' end -- - -- The new delay replaces an older one. + -- Sets the delay status to 'wait' -- - local function repl( d1, d2 ) + local function wait + ( + self, -- this delay + alarm -- alarm for the delay + ) + self[ k_nt ].status = 'wait' - log( - 'Delay', - d2.etype, ':', d2.path, - ' replaces ', - d1.etype, ':', d1.path - ) + self[ k_nt ].alarm = alarm + end - return 'replace' + -- + -- Creates a new delay. + -- + local function new + ( + etype, -- type of event. + -- 'Create', 'Modify', 'Attrib', 'Delete' or 'Move' + sync, -- the Sync this delay belongs to + alarm, -- latest point in time this should be catered for + path, -- path and file-/dirname of the delay relative + -- -- to the syncs root. + path2 -- used only in moves, path and file-/dirname of + -- move destination + ) + local delay = + { + blockedBy = blockedBy, + setActive = setActive, + wait = wait, + [ k_nt ] = + { + etype = etype, + sync = sync, + alarm = alarm, + path = path, + path2 = path2, + status = 'wait' + }, + } + setmetatable( delay, mt ) + + return delay end -- - -- Two delays nullificate each other. + -- Public interface + -- + return { new = new } +end )( ) + + +-- +-- Combines delays. +-- +local Combiner = ( function +( ) + -- + -- The new delay replaces the old one if it's a file -- - local function null( d1, d2 ) + local function refi + ( + d1, -- old delay + d2 -- new delay + ) + -- but a directory blocks + if d2.path:byte( -1 ) == 47 + then + log( + 'Delay', + d2.etype,': ',d2.path, + ' blocked by ', + d1.etype,': ',d1.path + ) + + return 'stack' + end log( 'Delay', - d2.etype,':',d2.path, - ' nullifies ', - d1.etype,':',d1.path + d2.etype, ': ', d2.path, + ' replaces ', + d1.etype, ': ', d1.path ) - return 'remove' - + return 'replace' end -- @@ -542,139 +810,115 @@ local combineNoMove = { Attrib = { - Attrib = abso, - Modify = repl, - Create = repl, - Delete = repl + Attrib = 'absorb', + Modify = 'replace', + Create = 'replace', + Delete = 'replace' }, Modify = { - Attrib = abso, - Modify = abso, - Create = repl, - Delete = repl + Attrib = 'absorb', + Modify = 'absorb', + Create = 'replace', + Delete = 'replace' }, Create = { - Attrib = abso, - Modify = abso, - Create = abso, - Delete = repl + Attrib = 'absorb', + Modify = 'absorb', + Create = 'absorb', + Delete = 'replace' }, Delete = { - Attrib = abso, - Modify = abso, - Create = refi, - Delete = abso + Attrib = 'absorb', + Modify = 'absorb', + Create = 'replace file,block dir', + Delete = 'absorb' }, } -- - -- Combines two delays + -- Returns the way two Delay should be combined. -- - local function combine( d1, d2 ) - - if d1.etype == 'Init' or d1.etype == 'Blanket' then - - -- everything is blocked by init or blanket delays. - if d2.path2 then - log( - 'Delay', - d2.etype,':',d2.path,'->',d2.path2, - ' blocked by ', - d1.etype,' event' - ) - else - log( - 'Delay', - d2.etype,':',d2.path, - ' blocked by ', - d1.etype,' event' - ) - end - + -- Result: + -- nil -- They don't affect each other. + -- 'stack' -- Old Delay blocks new Delay. + -- 'replace' -- Old Delay is replaced by new Delay. + -- 'absorb' -- Old Delay absorbs new Delay. + -- 'toDelete,stack' -- Old Delay is turned into a Delete + -- and blocks the new Delay. + -- 'split' -- New Delay a Move is to be split + -- into a Create and Delete. + -- + local function combine + ( + d1, -- old delay + d2 -- new delay + ) + if d1.etype == 'Init' or d1.etype == 'Blanket' + then return 'stack' end -- two normal events - if d1.etype ~= 'Move' and d2.etype ~= 'Move' then - - if d1.path == d2.path then - if d1.status == 'active' then - return 'stack' + if d1.etype ~= 'Move' and d2.etype ~= 'Move' + then + if d1.path == d2.path + then + -- lookups up the function in the combination matrix + -- and calls it + local result = combineNoMove[ d1.etype ][ d2.etype ] + + if result == 'replace file,block dir' + then + if d2.path:byte( -1 ) == 47 + then + return 'stack' + else + return 'replace' + end end - - return combineNoMove[ d1.etype ][ d2.etype ]( d1, d2 ) end -- if one is a parent directory of another, events are blocking - if d1.path:byte(-1) == 47 and string.starts(d2.path, d1.path) or - d2.path:byte(-1) == 47 and string.starts(d1.path, d2.path) + if d1.path:byte( -1 ) == 47 and string.starts( d2.path, d1.path ) + or d2.path:byte( -1 ) == 47 and string.starts( d1.path, d2.path ) then return 'stack' end return nil - end -- non-move event on a move. - if d1.etype == 'Move' and d2.etype ~= 'Move' then - -- if the from field could be damaged the events are stacked - if d1.path == d2.path or - d2.path:byte(-1) == 47 and string.starts(d1.path, d2.path) or - d1.path:byte(-1) == 47 and string.starts(d2.path, d1.path) + if d1.etype == 'Move' and d2.etype ~= 'Move' + then + -- if the move source could be damaged the events are stacked + if d1.path == d2.path + or d2.path:byte( -1 ) == 47 and string.starts( d1.path, d2.path ) + or d1.path:byte( -1 ) == 47 and string.starts( d2.path, d1.path ) then - log( - 'Delay', - d2.etype, ':', d2.path, - ' blocked by ', - 'Move :', d1.path,'->', d1.path2 - ) - return 'stack' end -- the event does something with the move destination - if d1.path2 == d2.path then - - if d2.etype == 'Delete' or d2.etype == 'Create' then - - if d1.status == 'active' then - return 'stack' - end - - log( - 'Delay', - d2.etype, ':', d2.path, - ' turns ', - 'Move :', d1.path, '->', d1.path2, - ' into ', - 'Delete:', d1.path - ) - d1.etype = 'Delete' - d1.path2 = nil - - return 'stack' + if d1.path2 == d2.path + then + if d2.etype == 'Delete' + or d2.etype == 'Create' + then + return 'toDelete,stack' end -- on 'Attrib' or 'Modify' simply stack on moves - return 'stack' end - if d2.path :byte(-1) == 47 and string.starts(d1.path2, d2.path) or - d1.path2:byte(-1) == 47 and string.starts(d2.path, d1.path2) + if d2.path:byte( -1 ) == 47 and string.starts( d1.path2, d2.path ) + or d1.path2:byte( -1 ) == 47 and string.starts( d2.path, d1.path2 ) then - log( - 'Delay' - ,d2.etype, ':', d2.path, - ' blocked by ', - 'Move:', d1.path, '->', d1.path2 - ) - return 'stack' end @@ -682,19 +926,15 @@ end -- a move upon a non-move event - if d1.etype ~= 'Move' and d2.etype == 'Move' then - if d1.path == d2.path or d1.path == d2.path2 or - d1.path :byte(-1) == 47 and string.starts(d2.path, d1.path) or - d1.path :byte(-1) == 47 and string.starts(d2.path2, d1.path) or - d2.path :byte(-1) == 47 and string.starts(d1.path, d2.path) or - d2.path2:byte(-1) == 47 and string.starts(d1.path, d2.path2) + if d1.etype ~= 'Move' and d2.etype == 'Move' + then + if d1.path == d2.path + or d1.path == d2.path2 + or d1.path:byte( -1 ) == 47 and string.starts( d2.path, d1.path ) + or d1.path:byte( -1 ) == 47 and string.starts( d2.path2, d1.path ) + or d2.path:byte( -1 ) == 47 and string.starts( d1.path, d2.path ) + or d2.path2:byte( -1 ) == 47 and string.starts( d1.path, d2.path2 ) then - log( - 'Delay', - 'Move:', d2.path, '->', d2.path2, - ' splits on ', - d1.etype, ':', d1.path - ) return 'split' end @@ -704,27 +944,22 @@ -- -- a move event upon a move event -- - if d1.etype == 'Move' and d2.etype == 'Move' then + if d1.etype == 'Move' and d2.etype == 'Move' + then -- TODO combine moves, - - if d1.path == d2.path or d1.path == d2.path2 or - d1.path2 == d2.path or d2.path2 == d2.path or - d1.path :byte(-1) == 47 and string.starts(d2.path, d1.path) or - d1.path :byte(-1) == 47 and string.starts(d2.path2, d1.path) or - d1.path2:byte(-1) == 47 and string.starts(d2.path, d1.path2) or - d1.path2:byte(-1) == 47 and string.starts(d2.path2, d1.path2) or - d2.path :byte(-1) == 47 and string.starts(d1.path, d2.path) or - d2.path :byte(-1) == 47 and string.starts(d1.path2, d2.path) or - d2.path2:byte(-1) == 47 and string.starts(d1.path, d2.path2) or - d2.path2:byte(-1) == 47 and string.starts(d1.path2, d2.path2) + if d1.path == d2.path + or d1.path == d2.path2 + or d1.path2 == d2.path + or d2.path2 == d2.path + or d1.path:byte( -1 ) == 47 and string.starts( d2.path, d1.path ) + or d1.path:byte( -1 ) == 47 and string.starts( d2.path2, d1.path ) + or d1.path2:byte( -1 ) == 47 and string.starts( d2.path, d1.path2 ) + or d1.path2:byte( -1 ) == 47 and string.starts( d2.path2, d1.path2 ) + or d2.path:byte( -1 ) == 47 and string.starts( d1.path, d2.path ) + or d2.path:byte( -1 ) == 47 and string.starts( d1.path2, d2.path ) + or d2.path2:byte( -1 ) == 47 and string.starts( d1.path, d2.path2 ) + or d2.path2:byte( -1 ) == 47 and string.starts( d1.path2, d2.path2 ) then - log( - 'Delay', - 'Move:', d2.path, '->', d1.path2, - ' splits on Move:', - d1.path, '->', d1.path2 - ) - return 'split' end @@ -736,17 +971,167 @@ -- + -- The new delay is absorbed by an older one. + -- + local function logAbsorb + ( + d1, -- old delay + d2 -- new delay + ) + log( + 'Delay', + d2.etype, ': ',d2.path, + ' absorbed by ', + d1.etype,': ',d1.path + ) + end + + -- + -- The new delay replaces the old one if it's a file. + -- + local function logReplace + ( + d1, -- old delay + d2 -- new delay + ) + log( + 'Delay', + d2.etype, ': ', d2.path, + ' replaces ', + d1.etype, ': ', d1.path + ) + end + + + -- + -- The new delay splits on the old one. + -- + local function logSplit + ( + d1, -- old delay + d2 -- new delay + ) + log( + 'Delay', + d2.etype, ': ', + d2.path, ' -> ', d2.path2, + ' splits on ', + d1.etype, ': ', d1.path + ) + end + + -- + -- The new delay is blocked by the old delay. + -- + local function logStack + ( + d1, -- old delay + d2 -- new delay + ) + local active = '' + + if d1.active + then + active = 'active ' + end + + if d2.path2 + then + log( + 'Delay', + d2.etype, ': ', + d2.path, '->', d2.path2, + ' blocked by ', + active, + d1.etype, ': ', d1.path + ) + else + log( + 'Delay', + d2.etype, ': ', d2.path, + ' blocked by ', + active, + d1.etype, ': ', d1.path + ) + end + end + + + -- + -- The new delay turns the old one (a move) into a delete and is blocked. + -- + local function logToDeleteStack + ( + d1, -- old delay + d2 -- new delay + ) + if d1.path2 + then + log( + 'Delay', + d2.etype, ': ', d2.path, + ' turns ', + d1.etype, ': ', d1.path, ' -> ', d1.path2, + ' into Delete: ', d1.path + ) + else + log( + 'Delay', + d2.etype, ': ', d2.path, + ' turns ', + d1.etype, ': ', d1.path, + ' into Delete: ', d1.path + ) + end + end + + + local logFuncs = + { + absorb = logAbsorb, + replace = logReplace, + split = logSplit, + stack = logStack, + [ 'toDelete,stack' ] = logToDeleteStack + } + + + -- + -- Prints the log message for a combination result + -- + local function log + ( + result, -- the combination result + d1, -- old delay + d2 -- new delay + ) + local lf = logFuncs[ result ] + + if not lf + then + error( 'unknown combination result: ' .. result ) + end + + lf( d1, d2 ) + end + + -- -- Public interface -- - return { combine = combine } + return + { + combine = combine, + log = log + } end )( ) + -- -- Creates inlets for syncs: the user interface for events. -- -local InletFactory = ( function( ) - +local InletFactory = ( function +( ) -- -- Table to receive the delay of an event -- or the delay list of an event list. @@ -773,9 +1158,13 @@ -- -- Removes the trailing slash from a path. -- - local function cutSlash( path ) - if string.byte(path, -1) == 47 then - return string.sub(path, 1, -2) + local function cutSlash + ( + path -- path to cut + ) + if string.byte( path, -1 ) == 47 + then + return string.sub( path, 1, -2 ) else return path end @@ -784,8 +1173,12 @@ -- -- Gets the path of an event. -- - local function getPath( event ) - if event.move ~= 'To' then + local function getPath + ( + event + ) + if event.move ~= 'To' + then return e2d[ event ].path else return e2d[ event ].path2 @@ -803,14 +1196,20 @@ -- -- TODO give user a readonly version. -- - config = function( event ) + config = function + ( + event + ) return e2d[ event ].sync.config end, - ----- + -- -- Returns the inlet belonging to an event. -- - inlet = function( event ) + inlet = function + ( + event + ) return e2d[ event ].sync.inlet end, @@ -819,14 +1218,18 @@ -- -- Can be: 'Attrib', 'Create', 'Delete', 'Modify' or 'Move', -- - etype = function( event ) + etype = function + ( + event + ) return e2d[ event ].etype end, -- -- Events are not lists. -- - isList = function( ) + isList = function + ( ) return false end, @@ -836,14 +1239,20 @@ -- Can be: -- 'wait', 'active', 'block'. -- - status = function( event ) + status = function + ( + event + ) return e2d[ event ].status end, -- -- Returns true if event relates to a directory -- - isdir = function( event ) + isdir = function + ( + event + ) return string.byte( getPath( event ), -1 ) == 47 end, @@ -852,7 +1261,10 @@ -- -- Includes a trailing slash for dirs. -- - name = function( event ) + name = function + ( + event + ) return string.match( getPath( event ), '[^/]+/?$' ) end, @@ -860,39 +1272,75 @@ -- Returns the name of the file/dir -- excluding a trailing slash for dirs. -- - basename = function( event ) + basename = function + ( + event + ) return string.match( getPath( event ), '([^/]+)/?$') end, - --- + -- -- Returns the file/dir relative to watch root -- including a trailing slash for dirs. -- - path = function( event ) - return getPath( event ) + path = function + ( + event + ) + local p = getPath( event ) + + if string.byte( p, 1 ) == 47 + then + p = string.sub( p, 2, -1 ) + end + + return p end, -- -- Returns the directory of the file/dir relative to watch root -- Always includes a trailing slash. -- - pathdir = function( event ) - return string.match( getPath( event ), '^(.*/)[^/]+/?' ) or '' + pathdir = function + ( + event + ) + local p = getPath( event ) + + if string.byte( p, 1 ) == 47 + then + p = string.sub( p, 2, -1 ) + end + + return string.match( p, '^(.*/)[^/]+/?' ) or '' end, -- -- Returns the file/dir relativ to watch root -- excluding a trailing slash for dirs. -- - pathname = function( event ) - return cutSlash( getPath( event ) ) + pathname = function + ( + event + ) + local p = getPath( event ) + + if string.byte( p, 1 ) == 47 + then + p = string.sub( p, 2, -1 ) + end + + return cutSlash( p ) end, - --- + -- -- Returns the absolute path of the watch root. -- All symlinks are resolved. -- - source = function( event ) + source = function + ( + event + ) return e2d[ event ].sync.source end, @@ -900,7 +1348,10 @@ -- Returns the absolute path of the file/dir -- including a trailing slash for dirs. -- - sourcePath = function( event ) + sourcePath = function + ( + event + ) return e2d[ event ].sync.source .. getPath( event ) end, @@ -908,23 +1359,37 @@ -- Returns the absolute dir of the file/dir -- including a trailing slash. -- - sourcePathdir = function( event ) - return e2d[event].sync.source .. - ( string.match( getPath( event ), '^(.*/)[^/]+/?' ) or '' ) + sourcePathdir = function + ( + event + ) + return( + e2d[event].sync.source + .. ( + string.match( getPath( event ), '^(.*/)[^/]+/?' ) + or '' + ) + ) end, - ------ + -- -- Returns the absolute path of the file/dir -- excluding a trailing slash for dirs. -- - sourcePathname = function( event ) + sourcePathname = function + ( + event + ) return e2d[ event ].sync.source .. cutSlash( getPath( event ) ) end, -- - -- Returns the configured target + -- Returns the configured target. -- - target = function( event ) + target = function + ( + event + ) return e2d[ event ].sync.config.target end, @@ -932,7 +1397,10 @@ -- Returns the relative dir/file appended to the target -- including a trailing slash for dirs. -- - targetPath = function( event ) + targetPath = function + ( + event + ) return e2d[ event ].sync.config.target .. getPath( event ) end, @@ -940,9 +1408,17 @@ -- Returns the dir of the dir/file appended to the target -- including a trailing slash. -- - targetPathdir = function( event ) - return e2d[ event ].sync.config.target .. - ( string.match( getPath( event ), '^(.*/)[^/]+/?' ) or '' ) + targetPathdir = function + ( + event + ) + return( + e2d[ event ].sync.config.target + .. ( + string.match( getPath( event ), '^(.*/)[^/]+/?' ) + or '' + ) + ) end, -- @@ -950,68 +1426,84 @@ -- excluding a trailing slash for dirs. -- targetPathname = function( event ) - return e2d[ event ].sync.config.target .. - cutSlash( getPath( event ) ) + return( + e2d[ event ].sync.config.target + .. cutSlash( getPath( event ) ) + ) end, } -- -- Retrievs event fields for the user script. -- - local eventMeta = { - - __index = function( event, field ) + local eventMeta = + { + __index = function + ( + event, + field + ) local f = eventFields[ field ] - if not f then - if field == 'move' then + + if not f + then + if field == 'move' + then -- possibly undefined return nil end - error( 'event does not have field "'..field..'"', 2 ) + + error( 'event does not have field "' .. field .. '"', 2 ) end + return f( event ) end - } -- -- Interface for user scripts to get list fields. -- - local eventListFuncs = { - + local eventListFuncs = + { -- -- Returns a list of paths of all events in list. -- - -- @param elist -- handle returned by getevents() - -- @param mutator -- if not nil called with (etype, path, path2) - -- returns one or two strings to add. -- - getPaths = function( elist, mutator ) - - local dlist = e2d[elist] + getPaths = function + ( + elist, -- handle returned by getevents( ) + mutator -- if not nil called with ( etype, path, path2 ) + -- returns one or two strings to add. + ) + local dlist = e2d[ elist ] - if not dlist then + if not dlist + then error( 'cannot find delay list from event list.' ) end local result = { } local resultn = 1 - for k, d in ipairs( dlist ) do - + for k, d in ipairs( dlist ) + do local s1, s2 - if mutator then + if mutator + then s1, s2 = mutator( d.etype, d.path, d.path2 ) else s1, s2 = d.path, d.path2 end result[ resultn ] = s1 + resultn = resultn + 1 - if s2 then + if s2 + then result[ resultn ] = s2 + resultn = resultn + 1 end end @@ -1024,33 +1516,38 @@ -- -- Retrievs event list fields for the user script -- - local eventListMeta = { - - __index = function( elist, func ) - - if func == 'isList' then + local eventListMeta = + { + __index = function + ( + elist, + func + ) + if func == 'isList' + then return true end - if func == 'config' then + if func == 'config' + then return e2d[ elist ].sync.config end local f = eventListFuncs[ func ] - if not f then + if not f + then error( 'event list does not have function "' .. func .. '"', 2 ) end - return function( ... ) + return function + ( ... ) return f( elist, ... ) end - end - } -- @@ -1066,34 +1563,34 @@ -- -- Encapsulates a delay into an event for the user script. -- - local function d2e( delay ) - + local function d2e + ( + delay -- delay to encapsulate + ) -- already created? - local eu = e2d2[delay] + local eu = e2d2[ delay ] - if delay.etype ~= 'Move' then - - if eu then - return eu - end + if delay.etype ~= 'Move' + then + if eu then return eu end local event = { } + setmetatable( event, eventMeta ) + e2d[ event ] = delay + e2d2[ delay ] = event return event - else -- moves have 2 events - origin and destination - if eu then - return eu[1], eu[2] - end + if eu then return eu[1], eu[2] end local event = { move = 'Fr' } local event2 = { move = 'To' } - setmetatable( event, eventMeta ) + setmetatable( event, eventMeta ) setmetatable( event2, eventMeta ) e2d[ event ] = delay @@ -1103,61 +1600,85 @@ -- move events have a field 'move' return event, event2 - end end -- -- Encapsulates a delay list into an event list for the user script. -- - local function dl2el( dlist ) - + local function dl2el + ( + dlist + ) local eu = e2d2[ dlist ] - if eu then - return eu - end + if eu then return eu end local elist = { } setmetatable( elist, eventListMeta ) e2d [ elist ] = dlist + e2d2[ dlist ] = elist return elist - end -- -- The functions the inlet provides. -- - local inletFuncs = { - + local inletFuncs = + { -- -- Adds an exclude. -- - addExclude = function( sync, pattern ) + addExclude = function + ( + sync, -- the sync of the inlet + pattern -- exlusion pattern to add + ) sync:addExclude( pattern ) end, + + -- + -- Appens a filter. + -- + appendFilter = function + ( + sync, -- the sync of the inlet + rule, -- '+' or '-' + pattern -- exlusion pattern to add + ) + sync:appendFilter( rule, pattern ) + end, + -- -- Removes an exclude. -- - rmExclude = function( sync, pattern ) + rmExclude = function + ( + sync, -- the sync of the inlet + pattern -- exlusion pattern to remove + ) sync:rmExclude( pattern ) end, -- - -- Gets the list of excludes in their original rsynlike patterns form. + -- Gets the list of excludes in their + -- rsync-like patterns form. -- - getExcludes = function( sync ) - + getExcludes = function + ( + sync -- the sync of the inlet + ) -- creates a copy local e = { } local en = 1; - for k, _ in pairs( sync.excludes.list ) do + for k, _ in pairs( sync.excludes.list ) + do e[ en ] = k; en = en + 1; end @@ -1166,51 +1687,110 @@ end, -- + -- Gets the list of filters and excldues + -- as rsync-like filter/patterns form. + -- + getFilters = function + ( + sync -- the sync of the inlet + ) + -- creates a copy + local e = { } + local en = 1; + + -- first takes the filters + if sync.filters + then + for _, entry in ipairs( sync.filters.list ) + do + e[ en ] = entry.rule .. ' ' .. entry.pattern; + en = en + 1; + end + end + + -- then the excludes + for k, _ in pairs( sync.excludes.list ) + do + e[ en ] = '- ' .. k; + en = en + 1; + end + + return e; + end, + + -- + -- Returns true if the sync has filters + -- + hasFilters = function + ( + sync -- the sync of the inlet + ) + return not not sync.filters + end, + + -- -- Creates a blanketEvent that blocks everything -- and is blocked by everything. -- - createBlanketEvent = function( sync ) + createBlanketEvent = function + ( + sync -- the sync of the inlet + ) return d2e( sync:addBlanketDelay( ) ) end, -- -- Discards a waiting event. -- - discardEvent = function( sync, event ) + discardEvent = function + ( + sync, + event + ) local delay = e2d[ event ] - if delay.status ~= 'wait' then + + if delay.status ~= 'wait' + then log( 'Error', 'Ignored cancel of a non-waiting event of type ', event.etype ) + return end + sync:removeDelay( delay ) end, -- -- Gets the next not blocked event from queue. -- - getEvent = function( sync ) + getEvent = function + ( + sync + ) return d2e( sync:getNextDelay( now( ) ) ) end, -- -- Gets all events that are not blocked by active events. -- - -- @param if not nil a function to test each delay - -- - getEvents = function( sync, test ) + getEvents = function + ( + sync, -- the sync of the inlet + test -- if not nil use this function to test if to include an event + ) local dlist = sync:getDelays( test ) + return dl2el( dlist ) end, -- - -- Returns the configuration table specified by sync{} + -- Returns the configuration table specified by sync{ } -- getConfig = function( sync ) - -- TODO gives a readonly handler only. + -- TODO give a readonly handler only. return sync.config end, } @@ -1218,14 +1798,18 @@ -- -- Forwards access to inlet functions. -- - local inletMeta = { - __index = function( inlet, func ) + local inletMeta = + { + __index = function + ( + inlet, + func + ) local f = inletFuncs[ func ] - if not f then - error( - 'inlet does not have function "'..func..'"', - 2 - ) + + if not f + then + error( 'inlet does not have function "'..func..'"', 2 ) end return function( ... ) @@ -1235,30 +1819,40 @@ } -- - -- Creates a new inlet for Sync. + -- Creates a new inlet for a sync. -- - local function newInlet( sync ) - + local function newInlet + ( + sync -- the sync to create the inlet for + ) -- Lsyncd runner controlled variables local inlet = { } -- sets use access methods setmetatable( inlet, inletMeta ) + inlets[ inlet ] = sync + return inlet end -- -- Returns the delay from a event. -- - local function getDelayOrList( event ) + local function getDelayOrList + ( + event + ) return e2d[ event ] end -- -- Returns the sync from an event or list -- - local function getSync( event ) + local function getSync + ( + event + ) return e2d[ event ].sync end @@ -1272,21 +1866,219 @@ getSync = getSync, newInlet = newInlet, } +end )( ) + + +-- +-- A set of exclude patterns. +-- +local Excludes = ( function +( ) + -- + -- Turns a rsync like file pattern to a lua pattern. + -- ( at best it can ) + -- + local function toLuaPattern + ( + p -- the rsync like pattern + ) + local o = p + + p = string.gsub( p, '%%', '%%%%' ) + p = string.gsub( p, '%^', '%%^' ) + p = string.gsub( p, '%$', '%%$' ) + p = string.gsub( p, '%(', '%%(' ) + p = string.gsub( p, '%)', '%%)' ) + p = string.gsub( p, '%.', '%%.' ) + p = string.gsub( p, '%[', '%%[' ) + p = string.gsub( p, '%]', '%%]' ) + p = string.gsub( p, '%+', '%%+' ) + p = string.gsub( p, '%-', '%%-' ) + p = string.gsub( p, '%?', '[^/]' ) + p = string.gsub( p, '%*', '[^/]*' ) + -- this was a ** before + p = string.gsub( p, '%[%^/%]%*%[%^/%]%*', '.*' ) + p = string.gsub( p, '^/', '^/' ) + + if p:sub( 1, 2 ) ~= '^/' + then + -- if does not begin with '^/' + -- then all matches should begin with '/'. + p = '/' .. p; + end + + log( 'Exclude', 'toLuaPattern "', o, '" = "', p, '"' ) + + return p + end + + -- + -- Adds a pattern to exclude. + -- + local function add + ( + self, + pattern -- the pattern to exclude + ) + if self.list[ pattern ] + then -- already in the list + return + end + + local lp = toLuaPattern( pattern ) + + self.list[ pattern ] = lp + end + + -- + -- Removes a pattern to exclude. + -- + local function remove + ( + self, -- self + pattern -- the pattern to remove + ) + -- already in the list? + if not self.list[ pattern ] + then + log( + 'Normal', + 'Removing not excluded exclude "' .. pattern .. '"' + ) + + return + end + + self.list[ pattern ] = nil + end + + -- + -- Adds a list of patterns to exclude. + -- + local function addList + ( + self, + plist + ) + for _, v in ipairs( plist ) + do + add( self, v ) + end + end + + -- + -- Loads the excludes from a file. + -- + local function loadFile + ( + self, -- self + file -- filename to load from + ) + f, err = io.open( file ) + + if not f + then + log( 'Error', 'Cannot open exclude file "', file,'": ', err ) + + terminate( -1 ) + end + + for line in f:lines() + do + -- lsyncd 2.0 does not support includes + + if not string.match( line, '^%s*%+' ) + and not string.match( line, '^%s*#' ) + and not string.match( line, '^%s*$' ) + then + local p = string.match( line, '%s*-?%s*(.*)' ) + + if p + then + add( self, p ) + end + end + end + + f:close( ) + end + + -- + -- Tests if 'path' is excluded. + -- + local function test + ( + self, -- self + path -- the path to test + ) + if path:byte( 1 ) ~= 47 + then + error( 'Paths for exlusion tests must start with \'/\'' ) + end + + for _, p in pairs( self.list ) + do + if p:byte( -1 ) == 36 + then + -- ends with $ + if path:match( p ) + then + return true + end + else + -- ends either end with / or $ + if path:match( p .. '/' ) + or path:match( p .. '$' ) + then + return true + end + end + end + + return false + end + + -- + -- Cretes a new exclude set. + -- + local function new + ( ) + return { + list = { }, + + -- functions + add = add, + addList = addList, + loadFile = loadFile, + remove = remove, + test = test, + } + end + -- + -- Public interface. + -- + return { new = new } end )( ) -- --- A set of exclude patterns +-- A set of filter patterns. -- -local Excludes = ( function( ) - +-- Filters allow excludes and includes +-- +local Filters = ( function +( ) -- -- Turns a rsync like file pattern to a lua pattern. -- ( at best it can ) -- - local function toLuaPattern( p ) + local function toLuaPattern + ( + p -- the rsync like pattern + ) local o = p + p = string.gsub( p, '%%', '%%%%' ) p = string.gsub( p, '%^', '%%^' ) p = string.gsub( p, '%$', '%%$' ) @@ -1303,96 +2095,78 @@ p = string.gsub( p, '%[%^/%]%*%[%^/%]%*', '.*' ) p = string.gsub( p, '^/', '^/' ) - if p:sub( 1, 2 ) ~= '^/' then + if p:sub( 1, 2 ) ~= '^/' + then -- if does not begin with '^/' -- then all matches should begin with '/'. p = '/' .. p; end - log( - 'Exclude', - 'toLuaPattern "', - o, '" = "', p, '"' - ) + log( 'Filter', 'toLuaPattern "', o, '" = "', p, '"' ) return p end -- - -- Adds a pattern to exclude + -- Appends a filter pattern -- - local function add( self, pattern ) + local function append + ( + self, -- the filters object + line -- filter line + ) + local rule, pattern = string.match( line, '%s*([+|-])%s*(.*)' ) - if self.list[ pattern ] then - -- already in the list - return + if not rule or not pattern + then + log( 'Error', 'Unknown filter rule: "', line, '"' ) + terminate( -1 ) end local lp = toLuaPattern( pattern ) - self.list[ pattern ] = lp + table.insert( self. list, { rule = rule, pattern = pattern, lp = lp } ) end -- - -- Removes a pattern to exclude. - -- - local function remove( self, pattern ) - - if not self.list[ pattern ] then - -- already in the list? - - log( - 'Normal', - 'Removing not excluded exclude "' .. pattern .. '"' - ) - - return - end - - self.list[pattern] = nil - - end - - - ----- -- Adds a list of patterns to exclude. -- - local function addList(self, plist) - for _, v in ipairs(plist) do - add(self, v) + local function appendList + ( + self, + plist + ) + for _, v in ipairs( plist ) + do + append( self, v ) end end -- - -- Loads the excludes from a file + -- Loads the filters from a file. -- - local function loadFile( self, file ) - + local function loadFile + ( + self, -- self + file -- filename to load from + ) f, err = io.open( file ) - if not f then - log( - 'Error', - 'Cannot open exclude file "', file,'": ', - err - ) + if not f + then + log( 'Error', 'Cannot open filter file "', file, '": ', err ) terminate( -1 ) end - for line in f:lines() do - - -- lsyncd 2.0 does not support includes - - if not string.match(line, '^%s*%+') and - not string.match(line, '^%s*#') and - not string.match(line, '^%s*$') then - local p = string.match( - line, '%s*-?%s*(.*)' - ) - if p then - add(self, p) - end + for line in f:lines( ) + do + if string.match( line, '^%s*#' ) + or string.match( line, '^%s*$' ) + then + -- a comment or empty line: ignore + else + append( self, line ) end end @@ -1400,64 +2174,76 @@ end -- - -- Tests if 'path' is excluded. + -- Tests if 'path' is filtered. -- - local function test( self, path ) + local function test + ( + self, -- self + path -- the path to test + ) + if path:byte( 1 ) ~= 47 + then + error( 'Paths for filter tests must start with \'/\'' ) + end - for _, p in pairs( self.list ) do + for _, entry in ipairs( self.list ) + do + local rule = entry.rule + local lp = entry.lp -- lua pattern - if p:byte( -1 ) == 36 then + if lp:byte( -1 ) == 36 + then -- ends with $ - - if path:match( p ) then - return true + if path:match( lp ) + then + return rule == '-' end - else - -- ends either end with / or $ - if path:match(p .. '/') or path:match(p .. '$') then - return true + if path:match( lp .. '/' ) + or path:match( lp .. '$' ) + then + return rule == '-' end - end end - return false - + -- nil means neither a positivie + -- or negative hit, thus excludes have to + -- be queried + return nil end - -- - -- Cretes a new exclude set + -- Cretes a new filter set. -- - local function new( ) - + local function new + ( ) return { list = { }, - -- functions - add = add, - addList = addList, - loadFile = loadFile, - remove = remove, - test = test, + append = append, + appendList = appendList, + loadFile = loadFile, + test = test, } - end + -- - -- Public interface + -- Public interface. -- return { new = new } end )( ) + + -- -- Holds information about one observed directory including subdirs. -- -local Sync = ( function( ) - +local Sync = ( function +( ) -- -- Syncs that have no name specified by the user script -- get an incremental default name 'Sync[X]' @@ -1467,90 +2253,127 @@ -- -- Adds an exclude. -- - local function addExclude( self, pattern ) - + local function addExclude + ( + self, + pattern + ) return self.excludes:add( pattern ) + end + local function appendFilter + ( + self, + rule, + pattern + ) + if not self.filters then self.filters = Filters.new( ) end + + return self.filters:append( rule, pattern ) end -- -- Removes an exclude. -- - local function rmExclude( self, pattern ) - + local function rmExclude + ( + self, + pattern + ) return self.excludes:remove( pattern ) - end -- -- Removes a delay. -- - local function removeDelay( self, delay ) - - if self.delays[ delay.dpos ] ~= delay then - error( 'Queue is broken, delay not a dpos' ) + local function removeDelay + ( + self, + delay + ) + if self.delays[ delay.dpos ] ~= delay + then + error( 'Queue is broken, delay not at dpos' ) end - Queue.remove( self.delays, delay.dpos ) - - -- free all delays blocked by this one. - if delay.blocks then - - for i, vd in pairs( delay.blocks ) do + self.delays:remove( delay.dpos ) + -- frees all delays blocked by this one. + if delay.blocks + then + for _, vd in pairs( delay.blocks ) + do vd.status = 'wait' - end - end end - + + -- - -- Returns true if this Sync concerns about 'path' - -- - local function concerns( self, path ) + -- Returns true if the relative path is excluded or filtered + -- + local function testFilter + ( + self, -- the Sync + path -- the relative path + ) + -- never filter the relative root itself + -- ( that would make zero sense ) + if path == '/' then return false end - -- not concerned if watch rootdir doesnt match - if not path:starts( self.source ) then + local filter = self.filters and self.filters:test( path ) - return false + if filter ~= nil then return filter end + + -- otherwise check excludes if concerned + return self.excludes:test( path ) + end + -- + -- Returns true if this Sync concerns about 'path'. + -- + local function concerns + ( + self, -- the Sync + path -- the absolute path + ) + -- not concerned if watch rootdir doesn't match + if not path:starts( self.source ) + then + return false end -- a sub dir and not concerned about subdirs - if self.config.subdirs == false and - - path:sub( #self.source, -1 ):match( '[^/]+/?' ) - + if self.config.subdirs == false + and path:sub( #self.source, -1 ):match( '[^/]+/?' ) then - return false - end - -- concerned if not excluded - return not self.excludes:test( path:sub( #self.source ) ) - + return not testFilter( self, path:sub( #self.source ) ) end -- - -- Collects a child process + -- Collects a child process. -- - local function collect( self, pid, exitcode ) - + local function collect + ( + self, -- the sync + pid, -- process id of collected child process + exitcode -- exitcode of child process + ) local delay = self.processes[ pid ] - if not delay then - -- not a child of this sync. - return - end - - if delay.status then + -- not a child of this sync? + if not delay then return end + if delay.status + then log( 'Delay', 'collected an event' ) - if delay.status ~= 'active' then - error('collecting a non-active process') + if delay.status ~= 'active' + then + error( 'collecting a non-active process' ) end local rc = self.config.collect( @@ -1558,14 +2381,16 @@ exitcode ) - if rc == 'die' then - log( 'Error', 'Critical exitcode.' ); - terminate( -1 ) - end + if rc == 'die' + then + log( 'Error', 'Critical exitcode.' ) - if rc ~= 'again' then + terminate( -1 ) + elseif rc ~= 'again' + then -- if its active again the collecter restarted the event removeDelay( self, delay ) + log( 'Delay', 'Finish of ', @@ -1577,56 +2402,44 @@ ) else -- sets the delay on wait again - delay.status = 'wait' - local alarm = self.config.delay -- delays at least 1 second - if alarm < 1 then - alarm = 1 - end + if alarm < 1 then alarm = 1 end - delay.alarm = now( ) + alarm + delay:wait( now( ) + alarm ) end else - log( - 'Delay', - 'collected a list' - ) + log( 'Delay', 'collected a list' ) local rc = self.config.collect( InletFactory.dl2el( delay ), exitcode ) - if rc == 'die' then + if rc == 'die' + then log( 'Error', 'Critical exitcode.' ); - terminate( -1 ) - end - - if rc == 'again' then + terminate( -1 ) + elseif rc == 'again' + then -- sets the delay on wait again - delay.status = 'wait' local alarm = self.config.delay - -- delays at least 1 second - if alarm < 1 then - alarm = 1 - end + + -- delays are at least 1 second + if alarm < 1 then alarm = 1 end alarm = now() + alarm - for _, d in ipairs( delay ) do - d.alarm = alarm - d.status = 'wait' + for _, d in ipairs( delay ) + do + d:wait( alarm ) end - end - - for _, d in ipairs( delay ) do - if rc ~= 'again' then + else + for _, d in ipairs( delay ) + do removeDelay( self, d ) - else - d.status = 'wait' end end @@ -1643,23 +2456,25 @@ -- A delay can block 'n' other delays, -- but is blocked at most by one, the latest delay. -- - local function stack( oldDelay, newDelay ) - - newDelay.status = 'block' - - if not oldDelay.blocks then - oldDelay.blocks = { } - end - - table.insert( oldDelay.blocks, newDelay ) - + local function stack + ( + oldDelay, + newDelay + ) + newDelay:blockedBy( oldDelay ) end -- -- Puts an action on the delay stack. -- - local function delay( self, etype, time, path, path2 ) - + local function delay + ( + self, -- the sync + etype, -- the event type + time, -- time of the event + path, -- path of the event + path2 -- desitination path of move events + ) log( 'Function', 'delay( ', @@ -1670,147 +2485,120 @@ ' )' ) - -- TODO - local function recurse( ) - - if etype == 'Create' and path:byte( -1 ) == 47 then - + -- + -- In case new directories were created + -- looks through this directories and makes create events for + -- new stuff found in there. + -- + local function recurse + ( ) + if etype == 'Create' and path:byte( -1 ) == 47 + then local entries = lsyncd.readdir( self.source .. path ) - if entries then - - for dirname, isdir in pairs(entries) do - + if entries + then + for dirname, isdir in pairs( entries ) + do local pd = path .. dirname - if isdir then - pd = pd..'/' - end + if isdir then pd = pd..'/' end - log( - 'Delay', - 'Create creates Create on ', - pd - ) - delay( self, 'Create', time, pd, nil ) + log( 'Delay', 'Create creates Create on ', pd ) + delay( self, 'Create', time, pd, nil ) end - end - end - end -- exclusion tests - if not path2 then + if not path2 + then -- simple test for single path events - if self.excludes:test(path) then - log( - 'Exclude', - 'excluded ', - etype, - ' on "', - path, - '"' - ) + if testFilter( self, path ) + then + log( 'Filter', 'filtered ', etype, ' on "', path, '"' ) + return end else - -- for double paths (move) it might result into a split - local ex1 = self.excludes:test( path ) - local ex2 = self.excludes:test( path2 ) + -- for double paths ( move ) it might result into a split + local ex1 = testFilter( self, path ) - if ex1 and ex2 then + local ex2 = testFilter( self, path2 ) + if ex1 and ex2 + then log( - 'Exclude', - 'excluded "', - etype, - ' on "', - path, - '" -> "', - path2, - '"' + 'Filter', + 'filtered "', etype, ' on "', path, + '" -> "', path2, '"' ) return - - elseif not ex1 and ex2 then - + elseif not ex1 and ex2 + then -- splits the move if only partly excluded log( - 'Exclude', - 'excluded destination transformed ', + 'Filter', + 'filtered destination transformed ', etype, ' to Delete ', path ) - delay( - self, - 'Delete', - time, - path, - nil - ) + delay( self, 'Delete', time, path, nil ) return - - elseif ex1 and not ex2 then + elseif ex1 and not ex2 + then -- splits the move if only partly excluded log( - 'Exclude', - 'excluded origin transformed ', + 'Filter', + 'filtered origin transformed ', etype, ' to Create.', path2 ) - delay( - self, - 'Create', - time, - path2, - nil - ) + delay( self, 'Create', time, path2, nil ) return end end - if etype == 'Move' and not self.config.onMove then - + if etype == 'Move' + and not self.config.onMove + then -- if there is no move action defined, -- split a move as delete/create -- layer 1 scripts which want moves events have to -- set onMove simply to 'true' log( 'Delay', 'splitting Move into Delete & Create' ) + delay( self, 'Delete', time, path, nil ) + delay( self, 'Create', time, path2, nil ) - return + return end -- creates the new action local alarm - if time and self.config.delay then + + if time and self.config.delay + then alarm = time + self.config.delay else alarm = now( ) end -- new delay - local nd = Delay.new( - etype, - self, - alarm, - path, - path2 - ) - - if nd.etype == 'Init' or nd.etype == 'Blanket' then + local nd = Delay.new( etype, self, alarm, path, path2 ) + if nd.etype == 'Init' or nd.etype == 'Blanket' + then -- always stack init or blanket events on the last event log( 'Delay', @@ -1819,77 +2607,124 @@ ' event.' ) - if self.delays.size > 0 then - stack( self.delays[ self.delays.last ], nd ) + if self.delays:size( ) > 0 + then + stack( self.delays:last( ), nd ) end - nd.dpos = Queue.push( self.delays, nd ) + nd.dpos = self.delays:push( nd ) + recurse( ) return - end -- detects blocks and combos by working from back until -- front through the fifo - for il, od in Queue.qpairsReverse( self.delays ) do - + for il, od in self.delays:qpairsReverse( ) + do -- asks Combiner what to do local ac = Combiner.combine( od, nd ) - if ac then - if ac == 'remove' then - Queue.remove( self.delays, il ) - elseif ac == 'stack' then + if ac + then + Combiner.log( ac, od, nd ) + + if ac == 'remove' + then + self.delays:remove( il ) + elseif ac == 'stack' + then stack( od, nd ) - nd.dpos = Queue.push( self.delays, nd ) - elseif ac == 'absorb' then + + nd.dpos = self.delays:push( nd ) + elseif ac == 'toDelete,stack' + then + if od.status ~= 'active' + then + -- turns olddelay into a delete + local rd = Delay.new( 'Delete', self, od.alarm, od.path ) + + self.delays:replace( il, rd ) + + rd.dpos = il + + -- and stacks delay2 + stack( rd, nd ) + else + -- and stacks delay2 + stack( od, nd ) + end + + nd.dpos = self.delays:push( nd ) + elseif ac == 'absorb' + then -- nada - elseif ac == 'replace' then - od.etype = nd.etype - od.path = nd.path - od.path2 = nd.path2 - elseif ac == 'split' then + elseif ac == 'replace' + then + if od.status ~= 'active' + then + self.delays:replace( il, nd ) + + nd.dpos = il + else + stack( od, nd ) + + nd.dpos = self.delays:push( nd ) + end + elseif ac == 'split' + then delay( self, 'Delete', time, path, nil ) + delay( self, 'Create', time, path2, nil ) else error( 'unknown result of combine()' ) end + recurse( ) + return end il = il - 1 end - if nd.path2 then - log( 'Delay','New ',nd.etype,':',nd.path,'->',nd.path2 ) + if nd.path2 + then + log( 'Delay', 'New ', nd.etype, ': ', nd.path, ' -> ', nd.path2 ) else - log( 'Delay','New ',nd.etype,':',nd.path ) + log( 'Delay', 'New ', nd.etype, ': ', nd.path ) end -- no block or combo - nd.dpos = Queue.push( self.delays, nd ) + nd.dpos = self.delays:push( nd ) + recurse( ) end -- -- Returns the soonest alarm for this Sync. -- - local function getAlarm( self ) - - if self.processes:size( ) >= self.config.maxProcesses then + local function getAlarm + ( + self + ) + if self.processes:size( ) >= self.config.maxProcesses + then return false end -- first checks if more processes could be spawned - if self.processes:size( ) < self.config.maxProcesses then - + if self.processes:size( ) < self.config.maxProcesses + then -- finds the nearest delay waiting to be spawned - for _, d in Queue.qpairs( self.delays ) do - if d.status == 'wait' then return d.alarm end + for _, d in self.delays:qpairs( ) + do + if d.status == 'wait' + then + return d.alarm + end end - end -- nothing to spawn @@ -1899,32 +2734,53 @@ -- -- Gets all delays that are not blocked by active delays. -- - -- @param test function to test each delay - -- - local function getDelays( self, test ) - local dlist = { sync = self} + local function getDelays + ( + self, -- the sync + test -- function to test each delay + ) + local dlist = { sync = self } + local dlistn = 1 + local blocks = { } -- -- inheritly transfers all blocks from delay -- - local function getBlocks( delay ) + local function getBlocks + ( + delay + ) blocks[ delay ] = true - if delay.blocks then - for i, d in ipairs( delay.blocks ) do + + if delay.blocks + then + for _, d in ipairs( delay.blocks ) + do getBlocks( d ) end end end - for i, d in Queue.qpairs( self.delays ) do - if d.status == 'active' or - ( test and not test( InletFactory.d2e( d ) ) ) + for _, d in self.delays:qpairs( ) + do + local tr = true + + if test + then + tr = test( InletFactory.d2e( d ) ) + end + + if tr == 'break' then break end + + if d.status == 'active' or not tr then getBlocks( d ) - elseif not blocks[ d ] then + elseif not blocks[ d ] + then dlist[ dlistn ] = d + dlistn = dlistn + 1 end end @@ -1935,8 +2791,11 @@ -- -- Creates new actions -- - local function invokeActions( self, timestamp ) - + local function invokeActions + ( + self, + timestamp + ) log( 'Function', 'invokeActions( "', @@ -1945,40 +2804,46 @@ ' )' ) - if self.processes:size( ) >= self.config.maxProcesses then + if self.processes:size( ) >= self.config.maxProcesses + then -- no new processes return end - for _, d in Queue.qpairs( self.delays ) do - + for _, d in self.delays:qpairs( ) + do -- if reached the global limit return - if uSettings.maxProcesses and - processCount >= uSettings.maxProcesses + if uSettings.maxProcesses + and processCount >= uSettings.maxProcesses then log('Alarm', 'at global process limit.') + return end - if self.delays.size < self.config.maxDelays then + if self.delays:size( ) < self.config.maxDelays + then -- time constrains are only concerned if not maxed -- the delay FIFO already. - if d.alarm ~= true and timestamp < d.alarm then + if d.alarm ~= true and timestamp < d.alarm + then -- reached point in stack where delays are in future return end end - if d.status == 'wait' then - + if d.status == 'wait' + then -- found a waiting delay - if d.etype ~= 'Init' then + if d.etype ~= 'Init' + then self.config.action( self.inlet ) else self.config.init( InletFactory.d2e( d ) ) end - if self.processes:size( ) >= self.config.maxProcesses then + if self.processes:size( ) >= self.config.maxProcesses + then -- no further processes return end @@ -1989,34 +2854,44 @@ -- -- Gets the next event to be processed. -- - local function getNextDelay( self, timestamp ) - - for i, d in Queue.qpairs( self.delays ) do - - if self.delays.size < self.config.maxDelays then + local function getNextDelay + ( + self, + timestamp + ) + for i, d in self.delays:qpairs( ) + do + if self.delays:size( ) < self.config.maxDelays + then -- time constrains are only concerned if not maxed -- the delay FIFO already. - if d.alarm ~= true and timestamp < d.alarm then + if d.alarm ~= true and timestamp < d.alarm + then -- reached point in stack where delays are in future return nil end end - if d.status == 'wait' then + if d.status == 'wait' + then -- found a waiting delay return d end end - end - ------ + -- -- Adds and returns a blanket delay thats blocks all. -- Used as custom marker. -- - local function addBlanketDelay( self ) + local function addBlanketDelay + ( + self + ) local newd = Delay.new( 'Blanket', self, true, '' ) - newd.dpos = Queue.push( self.delays, newd ) + + newd.dpos = self.delays:push( newd ) + return newd end @@ -2024,11 +2899,13 @@ -- Adds and returns a blanket delay thats blocks all. -- Used as startup marker to call init asap. -- - local function addInitDelay( self ) - + local function addInitDelay + ( + self + ) local newd = Delay.new( 'Init', self, true, '' ) - newd.dpos = Queue.push( self.delays, newd ) + newd.dpos = self.delays:push( newd ) return newd end @@ -2036,20 +2913,27 @@ -- -- Writes a status report about delays in this sync. -- - local function statusReport( self, f ) - + local function statusReport + ( + self, + f + ) local spaces = ' ' f:write( self.config.name, ' source=', self.source, '\n' ) - f:write( 'There are ', self.delays.size, ' delays\n') - for i, vd in Queue.qpairs( self.delays ) do + f:write( 'There are ', self.delays:size( ), ' delays\n') + + for i, vd in self.delays:qpairs( ) + do local st = vd.status + f:write( st, string.sub( spaces, 1, 7 - #st ) ) f:write( vd.etype, ' ' ) f:write( vd.path ) - if vd.path2 then + if vd.path2 + then f:write( ' -> ',vd.path2 ) end @@ -2057,15 +2941,34 @@ end - f:write( 'Excluding:\n' ) + f:write( 'Filtering:\n' ) local nothing = true - for t, p in pairs( self.excludes.list ) do - nothing = false - f:write( t,'\n' ) + if self.filters + then + for _, e in pairs( self.filters.list ) + do + nothing = false + + f:write( e.rule, ' ', e.pattern,'\n' ) + end + end + + if #self.excludes.list > 0 + then + f:write( 'From excludes:\n' ) + + for t, p in pairs( self.excludes.list ) + do + nothing = false + + f:write( '- ', t,'\n' ) + end end - if nothing then + + if nothing + then f:write(' nothing.\n') end @@ -2073,24 +2976,27 @@ end -- - -- Creates a new Sync + -- Creates a new Sync. -- - local function new( config ) - - local s = { + local function new + ( + config + ) + local s = + { -- fields - config = config, delays = Queue.new( ), source = config.source, processes = CountArray.new( ), excludes = Excludes.new( ), + filters = nil, -- functions - addBlanketDelay = addBlanketDelay, addExclude = addExclude, addInitDelay = addInitDelay, + appendFilter = appendFilter, collect = collect, concerns = concerns, delay = delay, @@ -2106,7 +3012,8 @@ s.inlet = InletFactory.newInlet( s ) -- provides a default name if needed - if not config.name then + if not config.name + then config.name = 'Sync' .. nextDefaultName end @@ -2114,14 +3021,35 @@ -- so Sync{n} will be the n-th call to sync{} nextDefaultName = nextDefaultName + 1 - -- loads exclusions - if config.exclude then + -- loads filters + if config.filter + then + local te = type( config.filter ) + + s.filters = Filters.new( ) + + if te == 'table' + then + s.filters:appendList( config.filter ) + elseif te == 'string' + then + s.filters:append( config.filter ) + else + error( 'type for filter must be table or string', 2 ) + end + + end + -- loads exclusions + if config.exclude + then local te = type( config.exclude ) - if te == 'table' then + if te == 'table' + then s.excludes:addList( config.exclude ) - elseif te == 'string' then + elseif te == 'string' + then s.excludes:add( config.exclude ) else error( 'type for exclude must be table or string', 2 ) @@ -2129,20 +3057,22 @@ end - if - config.delay ~= nil and - ( - type(config.delay) ~= 'number' or - config.delay < 0 - ) + if config.delay ~= nil + and ( type( config.delay ) ~= 'number' or config.delay < 0 ) then error( 'delay must be a number and >= 0', 2 ) end - if config.excludeFrom then + if config.filterFrom + then + if not s.filters then s.filters = Filters.new( ) end - s.excludes:loadFile( config.excludeFrom ) + s.filters:loadFile( config.filterFrom ) + end + if config.excludeFrom + then + s.excludes:loadFile( config.excludeFrom ) end return s @@ -2151,10 +3081,7 @@ -- -- Public interface -- - return { - new = new - } - + return { new = new } end )( ) @@ -2163,8 +3090,8 @@ -- -- Syncs maintains all configured syncs. -- -local Syncs = ( function( ) - +local Syncs = ( function +( ) -- -- the list of all syncs -- @@ -2179,11 +3106,12 @@ -- -- The cycle( ) sheduler goes into the next round of roundrobin. -- - local function nextRound( ) - + local function nextRound + ( ) round = round + 1; - if round > #syncsList then + if round > #syncsList + then round = 1 end @@ -2193,14 +3121,16 @@ -- -- Returns the round -- - local function getRound( ) + local function getRound + ( ) return round end -- -- Returns sync at listpos i -- - local function get( i ) + local function get + ( i ) return syncsList[ i ]; end @@ -2214,48 +3144,48 @@ -- Recurvely inherits a source table to a destionation table -- copying all keys from source. -- - -- table copy source ( cs ) - -- table copy destination ( cd ) - -- -- All entries with integer keys are inherited as additional -- sources for non-verbatim tables -- - local function inherit( cd, cs ) - + local function inherit + ( + cd, -- table copy destination + cs, -- table copy source + verbatim -- forced verbatim ( for e.g. 'exitcodes' ) + ) + -- First copies all entries with non-integer keys. -- - -- First copies all entries with non-integer keys - -- tables are merged, already present keys are not + -- Tables are merged; already present keys are not -- overwritten -- -- For verbatim tables integer keys are treated like - -- non integer keys - -- - for k, v in pairs( cs ) do + -- non-integer keys + for k, v in pairs( cs ) + do if ( - type( k ) ~= 'number' or - cs._verbatim == true + type( k ) ~= 'number' + or verbatim + or cs._verbatim == true ) and ( - type( cs._merge ) ~= 'table' or - cs._merge[ k ] == true + type( cs._merge ) ~= 'table' + or cs._merge[ k ] == true ) then inheritKV( cd, k, v ) end end - -- -- recursevely inherits all integer keyed tables -- ( for non-verbatim tables ) - -- - if cs._verbatim ~= true then - - local n = nil - for k, v in ipairs( cs ) do - n = k - if type( v ) == 'table' then + if cs._verbatim ~= true + then + for k, v in ipairs( cs ) + do + if type( v ) == 'table' + then inherit( cd, v ) else cd[ #cd + 1 ] = v @@ -2268,53 +3198,55 @@ -- -- Helper to inherit. Inherits one key. -- - inheritKV = function( cd, k, v ) + inheritKV = + function( + cd, -- table copy destination + k, -- key + v -- value + ) -- don't merge inheritance controls - if k == '_merge' or k == '_verbatim' then - return - end + if k == '_merge' or k == '_verbatim' then return end local dtype = type( cd [ k ] ) - if type( v ) == 'table' then - - if dtype == 'nil' then + if type( v ) == 'table' + then + if dtype == 'nil' + then cd[ k ] = { } - inherit( cd[ k ], v ) + inherit( cd[ k ], v, k == 'exitcodes' ) elseif dtype == 'table' and v._merge ~= false then - inherit( cd[ k ], v ) + inherit( cd[ k ], v, k == 'exitcodes' ) end - - elseif dtype == 'nil' then + elseif dtype == 'nil' + then cd[ k ] = v end - end -- - -- Adds a new sync (directory-tree to observe). + -- Adds a new sync. -- - local function add( config ) - - -- workaround for backwards compatibility - -- FIXME: remove when dropping that - if settings ~= settingsSafe then + local function add + ( + config + ) + -- Checks if user overwrote the settings function. + -- ( was Lsyncd <2.1 style ) + if settings ~= settingsSafe + then log( - 'Warn', - 'settings = { ... } is deprecated.\n'.. + 'Error', + 'Do not use settings = { ... }\n'.. ' please use settings{ ... } (without the equal sign)' ) - for k, v in pairs( settings ) do - uSettings[ k ] = v - end - - settings = settingsSafe + os.exit( -1 ) end -- Creates a new config table which inherits all keys/values @@ -2337,15 +3269,19 @@ } -- Lets settings override these values. - for _, v in ipairs( inheritSettings ) do - if uSettings[ v ] then + for _, v in ipairs( inheritSettings ) + do + if uSettings[ v ] + then config[ v ] = uSettings[ v ] end end -- Lets commandline override these values. - for _, v in ipairs( inheritSettings ) do - if clSettings[ v ] then + for _, v in ipairs( inheritSettings ) + do + if clSettings[ v ] + then config[ v ] = clSettings[ v ] end end @@ -2354,20 +3290,22 @@ -- lets the userscript 'prepare' function -- check and complete the config -- - if type( config.prepare ) == 'function' then - + if type( config.prepare ) == 'function' + then -- prepare is given a writeable copy of config config.prepare( config, 4 ) - end - if not config[ 'source' ] then + if not config[ 'source' ] + then local info = debug.getinfo( 3, 'Sl' ) + log( 'Error', info.short_src,':', info.currentline,': source missing from sync.' ) + terminate( -1 ) end @@ -2376,27 +3314,29 @@ -- local realsrc = lsyncd.realdir( config.source ) - if not realsrc then + if not realsrc + then log( 'Error', 'Cannot access source directory: ', config.source ) + terminate( -1 ) end config._source = config.source config.source = realsrc - if - not config.action and - not config.onAttrib and - not config.onCreate and - not config.onModify and - not config.onDelete and - not config.onMove + if not config.action + and not config.onAttrib + and not config.onCreate + and not config.onModify + and not config.onDelete + and not config.onMove then local info = debug.getinfo( 3, 'Sl' ) + log( 'Error', info.short_src, ':', @@ -2413,9 +3353,8 @@ config.monitor or Monitors.default( ) - if - config.monitor ~= 'inotify' and - config.monitor ~= 'fsevents' + if config.monitor ~= 'inotify' + and config.monitor ~= 'fsevents' then local info = debug.getinfo( 3, 'Sl' ) @@ -2431,7 +3370,7 @@ terminate( -1 ) end - --- creates the new sync + -- creates the new sync local s = Sync.new( config ) table.insert( syncsList, s ) @@ -2442,23 +3381,30 @@ -- -- Allows a for-loop to walk through all syncs. -- - local function iwalk( ) + local function iwalk + ( ) return ipairs( syncsList ) end -- -- Returns the number of syncs. -- - local size = function( ) + local size = function + ( ) return #syncsList end -- -- Tests if any sync is interested in a path. -- - local function concerns( path ) - for _, s in ipairs( syncsList ) do - if s:concerns( path ) then + local function concerns + ( + path + ) + for _, s in ipairs( syncsList ) + do + if s:concerns( path ) + then return true end end @@ -2486,12 +3432,17 @@ -- Returns the relative part of absolute path if it -- begins with root -- -local function splitPath( path, root ) - +local function splitPath +( + path, + root +) local rlen = #root + local sp = string.sub( path, 1, rlen ) - if sp == root then + if sp == root + then return string.sub( path, rlen, -1 ) else return nil @@ -2505,8 +3456,8 @@ -- -- All inotify specific implementation is enclosed here. -- -local Inotify = ( function( ) - +local Inotify = ( function +( ) -- -- A list indexed by inotify watch descriptors yielding -- the directories absolute paths. @@ -2528,19 +3479,21 @@ -- -- Stops watching a directory -- - -- path ... absolute path to unwatch - -- core ... if false not actually send the unwatch to the kernel - -- (used in moves which reuse the watch) - -- - local function removeWatch( path, core ) - + local function removeWatch + ( + path, -- absolute path to unwatch + core -- if false not actually send the unwatch to the kernel + -- ( used in moves which reuse the watch ) + ) local wd = pathwds[ path ] - if not wd then + if not wd + then return end - if core then + if core + then lsyncd.inotify.rmwatch( wd ) end @@ -2552,54 +3505,59 @@ -- -- Adds watches for a directory (optionally) including all subdirectories. -- - -- @param path absolute path of directory to observe - -- @param recurse true if recursing into subdirs -- - local function addWatch(path) + local function addWatch + ( + path -- absolute path of directory to observe + ) + log( 'Function', 'Inotify.addWatch( ', path, ' )' ) - log( - 'Function', - 'Inotify.addWatch( ', - path, - ' )' - ) + if not Syncs.concerns( path ) + then + log('Inotify', 'not concerning "', path, '"') - if not Syncs.concerns(path) then - log('Inotify', 'not concerning "',path,'"') return end -- registers the watch local inotifyMode = ( uSettings and uSettings.inotifyMode ) or ''; - local wd = lsyncd.inotify.addwatch( path, inotifyMode) ; + local wd = lsyncd.inotify.addwatch( path, inotifyMode ) ; - if wd < 0 then + if wd < 0 + then log( 'Inotify','Unable to add watch "', path, '"' ) + return end do -- If this watch descriptor is registered already - -- the kernel reuses it since old dir is gone. + -- the kernel reuses it since the old dir is gone. local op = wdpaths[ wd ] - if op and op ~= path then + + if op and op ~= path + then pathwds[ op ] = nil end end pathwds[ path ] = wd + wdpaths[ wd ] = path -- registers and adds watches for all subdirectories local entries = lsyncd.readdir( path ) - if not entries then + if not entries + then return end - for dirname, isdir in pairs( entries ) do - if isdir then + for dirname, isdir in pairs( entries ) + do + if isdir + then addWatch( path .. dirname .. '/' ) end end @@ -2608,21 +3566,26 @@ -- -- Adds a Sync to receive events. -- - -- sync: Object to receive events - -- rootdir: root dir to watch - -- - local function addSync( sync, rootdir ) - if syncRoots[ sync ] then + local function addSync + ( + sync, -- object to receive events. + rootdir -- root dir to watch + ) + if syncRoots[ sync ] + then error( 'duplicate sync in Inotify.addSync()' ) end + syncRoots[ sync ] = rootdir + addWatch( rootdir ) end -- -- Called when an event has occured. -- - local function event( + local function event + ( etype, -- 'Attrib', 'Modify', 'Create', 'Delete', 'Move' wd, -- watch descriptor, matches lsyncd.inotifyadd() isdir, -- true if filename is a directory @@ -2631,15 +3594,18 @@ wd2, -- watch descriptor for target if it's a Move filename2 -- string filename without path of Move target ) - if isdir then + if isdir + then filename = filename .. '/' - if filename2 then + if filename2 + then filename2 = filename2 .. '/' end end - if filename2 then + if filename2 + then log( 'Inotify', 'got event ', @@ -2663,47 +3629,59 @@ -- looks up the watch descriptor id local path = wdpaths[ wd ] - if path then + + if path + then path = path..filename end local path2 = wd2 and wdpaths[ wd2 ] - if path2 and filename2 then + if path2 and filename2 + then path2 = path2..filename2 end - if not path and path2 and etype == 'Move' then + if not path and path2 and etype == 'Move' + then log( 'Inotify', 'Move from deleted directory ', path2, ' becomes Create.' ) + path = path2 + path2 = nil + etype = 'Create' end - if not path then + if not path + then -- this is normal in case of deleted subdirs log( 'Inotify', 'event belongs to unknown watch descriptor.' ) + return end - for sync, root in pairs( syncRoots ) do repeat - + for sync, root in pairs( syncRoots ) + do repeat local relative = splitPath( path, root ) + local relative2 = nil - if path2 then + if path2 + then relative2 = splitPath( path2, root ) end - if not relative and not relative2 then + if not relative and not relative2 + then -- sync is not interested in this dir break -- continue end @@ -2711,32 +3689,43 @@ -- makes a copy of etype to possibly change it local etyped = etype - if etyped == 'Move' then - if not relative2 then + if etyped == 'Move' + then + if not relative2 + then log( 'Normal', 'Transformed Move to Delete for ', sync.config.name ) + etyped = 'Delete' - elseif not relative then + elseif not relative + then relative = relative2 + relative2 = nil + log( 'Normal', 'Transformed Move to Create for ', sync.config.name ) + etyped = 'Create' end end - if isdir then - if etyped == 'Create' then + if isdir + then + if etyped == 'Create' + then addWatch( path ) - elseif etyped == 'Delete' then + elseif etyped == 'Delete' + then removeWatch( path, true ) - elseif etyped == 'Move' then + elseif etyped == 'Move' + then removeWatch( path, false ) addWatch( path2 ) end @@ -2754,7 +3743,8 @@ f:write( 'Inotify watching ', wdpaths:size(), ' directories\n' ) - for wd, path in wdpaths:walk( ) do + for wd, path in wdpaths:walk( ) + do f:write( ' ', wd, ': ', path, '\n' ) end end @@ -2771,6 +3761,7 @@ end)( ) + -- -- Interface to OSX /dev/fsevents -- @@ -2779,25 +3770,24 @@ -- -- All fsevents specific implementation are enclosed here. -- -local Fsevents = ( function( ) - - +local Fsevents = ( function +( ) -- -- A list indexed by syncs yielding -- the root path the sync is interested in. -- local syncRoots = { } - -- -- Adds a Sync to receive events. -- - -- @param sync Object to receive events - -- @param dir dir to watch - -- - local function addSync( sync, dir ) - - if syncRoots[ sync ] then + local function addSync + ( + sync, -- object to receive events + dir -- dir to watch + ) + if syncRoots[ sync ] + then error( 'duplicate sync in Fanotify.addSync()' ) end @@ -2808,58 +3798,65 @@ -- -- Called when an event has occured. -- - local function event( + local function event + ( etype, -- 'Attrib', 'Modify', 'Create', 'Delete', 'Move' isdir, -- true if filename is a directory time, -- time of event path, -- path of file path2 -- path of target in case of 'Move' ) - if isdir then + if isdir + then path = path .. '/' - if path2 then - path2 = path2 .. '/' - end + if path2 then path2 = path2 .. '/' end end - log( - 'Fsevents', - etype, ',', - isdir, ',', - time, ',', - path, ',', - path2 - ) + log( 'Fsevents', etype, ',', isdir, ',', time, ',', path, ',', path2 ) - for _, sync in Syncs.iwalk() do repeat + for _, sync in Syncs.iwalk() + do repeat local root = sync.source -- TODO combine ifs - if not path:starts( root ) then - if not path2 or not path2:starts( root ) then + if not path:starts( root ) + then + if not path2 or not path2:starts( root ) + then break -- continue end end - local relative = splitPath( path, root ) + local relative = splitPath( path, root ) local relative2 - if path2 then + + if path2 + then relative2 = splitPath( path2, root ) end -- possibly change etype for this iteration only local etyped = etype - if etyped == 'Move' then - if not relative2 then - log('Normal', 'Transformed Move to Delete for ', sync.config.name) + + if etyped == 'Move' + then + if not relative2 + then + log( 'Normal', 'Transformed Move to Delete for ', sync.config.name ) + etyped = 'Delete' - elseif not relative then + + elseif not relative + then relative = relative2 + relative2 = nil - log('Normal', 'Transformed Move to Create for ', sync.config.name) + + log( 'Normal', 'Transformed Move to Create for ', sync.config.name ) + etyped = 'Create' end end @@ -2874,7 +3871,10 @@ -- -- Writes a status report about fsevents to a filedescriptor. -- - local function statusReport( f ) + local function statusReport + ( + f + ) -- TODO end @@ -2893,9 +3893,8 @@ -- Holds information about the event monitor capabilities -- of the core. -- -Monitors = ( function( ) - - +Monitors = ( function +( ) -- -- The cores monitor list -- @@ -2905,7 +3904,8 @@ -- -- The default event monitor. -- - local function default( ) + local function default + ( ) return list[ 1 ] end @@ -2914,7 +3914,8 @@ -- Initializes with info received from core -- local function initialize( clist ) - for k, v in ipairs( clist ) do + for k, v in ipairs( clist ) + do list[ k ] = v end end @@ -2978,8 +3979,8 @@ ) local args = { } - while str ~= '' do - + while str ~= '' + do -- break where argument stops local bp = #str @@ -2987,13 +3988,17 @@ local inQuote = false -- tests characters to be space and not within quotes - for i=1, #str do + for i = 1, #str + do local c = string.sub( str, i, i ) - if c == '"' then + if c == '"' + then inQuote = not inQuote - elseif c == ' ' and not inQuote then + elseif c == ' ' and not inQuote + then bp = i - 1 + break end end @@ -3014,41 +4019,49 @@ -- Translates a call to a binary to a lua function. -- TODO this has a little too blocking. -- - local function translateBinary( str ) - + local function translateBinary + ( + str + ) -- splits the string local args = splitStr( str ) -- true if there is a second event local haveEvent2 = false - for ia, iv in ipairs( args ) do - + for ia, iv in ipairs( args ) + do -- a list of arguments this arg is being split into local a = { { true, iv } } -- goes through all translates - for _, v in ipairs( transVars ) do + for _, v in ipairs( transVars ) + do local ai = 1 - while ai <= #a do - if a[ ai ][ 1 ] then + while ai <= #a + do + if a[ ai ][ 1 ] + then local pre, post = string.match( a[ ai ][ 2 ], '(.*)'..v[1]..'(.*)' ) - if pre then - - if v[3] > 1 then + if pre + then + if v[3] > 1 + then haveEvent2 = true end - if pre ~= '' then + if pre ~= '' + then table.insert( a, ai, { true, pre } ) ai = ai + 1 end a[ ai ] = { false, v[ 2 ] } - if post ~= '' then + if post ~= '' + then table.insert( a, ai + 1, { true, post } ) end end @@ -3061,13 +4074,12 @@ local as = '' local first = true - for _, v in ipairs( a ) do - - if not first then - as = as..' .. ' - end + for _, v in ipairs( a ) + do + if not first then as = as..' .. ' end - if v[ 1 ] then + if v[ 1 ] + then as = as .. '"' .. v[ 2 ] .. '"' else as = as .. v[ 2 ] @@ -3080,18 +4092,21 @@ end local ft - if not haveEvent2 then - ft = 'function(event)\n' + + if not haveEvent2 + then + ft = 'function( event )\n' else - ft = 'function(event, event2)\n' + ft = 'function( event, event2 )\n' end ft = ft .. " log('Normal', 'Event ', event.etype, \n" .. " ' spawns action \"".. str.."\"')\n" .. - " spawn(event" + " spawn( event" - for _, v in ipairs( args ) do + for _, v in ipairs( args ) + do ft = ft .. ',\n ' .. v end @@ -3104,40 +4119,45 @@ -- -- Translates a call using a shell to a lua function -- - local function translateShell( str ) - + local function translateShell + ( + str + ) local argn = 1 + local args = { } + local cmd = str + local lc = str -- true if there is a second event local haveEvent2 = false - for _, v in ipairs( transVars ) do - + for _, v in ipairs( transVars ) + do local occur = false cmd = string.gsub( cmd, v[ 1 ], - function( ) + function + ( ) occur = true return '"$' .. argn .. '"' end ) - lc = string.gsub( - lc, - v[1], - ']]..' .. v[2] .. '..[[' - ) + lc = string.gsub( lc, v[1], ']]..' .. v[2] .. '..[[' ) - if occur then + if occur + then argn = argn + 1 + table.insert( args, v[ 2 ] ) - if v[ 3 ] > 1 then + if v[ 3 ] > 1 + then haveEvent2 = true end end @@ -3145,10 +4165,12 @@ end local ft - if not haveEvent2 then - ft = 'function(event)\n' + + if not haveEvent2 + then + ft = 'function( event )\n' else - ft = 'function(event, event2)\n' + ft = 'function( event, event2 )\n' end -- TODO do array joining instead @@ -3157,7 +4179,8 @@ " [[ spawns shell \""..lc.."\"]])\n".. " spawnShell(event, [["..cmd.."]]" - for _, v in ipairs( args ) do + for _, v in ipairs( args ) + do ft = ft..',\n '..v end @@ -3170,28 +4193,28 @@ -- -- Writes a lua function for a layer 3 user script. -- - local function translate( str ) - -- trim spaces + local function translate + ( + str + ) + -- trims spaces str = string.match( str, '^%s*(.-)%s*$' ) local ft - if string.byte( str, 1, 1 ) == 47 then + + if string.byte( str, 1, 1 ) == 47 + then -- starts with / ft = translateBinary( str ) - elseif string.byte( str, 1, 1 ) == 94 then + elseif string.byte( str, 1, 1 ) == 94 + then -- starts with ^ ft = translateShell( str:sub( 2, -1 ) ) else ft = translateShell( str ) end - log( - 'FWrite', - 'translated "', - str, - '" to \n', - ft - ) + log( 'FWrite', 'translated "', str, '" to \n', ft ) return ft end @@ -3202,17 +4225,15 @@ -- return { translate = translate } - end )( ) -- --- Writes a status report file at most every [statusintervall] seconds. +-- Writes a status report file at most every 'statusintervall' seconds. -- -local StatusFile = ( function( ) - - +local StatusFile = ( function +( ) -- -- Timestamp when the status file has been written. -- @@ -3228,7 +4249,8 @@ -- -- Returns the alarm when the status file should be written- -- - local function getAlarm() + local function getAlarm + ( ) return alarm end @@ -3236,46 +4258,33 @@ -- -- Called to check if to write a status file. -- - local function write( timestamp ) - - log( - 'Function', - 'write( ', - timestamp, - ' )' - ) + local function write + ( + timestamp + ) + log( 'Function', 'write( ', timestamp, ' )' ) -- -- takes care not write too often -- - if uSettings.statusInterval > 0 then - + if uSettings.statusInterval > 0 + then -- already waiting? - if alarm and timestamp < alarm then - log( - 'Statusfile', - 'waiting(', - timestamp, - ' < ', - alarm, - ')' - ) + if alarm and timestamp < alarm + then + log( 'Statusfile', 'waiting(', timestamp, ' < ', alarm, ')' ) + return end -- determines when a next write will be possible - if not alarm then - - local nextWrite = - lastWritten and timestamp + - uSettings.statusInterval + if not alarm + then + local nextWrite = lastWritten and timestamp + uSettings.statusInterval - if nextWrite and timestamp < nextWrite then - log( - 'Statusfile', - 'setting alarm: ', - nextWrite - ) + if nextWrite and timestamp < nextWrite + then + log( 'Statusfile', 'setting alarm: ', nextWrite ) alarm = nextWrite return @@ -3290,7 +4299,8 @@ local f, err = io.open( uSettings.statusFile, 'w' ) - if not f then + if not f + then log( 'Error', 'Cannot open status file "' .. @@ -3303,12 +4313,15 @@ f:write( 'Lsyncd status report at ', os.date( ), '\n\n' ) - for i, s in Syncs.iwalk( ) do + for i, s in Syncs.iwalk( ) + do s:statusReport( f ) + f:write( '\n' ) end Inotify.statusReport( f ) + f:close( ) end @@ -3327,60 +4340,70 @@ -- -- Lets userscripts make their own alarms. -- -local UserAlarms = ( function( ) - +local UserAlarms = ( function +( ) local alarms = { } - -- -- Calls the user function at timestamp. -- - local function alarm( timestamp, func, extra ) - + local function alarm + ( + timestamp, + func, + extra + ) local idx - for k, v in ipairs( alarms ) do - if timestamp < v.timestamp then + + for k, v in ipairs( alarms ) + do + if timestamp < v.timestamp + then idx = k + break end end - local a = { + local a = + { timestamp = timestamp, func = func, extra = extra } - if idx then + if idx + then table.insert( alarms, idx, a ) else table.insert( alarms, a ) end - end -- -- Retrieves the soonest alarm. -- - local function getAlarm( ) - - if #alarms == 0 then + local function getAlarm + ( ) + if #alarms == 0 + then return false else return alarms[1].timestamp end - end -- -- Calls user alarms. -- - local function invoke( timestamp ) - while - #alarms > 0 and - alarms[ 1 ].timestamp <= timestamp + local function invoke + ( + timestamp + ) + while #alarms > 0 + and alarms[ 1 ].timestamp <= timestamp do alarms[ 1 ].func( alarms[ 1 ].timestamp, alarms[ 1 ].extra ) table.remove( alarms, 1 ) @@ -3397,7 +4420,6 @@ invoke = invoke } - end )( ) --============================================================================ @@ -3428,16 +4450,21 @@ -- -- Logs a backtrace -- -function runner.callError( message ) - log('Error', 'in Lua: ', message ) +function runner.callError +( + message +) + log( 'Error', 'in Lua: ', message ) -- prints backtrace local level = 2 - while true do + while true + do local info = debug.getinfo( level, 'Sl' ) - if not info then + if not info + then terminate( -1 ) end @@ -3458,18 +4485,22 @@ -- Called from core whenever a child process has finished and -- the zombie process was collected by core. -- -function runner.collectProcess( pid, exitcode ) - +function runner.collectProcess +( + pid, -- process id + exitcode -- exitcode +) processCount = processCount - 1 - if processCount < 0 then + if processCount < 0 + then error( 'negative number of processes!' ) end - for _, s in Syncs.iwalk() do - if s:collect(pid, exitcode) then return end + for _, s in Syncs.iwalk( ) + do + if s:collect( pid, exitcode ) then return end end - end -- @@ -3484,11 +4515,12 @@ function runner.cycle( timestamp -- the current kernel time (in jiffies) ) + log( 'Function', 'cycle( ', timestamp, ' )' ) - if lsyncdStatus == 'fade' then - - if processCount > 0 then - + if lsyncdStatus == 'fade' + then + if processCount > 0 + then if lastReportedWaiting == false or timestamp >= lastReportedWaiting + 60 @@ -3505,12 +4537,12 @@ return true else - return false end end - if lsyncdStatus ~= 'run' then + if lsyncdStatus ~= 'run' + then error( 'runner.cycle() called while not running!' ) end @@ -3519,22 +4551,22 @@ -- if possibly. But only let Syncs invoke actions if -- not at global limit -- - if - not uSettings.maxProcesses or - processCount < uSettings.maxProcesses + if not uSettings.maxProcesses + or processCount < uSettings.maxProcesses then local start = Syncs.getRound( ) local ir = start repeat - local s = Syncs.get( ir ) + s:invokeActions( timestamp ) - ir = ir + 1 - if ir > Syncs.size( ) then + ir = ir + 1 + if ir > Syncs.size( ) + then ir = 1 end until ir == start @@ -3544,7 +4576,8 @@ UserAlarms.invoke( timestamp ) - if uSettings.statusFile then + if uSettings.statusFile + then StatusFile.write( timestamp ) end @@ -3622,119 +4655,143 @@ -- -- second paramter is the function to call -- - local options = { - + local options = + { -- log is handled by core already. delay = - { - 1, - function( secs ) - clSettings.delay = secs + 0 - end - }, + { + 1, + function + ( + secs + ) + clSettings.delay = secs + 0 + end + }, insist = - { - 0, - function( ) - clSettings.insist = true - end - }, + { + 0, + function + ( ) + clSettings.insist = true + end + }, log = - { - 1, - nil - }, + { + 1, + nil + }, logfile = - { - 1, - function( file ) - clSettings.logfile = file - end - }, + { + 1, + function + ( + file + ) + clSettings.logfile = file + end + }, monitor = - { - -1, - function( monitor ) - if not monitor then - io.stdout:write( 'This Lsyncd supports these monitors:\n' ) - for _, v in ipairs(Monitors.list) do - io.stdout:write(' ',v,'\n') - end + { + -1, + function + ( + monitor + ) + if not monitor + then + io.stdout:write( 'This Lsyncd supports these monitors:\n' ) + for _, v in ipairs( Monitors.list ) + do + io.stdout:write( ' ', v, '\n' ) + end - io.stdout:write('\n') + io.stdout:write('\n') - lsyncd.terminate(-1) - else - clSettings.monitor = monitor - end + lsyncd.terminate( -1 ) + else + clSettings.monitor = monitor end - }, + end + }, nodaemon = - { - 0, - function( ) - clSettings.nodaemon = true - end - }, + { + 0, + function + ( ) + clSettings.nodaemon = true + end + }, pidfile = - { - 1, - function( file ) - clSettings.pidfile=file - end - }, + { + 1, + function + ( + file + ) + clSettings.pidfile=file + end + }, - rsync = - { - 2, - function( src, trg ) - clSettings.syncs = clSettings.syncs or { } - table.insert( - clSettings.syncs, - { 'rsync', src, trg } - ) - end - }, + rsync = + { + 2, + function + ( + src, + trg + ) + clSettings.syncs = clSettings.syncs or { } + table.insert( clSettings.syncs, { 'rsync', src, trg } ) + end + }, rsyncssh = - { - 3, - function( src, host, tdir ) - clSettings.syncs = clSettings.syncs or { } - table.insert( - clSettings.syncs, - { 'rsyncssh', src, host, tdir } - ) - end - }, + { + 3, + function + ( + src, + host, + tdir + ) + clSettings.syncs = clSettings.syncs or { } + + table.insert( clSettings.syncs, { 'rsyncssh', src, host, tdir } ) + end + }, direct = - { - 2, - function( src, trg ) - clSettings.syncs = clSettings.syncs or { } - table.insert( - clSettings.syncs, - { 'direct', src, trg } - ) - end - }, + { + 2, + function + ( + src, + trg + ) + clSettings.syncs = clSettings.syncs or { } + + table.insert( clSettings.syncs, { 'direct', src, trg } ) + end + }, version = - { - 0, - function( ) - io.stdout:write( 'Version: ', lsyncd_version, '\n' ) - os.exit( 0 ) - end - } + { + 0, + function + ( ) + io.stdout:write( 'Version: ', lsyncd_version, '\n' ) + + os.exit( 0 ) + end + } } -- non-opts is filled with all args that were no part dash options @@ -3742,14 +4799,17 @@ local nonopts = { } local i = 1 - while i <= #args do + while i <= #args + do local a = args[ i ] - if a:sub( 1, 1 ) ~= '-' then + if a:sub( 1, 1 ) ~= '-' + then table.insert( nonopts, args[ i ] ) else - if a:sub( 1, 2 ) == '--' then + if a:sub( 1, 2 ) == '--' + then a = a:sub( 3 ) else a = a:sub( 2 ) @@ -3757,70 +4817,67 @@ local o = options[ a ] - if not o then - log( - 'Error', - 'unknown option command line option ', - args[i] - ) + if not o + then + log( 'Error', 'unknown option command line option ', args[ i ] ) + os.exit( -1 ) end - if o[ 1 ] >= 0 and i + o[ 1 ] > #args then + if o[ 1 ] >= 0 and i + o[ 1 ] > #args + then log( 'Error', a ,' needs ', o[ 1 ],' arguments' ) + os.exit( -1 ) - elseif o[1] < 0 then + elseif o[1] < 0 + then o[ 1 ] = -o[ 1 ] end - if o[ 2 ] then - if o[ 1 ] == 0 then + if o[ 2 ] + then + if o[ 1 ] == 0 + then o[ 2 ]( ) - elseif o[ 1 ] == 1 then - o[ 2 ]( args[i + 1] ) - elseif o[ 1 ] == 2 then - o[ 2 ]( args[i + 1], args[i + 2] ) - elseif o[ 1 ] == 3 then - o[ 2 ]( args[i + 1], args[i + 2], args[i + 3] ) + elseif o[ 1 ] == 1 + then + o[ 2 ]( args[ i + 1] ) + elseif o[ 1 ] == 2 + then + o[ 2 ]( args[ i + 1], args[ i + 2] ) + elseif o[ 1 ] == 3 + then + o[ 2 ]( args[ i + 1], args[ i + 2], args[ i + 3] ) end end + i = i + o[1] end - i = i + 1 + i = i + 1 end - if clSettings.syncs then + if clSettings.syncs + then + if #nonopts ~= 0 + then + log( 'Error', 'There cannot be command line syncs and a config file together.' ) - if #nonopts ~= 0 then - log( - 'Error', - 'There cannot be command line syncs and config file together.' - ) os.exit( -1 ) end - else - - if #nonopts == 0 then - + if #nonopts == 0 + then runner.help( args[ 0 ] ) - - elseif #nonopts == 1 then - + elseif #nonopts == 1 + then return nonopts[ 1 ] - else - -- TODO make this possible - log( - 'Error', - 'There can only be one config file in command line.' - ) - os.exit( -1 ) + log( 'Error', 'There can only be one config file in the command line.' ) + os.exit( -1 ) end - end end @@ -3834,17 +4891,17 @@ -- function runner.initialize( firstTime ) - if settings ~= settingsSafe then + -- Checks if user overwrote the settings function. + -- ( was Lsyncd <2.1 style ) + if settings ~= settingsSafe + then log( - 'Warn', - 'settings = { ... } is deprecated.\n'.. - ' please use settings{ ... } (without the equal sign)' + 'Error', + 'Do not use settings = { ... }\n'.. + ' please use settings{ ... } ( without the equal sign )' ) - for k, v in pairs( settings ) do - uSettings[ k ] = v - end - + os.exit( -1 ) end lastReportedWaiting = false @@ -3855,30 +4912,12 @@ lockGlobals( ) -- - -- copies simple settings with numeric keys to 'key = true' settings. - -- - -- FIXME this can be removed when - -- Lsyncd 2.0.x backwards compatibility is dropped - -- - for k, v in ipairs( uSettings ) do - - if uSettings[ v ] then - log( - 'Error', - 'Double setting "' .. v.. '"' - ) - os.exit( -1 ) - end - - uSettings[ v ]= true - - end - - -- -- all command line settings overwrite config file settings -- - for k, v in pairs( clSettings ) do - if k ~= 'syncs' then + for k, v in pairs( clSettings ) + do + if k ~= 'syncs' + then uSettings[ k ] = v end end @@ -3886,81 +4925,81 @@ -- -- implicitly forces 'insist' on Lsyncd resets. -- - if not firstTime then + if not firstTime + then uSettings.insist = true end -- -- adds syncs specified by command line. -- - if clSettings.syncs then - - for _, s in ipairs( clSettings.syncs ) do - - if s[ 1 ] == 'rsync' then - + if clSettings.syncs + then + for _, s in ipairs( clSettings.syncs ) + do + if s[ 1 ] == 'rsync' + then sync{ default.rsync, source = s[ 2 ], target = s[ 3 ] } - - elseif s[ 1 ] == 'rsyncssh' then - + elseif s[ 1 ] == 'rsyncssh' + then sync{ default.rsyncssh, source = s[ 2 ], host = s[ 3 ], targetdir=s[ 4 ] } - - elseif s[ 1 ] == 'direct' then + elseif s[ 1 ] == 'direct' + then sync{ default.direct, source=s[ 2 ], target=s[ 3 ] } - end - end - end - if uSettings.nodaemon then + if uSettings.nodaemon + then lsyncd.configure( 'nodaemon' ) end - if uSettings.logfile then + if uSettings.logfile + then lsyncd.configure( 'logfile', uSettings.logfile ) end - if uSettings.logident then + if uSettings.logident + then lsyncd.configure( 'logident', uSettings.logident ) end - if uSettings.logfacility then + if uSettings.logfacility + then lsyncd.configure( 'logfacility', uSettings.logfacility ) end - if uSettings.pidfile then + if uSettings.pidfile + then lsyncd.configure( 'pidfile', uSettings.pidfile ) end -- -- Transfers some defaults to uSettings -- - if uSettings.statusInterval == nil then + if uSettings.statusInterval == nil + then uSettings.statusInterval = default.statusInterval end -- makes sure the user gave Lsyncd anything to do - if Syncs.size() == 0 then - - log( - 'Error', - 'Nothing to watch!' - ) + if Syncs.size() == 0 + then + log( 'Error', 'Nothing to watch!' ) os.exit( -1 ) end @@ -3970,7 +5009,8 @@ lsyncd.configure( 'running' ); - local ufuncs = { + local ufuncs = + { 'onAttrib', 'onCreate', 'onDelete', @@ -3980,102 +5020,96 @@ } -- translates layer 3 scripts - for _, s in Syncs.iwalk() do - + for _, s in Syncs.iwalk() + do -- checks if any user functions is a layer 3 string. local config = s.config - for _, fn in ipairs(ufuncs) do - - if type(config[fn]) == 'string' then - - local ft = functionWriter.translate(config[fn]) - config[fn] = assert(loadstring('return '..ft))() + for _, fn in ipairs( ufuncs ) + do + if type(config[fn]) == 'string' + then + local ft = functionWriter.translate( config[ fn ] ) + config[ fn ] = assert( load( 'return '..ft ) )( ) end - end end -- runs through the Syncs created by users - for _, s in Syncs.iwalk( ) do - - if s.config.monitor == 'inotify' then - + for _, s in Syncs.iwalk( ) + do + if s.config.monitor == 'inotify' + then Inotify.addSync( s, s.source ) - - elseif s.config.monitor == 'fsevents' then - + elseif s.config.monitor == 'fsevents' + then Fsevents.addSync( s, s.source ) - else - error( 'sync ' .. s.config.name .. ' has no known event monitor interface.' ) - end -- if the sync has an init function, the init delay -- is stacked which causes the init function to be called. - if s.config.init then - + if s.config.init + then s:addInitDelay( ) - end end - end -- -- Called by core to query the soonest alarm. -- --- @return false ... no alarm, core can in untimed sleep, or +-- @return false ... no alarm, core can go in untimed sleep -- true ... immediate action -- times ... the alarm time (only read if number is 1) -- -function runner.getAlarm( ) +function runner.getAlarm +( ) + log( 'Function', 'getAlarm( )' ) - if lsyncdStatus ~= 'run' then - return false - end + if lsyncdStatus ~= 'run' then return false end local alarm = false -- -- Checks if 'a' is sooner than the 'alarm' up-value. -- - local function checkAlarm( a ) - - if a == nil then - error('got nil alarm') - end + local function checkAlarm + ( + a -- alarm time + ) + if a == nil then error( 'got nil alarm' ) end - if alarm == true or not a then + if alarm == true or not a + then -- 'alarm' is already immediate or -- a not a new alarm return end -- sets 'alarm' to a if a is sooner - if not alarm or a < alarm then + if not alarm or a < alarm + then alarm = a end - end -- -- checks all syncs for their earliest alarm, -- but only if the global process limit is not yet reached. -- - if - not uSettings.maxProcesses or - processCount < uSettings.maxProcesses + if not uSettings.maxProcesses + or processCount < uSettings.maxProcesses then - for _, s in Syncs.iwalk( ) do - checkAlarm( s:getAlarm ( )) + for _, s in Syncs.iwalk( ) + do + checkAlarm( s:getAlarm( ) ) end else log( @@ -4090,14 +5124,9 @@ -- checks for an userAlarm checkAlarm( UserAlarms.getAlarm( ) ) - log( - 'Alarm', - 'runner.getAlarm returns: ', - alarm - ) + log( 'Alarm', 'runner.getAlarm returns: ', alarm ) return alarm - end @@ -4110,12 +5139,15 @@ -- -- Collector for every child process that finished in startup phase -- -function runner.collector( +function runner.collector +( pid, -- pid of the child process exitcode -- exitcode of the child process ) - if exitcode ~= 0 then - log('Error', 'Startup process',pid,' failed') + if exitcode ~= 0 + then + log( 'Error', 'Startup process', pid, ' failed' ) + terminate( -1 ) end @@ -4125,54 +5157,41 @@ -- -- Called by core when an overflow happened. -- -function runner.overflow( ) - - log( - 'Normal', - '--- OVERFLOW in event queue ---' - ) +function runner.overflow +( ) + log( 'Normal', '--- OVERFLOW in event queue ---' ) lsyncdStatus = 'fade' - end -- -- Called by core on a hup signal. -- -function runner.hup( ) - - log( - 'Normal', - '--- HUP signal, resetting ---' - ) +function runner.hup +( ) + log( 'Normal', '--- HUP signal, resetting ---' ) lsyncdStatus = 'fade' - end -- -- Called by core on a term signal. -- -function runner.term( sigcode ) - - local sigtexts = { - [ 2 ] = - 'INT', - - [ 15 ] = - 'TERM' +function runner.term +( + sigcode -- signal code +) + local sigtexts = + { + [ 2 ] = 'INT', + [ 15 ] = 'TERM' }; local sigtext = sigtexts[ sigcode ]; - if not sigtext then - sigtext = 'UNKNOWN' - end + if not sigtext then sigtext = 'UNKNOWN' end - log( - 'Normal', - '--- ', sigtext, ' signal, fading ---' - ) + log( 'Normal', '--- ', sigtext, ' signal, fading ---' ) lsyncdStatus = 'fade' @@ -4187,17 +5206,16 @@ -- -- Returns an Inlet to that sync. -- -function sync( opts ) - - if lsyncdStatus ~= 'init' then - error( - 'Sync can only be created during initialization.', - 2 - ) +function sync +( + opts +) + if lsyncdStatus ~= 'init' + then + error( 'Sync can only be created during initialization.', 2 ) end return Syncs.add( opts ).inlet - end @@ -4211,60 +5229,51 @@ binary, -- binary to call ... -- arguments ) - if - agent == nil or - type( agent ) ~= 'table' + if agent == nil + or type( agent ) ~= 'table' then - error( - 'spawning with an invalid agent', - 2 - ) + error( 'spawning with an invalid agent', 2 ) end - if lsyncdStatus == 'fade' then - log( - 'Normal', - 'ignored process spawning while fading' - ) + if lsyncdStatus == 'fade' + then + log( 'Normal', 'ignored process spawning while fading' ) return end - if type( binary ) ~= 'string' then - error( - 'calling spawn(agent, binary, ...): binary is not a string', - 2 - ) + if type( binary ) ~= 'string' + then + error( 'calling spawn(agent, binary, ...): binary is not a string', 2 ) end local dol = InletFactory.getDelayOrList( agent ) - if not dol then - error( - 'spawning with an unknown agent', - 2 - ) + if not dol + then + error( 'spawning with an unknown agent', 2 ) end -- -- checks if a spawn is called on an already active event -- - if dol.status then - + if dol.status + then -- is an event - if dol.status ~= 'wait' then + if dol.status ~= 'wait' + then error('spawn() called on an non-waiting event', 2) end - else -- is a list - - for _, d in ipairs(dol) do - if d.status ~= 'wait' and d.status ~= 'block' then - error('spawn() called on an non-waiting event list', 2) + for _, d in ipairs( dol ) + do + if d.status ~= 'wait' + and d.status ~= 'block' + then + error( 'spawn() called on an non-waiting event list', 2 ) end end - end -- @@ -4272,12 +5281,12 @@ -- local pid = lsyncd.exec( binary, ... ) - if pid and pid > 0 then - + if pid and pid > 0 + then processCount = processCount + 1 - if - uSettings.maxProcesses and - processCount > uSettings.maxProcesses + + if uSettings.maxProcesses + and processCount > uSettings.maxProcesses then error( 'Spawned too much processes!' ) end @@ -4285,67 +5294,61 @@ local sync = InletFactory.getSync( agent ) -- delay or list - if dol.status then - + if dol.status + then -- is a delay - dol.status = 'active' - sync.processes[ pid ] = dol + dol:setActive( ) + sync.processes[ pid ] = dol else - -- is a list - for _, d in ipairs( dol ) do - d.status = 'active' + for _, d in ipairs( dol ) + do + d:setActive( ) end - sync.processes[ pid ] = dol + sync.processes[ pid ] = dol end - end end -- -- Spawns a child process using the default shell. -- -function spawnShell( +function spawnShell +( agent, -- the delay(list) to spawn the command for command, -- the shell command ... -- additonal arguments ) - return spawn( - agent, - '/bin/sh', - '-c', - command, - '/bin/sh', - ... - ) + return spawn( agent, '/bin/sh', '-c', command, '/bin/sh', ... ) end ------ --- Observes a filedescriptor + -- -function observefd( +-- Observes a filedescriptor. +-- +function observefd +( fd, -- file descriptor ready, -- called when fd is ready to be read writey -- called when fd is ready to be written ) - return lsyncd.observe_fd( - fd, - ready, - writey - ) + return lsyncd.observe_fd( fd, ready, writey ) end + -- --- Stops observeing a filedescriptor +-- Stops observeing a filedescriptor. -- -function nonobservefd( +function nonobservefd +( fd -- file descriptor ) return lsyncd.nonobserve_fd( fd ) end + -- -- Calls func at timestamp. -- @@ -4354,44 +5357,70 @@ -- alarm = UserAlarms.alarm + -- -- Comfort routine also for user. -- Returns true if 'String' starts with 'Start' -- -function string.starts( String, Start ) - +function string.starts +( + String, + Start +) return string.sub( String, 1, #Start )==Start - end + -- -- Comfort routine also for user. -- Returns true if 'String' ends with 'End' -- -function string.ends( String, End ) - +function string.ends +( + String, + End +) return End == '' or string.sub( String, -#End ) == End - end + -- --- The Lsyncd 2.1 settings call +-- The settings call -- -function settings( a1 ) +function settings +( + a1 -- a string for getting a setting + -- or a table of key/value pairs to set these settings +) + -- if a1 is a string this is a get operation - if type( a1 ) == 'string' then + if type( a1 ) == 'string' + then return uSettings[ a1 ] end -- if its a table it sets all the value of the bale - for k, v in pairs( a1 ) do - if type( k ) ~= 'number' then + for k, v in pairs( a1 ) + do + if type( k ) ~= 'number' + then + if not settingsCheckgauge[ k ] + then + error( 'setting "'..k..'" unknown.', 2 ) + end + uSettings[ k ] = v else + if not settingsCheckgauge[ v ] + then + error( 'setting "'..v..'" unknown.', 2 ) + end + uSettings[ v ] = true end end end + settingsSafe = settings -- diff -Nru lsyncd-2.1.6/README.md lsyncd-2.2.3/README.md --- lsyncd-2.1.6/README.md 2015-10-15 06:29:47.000000000 +0000 +++ lsyncd-2.2.3/README.md 2018-03-09 12:39:11.000000000 +0000 @@ -6,7 +6,9 @@ Rsync+ssh is an advanced action configuration that uses a SSH to act file and directory moves directly on the target instead of re-transmitting the move destination over the wire. -Fine-grained customization can be achieved through the config file. Custom action configs can even be written from scratch in cascading layers ranging from shell scripts to code written in the [Lua language](http://www.lua.org/). This way simple, powerful and flexible configurations can be acheived. See the manual for details [Lsyncd21Manual](https://github.com/axkibe/lsyncd/wiki/Manual-to-Lsyncd-2.1.x) +Fine-grained customization can be achieved through the config file. Custom action configs can even be written from scratch in cascading layers ranging from shell scripts to code written in the [Lua language](http://www.lua.org/). This way simple, powerful and flexible configurations can be acheived. See [the manual](https://axkibe.github.io/lsyncd/) for details. + +Lsyncd 2.2.1 requires rsync >= 3.1 on all source and target machines. License: [GPLv2](http://www.fsf.org/licensing/licenses/info/GPLv2.html) or any later GPL version. @@ -20,6 +22,8 @@ [GlusterFS](http://www.gluster.org) and [BindFS](http://bindfs.org/) use a FUSE-Filesystem to interject kernel/userspace filesystem events. +[Mirror](https://github.com/stephenh/mirror) is an asynchronous synchronisation tool that takes use of the inotify notifications much like Lsyncd. The main differences are: it is developed specifically for master-master use, thus running on a daemon on both systems, uses its own transportation layer instead of rsync and is Java instead of Lsyncd's C core with Lua scripting. + Lsyncd usage examples --------------------- ```lsyncd -rsync /home remotehost.org::share/``` @@ -31,8 +35,9 @@ This will also rsync/watch '/home', but it uses a ssh connection to make moves local on the remotehost instead of re-transmitting the moved file over the wire. -Some more complicated examples, tips and tricks you can find in the [Lsyncd21Manual](https://github.com/axkibe/lsyncd/wiki/Manual-to-Lsyncd-2.1.x). +Some more complicated examples, tips and tricks you can find in the [manual](https://axkibe.github.io/lsyncd/). Disclaimer ---------- Besides the usual disclaimer in the license, we want to specifically emphasize that the authors, and any organizations the authors are associated with, can not be held responsible for data-loss caused by possible malfunctions of Lsyncd. + diff -Nru lsyncd-2.1.6/tests/churn-direct.lua lsyncd-2.2.3/tests/churn-direct.lua --- lsyncd-2.1.6/tests/churn-direct.lua 2015-10-15 06:29:47.000000000 +0000 +++ lsyncd-2.2.3/tests/churn-direct.lua 2018-03-09 12:39:11.000000000 +0000 @@ -1,8 +1,8 @@ -#!/usr/bin/lua -- a heavy duty test. -- makes thousends of random changes to the source tree require( 'posix' ) + dofile( 'tests/testlib.lua' ) cwriteln( '****************************************************************' ) @@ -12,7 +12,7 @@ local tdir, srcdir, trgdir = mktemps( ) -- makes some startup data -churn( srcdir, 10 ) +churn( srcdir, 10, init ) local logs = { } --local logs = {'-log', 'Exec', '-log', 'Delay' } @@ -20,13 +20,13 @@ './lsyncd', '-nodaemon', '-direct', srcdir, trgdir, - unpack( logs ) + table.unpack( logs ) ) cwriteln( 'waiting for Lsyncd to startup' ) posix.sleep( 1 ) -churn( srcdir, 500 ) +churn( srcdir, 500, false ) cwriteln( 'waiting for Lsyncd to finish its jobs.' ) posix.sleep( 10 ) diff -Nru lsyncd-2.1.6/tests/churn-rsync.lua lsyncd-2.2.3/tests/churn-rsync.lua --- lsyncd-2.1.6/tests/churn-rsync.lua 2015-10-15 06:29:47.000000000 +0000 +++ lsyncd-2.2.3/tests/churn-rsync.lua 2018-03-09 12:39:11.000000000 +0000 @@ -1,6 +1,6 @@ -#!/usr/bin/lua -- a heavy duty test. -- makes thousends of random changes to the source tree + require( 'posix' ) dofile( 'tests/testlib.lua' ) @@ -11,7 +11,7 @@ local tdir, srcdir, trgdir = mktemps( ) -- makes some startup data -churn( srcdir, 100 ) +churn( srcdir, 100, true ) local logs = { } -- logs = { "-log", "Delay", "-log", "Fsevents" } @@ -20,14 +20,14 @@ '-nodaemon', '-delay', '5', '-rsync', srcdir, trgdir, - unpack( logs ) + table.unpack( logs ) ) cwriteln( 'waiting for Lsyncd to startup' ) posix.sleep( 1 ) -churn( srcdir, 500 ) +churn( srcdir, 500, false ) cwriteln( 'waiting for Lsyncd to finish its jobs.' ) @@ -48,7 +48,7 @@ cwriteln( 'Signal terminating diff = ', code ) end -if exitcode ~= 0 +if code ~= 0 then os.exit( 1 ) else diff -Nru lsyncd-2.1.6/tests/churn-rsyncssh.lua lsyncd-2.2.3/tests/churn-rsyncssh.lua --- lsyncd-2.1.6/tests/churn-rsyncssh.lua 2015-10-15 06:29:47.000000000 +0000 +++ lsyncd-2.2.3/tests/churn-rsyncssh.lua 2018-03-09 12:39:11.000000000 +0000 @@ -1,4 +1,3 @@ -#!/usr/bin/lua -- a heavy duty test. -- makes thousends of random changes to the source tree @@ -15,7 +14,7 @@ local tdir, srcdir, trgdir = mktemps() -- makes some startup data -churn( srcdir, 100 ) +churn( srcdir, 5, true ) local logs = {} logs = { '-log', 'Delay' } @@ -29,20 +28,23 @@ srcdir, 'localhost', trgdir, - unpack(logs) + table.unpack(logs) ) cwriteln( 'waiting for Lsyncd to startup' ) posix.sleep( 1 ) -churn( srcdir, 100 ) +churn( srcdir, 150, false ) cwriteln( 'waiting for Lsyncd to finish its jobs.' ) posix.sleep( 10 ) cwriteln( 'killing the Lsyncd daemon' ) + posix.kill(pid) -local _, exitmsg, lexitcode = posix.wait(lpid) + +local _, exitmsg, lexitcode = posix.wait( lpid ) + cwriteln( 'Exitcode of Lsyncd = ', exitmsg, ' ', lexitcode ) _, result, code = os.execute( 'diff -r ' .. srcdir .. ' ' .. trgdir ) @@ -54,7 +56,7 @@ cwriteln( 'Signal terminating diff = ', code ) end -if exitcode ~= 0 +if code ~= 0 then os.exit( 1 ) else diff -Nru lsyncd-2.1.6/tests/exclude-rsync.lua lsyncd-2.2.3/tests/exclude-rsync.lua --- lsyncd-2.1.6/tests/exclude-rsync.lua 2015-10-15 06:29:47.000000000 +0000 +++ lsyncd-2.2.3/tests/exclude-rsync.lua 2018-03-09 12:39:11.000000000 +0000 @@ -1,124 +1,152 @@ -#!/usr/bin/lua -require("posix") -dofile("tests/testlib.lua") - -cwriteln("****************************************************************") -cwriteln(" Testing excludes ") -cwriteln("****************************************************************") - -local tdir, srcdir, trgdir = mktemps() -local logfile = tdir .. "log" -local cfgfile = tdir .. "config.lua" +require( 'posix' ) +dofile( 'tests/testlib.lua' ) + +cwriteln( '****************************************************************' ) +cwriteln( ' Testing excludes (rsync)' ) +cwriteln(' ****************************************************************' ) + +local tdir, srcdir, trgdir = mktemps( ) +local logfile = tdir .. 'log' +local cfgfile = tdir .. 'config.lua' local range = 5 -local log = {"-log", "all"} +local log = { '-log', 'all' } writefile(cfgfile, [[ -settings = { +settings { logfile = "]]..logfile..[[", nodaemon = true, - delay = 3, } sync { default.rsync, source = "]]..srcdir..[[", target = "]]..trgdir..[[", + delay = 3, exclude = { "erf", "/eaf", "erd/", "/ead/", }, -}]]); +}]]) -- writes all files -local function writefiles() - posix.mkdir(srcdir .. "d"); - writefile(srcdir .. "erf", "erf"); - writefile(srcdir .. "eaf", "erf"); - writefile(srcdir .. "erd", "erd"); - writefile(srcdir .. "ead", "ead"); - writefile(srcdir .. "d/erf", "erf"); - writefile(srcdir .. "d/eaf", "erf"); - writefile(srcdir .. "d/erd", "erd"); - writefile(srcdir .. "d/ead", "ead"); -end - --- test if the filename exists, fails if this is different to expect -local function testfile(filename, expect) - local stat, err = posix.stat(filename) - if stat and not expect then - cwriteln("failure: ",filename," should be excluded"); - os.exit(1); +local function writefiles +( ) + posix.mkdir( srcdir .. 'd' ) + writefile( srcdir .. 'erf', 'erf' ) + writefile( srcdir .. 'eaf', 'erf' ) + writefile( srcdir .. 'erd', 'erd' ) + writefile( srcdir .. 'ead', 'ead' ) + writefile( srcdir .. 'd/erf', 'erf' ) + writefile( srcdir .. 'd/eaf', 'erf' ) + writefile( srcdir .. 'd/erd', 'erd' ) + writefile( srcdir .. 'd/ead', 'ead' ) +end + +-- +-- Tests if the filename exists +-- fails if this is different to expect. +-- +local function testfile +( + filename, + expect +) + local stat, err = posix.stat( filename ) + + if stat and not expect + then + cwriteln( 'failure: ', filename, ' should be excluded') + + os.exit( 1 ) end - if not stat and expect then - cwriteln("failure: ",filename," should not be excluded"); - os.exit(1); + + if not stat and expect + then + cwriteln( 'failure: ', filename, ' should not be excluded' ) + os.exit( 1 ) end end -- test all files -local function testfiles() - testfile(trgdir .. "erf", false); - testfile(trgdir .. "eaf", false); - testfile(trgdir .. "erd", true); - testfile(trgdir .. "ead", true); - testfile(trgdir .. "d/erf", false); - testfile(trgdir .. "d/eaf", true); - testfile(trgdir .. "d/erd", true); - testfile(trgdir .. "d/ead", true); +local function testfiles +( ) + testfile( trgdir .. 'erf', false ) + testfile( trgdir .. 'eaf', false ) + testfile( trgdir .. 'erd', true ) + testfile( trgdir .. 'ead', true ) + testfile( trgdir .. 'd/erf', false ) + testfile( trgdir .. 'd/eaf', true ) + testfile( trgdir .. 'd/erd', true ) + testfile( trgdir .. 'd/ead', true ) end -cwriteln( 'testing startup excludes' ); -writefiles( ); -cwriteln( 'starting Lsyncd' ); -local pid = spawn( './lsyncd', cfgfile, '-log', 'all'); +cwriteln( 'testing startup excludes' ) + +writefiles( ) + +cwriteln( 'starting Lsyncd' ) + +local pid = spawn( './lsyncd', cfgfile, '-log', 'all' ) + +cwriteln( 'waiting for Lsyncd to start' ) -cwriteln( 'waiting for Lsyncd to start' ); posix.sleep( 3 ) -cwriteln( 'testing excludes after startup' ); -testfiles( ); -cwriteln( 'ok, removing sources' ); + +cwriteln( 'testing excludes after startup' ) + +testfiles( ) + +cwriteln( 'ok, removing sources' ) if srcdir:sub( 1,4 ) ~= '/tmp' then -- just to make sure before rm -rf - cwriteln( 'exit before drama, srcdir is "', srcdir, '"' ); - os.exit( 1 ); + cwriteln( 'exit before drama, srcdir is "', srcdir, '"' ) + + os.exit( 1 ) end -os.execute( 'rm -rf '..srcdir..'/*' ); +os.execute( 'rm -rf '..srcdir..'/*' ) -cwriteln( 'waiting for Lsyncd to remove destination' ); +cwriteln( 'waiting for Lsyncd to remove destination' ) -posix.sleep( 5 ); +posix.sleep( 5 ) _, result, code = os.execute( 'diff -urN ' .. srcdir .. ' ' .. trgdir ) if result ~= 'exit' or code ~= 0 then - cwriteln( 'fail, target directory not empty!' ); - os.exit( 1 ); + cwriteln( 'fail, target directory not empty!' ) + + os.exit( 1 ) end -cwriteln( 'writing files after startup' ); -writefiles( ); -cwriteln( 'waiting for Lsyncd to transmit changes' ); -posix.sleep( 5 ); -testfiles( ); - -cwriteln( 'killing started Lsyncd' ); -posix.kill( pid ); -local _, exitmsg, lexitcode = posix.wait( lpid ); -cwriteln( 'Exitcode of Lsyncd = ', exitmsg, ' ', lexitcode ); +cwriteln( 'writing files after startup' ) + +writefiles( ) + +cwriteln( 'waiting for Lsyncd to transmit changes' ) + +posix.sleep( 5 ) + +testfiles( ) + +cwriteln( 'killing started Lsyncd' ) + +posix.kill( pid ) +local _, exitmsg, exitcode = posix.wait( lpid ) + +cwriteln( 'Exitcode of Lsyncd = ', exitmsg, ' ', exitcode ); -if lexitcode == 143 +if exitcode == 143 then - cwriteln( "OK" ); - os.exit( 0 ); + cwriteln( 'OK' ) + os.exit( 0 ) else - os.exit( 1 ); + os.exit( 1 ) end -- TODO remove temp diff -Nru lsyncd-2.1.6/tests/exclude-rsyncssh.lua lsyncd-2.2.3/tests/exclude-rsyncssh.lua --- lsyncd-2.1.6/tests/exclude-rsyncssh.lua 2015-10-15 06:29:47.000000000 +0000 +++ lsyncd-2.2.3/tests/exclude-rsyncssh.lua 2018-03-09 12:39:11.000000000 +0000 @@ -1,10 +1,8 @@ -#!/usr/bin/lua - require( 'posix' ) dofile( 'tests/testlib.lua' ) cwriteln( '****************************************************************' ); -cwriteln( ' Testing excludes' ); +cwriteln( ' Testing excludes (rsyncssh)' ); cwriteln( '****************************************************************' ); cwriteln( ' (this test needs passwordless ssh localhost access ' ); cwriteln( ' for current user)' ); @@ -17,14 +15,14 @@ log = {'-log', 'all'} writefile(cfgfile, [[ -settings = { +settings { logfile = ']]..logfile..[[', - nodaemon = true, - delay = 3, + nodaemon = true } sync { default.rsyncssh, + delay = 3, host = 'localhost', source = ']]..srcdir..[[', targetdir = ']]..trgdir..[[', @@ -37,64 +35,77 @@ }]]); -- writes all files -local function writefiles() - posix.mkdir(srcdir .. 'd'); - writefile(srcdir .. 'erf', 'erf'); - writefile(srcdir .. 'eaf', 'erf'); - writefile(srcdir .. 'erd', 'erd'); - writefile(srcdir .. 'ead', 'ead'); - writefile(srcdir .. 'd/erf', 'erf'); - writefile(srcdir .. 'd/eaf', 'erf'); - writefile(srcdir .. 'd/erd', 'erd'); - writefile(srcdir .. 'd/ead', 'ead'); +local function writefiles +( ) + posix.mkdir( srcdir .. 'd' ) + writefile( srcdir .. 'erf', 'erf' ) + writefile( srcdir .. 'eaf', 'erf' ) + writefile( srcdir .. 'erd', 'erd' ) + writefile( srcdir .. 'ead', 'ead' ) + writefile( srcdir .. 'd/erf', 'erf' ) + writefile( srcdir .. 'd/eaf', 'erf' ) + writefile( srcdir .. 'd/erd', 'erd' ) + writefile( srcdir .. 'd/ead', 'ead' ) end -- test if the filename exists, fails if this is different to expect -local function testfile(filename, expect) - local stat, err = posix.stat(filename) - if stat and not expect then - cwriteln('failure: ',filename,' should be excluded'); - os.exit(1); +local function testfile +( + filename, + expect +) + local stat, err = posix.stat( filename ) + + if stat and not expect + then + cwriteln( 'failure: ',filename,' should be excluded' ); + os.exit( 1 ); end - if not stat and expect then - cwriteln('failure: ',filename,' should not be excluded'); - os.exit(1); + + if not stat and expect + then + cwriteln( 'failure: ',filename,' should not be excluded' ); + os.exit( 1 ); end end -- test all files -local function testfiles() - testfile( trgdir .. 'erf', false ); - testfile( trgdir .. 'eaf', false ); - testfile( trgdir .. 'erd', true ); - testfile( trgdir .. 'ead', true ); - testfile( trgdir .. 'd/erf', false ); - testfile( trgdir .. 'd/eaf', true ); - testfile( trgdir .. 'd/erd', true ); - testfile( trgdir .. 'd/ead', true ); +local function testfiles +( ) + testfile( trgdir .. 'erf', false ) + testfile( trgdir .. 'eaf', false ) + testfile( trgdir .. 'erd', true ) + testfile( trgdir .. 'ead', true ) + testfile( trgdir .. 'd/erf', false ) + testfile( trgdir .. 'd/eaf', true ) + testfile( trgdir .. 'd/erd', true ) + testfile( trgdir .. 'd/ead', true ) end -cwriteln('testing startup excludes'); -writefiles(); -cwriteln('starting Lsyncd'); -local pid = spawn('./lsyncd', cfgfile, unpack(log)); -cwriteln('waiting for Lsyncd to start'); -posix.sleep(10) -cwriteln('testing excludes after startup'); -testfiles(); -cwriteln('ok, removing sources'); -if srcdir:sub(1,4) ~= '/tmp' then +cwriteln( 'testing startup excludes' ) +writefiles( ) +cwriteln( 'starting Lsyncd' ) +local pid = spawn( './lsyncd', cfgfile, table.unpack( log ) ) +cwriteln( 'waiting for Lsyncd to start' ) +posix.sleep( 10 ) +cwriteln( 'testing excludes after startup' ) +testfiles( ) +cwriteln( 'ok, removing sources' ) + +if srcdir:sub(1,4) ~= '/tmp' +then -- just to make sure before rm -rf - cwriteln('exist before drama, srcdir is "', srcdir, '"'); - os.exit(1); + cwriteln('exit before drama, srcdir is "', srcdir, '"') + + os.exit( 1 ) end -os.execute( 'rm -rf ' .. srcdir .. '/*' ); -cwriteln( 'waiting for Lsyncd to remove destination' ); -posix.sleep( 5 ); +os.execute( 'rm -rf ' .. srcdir .. '/*' ) +cwriteln( 'waiting for Lsyncd to remove destination' ) +posix.sleep( 5 ) -_, result, code = os.execute('diff -urN '..srcdir..' '..trgdir) ~= 0 +_, result, code = os.execute( 'diff -urN '..srcdir..' '..trgdir ) if result ~= 'exit' or code ~= 0 then @@ -102,17 +113,17 @@ os.exit( 1 ); end -cwriteln( 'writing files after startup' ); -writefiles( ); -cwriteln( 'waiting for Lsyncd to transmit changes' ); -posix.sleep( 15 ); -testfiles( ); - -cwriteln( 'killing started Lsyncd' ); -posix.kill( pid ); -local _, exitmsg, lexitcode = posix.wait( lpid ); -cwriteln( 'Exitcode of Lsyncd = ', exitmsg, ' ', lexitcode ); -posix.sleep( 1 ); +cwriteln( 'writing files after startup' ) +writefiles( ) +cwriteln( 'waiting for Lsyncd to transmit changes' ) +posix.sleep( 15 ) +testfiles( ) + +cwriteln( 'killing started Lsyncd' ) +posix.kill( pid ) +local _, exitmsg, lexitcode = posix.wait( lpid ) +cwriteln( 'Exitcode of Lsyncd = ', exitmsg, ' ', lexitcode ) +posix.sleep( 1 ) if lexitcode == 143 then diff -Nru lsyncd-2.1.6/tests/filter-rsync.lua lsyncd-2.2.3/tests/filter-rsync.lua --- lsyncd-2.1.6/tests/filter-rsync.lua 1970-01-01 00:00:00.000000000 +0000 +++ lsyncd-2.2.3/tests/filter-rsync.lua 2018-03-09 12:39:11.000000000 +0000 @@ -0,0 +1,146 @@ +require( 'posix' ) +dofile( 'tests/testlib.lua' ) + +cwriteln( '****************************************************************' ) +cwriteln( ' Testing filters (rsync)' ) +cwriteln( '****************************************************************' ) + +local tdir, srcdir, trgdir = mktemps( ) +local logfile = tdir .. "log" +local cfgfile = tdir .. "config.lua" +local range = 5 +local log = {"-log", "all"} + +writefile(cfgfile, [[ +settings { + logfile = ']]..logfile..[[', + nodaemon = true, +} + +sync { + default.rsync, + source = ']]..srcdir..[[', + target = ']]..trgdir..[[', + delay = 3, + filter = { + '- /ab**', + '+ /a**', + '- /**', + }, +}]]) + +-- writes all files +local function writefiles +( ) + writefile( srcdir .. 'abc', 'abc' ) + writefile( srcdir .. 'acc', 'acc' ) + writefile( srcdir .. 'baa', 'baa' ) + posix.mkdir( srcdir .. 'abx' ) + writefile( srcdir .. 'abx/a', 'abxa' ) + posix.mkdir( srcdir .. 'acx' ) + writefile( srcdir .. 'acx/x', 'acxx' ) +end + +-- +-- Tests if the filename exists +-- fails if this is different to expect. +-- +local function testfile +( + filename, + expect +) + local stat, err = posix.stat( filename ) + + if stat and not expect + then + cwriteln( 'failure: ', filename, ' should be filtered') + + os.exit( 1 ) + end + + if not stat and expect + then + cwriteln( 'failure: ', filename, ' should not be filtered' ) + os.exit( 1 ) + end +end + +-- test all files +local function testfiles +( ) + testfile( trgdir .. 'abc', false ) + testfile( trgdir .. 'acc', true ) + testfile( trgdir .. 'baa', false ) + testfile( trgdir .. 'abx/a', false ) + testfile( trgdir .. 'acx/x', true ) +end + + +cwriteln( 'testing startup filters' ) + +writefiles( ) + +cwriteln( 'starting Lsyncd' ) + +local pid = spawn( './lsyncd', cfgfile, '-log', 'all' ) + +cwriteln( 'waiting for Lsyncd to start' ) + +posix.sleep( 3 ) + +cwriteln( 'testing filters after startup' ) + +testfiles( ) + +cwriteln( 'ok, removing sources' ) + +if srcdir:sub( 1,4 ) ~= '/tmp' +then + -- just to make sure before rm -rf + cwriteln( 'exit before drama, srcdir is "', srcdir, '"' ) + + os.exit( 1 ) +end + +os.execute( 'rm -rf '..srcdir..'/*' ) + +cwriteln( 'waiting for Lsyncd to remove destination' ) + +posix.sleep( 5 ) + +_, result, code = os.execute( 'diff -urN ' .. srcdir .. ' ' .. trgdir ) + +if result ~= 'exit' or code ~= 0 +then + cwriteln( 'fail, target directory not empty!' ) + + os.exit( 1 ) +end + +cwriteln( 'writing files after startup' ) + +writefiles( ) + +cwriteln( 'waiting for Lsyncd to transmit changes' ) + +posix.sleep( 5 ) + +testfiles( ) + +cwriteln( 'killing started Lsyncd' ) + +posix.kill( pid ) +local _, exitmsg, exitcode = posix.wait( lpid ) + +cwriteln( 'Exitcode of Lsyncd = ', exitmsg, ' ', exitcode ); + +if exitcode == 143 +then + cwriteln( 'OK' ) + os.exit( 0 ) +else + os.exit( 1 ) +end + +-- TODO remove temp diff -Nru lsyncd-2.1.6/tests/l4rsyncdata.lua lsyncd-2.2.3/tests/l4rsyncdata.lua --- lsyncd-2.1.6/tests/l4rsyncdata.lua 2015-10-15 06:29:47.000000000 +0000 +++ lsyncd-2.2.3/tests/l4rsyncdata.lua 2018-03-09 12:39:11.000000000 +0000 @@ -1,4 +1,3 @@ -#!/usr/bin/lua require( 'posix' ) dofile( 'tests/testlib.lua' ) @@ -28,7 +27,7 @@ '-nodaemon', '-delay', '5', "-rsync", srcdir, trgdir, - unpack( logs ) + table.unpack( logs ) ) cwriteln( 'waiting for lsyncd to start' ) @@ -70,7 +69,7 @@ cwriteln( 'Signal terminating diff = "', code, '"') end -if result ~= 'exit' or exitcode ~= 0 +if result ~= 'exit' or code ~= 0 then os.exit( 1 ) else diff -Nru lsyncd-2.1.6/tests/schedule.lua lsyncd-2.2.3/tests/schedule.lua --- lsyncd-2.1.6/tests/schedule.lua 2015-10-15 06:29:47.000000000 +0000 +++ lsyncd-2.2.3/tests/schedule.lua 2018-03-09 12:39:11.000000000 +0000 @@ -1,10 +1,9 @@ -#!/usr/bin/lua -require("posix") -dofile("tests/testlib.lua") - -cwriteln("****************************************************************") -cwriteln(" Testing Lsyncd scheduler ") -cwriteln("****************************************************************") +require( 'posix' ) +dofile( 'tests/testlib.lua' ) + +cwriteln( '****************************************************************' ) +cwriteln( ' Testing Lsyncd scheduler ' ) +cwriteln( '****************************************************************' ) local tdir, srcdir, trgdir = mktemps() local logfile = tdir .. "log" @@ -12,10 +11,10 @@ local logs = {"-log", "all" } writefile(cfgfile, [[ -settings = { - logfile = "]]..logfile..[[", +settings { + logfile = "]]..logfile..[[", log = all, - nodaemon = true, + nodaemon = true, maxProcesses = 1 } @@ -47,30 +46,36 @@ -- test if the filename exists, fails if this is different to expect -local function testfile(filename) +local function testfile +( + filename +) local stat, err = posix.stat(filename) - if not stat then - cwriteln("failure: ",filename," missing") - os.exit(1) + + if not stat + then + cwriteln( 'failure: ', filename, ' missing' ) + os.exit( 1 ) end end -cwriteln("starting Lsyncd") -local pid = spawn("./lsyncd", cfgfile, unpack(logs)) -cwriteln("waiting for Lsyncd to do a few cycles") -posix.sleep(30) -cwriteln("look if every circle got a chance to run") -testfile(srcdir.."a") -testfile(srcdir.."b") -testfile(srcdir.."c") -cwriteln("killing started Lsyncd") -posix.kill(pid) -local _, exitmsg, lexitcode = posix.wait(lpid) -cwriteln("Exitcode of Lsyncd = ", exitmsg, " ", lexitcode) +cwriteln( 'starting Lsyncd' ) +local pid = spawn( './lsyncd', cfgfile, table.unpack( logs ) ) +cwriteln( 'waiting for Lsyncd to do a few cycles' ) +posix.sleep( 30 ) +cwriteln( 'look if every circle got a chance to run' ) +testfile( srcdir..'a' ) +testfile( srcdir..'b' ) +testfile( srcdir..'c' ) +cwriteln( 'killing started Lsyncd' ) +posix.kill( pid ) +local _, exitmsg, lexitcode = posix.wait( lpid ) +cwriteln( 'Exitcode of Lsyncd = ', exitmsg, ' ', lexitcode) posix.sleep(1); -if lexitcode == 143 then - cwriteln("OK") +if lexitcode == 143 +then + cwriteln( 'OK' ) os.exit( 0 ) else os.exit( 1 ) diff -Nru lsyncd-2.1.6/tests/testlib.lua lsyncd-2.2.3/tests/testlib.lua --- lsyncd-2.1.6/tests/testlib.lua 2015-10-15 06:29:47.000000000 +0000 +++ lsyncd-2.2.3/tests/testlib.lua 2018-03-09 12:39:11.000000000 +0000 @@ -1,136 +1,185 @@ -- common testing environment -posix = require('posix') +posix = require( 'posix' ) -- escape codes to colorize output on terminal local c1='\027[47;34m' + local c0='\027[0m' ---- --- writes colorized -- -function cwriteln(...) - io.write(c1, ...) - io.write(c0, '\n') +-- Writes colorized. +-- +function cwriteln +(...) + io.write( c1, '++ ', ... ) + io.write( c0, '\n' ) end ------ --- initializes the pseudo random generator --- if environemnt 'SEED' is set, use that as seed. -local seed = os.getenv('SEED') or os.time() -math.randomseed(seed) -cwriteln('random seed: ', seed) - ------ --- creates a tmp directory --- --- @returns the name of the directory --- -function mktempd() - local f = io.popen('mktemp -td ltest.XXX', 'r') - local s = f:read('*a') - f:close() - s = s:gsub('[\n\r]+', ' ') - s = s:match('^%s*(.-)%s*$') +-- +-- Initializes the pseudo random generator +-- +-- If environment variable 'SEED' is set, +-- that one is used seed. +-- +local seed = os.getenv( 'SEED') or os.time( ) + +math.randomseed( seed ) + +cwriteln( 'random seed: ', seed ) + +-- +-- Creates a tmp directory. +-- +-- Returns the name of the directory. +-- +function mktempd +( ) + local f = io.popen( 'mktemp -td ltest.XXX', 'r' ) + + local s = f:read( '*a' ) + + f:close( ) + + s = s:gsub( '[\n\r]+', ' ' ) + + s = s:match( '^%s*(.-)%s*$' ) + return s end ------ --- creates a tmp directory with the --- typical lsyncd test architecture --- --- @returns path of tmpdir --- path of srcdir --- path of trgdir -- - -function mktemps() - local tdir = mktempd()..'/' - cwriteln('using ', tdir, ' as test root') +-- Creates a tmp directory with the +-- typical lsyncd test architecture. +-- +-- returns path of tmpdir +-- path of srcdir +-- path of trgdir +-- +function mktemps +( ) + local tdir = mktempd() .. '/' + cwriteln( 'using ', tdir, ' as test root' ) local srcdir = tdir..'src/' local trgdir = tdir..'trg/' - posix.mkdir(srcdir) - posix.mkdir(trgdir) + posix.mkdir( srcdir ) + posix.mkdir( trgdir ) return tdir, srcdir, trgdir end ----- --- Writes a file with 'text' in it. +-- +-- Writes a file with 'text' in it -- and adds a newline. -- -function writefile(filename, text) - local f = io.open(filename, 'w') - if not f then - cwriteln('Cannot open "'..filename..'" for writing.') +function writefile +( + filename, + text +) + local f = io.open( filename, 'w' ) + + if not f + then + cwriteln( 'Cannot open "'..filename..'" for writing.' ) return false end - f:write(text) - f:write('\n') - f:close() + + f:write( text ) + f:write( '\n' ) + f:close( ) + return true end ------ --- spawns a subprocess. -- --- @returns the processes pid +-- Spawns a subprocess. +-- +-- Returns the processes pid. -- -function spawn(...) - args = {...} - cwriteln('spawning: ', table.concat(args, ' ')) - local pid = posix.fork() - if pid < 0 then - cwriteln('Error, failed fork!') - os.exit(-1) +function spawn +(...) + args = { ... } + + cwriteln( 'spawning: ', table.concat( args, ' ' ) ) + + local pid = posix.fork( ) + + if pid < 0 + then + cwriteln( 'Error, failed fork!' ) + + os.exit( -1 ) end - if pid == 0 then - posix.exec(...) + + if pid == 0 + then + posix.exec( ... ) -- should not return - cwriteln('Error, failed to spawn: ', ...) - os.exit(-1); + + cwriteln( 'Error, failed to spawn: ', ... ) + + os.exit( -1 ) end + return pid end ------ +-- -- Makes a lot of random data -- --- @param rootdir ... the directory to make data in --- @param n ... roughly how much data action will done -- -function churn(rootdir, n) +function churn +( + rootdir, -- the directory to make data in + n, -- roughly how much data action will be done + init -- if true init random data only, no sleeps or moves +) -- all dirs created, indexed by integer and path - root = {name=''} - alldirs = {root} - dirsWithFileI = {} - dirsWithFileD = {} + root = { name = '' } + alldirs = { root } + dirsWithFileI = { } + dirsWithFileD = { } - ----- + -- -- returns the name of a directory -- -- name is internal recursive paramter, keep it nil. -- - local function dirname(dir, name) + local function dirname + ( + dir, + name + ) name = name or '' - if not dir then + + if not dir + then return name end - return dirname(dir.parent, dir.name .. '/' .. name) + + return dirname( dir.parent, dir.name .. '/' .. name ) end - ----- + -- -- Picks a random dir. -- - local function pickDir(notRoot) - if notRoot then - if #alldirs <= 2 then + local function pickDir + ( + notRoot + ) + if notRoot + then + if #alldirs <= 2 + then return nil end - return alldirs[math.random(2, #alldirs)] + + return alldirs[ math.random( 2, #alldirs ) ] end - return alldirs[math.random(#alldirs)] + + return alldirs[ math.random( #alldirs ) ] end - ---- + -- -- Picks a random file. -- -- Returns 3 values: @@ -138,212 +187,312 @@ -- * the filename -- * number of files in directory -- - local function pickFile() + local function pickFile + ( ) -- picks the random directory - if #dirsWithFileI < 1 then + if #dirsWithFileI < 1 + then return end - local rdir = dirsWithFileI[math.random(1, #dirsWithFileI)] - if not rdir then + + local rdir = dirsWithFileI[ math.random( 1, #dirsWithFileI ) ] + + if not rdir + then return end -- counts the files in there local c = 0 - for name, _ in pairs(rdir) do - if #name == 2 then + + for name, _ in pairs(rdir) + do + if #name == 2 + then c = c + 1 end end -- picks one file at random - local cr = math.random(1, c) + local cr = math.random( 1, c ) + local fn - for name, _ in pairs(rdir) do - if #name == 2 then + + for name, _ in pairs( rdir ) + do + if #name == 2 + then -- filenames are 2 chars wide. cr = cr - 1 - if cr == 0 then + if cr == 0 + then fn = name break end end end + return rdir, fn, c end - ----- + -- -- Removes a reference to a file -- - -- @param dir --- directory reference - -- @param fn --- filename - -- @param c --- number of files in dir + -- @param dir -- directory reference + -- @param fn -- filename + -- @param c -- number of files in dir -- - local function rmFileReference(dir, fn, c) + local function rmFileReference + ( dir, fn, c ) dir[fn] = nil - if c == 1 then + + if c == 1 + then -- if last file from origin dir, it has no files anymore - for i, v in ipairs(dirsWithFileI) do - if v == dir then - table.remove(dirsWithFileI, i) + for i, v in ipairs( dirsWithFileI ) + do + if v == dir + then + table.remove( dirsWithFileI, i ) + break end end - dirsWithFileD[dir] = nil + + dirsWithFileD[ dir ] = nil end end - ---- + -- -- possible randomized behaviour. -- just gives it a pause -- - local function sleep() - cwriteln('..zzz..') - posix.sleep(1) + local function sleep + ( ) + cwriteln( '..zzz..' ) + + posix.sleep( 1 ) end - ---- + -- -- possible randomized behaviour. -- creates a directory -- - local function mkdir() + local function mkdir + ( ) -- chooses a random directory to create it into - local rdir = pickDir() + local rdir = pickDir( ) + -- creates a new random one letter name - local nn = string.char(96 + math.random(26)) - if not rdir[nn] then + local nn = string.char( 96 + math.random( 26 ) ) + + if not rdir[nn] + then local ndir = { name = nn, parent = rdir, } - local dn = dirname(ndir) - rdir[nn] = dn - table.insert(alldirs, ndir) - cwriteln('mkdir '..rootdir..dn) - posix.mkdir(rootdir..dn) + + local dn = dirname( ndir ) + + rdir[ nn ] = dn + + table.insert( alldirs, ndir ) + + cwriteln( 'mkdir '..rootdir..dn ) + + posix.mkdir( rootdir..dn ) end end - ---- - -- possible randomized behaviour. + -- + -- Possible randomized behaviour: -- Creates a file. -- - local function mkfile() + local function mkfile + ( ) -- chooses a random directory to create it into local rdir = pickDir() + -- creates a new random one letter name - local nn = 'f'..string.char(96 + math.random(26)) - local fn = dirname(rdir) .. nn - cwriteln('mkfile '..rootdir..fn) + local nn = 'f'..string.char( 96 + math.random( 26 ) ) + + local fn = dirname( rdir ) .. nn + + cwriteln( 'mkfile ' .. rootdir .. fn ) + local f = io.open(rootdir..fn, 'w') - if f then - for i=1,10 do - f:write(string.char(96 + math.random(26))) + + if f + then + for i = 1, 10 + do + f:write( string.char( 96 + math.random( 26 ) ) ) end - f:write('\n') - f:close() - rdir[nn]=true - if not dirsWithFileD[rdir] then - table.insert(dirsWithFileI, rdir) - dirsWithFileD[rdir]=true + + f:write( '\n' ) + + f:close( ) + + rdir[ nn ]=true + + if not dirsWithFileD[ rdir ] + then + table.insert( dirsWithFileI, rdir ) + + dirsWithFileD[ rdir ]=true end end end - ---- - -- possible randomized behaviour, - -- moves a directory. -- - local function mvdir() - if #alldirs <= 2 then + -- Possible randomized behaviour: + -- Moves a directory. + -- + local function mvdir + ( ) + if #alldirs <= 2 + then return end + -- chooses a random directory to move - local odir = pickDir(true) + local odir = pickDir( true ) + -- chooses a random directory to move to - local tdir = pickDir() + local tdir = pickDir( ) -- makes sure tdir is not a subdir of odir local dd = tdir - while dd do - if odir == dd then + + while dd + do + if odir == dd + then return end + dd = dd.parent end + -- origin name in the target dir already - if tdir[odir.name] ~= nil then + if tdir[odir.name] ~= nil + then return end - local on = dirname(odir) - local tn = dirname(tdir) - cwriteln('mvdir ',rootdir,on,' -> ',rootdir,tn,odir.name) - os.rename(rootdir..on, rootdir..tn..odir.name) - odir.parent[odir.name] = nil + + local on = dirname( odir ) + + local tn = dirname( tdir ) + + cwriteln( 'mvdir ', rootdir,on, ' -> ', rootdir, tn, odir.name ) + + os.rename( rootdir..on, rootdir..tn..odir.name ) + + odir.parent[ odir.name ] = nil + odir.parent = tdir - tdir[odir.name] = odir + + tdir[ odir.name ] = odir end - ---- + -- -- possible randomized behaviour, -- moves a file. -- - local function mvfile() - local odir, fn, c = pickFile() - if not odir then + local function mvfile + ( ) + local odir, fn, c = pickFile( ) + + if not odir + then return end + -- picks a directory with a file at random -- picks a target directory at random - local tdir = pickDir() - local on = dirname(odir) - local tn = dirname(tdir) - cwriteln('mvfile ',rootdir,on,fn,' -> ',rootdir,tn,fn) - os.rename(rootdir..on..fn, rootdir..tn..fn) - rmFileReference(odir, fn, c) + local tdir = pickDir( ) + + local on = dirname( odir ) + + local tn = dirname( tdir ) + + cwriteln( 'mvfile ', rootdir, on, fn, ' -> ', rootdir, tn, fn ) + + os.rename( rootdir..on..fn, rootdir..tn..fn ) + rmFileReference( odir, fn, c ) + + tdir[ fn ] = true - tdir[fn] = true - if not dirsWithFileD[tdir] then - dirsWithFileD[tdir] = true - table.insert(dirsWithFileI, tdir) + if not dirsWithFileD[ tdir ] + then + dirsWithFileD[ tdir ] = true + + table.insert( dirsWithFileI, tdir ) end end - ---- - -- possible randomized behaviour, - -- removes a file. -- - local function rmfile() - local dir, fn, c = pickFile() - if dir then - local dn = dirname(dir) - cwriteln('rmfile ',rootdir,dn,fn) - posix.unlink(rootdir..dn..fn) - rmFileReference(dir, fn, c) + -- Possible randomized behaviour: + -- Removes a file. + -- + local function rmfile + ( ) + local dir, fn, c = pickFile( ) + + if dir + then + local dn = dirname( dir ) + + cwriteln( 'rmfile ', rootdir, dn, fn ) + + posix.unlink( rootdir..dn..fn ) + + rmFileReference( dir, fn, c ) end end - local dice = { - { 10, sleep }, - { 20, mkfile }, - { 20, mkdir }, - { 20, mvdir }, - { 20, rmfile }, - } + local dice + + if init + then + dice = + { + { 10, mkfile }, + { 10, mkdir }, + } + else + dice = + { + { 50, sleep }, + { 20, mkfile }, + { 20, mkdir }, + { 20, mvdir }, + { 20, rmfile }, + } + end + + cwriteln( 'making random data' ) - cwriteln('making random data') local ndice = 0 - for i, d in ipairs(dice) do - ndice = ndice + d[1] - d[1] = ndice + + for i, d in ipairs( dice ) + do + ndice = ndice + d[ 1 ] + d[ 1 ] = ndice end - for ai=1,n do + for ai = 1, n + do -- throws a die what to do - local acn = math.random(ndice) - for i, d in ipairs(dice) do - if acn <= d[1] then - d[2]() + local acn = math.random( ndice ) + + for i, d in ipairs( dice ) + do + if acn <= d[ 1 ] + then + d[ 2 ]( ) + break end end