48 Commits

Author SHA1 Message Date
gSpot
b69fcf0bb2 v1.7. New optional checking feature: URL test. 2025-10-08 22:42:15 +03:00
gSpot
87d28fb618 Fixed a crash on startup. 2025-10-04 23:35:11 +03:00
gSpot
663ac6c90b luci-app-internet-detector: Fixed sorting function. 2025-10-03 21:11:55 +03:00
gSpot
10fedfded1 Minor fixes. 2025-10-02 18:23:46 +03:00
gSpot
1720662f73 Refactoring. Added respawn option to init-script. 2025-09-25 22:38:53 +03:00
gSpot
cb7dbba054 Added instance description option. 2025-09-04 19:47:00 +03:00
gSpot
e9814cf85b Changed common dir. 2025-08-25 23:49:39 +03:00
gSpot
73e98a682d luci-app-internet-detector: fixed typos. 2025-08-10 02:51:38 +03:00
gSpot
1c02ace538 mod_led_control: added netdev trigger, more options for timer trigger. 2025-08-09 18:37:11 +03:00
gSpot
9067d3d3ab luci-app-internet-detector: Fixed default values. 2025-08-01 21:40:01 +03:00
gSpot
eb93cd47b7 Added selection of logging level. 2025-08-01 18:50:07 +03:00
gSpot
fcd78c296c luci-app-internet-detector: minor UI improvements. 2025-07-23 15:55:40 +03:00
gSpot
e1d107de12 Fixed config. 2025-07-04 00:57:37 +03:00
gSpot
1806a459d2 README.md 2025-07-04 00:05:21 +03:00
gSpot
4ff18a1269 v1.6. New module: mod_telegram. mod_public_ip: Added support for HTTP services. 2025-07-04 00:02:25 +03:00
gSpot
f9aa55ca4d main.InternetDetector.debugOutput(). 2025-06-24 18:24:15 +03:00
gSpot
ef40cb3051 mod_email: minor improvements. 2025-06-12 01:20:44 +03:00
gSpot
0e65c8a059 mod_network_restart, mod_modem_restart, mod_user_scripts: New option attempt_interval. 2025-06-08 16:15:58 +03:00
gSpot
75652f5e7d Fixed mod_email.lua. 2025-06-05 01:17:46 +03:00
gSpot
9803fe0ab4 v1.5. Some new options. New package structure. 2025-06-05 01:00:21 +03:00
gSpot
d0c1f03ce5 mod_led_control: fixed regexp. 2025-05-30 22:35:30 +03:00
gSpot
63ddfa1ed0 Running modules with pcall(). 2025-05-27 16:11:57 +03:00
gSpot
36a70fa706 modules: Module.onExit(). 2025-04-06 15:35:03 +03:00
gSpot
5b669fe718 Fixed mod_public_ip 2025-03-23 17:56:17 +03:00
gSpot
55802f750f Fixes & improvements 2025-03-01 18:31:36 +03:00
gSpot
d94260e7ec luci-app-internet-detector: fixed "edit instance" 2025-02-24 19:34:50 +03:00
gSpot
8ad9c5a086 modules: bug fixes, refactoring 2025-02-21 17:36:43 +03:00
gSpot
6c19812db5 mod_led_control: multi LED 2025-02-18 16:12:41 +03:00
gSpot
dd46273f24 v1.4. New module: mod_regular_script 2025-02-16 16:26:43 +03:00
gSpot
90b711f55e Luaposix getopt for positional arguments 2025-02-06 18:55:57 +03:00
gSpot
c20703d5be Minor fixes 2025-01-23 01:51:17 +03:00
gSpot
e0e13d4962 Procd support 2024-12-16 21:10:49 +03:00
gSpot
be3b7e7dd2 Makefiles 2024-06-13 18:17:05 +03:00
gSpot
5e0729df31 Minor fixes 2024-05-25 19:53:25 +03:00
gSpot
8918f12ef6 Minor fixes 2024-05-22 23:39:20 +03:00
gSpot
13d8d06248 internet-detector-mod-email: minor improvements 2024-02-21 16:57:49 +03:00
gSpot
98d7b9d4f0 internet-detector-mod-email: minor improvements 2024-02-21 16:45:32 +03:00
gSpot
3f245bb410 v1.3. Refactoring. internet-detector-mod-modem-restart, internet-detector-mod-email 2024-02-19 17:50:33 +03:00
gSpot
b5a4429854 Minor fixes 2024-01-28 18:46:58 +03:00
gSpot
9364dabe0a v1.2. UI detector refactoring 2024-01-20 19:08:49 +03:00
gSpot
be0912d7ae v1.1. mod_public_ip: public-ip-script 2024-01-13 23:29:09 +03:00
gSpot
259f4cf122 Fixed socket closing 2023-08-29 23:52:07 +03:00
gSpot
170bd8ddd7 Modules priority 2023-08-14 19:59:21 +03:00
gSpot
f92713818d mod_led_control: LED actions 2023-08-03 16:57:06 +03:00
gSpot
ffc10b81e0 luci-app-internet-detector: fixed typos 2023-07-25 15:20:52 +03:00
gSpot
87022499e3 luci-app-internet-detector: Minor fixes 2023-05-11 03:08:14 +03:00
gSpot
1ac7652e0a luci-app-internet-detector: ru translation 2023-05-07 22:50:57 +03:00
gSpot
76acd939a9 v1.0. Luaposix, multiple service instances. 2023-05-01 14:39:23 +03:00
43 changed files with 5480 additions and 2213 deletions

View File

@@ -1,48 +1,78 @@
# 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, luci-lib-nixio, 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.
- Checking the availability of a host using ping (L3) or by connecting via TCP to a specified port (L4).
- Testing URL accessibility via HTTP (L7) (curl).
- 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.
- The daemon is written entirely in Lua using the nixio library.
- 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).
- Sending telegtam notification when Internet access is restored (internet-detector-mod-telegram).
- The daemon is written entirely in Lua using the luaposix library.
## Installation notes
**OpenWrt >= 21.02.**
**OpenWrt >= 21.02:**
**Dependences:** lua, luaposix, libuci-lua.
**Recommended:** curl.
## Installation notes:
opkg update
wget --no-check-certificate -O /tmp/internet-detector_0.6-0_all.ipk https://github.com/gSpotx2f/packages-openwrt/raw/master/current/internet-detector_0.6-0_all.ipk
opkg install /tmp/internet-detector_0.6-0_all.ipk
rm /tmp/internet-detector_0.6-0_all.ipk
/etc/init.d/internet-detector start
/etc/init.d/internet-detector enable
wget --no-check-certificate -O /tmp/internet-detector_1.7.0-r1_all.ipk https://github.com/gSpotx2f/packages-openwrt/raw/master/current/internet-detector_1.7.0-r1_all.ipk
opkg install /tmp/internet-detector_1.7.0-r1_all.ipk
rm /tmp/internet-detector_1.7.0-r1_all.ipk
service internet-detector start
service internet-detector enable
wget --no-check-certificate -O /tmp/luci-app-internet-detector_0.6-1_all.ipk https://github.com/gSpotx2f/packages-openwrt/raw/master/current/luci-app-internet-detector_0.6-1_all.ipk
opkg install /tmp/luci-app-internet-detector_0.6-1_all.ipk
rm /tmp/luci-app-internet-detector_0.6-1_all.ipk
/etc/init.d/rpcd restart
Email notification:
opkg install mailsend
wget --no-check-certificate -O /tmp/luci-app-internet-detector_1.7.0-r1_all.ipk https://github.com/gSpotx2f/packages-openwrt/raw/master/current/luci-app-internet-detector_1.7.0-r1_all.ipk
opkg install /tmp/luci-app-internet-detector_1.7.0-r1_all.ipk
rm /tmp/luci-app-internet-detector_1.7.0-r1_all.ipk
service rpcd restart
i18n-ru:
wget --no-check-certificate -O /tmp/luci-i18n-internet-detector-ru_0.6-1_all.ipk https://github.com/gSpotx2f/packages-openwrt/raw/master/current/luci-i18n-internet-detector-ru_0.6-1_all.ipk
opkg install /tmp/luci-i18n-internet-detector-ru_0.6-1_all.ipk
rm /tmp/luci-i18n-internet-detector-ru_0.6-1_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.7.0-r1_all.ipk https://github.com/gSpotx2f/packages-openwrt/raw/master/current/luci-i18n-internet-detector-ru_1.7.0-r1_all.ipk
opkg install /tmp/luci-i18n-internet-detector-ru_1.7.0-r1_all.ipk
rm /tmp/luci-i18n-internet-detector-ru_1.7.0-r1_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.7.0-r1_all.ipk https://github.com/gSpotx2f/packages-openwrt/raw/master/current/internet-detector-mod-modem-restart_1.7.0-r1_all.ipk
opkg install /tmp/internet-detector-mod-modem-restart_1.7.0-r1_all.ipk
rm /tmp/internet-detector-mod-modem-restart_1.7.0-r1_all.ipk
service 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.7.0-r1_all.ipk https://github.com/gSpotx2f/packages-openwrt/raw/master/current/internet-detector-mod-email_1.7.0-r1_all.ipk
opkg install /tmp/internet-detector-mod-email_1.7.0-r1_all.ipk
rm /tmp/internet-detector-mod-email_1.7.0-r1_all.ipk
service internet-detector restart
![](https://github.com/gSpotx2f/luci-app-internet-detector/blob/master/screenshots/05.jpg)
## Telegram notification module (internet-detector-mod-telegram):
**Dependences:** curl.
wget --no-check-certificate -O /tmp/internet-detector-mod-telegram_1.7.0-r1_all.ipk https://github.com/gSpotx2f/packages-openwrt/raw/master/current/internet-detector-mod-telegram_1.7.0-r1_all.ipk
opkg install /tmp/internet-detector-mod-telegram_1.7.0-r1_all.ipk
rm /tmp/internet-detector-mod-telegram_1.7.0-r1_all.ipk
service internet-detector restart
![](https://github.com/gSpotx2f/luci-app-internet-detector/blob/master/screenshots/06.jpg)

View File

@@ -0,0 +1,41 @@
#
# (с) 2025 gSpot (https://github.com/gSpotx2f/luci-app-internet-detector)
#
include $(TOPDIR)/rules.mk
PKG_NAME:=internet-detector-mod-email
PKG_VERSION:=1.7.0
PKG_RELEASE:=1
PKG_MAINTAINER:=gSpot <https://github.com/gSpotx2f/luci-app-internet-detector>
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/modules
$(INSTALL_DATA) ./files/usr/lib/lua/internet-detector/modules/mod_email.lua $(1)/usr/lib/lua/internet-detector/modules/mod_email.lua
endef
$(eval $(call BuildPackage,$(PKG_NAME)))

View File

@@ -0,0 +1,226 @@
--[[
Dependences:
mailsend
--]]
local unistd = require("posix.unistd")
local Module = {
name = "mod_email",
runPrio = 60,
config = {
debug = false,
},
syslog = function(level, msg) return true end,
debugOutput = function(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",
mtaConnectTimeout = 5,
mtaReadTimeout = 5,
mailRecipient = nil,
mailSender = nil,
mailUser = nil,
mailPassword = nil,
mailSmtp = nil,
mailSmtpPort = nil,
mailSecurity = "tls",
msgTextPattern = "[%s] (%s) @ %s", -- Message (host, instance, message)
msgSubPattern = "%s notification", -- Subject (host)
msgConnectPattern = "Connected: %s",
msgDisconnectPattern = "Disconnected: %s",
msgSeparator = " | ",
msgMaxItems = 50,
msgSendAttempts = 3,
msgSendTimeout = 5,
status = nil,
_enabled = false,
_deadCounter = 0,
_aliveCounter = 0,
_msgSentDisconnect = true,
_disconnected = true,
_msgSentConnect = true,
_connected = true,
_msgBuffer = {},
_msgSendCounter = 3,
_msgTimeoutCounter = 5,
}
function Module:init(t)
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
self.hostAlias = self.config.hostname
end
self.mailRecipient = t.mail_recipient
self.mailSender = t.mail_sender
self.mailUser = t.mail_user
self.mailPassword = t.mail_password
self.mailSmtp = t.mail_smtp
self.mailSmtpPort = t.mail_smtp_port
if t.mail_security ~= nil then
self.mailSecurity = t.mail_security
end
if tonumber(t.message_at_startup) == 1 then
self._msgSentDisconnect = false
self._disconnected = false
self._msgSentConnect = false
self._connected = false
end
if unistd.access(self.mta, "x") then
self._enabled = true
else
self._enabled = false
self.syslog("err", string.format("%s: %s is not available", self.name, self.mta))
end
if (not self.mailRecipient or
not self.mailSender or
not self.mailUser or
not self.mailPassword or
not self.mailSmtp or
not self.mailSmtpPort) then
self._enabled = false
self.syslog("warning", string.format(
"%s: Insufficient data to connect to the SMTP server", self.name))
end
self._msgSendCounter = self.msgSendAttempts
end
function Module:appendNotice(str)
self._msgBuffer[#self._msgBuffer + 1] = str
if #self._msgBuffer > self.msgMaxItems then
local t = {}
for i = #self._msgBuffer - self.msgMaxItems + 1, #self._msgBuffer do
t[#t + 1] = self._msgBuffer[i]
end
self._msgBuffer = t
end
end
function Module:sendMessage(msg, textPattern)
local retVal = 1
local verboseArg = ""
local emailMsg = string.format(
textPattern, self.hostAlias, self.config.serviceConfig.instance, msg)
-- Debug
if self.config.debug then
verboseArg = " -v"
end
self.debugOutput(string.format("--- %s ---", self.name))
local securityArgs = "-starttls -auth-login"
if self.mailSecurity == "ssl" then
securityArgs = "-ssl -auth"
end
local mtaCmd = string.format(
'%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(self.msgSubPattern, self.hostAlias),
emailMsg)
-- Debug
self.debugOutput(string.format("%s: %s", self.name, mtaCmd))
self.syslog("debug", string.format("%s: %s", self.name, mtaCmd))
retVal = os.execute(mtaCmd)
if retVal == 0 then
self.syslog("info", string.format(
"%s: Message sent to %s", self.name, self.mailRecipient))
self._msgBuffer = {}
else
self.syslog("err", string.format(
"%s: An error occured while sending message", self.name))
end
return retVal
end
function Module:run(currentStatus, lastStatus, timeDiff, timeNow, inetChecked)
if not self._enabled then
return
end
if currentStatus == 1 then
self._aliveCounter = 0
self._msgSentConnect = false
if not self._disconnected then
self._disconnected = true
self:appendNotice(string.format(
self.msgDisconnectPattern, os.date("%Y.%m.%d %H:%M:%S", os.time())))
end
if not self._msgSentDisconnect and (self.mode == 1 or self.mode == 2) then
if self._deadCounter >= self.deadPeriod then
self._msgSendCounter = 0
self._msgSentDisconnect = true
else
self._deadCounter = self._deadCounter + timeDiff
end
end
self._connected = false
elseif currentStatus == 0 then
self._deadCounter = 0
self._msgSentDisconnect = false
if not self._connected then
self._connected = true
self:appendNotice(string.format(
self.msgConnectPattern, os.date("%Y.%m.%d %H:%M:%S", os.time())))
end
if not self._msgSentConnect and (self.mode == 0 or self.mode == 2) then
if self._aliveCounter >= self.alivePeriod then
self._msgSendCounter = 0
self._msgSentConnect = true
else
self._aliveCounter = self._aliveCounter + timeDiff
end
end
self._disconnected = false
end
if self._msgSendCounter < self.msgSendAttempts then
if self._msgTimeoutCounter >= self.msgSendTimeout then
if #self._msgBuffer > 0 then
if self:sendMessage(table.concat(self._msgBuffer, self.msgSeparator), self.msgTextPattern) == 0 then
self._msgSendCounter = self.msgSendAttempts
else
self._msgSendCounter = self._msgSendCounter + 1
end
end
self._msgTimeoutCounter = 0
else
self._msgTimeoutCounter = self._msgTimeoutCounter + timeDiff
end
else
self._msgTimeoutCounter = self.msgSendTimeout
end
end
function Module:onExit()
return true
end
return Module

View File

@@ -0,0 +1,41 @@
#
# (с) 2025 gSpot (https://github.com/gSpotx2f/luci-app-internet-detector)
#
include $(TOPDIR)/rules.mk
PKG_NAME:=internet-detector-mod-modem-restart
PKG_VERSION:=1.7.0
PKG_RELEASE:=1
PKG_MAINTAINER:=gSpot <https://github.com/gSpotx2f/luci-app-internet-detector>
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/modules
$(INSTALL_DATA) ./files/usr/lib/lua/internet-detector/modules/mod_modem_restart.lua $(1)/usr/lib/lua/internet-detector/modules/mod_modem_restart.lua
endef
$(eval $(call BuildPackage,$(PKG_NAME)))

View File

@@ -0,0 +1,152 @@
--[[
Dependences:
modemmanager
--]]
local unistd = require("posix.unistd")
local Module = {
name = "mod_modem_restart",
runPrio = 40,
config = {},
syslog = function(level, msg) return true end,
debugOutput = function(msg) return true end,
writeValue = function(filePath, str) return false end,
readValue = function(filePath) return nil end,
mmcli = "/usr/bin/mmcli",
mmInit = "/etc/init.d/modemmanager",
deadPeriod = 600,
attempts = 1,
attemptInterval = 15,
ifaceTimeout = 0,
iface = nil,
anyBand = false,
status = nil,
_enabled = false,
_attemptsCounter = 0,
_attemptIntervalCounter = 0,
_deadCounter = 0,
_firstAttempt = true,
_ifaceRestarting = false,
_ifaceRestartCounter = 0,
_disconnectedAtStartup = false,
}
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:init(t)
if t.dead_period ~= nil then
self.deadPeriod = tonumber(t.dead_period)
end
if t.attempts ~= nil then
self.attempts = tonumber(t.attempts)
end
if t.attempt_interval ~= nil then
self.attemptInterval = tonumber(t.attempt_interval)
end
if t.iface ~= nil then
self.iface = t.iface
end
if t.iface_timeout ~= nil then
self.ifaceTimeout = tonumber(t.iface_timeout)
end
if t.any_band ~= nil then
self.anyBand = (tonumber(t.any_band) ~= 0)
end
if tonumber(t.disconnected_at_startup) == 1 then
self._disconnectedAtStartup = true
end
if not unistd.access(self.mmcli, "x") then
self.anyBand = false
end
if (unistd.access(self.mmInit, "x")
and os.execute(string.format("%s enabled", self.mmInit)) == 0) then
self._enabled = true
else
self._enabled = false
self.syslog("err", string.format(
"%s: modemmanager service is not available", self.name))
end
end
function Module:restartMM()
if os.execute(string.format("%s enabled", self.mmInit)) == 0 then
if self.anyBand then
self.syslog("info", string.format(
"%s: resetting current-bands to 'any'", self.name))
os.execute(string.format("%s -m any --set-current-bands=any", self.mmcli))
end
self.syslog("info", string.format("%s: reconnecting modem", self.name))
os.execute(string.format("%s restart", self.mmInit))
if self.iface then
self.syslog("info", string.format(
"%s: restarting network interface '%s'", self.name, self.iface))
self:toggleIface(false)
if self.ifaceTimeout < 1 then
self:toggleIface(true)
else
self._ifaceRestarting = true
end
end
else
self.syslog("warning", string.format(
"%s: modemmanager service is disabled", self.name))
end
if self.attempts > 0 then
self._attemptsCounter = self._attemptsCounter + 1
end
end
function Module:run(currentStatus, lastStatus, timeDiff, timeNow, inetChecked)
if not self._enabled then
return
end
if self.iface and self._ifaceRestarting then
if self._ifaceRestartCounter >= self.ifaceTimeout then
self:toggleIface(true)
self._ifaceRestarting = false
self._ifaceRestartCounter = 0
else
self._ifaceRestartCounter = self._ifaceRestartCounter + timeDiff
end
else
if currentStatus == 1 then
if self._disconnectedAtStartup and self._deadCounter >= self.deadPeriod then
if self.attempts == 0 or self._attemptsCounter < self.attempts then
if self._firstAttempt or self._attemptIntervalCounter >= self.attemptInterval then
self:restartMM()
self._attemptIntervalCounter = 0
self._firstAttempt = false
else
self._attemptIntervalCounter = self._attemptIntervalCounter + timeDiff
end
end
else
self._deadCounter = self._deadCounter + timeDiff
end
else
self._attemptsCounter = 0
self._attemptIntervalCounter = 0
self._deadCounter = 0
self._disconnectedAtStartup = true
self._firstAttempt = true
end
self._ifaceRestartCounter = 0
end
end
function Module:onExit()
return true
end
return Module

View File

@@ -0,0 +1,41 @@
#
# (с) 2025 gSpot (https://github.com/gSpotx2f/luci-app-internet-detector)
#
include $(TOPDIR)/rules.mk
PKG_NAME:=internet-detector-mod-telegram
PKG_VERSION:=1.7.0
PKG_RELEASE:=1
PKG_MAINTAINER:=gSpot <https://github.com/gSpotx2f/luci-app-internet-detector>
include $(INCLUDE_DIR)/package.mk
define Package/$(PKG_NAME)
SECTION:=net
CATEGORY:=Network
TITLE:=Telegram messenger module for internet-detector
URL:=https://github.com/gSpotx2f/luci-app-internet-detector
PKGARCH:=all
DEPENDS:=+internet-detector +curl
endef
define Package/$(PKG_NAME)/description
Telegram messenger 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/modules
$(INSTALL_DATA) ./files/usr/lib/lua/internet-detector/modules/mod_telegram.lua $(1)/usr/lib/lua/internet-detector/modules/mod_telegram.lua
endef
$(eval $(call BuildPackage,$(PKG_NAME)))

View File

@@ -0,0 +1,271 @@
--[[
Dependences:
curl
--]]
local unistd = require("posix.unistd")
local Module = {
name = "mod_telegram",
runPrio = 70,
syslog = function(level, msg) return true end,
debugOutput = function(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",
connectTimeout = 5,
tgAPIToken = nil,
tgChatId = nil,
tgMsgURLpattern = "https://api.telegram.org/bot%s/sendMessage?chat_id=%s&parse_mode=html&text=%s",
msgTextPattern = "<strong>[%s] (%s)</strong> @ %s", -- Message (host, instance, message)
msgConnectPattern = "Connected: %s",
msgDisconnectPattern = "Disconnected: %s",
msgSeparator = " | ",
msgMaxItems = 50,
msgSendAttempts = 3,
msgSendTimeout = 5,
curlExec = "/usr/bin/curl",
curlParams = "-s --no-keepalive",
status = nil,
_enabled = false,
_deadCounter = 0,
_aliveCounter = 0,
_msgSentDisconnect = true,
_disconnected = true,
_msgSentConnect = true,
_connected = true,
_msgBuffer = {},
_msgSendCounter = 3,
_msgTimeoutCounter = 5,
}
local function prequire(package)
local retVal, pkg = pcall(require, package)
return retVal and pkg
end
function Module:init(t)
self._enabled = true
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
self.hostAlias = self.config.hostname
end
if t.api_token ~= nil then
self.tgAPIToken = t.api_token
end
if t.chat_id ~= nil then
self.tgChatId = t.chat_id
end
if tonumber(t.message_at_startup) == 1 then
self._msgSentDisconnect = false
self._disconnected = false
self._msgSentConnect = false
self._connected = false
end
if unistd.access(self.curlExec, "x") then
self._enabled = true
else
self._enabled = false
self.syslog("err", string.format("%s: %s is not available", self.name, self.curlExec))
end
if not self.tgAPIToken then
self._enabled = false
self.syslog("err", string.format("%s: Telegram bot API token not specified.", self.name))
end
if not self.tgChatId then
self._enabled = false
self.syslog("err", string.format("%s: Telegram chat ID not specified.", self.name))
end
self._msgSendCounter = self.msgSendAttempts
end
function Module:escape(str)
local t = {}
for i in str:gmatch(".") do
if i:match("[^%w_]") then
t[#t + 1] = "%" .. string.format("%x", string.byte(i))
else
t[#t + 1] = i
end
end
return table.concat(t)
end
function Module:appendNotice(str)
self._msgBuffer[#self._msgBuffer + 1] = str
if #self._msgBuffer > self.msgMaxItems then
local t = {}
for i = #self._msgBuffer - self.msgMaxItems + 1, #self._msgBuffer do
t[#t + 1] = self._msgBuffer[i]
end
self._msgBuffer = t
end
end
function Module:httpRequest(url)
local retCode = 1, data
local fh = io.popen(string.format(
'%s --connect-timeout %s %s "%s"; printf "\n$?";',
self.curlExec,
self.connectTimeout,
self.curlParams,
url
), "r")
if fh then
data = fh:read("*a")
fh:close()
if data ~= nil then
local s, e = data:find("[0-9]+\n?$")
retCode = tonumber(data:sub(s))
data = data:sub(0, s - 2)
if not data or data == "" then
data = nil
end
end
else
retCode = 1
end
return retCode, data
end
function Module:parseResponse(str)
local ok, errCode, desc
ok = str:match('"ok":(%w+)')
if ok == "false" then
errCode = tonumber(str:match('"error_code":(%d+)'))
if errCode then
desc = str:match('"description":"([%w%s%p_]+)"')
end
end
return ok, errCode, desc
end
function Module:messageRequest(msg, textPattern)
local retVal = 1
local tgMsg = string.format(
textPattern, self.hostAlias, self.config.serviceConfig.instance, msg)
local url = string.format(
self.tgMsgURLpattern, self.tgAPIToken, self.tgChatId, self:escape(tgMsg))
local ok, errCode, desc
local retCode, data = self:httpRequest(url)
if data then
ok, errCode, desc = self:parseResponse(data)
end
if retCode == 0 and ok == "true" then
retVal = 0
self.syslog("info", string.format(
"%s: Message sent to chat %s", self.name, self.tgChatId))
else
if errCode == 400 or errCode == 406 then
retVal = 2
elseif (errCode == 401 or
errCode == 403 or
errCode == 404 or
errCode == 420) then
retVal = 3
end
if errCode and desc then
self.syslog("warning", string.format(
"%s: %s %s", self.name, tostring(errCode), tostring(desc)))
end
end
return retVal
end
function Module:sendMessage(msg, textPattern)
local retVal = self:messageRequest(msg, textPattern)
if retVal == 0 then
self._msgBuffer = {}
elseif retVal == 2 then
self.syslog("err", string.format(
"%s: Server error (invalid API token or chat ID)", self.name))
else
self.syslog("err", string.format(
"%s: An error occured while sending message", self.name))
end
return retVal
end
function Module:run(currentStatus, lastStatus, timeDiff, timeNow, inetChecked)
if not self._enabled then
return
end
if currentStatus == 1 then
self._aliveCounter = 0
self._msgSentConnect = false
if not self._disconnected then
self._disconnected = true
self:appendNotice(string.format(
self.msgDisconnectPattern, os.date("%Y.%m.%d %H:%M:%S", os.time())))
end
if not self._msgSentDisconnect and (self.mode == 1 or self.mode == 2) then
if self._deadCounter >= self.deadPeriod then
self._msgSendCounter = 0
self._msgSentDisconnect = true
else
self._deadCounter = self._deadCounter + timeDiff
end
end
self._connected = false
elseif currentStatus == 0 then
self._deadCounter = 0
self._msgSentDisconnect = false
if not self._connected then
self._connected = true
self:appendNotice(string.format(
self.msgConnectPattern, os.date("%Y.%m.%d %H:%M:%S", os.time())))
end
if not self._msgSentConnect and (self.mode == 0 or self.mode == 2) then
if self._aliveCounter >= self.alivePeriod then
self._msgSendCounter = 0
self._msgSentConnect = true
else
self._aliveCounter = self._aliveCounter + timeDiff
end
end
self._disconnected = false
end
if self._msgSendCounter < self.msgSendAttempts then
if self._msgTimeoutCounter >= self.msgSendTimeout then
if #self._msgBuffer > 0 then
local retVal = self:sendMessage(table.concat(self._msgBuffer, self.msgSeparator), self.msgTextPattern)
if retVal == 1 then
self._msgSendCounter = self._msgSendCounter + 1
else
self._msgSendCounter = self.msgSendAttempts
end
end
self._msgTimeoutCounter = 0
else
self._msgTimeoutCounter = self._msgTimeoutCounter + timeDiff
end
else
self._msgTimeoutCounter = self.msgSendTimeout
end
end
function Module:onExit()
return true
end
return Module

View File

@@ -1,12 +1,12 @@
#
# (с) 2021 gSpot (https://github.com/gSpotx2f/luci-app-internet-detector)
# (с) 2025 gSpot (https://github.com/gSpotx2f/luci-app-internet-detector)
#
include $(TOPDIR)/rules.mk
PKG_NAME:=internet-detector
PKG_VERSION:=0.6
PKG_RELEASE:=0
PKG_VERSION:=1.7.0
PKG_RELEASE:=1
PKG_MAINTAINER:=gSpot <https://github.com/gSpotx2f/luci-app-internet-detector>
include $(INCLUDE_DIR)/package.mk
@@ -17,17 +17,21 @@ define Package/$(PKG_NAME)
TITLE:=Internet detector
URL:=https://github.com/gSpotx2f/luci-app-internet-detector
PKGARCH:=all
DEPENDS:=+lua +luci-lib-nixio +libuci-lua
DEPENDS:=+lua +luaposix +libuci-lua
endef
define Package/$(PKG_NAME)/description
Internet-detector is a small daemon
for checking Internet availability.
Written in Lua using the nixio library.
Written in Lua using the luaposix library.
endef
define Package/$(PKG_NAME)/conffiles
/etc/config/internet-detector
/etc/internet-detector/down-script.internet
/etc/internet-detector/up-script.internet
/etc/internet-detector/public-ip-script.internet
/etc/internet-detector/regular-script.internet
endef
define Build/Configure
@@ -40,20 +44,24 @@ define Package/$(PKG_NAME)/install
$(INSTALL_DIR) $(1)/etc/config
$(INSTALL_CONF) ./files/etc/config/internet-detector $(1)/etc/config/internet-detector
$(INSTALL_DIR) $(1)/etc/internet-detector
$(INSTALL_BIN) ./files/etc/internet-detector/down-script $(1)/etc/internet-detector/down-script
$(INSTALL_BIN) ./files/etc/internet-detector/up-script $(1)/etc/internet-detector/up-script
$(INSTALL_DATA) ./files/etc/internet-detector/down-script.internet $(1)/etc/internet-detector/down-script.internet
$(INSTALL_DATA) ./files/etc/internet-detector/up-script.internet $(1)/etc/internet-detector/up-script.internet
$(INSTALL_DATA) ./files/etc/internet-detector/public-ip-script.internet $(1)/etc/internet-detector/public-ip-script.internet
$(INSTALL_DATA) ./files/etc/internet-detector/regular-script.internet $(1)/etc/internet-detector/regular-script.internet
$(INSTALL_DIR) $(1)/etc/init.d
$(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/init.lua $(1)/usr/lib/lua/internet-detector/init.lua
$(INSTALL_DIR) $(1)/usr/lib/lua/internet-detector/modules
$(INSTALL_DATA) ./files/usr/lib/lua/internet-detector/modules/mod_led_control.lua $(1)/usr/lib/lua/internet-detector/modules/mod_led_control.lua
$(INSTALL_DATA) ./files/usr/lib/lua/internet-detector/modules/mod_reboot.lua $(1)/usr/lib/lua/internet-detector/modules/mod_reboot.lua
$(INSTALL_DATA) ./files/usr/lib/lua/internet-detector/modules/mod_network_restart.lua $(1)/usr/lib/lua/internet-detector/modules/mod_network_restart.lua
$(INSTALL_DATA) ./files/usr/lib/lua/internet-detector/modules/mod_public_ip.lua $(1)/usr/lib/lua/internet-detector/modules/mod_public_ip.lua
$(INSTALL_DATA) ./files/usr/lib/lua/internet-detector/modules/mod_user_scripts.lua $(1)/usr/lib/lua/internet-detector/modules/mod_user_scripts.lua
$(INSTALL_DATA) ./files/usr/lib/lua/internet-detector/modules/mod_regular_script.lua $(1)/usr/lib/lua/internet-detector/modules/mod_regular_script.lua
endef
$(eval $(call BuildPackage,$(PKG_NAME)))

View File

@@ -1,52 +1,65 @@
config main 'config'
option mode '2'
option mode '1'
option logging_level '6'
config instance 'internet'
option enabled '1'
option description 'Default instance'
list hosts '8.8.8.8'
list hosts '1.1.1.1'
list urls 'https://www.google.com'
option check_type '0'
option tcp_port '53'
option ui_interval_up '6'
option ui_interval_down '1'
option ui_connection_attempts '1'
option ui_connection_timeout '1'
option service_interval_up '30'
option service_interval_down '5'
option service_connection_attempts '2'
option service_connection_timeout '2'
option service_enable_logger '1'
config module 'mod_led_control'
option enabled '0'
config module 'mod_reboot'
option enabled '0'
option dead_period '3600'
option force_reboot_delay '300'
config module 'mod_network_restart'
option enabled '0'
option dead_period '900'
option attempts '1'
option restart_timeout '0'
config module 'mod_modem_restart'
option enabled '0'
option dead_period '600'
option any_band '0'
config module 'mod_public_ip'
option enabled '0'
option provider 'opendns1'
option interval '600'
option timeout '3'
config module 'mod_email'
option enabled '0'
option alive_period '0'
option mail_smtp 'smtp.gmail.com'
option mail_smtp_port '587'
option mail_security 'tls'
config module 'mod_user_scripts'
option enabled '0'
option alive_period '0'
option dead_period '0'
option interval_up '30'
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_disconnected_at_startup '0'
option mod_reboot_dead_period '3600'
option mod_reboot_force_reboot_delay '300'
option mod_network_restart_enabled '0'
option mod_network_restart_disconnected_at_startup '0'
option mod_network_restart_dead_period '900'
option mod_network_restart_attempts '1'
option mod_network_restart_attempt_interval '60'
option mod_network_restart_device_timeout '0'
option mod_modem_restart_enabled '0'
option mod_modem_restart_disconnected_at_startup '0'
option mod_modem_restart_dead_period '600'
option mod_modem_restart_attempts '1'
option mod_modem_restart_attempt_interval '60'
option mod_modem_restart_iface_timeout '0'
option mod_modem_restart_any_band '0'
option mod_public_ip_enabled '0'
option mod_public_ip_provider 'opendns1'
option mod_public_ip_qtype '0'
option mod_public_ip_interval '600'
option mod_public_ip_interval_failed '60'
option mod_public_ip_request_attempts '2'
option mod_public_ip_timeout '2'
option mod_public_ip_enable_ip_script '0'
option mod_email_enabled '0'
option mod_email_message_at_startup '0'
option mod_email_mode '0'
option mod_email_alive_period '0'
option mod_email_mail_security 'tls'
option mod_telegram_enabled '0'
option mod_telegram_message_at_startup '0'
option mod_telegram_mode '0'
option mod_telegram_dead_period '0'
option mod_telegram_alive_period '0'
option mod_user_scripts_enabled '0'
option mod_user_scripts_alive_period '0'
option mod_user_scripts_up_script_attempts '1'
option mod_user_scripts_up_script_attempt_interval '60'
option mod_user_scripts_connected_at_startup '0'
option mod_user_scripts_dead_period '0'
option mod_user_scripts_down_script_attempts '1'
option mod_user_scripts_down_script_attempt_interval '60'
option mod_user_scripts_disconnected_at_startup '0'
option mod_regular_script_enabled '0'
option mod_regular_script_inet_state '2'
option mod_regular_script_interval '3600'

View File

@@ -3,17 +3,37 @@
START=97
STOP=01
ID="/usr/bin/internet-detector"
USE_PROCD=1
PROG="/usr/bin/internet-detector"
start() {
$ID
run_instance() {
config_get enabled "$1" enabled "0"
if [ $enabled = "1" ]; then
procd_open_instance "$1"
procd_set_param command "$PROG" "-a" "nodaemon" "-i" "$1"
procd_set_param respawn
procd_set_param term_timeout 60
procd_close_instance
fi
}
stop() {
$ID stop
start_service() {
config_load "internet-detector"
config_get mode "config" mode "0"
if [ $mode = "1" ]; then
config_foreach run_instance "instance"
fi
}
restart() {
stop_service() {
$PROG -a stop
}
reload_service() {
stop
start
}
service_triggers() {
procd_add_reload_trigger "internet-detector"
}

View File

@@ -0,0 +1,2 @@
# Shell commands that run when the public IP address changes.
# New IP is available as value of the $PUBLIC_IP variable.

View File

@@ -0,0 +1 @@
# Shell commands that are run regularly

View File

@@ -5,551 +5,94 @@
Dependences:
lua
luci-lib-nixio
luaposix
libuci-lua
(с) 2021 gSpot (https://github.com/gSpotx2f/luci-app-internet-detector)
(с) 2025 gSpot (https://github.com/gSpotx2f/luci-app-internet-detector)
--]]
-- Default settings
local Config = {
mode = 2,
enableLogger = true,
intervalUp = 30,
intervalDown = 5,
connectionAttempts = 2,
connectionTimeout = 2,
UIConnectionAttempts = 1,
UIConnectionTimeout = 1,
hosts = {
[1] = "8.8.8.8",
[2] = "1.1.1.1",
},
tcpPort = 53,
pingPacketSize = 56,
iface = nil,
checkType = 0, -- 0: TCP, 1: ping
hostname = "OpenWrt",
appName = "internet-detector",
commonDir = "/tmp/run",
debugLog = "/tmp/internet-detector.debug",
pingCmd = "/bin/ping",
pingParams = "-c 1",
debug = false,
modules = {},
parsedHosts = {},
}
Config.configDir = string.format("/etc/%s", Config.appName)
Config.modulesDir = string.format("/usr/lib/%s", Config.appName)
Config.pidFile = string.format("%s/%s.pid", Config.commonDir, Config.appName)
Config.statusFile = string.format("%s/%s.status", Config.commonDir, Config.appName)
-- Importing packages
local function prequire(package)
local retVal, pkg = pcall(require, package)
return retVal and pkg
end
local nixio = prequire("nixio")
if not nixio then
error("You need to install nixio...")
end
local uci = prequire("uci")
if not uci then
error("You need to install libuci-lua...")
end
-- Loading settings from UCI
local uciCursor = uci.cursor()
Config.mode = tonumber(uciCursor:get(
Config.appName, "config", "mode"))
Config.enableLogger = (tonumber(uciCursor:get(
Config.appName, "config", "service_enable_logger")) ~= 0)
Config.intervalUp = tonumber(uciCursor:get(
Config.appName, "config", "service_interval_up"))
Config.intervalDown = tonumber(uciCursor:get(
Config.appName, "config", "service_interval_down"))
Config.connectionAttempts = tonumber(uciCursor:get(
Config.appName, "config", "service_connection_attempts"))
Config.connectionTimeout = tonumber(uciCursor:get(
Config.appName, "config", "service_connection_timeout"))
Config.UIConnectionAttempts = tonumber(uciCursor:get(
Config.appName, "config", "ui_connection_attempts"))
Config.UIConnectionTimeout = tonumber(uciCursor:get(
Config.appName, "config", "ui_connection_timeout"))
Config.hosts = uciCursor:get(Config.appName, "config", "hosts")
local tcpPort = uciCursor:get(
Config.appName, "config", "tcp_port")
if tcpPort ~= nil then
Config.tcpPort = tonumber(tcpPort)
end
local pingPacketSize = uciCursor:get(
Config.appName, "config", "ping_packet_size")
if pingPacketSize ~= nil then
Config.pingPacketSize = tonumber(pingPacketSize)
end
local iface = uciCursor:get(
Config.appName, "config", "iface")
if iface ~= nil then
Config.iface = iface
end
Config.checkType = tonumber(uciCursor:get(
Config.appName, "config", "check_type"))
local hostname = uciCursor:get("system", "@[0]", "hostname")
if hostname ~= nil then
Config.hostname = hostname
end
local function 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
local function readValueFromFile(filePath)
local retValue
local fh = io.open(filePath, "r")
if fh then
retValue = fh:read("*l")
fh:close()
end
return retValue
end
local function statusJson(inet, t)
local lines = { [1] = string.format('"inet":%d', 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
local function writeLogMessage(level, msg)
if Config.enableLogger then
nixio.syslog(level, msg)
end
end
local function loadModules()
package.path = string.format("%s;%s/?.lua", package.path, Config.modulesDir)
Config.modules = {}
uciCursor:foreach(
Config.appName,
"module",
function(s)
local mod_name = s[".name"]
if mod_name and s.enabled == "1" then
local m = prequire(mod_name)
if m then
m.config = Config
m.syslog = writeLogMessage
m.writeValue = writeValueToFile
m.readValue = readValueFromFile
m:init(s)
Config.modules[#Config.modules + 1] = m
end
end
end
)
end
local function parseHost(host)
local addr, port = host:match("^([^:]+):?(%d*)")
return addr, tonumber(port) or false
end
local function parseHosts()
Config.parsedHosts = {}
for k, v in ipairs(Config.hosts) do
local addr, port = parseHost(v)
Config.parsedHosts[k] = { addr = addr, port = port }
end
end
local function pingHost(host)
local ping = string.format(
"%s %s -W %d -s %d%s %s > /dev/null 2>&1",
Config.pingCmd,
Config.pingParams,
Config.connectionTimeout,
Config.pingPacketSize,
Config.iface and (" -I " .. Config.iface) or "",
host
)
local retCode = os.execute(ping)
-- Debug
if Config.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
local function TCPConnectionToHost(host, port)
local retCode = 1
local addrInfo = nixio.getaddrinfo(host, "any")
if addrInfo then
local family = addrInfo[1].family
if family then
local socket = nixio.socket(family, "stream")
socket:setopt("socket", "sndtimeo", Config.connectionTimeout)
socket:setopt("socket", "rcvtimeo", Config.connectionTimeout)
if Config.iface then
socket:setopt("socket", "bindtodevice", Config.iface)
end
local success = socket:connect(host, port or Config.tcpPort)
-- Debug
if Config.debug then
local sockAddr, sockPort = socket:getsockname()
local peerAddr, peerPort = socket:getpeername()
io.stdout:write(string.format(
"--- TCP ---\ntime = %s\nconnectionTimeout = %s\niface = %s\nhost:port = %s:%s\nsockname = %s:%s\npeername = %s:%s\nsuccess = %s\n",
os.time(),
Config.connectionTimeout,
tostring(Config.iface),
host,
port or Config.tcpPort,
tostring(sockAddr),
tostring(sockPort),
tostring(peerAddr),
tostring(peerPort),
tostring(success))
)
io.stdout:flush()
end
socket:close()
retCode = success and 0 or 1
end
end
return retCode
end
local function checkHosts()
local checkFunc = (Config.checkType == 1) and pingHost or TCPConnectionToHost
local retCode = 1
for k, v in ipairs(Config.parsedHosts) do
for i = 1, Config.connectionAttempts do
if checkFunc(v.addr, v.port) == 0 then
retCode = 0
break
end
end
if retCode == 0 then
break
end
end
return retCode
end
local function main()
local lastStatus, currentStatus, timeNow, timeDiff, lastTime
local interval = Config.intervalUp
local counter = 0
while true do
if counter == 0 or counter >= interval then
currentStatus = checkHosts()
if not nixio.fs.access(Config.statusFile, "r") then
writeValueToFile(Config.statusFile, statusJson(currentStatus))
end
if currentStatus == 0 then
interval = Config.intervalUp
if lastStatus ~= nil and currentStatus ~= lastStatus then
writeValueToFile(Config.statusFile, statusJson(currentStatus))
writeLogMessage("notice", "Internet connected")
end
else
interval = Config.intervalDown
if lastStatus ~= nil and currentStatus ~= lastStatus then
writeValueToFile(Config.statusFile, statusJson(currentStatus))
writeLogMessage("notice", "Internet disconnected")
end
end
counter = 0
end
timeDiff = 0
for _, e in ipairs(Config.modules) do
timeNow = nixio.sysinfo().uptime
if lastTime then
timeDiff = timeDiff + timeNow - lastTime
else
timeDiff = 1
end
lastTime = timeNow
e:run(currentStatus, lastStatus, timeDiff)
end
local modulesStatus = {}
for k, v in ipairs(Config.modules) do
if v.status ~= nil then
modulesStatus[v.name] = v.status
end
end
if next(modulesStatus) then
writeValueToFile(Config.statusFile, statusJson(currentStatus, modulesStatus))
end
lastStatus = currentStatus
nixio.nanosleep(1)
counter = counter + 1
end
end
local function removeProcessFiles()
os.remove(Config.pidFile)
os.remove(Config.statusFile)
end
local function status()
if nixio.fs.access(Config.pidFile, "r") then
return "running"
else
return "stoped"
end
end
local function poll(attempts, timeout)
if Config.mode == 1 then
Config.connectionAttempts = Config.UIConnectionAttempts
Config.connectionTimeout = Config.UIConnectionTimeout
end
if attempts then
Config.connectionAttempts = attempts
end
if timeout then
Config.connectionTimeout = timeout
end
if checkHosts() == 0 then
return statusJson(0)
else
return statusJson(1)
end
end
local function inetStatus(json)
local inetStat = 1
if nixio.fs.access(Config.statusFile, "r") then
local inetStatVal = readValueFromFile(Config.statusFile)
inetStat = inetStatVal
elseif Config.mode == 1 then
inetStat = poll()
else
os.exit(126)
end
if not json then
local sVal = inetStat:match('"inet":[0-9]')
if sVal then
sVal = sVal:match("[0-9]")
inetStat = (tonumber(sVal) == 0) and "up" or "down"
end
end
return inetStat
end
local function stop()
local pidValue
if Config.enableLogger then
nixio.openlog(Config.appName)
end
if nixio.fs.access(Config.pidFile, "r") then
pidValue = readValueFromFile(Config.pidFile)
if pidValue then
local success
for i = 0, 10 do
success = nixio.kill(tonumber(pidValue), 15)
if success then
break
end
end
if not success then
io.stderr:write(string.format('No such process: "%s"\n', pidValue))
end
writeLogMessage("info", string.format("[%s] stoped", pidValue))
removeProcessFiles()
end
end
if not pidValue then
io.stderr:write(
string.format('PID file "%s" does not exist. %s not running?\n',
Config.pidFile, Config.appName))
end
if Config.enableLogger then
nixio.closelog()
end
end
local function preRun()
-- Exit if internet-detector mode != 2(Service)
if Config.mode ~= 2 then
io.stderr:write(string.format('Start failed, mode != 2\n', Config.appName))
os.exit(0)
end
if nixio.fs.access(Config.pidFile, "r") then
io.stderr:write(
string.format('PID file "%s" already exist. %s already running?\n',
Config.pidFile, Config.appName))
return false
end
return true
end
local function run()
local pidValue = nixio.getpid()
writeValueToFile(Config.pidFile, pidValue)
if Config.enableLogger then
nixio.openlog(Config.appName, "pid")
end
writeLogMessage("info", "started")
loadModules()
-- Loaded modules
local modules = {}
for _, v in ipairs(Config.modules) do
modules[#modules + 1] = string.format("%s", v.name)
end
if #modules > 0 then
writeLogMessage(
"info", string.format("Loaded modules: %s", table.concat(modules, ", "))
)
end
-- Debug
if Config.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()(Config, "Config.")
io.stdout:flush()
end
main()
if Config.enableLogger then
nixio.closelog()
end
end
local function noDaemon()
if not preRun() then
return
end
run()
end
local function daemon(debug)
if not preRun() then
return
end
-- UNIX double fork
if nixio.fork() == 0 then
nixio.setsid()
if nixio.fork() == 0 then
nixio.chdir("/")
nixio.umask(0)
local output = "/dev/null"
if debug then
output = Config.debugLog
Config.debug = true
end
io.stdout:flush()
io.stderr:flush()
nixio.dup(io.open("/dev/null", "r"), io.stdin)
nixio.dup(io.open(output, "a+"), io.stdout)
nixio.dup(io.open(output, "a+"), io.stderr)
run()
end
os.exit(0)
end
os.exit(0)
end
local function restart()
stop()
daemon()
end
-- Main section
parseHosts()
local getopt = require("posix.unistd").getopt
local InternetDetector = require("internet-detector.main")
local function help()
return string.format(
"Usage: %s [start|stop|restart|no-daemon|debug|status|inet-status|inet-status-json|poll [<attempts num>] [<timeout sec>]|--help]",
arg[0]
)
return table.concat({
[1] = string.format(
"Usage: %s -a daemon -i <UCI instance> | -a nodaemon -i <UCI instance> | -a debug -i <UCI instance> | -a stop | -S | -I | -U | -h",
arg[0]),
[2] = " -a ARG action: daemon | nodaemon | debug | stop",
[3] = " -i ARG instance: UCI instance name",
[4] = " -S status",
[5] = " -I inet status",
[6] = " -U uipoll",
[7] = " -h print this help text"
}, "\n")
end
local helpArgs = { ["-h"] = true, ["--help"] = true, ["help"] = true }
if arg[1] == "start" or #arg == 0 then
daemon()
elseif arg[1] == "no-daemon" then
noDaemon()
elseif arg[1] == "debug" then
daemon(true)
elseif arg[1] == "stop" then
stop()
elseif arg[1] == "restart" then
restart()
elseif arg[1] == "status" then
print(status())
elseif arg[1] == "inet-status" then
print(inetStatus())
elseif arg[1] == "inet-status-json" then
print(inetStatus(true))
elseif arg[1] == "poll" then
local attempts, timeout
if arg[2] and arg[2]:match("[0-9]+") then
attempts = tonumber(arg[2])
if arg[3] and arg[3]:match("[0-9]+") then
timeout = tonumber(arg[3])
end
local action, instance
local params = {}
local last_index = 1
for r, optarg, optind in getopt(arg, "a:i:SIUh") do
if r == "?" then
print("Error! Unrecognized option")
os.exit(1)
end
last_index = optind
if r == "a" then
action = optarg
elseif r == "i" then
instance = optarg
else
params[#params + 1] = r
end
end
if action == "stop" then
InternetDetector:stop()
elseif action then
if not instance then
print("Error! Instance not specified [-i]")
os.exit(1)
end
if action == "daemon" then
if InternetDetector:setServiceConfig(instance) then
InternetDetector:daemon()
else
os.exit(126)
end
elseif action == "nodaemon" then
if InternetDetector:setServiceConfig(instance) then
InternetDetector:noDaemon()
else
os.exit(126)
end
elseif action == "debug" then
if InternetDetector:setServiceConfig(instance) then
InternetDetector.debug = true
InternetDetector:noDaemon()
else
os.exit(126)
end
else
print("Error! Wrong action [-a]")
os.exit(1)
end
print(poll(attempts, timeout))
elseif helpArgs[arg[1]] then
print(help())
else
print(help())
os.exit(1)
if params[1] == "S" then
print(InternetDetector:status())
elseif params[1] == "I" then
print(InternetDetector:inetStatus())
elseif params[1] == "U" then
if InternetDetector:status() == "stoped" then
os.exit(126)
else
InternetDetector:setSIGUSR()
print(InternetDetector:inetStatus())
end
elseif params[1] == "h" then
print(help())
else
print(help())
os.exit(1)
end
end
os.exit(0)

View File

@@ -1,139 +0,0 @@
--[[
Dependences:
mailsend
--]]
local nixio = require("nixio")
local Module = {
name = "mod_email",
config = {},
syslog = function(level, msg) return true end,
writeValue = function(filePath, str) return false end,
readValue = function(filePath) return nil end,
alivePeriod = 0,
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',
mailSecurity = "tls",
status = nil,
_enabled = false,
_aliveCounter = 0,
_msgSent = true,
_disconnected = true,
_lastDisconnection = nil,
_lastConnection = nil,
}
function Module:init(t)
self.alivePeriod = tonumber(t.alive_period)
if t.host_alias then
self.hostAlias = t.host_alias
else
self.hostAlias = self.config.hostname
end
self.mailRecipient = t.mail_recipient
self.mailSender = t.mail_sender
self.mailUser = t.mail_user
self.mailPassword = t.mail_password
self.mailSmtp = t.mail_smtp
self.mailSmtpPort = t.mail_smtp_port
self.mailSecurity = t.mail_security
if nixio.fs.access(self.mta, "x") then
self._enabled = true
else
self._enabled = false
self.syslog("warning", string.format("%s: %s is not available", self.name, self.mta))
end
if (not self.mailRecipient or
not self.mailSender or
not self.mailUser or
not self.mailPassword or
not self.mailSmtp or
not self.mailSmtpPort) then
self._enabled = false
self.syslog("warning", string.format(
"%s: Insufficient data to connect to the SMTP server", self.name))
end
end
function Module:sendMessage(msg)
local verboseArg = ""
-- Debug
if self.config.debug then
verboseArg = " -v"
io.stdout:write("--- mod_email ---\n")
io.stdout:flush()
end
local securityArgs = "-starttls -auth-login"
if self.mailSecurity == "ssl" then
securityArgs = "-ssl -auth"
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"',
self.mta, verboseArg, securityArgs, self.mailSmtp, self.mailSmtpPort,
self.mailUser, self.mailPassword, self.mailSender, self.mailRecipient,
string.format("%s notification", self.hostAlias),
string.format("%s:\n%s", self.hostAlias, msg))
if os.execute(mtaCmd) ~= 0 then
self.syslog("err", string.format(
"%s: An error occured while sending message", self.name))
else
self.syslog("info", string.format(
"%s: Message sent to %s", self.name, self.mailRecipient))
end
end
function Module:run(currentStatus, lastStatus, timeDiff)
if not self._enabled then
return
end
if currentStatus == 1 then
self._aliveCounter = 0
self._msgSent = 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
end
else
if not self._msgSent 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(
"Internet connected: %s", self._lastConnection)
self:sendMessage(table.concat(message, ", "))
self._msgSent = true
end
else
self._aliveCounter = self._aliveCounter + timeDiff
end
end
self._disconnected = false
end
end
return Module

View File

@@ -1,91 +0,0 @@
local nixio = require("nixio")
local Module = {
name = "mod_led_control",
config = {},
syslog = function(level, msg) return true end,
writeValue = function(filePath, str) return false end,
readValue = function(filePath) return nil end,
runInterval = 5,
sysLedsDir = "/sys/class/leds",
ledName = nil,
status = nil,
_enabled = false,
_ledDir = nil,
_ledMaxBrightnessFile = nil,
_ledBrightnessFile = nil,
_ledMaxBrightness = nil,
_counter = 0,
}
function Module:resetLeds()
local dir = nixio.fs.dir(self.sysLedsDir)
if not dir then
return
end
for led in dir do
local brightness = string.format("%s/%s/brightness", self.sysLedsDir, led)
if nixio.fs.access(brightness, "w") then
self.writeValue(brightness, 0)
end
end
end
function Module:init(t)
self.ledName = t.led_name
if not self.ledName then
return
end
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)
self._ledMaxBrightness = self.readValue(self._ledMaxBrightnessFile) or 1
if (not nixio.fs.access(self._ledDir, "r") or
not nixio.fs.access(self._ledBrightnessFile, "r", "w")) then
self._enabled = false
self.syslog("warning", string.format("%s: LED '%s' is not available", self.name, self.ledName))
else
self._enabled = true
-- Reset all LEDs
--self:resetLeds()
end
end
function Module:getCurrentState()
local state = self.readValue(self._ledBrightnessFile)
if state and tonumber(state) > 0 then
return tonumber(state)
end
end
function Module:on()
self.writeValue(self._ledBrightnessFile, self._ledMaxBrightness)
end
function Module:off()
self.writeValue(self._ledBrightnessFile, 0)
end
function Module:run(currentStatus, lastStatus, timeDiff)
if not self._enabled then
return
end
if self._counter == 0 or self._counter >= self.runInterval or currentStatus ~= lastStatus then
if currentStatus == 0 then
if not self:getCurrentState() then
self:on()
end
else
if self:getCurrentState() then
self:off()
end
end
self._counter = 0
end
self._counter = self._counter + timeDiff
end
return Module

View File

@@ -1,85 +0,0 @@
--[[
Dependences:
modemmanager
--]]
local nixio = require("nixio")
local Module = {
name = "mod_modem_restart",
config = {},
syslog = function(level, msg) return true end,
writeValue = function(filePath, str) return false end,
readValue = function(filePath) return nil end,
mmcli = "/usr/bin/mmcli",
mmInit = "/etc/init.d/modemmanager",
deadPeriod = 0,
iface = nil,
anyBand = false,
status = nil,
_enabled = false,
_deadCounter = 0,
_restarted = false,
}
function Module:toggleIface(flag)
return os.execute(
string.format("%s %s", (flag and "/sbin/ifup" or "/sbin/ifdown"), self.iface)
)
end
function Module:restartMM()
if self.anyBand then
self.syslog("info", string.format(
"%s: resetting current-bands to 'any'", self.name))
os.execute(string.format("%s -m any --set-current-bands=any", self.mmcli))
end
self.syslog("info", string.format("%s: reconnecting modem", self.name))
os.execute(string.format("%s restart", self.mmInit))
if self.iface then
self.syslog("info", string.format(
"%s: restarting network interface '%s'", self.name, self.iface))
self:toggleIface(false)
self:toggleIface(true)
end
end
function Module:init(t)
self.deadPeriod = tonumber(t.dead_period)
self.iface = t.iface
self.anyBand = (tonumber(t.any_band) ~= 0)
if not nixio.fs.access(self.mmcli, "x") then
self.anyBand = false
end
if nixio.fs.access(self.mmInit, "x") then
self._enabled = true
else
self._enabled = false
self.syslog("warning", string.format(
"%s: modemmanager service is not available", self.name))
end
end
function Module:run(currentStatus, lastStatus, timeDiff)
if not self._enabled then
return
end
if currentStatus == 1 then
if not self._restarted then
if self._deadCounter >= self.deadPeriod then
self:restartMM()
self._restarted = true
else
self._deadCounter = self._deadCounter + timeDiff
end
end
else
self._deadCounter = 0
self._restarted = false
end
end
return Module

View File

@@ -1,92 +0,0 @@
local nixio = require("nixio")
local Module = {
name = "mod_network_restart",
config = {},
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,
restartTimeout = 0,
status = nil,
_attemptsCounter = 0,
_deadCounter = 0,
}
function Module:toggleFunc(flag)
return
end
function Module:toggleDevice(flag)
local ip = "/sbin/ip"
if nixio.fs.access(ip, "x") then
return os.execute(
string.format("%s link set dev %s %s", ip, self.iface, (flag and "up" or "down"))
)
end
end
function Module:toggleIface(flag)
return os.execute(
string.format("%s %s", (flag and "/sbin/ifup" or "/sbin/ifdown"), self.iface)
)
end
function Module:ifaceUp()
self:toggleFunc(true)
end
function Module:ifaceDown()
self:toggleFunc(false)
end
function Module:networkRestart()
return os.execute("/etc/init.d/network restart")
end
function Module:init(t)
local iface = t.iface
if iface then
self.iface = iface
if self.iface:match("^@") then
self.iface = self.iface:gsub("^@", "")
self.toggleFunc = self.toggleIface
else
self.toggleFunc = self.toggleDevice
end
end
self.attempts = tonumber(t.attempts)
self.deadPeriod = tonumber(t.dead_period)
self.restartTimeout = tonumber(t.restart_timeout)
end
function Module:run(currentStatus, lastStatus, timeDiff)
if currentStatus == 1 then
if self.attempts == 0 or self._attemptsCounter < self.attempts then
if self._deadCounter >= self.deadPeriod then
if self.iface then
self.syslog("info", string.format(
"%s: restarting network interface '%s'", self.name, self.iface))
self:ifaceDown()
nixio.nanosleep(self.restartTimeout)
self:ifaceUp()
else
self.syslog("info", string.format("%s: restarting network", self.name))
self:networkRestart()
end
self._deadCounter = 0
self._attemptsCounter = self._attemptsCounter + 1
else
self._deadCounter = self._deadCounter + timeDiff
end
end
else
self._attemptsCounter = 0
self._deadCounter = 0
end
end
return Module

View File

@@ -1,139 +0,0 @@
local nixio = require("nixio")
local Module = {
name = "mod_public_ip",
config = {},
syslog = function(level, msg) return true end,
writeValue = function(filePath, str) return false end,
readValue = function(filePath) return nil end,
runInterval = 600,
nslookup = "/usr/bin/nslookup",
timeout = 3,
providers = {
opendns1 = {
name = "opendns1", server = "208.67.222.222",
host = "myip.opendns.com", queryType = "a"
},
opendns2 = {
name = "opendns2", server = "208.67.220.220",
host = "myip.opendns.com", queryType = "a"
},
opendns3 = {
name = "opendns3", server = "208.67.222.220",
host = "myip.opendns.com", queryType = "a"
},
opendns4 = {
name = "opendns4", server = "208.67.220.222",
host = "myip.opendns.com", queryType = "a"
},
akamai = {
name = "akamai", server = "ns1-1.akamaitech.net",
host = "whoami.akamai.net", queryType = "a"
},
google = {
name = "google", server = "ns1.google.com",
host = "o-o.myaddr.l.google.com", queryType = "txt"
},
},
status = nil,
_provider = nil,
_nslookupCmd = nil,
_currentIp = nil,
_enabled = false,
_counter = 0,
}
function Module:parseA(str)
res = str:match("Name:%s+" .. self._provider.host .. "\nAddress:%s+[%w.:]+")
if res then
return res:match("[%w.:]+$")
end
end
function Module:parseGoogle(str)
res = str:match(self._provider.host .. '%s+text%s+=%s+"[%w.:]+"')
if res then
return res:gsub('"', ''):match("[%w.:]+$")
end
end
function Module:resolveIP()
local res
local fh = io.popen(self._nslookupCmd, "r")
if fh then
output = fh:read("*a")
fh:close()
if self._provider.name == "google" then
res = self:parseGoogle(output)
else
res = self:parseA(output)
end
else
self.syslog("err", string.format(
"%s: Nslookup call failed (%s)", self.name, self.nslookup))
end
return res or "Undefined"
end
function Module:init(t)
if t.interval then
self.runInterval = tonumber(t.interval)
end
if t.timeout then
self.timeout = tonumber(t.timeout)
end
if t.provider then
self._provider = self.providers[t.provider]
else
self._provider = self.providers.opendns1
end
if not nixio.fs.access(self.nslookup, "x") then
self._enabled = false
self.syslog(
"warning",
string.format("%s: '%s' does not exists", self.name, self.nslookup)
)
else
self._enabled = true
self._nslookupCmd = string.format(
"%s -type=%s -timeout=%d %s %s",
self.nslookup,
self._provider.queryType,
self.timeout,
self._provider.host,
self._provider.server
)
end
end
function Module:run(currentStatus, lastStatus, timeDiff)
if not self._enabled then
return
end
if currentStatus == 0 then
if self._counter == 0 or self._counter >= self.runInterval or currentStatus ~= lastStatus then
local ip = self:resolveIP()
if ip ~= self._currentIp then
self.status = ip
self.syslog(
"notice",
string.format("%s: public IP address %s", self.name, ip)
)
else
self.status = nil
end
self._currentIp = ip
self._counter = 0
else
self.status = nil
end
else
self.status = nil
self._currentIp = nil
self._counter = 0
end
self._counter = self._counter + timeDiff
end
return Module

View File

@@ -1,47 +0,0 @@
local nixio = require("nixio")
local Module = {
name = "mod_reboot",
config = {},
syslog = function(level, msg) return true end,
writeValue = function(filePath, str) return false end,
readValue = function(filePath) return nil end,
deadPeriod = 0,
forceRebootDelay = 0,
status = nil,
_deadCounter = 0,
}
function Module:rebootDevice()
self.syslog("warning", string.format("%s: reboot", self.name))
os.execute("/sbin/reboot &")
if self.forceRebootDelay > 0 then
nixio.nanosleep(self.forceRebootDelay)
self.syslog("warning", string.format("%s: force reboot", self.name))
self.writeValue("/proc/sys/kernel/sysrq", "1")
self.writeValue("/proc/sysrq-trigger", "b")
end
end
function Module:init(t)
self.deadPeriod = tonumber(t.dead_period)
self.forceRebootDelay = tonumber(t.force_reboot_delay)
end
function Module:run(currentStatus, lastStatus, timeDiff)
if currentStatus == 1 then
if self._deadCounter >= self.deadPeriod then
self:rebootDevice()
self._deadCounter = 0
else
self._deadCounter = self._deadCounter + timeDiff
end
else
self._deadCounter = 0
end
end
return Module

View File

@@ -1,64 +0,0 @@
local nixio = require("nixio")
local Module = {
name = "mod_user_scripts",
config = {},
syslog = function(level, msg) return true end,
writeValue = function(filePath, str) return false end,
readValue = function(filePath) return nil end,
deadPeriod = 0,
alivePeriod = 0,
upScript = "",
downScript = "",
status = nil,
_deadCounter = 0,
_aliveCounter = 0,
_upScriptExecuted = true,
_downScriptExecuted = true,
}
function Module:runExternalScript(scriptPath)
if nixio.fs.access(scriptPath, "x") then
os.execute(string.format('/bin/sh -c "%s" &', scriptPath))
end
end
function Module:init(t)
self.deadPeriod = tonumber(t.dead_period)
self.alivePeriod = tonumber(t.alive_period)
if self.config.configDir then
self.upScript = string.format("%s/up-script", self.config.configDir)
self.downScript = string.format("%s/down-script", self.config.configDir)
end
end
function Module:run(currentStatus, lastStatus, timeDiff)
if currentStatus == 1 then
self._aliveCounter = 0
self._downScriptExecuted = false
if not self._upScriptExecuted then
if self._deadCounter >= self.deadPeriod then
self:runExternalScript(self.downScript)
self._upScriptExecuted = true
else
self._deadCounter = self._deadCounter + timeDiff
end
end
else
self._deadCounter = 0
self._upScriptExecuted = false
if not self._downScriptExecuted then
if self._aliveCounter >= self.alivePeriod then
self:runExternalScript(self.upScript)
self._downScriptExecuted = true
else
self._aliveCounter = self._aliveCounter + timeDiff
end
end
end
end
return Module

View File

@@ -0,0 +1 @@
return require("internet-detector.main")

View File

@@ -0,0 +1,794 @@
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 = {
appName = "internet-detector",
libDir = "/usr/lib/lua",
logLevels = {
emerg = { level = syslog.LOG_EMERG, num = 0 },
alert = { level = syslog.LOG_ALERT, num = 1 },
crit = { level = syslog.LOG_CRIT, num = 2 },
err = { level = syslog.LOG_ERR, num = 3 },
warning = { level = syslog.LOG_WARNING, num = 4 },
notice = { level = syslog.LOG_NOTICE, num = 5 },
info = { level = syslog.LOG_INFO, num = 6 },
debug = { level = syslog.LOG_DEBUG, num = 7 },
},
pingCmd = "/bin/ping",
pingParams = "-c 1",
curlExec = "/usr/bin/curl",
curlParams = '-s --no-keepalive --head --user-agent "Mozilla/5.0 (X11; Linux x86_64; rv:142.0) Gecko/20100101 Firefox/142.0"',
mode = 0, -- 0: disabled, 1: Service, 2: UI detector
loggingLevel = 6,
hostname = "OpenWrt",
uiRunTime = 30,
noModules = false,
uiAvailModules = { mod_public_ip = true },
debug = false,
serviceConfig = {
hosts = {
[1] = "8.8.8.8",
[2] = "1.1.1.1",
},
urls = {
[1] = "https://www.google.com",
},
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,
proxy_type = nil,
proxy_host = nil,
proxy_port = nil,
iface = nil,
instance = nil,
},
modules = {},
parsedHosts = {},
proxyString = "",
uiCounter = 0,
pidFile = nil,
statusFile = nil,
}
InternetDetector.configDir = string.format("/etc/%s", InternetDetector.appName)
InternetDetector.modulesDir = string.format(
"%s/%s/modules", InternetDetector.libDir, InternetDetector.appName)
InternetDetector.commonDir = string.format("/tmp/run/%s", InternetDetector.appName)
InternetDetector.appNamePattern = InternetDetector.appName:gsub("-", "%%-")
InternetDetector.pidFilePattern = "^" .. InternetDetector.appNamePattern .. ".-%.pid$"
-- 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 loggingLevel, err = uciCursor:get(InternetDetector.appName, "config", "logging_level")
if loggingLevel ~= nil then
InternetDetector.loggingLevel = tonumber(loggingLevel)
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
function InternetDetector:prequire(package)
local ok, pkg = pcall(require, package)
return ok and pkg
end
function InternetDetector:loadInstanceConfig(instance)
local sections = uciCursor:get_all(self.appName)
local t = sections[instance]
if t then
for k, v in pairs(t) do
if type(v) == "string" and v:match("^[%d]+$") then
v = tonumber(v)
end
self.serviceConfig[k] = v
end
self.serviceConfig.instance = instance
self.serviceConfig.instanceNum = t[".index"]
return true
end
return false
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)
local levelItem = self.logLevels[level]
local levelValue = (levelItem and levelItem.level) or self.logLevels["info"].level
local num = (levelItem and levelItem.num) or self.logLevels["info"].num
if num <= self.loggingLevel then
syslog.syslog(levelValue, string.format(
"%s: %s", self.serviceConfig.instance or "", msg))
end
end
function InternetDetector:debugOutput(msg)
if self.debug then
io.stdout:write(string.format("[%s] %s\n", os.date("%Y.%m.%d-%H:%M:%S"), msg))
io.stdout:flush()
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.modules.%s", self.appName, modName))
else
m = self:prequire(string.format("%s.modules.%s", self.appName, modName))
end
if m then
m.config = self
m.syslog = function(level, msg) self:writeLogMessage(level, msg) end
m.debugOutput = function(msg) self:debugOutput(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:parseUrls()
self.parsedHosts = {}
for k, v in ipairs(self.serviceConfig.urls) do
self.parsedHosts[k] = { addr = v }
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)
self:debugOutput(string.format(
"--- Ping ---\ntime = %s\n%s\nretCode = %s", os.time(), ping, retCode))
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
self:debugOutput(string.format(
"GETADDRINFO ERROR: %s, %s", errMsg, errNum))
else
local family = saTable[1].family
if family then
local sock, errMsg, errNum = socket.socket(family, socket.SOCK_STREAM, 0)
if not sock then
self:debugOutput(string.format(
"SOCKET ERROR: %s, %s", errMsg, errNum))
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
self:debugOutput(string.format(
"SOCKET ERROR: %s, %s", errMsg, errNum))
unistd.close(sock)
return retCode
end
end
local success = socket.connect(sock, saTable[1])
if self.debug then
if not success then
self:debugOutput(string.format(
"SOCKET CONNECT ERROR: %s", 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 = {}
self:debugOutput(
string.format("SOCKET ERROR: %s, %s", err_s, e_s))
end
if not peerTable then
peerTable = {}
self:debugOutput(
string.format("SOCKET ERROR: %s, %s", err_p, e_p))
end
self:debugOutput(string.format(
"--- TCP ---\ntime = %s\nconnection_timeout = %s\niface = %s\nhost:port = [%s]:%s\nsockname = [%s]:%s\npeername = [%s]:%s\nsuccess = %s",
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))
)
end
socket.shutdown(sock, socket.SHUT_RDWR)
unistd.close(sock)
retCode = success and 0 or 1
end
end
return retCode
end
function InternetDetector:httpRequest(url)
local retCode = 1, data
local curl = string.format(
'%s%s%s --connect-timeout %s %s "%s"; printf "\n$?";',
self.curlExec,
self.serviceConfig.iface and (" --interface " .. self.serviceConfig.iface) or "",
self.proxyString,
self.serviceConfig.connection_timeout,
self.curlParams,
url
)
local fh = io.popen(curl, "r")
if fh then
data = fh:read("*a")
fh:close()
if data ~= nil then
local s, e = data:find("[0-9]+\n?$")
retCode = tonumber(data:sub(s))
data = data:sub(0, s - 2)
if not data or data == "" then
data = nil
end
end
else
retCode = 1
end
self:debugOutput(string.format(
"--- Curl ---\ntime = %s\n%s\nretCode = %s\ndata = [\n%s]\n",
os.time(),
curl,
retCode,
tostring(data)))
return retCode, data
end
function InternetDetector:getHTTPCode(data)
local httpCode
local respHeader = data:match("^HTTP/[^%c]+")
if respHeader then
httpCode = respHeader:match("%d%d%d")
end
return tonumber(httpCode)
end
function InternetDetector:checkURL(url)
local httpCode
local retCode, data = self:httpRequest(url)
if retCode == 0 and data then
httpCode = self:getHTTPCode(data)
end
return (httpCode ~= 200) and 1 or 0
end
function InternetDetector:exit()
for _, e in ipairs(self.modules) do
e:onExit()
end
self:removeProcessFiles()
if self.loggingLevel > 0 then
self:writeLogMessage("info", "stoped")
syslog.closelog()
end
os.exit(0)
end
function InternetDetector:resetUiCounter(signo)
self.uiCounter = 0
end
function InternetDetector:mainLoop()
signal.signal(signal.SIGTERM, function(signo) self:exit(signo) end)
signal.signal(signal.SIGINT, function(signo) self:exit(signo) end)
signal.signal(signal.SIGQUIT, function(signo) self:exit(signo) end)
signal.signal(signal.SIGUSR1, function(signo) self:resetUiCounter(signo) end)
local mTimeNow, mTimeDiff, mLastTime, uiTimeNow, uiLastTime
local lastStatus = -1
local currentStatus = -1
local interval = self.serviceConfig.interval_up
local modulesStatus = {}
local counter = 0
local inetChecked = false
local checking = false
local hostNum = 1
local attempt = 1
local checkFunc = self.TCPConnectionToHost
if self.serviceConfig.check_type == 1 then
checkFunc = self.pingHost
self:parseHosts()
elseif self.serviceConfig.check_type == 2 then
checkFunc = self.checkURL
self:parseUrls()
if (self.serviceConfig.proxy_type and self.serviceConfig.proxy_host and
self.serviceConfig.proxy_port) then
self.proxyString = string.format(
" --proxy %s://%s:%d",
self.serviceConfig.proxy_type,
self.serviceConfig.proxy_host,
self.serviceConfig.proxy_port)
end
else
self:parseHosts()
end
self:writeValueToFile(
self.statusFile, self:statusJson(currentStatus, self.serviceConfig.instance))
while true do
if counter == 0 or counter >= interval then
checking = true
end
inetChecked = false
if checking then
local newStatus = 1
if hostNum <= #self.parsedHosts then
if attempt <= self.serviceConfig.connection_attempts then
local addr = self.parsedHosts[hostNum].addr
local port = self.parsedHosts[hostNum].port
local retCode = 1
if self.debug then
retCode = checkFunc(self, addr, port)
else
local ok, status = pcall(checkFunc, self, addr, port)
if ok then
retCode = status
else
self:writeLogMessage("err", string.format(
"An error occurred while checking the host %s: %s",
tostring(addr),
tostring(status))
)
end
end
if retCode == 0 then
attempt = 1
hostNum = 1
checking = false
newStatus = 0
counter = 0
inetChecked = true
else
attempt = attempt + 1
if attempt > self.serviceConfig.connection_attempts then
attempt = 1
hostNum = hostNum + 1
end
end
else
attempt = 1
hostNum = hostNum + 1
end
if hostNum > #self.parsedHosts then
hostNum = 1
checking = false
counter = 0
inetChecked = true
end
else
hostNum = 1
checking = false
counter = 0
inetChecked = true
end
if inetChecked then
currentStatus = newStatus
if not stat.stat(self.statusFile) then
self:writeValueToFile(self.statusFile, self:statusJson(
currentStatus, self.serviceConfig.instance))
end
if currentStatus == 0 then
interval = self.serviceConfig.interval_up
if currentStatus ~= lastStatus then
self:writeValueToFile(self.statusFile, self:statusJson(
currentStatus, self.serviceConfig.instance))
self:writeLogMessage("notice", "Connected")
end
elseif currentStatus == 1 then
interval = self.serviceConfig.interval_down
if currentStatus ~= lastStatus then
self:writeValueToFile(self.statusFile, self:statusJson(
currentStatus, self.serviceConfig.instance))
self:writeLogMessage("notice", "Disconnected")
end
end
end
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
if self.debug then
e:run(currentStatus, lastStatus, mTimeDiff, mTimeNow, inetChecked)
else
local ok, err = pcall(e.run, e, currentStatus, lastStatus, mTimeDiff, mTimeNow, inetChecked)
if not ok then
self:writeLogMessage("err", string.format(
"%s: Module error: %s", e.name, tostring(err)))
end
end
end
local modStatusChanged = false
for k, v in ipairs(self.modules) do
if modulesStatus[v.name] ~= v.status then
modulesStatus[v.name] = v.status
modStatusChanged = true
end
end
if modStatusChanged and next(modulesStatus) then
self:writeValueToFile(self.statusFile, self:statusJson(
currentStatus, self.serviceConfig.instance, modulesStatus))
end
unistd.sleep(1)
if not checking then
lastStatus = currentStatus
counter = counter + 1
end
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:exit(signal.SIGTERM)
end
end
end
end
function InternetDetector:removeProcessFiles()
os.remove(self.statusFile)
os.remove(self.pidFile)
end
function InternetDetector:status()
local ok, commonDir = pcall(dirent.files, self.commonDir)
if ok then
for item in commonDir do
if item:match(self.pidFilePattern) 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 statusFilePattern = "^" .. self.appNamePattern .. ".-%.status$"
local lines = {}
for item in commonDir do
if item:match(statusFilePattern) 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 = false, pidValue
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 stopping error: %s (%s). PID: "%s"\n', errMsg, errNum, pidValue))
end
if errNum == 3 then
os.remove(pidFile)
end
retVal = true
else
os.remove(pidFile)
end
end
if not pidValue then
io.stderr:write(
string.format('PID file "%s" does not exists. Is the %s not running?\n',
pidFile, self.appName))
end
return retVal
end
function InternetDetector:stop()
local nopids = false
for i = 0, 100 do
nopids = true
local ok, commonDir = pcall(dirent.files, self.commonDir)
if ok then
for item in commonDir do
if item:match(self.pidFilePattern) then
if self:stopInstance(string.format("%s/%s", self.commonDir, item)) then
nopids = false
end
end
end
if nopids then
break
end
time.nanosleep({ tv_sec = 0, tv_nsec = 10000000 })
else
break
end
end
end
function InternetDetector:setSIGUSR()
local ok, commonDir = pcall(dirent.files, self.commonDir)
if ok then
for item in commonDir do
if item:match(self.pidFilePattern) 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
local s = stat.stat(self.commonDir)
if not s or not (stat.S_ISDIR(s.st_mode) ~= 0) then
if not stat.mkdir(self.commonDir) then
io.stderr:write(
string.format('Error occurred while creating %s. Exit.\n', self.commonDir))
os.exit(1)
end
end
if self.serviceConfig.check_type == 2 and not unistd.access(self.curlExec, "x") then
io.stderr:write(string.format(
"Error, %s is not available. You need to install curl.\n", self.curlExec))
os.exit(1)
end
local ok, commonDir = pcall(dirent.files, self.commonDir)
if ok then
local instancePattern = "^" .. self.appNamePattern .. "%." .. self.serviceConfig.instance .. "%.[%d]+%.pid$"
for item in commonDir do
if item:match(instancePattern) then
self:stopInstance(string.format("%s/%s", self.commonDir, item))
end
end
end
end
function InternetDetector:run()
local pidValue = unistd.getpid()
self.pidFile = string.format(
"%s/%s.%s.%s.pid", self.commonDir, self.appName, self.serviceConfig.instance, pidValue)
self.statusFile = string.format(
"%s/%s.%s.status", self.commonDir, self.appName, self.serviceConfig.instance)
self:writeValueToFile(self.pidFile, pidValue)
if self.loggingLevel > 0 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
self:debugOutput(string.format(
"%s%s = %s", 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
self:debugOutput("--- Config ---")
inspectTable()(self, "self.")
end
self:mainLoop()
self:exit()
end
function InternetDetector:noDaemon()
self:preRun()
self:run()
end
function InternetDetector:daemon()
self:preRun()
-- 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:loadInstanceConfig(instance) then
if self.mode == 2 then
self.loggingLevel = 0
self.noModules = true
end
return true
end
end
return InternetDetector

View File

@@ -0,0 +1,334 @@
local dirent = require("posix.dirent")
local time = require("posix.time")
local unistd = require("posix.unistd")
local Module = {
name = "mod_led_control",
runPrio = 10,
config = {},
syslog = function(level, msg) return true end,
debugOutput = function(msg) return true end,
writeValue = function(filePath, str) return false end,
readValue = function(filePath) return nil end,
runInterval = 5,
sysLedsDir = "/sys/class/leds",
ledsPerInstance = 3,
ledAction1Default = 1, -- 1: off, 2: on, 3: blinking, 4: netdev
ledAction2Default = 1,
ledBlinkDelayDefault = 500,
ledNetlinkDeviceDefault = nil,
ledNetdevModeLinkDefault = "1",
ledNetdevModeRxDefault = "0",
ledNetdevModeTxDefault = "0",
status = nil,
_enabled = false,
_leds = {},
_counter = 0,
_exit = false,
}
function Module:setLedAttrs(t)
t.ledDir = string.format("%s/%s", self.sysLedsDir, t.ledName)
t.ledMaxBrightnessFile = string.format("%s/max_brightness", t.ledDir)
t.ledBrightnessFile = string.format("%s/brightness", t.ledDir)
t.ledMaxBrightness = self.readValue(t.ledMaxBrightnessFile) or "1"
t.ledTriggerFile = string.format("%s/trigger", t.ledDir)
t.ledDelayOnFile = string.format("%s/delay_on", t.ledDir)
t.ledDelayOffFile = string.format("%s/delay_off", t.ledDir)
t.ledDeviceNameFile = string.format("%s/device_name", t.ledDir)
t.ledLinkFile = string.format("%s/link", t.ledDir)
t.ledRxFile = string.format("%s/rx", t.ledDir)
t.ledTxFile = string.format("%s/tx", t.ledDir)
t.ledPrevState = {
brightness = self.readValue(t.ledBrightnessFile),
trigger = self.readValue(t.ledTriggerFile),
}
if t.ledPrevState.trigger then
local val = t.ledPrevState.trigger:match("%[[%w%-_]+%]")
if val then
t.ledPrevState.trigger = val:gsub("[%]%[]", "")
end
end
end
function Module:checkLed(t)
return (unistd.access(t.ledDir, "r") and
unistd.access(t.ledBrightnessFile, "rw") and
unistd.access(t.ledTriggerFile, "rw"))
end
function Module:init(t)
for i = 1, self.ledsPerInstance do
self._leds[i] = {}
end
if t.led1_name then
self._enabled = true
else
return
end
for i, l in ipairs(self._leds) do
local led = "led" .. i
if t[led .. "_name"] ~= nil then
l.ledName = t[led .. "_name"]
l.ledAction1 = tonumber(t[led .. "_action_1"]) or self.ledAction1Default
l.ledAction2 = tonumber(t[led .. "_action_2"]) or self.ledAction2Default
l.ledBlinkOnDelay1 = tonumber(t[led .. "_blink_on_delay_1"]) or self.ledBlinkDelayDefault
l.ledBlinkOffDelay1 = tonumber(t[led .. "_blink_off_delay_1"]) or self.ledBlinkDelayDefault
l.ledBlinkOnDelay2 = tonumber(t[led .. "_blink_on_delay_2"]) or self.ledBlinkDelayDefault
l.ledBlinkOffDelay2 = tonumber(t[led .. "_blink_off_delay_2"]) or self.ledBlinkDelayDefault
l.ledNetlinkDevice1 = t[led .. "_netdev_device_1"] or self.ledNetlinkDeviceDefault
l.ledNetlinkDevice2 = t[led .. "_netdev_device_2"] or self.ledNetlinkDeviceDefault
l.ledNetdevModeLink1 = self.ledNetdevModeLinkDefault
l.ledNetdevModeTx1 = self.ledNetdevModeTxDefault
l.ledNetdevModeRx1 = self.ledNetdevModeRxDefault
l.ledNetdevModeLink2 = self.ledNetdevModeLinkDefault
l.ledNetdevModeTx2 = self.ledNetdevModeTxDefault
l.ledNetdevModeRx2 = self.ledNetdevModeRxDefault
local ndm1 = t[led .. "_netdev_mode_1"]
if ndm1 ~= nil and type(ndm1) == "table" then
local enabledFlags = {}
for _, v in ipairs(ndm1) do
enabledFlags[v] = "1"
end
l.ledNetdevModeLink1 = enabledFlags.link or "0"
l.ledNetdevModeTx1 = enabledFlags.tx or "0"
l.ledNetdevModeRx1 = enabledFlags.rx or "0"
end
local ndm2 = t[led .. "_netdev_mode_2"]
if ndm2 ~= nil and type(ndm2) == "table" then
local enabledFlags = {}
for _, v in ipairs(ndm2) do
enabledFlags[v] = "1"
end
l.ledNetdevModeLink2 = enabledFlags.link or "0"
l.ledNetdevModeTx2 = enabledFlags.tx or "0"
l.ledNetdevModeRx2 = enabledFlags.rx or "0"
end
self:setLedAttrs(l)
l.enabled = true
else
l.enabled = false
end
if l.enabled and not self:checkLed(l) then
self._enabled = false
self.syslog("err", string.format(
"%s: module disabled. LED '%s' is not available", self.name, l.ledName))
end
self._exit = false
end
end
function Module:checkLedTimer(t)
return (unistd.access(t.ledDelayOnFile, "rw") and unistd.access(t.ledDelayOffFile, "rw"))
end
function Module:checkLedNetdev(t)
return (unistd.access(t.ledDeviceNameFile, "rw") and
unistd.access(t.ledLinkFile, "rw") and
unistd.access(t.ledRxFile, "rw") and
unistd.access(t.ledTxFile, "rw"))
end
function Module:setTriggerNone(t)
self.writeValue(t.ledTriggerFile, "none")
self.debugOutput(string.format(
"%s: LED TRIGGER SET: none, %s", self.name, t.ledTriggerFile))
end
function Module:setTriggerTimer(t, delayOn, delayOff)
if not delayOn then
delayOn = self.ledBlinkDelayDefault
end
if not delayOff then
delayOff = self.ledBlinkDelayDefault
end
self.writeValue(t.ledTriggerFile, "timer")
for i = 0, 10 do
if self:checkLedTimer(t) then
self.writeValue(t.ledDelayOnFile, delayOn)
self.writeValue(t.ledDelayOffFile, delayOff)
break
else
time.nanosleep({ tv_sec = 0, tv_nsec = 500000 })
end
end
self.debugOutput(string.format(
"%s: LED TRIGGER SET: timer, %s; delayOn = %s, delayOff = %s",
self.name, t.ledTriggerFile, tostring(delayOn), tostring(delayOff))
)
end
function Module:setTriggerNetdev(t, device, link, tx, rx)
if not device then
return
end
self.writeValue(t.ledTriggerFile, "netdev")
for i = 0, 10 do
if self:checkLedNetdev(t) then
self.writeValue(t.ledDeviceNameFile, device)
self.writeValue(t.ledLinkFile, link)
self.writeValue(t.ledTxFile, tx)
self.writeValue(t.ledRxFile, rx)
break
else
time.nanosleep({ tv_sec = 0, tv_nsec = 500000 })
end
end
self.debugOutput(string.format(
"%s: LED TRIGGER SET: netdev, %s; device = %s, link = %s, rx = %s, tx = %s",
self.name, t.ledTriggerFile, tostring(device), tostring(link), tostring(rx), tostring(tx))
)
end
function Module:getCurrentTrigger(t)
local trigger = self.readValue(t.ledTriggerFile)
if trigger then
if trigger:match("%[timer%]") then
return "timer"
elseif trigger:match("%[netdev%]") then
return "netdev"
end
end
end
function Module:getTriggerValues(t, trigger)
local currentTrigger = self:getCurrentTrigger(t)
if trigger == currentTrigger then
if trigger == "timer" then
return {
trigger = currentTrigger,
delayOn = tonumber(self.readValue(t.ledDelayOnFile)),
delayOff = tonumber(self.readValue(t.ledDelayOffFile)),
}
elseif trigger == "netdev" then
return {
trigger = currentTrigger,
device = self.readValue(t.ledDeviceNameFile),
link = self.readValue(t.ledLinkFile),
tx = self.readValue(t.ledTxFile),
rx = self.readValue(t.ledRxFile),
}
end
end
return {}
end
function Module:on(t)
self:setTriggerNone(t)
self.writeValue(t.ledBrightnessFile, t.ledMaxBrightness)
self.debugOutput(string.format("%s: LED ON: %s", self.name, t.ledBrightnessFile))
end
function Module:off(t)
self:setTriggerNone(t)
self.writeValue(t.ledBrightnessFile, "0")
self.debugOutput(string.format("%s: LED OFF: %s", self.name, t.ledBrightnessFile))
end
function Module:getCurrentState(t)
local state = self.readValue(t.ledBrightnessFile)
if state and tonumber(state) > 0 then
return tonumber(state)
end
end
function Module:ledRunFunc(t, currentStatus)
if currentStatus == 0 then
if t.ledAction1 == 1 then
if self:getCurrentState(t) or self:getCurrentTrigger(t) then
self:off(t)
end
elseif t.ledAction1 == 2 then
if not self:getCurrentState(t) or self:getCurrentTrigger(t) then
self:on(t)
end
elseif t.ledAction1 == 3 then
local triggerValues = self:getTriggerValues(t, "timer")
if (not next(triggerValues)) or (triggerValues.delayOn ~= t.ledBlinkOnDelay1 or
triggerValues.delayOff ~= t.ledBlinkOffDelay1) then
self:setTriggerTimer(t, t.ledBlinkOnDelay1, t.ledBlinkOffDelay1)
end
elseif t.ledAction1 == 4 then
local triggerValues = self:getTriggerValues(t, "netdev")
if (not next(triggerValues)) or (triggerValues.device ~= t.ledNetlinkDevice1 or
triggerValues.link ~= t.ledNetdevModeLink1 or
triggerValues.tx ~= t.ledNetdevModeTx1 or
triggerValues.rx ~= t.ledNetdevModeRx1) then
self:setTriggerNetdev(t,
t.ledNetlinkDevice1, t.ledNetdevModeLink1,
t.ledNetdevModeTx1, t.ledNetdevModeRx1
)
end
end
elseif currentStatus == 1 then
if t.ledAction2 == 1 then
if self:getCurrentState(t) or self:getCurrentTrigger(t) then
self:off(t)
end
elseif t.ledAction2 == 2 then
if not self:getCurrentState(t) or self:getCurrentTrigger(t) then
self:on(t)
end
elseif t.ledAction2 == 3 then
local triggerValues = self:getTriggerValues(t, "timer")
if (not next(triggerValues)) or (triggerValues.delayOn ~= t.ledBlinkOnDelay2 or
triggerValues.delayOff ~= t.ledBlinkOffDelay2) then
self:setTriggerTimer(t, t.ledBlinkOnDelay2, t.ledBlinkOffDelay2)
end
elseif t.ledAction2 == 4 then
local triggerValues = self:getTriggerValues(t, "netdev")
if (not next(triggerValues)) or (triggerValues.device ~= t.ledNetlinkDevice2 or
triggerValues.link ~= t.ledNetdevModeLink2 or
triggerValues.tx ~= t.ledNetdevModeTx2 or
triggerValues.rx ~= t.ledNetdevModeRx2) then
self:setTriggerNetdev(t,
t.ledNetlinkDevice2, t.ledNetdevModeLink2,
t.ledNetdevModeTx2, t.ledNetdevModeRx2
)
end
end
end
end
function Module:run(currentStatus, lastStatus, timeDiff, timeNow, inetChecked)
if not self._enabled then
return
end
if self._counter == 0 or self._counter >= self.runInterval or currentStatus ~= lastStatus then
for _, t in ipairs(self._leds) do
if self._exit then
break
end
if t.enabled then
self:ledRunFunc(t, currentStatus)
end
end
self._counter = 0
end
self._counter = self._counter + timeDiff
end
function Module:onExit()
self._exit = true
for _, l in ipairs(self._leds) do
if l.ledPrevState then
if l.ledPrevState.brightness then
self.writeValue(l.ledBrightnessFile, l.ledPrevState.brightness)
end
if l.ledPrevState.trigger then
self.writeValue(l.ledTriggerFile, l.ledPrevState.trigger)
end
end
end
end
return Module

View File

@@ -0,0 +1,160 @@
local unistd = require("posix.unistd")
local Module = {
name = "mod_network_restart",
runPrio = 30,
config = {},
syslog = function(level, msg) return true end,
debugOutput = function(msg) return true end,
writeValue = function(filePath, str) return false end,
readValue = function(filePath) return nil end,
deadPeriod = 900,
attempts = 1,
attemptInterval = 15,
deviceTimeout = 0,
status = nil,
_attemptsCounter = 0,
_attemptIntervalCounter = 0,
_deadCounter = 0,
_firstAttempt = true,
_ifaceRestarting = false,
_ifaceRestartCounter = 0,
_netIfaces = {},
_netDevices = {},
_netItemsNum = 0,
_disconnectedAtStartup = false,
}
function Module:toggleDevices(flag)
if #self._netDevices == 0 then
return
end
local ip = "/sbin/ip"
if unistd.access(ip, "x") then
for _, v in ipairs(self._netDevices) do
os.execute(string.format("%s link set dev %s %s", ip, v, (flag and "up" or "down")))
end
end
end
function Module:toggleIfaces(flag)
if #self._netIfaces == 0 then
return
end
for _, v in ipairs(self._netIfaces) do
os.execute(string.format("%s %s", (flag and "/sbin/ifup" or "/sbin/ifdown"), v))
end
end
function Module:netItemsUp()
self:toggleDevices(true)
self:toggleIfaces(true)
end
function Module:netItemsDown()
self:toggleIfaces(false)
self:toggleDevices(false)
end
function Module:restartNetworkService()
return os.execute("/etc/init.d/network restart")
end
function Module:init(t)
if t.ifaces ~= nil and type(t.ifaces) == "table" then
self._netIfaces = {}
self._netDevices = {}
self._netItemsNum = 0
for k, v in ipairs(t.ifaces) do
if v:match("^@") then
self._netIfaces[#self._netIfaces + 1] = v:gsub("^@", "")
else
self._netDevices[#self._netDevices + 1] = v
end
self._netItemsNum = self._netItemsNum + 1
end
end
if t.dead_period ~= nil then
self.deadPeriod = tonumber(t.dead_period)
end
if t.attempts ~= nil then
self.attempts = tonumber(t.attempts)
end
if t.attempt_interval ~= nil then
self.attemptInterval = tonumber(t.attempt_interval)
end
if t.device_timeout ~= nil then
self.deviceTimeout = tonumber(t.device_timeout)
end
if tonumber(t.disconnected_at_startup) == 1 then
self._disconnectedAtStartup = true
end
end
function Module:networkRestartFunc()
if self._netItemsNum > 0 then
if #self._netIfaces > 0 then
self.syslog("info", string.format("%s: restarting interfaces: %s",
self.name, table.concat(self._netIfaces, ", ")))
end
if #self._netDevices > 0 then
self.syslog("info", string.format("%s: restarting devices: %s",
self.name, table.concat(self._netDevices, ", ")))
end
self:netItemsDown()
if self.deviceTimeout < 1 then
self:netItemsUp()
else
self._ifaceRestarting = true
end
else
self.syslog("info", string.format(
"%s: restarting network", self.name))
self:restartNetworkService()
end
if self.attempts > 0 then
self._attemptsCounter = self._attemptsCounter + 1
end
end
function Module:run(currentStatus, lastStatus, timeDiff, timeNow, inetChecked)
if self._ifaceRestarting then
if self._ifaceRestartCounter >= self.deviceTimeout then
self:netItemsUp()
self._ifaceRestarting = false
self._ifaceRestartCounter = 0
else
self._ifaceRestartCounter = self._ifaceRestartCounter + timeDiff
end
else
if currentStatus == 1 then
if self._disconnectedAtStartup and self._deadCounter >= self.deadPeriod then
if self.attempts == 0 or self._attemptsCounter < self.attempts then
if self._firstAttempt or self._attemptIntervalCounter >= self.attemptInterval then
self:networkRestartFunc()
self._attemptIntervalCounter = 0
self._firstAttempt = false
else
self._attemptIntervalCounter = self._attemptIntervalCounter + timeDiff
end
end
else
self._deadCounter = self._deadCounter + timeDiff
end
else
self._attemptsCounter = 0
self._attemptIntervalCounter = 0
self._deadCounter = 0
self._disconnectedAtStartup = true
self._firstAttempt = true
end
self._ifaceRestartCounter = 0
end
end
function Module:onExit()
return true
end
return Module

View File

@@ -0,0 +1,533 @@
local socket = require("posix.sys.socket")
local stdlib = require("posix.stdlib")
local unistd = require("posix.unistd")
local Module = {
name = "mod_public_ip",
runPrio = 50,
config = {
noModules = false,
debug = false,
serviceConfig = {
iface = nil,
proxy_type = nil,
proxy_host = nil,
proxy_port = nil,
},
},
syslog = function(level, msg) return true end,
debugOutput = function(msg) return true end,
writeValue = function(filePath, str) return false end,
readValue = function(filePath) return nil end,
port = 53,
runInterval = 600,
runIntervalFailed = 60,
runIntervalIPFailed = 1,
requestAttempts = 2,
timeout = 3,
curlExec = "/usr/bin/curl",
curlParams = '-s --no-keepalive --user-agent "Mozilla/5.0 (X11; Linux x86_64; rv:142.0) Gecko/20100101 Firefox/142.0"',
providers = {
opendns1 = {
name = "opendns1", type = "dns", host = "myip.opendns.com",
server = "208.67.222.222", server6 = "2620:119:35::35",
port = 53, queryType = "A", queryType6 = "AAAA",
},
opendns2 = {
name = "opendns2", type = "dns", host = "myip.opendns.com",
server = "208.67.220.220", server6 = "2620:119:35::35",
port = 53, queryType = "A", queryType6 = "AAAA",
},
opendns3 = {
name = "opendns3", type = "dns", host = "myip.opendns.com",
server = "208.67.222.220", server6 = "2620:119:35::35",
port = 53, queryType = "A", queryType6 = "AAAA",
},
opendns4 = {
name = "opendns4", type = "dns", host = "myip.opendns.com",
server = "208.67.220.222", server6 = "2620:119:35::35",
port = 53, queryType = "A", queryType6 = "AAAA",
},
google = {
name = "google", type = "dns", host = "o-o.myaddr.l.google.com",
server = "ns1.google.com", server6 = "ns1.google.com",
port = 53, queryType = "TXT", queryType6 = "TXT",
},
akamai = {
name = "akamai", type = "dns", host = "whoami.akamai.net",
server = "ns1-1.akamaitech.net", server6 = "ns1-1.akamaitech.net",
port = 53, queryType = "A", queryType6 = "AAAA",
},
akamai_http = {
name = "akamai_http", type = "http", url = "http://whatismyip.akamai.com/",
parseResponseFunc = nil,
},
amazonaws= {
name = "amazonaws", type = "http", url = "http://checkip.amazonaws.com/",
parseResponseFunc = nil,
},
wgetip= {
name = "wgetip", type = "http", url = "http://wgetip.com/",
parseResponseFunc = nil,
},
ifconfig= {
name = "ifconfig", type = "http", url = "http://ifconfig.me/",
parseResponseFunc = nil,
},
ipecho= {
name = "ipecho", type = "http", url = "http://ipecho.net/plain",
parseResponseFunc = nil,
},
canhazip= {
name = "canhazip", type = "http", url = "http://canhazip.com/",
parseResponseFunc = nil,
},
icanhazip = {
name = "icanhazip", type = "http", url = "http://icanhazip.com/",
parseResponseFunc = nil,
},
},
ipScript = "",
enableIpScript = false,
status = nil,
_proxyString = "",
_provider = nil,
_qtype = false,
_currentIp = nil,
_lastResolvedIp = nil,
_enabled = false,
_counter = 0,
_IPFalseCounter = 0,
_interval = 600,
_DNSPacket = nil,
_requestIP = nil,
}
function Module:runIpScript()
if not self.config.noModules and self.enableIpScript and unistd.access(self.ipScript, "r") then
stdlib.setenv("PUBLIC_IP", self.status)
os.execute(string.format('/bin/sh "%s" &', self.ipScript))
end
end
function Module:getQueryType(type)
local types = {
A = 1,
NS = 2,
MD = 3,
MF = 4,
CNAME = 5,
SOA = 6,
MB = 7,
MG = 8,
MR = 9,
NULL = 10,
WKS = 11,
PTS = 12,
HINFO = 13,
MINFO = 14,
MX = 15,
TXT = 16,
AAAA = 28,
}
return types[type]
end
function Module:buildMessage(address, queryType)
if not queryType then
queryType = "A"
end
queryType = self:getQueryType(queryType)
local addressString = ""
for part in address:gmatch("[^.]+") do
local t = {}
for i in part:gmatch(".") do
t[#t + 1] = i
end
addrLen = #part
addrPart = table.concat(t)
addressString = addressString .. string.char(addrLen) .. addrPart
end
local data = (
string.char(
0xaa, 0xaa,
0x01, 0x00,
0x00, 0x01, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00
) ..
addressString ..
string.char(
0x00,
0x00, queryType,
0x00, 0x01
)
)
return data
end
function Module:sendUDPMessage(message, server, port)
local success
local retCode = 1
local data
self.debugOutput(string.format("--- %s ---", self.name))
local saTable, errMsg, errNum = socket.getaddrinfo(server, port)
if not saTable then
self.debugOutput(string.format(
"GETADDRINFO ERROR: %s, %s", errMsg, errNum))
else
local family = saTable[1].family
if family then
local sock, errMsg, errNum = socket.socket(family, socket.SOCK_DGRAM, 0)
if not sock then
self.debugOutput(string.format(
"SOCKET ERROR: %s, %s", errMsg, errNum))
return retCode
end
socket.setsockopt(sock, socket.SOL_SOCKET,
socket.SO_SNDTIMEO, self.timeout, 0)
socket.setsockopt(sock, socket.SOL_SOCKET,
socket.SO_RCVTIMEO, self.timeout, 0)
if self.config.serviceConfig.iface then
local ok, errMsg, errNum = socket.setsockopt(sock, socket.SOL_SOCKET,
socket.SO_BINDTODEVICE, self.config.serviceConfig.iface)
if not ok then
self.debugOutput(string.format(
"SOCKET ERROR: %s, %s", errMsg, errNum))
unistd.close(sock)
return retCode
end
end
local ok, errMsg, errNum = socket.sendto(sock, message, saTable[1])
local response = {}
if ok then
local ret, resp, errNum = socket.recvfrom(sock, 1024)
data = ret
if data then
success = true
response = resp
else
self.debugOutput(string.format(
"SOCKET RECV ERROR: %s, %s", tostring(resp), tostring(errNum)))
end
else
self.debugOutput(string.format(
"SOCKET SEND ERROR: %s, %s", tostring(errMsg), tostring(errNum)))
end
self.debugOutput(string.format(
"--- UDP ---\ntime = %s\nconnection_timeout = %s\niface = %s\nserver = %s:%s\nsockname = %s:%s\nsuccess = %s",
os.time(),
self.timeout,
tostring(self.config.serviceConfig.iface),
server,
tostring(port),
tostring(response.addr),
tostring(response.port),
tostring(success))
)
unistd.close(sock)
retCode = success and 0 or 1
end
end
return retCode, tostring(data)
end
function Module:parseParts(message, start, parts)
local partStart = start + 2
local partLen = message:sub(start, start + 1)
if #partLen == 0 then
return parts
end
local partEnd = partStart + (tonumber(partLen, 16) * 2)
parts[#parts + 1] = message:sub(partStart, partEnd - 1)
if message:sub(partEnd, partEnd + 1) == "00" or partEnd > #message then
return parts
else
return self:parseParts(message, partEnd, parts)
end
end
function Module:decodeMessage(message)
local retTable = {}
local t = {}
for i = 1, #message do
t[#t + 1] = string.format("%.2x", string.byte(message, i))
end
message = table.concat(t)
local ANCOUNT = message:sub(13, 16)
local NSCOUNT = message:sub(17, 20)
local ARCOUNT = message:sub(21, 24)
-- Question section
local questionSectionStarts = 25
local questionParts = self:parseParts(message, questionSectionStarts, {})
local qtypeStarts = questionSectionStarts + (#table.concat(questionParts)) + (#questionParts * 2) + 1
local qclassStarts = qtypeStarts + 4
-- Answer section
local answerSectionStarts = qclassStarts + 4
local numAnswers = math.max(
tonumber(ANCOUNT, 16), tonumber(NSCOUNT, 16), tonumber(ARCOUNT, 16))
if numAnswers > 0 then
for answerCount = 1, numAnswers do
if answerSectionStarts < #message then
local ATYPE = tonumber(
message:sub(answerSectionStarts + 5, answerSectionStarts + 8), 16)
local RDLENGTH = tonumber(
message:sub(answerSectionStarts + 21, answerSectionStarts + 24), 16)
local RDDATA = message:sub(
answerSectionStarts + 25, answerSectionStarts + 24 + (RDLENGTH * 2))
local RDDATA_decoded = ""
if #RDDATA > 0 then
if ATYPE == self:getQueryType("A") or ATYPE == self:getQueryType("AAAA") then
local octets = {}
local sep = "."
if #RDDATA > 8 then
sep = ":"
for i = 1, #RDDATA, 4 do
local string = RDDATA:sub(i, i + 3)
string = string:gsub("^00?0?", "")
octets[#octets + 1] = string
end
else
for i = 1, #RDDATA, 2 do
octets[#octets + 1] = tonumber(RDDATA:sub(i, i + 1), 16)
end
end
RDDATA_decoded = table.concat(octets, sep):gsub("0:[0:]+", "::", 1):gsub("::+", "::")
else
local rdata_t = {}
for _, v in ipairs(self:parseParts(RDDATA, 1, {})) do
local t = {}
for i = 1, #v, 2 do
t[#t + 1] = string.char(tonumber(v:sub(i, i + 1), 16))
end
rdata_t[#rdata_t + 1] = table.concat(t)
end
RDDATA_decoded = table.concat(rdata_t)
end
end
answerSectionStarts = answerSectionStarts + 24 + (RDLENGTH * 2)
if RDDATA_decoded:match("^[a-fA-F0-9.:]+$") then
retTable[#retTable + 1] = RDDATA_decoded
end
end
end
end
return retTable
end
function Module:requestIPDNS()
local res
local qtype = self._qtype and self._provider.queryType6 or self._provider.queryType
local server = self._qtype and self._provider.server6 or self._provider.server
local port = self._provider.port or self.port
if not self._DNSPacket then
self._DNSPacket = self:buildMessage(self._provider.host, qtype)
end
local retCode, response = self:sendUDPMessage(self._DNSPacket, server, port)
if retCode == 0 and response then
local retTable = self:decodeMessage(response)
if #retTable > 0 then
res = table.concat(retTable, ", ")
end
else
self.syslog("warning", string.format(
"%s: UDP error when requesting an IP address", self.name))
end
return res
end
function Module:httpRequest(url)
local retCode = 1, data
local curl = string.format(
'%s%s%s --connect-timeout %s %s "%s"; printf "\n$?";',
self.curlExec,
self.config.serviceConfig.iface and (" --interface " .. self.config.serviceConfig.iface) or "",
self._proxyString,
self.timeout,
self.curlParams,
url
)
local fh = io.popen(curl, "r")
if fh then
data = fh:read("*a")
fh:close()
if data ~= nil then
local s, e = data:find("[0-9]+\n?$")
retCode = tonumber(data:sub(s))
data = data:sub(0, s - 2)
if not data or data == "" then
data = nil
end
end
else
retCode = 1
end
self.debugOutput(string.format(
"--- Curl ---\ntime = %s\n%s\nretCode = %s\ndata = [\n%s]\n",
os.time(),
curl,
retCode,
tostring(data))
)
return retCode, data
end
function Module:parseHTTPResponse(data)
data = data:gsub("^[%s%c]+", ""):gsub("[%s%c]+$", "")
if data:match("^[a-fA-F0-9.:]+$") then
return data
end
return
end
function Module:requestIPHTTP()
local res
local url = self._provider.url
local parseResponseFunc = self._provider.parseResponseFunc
if url then
local retCode, data = self:httpRequest(url)
if retCode == 0 and data then
if type(parseResponseFunc) == "function" then
res = parseResponseFunc(data)
else
res = self:parseHTTPResponse(data)
end
else
self.syslog("warning", string.format(
"%s: HTTP error when requesting an IP address", self.name))
end
end
return res
end
function Module:init(t)
if t.interval ~= nil then
self.runInterval = tonumber(t.interval)
end
if t.interval_failed ~= nil then
self.runIntervalFailed = tonumber(t.interval_failed)
end
if t.request_attempts ~= nil then
self.requestAttempts = tonumber(t.request_attempts)
end
if t.timeout ~= nil then
self.timeout = tonumber(t.timeout)
end
if t.provider ~= nil then
self._provider = self.providers[t.provider]
else
self._provider = self.providers.opendns1
end
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 ~= nil then
self.enableIpScript = (tonumber(t.enable_ip_script) ~= 0)
end
end
if t.qtype ~= nil then
self._qtype = (tonumber(t.qtype) ~= 0)
end
self._currentIp = nil
self._lastResolvedIp = nil
self._DNSPacket = nil
self._interval = self.runInterval
self._IPFalseCounter = 0
self._enabled = true
if not self._provider then
self._enabled = false
else
if self._provider.url and not unistd.access(self.curlExec, "x") then
self._enabled = false
self.syslog("err", string.format(
"%s: %s is not available. You need to install curl.", self.name, self.curlExec))
end
if self._provider.type == "dns" then
self._requestIP = self.requestIPDNS
elseif self._provider.type == "http" then
self._requestIP = self.requestIPHTTP
else
self._enabled = false
end
end
if (self.config.serviceConfig.proxy_type and
self.config.serviceConfig.proxy_host and
self.config.serviceConfig.proxy_port) then
self._proxyString = string.format(
" --proxy %s://%s:%d",
self.config.serviceConfig.proxy_type,
self.config.serviceConfig.proxy_host,
self.config.serviceConfig.proxy_port)
end
end
function Module:run(currentStatus, lastStatus, timeDiff, timeNow, inetChecked)
if not self._enabled then
return
end
if currentStatus == 0 then
if self._counter == 0 or self._counter >= self._interval or currentStatus ~= lastStatus then
local ip = self:_requestIP()
if not ip then
ip = ""
self._IPFalseCounter = self._IPFalseCounter + 1
if self._IPFalseCounter >= self.requestAttempts then
self._interval = self.runIntervalFailed
self._IPFalseCounter = 0
else
self._interval = self.runIntervalIPFailed
end
else
self._interval = self.runInterval
self._IPFalseCounter = 0
end
if ip ~= self._currentIp then
self.status = ip
if ip ~= "" then
if self._counter > 0 and ip ~= self._lastResolvedIp then
self.syslog(
"notice",
string.format("%s: public IP address changed to %s", self.name, ip)
)
self:runIpScript()
end
self._lastResolvedIp = ip
end
end
self._currentIp = ip
self._counter = 0
end
else
self._currentIp = nil
self.status = self._currentIp
self._IPFalseCounter = 0
self._counter = 0
self._interval = self.runInterval
end
self._counter = self._counter + timeDiff
end
function Module:onExit()
return true
end
return Module

View File

@@ -0,0 +1,63 @@
local unistd = require("posix.unistd")
local Module = {
name = "mod_reboot",
runPrio = 20,
config = {},
syslog = function(level, msg) return true end,
debugOutput = function(msg) return true end,
writeValue = function(filePath, str) return false end,
readValue = function(filePath) return nil end,
deadPeriod = 3600,
forceRebootDelay = 300,
antiBootloopDelay = 300,
status = nil,
_deadCounter = 0,
_rebooted = true,
}
function Module:rebootDevice()
self.syslog("warning", string.format("%s: reboot", self.name))
os.execute("/sbin/reboot &")
if self.forceRebootDelay > 0 then
unistd.sleep(self.forceRebootDelay)
self.syslog("warning", string.format("%s: force reboot", self.name))
self.writeValue("/proc/sys/kernel/sysrq", "1")
self.writeValue("/proc/sysrq-trigger", "b")
end
end
function Module:init(t)
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
if tonumber(t.disconnected_at_startup) == 1 then
self._rebooted = false
end
end
function Module:run(currentStatus, lastStatus, timeDiff, timeNow, inetChecked)
if currentStatus == 1 then
if not self._rebooted then
if timeNow >= self.antiBootloopDelay and self._deadCounter >= self.deadPeriod then
self:rebootDevice()
self._rebooted = true
else
self._deadCounter = self._deadCounter + timeDiff
end
end
else
self._deadCounter = 0
self._rebooted = false
end
end
function Module:onExit()
return true
end
return Module

View File

@@ -0,0 +1,67 @@
local stdlib = require("posix.stdlib")
local time = require("posix.time")
local unistd = require("posix.unistd")
local Module = {
name = "mod_regular_script",
runPrio = 90,
config = {},
syslog = function(level, msg) return true end,
debugOutput = function(msg) return true end,
writeValue = function(filePath, str) return false end,
readValue = function(filePath) return nil end,
inetState = 2, -- 0: connected, 1: disconnected, 2: both
runInterval = 3600,
script = "",
status = nil,
_nextTime = nil,
_firstRun = true,
}
function Module:runExternalScript(scriptPath, currentStatus)
if unistd.access(scriptPath, "r") then
stdlib.setenv("INET_STATE", currentStatus)
os.execute(string.format('/bin/sh "%s" &', scriptPath))
end
end
function Module:init(t)
if t.inet_state ~= nil then
self.inetState = tonumber(t.inet_state)
end
if t.interval ~= nil then
self.runInterval = tonumber(t.interval)
end
if self.config.configDir then
self.script = string.format(
"%s/regular-script.%s", self.config.configDir, self.config.serviceConfig.instance)
end
end
function Module:run(currentStatus, lastStatus, timeDiff, timeNow, inetChecked)
if not self._nextTime then
if timeNow < self.runInterval then
self._nextTime = self.runInterval
else
self._nextTime = timeNow - (timeNow % self.runInterval) + self.runInterval
end
end
if self._firstRun then
self.status = time.strftime ("%Y-%m-%d %H:%M:%S %z", time.localtime(time.time() + self._nextTime - timeNow))
self._firstRun = false
end
if timeNow >= self._nextTime then
self._nextTime = self._nextTime + self.runInterval
if self.inetState == 2 or (self.inetState == 0 and currentStatus == 0) or (self.inetState == 1 and currentStatus == 1) then
self.status = time.strftime ("%Y-%m-%d %H:%M:%S %z", time.localtime(time.time() + self._nextTime - timeNow))
self:runExternalScript(self.script, currentStatus)
end
end
end
function Module:onExit()
return true
end
return Module

View File

@@ -0,0 +1,134 @@
local unistd = require("posix.unistd")
local Module = {
name = "mod_user_scripts",
runPrio = 80,
config = {},
syslog = function(level, msg) return true end,
debugOutput = function(msg) return true end,
writeValue = function(filePath, str) return false end,
readValue = function(filePath) return nil end,
deadPeriod = 0,
alivePeriod = 0,
upScript = "",
downScript = "",
upScriptAttempts = 1,
upScriptAttemptInterval = 15,
downScriptAttempts = 1,
downScriptAttemptInterval = 15,
status = nil,
_deadCounter = 0,
_aliveCounter = 0,
_upScriptAttemptsCounter = 0,
_upScriptAttemptIntervalCounter = 0,
_downScriptAttemptsCounter = 0,
_downScriptAttemptIntervalCounter = 0,
_upScriptFirstAttempt = true,
_downScriptFirstAttempt = true,
_disconnectedAtStartup = false,
_connectedAtStartup = false,
}
function Module:runExternalScript(scriptPath)
if unistd.access(scriptPath, "r") then
os.execute(string.format('/bin/sh "%s" &', scriptPath))
end
end
function Module:init(t)
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.up_script_attempts ~= nil then
self.upScriptAttempts = tonumber(t.up_script_attempts)
end
if t.up_script_attempt_interval ~= nil then
self.upScriptAttemptInterval = tonumber(t.up_script_attempt_interval)
end
if t.down_script_attempts ~= nil then
self.downScriptAttempts = tonumber(t.down_script_attempts)
end
if t.down_script_attempt_interval ~= nil then
self.downScriptAttemptInterval = tonumber(t.down_script_attempt_interval)
end
if self.config.configDir then
self.upScript = string.format(
"%s/up-script.%s", self.config.configDir, self.config.serviceConfig.instance)
self.downScript = string.format(
"%s/down-script.%s", self.config.configDir, self.config.serviceConfig.instance)
end
if tonumber(t.connected_at_startup) == 1 then
self._connectedAtStartup = true
end
if tonumber(t.disconnected_at_startup) == 1 then
self._disconnectedAtStartup = true
end
end
function Module:runUpScriptFunc()
self:runExternalScript(self.upScript)
if self.upScriptAttempts > 0 then
self._upScriptAttemptsCounter = self._upScriptAttemptsCounter + 1
end
end
function Module:runDownScriptFunc()
self:runExternalScript(self.downScript)
if self.downScriptAttempts > 0 then
self._downScriptAttemptsCounter = self._downScriptAttemptsCounter + 1
end
end
function Module:run(currentStatus, lastStatus, timeDiff, timeNow, inetChecked)
if currentStatus == 1 then
self._upScriptAttemptsCounter = 0
self._upScriptAttemptIntervalCounter = 0
self._aliveCounter = 0
self._connectedAtStartup = true
self._upScriptFirstAttempt = true
if self._disconnectedAtStartup and self._deadCounter >= self.deadPeriod then
if self.downScriptAttempts == 0 or self._downScriptAttemptsCounter < self.downScriptAttempts then
if self._downScriptFirstAttempt or self._downScriptAttemptIntervalCounter >= self.downScriptAttemptInterval then
self:runDownScriptFunc()
self._downScriptAttemptIntervalCounter = 0
self._downScriptFirstAttempt = false
else
self._downScriptAttemptIntervalCounter = self._downScriptAttemptIntervalCounter + timeDiff
end
end
else
self._deadCounter = self._deadCounter + timeDiff
end
elseif currentStatus == 0 then
self._downScriptAttemptsCounter = 0
self._downScriptAttemptIntervalCounter = 0
self._deadCounter = 0
self._disconnectedAtStartup = true
self._downScriptFirstAttempt = true
if self._connectedAtStartup and self._aliveCounter >= self.alivePeriod then
if self.upScriptAttempts == 0 or self._upScriptAttemptsCounter < self.upScriptAttempts then
if self._upScriptFirstAttempt or self._upScriptAttemptIntervalCounter >= self.upScriptAttemptInterval then
self:runUpScriptFunc()
self._upScriptAttemptIntervalCounter = 0
self._upScriptFirstAttempt = false
else
self._upScriptAttemptIntervalCounter = self._upScriptAttemptIntervalCounter + timeDiff
end
end
else
self._aliveCounter = self._aliveCounter + timeDiff
end
end
end
function Module:onExit()
return true
end
return Module

View File

@@ -1,10 +1,12 @@
#
# (с) 2021 gSpot (https://github.com/gSpotx2f/luci-app-internet-detector)
# (с) 2025 gSpot (https://github.com/gSpotx2f/luci-app-internet-detector)
#
include $(TOPDIR)/rules.mk
PKG_VERSION:=0.6-1
PKG_NAME:=luci-app-internet-detector
PKG_VERSION:=1.7.0
PKG_RELEASE:=1
LUCI_TITLE:=LuCI support for internet-detector
LUCI_DEPENDS:=+internet-detector
LUCI_PKGARCH:=all

View File

@@ -1,118 +1,150 @@
'use strict';
'require baseclass';
'require fs';
'require rpc';
'require uci';
document.head.append(E('style', {'type': 'text/css'},
`
:root {
--app-id-font-color: #fff;
--app-id-connected-color: #2ea256;
--app-id-disconnected-color: #ff4e54;
--app-id-undefined-color: #8a8a8a;
--app-id-font-color: #454545;
--app-id-font-shadow: #fff;
--app-id-connected-color: #6bdebb;
--app-id-disconnected-color: #f8aeba;
--app-id-undefined-color: #dfdfdf;
}
:root[data-darkmode="true"] {
--app-id-font-color: #f6f6f6;
--app-id-font-shadow: #4d4d4d;
--app-id-connected-color: #005F20;
--app-id-disconnected-color: #a93734;
--app-id-undefined-color: #4d4d4d;
}
.id-connected {
--on-color: var(--app-id-font-color);
background-color: var(--app-id-connected-color) !important;
border-color: var(--app-id-connected-color) !important;
color: var(--app-id-font-color) !important;
text-shadow: 0 1px 1px var(--app-id-font-shadow);
}
.id-disconnected {
--on-color: var(--app-id-font-color);
background-color: var(--app-id-disconnected-color) !important;
border-color: var(--app-id-disconnected-color) !important;
color: var(--app-id-font-color) !important;
text-shadow: 0 1px 1px var(--app-id-font-shadow);
}
.id-undefined {
--on-color: var(--app-id-font-color);
background-color: var(--app-id-undefined-color) !important;
border-color: var(--app-id-undefined-color) !important;
color: var(--app-id-font-color) !important;
text-shadow: 0 1px 1px var(--app-id-font-shadow);
}
.id-label-status {
display: inline-block;
word-wrap: break-word;
margin: 2px !important;
padding: 4px 8px;
border: 1px solid;
-webkit-border-radius: 4px;
-moz-border-radius: 4px;
border-radius: 4px;
font-weight: bold;
box-shadow: 0 2px 4px rgba(0, 0, 0, 0.2);
}
`));
return baseclass.extend({
title : _('Internet'),
appName : 'internet-detector',
execPath : '/usr/bin/internet-detector',
inetStatus : null,
publicIp : null,
title : _('Internet'),
appName : 'internet-detector',
currentAppMode : null,
inetStatus : null,
inetStatusFromJson: function(res) {
let curInetStatus = null;
let curPubIp = null;
if(res.code === 0) {
try {
let json = JSON.parse(res.stdout.trim());
curInetStatus = json.inet;
curPubIp = json.mod_public_ip;
} catch(e) {};
};
return [ curInetStatus, curPubIp ];
callUIPoll: rpc.declare({
object: 'luci.internet-detector',
method: 'UIPoll',
expect: { '': {} }
}),
getUIPoll() {
return this.callUIPoll().then(data => {
return data;
});
},
load: async function() {
if(!(
'uiCheckIntervalUp' in window &&
'uiCheckIntervalDown' in window &&
'currentAppMode' in window
)) {
callInetStatus: rpc.declare({
object: 'luci.internet-detector',
method: 'InetStatus',
expect: { '': {} }
}),
getInetStatus() {
return this.callInetStatus().then(data => {
return data;
});
},
async load() {
if(!this.currentAppMode) {
await uci.load(this.appName).then(data => {
window.uiCheckIntervalUp = Number(uci.get(this.appName, 'config', 'ui_interval_up'));
window.uiCheckIntervalDown = Number(uci.get(this.appName, 'config', 'ui_interval_down'));
window.currentAppMode = uci.get(this.appName, 'config', 'mode');
this.currentAppMode = uci.get(this.appName, 'config', 'mode');
}).catch(e => {});
};
if(window.currentAppMode === '1' || window.currentAppMode === '2') {
window.internetDetectorCounter = ('internetDetectorCounter' in window) ?
++window.internetDetectorCounter : 0;
if(!('internetDetectorState' in window)) {
window.internetDetectorState = 2;
};
if(window.currentAppMode === '1' && (
(window.internetDetectorState === 0 && window.internetDetectorCounter % window.uiCheckIntervalUp) ||
(window.internetDetectorState === 1 && window.internetDetectorCounter % window.uiCheckIntervalDown)
)) {
return;
};
window.internetDetectorCounter = 0;
return L.resolveDefault(fs.exec(this.execPath, [ 'inet-status-json' ]), null);
if(this.currentAppMode == '2') {
return this.getUIPoll();
}
else {
window.internetDetectorState = 2;
else if(this.currentAppMode == '1') {
return L.resolveDefault(this.getInetStatus(), null);
};
},
render: function(data) {
if(window.currentAppMode === '0') {
return
};
if(data) {
[ window.internetDetectorState, this.publicIp ] = this.inetStatusFromJson(data);
};
let internetStatus = E('span', { 'class': 'label' });
if(window.internetDetectorState === 0) {
internetStatus.textContent = _('Connected') + (this.publicIp ? ' | %s: %s'.format(_('Public IP'), _(this.publicIp)) : '');
internetStatus.className = "label id-connected";
render(data) {
if(this.currentAppMode == '0') {
return;
}
else if(window.internetDetectorState === 1) {
internetStatus.textContent = _('Disconnected');
internetStatus.className = "label id-disconnected";
}
else {
internetStatus.textContent = _('Undefined');
internetStatus.className = "label id-undefined";
this.inetStatus = data;
let inetStatusArea = E('div', {});
if(!this.inetStatus || !this.inetStatus.instances || this.inetStatus.instances.length == 0) {
let label = E('span', { 'class': 'id-label-status id-undefined' }, _('Undefined'));
if(this.currentAppMode == '2') {
label.classList.add('spinning');
};
inetStatusArea.append(label);
} else {
this.inetStatus.instances.sort((a, b) => a.num - b.num);
for(let i of this.inetStatus.instances) {
let status = _('Disconnected');
let className = 'id-label-status id-disconnected';
if(i.inet == 0) {
status = _('Connected');
className = 'id-label-status id-connected';
}
else if(i.inet == -1) {
status = _('Undefined');
className = 'id-label-status id-undefined spinning';
};
let publicIp = (i.mod_public_ip !== undefined) ?
' | %s: %s'.format(_('Public IP'), (i.mod_public_ip == '') ? _('Undefined') : _(i.mod_public_ip))
: '';
inetStatusArea.append(
E('span', { 'class': className }, '%s%s%s'.format(
i.instance + ': ', status, publicIp)
)
);
};
};
return E('div', {
'class': 'cbi-section',
'style': 'margin-bottom:1em',
}, internetStatus);
}, inetStatusArea);
},
});

View File

@@ -20,9 +20,25 @@ msgid "<abbr title=\"Light Emitting Diode\">LED</abbr> control"
msgstr "Управление <abbr title=\"Светодиод\">LED</abbr>"
msgid ""
"<abbr title=\"Light Emitting Diode\">LED</abbr> is on when Internet is "
"available."
msgstr "<abbr title=\"Светодиод\">LED</abbr> включен если Интернет доступен."
"<abbr title=\"Light Emitting Diode\">LED</abbr> indicates the Internet status."
msgstr "<abbr title=\"Светодиод\">LED</abbr> отображает статус Интернет."
msgid "<abbr title=\"Light Emitting Diode\">LED</abbr> mode"
msgstr "Режим <abbr title=\"Светодиод\">LED</abbr>"
msgid ""
"<abbr title=\"Light Emitting Diode\">LED</abbr> will display the link activity of this network device."
msgstr ""
"<abbr title=\"Светодиод\">LED</abbr> будет отображать активность линка этого сетевого устройства."
msgid "Add instance"
msgstr "Добавить экземпляр"
msgid "After connection"
msgstr "После подключения"
msgid "After disconnection"
msgstr "После отключения"
msgid "Alive interval"
msgstr "Интервал при подключении"
@@ -30,17 +46,33 @@ 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 "Сообщение будет отправлено на email при подключении или отключении от Интернет."
msgid "An error has occurred"
msgstr "Произошла ошибка"
msgid "Attempt interval"
msgstr "Интервал попыток"
msgid "Attempts"
msgstr "Попытки"
msgid "Big: 248 bytes"
msgstr "Большой: 248 байт"
msgid "Blinking (kernel: timer)"
msgstr "Мигание (kernel: timer)"
msgid "Bot API token is missing!"
msgstr "Отсутствует API токен бота!"
msgid "Bot token"
msgstr "Токен бота"
msgid "ID чата"
msgstr ""
msgid "Check type"
msgstr "Тип проверки"
@@ -65,15 +97,27 @@ msgstr "Таймаут соединения"
msgid "Contents have been saved."
msgstr "Содержимое сохранено."
msgid "Curl is not available..."
msgstr "Curl недоступен..."
msgid "Dead interval"
msgstr "Интервал при отключении"
msgid "Dead period"
msgstr "Период после отключения"
msgid "Debug"
msgstr "Отладка"
msgid "Default port value for TCP connections."
msgstr "Стандартное значение порта для TCP-подключений."
msgid "Description"
msgstr "Описание"
msgid "Device"
msgstr "Устройство"
msgid "Device will be rebooted when the Internet is disconnected."
msgstr "Устройство будет перезагружено при отключении Интернет."
@@ -92,8 +136,8 @@ msgstr "Отключен"
msgid "Dismiss"
msgstr "Закрыть"
msgid "DNS provider"
msgstr "DNS провайдер"
msgid "DNS query type"
msgstr "Тип DNS-запроса"
msgid "Edit"
msgstr "Изменить"
@@ -101,6 +145,12 @@ msgstr "Изменить"
msgid "Edit down-script"
msgstr "Изменить down-script"
msgid "Edit regular-script"
msgstr "Изменить regular-script"
msgid "Edit public-ip-script"
msgstr "Изменить public-ip-script"
msgid "Edit up-script"
msgstr "Изменить up-script"
@@ -116,20 +166,23 @@ msgstr "Email-адрес отправителя."
msgid "Enable"
msgstr "Включить"
msgid "Enable logging"
msgstr "Включить запись событий в лог"
msgid "Enable public-ip-script"
msgstr "Включить public-ip-script"
msgid "Enabled"
msgstr "Включен"
msgid "Error"
msgstr "Ошибка"
msgid "Error!"
msgstr "Ошибка!"
msgid "Expecting:"
msgstr "Ожидается:"
msgid "Public IP"
msgstr "Публичный IP"
msgid "Public IP address"
msgstr "Публичный IP адрес"
msgid "Failed interval"
msgstr "Интервал при неудаче"
msgid "Failed to get %s init status: %s"
msgstr "Не удалось получить статус инициализации %s: %s"
@@ -153,14 +206,14 @@ msgid "Hosts"
msgstr "Хосты"
msgid "Hosts polling interval when the Internet is down."
msgstr "Интервал опроса хостов если Интернет не доступен."
msgstr "Интервал опроса хостов если Интернет недоступен."
msgid "Hosts polling interval when the Internet is up."
msgstr "Интервал опроса хостов если Интернет доступен."
msgid ""
"Hosts to check Internet availability. Hosts are polled (in list order) until "
"at least one of them responds."
"Hosts for checking Internet availability. Hosts are polled (in list "
"order) until at least one responds."
msgstr ""
"Хосты для проверки доступности Интернет. Хосты опрашиваются (в порядке "
"списка) до тех пор, пока хотя бы один из них не ответит."
@@ -168,9 +221,30 @@ msgstr ""
msgid "Huge: 1492 bytes"
msgstr "Огромный: 1492 байта"
msgid "ID of the Telegram chat to which messages will be sent."
msgstr "ID чата Telegram в который будут отправлены сообщения."
msgid "ICMP-echo request (ping)"
msgstr "Запрос ICMP-echo (ping)"
msgid "ICMP packet data size"
msgstr "Размер данных ICMP-пакета"
msgid "Incorrect bot token?"
msgstr "Неправильный токен бота?"
msgid "Info"
msgstr "Информация"
msgid "Instances"
msgstr "Экземпляры"
msgid "Interface"
msgstr "Интерфейс"
msgid "Interface timeout"
msgstr "Таймаут интерфейса"
msgid "Internet"
msgstr "Интернет"
@@ -186,47 +260,41 @@ msgstr "Статус Интернет"
msgid "Interval between IP address requests."
msgstr "Интервал между запросами IP адреса."
msgid "Interval between IP address requests if the IP address is not defined."
msgstr "Интервал между запросами IP адреса, если IP адрес не определён."
msgid "Interval between modem restarts."
msgstr "Интервал между перезапусками модема."
msgid "Interval between network restarts."
msgstr "Интервал между перезапусками сети."
msgid "Interval between down-script runs."
msgstr "Интервал между запусками down-script."
msgid "Interval between up-script runs."
msgstr "Интервал между запусками up-script."
msgid "Invalid instance name..."
msgstr "Недопустимое имя экземпляра..."
msgid "Jumbo: 9000 bytes"
msgstr "Гигантский: 9000 байт"
msgid "LED control"
msgstr "Управление LED"
msgid "Link On"
msgstr "Подключение"
msgid "Loading"
msgstr "Загрузка"
msgid ""
"Longest period of time after connecting to Internet before \"up-script\" "
"runs."
msgstr ""
"Максимальный промежуток времени после подключения к Интернет перед запуском "
"\"up-script\"."
msgid "Log event level."
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 ""
"Максимальный промежуток времени после отключения Интернет перед запуском "
"\"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 "Logging"
msgstr "Запись событий в лог"
msgid "Mailsend is not available..."
msgstr "Mailsend недоступен..."
@@ -244,15 +312,26 @@ msgstr ""
"Максимальное количество попыток перезапуска сети до появления доступа в "
"Интренет."
msgid ""
"Maximum number of up-script run attempts when connected to the Internet."
msgstr ""
"Максимальное количество попыток запуска up-script при подключении к Интренет."
msgid ""
"Maximum number of down-script run attempts before Internet access is available."
msgstr ""
"Максимальное количество попыток запуска down-script до появления доступа в "
"Интренет."
msgid "Maximum timeout for waiting for a response from the host."
msgstr "Максимальный таймаут ожидания ответа от хоста."
msgid "Modem will be restarted when the Internet is disconnected."
msgstr "Модем будет перезапущен при отключении Интернет."
msgid "ModemManager is not available..."
msgstr "ModemManager недоступен..."
msgid "Modem will be restarted when the Internet is disconnected."
msgstr "Модем будет перезапущен при отключении Интернет."
msgid ""
"ModemManger interface. If specified, it will be restarted after restarting "
"ModemManager."
@@ -261,24 +340,63 @@ msgstr ""
"ModemManger."
msgid ""
"Network interface for Internet access. If not specified, the default "
"interface is used."
"Network device for Internet access. If not specified, the default "
"device is used."
msgstr ""
"Сетевой интерфейс для доступа в Интернет. Если не указан, используется "
"интерфейс по умолчанию."
"Сетевое устройство для доступа в Интернет. Если не указано, используется "
"устройство по умолчанию."
msgid "Network device activity (kernel: netdev)"
msgstr "Активность сетевого устройства (kernel: netdev)"
msgid ""
"Network interface to restart. If not specified, then the network service is restarted."
"Network device or interface to restart. If not specified, then the network service is restarted."
msgstr ""
"Сетевой интерфейс для перезапуска. Если не задан, то будет перезапущена сетевая "
"Сетевое устройство или интерфейс для перезапуска. Если не задано, то будет перезапущена сетевая "
"служба."
msgid "Network will be restarted when the Internet is disconnected."
msgstr "Сеть будет перезапущена при отключении Интернет."
msgid "Next run"
msgstr "Следующий запуск"
msgid "No <abbr title=\"Light Emitting Diode\">LED</abbr>s available..."
msgstr "Нет доступных <abbr title=\"Светодиод\">LED</abbr>..."
msgid "No messages available. Write something to the bot and try again."
msgstr "Нет доступных сообщений. Напишите что-нибудь боту и попробуйте ещё раз."
msgid "Not scheduled"
msgstr "Не запланирован"
msgid "Notice"
msgstr "Сообщение"
msgid "Number of attempts to request an IP address."
msgstr "Количество попыток запроса IP адреса"
msgid "Off"
msgstr "Выключить"
msgid "Off-state delay"
msgstr "Задержка выключенного состояния"
msgid "Off-state delay for blinking option."
msgstr "Задержка выключенного состояния для опции мигания."
msgid "On"
msgstr "Включить"
msgid "On startup"
msgstr "При запуске"
msgid "On-state delay"
msgstr "Задержка включённого состояния"
msgid "On-state delay for blinking option."
msgstr "Задержка включённого состояния для опции мигания."
msgid "One of the following:"
msgstr "Одно из следующих значений:"
@@ -289,27 +407,73 @@ msgid "Password for SMTP authentication."
msgstr "Пароль для SMTP-аутентификации."
msgid ""
"Performing actions when connecting and disconnecting the Internet (available "
"in the \"Service\" mode)."
"Period of time after connecting to Internet before up-script runs."
msgstr ""
"Выполнение действий при подключении и отключении Интернет (доступно в режиме "
"\"Служба\")."
"Период времени после подключения к Интернет перед запуском up-script."
msgid "Ping host"
msgstr "Пинг хоста"
msgid ""
"Period of time after connecting to the Internet before sending a message."
msgstr "Период времени после подключения к Интернет перед отправкой сообщения."
msgid "Ping packet size"
msgstr "Размер пакета Ping"
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 "Regular script"
msgstr "Регулярный скрипт"
msgid "Polling interval"
msgstr "Интервал опроса"
msgid "Provider"
msgstr "Провайдер"
msgid "Proxy"
msgstr "Прокси"
msgid "Proxy host"
msgstr "Хост прокси"
msgid "Proxy port"
msgstr "Порт прокси"
msgid "Public IP"
msgstr "Публичный IP"
msgid "Public IP address"
msgstr "Публичный IP адрес"
msgid "Reboot device"
msgstr "Перезагрузка устройства"
msgid "Reboot device if the Internet is disconnected at service startup."
msgstr "Перезагрузка устройства если Интренет отключен при запуске службы."
msgid "Receive"
msgstr "Приём"
msgid "Recipient"
msgstr "Получатель"
msgid "Request chat ID from bot API"
msgstr "Запросить ID чата через API бота"
msgid "Restart"
msgstr "Перезапуск"
@@ -319,18 +483,36 @@ msgstr "Попытки перезапуска"
msgid "Restart modem"
msgstr "Перезапуск модема"
msgid "Restart modem if the Internet is disconnected at service startup."
msgstr "Перезапуск модема если Интренет отключен при запуске службы."
msgid "Restart network"
msgstr "Перезапуск сети"
msgid "Restart network if the Internet is disconnected at service startup."
msgstr "Перезапуск сети если Интренет отключен при запуске службы."
msgid "Restart service"
msgstr "Перезапуск службы"
msgid "Restart timeout"
msgstr "Таймаут перезапуска"
msgid "Device timeout"
msgstr "Таймаут устройства"
msgid "Run interval"
msgstr "Интервал запуска"
msgid "Run if Internet state is"
msgstr "Выполнять если статус Интернет"
msgid "Run service at startup"
msgstr "Запуск службы при старте"
msgid "Run down-script if the Internet is disconnected at service startup."
msgstr "Выполнить down-script если Интренет отключен при запуске службы."
msgid "Run up-script if the Internet is connected at service startup."
msgstr "Выполнить up-script если Интренет подключен при запуске службы."
msgid "Running"
msgstr "Выполняется"
@@ -349,6 +531,9 @@ msgstr "Сохранить"
msgid "Security"
msgstr "Безопасность"
msgid "Send message on service startup."
msgstr "Отправлять сообщение при запуске службы."
msgid "Sender"
msgstr "Отправитель"
@@ -364,11 +549,8 @@ msgstr "Не удалось выполнить действие службы \"%
msgid "Service configuration"
msgstr "Конфигурация службы"
msgid "Service for determining the public IP address through DNS."
msgstr "Сервис для определения публичного IP адреса через DNS."
msgid "Service modules"
msgstr "Модули службы"
msgid "Service for determining the public IP address."
msgstr "Сервис для определения публичного IP адреса."
msgid "Service: detector always runs as a system service."
msgstr "Служба: детектор работает постоянно, как системная служба."
@@ -376,9 +558,18 @@ msgstr "Служба: детектор работает постоянно, ка
msgid "Set the modem to be allowed to use any band."
msgstr "Разрешить модему использование любой частоты."
msgid "Shell commands that are run regularly."
msgstr "Команды shell выполняемые регулярно."
msgid "Shell commands that run when connected to the Internet."
msgstr "Команды shell выполняемые при подключении к Интернет."
msgid "Shell commands that run regularly at a specified interval. Current state of the Internet is available as value of the <code>$INET_STATE</code> variable (<code>0</code> - connected, <code>1</code> - disconnected)."
msgstr "Команды shell выполняемые регулярно с заданным интервалом. Текущее состояние Интернет доступно как значение переменной <code>$INET_STATE</code> (<code>0</code> - подключен, <code>1</code> - отключен)."
msgid "Shell commands that run when the public IP address changes. New IP is available as value of the <code>$PUBLIC_IP</code> variable (empty string if undefined)."
msgstr "Команды shell выполняемые при изменении публичного IP адреса. Новый IP доступен как значение переменной <code>$PUBLIC_IP</code> (пустая строка если не определён)."
msgid "Shell commands to run when connected or disconnected from the Internet."
msgstr "Команды shell выполняемые при подключении или отключении Интернет."
@@ -400,18 +591,36 @@ msgstr "TCP-порт"
msgid "TCP port connection"
msgstr "Подключение к TCP-порту"
msgid "Telegram bot API token."
msgstr "API токен Telegram бота."
msgid "Telegram message will be sent when connected or disconnected from the Internet."
msgstr "Сообщение в Telegram будет отправлено при подключении или отключении от Интернет."
msgid "The type of record requested in the DNS query (if the service supports it)."
msgstr "Тип записи запрашиваемой в DNS-запросе (если сервис поддерживает)."
msgid "TLS: use STARTTLS if the server supports it."
msgstr "TLS: использовать STARTTLS если сервер поддерживает."
msgid "Timeout between stopping and starting the interface."
msgstr "Таймаут между остановкой и запуском интерфейса."
msgid "Timeout between stopping and starting a network device when restarting."
msgstr "Таймаут между остановкой и запуском сетевого устройства при перезапуске."
msgid "Timeout between stopping and starting a ModemManger interface when restarting."
msgstr "Таймаут между остановкой и запуском интерфейса ModemManger при перезапуске."
msgid "To support HTTP services, you need to install curl."
msgstr "Для поддержки HTTP сервисов необходимо установить curl."
msgid "To support URL checking, you need to install curl."
msgstr "Для поддержки проверки URL необходимо установить curl."
msgid "Transmit"
msgstr "Передача"
msgid "Type a time string"
msgstr "Введите строку времени"
msgid "UI detector configuration"
msgstr "Конфигурация UI детектора"
msgid "Unable to read the contents"
msgstr "Невозможно прочитать содержимое"
@@ -424,6 +633,16 @@ msgstr "Неопределён"
msgid "Unlock modem bands"
msgstr "Освободить частоты модема"
msgid "URL test (HTTP)"
msgstr "Тест URL (HTTP)"
msgid ""
"URLs for checking Internet availability. URLs are polled (in list "
"order) until at least one returns HTTP status code 200."
msgstr ""
"URL для проверки доступности Интернет. URL опрашиваются (в порядке "
"списка) до тех пор, пока хотя бы один из них не вернёт код статуса HTTP 200."
msgid "User"
msgstr "Пользователь"
@@ -438,19 +657,51 @@ msgstr ""
"Ожидание завершения перезагрузки перед выполнением принудительной "
"перезагрузки."
msgid "Web UI only"
msgstr "Только web-интерфейс"
msgid "Warning"
msgstr "Внимание"
msgid "Web UI only (UI detector)"
msgstr "Только web-интерфейс (UI детектор)"
msgid "Web UI only: detector works only when the Web UI is open (UI detector)."
msgstr ""
"Только web-интерфейс: детектор работает только в web-интерфейсе (UI "
"детектор)."
msgid "When email will be sent"
msgstr "Когда будет отправлено сообщение"
msgid "When message will be sent"
msgstr "Когда будет отправлено сообщение"
msgid "Windows: 32 bytes"
msgstr "Windows: 32 байта"
msgid "Write messages to the system log."
msgstr "Записывать сообщения в системный журнал."
msgid ""
"You need to register a new %sTelegram bot%s. Then get the bot's API token and paste it into the <code>Bot token</code> field. After that, open a chat with the bot, write something (in the Telegram app) and you will be able to get the chat ID using the <code>ID</code> button."
msgstr ""
"Необходимо зарегистрировать новый %sTelegram бот%s. Затем получить API токен бота и вставить его в поле <code>Токен бота</code>. После этого, откройте чат с ботом, напишите что-нибудь (в приложении Telegram) и можно будет получить ID чата с помощью кнопки <code>ID</code>."
msgid "after connection"
msgstr "после подключения"
msgid "after connection or disconnection"
msgstr "после подключения или отключения"
msgid "after disconnection"
msgstr "после отключения"
msgid "connected"
msgstr "подключен"
msgid "connected or disconnected"
msgstr "подключен или отключен"
msgid "days"
msgstr "дни"
msgid "disconnected"
msgstr "отключен"
msgid "down-script"
msgstr "down-script"
@@ -458,6 +709,9 @@ msgstr "down-script"
msgid "hour"
msgstr "час"
msgid "infinitely"
msgstr "бесконечно"
msgid "hours"
msgstr "часы"
@@ -467,6 +721,9 @@ msgstr "мин"
msgid "minutes"
msgstr "минуты"
msgid "msec"
msgstr "мсек"
msgid "sec"
msgstr "сек"

View File

@@ -8,8 +8,23 @@ msgid "<abbr title=\"Light Emitting Diode\">LED</abbr> control"
msgstr ""
msgid ""
"<abbr title=\"Light Emitting Diode\">LED</abbr> is on when Internet is "
"available."
"<abbr title=\"Light Emitting Diode\">LED</abbr> indicates the Internet status."
msgstr ""
msgid "<abbr title=\"Light Emitting Diode\">LED</abbr> mode"
msgstr ""
msgid ""
"<abbr title=\"Light Emitting Diode\">LED</abbr> will display the link activity of this network device."
msgstr ""
msgid "Add instance"
msgstr ""
msgid "After connection"
msgstr ""
msgid "After disconnection"
msgstr ""
msgid "Alive interval"
@@ -18,17 +33,33 @@ 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"
msgstr ""
msgid "Attempt interval"
msgstr ""
msgid "Attempts"
msgstr ""
msgid "Big: 248 bytes"
msgstr ""
msgid "Blinking (kernel: timer)"
msgstr ""
msgid "Bot API token is missing!"
msgstr ""
msgid "Bot token"
msgstr ""
msgid "Chat ID"
msgstr ""
msgid "Check type"
msgstr ""
@@ -53,15 +84,27 @@ msgstr ""
msgid "Contents have been saved."
msgstr ""
msgid "Curl is not available..."
msgstr ""
msgid "Dead interval"
msgstr ""
msgid "Dead period"
msgstr ""
msgid "Debug"
msgstr ""
msgid "Default port value for TCP connections."
msgstr ""
msgid "Description"
msgstr ""
msgid "Device"
msgstr ""
msgid "Device will be rebooted when the Internet is disconnected."
msgstr ""
@@ -80,7 +123,7 @@ msgstr ""
msgid "Dismiss"
msgstr ""
msgid "DNS provider"
msgid "DNS query type"
msgstr ""
msgid "Edit"
@@ -89,6 +132,12 @@ msgstr ""
msgid "Edit down-script"
msgstr ""
msgid "Edit regular-script"
msgstr ""
msgid "Edit public-ip-script"
msgstr ""
msgid "Edit up-script"
msgstr ""
@@ -104,19 +153,22 @@ msgstr ""
msgid "Enable"
msgstr ""
msgid "Enable logging"
msgid "Enable public-ip-script"
msgstr ""
msgid "Enabled"
msgstr ""
msgid "Error"
msgstr ""
msgid "Error!"
msgstr ""
msgid "Expecting:"
msgstr ""
msgid "Public IP"
msgstr ""
msgid "Public IP address"
msgid "Failed interval"
msgstr ""
msgid "Failed to get %s init status: %s"
@@ -147,16 +199,37 @@ msgid "Hosts polling interval when the Internet is up."
msgstr ""
msgid ""
"Hosts to check Internet availability. Hosts are polled (in list order) until "
"at least one of them responds."
"Hosts for checking Internet availability. Hosts are polled (in list "
"order) until at least one responds."
msgstr ""
msgid "Huge: 1492 bytes"
msgstr ""
msgid "ID of the Telegram chat to which messages will be sent."
msgstr ""
msgid "ICMP-echo request (ping)"
msgstr ""
msgid "ICMP packet data size"
msgstr ""
msgid "Incorrect bot token?"
msgstr ""
msgid "Info"
msgstr ""
msgid "Instances"
msgstr ""
msgid "Interface"
msgstr ""
msgid "Interface timeout"
msgstr ""
msgid "Internet"
msgstr ""
@@ -172,38 +245,40 @@ msgstr ""
msgid "Interval between IP address requests."
msgstr ""
msgid "Interval between IP address requests if the IP address is not defined."
msgstr ""
msgid "Interval between modem restarts."
msgstr ""
msgid "Interval between network restarts."
msgstr ""
msgid "Interval between down-script runs."
msgstr ""
msgid "Interval between up-script runs."
msgstr ""
msgid "Invalid instance name..."
msgstr ""
msgid "Jumbo: 9000 bytes"
msgstr ""
msgid "LED control"
msgstr ""
msgid "Link On"
msgstr ""
msgid "Loading"
msgstr ""
msgid ""
"Longest period of time after connecting to Internet before \"up-script\" "
"runs."
msgid "Log event level."
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."
msgid "Logging"
msgstr ""
msgid "Mailsend is not available..."
@@ -220,13 +295,21 @@ msgid ""
"available."
msgstr ""
msgid ""
"Maximum number of up-script run attempts when connected to the Internet."
msgstr ""
msgid ""
"Maximum number of down-script run attempts before Internet access is available."
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 ""
@@ -235,20 +318,59 @@ msgid ""
msgstr ""
msgid ""
"Network interface for Internet access. If not specified, the default "
"Network device for Internet access. If not specified, the default "
"interface is used."
msgstr ""
msgid "Network device activity (kernel: netdev)"
msgstr ""
msgid ""
"Network interface to restart. If not specified, then the network service is restarted."
"Network device or interface to restart. If not specified, then the network service is restarted."
msgstr ""
msgid "Network will be restarted when the Internet is disconnected."
msgstr ""
msgid "Next run"
msgstr ""
msgid "No <abbr title=\"Light Emitting Diode\">LED</abbr>s available..."
msgstr ""
msgid "No messages available. Write something to the bot and try again."
msgstr ""
msgid "Not scheduled"
msgstr ""
msgid "Notice"
msgstr ""
msgid "Number of attempts to request an IP address."
msgstr ""
msgid "Off"
msgstr ""
msgid "Off-state delay"
msgstr ""
msgid "Off-state delay for blinking option."
msgstr ""
msgid "On"
msgstr ""
msgid "On startup"
msgstr ""
msgid "On-state delay"
msgstr ""
msgid "On-state delay for blinking option."
msgstr ""
msgid "One of the following:"
msgstr ""
@@ -259,25 +381,70 @@ msgid "Password for SMTP authentication."
msgstr ""
msgid ""
"Performing actions when connecting and disconnecting the Internet (available "
"in the \"Service\" mode)."
"Period of time after connecting to Internet before up-script runs."
msgstr ""
msgid "Ping host"
msgid ""
"Period of time after connecting to the Internet before sending a message."
msgstr ""
msgid "Ping packet size"
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 "Regular script"
msgstr ""
msgid "Polling interval"
msgstr ""
msgid "Provider"
msgstr ""
msgid "Proxy"
msgstr ""
msgid "Proxy host"
msgstr ""
msgid "Proxy port"
msgstr ""
msgid "Public IP"
msgstr ""
msgid "Public IP address"
msgstr ""
msgid "Reboot device"
msgstr ""
msgid "Reboot device if the Internet is disconnected at service startup."
msgstr ""
msgid "Receive"
msgstr ""
msgid "Recipient"
msgstr ""
msgid "Request chat ID from bot API"
msgstr ""
msgid "Restart"
msgstr ""
@@ -287,18 +454,36 @@ msgstr ""
msgid "Restart modem"
msgstr ""
msgid "Restart modem if the Internet is disconnected at service startup."
msgstr ""
msgid "Restart network"
msgstr ""
msgid "Restart network if the Internet is disconnected at service startup."
msgstr ""
msgid "Restart service"
msgstr ""
msgid "Restart timeout"
msgid "Device timeout"
msgstr ""
msgid "Run interval"
msgstr ""
msgid "Run if Internet state is"
msgstr ""
msgid "Run service at startup"
msgstr ""
msgid "Run down-script if the Internet is disconnected at service startup."
msgstr ""
msgid "Run up-script if the Internet is connected at service startup."
msgstr ""
msgid "Running"
msgstr ""
@@ -317,6 +502,9 @@ msgstr ""
msgid "Security"
msgstr ""
msgid "Send message on service startup."
msgstr ""
msgid "Sender"
msgstr ""
@@ -332,10 +520,7 @@ msgstr ""
msgid "Service configuration"
msgstr ""
msgid "Service for determining the public IP address through DNS."
msgstr ""
msgid "Service modules"
msgid "Service for determining the public IP address."
msgstr ""
msgid "Service: detector always runs as a system service."
@@ -344,9 +529,18 @@ msgstr ""
msgid "Set the modem to be allowed to use any band."
msgstr ""
msgid "Shell commands that are run regularly."
msgstr ""
msgid "Shell commands that run when connected to the Internet."
msgstr ""
msgid "Shell commands that run regularly at a specified interval. Current state of the Internet is available as value of the <code>$INET_STATE</code> variable (<code>0</code> - connected, <code>1</code> - disconnected)."
msgstr ""
msgid "Shell commands that run when the public IP address changes. New IP is available as value of the <code>$PUBLIC_IP</code> variable (empty string if undefined)."
msgstr ""
msgid "Shell commands to run when connected or disconnected from the Internet."
msgstr ""
@@ -368,18 +562,36 @@ msgstr ""
msgid "TCP port connection"
msgstr ""
msgid "Telegram bot API token."
msgstr ""
msgid "Telegram message will be sent when connected or disconnected from the Internet."
msgstr ""
msgid "The type of record requested in the DNS query (if the service supports it)."
msgstr ""
msgid "TLS: use STARTTLS if the server supports it."
msgstr ""
msgid "Timeout between stopping and starting the interface."
msgid "Timeout between stopping and starting a network device when restarting."
msgstr ""
msgid "Timeout between stopping and starting a ModemManger interface when restarting."
msgstr ""
msgid "To support HTTP services, you need to install curl."
msgstr ""
msgid "To support URL checking, you need to install curl."
msgstr ""
msgid "Transmit"
msgstr ""
msgid "Type a time string"
msgstr ""
msgid "UI detector configuration"
msgstr ""
msgid "Unable to read the contents"
msgstr ""
@@ -392,6 +604,14 @@ msgstr ""
msgid "Unlock modem bands"
msgstr ""
msgid "URL test (HTTP)"
msgstr ""
msgid ""
"URLs for checking Internet availability. URLs are polled (in list "
"order) until at least one returns HTTP status code 200."
msgstr ""
msgid "User"
msgstr ""
@@ -404,16 +624,44 @@ msgstr ""
msgid "Waiting for a reboot to complete before performing a forced reboot."
msgstr ""
msgid "Web UI only"
msgid "Warning"
msgstr ""
msgid "Web UI only (UI detector)"
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 "When message will be sent"
msgstr ""
msgid "Windows: 32 bytes"
msgstr ""
msgid "Write messages to the system log."
msgid ""
"You need to register a new %sTelegram bot%s. Then get the bot's API token and paste it into the <code>Bot token</code> field. After that, open a chat with the bot, write something (in the Telegram app) and you will be able to get the chat ID using the <code>ID</code> button."
msgstr ""
msgid "after connection"
msgstr ""
msgid "after disconnection"
msgstr ""
msgid "connected"
msgstr ""
msgid "connected or disconnected"
msgstr ""
msgid "days"
msgstr ""
msgid "disconnected"
msgstr ""
msgid "down-script"
@@ -425,12 +673,18 @@ msgstr ""
msgid "hours"
msgstr ""
msgid "infinitely"
msgstr ""
msgid "min"
msgstr ""
msgid "minutes"
msgstr ""
msgid "msec"
msgstr ""
msgid "sec"
msgstr ""

View File

@@ -0,0 +1,97 @@
#!/usr/bin/env lua
local appName = "internet-detector"
local appExec = "/usr/bin/internet-detector"
local mailsendExec = "/usr/bin/mailsend"
local modemManagerInit = "/etc/init.d/modemmanager"
local curlExec = "/usr/bin/curl"
local InternetDetector = require(appName .. ".main")
local uci = require("uci")
local unistd = require("posix.unistd")
local function prequire(package)
local retVal, pkg = pcall(require, package)
return retVal and pkg
end
local function init()
local lines = {}
if prequire(appName .. ".modules.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 .. ".modules.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
if prequire(appName .. ".modules.mod_telegram") then
lines[#lines + 1] = '"telegram":true'
else
lines[#lines + 1] = '"telegram":false'
end
if unistd.access(curlExec, "x") then
lines[#lines + 1] = '"curl_exec":true'
else
lines[#lines + 1] = '"curl_exec":false'
end
return string.format("{%s}", table.concat(lines, ","))
end
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 -a daemon -i %s", appExec, s[".name"]))
end
end
)
end
end
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)

View File

@@ -4,20 +4,23 @@
"read": {
"file": {
"/sys/class/leds": [ "list" ],
"/etc/internet-detector/up-script": [ "read" ],
"/etc/internet-detector/down-script": [ "read" ],
"/usr/bin/internet-detector*": [ "exec" ],
"/usr/bin/mailsend": [ "exec" ]
"/etc/internet-detector/up-script*": [ "read" ],
"/etc/internet-detector/down-script*": [ "read" ],
"/etc/internet-detector/public-ip-script*": [ "read" ],
"/etc/internet-detector/regular-script*": [ "read" ]
},
"uci": [ "internet-detector" ],
"ubus": {
"luci": [ "getInitList", "setInitAction" ]
"luci": [ "getInitList", "setInitAction" ],
"luci.internet-detector": [ "Init", "Status", "InetStatus", "UIPoll" ]
}
},
"write": {
"file": {
"/etc/internet-detector/up-script": [ "write" ],
"/etc/internet-detector/down-script": [ "write" ]
"/etc/internet-detector/up-script*": [ "write" ],
"/etc/internet-detector/down-script*": [ "write" ],
"/etc/internet-detector/public-ip-script*": [ "write" ],
"/etc/internet-detector/regular-script*": [ "write" ]
},
"uci": [ "internet-detector" ]
}

Binary file not shown.

Before

Width:  |  Height:  |  Size: 233 KiB

After

Width:  |  Height:  |  Size: 120 KiB

Binary file not shown.

Before

Width:  |  Height:  |  Size: 65 KiB

After

Width:  |  Height:  |  Size: 129 KiB

BIN
screenshots/03.jpg Normal file

Binary file not shown.

After

Width:  |  Height:  |  Size: 44 KiB

BIN
screenshots/04.jpg Normal file

Binary file not shown.

After

Width:  |  Height:  |  Size: 101 KiB

BIN
screenshots/05.jpg Normal file

Binary file not shown.

After

Width:  |  Height:  |  Size: 146 KiB

BIN
screenshots/06.jpg Normal file

Binary file not shown.

After

Width:  |  Height:  |  Size: 166 KiB