diff --git a/.editorconfig b/.editorconfig new file mode 100644 index 0000000..7708c2b --- /dev/null +++ b/.editorconfig @@ -0,0 +1,8 @@ +# 4 tab indentation + +indent_style = tab +indent_size = 4 + +[*.nix] +indent_style = space +indent_size = 2 diff --git a/CMakeLists.txt b/CMakeLists.txt index e30821a..2bc6d93 100644 --- a/CMakeLists.txt +++ b/CMakeLists.txt @@ -1,7 +1,7 @@ # preamble project( Lsyncd ) cmake_minimum_required( VERSION 3.10 ) -set( LSYNCD_VERSION 2.2.3 ) +set( LSYNCD_VERSION 2.3.0-beta1 ) set( CMAKE_MODULE_PATH ${CMAKE_MODULE_PATH} "${CMAKE_SOURCE_DIR}/cmake/" ) @@ -118,6 +118,6 @@ add_executable( lsyncd ${LSYNCD_SRC} ) target_link_libraries( lsyncd ${LUA_LIBRARIES} ) install( TARGETS lsyncd RUNTIME DESTINATION bin ) -install( FILES doc/manpage/lsyncd.1 DESTINATION man ) +install( FILES doc/manpage/lsyncd.1 DESTINATION ${CMAKE_INSTALL_MANDIR}/man1 COMPONENT man ) install( DIRECTORY examples DESTINATION doc ) diff --git a/README.md b/README.md index 7aeee6a..40e71dd 100644 --- a/README.md +++ b/README.md @@ -6,7 +6,7 @@ Lsyncd watches a local directory trees event monitor interface (inotify or fseve 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](https://axkibe.github.io/lsyncd/) for details. +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 achieved. See [the manual](https://axkibe.github.io/lsyncd/) for details. Lsyncd 2.2.1 requires rsync >= 3.1 on all source and target machines. diff --git a/cmake/FindLua.cmake b/cmake/FindLua.cmake index a20af5b..6628209 100644 --- a/cmake/FindLua.cmake +++ b/cmake/FindLua.cmake @@ -16,6 +16,7 @@ #============================================================================= # Copyright 2007-2009 Kitware, Inc. # Modified to support Lua 5.2 by LuaDist 2012 +# Modified to support Lua 5.4 by LuaDist 2022 # # Distributed under the OSI-approved BSD License (the "License"); # see accompanying file Copyright.txt for details. @@ -27,7 +28,7 @@ # (To distribute this file outside of CMake, substitute the full # License text for the above reference.) # -# This module will try to find the newest Lua version down to 5.2 +# This module will try to find the newest Lua version down to 5.4 # Always search for non-versioned lua first (recommended) SET(_POSSIBLE_LUA_INCLUDE include include/lua) @@ -36,7 +37,7 @@ SET(_POSSIBLE_LUA_INCLUDE include include/lua) #SET(_POSSIBLE_LUA_LIBRARY lua) # Determine possible naming suffixes (there is no standard for this) -SET(_POSSIBLE_SUFFIXES "52" "5.2" "-5.2" "53" "5.3" "-5.3" "") +SET(_POSSIBLE_SUFFIXES "54" "5.4" "-5.4" "53" "5.3" "-5.3" "52" "5.2" "-5.2" "") # Set up possible search names and locations FOREACH(_SUFFIX IN LISTS _POSSIBLE_SUFFIXES) diff --git a/default-rsync.lua b/default-rsync.lua index 390df1f..e422b72 100644 --- a/default-rsync.lua +++ b/default-rsync.lua @@ -63,6 +63,7 @@ rsync.checkgauge = { copy_links = true, copy_unsafe_links = true, cvs_exclude = true, + delete_excluded = true, dry_run = true, executability = true, existing = true, @@ -128,6 +129,9 @@ rsync.action = function -- gets all events ready for syncing local elist = inlet.getEvents( eventNotInitBlank ) + local substitudes = inlet.getSubstitutionData(elist, {}) + local target = substitudeCommands(config.target, substitudes) + -- gets the list of paths for the event list -- deletes create multi match patterns local paths = elist.getPaths( ) @@ -236,7 +240,7 @@ rsync.action = function '--include-from=-', '--exclude=*', config.source, - config.target + target ) end @@ -317,7 +321,7 @@ rsync.init = function local filters = inlet.hasFilters( ) and inlet.getFilters( ) - local delete = nil + local delete = {} local target = config.target @@ -331,12 +335,20 @@ rsync.init = function target = config.host .. ':' .. config.targetdir end + local substitudes = inlet.getSubstitutionData(event, {}) + target = substitudeCommands(target, substitudes) + if config.delete == true or config.delete == 'startup' then delete = { '--delete', '--ignore-errors' } end + if config.rsync.delete_excluded == true + then + table.insert( delete, '--delete-excluded' ) + end + if not filters and #excludes == 0 then -- starts rsync without any filters or excludes diff --git a/default.lua b/default.lua index d689260..e180fd7 100644 --- a/default.lua +++ b/default.lua @@ -52,6 +52,7 @@ default.checkgauge = { prepare = true, source = true, target = true, + tunnel = true, } -- @@ -120,7 +121,14 @@ default.collect = function agent.target, ' finished.' ) - + if settings('onepass') + then + log( + 'Normal', + 'onepass config set, exiting' + ) + terminate( 0 ) + end return 'ok' elseif rc == 'again' then diff --git a/flake.lock b/flake.lock index b06a406..68efe45 100644 --- a/flake.lock +++ b/flake.lock @@ -17,16 +17,16 @@ }, "nixpkgs": { "locked": { - "lastModified": 1637709854, - "narHash": "sha256-y98gkOBUEiPAmwRhZPzTQ0YayZKPS2loNgA0GcNewMM=", + "lastModified": 1639488789, + "narHash": "sha256-Ey12CBni1jlEGoW4eH4X0hugWs25MxHMcNH4N8VVX0U=", "owner": "nixos", "repo": "nixpkgs", - "rev": "9c43581935a23d56734bd02da0ba8e7fda21e747", + "rev": "ce635e9dca8f7e2bfab19a3667d7e697c019c68b", "type": "github" }, "original": { "owner": "nixos", - "ref": "release-21.05", + "ref": "release-21.11", "repo": "nixpkgs", "type": "github" } diff --git a/flake.nix b/flake.nix index 8552740..ac4a2a7 100644 --- a/flake.nix +++ b/flake.nix @@ -1,27 +1,37 @@ { description = "Lsyncd (Live Syncing Daemon)"; - inputs.nixpkgs.url = "github:nixos/nixpkgs/release-21.05"; + inputs.nixpkgs.url = "github:nixos/nixpkgs/release-21.11"; inputs.flake-utils.url = "github:numtide/flake-utils"; outputs = { self, nixpkgs, flake-utils }: flake-utils.lib.eachDefaultSystem (system: let - pkgs = nixpkgs.legacyPackages.${system}; + pkgs = (import nixpkgs { + inherit system; + # Makes the config pure as well. See /top-level/impure.nix: + config = { + allowBroken = true; + };}); #.legacyPackages.${system}; defaultDeps = with pkgs; [ gcc cmake + gnumake glib rsync openssh + curl ]; version = builtins.elemAt (builtins.match ''.*set\(.LSYNCD_VERSION ([0-9\.]*).*'' (builtins.substring 0 500 (builtins.readFile ./CMakeLists.txt))) 0; - - + mylua5_4 = pkgs.lua5_4.override({ + packageOverrides = luaself: luaprev: { + luarocks = luaprev.luarocks-3_7; + }; + }); buildExtensions = luapkgs: ( let @@ -82,11 +92,34 @@ } ); - buildTypes = { - lua5_1 = [(pkgs.lua5_1.withPackages (ps: [ps.luaposix ps.penlight (buildExtensions pkgs.lua51Packages)]))]; - lua5_2 = [(pkgs.lua5_2.withPackages (ps: [ps.luaposix ps.penlight (buildExtensions pkgs.lua52Packages)]))]; - lua5_3 = [(pkgs.lua5_3.withPackages (ps: [ps.luaposix ps.penlight (buildExtensions pkgs.lua53Packages)]))]; - lua5_4 = [(pkgs.lua5_4.withPackages (ps: [ps.luaposix ps.penlight (buildExtensions pkgs.lua54Packages)]))]; + luaposix35 = mylua: mylua.pkgs.buildLuarocksPackage { + pname = "luaposix"; + lua = mylua; + version = "35.1-1"; + knownRockspec = (pkgs.fetchurl { + url = "https://luarocks.org/luaposix-35.1-1.rockspec"; + sha256 = "1n6c7qyabj2y95jmbhf8fxbrp9i73kphmwalsam07f9w9h995xh1"; + }).outPath; + src = pkgs.fetchurl { + url = "http://github.com/luaposix/luaposix/archive/v35.1.zip"; + sha256 = "1c03chkzwr2p1wd0hs1bafl2890fqbrfc3qk0wxbd202gc6128zi"; + }; + + # + propagatedBuildInputs = [ mylua ]; + + meta = { + homepage = "http://github.com/luaposix/luaposix/"; + description = "Lua bindings for POSIX"; + license.fullName = "MIT/X11"; + }; + }; + + buildTypes = { + lua5_1 = [pkgs.lua5_1 pkgs.lua51Packages.luaposix (buildExtensions pkgs.lua51Packages)]; + lua5_2 = [pkgs.lua5_2 pkgs.lua52Packages.luaposix (buildExtensions pkgs.lua51Packages)]; + lua5_3 = [pkgs.lua5_3 pkgs.lua53Packages.luaposix (buildExtensions pkgs.lua51Packages)]; + lua5_4 = [pkgs.lua5_3 (luaposix35 mylua5_4) (buildExtensions mylua5_4)]; }; in let @@ -95,9 +128,12 @@ name = "lsyncd"; src = ./.; - + buildInputs = defaultDeps ++ luaPackages; - }); + }); + mkDev = packages: pkgs.mkShell { + propagatedBuildInputs = defaultDeps ++ packages; + }; in { packages = { @@ -108,10 +144,15 @@ lsyncd_lua5_4 = mkLsync buildTypes.lua5_4; }; + devShells = { + lsyncd = mkDev buildTypes.lua5_3; + lsyncd_lua5_1 = mkDev buildTypes.lua5_1; + lsyncd_lua5_2 = mkDev buildTypes.lua5_2; + lsyncd_lua5_3 = mkDev buildTypes.lua5_3; + lsyncd_lua5_4 = mkDev buildTypes.lua5_4; + }; + defaultPackage = self.packages.${system}.lsyncd; - # devShell = pkgs.mkShell { - # buildInputs = defaultDeps ++ buildTypes.lua5_3; - # }; } ); } \ No newline at end of file diff --git a/lsyncd.c b/lsyncd.c index 7ca6ce8..8fac8ac 100644 --- a/lsyncd.c +++ b/lsyncd.c @@ -19,6 +19,8 @@ #define SYSLOG_NAMES 1 +#include +#include #include #include #include @@ -46,6 +48,11 @@ #include #include +#if defined(LUA_VERSION_NUM) && LUA_VERSION_NUM >= 504 +#define lua_objlen lua_rawlen +#endif + + /* | The Lua part of Lsyncd */ @@ -135,6 +142,22 @@ int pidfile_fd = 0; static long clocks_per_sec; +/* +| Dummy variable of which it's address is used as +| the cores index in the lua registry to +| the lua runners function table in the lua registry. +*/ +static int runner; + + +/* +| Dummy variable of which it's address is used as +| the cores index n the lua registry to +| the lua runners error handler. +*/ +static int callError; + + /** * signal handler */ @@ -438,6 +461,40 @@ printlogf0(lua_State *L, } +/* + | Print a traceback of the error + */ +static int l_traceback (lua_State *L) { + // runner.callError + lua_getglobal(L, "debug"); + lua_getfield(L, -1, "traceback"); + lua_pushvalue(L, 1); + lua_pushinteger(L, 2); + lua_call(L, 2, 1); + printlogf( L, "traceback", "%s", lua_tostring(L, -1) ); + return 1; +} + +/* + | Call runners terminate function and exit with given exit code + */ +static void safeexit (lua_State *L, int exitcode) { + // load_runner_func(L, "teardown"); + // pushes the function + lua_pushlightuserdata( L, (void *) &runner ); + lua_gettable( L, LUA_REGISTRYINDEX ); + lua_pushstring( L, "teardown" ); + lua_gettable( L, -2 ); + lua_remove( L, -2 ); + lua_pushinteger(L, exitcode); + lua_call(L, 2, 1); + if (lua_isinteger(L, -1)) { + exitcode = luaL_checkinteger(L, -1); + } + exit(exitcode); +} + + /*~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~* ( Simple memory management ) *~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~*/ @@ -605,23 +662,6 @@ pipe_tidy( struct observance * observance ) ( Helper Routines ) *~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~*/ - -/* -| Dummy variable of which it's address is used as -| the cores index in the lua registry to -| the lua runners function table in the lua registry. -*/ -static int runner; - - -/* -| Dummy variable of which it's address is used as -| the cores index n the lua registry to -| the lua runners error handler. -*/ -static int callError; - - /* | Sets the close-on-exit flag of a file descriptor. */ @@ -697,7 +737,7 @@ write_pidfile pidfile ); - exit( -1 ); + safeexit(L, -1 ); } int rc = lockf( pidfile_fd, F_TLOCK, 0 ); @@ -710,7 +750,7 @@ write_pidfile pidfile ); - exit( -1 ); + safeexit(L, -1 ); } snprintf( buf, sizeof( buf ), "%i\n", getpid( ) ); @@ -918,7 +958,7 @@ user_obs_ready( // calls the user function if( lua_pcall( L, 1, 0, -3 ) ) { - exit( -1 ); + safeexit(L, -1 ); } lua_pop( L, 2 ); @@ -954,7 +994,7 @@ user_obs_writey( // calls the user function if( lua_pcall( L, 1, 0, -3 ) ) { - exit(-1); + safeexit(L, -1); } lua_pop( L, 2 ); @@ -1078,6 +1118,82 @@ l_now(lua_State *L) return 1; } +/* +| Sends a signal to proceess pid +| +| Params on Lua stack: +| 1: pid +| 2: signal +| +| Returns on Lua stack: +| return value of kill +*/ +static int +l_kill( lua_State *L ) +{ + pid_t pid = luaL_checkinteger( L, 1 ); + int sig = luaL_checkinteger( L, 2 ); + + int rv = kill(pid, sig ); + + lua_pushinteger( L, rv ); + + return 1; +} + +/* +| Returns a free port of host +| +| Params on Lua stack: +| (not yet) 1: hostname or ip for bind +| +| Returns on Lua stack: +| return integer of free port +| +*/ +static int +l_free_port(lua_State *L) { + int sock = socket(AF_INET, SOCK_STREAM, 0); + if(sock < 0) { + printf("error opening socket\n"); + goto error; + } + + struct sockaddr_in serv_addr; + memset((char *) &serv_addr, 0, sizeof(serv_addr)); + serv_addr.sin_family = AF_INET; + serv_addr.sin_addr.s_addr = INADDR_ANY; + serv_addr.sin_port = 0; + if (bind(sock, (struct sockaddr *) &serv_addr, sizeof(serv_addr)) < 0) { + if(errno == EADDRINUSE) { + printf("the port is not available. already to other process\n"); + goto error; + } else { + printf("could not bind to process (%d) %s\n", errno, strerror(errno)); + goto error; + } + } + + socklen_t len = sizeof(serv_addr); + if (getsockname(sock, (struct sockaddr *)&serv_addr, &len) == -1) { + goto error; + } + + lua_pushinteger(L, ntohs(serv_addr.sin_port)); + + if (close (sock) < 0 ) { + printf("did not close: %s\n", strerror(errno)); + lua_pop ( L, 1 ); + goto error; + } + + return 1; +error: + lua_pushnil(L); + return 1; +} + + /* | Executes a subprocess. Does not wait for it to return. @@ -1203,7 +1319,7 @@ l_exec( lua_State *L ) "in spawn(), expected a string after pipe '<'" ); - exit( -1 ); + safeexit(L, -1 ); } pipe_text = lua_tolstring( L, 3, &pipe_len ); @@ -1215,7 +1331,7 @@ l_exec( lua_State *L ) { logstring( "Error", "cannot create a pipe!" ); - exit( -1 ); + safeexit(L, -1 ); } // always closes the write end for child processes @@ -1839,6 +1955,8 @@ static const luaL_Reg lsyncdlib[] = { "exec", l_exec }, { "log", l_log }, { "now", l_now }, + { "kill", l_kill }, + { "get_free_port", l_free_port }, { "nonobserve_fd", l_nonobserve_fd }, { "observe_fd", l_observe_fd }, { "readdir", l_readdir }, @@ -1861,7 +1979,7 @@ l_jiffies_add( lua_State *L ) if( p1 && p2 ) { logstring( "Error", "Cannot add two timestamps!" ); - exit( -1 ); + safeexit(L, -1 ); } { @@ -1881,6 +1999,33 @@ l_jiffies_add( lua_State *L ) } } +/* +| Adds a number in seconds to a jiffy timestamp. +*/ +static int +l_jiffies_concat( lua_State *L ) +{ + char buf[1024]; + clock_t *p1 = ( clock_t * ) lua_touserdata( L, 1 ); + clock_t *p2 = ( clock_t * ) lua_touserdata( L, 2 ); + + if( p1 && p2 ) + { + logstring( "Error", "Cannot add two timestamps!" ); + safeexit(L, -1 ); + } + + { + if (p1) { + snprintf( buf, sizeof(buf), "%Lf", (long double)(*p1)); + lua_pushfstring(L, "%s%s", &buf, luaL_checkstring( L, 2)); + } else { + snprintf( buf, sizeof(buf), "%Lf", (long double)(*p2)); + lua_pushfstring(L, "%s%s", luaL_checkstring( L, 1), &buf); + } + return 1; + } +} /* | Subracts two jiffy timestamps resulting in a number in seconds @@ -1987,6 +2132,9 @@ register_lsyncd( lua_State *L ) lua_pushcfunction( L, l_jiffies_eq ); lua_setfield( L, mt, "__eq" ); + lua_pushcfunction( L, l_jiffies_concat ); + lua_setfield( L, mt, "__concat" ); + lua_pop( L, 1 ); // pop(mt) #ifdef WITH_INOTIFY @@ -2162,7 +2310,7 @@ masterloop(lua_State *L) if( lua_pcall( L, 0, 1, -2 ) ) { - exit( -1 ); + safeexit(L, -1 ); } if( lua_type( L, -1 ) == LUA_TBOOLEAN) @@ -2348,7 +2496,9 @@ masterloop(lua_State *L) lua_pushinteger( L, WEXITSTATUS( status ) ); if ( lua_pcall( L, 2, 0, -4 ) ) - { exit(-1); } + { + safeexit(L, -1); + } lua_pop( L, 1 ); } @@ -2360,7 +2510,7 @@ masterloop(lua_State *L) if( lua_pcall( L, 0, 0, -2 ) ) { - exit( -1 ); + safeexit( L, -1 ); } lua_pop( L, 1 ); @@ -2377,7 +2527,7 @@ masterloop(lua_State *L) if( lua_pcall( L, 1, 0, -3 ) ) { - exit( -1 ); + safeexit(L, -1 ); } lua_pop( L, 1 ); @@ -2393,7 +2543,7 @@ masterloop(lua_State *L) if( lua_pcall( L, 1, 1, -3 ) ) { - exit( -1 ); + safeexit(L, -1 ); } if( !lua_toboolean( L, -1 ) ) @@ -2411,7 +2561,7 @@ masterloop(lua_State *L) "internal, stack is dirty." ); l_stackdump( L ); - exit( -1 ); + safeexit(L, -1 ); } } } @@ -2791,8 +2941,9 @@ main1( int argc, char *argv[] ) lsyncd_config_file, lua_tostring( L, -1 ) ); + l_traceback(L); - exit( -1 ); + safeexit(L, -1 ); } } diff --git a/lsyncd.h b/lsyncd.h index 1961a5c..9a4ec67 100644 --- a/lsyncd.h +++ b/lsyncd.h @@ -57,6 +57,7 @@ extern struct settings { int log_facility; // The syslog facility int log_level; // -1 logs everything, 0 normal mode, LOG_ERROR errors only. bool nodaemon; // True if Lsyncd shall not daemonize. + bool onepass; // True if Lsyncd should exit after first sync pass char * pidfile; // If not NULL Lsyncd writes its pid into this file. } settings; diff --git a/lsyncd.lua b/lsyncd.lua index 02922e4..5f1493a 100644 --- a/lsyncd.lua +++ b/lsyncd.lua @@ -25,17 +25,7 @@ then lsyncd.terminate( -1 ) end -lsyncd_version = '2.2.3' - - --- --- Hides the core interface from user scripts. --- -local _l = lsyncd -lsyncd = nil - -local lsyncd = _l -_l = nil +lsyncd_version = '2.3.0-beta1' -- @@ -47,6 +37,120 @@ now = lsyncd.now readdir = lsyncd.readdir +inheritKV = nil + +-- +-- Recurvely inherits a source table to a destionation table +-- copying all keys from source. +-- +-- All entries with integer keys are inherited as additional +-- sources for non-verbatim tables +-- +inherit = function +( + cd, -- table copy destination + cs, -- table copy source + verbatim, -- forced verbatim ( for e.g. 'exitcodes' ) + ignored -- table of keys not to copy +) + -- First copies all entries with non-integer keys. + -- + -- 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 + if type(ignored) == 'table' and table.contains(ignored, k) + then + -- do nothing + -- print("ignore x", k) + elseif + ( + type( k ) ~= 'number' + or verbatim + or cs._verbatim == true + ) + and + ( + type( cs ) ~= CountArray + and 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 + for k, v in ipairs( cs ) + do + if type( v ) == 'table' + then + inherit( cd, v ) + else + cd[ #cd + 1 ] = v + end + end + + end +end + + +table.contains = function +( + t, -- array to search in. Only the numeric values are tested + needle -- value to search for +) + for _, v in ipairs(t) do + if needle == v then + return true + end + end + return false +end + +-- lsyncd.inherit = inherit +-- print("inherit ") + + +-- +-- Helper to inherit. Inherits one key. +-- +inheritKV = + function( + cd, -- table copy destination + k, -- key + v -- value + ) + + -- don't merge inheritance controls + if k == '_merge' or k == '_verbatim' then return end + + local dtype = type( cd [ k ] ) + + if type( v ) == 'table' + then + if dtype == 'nil' + then + cd[ k ] = { } + inherit( cd[ k ], v, k == 'exitcodes' ) + elseif + dtype == 'table' and + v._merge ~= false + then + inherit( cd[ k ], v, k == 'exitcodes' ) + end + elseif dtype == 'nil' + then + cd[ k ] = v + end +end + -- -- Coping globals to ensure userscripts cannot change this. -- @@ -54,6 +158,8 @@ local log = log local terminate = terminate local now = now local readdir = readdir +local inherit = inherit +local inheritKV = inheritKV -- -- Predeclarations. @@ -75,6 +181,7 @@ local settingsCheckgauge = logfile = true, pidfile = true, nodaemon = true, + onepass = true, statusFile = true, statusInterval = true, logfacility = true, @@ -200,6 +307,10 @@ local CountArray = ( function t, -- table being accessed k -- key used to access ) + if k == '_merge' or k == '_verbatim' + then + return nil + end if type( k ) ~= 'number' then error( 'Key "' .. k .. '" invalid for CountArray', 2 ) @@ -990,6 +1101,7 @@ local Combiner = ( function -- The new delay replaces the old one if it's a file. -- local function logReplace + ( d1, -- old delay d2 -- new delay @@ -1793,6 +1905,20 @@ local InletFactory = ( function -- TODO give a readonly handler only. return sync.config end, + + -- + -- Returns the sync for this Inlet + -- + getSync = function( sync ) + return sync + end, + + -- + -- Substitutes parameters in arguments + -- + getSubstitutionData = function( sync, event, data) + return sync.getSubstitutionData(sync, event, data) + end, } -- @@ -2307,11 +2433,11 @@ local Sync = ( function end end end - - + + -- -- Returns true if the relative path is excluded or filtered - -- + -- local function testFilter ( self, -- the Sync @@ -2365,7 +2491,9 @@ local Sync = ( function local delay = self.processes[ pid ] -- not a child of this sync? - if not delay then return end + if not delay then + return false + end if delay.status then @@ -2400,6 +2528,9 @@ local Sync = ( function ' = ', exitcode ) + -- sets the initDone after the first success + self.initDone = true + else -- sets the delay on wait again local alarm = self.config.delay @@ -2447,6 +2578,8 @@ local Sync = ( function end self.processes[ pid ] = nil + -- we handled this process + return true end -- @@ -2803,6 +2936,18 @@ local Sync = ( function timestamp, ' )' ) + if self.disabled + then + return + end + + -- tunnel configured but not up + if self.config.tunnel and + self.config.tunnel:isReady() == false then + log('Tunnel', 'Tunnel for Sync ', self.config.name, ' not ready. Blocking events') + self.config.tunnel:blockSync(self) + return + end if self.processes:size( ) >= self.config.maxProcesses then @@ -2975,6 +3120,16 @@ local Sync = ( function f:write( '\n' ) end + -- + -- Returns substitude data for event + -- + local function getSubstitutionData(self, event, data) + if self.config.tunnel then + data = self.config.tunnel:getSubstitutionData(event, data) + end + return data + end + -- -- Creates a new Sync. -- @@ -2991,6 +3146,9 @@ local Sync = ( function processes = CountArray.new( ), excludes = Excludes.new( ), filters = nil, + initDone = false, + disabled = false, + tunnelBlock = nil, -- functions addBlanketDelay = addBlanketDelay, @@ -3007,6 +3165,7 @@ local Sync = ( function removeDelay = removeDelay, rmExclude = rmExclude, statusReport = statusReport, + getSubstitutionData = getSubstitutionData, } s.inlet = InletFactory.newInlet( s ) @@ -3085,6 +3244,625 @@ local Sync = ( function end )( ) +-- +-- Basic Tunnel provider. +-- +Tunnel = (function() + + Tunnel = {} + + local TUNNEL_CMD_TYPES = { + CMD = 1, + CHK = 2 + } + + local TUNNEL_STATUS = { + UNKNOWN = 0, + DOWN = 1, + DISABLED = 2, + CONNECTING = 3, + UP = 4, + RETRY_TIMEOUT = 5, + UP_RETRY_TIMEOUT = 6, + } + + local TUNNEL_MODES = { + COMMAND = "command", + POOL = "pool", + } + + local TUNNEL_DISTRIBUTION = { + ROUNDROBIN = "rr", + } + + local TUNNEL_SUBSTITIONS = { + "localhost", + "localport" + } + + local nextTunnelName = 1 + + Tunnel.defaults = { + mode = TUNNEL_MODES.COMMAND, + parallel = 1, + distribution = TUNNEL_DISTRIBUTION.ROUNDROBIN, + command = nil, + checkCommand = nil, + checkExitCodes = {0}, + checkMaxFailed = 5, + retryDelay = 10, + readyDelay = 5, + localhost = 'localhost', + } + -- export constants + Tunnel.TUNNEL_CMD_TYPES = TUNNEL_CMD_TYPES + Tunnel.TUNNEL_STATUS = TUNNEL_STATUS + Tunnel.TUNNEL_MODES = TUNNEL_MODES + Tunnel.TUNNEL_DISTRIBUTION = TUNNEL_DISTRIBUTION + Tunnel.TUNNEL_SUBSTITIONS = TUNNEL_SUBSTITIONS + + function Tunnel.new( + options + ) + local rv = { + processes = CountArray.new( ), + blocks = {}, + ready = false, + retryCount = 0, + status = TUNNEL_STATUS.DOWN, + alarm = false, + rrCounter = 0, + } + -- provides a default name if needed + if options.name == nil + then + options.name = 'Tunnel' .. nextTunnelName + end + + nextTunnelName = nextTunnelName + 1 + + inherit(options, Tunnel.defaults) + + rv.options = options + + inherit(rv, Tunnel) + + --setmetatable(rv, Tunnel) + -- self.__index = self + + return rv + end + + function Tunnel.statusToText(status) + for n, i in pairs(TUNNEL_STATUS) do + if status == i then + return n + end + end + return TUNNEL_STATUS.UNKNOWN + end + + -- Returns the status of tunnel as text + function Tunnel:statusText() + return Tunnel.statusToText(self.status) + end + + -- + -- Returns next alarm + -- + function Tunnel:getAlarm() + return self.alarm + end + + -- + -- User supplied function to check if tunnel is up + function Tunnel:check() + return true + end + + function Tunnel:isReady() + return self.status == TUNNEL_STATUS.UP or + self.status == TUNNEL_STATUS.UP_RETRY_TIMEOUT + end + + function Tunnel:setStatus(status) + log('Tunnel',self.options.name,': status change: ', + self:statusText(), " -> ", Tunnel.statusToText(status)) + self.status = status + end + + -- + -- Returns the number of processes currently running + -- + function Tunnel:countProcs(timestamp) + local run = 0 + local starting = 0 + local dead = 0 + if timestamp == nil then + timestamp = now() + end + + for pid, pd in self.processes:walk() do + if pd.type == TUNNEL_CMD_TYPES.CMD then + -- process needs to run for at least some time + if lsyncd.kill(pid, 0) ~= 0 then + dead = dead + 1 + elseif (pd.started + self.options.readyDelay) > timestamp then + starting = starting + 1 + else + pd.ready = true + run = run + 1 + end + end + end + + return run, starting, dead + end + + -- + -- Check if the tunnel is up + function Tunnel:invoke(timestamp) + -- lsyncd.kill() + if self:check() == false then + -- check failed, consider tunnel broken + self:setStatus(TUNNEL_STATUS.DOWN) + end + + if self.status == TUNNEL_STATUS.RETRY_TIMEOUT then + if self.alarm <= timestamp then + log( + 'Tunnel', + 'Retry setup ', self.options.name + ) + self:start() + return + else + -- timeout not yet reached + self.alarm = now() + 1 + return + end + elseif self.status == TUNNEL_STATUS.DOWN then + self:start() + return + elseif self.status == TUNNEL_STATUS.DISABLED then + self.alarm = false + return + end + + local parallel = self.options.parallel + local run, starting, dead = self:countProcs(timestamp) + + -- check if enough child processes are running + if self.status == TUNNEL_STATUS.CONNECTING then + if run > 0 then + log( + 'Tunnel', + 'Setup of tunnel ', self.options.name, ' sucessfull' + ) + self:setStatus(TUNNEL_STATUS.UP) + self:unblockSyncs() + end + elseif self.status == TUNNEL_STATUS.UP and run == 0 then + -- no good process running, degrade + log( + 'Tunnel', + 'Tunnel ', self.options.name, ' changed to CONNECTING' + ) + self:setStatus(TUNNEL_STATUS.CONNECTING) + end + + local spawned = 0 + -- start more processes if necesarry + while run + starting + spawned < self.options.parallel do + self:spawn() + spawned = spawned + 1 + end + + -- trigger next delay + if starting + spawned == 0 then + self.alarm = false + else + self.alarm = now() + 1 + end + end + + -- + -- Check if Sync is already blocked by Tunnel + function Tunnel:getBlockerForSync( + sync + ) + for _, eblock in ipairs(self.blocks) do + if eblock.sync == sync then + return eblock + end + end + return nil + end + + -- + -- Create a block on the sync until the tunnel reaches ready state + function Tunnel:blockSync( + sync + ) + local block = self:getBlockerForSync(sync) + + if block then + -- delay the block by another second + block:wait( now( ) + 1 ) + return + end + + local block = sync:addBlanketDelay() + sync.tunnelBlock = block + + table.insert (self.blocks, block) + -- set the new delay to be a block for existing delays + for _, eblock in sync.delays:qpairs() do + if eblock ~= block then + eblock:blockedBy(block) + end + end + -- delay tunnel check by 1 second + block:wait( now( ) + 1 ) + end + + -- + -- Create a block on the sync until the tunnel reaches ready state + function Tunnel:unblockSyncs() + for i,blk in ipairs(self.blocks) do + blk.sync:removeDelay(blk) + blk.sync.tunnelBlock = nil + end + self.blocks = {} + end + + -- + -- Spawn a single tunnel program + -- + function Tunnel:spawn() + local opts = { + type = TUNNEL_CMD_TYPES.CMD, + started = now(), + localhost = self.options.localhost, + ready = false, + } + local cmd = self.options.command + + if self.options.mode == TUNNEL_MODES.POOL then + opts.localport = lsyncd.get_free_port() + end + cmd = substitudeCommands(cmd, opts) + + if #cmd < 1 then + log('Error', + '', + self.options + ) + error( 'start tunnel of mode command with empty command', 2 ) + -- FIXME: add line which tunnel was called + return false + end + local bin = cmd[1] + -- for _,v in ipairs(cmd) do + -- if type( v ) ~= 'string' then + -- error( 'tunnel command must be a list of strings', 2 ) + -- end + -- end + log( + 'Info', + 'Start tunnel command ', + cmd + ) + local pid = lsyncd.exec(bin, table.unpack(cmd, 2)) + --local pid = spawn(bin, table.unpack(self.options.command, 2)) + if pid and pid > 0 then + self.processes[pid] = opts + self.retryCount = 0 + self.alarm = now() + 1 + else + self.alarm = now() + self.options.retryDelay + if self.status == TUNNEL_STATUS.UP then + self:setStatus(TUNNEL_STATUS.UP_RETRY_TIMEOUT) + else + self:setStatus(TUNNEL_STATUS.RETRY_TIMEOUT) + end + end + end + + function Tunnel:start() + if self.status == TUNNEL_STATUS.UP or + self.status == TUNNEL_STATUS.CONNECTING then + return + end + + if self.options.mode == TUNNEL_MODES.COMMAND or + self.options.mode == TUNNEL_MODES.POOL then + + self:setStatus(TUNNEL_STATUS.CONNECTING) + self:invoke(now()) + else + error('unknown tunnel mode:' .. self.options.mode) + self:setStatus(TUNNEL_STATUS.DISABLED) + end + end + + -- + -- collect pids of exited child processes. Restart the tunnel if necessary + --- + function Tunnel:collect ( + pid, + exitcode + ) + local proc = self.processes[pid] + + if proc == nil then + return false + end + log('Debug', + "collect tunnel event. pid: ", pid," exitcode: ", exitcode) + local run, starting, dead = self:countProcs() + -- cases in which the tunnel command is handled + if proc.type == TUNNEL_CMD_TYPES.CMD then + if self.status == TUNNEL_STATUS.CONNECTING then + log( + 'Warning', + 'Starting tunnel failed.', + self.options.name + ) + self:setStatus(TUNNEL_STATUS.RETRY_TIMEOUT) + self.alarm = now() + self.options.retryDelay + else + log( + 'Info', + 'Tunnel died. Will Restarting', + self.options.name + ) + + if run == 0 then + self:setStatus(TUNNEL_STATUS.DOWN) + end + self.alarm = true + end + -- cases in which the check function has executed a program + elseif proc.type == TUNNEL_CMD_TYPES.CHK then + local good = false + if type(self.options.checkExitCodes) == 'table' then + + for _,i in iwalk(self.options.checkExitCodes) do + if exitcode == i then + good = true + end + end + else + if self.options.checkExitCodes == exitcode then + good = true + end + end + if good then + if self.isReady() == false then + log( + 'Info', + self.options.name, + ' Tunnel setup complete ' + ) + end + self:setStatus(TUNNEL_STATUS.UP) + self.checksFailed = 0 + else + if self.ready then + log( + 'Tunnel', + self.options.name + ' Check failed.' + ) + self.checksFailed = self.checksFailed + 1 + if self.checksFailed > self.options.checkMaxFailed then + self:kill() + end + end + self:setStatus(TUNNEL_STATUS.DOWN) + end + end + self.processes[pid] = nil + end + + -- + -- Stops all tunnel processes + -- + function Tunnel:kill () + log('Tunnel', 'Shutdown tunnel ', self.options.name) + for pid, pr in self.processes:walk() do + if pr.type == TUNNEL_CMD_TYPES.CMD then + log('Tunnel','Kill process ', pid) + lsyncd.kill(pid, 9) + end + end + self:setStatus(TUNNEL_STATUS.DISABLED) + end + + function Tunnel:isReady () + return self.status == TUNNEL_STATUS.UP + end + + -- + -- Fills/changes the opts table with additional values + -- for the transfer to be started + -- + function Tunnel:getSubstitutionData(event, opts) + local useProc, useProcLast = nil, nil + if self.options.mode == TUNNEL_MODES.POOL then + if self.options.distribution == TUNNEL_DISTRIBUTION.ROUNDROBIN then + local i = 0 + for pid, proc in self.processes:walk() do + if proc.ready == true then + useProcLast = proc + if (i % self.processes:size()) == self.rrCounter then + useProc = proc + self.rrCounter = self.rrCounter + 1 + end + end + end + if useProc == nil then + self.rrCounter = 0 + useProc = useProcLast + end + else + log('Tunnel', 'Unknown distribution mode: ', self.options.distribution) + os.exit(1) + end + end + if useProc then + for k,v in pairs(self.TUNNEL_SUBSTITIONS) do + if useProc[v] ~= nil then + opts[v] = useProc[v] + end + end + end + return opts + end + + -- + -- Writes a status report about this tunnel + -- + function Tunnel:statusReport(f) + f:write( 'Tunnel: name=', self.options.name, ' status=', self:statusText(), '\n' ) + + f:write( 'Running processes: ', self.processes:size( ), '\n') + + for pid, prc in self.processes:walk( ) + do + f:write(" pid=", pid, " type=", prc.type, " started="..prc.started, '\n') + end + + f:write( '\n' ) + end + + return Tunnel + +end)() -- Tunnel scope + +-- +-- Tunnels - a singleton +-- +-- Tunnels maintains all configured tunnels. +-- +local Tunnels = ( function + ( ) + -- + -- the list of all tunnels + -- + local tunnelList = Array.new( ) + + -- + -- Returns sync at listpos i + -- + local function get + ( i ) + return tunnelList[ i ]; + end + + -- + -- Adds a new tunnel. + -- + local function add + ( + tunnel + ) + table.insert( tunnelList, tunnel ) + + return tunnel + end + + -- + -- Allows a for-loop to walk through all syncs. + -- + local function iwalk + ( ) + return ipairs( tunnelList ) + end + + -- + -- Returns the number of syncs. + -- + local size = function + ( ) + return #tunnelList + end + + local nextCycle = false + -- + -- Cycle through all tunnels and call their invoke function + -- + local function invoke(timestamp) + for _,tunnel in ipairs( tunnelList ) + do + tunnel:invoke(timestamp) + end + nextCycle = now() + 5 + end + + -- + -- returns the next alarm + -- + local function getAlarm() + local rv = nextCycle + for _, tunnel in ipairs( tunnelList ) do + local ta = tunnel:getAlarm() + if ta ~= false + and ta < rv then + rv = ta + end + end + + return rv + end + + -- + -- closes all tunnels + -- + local function killAll() + local rv = true + for _, tunnel in ipairs( tunnelList ) do + local ta = tunnel:kill() + if ta ~= true then + rv = false + end + end + + return rv + end + + -- + -- Public interface + -- + return { + add = add, + get = get, + iwalk = iwalk, + size = size, + invoke = invoke, + getAlarm = getAlarm, + killAll = killAll, + statusReport = statusReport + } + end )( ) + + +-- +-- create a new tunnel from the passed options and registers the tunnel +tunnel = function (options) + log( + 'Debug', + 'create tunnel:', options + ) + local rv = Tunnel.new(options) + Tunnels.add(rv) + + return rv + +end + + -- -- Syncs - a singleton -- @@ -3134,100 +3912,6 @@ local Syncs = ( function return syncsList[ i ]; end - -- - -- Helper function for inherit - -- defined below - -- - local inheritKV - - -- - -- Recurvely inherits a source table to a destionation table - -- copying all keys from source. - -- - -- All entries with integer keys are inherited as additional - -- sources for non-verbatim tables - -- - 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. - -- - -- 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 - if - ( - type( k ) ~= 'number' - or verbatim - or cs._verbatim == true - ) - and - ( - 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 - for k, v in ipairs( cs ) - do - if type( v ) == 'table' - then - inherit( cd, v ) - else - cd[ #cd + 1 ] = v - end - end - - end - end - - -- - -- Helper to inherit. Inherits one key. - -- - inheritKV = - function( - cd, -- table copy destination - k, -- key - v -- value - ) - - -- don't merge inheritance controls - if k == '_merge' or k == '_verbatim' then return end - - local dtype = type( cd [ k ] ) - - if type( v ) == 'table' - then - if dtype == 'nil' - then - cd[ k ] = { } - inherit( cd[ k ], v, k == 'exitcodes' ) - elseif - dtype == 'table' and - v._merge ~= false - then - inherit( cd[ k ], v, k == 'exitcodes' ) - end - elseif dtype == 'nil' - then - cd[ k ] = v - end - end - -- -- Adds a new sync. @@ -3255,13 +3939,18 @@ local Syncs = ( function config = { } - inherit( config, uconfig ) + -- inherit the config but do not deep copy the tunnel object + -- the tunnel object is a reference to a object that might be shared + inherit( config, uconfig, nil, {"tunnel"} ) -- -- last and least defaults are inherited -- inherit( config, default ) + -- copy references + config.tunnel = uconfig.tunnel + local inheritSettings = { 'delay', 'maxDelays', @@ -3478,6 +4167,29 @@ function splitQuotedString return rv end +function substitudeCommands(cmd, data) + assert(type(data) == "table") + local getData = function(arg) + local rv = data[arg] + if rv ~= nil then + return rv + else + return "" + end + end + if type(cmd) == "string" then + return string.gsub(cmd, "%${(%w+)}", getData) + elseif type(cmd) == "table" then + local rv = {} + for i, v in ipairs(cmd) do + rv[i] = string.gsub(v, "%${(%w+)}", getData) + end + return rv + else + log("Error", "Unsupported type in replacCommand") + end +end + -- -- Interface to inotify. -- @@ -4349,6 +5061,13 @@ local StatusFile = ( function f:write( '\n' ) end + for i, t in Tunnels.iwalk( ) + do + t:statusReport( f ) + + f:write( '\n' ) + end + Inotify.statusReport( f ) f:close( ) @@ -4519,17 +5238,27 @@ function runner.collectProcess pid, -- process id exitcode -- exitcode ) - processCount = processCount - 1 + for _, s in Syncs.iwalk( ) + do + if s:collect( pid, exitcode ) then + processCount = processCount - 1 + break + end + end + + for _, s in Tunnels.iwalk( ) + do + if s:collect( pid, exitcode ) then + break + end + end if processCount < 0 then error( 'negative number of processes!' ) end - for _, s in Syncs.iwalk( ) - do - if s:collect( pid, exitcode ) then return end - end + end -- @@ -4566,6 +5295,7 @@ function runner.cycle( return true else + Tunnels.killAll() return false end end @@ -4575,6 +5305,31 @@ function runner.cycle( error( 'runner.cycle() called while not running!' ) end + -- check and start tunnels + if not uSettings.maxProcesses + or processCount < uSettings.maxProcesses + then + Tunnels.invoke( timestamp ) + end + + if uSettings.onepass + then + local allDone = true + for i, s in Syncs.iwalk( ) + do + if s.initDone == true + then + s.disabled = true + else + allDone = false + end + end + if allDone and processCount == 0 then + log( 'Info', 'onepass active and all syncs finished. Exiting successfully') + os.exit(0) + end + end + -- -- goes through all syncs and spawns more actions -- if possibly. But only let Syncs invoke actions if @@ -4643,6 +5398,7 @@ OPTIONS: -log [Category] Turns on logging for a debug category -logfile FILE Writes log to FILE (DEFAULT: uses syslog) -nodaemon Does not detach and logs to stdout/stderr + -onepass Sync once and exit -pidfile FILE Writes Lsyncds PID into FILE -runner FILE Loads Lsyncds lua part from FILE -script FILE Script to load before execting runner (ADVANCED) @@ -4760,6 +5516,15 @@ function runner.configure( args, monitors ) end }, + onepass = + { + 0, + function + ( ) + clSettings.onepass = true + end + }, + pidfile = { 1, @@ -5065,7 +5830,7 @@ function runner.initialize( firstTime ) -- makes sure the user gave Lsyncd anything to do if Syncs.size() == 0 then - log( 'Error', 'Nothing to watch!' ) + log( 'Error', 'Nothing to watch!' ) os.exit( -1 ) end @@ -5183,6 +5948,8 @@ function runner.getAlarm 'at global process limit.' ) end + -- checks for tunnel alarm + checkAlarm( Tunnels.getAlarm( ) ) -- checks if a statusfile write has been delayed checkAlarm( StatusFile.getAlarm( ) ) @@ -5263,6 +6030,19 @@ function runner.term end +-- +-- Called by core on a term signal. +-- +function runner.teardown + ( + exitCode -- exitcode that will be returned + ) + -- ensure we will all stray tunnels when we hard exit + Tunnels.killAll() + + return exitCode +end + --============================================================================ -- Lsyncd runner's user interface --============================================================================ @@ -5387,7 +6167,7 @@ function spawnShell command, -- the shell command ... -- additonal arguments ) - return spawn( agent, '/bin/sh', '-c', command, '/bin/sh', ... ) + return spawn( agent, 'sh', '-c', command, 'sh', ... ) end diff --git a/tests/testlib.lua b/tests/testlib.lua index 34fee4e..5a5ba69 100644 --- a/tests/testlib.lua +++ b/tests/testlib.lua @@ -1,8 +1,7 @@ -- common testing environment posix = require( 'posix' ) string = require( 'string' ) -path = require( 'pl.path' ) -stringx = require( 'pl.stringx' ) + local sys_stat = require "posix.sys.stat" -- escape codes to colorize output on terminal @@ -94,10 +93,39 @@ function writefile return true end +function splitpath(P) + local i = #P + local ch = P:sub(i,i) + while i > 0 and ch ~= "/" do + i = i - 1 + ch = P:sub(i,i) + end + if i == 0 then + return '',P + else + return P:sub(1,i-1), P:sub(i+1) + end +end + +function isabs(p) + return string.sub(p, 1, 2) == "/" +end + +function abspath(P,pwd) + P = P:gsub('[\\/]$','') + if not isabs(P) then + local rv = posix.unistd.getcwd() .. "/" .. P + return rv + end + return P +end + + function script_path() -- local str = debug.getinfo(2, "S").source:sub(2) -- return str:match("(.*/)") - return path.dirname(path.abspath(debug.getinfo(1).short_src)) + local dir, file = splitpath(abspath(debug.getinfo(1).short_src)) + return dir end function which(exec) @@ -159,6 +187,11 @@ function startSshd() return true end + +function strip(s) + return s:match "^%s*(.-)%s*$" +end + -- -- Stop test ssh server -- @@ -169,7 +202,7 @@ function stopSshd() then return false end - pid = stringx.strip(f:read("*a")) + pid = strip(f:read("*a")) posix.kill(tonumber(pid)) end diff --git a/tests/utils_test.lua b/tests/utils_test.lua index c7e6942..c793c3f 100644 --- a/tests/utils_test.lua +++ b/tests/utils_test.lua @@ -9,4 +9,19 @@ assert(isTableEqual( {"-p", "22", "-i", "/home/test/bla blu/id_rsa"} )) +-- test string replacement +local testData = { + localPort = 1234, + localHost = "localhorst" +} +assert(substitudeCommands("echo ssh ${localHost}:${localPort}", testData) == + "echo ssh localhorst:1234") + +assert(isTableEqual( + substitudeCommands({"-p${doesNotExist}", "2${localHost}2", "-i '${localPort}'"}, testData), + {"-p", "2localhorst2", "-i '1234'"} +)) + +assert(type(lsyncd.get_free_port()) == "number") + os.exit(0) \ No newline at end of file