diff --git a/README.md b/README.md index 9942b2e..3a703c6 100644 --- a/README.md +++ b/README.md @@ -1,49 +1,63 @@ # Internet detector for OpenWrt. -Internet-detector is an application for checking the availability of the Internet. Performs periodic connections to a known public host (8.8.8.8, 1.1.1.1) and determines the actual Internet availability. - -**OpenWrt** >= 19.07. - -**Dependences:** lua, luaposix, libuci-lua. +Internet-detector is an application for checking the availability of the Internet. Performs periodic connections to a known public host and determines the actual Internet availability. **Features:** - It can run continuously as a system service or only in an open web interface. - Checking the availability of a host using ping or by connecting via TCP to a specified port. - LED indication of Internet availability. ![](https://github.com/gSpotx2f/luci-app-internet-detector/blob/master/screenshots/internet-led.jpg) - - Performing actions when connecting and disconnecting the Internet (Restarting network, modem or device. Executing custom shell scripts). - - Sending email notification when Internet access is restored. + - Performing actions when connecting and disconnecting the Internet: rebooting device, restarting network or modem (internet-detector-mod-modem-restart), executing custom shell scripts. + - Sending email notification when Internet access is restored (internet-detector-mod-email). - The daemon is written entirely in Lua using the luaposix library. -## Installation notes +**Dependences:** lua, luaposix, libuci-lua. -**OpenWrt >= 21.02:** +## Installation notes (OpenWrt >= 21.02) opkg update - wget --no-check-certificate -O /tmp/internet-detector_1.2-0_all.ipk https://github.com/gSpotx2f/packages-openwrt/raw/master/current/internet-detector_1.2-0_all.ipk - opkg install /tmp/internet-detector_1.2-0_all.ipk - rm /tmp/internet-detector_1.2-0_all.ipk + wget --no-check-certificate -O /tmp/internet-detector_1.3-0_all.ipk https://github.com/gSpotx2f/packages-openwrt/raw/master/current/internet-detector_1.3-0_all.ipk + opkg install /tmp/internet-detector_1.3-0_all.ipk + rm /tmp/internet-detector_1.3-0_all.ipk /etc/init.d/internet-detector start /etc/init.d/internet-detector enable - wget --no-check-certificate -O /tmp/luci-app-internet-detector_1.2-0_all.ipk https://github.com/gSpotx2f/packages-openwrt/raw/master/current/luci-app-internet-detector_1.2-0_all.ipk - opkg install /tmp/luci-app-internet-detector_1.2-0_all.ipk - rm /tmp/luci-app-internet-detector_1.2-0_all.ipk + wget --no-check-certificate -O /tmp/luci-app-internet-detector_1.3-0_all.ipk https://github.com/gSpotx2f/packages-openwrt/raw/master/current/luci-app-internet-detector_1.3-0_all.ipk + opkg install /tmp/luci-app-internet-detector_1.3-0_all.ipk + rm /tmp/luci-app-internet-detector_1.3-0_all.ipk /etc/init.d/rpcd restart -Email notification: - - opkg install mailsend - i18n-ru: - wget --no-check-certificate -O /tmp/luci-i18n-internet-detector-ru_1.2-0_all.ipk https://github.com/gSpotx2f/packages-openwrt/raw/master/current/luci-i18n-internet-detector-ru_1.2-0_all.ipk - opkg install /tmp/luci-i18n-internet-detector-ru_1.2-0_all.ipk - rm /tmp/luci-i18n-internet-detector-ru_1.2-0_all.ipk - -**[OpenWrt 19.07](https://github.com/gSpotx2f/luci-app-internet-detector/tree/19.07)** + wget --no-check-certificate -O /tmp/luci-i18n-internet-detector-ru_1.3-0_all.ipk https://github.com/gSpotx2f/packages-openwrt/raw/master/current/luci-i18n-internet-detector-ru_1.3-0_all.ipk + opkg install /tmp/luci-i18n-internet-detector-ru_1.3-0_all.ipk + rm /tmp/luci-i18n-internet-detector-ru_1.3-0_all.ipk ## Screenshots: ![](https://github.com/gSpotx2f/luci-app-internet-detector/blob/master/screenshots/01.jpg) ![](https://github.com/gSpotx2f/luci-app-internet-detector/blob/master/screenshots/02.jpg) ![](https://github.com/gSpotx2f/luci-app-internet-detector/blob/master/screenshots/03.jpg) + +## Modem restart module (internet-detector-mod-modem-restart): + +**Dependences:** modemmanager. + + wget --no-check-certificate -O /tmp/internet-detector-mod-modem-restart_1.3-0_all.ipk https://github.com/gSpotx2f/packages-openwrt/raw/master/current/internet-detector-mod-modem-restart_1.3-0_all.ipk + opkg install /tmp/internet-detector-mod-modem-restart_1.3-0_all.ipk + rm /tmp/internet-detector-mod-modem-restart_1.3-0_all.ipk + /etc/init.d/internet-detector restart + +![](https://github.com/gSpotx2f/luci-app-internet-detector/blob/master/screenshots/04.jpg) + +## Email notification module (internet-detector-mod-email): + +**Dependences:** mailsend. + + wget --no-check-certificate -O /tmp/internet-detector-mod-email_1.3-0_all.ipk https://github.com/gSpotx2f/packages-openwrt/raw/master/current/internet-detector-mod-email_1.3-0_all.ipk + opkg install /tmp/internet-detector-mod-email_1.3-0_all.ipk + rm /tmp/internet-detector-mod-email_1.3-0_all.ipk + /etc/init.d/internet-detector restart + +![](https://github.com/gSpotx2f/luci-app-internet-detector/blob/master/screenshots/05.jpg) + +## [OpenWrt 19.07](https://github.com/gSpotx2f/luci-app-internet-detector/tree/19.07) diff --git a/internet-detector-mod-email/Makefile b/internet-detector-mod-email/Makefile new file mode 100644 index 0000000..07ec8b4 --- /dev/null +++ b/internet-detector-mod-email/Makefile @@ -0,0 +1,41 @@ +# +# (с) 2024 gSpot (https://github.com/gSpotx2f/luci-app-internet-detector) +# + +include $(TOPDIR)/rules.mk + +PKG_NAME:=internet-detector-mod-email +PKG_VERSION:=1.3 +PKG_RELEASE:=0 +PKG_MAINTAINER:=gSpot + +include $(INCLUDE_DIR)/package.mk + +define Package/$(PKG_NAME) + SECTION:=net + CATEGORY:=Network + TITLE:=Email module for internet-detector + URL:=https://github.com/gSpotx2f/luci-app-internet-detector + PKGARCH:=all + DEPENDS:=+internet-detector +mailsend +endef + +define Package/$(PKG_NAME)/description + Email support for internet-detector. +endef + +define Package/$(PKG_NAME)/conffiles +endef + +define Build/Configure +endef + +define Build/Compile +endef + +define Package/$(PKG_NAME)/install + $(INSTALL_DIR) $(1)/usr/lib/lua/internet-detector + $(INSTALL_DATA) ./files/usr/lib/lua/internet-detector/mod_email.lua $(1)/usr/lib/lua/internet-detector/mod_email.lua +endef + +$(eval $(call BuildPackage,$(PKG_NAME))) diff --git a/internet-detector/files/usr/lib/internet-detector/mod_email.lua b/internet-detector-mod-email/files/usr/lib/lua/internet-detector/mod_email.lua similarity index 60% rename from internet-detector/files/usr/lib/internet-detector/mod_email.lua rename to internet-detector-mod-email/files/usr/lib/lua/internet-detector/mod_email.lua index d1f4cb7..f75055e 100644 --- a/internet-detector/files/usr/lib/internet-detector/mod_email.lua +++ b/internet-detector-mod-email/files/usr/lib/lua/internet-detector/mod_email.lua @@ -13,27 +13,42 @@ local Module = { syslog = function(level, msg) return true end, writeValue = function(filePath, str) return false end, readValue = function(filePath) return nil end, + deadPeriod = 0, alivePeriod = 0, + mode = 0, -- 0: connected, 1: disconnected, 2: both hostAlias = "OpenWrt", mta = "/usr/bin/mailsend", - mailRecipient = "email@gmail.com", - mailSender = "email@gmail.com", - mailUser = "email@gmail.com", - mailPassword = "password", - mailSmtp = "smtp.gmail.com", - mailSmtpPort = '587', + mtaConnectTimeout = 5, -- default = 5 + mtaReadTimeout = 5, -- default = 5 + mailRecipient = nil, + mailSender = nil, + mailUser = nil, + mailPassword = nil, + mailSmtp = nil, + mailSmtpPort = nil, mailSecurity = "tls", status = nil, _enabled = false, + _deadCounter = 0, _aliveCounter = 0, - _msgSent = true, + _msgSentDisconnect = true, + _msgSentConnect = true, _disconnected = true, _lastDisconnection = nil, _lastConnection = nil, + _message = {}, } function Module:init(t) - self.alivePeriod = tonumber(t.alive_period) + if t.mode ~= nil then + self.mode = tonumber(t.mode) + end + if t.dead_period ~= nil then + self.deadPeriod = tonumber(t.dead_period) + end + if t.alive_period ~= nil then + self.alivePeriod = tonumber(t.alive_period) + end if t.host_alias then self.hostAlias = t.host_alias else @@ -46,7 +61,10 @@ function Module:init(t) self.mailPassword = t.mail_password self.mailSmtp = t.mail_smtp self.mailSmtpPort = t.mail_smtp_port - self.mailSecurity = t.mail_security + + if t.mail_security ~= nil then + self.mailSecurity = t.mail_security + end if unistd.access(self.mta, "x") then self._enabled = true @@ -83,11 +101,19 @@ function Module:sendMessage(msg) end local mtaCmd = string.format( - '%s%s %s -smtp "%s" -port %s -cs utf-8 -user "%s" -pass "%s" -f "%s" -t "%s" -sub "%s" -M "%s"', + '%s%s %s -smtp "%s" -port %s -ct %s -read-timeout %s -cs utf-8 -user "%s" -pass "%s" -f "%s" -t "%s" -sub "%s" -M "%s"', self.mta, verboseArg, securityArgs, self.mailSmtp, self.mailSmtpPort, + self.mtaConnectTimeout, self.mtaReadTimeout, self.mailUser, self.mailPassword, self.mailSender, self.mailRecipient, string.format("%s notification", self.hostAlias), - string.format("%s:\n%s", self.hostAlias, msg)) + string.format("[%s]: %s:\n%s", self.hostAlias, self.config.serviceConfig.instance, msg)) + + -- Debug + if self.config.debug then + io.stdout:write(string.format("%s: %s\n", self.name, mtaCmd)) + io.stdout:flush() + self.syslog("debug", string.format("%s: %s", self.name, mtaCmd)) + end if os.execute(mtaCmd) ~= 0 then self.syslog("err", string.format( @@ -102,44 +128,46 @@ function Module:run(currentStatus, lastStatus, timeDiff) if not self._enabled then return end - if currentStatus == 1 then self._aliveCounter = 0 - self._msgSent = false + self._msgSentConnect = false self._lastConnection = nil if not self._disconnected then self._disconnected = true if not self._lastDisconnection then self._lastDisconnection = os.date("%Y.%m.%d %H:%M:%S", os.time()) end + self._message[#self._message + 1] = string.format( + "Internet disconnected: %s", self._lastDisconnection) + end + if not self._msgSentDisconnect and (self.mode == 1 or self.mode == 2) then + if self._deadCounter >= self.deadPeriod then + self._lastDisconnection = nil + self:sendMessage(table.concat(self._message, ", ")) + self._message = {} + self._msgSentDisconnect = true + else + self._deadCounter = self._deadCounter + timeDiff + end end - else - - if not self._msgSent then - + self._deadCounter = 0 + self._msgSentDisconnect = false + if not self._msgSentConnect and (self.mode == 0 or self.mode == 2) then if not self._lastConnection then self._lastConnection = os.date("%Y.%m.%d %H:%M:%S", os.time()) end if self._aliveCounter >= self.alivePeriod then - local message = {} - if self._lastDisconnection then - message[#message + 1] = string.format( - "Internet disconnected: %s", self._lastDisconnection) - self._lastDisconnection = nil - end - if self._lastConnection then - message[#message + 1] = string.format( + self._message[#self._message + 1] = string.format( "Internet connected: %s", self._lastConnection) - self:sendMessage(table.concat(message, ", ")) - self._msgSent = true - end + self:sendMessage(table.concat(self._message, "; ")) + self._message = {} + self._msgSentConnect = true else self._aliveCounter = self._aliveCounter + timeDiff end end - self._disconnected = false end end diff --git a/internet-detector-mod-modem-restart/Makefile b/internet-detector-mod-modem-restart/Makefile new file mode 100644 index 0000000..da442df --- /dev/null +++ b/internet-detector-mod-modem-restart/Makefile @@ -0,0 +1,41 @@ +# +# (с) 2024 gSpot (https://github.com/gSpotx2f/luci-app-internet-detector) +# + +include $(TOPDIR)/rules.mk + +PKG_NAME:=internet-detector-mod-modem-restart +PKG_VERSION:=1.3 +PKG_RELEASE:=0 +PKG_MAINTAINER:=gSpot + +include $(INCLUDE_DIR)/package.mk + +define Package/$(PKG_NAME) + SECTION:=net + CATEGORY:=Network + TITLE:=Modem restart module for internet-detector + URL:=https://github.com/gSpotx2f/luci-app-internet-detector + PKGARCH:=all + DEPENDS:=+internet-detector +modemmanager +endef + +define Package/$(PKG_NAME)/description + Support modem restart for internet detector. +endef + +define Package/$(PKG_NAME)/conffiles +endef + +define Build/Configure +endef + +define Build/Compile +endef + +define Package/$(PKG_NAME)/install + $(INSTALL_DIR) $(1)/usr/lib/lua/internet-detector + $(INSTALL_DATA) ./files/usr/lib/lua/internet-detector/mod_modem_restart.lua $(1)/usr/lib/lua/internet-detector/mod_modem_restart.lua +endef + +$(eval $(call BuildPackage,$(PKG_NAME))) diff --git a/internet-detector/files/usr/lib/internet-detector/mod_modem_restart.lua b/internet-detector-mod-modem-restart/files/usr/lib/lua/internet-detector/mod_modem_restart.lua similarity index 77% rename from internet-detector/files/usr/lib/internet-detector/mod_modem_restart.lua rename to internet-detector-mod-modem-restart/files/usr/lib/lua/internet-detector/mod_modem_restart.lua index 405e263..c9e15a9 100644 --- a/internet-detector/files/usr/lib/internet-detector/mod_modem_restart.lua +++ b/internet-detector-mod-modem-restart/files/usr/lib/lua/internet-detector/mod_modem_restart.lua @@ -13,7 +13,7 @@ local Module = { readValue = function(filePath) return nil end, mmcli = "/usr/bin/mmcli", mmInit = "/etc/init.d/modemmanager", - deadPeriod = 0, + deadPeriod = 600, iface = nil, anyBand = false, status = nil, @@ -23,12 +23,21 @@ local Module = { } function Module:toggleIface(flag) + if not self.iface then + return + end return os.execute( string.format("%s %s", (flag and "/sbin/ifup" or "/sbin/ifdown"), self.iface) ) end function Module:restartMM() + if os.execute(string.format("%s enabled", self.mmInit)) ~= 0 then + self.syslog("warning", string.format( + "%s: modemmanager service is disabled", self.name)) + return + end + if self.anyBand then self.syslog("info", string.format( "%s: resetting current-bands to 'any'", self.name)) @@ -47,15 +56,22 @@ function Module:restartMM() end function Module:init(t) - self.deadPeriod = tonumber(t.dead_period) - self.iface = t.iface - self.anyBand = (tonumber(t.any_band) ~= 0) + if t.dead_period ~= nil then + self.deadPeriod = tonumber(t.dead_period) + end + if t.iface ~= nil then + self.iface = t.iface + end + if t.any_band ~= nil then + self.anyBand = (tonumber(t.any_band) ~= 0) + end if not unistd.access(self.mmcli, "x") then self.anyBand = false end - if unistd.access(self.mmInit, "x") then + if (unistd.access(self.mmInit, "x") + and os.execute(string.format("%s enabled", self.mmInit)) == 0) then self._enabled = true else self._enabled = false diff --git a/internet-detector/Makefile b/internet-detector/Makefile index 5f489f5..3a1ae22 100644 --- a/internet-detector/Makefile +++ b/internet-detector/Makefile @@ -5,7 +5,7 @@ include $(TOPDIR)/rules.mk PKG_NAME:=internet-detector -PKG_VERSION:=1.2 +PKG_VERSION:=1.3 PKG_RELEASE:=0 PKG_MAINTAINER:=gSpot @@ -50,14 +50,13 @@ define Package/$(PKG_NAME)/install $(INSTALL_BIN) ./files/etc/init.d/internet-detector $(1)/etc/init.d/internet-detector $(INSTALL_DIR) $(1)/usr/bin $(INSTALL_BIN) ./files/usr/bin/internet-detector $(1)/usr/bin/internet-detector - $(INSTALL_DIR) $(1)/usr/lib/internet-detector - $(INSTALL_DATA) ./files/usr/lib/internet-detector/mod_email.lua $(1)/usr/lib/internet-detector/mod_email.lua - $(INSTALL_DATA) ./files/usr/lib/internet-detector/mod_public_ip.lua $(1)/usr/lib/internet-detector/mod_public_ip.lua - $(INSTALL_DATA) ./files/usr/lib/internet-detector/mod_led_control.lua $(1)/usr/lib/internet-detector/mod_led_control.lua - $(INSTALL_DATA) ./files/usr/lib/internet-detector/mod_modem_restart.lua $(1)/usr/lib/internet-detector/mod_modem_restart.lua - $(INSTALL_DATA) ./files/usr/lib/internet-detector/mod_network_restart.lua $(1)/usr/lib/internet-detector/mod_network_restart.lua - $(INSTALL_DATA) ./files/usr/lib/internet-detector/mod_reboot.lua $(1)/usr/lib/internet-detector/mod_reboot.lua - $(INSTALL_DATA) ./files/usr/lib/internet-detector/mod_user_scripts.lua $(1)/usr/lib/internet-detector/mod_user_scripts.lua + $(INSTALL_DIR) $(1)/usr/lib/lua/internet-detector + $(INSTALL_DATA) ./files/usr/lib/lua/internet-detector/main.lua $(1)/usr/lib/lua/internet-detector/main.lua + $(INSTALL_DATA) ./files/usr/lib/lua/internet-detector/mod_led_control.lua $(1)/usr/lib/lua/internet-detector/mod_led_control.lua + $(INSTALL_DATA) ./files/usr/lib/lua/internet-detector/mod_reboot.lua $(1)/usr/lib/lua/internet-detector/mod_reboot.lua + $(INSTALL_DATA) ./files/usr/lib/lua/internet-detector/mod_network_restart.lua $(1)/usr/lib/lua/internet-detector/mod_network_restart.lua + $(INSTALL_DATA) ./files/usr/lib/lua/internet-detector/mod_public_ip.lua $(1)/usr/lib/lua/internet-detector/mod_public_ip.lua + $(INSTALL_DATA) ./files/usr/lib/lua/internet-detector/mod_user_scripts.lua $(1)/usr/lib/lua/internet-detector/mod_user_scripts.lua endef $(eval $(call BuildPackage,$(PKG_NAME))) diff --git a/internet-detector/files/etc/config/internet-detector b/internet-detector/files/etc/config/internet-detector index 54356ca..2bfcc31 100644 --- a/internet-detector/files/etc/config/internet-detector +++ b/internet-detector/files/etc/config/internet-detector @@ -1,3 +1,4 @@ + config main 'config' option mode '1' option enable_logger '1' @@ -12,7 +13,6 @@ config instance 'internet' option interval_down '5' option connection_attempts '2' option connection_timeout '2' - option mod_led_control_enabled '0' option mod_reboot_enabled '0' option mod_reboot_dead_period '3600' option mod_reboot_force_reboot_delay '300' @@ -30,9 +30,8 @@ config instance 'internet' option mod_public_ip_timeout '3' option mod_public_ip_enable_ip_script '0' option mod_email_enabled '0' + option mod_email_mode '0' option mod_email_alive_period '0' - option mod_email_mail_smtp 'smtp.gmail.com' - option mod_email_mail_smtp_port '587' option mod_email_mail_security 'tls' option mod_user_scripts_enabled '0' option mod_user_scripts_alive_period '0' diff --git a/internet-detector/files/usr/bin/internet-detector b/internet-detector/files/usr/bin/internet-detector index 19491d3..7c510b5 100755 --- a/internet-detector/files/usr/bin/internet-detector +++ b/internet-detector/files/usr/bin/internet-detector @@ -11,633 +11,7 @@ (с) 2024 gSpot (https://github.com/gSpotx2f/luci-app-internet-detector) --]] --- Importing packages - -local dirent = require("posix.dirent") -local fcntl = require("posix.fcntl") -local signal = require("posix.signal") -local socket = require("posix.sys.socket") -local stat = require("posix.sys.stat") -local syslog = require("posix.syslog") -local time = require("posix.time") -local unistd = require("posix.unistd") -local uci = require("uci") - --- Default settings - -local InternetDetector = { - mode = 0, -- 0: disabled, 1: Service, 2: UI detector - enableLogger = true, - hostname = "OpenWrt", - appName = "internet-detector", - commonDir = "/tmp/run", - pingCmd = "/bin/ping", - pingParams = "-c 1", - uiRunTime = 30, - noModules = false, - uiAvailModules = { mod_public_ip = true }, - debug = false, - serviceConfig = { - hosts = { - [1] = "8.8.8.8", - [2] = "1.1.1.1", - }, - check_type = 0, -- 0: TCP, 1: ICMP - tcp_port = 53, - icmp_packet_size = 56, - interval_up = 30, - interval_down = 5, - connection_attempts = 2, - connection_timeout = 2, - iface = nil, - instance = nil, - }, - modules = {}, - parsedHosts = {}, - uiCounter = 0, -} -InternetDetector.configDir = string.format("/etc/%s", InternetDetector.appName) -InternetDetector.modulesDir = string.format("/usr/lib/%s", InternetDetector.appName) - --- Loading settings from UCI - -local uciCursor = uci.cursor() -InternetDetector.mode = tonumber( - uciCursor:get(InternetDetector.appName, "config", "mode")) -InternetDetector.enableLogger = (tonumber( - uciCursor:get(InternetDetector.appName, "config", "enable_logger")) ~= 0) -local hostname = uciCursor:get("system", "@[0]", "hostname") -if hostname ~= nil then - InternetDetector.hostname = hostname -end - -local RUNNING - -function InternetDetector:prequire(package) - local retVal, pkg = pcall(require, package) - return retVal and pkg -end - -function InternetDetector:loadUCIConfig(sType, instance) - local success - local num = 0 - uciCursor:foreach( - self.appName, - sType, - function(s) - if s[".name"] == instance then - for k, v in pairs(s) do - if type(v) == "string" and v:match("^[%d]+$") then - v = tonumber(v) - end - self.serviceConfig[k] = v - end - success = true - self.serviceConfig.instanceNum = num - end - num = num + 1 - end - ) - self.serviceConfig.instance = instance - self.pidFile = string.format( - "%s/%s.%s.pid", self.commonDir, self.appName, instance) - self.statusFile = string.format( - "%s/%s.%s.status", self.commonDir, self.appName, instance) - return success -end - -function InternetDetector:writeValueToFile(filePath, str) - local retValue = false - local fh = io.open(filePath, "w") - if fh then - fh:setvbuf("no") - fh:write(string.format("%s\n", str)) - fh:close() - retValue = true - end - return retValue -end - -function InternetDetector:readValueFromFile(filePath) - local retValue - local fh = io.open(filePath, "r") - if fh then - retValue = fh:read("*l") - fh:close() - end - return retValue -end - -function InternetDetector:statusJson(inet, instance, t) - local lines = { [1] = string.format( - '{"instance":"%s","num":"%d","inet":%d', - instance, - self.serviceConfig.instanceNum, - inet)} - if t then - for k, v in pairs(t) do - lines[#lines + 1] = string.format('"%s":"%s"', k, v) - end - end - return table.concat(lines, ",") .. "}" -end - -function InternetDetector:writeLogMessage(level, msg) - if self.enableLogger then - local levels = { - emerg = syslog.LOG_EMERG, - alert = syslog.LOG_ALERT, - crit = syslog.LOG_CRIT, - err = syslog.LOG_ERR, - warning = syslog.LOG_WARNING, - notice = syslog.LOG_NOTICE, - info = syslog.LOG_INFO, - debug = syslog.LOG_DEBUG, - } - syslog.syslog(levels[level] or syslog.LOG_INFO, string.format( - "%s: %s", self.serviceConfig.instance or "", msg)) - end -end - -function InternetDetector:loadModules() - package.path = string.format("%s;%s/?.lua", package.path, self.modulesDir) - self.modules = {} - local ok, modulesDir = pcall(dirent.files, self.modulesDir) - if ok then - for item in modulesDir do - if item:match("^mod_") then - local modName = item:gsub("%.lua$", "") - if self.noModules and not self.uiAvailModules[modName] then - else - local modConfig = {} - for k, v in pairs(self.serviceConfig) do - if k:match("^" .. modName) then - modConfig[k:gsub("^" .. modName .. "_", "")] = v - end - end - if modConfig.enabled == 1 then - local m = self:prequire(modName) - if m then - m.config = self - m.syslog = function(level, msg) self:writeLogMessage(level, msg) end - m.writeValue = function(filePath, str) return self:writeValueToFile(filePath, str) end - m.readValue = function(filePath) return self:readValueFromFile(filePath) end - m:init(modConfig) - self.modules[#self.modules + 1] = m - end - end - end - end - end - table.sort(self.modules, function(a, b) return a.runPrio < b.runPrio end) - end -end - -function InternetDetector:parseHost(host) - local addr, port = host:match("^([^%[%]:]+):?(%d?%d?%d?%d?%d?)$") - if not addr then - addr, port = host:match("^%[?([^%[%]]+)%]?:?(%d?%d?%d?%d?%d?)$") - end - return addr, tonumber(port) -end - -function InternetDetector:parseHosts() - self.parsedHosts = {} - for k, v in ipairs(self.serviceConfig.hosts) do - local addr, port = self:parseHost(v) - self.parsedHosts[k] = { addr = addr, port = port } - end -end - -function InternetDetector:pingHost(host) - local ping = string.format( - "%s %s -W %d -s %d%s %s > /dev/null 2>&1", - self.pingCmd, - self.pingParams, - self.serviceConfig.connection_timeout, - self.serviceConfig.icmp_packet_size, - self.serviceConfig.iface and (" -I " .. self.serviceConfig.iface) or "", - host - ) - local retCode = os.execute(ping) - - if self.debug then - io.stdout:write(string.format( - "--- Ping ---\ntime = %s\n%s\nretCode = %s\n", os.time(), ping, retCode) - ) - io.stdout:flush() - end - - return retCode -end - -function InternetDetector:TCPConnectionToHost(host, port) - local retCode = 1 - local saTable, errMsg, errNum = socket.getaddrinfo(host, port or self.serviceConfig.tcp_port) - - if not saTable then - if self.debug then - io.stdout:write(string.format( - "GETADDRINFO ERROR: %s, %s\n", errMsg, errNum)) - end - else - local family = saTable[1].family - - if family then - local sock, errMsg, errNum = socket.socket(family, socket.SOCK_STREAM, 0) - - if not sock then - if self.debug then - io.stdout:write(string.format( - "SOCKET ERROR: %s, %s\n", errMsg, errNum)) - end - return retCode - end - - socket.setsockopt(sock, socket.SOL_SOCKET, - socket.SO_SNDTIMEO, self.serviceConfig.connection_timeout, 0) - socket.setsockopt(sock, socket.SOL_SOCKET, - socket.SO_RCVTIMEO, self.serviceConfig.connection_timeout, 0) - - if self.serviceConfig.iface then - local ok, errMsg, errNum = socket.setsockopt(sock, socket.SOL_SOCKET, - socket.SO_BINDTODEVICE, self.serviceConfig.iface) - if not ok then - if self.debug then - io.stdout:write(string.format( - "SOCKET ERROR: %s, %s\n", errMsg, errNum)) - end - unistd.close(sock) - return retCode - end - end - - local success = socket.connect(sock, saTable[1]) - - if self.debug then - if not success then - io.stdout:write(string.format( - "SOCKET CONNECT ERROR: %s\n", tostring(success))) - end - local sockTable, err_s, e_s = socket.getsockname(sock) - local peerTable, err_p, e_p = socket.getpeername(sock) - if not sockTable then - sockTable = {} - io.stdout:write( - string.format("SOCKET ERROR: %s, %s\n", err_s, e_s)) - end - if not peerTable then - peerTable = {} - io.stdout:write( - string.format("SOCKET ERROR: %s, %s\n", err_p, e_p)) - end - io.stdout:write(string.format( - "--- TCP ---\ntime = %s\nconnection_timeout = %s\niface = %s\nhost:port = [%s]:%s\nsockname = [%s]:%s\npeername = [%s]:%s\nsuccess = %s\n", - os.time(), - self.serviceConfig.connection_timeout, - tostring(self.serviceConfig.iface), - host, - port or self.serviceConfig.tcp_port, - tostring(sockTable.addr), - tostring(sockTable.port), - tostring(peerTable.addr), - tostring(peerTable.port), - tostring(success)) - ) - io.stdout:flush() - end - - socket.shutdown(sock, socket.SHUT_RDWR) - unistd.close(sock) - retCode = success and 0 or 1 - end - end - return retCode -end - -function InternetDetector:checkHosts() - local checkFunc = (self.serviceConfig.check_type == 1) and self.pingHost or self.TCPConnectionToHost - local retCode = 1 - for k, v in ipairs(self.parsedHosts) do - for i = 1, self.serviceConfig.connection_attempts do - if checkFunc(self, v.addr, v.port) == 0 then - retCode = 0 - break - end - end - if retCode == 0 then - break - end - end - return retCode -end - -function InternetDetector:breakMain(signo) - RUNNING = false -end - -function InternetDetector:resetUiCounter(signo) - self.uiCounter = 0 -end - -function InternetDetector:main() - signal.signal(signal.SIGTERM, function(signo) self:breakMain(signo) end) - signal.signal(signal.SIGINT, function(signo) self:breakMain(signo) end) - signal.signal(signal.SIGQUIT, function(signo) self:breakMain(signo) end) - signal.signal(signal.SIGUSR1, function(signo) self:resetUiCounter(signo) end) - - local lastStatus, currentStatus, mTimeNow, mTimeDiff, mLastTime, uiTimeNow, uiLastTime - local interval = self.serviceConfig.interval_up - local counter = 0 - local onStart = true - RUNNING = true - while RUNNING do - if counter == 0 or counter >= interval then - currentStatus = self:checkHosts() - if onStart or not stat.stat(self.statusFile) then - self:writeValueToFile(self.statusFile, self:statusJson( - currentStatus, self.serviceConfig.instance)) - onStart = false - end - - if currentStatus == 0 then - interval = self.serviceConfig.interval_up - if lastStatus ~= nil and currentStatus ~= lastStatus then - self:writeValueToFile(self.statusFile, self:statusJson( - currentStatus, self.serviceConfig.instance)) - self:writeLogMessage("notice", "Connected") - end - else - interval = self.serviceConfig.interval_down - if lastStatus ~= nil and currentStatus ~= lastStatus then - self:writeValueToFile(self.statusFile, self:statusJson( - currentStatus, self.serviceConfig.instance)) - self:writeLogMessage("notice", "Disconnected") - end - end - - counter = 0 - end - - mTimeDiff = 0 - for _, e in ipairs(self.modules) do - mTimeNow = time.clock_gettime(time.CLOCK_MONOTONIC).tv_sec - if mLastTime then - mTimeDiff = mTimeDiff + mTimeNow - mLastTime - else - mTimeDiff = 1 - end - mLastTime = mTimeNow - e:run(currentStatus, lastStatus, mTimeDiff) - end - - local modulesStatus = {} - for k, v in ipairs(self.modules) do - if v.status ~= nil then - modulesStatus[v.name] = v.status - end - end - if next(modulesStatus) then - self:writeValueToFile(self.statusFile, self:statusJson( - currentStatus, self.serviceConfig.instance, modulesStatus)) - end - - lastStatus = currentStatus - unistd.sleep(1) - counter = counter + 1 - - if self.mode == 2 then - uiTimeNow = time.clock_gettime(time.CLOCK_MONOTONIC).tv_sec - if uiLastTime then - self.uiCounter = self.uiCounter + uiTimeNow - uiLastTime - else - self.uiCounter = self.uiCounter + 1 - end - uiLastTime = uiTimeNow - if self.uiCounter >= self.uiRunTime then - self:breakMain(signal.SIGTERM) - end - end - end -end - -function InternetDetector:removeProcessFiles() - os.remove(string.format( - "%s/%s.%s.pid", self.commonDir, self.appName, self.serviceConfig.instance)) - os.remove(string.format( - "%s/%s.%s.status", self.commonDir, self.appName, self.serviceConfig.instance)) -end - -function InternetDetector:status() - local ok, commonDir = pcall(dirent.files, self.commonDir) - if ok then - local appName = self.appName:gsub("-", "%%-") - for item in commonDir do - if item:match("^" .. appName .. ".-%.pid$") then - return "running" - end - end - end - return "stoped" -end - -function InternetDetector:inetStatus() - local inetStat = '{"instances":[]}' - local ok, commonDir = pcall(dirent.files, self.commonDir) - if ok then - local appName = self.appName:gsub("-", "%%-") - local lines = {} - for item in commonDir do - if item:match("^" .. appName .. ".-%.status$") then - lines[#lines + 1] = self:readValueFromFile( - string.format("%s/%s", self.commonDir, item)) - end - end - inetStat = '{"instances":[' .. table.concat(lines, ",") .. "]}" - end - return inetStat -end - -function InternetDetector:stopInstance(pidFile) - local retVal - if stat.stat(pidFile) then - pidValue = self:readValueFromFile(pidFile) - if pidValue then - local ok, errMsg, errNum - for i = 0, 10 do - ok, errMsg, errNum = signal.kill(tonumber(pidValue), signal.SIGTERM) - if ok then - break - end - end - if not ok then - io.stderr:write(string.format( - 'Process stop error: %s (%s). PID: "%s"\n', errMsg, errNum, pidValue)) - end - if errNum == 3 then - os.remove(pidFile) - end - retVal = true - end - end - if not pidValue then - io.stderr:write( - string.format('PID file "%s" does not exist. %s not running?\n', - pidFile, self.appName)) - end - return retVal -end - -function InternetDetector:stop() - local appName = self.appName:gsub("-", "%%-") - local success - for i = 0, 10 do - success = true - local ok, commonDir = pcall(dirent.files, self.commonDir) - if ok then - for item in commonDir do - if item:match("^" .. appName .. ".-%.pid$") then - self:stopInstance(string.format("%s/%s", self.commonDir, item)) - success = false - end - end - if success then - break - end - unistd.sleep(1) - else - break - end - end -end - -function InternetDetector:setSIGUSR() - local appName = self.appName:gsub("-", "%%-") - local ok, commonDir = pcall(dirent.files, self.commonDir) - if ok then - for item in commonDir do - if item:match("^" .. appName .. ".-%.pid$") then - pidValue = self:readValueFromFile(string.format("%s/%s", self.commonDir, item)) - if pidValue then - signal.kill(tonumber(pidValue), signal.SIGUSR1) - end - end - end - end -end - -function InternetDetector:preRun() - -- Exit if internet-detector mode != (1 or 2) - if self.mode ~= 1 and self.mode ~= 2 then - io.stderr:write(string.format('Start failed, mode != (1 or 2)\n', self.appName)) - os.exit(0) - end - if stat.stat(self.pidFile) then - io.stderr:write( - string.format('PID file "%s" already exist. %s already running?\n', - self.pidFile, self.appName)) - return false - end - return true -end - -function InternetDetector:run() - local pidValue = unistd.getpid() - self:writeValueToFile(self.pidFile, pidValue) - if self.enableLogger then - syslog.openlog(self.appName, syslog.LOG_PID, syslog.LOG_DAEMON) - end - self:writeLogMessage("info", "started") - self:loadModules() - - -- Loaded modules - local modules = {} - for _, v in ipairs(self.modules) do - modules[#modules + 1] = string.format("%s", v.name) - end - if #modules > 0 then - self:writeLogMessage( - "info", string.format("Loaded modules: %s", table.concat(modules, ", ")) - ) - end - - if self.debug then - local function inspectTable() - local tables = {}, f - f = function(t, prefix) - tables[t] = true - for k, v in pairs(t) do - io.stdout:write(string.format( - "%s%s = %s\n", prefix, k, tostring(v)) - ) - if type(v) == "table" and not tables[v] then - f(v, string.format("%s%s.", prefix, k)) - end - end - end - return f - end - - io.stdout:write("--- Config ---\n") - inspectTable()(self, "self.") - io.stdout:flush() - end - - self:writeValueToFile( - self.statusFile, self:statusJson(-1, self.serviceConfig.instance)) - - self:main() - - self:removeProcessFiles() - if self.enableLogger then - self:writeLogMessage("info", "stoped") - syslog.closelog() - end -end - -function InternetDetector:noDaemon() - if not self:preRun() then - return - end - self:run() -end - -function InternetDetector:daemon() - if not self:preRun() then - return - end - -- UNIX double fork - if unistd.fork() == 0 then - unistd.setpid("s") - if unistd.fork() == 0 then - unistd.chdir("/") - stat.umask(0) - local devnull = fcntl.open("/dev/null", fcntl.O_RDWR) - io.stdout:flush() - io.stderr:flush() - unistd.dup2(devnull, 0) -- io.stdin - unistd.dup2(devnull, 1) -- io.stdout - unistd.dup2(devnull, 2) -- io.stderr - self:run() - unistd.close(devnull) - end - os.exit(0) - end - os.exit(0) -end - -function InternetDetector:setServiceConfig(instance) - if self:loadUCIConfig("instance", instance) then - self:parseHosts() - if self.mode == 2 then - self.enableLogger = false - self.noModules = true - end - return true - end -end - --- Main section +local InternetDetector = require("internet-detector.main") local function help() return string.format( diff --git a/internet-detector/files/usr/lib/lua/internet-detector/main.lua b/internet-detector/files/usr/lib/lua/internet-detector/main.lua new file mode 100644 index 0000000..52bfeef --- /dev/null +++ b/internet-detector/files/usr/lib/lua/internet-detector/main.lua @@ -0,0 +1,643 @@ + +local dirent = require("posix.dirent") +local fcntl = require("posix.fcntl") +local signal = require("posix.signal") +local socket = require("posix.sys.socket") +local stat = require("posix.sys.stat") +local syslog = require("posix.syslog") +local time = require("posix.time") +local unistd = require("posix.unistd") +local uci = require("uci") + +-- Default settings + +local InternetDetector = { + mode = 0, -- 0: disabled, 1: Service, 2: UI detector + enableLogger = true, + hostname = "OpenWrt", + appName = "internet-detector", + commonDir = "/tmp/run", + libDir = "/usr/lib/lua", + pingCmd = "/bin/ping", + pingParams = "-c 1", + uiRunTime = 30, + noModules = false, + uiAvailModules = { mod_public_ip = true }, + debug = false, + serviceConfig = { + hosts = { + [1] = "8.8.8.8", + [2] = "1.1.1.1", + }, + check_type = 0, -- 0: TCP, 1: ICMP + tcp_port = 53, + icmp_packet_size = 56, + interval_up = 30, + interval_down = 5, + connection_attempts = 2, + connection_timeout = 2, + iface = nil, + instance = nil, + }, + modules = {}, + parsedHosts = {}, + uiCounter = 0, +} +InternetDetector.configDir = string.format("/etc/%s", InternetDetector.appName) +InternetDetector.modulesDir = string.format( + "%s/%s", InternetDetector.libDir, InternetDetector.appName) + +-- Loading settings from UCI + +local uciCursor = uci.cursor() +local mode, err = uciCursor:get(InternetDetector.appName, "config", "mode") +if mode ~= nil then + InternetDetector.mode = tonumber(mode) +elseif err then + io.stderr:write(string.format("Error: %s\n", err)) +end +local enableLogger, err = uciCursor:get(InternetDetector.appName, "config", "enable_logger") +if enableLogger ~= nil then + InternetDetector.enableLogger = (tonumber(enableLogger) ~= 0) +elseif err then + io.stderr:write(string.format("Error: %s\n", err)) +end +local hostname, err = uciCursor:get("system", "@[0]", "hostname") +if hostname ~= nil then + InternetDetector.hostname = hostname +elseif err then + io.stderr:write(string.format("Error: %s\n", err)) +end + +local _RUNNING + +function InternetDetector:prequire(package) + local retVal, pkg = pcall(require, package) + return retVal and pkg +end + +function InternetDetector:loadUCIConfig(sType, instance) + local success + local num = 0 + uciCursor:foreach( + self.appName, + sType, + function(s) + if s[".name"] == instance then + for k, v in pairs(s) do + if type(v) == "string" and v:match("^[%d]+$") then + v = tonumber(v) + end + self.serviceConfig[k] = v + end + success = true + self.serviceConfig.instanceNum = num + end + num = num + 1 + end + ) + self.serviceConfig.instance = instance + self.pidFile = string.format( + "%s/%s.%s.pid", self.commonDir, self.appName, instance) + self.statusFile = string.format( + "%s/%s.%s.status", self.commonDir, self.appName, instance) + return success +end + +function InternetDetector:writeValueToFile(filePath, str) + local retValue = false + local fh = io.open(filePath, "w") + if fh then + fh:setvbuf("no") + fh:write(string.format("%s\n", str)) + fh:close() + retValue = true + end + return retValue +end + +function InternetDetector:readValueFromFile(filePath) + local retValue + local fh = io.open(filePath, "r") + if fh then + retValue = fh:read("*l") + fh:close() + end + return retValue +end + +function InternetDetector:statusJson(inet, instance, t) + local lines = { [1] = string.format( + '{"instance":"%s","num":"%d","inet":%d', + instance, + self.serviceConfig.instanceNum, + inet)} + if t then + for k, v in pairs(t) do + lines[#lines + 1] = string.format('"%s":"%s"', k, v) + end + end + return table.concat(lines, ",") .. "}" +end + +function InternetDetector:writeLogMessage(level, msg) + if self.enableLogger then + local levels = { + emerg = syslog.LOG_EMERG, + alert = syslog.LOG_ALERT, + crit = syslog.LOG_CRIT, + err = syslog.LOG_ERR, + warning = syslog.LOG_WARNING, + notice = syslog.LOG_NOTICE, + info = syslog.LOG_INFO, + debug = syslog.LOG_DEBUG, + } + syslog.syslog(levels[level] or syslog.LOG_INFO, string.format( + "%s: %s", self.serviceConfig.instance or "", msg)) + end +end + +function InternetDetector:loadModules() + self.modules = {} + local ok, modulesDir = pcall(dirent.files, self.modulesDir) + if ok then + for item in modulesDir do + if item:match("^mod_") then + local modName = item:gsub("%.lua$", "") + if self.noModules and not self.uiAvailModules[modName] then + else + local modConfig = {} + for k, v in pairs(self.serviceConfig) do + if k:match("^" .. modName) then + modConfig[k:gsub("^" .. modName .. "_", "")] = v + end + end + if modConfig.enabled == 1 then + local m + if self.debug then + m = require(string.format("%s.%s", self.appName, modName)) + else + m = self:prequire(string.format("%s.%s", self.appName, modName)) + end + if m then + m.config = self + m.syslog = function(level, msg) self:writeLogMessage(level, msg) end + m.writeValue = function(filePath, str) return self:writeValueToFile(filePath, str) end + m.readValue = function(filePath) return self:readValueFromFile(filePath) end + m:init(modConfig) + self.modules[#self.modules + 1] = m + end + end + end + end + end + table.sort(self.modules, function(a, b) return a.runPrio < b.runPrio end) + end +end + +function InternetDetector:parseHost(host) + local addr, port = host:match("^([^%[%]:]+):?(%d?%d?%d?%d?%d?)$") + if not addr then + addr, port = host:match("^%[?([^%[%]]+)%]?:?(%d?%d?%d?%d?%d?)$") + end + return addr, tonumber(port) +end + +function InternetDetector:parseHosts() + self.parsedHosts = {} + for k, v in ipairs(self.serviceConfig.hosts) do + local addr, port = self:parseHost(v) + self.parsedHosts[k] = { addr = addr, port = port } + end +end + +function InternetDetector:pingHost(host) + local ping = string.format( + "%s %s -W %d -s %d%s %s > /dev/null 2>&1", + self.pingCmd, + self.pingParams, + self.serviceConfig.connection_timeout, + self.serviceConfig.icmp_packet_size, + self.serviceConfig.iface and (" -I " .. self.serviceConfig.iface) or "", + host + ) + local retCode = os.execute(ping) + + if self.debug then + io.stdout:write(string.format( + "--- Ping ---\ntime = %s\n%s\nretCode = %s\n", os.time(), ping, retCode) + ) + io.stdout:flush() + end + + return retCode +end + +function InternetDetector:TCPConnectionToHost(host, port) + local retCode = 1 + local saTable, errMsg, errNum = socket.getaddrinfo(host, port or self.serviceConfig.tcp_port) + + if not saTable then + if self.debug then + io.stdout:write(string.format( + "GETADDRINFO ERROR: %s, %s\n", errMsg, errNum)) + end + else + local family = saTable[1].family + if family then + local sock, errMsg, errNum = socket.socket(family, socket.SOCK_STREAM, 0) + + if not sock then + if self.debug then + io.stdout:write(string.format( + "SOCKET ERROR: %s, %s\n", errMsg, errNum)) + end + return retCode + end + + socket.setsockopt(sock, socket.SOL_SOCKET, + socket.SO_SNDTIMEO, self.serviceConfig.connection_timeout, 0) + socket.setsockopt(sock, socket.SOL_SOCKET, + socket.SO_RCVTIMEO, self.serviceConfig.connection_timeout, 0) + + if self.serviceConfig.iface then + local ok, errMsg, errNum = socket.setsockopt(sock, socket.SOL_SOCKET, + socket.SO_BINDTODEVICE, self.serviceConfig.iface) + if not ok then + if self.debug then + io.stdout:write(string.format( + "SOCKET ERROR: %s, %s\n", errMsg, errNum)) + end + + unistd.close(sock) + return retCode + end + end + + local success = socket.connect(sock, saTable[1]) + + if self.debug then + if not success then + io.stdout:write(string.format( + "SOCKET CONNECT ERROR: %s\n", tostring(success))) + end + local sockTable, err_s, e_s = socket.getsockname(sock) + local peerTable, err_p, e_p = socket.getpeername(sock) + if not sockTable then + sockTable = {} + io.stdout:write( + string.format("SOCKET ERROR: %s, %s\n", err_s, e_s)) + end + if not peerTable then + peerTable = {} + io.stdout:write( + string.format("SOCKET ERROR: %s, %s\n", err_p, e_p)) + end + io.stdout:write(string.format( + "--- TCP ---\ntime = %s\nconnection_timeout = %s\niface = %s\nhost:port = [%s]:%s\nsockname = [%s]:%s\npeername = [%s]:%s\nsuccess = %s\n", + os.time(), + self.serviceConfig.connection_timeout, + tostring(self.serviceConfig.iface), + host, + port or self.serviceConfig.tcp_port, + tostring(sockTable.addr), + tostring(sockTable.port), + tostring(peerTable.addr), + tostring(peerTable.port), + tostring(success)) + ) + io.stdout:flush() + end + + socket.shutdown(sock, socket.SHUT_RDWR) + unistd.close(sock) + retCode = success and 0 or 1 + end + end + return retCode +end + +function InternetDetector:checkHosts() + local checkFunc = (self.serviceConfig.check_type == 1) and self.pingHost or self.TCPConnectionToHost + local retCode = 1 + for k, v in ipairs(self.parsedHosts) do + for i = 1, self.serviceConfig.connection_attempts do + if checkFunc(self, v.addr, v.port) == 0 then + retCode = 0 + break + end + end + if retCode == 0 then + break + end + end + return retCode +end + +function InternetDetector:breakMainLoop(signo) + _RUNNING = false +end + +function InternetDetector:resetUiCounter(signo) + self.uiCounter = 0 +end + +function InternetDetector:mainLoop() + signal.signal(signal.SIGTERM, function(signo) self:breakMainLoop(signo) end) + signal.signal(signal.SIGINT, function(signo) self:breakMainLoop(signo) end) + signal.signal(signal.SIGQUIT, function(signo) self:breakMainLoop(signo) end) + signal.signal(signal.SIGUSR1, function(signo) self:resetUiCounter(signo) end) + + local lastStatus, currentStatus, mTimeNow, mTimeDiff, mLastTime, uiTimeNow, uiLastTime + local interval = self.serviceConfig.interval_up + local counter = 0 + local onStart = true + _RUNNING = true + while _RUNNING do + if counter == 0 or counter >= interval then + currentStatus = self:checkHosts() + if onStart or not stat.stat(self.statusFile) then + self:writeValueToFile(self.statusFile, self:statusJson( + currentStatus, self.serviceConfig.instance)) + onStart = false + end + + if currentStatus == 0 then + interval = self.serviceConfig.interval_up + if lastStatus ~= nil and currentStatus ~= lastStatus then + self:writeValueToFile(self.statusFile, self:statusJson( + currentStatus, self.serviceConfig.instance)) + self:writeLogMessage("notice", "Connected") + end + else + interval = self.serviceConfig.interval_down + if lastStatus ~= nil and currentStatus ~= lastStatus then + self:writeValueToFile(self.statusFile, self:statusJson( + currentStatus, self.serviceConfig.instance)) + self:writeLogMessage("notice", "Disconnected") + end + end + + counter = 0 + end + + mTimeDiff = 0 + for _, e in ipairs(self.modules) do + mTimeNow = time.clock_gettime(time.CLOCK_MONOTONIC).tv_sec + if mLastTime then + mTimeDiff = mTimeDiff + mTimeNow - mLastTime + else + mTimeDiff = 1 + end + mLastTime = mTimeNow + e:run(currentStatus, lastStatus, mTimeDiff) + end + + local modulesStatus = {} + for k, v in ipairs(self.modules) do + if v.status ~= nil then + modulesStatus[v.name] = v.status + end + end + if next(modulesStatus) then + self:writeValueToFile(self.statusFile, self:statusJson( + currentStatus, self.serviceConfig.instance, modulesStatus)) + end + + lastStatus = currentStatus + unistd.sleep(1) + counter = counter + 1 + + if self.mode == 2 then + uiTimeNow = time.clock_gettime(time.CLOCK_MONOTONIC).tv_sec + if uiLastTime then + self.uiCounter = self.uiCounter + uiTimeNow - uiLastTime + else + self.uiCounter = self.uiCounter + 1 + end + uiLastTime = uiTimeNow + + if self.uiCounter >= self.uiRunTime then + self:breakMainLoop(signal.SIGTERM) + end + end + end +end + +function InternetDetector:removeProcessFiles() + os.remove(string.format( + "%s/%s.%s.pid", self.commonDir, self.appName, self.serviceConfig.instance)) + os.remove(string.format( + "%s/%s.%s.status", self.commonDir, self.appName, self.serviceConfig.instance)) +end + +function InternetDetector:status() + local ok, commonDir = pcall(dirent.files, self.commonDir) + if ok then + local appName = self.appName:gsub("-", "%%-") + for item in commonDir do + if item:match("^" .. appName .. ".-%.pid$") then + return "running" + end + end + end + return "stoped" +end + +function InternetDetector:inetStatus() + local inetStat = '{"instances":[]}' + local ok, commonDir = pcall(dirent.files, self.commonDir) + if ok then + local appName = self.appName:gsub("-", "%%-") + local lines = {} + for item in commonDir do + if item:match("^" .. appName .. ".-%.status$") then + lines[#lines + 1] = self:readValueFromFile( + string.format("%s/%s", self.commonDir, item)) + end + end + inetStat = '{"instances":[' .. table.concat(lines, ",") .. "]}" + end + return inetStat +end + +function InternetDetector:stopInstance(pidFile) + local retVal + if stat.stat(pidFile) then + pidValue = self:readValueFromFile(pidFile) + if pidValue then + local ok, errMsg, errNum + for i = 0, 10 do + ok, errMsg, errNum = signal.kill(tonumber(pidValue), signal.SIGTERM) + if ok then + break + end + end + if not ok then + io.stderr:write(string.format( + 'Process stop error: %s (%s). PID: "%s"\n', errMsg, errNum, pidValue)) + end + if errNum == 3 then + os.remove(pidFile) + end + retVal = true + end + end + if not pidValue then + io.stderr:write( + string.format('PID file "%s" does not exist. %s not running?\n', + pidFile, self.appName)) + end + return retVal +end + +function InternetDetector:stop() + local appName = self.appName:gsub("-", "%%-") + local success + for i = 0, 10 do + success = true + local ok, commonDir = pcall(dirent.files, self.commonDir) + if ok then + for item in commonDir do + if item:match("^" .. appName .. ".-%.pid$") then + self:stopInstance(string.format("%s/%s", self.commonDir, item)) + success = false + end + end + if success then + break + end + unistd.sleep(1) + else + break + end + end +end + +function InternetDetector:setSIGUSR() + local appName = self.appName:gsub("-", "%%-") + local ok, commonDir = pcall(dirent.files, self.commonDir) + if ok then + for item in commonDir do + if item:match("^" .. appName .. ".-%.pid$") then + pidValue = self:readValueFromFile(string.format("%s/%s", self.commonDir, item)) + if pidValue then + signal.kill(tonumber(pidValue), signal.SIGUSR1) + end + end + end + end +end + +function InternetDetector:preRun() + -- Exit if internet-detector mode != (1 or 2) + if self.mode ~= 1 and self.mode ~= 2 then + io.stderr:write(string.format('Start failed, mode != (1 or 2)\n', self.appName)) + os.exit(0) + end + if stat.stat(self.pidFile) then + io.stderr:write( + string.format('PID file "%s" already exist. %s already running?\n', + self.pidFile, self.appName)) + return false + end + return true +end + +function InternetDetector:run() + local pidValue = unistd.getpid() + self:writeValueToFile(self.pidFile, pidValue) + if self.enableLogger then + syslog.openlog(self.appName, syslog.LOG_PID, syslog.LOG_DAEMON) + end + self:writeLogMessage("info", "started") + self:loadModules() + + -- Loaded modules + local modules = {} + for _, v in ipairs(self.modules) do + modules[#modules + 1] = string.format("%s", v.name) + end + if #modules > 0 then + self:writeLogMessage( + "info", string.format("Loaded modules: %s", table.concat(modules, ", ")) + ) + end + + if self.debug then + local function inspectTable() + local tables = {}, f + f = function(t, prefix) + tables[t] = true + for k, v in pairs(t) do + io.stdout:write(string.format( + "%s%s = %s\n", prefix, k, tostring(v)) + ) + if type(v) == "table" and not tables[v] then + f(v, string.format("%s%s.", prefix, k)) + end + end + end + return f + end + + io.stdout:write("--- Config ---\n") + inspectTable()(self, "self.") + io.stdout:flush() + end + + self:writeValueToFile( + self.statusFile, self:statusJson(-1, self.serviceConfig.instance)) + + self:mainLoop() + + self:removeProcessFiles() + if self.enableLogger then + self:writeLogMessage("info", "stoped") + syslog.closelog() + end +end + +function InternetDetector:noDaemon() + if not self:preRun() then + return + end + self:run() +end + +function InternetDetector:daemon() + if not self:preRun() then + return + end + -- UNIX double fork + if unistd.fork() == 0 then + unistd.setpid("s") + if unistd.fork() == 0 then + unistd.chdir("/") + stat.umask(0) + local devnull = fcntl.open("/dev/null", fcntl.O_RDWR) + io.stdout:flush() + io.stderr:flush() + unistd.dup2(devnull, 0) -- io.stdin + unistd.dup2(devnull, 1) -- io.stdout + unistd.dup2(devnull, 2) -- io.stderr + self:run() + unistd.close(devnull) + end + os.exit(0) + end + os.exit(0) +end + +function InternetDetector:setServiceConfig(instance) + if self:loadUCIConfig("instance", instance) then + self:parseHosts() + if self.mode == 2 then + self.enableLogger = false + self.noModules = true + end + return true + end +end + +return InternetDetector diff --git a/internet-detector/files/usr/lib/internet-detector/mod_led_control.lua b/internet-detector/files/usr/lib/lua/internet-detector/mod_led_control.lua similarity index 94% rename from internet-detector/files/usr/lib/internet-detector/mod_led_control.lua rename to internet-detector/files/usr/lib/lua/internet-detector/mod_led_control.lua index ae57c86..95d3653 100644 --- a/internet-detector/files/usr/lib/internet-detector/mod_led_control.lua +++ b/internet-detector/files/usr/lib/lua/internet-detector/mod_led_control.lua @@ -38,12 +38,17 @@ function Module:resetLeds() end function Module:init(t) - self.ledName = t.led_name - if not self.ledName then + if not t.led_name then return + else + self.ledName = t.led_name + end + if t.led_action_1 ~= nil then + self.ledAction1 = tonumber(t.led_action_1) + end + if t.led_action_2 ~= nil then + self.ledAction2 = tonumber(t.led_action_2) end - self.ledAction1 = tonumber(t.led_action_1) - self.ledAction2 = tonumber(t.led_action_2) self._ledDir = string.format("%s/%s", self.sysLedsDir, self.ledName) self._ledMaxBrightnessFile = string.format("%s/max_brightness", self._ledDir) self._ledBrightnessFile = string.format("%s/brightness", self._ledDir) diff --git a/internet-detector/files/usr/lib/internet-detector/mod_network_restart.lua b/internet-detector/files/usr/lib/lua/internet-detector/mod_network_restart.lua similarity index 84% rename from internet-detector/files/usr/lib/internet-detector/mod_network_restart.lua rename to internet-detector/files/usr/lib/lua/internet-detector/mod_network_restart.lua index 66456f2..54167d3 100644 --- a/internet-detector/files/usr/lib/internet-detector/mod_network_restart.lua +++ b/internet-detector/files/usr/lib/lua/internet-detector/mod_network_restart.lua @@ -8,9 +8,9 @@ local Module = { syslog = function(level, msg) return true end, writeValue = function(filePath, str) return false end, readValue = function(filePath) return nil end, - iface = false, - attempts = 0, - deadPeriod = 0, + deadPeriod = 900, + attempts = 1, + iface = nil, restartTimeout = 0, status = nil, _attemptsCounter = 0, @@ -22,6 +22,9 @@ function Module:toggleFunc(flag) end function Module:toggleDevice(flag) + if not self.iface then + return + end local ip = "/sbin/ip" if unistd.access(ip, "x") then return os.execute(string.format( @@ -31,6 +34,9 @@ function Module:toggleDevice(flag) end function Module:toggleIface(flag) + if not self.iface then + return + end return os.execute( string.format("%s %s", (flag and "/sbin/ifup" or "/sbin/ifdown"), self.iface) ) @@ -59,9 +65,15 @@ function Module:init(t) self.toggleFunc = self.toggleDevice end end - self.attempts = tonumber(t.attempts) - self.deadPeriod = tonumber(t.dead_period) - self.restartTimeout = tonumber(t.restart_timeout) + if t.attempts ~= nil then + self.attempts = tonumber(t.attempts) + end + if t.dead_period ~= nil then + self.deadPeriod = tonumber(t.dead_period) + end + if t.restart_timeout ~= nil then + self.restartTimeout = tonumber(t.restart_timeout) + end end function Module:run(currentStatus, lastStatus, timeDiff) diff --git a/internet-detector/files/usr/lib/internet-detector/mod_public_ip.lua b/internet-detector/files/usr/lib/lua/internet-detector/mod_public_ip.lua similarity index 98% rename from internet-detector/files/usr/lib/internet-detector/mod_public_ip.lua rename to internet-detector/files/usr/lib/lua/internet-detector/mod_public_ip.lua index 76039f9..14cb7fe 100644 --- a/internet-detector/files/usr/lib/internet-detector/mod_public_ip.lua +++ b/internet-detector/files/usr/lib/lua/internet-detector/mod_public_ip.lua @@ -20,7 +20,6 @@ local Module = { runInterval = 600, runIntervalFailed = 60, timeout = 3, - reqAttempts = 3, providers = { opendns1 = { name = "opendns1", host = "myip.opendns.com", @@ -333,13 +332,13 @@ function Module:resolveIP() end function Module:init(t) - if t.interval then + if t.interval ~= nil then self.runInterval = tonumber(t.interval) end - if t.timeout then + if t.timeout ~= nil then self.timeout = tonumber(t.timeout) end - if t.provider then + if t.provider ~= nil then self._provider = self.providers[t.provider] else self._provider = self.providers.opendns1 @@ -347,11 +346,13 @@ function Module:init(t) if self.config.configDir then self.ipScript = string.format( "%s/public-ip-script.%s", self.config.configDir, self.config.serviceConfig.instance) - if t.enable_ip_script then + if t.enable_ip_script ~= nil then self.enableIpScript = (tonumber(t.enable_ip_script) ~= 0) end end - self._qtype = (tonumber(t.qtype) ~= 0) + if t.qtype ~= nil then + self._qtype = (tonumber(t.qtype) ~= 0) + end self._currentIp = nil self._DNSPacket = nil self._interval = self.runInterval diff --git a/internet-detector/files/usr/lib/internet-detector/mod_reboot.lua b/internet-detector/files/usr/lib/lua/internet-detector/mod_reboot.lua similarity index 82% rename from internet-detector/files/usr/lib/internet-detector/mod_reboot.lua rename to internet-detector/files/usr/lib/lua/internet-detector/mod_reboot.lua index af39b22..6ec9c12 100644 --- a/internet-detector/files/usr/lib/internet-detector/mod_reboot.lua +++ b/internet-detector/files/usr/lib/lua/internet-detector/mod_reboot.lua @@ -8,8 +8,8 @@ local Module = { syslog = function(level, msg) return true end, writeValue = function(filePath, str) return false end, readValue = function(filePath) return nil end, - deadPeriod = 0, - forceRebootDelay = 0, + deadPeriod = 3600, + forceRebootDelay = 300, status = nil, _deadCounter = 0, } @@ -26,8 +26,12 @@ function Module:rebootDevice() end function Module:init(t) - self.deadPeriod = tonumber(t.dead_period) - self.forceRebootDelay = tonumber(t.force_reboot_delay) + if t.dead_period ~= nil then + self.deadPeriod = tonumber(t.dead_period) + end + if t.force_reboot_delay ~= nil then + self.forceRebootDelay = tonumber(t.force_reboot_delay) + end end function Module:run(currentStatus, lastStatus, timeDiff) diff --git a/internet-detector/files/usr/lib/internet-detector/mod_user_scripts.lua b/internet-detector/files/usr/lib/lua/internet-detector/mod_user_scripts.lua similarity index 90% rename from internet-detector/files/usr/lib/internet-detector/mod_user_scripts.lua rename to internet-detector/files/usr/lib/lua/internet-detector/mod_user_scripts.lua index ef6f148..9da42a4 100644 --- a/internet-detector/files/usr/lib/internet-detector/mod_user_scripts.lua +++ b/internet-detector/files/usr/lib/lua/internet-detector/mod_user_scripts.lua @@ -3,7 +3,7 @@ local unistd = require("posix.unistd") local Module = { name = "mod_user_scripts", - runPrio = 70, + runPrio = 80, config = {}, syslog = function(level, msg) return true end, writeValue = function(filePath, str) return false end, @@ -26,8 +26,12 @@ function Module:runExternalScript(scriptPath) end function Module:init(t) - self.deadPeriod = tonumber(t.dead_period) - self.alivePeriod = tonumber(t.alive_period) + if t.dead_period ~= nil then + self.deadPeriod = tonumber(t.dead_period) + end + if t.alive_period ~= nil then + self.alivePeriod = tonumber(t.alive_period) + end if self.config.configDir then self.upScript = string.format( "%s/up-script.%s", self.config.configDir, self.config.serviceConfig.instance) diff --git a/luci-app-internet-detector/Makefile b/luci-app-internet-detector/Makefile index 44500be..ea7438a 100644 --- a/luci-app-internet-detector/Makefile +++ b/luci-app-internet-detector/Makefile @@ -4,7 +4,7 @@ include $(TOPDIR)/rules.mk -PKG_VERSION:=1.2-0 +PKG_VERSION:=1.3-0 LUCI_TITLE:=LuCI support for internet-detector LUCI_DEPENDS:=+internet-detector LUCI_PKGARCH:=all diff --git a/luci-app-internet-detector/htdocs/luci-static/resources/view/internet-detector.js b/luci-app-internet-detector/htdocs/luci-static/resources/view/internet-detector.js index 49a74ed..c0ea24c 100644 --- a/luci-app-internet-detector/htdocs/luci-static/resources/view/internet-detector.js +++ b/luci-app-internet-detector/htdocs/luci-static/resources/view/internet-detector.js @@ -129,10 +129,8 @@ var Timefield = ui.Textfield.extend({ return view.extend({ appName : 'internet-detector', - execPath : '/usr/bin/internet-detector', configDir : '/etc/internet-detector', ledsPath : '/sys/class/leds', - mtaPath : '/usr/bin/mailsend', pollInterval : L.env.pollinterval, appStatus : 'stoped', initStatus : null, @@ -144,7 +142,9 @@ return view.extend({ defaultHosts : [ '8.8.8.8', '1.1.1.1' ], leds : [], mm : false, - mta : false, + mmInit : false, + email : false, + emailExec : false, callInitStatus: rpc.declare({ object: 'luci', @@ -185,6 +185,18 @@ return view.extend({ }); }, + callInit: rpc.declare({ + object: 'luci.internet-detector', + method: 'Init', + expect: { '': {} } + }), + + getInit() { + return this.callInit().then(data => { + return data; + }); + }, + callUIPoll: rpc.declare({ object: 'luci.internet-detector', method: 'UIPoll', @@ -197,6 +209,30 @@ return view.extend({ }); }, + callStatus: rpc.declare({ + object: 'luci.internet-detector', + method: 'Status', + expect: { '': {} } + }), + + getStatus() { + return this.callStatus().then(data => { + return data; + }); + }, + + callInetStatus: rpc.declare({ + object: 'luci.internet-detector', + method: 'InetStatus', + expect: { '': {} } + }), + + getInetStatus() { + return this.callInetStatus().then(data => { + return data; + }); + }, + setInternetStatus() { this.inetStatusArea.innerHTML = ''; @@ -240,25 +276,13 @@ return view.extend({ }; }, - inetStatusFromJson(res) { - let inetStatData = null; - if(res.code === 0) { - try { - inetStatData = JSON.parse(res.stdout.trim()); - } catch(e) {}; - }; - return inetStatData; - }, - servicePoll() { return Promise.all([ - fs.exec(this.execPath, [ 'status' ]), - fs.exec(this.execPath, [ 'inet-status' ]), + this.getStatus(), + this.getInetStatus(), ]).then(stat => { - let curAppStatus = (stat[0].code === 0) ? stat[0].stdout.trim() : null; - let inetStatData = this.inetStatusFromJson(stat[1]); - this.appStatus = curAppStatus; - this.inetStatus = inetStatData; + this.appStatus = stat[0].status; + this.inetStatus = stat[1]; this.setInternetStatus(); }).catch(e => { this.appStatus = 'stoped'; @@ -467,11 +491,10 @@ return view.extend({ load() { return Promise.all([ - fs.exec(this.execPath, [ 'status' ]), + this.getStatus(), this.getInitStatus(), L.resolveDefault(fs.list(this.ledsPath), []), - this.callInitStatus('modemmanager'), - L.resolveDefault(fs.stat(this.mtaPath), null), + this.getInit(), uci.load(this.appName), ]).catch(e => { ui.addNotification(null, E('p', _('An error has occurred') + ': %s'.format(e.message))); @@ -482,16 +505,24 @@ return view.extend({ if(!data) { return; }; - this.appStatus = (data[0].code === 0) ? data[0].stdout.trim() : null; - this.initStatus = data[1]; - this.leds = data[2]; - if(data[3].modemmanager) { - this.mm = true; + this.appStatus = (data[0].code === 0) ? data[0].stdout.trim() : null; + this.initStatus = data[1]; + this.leds = data[2]; + if(data[3]) { + if(data[3].mm_mod) { + this.mm = true; + }; + if(data[3].mm_init) { + this.mmInit = true; + }; + if(data[3].email_mod) { + this.email = true; + }; + if(data[3].email_exec) { + this.emailExec = true; + }; }; - if(data[4]) { - this.mta = true; - }; - this.currentAppMode = uci.get(this.appName, 'config', 'mode'); + this.currentAppMode = uci.get(this.appName, 'config', 'mode'); let s, o, ss; let m = new form.Map(this.appName, @@ -539,6 +570,7 @@ return view.extend({ _('Service: detector always runs as a system service.'), _('Web UI only: detector works only when the Web UI is open (UI detector).') ); + mode.default = '0'; /* Service instances configuration */ @@ -551,6 +583,7 @@ return view.extend({ _('Write messages to the system log.') ); o.rmempty = false; + o.default = '1'; }; s = m.section(form.GridSection, 'instance'); @@ -591,7 +624,6 @@ return view.extend({ 'hosts', _('Hosts'), _('Hosts to check Internet availability. Hosts are polled (in list order) until at least one of them responds.') ); - //o.datatype = 'or(host,hostport)'; o.datatype = 'or(or(host,hostport),ipaddrport(1))'; o.default = this.defaultHosts; o.rmempty = false; @@ -692,13 +724,17 @@ return view.extend({ s.tab('led_control', _('LED control')); s.tab('reboot_device', _('Reboot device')); s.tab('restart_network', _('Restart network')); - s.tab('restart_modem', _('Restart modem')); + if(this.mm) { + s.tab('restart_modem', _('Restart modem')); + }; }; s.tab('public_ip', _('Public IP address')); if(this.currentAppMode !== '2') { - s.tab('email', _('Email notification')); + if(this.email) { + s.tab('email', _('Email notification')); + }; s.tab('user_scripts', _('User scripts')); }; @@ -777,7 +813,7 @@ return view.extend({ // dead_period o = s.taboption('reboot_device', this.CBITimeInput, 'mod_reboot_dead_period', _('Dead period'), - _('Longest period of time without Internet access until the device is rebooted.') + _('Period of time without Internet access until the device is rebooted.') ); o.default = '3600'; o.rmempty = false; @@ -816,7 +852,7 @@ return view.extend({ // dead_period o = s.taboption('restart_network', this.CBITimeInput, 'mod_network_restart_dead_period', _('Dead period'), - _('Longest period of time without Internet access before network restart.') + _('Period of time without Internet access before network restart.') ); o.default = '900'; o.rmempty = false; @@ -863,57 +899,56 @@ return view.extend({ // Restart modem - o = s.taboption('restart_modem', form.DummyValue, '_dummy'); - o.rawhtml = true; - o.default = '
' + - _('Modem will be restarted when the Internet is disconnected.') + - '
'; - o.modalonly = true; - if(this.mm) { + if(this.mmInit) { + o = s.taboption('restart_modem', form.DummyValue, '_dummy'); + o.rawhtml = true; + o.default = '
' + + _('Modem will be restarted when the Internet is disconnected.') + + '
'; + o.modalonly = true; - // enabled - o = s.taboption('restart_modem', form.Flag, 'mod_modem_restart_enabled', - _('Enabled'), - ); - o.rmempty = false; - o.modalonly = true; + // enabled + o = s.taboption('restart_modem', form.Flag, 'mod_modem_restart_enabled', + _('Enabled'), + ); + o.rmempty = false; + o.modalonly = true; - // dead_period - o = s.taboption('restart_modem', this.CBITimeInput, - 'mod_modem_restart_dead_period', _('Dead period'), - _('Longest period of time without Internet access before modem restart.') - ); - o.default = '600'; - o.rmempty = false; - o.modalonly = true; + // dead_period + o = s.taboption('restart_modem', this.CBITimeInput, + 'mod_modem_restart_dead_period', _('Dead period'), + _('Period of time without Internet access before modem restart.') + ); + o.default = '600'; + o.rmempty = false; + o.modalonly = true; - // any_band - o = s.taboption('restart_modem', form.Flag, - 'mod_modem_restart_any_band', _('Unlock modem bands'), - _('Set the modem to be allowed to use any band.') - ); - o.rmempty = false; - o.modalonly = true; + // any_band + o = s.taboption('restart_modem', form.Flag, + 'mod_modem_restart_any_band', _('Unlock modem bands'), + _('Set the modem to be allowed to use any band.') + ); + o.rmempty = false; + o.modalonly = true; - // iface - o = s.taboption('restart_modem', widgets.NetworkSelect, 'mod_modem_restart_iface', - _('Interface'), - _('ModemManger interface. If specified, it will be restarted after restarting ModemManager.') - ); - o.multiple = false; - o.nocreate = true; - o.modalonly = true; - - } else { - o = s.taboption('restart_modem', form.DummyValue, '_dummy'); - o.rawhtml = true; - o.default = '
' + - _('ModemManager is not available...') + - '
'; - o.modalonly = true; + // iface + o = s.taboption('restart_modem', widgets.NetworkSelect, 'mod_modem_restart_iface', + _('Interface'), + _('ModemManger interface. If specified, it will be restarted after restarting ModemManager.') + ); + o.multiple = false; + o.nocreate = true; + o.modalonly = true; + } else { + o = s.taboption('restart_modem', form.DummyValue, '_dummy'); + o.rawhtml = true; + o.default = '
' + + _('ModemManager is not available...') + + '
'; + o.modalonly = true; + }; }; - }; // Public IP address @@ -996,97 +1031,118 @@ return view.extend({ ); o.modalonly = true; - // Email notification - o = s.taboption('email', form.DummyValue, '_dummy'); - o.rawhtml = true; - o.default = '
' + - _('An email will be sent when the internet connection is restored after being disconnected.') + - '
'; - o.modalonly = true; + if(this.email) { + if(this.emailExec) { + o = s.taboption('email', form.DummyValue, '_dummy'); + o.rawhtml = true; + o.default = '
' + + _('An email will be sent when connected or disconnected from the Internet.') + + '
'; + o.modalonly = true; - if(this.mta) { + // enabled + o = s.taboption('email', form.Flag, 'mod_email_enabled', + _('Enabled')); + o.rmempty = false; + o.modalonly = true; - // enabled - o = s.taboption('email', form.Flag, 'mod_email_enabled', - _('Enabled')); - o.rmempty = false; - o.modalonly = true; + // mode + o = s.taboption('email', form.ListValue, + 'mod_email_mode', _('When email will be sent') + ); + o.modalonly = true; + o.value(0, _('after connection')); + o.value(1, _('after disconnection')); + o.value(2, _('after connection or disconnection')); + o.default = '0'; - // alive_period - o = s.taboption('email', this.CBITimeInput, - 'mod_email_alive_period', _('Alive period'), - _('Longest period of time after connecting to the Internet before sending a message.') - ); - o.rmempty = false; - o.modalonly = true; + // alive_period + o = s.taboption('email', this.CBITimeInput, + 'mod_email_alive_period', _('Alive period'), + _('Period of time after connecting to the Internet before sending a message.') + ); + o.rmempty = false; + o.modalonly = true; + o.depends({ 'mod_email_mode': '0' }); + o.depends({ 'mod_email_mode': '2' }); + o.default = '0'; - // host_alias - o = s.taboption('email', form.Value, 'mod_email_host_alias', - _('Host alias'), - _('Host identifier in messages. If not specified, hostname will be used.')); - o.modalonly = true; + // dead_period + o = s.taboption('email', this.CBITimeInput, + 'mod_email_dead_period', _('Dead period'), + _('Period of time after disconnecting from Internet before sending a message.') + ); + o.rmempty = false; + o.modalonly = true; + o.depends({ 'mod_email_mode': '1' }); + o.depends({ 'mod_email_mode': '2' }); + o.default = '0'; - // mail_recipient - o = s.taboption('email', form.Value, - 'mod_email_mail_recipient', _('Recipient')); - o.description = _('Email address of the recipient.'); - o.modalonly = true; + // host_alias + o = s.taboption('email', form.Value, 'mod_email_host_alias', + _('Host alias'), + _('Host identifier in messages. If not specified, hostname will be used.')); + o.modalonly = true; - // mail_sender - o = s.taboption('email', form.Value, - 'mod_email_mail_sender', _('Sender')); - o.description = _('Email address of the sender.'); - o.modalonly = true; + // mail_recipient + o = s.taboption('email', form.Value, + 'mod_email_mail_recipient', _('Recipient')); + o.description = _('Email address of the recipient.'); + o.modalonly = true; - // mail_user - o = s.taboption('email', form.Value, - 'mod_email_mail_user', _('User')); - o.description = _('Username for SMTP authentication.'); - o.modalonly = true; + // mail_sender + o = s.taboption('email', form.Value, + 'mod_email_mail_sender', _('Sender')); + o.description = _('Email address of the sender.'); + o.modalonly = true; - // mail_password - o = s.taboption('email', form.Value, - 'mod_email_mail_password', _('Password')); - o.description = _('Password for SMTP authentication.'); - o.password = true; - o.modalonly = true; + // mail_user + o = s.taboption('email', form.Value, + 'mod_email_mail_user', _('User')); + o.description = _('Username for SMTP authentication.'); + o.modalonly = true; - // mail_smtp - o = s.taboption('email', form.Value, - 'mod_email_mail_smtp', _('SMTP server')); - o.description = _('Hostname/IP address of the SMTP server.'); - o.datatype = 'host'; - o.default = 'smtp.gmail.com'; - o.modalonly = true; + // mail_password + o = s.taboption('email', form.Value, + 'mod_email_mail_password', _('Password')); + o.description = _('Password for SMTP authentication.'); + o.password = true; + o.modalonly = true; - // mail_smtp_port - o = s.taboption('email', form.Value, - 'mod_email_mail_smtp_port', _('SMTP server port')); - o.datatype = 'port'; - o.default = '587'; - o.modalonly = true; + // mail_smtp + o = s.taboption('email', form.Value, + 'mod_email_mail_smtp', _('SMTP server')); + o.description = _('Hostname/IP address of the SMTP server.'); + o.datatype = 'host'; + o.modalonly = true; - // mail_security - o = s.taboption('email', form.ListValue, - 'mod_email_mail_security', _('Security')); - o.description = '%s
%s'.format( - _('TLS: use STARTTLS if the server supports it.'), - _('SSL: SMTP over SSL.'), - ); - o.value('tls', 'TLS'); - o.value('ssl', 'SSL'); - o.default = 'tls'; - o.modalonly = true; + // mail_smtp_port + o = s.taboption('email', form.Value, + 'mod_email_mail_smtp_port', _('SMTP server port')); + o.datatype = 'port'; + o.modalonly = true; - } else { - o = s.taboption('email', form.DummyValue, '_dummy'); - o.rawhtml = true; - o.default = '
' + - _('Mailsend is not available...') + - '
'; - o.modalonly = true; + // mail_security + o = s.taboption('email', form.ListValue, + 'mod_email_mail_security', _('Security')); + o.description = '%s
%s'.format( + _('TLS: use STARTTLS if the server supports it.'), + _('SSL: SMTP over SSL.'), + ); + o.value('tls', 'TLS'); + o.value('ssl', 'SSL'); + o.default = 'tls'; + o.modalonly = true; + } else { + o = s.taboption('email', form.DummyValue, '_dummy'); + o.rawhtml = true; + o.default = '
' + + _('Mailsend is not available...') + + '
'; + o.modalonly = true; + }; }; // User scripts @@ -1116,7 +1172,7 @@ return view.extend({ // alive_period o = s.taboption('user_scripts', this.CBITimeInput, 'mod_user_scripts_alive_period', _('Alive period'), - _('Longest period of time after connecting to Internet before "up-script" runs.') + _('Period of time after connecting to Internet before "up-script" runs.') ); o.default = '0'; o.rmempty = false; @@ -1134,7 +1190,7 @@ return view.extend({ // dead_period o = s.taboption('user_scripts', this.CBITimeInput, 'mod_user_scripts_dead_period', _('Dead period'), - _('Longest period of time after disconnecting from Internet before "down-script" runs.') + _('Period of time after disconnecting from Internet before "down-script" runs.') ); o.default = '0'; o.rmempty = false; @@ -1156,7 +1212,6 @@ return view.extend({ }, handleSaveApply(ev, mode) { - poll.stop(); return this.handleSave(ev).then(() => { ui.changes.apply(mode == '0'); window.setTimeout(() => this.serviceRestart(), 3000); diff --git a/luci-app-internet-detector/htdocs/luci-static/resources/view/status/include/00_internet.js b/luci-app-internet-detector/htdocs/luci-static/resources/view/status/include/00_internet.js index c15556a..7a34281 100644 --- a/luci-app-internet-detector/htdocs/luci-static/resources/view/status/include/00_internet.js +++ b/luci-app-internet-detector/htdocs/luci-static/resources/view/status/include/00_internet.js @@ -57,7 +57,6 @@ document.head.append(E('style', {'type': 'text/css'}, return baseclass.extend({ title : _('Internet'), appName : 'internet-detector', - execPath : '/usr/bin/internet-detector', currentAppMode : null, inetStatus : null, @@ -73,14 +72,16 @@ return baseclass.extend({ }); }, - inetStatusFromJson(res) { - let inetStatData = null; - if(res.code === 0) { - try { - inetStatData = JSON.parse(res.stdout.trim()); - } catch(e) {}; - }; - return inetStatData; + callInetStatus: rpc.declare({ + object: 'luci.internet-detector', + method: 'InetStatus', + expect: { '': {} } + }), + + getInetStatus() { + return this.callInetStatus().then(data => { + return data; + }); }, async load() { @@ -94,7 +95,7 @@ return baseclass.extend({ return this.getUIPoll(); } else if(this.currentAppMode === '1') { - return L.resolveDefault(fs.exec(this.execPath, [ 'inet-status' ]), null); + return L.resolveDefault(this.getInetStatus(), null); }; }, @@ -102,9 +103,7 @@ return baseclass.extend({ if(this.currentAppMode === '0') { return; } - else if(this.currentAppMode === '1' && data) { - data = this.inetStatusFromJson(data); - }; + this.inetStatus = data; let inetStatusArea = E('div', {}); diff --git a/luci-app-internet-detector/po/ru/internet-detector.po b/luci-app-internet-detector/po/ru/internet-detector.po index 2b75ae2..358c654 100644 --- a/luci-app-internet-detector/po/ru/internet-detector.po +++ b/luci-app-internet-detector/po/ru/internet-detector.po @@ -38,10 +38,8 @@ msgstr "Интервал при подключении" msgid "Alive period" msgstr "Период после подключения" -msgid "" -"An email will be sent when the internet connection is restored after being " -"disconnected." -msgstr "Сообщение будет отправлено при восстановлении соединения с Интернет после отключения." +msgid "An email will be sent when connected or disconnected from the Internet." +msgstr "Сообщение будет отправлено при подключении или отключении от Интернет." msgid "An error has occurred" msgstr "Произошла ошибка" @@ -221,41 +219,8 @@ msgstr "Управление LED" msgid "Loading" msgstr "Загрузка" -msgid "" -"Longest period of time after connecting to Internet before \"up-script\" " -"runs." -msgstr "" -"Максимальный промежуток времени после подключения к Интернет перед запуском " -"\"up-script\"." - -msgid "" -"Longest period of time after connecting to the Internet before sending a " -"message." -msgstr "Максимальный промежуток времени после подключения Интернет перед отправкой сообщения." - -msgid "" -"Longest period of time after disconnecting from Internet before \"down-script" -"\" runs." -msgstr "" -"Максимальный промежуток времени после отключения Интернет перед запуском " -"\"down-script\"." - -msgid "Longest period of time without Internet access before modem restart." -msgstr "" -"Максимальное время отсутствия доступа в Интренет перед перезапуском модема." - -msgid "Longest period of time without Internet access before network restart." -msgstr "" -"Максимальное время отсутствия доступа в Интренет перед перезапуском сети." - -msgid "" -"Longest period of time without Internet access until the device is rebooted." -msgstr "" -"Максимальное время отсутствия доступа в Интренет перед перезагрузкой " -"устройства." - msgid "Mailsend is not available..." -msgstr "Mailsend недоступен..." +msgstr "Mailsend не доступен..." msgid "Main settings" msgstr "Основные настройки" @@ -273,12 +238,12 @@ msgstr "" msgid "Maximum timeout for waiting for a response from the host." msgstr "Максимальный таймаут ожидания ответа от хоста." +msgid "ModemManager is not available..." +msgstr "ModemManager не доступен..." + msgid "Modem will be restarted when the Internet is disconnected." msgstr "Модем будет перезапущен при отключении Интернет." -msgid "ModemManager is not available..." -msgstr "ModemManager недоступен..." - msgid "" "ModemManger interface. If specified, it will be restarted after restarting " "ModemManager." @@ -320,6 +285,35 @@ msgstr "Пароль" msgid "Password for SMTP authentication." msgstr "Пароль для SMTP-аутентификации." +msgid "" +"Period of time after connecting to Internet before \"up-script\" runs." +msgstr "" +"Период времени после подключения к Интернет перед запуском \"up-script\"." + +msgid "" +"Period of time after connecting to the Internet before sending a message." +msgstr "Период времени после подключения к Интернет перед отправкой сообщения." + +msgid "" +"Period of time after disconnecting from Internet before \"down-script\" runs." +msgstr "" +"Период времени после отключения от Интернет перед запуском \"down-script\"." + +msgid "" +"Period of time after disconnecting from Internet before sending a message." +msgstr "Период времени отсутствия доступа в Интренет перед отправкой сообщения." + +msgid "Period of time without Internet access before modem restart." +msgstr "Период времени отсутствия доступа в Интренет перед перезапуском модема." + +msgid "Period of time without Internet access before network restart." +msgstr "Период времени отсутствия доступа в Интренет перед перезапуском сети." + +msgid "" +"Period of time without Internet access until the device is rebooted." +msgstr "" +"Период времени отсутствия доступа в Интренет перед перезагрузкой устройства." + msgid "Polling interval" msgstr "Интервал опроса" @@ -468,12 +462,24 @@ msgstr "" "Только web-интерфейс: детектор работает только в web-интерфейсе (UI " "детектор)." +msgid "When email will be sent" +msgstr "Когда будет отправлено сообщение" + msgid "Windows: 32 bytes" msgstr "Windows: 32 байта" msgid "Write messages to the system log." msgstr "Записывать сообщения в системный журнал." +msgid "after connection" +msgstr "после подключения" + +msgid "after disconnection" +msgstr "после отключения" + +msgid "after connection or disconnection" +msgstr "после подключения или отключения" + msgid "down-script" msgstr "down-script" diff --git a/luci-app-internet-detector/po/templates/internet-detector.pot b/luci-app-internet-detector/po/templates/internet-detector.pot index bd58814..dc1d333 100644 --- a/luci-app-internet-detector/po/templates/internet-detector.pot +++ b/luci-app-internet-detector/po/templates/internet-detector.pot @@ -26,9 +26,7 @@ msgstr "" msgid "Alive period" msgstr "" -msgid "" -"An email will be sent when the internet connection is restored after being " -"disconnected." +msgid "An email will be sent when connected or disconnected from the Internet." msgstr "" msgid "An error has occurred" @@ -207,31 +205,6 @@ msgstr "" msgid "Loading" msgstr "" -msgid "" -"Longest period of time after connecting to Internet before \"up-script\" " -"runs." -msgstr "" - -msgid "" -"Longest period of time after connecting to the Internet before sending a " -"message." -msgstr "" - -msgid "" -"Longest period of time after disconnecting from Internet before \"down-script" -"\" runs." -msgstr "" - -msgid "Longest period of time without Internet access before modem restart." -msgstr "" - -msgid "Longest period of time without Internet access before network restart." -msgstr "" - -msgid "" -"Longest period of time without Internet access until the device is rebooted." -msgstr "" - msgid "Mailsend is not available..." msgstr "" @@ -249,10 +222,10 @@ msgstr "" msgid "Maximum timeout for waiting for a response from the host." msgstr "" -msgid "Modem will be restarted when the Internet is disconnected." +msgid "ModemManager is not available..." msgstr "" -msgid "ModemManager is not available..." +msgid "Modem will be restarted when the Internet is disconnected." msgstr "" msgid "" @@ -290,6 +263,32 @@ msgstr "" msgid "Password for SMTP authentication." msgstr "" +msgid "" +"Period of time after connecting to Internet before \"up-script\" runs." +msgstr "" + +msgid "" +"Period of time after connecting to the Internet before sending a message." +msgstr "" + +msgid "" +"Period of time after disconnecting from Internet before \"down-script\" runs." +msgstr "" + +msgid "" +"Period of time after disconnecting from Internet before sending a message." +msgstr "" + +msgid "Period of time without Internet access before modem restart." +msgstr "" + +msgid "Period of time without Internet access before network restart." +msgstr "" + +msgid "" +"Period of time without Internet access until the device is rebooted." +msgstr "" + msgid "Polling interval" msgstr "" @@ -434,12 +433,24 @@ msgstr "" msgid "Web UI only: detector works only when the Web UI is open (UI detector)." msgstr "" +msgid "When email will be sent" +msgstr "" + msgid "Windows: 32 bytes" msgstr "" msgid "Write messages to the system log." msgstr "" +msgid "after connection" +msgstr "" + +msgid "after disconnection" +msgstr "" + +msgid "both" +msgstr "" + msgid "down-script" msgstr "" diff --git a/luci-app-internet-detector/root/usr/libexec/rpcd/luci.internet-detector b/luci-app-internet-detector/root/usr/libexec/rpcd/luci.internet-detector index 6ff71d0..25a96a0 100755 --- a/luci-app-internet-detector/root/usr/libexec/rpcd/luci.internet-detector +++ b/luci-app-internet-detector/root/usr/libexec/rpcd/luci.internet-detector @@ -1,46 +1,86 @@ -#!/bin/sh +#!/usr/bin/env lua -. /lib/functions.sh -. /usr/share/libubox/jshn.sh +local appName = "internet-detector" +local appExec = "/usr/bin/internet-detector" +local mailsendExec = "/usr/bin/mailsend" +local modemManagerInit = "/etc/init.d/modemmanager" -readonly ID_EXEC="/usr/bin/internet-detector" +local InternetDetector = require(appName .. ".main") +local uci = require("uci") +local unistd = require("posix.unistd") -run_instance() { - config_get enabled "$1" enabled "0" - if [ $enabled = "1" ]; then - $ID_EXEC service "$1" > /dev/null 2>&1 - fi -} +local function prequire(package) + local retVal, pkg = pcall(require, package) + return retVal and pkg +end -start_ui_instances() { - config_load internet-detector - config_get mode "config" mode "0" - if [ $mode = "2" ]; then - config_foreach run_instance "instance" - fi -} +local function init() + local lines = {} + if prequire(appName .. ".mod_modem_restart") then + lines[#lines + 1] = '"mm_mod":true' + if (unistd.access(modemManagerInit, "x") and + os.execute(modemManagerInit .. " enabled") == 0) then + lines[#lines + 1] = '"mm_init":true' + else + lines[#lines + 1] = '"mm_init":false' + end + else + lines[#lines + 1] = '"mm_mod":false' + end + if prequire(appName .. ".mod_email") then + lines[#lines + 1] = '"email_mod":true' + if unistd.access(mailsendExec, "x") then + lines[#lines + 1] = '"email_exec":true' + else + lines[#lines + 1] = '"email_exec":false' + end + else + lines[#lines + 1] = '"email_mod":false' + end + return string.format("{%s}", table.concat(lines, ",")) +end -ui_poll() { - $ID_EXEC uipoll - if [ $? -eq 126 ]; then - start_ui_instances - $ID_EXEC inet-status - fi -} +local function startUiInstances() + local uciCursor = uci.cursor() + local mode = tonumber(uciCursor:get(appName, "config", "mode")) + if mode == 2 then + uciCursor:foreach( + appName, + "instance", + function(s) + if s.enabled == "1" then + os.execute(string.format("%s service %s", appExec, s[".name"])) + end + end + ) + end +end -case "$1" in - list) - json_init - json_add_object "UIPoll" - json_close_object - json_dump - json_cleanup - ;; - call) - case "$2" in - UIPoll) - ui_poll - ;; - esac - ;; -esac +local function uiPoll() + if InternetDetector:status() == "stoped" then + startUiInstances() + else + InternetDetector:setSIGUSR() + end + return InternetDetector:inetStatus() +end + +local function list() + io.write('{"Init":{},"Status":{},"InetStatus":{},"UIPoll":{}}') +end + +if arg[1] == "list" then + list() +elseif arg[1] == "call" then + if arg[2] == "Init" then + io.write(init()) + elseif arg[2] == "Status" then + io.write(string.format('{"status":"%s"}', tostring(InternetDetector:status()))) + elseif arg[2] == "InetStatus" then + io.write(InternetDetector:inetStatus()) + elseif arg[2] == "UIPoll" then + io.write(uiPoll()) + end +end + +os.exit(0) diff --git a/luci-app-internet-detector/root/usr/share/rpcd/acl.d/luci-app-internet-detector.json b/luci-app-internet-detector/root/usr/share/rpcd/acl.d/luci-app-internet-detector.json index 7bc459e..893ac23 100644 --- a/luci-app-internet-detector/root/usr/share/rpcd/acl.d/luci-app-internet-detector.json +++ b/luci-app-internet-detector/root/usr/share/rpcd/acl.d/luci-app-internet-detector.json @@ -6,14 +6,12 @@ "/sys/class/leds": [ "list" ], "/etc/internet-detector/up-script*": [ "read" ], "/etc/internet-detector/down-script*": [ "read" ], - "/etc/internet-detector/public-ip-script*": [ "read" ], - "/usr/bin/internet-detector*": [ "exec" ], - "/usr/bin/mailsend": [ "exec" ] + "/etc/internet-detector/public-ip-script*": [ "read" ] }, "uci": [ "internet-detector" ], "ubus": { "luci": [ "getInitList", "setInitAction" ], - "luci.internet-detector": [ "UIPoll" ] + "luci.internet-detector": [ "Init", "Status", "InetStatus", "UIPoll" ] } }, "write": { diff --git a/screenshots/02.jpg b/screenshots/02.jpg index c0794b6..3325125 100644 Binary files a/screenshots/02.jpg and b/screenshots/02.jpg differ diff --git a/screenshots/04.jpg b/screenshots/04.jpg new file mode 100644 index 0000000..f5516fc Binary files /dev/null and b/screenshots/04.jpg differ diff --git a/screenshots/05.jpg b/screenshots/05.jpg new file mode 100644 index 0000000..8e73cea Binary files /dev/null and b/screenshots/05.jpg differ